@wdprlib/parser 3.1.2 → 3.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.cjs +295 -118
- package/dist/index.js +272 -95
- package/package.json +5 -3
- package/src/index.ts +163 -0
- package/src/lexer/index.ts +20 -0
- package/src/lexer/lexer.ts +687 -0
- package/src/lexer/tokens.ts +141 -0
- package/src/parser/constants.ts +173 -0
- package/src/parser/depth.ts +251 -0
- package/src/parser/index.ts +18 -0
- package/src/parser/parse.ts +315 -0
- package/src/parser/postprocess/divAdjacentParagraph.ts +76 -0
- package/src/parser/postprocess/index.ts +15 -0
- package/src/parser/postprocess/spanStrip.ts +697 -0
- package/src/parser/preprocess/expr.ts +265 -0
- package/src/parser/preprocess/index.ts +38 -0
- package/src/parser/preprocess/typography.ts +67 -0
- package/src/parser/preprocess/utils.ts +250 -0
- package/src/parser/preprocess/whitespace.ts +111 -0
- package/src/parser/rules/block/align.ts +282 -0
- package/src/parser/rules/block/bibliography.ts +359 -0
- package/src/parser/rules/block/block-list.ts +689 -0
- package/src/parser/rules/block/blockquote.ts +238 -0
- package/src/parser/rules/block/center.ts +87 -0
- package/src/parser/rules/block/clear-float.ts +75 -0
- package/src/parser/rules/block/code.ts +187 -0
- package/src/parser/rules/block/collapsible.ts +337 -0
- package/src/parser/rules/block/comment.ts +73 -0
- package/src/parser/rules/block/content-separator.ts +79 -0
- package/src/parser/rules/block/definition-list.ts +270 -0
- package/src/parser/rules/block/div.ts +400 -0
- package/src/parser/rules/block/embed-block.ts +153 -0
- package/src/parser/rules/block/footnoteblock.ts +200 -0
- package/src/parser/rules/block/heading.ts +142 -0
- package/src/parser/rules/block/horizontal-rule.ts +61 -0
- package/src/parser/rules/block/html.ts +222 -0
- package/src/parser/rules/block/iframe.ts +239 -0
- package/src/parser/rules/block/iftags.ts +150 -0
- package/src/parser/rules/block/include.ts +179 -0
- package/src/parser/rules/block/index.ts +127 -0
- package/src/parser/rules/block/list.ts +244 -0
- package/src/parser/rules/block/math.ts +183 -0
- package/src/parser/rules/block/module/backlinks/index.ts +31 -0
- package/src/parser/rules/block/module/backlinks/types.ts +21 -0
- package/src/parser/rules/block/module/categories/index.ts +34 -0
- package/src/parser/rules/block/module/categories/types.ts +21 -0
- package/src/parser/rules/block/module/css/index.ts +37 -0
- package/src/parser/rules/block/module/iftags/condition.ts +109 -0
- package/src/parser/rules/block/module/iftags/index.ts +26 -0
- package/src/parser/rules/block/module/iftags/preprocess.ts +140 -0
- package/src/parser/rules/block/module/iftags/resolve.ts +73 -0
- package/src/parser/rules/block/module/iftags/types.ts +63 -0
- package/src/parser/rules/block/module/include/index.ts +20 -0
- package/src/parser/rules/block/module/include/resolve.ts +556 -0
- package/src/parser/rules/block/module/index.ts +122 -0
- package/src/parser/rules/block/module/join/index.ts +34 -0
- package/src/parser/rules/block/module/join/types.ts +23 -0
- package/src/parser/rules/block/module/listpages/compiler.ts +453 -0
- package/src/parser/rules/block/module/listpages/extract.ts +410 -0
- package/src/parser/rules/block/module/listpages/index.ts +83 -0
- package/src/parser/rules/block/module/listpages/normalize.ts +390 -0
- package/src/parser/rules/block/module/listpages/parser.ts +106 -0
- package/src/parser/rules/block/module/listpages/resolve.ts +130 -0
- package/src/parser/rules/block/module/listpages/types.ts +513 -0
- package/src/parser/rules/block/module/listpages/url-resolver.ts +186 -0
- package/src/parser/rules/block/module/listusers/compiler.ts +77 -0
- package/src/parser/rules/block/module/listusers/extract.ts +45 -0
- package/src/parser/rules/block/module/listusers/index.ts +36 -0
- package/src/parser/rules/block/module/listusers/parser.ts +54 -0
- package/src/parser/rules/block/module/listusers/resolve.ts +58 -0
- package/src/parser/rules/block/module/listusers/types.ts +93 -0
- package/src/parser/rules/block/module/mapping.ts +61 -0
- package/src/parser/rules/block/module/page-tree/index.ts +38 -0
- package/src/parser/rules/block/module/page-tree/types.ts +29 -0
- package/src/parser/rules/block/module/rate/index.ts +28 -0
- package/src/parser/rules/block/module/rate/types.ts +19 -0
- package/src/parser/rules/block/module/resolve.ts +411 -0
- package/src/parser/rules/block/module/types-common.ts +59 -0
- package/src/parser/rules/block/module/types.ts +61 -0
- package/src/parser/rules/block/module/utils.ts +43 -0
- package/src/parser/rules/block/module/walk.ts +380 -0
- package/src/parser/rules/block/module.ts +164 -0
- package/src/parser/rules/block/orphan-li.ts +177 -0
- package/src/parser/rules/block/paragraph.ts +157 -0
- package/src/parser/rules/block/table-block.ts +726 -0
- package/src/parser/rules/block/table.ts +441 -0
- package/src/parser/rules/block/tabview.ts +331 -0
- package/src/parser/rules/block/toc.ts +129 -0
- package/src/parser/rules/block/utils.ts +615 -0
- package/src/parser/rules/index.ts +49 -0
- package/src/parser/rules/inline/anchor-name.ts +154 -0
- package/src/parser/rules/inline/anchor.ts +327 -0
- package/src/parser/rules/inline/bibcite.ts +153 -0
- package/src/parser/rules/inline/bold.ts +86 -0
- package/src/parser/rules/inline/color.ts +140 -0
- package/src/parser/rules/inline/comment.ts +90 -0
- package/src/parser/rules/inline/equation-ref.ts +115 -0
- package/src/parser/rules/inline/expr.ts +526 -0
- package/src/parser/rules/inline/footnote.ts +223 -0
- package/src/parser/rules/inline/guillemet.ts +64 -0
- package/src/parser/rules/inline/html.ts +132 -0
- package/src/parser/rules/inline/image.ts +328 -0
- package/src/parser/rules/inline/index.ts +150 -0
- package/src/parser/rules/inline/italic.ts +74 -0
- package/src/parser/rules/inline/line-break.ts +326 -0
- package/src/parser/rules/inline/link-anchor.ts +147 -0
- package/src/parser/rules/inline/link-single.ts +164 -0
- package/src/parser/rules/inline/link-star.ts +134 -0
- package/src/parser/rules/inline/link-triple.ts +267 -0
- package/src/parser/rules/inline/math-inline.ts +126 -0
- package/src/parser/rules/inline/monospace.ts +78 -0
- package/src/parser/rules/inline/raw.ts +262 -0
- package/src/parser/rules/inline/size.ts +244 -0
- package/src/parser/rules/inline/span.ts +424 -0
- package/src/parser/rules/inline/strikethrough.ts +115 -0
- package/src/parser/rules/inline/subscript.ts +84 -0
- package/src/parser/rules/inline/superscript.ts +84 -0
- package/src/parser/rules/inline/text.ts +84 -0
- package/src/parser/rules/inline/underline.ts +127 -0
- package/src/parser/rules/inline/user.ts +147 -0
- package/src/parser/rules/inline/utils.ts +344 -0
- package/src/parser/rules/types.ts +252 -0
- package/src/parser/rules/utils.ts +155 -0
- package/src/parser/toc.ts +130 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module system for Wikidot dynamic constructs.
|
|
3
|
+
*
|
|
4
|
+
* Wikidot supports several "module" blocks (`[[module ListPages]]`,
|
|
5
|
+
* `[[module ListUsers]]`, `[[module CSS]]`, etc.) and pseudo-blocks
|
|
6
|
+
* (`[[include]]`, `[[iftags]]`) that require external data to resolve.
|
|
7
|
+
*
|
|
8
|
+
* This barrel module re-exports everything needed to:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Extract** data requirements from a parsed AST
|
|
11
|
+
* ({@link extractDataRequirements})
|
|
12
|
+
* 2. **Compile** body templates that contain `%%variable%%` placeholders
|
|
13
|
+
* ({@link compileTemplate}, {@link compileListUsersTemplate})
|
|
14
|
+
* 3. **Resolve** modules by injecting fetched data back into the AST
|
|
15
|
+
* ({@link resolveModules})
|
|
16
|
+
* 4. **Resolve includes** by fetching and inlining included pages
|
|
17
|
+
* ({@link resolveIncludes})
|
|
18
|
+
*
|
|
19
|
+
* @module
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
// Module rule types and registry
|
|
23
|
+
export type { ModuleRule } from "./types";
|
|
24
|
+
export { MODULE_RULES, getModuleRuleByName } from "./mapping";
|
|
25
|
+
|
|
26
|
+
// Module parsers
|
|
27
|
+
export { rateModuleRule } from "./rate/index";
|
|
28
|
+
export { cssModuleRule } from "./css/index";
|
|
29
|
+
export { backlinksModuleRule } from "./backlinks/index";
|
|
30
|
+
export { categoriesModuleRule } from "./categories/index";
|
|
31
|
+
export { joinModuleRule } from "./join/index";
|
|
32
|
+
export { pageTreeModuleRule } from "./page-tree/index";
|
|
33
|
+
export { listPagesModuleRule } from "./listpages/parser";
|
|
34
|
+
export { listUsersModuleRule } from "./listusers/parser";
|
|
35
|
+
|
|
36
|
+
// Module data types (parser-only modules)
|
|
37
|
+
export type { RateModuleData } from "./rate/types";
|
|
38
|
+
export type { BacklinksModuleData } from "./backlinks/types";
|
|
39
|
+
export type { CategoriesModuleData } from "./categories/types";
|
|
40
|
+
export type { JoinModuleData } from "./join/types";
|
|
41
|
+
export type { PageTreeModuleData } from "./page-tree/types";
|
|
42
|
+
|
|
43
|
+
// Common types
|
|
44
|
+
export type { DataProvider } from "./types-common";
|
|
45
|
+
|
|
46
|
+
// ListPages module
|
|
47
|
+
export type {
|
|
48
|
+
ListPagesQuery,
|
|
49
|
+
ListPagesVariable,
|
|
50
|
+
ListPagesDataRequirement,
|
|
51
|
+
DataRequirements,
|
|
52
|
+
UserInfo,
|
|
53
|
+
PageData,
|
|
54
|
+
SiteContext,
|
|
55
|
+
ListPagesExternalData,
|
|
56
|
+
ListPagesDataFetcher,
|
|
57
|
+
VariableContext,
|
|
58
|
+
CompiledTemplate,
|
|
59
|
+
ExtractionResult,
|
|
60
|
+
ParseFunction,
|
|
61
|
+
ListPagesModuleData,
|
|
62
|
+
// Normalized types
|
|
63
|
+
NormalizedListPagesQuery,
|
|
64
|
+
NormalizedTags,
|
|
65
|
+
NormalizedCategory,
|
|
66
|
+
NormalizedOrder,
|
|
67
|
+
NormalizedParent,
|
|
68
|
+
NormalizedDateSelector,
|
|
69
|
+
NormalizedNumericSelector,
|
|
70
|
+
} from "./listpages";
|
|
71
|
+
export {
|
|
72
|
+
extractDataRequirements,
|
|
73
|
+
isListPagesModule,
|
|
74
|
+
resolveListPages,
|
|
75
|
+
compileTemplate,
|
|
76
|
+
// Query normalization (for advanced use cases)
|
|
77
|
+
normalizeQuery,
|
|
78
|
+
parseTags,
|
|
79
|
+
parseCategory,
|
|
80
|
+
parseOrder,
|
|
81
|
+
parseParent,
|
|
82
|
+
parseDateSelector,
|
|
83
|
+
parseNumericSelector,
|
|
84
|
+
} from "./listpages";
|
|
85
|
+
|
|
86
|
+
// IfTags module
|
|
87
|
+
export type { TagCondition, IfTagsResolver, IfTagsData, IfTagsResolveResult } from "./iftags";
|
|
88
|
+
export {
|
|
89
|
+
parseTagCondition,
|
|
90
|
+
evaluateTagCondition,
|
|
91
|
+
isIfTagsElement,
|
|
92
|
+
resolveIfTags,
|
|
93
|
+
preprocessIftags,
|
|
94
|
+
} from "./iftags";
|
|
95
|
+
|
|
96
|
+
// Include module
|
|
97
|
+
export type { IncludeFetcher, AsyncIncludeFetcher, ResolveIncludesOptions } from "./include";
|
|
98
|
+
export { resolveIncludes, resolveIncludesAsync } from "./include";
|
|
99
|
+
|
|
100
|
+
// ListUsers module
|
|
101
|
+
export type {
|
|
102
|
+
ListUsersVariable,
|
|
103
|
+
ListUsersUserData,
|
|
104
|
+
ListUsersDataRequirement,
|
|
105
|
+
ListUsersExternalData,
|
|
106
|
+
ListUsersDataFetcher,
|
|
107
|
+
ListUsersVariableContext,
|
|
108
|
+
ListUsersCompiledTemplate,
|
|
109
|
+
ListUsersModuleData,
|
|
110
|
+
} from "./listusers";
|
|
111
|
+
export {
|
|
112
|
+
listUsersModuleRule as listUsersRule,
|
|
113
|
+
extractListUsersVariables,
|
|
114
|
+
compileListUsersTemplate,
|
|
115
|
+
isListUsersModule,
|
|
116
|
+
resolveListUsers,
|
|
117
|
+
} from "./listusers";
|
|
118
|
+
|
|
119
|
+
// Module resolver
|
|
120
|
+
export type { ResolveOptions } from "./resolve";
|
|
121
|
+
export { resolveModules } from "./resolve";
|
|
122
|
+
export { STYLE_SLOT_PREFIX } from "@wdprlib/ast";
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Parser rule for the Wikidot `[[module Join]]` block.
|
|
4
|
+
*
|
|
5
|
+
* Renders a "Join this site" button. Accepts an optional `button` attribute
|
|
6
|
+
* to customize the button text.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ModuleRule } from "../types";
|
|
12
|
+
import type { JoinModuleData } from "./types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Module rule for `[[module Join]]`.
|
|
16
|
+
*
|
|
17
|
+
* Extracts the `button` attribute as the custom button text. All other
|
|
18
|
+
* attributes are passed through in the `attributes` record for the
|
|
19
|
+
* rendering application to handle.
|
|
20
|
+
*/
|
|
21
|
+
export const joinModuleRule: ModuleRule = {
|
|
22
|
+
name: "module-join",
|
|
23
|
+
acceptsNames: ["join"],
|
|
24
|
+
hasBody: false,
|
|
25
|
+
|
|
26
|
+
parse(_ctx, _pos, args): JoinModuleData {
|
|
27
|
+
const { button, ...rest } = args;
|
|
28
|
+
return {
|
|
29
|
+
module: "join",
|
|
30
|
+
"button-text": button ?? null,
|
|
31
|
+
attributes: rest,
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Type definitions for the Join module.
|
|
4
|
+
*
|
|
5
|
+
* The `[[module Join]]` block renders a "Join this site" button that allows
|
|
6
|
+
* visitors to apply for site membership.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* AST data for a `[[module Join]]` element.
|
|
13
|
+
*
|
|
14
|
+
* The rendering application should display a membership application button
|
|
15
|
+
* with the specified text.
|
|
16
|
+
*/
|
|
17
|
+
export interface JoinModuleData {
|
|
18
|
+
module: "join";
|
|
19
|
+
/** Custom button text, or null to use the default "Join this Site" text */
|
|
20
|
+
"button-text": string | null;
|
|
21
|
+
/** Additional attributes passed to the module */
|
|
22
|
+
attributes: Record<string, string>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Template compiler for the ListPages module.
|
|
4
|
+
*
|
|
5
|
+
* Compiles ListPages template strings (e.g., `"%%title%% by %%created_by%%"`)
|
|
6
|
+
* into executable functions that can be called repeatedly with different page
|
|
7
|
+
* data for fast rendering. The compilation step splits the template into static
|
|
8
|
+
* string segments and dynamic getter functions, avoiding repeated regex matching
|
|
9
|
+
* during rendering.
|
|
10
|
+
*
|
|
11
|
+
* Supported variable syntax:
|
|
12
|
+
* - `%%name%%` - Simple variable (e.g., `%%title%%`, `%%rating%%`)
|
|
13
|
+
* - `%%name{param}%%` - Parameterized variable (e.g., `%%content{2}%%`, `%%form_data{color}%%`)
|
|
14
|
+
* - `%%name(param)%%` - Parenthesized parameter (e.g., `%%preview(100)%%`)
|
|
15
|
+
* - `%%name|format%%` - Formatted variable (e.g., `%%created_at|%Y-%m-%d%%`, `%%tags_linked|/tag/%%`)
|
|
16
|
+
*
|
|
17
|
+
* The compiled function is a closure over the parsed template parts, providing
|
|
18
|
+
* O(n) rendering time proportional to the number of template segments.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { CompiledTemplate, VariableContext, PageData, UserInfo } from "./types";
|
|
24
|
+
|
|
25
|
+
/** Default character count for `%%preview%%` when no length is specified (Wikidot default). */
|
|
26
|
+
const DEFAULT_PREVIEW_LENGTH = 200;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Regex pattern for matching ListPages template variables with all parameter variants.
|
|
30
|
+
*
|
|
31
|
+
* Captures: [1] variable name, [2] brace parameter, [3] paren parameter, [4] format string.
|
|
32
|
+
* The format portion allows single `%` characters (for strftime tokens like `%Y`)
|
|
33
|
+
* but stops at `%%` (which terminates the variable).
|
|
34
|
+
*/
|
|
35
|
+
const VARIABLE_REGEX =
|
|
36
|
+
/%%([a-z_]+)(?:\{([^}]+)\})?(?:\((\d+)\))?(?:\|([^%]*(?:%(?!%)[^%]*)*))?%%/gi;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Compile a ListPages template string into an executable function.
|
|
40
|
+
*
|
|
41
|
+
* The template is split into alternating static strings and dynamic getter
|
|
42
|
+
* functions. The returned function concatenates these parts with the getter
|
|
43
|
+
* functions evaluated against the provided variable context.
|
|
44
|
+
*
|
|
45
|
+
* @param template - The template string containing `%%variable%%` placeholders
|
|
46
|
+
* @returns A compiled function that accepts a `VariableContext` and returns the rendered string
|
|
47
|
+
*/
|
|
48
|
+
export function compileTemplate(template: string): CompiledTemplate {
|
|
49
|
+
const parts: (string | ((ctx: VariableContext) => string))[] = [];
|
|
50
|
+
let lastIndex = 0;
|
|
51
|
+
|
|
52
|
+
// Split template into static and dynamic parts
|
|
53
|
+
for (const match of template.matchAll(VARIABLE_REGEX)) {
|
|
54
|
+
// Add static part before this match
|
|
55
|
+
if (match.index !== undefined && match.index > lastIndex) {
|
|
56
|
+
parts.push(template.slice(lastIndex, match.index));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Convert variable to getter function
|
|
60
|
+
const [, varName, braceParam, parenParam, format] = match;
|
|
61
|
+
if (!varName) continue;
|
|
62
|
+
const getter = createVariableGetter(varName.toLowerCase(), braceParam, parenParam, format);
|
|
63
|
+
parts.push(getter);
|
|
64
|
+
|
|
65
|
+
lastIndex = match.index !== undefined ? match.index + match[0].length : lastIndex;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Add remaining static part
|
|
69
|
+
if (lastIndex < template.length) {
|
|
70
|
+
parts.push(template.slice(lastIndex));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Return compiled function
|
|
74
|
+
return (ctx: VariableContext): string => {
|
|
75
|
+
let result = "";
|
|
76
|
+
for (const part of parts) {
|
|
77
|
+
result += typeof part === "string" ? part : part(ctx);
|
|
78
|
+
}
|
|
79
|
+
return result;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a getter function for a specific template variable.
|
|
85
|
+
*
|
|
86
|
+
* Handles all variable variants: parameterized (`{param}`), parenthesized (`(param)`),
|
|
87
|
+
* formatted (`|format`), and simple. Returns a function that extracts the appropriate
|
|
88
|
+
* value from a `VariableContext`.
|
|
89
|
+
*
|
|
90
|
+
* Unknown variable names return a function that always returns an empty string,
|
|
91
|
+
* matching Wikidot's behavior of silently ignoring unknown variables.
|
|
92
|
+
*
|
|
93
|
+
* @param name - Lowercase variable name
|
|
94
|
+
* @param braceParam - Content inside `{...}`, if present
|
|
95
|
+
* @param parenParam - Content inside `(...)`, if present
|
|
96
|
+
* @param format - Content after `|`, if present
|
|
97
|
+
* @returns A function that extracts the variable's value from a VariableContext
|
|
98
|
+
*/
|
|
99
|
+
function createVariableGetter(
|
|
100
|
+
name: string,
|
|
101
|
+
braceParam?: string,
|
|
102
|
+
parenParam?: string,
|
|
103
|
+
format?: string,
|
|
104
|
+
): (ctx: VariableContext) => string {
|
|
105
|
+
// Parameterized variables with {param}
|
|
106
|
+
if (braceParam !== undefined) {
|
|
107
|
+
switch (name) {
|
|
108
|
+
case "content": {
|
|
109
|
+
// %%content{n}%% - n is 1-indexed, split content by ==== on demand
|
|
110
|
+
const idx = Number(braceParam) - 1;
|
|
111
|
+
return (ctx) => {
|
|
112
|
+
if (!ctx.page.content) return "";
|
|
113
|
+
const sections = splitContentSections(ctx.page.content);
|
|
114
|
+
return sections[idx] ?? "";
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
case "form_data":
|
|
118
|
+
return (ctx) => ctx.page.formData?.[braceParam] ?? "";
|
|
119
|
+
case "form_raw":
|
|
120
|
+
return (ctx) => ctx.page.formRaw?.[braceParam] ?? "";
|
|
121
|
+
case "form_label":
|
|
122
|
+
return (ctx) => ctx.page.formLabel?.[braceParam] ?? "";
|
|
123
|
+
case "form_hint":
|
|
124
|
+
return (ctx) => ctx.page.formHint?.[braceParam] ?? "";
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Parameterized variables with (param)
|
|
129
|
+
if (parenParam !== undefined && name === "preview") {
|
|
130
|
+
const len = Number(parenParam);
|
|
131
|
+
return (ctx) => (ctx.page.content ?? "").slice(0, len);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Date variables with format
|
|
135
|
+
if (format !== undefined) {
|
|
136
|
+
switch (name) {
|
|
137
|
+
case "created_at":
|
|
138
|
+
return (ctx) => formatDate(ctx.page.createdAt, format);
|
|
139
|
+
case "updated_at":
|
|
140
|
+
return (ctx) => formatDate(ctx.page.updatedAt, format);
|
|
141
|
+
case "commented_at":
|
|
142
|
+
return (ctx) => (ctx.page.commentedAt ? formatDate(ctx.page.commentedAt, format) : "");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// tags_linked with prefix
|
|
147
|
+
if (name === "tags_linked") {
|
|
148
|
+
const prefix = format ?? "/system:page-tags/tag/";
|
|
149
|
+
return (ctx) => formatTagsLinked(ctx.page.tags, prefix);
|
|
150
|
+
}
|
|
151
|
+
if (name === "_tags_linked") {
|
|
152
|
+
const prefix = format ?? "/system:page-tags/tag/";
|
|
153
|
+
return (ctx) => formatTagsLinked(ctx.page.hiddenTags, prefix);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Simple variables
|
|
157
|
+
const getter = SIMPLE_GETTERS[name];
|
|
158
|
+
if (getter) return getter;
|
|
159
|
+
|
|
160
|
+
// Unknown variable - return empty string
|
|
161
|
+
return () => "";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// =============================================================================
|
|
165
|
+
// Simple Variable Getters
|
|
166
|
+
// =============================================================================
|
|
167
|
+
|
|
168
|
+
const SIMPLE_GETTERS: Record<string, (ctx: VariableContext) => string> = {
|
|
169
|
+
// Lifecycle - created
|
|
170
|
+
created_at: (ctx) => formatDate(ctx.page.createdAt),
|
|
171
|
+
created_by: (ctx) => ctx.page.createdBy?.name ?? "Anonymous",
|
|
172
|
+
created_by_unix: (ctx) => ctx.page.createdBy?.unixName ?? "",
|
|
173
|
+
created_by_id: (ctx) => String(ctx.page.createdBy?.id ?? 0),
|
|
174
|
+
created_by_linked: (ctx) => formatUserLinked(ctx.page.createdBy),
|
|
175
|
+
|
|
176
|
+
// Lifecycle - updated
|
|
177
|
+
updated_at: (ctx) => formatDate(ctx.page.updatedAt),
|
|
178
|
+
updated_by: (ctx) => ctx.page.updatedBy?.name ?? "Anonymous",
|
|
179
|
+
updated_by_unix: (ctx) => ctx.page.updatedBy?.unixName ?? "",
|
|
180
|
+
updated_by_id: (ctx) => String(ctx.page.updatedBy?.id ?? 0),
|
|
181
|
+
updated_by_linked: (ctx) => formatUserLinked(ctx.page.updatedBy),
|
|
182
|
+
|
|
183
|
+
// Lifecycle - commented
|
|
184
|
+
commented_at: (ctx) => (ctx.page.commentedAt ? formatDate(ctx.page.commentedAt) : ""),
|
|
185
|
+
commented_by: (ctx) => ctx.page.commentedBy?.name ?? "",
|
|
186
|
+
commented_by_unix: (ctx) => ctx.page.commentedBy?.unixName ?? "",
|
|
187
|
+
commented_by_id: (ctx) => String(ctx.page.commentedBy?.id ?? 0),
|
|
188
|
+
commented_by_linked: (ctx) => formatUserLinked(ctx.page.commentedBy),
|
|
189
|
+
|
|
190
|
+
// Structure - page
|
|
191
|
+
name: (ctx) => ctx.page.name,
|
|
192
|
+
category: (ctx) => ctx.page.category,
|
|
193
|
+
fullname: (ctx) => ctx.page.fullname,
|
|
194
|
+
title: (ctx) => ctx.page.title,
|
|
195
|
+
title_linked: (ctx) => `[[[${ctx.page.fullname} | ${ctx.page.title}]]]`,
|
|
196
|
+
link: (ctx) => `https://${ctx.site.domain}/${ctx.page.fullname}`,
|
|
197
|
+
|
|
198
|
+
// Structure - parent
|
|
199
|
+
parent_name: (ctx) => ctx.page.parentName ?? "",
|
|
200
|
+
parent_category: (ctx) => ctx.page.parentCategory ?? "",
|
|
201
|
+
parent_fullname: (ctx) => ctx.page.parentFullname ?? "",
|
|
202
|
+
parent_title: (ctx) => ctx.page.parentTitle ?? "",
|
|
203
|
+
parent_title_linked: (ctx) =>
|
|
204
|
+
ctx.page.parentFullname ? `[[[${ctx.page.parentFullname} | ${ctx.page.parentTitle}]]]` : "",
|
|
205
|
+
|
|
206
|
+
// Content
|
|
207
|
+
content: (ctx) => ctx.page.content ?? "",
|
|
208
|
+
preview: (ctx) => (ctx.page.content ?? "").slice(0, DEFAULT_PREVIEW_LENGTH),
|
|
209
|
+
summary: (ctx) => getSummary(ctx.page),
|
|
210
|
+
first_paragraph: (ctx) => getFirstParagraph(ctx.page.content),
|
|
211
|
+
|
|
212
|
+
// Tags
|
|
213
|
+
tags: (ctx) => ctx.page.tags.join(" "),
|
|
214
|
+
_tags: (ctx) => ctx.page.hiddenTags.join(" "),
|
|
215
|
+
|
|
216
|
+
// Metrics
|
|
217
|
+
children: (ctx) => String(ctx.page.children),
|
|
218
|
+
comments: (ctx) => String(ctx.page.comments),
|
|
219
|
+
size: (ctx) => String(ctx.page.size),
|
|
220
|
+
rating: (ctx) => String(ctx.page.rating),
|
|
221
|
+
rating_votes: (ctx) => String(ctx.page.ratingVotes),
|
|
222
|
+
rating_percent: (ctx) => String(ctx.page.ratingPercent ?? 0),
|
|
223
|
+
revisions: (ctx) => String(ctx.page.revisions),
|
|
224
|
+
|
|
225
|
+
// Pagination
|
|
226
|
+
index: (ctx) => String(ctx.index),
|
|
227
|
+
total: (ctx) => String(ctx.total),
|
|
228
|
+
limit: (ctx) => (ctx.limit !== undefined ? String(ctx.limit) : ""),
|
|
229
|
+
total_or_limit: (ctx) =>
|
|
230
|
+
String(ctx.limit !== undefined ? Math.min(ctx.total, ctx.limit) : ctx.total),
|
|
231
|
+
|
|
232
|
+
// Site context
|
|
233
|
+
site_title: (ctx) => ctx.site.title,
|
|
234
|
+
site_name: (ctx) => ctx.site.name,
|
|
235
|
+
site_domain: (ctx) => ctx.site.domain,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// =============================================================================
|
|
239
|
+
// Helper Functions
|
|
240
|
+
// =============================================================================
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Format a Date object using an optional strftime-like format string.
|
|
244
|
+
*
|
|
245
|
+
* When no format is provided, returns the ISO 8601 string representation.
|
|
246
|
+
*
|
|
247
|
+
* @param date - The date to format
|
|
248
|
+
* @param format - Optional strftime format string (e.g., `"%Y-%m-%d"`)
|
|
249
|
+
* @returns Formatted date string
|
|
250
|
+
*/
|
|
251
|
+
function formatDate(date: Date, format?: string): string {
|
|
252
|
+
if (!format) {
|
|
253
|
+
// Default ISO format
|
|
254
|
+
return date.toISOString();
|
|
255
|
+
}
|
|
256
|
+
return strftime(date, format);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Format a user as Wikidot's `[[*user name]]` inline syntax for linked display.
|
|
261
|
+
*
|
|
262
|
+
* @param user - User info, or undefined for anonymous display
|
|
263
|
+
* @returns Wikidot user link syntax, or "Anonymous" if no user
|
|
264
|
+
*/
|
|
265
|
+
function formatUserLinked(user?: UserInfo): string {
|
|
266
|
+
if (!user) return "Anonymous";
|
|
267
|
+
return `[[*user ${user.name}]]`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Format an array of tags as space-separated Wikidot link syntax.
|
|
272
|
+
*
|
|
273
|
+
* Each tag becomes `[prefix/tag tag]` where prefix defaults to
|
|
274
|
+
* `/system:page-tags/tag/` unless overridden by the template's pipe format.
|
|
275
|
+
*
|
|
276
|
+
* @param tags - Array of tag names
|
|
277
|
+
* @param prefix - URL prefix for tag links
|
|
278
|
+
* @returns Space-separated string of Wikidot link syntax, or empty string if no tags
|
|
279
|
+
*/
|
|
280
|
+
function formatTagsLinked(tags: string[], prefix: string): string {
|
|
281
|
+
if (tags.length === 0) return "";
|
|
282
|
+
return tags.map((tag) => `[${prefix}${tag} ${tag}]`).join(" ");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Split content by ==== separators
|
|
287
|
+
*
|
|
288
|
+
* wikidot uses preg_split('/^([=]{4,})$/m', $source) to split content
|
|
289
|
+
* %%content{n}%% references sections[n-1] (1-indexed)
|
|
290
|
+
*/
|
|
291
|
+
function splitContentSections(content: string): string[] {
|
|
292
|
+
return content.split(/^={4,}$/m).map((section) => section.trim());
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Extract a summary from page content.
|
|
297
|
+
*
|
|
298
|
+
* If the content contains `====` section separators, returns the first section
|
|
299
|
+
* (equivalent to `%%content{1}%%`). Otherwise, returns the first paragraph.
|
|
300
|
+
*
|
|
301
|
+
* @param page - Page data containing content
|
|
302
|
+
* @returns Summary text extracted from the page content
|
|
303
|
+
*/
|
|
304
|
+
function getSummary(page: PageData): string {
|
|
305
|
+
if (page.content) {
|
|
306
|
+
const sections = splitContentSections(page.content);
|
|
307
|
+
if (sections.length > 1) {
|
|
308
|
+
return sections[0]?.trim() ?? "";
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
return getFirstParagraph(page.content);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Extract the first paragraph from wikitext content.
|
|
316
|
+
*
|
|
317
|
+
* Strips headings, TOC directives, div blocks, and module blocks before
|
|
318
|
+
* splitting on double newlines to find the first content paragraph.
|
|
319
|
+
*
|
|
320
|
+
* @param content - Raw wikitext content, or undefined
|
|
321
|
+
* @returns The first paragraph text, or empty string if no content
|
|
322
|
+
*/
|
|
323
|
+
function getFirstParagraph(content?: string): string {
|
|
324
|
+
if (!content) return "";
|
|
325
|
+
|
|
326
|
+
// Remove headings, TOC, div, module blocks
|
|
327
|
+
const s = content
|
|
328
|
+
.replace(/^(\+{1,6}) (.*)/gm, "")
|
|
329
|
+
.replace(/^\[\[toc(\s[^\]]+)?\]\]/gim, "")
|
|
330
|
+
.replace(/^\[\[\/?div(\s[^\]]+)?\]\]/gim, "")
|
|
331
|
+
.replace(/^\[\[\/?module(\s[^\]]+)?\]\]/gim, "")
|
|
332
|
+
.trim();
|
|
333
|
+
|
|
334
|
+
// Split by double newlines and take first
|
|
335
|
+
const paragraphs = s.split(/\n{2,}/);
|
|
336
|
+
return paragraphs[0]?.trim() ?? "";
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** Full month names for strftime `%B` token. Static to avoid per-call allocation. */
|
|
340
|
+
const MONTHS = [
|
|
341
|
+
"January",
|
|
342
|
+
"February",
|
|
343
|
+
"March",
|
|
344
|
+
"April",
|
|
345
|
+
"May",
|
|
346
|
+
"June",
|
|
347
|
+
"July",
|
|
348
|
+
"August",
|
|
349
|
+
"September",
|
|
350
|
+
"October",
|
|
351
|
+
"November",
|
|
352
|
+
"December",
|
|
353
|
+
];
|
|
354
|
+
/** Abbreviated month names for strftime `%b` token. */
|
|
355
|
+
const MONTHS_SHORT = [
|
|
356
|
+
"Jan",
|
|
357
|
+
"Feb",
|
|
358
|
+
"Mar",
|
|
359
|
+
"Apr",
|
|
360
|
+
"May",
|
|
361
|
+
"Jun",
|
|
362
|
+
"Jul",
|
|
363
|
+
"Aug",
|
|
364
|
+
"Sep",
|
|
365
|
+
"Oct",
|
|
366
|
+
"Nov",
|
|
367
|
+
"Dec",
|
|
368
|
+
];
|
|
369
|
+
/** Full day names for strftime `%A` token. */
|
|
370
|
+
const DAYS = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
371
|
+
/** Abbreviated day names for strftime `%a` token. */
|
|
372
|
+
const DAYS_SHORT = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
|
373
|
+
|
|
374
|
+
/** Pre-compiled regex matching strftime tokens (`%X` where X is a supported letter or `%`). */
|
|
375
|
+
const STRFTIME_REGEX = /%([YymdHeHIMSpbBaAwjZz%])/g;
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Minimal strftime implementation supporting common tokens.
|
|
379
|
+
*
|
|
380
|
+
* Uses a single-pass regex replace for O(n) performance. All dates are
|
|
381
|
+
* treated as UTC to match Wikidot's server-side rendering behavior.
|
|
382
|
+
*
|
|
383
|
+
* Supported tokens: `%Y` (4-digit year), `%y` (2-digit year), `%m` (month),
|
|
384
|
+
* `%d` (zero-padded day), `%e` (day), `%H` (24h hour), `%I` (12h hour),
|
|
385
|
+
* `%M` (minute), `%S` (second), `%p` (AM/PM), `%b`/`%B` (month name),
|
|
386
|
+
* `%a`/`%A` (day name), `%w` (weekday number), `%j` (day of year),
|
|
387
|
+
* `%Z`/`%z` (timezone), `%%` (literal percent).
|
|
388
|
+
*
|
|
389
|
+
* @param date - The date to format
|
|
390
|
+
* @param format - strftime format string
|
|
391
|
+
* @returns Formatted date string
|
|
392
|
+
*/
|
|
393
|
+
function strftime(date: Date, format: string): string {
|
|
394
|
+
const pad = (n: number, len = 2) => String(n).padStart(len, "0");
|
|
395
|
+
|
|
396
|
+
return format.replace(STRFTIME_REGEX, (_, token: string) => {
|
|
397
|
+
switch (token) {
|
|
398
|
+
case "Y":
|
|
399
|
+
return String(date.getFullYear());
|
|
400
|
+
case "y":
|
|
401
|
+
return String(date.getFullYear()).slice(-2);
|
|
402
|
+
case "m":
|
|
403
|
+
return pad(date.getMonth() + 1);
|
|
404
|
+
case "d":
|
|
405
|
+
return pad(date.getDate());
|
|
406
|
+
case "e":
|
|
407
|
+
return String(date.getDate());
|
|
408
|
+
case "H":
|
|
409
|
+
return pad(date.getHours());
|
|
410
|
+
case "I":
|
|
411
|
+
return pad(date.getHours() % 12 || 12);
|
|
412
|
+
case "M":
|
|
413
|
+
return pad(date.getMinutes());
|
|
414
|
+
case "S":
|
|
415
|
+
return pad(date.getSeconds());
|
|
416
|
+
case "p":
|
|
417
|
+
return date.getHours() < 12 ? "AM" : "PM";
|
|
418
|
+
case "b":
|
|
419
|
+
return MONTHS_SHORT[date.getMonth()] ?? "";
|
|
420
|
+
case "B":
|
|
421
|
+
return MONTHS[date.getMonth()] ?? "";
|
|
422
|
+
case "a":
|
|
423
|
+
return DAYS_SHORT[date.getDay()] ?? "";
|
|
424
|
+
case "A":
|
|
425
|
+
return DAYS[date.getDay()] ?? "";
|
|
426
|
+
case "w":
|
|
427
|
+
return String(date.getDay());
|
|
428
|
+
case "j":
|
|
429
|
+
return pad(getDayOfYear(date), 3);
|
|
430
|
+
case "Z":
|
|
431
|
+
return "UTC";
|
|
432
|
+
case "z":
|
|
433
|
+
return "+0000";
|
|
434
|
+
case "%":
|
|
435
|
+
return "%";
|
|
436
|
+
default:
|
|
437
|
+
return `%${token}`;
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Calculate the day of year (1-366) for a given date.
|
|
444
|
+
*
|
|
445
|
+
* @param date - The date to calculate for
|
|
446
|
+
* @returns Day of year as an integer (1 = January 1st)
|
|
447
|
+
*/
|
|
448
|
+
function getDayOfYear(date: Date): number {
|
|
449
|
+
const start = new Date(date.getFullYear(), 0, 0);
|
|
450
|
+
const diff = date.getTime() - start.getTime();
|
|
451
|
+
const oneDay = 1000 * 60 * 60 * 24;
|
|
452
|
+
return Math.floor(diff / oneDay);
|
|
453
|
+
}
|