@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,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Parses the Wikidot footnote syntax: `[[footnote]]content[[/footnote]]`.
|
|
4
|
+
*
|
|
5
|
+
* Footnotes work in two parts: the inline `[[footnote]]` block produces
|
|
6
|
+
* a numbered superscript reference marker at the point of use, while the
|
|
7
|
+
* actual footnote content is collected separately and rendered by a
|
|
8
|
+
* `[[footnoteblock]]` element (typically at the bottom of the page).
|
|
9
|
+
*
|
|
10
|
+
* Footnote content supports multiple paragraphs:
|
|
11
|
+
* - The first paragraph is rendered as inline content (no wrapping `<p>` tag)
|
|
12
|
+
* - Subsequent paragraphs (separated by blank lines) are each wrapped
|
|
13
|
+
* in `<p>` tags, matching Wikidot's rendering behavior
|
|
14
|
+
* - Single newlines within a paragraph become `<br />` elements
|
|
15
|
+
*
|
|
16
|
+
* The parsed footnote content is pushed into `ctx.footnotes` (an array
|
|
17
|
+
* of Element arrays) so the renderer can later assign sequential numbers
|
|
18
|
+
* and generate the footnote block.
|
|
19
|
+
*
|
|
20
|
+
* Produces a simple `"footnote"` AST element (a marker with no data)
|
|
21
|
+
* at the inline reference point.
|
|
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
|
+
* Inline rule for parsing `[[footnote]]content[[/footnote]]`.
|
|
33
|
+
*
|
|
34
|
+
* Triggered by a `BLOCK_OPEN` (`[[`) token. Verifies the block name
|
|
35
|
+
* is `footnote`, then parses multiline inline content until the matching
|
|
36
|
+
* `[[/footnote]]` closing tag is found.
|
|
37
|
+
*
|
|
38
|
+
* Side effect: appends the parsed footnote content to `ctx.footnotes`.
|
|
39
|
+
*/
|
|
40
|
+
export const footnoteRule: InlineRule = {
|
|
41
|
+
name: "footnote",
|
|
42
|
+
startTokens: ["BLOCK_OPEN"],
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Attempts to parse a footnote block at the current position.
|
|
46
|
+
*
|
|
47
|
+
* @param ctx - Parse context with token stream and current position
|
|
48
|
+
* @returns A successful result with a `"footnote"` marker element,
|
|
49
|
+
* or `{ success: false }` if this is not a valid footnote
|
|
50
|
+
*/
|
|
51
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
52
|
+
const openToken = currentToken(ctx);
|
|
53
|
+
if (openToken.type !== "BLOCK_OPEN") {
|
|
54
|
+
return { success: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let pos = ctx.pos + 1;
|
|
58
|
+
let consumed = 1;
|
|
59
|
+
|
|
60
|
+
// Parse block name
|
|
61
|
+
const nameResult = parseBlockName(ctx, pos);
|
|
62
|
+
if (!nameResult) {
|
|
63
|
+
return { success: false };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const blockName = nameResult.name;
|
|
67
|
+
if (blockName !== "footnote") {
|
|
68
|
+
return { success: false };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pos += nameResult.consumed;
|
|
72
|
+
consumed += nameResult.consumed;
|
|
73
|
+
|
|
74
|
+
// Expect ]]
|
|
75
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
76
|
+
pos++;
|
|
77
|
+
consumed++;
|
|
78
|
+
}
|
|
79
|
+
if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
|
|
80
|
+
return { success: false };
|
|
81
|
+
}
|
|
82
|
+
pos++;
|
|
83
|
+
consumed++;
|
|
84
|
+
|
|
85
|
+
// Parse content until [[/footnote]]
|
|
86
|
+
// Wikidot footnote behavior:
|
|
87
|
+
// - First paragraph: inline content (no <p> tag)
|
|
88
|
+
// - After blank line: content wrapped in <p> tag
|
|
89
|
+
const paragraphs: Element[][] = [[]];
|
|
90
|
+
let currentParagraph = 0;
|
|
91
|
+
let foundClose = false;
|
|
92
|
+
|
|
93
|
+
while (pos < ctx.tokens.length) {
|
|
94
|
+
const token = ctx.tokens[pos];
|
|
95
|
+
if (!token || token.type === "EOF") {
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check for [[/footnote]]
|
|
100
|
+
if (token.type === "BLOCK_END_OPEN") {
|
|
101
|
+
const closeNameResult = parseBlockName(ctx, pos + 1);
|
|
102
|
+
if (closeNameResult && closeNameResult.name === "footnote") {
|
|
103
|
+
foundClose = true;
|
|
104
|
+
// Skip [[/footnote]]
|
|
105
|
+
pos++; // [[/
|
|
106
|
+
consumed++;
|
|
107
|
+
pos += closeNameResult.consumed; // footnote
|
|
108
|
+
consumed += closeNameResult.consumed;
|
|
109
|
+
// Skip whitespace
|
|
110
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
111
|
+
pos++;
|
|
112
|
+
consumed++;
|
|
113
|
+
}
|
|
114
|
+
// Skip ]]
|
|
115
|
+
if (ctx.tokens[pos]?.type === "BLOCK_CLOSE") {
|
|
116
|
+
pos++;
|
|
117
|
+
consumed++;
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Handle NEWLINE - check if it's a paragraph break (blank line)
|
|
124
|
+
if (token.type === "NEWLINE") {
|
|
125
|
+
pos++;
|
|
126
|
+
consumed++;
|
|
127
|
+
// Peek ahead: skip whitespace to check for blank line (e.g. "\n \n")
|
|
128
|
+
let peekPos = pos;
|
|
129
|
+
let peekConsumed = 0;
|
|
130
|
+
while (ctx.tokens[peekPos]?.type === "WHITESPACE") {
|
|
131
|
+
peekPos++;
|
|
132
|
+
peekConsumed++;
|
|
133
|
+
}
|
|
134
|
+
// Look ahead for another NEWLINE (blank line = paragraph break)
|
|
135
|
+
if (ctx.tokens[peekPos]?.type === "NEWLINE") {
|
|
136
|
+
// Commit the whitespace skip
|
|
137
|
+
pos = peekPos;
|
|
138
|
+
consumed += peekConsumed;
|
|
139
|
+
// Skip all consecutive newlines
|
|
140
|
+
while (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
141
|
+
pos++;
|
|
142
|
+
consumed++;
|
|
143
|
+
}
|
|
144
|
+
// Start new paragraph
|
|
145
|
+
currentParagraph++;
|
|
146
|
+
paragraphs[currentParagraph] = [];
|
|
147
|
+
} else {
|
|
148
|
+
// Single newline - just continue (becomes space or line-break)
|
|
149
|
+
// For Wikidot compatibility, single newlines in footnotes become <br />
|
|
150
|
+
paragraphs[currentParagraph]!.push({ element: "line-break" });
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Parse inline content (including newlines for multiline footnotes)
|
|
156
|
+
const inlineCtx: ParseContext = { ...ctx, pos };
|
|
157
|
+
const inlineResult = parseInlineUntil(inlineCtx, "BLOCK_END_OPEN");
|
|
158
|
+
if (inlineResult.elements.length > 0) {
|
|
159
|
+
paragraphs[currentParagraph]!.push(...inlineResult.elements);
|
|
160
|
+
pos += inlineResult.consumed;
|
|
161
|
+
consumed += inlineResult.consumed;
|
|
162
|
+
} else {
|
|
163
|
+
// Fallback: just add as text
|
|
164
|
+
paragraphs[currentParagraph]!.push({ element: "text", data: token.value });
|
|
165
|
+
pos++;
|
|
166
|
+
consumed++;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Build children: first paragraph inline, subsequent paragraphs wrapped in <p>
|
|
171
|
+
const children: Element[] = [];
|
|
172
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
173
|
+
const para = paragraphs[i]!;
|
|
174
|
+
if (para.length === 0) continue;
|
|
175
|
+
// Remove leading/trailing line-breaks
|
|
176
|
+
while (para.length > 0 && para[0]?.element === "line-break") {
|
|
177
|
+
para.shift();
|
|
178
|
+
}
|
|
179
|
+
while (para.length > 0 && para[para.length - 1]?.element === "line-break") {
|
|
180
|
+
para.pop();
|
|
181
|
+
}
|
|
182
|
+
if (para.length === 0) continue;
|
|
183
|
+
|
|
184
|
+
if (i === 0) {
|
|
185
|
+
// First paragraph: inline
|
|
186
|
+
children.push(...para);
|
|
187
|
+
} else {
|
|
188
|
+
// Subsequent paragraphs: wrapped in <p>
|
|
189
|
+
children.push({
|
|
190
|
+
element: "container",
|
|
191
|
+
data: {
|
|
192
|
+
type: "paragraph",
|
|
193
|
+
attributes: {},
|
|
194
|
+
elements: para,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!foundClose) {
|
|
201
|
+
ctx.diagnostics.push({
|
|
202
|
+
severity: "warning",
|
|
203
|
+
code: "unclosed-block",
|
|
204
|
+
message: "Missing closing tag [[/footnote]] for [[footnote]]",
|
|
205
|
+
position: openToken.position,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Store footnote content in context
|
|
210
|
+
ctx.footnotes.push(children);
|
|
211
|
+
|
|
212
|
+
// Return simple footnote marker
|
|
213
|
+
return {
|
|
214
|
+
success: true,
|
|
215
|
+
elements: [
|
|
216
|
+
{
|
|
217
|
+
element: "footnote",
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
consumed,
|
|
221
|
+
};
|
|
222
|
+
},
|
|
223
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Parses Wikidot's guillemet (angle quotation mark) syntax.
|
|
4
|
+
*
|
|
5
|
+
* Converts ASCII double-angle-bracket sequences into their Unicode
|
|
6
|
+
* typographic equivalents:
|
|
7
|
+
* - `<<` becomes `\u00AB` (LEFT-POINTING DOUBLE ANGLE QUOTATION MARK)
|
|
8
|
+
* - `>>` becomes `\u00BB` (RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK)
|
|
9
|
+
*
|
|
10
|
+
* These typographic characters are commonly used in European languages
|
|
11
|
+
* (particularly French and Russian) as quotation marks. Wikidot provides
|
|
12
|
+
* this shorthand so authors do not need to type the Unicode characters
|
|
13
|
+
* directly.
|
|
14
|
+
*
|
|
15
|
+
* Produces a `"text"` AST element containing the Unicode character.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
import type { Element } from "@wdprlib/ast";
|
|
20
|
+
import type { InlineRule, ParseContext, RuleResult } from "../types";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Inline rule for converting `<<` and `>>` to typographic guillemets.
|
|
24
|
+
*
|
|
25
|
+
* Triggered by `LEFT_DOUBLE_ANGLE` or `RIGHT_DOUBLE_ANGLE` tokens.
|
|
26
|
+
* This is a simple one-to-one token replacement with no content
|
|
27
|
+
* parsing or nesting.
|
|
28
|
+
*/
|
|
29
|
+
export const guillemetRule: InlineRule = {
|
|
30
|
+
name: "guillemet",
|
|
31
|
+
startTokens: ["LEFT_DOUBLE_ANGLE", "RIGHT_DOUBLE_ANGLE"],
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Converts a double-angle-bracket token to its Unicode guillemet equivalent.
|
|
35
|
+
*
|
|
36
|
+
* @param ctx - Parse context with token stream and current position
|
|
37
|
+
* @returns A successful result with a `"text"` element containing the
|
|
38
|
+
* Unicode guillemet character, or `{ success: false }` if the
|
|
39
|
+
* token is neither `<<` nor `>>`
|
|
40
|
+
*/
|
|
41
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
42
|
+
const token = ctx.tokens[ctx.pos];
|
|
43
|
+
|
|
44
|
+
// << → «
|
|
45
|
+
if (token?.type === "LEFT_DOUBLE_ANGLE") {
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
elements: [{ element: "text", data: "\u00AB" }],
|
|
49
|
+
consumed: 1,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// >> → »
|
|
54
|
+
if (token?.type === "RIGHT_DOUBLE_ANGLE") {
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
elements: [{ element: "text", data: "\u00BB" }],
|
|
58
|
+
consumed: 1,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { success: false };
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Inline-position gate for `[[html]]...[[/html]]` when the parser is
|
|
4
|
+
* configured with `allowHtmlBlocks: false`.
|
|
5
|
+
*
|
|
6
|
+
* The block-level {@link htmlBlockRule} already removes `[[html]]` blocks
|
|
7
|
+
* that sit at the start of a line, but the block dispatcher never
|
|
8
|
+
* reaches a `[[html]]` that appears mid-paragraph. Without this inline
|
|
9
|
+
* rule, the body of a disabled-but-inline-positioned `[[html]]` would
|
|
10
|
+
* end up parsed as paragraph text and leak into the output as escaped
|
|
11
|
+
* HTML.
|
|
12
|
+
*
|
|
13
|
+
* When enabled (`allowHtmlBlocks !== false`), the rule does nothing
|
|
14
|
+
* (returns `success: false`) so the existing paragraph behaviour is
|
|
15
|
+
* preserved: a stray inline `[[html]]` renders as text. The block-level
|
|
16
|
+
* rule handles the proper case where `[[html]]` is on its own line.
|
|
17
|
+
*
|
|
18
|
+
* When disabled (`allowHtmlBlocks === false`):
|
|
19
|
+
* - A well-formed `[[html ...]]...[[/html]]` is fully consumed and
|
|
20
|
+
* produces no AST element, emitting an `html-block-disabled` info
|
|
21
|
+
* diagnostic.
|
|
22
|
+
* - An unclosed `[[html ...]]` is consumed to the end of the token
|
|
23
|
+
* stream so the body cannot leak as inline text, emitting both
|
|
24
|
+
* `unclosed-block` (warning) and `html-block-disabled` (info).
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { Element } from "@wdprlib/ast";
|
|
30
|
+
import type { InlineRule, ParseContext, RuleResult } from "../types";
|
|
31
|
+
import { currentToken } from "../types";
|
|
32
|
+
import { parseBlockName, parseAttributesRaw } from "../block/utils";
|
|
33
|
+
import { lookaheadHasHtmlClose } from "../block/html";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Inline rule that gates `[[html]]` when the setting disallows it.
|
|
37
|
+
*/
|
|
38
|
+
export const htmlInlineRule: InlineRule = {
|
|
39
|
+
name: "html",
|
|
40
|
+
startTokens: ["BLOCK_OPEN"],
|
|
41
|
+
|
|
42
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
43
|
+
const openToken = currentToken(ctx);
|
|
44
|
+
if (openToken.type !== "BLOCK_OPEN") {
|
|
45
|
+
return { success: false };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let pos = ctx.pos + 1;
|
|
49
|
+
let consumed = 1;
|
|
50
|
+
|
|
51
|
+
const nameResult = parseBlockName(ctx, pos);
|
|
52
|
+
if (!nameResult || nameResult.name.toLowerCase() !== "html") {
|
|
53
|
+
return { success: false };
|
|
54
|
+
}
|
|
55
|
+
pos += nameResult.consumed;
|
|
56
|
+
consumed += nameResult.consumed;
|
|
57
|
+
|
|
58
|
+
const attrResult = parseAttributesRaw(ctx, pos);
|
|
59
|
+
pos += attrResult.consumed;
|
|
60
|
+
consumed += attrResult.consumed;
|
|
61
|
+
|
|
62
|
+
if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
|
|
63
|
+
return { success: false };
|
|
64
|
+
}
|
|
65
|
+
pos++;
|
|
66
|
+
consumed++;
|
|
67
|
+
|
|
68
|
+
// Enabled: leave inline `[[html]]` alone — it falls through to text
|
|
69
|
+
// rendering, matching the historical behaviour for stray block-named
|
|
70
|
+
// openers used inline.
|
|
71
|
+
if (ctx.settings.allowHtmlBlocks !== false) {
|
|
72
|
+
return { success: false };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Disabled path: consume the body until a real `[[/html]]` (BLOCK_END_OPEN
|
|
76
|
+
// + name + BLOCK_CLOSE, allowing whitespace inside the close tag).
|
|
77
|
+
// Only allow the blank-line stop when no real close exists ahead, so
|
|
78
|
+
// a closed body that spans paragraphs is still consumed correctly.
|
|
79
|
+
const hasCloseAhead = lookaheadHasHtmlClose(ctx, pos);
|
|
80
|
+
let foundClose = false;
|
|
81
|
+
while (pos < ctx.tokens.length) {
|
|
82
|
+
const token = ctx.tokens[pos];
|
|
83
|
+
if (!token || token.type === "EOF") break;
|
|
84
|
+
|
|
85
|
+
// Stop at a blank line so an unclosed inline `[[html]]` does not
|
|
86
|
+
// swallow subsequent paragraphs.
|
|
87
|
+
if (!hasCloseAhead && token.type === "NEWLINE" && ctx.tokens[pos + 1]?.type === "NEWLINE") {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (token.type === "BLOCK_END_OPEN") {
|
|
92
|
+
const closeNameResult = parseBlockName(ctx, pos + 1);
|
|
93
|
+
if (closeNameResult?.name.toLowerCase() === "html") {
|
|
94
|
+
let checkPos = pos + 1 + closeNameResult.consumed;
|
|
95
|
+
while (ctx.tokens[checkPos]?.type === "WHITESPACE") checkPos++;
|
|
96
|
+
if (ctx.tokens[checkPos]?.type === "BLOCK_CLOSE") {
|
|
97
|
+
foundClose = true;
|
|
98
|
+
// Consume `[[/html]]` (and optional trailing newline) too.
|
|
99
|
+
consumed += checkPos - pos + 1;
|
|
100
|
+
pos = checkPos + 1;
|
|
101
|
+
if (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
102
|
+
pos++;
|
|
103
|
+
consumed++;
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
pos++;
|
|
111
|
+
consumed++;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!foundClose) {
|
|
115
|
+
ctx.diagnostics.push({
|
|
116
|
+
severity: "warning",
|
|
117
|
+
code: "unclosed-block",
|
|
118
|
+
message: "Missing closing tag [[/html]] for [[html]]",
|
|
119
|
+
position: openToken.position,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
ctx.diagnostics.push({
|
|
124
|
+
severity: "info",
|
|
125
|
+
code: "html-block-disabled",
|
|
126
|
+
message: "[[html]] block ignored: disabled by settings",
|
|
127
|
+
position: openToken.position,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return { success: true, elements: [], consumed };
|
|
131
|
+
},
|
|
132
|
+
};
|