@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
|
@@ -20,8 +20,8 @@ use crate::ast::{SfcAst, SfcStyleScope};
|
|
|
20
20
|
use crate::progressive::ProgressiveRegistry;
|
|
21
21
|
use crate::scanner::{scan, ScanResult};
|
|
22
22
|
use crate::theme::{extract_theme_blocks, ThemeRegistry};
|
|
23
|
-
use crate::tokens::utility_to_css;
|
|
24
|
-
use crate::variants::{split_variants, Variant};
|
|
23
|
+
use crate::tokens::{animation_keyframes, utility_to_css};
|
|
24
|
+
use crate::variants::{split_variants, AttrMatch, Variant};
|
|
25
25
|
|
|
26
26
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
27
27
|
pub enum OutputMode {
|
|
@@ -29,11 +29,62 @@ pub enum OutputMode {
|
|
|
29
29
|
Scoped,
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/// A recoverable error raised while emitting CSS from an SFC AST.
|
|
33
|
+
///
|
|
34
|
+
/// This is the precursor error channel (R-RESULT): `emit_sfc_scoped` /
|
|
35
|
+
/// `compile_sfc_scoped` / the per-SFC cache return `Result<String, CompileError>`
|
|
36
|
+
/// so later passes (`@apply` unknown-utility, variant validation) can hard-error
|
|
37
|
+
/// instead of silently dropping. The `aihu-css-compile` binary prints the
|
|
38
|
+
/// `Display` message to stderr and exits non-zero; the TS bridge surfaces it as
|
|
39
|
+
/// a thrown `Error` carrying that message.
|
|
40
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
41
|
+
pub enum CompileError {
|
|
42
|
+
/// An authored `@style` block opened `@theme` with no `{ … }` body.
|
|
43
|
+
MalformedTheme { detail: String },
|
|
44
|
+
/// An `@apply` directive referenced a utility token whose base utility is
|
|
45
|
+
/// not in the table (Task 1.4 — unknown utility hard-errors).
|
|
46
|
+
UnknownApplyUtility { token: String },
|
|
47
|
+
/// An `@apply` inside a `$global` `@style` block used a variant token that
|
|
48
|
+
/// implies `&`/host/relational scoping (Task 1.4 — base utilities allowed in
|
|
49
|
+
/// `$global`, scope-implying variants rejected).
|
|
50
|
+
GlobalApplyVariant { token: String },
|
|
51
|
+
/// A `@style` block failed to parse structurally (R-SHARED-PARSER).
|
|
52
|
+
StyleParse { detail: String },
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
impl std::fmt::Display for CompileError {
|
|
56
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
57
|
+
match self {
|
|
58
|
+
CompileError::MalformedTheme { detail } => {
|
|
59
|
+
write!(f, "malformed @theme block in authored @style: {detail}")
|
|
60
|
+
}
|
|
61
|
+
CompileError::UnknownApplyUtility { token } => {
|
|
62
|
+
write!(f, "unknown utility in @apply: `{token}`")
|
|
63
|
+
}
|
|
64
|
+
CompileError::GlobalApplyVariant { token } => {
|
|
65
|
+
write!(
|
|
66
|
+
f,
|
|
67
|
+
"@apply in a $global @style block may not use the scope-implying \
|
|
68
|
+
variant `{token}` (only base utilities are allowed in $global)"
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
CompileError::StyleParse { detail } => {
|
|
72
|
+
write!(f, "failed to parse @style block: {detail}")
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
impl std::error::Error for CompileError {}
|
|
79
|
+
|
|
32
80
|
/// CSS-escape a class name for use in a selector (`bg-[#fff]` → `bg-\[\#fff\]`).
|
|
33
81
|
fn escape_class(class: &str) -> String {
|
|
34
82
|
let mut out = String::with_capacity(class.len() + 4);
|
|
35
83
|
for c in class.chars() {
|
|
36
|
-
if matches!(
|
|
84
|
+
if matches!(
|
|
85
|
+
c,
|
|
86
|
+
'[' | ']' | '#' | '(' | ')' | '.' | '%' | '/' | ':' | ',' | '@' | '=' | '"'
|
|
87
|
+
) {
|
|
37
88
|
out.push('\\');
|
|
38
89
|
}
|
|
39
90
|
out.push(c);
|
|
@@ -71,7 +122,11 @@ fn emit_token(token: &str, theme: &ThemeRegistry, prog: &ProgressiveRegistry) ->
|
|
|
71
122
|
|
|
72
123
|
// The base (innermost) selector and declaration body.
|
|
73
124
|
let mut selector = class_sel;
|
|
74
|
-
|
|
125
|
+
// Wrapping at-rule (e.g. `@media (min-width: …)` for breakpoints or
|
|
126
|
+
// `@container (min-width: …)` for container queries). Generalized from the
|
|
127
|
+
// old `media: Option<String>` slot so both `@media` and `@container` wrap
|
|
128
|
+
// the rule uniformly: `<at-rule> { <rule> }`.
|
|
129
|
+
let mut at_rule: Option<String> = None;
|
|
75
130
|
let mut dark_cascade = false;
|
|
76
131
|
|
|
77
132
|
for v in &variants {
|
|
@@ -81,13 +136,48 @@ fn emit_token(token: &str, theme: &ThemeRegistry, prog: &ProgressiveRegistry) ->
|
|
|
81
136
|
Variant::SlottedTag(tag) => selector = format!("::slotted({tag}{selector})"),
|
|
82
137
|
Variant::Part(name) => selector = format!("::part({name})"),
|
|
83
138
|
Variant::Pseudo(pc) => selector = format!("{selector}:{pc}"),
|
|
139
|
+
Variant::PseudoElement(pe) => selector = format!("{selector}::{pe}"),
|
|
84
140
|
Variant::ArbitrarySelector(sel) => {
|
|
85
141
|
// `[&>div]:` → substitute `&` for the base selector.
|
|
86
142
|
selector = sel.replace('&', &selector);
|
|
87
143
|
}
|
|
144
|
+
Variant::Group(Some(state)) => {
|
|
145
|
+
// `group-hover:bg-x` → `.group:hover .group-hover\:bg-x`.
|
|
146
|
+
// Prepend a descendant-combinator ancestor selector: the rule
|
|
147
|
+
// applies to the element bearing this class when an ancestor
|
|
148
|
+
// marked `class="group"` is in `:<state>`. Within a shadow root
|
|
149
|
+
// both the marker and the styled element live in the same tree,
|
|
150
|
+
// so the class selectors match per spec §6.3 scoping.
|
|
151
|
+
selector = format!(".group:{state} {selector}");
|
|
152
|
+
}
|
|
153
|
+
Variant::Peer(Some(state)) => {
|
|
154
|
+
// `peer-checked:bg-x` → `.peer:checked ~ .peer-checked\:bg-x`.
|
|
155
|
+
// Prepend a subsequent-sibling-combinator selector: the rule
|
|
156
|
+
// applies when a PRIOR sibling marked `class="peer"` is in
|
|
157
|
+
// `:<state>`. CSS can only look backward to earlier siblings,
|
|
158
|
+
// so `peer` must appear before the styled element in source.
|
|
159
|
+
selector = format!(".peer:{state} ~ {selector}");
|
|
160
|
+
}
|
|
161
|
+
// Bare `group`/`peer` never reach here (they are marker utilities,
|
|
162
|
+
// not variant prefixes); a `None` state is unreachable but handled
|
|
163
|
+
// defensively as a no-op so the base selector is emitted unchanged.
|
|
164
|
+
Variant::Group(None) | Variant::Peer(None) => {}
|
|
165
|
+
// aria-*/data-* attribute variants compile to an attribute selector
|
|
166
|
+
// appended to the base: `aria-checked:` → `.cls[aria-checked="true"]`,
|
|
167
|
+
// `data-[state=open]:` → `.cls[data-state="open"]`. A keyword data-*
|
|
168
|
+
// (`data-active:`) emits a presence selector `[data-active]`.
|
|
169
|
+
Variant::Aria(m) => selector = format!("{selector}{}", attr_selector("aria", m)),
|
|
170
|
+
Variant::Data(m) => selector = format!("{selector}{}", attr_selector("data", m)),
|
|
88
171
|
Variant::Breakpoint(bp) => {
|
|
89
172
|
if let Some(min) = theme.breakpoint(bp) {
|
|
90
|
-
|
|
173
|
+
at_rule = Some(format!("@media (min-width: {min})"));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Container queries wrap the rule in an `@container` at-rule keyed on
|
|
177
|
+
// the container breakpoint scale (mirrors `breakpoint()`).
|
|
178
|
+
Variant::Container(bp) => {
|
|
179
|
+
if let Some(min) = theme.container_breakpoint(bp) {
|
|
180
|
+
at_rule = Some(format!("@container (min-width: {min})"));
|
|
91
181
|
}
|
|
92
182
|
}
|
|
93
183
|
Variant::Dark | Variant::HostContextDark => {
|
|
@@ -112,12 +202,41 @@ fn emit_token(token: &str, theme: &ThemeRegistry, prog: &ProgressiveRegistry) ->
|
|
|
112
202
|
format!("{selector} {{ {body} }}\n")
|
|
113
203
|
};
|
|
114
204
|
|
|
115
|
-
|
|
116
|
-
Some(
|
|
205
|
+
let rule = match at_rule {
|
|
206
|
+
Some(at) => format!("{at} {{\n{rule}}}\n"),
|
|
207
|
+
None => rule,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Hoist the @keyframes an `animate-*` utility depends on as a top-level
|
|
211
|
+
// sibling rule (it cannot live nested inside the selector body). Re-emitting
|
|
212
|
+
// an identical block is idempotent in CSS, so per-occurrence emission is
|
|
213
|
+
// safe. `base` is the variant-stripped class (e.g. `animate-spin`).
|
|
214
|
+
Some(match animation_keyframes(&base) {
|
|
215
|
+
Some(kf) => format!("{rule}{kf}\n"),
|
|
117
216
|
None => rule,
|
|
118
217
|
})
|
|
119
218
|
}
|
|
120
219
|
|
|
220
|
+
/// Build an attribute-selector fragment for an `aria-*`/`data-*` variant.
|
|
221
|
+
///
|
|
222
|
+
/// `attr_selector("aria", Name{checked, true})` → `[aria-checked="true"]`;
|
|
223
|
+
/// `attr_selector("data", NameValue{state, open})` → `[data-state="open"]`;
|
|
224
|
+
/// `attr_selector("data", Name{active, false})` → `[data-active]` (presence).
|
|
225
|
+
pub(crate) fn attr_selector(family: &str, m: &AttrMatch) -> String {
|
|
226
|
+
match m {
|
|
227
|
+
AttrMatch::Name { name, imply_true } => {
|
|
228
|
+
if *imply_true {
|
|
229
|
+
format!("[{family}-{name}=\"true\"]")
|
|
230
|
+
} else {
|
|
231
|
+
format!("[{family}-{name}]")
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
AttrMatch::NameValue { name, value } => {
|
|
235
|
+
format!("[{family}-{name}=\"{value}\"]")
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
121
240
|
/// Emit CSS for a scanned utility set in the given mode.
|
|
122
241
|
pub fn emit(result: &ScanResult, theme: &ThemeRegistry, mode: OutputMode) -> String {
|
|
123
242
|
emit_with_progressive(result, theme, &ProgressiveRegistry::with_builtins(), mode)
|
|
@@ -138,6 +257,10 @@ pub fn emit_with_progressive(
|
|
|
138
257
|
// Flat back-compat: only plain utilities, no variant wrapping.
|
|
139
258
|
if let Some(body) = utility_to_css(token) {
|
|
140
259
|
out.push_str(&format!(".{token} {{ {body} }}\n"));
|
|
260
|
+
if let Some(kf) = animation_keyframes(token) {
|
|
261
|
+
out.push_str(kf);
|
|
262
|
+
out.push('\n');
|
|
263
|
+
}
|
|
141
264
|
}
|
|
142
265
|
}
|
|
143
266
|
OutputMode::Scoped => {
|
|
@@ -152,7 +275,7 @@ pub fn emit_with_progressive(
|
|
|
152
275
|
|
|
153
276
|
/// Compile a full SFC AST to scoped CSS: theme tokens (`:host`-level custom
|
|
154
277
|
/// props) + scanned utility rules + the folded authored `@style` block.
|
|
155
|
-
pub fn emit_sfc_scoped(ast: &SfcAst) -> String {
|
|
278
|
+
pub fn emit_sfc_scoped(ast: &SfcAst) -> Result<String, CompileError> {
|
|
156
279
|
let mut theme = ThemeRegistry::with_aihu_defaults();
|
|
157
280
|
|
|
158
281
|
// Parse @theme directives from the authored style block first so utilities
|
|
@@ -166,52 +289,88 @@ pub fn emit_sfc_scoped(ast: &SfcAst) -> String {
|
|
|
166
289
|
|
|
167
290
|
let result = scan(ast);
|
|
168
291
|
let prog = ProgressiveRegistry::with_builtins();
|
|
169
|
-
let mut out = String::new();
|
|
170
292
|
|
|
171
|
-
//
|
|
172
|
-
|
|
293
|
+
// Build the rule body first — preflight + scanned utilities + folded
|
|
294
|
+
// `@style` — so we can discover which palette `--color-*` tokens it
|
|
295
|
+
// references and register them before emitting the `:host` token block.
|
|
296
|
+
let mut body = String::new();
|
|
173
297
|
|
|
174
|
-
//
|
|
175
|
-
|
|
298
|
+
// Preflight border reset (Tailwind v4 parity). Browsers default
|
|
299
|
+
// `border-style: none`, so a bare `.border { border-width: 1px }` paints
|
|
300
|
+
// nothing. Emit a single one-time rule so every border utility renders a
|
|
301
|
+
// visible solid line. This is one rule per sheet (not per token), so the
|
|
302
|
+
// size impact is negligible; the matching utility wins by specificity.
|
|
303
|
+
body.push_str("*, ::before, ::after { border-style: solid; border-width: 0; }\n");
|
|
176
304
|
|
|
177
|
-
//
|
|
305
|
+
// Scanned utility rules (scoped) — progressive prefixes routed via `prog`.
|
|
306
|
+
body.push_str(&emit_with_progressive(
|
|
307
|
+
&result,
|
|
308
|
+
&theme,
|
|
309
|
+
&prog,
|
|
310
|
+
OutputMode::Scoped,
|
|
311
|
+
));
|
|
312
|
+
|
|
313
|
+
// Fold the authored @style block (minus @theme directives), expanding any
|
|
314
|
+
// `@apply` directives first (Task 1.4). Base utilities inline as
|
|
315
|
+
// declarations; variant tokens lift to nested `&…` rules on the recipe's
|
|
316
|
+
// own selector. Unknown utilities / illegal `$global` variants hard-error.
|
|
178
317
|
if let Some(style) = &ast.style {
|
|
179
|
-
let
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
318
|
+
let stripped = strip_theme_blocks(&style.content)?;
|
|
319
|
+
if !stripped.trim().is_empty() {
|
|
320
|
+
let authored = crate::apply::expand_apply(&stripped, style.scope, &theme)?;
|
|
321
|
+
let authored = authored.trim();
|
|
322
|
+
if !authored.is_empty() {
|
|
323
|
+
match style.scope {
|
|
324
|
+
// Scoped: it already lives in the shadow <style>; pass through.
|
|
325
|
+
SfcStyleScope::Scoped => {
|
|
326
|
+
body.push_str("/* authored @style (scoped) */\n");
|
|
327
|
+
body.push_str(authored);
|
|
328
|
+
body.push('\n');
|
|
329
|
+
}
|
|
330
|
+
// Global ($global): passed through unscoped (edge E6). The
|
|
331
|
+
// compiler hoists this out of the shadow root.
|
|
332
|
+
SfcStyleScope::Global => {
|
|
333
|
+
body.push_str("/* authored @style ($global — unscoped) */\n");
|
|
334
|
+
body.push_str(authored);
|
|
335
|
+
body.push('\n');
|
|
336
|
+
}
|
|
195
337
|
}
|
|
196
338
|
}
|
|
197
339
|
}
|
|
198
340
|
}
|
|
199
341
|
|
|
200
|
-
|
|
342
|
+
// Register the palette tokens the body references (Tailwind ships the full
|
|
343
|
+
// palette in its default theme) so `var(--color-amber-200)` resolves at
|
|
344
|
+
// `:host`. Only the referenced tokens are added — not all 286.
|
|
345
|
+
crate::tokens::register_used_palette(&body, &mut theme);
|
|
346
|
+
|
|
347
|
+
// Theme tokens at :host (now incl. used palette) so var(--color-*) resolves
|
|
348
|
+
// inside the shadow root, then the rule body.
|
|
349
|
+
let mut out = theme.emit_host_tokens();
|
|
350
|
+
out.push_str(&body);
|
|
351
|
+
|
|
352
|
+
Ok(out)
|
|
201
353
|
}
|
|
202
354
|
|
|
203
355
|
/// Remove `@theme { ... }` blocks from style content (they become host tokens,
|
|
204
356
|
/// not raw CSS).
|
|
205
|
-
|
|
357
|
+
///
|
|
358
|
+
/// An `@theme` opener with no `{ … }` body is a malformed authored block and
|
|
359
|
+
/// now hard-errors via [`CompileError`] (R-RESULT) instead of silently keeping
|
|
360
|
+
/// the broken text verbatim — the first real error this `Result` channel
|
|
361
|
+
/// surfaces. Later passes (`@apply`, variant validation) add more variants.
|
|
362
|
+
fn strip_theme_blocks(style_content: &str) -> Result<String, CompileError> {
|
|
206
363
|
let mut out = String::new();
|
|
207
364
|
let mut rest = style_content;
|
|
208
365
|
while let Some(at) = rest.find("@theme") {
|
|
209
366
|
out.push_str(&rest[..at]);
|
|
210
367
|
let after = &rest[at + "@theme".len()..];
|
|
211
368
|
let Some(open) = after.find('{') else {
|
|
212
|
-
// Malformed
|
|
213
|
-
|
|
214
|
-
return
|
|
369
|
+
// Malformed: `@theme` with no `{` body. Hard-error rather than
|
|
370
|
+
// emitting the broken text verbatim.
|
|
371
|
+
return Err(CompileError::MalformedTheme {
|
|
372
|
+
detail: "expected `{` after `@theme`".to_string(),
|
|
373
|
+
});
|
|
215
374
|
};
|
|
216
375
|
let body_start = open + 1;
|
|
217
376
|
let mut depth = 1u32;
|
|
@@ -232,5 +391,5 @@ fn strip_theme_blocks(style_content: &str) -> String {
|
|
|
232
391
|
rest = &after[end + 1..];
|
|
233
392
|
}
|
|
234
393
|
out.push_str(rest);
|
|
235
|
-
out
|
|
394
|
+
Ok(out)
|
|
236
395
|
}
|
|
@@ -5,21 +5,29 @@
|
|
|
5
5
|
//! of utility classes (see tokens.rs); Plan 2 wires the AST scanner; Plan 3
|
|
6
6
|
//! adds variants and progressive features.
|
|
7
7
|
|
|
8
|
+
pub mod apply;
|
|
8
9
|
pub mod ast;
|
|
9
10
|
pub mod cache;
|
|
10
11
|
pub mod emit;
|
|
11
12
|
pub mod features;
|
|
13
|
+
pub mod palette;
|
|
12
14
|
pub mod progressive;
|
|
13
15
|
pub mod scanner;
|
|
16
|
+
pub mod style_parser;
|
|
14
17
|
pub mod theme;
|
|
15
18
|
pub mod tokens;
|
|
16
19
|
pub mod variants;
|
|
17
20
|
|
|
21
|
+
pub use apply::expand_apply;
|
|
18
22
|
pub use ast::{parse_ast, AstError, SfcAst, SfcAttr, SfcNode, SfcStyleScope};
|
|
19
23
|
pub use cache::{hash_ast, CssCache};
|
|
20
|
-
pub use emit::{emit, emit_sfc_scoped, OutputMode};
|
|
24
|
+
pub use emit::{emit, emit_sfc_scoped, CompileError, OutputMode};
|
|
21
25
|
pub use progressive::{ProgressiveFeature, ProgressiveRegistry};
|
|
22
26
|
pub use scanner::{scan, scan_ast, ScanResult};
|
|
27
|
+
pub use style_parser::{
|
|
28
|
+
parse_style, ApplyDirective, AtRule, AtStatement, Declaration, StyleNode, StyleParseError,
|
|
29
|
+
StyleRule, StyleSheet,
|
|
30
|
+
};
|
|
23
31
|
pub use theme::ThemeRegistry;
|
|
24
32
|
pub use variants::{split_variants, Variant};
|
|
25
33
|
|
|
@@ -42,6 +50,11 @@ pub fn compile_classes(classes: &[String]) -> String {
|
|
|
42
50
|
output.push_str(" { ");
|
|
43
51
|
output.push_str(&body);
|
|
44
52
|
output.push_str(" }\n");
|
|
53
|
+
// Hoist the matching @keyframes as a sibling rule (animate-* only).
|
|
54
|
+
if let Some(kf) = tokens::animation_keyframes(class) {
|
|
55
|
+
output.push_str(kf);
|
|
56
|
+
output.push('\n');
|
|
57
|
+
}
|
|
45
58
|
}
|
|
46
59
|
}
|
|
47
60
|
output
|
|
@@ -62,6 +75,6 @@ pub fn compile_sfc(ast: &SfcAst) -> String {
|
|
|
62
75
|
/// `:host`-level theme tokens, variant-resolved utility rules, and the folded
|
|
63
76
|
/// authored `@style` block. This is the production entry consumed by the TS
|
|
64
77
|
/// bridge / `aihu-css-compile --ast-json`.
|
|
65
|
-
pub fn compile_sfc_scoped(ast: &SfcAst) -> String {
|
|
78
|
+
pub fn compile_sfc_scoped(ast: &SfcAst) -> Result<String, CompileError> {
|
|
66
79
|
emit_sfc_scoped(ast)
|
|
67
80
|
}
|