@wdprlib/parser 4.1.0 → 4.2.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/dist/index.js CHANGED
@@ -4468,6 +4468,92 @@ var listUsersModuleRule = {
4468
4468
  }
4469
4469
  };
4470
4470
 
4471
+ // packages/parser/src/parser/rules/block/module/tagcloud/parser.ts
4472
+ import { CSS_LENGTH_UNITS } from "@wdprlib/ast";
4473
+ var FONT_SIZE_PATTERN = new RegExp(`^([0-9]+)(${CSS_LENGTH_UNITS.join("|")})$`, "i");
4474
+ var COLOR_PATTERN = /^[0-9]+,[0-9]+,[0-9]+$/;
4475
+ var POSITIVE_INT_PATTERN = /^[0-9]+$/;
4476
+ var ERROR_FONT_FORMAT = "Unsupported format for font size. Use a number followed by a CSS length unit such as px, em or %.";
4477
+ var ERROR_FONT_MISMATCH = "Format for minFontSize and maxFontSize must use the same unit.";
4478
+ var ERROR_COLOR_FORMAT = 'Unsupported color format. Use "RRR,GGG,BBB" for Red,Green,Blue each within 0-255 range.';
4479
+ function errorBlock(message) {
4480
+ return {
4481
+ element: "container",
4482
+ data: {
4483
+ type: "div",
4484
+ attributes: { class: "error-block" },
4485
+ elements: [{ element: "text", data: message }]
4486
+ }
4487
+ };
4488
+ }
4489
+ function parseLimit(value) {
4490
+ if (!value || !POSITIVE_INT_PATTERN.test(value))
4491
+ return 50;
4492
+ const num = Number.parseInt(value, 10);
4493
+ return Number.isSafeInteger(num) && num > 0 ? num : 50;
4494
+ }
4495
+ function normalizeTarget(value) {
4496
+ if (!value)
4497
+ return "/system:page-tags/tag/";
4498
+ let target = value;
4499
+ if (!target.startsWith("/"))
4500
+ target = `/${target}`;
4501
+ if (!target.endsWith("/"))
4502
+ target = `${target}/`;
4503
+ return `${target}tag/`;
4504
+ }
4505
+ function parseColor(value) {
4506
+ const [r = 0, g = 0, b = 0] = value.split(",").map((part) => Number.parseInt(part, 10));
4507
+ return [r, g, b];
4508
+ }
4509
+ var tagCloudModuleRule = {
4510
+ name: "module-tagcloud",
4511
+ acceptsNames: ["tagcloud"],
4512
+ hasBody: false,
4513
+ parse(_ctx, _pos, args) {
4514
+ const maxFontSize = args.maxfontsize;
4515
+ const minFontSize = args.minfontsize;
4516
+ const maxColor = args.maxcolor;
4517
+ const minColor = args.mincolor;
4518
+ let sizeSmall = 100;
4519
+ let sizeBig = 300;
4520
+ let fontSizeUnit = "%";
4521
+ if (maxFontSize && minFontSize) {
4522
+ const maxMatch = FONT_SIZE_PATTERN.exec(maxFontSize);
4523
+ if (!maxMatch)
4524
+ return errorBlock(ERROR_FONT_FORMAT);
4525
+ const maxUnit = maxMatch[2].toLowerCase();
4526
+ const minMatch = FONT_SIZE_PATTERN.exec(minFontSize);
4527
+ if (!minMatch || minMatch[2].toLowerCase() !== maxUnit) {
4528
+ return errorBlock(ERROR_FONT_MISMATCH);
4529
+ }
4530
+ sizeBig = Number.parseInt(maxMatch[1], 10);
4531
+ sizeSmall = Number.parseInt(minMatch[1], 10);
4532
+ fontSizeUnit = maxUnit;
4533
+ }
4534
+ let colorSmall = [128, 128, 192];
4535
+ let colorBig = [64, 64, 128];
4536
+ if (maxColor && minColor) {
4537
+ if (!COLOR_PATTERN.test(maxColor) || !COLOR_PATTERN.test(minColor)) {
4538
+ return errorBlock(ERROR_COLOR_FORMAT);
4539
+ }
4540
+ colorSmall = parseColor(minColor);
4541
+ colorBig = parseColor(maxColor);
4542
+ }
4543
+ return {
4544
+ module: "tag-cloud",
4545
+ "min-font-size": sizeSmall,
4546
+ "max-font-size": sizeBig,
4547
+ "font-size-unit": fontSizeUnit,
4548
+ "min-color": colorSmall,
4549
+ "max-color": colorBig,
4550
+ target: normalizeTarget(args.target),
4551
+ limit: parseLimit(args.limit),
4552
+ category: args.category || null
4553
+ };
4554
+ }
4555
+ };
4556
+
4471
4557
  // packages/parser/src/parser/rules/block/module/mapping.ts
4472
4558
  var MODULE_RULES = [
4473
4559
  rateModuleRule,
@@ -4477,7 +4563,8 @@ var MODULE_RULES = [
4477
4563
  joinModuleRule,
4478
4564
  pageTreeModuleRule,
4479
4565
  listPagesModuleRule,
4480
- listUsersModuleRule
4566
+ listUsersModuleRule,
4567
+ tagCloudModuleRule
4481
4568
  ];
4482
4569
  var moduleRuleMap = new Map;
4483
4570
  for (const rule of MODULE_RULES) {
@@ -5558,18 +5645,130 @@ function extractListUsersModule(listUsers, state, result) {
5558
5645
  result.compiledListUsersTemplates.set(id, compileListUsersTemplate(body));
5559
5646
  }
5560
5647
 
5648
+ // packages/parser/src/parser/rules/block/module/tagcloud/resolve.ts
5649
+ function isTagCloudModule(module) {
5650
+ return module.module === "tag-cloud";
5651
+ }
5652
+ var NO_TAGS_MESSAGE_PREFIX = "It seems you have no tags attached to pages. To attach a tag simply click on the ";
5653
+ var NO_TAGS_MESSAGE_SUFFIX = " button at the bottom of any page.";
5654
+ function rawUrlEncode(value) {
5655
+ return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
5656
+ }
5657
+ function selectTags(tags, limit) {
5658
+ return tags.toSorted((a, b) => b.weight - a.weight || compareTags(a.tag, b.tag)).slice(0, limit).toSorted((a, b) => compareTags(a.tag, b.tag));
5659
+ }
5660
+ function compareTags(a, b) {
5661
+ return a < b ? -1 : a > b ? 1 : 0;
5662
+ }
5663
+ function resolveTagCloud(module, data) {
5664
+ if (data.status === "category-not-found") {
5665
+ return [
5666
+ {
5667
+ element: "container",
5668
+ data: {
5669
+ type: "div",
5670
+ attributes: { class: "error-block" },
5671
+ elements: [{ element: "text", data: `Category "${data.category}" can not be found.` }]
5672
+ }
5673
+ }
5674
+ ];
5675
+ }
5676
+ const tags = selectTags(data.tags, module.limit);
5677
+ if (tags.length === 0) {
5678
+ return [noTagsMessage()];
5679
+ }
5680
+ let minWeight = Number.POSITIVE_INFINITY;
5681
+ let maxWeight = Number.NEGATIVE_INFINITY;
5682
+ for (const tag of tags) {
5683
+ if (tag.weight < minWeight)
5684
+ minWeight = tag.weight;
5685
+ if (tag.weight > maxWeight)
5686
+ maxWeight = tag.weight;
5687
+ }
5688
+ const weightRange = maxWeight - minWeight;
5689
+ const categorySuffix = data.category !== null ? `/category/${rawUrlEncode(data.category)}` : "";
5690
+ const anchors = [];
5691
+ for (const tag of tags) {
5692
+ const a = weightRange === 0 ? 0 : (tag.weight - minWeight) / weightRange;
5693
+ const fontSize = interpolate(module["min-font-size"], module["max-font-size"], a);
5694
+ const [r, g, b] = [0, 1, 2].map((i) => interpolate(module["min-color"][i], module["max-color"][i], a));
5695
+ anchors.push({ element: "text", data: `
5696
+ ` });
5697
+ anchors.push({
5698
+ element: "anchor",
5699
+ data: {
5700
+ target: null,
5701
+ attributes: {
5702
+ class: "tag",
5703
+ href: `${module.target}${rawUrlEncode(tag.tag)}${categorySuffix}`,
5704
+ style: `font-size: ${fontSize}${module["font-size-unit"]}; color: rgb(${r}, ${g}, ${b});`
5705
+ },
5706
+ elements: [{ element: "text", data: tag.tag }]
5707
+ }
5708
+ });
5709
+ }
5710
+ anchors.push({ element: "text", data: `
5711
+ ` });
5712
+ return [
5713
+ {
5714
+ element: "container",
5715
+ data: {
5716
+ type: "div",
5717
+ attributes: { class: "pages-tag-cloud-box" },
5718
+ elements: anchors
5719
+ }
5720
+ }
5721
+ ];
5722
+ }
5723
+ function interpolate(min, max, a) {
5724
+ return Math.round(min + (max - min) * a);
5725
+ }
5726
+ function noTagsMessage() {
5727
+ return {
5728
+ element: "container",
5729
+ data: {
5730
+ type: "paragraph",
5731
+ attributes: {},
5732
+ elements: [
5733
+ { element: "text", data: NO_TAGS_MESSAGE_PREFIX },
5734
+ {
5735
+ element: "container",
5736
+ data: {
5737
+ type: "italics",
5738
+ attributes: {},
5739
+ elements: [{ element: "text", data: "tags" }]
5740
+ }
5741
+ },
5742
+ { element: "text", data: NO_TAGS_MESSAGE_SUFFIX }
5743
+ ]
5744
+ }
5745
+ };
5746
+ }
5747
+
5748
+ // packages/parser/src/parser/rules/block/module/listpages/extraction/tagcloud.ts
5749
+ function extractTagCloudModule(tagCloud, state, result) {
5750
+ const id = state.nextId++;
5751
+ result.requirements.tagCloud.push({
5752
+ id,
5753
+ category: tagCloud.category,
5754
+ limit: tagCloud.limit
5755
+ });
5756
+ }
5757
+
5561
5758
  // packages/parser/src/parser/rules/block/module/listpages/extract.ts
5562
5759
  function extractDataRequirements(ast) {
5563
5760
  const result = {
5564
5761
  requirements: {
5565
5762
  listPages: [],
5566
- listUsers: []
5763
+ listUsers: [],
5764
+ tagCloud: []
5567
5765
  },
5568
5766
  compiledListPagesTemplates: new Map,
5569
5767
  compiledListUsersTemplates: new Map
5570
5768
  };
5571
5769
  const listPagesState = { nextId: 0 };
5572
5770
  const listUsersState = { nextId: 0 };
5771
+ const tagCloudState = { nextId: 0 };
5573
5772
  walkElements(ast.elements, (element) => {
5574
5773
  if (element.element !== "module")
5575
5774
  return;
@@ -5577,6 +5776,8 @@ function extractDataRequirements(ast) {
5577
5776
  extractListPagesModule(element.data, listPagesState, result);
5578
5777
  } else if (isListUsersModule(element.data)) {
5579
5778
  extractListUsersModule(element.data, listUsersState, result);
5779
+ } else if (isTagCloudModule(element.data)) {
5780
+ extractTagCloudModule(element.data, tagCloudState, result);
5580
5781
  }
5581
5782
  });
5582
5783
  return result;
@@ -6692,6 +6893,16 @@ async function buildListUsersDataMap(dataProvider, requirements) {
6692
6893
  }
6693
6894
  return dataMap;
6694
6895
  }
6896
+ async function buildTagCloudDataMap(dataProvider, requirements) {
6897
+ const dataMap = new Map;
6898
+ for (const req of requirements) {
6899
+ const data = await dataProvider.fetchTagCloud?.(req);
6900
+ if (data) {
6901
+ dataMap.set(req.id, data);
6902
+ }
6903
+ }
6904
+ return dataMap;
6905
+ }
6695
6906
 
6696
6907
  // packages/parser/src/parser/rules/block/module/resolution/contexts.ts
6697
6908
  async function buildListPagesContext(dataProvider, requirements, compiledTemplates, parse, urlPath) {
@@ -6708,6 +6919,16 @@ async function buildListPagesContext(dataProvider, requirements, compiledTemplat
6708
6919
  parse
6709
6920
  };
6710
6921
  }
6922
+ async function buildTagCloudContext(dataProvider, requirements) {
6923
+ if (requirements.length === 0 || !dataProvider.fetchTagCloud) {
6924
+ return null;
6925
+ }
6926
+ const dataMap = await buildTagCloudDataMap(dataProvider, requirements);
6927
+ if (dataMap.size === 0) {
6928
+ return null;
6929
+ }
6930
+ return { dataMap };
6931
+ }
6711
6932
  async function buildListUsersContext(dataProvider, requirements, compiledTemplates, parse) {
6712
6933
  if (requirements.length === 0 || !dataProvider.fetchListUsers) {
6713
6934
  return null;
@@ -6764,10 +6985,27 @@ function resolveDynamicModuleElement(element, ctx, ids) {
6764
6985
  ids: { ...ids, listUsersId: listUsersId + 1 }
6765
6986
  };
6766
6987
  }
6988
+ if (isTagCloudModule(element.data)) {
6989
+ const elements = [];
6990
+ const tagCloudId = ids.tagCloudId;
6991
+ if (ctx.tagCloud) {
6992
+ const moduleData = ctx.tagCloud.dataMap.get(tagCloudId);
6993
+ if (moduleData) {
6994
+ elements.push(...resolveTagCloud(element.data, moduleData));
6995
+ }
6996
+ } else if (!ctx.fetchTagCloudProvided) {
6997
+ elements.push(element);
6998
+ }
6999
+ return {
7000
+ handled: true,
7001
+ elements,
7002
+ ids: { ...ids, tagCloudId: tagCloudId + 1 }
7003
+ };
7004
+ }
6767
7005
  return { handled: false, elements: [element], ids };
6768
7006
  }
6769
7007
  function countDynamicModules(elements) {
6770
- const counts = { listPagesId: 0, listUsersId: 0 };
7008
+ const counts = { listPagesId: 0, listUsersId: 0, tagCloudId: 0 };
6771
7009
  walkElements(elements, (element) => {
6772
7010
  if (element.element !== "module")
6773
7011
  return;
@@ -6775,6 +7013,8 @@ function countDynamicModules(elements) {
6775
7013
  counts.listPagesId++;
6776
7014
  } else if (isListUsersModule2(element.data)) {
6777
7015
  counts.listUsersId++;
7016
+ } else if (isTagCloudModule(element.data)) {
7017
+ counts.tagCloudId++;
6778
7018
  }
6779
7019
  });
6780
7020
  return counts;
@@ -6785,12 +7025,18 @@ function walkAndResolve(elements, ctx) {
6785
7025
  const result = [];
6786
7026
  let listPagesId = ctx.listPagesIdCounter;
6787
7027
  let listUsersId = ctx.listUsersIdCounter;
7028
+ let tagCloudId = ctx.tagCloudIdCounter;
6788
7029
  for (const element of elements) {
6789
- const dynamicModule = resolveDynamicModuleElement(element, ctx, { listPagesId, listUsersId });
7030
+ const dynamicModule = resolveDynamicModuleElement(element, ctx, {
7031
+ listPagesId,
7032
+ listUsersId,
7033
+ tagCloudId
7034
+ });
6790
7035
  if (dynamicModule.handled) {
6791
7036
  result.push(...dynamicModule.elements);
6792
7037
  listPagesId = dynamicModule.ids.listPagesId;
6793
7038
  listUsersId = dynamicModule.ids.listUsersId;
7039
+ tagCloudId = dynamicModule.ids.tagCloudId;
6794
7040
  continue;
6795
7041
  }
6796
7042
  if (isIfTagsElement(element)) {
@@ -6801,21 +7047,25 @@ function walkAndResolve(elements, ctx) {
6801
7047
  const childResult = walkAndResolve(ifTagsData.elements, {
6802
7048
  ...ctx,
6803
7049
  listPagesIdCounter: listPagesId,
6804
- listUsersIdCounter: listUsersId
7050
+ listUsersIdCounter: listUsersId,
7051
+ tagCloudIdCounter: tagCloudId
6805
7052
  });
6806
7053
  result.push(...childResult.elements);
6807
7054
  listPagesId = childResult.nextListPagesId;
6808
7055
  listUsersId = childResult.nextListUsersId;
7056
+ tagCloudId = childResult.nextTagCloudId;
6809
7057
  } else {
6810
7058
  const counts = countDynamicModules(ifTagsData.elements);
6811
7059
  listPagesId += counts.listPagesId;
6812
7060
  listUsersId += counts.listUsersId;
7061
+ tagCloudId += counts.tagCloudId;
6813
7062
  }
6814
7063
  } else {
6815
7064
  const childResult = walkAndResolve(ifTagsData.elements, {
6816
7065
  ...ctx,
6817
7066
  listPagesIdCounter: listPagesId,
6818
- listUsersIdCounter: listUsersId
7067
+ listUsersIdCounter: listUsersId,
7068
+ tagCloudIdCounter: tagCloudId
6819
7069
  });
6820
7070
  result.push({
6821
7071
  element: "if-tags",
@@ -6826,28 +7076,37 @@ function walkAndResolve(elements, ctx) {
6826
7076
  });
6827
7077
  listPagesId = childResult.nextListPagesId;
6828
7078
  listUsersId = childResult.nextListUsersId;
7079
+ tagCloudId = childResult.nextTagCloudId;
6829
7080
  }
6830
7081
  continue;
6831
7082
  }
6832
- const mapped = mapElementChildrenWithState(element, { listPagesId, listUsersId }, (children, state) => {
7083
+ const mapped = mapElementChildrenWithState(element, { listPagesId, listUsersId, tagCloudId }, (children, state) => {
6833
7084
  const childResult = walkAndResolve(children, {
6834
7085
  ...ctx,
6835
7086
  listPagesIdCounter: state.listPagesId,
6836
- listUsersIdCounter: state.listUsersId
7087
+ listUsersIdCounter: state.listUsersId,
7088
+ tagCloudIdCounter: state.tagCloudId
6837
7089
  });
6838
7090
  return {
6839
7091
  elements: childResult.elements,
6840
7092
  state: {
6841
7093
  listPagesId: childResult.nextListPagesId,
6842
- listUsersId: childResult.nextListUsersId
7094
+ listUsersId: childResult.nextListUsersId,
7095
+ tagCloudId: childResult.nextTagCloudId
6843
7096
  }
6844
7097
  };
6845
7098
  });
6846
7099
  result.push(mapped.element);
6847
7100
  listPagesId = mapped.state.listPagesId;
6848
7101
  listUsersId = mapped.state.listUsersId;
7102
+ tagCloudId = mapped.state.tagCloudId;
6849
7103
  }
6850
- return { elements: result, nextListPagesId: listPagesId, nextListUsersId: listUsersId };
7104
+ return {
7105
+ elements: result,
7106
+ nextListPagesId: listPagesId,
7107
+ nextListUsersId: listUsersId,
7108
+ nextTagCloudId: tagCloudId
7109
+ };
6851
7110
  }
6852
7111
 
6853
7112
  // packages/parser/src/parser/rules/block/module/resolution/styles.ts
@@ -6886,15 +7145,19 @@ async function resolveModules(ast, dataProvider, options) {
6886
7145
  const parse = createModuleParseFunction(options, dataProvider);
6887
7146
  const listPagesCtx = await buildListPagesContext(dataProvider, options.requirements.listPages ?? [], options.compiledListPagesTemplates, parse, options.urlPath);
6888
7147
  const listUsersCtx = await buildListUsersContext(dataProvider, options.requirements.listUsers ?? [], options.compiledListUsersTemplates, parse);
7148
+ const tagCloudCtx = await buildTagCloudContext(dataProvider, options.requirements.tagCloud ?? []);
6889
7149
  const pageTags = dataProvider.getPageTags?.() ?? null;
6890
7150
  const resolvedElements = walkAndResolve(ast.elements, {
6891
7151
  listPages: listPagesCtx,
6892
7152
  listUsers: listUsersCtx,
7153
+ tagCloud: tagCloudCtx,
6893
7154
  fetchListPagesProvided: dataProvider.fetchListPages !== undefined,
6894
7155
  fetchListUsersProvided: dataProvider.fetchListUsers !== undefined,
7156
+ fetchTagCloudProvided: dataProvider.fetchTagCloud !== undefined,
6895
7157
  pageTags,
6896
7158
  listPagesIdCounter: 0,
6897
- listUsersIdCounter: 0
7159
+ listUsersIdCounter: 0,
7160
+ tagCloudIdCounter: 0
6898
7161
  });
6899
7162
  const { elements: finalElements, styles } = collectStyles(resolvedElements.elements);
6900
7163
  const result = {
@@ -13038,6 +13301,7 @@ function parse(source, options) {
13038
13301
  export {
13039
13302
  tokenize,
13040
13303
  text,
13304
+ resolveTagCloud,
13041
13305
  resolveModules,
13042
13306
  resolveListUsers,
13043
13307
  resolveIncludesWithTrace,
@@ -13059,6 +13323,7 @@ export {
13059
13323
  link,
13060
13324
  lineBreak,
13061
13325
  italics,
13326
+ isTagCloudModule,
13062
13327
  isListUsersModule2 as isListUsersModule,
13063
13328
  horizontalRule,
13064
13329
  heading,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdprlib/parser",
3
- "version": "4.1.0",
3
+ "version": "4.2.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.1.0"
44
+ "@wdprlib/ast": "2.2.0"
45
45
  }
46
46
  }
package/src/index.ts CHANGED
@@ -133,6 +133,12 @@ export type {
133
133
  ListUsersDataFetcher,
134
134
  ListUsersVariableContext,
135
135
  ListUsersCompiledTemplate,
136
+ // TagCloud types
137
+ TagCloudDataRequirement,
138
+ TagCloudTagData,
139
+ TagCloudExternalData,
140
+ TagCloudDataFetcher,
141
+ TagCloudModuleData,
136
142
  // Normalized query types
137
143
  NormalizedListPagesQuery,
138
144
  NormalizedTags,
@@ -167,4 +173,7 @@ export {
167
173
  compileListUsersTemplate,
168
174
  isListUsersModule,
169
175
  resolveListUsers,
176
+ // TagCloud
177
+ isTagCloudModule,
178
+ resolveTagCloud,
170
179
  } from "./parser/rules/block/module/index";
@@ -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
+ }