@wdprlib/parser 4.3.0 → 5.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.
Files changed (32) hide show
  1. package/README.md +26 -38
  2. package/dist/index.cjs +1035 -249
  3. package/dist/index.d.cts +203 -158
  4. package/dist/index.d.ts +203 -158
  5. package/dist/index.js +930 -139
  6. package/package.json +2 -2
  7. package/src/index.ts +13 -0
  8. package/src/parser/parse/context.ts +1 -0
  9. package/src/parser/parse/options.ts +6 -0
  10. package/src/parser/parse/result.ts +3 -1
  11. package/src/parser/preprocess/expr/index.ts +26 -0
  12. package/src/parser/preprocess/utils/raw-regions.ts +16 -7
  13. package/src/parser/rules/block/module/include/index.ts +6 -1
  14. package/src/parser/rules/block/module/include/resolve/index.ts +23 -3
  15. package/src/parser/rules/block/module/include/resolve/iterate.ts +71 -0
  16. package/src/parser/rules/block/module/index.ts +4 -1
  17. package/src/parser/rules/block/module/listpages/index.ts +2 -0
  18. package/src/parser/rules/block/module/listpages/resolution/items.ts +2 -2
  19. package/src/parser/rules/block/module/listpages/resolution/wrapper.ts +3 -3
  20. package/src/parser/rules/block/module/listpages/selectors.ts +64 -0
  21. package/src/parser/rules/block/module/listpages/types/external-data.ts +22 -0
  22. package/src/parser/rules/block/module/listusers/resolve.ts +2 -2
  23. package/src/parser/rules/block/module/resolution/document.ts +357 -0
  24. package/src/parser/rules/block/module/resolution/resolve-async.ts +269 -0
  25. package/src/parser/rules/block/module/resolution/styles.ts +134 -12
  26. package/src/parser/rules/block/module/resolution/walk-resolve.ts +19 -1
  27. package/src/parser/rules/block/module/resolve.ts +32 -13
  28. package/src/parser/rules/block/module/types.ts +7 -2
  29. package/src/parser/rules/contracts/parse-context.ts +1 -0
  30. package/src/pipeline/index.ts +7 -0
  31. package/src/pipeline/process.ts +172 -0
  32. package/src/pipeline/types.ts +63 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdprlib/parser",
3
- "version": "4.3.0",
3
+ "version": "5.0.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": "2.3.0"
44
+ "@wdprlib/ast": "3.0.0"
45
45
  }
46
46
  }
package/src/index.ts CHANGED
@@ -98,6 +98,15 @@ export { Lexer, tokenize, createToken } from "./lexer";
98
98
  export type { ParserOptions } from "./parser";
99
99
  export { Parser, parse } from "./parser";
100
100
 
101
+ // High-level parser pipeline
102
+ export { processWikitext } from "./pipeline";
103
+ export type {
104
+ ProcessedWikitextDocument,
105
+ ProcessWikitextCallbackContext,
106
+ ProcessWikitextDataProvider,
107
+ ProcessWikitextOptions,
108
+ } from "./pipeline";
109
+
101
110
  // Modules (ListPages, ListUsers, IfTags, Include, etc.)
102
111
  export type {
103
112
  // ListPages query types
@@ -121,6 +130,7 @@ export type {
121
130
  ExtractionResult,
122
131
  // Resolution types
123
132
  ParseFunction,
133
+ ModuleParseResult,
124
134
  ModuleSourceTransform,
125
135
  ResolveOptions,
126
136
  // Include resolution
@@ -163,6 +173,7 @@ export {
163
173
  extractIncludeReferences,
164
174
  resolveIncludes,
165
175
  resolveIncludesAsync,
176
+ resolveIncludesAsyncWithTrace,
166
177
  resolveIncludesWithTrace,
167
178
  // IfTags source-level preprocessing (run between include expansion and parse)
168
179
  preprocessIftags,
@@ -174,6 +185,8 @@ export {
174
185
  parseParent,
175
186
  parseDateSelector,
176
187
  parseNumericSelector,
188
+ definePageData,
189
+ matchesListPagesSelectors,
177
190
  // ListUsers
178
191
  extractListUsersVariables,
179
192
  compileListUsersTemplate,
@@ -10,6 +10,7 @@ export function createParseContext(tokens: Token[], options: ParserOptions = {})
10
10
  version: options.version ?? "wikidot",
11
11
  trackPositions: options.trackPositions ?? true,
12
12
  settings: options.settings ?? DEFAULT_SETTINGS,
13
+ appendImplicitFootnoteBlock: options.appendImplicitFootnoteBlock ?? true,
13
14
  footnotes: [],
14
15
  tocEntries: [],
15
16
  codeBlocks: [],
@@ -31,4 +31,10 @@ export interface ParserOptions {
31
31
  * - `string[]`: every iftags block is evaluated against the given tags eagerly.
32
32
  */
33
33
  pageTags?: string[] | null;
34
+ /**
35
+ * Append the implicit document-level footnote block when no explicit block
36
+ * exists. Defaults to true. Fragment pipelines can disable this and add one
37
+ * block after all fragments have been merged.
38
+ */
39
+ appendImplicitFootnoteBlock?: boolean;
34
40
  }
@@ -10,7 +10,9 @@ import { containsFootnoteBlock } from "./footnotes";
10
10
 
11
11
  export function finalizeParseResult(ctx: ParseContext, children: Element[]): ParseResult {
12
12
  const cleanedChildren = postprocessChildren(children);
13
- appendImplicitFootnoteBlock(cleanedChildren);
13
+ if (ctx.appendImplicitFootnoteBlock) {
14
+ appendImplicitFootnoteBlock(cleanedChildren);
15
+ }
14
16
 
15
17
  return {
16
18
  ast: buildSyntaxTree(ctx, cleanedChildren),
@@ -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. Extends both sentinel characters until neither appears.
16
+ * with content.
17
17
  */
18
18
  export function makeUniqueSentinels(source: string): Sentinels {
19
- let open = BASE_PLACEHOLDER_OPEN;
20
- let close = BASE_PLACEHOLDER_CLOSE;
21
- while (source.includes(open) || source.includes(close)) {
22
- open += BASE_PLACEHOLDER_OPEN;
23
- close += BASE_PLACEHOLDER_CLOSE;
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
- return { open, close };
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
  /**
@@ -16,7 +16,12 @@
16
16
  * @module
17
17
  */
18
18
 
19
- export { resolveIncludes, resolveIncludesAsync, resolveIncludesWithTrace } from "./resolve";
19
+ export {
20
+ resolveIncludes,
21
+ resolveIncludesAsync,
22
+ resolveIncludesAsyncWithTrace,
23
+ resolveIncludesWithTrace,
24
+ } from "./resolve";
20
25
  export { extractIncludeReferences } from "./references";
21
26
  export type {
22
27
  IncludeFetcher,
@@ -7,7 +7,11 @@
7
7
 
8
8
  import type { PageRef } from "@wdprlib/ast";
9
9
  import { createCachedAsyncIncludeFetcher, createCachedIncludeFetcher } from "./cache";
10
- import { expandIterative, expandIterativeAsync, expandIterativeWithTrace } from "./iterate";
10
+ import {
11
+ expandIterative,
12
+ expandIterativeAsyncWithTrace,
13
+ expandIterativeWithTrace,
14
+ } from "./iterate";
11
15
  import type {
12
16
  AsyncIncludeFetcher,
13
17
  IncludeFetcher,
@@ -84,13 +88,29 @@ export async function resolveIncludesAsync(
84
88
  fetcher: AsyncIncludeFetcher,
85
89
  options?: ResolveIncludesOptions,
86
90
  ): Promise<string> {
91
+ return (await resolveIncludesAsyncWithTrace(source, fetcher, options)).source;
92
+ }
93
+
94
+ /**
95
+ * Async include expansion with dependency and iteration trace data.
96
+ */
97
+ export async function resolveIncludesAsyncWithTrace(
98
+ source: string,
99
+ fetcher: AsyncIncludeFetcher,
100
+ options?: ResolveIncludesOptions,
101
+ ): Promise<ResolveIncludesTraceResult> {
87
102
  if (options?.settings && !options.settings.enablePageSyntax) {
88
- return source;
103
+ return {
104
+ source,
105
+ dependencies: [],
106
+ iterations: [],
107
+ reachedMaxIterations: false,
108
+ };
89
109
  }
90
110
 
91
111
  const maxIterations = options?.maxIterations ?? 10;
92
112
  const cachedFetcher = createCachedAsyncIncludeFetcher(fetcher, normalizePageKey);
93
- return expandIterativeAsync(source, cachedFetcher, maxIterations);
113
+ return expandIterativeAsyncWithTrace(source, cachedFetcher, maxIterations);
94
114
  }
95
115
 
96
116
  /**
@@ -102,6 +102,41 @@ export async function expandIterativeAsync(
102
102
  return current;
103
103
  }
104
104
 
105
+ /** Async counterpart of {@link expandIterativeWithTrace}. */
106
+ export async function expandIterativeAsyncWithTrace(
107
+ source: string,
108
+ fetcher: AsyncIncludeFetcher,
109
+ maxIterations: number,
110
+ ): Promise<ResolveIncludesTraceResult> {
111
+ let current = source;
112
+ const replacementCache = new Map<string, Promise<string>>();
113
+ const dependencies: IncludeDependency[] = [];
114
+ const iterations: ResolveIncludesTraceResult["iterations"] = [];
115
+
116
+ for (let i = 0; i < maxIterations; i++) {
117
+ const expanded = await expandOneIterationAsyncWithTrace(current, fetcher, replacementCache, i);
118
+ if (expanded === null) break;
119
+
120
+ dependencies.push(...expanded.dependencies);
121
+ iterations.push({
122
+ iteration: i,
123
+ directives: expanded.references,
124
+ changed: expanded.source !== current,
125
+ });
126
+
127
+ if (expanded.source === current) break;
128
+ current = expanded.source;
129
+ }
130
+
131
+ return {
132
+ source: current,
133
+ dependencies,
134
+ iterations,
135
+ reachedMaxIterations:
136
+ iterations.length === maxIterations && scanIncludeDirectives(current).length > 0,
137
+ };
138
+ }
139
+
105
140
  function expandOneIteration(
106
141
  source: string,
107
142
  fetcher: IncludeFetcher,
@@ -179,6 +214,42 @@ async function expandOneIterationAsync(
179
214
  return parts.join("");
180
215
  }
181
216
 
217
+ async function expandOneIterationAsyncWithTrace(
218
+ source: string,
219
+ fetcher: AsyncIncludeFetcher,
220
+ replacementCache: Map<string, Promise<string>>,
221
+ iteration: number,
222
+ ): Promise<{
223
+ source: string;
224
+ references: IncludeReference[];
225
+ dependencies: IncludeDependency[];
226
+ } | null> {
227
+ if (!MAYBE_INCLUDE_PATTERN.test(source)) return null;
228
+
229
+ const directives = scanIncludeDirectives(source);
230
+ if (directives.length === 0) return null;
231
+
232
+ const references = directives.map(createIncludeReference);
233
+ const replacements = await Promise.all(
234
+ directives.map(({ inner }) => replaceCachedAsync(inner, fetcher, replacementCache)),
235
+ );
236
+ const parts: string[] = [];
237
+ let lastPos = 0;
238
+
239
+ for (let i = 0; i < directives.length; i++) {
240
+ const { start, end } = directives[i]!;
241
+ parts.push(source.slice(lastPos, start), replacements[i]!);
242
+ lastPos = end;
243
+ }
244
+ parts.push(source.slice(lastPos));
245
+
246
+ return {
247
+ source: parts.join(""),
248
+ references,
249
+ dependencies: references.map((reference) => ({ ...reference, iteration })),
250
+ };
251
+ }
252
+
182
253
  function replaceCached(inner: string, fetcher: IncludeFetcher, cache: Map<string, string>): string {
183
254
  const cached = cache.get(inner);
184
255
  if (cached !== undefined) return cached;
@@ -20,7 +20,7 @@
20
20
  */
21
21
 
22
22
  // Module rule types and registry
23
- export type { ModuleRule } from "./types";
23
+ export type { ModuleParseResult, ModuleRule } from "./types";
24
24
  export { MODULE_RULES, getModuleRuleByName } from "./mapping";
25
25
  export { moduleRule } from "./rule";
26
26
 
@@ -82,6 +82,8 @@ export {
82
82
  parseParent,
83
83
  parseDateSelector,
84
84
  parseNumericSelector,
85
+ definePageData,
86
+ matchesListPagesSelectors,
85
87
  } from "./listpages";
86
88
 
87
89
  // IfTags module
@@ -106,6 +108,7 @@ export {
106
108
  extractIncludeReferences,
107
109
  resolveIncludes,
108
110
  resolveIncludesAsync,
111
+ resolveIncludesAsyncWithTrace,
109
112
  resolveIncludesWithTrace,
110
113
  } from "./include";
111
114
 
@@ -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";
@@ -1,6 +1,6 @@
1
1
  import type { Element } from "@wdprlib/ast";
2
2
  import type { CompiledTemplate, ListPagesExternalData, VariableContext } from "../types";
3
- import type { ParseFunction } from "../../types";
3
+ import { getModuleParseAst, type ParseFunction } from "../../types";
4
4
  import type { ListPagesModuleData } from "../resolve";
5
5
 
6
6
  export function renderListPagesItems(
@@ -23,7 +23,7 @@ export function renderListPagesItems(
23
23
  site: data.site,
24
24
  };
25
25
 
26
- const itemAst = parse(compiledTemplate(ctx));
26
+ const itemAst = getModuleParseAst(parse(compiledTemplate(ctx)));
27
27
 
28
28
  if (module.separate) {
29
29
  items.push({
@@ -1,5 +1,5 @@
1
1
  import type { Element } from "@wdprlib/ast";
2
- import type { ParseFunction } from "../../types";
2
+ import { getModuleParseAst, type ParseFunction } from "../../types";
3
3
  import type { ListPagesModuleData } from "../resolve";
4
4
 
5
5
  export function wrapListPagesResult(
@@ -14,14 +14,14 @@ export function wrapListPagesResult(
14
14
  const result: Element[] = [];
15
15
 
16
16
  if (module["prepend-line"] && !module.separate) {
17
- const prependAst = parse(module["prepend-line"]);
17
+ const prependAst = getModuleParseAst(parse(module["prepend-line"]));
18
18
  result.push(...prependAst.elements);
19
19
  }
20
20
 
21
21
  result.push(...items);
22
22
 
23
23
  if (module["append-line"] && !module.separate) {
24
- const appendAst = parse(module["append-line"]);
24
+ const appendAst = getModuleParseAst(parse(module["append-line"]));
25
25
  result.push(...appendAst.elements);
26
26
  }
27
27
 
@@ -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
  */
@@ -15,7 +15,7 @@ import type {
15
15
  ListUsersCompiledTemplate,
16
16
  ListUsersVariableContext,
17
17
  } from "./types";
18
- import type { ParseFunction } from "../types";
18
+ import { getModuleParseAst, type ParseFunction } from "../types";
19
19
 
20
20
  /**
21
21
  * Narrowed type for the list-users variant of the Module discriminated union.
@@ -53,6 +53,6 @@ export function resolveListUsers(
53
53
  ): Element[] {
54
54
  const ctx: ListUsersVariableContext = { user: data.user };
55
55
  const substituted = compiledTemplate(ctx);
56
- const itemAst = parse(substituted);
56
+ const itemAst = getModuleParseAst(parse(substituted));
57
57
  return itemAst.elements;
58
58
  }