@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,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Block rule for Wikidot collapsible blocks: `[[collapsible]]...[[/collapsible]]`.
|
|
3
|
+
*
|
|
4
|
+
* A collapsible renders as a show/hide toggle with body content that can
|
|
5
|
+
* be expanded or collapsed. The opening tag accepts several attributes
|
|
6
|
+
* (which may span multiple lines):
|
|
7
|
+
*
|
|
8
|
+
* - `show` -- label text for the "show" link (default: "+ show block").
|
|
9
|
+
* - `hide` -- label text for the "hide" link (default: "- hide block").
|
|
10
|
+
* - `folded` -- when `"no"`, the block starts in the expanded state.
|
|
11
|
+
* - `hideLocation` -- where the toggle link appears: `"top"` (default),
|
|
12
|
+
* `"bottom"`, `"both"`, or `"neither"`/`"none"`.
|
|
13
|
+
*
|
|
14
|
+
* Key Wikidot-specific behaviours:
|
|
15
|
+
* - Collapsibles cannot nest. When the body parser encounters a second
|
|
16
|
+
* `[[collapsible]]`, it is treated as plain text. This is achieved by
|
|
17
|
+
* filtering the collapsible rule out of the block rule list for body parsing.
|
|
18
|
+
* - Orphaned `[[/collapsible]]` tags after the matched close are consumed and
|
|
19
|
+
* emitted as `<br />` + literal text, matching Wikidot rendering.
|
|
20
|
+
* - An inline form (`[[collapsible]]text[[/collapsible]]` on one line) is
|
|
21
|
+
* supported but uncommon.
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import type { Element } from "@wdprlib/ast";
|
|
26
|
+
import type { BlockRule, ParseContext, RuleResult } from "../types";
|
|
27
|
+
import { currentToken } from "../types";
|
|
28
|
+
import { parseBlockName, parseBlocksUntil } from "./utils";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parses block attributes that may be spread across multiple lines.
|
|
32
|
+
*
|
|
33
|
+
* Unlike the standard {@link parseAttributes}, this variant allows NEWLINE
|
|
34
|
+
* tokens between attribute pairs, which Wikidot permits for `[[collapsible]]`
|
|
35
|
+
* tags with many attributes.
|
|
36
|
+
*
|
|
37
|
+
* @param ctx - Parse context.
|
|
38
|
+
* @param startPos - Token index to start scanning.
|
|
39
|
+
* @returns Parsed key/value pairs and the number of tokens consumed.
|
|
40
|
+
*/
|
|
41
|
+
function parseMultilineAttributes(
|
|
42
|
+
ctx: ParseContext,
|
|
43
|
+
startPos: number,
|
|
44
|
+
): { attrs: Record<string, string>; consumed: number } {
|
|
45
|
+
const attrs: Record<string, string> = {};
|
|
46
|
+
let pos = startPos;
|
|
47
|
+
let consumed = 0;
|
|
48
|
+
|
|
49
|
+
while (pos < ctx.tokens.length) {
|
|
50
|
+
const token = ctx.tokens[pos];
|
|
51
|
+
if (!token || token.type === "BLOCK_CLOSE" || token.type === "EOF") {
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (token.type === "WHITESPACE" || token.type === "NEWLINE") {
|
|
56
|
+
pos++;
|
|
57
|
+
consumed++;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (token.type === "TEXT" || token.type === "IDENTIFIER") {
|
|
62
|
+
let name = token.value;
|
|
63
|
+
pos++;
|
|
64
|
+
consumed++;
|
|
65
|
+
|
|
66
|
+
// Handle hyphenated names (e.g. "hide-location")
|
|
67
|
+
while (
|
|
68
|
+
ctx.tokens[pos]?.type === "TEXT" &&
|
|
69
|
+
ctx.tokens[pos]?.value === "-" &&
|
|
70
|
+
(ctx.tokens[pos + 1]?.type === "IDENTIFIER" || ctx.tokens[pos + 1]?.type === "TEXT")
|
|
71
|
+
) {
|
|
72
|
+
name += "-";
|
|
73
|
+
pos++;
|
|
74
|
+
consumed++;
|
|
75
|
+
name += ctx.tokens[pos]?.value ?? "";
|
|
76
|
+
pos++;
|
|
77
|
+
consumed++;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const eqToken = ctx.tokens[pos];
|
|
81
|
+
if (eqToken?.type === "EQUALS") {
|
|
82
|
+
pos++;
|
|
83
|
+
consumed++;
|
|
84
|
+
const valueToken = ctx.tokens[pos];
|
|
85
|
+
if (valueToken?.type === "QUOTED_STRING") {
|
|
86
|
+
let value = valueToken.value;
|
|
87
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
88
|
+
value = value.slice(1, -1);
|
|
89
|
+
}
|
|
90
|
+
attrs[name] = value;
|
|
91
|
+
pos++;
|
|
92
|
+
consumed++;
|
|
93
|
+
} else if (valueToken?.type === "TEXT" || valueToken?.type === "IDENTIFIER") {
|
|
94
|
+
attrs[name] = valueToken.value;
|
|
95
|
+
pos++;
|
|
96
|
+
consumed++;
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
attrs[name] = "true";
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
// Unknown token type in attribute context, stop
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { attrs, consumed };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Tests whether the tokens at `pos` form a `[[/collapsible]]` closing tag.
|
|
112
|
+
*
|
|
113
|
+
* @param ctx - Parse context.
|
|
114
|
+
* @param pos - Token index to inspect.
|
|
115
|
+
* @returns `true` if the closing tag is found.
|
|
116
|
+
*/
|
|
117
|
+
function isCollapsibleClose(ctx: ParseContext, pos: number): boolean {
|
|
118
|
+
if (ctx.tokens[pos]?.type !== "BLOCK_END_OPEN") return false;
|
|
119
|
+
const nameResult = parseBlockName(ctx, pos + 1);
|
|
120
|
+
return nameResult?.name === "collapsible";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Counts the number of tokens occupied by the `[[/collapsible]]` closing
|
|
125
|
+
* tag, including the optional trailing NEWLINE.
|
|
126
|
+
*
|
|
127
|
+
* @param ctx - Parse context.
|
|
128
|
+
* @param pos - Token index at the BLOCK_END_OPEN.
|
|
129
|
+
* @returns Total token count of the closing tag.
|
|
130
|
+
*/
|
|
131
|
+
function consumeCloseTag(ctx: ParseContext, pos: number): number {
|
|
132
|
+
let closeConsumed = 1; // BLOCK_END_OPEN
|
|
133
|
+
const nameResult = parseBlockName(ctx, pos + 1);
|
|
134
|
+
if (nameResult) closeConsumed += nameResult.consumed;
|
|
135
|
+
if (ctx.tokens[pos + closeConsumed]?.type === "BLOCK_CLOSE") closeConsumed++;
|
|
136
|
+
if (ctx.tokens[pos + closeConsumed]?.type === "NEWLINE") closeConsumed++;
|
|
137
|
+
return closeConsumed;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Block names excluded from rule dispatch and paragraph-boundary detection. */
|
|
141
|
+
const EXCLUDED_BLOCKS = new Set(["collapsible"]);
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Block rule for `[[collapsible ...]]...[[/collapsible]]`.
|
|
145
|
+
*
|
|
146
|
+
* Parsing strategy:
|
|
147
|
+
* 1. Match BLOCK_OPEN + name "collapsible".
|
|
148
|
+
* 2. Parse multiline attributes (show, hide, folded, hideLocation, etc.).
|
|
149
|
+
* 3. If a NEWLINE follows the opening tag, parse body as block content
|
|
150
|
+
* with the collapsible rule itself excluded (to prevent nesting).
|
|
151
|
+
* Otherwise, parse inline content until close tag or end of line
|
|
152
|
+
* (inline form).
|
|
153
|
+
* 4. Consume the `[[/collapsible]]` closing tag.
|
|
154
|
+
* 5. Consume any orphaned `[[/collapsible]]` tags that follow, converting
|
|
155
|
+
* them to `<br />` + literal text.
|
|
156
|
+
* 6. Derive `show-top` / `show-bottom` booleans from the `hideLocation`
|
|
157
|
+
* attribute.
|
|
158
|
+
*/
|
|
159
|
+
export const collapsibleRule: BlockRule = {
|
|
160
|
+
name: "collapsible",
|
|
161
|
+
startTokens: ["BLOCK_OPEN"],
|
|
162
|
+
requiresLineStart: false,
|
|
163
|
+
|
|
164
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
165
|
+
const openToken = currentToken(ctx);
|
|
166
|
+
if (openToken.type !== "BLOCK_OPEN") {
|
|
167
|
+
return { success: false };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let pos = ctx.pos + 1;
|
|
171
|
+
let consumed = 1;
|
|
172
|
+
|
|
173
|
+
const nameResult = parseBlockName(ctx, pos);
|
|
174
|
+
if (!nameResult || nameResult.name !== "collapsible") {
|
|
175
|
+
return { success: false };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
pos += nameResult.consumed;
|
|
179
|
+
consumed += nameResult.consumed;
|
|
180
|
+
|
|
181
|
+
const attrResult = parseMultilineAttributes(ctx, pos);
|
|
182
|
+
pos += attrResult.consumed;
|
|
183
|
+
consumed += attrResult.consumed;
|
|
184
|
+
|
|
185
|
+
if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
|
|
186
|
+
return { success: false };
|
|
187
|
+
}
|
|
188
|
+
pos++;
|
|
189
|
+
consumed++;
|
|
190
|
+
|
|
191
|
+
// Record opening tag position for diagnostics
|
|
192
|
+
const openPosition = openToken.position;
|
|
193
|
+
|
|
194
|
+
const hasNewlineAfterOpen = ctx.tokens[pos]?.type === "NEWLINE";
|
|
195
|
+
if (hasNewlineAfterOpen) {
|
|
196
|
+
pos++;
|
|
197
|
+
consumed++;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let bodyElements: Element[];
|
|
201
|
+
|
|
202
|
+
if (
|
|
203
|
+
!hasNewlineAfterOpen &&
|
|
204
|
+
ctx.tokens[pos]?.type !== "EOF" &&
|
|
205
|
+
ctx.tokens[pos]?.type !== "BLOCK_END_OPEN"
|
|
206
|
+
) {
|
|
207
|
+
// Inline form: [[collapsible]]content[[/collapsible]] on same line
|
|
208
|
+
// Parse inline content until [[/collapsible]] or NEWLINE
|
|
209
|
+
const inlineElements: Element[] = [];
|
|
210
|
+
let inlineConsumed = 0;
|
|
211
|
+
let inlinePos = pos;
|
|
212
|
+
|
|
213
|
+
while (inlinePos < ctx.tokens.length) {
|
|
214
|
+
const token = ctx.tokens[inlinePos];
|
|
215
|
+
if (!token || token.type === "EOF" || token.type === "NEWLINE") break;
|
|
216
|
+
if (isCollapsibleClose(ctx, inlinePos)) break;
|
|
217
|
+
inlineElements.push({ element: "text", data: token.value });
|
|
218
|
+
inlinePos++;
|
|
219
|
+
inlineConsumed++;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
consumed += inlineConsumed;
|
|
223
|
+
pos += inlineConsumed;
|
|
224
|
+
|
|
225
|
+
if (inlineElements.length > 0) {
|
|
226
|
+
bodyElements = [
|
|
227
|
+
{
|
|
228
|
+
element: "container",
|
|
229
|
+
data: {
|
|
230
|
+
type: "paragraph",
|
|
231
|
+
attributes: {},
|
|
232
|
+
elements: inlineElements,
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
];
|
|
236
|
+
} else {
|
|
237
|
+
bodyElements = [];
|
|
238
|
+
}
|
|
239
|
+
} else {
|
|
240
|
+
// Block form: parse content recursively until [[/collapsible]]
|
|
241
|
+
// Collapsible cannot be nested in Wikidot - nested [[collapsible]] becomes plain text.
|
|
242
|
+
// excludedBlockNames removes the collapsible rule from dispatch AND prevents
|
|
243
|
+
// [[collapsible]] / [[/collapsible]] tokens from triggering paragraph splits.
|
|
244
|
+
const bodyCtx: ParseContext = { ...ctx, pos };
|
|
245
|
+
|
|
246
|
+
const closeCondition = (checkCtx: ParseContext): boolean => {
|
|
247
|
+
return isCollapsibleClose(checkCtx, checkCtx.pos);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const bodyResult = parseBlocksUntil(bodyCtx, closeCondition, {
|
|
251
|
+
excludedBlockNames: EXCLUDED_BLOCKS,
|
|
252
|
+
});
|
|
253
|
+
consumed += bodyResult.consumed;
|
|
254
|
+
pos += bodyResult.consumed;
|
|
255
|
+
|
|
256
|
+
bodyElements = bodyResult.elements;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Check for missing close tag
|
|
260
|
+
if (!isCollapsibleClose(ctx, pos)) {
|
|
261
|
+
ctx.diagnostics.push({
|
|
262
|
+
severity: "warning",
|
|
263
|
+
code: "unclosed-block",
|
|
264
|
+
message: "Missing closing tag [[/collapsible]] for [[collapsible]]",
|
|
265
|
+
position: openPosition,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Consume [[/collapsible]]
|
|
270
|
+
if (isCollapsibleClose(ctx, pos)) {
|
|
271
|
+
const closeConsumed = consumeCloseTag(ctx, pos);
|
|
272
|
+
consumed += closeConsumed;
|
|
273
|
+
pos += closeConsumed;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Consume orphaned [[/collapsible]] after the block
|
|
277
|
+
// Wikidot renders these as bare <br />[[/collapsible]] (no <p> wrapper)
|
|
278
|
+
const orphanedElements: Element[] = [];
|
|
279
|
+
while (isCollapsibleClose(ctx, pos)) {
|
|
280
|
+
orphanedElements.push({ element: "line-break" });
|
|
281
|
+
|
|
282
|
+
// Convert tokens of [[/collapsible]] to text
|
|
283
|
+
while (pos < ctx.tokens.length) {
|
|
284
|
+
const t = ctx.tokens[pos];
|
|
285
|
+
if (!t || t.type === "NEWLINE" || t.type === "EOF") break;
|
|
286
|
+
orphanedElements.push({ element: "text", data: t.value });
|
|
287
|
+
consumed++;
|
|
288
|
+
pos++;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Skip trailing NEWLINE
|
|
292
|
+
if (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
293
|
+
consumed++;
|
|
294
|
+
pos++;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Determine show-top/show-bottom from hideLocation attribute
|
|
299
|
+
const hideLocation = (
|
|
300
|
+
attrResult.attrs.hideLocation ??
|
|
301
|
+
attrResult.attrs.hidelocation ??
|
|
302
|
+
"top"
|
|
303
|
+
).toLowerCase();
|
|
304
|
+
let showTop = true;
|
|
305
|
+
let showBottom = false;
|
|
306
|
+
if (hideLocation === "both") {
|
|
307
|
+
showTop = true;
|
|
308
|
+
showBottom = true;
|
|
309
|
+
} else if (hideLocation === "bottom") {
|
|
310
|
+
showTop = false;
|
|
311
|
+
showBottom = true;
|
|
312
|
+
} else if (hideLocation === "neither" || hideLocation === "none") {
|
|
313
|
+
showTop = false;
|
|
314
|
+
showBottom = false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
success: true,
|
|
319
|
+
elements: [
|
|
320
|
+
{
|
|
321
|
+
element: "collapsible",
|
|
322
|
+
data: {
|
|
323
|
+
elements: bodyElements,
|
|
324
|
+
attributes: {},
|
|
325
|
+
"start-open": attrResult.attrs.folded === "no",
|
|
326
|
+
"show-text": attrResult.attrs.show ?? null,
|
|
327
|
+
"hide-text": attrResult.attrs.hide ?? null,
|
|
328
|
+
"show-top": showTop,
|
|
329
|
+
"show-bottom": showBottom,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
...orphanedElements,
|
|
333
|
+
],
|
|
334
|
+
consumed,
|
|
335
|
+
};
|
|
336
|
+
},
|
|
337
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Block rule for Wikidot comments: `[!-- ... --]`.
|
|
4
|
+
*
|
|
5
|
+
* Comments may span multiple lines and are completely stripped from the
|
|
6
|
+
* rendered output. The parser consumes all tokens from COMMENT_OPEN
|
|
7
|
+
* (`[!--`) through the matching COMMENT_CLOSE (`--]`), inclusive, plus
|
|
8
|
+
* any trailing newline.
|
|
9
|
+
*
|
|
10
|
+
* If the closing `--]` is never found (unterminated comment), the rule
|
|
11
|
+
* fails and tokens are left for other rules to handle.
|
|
12
|
+
*
|
|
13
|
+
* This rule requires line start so that inline comments appearing mid-line
|
|
14
|
+
* are handled by a separate inline rule instead.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import type { Element } from "@wdprlib/ast";
|
|
19
|
+
import type { BlockRule, ParseContext, RuleResult } from "../types";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Block rule for line-start comments (`[!-- ... --]`).
|
|
23
|
+
*
|
|
24
|
+
* Returns an empty elements array on success -- comments produce no output.
|
|
25
|
+
*/
|
|
26
|
+
export const blockCommentRule: BlockRule = {
|
|
27
|
+
name: "blockComment",
|
|
28
|
+
startTokens: ["COMMENT_OPEN"],
|
|
29
|
+
requiresLineStart: true,
|
|
30
|
+
|
|
31
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
32
|
+
let pos = ctx.pos + 1; // skip [!--
|
|
33
|
+
let consumed = 1;
|
|
34
|
+
|
|
35
|
+
// Consume all tokens until we find --]
|
|
36
|
+
while (pos < ctx.tokens.length) {
|
|
37
|
+
const token = ctx.tokens[pos];
|
|
38
|
+
if (!token) {
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (token.type === "COMMENT_CLOSE") {
|
|
43
|
+
consumed++;
|
|
44
|
+
pos++;
|
|
45
|
+
|
|
46
|
+
// Consume trailing newline if present
|
|
47
|
+
if (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
48
|
+
consumed++;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Return empty elements - comment is discarded
|
|
52
|
+
return {
|
|
53
|
+
success: true,
|
|
54
|
+
elements: [],
|
|
55
|
+
consumed,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (token.type === "EOF") {
|
|
60
|
+
// Unterminated comment — let the inline comment rule emit the diagnostic
|
|
61
|
+
// to avoid duplication when the paragraph fallback retries this token.
|
|
62
|
+
return { success: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
pos++;
|
|
66
|
+
consumed++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Unterminated comment — let the inline comment rule emit the diagnostic
|
|
70
|
+
// to avoid duplication when the paragraph fallback retries this token.
|
|
71
|
+
return { success: false };
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Block rule for the Wikidot content separator: `====` (four or more `=`
|
|
4
|
+
* signs at the start of a line).
|
|
5
|
+
*
|
|
6
|
+
* A content separator is a structural divider in Wikidot pages, distinct
|
|
7
|
+
* from a horizontal rule (`----`). It signals a semantic section boundary
|
|
8
|
+
* rather than a visual line.
|
|
9
|
+
*
|
|
10
|
+
* Conditions:
|
|
11
|
+
* - Must start at line start.
|
|
12
|
+
* - Requires at least four consecutive EQUALS tokens.
|
|
13
|
+
* - Must be followed by NEWLINE or EOF (no trailing text allowed).
|
|
14
|
+
*
|
|
15
|
+
* A single `=` followed by whitespace is the center-alignment rule, and
|
|
16
|
+
* two or three `=` signs are not special -- only four or more trigger this
|
|
17
|
+
* rule.
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
import type { Element } from "@wdprlib/ast";
|
|
22
|
+
import type { BlockRule, ParseContext, RuleResult } from "../types";
|
|
23
|
+
import { currentToken } from "../types";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Block rule for the content separator (`====`).
|
|
27
|
+
*
|
|
28
|
+
* Produces a `content-separator` element with no data payload.
|
|
29
|
+
*/
|
|
30
|
+
export const contentSeparatorRule: BlockRule = {
|
|
31
|
+
name: "content-separator",
|
|
32
|
+
startTokens: ["EQUALS"],
|
|
33
|
+
requiresLineStart: true,
|
|
34
|
+
|
|
35
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
36
|
+
const first = currentToken(ctx);
|
|
37
|
+
|
|
38
|
+
if (!first.lineStart) {
|
|
39
|
+
return { success: false };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Count consecutive = tokens at line start
|
|
43
|
+
let pos = ctx.pos;
|
|
44
|
+
let equalsCount = 0;
|
|
45
|
+
|
|
46
|
+
while (ctx.tokens[pos]?.type === "EQUALS") {
|
|
47
|
+
equalsCount++;
|
|
48
|
+
pos++;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Need at least 4 equals signs for content separator
|
|
52
|
+
if (equalsCount < 4) {
|
|
53
|
+
return { success: false };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Must be followed by newline or EOF
|
|
57
|
+
const nextToken = ctx.tokens[pos];
|
|
58
|
+
if (nextToken && nextToken.type !== "NEWLINE" && nextToken.type !== "EOF") {
|
|
59
|
+
return { success: false };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let consumed = equalsCount;
|
|
63
|
+
|
|
64
|
+
// Consume newline if present
|
|
65
|
+
if (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
66
|
+
consumed++;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
success: true,
|
|
71
|
+
elements: [
|
|
72
|
+
{
|
|
73
|
+
element: "content-separator",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
consumed,
|
|
77
|
+
};
|
|
78
|
+
},
|
|
79
|
+
};
|