@wdprlib/parser 5.3.0 → 6.0.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.cjs +2429 -1199
- package/dist/index.d.cts +95 -8
- package/dist/index.d.ts +95 -8
- package/dist/index.js +2385 -1152
- package/package.json +3 -2
- package/src/build-info.generated.ts +2 -2
- package/src/index.ts +3 -0
- package/src/lexer/lexer.ts +34 -5
- package/src/lexer/quoted-string.ts +4 -4
- package/src/lexer/syntax-actions.ts +6 -1
- package/src/lexer/text-actions.ts +15 -1
- package/src/lexer/url-schemes.ts +78 -0
- package/src/parser/constants.ts +4 -0
- package/src/parser/parse/block.ts +1 -1
- package/src/parser/postprocess/divAdjacentParagraph.ts +23 -21
- package/src/parser/postprocess/spanStrip/merge.ts +8 -2
- package/src/parser/preprocess/typography.ts +25 -3
- package/src/parser/preprocess/utils/raw-regions.ts +59 -14
- package/src/parser/preprocess/whitespace/index.ts +8 -1
- package/src/parser/rules/block/bibliography/entry-content.ts +1 -1
- package/src/parser/rules/block/block-list/bare-content.ts +3 -1
- package/src/parser/rules/block/block-list/bare-paragraph.ts +7 -2
- package/src/parser/rules/block/block-list/item-content.ts +7 -3
- package/src/parser/rules/block/block-list/li-content.ts +8 -3
- package/src/parser/rules/block/block-list/li-item.ts +1 -1
- package/src/parser/rules/block/blockquote/build.ts +1 -1
- package/src/parser/rules/block/code/boundary.ts +76 -0
- package/src/parser/rules/block/code/content.ts +11 -40
- package/src/parser/rules/block/code/index.ts +8 -31
- package/src/parser/rules/block/code/open.ts +46 -0
- package/src/parser/rules/block/definition-list/item-key.ts +1 -1
- package/src/parser/rules/block/definition-list/item-value.ts +1 -1
- package/src/parser/rules/block/div/failed.ts +2 -0
- package/src/parser/rules/block/index.ts +3 -0
- package/src/parser/rules/block/list/line.ts +6 -3
- package/src/parser/rules/block/math/boundary.ts +104 -0
- package/src/parser/rules/block/math/index.ts +17 -57
- package/src/parser/rules/block/module/index.ts +3 -2
- package/src/parser/rules/block/module/listpages/extraction/listpages.ts +5 -0
- package/src/parser/rules/block/module/listpages/extraction/query.ts +1 -0
- package/src/parser/rules/block/module/listpages/extraction/template.ts +16 -0
- package/src/parser/rules/block/module/listpages/extraction/variables.ts +1 -0
- package/src/parser/rules/block/module/listpages/index.ts +1 -0
- package/src/parser/rules/block/module/listpages/normalization/order-parent.ts +9 -1
- package/src/parser/rules/block/module/listpages/normalize.ts +1 -0
- package/src/parser/rules/block/module/listpages/parser.ts +1 -0
- package/src/parser/rules/block/module/listpages/resolve.ts +4 -2
- package/src/parser/rules/block/module/listpages/template/excerpt.ts +63 -0
- package/src/parser/rules/block/module/listpages/template/format/content.ts +0 -32
- package/src/parser/rules/block/module/listpages/template/format/index.ts +1 -1
- package/src/parser/rules/block/module/listpages/template/getters/index.ts +14 -0
- package/src/parser/rules/block/module/listpages/template/getters/parameterized.ts +3 -1
- package/src/parser/rules/block/module/listpages/template/getters/registered.ts +52 -0
- package/src/parser/rules/block/module/listpages/template/getters/simple.ts +15 -5
- package/src/parser/rules/block/module/listpages/template/literal.ts +6 -0
- package/src/parser/rules/block/module/listpages/template/syntax.ts +10 -1
- package/src/parser/rules/block/module/listpages/types/data-requirements.ts +6 -0
- package/src/parser/rules/block/module/listpages/types/external-data.ts +36 -2
- package/src/parser/rules/block/module/listpages/types/normalized-query.ts +14 -4
- package/src/parser/rules/block/module/listpages/types/query.ts +2 -0
- package/src/parser/rules/block/module/listpages/types/variables.ts +5 -0
- package/src/parser/rules/block/module/listpages/url-resolution/fields.ts +2 -0
- package/src/parser/rules/block/module/listusers/resolve.ts +2 -1
- package/src/parser/rules/block/module/mapping.ts +2 -1
- package/src/parser/rules/block/module/rate/index.ts +16 -19
- package/src/parser/rules/block/module/rate/resolve.ts +108 -0
- package/src/parser/rules/block/module/rate/types.ts +6 -16
- package/src/parser/rules/block/module/resolution/resolve-async.ts +2 -1
- package/src/parser/rules/block/module/resolve.ts +10 -8
- package/src/parser/rules/block/module/types-common.ts +4 -0
- package/src/parser/rules/block/note/boundary.ts +88 -0
- package/src/parser/rules/block/note/index.ts +37 -0
- package/src/parser/rules/block/paragraph/index.ts +12 -4
- package/src/parser/rules/block/paragraph/normalize.ts +6 -1
- package/src/parser/rules/block/parsing/block-item.ts +1 -1
- package/src/parser/rules/block/parsing/content.ts +1 -1
- package/src/parser/rules/block/parsing/inline-content.ts +6 -3
- package/src/parser/rules/block/table/pipe/cell.ts +3 -1
- package/src/parser/rules/block/table-block/cell-content/index.ts +1 -1
- package/src/parser/rules/block/table-block/cell-content/segments.ts +5 -2
- package/src/parser/rules/block/table-block/cell-newline.ts +2 -1
- package/src/parser/rules/contracts/rule.ts +4 -2
- package/src/parser/rules/contracts/scope.ts +2 -0
- package/src/parser/rules/inline/anchor/child.ts +8 -2
- package/src/parser/rules/inline/anchor/content.ts +3 -1
- package/src/parser/rules/inline/anchor/index.ts +4 -1
- package/src/parser/rules/inline/anchor/newline.ts +2 -1
- package/src/parser/rules/inline/autolink.ts +153 -0
- package/src/parser/rules/inline/button/attributes.ts +17 -0
- package/src/parser/rules/inline/button/index.ts +17 -0
- package/src/parser/rules/inline/button/syntax.ts +56 -0
- package/src/parser/rules/inline/date/index.ts +17 -0
- package/src/parser/rules/inline/date/syntax.ts +46 -0
- package/src/parser/rules/inline/email/candidates.ts +134 -0
- package/src/parser/rules/inline/email/index.ts +36 -0
- package/src/parser/rules/inline/email/scan.ts +76 -0
- package/src/parser/rules/inline/expr/branch.ts +3 -1
- package/src/parser/rules/inline/footnote/boundary.ts +56 -0
- package/src/parser/rules/inline/footnote/content.ts +29 -41
- package/src/parser/rules/inline/footnote/elements.ts +9 -34
- package/src/parser/rules/inline/footnote/index.ts +4 -1
- package/src/parser/rules/inline/formatting/close.ts +12 -0
- package/src/parser/rules/inline/index.ts +14 -0
- package/src/parser/rules/inline/line-break/newline.ts +8 -1
- package/src/parser/rules/inline/link-bracket/direct-url.ts +11 -3
- package/src/parser/rules/inline/link-bracket/parsed.ts +9 -4
- package/src/parser/rules/inline/link-bracket/parts.ts +14 -36
- package/src/parser/rules/inline/link-bracket/special-target.ts +9 -0
- package/src/parser/rules/inline/link-single.ts +9 -7
- package/src/parser/rules/inline/link-triple/index.ts +1 -0
- package/src/parser/rules/inline/link-triple/label.ts +7 -1
- package/src/parser/rules/inline/parsing/automatic-line-break.ts +35 -0
- package/src/parser/rules/inline/parsing/block-boundary.ts +2 -0
- package/src/parser/rules/inline/parsing/block-start-predicates.ts +10 -0
- package/src/parser/rules/inline/parsing/inline-content.ts +46 -7
- package/src/parser/rules/inline/parsing/plain-text.ts +7 -2
- package/src/parser/rules/inline/parsing/preserved-line-break.ts +13 -0
- package/src/parser/rules/inline/parsing/raw-tag.ts +19 -0
- package/src/parser/rules/inline/raw/angle.ts +2 -1
- package/src/parser/rules/inline/raw/end.ts +21 -1
- package/src/parser/rules/inline/size/content.ts +32 -5
- package/src/parser/rules/inline/size/value.ts +11 -0
- package/src/parser/rules/inline/social/index.ts +17 -0
- package/src/parser/rules/inline/social/syntax.ts +40 -0
- package/src/parser/rules/inline/span/content.ts +3 -1
- package/src/parser/rules/inline/span/newline.ts +2 -1
- package/src/parser/rules/opaque-probe.ts +58 -0
- package/src/pipeline/process.ts +13 -0
- package/src/pipeline/types.ts +19 -0
- package/src/parser/rules/block/math/content.ts +0 -54
- package/src/parser/rules/block/math/name.ts +0 -35
- package/src/parser/rules/inline/footnote/child.ts +0 -22
- package/src/parser/rules/inline/footnote/newline.ts +0 -27
|
@@ -32,6 +32,11 @@ export function extractListPagesModule(
|
|
|
32
32
|
contentSectionIndices: extraction.contentIndices,
|
|
33
33
|
previewLengths: extraction.previewLengths,
|
|
34
34
|
formFields: extraction.formFields,
|
|
35
|
+
metadataKeys: extraction.metadataKeys,
|
|
36
|
+
customRateKeys: extraction.customRateKeys,
|
|
37
|
+
needsReadableText: extraction.variables.some((name) =>
|
|
38
|
+
["preview", "preview_n", "summary", "first_paragraph", "excerpt"].includes(name),
|
|
39
|
+
),
|
|
35
40
|
tagsLinkPrefix: extraction.tagsLinkPrefix,
|
|
36
41
|
hiddenTagsLinkPrefix: extraction.hiddenTagsLinkPrefix,
|
|
37
42
|
urlAttrPrefix: listPages["url-attr-prefix"],
|
|
@@ -21,6 +21,7 @@ export function buildQuery(module: ListPagesModuleData): ListPagesQuery {
|
|
|
21
21
|
updatedAt: module["updated-at"],
|
|
22
22
|
createdBy: module["created-by"],
|
|
23
23
|
rating: module.rating,
|
|
24
|
+
ratingAxis: module["rating-axis"],
|
|
24
25
|
votes: module.votes,
|
|
25
26
|
name: module.name,
|
|
26
27
|
fullname: module.fullname,
|
|
@@ -19,6 +19,8 @@ export interface TemplateExtraction {
|
|
|
19
19
|
contentIndices: number[];
|
|
20
20
|
previewLengths: number[];
|
|
21
21
|
formFields: string[];
|
|
22
|
+
metadataKeys: string[];
|
|
23
|
+
customRateKeys: string[];
|
|
22
24
|
tagsLinkPrefix?: string;
|
|
23
25
|
hiddenTagsLinkPrefix?: string;
|
|
24
26
|
}
|
|
@@ -31,6 +33,8 @@ export function extractVariablesFromTemplate(template: string): TemplateExtracti
|
|
|
31
33
|
const contentIndices = new Set<number>();
|
|
32
34
|
const previewLengths = new Set<number>();
|
|
33
35
|
const formFields = new Set<string>();
|
|
36
|
+
const metadataKeys = new Set<string>();
|
|
37
|
+
const customRateKeys = new Set<string>();
|
|
34
38
|
let tagsLinkPrefix: string | undefined;
|
|
35
39
|
let hiddenTagsLinkPrefix: string | undefined;
|
|
36
40
|
|
|
@@ -39,6 +43,16 @@ export function extractVariablesFromTemplate(template: string): TemplateExtracti
|
|
|
39
43
|
|
|
40
44
|
if (match.braceParam !== undefined) {
|
|
41
45
|
switch (varName) {
|
|
46
|
+
case "metadata":
|
|
47
|
+
metadataKeys.add(match.braceParam);
|
|
48
|
+
variables.add("metadata");
|
|
49
|
+
continue;
|
|
50
|
+
case "customrate":
|
|
51
|
+
case "customrate_votes":
|
|
52
|
+
case "customrate_percent":
|
|
53
|
+
customRateKeys.add(match.braceParam);
|
|
54
|
+
variables.add(varName);
|
|
55
|
+
continue;
|
|
42
56
|
case "content":
|
|
43
57
|
contentIndices.add(Number(match.braceParam));
|
|
44
58
|
variables.add("content_n");
|
|
@@ -90,6 +104,8 @@ export function extractVariablesFromTemplate(template: string): TemplateExtracti
|
|
|
90
104
|
contentIndices: Array.from(contentIndices).sort((a, b) => a - b),
|
|
91
105
|
previewLengths: Array.from(previewLengths).sort((a, b) => a - b),
|
|
92
106
|
formFields: Array.from(formFields).sort(),
|
|
107
|
+
metadataKeys: [...metadataKeys].sort(),
|
|
108
|
+
customRateKeys: [...customRateKeys].sort(),
|
|
93
109
|
tagsLinkPrefix,
|
|
94
110
|
hiddenTagsLinkPrefix,
|
|
95
111
|
};
|
|
@@ -3,7 +3,7 @@ import type { NormalizedOrder, NormalizedParent, OrderDirection, OrderField } fr
|
|
|
3
3
|
/**
|
|
4
4
|
* Mapping from Wikidot's order field names to normalized OrderField values.
|
|
5
5
|
*/
|
|
6
|
-
const ORDER_FIELD_MAP: Record<string, OrderField
|
|
6
|
+
const ORDER_FIELD_MAP: Record<string, Exclude<OrderField, "metadata">> = {
|
|
7
7
|
datecreated: "created_at",
|
|
8
8
|
dateedited: "updated_at",
|
|
9
9
|
title: "title",
|
|
@@ -12,6 +12,7 @@ const ORDER_FIELD_MAP: Record<string, OrderField> = {
|
|
|
12
12
|
votes: "votes",
|
|
13
13
|
revisions: "revisions",
|
|
14
14
|
comments: "comments",
|
|
15
|
+
commented_at: "commented_at",
|
|
15
16
|
pagelength: "size",
|
|
16
17
|
size: "size",
|
|
17
18
|
random: "random",
|
|
@@ -23,6 +24,13 @@ const ORDER_FIELD_MAP: Record<string, OrderField> = {
|
|
|
23
24
|
* Parse order string into structured format.
|
|
24
25
|
*/
|
|
25
26
|
export function parseOrder(value: string): NormalizedOrder {
|
|
27
|
+
const metadata = /^metadata\{([^}]*)\}(?:\s+(asc|desc))?$/i.exec(value.trim());
|
|
28
|
+
if (metadata)
|
|
29
|
+
return {
|
|
30
|
+
field: "metadata",
|
|
31
|
+
key: metadata[1]!,
|
|
32
|
+
direction: metadata[2]?.toLowerCase() === "asc" ? "asc" : "desc",
|
|
33
|
+
};
|
|
26
34
|
const defaultOrder: NormalizedOrder = { field: "created_at", direction: "desc" };
|
|
27
35
|
const trimmed = value.trim().toLowerCase();
|
|
28
36
|
if (!trimmed) return defaultOrder;
|
|
@@ -36,6 +36,7 @@ export function normalizeQuery(query: ListPagesQuery): NormalizedListPagesQuery
|
|
|
36
36
|
if (query.pagetype) result.pagetype = query.pagetype;
|
|
37
37
|
if (query.linkTo) result.linkTo = query.linkTo;
|
|
38
38
|
if (query.createdBy) result.createdBy = query.createdBy;
|
|
39
|
+
if (query.ratingAxis !== undefined) result.ratingAxis = query.ratingAxis;
|
|
39
40
|
if (query.name) result.name = query.name;
|
|
40
41
|
if (query.fullname) result.fullname = query.fullname;
|
|
41
42
|
if (query.range) result.range = query.range;
|
|
@@ -23,6 +23,7 @@ import type { ListPagesExternalData, CompiledTemplate } from "./types";
|
|
|
23
23
|
import type { ParseFunction } from "../types";
|
|
24
24
|
import { renderListPagesItems } from "./resolution/items";
|
|
25
25
|
import { wrapListPagesResult } from "./resolution/wrapper";
|
|
26
|
+
import { suppressModuleRatings } from "../rate/resolve";
|
|
26
27
|
export type { ParseFunction };
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -55,6 +56,7 @@ export function resolveListPages(
|
|
|
55
56
|
compiledTemplate: CompiledTemplate,
|
|
56
57
|
parse: ParseFunction,
|
|
57
58
|
): Element[] {
|
|
58
|
-
const
|
|
59
|
-
|
|
59
|
+
const parseFragment: ParseFunction = (source) => suppressModuleRatings(parse(source));
|
|
60
|
+
const items = renderListPagesItems(module, data, compiledTemplate, parseFragment);
|
|
61
|
+
return wrapListPagesResult(module, items, parseFragment, data);
|
|
60
62
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { TextExcerptOptions } from "@wdprlib/ast";
|
|
2
|
+
|
|
3
|
+
/** Find }%% outside quoted excerpt arguments, allowing regex quantifiers and literal %%. */
|
|
4
|
+
export function findExcerptEnd(template: string, contentStart: number): number {
|
|
5
|
+
let quote: string | undefined;
|
|
6
|
+
for (let cursor = contentStart + "excerpt{".length; cursor < template.length; cursor++) {
|
|
7
|
+
const char = template[cursor];
|
|
8
|
+
if (quote) {
|
|
9
|
+
if (char === "\\") cursor++;
|
|
10
|
+
else if (char === quote) quote = undefined;
|
|
11
|
+
} else if (char === '"' || char === "'") {
|
|
12
|
+
quote = char;
|
|
13
|
+
} else if (char === "}") {
|
|
14
|
+
return template.startsWith("%%", cursor + 1) ? cursor + 1 : -1;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return -1;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Excerpt uses quoted named arguments; regex backslashes stay literal. */
|
|
21
|
+
export function parseExcerptOptions(input: string): TextExcerptOptions | null {
|
|
22
|
+
if (input.length > 8_192) return null;
|
|
23
|
+
const attributes = new Map<string, string>();
|
|
24
|
+
let cursor = 0;
|
|
25
|
+
while (cursor < input.length) {
|
|
26
|
+
while (cursor < input.length && /\s/.test(input[cursor]!)) cursor++;
|
|
27
|
+
if (cursor === input.length) break;
|
|
28
|
+
const start = cursor;
|
|
29
|
+
while (cursor < input.length && /[a-z]/.test(input[cursor]!)) cursor++;
|
|
30
|
+
const name = input.slice(start, cursor);
|
|
31
|
+
if (!["pattern", "flags", "group", "match", "max"].includes(name) || attributes.has(name))
|
|
32
|
+
return null;
|
|
33
|
+
while (cursor < input.length && /\s/.test(input[cursor]!)) cursor++;
|
|
34
|
+
if (input[cursor++] !== "=") return null;
|
|
35
|
+
while (cursor < input.length && /\s/.test(input[cursor]!)) cursor++;
|
|
36
|
+
const quote = input[cursor++];
|
|
37
|
+
if (quote !== '"' && quote !== "'") return null;
|
|
38
|
+
let value = "";
|
|
39
|
+
while (cursor < input.length && input[cursor] !== quote) {
|
|
40
|
+
if (input[cursor] === "\\" && (input[cursor + 1] === quote || input[cursor + 1] === "\\")) {
|
|
41
|
+
value += input[cursor + 1] === quote ? quote : "\\\\";
|
|
42
|
+
cursor += 2;
|
|
43
|
+
} else value += input[cursor++];
|
|
44
|
+
}
|
|
45
|
+
if (input[cursor++] !== quote) return null;
|
|
46
|
+
if (cursor < input.length && !/\s/.test(input[cursor]!)) return null;
|
|
47
|
+
attributes.set(name, value);
|
|
48
|
+
}
|
|
49
|
+
const pattern = attributes.get("pattern");
|
|
50
|
+
if (pattern === undefined) return null;
|
|
51
|
+
const group = attributes.get("group");
|
|
52
|
+
const match = attributes.get("match");
|
|
53
|
+
const max = attributes.get("max");
|
|
54
|
+
if ((match !== undefined && !/^\d+$/.test(match)) || (max !== undefined && !/^\d+$/.test(max)))
|
|
55
|
+
return null;
|
|
56
|
+
return {
|
|
57
|
+
pattern,
|
|
58
|
+
flags: attributes.get("flags"),
|
|
59
|
+
group: group === undefined ? undefined : /^\d+$/.test(group) ? Number(group) : group,
|
|
60
|
+
match: match === undefined ? undefined : Number(match),
|
|
61
|
+
maxLength: max === undefined ? undefined : Number(max),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { PageData } from "../../types";
|
|
2
|
-
|
|
3
1
|
/**
|
|
4
2
|
* Split content by ==== separators.
|
|
5
3
|
*
|
|
@@ -9,33 +7,3 @@ import type { PageData } from "../../types";
|
|
|
9
7
|
export function splitContentSections(content: string): string[] {
|
|
10
8
|
return content.split(/^={4,}$/m).map((section) => section.trim());
|
|
11
9
|
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Extract a summary from page content.
|
|
15
|
-
*/
|
|
16
|
-
export function getSummary(page: PageData): string {
|
|
17
|
-
if (page.content) {
|
|
18
|
-
const sections = splitContentSections(page.content);
|
|
19
|
-
if (sections.length > 1) {
|
|
20
|
-
return sections[0]?.trim() ?? "";
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return getFirstParagraph(page.content);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Extract the first paragraph from wikitext content.
|
|
28
|
-
*/
|
|
29
|
-
export function getFirstParagraph(content?: string): string {
|
|
30
|
-
if (!content) return "";
|
|
31
|
-
|
|
32
|
-
const stripped = content
|
|
33
|
-
.replace(/^(\+{1,6}) (.*)/gm, "")
|
|
34
|
-
.replace(/^\[\[toc(\s[^\]]+)?\]\]/gim, "")
|
|
35
|
-
.replace(/^\[\[\/?div(\s[^\]]+)?\]\]/gim, "")
|
|
36
|
-
.replace(/^\[\[\/?module(\s[^\]]+)?\]\]/gim, "")
|
|
37
|
-
.trim();
|
|
38
|
-
|
|
39
|
-
const paragraphs = stripped.split(/\n{2,}/);
|
|
40
|
-
return paragraphs[0]?.trim() ?? "";
|
|
41
|
-
}
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
} from "./parameterized";
|
|
7
7
|
import { SIMPLE_GETTERS } from "./simple";
|
|
8
8
|
import type { VariableGetter } from "./types";
|
|
9
|
+
import { createRegisteredGetter } from "./registered";
|
|
10
|
+
import { compileTextExcerpt } from "@wdprlib/ast";
|
|
11
|
+
import { literalWikitext } from "../literal";
|
|
12
|
+
import { parseExcerptOptions } from "../excerpt";
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* Create a getter function for a specific template variable.
|
|
@@ -20,8 +24,18 @@ export function createVariableGetter(
|
|
|
20
24
|
parenParam?: string,
|
|
21
25
|
format?: string,
|
|
22
26
|
): VariableGetter {
|
|
27
|
+
if (name === "excerpt") {
|
|
28
|
+
const options = braceParam === undefined ? null : parseExcerptOptions(braceParam);
|
|
29
|
+
if (!options || parenParam !== undefined || format !== undefined) return () => "";
|
|
30
|
+
const extract = compileTextExcerpt(options);
|
|
31
|
+
return (ctx) => literalWikitext(extract(ctx.page.readableText ?? ""));
|
|
32
|
+
}
|
|
33
|
+
const registeredGetter =
|
|
34
|
+
braceParam !== undefined ? createRegisteredGetter(name, braceParam, format) : null;
|
|
35
|
+
if (registeredGetter) return registeredGetter;
|
|
23
36
|
const braceGetter = braceParam !== undefined ? createBraceParamGetter(name, braceParam) : null;
|
|
24
37
|
if (braceGetter) return braceGetter;
|
|
38
|
+
if (braceParam !== undefined) return () => "";
|
|
25
39
|
|
|
26
40
|
const parenGetter = parenParam !== undefined ? createParenParamGetter(name, parenParam) : null;
|
|
27
41
|
if (parenGetter) return parenGetter;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { formatDate, formatTagsLinked, splitContentSections } from "../format";
|
|
2
2
|
import type { VariableGetter } from "./types";
|
|
3
|
+
import { excerptText } from "@wdprlib/ast";
|
|
4
|
+
import { literalWikitext } from "../literal";
|
|
3
5
|
|
|
4
6
|
export function createBraceParamGetter(name: string, param: string): VariableGetter | null {
|
|
5
7
|
switch (name) {
|
|
@@ -28,7 +30,7 @@ export function createParenParamGetter(name: string, param: string): VariableGet
|
|
|
28
30
|
if (name !== "preview") return null;
|
|
29
31
|
|
|
30
32
|
const len = Number(param);
|
|
31
|
-
return (ctx) => (ctx.page.
|
|
33
|
+
return (ctx) => literalWikitext(excerptText(ctx.page.readableText ?? "", { maxLength: len }));
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export function createFormattedGetter(name: string, format: string): VariableGetter | null {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { formatDate } from "../format";
|
|
2
|
+
import { literalWikitext } from "../literal";
|
|
3
|
+
import type { VariableGetter } from "./types";
|
|
4
|
+
|
|
5
|
+
/** Unavailable values stay empty; zero is an available aggregate. */
|
|
6
|
+
export function createRegisteredGetter(
|
|
7
|
+
name: string,
|
|
8
|
+
key: string,
|
|
9
|
+
format?: string,
|
|
10
|
+
): VariableGetter | null {
|
|
11
|
+
if (name === "metadata")
|
|
12
|
+
return (ctx) => {
|
|
13
|
+
const metadata = ctx.page.metadata;
|
|
14
|
+
const entry = metadata && Object.hasOwn(metadata, key) ? metadata[key] : null;
|
|
15
|
+
if (!entry) return "";
|
|
16
|
+
let value: string;
|
|
17
|
+
switch (entry.type) {
|
|
18
|
+
case "text":
|
|
19
|
+
value = entry.value;
|
|
20
|
+
break;
|
|
21
|
+
case "number":
|
|
22
|
+
value = String(entry.value);
|
|
23
|
+
break;
|
|
24
|
+
case "date":
|
|
25
|
+
value = formatDate(entry.value, format);
|
|
26
|
+
break;
|
|
27
|
+
case "user":
|
|
28
|
+
value =
|
|
29
|
+
format === "id"
|
|
30
|
+
? String(entry.value.id)
|
|
31
|
+
: format === "unix"
|
|
32
|
+
? entry.value.unixName
|
|
33
|
+
: entry.value.name;
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
return literalWikitext(value);
|
|
37
|
+
};
|
|
38
|
+
if (name === "customrate" || name === "customrate_votes" || name === "customrate_percent")
|
|
39
|
+
return (ctx) => {
|
|
40
|
+
const ratings = ctx.page.customRates;
|
|
41
|
+
const entry = ratings && Object.hasOwn(ratings, key) ? ratings[key] : null;
|
|
42
|
+
if (!entry) return "";
|
|
43
|
+
return String(
|
|
44
|
+
name === "customrate"
|
|
45
|
+
? entry.points
|
|
46
|
+
: name === "customrate_votes"
|
|
47
|
+
? entry.votes
|
|
48
|
+
: entry.percent,
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { formatDate, formatUserLinked
|
|
1
|
+
import { formatDate, formatUserLinked } from "../format";
|
|
2
|
+
import { countCharacters, excerptText } from "@wdprlib/ast";
|
|
3
|
+
import { literalWikitext } from "../literal";
|
|
2
4
|
import type { VariableGetter } from "./types";
|
|
3
5
|
|
|
4
6
|
/** Default character count for `%%preview%%` when no length is specified (Wikidot default). */
|
|
@@ -38,16 +40,24 @@ export const SIMPLE_GETTERS: Record<string, VariableGetter> = {
|
|
|
38
40
|
ctx.page.parentFullname ? `[[[${ctx.page.parentFullname} | ${ctx.page.parentTitle}]]]` : "",
|
|
39
41
|
|
|
40
42
|
content: (ctx) => ctx.page.content ?? "",
|
|
41
|
-
preview: (ctx) =>
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
preview: (ctx) =>
|
|
44
|
+
literalWikitext(
|
|
45
|
+
excerptText(ctx.page.readableText ?? "", { maxLength: DEFAULT_PREVIEW_LENGTH }),
|
|
46
|
+
),
|
|
47
|
+
summary: (ctx) => literalWikitext(ctx.page.firstParagraph ?? ""),
|
|
48
|
+
first_paragraph: (ctx) => literalWikitext(ctx.page.firstParagraph ?? ""),
|
|
44
49
|
|
|
45
50
|
tags: (ctx) => ctx.page.tags.join(" "),
|
|
46
51
|
_tags: (ctx) => ctx.page.hiddenTags.join(" "),
|
|
47
52
|
|
|
48
53
|
children: (ctx) => String(ctx.page.children),
|
|
49
54
|
comments: (ctx) => String(ctx.page.comments),
|
|
50
|
-
size: (ctx) =>
|
|
55
|
+
size: (ctx) =>
|
|
56
|
+
ctx.page.readableText !== undefined
|
|
57
|
+
? String(countCharacters(ctx.page.readableText))
|
|
58
|
+
: ctx.page.size !== undefined
|
|
59
|
+
? String(ctx.page.size)
|
|
60
|
+
: "",
|
|
51
61
|
rating: (ctx) => String(ctx.page.rating),
|
|
52
62
|
rating_votes: (ctx) => String(ctx.page.ratingVotes),
|
|
53
63
|
rating_percent: (ctx) => String(ctx.page.ratingPercent ?? 0),
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Protect text before secondary include expansion and wikitext parsing. */
|
|
2
|
+
export function literalWikitext(value: string): string {
|
|
3
|
+
if (value === "") return "";
|
|
4
|
+
// Encode delimiters and newlines too: a value cannot close this raw region.
|
|
5
|
+
return `@<${Array.from(value, (char) => `&#${char.codePointAt(0)};`).join("")}>@`;
|
|
6
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { findExcerptEnd } from "./excerpt";
|
|
2
|
+
|
|
1
3
|
export interface TemplateVariableMatch {
|
|
2
4
|
raw: string;
|
|
3
5
|
index: number;
|
|
@@ -25,7 +27,10 @@ export function scanTemplateVariables(template: string): TemplateVariableMatch[]
|
|
|
25
27
|
if (start === -1) break;
|
|
26
28
|
|
|
27
29
|
const contentStart = start + 2;
|
|
28
|
-
const end =
|
|
30
|
+
const end =
|
|
31
|
+
template.slice(contentStart, contentStart + 8).toLowerCase() === "excerpt{"
|
|
32
|
+
? findExcerptEnd(template, contentStart)
|
|
33
|
+
: template.indexOf("%%", contentStart);
|
|
29
34
|
if (end === -1) break;
|
|
30
35
|
|
|
31
36
|
const parsed = parseTemplateVariableContent(template.slice(contentStart, end));
|
|
@@ -53,6 +58,10 @@ function parseTemplateVariableContent(
|
|
|
53
58
|
if (cursor === 0) return null;
|
|
54
59
|
|
|
55
60
|
const name = content.slice(0, cursor);
|
|
61
|
+
if (name.toLowerCase() === "excerpt" && content[cursor] === "{") {
|
|
62
|
+
if (!content.endsWith("}")) return null;
|
|
63
|
+
return { name, braceParam: content.slice(cursor + 1, -1) };
|
|
64
|
+
}
|
|
56
65
|
let braceParam: string | undefined;
|
|
57
66
|
let parenParam: string | undefined;
|
|
58
67
|
let format: string | undefined;
|
|
@@ -24,6 +24,12 @@ export interface ListPagesDataRequirement {
|
|
|
24
24
|
|
|
25
25
|
/** Field names needed for form_data{field} etc. */
|
|
26
26
|
formFields?: string[];
|
|
27
|
+
/** Exact metadata keys used by display templates. Also validate query.order on the host. */
|
|
28
|
+
metadataKeys?: string[];
|
|
29
|
+
/** Exact custom rating keys needed for display, independent of query.ratingAxis. */
|
|
30
|
+
customRateKeys?: string[];
|
|
31
|
+
/** Supply PageData.readableText and firstParagraph for previews, excerpts, and summaries. */
|
|
32
|
+
needsReadableText?: boolean;
|
|
27
33
|
|
|
28
34
|
/** Prefix for tags_linked|prefix */
|
|
29
35
|
tagsLinkPrefix?: string;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import type { RatingAggregate } from "@wdprlib/ast";
|
|
2
|
+
|
|
3
|
+
/** Values for host-registered metadata keys. Missing entries remain unavailable. */
|
|
4
|
+
export type PageMetadataValue =
|
|
5
|
+
| { type: "text"; value: string }
|
|
6
|
+
| { type: "number"; value: number }
|
|
7
|
+
| { type: "date"; value: Date }
|
|
8
|
+
| { type: "user"; value: UserInfo };
|
|
9
|
+
|
|
1
10
|
/**
|
|
2
11
|
* User information.
|
|
3
12
|
*/
|
|
@@ -11,6 +20,18 @@ export interface UserInfo {
|
|
|
11
20
|
* Page data provided by an external source.
|
|
12
21
|
*/
|
|
13
22
|
export interface PageData {
|
|
23
|
+
/**
|
|
24
|
+
* `%%metadata{key}%%`: only registered, readable entries; keys are case-sensitive.
|
|
25
|
+
* Values are literal text. Date values accept strftime; user format is name, |id or |unix.
|
|
26
|
+
* Tag change date/editor may be host-defined keys; WDPR reserves no such keys.
|
|
27
|
+
*/
|
|
28
|
+
metadata?: Readonly<Record<string, PageMetadataValue | null>>;
|
|
29
|
+
/**
|
|
30
|
+
* `%%customrate{key}%%`, `%%customrate_votes{key}%%`, `%%customrate_percent{key}%%`.
|
|
31
|
+
* Supply readable aggregates for requirement.customRateKeys, just as formFields selects
|
|
32
|
+
* form data. Missing keys expand to empty text; a supplied zero remains zero.
|
|
33
|
+
*/
|
|
34
|
+
customRates?: Readonly<Record<string, RatingAggregate | null>>;
|
|
14
35
|
// Identity
|
|
15
36
|
name: string;
|
|
16
37
|
category: string;
|
|
@@ -37,6 +58,19 @@ export interface PageData {
|
|
|
37
58
|
|
|
38
59
|
// Wikitext content split by ==== when resolving %%content{n}%%.
|
|
39
60
|
content?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Text extracted from this page's resolved AST for preview and
|
|
63
|
+
* `%%excerpt{pattern="..." group="1" match="2" max="200"}%%`.
|
|
64
|
+
* Values stay literal; raw content is never a fallback.
|
|
65
|
+
* The host owns extraction policy, dependency invalidation and recursion limits.
|
|
66
|
+
*/
|
|
67
|
+
readableText?: string;
|
|
68
|
+
/**
|
|
69
|
+
* First nonempty paragraph from extractFirstParagraph, for summary and first_paragraph.
|
|
70
|
+
* Uses the same extraction policy as readableText. Empty when no readable paragraph
|
|
71
|
+
* exists; undefined when unavailable. Headings and appended footnotes are excluded.
|
|
72
|
+
*/
|
|
73
|
+
firstParagraph?: string;
|
|
40
74
|
|
|
41
75
|
// Tags
|
|
42
76
|
tags: string[];
|
|
@@ -51,7 +85,8 @@ export interface PageData {
|
|
|
51
85
|
// Metrics
|
|
52
86
|
children: number;
|
|
53
87
|
comments: number;
|
|
54
|
-
|
|
88
|
+
/** Materialized readable character count, computed with countCharacters. */
|
|
89
|
+
size?: number;
|
|
55
90
|
rating: number;
|
|
56
91
|
ratingVotes: number;
|
|
57
92
|
ratingPercent?: number;
|
|
@@ -73,7 +108,6 @@ export function definePageData(input: PageDataInput): PageData {
|
|
|
73
108
|
hiddenTags: input.hiddenTags ?? [],
|
|
74
109
|
children: input.children ?? 0,
|
|
75
110
|
comments: input.comments ?? 0,
|
|
76
|
-
size: input.size ?? 0,
|
|
77
111
|
rating: input.rating ?? 0,
|
|
78
112
|
ratingVotes: input.ratingVotes ?? 0,
|
|
79
113
|
revisions: input.revisions ?? 0,
|
|
@@ -32,6 +32,8 @@ export interface NormalizedCategory {
|
|
|
32
32
|
export type OrderField =
|
|
33
33
|
| "created_at"
|
|
34
34
|
| "updated_at"
|
|
35
|
+
| "commented_at"
|
|
36
|
+
| "metadata"
|
|
35
37
|
| "title"
|
|
36
38
|
| "fullname"
|
|
37
39
|
| "rating"
|
|
@@ -48,11 +50,13 @@ export type OrderDirection = "asc" | "desc";
|
|
|
48
50
|
|
|
49
51
|
/**
|
|
50
52
|
* Normalized order specification.
|
|
53
|
+
* Providers validate metadata registration, readability and sortability without falling back
|
|
54
|
+
* to another field. Sort before pagination with a stable page ID tie-breaker. For commented_at,
|
|
55
|
+
* put pages without comments last and use the same visible, non-deleted comments as the count.
|
|
51
56
|
*/
|
|
52
|
-
export
|
|
53
|
-
field: OrderField
|
|
54
|
-
direction: OrderDirection;
|
|
55
|
-
}
|
|
57
|
+
export type NormalizedOrder =
|
|
58
|
+
| { field: Exclude<OrderField, "metadata">; direction: OrderDirection }
|
|
59
|
+
| { field: "metadata"; key: string; direction: OrderDirection };
|
|
56
60
|
|
|
57
61
|
/**
|
|
58
62
|
* Normalized parent selector.
|
|
@@ -107,6 +111,12 @@ export interface NormalizedListPagesQuery {
|
|
|
107
111
|
updatedAt?: NormalizedDateSelector;
|
|
108
112
|
createdBy?: string;
|
|
109
113
|
rating?: NormalizedNumericSelector;
|
|
114
|
+
/**
|
|
115
|
+
* `rating-axis` selects the custom aggregate for rating/votes filters and order; omission
|
|
116
|
+
* selects main. Verify registration and read access before filtering, counting or sorting.
|
|
117
|
+
* Unknown keys must not fall back to main. Display variables select their own keys.
|
|
118
|
+
*/
|
|
119
|
+
ratingAxis?: string;
|
|
110
120
|
votes?: NormalizedNumericSelector;
|
|
111
121
|
name?: string;
|
|
112
122
|
fullname?: string;
|
|
@@ -34,6 +34,8 @@ export interface ListPagesQuery {
|
|
|
34
34
|
|
|
35
35
|
/** Rating selector. @untrusted User input. */
|
|
36
36
|
rating?: string;
|
|
37
|
+
/** Exact registered custom key for rating/votes filters and order; omitted means main. */
|
|
38
|
+
ratingAxis?: string;
|
|
37
39
|
|
|
38
40
|
/** Votes selector. @untrusted User input. */
|
|
39
41
|
votes?: string;
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* All supported ListPages template variables.
|
|
3
3
|
*/
|
|
4
4
|
export type ListPagesVariable =
|
|
5
|
+
| "metadata"
|
|
6
|
+
| "customrate"
|
|
7
|
+
| "customrate_votes"
|
|
8
|
+
| "customrate_percent"
|
|
5
9
|
// Lifecycle - created
|
|
6
10
|
| "created_at"
|
|
7
11
|
| "created_by"
|
|
@@ -38,6 +42,7 @@ export type ListPagesVariable =
|
|
|
38
42
|
| "content_n"
|
|
39
43
|
| "preview"
|
|
40
44
|
| "preview_n"
|
|
45
|
+
| "excerpt"
|
|
41
46
|
| "summary"
|
|
42
47
|
| "first_paragraph"
|
|
43
48
|
// Tags
|
|
@@ -36,6 +36,7 @@ type StringUrlQueryKey = Extract<
|
|
|
36
36
|
| "updatedAt"
|
|
37
37
|
| "createdBy"
|
|
38
38
|
| "rating"
|
|
39
|
+
| "ratingAxis"
|
|
39
40
|
| "votes"
|
|
40
41
|
>;
|
|
41
42
|
|
|
@@ -81,6 +82,7 @@ export const URL_RESOLVABLE_FIELDS: readonly UrlResolvableField[] = [
|
|
|
81
82
|
aliases: ["createdby", "created_by"],
|
|
82
83
|
},
|
|
83
84
|
{ attr: "rating", queryKey: "rating", type: "string" },
|
|
85
|
+
{ attr: "rating-axis", queryKey: "ratingAxis", type: "string" },
|
|
84
86
|
{ attr: "votes", queryKey: "votes", type: "string" },
|
|
85
87
|
{ attr: "reverse", queryKey: "reverse", type: "boolean" },
|
|
86
88
|
];
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
ListUsersVariableContext,
|
|
17
17
|
} from "./types";
|
|
18
18
|
import { getModuleParseAst, type ParseFunction } from "../types";
|
|
19
|
+
import { suppressModuleRatings } from "../rate/resolve";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Narrowed type for the list-users variant of the Module discriminated union.
|
|
@@ -53,6 +54,6 @@ export function resolveListUsers(
|
|
|
53
54
|
): Element[] {
|
|
54
55
|
const ctx: ListUsersVariableContext = { user: data.user };
|
|
55
56
|
const substituted = compiledTemplate(ctx);
|
|
56
|
-
const itemAst = getModuleParseAst(parse(substituted));
|
|
57
|
+
const itemAst = getModuleParseAst(suppressModuleRatings(parse(substituted)));
|
|
57
58
|
return itemAst.elements;
|
|
58
59
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { ModuleRule } from "./types";
|
|
14
|
-
import { rateModuleRule } from "./rate/index";
|
|
14
|
+
import { rateModuleRule, customRateModuleRule } from "./rate/index";
|
|
15
15
|
import { cssModuleRule } from "./css/index";
|
|
16
16
|
import { backlinksModuleRule } from "./backlinks/index";
|
|
17
17
|
import { categoriesModuleRule } from "./categories/index";
|
|
@@ -29,6 +29,7 @@ import { tagCloudModuleRule } from "./tagcloud/parser";
|
|
|
29
29
|
*/
|
|
30
30
|
export const MODULE_RULES: ModuleRule[] = [
|
|
31
31
|
rateModuleRule,
|
|
32
|
+
customRateModuleRule,
|
|
32
33
|
cssModuleRule,
|
|
33
34
|
backlinksModuleRule,
|
|
34
35
|
categoriesModuleRule,
|