nx_css_selector_match.nx source
↩ module page · 165 lines · 6027 B
1// nx_css_selector_match.nx -- match a parsed CssRule selector against
2// an element's (tag, id, class-list). Phase 2 third primitive after
3// nx_css_tokenize and nx_css_parse; the core of the eventual
4// nx_css_apply layer that walks a DOM tree and cascades styles.
5//
6// Subset matched:
7// TAG selector -- exact case-sensitive byte-equal vs element tag
8// CLASS selector -- one of the whitespace-separated tokens in the
9// element's class attribute byte-equal
10// ID selector -- byte-equal vs the element's id attribute
11//
12// What it does NOT handle yet (named Phase 2b improvements):
13// - case-insensitive tag matching for HTML mode (HTML5 lowercases
14// tags; until nx_html_tokenizer normalizes, callers can pre-lowercase)
15// - compound selectors (h1.foo) -- needs Phase 2b
16// - descendant chains (header h1) -- needs DOM-tree context
17// - sibling/child combinators (+ ~ >)
18// - attribute selectors [href^="..."]
19// - pseudo-classes (:hover, :nth-child)
20// - namespace selectors
21//
22// Per cardinal feedback-honest-perf-verdict-no-aspirational-claims:
23// gap list is EXACT and tested.
24//
25// genealogy_id: w3c_css_selectors_module_level_3 +
26// substrate_browser_phase_2_selector_match
27// lineage_id: nishi_browser_css_selector_match_v1
28//
29// nx_safety_envelope:
30// intended_use: "CSS selector match -- browser front-end style apply"
31// sil_target: SIL1
32// evidence: [W3C_CSS_Selectors_canonical_basis,
33// sealed_selector_kind_enum,
34// bounded_class_list_walk,
35// pure_function_no_allocation]
36// verdict: NOT_YET_EVALUATED
37
38import "nx_syscalls.nx"
39import "nx_css_tokenize.nx"
40import "nx_css_parse.nx"
41
42// Element view -- caller-supplied. Offsets/lengths are into a single
43// document buffer shared with the parsed CssRule (so byte equality is
44// well-defined). Class attribute is the FULL attribute value
45// (potentially multiple whitespace-separated classes; matcher walks
46// the tokens internally).
47struct CssElement {
48 src: *u8, // document bytes (shared with parsed CssRule)
49 tag_off: i64,
50 tag_len: i64,
51 id_off: i64, // 0/0 if no id attribute
52 id_len: i64,
53 class_off: i64, // 0/0 if no class attribute
54 class_len: i64
55}
56
57const NX_CSS_ELEMENT_BYTES: i64 = 56
58
59// ---- helpers ----
60
61// Byte-equal compare two regions of the same source buffer. Returns 1 if
62// equal, 0 otherwise. Two zero-length regions compare equal.
63func _css_bytes_equal(src: *u8, a_off: i64, a_len: i64,
64 b_off: i64, b_len: i64) -> i64 {
65 if a_len != b_len { return 0 }
66 var i: i64 = 0
67 while i < a_len {
68 let ca: i64 = (src[a_off + i] as i64) & 255
69 let cb: i64 = (src[b_off + i] as i64) & 255
70 if ca != cb { return 0 }
71 i = i + 1
72 }
73 return 1
74}
75
76func _css_is_class_separator(c: i64) -> i64 {
77 if c == 32 { return 1 } // ' '
78 if c == 9 { return 1 } // \t
79 if c == 10 { return 1 } // \n
80 if c == 13 { return 1 } // \r
81 if c == 12 { return 1 } // \f
82 return 0
83}
84
85// Check whether the element's class attribute contains a token that
86// byte-equals (sel_off, sel_len) in the source buffer. Whitespace-
87// separated tokens per HTML5 attribute parsing.
88func _css_class_list_contains(src: *u8, class_off: i64, class_len: i64,
89 sel_off: i64, sel_len: i64) -> i64 {
90 if sel_len == 0 { return 0 }
91 if class_len == 0 { return 0 }
92 var i: i64 = 0
93 while i < class_len {
94 // Skip leading separators.
95 var c: i64 = (src[class_off + i] as i64) & 255
96 var keep_skip: i64 = 1
97 while keep_skip == 1 {
98 if i >= class_len { keep_skip = 0 }
99 else {
100 c = (src[class_off + i] as i64) & 255
101 if _css_is_class_separator(c) == 1 { i = i + 1 }
102 else { keep_skip = 0 }
103 }
104 }
105 if i >= class_len { return 0 }
106 // Mark token start.
107 let tok_start: i64 = class_off + i
108 var keep_tok: i64 = 1
109 while keep_tok == 1 {
110 if i >= class_len { keep_tok = 0 }
111 else {
112 c = (src[class_off + i] as i64) & 255
113 if _css_is_class_separator(c) == 1 { keep_tok = 0 }
114 else { i = i + 1 }
115 }
116 }
117 let tok_len: i64 = (class_off + i) - tok_start
118 if _css_bytes_equal(src, tok_start, tok_len, sel_off, sel_len) == 1 {
119 return 1
120 }
121 }
122 return 0
123}
124
125// ---- public API ----
126
127// Initialize a CssElement view in-place.
128func nx_css_element_init(el: *CssElement, src: *u8,
129 tag_off: i64, tag_len: i64,
130 id_off: i64, id_len: i64,
131 class_off: i64, class_len: i64) -> i64 {
132 el.src = src
133 el.tag_off = tag_off
134 el.tag_len = tag_len
135 el.id_off = id_off
136 el.id_len = id_len
137 el.class_off = class_off
138 el.class_len = class_len
139 return 0
140}
141
142// Return 1 if the given parsed CssRule's simple selector matches the
143// element, 0 otherwise. The CssRule and CssElement MUST share the
144// same source buffer (or have byte-identical regions at the named
145// offsets); this primitive does not own or interpret either buffer.
146func nx_css_selector_match(rule: *CssRule, el: *CssElement) -> i64 {
147 let kind: i64 = rule.sel_kind
148
149 if kind == NX_CSS_SEL_TAG {
150 return _css_bytes_equal(el.src, el.tag_off, el.tag_len,
151 rule.sel_off, rule.sel_len)
152 }
153
154 if kind == NX_CSS_SEL_CLASS {
155 return _css_class_list_contains(el.src, el.class_off, el.class_len,
156 rule.sel_off, rule.sel_len)
157 }
158
159 if kind == NX_CSS_SEL_ID {
160 return _css_bytes_equal(el.src, el.id_off, el.id_len,
161 rule.sel_off, rule.sel_len)
162 }
163
164 return 0
165}