@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,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Block rule for Wikidot embed blocks: `[[embed]]`, `[[embedvideo]]`,
|
|
4
|
+
* and `[[embedaudio]]` (each with a matching closing tag).
|
|
5
|
+
*
|
|
6
|
+
* In original Wikidot, only HTML that matches a server-side allow-list is
|
|
7
|
+
* rendered. This parser does not perform that filtering -- the raw content
|
|
8
|
+
* between the tags is stored verbatim as an `embed-block` element. Validation
|
|
9
|
+
* and sanitisation are expected to happen at rendering time or on the server.
|
|
10
|
+
*
|
|
11
|
+
* The embed block is wrapped in a paragraph container in the AST, matching
|
|
12
|
+
* Wikidot's rendering behaviour where embeds sit inside `<p>` tags.
|
|
13
|
+
*
|
|
14
|
+
* If no closing tag is found, the rule fails to prevent consuming the rest
|
|
15
|
+
* of the document.
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
import type { Element } from "@wdprlib/ast";
|
|
20
|
+
import type { BlockRule, ParseContext, RuleResult } from "../types";
|
|
21
|
+
import { currentToken } from "../types";
|
|
22
|
+
import { parseBlockName } from "./utils";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Block rule for `[[embed]]`, `[[embedvideo]]`, and `[[embedaudio]]`.
|
|
26
|
+
*
|
|
27
|
+
* Content between the opening and closing tags is captured as raw text.
|
|
28
|
+
* The block name matching is case-insensitive; the closing tag may use
|
|
29
|
+
* any of the three names (`embed`, `embedvideo`, `embedaudio`) regardless
|
|
30
|
+
* of which was used to open.
|
|
31
|
+
*/
|
|
32
|
+
export const embedBlockRule: BlockRule = {
|
|
33
|
+
name: "embed-block",
|
|
34
|
+
startTokens: ["BLOCK_OPEN"],
|
|
35
|
+
requiresLineStart: false,
|
|
36
|
+
|
|
37
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
38
|
+
const openToken = currentToken(ctx);
|
|
39
|
+
if (openToken.type !== "BLOCK_OPEN") {
|
|
40
|
+
return { success: false };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let pos = ctx.pos + 1;
|
|
44
|
+
let consumed = 1;
|
|
45
|
+
|
|
46
|
+
// Parse block name
|
|
47
|
+
const nameResult = parseBlockName(ctx, pos);
|
|
48
|
+
if (!nameResult) {
|
|
49
|
+
return { success: false };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const blockName = nameResult.name.toLowerCase();
|
|
53
|
+
if (blockName !== "embed" && blockName !== "embedvideo" && blockName !== "embedaudio") {
|
|
54
|
+
return { success: false };
|
|
55
|
+
}
|
|
56
|
+
pos += nameResult.consumed;
|
|
57
|
+
consumed += nameResult.consumed;
|
|
58
|
+
|
|
59
|
+
// Skip whitespace
|
|
60
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
61
|
+
pos++;
|
|
62
|
+
consumed++;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Expect ]]
|
|
66
|
+
if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
|
|
67
|
+
return { success: false };
|
|
68
|
+
}
|
|
69
|
+
pos++;
|
|
70
|
+
consumed++;
|
|
71
|
+
|
|
72
|
+
// Collect content until [[/embed]], [[/embedvideo]], or [[/embedaudio]]
|
|
73
|
+
let contents = "";
|
|
74
|
+
let foundClose = false;
|
|
75
|
+
|
|
76
|
+
while (pos < ctx.tokens.length) {
|
|
77
|
+
const token = ctx.tokens[pos];
|
|
78
|
+
if (!token) break;
|
|
79
|
+
|
|
80
|
+
// Check for closing [[/embed*]]
|
|
81
|
+
if (token.type === "BLOCK_END_OPEN") {
|
|
82
|
+
const closeNameResult = parseBlockName(ctx, pos + 1);
|
|
83
|
+
if (closeNameResult) {
|
|
84
|
+
const closeName = closeNameResult.name.toLowerCase();
|
|
85
|
+
if (closeName === "embed" || closeName === "embedvideo" || closeName === "embedaudio") {
|
|
86
|
+
foundClose = true;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
contents += token.value;
|
|
93
|
+
pos++;
|
|
94
|
+
consumed++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Require closing tag - without it, fail to prevent consuming entire document
|
|
98
|
+
if (!foundClose) {
|
|
99
|
+
ctx.diagnostics.push({
|
|
100
|
+
severity: "warning",
|
|
101
|
+
code: "unclosed-block",
|
|
102
|
+
message: `Missing closing tag [[/${blockName}]] for [[${blockName}]]`,
|
|
103
|
+
position: openToken.position,
|
|
104
|
+
});
|
|
105
|
+
return { success: false };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Consume [[/embed*]]
|
|
109
|
+
if (ctx.tokens[pos]?.type === "BLOCK_END_OPEN") {
|
|
110
|
+
pos++;
|
|
111
|
+
consumed++;
|
|
112
|
+
const closeNameResult = parseBlockName(ctx, pos);
|
|
113
|
+
if (closeNameResult) {
|
|
114
|
+
pos += closeNameResult.consumed;
|
|
115
|
+
consumed += closeNameResult.consumed;
|
|
116
|
+
}
|
|
117
|
+
if (ctx.tokens[pos]?.type === "BLOCK_CLOSE") {
|
|
118
|
+
pos++;
|
|
119
|
+
consumed++;
|
|
120
|
+
}
|
|
121
|
+
if (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
122
|
+
pos++;
|
|
123
|
+
consumed++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Trim the contents
|
|
128
|
+
contents = contents.trim();
|
|
129
|
+
|
|
130
|
+
// Wikidotと同じく、embed-blockをparagraphで囲む
|
|
131
|
+
return {
|
|
132
|
+
success: true,
|
|
133
|
+
elements: [
|
|
134
|
+
{
|
|
135
|
+
element: "container",
|
|
136
|
+
data: {
|
|
137
|
+
type: "paragraph",
|
|
138
|
+
attributes: {},
|
|
139
|
+
elements: [
|
|
140
|
+
{
|
|
141
|
+
element: "embed-block",
|
|
142
|
+
data: {
|
|
143
|
+
contents,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
consumed,
|
|
151
|
+
};
|
|
152
|
+
},
|
|
153
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Block rule for the Wikidot footnote block: `[[footnoteblock]]`.
|
|
4
|
+
*
|
|
5
|
+
* This self-closing block tag marks the location in the page where all
|
|
6
|
+
* collected footnotes (from `[[footnote]]...[[/footnote]]` inline markers)
|
|
7
|
+
* should be rendered. It is analogous to a "footnotes section" placeholder.
|
|
8
|
+
*
|
|
9
|
+
* Optional attributes:
|
|
10
|
+
* - `title` -- custom heading text for the footnotes section.
|
|
11
|
+
* - `hide` -- when `"true"` or `"yes"`, suppresses footnote rendering.
|
|
12
|
+
*
|
|
13
|
+
* Wikidot only honours the FIRST `[[footnoteblock]]` in a document;
|
|
14
|
+
* subsequent occurrences are treated as plain text. The parser tracks
|
|
15
|
+
* this via `ctx.scope.footnoteBlockParsed`, but note: that flag is per
|
|
16
|
+
* spread copy of `ParseContext` (see {@link ScopeContext.footnoteBlockParsed}).
|
|
17
|
+
* In practice the duplicate-rejection only fires for two top-level
|
|
18
|
+
* `[[footnoteblock]]` tokens; siblings inside the same body or across
|
|
19
|
+
* nested bodies both succeed today. The auto-append decision in
|
|
20
|
+
* `Parser.parse` uses a post-parse AST walk, so it is unaffected.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import type { Element } from "@wdprlib/ast";
|
|
25
|
+
import type { BlockRule, ParseContext, RuleResult } from "../types";
|
|
26
|
+
import { currentToken } from "../types";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parses key/value attributes from tokens (e.g. `title="Custom title"`).
|
|
30
|
+
*
|
|
31
|
+
* This is a local attribute parser specific to the footnoteblock rule.
|
|
32
|
+
* It handles TEXT or IDENTIFIER names, optional `=` with quoted or
|
|
33
|
+
* unquoted values, and boolean attributes (name without value).
|
|
34
|
+
*
|
|
35
|
+
* @param ctx - Parse context.
|
|
36
|
+
* @param startPos - Token index to start scanning.
|
|
37
|
+
* @returns Parsed attribute map and the number of tokens consumed.
|
|
38
|
+
*/
|
|
39
|
+
function parseAttributes(
|
|
40
|
+
ctx: ParseContext,
|
|
41
|
+
startPos: number,
|
|
42
|
+
): { attrs: Record<string, string>; consumed: number } {
|
|
43
|
+
const attrs: Record<string, string> = {};
|
|
44
|
+
let pos = startPos;
|
|
45
|
+
let consumed = 0;
|
|
46
|
+
|
|
47
|
+
while (pos < ctx.tokens.length) {
|
|
48
|
+
const token = ctx.tokens[pos];
|
|
49
|
+
if (
|
|
50
|
+
!token ||
|
|
51
|
+
token.type === "BLOCK_CLOSE" ||
|
|
52
|
+
token.type === "NEWLINE" ||
|
|
53
|
+
token.type === "EOF"
|
|
54
|
+
) {
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Skip whitespace
|
|
59
|
+
if (token.type === "WHITESPACE") {
|
|
60
|
+
pos++;
|
|
61
|
+
consumed++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Attribute name (TEXT or IDENTIFIER token)
|
|
66
|
+
if (token.type === "TEXT" || token.type === "IDENTIFIER") {
|
|
67
|
+
const name = token.value.toLowerCase();
|
|
68
|
+
pos++;
|
|
69
|
+
consumed++;
|
|
70
|
+
|
|
71
|
+
// Check for =
|
|
72
|
+
const eqToken = ctx.tokens[pos];
|
|
73
|
+
if (eqToken?.type === "EQUALS") {
|
|
74
|
+
pos++;
|
|
75
|
+
consumed++;
|
|
76
|
+
|
|
77
|
+
// Get value (quoted string or text)
|
|
78
|
+
const valueToken = ctx.tokens[pos];
|
|
79
|
+
if (valueToken?.type === "QUOTED_STRING") {
|
|
80
|
+
// Remove quotes
|
|
81
|
+
let value = valueToken.value;
|
|
82
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
83
|
+
value = value.slice(1, -1);
|
|
84
|
+
}
|
|
85
|
+
attrs[name] = value;
|
|
86
|
+
pos++;
|
|
87
|
+
consumed++;
|
|
88
|
+
} else if (valueToken?.type === "TEXT" || valueToken?.type === "IDENTIFIER") {
|
|
89
|
+
attrs[name] = valueToken.value;
|
|
90
|
+
pos++;
|
|
91
|
+
consumed++;
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
// Boolean attribute
|
|
95
|
+
attrs[name] = "true";
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
// Unknown token, skip
|
|
99
|
+
pos++;
|
|
100
|
+
consumed++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { attrs, consumed };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Block rule for `[[footnoteblock]]`.
|
|
109
|
+
*
|
|
110
|
+
* Parsing strategy:
|
|
111
|
+
* 1. Match BLOCK_OPEN + name "footnoteblock" (case-insensitive).
|
|
112
|
+
* 2. Parse optional attributes (`title`, `hide`).
|
|
113
|
+
* 3. Consume closing `]]`.
|
|
114
|
+
* 4. If `ctx.scope.footnoteBlockParsed` is already `true` on the current
|
|
115
|
+
* `ParseContext` copy, fail. Because `parseBlocksUntil` spreads a
|
|
116
|
+
* fresh `ctx` per sibling rule, this only rejects a second
|
|
117
|
+
* `[[footnoteblock]]` that arrives via the parser's top-level
|
|
118
|
+
* dispatch loop in practice. See {@link ScopeContext.footnoteBlockParsed}.
|
|
119
|
+
* 5. Replace `ctx.scope` with the flag set to `true` and emit a `footnote-block`
|
|
120
|
+
* element.
|
|
121
|
+
*/
|
|
122
|
+
export const footnoteBlockRule: BlockRule = {
|
|
123
|
+
name: "footnoteBlock",
|
|
124
|
+
startTokens: ["BLOCK_OPEN"],
|
|
125
|
+
requiresLineStart: true,
|
|
126
|
+
|
|
127
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
128
|
+
const openToken = currentToken(ctx);
|
|
129
|
+
if (openToken.type !== "BLOCK_OPEN") {
|
|
130
|
+
return { success: false };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let pos = ctx.pos + 1;
|
|
134
|
+
let consumed = 1;
|
|
135
|
+
|
|
136
|
+
// Skip whitespace
|
|
137
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
138
|
+
pos++;
|
|
139
|
+
consumed++;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for block name
|
|
143
|
+
const nameToken = ctx.tokens[pos];
|
|
144
|
+
if (!nameToken || (nameToken.type !== "TEXT" && nameToken.type !== "IDENTIFIER")) {
|
|
145
|
+
return { success: false };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const blockName = nameToken.value.toLowerCase();
|
|
149
|
+
if (blockName !== "footnoteblock") {
|
|
150
|
+
return { success: false };
|
|
151
|
+
}
|
|
152
|
+
pos++;
|
|
153
|
+
consumed++;
|
|
154
|
+
|
|
155
|
+
// Parse optional attributes (title="...")
|
|
156
|
+
const { attrs, consumed: attrConsumed } = parseAttributes(ctx, pos);
|
|
157
|
+
pos += attrConsumed;
|
|
158
|
+
consumed += attrConsumed;
|
|
159
|
+
|
|
160
|
+
// Expect ]]
|
|
161
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
162
|
+
pos++;
|
|
163
|
+
consumed++;
|
|
164
|
+
}
|
|
165
|
+
if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
|
|
166
|
+
return { success: false };
|
|
167
|
+
}
|
|
168
|
+
pos++;
|
|
169
|
+
consumed++;
|
|
170
|
+
|
|
171
|
+
// Reject a second `[[footnoteblock]]` that arrives on the same
|
|
172
|
+
// `ParseContext` copy (in practice: at the top level — siblings
|
|
173
|
+
// inside `parseBlocksUntil` each get a fresh spread). The flag
|
|
174
|
+
// lives in the immutable `scope` group, so the mutation is
|
|
175
|
+
// expressed as a scope replacement. The cross-scope duplicate-
|
|
176
|
+
// rejection is a separate, known limitation.
|
|
177
|
+
if (ctx.scope.footnoteBlockParsed) {
|
|
178
|
+
return { success: false };
|
|
179
|
+
}
|
|
180
|
+
ctx.scope = { ...ctx.scope, footnoteBlockParsed: true };
|
|
181
|
+
|
|
182
|
+
// Extract title and hide from attributes
|
|
183
|
+
const title = attrs.title !== undefined ? attrs.title : null;
|
|
184
|
+
const hide = attrs.hide === "true" || attrs.hide === "yes";
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
success: true,
|
|
188
|
+
elements: [
|
|
189
|
+
{
|
|
190
|
+
element: "footnote-block",
|
|
191
|
+
data: {
|
|
192
|
+
title,
|
|
193
|
+
hide,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
consumed,
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Block rule for Wikidot headings: `+ Heading` through `++++++ Heading`.
|
|
4
|
+
*
|
|
5
|
+
* Headings are written with one to six `+` characters at the start of a
|
|
6
|
+
* line, followed by mandatory whitespace and then inline content:
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* + H1 heading
|
|
10
|
+
* ++ H2 heading
|
|
11
|
+
* +++ H3 heading
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* An optional `*` immediately after the `+` markers hides the heading
|
|
15
|
+
* from the table of contents:
|
|
16
|
+
*
|
|
17
|
+
* ```
|
|
18
|
+
* +* Hidden H1
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* Seven or more `+` characters are NOT valid headings in Wikidot and
|
|
22
|
+
* the rule will fail, letting them fall through to paragraph parsing.
|
|
23
|
+
*
|
|
24
|
+
* Non-hidden headings are registered in `ctx.tocEntries` for later use
|
|
25
|
+
* by the table-of-contents module.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
import type { Element, HeadingLevel } from "@wdprlib/ast";
|
|
30
|
+
import type { BlockRule, ParseContext, RuleResult } from "../types";
|
|
31
|
+
import { currentToken } from "../types";
|
|
32
|
+
import { parseInlineUntil } from "../inline/utils";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Block rule for Wikidot headings (`+ ` through `++++++ `).
|
|
36
|
+
*
|
|
37
|
+
* Produces a container element with `type: { header: { level, "has-toc" } }`.
|
|
38
|
+
* The heading text is parsed for inline markup (bold, links, etc.).
|
|
39
|
+
*/
|
|
40
|
+
export const headingRule: BlockRule = {
|
|
41
|
+
name: "heading",
|
|
42
|
+
startTokens: ["HEADING_MARKER"],
|
|
43
|
+
requiresLineStart: true,
|
|
44
|
+
|
|
45
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
46
|
+
const marker = currentToken(ctx);
|
|
47
|
+
|
|
48
|
+
if (!marker.lineStart) {
|
|
49
|
+
return { success: false };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Wikidot requires whitespace after heading marker
|
|
53
|
+
// Check format: + (space)content OR +* (space)content
|
|
54
|
+
let pos = ctx.pos + 1;
|
|
55
|
+
let consumed = 1;
|
|
56
|
+
let hidden = false;
|
|
57
|
+
|
|
58
|
+
// Check for hidden marker (*) - must come immediately after + markers
|
|
59
|
+
if (ctx.tokens[pos]?.type === "STAR") {
|
|
60
|
+
hidden = true;
|
|
61
|
+
pos++;
|
|
62
|
+
consumed++;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Whitespace is required after + or +*
|
|
66
|
+
if (ctx.tokens[pos]?.type !== "WHITESPACE") {
|
|
67
|
+
return { success: false };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Wikidot only supports h1-h6 (1-6 plus signs). 7+ is not a heading.
|
|
71
|
+
if (marker.value.length > 6) {
|
|
72
|
+
return { success: false };
|
|
73
|
+
}
|
|
74
|
+
const depth = marker.value.length as 1 | 2 | 3 | 4 | 5 | 6;
|
|
75
|
+
|
|
76
|
+
// Skip whitespace
|
|
77
|
+
while (ctx.tokens[pos]?.type === "WHITESPACE") {
|
|
78
|
+
pos++;
|
|
79
|
+
consumed++;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Parse inline content until newline
|
|
83
|
+
const inlineCtx: ParseContext = { ...ctx, pos };
|
|
84
|
+
const inlineResult = parseInlineUntil(inlineCtx, "NEWLINE");
|
|
85
|
+
const children: Element[] = inlineResult.elements;
|
|
86
|
+
consumed += inlineResult.consumed;
|
|
87
|
+
pos += inlineResult.consumed;
|
|
88
|
+
|
|
89
|
+
// Consume newline
|
|
90
|
+
if (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
91
|
+
consumed++;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Store TOC entry (only non-hidden headings)
|
|
95
|
+
if (!hidden) {
|
|
96
|
+
const headingText = extractText(children);
|
|
97
|
+
ctx.tocEntries.push({ level: depth, text: headingText });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
elements: [
|
|
103
|
+
{
|
|
104
|
+
element: "container",
|
|
105
|
+
data: {
|
|
106
|
+
type: { header: { level: depth as HeadingLevel, "has-toc": !hidden } },
|
|
107
|
+
attributes: {},
|
|
108
|
+
elements: children,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
consumed,
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Recursively extracts the plain-text content from a tree of elements.
|
|
119
|
+
*
|
|
120
|
+
* Used to build the `text` field for table-of-contents entries. Only
|
|
121
|
+
* `text` elements and containers with nested `elements` are traversed;
|
|
122
|
+
* other element types (images, etc.) are ignored.
|
|
123
|
+
*
|
|
124
|
+
* @param elements - The heading's inline child elements.
|
|
125
|
+
* @returns Concatenated plain text.
|
|
126
|
+
*/
|
|
127
|
+
function extractText(elements: Element[]): string {
|
|
128
|
+
let text = "";
|
|
129
|
+
for (const el of elements) {
|
|
130
|
+
if (el.element === "text" && typeof el.data === "string") {
|
|
131
|
+
text += el.data;
|
|
132
|
+
} else if (
|
|
133
|
+
el.element === "container" &&
|
|
134
|
+
el.data &&
|
|
135
|
+
typeof el.data === "object" &&
|
|
136
|
+
"elements" in el.data
|
|
137
|
+
) {
|
|
138
|
+
text += extractText(el.data.elements as Element[]);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return text;
|
|
142
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Block rule for Wikidot horizontal rules: `----` (four or more hyphens
|
|
4
|
+
* at the start of a line).
|
|
5
|
+
*
|
|
6
|
+
* The lexer emits an HR_MARKER token for sequences of four or more `-`
|
|
7
|
+
* characters at line start. This rule consumes the marker, any remaining
|
|
8
|
+
* tokens on the line, and an optional trailing newline, producing a single
|
|
9
|
+
* `horizontal-rule` element (rendered as `<hr />`).
|
|
10
|
+
*
|
|
11
|
+
* Any text after the `----` on the same line is silently discarded,
|
|
12
|
+
* matching Wikidot's behaviour.
|
|
13
|
+
*
|
|
14
|
+
* @module
|
|
15
|
+
*/
|
|
16
|
+
import type { Element } from "@wdprlib/ast";
|
|
17
|
+
import type { BlockRule, ParseContext, RuleResult } from "../types";
|
|
18
|
+
import { currentToken } from "../types";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Block rule for horizontal rules (`----`).
|
|
22
|
+
*
|
|
23
|
+
* Produces a `horizontal-rule` element with no data payload.
|
|
24
|
+
*/
|
|
25
|
+
export const horizontalRuleRule: BlockRule = {
|
|
26
|
+
name: "horizontalRule",
|
|
27
|
+
startTokens: ["HR_MARKER"],
|
|
28
|
+
requiresLineStart: true,
|
|
29
|
+
|
|
30
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
31
|
+
const marker = currentToken(ctx);
|
|
32
|
+
|
|
33
|
+
if (!marker.lineStart) {
|
|
34
|
+
return { success: false };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let pos = ctx.pos + 1;
|
|
38
|
+
let consumed = 1;
|
|
39
|
+
|
|
40
|
+
// Skip to end of line
|
|
41
|
+
while (pos < ctx.tokens.length) {
|
|
42
|
+
const token = ctx.tokens[pos];
|
|
43
|
+
if (!token || token.type === "NEWLINE" || token.type === "EOF") {
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
pos++;
|
|
47
|
+
consumed++;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Consume newline
|
|
51
|
+
if (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
52
|
+
consumed++;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
success: true,
|
|
57
|
+
elements: [{ element: "horizontal-rule" }],
|
|
58
|
+
consumed,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
};
|