@wdprlib/parser 4.1.0 → 4.3.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 +11 -1
- package/dist/index.cjs +476 -39
- package/dist/index.d.cts +121 -2
- package/dist/index.d.ts +121 -2
- package/dist/index.js +448 -11
- package/package.json +2 -2
- package/src/index.ts +15 -0
- 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/index.ts +10 -0
- package/src/parser/rules/block/module/listpages/extract.ts +9 -0
- package/src/parser/rules/block/module/listpages/extraction/tagcloud.ts +25 -0
- package/src/parser/rules/block/module/listpages/types/data-requirements.ts +2 -0
- package/src/parser/rules/block/module/mapping.ts +2 -0
- package/src/parser/rules/block/module/resolution/contexts.ts +29 -1
- package/src/parser/rules/block/module/resolution/data-maps.ts +17 -0
- package/src/parser/rules/block/module/resolution/dynamic-modules.ts +29 -2
- package/src/parser/rules/block/module/resolution/walk-resolve.ts +28 -4
- package/src/parser/rules/block/module/resolve.ts +11 -1
- package/src/parser/rules/block/module/tagcloud/index.ts +15 -0
- package/src/parser/rules/block/module/tagcloud/parser.ts +127 -0
- package/src/parser/rules/block/module/tagcloud/resolve.ts +173 -0
- package/src/parser/rules/block/module/tagcloud/types.ts +85 -0
- package/src/parser/rules/block/module/types-common.ts +15 -0
|
@@ -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
|
];
|
|
@@ -128,6 +128,16 @@ export {
|
|
|
128
128
|
resolveListUsers,
|
|
129
129
|
} from "./listusers";
|
|
130
130
|
|
|
131
|
+
// TagCloud module
|
|
132
|
+
export type {
|
|
133
|
+
TagCloudDataRequirement,
|
|
134
|
+
TagCloudTagData,
|
|
135
|
+
TagCloudExternalData,
|
|
136
|
+
TagCloudDataFetcher,
|
|
137
|
+
TagCloudModuleData,
|
|
138
|
+
} from "./tagcloud";
|
|
139
|
+
export { tagCloudModuleRule as tagCloudRule, isTagCloudModule, resolveTagCloud } from "./tagcloud";
|
|
140
|
+
|
|
131
141
|
// Module resolver
|
|
132
142
|
export type { ModuleSourceTransform, ResolveOptions } from "./resolve";
|
|
133
143
|
export { resolveModules } from "./resolve";
|
|
@@ -32,6 +32,11 @@ import {
|
|
|
32
32
|
isListUsersModule,
|
|
33
33
|
type ListUsersExtractionState,
|
|
34
34
|
} from "./extraction/listusers";
|
|
35
|
+
import {
|
|
36
|
+
extractTagCloudModule,
|
|
37
|
+
isTagCloudModule,
|
|
38
|
+
type TagCloudExtractionState,
|
|
39
|
+
} from "./extraction/tagcloud";
|
|
35
40
|
import type { ExtractionResult } from "./extraction/result";
|
|
36
41
|
export type { ExtractionResult } from "./extraction/result";
|
|
37
42
|
|
|
@@ -54,6 +59,7 @@ export function extractDataRequirements(ast: SyntaxTree): ExtractionResult {
|
|
|
54
59
|
requirements: {
|
|
55
60
|
listPages: [],
|
|
56
61
|
listUsers: [],
|
|
62
|
+
tagCloud: [],
|
|
57
63
|
},
|
|
58
64
|
compiledListPagesTemplates: new Map(),
|
|
59
65
|
compiledListUsersTemplates: new Map(),
|
|
@@ -61,6 +67,7 @@ export function extractDataRequirements(ast: SyntaxTree): ExtractionResult {
|
|
|
61
67
|
|
|
62
68
|
const listPagesState: ListPagesExtractionState = { nextId: 0 };
|
|
63
69
|
const listUsersState: ListUsersExtractionState = { nextId: 0 };
|
|
70
|
+
const tagCloudState: TagCloudExtractionState = { nextId: 0 };
|
|
64
71
|
|
|
65
72
|
walkElements(ast.elements, (element) => {
|
|
66
73
|
if (element.element !== "module") return;
|
|
@@ -69,6 +76,8 @@ export function extractDataRequirements(ast: SyntaxTree): ExtractionResult {
|
|
|
69
76
|
extractListPagesModule(element.data, listPagesState, result);
|
|
70
77
|
} else if (isListUsersModule(element.data)) {
|
|
71
78
|
extractListUsersModule(element.data, listUsersState, result);
|
|
79
|
+
} else if (isTagCloudModule(element.data)) {
|
|
80
|
+
extractTagCloudModule(element.data, tagCloudState, result);
|
|
72
81
|
}
|
|
73
82
|
});
|
|
74
83
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Module } from "@wdprlib/ast";
|
|
2
|
+
import { isTagCloudModule } from "../../tagcloud/resolve";
|
|
3
|
+
import type { ExtractionResult } from "./result";
|
|
4
|
+
|
|
5
|
+
export type TagCloudModuleForExtraction = Extract<Module, { module: "tag-cloud" }>;
|
|
6
|
+
|
|
7
|
+
export interface TagCloudExtractionState {
|
|
8
|
+
nextId: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { isTagCloudModule };
|
|
12
|
+
|
|
13
|
+
export function extractTagCloudModule(
|
|
14
|
+
tagCloud: TagCloudModuleForExtraction,
|
|
15
|
+
state: TagCloudExtractionState,
|
|
16
|
+
result: ExtractionResult,
|
|
17
|
+
): void {
|
|
18
|
+
const id = state.nextId++;
|
|
19
|
+
|
|
20
|
+
result.requirements.tagCloud.push({
|
|
21
|
+
id,
|
|
22
|
+
category: tagCloud.category,
|
|
23
|
+
limit: tagCloud.limit,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ListUsersDataRequirement } from "../../listusers/types";
|
|
2
|
+
import type { TagCloudDataRequirement } from "../../tagcloud/types";
|
|
2
3
|
import type { ListPagesQuery } from "./query";
|
|
3
4
|
import type { ListPagesVariable } from "./variables";
|
|
4
5
|
|
|
@@ -49,4 +50,5 @@ export interface ListPagesDataRequirement {
|
|
|
49
50
|
export interface DataRequirements {
|
|
50
51
|
listPages: ListPagesDataRequirement[];
|
|
51
52
|
listUsers: ListUsersDataRequirement[];
|
|
53
|
+
tagCloud: TagCloudDataRequirement[];
|
|
52
54
|
}
|
|
@@ -19,6 +19,7 @@ import { joinModuleRule } from "./join/index";
|
|
|
19
19
|
import { pageTreeModuleRule } from "./page-tree/index";
|
|
20
20
|
import { listPagesModuleRule } from "./listpages/parser";
|
|
21
21
|
import { listUsersModuleRule } from "./listusers/parser";
|
|
22
|
+
import { tagCloudModuleRule } from "./tagcloud/parser";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Complete list of all registered module rules.
|
|
@@ -35,6 +36,7 @@ export const MODULE_RULES: ModuleRule[] = [
|
|
|
35
36
|
pageTreeModuleRule,
|
|
36
37
|
listPagesModuleRule,
|
|
37
38
|
listUsersModuleRule,
|
|
39
|
+
tagCloudModuleRule,
|
|
38
40
|
];
|
|
39
41
|
|
|
40
42
|
/**
|
|
@@ -9,8 +9,9 @@ import type {
|
|
|
9
9
|
ListUsersDataRequirement,
|
|
10
10
|
ListUsersExternalData,
|
|
11
11
|
} from "../listusers/types";
|
|
12
|
+
import type { TagCloudDataRequirement, TagCloudExternalData } from "../tagcloud/types";
|
|
12
13
|
import type { ParseFunction } from "../listpages/resolve";
|
|
13
|
-
import { buildListPagesDataMap, buildListUsersDataMap } from "./data-maps";
|
|
14
|
+
import { buildListPagesDataMap, buildListUsersDataMap, buildTagCloudDataMap } from "./data-maps";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Context for ListPages resolution.
|
|
@@ -54,6 +55,33 @@ export async function buildListPagesContext(
|
|
|
54
55
|
};
|
|
55
56
|
}
|
|
56
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Context for TagCloud resolution.
|
|
60
|
+
*
|
|
61
|
+
* TagCloud has no template body, so unlike ListPages/ListUsers the context
|
|
62
|
+
* carries only the fetched data.
|
|
63
|
+
*/
|
|
64
|
+
export interface TagCloudContext {
|
|
65
|
+
dataMap: Map<number, TagCloudExternalData>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function buildTagCloudContext(
|
|
69
|
+
dataProvider: DataProvider,
|
|
70
|
+
requirements: TagCloudDataRequirement[],
|
|
71
|
+
): Promise<TagCloudContext | null> {
|
|
72
|
+
if (requirements.length === 0 || !dataProvider.fetchTagCloud) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const dataMap = await buildTagCloudDataMap(dataProvider, requirements);
|
|
77
|
+
|
|
78
|
+
if (dataMap.size === 0) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { dataMap };
|
|
83
|
+
}
|
|
84
|
+
|
|
57
85
|
export async function buildListUsersContext(
|
|
58
86
|
dataProvider: DataProvider,
|
|
59
87
|
requirements: ListUsersDataRequirement[],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DataProvider } from "../types-common";
|
|
2
2
|
import type { ListPagesDataRequirement, ListPagesExternalData } from "../listpages/types";
|
|
3
3
|
import type { ListUsersDataRequirement, ListUsersExternalData } from "../listusers/types";
|
|
4
|
+
import type { TagCloudDataRequirement, TagCloudExternalData } from "../tagcloud/types";
|
|
4
5
|
import { parseUrlParams, resolveAndNormalizeQuery } from "../listpages/url-resolver";
|
|
5
6
|
|
|
6
7
|
export async function buildListPagesDataMap(
|
|
@@ -37,3 +38,19 @@ export async function buildListUsersDataMap(
|
|
|
37
38
|
|
|
38
39
|
return dataMap;
|
|
39
40
|
}
|
|
41
|
+
|
|
42
|
+
export async function buildTagCloudDataMap(
|
|
43
|
+
dataProvider: DataProvider,
|
|
44
|
+
requirements: TagCloudDataRequirement[],
|
|
45
|
+
): Promise<Map<number, TagCloudExternalData>> {
|
|
46
|
+
const dataMap = new Map<number, TagCloudExternalData>();
|
|
47
|
+
|
|
48
|
+
for (const req of requirements) {
|
|
49
|
+
const data = await dataProvider.fetchTagCloud?.(req);
|
|
50
|
+
if (data) {
|
|
51
|
+
dataMap.set(req.id, data);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return dataMap;
|
|
56
|
+
}
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
import type { Element } from "@wdprlib/ast";
|
|
2
2
|
import { isListPagesModule, resolveListPages } from "../listpages/resolve";
|
|
3
3
|
import { isListUsersModule, resolveListUsers } from "../listusers/resolve";
|
|
4
|
+
import { isTagCloudModule, resolveTagCloud } from "../tagcloud/resolve";
|
|
4
5
|
import { walkElements } from "../walk";
|
|
5
|
-
import type { ListPagesContext, ListUsersContext } from "./contexts";
|
|
6
|
+
import type { ListPagesContext, ListUsersContext, TagCloudContext } from "./contexts";
|
|
6
7
|
|
|
7
8
|
export interface DynamicModuleContext {
|
|
8
9
|
listPages: ListPagesContext | null;
|
|
9
10
|
listUsers: ListUsersContext | null;
|
|
11
|
+
tagCloud: TagCloudContext | null;
|
|
10
12
|
fetchListPagesProvided: boolean;
|
|
11
13
|
fetchListUsersProvided: boolean;
|
|
14
|
+
fetchTagCloudProvided: boolean;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export interface DynamicModuleIds {
|
|
15
18
|
listPagesId: number;
|
|
16
19
|
listUsersId: number;
|
|
20
|
+
tagCloudId: number;
|
|
17
21
|
}
|
|
18
22
|
|
|
19
23
|
export interface DynamicModuleResolution {
|
|
@@ -75,11 +79,32 @@ export function resolveDynamicModuleElement(
|
|
|
75
79
|
};
|
|
76
80
|
}
|
|
77
81
|
|
|
82
|
+
if (isTagCloudModule(element.data)) {
|
|
83
|
+
const elements: Element[] = [];
|
|
84
|
+
const tagCloudId = ids.tagCloudId;
|
|
85
|
+
|
|
86
|
+
if (ctx.tagCloud) {
|
|
87
|
+
const moduleData = ctx.tagCloud.dataMap.get(tagCloudId);
|
|
88
|
+
|
|
89
|
+
if (moduleData) {
|
|
90
|
+
elements.push(...resolveTagCloud(element.data, moduleData));
|
|
91
|
+
}
|
|
92
|
+
} else if (!ctx.fetchTagCloudProvided) {
|
|
93
|
+
elements.push(element);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
handled: true,
|
|
98
|
+
elements,
|
|
99
|
+
ids: { ...ids, tagCloudId: tagCloudId + 1 },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
78
103
|
return { handled: false, elements: [element], ids };
|
|
79
104
|
}
|
|
80
105
|
|
|
81
106
|
export function countDynamicModules(elements: Element[]): DynamicModuleIds {
|
|
82
|
-
const counts: DynamicModuleIds = { listPagesId: 0, listUsersId: 0 };
|
|
107
|
+
const counts: DynamicModuleIds = { listPagesId: 0, listUsersId: 0, tagCloudId: 0 };
|
|
83
108
|
walkElements(elements, (element) => {
|
|
84
109
|
if (element.element !== "module") return;
|
|
85
110
|
|
|
@@ -87,6 +112,8 @@ export function countDynamicModules(elements: Element[]): DynamicModuleIds {
|
|
|
87
112
|
counts.listPagesId++;
|
|
88
113
|
} else if (isListUsersModule(element.data)) {
|
|
89
114
|
counts.listUsersId++;
|
|
115
|
+
} else if (isTagCloudModule(element.data)) {
|
|
116
|
+
counts.tagCloudId++;
|
|
90
117
|
}
|
|
91
118
|
});
|
|
92
119
|
return counts;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Element } from "@wdprlib/ast";
|
|
2
2
|
import { mapElementChildrenWithState } from "../walk";
|
|
3
3
|
import { isIfTagsElement, resolveIfTags, type IfTagsData } from "../iftags/resolve";
|
|
4
|
-
import type { ListPagesContext, ListUsersContext } from "./contexts";
|
|
4
|
+
import type { ListPagesContext, ListUsersContext, TagCloudContext } from "./contexts";
|
|
5
5
|
import { countDynamicModules, resolveDynamicModuleElement } from "./dynamic-modules";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -10,19 +10,24 @@ import { countDynamicModules, resolveDynamicModuleElement } from "./dynamic-modu
|
|
|
10
10
|
export interface WalkContext {
|
|
11
11
|
listPages: ListPagesContext | null;
|
|
12
12
|
listUsers: ListUsersContext | null;
|
|
13
|
+
tagCloud: TagCloudContext | null;
|
|
13
14
|
/** Whether fetchListPages callback was provided, even if no data returned. */
|
|
14
15
|
fetchListPagesProvided: boolean;
|
|
15
16
|
/** Whether fetchListUsers callback was provided, even if no data returned. */
|
|
16
17
|
fetchListUsersProvided: boolean;
|
|
18
|
+
/** Whether fetchTagCloud callback was provided, even if no data returned. */
|
|
19
|
+
fetchTagCloudProvided: boolean;
|
|
17
20
|
pageTags: string[] | null;
|
|
18
21
|
listPagesIdCounter: number;
|
|
19
22
|
listUsersIdCounter: number;
|
|
23
|
+
tagCloudIdCounter: number;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
export interface WalkResult {
|
|
23
27
|
elements: Element[];
|
|
24
28
|
nextListPagesId: number;
|
|
25
29
|
nextListUsersId: number;
|
|
30
|
+
nextTagCloudId: number;
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
/**
|
|
@@ -32,13 +37,19 @@ export function walkAndResolve(elements: Element[], ctx: WalkContext): WalkResul
|
|
|
32
37
|
const result: Element[] = [];
|
|
33
38
|
let listPagesId = ctx.listPagesIdCounter;
|
|
34
39
|
let listUsersId = ctx.listUsersIdCounter;
|
|
40
|
+
let tagCloudId = ctx.tagCloudIdCounter;
|
|
35
41
|
|
|
36
42
|
for (const element of elements) {
|
|
37
|
-
const dynamicModule = resolveDynamicModuleElement(element, ctx, {
|
|
43
|
+
const dynamicModule = resolveDynamicModuleElement(element, ctx, {
|
|
44
|
+
listPagesId,
|
|
45
|
+
listUsersId,
|
|
46
|
+
tagCloudId,
|
|
47
|
+
});
|
|
38
48
|
if (dynamicModule.handled) {
|
|
39
49
|
result.push(...dynamicModule.elements);
|
|
40
50
|
listPagesId = dynamicModule.ids.listPagesId;
|
|
41
51
|
listUsersId = dynamicModule.ids.listUsersId;
|
|
52
|
+
tagCloudId = dynamicModule.ids.tagCloudId;
|
|
42
53
|
continue;
|
|
43
54
|
}
|
|
44
55
|
|
|
@@ -52,20 +63,24 @@ export function walkAndResolve(elements: Element[], ctx: WalkContext): WalkResul
|
|
|
52
63
|
...ctx,
|
|
53
64
|
listPagesIdCounter: listPagesId,
|
|
54
65
|
listUsersIdCounter: listUsersId,
|
|
66
|
+
tagCloudIdCounter: tagCloudId,
|
|
55
67
|
});
|
|
56
68
|
result.push(...childResult.elements);
|
|
57
69
|
listPagesId = childResult.nextListPagesId;
|
|
58
70
|
listUsersId = childResult.nextListUsersId;
|
|
71
|
+
tagCloudId = childResult.nextTagCloudId;
|
|
59
72
|
} else {
|
|
60
73
|
const counts = countDynamicModules(ifTagsData.elements);
|
|
61
74
|
listPagesId += counts.listPagesId;
|
|
62
75
|
listUsersId += counts.listUsersId;
|
|
76
|
+
tagCloudId += counts.tagCloudId;
|
|
63
77
|
}
|
|
64
78
|
} else {
|
|
65
79
|
const childResult = walkAndResolve(ifTagsData.elements, {
|
|
66
80
|
...ctx,
|
|
67
81
|
listPagesIdCounter: listPagesId,
|
|
68
82
|
listUsersIdCounter: listUsersId,
|
|
83
|
+
tagCloudIdCounter: tagCloudId,
|
|
69
84
|
});
|
|
70
85
|
result.push({
|
|
71
86
|
element: "if-tags",
|
|
@@ -76,24 +91,27 @@ export function walkAndResolve(elements: Element[], ctx: WalkContext): WalkResul
|
|
|
76
91
|
});
|
|
77
92
|
listPagesId = childResult.nextListPagesId;
|
|
78
93
|
listUsersId = childResult.nextListUsersId;
|
|
94
|
+
tagCloudId = childResult.nextTagCloudId;
|
|
79
95
|
}
|
|
80
96
|
continue;
|
|
81
97
|
}
|
|
82
98
|
|
|
83
99
|
const mapped = mapElementChildrenWithState(
|
|
84
100
|
element,
|
|
85
|
-
{ listPagesId, listUsersId },
|
|
101
|
+
{ listPagesId, listUsersId, tagCloudId },
|
|
86
102
|
(children, state) => {
|
|
87
103
|
const childResult = walkAndResolve(children, {
|
|
88
104
|
...ctx,
|
|
89
105
|
listPagesIdCounter: state.listPagesId,
|
|
90
106
|
listUsersIdCounter: state.listUsersId,
|
|
107
|
+
tagCloudIdCounter: state.tagCloudId,
|
|
91
108
|
});
|
|
92
109
|
return {
|
|
93
110
|
elements: childResult.elements,
|
|
94
111
|
state: {
|
|
95
112
|
listPagesId: childResult.nextListPagesId,
|
|
96
113
|
listUsersId: childResult.nextListUsersId,
|
|
114
|
+
tagCloudId: childResult.nextTagCloudId,
|
|
97
115
|
},
|
|
98
116
|
};
|
|
99
117
|
},
|
|
@@ -101,7 +119,13 @@ export function walkAndResolve(elements: Element[], ctx: WalkContext): WalkResul
|
|
|
101
119
|
result.push(mapped.element);
|
|
102
120
|
listPagesId = mapped.state.listPagesId;
|
|
103
121
|
listUsersId = mapped.state.listUsersId;
|
|
122
|
+
tagCloudId = mapped.state.tagCloudId;
|
|
104
123
|
}
|
|
105
124
|
|
|
106
|
-
return {
|
|
125
|
+
return {
|
|
126
|
+
elements: result,
|
|
127
|
+
nextListPagesId: listPagesId,
|
|
128
|
+
nextListUsersId: listUsersId,
|
|
129
|
+
nextTagCloudId: tagCloudId,
|
|
130
|
+
};
|
|
107
131
|
}
|