@wdprlib/parser 3.1.2 → 3.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.
Files changed (124) hide show
  1. package/dist/index.cjs +295 -118
  2. package/dist/index.js +272 -95
  3. package/package.json +5 -3
  4. package/src/index.ts +163 -0
  5. package/src/lexer/index.ts +20 -0
  6. package/src/lexer/lexer.ts +687 -0
  7. package/src/lexer/tokens.ts +141 -0
  8. package/src/parser/constants.ts +173 -0
  9. package/src/parser/depth.ts +251 -0
  10. package/src/parser/index.ts +18 -0
  11. package/src/parser/parse.ts +315 -0
  12. package/src/parser/postprocess/divAdjacentParagraph.ts +76 -0
  13. package/src/parser/postprocess/index.ts +15 -0
  14. package/src/parser/postprocess/spanStrip.ts +697 -0
  15. package/src/parser/preprocess/expr.ts +265 -0
  16. package/src/parser/preprocess/index.ts +38 -0
  17. package/src/parser/preprocess/typography.ts +67 -0
  18. package/src/parser/preprocess/utils.ts +250 -0
  19. package/src/parser/preprocess/whitespace.ts +111 -0
  20. package/src/parser/rules/block/align.ts +282 -0
  21. package/src/parser/rules/block/bibliography.ts +359 -0
  22. package/src/parser/rules/block/block-list.ts +689 -0
  23. package/src/parser/rules/block/blockquote.ts +238 -0
  24. package/src/parser/rules/block/center.ts +87 -0
  25. package/src/parser/rules/block/clear-float.ts +75 -0
  26. package/src/parser/rules/block/code.ts +187 -0
  27. package/src/parser/rules/block/collapsible.ts +337 -0
  28. package/src/parser/rules/block/comment.ts +73 -0
  29. package/src/parser/rules/block/content-separator.ts +79 -0
  30. package/src/parser/rules/block/definition-list.ts +270 -0
  31. package/src/parser/rules/block/div.ts +400 -0
  32. package/src/parser/rules/block/embed-block.ts +153 -0
  33. package/src/parser/rules/block/footnoteblock.ts +200 -0
  34. package/src/parser/rules/block/heading.ts +142 -0
  35. package/src/parser/rules/block/horizontal-rule.ts +61 -0
  36. package/src/parser/rules/block/html.ts +222 -0
  37. package/src/parser/rules/block/iframe.ts +239 -0
  38. package/src/parser/rules/block/iftags.ts +150 -0
  39. package/src/parser/rules/block/include.ts +179 -0
  40. package/src/parser/rules/block/index.ts +127 -0
  41. package/src/parser/rules/block/list.ts +244 -0
  42. package/src/parser/rules/block/math.ts +183 -0
  43. package/src/parser/rules/block/module/backlinks/index.ts +31 -0
  44. package/src/parser/rules/block/module/backlinks/types.ts +21 -0
  45. package/src/parser/rules/block/module/categories/index.ts +34 -0
  46. package/src/parser/rules/block/module/categories/types.ts +21 -0
  47. package/src/parser/rules/block/module/css/index.ts +37 -0
  48. package/src/parser/rules/block/module/iftags/condition.ts +109 -0
  49. package/src/parser/rules/block/module/iftags/index.ts +26 -0
  50. package/src/parser/rules/block/module/iftags/preprocess.ts +140 -0
  51. package/src/parser/rules/block/module/iftags/resolve.ts +73 -0
  52. package/src/parser/rules/block/module/iftags/types.ts +63 -0
  53. package/src/parser/rules/block/module/include/index.ts +20 -0
  54. package/src/parser/rules/block/module/include/resolve.ts +556 -0
  55. package/src/parser/rules/block/module/index.ts +122 -0
  56. package/src/parser/rules/block/module/join/index.ts +34 -0
  57. package/src/parser/rules/block/module/join/types.ts +23 -0
  58. package/src/parser/rules/block/module/listpages/compiler.ts +453 -0
  59. package/src/parser/rules/block/module/listpages/extract.ts +410 -0
  60. package/src/parser/rules/block/module/listpages/index.ts +83 -0
  61. package/src/parser/rules/block/module/listpages/normalize.ts +390 -0
  62. package/src/parser/rules/block/module/listpages/parser.ts +106 -0
  63. package/src/parser/rules/block/module/listpages/resolve.ts +130 -0
  64. package/src/parser/rules/block/module/listpages/types.ts +513 -0
  65. package/src/parser/rules/block/module/listpages/url-resolver.ts +186 -0
  66. package/src/parser/rules/block/module/listusers/compiler.ts +77 -0
  67. package/src/parser/rules/block/module/listusers/extract.ts +45 -0
  68. package/src/parser/rules/block/module/listusers/index.ts +36 -0
  69. package/src/parser/rules/block/module/listusers/parser.ts +54 -0
  70. package/src/parser/rules/block/module/listusers/resolve.ts +58 -0
  71. package/src/parser/rules/block/module/listusers/types.ts +93 -0
  72. package/src/parser/rules/block/module/mapping.ts +61 -0
  73. package/src/parser/rules/block/module/page-tree/index.ts +38 -0
  74. package/src/parser/rules/block/module/page-tree/types.ts +29 -0
  75. package/src/parser/rules/block/module/rate/index.ts +28 -0
  76. package/src/parser/rules/block/module/rate/types.ts +19 -0
  77. package/src/parser/rules/block/module/resolve.ts +411 -0
  78. package/src/parser/rules/block/module/types-common.ts +59 -0
  79. package/src/parser/rules/block/module/types.ts +61 -0
  80. package/src/parser/rules/block/module/utils.ts +43 -0
  81. package/src/parser/rules/block/module/walk.ts +380 -0
  82. package/src/parser/rules/block/module.ts +164 -0
  83. package/src/parser/rules/block/orphan-li.ts +177 -0
  84. package/src/parser/rules/block/paragraph.ts +157 -0
  85. package/src/parser/rules/block/table-block.ts +726 -0
  86. package/src/parser/rules/block/table.ts +441 -0
  87. package/src/parser/rules/block/tabview.ts +331 -0
  88. package/src/parser/rules/block/toc.ts +129 -0
  89. package/src/parser/rules/block/utils.ts +615 -0
  90. package/src/parser/rules/index.ts +49 -0
  91. package/src/parser/rules/inline/anchor-name.ts +154 -0
  92. package/src/parser/rules/inline/anchor.ts +327 -0
  93. package/src/parser/rules/inline/bibcite.ts +153 -0
  94. package/src/parser/rules/inline/bold.ts +86 -0
  95. package/src/parser/rules/inline/color.ts +140 -0
  96. package/src/parser/rules/inline/comment.ts +90 -0
  97. package/src/parser/rules/inline/equation-ref.ts +115 -0
  98. package/src/parser/rules/inline/expr.ts +526 -0
  99. package/src/parser/rules/inline/footnote.ts +223 -0
  100. package/src/parser/rules/inline/guillemet.ts +64 -0
  101. package/src/parser/rules/inline/html.ts +132 -0
  102. package/src/parser/rules/inline/image.ts +328 -0
  103. package/src/parser/rules/inline/index.ts +150 -0
  104. package/src/parser/rules/inline/italic.ts +74 -0
  105. package/src/parser/rules/inline/line-break.ts +326 -0
  106. package/src/parser/rules/inline/link-anchor.ts +147 -0
  107. package/src/parser/rules/inline/link-single.ts +164 -0
  108. package/src/parser/rules/inline/link-star.ts +134 -0
  109. package/src/parser/rules/inline/link-triple.ts +267 -0
  110. package/src/parser/rules/inline/math-inline.ts +126 -0
  111. package/src/parser/rules/inline/monospace.ts +78 -0
  112. package/src/parser/rules/inline/raw.ts +262 -0
  113. package/src/parser/rules/inline/size.ts +244 -0
  114. package/src/parser/rules/inline/span.ts +424 -0
  115. package/src/parser/rules/inline/strikethrough.ts +115 -0
  116. package/src/parser/rules/inline/subscript.ts +84 -0
  117. package/src/parser/rules/inline/superscript.ts +84 -0
  118. package/src/parser/rules/inline/text.ts +84 -0
  119. package/src/parser/rules/inline/underline.ts +127 -0
  120. package/src/parser/rules/inline/user.ts +147 -0
  121. package/src/parser/rules/inline/utils.ts +344 -0
  122. package/src/parser/rules/types.ts +252 -0
  123. package/src/parser/rules/utils.ts +155 -0
  124. package/src/parser/toc.ts +130 -0
@@ -0,0 +1,183 @@
1
+ /**
2
+ *
3
+ * Block rule for the Wikidot math block: `[[math name]]...[[/math]]`.
4
+ *
5
+ * A math block captures LaTeX source code between the tags and stores it
6
+ * as a `math` element in the AST. The content is not parsed for inline
7
+ * markup -- it is collected as raw text.
8
+ *
9
+ * An optional name parameter after "math" can be used to label the
10
+ * equation (e.g. `[[math euler]]`). This name can then be referenced
11
+ * elsewhere in the document.
12
+ *
13
+ * Special handling for BACKSLASH_BREAK tokens: the preprocessor converts
14
+ * `\\\n` (LaTeX line break followed by newline) into a special token.
15
+ * Inside math blocks, this must be restored to `\\\n` since it is valid
16
+ * LaTeX, not a Wikidot line continuation.
17
+ *
18
+ * Empty math blocks (no LaTeX content after trimming) are treated as
19
+ * invalid and the rule fails.
20
+ *
21
+ * @module
22
+ */
23
+ import type { Element } from "@wdprlib/ast";
24
+ import type { BlockRule, ParseContext, RuleResult } from "../types";
25
+ import { currentToken } from "../types";
26
+ import { parseBlockName } from "./utils";
27
+
28
+ /**
29
+ * Block rule for `[[math name]]...[[/math]]`.
30
+ *
31
+ * Content is captured as raw LaTeX source. BACKSLASH_BREAK tokens are
32
+ * restored to their original `\\\n` form for correct LaTeX rendering.
33
+ */
34
+ export const mathBlockRule: BlockRule = {
35
+ name: "math",
36
+ startTokens: ["BLOCK_OPEN"],
37
+ requiresLineStart: false,
38
+
39
+ parse(ctx: ParseContext): RuleResult<Element> {
40
+ const openToken = currentToken(ctx);
41
+ if (openToken.type !== "BLOCK_OPEN") {
42
+ return { success: false };
43
+ }
44
+
45
+ let pos = ctx.pos + 1;
46
+ let consumed = 1;
47
+
48
+ // Parse block name
49
+ const nameResult = parseBlockName(ctx, pos);
50
+ if (!nameResult || nameResult.name.toLowerCase() !== "math") {
51
+ return { success: false };
52
+ }
53
+ pos += nameResult.consumed;
54
+ consumed += nameResult.consumed;
55
+
56
+ // Skip whitespace
57
+ while (ctx.tokens[pos]?.type === "WHITESPACE") {
58
+ pos++;
59
+ consumed++;
60
+ }
61
+
62
+ // Parse optional name
63
+ let name: string | null = null;
64
+ const nameToken = ctx.tokens[pos];
65
+ if (nameToken?.type === "IDENTIFIER" || nameToken?.type === "TEXT") {
66
+ let nameParts = "";
67
+ while (pos < ctx.tokens.length) {
68
+ const token = ctx.tokens[pos];
69
+ if (
70
+ !token ||
71
+ token.type === "BLOCK_CLOSE" ||
72
+ token.type === "WHITESPACE" ||
73
+ token.type === "NEWLINE"
74
+ ) {
75
+ break;
76
+ }
77
+ nameParts += token.value;
78
+ pos++;
79
+ consumed++;
80
+ }
81
+ if (nameParts) {
82
+ name = nameParts;
83
+ }
84
+ }
85
+
86
+ // Skip whitespace
87
+ while (ctx.tokens[pos]?.type === "WHITESPACE") {
88
+ pos++;
89
+ consumed++;
90
+ }
91
+
92
+ // Expect ]]
93
+ if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
94
+ return { success: false };
95
+ }
96
+ pos++;
97
+ consumed++;
98
+
99
+ // Skip newline after opening tag
100
+ if (ctx.tokens[pos]?.type === "NEWLINE") {
101
+ pos++;
102
+ consumed++;
103
+ }
104
+
105
+ // Collect LaTeX content until [[/math]]
106
+ // BACKSLASH_BREAK (U+E000) was created by preprocessing from "\\\n"
107
+ // In math blocks, we need to restore this as "\\\n" for LaTeX line breaks
108
+ let latexSource = "";
109
+
110
+ while (pos < ctx.tokens.length) {
111
+ const token = ctx.tokens[pos];
112
+ if (!token) break;
113
+
114
+ // Check for closing [[/math]]
115
+ if (token.type === "BLOCK_END_OPEN") {
116
+ const closeNameResult = parseBlockName(ctx, pos + 1);
117
+ if (closeNameResult?.name.toLowerCase() === "math") {
118
+ break;
119
+ }
120
+ }
121
+
122
+ // Restore BACKSLASH_BREAK to original "\\\n" for LaTeX
123
+ if (token.type === "BACKSLASH_BREAK") {
124
+ latexSource += "\\\n";
125
+ } else {
126
+ latexSource += token.value;
127
+ }
128
+ pos++;
129
+ consumed++;
130
+ }
131
+
132
+ // Diagnostic for missing close tag
133
+ if (ctx.tokens[pos]?.type !== "BLOCK_END_OPEN") {
134
+ ctx.diagnostics.push({
135
+ severity: "warning",
136
+ code: "unclosed-block",
137
+ message: "Missing closing tag [[/math]] for [[math]]",
138
+ position: openToken.position,
139
+ });
140
+ }
141
+
142
+ // Consume [[/math]]
143
+ if (ctx.tokens[pos]?.type === "BLOCK_END_OPEN") {
144
+ pos++;
145
+ consumed++;
146
+ const closeNameResult = parseBlockName(ctx, pos);
147
+ if (closeNameResult) {
148
+ pos += closeNameResult.consumed;
149
+ consumed += closeNameResult.consumed;
150
+ }
151
+ if (ctx.tokens[pos]?.type === "BLOCK_CLOSE") {
152
+ pos++;
153
+ consumed++;
154
+ }
155
+ if (ctx.tokens[pos]?.type === "NEWLINE") {
156
+ pos++;
157
+ consumed++;
158
+ }
159
+ }
160
+
161
+ // Trim the LaTeX source
162
+ latexSource = latexSource.trim();
163
+
164
+ // Empty math is invalid
165
+ if (!latexSource) {
166
+ return { success: false };
167
+ }
168
+
169
+ return {
170
+ success: true,
171
+ elements: [
172
+ {
173
+ element: "math",
174
+ data: {
175
+ name,
176
+ "latex-source": latexSource,
177
+ },
178
+ },
179
+ ],
180
+ consumed,
181
+ };
182
+ },
183
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ *
3
+ * Parser rule for the Wikidot `[[module Backlinks]]` block.
4
+ *
5
+ * Displays pages that link to the current page (or a specified page).
6
+ * Accepts an optional `page` attribute to target a specific page.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import type { ModuleRule } from "../types";
12
+ import type { BacklinksModuleData } from "./types";
13
+
14
+ /**
15
+ * Module rule for `[[module Backlinks]]`.
16
+ *
17
+ * Parses the optional `page` attribute. When not specified, the rendering
18
+ * application should show backlinks for the current page.
19
+ */
20
+ export const backlinksModuleRule: ModuleRule = {
21
+ name: "module-backlinks",
22
+ acceptsNames: ["backlinks"],
23
+ hasBody: false,
24
+
25
+ parse(_ctx, _pos, args): BacklinksModuleData {
26
+ return {
27
+ module: "backlinks",
28
+ page: args.page ?? null,
29
+ };
30
+ },
31
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ *
3
+ * Type definitions for the Backlinks module.
4
+ *
5
+ * The `[[module Backlinks]]` block displays a list of pages that link to
6
+ * the specified page (or the current page if no `page` attribute is given).
7
+ *
8
+ * @module
9
+ */
10
+
11
+ /**
12
+ * AST data for a `[[module Backlinks]]` element.
13
+ *
14
+ * The rendering application should query for pages that contain links to
15
+ * the target page and display them as a list.
16
+ */
17
+ export interface BacklinksModuleData {
18
+ module: "backlinks";
19
+ /** Target page for which to find backlinks, or null for the current page */
20
+ page: string | null;
21
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ *
3
+ * Parser rule for the Wikidot `[[module Categories]]` block.
4
+ *
5
+ * Displays a list of page categories on the site. Accepts an optional
6
+ * `include-hidden` boolean attribute to control whether hidden categories
7
+ * (those prefixed with `_`) are shown.
8
+ *
9
+ * @module
10
+ */
11
+
12
+ import type { ModuleRule } from "../types";
13
+ import { parseBool } from "../utils";
14
+ import type { CategoriesModuleData } from "./types";
15
+
16
+ /**
17
+ * Module rule for `[[module Categories]]`.
18
+ *
19
+ * Parses the `include-hidden` attribute (accepts both `include-hidden` and
20
+ * `includehidden` forms, since Wikidot lowercases all attribute names).
21
+ * Defaults to false.
22
+ */
23
+ export const categoriesModuleRule: ModuleRule = {
24
+ name: "module-categories",
25
+ acceptsNames: ["categories"],
26
+ hasBody: false,
27
+
28
+ parse(_ctx, _pos, args): CategoriesModuleData {
29
+ return {
30
+ module: "categories",
31
+ "include-hidden": parseBool(args["include-hidden"] ?? args.includehidden, false),
32
+ };
33
+ },
34
+ };
@@ -0,0 +1,21 @@
1
+ /**
2
+ *
3
+ * Type definitions for the Categories module.
4
+ *
5
+ * The `[[module Categories]]` block displays a list of page categories
6
+ * on the current site.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ /**
12
+ * AST data for a `[[module Categories]]` element.
13
+ *
14
+ * The rendering application should query the site's category list and
15
+ * display them, optionally including hidden categories.
16
+ */
17
+ export interface CategoriesModuleData {
18
+ module: "categories";
19
+ /** Whether to include hidden categories (prefixed with `_`) in the listing */
20
+ "include-hidden": boolean;
21
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ *
3
+ * Parser rule for the Wikidot `[[module CSS]]` block.
4
+ *
5
+ * Allows embedding custom CSS styles within a Wikidot page. The module body
6
+ * contains raw CSS text that is emitted as a `style` element in the AST.
7
+ *
8
+ * @example
9
+ * ```
10
+ * [[module CSS]]
11
+ * .custom-class { color: red; }
12
+ * [[/module]]
13
+ * ```
14
+ *
15
+ * @module
16
+ */
17
+
18
+ import type { Element } from "@wdprlib/ast";
19
+ import type { ModuleRule } from "../types";
20
+
21
+ /**
22
+ * Module rule for `[[module CSS]]`.
23
+ *
24
+ * Unlike most modules that produce a `Module` AST node, the CSS module
25
+ * produces a direct `style` Element. This is because CSS content is not
26
+ * a Wikidot module that needs external data resolution -- it can be
27
+ * rendered immediately as a `<style>` tag.
28
+ */
29
+ export const cssModuleRule: ModuleRule = {
30
+ name: "module-css",
31
+ acceptsNames: ["css"],
32
+ hasBody: true,
33
+
34
+ parse(_ctx, _pos, _args, body): Element {
35
+ return { element: "style", data: body ?? "" };
36
+ },
37
+ };
@@ -0,0 +1,109 @@
1
+ /**
2
+ *
3
+ * Parsing and evaluation of `[[iftags]]` condition strings.
4
+ *
5
+ * The condition string uses a simple syntax where each whitespace-separated
6
+ * token is a tag name with an optional prefix:
7
+ * - `+tag` - Tag must be present (AND condition)
8
+ * - `-tag` - Tag must be absent (NOT condition)
9
+ * - `tag` - At least one bare tag must be present (OR condition)
10
+ *
11
+ * All three categories must independently be satisfied:
12
+ * required (AND) + forbidden (AND) + optional (OR).
13
+ *
14
+ * @module
15
+ */
16
+
17
+ import type { TagCondition } from "./types";
18
+
19
+ /**
20
+ * Parse iftags condition string into structured format
21
+ *
22
+ * @param condition - Raw condition string like "+fruit -admin component"
23
+ * @returns Parsed condition with required, forbidden, and optional tags
24
+ */
25
+ export function parseTagCondition(condition: string): TagCondition {
26
+ const required: string[] = [];
27
+ const forbidden: string[] = [];
28
+ const optional: string[] = [];
29
+ let hasEmptyRequired = false;
30
+ let hasEmptyForbidden = false;
31
+
32
+ const parts = condition.trim().split(/\s+/);
33
+
34
+ for (const part of parts) {
35
+ if (!part) continue;
36
+
37
+ if (part.startsWith("+")) {
38
+ const tag = part.slice(1);
39
+ if (tag) required.push(tag);
40
+ else hasEmptyRequired = true;
41
+ } else if (part.startsWith("-")) {
42
+ const tag = part.slice(1);
43
+ if (tag) forbidden.push(tag);
44
+ else hasEmptyForbidden = true;
45
+ } else {
46
+ optional.push(part);
47
+ }
48
+ }
49
+
50
+ return { required, forbidden, optional, hasEmptyRequired, hasEmptyForbidden };
51
+ }
52
+
53
+ /**
54
+ * Evaluate if a tag condition matches the given tags
55
+ *
56
+ * @param condition - Parsed tag condition
57
+ * @param pageTags - Actual tags on the page
58
+ * @returns true if condition is satisfied
59
+ */
60
+ export function evaluateTagCondition(condition: TagCondition, pageTags: string[]): boolean {
61
+ const noNamedTokens =
62
+ condition.required.length === 0 &&
63
+ condition.forbidden.length === 0 &&
64
+ condition.optional.length === 0;
65
+
66
+ // No tokens at all = supercommentout (`[[iftags]]`) → never match.
67
+ if (noNamedTokens && !condition.hasEmptyRequired && !condition.hasEmptyForbidden) {
68
+ return false;
69
+ }
70
+
71
+ // A bare `+` (with or without other tokens) means "require an unnamed
72
+ // tag", which can never be satisfied — the whole condition is Hide.
73
+ if (condition.hasEmptyRequired) {
74
+ return false;
75
+ }
76
+
77
+ // `[[iftags - ]]` — bare `-` token alone means "forbid nothing", which
78
+ // is trivially true, so this is Show Always. When combined with other
79
+ // tokens the bare `-` adds no constraint, so we fall through to the
80
+ // standard evaluation below.
81
+ if (condition.hasEmptyForbidden && noNamedTokens) {
82
+ return true;
83
+ }
84
+
85
+ const tagSet = new Set(pageTags);
86
+
87
+ // All required tags must be present
88
+ for (const tag of condition.required) {
89
+ if (!tagSet.has(tag)) {
90
+ return false;
91
+ }
92
+ }
93
+
94
+ // All forbidden tags must be absent
95
+ for (const tag of condition.forbidden) {
96
+ if (tagSet.has(tag)) {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ // At least one optional tag must be present (if any specified)
102
+ if (condition.optional.length > 0) {
103
+ if (!condition.optional.some((tag) => tagSet.has(tag))) {
104
+ return false;
105
+ }
106
+ }
107
+
108
+ return true;
109
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ *
3
+ * IfTags conditional rendering module for Wikidot's `[[iftags]]` block.
4
+ *
5
+ * Enables conditional content display based on the current page's tags.
6
+ * The condition syntax supports required tags (`+tag` or bare `tag`) and
7
+ * forbidden tags (`-tag`). Content inside the block is only rendered when
8
+ * all conditions are satisfied.
9
+ *
10
+ * Exports condition parsing, evaluation, and AST resolution functions.
11
+ *
12
+ * @module
13
+ */
14
+
15
+ // Types
16
+ export type { TagCondition, IfTagsResolver } from "./types";
17
+
18
+ // Condition parsing and evaluation
19
+ export { parseTagCondition, evaluateTagCondition } from "./condition";
20
+
21
+ // Resolution
22
+ export type { IfTagsData, IfTagsResolveResult } from "./resolve";
23
+ export { isIfTagsElement, resolveIfTags } from "./resolve";
24
+
25
+ // Source-level preprocessing (must run after include expansion, before parse)
26
+ export { preprocessIftags } from "./preprocess";
@@ -0,0 +1,140 @@
1
+ /**
2
+ *
3
+ * Text-level expansion of `[[iftags]]` blocks before parsing.
4
+ *
5
+ * Unlike the AST-level {@link resolveIfTags} resolver, which evaluates
6
+ * `if-tags` nodes after parsing, this pass operates directly on the
7
+ * raw wikitext source. The difference matters for templates that embed
8
+ * an `[[iftags]]` block inside another construct's attribute string,
9
+ * for example:
10
+ *
11
+ * ```wikitext
12
+ * [[div_ class="x" [[iftags +foo]]style="display:none;"[[/iftags]]]]
13
+ * ```
14
+ *
15
+ * The block-level tokenizer cannot recover a well-formed opener from
16
+ * that input — the inner `[[iftags ...]]X[[/iftags]]` has to collapse
17
+ * to either `X` or the empty string *before* the parser sees the outer
18
+ * tag, so the attribute string becomes plain text again.
19
+ *
20
+ * `pageTags` semantics:
21
+ *
22
+ * - `string[]`: full pass. Every `[[iftags]]` is evaluated against the
23
+ * given tag set and collapses to either its body or the empty string.
24
+ * The AST will contain no `if-tags` nodes after parsing.
25
+ * - `null`: tags are unknown (e.g. draft preview, fixture test).
26
+ * The pass still runs but only collapses `[[iftags]]` blocks that are
27
+ * embedded inside another block's opener (`[[name ... [[iftags ...]]X[[/iftags]] ... ]]`).
28
+ * Block-level `[[iftags]]` are left alone for {@link resolveIfTags}
29
+ * to evaluate later when real tags are supplied via `getPageTags`.
30
+ * Opener-embedded iftags are collapsed using an empty-tag assumption
31
+ * (i.e. `+tag` conditions fail, `-tag` conditions pass). This is a
32
+ * lossy fallback that keeps the outer block parseable; callers that
33
+ * need accurate rendering should pass the real tags as `string[]`.
34
+ *
35
+ * Pipeline order (when invoked via `parse()`):
36
+ *
37
+ * ```
38
+ * getPageTags → resolveIncludes → parse({ pageTags }) → resolveModules
39
+ * ```
40
+ *
41
+ * Running this after include expansion is intentional: an included
42
+ * page may itself embed `[[iftags]]`, and the condition is evaluated
43
+ * against the *including* page's tags, not the included page's.
44
+ *
45
+ * @module
46
+ */
47
+
48
+ import { parseTagCondition, evaluateTagCondition } from "./condition";
49
+ import {
50
+ computeBracketDepths,
51
+ makeUniqueSentinels,
52
+ maskRawRegions,
53
+ restorePlaceholders,
54
+ } from "../../../../preprocess/utils";
55
+
56
+ /**
57
+ * Matches one `[[iftags ...]]X[[/iftags]]` where `X` contains no further
58
+ * `[[iftags]]` opener or closer. Used for innermost-first reduction.
59
+ *
60
+ * - `g` (global): a single `replace` pass rewrites every innermost block,
61
+ * so sibling blocks collapse together and the reduction loop runs once
62
+ * per nesting level rather than once per block.
63
+ * - `i` (case-insensitive): Wikidot block names are case-insensitive
64
+ * - `s` (dot matches newline): bodies can span multiple lines
65
+ * - `[^\]]*` for the condition: tag-condition tokens (`+tag`, `-tag`,
66
+ * bare names) never contain `]`, and stopping at the first `]` keeps
67
+ * the regex linear in body length even on degenerate input.
68
+ */
69
+ const INNERMOST_IFTAGS_PATTERN =
70
+ /\[\[\s*iftags\b([^\]]*)\]\]((?:(?!\[\[\s*iftags\b|\[\[\/\s*iftags\s*\]\]).)*)\[\[\/\s*iftags\s*\]\]/gis;
71
+
72
+ /**
73
+ * Expand `[[iftags ...]]X[[/iftags]]` directives in `source` against the
74
+ * current page's tags.
75
+ *
76
+ * Behaviour:
77
+ * - Raw regions (`[[code]]`, `[[html]]`, `@@...@@`, `@<...>@`) are
78
+ * protected: literal `[[iftags]]` tokens inside them are not expanded.
79
+ * - Nested `[[iftags]]` are processed innermost-first, so an outer
80
+ * block can re-process the now-flattened inner body uniformly.
81
+ * - `pageTags === null`: only `[[iftags]]` blocks embedded inside
82
+ * another block's opener are collapsed (using an empty-tag fallback
83
+ * so `+tag` conditions fail and `-tag` conditions pass). Block-level
84
+ * iftags are left intact for the AST-level resolver.
85
+ *
86
+ * @param source Raw wikitext (typically after include expansion).
87
+ * @param pageTags Tags of the page being rendered, or `null` for the
88
+ * opener-embedded-only fallback mode.
89
+ * @returns Source with matching iftags replaced by their bodies and
90
+ * unmatched iftags removed entirely.
91
+ */
92
+ export function preprocessIftags(source: string, pageTags: string[] | null): string {
93
+ if (!source.includes("[[")) return source; // fast path
94
+
95
+ const sentinels = makeUniqueSentinels(source);
96
+ const { masked, placeholders } = maskRawRegions(source, sentinels);
97
+ const reduced = reduceIftags(masked, pageTags);
98
+ return restorePlaceholders(reduced, placeholders, sentinels);
99
+ }
100
+
101
+ /**
102
+ * Replace `[[iftags ...]]X[[/iftags]]` blocks innermost-first until no
103
+ * iftags pair remains.
104
+ *
105
+ * Behaviour by `pageTags`:
106
+ * - `string[]`: every match collapses to body / empty based on tag membership.
107
+ * - `null`: only matches whose start offset has `bracketDepth > 0`
108
+ * (i.e. embedded inside an outer block opener) are collapsed, using
109
+ * an empty-tag assumption. Block-level matches are returned verbatim
110
+ * so {@link resolveIfTags} can evaluate them later.
111
+ *
112
+ * The loop terminates when a pass changes nothing.
113
+ */
114
+ function reduceIftags(source: string, pageTags: string[] | null): string {
115
+ let current = source;
116
+ // Worst-case bound: one pass eliminates at least one nesting level, and
117
+ // nesting depth is at most `source.length`. The explicit cap stops a
118
+ // runaway regex (e.g. pathological zero-width match) from looping forever.
119
+ const maxIterations = source.length + 1;
120
+ const tagSet: string[] = pageTags ?? [];
121
+ for (let i = 0; i < maxIterations; i++) {
122
+ const depths = pageTags === null ? computeBracketDepths(current) : null;
123
+ let changed = false;
124
+ const next = current.replace(
125
+ INNERMOST_IFTAGS_PATTERN,
126
+ (match, cond: string, body: string, offset: number) => {
127
+ if (depths !== null && depths[offset] === 0) {
128
+ // block-level iftags in `null` mode → leave for AST resolver
129
+ return match;
130
+ }
131
+ changed = true;
132
+ const condition = parseTagCondition(cond);
133
+ return evaluateTagCondition(condition, tagSet) ? body : "";
134
+ },
135
+ );
136
+ if (!changed) return current;
137
+ current = next;
138
+ }
139
+ return current;
140
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ *
3
+ * Resolution of `[[iftags]]` conditional blocks.
4
+ *
5
+ * During the resolve phase, each `[[iftags]]` element is evaluated against
6
+ * the current page's tags. If the condition matches, the element's children
7
+ * are included in the output; otherwise, they are omitted. If page tags are
8
+ * not available (null), the element is kept as-is for later resolution.
9
+ *
10
+ * @module
11
+ */
12
+
13
+ import type { Element } from "@wdprlib/ast";
14
+ import { parseTagCondition, evaluateTagCondition } from "./condition";
15
+
16
+ /**
17
+ * Data structure for an `[[iftags]]` element in the AST.
18
+ */
19
+ export interface IfTagsData {
20
+ condition: string;
21
+ elements: Element[];
22
+ }
23
+
24
+ /**
25
+ * Result of attempting to resolve an `[[iftags]]` element.
26
+ *
27
+ * The `evaluated` flag indicates whether the condition was actually tested.
28
+ * When `pageTags` is null (tags not available), the condition is not evaluated
29
+ * and the element should be kept in the AST for later resolution.
30
+ */
31
+ export interface IfTagsResolveResult {
32
+ /**
33
+ * Whether the condition was evaluated
34
+ * - true: condition was evaluated (pageTags provided)
35
+ * - false: pageTags was null, element kept as-is
36
+ */
37
+ evaluated: boolean;
38
+
39
+ /**
40
+ * Whether the condition matched (only meaningful if evaluated=true)
41
+ */
42
+ matched: boolean;
43
+ }
44
+
45
+ /**
46
+ * Type guard to check if an element is an `[[iftags]]` element.
47
+ *
48
+ * @param element - The element to check
49
+ * @returns true if the element has `element: "if-tags"` and appropriate data structure
50
+ */
51
+ export function isIfTagsElement(
52
+ element: Element,
53
+ ): element is Element & { element: "if-tags"; data: IfTagsData } {
54
+ return element.element === "if-tags";
55
+ }
56
+
57
+ /**
58
+ * Evaluate an iftags condition against page tags
59
+ *
60
+ * @param data - IfTags element data with condition and child elements
61
+ * @param pageTags - Current page's tags, or null if not available
62
+ * @returns Result indicating if evaluated and if matched
63
+ */
64
+ export function resolveIfTags(data: IfTagsData, pageTags: string[] | null): IfTagsResolveResult {
65
+ if (pageTags === null) {
66
+ return { evaluated: false, matched: false };
67
+ }
68
+
69
+ const condition = parseTagCondition(data.condition);
70
+ const matched = evaluateTagCondition(condition, pageTags);
71
+
72
+ return { evaluated: true, matched };
73
+ }