tsv_svelte 0.8.0

Svelte formatter and parser compatible with Svelte's parse() — part of tsv
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
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
// Element parsing

use bumpalo::collections::Vec as BumpVec;

use crate::ast::*;
use crate::lexer::TokenKind;
use crate::whitespace::{char_at, is_svelte_ws, name_run_end};
use tsv_lang::{Error, Span};

use super::expression_tag::SequenceLocation;
use super::parser_impl::SvelteParser;
use super::{find_exact_tag_close, rcdata_close_at};

/// Svelte's `regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/` — the characters that
/// end a *tag* name run (`read_tag` in `1-parse/state/element.js`). Everything else is part
/// of the name, including the `=`, `"` and `'` that end an *attribute* name
/// ([`is_attr_name_terminator`](super::attribute)) — a tag name is read against the narrower
/// class, so the two are deliberately separate predicates.
///
/// A `char` question, not a byte one: the `\s` arm is Unicode ([`is_svelte_ws`]).
const fn is_tag_name_terminator(c: char) -> bool {
    is_svelte_ws(c) || matches!(c, '/' | '>')
}

/// Byte offset of the first tag-name terminator at/after `start`. The name is
/// `source[start..end]`.
///
/// The lexer's identifier scan is narrower than this run, so a glued symbol (`<div%x>`), a
/// `{` (`<div{x}>`) or a Unicode space outside JS `\s` (U+0085 NEL) sits *past* the token
/// end and still belongs to the name. In the common case the token already ends at a
/// terminator, so the scan stops on its first check.
pub(super) fn tag_name_end(source: &str, start: usize) -> usize {
    name_run_end(source, start, is_tag_name_terminator)
}

/// Whether `name` is an acceptable Svelte element or component tag name, mirroring Svelte's
/// accept gate (`1-parse/state/element.js`): `is_valid_element_name(name) ||
/// regex_valid_component_name.test(name)`. The component half reuses the shared
/// [`is_component_name`] predicate — `char::is_uppercase`, a superset of Svelte's `\p{Lu}`
/// over the *valid* set — so a non-ASCII-initial name is admissible only when it is a
/// component (`<Δfoo>`, `<ns.Comp>`), never a plain non-ASCII element (`<élan>`, `<你好>`).
/// tsv's tag lexer reads a raw name run, so without this whole-name check it over-accepts
/// names the HTML tokenizer (start tag only on `<` + ASCII alpha) and Svelte both reject.
fn is_valid_tag_name(name: &str) -> bool {
    is_valid_element_name(name) || is_component_name(name)
}

/// Whether `name` claims the reserved `svelte:` namespace without naming a meta tag —
/// Svelte's `tag.name.startsWith('svelte:') && !meta_tags.has(tag.name)`
/// (`1-parse/state/element.js`), which errors as `svelte_meta_invalid_tag`.
///
/// Rejecting is the only representable answer, which is what makes this a *parser* concern
/// rather than one a later validate pass could own: `meta_tags.has(name)` **is** the node-type
/// decision, so accepting `<svelte:foo>` forces a fabricated `RegularElement { name:
/// "svelte:foo" }` — a shape Svelte's AST never contains, and no post-parse pass can repair a
/// wire that is already wrong. Svelte's editor-tolerant `loose` mode grants this check no
/// escape either (unlike `tag_invalid_name`, which forgives a mid-typed `<div.`).
///
/// Only that exact prefix is reserved: `<sveltex:foo>` and `<foo:bar>` stay ordinary
/// namespaced elements.
fn is_reserved_namespace_miss(name: &str) -> bool {
    name.starts_with("svelte:") && !SpecialElementTag::is_meta_tag_name(name)
}

/// Where the element being read sits: directly in the component root's fragment, or under
/// anything else — an element, a component, a block, another meta tag. Svelte's
/// `parent.type !== 'Root'`, the placement half of the root-only meta tag rule
/// ([`RootOnlyMetaTag`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ElementParent {
    Root,
    Nested,
}

/// Svelte's `root_only_meta_tags` (`1-parse/state/element.js`): the meta tags a component
/// may hold only as a direct child of its root, and at most once each. Both rules are
/// parse errors there, raised where the tag name is read — `svelte_meta_duplicate` first,
/// then `svelte_meta_invalid_placement` — so they are parse errors here, at the same
/// point ([`SvelteParser::admit_root_only_meta_tag`]).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RootOnlyMetaTag {
    Head,
    Options,
    Window,
    Document,
    Body,
}

impl RootOnlyMetaTag {
    pub(crate) fn from_tag_name(name: &str) -> Option<Self> {
        Some(match name.strip_prefix("svelte:")? {
            "head" => Self::Head,
            "options" => Self::Options,
            "window" => Self::Window,
            "document" => Self::Document,
            "body" => Self::Body,
            _ => return None,
        })
    }

    /// This tag's bit in the parser's seen set (`SvelteParser::root_only_meta_tags_seen`).
    const fn bit(self) -> u8 {
        1 << self as u8
    }
}

/// Whether `name` is the meta tag that has **no fragment-node form**, so it exists only
/// where the root dispatch (`parse_root`) builds it. Exactly `svelte:options` today: it
/// fills `Root`'s single `Option` slot rather than becoming a `FragmentNode`. Reaching
/// the element parser therefore means it is nested inside an element or a block.
///
/// The root-only rule ([`RootOnlyMetaTag`]) already rejects that placement, as it does for
/// the four siblings — `svelte:head` / `svelte:window` / `svelte:body` / `svelte:document`,
/// which each have a `SpecialElementTag`. This predicate states the stronger fact about
/// `svelte:options` alone: accepting one here would fabricate a
/// `RegularElement { name: "svelte:options" }`, a shape Svelte's AST never contains, so the
/// element parser refuses it whatever parent it was handed.
///
/// Derived from [`SpecialElementTag`] rather than a second literal, so the two cannot
/// drift: within the reserved namespace, a meta tag with no variant *is* a meta tag with
/// no fragment-node form. The `svelte:` gate comes first for the reason
/// [`SpecialElementTag::is_meta_tag_name`] documents — without it `slot` inside a
/// `<template shadowrootmode>` would match, since it is admitted as a meta tag yet
/// deliberately yields `None` there.
fn is_root_slot_meta_tag(name: &str) -> bool {
    name.starts_with("svelte:")
        && SpecialElementTag::is_meta_tag_name(name)
        && SpecialElementTag::from_tag_name(name, false, false).is_none()
}

/// Port of Svelte's `is_valid_element_name` (`1-parse/state/element.js`): a doctype
/// (`<!DOCTYPE>`), a namespaced meta/element name (`<svelte:head>`, `<foo:bar>`), or a valid
/// HTML/SVG/MathML/custom element name (`REGEX_VALID_TAG_NAME`).
///
/// The three arms are pure and the answer is their union, so the order is free; the local
/// name goes first because it is what nearly every tag is (`div`, `Button`, `my-elem`), and
/// it answers those in one walk with no search for a `:` the other two arms would begin with.
fn is_valid_element_name(name: &str) -> bool {
    is_regex_valid_tag_name(name) || is_doctype_name(name) || is_namespaced_name(name)
}

/// `regex_doctype_name` = `/^![a-zA-Z]+$/` — `!` then one or more ASCII letters.
fn is_doctype_name(name: &str) -> bool {
    name.strip_prefix('!')
        .is_some_and(|rest| !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_alphabetic()))
}

/// `regex_namespaced_name` = `/^[a-zA-Z][a-zA-Z0-9]*:[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9]$/` — a
/// single ASCII colon; the prefix is `[a-zA-Z][a-zA-Z0-9]*`, the local part is ≥2 ASCII chars
/// starting with a letter and ending alphanumeric (interior `-` allowed). Covers `svelte:*`
/// meta tags and namespaced regular elements (`foo:bar`).
fn is_namespaced_name(name: &str) -> bool {
    // A byte search, not `split_once(':')`: a tag name is a few bytes, and the `char` pattern
    // costs an out-of-line searcher call that outweighs the walk it does. The `:` is ASCII, so
    // either side of it is a char boundary.
    let bytes = name.as_bytes();
    let Some(colon) = bytes.iter().position(|&b| b == b':') else {
        return false;
    };
    let (prefix, local) = (&bytes[..colon], &bytes[colon + 1..]);
    if !prefix.first().is_some_and(u8::is_ascii_alphabetic)
        || !prefix[1..].iter().all(u8::is_ascii_alphanumeric)
    {
        return false;
    }
    local.len() >= 2
        && local[0].is_ascii_alphabetic()
        && local[local.len() - 1].is_ascii_alphanumeric()
        && local[1..local.len() - 1]
            .iter()
            .all(|&b| b.is_ascii_alphanumeric() || b == b'-')
}

/// `REGEX_VALID_TAG_NAME` (`utils.js`): `/^[a-zA-Z][a-zA-Z0-9]*(-[…PCENChar…]*)?$/u` — an
/// ASCII-alpha start, ASCII alphanumerics, then optionally a hyphen introducing a run of
/// [tag-name tail characters](crate::html::is_svelte_tag_name_tail_char) (custom-element names such as
/// `<my-café>`). The Unicode ranges are literal, so no general-category lookup is needed.
///
/// The head is read on bytes: its whole class is ASCII, so a byte that is not an ASCII
/// alphanumeric or the hyphen — every byte of a non-ASCII character included — fails the
/// name there. Only the tail past the hyphen can hold a non-ASCII character, and it is
/// asked a character at a time, out of line: a custom-element name is the rare tag.
fn is_regex_valid_tag_name(name: &str) -> bool {
    let bytes = name.as_bytes();
    if !bytes.first().is_some_and(u8::is_ascii_alphabetic) {
        return false;
    }
    // the leading `[a-zA-Z0-9]*` run
    let mut i = 1;
    while i < bytes.len() && bytes[i].is_ascii_alphanumeric() {
        i += 1;
    }
    match bytes.get(i) {
        None => true,
        // the hyphen that opens the PCENChar tail; ASCII, so the cut is a char boundary
        Some(b'-') => is_tag_name_tail_run(&name[i + 1..]),
        Some(_) => false,
    }
}

/// Whether every character of `tail` is a
/// [tag-name tail character](crate::html::is_svelte_tag_name_tail_char) — the run a custom-element name
/// carries after its first hyphen (an empty one is valid).
#[cold]
#[inline(never)]
fn is_tag_name_tail_run(tail: &str) -> bool {
    tail.chars().all(crate::html::is_svelte_tag_name_tail_char)
}

/// Result type for parsing elements - either a regular element or a special element.
pub(crate) enum ParsedElement<'arena> {
    Element(Element<'arena>),
    SpecialElement(SpecialElement<'arena>),
}

/// Where an opening tag ended, as [`SvelteParser::read_opening_tag_end`] read it.
///
/// The two offsets are adjacent and easy to transpose, so they are named rather than
/// positional: `gt` is the `>` byte itself — the element's `open_tag_end`, which is where the
/// printer looks for a comment run trailing the last attribute — while `after_gt` is the byte
/// past it, where content starts and where a childless element ends.
pub(super) struct OpeningTagEnd {
    pub(super) self_closing: bool,
    pub(super) gt: u32,
    pub(super) after_gt: usize,
}

/// How an element's children may end without the element's own `</tag>` — HTML5 implicit tag
/// closing, which only HTML `RegularElement`s take part in (Svelte gates it on
/// `parent.type === 'RegularElement'`).
///
/// One byte, held by [`SvelteParser::parse_children`] across every child — a frame paid once
/// per level of element nesting. It settles whether the element has a row in the auto-close
/// table once, so an element without one never looks the table up per child; an element with one
/// looks its row up again at each child's opening tag (see
/// [`SvelteParser::next_open_tag_closes`] for why the row itself is not carried).
#[derive(Clone, Copy, PartialEq, Eq)]
enum ImplicitClose {
    /// Components and special elements (`svelte:*`, `slot`, …): only their own `</tag>` closes
    /// them, and a mismatched close is an error.
    Never,
    /// An HTML element: an ancestor's `</other>` also closes it, where it stands.
    AtAncestorClose,
    /// An HTML element with a row in the auto-close table (`<li>`, `<p>`, table parts,
    /// …): an opening tag among the row's triggers also closes it.
    AlsoAtNextTag,
}

impl ImplicitClose {
    /// The regime for an HTML element named `tag_name`.
    fn for_html_element(tag_name: &str) -> Self {
        if crate::html::svelte_autoclosing_row(tag_name).is_some() {
            Self::AlsoAtNextTag
        } else {
            Self::AtAncestorClose
        }
    }
}

/// What the `this` attribute of a special element bound to, if it was there at all.
///
/// The two tags accept different forms — `<svelte:element>` takes either (`this="div"` and
/// `this={tag}` are both legal), `<svelte:component>` only the braced one, a
/// non-`{expression}` being rejected as it is parsed — so what survives for the component is
/// always an `ExpressionTag`. One enum rather than a pair of `Option`s because the forms are
/// mutually exclusive: a value can't be built that claims to be both.
enum SpecialThisBinding<'arena> {
    /// Nothing bound: either no `this` was written, or the one that was is valueless
    /// (`<svelte:element this>`). The two need no distinction — Svelte rejects both under
    /// the one `svelte_element_missing_this`, and because the binding is taken **by index**
    /// a valueless `this` cannot hand its position to a later one.
    Absent,
    /// `<svelte:element this={tag}>` / `<svelte:element this="div">`.
    Element(SpecialThis<'arena>),
    /// `<svelte:component this={Comp}>`.
    Component(ExpressionTag<'arena>),
}

/// Result of parsing special element attributes: the attribute list with the binding `this`
/// lifted out of it, plus that binding.
///
/// Only the *first* `this` is lifted — a repeated one stays in the attribute list, as an
/// ordinary attribute of that name.
type SpecialElementAttrs<'arena> = (
    BumpVec<'arena, AttributeNode<'arena>>,
    SpecialThisBinding<'arena>,
);

impl<'a, 'arena> SvelteParser<'a, 'arena> {
    /// Whether an attribute list carries a `shadowrootmode` attribute (by name). On a
    /// RegularElement this marks a declarative-shadow-root template, so descendant `<slot>`s are
    /// ordinary elements rather than `SlotElement`s — mirrors Svelte's
    /// `parent_is_shadowroot_template` (`1-parse/state/element.js`).
    fn attrs_have_shadowrootmode(&self, attributes: &[AttributeNode<'arena>]) -> bool {
        attributes.iter().any(|attr| {
            matches!(attr, AttributeNode::Attribute(a) if a.name(self.source) == "shadowrootmode")
        })
    }

    /// Apply the root-only meta tag rule to the tag opening at `start` (its `<`), in
    /// Svelte's order: a second `tag` in the component is `svelte_meta_duplicate`, then one
    /// outside the root is `svelte_meta_invalid_placement`; a tag that passes both is
    /// recorded as seen. The seen set is per component, so a tag anywhere earlier at the
    /// root counts, whatever sits between the two.
    pub(crate) fn admit_root_only_meta_tag(
        &mut self,
        tag: RootOnlyMetaTag,
        tag_name: &str,
        start: usize,
        parent: ElementParent,
    ) -> Result<(), Error> {
        if self.root_only_meta_tags_seen & tag.bit() != 0 {
            return Err(self.error_msg_at(
                &format!("A component can only have one `<{tag_name}>` element"),
                start,
            ));
        }
        if parent != ElementParent::Root {
            return Err(self.error_msg_at(
                &format!("`<{tag_name}>` tags cannot be inside elements or blocks"),
                start,
            ));
        }
        self.root_only_meta_tags_seen |= tag.bit();
        Ok(())
    }

    /// Parse an element or special element: `<tag></tag>` or `<tag/>` or `<void>`
    ///
    /// Detects special elements (svelte:*, slot) and parses them appropriately.
    /// Returns a ParsedElement enum to distinguish between regular and special elements.
    ///
    /// `parent` says whether the element is a direct child of the component root, which the
    /// root-only meta tags require ([`RootOnlyMetaTag`]).
    pub(crate) fn parse_element_or_special(
        &mut self,
        parent: ElementParent,
    ) -> Result<ParsedElement<'arena>, Error> {
        // two wire levels: the element and the `Fragment` its children sit in (the depth
        // limit's count)
        self.nested(2, |p| p.parse_element_or_special_body(parent))
    }

    /// [`Self::parse_element_or_special`] inside its depth levels.
    fn parse_element_or_special_body(
        &mut self,
        parent: ElementParent,
    ) -> Result<ParsedElement<'arena>, Error> {
        let start = self.current_start();

        // Parse opening tag: <tag>
        self.expect(TokenKind::LeftAngle)?;

        if !self.check(TokenKind::Identifier) {
            return Err(self.error_expected_found("tag name"));
        }

        // Read the whole tag name as Svelte's `read_tag` does — a raw run up to `\s`, `/` or
        // `>`. The lexer only scanned the leading identifier, and grading that prefix alone
        // admits names Svelte rejects: a glued symbol (`<div%x>`), a `{` (`<div{x}>`) or a
        // U+0085 NEL all end the token but stay *inside* the name. The printer then re-emits
        // the tail as an attribute, spacing it away from the name — turning source Svelte
        // rejects into source it accepts. `&'a str` borrows the source, so the name survives
        // the `&mut self` calls below.
        let name_start = self.current_start();
        let name_end = tag_name_end(self.source, self.current_end());
        let tag_name = &self.source[name_start..name_end];
        let name_span = Span {
            start: name_start as u32,
            end: name_end as u32,
        };

        // The tag name must immediately follow `<`: `svelte.parse` rejects `< div>`
        // (whitespace after `<`, which tsv's tag lexer skips) — its `read_tag` reads an empty
        // name there, which no gate below accepts.
        if name_start != start + 1 {
            return Err(self.error_msg_at("Expected a valid element or component name", start + 1));
        }

        // The `svelte:` namespace is reserved, so a name in it that isn't a known meta tag is
        // a parse error — and, as in Svelte, this gate comes *before* the general name check.
        if is_reserved_namespace_miss(tag_name) {
            return Err(self.error_msg_at("Invalid `<svelte:...>` tag name", start + 1));
        }

        // A valid element or component name: `svelte.parse` rejects `<1>` / `<_x>` / `<$x>`
        // (invalid start char) and `<élan>` / `<divä>` (non-ASCII outside a valid
        // custom-element/component name) at parse.
        if !is_valid_tag_name(tag_name) {
            return Err(self.error_msg_at("Expected a valid element or component name", start + 1));
        }

        // The root-only meta tags: at most one of each, and only directly in the root. As in
        // Svelte, the check comes *after* the name gates and *before* the node-type decision
        // below. Only the root dispatch can build `<svelte:options>`, so one that reaches
        // this parser is nested whatever the caller said — tsv's AST cannot hold it here.
        if let Some(tag) = RootOnlyMetaTag::from_tag_name(tag_name) {
            let parent = if is_root_slot_meta_tag(tag_name) {
                ElementParent::Nested
            } else {
                parent
            };
            self.admit_root_only_meta_tag(tag, tag_name, start, parent)?;
        }

        self.advance_past_name(name_end)?;

        // Check if this is a special element. `title`/`slot` classification depends on the
        // ancestor context tracked on the parser (see `SpecialElementTag::from_tag_name`).
        if let Some(special_tag) = SpecialElementTag::from_tag_name(
            tag_name,
            self.in_svelte_head,
            self.in_shadowroot_template,
        ) {
            return self.parse_special_element_body(start, name_span, special_tag);
        }

        // Regular element or component. The tag name is span-identity
        // (`source[name_span]`), so nothing to intern here. The name's classification is
        // computed once, here, for the element to store; the body also reads the element kind
        // and the void test from it.
        let facts = TagFacts::compute(tag_name);

        self.parse_regular_element_body(start, tag_name, facts, name_span)
    }

    /// Consume what closes an opening tag: the optional self-closing `/`, then the required
    /// `>`. Shared by both element bodies and by the root dispatch's
    /// [`parse_svelte_options`](super::SvelteParser::parse_svelte_options), which differ in
    /// everything *around* the tag but terminate it identically.
    ///
    /// Consuming the `>` lexes the token behind it as template, so this is for a tag whose
    /// content IS template. One whose content is scanned raw stops at the `>` instead — see
    /// [`read_opening_tag_end`](Self::read_opening_tag_end).
    pub(super) fn finish_opening_tag(&mut self) -> Result<OpeningTagEnd, Error> {
        let opening = self.read_opening_tag_end()?;
        self.advance()?; // consume >
        Ok(opening)
    }

    /// Read what closes an opening tag — the optional self-closing `/`, consumed, then the
    /// required `>`, which is left as the current token.
    ///
    /// Stopping there is what a raw content scan needs: the bytes behind the `>` of a nested
    /// `<script>` / `<style>` or a `<textarea>` are not template, so no template token may be
    /// lexed from them. An unterminated `<!--` opening such content is text to Svelte, where
    /// a template lex of it fails for want of a `-->` — or, with one further down the
    /// document, passes only by reading through to it, so the element parses or not on bytes
    /// far outside it, which the formatter may move (a hoisted `<script !-->` takes the
    /// closer above the opener). The scan takes the content from `after_gt` and repositions
    /// the lexer itself; a caller whose content is template consumes the `>`
    /// ([`finish_opening_tag`](Self::finish_opening_tag)).
    fn read_opening_tag_end(&mut self) -> Result<OpeningTagEnd, Error> {
        let self_closing = self.check(TokenKind::Slash);
        if self_closing {
            self.advance()?; // consume /
        }

        if !self.check(TokenKind::RightAngle) {
            return Err(self.error_expected_kind(TokenKind::RightAngle));
        }

        // See `OpeningTagEnd` for which offset is which.
        Ok(OpeningTagEnd {
            self_closing,
            gt: self.current_start() as u32,
            after_gt: self.current_end(),
        })
    }

    /// Parse a regular element (HTML or component)
    fn parse_regular_element_body(
        &mut self,
        start: usize,
        tag_name: &'a str,
        facts: TagFacts,
        name_span: Span,
    ) -> Result<ParsedElement<'arena>, Error> {
        // Parse attributes
        let attributes = self.parse_attributes()?;

        // The `>` is still the current token: the two template regimes below consume it, the
        // two raw ones scan from behind it without lexing there (`read_opening_tag_end`).
        let opening = self.read_opening_tag_end()?;

        // Resolve this element's children and end offset. The four content regimes differ
        // only in how they produce `(nodes, end)`; the element is assembled once below.
        let (nodes, end): (&'arena [FragmentNode<'arena>], u32) = if facts.is_void()
            || opening.self_closing
        {
            // Void and self-closing elements have no children or closing tag
            // (classification lives in `crate::html`, shared with the printer).
            self.advance()?; // consume >
            (&[], opening.after_gt as u32)
        } else if facts.is_raw_text() {
            // Nested <style>/<script> are raw text (not parsed as Svelte template) —
            // per Svelte docs, "the <style> tag will be inserted as-is into the DOM".
            self.holds_nested_raw_text = true;
            self.nested_raw_text_nesting = self.nested_raw_text_nesting.max(self.island_nesting());
            let child_nodes = self.parse_raw_text_content(tag_name, opening.after_gt, start)?;
            let end = self.parse_closing_tag(tag_name)?;
            (child_nodes.into_bump_slice(), end)
        } else if tag_name == "textarea" {
            // <textarea> is RCDATA: raw text with live {expr} interpolation up to a
            // whitespace/attribute-tolerant </textarea…>, where `<` is literal text
            // (never a nested element). Svelte's sole RCDATA element — a sibling of the
            // <script>/<style> raw-text branch above, but interleaving Text +
            // ExpressionTag chunks.
            let (child_nodes, end) = self.parse_rcdata_content(opening.after_gt, start)?;
            (child_nodes.into_bump_slice(), end)
        } else {
            // Parse children. Only HTML elements participate in HTML5 implicit tag
            // closing (Svelte gates auto-close on `parent.type === 'RegularElement'`);
            // components and `svelte:*` keep the strict explicit-close requirement.
            // `parse_children` resolves `end` — past this element's `</tag>` (explicit
            // close) or at the `<` that implicitly closed it.
            self.advance()?; // consume >
            let is_html = facts.element_kind() == ElementKind::Html;
            // Enter this element's ancestor context: a RegularElement/Component resets head
            // context (mirrors Svelte's `parent_is_head`), and a RegularElement carrying
            // `shadowrootmode` turns on shadow-root-template context for its subtree
            // (`parent_is_shadowroot_template`).
            let in_shadow = self.in_shadowroot_template
                || (is_html && self.attrs_have_shadowrootmode(&attributes));
            let implicit_close = if is_html {
                ImplicitClose::for_html_element(tag_name)
            } else {
                ImplicitClose::Never
            };
            let (child_nodes, end) = self.parse_children_in_context(
                false,
                in_shadow,
                tag_name,
                opening.after_gt,
                start,
                implicit_close,
            )?;
            (child_nodes.into_bump_slice(), end)
        };

        Ok(ParsedElement::Element(Element {
            kind: facts.element_kind(),
            facts,
            attributes: attributes.into_bump_slice(),
            fragment: Fragment { nodes },
            span: Span {
                start: start as u32,
                end,
            },
            name_span,
            open_tag_end: opening.gt,
        }))
    }

    /// Parse a special element (svelte:*, slot, etc.)
    fn parse_special_element_body(
        &mut self,
        start: usize,
        name_span: Span,
        tag: SpecialElementTag,
    ) -> Result<ParsedElement<'arena>, Error> {
        // Parse attributes, extracting `this` for SvelteElement and SvelteComponent
        let (attributes, this) = self.parse_special_element_attributes(tag)?;

        // Construct the final SpecialElementKind, rejecting a `this`-less element/component
        // the way Svelte's parser does.
        let kind = self.build_special_element_kind(tag, this, name_span)?;

        let opening = self.finish_opening_tag()?;

        // Resolve children and end offset, then assemble once — the two content regimes here
        // are the same shape as the regular element's four above.
        let (nodes, end): (&'arena [FragmentNode<'arena>], u32) = if opening.self_closing {
            // Self-closing special elements have no children or closing tag
            (&[], opening.after_gt as u32)
        } else {
            // Parse children. Special elements (`svelte:*`, `slot`, …) are not HTML
            // RegularElements, so they never auto-close (`ImplicitClose::Never`) — a
            // mismatched close falls to `parse_closing_tag`'s strict error.
            //
            // Ancestor context: `<svelte:head>` turns head context on; every other special
            // element is transparent (Svelte's
            // `parent_is_head`/`parent_is_shadowroot_template` only stop at a
            // RegularElement/Component), so both flags carry through unchanged otherwise.
            let in_head = tag == SpecialElementTag::SvelteHead || self.in_svelte_head;
            let (child_nodes, end) = self.parse_children_in_context(
                in_head,
                self.in_shadowroot_template,
                tag.tag_name(),
                opening.after_gt,
                start,
                ImplicitClose::Never,
            )?;
            (child_nodes.into_bump_slice(), end)
        };

        Ok(ParsedElement::SpecialElement(SpecialElement {
            kind,
            attributes: attributes.into_bump_slice(),
            fragment: Fragment { nodes },
            span: Span {
                start: start as u32,
                end,
            },
            name_span,
            open_tag_end: opening.gt,
        }))
    }

    /// Build the final SpecialElementKind from the tag and the extracted `this` binding.
    ///
    /// The two tags that take a `this` require one: Svelte's parser rejects the element
    /// without it (`svelte_element_missing_this`) and the component likewise
    /// (`svelte_component_missing_this`), and tsv is a drop-in for that parser. Rejecting
    /// here rather than fabricating a placeholder is load-bearing, not pedantry — the
    /// invented binding would reach the printer and be emitted as source the author never
    /// wrote (a spurious `this=""` / `this={null}`), and the element's zero-span placeholder
    /// literal panics when formatted (`format_string_literal_from_ast` slicing `""[1..len-1]`).
    ///
    /// The component's *other* rejection — a `this` that is not an `{expression}` — belongs
    /// to [`Self::parse_special_element_attributes`] instead, which is where the difference
    /// between "no `this` at all" and "a `this` we cannot use" is still visible.
    fn build_special_element_kind(
        &self,
        tag: SpecialElementTag,
        this: SpecialThisBinding<'arena>,
        name_span: Span,
    ) -> Result<SpecialElementKind<'arena>, Error> {
        Ok(match tag {
            SpecialElementTag::SvelteElement => {
                // A missing `this` and a valueless one are both `Absent` — Svelte reports
                // them with the one `svelte_element_missing_this`.
                let SpecialThisBinding::Element(tag) = this else {
                    return Err(self.error_msg_at(
                        "`<svelte:element>` must have a 'this' attribute with a value",
                        name_span.start as usize,
                    ));
                };
                SpecialElementKind::SvelteElement { tag }
            }
            SpecialElementTag::SvelteComponent => {
                // Only `Absent` can reach the `else`: a component's valueless or
                // non-`{expression}` `this` was already rejected as it was parsed.
                let SpecialThisBinding::Component(expression) = this else {
                    return Err(self.error_msg_at(
                        "`<svelte:component>` must have a 'this' attribute",
                        name_span.start as usize,
                    ));
                };
                SpecialElementKind::SvelteComponent { expression }
            }
            SpecialElementTag::SvelteHead => SpecialElementKind::SvelteHead,
            SpecialElementTag::SvelteWindow => SpecialElementKind::SvelteWindow,
            SpecialElementTag::SvelteBody => SpecialElementKind::SvelteBody,
            SpecialElementTag::SvelteDocument => SpecialElementKind::SvelteDocument,
            SpecialElementTag::SvelteSelf => SpecialElementKind::SvelteSelf,
            SpecialElementTag::SlotElement => SpecialElementKind::SlotElement,
            SpecialElementTag::SvelteFragment => SpecialElementKind::SvelteFragment,
            SpecialElementTag::SvelteBoundary => SpecialElementKind::SvelteBoundary,
            SpecialElementTag::TitleElement => SpecialElementKind::TitleElement,
        })
    }

    /// Parse attributes for a special element, extracting `this` for svelte:element and svelte:component
    ///
    /// Only the **first** `this` binds, and only on the two tags that take one. This mirrors
    /// Svelte's `findIndex` + `splice` directly — take the binding out **by index** — rather
    /// than folding over the list, which is what makes "a repeated `this` is neither an error
    /// nor a replacement" structural: there is no second chance to give away. It stays an
    /// ordinary attribute, and the uniqueness check that would otherwise reject it is waived
    /// for exactly this name (`<svelte:element bind:this this=…>` is legal).
    fn parse_special_element_attributes(
        &mut self,
        tag: SpecialElementTag,
    ) -> Result<SpecialElementAttrs<'arena>, Error> {
        let mut attributes = self.parse_attributes()?;

        // Nine of the eleven special tags never bind a `this`; for them the list is already
        // the answer.
        if !tag.takes_this() {
            return Ok((attributes, SpecialThisBinding::Absent));
        }

        // Svelte's `findIndex`. Compare the span-identity name directly — no per-attribute
        // `String`.
        let source = self.source;
        let found = attributes
            .iter()
            .enumerate()
            .find_map(|(i, node)| match node {
                AttributeNode::Attribute(a) if a.name(source) == "this" => Some((i, a)),
                _ => None,
            });
        let Some((index, a)) = found else {
            return Ok((attributes, SpecialThisBinding::Absent));
        };

        let this = if tag == SpecialElementTag::SvelteElement {
            // Svelte keeps chunk[0] and warns (`svelte_element_invalid_this`) when the value
            // is not a lone `{expression}` — a Svelte 4 behaviour it preserves on purpose. A
            // valueless `this` is the one it rejects, which it leaves to
            // [`Self::build_special_element_kind`].
            match a.value.and_then(<[_]>::first) {
                // Keep the whole tag, not just its expression: the `{…}` span is where the
                // printer looks for comments.
                Some(AttributeValue::ExpressionTag(et)) => {
                    SpecialThisBinding::Element(SpecialThis::Braced(et.clone()))
                }
                // String value: no expression is parsed, so keep the decoded text itself. It
                // is copied once into the arena (the source slice carries entities and no
                // quotes, so it is not a verbatim slice of it).
                Some(AttributeValue::Text(t)) => SpecialThisBinding::Element(SpecialThis::Plain {
                    content: self.alloc_str_in(&t.data(source)),
                    span: t.span,
                }),
                None => SpecialThisBinding::Absent,
            }
        } else {
            // `<svelte:component>`, the only other tag `takes_this` admits. Svelte's
            // `is_expression_attribute`: exactly one chunk, and an `{expression}`. A bare
            // `this`, a string, or a multi-chunk value (`this="a{b}"`, `this={a}{b}`) is
            // rejected outright — where `<svelte:element>` above merely warns.
            let Some([AttributeValue::ExpressionTag(et)]) = a.value else {
                return Err(self.error_msg_at(
                    "Invalid component definition — must be an `{expression}`",
                    a.span.start as usize,
                ));
            };
            // Keep the whole tag — see the `svelte:element` arm above.
            SpecialThisBinding::Component(et.clone())
        };

        // Svelte's `splice`: the binding leaves the list, everything else keeps its place.
        attributes.remove(index);

        Ok((attributes, this))
    }

    /// Parse an element's children under a given ancestor context (`in_svelte_head` /
    /// `in_shadowroot_template`), restoring the caller's context afterward — so the context is
    /// scoped to this subtree and siblings are unaffected (Svelte's stack push/pop). Delegates to
    /// [`Self::parse_children`] for the actual parse; the save/restore is the only added work. The
    /// restore also runs on the error path, though the parse aborts then anyway.
    fn parse_children_in_context(
        &mut self,
        in_svelte_head: bool,
        in_shadowroot_template: bool,
        tag_name: &str,
        opening_tag_end: usize,
        start: usize,
        implicit_close: ImplicitClose,
    ) -> Result<(BumpVec<'arena, FragmentNode<'arena>>, u32), Error> {
        let saved_head = self.in_svelte_head;
        let saved_shadow = self.in_shadowroot_template;
        self.in_svelte_head = in_svelte_head;
        self.in_shadowroot_template = in_shadowroot_template;
        let result = self.parse_children(tag_name, opening_tag_end, start, implicit_close);
        self.in_svelte_head = saved_head;
        self.in_shadowroot_template = saved_shadow;
        result
    }

    /// Parse children until this element's end is resolved, returning the child
    /// nodes and the element's end byte offset. The end is either past this
    /// element's own `</tag>` (consumed here via `parse_closing_tag`) or, under
    /// HTML5 implicit tag closing, the offset of the `<` that triggered the implicit
    /// close (an ancestor's `</other>` or an auto-closing sibling `<next>`, left
    /// unconsumed for the caller's caller to re-read — matching Svelte's
    /// `parent.end = start`). `implicit_close` says which of those implicit closes
    /// apply: only HTML `RegularElement`s participate (see [`ImplicitClose`]). Callers
    /// that establish an ancestor context (head / shadowroot-template) should go through
    /// [`Self::parse_children_in_context`].
    fn parse_children(
        &mut self,
        tag_name: &str,
        opening_tag_end: usize,
        start: usize,
        implicit_close: ImplicitClose,
    ) -> Result<(BumpVec<'arena, FragmentNode<'arena>>, u32), Error> {
        let mut child_nodes = self.bvec();
        let mut last_end = opening_tag_end;

        loop {
            // Capture text/whitespace gaps between tokens
            self.capture_text_if_gap(last_end, &mut child_nodes)?;

            if self.check(TokenKind::Comment) {
                let comment = self.parse_comment()?;
                last_end = comment.span.end_usize();
                child_nodes.push(FragmentNode::Comment(comment));
            } else if self.check(TokenKind::LeftBrace) {
                let tag = self.parse_brace_tag()?;
                last_end = tag.span().end_usize();
                child_nodes.push(tag);
            } else if self.check(TokenKind::LeftAngle) {
                // A `<…>` may end this element rather than nest inside it. Both
                // auto-close paths leave the triggering `<` unconsumed for the
                // caller's caller and end this element at its offset.
                // TODO: a diagnostics layer would record the implicit close at these
                // two points (Svelte's `element_implicitly_closed` warning).
                if self.is_next_token(TokenKind::Slash)? {
                    // A closing tag `</name>`. Our own close → consume it. An HTML
                    // element's mismatched close is an ancestor's → leave it, so it
                    // unwinds to the matching ancestor (or errors at the root if none
                    // matches). A non-HTML parent takes the strict mismatch error.
                    let end = if implicit_close != ImplicitClose::Never
                        && !self.is_closing_tag_for(tag_name)
                    {
                        self.current_start() as u32
                    } else {
                        self.parse_closing_tag(tag_name)?
                    };
                    return Ok((child_nodes, end));
                }
                // An opening tag `<next>` that the auto-close table says closes
                // this element — leave it for the parent to adopt as a sibling. Only an element
                // with a row in the table asks (most have none, so most children skip the peeked
                // name and the lookup), and skipping changes nothing: `is_next_token` above has
                // already filled the peek slot, so the read is side-effect-free and cannot fail.
                if implicit_close == ImplicitClose::AlsoAtNextTag
                    && self.next_open_tag_closes(tag_name)?
                {
                    return Ok((child_nodes, self.current_start() as u32));
                }
                // Parse child element (may be special or regular)
                let child = self.parse_element_or_special(ElementParent::Nested)?;
                match child {
                    ParsedElement::Element(elem) => {
                        last_end = elem.span.end_usize();
                        child_nodes.push(FragmentNode::Element(elem));
                    }
                    ParsedElement::SpecialElement(elem) => {
                        last_end = elem.span.end_usize();
                        child_nodes.push(FragmentNode::SpecialElement(elem));
                    }
                }
            } else if self.check(TokenKind::BlockOpen) {
                let block = self.parse_block()?;
                last_end = block.span().end_usize();
                child_nodes.push(block);
            } else if self.check(TokenKind::TagOpen) {
                let tag = self.parse_template_tag()?;
                last_end = tag.span().end_usize();
                child_nodes.push(tag);
            } else if self.check(TokenKind::Eof) {
                return Err(self.error_unclosed_element(tag_name, start));
            } else {
                return Err(self.error_expected_found(
                    "element, expression tag, comment, block, or closing tag",
                ));
            }
        }
    }

    /// Whether the opening tag at the current `<` implicitly closes the element `tag_name` —
    /// its name is one of the triggers in `tag_name`'s row of the auto-close table. Only
    /// asked for an element that has a row ([`ImplicitClose::AlsoAtNextTag`]), so the lookup
    /// here always finds one.
    ///
    /// The row is looked up again rather than carried in [`ImplicitClose`]: passing it in would
    /// hand this call a loop-invariant byte argument, whose widened copy the compiler keeps in a
    /// stack slot of its own in that recursive frame, where `tag_name` is already held. Outlined
    /// so the trigger `match` and the peeked name hold no stack slot there either.
    #[inline(never)]
    fn next_open_tag_closes(&mut self, tag_name: &str) -> Result<bool, Error> {
        let Some(next_name) = self.peek_open_tag_name()? else {
            return Ok(false);
        };
        Ok(crate::html::svelte_autoclosing_row(tag_name)
            .is_some_and(|row| row.is_closed_by(next_name)))
    }

    /// The "Unclosed element" error for `parse_children`, outlined so the `format!` of the
    /// tag name holds no stack slot in that recursive frame.
    #[cold]
    #[inline(never)]
    fn error_unclosed_element(&self, tag_name: &str, start: usize) -> Error {
        self.error_unclosed_at(&format!("element: <{tag_name}>"), start)
    }

    /// Whether the `</…>` closing tag at the current `<` names `tag_name`.
    ///
    /// The parser is positioned at the `<` of a closing tag (current token
    /// `LeftAngle`, next `Slash`); this reads the name straight from source
    /// without consuming tokens, so a non-matching (ancestor's) close can be left
    /// in place for the caller to re-read. The name must be exactly `tag_name`
    /// followed by a tag-name terminator (whitespace, `/`, `>`, or EOF), so `</li>`
    /// matches `li` but `</link>` does not.
    ///
    /// The terminators are [`is_tag_name_terminator`] — Svelte's
    /// `regex_whitespace_or_slash_or_closing_tag = /(\s|\/|>)/`, the same class the opening
    /// tag's name run ends at — so the whitespace arm is the full
    /// [`crate::whitespace::is_svelte_ws`] set, not just ASCII: `</div\u{a0}>` closes a `div`.
    fn is_closing_tag_for(&self, tag_name: &str) -> bool {
        let name_start = self.current_start() + 2; // past `</`
        if !self
            .source
            .get(name_start..)
            .is_some_and(|rest| rest.starts_with(tag_name))
        {
            return false;
        }
        // a terminator, or EOF
        match char_at(self.source, name_start + tag_name.len()) {
            None => true,
            Some((c, _)) => is_tag_name_terminator(c),
        }
    }

    /// The tag name of the opening tag at the current `<` (peeked, not consumed).
    ///
    /// Returns `None` when the token after `<` is not an identifier (e.g. `<!…`,
    /// `<>`), in which case there is no name to test against the auto-close table.
    /// The returned `&'a str` borrows the immutable source, so it survives the
    /// `&mut self` borrow (same pattern as `current_value`).
    ///
    /// Reads the whole name **run** ([`tag_name_end`]) rather than the peeked identifier
    /// token, so the name handed to the auto-close table is the one the element parser will
    /// grade. A token-only read is verdict-neutral here — a run that outran its token can
    /// only be an invalid name, which `parse_element_or_special` rejects on either branch —
    /// but the same shortcut *was* a live bug in `is_next_tag`, so this asks the same
    /// question the same way rather than resting on the downstream reject.
    fn peek_open_tag_name(&mut self) -> Result<Option<&'a str>, Error> {
        self.fill_peek()?;
        Ok(self.peek.as_ref().and_then(|p| {
            (p.kind == TokenKind::Identifier).then(|| {
                let name_start = p.start as usize;
                let name_end = tag_name_end(self.source, p.end as usize);
                &self.source[name_start..name_end]
            })
        }))
    }

    /// Consume a `</name>` closing tag (lexer positioned at `<`) and return the byte
    /// offset past `>`. Shared by the generic element path and the raw-text
    /// `<script>` / `<style>` parsers, so all three agree on tag-close tokenization
    /// (whitespace before `>` skipped by the lexer, mismatch → one error).
    ///
    /// Compares the identifier **token**, not the name run — sound here, unlike in
    /// `is_next_tag`, because the very next check is `expect(RightAngle)`: every character
    /// that would extend the run past its token is by definition not `\s`, `/` or `>`, so a
    /// longer run cannot reach the `>` and `</div%x>` is rejected either way.
    pub(super) fn parse_closing_tag(&mut self, expected_name: &str) -> Result<u32, Error> {
        self.expect(TokenKind::LeftAngle)?;
        self.expect(TokenKind::Slash)?;

        if !self.check(TokenKind::Identifier) {
            return Err(self.error_expected_found("tag name"));
        }

        let closing_tag_name = self.current_value();
        if closing_tag_name != expected_name {
            return Err(self.error_msg(&format!(
                "Mismatched tags: expected closing tag for '{expected_name}' but found '{closing_tag_name}'"
            )));
        }
        self.advance()?;

        let end = self.current_end();
        self.expect(TokenKind::RightAngle)?;

        Ok(end as u32)
    }

    /// Parse raw text content for nested `<style>` and `<script>` elements.
    /// These elements should not have their content parsed as Svelte template syntax.
    /// Returns a single Text node with the raw content, or empty vec if no content.
    fn parse_raw_text_content(
        &mut self,
        tag_name: &str,
        content_start: usize,
        element_start: usize,
    ) -> Result<BumpVec<'arena, FragmentNode<'arena>>, Error> {
        // Nested raw-text uses an EXACT `</tag>` close (no `\s*` before `>`), matching
        // Svelte's generic element parser — unlike a top-level `<script>`/`<style>`, which
        // reads via the whitespace-tolerant `find_raw_text_close`. See that function.
        let content_end = find_exact_tag_close(self.source, content_start, tag_name.as_bytes())
            .ok_or_else(|| {
                self.error_msg_at(&format!("Unterminated <{tag_name}> element"), element_start)
            })?;

        // Reposition the lexer to the closing tag — the first token lexed since the opener's
        // `>`, the content between them being scanned and never lexed. We resume AT the `<`,
        // a `LeftAngle` in either mode; `parse_closing_tag` consumes the close and its `>`
        // returns the lexer to template mode. Contrast `parse_rcdata_content`, which resumes
        // PAST the close's `>`.
        self.advance_to_position(content_end)?;

        // Create a Text node (Svelte always emits one, even if empty)
        let span = Span {
            start: content_start as u32,
            end: content_end as u32,
        };
        let mut nodes = self.bvec();
        nodes.push(FragmentNode::Text(Text::new(
            span,
            TextDecoding::Raw,
            span,
            self.source,
        )));
        Ok(nodes)
    }

    /// Parse RCDATA content for `<textarea>` — Svelte's sole RCDATA element (verified
    /// against the oracle: `<title>` is *not* RCDATA in Svelte; its children parse as a
    /// normal fragment). RCDATA (HTML §13.2.5.2) is raw text with live `{expr}`
    /// interpolation but no nested elements: `<` is literal text, read up to a
    /// whitespace/attribute-tolerant `</textarea…>`. Ports Svelte's `read_sequence`
    /// (`1-parse/state/element.js`): scan the content bytes, flushing `Text` chunks and,
    /// at each `{`, parsing an `ExpressionTag`.
    ///
    /// Returns the child nodes and the element end (byte offset past the close tag's `>`),
    /// and repositions the lexer there. Not routed through `parse_closing_tag` — the close
    /// tag may carry whitespace/attributes (`</textarea data-x >`) that its exact
    /// tokenization rejects.
    fn parse_rcdata_content(
        &mut self,
        content_start: usize,
        element_start: usize,
    ) -> Result<(BumpVec<'arena, FragmentNode<'arena>>, u32), Error> {
        // `&'a [u8]` borrowed from the immutable source, so it survives the `&mut self`
        // expression-tag parse below (its lifetime is the source's, not this borrow's).
        let bytes = self.source.as_bytes();
        let mut nodes = self.bvec();
        let mut chunk_start = content_start;
        let mut i = content_start;

        let close_gt = loop {
            // A `</textarea…>` at `i` ends the RCDATA (checked first, like `read_sequence`'s
            // `done()`); flush the pending text and stop.
            if let Some((close_lt, close_gt)) = rcdata_close_at(self.source, i, b"textarea") {
                self.push_rcdata_text(&mut nodes, chunk_start, close_lt);
                break close_gt;
            }
            match bytes.get(i) {
                // EOF before any close — Svelte's `unexpected_eof` (both parsers reject).
                None => {
                    return Err(self.error_unclosed_at("<textarea> element", element_start));
                }
                // `{expr}` — flush the text before it, parse the tag off the byte position
                // (no lexer), resume after the `}`. A `{` with no matching `}` errors in
                // `parse_expression_tag_at`, matching Svelte's reject. A `{#`/`{@` is not an
                // expression here at all: it is rejected first, like Svelte's `read_sequence`.
                Some(b'{') => {
                    self.push_rcdata_text(&mut nodes, chunk_start, i);
                    let tag =
                        self.parse_sequence_expression_tag_at(i, SequenceLocation::InsideTextarea)?;
                    i = tag.span.end as usize;
                    chunk_start = i;
                    nodes.push(FragmentNode::ExpressionTag(tag));
                }
                // Any other byte (including `<`) is literal RCDATA text.
                Some(_) => i += 1,
            }
        };

        let end = close_gt + 1;
        // After `</textarea…>` the document is template again, and `advance_to_position`
        // keeps whatever mode it finds. The lexer's last token is the opener's `>`, which
        // left it in template mode; the mode is stated here rather than inherited, so a
        // bare-text sibling (`</textarea>x`) can never lex as a tag's Identifier.
        self.lexer.inside_tag = false;
        self.advance_to_position(end)?;
        Ok((nodes, end as u32))
    }

    /// Push a `Text` chunk covering `[start, end)`, skipping it when empty (Svelte's
    /// `flush`: an empty chunk emits no node). RCDATA text decodes with attribute-value
    /// rules — Svelte hardcodes `decode_character_references(raw, true)` in `read_sequence`.
    fn push_rcdata_text(
        &self,
        nodes: &mut BumpVec<'arena, FragmentNode<'arena>>,
        start: usize,
        end: usize,
    ) {
        if end > start {
            let span = Span {
                start: start as u32,
                end: end as u32,
            };
            nodes.push(FragmentNode::Text(Text::new(
                span,
                TextDecoding::AttributeValue,
                span,
                self.source,
            )));
        }
    }
}

#[cfg(test)]
mod tests {
    use super::{
        RootOnlyMetaTag, is_doctype_name, is_namespaced_name, is_regex_valid_tag_name,
        is_reserved_namespace_miss, is_root_slot_meta_tag, is_valid_element_name,
        is_valid_tag_name, tag_name_end,
    };
    use crate::ast::is_component_name;

    /// The run ends at `(\s|\/|>)` or EOF and at **nothing else**, so whatever the lexer's
    /// narrower identifier scan stopped at is still part of the name — that is the whole
    /// point of reading the run rather than grading the token.
    ///
    /// Each case is `(name, tail)`: the expected end is `name.len()`, so a position is
    /// built from a prefix length rather than searched for.
    #[test]
    fn tag_name_run_ends_only_at_whitespace_slash_or_gt() {
        for (name, tail) in [
            // the three terminators, and EOF
            ("div", ">"),
            ("div", "/>"),
            ("div", " x"),
            ("div", ""),
            // the whitespace arm is JS `\s`, so it is Unicode-wide
            ("div", "\u{a0}>"),   // NO-BREAK SPACE
            ("div", "\u{feff}>"), // ZWNBSP — in JS `\s`, not in Rust's `White_Space`
            // U+0085 NEL is `White_Space` but NOT JS `\s`, so it stays in the name
            ("div\u{85}x", ">"),
            // a glued symbol or `{` is likewise ordinary name content
            ("div%x", ">"),
            ("div{x}", ">"),
            // `=`, `"` and `'` end an ATTRIBUTE name but not a tag name — the two
            // terminator classes are deliberately different predicates
            ("div=x", ">"),
            ("div\"x", ">"),
            ("div'x", ">"),
            // valid names the run must carry whole
            ("my-elem", ">"),
            ("my-caf\u{e9}", ">"),
            ("svelte:head", ">"),
            ("Foo.Bar", ">"),
            ("!DOCTYPE", " html>"),
        ] {
            let source = format!("{name}{tail}");
            assert_eq!(tag_name_end(&source, 0), name.len(), "{source:?}");
        }
    }

    /// [`is_component_name`] agrees with its two-search spelling at every arrangement of the
    /// characters it keys on — the refusing `:`, the admitting `.`, an ASCII and a non-ASCII
    /// uppercase initial, their lowercase twins, and a character neither reads.
    #[test]
    fn component_name_matches_its_reference() {
        const PIECES: [&str; 8] = [":", ".", "A", "a", "\u{394}", "\u{e9}", "-", "\u{1f600}"];
        crate::test_support::for_every_arrangement(&PIECES, 5, |name| {
            let reference = !name.contains(':')
                && (name.contains('.') || name.chars().next().is_some_and(char::is_uppercase));
            assert_eq!(is_component_name(name), reference, "{name:?}");
        });
    }

    #[test]
    fn component_classification() {
        // Uppercase first char ⇒ component.
        assert!(is_component_name("Comp"));
        assert!(is_component_name("ns.Comp"));
        assert!(is_component_name("deep.nested.Comp"));
        // Any dotted tag is member access ⇒ component, regardless of segment casing.
        assert!(is_component_name("Object.component"));
        assert!(is_component_name("object.property"));
        assert!(is_component_name("ns.lower"));
        // No dot + lowercase first char ⇒ regular HTML element.
        assert!(!is_component_name("div"));
        // Empty name has no first char.
        assert!(!is_component_name(""));
        // Non-ASCII uppercase still counts (the printer must agree — see TagFacts).
        assert!(is_component_name("Über"));
        assert!(!is_component_name("élan"));
        // A `:`-namespaced tag is a RegularElement, never a component — even with an
        // uppercase prefix (Svelte's `regex_valid_component_name` rejects `:`).
        assert!(!is_component_name("foo:bar"));
        assert!(!is_component_name("Foo:bar"));
        assert!(!is_component_name("svelte:head"));
    }

    #[test]
    fn accepts_valid_element_and_component_names() {
        // Standard HTML/SVG elements and ASCII custom elements.
        for name in [
            "div",
            "h1",
            "a",
            "my-elem",
            "foreignObject",
            "annotation-xml",
        ] {
            assert!(is_valid_tag_name(name), "should accept <{name}>");
        }
        // Custom-element names with PCENChar after the hyphen (non-ASCII, `.`, `_`).
        for name in ["my-café", "x-\u{00B7}", "a-b.c", "a-b_c"] {
            assert!(is_valid_tag_name(name), "should accept <{name}>");
        }
        // Components: uppercase-initial (incl. non-ASCII \p{Lu}) and dotted member access.
        for name in ["Foo", "MyComp", "Δfoo", "ns.Comp", "deep.nested.Comp"] {
            assert!(is_valid_tag_name(name), "should accept <{name}>");
        }
        // Doctype and namespaced/meta tags.
        for name in [
            "!DOCTYPE",
            "!doctype",
            "svelte:head",
            "svelte:component",
            "foo:bar",
        ] {
            assert!(is_valid_tag_name(name), "should accept <{name}>");
        }
    }

    #[test]
    fn rejects_invalid_tag_names() {
        // Non-ASCII start that is not a component (lowercase / caseless / titlecase). A
        // titlecase letter (`Dž`, category Lt) is not `\p{Lu}` nor `Uppercase`, so it is not a
        // component name — matching Svelte.
        for name in ["élan", "δfoo", "你好", "Džfoo"] {
            assert!(!is_valid_tag_name(name), "should reject <{name}>");
        }
        // ASCII start but a non-ASCII letter mid-name without a hyphen (not PCENChar-eligible).
        assert!(!is_valid_tag_name("divä"));
        // Stray delimiters the lexer folds into the name run.
        assert!(!is_valid_tag_name("a|b"));
        // Namespaced local part too short / hyphen-terminated / non-ASCII.
        for name in ["foo:b", "foo:bar-", "foo:bär"] {
            assert!(!is_namespaced_name(name), "namespaced should reject {name}");
            assert!(
                !is_valid_element_name(name),
                "element name should reject {name}"
            );
        }
        // Doctype must be `!` + ASCII letters only.
        assert!(!is_valid_element_name("!-"));
        assert!(!is_valid_element_name("!doc7"));
    }

    /// `regex_namespaced_name` transcribed through `split_once(':')`, the reference the byte
    /// search in [`is_namespaced_name`] is graded against.
    fn namespaced_name_reference(name: &str) -> bool {
        let Some((prefix, local)) = name.split_once(':') else {
            return false;
        };
        let (prefix, local) = (prefix.as_bytes(), local.as_bytes());
        prefix.first().is_some_and(u8::is_ascii_alphabetic)
            && prefix[1..].iter().all(u8::is_ascii_alphanumeric)
            && local.len() >= 2
            && local[0].is_ascii_alphabetic()
            && local[local.len() - 1].is_ascii_alphanumeric()
            && local[1..local.len() - 1]
                .iter()
                .all(|&b| b.is_ascii_alphanumeric() || b == b'-')
    }

    /// `REGEX_VALID_TAG_NAME` as one character loop — the predicate
    /// [`is_regex_valid_tag_name`]'s byte head and character tail split between them,
    /// spelled whole so the split is graded against something that never made it.
    fn regex_valid_tag_name_reference(name: &str) -> bool {
        let mut chars = name.chars();
        if !chars.next().is_some_and(|c| c.is_ascii_alphabetic()) {
            return false;
        }
        let mut after_hyphen = false;
        for c in chars {
            if after_hyphen {
                if !crate::html::is_svelte_tag_name_tail_char(c) {
                    return false;
                }
            } else if c.is_ascii_alphanumeric() {
                // still in the leading `[a-zA-Z0-9]*` run
            } else if c == '-' {
                after_hyphen = true;
            } else {
                return false;
            }
        }
        true
    }

    /// Every Unicode scalar at each position the local-name predicate can meet it: alone,
    /// behind the ASCII head, as the byte after the head, behind the hyphen, deeper in the
    /// tail, and followed by more name.
    #[test]
    fn element_local_name_matches_the_reference_for_every_scalar() {
        let mut checked = 0usize;
        for cp in 0..=0x10_ffff_u32 {
            let Some(c) = char::from_u32(cp) else {
                continue;
            };
            for name in [
                format!("{c}"),
                format!("{c}b"),
                format!("a{c}"),
                format!("a1{c}b"),
                format!("a{c}-b"),
                format!("a-{c}"),
                format!("a-b{c}c"),
                format!("a-{c}{c}"),
                format!("a--{c}"),
            ] {
                assert_eq!(
                    is_regex_valid_tag_name(&name),
                    regex_valid_tag_name_reference(&name),
                    "U+{cp:04X} in {name:?}"
                );
                checked += 1;
            }
        }
        assert!(checked > 9_000_000, "the scalar walk ran ({checked} cases)");
    }

    /// The element-name predicates agree with their reference spellings at every arrangement
    /// of the characters each arm keys on — the namespace colon (one, two, leading, trailing),
    /// the doctype `!`, the custom-element hyphen and its non-ASCII `PCENChar` tail (a BMP
    /// letter, a middle dot, an astral character), and characters no arm admits — so the
    /// union's order and the colon search are graded at every offset, not on the few names a
    /// corpus spells.
    #[test]
    fn element_name_predicates_match_their_references() {
        const PIECES: [&str; 12] = [
            "a",
            "Z",
            "0",
            "-",
            ":",
            "!",
            ".",
            "_",
            "\u{e9}",
            "\u{b7}",
            "\u{1f600}",
            "|",
        ];
        crate::test_support::for_every_arrangement(&PIECES, 5, |name| {
            let namespaced = namespaced_name_reference(name);
            assert_eq!(is_namespaced_name(name), namespaced, "{name:?}");
            assert_eq!(
                is_regex_valid_tag_name(name),
                regex_valid_tag_name_reference(name),
                "{name:?}"
            );
            assert_eq!(
                is_valid_element_name(name),
                is_doctype_name(name) || namespaced || is_regex_valid_tag_name(name),
                "{name:?}"
            );
        });
        for (name, valid) in [
            ("svelte:head", true),
            ("a:bc", true),
            ("a:b-c", true),
            ("a1:b2", true),
            ("a:b", false),
            ("a:b-", false),
            (":ab", false),
            ("1a:bc", false),
            ("a::bc", false),
            ("a:b:c", false),
            ("a:bc:", false),
            ("!DOCTYPE", true),
            ("!", false),
            ("my-caf\u{e9}", true),
            ("x-\u{1f600}", true),
            ("x\u{e9}", false),
        ] {
            assert_eq!(is_valid_element_name(name), valid, "{name:?}");
        }
    }

    /// The reserved namespace is exactly `svelte:`, matched case-sensitively, and holds
    /// exactly the ten meta tags — so this predicate splits names `is_valid_tag_name`
    /// accepts wholesale into the ones Svelte builds a node type for and the ones it
    /// rejects.
    #[test]
    fn reserved_namespace_admits_only_the_meta_tags() {
        // Every meta tag passes, `svelte:options` included — it has no
        // `SpecialElementTag` (it fills `Root`'s `Option` slot), so it is the arm most
        // likely to be dropped by a future edit.
        for name in [
            "svelte:head",
            "svelte:options",
            "svelte:window",
            "svelte:document",
            "svelte:body",
            "svelte:element",
            "svelte:component",
            "svelte:self",
            "svelte:fragment",
            "svelte:boundary",
        ] {
            assert!(is_valid_tag_name(name), "should accept <{name}>");
            assert!(
                !is_reserved_namespace_miss(name),
                "meta tag <{name}> should not be a miss"
            );
        }
        // A name in the namespace that is not one of them — including a near-miss on a
        // real tag, and a case difference (the namespace and the local part are both
        // matched exactly).
        for name in [
            "svelte:foo",
            "svelte:headx",
            "svelte:optionsx",
            "svelte:Head",
            "svelte:HEAD",
        ] {
            assert!(
                is_reserved_namespace_miss(name),
                "<{name}> should be a miss"
            );
        }
        // Only that exact prefix is reserved: a different namespace, an uppercase
        // spelling of it, and a plain name are all untouched by the rule.
        for name in [
            "sveltex:foo",
            "foo:bar",
            "SVELTE:head",
            "Svelte:head",
            "div",
            "svelte",
        ] {
            assert!(
                !is_reserved_namespace_miss(name),
                "<{name}> is outside the reserved namespace"
            );
        }
    }

    /// The root-slot predicate must select `svelte:options` and nothing else — every other
    /// meta tag has a `SpecialElementTag`, so it has a fragment-node form.
    #[test]
    fn root_slot_meta_tag_is_only_svelte_options() {
        assert!(is_root_slot_meta_tag("svelte:options"));
        // The four `root_only_meta_tags` siblings share the placement RULE but not the
        // representability problem.
        for name in [
            "svelte:head",
            "svelte:window",
            "svelte:body",
            "svelte:document",
        ] {
            assert!(
                !is_root_slot_meta_tag(name),
                "<{name}> has a node type, so it is not a root-slot tag"
            );
        }
        // The freely-nestable meta tags, and names outside the namespace.
        for name in [
            "svelte:element",
            "svelte:component",
            "svelte:self",
            "svelte:fragment",
            "svelte:boundary",
            "svelte:foo",
            "div",
            "title",
            "Comp",
        ] {
            assert!(
                !is_root_slot_meta_tag(name),
                "<{name}> is not a root-slot tag"
            );
        }
        // ⚠️ `slot` is the trap the `svelte:` gate exists for: it is admitted by
        // `is_meta_tag_name`, yet `from_tag_name` deliberately returns `None` inside a
        // `<template shadowrootmode>` — without the gate that context would reject it.
        assert!(!is_root_slot_meta_tag("slot"));
    }

    /// The root-only set is Svelte's five, spelled exactly — each with its own bit — and
    /// every root-slot tag is in it.
    #[test]
    fn root_only_meta_tags_are_svelte_s_five() {
        let names = [
            "svelte:head",
            "svelte:options",
            "svelte:window",
            "svelte:document",
            "svelte:body",
        ];
        let mut bits = 0u8;
        for name in names {
            let tag = RootOnlyMetaTag::from_tag_name(name)
                .unwrap_or_else(|| panic!("<{name}> is root-only"));
            assert_eq!(bits & tag.bit(), 0, "<{name}> shares a bit");
            bits |= tag.bit();
        }
        for name in [
            "svelte:element",
            "svelte:component",
            "svelte:self",
            "svelte:fragment",
            "svelte:boundary",
            "svelte:headx",
            "svelte:Head",
            "head",
            "window",
            "title",
            "slot",
            "div",
        ] {
            assert_eq!(RootOnlyMetaTag::from_tag_name(name), None, "<{name}>");
            assert!(!is_root_slot_meta_tag(name), "<{name}>");
        }
        assert!(RootOnlyMetaTag::from_tag_name("svelte:options").is_some());
    }
}