@wdprlib/render 1.0.0 → 1.1.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 +102 -39
- package/dist/index.d.cts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +98 -35
- package/package.json +6 -6
package/dist/index.cjs
CHANGED
|
@@ -44,10 +44,15 @@ var __export = (target, all) => {
|
|
|
44
44
|
var exports_src = {};
|
|
45
45
|
__export(exports_src, {
|
|
46
46
|
renderToHtml: () => renderToHtml,
|
|
47
|
+
createSettings: () => import_ast3.createSettings,
|
|
48
|
+
DEFAULT_SETTINGS: () => import_ast3.DEFAULT_SETTINGS,
|
|
47
49
|
DEFAULT_EMBED_ALLOWLIST: () => DEFAULT_EMBED_ALLOWLIST
|
|
48
50
|
});
|
|
49
51
|
module.exports = __toCommonJS(exports_src);
|
|
50
52
|
|
|
53
|
+
// packages/render/src/context.ts
|
|
54
|
+
var import_ast = require("@wdprlib/ast");
|
|
55
|
+
|
|
51
56
|
// packages/render/src/escape.ts
|
|
52
57
|
function escapeHtml(text) {
|
|
53
58
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -418,6 +423,8 @@ class RenderContext {
|
|
|
418
423
|
_equationIndex = 0;
|
|
419
424
|
_htmlBlockIndex = 0;
|
|
420
425
|
_bibciteCounter = 0;
|
|
426
|
+
_idSuffix;
|
|
427
|
+
settings;
|
|
421
428
|
options;
|
|
422
429
|
footnotes;
|
|
423
430
|
styles;
|
|
@@ -426,6 +433,8 @@ class RenderContext {
|
|
|
426
433
|
bibliographyMap;
|
|
427
434
|
bibliographyEntries;
|
|
428
435
|
constructor(tree, options = {}) {
|
|
436
|
+
this.settings = options.settings ?? import_ast.DEFAULT_SETTINGS;
|
|
437
|
+
this._idSuffix = this.settings.useTrueIds ? null : Math.random().toString(16).slice(2, 8);
|
|
429
438
|
this.options = options;
|
|
430
439
|
this.footnotes = options.footnotes ?? tree.footnotes ?? [];
|
|
431
440
|
this.styles = tree.styles ?? [];
|
|
@@ -479,6 +488,18 @@ class RenderContext {
|
|
|
479
488
|
nextBibciteCounter() {
|
|
480
489
|
return ++this._bibciteCounter;
|
|
481
490
|
}
|
|
491
|
+
generateId(prefix, index) {
|
|
492
|
+
if (this._idSuffix === null) {
|
|
493
|
+
return `${prefix}${index}`;
|
|
494
|
+
}
|
|
495
|
+
return `${prefix}${index}-${this._idSuffix}`;
|
|
496
|
+
}
|
|
497
|
+
generateFixedId(name) {
|
|
498
|
+
if (this._idSuffix === null) {
|
|
499
|
+
return name;
|
|
500
|
+
}
|
|
501
|
+
return `${name}-${this._idSuffix}`;
|
|
502
|
+
}
|
|
482
503
|
get page() {
|
|
483
504
|
return this.options.page;
|
|
484
505
|
}
|
|
@@ -488,15 +509,23 @@ class RenderContext {
|
|
|
488
509
|
case "url": {
|
|
489
510
|
const url = source.data;
|
|
490
511
|
if (url.startsWith("/") && !url.startsWith("//")) {
|
|
512
|
+
if (!this.settings.allowLocalPaths)
|
|
513
|
+
return null;
|
|
491
514
|
return `/local--files${url}`;
|
|
492
515
|
}
|
|
493
516
|
return url;
|
|
494
517
|
}
|
|
495
518
|
case "file1":
|
|
519
|
+
if (!this.settings.allowLocalPaths)
|
|
520
|
+
return null;
|
|
496
521
|
return pageName ? `/local--files/${pageName}/${source.data.file}` : `/local--files/${source.data.file}`;
|
|
497
522
|
case "file2":
|
|
523
|
+
if (!this.settings.allowLocalPaths)
|
|
524
|
+
return null;
|
|
498
525
|
return `/local--files/${source.data.page}/${source.data.file}`;
|
|
499
526
|
case "file3":
|
|
527
|
+
if (!this.settings.allowLocalPaths)
|
|
528
|
+
return null;
|
|
500
529
|
return `/local--files/${source.data.site}/${source.data.page}/${source.data.file}`;
|
|
501
530
|
}
|
|
502
531
|
}
|
|
@@ -548,28 +577,28 @@ class RenderContext {
|
|
|
548
577
|
}
|
|
549
578
|
|
|
550
579
|
// packages/render/src/elements/container.ts
|
|
551
|
-
var
|
|
580
|
+
var import_ast2 = require("@wdprlib/ast");
|
|
552
581
|
function renderContainer(ctx, data) {
|
|
553
582
|
const { type, attributes, elements } = data;
|
|
554
|
-
if (
|
|
583
|
+
if (import_ast2.isHeaderType(type)) {
|
|
555
584
|
renderHeader(ctx, type.header.level, type.header["has-toc"], attributes, elements);
|
|
556
585
|
return;
|
|
557
586
|
}
|
|
558
|
-
if (
|
|
587
|
+
if (import_ast2.isAlignType(type)) {
|
|
559
588
|
ctx.push(`<div style="text-align: ${type.align};">`);
|
|
560
589
|
renderElements(ctx, elements);
|
|
561
590
|
ctx.push("</div>");
|
|
562
591
|
return;
|
|
563
592
|
}
|
|
564
|
-
if (
|
|
593
|
+
if (import_ast2.isStringContainerType(type)) {
|
|
565
594
|
renderStringContainer(ctx, type, attributes, elements);
|
|
566
595
|
}
|
|
567
596
|
}
|
|
568
597
|
function renderHeader(ctx, level, hasToc, attributes, elements) {
|
|
569
598
|
const tag = `h${level}`;
|
|
570
599
|
if (hasToc) {
|
|
571
|
-
const tocId = ctx.nextTocIndex();
|
|
572
|
-
ctx.push(`<${tag} id="
|
|
600
|
+
const tocId = ctx.generateId("toc", ctx.nextTocIndex());
|
|
601
|
+
ctx.push(`<${tag} id="${tocId}"${renderAttrs(attributes)}>`);
|
|
573
602
|
} else {
|
|
574
603
|
ctx.push(`<${tag}${renderAttrs(attributes)}>`);
|
|
575
604
|
}
|
|
@@ -855,6 +884,8 @@ function renderAnchorName(ctx, name) {
|
|
|
855
884
|
// packages/render/src/elements/image.ts
|
|
856
885
|
function renderImage(ctx, data) {
|
|
857
886
|
let src = ctx.resolveImageSource(data.source);
|
|
887
|
+
if (src === null)
|
|
888
|
+
return;
|
|
858
889
|
if (isDangerousUrl(src)) {
|
|
859
890
|
src = "#invalid-url";
|
|
860
891
|
}
|
|
@@ -3939,7 +3970,7 @@ function fnv1aHash(input, hexLen) {
|
|
|
3939
3970
|
function renderTabView(ctx, tabs) {
|
|
3940
3971
|
const labelString = tabs.map((t) => t.label).join("");
|
|
3941
3972
|
const hash = md5Hash(labelString);
|
|
3942
|
-
const widgetId = `wiki-tabview-${hash}
|
|
3973
|
+
const widgetId = ctx.generateFixedId(`wiki-tabview-${hash}`);
|
|
3943
3974
|
ctx.push(`<div id="${widgetId}" class="yui-navset">`);
|
|
3944
3975
|
ctx.push(`<ul class="yui-nav">`);
|
|
3945
3976
|
for (let i = 0;i < tabs.length; i++) {
|
|
@@ -3954,7 +3985,8 @@ function renderTabView(ctx, tabs) {
|
|
|
3954
3985
|
for (let i = 0;i < tabs.length; i++) {
|
|
3955
3986
|
const tab = tabs[i];
|
|
3956
3987
|
const displayStyle = i === 0 ? "" : ` style="display:none"`;
|
|
3957
|
-
ctx.
|
|
3988
|
+
const tabId = ctx.generateId("wiki-tab-0-", i);
|
|
3989
|
+
ctx.push(`<div id="${tabId}"${displayStyle}>`);
|
|
3958
3990
|
renderElements(ctx, tab.elements);
|
|
3959
3991
|
ctx.push("</div>");
|
|
3960
3992
|
}
|
|
@@ -3967,8 +3999,9 @@ function md5Hash(input) {
|
|
|
3967
3999
|
|
|
3968
4000
|
// packages/render/src/elements/footnote.ts
|
|
3969
4001
|
function renderFootnoteRef(ctx, index) {
|
|
4002
|
+
const id = ctx.generateId("footnoteref-", index);
|
|
3970
4003
|
ctx.push(`<sup class="footnoteref">`);
|
|
3971
|
-
ctx.push(`<a id="
|
|
4004
|
+
ctx.push(`<a id="${id}" href="javascript:;" class="footnoteref">${index}</a>`);
|
|
3972
4005
|
ctx.push("</sup>");
|
|
3973
4006
|
}
|
|
3974
4007
|
function renderFootnoteBlock(ctx, data) {
|
|
@@ -3980,7 +4013,8 @@ function renderFootnoteBlock(ctx, data) {
|
|
|
3980
4013
|
for (let i = 0;i < ctx.footnotes.length; i++) {
|
|
3981
4014
|
const index = i + 1;
|
|
3982
4015
|
const elements = ctx.footnotes[i] ?? [];
|
|
3983
|
-
ctx.
|
|
4016
|
+
const fnId = ctx.generateId("footnote-", index);
|
|
4017
|
+
ctx.push(`<div class="footnote-footer" id="${fnId}">`);
|
|
3984
4018
|
ctx.push(`<a href="javascript:;">${index}</a>. `);
|
|
3985
4019
|
renderElements(ctx, elements);
|
|
3986
4020
|
ctx.push("</div>");
|
|
@@ -4018,7 +4052,7 @@ function renderMath(ctx, data) {
|
|
|
4018
4052
|
const index = ctx.nextEquationIndex() + 1;
|
|
4019
4053
|
const latex = data["latex-source"];
|
|
4020
4054
|
const mathml = renderLatexToMathML(latex, true);
|
|
4021
|
-
const id = data.name ?
|
|
4055
|
+
const id = data.name ? ctx.generateId("equation-", data.name) : ctx.generateId("equation-", index);
|
|
4022
4056
|
const dataName = data.name ? ` data-name="${escapeAttr(data.name)}"` : "";
|
|
4023
4057
|
ctx.push(`<div class="math-block" id="${escapeAttr(id)}"${dataName}>`);
|
|
4024
4058
|
if (data.name) {
|
|
@@ -4057,7 +4091,7 @@ function renderMathInline(ctx, data) {
|
|
|
4057
4091
|
ctx.push("</span>");
|
|
4058
4092
|
}
|
|
4059
4093
|
function renderEquationRef(ctx, name) {
|
|
4060
|
-
const id =
|
|
4094
|
+
const id = ctx.generateId("equation-", name);
|
|
4061
4095
|
ctx.push(`<span class="eref" data-target="${escapeAttr(id)}">`);
|
|
4062
4096
|
ctx.push(`<a class="eref-link" href="#${escapeAttr(id)}">`);
|
|
4063
4097
|
ctx.push(escapeHtml(name));
|
|
@@ -4209,8 +4243,8 @@ function renderGitlabSnippet(ctx, snippetId) {
|
|
|
4209
4243
|
}
|
|
4210
4244
|
|
|
4211
4245
|
// packages/render/src/elements/embed-block.ts
|
|
4212
|
-
var
|
|
4213
|
-
var
|
|
4246
|
+
var import_htmlparser2 = require("htmlparser2");
|
|
4247
|
+
var import_sanitize_html = __toESM(require("sanitize-html"));
|
|
4214
4248
|
var BOOLEAN_ATTRIBUTES = [
|
|
4215
4249
|
"allowfullscreen",
|
|
4216
4250
|
"async",
|
|
@@ -4244,21 +4278,42 @@ var DEFAULT_EMBED_ALLOWLIST = [
|
|
|
4244
4278
|
{ host: "w.soundcloud.com", pathPrefix: "/player/" },
|
|
4245
4279
|
{ host: "codepen.io" }
|
|
4246
4280
|
];
|
|
4247
|
-
var
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4281
|
+
var SANITIZE_CONFIG = {
|
|
4282
|
+
allowedTags: ["iframe"],
|
|
4283
|
+
allowedAttributes: {
|
|
4284
|
+
iframe: [
|
|
4285
|
+
"src",
|
|
4286
|
+
"allow",
|
|
4287
|
+
"allowfullscreen",
|
|
4288
|
+
"frameborder",
|
|
4289
|
+
"height",
|
|
4290
|
+
"loading",
|
|
4291
|
+
"referrerpolicy",
|
|
4292
|
+
"sandbox",
|
|
4293
|
+
"title",
|
|
4294
|
+
"width"
|
|
4295
|
+
]
|
|
4296
|
+
},
|
|
4297
|
+
allowedSchemes: ["https"]
|
|
4298
|
+
};
|
|
4299
|
+
function findIframes(html) {
|
|
4300
|
+
const doc = import_htmlparser2.parseDocument(html);
|
|
4301
|
+
const iframes = [];
|
|
4302
|
+
function walk(nodes) {
|
|
4303
|
+
for (const node of nodes) {
|
|
4304
|
+
if (node.type === "tag") {
|
|
4305
|
+
if (node.name === "iframe") {
|
|
4306
|
+
iframes.push(node);
|
|
4307
|
+
}
|
|
4308
|
+
if (node.children) {
|
|
4309
|
+
walk(node.children);
|
|
4310
|
+
}
|
|
4311
|
+
}
|
|
4254
4312
|
}
|
|
4255
4313
|
}
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
ADD_ATTR: ["allow", "allowfullscreen", "frameborder", "loading", "referrerpolicy", "sandbox"],
|
|
4260
|
-
FORBID_ATTR: ["srcdoc", "onload", "onerror", "onclick"]
|
|
4261
|
-
};
|
|
4314
|
+
walk(doc.children);
|
|
4315
|
+
return iframes;
|
|
4316
|
+
}
|
|
4262
4317
|
function matchesHostPattern(hostname, pattern) {
|
|
4263
4318
|
const lowerHostname = hostname.toLowerCase();
|
|
4264
4319
|
const lowerPattern = pattern.toLowerCase();
|
|
@@ -4288,20 +4343,16 @@ function matchesAllowlistEntry(url, entry) {
|
|
|
4288
4343
|
return true;
|
|
4289
4344
|
}
|
|
4290
4345
|
function validateAndSanitizeEmbed(content, allowlist) {
|
|
4291
|
-
const sanitized =
|
|
4292
|
-
...DOMPURIFY_CONFIG,
|
|
4293
|
-
RETURN_TRUSTED_TYPE: false
|
|
4294
|
-
});
|
|
4346
|
+
const sanitized = import_sanitize_html.default(content.trim(), SANITIZE_CONFIG);
|
|
4295
4347
|
if (!sanitized.trim()) {
|
|
4296
4348
|
return null;
|
|
4297
4349
|
}
|
|
4298
|
-
const
|
|
4299
|
-
const iframes = dom.window.document.querySelectorAll("iframe");
|
|
4350
|
+
const iframes = findIframes(sanitized);
|
|
4300
4351
|
if (iframes.length !== 1) {
|
|
4301
4352
|
return null;
|
|
4302
4353
|
}
|
|
4303
4354
|
const iframe = iframes[0];
|
|
4304
|
-
const src = iframe.
|
|
4355
|
+
const src = iframe.attribs.src?.trim();
|
|
4305
4356
|
if (!src) {
|
|
4306
4357
|
return null;
|
|
4307
4358
|
}
|
|
@@ -4395,8 +4446,9 @@ function renderBibliographyCite(ctx, data) {
|
|
|
4395
4446
|
return;
|
|
4396
4447
|
}
|
|
4397
4448
|
const idSuffix = generateIdSuffix(data.label, counter);
|
|
4398
|
-
const id = `bibcite-${number}
|
|
4399
|
-
const
|
|
4449
|
+
const id = ctx.generateId(`bibcite-${number}-`, idSuffix);
|
|
4450
|
+
const bibitemId = ctx.generateId("bibitem-", number);
|
|
4451
|
+
const onclick = `WIKIDOT.page.utils.scrollToReference('${bibitemId}')`;
|
|
4400
4452
|
ctx.push(`<a href="javascript:;" class="bibcite" id="${id}" onclick="${escapeAttr(onclick)}">`);
|
|
4401
4453
|
ctx.push(String(number));
|
|
4402
4454
|
ctx.push("</a>");
|
|
@@ -4409,7 +4461,8 @@ function renderBibliographyBlock(ctx, data, renderElements2) {
|
|
|
4409
4461
|
ctx.push(`<div class="title">${escapeHtml(title)}</div>`);
|
|
4410
4462
|
let index = 1;
|
|
4411
4463
|
for (const entry of data.entries) {
|
|
4412
|
-
ctx.
|
|
4464
|
+
const itemId = ctx.generateId("bibitem-", index);
|
|
4465
|
+
ctx.push(`<div class="bibitem" id="${itemId}">`);
|
|
4413
4466
|
ctx.push(`${index}. `);
|
|
4414
4467
|
renderElements2(ctx, entry.value);
|
|
4415
4468
|
ctx.push("</div>");
|
|
@@ -4442,12 +4495,19 @@ function renderTocList(ctx, listData, depth) {
|
|
|
4442
4495
|
renderTocItem(ctx, item, depth);
|
|
4443
4496
|
}
|
|
4444
4497
|
}
|
|
4498
|
+
function rewriteTocAnchor(ctx, href) {
|
|
4499
|
+
const match = /^#toc(\d+)$/.exec(href);
|
|
4500
|
+
if (!match)
|
|
4501
|
+
return href;
|
|
4502
|
+
return `#${ctx.generateId("toc", Number(match[1]))}`;
|
|
4503
|
+
}
|
|
4445
4504
|
function renderTocItem(ctx, item, depth) {
|
|
4446
4505
|
if (item["item-type"] === "elements") {
|
|
4447
4506
|
for (const el of item.elements) {
|
|
4448
4507
|
const link = extractLinkText(el);
|
|
4449
4508
|
if (link) {
|
|
4450
|
-
|
|
4509
|
+
const href = rewriteTocAnchor(ctx, link.href);
|
|
4510
|
+
ctx.push(`<div style="margin-left: ${depth}em;"><a href="${escapeAttr(href)}">${escapeHtml(link.text)}</a></div>`);
|
|
4451
4511
|
}
|
|
4452
4512
|
}
|
|
4453
4513
|
} else if (item["item-type"] === "sub-list") {
|
|
@@ -5012,7 +5072,7 @@ function formatNumber(n) {
|
|
|
5012
5072
|
function renderToHtml(tree, options = {}) {
|
|
5013
5073
|
const ctx = new RenderContext(tree, options);
|
|
5014
5074
|
renderElements(ctx, tree.elements);
|
|
5015
|
-
if (tree.styles?.length) {
|
|
5075
|
+
if (ctx.settings.allowStyleElements && tree.styles?.length) {
|
|
5016
5076
|
for (const style of tree.styles) {
|
|
5017
5077
|
ctx.push(`<style>${escapeStyleContent(style)}</style>`);
|
|
5018
5078
|
}
|
|
@@ -5156,3 +5216,6 @@ function renderElement(ctx, element) {
|
|
|
5156
5216
|
break;
|
|
5157
5217
|
}
|
|
5158
5218
|
}
|
|
5219
|
+
|
|
5220
|
+
// packages/render/src/index.ts
|
|
5221
|
+
var import_ast3 = require("@wdprlib/ast");
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SyntaxTree as SyntaxTree2 } from "@wdprlib/ast";
|
|
2
|
-
import { Element } from "@wdprlib/ast";
|
|
2
|
+
import { Element, WikitextSettings } from "@wdprlib/ast";
|
|
3
3
|
/**
|
|
4
4
|
* Allowlist entry for embed content validation
|
|
5
5
|
* Each entry specifies a host pattern and optional path prefix
|
|
@@ -15,7 +15,7 @@ interface EmbedAllowlistEntry {
|
|
|
15
15
|
* Only iframes with src matching these host+path patterns will be rendered.
|
|
16
16
|
*
|
|
17
17
|
* Note: Set to null to allow any HTTPS iframe (Wikidot's 'anyiframe' behavior).
|
|
18
|
-
*
|
|
18
|
+
* sanitize-html still enforces HTTPS-only and blocks dangerous attributes.
|
|
19
19
|
*/
|
|
20
20
|
declare const DEFAULT_EMBED_ALLOWLIST: EmbedAllowlistEntry[] | null;
|
|
21
21
|
/**
|
|
@@ -70,6 +70,8 @@ interface RenderResolvers {
|
|
|
70
70
|
* Options for HTML rendering
|
|
71
71
|
*/
|
|
72
72
|
interface RenderOptions {
|
|
73
|
+
/** Wikitext settings controlling rendering behavior */
|
|
74
|
+
settings?: WikitextSettings;
|
|
73
75
|
/** Page context for resolving file paths, links, etc. */
|
|
74
76
|
page?: PageContext;
|
|
75
77
|
/** Pre-collected footnote elements from SyntaxTree.footnotes */
|
|
@@ -100,4 +102,6 @@ interface RenderOptions {
|
|
|
100
102
|
* Render a SyntaxTree to HTML string
|
|
101
103
|
*/
|
|
102
104
|
declare function renderToHtml(tree: SyntaxTree2, options?: RenderOptions): string;
|
|
103
|
-
|
|
105
|
+
import { WikitextMode, WikitextSettings as WikitextSettings3 } from "@wdprlib/ast";
|
|
106
|
+
import { createSettings, DEFAULT_SETTINGS } from "@wdprlib/ast";
|
|
107
|
+
export { renderToHtml, createSettings, WikitextSettings3 as WikitextSettings, WikitextMode, ResolvedUser, RenderResolvers, RenderOptions, PageContext, DEFAULT_SETTINGS, DEFAULT_EMBED_ALLOWLIST };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SyntaxTree as SyntaxTree2 } from "@wdprlib/ast";
|
|
2
|
-
import { Element } from "@wdprlib/ast";
|
|
2
|
+
import { Element, WikitextSettings } from "@wdprlib/ast";
|
|
3
3
|
/**
|
|
4
4
|
* Allowlist entry for embed content validation
|
|
5
5
|
* Each entry specifies a host pattern and optional path prefix
|
|
@@ -15,7 +15,7 @@ interface EmbedAllowlistEntry {
|
|
|
15
15
|
* Only iframes with src matching these host+path patterns will be rendered.
|
|
16
16
|
*
|
|
17
17
|
* Note: Set to null to allow any HTTPS iframe (Wikidot's 'anyiframe' behavior).
|
|
18
|
-
*
|
|
18
|
+
* sanitize-html still enforces HTTPS-only and blocks dangerous attributes.
|
|
19
19
|
*/
|
|
20
20
|
declare const DEFAULT_EMBED_ALLOWLIST: EmbedAllowlistEntry[] | null;
|
|
21
21
|
/**
|
|
@@ -70,6 +70,8 @@ interface RenderResolvers {
|
|
|
70
70
|
* Options for HTML rendering
|
|
71
71
|
*/
|
|
72
72
|
interface RenderOptions {
|
|
73
|
+
/** Wikitext settings controlling rendering behavior */
|
|
74
|
+
settings?: WikitextSettings;
|
|
73
75
|
/** Page context for resolving file paths, links, etc. */
|
|
74
76
|
page?: PageContext;
|
|
75
77
|
/** Pre-collected footnote elements from SyntaxTree.footnotes */
|
|
@@ -100,4 +102,6 @@ interface RenderOptions {
|
|
|
100
102
|
* Render a SyntaxTree to HTML string
|
|
101
103
|
*/
|
|
102
104
|
declare function renderToHtml(tree: SyntaxTree2, options?: RenderOptions): string;
|
|
103
|
-
|
|
105
|
+
import { WikitextMode, WikitextSettings as WikitextSettings3 } from "@wdprlib/ast";
|
|
106
|
+
import { createSettings, DEFAULT_SETTINGS } from "@wdprlib/ast";
|
|
107
|
+
export { renderToHtml, createSettings, WikitextSettings3 as WikitextSettings, WikitextMode, ResolvedUser, RenderResolvers, RenderOptions, PageContext, DEFAULT_SETTINGS, DEFAULT_EMBED_ALLOWLIST };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// packages/render/src/context.ts
|
|
2
|
+
import { DEFAULT_SETTINGS } from "@wdprlib/ast";
|
|
3
|
+
|
|
1
4
|
// packages/render/src/escape.ts
|
|
2
5
|
function escapeHtml(text) {
|
|
3
6
|
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
@@ -368,6 +371,8 @@ class RenderContext {
|
|
|
368
371
|
_equationIndex = 0;
|
|
369
372
|
_htmlBlockIndex = 0;
|
|
370
373
|
_bibciteCounter = 0;
|
|
374
|
+
_idSuffix;
|
|
375
|
+
settings;
|
|
371
376
|
options;
|
|
372
377
|
footnotes;
|
|
373
378
|
styles;
|
|
@@ -376,6 +381,8 @@ class RenderContext {
|
|
|
376
381
|
bibliographyMap;
|
|
377
382
|
bibliographyEntries;
|
|
378
383
|
constructor(tree, options = {}) {
|
|
384
|
+
this.settings = options.settings ?? DEFAULT_SETTINGS;
|
|
385
|
+
this._idSuffix = this.settings.useTrueIds ? null : Math.random().toString(16).slice(2, 8);
|
|
379
386
|
this.options = options;
|
|
380
387
|
this.footnotes = options.footnotes ?? tree.footnotes ?? [];
|
|
381
388
|
this.styles = tree.styles ?? [];
|
|
@@ -429,6 +436,18 @@ class RenderContext {
|
|
|
429
436
|
nextBibciteCounter() {
|
|
430
437
|
return ++this._bibciteCounter;
|
|
431
438
|
}
|
|
439
|
+
generateId(prefix, index) {
|
|
440
|
+
if (this._idSuffix === null) {
|
|
441
|
+
return `${prefix}${index}`;
|
|
442
|
+
}
|
|
443
|
+
return `${prefix}${index}-${this._idSuffix}`;
|
|
444
|
+
}
|
|
445
|
+
generateFixedId(name) {
|
|
446
|
+
if (this._idSuffix === null) {
|
|
447
|
+
return name;
|
|
448
|
+
}
|
|
449
|
+
return `${name}-${this._idSuffix}`;
|
|
450
|
+
}
|
|
432
451
|
get page() {
|
|
433
452
|
return this.options.page;
|
|
434
453
|
}
|
|
@@ -438,15 +457,23 @@ class RenderContext {
|
|
|
438
457
|
case "url": {
|
|
439
458
|
const url = source.data;
|
|
440
459
|
if (url.startsWith("/") && !url.startsWith("//")) {
|
|
460
|
+
if (!this.settings.allowLocalPaths)
|
|
461
|
+
return null;
|
|
441
462
|
return `/local--files${url}`;
|
|
442
463
|
}
|
|
443
464
|
return url;
|
|
444
465
|
}
|
|
445
466
|
case "file1":
|
|
467
|
+
if (!this.settings.allowLocalPaths)
|
|
468
|
+
return null;
|
|
446
469
|
return pageName ? `/local--files/${pageName}/${source.data.file}` : `/local--files/${source.data.file}`;
|
|
447
470
|
case "file2":
|
|
471
|
+
if (!this.settings.allowLocalPaths)
|
|
472
|
+
return null;
|
|
448
473
|
return `/local--files/${source.data.page}/${source.data.file}`;
|
|
449
474
|
case "file3":
|
|
475
|
+
if (!this.settings.allowLocalPaths)
|
|
476
|
+
return null;
|
|
450
477
|
return `/local--files/${source.data.site}/${source.data.page}/${source.data.file}`;
|
|
451
478
|
}
|
|
452
479
|
}
|
|
@@ -518,8 +545,8 @@ function renderContainer(ctx, data) {
|
|
|
518
545
|
function renderHeader(ctx, level, hasToc, attributes, elements) {
|
|
519
546
|
const tag = `h${level}`;
|
|
520
547
|
if (hasToc) {
|
|
521
|
-
const tocId = ctx.nextTocIndex();
|
|
522
|
-
ctx.push(`<${tag} id="
|
|
548
|
+
const tocId = ctx.generateId("toc", ctx.nextTocIndex());
|
|
549
|
+
ctx.push(`<${tag} id="${tocId}"${renderAttrs(attributes)}>`);
|
|
523
550
|
} else {
|
|
524
551
|
ctx.push(`<${tag}${renderAttrs(attributes)}>`);
|
|
525
552
|
}
|
|
@@ -805,6 +832,8 @@ function renderAnchorName(ctx, name) {
|
|
|
805
832
|
// packages/render/src/elements/image.ts
|
|
806
833
|
function renderImage(ctx, data) {
|
|
807
834
|
let src = ctx.resolveImageSource(data.source);
|
|
835
|
+
if (src === null)
|
|
836
|
+
return;
|
|
808
837
|
if (isDangerousUrl(src)) {
|
|
809
838
|
src = "#invalid-url";
|
|
810
839
|
}
|
|
@@ -3889,7 +3918,7 @@ function fnv1aHash(input, hexLen) {
|
|
|
3889
3918
|
function renderTabView(ctx, tabs) {
|
|
3890
3919
|
const labelString = tabs.map((t) => t.label).join("");
|
|
3891
3920
|
const hash = md5Hash(labelString);
|
|
3892
|
-
const widgetId = `wiki-tabview-${hash}
|
|
3921
|
+
const widgetId = ctx.generateFixedId(`wiki-tabview-${hash}`);
|
|
3893
3922
|
ctx.push(`<div id="${widgetId}" class="yui-navset">`);
|
|
3894
3923
|
ctx.push(`<ul class="yui-nav">`);
|
|
3895
3924
|
for (let i = 0;i < tabs.length; i++) {
|
|
@@ -3904,7 +3933,8 @@ function renderTabView(ctx, tabs) {
|
|
|
3904
3933
|
for (let i = 0;i < tabs.length; i++) {
|
|
3905
3934
|
const tab = tabs[i];
|
|
3906
3935
|
const displayStyle = i === 0 ? "" : ` style="display:none"`;
|
|
3907
|
-
ctx.
|
|
3936
|
+
const tabId = ctx.generateId("wiki-tab-0-", i);
|
|
3937
|
+
ctx.push(`<div id="${tabId}"${displayStyle}>`);
|
|
3908
3938
|
renderElements(ctx, tab.elements);
|
|
3909
3939
|
ctx.push("</div>");
|
|
3910
3940
|
}
|
|
@@ -3917,8 +3947,9 @@ function md5Hash(input) {
|
|
|
3917
3947
|
|
|
3918
3948
|
// packages/render/src/elements/footnote.ts
|
|
3919
3949
|
function renderFootnoteRef(ctx, index) {
|
|
3950
|
+
const id = ctx.generateId("footnoteref-", index);
|
|
3920
3951
|
ctx.push(`<sup class="footnoteref">`);
|
|
3921
|
-
ctx.push(`<a id="
|
|
3952
|
+
ctx.push(`<a id="${id}" href="javascript:;" class="footnoteref">${index}</a>`);
|
|
3922
3953
|
ctx.push("</sup>");
|
|
3923
3954
|
}
|
|
3924
3955
|
function renderFootnoteBlock(ctx, data) {
|
|
@@ -3930,7 +3961,8 @@ function renderFootnoteBlock(ctx, data) {
|
|
|
3930
3961
|
for (let i = 0;i < ctx.footnotes.length; i++) {
|
|
3931
3962
|
const index = i + 1;
|
|
3932
3963
|
const elements = ctx.footnotes[i] ?? [];
|
|
3933
|
-
ctx.
|
|
3964
|
+
const fnId = ctx.generateId("footnote-", index);
|
|
3965
|
+
ctx.push(`<div class="footnote-footer" id="${fnId}">`);
|
|
3934
3966
|
ctx.push(`<a href="javascript:;">${index}</a>. `);
|
|
3935
3967
|
renderElements(ctx, elements);
|
|
3936
3968
|
ctx.push("</div>");
|
|
@@ -3968,7 +4000,7 @@ function renderMath(ctx, data) {
|
|
|
3968
4000
|
const index = ctx.nextEquationIndex() + 1;
|
|
3969
4001
|
const latex = data["latex-source"];
|
|
3970
4002
|
const mathml = renderLatexToMathML(latex, true);
|
|
3971
|
-
const id = data.name ?
|
|
4003
|
+
const id = data.name ? ctx.generateId("equation-", data.name) : ctx.generateId("equation-", index);
|
|
3972
4004
|
const dataName = data.name ? ` data-name="${escapeAttr(data.name)}"` : "";
|
|
3973
4005
|
ctx.push(`<div class="math-block" id="${escapeAttr(id)}"${dataName}>`);
|
|
3974
4006
|
if (data.name) {
|
|
@@ -4007,7 +4039,7 @@ function renderMathInline(ctx, data) {
|
|
|
4007
4039
|
ctx.push("</span>");
|
|
4008
4040
|
}
|
|
4009
4041
|
function renderEquationRef(ctx, name) {
|
|
4010
|
-
const id =
|
|
4042
|
+
const id = ctx.generateId("equation-", name);
|
|
4011
4043
|
ctx.push(`<span class="eref" data-target="${escapeAttr(id)}">`);
|
|
4012
4044
|
ctx.push(`<a class="eref-link" href="#${escapeAttr(id)}">`);
|
|
4013
4045
|
ctx.push(escapeHtml(name));
|
|
@@ -4159,8 +4191,8 @@ function renderGitlabSnippet(ctx, snippetId) {
|
|
|
4159
4191
|
}
|
|
4160
4192
|
|
|
4161
4193
|
// packages/render/src/elements/embed-block.ts
|
|
4162
|
-
import
|
|
4163
|
-
import
|
|
4194
|
+
import { parseDocument } from "htmlparser2";
|
|
4195
|
+
import sanitizeHtml from "sanitize-html";
|
|
4164
4196
|
var BOOLEAN_ATTRIBUTES = [
|
|
4165
4197
|
"allowfullscreen",
|
|
4166
4198
|
"async",
|
|
@@ -4194,21 +4226,42 @@ var DEFAULT_EMBED_ALLOWLIST = [
|
|
|
4194
4226
|
{ host: "w.soundcloud.com", pathPrefix: "/player/" },
|
|
4195
4227
|
{ host: "codepen.io" }
|
|
4196
4228
|
];
|
|
4197
|
-
var
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4229
|
+
var SANITIZE_CONFIG = {
|
|
4230
|
+
allowedTags: ["iframe"],
|
|
4231
|
+
allowedAttributes: {
|
|
4232
|
+
iframe: [
|
|
4233
|
+
"src",
|
|
4234
|
+
"allow",
|
|
4235
|
+
"allowfullscreen",
|
|
4236
|
+
"frameborder",
|
|
4237
|
+
"height",
|
|
4238
|
+
"loading",
|
|
4239
|
+
"referrerpolicy",
|
|
4240
|
+
"sandbox",
|
|
4241
|
+
"title",
|
|
4242
|
+
"width"
|
|
4243
|
+
]
|
|
4244
|
+
},
|
|
4245
|
+
allowedSchemes: ["https"]
|
|
4246
|
+
};
|
|
4247
|
+
function findIframes(html) {
|
|
4248
|
+
const doc = parseDocument(html);
|
|
4249
|
+
const iframes = [];
|
|
4250
|
+
function walk(nodes) {
|
|
4251
|
+
for (const node of nodes) {
|
|
4252
|
+
if (node.type === "tag") {
|
|
4253
|
+
if (node.name === "iframe") {
|
|
4254
|
+
iframes.push(node);
|
|
4255
|
+
}
|
|
4256
|
+
if (node.children) {
|
|
4257
|
+
walk(node.children);
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4204
4260
|
}
|
|
4205
4261
|
}
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
ADD_ATTR: ["allow", "allowfullscreen", "frameborder", "loading", "referrerpolicy", "sandbox"],
|
|
4210
|
-
FORBID_ATTR: ["srcdoc", "onload", "onerror", "onclick"]
|
|
4211
|
-
};
|
|
4262
|
+
walk(doc.children);
|
|
4263
|
+
return iframes;
|
|
4264
|
+
}
|
|
4212
4265
|
function matchesHostPattern(hostname, pattern) {
|
|
4213
4266
|
const lowerHostname = hostname.toLowerCase();
|
|
4214
4267
|
const lowerPattern = pattern.toLowerCase();
|
|
@@ -4238,20 +4291,16 @@ function matchesAllowlistEntry(url, entry) {
|
|
|
4238
4291
|
return true;
|
|
4239
4292
|
}
|
|
4240
4293
|
function validateAndSanitizeEmbed(content, allowlist) {
|
|
4241
|
-
const sanitized =
|
|
4242
|
-
...DOMPURIFY_CONFIG,
|
|
4243
|
-
RETURN_TRUSTED_TYPE: false
|
|
4244
|
-
});
|
|
4294
|
+
const sanitized = sanitizeHtml(content.trim(), SANITIZE_CONFIG);
|
|
4245
4295
|
if (!sanitized.trim()) {
|
|
4246
4296
|
return null;
|
|
4247
4297
|
}
|
|
4248
|
-
const
|
|
4249
|
-
const iframes = dom.window.document.querySelectorAll("iframe");
|
|
4298
|
+
const iframes = findIframes(sanitized);
|
|
4250
4299
|
if (iframes.length !== 1) {
|
|
4251
4300
|
return null;
|
|
4252
4301
|
}
|
|
4253
4302
|
const iframe = iframes[0];
|
|
4254
|
-
const src = iframe.
|
|
4303
|
+
const src = iframe.attribs.src?.trim();
|
|
4255
4304
|
if (!src) {
|
|
4256
4305
|
return null;
|
|
4257
4306
|
}
|
|
@@ -4345,8 +4394,9 @@ function renderBibliographyCite(ctx, data) {
|
|
|
4345
4394
|
return;
|
|
4346
4395
|
}
|
|
4347
4396
|
const idSuffix = generateIdSuffix(data.label, counter);
|
|
4348
|
-
const id = `bibcite-${number}
|
|
4349
|
-
const
|
|
4397
|
+
const id = ctx.generateId(`bibcite-${number}-`, idSuffix);
|
|
4398
|
+
const bibitemId = ctx.generateId("bibitem-", number);
|
|
4399
|
+
const onclick = `WIKIDOT.page.utils.scrollToReference('${bibitemId}')`;
|
|
4350
4400
|
ctx.push(`<a href="javascript:;" class="bibcite" id="${id}" onclick="${escapeAttr(onclick)}">`);
|
|
4351
4401
|
ctx.push(String(number));
|
|
4352
4402
|
ctx.push("</a>");
|
|
@@ -4359,7 +4409,8 @@ function renderBibliographyBlock(ctx, data, renderElements2) {
|
|
|
4359
4409
|
ctx.push(`<div class="title">${escapeHtml(title)}</div>`);
|
|
4360
4410
|
let index = 1;
|
|
4361
4411
|
for (const entry of data.entries) {
|
|
4362
|
-
ctx.
|
|
4412
|
+
const itemId = ctx.generateId("bibitem-", index);
|
|
4413
|
+
ctx.push(`<div class="bibitem" id="${itemId}">`);
|
|
4363
4414
|
ctx.push(`${index}. `);
|
|
4364
4415
|
renderElements2(ctx, entry.value);
|
|
4365
4416
|
ctx.push("</div>");
|
|
@@ -4392,12 +4443,19 @@ function renderTocList(ctx, listData, depth) {
|
|
|
4392
4443
|
renderTocItem(ctx, item, depth);
|
|
4393
4444
|
}
|
|
4394
4445
|
}
|
|
4446
|
+
function rewriteTocAnchor(ctx, href) {
|
|
4447
|
+
const match = /^#toc(\d+)$/.exec(href);
|
|
4448
|
+
if (!match)
|
|
4449
|
+
return href;
|
|
4450
|
+
return `#${ctx.generateId("toc", Number(match[1]))}`;
|
|
4451
|
+
}
|
|
4395
4452
|
function renderTocItem(ctx, item, depth) {
|
|
4396
4453
|
if (item["item-type"] === "elements") {
|
|
4397
4454
|
for (const el of item.elements) {
|
|
4398
4455
|
const link = extractLinkText(el);
|
|
4399
4456
|
if (link) {
|
|
4400
|
-
|
|
4457
|
+
const href = rewriteTocAnchor(ctx, link.href);
|
|
4458
|
+
ctx.push(`<div style="margin-left: ${depth}em;"><a href="${escapeAttr(href)}">${escapeHtml(link.text)}</a></div>`);
|
|
4401
4459
|
}
|
|
4402
4460
|
}
|
|
4403
4461
|
} else if (item["item-type"] === "sub-list") {
|
|
@@ -4962,7 +5020,7 @@ function formatNumber(n) {
|
|
|
4962
5020
|
function renderToHtml(tree, options = {}) {
|
|
4963
5021
|
const ctx = new RenderContext(tree, options);
|
|
4964
5022
|
renderElements(ctx, tree.elements);
|
|
4965
|
-
if (tree.styles?.length) {
|
|
5023
|
+
if (ctx.settings.allowStyleElements && tree.styles?.length) {
|
|
4966
5024
|
for (const style of tree.styles) {
|
|
4967
5025
|
ctx.push(`<style>${escapeStyleContent(style)}</style>`);
|
|
4968
5026
|
}
|
|
@@ -5106,7 +5164,12 @@ function renderElement(ctx, element) {
|
|
|
5106
5164
|
break;
|
|
5107
5165
|
}
|
|
5108
5166
|
}
|
|
5167
|
+
|
|
5168
|
+
// packages/render/src/index.ts
|
|
5169
|
+
import { createSettings, DEFAULT_SETTINGS as DEFAULT_SETTINGS2 } from "@wdprlib/ast";
|
|
5109
5170
|
export {
|
|
5110
5171
|
renderToHtml,
|
|
5172
|
+
createSettings,
|
|
5173
|
+
DEFAULT_SETTINGS2 as DEFAULT_SETTINGS,
|
|
5111
5174
|
DEFAULT_EMBED_ALLOWLIST
|
|
5112
5175
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wdprlib/render",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "HTML renderer for Wikidot markup",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"html",
|
|
@@ -39,13 +39,13 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@wdprlib/ast": "1.
|
|
43
|
-
"
|
|
44
|
-
"
|
|
42
|
+
"@wdprlib/ast": "1.1.0",
|
|
43
|
+
"domhandler": "^5.0.3",
|
|
44
|
+
"htmlparser2": "^10.0.0",
|
|
45
|
+
"sanitize-html": "^2.14.0",
|
|
45
46
|
"temml": "^0.13.1"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
|
-
"@types/
|
|
49
|
-
"@types/jsdom": "^27.0.0"
|
|
49
|
+
"@types/sanitize-html": "^2.13.0"
|
|
50
50
|
}
|
|
51
51
|
}
|