@wdprlib/ast 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/element.ts ADDED
@@ -0,0 +1,1309 @@
1
+ /**
2
+ * AST element types for Wikidot markup.
3
+ *
4
+ * Wikidot markup (`+ heading`, `**bold**`, `[[module ListPages]]`, etc.) is parsed into
5
+ * a structured representation defined here. Each {@link Element} is a tagged union of
6
+ * `{ element: tag, data: payload }`, where the data shape for each tag is defined in
7
+ * {@link ElementDataMap}.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { parse } from "@wdprlib/parser";
12
+ * const tree = parse("**Hello** world");
13
+ * // tree.elements[0] → { element: "container", data: { type: "paragraph", ... } }
14
+ * ```
15
+ *
16
+ * @module
17
+ */
18
+
19
+ import type { CssLengthUnit } from "./css";
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Primitive types
23
+ // ---------------------------------------------------------------------------
24
+
25
+ /**
26
+ * Key-value map of HTML attributes.
27
+ * Populated from the Wikidot `_ class="foo" style="color:red"` attribute syntax.
28
+ *
29
+ * @group Primitives
30
+ */
31
+ export type AttributeMap = Record<string, string>;
32
+
33
+ /**
34
+ * Key-value map of include variables.
35
+ * Populated from `[[include page | key=value]]` pairs.
36
+ *
37
+ * @group Primitives
38
+ */
39
+ export type VariableMap = Record<string, string>;
40
+
41
+ /**
42
+ * Text alignment direction.
43
+ * Maps to Wikidot alignment blocks: `[[=]]` (center), `[[<]]` (left),
44
+ * `[[>]]` (right), `[[==]]` (justify).
45
+ *
46
+ * @group Primitives
47
+ */
48
+ export type Alignment = "left" | "right" | "center" | "justify";
49
+
50
+ /**
51
+ * Image float alignment. Used in `[[image]]` positioning.
52
+ *
53
+ * When `float` is true, the image uses CSS float.
54
+ * When false, it uses text-align only.
55
+ *
56
+ * @group Primitives
57
+ */
58
+ export interface FloatAlignment {
59
+ align: Alignment;
60
+ /** Whether to use CSS float (true) or just text-align (false) */
61
+ float: boolean;
62
+ }
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // Container types
66
+ // ---------------------------------------------------------------------------
67
+
68
+ /**
69
+ * Heading level (1-6). Corresponds to Wikidot `+` (h1) through `++++++` (h6).
70
+ *
71
+ * @group Container Types
72
+ */
73
+ export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
74
+
75
+ /**
76
+ * Heading configuration. Carries the level and whether this heading
77
+ * should appear in the table of contents.
78
+ *
79
+ * In Wikidot, `+*` (asterisk suffix) excludes the heading from the TOC.
80
+ *
81
+ * @group Container Types
82
+ */
83
+ export interface Heading {
84
+ level: HeadingLevel;
85
+ /** false when the heading uses `+*` syntax to opt out of the TOC */
86
+ "has-toc": boolean;
87
+ }
88
+
89
+ /**
90
+ * Discriminator for heading containers within {@link ContainerType}.
91
+ *
92
+ * @group Container Types
93
+ */
94
+ export interface HeaderType {
95
+ header: Heading;
96
+ }
97
+
98
+ /**
99
+ * Discriminator for alignment-block containers within {@link ContainerType}.
100
+ * Produced by `[[=]]`, `[[<]]`, `[[>]]`, and `[[==]]` blocks.
101
+ *
102
+ * @group Container Types
103
+ */
104
+ export interface AlignType {
105
+ align: Alignment;
106
+ }
107
+
108
+ /**
109
+ * Container types expressible as plain string literals.
110
+ * Covers inline formatting (`**bold**`, `//italics//`, etc.) and
111
+ * block-level structures (`div`, `blockquote`, `table-cell`, etc.).
112
+ *
113
+ * @group Container Types
114
+ */
115
+ export type StringContainerType =
116
+ | "bold"
117
+ | "italics"
118
+ | "underline"
119
+ | "superscript"
120
+ | "subscript"
121
+ | "strikethrough"
122
+ | "monospace"
123
+ | "span"
124
+ | "div"
125
+ | "blockquote"
126
+ | "size"
127
+ | "paragraph"
128
+ | "heading"
129
+ | "collapsible"
130
+ | "definition-list"
131
+ | "definition-list-item"
132
+ | "definition-list-key"
133
+ | "definition-list-value"
134
+ | "table-row"
135
+ | "table-cell";
136
+
137
+ /**
138
+ * Union of all container type discriminators.
139
+ * Every container element in the AST carries one of these to identify
140
+ * what kind of container it is.
141
+ *
142
+ * - String literals: inline formatting and block structures
143
+ * - {@link HeaderType}: heading elements (`+ Heading`)
144
+ * - {@link AlignType}: alignment blocks (`[[=]]...[[/=]]`)
145
+ *
146
+ * @group Container Types
147
+ */
148
+ export type ContainerType = StringContainerType | HeaderType | AlignType;
149
+
150
+ /**
151
+ * Type guard: checks whether a {@link ContainerType} is a plain string literal.
152
+ *
153
+ * @group Container Types
154
+ */
155
+ export function isStringContainerType(type: ContainerType): type is StringContainerType {
156
+ return typeof type === "string";
157
+ }
158
+
159
+ /**
160
+ * Type guard: checks whether a {@link ContainerType} is a {@link HeaderType}.
161
+ *
162
+ * @group Container Types
163
+ */
164
+ export function isHeaderType(type: ContainerType): type is HeaderType {
165
+ return typeof type === "object" && type !== null && "header" in type;
166
+ }
167
+
168
+ /**
169
+ * Type guard: checks whether a {@link ContainerType} is an {@link AlignType}.
170
+ *
171
+ * @group Container Types
172
+ */
173
+ export function isAlignType(type: ContainerType): type is AlignType {
174
+ return typeof type === "object" && type !== null && "align" in type;
175
+ }
176
+
177
+ /**
178
+ * Data payload for container elements (paragraphs, bold, headings, divs, etc.).
179
+ *
180
+ * Every nestable Wikidot construct (`**bold**`, `[[div]]...[[/div]]`,
181
+ * `+ heading`, etc.) is represented as an `{ element: "container", data: ContainerData }`.
182
+ *
183
+ * The `_`-prefixed fields are internal parser bookkeeping that gets stripped before
184
+ * the final AST is returned. They coordinate paragraph splitting and span unwrapping
185
+ * during post-processing.
186
+ *
187
+ * @group Container Types
188
+ */
189
+ export interface ContainerData {
190
+ /** Identifies the kind of container and determines how it renders */
191
+ type: ContainerType;
192
+ /** HTML attributes specified via `_ class="..." style="..."` syntax */
193
+ attributes: AttributeMap;
194
+ /** Child elements nested inside this container */
195
+ elements: Element[];
196
+ /**
197
+ * Set on `[[span_]]` elements. Signals the post-processor to merge adjacent
198
+ * paragraphs, removing the `<p>` wrapper around the span's content.
199
+ * Consumed during post-processing; never present in the final AST.
200
+ * @internal
201
+ */
202
+ _paragraphStrip?: boolean;
203
+ /**
204
+ * Set on empty `[[span_]][[/span_]]` elements. Acts as a line-break absorber:
205
+ * adjacent line-breaks are removed around this marker.
206
+ * Consumed during post-processing; never present in the final AST.
207
+ * @internal
208
+ */
209
+ _emptyParagraphStrip?: boolean;
210
+ /**
211
+ * Set on content that follows a blank line inside `[[span_]]`.
212
+ * Indicates this content should be extracted outside its paragraph wrapper.
213
+ * Consumed during post-processing; never present in the final AST.
214
+ * @internal
215
+ */
216
+ _escapedFromParagraph?: boolean;
217
+ /**
218
+ * Set on an orphaned `[[/span]]` closing tag (no matching open tag).
219
+ * The paragraph rule uses this to retroactively wrap preceding content in a span.
220
+ * Consumed during post-processing; never present in the final AST.
221
+ * @internal
222
+ */
223
+ _closeSpan?: boolean;
224
+ /**
225
+ * Set on the 2nd+ segments of a regular `[[span]]` that was split by blank lines.
226
+ * Marks where the post-processor should split the enclosing paragraph.
227
+ * Consumed during post-processing; never present in the final AST.
228
+ * @internal
229
+ */
230
+ _splitByBlankLine?: boolean;
231
+ }
232
+
233
+ // ---------------------------------------------------------------------------
234
+ // Link types
235
+ // ---------------------------------------------------------------------------
236
+
237
+ /**
238
+ * Link target window. Maps to the HTML `target` attribute.
239
+ * In Wikidot, `*` suffix on a link (`[[[page*]]]`) sets `"new-tab"`.
240
+ *
241
+ * @group Link Types
242
+ */
243
+ export type AnchorTarget = "new-tab" | "parent" | "top" | "same";
244
+
245
+ /**
246
+ * Reference to an internal wiki page.
247
+ * Produced by `[[[page]]]` or cross-site `[[[site:page]]]` syntax.
248
+ *
249
+ * @group Link Types
250
+ */
251
+ export interface PageRef {
252
+ /** Site name for cross-site links; null for same-site links */
253
+ site: string | null;
254
+ /** Page UNIX name (e.g. `"scp-001"`, `"system:page-tags"`) */
255
+ page: string;
256
+ }
257
+
258
+ /**
259
+ * Link destination: either a {@link PageRef} for internal wiki links
260
+ * or a plain URL string for external links.
261
+ *
262
+ * @group Link Types
263
+ */
264
+ export type LinkLocation = PageRef | string;
265
+
266
+ /**
267
+ * Link display label.
268
+ *
269
+ * - `{ text: string }` — explicit text (`[[[page | label]]]`)
270
+ * - `{ url: string | null }` — use the URL itself as the label
271
+ * - `"page"` — use the page name as the label (`[[[page]]]`)
272
+ *
273
+ * @group Link Types
274
+ */
275
+ export type LinkLabel = { text: string } | { url: string | null } | "page";
276
+
277
+ /**
278
+ * Link classification, determined by the syntax used.
279
+ *
280
+ * - `"direct"` — bare URL (`[http://...]`)
281
+ * - `"page"` — page link (`[[[some-page]]]`)
282
+ * - `"interwiki"` — interwiki link (`[[[wikipedia:article]]]`)
283
+ * - `"anchor"` — in-page anchor (`[[# section]]`)
284
+ * - `"table-of-contents"` — TOC-generated link
285
+ *
286
+ * @group Link Types
287
+ */
288
+ export type LinkType = "direct" | "page" | "interwiki" | "anchor" | "table-of-contents";
289
+
290
+ // ---------------------------------------------------------------------------
291
+ // Image types
292
+ // ---------------------------------------------------------------------------
293
+
294
+ /**
295
+ * Image source. Wikidot supports four resolution strategies:
296
+ *
297
+ * - `"url"` — absolute URL
298
+ * - `"file1"` — file attached to the current page (`filename`)
299
+ * - `"file2"` — file on another page (`page/filename`)
300
+ * - `"file3"` — file on another site (`site:page/filename`)
301
+ *
302
+ * @group Image Types
303
+ */
304
+ export type ImageSource =
305
+ | { type: "url"; data: string }
306
+ | { type: "file1"; data: { file: string } }
307
+ | { type: "file2"; data: { page: string; file: string } }
308
+ | { type: "file3"; data: { site: string; page: string; file: string } };
309
+
310
+ // ---------------------------------------------------------------------------
311
+ // List types
312
+ // ---------------------------------------------------------------------------
313
+
314
+ /**
315
+ * List style. `"bullet"` for `*` items, `"numbered"` for `#` items,
316
+ * `"generic"` for `[[li]]` block items.
317
+ *
318
+ * @group List Types
319
+ */
320
+ export type ListType = "bullet" | "numbered" | "generic";
321
+
322
+ /**
323
+ * A single list item. Either a leaf with inline content, or a nested sub-list.
324
+ *
325
+ * @group List Types
326
+ */
327
+ export type ListItem =
328
+ | {
329
+ "item-type": "elements";
330
+ attributes: AttributeMap;
331
+ elements: Element[];
332
+ }
333
+ | {
334
+ "item-type": "sub-list";
335
+ element: "list";
336
+ data: ListData;
337
+ };
338
+
339
+ /**
340
+ * Data payload for a list element (`* item`, `# item`, or `[[li]]`).
341
+ *
342
+ * @group List Types
343
+ */
344
+ export interface ListData {
345
+ type: ListType;
346
+ attributes: AttributeMap;
347
+ items: ListItem[];
348
+ }
349
+
350
+ /**
351
+ * A single entry in a definition list (`: key : value`).
352
+ *
353
+ * @group List Types
354
+ */
355
+ export interface DefinitionListItem {
356
+ /** Plain-text representation of the key (for quick lookups) */
357
+ key_string: string;
358
+ /** Rich-content key (may contain inline formatting) */
359
+ key: Element[];
360
+ /** Rich-content value */
361
+ value: Element[];
362
+ }
363
+
364
+ // ---------------------------------------------------------------------------
365
+ // Table types
366
+ // ---------------------------------------------------------------------------
367
+
368
+ /**
369
+ * A single table cell (`||` delimited).
370
+ *
371
+ * @group Table Types
372
+ */
373
+ export interface TableCell {
374
+ /** true if this cell is a header cell (`||~`) */
375
+ header: boolean;
376
+ /** Number of columns this cell spans (via `||` count) */
377
+ "column-span": number;
378
+ /** Explicit alignment, or null for default */
379
+ align: Alignment | null;
380
+ attributes: AttributeMap;
381
+ elements: Element[];
382
+ }
383
+
384
+ /**
385
+ * A single table row.
386
+ *
387
+ * @group Table Types
388
+ */
389
+ export interface TableRow {
390
+ attributes: AttributeMap;
391
+ cells: TableCell[];
392
+ }
393
+
394
+ /**
395
+ * Data payload for a table element.
396
+ *
397
+ * @group Table Types
398
+ */
399
+ export interface TableData {
400
+ attributes: AttributeMap;
401
+ rows: TableRow[];
402
+ }
403
+
404
+ // ---------------------------------------------------------------------------
405
+ // Block element data types
406
+ // ---------------------------------------------------------------------------
407
+
408
+ /**
409
+ * A single tab in a `[[tabview]]` block.
410
+ *
411
+ * @group Block Elements
412
+ */
413
+ export interface TabData {
414
+ /** Tab title displayed in the tab bar */
415
+ label: string;
416
+ /** Content inside the tab panel */
417
+ elements: Element[];
418
+ }
419
+
420
+ /**
421
+ * Data for a `[[code]]` block.
422
+ *
423
+ * @group Block Elements
424
+ */
425
+ export interface CodeBlockData {
426
+ /** Raw source text inside the code block */
427
+ contents: string;
428
+ /** Language identifier for syntax highlighting, or null */
429
+ language: string | null;
430
+ /** Optional name/label for the code block */
431
+ name: string | null;
432
+ }
433
+
434
+ /**
435
+ * Data for a `[[collapsible]]` block.
436
+ *
437
+ * @group Block Elements
438
+ */
439
+ export interface CollapsibleData {
440
+ elements: Element[];
441
+ attributes: AttributeMap;
442
+ /** Whether the block starts in the expanded state */
443
+ "start-open": boolean;
444
+ /** Custom text for the "show" toggle, or null for default */
445
+ "show-text": string | null;
446
+ /** Custom text for the "hide" toggle, or null for default */
447
+ "hide-text": string | null;
448
+ /** Whether to show the toggle at the top */
449
+ "show-top": boolean;
450
+ /** Whether to show the toggle at the bottom */
451
+ "show-bottom": boolean;
452
+ }
453
+
454
+ // ---------------------------------------------------------------------------
455
+ // Module types
456
+ // ---------------------------------------------------------------------------
457
+
458
+ /**
459
+ * Discriminated union of all `[[module ...]]` block types.
460
+ *
461
+ * Known modules have fully typed fields; unknown modules fall back to
462
+ * `{ module: "unknown" }` with raw arguments preserved.
463
+ *
464
+ * @group Module Types
465
+ */
466
+ export type Module =
467
+ | {
468
+ /** Unrecognized module — preserves raw arguments for pass-through */
469
+ module: "unknown";
470
+ name: string;
471
+ arguments: AttributeMap;
472
+ body?: string;
473
+ }
474
+ | {
475
+ /** `[[module Backlinks]]` — lists pages that link to a given page */
476
+ module: "backlinks";
477
+ /** Target page, or null for the current page */
478
+ page: string | null;
479
+ }
480
+ | {
481
+ /** `[[module Categories]]` — lists site categories */
482
+ module: "categories";
483
+ /** Whether to include categories marked as hidden */
484
+ "include-hidden": boolean;
485
+ }
486
+ | {
487
+ /** `[[module Join]]` — site membership join button */
488
+ module: "join";
489
+ "button-text": string | null;
490
+ attributes: AttributeMap;
491
+ }
492
+ | {
493
+ /** `[[module PageTree]]` — hierarchical page tree */
494
+ module: "page-tree";
495
+ /** Root page, or null for the site root */
496
+ root: string | null;
497
+ "show-root": boolean;
498
+ /** Max depth, or null for unlimited */
499
+ depth: number | null;
500
+ }
501
+ | {
502
+ /** `[[module Rate]]` — page rating widget */
503
+ module: "rate";
504
+ }
505
+ | {
506
+ /** `[[module TagCloud]]` — weighted cloud of page tags */
507
+ module: "tag-cloud";
508
+ /** Numeric part of the font size for the lightest-weighted tag */
509
+ "min-font-size": number;
510
+ /** Numeric part of the font size for the heaviest-weighted tag */
511
+ "max-font-size": number;
512
+ /** Unit shared by both font sizes (lowercase) */
513
+ "font-size-unit": CssLengthUnit;
514
+ /** RGB components for the lightest-weighted tag */
515
+ "min-color": [number, number, number];
516
+ /** RGB components for the heaviest-weighted tag */
517
+ "max-color": [number, number, number];
518
+ /** Normalized link target prefix, always ending with `/tag/` */
519
+ target: string;
520
+ /** Maximum number of tags to display */
521
+ limit: number;
522
+ /** Category filter, or null for all categories */
523
+ category: string | null;
524
+ }
525
+ | {
526
+ /** `[[module ListUsers]]` — user listing with template body */
527
+ module: "list-users";
528
+ /** User selector expression (e.g. `"."` for current user) */
529
+ users: string;
530
+ /** Template body with `%%variable%%` placeholders */
531
+ body?: string;
532
+ attributes: AttributeMap;
533
+ }
534
+ | {
535
+ /**
536
+ * `[[module ListPages]]` — the most complex module.
537
+ * Queries pages by various criteria and renders each through a template body.
538
+ */
539
+ module: "list-pages";
540
+ // -- Selection criteria --
541
+ category?: string;
542
+ tags?: string;
543
+ parent?: string;
544
+ "link-to"?: string;
545
+ "created-by"?: string;
546
+ "created-at"?: string;
547
+ "updated-at"?: string;
548
+ rating?: string;
549
+ votes?: string;
550
+ name?: string;
551
+ fullname?: string;
552
+ range?: string;
553
+ pagetype?: string;
554
+ // -- Pagination --
555
+ offset?: number;
556
+ limit?: number;
557
+ "per-page"?: number;
558
+ // -- Ordering --
559
+ order?: string;
560
+ // -- Display options --
561
+ reverse: boolean;
562
+ separate: boolean;
563
+ wrapper: boolean;
564
+ "prepend-line"?: string;
565
+ "append-line"?: string;
566
+ // -- RSS options --
567
+ rss?: string;
568
+ "rss-description"?: string;
569
+ "rss-home"?: string;
570
+ "rss-limit"?: number;
571
+ "rss-only": boolean;
572
+ // -- Advanced options --
573
+ /** Prefix for URL path parameters (HPC support) */
574
+ "url-attr-prefix"?: string;
575
+ /** Template body with `%%variable%%` placeholders */
576
+ body?: string;
577
+ attributes: AttributeMap;
578
+ };
579
+
580
+ // ---------------------------------------------------------------------------
581
+ // Embed types
582
+ // ---------------------------------------------------------------------------
583
+
584
+ /**
585
+ * Inline embed from `[[embed]]` syntax (not `[[embed]]...[[/embed]]` blocks).
586
+ * Supports a fixed set of providers.
587
+ *
588
+ * @group Embed Types
589
+ */
590
+ export type Embed =
591
+ | { embed: "youtube"; data: { "video-id": string } }
592
+ | { embed: "vimeo"; data: { "video-id": string } }
593
+ | { embed: "github-gist"; data: { username: string; hash: string } }
594
+ | { embed: "gitlab-snippet"; data: { "snippet-id": string } };
595
+
596
+ // ---------------------------------------------------------------------------
597
+ // Miscellaneous value types
598
+ // ---------------------------------------------------------------------------
599
+
600
+ /**
601
+ * Parsed `[[date]]` value with timezone.
602
+ *
603
+ * @group Value Types
604
+ */
605
+ export interface DateItem {
606
+ /** Unix timestamp (seconds) */
607
+ timestamp: number;
608
+ /** IANA timezone identifier */
609
+ timezone: string;
610
+ }
611
+
612
+ /**
613
+ * Direction for `[[f<]]`, `[[f>]]`, or `[[f=]]` (clear-float).
614
+ *
615
+ * @group Value Types
616
+ */
617
+ export type ClearFloat = "left" | "right" | "both";
618
+
619
+ // ---------------------------------------------------------------------------
620
+ // Named data types for Element variants
621
+ // ---------------------------------------------------------------------------
622
+
623
+ /**
624
+ * Data for `[[a]]` anchor element.
625
+ *
626
+ * @group Element Data
627
+ */
628
+ export interface AnchorData {
629
+ target: AnchorTarget | null;
630
+ attributes: AttributeMap;
631
+ elements: Element[];
632
+ }
633
+
634
+ /**
635
+ * Data for link elements (`[[[page]]]`, `[http://...]`, etc.).
636
+ *
637
+ * @group Element Data
638
+ */
639
+ export interface LinkData {
640
+ type: LinkType;
641
+ link: LinkLocation;
642
+ /** Extra path segment (e.g. anchor fragment) */
643
+ extra: string | null;
644
+ label: LinkLabel;
645
+ target: AnchorTarget | null;
646
+ }
647
+
648
+ /**
649
+ * Data for `[[image]]` elements.
650
+ *
651
+ * @group Element Data
652
+ */
653
+ export interface ImageData {
654
+ source: ImageSource;
655
+ /** If set, the image becomes a clickable link */
656
+ link: LinkLocation | null;
657
+ alignment: FloatAlignment | null;
658
+ attributes: AttributeMap;
659
+ }
660
+
661
+ /**
662
+ * Data for `[[toc]]` (table of contents) elements.
663
+ *
664
+ * @group Element Data
665
+ */
666
+ export interface TableOfContentsData {
667
+ attributes: AttributeMap;
668
+ align: Alignment | null;
669
+ }
670
+
671
+ /**
672
+ * Data for `[[footnoteblock]]` elements.
673
+ *
674
+ * @group Element Data
675
+ */
676
+ export interface FootnoteBlockData {
677
+ /** Custom title for the footnote section */
678
+ title: string | null;
679
+ /** If true, the block is hidden (footnotes rendered inline instead) */
680
+ hide?: boolean;
681
+ }
682
+
683
+ /**
684
+ * Data for `[[bibcite label]]` (bibliography citation) elements.
685
+ * Renders as a numbered reference link in the text.
686
+ *
687
+ * @group Element Data
688
+ */
689
+ export interface BibliographyCiteData {
690
+ /** Citation key that matches an entry in `[[bibliography]]` */
691
+ label: string;
692
+ /** Whether to render the citation number in brackets */
693
+ brackets: boolean;
694
+ }
695
+
696
+ /**
697
+ * Data for `[[bibliography]]` block elements.
698
+ * Collects all cited entries and renders as a reference list.
699
+ *
700
+ * @group Element Data
701
+ */
702
+ export interface BibliographyBlockData {
703
+ /** Definition list entries (`: label : description`) */
704
+ entries: DefinitionListItem[];
705
+ /** Custom section title, or null for default */
706
+ title: string | null;
707
+ /** If true, the block is hidden (for inline citation rendering) */
708
+ hide: boolean;
709
+ }
710
+
711
+ /**
712
+ * Data for `[[user name]]` elements.
713
+ *
714
+ * @group Element Data
715
+ */
716
+ export interface UserData {
717
+ name: string;
718
+ /** Whether to show the user's avatar alongside the name */
719
+ "show-avatar": boolean;
720
+ }
721
+
722
+ /**
723
+ * Data for `[[date timestamp]]` elements.
724
+ *
725
+ * @group Element Data
726
+ */
727
+ export interface DateData {
728
+ value: DateItem;
729
+ /** strftime-style format string, or null for default */
730
+ format: string | null;
731
+ /** Whether to show a tooltip with the full date on hover */
732
+ hover: boolean;
733
+ }
734
+
735
+ /**
736
+ * Data for `##color|text##` inline color syntax.
737
+ *
738
+ * @group Element Data
739
+ */
740
+ export interface ColorData {
741
+ /** CSS color value (name, hex, rgb, etc.) */
742
+ color: string;
743
+ elements: Element[];
744
+ }
745
+
746
+ /**
747
+ * Data for `[[math label]]` block math (LaTeX).
748
+ *
749
+ * @group Element Data
750
+ */
751
+ export interface MathData {
752
+ /** Optional equation label for cross-references */
753
+ name: string | null;
754
+ /** Raw LaTeX source */
755
+ "latex-source": string;
756
+ }
757
+
758
+ /**
759
+ * Data for `[[$ ... $]]` inline math (LaTeX).
760
+ *
761
+ * @group Element Data
762
+ */
763
+ export interface MathInlineData {
764
+ /** Raw LaTeX source */
765
+ "latex-source": string;
766
+ }
767
+
768
+ /**
769
+ * Data for `[[html]]` block elements.
770
+ * Contains raw HTML that is sanitized at render time.
771
+ *
772
+ * @group Element Data
773
+ */
774
+ export interface HtmlData {
775
+ /** Raw HTML content */
776
+ contents: string;
777
+ /** Optional `<style>` content extracted from the HTML */
778
+ style?: string;
779
+ }
780
+
781
+ /**
782
+ * Data for `[[embed]]...[[/embed]]` block elements.
783
+ * Contains raw HTML that is validated against an allowlist at render time.
784
+ * Unlike the `html` element, `embed-block` is paragraph-safe.
785
+ *
786
+ * @group Element Data
787
+ */
788
+ export interface EmbedBlockData {
789
+ /** Raw HTML content */
790
+ contents: string;
791
+ }
792
+
793
+ /**
794
+ * Data for `[[iframe url]]` elements.
795
+ *
796
+ * @group Element Data
797
+ */
798
+ export interface IframeData {
799
+ url: string;
800
+ attributes: AttributeMap;
801
+ }
802
+
803
+ /**
804
+ * Data for `[[include page]]` elements.
805
+ * After resolution via `resolveIncludes()`, `elements` is populated
806
+ * with the included page's parsed content.
807
+ *
808
+ * @group Element Data
809
+ */
810
+ export interface IncludeData {
811
+ /** Whether this include appeared in an inline (paragraph-safe) context */
812
+ "paragraph-safe": boolean;
813
+ /** Variables passed to the included page (`key=value` pairs) */
814
+ variables: VariableMap;
815
+ /** Target page reference */
816
+ location: PageRef;
817
+ /** Parsed content of the included page (empty before resolution) */
818
+ elements: Element[];
819
+ }
820
+
821
+ /**
822
+ * Data for `[[iftags]]` conditional blocks.
823
+ * Content is shown/hidden based on the current page's tags.
824
+ *
825
+ * @group Element Data
826
+ */
827
+ export interface IfTagsData {
828
+ /** Tag condition expression (e.g. `"+scp -joke"`) */
829
+ condition: string;
830
+ elements: Element[];
831
+ }
832
+
833
+ /**
834
+ * Data for `[[#expr expression]]` inline expressions.
835
+ * The expression is stored as a string and evaluated at render time.
836
+ *
837
+ * @group Element Data
838
+ */
839
+ export interface ExprData {
840
+ expression: string;
841
+ }
842
+
843
+ /**
844
+ * Data for `[[#if value | then | else]]` conditionals.
845
+ * Simple truthy check — false values: `"false"`, `"null"`, `""`, `"0"`.
846
+ *
847
+ * @group Element Data
848
+ */
849
+ export interface IfCondData {
850
+ condition: string;
851
+ then: Element[];
852
+ else: Element[];
853
+ }
854
+
855
+ /**
856
+ * Data for `[[#ifexpr expression | then | else]]` conditionals.
857
+ * Evaluates the expression numerically and branches on the result.
858
+ *
859
+ * @group Element Data
860
+ */
861
+ export interface IfExprData {
862
+ expression: string;
863
+ then: Element[];
864
+ else: Element[];
865
+ }
866
+
867
+ // ---------------------------------------------------------------------------
868
+ // Element core types
869
+ // ---------------------------------------------------------------------------
870
+
871
+ /**
872
+ * Maps each element tag name to its data type.
873
+ *
874
+ * `void` means the element carries no data property (e.g. `line-break`).
875
+ *
876
+ * Declared as `type` (not `interface`) to prevent accidental declaration merging.
877
+ *
878
+ * @group Core
879
+ */
880
+ export type ElementDataMap = {
881
+ container: ContainerData;
882
+ module: Module;
883
+ text: string;
884
+ raw: string;
885
+ variable: string;
886
+ email: string;
887
+ table: TableData;
888
+ "tab-view": TabData[];
889
+ anchor: AnchorData;
890
+ "anchor-name": string;
891
+ link: LinkData;
892
+ image: ImageData;
893
+ list: ListData;
894
+ "definition-list": DefinitionListItem[];
895
+ collapsible: CollapsibleData;
896
+ "table-of-contents": TableOfContentsData;
897
+ footnote: void;
898
+ "footnote-ref": number;
899
+ "footnote-block": FootnoteBlockData;
900
+ "bibliography-cite": BibliographyCiteData;
901
+ "bibliography-block": BibliographyBlockData;
902
+ user: UserData;
903
+ date: DateData;
904
+ color: ColorData;
905
+ code: CodeBlockData;
906
+ math: MathData;
907
+ "math-inline": MathInlineData;
908
+ "equation-reference": string;
909
+ embed: Embed;
910
+ "embed-block": EmbedBlockData;
911
+ html: HtmlData;
912
+ iframe: IframeData;
913
+ include: IncludeData;
914
+ style: string;
915
+ "line-break": void;
916
+ "line-breaks": number;
917
+ "clear-float": ClearFloat;
918
+ "horizontal-rule": void;
919
+ "content-separator": void;
920
+ "if-tags": IfTagsData;
921
+ expr: ExprData;
922
+ if: IfCondData;
923
+ ifexpr: IfExprData;
924
+ };
925
+
926
+ /**
927
+ * Union of all valid element tag names.
928
+ *
929
+ * @group Core
930
+ */
931
+ export type ElementName = keyof ElementDataMap;
932
+
933
+ /**
934
+ * Resolves the data type for a given element tag name.
935
+ *
936
+ * @group Core
937
+ */
938
+ export type ElementData<K extends ElementName> = ElementDataMap[K];
939
+
940
+ /**
941
+ * Resolves the full element shape for a given tag name.
942
+ * Elements with `void` data omit the `data` property entirely.
943
+ *
944
+ * @group Core
945
+ */
946
+ export type ElementOf<K extends ElementName> = ElementDataMap[K] extends void
947
+ ? { element: K }
948
+ : { element: K; data: ElementDataMap[K] };
949
+
950
+ /**
951
+ * A single AST node. Tagged union over all element types.
952
+ *
953
+ * Use `element.element` to discriminate, then access `element.data`
954
+ * with the appropriate type.
955
+ *
956
+ * @example
957
+ * ```ts
958
+ * if (el.element === "text") {
959
+ * console.log(el.data); // string
960
+ * } else if (el.element === "container") {
961
+ * console.log(el.data.type); // ContainerType
962
+ * }
963
+ * ```
964
+ *
965
+ * @group Core
966
+ */
967
+ export type Element = {
968
+ [K in ElementName]: ElementOf<K>;
969
+ }[ElementName];
970
+
971
+ /**
972
+ * Table-of-contents entry collected during parsing.
973
+ * Used internally to build the TOC sidebar.
974
+ *
975
+ * @group Core
976
+ */
977
+ export interface TocEntry {
978
+ /** Heading nesting level (1-6) */
979
+ level: number;
980
+ /** Plain-text heading content */
981
+ text: string;
982
+ }
983
+
984
+ /**
985
+ * Root of the parsed AST.
986
+ *
987
+ * Besides the main `elements` array, the tree may carry extracted
988
+ * side-channel data (TOC, styles, code blocks, footnotes) that is
989
+ * collected during parsing and used at render time.
990
+ *
991
+ * @group Core
992
+ */
993
+ export interface SyntaxTree {
994
+ /** Top-level elements of the document */
995
+ elements: Element[];
996
+ /** Generated table-of-contents entries (if any headings have `has-toc: true`) */
997
+ "table-of-contents"?: Element[];
998
+ /** CSS from `[[module CSS]]` blocks */
999
+ styles?: string[];
1000
+ /** Raw HTML from `[[html]]` blocks (rendered in sandboxed iframes) */
1001
+ "html-blocks"?: string[];
1002
+ /** Code blocks extracted for deferred syntax highlighting */
1003
+ "code-blocks"?: CodeBlockData[];
1004
+ /** Footnote content arrays, indexed by footnote number */
1005
+ footnotes?: Element[][];
1006
+ }
1007
+
1008
+ // ---------------------------------------------------------------------------
1009
+ // Factory functions
1010
+ // ---------------------------------------------------------------------------
1011
+
1012
+ /**
1013
+ * Create a text element.
1014
+ *
1015
+ * @group Factories
1016
+ */
1017
+ export function text(value: string): Element {
1018
+ return { element: "text", data: value };
1019
+ }
1020
+
1021
+ /**
1022
+ * Create a container element with the given type and children.
1023
+ *
1024
+ * @group Factories
1025
+ */
1026
+ export function container(
1027
+ type: ContainerType,
1028
+ elements: Element[],
1029
+ attributes: AttributeMap = {},
1030
+ ): Element {
1031
+ return {
1032
+ element: "container",
1033
+ data: { type, attributes, elements },
1034
+ };
1035
+ }
1036
+
1037
+ /**
1038
+ * Create a paragraph container.
1039
+ *
1040
+ * @group Factories
1041
+ */
1042
+ export function paragraph(elements: Element[], attributes: AttributeMap = {}): Element {
1043
+ return container("paragraph", elements, attributes);
1044
+ }
1045
+
1046
+ /**
1047
+ * Create a bold (`**...**`) container.
1048
+ *
1049
+ * @group Factories
1050
+ */
1051
+ export function bold(elements: Element[], attributes: AttributeMap = {}): Element {
1052
+ return container("bold", elements, attributes);
1053
+ }
1054
+
1055
+ /**
1056
+ * Create an italics (`//...//`) container.
1057
+ *
1058
+ * @group Factories
1059
+ */
1060
+ export function italics(elements: Element[], attributes: AttributeMap = {}): Element {
1061
+ return container("italics", elements, attributes);
1062
+ }
1063
+
1064
+ /**
1065
+ * Create a heading (`+ ...` through `++++++ ...`) container.
1066
+ *
1067
+ * @param level - Heading depth (1-6)
1068
+ * @param elements - Heading content
1069
+ * @param hasToc - Whether to include in the table of contents (default: true)
1070
+ * @param attributes - Optional HTML attributes
1071
+ *
1072
+ * @group Factories
1073
+ */
1074
+ export function heading(
1075
+ level: HeadingLevel,
1076
+ elements: Element[],
1077
+ hasToc = true,
1078
+ attributes: AttributeMap = {},
1079
+ ): Element {
1080
+ return container({ header: { level, "has-toc": hasToc } }, elements, attributes);
1081
+ }
1082
+
1083
+ /**
1084
+ * Create a line-break element.
1085
+ *
1086
+ * @group Factories
1087
+ */
1088
+ export function lineBreak(): Element {
1089
+ return { element: "line-break" };
1090
+ }
1091
+
1092
+ /**
1093
+ * Create a horizontal rule (`----`) element.
1094
+ *
1095
+ * @group Factories
1096
+ */
1097
+ export function horizontalRule(): Element {
1098
+ return { element: "horizontal-rule" };
1099
+ }
1100
+
1101
+ /**
1102
+ * Create a link element.
1103
+ *
1104
+ * @param linkLocation - Destination (URL string or {@link PageRef})
1105
+ * @param label - Display label
1106
+ * @param options - Optional type, extra path, and target overrides
1107
+ *
1108
+ * @group Factories
1109
+ */
1110
+ export function link(
1111
+ linkLocation: LinkLocation,
1112
+ label: LinkLabel,
1113
+ options: {
1114
+ type?: LinkType;
1115
+ extra?: string | null;
1116
+ target?: AnchorTarget | null;
1117
+ } = {},
1118
+ ): Element {
1119
+ return {
1120
+ element: "link",
1121
+ data: {
1122
+ type: options.type ?? (typeof linkLocation === "string" ? "direct" : "page"),
1123
+ link: linkLocation,
1124
+ extra: options.extra ?? null,
1125
+ label,
1126
+ target: options.target ?? null,
1127
+ },
1128
+ };
1129
+ }
1130
+
1131
+ /**
1132
+ * Create a list element.
1133
+ *
1134
+ * @group Factories
1135
+ */
1136
+ export function list(type: ListType, items: ListItem[], attributes: AttributeMap = {}): Element {
1137
+ return {
1138
+ element: "list",
1139
+ data: { type, attributes, items },
1140
+ };
1141
+ }
1142
+
1143
+ /**
1144
+ * Create a list item containing inline elements.
1145
+ *
1146
+ * @group Factories
1147
+ */
1148
+ export function listItemElements(elements: Element[], attributes: AttributeMap = {}): ListItem {
1149
+ return {
1150
+ "item-type": "elements",
1151
+ attributes,
1152
+ elements,
1153
+ };
1154
+ }
1155
+
1156
+ /**
1157
+ * Create a list item containing a nested sub-list.
1158
+ *
1159
+ * @group Factories
1160
+ */
1161
+ export function listItemSubList(data: ListData): ListItem {
1162
+ return {
1163
+ "item-type": "sub-list",
1164
+ element: "list",
1165
+ data,
1166
+ };
1167
+ }
1168
+
1169
+ // ---------------------------------------------------------------------------
1170
+ // Paragraph safety checks
1171
+ // ---------------------------------------------------------------------------
1172
+
1173
+ /**
1174
+ * Check whether a container type can appear inside a `<p>` element.
1175
+ *
1176
+ * Inline formatting (bold, italics, span, etc.) is paragraph-safe.
1177
+ * Block-level structures (div, blockquote, heading, etc.) are not.
1178
+ *
1179
+ * @group Utilities
1180
+ */
1181
+ export function isContainerTypeParagraphSafe(type: ContainerType): boolean {
1182
+ if (isHeaderType(type)) return false;
1183
+ if (isAlignType(type)) return false;
1184
+ // String container types
1185
+ switch (type) {
1186
+ case "bold":
1187
+ case "italics":
1188
+ case "underline":
1189
+ case "superscript":
1190
+ case "subscript":
1191
+ case "strikethrough":
1192
+ case "monospace":
1193
+ case "span":
1194
+ case "size":
1195
+ return true;
1196
+ case "div":
1197
+ case "blockquote":
1198
+ case "paragraph":
1199
+ case "heading":
1200
+ case "collapsible":
1201
+ case "definition-list":
1202
+ case "definition-list-item":
1203
+ case "definition-list-key":
1204
+ case "definition-list-value":
1205
+ case "table-row":
1206
+ case "table-cell":
1207
+ return false;
1208
+ default:
1209
+ // Unknown types are treated as not paragraph-safe for safety
1210
+ return false;
1211
+ }
1212
+ }
1213
+
1214
+ /**
1215
+ * Check whether an element can appear inside a `<p>` element.
1216
+ *
1217
+ * Performs a surface-level check on the element tag (and container type
1218
+ * for containers). Does not recurse into child elements.
1219
+ *
1220
+ * Used by the parser to decide whether to wrap adjacent inline elements
1221
+ * in a paragraph or leave them as block-level siblings.
1222
+ *
1223
+ * @group Utilities
1224
+ */
1225
+ export function isParagraphSafe(element: Element): boolean {
1226
+ switch (element.element) {
1227
+ case "container": {
1228
+ const data = element.data as ContainerData;
1229
+ return isContainerTypeParagraphSafe(data.type);
1230
+ }
1231
+ case "module":
1232
+ return false;
1233
+ case "text":
1234
+ case "raw":
1235
+ case "variable":
1236
+ case "email":
1237
+ return true;
1238
+ case "table":
1239
+ return false;
1240
+ case "tab-view":
1241
+ return false;
1242
+ case "anchor":
1243
+ case "anchor-name":
1244
+ case "link":
1245
+ return true;
1246
+ case "image":
1247
+ return true;
1248
+ case "list":
1249
+ return false;
1250
+ case "definition-list":
1251
+ return false;
1252
+ case "collapsible":
1253
+ return false;
1254
+ case "table-of-contents":
1255
+ return false;
1256
+ case "footnote":
1257
+ return true;
1258
+ case "footnote-ref":
1259
+ return true;
1260
+ case "footnote-block":
1261
+ return false;
1262
+ case "bibliography-cite":
1263
+ return true;
1264
+ case "bibliography-block":
1265
+ return false;
1266
+ case "user":
1267
+ return true;
1268
+ case "date":
1269
+ return true;
1270
+ case "color":
1271
+ return true;
1272
+ case "code":
1273
+ return false;
1274
+ case "math":
1275
+ return false;
1276
+ case "math-inline":
1277
+ return true;
1278
+ case "embed":
1279
+ return false;
1280
+ case "embed-block":
1281
+ return true;
1282
+ case "html":
1283
+ case "iframe":
1284
+ return false;
1285
+ case "include": {
1286
+ const data = element.data as IncludeData;
1287
+ return data["paragraph-safe"];
1288
+ }
1289
+ case "style":
1290
+ return false;
1291
+ case "line-break":
1292
+ case "line-breaks":
1293
+ return true;
1294
+ case "clear-float":
1295
+ return false;
1296
+ case "horizontal-rule":
1297
+ return false;
1298
+ case "content-separator":
1299
+ return false;
1300
+ case "if-tags":
1301
+ return false;
1302
+ case "expr":
1303
+ case "if":
1304
+ case "ifexpr":
1305
+ return true;
1306
+ default:
1307
+ return false;
1308
+ }
1309
+ }