@wdprlib/parser 3.1.1 → 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 +312 -121
- package/dist/index.js +289 -98
- 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,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Parses Wikidot's inline expression and conditional block syntax:
|
|
4
|
+
*
|
|
5
|
+
* - `[[#expr expression]]` -- evaluate a mathematical expression and display the result
|
|
6
|
+
* - `[[#if value | then | else]]` -- simple truthy/falsy conditional
|
|
7
|
+
* - `[[#ifexpr expression | then | else]]` -- expression-based conditional
|
|
8
|
+
*
|
|
9
|
+
* All three forms begin with `[[#` followed by the keyword. The `#`
|
|
10
|
+
* prefix distinguishes these from regular `[[block]]` syntax.
|
|
11
|
+
*
|
|
12
|
+
* `[[#if]]` treats its condition as a string and considers these values
|
|
13
|
+
* falsy: `"false"`, `"null"`, `""`, `"0"`. Everything else is truthy.
|
|
14
|
+
*
|
|
15
|
+
* `[[#ifexpr]]` evaluates its condition as a mathematical expression
|
|
16
|
+
* and treats the numeric result as falsy when zero.
|
|
17
|
+
*
|
|
18
|
+
* Both conditional forms support an optional else branch: when only
|
|
19
|
+
* one pipe-separated branch is provided, the else branch is empty.
|
|
20
|
+
*
|
|
21
|
+
* Expressions are limited to 256 characters (after trimming) to
|
|
22
|
+
* prevent abuse.
|
|
23
|
+
*
|
|
24
|
+
* The pipe (`|`) delimiter is depth-aware: pipes inside nested `[[]]`
|
|
25
|
+
* or `[[[]]]` blocks are not treated as branch separators.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
import type { Element } from "@wdprlib/ast";
|
|
30
|
+
import type { InlineRule, ParseContext, RuleResult } from "../types";
|
|
31
|
+
import { currentToken } from "../types";
|
|
32
|
+
|
|
33
|
+
/** Maximum allowed length for expression strings after trimming. */
|
|
34
|
+
const MAX_EXPRESSION_LENGTH = 256;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parses inline content for the then/else branches of `[[#if]]` and `[[#ifexpr]]`.
|
|
38
|
+
*
|
|
39
|
+
* Collects inline elements (by delegating to the registered inline rules) until
|
|
40
|
+
* a top-level `PIPE` token or the enclosing `BLOCK_CLOSE` (`]]`) is reached.
|
|
41
|
+
*
|
|
42
|
+
* Nesting depth is tracked so that `PIPE` and `BLOCK_CLOSE` tokens inside
|
|
43
|
+
* nested `[[...]]` or `[[[...]]]` blocks are not mistaken for branch
|
|
44
|
+
* delimiters or the outer block's closing marker.
|
|
45
|
+
*
|
|
46
|
+
* @param ctx - The current parse context
|
|
47
|
+
* @param startPos - Token index at which to begin scanning branch content
|
|
48
|
+
* @returns An object containing the parsed elements, the number of tokens consumed,
|
|
49
|
+
* and whether the branch ended with a `PIPE` (indicating an else branch follows)
|
|
50
|
+
*/
|
|
51
|
+
function parseInlineBranch(
|
|
52
|
+
ctx: ParseContext,
|
|
53
|
+
startPos: number,
|
|
54
|
+
): { elements: Element[]; consumed: number; endedWithPipe: boolean } {
|
|
55
|
+
const elements: Element[] = [];
|
|
56
|
+
let consumed = 0;
|
|
57
|
+
let pos = startPos;
|
|
58
|
+
let depth = 0; // Track [[ ]] nesting depth
|
|
59
|
+
|
|
60
|
+
const { inlineRules } = ctx;
|
|
61
|
+
|
|
62
|
+
while (pos < ctx.tokens.length) {
|
|
63
|
+
const token = ctx.tokens[pos];
|
|
64
|
+
if (!token || token.type === "EOF" || token.type === "NEWLINE") {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Track nesting (both [[...]] and [[[...]]] blocks)
|
|
69
|
+
if (
|
|
70
|
+
token.type === "BLOCK_OPEN" ||
|
|
71
|
+
token.type === "BLOCK_END_OPEN" ||
|
|
72
|
+
token.type === "LINK_OPEN"
|
|
73
|
+
) {
|
|
74
|
+
depth++;
|
|
75
|
+
} else if (token.type === "BLOCK_CLOSE" || token.type === "LINK_CLOSE") {
|
|
76
|
+
if (depth === 0) {
|
|
77
|
+
// End of the outer block
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
depth--;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Stop at PIPE only at top level
|
|
84
|
+
if (token.type === "PIPE" && depth === 0) {
|
|
85
|
+
return { elements, consumed, endedWithPipe: true };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Try inline rules
|
|
89
|
+
const inlineCtx: ParseContext = { ...ctx, pos };
|
|
90
|
+
let matched = false;
|
|
91
|
+
|
|
92
|
+
for (const rule of inlineRules) {
|
|
93
|
+
// Skip rules that would consume our delimiters
|
|
94
|
+
if (rule.startTokens.includes("PIPE") || rule.startTokens.includes("BLOCK_CLOSE")) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (rule.startTokens.length === 0 || rule.startTokens.includes(token.type)) {
|
|
98
|
+
const result = rule.parse(inlineCtx);
|
|
99
|
+
if (result.success) {
|
|
100
|
+
elements.push(...result.elements);
|
|
101
|
+
consumed += result.consumed;
|
|
102
|
+
pos += result.consumed;
|
|
103
|
+
matched = true;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!matched) {
|
|
110
|
+
// Fallback to text
|
|
111
|
+
elements.push({ element: "text", data: token.value });
|
|
112
|
+
consumed++;
|
|
113
|
+
pos++;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { elements, consumed, endedWithPipe: false };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Collects raw text for the expression or condition portion of `[[#expr]]`,
|
|
122
|
+
* `[[#if]]`, and `[[#ifexpr]]` blocks.
|
|
123
|
+
*
|
|
124
|
+
* Unlike {@link parseInlineBranch}, this function collects raw token values
|
|
125
|
+
* as a concatenated string rather than parsing them as inline elements.
|
|
126
|
+
* This is appropriate for expressions and conditions that are evaluated
|
|
127
|
+
* at runtime rather than rendered as markup.
|
|
128
|
+
*
|
|
129
|
+
* Tracks nesting depth to correctly handle `PIPE` tokens that appear
|
|
130
|
+
* inside nested `[[...]]` or `[[[...]]]` blocks.
|
|
131
|
+
*
|
|
132
|
+
* @param ctx - The current parse context
|
|
133
|
+
* @param startPos - Token index at which to begin collecting text
|
|
134
|
+
* @returns An object containing the trimmed expression text, the number of tokens
|
|
135
|
+
* consumed, and whether collection ended at a `PIPE` (vs. `BLOCK_CLOSE`)
|
|
136
|
+
*/
|
|
137
|
+
function collectExpressionText(
|
|
138
|
+
ctx: ParseContext,
|
|
139
|
+
startPos: number,
|
|
140
|
+
): { text: string; consumed: number; endedWithPipe: boolean } {
|
|
141
|
+
let text = "";
|
|
142
|
+
let consumed = 0;
|
|
143
|
+
let pos = startPos;
|
|
144
|
+
let depth = 0;
|
|
145
|
+
|
|
146
|
+
while (pos < ctx.tokens.length) {
|
|
147
|
+
const token = ctx.tokens[pos];
|
|
148
|
+
if (!token || token.type === "EOF" || token.type === "NEWLINE") {
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Track nesting (both [[...]] and [[[...]]] blocks)
|
|
153
|
+
if (
|
|
154
|
+
token.type === "BLOCK_OPEN" ||
|
|
155
|
+
token.type === "BLOCK_END_OPEN" ||
|
|
156
|
+
token.type === "LINK_OPEN"
|
|
157
|
+
) {
|
|
158
|
+
depth++;
|
|
159
|
+
} else if (token.type === "BLOCK_CLOSE" || token.type === "LINK_CLOSE") {
|
|
160
|
+
if (depth === 0) {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
depth--;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Stop at PIPE only at top level
|
|
167
|
+
if (token.type === "PIPE" && depth === 0) {
|
|
168
|
+
return { text: text.trim(), consumed, endedWithPipe: true };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
text += token.value;
|
|
172
|
+
consumed++;
|
|
173
|
+
pos++;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { text: text.trim(), consumed, endedWithPipe: false };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Inline rule for parsing `[[#expr expression]]`.
|
|
181
|
+
*
|
|
182
|
+
* Evaluates the given mathematical expression at runtime and displays
|
|
183
|
+
* the result inline. The expression is case-sensitive and must use
|
|
184
|
+
* lowercase `expr`.
|
|
185
|
+
*
|
|
186
|
+
* Produces an `"expr"` AST element whose `data.expression` field
|
|
187
|
+
* contains the raw expression string for later evaluation.
|
|
188
|
+
*/
|
|
189
|
+
export const exprRule: InlineRule = {
|
|
190
|
+
name: "expr",
|
|
191
|
+
startTokens: ["BLOCK_OPEN"],
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Attempts to parse a `[[#expr expression]]` block at the current position.
|
|
195
|
+
*
|
|
196
|
+
* @param ctx - Parse context with token stream and current position
|
|
197
|
+
* @returns A successful result with an `"expr"` element, or `{ success: false }`
|
|
198
|
+
*/
|
|
199
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
200
|
+
const openToken = currentToken(ctx);
|
|
201
|
+
if (openToken.type !== "BLOCK_OPEN") {
|
|
202
|
+
return { success: false };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let pos = ctx.pos + 1;
|
|
206
|
+
let consumed = 1;
|
|
207
|
+
|
|
208
|
+
// Expect # immediately after [[
|
|
209
|
+
const hashToken = ctx.tokens[pos];
|
|
210
|
+
if (!hashToken || hashToken.type !== "HASH") {
|
|
211
|
+
return { success: false };
|
|
212
|
+
}
|
|
213
|
+
pos++;
|
|
214
|
+
consumed++;
|
|
215
|
+
|
|
216
|
+
// Expect identifier "expr" (case-sensitive - Wikidot only supports lowercase)
|
|
217
|
+
const idToken = ctx.tokens[pos];
|
|
218
|
+
if (!idToken || idToken.type !== "IDENTIFIER" || idToken.value !== "expr") {
|
|
219
|
+
return { success: false };
|
|
220
|
+
}
|
|
221
|
+
pos++;
|
|
222
|
+
consumed++;
|
|
223
|
+
|
|
224
|
+
// Skip whitespace
|
|
225
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
226
|
+
pos++;
|
|
227
|
+
consumed++;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Collect expression until ]]
|
|
231
|
+
const exprResult = collectExpressionText(ctx, pos);
|
|
232
|
+
const expression = exprResult.text;
|
|
233
|
+
pos += exprResult.consumed;
|
|
234
|
+
consumed += exprResult.consumed;
|
|
235
|
+
|
|
236
|
+
// Validate expression length
|
|
237
|
+
if (expression.length > MAX_EXPRESSION_LENGTH) {
|
|
238
|
+
return { success: false };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Expect ]]
|
|
242
|
+
if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
|
|
243
|
+
return { success: false };
|
|
244
|
+
}
|
|
245
|
+
pos++;
|
|
246
|
+
consumed++;
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
success: true,
|
|
250
|
+
elements: [
|
|
251
|
+
{
|
|
252
|
+
element: "expr",
|
|
253
|
+
data: { expression },
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
consumed,
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Inline rule for parsing `[[#if value | then | else]]`.
|
|
263
|
+
*
|
|
264
|
+
* Performs a simple truthy/falsy check on a string value. The value
|
|
265
|
+
* is treated as false when it matches `"false"`, `"null"`, `""`,
|
|
266
|
+
* or `"0"` (case-insensitive); all other values are truthy.
|
|
267
|
+
*
|
|
268
|
+
* The then branch is required (separated from the condition by `|`).
|
|
269
|
+
* The else branch is optional (separated from the then branch by
|
|
270
|
+
* another `|`).
|
|
271
|
+
*
|
|
272
|
+
* Produces an `"if"` AST element with `condition`, `then`, and
|
|
273
|
+
* `else` fields.
|
|
274
|
+
*/
|
|
275
|
+
export const ifRule: InlineRule = {
|
|
276
|
+
name: "if",
|
|
277
|
+
startTokens: ["BLOCK_OPEN"],
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Attempts to parse a `[[#if value | then | else]]` block at the current position.
|
|
281
|
+
*
|
|
282
|
+
* @param ctx - Parse context with token stream and current position
|
|
283
|
+
* @returns A successful result with an `"if"` element, or `{ success: false }`
|
|
284
|
+
*/
|
|
285
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
286
|
+
const openToken = currentToken(ctx);
|
|
287
|
+
if (openToken.type !== "BLOCK_OPEN") {
|
|
288
|
+
return { success: false };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let pos = ctx.pos + 1;
|
|
292
|
+
let consumed = 1;
|
|
293
|
+
|
|
294
|
+
// Expect # immediately after [[
|
|
295
|
+
const hashToken = ctx.tokens[pos];
|
|
296
|
+
if (!hashToken || hashToken.type !== "HASH") {
|
|
297
|
+
return { success: false };
|
|
298
|
+
}
|
|
299
|
+
pos++;
|
|
300
|
+
consumed++;
|
|
301
|
+
|
|
302
|
+
// Expect identifier "if" (case-sensitive - Wikidot only supports lowercase)
|
|
303
|
+
const idToken = ctx.tokens[pos];
|
|
304
|
+
if (!idToken || idToken.type !== "IDENTIFIER" || idToken.value !== "if") {
|
|
305
|
+
return { success: false };
|
|
306
|
+
}
|
|
307
|
+
pos++;
|
|
308
|
+
consumed++;
|
|
309
|
+
|
|
310
|
+
// Skip whitespace
|
|
311
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
312
|
+
pos++;
|
|
313
|
+
consumed++;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Collect condition until first |
|
|
317
|
+
const condResult = collectExpressionText(ctx, pos);
|
|
318
|
+
if (!condResult.endedWithPipe) {
|
|
319
|
+
return { success: false }; // Must have | separator
|
|
320
|
+
}
|
|
321
|
+
const condition = condResult.text;
|
|
322
|
+
pos += condResult.consumed;
|
|
323
|
+
consumed += condResult.consumed;
|
|
324
|
+
|
|
325
|
+
// Validate condition length
|
|
326
|
+
if (condition.length > MAX_EXPRESSION_LENGTH) {
|
|
327
|
+
return { success: false };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Skip the PIPE
|
|
331
|
+
if (ctx.tokens[pos]?.type !== "PIPE") {
|
|
332
|
+
return { success: false };
|
|
333
|
+
}
|
|
334
|
+
pos++;
|
|
335
|
+
consumed++;
|
|
336
|
+
|
|
337
|
+
// Skip whitespace after |
|
|
338
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
339
|
+
pos++;
|
|
340
|
+
consumed++;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Parse "then" branch until next |
|
|
344
|
+
const thenResult = parseInlineBranch(ctx, pos);
|
|
345
|
+
pos += thenResult.consumed;
|
|
346
|
+
consumed += thenResult.consumed;
|
|
347
|
+
|
|
348
|
+
let elseElements: Element[] = [];
|
|
349
|
+
|
|
350
|
+
if (thenResult.endedWithPipe) {
|
|
351
|
+
// Skip the PIPE
|
|
352
|
+
if (ctx.tokens[pos]?.type !== "PIPE") {
|
|
353
|
+
return { success: false };
|
|
354
|
+
}
|
|
355
|
+
pos++;
|
|
356
|
+
consumed++;
|
|
357
|
+
|
|
358
|
+
// Skip whitespace after |
|
|
359
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
360
|
+
pos++;
|
|
361
|
+
consumed++;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Parse "else" branch until ]]
|
|
365
|
+
const elseResult = parseInlineBranch(ctx, pos);
|
|
366
|
+
elseElements = elseResult.elements;
|
|
367
|
+
pos += elseResult.consumed;
|
|
368
|
+
consumed += elseResult.consumed;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// Expect ]]
|
|
372
|
+
if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
|
|
373
|
+
return { success: false };
|
|
374
|
+
}
|
|
375
|
+
pos++;
|
|
376
|
+
consumed++;
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
success: true,
|
|
380
|
+
elements: [
|
|
381
|
+
{
|
|
382
|
+
element: "if",
|
|
383
|
+
data: {
|
|
384
|
+
condition,
|
|
385
|
+
then: thenResult.elements,
|
|
386
|
+
else: elseElements,
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
],
|
|
390
|
+
consumed,
|
|
391
|
+
};
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Inline rule for parsing `[[#ifexpr expression | then | else]]`.
|
|
397
|
+
*
|
|
398
|
+
* Evaluates a mathematical expression and uses the result to choose
|
|
399
|
+
* between the then and else branches. A zero result selects the else
|
|
400
|
+
* branch; any non-zero result selects the then branch.
|
|
401
|
+
*
|
|
402
|
+
* Like `[[#if]]`, the then branch is required and the else branch
|
|
403
|
+
* is optional.
|
|
404
|
+
*
|
|
405
|
+
* Produces an `"ifexpr"` AST element with `expression`, `then`, and
|
|
406
|
+
* `else` fields.
|
|
407
|
+
*/
|
|
408
|
+
export const ifExprRule: InlineRule = {
|
|
409
|
+
name: "ifexpr",
|
|
410
|
+
startTokens: ["BLOCK_OPEN"],
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Attempts to parse a `[[#ifexpr expression | then | else]]` block at the current position.
|
|
414
|
+
*
|
|
415
|
+
* @param ctx - Parse context with token stream and current position
|
|
416
|
+
* @returns A successful result with an `"ifexpr"` element, or `{ success: false }`
|
|
417
|
+
*/
|
|
418
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
419
|
+
const openToken = currentToken(ctx);
|
|
420
|
+
if (openToken.type !== "BLOCK_OPEN") {
|
|
421
|
+
return { success: false };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
let pos = ctx.pos + 1;
|
|
425
|
+
let consumed = 1;
|
|
426
|
+
|
|
427
|
+
// Expect # immediately after [[
|
|
428
|
+
const hashToken = ctx.tokens[pos];
|
|
429
|
+
if (!hashToken || hashToken.type !== "HASH") {
|
|
430
|
+
return { success: false };
|
|
431
|
+
}
|
|
432
|
+
pos++;
|
|
433
|
+
consumed++;
|
|
434
|
+
|
|
435
|
+
// Expect identifier "ifexpr" (case-sensitive - Wikidot only supports lowercase)
|
|
436
|
+
const idToken = ctx.tokens[pos];
|
|
437
|
+
if (!idToken || idToken.type !== "IDENTIFIER" || idToken.value !== "ifexpr") {
|
|
438
|
+
return { success: false };
|
|
439
|
+
}
|
|
440
|
+
pos++;
|
|
441
|
+
consumed++;
|
|
442
|
+
|
|
443
|
+
// Skip whitespace
|
|
444
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
445
|
+
pos++;
|
|
446
|
+
consumed++;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Collect expression until first |
|
|
450
|
+
const exprResult = collectExpressionText(ctx, pos);
|
|
451
|
+
if (!exprResult.endedWithPipe) {
|
|
452
|
+
return { success: false }; // Must have | separator
|
|
453
|
+
}
|
|
454
|
+
const expression = exprResult.text;
|
|
455
|
+
pos += exprResult.consumed;
|
|
456
|
+
consumed += exprResult.consumed;
|
|
457
|
+
|
|
458
|
+
// Validate expression length
|
|
459
|
+
if (expression.length > MAX_EXPRESSION_LENGTH) {
|
|
460
|
+
return { success: false };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Skip the PIPE
|
|
464
|
+
if (ctx.tokens[pos]?.type !== "PIPE") {
|
|
465
|
+
return { success: false };
|
|
466
|
+
}
|
|
467
|
+
pos++;
|
|
468
|
+
consumed++;
|
|
469
|
+
|
|
470
|
+
// Skip whitespace after |
|
|
471
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
472
|
+
pos++;
|
|
473
|
+
consumed++;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Parse "then" branch until next |
|
|
477
|
+
const thenResult = parseInlineBranch(ctx, pos);
|
|
478
|
+
pos += thenResult.consumed;
|
|
479
|
+
consumed += thenResult.consumed;
|
|
480
|
+
|
|
481
|
+
let elseElements: Element[] = [];
|
|
482
|
+
|
|
483
|
+
if (thenResult.endedWithPipe) {
|
|
484
|
+
// Skip the PIPE
|
|
485
|
+
if (ctx.tokens[pos]?.type !== "PIPE") {
|
|
486
|
+
return { success: false };
|
|
487
|
+
}
|
|
488
|
+
pos++;
|
|
489
|
+
consumed++;
|
|
490
|
+
|
|
491
|
+
// Skip whitespace after |
|
|
492
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
493
|
+
pos++;
|
|
494
|
+
consumed++;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Parse "else" branch until ]]
|
|
498
|
+
const elseResult = parseInlineBranch(ctx, pos);
|
|
499
|
+
elseElements = elseResult.elements;
|
|
500
|
+
pos += elseResult.consumed;
|
|
501
|
+
consumed += elseResult.consumed;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Expect ]]
|
|
505
|
+
if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
|
|
506
|
+
return { success: false };
|
|
507
|
+
}
|
|
508
|
+
pos++;
|
|
509
|
+
consumed++;
|
|
510
|
+
|
|
511
|
+
return {
|
|
512
|
+
success: true,
|
|
513
|
+
elements: [
|
|
514
|
+
{
|
|
515
|
+
element: "ifexpr",
|
|
516
|
+
data: {
|
|
517
|
+
expression,
|
|
518
|
+
then: thenResult.elements,
|
|
519
|
+
else: elseElements,
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
],
|
|
523
|
+
consumed,
|
|
524
|
+
};
|
|
525
|
+
},
|
|
526
|
+
};
|