@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.
- package/dist/index.cjs +295 -118
- package/dist/index.js +272 -95
- package/package.json +5 -3
- package/src/index.ts +163 -0
- package/src/lexer/index.ts +20 -0
- package/src/lexer/lexer.ts +687 -0
- package/src/lexer/tokens.ts +141 -0
- package/src/parser/constants.ts +173 -0
- package/src/parser/depth.ts +251 -0
- package/src/parser/index.ts +18 -0
- package/src/parser/parse.ts +315 -0
- package/src/parser/postprocess/divAdjacentParagraph.ts +76 -0
- package/src/parser/postprocess/index.ts +15 -0
- package/src/parser/postprocess/spanStrip.ts +697 -0
- package/src/parser/preprocess/expr.ts +265 -0
- package/src/parser/preprocess/index.ts +38 -0
- package/src/parser/preprocess/typography.ts +67 -0
- package/src/parser/preprocess/utils.ts +250 -0
- package/src/parser/preprocess/whitespace.ts +111 -0
- package/src/parser/rules/block/align.ts +282 -0
- package/src/parser/rules/block/bibliography.ts +359 -0
- package/src/parser/rules/block/block-list.ts +689 -0
- package/src/parser/rules/block/blockquote.ts +238 -0
- package/src/parser/rules/block/center.ts +87 -0
- package/src/parser/rules/block/clear-float.ts +75 -0
- package/src/parser/rules/block/code.ts +187 -0
- package/src/parser/rules/block/collapsible.ts +337 -0
- package/src/parser/rules/block/comment.ts +73 -0
- package/src/parser/rules/block/content-separator.ts +79 -0
- package/src/parser/rules/block/definition-list.ts +270 -0
- package/src/parser/rules/block/div.ts +400 -0
- package/src/parser/rules/block/embed-block.ts +153 -0
- package/src/parser/rules/block/footnoteblock.ts +200 -0
- package/src/parser/rules/block/heading.ts +142 -0
- package/src/parser/rules/block/horizontal-rule.ts +61 -0
- package/src/parser/rules/block/html.ts +222 -0
- package/src/parser/rules/block/iframe.ts +239 -0
- package/src/parser/rules/block/iftags.ts +150 -0
- package/src/parser/rules/block/include.ts +179 -0
- package/src/parser/rules/block/index.ts +127 -0
- package/src/parser/rules/block/list.ts +244 -0
- package/src/parser/rules/block/math.ts +183 -0
- package/src/parser/rules/block/module/backlinks/index.ts +31 -0
- package/src/parser/rules/block/module/backlinks/types.ts +21 -0
- package/src/parser/rules/block/module/categories/index.ts +34 -0
- package/src/parser/rules/block/module/categories/types.ts +21 -0
- package/src/parser/rules/block/module/css/index.ts +37 -0
- package/src/parser/rules/block/module/iftags/condition.ts +109 -0
- package/src/parser/rules/block/module/iftags/index.ts +26 -0
- package/src/parser/rules/block/module/iftags/preprocess.ts +140 -0
- package/src/parser/rules/block/module/iftags/resolve.ts +73 -0
- package/src/parser/rules/block/module/iftags/types.ts +63 -0
- package/src/parser/rules/block/module/include/index.ts +20 -0
- package/src/parser/rules/block/module/include/resolve.ts +556 -0
- package/src/parser/rules/block/module/index.ts +122 -0
- package/src/parser/rules/block/module/join/index.ts +34 -0
- package/src/parser/rules/block/module/join/types.ts +23 -0
- package/src/parser/rules/block/module/listpages/compiler.ts +453 -0
- package/src/parser/rules/block/module/listpages/extract.ts +410 -0
- package/src/parser/rules/block/module/listpages/index.ts +83 -0
- package/src/parser/rules/block/module/listpages/normalize.ts +390 -0
- package/src/parser/rules/block/module/listpages/parser.ts +106 -0
- package/src/parser/rules/block/module/listpages/resolve.ts +130 -0
- package/src/parser/rules/block/module/listpages/types.ts +513 -0
- package/src/parser/rules/block/module/listpages/url-resolver.ts +186 -0
- package/src/parser/rules/block/module/listusers/compiler.ts +77 -0
- package/src/parser/rules/block/module/listusers/extract.ts +45 -0
- package/src/parser/rules/block/module/listusers/index.ts +36 -0
- package/src/parser/rules/block/module/listusers/parser.ts +54 -0
- package/src/parser/rules/block/module/listusers/resolve.ts +58 -0
- package/src/parser/rules/block/module/listusers/types.ts +93 -0
- package/src/parser/rules/block/module/mapping.ts +61 -0
- package/src/parser/rules/block/module/page-tree/index.ts +38 -0
- package/src/parser/rules/block/module/page-tree/types.ts +29 -0
- package/src/parser/rules/block/module/rate/index.ts +28 -0
- package/src/parser/rules/block/module/rate/types.ts +19 -0
- package/src/parser/rules/block/module/resolve.ts +411 -0
- package/src/parser/rules/block/module/types-common.ts +59 -0
- package/src/parser/rules/block/module/types.ts +61 -0
- package/src/parser/rules/block/module/utils.ts +43 -0
- package/src/parser/rules/block/module/walk.ts +380 -0
- package/src/parser/rules/block/module.ts +164 -0
- package/src/parser/rules/block/orphan-li.ts +177 -0
- package/src/parser/rules/block/paragraph.ts +157 -0
- package/src/parser/rules/block/table-block.ts +726 -0
- package/src/parser/rules/block/table.ts +441 -0
- package/src/parser/rules/block/tabview.ts +331 -0
- package/src/parser/rules/block/toc.ts +129 -0
- package/src/parser/rules/block/utils.ts +615 -0
- package/src/parser/rules/index.ts +49 -0
- package/src/parser/rules/inline/anchor-name.ts +154 -0
- package/src/parser/rules/inline/anchor.ts +327 -0
- package/src/parser/rules/inline/bibcite.ts +153 -0
- package/src/parser/rules/inline/bold.ts +86 -0
- package/src/parser/rules/inline/color.ts +140 -0
- package/src/parser/rules/inline/comment.ts +90 -0
- package/src/parser/rules/inline/equation-ref.ts +115 -0
- package/src/parser/rules/inline/expr.ts +526 -0
- package/src/parser/rules/inline/footnote.ts +223 -0
- package/src/parser/rules/inline/guillemet.ts +64 -0
- package/src/parser/rules/inline/html.ts +132 -0
- package/src/parser/rules/inline/image.ts +328 -0
- package/src/parser/rules/inline/index.ts +150 -0
- package/src/parser/rules/inline/italic.ts +74 -0
- package/src/parser/rules/inline/line-break.ts +326 -0
- package/src/parser/rules/inline/link-anchor.ts +147 -0
- package/src/parser/rules/inline/link-single.ts +164 -0
- package/src/parser/rules/inline/link-star.ts +134 -0
- package/src/parser/rules/inline/link-triple.ts +267 -0
- package/src/parser/rules/inline/math-inline.ts +126 -0
- package/src/parser/rules/inline/monospace.ts +78 -0
- package/src/parser/rules/inline/raw.ts +262 -0
- package/src/parser/rules/inline/size.ts +244 -0
- package/src/parser/rules/inline/span.ts +424 -0
- package/src/parser/rules/inline/strikethrough.ts +115 -0
- package/src/parser/rules/inline/subscript.ts +84 -0
- package/src/parser/rules/inline/superscript.ts +84 -0
- package/src/parser/rules/inline/text.ts +84 -0
- package/src/parser/rules/inline/underline.ts +127 -0
- package/src/parser/rules/inline/user.ts +147 -0
- package/src/parser/rules/inline/utils.ts +344 -0
- package/src/parser/rules/types.ts +252 -0
- package/src/parser/rules/utils.ts +155 -0
- 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
|
+
}
|