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