@aihu/css-engine 0.2.5 → 0.4.0
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.
- package/README.md +27 -22
- package/crates/aihu-css-core/src/apply.rs +314 -0
- package/crates/aihu-css-core/src/bin/main.rs +8 -7
- package/crates/aihu-css-core/src/cache.rs +8 -5
- package/crates/aihu-css-core/src/emit.rs +195 -36
- package/crates/aihu-css-core/src/lib.rs +15 -2
- package/crates/aihu-css-core/src/palette.rs +301 -0
- package/crates/aihu-css-core/src/style_parser.rs +587 -0
- package/crates/aihu-css-core/src/theme.rs +14 -0
- package/crates/aihu-css-core/src/tokens.rs +1196 -29
- package/crates/aihu-css-core/src/variants.rs +251 -3
- package/crates/aihu-css-core/tests/apply.rs +203 -0
- package/crates/aihu-css-core/tests/apply_regression.rs +150 -0
- package/crates/aihu-css-core/tests/binary_error.rs +61 -0
- package/crates/aihu-css-core/tests/cache.rs +8 -8
- package/crates/aihu-css-core/tests/emit.rs +284 -17
- package/crates/aihu-css-core/tests/parity.rs +274 -0
- package/crates/aihu-css-core/tests/progressive_snapshot.rs +8 -8
- package/crates/aihu-css-core/tests/scoped_snapshot.rs +80 -8
- package/crates/aihu-css-core/tests/snapshots/apply__apply_inside_nested_rule.snap +11 -0
- package/crates/aihu-css-core/tests/snapshots/apply__arbitrary_value_utility_in_apply.snap +8 -0
- package/crates/aihu-css-core/tests/snapshots/apply__arbitrary_value_variant_in_apply.snap +10 -0
- package/crates/aihu-css-core/tests/snapshots/apply__base_utility_inlines_declarations.snap +9 -0
- package/crates/aihu-css-core/tests/snapshots/apply__dark_variant_cascade_in_apply.snap +10 -0
- package/crates/aihu-css-core/tests/snapshots/apply__data_attribute_variant.snap +10 -0
- package/crates/aihu-css-core/tests/snapshots/apply__multi_token_apply.snap +11 -0
- package/crates/aihu-css-core/tests/snapshots/apply__multiple_apply_directives_per_rule.snap +12 -0
- package/crates/aihu-css-core/tests/snapshots/apply__responsive_variant_wraps_media.snap +12 -0
- package/crates/aihu-css-core/tests/snapshots/apply__single_variant_lifts_to_nested_rule.snap +10 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__anchor_snapshot.snap +1 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__popover_snapshot.snap +1 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__text_balance_snapshot.snap +1 -0
- package/crates/aihu-css-core/tests/snapshots/progressive_snapshot__view_transition_snapshot.snap +1 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_animate_spin_hoists_keyframes.snap +25 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_divide_y_nested_rule.snap +24 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_output_for_sfc.snap +1 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_space_y_nested_rule.snap +24 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_transition_and_transform.snap +26 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_with_authored_style_block.snap +6 -2
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__scoped_with_global_style_block.snap +5 -2
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__standard_variants.snap +1 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__theme_default_vs_override.snap +2 -0
- package/crates/aihu-css-core/tests/snapshots/scoped_snapshot__wc_native_variants.snap +1 -0
- package/crates/aihu-css-core/tests/style_parser.rs +257 -0
- package/crates/aihu-css-core/tests/tokens.rs +526 -7
- package/dist/index.d.ts +0 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -18
- package/dist/index.js.map +1 -1
- package/dist/runtime/cn.js +13 -0
- package/dist/runtime/cn.js.map +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -51,7 +51,7 @@ import { defineConfig } from 'vite'
|
|
|
51
51
|
export default defineConfig({
|
|
52
52
|
plugins: [
|
|
53
53
|
viteAihuPlugin({
|
|
54
|
-
css: { shadowMode: 'none' }, // ←
|
|
54
|
+
css: { shadowMode: 'none' }, // ← styles this component + external light-DOM children
|
|
55
55
|
}),
|
|
56
56
|
],
|
|
57
57
|
})
|
|
@@ -70,17 +70,22 @@ bun add @aihu/css-engine
|
|
|
70
70
|
}
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
That's it.
|
|
74
|
-
|
|
73
|
+
That's it. With the default `shadowMode: 'open'`, the scoped rules
|
|
74
|
+
(`.flex{display:flex}`, `.gap-6{gap:1.5rem}`, etc.) fold into each component's
|
|
75
|
+
shadow `<style>`. With `shadowMode: 'none'` (the example above) they instead
|
|
76
|
+
land in `dist/assets/index-*.css` after `bun run build`.
|
|
75
77
|
|
|
76
|
-
####
|
|
78
|
+
#### When do you need `shadowMode: 'none'`?
|
|
77
79
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
80
|
+
Scoped utility classes work fine behind a shadow root (the default `'open'`):
|
|
81
|
+
`@aihu/css-engine` compiles each SFC's classes to a per-component stylesheet and
|
|
82
|
+
folds it into that component's shadow `<style>`. It is scoped by design and does
|
|
83
|
+
**not** rely on the global cascade.
|
|
84
|
+
|
|
85
|
+
Use `'none'` only when you want the utility CSS in the light DOM — for example to
|
|
86
|
+
style external / slotted child elements that live outside your component's shadow
|
|
87
|
+
root, or to emit a single global sheet. (Truly global frameworks like Tailwind,
|
|
88
|
+
UnoCSS, or Pico do require `'none'`, but `@aihu/css-engine` does not.)
|
|
84
89
|
|
|
85
90
|
#### Style packs vs the utility scanner — they are separate
|
|
86
91
|
|
|
@@ -88,7 +93,7 @@ Two distinct things ship in `@aihu/css-engine`:
|
|
|
88
93
|
|
|
89
94
|
| Concern | What you do | Output |
|
|
90
95
|
|---|---|---|
|
|
91
|
-
| **Utility scanner** (this section) | Install package
|
|
96
|
+
| **Utility scanner** (this section) | Install the package (works in any shadow mode; default `'open'` folds into the shadow style) | Per-component scoped rules (or the Vite CSS bundle under `'none'`) |
|
|
92
97
|
| **Theme packs** (color tokens, fonts) | `import "@aihu/css-engine/styles/aihu-graphite.css"` in your entry | `--color-*` / `--font-*` CSS custom properties at `:root` |
|
|
93
98
|
|
|
94
99
|
Theme packs are plain CSS imports — they emit token variables, not utility
|
|
@@ -193,7 +198,7 @@ npm install @aihu/css-engine
|
|
|
193
198
|
bun add @aihu/css-engine
|
|
194
199
|
```
|
|
195
200
|
|
|
196
|
-
<sub><i>Auto-generated against `@aihu/css-engine@0.
|
|
201
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.4.0`.</i></sub>
|
|
197
202
|
|
|
198
203
|
<!-- END_AUTOGEN: install -->
|
|
199
204
|
|
|
@@ -204,12 +209,12 @@ bun add @aihu/css-engine
|
|
|
204
209
|
|
|
205
210
|
| | |
|
|
206
211
|
|---|---|
|
|
207
|
-
| **Version** | `0.
|
|
212
|
+
| **Version** | `0.4.0` |
|
|
208
213
|
| **Tier** | D — Compiler — CSS engine (Tailwind v4 hard fork, WC-native scoped output) |
|
|
209
214
|
| **Published files** | 5 entries |
|
|
210
215
|
| **License** | MIT |
|
|
211
216
|
|
|
212
|
-
<sub><i>Auto-generated against `@aihu/css-engine@0.
|
|
217
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.4.0`.</i></sub>
|
|
213
218
|
|
|
214
219
|
<!-- END_AUTOGEN: stats -->
|
|
215
220
|
|
|
@@ -228,7 +233,7 @@ bun add @aihu/css-engine
|
|
|
228
233
|
| `./runtime/cn` | `./dist/runtime/cn.js` | `—` |
|
|
229
234
|
| `./runtime/progressive` | `./dist/runtime/progressive.js` | `—` |
|
|
230
235
|
|
|
231
|
-
<sub><i>Auto-generated against `@aihu/css-engine@0.
|
|
236
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.4.0`.</i></sub>
|
|
232
237
|
|
|
233
238
|
<!-- END_AUTOGEN: exports -->
|
|
234
239
|
|
|
@@ -243,12 +248,12 @@ bun add @aihu/css-engine
|
|
|
243
248
|
|
|
244
249
|
**Optional dependencies (platform-specific):**
|
|
245
250
|
|
|
246
|
-
- `@aihu/css-engine-darwin-arm64` — `0.1.
|
|
247
|
-
- `@aihu/css-engine-darwin-x64` — `0.1.
|
|
248
|
-
- `@aihu/css-engine-linux-x64-gnu` — `0.1.
|
|
249
|
-
- `@aihu/css-engine-win32-x64-msvc` — `0.1.
|
|
251
|
+
- `@aihu/css-engine-darwin-arm64` — `0.1.3`
|
|
252
|
+
- `@aihu/css-engine-darwin-x64` — `0.1.3`
|
|
253
|
+
- `@aihu/css-engine-linux-x64-gnu` — `0.1.3`
|
|
254
|
+
- `@aihu/css-engine-win32-x64-msvc` — `0.1.3`
|
|
250
255
|
|
|
251
|
-
<sub><i>Auto-generated against `@aihu/css-engine@0.
|
|
256
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.4.0`.</i></sub>
|
|
252
257
|
|
|
253
258
|
<!-- END_AUTOGEN: deps -->
|
|
254
259
|
|
|
@@ -261,7 +266,7 @@ bun add @aihu/css-engine
|
|
|
261
266
|
- [@aihu/compiler](../compiler)
|
|
262
267
|
- [Aihu framework root](../../README.md)
|
|
263
268
|
|
|
264
|
-
<sub><i>Auto-generated against `@aihu/css-engine@0.
|
|
269
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.4.0`.</i></sub>
|
|
265
270
|
|
|
266
271
|
<!-- END_AUTOGEN: see-also -->
|
|
267
272
|
|
|
@@ -272,6 +277,6 @@ bun add @aihu/css-engine
|
|
|
272
277
|
|
|
273
278
|
MIT — see [LICENSE](../../LICENSE).
|
|
274
279
|
|
|
275
|
-
<sub><i>Auto-generated against `@aihu/css-engine@0.
|
|
280
|
+
<sub><i>Auto-generated against `@aihu/css-engine@0.4.0`.</i></sub>
|
|
276
281
|
|
|
277
282
|
<!-- END_AUTOGEN: license -->
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
//! `apply.rs` — `@apply` expansion inside `@style` (Task 1.4, R-APPLY-PARSE).
|
|
2
|
+
//!
|
|
3
|
+
//! Consumes the structured rule tree from [`crate::style_parser`] and replaces
|
|
4
|
+
//! every rule's `@apply <tokens>` directives in place:
|
|
5
|
+
//!
|
|
6
|
+
//! - **Base** tokens (no variant prefix) inline the *same declarations* their
|
|
7
|
+
//! utility class produces ([`utility_to_css`]) directly into the current
|
|
8
|
+
//! rule's declaration list, in source order, before the rule's authored
|
|
9
|
+
//! declarations? No — appended after, so authored declarations can override
|
|
10
|
+
//! (last-wins, matching CSS). Actually `@apply` directives are emitted in
|
|
11
|
+
//! source order relative to the rule body; we splice the inlined declarations
|
|
12
|
+
//! at the point the directive appears. See [`expand_rule`].
|
|
13
|
+
//! - **Variant** tokens (`hover:`, `data-[state=open]:`, `group:`, `md:`,
|
|
14
|
+
//! `dark:`, arbitrary `[&>x]:`, host/slotted/part, …) resolve STRUCTURALLY via
|
|
15
|
+
//! [`crate::variants::resolve_variants`] against `&` (the recipe's own
|
|
16
|
+
//! selector) to a *nested* rule on the current rule — `@apply hover:bg-accent`
|
|
17
|
+
//! inside `.btn {}` becomes `.btn { & { … } &:hover { background: … } }`. We
|
|
18
|
+
//! do NOT call `emit_token` + strip the class selector (that yields the wrong
|
|
19
|
+
//! `.hover\:bg-accent:hover` output — Codex).
|
|
20
|
+
//!
|
|
21
|
+
//! Unknown base utility → [`CompileError::UnknownApplyUtility`]. In a `$global`
|
|
22
|
+
//! block, a variant token that implies `&`/host/relational scoping →
|
|
23
|
+
//! [`CompileError::GlobalApplyVariant`] (base utilities still allowed).
|
|
24
|
+
|
|
25
|
+
use crate::ast::SfcStyleScope;
|
|
26
|
+
use crate::emit::CompileError;
|
|
27
|
+
use crate::style_parser::{
|
|
28
|
+
parse_style, ApplyDirective, AtRule, Declaration, StyleNode, StyleRule, StyleSheet,
|
|
29
|
+
};
|
|
30
|
+
use crate::theme::ThemeRegistry;
|
|
31
|
+
use crate::tokens::utility_to_css;
|
|
32
|
+
use crate::variants::{resolve_variants, split_variants};
|
|
33
|
+
|
|
34
|
+
/// Parse an authored `@style` body, expand every `@apply`, and render back to
|
|
35
|
+
/// CSS. This is the single entry the emitter calls in place of folding the raw
|
|
36
|
+
/// `@style` text verbatim.
|
|
37
|
+
pub fn expand_apply(
|
|
38
|
+
style_content: &str,
|
|
39
|
+
scope: SfcStyleScope,
|
|
40
|
+
theme: &ThemeRegistry,
|
|
41
|
+
) -> Result<String, CompileError> {
|
|
42
|
+
let mut sheet = parse_style(style_content).map_err(|e| CompileError::StyleParse {
|
|
43
|
+
detail: e.to_string(),
|
|
44
|
+
})?;
|
|
45
|
+
expand_sheet(&mut sheet, scope, theme)?;
|
|
46
|
+
Ok(sheet.to_css())
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Expand `@apply` across every node in a parsed sheet, in place.
|
|
50
|
+
fn expand_sheet(
|
|
51
|
+
sheet: &mut StyleSheet,
|
|
52
|
+
scope: SfcStyleScope,
|
|
53
|
+
theme: &ThemeRegistry,
|
|
54
|
+
) -> Result<(), CompileError> {
|
|
55
|
+
for node in &mut sheet.nodes {
|
|
56
|
+
expand_node(node, scope, theme)?;
|
|
57
|
+
}
|
|
58
|
+
Ok(())
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn expand_node(
|
|
62
|
+
node: &mut StyleNode,
|
|
63
|
+
scope: SfcStyleScope,
|
|
64
|
+
theme: &ThemeRegistry,
|
|
65
|
+
) -> Result<(), CompileError> {
|
|
66
|
+
match node {
|
|
67
|
+
StyleNode::Rule(rule) => expand_rule(rule, scope, theme),
|
|
68
|
+
StyleNode::AtRule(at) => expand_at_rule(at, scope, theme),
|
|
69
|
+
StyleNode::AtStatement(_) => Ok(()),
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fn expand_at_rule(
|
|
74
|
+
at: &mut AtRule,
|
|
75
|
+
scope: SfcStyleScope,
|
|
76
|
+
theme: &ThemeRegistry,
|
|
77
|
+
) -> Result<(), CompileError> {
|
|
78
|
+
for node in &mut at.body {
|
|
79
|
+
expand_node(node, scope, theme)?;
|
|
80
|
+
}
|
|
81
|
+
Ok(())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Expand a single rule: inline base-utility declarations, lift variant tokens
|
|
85
|
+
/// into nested rules, recurse into already-nested rules, then drop the consumed
|
|
86
|
+
/// `@apply` directives.
|
|
87
|
+
fn expand_rule(
|
|
88
|
+
rule: &mut StyleRule,
|
|
89
|
+
scope: SfcStyleScope,
|
|
90
|
+
theme: &ThemeRegistry,
|
|
91
|
+
) -> Result<(), CompileError> {
|
|
92
|
+
// Take the directives out; we re-classify their tokens into inline
|
|
93
|
+
// declarations (base) and nested rules (variant).
|
|
94
|
+
let applies = std::mem::take(&mut rule.applies);
|
|
95
|
+
let mut inlined: Vec<Declaration> = Vec::new();
|
|
96
|
+
let mut nested_from_apply: Vec<StyleNode> = Vec::new();
|
|
97
|
+
|
|
98
|
+
for ApplyDirective { tokens } in &applies {
|
|
99
|
+
for token in tokens {
|
|
100
|
+
let (variants, base) = split_variants(token);
|
|
101
|
+
let body = utility_to_css(&base).ok_or_else(|| CompileError::UnknownApplyUtility {
|
|
102
|
+
token: token.clone(),
|
|
103
|
+
})?;
|
|
104
|
+
|
|
105
|
+
if variants.is_empty() {
|
|
106
|
+
// Base utility: inline its declarations into the current rule.
|
|
107
|
+
inlined.extend(parse_declarations(&body));
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Variant token → structural nested rule on `&`.
|
|
112
|
+
let resolved = resolve_variants(&variants, "&", theme);
|
|
113
|
+
|
|
114
|
+
// `$global` blocks have no `&`/host scope: reject scope-implying
|
|
115
|
+
// variants; a bare breakpoint/container/dark variant has no
|
|
116
|
+
// host/relational implication and is allowed.
|
|
117
|
+
if matches!(scope, SfcStyleScope::Global) && resolved.needs_scope {
|
|
118
|
+
return Err(CompileError::GlobalApplyVariant {
|
|
119
|
+
token: token.clone(),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let decls = parse_declarations(&body);
|
|
124
|
+
let nested_rule = StyleRule {
|
|
125
|
+
selector: resolved.selector,
|
|
126
|
+
declarations: decls,
|
|
127
|
+
applies: Vec::new(),
|
|
128
|
+
nested: Vec::new(),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// Wrap in dark-cascade and/or at-rule layers as needed. The dark
|
|
132
|
+
// cascade rewrites the selector to the Firefox-safe gate; the
|
|
133
|
+
// at-rule (media/container) wraps the whole thing.
|
|
134
|
+
let mut node = if resolved.dark_cascade {
|
|
135
|
+
dark_cascade_node(nested_rule)
|
|
136
|
+
} else {
|
|
137
|
+
StyleNode::Rule(nested_rule)
|
|
138
|
+
};
|
|
139
|
+
if let Some(at) = resolved.at_rule {
|
|
140
|
+
let (name, prelude) = split_at_prelude(&at);
|
|
141
|
+
node = StyleNode::AtRule(AtRule {
|
|
142
|
+
name,
|
|
143
|
+
prelude,
|
|
144
|
+
body: vec![node],
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
nested_from_apply.push(node);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Inlined base declarations come first (so the rule's own authored
|
|
152
|
+
// declarations, appended after, win on conflict — CSS last-declaration
|
|
153
|
+
// wins). Prepend to preserve that ordering.
|
|
154
|
+
if !inlined.is_empty() {
|
|
155
|
+
let mut decls = inlined;
|
|
156
|
+
decls.append(&mut rule.declarations);
|
|
157
|
+
rule.declarations = decls;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Variant-derived nested rules go before any authored nested nodes so the
|
|
161
|
+
// expanded output reads `&:hover {…}` right after the base declarations.
|
|
162
|
+
if !nested_from_apply.is_empty() {
|
|
163
|
+
nested_from_apply.append(&mut rule.nested);
|
|
164
|
+
rule.nested = nested_from_apply;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Recurse into nested rules (authored or expanded — expanded ones carry no
|
|
168
|
+
// further `@apply`, so this is a no-op for them but keeps the walk uniform).
|
|
169
|
+
for node in &mut rule.nested {
|
|
170
|
+
expand_node(node, scope, theme)?;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
Ok(())
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Build the Firefox-safe dark-cascade node for a resolved variant rule. Mirrors
|
|
177
|
+
/// the gate the emitter uses (`emit_token`): the rule applies only under
|
|
178
|
+
/// `:host([data-theme="dark"])` or `:root.dark`. The nested `&` in the resolved
|
|
179
|
+
/// selector is expanded against each gate prefix.
|
|
180
|
+
fn dark_cascade_node(rule: StyleRule) -> StyleNode {
|
|
181
|
+
// `rule.selector` is the resolved selector starting from `&` (e.g. `&` for a
|
|
182
|
+
// bare `dark:`, or `&:hover` for `dark:hover:`). Compose the two gated
|
|
183
|
+
// selectors by substituting the host/root prefix for the leading `&`.
|
|
184
|
+
let sel = rule.selector;
|
|
185
|
+
let host = sel.replacen('&', ":host([data-theme=\"dark\"]) &", 1);
|
|
186
|
+
let root = sel.replacen('&', ":root.dark &", 1);
|
|
187
|
+
StyleNode::Rule(StyleRule {
|
|
188
|
+
selector: format!("{host}, {root}"),
|
|
189
|
+
declarations: rule.declarations,
|
|
190
|
+
applies: Vec::new(),
|
|
191
|
+
nested: Vec::new(),
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/// Split a wrapping at-rule string (`@media (min-width: 600px)`) into its name
|
|
196
|
+
/// (`@media`) and prelude (`(min-width: 600px)`).
|
|
197
|
+
fn split_at_prelude(at: &str) -> (String, String) {
|
|
198
|
+
let at = at.trim();
|
|
199
|
+
match at.find(char::is_whitespace) {
|
|
200
|
+
Some(i) => (at[..i].to_string(), at[i..].trim().to_string()),
|
|
201
|
+
None => (at.to_string(), String::new()),
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// Split a utility's emitted CSS body (`"display: flex; align-items: center;"`)
|
|
206
|
+
/// into individual [`Declaration`]s. The bodies the token table produces are
|
|
207
|
+
/// simple `prop: value;` lists, but values can contain `:` (e.g.
|
|
208
|
+
/// `cubic-bezier(...)`) and `;` inside parens — so split on top-level `;`/`:`
|
|
209
|
+
/// honouring parens/brackets/strings (reusing the same discipline as the parser).
|
|
210
|
+
fn parse_declarations(body: &str) -> Vec<Declaration> {
|
|
211
|
+
let mut out = Vec::new();
|
|
212
|
+
for chunk in split_top_level_semicolons(body) {
|
|
213
|
+
let chunk = chunk.trim();
|
|
214
|
+
if chunk.is_empty() {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if let Some((prop, value)) = split_first_colon(chunk) {
|
|
218
|
+
out.push(Declaration {
|
|
219
|
+
prop: prop.trim().to_string(),
|
|
220
|
+
value: value.trim().to_string(),
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
// A chunk with no top-level `:` (e.g. a nested `& > * + * { … }` body
|
|
224
|
+
// from `divide-*`/`space-*`) is not a flat declaration; those utilities
|
|
225
|
+
// are not used via `@apply` in the current recipes, and emitting them as
|
|
226
|
+
// a raw declaration would be wrong. We drop such non-declaration chunks
|
|
227
|
+
// here rather than mis-emit; if a recipe needs them, they should be
|
|
228
|
+
// authored directly (R-NO-PREMIGRATION-BREAK regression guards this).
|
|
229
|
+
}
|
|
230
|
+
out
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// Split on top-level `;` (not inside `(...)`, `[...]`, or strings).
|
|
234
|
+
fn split_top_level_semicolons(body: &str) -> Vec<&str> {
|
|
235
|
+
let bytes = body.as_bytes();
|
|
236
|
+
let mut out = Vec::new();
|
|
237
|
+
let mut start = 0usize;
|
|
238
|
+
let mut paren = 0u32;
|
|
239
|
+
let mut bracket = 0u32;
|
|
240
|
+
let mut i = 0usize;
|
|
241
|
+
while i < bytes.len() {
|
|
242
|
+
match bytes[i] {
|
|
243
|
+
b'"' | b'\'' => {
|
|
244
|
+
let q = bytes[i];
|
|
245
|
+
i += 1;
|
|
246
|
+
while i < bytes.len() {
|
|
247
|
+
if bytes[i] == b'\\' {
|
|
248
|
+
i += 2;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
if bytes[i] == q {
|
|
252
|
+
i += 1;
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
i += 1;
|
|
256
|
+
}
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
b'(' => paren += 1,
|
|
260
|
+
b')' => paren = paren.saturating_sub(1),
|
|
261
|
+
b'[' => bracket += 1,
|
|
262
|
+
b']' => bracket = bracket.saturating_sub(1),
|
|
263
|
+
b';' if paren == 0 && bracket == 0 => {
|
|
264
|
+
out.push(&body[start..i]);
|
|
265
|
+
start = i + 1;
|
|
266
|
+
}
|
|
267
|
+
_ => {}
|
|
268
|
+
}
|
|
269
|
+
i += 1;
|
|
270
|
+
}
|
|
271
|
+
if start < body.len() {
|
|
272
|
+
out.push(&body[start..]);
|
|
273
|
+
}
|
|
274
|
+
out
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// Split a declaration chunk on its first top-level `:` (none inside
|
|
278
|
+
/// `(...)`/`[...]`/strings).
|
|
279
|
+
fn split_first_colon(chunk: &str) -> Option<(&str, &str)> {
|
|
280
|
+
let bytes = chunk.as_bytes();
|
|
281
|
+
let mut paren = 0u32;
|
|
282
|
+
let mut bracket = 0u32;
|
|
283
|
+
let mut i = 0usize;
|
|
284
|
+
while i < bytes.len() {
|
|
285
|
+
match bytes[i] {
|
|
286
|
+
b'"' | b'\'' => {
|
|
287
|
+
let q = bytes[i];
|
|
288
|
+
i += 1;
|
|
289
|
+
while i < bytes.len() {
|
|
290
|
+
if bytes[i] == b'\\' {
|
|
291
|
+
i += 2;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if bytes[i] == q {
|
|
295
|
+
i += 1;
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
i += 1;
|
|
299
|
+
}
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
b'(' => paren += 1,
|
|
303
|
+
b')' => paren = paren.saturating_sub(1),
|
|
304
|
+
b'[' => bracket += 1,
|
|
305
|
+
b']' => bracket = bracket.saturating_sub(1),
|
|
306
|
+
b':' if paren == 0 && bracket == 0 => {
|
|
307
|
+
return Some((&chunk[..i], &chunk[i + 1..]));
|
|
308
|
+
}
|
|
309
|
+
_ => {}
|
|
310
|
+
}
|
|
311
|
+
i += 1;
|
|
312
|
+
}
|
|
313
|
+
None
|
|
314
|
+
}
|
|
@@ -48,13 +48,14 @@ fn main() {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
let css = if ast_mode {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
|
|
51
|
+
let ast = aihu_css_core::parse_ast(&buf).unwrap_or_else(|e| {
|
|
52
|
+
eprintln!("aihu-css-compile: {e}");
|
|
53
|
+
std::process::exit(1);
|
|
54
|
+
});
|
|
55
|
+
aihu_css_core::compile_sfc_scoped(&ast).unwrap_or_else(|e| {
|
|
56
|
+
eprintln!("aihu-css-compile: {e}");
|
|
57
|
+
std::process::exit(1);
|
|
58
|
+
})
|
|
58
59
|
} else {
|
|
59
60
|
let classes: Vec<String> = buf
|
|
60
61
|
.lines()
|
|
@@ -11,7 +11,7 @@ use std::collections::HashMap;
|
|
|
11
11
|
use std::hash::{Hash, Hasher};
|
|
12
12
|
|
|
13
13
|
use crate::ast::{SfcAst, SfcAttr, SfcNode, SfcStyleScope};
|
|
14
|
-
use crate::emit::emit_sfc_scoped;
|
|
14
|
+
use crate::emit::{emit_sfc_scoped, CompileError};
|
|
15
15
|
|
|
16
16
|
/// An in-process compilation cache. Construct one per dev session / build run.
|
|
17
17
|
#[derive(Debug, Default)]
|
|
@@ -32,16 +32,19 @@ impl CssCache {
|
|
|
32
32
|
/// Compile an SFC, returning a cached result on an unchanged-input hit.
|
|
33
33
|
/// `theme_version` participates in the key so a theme change invalidates
|
|
34
34
|
/// every entry.
|
|
35
|
-
|
|
35
|
+
///
|
|
36
|
+
/// On a compile error (R-RESULT) the error propagates and nothing is
|
|
37
|
+
/// cached, so a later fixed input re-runs the full compile path.
|
|
38
|
+
pub fn compile(&mut self, ast: &SfcAst, theme_version: u64) -> Result<String, CompileError> {
|
|
36
39
|
let key = hash_ast(ast, theme_version);
|
|
37
40
|
if let Some(cached) = self.entries.get(&key) {
|
|
38
41
|
self.hits += 1;
|
|
39
|
-
return cached.clone();
|
|
42
|
+
return Ok(cached.clone());
|
|
40
43
|
}
|
|
41
44
|
self.recompiles += 1;
|
|
42
|
-
let css = emit_sfc_scoped(ast)
|
|
45
|
+
let css = emit_sfc_scoped(ast)?;
|
|
43
46
|
self.entries.insert(key, css.clone());
|
|
44
|
-
css
|
|
47
|
+
Ok(css)
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
/// Total full recompiles since construction.
|