@wdprlib/parser 5.1.5 → 5.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/LICENSE +661 -0
- package/README.md +1 -1
- package/THIRD-PARTY-LICENSES.md +23 -0
- package/dist/index.cjs +655 -436
- package/dist/index.d.cts +11 -4
- package/dist/index.d.ts +11 -4
- package/dist/index.js +653 -435
- package/licenses/LGPL-2.1.txt +501 -0
- package/package.json +6 -3
- package/src/build-info.generated.ts +11 -0
- package/src/index.ts +2 -0
- package/src/lexer/lexer.ts +6 -4
- package/src/lexer/punctuation.ts +2 -1
- package/src/lexer/spacing-actions.ts +15 -0
- package/src/lexer/state.ts +30 -4
- package/src/lexer/token-factory.ts +15 -2
- package/src/lexer/tokens.ts +4 -3
- package/src/parser/rules/block/blockquote/build.ts +81 -37
- package/src/parser/rules/block/blockquote/index.ts +5 -6
- package/src/parser/rules/block/blockquote/line.ts +11 -10
- package/src/parser/rules/block/blockquote/lines.ts +102 -4
- package/src/parser/rules/block/code/content.ts +0 -3
- package/src/parser/rules/block/code/index.ts +14 -10
- package/src/parser/rules/block/definition-list/index.ts +4 -10
- package/src/parser/rules/block/definition-list/item-value.ts +2 -11
- package/src/parser/rules/block/definition-list/items.ts +1 -2
- package/src/parser/rules/block/embed-block/index.ts +30 -30
- package/src/parser/rules/block/heading/index.ts +7 -0
- package/src/parser/rules/block/paragraph/index.ts +41 -10
- package/src/parser/rules/block/parsing/content.ts +18 -1
- package/src/parser/rules/block/table/pipe/cell.ts +36 -1
- package/src/parser/rules/block/table/pipe/row.ts +21 -1
- package/src/parser/rules/block/toc/element.ts +2 -2
- package/src/parser/rules/block/toc/index.ts +2 -2
- package/src/parser/rules/block/toc/open.ts +5 -18
- package/src/parser/rules/contracts/scope.ts +4 -0
- package/src/parser/rules/inline/bold.ts +5 -5
- package/src/parser/rules/inline/color/syntax.ts +5 -8
- package/src/parser/rules/inline/formatting/close.ts +39 -0
- package/src/parser/rules/inline/formatting/container.ts +7 -4
- package/src/parser/rules/inline/index.ts +2 -0
- package/src/parser/rules/inline/italic.ts +5 -5
- package/src/parser/rules/inline/monospace.ts +5 -5
- package/src/parser/rules/inline/parsing/block-start-predicates.ts +2 -0
- package/src/parser/rules/inline/parsing/inline-content.ts +58 -4
- package/src/parser/rules/inline/raw/end.ts +28 -0
- package/src/parser/rules/inline/span/content.ts +32 -1
- package/src/parser/rules/inline/strikethrough/index.ts +1 -1
- package/src/parser/rules/inline/strikethrough/parse.ts +2 -9
- package/src/parser/rules/inline/strikethrough/syntax.ts +3 -20
- package/src/parser/rules/inline/subscript.ts +5 -5
- package/src/parser/rules/inline/superscript.ts +5 -5
- package/src/parser/rules/inline/underline/index.ts +5 -78
- package/src/parser/rules/tokens.ts +6 -15
- package/src/parser/rules/inline/underline/child.ts +0 -26
- package/src/parser/rules/inline/underline/content.ts +0 -29
package/src/lexer/state.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface LexerState {
|
|
|
10
10
|
line: number;
|
|
11
11
|
column: number;
|
|
12
12
|
lineStart: boolean;
|
|
13
|
+
quoteContentStart: boolean;
|
|
13
14
|
tokens: Token[];
|
|
14
15
|
}
|
|
15
16
|
|
|
@@ -20,10 +21,20 @@ export function createInitialLexerState(source: string): LexerState {
|
|
|
20
21
|
line: 1,
|
|
21
22
|
column: 1,
|
|
22
23
|
lineStart: true,
|
|
24
|
+
quoteContentStart: false,
|
|
23
25
|
tokens: [],
|
|
24
26
|
};
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
export function isSyntaxLineStart(state: LexerState): boolean {
|
|
30
|
+
return state.lineStart || state.quoteContentStart;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** An indented `>` is not a blockquote, so its content keeps the enclosing line. */
|
|
34
|
+
export function isLineStartQuoteMarker(token: Token | undefined): boolean {
|
|
35
|
+
return token?.type === "BLOCKQUOTE_MARKER" && token.lineStart;
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
export function isAtEnd(state: LexerState): boolean {
|
|
28
39
|
return state.pos >= state.source.length;
|
|
29
40
|
}
|
|
@@ -46,20 +57,33 @@ export function advanceBy(state: LexerState, n = 1): void {
|
|
|
46
57
|
updatePosition(state, start, end);
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
export function advanceByToken(
|
|
60
|
+
export function advanceByToken(
|
|
61
|
+
state: LexerState,
|
|
62
|
+
type: TokenType,
|
|
63
|
+
length: number,
|
|
64
|
+
value = "",
|
|
65
|
+
): void {
|
|
66
|
+
const afterQuoteMarker = isLineStartQuoteMarker(state.tokens[state.tokens.length - 1]);
|
|
50
67
|
state.pos += length;
|
|
51
68
|
|
|
52
69
|
if (type === "NEWLINE") {
|
|
53
70
|
state.line++;
|
|
54
71
|
state.column = 1;
|
|
55
72
|
state.lineStart = true;
|
|
73
|
+
state.quoteContentStart = false;
|
|
56
74
|
return;
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
state.column += length;
|
|
60
|
-
if (type
|
|
61
|
-
|
|
78
|
+
if (type === "WHITESPACE") {
|
|
79
|
+
if (afterQuoteMarker && value === " ") {
|
|
80
|
+
state.quoteContentStart = true;
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
62
83
|
}
|
|
84
|
+
|
|
85
|
+
state.lineStart = false;
|
|
86
|
+
state.quoteContentStart = false;
|
|
63
87
|
}
|
|
64
88
|
|
|
65
89
|
function updatePosition(state: LexerState, start: number, end: number): void {
|
|
@@ -71,8 +95,9 @@ function updatePositionFromValue(state: LexerState, value: string): void {
|
|
|
71
95
|
const firstNewline = value.indexOf("\n");
|
|
72
96
|
if (firstNewline === -1) {
|
|
73
97
|
state.column += value.length;
|
|
74
|
-
if (
|
|
98
|
+
if (hasNonLineStartSpacing(value, 0)) {
|
|
75
99
|
state.lineStart = false;
|
|
100
|
+
state.quoteContentStart = false;
|
|
76
101
|
}
|
|
77
102
|
return;
|
|
78
103
|
}
|
|
@@ -90,6 +115,7 @@ function updatePositionFromValue(state: LexerState, value: string): void {
|
|
|
90
115
|
state.line += newlineCount;
|
|
91
116
|
state.column = value.length - lastNewline;
|
|
92
117
|
state.lineStart = !hasNonLineStartSpacing(value, lastNewline + 1);
|
|
118
|
+
state.quoteContentStart = false;
|
|
93
119
|
}
|
|
94
120
|
|
|
95
121
|
function hasNonLineStartSpacing(value: string, start: number): boolean {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Position } from "@wdprlib/ast";
|
|
2
|
-
import type
|
|
2
|
+
import { isLineStartQuoteMarker, type LexerState } from "./state";
|
|
3
3
|
import type { Token, TokenType } from "./tokens";
|
|
4
4
|
|
|
5
5
|
const ZERO_POSITION: Position = {
|
|
@@ -58,5 +58,18 @@ function currentTokenPosition(state: LexerState, value: string): Position {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
function isTokenAtLineStart(state: LexerState): boolean {
|
|
61
|
-
|
|
61
|
+
if (state.tokens.length === 0) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const last = state.tokens[state.tokens.length - 1];
|
|
66
|
+
if (last?.type === "NEWLINE") {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
last?.type === "WHITESPACE" &&
|
|
72
|
+
last.value === " " &&
|
|
73
|
+
isLineStartQuoteMarker(state.tokens[state.tokens.length - 2])
|
|
74
|
+
);
|
|
62
75
|
}
|
package/src/lexer/tokens.ts
CHANGED
|
@@ -113,9 +113,10 @@ export interface Token {
|
|
|
113
113
|
/** Start/end location in the original source string */
|
|
114
114
|
position: Position;
|
|
115
115
|
/**
|
|
116
|
-
* `true` when this token
|
|
117
|
-
*
|
|
118
|
-
*
|
|
116
|
+
* `true` when this token opens a logical line: the first non-whitespace
|
|
117
|
+
* token on a source line, or the first token after a blockquote prefix.
|
|
118
|
+
* Block-level rules (headings, lists, blockquotes) check this flag before
|
|
119
|
+
* attempting to match.
|
|
119
120
|
*/
|
|
120
121
|
lineStart: boolean;
|
|
121
122
|
}
|
|
@@ -1,55 +1,67 @@
|
|
|
1
1
|
import type { Element } from "@wdprlib/ast";
|
|
2
|
+
import { createToken, type Token } from "../../../../lexer";
|
|
2
3
|
import { processDepths, type DepthList } from "../../../depth";
|
|
4
|
+
import type { ParseContext } from "../../types";
|
|
5
|
+
import { parseBlocksUntil } from "../utils";
|
|
3
6
|
import type { BlockquoteLine, ParsedBlockquoteLine } from "./lines";
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Blocks Wikidot protects before parsing, by matching an opening tag at the
|
|
10
|
+
* start of a line. A `>` prefix defeats that match, so inside a blockquote
|
|
11
|
+
* only their tags stay literal while the body parses as usual.
|
|
12
|
+
*/
|
|
13
|
+
const EXCLUDED_BLOCK_NAMES: ReadonlySet<string> = new Set([
|
|
14
|
+
"bibliography",
|
|
15
|
+
"code",
|
|
16
|
+
"html",
|
|
17
|
+
"include",
|
|
18
|
+
"math",
|
|
19
|
+
"module",
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const NEVER_CLOSES = () => false;
|
|
23
|
+
|
|
24
|
+
export function buildBlockquoteElements(
|
|
25
|
+
ctx: ParseContext,
|
|
26
|
+
lines: ParsedBlockquoteLine[],
|
|
27
|
+
): Element[] {
|
|
6
28
|
const depthTrees = processDepths<null, BlockquoteLine>(null, lines);
|
|
7
|
-
return depthTrees
|
|
29
|
+
return depthTrees
|
|
30
|
+
.map(({ list }) => buildBlockquoteElement(ctx, list))
|
|
31
|
+
.filter((element): element is Element => element !== null);
|
|
8
32
|
}
|
|
9
33
|
|
|
10
|
-
function buildBlockquoteElement(
|
|
34
|
+
function buildBlockquoteElement(
|
|
35
|
+
ctx: ParseContext,
|
|
36
|
+
list: DepthList<null, BlockquoteLine>,
|
|
37
|
+
): Element | null {
|
|
11
38
|
const children: Element[] = [];
|
|
12
|
-
let
|
|
13
|
-
|
|
14
|
-
function
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
currentParagraphChildren[currentParagraphChildren.length - 1]?.element === "line-break"
|
|
19
|
-
) {
|
|
20
|
-
currentParagraphChildren.pop();
|
|
21
|
-
}
|
|
22
|
-
if (currentParagraphChildren.length > 0) {
|
|
23
|
-
children.push({
|
|
24
|
-
element: "container",
|
|
25
|
-
data: {
|
|
26
|
-
type: "paragraph",
|
|
27
|
-
attributes: {},
|
|
28
|
-
elements: currentParagraphChildren,
|
|
29
|
-
},
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
currentParagraphChildren = [];
|
|
33
|
-
}
|
|
39
|
+
let pending: BlockquoteLine[] = [];
|
|
40
|
+
|
|
41
|
+
function flushPending() {
|
|
42
|
+
if (pending.length === 0) return;
|
|
43
|
+
children.push(...parseLines(ctx, pending));
|
|
44
|
+
pending = [];
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
for (const item of list) {
|
|
37
48
|
if (item.kind === "item") {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
} else {
|
|
47
|
-
flushParagraph();
|
|
48
|
-
children.push(buildBlockquoteElement(item.children));
|
|
49
|
+
pending.push(item.value);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
flushPending();
|
|
54
|
+
const nested = buildBlockquoteElement(ctx, item.children);
|
|
55
|
+
if (nested) {
|
|
56
|
+
children.push(nested);
|
|
49
57
|
}
|
|
50
58
|
}
|
|
51
59
|
|
|
52
|
-
|
|
60
|
+
flushPending();
|
|
61
|
+
|
|
62
|
+
if (children.length === 0) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
53
65
|
|
|
54
66
|
return {
|
|
55
67
|
element: "container",
|
|
@@ -60,3 +72,35 @@ function buildBlockquoteElement(list: DepthList<null, BlockquoteLine>): Element
|
|
|
60
72
|
},
|
|
61
73
|
};
|
|
62
74
|
}
|
|
75
|
+
|
|
76
|
+
function parseLines(ctx: ParseContext, lines: BlockquoteLine[]): Element[] {
|
|
77
|
+
const tokens = sliceLineTokens(ctx, lines);
|
|
78
|
+
const lineCtx: ParseContext = { ...ctx, tokens, pos: 0 };
|
|
79
|
+
|
|
80
|
+
return parseBlocksUntil(lineCtx, NEVER_CLOSES, {
|
|
81
|
+
excludedBlockNames: EXCLUDED_BLOCK_NAMES,
|
|
82
|
+
}).elements;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function sliceLineTokens(ctx: ParseContext, lines: BlockquoteLine[]): Token[] {
|
|
86
|
+
const tokens: Token[] = [];
|
|
87
|
+
|
|
88
|
+
for (const { start, end } of lines) {
|
|
89
|
+
for (let pos = start; pos < end; pos++) {
|
|
90
|
+
const token = ctx.tokens[pos];
|
|
91
|
+
if (token) {
|
|
92
|
+
tokens.push(token);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const last = tokens[tokens.length - 1];
|
|
98
|
+
tokens.push(createToken("EOF", "", last?.position ?? ZERO_POSITION));
|
|
99
|
+
|
|
100
|
+
return tokens;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const ZERO_POSITION = {
|
|
104
|
+
start: { line: 0, column: 0, offset: 0 },
|
|
105
|
+
end: { line: 0, column: 0, offset: 0 },
|
|
106
|
+
};
|
|
@@ -35,11 +35,11 @@ import { buildBlockquoteElements, collectBlockquoteLines } from "./lines";
|
|
|
35
35
|
* Parsing strategy:
|
|
36
36
|
* 1. Collect consecutive lines that begin with BLOCKQUOTE_MARKER at line start.
|
|
37
37
|
* 2. For each line, record the depth (number of `>` chars, zero-indexed)
|
|
38
|
-
* and
|
|
38
|
+
* and the token range of the content after the mandatory space.
|
|
39
39
|
* 3. Lines missing the required space are consumed but produce no output.
|
|
40
40
|
* 4. Feed the flat depth list into {@link processDepths} to build a nested tree.
|
|
41
|
-
* 5. Recursively convert the tree into nested blockquote container elements
|
|
42
|
-
*
|
|
41
|
+
* 5. Recursively convert the tree into nested blockquote container elements,
|
|
42
|
+
* re-parsing each run of content tokens as blocks.
|
|
43
43
|
*/
|
|
44
44
|
export const blockquoteRule: BlockRule = {
|
|
45
45
|
name: "blockquote",
|
|
@@ -64,11 +64,10 @@ export const blockquoteRule: BlockRule = {
|
|
|
64
64
|
return { success: false };
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
const blockquotes = buildBlockquoteElements(blockquoteLines.lines);
|
|
67
|
+
const blockquotes = buildBlockquoteElements(ctx, blockquoteLines.lines);
|
|
68
68
|
|
|
69
|
-
// Return first blockquote (should usually be only one)
|
|
70
69
|
if (blockquotes.length === 0) {
|
|
71
|
-
return { success:
|
|
70
|
+
return { success: true, elements: [], consumed: blockquoteLines.consumed };
|
|
72
71
|
}
|
|
73
72
|
|
|
74
73
|
return {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ParseContext } from "../../types";
|
|
2
|
-
import { parseInlineUntil } from "../../inline/utils";
|
|
3
2
|
import type { ParsedBlockquoteLine } from "./lines";
|
|
4
3
|
|
|
5
4
|
/**
|
|
@@ -35,19 +34,21 @@ export function parseBlockquoteLine(
|
|
|
35
34
|
return { kind: "skipped", consumed: consumeLineRemainder(ctx, pos) + consumed };
|
|
36
35
|
}
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
pos++;
|
|
38
|
+
consumed++;
|
|
39
|
+
|
|
40
|
+
const contentStart = pos;
|
|
41
|
+
while (
|
|
42
|
+
pos < ctx.tokens.length &&
|
|
43
|
+
ctx.tokens[pos]?.type !== "NEWLINE" &&
|
|
44
|
+
ctx.tokens[pos]?.type !== "EOF"
|
|
45
|
+
) {
|
|
39
46
|
pos++;
|
|
40
47
|
consumed++;
|
|
41
48
|
}
|
|
42
49
|
|
|
43
|
-
const inlineCtx: ParseContext = { ...ctx, pos };
|
|
44
|
-
const inlineResult = parseInlineUntil(inlineCtx, "NEWLINE");
|
|
45
|
-
consumed += inlineResult.consumed;
|
|
46
|
-
pos += inlineResult.consumed;
|
|
47
|
-
|
|
48
|
-
let hasLineBreak = false;
|
|
49
50
|
if (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
50
|
-
|
|
51
|
+
pos++;
|
|
51
52
|
consumed++;
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -56,7 +57,7 @@ export function parseBlockquoteLine(
|
|
|
56
57
|
line: {
|
|
57
58
|
depth: depth - 1,
|
|
58
59
|
ltype: null,
|
|
59
|
-
value: {
|
|
60
|
+
value: { start: contentStart, end: pos },
|
|
60
61
|
},
|
|
61
62
|
consumed,
|
|
62
63
|
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import type { Element } from "@wdprlib/ast";
|
|
2
1
|
import type { ParseContext } from "../../types";
|
|
3
2
|
import { parseBlockquoteLine } from "./line";
|
|
4
3
|
export { buildBlockquoteElements } from "./build";
|
|
5
4
|
|
|
5
|
+
/** Half-open range of the line's content tokens in the enclosing token stream. */
|
|
6
6
|
export interface BlockquoteLine {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface ParsedBlockquoteLine {
|
|
@@ -26,7 +26,14 @@ export function collectBlockquoteLines(ctx: ParseContext): ParsedBlockquoteLines
|
|
|
26
26
|
|
|
27
27
|
while (pos < ctx.tokens.length) {
|
|
28
28
|
const result = parseBlockquoteLine(ctx, pos);
|
|
29
|
-
if (result.kind === "stop")
|
|
29
|
+
if (result.kind === "stop") {
|
|
30
|
+
const resumed = extendToCommentClose(ctx, lines, pos);
|
|
31
|
+
if (resumed === pos) break;
|
|
32
|
+
|
|
33
|
+
consumed += resumed - pos;
|
|
34
|
+
pos = resumed;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
30
37
|
|
|
31
38
|
pos += result.consumed;
|
|
32
39
|
consumed += result.consumed;
|
|
@@ -35,5 +42,96 @@ export function collectBlockquoteLines(ctx: ParseContext): ParsedBlockquoteLines
|
|
|
35
42
|
}
|
|
36
43
|
}
|
|
37
44
|
|
|
45
|
+
blankCommentOnlyLines(ctx, lines);
|
|
46
|
+
|
|
38
47
|
return { lines, consumed };
|
|
39
48
|
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Comments are stripped before the quoted block is split, so one opened on a
|
|
52
|
+
* quoted line closes wherever its `--]` is, quoted or not. The rest of the
|
|
53
|
+
* closing line joins the quoted line the comment started on.
|
|
54
|
+
*/
|
|
55
|
+
function extendToCommentClose(
|
|
56
|
+
ctx: ParseContext,
|
|
57
|
+
lines: ParsedBlockquoteLine[],
|
|
58
|
+
stopPos: number,
|
|
59
|
+
): number {
|
|
60
|
+
const last = lines[lines.length - 1];
|
|
61
|
+
if (!last || !endsInsideComment(ctx, lines)) {
|
|
62
|
+
return stopPos;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let pos = stopPos;
|
|
66
|
+
while (pos < ctx.tokens.length && ctx.tokens[pos]?.type !== "COMMENT_CLOSE") {
|
|
67
|
+
if (ctx.tokens[pos]?.type === "EOF") return stopPos;
|
|
68
|
+
pos++;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
while (pos < ctx.tokens.length && ctx.tokens[pos]?.type !== "EOF") {
|
|
72
|
+
pos++;
|
|
73
|
+
if (ctx.tokens[pos - 1]?.type === "NEWLINE") break;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
last.value.end = pos;
|
|
77
|
+
return pos;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function endsInsideComment(ctx: ParseContext, lines: ParsedBlockquoteLine[]): boolean {
|
|
81
|
+
let open = false;
|
|
82
|
+
|
|
83
|
+
for (const { value } of lines) {
|
|
84
|
+
for (let pos = value.start; pos < value.end; pos++) {
|
|
85
|
+
const type = ctx.tokens[pos]?.type;
|
|
86
|
+
if (type === "COMMENT_OPEN") open = true;
|
|
87
|
+
else if (type === "COMMENT_CLOSE") open = false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return open;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Wikidot strips comments before parsing the quoted content, so a line left
|
|
96
|
+
* with nothing becomes blank and separates paragraphs. Unterminated comments
|
|
97
|
+
* stay literal, hence the scan only covers closed ones.
|
|
98
|
+
*/
|
|
99
|
+
function blankCommentOnlyLines(ctx: ParseContext, lines: ParsedBlockquoteLine[]): void {
|
|
100
|
+
const positions: { line: number; pos: number }[] = [];
|
|
101
|
+
lines.forEach((line, index) => {
|
|
102
|
+
for (let pos = line.value.start; pos < line.value.end; pos++) {
|
|
103
|
+
if (ctx.tokens[pos]?.type !== "NEWLINE") {
|
|
104
|
+
positions.push({ line: index, pos });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const commented: boolean[] = Array.from({ length: positions.length }, () => false);
|
|
110
|
+
for (let open = 0; open < positions.length; open++) {
|
|
111
|
+
if (ctx.tokens[positions[open]!.pos]?.type !== "COMMENT_OPEN") continue;
|
|
112
|
+
|
|
113
|
+
let close = open + 1;
|
|
114
|
+
while (
|
|
115
|
+
close < positions.length &&
|
|
116
|
+
ctx.tokens[positions[close]!.pos]?.type !== "COMMENT_CLOSE"
|
|
117
|
+
) {
|
|
118
|
+
close++;
|
|
119
|
+
}
|
|
120
|
+
if (close === positions.length) break;
|
|
121
|
+
|
|
122
|
+
commented.fill(true, open, close + 1);
|
|
123
|
+
open = close;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const hasContent: boolean[] = Array.from({ length: lines.length }, () => false);
|
|
127
|
+
positions.forEach(({ line, pos }, index) => {
|
|
128
|
+
if (commented[index] || ctx.tokens[pos]?.type === "WHITESPACE") return;
|
|
129
|
+
hasContent[line] = true;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
lines.forEach((line, index) => {
|
|
133
|
+
if (hasContent[index]) return;
|
|
134
|
+
const { end } = line.value;
|
|
135
|
+
line.value.start = ctx.tokens[end - 1]?.type === "NEWLINE" ? end - 1 : end;
|
|
136
|
+
});
|
|
137
|
+
}
|
|
@@ -15,6 +15,9 @@ import { currentToken } from "../../types";
|
|
|
15
15
|
import { parseBlockName } from "../utils";
|
|
16
16
|
import { parseAttributesRaw } from "../utils";
|
|
17
17
|
import { repairSwallowedCodeClose } from "./attributes";
|
|
18
|
+
import { parseInlineUntil } from "../../inline/utils";
|
|
19
|
+
import { getParagraphNewlineBoundary } from "../../inline/parsing/paragraph-boundary";
|
|
20
|
+
import { normalizeParagraphElements } from "../paragraph/normalize";
|
|
18
21
|
import { collectCodeContent } from "./content";
|
|
19
22
|
|
|
20
23
|
/**
|
|
@@ -86,15 +89,16 @@ export const codeBlockRule: BlockRule = {
|
|
|
86
89
|
};
|
|
87
90
|
ctx.codeBlocks.push(codeBlockData);
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
consumed
|
|
98
|
-
}
|
|
92
|
+
const elements: Element[] = [{ element: "code", data: codeBlockData }];
|
|
93
|
+
if (
|
|
94
|
+
ctx.tokens[pos]?.type === "NEWLINE" &&
|
|
95
|
+
!getParagraphNewlineBoundary(ctx, pos, false).shouldBreak &&
|
|
96
|
+
!ctx.scope.blockCloseCondition?.({ ...ctx, pos: pos + 1 })
|
|
97
|
+
) {
|
|
98
|
+
const after = parseInlineUntil({ ...ctx, pos: pos + 1 }, "PARAGRAPH_BREAK");
|
|
99
|
+
elements.push({ element: "line-break" }, ...normalizeParagraphElements(after.elements));
|
|
100
|
+
consumed += 1 + after.consumed;
|
|
101
|
+
}
|
|
102
|
+
return { success: true, elements, consumed };
|
|
99
103
|
},
|
|
100
104
|
};
|
|
@@ -14,10 +14,8 @@
|
|
|
14
14
|
* Key parsing details:
|
|
15
15
|
* - Whitespace after the first colon is required (`": key"` not `":key"`).
|
|
16
16
|
* - The key portion supports inline markup (bold, links, etc.).
|
|
17
|
-
* - The value
|
|
18
|
-
*
|
|
19
|
-
* - A single newline within the value does NOT break the entry -- parsing
|
|
20
|
-
* continues on the next line.
|
|
17
|
+
* - The value ends at the next newline or the end of the document.
|
|
18
|
+
* - Explicit line continuations (` _`) can extend a value across source lines.
|
|
21
19
|
*
|
|
22
20
|
* @module
|
|
23
21
|
*/
|
|
@@ -49,14 +47,10 @@ export const definitionListRule: BlockRule = {
|
|
|
49
47
|
return { success: false };
|
|
50
48
|
}
|
|
51
49
|
|
|
50
|
+
const items = toDefinitionListItems(result.items.filter((item) => item.value.length > 0));
|
|
52
51
|
return {
|
|
53
52
|
success: true,
|
|
54
|
-
elements: [
|
|
55
|
-
{
|
|
56
|
-
element: "definition-list",
|
|
57
|
-
data: toDefinitionListItems(result.items),
|
|
58
|
-
},
|
|
59
|
-
],
|
|
53
|
+
elements: items.length > 0 ? [{ element: "definition-list", data: items }] : [],
|
|
60
54
|
consumed: result.consumed,
|
|
61
55
|
};
|
|
62
56
|
},
|
|
@@ -27,17 +27,8 @@ export function parseDefinitionItemValue(
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
if (token.type === "NEWLINE") {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
pos++;
|
|
33
|
-
consumed++;
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
if (nextToken?.type === "NEWLINE" || !nextToken || nextToken.type === "EOF") {
|
|
37
|
-
pos++;
|
|
38
|
-
consumed++;
|
|
39
|
-
break;
|
|
40
|
-
}
|
|
30
|
+
consumed++;
|
|
31
|
+
break;
|
|
41
32
|
}
|
|
42
33
|
|
|
43
34
|
const inlineCtx: ParseContext = { ...ctx, pos };
|
|
@@ -21,8 +21,7 @@ export interface ParsedDefinitionItem {
|
|
|
21
21
|
*
|
|
22
22
|
* The function expects `startPos` to point at a line-start COLON token.
|
|
23
23
|
* It consumes the first colon, mandatory whitespace, key tokens up to
|
|
24
|
-
* the second colon, then value tokens until a
|
|
25
|
-
* or end of input.
|
|
24
|
+
* the second colon, then value tokens until a newline or end of input.
|
|
26
25
|
*/
|
|
27
26
|
export function parseDefinitionItem(
|
|
28
27
|
ctx: ParseContext,
|
|
@@ -8,9 +8,6 @@
|
|
|
8
8
|
* between the tags is stored verbatim as an `embed-block` element. Validation
|
|
9
9
|
* and sanitisation are expected to happen at rendering time or on the server.
|
|
10
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
11
|
* If no closing tag is found, the rule fails to prevent consuming the rest
|
|
15
12
|
* of the document.
|
|
16
13
|
*
|
|
@@ -18,6 +15,8 @@
|
|
|
18
15
|
*/
|
|
19
16
|
import type { Element } from "@wdprlib/ast";
|
|
20
17
|
import type { BlockRule, ParseContext, RuleResult } from "../../types";
|
|
18
|
+
import { parseInlineUntil } from "../../inline/utils";
|
|
19
|
+
import { normalizeParagraphElements } from "../paragraph/normalize";
|
|
21
20
|
import { currentToken } from "../../types";
|
|
22
21
|
import { collectEmbedContent, consumeEmbedClose } from "./content";
|
|
23
22
|
import { parseEmbedBlockOpen } from "./open";
|
|
@@ -34,6 +33,8 @@ export const embedBlockRule: BlockRule = {
|
|
|
34
33
|
name: "embed-block",
|
|
35
34
|
startTokens: ["BLOCK_OPEN"],
|
|
36
35
|
requiresLineStart: false,
|
|
36
|
+
preservesPrecedingLineBreak: true,
|
|
37
|
+
isStartPattern: (ctx, pos) => parseEmbedBlockOpen(ctx, pos) !== null,
|
|
37
38
|
|
|
38
39
|
parse(ctx: ParseContext): RuleResult<Element> {
|
|
39
40
|
const openToken = currentToken(ctx);
|
|
@@ -53,12 +54,17 @@ export const embedBlockRule: BlockRule = {
|
|
|
53
54
|
consumed += contentResult.consumed;
|
|
54
55
|
|
|
55
56
|
if (!contentResult.foundClose) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
if (
|
|
58
|
+
!ctx.diagnostics.some(
|
|
59
|
+
(d) => d.code === "unclosed-block" && d.position === openToken.position,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
ctx.diagnostics.push({
|
|
63
|
+
severity: "warning",
|
|
64
|
+
code: "unclosed-block",
|
|
65
|
+
message: `Missing closing tag [[/${openResult.blockName}]] for [[${openResult.blockName}]]`,
|
|
66
|
+
position: openToken.position,
|
|
67
|
+
});
|
|
62
68
|
return { success: false };
|
|
63
69
|
}
|
|
64
70
|
|
|
@@ -66,26 +72,20 @@ export const embedBlockRule: BlockRule = {
|
|
|
66
72
|
pos += closeConsumed;
|
|
67
73
|
consumed += closeConsumed;
|
|
68
74
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
],
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
],
|
|
88
|
-
consumed,
|
|
89
|
-
};
|
|
75
|
+
const elements: Element[] = [
|
|
76
|
+
{ element: "embed-block", data: { contents: contentResult.contents.trim() } },
|
|
77
|
+
];
|
|
78
|
+
if (
|
|
79
|
+
ctx.scope.inlineEnd === undefined &&
|
|
80
|
+
ctx.tokens[pos]?.type !== "NEWLINE" &&
|
|
81
|
+
ctx.tokens[pos]?.type !== "EOF"
|
|
82
|
+
) {
|
|
83
|
+
const after = parseInlineUntil({ ...ctx, pos }, "PARAGRAPH_BREAK");
|
|
84
|
+
const tail = normalizeParagraphElements(after.elements);
|
|
85
|
+
if (tail[0]?.element === "text") tail[0].data = tail[0].data.trimStart();
|
|
86
|
+
elements.push(...tail);
|
|
87
|
+
consumed += after.consumed;
|
|
88
|
+
}
|
|
89
|
+
return { success: true, elements, consumed };
|
|
90
90
|
},
|
|
91
91
|
};
|