velox-sfc 0.1.2

Single-file component compiler for the Velox UI framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
use std::path::Path;

use crate::sfc::Sfc;

/// Extremely simple stub codegen that emits a Rust module with raw blocks.
/// Later, you'll compile `<template>` into VNodes and `<style>` into native styles.
pub fn to_stub_rs(sfc: &Sfc, component_name: &str) -> String {
    to_stub_rs_inner(sfc, component_name, None, true)
}

/// Same as `to_stub_rs` but accepts an optional base path for import resolution.
/// When provided, relative imports are resolved from this path instead of CWD.
///
/// The output is wrapped in `pub mod {name} { ... }`, suitable for `include!()`
/// in a standalone build script context.
pub fn to_stub_rs_with_base(sfc: &Sfc, component_name: &str, base_path: Option<&Path>) -> String {
    to_stub_rs_inner(sfc, component_name, base_path, true)
}

/// Same as `to_stub_rs_with_base` but emits the module body *without* the
/// `pub mod {name} { ... }` wrapper. Use this when the caller already manages
/// module declarations (e.g., via `#[path]` attributes in a component tree).
pub fn to_stub_rs_unwrapped(sfc: &Sfc, component_name: &str, base_path: Option<&Path>) -> String {
    to_stub_rs_inner(sfc, component_name, base_path, false)
}

fn to_stub_rs_inner(
    sfc: &Sfc,
    component_name: &str,
    base_path: Option<&Path>,
    wrap_module: bool,
) -> String {
    let t = sfc
        .template
        .as_ref()
        .map(|b| b.content.as_str())
        .unwrap_or("");
    let ss = sfc
        .script_setup
        .as_ref()
        .map(|b| b.content.as_str())
        .unwrap_or("");
    let s = sfc
        .script
        .as_ref()
        .map(|b| b.content.as_str())
        .unwrap_or("");
    let st = sfc.style.as_ref().map(|b| b.content.as_str()).unwrap_or("");

    let name = sanitize_ident(component_name);
    let open = "{";
    let close = "}";

    // Create resolver for component imports using the provided base path.
    // If no base path is given, fall back to the current working directory.
    let resolver_base = base_path
        .map(|p| p.to_path_buf())
        .unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
    let mut resolver = crate::component_resolver::ComponentResolver::new(resolver_base);
    if let Some(script_setup) = &sfc.script_setup {
        resolver.parse_imports(&script_setup.content);
    }

    // NOTE: We intentionally do NOT generate `pub mod {name};` declarations here.
    // The caller (e.g., compile_component_tree in veloxc) is responsible for
    // generating `#[path = "..."] pub mod ...;` declarations with correct file paths.
    // Generating bare `pub mod` here would conflict with the caller's declarations
    // and cause "file not found for module" errors.

    // A parent component may call `{child}::render_with_callbacks(...)` whenever
    // it attaches an `@event` to a component tag, so every component must expose
    // the emit infrastructure (thread-local callback registry) unconditionally.
    let needs_emit_infra = true;

    // Scoped CSS: when the `<style>` block is declared `scoped`, this component
    // gets a unique `data-v-<hash>` scope id and every CSS selector is prefixed
    // with the matching attribute selector (e.g. `.header` becomes
    // `.header[data-v-abc1ef23]`), confining the rule to this component's
    // elements. The scope id is also exposed as `SCOPE_ID` so the caller can tag
    // rendered VNodes with the same attribute. Unscoped styles pass through
    // unchanged and apply globally.
    let (processed_style, scope_id) = match sfc.style.as_ref() {
        Some(style) if is_scoped(style) => {
            let scope_id = generate_scope_id(component_name);
            (scope_css(st, &scope_id), Some(scope_id))
        }
        _ => (st.to_string(), None),
    };
    let processed_template = t.to_string();

    // Indent level: 4 spaces when wrapped in a module, 0 when unwrapped.
    let indent = if wrap_module { "    " } else { "" };
    let inner_indent = if wrap_module { "        " } else { "    " };

    // Build output manually to allow embedding script_setup as raw Rust.
    let mut out = String::new();
    out.push_str("// @generated by velox-sfc (stub)\n");
    // Always emit inner #![allow] — works both at file-level and inside pub mod {} blocks.
    // When the CLI wraps this in pub mod {name} { ... }, the #![allow] applies to the module body.
    // `unused_variables` covers generated functions whose `state` param is unused
    // in presentational components and v-for index variables in Resolve mode. The
    // clippy allows cover common patterns in generated render code (object-class
    // ternaries, stringified value props, duplicated descendant #[path] modules).
    out.push_str(
        "#![allow(clippy::vec_init_then_push, clippy::arc_with_non_send_sync, \
         clippy::blocks_in_conditions, clippy::duplicate_mod, \
         clippy::match_single_binding, clippy::needless_borrows_for_generic_args, \
         clippy::useless_format, unused_variables)]\n",
    );

    // The component module opens BEFORE the emit infrastructure, so the
    // infrastructure is part of it rather than a sibling of it. Both halves of
    // the emit path reach it from inside the module — `script_rs::emit` as
    // `super::emit`, `render_with_callbacks` as a bare `set_emit_callbacks` —
    // and neither path resolves when the registry sits one level out. The
    // helper was written for the unwrapped form (where the file IS the module,
    // so the two levels are the same one) and the wrapped form never compiled a
    // component that emitted.
    if wrap_module {
        out.push_str(&format!("pub mod {} {}\n", name, open));
    }

    // If any component in the template uses @event, we need the emit module
    if needs_emit_infra {
        out.push_str(&format!("{indent}use std::cell::RefCell;\n"));
        out.push_str(&format!("{indent}use std::collections::HashMap;\n\n"));
        // Emit infrastructure is already indented for module-level (4 spaces).
        // When unwrapped, strip that indentation.
        let emit_code = generate_emit_infrastructure();
        if wrap_module {
            out.push_str(&emit_code);
        } else {
            for line in emit_code.lines() {
                let stripped = line.strip_prefix("    ").unwrap_or(line);
                out.push_str(stripped);
                out.push('\n');
            }
        }
        out.push('\n');
    }

    out.push_str(&format!(
        "{indent}pub const TEMPLATE: &str = r#\"{}\"#;\n",
        escape_raw_string(&processed_template)
    ));
    out.push_str(&format!(
        "{indent}pub const SCRIPT_SETUP: &str = r#\"{}\"#;\n",
        escape_raw_string(ss)
    ));
    out.push_str(&format!(
        "{indent}pub const SCRIPT: &str = r#\"{}\"#;\n",
        escape_raw_string(s)
    ));
    out.push_str(&format!(
        "{indent}pub const STYLE: &str = r#\"{}\"#;\n",
        escape_raw_string(&processed_style)
    ));
    // Emit the component's scope id (empty for unscoped components) so callers
    // can tag rendered VNodes with the same `data-v-*` attribute the scoped
    // selectors rely on.
    out.push_str(&format!(
        "{indent}pub const SCOPE_ID: &str = \"{}\";\n",
        scope_id.unwrap_or_default()
    ));
    if !ss.is_empty() {
        out.push_str(&format!("{indent}pub mod script_rs {{\n{inner_indent}#![allow(unused_variables, unused_imports, unused_mut, unused_assignments, clippy::new_without_default)]\n"));
        out.push_str(&format!("{inner_indent}use super::*;\n"));

        // If this component itself needs emit infrastructure (parent uses @event on it),
        // add the callback type and emit helper
        if ss.contains("define_emits") || ss.contains("emit(") {
            let helpers = generate_component_emit_helpers();
            // helpers are indented for module-level; adjust for inner_indent
            for line in helpers.lines() {
                // Strip the 8-space prefix that was designed for the wrapped format
                let stripped = line
                    .strip_prefix("        ")
                    .unwrap_or(line.strip_prefix("    ").unwrap_or(line));
                out.push_str(&format!("{inner_indent}{stripped}\n"));
            }
        }

        // Insert user code as-is, BUT skip import lines (e.g., `import X from 'Y.vx'`).
        // Import lines are a Velox DSL, not valid Rust. They are already stored
        // in the SCRIPT_SETUP constant above. The caller handles module declarations
        // for imported components via #[path] attributes.
        for line in ss.lines() {
            if is_import_line(line) {
                continue;
            }
            out.push_str(&format!("{inner_indent}{line}\n"));
        }

        // Generate v-model setter methods on the State struct.
        // Parse the template to find v-model directives, then emit setter methods
        // that the event dispatch system can call (e.g., __vmodel_set_counter).
        if let Some(template_block) = &sfc.template
            && let Ok(template_nodes) =
                crate::template_parse::parse_template_to_ast(&template_block.content)
        {
            let vmodels = crate::template_codegen::collect_vmodel_expressions(&template_nodes);
            if !vmodels.is_empty() {
                // The setters are `&self` methods, so they go in an `impl State`
                // block — a second one is fine, the script's own `impl` has
                // already been emitted above. They used to be appended after it,
                // which made them free functions: `state.__vmodel_set_draft(p)`
                // in the generated dispatcher then had no method to call (E0599)
                // and no `v-model` could compile at all.
                let setters = crate::template_codegen::generate_vmodel_setters(&vmodels);
                out.push_str(&format!("{inner_indent}impl State {{\n"));
                for line in setters.lines() {
                    out.push_str(&format!("{inner_indent}    {line}\n"));
                }
                out.push_str(&format!("{inner_indent}}}\n"));
            }
        }
        out.push_str(&format!("{indent}}}\n"));
    }

    // The name a parent writes this component's props with.
    //
    // A component that declares `pub struct Props` gets that struct, so a parent
    // builds it as a struct literal and `rustc` checks every field it binds: a
    // missing prop is a compile error rather than a value nobody asked for. A
    // component that declares no `Props` gets an empty stand-in, so a parent
    // that binds nothing to it still compiles, and one that binds something
    // cannot.
    out.push_str(&generate_props_arg(ss, indent));

    // Always generate render_with_callbacks so a parent can pass event callbacks
    // regardless of whether this component declares emits.
    let render_cb = generate_render_with_callbacks_inside_module();

    // The template has 4-space indentation; adjust to indent level
    for line in render_cb.lines() {
        let stripped = line.strip_prefix("    ").unwrap_or(line);
        out.push_str(&format!("{indent}{stripped}\n"));
    }

    // The slots-only entry point, for a parent that passes slot content and no
    // `@event`. Emitted unconditionally, and under its own name: Rust has no
    // overloading, so sharing `render_with_slots` with the four-argument
    // variant above made the two definitions collide (E0428) in every component
    // whose template has a `<slot>`, while gating it on `t.contains("<slot")`
    // left a parent calling it against a child without one with no such
    // function to call (E0599). One name per arity, always present, is the only
    // arrangement where the call site can be generated without knowing what the
    // child's template contains.
    let render_slots = generate_render_with_slots_only_inside_module();
    for line in render_slots.lines() {
        let stripped = line.strip_prefix("    ").unwrap_or(line);
        out.push_str(&format!("{indent}{stripped}\n"));
    }

    if wrap_module {
        out.push_str(close);
        out.push('\n');
    }
    out
}

/// Check if a line is a Velox component import statement.
/// Matches patterns like:
///   import MyButton from './components/Button.vx'
///   import { Button, Card } from './components.vx'
fn is_import_line(line: &str) -> bool {
    let trimmed = line.trim();
    trimmed.starts_with("import ") && trimmed.contains(" from ")
}

/// True when the `<style>` block is declared `scoped`, which requires
/// attribute-scoped selectors and a per-component scope id.
pub fn is_scoped(style: &crate::sfc::StyleBlock) -> bool {
    style.attrs.iter().any(|a| a.name == "scoped")
}

/// Generate a unique, stable scope id for a component (Vue-style
/// `data-v-<hash>`). The 32-bit FNV-1a hash of the component name yields a
/// distinct id per component and is deterministic, so generated code is
/// reproducible and golden-file stable.
pub fn generate_scope_id(component_name: &str) -> String {
    let mut hash: u32 = 0x811c9dc5;
    for b in component_name.bytes() {
        hash ^= u32::from(b);
        hash = hash.wrapping_mul(0x01000193);
    }
    format!("data-v-{:08x}", hash)
}

/// Rewrite a CSS stylesheet so every selector compound carries the scope
/// attribute, preserving combinators between compounds:
///
/// ```css
/// .header h1 { ... }   ->  .header[data-v-x] h1[data-v-x] { ... }
/// div > .card { ... }  ->  div[data-v-x] > .card[data-v-x] { ... }
/// h1, .a { ... }       ->  h1[data-v-x], .a[data-v-x] { ... }
/// ```
///
/// Declaration blocks are copied verbatim. `@keyframes` blocks keep their
/// inner keyframe selectors (`from`/`to`/percent offsets) unscoped, while
/// `@media` preludes pass through and only their inner style-rule selectors
/// are scoped. Appending the attribute to every compound confines the rule to
/// elements that all belong to this component — every element rendered from
/// this component's template is tagged with the same `data-v-*` attribute, so
/// a rule like `.header h1[data-v-*]` could otherwise leak across component
/// boundaries via an un-tagged ancestor.
///
/// CSS comments are replaced by a space before anything else happens (see
/// [`strip_css_comments`]). Without that, a comment between two rules is glued
/// onto the front of the next rule's prelude, and the per-compound tokenizer
/// then appends the scope attribute to the comment's own `*/` as though it were
/// a compound selector: `.dark .app` was emitted as
/// `[data-v-x] .dark[data-v-x] .app[data-v-x]`, a three-part descendant
/// selector requiring a scope-tagged ancestor strictly above the `.dark`
/// carrier. The carrier is the tree root, whose ancestor list is empty, so
/// `match_prefix` finds no candidate and the rule can never match.
pub fn scope_css(css: &str, scope_id: &str) -> String {
    let stripped = strip_css_comments(css);
    let attr = format!("[{scope_id}]");
    let mut out = String::with_capacity(stripped.len() + 32);
    let mut prelude = String::new();
    // Stack of block types: true = at-rule container (@media, @keyframes), false = style rule.
    let mut stack: Vec<bool> = Vec::new();
    // Depth of @keyframes container, if any. Selectors inside it (0%, from, to) must not be scoped.
    let mut keyframes_depth: Option<usize> = None;
    for c in stripped.chars() {
        match c {
            '{' => {
                let trimmed = prelude.trim().to_string();
                let is_at_rule = trimmed.starts_with('@');
                let is_keyframes = trimmed.starts_with("@keyframes")
                    || trimmed.starts_with("@-webkit-keyframes")
                    || trimmed.starts_with("@-moz-keyframes");
                if is_keyframes {
                    keyframes_depth = Some(stack.len());
                }
                if is_at_rule {
                    out.push_str(&prelude);
                } else if keyframes_depth.is_some() {
                    // Inside @keyframes: keyframe selectors (0%, from, to) are not scoped.
                    out.push_str(&prelude);
                } else if trimmed.is_empty() {
                    out.push_str(&prelude);
                } else {
                    let inside_style = stack.last() == Some(&false);
                    if inside_style {
                        // Unexpected nested selector inside declarations — pass through.
                        out.push_str(&prelude);
                    } else {
                        out.push_str(&scope_selector_list(&prelude, &attr));
                    }
                }
                prelude.clear();
                stack.push(is_at_rule);
                out.push(c);
            }
            '}' => {
                out.push(c);
                stack.pop();
                if let Some(kd) = keyframes_depth
                    && stack.len() <= kd
                {
                    keyframes_depth = None;
                }
            }
            _ => {
                let inside_style = stack.last() == Some(&false);
                if inside_style {
                    // Inside a style rule's declaration block — copy verbatim.
                    out.push(c);
                } else {
                    prelude.push(c);
                }
            }
        }
    }
    out.push_str(&prelude);
    out
}

/// Replace every CSS comment (`/* … */`) with a single space.
///
/// [`scope_css`] appends the scope attribute to every whitespace-separated
/// token of a selector prelude, so comment text must never reach one: a
/// comment sitting between two rules used to be glued onto the front of the
/// NEXT rule's prelude and then scoped as if it were a compound selector.
/// Comments must also not contribute braces — a `}` inside a comment used to
/// close the declaration block it sat in — so the text is removed here, once,
/// before any scoping decision is made.
///
/// This is a tokenizing scan, not `split("/*")`, because a `/*` inside a
/// quoted string or inside an unquoted `url(…)` token is NOT a comment opener.
/// Treating it as one truncates the remainder of the stylesheet: a naive
/// string-split corrupts `content: "/*"`, `a[href="/*"]` and `url(/*)`. The
/// scan therefore tracks the two contexts that suppress comment recognition —
/// a quoted string, and the body of an unquoted `url()` token — and neither
/// can hide a comment opener from the other.
///
/// An unterminated comment runs to end of input (CSS Syntax 3: "consume the
/// remnants of the bad comment"), so this terminates on any input.
///
/// A stylesheet containing no `/*` is returned borrowed and untouched, which is
/// what keeps [`scope_css`] byte-identical on comment-free CSS.
fn strip_css_comments(css: &str) -> std::borrow::Cow<'_, str> {
    use std::borrow::Cow;

    // Only a literal `/*` can open a comment, so a sheet without one cannot
    // contain one — not even inside a string or a url(), where it would be
    // inert anyway. Nothing to scan, and the borrow keeps output identical.
    if !css.contains("/*") {
        return Cow::Borrowed(css);
    }
    let bytes = css.as_bytes();
    let mut out = String::with_capacity(css.len());
    // Start of the run of source text not yet copied into `out`. Comments are
    // collapsed to a single space and everything between runs is preserved
    // verbatim, so declaration blocks survive byte-for-byte. Every byte offset
    // this scan stops on is ASCII (`/`, a quote, `)`, `*`) or the end of input,
    // so it is always a character boundary and the slices never split a
    // multi-byte character.
    let mut run_start = 0usize;
    let mut i = 0usize;
    while i < bytes.len() {
        if bytes[i] == b'/' && bytes.get(i + 1) == Some(&b'*') {
            out.push_str(&css[run_start..i]);
            out.push(' ');
            i = skip_css_comment(bytes, i + 2);
            run_start = i;
        } else if bytes[i] == b'"' || bytes[i] == b'\'' {
            i = skip_css_string(bytes, i);
        } else if at_url_open(bytes, i) {
            i = skip_css_url(bytes, i);
        } else {
            // Advance by one byte, not one char: the loop above only ever stops
            // on ASCII, and the only slicing offsets are those stops.
            i += 1;
        }
    }
    out.push_str(&css[run_start..]);
    Cow::Owned(out)
}

/// Byte offset just past the `*/` that closes the comment whose body starts at
/// `start`, or the end of input when the comment is never closed. Every byte is
/// legal inside a comment except that `*/` ends it, and CSS does not nest
/// comments, so a linear scan is exact.
fn skip_css_comment(bytes: &[u8], start: usize) -> usize {
    let mut i = start;
    while i < bytes.len() {
        if bytes[i] == b'*' && bytes.get(i + 1) == Some(&b'/') {
            return i + 2;
        }
        i += 1;
    }
    i
}

/// Byte offset just past the string whose opening quote is at `open`.
///
/// A `\` escapes the next byte, and an unescaped newline terminates the string
/// as a bad-string token without consuming the newline, so neither can run the
/// scan past the stylesheet.
fn skip_css_string(bytes: &[u8], open: usize) -> usize {
    let quote = bytes[open];
    let mut i = open + 1;
    while i < bytes.len() {
        match bytes[i] {
            b'\\' => i = (i + 2).min(bytes.len()),
            b'\n' | b'\r' | b'\x0C' => return i,
            b if b == quote => return i + 1,
            _ => i += 1,
        }
    }
    i
}

/// True when `i` starts a `url(` token — an unquoted URL, whose body is literal
/// text rather than a token stream. `url (` with a space is a function token,
/// and an identifier that merely ends in `url` (`myurl(`) is that identifier
/// followed by a parenthesis block, so neither is a URL.
fn at_url_open(bytes: &[u8], i: usize) -> bool {
    fn ident_byte(b: u8) -> bool {
        b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'\\' | b'\0')
    }
    if i > 0 && ident_byte(bytes[i - 1]) {
        return false;
    }
    matches!(
        &bytes[i..i.saturating_add(4).min(bytes.len())],
        [b'u' | b'U', b'r' | b'R', b'l' | b'L', b'(']
    )
}

/// Byte offset just past the `)` that closes the unquoted URL token opening at
/// `i`. Quoted URLs are handled as ordinary strings by the caller; this only
/// needs to reach the closing paren, and an unterminated one runs to end of
/// input.
fn skip_css_url(bytes: &[u8], open: usize) -> usize {
    let mut i = open + 4;
    while i < bytes.len() && bytes[i] != b')' {
        i += if bytes[i] == b'\\' { 2 } else { 1 };
    }
    (i + 1).min(bytes.len())
}

/// Append the scope attribute to each selector in a comma-separated selector
/// list. Non-selector text between rules is carried through unchanged.
fn scope_selector_list(prelude: &str, attr: &str) -> String {
    let mut out = String::with_capacity(prelude.len() + 32);
    for (i, sel) in prelude.split(',').enumerate() {
        if i > 0 {
            out.push(',');
        }
        out.push_str(&scope_single_selector(sel, attr));
    }
    out
}

/// Append `[data-v-<scope>]` to every compound in a single complex selector,
/// preserving the combinators (`>`, `+`, `~`) between compounds.
///
/// `div > .card` -> `div[data-v-x] > .card[data-v-x]`
/// `.header h1`  -> `.header[data-v-x] h1[data-v-x]`
fn scope_single_selector(sel: &str, attr: &str) -> String {
    let spaced = pad_child_combinators(sel);
    let mut out = String::with_capacity(spaced.len() + 16);
    let mut pending_combinator: Option<&str> = None;
    let mut wrote_compound = false;
    for token in spaced.split_whitespace() {
        if matches!(token, ">" | "+" | "~") {
            pending_combinator = Some(token);
            continue;
        }
        if wrote_compound {
            out.push(' ');
            if let Some(comb) = pending_combinator {
                out.push_str(comb);
                out.push(' ');
            }
        }
        pending_combinator = None;
        out.push_str(token);
        out.push_str(attr);
        wrote_compound = true;
    }
    if !wrote_compound {
        return sel.to_string();
    }
    out
}

/// Re-space `>` so the whitespace tokenizer sees it as a standalone token,
/// which makes `div>.card` behave like `div > .card`. A `>` inside an
/// attribute selector (e.g. `[data-x="a>b"]`) is left untouched.
fn pad_child_combinators(raw: &str) -> String {
    let mut out = String::with_capacity(raw.len() + 4);
    let mut bracket_depth = 0usize;
    let mut quote: Option<char> = None;
    for ch in raw.chars() {
        if let Some(q) = quote {
            out.push(ch);
            if ch == q {
                quote = None;
            }
            continue;
        }
        match ch {
            '[' => {
                bracket_depth += 1;
                out.push(ch);
            }
            ']' => {
                bracket_depth = bracket_depth.saturating_sub(1);
                out.push(ch);
            }
            '"' | '\'' if bracket_depth > 0 => {
                quote = Some(ch);
                out.push(ch);
            }
            '>' if bracket_depth == 0 => out.push_str(" > "),
            _ => out.push(ch),
        }
    }
    out
}

/// Generate the global emit infrastructure: Callbacks type, emit function,
/// set_callback function, and slots thread-local using thread-local storage.
///
/// Two registries live here, and they answer different questions about the same
/// `@event` binding:
///
/// - `EMIT_CALLBACKS` maps an EVENT to the handler NAME the parent wrote
///   (`@confirm="on_confirm"` -> `confirm` -> `on_confirm`). The child reads it
///   in [`emit`], because the child is the side that knows which event it just
///   emitted.
/// - `EMIT_DISPATCH` maps that handler NAME to the CALL that runs it, registered
///   by the parent through [`set_emit_dispatch`] at the moment it renders the
///   child. The parent is the side that has a `State` to call the method on, so
///   this is the only registry that can turn a name into a running handler.
///
/// Neither alone is enough, which is why the lookup in `emit` goes through both:
/// the first says which method the author meant, the second is the only thing
/// that can reach it. The pair is also what a function-typed prop reuses — see
/// `dispatch_emit`.
///
/// The registries are per generated FILE, not global to the crate. A child
/// resolves its own emit against its own module, which is why the parent has to
/// name the child (`Child::set_emit_dispatch(..)`) rather than reach a shared
/// table: each component module carries its own copy of this infrastructure and
/// nothing else in the crate can name it.
fn generate_emit_infrastructure() -> String {
    r#"    /// Type alias for callback maps passed to child components.
    pub type Callbacks = std::collections::HashMap<&'static str, &'static str>;

    /// One handler a parent hands a child, as something the child can call.
    ///
    /// `Rc<RefCell<dyn FnMut(&str)>>`, and the shape is not decoration — both
    /// halves of it are load-bearing:
    ///
    /// - The `Rc` is what makes a re-entrant call possible. A `Box<dyn FnMut>`
    ///   is owned by the registry cell, so calling it means calling it while
    ///   that cell is still borrowed, and a handler that emitted in turn would
    ///   panic on the second borrow. Cloning the handle out first leaves the
    ///   registry unborrowed for the whole call. The registry is thread-local,
    ///   so the handle never has to be `Send`.
    /// - The inner `RefCell` is what makes the handle callable at all. `Fn`/`FnMut`
    ///   are implemented for `Box<F>` and for `&F`, but NOT for `Rc<F>`, so
    ///   `Rc<dyn FnMut(..)>` cannot be called without a `DerefMut` that `Rc` does
    ///   not implement. The inner cell is where the `&mut` comes from; the
    ///   registration itself is never removed, so a handler that panics does not
    ///   take its own binding down with it.
    pub type EventHandler = std::rc::Rc<std::cell::RefCell<dyn FnMut(&str)>>;

    // Thread-local callback registry for emit system.
    // Stores event name -> handler function name mappings.
    thread_local! {
        static EMIT_CALLBACKS: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
        // Handler name -> the call that runs it. Written by the parent that
        // rendered this component, read by `emit` and `dispatch_emit`.
        static EMIT_DISPATCH: RefCell<HashMap<String, EventHandler>> = RefCell::new(HashMap::new());
        static SLOTS: RefCell<HashMap<String, velox_dom::VNode>> = RefCell::new(HashMap::new());
    }

    /// Register callbacks for the current render scope.
    /// Called by `render_with_callbacks` before rendering a child component.
    pub fn set_emit_callbacks(callbacks: &Callbacks) {
        EMIT_CALLBACKS.with(|cell| {
            let mut map = cell.borrow_mut();
            map.clear();
            for (k, v) in callbacks {
                map.insert(k.to_string(), v.to_string());
            }
        });
    }

    /// Store slots for the current render scope so `<slot>` elements can find them.
    pub fn set_slots(slots: &std::collections::HashMap<&str, velox_dom::VNode>) {
        SLOTS.with(|cell| {
            let mut map = cell.borrow_mut();
            map.clear();
            for (k, v) in slots {
                map.insert(k.to_string(), v.clone());
            }
        });
    }

    /// Register the call that runs one handler, keyed by the handler NAME the
    /// parent wrote on the tag (`@confirm="on_confirm"` registers `on_confirm`).
    ///
    /// A parent calls this for every handler it binds, immediately before it
    /// calls `render_with_callbacks` / `render_with_slots`, and the call it
    /// passes is its own event dispatcher: so the payload a child emits lands on
    /// the parent's `State` method, with the arity the method declared already
    /// decided by the dispatcher.
    ///
    /// Registering a name twice replaces the call, so one component rendered
    /// twice in a pass under different handlers is last-wins rather than both.
    pub fn set_emit_dispatch(handler: &str, call: EventHandler) {
        EMIT_DISPATCH.with(|cell| {
            cell.borrow_mut().insert(handler.to_string(), call);
        });
    }

    /// Run the handler registered under `handler` with `payload`.
    ///
    /// This is the entry point a FUNCTION-TYPED prop goes through: a parent
    /// binding `:on_confirm="on_confirm"` hands the child a closure that calls
    /// this with the name it read off the binding, so a child that prefers a
    /// callable prop over `emit("..")` reaches exactly the same handler.
    ///
    /// Returns whether a handler was registered, so a caller can tell a handler
    /// that ran and decided to do nothing from one that was never bound.
    pub fn dispatch_emit(handler: &str, payload: &str) -> bool {
        // Clone the handle out so the call happens with the registry cell
        // unborrowed — a handler that emits in turn reaches this function again
        // while its own call is still on the stack.
        let call = EMIT_DISPATCH.with(|cell| cell.borrow().get(handler).cloned());
        let Some(call) = call else { return false };
        (call.borrow_mut())(payload);
        true
    }

    /// Emit an event from a child component to its parent.
    ///
    /// Resolves the handler the parent bound for `event` and RUNS it: the
    /// dispatcher the parent registered is invoked with `payload`, which is what
    /// reaches the parent's `State` method. Returns the handler name it
    /// resolved, or `None` when the parent bound no handler for this event —
    /// the one case where an emit has nowhere to go.
    pub fn emit(event: &str, payload: &str) -> Option<String> {
        let handler = EMIT_CALLBACKS.with(|cell| cell.borrow().get(event).cloned())?;
        dispatch_emit(&handler, payload);
        Some(handler)
    }

    /// Look up slot content by name. Returns the slot VNode if found,
    /// otherwise calls the fallback closure.
    pub fn render_slot(
        name: &str,
        fallback: impl FnOnce() -> velox_dom::VNode,
    ) -> velox_dom::VNode {
        SLOTS.with(|cell| {
            let map = cell.borrow();
            if let Some(v) = map.get(name) {
                return v.clone();
            }
            drop(map);
            fallback()
        })
    }"#
    .to_string()
}

/// Generate emit helpers that go inside the component's script_rs module.
fn generate_component_emit_helpers() -> String {
    r#"
        /// Emit an event to the parent component that rendered this one.
        ///
        /// `event` names the binding on the parent's side: the parent wrote
        /// `@confirm="on_confirm"` on the tag that rendered this component, and
        /// `emit("confirm", payload)` runs that parent's `on_confirm` with
        /// `payload`. The parent owns the call — it is the side with the `State`
        /// the method lives on — so this component never names a handler, only
        /// the event it wants the parent to hear about.
        ///
        /// A parent that bound no handler for this event has nothing to run, and
        /// that is the one case where the emit goes nowhere: it is not an error,
        /// because `@event` on a component tag is opt-in per event.
        pub fn emit(event: &str, payload: &str) {
            let _handler = super::emit(event, payload);
        }
"#
    .to_string()
}

/// Generate the name a parent writes this component's props with.
///
/// A component that declares `pub struct Props` gets an alias for that struct, so
/// a parent builds it as a struct literal and `rustc` checks every field it
/// binds. A component that declares no `Props` gets a stand-in with no fields of
/// its own: a parent that binds nothing to it still compiles, and one that binds
/// something cannot. Its `values` map is still readable, because a component
/// with no `Props` struct and no way to receive an attribute has nothing else to
/// resolve a read from.
///
/// This is the DEFINING half of the name and it lives in the stub half of the
/// pipeline, while every `render_with_props(props: PropsArg)` that names it is
/// emitted by the template half (`template_codegen::generate_render_with_props`).
/// The two halves are only ever correct together, so a caller that assembles a
/// module by hand — rather than through `to_stub_rs*` plus
/// `compile_template_to_rs_full*` as `veloxc` does — must emit both or
/// neither, or the module does not compile (`E0412: cannot find type 'PropsArg'`).
/// It is `pub` for exactly that caller.
pub fn generate_props_arg(ss: &str, indent: &str) -> String {
    if crate::script_index::extract_props_fields(ss).is_empty() {
        return format!(
            "{indent}/// This component declares no `Props` struct, so a parent binds nothing to it.\n\
             {indent}pub struct PropsArg {{\n\
             {indent}    pub values: std::collections::HashMap<String, String>,\n\
             {indent}}}\n"
        );
    }
    format!(
        "{indent}/// The typed channel across this component's boundary.\n\
         {indent}pub type PropsArg = script_rs::Props;\n"
    )
}

/// Generate `render_with_callbacks` function that accepts callbacks from a parent.
/// Also generates `render_with_slots` overloads for components that accept slots.
/// This goes INSIDE the module (before the closing brace).
fn generate_render_with_callbacks_inside_module() -> String {
    r#"
    /// Render this component with callbacks from a parent component.
    /// The callbacks map event names to parent handler method names.
    pub fn render_with_callbacks(
        props: PropsArg,
        callbacks: &std::collections::HashMap<&'static str, &'static str>,
        _handler_names: &[&'static str],
    ) -> velox_dom::VNode {
        // Register callbacks in thread-local storage so emit() can find them.
        // set_emit_callbacks lives at the same level as this function (both inside
        // the component module), so no path qualifier is needed.
        set_emit_callbacks(callbacks);
        // Render using the standard props path
        render_with_props(props)
    }

    /// Render this component with callbacks and slots from a parent component.
    /// The callbacks map event names to parent handler method names.
    /// The slots map slot names to pre-rendered VNode trees.
    pub fn render_with_slots(
        props: PropsArg,
        callbacks: &std::collections::HashMap<&'static str, &'static str>,
        _handler_names: &[&'static str],
        slots: &std::collections::HashMap<&str, velox_dom::VNode>,
    ) -> velox_dom::VNode {
        set_emit_callbacks(callbacks);
        set_slots(slots);
        render_with_props(props)
    }
"#
    .to_string()
}

/// Generate the `render_with_slots_only` function: props and slots from a
/// parent, with no `@event` bound. Goes INSIDE the module (before the closing
/// brace).
fn generate_render_with_slots_only_inside_module() -> String {
    r#"
    /// Render this component with slot content from a parent component.
    /// The slots map maps slot names to pre-rendered VNode trees.
    ///
    /// Distinct from `render_with_slots` above rather than another arity of it:
    /// Rust resolves calls by name, so two functions with one name is a
    /// duplicate definition however many arguments each takes.
    pub fn render_with_slots_only(
        props: PropsArg,
        slots: &std::collections::HashMap<&str, velox_dom::VNode>,
    ) -> velox_dom::VNode {
        set_slots(slots);
        render_with_props(props)
    }
"#
    .to_string()
}

/// Turn `raw` into a Rust identifier safe to interpolate into generated code:
/// lowercased, with anything that cannot appear in an identifier replaced.
///
/// PRIVATE, and deliberately so. It was `pub(crate)` on the stated ground that
/// the template half of codegen names generated locals with it
/// (`template_codegen::function_prop_value`) and the two halves have to agree on
/// what a safe name is. That consumer does not exist: the only references to this
/// function in the crate were its definition and the one call site below. A
/// justification naming a consumer that is not there is worse than no
/// justification — it describes an agreement the code has not made, and the next
/// reader trusts it. If `template_codegen` ever DOES need to name a local from a
/// prop field, it should call this — and then this should become `pub(crate)`
/// again, with that caller named in this comment.
fn sanitize_ident(raw: &str) -> String {
    let mut out = String::with_capacity(raw.len());
    for (i, ch) in raw.chars().enumerate() {
        if (i == 0 && ch.is_ascii_digit()) || !is_ident_char(ch) {
            out.push('_');
        } else {
            out.push(ch.to_ascii_lowercase());
        }
    }
    if out.is_empty() { "_comp".into() } else { out }
}

fn is_ident_char(ch: char) -> bool {
    ch.is_ascii_alphanumeric() || ch == '_'
}

/// Escape content for use in r#"..."# raw string literals.
/// The sequence "# would terminate the raw string early, so we escape it.
fn escape_raw_string(s: &str) -> String {
    s.replace("\"#", "\"\\#")
}