@wdprlib/render 2.1.0 → 3.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 +2344 -1668
- package/dist/index.d.cts +15 -13
- package/dist/index.d.ts +15 -13
- package/dist/index.js +2375 -1699
- package/package.json +1 -1
- package/src/context/attributes.ts +14 -0
- package/src/context/bibliography.ts +109 -0
- package/src/context/counters.ts +51 -0
- package/src/context/image-urls.ts +31 -0
- package/src/context/index.ts +285 -0
- package/src/context/output.ts +17 -0
- package/src/context/page-urls.ts +81 -0
- package/src/context/style-slots.ts +29 -0
- package/src/context/urls.ts +2 -0
- package/src/elements/bibliography/block.ts +27 -0
- package/src/elements/bibliography/cite.ts +23 -0
- package/src/elements/bibliography/ids.ts +9 -0
- package/src/elements/bibliography/index.ts +9 -0
- package/src/elements/code/contents.ts +18 -0
- package/src/elements/code/index.ts +29 -0
- package/src/elements/collapsible/index.ts +31 -0
- package/src/elements/collapsible/labels.ts +35 -0
- package/src/elements/collapsible/link.ts +11 -0
- package/src/elements/collapsible/sections.ts +39 -0
- package/src/elements/container/attributes.ts +28 -0
- package/src/elements/container/header.ts +27 -0
- package/src/elements/container/index.ts +35 -0
- package/src/elements/container/string-container.ts +40 -0
- package/src/elements/container/string-types.ts +63 -0
- package/src/elements/container/wrappers.ts +32 -0
- package/src/elements/date/format.ts +20 -0
- package/src/elements/{date.ts → date/index.ts} +4 -29
- package/src/elements/date/output.ts +6 -0
- package/src/elements/embed/iframe.ts +8 -0
- package/src/elements/embed/index.ts +28 -0
- package/src/elements/embed/providers.ts +43 -0
- package/src/elements/embed/validation.ts +15 -0
- package/src/elements/embed-block/allowlist.ts +60 -0
- package/src/elements/embed-block/boolean-attributes.ts +38 -0
- package/src/elements/embed-block/iframe.ts +33 -0
- package/src/elements/embed-block/index.ts +31 -0
- package/src/elements/embed-block/sanitize-config.ts +22 -0
- package/src/elements/embed-block/sanitize.ts +44 -0
- package/src/elements/expr/branch.ts +29 -0
- package/src/elements/expr/index.ts +63 -0
- package/src/elements/expr/result.ts +19 -0
- package/src/elements/footnote/body.ts +11 -0
- package/src/elements/footnote/index.ts +35 -0
- package/src/elements/footnote/ref.ts +16 -0
- package/src/elements/html/attributes.ts +24 -0
- package/src/elements/html/index.ts +39 -0
- package/src/elements/html/url.ts +19 -0
- package/src/elements/iframe/attributes.ts +28 -0
- package/src/elements/iframe/index.ts +22 -0
- package/src/elements/iftags/condition.ts +42 -0
- package/src/elements/iftags/index.ts +39 -0
- package/src/elements/iftags/style-slot.ts +23 -0
- package/src/elements/iftags/tokens.ts +36 -0
- package/src/elements/image/alignment.ts +44 -0
- package/src/elements/image/attributes.ts +10 -0
- package/src/elements/image/img-attributes.ts +26 -0
- package/src/elements/image/index.ts +36 -0
- package/src/elements/image/link-href.ts +24 -0
- package/src/elements/image/link.ts +13 -0
- package/src/elements/image/source.ts +16 -0
- package/src/elements/{include.ts → include/index.ts} +5 -13
- package/src/elements/include/missing.ts +15 -0
- package/src/elements/link/anchor-name.ts +6 -0
- package/src/elements/link/anchor.ts +27 -0
- package/src/elements/link/attributes.ts +47 -0
- package/src/elements/link/index.ts +26 -0
- package/src/elements/link/label.ts +23 -0
- package/src/elements/link/target.ts +20 -0
- package/src/elements/list/attributes.ts +19 -0
- package/src/elements/list/definition-list.ts +16 -0
- package/src/elements/list/index.ts +48 -0
- package/src/elements/list/item-rendering.ts +38 -0
- package/src/elements/list/items.ts +61 -0
- package/src/elements/list/no-marker.ts +53 -0
- package/src/elements/list/paragraphs.ts +34 -0
- package/src/elements/list/trim.ts +38 -0
- package/src/elements/math/block.ts +29 -0
- package/src/elements/math/equation-ref.ts +12 -0
- package/src/elements/math/index.ts +14 -0
- package/src/elements/math/inline.ts +19 -0
- package/src/elements/math/latex.ts +27 -0
- package/src/elements/math/source.ts +18 -0
- package/src/elements/module/backlinks.ts +2 -1
- package/src/elements/module/categories.ts +2 -2
- package/src/elements/module/empty-container.ts +10 -0
- package/src/elements/module/index.ts +2 -4
- package/src/elements/module/join-markup.ts +10 -0
- package/src/elements/module/join.ts +2 -7
- package/src/elements/module/listpages.ts +2 -2
- package/src/elements/module/listusers.ts +2 -2
- package/src/elements/module/page-tree.ts +2 -2
- package/src/elements/module/rate-markup.ts +10 -0
- package/src/elements/module/rate.ts +4 -13
- package/src/elements/module/unknown.ts +11 -0
- package/src/elements/tab-view/ids.ts +16 -0
- package/src/elements/tab-view/index.ts +31 -0
- package/src/elements/tab-view/navigation.ts +15 -0
- package/src/elements/tab-view/panels.ts +16 -0
- package/src/elements/table/attributes.ts +23 -0
- package/src/elements/table/cell-attributes.ts +62 -0
- package/src/elements/table/cell.ts +13 -0
- package/src/elements/table/index.ts +27 -0
- package/src/elements/text/email.ts +20 -0
- package/src/elements/text/index.ts +11 -0
- package/src/elements/text/plain.ts +11 -0
- package/src/elements/text/raw.ts +20 -0
- package/src/elements/toc/body.ts +12 -0
- package/src/elements/toc/entries.ts +34 -0
- package/src/elements/toc/frame.ts +27 -0
- package/src/elements/toc/index.ts +17 -0
- package/src/elements/toc/link.ts +26 -0
- package/src/elements/user/index.ts +40 -0
- package/src/elements/user/markup.ts +34 -0
- package/src/elements/user/resolve.ts +6 -0
- package/src/escape/attribute-allowlists.ts +101 -0
- package/src/escape/attributes.ts +62 -0
- package/src/escape/css-color-functions.ts +18 -0
- package/src/escape/css-colors.ts +183 -0
- package/src/escape/css-danger.ts +22 -0
- package/src/escape/css-normalize.ts +54 -0
- package/src/escape/css-style.ts +78 -0
- package/src/escape/css-urls.ts +76 -0
- package/src/escape/css.ts +4 -0
- package/src/escape/email.ts +22 -0
- package/src/escape/html.ts +68 -0
- package/src/escape/index.ts +15 -0
- package/src/escape/url.ts +18 -0
- package/src/libs/highlighter/engine/end-pattern.ts +26 -0
- package/src/libs/highlighter/engine/html.ts +19 -0
- package/src/libs/highlighter/engine/index.ts +3 -0
- package/src/libs/highlighter/engine/keywords.ts +22 -0
- package/src/libs/highlighter/engine/parts.ts +36 -0
- package/src/libs/highlighter/engine/preprocess.ts +10 -0
- package/src/libs/highlighter/engine/render.ts +31 -0
- package/src/libs/highlighter/engine/token.ts +7 -0
- package/src/libs/highlighter/engine/tokenizer.ts +266 -0
- package/src/libs/highlighter/engine/utils.ts +38 -0
- package/src/render/collected-styles.ts +22 -0
- package/src/render/dispatch.ts +181 -0
- package/src/render/index.ts +28 -0
- package/src/render/primitives.ts +17 -0
- package/src/render/style-tag.ts +6 -0
- package/src/render/style.ts +15 -0
- package/src/types.ts +6 -2
- package/src/context.ts +0 -422
- package/src/elements/bibliography.ts +0 -123
- package/src/elements/code.ts +0 -49
- package/src/elements/collapsible.ts +0 -105
- package/src/elements/container.ts +0 -302
- package/src/elements/embed-block.ts +0 -327
- package/src/elements/embed.ts +0 -166
- package/src/elements/expr.ts +0 -102
- package/src/elements/footnote.ts +0 -76
- package/src/elements/html.ts +0 -79
- package/src/elements/iframe.ts +0 -44
- package/src/elements/iftags.ts +0 -118
- package/src/elements/image.ts +0 -154
- package/src/elements/link.ts +0 -201
- package/src/elements/list.ts +0 -241
- package/src/elements/math.ts +0 -177
- package/src/elements/tab-view.ts +0 -75
- package/src/elements/table.ts +0 -101
- package/src/elements/text.ts +0 -57
- package/src/elements/toc.ts +0 -147
- package/src/elements/user.ts +0 -79
- package/src/escape.ts +0 -829
- package/src/libs/highlighter/engine.ts +0 -352
- package/src/render.ts +0 -231
package/src/elements/embed.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* Renderer for inline embed elements (`[[embed]]`) that reference
|
|
4
|
-
* third-party content providers.
|
|
5
|
-
*
|
|
6
|
-
* Supported providers:
|
|
7
|
-
* - YouTube (`[[embedvideo youtube:VIDEO_ID]]`)
|
|
8
|
-
* - Vimeo (`[[embedvideo vimeo:VIDEO_ID]]`)
|
|
9
|
-
* - GitHub Gist (`[[embed github-gist:USER/HASH]]`)
|
|
10
|
-
* - GitLab Snippet (`[[embed gitlab-snippet:ID]]`)
|
|
11
|
-
*
|
|
12
|
-
* Each provider has a strict ID validation function to prevent path
|
|
13
|
-
* traversal, injection, and other attacks via embed parameters.
|
|
14
|
-
*
|
|
15
|
-
* @module
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import type { Embed } from "@wdprlib/ast";
|
|
19
|
-
import type { RenderContext } from "../context";
|
|
20
|
-
import { escapeAttr } from "../escape";
|
|
21
|
-
|
|
22
|
-
// =============================================================================
|
|
23
|
-
// ID Validation
|
|
24
|
-
// Prevents path traversal and injection via embed parameters
|
|
25
|
-
// =============================================================================
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Validate a YouTube or Vimeo video ID.
|
|
29
|
-
* Only alphanumeric characters, underscores, and hyphens are allowed.
|
|
30
|
-
*
|
|
31
|
-
* @param id - The video ID string to validate.
|
|
32
|
-
* @returns `true` if the ID contains only safe characters.
|
|
33
|
-
*/
|
|
34
|
-
function isValidVideoId(id: string): boolean {
|
|
35
|
-
return /^[a-zA-Z0-9_-]+$/.test(id);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Validate a GitHub username (alphanumeric + hyphen, 1-39 characters).
|
|
40
|
-
*
|
|
41
|
-
* @param username - The GitHub username to validate.
|
|
42
|
-
* @returns `true` if the username matches GitHub's format rules.
|
|
43
|
-
*/
|
|
44
|
-
function isValidGithubUsername(username: string): boolean {
|
|
45
|
-
return /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/.test(username);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Validate a GitHub Gist hash (lowercase hex characters only).
|
|
50
|
-
*
|
|
51
|
-
* @param hash - The gist hash string to validate.
|
|
52
|
-
* @returns `true` if the hash contains only hex characters.
|
|
53
|
-
*/
|
|
54
|
-
function isValidGistHash(hash: string): boolean {
|
|
55
|
-
return /^[a-f0-9]+$/.test(hash);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Validate a GitLab snippet ID (numeric digits only).
|
|
60
|
-
*
|
|
61
|
-
* @param id - The snippet ID string to validate.
|
|
62
|
-
* @returns `true` if the ID is numeric.
|
|
63
|
-
*/
|
|
64
|
-
function isValidGitlabSnippetId(id: string): boolean {
|
|
65
|
-
return /^[0-9]+$/.test(id);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// =============================================================================
|
|
69
|
-
// Render Functions
|
|
70
|
-
// =============================================================================
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Render an inline embed element by dispatching to the appropriate
|
|
74
|
-
* provider-specific renderer.
|
|
75
|
-
*
|
|
76
|
-
* Invalid provider parameters (e.g. a video ID containing path traversal
|
|
77
|
-
* characters) result in an HTML comment instead of the embed.
|
|
78
|
-
*
|
|
79
|
-
* @param ctx - The current render context.
|
|
80
|
-
* @param data - Embed data with provider type and provider-specific fields.
|
|
81
|
-
*/
|
|
82
|
-
export function renderEmbed(ctx: RenderContext, data: Embed): void {
|
|
83
|
-
switch (data.embed) {
|
|
84
|
-
case "youtube":
|
|
85
|
-
renderYoutube(ctx, data.data["video-id"]);
|
|
86
|
-
break;
|
|
87
|
-
case "vimeo":
|
|
88
|
-
renderVimeo(ctx, data.data["video-id"]);
|
|
89
|
-
break;
|
|
90
|
-
case "github-gist":
|
|
91
|
-
renderGithubGist(ctx, data.data.username, data.data.hash);
|
|
92
|
-
break;
|
|
93
|
-
case "gitlab-snippet":
|
|
94
|
-
renderGitlabSnippet(ctx, data.data["snippet-id"]);
|
|
95
|
-
break;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Render a YouTube embed as a responsive iframe.
|
|
101
|
-
*
|
|
102
|
-
* @param ctx - The current render context.
|
|
103
|
-
* @param videoId - YouTube video ID (validated before use).
|
|
104
|
-
*/
|
|
105
|
-
function renderYoutube(ctx: RenderContext, videoId: string): void {
|
|
106
|
-
if (!isValidVideoId(videoId)) {
|
|
107
|
-
ctx.push(`<!-- Invalid YouTube video ID -->`);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
ctx.push(`<div class="embed-youtube">`);
|
|
111
|
-
ctx.push(
|
|
112
|
-
`<iframe src="https://www.youtube.com/embed/${escapeAttr(videoId)}" ` +
|
|
113
|
-
`frameborder="0" allowfullscreen></iframe>`,
|
|
114
|
-
);
|
|
115
|
-
ctx.push("</div>");
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Render a Vimeo embed as a responsive iframe.
|
|
120
|
-
*
|
|
121
|
-
* @param ctx - The current render context.
|
|
122
|
-
* @param videoId - Vimeo video ID (validated before use).
|
|
123
|
-
*/
|
|
124
|
-
function renderVimeo(ctx: RenderContext, videoId: string): void {
|
|
125
|
-
if (!isValidVideoId(videoId)) {
|
|
126
|
-
ctx.push(`<!-- Invalid Vimeo video ID -->`);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
ctx.push(`<div class="embed-vimeo">`);
|
|
130
|
-
ctx.push(
|
|
131
|
-
`<iframe src="https://player.vimeo.com/video/${escapeAttr(videoId)}" ` +
|
|
132
|
-
`frameborder="0" allowfullscreen></iframe>`,
|
|
133
|
-
);
|
|
134
|
-
ctx.push("</div>");
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Render a GitHub Gist embed as a `<script>` tag.
|
|
139
|
-
*
|
|
140
|
-
* @param ctx - The current render context.
|
|
141
|
-
* @param username - GitHub username owning the gist (validated before use).
|
|
142
|
-
* @param hash - Gist hash identifier (validated before use).
|
|
143
|
-
*/
|
|
144
|
-
function renderGithubGist(ctx: RenderContext, username: string, hash: string): void {
|
|
145
|
-
if (!isValidGithubUsername(username) || !isValidGistHash(hash)) {
|
|
146
|
-
ctx.push(`<!-- Invalid GitHub Gist parameters -->`);
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
ctx.push(
|
|
150
|
-
`<script src="https://gist.github.com/${escapeAttr(username)}/${escapeAttr(hash)}.js"></script>`,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Render a GitLab Snippet embed as a `<script>` tag.
|
|
156
|
-
*
|
|
157
|
-
* @param ctx - The current render context.
|
|
158
|
-
* @param snippetId - GitLab snippet ID (validated before use).
|
|
159
|
-
*/
|
|
160
|
-
function renderGitlabSnippet(ctx: RenderContext, snippetId: string): void {
|
|
161
|
-
if (!isValidGitlabSnippetId(snippetId)) {
|
|
162
|
-
ctx.push(`<!-- Invalid GitLab snippet ID -->`);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
ctx.push(`<script src="https://gitlab.com/snippets/${escapeAttr(snippetId)}.js"></script>`);
|
|
166
|
-
}
|
package/src/elements/expr.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* Renderers for Wikidot's expression and conditional constructs:
|
|
4
|
-
*
|
|
5
|
-
* - `[[#expr EXPRESSION]]` -- evaluate a mathematical expression and
|
|
6
|
-
* display the numeric result.
|
|
7
|
-
* - `[[#if VALUE | THEN | ELSE]]` -- simple string-based truthiness check.
|
|
8
|
-
* - `[[#ifexpr EXPRESSION | THEN | ELSE]]` -- evaluate a math expression
|
|
9
|
-
* and branch on the numeric result (0 = false, non-zero = true).
|
|
10
|
-
*
|
|
11
|
-
* All error messages match Wikidot's format (`"run-time error: ..."`).
|
|
12
|
-
*
|
|
13
|
-
* @module
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type { Element, ExprData, IfCondData, IfExprData } from "@wdprlib/ast";
|
|
17
|
-
import type { RenderContext } from "../context";
|
|
18
|
-
import { renderElements } from "../render";
|
|
19
|
-
import { evaluateExpression, formatExprValue, isTruthy } from "@wdprlib/ast";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Render a `[[#expr]]` element.
|
|
23
|
-
*
|
|
24
|
-
* Evaluates the mathematical expression and outputs the formatted numeric
|
|
25
|
-
* result. On evaluation error, a Wikidot-compatible error message is
|
|
26
|
-
* displayed. Empty expressions produce no output.
|
|
27
|
-
*
|
|
28
|
-
* @param ctx - The current render context.
|
|
29
|
-
* @param data - Expression data containing the expression string.
|
|
30
|
-
*/
|
|
31
|
-
export function renderExpr(ctx: RenderContext, data: ExprData): void {
|
|
32
|
-
const result = evaluateExpression(data.expression);
|
|
33
|
-
if (result.success) {
|
|
34
|
-
ctx.pushEscaped(formatExprValue(result.value));
|
|
35
|
-
} else if (result.error !== "empty expression") {
|
|
36
|
-
ctx.pushEscaped(`run-time error: ${result.error}`);
|
|
37
|
-
}
|
|
38
|
-
// Empty expression outputs nothing
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Render a `[[#if]]` conditional element.
|
|
43
|
-
*
|
|
44
|
-
* The condition is treated as a string: values `"false"`, `"null"`,
|
|
45
|
-
* `""`, and `"0"` are falsy; everything else is truthy. The selected
|
|
46
|
-
* branch's elements are rendered with trailing whitespace trimmed.
|
|
47
|
-
*
|
|
48
|
-
* @param ctx - The current render context.
|
|
49
|
-
* @param data - If-condition data with condition string and then/else branches.
|
|
50
|
-
*/
|
|
51
|
-
export function renderIf(ctx: RenderContext, data: IfCondData): void {
|
|
52
|
-
const elements = isTruthy(data.condition) ? data.then : data.else;
|
|
53
|
-
renderBranchElements(ctx, elements);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Render a `[[#ifexpr]]` conditional expression element.
|
|
58
|
-
*
|
|
59
|
-
* Evaluates the mathematical expression; a result of 0 selects the
|
|
60
|
-
* `else` branch, any non-zero result selects the `then` branch.
|
|
61
|
-
* On evaluation error, a Wikidot-compatible error message is displayed
|
|
62
|
-
* and neither branch is rendered.
|
|
63
|
-
*
|
|
64
|
-
* @param ctx - The current render context.
|
|
65
|
-
* @param data - If-expression data with expression string and then/else branches.
|
|
66
|
-
*/
|
|
67
|
-
export function renderIfExpr(ctx: RenderContext, data: IfExprData): void {
|
|
68
|
-
const result = evaluateExpression(data.expression);
|
|
69
|
-
if (!result.success) {
|
|
70
|
-
// ifexpr: error outputs error message (Wikidot-compatible)
|
|
71
|
-
ctx.pushEscaped(`run-time error: ${result.error}`);
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
// 0 selects else branch, non-zero selects then branch
|
|
75
|
-
const elements = result.value !== 0 ? data.then : data.else;
|
|
76
|
-
renderBranchElements(ctx, elements);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Render a branch's elements, trimming trailing whitespace-only text nodes.
|
|
81
|
-
*
|
|
82
|
-
* Wikidot strips trailing whitespace from `#if` / `#ifexpr` branch output.
|
|
83
|
-
* This function finds the last non-whitespace element and renders only
|
|
84
|
-
* up to that point.
|
|
85
|
-
*
|
|
86
|
-
* @param ctx - The current render context.
|
|
87
|
-
* @param elements - The branch's element array.
|
|
88
|
-
*/
|
|
89
|
-
function renderBranchElements(ctx: RenderContext, elements: Element[]): void {
|
|
90
|
-
// Find the last non-whitespace element
|
|
91
|
-
let lastIdx = elements.length - 1;
|
|
92
|
-
while (lastIdx >= 0) {
|
|
93
|
-
const el = elements[lastIdx]!;
|
|
94
|
-
if (el.element === "text" && typeof el.data === "string" && el.data.trim() === "") {
|
|
95
|
-
lastIdx--;
|
|
96
|
-
} else {
|
|
97
|
-
break;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
// Render only up to the last non-whitespace element
|
|
101
|
-
renderElements(ctx, elements.slice(0, lastIdx + 1));
|
|
102
|
-
}
|
package/src/elements/footnote.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* Renderers for Wikidot footnote markup.
|
|
4
|
-
*
|
|
5
|
-
* - `[[footnote]]...[[/footnote]]` -- inline footnote reference that renders
|
|
6
|
-
* as a superscript number linking to the footnote body.
|
|
7
|
-
* - `[[footnoteblock]]` -- block element that lists all footnote bodies
|
|
8
|
-
* collected during the render pass.
|
|
9
|
-
*
|
|
10
|
-
* The runtime `footnote` module adds hover tooltips and click-to-scroll
|
|
11
|
-
* behavior to these elements.
|
|
12
|
-
*
|
|
13
|
-
* @module
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type { FootnoteBlockData } from "@wdprlib/ast";
|
|
17
|
-
import type { RenderContext } from "../context";
|
|
18
|
-
import { escapeHtml } from "../escape";
|
|
19
|
-
import { renderElements } from "../render";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Render an inline footnote reference as a superscript link.
|
|
23
|
-
*
|
|
24
|
-
* Produces `<sup class="footnoteref"><a id="footnoteref-N" ...>N</a></sup>`.
|
|
25
|
-
* The ID is used by the runtime module for bidirectional scroll navigation
|
|
26
|
-
* between the reference and its footnote body.
|
|
27
|
-
*
|
|
28
|
-
* @param ctx - The current render context.
|
|
29
|
-
* @param index - The 1-based footnote number.
|
|
30
|
-
*/
|
|
31
|
-
export function renderFootnoteRef(ctx: RenderContext, index: number): void {
|
|
32
|
-
const id = ctx.generateId("footnoteref-", index);
|
|
33
|
-
ctx.push(`<sup class="footnoteref">`);
|
|
34
|
-
ctx.push(`<a id="${id}" href="javascript:;" class="footnoteref">${index}</a>`);
|
|
35
|
-
ctx.push("</sup>");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Render a `[[footnoteblock]]` element that lists all footnote bodies.
|
|
40
|
-
*
|
|
41
|
-
* Produces a Wikidot-compatible structure:
|
|
42
|
-
* ```html
|
|
43
|
-
* <div class="footnotes-footer">
|
|
44
|
-
* <div class="title">Footnotes</div>
|
|
45
|
-
* <div class="footnote-footer" id="footnote-1">
|
|
46
|
-
* <a href="javascript:;">1</a>. ...content...
|
|
47
|
-
* </div>
|
|
48
|
-
* </div>
|
|
49
|
-
* ```
|
|
50
|
-
*
|
|
51
|
-
* If there are no footnotes, the block is not rendered at all.
|
|
52
|
-
*
|
|
53
|
-
* @param ctx - The current render context.
|
|
54
|
-
* @param data - Footnote block data with optional custom title.
|
|
55
|
-
*/
|
|
56
|
-
export function renderFootnoteBlock(ctx: RenderContext, data: FootnoteBlockData): void {
|
|
57
|
-
if (ctx.footnotes.length === 0) return;
|
|
58
|
-
const title = data.title ?? "Footnotes";
|
|
59
|
-
|
|
60
|
-
ctx.push(`<div class="footnotes-footer">`);
|
|
61
|
-
ctx.push(`<div class="title">${escapeHtml(title)}</div>`);
|
|
62
|
-
|
|
63
|
-
// Render each footnote
|
|
64
|
-
for (let i = 0; i < ctx.footnotes.length; i++) {
|
|
65
|
-
const index = i + 1;
|
|
66
|
-
const elements = ctx.footnotes[i] ?? [];
|
|
67
|
-
|
|
68
|
-
const fnId = ctx.generateId("footnote-", index);
|
|
69
|
-
ctx.push(`<div class="footnote-footer" id="${fnId}">`);
|
|
70
|
-
ctx.push(`<a href="javascript:;">${index}</a>. `);
|
|
71
|
-
renderElements(ctx, elements);
|
|
72
|
-
ctx.push("</div>");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
ctx.push("</div>");
|
|
76
|
-
}
|
package/src/elements/html.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* Renderer for `[[html]]...[[/html]]` blocks in Wikidot markup.
|
|
4
|
-
*
|
|
5
|
-
* HTML blocks are rendered as sandboxed iframes. The iframe `src` URL
|
|
6
|
-
* can be provided by a resolver callback; when absent, a deterministic
|
|
7
|
-
* default URL is generated from the content hash.
|
|
8
|
-
*
|
|
9
|
-
* The actual HTML content is not inlined into the page -- it is served
|
|
10
|
-
* separately at the iframe URL. The runtime `html-block` module handles
|
|
11
|
-
* auto-resizing of the iframe via `postMessage`.
|
|
12
|
-
*
|
|
13
|
-
* @module
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type { HtmlData } from "@wdprlib/ast";
|
|
17
|
-
import type { RenderContext } from "../context";
|
|
18
|
-
import { escapeAttr, sanitizeStyleValue } from "../escape";
|
|
19
|
-
import { syncHashSha1 } from "../hash";
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Generate a default iframe `src` URL for an HTML block when no
|
|
23
|
-
* resolver callback is provided.
|
|
24
|
-
*
|
|
25
|
-
* The URL follows Wikidot's pattern: `/{pageName}/html/{hash}-{nonce}`.
|
|
26
|
-
* The hash is a SHA-1-length FNV hash of the content, and the nonce
|
|
27
|
-
* is derived from the content length and first hash character to
|
|
28
|
-
* provide additional uniqueness.
|
|
29
|
-
*
|
|
30
|
-
* @param pageName - The current page name (may be empty).
|
|
31
|
-
* @param contents - Raw HTML content to hash.
|
|
32
|
-
* @returns A URL path string, always starting with `/`.
|
|
33
|
-
*/
|
|
34
|
-
function generateDefaultUrl(pageName: string, contents: string): string {
|
|
35
|
-
const hash = syncHashSha1(contents);
|
|
36
|
-
const nonce = BigInt(contents.length) * 1000000000n + BigInt(hash.charCodeAt(0)) * 100000000n;
|
|
37
|
-
// Ensure leading slash even when pageName is empty (avoid protocol-relative URL)
|
|
38
|
-
const path = pageName ? `/${pageName}/html/${hash}-${nonce}` : `/html/${hash}-${nonce}`;
|
|
39
|
-
return path;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Render a `[[html]]` block as an iframe wrapped in a `<p>` element.
|
|
44
|
-
*
|
|
45
|
-
* The iframe uses `class="html-block-iframe"` so the runtime can
|
|
46
|
-
* identify it for auto-resize. An optional `sandbox` attribute and
|
|
47
|
-
* custom `style` attribute (from `[[html style="..."]]`) are applied.
|
|
48
|
-
*
|
|
49
|
-
* @param ctx - The current render context.
|
|
50
|
-
* @param data - HTML block data containing the raw HTML contents and optional style.
|
|
51
|
-
*/
|
|
52
|
-
export function renderHtmlBlock(ctx: RenderContext, data: HtmlData): void {
|
|
53
|
-
// Settings-level enforcement boundary: skip rendering entirely when
|
|
54
|
-
// `[[html]]` is disabled. Placed before any counter advance or
|
|
55
|
-
// resolver invocation so a disabled-but-still-in-AST block (manually
|
|
56
|
-
// built trees, cached ASTs, foreign parsers) has no observable effect.
|
|
57
|
-
if (ctx.settings.allowHtmlBlocks === false) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const index = ctx.nextHtmlBlockIndex();
|
|
62
|
-
const pageName = ctx.page?.pageName ?? "";
|
|
63
|
-
|
|
64
|
-
// Use callback URL if provided, otherwise generate default URL
|
|
65
|
-
const callbackUrl = ctx.options.resolvers?.htmlBlockUrl?.(index);
|
|
66
|
-
const src = callbackUrl || generateDefaultUrl(pageName, data.contents);
|
|
67
|
-
|
|
68
|
-
// Build sandbox attribute (null/undefined = no sandbox, Wikidot compatible)
|
|
69
|
-
const sandbox = ctx.options.htmlBlockSandbox;
|
|
70
|
-
const sandboxAttr = sandbox ? ` sandbox="${escapeAttr(sandbox)}"` : "";
|
|
71
|
-
|
|
72
|
-
// Build style attribute (from [[html style="..."]]) with CSS injection protection
|
|
73
|
-
const styleAttr = data.style ? ` style="${escapeAttr(sanitizeStyleValue(data.style))}"` : "";
|
|
74
|
-
|
|
75
|
-
// Wikidot wraps html block iframe in a paragraph
|
|
76
|
-
ctx.push(
|
|
77
|
-
`<p><iframe src="${escapeAttr(src)}"${sandboxAttr} allowtransparency="true" frameborder="0" class="html-block-iframe"${styleAttr}></iframe></p>`,
|
|
78
|
-
);
|
|
79
|
-
}
|
package/src/elements/iframe.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* Renderer for `[[iframe URL]]` inline iframe elements.
|
|
4
|
-
*
|
|
5
|
-
* Unlike `[[embed]]` blocks, `[[iframe]]` directly references a URL.
|
|
6
|
-
* The URL is checked for dangerous schemes, and standard iframe
|
|
7
|
-
* attributes (align, frameborder, height, etc.) are extracted from the
|
|
8
|
-
* AST's attribute map and rendered with proper escaping.
|
|
9
|
-
*
|
|
10
|
-
* @module
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import type { IframeData } from "@wdprlib/ast";
|
|
14
|
-
import type { RenderContext } from "../context";
|
|
15
|
-
import { escapeAttr, isDangerousUrl, sanitizeStyleValue } from "../escape";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Render an `[[iframe URL]]` element.
|
|
19
|
-
*
|
|
20
|
-
* The iframe is wrapped in a `<p>` element to match Wikidot's output.
|
|
21
|
-
* Standard iframe attributes are rendered from the AST's attribute map,
|
|
22
|
-
* with the `style` attribute sanitized against CSS injection. Dangerous
|
|
23
|
-
* URL schemes are replaced with `#invalid-url`.
|
|
24
|
-
*
|
|
25
|
-
* @param ctx - The current render context.
|
|
26
|
-
* @param data - Iframe data containing the URL and attribute map.
|
|
27
|
-
*/
|
|
28
|
-
export function renderIframe(ctx: RenderContext, data: IframeData): void {
|
|
29
|
-
const url = isDangerousUrl(data.url) ? "#invalid-url" : data.url;
|
|
30
|
-
const attrs: string[] = [`src="${escapeAttr(url)}"`];
|
|
31
|
-
|
|
32
|
-
// Standard iframe attributes from the attributes map
|
|
33
|
-
const iframeAttrs = ["align", "frameborder", "height", "scrolling", "width", "class", "style"];
|
|
34
|
-
for (const attr of iframeAttrs) {
|
|
35
|
-
let value = data.attributes[attr] ?? "";
|
|
36
|
-
// Sanitize style attribute to prevent CSS injection
|
|
37
|
-
if (attr === "style") {
|
|
38
|
-
value = sanitizeStyleValue(value);
|
|
39
|
-
}
|
|
40
|
-
attrs.push(`${attr}="${escapeAttr(value)}"`);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
ctx.push(`<p><iframe ${attrs.join(" ")}></iframe></p>`);
|
|
44
|
-
}
|
package/src/elements/iftags.ts
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
* Renderer for `[[iftags]]...[[/iftags]]` conditional blocks.
|
|
4
|
-
*
|
|
5
|
-
* Wikidot's `iftags` construct conditionally renders content based on
|
|
6
|
-
* whether the current page's tags match a condition string. The condition
|
|
7
|
-
* supports three kinds of tag tokens:
|
|
8
|
-
*
|
|
9
|
-
* - `+tag` -- required: the tag must be present
|
|
10
|
-
* - `-tag` -- excluded: the tag must NOT be present
|
|
11
|
-
* - `tag` (no prefix) -- optional group: at least one unprefixed tag must be present
|
|
12
|
-
*
|
|
13
|
-
* All three categories must independently be satisfied for the condition
|
|
14
|
-
* to evaluate to true.
|
|
15
|
-
*
|
|
16
|
-
* @module
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import type { IfTagsData } from "@wdprlib/ast";
|
|
20
|
-
import type { RenderContext } from "../context";
|
|
21
|
-
import { renderElements } from "../render";
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Evaluate an iftags condition string against a list of page tags.
|
|
25
|
-
*
|
|
26
|
-
* The condition is a space-separated list of tokens. All required tags
|
|
27
|
-
* (`+tag`) must be present, all excluded tags (`-tag`) must be absent,
|
|
28
|
-
* and at least one optional tag (bare `tag`) must be present (if any
|
|
29
|
-
* optional tags are specified).
|
|
30
|
-
*
|
|
31
|
-
* An empty condition always evaluates to `false`.
|
|
32
|
-
*
|
|
33
|
-
* @param condition - The condition string (e.g. `"+scp -joke tale"`).
|
|
34
|
-
* @param pageTags - Array of tags currently assigned to the page.
|
|
35
|
-
* @returns `true` if the condition is satisfied.
|
|
36
|
-
*/
|
|
37
|
-
function evaluateIfTagsCondition(condition: string, pageTags: string[]): boolean {
|
|
38
|
-
const pageTagSet = new Set(pageTags.map((t) => t.toLowerCase()));
|
|
39
|
-
const tokens = condition.split(/\s+/).filter(Boolean);
|
|
40
|
-
|
|
41
|
-
// Empty condition = never show
|
|
42
|
-
if (tokens.length === 0) {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const required: string[] = [];
|
|
47
|
-
const excluded: string[] = [];
|
|
48
|
-
const optional: string[] = [];
|
|
49
|
-
|
|
50
|
-
for (const token of tokens) {
|
|
51
|
-
if (token.startsWith("+")) {
|
|
52
|
-
const tag = token.slice(1).toLowerCase();
|
|
53
|
-
if (tag) required.push(tag);
|
|
54
|
-
} else if (token.startsWith("-")) {
|
|
55
|
-
const tag = token.slice(1).toLowerCase();
|
|
56
|
-
if (tag) excluded.push(tag);
|
|
57
|
-
} else {
|
|
58
|
-
optional.push(token.toLowerCase());
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// If all tokens had empty tag names (e.g. "+" or "-"), treat as empty condition
|
|
63
|
-
if (required.length === 0 && excluded.length === 0 && optional.length === 0) {
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// All required tags must be present
|
|
68
|
-
for (const tag of required) {
|
|
69
|
-
if (!pageTagSet.has(tag)) return false;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// All excluded tags must NOT be present
|
|
73
|
-
for (const tag of excluded) {
|
|
74
|
-
if (pageTagSet.has(tag)) return false;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// If there are optional tags, at least one must be present
|
|
78
|
-
if (optional.length > 0) {
|
|
79
|
-
const hasAnyOptional = optional.some((tag) => pageTagSet.has(tag));
|
|
80
|
-
if (!hasAnyOptional) return false;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Render an `[[iftags]]` block.
|
|
88
|
-
*
|
|
89
|
-
* Evaluates the condition against the page's tags (from `ctx.page.tags`).
|
|
90
|
-
* If no page tags are available, an empty array is used (all conditions
|
|
91
|
-
* requiring present tags will fail).
|
|
92
|
-
*
|
|
93
|
-
* @param ctx - The current render context.
|
|
94
|
-
* @param data - IfTags data with condition string and child elements.
|
|
95
|
-
*/
|
|
96
|
-
export function renderIfTags(ctx: RenderContext, data: IfTagsData): void {
|
|
97
|
-
const pageTags = ctx.page?.tags ?? [];
|
|
98
|
-
|
|
99
|
-
if (evaluateIfTagsCondition(data.condition, pageTags)) {
|
|
100
|
-
const prev = ctx.renderInlineStyles;
|
|
101
|
-
ctx.renderInlineStyles = true;
|
|
102
|
-
|
|
103
|
-
// If a style slot was assigned during resolve, collect styles into
|
|
104
|
-
// it so they appear at the correct source-order position in the
|
|
105
|
-
// final output. Otherwise fall back to inline rendering.
|
|
106
|
-
const slotId = (data as IfTagsData & { _styleSlot?: number })._styleSlot;
|
|
107
|
-
if (slotId !== undefined) {
|
|
108
|
-
ctx.enterStyleSlot(slotId);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
renderElements(ctx, data.elements);
|
|
112
|
-
|
|
113
|
-
if (slotId !== undefined) {
|
|
114
|
-
ctx.exitStyleSlot();
|
|
115
|
-
}
|
|
116
|
-
ctx.renderInlineStyles = prev;
|
|
117
|
-
}
|
|
118
|
-
}
|