Skip to content

Commit 054b2ea

Browse files
committed
Support nested CSS rules in modal layouts
1 parent e4647f1 commit 054b2ea

8 files changed

Lines changed: 512 additions & 23 deletions

File tree

‎experiments/WebScene.NativeEngine.Probe/native/html_parser/src/lib.rs‎

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -958,15 +958,13 @@ mod css_syntax {
958958
if prelude.name.eq_ignore_ascii_case("font-face")
959959
|| prelude.name.eq_ignore_ascii_case("page")
960960
{
961-
let before = self.state.declaration_count.get();
962-
parse_css_stream_declaration_list(
961+
declarations = parse_css_stream_declaration_list(
963962
input,
964963
CssStreamingParser {
965964
state: self.state.clone(),
966965
parent_index: index,
967966
},
968967
);
969-
declarations = (self.state.declaration_count.get() - before) as usize;
970968
} else if prelude.name.eq_ignore_ascii_case("media")
971969
|| prelude.name.eq_ignore_ascii_case("supports")
972970
|| prelude.name.eq_ignore_ascii_case("layer")
@@ -1014,24 +1012,21 @@ mod css_syntax {
10141012
let index = self
10151013
.state
10161014
.begin_rule(CSS_RULE_STYLE, true, self.parent_index, "", prelude, _start.source_location());
1017-
let before = self.state.declaration_count.get();
1018-
parse_css_stream_declaration_list(
1015+
let declarations = parse_css_stream_declaration_list(
10191016
input,
10201017
CssStreamingParser {
10211018
state: self.state.clone(),
10221019
parent_index: index,
10231020
},
10241021
);
1025-
self.state.end_rule(
1026-
index,
1027-
(self.state.declaration_count.get() - before) as usize,
1028-
);
1022+
self.state.end_rule(index, declarations);
10291023
Ok(())
10301024
}
10311025
}
10321026

10331027
struct CssStreamingDeclarationListParser {
10341028
parser: CssStreamingParser,
1029+
direct_declarations: usize,
10351030
}
10361031

10371032
impl<'i> DeclarationParser<'i> for CssStreamingDeclarationListParser {
@@ -1044,7 +1039,11 @@ mod css_syntax {
10441039
input: &mut Parser<'i, 't>,
10451040
declaration_start: &ParserState,
10461041
) -> Result<Self::Declaration, ParseError<'i, Self::Error>> {
1047-
self.parser.parse_value(name, input, declaration_start)
1042+
let parsed = self.parser.parse_value(name, input, declaration_start);
1043+
if matches!(parsed, Ok(CssStreamingBodyItem::Declaration)) {
1044+
self.direct_declarations += 1;
1045+
}
1046+
parsed
10481047
}
10491048
}
10501049

@@ -1082,9 +1081,45 @@ mod css_syntax {
10821081
}
10831082

10841083
impl<'i> QualifiedRuleParser<'i> for CssStreamingDeclarationListParser {
1085-
type Prelude = ();
1084+
type Prelude = &'i str;
10861085
type QualifiedRule = CssStreamingBodyItem;
10871086
type Error = ();
1087+
1088+
fn parse_prelude<'t>(
1089+
&mut self,
1090+
input: &mut Parser<'i, 't>,
1091+
) -> Result<Self::Prelude, ParseError<'i, Self::Error>> {
1092+
let prelude = consume_raw(input);
1093+
if prelude.is_empty() {
1094+
return Err(input.new_error(BasicParseErrorKind::QualifiedRuleInvalid));
1095+
}
1096+
Ok(prelude)
1097+
}
1098+
1099+
fn parse_block<'t>(
1100+
&mut self,
1101+
prelude: Self::Prelude,
1102+
start: &ParserState,
1103+
input: &mut Parser<'i, 't>,
1104+
) -> Result<Self::QualifiedRule, ParseError<'i, Self::Error>> {
1105+
let index = self.parser.state.begin_rule(
1106+
CSS_RULE_STYLE,
1107+
true,
1108+
self.parser.parent_index,
1109+
"",
1110+
prelude,
1111+
start.source_location(),
1112+
);
1113+
let declarations = parse_css_stream_declaration_list(
1114+
input,
1115+
CssStreamingParser {
1116+
state: self.parser.state.clone(),
1117+
parent_index: index,
1118+
},
1119+
);
1120+
self.parser.state.end_rule(index, declarations);
1121+
Ok(CssStreamingBodyItem::Ignored)
1122+
}
10881123
}
10891124

10901125
impl RuleBodyItemParser<'_, CssStreamingBodyItem, ()> for CssStreamingDeclarationListParser {
@@ -1093,23 +1128,27 @@ mod css_syntax {
10931128
}
10941129

10951130
fn parse_qualified(&self) -> bool {
1096-
false
1131+
true
10971132
}
10981133
}
10991134

11001135
fn parse_css_stream_declaration_list<'i>(
11011136
input: &mut Parser<'i, '_>,
11021137
parser: CssStreamingParser,
1103-
) {
1138+
) -> usize {
11041139
let errors = parser.state.errors.clone();
11051140
let first_error = parser.state.first_error.clone();
1106-
let mut body_parser = CssStreamingDeclarationListParser { parser };
1141+
let mut body_parser = CssStreamingDeclarationListParser {
1142+
parser,
1143+
direct_declarations: 0,
1144+
};
11071145
for item in RuleBodyParser::new(input, &mut body_parser) {
11081146
if let Err((error, _)) = item {
11091147
if first_error.get().is_none() { first_error.set(Some(error.location)); }
11101148
errors.set(errors.get() + 1);
11111149
}
11121150
}
1151+
body_parser.direct_declarations
11131152
}
11141153

11151154
fn parse_css_stream_rule_list<'i>(input: &mut Parser<'i, '_>, mut parser: CssStreamingParser) {

‎experiments/WebScene.NativeEngine.Probe/native/webscene_css_stylesheet_sink.h‎

Lines changed: 151 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
#include "webscene_css_matching.h"
44
#include "webscene_css_transitions.h"
55

6+
#include <iterator>
7+
68
namespace webscene_native::css {
79
// Host supplies rule/keyframe storage, media capability inventory and diagnostics.
8-
// This adapter preserves the existing runtime at-rule policy; it is not a full
9-
// implementation of supports, layers, containers or CSS nesting.
10+
// This adapter preserves the existing runtime at-rule policy. CSS nesting is
11+
// expanded before selector compilation; supports, layers, and containers remain
12+
// the bounded implementations documented below.
1013
template<typename Host>
1114
class stylesheet_sink final : public css_syntax_sink {
1215
public:
@@ -48,7 +51,16 @@ class stylesheet_sink final : public css_syntax_sink {
4851

4952
if (kind == css_syntax_style_rule) {
5053
current.prelude = std::string(prelude);
51-
current.children_active = false;
54+
if (const auto* parent = nearest_style_frame(); parent != nullptr
55+
&& !parent->keyframes) {
56+
current.prelude = combine_nested_selectors(
57+
parent->prelude, current.prelude);
58+
owner_.record_feature(
59+
"css", "css-nesting", "supported",
60+
"qualified nested rules with implicit descendant, combinator, and ampersand expansion",
61+
"stylesheet-parser");
62+
}
63+
current.children_active = current.active;
5264
// Two declarations is the common generated-rule shape and
5365
// avoids the former IR's exact-size allocation without
5466
// over-reserving every small rule.
@@ -156,11 +168,33 @@ class stylesheet_sink final : public css_syntax_sink {
156168
inherited_media.push_back(ancestor.media_query);
157169
}
158170
}
159-
owner_.append_parsed_css_style_rule(
160-
std::move(current.prelude),
161-
std::move(current.declarations),
162-
inherited_media,
163-
stylesheet_address_);
171+
std::vector<completed_style_rule> completed;
172+
completed.reserve(1U + current.nested_rules.size());
173+
if (!current.declarations.empty()) {
174+
completed.push_back({
175+
std::move(current.prelude),
176+
std::move(current.declarations),
177+
std::move(inherited_media)});
178+
}
179+
completed.insert(
180+
completed.end(),
181+
std::make_move_iterator(current.nested_rules.begin()),
182+
std::make_move_iterator(current.nested_rules.end()));
183+
if (auto* parent = nearest_style_frame(); parent != nullptr
184+
&& !parent->keyframes) {
185+
parent->nested_rules.insert(
186+
parent->nested_rules.end(),
187+
std::make_move_iterator(completed.begin()),
188+
std::make_move_iterator(completed.end()));
189+
return true;
190+
}
191+
for (auto& rule : completed) {
192+
owner_.append_parsed_css_style_rule(
193+
std::move(rule.selector),
194+
std::move(rule.declarations),
195+
rule.media_queries,
196+
stylesheet_address_);
197+
}
164198
} else if (current.keyframes) {
165199
completed_keyframes_.emplace_back(
166200
std::move(current.prelude),
@@ -177,19 +211,128 @@ class stylesheet_sink final : public css_syntax_sink {
177211
}
178212

179213
private:
214+
struct completed_style_rule final {
215+
std::string selector;
216+
std::vector<css_declaration> declarations;
217+
std::vector<std::string> media_queries;
218+
};
219+
180220
struct frame final {
181221
size_t rule_index{css_syntax_no_parent};
182222
uint32_t kind{css_syntax_style_rule};
183223
std::string prelude;
184224
std::string media_query;
185225
std::vector<css_declaration> declarations;
226+
std::vector<completed_style_rule> nested_rules;
186227
css_opacity_keyframes keyframe_definition;
187228
size_t observed_declarations{0U};
188229
bool active{false};
189230
bool children_active{false};
190231
bool keyframes{false};
191232
};
192233

234+
frame* nearest_style_frame() noexcept
235+
{
236+
for (auto iterator = stack_.rbegin(); iterator != stack_.rend(); ++iterator) {
237+
if (iterator->keyframes) return nullptr;
238+
if (iterator->kind == css_syntax_style_rule) return &*iterator;
239+
}
240+
return nullptr;
241+
}
242+
243+
const frame* nearest_style_frame() const noexcept
244+
{
245+
for (auto iterator = stack_.rbegin(); iterator != stack_.rend(); ++iterator) {
246+
if (iterator->keyframes) return nullptr;
247+
if (iterator->kind == css_syntax_style_rule) return &*iterator;
248+
}
249+
return nullptr;
250+
}
251+
252+
static std::vector<std::string> split_selector_list(std::string_view value)
253+
{
254+
std::vector<std::string> result;
255+
size_t start = 0U;
256+
size_t parentheses = 0U;
257+
size_t brackets = 0U;
258+
char quote = '\0';
259+
auto escaped = false;
260+
for (size_t index = 0U; index <= value.size(); ++index) {
261+
const auto character = index < value.size() ? value[index] : ',';
262+
if (escaped) {
263+
escaped = false;
264+
continue;
265+
}
266+
if (character == '\\') {
267+
escaped = true;
268+
continue;
269+
}
270+
if (quote != '\0') {
271+
if (character == quote) quote = '\0';
272+
continue;
273+
}
274+
if (character == '\'' || character == '"') {
275+
quote = character;
276+
continue;
277+
}
278+
if (character == '(') ++parentheses;
279+
else if (character == ')' && parentheses > 0U) --parentheses;
280+
else if (character == '[') ++brackets;
281+
else if (character == ']' && brackets > 0U) --brackets;
282+
else if (character == ',' && parentheses == 0U && brackets == 0U) {
283+
auto selector = trim_value(value.substr(start, index - start));
284+
if (!selector.empty()) result.push_back(std::move(selector));
285+
start = index + 1U;
286+
}
287+
}
288+
return result;
289+
}
290+
291+
static std::string replace_nesting_selector(
292+
std::string child,
293+
const std::string& parent)
294+
{
295+
size_t offset = 0U;
296+
while ((offset = child.find('&', offset)) != std::string::npos) {
297+
child.replace(offset, 1U, parent);
298+
offset += parent.size();
299+
}
300+
return child;
301+
}
302+
303+
static std::string combine_nested_selectors(
304+
std::string_view parent,
305+
std::string_view child)
306+
{
307+
const auto parents = split_selector_list(parent);
308+
const auto children = split_selector_list(child);
309+
if (parents.empty() || children.empty()) return {};
310+
std::string parent_reference;
311+
if (parents.size() == 1U) {
312+
parent_reference = parents.front();
313+
} else {
314+
parent_reference = ":is(";
315+
for (const auto& selector : parents) {
316+
if (parent_reference.size() > 4U) parent_reference += ", ";
317+
parent_reference += selector;
318+
}
319+
parent_reference += ')';
320+
}
321+
std::string result;
322+
for (const auto& child_selector : children) {
323+
if (!result.empty()) result += ", ";
324+
if (child_selector.find('&') != std::string::npos) {
325+
result += replace_nesting_selector(
326+
child_selector, parent_reference);
327+
} else {
328+
result += parent_reference;
329+
result += ' ';
330+
result += child_selector;
331+
}
332+
}
333+
return result;
334+
}
335+
193336
Host& owner_;
194337
const std::string& stylesheet_address_;
195338
std::vector<frame> stack_;

‎experiments/WebScene.NativeEngine.Probe/tests/css_parser_tests.cpp‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,31 @@ void direct_streaming_sink()
158158
require(parsed.metrics.parser_retained_bytes == 0U, "direct parser stream retains no Rust output");
159159
}
160160

161+
void nested_style_rule_stream()
162+
{
163+
const auto parsed = parse_css_syntax_stylesheet(R"CSS(
164+
.modal, .panel {
165+
position: fixed;
166+
.resizable { position: absolute; }
167+
&.wide { width: 80%; }
168+
> .content { min-width: 0; }
169+
}
170+
)CSS");
171+
require(static_cast<bool>(parsed), parsed.error);
172+
require(parsed.metrics.parse_error_count == 0U,
173+
"valid nested style rules have no syntax errors");
174+
require(parsed.rules.size() == 4U,
175+
"nested style rules are retained in the rule tree");
176+
require(parsed.rules[1].parent_index == 0U
177+
&& parsed.rules[2].parent_index == 0U
178+
&& parsed.rules[3].parent_index == 0U,
179+
"nested style rules point to their style-rule parent");
180+
require(parsed.rules[0].declaration_count == 1U,
181+
"the parent declaration is retained beside nested rules");
182+
require(parsed.declarations.size() == 4U,
183+
"nested declarations are emitted exactly once");
184+
}
185+
161186
} // namespace
162187

163188
int main()
@@ -166,6 +191,7 @@ int main()
166191
stylesheet_structure();
167192
invalid_utf8_is_rejected();
168193
direct_streaming_sink();
194+
nested_style_rule_stream();
169195
std::cout << "CSS parser tests passed\n";
170196
return 0;
171197
}

0 commit comments

Comments
 (0)