nx_css_apply.nx source
↩ module page · 181 lines · 7523 B
1// nx_css_apply.nx -- CSS cascade for the Nishi browser. Phase 2
2// fourth primitive after nx_css_tokenize, nx_css_parse,
3// nx_css_selector_match. Walks a caller-supplied element array,
4// matches every parsed CssRule against every element, and emits a
5// flat `CssComputedDecl[]` of (element_idx, declaration) pairs in
6// SOURCE ORDER. Cascade-by-source-order means downstream queries
7// pick the LAST matching entry for "winner" semantics.
8//
9// What it does:
10// - O(R × E) match loop -- fine at andelinwest.com scale
11// (~50 rules × ~100 elements = 5k pairs). Bigger sites get a
12// reverse selector index in Phase 2c (queued).
13// - Per matched rule, the rule's decl_count declarations are
14// copied into the output array, each tagged with element_idx.
15// - Within an element_idx group, declarations appear in source
16// order (stylesheet position, then rule position, then decl
17// position).
18// - Lookup helper `nx_css_lookup_property` scans the flat array
19// and returns the LAST entry whose property name byte-equals
20// the query for the given element. This is the value the
21// layout / render layers consume.
22//
23// What it does NOT handle yet (named Phase 2b/2c improvements):
24// - specificity (per CSS spec: id > class > tag; ties broken by
25// source order). Phase 2 ships SOURCE-ORDER-ONLY cascade --
26// adequate for hand-written stylesheets where specificity is
27// implicit.
28// - !important
29// - inherited properties (color, font-* propagate to children;
30// needs DOM tree walk + parent computed_style lookup)
31// - initial / inherit / unset / revert keyword values
32// - reverse selector index for large sheets
33//
34// Per cardinal feedback-honest-perf-verdict-no-aspirational-claims:
35// gap list is EXACT and tested.
36//
37// genealogy_id: w3c_css_cascade_module_level_4 +
38// substrate_browser_phase_2_apply
39// lineage_id: nishi_browser_css_apply_v1
40//
41// nx_safety_envelope:
42// intended_use: "CSS cascade -- browser front-end style apply"
43// sil_target: SIL1
44// evidence: [W3C_CSS_cascade_canonical_basis,
45// bounded_O_R_E_match_loop,
46// source_order_explicit,
47// pure_function_caller_owned_memory]
48// verdict: NOT_YET_EVALUATED
49
50import "nx_syscalls.nx"
51import "nx_css_tokenize.nx"
52import "nx_css_parse.nx"
53import "nx_css_selector_match.nx"
54
55// One entry in the flat cascade output array.
56struct CssComputedDecl {
57 element_idx: i64,
58 prop_off: i64,
59 prop_len: i64,
60 val_kind: i64,
61 val_off: i64,
62 val_len: i64,
63 unit_off: i64,
64 unit_len: i64
65}
66
67const NX_CSS_COMPUTED_DECL_BYTES: i64 = 64
68
69// ---- helpers ----
70
71// Byte-equal compare two regions of the same source buffer. Same as
72// the matcher's helper, duplicated here to avoid cross-module
73// private exposure -- nxc2 doesn't have a shared-private convention
74// yet and the helper is 8 lines.
75func _css_apply_bytes_equal(src: *u8, a_off: i64, a_len: i64,
76 b_off: i64, b_len: i64) -> i64 {
77 if a_len != b_len { return 0 }
78 var i: i64 = 0
79 while i < a_len {
80 let ca: i64 = (src[a_off + i] as i64) & 255
81 let cb: i64 = (src[b_off + i] as i64) & 255
82 if ca != cb { return 0 }
83 i = i + 1
84 }
85 return 1
86}
87
88// ---- public API ----
89
90// Apply all rules to all elements. Output goes into `computed` array,
91// up to `max_computed` entries. Returns 1 on success, 0 if the output
92// array is too small for the matches found (caller can retry with a
93// larger array; partial fill is preserved up to overflow).
94//
95// `src` is the shared byte buffer that backs both the parsed rules
96// (selector / decl property / decl value offsets all point here) and
97// the element views (tag / id / class offsets all point here).
98//
99// `out_count` is written with the number of entries actually filled.
100func nx_css_apply(src: *u8,
101 rules: *CssRule, n_rules: i64,
102 decls: *CssDeclaration, n_decls: i64,
103 elements: *CssElement, n_elements: i64,
104 computed: *CssComputedDecl, max_computed: i64,
105 out_count: *i64) -> i64 {
106 var written: i64 = 0
107 var e: i64 = 0
108 while e < n_elements {
109 let el: *CssElement = (elements as *u8 + (e as nx_size) * (NX_CSS_ELEMENT_BYTES as nx_size)) as *CssElement
110 var r: i64 = 0
111 while r < n_rules {
112 let rule: *CssRule = (rules as *u8 + (r as nx_size) * (NX_CSS_RULE_BYTES as nx_size)) as *CssRule
113 let m: i64 = nx_css_selector_match(rule, el)
114 if m == 1 {
115 // Copy each of the rule's declarations into the
116 // output, tagged with this element's index.
117 var d: i64 = 0
118 while d < rule.decl_count {
119 let src_decl_idx: i64 = rule.decl_first + d
120 if src_decl_idx >= n_decls {
121 // Defensive: malformed input. Stop emitting
122 // for this rule.
123 d = rule.decl_count
124 } else {
125 if written >= max_computed {
126 out_count[0] = written
127 return 0
128 }
129 let sd: *CssDeclaration = (decls as *u8 + (src_decl_idx as nx_size) * (NX_CSS_DECLARATION_BYTES as nx_size)) as *CssDeclaration
130 let cd: *CssComputedDecl = (computed as *u8 + (written as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
131 cd.element_idx = e
132 cd.prop_off = sd.prop_off
133 cd.prop_len = sd.prop_len
134 cd.val_kind = sd.val_kind
135 cd.val_off = sd.val_off
136 cd.val_len = sd.val_len
137 cd.unit_off = sd.unit_off
138 cd.unit_len = sd.unit_len
139 written = written + 1
140 d = d + 1
141 }
142 }
143 }
144 r = r + 1
145 }
146 e = e + 1
147 }
148 out_count[0] = written
149 return 1
150}
151
152// Query the cascade output for the LAST entry that:
153// - has element_idx == query_element
154// - has property name byte-equal to (prop_off, prop_len) in `src`
155//
156// Returns a pointer to the matching CssComputedDecl, or 0 if no
157// match. Caller reads val_kind / val_off / val_len / unit_off /
158// unit_len from the returned struct.
159//
160// "Last" implements source-order cascade winner-take-all per CSS
161// spec section 6.4.4 (Phase 2 source-order-only; specificity in
162// Phase 2b).
163func nx_css_lookup_property(src: *u8,
164 computed: *CssComputedDecl, count: i64,
165 query_element: i64,
166 prop_off: i64, prop_len: i64) -> *CssComputedDecl {
167 // Walk in reverse so the first byte-equal hit IS the last
168 // appearance in source order.
169 var i: i64 = count - 1
170 while i >= 0 {
171 let cd: *CssComputedDecl = (computed as *u8 + (i as nx_size) * (NX_CSS_COMPUTED_DECL_BYTES as nx_size)) as *CssComputedDecl
172 if cd.element_idx == query_element {
173 if _css_apply_bytes_equal(src, cd.prop_off, cd.prop_len,
174 prop_off, prop_len) == 1 {
175 return cd
176 }
177 }
178 i = i - 1
179 }
180 return 0 as *CssComputedDecl
181}