@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.
@@ -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, { listPagesId, listUsersId });
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 { elements: result, nextListPagesId: listPagesId, nextListUsersId: listUsersId };
125
+ return {
126
+ elements: result,
127
+ nextListPagesId: listPagesId,
128
+ nextListUsersId: listUsersId,
129
+ nextTagCloudId: tagCloudId,
130
+ };
107
131
  }