///|
fn make_render_context(
viewport : @types.Size[Double],
) -> @renderer.RenderContext {
{
..@renderer.RenderContext::default(),
viewport_width: viewport.width,
viewport_height: viewport.height,
}
}
///|
fn paint_tree_from_node_layout(
node : @layout.Node,
layout : @layout_types.Layout,
viewport : @types.Size[Double],
) -> @paint_model.PaintNode {
match
@paint_render.from_node_and_layout_with_viewport_rect(
node,
layout,
0.0,
0.0,
viewport.width,
viewport.height,
0.0,
0.0,
) {
Some(paint_tree) => paint_tree
None => @paint_render.from_node_and_layout(node, layout)
}
}
///|
pub(all) struct PreparedVrtPage {
doc : @html.Document
viewport : @types.Size[Double]
ctx : @renderer.RenderContext
prepared : @renderer.PreparedRenderDocument
}
///|
pub fn prepare_external_css(
external_css : Array[String],
) -> @renderer.PreparedExternalCss {
@renderer.prepare_external_css(external_css)
}
///|
pub fn prepare_vrt_page(
html : String,
viewport : @types.Size[Double],
external_css : Array[String],
) -> PreparedVrtPage {
let doc = @html.parse_document(html)
let ctx = make_render_context(viewport)
{
doc,
viewport,
ctx,
prepared: @renderer.prepare_render_document(doc, ctx, external_css),
}
}
///|
pub fn prepare_vrt_page_with_prepared_external_css(
html : String,
viewport : @types.Size[Double],
external_css : @renderer.PreparedExternalCss,
) -> PreparedVrtPage {
let doc = @html.parse_document(html)
let ctx = make_render_context(viewport)
{
doc,
viewport,
ctx,
prepared: @renderer.prepare_render_document_with_prepared_external_css(
doc, ctx, external_css,
),
}
}
///|
pub fn render_prepared_vrt_page_to_paint_tree(
page : PreparedVrtPage,
) -> @paint_model.PaintNode {
let node = @renderer.build_render_root_node(page.doc, page.ctx, page.prepared)
let layout = @renderer.compute_layout_from_render_root(
node,
page.prepared,
page.ctx,
)
paint_tree_from_node_layout(node, layout, page.viewport)
}
///|
pub fn render_prepared_vrt_page_to_paint_tree_json(
page : PreparedVrtPage,
) -> String {
render_prepared_vrt_page_to_paint_tree(page).to_json_string()
}
///|
pub fn render_html_to_paint_tree(
html : String,
viewport : @types.Size[Double],
) -> @paint_model.PaintNode {
render_html_to_paint_tree_with_external_css(html, viewport, [])
}
///|
pub fn render_html_to_paint_tree_with_external_css(
html : String,
viewport : @types.Size[Double],
external_css : Array[String],
) -> @paint_model.PaintNode {
let (node, layout) = @renderer.render_to_node_and_layout_with_external_css(
html,
make_render_context(viewport),
external_css,
)
paint_tree_from_node_layout(node, layout, viewport)
}
///|
pub fn render_html_to_paint_tree_with_prepared_external_css(
html : String,
viewport : @types.Size[Double],
external_css : @renderer.PreparedExternalCss,
) -> @paint_model.PaintNode {
let doc = @html.parse_document(html)
let (node, layout) = @renderer.render_to_node_and_layout_with_prepared_external_css(
doc,
make_render_context(viewport),
external_css,
)
paint_tree_from_node_layout(node, layout, viewport)
}
///|
pub fn render_html_to_paint_tree_json(
html : String,
viewport : @types.Size[Double],
) -> String {
render_html_to_paint_tree(html, viewport).to_json_string()
}
///|
pub fn diff_rendered_paint_trees(
baseline_html : String,
current_html : String,
viewport : @types.Size[Double],
) -> @paint_diff.PaintTreeDiff {
let baseline = render_html_to_paint_tree(baseline_html, viewport)
let current = render_html_to_paint_tree(current_html, viewport)
@paint_diff.diff_trees(baseline, current)
}
///|
pub(all) struct CssMutation {
selector : String
property : String
action : CssMutationAction
} derive(Eq, Debug)
///|
pub(all) enum CssMutationAction {
Remove
Override(String)
} derive(Eq, Debug)
///|
pub(all) struct RenderVariant {
id : String
mutations : Array[CssMutation]
} derive(Eq, Debug)
///|
pub(all) struct RenderVariantResult {
id : String
paint_tree : @paint_model.PaintNode
}
///|
pub impl Show for CssMutation with fn output(self, logger) {
logger.write_string("{selector: ")
logger.write_string("\"")
logger.write_string(self.selector)
logger.write_string("\"")
logger.write_string(", property: ")
logger.write_string("\"")
logger.write_string(self.property)
logger.write_string("\"")
logger.write_string(", action: \{self.action}}")
}
///|
pub impl Show for CssMutationAction with fn output(self, logger) {
match self {
Remove => logger.write_string("Remove")
Override(value) => {
logger.write_string("Override(")
logger.write_string("\"")
logger.write_string(value)
logger.write_string("\"")
logger.write_string(")")
}
}
}
///|
pub impl Show for RenderVariant with fn output(self, logger) {
logger.write_string("{id: ")
logger.write_string("\"")
logger.write_string(self.id)
logger.write_string("\"")
logger.write_string(", mutations: ")
logger.write_object(to_repr(self.mutations))
logger.write_string("}")
}
///|
pub impl Show for RenderVariantResult with fn output(self, logger) {
logger.write_string("{id: ")
logger.write_string("\"")
logger.write_string(self.id)
logger.write_string("\"")
logger.write_string(", paint_tree: \{self.paint_tree}}")
}
///|
pub fn render_html_batch_variants(
html : String,
viewport : @types.Size[Double],
variants : Array[RenderVariant],
) -> Array[RenderVariantResult] {
let doc = @html.parse_document(html)
let ctx = make_render_context(viewport)
let results : Array[RenderVariantResult] = []
for variant in variants {
let override_css = mutations_to_css(variant.mutations)
let external_css : Array[String] = if override_css.is_empty() {
[]
} else {
[override_css]
}
let (node, layout) = @renderer.render_to_node_and_layout_with_document(
doc, ctx, external_css,
)
let paint_tree = paint_tree_from_node_layout(node, layout, viewport)
results.push({ id: variant.id, paint_tree })
}
results
}
///|
fn mutations_to_css(mutations : Array[CssMutation]) -> String {
if mutations.is_empty() {
return ""
}
let by_selector : Map[String, Array[(String, String)]] = {}
for mutation in mutations {
let value = match mutation.action {
CssMutationAction::Remove => "unset"
CssMutationAction::Override(v) => v
}
match by_selector.get(mutation.selector) {
Some(props) => props.push((mutation.property, value))
None => by_selector.set(mutation.selector, [(mutation.property, value)])
}
}
let buf = StringBuilder::new()
by_selector.each(fn(selector, props) {
buf.write_string(selector)
buf.write_string(" { ")
for item in props {
buf.write_string(item.0)
buf.write_string(": ")
buf.write_string(item.1)
buf.write_string(" !important; ")
}
buf.write_string("} ")
})
buf.to_string()
}
///|
/// A reusable render "snapshot" for VRT branching. `open` parses + cascades +
/// lays out + paints the base document once; each branch applies a CSS-mutation
/// delta. A branch can either re-layout fully (branch_full, for geometry-
/// affecting changes) or reuse the base layout (branch_reusing_layout, valid
/// when the delta is paint-only) — the latter skips the dominant layout cost.
pub(all) struct RenderSession {
doc : @html.Document
viewport : @types.Size[Double]
ctx : @renderer.RenderContext
base_node : @layout.Node
base_layout : @layout_types.Layout
base_paint : @paint_model.PaintNode
// External side effects observed while rendering the base — text measurements
// and image intrinsic sizes. Stored so branches replay them (deterministic,
// no font/image engine) and so the whole session can be serialized and
// reproduced on a machine without those engines.
measurements : Array[@renderer.RecordedMetric]
images : Array[@renderer.RecordedImage]
}
///|
fn render_session_external_css(mutations : Array[CssMutation]) -> Array[String] {
let override_css = mutations_to_css(mutations)
if override_css.is_empty() {
[]
} else {
[override_css]
}
}
///|
/// Start recording all external render side effects (text + image).
fn session_start_recording() -> Unit {
@renderer.start_text_metrics_recording()
@renderer.start_image_intrinsic_recording()
}
///|
/// Replay all external render side effects (a miss falls back to live).
fn session_install_replay(
measurements : Array[@renderer.RecordedMetric],
images : Array[@renderer.RecordedImage],
) -> Unit {
@renderer.install_text_metrics_replay(measurements)
@renderer.install_image_intrinsic_replay(images)
}
///|
/// Disable record/replay for all external render side effects.
fn session_clear_record_replay() -> Unit {
@renderer.clear_text_metrics_record_replay()
@renderer.clear_image_intrinsic_record_replay()
}
///|
/// Parse + cascade + layout + paint the base document once, recording the
/// external text-measurement side effect so branches (and a restored session)
/// reproduce it without a font engine.
pub fn RenderSession::open(
html : String,
viewport : @types.Size[Double],
external_css : Array[String],
) -> RenderSession {
let doc = @html.parse_document(html)
let ctx = make_render_context(viewport)
session_start_recording()
let prepared = @renderer.prepare_render_document(doc, ctx, external_css)
let node = @renderer.build_render_root_node(doc, ctx, prepared)
let layout = @renderer.compute_layout_from_render_root(node, prepared, ctx)
let paint = paint_tree_from_node_layout(node, layout, viewport)
let measurements = @renderer.take_text_metrics_recording()
let images = @renderer.take_image_intrinsic_recording()
{
doc,
viewport,
ctx,
base_node: node,
base_layout: layout,
base_paint: paint,
measurements,
images,
}
}
///|
/// Restore a session from a previously recorded measurement set (e.g. one
/// serialized to disk). The base is rendered by replaying `measurements`, so no
/// font engine is required; branches then reuse the same recording. This is the
/// portable side of the snapshot: capture once with open(), persist
/// session.measurements, reproduce anywhere.
pub fn RenderSession::open_with_recording(
html : String,
viewport : @types.Size[Double],
external_css : Array[String],
measurements : Array[@renderer.RecordedMetric],
images : Array[@renderer.RecordedImage],
) -> RenderSession {
let doc = @html.parse_document(html)
let ctx = make_render_context(viewport)
session_install_replay(measurements, images)
let prepared = @renderer.prepare_render_document(doc, ctx, external_css)
let node = @renderer.build_render_root_node(doc, ctx, prepared)
let layout = @renderer.compute_layout_from_render_root(node, prepared, ctx)
let paint = paint_tree_from_node_layout(node, layout, viewport)
session_clear_record_replay()
{
doc,
viewport,
ctx,
base_node: node,
base_layout: layout,
base_paint: paint,
measurements,
images,
}
}
///|
/// The recorded text measurements; serialize with
/// @renderer.serialize_text_metrics_recording.
pub fn RenderSession::recorded_measurements(
self : RenderSession,
) -> Array[@renderer.RecordedMetric] {
self.measurements
}
///|
/// The recorded image intrinsic sizes; serialize with
/// @renderer.serialize_image_intrinsic_recording.
pub fn RenderSession::recorded_images(
self : RenderSession,
) -> Array[@renderer.RecordedImage] {
self.images
}
///|
/// The base (unmutated) paint tree captured at open().
pub fn RenderSession::base_paint_tree(
self : RenderSession,
) -> @paint_model.PaintNode {
self.base_paint
}
///|
/// Branch with a full re-layout (correct for any mutation, including ones that
/// change box geometry). Re-cascades + re-lays-out + paints from scratch,
/// reusing only the parsed document.
pub fn RenderSession::branch_full(
self : RenderSession,
mutations : Array[CssMutation],
) -> @paint_model.PaintNode {
let external = render_session_external_css(mutations)
// Replay the recorded side effects (a delta that introduces new text/sizes
// or images falls back to the live resolver for those misses).
session_install_replay(self.measurements, self.images)
let prepared = @renderer.prepare_render_document(self.doc, self.ctx, external)
let node = @renderer.build_render_root_node(self.doc, self.ctx, prepared)
let layout = @renderer.compute_layout_from_render_root(
node,
prepared,
self.ctx,
)
let paint = paint_tree_from_node_layout(node, layout, self.viewport)
session_clear_record_replay()
paint
}
///|
/// Branch reusing the base layout: re-cascade the mutated document but paint it
/// against the base layout, skipping re-layout. Valid only when the mutation is
/// paint-only (it changes paint properties — color, background, opacity, … —
/// not box geometry); for such a delta the layout is identical, so the painted
/// result equals branch_full at a fraction of the cost.
pub fn RenderSession::branch_reusing_layout(
self : RenderSession,
mutations : Array[CssMutation],
) -> @paint_model.PaintNode {
let external = render_session_external_css(mutations)
// A paint-only delta keeps the same text/sizes/images, so the recording is
// complete and no live resolution happens.
session_install_replay(self.measurements, self.images)
let prepared = @renderer.prepare_render_document(self.doc, self.ctx, external)
let node = @renderer.build_render_root_node(self.doc, self.ctx, prepared)
let paint = paint_tree_from_node_layout(node, self.base_layout, self.viewport)
session_clear_record_replay()
paint
}
///|
/// Paint-tree diff of a full-relayout branch against the base.
pub fn RenderSession::branch_full_diff(
self : RenderSession,
mutations : Array[CssMutation],
) -> @paint_diff.PaintTreeDiff {
@paint_diff.diff_trees(self.base_paint, self.branch_full(mutations))
}
///|
/// Paint-tree diff of a layout-reusing (paint-only) branch against the base.
pub fn RenderSession::branch_reusing_layout_diff(
self : RenderSession,
mutations : Array[CssMutation],
) -> @paint_diff.PaintTreeDiff {
@paint_diff.diff_trees(self.base_paint, self.branch_reusing_layout(mutations))
}
///|
/// Whether this mutation only affects paint (color/background/opacity/…), not
/// box geometry — i.e. whether a branch carrying only such mutations can reuse
/// the base layout.
pub fn CssMutation::is_paint_only(self : CssMutation) -> Bool {
not(@dom.is_layout_property(self.property.to_lower()))
}
///|
/// Branch with automatic strategy: if every mutation is paint-only the base
/// layout is reused (fast path); otherwise a full re-layout is performed. This
/// is the convenient default — correct for any delta, fast for recolor/theme
/// variants (the common VRT case).
pub fn RenderSession::branch(
self : RenderSession,
mutations : Array[CssMutation],
) -> @paint_model.PaintNode {
let all_paint_only = mutations.iter().all(fn(m) { m.is_paint_only() })
if all_paint_only {
self.branch_reusing_layout(mutations)
} else {
self.branch_full(mutations)
}
}
///|
/// Paint-tree diff of an auto-strategy branch against the base.
pub fn RenderSession::branch_diff(
self : RenderSession,
mutations : Array[CssMutation],
) -> @paint_diff.PaintTreeDiff {
@paint_diff.diff_trees(self.base_paint, self.branch(mutations))
}
///|
/// Absolute geometry of one laid-out box, in the form page JS needs for
/// `getBoundingClientRect()` / `offsetWidth` / `offsetHeight`. `index` is the
/// pre-order (document-order) position in the layout tree — a stable key a JS
/// DOM built in the same order can match. The crater layout tree already stores
/// absolute viewport coordinates, so x/y are left/top directly.
pub(all) struct LayoutBox {
id : String
index : Int
x : Double
y : Double
width : Double
height : Double
} derive(Eq, Show)
///|
fn collect_layout_boxes_into(
layout : @layout_types.Layout,
next_index : Ref[Int],
out : Array[LayoutBox],
) -> Unit {
let index = next_index.val
next_index.val = index + 1
out.push({
id: layout.id,
index,
x: layout.x,
y: layout.y,
width: layout.width,
height: layout.height,
})
for child in layout.children {
collect_layout_boxes_into(child, next_index, out)
}
}
///|
/// Flatten a layout tree into per-box absolute geometry in document order. This
/// is the data layer that connects crater's layout engine to page JS box
/// measurement (the bridge would inject these and have getBoundingClientRect /
/// offsetWidth / offsetHeight read them, keyed by `index`, instead of the
/// inline-style heuristic the runtime uses today).
pub fn collect_layout_boxes(layout : @layout_types.Layout) -> Array[LayoutBox] {
let out : Array[LayoutBox] = []
collect_layout_boxes_into(layout, { val: 0 }, out)
out
}
///|
/// The first box whose layout id contains `id_fragment` (e.g. an element id like
/// "main#content" or "#sidebar"), for getElementById-style measurement.
pub fn layout_box_by_id(
layout : @layout_types.Layout,
id_fragment : String,
) -> LayoutBox? {
let boxes = collect_layout_boxes(layout)
for b in boxes {
if b.id.contains(id_fragment) {
return Some(b)
}
}
None
}
///|
/// Per-box geometry of the session's base render — the geometry page JS would
/// observe via getBoundingClientRect on the initial layout.
pub fn RenderSession::element_boxes(self : RenderSession) -> Array[LayoutBox] {
collect_layout_boxes(self.base_layout)
}
///|
/// CSS string for a computed `display`, in the form `getComputedStyle` returns.
fn display_to_css(d : @types.Display) -> String {
match d {
@types.Display::Block => "block"
@types.Display::Inline => "inline"
@types.Display::InlineBlock => "inline-block"
@types.Display::Flex => "flex"
@types.Display::InlineFlex => "inline-flex"
@types.Display::Grid => "grid"
@types.Display::InlineGrid => "inline-grid"
@types.Display::Table => "table"
@types.Display::InlineTable => "inline-table"
@types.Display::TableRow => "table-row"
@types.Display::TableCell => "table-cell"
@types.Display::TableCaption => "table-caption"
@types.Display::TableRowGroup => "table-row-group"
@types.Display::TableHeaderGroup => "table-header-group"
@types.Display::TableFooterGroup => "table-footer-group"
@types.Display::TableColumn => "table-column"
@types.Display::TableColumnGroup => "table-column-group"
@types.Display::None => "none"
@types.Display::Contents => "contents"
@types.Display::FlowRoot => "flow-root"
}
}
///|
/// CSS string for a computed `position`.
fn position_to_css(p : @types.Position) -> String {
match p {
@types.Position::Static => "static"
@types.Position::Relative => "relative"
@types.Position::Absolute => "absolute"
@types.Position::Fixed => "fixed"
}
}
///|
/// CSS string for a computed `visibility`.
fn visibility_to_css(v : @style.Visibility) -> String {
match v {
@style.Visibility::Visible => "visible"
@style.Visibility::Hidden => "hidden"
@style.Visibility::Collapse => "collapse"
}
}
///|
/// CSS string for a computed `z-index` (`auto` or an integer).
fn z_index_to_css(z : @style.ZIndex) -> String {
match z {
@style.ZIndex::Auto => "auto"
@style.ZIndex::Value(n) => n.to_string()
}
}
///|
/// Format a Double as a CSS pixel length, dropping a trailing `.0` so an
/// integral 20.0 serializes as `20px` (matching getComputedStyle), not `20.0px`.
fn css_px(v : Double) -> String {
let i = v.to_int()
if i.to_double() == v {
"\{i}px"
} else {
"\{v}px"
}
}
///|
/// Format a unitless Double (e.g. opacity), dropping a trailing `.0`.
fn css_number(v : Double) -> String {
let i = v.to_int()
if i.to_double() == v {
"\{i}"
} else {
"\{v}"
}
}
///|
/// Resolved computed style of one element, in the form page JS `getComputedStyle`
/// reads. `index` is the pre-order (document-order) position in the render node
/// tree — the same key `collect_layout_boxes` assigns to the parallel layout
/// tree, so a JS DOM built in the same order can join geometry and computed
/// style by index. Values are serialized as CSS strings (display `"flex"`, color
/// `"rgb(255, 0, 0)"`, font-size `"20px"`). This is the node-tree analogue of
/// `collect_layout_boxes`: the data layer a getComputedStyle bridge would inject.
pub(all) struct ComputedStyleEntry {
id : String
index : Int
display : String
position : String
visibility : String
font_size : String
font_family : String
opacity : String
z_index : String
color : String
background_color : String
} derive(Eq, Show)
///|
fn collect_computed_styles_into(
node : @layout.Node,
next_index : Ref[Int],
out : Array[ComputedStyleEntry],
) -> Unit {
let index = next_index.val
next_index.val = index + 1
let s = node.style
out.push({
id: node.id,
index,
display: display_to_css(s.display),
position: position_to_css(s.position),
visibility: visibility_to_css(s.visibility),
font_size: css_px(s.font_size),
font_family: s.font_family,
opacity: css_number(s.opacity),
z_index: z_index_to_css(s.z_index),
color: s.color.to_rgba_string(),
background_color: s.background_color.to_rgba_string(),
})
for child in node.children {
collect_computed_styles_into(child, next_index, out)
}
}
///|
/// Flatten a render node tree into per-element computed style in document order.
/// This is the parallel of `collect_layout_boxes` for the page-JS
/// `getComputedStyle` bridge: the node tree carries the resolved `@style.Style`
/// per element, and the indexes line up with the layout-box indexes so the two
/// can be joined element-for-element.
pub fn collect_computed_styles(
node : @layout.Node,
) -> Array[ComputedStyleEntry] {
let out : Array[ComputedStyleEntry] = []
collect_computed_styles_into(node, { val: 0 }, out)
out
}
///|
/// The first element whose node id contains `id_fragment`, for
/// getComputedStyle-by-element lookup.
pub fn computed_style_by_id(
node : @layout.Node,
id_fragment : String,
) -> ComputedStyleEntry? {
let entries = collect_computed_styles(node)
for e in entries {
if e.id.contains(id_fragment) {
return Some(e)
}
}
None
}
///|
/// Per-element computed style of the session's base render — what page JS would
/// observe via getComputedStyle on the initial render.
pub fn RenderSession::element_computed_styles(
self : RenderSession,
) -> Array[ComputedStyleEntry] {
collect_computed_styles(self.base_node)
}
///|
/// Emit `s` as a double-quoted JS/JSON string literal, escaping the characters
/// that would break the literal. Node ids and CSS value strings can carry `"`
/// (rare) or backslashes, so escape both plus the line terminators.
fn bridge_js_string(s : String) -> String {
let buf = StringBuilder::new()
buf.write_char('"')
for c in s.iter() {
match c {
'"' => buf.write_string("\\\"")
'\\' => buf.write_string("\\\\")
'\n' => buf.write_string("\\n")
'\r' => buf.write_string("\\r")
_ => buf.write_char(c)
}
}
buf.write_char('"')
buf.to_string()
}
///|
/// Serialize the layout-box geometry index and the computed-style index into the
/// JS the dynamic-rendering bridge injects into the page realm *before* page JS
/// runs. It defines two globals as document-order arrays:
///
/// - `globalThis.__craterLayoutBoxes` — `{index, id, x, y, width, height}`
/// - `globalThis.__craterComputedStyles` — `{index, id, display, position,
/// visibility, fontSize, fontFamily, opacity, zIndex, color, backgroundColor}`
///
/// Each entry carries both the pre-order `index` and the `tag#id` `id` string so
/// the mock DOM can join an element to its real geometry / resolved style by id
/// (unique when the element has an id attribute) or, failing that, by
/// document-order index. With these present, `getBoundingClientRect` /
/// `offsetWidth` / `offsetHeight` / `getComputedStyle` read crater's real layout
/// instead of the inline-style heuristic. This is the JS payload half of step
/// (1) "expose real layout to page JS" in
/// `docs/dynamic-rendering-js-bridge-design.md`; the mock-DOM consumer lives in
/// `browser/native/js_v8/mock_dom_full.mbt`.
pub fn layout_bridge_init_js(
boxes : Array[LayoutBox],
styles : Array[ComputedStyleEntry],
) -> String {
let buf = StringBuilder::new()
buf.write_string("globalThis.__craterLayoutBoxes = [")
for i, b in boxes {
if i > 0 {
buf.write_string(",")
}
buf.write_string("{\"index\":")
buf.write_string(b.index.to_string())
buf.write_string(",\"id\":")
buf.write_string(bridge_js_string(b.id))
buf.write_string(",\"x\":")
buf.write_string(css_number(b.x))
buf.write_string(",\"y\":")
buf.write_string(css_number(b.y))
buf.write_string(",\"width\":")
buf.write_string(css_number(b.width))
buf.write_string(",\"height\":")
buf.write_string(css_number(b.height))
buf.write_string("}")
}
buf.write_string("];\n")
buf.write_string("globalThis.__craterComputedStyles = [")
for i, s in styles {
if i > 0 {
buf.write_string(",")
}
buf.write_string("{\"index\":")
buf.write_string(s.index.to_string())
buf.write_string(",\"id\":")
buf.write_string(bridge_js_string(s.id))
buf.write_string(",\"display\":")
buf.write_string(bridge_js_string(s.display))
buf.write_string(",\"position\":")
buf.write_string(bridge_js_string(s.position))
buf.write_string(",\"visibility\":")
buf.write_string(bridge_js_string(s.visibility))
buf.write_string(",\"fontSize\":")
buf.write_string(bridge_js_string(s.font_size))
buf.write_string(",\"fontFamily\":")
buf.write_string(bridge_js_string(s.font_family))
buf.write_string(",\"opacity\":")
buf.write_string(bridge_js_string(s.opacity))
buf.write_string(",\"zIndex\":")
buf.write_string(bridge_js_string(s.z_index))
buf.write_string(",\"color\":")
buf.write_string(bridge_js_string(s.color))
buf.write_string(",\"backgroundColor\":")
buf.write_string(bridge_js_string(s.background_color))
buf.write_string("}")
}
buf.write_string("];\n")
buf.to_string()
}
///|
/// The bridge-injection JS for the session's base render — the layout geometry
/// and resolved styles page JS would observe on the initial layout. A host that
/// drives the JS realm (the browser shell over the native V8 runtime) evaluates
/// this before running page scripts so DOM measurement reads crater's real
/// layout. See `layout_bridge_init_js`.
pub fn RenderSession::layout_bridge_init_js(self : RenderSession) -> String {
layout_bridge_init_js(self.element_boxes(), self.element_computed_styles())
}