@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,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Block rule for Wikidot tabbed content: `[[tabview]]` (or `[[tabs]]`).
|
|
4
|
+
*
|
|
5
|
+
* A tabview contains one or more `[[tab Label]]...[[/tab]]` blocks:
|
|
6
|
+
*
|
|
7
|
+
* ```
|
|
8
|
+
* [[tabview]]
|
|
9
|
+
* [[tab First Tab]]
|
|
10
|
+
* Content of the first tab.
|
|
11
|
+
* [[/tab]]
|
|
12
|
+
* [[tab Second Tab]]
|
|
13
|
+
* Content of the second tab.
|
|
14
|
+
* [[/tab]]
|
|
15
|
+
* [[/tabview]]
|
|
16
|
+
* ```
|
|
17
|
+
*
|
|
18
|
+
* Key behaviours:
|
|
19
|
+
* - Both `[[tabview]]` and `[[tabs]]` are accepted as the outer wrapper.
|
|
20
|
+
* - Any attributes or text after the block name on the opening tag are
|
|
21
|
+
* silently ignored (Wikidot behaviour: `[[tabview Foo]]` is valid).
|
|
22
|
+
* - If a tab has no label, it defaults to `"untitled"`.
|
|
23
|
+
* - Tab body content is parsed as block-level markup.
|
|
24
|
+
* - An empty tabview (no tabs) fails the rule, falling back to text.
|
|
25
|
+
* - Non-tab content between tabs (other than whitespace/newlines) causes
|
|
26
|
+
* the rule to fail.
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
import type { Element, TabData } from "@wdprlib/ast";
|
|
31
|
+
import type { BlockRule, ParseContext, RuleResult } from "../types";
|
|
32
|
+
import { currentToken } from "../types";
|
|
33
|
+
import { parseBlockName, parseBlocksUntil } from "./utils";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parses a single `[[tab Label]]...[[/tab]]` block within a tabview.
|
|
37
|
+
*
|
|
38
|
+
* The label is everything between the block name and `]]` (leading
|
|
39
|
+
* whitespace is trimmed). An empty label defaults to `"untitled"`.
|
|
40
|
+
* Newlines are not allowed in the label -- if one is encountered, the
|
|
41
|
+
* parse fails.
|
|
42
|
+
*
|
|
43
|
+
* Body content is parsed as block-level markup via {@link parseBlocksUntil}.
|
|
44
|
+
*
|
|
45
|
+
* @param ctx - Parse context, positioned before the expected `[[tab ...]]`.
|
|
46
|
+
* @returns The tab data and consumed count, or `null` on failure.
|
|
47
|
+
*/
|
|
48
|
+
function parseTab(ctx: ParseContext): { tab: TabData; consumed: number } | null {
|
|
49
|
+
let pos = ctx.pos;
|
|
50
|
+
let consumed = 0;
|
|
51
|
+
|
|
52
|
+
// Skip whitespace/newlines before tab
|
|
53
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE" || ctx.tokens[pos]?.type === "NEWLINE") {
|
|
54
|
+
pos++;
|
|
55
|
+
consumed++;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Expect [[
|
|
59
|
+
if (ctx.tokens[pos]?.type !== "BLOCK_OPEN") {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
pos++;
|
|
63
|
+
consumed++;
|
|
64
|
+
|
|
65
|
+
// Parse block name
|
|
66
|
+
const nameResult = parseBlockName(ctx, pos);
|
|
67
|
+
if (!nameResult || nameResult.name.toLowerCase() !== "tab") {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
pos += nameResult.consumed;
|
|
71
|
+
consumed += nameResult.consumed;
|
|
72
|
+
|
|
73
|
+
// Parse label (everything until ]])
|
|
74
|
+
let label = "";
|
|
75
|
+
while (pos < ctx.tokens.length) {
|
|
76
|
+
const token = ctx.tokens[pos];
|
|
77
|
+
if (!token) break;
|
|
78
|
+
if (token.type === "BLOCK_CLOSE") {
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
if (token.type === "NEWLINE") {
|
|
82
|
+
// No newline allowed in label
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
// Skip leading whitespace in label
|
|
86
|
+
if (label === "" && token.type === "WHITESPACE") {
|
|
87
|
+
pos++;
|
|
88
|
+
consumed++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
label += token.value;
|
|
92
|
+
pos++;
|
|
93
|
+
consumed++;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Expect ]]
|
|
97
|
+
if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
pos++;
|
|
101
|
+
consumed++;
|
|
102
|
+
|
|
103
|
+
// Skip newline after opening tag
|
|
104
|
+
if (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
105
|
+
pos++;
|
|
106
|
+
consumed++;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Close condition for [[/tab]]
|
|
110
|
+
const closeCondition = (checkCtx: ParseContext): boolean => {
|
|
111
|
+
const token = checkCtx.tokens[checkCtx.pos];
|
|
112
|
+
if (token?.type === "BLOCK_END_OPEN") {
|
|
113
|
+
const closeNameResult = parseBlockName(checkCtx, checkCtx.pos + 1);
|
|
114
|
+
if (closeNameResult?.name.toLowerCase() === "tab") {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Parse body
|
|
122
|
+
const bodyCtx: ParseContext = { ...ctx, pos };
|
|
123
|
+
const bodyResult = parseBlocksUntil(bodyCtx, closeCondition);
|
|
124
|
+
consumed += bodyResult.consumed;
|
|
125
|
+
pos += bodyResult.consumed;
|
|
126
|
+
|
|
127
|
+
// Check for missing close tag
|
|
128
|
+
if (ctx.tokens[pos]?.type !== "BLOCK_END_OPEN") {
|
|
129
|
+
ctx.diagnostics.push({
|
|
130
|
+
severity: "warning",
|
|
131
|
+
code: "unclosed-block",
|
|
132
|
+
message: "Missing closing tag [[/tab]] for [[tab]]",
|
|
133
|
+
position: ctx.tokens[ctx.pos]?.position ?? {
|
|
134
|
+
start: { line: 0, column: 0, offset: 0 },
|
|
135
|
+
end: { line: 0, column: 0, offset: 0 },
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Consume [[/tab]]
|
|
141
|
+
if (ctx.tokens[pos]?.type === "BLOCK_END_OPEN") {
|
|
142
|
+
pos++;
|
|
143
|
+
consumed++;
|
|
144
|
+
const closeNameResult = parseBlockName(ctx, pos);
|
|
145
|
+
if (closeNameResult) {
|
|
146
|
+
pos += closeNameResult.consumed;
|
|
147
|
+
consumed += closeNameResult.consumed;
|
|
148
|
+
}
|
|
149
|
+
if (ctx.tokens[pos]?.type === "BLOCK_CLOSE") {
|
|
150
|
+
pos++;
|
|
151
|
+
consumed++;
|
|
152
|
+
}
|
|
153
|
+
// Skip trailing newline
|
|
154
|
+
if (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
155
|
+
pos++;
|
|
156
|
+
consumed++;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Default label if empty
|
|
161
|
+
const finalLabel = label.trim() || "untitled";
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
tab: {
|
|
165
|
+
label: finalLabel,
|
|
166
|
+
elements: bodyResult.elements,
|
|
167
|
+
},
|
|
168
|
+
consumed,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Block rule for `[[tabview]]`/`[[tabs]]` with `[[tab]]` children.
|
|
174
|
+
*
|
|
175
|
+
* Parsing strategy:
|
|
176
|
+
* 1. Match BLOCK_OPEN + name "tabview" or "tabs" (case-insensitive).
|
|
177
|
+
* 2. Skip any trailing text/attributes on the opening tag (ignored).
|
|
178
|
+
* 3. Consume `]]` and optional newline.
|
|
179
|
+
* 4. Repeatedly parse `[[tab Label]]...[[/tab]]` blocks via `parseTab()`.
|
|
180
|
+
* 5. Whitespace and newlines between tabs are skipped; other content fails.
|
|
181
|
+
* 6. Consume `[[/tabview]]` or `[[/tabs]]`.
|
|
182
|
+
* 7. If no tabs were found, fail the rule.
|
|
183
|
+
* 8. Emit a `tab-view` element containing the array of tab data.
|
|
184
|
+
*/
|
|
185
|
+
export const tabviewRule: BlockRule = {
|
|
186
|
+
name: "tabview",
|
|
187
|
+
startTokens: ["BLOCK_OPEN"],
|
|
188
|
+
requiresLineStart: false,
|
|
189
|
+
|
|
190
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
191
|
+
const openToken = currentToken(ctx);
|
|
192
|
+
if (openToken.type !== "BLOCK_OPEN") {
|
|
193
|
+
return { success: false };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
let pos = ctx.pos + 1;
|
|
197
|
+
let consumed = 1;
|
|
198
|
+
|
|
199
|
+
// Parse block name
|
|
200
|
+
const nameResult = parseBlockName(ctx, pos);
|
|
201
|
+
if (!nameResult) {
|
|
202
|
+
return { success: false };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const blockName = nameResult.name.toLowerCase();
|
|
206
|
+
// Accept tabview or tabs
|
|
207
|
+
if (blockName !== "tabview" && blockName !== "tabs") {
|
|
208
|
+
return { success: false };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
pos += nameResult.consumed;
|
|
212
|
+
consumed += nameResult.consumed;
|
|
213
|
+
|
|
214
|
+
// Skip any attributes/name (Wikidot ignores [[tabview Foo]])
|
|
215
|
+
while (
|
|
216
|
+
pos < ctx.tokens.length &&
|
|
217
|
+
ctx.tokens[pos]?.type !== "BLOCK_CLOSE" &&
|
|
218
|
+
ctx.tokens[pos]?.type !== "NEWLINE"
|
|
219
|
+
) {
|
|
220
|
+
pos++;
|
|
221
|
+
consumed++;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Expect ]]
|
|
225
|
+
if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
|
|
226
|
+
return { success: false };
|
|
227
|
+
}
|
|
228
|
+
pos++;
|
|
229
|
+
consumed++;
|
|
230
|
+
|
|
231
|
+
// Skip newline after opening tag
|
|
232
|
+
if (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
233
|
+
pos++;
|
|
234
|
+
consumed++;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Parse tabs
|
|
238
|
+
const tabs: TabData[] = [];
|
|
239
|
+
const tabCtx: ParseContext = { ...ctx, pos };
|
|
240
|
+
|
|
241
|
+
while (pos < ctx.tokens.length) {
|
|
242
|
+
// Check for EOF
|
|
243
|
+
if (ctx.tokens[pos]?.type === "EOF") {
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check for closing [[/tabview]] or [[/tabs]]
|
|
248
|
+
if (ctx.tokens[pos]?.type === "BLOCK_END_OPEN") {
|
|
249
|
+
const closeNameResult = parseBlockName(ctx, pos + 1);
|
|
250
|
+
const closeName = closeNameResult?.name.toLowerCase();
|
|
251
|
+
if (closeName === "tabview" || closeName === "tabs") {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Try to parse a tab
|
|
257
|
+
const tabResult = parseTab({ ...tabCtx, pos });
|
|
258
|
+
if (tabResult) {
|
|
259
|
+
tabs.push(tabResult.tab);
|
|
260
|
+
pos += tabResult.consumed;
|
|
261
|
+
consumed += tabResult.consumed;
|
|
262
|
+
} else {
|
|
263
|
+
// Skip whitespace/newlines between tabs
|
|
264
|
+
if (ctx.tokens[pos]?.type === "WHITESPACE" || ctx.tokens[pos]?.type === "NEWLINE") {
|
|
265
|
+
pos++;
|
|
266
|
+
consumed++;
|
|
267
|
+
} else {
|
|
268
|
+
// Non-tab content in tabview - fail
|
|
269
|
+
return { success: false };
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Check for missing close tag
|
|
275
|
+
const hasTabviewClose =
|
|
276
|
+
ctx.tokens[pos]?.type === "BLOCK_END_OPEN" &&
|
|
277
|
+
(() => {
|
|
278
|
+
const n = parseBlockName(ctx, pos + 1);
|
|
279
|
+
const name = n?.name.toLowerCase();
|
|
280
|
+
return name === "tabview" || name === "tabs";
|
|
281
|
+
})();
|
|
282
|
+
if (!hasTabviewClose) {
|
|
283
|
+
ctx.diagnostics.push({
|
|
284
|
+
severity: "warning",
|
|
285
|
+
code: "unclosed-block",
|
|
286
|
+
message: `Missing closing tag [[/${blockName}]] for [[${blockName}]]`,
|
|
287
|
+
position: openToken.position,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Consume [[/tabview]] or [[/tabs]]
|
|
292
|
+
if (ctx.tokens[pos]?.type === "BLOCK_END_OPEN") {
|
|
293
|
+
pos++;
|
|
294
|
+
consumed++;
|
|
295
|
+
const closeNameResult = parseBlockName(ctx, pos);
|
|
296
|
+
if (closeNameResult) {
|
|
297
|
+
pos += closeNameResult.consumed;
|
|
298
|
+
consumed += closeNameResult.consumed;
|
|
299
|
+
}
|
|
300
|
+
if (ctx.tokens[pos]?.type === "BLOCK_CLOSE") {
|
|
301
|
+
pos++;
|
|
302
|
+
consumed++;
|
|
303
|
+
}
|
|
304
|
+
if (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
305
|
+
pos++;
|
|
306
|
+
consumed++;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Empty tabview is invalid - return failure so it falls back to plain text.
|
|
311
|
+
// Note: Ideally, a failed tabview like "EMPTY:\n[[tabview]]\n[[/tabview]]" should
|
|
312
|
+
// render as a single paragraph with <br /> separators (matching original Wikidot).
|
|
313
|
+
// Currently, the paragraph parser treats [[tabview]] as a block-start token and
|
|
314
|
+
// splits it into separate paragraphs. We chose to wrap each in <p> tags instead
|
|
315
|
+
// of implementing complex lookahead to detect invalid tabviews.
|
|
316
|
+
if (tabs.length === 0) {
|
|
317
|
+
return { success: false };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
success: true,
|
|
322
|
+
elements: [
|
|
323
|
+
{
|
|
324
|
+
element: "tab-view",
|
|
325
|
+
data: tabs,
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
consumed,
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
};
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table of contents rule: [[toc]], [[f<toc ...]], [[f>toc ...]]
|
|
3
|
+
*
|
|
4
|
+
* Variants:
|
|
5
|
+
* - [[toc]] - basic TOC
|
|
6
|
+
* - [[f<toc ...]] - float left
|
|
7
|
+
* - [[f>toc ...]] - float right
|
|
8
|
+
*
|
|
9
|
+
* Note: Wikidot ignores attributes on [[toc]] (class, style, id are not applied)
|
|
10
|
+
* [[>toc]] and [[<toc]] are invalid in Wikidot and not supported.
|
|
11
|
+
*/
|
|
12
|
+
import type { Alignment, Element } from "@wdprlib/ast";
|
|
13
|
+
import type { BlockRule, ParseContext, RuleResult } from "../types";
|
|
14
|
+
import { currentToken } from "../types";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Skip tokens until BLOCK_CLOSE (Wikidot ignores toc attributes)
|
|
18
|
+
*/
|
|
19
|
+
function skipUntilClose(ctx: ParseContext, startPos: number): number {
|
|
20
|
+
let pos = startPos;
|
|
21
|
+
while (pos < ctx.tokens.length) {
|
|
22
|
+
const token = ctx.tokens[pos];
|
|
23
|
+
if (
|
|
24
|
+
!token ||
|
|
25
|
+
token.type === "BLOCK_CLOSE" ||
|
|
26
|
+
token.type === "NEWLINE" ||
|
|
27
|
+
token.type === "EOF"
|
|
28
|
+
) {
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
pos++;
|
|
32
|
+
}
|
|
33
|
+
return pos;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const tocRule: BlockRule = {
|
|
37
|
+
name: "toc",
|
|
38
|
+
startTokens: ["BLOCK_OPEN"],
|
|
39
|
+
requiresLineStart: true,
|
|
40
|
+
|
|
41
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
42
|
+
const openToken = currentToken(ctx);
|
|
43
|
+
if (openToken.type !== "BLOCK_OPEN") {
|
|
44
|
+
return { success: false };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let pos = ctx.pos + 1;
|
|
48
|
+
let align: Alignment | null = null;
|
|
49
|
+
|
|
50
|
+
// Skip whitespace after [[
|
|
51
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
52
|
+
pos++;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const firstToken = ctx.tokens[pos];
|
|
56
|
+
if (!firstToken || (firstToken.type !== "TEXT" && firstToken.type !== "IDENTIFIER")) {
|
|
57
|
+
return { success: false };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const firstValue = firstToken.value.toLowerCase();
|
|
61
|
+
|
|
62
|
+
if (firstValue === "toc") {
|
|
63
|
+
// [[toc ...]]
|
|
64
|
+
// Page syntax disabled (e.g., forum-post mode)
|
|
65
|
+
if (!ctx.settings.enablePageSyntax) {
|
|
66
|
+
return { success: false };
|
|
67
|
+
}
|
|
68
|
+
pos++;
|
|
69
|
+
} else if (firstValue === "f") {
|
|
70
|
+
// Possibly [[f<toc ...]] or [[f>toc ...]]
|
|
71
|
+
pos++;
|
|
72
|
+
const dirToken = ctx.tokens[pos];
|
|
73
|
+
if (!dirToken) {
|
|
74
|
+
return { success: false };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (dirToken.type === "TEXT" && dirToken.value === "<") {
|
|
78
|
+
align = "left";
|
|
79
|
+
pos++;
|
|
80
|
+
} else if (dirToken.type === "TEXT" && dirToken.value === ">") {
|
|
81
|
+
align = "right";
|
|
82
|
+
pos++;
|
|
83
|
+
} else {
|
|
84
|
+
return { success: false };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Now expect "toc"
|
|
88
|
+
const tocToken = ctx.tokens[pos];
|
|
89
|
+
if (!tocToken || (tocToken.type !== "TEXT" && tocToken.type !== "IDENTIFIER")) {
|
|
90
|
+
return { success: false };
|
|
91
|
+
}
|
|
92
|
+
if (tocToken.value.toLowerCase() !== "toc") {
|
|
93
|
+
return { success: false };
|
|
94
|
+
}
|
|
95
|
+
// Page syntax disabled (e.g., forum-post mode)
|
|
96
|
+
if (!ctx.settings.enablePageSyntax) {
|
|
97
|
+
return { success: false };
|
|
98
|
+
}
|
|
99
|
+
pos++;
|
|
100
|
+
} else {
|
|
101
|
+
return { success: false };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Skip any tokens until ]] (Wikidot ignores toc attributes)
|
|
105
|
+
pos = skipUntilClose(ctx, pos);
|
|
106
|
+
|
|
107
|
+
// Expect ]]
|
|
108
|
+
if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
|
|
109
|
+
return { success: false };
|
|
110
|
+
}
|
|
111
|
+
pos++;
|
|
112
|
+
|
|
113
|
+
const consumed = pos - ctx.pos;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
success: true,
|
|
117
|
+
elements: [
|
|
118
|
+
{
|
|
119
|
+
element: "table-of-contents",
|
|
120
|
+
data: {
|
|
121
|
+
attributes: {},
|
|
122
|
+
align,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
consumed,
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
};
|