@wdprlib/parser 5.1.6 → 5.3.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 +764 -452
- package/dist/index.d.cts +24 -4
- package/dist/index.d.ts +24 -4
- package/dist/index.js +762 -452
- package/package.json +2 -2
- 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/module/listpages/resolution/items.ts +2 -2
- package/src/parser/rules/block/module/listpages/resolution/pager.ts +43 -0
- package/src/parser/rules/block/module/listpages/resolution/wrapper.ts +6 -1
- package/src/parser/rules/block/module/listpages/resolve.ts +1 -1
- package/src/parser/rules/block/module/listpages/types/external-data.ts +13 -0
- package/src/parser/rules/block/module/listpages/url-resolution/params.ts +5 -4
- package/src/parser/rules/block/module/resolution/data-maps.ts +75 -3
- package/src/parser/rules/block/module/resolution/resolve-async.ts +12 -3
- 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/pipeline/process.ts +8 -1
- package/src/parser/rules/inline/underline/child.ts +0 -26
- package/src/parser/rules/inline/underline/content.ts +0 -29
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wdprlib/parser",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"description": "Parser for Wikidot markup",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ast",
|
|
@@ -44,6 +44,6 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@braintree/sanitize-url": "^7.1.1",
|
|
47
|
-
"@wdprlib/ast": "4.0
|
|
47
|
+
"@wdprlib/ast": "4.2.0"
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Generated by bun run build:info. Do not edit.
|
|
2
|
+
/** Build-time package metadata; unavailable Git information is null. */
|
|
3
|
+
export const buildInfo: Readonly<{
|
|
4
|
+
version: string;
|
|
5
|
+
sha: string | null;
|
|
6
|
+
dirty: boolean | null;
|
|
7
|
+
}> = Object.freeze({
|
|
8
|
+
version: "5.3.0",
|
|
9
|
+
sha: "b71b84c705e05717f5ea12fd89684a91b669dbff",
|
|
10
|
+
dirty: false,
|
|
11
|
+
});
|
package/src/index.ts
CHANGED
package/src/lexer/lexer.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
createInitialLexerState,
|
|
12
12
|
current,
|
|
13
13
|
isAtEnd,
|
|
14
|
+
isSyntaxLineStart,
|
|
14
15
|
type LexerState,
|
|
15
16
|
} from "./state";
|
|
16
17
|
import { findInvalidAnchorNameEnd } from "./anchor";
|
|
@@ -19,7 +20,7 @@ import { scanSimpleSyntaxToken } from "./syntax-actions";
|
|
|
19
20
|
import type { TokenAction } from "./token-actions";
|
|
20
21
|
import { scanPunctuationToken } from "./punctuation";
|
|
21
22
|
import { scanCompactTextToken, scanTextToken } from "./text-actions";
|
|
22
|
-
import { scanSpacingToken } from "./spacing-actions";
|
|
23
|
+
import { limitBlockquotePrefixSpace, scanSpacingToken } from "./spacing-actions";
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Converts a Wikidot markup source string into a flat array of {@link Token}s.
|
|
@@ -119,7 +120,7 @@ export class Lexer {
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
private emitTokenAction(action: TokenAction): void {
|
|
122
|
-
advanceByToken(this.state, action.type, action.length);
|
|
123
|
+
advanceByToken(this.state, action.type, action.length, action.value);
|
|
123
124
|
this.addToken(action.type, action.value);
|
|
124
125
|
}
|
|
125
126
|
|
|
@@ -138,12 +139,12 @@ export class Lexer {
|
|
|
138
139
|
*/
|
|
139
140
|
private scanToken(): void {
|
|
140
141
|
const char = this.current();
|
|
141
|
-
const isLineStart = this.state
|
|
142
|
+
const isLineStart = isSyntaxLineStart(this.state);
|
|
142
143
|
const src = this.state.source;
|
|
143
144
|
|
|
144
145
|
const spacingAction = scanSpacingToken(src, this.state.pos);
|
|
145
146
|
if (spacingAction) {
|
|
146
|
-
this.emitTokenAction(spacingAction);
|
|
147
|
+
this.emitTokenAction(limitBlockquotePrefixSpace(spacingAction, this.state.tokens.at(-1)));
|
|
147
148
|
return;
|
|
148
149
|
}
|
|
149
150
|
|
|
@@ -152,6 +153,7 @@ export class Lexer {
|
|
|
152
153
|
source: src,
|
|
153
154
|
pos: this.state.pos,
|
|
154
155
|
lineStart: isLineStart,
|
|
156
|
+
physicalLineStart: this.state.lineStart,
|
|
155
157
|
splitBlockClose: this.splitBlockClosePositions.has(this.state.pos),
|
|
156
158
|
findInvalidAnchorNameEnd: () => this.findInvalidAnchorNameEnd(),
|
|
157
159
|
});
|
package/src/lexer/punctuation.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface PunctuationScanInput {
|
|
|
14
14
|
source: string;
|
|
15
15
|
pos: number;
|
|
16
16
|
lineStart: boolean;
|
|
17
|
+
physicalLineStart: boolean;
|
|
17
18
|
splitBlockClose: boolean;
|
|
18
19
|
findInvalidAnchorNameEnd: () => number | null;
|
|
19
20
|
}
|
|
@@ -51,7 +52,7 @@ export function scanPunctuationToken(input: PunctuationScanInput): PunctuationSc
|
|
|
51
52
|
return { handled: true, actions: scanAtToken(source, pos) };
|
|
52
53
|
|
|
53
54
|
case ">":
|
|
54
|
-
return { handled: true, actions: scanGreaterToken(source, pos,
|
|
55
|
+
return { handled: true, actions: scanGreaterToken(source, pos, input.physicalLineStart) };
|
|
55
56
|
|
|
56
57
|
case "-":
|
|
57
58
|
return { handled: true, actions: scanDashToken(source, pos, lineStart) };
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import { findWhitespaceRunEnd } from "./runs";
|
|
2
|
+
import { isLineStartQuoteMarker } from "./state";
|
|
2
3
|
import type { TokenAction } from "./token-actions";
|
|
4
|
+
import type { Token } from "./tokens";
|
|
5
|
+
|
|
6
|
+
export function limitBlockquotePrefixSpace(
|
|
7
|
+
action: TokenAction,
|
|
8
|
+
previous: Token | undefined,
|
|
9
|
+
): TokenAction {
|
|
10
|
+
if (!isLineStartQuoteMarker(previous) || action.type !== "WHITESPACE") {
|
|
11
|
+
return action;
|
|
12
|
+
}
|
|
13
|
+
if (action.length <= 1 || !action.value.startsWith(" ")) {
|
|
14
|
+
return action;
|
|
15
|
+
}
|
|
16
|
+
return { type: "WHITESPACE", value: " ", length: 1 };
|
|
17
|
+
}
|
|
3
18
|
|
|
4
19
|
export function scanSpacingToken(src: string, pos: number): TokenAction | null {
|
|
5
20
|
const char = src[pos];
|
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
|
},
|