@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.
Files changed (173) hide show
  1. package/dist/index.cjs +2344 -1668
  2. package/dist/index.d.cts +15 -13
  3. package/dist/index.d.ts +15 -13
  4. package/dist/index.js +2375 -1699
  5. package/package.json +1 -1
  6. package/src/context/attributes.ts +14 -0
  7. package/src/context/bibliography.ts +109 -0
  8. package/src/context/counters.ts +51 -0
  9. package/src/context/image-urls.ts +31 -0
  10. package/src/context/index.ts +285 -0
  11. package/src/context/output.ts +17 -0
  12. package/src/context/page-urls.ts +81 -0
  13. package/src/context/style-slots.ts +29 -0
  14. package/src/context/urls.ts +2 -0
  15. package/src/elements/bibliography/block.ts +27 -0
  16. package/src/elements/bibliography/cite.ts +23 -0
  17. package/src/elements/bibliography/ids.ts +9 -0
  18. package/src/elements/bibliography/index.ts +9 -0
  19. package/src/elements/code/contents.ts +18 -0
  20. package/src/elements/code/index.ts +29 -0
  21. package/src/elements/collapsible/index.ts +31 -0
  22. package/src/elements/collapsible/labels.ts +35 -0
  23. package/src/elements/collapsible/link.ts +11 -0
  24. package/src/elements/collapsible/sections.ts +39 -0
  25. package/src/elements/container/attributes.ts +28 -0
  26. package/src/elements/container/header.ts +27 -0
  27. package/src/elements/container/index.ts +35 -0
  28. package/src/elements/container/string-container.ts +40 -0
  29. package/src/elements/container/string-types.ts +63 -0
  30. package/src/elements/container/wrappers.ts +32 -0
  31. package/src/elements/date/format.ts +20 -0
  32. package/src/elements/{date.ts → date/index.ts} +4 -29
  33. package/src/elements/date/output.ts +6 -0
  34. package/src/elements/embed/iframe.ts +8 -0
  35. package/src/elements/embed/index.ts +28 -0
  36. package/src/elements/embed/providers.ts +43 -0
  37. package/src/elements/embed/validation.ts +15 -0
  38. package/src/elements/embed-block/allowlist.ts +60 -0
  39. package/src/elements/embed-block/boolean-attributes.ts +38 -0
  40. package/src/elements/embed-block/iframe.ts +33 -0
  41. package/src/elements/embed-block/index.ts +31 -0
  42. package/src/elements/embed-block/sanitize-config.ts +22 -0
  43. package/src/elements/embed-block/sanitize.ts +44 -0
  44. package/src/elements/expr/branch.ts +29 -0
  45. package/src/elements/expr/index.ts +63 -0
  46. package/src/elements/expr/result.ts +19 -0
  47. package/src/elements/footnote/body.ts +11 -0
  48. package/src/elements/footnote/index.ts +35 -0
  49. package/src/elements/footnote/ref.ts +16 -0
  50. package/src/elements/html/attributes.ts +24 -0
  51. package/src/elements/html/index.ts +39 -0
  52. package/src/elements/html/url.ts +19 -0
  53. package/src/elements/iframe/attributes.ts +28 -0
  54. package/src/elements/iframe/index.ts +22 -0
  55. package/src/elements/iftags/condition.ts +42 -0
  56. package/src/elements/iftags/index.ts +39 -0
  57. package/src/elements/iftags/style-slot.ts +23 -0
  58. package/src/elements/iftags/tokens.ts +36 -0
  59. package/src/elements/image/alignment.ts +44 -0
  60. package/src/elements/image/attributes.ts +10 -0
  61. package/src/elements/image/img-attributes.ts +26 -0
  62. package/src/elements/image/index.ts +36 -0
  63. package/src/elements/image/link-href.ts +24 -0
  64. package/src/elements/image/link.ts +13 -0
  65. package/src/elements/image/source.ts +16 -0
  66. package/src/elements/{include.ts → include/index.ts} +5 -13
  67. package/src/elements/include/missing.ts +15 -0
  68. package/src/elements/link/anchor-name.ts +6 -0
  69. package/src/elements/link/anchor.ts +27 -0
  70. package/src/elements/link/attributes.ts +47 -0
  71. package/src/elements/link/index.ts +26 -0
  72. package/src/elements/link/label.ts +23 -0
  73. package/src/elements/link/target.ts +20 -0
  74. package/src/elements/list/attributes.ts +19 -0
  75. package/src/elements/list/definition-list.ts +16 -0
  76. package/src/elements/list/index.ts +48 -0
  77. package/src/elements/list/item-rendering.ts +38 -0
  78. package/src/elements/list/items.ts +61 -0
  79. package/src/elements/list/no-marker.ts +53 -0
  80. package/src/elements/list/paragraphs.ts +34 -0
  81. package/src/elements/list/trim.ts +38 -0
  82. package/src/elements/math/block.ts +29 -0
  83. package/src/elements/math/equation-ref.ts +12 -0
  84. package/src/elements/math/index.ts +14 -0
  85. package/src/elements/math/inline.ts +19 -0
  86. package/src/elements/math/latex.ts +27 -0
  87. package/src/elements/math/source.ts +18 -0
  88. package/src/elements/module/backlinks.ts +2 -1
  89. package/src/elements/module/categories.ts +2 -2
  90. package/src/elements/module/empty-container.ts +10 -0
  91. package/src/elements/module/index.ts +2 -4
  92. package/src/elements/module/join-markup.ts +10 -0
  93. package/src/elements/module/join.ts +2 -7
  94. package/src/elements/module/listpages.ts +2 -2
  95. package/src/elements/module/listusers.ts +2 -2
  96. package/src/elements/module/page-tree.ts +2 -2
  97. package/src/elements/module/rate-markup.ts +10 -0
  98. package/src/elements/module/rate.ts +4 -13
  99. package/src/elements/module/unknown.ts +11 -0
  100. package/src/elements/tab-view/ids.ts +16 -0
  101. package/src/elements/tab-view/index.ts +31 -0
  102. package/src/elements/tab-view/navigation.ts +15 -0
  103. package/src/elements/tab-view/panels.ts +16 -0
  104. package/src/elements/table/attributes.ts +23 -0
  105. package/src/elements/table/cell-attributes.ts +62 -0
  106. package/src/elements/table/cell.ts +13 -0
  107. package/src/elements/table/index.ts +27 -0
  108. package/src/elements/text/email.ts +20 -0
  109. package/src/elements/text/index.ts +11 -0
  110. package/src/elements/text/plain.ts +11 -0
  111. package/src/elements/text/raw.ts +20 -0
  112. package/src/elements/toc/body.ts +12 -0
  113. package/src/elements/toc/entries.ts +34 -0
  114. package/src/elements/toc/frame.ts +27 -0
  115. package/src/elements/toc/index.ts +17 -0
  116. package/src/elements/toc/link.ts +26 -0
  117. package/src/elements/user/index.ts +40 -0
  118. package/src/elements/user/markup.ts +34 -0
  119. package/src/elements/user/resolve.ts +6 -0
  120. package/src/escape/attribute-allowlists.ts +101 -0
  121. package/src/escape/attributes.ts +62 -0
  122. package/src/escape/css-color-functions.ts +18 -0
  123. package/src/escape/css-colors.ts +183 -0
  124. package/src/escape/css-danger.ts +22 -0
  125. package/src/escape/css-normalize.ts +54 -0
  126. package/src/escape/css-style.ts +78 -0
  127. package/src/escape/css-urls.ts +76 -0
  128. package/src/escape/css.ts +4 -0
  129. package/src/escape/email.ts +22 -0
  130. package/src/escape/html.ts +68 -0
  131. package/src/escape/index.ts +15 -0
  132. package/src/escape/url.ts +18 -0
  133. package/src/libs/highlighter/engine/end-pattern.ts +26 -0
  134. package/src/libs/highlighter/engine/html.ts +19 -0
  135. package/src/libs/highlighter/engine/index.ts +3 -0
  136. package/src/libs/highlighter/engine/keywords.ts +22 -0
  137. package/src/libs/highlighter/engine/parts.ts +36 -0
  138. package/src/libs/highlighter/engine/preprocess.ts +10 -0
  139. package/src/libs/highlighter/engine/render.ts +31 -0
  140. package/src/libs/highlighter/engine/token.ts +7 -0
  141. package/src/libs/highlighter/engine/tokenizer.ts +266 -0
  142. package/src/libs/highlighter/engine/utils.ts +38 -0
  143. package/src/render/collected-styles.ts +22 -0
  144. package/src/render/dispatch.ts +181 -0
  145. package/src/render/index.ts +28 -0
  146. package/src/render/primitives.ts +17 -0
  147. package/src/render/style-tag.ts +6 -0
  148. package/src/render/style.ts +15 -0
  149. package/src/types.ts +6 -2
  150. package/src/context.ts +0 -422
  151. package/src/elements/bibliography.ts +0 -123
  152. package/src/elements/code.ts +0 -49
  153. package/src/elements/collapsible.ts +0 -105
  154. package/src/elements/container.ts +0 -302
  155. package/src/elements/embed-block.ts +0 -327
  156. package/src/elements/embed.ts +0 -166
  157. package/src/elements/expr.ts +0 -102
  158. package/src/elements/footnote.ts +0 -76
  159. package/src/elements/html.ts +0 -79
  160. package/src/elements/iframe.ts +0 -44
  161. package/src/elements/iftags.ts +0 -118
  162. package/src/elements/image.ts +0 -154
  163. package/src/elements/link.ts +0 -201
  164. package/src/elements/list.ts +0 -241
  165. package/src/elements/math.ts +0 -177
  166. package/src/elements/tab-view.ts +0 -75
  167. package/src/elements/table.ts +0 -101
  168. package/src/elements/text.ts +0 -57
  169. package/src/elements/toc.ts +0 -147
  170. package/src/elements/user.ts +0 -79
  171. package/src/escape.ts +0 -829
  172. package/src/libs/highlighter/engine.ts +0 -352
  173. package/src/render.ts +0 -231
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }