@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
@@ -0,0 +1,29 @@
1
+ import type { MathData } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+ import { escapeAttr, escapeHtml } from "../../escape";
4
+ import { renderLatexToMathML } from "./latex";
5
+ import { pushHiddenLatexSource, pushMathRender } from "./source";
6
+
7
+ export function renderMath(ctx: RenderContext, data: MathData): void {
8
+ const index = ctx.nextEquationIndex() + 1;
9
+ const latex = data["latex-source"];
10
+ const mathml = renderLatexToMathML(latex, true);
11
+
12
+ const id = data.name
13
+ ? ctx.generateId("equation-", data.name)
14
+ : ctx.generateId("equation-", index);
15
+ const dataName = data.name ? ` data-name="${escapeAttr(data.name)}"` : "";
16
+
17
+ ctx.push(`<div class="math-block" id="${escapeAttr(id)}"${dataName}>`);
18
+ if (data.name) {
19
+ ctx.push(`<span class="equation-number">(${index})</span>`);
20
+ }
21
+
22
+ pushHiddenLatexSource(ctx, latex);
23
+ pushMathRender(ctx, mathml, () => {
24
+ ctx.push(`<span class="math-error">`);
25
+ ctx.push(escapeHtml(latex));
26
+ ctx.push(`</span>`);
27
+ });
28
+ ctx.push("</div>");
29
+ }
@@ -0,0 +1,12 @@
1
+ import type { RenderContext } from "../../context";
2
+ import { escapeAttr, escapeHtml } from "../../escape";
3
+
4
+ export function renderEquationRef(ctx: RenderContext, name: string): void {
5
+ const id = ctx.generateId("equation-", name);
6
+ ctx.push(`<span class="eref" data-target="${escapeAttr(id)}">`);
7
+ ctx.push(`<a class="eref-link" href="#${escapeAttr(id)}">`);
8
+ ctx.push(escapeHtml(name));
9
+ ctx.push(`</a>`);
10
+ ctx.push(`<span class="eref-tooltip" aria-hidden="true"></span>`);
11
+ ctx.push("</span>");
12
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ *
3
+ * Renderers for Wikidot mathematical notation elements.
4
+ *
5
+ * - `[[math]]...[[/math]]` -- display-mode (block) math
6
+ * - `[[$ ... $]]` -- inline math
7
+ * - `[[eref name]]` -- equation reference
8
+ *
9
+ * @module
10
+ */
11
+
12
+ export { renderMath } from "./block";
13
+ export { renderMathInline } from "./inline";
14
+ export { renderEquationRef } from "./equation-ref";
@@ -0,0 +1,19 @@
1
+ import type { MathInlineData } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+ import { escapeHtml } from "../../escape";
4
+ import { renderLatexToMathML } from "./latex";
5
+ import { pushHiddenLatexSource, pushMathRender } from "./source";
6
+
7
+ export function renderMathInline(ctx: RenderContext, data: MathInlineData): void {
8
+ const latex = data["latex-source"];
9
+ const mathml = renderLatexToMathML(latex, false);
10
+
11
+ ctx.push(`<span class="math-inline">`);
12
+ pushHiddenLatexSource(ctx, latex);
13
+ pushMathRender(ctx, mathml, () => {
14
+ ctx.push(`<span class="math-error">$`);
15
+ ctx.push(escapeHtml(latex));
16
+ ctx.push(`$</span>`);
17
+ });
18
+ ctx.push("</span>");
19
+ }
@@ -0,0 +1,27 @@
1
+ import temml from "temml";
2
+
3
+ export function renderLatexToMathML(latex: string, displayMode: boolean): string {
4
+ try {
5
+ let processedLatex = latex;
6
+ if (displayMode && needsAlignedWrapper(latex)) {
7
+ processedLatex = `\\begin{aligned}\n${latex}\n\\end{aligned}`;
8
+ }
9
+
10
+ return temml.renderToString(processedLatex, {
11
+ displayMode,
12
+ throwOnError: false,
13
+ annotate: false,
14
+ });
15
+ } catch {
16
+ return "";
17
+ }
18
+ }
19
+
20
+ function needsAlignedWrapper(latex: string): boolean {
21
+ if (/\\begin\s*\{/.test(latex)) {
22
+ return false;
23
+ }
24
+
25
+ const withoutEscaped = latex.replace(/\\&/g, "");
26
+ return withoutEscaped.includes("&");
27
+ }
@@ -0,0 +1,18 @@
1
+ import type { RenderContext } from "../../context";
2
+ import { escapeHtml } from "../../escape";
3
+
4
+ export function pushHiddenLatexSource(ctx: RenderContext, latex: string): void {
5
+ ctx.push(`<code class="math-source" hidden aria-hidden="true">`);
6
+ ctx.push(escapeHtml(latex));
7
+ ctx.push(`</code>`);
8
+ }
9
+
10
+ export function pushMathRender(ctx: RenderContext, mathml: string, fallback: () => void): void {
11
+ ctx.push(`<span class="math-render">`);
12
+ if (mathml) {
13
+ ctx.push(mathml);
14
+ } else {
15
+ fallback();
16
+ }
17
+ ctx.push(`</span>`);
18
+ }
@@ -12,6 +12,7 @@
12
12
 
13
13
  import type { Module } from "@wdprlib/ast";
14
14
  import type { RenderContext } from "../../context";
15
+ import { renderIndentedEmptyModuleContainer } from "./empty-container";
15
16
 
16
17
  /**
17
18
  * Render a `[[module Backlinks]]` element as an empty container.
@@ -24,5 +25,5 @@ export function renderBacklinks(
24
25
  _data: Extract<Module, { module: "backlinks" }>,
25
26
  ): void {
26
27
  // Wikidot outputs just the container div (backlinks are populated at runtime)
27
- ctx.push(`<div class="backlinks-module-box">\n\t</div>`);
28
+ renderIndentedEmptyModuleContainer(ctx, "backlinks-module-box");
28
29
  }
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type { Module } from "@wdprlib/ast";
13
13
  import type { RenderContext } from "../../context";
14
+ import { renderEmptyModuleContainer } from "./empty-container";
14
15
 
15
16
  /**
16
17
  * Render a `[[module Categories]]` element as an empty container.
@@ -22,6 +23,5 @@ export function renderCategories(
22
23
  ctx: RenderContext,
23
24
  _data: Extract<Module, { module: "categories" }>,
24
25
  ): void {
25
- ctx.push(`<div class="categories-module-box">`);
26
- ctx.push("</div>");
26
+ renderEmptyModuleContainer(ctx, "categories-module-box");
27
27
  }
@@ -0,0 +1,10 @@
1
+ import type { RenderContext } from "../../context";
2
+
3
+ export function renderEmptyModuleContainer(ctx: RenderContext, className: string): void {
4
+ ctx.push(`<div class="${className}">`);
5
+ ctx.push("</div>");
6
+ }
7
+
8
+ export function renderIndentedEmptyModuleContainer(ctx: RenderContext, className: string): void {
9
+ ctx.push(`<div class="${className}">\n\t</div>`);
10
+ }
@@ -22,6 +22,7 @@ import { renderPageTree } from "./page-tree";
22
22
  import { renderRate } from "./rate";
23
23
  import { renderListUsers } from "./listusers";
24
24
  import { renderListPages } from "./listpages";
25
+ import { renderUnknownModule } from "./unknown";
25
26
 
26
27
  /**
27
28
  * Render a `[[module]]` element by dispatching on the module name.
@@ -37,10 +38,7 @@ import { renderListPages } from "./listpages";
37
38
  export function renderModule(ctx: RenderContext, data: Module): void {
38
39
  switch (data.module) {
39
40
  case "unknown":
40
- // Render error block for unknown modules
41
- ctx.push(
42
- `<div class="error-block">[[module <em>${data.name}</em>]] No such module, please <a href="https://www.wikidot.com/doc:modules" target="_blank" rel="noopener noreferrer">check available modules</a> and fix this page.</div>`,
43
- );
41
+ renderUnknownModule(ctx, data);
44
42
  break;
45
43
  case "backlinks":
46
44
  renderBacklinks(ctx, data);
@@ -0,0 +1,10 @@
1
+ import type { Module } from "@wdprlib/ast";
2
+ import { escapeAttr, escapeHtml } from "../../escape";
3
+
4
+ type JoinModule = Extract<Module, { module: "join" }>;
5
+
6
+ export function renderJoinMarkup(data: JoinModule): string {
7
+ const buttonText = data["button-text"] ?? "Join";
8
+ const className = data.attributes?.class ?? "join-box";
9
+ return `<div class="${escapeAttr(className)}"><a href="javascript:;">${escapeHtml(buttonText)}</a></div>`;
10
+ }
@@ -11,7 +11,7 @@
11
11
 
12
12
  import type { Module } from "@wdprlib/ast";
13
13
  import type { RenderContext } from "../../context";
14
- import { escapeAttr, escapeHtml } from "../../escape";
14
+ import { renderJoinMarkup } from "./join-markup";
15
15
 
16
16
  /**
17
17
  * Render a `[[module Join]]` element with a clickable join button.
@@ -24,10 +24,5 @@ import { escapeAttr, escapeHtml } from "../../escape";
24
24
  * @param data - Join module data with optional `button-text` and CSS class.
25
25
  */
26
26
  export function renderJoin(ctx: RenderContext, data: Extract<Module, { module: "join" }>): void {
27
- const buttonText = data["button-text"] ?? "Join";
28
- const attrs = data.attributes ?? {};
29
- const className = attrs.class ?? "join-box";
30
- ctx.push(`<div class="${escapeAttr(className)}">`);
31
- ctx.push(`<a href="javascript:;">${escapeHtml(buttonText)}</a>`);
32
- ctx.push("</div>");
27
+ ctx.push(renderJoinMarkup(data));
33
28
  }
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type { Module } from "@wdprlib/ast";
13
13
  import type { RenderContext } from "../../context";
14
+ import { renderEmptyModuleContainer } from "./empty-container";
14
15
 
15
16
  /**
16
17
  * Render a `[[module ListPages]]` element as an empty container.
@@ -22,6 +23,5 @@ export function renderListPages(
22
23
  ctx: RenderContext,
23
24
  _data: Extract<Module, { module: "list-pages" }>,
24
25
  ): void {
25
- ctx.push(`<div class="list-pages-box">`);
26
- ctx.push("</div>");
26
+ renderEmptyModuleContainer(ctx, "list-pages-box");
27
27
  }
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type { Module } from "@wdprlib/ast";
13
13
  import type { RenderContext } from "../../context";
14
+ import { renderEmptyModuleContainer } from "./empty-container";
14
15
 
15
16
  /**
16
17
  * Render a `[[module ListUsers]]` element as an empty container.
@@ -22,6 +23,5 @@ export function renderListUsers(
22
23
  ctx: RenderContext,
23
24
  _data: Extract<Module, { module: "list-users" }>,
24
25
  ): void {
25
- ctx.push(`<div class="list-users-module-box">`);
26
- ctx.push("</div>");
26
+ renderEmptyModuleContainer(ctx, "list-users-module-box");
27
27
  }
@@ -11,6 +11,7 @@
11
11
 
12
12
  import type { Module } from "@wdprlib/ast";
13
13
  import type { RenderContext } from "../../context";
14
+ import { renderEmptyModuleContainer } from "./empty-container";
14
15
 
15
16
  /**
16
17
  * Render a `[[module PageTree]]` element as an empty container.
@@ -22,6 +23,5 @@ export function renderPageTree(
22
23
  ctx: RenderContext,
23
24
  _data: Extract<Module, { module: "page-tree" }>,
24
25
  ): void {
25
- ctx.push(`<div class="page-tree-module-box">`);
26
- ctx.push("</div>");
26
+ renderEmptyModuleContainer(ctx, "page-tree-module-box");
27
27
  }
@@ -0,0 +1,10 @@
1
+ export function getRateWidgetParts(): string[] {
2
+ return [
3
+ `<div class="page-rate-widget-box">`,
4
+ `<span class="rate-points">rating:&nbsp;<span class="number prw54353">0</span></span>`,
5
+ `<span class="rateup btn btn-default"><a title="I like it" href="javascript:;">+</a></span>`,
6
+ `<span class="ratedown btn btn-default"><a title="I don't like it" href="javascript:;">&#8211;</a></span>`,
7
+ `<span class="cancel btn btn-default"><a title="Cancel my vote" href="javascript:;">x</a></span>`,
8
+ "</div>",
9
+ ];
10
+ }
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import type { RenderContext } from "../../context";
18
+ import { getRateWidgetParts } from "./rate-markup";
18
19
 
19
20
  /**
20
21
  * Render a `[[module Rate]]` page rating widget.
@@ -28,17 +29,7 @@ import type { RenderContext } from "../../context";
28
29
  * @param ctx - The current render context.
29
30
  */
30
31
  export function renderRate(ctx: RenderContext): void {
31
- ctx.push(`<div class="page-rate-widget-box">`);
32
- ctx.push(`<span class="rate-points">rating:&nbsp;<span class="number prw54353">0</span></span>`);
33
- ctx.push(
34
- `<span class="rateup btn btn-default"><a title="I like it" href="javascript:;">+</a></span>`,
35
- );
36
- // &#8211; is en-dash
37
- ctx.push(
38
- `<span class="ratedown btn btn-default"><a title="I don't like it" href="javascript:;">&#8211;</a></span>`,
39
- );
40
- ctx.push(
41
- `<span class="cancel btn btn-default"><a title="Cancel my vote" href="javascript:;">x</a></span>`,
42
- );
43
- ctx.push("</div>");
32
+ for (const part of getRateWidgetParts()) {
33
+ ctx.push(part);
34
+ }
44
35
  }
@@ -0,0 +1,11 @@
1
+ import type { Module } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+
4
+ export function renderUnknownModule(
5
+ ctx: RenderContext,
6
+ data: Extract<Module, { module: "unknown" }>,
7
+ ): void {
8
+ ctx.push(
9
+ `<div class="error-block">[[module <em>${data.name}</em>]] No such module, please <a href="https://www.wikidot.com/doc:modules" target="_blank" rel="noopener noreferrer">check available modules</a> and fix this page.</div>`,
10
+ );
11
+ }
@@ -0,0 +1,16 @@
1
+ import type { TabData } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+ import { syncHashMd5 } from "../../hash";
4
+
5
+ export function getTabViewWidgetId(
6
+ ctx: RenderContext,
7
+ tabs: TabData[],
8
+ tabViewIndex: number,
9
+ ): string {
10
+ const labelString = tabs.map((tab) => tab.label).join("");
11
+ return ctx.generateFixedId(`wiki-tabview-${tabViewIndex}-${syncHashMd5(labelString)}`);
12
+ }
13
+
14
+ export function getTabPanelId(ctx: RenderContext, tabViewIndex: number, tabIndex: number): string {
15
+ return ctx.generateId(`wiki-tab-${tabViewIndex}-`, tabIndex);
16
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ *
3
+ * Renderer for `[[tabview]]...[[/tabview]]` tab containers.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ import type { TabData } from "@wdprlib/ast";
9
+ import type { RenderContext } from "../../context";
10
+ import { renderTabPanels } from "./panels";
11
+ import { renderTabNavigation } from "./navigation";
12
+ import { getTabViewWidgetId } from "./ids";
13
+
14
+ /**
15
+ * Render a `[[tabview]]` element with YUI-compatible HTML structure.
16
+ *
17
+ * The first tab is selected by default (visible, with the `selected` class
18
+ * on its nav item). All other tabs have `display:none` on their content divs.
19
+ *
20
+ * @param ctx - The current render context.
21
+ * @param tabs - Array of tab data, each with a label and child elements.
22
+ */
23
+ export function renderTabView(ctx: RenderContext, tabs: TabData[]): void {
24
+ const tabViewIndex = ctx.nextTabViewIndex();
25
+ const widgetId = getTabViewWidgetId(ctx, tabs, tabViewIndex);
26
+
27
+ ctx.push(`<div id="${widgetId}" class="yui-navset">`);
28
+ renderTabNavigation(ctx, tabs);
29
+ renderTabPanels(ctx, tabs, tabViewIndex);
30
+ ctx.push("</div>");
31
+ }
@@ -0,0 +1,15 @@
1
+ import type { TabData } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+ import { escapeHtml } from "../../escape";
4
+
5
+ export function renderTabNavigation(ctx: RenderContext, tabs: TabData[]): void {
6
+ ctx.push(`<ul class="yui-nav">`);
7
+ for (let i = 0; i < tabs.length; i++) {
8
+ const tab = tabs[i]!;
9
+ const selectedClass = i === 0 ? ` class="selected"` : "";
10
+ ctx.push(`<li${selectedClass}>`);
11
+ ctx.push(`<a href="javascript:;"><em>${escapeHtml(tab.label)}</em></a>`);
12
+ ctx.push("</li>");
13
+ }
14
+ ctx.push("</ul>");
15
+ }
@@ -0,0 +1,16 @@
1
+ import type { TabData } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+ import { renderElements } from "../../render";
4
+ import { getTabPanelId } from "./ids";
5
+
6
+ export function renderTabPanels(ctx: RenderContext, tabs: TabData[], tabViewIndex: number): void {
7
+ ctx.push(`<div class="yui-content">`);
8
+ for (let i = 0; i < tabs.length; i++) {
9
+ const tab = tabs[i]!;
10
+ const displayStyle = i === 0 ? "" : ` style="display:none"`;
11
+ ctx.push(`<div id="${getTabPanelId(ctx, tabViewIndex, i)}"${displayStyle}>`);
12
+ renderElements(ctx, tab.elements);
13
+ ctx.push("</div>");
14
+ }
15
+ ctx.push("</div>");
16
+ }
@@ -0,0 +1,23 @@
1
+ import { escapeAttr, sanitizeAttributes } from "../../escape";
2
+
3
+ export { renderTableCellAttrs } from "./cell-attributes";
4
+
5
+ export function renderTableAttrs(attributes: Record<string, string>): string {
6
+ let hasRenderableAttributes = false;
7
+ for (const key in attributes) {
8
+ if (!key.startsWith("_")) {
9
+ hasRenderableAttributes = true;
10
+ break;
11
+ }
12
+ }
13
+ if (!hasRenderableAttributes) return "";
14
+
15
+ const safe = sanitizeAttributes(attributes);
16
+ let result = "";
17
+ for (const key in safe) {
18
+ if (key.startsWith("_")) continue;
19
+ const value = safe[key]!;
20
+ result += ` ${key}="${escapeAttr(value)}"`;
21
+ }
22
+ return result;
23
+ }
@@ -0,0 +1,62 @@
1
+ import type { TableCell } from "@wdprlib/ast";
2
+ import { escapeAttr, sanitizeAttributes } from "../../escape";
3
+
4
+ export function renderTableCellAttrs(cell: TableCell): string {
5
+ const attrs: string[] = [];
6
+ const safeCellAttrs = sanitizeAttributes(cell.attributes);
7
+
8
+ appendColumnSpan(attrs, cell);
9
+ appendRowSpan(attrs, safeCellAttrs);
10
+ appendAlignmentStyle(attrs, cell, safeCellAttrs);
11
+ appendRemainingCellAttributes(attrs, cell, safeCellAttrs);
12
+
13
+ return attrs.length > 0 ? " " + attrs.join(" ") : "";
14
+ }
15
+
16
+ function appendColumnSpan(attrs: string[], cell: TableCell): void {
17
+ if (cell["column-span"] > 1) {
18
+ attrs.push(`colspan="${cell["column-span"]}"`);
19
+ }
20
+ }
21
+
22
+ function appendRowSpan(attrs: string[], safeCellAttrs: Record<string, string>): void {
23
+ if (!safeCellAttrs.rowspan) {
24
+ return;
25
+ }
26
+
27
+ const rowspan = parseInt(safeCellAttrs.rowspan, 10);
28
+ if (rowspan > 1) {
29
+ attrs.push(`rowspan="${rowspan}"`);
30
+ }
31
+ }
32
+
33
+ function appendAlignmentStyle(
34
+ attrs: string[],
35
+ cell: TableCell,
36
+ safeCellAttrs: Record<string, string>,
37
+ ): void {
38
+ if (!cell.align) {
39
+ return;
40
+ }
41
+
42
+ const existingStyle = safeCellAttrs.style ?? "";
43
+ const alignStyle = `text-align: ${cell.align};`;
44
+ if (existingStyle) {
45
+ attrs.push(`style="${escapeAttr(existingStyle + "; " + alignStyle)}"`);
46
+ } else {
47
+ attrs.push(`style="${alignStyle}"`);
48
+ }
49
+ }
50
+
51
+ function appendRemainingCellAttributes(
52
+ attrs: string[],
53
+ cell: TableCell,
54
+ safeCellAttrs: Record<string, string>,
55
+ ): void {
56
+ for (const key in safeCellAttrs) {
57
+ if (key === "style" && cell.align) continue;
58
+ if (key === "rowspan") continue;
59
+ const value = safeCellAttrs[key]!;
60
+ attrs.push(`${key}="${escapeAttr(value)}"`);
61
+ }
62
+ }
@@ -0,0 +1,13 @@
1
+ import type { TableCell } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+ import { renderElements } from "../../render";
4
+ import { renderTableCellAttrs } from "./attributes";
5
+
6
+ export function renderTableCell(ctx: RenderContext, cell: TableCell): void {
7
+ const tag = cell.header ? "th" : "td";
8
+ const attrStr = renderTableCellAttrs(cell);
9
+
10
+ ctx.push(`<${tag}${attrStr}>`);
11
+ renderElements(ctx, cell.elements);
12
+ ctx.push(`</${tag}>`);
13
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ *
3
+ * Renderer for Wikidot table elements.
4
+ *
5
+ * @module
6
+ */
7
+
8
+ import type { TableData } from "@wdprlib/ast";
9
+ import type { RenderContext } from "../../context";
10
+ import { renderTableAttrs } from "./attributes";
11
+ import { renderTableCell } from "./cell";
12
+
13
+ export function renderTable(ctx: RenderContext, data: TableData): void {
14
+ const isPipeTable = data.attributes._source === "pipe";
15
+ const classAttr = isPipeTable ? ' class="wiki-content-table"' : "";
16
+ ctx.push(`<table${classAttr}${renderTableAttrs(data.attributes)}>`);
17
+
18
+ for (const row of data.rows) {
19
+ ctx.push(`<tr${renderTableAttrs(row.attributes)}>`);
20
+ for (const cell of row.cells) {
21
+ renderTableCell(ctx, cell);
22
+ }
23
+ ctx.push("</tr>");
24
+ }
25
+
26
+ ctx.push("</table>");
27
+ }
@@ -0,0 +1,20 @@
1
+ import type { RenderContext } from "../../context";
2
+ import { escapeAttr, escapeHtml, isValidEmail } from "../../escape";
3
+
4
+ /**
5
+ * Render an email address element as a `mailto:` link.
6
+ *
7
+ * The email is validated before creating the link. Invalid email addresses
8
+ * are rendered as plain escaped text to prevent `mailto:` injection.
9
+ *
10
+ * @param ctx - The current render context.
11
+ * @param email - The email address string.
12
+ */
13
+ export function renderEmail(ctx: RenderContext, email: string): void {
14
+ if (!isValidEmail(email)) {
15
+ ctx.pushEscaped(email);
16
+ return;
17
+ }
18
+
19
+ ctx.push(`<a href="mailto:${escapeAttr(email)}">${escapeHtml(email)}</a>`);
20
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ *
3
+ * Renderers for text-level AST nodes: plain text, raw/literal text,
4
+ * and email addresses.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ export { renderEmail } from "./email";
10
+ export { renderRaw } from "./raw";
11
+ export { renderText } from "./plain";
@@ -0,0 +1,11 @@
1
+ import type { RenderContext } from "../../context";
2
+
3
+ /**
4
+ * Render a plain text node by HTML-escaping and appending to the output.
5
+ *
6
+ * @param ctx - The current render context.
7
+ * @param data - The raw text content.
8
+ */
9
+ export function renderText(ctx: RenderContext, data: string): void {
10
+ ctx.pushEscaped(data);
11
+ }
@@ -0,0 +1,20 @@
1
+ import type { RenderContext } from "../../context";
2
+ import { escapeHtml } from "../../escape";
3
+
4
+ /**
5
+ * Render raw/literal text (Wikidot `@@...@@` syntax).
6
+ *
7
+ * Raw text is rendered inside a `<span style="white-space: pre-wrap;">` with
8
+ * spaces encoded as `&#32;` to preserve Wikidot's exact formatting. Empty
9
+ * strings produce no output.
10
+ *
11
+ * @param ctx - The current render context.
12
+ * @param data - The raw text content.
13
+ */
14
+ export function renderRaw(ctx: RenderContext, data: string): void {
15
+ if (data === "") return;
16
+
17
+ ctx.push(`<span style="white-space: pre-wrap;">`);
18
+ ctx.push(escapeHtml(data).replace(/ /g, "&#32;"));
19
+ ctx.push("</span>");
20
+ }
@@ -0,0 +1,12 @@
1
+ import type { RenderContext } from "../../context";
2
+ import { renderTocEntries } from "./entries";
3
+
4
+ export function renderTocBody(ctx: RenderContext): void {
5
+ ctx.push(
6
+ `<div id="toc-action-bar"><a href="javascript:;">Fold</a><a style="display: none" href="javascript:;">Unfold</a></div>`,
7
+ );
8
+ ctx.push(`<div class="title">Table of Contents</div>`);
9
+ ctx.push(`<div id="toc-list">`);
10
+ renderTocEntries(ctx, ctx.tocElements);
11
+ ctx.push("</div>");
12
+ }