vize_canon 0.206.0

Canon - The standard of correctness for Vize type checking
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
//! Helper functions and constants for virtual TypeScript generation.
//!
//! Contains utility functions for type declarations, event type mapping,
//! identifier conversion, and template context generation.

use std::ops::Range;

use super::types::VirtualTsOptions;
use vize_carton::String;
use vize_carton::append;
use vize_carton::config::VueVersion;
use vize_croquis::macros::{
    DEFINE_EMITS, DEFINE_EXPOSE, DEFINE_MODEL, DEFINE_PROPS, DEFINE_SLOTS, WITH_DEFAULTS,
};

pub(crate) const USE_TEMPLATE_REF: &str = "useTemplateRef";

/// Names declared by the generated setup-scope helper block.
///
/// This includes Vue compiler macros plus runtime helper shims that are modeled
/// inside `__setup()`. It is intentionally broader than `COMPILER_MACRO_NAMES`.
pub(crate) const SETUP_SCOPE_HELPER_NAMES: &[&str] = &[
    DEFINE_PROPS,
    DEFINE_EMITS,
    DEFINE_EXPOSE,
    DEFINE_MODEL,
    DEFINE_SLOTS,
    WITH_DEFAULTS,
    USE_TEMPLATE_REF,
];

/// Shared type-helper text used both by the per-file embedded preamble and by
/// the hoisted ambient helpers file. Declared as a macro so the exact same
/// bytes can be spliced into both constants at compile time.
macro_rules! vue_type_aliases_text {
    () => {
        r#"type __EmitShape<T> = T extends (...args: any[]) => any ? T : T extends Record<string, any> ? { [K in keyof T]: T[K] extends (...args: infer A) => any ? A : T[K] extends any[] ? T[K] : any[]; } : Record<string, any[]>;
type __EmitArgs<T, K extends keyof T> = T[K] extends any[] ? T[K] : any[];
type __EmitFn<T> = __EmitShape<T> extends (...args: any[]) => any ? __EmitShape<T> : (<K extends keyof __EmitShape<T>>(event: K, ...args: __EmitArgs<__EmitShape<T>, K>) => void);
type __RuntimePropCtor<T> = T extends readonly (infer U)[] ? __RuntimePropCtor<U> : T extends { type: infer U } ? __RuntimePropCtor<U> : T extends StringConstructor ? string : T extends NumberConstructor ? number : T extends BooleanConstructor ? boolean : T extends ArrayConstructor ? unknown[] : T extends ObjectConstructor ? Record<string, unknown> : T extends DateConstructor ? Date : T extends FunctionConstructor ? (...args: any[]) => any : unknown;
type __RuntimePropResolved<T> = T extends { required: true } ? true : T extends { default: any } ? true : false;
type __RuntimePropShape<T extends Record<string, any>> = { [K in keyof T]: __RuntimePropResolved<T[K]> extends true ? __RuntimePropCtor<T[K]> : __RuntimePropCtor<T[K]> | undefined; };
type __DefaultFactory<T> = (props: any) => T;
type __WithDefaultValue<T> = T | __DefaultFactory<T>;
type __WithDefaultsArgs<T> = { [K in keyof T]?: __WithDefaultValue<T[K]> };
type __WithDefaultsResult<T, D extends __WithDefaultsArgs<T>> = Omit<T, keyof D> & Required<Pick<T, keyof D & keyof T>>;
type __Ref<T> = import('vue').Ref<T>;
type __ShallowRef<T> = import('vue').ShallowRef<T>;"#
    };
}

macro_rules! v_for_list_decls_text {
    () => {
        r#"declare function __vForList<T>(source: readonly T[] | undefined | null): readonly [item: T, key: number, index: number][];
declare function __vForList(source: number | undefined | null): readonly [item: number, key: number, index: number][];
declare function __vForList(source: string | undefined | null): readonly [item: string, key: number, index: number][];
declare function __vForList<T>(source: Iterable<T> | undefined | null): readonly [item: T, key: number, index: number][];
declare function __vForList<T extends object>(source: T | undefined | null): readonly [item: T[keyof T], key: keyof T, index: number][];"#
    };
}

macro_rules! vue_type_helpers_text {
    () => {
        concat!(vue_type_aliases_text!(), "\n", v_for_list_decls_text!())
    };
}

/// Emit-overload helper text shared between the per-file embedded emission and
/// the hoisted ambient helpers file. Each line ends with `\n`.
///
/// Deliberately excludes `__EmitProps`: that alias dereferences
/// `import('vue').EmitsToProps`, which only exists on Vue >= 3.3, so it stays
/// per-file and is only emitted for components that actually declare emits —
/// exactly as before hoisting.
macro_rules! emit_overload_helpers_text {
    () => {
        concat!(
            "type __VizeOverloadProps<TOverload> = Pick<TOverload, keyof TOverload>;\n",
            "type __VizeOverloadUnionRecursive<TOverload, TPartialOverload = unknown> = TOverload extends (...args: infer TArgs) => infer TReturn ? TPartialOverload extends TOverload ? never : __VizeOverloadUnionRecursive<TPartialOverload & TOverload, TPartialOverload & ((...args: TArgs) => TReturn) & __VizeOverloadProps<TOverload>> | ((...args: TArgs) => TReturn) : never;\n",
            "type __VizeOverloadUnion<TOverload extends (...args: any[]) => any> = Exclude<__VizeOverloadUnionRecursive<(() => never) & TOverload>, TOverload extends () => never ? never : () => never>;\n",
            "type __VizeOverloadParameters<T extends (...args: any[]) => any> = Parameters<__VizeOverloadUnion<T>>;\n",
            "type __VizeIsStringLiteral<T> = T extends string ? string extends T ? false : true : false;\n",
            "type __VizeParametersToFns<T extends any[]> = { [K in T[0]]: __VizeIsStringLiteral<K> extends true ? (...args: T extends [e: infer E, ...args: infer P] ? K extends E ? P : never : never) => any : never };\n",
            "type __EmitOptions<T> = { [K in keyof __EmitShape<T> & string]: (...args: __EmitArgs<__EmitShape<T>, K>) => any } & (__EmitShape<T> extends (...args: any[]) => any ? __VizeParametersToFns<__VizeOverloadParameters<__EmitShape<T>>> : {});\n",
        )
    };
}

/// Shared type helpers used by generated virtual modules and setup-scope macros.
pub(crate) const VUE_TYPE_HELPERS: &str = vue_type_helpers_text!();

/// Emit-overload helpers embedded per-file when the shared preamble is not
/// hoisted. In hoisted mode the same text lives in the ambient helpers file.
pub(crate) const EMIT_OVERLOAD_HELPERS: &str = emit_overload_helpers_text!();

/// Per-file `__EmitProps` alias. Stays per-file in both modes because
/// `EmitsToProps` is only exported by Vue >= 3.3; emitting it only for
/// components with emits keeps older-Vue programs error-free, as before.
pub(crate) const EMIT_PROPS_HELPER: &str =
    "type __EmitProps<T> = import('vue').EmitsToProps<__EmitOptions<T>>;\n";

/// Vue setup-scope helpers - these are defined inside setup scope, NOT globally.
/// Compiler macros stay setup-only, while runtime helper shims model Vue APIs.
/// Parameters and type parameters are prefixed with _ to avoid "unused" warnings.
pub(crate) const VUE_SETUP_HELPERS: &str = r#"  // Compiler macros (only valid in setup scope, not global)
  function defineProps<_T = unknown>(): _T;
  function defineProps<const _T extends readonly string[]>(_props: _T): { [K in _T[number]]?: any };
  function defineProps<const _T extends Record<string, any>>(_props: _T): __RuntimePropShape<_T>;
  function defineProps(_props?: any) { void _props; return undefined as any; }
  function defineEmits<_T = unknown>(): __EmitFn<_T>;
  function defineEmits<const _T extends readonly string[]>(_events: _T): (event: _T[number], ...args: any[]) => void;
  function defineEmits<const _T extends Record<string, any>>(_events: _T): __EmitFn<_T>;
  function defineEmits(_events?: any) { void _events; return (() => {}) as any; }
  function defineExpose<_T = unknown>(_exposed?: _T): void { void _exposed; }
  function defineModel<_T = unknown>(): __Ref<_T | undefined>;
  function defineModel<_T = unknown>(_options: any): __Ref<_T>;
  function defineModel<_T = unknown>(_name: string, _options?: any): __Ref<_T>;
  function defineModel(_name_or_options?: any, _options?: any) { void _name_or_options; void _options; return undefined as any; }
  function defineSlots<_T = unknown>(): _T { return undefined as unknown as _T; }
  function withDefaults<_T, _D extends __WithDefaultsArgs<_T>>(_props: _T, _defaults: _D): __WithDefaultsResult<_T, _D> { void _props; void _defaults; return undefined as unknown as __WithDefaultsResult<_T, _D>; }
  function useTemplateRef<_T = any>(_key: string): __ShallowRef<_T | null> { void _key; return undefined as unknown as __ShallowRef<_T | null>; }
  // Mark compiler macros as used
  void defineProps; void defineEmits; void defineExpose; void defineModel; void defineSlots; void withDefaults; void useTemplateRef;"#;

/// ImportMeta augmentation for Vite/Nuxt projects.
/// Uses `/// <reference types="..." />` to pull in existing type definitions
/// from frameworks like Vite, Nuxt, etc. when available.
pub(crate) const IMPORT_META_AUGMENTATION: &str = r#"// ImportMeta augmentation (reference existing framework types)
/// <reference types="vite/client" />
declare global {
  // Extend ImportMeta with Nuxt-specific properties not covered by vite/client
  interface ImportMeta {
    client: boolean;
    server: boolean;
    dev: boolean;
    prod: boolean;
    ssr: boolean;
  }
}
"#;

/// Per-file setup-scope helper block emitted when the shared preamble is
/// hoisted: the macro signatures live once in the ambient helpers file as
/// `__vize_*` globals and are aliased into setup scope here, so compiler
/// macros stay setup-scope-only and still shadow same-named module imports
/// exactly like the embedded `function` declarations did.
pub(crate) const VUE_SETUP_HELPERS_HOISTED: &str = r#"  // Compiler macros (setup-scope only; signatures hoisted to the shared helpers file)
  const defineProps = __vize_defineProps;
  const defineEmits = __vize_defineEmits;
  const defineExpose = __vize_defineExpose;
  const defineModel = __vize_defineModel;
  const defineSlots = __vize_defineSlots;
  const withDefaults = __vize_withDefaults;
  const useTemplateRef = __vize_useTemplateRef;
  // Mark compiler macros as used
  void defineProps; void defineEmits; void defineExpose; void defineModel; void defineSlots; void withDefaults; void useTemplateRef;"#;

/// File name of the shared ambient helpers declaration materialized once per
/// program when the preamble is hoisted out of the generated virtual modules.
pub const SHARED_PREAMBLE_FILE_NAME: &str = "__vize_helpers.d.ts";

/// Content of the shared ambient helpers file.
///
/// This file is a global script (no imports/exports), so every declaration
/// merges into the program's global scope exactly once:
///
/// - the `ImportMeta` augmentation becomes a plain global interface merge,
///   so generated modules stop carrying per-file `declare global` blocks
///   (which made every module a global-scope augmenter and defeated
///   incremental rebuilds);
/// - the generic type helpers ([`VUE_TYPE_HELPERS`] /
///   [`EMIT_OVERLOAD_HELPERS`]) are file-independent and hoist verbatim;
/// - the compiler-macro signatures are declared as `__vize_*` global
///   functions with byte-identical signatures (same overload order, type
///   parameter and parameter names) and aliased into each module's
///   `__setup()` scope, preserving exact overload parity while keeping the
///   macros invalid outside setup scope.
pub const SHARED_PREAMBLE_DTS: &str = concat!(
    "// ============================================\n",
    "// Shared ambient helpers for vize virtual TypeScript\n",
    "// Generated by vize\n",
    "// ============================================\n",
    "// Global script: one copy of these declarations per program replaces the\n",
    "// preamble previously embedded in every generated .vue.ts module.\n",
    "\n",
    "// ImportMeta augmentation (reference existing framework types)\n",
    "/// <reference types=\"vite/client\" />\n",
    "// Extend ImportMeta with Nuxt-specific properties not covered by vite/client\n",
    "interface ImportMeta {\n",
    "  client: boolean;\n",
    "  server: boolean;\n",
    "  dev: boolean;\n",
    "  prod: boolean;\n",
    "  ssr: boolean;\n",
    "}\n",
    "\n",
    "// Shared type helpers used by generated virtual modules\n",
    vue_type_helpers_text!(),
    "\n\n",
    "// Emit-overload helpers (consumed by the per-file __EmitProps alias)\n",
    emit_overload_helpers_text!(),
    "\n",
    "// Compiler-macro signatures (aliased inside each module's __setup() scope)\n",
    "declare function __vize_defineProps<_T = unknown>(): _T;\n",
    "declare function __vize_defineProps<const _T extends readonly string[]>(_props: _T): { [K in _T[number]]?: any };\n",
    "declare function __vize_defineProps<const _T extends Record<string, any>>(_props: _T): __RuntimePropShape<_T>;\n",
    "declare function __vize_defineEmits<_T = unknown>(): __EmitFn<_T>;\n",
    "declare function __vize_defineEmits<const _T extends readonly string[]>(_events: _T): (event: _T[number], ...args: any[]) => void;\n",
    "declare function __vize_defineEmits<const _T extends Record<string, any>>(_events: _T): __EmitFn<_T>;\n",
    "declare function __vize_defineExpose<_T = unknown>(_exposed?: _T): void;\n",
    "declare function __vize_defineModel<_T = unknown>(): __Ref<_T | undefined>;\n",
    "declare function __vize_defineModel<_T = unknown>(_options: any): __Ref<_T>;\n",
    "declare function __vize_defineModel<_T = unknown>(_name: string, _options?: any): __Ref<_T>;\n",
    "declare function __vize_defineSlots<_T = unknown>(): _T;\n",
    "declare function __vize_withDefaults<_T, _D extends __WithDefaultsArgs<_T>>(_props: _T, _defaults: _D): __WithDefaultsResult<_T, _D>;\n",
    "declare function __vize_useTemplateRef<_T = any>(_key: string): __ShallowRef<_T | null>;\n",
);

/// Helper declarations shipped next to emitted declaration outputs.
///
/// Emitted `.vue.d.ts` files reference the shared helper type aliases by
/// name (e.g. `__EmitFn<Emits>`), so declaration output directories carry one
/// copy of the type aliases, wired up via a `/// <reference path>` from each
/// emitted file. Deliberately excludes the `vite/client` reference, the
/// `ImportMeta` augmentation, and the value-level macro declarations: emitted
/// declarations never reference those, and they must not leak into consumer
/// programs.
pub const DECLARATION_HELPERS_DTS: &str = concat!(
    "// Shared helper types for vize-generated declaration files.\n",
    "// Generated by vize\n",
    vue_type_aliases_text!(),
    "\n",
    emit_overload_helpers_text!(),
);

/// Vue 2-only public-instance members that are absent from Vue 3's
/// `ComponentPublicInstance`.
///
/// In a Vue 2 / 2.7 dialect, template (and `this`) references such as
/// `$listeners`, `$children`, `$scopedSlots`, the `$on`/`$off`/`$once` event
/// emitter, `$set`/`$delete`, and `$createElement`/`_c` are valid but resolve
/// to nothing on the Vue 3 instance type, so Corsa would false-error on them.
/// They are emitted as permissive `any` bindings so v2 templates type-check.
/// Vue 3 output never emits these, so it stays byte-identical.
const VUE2_INSTANCE_MEMBERS: &[&str] = &[
    "$listeners",
    "$children",
    "$scopedSlots",
    "$on",
    "$off",
    "$once",
    "$set",
    "$delete",
    "$createElement",
    "_c",
];

/// Generate Vue template context declarations dynamically.
///
/// Derives `$`-prefixed globals from `ComponentPublicInstance` so that
/// type resolution is delegated to Corsa via Vue's type system
/// (including `ComponentCustomProperties` augmentations from plugins).
///
/// When `dialect` is a Vue 2 line (`V2` / `V2_7`), the context is additionally
/// augmented with Vue 2-only public-instance members (see
/// [`VUE2_INSTANCE_MEMBERS`]) so legacy templates do not false-error. Vue 3
/// (the default) emits the exact same output as before — byte-identical.
pub(crate) fn generate_template_context(options: &VirtualTsOptions, dialect: VueVersion) -> String {
    let mut ctx = String::default();

    let needs_global_helper =
        !options.template_globals.is_empty() || !options.css_modules.is_empty();

    // Vue 2 / 2.7 share a template dialect whose public instance still exposes
    // members removed in Vue 3 (`$listeners`, `$children`, the `$on`/`$off`/
    // `$once` event emitter, `$set`/`$delete`, `$createElement`, ...).
    let vue2_dialect = matches!(dialect, VueVersion::V2 | VueVersion::V2_7);

    // Instance type + conditional accessor helper
    ctx.push_str("    // Vue template context (delegates to ComponentPublicInstance)\n");
    ctx.push_str("    type __Ctx = import('vue').ComponentPublicInstance;\n");
    if needs_global_helper {
        ctx.push_str("    type __Global<K extends string, F = unknown> = K extends keyof __Ctx ? __Ctx[K] : F;\n");
    }
    ctx.push_str("    const __ctx = undefined as unknown as __Ctx;\n");

    // Core Vue globals (always present on ComponentPublicInstance)
    ctx.push_str("    const $attrs = __ctx.$attrs;\n");
    ctx.push_str("    const $slots = __ctx.$slots;\n");
    ctx.push_str("    const $refs = __ctx.$refs;\n");
    ctx.push_str("    const $emit = __ctx.$emit;\n");

    // Vue 2-only instance members (absent from Vue 3's ComponentPublicInstance).
    if vue2_dialect {
        ctx.push_str("    // Vue 2 instance members (not on Vue 3 ComponentPublicInstance)\n");
        for member in VUE2_INSTANCE_MEMBERS {
            append!(ctx, "    const {member} = undefined as any;\n");
        }
    }

    // Plugin globals (resolved via ComponentCustomProperties if augmented,
    // otherwise falls back to the configured type_annotation)
    if !options.template_globals.is_empty() {
        ctx.push_str("    // Plugin globals (via ComponentCustomProperties)\n");
        for global in &options.template_globals {
            append!(
                ctx,
                "    const {}: __Global<'{}', {}> = undefined as any;\n",
                global.name,
                global.name,
                global.type_annotation
            );
        }
    }

    // CSS module globals (resolved via ComponentCustomProperties if augmented,
    // otherwise falls back to Record<string, string>)
    if !options.css_modules.is_empty() {
        ctx.push_str("    // CSS modules (from <style module>)\n");
        for module_name in &options.css_modules {
            append!(
                ctx,
                "    const {module_name}: __Global<'{module_name}', Record<string, string>> = undefined as any;\n"
            );
        }
    }

    // Mark all as used
    ctx.push_str("    void __ctx; void $attrs; void $slots; void $refs; void $emit;\n");
    if vue2_dialect {
        ctx.push_str("    ");
        for member in VUE2_INSTANCE_MEMBERS {
            append!(ctx, "void {member};");
        }
        ctx.push('\n');
    }
    if !options.template_globals.is_empty() {
        ctx.push_str("    ");
        for (i, global) in options.template_globals.iter().enumerate() {
            if i > 0 {
                ctx.push(' ');
            }
            append!(ctx, "void {};", global.name);
        }
        ctx.push('\n');
    }
    if !options.css_modules.is_empty() {
        ctx.push_str("    ");
        for (i, module_name) in options.css_modules.iter().enumerate() {
            if i > 0 {
                ctx.push(' ');
            }
            append!(ctx, "void {module_name};");
        }
        ctx.push('\n');
    }

    ctx
}

/// Get the generated subrange that corresponds to a specific source expression.
///
/// This keeps source maps anchored to the actual expression text instead of
/// any wrapping code we emit around it (`void (...)`, `as Foo`, handler shims).
pub(crate) fn generated_text_range(
    generated_segment: &str,
    mapped_text: &str,
    generated_start: usize,
) -> Range<usize> {
    if mapped_text.is_empty() {
        return generated_start..generated_start + generated_segment.len();
    }

    let relative_start = generated_segment.find(mapped_text).unwrap_or(0);
    let start = generated_start + relative_start;
    start..start + mapped_text.len()
}

/// Get the TypeScript event type for a DOM event name.
/// Returns the specific event interface (MouseEvent, KeyboardEvent, etc.)
pub(crate) fn get_dom_event_type(event_name: &str) -> &'static str {
    match event_name {
        // Mouse events
        "dblclick" | "mousedown" | "mouseup" | "mousemove" | "mouseenter" | "mouseleave"
        | "mouseover" | "mouseout" | "contextmenu" => "MouseEvent",

        // Pointer events
        // `click`/`auxclick` are PointerEvent in current TypeScript DOM maps.
        "click" | "auxclick" | "pointerdown" | "pointerup" | "pointermove" | "pointerenter"
        | "pointerleave" | "pointerover" | "pointerout" | "pointercancel" | "gotpointercapture"
        | "lostpointercapture" => "PointerEvent",

        // Touch events
        "touchstart" | "touchend" | "touchmove" | "touchcancel" => "TouchEvent",

        // Keyboard events
        "keydown" | "keyup" | "keypress" => "KeyboardEvent",

        // Focus events
        "focus" | "blur" | "focusin" | "focusout" => "FocusEvent",

        // Input events
        "input" | "beforeinput" => "InputEvent",

        // Composition events
        "compositionstart" | "compositionend" | "compositionupdate" => "CompositionEvent",

        // Form events
        "submit" => "SubmitEvent",
        "change" => "Event",
        "reset" => "Event",

        // Drag events
        "drag" | "dragstart" | "dragend" | "dragenter" | "dragleave" | "dragover" | "drop" => {
            "DragEvent"
        }

        // Clipboard events
        "cut" | "copy" | "paste" => "ClipboardEvent",

        // Wheel events
        "wheel" => "WheelEvent",

        // Animation events
        "animationstart" | "animationend" | "animationiteration" | "animationcancel" => {
            "AnimationEvent"
        }

        // Transition events
        "transitionstart" | "transitionend" | "transitionrun" | "transitioncancel" => {
            "TransitionEvent"
        }

        // UI events
        "scroll" | "resize" => "Event",

        // Media events
        "play" | "pause" | "ended" | "loadeddata" | "loadedmetadata" | "timeupdate"
        | "volumechange" | "waiting" | "seeking" | "seeked" | "ratechange" | "durationchange"
        | "canplay" | "canplaythrough" | "playing" | "progress" | "stalled" | "suspend"
        | "emptied" | "abort" => "Event",

        // Error/Load events
        "error" => "ErrorEvent",
        "load" => "Event",

        // Selection events
        "select" | "selectionchange" | "selectstart" => "Event",

        // Modern UI events that were absent — without an entry, `@toggle` /
        // `@beforetoggle` etc. fell back to the bare `Event` interface and
        // the user lost the specific payload members. See #688.
        "toggle" | "beforetoggle" => "ToggleEvent",
        "formdata" => "FormDataEvent",
        "popstate" => "PopStateEvent",
        "hashchange" => "HashChangeEvent",
        "message" => "MessageEvent",
        "storage" => "StorageEvent",
        "online" | "offline" => "Event",
        "securitypolicyviolation" => "SecurityPolicyViolationEvent",

        // Default fallback
        _ => "Event",
    }
}

#[cfg(test)]
mod event_type_tests {
    use super::get_dom_event_type;

    #[test]
    fn maps_legacy_dom_events() {
        assert_eq!(get_dom_event_type("click"), "PointerEvent");
        assert_eq!(get_dom_event_type("auxclick"), "PointerEvent");
        assert_eq!(get_dom_event_type("dblclick"), "MouseEvent");
        assert_eq!(get_dom_event_type("keydown"), "KeyboardEvent");
        assert_eq!(get_dom_event_type("submit"), "SubmitEvent");
    }

    #[test]
    fn maps_modern_dom_events() {
        // These fell back to `Event` before #688 — now they get the specific
        // interface so `e.newState` / `e.formData` etc. complete.
        assert_eq!(get_dom_event_type("toggle"), "ToggleEvent");
        assert_eq!(get_dom_event_type("beforetoggle"), "ToggleEvent");
        assert_eq!(get_dom_event_type("formdata"), "FormDataEvent");
    }

    #[test]
    fn unknown_events_fall_back_to_event() {
        assert_eq!(get_dom_event_type("totally-made-up"), "Event");
    }
}

/// Convert kebab-case or PascalCase prop name to camelCase.
/// Vue normalizes prop names to camelCase internally.
/// Examples: "my-prop" -> "myProp", "MyProp" -> "myProp"
pub(crate) fn to_camel_case(s: &str) -> String {
    let mut result = String::with_capacity(s.len());
    let mut capitalize_next = false;
    let mut first = true;

    for c in s.chars() {
        if c == '-' || c == '_' {
            capitalize_next = true;
        } else if capitalize_next {
            result.push(c.to_ascii_uppercase());
            capitalize_next = false;
        } else if first {
            // First character should be lowercase
            result.push(c.to_ascii_lowercase());
            first = false;
        } else {
            result.push(c);
        }
    }

    result
}

/// Sanitize a string to be a valid TypeScript identifier.
/// Replaces invalid characters (like ':') with underscores and prefixes
/// reserved words.
/// Examples: "update:title" -> "update_title", "my-event" -> "my_event"
pub(crate) fn to_safe_identifier(s: &str) -> String {
    let mut result = to_safe_identifier_fragment(s);

    if !result
        .chars()
        .next()
        .is_some_and(|c| c.is_ascii_alphabetic() || c == '_' || c == '$')
    {
        result.insert(0, '_');
    }
    if is_reserved_identifier(result.as_str()) {
        result.insert(0, '_');
    }

    result
}

/// Sanitize a string for use inside a generated identifier that already has a
/// safe prefix (for example `_slot_{name}`).
pub(crate) fn to_safe_identifier_fragment(s: &str) -> String {
    let mut result = String::with_capacity(s.len().max(1));

    for c in s.chars() {
        if c.is_ascii_alphanumeric() || c == '_' || c == '$' {
            result.push(c);
        } else {
            result.push('_');
        }
    }

    if result.is_empty() {
        result.push('_');
    }

    result
}

#[inline]
pub(crate) fn is_reserved_identifier(s: &str) -> bool {
    matches!(
        s,
        "await"
            | "break"
            | "case"
            | "catch"
            | "class"
            | "const"
            | "continue"
            | "debugger"
            | "default"
            | "delete"
            | "do"
            | "else"
            | "enum"
            | "export"
            | "extends"
            | "false"
            | "finally"
            | "for"
            | "function"
            | "if"
            | "import"
            | "in"
            | "instanceof"
            | "new"
            | "null"
            | "return"
            | "super"
            | "switch"
            | "this"
            | "throw"
            | "true"
            | "try"
            | "typeof"
            | "var"
            | "void"
            | "while"
            | "with"
            | "yield"
            | "let"
            | "static"
            | "implements"
            | "interface"
            | "package"
            | "private"
            | "protected"
            | "public"
            | "as"
            | "from"
            | "of"
    )
}