@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,262 @@
1
+ /**
2
+ *
3
+ * Parses the Wikidot raw (verbatim) text syntaxes: `@@...@@` and `@<...>@`.
4
+ *
5
+ * Raw text bypasses all inline formatting -- the content between the
6
+ * delimiters is emitted as-is without interpretation of any Wikidot
7
+ * markup characters.
8
+ *
9
+ * Two syntax variants are supported:
10
+ *
11
+ * 1. Double-at syntax (`@@...@@`): The more common form. Content between
12
+ * the markers is treated as raw text. Several Wikidot quirks apply:
13
+ * - Empty raw (`@@@@` = two consecutive `RAW_OPEN` tokens) produces
14
+ * no output
15
+ * - `@@\n@@` (raw spanning a newline with no content) also produces
16
+ * no output
17
+ * - Content containing both `@<` and `>@` is discarded entirely
18
+ * - `>@` followed immediately by `@@` is split: `>` becomes raw
19
+ * content, `@@` acts as the closer, and the leftover `@` becomes text
20
+ *
21
+ * 2. Angle-bracket syntax (`@<...>@`): A less common form that also
22
+ * treats content as raw text. If the closing `>@` is not found on the
23
+ * same line, `@<` is treated as literal text. The `@<\n>@` spanning
24
+ * pattern outputs only `@<` as text.
25
+ *
26
+ * Produces a `"raw"` AST element whose `data` field contains the
27
+ * verbatim text string, or empty elements when the raw content is discarded.
28
+ *
29
+ * @module
30
+ */
31
+ import type { Element } from "@wdprlib/ast";
32
+ import type { InlineRule, ParseContext, RuleResult } from "../types";
33
+ import { currentToken, hasClosingMarkerBeforeNewline } from "../types";
34
+
35
+ /**
36
+ * Inline rule for parsing `@@...@@` and `@<...>@` raw (verbatim) text.
37
+ *
38
+ * Triggered by either `RAW_OPEN` (`@@`) or `RAW_BLOCK_OPEN` (`@<`) tokens.
39
+ * Delegates to the appropriate handler based on the opening token type.
40
+ */
41
+ export const rawRule: InlineRule = {
42
+ name: "raw",
43
+ startTokens: ["RAW_OPEN", "RAW_BLOCK_OPEN"],
44
+
45
+ /**
46
+ * Attempts to parse raw text at the current position.
47
+ *
48
+ * @param ctx - Parse context with token stream and current position
49
+ * @returns A successful result with `"raw"` element(s), text fallback,
50
+ * or empty elements depending on the variant and content
51
+ */
52
+ parse(ctx: ParseContext): RuleResult<Element> {
53
+ const startToken = currentToken(ctx);
54
+
55
+ // Handle @<...>@ syntax
56
+ if (startToken.type === "RAW_BLOCK_OPEN") {
57
+ return parseAngleRaw(ctx);
58
+ }
59
+
60
+ // Handle @@...@@ syntax
61
+ return parseDoubleAtRaw(ctx);
62
+ },
63
+ };
64
+
65
+ /**
66
+ * Parses the `@@...@@` double-at raw text syntax.
67
+ *
68
+ * Handles several Wikidot-specific edge cases:
69
+ * - Consecutive `@@@@` (two RAW_OPEN tokens) = empty raw, no output
70
+ * - `@@\n@@` = empty raw spanning newline, no output
71
+ * - Content with both `@<` and `>@` embedded = entirely discarded
72
+ * - `>@` immediately before `@@` = split into raw content + text
73
+ *
74
+ * When no closing `@@` is found on the same line, the opening `@@`
75
+ * is emitted as literal text.
76
+ *
77
+ * @param ctx - Parse context positioned at the opening `@@` token
78
+ * @returns Parse result with raw element(s), empty array, or text fallback
79
+ */
80
+ function parseDoubleAtRaw(ctx: ParseContext): RuleResult<Element> {
81
+ const startToken = currentToken(ctx);
82
+ let pos = ctx.pos + 1;
83
+
84
+ const next1 = ctx.tokens[pos];
85
+
86
+ // Wikidot behavior for consecutive @@:
87
+ // @@@@ (RAW_OPEN RAW_OPEN) -> empty raw (no output), rest becomes plain text
88
+ // @@@@@ (5 @s) -> empty raw + @ (text)
89
+ // @@@@@@ (6 @s) -> empty raw + @@ (text)
90
+ // Empty raw produces no output, so we just consume the tokens.
91
+ if (next1?.type === "RAW_OPEN") {
92
+ return {
93
+ success: true,
94
+ elements: [], // Empty raw produces no output
95
+ consumed: 2, // Only consume the two RAW_OPEN tokens (@@@@)
96
+ };
97
+ }
98
+
99
+ // Check if closing @@ exists before newline
100
+ if (!hasClosingMarkerBeforeNewline({ ...ctx, pos }, "RAW_OPEN")) {
101
+ // Special case: @@\n@@ (empty raw spanning newline)
102
+ // Wikidot treats this as an empty raw that produces no output
103
+ const nextToken = ctx.tokens[pos];
104
+ if (nextToken?.type === "NEWLINE") {
105
+ const afterNewline = ctx.tokens[pos + 1];
106
+ if (afterNewline?.type === "RAW_OPEN") {
107
+ // Consume @@, NEWLINE, @@ and produce no output
108
+ return {
109
+ success: true,
110
+ elements: [],
111
+ consumed: 3, // @@ + NEWLINE + @@
112
+ };
113
+ }
114
+ }
115
+ return {
116
+ success: true,
117
+ elements: [{ element: "text", data: startToken.value }],
118
+ consumed: 1,
119
+ };
120
+ }
121
+
122
+ // Collect raw content and check for embedded @< and >@
123
+ let value = "";
124
+ let consumed = 1; // opening @@
125
+ let hasBlockOpen = false; // has @<
126
+ let hasBlockClose = false; // has >@
127
+
128
+ // In @@...@@ syntax, only RAW_OPEN (@@) acts as closer.
129
+ // Special handling for RAW_BLOCK_CLOSE (>@) followed by RAW_OPEN (@@):
130
+ // Wikidot treats ">@@@" as ">@@" + "@", so we only take ">" and let the
131
+ // @ combine with the following @@ to form the closer.
132
+ while (pos < ctx.tokens.length) {
133
+ const token = ctx.tokens[pos];
134
+ if (!token || token.type === "RAW_OPEN" || token.type === "NEWLINE" || token.type === "EOF") {
135
+ break;
136
+ }
137
+ // Check if RAW_BLOCK_CLOSE (>@) is followed by RAW_OPEN (@@)
138
+ // Wikidot interprets ">@@@" as ">" (raw content) + "@@" (closer) + "@" (text)
139
+ // We need to only take ">" and output "@" as trailing text after the raw element
140
+ if (token.type === "RAW_BLOCK_CLOSE") {
141
+ const nextToken = ctx.tokens[pos + 1];
142
+ if (nextToken?.type === "RAW_OPEN") {
143
+ // Only take the ">" part
144
+ value += ">";
145
+ consumed += 2; // Consume both RAW_BLOCK_CLOSE and RAW_OPEN (as closer)
146
+ // Return raw element followed by "@" text (from the >@ token's @)
147
+ return {
148
+ success: true,
149
+ elements: [
150
+ { element: "raw", data: value },
151
+ { element: "text", data: "@" },
152
+ ],
153
+ consumed,
154
+ };
155
+ }
156
+ // Mark that we have embedded >@ (not followed by @@)
157
+ hasBlockClose = true;
158
+ }
159
+ // Track embedded @< token
160
+ if (token.type === "RAW_BLOCK_OPEN") {
161
+ hasBlockOpen = true;
162
+ }
163
+ value += token.value;
164
+ consumed++;
165
+ pos++;
166
+ }
167
+
168
+ // Consume closing @@
169
+ if (ctx.tokens[pos]?.type === "RAW_OPEN") {
170
+ consumed++;
171
+ pos++;
172
+ }
173
+
174
+ // Wikidot quirk: @@...@@ containing BOTH @< AND >@ is discarded entirely
175
+ // (produces no output, not even text fallback)
176
+ // Having only @< or only >@ is fine - they're treated as raw content.
177
+ if (hasBlockOpen && hasBlockClose) {
178
+ return {
179
+ success: true,
180
+ elements: [],
181
+ consumed,
182
+ };
183
+ }
184
+
185
+ return {
186
+ success: true,
187
+ elements: [{ element: "raw", data: value }],
188
+ consumed,
189
+ };
190
+ }
191
+
192
+ /**
193
+ * Parses the `@<...>@` angle-bracket raw text syntax.
194
+ *
195
+ * This is the less common raw text form. Content between `@<` and `>@`
196
+ * is treated as verbatim text.
197
+ *
198
+ * Edge cases:
199
+ * - `@<\n>@` (spanning a newline) outputs only `@<` as text and
200
+ * consumes all three tokens
201
+ * - No closing `>@` on the same line = `@<` emitted as literal text
202
+ *
203
+ * @param ctx - Parse context positioned at the opening `@<` token
204
+ * @returns Parse result with a raw element or text fallback
205
+ */
206
+ function parseAngleRaw(ctx: ParseContext): RuleResult<Element> {
207
+ const startToken = currentToken(ctx);
208
+ let pos = ctx.pos + 1;
209
+
210
+ // Check if closing >@ exists before newline
211
+ if (!hasClosingMarkerBeforeNewline({ ...ctx, pos }, "RAW_BLOCK_CLOSE")) {
212
+ // Special case: @<\n>@ (empty raw spanning newline)
213
+ // Wikidot treats @< as text and >@ disappears
214
+ const nextToken = ctx.tokens[pos];
215
+ if (nextToken?.type === "NEWLINE") {
216
+ const afterNewline = ctx.tokens[pos + 1];
217
+ if (afterNewline?.type === "RAW_BLOCK_CLOSE") {
218
+ // Consume @<, NEWLINE, >@ - output only @< as text
219
+ return {
220
+ success: true,
221
+ elements: [{ element: "text", data: startToken.value }],
222
+ consumed: 3, // @< + NEWLINE + >@
223
+ };
224
+ }
225
+ }
226
+ return {
227
+ success: true,
228
+ elements: [{ element: "text", data: startToken.value }],
229
+ consumed: 1,
230
+ };
231
+ }
232
+
233
+ // Collect raw content
234
+ let value = "";
235
+ let consumed = 1; // opening @<
236
+
237
+ while (pos < ctx.tokens.length) {
238
+ const token = ctx.tokens[pos];
239
+ if (
240
+ !token ||
241
+ token.type === "RAW_BLOCK_CLOSE" ||
242
+ token.type === "NEWLINE" ||
243
+ token.type === "EOF"
244
+ ) {
245
+ break;
246
+ }
247
+ value += token.value;
248
+ consumed++;
249
+ pos++;
250
+ }
251
+
252
+ // Consume closing >@
253
+ if (ctx.tokens[pos]?.type === "RAW_BLOCK_CLOSE") {
254
+ consumed++;
255
+ }
256
+
257
+ return {
258
+ success: true,
259
+ elements: [{ element: "raw", data: value }],
260
+ consumed,
261
+ };
262
+ }
@@ -0,0 +1,244 @@
1
+ /**
2
+ *
3
+ * Parses the Wikidot font size syntax: `[[size value]]text[[/size]]`.
4
+ *
5
+ * This syntax wraps inline content in a `<span>` with an explicit
6
+ * `font-size` CSS style. The size value must include a number and
7
+ * a supported CSS unit.
8
+ *
9
+ * Supported units (matching Wikidot's implementation):
10
+ * `px`, `em`, `rem`, `ex`, `%`, `cm`, `mm`, `in`, `pc`
11
+ *
12
+ * Notably, `pt`, `vh`, `vw`, and other modern CSS units are NOT
13
+ * supported and will cause the parse to fail.
14
+ *
15
+ * Wikidot syntax examples:
16
+ * - `[[size 120%]]larger text[[/size]]`
17
+ * - `[[size 0.8em]]smaller text[[/size]]`
18
+ * - `[[size 24px]]fixed size[[/size]]`
19
+ *
20
+ * Produces a `"container"` AST element with `type: "size"` and a
21
+ * `style` attribute containing the `font-size` declaration.
22
+ *
23
+ * @module
24
+ */
25
+ import type { Element } from "@wdprlib/ast";
26
+ import type { InlineRule, ParseContext, RuleResult } from "../types";
27
+ import { currentToken } from "../types";
28
+ import { parseBlockName } from "../utils";
29
+ import { parseInlineUntil } from "./utils";
30
+
31
+ /**
32
+ * CSS size units that Wikidot recognizes in `[[size]]` blocks.
33
+ * Other valid CSS units (like `pt`, `vh`, `vw`) are deliberately
34
+ * excluded to match original Wikidot behavior.
35
+ */
36
+ const VALID_SIZE_UNITS = ["px", "em", "rem", "ex", "%", "cm", "mm", "in", "pc"];
37
+
38
+ /**
39
+ * Validates a size string against Wikidot-supported CSS units.
40
+ *
41
+ * The value must match the pattern `<number><unit>`, where the number
42
+ * can be an integer or decimal and the unit must be one of the
43
+ * {@link VALID_SIZE_UNITS}.
44
+ *
45
+ * @param size - The size string to validate (e.g. `"120%"`, `"1.5em"`)
46
+ * @returns `true` if the size value is valid
47
+ */
48
+ function isValidSizeValue(size: string): boolean {
49
+ // Match number + unit pattern
50
+ const unitPattern = VALID_SIZE_UNITS.join("|");
51
+ const match = size.match(new RegExp(`^(\\d+(?:\\.\\d+)?)(${unitPattern})$`, "i"));
52
+ return match !== null;
53
+ }
54
+
55
+ /**
56
+ * Extracts and validates a size value from the token stream.
57
+ *
58
+ * Skips leading whitespace, then collects consecutive non-whitespace,
59
+ * non-delimiter tokens and joins them into a size string. The resulting
60
+ * string is validated against {@link isValidSizeValue}.
61
+ *
62
+ * @param ctx - The current parse context
63
+ * @param startPos - Token index at which to begin scanning
64
+ * @returns An object with the validated size string and tokens consumed,
65
+ * or `null` if no valid size value was found
66
+ */
67
+ function parseSizeValue(
68
+ ctx: ParseContext,
69
+ startPos: number,
70
+ ): { size: string; consumed: number } | null {
71
+ let pos = startPos;
72
+ let consumed = 0;
73
+
74
+ // Skip whitespace
75
+ while (ctx.tokens[pos]?.type === "WHITESPACE") {
76
+ pos++;
77
+ consumed++;
78
+ }
79
+
80
+ // Collect size value tokens until ]]
81
+ const parts: string[] = [];
82
+ while (pos < ctx.tokens.length) {
83
+ const token = ctx.tokens[pos];
84
+ if (
85
+ !token ||
86
+ token.type === "BLOCK_CLOSE" ||
87
+ token.type === "NEWLINE" ||
88
+ token.type === "EOF"
89
+ ) {
90
+ break;
91
+ }
92
+ if (token.type === "WHITESPACE") {
93
+ break; // Size value shouldn't have spaces
94
+ }
95
+ parts.push(token.value);
96
+ pos++;
97
+ consumed++;
98
+ }
99
+
100
+ if (parts.length === 0) {
101
+ return null;
102
+ }
103
+
104
+ const size = parts.join("");
105
+
106
+ // Validate against supported units
107
+ if (!isValidSizeValue(size)) {
108
+ return null;
109
+ }
110
+
111
+ return { size, consumed };
112
+ }
113
+
114
+ /**
115
+ * Inline rule for parsing `[[size value]]text[[/size]]`.
116
+ *
117
+ * Triggered by a `BLOCK_OPEN` (`[[`) token. Verifies the block name
118
+ * is `size`, parses and validates the size value, then recursively
119
+ * parses inline content until the matching `[[/size]]` closing tag.
120
+ *
121
+ * Fails if:
122
+ * - The block name is not `size`
123
+ * - The size value is missing or uses an unsupported unit
124
+ * - No closing `]]` after the size value
125
+ */
126
+ export const sizeRule: InlineRule = {
127
+ name: "size",
128
+ startTokens: ["BLOCK_OPEN"],
129
+
130
+ /**
131
+ * Attempts to parse a size block at the current position.
132
+ *
133
+ * @param ctx - Parse context with token stream and current position
134
+ * @returns A successful result with a `"container"` element of type `"size"`,
135
+ * or `{ success: false }`
136
+ */
137
+ parse(ctx: ParseContext): RuleResult<Element> {
138
+ const openToken = currentToken(ctx);
139
+ if (openToken.type !== "BLOCK_OPEN") {
140
+ return { success: false };
141
+ }
142
+
143
+ let pos = ctx.pos + 1;
144
+ let consumed = 1;
145
+
146
+ // Parse block name
147
+ const nameResult = parseBlockName(ctx, pos);
148
+ if (!nameResult) {
149
+ return { success: false };
150
+ }
151
+
152
+ const blockName = nameResult.name;
153
+ if (blockName !== "size") {
154
+ return { success: false };
155
+ }
156
+
157
+ pos += nameResult.consumed;
158
+ consumed += nameResult.consumed;
159
+
160
+ // Parse size value
161
+ const sizeResult = parseSizeValue(ctx, pos);
162
+ if (!sizeResult) {
163
+ return { success: false };
164
+ }
165
+
166
+ pos += sizeResult.consumed;
167
+ consumed += sizeResult.consumed;
168
+
169
+ // Expect ]]
170
+ if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
171
+ return { success: false };
172
+ }
173
+ pos++;
174
+ consumed++;
175
+
176
+ // Parse inline content until [[/size]]
177
+ const children: Element[] = [];
178
+ let foundClose = false;
179
+
180
+ while (pos < ctx.tokens.length) {
181
+ const token = ctx.tokens[pos];
182
+ if (!token || token.type === "EOF") {
183
+ break;
184
+ }
185
+
186
+ // Check for [[/size]]
187
+ if (token.type === "BLOCK_END_OPEN") {
188
+ const closeNameResult = parseBlockName(ctx, pos + 1);
189
+ if (closeNameResult && closeNameResult.name === "size") {
190
+ foundClose = true;
191
+ // Skip [[/size]]
192
+ pos++; // [[/
193
+ consumed++;
194
+ pos += closeNameResult.consumed; // size
195
+ consumed += closeNameResult.consumed;
196
+ // Skip ]]
197
+ if (ctx.tokens[pos]?.type === "BLOCK_CLOSE") {
198
+ pos++;
199
+ consumed++;
200
+ }
201
+ break;
202
+ }
203
+ }
204
+
205
+ // Parse inline content
206
+ const inlineCtx: ParseContext = { ...ctx, pos };
207
+ const inlineResult = parseInlineUntil(inlineCtx, "BLOCK_END_OPEN");
208
+ if (inlineResult.elements.length > 0) {
209
+ children.push(...inlineResult.elements);
210
+ pos += inlineResult.consumed;
211
+ consumed += inlineResult.consumed;
212
+ } else {
213
+ // Fallback: just add as text
214
+ children.push({ element: "text", data: token.value });
215
+ pos++;
216
+ consumed++;
217
+ }
218
+ }
219
+
220
+ if (!foundClose) {
221
+ ctx.diagnostics.push({
222
+ severity: "warning",
223
+ code: "unclosed-block",
224
+ message: "Missing closing tag [[/size]] for [[size]]",
225
+ position: openToken.position,
226
+ });
227
+ }
228
+
229
+ return {
230
+ success: true,
231
+ elements: [
232
+ {
233
+ element: "container",
234
+ data: {
235
+ type: "size",
236
+ attributes: { style: `font-size:${sizeResult.size};` },
237
+ elements: children,
238
+ },
239
+ },
240
+ ],
241
+ consumed,
242
+ };
243
+ },
244
+ };