@wdprlib/parser 4.3.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 +26 -38
- package/dist/index.cjs +801 -134
- package/dist/index.d.cts +195 -158
- package/dist/index.d.ts +195 -158
- package/dist/index.js +783 -113
- package/package.json +2 -2
- package/src/index.ts +11 -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/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
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type { Diagnostic, Element, ParseResult, SyntaxTree } from "@wdprlib/ast";
|
|
2
|
+
import type { DataProvider } from "../types-common";
|
|
3
|
+
import type { ModuleParseResult } from "../types";
|
|
4
|
+
import { isListPagesModule, type ListPagesModuleData } from "../listpages/resolve";
|
|
5
|
+
import type {
|
|
6
|
+
CompiledTemplate,
|
|
7
|
+
ListPagesDataRequirement,
|
|
8
|
+
ListPagesExternalData,
|
|
9
|
+
VariableContext,
|
|
10
|
+
} from "../listpages/types";
|
|
11
|
+
import { isListUsersModule } from "../listusers/resolve";
|
|
12
|
+
import type {
|
|
13
|
+
ListUsersCompiledTemplate,
|
|
14
|
+
ListUsersDataRequirement,
|
|
15
|
+
ListUsersExternalData,
|
|
16
|
+
ListUsersVariableContext,
|
|
17
|
+
} from "../listusers/types";
|
|
18
|
+
import { isTagCloudModule, resolveTagCloud } from "../tagcloud/resolve";
|
|
19
|
+
import type { TagCloudDataRequirement } from "../tagcloud/types";
|
|
20
|
+
import { resolveIfTags } from "../iftags/resolve";
|
|
21
|
+
import {
|
|
22
|
+
getGenericElementChildren,
|
|
23
|
+
listElement,
|
|
24
|
+
withGenericElementChildren,
|
|
25
|
+
} from "../walk/children";
|
|
26
|
+
import { buildListPagesDataMap, buildListUsersDataMap, buildTagCloudDataMap } from "./data-maps";
|
|
27
|
+
import { ModuleDocumentRegistry } from "./document";
|
|
28
|
+
import { collectStyles, mergeCollectedStyles } from "./styles";
|
|
29
|
+
|
|
30
|
+
export type AsyncModuleParseFunction = (source: string) => Promise<ModuleParseResult>;
|
|
31
|
+
|
|
32
|
+
export interface ResolveModulesWithAsyncParseOptions {
|
|
33
|
+
parse: AsyncModuleParseFunction;
|
|
34
|
+
compiledListPagesTemplates: Map<number, CompiledTemplate>;
|
|
35
|
+
compiledListUsersTemplates?: Map<number, ListUsersCompiledTemplate>;
|
|
36
|
+
requirements: {
|
|
37
|
+
listPages?: ListPagesDataRequirement[];
|
|
38
|
+
listUsers?: ListUsersDataRequirement[];
|
|
39
|
+
tagCloud?: TagCloudDataRequirement[];
|
|
40
|
+
};
|
|
41
|
+
urlPath?: string;
|
|
42
|
+
pageTags: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface AsyncModuleResolutionResult {
|
|
46
|
+
ast: SyntaxTree;
|
|
47
|
+
diagnostics: Diagnostic[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface ResolutionState {
|
|
51
|
+
listPagesId: number;
|
|
52
|
+
listUsersId: number;
|
|
53
|
+
tagCloudId: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ResolutionContext {
|
|
57
|
+
dataProvider: DataProvider;
|
|
58
|
+
listPagesData: Map<number, ListPagesExternalData>;
|
|
59
|
+
listUsersData: Map<number, ListUsersExternalData>;
|
|
60
|
+
tagCloudData: Awaited<ReturnType<typeof buildTagCloudDataMap>>;
|
|
61
|
+
options: ResolveModulesWithAsyncParseOptions;
|
|
62
|
+
parse: (source: string) => Promise<SyntaxTree>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function resolveModulesWithAsyncParse(
|
|
66
|
+
ast: SyntaxTree,
|
|
67
|
+
dataProvider: DataProvider,
|
|
68
|
+
options: ResolveModulesWithAsyncParseOptions,
|
|
69
|
+
): Promise<AsyncModuleResolutionResult> {
|
|
70
|
+
const registry = new ModuleDocumentRegistry();
|
|
71
|
+
registry.register(ast);
|
|
72
|
+
const parse = async (source: string): Promise<SyntaxTree> =>
|
|
73
|
+
registry.register(await options.parse(source));
|
|
74
|
+
|
|
75
|
+
const [listPagesData, listUsersData, tagCloudData] = await Promise.all([
|
|
76
|
+
buildListPagesDataMap(dataProvider, options.requirements.listPages ?? [], options.urlPath),
|
|
77
|
+
buildListUsersDataMap(dataProvider, options.requirements.listUsers ?? []),
|
|
78
|
+
buildTagCloudDataMap(dataProvider, options.requirements.tagCloud ?? []),
|
|
79
|
+
]);
|
|
80
|
+
const context: ResolutionContext = {
|
|
81
|
+
dataProvider,
|
|
82
|
+
listPagesData,
|
|
83
|
+
listUsersData,
|
|
84
|
+
tagCloudData,
|
|
85
|
+
options,
|
|
86
|
+
parse,
|
|
87
|
+
};
|
|
88
|
+
const state: ResolutionState = { listPagesId: 0, listUsersId: 0, tagCloudId: 0 };
|
|
89
|
+
const resolvedElements = await resolveElements(ast.elements, context, state);
|
|
90
|
+
const { elements, styles, anchoredStyles } = collectStyles(resolvedElements);
|
|
91
|
+
const intermediate: SyntaxTree = { ...ast, elements };
|
|
92
|
+
const mergedStyles = mergeCollectedStyles(ast.styles, styles, new Map(), anchoredStyles);
|
|
93
|
+
if (mergedStyles.length > 0) intermediate.styles = mergedStyles;
|
|
94
|
+
else delete intermediate.styles;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
ast: registry.finalize(intermediate, elements, options.pageTags),
|
|
98
|
+
diagnostics: registry.diagnostics,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function resolveElements(
|
|
103
|
+
elements: Element[],
|
|
104
|
+
context: ResolutionContext,
|
|
105
|
+
state: ResolutionState,
|
|
106
|
+
): Promise<Element[]> {
|
|
107
|
+
const result: Element[] = [];
|
|
108
|
+
for (const element of elements) {
|
|
109
|
+
if (element.element === "module") {
|
|
110
|
+
const resolved = await resolveModuleElement(element, context, state);
|
|
111
|
+
if (resolved !== null) {
|
|
112
|
+
result.push(...resolved);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (element.element === "if-tags") {
|
|
118
|
+
const resolution = resolveIfTags(element.data, context.options.pageTags);
|
|
119
|
+
if (resolution.matched) {
|
|
120
|
+
result.push(...(await resolveElements(element.data.elements, context, state)));
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
result.push(await resolveElementChildren(element, context, state));
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function resolveModuleElement(
|
|
131
|
+
element: Extract<Element, { element: "module" }>,
|
|
132
|
+
context: ResolutionContext,
|
|
133
|
+
state: ResolutionState,
|
|
134
|
+
): Promise<Element[] | null> {
|
|
135
|
+
if (isListPagesModule(element.data)) {
|
|
136
|
+
const id = state.listPagesId++;
|
|
137
|
+
const data = context.listPagesData.get(id);
|
|
138
|
+
const template = context.options.compiledListPagesTemplates.get(id);
|
|
139
|
+
if (data && template) return resolveListPagesAsync(element.data, data, template, context.parse);
|
|
140
|
+
return context.dataProvider.fetchListPages ? [] : [element];
|
|
141
|
+
}
|
|
142
|
+
if (isListUsersModule(element.data)) {
|
|
143
|
+
const id = state.listUsersId++;
|
|
144
|
+
const data = context.listUsersData.get(id);
|
|
145
|
+
const template = context.options.compiledListUsersTemplates?.get(id);
|
|
146
|
+
if (data && template) {
|
|
147
|
+
const variableContext: ListUsersVariableContext = { user: data.user };
|
|
148
|
+
return (await context.parse(template(variableContext))).elements;
|
|
149
|
+
}
|
|
150
|
+
return context.dataProvider.fetchListUsers ? [] : [element];
|
|
151
|
+
}
|
|
152
|
+
if (isTagCloudModule(element.data)) {
|
|
153
|
+
const id = state.tagCloudId++;
|
|
154
|
+
const data = context.tagCloudData.get(id);
|
|
155
|
+
if (data) return resolveTagCloud(element.data, data);
|
|
156
|
+
return context.dataProvider.fetchTagCloud ? [] : [element];
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function resolveListPagesAsync(
|
|
162
|
+
module: ListPagesModuleData,
|
|
163
|
+
data: ListPagesExternalData,
|
|
164
|
+
template: CompiledTemplate,
|
|
165
|
+
parse: (source: string) => Promise<SyntaxTree>,
|
|
166
|
+
): Promise<Element[]> {
|
|
167
|
+
if (data.pages.length === 0) return [];
|
|
168
|
+
const result: Element[] = [];
|
|
169
|
+
|
|
170
|
+
if (module["prepend-line"] && !module.separate) {
|
|
171
|
+
result.push(...(await parse(module["prepend-line"])).elements);
|
|
172
|
+
}
|
|
173
|
+
for (let i = 0; i < data.pages.length; i++) {
|
|
174
|
+
const page = data.pages[i];
|
|
175
|
+
if (!page) continue;
|
|
176
|
+
const variableContext: VariableContext = {
|
|
177
|
+
page,
|
|
178
|
+
index: i + 1,
|
|
179
|
+
total: data.totalCount,
|
|
180
|
+
limit: module.limit,
|
|
181
|
+
site: data.site,
|
|
182
|
+
};
|
|
183
|
+
const parsed = await parse(template(variableContext));
|
|
184
|
+
if (module.separate) {
|
|
185
|
+
result.push({
|
|
186
|
+
element: "container",
|
|
187
|
+
data: {
|
|
188
|
+
type: "div",
|
|
189
|
+
attributes: { class: "list-pages-item" },
|
|
190
|
+
elements: parsed.elements,
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
} else {
|
|
194
|
+
result.push(...parsed.elements);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (module["append-line"] && !module.separate) {
|
|
198
|
+
result.push(...(await parse(module["append-line"])).elements);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return module.wrapper
|
|
202
|
+
? [
|
|
203
|
+
{
|
|
204
|
+
element: "container",
|
|
205
|
+
data: {
|
|
206
|
+
type: "div",
|
|
207
|
+
attributes: { class: "list-pages-box" },
|
|
208
|
+
elements: result,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
]
|
|
212
|
+
: result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function resolveElementChildren(
|
|
216
|
+
element: Element,
|
|
217
|
+
context: ResolutionContext,
|
|
218
|
+
state: ResolutionState,
|
|
219
|
+
): Promise<Element> {
|
|
220
|
+
if (element.element === "list") {
|
|
221
|
+
const items = [];
|
|
222
|
+
for (const item of element.data.items) {
|
|
223
|
+
if (item["item-type"] === "elements") {
|
|
224
|
+
items.push({ ...item, elements: await resolveElements(item.elements, context, state) });
|
|
225
|
+
} else {
|
|
226
|
+
const resolved = (await resolveElements([listElement(item.data)], context, state))[0];
|
|
227
|
+
items.push(resolved?.element === "list" ? { ...item, data: resolved.data } : item);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { ...element, data: { ...element.data, items } };
|
|
231
|
+
}
|
|
232
|
+
if (element.element === "table") {
|
|
233
|
+
const rows = [];
|
|
234
|
+
for (const row of element.data.rows) {
|
|
235
|
+
const cells = [];
|
|
236
|
+
for (const cell of row.cells) {
|
|
237
|
+
cells.push({ ...cell, elements: await resolveElements(cell.elements, context, state) });
|
|
238
|
+
}
|
|
239
|
+
rows.push({ ...row, cells });
|
|
240
|
+
}
|
|
241
|
+
return { ...element, data: { ...element.data, rows } };
|
|
242
|
+
}
|
|
243
|
+
if (element.element === "definition-list") {
|
|
244
|
+
const entries = [];
|
|
245
|
+
for (const entry of element.data) {
|
|
246
|
+
entries.push({
|
|
247
|
+
...entry,
|
|
248
|
+
key: await resolveElements(entry.key, context, state),
|
|
249
|
+
value: await resolveElements(entry.value, context, state),
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return { ...element, data: entries };
|
|
253
|
+
}
|
|
254
|
+
if (element.element === "tab-view") {
|
|
255
|
+
const tabs = [];
|
|
256
|
+
for (const tab of element.data) {
|
|
257
|
+
tabs.push({ ...tab, elements: await resolveElements(tab.elements, context, state) });
|
|
258
|
+
}
|
|
259
|
+
return { ...element, data: tabs };
|
|
260
|
+
}
|
|
261
|
+
const children = getGenericElementChildren(element);
|
|
262
|
+
return children === null
|
|
263
|
+
? element
|
|
264
|
+
: withGenericElementChildren(element, await resolveElements(children, context, state));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function asParseResult(ast: SyntaxTree, diagnostics: Diagnostic[]): ParseResult {
|
|
268
|
+
return { ast, diagnostics };
|
|
269
|
+
}
|
|
@@ -1,41 +1,161 @@
|
|
|
1
1
|
import type { Element } from "@wdprlib/ast";
|
|
2
|
-
import { STYLE_SLOT_PREFIX } from "@wdprlib/ast";
|
|
3
|
-
import { mapElementChildren } from "../walk";
|
|
2
|
+
import { STYLE_ANCHOR_PREFIX, STYLE_SLOT_PREFIX } from "@wdprlib/ast";
|
|
3
|
+
import { mapElementChildren, walkElements } from "../walk";
|
|
4
4
|
import type { IfTagsData } from "../iftags/resolve";
|
|
5
5
|
|
|
6
6
|
type IfTagsDataWithStyleSlot = IfTagsData & {
|
|
7
|
-
_styleSlot
|
|
7
|
+
_styleSlot?: number;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
+
export type ResolvedStyleSlots = ReadonlyMap<number, readonly string[]>;
|
|
11
|
+
|
|
12
|
+
export interface StyleCollectionResult {
|
|
13
|
+
elements: Element[];
|
|
14
|
+
styles: string[];
|
|
15
|
+
anchoredStyles: string[];
|
|
16
|
+
anchors: Element[];
|
|
17
|
+
}
|
|
18
|
+
|
|
10
19
|
/**
|
|
11
20
|
* Collect and remove style elements from the AST.
|
|
12
21
|
*
|
|
13
22
|
* Unresolved `if-tags` elements receive style-slot placeholders so render-time
|
|
14
23
|
* iftags evaluation can preserve style order relative to already collected CSS.
|
|
15
24
|
*/
|
|
16
|
-
export function collectStyles(
|
|
25
|
+
export function collectStyles(
|
|
26
|
+
elements: Element[],
|
|
27
|
+
ignoredAnchors: WeakSet<Element> = new WeakSet(),
|
|
28
|
+
): StyleCollectionResult {
|
|
17
29
|
const styles: string[] = [];
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
30
|
+
const anchoredStyles: string[] = [];
|
|
31
|
+
const anchors: Element[] = [];
|
|
32
|
+
const usedSlots = collectExistingStyleSlots(elements);
|
|
33
|
+
const ctx = { nextSlotId: 0, usedSlots };
|
|
34
|
+
const filtered = collectStylesFromElements(
|
|
35
|
+
elements,
|
|
36
|
+
styles,
|
|
37
|
+
anchoredStyles,
|
|
38
|
+
anchors,
|
|
39
|
+
ignoredAnchors,
|
|
40
|
+
ctx,
|
|
41
|
+
);
|
|
42
|
+
return { elements: filtered, styles, anchoredStyles, anchors };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Preserve styles collected by earlier resolution passes while keeping
|
|
47
|
+
* unresolved IfTags style-slot markers idempotent across repeated passes.
|
|
48
|
+
*/
|
|
49
|
+
export function mergeCollectedStyles(
|
|
50
|
+
existing: readonly string[] | undefined,
|
|
51
|
+
collected: readonly string[],
|
|
52
|
+
resolvedSlots: ResolvedStyleSlots = new Map(),
|
|
53
|
+
anchoredStyles: readonly string[] = [],
|
|
54
|
+
): string[] {
|
|
55
|
+
const unanchoredPrevious = [...(existing ?? [])];
|
|
56
|
+
for (const style of anchoredStyles) removeFirst(unanchoredPrevious, style);
|
|
57
|
+
for (const style of collected) {
|
|
58
|
+
if (isStyleSlotMarker(style)) removeFirst(unanchoredPrevious, style);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const merged = unanchoredPrevious.filter((style) => !isStyleSlotMarker(style));
|
|
62
|
+
for (const style of collected) {
|
|
63
|
+
if (isStyleSlotMarker(style)) appendStyleSlot(merged, style, resolvedSlots);
|
|
64
|
+
else merged.push(style);
|
|
65
|
+
}
|
|
66
|
+
return merged;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isStyleSlotMarker(style: string): boolean {
|
|
70
|
+
return style.startsWith(STYLE_SLOT_PREFIX);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function createStyleSlotMarker(slotId: number): string {
|
|
74
|
+
return `${STYLE_SLOT_PREFIX}${slotId}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getStyleSlotId(data: IfTagsData): number | undefined {
|
|
78
|
+
const slotId = (data as IfTagsDataWithStyleSlot)._styleSlot;
|
|
79
|
+
return Number.isSafeInteger(slotId) && slotId! >= 0 ? slotId : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function removeFirst(values: string[], value: string): void {
|
|
83
|
+
const index = values.indexOf(value);
|
|
84
|
+
if (index >= 0) values.splice(index, 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function appendStyleSlot(
|
|
88
|
+
target: string[],
|
|
89
|
+
marker: string,
|
|
90
|
+
resolvedSlots: ResolvedStyleSlots,
|
|
91
|
+
): void {
|
|
92
|
+
const slotId = Number(marker.slice(STYLE_SLOT_PREFIX.length));
|
|
93
|
+
const replacement = resolvedSlots.get(slotId);
|
|
94
|
+
if (replacement) target.push(...replacement);
|
|
95
|
+
else target.push(marker);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function collectExistingStyleSlots(elements: Element[]): Set<number> {
|
|
99
|
+
const slots = new Set<number>();
|
|
100
|
+
walkElements(elements, (element) => {
|
|
101
|
+
if (element.element !== "if-tags") return;
|
|
102
|
+
const slotId = getStyleSlotId(element.data);
|
|
103
|
+
if (slotId !== undefined) slots.add(slotId);
|
|
104
|
+
});
|
|
105
|
+
return slots;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function allocateStyleSlot(ctx: StyleCollectionContext): number {
|
|
109
|
+
while (ctx.usedSlots.has(ctx.nextSlotId)) ctx.nextSlotId++;
|
|
110
|
+
const slotId = ctx.nextSlotId++;
|
|
111
|
+
ctx.usedSlots.add(slotId);
|
|
112
|
+
return slotId;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface StyleCollectionContext {
|
|
116
|
+
nextSlotId: number;
|
|
117
|
+
usedSlots: Set<number>;
|
|
21
118
|
}
|
|
22
119
|
|
|
23
120
|
function collectStylesFromElements(
|
|
24
121
|
elements: Element[],
|
|
25
122
|
styles: string[],
|
|
26
|
-
|
|
123
|
+
anchoredStyles: string[],
|
|
124
|
+
anchors: Element[],
|
|
125
|
+
ignoredAnchors: WeakSet<Element>,
|
|
126
|
+
ctx: StyleCollectionContext,
|
|
27
127
|
): Element[] {
|
|
28
128
|
const result: Element[] = [];
|
|
29
129
|
|
|
30
130
|
for (const element of elements) {
|
|
31
131
|
if (element.element === "style") {
|
|
32
|
-
|
|
132
|
+
const css = element.data as string;
|
|
133
|
+
if (css.startsWith(STYLE_SLOT_PREFIX)) {
|
|
134
|
+
styles.push(css);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
if (css.startsWith(STYLE_ANCHOR_PREFIX)) {
|
|
138
|
+
anchors.push(element);
|
|
139
|
+
if (ignoredAnchors.has(element)) {
|
|
140
|
+
result.push(element);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const anchoredCss = css.slice(STYLE_ANCHOR_PREFIX.length);
|
|
144
|
+
styles.push(anchoredCss);
|
|
145
|
+
anchoredStyles.push(anchoredCss);
|
|
146
|
+
result.push(element);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
styles.push(css);
|
|
150
|
+
const anchor: Element = { element: "style", data: `${STYLE_ANCHOR_PREFIX}${css}` };
|
|
151
|
+
anchors.push(anchor);
|
|
152
|
+
result.push(anchor);
|
|
33
153
|
continue;
|
|
34
154
|
}
|
|
35
155
|
|
|
36
156
|
if (element.element === "if-tags") {
|
|
37
|
-
const slotId = ctx
|
|
38
|
-
styles.push(
|
|
157
|
+
const slotId = getStyleSlotId(element.data) ?? allocateStyleSlot(ctx);
|
|
158
|
+
styles.push(createStyleSlotMarker(slotId));
|
|
39
159
|
const data: IfTagsDataWithStyleSlot = { ...(element.data as IfTagsData), _styleSlot: slotId };
|
|
40
160
|
result.push({
|
|
41
161
|
element: "if-tags",
|
|
@@ -45,7 +165,9 @@ function collectStylesFromElements(
|
|
|
45
165
|
}
|
|
46
166
|
|
|
47
167
|
result.push(
|
|
48
|
-
mapElementChildren(element, (children) =>
|
|
168
|
+
mapElementChildren(element, (children) =>
|
|
169
|
+
collectStylesFromElements(children, styles, anchoredStyles, anchors, ignoredAnchors, ctx),
|
|
170
|
+
),
|
|
49
171
|
);
|
|
50
172
|
}
|
|
51
173
|
|
|
@@ -3,6 +3,7 @@ import { mapElementChildrenWithState } from "../walk";
|
|
|
3
3
|
import { isIfTagsElement, resolveIfTags, type IfTagsData } from "../iftags/resolve";
|
|
4
4
|
import type { ListPagesContext, ListUsersContext, TagCloudContext } from "./contexts";
|
|
5
5
|
import { countDynamicModules, resolveDynamicModuleElement } from "./dynamic-modules";
|
|
6
|
+
import { collectStyles, createStyleSlotMarker, getStyleSlotId } from "./styles";
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Resolution context passed through AST traversal.
|
|
@@ -21,6 +22,8 @@ export interface WalkContext {
|
|
|
21
22
|
listPagesIdCounter: number;
|
|
22
23
|
listUsersIdCounter: number;
|
|
23
24
|
tagCloudIdCounter: number;
|
|
25
|
+
resolvedStyleSlots: Map<number, string[]>;
|
|
26
|
+
routedStyleAnchors: WeakSet<Element>;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export interface WalkResult {
|
|
@@ -56,6 +59,7 @@ export function walkAndResolve(elements: Element[], ctx: WalkContext): WalkResul
|
|
|
56
59
|
if (isIfTagsElement(element)) {
|
|
57
60
|
const ifTagsData = element.data as IfTagsData;
|
|
58
61
|
const resolveResult = resolveIfTags(ifTagsData, ctx.pageTags);
|
|
62
|
+
const styleSlotId = getStyleSlotId(ifTagsData);
|
|
59
63
|
|
|
60
64
|
if (resolveResult.evaluated) {
|
|
61
65
|
if (resolveResult.matched) {
|
|
@@ -65,11 +69,25 @@ export function walkAndResolve(elements: Element[], ctx: WalkContext): WalkResul
|
|
|
65
69
|
listUsersIdCounter: listUsersId,
|
|
66
70
|
tagCloudIdCounter: tagCloudId,
|
|
67
71
|
});
|
|
68
|
-
|
|
72
|
+
if (styleSlotId === undefined) {
|
|
73
|
+
result.push(...childResult.elements);
|
|
74
|
+
} else {
|
|
75
|
+
const collected = collectStyles(childResult.elements);
|
|
76
|
+
ctx.resolvedStyleSlots.set(styleSlotId, collected.styles);
|
|
77
|
+
for (const anchor of collected.anchors) ctx.routedStyleAnchors.add(anchor);
|
|
78
|
+
result.push(
|
|
79
|
+
{ element: "style", data: createStyleSlotMarker(styleSlotId) },
|
|
80
|
+
...collected.elements,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
69
83
|
listPagesId = childResult.nextListPagesId;
|
|
70
84
|
listUsersId = childResult.nextListUsersId;
|
|
71
85
|
tagCloudId = childResult.nextTagCloudId;
|
|
72
86
|
} else {
|
|
87
|
+
if (styleSlotId !== undefined) {
|
|
88
|
+
ctx.resolvedStyleSlots.set(styleSlotId, []);
|
|
89
|
+
result.push({ element: "style", data: createStyleSlotMarker(styleSlotId) });
|
|
90
|
+
}
|
|
73
91
|
const counts = countDynamicModules(ifTagsData.elements);
|
|
74
92
|
listPagesId += counts.listPagesId;
|
|
75
93
|
listUsersId += counts.listUsersId;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* @module
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import type { SyntaxTree } from "@wdprlib/ast";
|
|
19
|
+
import type { Diagnostic, Element, SyntaxTree } from "@wdprlib/ast";
|
|
20
20
|
import type { DataProvider } from "./types-common";
|
|
21
21
|
import { resolveIncludes } from "./include";
|
|
22
22
|
import type { ListPagesDataRequirement, CompiledTemplate } from "./listpages/types";
|
|
@@ -29,7 +29,8 @@ import {
|
|
|
29
29
|
buildTagCloudContext,
|
|
30
30
|
} from "./resolution/contexts";
|
|
31
31
|
import { walkAndResolve } from "./resolution/walk-resolve";
|
|
32
|
-
import { collectStyles } from "./resolution/styles";
|
|
32
|
+
import { collectStyles, mergeCollectedStyles } from "./resolution/styles";
|
|
33
|
+
import { containsSyntaxFootnoteBlock, ModuleDocumentRegistry } from "./resolution/document";
|
|
33
34
|
|
|
34
35
|
const MODULE_SECONDARY_INCLUDE_MAX_ITERATIONS = 5;
|
|
35
36
|
|
|
@@ -110,6 +111,9 @@ export interface ResolveOptions {
|
|
|
110
111
|
* state outside wdpr.
|
|
111
112
|
*/
|
|
112
113
|
transformModuleSource?: ModuleSourceTransform;
|
|
114
|
+
|
|
115
|
+
/** Receives diagnostics emitted by ListPages/ListUsers secondary parses. */
|
|
116
|
+
onDiagnostics?: (diagnostics: Diagnostic[]) => void;
|
|
113
117
|
}
|
|
114
118
|
|
|
115
119
|
/**
|
|
@@ -131,7 +135,9 @@ export async function resolveModules(
|
|
|
131
135
|
dataProvider: DataProvider,
|
|
132
136
|
options: ResolveOptions,
|
|
133
137
|
): Promise<SyntaxTree> {
|
|
134
|
-
const
|
|
138
|
+
const registry = new ModuleDocumentRegistry();
|
|
139
|
+
registry.register(ast);
|
|
140
|
+
const parse = createModuleParseFunction(options, dataProvider, registry);
|
|
135
141
|
const listPagesCtx = await buildListPagesContext(
|
|
136
142
|
dataProvider,
|
|
137
143
|
options.requirements.listPages ?? [],
|
|
@@ -147,6 +153,8 @@ export async function resolveModules(
|
|
|
147
153
|
);
|
|
148
154
|
const tagCloudCtx = await buildTagCloudContext(dataProvider, options.requirements.tagCloud ?? []);
|
|
149
155
|
const pageTags = dataProvider.getPageTags?.() ?? null;
|
|
156
|
+
const resolvedStyleSlots = new Map<number, string[]>();
|
|
157
|
+
const routedStyleAnchors = new WeakSet<Element>();
|
|
150
158
|
|
|
151
159
|
// Resolve AST
|
|
152
160
|
const resolvedElements = walkAndResolve(ast.elements, {
|
|
@@ -160,33 +168,44 @@ export async function resolveModules(
|
|
|
160
168
|
listPagesIdCounter: 0,
|
|
161
169
|
listUsersIdCounter: 0,
|
|
162
170
|
tagCloudIdCounter: 0,
|
|
171
|
+
resolvedStyleSlots,
|
|
172
|
+
routedStyleAnchors,
|
|
163
173
|
});
|
|
164
174
|
|
|
165
175
|
// Collect style elements from resolved AST
|
|
166
|
-
const {
|
|
176
|
+
const {
|
|
177
|
+
elements: finalElements,
|
|
178
|
+
styles,
|
|
179
|
+
anchoredStyles,
|
|
180
|
+
} = collectStyles(resolvedElements.elements, routedStyleAnchors);
|
|
167
181
|
|
|
168
182
|
const result: SyntaxTree = {
|
|
169
183
|
...ast,
|
|
170
184
|
elements: finalElements,
|
|
171
185
|
};
|
|
172
186
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
187
|
+
const mergedStyles = mergeCollectedStyles(ast.styles, styles, resolvedStyleSlots, anchoredStyles);
|
|
188
|
+
if (mergedStyles.length > 0) result.styles = mergedStyles;
|
|
176
189
|
|
|
177
|
-
|
|
190
|
+
options.onDiagnostics?.(registry.diagnostics);
|
|
191
|
+
return registry.finalize(
|
|
192
|
+
result,
|
|
193
|
+
finalElements,
|
|
194
|
+
pageTags,
|
|
195
|
+
containsSyntaxFootnoteBlock(ast.elements),
|
|
196
|
+
);
|
|
178
197
|
}
|
|
179
198
|
|
|
180
199
|
function createModuleParseFunction(
|
|
181
200
|
options: ResolveOptions,
|
|
182
201
|
dataProvider: DataProvider,
|
|
202
|
+
registry: ModuleDocumentRegistry,
|
|
183
203
|
): ParseFunction {
|
|
184
204
|
const transform = createModuleSourceTransform(options, dataProvider);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return (source: string) => options.parse(transform(source));
|
|
205
|
+
return (source: string) =>
|
|
206
|
+
registry.register(options.parse(transform ? transform(source) : source), {
|
|
207
|
+
stripLegacyImplicitFootnoteBlock: true,
|
|
208
|
+
});
|
|
190
209
|
}
|
|
191
210
|
|
|
192
211
|
function createModuleSourceTransform(
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* @module
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type { Element, Module } from "@wdprlib/ast";
|
|
13
|
+
import type { Element, Module, ParseResult, SyntaxTree } from "@wdprlib/ast";
|
|
14
14
|
import type { ParseContext } from "../../types";
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -23,7 +23,12 @@ import type { ParseContext } from "../../types";
|
|
|
23
23
|
* @param input - Wikitext string to parse
|
|
24
24
|
* @returns Object containing the parsed elements
|
|
25
25
|
*/
|
|
26
|
-
export type
|
|
26
|
+
export type ModuleParseResult = SyntaxTree | ParseResult;
|
|
27
|
+
export type ParseFunction = (input: string) => ModuleParseResult;
|
|
28
|
+
|
|
29
|
+
export function getModuleParseAst(result: ModuleParseResult): SyntaxTree {
|
|
30
|
+
return "ast" in result ? result.ast : result;
|
|
31
|
+
}
|
|
27
32
|
|
|
28
33
|
/**
|
|
29
34
|
* Definition of a module rule that handles a specific Wikidot module type.
|