@wdprlib/parser 4.2.0 → 4.4.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/README.md +28 -38
- package/dist/index.cjs +973 -134
- package/dist/index.d.cts +195 -158
- package/dist/index.d.ts +195 -158
- package/dist/index.js +955 -113
- package/package.json +2 -2
- package/src/index.ts +17 -0
- package/src/parser/parse/context.ts +1 -0
- package/src/parser/parse/options.ts +6 -0
- package/src/parser/parse/result.ts +3 -1
- package/src/parser/rules/block/gallery/index.ts +215 -0
- package/src/parser/rules/block/gallery/items.ts +62 -0
- package/src/parser/rules/block/index.ts +3 -0
- package/src/parser/rules/block/module/include/index.ts +6 -1
- package/src/parser/rules/block/module/include/resolve/index.ts +23 -3
- package/src/parser/rules/block/module/include/resolve/iterate.ts +71 -0
- package/src/parser/rules/block/module/index.ts +2 -1
- package/src/parser/rules/block/module/listpages/resolution/items.ts +2 -2
- package/src/parser/rules/block/module/listpages/resolution/wrapper.ts +3 -3
- package/src/parser/rules/block/module/listusers/resolve.ts +2 -2
- package/src/parser/rules/block/module/resolution/document.ts +357 -0
- package/src/parser/rules/block/module/resolution/resolve-async.ts +269 -0
- package/src/parser/rules/block/module/resolution/styles.ts +134 -12
- package/src/parser/rules/block/module/resolution/walk-resolve.ts +19 -1
- package/src/parser/rules/block/module/resolve.ts +32 -13
- package/src/parser/rules/block/module/types.ts +7 -2
- package/src/parser/rules/contracts/parse-context.ts +1 -0
- package/src/pipeline/index.ts +7 -0
- package/src/pipeline/process.ts +105 -0
- 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
|
+
"version": "4.4.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.
|
|
44
|
+
"@wdprlib/ast": "2.4.0"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/src/index.ts
CHANGED
|
@@ -58,6 +58,12 @@ export type {
|
|
|
58
58
|
DateItem,
|
|
59
59
|
Embed,
|
|
60
60
|
TocEntry,
|
|
61
|
+
// Gallery
|
|
62
|
+
GallerySize,
|
|
63
|
+
GalleryOrder,
|
|
64
|
+
GalleryItem,
|
|
65
|
+
GalleryContent,
|
|
66
|
+
GalleryData,
|
|
61
67
|
// Diagnostics
|
|
62
68
|
Diagnostic,
|
|
63
69
|
DiagnosticSeverity,
|
|
@@ -92,6 +98,15 @@ export { Lexer, tokenize, createToken } from "./lexer";
|
|
|
92
98
|
export type { ParserOptions } from "./parser";
|
|
93
99
|
export { Parser, parse } from "./parser";
|
|
94
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
|
+
|
|
95
110
|
// Modules (ListPages, ListUsers, IfTags, Include, etc.)
|
|
96
111
|
export type {
|
|
97
112
|
// ListPages query types
|
|
@@ -115,6 +130,7 @@ export type {
|
|
|
115
130
|
ExtractionResult,
|
|
116
131
|
// Resolution types
|
|
117
132
|
ParseFunction,
|
|
133
|
+
ModuleParseResult,
|
|
118
134
|
ModuleSourceTransform,
|
|
119
135
|
ResolveOptions,
|
|
120
136
|
// Include resolution
|
|
@@ -157,6 +173,7 @@ export {
|
|
|
157
173
|
extractIncludeReferences,
|
|
158
174
|
resolveIncludes,
|
|
159
175
|
resolveIncludesAsync,
|
|
176
|
+
resolveIncludesAsyncWithTrace,
|
|
160
177
|
resolveIncludesWithTrace,
|
|
161
178
|
// IfTags source-level preprocessing (run between include expansion and parse)
|
|
162
179
|
preprocessIftags,
|
|
@@ -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
|
|
13
|
+
if (ctx.appendImplicitFootnoteBlock) {
|
|
14
|
+
appendImplicitFootnoteBlock(cleanedChildren);
|
|
15
|
+
}
|
|
14
16
|
|
|
15
17
|
return {
|
|
16
18
|
ast: buildSyntaxTree(ctx, cleanedChildren),
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Block rule for the Wikidot `[[gallery]]` image gallery.
|
|
4
|
+
*
|
|
5
|
+
* ```
|
|
6
|
+
* [[gallery size="thumbnail" order="name" viewer="no"]]
|
|
7
|
+
* : first-image.jpg
|
|
8
|
+
* : *page/other-image.jpg link="some-page" alt="Alt text"
|
|
9
|
+
* [[/gallery]]
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* The content form requires the `]]` to be followed immediately by one or
|
|
13
|
+
* more `: source` lines and a `[[/gallery]]` close tag (Wikidot regex
|
|
14
|
+
* `\[\[gallery(\s[^\]]*?)?\]\](?:((?:\n: [^\n]+)+)\n\[\[\/gallery\]\])?`).
|
|
15
|
+
* Anything else falls back to the standalone content-less form, which shows
|
|
16
|
+
* the current page's image attachments after data resolution
|
|
17
|
+
* (`content: { type: "auto", files: null }`); the following text then
|
|
18
|
+
* parses normally.
|
|
19
|
+
*
|
|
20
|
+
* Honored opening-tag attributes are `size` (small / medium / thumbnail /
|
|
21
|
+
* square / original, fallback thumbnail), `viewer` (`"no"`/`"false"`
|
|
22
|
+
* disable the lightbox) and `order` (auto-collection sort order, with
|
|
23
|
+
* Wikidot's deprecated aliases normalized).
|
|
24
|
+
*
|
|
25
|
+
* Deliberate differences from the Wikidot parser: the opening tag must
|
|
26
|
+
* fit on one line and attribute names are lowercased with unquoted values
|
|
27
|
+
* accepted (both shared with wdpr's other block rules), and `flickr:`
|
|
28
|
+
* sources get no special treatment — they resolve like any other
|
|
29
|
+
* filename (wdpr does not call the Flickr API).
|
|
30
|
+
*
|
|
31
|
+
* @module
|
|
32
|
+
*/
|
|
33
|
+
import type { Element, GalleryItem, GalleryOrder, GallerySize } from "@wdprlib/ast";
|
|
34
|
+
import type { BlockRule, ParseContext, RuleResult } from "../../types";
|
|
35
|
+
import { parseAttributesRaw, parseBlockName } from "../utils";
|
|
36
|
+
import { parseGalleryItemLine } from "./items";
|
|
37
|
+
|
|
38
|
+
export { parseGalleryItemLine } from "./items";
|
|
39
|
+
|
|
40
|
+
const GALLERY_SIZES: readonly string[] = ["small", "medium", "thumbnail", "square", "original"];
|
|
41
|
+
|
|
42
|
+
/** Validate a size keyword, falling back to thumbnail like Wikidot. */
|
|
43
|
+
function normalizeSize(value: string | undefined): GallerySize {
|
|
44
|
+
return value !== undefined && GALLERY_SIZES.includes(value)
|
|
45
|
+
? (value as GallerySize)
|
|
46
|
+
: "thumbnail";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `viewer="no"` / `viewer="false"` disable the lightbox; anything else enables it. */
|
|
50
|
+
function normalizeViewer(value: string | undefined): boolean {
|
|
51
|
+
return value !== "no" && value !== "false";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Normalize the `order` attribute, folding Wikidot's deprecated aliases
|
|
56
|
+
* (`nameDesc`, `dateAdded`, `dateAddedDesc`) and the documented
|
|
57
|
+
* ListPages-compatibility forms (`"name desc desc"` / `"created_at desc
|
|
58
|
+
* desc"`, which mean the same as without the `desc desc`).
|
|
59
|
+
*/
|
|
60
|
+
function normalizeOrder(value: string | undefined): GalleryOrder {
|
|
61
|
+
switch (value) {
|
|
62
|
+
case "name":
|
|
63
|
+
case "name desc":
|
|
64
|
+
case "created_at":
|
|
65
|
+
case "created_at desc":
|
|
66
|
+
return value;
|
|
67
|
+
case "nameDesc":
|
|
68
|
+
return "name desc";
|
|
69
|
+
case "dateAdded":
|
|
70
|
+
return "created_at";
|
|
71
|
+
case "dateAddedDesc":
|
|
72
|
+
return "created_at desc";
|
|
73
|
+
case "name desc desc":
|
|
74
|
+
return "name";
|
|
75
|
+
case "created_at desc desc":
|
|
76
|
+
return "created_at";
|
|
77
|
+
default:
|
|
78
|
+
return "name";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
interface GalleryContentResult {
|
|
83
|
+
/** Trimmed line contents (after the leading `: `) */
|
|
84
|
+
lines: string[];
|
|
85
|
+
/** Token count from the content start (the NEWLINE after `]]`) through `[[/gallery]]` */
|
|
86
|
+
consumed: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Try to match the content form starting at `pos` (the token right after the
|
|
91
|
+
* opening tag's `]]`): one NEWLINE, then consecutive `: source` lines, then a
|
|
92
|
+
* NEWLINE directly followed by `[[/gallery]]`. Returns null when the token
|
|
93
|
+
* stream deviates from the Wikidot regex, which makes the gallery standalone.
|
|
94
|
+
*/
|
|
95
|
+
function tryParseGalleryContent(ctx: ParseContext, pos: number): GalleryContentResult | null {
|
|
96
|
+
const start = pos;
|
|
97
|
+
if (ctx.tokens[pos]?.type !== "NEWLINE") {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const lines: string[] = [];
|
|
102
|
+
let p = pos + 1;
|
|
103
|
+
|
|
104
|
+
for (;;) {
|
|
105
|
+
const colon = ctx.tokens[p];
|
|
106
|
+
if (colon?.type !== "COLON" || !colon.lineStart) {
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
// Wikidot requires a literal space after the colon (`\n: `)
|
|
110
|
+
const space = ctx.tokens[p + 1];
|
|
111
|
+
if (space?.type !== "WHITESPACE" || !space.value.startsWith(" ")) {
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let content = "";
|
|
116
|
+
let q = p + 1;
|
|
117
|
+
while (q < ctx.tokens.length) {
|
|
118
|
+
const token = ctx.tokens[q];
|
|
119
|
+
if (!token || token.type === "NEWLINE" || token.type === "EOF") {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
content += token.value;
|
|
123
|
+
q++;
|
|
124
|
+
}
|
|
125
|
+
// `: [^\n]+` needs at least one character after the space
|
|
126
|
+
if (content === " ") {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
if (ctx.tokens[q]?.type !== "NEWLINE") {
|
|
130
|
+
// line hit EOF: the close tag can no longer follow on its own line
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
lines.push(content.trim());
|
|
135
|
+
p = q + 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (lines.length === 0) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The last consumed NEWLINE must be directly followed by [[/gallery]]
|
|
143
|
+
if (ctx.tokens[p]?.type !== "BLOCK_END_OPEN") {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const nameResult = parseBlockName(ctx, p + 1);
|
|
147
|
+
if (!nameResult || nameResult.name !== "gallery") {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const closePos = p + 1 + nameResult.consumed;
|
|
151
|
+
if (ctx.tokens[closePos]?.type !== "BLOCK_CLOSE") {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return { lines, consumed: closePos + 1 - start };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const galleryRule: BlockRule = {
|
|
159
|
+
name: "gallery",
|
|
160
|
+
startTokens: ["BLOCK_OPEN"],
|
|
161
|
+
requiresLineStart: true,
|
|
162
|
+
|
|
163
|
+
parse(ctx: ParseContext): RuleResult<Element> {
|
|
164
|
+
if (ctx.tokens[ctx.pos]?.type !== "BLOCK_OPEN") {
|
|
165
|
+
return { success: false };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
let pos = ctx.pos + 1;
|
|
169
|
+
const nameResult = parseBlockName(ctx, pos);
|
|
170
|
+
if (!nameResult || nameResult.name !== "gallery") {
|
|
171
|
+
return { success: false };
|
|
172
|
+
}
|
|
173
|
+
pos += nameResult.consumed;
|
|
174
|
+
|
|
175
|
+
const attrResult = parseAttributesRaw(ctx, pos);
|
|
176
|
+
pos += attrResult.consumed;
|
|
177
|
+
|
|
178
|
+
if (ctx.tokens[pos]?.type !== "BLOCK_CLOSE") {
|
|
179
|
+
return { success: false };
|
|
180
|
+
}
|
|
181
|
+
pos++;
|
|
182
|
+
|
|
183
|
+
const size = normalizeSize(attrResult.attrs.size);
|
|
184
|
+
const viewer = normalizeViewer(attrResult.attrs.viewer);
|
|
185
|
+
const order = normalizeOrder(attrResult.attrs.order);
|
|
186
|
+
const openConsumed = pos - ctx.pos;
|
|
187
|
+
|
|
188
|
+
const content = tryParseGalleryContent(ctx, pos);
|
|
189
|
+
if (!content) {
|
|
190
|
+
return {
|
|
191
|
+
success: true,
|
|
192
|
+
elements: [
|
|
193
|
+
{
|
|
194
|
+
element: "gallery",
|
|
195
|
+
data: { size, order, viewer, content: { type: "auto", files: null } },
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
consumed: openConsumed,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const items: GalleryItem[] = content.lines.map(parseGalleryItemLine);
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
success: true,
|
|
206
|
+
elements: [
|
|
207
|
+
{
|
|
208
|
+
element: "gallery",
|
|
209
|
+
data: { size, order, viewer, content: { type: "items", items } },
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
consumed: openConsumed + content.consumed,
|
|
213
|
+
};
|
|
214
|
+
},
|
|
215
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { GalleryItem } from "@wdprlib/ast";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parse one gallery content line (the text after the leading `: `,
|
|
5
|
+
* already trimmed) into a {@link GalleryItem}.
|
|
6
|
+
*
|
|
7
|
+
* The text before the first space is the source; the rest is parsed as
|
|
8
|
+
* `key="value"` attributes of which `link` and `alt` are honored. A
|
|
9
|
+
* leading `*` on the source or on the link value requests a new window.
|
|
10
|
+
*/
|
|
11
|
+
export function parseGalleryItemLine(content: string): GalleryItem {
|
|
12
|
+
const spacePos = content.indexOf(" ");
|
|
13
|
+
let source = spacePos < 0 ? content : content.slice(0, spacePos);
|
|
14
|
+
const attrText = spacePos < 0 ? "" : content.slice(spacePos + 1);
|
|
15
|
+
|
|
16
|
+
let newWindow = false;
|
|
17
|
+
if (source.startsWith("*")) {
|
|
18
|
+
source = source.slice(1);
|
|
19
|
+
newWindow = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const attrs = parseItemAttrs(attrText);
|
|
23
|
+
let link = attrs.get("link") ?? null;
|
|
24
|
+
if (link !== null && link.startsWith("*")) {
|
|
25
|
+
newWindow = true;
|
|
26
|
+
link = link.slice(1);
|
|
27
|
+
}
|
|
28
|
+
const alt = attrs.get("alt") ?? null;
|
|
29
|
+
|
|
30
|
+
return { source, link, alt, newWindow };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Parse `key="value"` attribute pairs from a gallery item line, following
|
|
35
|
+
* the splitting behavior of Wikidot's gallery `getAttrs()`: fragments are
|
|
36
|
+
* separated by `="`, values run to the last `"` in each fragment and are
|
|
37
|
+
* backslash-unescaped, and keys are not lowercased.
|
|
38
|
+
*/
|
|
39
|
+
function parseItemAttrs(text: string): Map<string, string> {
|
|
40
|
+
const attrs = new Map<string, string>();
|
|
41
|
+
const parts = text.trim().split('="');
|
|
42
|
+
let key = parts[0]?.trim() ?? "";
|
|
43
|
+
|
|
44
|
+
for (let i = 1; i < parts.length; i++) {
|
|
45
|
+
const val = parts[i] ?? "";
|
|
46
|
+
const quotePos = val.lastIndexOf('"');
|
|
47
|
+
if (quotePos < 0) {
|
|
48
|
+
attrs.set(key, "");
|
|
49
|
+
key = val.slice(1).trim();
|
|
50
|
+
} else {
|
|
51
|
+
attrs.set(key, stripslashes(val.slice(0, quotePos)));
|
|
52
|
+
key = val.slice(quotePos + 1).trim();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return attrs;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** PHP-style `stripslashes()`: drop each escaping backslash, and a trailing lone one. */
|
|
60
|
+
function stripslashes(value: string): string {
|
|
61
|
+
return value.replace(/\\(.)/gs, "$1").replace(/\\$/, "");
|
|
62
|
+
}
|
|
@@ -49,6 +49,7 @@ import { iftagsRule } from "./iftags";
|
|
|
49
49
|
import { tocRule } from "./toc";
|
|
50
50
|
import { orphanLiRule } from "./orphan-li";
|
|
51
51
|
import { bibliographyRule } from "./bibliography";
|
|
52
|
+
import { galleryRule } from "./gallery";
|
|
52
53
|
|
|
53
54
|
export { headingRule } from "./heading";
|
|
54
55
|
export { horizontalRuleRule } from "./horizontal-rule";
|
|
@@ -79,6 +80,7 @@ export { iftagsRule } from "./iftags";
|
|
|
79
80
|
export { tocRule } from "./toc";
|
|
80
81
|
export { orphanLiRule } from "./orphan-li";
|
|
81
82
|
export { bibliographyRule } from "./bibliography";
|
|
83
|
+
export { galleryRule } from "./gallery";
|
|
82
84
|
|
|
83
85
|
/**
|
|
84
86
|
* All block rules in priority order.
|
|
@@ -120,6 +122,7 @@ export const blockRules: BlockRule[] = [
|
|
|
120
122
|
iframeRule,
|
|
121
123
|
iftagsRule,
|
|
122
124
|
bibliographyRule,
|
|
125
|
+
galleryRule,
|
|
123
126
|
divRule,
|
|
124
127
|
// paragraphRule is not included - used as fallback
|
|
125
128
|
];
|
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
* @module
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
export {
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
|
@@ -106,6 +106,7 @@ export {
|
|
|
106
106
|
extractIncludeReferences,
|
|
107
107
|
resolveIncludes,
|
|
108
108
|
resolveIncludesAsync,
|
|
109
|
+
resolveIncludesAsyncWithTrace,
|
|
109
110
|
resolveIncludesWithTrace,
|
|
110
111
|
} from "./include";
|
|
111
112
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Element } from "@wdprlib/ast";
|
|
2
2
|
import type { CompiledTemplate, ListPagesExternalData, VariableContext } from "../types";
|
|
3
|
-
import type
|
|
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
|
|
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
|
|
|
@@ -15,7 +15,7 @@ import type {
|
|
|
15
15
|
ListUsersCompiledTemplate,
|
|
16
16
|
ListUsersVariableContext,
|
|
17
17
|
} from "./types";
|
|
18
|
-
import type
|
|
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
|
}
|