@wdprlib/parser 4.4.0 → 5.1.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 +400 -175
- package/dist/index.d.cts +16 -13
- package/dist/index.d.ts +16 -13
- package/dist/index.js +388 -161
- package/package.json +2 -2
- package/src/index.ts +2 -0
- package/src/parser/parse/context.ts +7 -2
- package/src/parser/parse/index.ts +21 -3
- package/src/parser/parse/parser.ts +37 -28
- package/src/parser/preprocess/expr/index.ts +26 -0
- package/src/parser/preprocess/utils/raw-regions.ts +16 -7
- package/src/parser/rules/block/include/index.ts +20 -2
- package/src/parser/rules/block/module/include/resolve/index.ts +28 -0
- package/src/parser/rules/block/module/include/resolve/iterate.ts +28 -4
- package/src/parser/rules/block/module/include/scanner.ts +58 -1
- package/src/parser/rules/block/module/index.ts +2 -0
- package/src/parser/rules/block/module/listpages/index.ts +2 -0
- package/src/parser/rules/block/module/listpages/selectors.ts +64 -0
- package/src/parser/rules/block/module/listpages/types/external-data.ts +22 -0
- package/src/parser/rules/contracts/parse-context.ts +2 -0
- package/src/pipeline/process.ts +118 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wdprlib/parser",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.1.0",
|
|
4
4
|
"description": "Parser for Wikidot markup",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ast",
|
|
@@ -41,6 +41,6 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@braintree/sanitize-url": "^7.1.1",
|
|
44
|
-
"@wdprlib/ast": "
|
|
44
|
+
"@wdprlib/ast": "3.0.0"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { DEFAULT_SETTINGS } from "@wdprlib/ast";
|
|
1
|
+
import { DEFAULT_SETTINGS, type PageRef } from "@wdprlib/ast";
|
|
2
2
|
import type { Token } from "../../lexer";
|
|
3
3
|
import { blockFallbackRule, blockRules, inlineRules, type ParseContext } from "../rules";
|
|
4
4
|
import type { ParserOptions } from "./options";
|
|
5
5
|
|
|
6
|
-
export function createParseContext(
|
|
6
|
+
export function createParseContext(
|
|
7
|
+
tokens: Token[],
|
|
8
|
+
options: ParserOptions = {},
|
|
9
|
+
deferInclude?: (location: PageRef) => boolean,
|
|
10
|
+
): ParseContext {
|
|
7
11
|
return {
|
|
8
12
|
tokens,
|
|
9
13
|
pos: 0,
|
|
@@ -11,6 +15,7 @@ export function createParseContext(tokens: Token[], options: ParserOptions = {})
|
|
|
11
15
|
trackPositions: options.trackPositions ?? true,
|
|
12
16
|
settings: options.settings ?? DEFAULT_SETTINGS,
|
|
13
17
|
appendImplicitFootnoteBlock: options.appendImplicitFootnoteBlock ?? true,
|
|
18
|
+
deferInclude,
|
|
14
19
|
footnotes: [],
|
|
15
20
|
tocEntries: [],
|
|
16
21
|
codeBlocks: [],
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ParseResult } from "@wdprlib/ast";
|
|
1
|
+
import type { PageRef, ParseResult } from "@wdprlib/ast";
|
|
2
2
|
import { tokenize } from "../../lexer";
|
|
3
|
-
import { Parser } from "./parser";
|
|
3
|
+
import { Parser, parseTokensWithIncludeDeferral } from "./parser";
|
|
4
4
|
import { parseLargePlainTextDocument, parsePlainNonAsciiDocument } from "./plain-non-ascii";
|
|
5
5
|
import { prepareSourceForParse } from "./source";
|
|
6
6
|
import type { ParserOptions } from "./options";
|
|
@@ -23,6 +23,22 @@ export { Parser } from "./parser";
|
|
|
23
23
|
* @since 2.0.0
|
|
24
24
|
*/
|
|
25
25
|
export function parse(source: string, options?: ParserOptions): ParseResult {
|
|
26
|
+
return parseSource(source, options);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseWithIncludeDeferral(
|
|
30
|
+
source: string,
|
|
31
|
+
options: ParserOptions,
|
|
32
|
+
deferInclude: (location: PageRef) => boolean,
|
|
33
|
+
): ParseResult {
|
|
34
|
+
return parseSource(source, options, deferInclude);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseSource(
|
|
38
|
+
source: string,
|
|
39
|
+
options?: ParserOptions,
|
|
40
|
+
deferInclude?: (location: PageRef) => boolean,
|
|
41
|
+
): ParseResult {
|
|
26
42
|
const plainResult = parsePlainNonAsciiDocument(source);
|
|
27
43
|
if (plainResult) {
|
|
28
44
|
return plainResult;
|
|
@@ -38,5 +54,7 @@ export function parse(source: string, options?: ParserOptions): ParseResult {
|
|
|
38
54
|
trackPositions: options?.trackPositions,
|
|
39
55
|
compactTextRuns: preprocessed.length >= COMPACT_TEXT_RUN_SOURCE_LENGTH,
|
|
40
56
|
});
|
|
41
|
-
return
|
|
57
|
+
return deferInclude
|
|
58
|
+
? parseTokensWithIncludeDeferral(tokens, options ?? {}, deferInclude)
|
|
59
|
+
: new Parser(tokens, options).parse();
|
|
42
60
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Element, ParseResult } from "@wdprlib/ast";
|
|
1
|
+
import type { Element, PageRef, ParseResult } from "@wdprlib/ast";
|
|
2
2
|
import type { Token } from "../../lexer";
|
|
3
3
|
import type { ParseContext } from "../rules";
|
|
4
4
|
import type { ParserOptions } from "./options";
|
|
@@ -33,26 +33,35 @@ export class Parser {
|
|
|
33
33
|
* @since 2.0.0
|
|
34
34
|
*/
|
|
35
35
|
parse(): ParseResult {
|
|
36
|
-
|
|
36
|
+
return parseContext(this.ctx);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
export function parseTokensWithIncludeDeferral(
|
|
41
|
+
tokens: Token[],
|
|
42
|
+
options: ParserOptions,
|
|
43
|
+
deferInclude: (location: PageRef) => boolean,
|
|
44
|
+
): ParseResult {
|
|
45
|
+
return parseContext(createParseContext(tokens, options, deferInclude));
|
|
46
|
+
}
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
function parseContext(ctx: ParseContext): ParseResult {
|
|
49
|
+
const children: Element[] = [];
|
|
45
50
|
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
while (!isAtEnd(ctx)) {
|
|
52
|
+
children.push(...parseBlock(ctx));
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
55
|
+
return finalizeParseResult(ctx, children);
|
|
56
|
+
}
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
function isAtEnd(ctx: ParseContext): boolean {
|
|
59
|
+
return ctx.pos >= ctx.tokens.length || currentToken(ctx).type === "EOF";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function currentToken(ctx: ParseContext): Token {
|
|
63
|
+
return (
|
|
64
|
+
ctx.tokens[ctx.pos] ?? {
|
|
56
65
|
type: "EOF",
|
|
57
66
|
value: "",
|
|
58
67
|
position: {
|
|
@@ -60,20 +69,20 @@ export class Parser {
|
|
|
60
69
|
end: { line: 0, column: 0, offset: 0 },
|
|
61
70
|
},
|
|
62
71
|
lineStart: false,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
private skipWhitespace(): void {
|
|
67
|
-
while (this.currentToken().type === "WHITESPACE") {
|
|
68
|
-
this.ctx.pos++;
|
|
69
72
|
}
|
|
70
|
-
|
|
73
|
+
);
|
|
74
|
+
}
|
|
71
75
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
() => this.skipWhitespace(),
|
|
76
|
-
() => this.isAtEnd(),
|
|
77
|
-
);
|
|
76
|
+
function skipWhitespace(ctx: ParseContext): void {
|
|
77
|
+
while (currentToken(ctx).type === "WHITESPACE") {
|
|
78
|
+
ctx.pos++;
|
|
78
79
|
}
|
|
79
80
|
}
|
|
81
|
+
|
|
82
|
+
function parseBlock(ctx: ParseContext): Element[] {
|
|
83
|
+
return parseNextBlock(
|
|
84
|
+
ctx,
|
|
85
|
+
() => skipWhitespace(ctx),
|
|
86
|
+
() => isAtEnd(ctx),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -7,8 +7,11 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { makeUniqueSentinels, maskRawRegions, restorePlaceholders } from "../utils";
|
|
10
|
+
import { matchDirectiveKind } from "./kind";
|
|
10
11
|
import { expandInnermost } from "./scan";
|
|
11
12
|
|
|
13
|
+
const MAX_EXPR_NESTING = 64;
|
|
14
|
+
|
|
12
15
|
/**
|
|
13
16
|
* Resolve every `[[#if]]` / `[[#ifexpr]]` / `[[#expr]]` that sits inside
|
|
14
17
|
* another block's opener (depth > 0). Top-level directives are left for
|
|
@@ -21,6 +24,7 @@ export function preprocessExpr(source: string): string {
|
|
|
21
24
|
|
|
22
25
|
const sentinels = makeUniqueSentinels(source);
|
|
23
26
|
const { masked, placeholders } = maskRawRegions(source, sentinels);
|
|
27
|
+
if (exceedsExprNestingLimit(masked)) return source;
|
|
24
28
|
const reduced = reduceExpr(masked);
|
|
25
29
|
return restorePlaceholders(reduced, placeholders, sentinels);
|
|
26
30
|
}
|
|
@@ -43,3 +47,25 @@ function reduceExpr(source: string): string {
|
|
|
43
47
|
}
|
|
44
48
|
return current;
|
|
45
49
|
}
|
|
50
|
+
|
|
51
|
+
function exceedsExprNestingLimit(source: string): boolean {
|
|
52
|
+
const expressionStack: boolean[] = [];
|
|
53
|
+
let expressionDepth = 0;
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < source.length; i++) {
|
|
56
|
+
if (source.startsWith("[[", i)) {
|
|
57
|
+
const isExpression = matchDirectiveKind(source, i) !== null;
|
|
58
|
+
expressionStack.push(isExpression);
|
|
59
|
+
if (isExpression && ++expressionDepth > MAX_EXPR_NESTING) return true;
|
|
60
|
+
i++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (source.startsWith("]]", i)) {
|
|
65
|
+
if (expressionStack.pop()) expressionDepth--;
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
@@ -13,16 +13,25 @@ export interface Sentinels {
|
|
|
13
13
|
* Choose sentinel strings that are guaranteed not to appear in `source`.
|
|
14
14
|
* The placeholders we splice into the masked source have the form
|
|
15
15
|
* `<open><digits><close>`, so the restore pass must not confuse them
|
|
16
|
-
* with content.
|
|
16
|
+
* with content.
|
|
17
17
|
*/
|
|
18
18
|
export function makeUniqueSentinels(source: string): Sentinels {
|
|
19
|
-
let
|
|
20
|
-
let
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
let openRun = 0;
|
|
20
|
+
let closeRun = 0;
|
|
21
|
+
let longestOpenRun = 0;
|
|
22
|
+
let longestCloseRun = 0;
|
|
23
|
+
|
|
24
|
+
for (const char of source) {
|
|
25
|
+
openRun = char === BASE_PLACEHOLDER_OPEN ? openRun + 1 : 0;
|
|
26
|
+
closeRun = char === BASE_PLACEHOLDER_CLOSE ? closeRun + 1 : 0;
|
|
27
|
+
longestOpenRun = Math.max(longestOpenRun, openRun);
|
|
28
|
+
longestCloseRun = Math.max(longestCloseRun, closeRun);
|
|
24
29
|
}
|
|
25
|
-
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
open: BASE_PLACEHOLDER_OPEN.repeat(longestOpenRun + 1),
|
|
33
|
+
close: BASE_PLACEHOLDER_CLOSE.repeat(longestCloseRun + 1),
|
|
34
|
+
};
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { lineBreak, paragraph, text, type Element } from "@wdprlib/ast";
|
|
2
2
|
import type { BlockRule, ParseContext, RuleResult } from "../../types";
|
|
3
3
|
import { currentToken } from "../../types";
|
|
4
4
|
import { parseBlockName } from "../utils";
|
|
@@ -46,6 +46,7 @@ export const includeRule: BlockRule = {
|
|
|
46
46
|
}
|
|
47
47
|
pos++;
|
|
48
48
|
consumed++;
|
|
49
|
+
const directiveEnd = pos;
|
|
49
50
|
|
|
50
51
|
if (ctx.tokens[pos]?.type === "NEWLINE") {
|
|
51
52
|
pos++;
|
|
@@ -56,6 +57,23 @@ export const includeRule: BlockRule = {
|
|
|
56
57
|
return { success: false };
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
const location = parsePageRef(args.target);
|
|
61
|
+
if (ctx.deferInclude?.(location)) {
|
|
62
|
+
// Keep Wikidot's parser extent here. Nested block markup in an include
|
|
63
|
+
// value must delay its closing brackets through variable expansion.
|
|
64
|
+
const source = ctx.tokens
|
|
65
|
+
.slice(ctx.pos, directiveEnd)
|
|
66
|
+
.map((token) => token.value)
|
|
67
|
+
.join("");
|
|
68
|
+
const elements: Element[] = [];
|
|
69
|
+
const lines = source.split("\n");
|
|
70
|
+
for (let index = 0; index < lines.length; index++) {
|
|
71
|
+
if (index > 0) elements.push(lineBreak());
|
|
72
|
+
if (lines[index] !== "") elements.push(text(lines[index]!));
|
|
73
|
+
}
|
|
74
|
+
return { success: true, elements: [paragraph(elements)], consumed };
|
|
75
|
+
}
|
|
76
|
+
|
|
59
77
|
return {
|
|
60
78
|
success: true,
|
|
61
79
|
elements: [
|
|
@@ -64,7 +82,7 @@ export const includeRule: BlockRule = {
|
|
|
64
82
|
data: {
|
|
65
83
|
"paragraph-safe": false,
|
|
66
84
|
variables: parseVariables(args.argumentTokens),
|
|
67
|
-
location
|
|
85
|
+
location,
|
|
68
86
|
elements: [],
|
|
69
87
|
},
|
|
70
88
|
},
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import type {
|
|
16
16
|
AsyncIncludeFetcher,
|
|
17
17
|
IncludeFetcher,
|
|
18
|
+
IncludeReference,
|
|
18
19
|
ResolveIncludesOptions,
|
|
19
20
|
ResolveIncludesTraceResult,
|
|
20
21
|
} from "./types";
|
|
@@ -113,6 +114,33 @@ export async function resolveIncludesAsyncWithTrace(
|
|
|
113
114
|
return expandIterativeAsyncWithTrace(source, cachedFetcher, maxIterations);
|
|
114
115
|
}
|
|
115
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Resolve includes while leaving selected directives untouched for the
|
|
119
|
+
* high-level parser pipeline to render literally.
|
|
120
|
+
*
|
|
121
|
+
* This helper is intentionally not exported from the package barrel so the
|
|
122
|
+
* public low-level resolver contract remains unchanged.
|
|
123
|
+
*/
|
|
124
|
+
export async function resolveIncludesAsyncWithTraceSelective(
|
|
125
|
+
source: string,
|
|
126
|
+
fetcher: AsyncIncludeFetcher,
|
|
127
|
+
shouldDefer: (reference: IncludeReference) => boolean,
|
|
128
|
+
options?: ResolveIncludesOptions,
|
|
129
|
+
): Promise<ResolveIncludesTraceResult> {
|
|
130
|
+
if (options?.settings && !options.settings.enablePageSyntax) {
|
|
131
|
+
return {
|
|
132
|
+
source,
|
|
133
|
+
dependencies: [],
|
|
134
|
+
iterations: [],
|
|
135
|
+
reachedMaxIterations: false,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const maxIterations = options?.maxIterations ?? 10;
|
|
140
|
+
const cachedFetcher = createCachedAsyncIncludeFetcher(fetcher, normalizePageKey);
|
|
141
|
+
return expandIterativeAsyncWithTrace(source, cachedFetcher, maxIterations, shouldDefer);
|
|
142
|
+
}
|
|
143
|
+
|
|
116
144
|
/**
|
|
117
145
|
* Normalize a PageRef into a consistent string key for cache lookups.
|
|
118
146
|
*
|
|
@@ -107,6 +107,7 @@ export async function expandIterativeAsyncWithTrace(
|
|
|
107
107
|
source: string,
|
|
108
108
|
fetcher: AsyncIncludeFetcher,
|
|
109
109
|
maxIterations: number,
|
|
110
|
+
shouldDefer?: (reference: IncludeReference) => boolean,
|
|
110
111
|
): Promise<ResolveIncludesTraceResult> {
|
|
111
112
|
let current = source;
|
|
112
113
|
const replacementCache = new Map<string, Promise<string>>();
|
|
@@ -114,7 +115,13 @@ export async function expandIterativeAsyncWithTrace(
|
|
|
114
115
|
const iterations: ResolveIncludesTraceResult["iterations"] = [];
|
|
115
116
|
|
|
116
117
|
for (let i = 0; i < maxIterations; i++) {
|
|
117
|
-
const expanded = await expandOneIterationAsyncWithTrace(
|
|
118
|
+
const expanded = await expandOneIterationAsyncWithTrace(
|
|
119
|
+
current,
|
|
120
|
+
fetcher,
|
|
121
|
+
replacementCache,
|
|
122
|
+
i,
|
|
123
|
+
shouldDefer,
|
|
124
|
+
);
|
|
118
125
|
if (expanded === null) break;
|
|
119
126
|
|
|
120
127
|
dependencies.push(...expanded.dependencies);
|
|
@@ -133,7 +140,7 @@ export async function expandIterativeAsyncWithTrace(
|
|
|
133
140
|
dependencies,
|
|
134
141
|
iterations,
|
|
135
142
|
reachedMaxIterations:
|
|
136
|
-
iterations.length === maxIterations &&
|
|
143
|
+
iterations.length === maxIterations && hasResolvableDirectives(current, shouldDefer),
|
|
137
144
|
};
|
|
138
145
|
}
|
|
139
146
|
|
|
@@ -219,6 +226,7 @@ async function expandOneIterationAsyncWithTrace(
|
|
|
219
226
|
fetcher: AsyncIncludeFetcher,
|
|
220
227
|
replacementCache: Map<string, Promise<string>>,
|
|
221
228
|
iteration: number,
|
|
229
|
+
shouldDefer?: (reference: IncludeReference) => boolean,
|
|
222
230
|
): Promise<{
|
|
223
231
|
source: string;
|
|
224
232
|
references: IncludeReference[];
|
|
@@ -230,8 +238,13 @@ async function expandOneIterationAsyncWithTrace(
|
|
|
230
238
|
if (directives.length === 0) return null;
|
|
231
239
|
|
|
232
240
|
const references = directives.map(createIncludeReference);
|
|
241
|
+
const deferred = references.map((reference) => shouldDefer?.(reference) ?? false);
|
|
233
242
|
const replacements = await Promise.all(
|
|
234
|
-
directives.map((
|
|
243
|
+
directives.map((directive, index) =>
|
|
244
|
+
deferred[index]
|
|
245
|
+
? Promise.resolve(source.slice(directive.start, directive.end))
|
|
246
|
+
: replaceCachedAsync(directive.inner, fetcher, replacementCache),
|
|
247
|
+
),
|
|
235
248
|
);
|
|
236
249
|
const parts: string[] = [];
|
|
237
250
|
let lastPos = 0;
|
|
@@ -246,10 +259,21 @@ async function expandOneIterationAsyncWithTrace(
|
|
|
246
259
|
return {
|
|
247
260
|
source: parts.join(""),
|
|
248
261
|
references,
|
|
249
|
-
dependencies: references
|
|
262
|
+
dependencies: references
|
|
263
|
+
.filter((_, index) => !deferred[index])
|
|
264
|
+
.map((reference) => ({ ...reference, iteration })),
|
|
250
265
|
};
|
|
251
266
|
}
|
|
252
267
|
|
|
268
|
+
function hasResolvableDirectives(
|
|
269
|
+
source: string,
|
|
270
|
+
shouldDefer: ((reference: IncludeReference) => boolean) | undefined,
|
|
271
|
+
): boolean {
|
|
272
|
+
const directives = scanIncludeDirectives(source);
|
|
273
|
+
if (!shouldDefer) return directives.length > 0;
|
|
274
|
+
return directives.some((directive) => !shouldDefer(createIncludeReference(directive)));
|
|
275
|
+
}
|
|
276
|
+
|
|
253
277
|
function replaceCached(inner: string, fetcher: IncludeFetcher, cache: Map<string, string>): string {
|
|
254
278
|
const cached = cache.get(inner);
|
|
255
279
|
if (cached !== undefined) return cached;
|
|
@@ -40,6 +40,8 @@ export function scanIncludeDirectives(source: string): IncludeDirectiveMatch[] {
|
|
|
40
40
|
|
|
41
41
|
let depth = 0;
|
|
42
42
|
let linkDepth = 0;
|
|
43
|
+
const nestedQuotes: Array<number | null> = [];
|
|
44
|
+
const nestedQuoteAllowed: boolean[] = [];
|
|
43
45
|
let i = start;
|
|
44
46
|
let closeEnd = -1;
|
|
45
47
|
|
|
@@ -48,7 +50,37 @@ export function scanIncludeDirectives(source: string): IncludeDirectiveMatch[] {
|
|
|
48
50
|
const next = source.charCodeAt(i + 1);
|
|
49
51
|
const nextNext = source.charCodeAt(i + 2);
|
|
50
52
|
|
|
51
|
-
if (ch ===
|
|
53
|
+
if (ch === 10 || ch === 13) {
|
|
54
|
+
linkDepth = 0;
|
|
55
|
+
if (depth > 1) {
|
|
56
|
+
depth = 1;
|
|
57
|
+
nestedQuotes.length = 0;
|
|
58
|
+
nestedQuoteAllowed.length = 0;
|
|
59
|
+
}
|
|
60
|
+
i++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const nestedQuote = nestedQuotes.at(-1);
|
|
65
|
+
if (nestedQuote !== undefined && nestedQuote !== null) {
|
|
66
|
+
if (ch === 92) {
|
|
67
|
+
i += 2;
|
|
68
|
+
} else {
|
|
69
|
+
if (ch === nestedQuote) {
|
|
70
|
+
nestedQuotes[nestedQuotes.length - 1] = null;
|
|
71
|
+
nestedQuoteAllowed[nestedQuoteAllowed.length - 1] = false;
|
|
72
|
+
}
|
|
73
|
+
i++;
|
|
74
|
+
}
|
|
75
|
+
} else if (
|
|
76
|
+
nestedQuote === null &&
|
|
77
|
+
nestedQuoteAllowed.at(-1) === true &&
|
|
78
|
+
(ch === 34 || ch === 39)
|
|
79
|
+
) {
|
|
80
|
+
nestedQuotes[nestedQuotes.length - 1] = ch;
|
|
81
|
+
nestedQuoteAllowed[nestedQuoteAllowed.length - 1] = false;
|
|
82
|
+
i++;
|
|
83
|
+
} else if (ch === OPEN_BRACKET && next === OPEN_BRACKET && nextNext === OPEN_BRACKET) {
|
|
52
84
|
linkDepth++;
|
|
53
85
|
i += 3;
|
|
54
86
|
} else if (
|
|
@@ -62,10 +94,28 @@ export function scanIncludeDirectives(source: string): IncludeDirectiveMatch[] {
|
|
|
62
94
|
} else if (linkDepth > 0) {
|
|
63
95
|
i++;
|
|
64
96
|
} else if (ch === OPEN_BRACKET && next === OPEN_BRACKET) {
|
|
97
|
+
if (nestedQuoteAllowed.length > 0) {
|
|
98
|
+
nestedQuoteAllowed[nestedQuoteAllowed.length - 1] = false;
|
|
99
|
+
}
|
|
100
|
+
if (depth > 0) {
|
|
101
|
+
nestedQuotes.push(null);
|
|
102
|
+
nestedQuoteAllowed.push(false);
|
|
103
|
+
}
|
|
65
104
|
depth++;
|
|
66
105
|
i += 2;
|
|
106
|
+
} else if (ch === CLOSE_BRACKET && next !== CLOSE_BRACKET && depth > 1) {
|
|
107
|
+
// A lone `]` is the first close of an intentionally incomplete nested
|
|
108
|
+
// token (for example `[[module ...]` passed through an include value).
|
|
109
|
+
depth--;
|
|
110
|
+
nestedQuotes.pop();
|
|
111
|
+
nestedQuoteAllowed.pop();
|
|
112
|
+
i++;
|
|
67
113
|
} else if (ch === CLOSE_BRACKET && next === CLOSE_BRACKET) {
|
|
68
114
|
const closeStart = i;
|
|
115
|
+
if (depth > 1) {
|
|
116
|
+
nestedQuotes.pop();
|
|
117
|
+
nestedQuoteAllowed.pop();
|
|
118
|
+
}
|
|
69
119
|
depth--;
|
|
70
120
|
i += 2;
|
|
71
121
|
|
|
@@ -83,6 +133,13 @@ export function scanIncludeDirectives(source: string): IncludeDirectiveMatch[] {
|
|
|
83
133
|
}
|
|
84
134
|
}
|
|
85
135
|
} else {
|
|
136
|
+
if (nestedQuoteAllowed.length > 0) {
|
|
137
|
+
if (ch === 61) {
|
|
138
|
+
nestedQuoteAllowed[nestedQuoteAllowed.length - 1] = true;
|
|
139
|
+
} else if (ch !== 32 && ch !== 9) {
|
|
140
|
+
nestedQuoteAllowed[nestedQuoteAllowed.length - 1] = false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
86
143
|
i++;
|
|
87
144
|
}
|
|
88
145
|
}
|
|
@@ -59,6 +59,8 @@ export { extractDataRequirements } from "./extract";
|
|
|
59
59
|
// Resolution
|
|
60
60
|
export type { ParseFunction, ListPagesModuleData } from "./resolve";
|
|
61
61
|
export { isListPagesModule, resolveListPages } from "./resolve";
|
|
62
|
+
export { definePageData } from "./types/external-data";
|
|
63
|
+
export { matchesListPagesSelectors } from "./selectors";
|
|
62
64
|
|
|
63
65
|
// Compiler
|
|
64
66
|
export { compileTemplate } from "./compiler";
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { PageData } from "./types/external-data";
|
|
2
|
+
import type {
|
|
3
|
+
NormalizedCategory,
|
|
4
|
+
NormalizedListPagesQuery,
|
|
5
|
+
NormalizedTags,
|
|
6
|
+
} from "./types/normalized-query";
|
|
7
|
+
|
|
8
|
+
type SelectorPage = Pick<PageData, "category" | "tags" | "hiddenTags">;
|
|
9
|
+
type CurrentPage = { category: string; tags: readonly string[] };
|
|
10
|
+
|
|
11
|
+
export function matchesListPagesSelectors(
|
|
12
|
+
page: SelectorPage,
|
|
13
|
+
query: Pick<NormalizedListPagesQuery, "category" | "tags">,
|
|
14
|
+
currentPage: CurrentPage,
|
|
15
|
+
): boolean {
|
|
16
|
+
return (
|
|
17
|
+
matchesCategory(page.category, query.category, currentPage.category) &&
|
|
18
|
+
matchesTags(page, query.tags, currentPage)
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function matchesCategory(
|
|
23
|
+
category: string,
|
|
24
|
+
selector: NormalizedCategory | undefined,
|
|
25
|
+
currentCategory: string,
|
|
26
|
+
): boolean {
|
|
27
|
+
if (!selector) return category === currentCategory;
|
|
28
|
+
if (selector.exclude.includes(category)) return false;
|
|
29
|
+
|
|
30
|
+
const hasPositiveSelector = selector.all || selector.current || selector.include.length > 0;
|
|
31
|
+
return (
|
|
32
|
+
!hasPositiveSelector ||
|
|
33
|
+
selector.all ||
|
|
34
|
+
(selector.current && category === currentCategory) ||
|
|
35
|
+
selector.include.includes(category)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function matchesTags(
|
|
40
|
+
page: Pick<SelectorPage, "tags" | "hiddenTags">,
|
|
41
|
+
selector: NormalizedTags | undefined,
|
|
42
|
+
currentPage: Pick<CurrentPage, "tags">,
|
|
43
|
+
): boolean {
|
|
44
|
+
if (!selector) return true;
|
|
45
|
+
|
|
46
|
+
const allTags = new Set([...page.tags, ...page.hiddenTags]);
|
|
47
|
+
if (selector.all.some((tag) => !allTags.has(tag))) return false;
|
|
48
|
+
if (selector.any.length > 0 && !selector.any.some((tag) => allTags.has(tag))) return false;
|
|
49
|
+
if (selector.none.some((tag) => allTags.has(tag))) return false;
|
|
50
|
+
|
|
51
|
+
if (selector.special === "none") return allTags.size === 0;
|
|
52
|
+
const currentTags = new Set(currentPage.tags.filter((tag) => !tag.startsWith("_")));
|
|
53
|
+
if (selector.special === "same-visible") {
|
|
54
|
+
return page.tags.some((tag) => currentTags.has(tag));
|
|
55
|
+
}
|
|
56
|
+
if (selector.special === "same-all") {
|
|
57
|
+
return setsEqual(new Set(page.tags), currentTags);
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function setsEqual(left: ReadonlySet<string>, right: ReadonlySet<string>): boolean {
|
|
63
|
+
return left.size === right.size && [...left].every((value) => right.has(value));
|
|
64
|
+
}
|
|
@@ -58,6 +58,28 @@ export interface PageData {
|
|
|
58
58
|
revisions: number;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
type PageDataInput = Pick<PageData, "fullname" | "title" | "createdAt" | "updatedAt" | "tags"> &
|
|
62
|
+
Partial<Omit<PageData, "fullname" | "title" | "createdAt" | "updatedAt" | "tags">>;
|
|
63
|
+
|
|
64
|
+
export function definePageData(input: PageDataInput): PageData {
|
|
65
|
+
const separator = input.fullname.indexOf(":");
|
|
66
|
+
const category = separator === -1 ? "_default" : input.fullname.slice(0, separator);
|
|
67
|
+
const name = separator === -1 ? input.fullname : input.fullname.slice(separator + 1);
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
...input,
|
|
71
|
+
name: input.name ?? name,
|
|
72
|
+
category: input.category ?? category,
|
|
73
|
+
hiddenTags: input.hiddenTags ?? [],
|
|
74
|
+
children: input.children ?? 0,
|
|
75
|
+
comments: input.comments ?? 0,
|
|
76
|
+
size: input.size ?? 0,
|
|
77
|
+
rating: input.rating ?? 0,
|
|
78
|
+
ratingVotes: input.ratingVotes ?? 0,
|
|
79
|
+
revisions: input.revisions ?? 0,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
61
83
|
/**
|
|
62
84
|
* Site context information.
|
|
63
85
|
*/
|
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
CodeBlockData,
|
|
4
4
|
Diagnostic,
|
|
5
5
|
Element,
|
|
6
|
+
PageRef,
|
|
6
7
|
TocEntry,
|
|
7
8
|
Version,
|
|
8
9
|
WikitextSettings,
|
|
@@ -26,6 +27,7 @@ export interface ParseContext {
|
|
|
26
27
|
trackPositions: boolean;
|
|
27
28
|
settings: WikitextSettings;
|
|
28
29
|
appendImplicitFootnoteBlock: boolean;
|
|
30
|
+
deferInclude?: (location: PageRef) => boolean;
|
|
29
31
|
footnotes: Element[][];
|
|
30
32
|
tocEntries: TocEntry[];
|
|
31
33
|
codeBlocks: CodeBlockData[];
|