@wdprlib/render 1.2.0 → 1.2.3
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 +75 -12
- package/dist/index.d.cts +83 -30
- package/dist/index.d.ts +83 -30
- package/dist/index.js +72 -9
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -44,12 +44,15 @@ var __export = (target, all) => {
|
|
|
44
44
|
var exports_src = {};
|
|
45
45
|
__export(exports_src, {
|
|
46
46
|
renderToHtml: () => renderToHtml,
|
|
47
|
-
createSettings: () =>
|
|
48
|
-
DEFAULT_SETTINGS: () =>
|
|
47
|
+
createSettings: () => import_ast4.createSettings,
|
|
48
|
+
DEFAULT_SETTINGS: () => import_ast4.DEFAULT_SETTINGS,
|
|
49
49
|
DEFAULT_EMBED_ALLOWLIST: () => DEFAULT_EMBED_ALLOWLIST
|
|
50
50
|
});
|
|
51
51
|
module.exports = __toCommonJS(exports_src);
|
|
52
52
|
|
|
53
|
+
// packages/render/src/render.ts
|
|
54
|
+
var import_ast3 = require("@wdprlib/ast");
|
|
55
|
+
|
|
53
56
|
// packages/render/src/context.ts
|
|
54
57
|
var import_ast = require("@wdprlib/ast");
|
|
55
58
|
|
|
@@ -324,11 +327,17 @@ function isValidCssColor(color) {
|
|
|
324
327
|
if (/^#[0-9a-f]{3}([0-9a-f])?$/.test(trimmed) || /^#[0-9a-f]{6}([0-9a-f]{2})?$/.test(trimmed)) {
|
|
325
328
|
return true;
|
|
326
329
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
330
|
+
const fnMatch = trimmed.match(/^(rgba?|hsla?)\(([^)]*)\)$/);
|
|
331
|
+
if (fnMatch) {
|
|
332
|
+
const fn = fnMatch[1];
|
|
333
|
+
const args = fnMatch[2].split(",").map((s) => s.trim()).join(",");
|
|
334
|
+
if (fn.startsWith("rgb")) {
|
|
335
|
+
if (/^\d{1,3},\d{1,3},\d{1,3}(,(0|1|0?\.\d+))?$/.test(args))
|
|
336
|
+
return true;
|
|
337
|
+
} else {
|
|
338
|
+
if (/^\d{1,3},\d{1,3}%,\d{1,3}%(,(0|1|0?\.\d+))?$/.test(args))
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
332
341
|
}
|
|
333
342
|
return false;
|
|
334
343
|
}
|
|
@@ -418,6 +427,9 @@ function sanitizeAttributes(attributes) {
|
|
|
418
427
|
// packages/render/src/context.ts
|
|
419
428
|
class RenderContext {
|
|
420
429
|
chunks = [];
|
|
430
|
+
renderInlineStyles = false;
|
|
431
|
+
_styleSlotId = null;
|
|
432
|
+
_styleSlotContents = new Map;
|
|
421
433
|
_tocIndex = 0;
|
|
422
434
|
_footnoteIndex = 0;
|
|
423
435
|
_equationIndex = 0;
|
|
@@ -473,6 +485,26 @@ class RenderContext {
|
|
|
473
485
|
getOutput() {
|
|
474
486
|
return this.chunks.join("");
|
|
475
487
|
}
|
|
488
|
+
enterStyleSlot(slotId) {
|
|
489
|
+
this._styleSlotId = slotId;
|
|
490
|
+
if (!this._styleSlotContents.has(slotId)) {
|
|
491
|
+
this._styleSlotContents.set(slotId, []);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
exitStyleSlot() {
|
|
495
|
+
this._styleSlotId = null;
|
|
496
|
+
}
|
|
497
|
+
hasActiveStyleSlot() {
|
|
498
|
+
return this._styleSlotId !== null;
|
|
499
|
+
}
|
|
500
|
+
pushToStyleSlot(css) {
|
|
501
|
+
if (this._styleSlotId !== null) {
|
|
502
|
+
this._styleSlotContents.get(this._styleSlotId).push(css);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
getStyleSlotContents(slotId) {
|
|
506
|
+
return this._styleSlotContents.get(slotId) ?? [];
|
|
507
|
+
}
|
|
476
508
|
nextTocIndex() {
|
|
477
509
|
return this._tocIndex++;
|
|
478
510
|
}
|
|
@@ -4117,7 +4149,7 @@ function renderJoin(ctx, data) {
|
|
|
4117
4149
|
const buttonText = data["button-text"] ?? "Join";
|
|
4118
4150
|
const attrs = data.attributes ?? {};
|
|
4119
4151
|
const className = attrs.class ?? "join-box";
|
|
4120
|
-
ctx.push(`<div class="${
|
|
4152
|
+
ctx.push(`<div class="${escapeAttr(className)}">`);
|
|
4121
4153
|
ctx.push(`<a href="javascript:;">${escapeHtml(buttonText)}</a>`);
|
|
4122
4154
|
ctx.push("</div>");
|
|
4123
4155
|
}
|
|
@@ -4610,13 +4642,20 @@ function evaluateIfTagsCondition(condition, pageTags) {
|
|
|
4610
4642
|
const optional = [];
|
|
4611
4643
|
for (const token of tokens) {
|
|
4612
4644
|
if (token.startsWith("+")) {
|
|
4613
|
-
|
|
4645
|
+
const tag = token.slice(1).toLowerCase();
|
|
4646
|
+
if (tag)
|
|
4647
|
+
required.push(tag);
|
|
4614
4648
|
} else if (token.startsWith("-")) {
|
|
4615
|
-
|
|
4649
|
+
const tag = token.slice(1).toLowerCase();
|
|
4650
|
+
if (tag)
|
|
4651
|
+
excluded.push(tag);
|
|
4616
4652
|
} else {
|
|
4617
4653
|
optional.push(token.toLowerCase());
|
|
4618
4654
|
}
|
|
4619
4655
|
}
|
|
4656
|
+
if (required.length === 0 && excluded.length === 0 && optional.length === 0) {
|
|
4657
|
+
return false;
|
|
4658
|
+
}
|
|
4620
4659
|
for (const tag of required) {
|
|
4621
4660
|
if (!pageTagSet.has(tag))
|
|
4622
4661
|
return false;
|
|
@@ -4635,7 +4674,17 @@ function evaluateIfTagsCondition(condition, pageTags) {
|
|
|
4635
4674
|
function renderIfTags(ctx, data) {
|
|
4636
4675
|
const pageTags = ctx.page?.tags ?? [];
|
|
4637
4676
|
if (evaluateIfTagsCondition(data.condition, pageTags)) {
|
|
4677
|
+
const prev = ctx.renderInlineStyles;
|
|
4678
|
+
ctx.renderInlineStyles = true;
|
|
4679
|
+
const slotId = data._styleSlot;
|
|
4680
|
+
if (slotId !== undefined) {
|
|
4681
|
+
ctx.enterStyleSlot(slotId);
|
|
4682
|
+
}
|
|
4638
4683
|
renderElements(ctx, data.elements);
|
|
4684
|
+
if (slotId !== undefined) {
|
|
4685
|
+
ctx.exitStyleSlot();
|
|
4686
|
+
}
|
|
4687
|
+
ctx.renderInlineStyles = prev;
|
|
4639
4688
|
}
|
|
4640
4689
|
}
|
|
4641
4690
|
|
|
@@ -5079,7 +5128,14 @@ function renderToHtml(tree, options = {}) {
|
|
|
5079
5128
|
renderElements(ctx, tree.elements);
|
|
5080
5129
|
if (ctx.settings.allowStyleElements && tree.styles?.length) {
|
|
5081
5130
|
for (const style of tree.styles) {
|
|
5082
|
-
|
|
5131
|
+
if (style.startsWith(import_ast3.STYLE_SLOT_PREFIX)) {
|
|
5132
|
+
const slotId = parseInt(style.slice(import_ast3.STYLE_SLOT_PREFIX.length), 10);
|
|
5133
|
+
for (const css of ctx.getStyleSlotContents(slotId)) {
|
|
5134
|
+
ctx.push(`<style>${escapeStyleContent(css)}</style>`);
|
|
5135
|
+
}
|
|
5136
|
+
} else {
|
|
5137
|
+
ctx.push(`<style>${escapeStyleContent(style)}</style>`);
|
|
5138
|
+
}
|
|
5083
5139
|
}
|
|
5084
5140
|
}
|
|
5085
5141
|
return ctx.getOutput();
|
|
@@ -5191,6 +5247,13 @@ function renderElement(ctx, element) {
|
|
|
5191
5247
|
renderIfTags(ctx, element.data);
|
|
5192
5248
|
break;
|
|
5193
5249
|
case "style":
|
|
5250
|
+
if (ctx.renderInlineStyles && ctx.settings.allowStyleElements) {
|
|
5251
|
+
if (ctx.hasActiveStyleSlot()) {
|
|
5252
|
+
ctx.pushToStyleSlot(element.data);
|
|
5253
|
+
} else {
|
|
5254
|
+
ctx.push(`<style>${escapeStyleContent(element.data)}</style>`);
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5194
5257
|
break;
|
|
5195
5258
|
case "line-break":
|
|
5196
5259
|
ctx.push("<br />");
|
|
@@ -5223,4 +5286,4 @@ function renderElement(ctx, element) {
|
|
|
5223
5286
|
}
|
|
5224
5287
|
|
|
5225
5288
|
// packages/render/src/index.ts
|
|
5226
|
-
var
|
|
5289
|
+
var import_ast4 = require("@wdprlib/ast");
|
package/dist/index.d.cts
CHANGED
|
@@ -19,71 +19,113 @@ interface EmbedAllowlistEntry {
|
|
|
19
19
|
*/
|
|
20
20
|
declare const DEFAULT_EMBED_ALLOWLIST: EmbedAllowlistEntry[] | null;
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Contextual information about the wiki page being rendered.
|
|
23
|
+
*
|
|
24
|
+
* The renderer uses this to resolve relative links, local file paths,
|
|
25
|
+
* page-existence checks (adding a `"newpage"` CSS class to links
|
|
26
|
+
* targeting non-existent pages), and `[[iftags]]` evaluation.
|
|
27
|
+
*
|
|
28
|
+
* @group Render Options
|
|
23
29
|
*/
|
|
24
30
|
interface PageContext {
|
|
25
|
-
/**
|
|
31
|
+
/** Full page name including category prefix (e.g. `"secret:test2"`) */
|
|
26
32
|
pageName: string;
|
|
27
|
-
/** Site slug (e.g
|
|
33
|
+
/** Site slug used to build inter-site URLs (e.g. `"scp-wiki"`) */
|
|
28
34
|
site?: string;
|
|
29
|
-
/** Site domain (e.g
|
|
35
|
+
/** Site domain used for absolute URL generation (e.g. `"scp-wiki.wikidot.com"`) */
|
|
30
36
|
domain?: string;
|
|
31
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Returns whether a page exists on the site.
|
|
39
|
+
* When a target page does not exist, the renderer adds `class="newpage"`
|
|
40
|
+
* to the link element — the standard Wikidot convention for red-links.
|
|
41
|
+
*/
|
|
32
42
|
pageExists?: (page: string) => boolean;
|
|
33
|
-
/** Page tags for [[iftags]]
|
|
43
|
+
/** Page tags used for client-side `[[iftags]]` evaluation during rendering */
|
|
34
44
|
tags?: string[];
|
|
35
45
|
}
|
|
36
46
|
/**
|
|
37
|
-
*
|
|
47
|
+
* User profile data returned by a user-resolver callback.
|
|
48
|
+
*
|
|
49
|
+
* Passed to the renderer to produce the Wikidot user-info markup
|
|
50
|
+
* (`[[user username]]`). When a field is omitted the corresponding
|
|
51
|
+
* UI element is simply not emitted.
|
|
52
|
+
*
|
|
53
|
+
* @group Render Options
|
|
38
54
|
*/
|
|
39
55
|
interface ResolvedUser {
|
|
40
|
-
/**
|
|
56
|
+
/** Display name shown in the rendered output (falls back to the raw username) */
|
|
41
57
|
name?: string;
|
|
42
|
-
/**
|
|
58
|
+
/** Profile URL. When omitted the username is rendered as a non-navigable `<span>` */
|
|
43
59
|
url?: string;
|
|
44
|
-
/** Avatar image URL.
|
|
60
|
+
/** Avatar image URL. When omitted no avatar `<img>` is rendered */
|
|
45
61
|
avatarUrl?: string;
|
|
46
|
-
/** Karma image URL
|
|
62
|
+
/** Karma-badge image URL shown behind the avatar (Wikidot-specific feature) */
|
|
47
63
|
karmaUrl?: string;
|
|
48
64
|
}
|
|
49
65
|
/**
|
|
50
|
-
*
|
|
66
|
+
* Async/sync resolver callbacks for content that depends on external data.
|
|
67
|
+
*
|
|
68
|
+
* Unlike the parser's `DataProvider` (which fetches bulk data for
|
|
69
|
+
* module expansion), these resolvers are called per-element during the
|
|
70
|
+
* rendering pass.
|
|
71
|
+
*
|
|
72
|
+
* @group Render Options
|
|
51
73
|
*/
|
|
52
74
|
interface RenderResolvers {
|
|
53
75
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
76
|
+
* Look up a user profile by username.
|
|
77
|
+
*
|
|
78
|
+
* @param username - The raw username from `[[user username]]`
|
|
79
|
+
* @returns Profile data for rendering, or `null` if the user is unknown.
|
|
80
|
+
* When `null` or when the resolver is omitted, the username is
|
|
81
|
+
* rendered as plain text.
|
|
57
82
|
*/
|
|
58
83
|
user?: (username: string) => ResolvedUser | null;
|
|
59
84
|
/**
|
|
60
|
-
*
|
|
61
|
-
* Called with the index of the htmlBlock (0-based, matching tree["html-blocks"] order).
|
|
62
|
-
* If not provided or returns empty string, uses default pattern: /{pageName}/html/{hash}-{nonce}
|
|
85
|
+
* Build an iframe `src` URL for an `[[html]]` block.
|
|
63
86
|
*
|
|
64
|
-
*
|
|
65
|
-
* The
|
|
87
|
+
* @param index - Zero-based index matching `SyntaxTree["html-blocks"]`
|
|
88
|
+
* @returns The URL string. When empty or when the resolver is omitted,
|
|
89
|
+
* the default pattern `/{pageName}/html/{hash}-{nonce}` is used.
|
|
90
|
+
*
|
|
91
|
+
* @security The returned URL is injected directly into the iframe `src`
|
|
92
|
+
* attribute. The caller must validate the scheme to reject `javascript:`,
|
|
93
|
+
* `data:`, and other dangerous protocols.
|
|
66
94
|
*/
|
|
67
95
|
htmlBlockUrl?: (index: number) => string;
|
|
68
96
|
}
|
|
69
97
|
/**
|
|
70
|
-
*
|
|
98
|
+
* Full configuration for `renderToHtml()`.
|
|
99
|
+
*
|
|
100
|
+
* Every field is optional; defaults produce safe, standalone HTML output
|
|
101
|
+
* suitable for a full wiki page.
|
|
102
|
+
*
|
|
103
|
+
* @group Render Options
|
|
71
104
|
*/
|
|
72
105
|
interface RenderOptions {
|
|
73
106
|
/**
|
|
74
|
-
* Base URL
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
107
|
+
* Base URL for resolving protocol-relative URLs (e.g. `"//example.com/path"`).
|
|
108
|
+
*
|
|
109
|
+
* The scheme of this URL (`http:` or `https:`) is prepended to
|
|
110
|
+
* protocol-relative references. When omitted, HTTPS is assumed.
|
|
111
|
+
*
|
|
112
|
+
* @example "https://scp-wiki.wikidot.com"
|
|
78
113
|
*/
|
|
79
114
|
baseUrl?: string;
|
|
80
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Context-dependent feature flags.
|
|
117
|
+
* Defaults to page-mode settings when omitted.
|
|
118
|
+
*/
|
|
81
119
|
settings?: WikitextSettings;
|
|
82
|
-
/** Page context for resolving file paths,
|
|
120
|
+
/** Page context for resolving relative links, local file paths, etc. */
|
|
83
121
|
page?: PageContext;
|
|
84
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Pre-collected footnote element arrays from `SyntaxTree.footnotes`.
|
|
124
|
+
* Passed through so the renderer can emit footnote bodies in the
|
|
125
|
+
* `[[footnoteblock]]` section.
|
|
126
|
+
*/
|
|
85
127
|
footnotes?: Element[][];
|
|
86
|
-
/**
|
|
128
|
+
/** Callbacks for resolving users, HTML-block URLs, etc. */
|
|
87
129
|
resolvers?: RenderResolvers;
|
|
88
130
|
/**
|
|
89
131
|
* Sandbox attribute value for htmlBlock iframes.
|
|
@@ -106,7 +148,18 @@ interface RenderOptions {
|
|
|
106
148
|
embedAllowlist?: EmbedAllowlistEntry[] | null;
|
|
107
149
|
}
|
|
108
150
|
/**
|
|
109
|
-
* Render a SyntaxTree to HTML string
|
|
151
|
+
* Render a {@link SyntaxTree} to an HTML string.
|
|
152
|
+
*
|
|
153
|
+
* This is the main entry point of `@wdprlib/render`. It walks the AST
|
|
154
|
+
* produced by `@wdprlib/parser`, serialises each element to HTML, and
|
|
155
|
+
* appends any collected `[[module CSS]]` styles at the end (when
|
|
156
|
+
* `WikitextSettings.allowStyleElements` is `true`).
|
|
157
|
+
*
|
|
158
|
+
* @param tree - Parsed AST (from `parse()` or `resolveModules()`)
|
|
159
|
+
* @param options - Rendering configuration
|
|
160
|
+
* @returns Complete HTML string
|
|
161
|
+
*
|
|
162
|
+
* @group Render
|
|
110
163
|
*/
|
|
111
164
|
declare function renderToHtml(tree: SyntaxTree2, options?: RenderOptions): string;
|
|
112
165
|
import { WikitextMode, WikitextSettings as WikitextSettings3 } from "@wdprlib/ast";
|
package/dist/index.d.ts
CHANGED
|
@@ -19,71 +19,113 @@ interface EmbedAllowlistEntry {
|
|
|
19
19
|
*/
|
|
20
20
|
declare const DEFAULT_EMBED_ALLOWLIST: EmbedAllowlistEntry[] | null;
|
|
21
21
|
/**
|
|
22
|
-
*
|
|
22
|
+
* Contextual information about the wiki page being rendered.
|
|
23
|
+
*
|
|
24
|
+
* The renderer uses this to resolve relative links, local file paths,
|
|
25
|
+
* page-existence checks (adding a `"newpage"` CSS class to links
|
|
26
|
+
* targeting non-existent pages), and `[[iftags]]` evaluation.
|
|
27
|
+
*
|
|
28
|
+
* @group Render Options
|
|
23
29
|
*/
|
|
24
30
|
interface PageContext {
|
|
25
|
-
/**
|
|
31
|
+
/** Full page name including category prefix (e.g. `"secret:test2"`) */
|
|
26
32
|
pageName: string;
|
|
27
|
-
/** Site slug (e.g
|
|
33
|
+
/** Site slug used to build inter-site URLs (e.g. `"scp-wiki"`) */
|
|
28
34
|
site?: string;
|
|
29
|
-
/** Site domain (e.g
|
|
35
|
+
/** Site domain used for absolute URL generation (e.g. `"scp-wiki.wikidot.com"`) */
|
|
30
36
|
domain?: string;
|
|
31
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Returns whether a page exists on the site.
|
|
39
|
+
* When a target page does not exist, the renderer adds `class="newpage"`
|
|
40
|
+
* to the link element — the standard Wikidot convention for red-links.
|
|
41
|
+
*/
|
|
32
42
|
pageExists?: (page: string) => boolean;
|
|
33
|
-
/** Page tags for [[iftags]]
|
|
43
|
+
/** Page tags used for client-side `[[iftags]]` evaluation during rendering */
|
|
34
44
|
tags?: string[];
|
|
35
45
|
}
|
|
36
46
|
/**
|
|
37
|
-
*
|
|
47
|
+
* User profile data returned by a user-resolver callback.
|
|
48
|
+
*
|
|
49
|
+
* Passed to the renderer to produce the Wikidot user-info markup
|
|
50
|
+
* (`[[user username]]`). When a field is omitted the corresponding
|
|
51
|
+
* UI element is simply not emitted.
|
|
52
|
+
*
|
|
53
|
+
* @group Render Options
|
|
38
54
|
*/
|
|
39
55
|
interface ResolvedUser {
|
|
40
|
-
/**
|
|
56
|
+
/** Display name shown in the rendered output (falls back to the raw username) */
|
|
41
57
|
name?: string;
|
|
42
|
-
/**
|
|
58
|
+
/** Profile URL. When omitted the username is rendered as a non-navigable `<span>` */
|
|
43
59
|
url?: string;
|
|
44
|
-
/** Avatar image URL.
|
|
60
|
+
/** Avatar image URL. When omitted no avatar `<img>` is rendered */
|
|
45
61
|
avatarUrl?: string;
|
|
46
|
-
/** Karma image URL
|
|
62
|
+
/** Karma-badge image URL shown behind the avatar (Wikidot-specific feature) */
|
|
47
63
|
karmaUrl?: string;
|
|
48
64
|
}
|
|
49
65
|
/**
|
|
50
|
-
*
|
|
66
|
+
* Async/sync resolver callbacks for content that depends on external data.
|
|
67
|
+
*
|
|
68
|
+
* Unlike the parser's `DataProvider` (which fetches bulk data for
|
|
69
|
+
* module expansion), these resolvers are called per-element during the
|
|
70
|
+
* rendering pass.
|
|
71
|
+
*
|
|
72
|
+
* @group Render Options
|
|
51
73
|
*/
|
|
52
74
|
interface RenderResolvers {
|
|
53
75
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
76
|
+
* Look up a user profile by username.
|
|
77
|
+
*
|
|
78
|
+
* @param username - The raw username from `[[user username]]`
|
|
79
|
+
* @returns Profile data for rendering, or `null` if the user is unknown.
|
|
80
|
+
* When `null` or when the resolver is omitted, the username is
|
|
81
|
+
* rendered as plain text.
|
|
57
82
|
*/
|
|
58
83
|
user?: (username: string) => ResolvedUser | null;
|
|
59
84
|
/**
|
|
60
|
-
*
|
|
61
|
-
* Called with the index of the htmlBlock (0-based, matching tree["html-blocks"] order).
|
|
62
|
-
* If not provided or returns empty string, uses default pattern: /{pageName}/html/{hash}-{nonce}
|
|
85
|
+
* Build an iframe `src` URL for an `[[html]]` block.
|
|
63
86
|
*
|
|
64
|
-
*
|
|
65
|
-
* The
|
|
87
|
+
* @param index - Zero-based index matching `SyntaxTree["html-blocks"]`
|
|
88
|
+
* @returns The URL string. When empty or when the resolver is omitted,
|
|
89
|
+
* the default pattern `/{pageName}/html/{hash}-{nonce}` is used.
|
|
90
|
+
*
|
|
91
|
+
* @security The returned URL is injected directly into the iframe `src`
|
|
92
|
+
* attribute. The caller must validate the scheme to reject `javascript:`,
|
|
93
|
+
* `data:`, and other dangerous protocols.
|
|
66
94
|
*/
|
|
67
95
|
htmlBlockUrl?: (index: number) => string;
|
|
68
96
|
}
|
|
69
97
|
/**
|
|
70
|
-
*
|
|
98
|
+
* Full configuration for `renderToHtml()`.
|
|
99
|
+
*
|
|
100
|
+
* Every field is optional; defaults produce safe, standalone HTML output
|
|
101
|
+
* suitable for a full wiki page.
|
|
102
|
+
*
|
|
103
|
+
* @group Render Options
|
|
71
104
|
*/
|
|
72
105
|
interface RenderOptions {
|
|
73
106
|
/**
|
|
74
|
-
* Base URL
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
107
|
+
* Base URL for resolving protocol-relative URLs (e.g. `"//example.com/path"`).
|
|
108
|
+
*
|
|
109
|
+
* The scheme of this URL (`http:` or `https:`) is prepended to
|
|
110
|
+
* protocol-relative references. When omitted, HTTPS is assumed.
|
|
111
|
+
*
|
|
112
|
+
* @example "https://scp-wiki.wikidot.com"
|
|
78
113
|
*/
|
|
79
114
|
baseUrl?: string;
|
|
80
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Context-dependent feature flags.
|
|
117
|
+
* Defaults to page-mode settings when omitted.
|
|
118
|
+
*/
|
|
81
119
|
settings?: WikitextSettings;
|
|
82
|
-
/** Page context for resolving file paths,
|
|
120
|
+
/** Page context for resolving relative links, local file paths, etc. */
|
|
83
121
|
page?: PageContext;
|
|
84
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Pre-collected footnote element arrays from `SyntaxTree.footnotes`.
|
|
124
|
+
* Passed through so the renderer can emit footnote bodies in the
|
|
125
|
+
* `[[footnoteblock]]` section.
|
|
126
|
+
*/
|
|
85
127
|
footnotes?: Element[][];
|
|
86
|
-
/**
|
|
128
|
+
/** Callbacks for resolving users, HTML-block URLs, etc. */
|
|
87
129
|
resolvers?: RenderResolvers;
|
|
88
130
|
/**
|
|
89
131
|
* Sandbox attribute value for htmlBlock iframes.
|
|
@@ -106,7 +148,18 @@ interface RenderOptions {
|
|
|
106
148
|
embedAllowlist?: EmbedAllowlistEntry[] | null;
|
|
107
149
|
}
|
|
108
150
|
/**
|
|
109
|
-
* Render a SyntaxTree to HTML string
|
|
151
|
+
* Render a {@link SyntaxTree} to an HTML string.
|
|
152
|
+
*
|
|
153
|
+
* This is the main entry point of `@wdprlib/render`. It walks the AST
|
|
154
|
+
* produced by `@wdprlib/parser`, serialises each element to HTML, and
|
|
155
|
+
* appends any collected `[[module CSS]]` styles at the end (when
|
|
156
|
+
* `WikitextSettings.allowStyleElements` is `true`).
|
|
157
|
+
*
|
|
158
|
+
* @param tree - Parsed AST (from `parse()` or `resolveModules()`)
|
|
159
|
+
* @param options - Rendering configuration
|
|
160
|
+
* @returns Complete HTML string
|
|
161
|
+
*
|
|
162
|
+
* @group Render
|
|
110
163
|
*/
|
|
111
164
|
declare function renderToHtml(tree: SyntaxTree2, options?: RenderOptions): string;
|
|
112
165
|
import { WikitextMode, WikitextSettings as WikitextSettings3 } from "@wdprlib/ast";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// packages/render/src/render.ts
|
|
2
|
+
import { STYLE_SLOT_PREFIX } from "@wdprlib/ast";
|
|
3
|
+
|
|
1
4
|
// packages/render/src/context.ts
|
|
2
5
|
import { DEFAULT_SETTINGS } from "@wdprlib/ast";
|
|
3
6
|
|
|
@@ -272,11 +275,17 @@ function isValidCssColor(color) {
|
|
|
272
275
|
if (/^#[0-9a-f]{3}([0-9a-f])?$/.test(trimmed) || /^#[0-9a-f]{6}([0-9a-f]{2})?$/.test(trimmed)) {
|
|
273
276
|
return true;
|
|
274
277
|
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
278
|
+
const fnMatch = trimmed.match(/^(rgba?|hsla?)\(([^)]*)\)$/);
|
|
279
|
+
if (fnMatch) {
|
|
280
|
+
const fn = fnMatch[1];
|
|
281
|
+
const args = fnMatch[2].split(",").map((s) => s.trim()).join(",");
|
|
282
|
+
if (fn.startsWith("rgb")) {
|
|
283
|
+
if (/^\d{1,3},\d{1,3},\d{1,3}(,(0|1|0?\.\d+))?$/.test(args))
|
|
284
|
+
return true;
|
|
285
|
+
} else {
|
|
286
|
+
if (/^\d{1,3},\d{1,3}%,\d{1,3}%(,(0|1|0?\.\d+))?$/.test(args))
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
280
289
|
}
|
|
281
290
|
return false;
|
|
282
291
|
}
|
|
@@ -366,6 +375,9 @@ function sanitizeAttributes(attributes) {
|
|
|
366
375
|
// packages/render/src/context.ts
|
|
367
376
|
class RenderContext {
|
|
368
377
|
chunks = [];
|
|
378
|
+
renderInlineStyles = false;
|
|
379
|
+
_styleSlotId = null;
|
|
380
|
+
_styleSlotContents = new Map;
|
|
369
381
|
_tocIndex = 0;
|
|
370
382
|
_footnoteIndex = 0;
|
|
371
383
|
_equationIndex = 0;
|
|
@@ -421,6 +433,26 @@ class RenderContext {
|
|
|
421
433
|
getOutput() {
|
|
422
434
|
return this.chunks.join("");
|
|
423
435
|
}
|
|
436
|
+
enterStyleSlot(slotId) {
|
|
437
|
+
this._styleSlotId = slotId;
|
|
438
|
+
if (!this._styleSlotContents.has(slotId)) {
|
|
439
|
+
this._styleSlotContents.set(slotId, []);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
exitStyleSlot() {
|
|
443
|
+
this._styleSlotId = null;
|
|
444
|
+
}
|
|
445
|
+
hasActiveStyleSlot() {
|
|
446
|
+
return this._styleSlotId !== null;
|
|
447
|
+
}
|
|
448
|
+
pushToStyleSlot(css) {
|
|
449
|
+
if (this._styleSlotId !== null) {
|
|
450
|
+
this._styleSlotContents.get(this._styleSlotId).push(css);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
getStyleSlotContents(slotId) {
|
|
454
|
+
return this._styleSlotContents.get(slotId) ?? [];
|
|
455
|
+
}
|
|
424
456
|
nextTocIndex() {
|
|
425
457
|
return this._tocIndex++;
|
|
426
458
|
}
|
|
@@ -4065,7 +4097,7 @@ function renderJoin(ctx, data) {
|
|
|
4065
4097
|
const buttonText = data["button-text"] ?? "Join";
|
|
4066
4098
|
const attrs = data.attributes ?? {};
|
|
4067
4099
|
const className = attrs.class ?? "join-box";
|
|
4068
|
-
ctx.push(`<div class="${
|
|
4100
|
+
ctx.push(`<div class="${escapeAttr(className)}">`);
|
|
4069
4101
|
ctx.push(`<a href="javascript:;">${escapeHtml(buttonText)}</a>`);
|
|
4070
4102
|
ctx.push("</div>");
|
|
4071
4103
|
}
|
|
@@ -4558,13 +4590,20 @@ function evaluateIfTagsCondition(condition, pageTags) {
|
|
|
4558
4590
|
const optional = [];
|
|
4559
4591
|
for (const token of tokens) {
|
|
4560
4592
|
if (token.startsWith("+")) {
|
|
4561
|
-
|
|
4593
|
+
const tag = token.slice(1).toLowerCase();
|
|
4594
|
+
if (tag)
|
|
4595
|
+
required.push(tag);
|
|
4562
4596
|
} else if (token.startsWith("-")) {
|
|
4563
|
-
|
|
4597
|
+
const tag = token.slice(1).toLowerCase();
|
|
4598
|
+
if (tag)
|
|
4599
|
+
excluded.push(tag);
|
|
4564
4600
|
} else {
|
|
4565
4601
|
optional.push(token.toLowerCase());
|
|
4566
4602
|
}
|
|
4567
4603
|
}
|
|
4604
|
+
if (required.length === 0 && excluded.length === 0 && optional.length === 0) {
|
|
4605
|
+
return false;
|
|
4606
|
+
}
|
|
4568
4607
|
for (const tag of required) {
|
|
4569
4608
|
if (!pageTagSet.has(tag))
|
|
4570
4609
|
return false;
|
|
@@ -4583,7 +4622,17 @@ function evaluateIfTagsCondition(condition, pageTags) {
|
|
|
4583
4622
|
function renderIfTags(ctx, data) {
|
|
4584
4623
|
const pageTags = ctx.page?.tags ?? [];
|
|
4585
4624
|
if (evaluateIfTagsCondition(data.condition, pageTags)) {
|
|
4625
|
+
const prev = ctx.renderInlineStyles;
|
|
4626
|
+
ctx.renderInlineStyles = true;
|
|
4627
|
+
const slotId = data._styleSlot;
|
|
4628
|
+
if (slotId !== undefined) {
|
|
4629
|
+
ctx.enterStyleSlot(slotId);
|
|
4630
|
+
}
|
|
4586
4631
|
renderElements(ctx, data.elements);
|
|
4632
|
+
if (slotId !== undefined) {
|
|
4633
|
+
ctx.exitStyleSlot();
|
|
4634
|
+
}
|
|
4635
|
+
ctx.renderInlineStyles = prev;
|
|
4587
4636
|
}
|
|
4588
4637
|
}
|
|
4589
4638
|
|
|
@@ -5027,7 +5076,14 @@ function renderToHtml(tree, options = {}) {
|
|
|
5027
5076
|
renderElements(ctx, tree.elements);
|
|
5028
5077
|
if (ctx.settings.allowStyleElements && tree.styles?.length) {
|
|
5029
5078
|
for (const style of tree.styles) {
|
|
5030
|
-
|
|
5079
|
+
if (style.startsWith(STYLE_SLOT_PREFIX)) {
|
|
5080
|
+
const slotId = parseInt(style.slice(STYLE_SLOT_PREFIX.length), 10);
|
|
5081
|
+
for (const css of ctx.getStyleSlotContents(slotId)) {
|
|
5082
|
+
ctx.push(`<style>${escapeStyleContent(css)}</style>`);
|
|
5083
|
+
}
|
|
5084
|
+
} else {
|
|
5085
|
+
ctx.push(`<style>${escapeStyleContent(style)}</style>`);
|
|
5086
|
+
}
|
|
5031
5087
|
}
|
|
5032
5088
|
}
|
|
5033
5089
|
return ctx.getOutput();
|
|
@@ -5139,6 +5195,13 @@ function renderElement(ctx, element) {
|
|
|
5139
5195
|
renderIfTags(ctx, element.data);
|
|
5140
5196
|
break;
|
|
5141
5197
|
case "style":
|
|
5198
|
+
if (ctx.renderInlineStyles && ctx.settings.allowStyleElements) {
|
|
5199
|
+
if (ctx.hasActiveStyleSlot()) {
|
|
5200
|
+
ctx.pushToStyleSlot(element.data);
|
|
5201
|
+
} else {
|
|
5202
|
+
ctx.push(`<style>${escapeStyleContent(element.data)}</style>`);
|
|
5203
|
+
}
|
|
5204
|
+
}
|
|
5142
5205
|
break;
|
|
5143
5206
|
case "line-break":
|
|
5144
5207
|
ctx.push("<br />");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wdprlib/render",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "HTML renderer for Wikidot markup",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"html",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@wdprlib/ast": "1.1.
|
|
42
|
+
"@wdprlib/ast": "1.1.1",
|
|
43
43
|
"domhandler": "^5.0.3",
|
|
44
44
|
"htmlparser2": "^10.0.0",
|
|
45
45
|
"sanitize-html": "^2.14.0",
|