@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.
- package/dist/index.cjs +2332 -2032
- package/dist/index.d.cts +15 -13
- package/dist/index.d.ts +15 -13
- package/dist/index.js +2336 -2036
- package/package.json +5 -3
- package/src/context/attributes.ts +14 -0
- package/src/context/bibliography.ts +109 -0
- package/src/context/counters.ts +51 -0
- package/src/context/image-urls.ts +31 -0
- package/src/context/index.ts +285 -0
- package/src/context/output.ts +17 -0
- package/src/context/page-urls.ts +81 -0
- package/src/context/style-slots.ts +29 -0
- package/src/context/urls.ts +2 -0
- package/src/elements/bibliography/block.ts +27 -0
- package/src/elements/bibliography/cite.ts +23 -0
- package/src/elements/bibliography/ids.ts +9 -0
- package/src/elements/bibliography/index.ts +9 -0
- package/src/elements/clear-float.ts +27 -0
- package/src/elements/code/contents.ts +18 -0
- package/src/elements/code/index.ts +29 -0
- package/src/elements/collapsible/index.ts +31 -0
- package/src/elements/collapsible/labels.ts +35 -0
- package/src/elements/collapsible/link.ts +11 -0
- package/src/elements/collapsible/sections.ts +39 -0
- package/src/elements/color.ts +32 -0
- package/src/elements/container/attributes.ts +28 -0
- package/src/elements/container/header.ts +27 -0
- package/src/elements/container/index.ts +35 -0
- package/src/elements/container/string-container.ts +40 -0
- package/src/elements/container/string-types.ts +63 -0
- package/src/elements/container/wrappers.ts +32 -0
- package/src/elements/date/format.ts +20 -0
- package/src/elements/date/index.ts +34 -0
- package/src/elements/date/output.ts +6 -0
- package/src/elements/embed/iframe.ts +8 -0
- package/src/elements/embed/index.ts +28 -0
- package/src/elements/embed/providers.ts +43 -0
- package/src/elements/embed/validation.ts +15 -0
- package/src/elements/embed-block/allowlist.ts +60 -0
- package/src/elements/embed-block/boolean-attributes.ts +38 -0
- package/src/elements/embed-block/iframe.ts +33 -0
- package/src/elements/embed-block/index.ts +31 -0
- package/src/elements/embed-block/sanitize-config.ts +22 -0
- package/src/elements/embed-block/sanitize.ts +44 -0
- package/src/elements/expr/branch.ts +29 -0
- package/src/elements/expr/index.ts +63 -0
- package/src/elements/expr/result.ts +19 -0
- package/src/elements/footnote/body.ts +11 -0
- package/src/elements/footnote/index.ts +35 -0
- package/src/elements/footnote/ref.ts +16 -0
- package/src/elements/html/attributes.ts +24 -0
- package/src/elements/html/index.ts +39 -0
- package/src/elements/html/url.ts +19 -0
- package/src/elements/iframe/attributes.ts +28 -0
- package/src/elements/iframe/index.ts +22 -0
- package/src/elements/iftags/condition.ts +42 -0
- package/src/elements/iftags/index.ts +39 -0
- package/src/elements/iftags/style-slot.ts +23 -0
- package/src/elements/iftags/tokens.ts +36 -0
- package/src/elements/image/alignment.ts +44 -0
- package/src/elements/image/attributes.ts +10 -0
- package/src/elements/image/img-attributes.ts +26 -0
- package/src/elements/image/index.ts +36 -0
- package/src/elements/image/link-href.ts +24 -0
- package/src/elements/image/link.ts +13 -0
- package/src/elements/image/source.ts +16 -0
- package/src/elements/include/index.ts +35 -0
- package/src/elements/include/missing.ts +15 -0
- package/src/elements/index.ts +35 -0
- package/src/elements/line-break.ts +22 -0
- package/src/elements/link/anchor-name.ts +6 -0
- package/src/elements/link/anchor.ts +27 -0
- package/src/elements/link/attributes.ts +47 -0
- package/src/elements/link/index.ts +26 -0
- package/src/elements/link/label.ts +23 -0
- package/src/elements/link/target.ts +20 -0
- package/src/elements/list/attributes.ts +19 -0
- package/src/elements/list/definition-list.ts +16 -0
- package/src/elements/list/index.ts +48 -0
- package/src/elements/list/item-rendering.ts +38 -0
- package/src/elements/list/items.ts +61 -0
- package/src/elements/list/no-marker.ts +53 -0
- package/src/elements/list/paragraphs.ts +34 -0
- package/src/elements/list/trim.ts +38 -0
- package/src/elements/math/block.ts +29 -0
- package/src/elements/math/equation-ref.ts +12 -0
- package/src/elements/math/index.ts +14 -0
- package/src/elements/math/inline.ts +19 -0
- package/src/elements/math/latex.ts +27 -0
- package/src/elements/math/source.ts +18 -0
- package/src/elements/module/backlinks.ts +29 -0
- package/src/elements/module/categories.ts +27 -0
- package/src/elements/module/empty-container.ts +10 -0
- package/src/elements/module/index.ts +65 -0
- package/src/elements/module/join-markup.ts +10 -0
- package/src/elements/module/join.ts +28 -0
- package/src/elements/module/listpages.ts +27 -0
- package/src/elements/module/listusers.ts +27 -0
- package/src/elements/module/page-tree.ts +27 -0
- package/src/elements/module/rate-markup.ts +10 -0
- package/src/elements/module/rate.ts +35 -0
- package/src/elements/module/unknown.ts +11 -0
- package/src/elements/tab-view/ids.ts +16 -0
- package/src/elements/tab-view/index.ts +31 -0
- package/src/elements/tab-view/navigation.ts +15 -0
- package/src/elements/tab-view/panels.ts +16 -0
- package/src/elements/table/attributes.ts +23 -0
- package/src/elements/table/cell-attributes.ts +62 -0
- package/src/elements/table/cell.ts +13 -0
- package/src/elements/table/index.ts +27 -0
- package/src/elements/text/email.ts +20 -0
- package/src/elements/text/index.ts +11 -0
- package/src/elements/text/plain.ts +11 -0
- package/src/elements/text/raw.ts +20 -0
- package/src/elements/toc/body.ts +12 -0
- package/src/elements/toc/entries.ts +34 -0
- package/src/elements/toc/frame.ts +27 -0
- package/src/elements/toc/index.ts +17 -0
- package/src/elements/toc/link.ts +26 -0
- package/src/elements/user/index.ts +40 -0
- package/src/elements/user/markup.ts +34 -0
- package/src/elements/user/resolve.ts +6 -0
- package/src/escape/attribute-allowlists.ts +101 -0
- package/src/escape/attributes.ts +62 -0
- package/src/escape/css-color-functions.ts +18 -0
- package/src/escape/css-colors.ts +183 -0
- package/src/escape/css-danger.ts +22 -0
- package/src/escape/css-normalize.ts +54 -0
- package/src/escape/css-style.ts +78 -0
- package/src/escape/css-urls.ts +76 -0
- package/src/escape/css.ts +4 -0
- package/src/escape/email.ts +22 -0
- package/src/escape/html.ts +68 -0
- package/src/escape/index.ts +15 -0
- package/src/escape/url.ts +18 -0
- package/src/hash.ts +62 -0
- package/src/index.ts +26 -0
- package/src/libs/highlighter/engine/end-pattern.ts +26 -0
- package/src/libs/highlighter/engine/html.ts +19 -0
- package/src/libs/highlighter/engine/index.ts +3 -0
- package/src/libs/highlighter/engine/keywords.ts +22 -0
- package/src/libs/highlighter/engine/parts.ts +36 -0
- package/src/libs/highlighter/engine/preprocess.ts +10 -0
- package/src/libs/highlighter/engine/render.ts +31 -0
- package/src/libs/highlighter/engine/token.ts +7 -0
- package/src/libs/highlighter/engine/tokenizer.ts +266 -0
- package/src/libs/highlighter/engine/utils.ts +38 -0
- package/src/libs/highlighter/index.ts +70 -0
- package/src/libs/highlighter/languages/cpp.ts +345 -0
- package/src/libs/highlighter/languages/css.ts +104 -0
- package/src/libs/highlighter/languages/diff.ts +154 -0
- package/src/libs/highlighter/languages/dtd.ts +99 -0
- package/src/libs/highlighter/languages/html.ts +59 -0
- package/src/libs/highlighter/languages/java.ts +251 -0
- package/src/libs/highlighter/languages/javascript.ts +213 -0
- package/src/libs/highlighter/languages/php.ts +433 -0
- package/src/libs/highlighter/languages/python.ts +308 -0
- package/src/libs/highlighter/languages/ruby.ts +360 -0
- package/src/libs/highlighter/languages/sql.ts +125 -0
- package/src/libs/highlighter/languages/xml.ts +68 -0
- package/src/libs/highlighter/types.ts +44 -0
- package/src/render/collected-styles.ts +22 -0
- package/src/render/dispatch.ts +181 -0
- package/src/render/index.ts +28 -0
- package/src/render/primitives.ts +17 -0
- package/src/render/style-tag.ts +6 -0
- package/src/render/style.ts +15 -0
- package/src/types.ts +144 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wdprlib/render",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "HTML renderer for Wikidot markup",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"html",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"directory": "packages/render"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
-
"dist"
|
|
19
|
+
"dist",
|
|
20
|
+
"src"
|
|
20
21
|
],
|
|
21
22
|
"type": "module",
|
|
22
23
|
"sideEffects": false,
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
"types": "./dist/index.d.ts",
|
|
26
27
|
"exports": {
|
|
27
28
|
".": {
|
|
29
|
+
"bun": "./src/index.ts",
|
|
28
30
|
"import": {
|
|
29
31
|
"types": "./dist/index.d.ts",
|
|
30
32
|
"default": "./dist/index.js"
|
|
@@ -39,7 +41,7 @@
|
|
|
39
41
|
"access": "public"
|
|
40
42
|
},
|
|
41
43
|
"dependencies": {
|
|
42
|
-
"@wdprlib/ast": "2.
|
|
44
|
+
"@wdprlib/ast": "2.1.0",
|
|
43
45
|
"domhandler": "^5.0.3",
|
|
44
46
|
"htmlparser2": "^10.0.0",
|
|
45
47
|
"sanitize-html": "^2.14.0",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { escapeAttr, sanitizeAttributes } from "../escape";
|
|
2
|
+
|
|
3
|
+
export function renderAttributeString(attributes: Record<string, string>): string {
|
|
4
|
+
const safe = sanitizeAttributes(attributes);
|
|
5
|
+
let result = "";
|
|
6
|
+
for (const [key, value] of Object.entries(safe)) {
|
|
7
|
+
if (value !== "") {
|
|
8
|
+
result += ` ${key}="${escapeAttr(value)}"`;
|
|
9
|
+
} else {
|
|
10
|
+
result += ` ${key}=""`;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return result;
|
|
14
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { BibliographyBlockData, Element, ListData, TableData, TabData } from "@wdprlib/ast";
|
|
2
|
+
|
|
3
|
+
interface BibliographyState {
|
|
4
|
+
map: Map<string, number>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class BibliographyIndex {
|
|
8
|
+
private state: BibliographyState | null = null;
|
|
9
|
+
|
|
10
|
+
constructor(private readonly elements: Element[]) {}
|
|
11
|
+
|
|
12
|
+
getCitationNumber(label: string): number | undefined {
|
|
13
|
+
return this.getState().map.get(label);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private getState(): BibliographyState {
|
|
17
|
+
if (this.state === null) {
|
|
18
|
+
this.state = collectBibliographyState(this.elements);
|
|
19
|
+
}
|
|
20
|
+
return this.state;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function collectBibliographyState(elements: Element[]): BibliographyState {
|
|
25
|
+
const state: BibliographyState = {
|
|
26
|
+
map: new Map(),
|
|
27
|
+
};
|
|
28
|
+
collectFromElements(elements, state);
|
|
29
|
+
return state;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function collectFromElements(elements: Element[], state: BibliographyState): void {
|
|
33
|
+
for (const element of elements) {
|
|
34
|
+
if (element.element === "bibliography-block") {
|
|
35
|
+
addBlockEntries(element.data, state);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
collectFromChildren(element, state);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function addBlockEntries(data: BibliographyBlockData, state: BibliographyState): void {
|
|
43
|
+
for (const entry of data.entries) {
|
|
44
|
+
if (state.map.has(entry.key_string)) continue;
|
|
45
|
+
|
|
46
|
+
state.map.set(entry.key_string, state.map.size + 1);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function collectFromChildren(element: Element, state: BibliographyState): void {
|
|
51
|
+
switch (element.element) {
|
|
52
|
+
case "list":
|
|
53
|
+
collectFromList(element.data, state);
|
|
54
|
+
return;
|
|
55
|
+
case "table":
|
|
56
|
+
collectFromTable(element.data, state);
|
|
57
|
+
return;
|
|
58
|
+
case "definition-list":
|
|
59
|
+
for (const item of element.data) {
|
|
60
|
+
collectFromElements(item.key, state);
|
|
61
|
+
collectFromElements(item.value, state);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
case "tab-view":
|
|
65
|
+
collectFromTabs(element.data, state);
|
|
66
|
+
return;
|
|
67
|
+
default:
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (hasElementChildren(element)) {
|
|
72
|
+
collectFromElements(element.data.elements, state);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function collectFromList(data: ListData, state: BibliographyState): void {
|
|
77
|
+
for (const item of data.items) {
|
|
78
|
+
if (item["item-type"] === "elements") {
|
|
79
|
+
collectFromElements(item.elements, state);
|
|
80
|
+
} else {
|
|
81
|
+
collectFromList(item.data, state);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function collectFromTable(data: TableData, state: BibliographyState): void {
|
|
87
|
+
for (const row of data.rows) {
|
|
88
|
+
for (const cell of row.cells) {
|
|
89
|
+
collectFromElements(cell.elements, state);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function collectFromTabs(tabs: TabData[], state: BibliographyState): void {
|
|
95
|
+
for (const tab of tabs) {
|
|
96
|
+
collectFromElements(tab.elements, state);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function hasElementChildren(
|
|
101
|
+
element: Element,
|
|
102
|
+
): element is Element & { data: { elements: Element[] } } {
|
|
103
|
+
if (!("data" in element)) return false;
|
|
104
|
+
|
|
105
|
+
const data: unknown = element.data;
|
|
106
|
+
return (
|
|
107
|
+
data !== null && typeof data === "object" && "elements" in data && Array.isArray(data.elements)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export class RenderCounters {
|
|
2
|
+
private tocIndex = 0;
|
|
3
|
+
private footnoteIndex = 0;
|
|
4
|
+
private equationIndex = 0;
|
|
5
|
+
private htmlBlockIndex = 0;
|
|
6
|
+
private bibciteCounter = 0;
|
|
7
|
+
private tabViewIndex = 0;
|
|
8
|
+
private readonly idSuffix: string | null;
|
|
9
|
+
|
|
10
|
+
constructor(useTrueIds: boolean) {
|
|
11
|
+
this.idSuffix = useTrueIds ? null : Math.random().toString(16).slice(2, 8);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
nextTocIndex(): number {
|
|
15
|
+
return this.tocIndex++;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
nextFootnoteIndex(): number {
|
|
19
|
+
return this.footnoteIndex++;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
nextEquationIndex(): number {
|
|
23
|
+
return this.equationIndex++;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
nextHtmlBlockIndex(): number {
|
|
27
|
+
return this.htmlBlockIndex++;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
nextBibciteCounter(): number {
|
|
31
|
+
return ++this.bibciteCounter;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
nextTabViewIndex(): number {
|
|
35
|
+
return this.tabViewIndex++;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
generateId(prefix: string, index: number | string): string {
|
|
39
|
+
if (this.idSuffix === null) {
|
|
40
|
+
return `${prefix}${index}`;
|
|
41
|
+
}
|
|
42
|
+
return `${prefix}${index}-${this.idSuffix}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
generateFixedId(name: string): string {
|
|
46
|
+
if (this.idSuffix === null) {
|
|
47
|
+
return name;
|
|
48
|
+
}
|
|
49
|
+
return `${name}-${this.idSuffix}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ImageSource, WikitextSettings } from "@wdprlib/ast";
|
|
2
|
+
import type { PageContext } from "../types";
|
|
3
|
+
|
|
4
|
+
export function resolveImageSource(
|
|
5
|
+
source: ImageSource,
|
|
6
|
+
settings: WikitextSettings,
|
|
7
|
+
page: PageContext | undefined,
|
|
8
|
+
): string | null {
|
|
9
|
+
const pageName = page?.pageName;
|
|
10
|
+
switch (source.type) {
|
|
11
|
+
case "url": {
|
|
12
|
+
const url = source.data;
|
|
13
|
+
if (url.startsWith("/") && !url.startsWith("//")) {
|
|
14
|
+
if (!settings.allowLocalPaths) return null;
|
|
15
|
+
return `/local--files${url}`;
|
|
16
|
+
}
|
|
17
|
+
return url;
|
|
18
|
+
}
|
|
19
|
+
case "file1":
|
|
20
|
+
if (!settings.allowLocalPaths) return null;
|
|
21
|
+
return pageName
|
|
22
|
+
? `/local--files/${pageName}/${source.data.file}`
|
|
23
|
+
: `/local--files/${source.data.file}`;
|
|
24
|
+
case "file2":
|
|
25
|
+
if (!settings.allowLocalPaths) return null;
|
|
26
|
+
return `/local--files/${source.data.page}/${source.data.file}`;
|
|
27
|
+
case "file3":
|
|
28
|
+
if (!settings.allowLocalPaths) return null;
|
|
29
|
+
return `/local--files/${source.data.site}/${source.data.page}/${source.data.file}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* Central rendering context that tracks state during a single HTML render pass.
|
|
4
|
+
*
|
|
5
|
+
* Every render invocation creates one {@link RenderContext} instance, which serves
|
|
6
|
+
* as both an output buffer and a registry for sequential counters (footnotes,
|
|
7
|
+
* TOC headings, equations, HTML blocks, bibliography citations). The context
|
|
8
|
+
* also exposes helpers for resolving image sources, page links, and
|
|
9
|
+
* HTML attribute maps -- operations that depend on the current
|
|
10
|
+
* {@link WikitextSettings} and {@link PageContext}.
|
|
11
|
+
*
|
|
12
|
+
* @module
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type {
|
|
16
|
+
Element,
|
|
17
|
+
ImageSource,
|
|
18
|
+
LinkLocation,
|
|
19
|
+
SyntaxTree,
|
|
20
|
+
WikitextSettings,
|
|
21
|
+
} from "@wdprlib/ast";
|
|
22
|
+
import { DEFAULT_SETTINGS } from "@wdprlib/ast";
|
|
23
|
+
import type { RenderOptions, PageContext } from "../types";
|
|
24
|
+
import { renderAttributeString } from "./attributes";
|
|
25
|
+
import { BibliographyIndex } from "./bibliography";
|
|
26
|
+
import { RenderCounters } from "./counters";
|
|
27
|
+
import { RenderOutputBuffer } from "./output";
|
|
28
|
+
import { StyleSlotState } from "./style-slots";
|
|
29
|
+
import {
|
|
30
|
+
resolveImageSource as resolveImageSourceUrl,
|
|
31
|
+
resolvePageLink as resolvePageLinkUrl,
|
|
32
|
+
} from "./urls";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Manages rendering state and accumulates HTML output for a single render pass.
|
|
36
|
+
*
|
|
37
|
+
* The context is created once per call to `renderToHtml` and threaded through
|
|
38
|
+
* every element renderer. It provides:
|
|
39
|
+
*
|
|
40
|
+
* - An HTML output buffer ({@link push}, {@link pushEscaped}, {@link getOutput})
|
|
41
|
+
* - Sequential counters for footnotes, TOC entries, equations, etc.
|
|
42
|
+
* - ID generation that optionally appends a random suffix to avoid collisions
|
|
43
|
+
* when multiple rendered fragments coexist on the same page
|
|
44
|
+
* - Resolution of {@link ImageSource} and {@link LinkLocation} values into
|
|
45
|
+
* concrete URLs, applying Wikidot page-name normalization rules
|
|
46
|
+
* - Attribute rendering with XSS sanitization
|
|
47
|
+
* - A lazy bibliography citation index built from `bibliography-block`
|
|
48
|
+
* elements when a citation is rendered
|
|
49
|
+
*/
|
|
50
|
+
export class RenderContext {
|
|
51
|
+
/** Accumulated HTML fragments; joined by {@link getOutput}. */
|
|
52
|
+
private output = new RenderOutputBuffer();
|
|
53
|
+
/**
|
|
54
|
+
* When true, style elements in the AST are rendered rather than
|
|
55
|
+
* silently skipped. Set while rendering children of unresolved
|
|
56
|
+
* `[[iftags]]` blocks whose styles were not collected during resolve.
|
|
57
|
+
*/
|
|
58
|
+
renderInlineStyles = false;
|
|
59
|
+
/** State for style slots collected while rendering unresolved `[[iftags]]` blocks. */
|
|
60
|
+
private _styleSlots = new StyleSlotState();
|
|
61
|
+
/** Sequential counters and ID suffix state for this render pass. */
|
|
62
|
+
private counters: RenderCounters;
|
|
63
|
+
|
|
64
|
+
/** Merged wikitext settings (page-mode defaults when omitted). */
|
|
65
|
+
readonly settings: WikitextSettings;
|
|
66
|
+
/** Full render options supplied by the caller. */
|
|
67
|
+
readonly options: RenderOptions;
|
|
68
|
+
/** Footnote element arrays collected from the syntax tree. */
|
|
69
|
+
readonly footnotes: Element[][];
|
|
70
|
+
/** CSS `<style>` blocks extracted from the syntax tree. */
|
|
71
|
+
readonly styles: string[];
|
|
72
|
+
/** Raw HTML strings for `[[html]]` blocks, indexed by insertion order. */
|
|
73
|
+
readonly htmlBlocks: string[];
|
|
74
|
+
/** Pre-built TOC element tree for `[[toc]]` rendering. */
|
|
75
|
+
readonly tocElements: Element[];
|
|
76
|
+
/** Lazily built bibliography lookup data. */
|
|
77
|
+
private readonly bibliography: BibliographyIndex;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create a new render context from a parsed syntax tree.
|
|
81
|
+
*
|
|
82
|
+
* @param tree - The syntax tree produced by the parser. Footnotes,
|
|
83
|
+
* styles, html-blocks, and table-of-contents data are extracted from
|
|
84
|
+
* the tree and stored for later use by element renderers.
|
|
85
|
+
* @param options - Caller-supplied render configuration. Missing fields
|
|
86
|
+
* fall back to safe defaults.
|
|
87
|
+
*/
|
|
88
|
+
constructor(tree: SyntaxTree, options: RenderOptions = {}) {
|
|
89
|
+
this.settings = options.settings ?? DEFAULT_SETTINGS;
|
|
90
|
+
this.counters = new RenderCounters(this.settings.useTrueIds);
|
|
91
|
+
this.options = options;
|
|
92
|
+
this.footnotes = options.footnotes ?? tree.footnotes ?? [];
|
|
93
|
+
this.styles = tree.styles ?? [];
|
|
94
|
+
this.htmlBlocks = tree["html-blocks"] ?? [];
|
|
95
|
+
this.tocElements = tree["table-of-contents"] ?? [];
|
|
96
|
+
|
|
97
|
+
this.bibliography = new BibliographyIndex(tree.elements);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Return the 1-indexed bibliography citation number for a label, if defined. */
|
|
101
|
+
getBibliographyCitationNumber(label: string): number | undefined {
|
|
102
|
+
return this.bibliography.getCitationNumber(label);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Append a raw HTML string to the output buffer without escaping.
|
|
107
|
+
*
|
|
108
|
+
* @param html - Trusted HTML fragment to append.
|
|
109
|
+
*/
|
|
110
|
+
push(html: string): void {
|
|
111
|
+
this.output.push(html);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* HTML-escape the given text and append it to the output buffer.
|
|
116
|
+
*
|
|
117
|
+
* @param text - Untrusted text content (will be entity-escaped).
|
|
118
|
+
*/
|
|
119
|
+
pushEscaped(text: string): void {
|
|
120
|
+
this.output.pushEscaped(text);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Join all buffered HTML fragments and return the final HTML string.
|
|
125
|
+
*
|
|
126
|
+
* @returns The complete rendered HTML output.
|
|
127
|
+
*/
|
|
128
|
+
getOutput(): string {
|
|
129
|
+
return this.output.getOutput();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Enter a style slot: subsequent {@link pushToStyleSlot} calls collect into this slot. */
|
|
133
|
+
enterStyleSlot(slotId: number): void {
|
|
134
|
+
this._styleSlots.enter(slotId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Exit the current style slot. */
|
|
138
|
+
exitStyleSlot(): void {
|
|
139
|
+
this._styleSlots.exit();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Whether a style slot is currently active. */
|
|
143
|
+
hasActiveStyleSlot(): boolean {
|
|
144
|
+
return this._styleSlots.hasActiveSlot();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Push a CSS string into the active style slot. */
|
|
148
|
+
pushToStyleSlot(css: string): void {
|
|
149
|
+
this._styleSlots.push(css);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Retrieve collected CSS strings for a given style slot. */
|
|
153
|
+
getStyleSlotContents(slotId: number): string[] {
|
|
154
|
+
return this._styleSlots.getContents(slotId);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Return the current TOC heading index and advance the counter.
|
|
159
|
+
*
|
|
160
|
+
* @returns The index before incrementing (0-based).
|
|
161
|
+
*/
|
|
162
|
+
nextTocIndex(): number {
|
|
163
|
+
return this.counters.nextTocIndex();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Return the current footnote index and advance the counter.
|
|
168
|
+
*
|
|
169
|
+
* @returns The index before incrementing (0-based).
|
|
170
|
+
*/
|
|
171
|
+
nextFootnoteIndex(): number {
|
|
172
|
+
return this.counters.nextFootnoteIndex();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Return the current equation index and advance the counter.
|
|
177
|
+
*
|
|
178
|
+
* @returns The index before incrementing (0-based).
|
|
179
|
+
*/
|
|
180
|
+
nextEquationIndex(): number {
|
|
181
|
+
return this.counters.nextEquationIndex();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Return the current HTML block index and advance the counter.
|
|
186
|
+
*
|
|
187
|
+
* @returns The index before incrementing (0-based).
|
|
188
|
+
*/
|
|
189
|
+
nextHtmlBlockIndex(): number {
|
|
190
|
+
return this.counters.nextHtmlBlockIndex();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Advance the bibliography citation counter and return the new value.
|
|
195
|
+
* Used to generate unique `bibcite-N-XXXXX` element IDs.
|
|
196
|
+
*
|
|
197
|
+
* @returns The counter value after incrementing (1-based).
|
|
198
|
+
*/
|
|
199
|
+
nextBibciteCounter(): number {
|
|
200
|
+
return this.counters.nextBibciteCounter();
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Return the current tab-view index and advance the counter.
|
|
205
|
+
*
|
|
206
|
+
* @returns The index before incrementing (0-based).
|
|
207
|
+
*/
|
|
208
|
+
nextTabViewIndex(): number {
|
|
209
|
+
return this.counters.nextTabViewIndex();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Generate an element ID.
|
|
214
|
+
* When useTrueIds is true, returns `${prefix}${index}`.
|
|
215
|
+
* When false, appends a random suffix to prevent collisions across fragments.
|
|
216
|
+
*/
|
|
217
|
+
generateId(prefix: string, index: number | string): string {
|
|
218
|
+
return this.counters.generateId(prefix, index);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Generate a fixed element ID (no index).
|
|
223
|
+
* When useTrueIds is false, appends a random suffix.
|
|
224
|
+
*/
|
|
225
|
+
generateFixedId(name: string): string {
|
|
226
|
+
return this.counters.generateFixedId(name);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The page context for the current render, if provided.
|
|
231
|
+
* Contains page name, site, tags, and a page-existence checker.
|
|
232
|
+
*/
|
|
233
|
+
get page(): PageContext | undefined {
|
|
234
|
+
return this.options.page;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Resolve an {@link ImageSource} to a concrete `src` URL string.
|
|
239
|
+
*
|
|
240
|
+
* Wikidot supports several image source forms:
|
|
241
|
+
* - `url` -- a direct URL or local path
|
|
242
|
+
* - `file1` -- a file attached to the current page (`/local--files/{page}/{file}`)
|
|
243
|
+
* - `file2` -- a file attached to a named page
|
|
244
|
+
* - `file3` -- a file on a named site and page
|
|
245
|
+
*
|
|
246
|
+
* Local paths (starting with `/` but not `//`) and file-type sources are
|
|
247
|
+
* blocked when `allowLocalPaths` is false in the settings.
|
|
248
|
+
*
|
|
249
|
+
* @param source - The image source descriptor from the AST.
|
|
250
|
+
* @returns The resolved URL, or `null` if the source is blocked by settings.
|
|
251
|
+
*/
|
|
252
|
+
resolveImageSource(source: ImageSource): string | null {
|
|
253
|
+
return resolveImageSourceUrl(source, this.settings, this.page);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Resolve a {@link LinkLocation} to an `href` string.
|
|
258
|
+
*
|
|
259
|
+
* Handles plain URL strings and structured `PageRef` objects. For page
|
|
260
|
+
* references the page name is normalized to lowercase, spaces are replaced
|
|
261
|
+
* with hyphens, and slashes become hyphens (Wikidot URL convention).
|
|
262
|
+
* Anchors (`#`) and cross-site references (`site` field) are handled.
|
|
263
|
+
*
|
|
264
|
+
* @param location - A raw URL string or a `PageRef` object from the AST.
|
|
265
|
+
* @returns The resolved href string, starting with `/` for local or unresolved
|
|
266
|
+
* cross-site pages and `https://` when a cross-site domain is known.
|
|
267
|
+
*/
|
|
268
|
+
resolvePageLink(location: LinkLocation): string {
|
|
269
|
+
return resolvePageLinkUrl(location, this.page);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Sanitize and render an attribute map to an HTML attribute string.
|
|
274
|
+
*
|
|
275
|
+
* Dangerous attributes (event handlers, unsafe URLs) are stripped by
|
|
276
|
+
* {@link sanitizeAttributes}. Each surviving key-value pair is escaped
|
|
277
|
+
* and formatted as ` key="value"`.
|
|
278
|
+
*
|
|
279
|
+
* @param attributes - Raw attribute map from the AST.
|
|
280
|
+
* @returns A string of HTML attributes with a leading space, or `""` if empty.
|
|
281
|
+
*/
|
|
282
|
+
renderAttributes(attributes: Record<string, string>): string {
|
|
283
|
+
return renderAttributeString(attributes);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { escapeHtml } from "../escape";
|
|
2
|
+
|
|
3
|
+
export class RenderOutputBuffer {
|
|
4
|
+
private chunks: string[] = [];
|
|
5
|
+
|
|
6
|
+
push(html: string): void {
|
|
7
|
+
this.chunks.push(html);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
pushEscaped(text: string): void {
|
|
11
|
+
this.chunks.push(escapeHtml(text));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
getOutput(): string {
|
|
15
|
+
return this.chunks.join("");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { LinkLocation } from "@wdprlib/ast";
|
|
2
|
+
import type { PageContext } from "../types";
|
|
3
|
+
|
|
4
|
+
export function resolvePageLink(
|
|
5
|
+
location: LinkLocation,
|
|
6
|
+
pageContext: PageContext | undefined,
|
|
7
|
+
): string {
|
|
8
|
+
if (typeof location === "string") {
|
|
9
|
+
return location;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const page = location.page;
|
|
13
|
+
if (page.startsWith("//")) {
|
|
14
|
+
return page.toLowerCase();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const hashIdx = page.indexOf("#");
|
|
18
|
+
if (hashIdx !== -1) {
|
|
19
|
+
let pagePart = page.slice(0, hashIdx);
|
|
20
|
+
const anchor = page.slice(hashIdx);
|
|
21
|
+
if (pagePart.endsWith("/")) {
|
|
22
|
+
pagePart = pagePart.slice(0, -1);
|
|
23
|
+
}
|
|
24
|
+
return `/${pagePart.toLowerCase()}${anchor.toLowerCase()}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const normalizedPage = normalizePageName(page);
|
|
28
|
+
const safePage = normalizedPage.startsWith("/") ? normalizedPage.slice(1) : normalizedPage;
|
|
29
|
+
|
|
30
|
+
if (location.site) {
|
|
31
|
+
const domain = resolveSiteDomain(location.site, pageContext);
|
|
32
|
+
if (domain !== null) {
|
|
33
|
+
return `https://${domain}/${safePage}`;
|
|
34
|
+
}
|
|
35
|
+
return `/${normalizePageName(location.site)}/${safePage}`;
|
|
36
|
+
}
|
|
37
|
+
return `/${safePage}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveSiteDomain(site: string, pageContext: PageContext | undefined): string | null {
|
|
41
|
+
const configuredDomain =
|
|
42
|
+
pageContext?.resolveSiteDomain?.(site) ??
|
|
43
|
+
pageContext?.siteDomains?.[site] ??
|
|
44
|
+
(pageContext?.site === site ? pageContext.domain : undefined);
|
|
45
|
+
|
|
46
|
+
if (configuredDomain) return normalizeDomain(configuredDomain);
|
|
47
|
+
if (site.includes(".")) {
|
|
48
|
+
return normalizeDomain(site);
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeDomain(domain: string): string {
|
|
54
|
+
let normalized = domain;
|
|
55
|
+
const lower = normalized.toLowerCase();
|
|
56
|
+
if (lower.startsWith("https://")) {
|
|
57
|
+
normalized = normalized.slice("https://".length);
|
|
58
|
+
} else if (lower.startsWith("http://")) {
|
|
59
|
+
normalized = normalized.slice("http://".length);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let end = normalized.length;
|
|
63
|
+
while (end > 0 && normalized[end - 1] === "/") {
|
|
64
|
+
end--;
|
|
65
|
+
}
|
|
66
|
+
return end === normalized.length ? normalized : normalized.slice(0, end);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function normalizePageName(page: string): string {
|
|
70
|
+
let normalized = page.toLowerCase();
|
|
71
|
+
if (normalized.indexOf(":") !== -1) {
|
|
72
|
+
normalized = normalized.replace(/:\s+/g, ":");
|
|
73
|
+
}
|
|
74
|
+
if (/\s/.test(normalized)) {
|
|
75
|
+
normalized = normalized.replace(/\s+/g, "-").trim();
|
|
76
|
+
}
|
|
77
|
+
if (!normalized.startsWith("/") && normalized.indexOf("/") !== -1) {
|
|
78
|
+
normalized = normalized.replace(/\//g, "-");
|
|
79
|
+
}
|
|
80
|
+
return normalized;
|
|
81
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export class StyleSlotState {
|
|
2
|
+
private activeSlotId: number | null = null;
|
|
3
|
+
private contents = new Map<number, string[]>();
|
|
4
|
+
|
|
5
|
+
enter(slotId: number): void {
|
|
6
|
+
this.activeSlotId = slotId;
|
|
7
|
+
if (!this.contents.has(slotId)) {
|
|
8
|
+
this.contents.set(slotId, []);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
exit(): void {
|
|
13
|
+
this.activeSlotId = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
hasActiveSlot(): boolean {
|
|
17
|
+
return this.activeSlotId !== null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
push(css: string): void {
|
|
21
|
+
if (this.activeSlotId !== null) {
|
|
22
|
+
this.contents.get(this.activeSlotId)!.push(css);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getContents(slotId: number): string[] {
|
|
27
|
+
return this.contents.get(slotId) ?? [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { BibliographyBlockData, Element } from "@wdprlib/ast";
|
|
2
|
+
import type { RenderContext } from "../../context";
|
|
3
|
+
import { escapeHtml } from "../../escape";
|
|
4
|
+
|
|
5
|
+
export function renderBibliographyBlock(
|
|
6
|
+
ctx: RenderContext,
|
|
7
|
+
data: BibliographyBlockData,
|
|
8
|
+
renderElements: (ctx: RenderContext, elements: Element[]) => void,
|
|
9
|
+
): void {
|
|
10
|
+
if (data.hide) return;
|
|
11
|
+
|
|
12
|
+
const title = data.title ?? "Bibliography";
|
|
13
|
+
ctx.push(`<div class="bibitems">`);
|
|
14
|
+
ctx.push(`<div class="title">${escapeHtml(title)}</div>`);
|
|
15
|
+
|
|
16
|
+
let index = 1;
|
|
17
|
+
for (const entry of data.entries) {
|
|
18
|
+
const itemId = ctx.generateId("bibitem-", index);
|
|
19
|
+
ctx.push(`<div class="bibitem" id="${itemId}">`);
|
|
20
|
+
ctx.push(`${index}. `);
|
|
21
|
+
renderElements(ctx, entry.value);
|
|
22
|
+
ctx.push("</div>");
|
|
23
|
+
index++;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ctx.push("</div>");
|
|
27
|
+
}
|