//! Code for a layer, as shown by the Inspect panel and returned by `inspect.code`: CSS for the //! layer itself, and SwiftUI or Jetpack Compose for its subtree (stacks for auto layout, absolute //! offsets otherwise). The output is a starting point, not a pixel-exact translation. use figcraft_color::{Color, PaintKind}; use figcraft_doc::*; use serde_json::json; use crate::cmd::CommandSpec; use crate::params::*; use crate::{bad, spec}; pub fn specs() -> Vec { vec![spec!( "inspect.code", "Code for layer", "{id?, language?: css|html|swiftui|compose (default css; html is the whole layer tree)} → {code}", false, |s, p| { let id = targets(s, p).first().copied().ok_or_else(|| bad("inspect.code", "select a layer"))?; let lang = match string(p, "language") { Some(l) => CodeLang::from_id(&l).ok_or_else(|| bad("inspect.code", "language: css|swiftui|compose"))?, None => CodeLang::Css, }; let doc = s.doc()?; if !doc.contains(id) { return Err(bad("inspect.code", "no such layer")); } Ok(json!({"code": code_for(doc, id, lang), "language": lang.id()})) } )] } /// Languages the Inspect panel offers. #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] pub enum CodeLang { #[default] Css, /// The whole layer tree as nested HTML elements with a stylesheet. Html, SwiftUi, Compose, } impl CodeLang { pub const ALL: [CodeLang; 4] = [CodeLang::Css, CodeLang::Html, CodeLang::SwiftUi, CodeLang::Compose]; pub fn id(self) -> &'static str { match self { CodeLang::Css => "css", CodeLang::Html => "html", CodeLang::SwiftUi => "swiftui", CodeLang::Compose => "compose", } } pub fn label(self) -> &'static str { match self { CodeLang::Css => "CSS", CodeLang::Html => "HTML + CSS", CodeLang::SwiftUi => "SwiftUI", CodeLang::Compose => "Compose", } } pub fn from_id(s: &str) -> Option { CodeLang::ALL.into_iter().find(|l| l.id().eq_ignore_ascii_case(s) || l.label().eq_ignore_ascii_case(s)) } } /// Layers deeper than this (or past this many children) are summarised. const MAX_DEPTH: usize = 12; const MAX_CHILDREN: usize = 200; /// `v` without trailing zeros (2 decimals). fn num(v: f64) -> String { if !v.is_finite() { return "0".into(); } let r = (v * 100.0).round() / 100.0; if (r - r.round()).abs() < 1e-9 { format!("{}", r.round() as i64) } else { format!("{r}") } } pub fn code_for(doc: &Document, id: NodeId, lang: CodeLang) -> String { match lang { CodeLang::Css => css(doc, id), CodeLang::Html => html(doc, id), CodeLang::SwiftUi => { let mut out = String::new(); swift(doc, id, 0, &mut out); out } CodeLang::Compose => { let mut out = String::new(); compose(doc, id, 0, &mut out); out } } } fn first_fill(n: &Node) -> Option { n.fills.iter().filter(|p| p.visible).find_map(|p| match &p.kind { PaintKind::Solid { color } => Some(color.with_alpha(color.a * p.opacity)), PaintKind::Gradient(g) => g.sorted_stops().first().map(|s| s.color), PaintKind::Image(_) => None, }) } fn stroke_color(n: &Node) -> Option { if !n.has_visible_stroke() { return None; } n.strokes.iter().filter(|p| p.visible).find_map(|p| p.solid_color().map(|c| c.with_alpha(c.a * p.opacity))) } fn drop_shadow(n: &Node) -> Option<&Shadow> { n.effects.iter().filter(|e| e.visible).find_map(|e| match &e.kind { EffectKind::DropShadow(s) => Some(s), _ => None, }) } fn radius(n: &Node) -> f64 { n.corners.uniform().unwrap_or_else(|| n.corners.radii.iter().copied().fold(0.0, f64::max)) } /// Text content as a string literal (escaped for Swift; Kotlin also escapes `$`). fn literal(s: &str, kotlin: bool) -> String { let mut out = String::from("\""); for c in s.chars().take(10_000) { match c { '"' => out.push_str("\\\""), '\\' => out.push_str("\\\\"), '\n' | '\u{2028}' => out.push_str("\\n"), '\r' => {} '\t' => out.push_str("\\t"), '$' if kotlin => out.push_str("\\$"), c if c.is_control() => {} c => out.push(c), } } out.push('"'); out } /// Visible children in paint order (bottom first), capped. fn kids(doc: &Document, n: &Node) -> Vec { n.children.iter().copied().filter(|c| doc.node(*c).is_some_and(|k| k.visible)).take(MAX_CHILDREN).collect() } fn indent(depth: usize) -> String { " ".repeat(depth) } // ------------------------------------------------------------------ CSS fn css(doc: &Document, id: NodeId) -> String { let Some(n) = doc.node(id) else { return String::new() }; format!("/* {} */\n{}", n.name.replace("*/", "* /"), css_decls(n)) } /// The CSS declarations for `n`'s own look and size, one per line. fn css_decls(n: &Node) -> String { let mut out = format!("width: {}px;\nheight: {}px;\n", num(n.width), num(n.height)); if let Some(l) = &n.layout { out.push_str("display: flex;\n"); out.push_str(&format!("flex-direction: {};\n", if l.mode == LayoutMode::Vertical { "column" } else { "row" })); if l.wrap { out.push_str("flex-wrap: wrap;\n"); } out.push_str(&format!("gap: {}px;\n", num(l.gap))); let [pt, pr, pb, pl] = l.padding; out.push_str(&format!("padding: {}px {}px {}px {}px;\n", num(pt), num(pr), num(pb), num(pl))); out.push_str(&format!( "justify-content: {};\nalign-items: {};\n", match l.main_align { MainAlign::Min => "flex-start", MainAlign::Center => "center", MainAlign::Max => "flex-end", MainAlign::SpaceBetween => "space-between", }, match l.cross_align { CrossAlign::Min => "flex-start", CrossAlign::Center => "center", CrossAlign::Max => "flex-end", CrossAlign::Baseline => "baseline", } )); } if let Some(r) = n.corners.uniform().filter(|r| *r > 0.0) { out.push_str(&format!("border-radius: {}px;\n", num(r))); } else if n.corners.radii.iter().any(|r| *r > 0.0) { let [a, b, c, d] = n.corners.radii; out.push_str(&format!("border-radius: {}px {}px {}px {}px;\n", num(a), num(b), num(c), num(d))); } if n.opacity < 1.0 { out.push_str(&format!("opacity: {};\n", num(f64::from(n.opacity)))); } for p in n.fills.iter().filter(|p| p.visible) { match &p.kind { PaintKind::Solid { color } => { let key = if matches!(n.kind, NodeKind::Text(_)) { "color" } else { "background" }; out.push_str(&format!("{key}: {};\n", color.with_alpha(color.a * p.opacity).to_css())); } PaintKind::Gradient(g) => { let stops: Vec = g.sorted_stops().iter().map(|s| format!("{} {}%", s.color.to_css(), (s.position * 100.0).round())).collect(); out.push_str(&format!("background: linear-gradient({});\n", stops.join(", "))); } PaintKind::Image(_) => out.push_str("background: url(image.png) center / cover;\n"), } } if let Some(c) = stroke_color(n) { out.push_str(&format!("border: {}px solid {};\n", num(n.stroke.weight), c.to_css())); } let shadows: Vec = n .effects .iter() .filter(|e| e.visible) .filter_map(|e| match &e.kind { EffectKind::DropShadow(s) => { Some(format!("{}px {}px {}px {}px {}", num(s.offset.0), num(s.offset.1), num(s.radius), num(s.spread), s.color.to_css())) } EffectKind::InnerShadow(s) => { Some(format!("inset {}px {}px {}px {}px {}", num(s.offset.0), num(s.offset.1), num(s.radius), num(s.spread), s.color.to_css())) } _ => None, }) .collect(); if !shadows.is_empty() { out.push_str(&format!("box-shadow: {};\n", shadows.join(", "))); } for e in n.effects.iter().filter(|e| e.visible) { match e.kind { EffectKind::LayerBlur { radius } => out.push_str(&format!("filter: blur({}px);\n", num(radius / 2.0))), EffectKind::BackgroundBlur { radius } => out.push_str(&format!("backdrop-filter: blur({}px);\n", num(radius / 2.0))), // CSS has no refraction: the frost is the nearest. EffectKind::Glass(ref g) => out.push_str(&format!("backdrop-filter: blur({}px);\n", num(g.sanitized().frost / 2.0))), _ => {} } } if let NodeKind::Text(t) = &n.kind { out.push_str(&format!( "font-family: \"{}\";\nfont-size: {}px;\nfont-weight: {};\n", t.style.font_family, num(t.style.font_size), t.style.weight() )); if t.style.italic() { out.push_str("font-style: italic;\n"); } match t.style.line_height { LineHeight::Pixels(v) => out.push_str(&format!("line-height: {}px;\n", num(v))), LineHeight::Percent(p) => out.push_str(&format!("line-height: {}%;\n", num(p))), LineHeight::Auto => {} } match t.style.letter_spacing { LetterSpacing::Pixels(v) if v != 0.0 => out.push_str(&format!("letter-spacing: {}px;\n", num(v))), LetterSpacing::Percent(p) if p != 0.0 => out.push_str(&format!("letter-spacing: {}em;\n", num(p / 100.0))), _ => {} } } out } // ------------------------------------------------------------------ HTML /// A CSS class name from a layer name ("Card / Title" → "card-title"), unique in `used`. fn class_name(name: &str, used: &mut Vec) -> String { let mut base = String::new(); for c in name.chars().flat_map(char::to_lowercase) { if c.is_ascii_alphanumeric() { base.push(c); } else if !base.ends_with('-') && !base.is_empty() { base.push('-'); } } let base = base.trim_end_matches('-').chars().take(40).collect::(); let base = if base.is_empty() || base.starts_with(|c: char| c.is_ascii_digit()) { format!("layer-{base}") } else { base }; let name = (1..).map(|k| if k == 1 { base.clone() } else { format!("{base}-{k}") }).find(|n| !used.contains(n)).unwrap_or(base); used.push(name.clone()); name } fn html_escape(s: &str) -> String { s.replace('&', "&").replace('<', "<").replace('>', ">").replace('\n', "
") } /// The whole tree under `id` as nested HTML with a stylesheet: auto layout frames become /// flexboxes, other children are placed absolutely in their parent, text becomes paragraphs. fn html(doc: &Document, id: NodeId) -> String { let mut out = Html { used: Vec::new(), markup: String::new(), styles: String::new() }; out.node(doc, id, None, 0); format!("{}\n\n", out.markup, out.styles) } /// HTML output being built: the class names taken, the markup and the stylesheet. struct Html { used: Vec, markup: String, styles: String, } impl Html { fn node(&mut self, doc: &Document, id: NodeId, parent: Option<&Node>, depth: usize) { let Some(n) = doc.node(id) else { return }; let class = class_name(&n.name, &mut self.used); let pad = indent(depth); // How it sits in its parent: in the flex flow, or placed at its position. let mut decls = String::new(); let in_flow = parent.is_some_and(|p| p.layout.is_some()) && !n.child_layout.absolute; if depth == 0 { decls.push_str("position: relative;\n"); } else if in_flow { let vertical = parent.and_then(|p| p.layout.as_ref()).is_some_and(|l| l.mode == LayoutMode::Vertical); let (main, cross) = if vertical { (n.child_layout.sizing_v, n.child_layout.sizing_h) } else { (n.child_layout.sizing_h, n.child_layout.sizing_v) }; if main == Sizing::Fill { decls.push_str("flex: 1 1 0;\n"); } if cross == Sizing::Fill { decls.push_str("align-self: stretch;\n"); } } else { let c = n.transform.as_coeffs(); decls.push_str(&format!("position: absolute;\nleft: {}px;\ntop: {}px;\n", num(c[4]), num(c[5]))); let rot = figcraft_geom::rotation_degrees(n.transform); if rot.abs() > 0.01 { decls.push_str(&format!("transform: rotate({}deg);\ntransform-origin: 0 0;\n", num(-rot))); } } let mut own = css_decls(n); if n.layout.is_none() && !n.children.is_empty() && depth > 0 && in_flow { own.push_str("position: relative;\n"); } // Hugging sides take their content's size. if n.layout.is_some() || matches!(n.kind, NodeKind::Text(_)) { if n.child_layout.sizing_h == Sizing::Hug { own = own.replace(&format!("width: {}px;\n", num(n.width)), ""); } if n.child_layout.sizing_v == Sizing::Hug { own = own.replace(&format!("height: {}px;\n", num(n.height)), ""); } } decls.push_str(&own); self.styles.push_str(&format!(".{class} {{\n")); for line in decls.lines() { self.styles.push_str(&format!(" {line}\n")); } self.styles.push_str("}\n"); match &n.kind { NodeKind::Text(t) => self.markup.push_str(&format!("{pad}

{}

\n", html_escape(&t.characters))), _ => { let kids = kids(doc, n); if kids.is_empty() || depth >= MAX_DEPTH { self.markup.push_str(&format!("{pad}
\n")); } else { self.markup.push_str(&format!("{pad}
\n")); // Bottom layer first: later elements paint on top, like the layer order. for k in kids { self.node(doc, k, Some(n), depth + 1); } self.markup.push_str(&format!("{pad}
\n")); } } } } } // ------------------------------------------------------------------ SwiftUI fn swift_color(c: Color) -> String { format!("Color(red: {}, green: {}, blue: {}, opacity: {})", num(f64::from(c.r)), num(f64::from(c.g)), num(f64::from(c.b)), num(f64::from(c.a))) } fn swift_weight(w: u16) -> &'static str { match w { 0..=150 => ".thin", 151..=250 => ".ultraLight", 251..=350 => ".light", 351..=450 => ".regular", 451..=550 => ".medium", 551..=650 => ".semibold", 651..=750 => ".bold", 751..=850 => ".heavy", _ => ".black", } } fn swift(doc: &Document, id: NodeId, depth: usize, out: &mut String) { let Some(n) = doc.node(id) else { return }; let pad = indent(depth); if depth > MAX_DEPTH { out.push_str(&format!("{pad}// {} …\n", n.name)); return; } let mut mods: Vec = Vec::new(); match &n.kind { NodeKind::Text(t) => { out.push_str(&format!("{pad}Text({})\n", literal(&t.characters, false))); mods.push(format!( ".font(.custom({}, size: {}).weight({}))", literal(&t.style.font_family, false), num(t.style.font_size), swift_weight(t.style.weight()) )); if let Some(c) = first_fill(n) { mods.push(format!(".foregroundColor({})", swift_color(c))); } } NodeKind::Ellipse { .. } => { out.push_str(&format!("{pad}Ellipse()\n")); if let Some(c) = first_fill(n) { mods.push(format!(".fill({})", swift_color(c))); } mods.push(format!(".frame(width: {}, height: {})", num(n.width), num(n.height))); } NodeKind::Rectangle => { out.push_str(&format!("{pad}RoundedRectangle(cornerRadius: {})\n", num(radius(n)))); if let Some(c) = first_fill(n) { mods.push(format!(".fill({})", swift_color(c))); } mods.push(format!(".frame(width: {}, height: {})", num(n.width), num(n.height))); } k if k.is_frame_like() || matches!(k, NodeKind::Group) => { let children = kids(doc, n); let (open, close) = match n.layout.as_ref().map(|l| (l.mode, l)) { Some((LayoutMode::Vertical, l)) => (format!("VStack(alignment: .leading, spacing: {}) {{", num(l.gap)), "}"), Some((_, l)) => (format!("HStack(alignment: .center, spacing: {}) {{", num(l.gap)), "}"), None => ("ZStack(alignment: .topLeading) {".to_string(), "}"), }; out.push_str(&format!("{pad}{open}\n")); for c in &children { swift(doc, *c, depth + 1, out); // Free children sit at their offset inside a ZStack. if n.layout.is_none() && let Some(cn) = doc.node(*c) { let [_, _, _, _, x, y] = cn.transform.as_coeffs(); out.push_str(&format!("{} .offset(x: {}, y: {})\n", indent(depth + 1), num(x), num(y))); } } out.push_str(&format!("{pad}{close}\n")); if let Some(l) = &n.layout { let [t, r, b, lft] = l.padding; if [t, r, b, lft].iter().any(|v| *v > 0.0) { mods.push(format!(".padding(EdgeInsets(top: {}, leading: {}, bottom: {}, trailing: {}))", num(t), num(lft), num(b), num(r))); } } mods.push(format!(".frame(width: {}, height: {}, alignment: .topLeading)", num(n.width), num(n.height))); if let Some(c) = first_fill(n) { mods.push(format!(".background({})", swift_color(c))); } if radius(n) > 0.0 { mods.push(format!(".clipShape(RoundedRectangle(cornerRadius: {}))", num(radius(n)))); } } _ => { out.push_str(&format!("{pad}// {}: export as an image asset\n{pad}Image({})\n", n.kind.label(), literal(&n.name, false))); mods.push(".resizable()".into()); mods.push(format!(".frame(width: {}, height: {})", num(n.width), num(n.height))); } } if let Some(c) = stroke_color(n) { mods.push(format!( ".overlay(RoundedRectangle(cornerRadius: {}).stroke({}, lineWidth: {}))", num(radius(n)), swift_color(c), num(n.stroke.weight) )); } if let Some(s) = drop_shadow(n) { mods.push(format!( ".shadow(color: {}, radius: {}, x: {}, y: {})", swift_color(s.color), num(s.radius / 2.0), num(s.offset.0), num(s.offset.1) )); } if n.opacity < 1.0 { mods.push(format!(".opacity({})", num(f64::from(n.opacity)))); } for m in mods { out.push_str(&format!("{pad} {m}\n")); } } // ------------------------------------------------------------------ Jetpack Compose fn kotlin_color(c: Color) -> String { let [r, g, b, a] = c.to_rgba8(); format!("Color(0x{a:02X}{r:02X}{g:02X}{b:02X})") } /// The Modifier chain for a layer (size, shadow, background, border, alpha, padding). fn modifier(n: &Node, shape: &str, padding: bool) -> String { let mut m = format!("Modifier.size({}.dp, {}.dp)", num(n.width), num(n.height)); if let Some(s) = drop_shadow(n) { m.push_str(&format!(".shadow({}.dp, {shape})", num(s.radius / 2.0))); } if let Some(c) = first_fill(n) { m.push_str(&format!(".background({}, {shape})", kotlin_color(c))); } if let Some(c) = stroke_color(n) { m.push_str(&format!(".border({}.dp, {}, {shape})", num(n.stroke.weight), kotlin_color(c))); } if n.opacity < 1.0 { m.push_str(&format!(".alpha({}f)", num(f64::from(n.opacity)))); } if padding && let Some(l) = &n.layout { let [t, r, b, lft] = l.padding; if [t, r, b, lft].iter().any(|v| *v > 0.0) { m.push_str(&format!(".padding(start = {}.dp, top = {}.dp, end = {}.dp, bottom = {}.dp)", num(lft), num(t), num(r), num(b))); } } m } fn compose(doc: &Document, id: NodeId, depth: usize, out: &mut String) { let Some(n) = doc.node(id) else { return }; let pad = indent(depth); if depth > MAX_DEPTH { out.push_str(&format!("{pad}// {} …\n", n.name)); return; } let shape = if radius(n) > 0.0 { format!("RoundedCornerShape({}.dp)", num(radius(n))) } else { "RectangleShape".to_string() }; match &n.kind { NodeKind::Text(t) => { let color = first_fill(n).map(kotlin_color).unwrap_or_else(|| "Color.Unspecified".into()); out.push_str(&format!( "{pad}Text(\n{pad} text = {},\n{pad} fontSize = {}.sp,\n{pad} fontWeight = FontWeight.W{},\n{pad} color = {color},\n{pad})\n", literal(&t.characters, true), num(t.style.font_size), (t.style.weight().clamp(100, 900) / 100) * 100 )); } NodeKind::Ellipse { .. } => out.push_str(&format!("{pad}Box(modifier = {})\n", modifier(n, "CircleShape", false))), NodeKind::Rectangle => out.push_str(&format!("{pad}Box(modifier = {})\n", modifier(n, &shape, false))), k if k.is_frame_like() || matches!(k, NodeKind::Group) => { let children = kids(doc, n); let open = match n.layout.as_ref().map(|l| (l.mode, l)) { Some((LayoutMode::Vertical, l)) => format!("Column(\n{pad} modifier = {},\n{pad} verticalArrangement = Arrangement.spacedBy({}.dp),\n{pad}) {{", modifier(n, &shape, true), num(l.gap)), Some((_, l)) => format!( "Row(\n{pad} modifier = {},\n{pad} horizontalArrangement = Arrangement.spacedBy({}.dp),\n{pad} verticalAlignment = Alignment.CenterVertically,\n{pad}) {{", modifier(n, &shape, true), num(l.gap) ), None => format!("Box(modifier = {}) {{", modifier(n, &shape, false)), }; out.push_str(&format!("{pad}{open}\n")); for c in &children { if n.layout.is_none() && let Some(cn) = doc.node(*c) { let [_, _, _, _, x, y] = cn.transform.as_coeffs(); out.push_str(&format!("{}Box(modifier = Modifier.offset({}.dp, {}.dp)) {{\n", indent(depth + 1), num(x), num(y))); compose(doc, *c, depth + 2, out); out.push_str(&format!("{}}}\n", indent(depth + 1))); } else { compose(doc, *c, depth + 1, out); } } out.push_str(&format!("{pad}}}\n")); } _ => out.push_str(&format!( "{pad}// {}: export as an image asset\n{pad}Image(painter = painterResource(R.drawable.asset), contentDescription = {}, modifier = Modifier.size({}.dp, {}.dp))\n", n.kind.label(), literal(&n.name, true), num(n.width), num(n.height) )), } } #[cfg(test)] mod tests { use super::*; use crate::Session; use crate::params::id_of; use serde_json::json; fn card(s: &mut Session) -> NodeId { let f = id_of(&s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 200, "height": 100, "name": "Card"})).unwrap()["id"]).unwrap(); s.execute("node.set", &json!({"id": f, "props": {"fills": ["#FFFFFF"], "cornerRadius": 12}})).unwrap(); s.execute("text.create", &json!({"text": "Say \"hi\" $5\nnow", "x": 0, "y": 0, "parent": f})).unwrap(); s.execute("shape.ellipse", &json!({"x": 0, "y": 0, "width": 24, "height": 24, "parent": f})).unwrap(); s.execute("layout.add", &json!({"ids": [f]})).unwrap(); f } #[test] fn swiftui_and_compose_for_an_auto_layout_card() { let mut s = Session::new(); let f = card(&mut s); let doc = s.doc().unwrap(); let sw = code_for(doc, f, CodeLang::SwiftUi); assert!(sw.contains("Stack(alignment:"), "{sw}"); assert!(sw.contains("Text(\"Say \\\"hi\\\" $5\\nnow\")"), "escaped: {sw}"); assert!(sw.contains("Ellipse()")); assert!(sw.contains(".clipShape(RoundedRectangle(cornerRadius: 12))")); let kt = code_for(doc, f, CodeLang::Compose); assert!(kt.contains("Arrangement.spacedBy("), "{kt}"); assert!(kt.contains("\\$5"), "Kotlin escapes $: {kt}"); assert!(kt.contains("CircleShape")); assert!(kt.contains("RoundedCornerShape(12.dp)")); let css = code_for(doc, f, CodeLang::Css); assert!(css.contains("display: flex;") && css.contains("border-radius: 12px;")); // Through the command. let v = s.execute("inspect.code", &json!({"id": f, "language": "compose"})).unwrap(); assert_eq!(v["code"].as_str().unwrap(), kt); assert!(s.execute("inspect.code", &json!({"id": f, "language": "cobol"})).is_err()); } #[test] fn free_children_get_offsets_and_deep_trees_are_capped() { let mut s = Session::new(); let mut parent = id_of(&s.execute("frame.create", &json!({"x": 0, "y": 0, "width": 50, "height": 50})).unwrap()["id"]).unwrap(); let root = parent; s.execute("shape.rectangle", &json!({"x": 7, "y": 9, "width": 5, "height": 5, "parent": root})).unwrap(); for _ in 0..20 { parent = id_of(&s.execute("frame.create", &json!({"x": 1, "y": 1, "width": 40, "height": 40, "parent": parent})).unwrap()["id"]).unwrap(); } let doc = s.doc().unwrap(); let sw = code_for(doc, root, CodeLang::SwiftUi); assert!(sw.contains(".offset(x: 7, y: 9)"), "{sw}"); assert!(sw.contains("…"), "depth capped"); let kt = code_for(doc, root, CodeLang::Compose); assert!(kt.contains("Modifier.offset(7.dp, 9.dp)")); assert!(code_for(doc, NodeId(999_999), CodeLang::Css).is_empty()); } }