@wdprlib/render 2.0.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 (169) hide show
  1. package/dist/index.cjs +2332 -2032
  2. package/dist/index.d.cts +15 -13
  3. package/dist/index.d.ts +15 -13
  4. package/dist/index.js +2336 -2036
  5. package/package.json +5 -3
  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/clear-float.ts +27 -0
  20. package/src/elements/code/contents.ts +18 -0
  21. package/src/elements/code/index.ts +29 -0
  22. package/src/elements/collapsible/index.ts +31 -0
  23. package/src/elements/collapsible/labels.ts +35 -0
  24. package/src/elements/collapsible/link.ts +11 -0
  25. package/src/elements/collapsible/sections.ts +39 -0
  26. package/src/elements/color.ts +32 -0
  27. package/src/elements/container/attributes.ts +28 -0
  28. package/src/elements/container/header.ts +27 -0
  29. package/src/elements/container/index.ts +35 -0
  30. package/src/elements/container/string-container.ts +40 -0
  31. package/src/elements/container/string-types.ts +63 -0
  32. package/src/elements/container/wrappers.ts +32 -0
  33. package/src/elements/date/format.ts +20 -0
  34. package/src/elements/date/index.ts +34 -0
  35. package/src/elements/date/output.ts +6 -0
  36. package/src/elements/embed/iframe.ts +8 -0
  37. package/src/elements/embed/index.ts +28 -0
  38. package/src/elements/embed/providers.ts +43 -0
  39. package/src/elements/embed/validation.ts +15 -0
  40. package/src/elements/embed-block/allowlist.ts +60 -0
  41. package/src/elements/embed-block/boolean-attributes.ts +38 -0
  42. package/src/elements/embed-block/iframe.ts +33 -0
  43. package/src/elements/embed-block/index.ts +31 -0
  44. package/src/elements/embed-block/sanitize-config.ts +22 -0
  45. package/src/elements/embed-block/sanitize.ts +44 -0
  46. package/src/elements/expr/branch.ts +29 -0
  47. package/src/elements/expr/index.ts +63 -0
  48. package/src/elements/expr/result.ts +19 -0
  49. package/src/elements/footnote/body.ts +11 -0
  50. package/src/elements/footnote/index.ts +35 -0
  51. package/src/elements/footnote/ref.ts +16 -0
  52. package/src/elements/html/attributes.ts +24 -0
  53. package/src/elements/html/index.ts +39 -0
  54. package/src/elements/html/url.ts +19 -0
  55. package/src/elements/iframe/attributes.ts +28 -0
  56. package/src/elements/iframe/index.ts +22 -0
  57. package/src/elements/iftags/condition.ts +42 -0
  58. package/src/elements/iftags/index.ts +39 -0
  59. package/src/elements/iftags/style-slot.ts +23 -0
  60. package/src/elements/iftags/tokens.ts +36 -0
  61. package/src/elements/image/alignment.ts +44 -0
  62. package/src/elements/image/attributes.ts +10 -0
  63. package/src/elements/image/img-attributes.ts +26 -0
  64. package/src/elements/image/index.ts +36 -0
  65. package/src/elements/image/link-href.ts +24 -0
  66. package/src/elements/image/link.ts +13 -0
  67. package/src/elements/image/source.ts +16 -0
  68. package/src/elements/include/index.ts +35 -0
  69. package/src/elements/include/missing.ts +15 -0
  70. package/src/elements/index.ts +35 -0
  71. package/src/elements/line-break.ts +22 -0
  72. package/src/elements/link/anchor-name.ts +6 -0
  73. package/src/elements/link/anchor.ts +27 -0
  74. package/src/elements/link/attributes.ts +47 -0
  75. package/src/elements/link/index.ts +26 -0
  76. package/src/elements/link/label.ts +23 -0
  77. package/src/elements/link/target.ts +20 -0
  78. package/src/elements/list/attributes.ts +19 -0
  79. package/src/elements/list/definition-list.ts +16 -0
  80. package/src/elements/list/index.ts +48 -0
  81. package/src/elements/list/item-rendering.ts +38 -0
  82. package/src/elements/list/items.ts +61 -0
  83. package/src/elements/list/no-marker.ts +53 -0
  84. package/src/elements/list/paragraphs.ts +34 -0
  85. package/src/elements/list/trim.ts +38 -0
  86. package/src/elements/math/block.ts +29 -0
  87. package/src/elements/math/equation-ref.ts +12 -0
  88. package/src/elements/math/index.ts +14 -0
  89. package/src/elements/math/inline.ts +19 -0
  90. package/src/elements/math/latex.ts +27 -0
  91. package/src/elements/math/source.ts +18 -0
  92. package/src/elements/module/backlinks.ts +29 -0
  93. package/src/elements/module/categories.ts +27 -0
  94. package/src/elements/module/empty-container.ts +10 -0
  95. package/src/elements/module/index.ts +65 -0
  96. package/src/elements/module/join-markup.ts +10 -0
  97. package/src/elements/module/join.ts +28 -0
  98. package/src/elements/module/listpages.ts +27 -0
  99. package/src/elements/module/listusers.ts +27 -0
  100. package/src/elements/module/page-tree.ts +27 -0
  101. package/src/elements/module/rate-markup.ts +10 -0
  102. package/src/elements/module/rate.ts +35 -0
  103. package/src/elements/module/unknown.ts +11 -0
  104. package/src/elements/tab-view/ids.ts +16 -0
  105. package/src/elements/tab-view/index.ts +31 -0
  106. package/src/elements/tab-view/navigation.ts +15 -0
  107. package/src/elements/tab-view/panels.ts +16 -0
  108. package/src/elements/table/attributes.ts +23 -0
  109. package/src/elements/table/cell-attributes.ts +62 -0
  110. package/src/elements/table/cell.ts +13 -0
  111. package/src/elements/table/index.ts +27 -0
  112. package/src/elements/text/email.ts +20 -0
  113. package/src/elements/text/index.ts +11 -0
  114. package/src/elements/text/plain.ts +11 -0
  115. package/src/elements/text/raw.ts +20 -0
  116. package/src/elements/toc/body.ts +12 -0
  117. package/src/elements/toc/entries.ts +34 -0
  118. package/src/elements/toc/frame.ts +27 -0
  119. package/src/elements/toc/index.ts +17 -0
  120. package/src/elements/toc/link.ts +26 -0
  121. package/src/elements/user/index.ts +40 -0
  122. package/src/elements/user/markup.ts +34 -0
  123. package/src/elements/user/resolve.ts +6 -0
  124. package/src/escape/attribute-allowlists.ts +101 -0
  125. package/src/escape/attributes.ts +62 -0
  126. package/src/escape/css-color-functions.ts +18 -0
  127. package/src/escape/css-colors.ts +183 -0
  128. package/src/escape/css-danger.ts +22 -0
  129. package/src/escape/css-normalize.ts +54 -0
  130. package/src/escape/css-style.ts +78 -0
  131. package/src/escape/css-urls.ts +76 -0
  132. package/src/escape/css.ts +4 -0
  133. package/src/escape/email.ts +22 -0
  134. package/src/escape/html.ts +68 -0
  135. package/src/escape/index.ts +15 -0
  136. package/src/escape/url.ts +18 -0
  137. package/src/hash.ts +62 -0
  138. package/src/index.ts +26 -0
  139. package/src/libs/highlighter/engine/end-pattern.ts +26 -0
  140. package/src/libs/highlighter/engine/html.ts +19 -0
  141. package/src/libs/highlighter/engine/index.ts +3 -0
  142. package/src/libs/highlighter/engine/keywords.ts +22 -0
  143. package/src/libs/highlighter/engine/parts.ts +36 -0
  144. package/src/libs/highlighter/engine/preprocess.ts +10 -0
  145. package/src/libs/highlighter/engine/render.ts +31 -0
  146. package/src/libs/highlighter/engine/token.ts +7 -0
  147. package/src/libs/highlighter/engine/tokenizer.ts +266 -0
  148. package/src/libs/highlighter/engine/utils.ts +38 -0
  149. package/src/libs/highlighter/index.ts +70 -0
  150. package/src/libs/highlighter/languages/cpp.ts +345 -0
  151. package/src/libs/highlighter/languages/css.ts +104 -0
  152. package/src/libs/highlighter/languages/diff.ts +154 -0
  153. package/src/libs/highlighter/languages/dtd.ts +99 -0
  154. package/src/libs/highlighter/languages/html.ts +59 -0
  155. package/src/libs/highlighter/languages/java.ts +251 -0
  156. package/src/libs/highlighter/languages/javascript.ts +213 -0
  157. package/src/libs/highlighter/languages/php.ts +433 -0
  158. package/src/libs/highlighter/languages/python.ts +308 -0
  159. package/src/libs/highlighter/languages/ruby.ts +360 -0
  160. package/src/libs/highlighter/languages/sql.ts +125 -0
  161. package/src/libs/highlighter/languages/xml.ts +68 -0
  162. package/src/libs/highlighter/types.ts +44 -0
  163. package/src/render/collected-styles.ts +22 -0
  164. package/src/render/dispatch.ts +181 -0
  165. package/src/render/index.ts +28 -0
  166. package/src/render/primitives.ts +17 -0
  167. package/src/render/style-tag.ts +6 -0
  168. package/src/render/style.ts +15 -0
  169. package/src/types.ts +144 -0
@@ -0,0 +1,23 @@
1
+ import type { BibliographyCiteData } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+ import { escapeAttr, escapeHtml } from "../../escape";
4
+ import { generateBibliographyIdSuffix } from "./ids";
5
+
6
+ export function renderBibliographyCite(ctx: RenderContext, data: BibliographyCiteData): void {
7
+ const number = ctx.getBibliographyCitationNumber(data.label);
8
+ const counter = ctx.nextBibciteCounter();
9
+
10
+ if (number === undefined) {
11
+ ctx.push(escapeHtml(data.label));
12
+ return;
13
+ }
14
+
15
+ const idSuffix = generateBibliographyIdSuffix(data.label, counter);
16
+ const id = ctx.generateId(`bibcite-${number}-`, idSuffix);
17
+ const bibitemId = ctx.generateId("bibitem-", number);
18
+ const onclick = `WIKIDOT.page.utils.scrollToReference('${bibitemId}')`;
19
+
20
+ ctx.push(`<a href="javascript:;" class="bibcite" id="${id}" onclick="${escapeAttr(onclick)}">`);
21
+ ctx.push(String(number));
22
+ ctx.push("</a>");
23
+ }
@@ -0,0 +1,9 @@
1
+ export function generateBibliographyIdSuffix(label: string, counter: number): string {
2
+ let h = 0x811c9dc5;
3
+ const input = label + counter;
4
+ for (let i = 0; i < input.length; i++) {
5
+ h ^= input.charCodeAt(i);
6
+ h = Math.imul(h, 0x01000193);
7
+ }
8
+ return (h >>> 0).toString(16).slice(0, 6);
9
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ *
3
+ * Renderers for Wikidot bibliography markup.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ export { renderBibliographyCite } from "./cite";
9
+ export { renderBibliographyBlock } from "./block";
@@ -0,0 +1,27 @@
1
+ /**
2
+ *
3
+ * Renderer for the Wikidot `~~~~~` (clear-float) markup.
4
+ *
5
+ * Wikidot uses `~~~~~` (five tildes) to insert a CSS float-clearing
6
+ * `<div>`. The direction (`left`, `right`, or `both`) is determined
7
+ * by the number and placement of tildes in the source markup.
8
+ *
9
+ * @module
10
+ */
11
+
12
+ import type { ClearFloat } from "@wdprlib/ast";
13
+ import type { RenderContext } from "../context";
14
+
15
+ /**
16
+ * Render a clear-float element as an invisible `<div>` with the
17
+ * appropriate CSS `clear` property.
18
+ *
19
+ * The output matches Wikidot's rendering: a zero-height div with
20
+ * `font-size: 1px` to prevent layout collapse in some browsers.
21
+ *
22
+ * @param ctx - The current render context.
23
+ * @param direction - CSS clear direction (`"left"`, `"right"`, or `"both"`).
24
+ */
25
+ export function renderClearFloat(ctx: RenderContext, direction: ClearFloat): void {
26
+ ctx.push(`<div style="clear:${direction}; height: 0px; font-size: 1px"></div>`);
27
+ }
@@ -0,0 +1,18 @@
1
+ import type { CodeBlockData } from "@wdprlib/ast";
2
+ import { escapeHtml } from "../../escape";
3
+ import { highlight } from "../../libs/highlighter";
4
+
5
+ export function renderCodeContents(data: CodeBlockData): string {
6
+ if (data.language) {
7
+ const highlighted = highlight(data.contents, data.language);
8
+ if (highlighted) {
9
+ return highlighted;
10
+ }
11
+ }
12
+
13
+ return renderPlainCode(data.contents);
14
+ }
15
+
16
+ function renderPlainCode(contents: string): string {
17
+ return `<pre><code>${escapeHtml(contents)}</code></pre>`;
18
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ *
3
+ * Renderer for `[[code]]...[[/code]]` blocks in Wikidot markup.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ import type { CodeBlockData } from "@wdprlib/ast";
9
+ import type { RenderContext } from "../../context";
10
+ import { renderCodeContents } from "./contents";
11
+
12
+ /**
13
+ * Render a `[[code]]` block.
14
+ *
15
+ * The block is wrapped in `<div class="code">`. If the block is empty,
16
+ * the div is closed immediately with no inner content.
17
+ *
18
+ * @param ctx - The current render context.
19
+ * @param data - Code block data containing contents and optional language.
20
+ */
21
+ export function renderCode(ctx: RenderContext, data: CodeBlockData): void {
22
+ ctx.push(`<div class="code">`);
23
+
24
+ if (data.contents !== "") {
25
+ ctx.push(renderCodeContents(data));
26
+ }
27
+
28
+ ctx.push("</div>");
29
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ *
3
+ * Renderer for `[[collapsible]]...[[/collapsible]]` blocks.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ import type { CollapsibleData } from "@wdprlib/ast";
9
+ import type { RenderContext } from "../../context";
10
+ import { getCollapsibleLabels } from "./labels";
11
+ import { renderFoldedSection, renderUnfoldedSection } from "./sections";
12
+
13
+ /**
14
+ * Render a `[[collapsible]]` block with Wikidot-compatible HTML structure.
15
+ *
16
+ * The output contains both folded and unfolded states. Spaces in
17
+ * show/hide labels are encoded as `&nbsp;` to match Wikidot's behavior.
18
+ *
19
+ * @param ctx - The current render context.
20
+ * @param data - Collapsible block data with show/hide text, start-open
21
+ * flag, and top/bottom link placement options.
22
+ */
23
+ export function renderCollapsible(ctx: RenderContext, data: CollapsibleData): void {
24
+ const startOpen = data["start-open"];
25
+ const labels = getCollapsibleLabels(data);
26
+
27
+ ctx.push(`<div class="collapsible-block">`);
28
+ renderFoldedSection(ctx, startOpen, labels);
29
+ renderUnfoldedSection(ctx, data, labels);
30
+ ctx.push("</div>");
31
+ }
@@ -0,0 +1,35 @@
1
+ import type { CollapsibleData } from "@wdprlib/ast";
2
+ import { escapeHtml } from "../../escape";
3
+
4
+ export interface CollapsibleLabels {
5
+ show: string;
6
+ hide: string;
7
+ }
8
+
9
+ export function getCollapsibleLabels(data: CollapsibleData): CollapsibleLabels {
10
+ return {
11
+ show: data["show-text"]
12
+ ? formatLabelText(data["show-text"])
13
+ : formatCollapsibleText("+", "show block"),
14
+ hide: data["hide-text"]
15
+ ? formatLabelText(data["hide-text"])
16
+ : formatCollapsibleText("\u2013", "hide block"),
17
+ };
18
+ }
19
+
20
+ /**
21
+ * Format a default collapsible link label by prepending a prefix symbol
22
+ * (e.g. "+" or en-dash) with `&nbsp;` encoding for spaces.
23
+ */
24
+ function formatCollapsibleText(prefix: string, text: string): string {
25
+ const encoded = escapeHtml(text).replace(/ /g, "&nbsp;");
26
+ return `${prefix}&nbsp;${encoded}`;
27
+ }
28
+
29
+ /**
30
+ * Format a custom collapsible link label by escaping HTML and
31
+ * replacing spaces with `&nbsp;` (matching Wikidot behavior).
32
+ */
33
+ function formatLabelText(text: string): string {
34
+ return escapeHtml(text).replace(/ /g, "&nbsp;");
35
+ }
@@ -0,0 +1,11 @@
1
+ import type { RenderContext } from "../../context";
2
+
3
+ export function renderCollapsibleLink(ctx: RenderContext, label: string): void {
4
+ ctx.push(`<a class="collapsible-block-link" href="javascript:;">${label}</a>`);
5
+ }
6
+
7
+ export function renderHideLink(ctx: RenderContext, label: string): void {
8
+ ctx.push(`<div class="collapsible-block-unfolded-link">`);
9
+ renderCollapsibleLink(ctx, label);
10
+ ctx.push("</div>");
11
+ }
@@ -0,0 +1,39 @@
1
+ import type { CollapsibleData } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+ import { renderElements } from "../../render";
4
+ import type { CollapsibleLabels } from "./labels";
5
+ import { renderCollapsibleLink, renderHideLink } from "./link";
6
+
7
+ export function renderFoldedSection(
8
+ ctx: RenderContext,
9
+ startOpen: boolean,
10
+ labels: CollapsibleLabels,
11
+ ): void {
12
+ const foldedStyle = startOpen ? ` style="display:none"` : "";
13
+ ctx.push(`<div class="collapsible-block-folded"${foldedStyle}>`);
14
+ renderCollapsibleLink(ctx, labels.show);
15
+ ctx.push("</div>");
16
+ }
17
+
18
+ export function renderUnfoldedSection(
19
+ ctx: RenderContext,
20
+ data: CollapsibleData,
21
+ labels: CollapsibleLabels,
22
+ ): void {
23
+ const unfoldedStyle = data["start-open"] ? "" : ` style="display:none"`;
24
+ ctx.push(`<div class="collapsible-block-unfolded"${unfoldedStyle}>`);
25
+
26
+ if (data["show-top"] || (!data["show-top"] && !data["show-bottom"])) {
27
+ renderHideLink(ctx, labels.hide);
28
+ }
29
+
30
+ ctx.push(`<div class="collapsible-block-content">`);
31
+ renderElements(ctx, data.elements);
32
+ ctx.push("</div>");
33
+
34
+ if (data["show-bottom"]) {
35
+ renderHideLink(ctx, labels.hide);
36
+ }
37
+
38
+ ctx.push("</div>");
39
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ *
3
+ * Renderer for `##color|text##` inline color markup in Wikidot syntax.
4
+ *
5
+ * The color value is sanitized to prevent CSS injection before being
6
+ * injected into an inline `style` attribute.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import type { ColorData } from "@wdprlib/ast";
12
+ import type { RenderContext } from "../context";
13
+ import { escapeAttr, sanitizeCssColor } from "../escape";
14
+ import { renderElements } from "../render";
15
+
16
+ /**
17
+ * Render an inline color element (`##color|text##`).
18
+ *
19
+ * Wraps the child elements in a `<span>` with an inline `color` style.
20
+ * The user-supplied color value is validated and sanitized; invalid
21
+ * values fall back to `"inherit"`.
22
+ *
23
+ * @param ctx - The current render context.
24
+ * @param data - Color element data with the color value and child elements.
25
+ */
26
+ export function renderColor(ctx: RenderContext, data: ColorData): void {
27
+ // Sanitize color value to prevent CSS injection
28
+ const safeColor = sanitizeCssColor(data.color, "inherit");
29
+ ctx.push(`<span style="color: ${escapeAttr(safeColor)}">`);
30
+ renderElements(ctx, data.elements);
31
+ ctx.push("</span>");
32
+ }
@@ -0,0 +1,28 @@
1
+ import { escapeAttr, sanitizeAttributes } from "../../escape";
2
+
3
+ /**
4
+ * Sanitize and format an attribute map into an HTML attribute string.
5
+ */
6
+ export function renderContainerAttrs(attributes: Record<string, string>): string {
7
+ if (!hasAttributes(attributes)) {
8
+ return "";
9
+ }
10
+
11
+ const safe = sanitizeAttributes(attributes);
12
+ let result = "";
13
+ for (const key in safe) {
14
+ if (key.startsWith("_")) {
15
+ continue;
16
+ }
17
+ const value = safe[key]!;
18
+ result += value !== "" ? ` ${key}="${escapeAttr(value)}"` : ` ${key}=""`;
19
+ }
20
+ return result;
21
+ }
22
+
23
+ export function hasAttributes(attributes: Record<string, string>): boolean {
24
+ for (const _ in attributes) {
25
+ return true;
26
+ }
27
+ return false;
28
+ }
@@ -0,0 +1,27 @@
1
+ import type { Element } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+ import { renderElements } from "../../render";
4
+ import { renderContainerAttrs } from "./attributes";
5
+
6
+ /**
7
+ * Render a heading element (`h1`..`h6`).
8
+ */
9
+ export function renderHeader(
10
+ ctx: RenderContext,
11
+ level: number,
12
+ hasToc: boolean,
13
+ attributes: Record<string, string>,
14
+ elements: Element[],
15
+ ): void {
16
+ const tag = `h${level}`;
17
+ if (hasToc) {
18
+ const tocId = ctx.generateId("toc", ctx.nextTocIndex());
19
+ ctx.push(`<${tag} id="${tocId}"${renderContainerAttrs(attributes)}>`);
20
+ } else {
21
+ ctx.push(`<${tag}${renderContainerAttrs(attributes)}>`);
22
+ }
23
+ ctx.push("<span>");
24
+ renderElements(ctx, elements);
25
+ ctx.push("</span>");
26
+ ctx.push(`</${tag}>`);
27
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Renderer for general container AST elements.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import type { ContainerData } from "@wdprlib/ast";
8
+ import { isAlignType, isHeaderType, isStringContainerType } from "@wdprlib/ast";
9
+ import type { RenderContext } from "../../context";
10
+ import { renderElements } from "../../render";
11
+ import { renderHeader } from "./header";
12
+ import { renderStringContainer } from "./string-container";
13
+
14
+ /**
15
+ * Render a container element by dispatching on its `type` discriminant.
16
+ */
17
+ export function renderContainer(ctx: RenderContext, data: ContainerData): void {
18
+ const { type, attributes, elements } = data;
19
+
20
+ if (isHeaderType(type)) {
21
+ renderHeader(ctx, type.header.level, type.header["has-toc"], attributes, elements);
22
+ return;
23
+ }
24
+
25
+ if (isAlignType(type)) {
26
+ ctx.push(`<div style="text-align: ${type.align};">`);
27
+ renderElements(ctx, elements);
28
+ ctx.push("</div>");
29
+ return;
30
+ }
31
+
32
+ if (isStringContainerType(type)) {
33
+ renderStringContainer(ctx, type, attributes, elements);
34
+ }
35
+ }
@@ -0,0 +1,40 @@
1
+ import type { Element } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+ import { renderElements } from "../../render";
4
+ import { hasAttributes } from "./attributes";
5
+ import { getStringContainerRendering, isContentsOnlyStringContainer } from "./string-types";
6
+ import { renderPlainWrapped, renderStyledSpan, renderWrapped } from "./wrappers";
7
+
8
+ /**
9
+ * Render a container whose type is a plain string identifier.
10
+ */
11
+ export function renderStringContainer(
12
+ ctx: RenderContext,
13
+ type: string,
14
+ attributes: Record<string, string>,
15
+ elements: Element[],
16
+ ): void {
17
+ if (type === "div" && elements.length === 0 && !hasAttributes(attributes)) {
18
+ return;
19
+ }
20
+
21
+ if (isContentsOnlyStringContainer(type)) {
22
+ renderElements(ctx, elements);
23
+ return;
24
+ }
25
+
26
+ const rendering = getStringContainerRendering(type);
27
+ switch (rendering.kind) {
28
+ case "wrapped":
29
+ renderWrapped(ctx, rendering.tag, attributes, elements);
30
+ return;
31
+ case "styled-span":
32
+ renderStyledSpan(ctx, rendering.style, attributes, elements);
33
+ return;
34
+ case "plain-wrapped":
35
+ renderPlainWrapped(ctx, rendering.tag, elements);
36
+ return;
37
+ case "contents":
38
+ renderElements(ctx, elements);
39
+ }
40
+ }
@@ -0,0 +1,63 @@
1
+ type StringContainerRendering =
2
+ | { kind: "wrapped"; tag: string }
3
+ | { kind: "styled-span"; style: string }
4
+ | { kind: "plain-wrapped"; tag: string }
5
+ | { kind: "contents" };
6
+
7
+ const WRAPPED_TAGS: Readonly<Record<string, string>> = {
8
+ paragraph: "p",
9
+ bold: "strong",
10
+ italics: "em",
11
+ superscript: "sup",
12
+ subscript: "sub",
13
+ monospace: "tt",
14
+ span: "span",
15
+ div: "div",
16
+ blockquote: "blockquote",
17
+ mark: "mark",
18
+ insertion: "ins",
19
+ deletion: "del",
20
+ size: "span",
21
+ ruby: "ruby",
22
+ "ruby-text": "rt",
23
+ "table-row": "tr",
24
+ "table-cell": "td",
25
+ };
26
+
27
+ const STYLED_SPANS: Readonly<Record<string, string>> = {
28
+ underline: "text-decoration: underline;",
29
+ strikethrough: "text-decoration: line-through;",
30
+ hidden: "display: none",
31
+ invisible: "visibility: hidden",
32
+ };
33
+
34
+ const PLAIN_WRAPPED_TAGS: Readonly<Record<string, string>> = {
35
+ "definition-list": "dl",
36
+ "definition-list-key": "dt",
37
+ "definition-list-value": "dd",
38
+ };
39
+
40
+ const CONTENTS_ONLY_TYPES = new Set(["heading", "collapsible", "definition-list-item"]);
41
+
42
+ export function getStringContainerRendering(type: string): StringContainerRendering {
43
+ const wrappedTag = WRAPPED_TAGS[type];
44
+ if (wrappedTag) {
45
+ return { kind: "wrapped", tag: wrappedTag };
46
+ }
47
+
48
+ const style = STYLED_SPANS[type];
49
+ if (style) {
50
+ return { kind: "styled-span", style };
51
+ }
52
+
53
+ const plainTag = PLAIN_WRAPPED_TAGS[type];
54
+ if (plainTag) {
55
+ return { kind: "plain-wrapped", tag: plainTag };
56
+ }
57
+
58
+ return { kind: "contents" };
59
+ }
60
+
61
+ export function isContentsOnlyStringContainer(type: string): boolean {
62
+ return CONTENTS_ONLY_TYPES.has(type);
63
+ }
@@ -0,0 +1,32 @@
1
+ import type { Element } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+ import { renderElements } from "../../render";
4
+ import { renderContainerAttrs } from "./attributes";
5
+
6
+ export function renderWrapped(
7
+ ctx: RenderContext,
8
+ tag: string,
9
+ attributes: Record<string, string>,
10
+ elements: Element[],
11
+ ): void {
12
+ ctx.push(`<${tag}${renderContainerAttrs(attributes)}>`);
13
+ renderElements(ctx, elements);
14
+ ctx.push(`</${tag}>`);
15
+ }
16
+
17
+ export function renderPlainWrapped(ctx: RenderContext, tag: string, elements: Element[]): void {
18
+ ctx.push(`<${tag}>`);
19
+ renderElements(ctx, elements);
20
+ ctx.push(`</${tag}>`);
21
+ }
22
+
23
+ export function renderStyledSpan(
24
+ ctx: RenderContext,
25
+ style: string,
26
+ attributes: Record<string, string>,
27
+ elements: Element[],
28
+ ): void {
29
+ ctx.push(`<span style="${style}"${renderContainerAttrs(attributes)}>`);
30
+ renderElements(ctx, elements);
31
+ ctx.push("</span>");
32
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Format a `Date` using a subset of `strftime` specifiers.
3
+ *
4
+ * Supported specifiers: `%Y` (4-digit year), `%m` (zero-padded month),
5
+ * `%d` (zero-padded day), `%H` (zero-padded hours), `%M` (zero-padded
6
+ * minutes), `%S` (zero-padded seconds).
7
+ *
8
+ * @param date - The date to format.
9
+ * @param format - A strftime-compatible format string.
10
+ * @returns The formatted date string.
11
+ */
12
+ export function formatDate(date: Date, format: string): string {
13
+ return format
14
+ .replace(/%Y/g, String(date.getFullYear()))
15
+ .replace(/%m/g, String(date.getMonth() + 1).padStart(2, "0"))
16
+ .replace(/%d/g, String(date.getDate()).padStart(2, "0"))
17
+ .replace(/%H/g, String(date.getHours()).padStart(2, "0"))
18
+ .replace(/%M/g, String(date.getMinutes()).padStart(2, "0"))
19
+ .replace(/%S/g, String(date.getSeconds()).padStart(2, "0"));
20
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ *
3
+ * Renderer for Wikidot's date/time elements.
4
+ *
5
+ * Wikidot stores dates as Unix timestamps and optionally provides a
6
+ * `strftime`-style format string. The renderer formats the date
7
+ * server-side; when the `hover` flag is set, the output is wrapped in
8
+ * `<span class="odate">` so that the runtime `odate` module can
9
+ * reformat it to the user's local timezone on the client.
10
+ *
11
+ * @module
12
+ */
13
+
14
+ import type { DateData } from "@wdprlib/ast";
15
+ import type { RenderContext } from "../../context";
16
+ import { formatDate } from "./format";
17
+ import { renderDateOutput } from "./output";
18
+
19
+ /**
20
+ * Render a date element, optionally wrapping in an `odate` span for
21
+ * client-side timezone conversion.
22
+ *
23
+ * When `data.format` is provided, the date is formatted using a subset
24
+ * of `strftime` specifiers (`%Y`, `%m`, `%d`, `%H`, `%M`, `%S`).
25
+ * Otherwise, `Date.toLocaleString()` is used as a fallback.
26
+ *
27
+ * @param ctx - The current render context.
28
+ * @param data - Date element data with timestamp, optional format, and hover flag.
29
+ */
30
+ export function renderDate(ctx: RenderContext, data: DateData): void {
31
+ const date = new Date(data.value.timestamp * 1000);
32
+ const formatted = data.format ? formatDate(date, data.format) : date.toLocaleString();
33
+ ctx.push(renderDateOutput(formatted, data.hover));
34
+ }
@@ -0,0 +1,6 @@
1
+ import { escapeHtml } from "../../escape";
2
+
3
+ export function renderDateOutput(formatted: string, hover: boolean): string {
4
+ const escaped = escapeHtml(formatted);
5
+ return hover ? `<span class="odate">${escaped}</span>` : escaped;
6
+ }
@@ -0,0 +1,8 @@
1
+ import type { RenderContext } from "../../context";
2
+ import { escapeAttr } from "../../escape";
3
+
4
+ export function renderEmbedIframe(ctx: RenderContext, className: string, src: string): void {
5
+ ctx.push(`<div class="${className}">`);
6
+ ctx.push(`<iframe src="${escapeAttr(src)}" frameborder="0" allowfullscreen></iframe>`);
7
+ ctx.push("</div>");
8
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ *
3
+ * Renderer for inline embed elements (`[[embed]]`) that reference
4
+ * third-party content providers.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ import type { Embed } from "@wdprlib/ast";
10
+ import type { RenderContext } from "../../context";
11
+ import { renderGithubGist, renderGitlabSnippet, renderVimeo, renderYoutube } from "./providers";
12
+
13
+ export function renderEmbed(ctx: RenderContext, data: Embed): void {
14
+ switch (data.embed) {
15
+ case "youtube":
16
+ renderYoutube(ctx, data.data["video-id"]);
17
+ break;
18
+ case "vimeo":
19
+ renderVimeo(ctx, data.data["video-id"]);
20
+ break;
21
+ case "github-gist":
22
+ renderGithubGist(ctx, data.data.username, data.data.hash);
23
+ break;
24
+ case "gitlab-snippet":
25
+ renderGitlabSnippet(ctx, data.data["snippet-id"]);
26
+ break;
27
+ }
28
+ }
@@ -0,0 +1,43 @@
1
+ import type { RenderContext } from "../../context";
2
+ import { escapeAttr } from "../../escape";
3
+ import { renderEmbedIframe } from "./iframe";
4
+ import {
5
+ isValidGithubUsername,
6
+ isValidGistHash,
7
+ isValidGitlabSnippetId,
8
+ isValidVideoId,
9
+ } from "./validation";
10
+
11
+ export function renderYoutube(ctx: RenderContext, videoId: string): void {
12
+ if (!isValidVideoId(videoId)) {
13
+ ctx.push(`<!-- Invalid YouTube video ID -->`);
14
+ return;
15
+ }
16
+ renderEmbedIframe(ctx, "embed-youtube", `https://www.youtube.com/embed/${videoId}`);
17
+ }
18
+
19
+ export function renderVimeo(ctx: RenderContext, videoId: string): void {
20
+ if (!isValidVideoId(videoId)) {
21
+ ctx.push(`<!-- Invalid Vimeo video ID -->`);
22
+ return;
23
+ }
24
+ renderEmbedIframe(ctx, "embed-vimeo", `https://player.vimeo.com/video/${videoId}`);
25
+ }
26
+
27
+ export function renderGithubGist(ctx: RenderContext, username: string, hash: string): void {
28
+ if (!isValidGithubUsername(username) || !isValidGistHash(hash)) {
29
+ ctx.push(`<!-- Invalid GitHub Gist parameters -->`);
30
+ return;
31
+ }
32
+ ctx.push(
33
+ `<script src="https://gist.github.com/${escapeAttr(username)}/${escapeAttr(hash)}.js"></script>`,
34
+ );
35
+ }
36
+
37
+ export function renderGitlabSnippet(ctx: RenderContext, snippetId: string): void {
38
+ if (!isValidGitlabSnippetId(snippetId)) {
39
+ ctx.push(`<!-- Invalid GitLab snippet ID -->`);
40
+ return;
41
+ }
42
+ ctx.push(`<script src="https://gitlab.com/snippets/${escapeAttr(snippetId)}.js"></script>`);
43
+ }
@@ -0,0 +1,15 @@
1
+ export function isValidVideoId(id: string): boolean {
2
+ return /^[a-zA-Z0-9_-]+$/.test(id);
3
+ }
4
+
5
+ export function isValidGithubUsername(username: string): boolean {
6
+ return /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,37}[a-zA-Z0-9])?$/.test(username);
7
+ }
8
+
9
+ export function isValidGistHash(hash: string): boolean {
10
+ return /^[a-f0-9]+$/.test(hash);
11
+ }
12
+
13
+ export function isValidGitlabSnippetId(id: string): boolean {
14
+ return /^[0-9]+$/.test(id);
15
+ }