///|
/// Document preparation and external CSS cache helpers.
///|
pub struct PseudoRule {
base_selector : @css.ComplexSelector?
base_selector_text : String
declarations : Array[@css.Declaration]
media_query : @css.MediaQueryList?
}
///|
/// Indexed pseudo rules for fast lookup (same strategy as SelectorIndex)
pub struct PseudoRuleIndex {
rules : Array[PseudoRule]
by_id : Map[String, Array[Int]]
by_class : Map[String, Array[Int]]
by_tag : Map[String, Array[Int]]
universal : Array[Int]
}
///|
fn PseudoRuleIndex::build(rules : Array[PseudoRule]) -> PseudoRuleIndex {
let idx : PseudoRuleIndex = {
rules,
by_id: {},
by_class: {},
by_tag: {},
universal: [],
}
for i, rule in rules {
match rule.base_selector {
Some(sel) => {
// Index by head compound selector's most selective key
let mut indexed = false
// Check ID
for sub in sel.head.subclasses {
if !indexed {
match sub {
@css.SimpleSelector::Id(name) => {
match idx.by_id.get(name) {
Some(arr) => arr.push(i)
None => idx.by_id.set(name, [i])
}
indexed = true
}
_ => ()
}
}
}
// Check class
if !indexed {
for sub in sel.head.subclasses {
if !indexed {
match sub {
@css.SimpleSelector::Class(name) => {
match idx.by_class.get(name) {
Some(arr) => arr.push(i)
None => idx.by_class.set(name, [i])
}
indexed = true
}
_ => ()
}
}
}
}
// Check tag
if !indexed {
match sel.head.type_selector {
Some(@css.SimpleSelector::Type(name)) => {
let tag = name.to_lower()
match idx.by_tag.get(tag) {
Some(arr) => arr.push(i)
None => idx.by_tag.set(tag, [i])
}
indexed = true
}
_ => ()
}
}
if !indexed {
idx.universal.push(i)
}
}
None => idx.universal.push(i)
}
}
idx
}
///|
fn PseudoRuleIndex::get_candidates(
self : PseudoRuleIndex,
element : @css.Element,
) -> Array[Int] {
let candidates : Array[Int] = []
match element.id {
Some(id) =>
match self.by_id.get(id) {
Some(arr) =>
for i in arr {
candidates.push(i)
}
None => ()
}
None => ()
}
for cls in element.classes {
match self.by_class.get(cls) {
Some(arr) =>
for i in arr {
candidates.push(i)
}
None => ()
}
}
let tag = element.tag_name.to_lower()
match self.by_tag.get(tag) {
Some(arr) =>
for i in arr {
candidates.push(i)
}
None => ()
}
for i in self.universal {
candidates.push(i)
}
candidates
}
///|
pub struct PreparedExternalCss {
stylesheets : Array[@css.Stylesheet]
indexed_stylesheets : Array[@css.IndexedStylesheet]
before_rules : Array[PseudoRule]
after_rules : Array[PseudoRule]
before_index : PseudoRuleIndex
after_index : PseudoRuleIndex
total_rules : Int
}
///|
let external_css_bundle_cache : Ref[Map[String, PreparedExternalCss]] = {
val: {},
}
///|
fn external_css_cache_key(external_css : Array[String]) -> String {
let mut total_len = 0
for css in external_css {
total_len = total_len + css.length() + 16
}
let buf = StringBuilder::new(size_hint=total_len)
for css in external_css {
buf.write_string(css.length().to_string())
buf.write_string(":")
buf.write_string(css)
}
buf.to_string()
}
///|
fn collect_pseudo_rules_from_stylesheet(
stylesheet : @css.Stylesheet,
before_rules : Array[PseudoRule],
after_rules : Array[PseudoRule],
) -> Unit {
for rule in stylesheet.rules {
let sel = rule.selector_text
if sel.contains("before") {
match selector_text_without_pseudo(sel, Before) {
Some(base_text) => {
let base_sel = @css.parse_selector(base_text)
before_rules.push({
base_selector: base_sel,
base_selector_text: base_text,
declarations: rule.declarations,
media_query: rule.media_query,
})
}
None => ()
}
}
if sel.contains("after") {
match selector_text_without_pseudo(sel, After) {
Some(base_text) => {
let base_sel = @css.parse_selector(base_text)
after_rules.push({
base_selector: base_sel,
base_selector_text: base_text,
declarations: rule.declarations,
media_query: rule.media_query,
})
}
None => ()
}
}
}
}
///|
fn empty_prepared_external_css() -> PreparedExternalCss {
{
stylesheets: [],
indexed_stylesheets: [],
before_rules: [],
after_rules: [],
before_index: {
rules: [],
by_id: {},
by_class: {},
by_tag: {},
universal: [],
},
after_index: {
rules: [],
by_id: {},
by_class: {},
by_tag: {},
universal: [],
},
total_rules: 0,
}
}
///|
pub fn prepare_external_css(
external_css : Array[String],
) -> PreparedExternalCss {
if external_css.is_empty() {
return empty_prepared_external_css()
}
let key = external_css_cache_key(external_css)
match external_css_bundle_cache.val.get(key) {
Some(bundle) => bundle
None => {
let stylesheets : Array[@css.Stylesheet] = []
let indexed_stylesheets : Array[@css.IndexedStylesheet] = []
let before_rules : Array[PseudoRule] = []
let after_rules : Array[PseudoRule] = []
let mut total_rules = 0
for css in external_css {
let stylesheet = @css.parse_stylesheet(
@html.flatten_css_cascade_layers(css),
)
total_rules += stylesheet.rules.length()
collect_pseudo_rules_from_stylesheet(
stylesheet, before_rules, after_rules,
)
stylesheets.push(stylesheet)
indexed_stylesheets.push(@css.IndexedStylesheet::new(stylesheet))
}
let bundle : PreparedExternalCss = {
stylesheets,
indexed_stylesheets,
before_rules,
after_rules,
before_index: PseudoRuleIndex::build(before_rules),
after_index: PseudoRuleIndex::build(after_rules),
total_rules,
}
external_css_bundle_cache.val.set(key, bundle)
bundle
}
}
}
///|
pub struct PreparedRenderDocument {
stylesheets : Array[@css.Stylesheet]
indexed_stylesheets : Array[@css.IndexedStylesheet]
css_vars : Map[String, String]
doc_root_selector : @css.Element
doc_root_style : @style.Style
render_root : @html.Element
render_root_tag : String
body_uses_document_root_style : Bool
suppress_quirks_body_ua_top_margin : Bool
before_index : PseudoRuleIndex
after_index : PseudoRuleIndex
}
///|
fn text_is_whitespace_only(s : String) -> Bool {
for i = 0; i < s.length(); i = i + 1 {
let c = s[i].to_int().unsafe_to_char()
if c != ' ' && c != '\t' && c != '\n' && c != '\r' {
return false
}
}
true
}
///|
fn tag_has_ua_top_margin(tag : String) -> Bool {
match tag.to_lower() {
"p"
| "h1"
| "h2"
| "h3"
| "h4"
| "h5"
| "h6"
| "ul"
| "ol"
| "menu"
| "dl"
| "pre"
| "blockquote"
| "figure" => true
_ => false
}
}
///|
fn leading_margin_is_ua_default(elem : @html.Element) -> Bool {
if elem.style is Some(_) {
return false
}
if tag_has_ua_top_margin(elem.tag) {
return true
}
for child in elem.children {
match child {
@html.Node::Text(t) => if !text_is_whitespace_only(t) { return false }
@html.Node::Element(child_elem) =>
if leading_margin_is_ua_default(child_elem) {
return true
} else if child_elem.style is Some(_) {
return false
}
}
}
false
}
///|
fn should_suppress_quirks_body_ua_top_margin(
doc : @html.Document,
render_root : @html.Element,
external_bundle : PreparedExternalCss,
) -> Bool {
if !doc.quirks_mode ||
render_root.tag.to_lower() != "body" ||
!doc.stylesheets.is_empty() ||
external_bundle.total_rules > 0 {
return false
}
for child in render_root.children {
match child {
@html.Node::Text(t) => if !text_is_whitespace_only(t) { return false }
@html.Node::Element(elem) =>
if leading_margin_is_ua_default(elem) {
return true
} else if elem.style is Some(_) {
return false
}
}
}
false
}
///|
pub fn prepare_render_document(
doc : @html.Document,
ctx : RenderContext,
external_css : Array[String],
) -> PreparedRenderDocument {
let _t0 = perf_clock_us()
let external_bundle = prepare_external_css(external_css)
prepare_render_document_with_external_css_bundle(
doc, ctx, external_bundle, _t0,
)
}
///|
pub fn prepare_render_document_with_prepared_external_css(
doc : @html.Document,
ctx : RenderContext,
external_css : PreparedExternalCss,
) -> PreparedRenderDocument {
let _t0 = perf_clock_us()
prepare_render_document_with_external_css_bundle(doc, ctx, external_css, _t0)
}
///|
fn prepare_render_document_with_external_css_bundle(
doc : @html.Document,
ctx : RenderContext,
external_bundle : PreparedExternalCss,
t0 : Int64,
) -> PreparedRenderDocument {
let stylesheets : Array[@css.Stylesheet] = []
for stylesheet in external_bundle.stylesheets {
stylesheets.push(stylesheet)
}
let indexed_stylesheets : Array[@css.IndexedStylesheet] = []
for indexed in external_bundle.indexed_stylesheets {
indexed_stylesheets.push(indexed)
}
let before_rules = external_bundle.before_rules.copy()
let after_rules = external_bundle.after_rules.copy()
let mut total_rules = external_bundle.total_rules
for css in doc.stylesheets {
let stylesheet = @css.parse_stylesheet(
@html.flatten_css_cascade_layers(css),
)
total_rules += stylesheet.rules.length()
collect_pseudo_rules_from_stylesheet(stylesheet, before_rules, after_rules)
stylesheets.push(stylesheet)
indexed_stylesheets.push(@css.IndexedStylesheet::new(stylesheet))
}
let _t1 = perf_clock_us()
maybe_log_perf(
"[perf] css_parse=" +
((_t1 - t0) / 1000L).to_string() +
"ms rules=" +
total_rules.to_string(),
)
let _t2 = perf_clock_us()
let css_vars = collect_root_css_variables(stylesheets, ctx)
let doc_root_selector = if indexed_stylesheets.length() == 0 {
html_to_selector_element_minimal(doc.root, None)
} else {
html_to_selector_element(doc.root, None)
}
let doc_root_style = compute_element_style_indexed(
doc_root_selector,
doc.root.style,
indexed_stylesheets,
true,
ctx,
None,
css_vars,
)
let render_root = select_render_root(doc.root, doc_root_style)
let render_root_tag = render_root.tag.to_lower()
let suppress_quirks_body_ua_top_margin = should_suppress_quirks_body_ua_top_margin(
doc, render_root, external_bundle,
)
let prepared : PreparedRenderDocument = {
stylesheets,
indexed_stylesheets,
css_vars,
doc_root_selector,
doc_root_style,
render_root,
render_root_tag,
body_uses_document_root_style: render_root_tag == "body" &&
doc.root.tag.to_lower() != "body",
suppress_quirks_body_ua_top_margin,
before_index: if doc.stylesheets.is_empty() {
external_bundle.before_index
} else {
PseudoRuleIndex::build(before_rules)
},
after_index: if doc.stylesheets.is_empty() {
external_bundle.after_index
} else {
PseudoRuleIndex::build(after_rules)
},
}
maybe_log_perf(
"[perf] css_parse=" +
((_t1 - t0) / 1000L).to_string() +
"ms index=" +
((_t2 - _t1) / 1000L).to_string() +
"ms rules=" +
total_rules.to_string() +
" pseudo_before=" +
before_rules.length().to_string() +
" pseudo_after=" +
after_rules.length().to_string(),
)
prepared
}