xbintsc 0.5.51 → 0.5.52

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/dist/src/cli/help.d.ts +1 -1
  2. package/dist/src/driver/version.d.ts +1 -1
  3. package/dist/src/driver/version.js +1 -1
  4. package/dist/src/extensions/gui/features.d.ts +9 -6
  5. package/dist/src/extensions/gui/features.js +14 -9
  6. package/dist/src/extensions/gui/features.js.map +1 -1
  7. package/dist/tests/e2e/gui-css-coverage.test.d.ts +9 -0
  8. package/dist/tests/e2e/gui-css-coverage.test.js +333 -0
  9. package/dist/tests/e2e/gui-css-coverage.test.js.map +1 -0
  10. package/dist/tests/e2e/gui-css-features.test.d.ts +6 -0
  11. package/dist/tests/e2e/gui-css-features.test.js +270 -0
  12. package/dist/tests/e2e/gui-css-features.test.js.map +1 -0
  13. package/dist/tests/e2e/gui-effects.test.js +92 -11
  14. package/dist/tests/e2e/gui-effects.test.js.map +1 -1
  15. package/dist/tests/e2e/gui-font-face.test.d.ts +10 -0
  16. package/dist/tests/e2e/gui-font-face.test.js +142 -0
  17. package/dist/tests/e2e/gui-font-face.test.js.map +1 -0
  18. package/dist/tests/e2e/gui-fonts.test.d.ts +10 -0
  19. package/dist/tests/e2e/gui-fonts.test.js +150 -0
  20. package/dist/tests/e2e/gui-fonts.test.js.map +1 -0
  21. package/dist/tests/e2e/gui-helpers.d.ts +5 -0
  22. package/dist/tests/e2e/gui-helpers.js +4 -0
  23. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  24. package/dist/tests/e2e/gui-layout.test.js +3 -2
  25. package/dist/tests/e2e/gui-layout.test.js.map +1 -1
  26. package/dist/tests/e2e/gui-track-sizing.test.d.ts +6 -0
  27. package/dist/tests/e2e/gui-track-sizing.test.js +173 -0
  28. package/dist/tests/e2e/gui-track-sizing.test.js.map +1 -0
  29. package/dist/tests/e2e/gui-transform.test.js +3 -2
  30. package/dist/tests/e2e/gui-transform.test.js.map +1 -1
  31. package/dist/tests/extensions/gui-features.test.js +8 -1
  32. package/dist/tests/extensions/gui-features.test.js.map +1 -1
  33. package/doc/ai/extensions.md +10 -5
  34. package/doc/ai/zh-CN/extensions.md +9 -4
  35. package/doc/gui/animation.md +39 -9
  36. package/doc/gui/build.md +6 -5
  37. package/doc/gui/dom.md +20 -4
  38. package/doc/gui/known-issues.md +31 -23
  39. package/doc/gui/layout.md +30 -7
  40. package/doc/gui/rendering.md +84 -26
  41. package/doc/gui.md +62 -14
  42. package/doc/zh-CN/gui/animation.md +33 -10
  43. package/doc/zh-CN/gui/build.md +5 -5
  44. package/doc/zh-CN/gui/dom.md +16 -3
  45. package/doc/zh-CN/gui/known-issues.md +21 -12
  46. package/doc/zh-CN/gui/layout.md +22 -5
  47. package/doc/zh-CN/gui/rendering.md +61 -20
  48. package/doc/zh-CN/gui.md +53 -15
  49. package/package.json +7 -7
  50. package/runtime/ext_gui/animation_values.cpp +425 -0
  51. package/runtime/ext_gui/animation_values.h +64 -0
  52. package/runtime/ext_gui/css.cpp +172 -21
  53. package/runtime/ext_gui/css.h +75 -4
  54. package/runtime/ext_gui/css_match.cpp +4 -0
  55. package/runtime/ext_gui/css_sheet.cpp +218 -2
  56. package/runtime/ext_gui/document.cpp +42 -192
  57. package/runtime/ext_gui/document.h +25 -12
  58. package/runtime/ext_gui/document_animation.cpp +428 -0
  59. package/runtime/ext_gui/effect_values.cpp +245 -21
  60. package/runtime/ext_gui/gradient_values.cpp +24 -0
  61. package/runtime/ext_gui/grid_values.cpp +23 -0
  62. package/runtime/ext_gui/gui_cursor.cpp +50 -0
  63. package/runtime/ext_gui/gui_events.cpp +16 -4
  64. package/runtime/ext_gui/gui_internal.h +3 -0
  65. package/runtime/ext_gui/image_api.h +3 -1
  66. package/runtime/ext_gui/image_feature.cpp +4 -2
  67. package/runtime/ext_gui/inline_svg.cpp +95 -8
  68. package/runtime/ext_gui/inline_svg.h +5 -1
  69. package/runtime/ext_gui/layout.cpp +3 -2
  70. package/runtime/ext_gui/layout.h +6 -0
  71. package/runtime/ext_gui/layout_flex.cpp +134 -29
  72. package/runtime/ext_gui/layout_flow.cpp +58 -7
  73. package/runtime/ext_gui/layout_flow_internal.h +39 -4
  74. package/runtime/ext_gui/layout_grid.cpp +31 -0
  75. package/runtime/ext_gui/layout_inline.cpp +26 -2
  76. package/runtime/ext_gui/layout_vertical.cpp +196 -0
  77. package/runtime/ext_gui/paint.cpp +67 -9
  78. package/runtime/ext_gui/paint.h +61 -8
  79. package/runtime/ext_gui/paint_backdrop.cpp +211 -0
  80. package/runtime/ext_gui/paint_background.cpp +175 -97
  81. package/runtime/ext_gui/paint_effects.cpp +12 -6
  82. package/runtime/ext_gui/paint_internal.h +27 -0
  83. package/runtime/ext_gui/paint_outline.cpp +44 -0
  84. package/runtime/ext_gui/paint_shadow.cpp +368 -0
  85. package/runtime/ext_gui/renderer.cpp +15 -7
  86. package/runtime/ext_gui/renderer.h +10 -1
  87. package/runtime/ext_gui/renderer_backdrop.cpp +374 -0
  88. package/runtime/ext_gui/renderer_bitmap.cpp +82 -0
  89. package/runtime/ext_gui/renderer_gradient.cpp +51 -17
  90. package/runtime/ext_gui/renderer_image.cpp +6 -1
  91. package/runtime/ext_gui/renderer_internal.h +21 -0
  92. package/runtime/ext_gui/renderer_layer.cpp +27 -1
  93. package/runtime/ext_gui/renderer_text.cpp +2 -1
  94. package/runtime/ext_gui/style.cpp +62 -8
  95. package/runtime/ext_gui/style.h +111 -4
  96. package/runtime/ext_gui/style_cascade.cpp +83 -0
  97. package/runtime/ext_gui/style_internal.h +13 -1
  98. package/runtime/ext_gui/style_length.cpp +92 -0
  99. package/runtime/ext_gui/style_print.cpp +83 -2
  100. package/runtime/ext_gui/style_svg.cpp +40 -0
  101. package/runtime/ext_gui/style_text.cpp +302 -0
  102. package/runtime/ext_gui/style_ui.cpp +150 -0
  103. package/runtime/ext_gui/style_values.cpp +76 -62
  104. package/runtime/ext_gui/style_values_internal.h +23 -0
  105. package/runtime/ext_gui/text.cpp +4 -2
  106. package/runtime/ext_gui/text.h +11 -4
  107. package/runtime/ext_gui/text_cache.cpp +11 -2
  108. package/runtime/ext_gui/text_font_db.cpp +427 -0
  109. package/runtime/ext_gui/text_font_paths.cpp +250 -0
  110. package/runtime/ext_gui/text_font_paths.h +39 -0
  111. package/runtime/ext_gui/text_font_sfnt.cpp +396 -0
  112. package/runtime/ext_gui/text_font_sfnt.h +44 -0
  113. package/runtime/ext_gui/text_fonts.cpp +348 -0
  114. package/runtime/ext_gui/text_fonts.h +78 -0
  115. package/runtime/ext_gui/text_fonts_internal.h +77 -0
  116. package/runtime/ext_gui/text_hb.cpp +63 -179
  117. package/runtime/ext_gui/text_stb.cpp +49 -193
  118. package/runtime/ext_gui/transform.cpp +107 -9
  119. package/runtime/ext_gui/transform.h +5 -0
  120. package/runtime/ext_gui/window.cpp +1 -1
  121. package/src/driver/version.ts +1 -1
  122. package/src/extensions/gui/features.ts +13 -9
@@ -112,6 +112,163 @@ static std::vector<std::string> splitFunctionArgs(const std::string &text) {
112
112
  return args;
113
113
  }
114
114
 
115
+ /** A scalar token (`12px`, `50%`, `1.5`) as a `Length`. Absolute lengths are
116
+ * converted to pixels here; relative units stay symbolic so layout can resolve
117
+ * them against the element that uses them. */
118
+ static Length makeScalarLength(double number, const std::string &rawUnit) {
119
+ Length length;
120
+ std::string unit = xt_css_lower(xt_css_trim(rawUnit));
121
+ length.value = (float)number;
122
+ if (unit.empty() || unit == "px") length.unit = Unit::Px;
123
+ else if (unit == "%") length.unit = Unit::Percent;
124
+ else if (unit == "em") length.unit = Unit::Em;
125
+ else if (unit == "rem") length.unit = Unit::Rem;
126
+ else if (unit == "vw") length.unit = Unit::Vw;
127
+ else if (unit == "vh") length.unit = Unit::Vh;
128
+ else if (unit == "pt") { length.value = (float)number * (96.0f / 72.0f); length.unit = Unit::Px; }
129
+ else if (unit == "pc") { length.value = (float)number * 16.0f; length.unit = Unit::Px; }
130
+ else if (unit == "in") { length.value = (float)number * 96.0f; length.unit = Unit::Px; }
131
+ else if (unit == "cm") { length.value = (float)number * (96.0f / 2.54f); length.unit = Unit::Px; }
132
+ else if (unit == "mm") { length.value = (float)number * (96.0f / 25.4f); length.unit = Unit::Px; }
133
+ else if (unit == "q") { length.value = (float)number * (96.0f / 101.6f); length.unit = Unit::Px; }
134
+ else length.unit = Unit::Invalid;
135
+ return length;
136
+ }
137
+
138
+ static Length parseFunctionLength(const std::string &name, const std::string &inner);
139
+ static bool parseMathExpression(const std::string &text, Length *out);
140
+
141
+ /** Recursive-descent parser for `calc()` arithmetic (`+ - * /`, parentheses,
142
+ * nested `calc`/`min`/`max`/`clamp` and unit-bearing numbers). The expression
143
+ * tree stays unresolved: `xt_style_resolve_absolute` walks it once the
144
+ * containing block and font size are known. */
145
+ struct MathParser {
146
+ explicit MathParser(const std::string &text) : text_(text) {}
147
+
148
+ bool parse(Length *out) {
149
+ if (!parseSum(out)) return false;
150
+ skip();
151
+ return index_ == text_.size();
152
+ }
153
+
154
+ private:
155
+ void skip() {
156
+ while (index_ < text_.size() && xt_is_css_space(text_[index_])) index_++;
157
+ }
158
+
159
+ bool parseSum(Length *out) {
160
+ Length left;
161
+ if (!parseProduct(&left)) return false;
162
+ for (;;) {
163
+ skip();
164
+ if (index_ >= text_.size() || (text_[index_] != '+' && text_[index_] != '-')) break;
165
+ char op = text_[index_++];
166
+ Length right;
167
+ if (!parseProduct(&right)) return false;
168
+ Length node;
169
+ node.unit = Unit::Calc;
170
+ node.op = op;
171
+ node.args.push_back(left);
172
+ node.args.push_back(right);
173
+ left = node;
174
+ }
175
+ *out = left;
176
+ return true;
177
+ }
178
+
179
+ bool parseProduct(Length *out) {
180
+ Length left;
181
+ if (!parseOperand(&left)) return false;
182
+ for (;;) {
183
+ skip();
184
+ if (index_ >= text_.size() || (text_[index_] != '*' && text_[index_] != '/')) break;
185
+ char op = text_[index_++];
186
+ Length right;
187
+ if (!parseOperand(&right)) return false;
188
+ Length node;
189
+ node.unit = Unit::Calc;
190
+ node.op = op;
191
+ node.args.push_back(left);
192
+ node.args.push_back(right);
193
+ left = node;
194
+ }
195
+ *out = left;
196
+ return true;
197
+ }
198
+
199
+ bool parseOperand(Length *out) {
200
+ skip();
201
+ if (index_ >= text_.size()) return false;
202
+ if (text_[index_] == '(') {
203
+ index_++;
204
+ if (!parseSum(out)) return false;
205
+ skip();
206
+ if (index_ >= text_.size() || text_[index_] != ')') return false;
207
+ index_++;
208
+ return true;
209
+ }
210
+ if (std::isalpha((unsigned char)text_[index_])) return parseFunction(out);
211
+ return parseNumber(out);
212
+ }
213
+
214
+ bool parseFunction(Length *out) {
215
+ size_t nameStart = index_;
216
+ while (index_ < text_.size() &&
217
+ (std::isalpha((unsigned char)text_[index_]) || text_[index_] == '-'))
218
+ index_++;
219
+ std::string name = xt_css_lower(text_.substr(nameStart, index_ - nameStart));
220
+ skip();
221
+ if (index_ >= text_.size() || text_[index_] != '(') return false;
222
+ size_t open = index_;
223
+ int depth = 0;
224
+ size_t close = std::string::npos;
225
+ for (; index_ < text_.size(); index_++) {
226
+ if (text_[index_] == '(') depth++;
227
+ else if (text_[index_] == ')') {
228
+ depth--;
229
+ if (depth == 0) {
230
+ close = index_;
231
+ break;
232
+ }
233
+ }
234
+ }
235
+ if (close == std::string::npos) return false;
236
+ std::string inner = text_.substr(open + 1, close - open - 1);
237
+ index_ = close + 1;
238
+ if (name == "calc") return parseMathExpression(inner, out);
239
+ if (name == "min" || name == "max" || name == "clamp" || name == "env") {
240
+ *out = parseFunctionLength(name, inner);
241
+ return out->unit != Unit::Invalid;
242
+ }
243
+ return false;
244
+ }
245
+
246
+ bool parseNumber(Length *out) {
247
+ skip();
248
+ const char *begin = text_.c_str() + index_;
249
+ char *end = nullptr;
250
+ double number = std::strtod(begin, &end);
251
+ if (end == begin || end == nullptr) return false;
252
+ index_ = (size_t)(end - text_.c_str());
253
+ size_t unitStart = index_;
254
+ while (index_ < text_.size() &&
255
+ (std::isalpha((unsigned char)text_[index_]) || text_[index_] == '%'))
256
+ index_++;
257
+ std::string unit = text_.substr(unitStart, index_ - unitStart);
258
+ *out = makeScalarLength(number, unit);
259
+ if (unit.empty()) out->unit = Unit::Number;
260
+ return out->unit != Unit::Invalid;
261
+ }
262
+
263
+ const std::string &text_;
264
+ size_t index_ = 0;
265
+ };
266
+
267
+ static bool parseMathExpression(const std::string &text, Length *out) {
268
+ MathParser parser(text);
269
+ return parser.parse(out);
270
+ }
271
+
115
272
  /** `clamp(min, value, max)`, `min(...)` and `max(...)` over lengths. The
116
273
  * arguments stay unresolved; layout resolves them against the element's em and
117
274
  * the viewport. `env(safe-area-inset-*)` has no notch information in a desktop
@@ -135,8 +292,8 @@ static Length parseFunctionLength(const std::string &name, const std::string &in
135
292
  }
136
293
  result.unit = name == "clamp" ? Unit::Clamp : (name == "min" ? Unit::Min : Unit::Max);
137
294
  for (const std::string &arg : args) {
138
- Length value = xt_css_parse_length(arg);
139
- if (value.unit == Unit::Invalid) {
295
+ Length value;
296
+ if (!parseMathExpression(arg, &value)) {
140
297
  result.unit = Unit::Invalid;
141
298
  result.args.clear();
142
299
  return result;
@@ -163,34 +320,28 @@ Length xt_css_parse_length(const std::string &value) {
163
320
  }
164
321
  size_t open = text.find('(');
165
322
  if (open != std::string::npos && !text.empty() && text.back() == ')') {
166
- std::string name = text.substr(0, open);
323
+ std::string name = xt_css_trim(text.substr(0, open));
324
+ std::string inner = text.substr(open + 1, text.size() - open - 2);
325
+ if (name == "calc") {
326
+ Length parsed;
327
+ if (!parseMathExpression(inner, &parsed)) parsed.unit = Unit::Invalid;
328
+ return parsed;
329
+ }
167
330
  if (name == "clamp" || name == "min" || name == "max" || name == "env") {
168
- return parseFunctionLength(name, text.substr(open + 1, text.size() - open - 2));
331
+ return parseFunctionLength(name, inner);
169
332
  }
170
333
  }
334
+ /* A scalar keeps the historical `px` default for a unitless token; only
335
+ * arithmetic treats `2` as a number. */
171
336
  char *end = nullptr;
172
- float number = strtof(text.c_str(), &end);
173
- if (end == text.c_str()) {
337
+ double number = std::strtod(text.c_str(), &end);
338
+ if (end == text.c_str() || end == nullptr) {
174
339
  length.unit = Unit::Invalid;
175
340
  return length;
176
341
  }
177
342
  std::string unit = end;
178
343
  while (!unit.empty() && xt_is_css_space(unit[0])) unit.erase(unit.begin());
179
- length.value = number;
180
- if (unit.empty() || unit == "px") length.unit = Unit::Px;
181
- else if (unit == "%") length.unit = Unit::Percent;
182
- else if (unit == "em") length.unit = Unit::Em;
183
- else if (unit == "rem") length.unit = Unit::Rem;
184
- else if (unit == "vw") length.unit = Unit::Vw;
185
- else if (unit == "vh") length.unit = Unit::Vh;
186
- else if (unit == "pt") { length.value = number * (96.0f / 72.0f); length.unit = Unit::Px; }
187
- else if (unit == "pc") { length.value = number * 16.0f; length.unit = Unit::Px; }
188
- else if (unit == "in") { length.value = number * 96.0f; length.unit = Unit::Px; }
189
- else if (unit == "cm") { length.value = number * (96.0f / 2.54f); length.unit = Unit::Px; }
190
- else if (unit == "mm") { length.value = number * (96.0f / 25.4f); length.unit = Unit::Px; }
191
- else if (unit == "q") { length.value = number * (96.0f / 101.6f); length.unit = Unit::Px; }
192
- else length.unit = Unit::Invalid;
193
- return length;
344
+ return makeScalarLength(number, unit);
194
345
  }
195
346
 
196
347
  namespace {
@@ -17,14 +17,35 @@
17
17
 
18
18
  namespace xtgui {
19
19
 
20
- enum class Unit { Auto, None, Px, Percent, Em, Rem, Vw, Vh, Number, Clamp, Min, Max, Invalid };
20
+ enum class Unit {
21
+ Auto,
22
+ None,
23
+ Px,
24
+ Percent,
25
+ Em,
26
+ Rem,
27
+ Vw,
28
+ Vh,
29
+ Number,
30
+ Clamp,
31
+ Min,
32
+ Max,
33
+ /** A `calc()` sub-expression: `args[0] <op> args[1]`. Nested arithmetic keeps
34
+ * its operands unresolved, so only the units actually present are resolved
35
+ * against the element (a `calc(100% - 2em)` depends on both the containing
36
+ * block and the font size). */
37
+ Calc,
38
+ Invalid
39
+ };
21
40
 
22
41
  struct Length {
23
42
  float value = 0.0f;
24
43
  Unit unit = Unit::Auto;
25
- /* Arguments of `clamp(...)`, `min(...)` and `max(...)`; empty for a scalar
26
- * length. Kept unresolved so vw/vh/em are resolved against the element that
27
- * uses the length, not the viewport at parse time. */
44
+ /** `+`, `-`, `*` or `/` for `Unit::Calc`; zero otherwise. */
45
+ char op = 0;
46
+ /* Arguments of `clamp(...)`, `min(...)`, `max(...)` and `calc(...)`; empty for
47
+ * a scalar length. Kept unresolved so vw/vh/em are resolved against the
48
+ * element that uses the length, not the viewport at parse time. */
28
49
  std::vector<Length> args = {};
29
50
  };
30
51
 
@@ -79,21 +100,71 @@ struct Rule {
79
100
  std::string media;
80
101
  };
81
102
 
103
+ /** One step of an `@keyframes` rule: a normalised offset (0..1) and the
104
+ * declarations that apply there. */
105
+ struct Keyframe {
106
+ float offset = 0.0f;
107
+ std::vector<Declaration> declarations;
108
+ };
109
+
110
+ /** One `@keyframes <name>` block. Offsets are kept sorted so the animation
111
+ * engine can bracket a progress with two adjacent steps. */
112
+ struct KeyframesRule {
113
+ std::string name;
114
+ std::vector<Keyframe> keyframes;
115
+ };
116
+
117
+ /** One `@font-face` source: a local file URL plus the optional `format(...)`
118
+ * hint. `local(...)` sources are dropped at parse time (the runtime has no
119
+ * name-to-file font lookup for them). */
120
+ struct FontFaceSource {
121
+ std::string url;
122
+ std::string format;
123
+ };
124
+
125
+ /** One author `@font-face` rule. The runtime resolves the sources to a real
126
+ * font file when the document loads; the declared family name is what CSS
127
+ * matches, even when it differs from the font's internal name. */
128
+ struct FontFaceRule {
129
+ std::string family;
130
+ std::vector<FontFaceSource> sources;
131
+ /** `font-weight` descriptor. A range (`100 900`) is kept as min/max; when the
132
+ * descriptor is absent the font's OS/2 weight is used. */
133
+ bool has_weight = false;
134
+ int weight_min = 400;
135
+ int weight_max = 400;
136
+ /** `font-style` descriptor; absent means the font's own italic flag. */
137
+ bool has_italic = false;
138
+ bool italic = false;
139
+ };
140
+
82
141
  struct StyleSheet {
83
142
  std::vector<Rule> rules;
143
+ /** `@keyframes` blocks, in declaration order. */
144
+ std::vector<KeyframesRule> keyframes;
145
+ /** `@font-face` blocks, in declaration order. */
146
+ std::vector<FontFaceRule> font_faces;
84
147
  /* Facts the cascade can act on once per sheet instead of once per element (a
85
148
  * `@media` block, a `::before` rule or a `transition` declaration used to be
86
149
  * discovered by scanning every rule for every node). */
87
150
  bool has_pseudo_elements = false;
88
151
  bool has_hover = false;
89
152
  bool has_focus = false;
153
+ bool has_active = false;
90
154
  bool has_transitions = false;
155
+ bool has_animations = false;
91
156
  };
92
157
 
158
+ /** Look up the last `@keyframes` rule with `name` (later blocks replace
159
+ * earlier ones), or null when the sheet has no such animation. */
160
+ const KeyframesRule *xt_css_find_keyframes(const StyleSheet &sheet, const std::string &name);
161
+
93
162
  /** Dynamic element state consulted by `:hover` / `:focus` while matching. */
94
163
  struct MatchState {
95
164
  const Node *hover = nullptr;
96
165
  const Node *focus = nullptr;
166
+ /** The element currently held down by the primary pointer (`:active`). */
167
+ const Node *active = nullptr;
97
168
  };
98
169
 
99
170
  StyleSheet xt_css_parse_stylesheet(const std::string &text);
@@ -93,6 +93,10 @@ bool matchPseudo(const Node *node, const std::string &pseudo, const MatchState &
93
93
  if (pseudo == "root") return node->parent != nullptr && node->parent->isDocument();
94
94
  if (pseudo == "hover") return isHoverTarget(node, state.hover);
95
95
  if (pseudo == "focus") return node == state.focus;
96
+ /* The engine has no keyboard modality tracking, so focus-visible is the same
97
+ * as focus (a mouse click also shows the focus ring). */
98
+ if (pseudo == "focus-visible") return node == state.focus;
99
+ if (pseudo == "active") return isHoverTarget(node, state.active);
96
100
  if (pseudo == "empty") {
97
101
  for (const std::unique_ptr<Node> &child : node->children) {
98
102
  if (child->isElement()) return false;
@@ -8,6 +8,11 @@
8
8
 
9
9
  #include "css_internal.h"
10
10
 
11
+ #include <algorithm>
12
+ #include <cctype>
13
+ #include <cstring>
14
+ #include <utility>
15
+
11
16
  namespace xtgui {
12
17
 
13
18
  namespace {
@@ -92,6 +97,202 @@ std::string stripImportant(std::string value, bool *important) {
92
97
  return value;
93
98
  }
94
99
 
100
+ /** Strip matching quotes from an at-rule prelude name. */
101
+ std::string unquote(std::string text) {
102
+ text = xt_css_trim(text);
103
+ if (text.size() >= 2 && (text.front() == '"' || text.front() == '\'') &&
104
+ text.back() == text.front()) {
105
+ text = text.substr(1, text.size() - 2);
106
+ }
107
+ return text;
108
+ }
109
+
110
+ bool parseKeyframeOffset(const std::string &text, float *offset) {
111
+ std::string token = xt_css_lower(xt_css_trim(text));
112
+ if (token == "from") {
113
+ *offset = 0.0f;
114
+ return true;
115
+ }
116
+ if (token == "to") {
117
+ *offset = 1.0f;
118
+ return true;
119
+ }
120
+ if (!token.empty() && token.back() == '%') {
121
+ float value = 0.0f;
122
+ if (!xt_css_parse_number(token.substr(0, token.size() - 1), &value)) return false;
123
+ *offset = value / 100.0f;
124
+ return true;
125
+ }
126
+ return false;
127
+ }
128
+
129
+ /** Parse the body of an `@keyframes` block into one rule. A selector list such
130
+ * as `0%, 50%` creates one step per offset with the same declarations. */
131
+ void parseKeyframesBlock(const std::string &block, const std::string &prelude,
132
+ StyleSheet *sheet) {
133
+ KeyframesRule rule;
134
+ rule.name = unquote(prelude);
135
+ if (rule.name.empty()) return;
136
+ size_t index = 0;
137
+ while (index < block.size()) {
138
+ while (index < block.size() && xt_is_css_space(block[index])) index++;
139
+ if (index >= block.size()) break;
140
+ size_t brace = block.find('{', index);
141
+ if (brace == std::string::npos) break;
142
+ size_t close = findBlockEnd(block, brace);
143
+ if (close == std::string::npos) break;
144
+ std::string selectors = block.substr(index, brace - index);
145
+ std::vector<Declaration> declarations =
146
+ xt_css_parse_declarations(block.substr(brace + 1, close - brace - 1));
147
+ for (const std::string &part : xt_css_split_commas(selectors)) {
148
+ float offset = 0.0f;
149
+ if (!parseKeyframeOffset(part, &offset)) continue;
150
+ Keyframe keyframe;
151
+ keyframe.offset = offset;
152
+ keyframe.declarations = declarations;
153
+ rule.keyframes.push_back(std::move(keyframe));
154
+ }
155
+ index = close + 1;
156
+ }
157
+ if (rule.keyframes.empty()) return;
158
+ std::stable_sort(rule.keyframes.begin(), rule.keyframes.end(),
159
+ [](const Keyframe &a, const Keyframe &b) { return a.offset < b.offset; });
160
+ sheet->keyframes.push_back(std::move(rule));
161
+ }
162
+
163
+ /** Locate `name(` in a lower-cased value, returning the index of `(`. A match
164
+ * inside another identifier (`myurl(`) is rejected. */
165
+ size_t findFunction(const std::string &lower, const char *name, size_t from) {
166
+ const size_t length = std::strlen(name);
167
+ size_t pos = lower.find(name, from);
168
+ while (pos != std::string::npos) {
169
+ const bool boundary =
170
+ pos == 0 || !(std::isalnum((unsigned char)lower[pos - 1]) || lower[pos - 1] == '-' ||
171
+ lower[pos - 1] == '_');
172
+ size_t open = pos + length;
173
+ while (open < lower.size() && std::isspace((unsigned char)lower[open])) open++;
174
+ if (boundary && open < lower.size() && lower[open] == '(') return open;
175
+ pos = lower.find(name, pos + length);
176
+ }
177
+ return std::string::npos;
178
+ }
179
+
180
+ /** Extract the argument of a function whose `(` is at `open_paren`. Handles
181
+ * quotes and nested parentheses; trims the result. */
182
+ bool functionArgument(const std::string &text, size_t open_paren, std::string *out,
183
+ size_t *close_paren) {
184
+ int depth = 0;
185
+ char quote = 0;
186
+ for (size_t i = open_paren; i < text.size(); i++) {
187
+ const char c = text[i];
188
+ if (quote != 0) {
189
+ if (c == quote && (i == 0 || text[i - 1] != '\\')) quote = 0;
190
+ continue;
191
+ }
192
+ if (c == '"' || c == '\'') {
193
+ quote = c;
194
+ } else if (c == '(') {
195
+ depth++;
196
+ } else if (c == ')') {
197
+ depth--;
198
+ if (depth == 0) {
199
+ *out = xt_css_trim(text.substr(open_paren + 1, i - open_paren - 1));
200
+ *close_paren = i;
201
+ return true;
202
+ }
203
+ }
204
+ }
205
+ return false;
206
+ }
207
+
208
+ /** Parse the `src` descriptor: keep every `url(...)` source in order and drop
209
+ * `local(...)`. The `format(...)` hint is retained for diagnostics/future
210
+ * format selection; the runtime simply tries the sources in order. */
211
+ void parseFontFaceSources(const std::string &value, FontFaceRule *rule) {
212
+ rule->sources.clear();
213
+ for (const std::string &part : xt_css_split_commas(value)) {
214
+ const std::string lower = xt_css_lower(part);
215
+ const size_t open = findFunction(lower, "url", 0);
216
+ if (open == std::string::npos) continue;
217
+ std::string url;
218
+ size_t close = 0;
219
+ if (!functionArgument(part, open, &url, &close)) continue;
220
+ url = unquote(url);
221
+ if (url.empty()) continue;
222
+ FontFaceSource source;
223
+ source.url = url;
224
+ const size_t format_open = findFunction(lower, "format", close + 1);
225
+ if (format_open != std::string::npos) {
226
+ std::string format;
227
+ size_t format_close = 0;
228
+ if (functionArgument(part, format_open, &format, &format_close)) {
229
+ source.format = unquote(format);
230
+ }
231
+ }
232
+ rule->sources.push_back(std::move(source));
233
+ }
234
+ }
235
+
236
+ /** `font-weight: normal | bold | <number> [<number>]`, with the range clamped. */
237
+ bool parseFontWeightDescriptor(const std::string &value, int *minimum, int *maximum) {
238
+ const std::string text = xt_css_lower(xt_css_trim(value));
239
+ if (text.empty()) return false;
240
+ if (text == "normal") {
241
+ *minimum = 400;
242
+ *maximum = 400;
243
+ return true;
244
+ }
245
+ if (text == "bold") {
246
+ *minimum = 700;
247
+ *maximum = 700;
248
+ return true;
249
+ }
250
+ std::vector<int> weights;
251
+ for (const std::string &token : xt_css_split_whitespace(text)) {
252
+ float number = 0.0f;
253
+ if (!xt_css_parse_number(token, &number)) continue;
254
+ int weight = (int)(number + 0.5f);
255
+ weights.push_back(std::min(1000, std::max(1, weight)));
256
+ }
257
+ if (weights.empty()) return false;
258
+ *minimum = weights.front();
259
+ *maximum = weights.back();
260
+ if (*minimum > *maximum) std::swap(*minimum, *maximum);
261
+ return true;
262
+ }
263
+
264
+ /** Parse one `@font-face` block. A rule without a family name or a local source
265
+ * is ignored. */
266
+ void parseFontFaceBlock(const std::string &block, StyleSheet *sheet) {
267
+ FontFaceRule rule;
268
+ for (const Declaration &declaration : xt_css_parse_declarations(block)) {
269
+ if (declaration.property == "font-family") {
270
+ rule.family = unquote(xt_css_trim(declaration.value));
271
+ } else if (declaration.property == "src") {
272
+ parseFontFaceSources(declaration.value, &rule);
273
+ } else if (declaration.property == "font-weight") {
274
+ int minimum = 400;
275
+ int maximum = 400;
276
+ if (parseFontWeightDescriptor(declaration.value, &minimum, &maximum)) {
277
+ rule.has_weight = true;
278
+ rule.weight_min = minimum;
279
+ rule.weight_max = maximum;
280
+ }
281
+ } else if (declaration.property == "font-style") {
282
+ const std::string text = xt_css_lower(xt_css_trim(declaration.value));
283
+ if (text == "normal") {
284
+ rule.has_italic = true;
285
+ rule.italic = false;
286
+ } else if (text == "italic" || text.rfind("oblique", 0) == 0) {
287
+ rule.has_italic = true;
288
+ rule.italic = true;
289
+ }
290
+ }
291
+ }
292
+ if (rule.family.empty() || rule.sources.empty()) return;
293
+ sheet->font_faces.push_back(std::move(rule));
294
+ }
295
+
95
296
  /** Parse one rule list, carrying the enclosing `@media` prelude. */
96
297
  void parseRules(const std::string &text, const std::string &media, int *order,
97
298
  StyleSheet *sheet) {
@@ -116,9 +317,14 @@ void parseRules(const std::string &text, const std::string &media, int *order,
116
317
  std::string query = xt_css_trim(text.substr(nameEnd, brace - nameEnd));
117
318
  std::string nested = combineMedia(media, query);
118
319
  parseRules(text.substr(brace + 1, close - brace - 1), nested, order, sheet);
320
+ } else if (name == "font-face") {
321
+ parseFontFaceBlock(text.substr(brace + 1, close - brace - 1), sheet);
322
+ } else if (name.size() >= 9 && name.compare(name.size() - 9, 9, "keyframes") == 0) {
323
+ parseKeyframesBlock(text.substr(brace + 1, close - brace - 1),
324
+ text.substr(nameEnd, brace - nameEnd), sheet);
119
325
  }
120
- /* Other block at-rules (`@supports`, `@keyframes`, `@font-face`, …) are
121
- * not part of the engine's subset: ignore the whole block. */
326
+ /* Other block at-rules (`@supports`, …) are not part of the engine's
327
+ * subset: ignore the whole block. */
122
328
  index = close + 1;
123
329
  } else {
124
330
  index = (semi == std::string::npos) ? text.size() : semi + 1;
@@ -152,11 +358,13 @@ void computeSheetFlags(StyleSheet *sheet) {
152
358
  for (const std::string &pseudo : compound.pseudoClasses) {
153
359
  if (pseudo.find("hover") != std::string::npos) sheet->has_hover = true;
154
360
  if (pseudo.find("focus") != std::string::npos) sheet->has_focus = true;
361
+ if (pseudo == "active") sheet->has_active = true;
155
362
  }
156
363
  }
157
364
  }
158
365
  for (const Declaration &declaration : rule.declarations) {
159
366
  if (declaration.property.compare(0, 10, "transition") == 0) sheet->has_transitions = true;
367
+ if (declaration.property.compare(0, 9, "animation") == 0) sheet->has_animations = true;
160
368
  }
161
369
  }
162
370
  }
@@ -183,6 +391,14 @@ std::vector<Declaration> xt_css_parse_declarations(const std::string &text) {
183
391
  return declarations;
184
392
  }
185
393
 
394
+ const KeyframesRule *xt_css_find_keyframes(const StyleSheet &sheet, const std::string &name) {
395
+ /* Later `@keyframes` blocks with the same name replace earlier ones. */
396
+ for (size_t i = sheet.keyframes.size(); i-- > 0;) {
397
+ if (sheet.keyframes[i].name == name) return &sheet.keyframes[i];
398
+ }
399
+ return nullptr;
400
+ }
401
+
186
402
  StyleSheet xt_css_parse_stylesheet(const std::string &text) {
187
403
  StyleSheet sheet;
188
404
  std::string clean = stripComments(text);