@wdprlib/parser 4.0.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/README.md +9 -1
- package/dist/index.cjs +345 -64
- package/dist/index.d.cts +121 -2
- package/dist/index.d.ts +121 -2
- package/dist/index.js +317 -36
- package/package.json +2 -2
- package/src/index.ts +9 -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/listpages/url-resolution/fields.ts +4 -4
- package/src/parser/rules/block/module/listpages/url-resolution/params.ts +12 -1
- package/src/parser/rules/block/module/listpages/url-resolution/resolve.ts +10 -1
- package/src/parser/rules/block/module/listpages/url-resolution/value.ts +13 -4
- 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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { ListPagesQuery } from "../types";
|
|
2
2
|
|
|
3
3
|
export type UrlResolvableField =
|
|
4
|
-
| { attr: string; queryKey: StringUrlQueryKey; type: "string" }
|
|
5
|
-
| { attr: string; queryKey: NumberUrlQueryKey; type: "number" }
|
|
6
|
-
| { attr: string; queryKey: BooleanUrlQueryKey; type: "boolean" };
|
|
4
|
+
| { attr: string; queryKey: StringUrlQueryKey; type: "string"; urlAttrs?: readonly string[] }
|
|
5
|
+
| { attr: string; queryKey: NumberUrlQueryKey; type: "number"; urlAttrs?: readonly string[] }
|
|
6
|
+
| { attr: string; queryKey: BooleanUrlQueryKey; type: "boolean"; urlAttrs?: readonly string[] };
|
|
7
7
|
|
|
8
8
|
type StringUrlQueryKey = Extract<
|
|
9
9
|
keyof ListPagesQuery,
|
|
@@ -33,7 +33,7 @@ export const URL_RESOLVABLE_FIELDS: readonly UrlResolvableField[] = [
|
|
|
33
33
|
{ attr: "limit", queryKey: "limit", type: "number" },
|
|
34
34
|
{ attr: "per-page", queryKey: "perPage", type: "number" },
|
|
35
35
|
{ attr: "order", queryKey: "order", type: "string" },
|
|
36
|
-
{ attr: "tags", queryKey: "tags", type: "string" },
|
|
36
|
+
{ attr: "tags", queryKey: "tags", type: "string", urlAttrs: ["tag"] },
|
|
37
37
|
{ attr: "category", queryKey: "category", type: "string" },
|
|
38
38
|
{ attr: "parent", queryKey: "parent", type: "string" },
|
|
39
39
|
{ attr: "range", queryKey: "range", type: "string" },
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { URL_RESOLVABLE_FIELDS } from "./fields";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Parse URL path parameters like /offset/1/page2_limit/1.
|
|
3
5
|
* Returns a map of parameter name -> value.
|
|
@@ -5,9 +7,10 @@
|
|
|
5
7
|
export function parseUrlParams(url: string): Map<string, string> {
|
|
6
8
|
const params = new Map<string, string>();
|
|
7
9
|
const parts = url.split("/").filter(Boolean);
|
|
10
|
+
const pairStart = isUrlParameter(parts[0]) ? 0 : 1;
|
|
8
11
|
|
|
9
12
|
// Skip the page name (first part), parse key/value pairs.
|
|
10
|
-
for (let i =
|
|
13
|
+
for (let i = pairStart; i < parts.length - 1; i += 2) {
|
|
11
14
|
const key = parts[i];
|
|
12
15
|
const value = parts[i + 1];
|
|
13
16
|
if (key && value) {
|
|
@@ -17,3 +20,11 @@ export function parseUrlParams(url: string): Map<string, string> {
|
|
|
17
20
|
|
|
18
21
|
return params;
|
|
19
22
|
}
|
|
23
|
+
|
|
24
|
+
const URL_PARAMETER_NAMES = new Set(
|
|
25
|
+
URL_RESOLVABLE_FIELDS.flatMap((field) => [field.attr, ...(field.urlAttrs ?? [])]),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
function isUrlParameter(param: string | undefined): boolean {
|
|
29
|
+
return param !== undefined && URL_PARAMETER_NAMES.has(param);
|
|
30
|
+
}
|
|
@@ -25,7 +25,12 @@ export function resolveQuery(
|
|
|
25
25
|
const rawValue = rawAttributes[field.attr];
|
|
26
26
|
if (!rawValue) continue;
|
|
27
27
|
|
|
28
|
-
const resolvedValue = resolveUrlValue(
|
|
28
|
+
const resolvedValue = resolveUrlValue(
|
|
29
|
+
rawValue,
|
|
30
|
+
getUrlParamNames(field),
|
|
31
|
+
urlParams,
|
|
32
|
+
urlAttrPrefix,
|
|
33
|
+
);
|
|
29
34
|
if (resolvedValue === undefined) continue;
|
|
30
35
|
|
|
31
36
|
assignResolvedUrlField(resolved, field, resolvedValue);
|
|
@@ -34,6 +39,10 @@ export function resolveQuery(
|
|
|
34
39
|
return resolved;
|
|
35
40
|
}
|
|
36
41
|
|
|
42
|
+
function getUrlParamNames(field: (typeof URL_RESOLVABLE_FIELDS)[number]): readonly string[] {
|
|
43
|
+
return field.urlAttrs ? [field.attr, ...field.urlAttrs] : [field.attr];
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
/**
|
|
38
47
|
* Resolve all `@URL` parameters and normalize the query.
|
|
39
48
|
*
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
* Resolve @URL|default format with actual URL parameters.
|
|
3
3
|
*
|
|
4
4
|
* @param rawValue - The raw attribute value (e.g., "@URL|0")
|
|
5
|
-
* @param
|
|
5
|
+
* @param paramNames - The parameter name or names (e.g., "offset")
|
|
6
6
|
* @param urlParams - Parsed URL parameters
|
|
7
7
|
* @param prefix - Optional URL attribute prefix (e.g., "page2")
|
|
8
8
|
* @returns Resolved value
|
|
9
9
|
*/
|
|
10
10
|
export function resolveUrlValue(
|
|
11
11
|
rawValue: string | undefined,
|
|
12
|
-
|
|
12
|
+
paramNames: string | readonly string[],
|
|
13
13
|
urlParams: Map<string, string>,
|
|
14
14
|
prefix?: string,
|
|
15
15
|
): string | undefined {
|
|
@@ -20,6 +20,15 @@ export function resolveUrlValue(
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
const defaultValue = rawValue.includes("|") ? rawValue.split("|")[1] : undefined;
|
|
23
|
-
const
|
|
24
|
-
|
|
23
|
+
for (const paramName of toParamNames(paramNames)) {
|
|
24
|
+
const actualParamName = prefix ? `${prefix}_${paramName}` : paramName;
|
|
25
|
+
const value = urlParams.get(actualParamName);
|
|
26
|
+
if (value !== undefined) return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return defaultValue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toParamNames(paramNames: string | readonly string[]): readonly string[] {
|
|
33
|
+
return typeof paramNames === "string" ? [paramNames] : paramNames;
|
|
25
34
|
}
|
|
@@ -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
|
}
|
|
@@ -21,8 +21,13 @@ import type { DataProvider } from "./types-common";
|
|
|
21
21
|
import { resolveIncludes } from "./include";
|
|
22
22
|
import type { ListPagesDataRequirement, CompiledTemplate } from "./listpages/types";
|
|
23
23
|
import type { ListUsersDataRequirement, ListUsersCompiledTemplate } from "./listusers/types";
|
|
24
|
+
import type { TagCloudDataRequirement } from "./tagcloud/types";
|
|
24
25
|
import type { ParseFunction } from "./listpages/resolve";
|
|
25
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
buildListPagesContext,
|
|
28
|
+
buildListUsersContext,
|
|
29
|
+
buildTagCloudContext,
|
|
30
|
+
} from "./resolution/contexts";
|
|
26
31
|
import { walkAndResolve } from "./resolution/walk-resolve";
|
|
27
32
|
import { collectStyles } from "./resolution/styles";
|
|
28
33
|
|
|
@@ -71,6 +76,7 @@ export interface ResolveOptions {
|
|
|
71
76
|
requirements: {
|
|
72
77
|
listPages?: ListPagesDataRequirement[];
|
|
73
78
|
listUsers?: ListUsersDataRequirement[];
|
|
79
|
+
tagCloud?: TagCloudDataRequirement[];
|
|
74
80
|
};
|
|
75
81
|
|
|
76
82
|
/**
|
|
@@ -139,17 +145,21 @@ export async function resolveModules(
|
|
|
139
145
|
options.compiledListUsersTemplates,
|
|
140
146
|
parse,
|
|
141
147
|
);
|
|
148
|
+
const tagCloudCtx = await buildTagCloudContext(dataProvider, options.requirements.tagCloud ?? []);
|
|
142
149
|
const pageTags = dataProvider.getPageTags?.() ?? null;
|
|
143
150
|
|
|
144
151
|
// Resolve AST
|
|
145
152
|
const resolvedElements = walkAndResolve(ast.elements, {
|
|
146
153
|
listPages: listPagesCtx,
|
|
147
154
|
listUsers: listUsersCtx,
|
|
155
|
+
tagCloud: tagCloudCtx,
|
|
148
156
|
fetchListPagesProvided: dataProvider.fetchListPages !== undefined,
|
|
149
157
|
fetchListUsersProvided: dataProvider.fetchListUsers !== undefined,
|
|
158
|
+
fetchTagCloudProvided: dataProvider.fetchTagCloud !== undefined,
|
|
150
159
|
pageTags,
|
|
151
160
|
listPagesIdCounter: 0,
|
|
152
161
|
listUsersIdCounter: 0,
|
|
162
|
+
tagCloudIdCounter: 0,
|
|
153
163
|
});
|
|
154
164
|
|
|
155
165
|
// Collect style elements from resolved AST
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Barrel exports for the TagCloud module.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { tagCloudModuleRule } from "./parser";
|
|
9
|
+
export { isTagCloudModule, resolveTagCloud, type TagCloudModuleData } from "./resolve";
|
|
10
|
+
export type {
|
|
11
|
+
TagCloudDataFetcher,
|
|
12
|
+
TagCloudDataRequirement,
|
|
13
|
+
TagCloudExternalData,
|
|
14
|
+
TagCloudTagData,
|
|
15
|
+
} from "./types";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Parser rule for the Wikidot `[[module TagCloud]]` block.
|
|
4
|
+
*
|
|
5
|
+
* Parses font size, color, target, limit, and category attributes into a
|
|
6
|
+
* `tag-cloud` Module AST node, mirroring the validation behavior of Wikidot's
|
|
7
|
+
* `PagesTagCloudModule`. Invalid font size or color formats produce an error
|
|
8
|
+
* block element, matching Wikidot's ProcessException messages.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { CSS_LENGTH_UNITS, type CssLengthUnit, type Element, type Module } from "@wdprlib/ast";
|
|
14
|
+
import type { ModuleRule } from "../types";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Matches a font size value like `300%`, `2em`, or `16px`.
|
|
18
|
+
*
|
|
19
|
+
* Wikidot only accepts `px`, `em`, and `%` here; wdpr deliberately extends
|
|
20
|
+
* this to all CSS length units (see `CSS_LENGTH_UNITS`), case-insensitively.
|
|
21
|
+
*/
|
|
22
|
+
const FONT_SIZE_PATTERN = new RegExp(`^([0-9]+)(${CSS_LENGTH_UNITS.join("|")})$`, "i");
|
|
23
|
+
|
|
24
|
+
/** Matches a color value like `64,64,128` (range is not enforced, per Wikidot). */
|
|
25
|
+
const COLOR_PATTERN = /^[0-9]+,[0-9]+,[0-9]+$/;
|
|
26
|
+
|
|
27
|
+
/** Matches a strictly positive integer, mirroring PHP's `is_numeric` + `> 0` gate. */
|
|
28
|
+
const POSITIVE_INT_PATTERN = /^[0-9]+$/;
|
|
29
|
+
|
|
30
|
+
const ERROR_FONT_FORMAT =
|
|
31
|
+
"Unsupported format for font size. Use a number followed by a CSS length unit such as px, em or %.";
|
|
32
|
+
const ERROR_FONT_MISMATCH = "Format for minFontSize and maxFontSize must use the same unit.";
|
|
33
|
+
const ERROR_COLOR_FORMAT =
|
|
34
|
+
'Unsupported color format. Use "RRR,GGG,BBB" for Red,Green,Blue each within 0-255 range.';
|
|
35
|
+
|
|
36
|
+
/** Build the Wikidot-compatible error block for invalid attribute values. */
|
|
37
|
+
function errorBlock(message: string): Element {
|
|
38
|
+
return {
|
|
39
|
+
element: "container",
|
|
40
|
+
data: {
|
|
41
|
+
type: "div",
|
|
42
|
+
attributes: { class: "error-block" },
|
|
43
|
+
elements: [{ element: "text", data: message }],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Parse a limit value, falling back to 50 unless it is a safe positive integer. */
|
|
49
|
+
function parseLimit(value: string | undefined): number {
|
|
50
|
+
if (!value || !POSITIVE_INT_PATTERN.test(value)) return 50;
|
|
51
|
+
const num = Number.parseInt(value, 10);
|
|
52
|
+
return Number.isSafeInteger(num) && num > 0 ? num : 50;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Normalize a target path to `/<path>/tag/`, or the Wikidot default. */
|
|
56
|
+
function normalizeTarget(value: string | undefined): string {
|
|
57
|
+
if (!value) return "/system:page-tags/tag/";
|
|
58
|
+
let target = value;
|
|
59
|
+
if (!target.startsWith("/")) target = `/${target}`;
|
|
60
|
+
if (!target.endsWith("/")) target = `${target}/`;
|
|
61
|
+
return `${target}tag/`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Parse a `R,G,B` color string into a tuple (input must match COLOR_PATTERN). */
|
|
65
|
+
function parseColor(value: string): [number, number, number] {
|
|
66
|
+
const [r = 0, g = 0, b = 0] = value.split(",").map((part) => Number.parseInt(part, 10));
|
|
67
|
+
return [r, g, b];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Module rule for `[[module TagCloud]]`.
|
|
72
|
+
*
|
|
73
|
+
* Displays a weighted cloud of page tags. Font sizes and colors are only
|
|
74
|
+
* honored when both min and max are given (Wikidot behavior); otherwise the
|
|
75
|
+
* defaults (100%-300%, rgb(128,128,192)-rgb(64,64,128)) apply. Tag data is
|
|
76
|
+
* supplied during the resolution phase via `DataProvider.fetchTagCloud`.
|
|
77
|
+
*/
|
|
78
|
+
export const tagCloudModuleRule: ModuleRule = {
|
|
79
|
+
name: "module-tagcloud",
|
|
80
|
+
acceptsNames: ["tagcloud"],
|
|
81
|
+
hasBody: false,
|
|
82
|
+
|
|
83
|
+
parse(_ctx, _pos, args): Module | Element {
|
|
84
|
+
const maxFontSize = args.maxfontsize;
|
|
85
|
+
const minFontSize = args.minfontsize;
|
|
86
|
+
const maxColor = args.maxcolor;
|
|
87
|
+
const minColor = args.mincolor;
|
|
88
|
+
|
|
89
|
+
let sizeSmall = 100;
|
|
90
|
+
let sizeBig = 300;
|
|
91
|
+
let fontSizeUnit: CssLengthUnit = "%";
|
|
92
|
+
if (maxFontSize && minFontSize) {
|
|
93
|
+
const maxMatch = FONT_SIZE_PATTERN.exec(maxFontSize);
|
|
94
|
+
if (!maxMatch) return errorBlock(ERROR_FONT_FORMAT);
|
|
95
|
+
const maxUnit = (maxMatch[2] as string).toLowerCase() as CssLengthUnit;
|
|
96
|
+
const minMatch = FONT_SIZE_PATTERN.exec(minFontSize);
|
|
97
|
+
if (!minMatch || (minMatch[2] as string).toLowerCase() !== maxUnit) {
|
|
98
|
+
return errorBlock(ERROR_FONT_MISMATCH);
|
|
99
|
+
}
|
|
100
|
+
sizeBig = Number.parseInt(maxMatch[1] as string, 10);
|
|
101
|
+
sizeSmall = Number.parseInt(minMatch[1] as string, 10);
|
|
102
|
+
fontSizeUnit = maxUnit;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let colorSmall: [number, number, number] = [128, 128, 192];
|
|
106
|
+
let colorBig: [number, number, number] = [64, 64, 128];
|
|
107
|
+
if (maxColor && minColor) {
|
|
108
|
+
if (!COLOR_PATTERN.test(maxColor) || !COLOR_PATTERN.test(minColor)) {
|
|
109
|
+
return errorBlock(ERROR_COLOR_FORMAT);
|
|
110
|
+
}
|
|
111
|
+
colorSmall = parseColor(minColor);
|
|
112
|
+
colorBig = parseColor(maxColor);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
module: "tag-cloud",
|
|
117
|
+
"min-font-size": sizeSmall,
|
|
118
|
+
"max-font-size": sizeBig,
|
|
119
|
+
"font-size-unit": fontSizeUnit,
|
|
120
|
+
"min-color": colorSmall,
|
|
121
|
+
"max-color": colorBig,
|
|
122
|
+
target: normalizeTarget(args.target),
|
|
123
|
+
limit: parseLimit(args.limit),
|
|
124
|
+
category: args.category || null,
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
};
|