@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,53 @@
1
+ import type { Element } from "@wdprlib/ast";
2
+ import type { RenderContext } from "../../context";
3
+ import { renderElement, renderElements } from "../../render";
4
+ import {
5
+ getParagraphIndices,
6
+ isLiCloseTextParagraph,
7
+ isParagraphElement,
8
+ type ParagraphElement,
9
+ } from "./paragraphs";
10
+ import { trimTextElements } from "./trim";
11
+
12
+ export function renderNoMarkerElements(ctx: RenderContext, elements: Element[]): void {
13
+ const trimmed = trimTextElements(elements);
14
+ if (trimmed.length === 0) return;
15
+
16
+ const paragraphIndices = getParagraphIndices(trimmed);
17
+ if (paragraphIndices.length === 0) {
18
+ renderElements(ctx, trimmed);
19
+ return;
20
+ }
21
+
22
+ const firstParagraphIdx = paragraphIndices[0]!;
23
+ const lastParagraphIdx = paragraphIndices[paragraphIndices.length - 1]!;
24
+
25
+ for (let i = 0; i < trimmed.length; i++) {
26
+ const el = trimmed[i]!;
27
+ if (isParagraphElement(el)) {
28
+ renderNoMarkerParagraph(ctx, el, i, firstParagraphIdx, lastParagraphIdx);
29
+ } else {
30
+ renderElement(ctx, el);
31
+ }
32
+ }
33
+ }
34
+
35
+ function renderNoMarkerParagraph(
36
+ ctx: RenderContext,
37
+ element: ParagraphElement,
38
+ index: number,
39
+ firstParagraphIdx: number,
40
+ lastParagraphIdx: number,
41
+ ): void {
42
+ if (
43
+ index === firstParagraphIdx ||
44
+ (index === lastParagraphIdx && isLiCloseTextParagraph(element))
45
+ ) {
46
+ renderElements(ctx, element.data.elements);
47
+ return;
48
+ }
49
+
50
+ ctx.push("<p>");
51
+ renderElements(ctx, element.data.elements);
52
+ ctx.push("</p>");
53
+ }
@@ -0,0 +1,34 @@
1
+ import type { ContainerData, Element } from "@wdprlib/ast";
2
+
3
+ export type ParagraphElement = Element & {
4
+ element: "container";
5
+ data: ContainerData & { type: "paragraph" };
6
+ };
7
+
8
+ export function getParagraphIndices(elements: Element[]): number[] {
9
+ const indices: number[] = [];
10
+
11
+ for (let i = 0; i < elements.length; i++) {
12
+ if (isParagraphElement(elements[i])) {
13
+ indices.push(i);
14
+ }
15
+ }
16
+
17
+ return indices;
18
+ }
19
+
20
+ export function isParagraphElement(element: Element | undefined): element is ParagraphElement {
21
+ return element?.element === "container" && element.data.type === "paragraph";
22
+ }
23
+
24
+ export function isLiCloseTextParagraph(element: ParagraphElement): boolean {
25
+ let combined = "";
26
+
27
+ for (const child of element.data.elements) {
28
+ if (child.element === "text") {
29
+ combined += child.data;
30
+ }
31
+ }
32
+
33
+ return combined.trim() === "[[/li]]";
34
+ }
@@ -0,0 +1,38 @@
1
+ import type { Element } from "@wdprlib/ast";
2
+
3
+ export function trimTextElements(elements: Element[]): Element[] {
4
+ if (elements.length === 0) return elements;
5
+
6
+ let start = 0;
7
+ let end = elements.length;
8
+
9
+ while (start < end) {
10
+ const el = elements[start]!;
11
+ if (el.element === "text" && typeof el.data === "string" && el.data.trim() === "") {
12
+ start++;
13
+ } else {
14
+ break;
15
+ }
16
+ }
17
+
18
+ while (end > start) {
19
+ const el = elements[end - 1]!;
20
+ if (el.element === "text" && typeof el.data === "string" && el.data.trim() === "") {
21
+ end--;
22
+ } else {
23
+ break;
24
+ }
25
+ }
26
+
27
+ if (start === 0 && end === elements.length) return elements;
28
+ return elements.slice(start, end);
29
+ }
30
+
31
+ export function hasNonWhitespaceElement(elements: Element[]): boolean {
32
+ for (const el of elements) {
33
+ if (el.element !== "text" || typeof el.data !== "string" || el.data.trim() !== "") {
34
+ return true;
35
+ }
36
+ }
37
+ return false;
38
+ }
@@ -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
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ *
3
+ * Renderer for `[[module Backlinks]]`.
4
+ *
5
+ * The backlinks module lists all pages that link to the current page.
6
+ * Because backlink data requires server-side queries, the renderer only
7
+ * outputs an empty container div. The actual content is populated at
8
+ * runtime or via server-side rendering.
9
+ *
10
+ * @module
11
+ */
12
+
13
+ import type { Module } from "@wdprlib/ast";
14
+ import type { RenderContext } from "../../context";
15
+ import { renderIndentedEmptyModuleContainer } from "./empty-container";
16
+
17
+ /**
18
+ * Render a `[[module Backlinks]]` element as an empty container.
19
+ *
20
+ * @param ctx - The current render context.
21
+ * @param _data - Backlinks module data (unused; the container is always empty).
22
+ */
23
+ export function renderBacklinks(
24
+ ctx: RenderContext,
25
+ _data: Extract<Module, { module: "backlinks" }>,
26
+ ): void {
27
+ // Wikidot outputs just the container div (backlinks are populated at runtime)
28
+ renderIndentedEmptyModuleContainer(ctx, "backlinks-module-box");
29
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ *
3
+ * Renderer for `[[module Categories]]`.
4
+ *
5
+ * The categories module displays the site's page categories. The
6
+ * renderer outputs an empty container div; category data is populated
7
+ * at runtime or via server-side rendering.
8
+ *
9
+ * @module
10
+ */
11
+
12
+ import type { Module } from "@wdprlib/ast";
13
+ import type { RenderContext } from "../../context";
14
+ import { renderEmptyModuleContainer } from "./empty-container";
15
+
16
+ /**
17
+ * Render a `[[module Categories]]` element as an empty container.
18
+ *
19
+ * @param ctx - The current render context.
20
+ * @param _data - Categories module data (unused; the container is always empty).
21
+ */
22
+ export function renderCategories(
23
+ ctx: RenderContext,
24
+ _data: Extract<Module, { module: "categories" }>,
25
+ ): void {
26
+ renderEmptyModuleContainer(ctx, "categories-module-box");
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
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ *
3
+ * Dispatcher for `[[module ModuleName]]` elements.
4
+ *
5
+ * Wikidot modules are server-side components that generate dynamic content.
6
+ * This renderer dispatches to the appropriate module-specific renderer based
7
+ * on the module name. Supported modules include Rate, Join, Backlinks,
8
+ * Categories, PageTree, ListPages, and ListUsers.
9
+ *
10
+ * Unknown module names produce a Wikidot-compatible error block with a
11
+ * link to the modules documentation page.
12
+ *
13
+ * @module
14
+ */
15
+
16
+ import type { Module } from "@wdprlib/ast";
17
+ import type { RenderContext } from "../../context";
18
+ import { renderBacklinks } from "./backlinks";
19
+ import { renderCategories } from "./categories";
20
+ import { renderJoin } from "./join";
21
+ import { renderPageTree } from "./page-tree";
22
+ import { renderRate } from "./rate";
23
+ import { renderListUsers } from "./listusers";
24
+ import { renderListPages } from "./listpages";
25
+ import { renderUnknownModule } from "./unknown";
26
+
27
+ /**
28
+ * Render a `[[module]]` element by dispatching on the module name.
29
+ *
30
+ * Each module outputs a container `<div>` with a module-specific CSS class.
31
+ * Some modules (like Rate and Join) render interactive UI elements; others
32
+ * (like Backlinks and ListPages) render empty containers that can be
33
+ * populated at runtime.
34
+ *
35
+ * @param ctx - The current render context.
36
+ * @param data - Module data with discriminated module type.
37
+ */
38
+ export function renderModule(ctx: RenderContext, data: Module): void {
39
+ switch (data.module) {
40
+ case "unknown":
41
+ renderUnknownModule(ctx, data);
42
+ break;
43
+ case "backlinks":
44
+ renderBacklinks(ctx, data);
45
+ break;
46
+ case "categories":
47
+ renderCategories(ctx, data);
48
+ break;
49
+ case "join":
50
+ renderJoin(ctx, data);
51
+ break;
52
+ case "page-tree":
53
+ renderPageTree(ctx, data);
54
+ break;
55
+ case "rate":
56
+ renderRate(ctx);
57
+ break;
58
+ case "list-users":
59
+ renderListUsers(ctx, data);
60
+ break;
61
+ case "list-pages":
62
+ renderListPages(ctx, data);
63
+ break;
64
+ }
65
+ }
@@ -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
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ *
3
+ * Renderer for `[[module Join]]`.
4
+ *
5
+ * The Join module displays a button that allows users to request
6
+ * membership in the wiki site. The button text can be customized
7
+ * via the `button-text` attribute; it defaults to "Join".
8
+ *
9
+ * @module
10
+ */
11
+
12
+ import type { Module } from "@wdprlib/ast";
13
+ import type { RenderContext } from "../../context";
14
+ import { renderJoinMarkup } from "./join-markup";
15
+
16
+ /**
17
+ * Render a `[[module Join]]` element with a clickable join button.
18
+ *
19
+ * The button is wrapped in a `<div>` with a configurable CSS class
20
+ * (defaults to `"join-box"`). The runtime `join` module attaches
21
+ * click handling via the `onJoin` callback.
22
+ *
23
+ * @param ctx - The current render context.
24
+ * @param data - Join module data with optional `button-text` and CSS class.
25
+ */
26
+ export function renderJoin(ctx: RenderContext, data: Extract<Module, { module: "join" }>): void {
27
+ ctx.push(renderJoinMarkup(data));
28
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ *
3
+ * Renderer for `[[module ListPages]]`.
4
+ *
5
+ * The ListPages module queries and displays a filtered list of wiki pages.
6
+ * Because the query results require server-side data, the renderer outputs
7
+ * an empty container div that can be populated at runtime.
8
+ *
9
+ * @module
10
+ */
11
+
12
+ import type { Module } from "@wdprlib/ast";
13
+ import type { RenderContext } from "../../context";
14
+ import { renderEmptyModuleContainer } from "./empty-container";
15
+
16
+ /**
17
+ * Render a `[[module ListPages]]` element as an empty container.
18
+ *
19
+ * @param ctx - The current render context.
20
+ * @param _data - ListPages module data (unused; the container is always empty).
21
+ */
22
+ export function renderListPages(
23
+ ctx: RenderContext,
24
+ _data: Extract<Module, { module: "list-pages" }>,
25
+ ): void {
26
+ renderEmptyModuleContainer(ctx, "list-pages-box");
27
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ *
3
+ * Renderer for `[[module ListUsers]]`.
4
+ *
5
+ * The ListUsers module displays a filtered list of site members.
6
+ * The renderer outputs an empty container div that can be populated
7
+ * at runtime or via server-side rendering.
8
+ *
9
+ * @module
10
+ */
11
+
12
+ import type { Module } from "@wdprlib/ast";
13
+ import type { RenderContext } from "../../context";
14
+ import { renderEmptyModuleContainer } from "./empty-container";
15
+
16
+ /**
17
+ * Render a `[[module ListUsers]]` element as an empty container.
18
+ *
19
+ * @param ctx - The current render context.
20
+ * @param _data - ListUsers module data (unused; the container is always empty).
21
+ */
22
+ export function renderListUsers(
23
+ ctx: RenderContext,
24
+ _data: Extract<Module, { module: "list-users" }>,
25
+ ): void {
26
+ renderEmptyModuleContainer(ctx, "list-users-module-box");
27
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ *
3
+ * Renderer for `[[module PageTree]]`.
4
+ *
5
+ * The PageTree module displays a hierarchical tree of child pages.
6
+ * The renderer outputs an empty container div; the tree structure
7
+ * is populated at runtime or via server-side rendering.
8
+ *
9
+ * @module
10
+ */
11
+
12
+ import type { Module } from "@wdprlib/ast";
13
+ import type { RenderContext } from "../../context";
14
+ import { renderEmptyModuleContainer } from "./empty-container";
15
+
16
+ /**
17
+ * Render a `[[module PageTree]]` element as an empty container.
18
+ *
19
+ * @param ctx - The current render context.
20
+ * @param _data - PageTree module data (unused; the container is always empty).
21
+ */
22
+ export function renderPageTree(
23
+ ctx: RenderContext,
24
+ _data: Extract<Module, { module: "page-tree" }>,
25
+ ): void {
26
+ renderEmptyModuleContainer(ctx, "page-tree-module-box");
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
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ *
3
+ * Renderer for `[[module Rate]]`.
4
+ *
5
+ * The Rate module displays a page rating widget with upvote, downvote,
6
+ * and cancel buttons. The initial score is rendered as 0; the runtime
7
+ * `rate` module handles click events and updates the display via the
8
+ * `onRate` callback.
9
+ *
10
+ * The widget HTML structure matches Wikidot's original output, using
11
+ * Bootstrap-style `btn btn-default` classes alongside Wikidot-specific
12
+ * class names (`rateup`, `ratedown`, `cancel`, `rate-points`).
13
+ *
14
+ * @module
15
+ */
16
+
17
+ import type { RenderContext } from "../../context";
18
+ import { getRateWidgetParts } from "./rate-markup";
19
+
20
+ /**
21
+ * Render a `[[module Rate]]` page rating widget.
22
+ *
23
+ * Outputs a `<div class="page-rate-widget-box">` containing:
24
+ * - A score display span (`.rate-points > .number`)
25
+ * - An upvote button (`.rateup`)
26
+ * - A downvote button (`.ratedown`) using an en-dash character
27
+ * - A cancel button (`.cancel`)
28
+ *
29
+ * @param ctx - The current render context.
30
+ */
31
+ export function renderRate(ctx: RenderContext): void {
32
+ for (const part of getRateWidgetParts()) {
33
+ ctx.push(part);
34
+ }
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
+ }