@wdprlib/parser 3.1.2 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/index.cjs +295 -118
  2. package/dist/index.js +272 -95
  3. package/package.json +5 -3
  4. package/src/index.ts +163 -0
  5. package/src/lexer/index.ts +20 -0
  6. package/src/lexer/lexer.ts +687 -0
  7. package/src/lexer/tokens.ts +141 -0
  8. package/src/parser/constants.ts +173 -0
  9. package/src/parser/depth.ts +251 -0
  10. package/src/parser/index.ts +18 -0
  11. package/src/parser/parse.ts +315 -0
  12. package/src/parser/postprocess/divAdjacentParagraph.ts +76 -0
  13. package/src/parser/postprocess/index.ts +15 -0
  14. package/src/parser/postprocess/spanStrip.ts +697 -0
  15. package/src/parser/preprocess/expr.ts +265 -0
  16. package/src/parser/preprocess/index.ts +38 -0
  17. package/src/parser/preprocess/typography.ts +67 -0
  18. package/src/parser/preprocess/utils.ts +250 -0
  19. package/src/parser/preprocess/whitespace.ts +111 -0
  20. package/src/parser/rules/block/align.ts +282 -0
  21. package/src/parser/rules/block/bibliography.ts +359 -0
  22. package/src/parser/rules/block/block-list.ts +689 -0
  23. package/src/parser/rules/block/blockquote.ts +238 -0
  24. package/src/parser/rules/block/center.ts +87 -0
  25. package/src/parser/rules/block/clear-float.ts +75 -0
  26. package/src/parser/rules/block/code.ts +187 -0
  27. package/src/parser/rules/block/collapsible.ts +337 -0
  28. package/src/parser/rules/block/comment.ts +73 -0
  29. package/src/parser/rules/block/content-separator.ts +79 -0
  30. package/src/parser/rules/block/definition-list.ts +270 -0
  31. package/src/parser/rules/block/div.ts +400 -0
  32. package/src/parser/rules/block/embed-block.ts +153 -0
  33. package/src/parser/rules/block/footnoteblock.ts +200 -0
  34. package/src/parser/rules/block/heading.ts +142 -0
  35. package/src/parser/rules/block/horizontal-rule.ts +61 -0
  36. package/src/parser/rules/block/html.ts +222 -0
  37. package/src/parser/rules/block/iframe.ts +239 -0
  38. package/src/parser/rules/block/iftags.ts +150 -0
  39. package/src/parser/rules/block/include.ts +179 -0
  40. package/src/parser/rules/block/index.ts +127 -0
  41. package/src/parser/rules/block/list.ts +244 -0
  42. package/src/parser/rules/block/math.ts +183 -0
  43. package/src/parser/rules/block/module/backlinks/index.ts +31 -0
  44. package/src/parser/rules/block/module/backlinks/types.ts +21 -0
  45. package/src/parser/rules/block/module/categories/index.ts +34 -0
  46. package/src/parser/rules/block/module/categories/types.ts +21 -0
  47. package/src/parser/rules/block/module/css/index.ts +37 -0
  48. package/src/parser/rules/block/module/iftags/condition.ts +109 -0
  49. package/src/parser/rules/block/module/iftags/index.ts +26 -0
  50. package/src/parser/rules/block/module/iftags/preprocess.ts +140 -0
  51. package/src/parser/rules/block/module/iftags/resolve.ts +73 -0
  52. package/src/parser/rules/block/module/iftags/types.ts +63 -0
  53. package/src/parser/rules/block/module/include/index.ts +20 -0
  54. package/src/parser/rules/block/module/include/resolve.ts +556 -0
  55. package/src/parser/rules/block/module/index.ts +122 -0
  56. package/src/parser/rules/block/module/join/index.ts +34 -0
  57. package/src/parser/rules/block/module/join/types.ts +23 -0
  58. package/src/parser/rules/block/module/listpages/compiler.ts +453 -0
  59. package/src/parser/rules/block/module/listpages/extract.ts +410 -0
  60. package/src/parser/rules/block/module/listpages/index.ts +83 -0
  61. package/src/parser/rules/block/module/listpages/normalize.ts +390 -0
  62. package/src/parser/rules/block/module/listpages/parser.ts +106 -0
  63. package/src/parser/rules/block/module/listpages/resolve.ts +130 -0
  64. package/src/parser/rules/block/module/listpages/types.ts +513 -0
  65. package/src/parser/rules/block/module/listpages/url-resolver.ts +186 -0
  66. package/src/parser/rules/block/module/listusers/compiler.ts +77 -0
  67. package/src/parser/rules/block/module/listusers/extract.ts +45 -0
  68. package/src/parser/rules/block/module/listusers/index.ts +36 -0
  69. package/src/parser/rules/block/module/listusers/parser.ts +54 -0
  70. package/src/parser/rules/block/module/listusers/resolve.ts +58 -0
  71. package/src/parser/rules/block/module/listusers/types.ts +93 -0
  72. package/src/parser/rules/block/module/mapping.ts +61 -0
  73. package/src/parser/rules/block/module/page-tree/index.ts +38 -0
  74. package/src/parser/rules/block/module/page-tree/types.ts +29 -0
  75. package/src/parser/rules/block/module/rate/index.ts +28 -0
  76. package/src/parser/rules/block/module/rate/types.ts +19 -0
  77. package/src/parser/rules/block/module/resolve.ts +411 -0
  78. package/src/parser/rules/block/module/types-common.ts +59 -0
  79. package/src/parser/rules/block/module/types.ts +61 -0
  80. package/src/parser/rules/block/module/utils.ts +43 -0
  81. package/src/parser/rules/block/module/walk.ts +380 -0
  82. package/src/parser/rules/block/module.ts +164 -0
  83. package/src/parser/rules/block/orphan-li.ts +177 -0
  84. package/src/parser/rules/block/paragraph.ts +157 -0
  85. package/src/parser/rules/block/table-block.ts +726 -0
  86. package/src/parser/rules/block/table.ts +441 -0
  87. package/src/parser/rules/block/tabview.ts +331 -0
  88. package/src/parser/rules/block/toc.ts +129 -0
  89. package/src/parser/rules/block/utils.ts +615 -0
  90. package/src/parser/rules/index.ts +49 -0
  91. package/src/parser/rules/inline/anchor-name.ts +154 -0
  92. package/src/parser/rules/inline/anchor.ts +327 -0
  93. package/src/parser/rules/inline/bibcite.ts +153 -0
  94. package/src/parser/rules/inline/bold.ts +86 -0
  95. package/src/parser/rules/inline/color.ts +140 -0
  96. package/src/parser/rules/inline/comment.ts +90 -0
  97. package/src/parser/rules/inline/equation-ref.ts +115 -0
  98. package/src/parser/rules/inline/expr.ts +526 -0
  99. package/src/parser/rules/inline/footnote.ts +223 -0
  100. package/src/parser/rules/inline/guillemet.ts +64 -0
  101. package/src/parser/rules/inline/html.ts +132 -0
  102. package/src/parser/rules/inline/image.ts +328 -0
  103. package/src/parser/rules/inline/index.ts +150 -0
  104. package/src/parser/rules/inline/italic.ts +74 -0
  105. package/src/parser/rules/inline/line-break.ts +326 -0
  106. package/src/parser/rules/inline/link-anchor.ts +147 -0
  107. package/src/parser/rules/inline/link-single.ts +164 -0
  108. package/src/parser/rules/inline/link-star.ts +134 -0
  109. package/src/parser/rules/inline/link-triple.ts +267 -0
  110. package/src/parser/rules/inline/math-inline.ts +126 -0
  111. package/src/parser/rules/inline/monospace.ts +78 -0
  112. package/src/parser/rules/inline/raw.ts +262 -0
  113. package/src/parser/rules/inline/size.ts +244 -0
  114. package/src/parser/rules/inline/span.ts +424 -0
  115. package/src/parser/rules/inline/strikethrough.ts +115 -0
  116. package/src/parser/rules/inline/subscript.ts +84 -0
  117. package/src/parser/rules/inline/superscript.ts +84 -0
  118. package/src/parser/rules/inline/text.ts +84 -0
  119. package/src/parser/rules/inline/underline.ts +127 -0
  120. package/src/parser/rules/inline/user.ts +147 -0
  121. package/src/parser/rules/inline/utils.ts +344 -0
  122. package/src/parser/rules/types.ts +252 -0
  123. package/src/parser/rules/utils.ts +155 -0
  124. package/src/parser/toc.ts +130 -0
@@ -0,0 +1,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
+ };