@wdprlib/parser 5.3.0 → 6.0.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 +2429 -1199
- package/dist/index.d.cts +95 -8
- package/dist/index.d.ts +95 -8
- package/dist/index.js +2385 -1152
- package/package.json +3 -2
- package/src/build-info.generated.ts +2 -2
- package/src/index.ts +3 -0
- package/src/lexer/lexer.ts +34 -5
- package/src/lexer/quoted-string.ts +4 -4
- package/src/lexer/syntax-actions.ts +6 -1
- package/src/lexer/text-actions.ts +15 -1
- package/src/lexer/url-schemes.ts +78 -0
- package/src/parser/constants.ts +4 -0
- package/src/parser/parse/block.ts +1 -1
- package/src/parser/postprocess/divAdjacentParagraph.ts +23 -21
- package/src/parser/postprocess/spanStrip/merge.ts +8 -2
- package/src/parser/preprocess/typography.ts +25 -3
- package/src/parser/preprocess/utils/raw-regions.ts +59 -14
- package/src/parser/preprocess/whitespace/index.ts +8 -1
- package/src/parser/rules/block/bibliography/entry-content.ts +1 -1
- package/src/parser/rules/block/block-list/bare-content.ts +3 -1
- package/src/parser/rules/block/block-list/bare-paragraph.ts +7 -2
- package/src/parser/rules/block/block-list/item-content.ts +7 -3
- package/src/parser/rules/block/block-list/li-content.ts +8 -3
- package/src/parser/rules/block/block-list/li-item.ts +1 -1
- package/src/parser/rules/block/blockquote/build.ts +1 -1
- package/src/parser/rules/block/code/boundary.ts +76 -0
- package/src/parser/rules/block/code/content.ts +11 -40
- package/src/parser/rules/block/code/index.ts +8 -31
- package/src/parser/rules/block/code/open.ts +46 -0
- package/src/parser/rules/block/definition-list/item-key.ts +1 -1
- package/src/parser/rules/block/definition-list/item-value.ts +1 -1
- package/src/parser/rules/block/div/failed.ts +2 -0
- package/src/parser/rules/block/index.ts +3 -0
- package/src/parser/rules/block/list/line.ts +6 -3
- package/src/parser/rules/block/math/boundary.ts +104 -0
- package/src/parser/rules/block/math/index.ts +17 -57
- package/src/parser/rules/block/module/index.ts +3 -2
- package/src/parser/rules/block/module/listpages/extraction/listpages.ts +5 -0
- package/src/parser/rules/block/module/listpages/extraction/query.ts +1 -0
- package/src/parser/rules/block/module/listpages/extraction/template.ts +16 -0
- package/src/parser/rules/block/module/listpages/extraction/variables.ts +1 -0
- package/src/parser/rules/block/module/listpages/index.ts +1 -0
- package/src/parser/rules/block/module/listpages/normalization/order-parent.ts +9 -1
- package/src/parser/rules/block/module/listpages/normalize.ts +1 -0
- package/src/parser/rules/block/module/listpages/parser.ts +1 -0
- package/src/parser/rules/block/module/listpages/resolve.ts +4 -2
- package/src/parser/rules/block/module/listpages/template/excerpt.ts +63 -0
- package/src/parser/rules/block/module/listpages/template/format/content.ts +0 -32
- package/src/parser/rules/block/module/listpages/template/format/index.ts +1 -1
- package/src/parser/rules/block/module/listpages/template/getters/index.ts +14 -0
- package/src/parser/rules/block/module/listpages/template/getters/parameterized.ts +3 -1
- package/src/parser/rules/block/module/listpages/template/getters/registered.ts +52 -0
- package/src/parser/rules/block/module/listpages/template/getters/simple.ts +15 -5
- package/src/parser/rules/block/module/listpages/template/literal.ts +6 -0
- package/src/parser/rules/block/module/listpages/template/syntax.ts +10 -1
- package/src/parser/rules/block/module/listpages/types/data-requirements.ts +6 -0
- package/src/parser/rules/block/module/listpages/types/external-data.ts +36 -2
- package/src/parser/rules/block/module/listpages/types/normalized-query.ts +14 -4
- package/src/parser/rules/block/module/listpages/types/query.ts +2 -0
- package/src/parser/rules/block/module/listpages/types/variables.ts +5 -0
- package/src/parser/rules/block/module/listpages/url-resolution/fields.ts +2 -0
- package/src/parser/rules/block/module/listusers/resolve.ts +2 -1
- package/src/parser/rules/block/module/mapping.ts +2 -1
- package/src/parser/rules/block/module/rate/index.ts +16 -19
- package/src/parser/rules/block/module/rate/resolve.ts +108 -0
- package/src/parser/rules/block/module/rate/types.ts +6 -16
- package/src/parser/rules/block/module/resolution/resolve-async.ts +2 -1
- package/src/parser/rules/block/module/resolve.ts +10 -8
- package/src/parser/rules/block/module/types-common.ts +4 -0
- package/src/parser/rules/block/note/boundary.ts +88 -0
- package/src/parser/rules/block/note/index.ts +37 -0
- package/src/parser/rules/block/paragraph/index.ts +12 -4
- package/src/parser/rules/block/paragraph/normalize.ts +6 -1
- package/src/parser/rules/block/parsing/block-item.ts +1 -1
- package/src/parser/rules/block/parsing/content.ts +1 -1
- package/src/parser/rules/block/parsing/inline-content.ts +6 -3
- package/src/parser/rules/block/table/pipe/cell.ts +3 -1
- package/src/parser/rules/block/table-block/cell-content/index.ts +1 -1
- package/src/parser/rules/block/table-block/cell-content/segments.ts +5 -2
- package/src/parser/rules/block/table-block/cell-newline.ts +2 -1
- package/src/parser/rules/contracts/rule.ts +4 -2
- package/src/parser/rules/contracts/scope.ts +2 -0
- package/src/parser/rules/inline/anchor/child.ts +8 -2
- package/src/parser/rules/inline/anchor/content.ts +3 -1
- package/src/parser/rules/inline/anchor/index.ts +4 -1
- package/src/parser/rules/inline/anchor/newline.ts +2 -1
- package/src/parser/rules/inline/autolink.ts +153 -0
- package/src/parser/rules/inline/button/attributes.ts +17 -0
- package/src/parser/rules/inline/button/index.ts +17 -0
- package/src/parser/rules/inline/button/syntax.ts +56 -0
- package/src/parser/rules/inline/date/index.ts +17 -0
- package/src/parser/rules/inline/date/syntax.ts +46 -0
- package/src/parser/rules/inline/email/candidates.ts +134 -0
- package/src/parser/rules/inline/email/index.ts +36 -0
- package/src/parser/rules/inline/email/scan.ts +76 -0
- package/src/parser/rules/inline/expr/branch.ts +3 -1
- package/src/parser/rules/inline/footnote/boundary.ts +56 -0
- package/src/parser/rules/inline/footnote/content.ts +29 -41
- package/src/parser/rules/inline/footnote/elements.ts +9 -34
- package/src/parser/rules/inline/footnote/index.ts +4 -1
- package/src/parser/rules/inline/formatting/close.ts +12 -0
- package/src/parser/rules/inline/index.ts +14 -0
- package/src/parser/rules/inline/line-break/newline.ts +8 -1
- package/src/parser/rules/inline/link-bracket/direct-url.ts +11 -3
- package/src/parser/rules/inline/link-bracket/parsed.ts +9 -4
- package/src/parser/rules/inline/link-bracket/parts.ts +14 -36
- package/src/parser/rules/inline/link-bracket/special-target.ts +9 -0
- package/src/parser/rules/inline/link-single.ts +9 -7
- package/src/parser/rules/inline/link-triple/index.ts +1 -0
- package/src/parser/rules/inline/link-triple/label.ts +7 -1
- package/src/parser/rules/inline/parsing/automatic-line-break.ts +35 -0
- package/src/parser/rules/inline/parsing/block-boundary.ts +2 -0
- package/src/parser/rules/inline/parsing/block-start-predicates.ts +10 -0
- package/src/parser/rules/inline/parsing/inline-content.ts +46 -7
- package/src/parser/rules/inline/parsing/plain-text.ts +7 -2
- package/src/parser/rules/inline/parsing/preserved-line-break.ts +13 -0
- package/src/parser/rules/inline/parsing/raw-tag.ts +19 -0
- package/src/parser/rules/inline/raw/angle.ts +2 -1
- package/src/parser/rules/inline/raw/end.ts +21 -1
- package/src/parser/rules/inline/size/content.ts +32 -5
- package/src/parser/rules/inline/size/value.ts +11 -0
- package/src/parser/rules/inline/social/index.ts +17 -0
- package/src/parser/rules/inline/social/syntax.ts +40 -0
- package/src/parser/rules/inline/span/content.ts +3 -1
- package/src/parser/rules/inline/span/newline.ts +2 -1
- package/src/parser/rules/opaque-probe.ts +58 -0
- package/src/pipeline/process.ts +13 -0
- package/src/pipeline/types.ts +19 -0
- package/src/parser/rules/block/math/content.ts +0 -54
- package/src/parser/rules/block/math/name.ts +0 -35
- package/src/parser/rules/inline/footnote/child.ts +0 -22
- package/src/parser/rules/inline/footnote/newline.ts +0 -27
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wdprlib/parser",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
4
4
|
"description": "Parser for Wikidot markup",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ast",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@braintree/sanitize-url": "^7.1.1",
|
|
47
|
-
"@wdprlib/ast": "
|
|
47
|
+
"@wdprlib/ast": "5.0.0",
|
|
48
|
+
"entities": "^7.0.1"
|
|
48
49
|
}
|
|
49
50
|
}
|
|
@@ -5,7 +5,7 @@ export const buildInfo: Readonly<{
|
|
|
5
5
|
sha: string | null;
|
|
6
6
|
dirty: boolean | null;
|
|
7
7
|
}> = Object.freeze({
|
|
8
|
-
version: "
|
|
9
|
-
sha: "
|
|
8
|
+
version: "6.0.0",
|
|
9
|
+
sha: "705ff97e22abd7bfb49d7f8f30087e6f09654c91",
|
|
10
10
|
dirty: false,
|
|
11
11
|
});
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
export { buildInfo } from "./build-info.generated";
|
|
24
|
+
export type { RatingRef, RatingVote, RatingAggregate, RatingState } from "@wdprlib/ast";
|
|
24
25
|
|
|
25
26
|
// Re-export AST types and utilities from @wdprlib/ast
|
|
26
27
|
export type {
|
|
@@ -120,11 +121,13 @@ export type {
|
|
|
120
121
|
// External data types
|
|
121
122
|
UserInfo,
|
|
122
123
|
PageData,
|
|
124
|
+
PageMetadataValue,
|
|
123
125
|
SiteContext,
|
|
124
126
|
ListPagesExternalData,
|
|
125
127
|
// Callback types
|
|
126
128
|
ListPagesDataFetcher,
|
|
127
129
|
DataProvider,
|
|
130
|
+
RatingsFetcher,
|
|
128
131
|
// Template types
|
|
129
132
|
VariableContext,
|
|
130
133
|
CompiledTemplate,
|
package/src/lexer/lexer.ts
CHANGED
|
@@ -50,6 +50,8 @@ export class Lexer {
|
|
|
50
50
|
* consume content up to the next `"` or newline.
|
|
51
51
|
*/
|
|
52
52
|
private blockOpenerDepth = 0;
|
|
53
|
+
private rawTagBounds: { source: string; close: number; outerDepth: number } | null = null;
|
|
54
|
+
private rawClosesExhausted = false;
|
|
53
55
|
|
|
54
56
|
constructor(source: string, options: LexerOptions = {}) {
|
|
55
57
|
this.options = {
|
|
@@ -92,8 +94,8 @@ export class Lexer {
|
|
|
92
94
|
* When invalid, returns the position of the closing ]] so the lexer can
|
|
93
95
|
* emit tokens that allow the inner [# text] to be parsed as a described link.
|
|
94
96
|
*/
|
|
95
|
-
private findInvalidAnchorNameEnd(): number | null {
|
|
96
|
-
return findInvalidAnchorNameEnd(
|
|
97
|
+
private findInvalidAnchorNameEnd(source: string): number | null {
|
|
98
|
+
return findInvalidAnchorNameEnd(source, this.state.pos);
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
/**
|
|
@@ -117,6 +119,24 @@ export class Lexer {
|
|
|
117
119
|
this.state.tokens.push(createLexerToken(this.state, type, value, this.options.trackPositions));
|
|
118
120
|
this.lastNonWhitespaceType = updateLastNonWhitespaceType(this.lastNonWhitespaceType, type);
|
|
119
121
|
this.blockOpenerDepth = nextBlockOpenerDepth(this.blockOpenerDepth, type);
|
|
122
|
+
if (
|
|
123
|
+
this.rawTagBounds === null &&
|
|
124
|
+
!this.rawClosesExhausted &&
|
|
125
|
+
(value.toLowerCase() === "button" || value.toLowerCase() === "social") &&
|
|
126
|
+
this.state.tokens.at(-2)?.type === "BLOCK_OPEN" &&
|
|
127
|
+
/\s/.test(this.current())
|
|
128
|
+
) {
|
|
129
|
+
const close = this.state.source.indexOf("]]", this.state.pos);
|
|
130
|
+
if (close < 0) {
|
|
131
|
+
this.rawClosesExhausted = true;
|
|
132
|
+
} else {
|
|
133
|
+
this.rawTagBounds = {
|
|
134
|
+
source: this.state.source.slice(0, close),
|
|
135
|
+
close,
|
|
136
|
+
outerDepth: this.blockOpenerDepth - 1,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
120
140
|
}
|
|
121
141
|
|
|
122
142
|
private emitTokenAction(action: TokenAction): void {
|
|
@@ -140,7 +160,16 @@ export class Lexer {
|
|
|
140
160
|
private scanToken(): void {
|
|
141
161
|
const char = this.current();
|
|
142
162
|
const isLineStart = isSyntaxLineStart(this.state);
|
|
143
|
-
const
|
|
163
|
+
const bounds = this.rawTagBounds;
|
|
164
|
+
const src = bounds?.source ?? this.state.source;
|
|
165
|
+
|
|
166
|
+
// Scanners see a bounded source so no token can consume part of the close.
|
|
167
|
+
if (bounds && this.state.pos === bounds.close) {
|
|
168
|
+
this.emitTokenAction({ type: "BLOCK_CLOSE", value: "]]", length: 2 });
|
|
169
|
+
this.blockOpenerDepth = bounds.outerDepth;
|
|
170
|
+
this.rawTagBounds = null;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
144
173
|
|
|
145
174
|
const spacingAction = scanSpacingToken(src, this.state.pos);
|
|
146
175
|
if (spacingAction) {
|
|
@@ -155,7 +184,7 @@ export class Lexer {
|
|
|
155
184
|
lineStart: isLineStart,
|
|
156
185
|
physicalLineStart: this.state.lineStart,
|
|
157
186
|
splitBlockClose: this.splitBlockClosePositions.has(this.state.pos),
|
|
158
|
-
findInvalidAnchorNameEnd: () => this.findInvalidAnchorNameEnd(),
|
|
187
|
+
findInvalidAnchorNameEnd: () => this.findInvalidAnchorNameEnd(src),
|
|
159
188
|
});
|
|
160
189
|
if (punctuation.handled) {
|
|
161
190
|
if (punctuation.clearSplitBlockCloseAt !== undefined) {
|
|
@@ -176,7 +205,7 @@ export class Lexer {
|
|
|
176
205
|
if (char === '"') {
|
|
177
206
|
const lastNonWs = this.lastNonWhitespaceTokenType();
|
|
178
207
|
if (this.blockOpenerDepth > 0 && lastNonWs === "EQUALS") {
|
|
179
|
-
this.addToken("QUOTED_STRING", scanQuotedString(this.state));
|
|
208
|
+
this.addToken("QUOTED_STRING", scanQuotedString(this.state, src.length));
|
|
180
209
|
return;
|
|
181
210
|
}
|
|
182
211
|
this.advance();
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { advance, current,
|
|
1
|
+
import { advance, current, type LexerState } from "./state";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Scan a quoted block-attribute value, including the opening quote and optional
|
|
5
5
|
* closing quote. Newline terminates the token without being consumed.
|
|
6
6
|
*/
|
|
7
|
-
export function scanQuotedString(state: LexerState): string {
|
|
7
|
+
export function scanQuotedString(state: LexerState, end: number = state.source.length): string {
|
|
8
8
|
let quoted = advance(state);
|
|
9
|
-
while (
|
|
9
|
+
while (state.pos < end && current(state) !== '"' && current(state) !== "\n") {
|
|
10
10
|
quoted += advance(state);
|
|
11
11
|
}
|
|
12
|
-
if (current(state) === '"') {
|
|
12
|
+
if (state.pos < end && current(state) === '"') {
|
|
13
13
|
quoted += advance(state);
|
|
14
14
|
}
|
|
15
15
|
return quoted;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TokenAction } from "./token-actions";
|
|
2
2
|
import { findRepeatedCharRunEnd } from "./runs";
|
|
3
3
|
import type { TokenType } from "./tokens";
|
|
4
|
+
import { startsWithUrl } from "./url-schemes";
|
|
4
5
|
|
|
5
6
|
export function scanSimpleSyntaxToken(
|
|
6
7
|
src: string,
|
|
@@ -42,7 +43,11 @@ export function scanSimpleSyntaxToken(
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
function scanStarToken(src: string, pos: number, isLineStart: boolean): TokenAction {
|
|
45
|
-
|
|
46
|
+
// `**http://x…` は太字にならない。Wikidotは2つ目の`*`をURLの新規タブプレフィックスとして
|
|
47
|
+
// 扱い、開きの`**`ペアが壊れる(`*` リテラル + `*http://x…` autolink + 末尾`**` リテラル)。
|
|
48
|
+
// 直後が有効なURLのときだけBOLD_MARKERに結合せず単一の`*`として切り出す。
|
|
49
|
+
// `**http://**`のようにURL本体が無い場合はautolinkが成立しないため太字のまま扱う
|
|
50
|
+
if (src[pos + 1] === "*" && !startsWithUrl(src, pos + 2)) {
|
|
46
51
|
return token("BOLD_MARKER", "**");
|
|
47
52
|
}
|
|
48
53
|
return isLineStart ? token("LIST_BULLET", "*") : token("STAR", "*");
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
isAsciiAlphanumericCode,
|
|
6
6
|
} from "./runs";
|
|
7
7
|
import type { TokenAction } from "./token-actions";
|
|
8
|
+
import { TRAILING_URL_SCHEME } from "./url-schemes";
|
|
8
9
|
|
|
9
10
|
export function scanTextToken(src: string, pos: number): TokenAction {
|
|
10
11
|
const char = src[pos] ?? "";
|
|
@@ -28,7 +29,20 @@ export function scanTextToken(src: string, pos: number): TokenAction {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
export function scanCompactTextToken(src: string, pos: number): TokenAction | null {
|
|
31
|
-
|
|
32
|
+
let end = findCompactPlainTextRunEnd(src, pos);
|
|
33
|
+
|
|
34
|
+
// 生URLの自動リンク化はIDENTIFIERトークンのスキーム名から発火する。
|
|
35
|
+
// このcompactモード(大きなソースで連続する平文を1つのTEXTトークンにまとめる高速化)が
|
|
36
|
+
// `see http` のようにスキーム名までTEXTに取り込むとリンク化されなくなるため、
|
|
37
|
+
// 次の文字が`:`でトークンがスキーム名で終わる場合はスキーム名の手前で切り、
|
|
38
|
+
// スキーム名を通常のIDENTIFIERスキャンに委ねる
|
|
39
|
+
if (end > pos && src[end] === ":") {
|
|
40
|
+
const match = TRAILING_URL_SCHEME.exec(src.slice(pos, end));
|
|
41
|
+
if (match) {
|
|
42
|
+
end -= match[1]!.length;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
return end > pos ? runToken(src, pos, end, "TEXT") : null;
|
|
33
47
|
}
|
|
34
48
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* URL schemes recognized for bare-URL auto-linking.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors Text_Wiki's Url rule configuration (http://, https://, ftp://,
|
|
6
|
+
* gopher://, news://, mailto:, mms://). The scheme names are shared between
|
|
7
|
+
* the lexer (compact text-run rewinding) and the autolink inline rule.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Scheme names (without `:`) that can start an auto-linked bare URL. */
|
|
13
|
+
export const URL_SCHEME_NAMES: ReadonlySet<string> = new Set([
|
|
14
|
+
"http",
|
|
15
|
+
"https",
|
|
16
|
+
"ftp",
|
|
17
|
+
"gopher",
|
|
18
|
+
"news",
|
|
19
|
+
"mailto",
|
|
20
|
+
"mms",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Matches a compact text run that ends with a URL scheme name.
|
|
25
|
+
*
|
|
26
|
+
* The boundary is `[^A-Za-z0-9]` (not Text_Wiki's `[^A-Za-z]`) so that runs
|
|
27
|
+
* like `123http` stay fused, matching the non-compact lexer which tokenizes
|
|
28
|
+
* `123http` as a single IDENTIFIER (such URLs are not auto-linked either way).
|
|
29
|
+
*/
|
|
30
|
+
export const TRAILING_URL_SCHEME: RegExp = /(?:^|[^A-Za-z0-9])(https?|ftp|gopher|news|mailto|mms)$/;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Matches the exact scheme prefix the autolink rule consumes:
|
|
34
|
+
* `://` for http/https/ftp/gopher/news/mms and `:` for mailto. Must stay in
|
|
35
|
+
* sync with the autolink rule's URL pattern (a looser `scheme:` test would
|
|
36
|
+
* split `**` markers for inputs like `**http:foo**` that never autolink,
|
|
37
|
+
* turning them into literal `**` instead of bold).
|
|
38
|
+
*/
|
|
39
|
+
const URL_SCHEME_PREFIX = /^(?:(?:https?|ftp|gopher|news|mms):\/\/|mailto:)/;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* URL body pattern, ported from Text_Wiki's Url rule regex: scheme + zero or
|
|
43
|
+
* more slash-terminated segments + final segment + one terminating character
|
|
44
|
+
* from `[A-Za-z%0-9/?=&~_]`.
|
|
45
|
+
*
|
|
46
|
+
* Scheme matching is case-sensitive (lowercase only) on purpose: Text_Wiki's
|
|
47
|
+
* inline URL regex has no `i` flag and lists lowercase schemes, so Wikidot does
|
|
48
|
+
* not auto-link `HTTP://…`. Shared by the autolink rule and the lexer's bold
|
|
49
|
+
* marker split.
|
|
50
|
+
*/
|
|
51
|
+
export const URL_PATTERN: RegExp =
|
|
52
|
+
/^(?:(?:https?|ftp|gopher|news|mms):\/\/|mailto:)(?:[^ \\/"']*\/)*[^ \t\n\\/"']*[A-Za-z%0-9/?=&~_]/;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Whether `src` begins a URL scheme prefix (e.g. `http://`) at `pos`.
|
|
56
|
+
*
|
|
57
|
+
* A prefix-only check, used for single-bracket links (`[ftp://x Label]`) whose
|
|
58
|
+
* URL portion is already delimited by the label, so the full URL body does not
|
|
59
|
+
* need to be validated here.
|
|
60
|
+
*/
|
|
61
|
+
export function startsWithUrlScheme(src: string, pos: number): boolean {
|
|
62
|
+
return URL_SCHEME_PREFIX.test(src.slice(pos, pos + 10));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Whether a complete autolinkable URL (not just a scheme prefix) begins at
|
|
67
|
+
* `pos` in `src`.
|
|
68
|
+
*
|
|
69
|
+
* Used by the lexer to decide whether `**` before a scheme is a bold marker or
|
|
70
|
+
* a literal `*` plus a new-tab URL prefix: only split the marker when a valid
|
|
71
|
+
* URL actually follows, so `**http://**` (no URL body) stays bold while
|
|
72
|
+
* `**http://x**` splits into `*` + autolink + `**`.
|
|
73
|
+
*/
|
|
74
|
+
export function startsWithUrl(src: string, pos: number): boolean {
|
|
75
|
+
// Scheme + a short body is enough to decide; cap the slice so a very long
|
|
76
|
+
// line does not get copied on every `**`.
|
|
77
|
+
return URL_PATTERN.test(src.slice(pos, pos + 2048));
|
|
78
|
+
}
|
package/src/parser/constants.ts
CHANGED
|
@@ -28,6 +28,10 @@ export const BLOCK_START_TOKENS: TokenType[] = [
|
|
|
28
28
|
"HEADING_MARKER",
|
|
29
29
|
"HR_MARKER",
|
|
30
30
|
"TABLE_MARKER",
|
|
31
|
+
"TABLE_HEADER",
|
|
32
|
+
"TABLE_LEFT",
|
|
33
|
+
"TABLE_CENTER",
|
|
34
|
+
"TABLE_RIGHT",
|
|
31
35
|
"COLON", // Definition list
|
|
32
36
|
"BLOCK_OPEN", // [[footnoteblock]], [[div]], etc.
|
|
33
37
|
"BLOCK_END_OPEN", // [[/div]], [[/collapsible]], etc.
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* In Wikidot, when a paragraph is a direct sibling of a `<div>` block (no other
|
|
6
|
-
* block elements between them), the `<p>` wrapping is removed and the inner
|
|
7
|
-
* elements are promoted to the parent level.
|
|
8
|
-
*
|
|
9
|
-
* When the unwrapped paragraph follows a div, a line-break element is prepended
|
|
10
|
-
* to represent the newline between the closing `</div>` and the bare text.
|
|
11
|
-
*
|
|
12
|
-
* Examples:
|
|
13
|
-
* `[[div]]inline[[/div]]\n[[div]]\n[[/div]]` → no `<p>` (adjacent to div)
|
|
14
|
-
* `[[div]]inline[[/div]]\n> a\n[[div]]\n[[/div]]` → has `<p>` (blockquote between)
|
|
15
|
-
*
|
|
16
|
-
* @module
|
|
2
|
+
* Preserve the bare rendering of unparsed div syntax beside a valid div.
|
|
3
|
+
* Ordinary paragraphs retain their wrappers, including beside div containers.
|
|
17
4
|
*/
|
|
18
5
|
import type { Element, ContainerData } from "@wdprlib/ast";
|
|
19
6
|
|
|
20
|
-
|
|
7
|
+
// Track the syntax node rather than its paragraph so splitting a paragraph
|
|
8
|
+
// cannot mark a separate fragment containing only ordinary text.
|
|
9
|
+
const unparsedDivStarts = new WeakSet<Element>();
|
|
10
|
+
|
|
11
|
+
export function markUnparsedDivStart(elements: Element[]): void {
|
|
12
|
+
const first = elements[0];
|
|
13
|
+
const text =
|
|
14
|
+
first?.element === "container" && first.data.type === "paragraph"
|
|
15
|
+
? first.data.elements[0]
|
|
16
|
+
: first;
|
|
17
|
+
if (text?.element === "text") unparsedDivStarts.add(text);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isUnparsedDivParagraph(el: Element | undefined): boolean {
|
|
21
21
|
if (!el || el.element !== "container") return false;
|
|
22
|
-
return (
|
|
22
|
+
return (
|
|
23
|
+
el.data.type === "paragraph" && el.data.elements.some((child) => unparsedDivStarts.has(child))
|
|
24
|
+
);
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
function isDivContainer(el: Element | undefined): boolean {
|
|
@@ -28,8 +30,8 @@ function isDivContainer(el: Element | undefined): boolean {
|
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
/**
|
|
31
|
-
* At a single nesting level, unwrap
|
|
32
|
-
* adjacent to div containers. A line-break is prepended when the paragraph
|
|
33
|
+
* At a single nesting level, unwrap paragraphs containing unparsed div syntax
|
|
34
|
+
* that are directly adjacent to div containers. A line-break is prepended when the paragraph
|
|
33
35
|
* follows a div.
|
|
34
36
|
*/
|
|
35
37
|
function suppressAtLevel(elements: Element[]): Element[] {
|
|
@@ -38,7 +40,7 @@ function suppressAtLevel(elements: Element[]): Element[] {
|
|
|
38
40
|
const unwrap = Array.from({ length: elements.length }, () => false);
|
|
39
41
|
|
|
40
42
|
for (let i = 0; i < elements.length; i++) {
|
|
41
|
-
if (!
|
|
43
|
+
if (!isUnparsedDivParagraph(elements[i])) continue;
|
|
42
44
|
const prevIsDiv = i > 0 && isDivContainer(elements[i - 1]);
|
|
43
45
|
const nextIsDiv = i < elements.length - 1 && isDivContainer(elements[i + 1]);
|
|
44
46
|
if (prevIsDiv || nextIsDiv) {
|
|
@@ -66,7 +68,7 @@ function suppressAtLevel(elements: Element[]): Element[] {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
/**
|
|
69
|
-
* Suppress
|
|
71
|
+
* Suppress wrapping of unparsed div syntax adjacent to div containers.
|
|
70
72
|
*
|
|
71
73
|
* Applied only at the top level. Inside div containers, paragraphs adjacent
|
|
72
74
|
* to nested divs retain their `<p>` wrapping (matching Wikidot behavior).
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
isContainer,
|
|
8
8
|
isEmptyExpr,
|
|
9
9
|
isSplitSpan,
|
|
10
|
+
isSpanStripMarker,
|
|
11
|
+
isWhitespaceText,
|
|
10
12
|
} from "./predicates";
|
|
11
13
|
import { splitParagraphAtBlankLineSpans, splitParagraphAtEmptyExpr } from "./split";
|
|
12
14
|
|
|
@@ -40,6 +42,10 @@ export function mergeSpanStripParagraphs(children: Element[]): Element[] {
|
|
|
40
42
|
continue;
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
const firstMeaningful = paraData.elements.find(
|
|
46
|
+
(child) => child.element !== "line-break" && !isWhitespaceText(child),
|
|
47
|
+
);
|
|
48
|
+
const keepParagraph = firstMeaningful && !isSpanStripMarker(firstMeaningful);
|
|
43
49
|
const mergedChildren: Element[] = [...paraData.elements];
|
|
44
50
|
i++;
|
|
45
51
|
|
|
@@ -55,7 +61,7 @@ export function mergeSpanStripParagraphs(children: Element[]): Element[] {
|
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
const hasSpanStrip = hasParagraphStripSpan(nextPara);
|
|
58
|
-
|
|
64
|
+
for (const element of nextParaData.elements) mergedChildren.push(element);
|
|
59
65
|
i++;
|
|
60
66
|
|
|
61
67
|
if (!hasSpanStrip) {
|
|
@@ -69,7 +75,7 @@ export function mergeSpanStripParagraphs(children: Element[]): Element[] {
|
|
|
69
75
|
const escapedSpans = extractEscapedSpans(mergedChildren);
|
|
70
76
|
removeLineBreaksAroundSpanStrip(mergedChildren);
|
|
71
77
|
|
|
72
|
-
if (escapedSpans.length > 0) {
|
|
78
|
+
if (escapedSpans.length > 0 || keepParagraph) {
|
|
73
79
|
if (mergedChildren.length > 0) {
|
|
74
80
|
result.push(paragraphElement(mergedChildren));
|
|
75
81
|
}
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
* @module
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
+
import { makeUniqueSentinels, maskRawRegions, restorePlaceholders } from "./utils";
|
|
23
|
+
|
|
22
24
|
/** Unicode left single quotation mark (U+2018) */
|
|
23
25
|
const LEFT_SINGLE_QUOTE = "\u2018"; // '
|
|
24
26
|
/** Unicode right single quotation mark (U+2019) */
|
|
@@ -66,14 +68,24 @@ function replaceDelimitedTypography(
|
|
|
66
68
|
let searchFrom = 0;
|
|
67
69
|
let result = "";
|
|
68
70
|
let lastCopied = 0;
|
|
71
|
+
let closeIndex = -1;
|
|
72
|
+
let newlineIndex = -1;
|
|
69
73
|
|
|
70
74
|
while (searchFrom < text.length) {
|
|
71
75
|
const openIndex = text.indexOf(opener, searchFrom);
|
|
72
76
|
if (openIndex === -1) break;
|
|
73
77
|
|
|
74
78
|
const contentStart = openIndex + opener.length;
|
|
75
|
-
|
|
79
|
+
if (closeIndex < contentStart) closeIndex = text.indexOf(closer, contentStart);
|
|
76
80
|
if (closeIndex === -1) break;
|
|
81
|
+
if (newlineIndex < contentStart) {
|
|
82
|
+
const nextNewline = text.indexOf("\n", contentStart);
|
|
83
|
+
newlineIndex = nextNewline === -1 ? text.length : nextNewline;
|
|
84
|
+
}
|
|
85
|
+
if (newlineIndex < closeIndex) {
|
|
86
|
+
searchFrom = newlineIndex + 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
77
89
|
|
|
78
90
|
result += text.slice(lastCopied, openIndex);
|
|
79
91
|
result += leftQuote;
|
|
@@ -99,7 +111,17 @@ function replaceDelimitedTypography(
|
|
|
99
111
|
* @returns Text with ASCII typography patterns replaced by Unicode equivalents
|
|
100
112
|
*/
|
|
101
113
|
export function substitute(text: string): string {
|
|
102
|
-
|
|
114
|
+
if (
|
|
115
|
+
!text.includes("`") &&
|
|
116
|
+
!text.includes(",,") &&
|
|
117
|
+
!text.includes("...") &&
|
|
118
|
+
!text.includes(". . .")
|
|
119
|
+
) {
|
|
120
|
+
return text;
|
|
121
|
+
}
|
|
122
|
+
const sentinels = makeUniqueSentinels(text);
|
|
123
|
+
const { masked, placeholders } = maskRawRegions(text, sentinels);
|
|
124
|
+
let result = masked;
|
|
103
125
|
|
|
104
126
|
// Double quotes: ``...'' -> "..."
|
|
105
127
|
if (result.includes("``") && result.includes("''")) {
|
|
@@ -128,5 +150,5 @@ export function substitute(text: string): string {
|
|
|
128
150
|
result = replaceExactEllipsisPattern(result, ". . .");
|
|
129
151
|
}
|
|
130
152
|
|
|
131
|
-
return result;
|
|
153
|
+
return restorePlaceholders(result, placeholders, sentinels);
|
|
132
154
|
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { tokenize, type Token } from "../../../lexer";
|
|
2
|
+
import { findCodeOpen } from "../../rules/block/code/open";
|
|
3
|
+
import { findCodeBodyBounds } from "../../rules/block/code/boundary";
|
|
4
|
+
|
|
1
5
|
const BASE_PLACEHOLDER_OPEN = "\uE000";
|
|
2
6
|
const BASE_PLACEHOLDER_CLOSE = "\uE001";
|
|
3
7
|
|
|
4
|
-
const RAW_BLOCK_OPEN_PATTERN = /\[\[
|
|
8
|
+
const RAW_BLOCK_OPEN_PATTERN = /\[\[html\b[^\]]*\]\]/iy;
|
|
5
9
|
|
|
6
10
|
/** Unique sentinel characters used to wrap raw-region placeholders. */
|
|
7
11
|
export interface Sentinels {
|
|
@@ -43,11 +47,23 @@ export function maskRawRegions(
|
|
|
43
47
|
sentinels: Sentinels,
|
|
44
48
|
): { masked: string; placeholders: string[] } {
|
|
45
49
|
const placeholders: string[] = [];
|
|
50
|
+
let tokens: Token[] | undefined;
|
|
51
|
+
const getTokens = () => (tokens ??= tokenize(source));
|
|
46
52
|
let masked = "";
|
|
47
53
|
let i = 0;
|
|
48
54
|
|
|
49
55
|
while (i < source.length) {
|
|
50
|
-
|
|
56
|
+
if (source.startsWith("[!--", i)) {
|
|
57
|
+
const close = source.indexOf("--]", i + 4);
|
|
58
|
+
if (close !== -1) {
|
|
59
|
+
const end = close + 3;
|
|
60
|
+
masked += source.slice(i, end);
|
|
61
|
+
i = end;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const rawBlock = tryMaskRawBlock(source, i, placeholders, sentinels, getTokens);
|
|
51
67
|
if (rawBlock) {
|
|
52
68
|
masked += rawBlock.placeholder;
|
|
53
69
|
i = rawBlock.end;
|
|
@@ -86,16 +102,30 @@ function tryMaskRawBlock(
|
|
|
86
102
|
pos: number,
|
|
87
103
|
placeholders: string[],
|
|
88
104
|
sentinels: Sentinels,
|
|
105
|
+
getTokens: () => Token[],
|
|
89
106
|
): { placeholder: string; end: number } | null {
|
|
90
107
|
if (source[pos] !== "[" || source[pos + 1] !== "[") return null;
|
|
91
108
|
|
|
109
|
+
if (source.slice(pos, pos + 6).toLowerCase() === "[[code") {
|
|
110
|
+
const tokens = getTokens();
|
|
111
|
+
const start = tokenAtOffset(tokens, pos);
|
|
112
|
+
const open = findCodeOpen(tokens, start);
|
|
113
|
+
if (!open) return null;
|
|
114
|
+
const bounds = open.closingSwallowed ? null : findCodeBodyBounds(tokens, open.bodyStart);
|
|
115
|
+
const end = open.closingSwallowed
|
|
116
|
+
? tokens[open.attributesEnd - 1]!.position.end.offset
|
|
117
|
+
: bounds!.foundClose
|
|
118
|
+
? tokens[bounds!.end - 1]!.position.end.offset
|
|
119
|
+
: source.length;
|
|
120
|
+
return { placeholder: pushPlaceholder(placeholders, source.slice(pos, end), sentinels), end };
|
|
121
|
+
}
|
|
122
|
+
|
|
92
123
|
RAW_BLOCK_OPEN_PATTERN.lastIndex = pos;
|
|
93
124
|
const openMatch = RAW_BLOCK_OPEN_PATTERN.exec(source);
|
|
94
125
|
if (!openMatch) return null;
|
|
95
126
|
|
|
96
|
-
const name = openMatch[1]!.toLowerCase();
|
|
97
127
|
const openLen = openMatch[0].length;
|
|
98
|
-
const closePattern =
|
|
128
|
+
const closePattern = /\[\[\/\s*html\s*\]\]/gi;
|
|
99
129
|
closePattern.lastIndex = pos + openLen;
|
|
100
130
|
const closeMatch = closePattern.exec(source);
|
|
101
131
|
|
|
@@ -107,12 +137,18 @@ function tryMaskRawBlock(
|
|
|
107
137
|
};
|
|
108
138
|
}
|
|
109
139
|
|
|
110
|
-
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
111
142
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
143
|
+
function tokenAtOffset(tokens: readonly Token[], offset: number): number {
|
|
144
|
+
let low = 0;
|
|
145
|
+
let high = tokens.length;
|
|
146
|
+
while (low < high) {
|
|
147
|
+
const middle = Math.floor((low + high) / 2);
|
|
148
|
+
if (tokens[middle]!.position.start.offset < offset) low = middle + 1;
|
|
149
|
+
else high = middle;
|
|
150
|
+
}
|
|
151
|
+
return tokens[low]?.position.start.offset === offset ? low : tokens.length;
|
|
116
152
|
}
|
|
117
153
|
|
|
118
154
|
function tryMaskRawInline(
|
|
@@ -140,17 +176,26 @@ function tryMaskSingleLineRaw(
|
|
|
140
176
|
placeholders: string[],
|
|
141
177
|
sentinels: Sentinels,
|
|
142
178
|
): { placeholder: string; end: number } | null {
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
if (closePos === -1 || (newline !== -1 && newline < closePos)) return null;
|
|
146
|
-
|
|
147
|
-
const end = closePos + close.length;
|
|
179
|
+
const end = singleLineRawEnd(source, pos, openerLength, close);
|
|
180
|
+
if (end === pos) return null;
|
|
148
181
|
return {
|
|
149
182
|
placeholder: pushPlaceholder(placeholders, source.slice(pos, end), sentinels),
|
|
150
183
|
end,
|
|
151
184
|
};
|
|
152
185
|
}
|
|
153
186
|
|
|
187
|
+
function singleLineRawEnd(
|
|
188
|
+
source: string,
|
|
189
|
+
pos: number,
|
|
190
|
+
openerLength: number,
|
|
191
|
+
close: string,
|
|
192
|
+
): number {
|
|
193
|
+
const closePos = source.indexOf(close, pos + openerLength);
|
|
194
|
+
const newline = source.indexOf("\n", pos + openerLength);
|
|
195
|
+
if (closePos === -1 || (newline !== -1 && newline < closePos)) return pos;
|
|
196
|
+
return closePos + close.length;
|
|
197
|
+
}
|
|
198
|
+
|
|
154
199
|
function pushPlaceholder(placeholders: string[], text: string, sentinels: Sentinels): string {
|
|
155
200
|
const idx = placeholders.length;
|
|
156
201
|
placeholders.push(text);
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { needsWhitespaceSubstitution, mayContainWhitespaceOnlyLine } from "./detection";
|
|
14
14
|
import { replaceLeadingSpaces } from "./leading-spaces";
|
|
15
15
|
import { CONCAT_LINES, DOS_MAC_NEWLINES, NULL_CHARS, TABS, WHITESPACE_ONLY_LINE } from "./patterns";
|
|
16
|
+
import { makeUniqueSentinels, maskRawRegions, restorePlaceholders } from "../utils";
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Apply all whitespace normalization substitutions to the given text.
|
|
@@ -41,7 +42,13 @@ export function substitute(text: string): string {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
if (result.indexOf("\\\n") !== -1) {
|
|
44
|
-
|
|
45
|
+
const sentinels = makeUniqueSentinels(result);
|
|
46
|
+
const { masked, placeholders } = maskRawRegions(result, sentinels);
|
|
47
|
+
result = restorePlaceholders(
|
|
48
|
+
masked.replace(CONCAT_LINES, String.fromCharCode(0xe000)),
|
|
49
|
+
placeholders,
|
|
50
|
+
sentinels,
|
|
51
|
+
);
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
if (result.indexOf("\t") !== -1) {
|
|
@@ -60,7 +60,7 @@ export function parseBibliographyContent(
|
|
|
60
60
|
const inlineCtx: ParseContext = { ...ctx, pos };
|
|
61
61
|
const result = parseInlineUntil(inlineCtx, "NEWLINE");
|
|
62
62
|
if (result.elements.length > 0) {
|
|
63
|
-
|
|
63
|
+
for (const element of result.elements) content.push(element);
|
|
64
64
|
pos += result.consumed;
|
|
65
65
|
consumed += result.consumed;
|
|
66
66
|
} else {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stripAutomaticLineBreak } from "../../inline/parsing/automatic-line-break";
|
|
1
2
|
import type { ListItem } from "@wdprlib/ast";
|
|
2
3
|
import type { ParseContext } from "../../types";
|
|
3
4
|
import { getCandidateInlineRules } from "../../inline/utils";
|
|
@@ -52,7 +53,7 @@ export function parseBareListContent(
|
|
|
52
53
|
if (consecutiveNewlines >= 2) {
|
|
53
54
|
flushBareParagraph(paragraphState);
|
|
54
55
|
} else {
|
|
55
|
-
appendBareParagraphLineBreakIfNeeded(paragraphState);
|
|
56
|
+
appendBareParagraphLineBreakIfNeeded(paragraphState, token);
|
|
56
57
|
}
|
|
57
58
|
continue;
|
|
58
59
|
}
|
|
@@ -66,6 +67,7 @@ export function parseBareListContent(
|
|
|
66
67
|
for (const rule of getCandidateInlineRules(ctx.inlineRules, token.type)) {
|
|
67
68
|
const result = rule.parse(inlineCtx);
|
|
68
69
|
if (result.success) {
|
|
70
|
+
stripAutomaticLineBreak(paragraphState.current, result.stripLeadingLineBreak);
|
|
69
71
|
appendBareParagraphElements(paragraphState, result.elements);
|
|
70
72
|
consumed += result.consumed;
|
|
71
73
|
pos += result.consumed;
|