@wdprlib/render 1.0.1 → 1.2.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 +73 -22
- package/dist/index.d.cts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +69 -18
- package/package.json +2 -2
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));
|
|
@@ -4260,7 +4294,7 @@ var SANITIZE_CONFIG = {
|
|
|
4260
4294
|
"width"
|
|
4261
4295
|
]
|
|
4262
4296
|
},
|
|
4263
|
-
allowedSchemes: ["https"]
|
|
4297
|
+
allowedSchemes: ["https", "http"]
|
|
4264
4298
|
};
|
|
4265
4299
|
function findIframes(html) {
|
|
4266
4300
|
const doc = import_htmlparser2.parseDocument(html);
|
|
@@ -4308,7 +4342,7 @@ function matchesAllowlistEntry(url, entry) {
|
|
|
4308
4342
|
}
|
|
4309
4343
|
return true;
|
|
4310
4344
|
}
|
|
4311
|
-
function validateAndSanitizeEmbed(content, allowlist) {
|
|
4345
|
+
function validateAndSanitizeEmbed(content, allowlist, baseUrl) {
|
|
4312
4346
|
const sanitized = import_sanitize_html.default(content.trim(), SANITIZE_CONFIG);
|
|
4313
4347
|
if (!sanitized.trim()) {
|
|
4314
4348
|
return null;
|
|
@@ -4324,11 +4358,16 @@ function validateAndSanitizeEmbed(content, allowlist) {
|
|
|
4324
4358
|
}
|
|
4325
4359
|
let url;
|
|
4326
4360
|
try {
|
|
4327
|
-
|
|
4361
|
+
if (src.startsWith("//")) {
|
|
4362
|
+
const base = baseUrl ?? "https://localhost";
|
|
4363
|
+
url = new URL(src, base);
|
|
4364
|
+
} else {
|
|
4365
|
+
url = new URL(src);
|
|
4366
|
+
}
|
|
4328
4367
|
} catch {
|
|
4329
4368
|
return null;
|
|
4330
4369
|
}
|
|
4331
|
-
if (url.protocol !== "https:") {
|
|
4370
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
4332
4371
|
return null;
|
|
4333
4372
|
}
|
|
4334
4373
|
if (allowlist !== null) {
|
|
@@ -4351,7 +4390,7 @@ function normalizeBooleanAttributes(html) {
|
|
|
4351
4390
|
}
|
|
4352
4391
|
function renderEmbedBlock(ctx, data) {
|
|
4353
4392
|
const allowlist = ctx.options.embedAllowlist !== undefined ? ctx.options.embedAllowlist : DEFAULT_EMBED_ALLOWLIST;
|
|
4354
|
-
const sanitized = validateAndSanitizeEmbed(data.contents, allowlist);
|
|
4393
|
+
const sanitized = validateAndSanitizeEmbed(data.contents, allowlist, ctx.options.baseUrl);
|
|
4355
4394
|
if (sanitized === null) {
|
|
4356
4395
|
ctx.push('<div class="error-block">Sorry, no match for the embedded content.</div>');
|
|
4357
4396
|
return;
|
|
@@ -4412,8 +4451,9 @@ function renderBibliographyCite(ctx, data) {
|
|
|
4412
4451
|
return;
|
|
4413
4452
|
}
|
|
4414
4453
|
const idSuffix = generateIdSuffix(data.label, counter);
|
|
4415
|
-
const id = `bibcite-${number}
|
|
4416
|
-
const
|
|
4454
|
+
const id = ctx.generateId(`bibcite-${number}-`, idSuffix);
|
|
4455
|
+
const bibitemId = ctx.generateId("bibitem-", number);
|
|
4456
|
+
const onclick = `WIKIDOT.page.utils.scrollToReference('${bibitemId}')`;
|
|
4417
4457
|
ctx.push(`<a href="javascript:;" class="bibcite" id="${id}" onclick="${escapeAttr(onclick)}">`);
|
|
4418
4458
|
ctx.push(String(number));
|
|
4419
4459
|
ctx.push("</a>");
|
|
@@ -4426,7 +4466,8 @@ function renderBibliographyBlock(ctx, data, renderElements2) {
|
|
|
4426
4466
|
ctx.push(`<div class="title">${escapeHtml(title)}</div>`);
|
|
4427
4467
|
let index = 1;
|
|
4428
4468
|
for (const entry of data.entries) {
|
|
4429
|
-
ctx.
|
|
4469
|
+
const itemId = ctx.generateId("bibitem-", index);
|
|
4470
|
+
ctx.push(`<div class="bibitem" id="${itemId}">`);
|
|
4430
4471
|
ctx.push(`${index}. `);
|
|
4431
4472
|
renderElements2(ctx, entry.value);
|
|
4432
4473
|
ctx.push("</div>");
|
|
@@ -4459,12 +4500,19 @@ function renderTocList(ctx, listData, depth) {
|
|
|
4459
4500
|
renderTocItem(ctx, item, depth);
|
|
4460
4501
|
}
|
|
4461
4502
|
}
|
|
4503
|
+
function rewriteTocAnchor(ctx, href) {
|
|
4504
|
+
const match = /^#toc(\d+)$/.exec(href);
|
|
4505
|
+
if (!match)
|
|
4506
|
+
return href;
|
|
4507
|
+
return `#${ctx.generateId("toc", Number(match[1]))}`;
|
|
4508
|
+
}
|
|
4462
4509
|
function renderTocItem(ctx, item, depth) {
|
|
4463
4510
|
if (item["item-type"] === "elements") {
|
|
4464
4511
|
for (const el of item.elements) {
|
|
4465
4512
|
const link = extractLinkText(el);
|
|
4466
4513
|
if (link) {
|
|
4467
|
-
|
|
4514
|
+
const href = rewriteTocAnchor(ctx, link.href);
|
|
4515
|
+
ctx.push(`<div style="margin-left: ${depth}em;"><a href="${escapeAttr(href)}">${escapeHtml(link.text)}</a></div>`);
|
|
4468
4516
|
}
|
|
4469
4517
|
}
|
|
4470
4518
|
} else if (item["item-type"] === "sub-list") {
|
|
@@ -5029,7 +5077,7 @@ function formatNumber(n) {
|
|
|
5029
5077
|
function renderToHtml(tree, options = {}) {
|
|
5030
5078
|
const ctx = new RenderContext(tree, options);
|
|
5031
5079
|
renderElements(ctx, tree.elements);
|
|
5032
|
-
if (tree.styles?.length) {
|
|
5080
|
+
if (ctx.settings.allowStyleElements && tree.styles?.length) {
|
|
5033
5081
|
for (const style of tree.styles) {
|
|
5034
5082
|
ctx.push(`<style>${escapeStyleContent(style)}</style>`);
|
|
5035
5083
|
}
|
|
@@ -5173,3 +5221,6 @@ function renderElement(ctx, element) {
|
|
|
5173
5221
|
break;
|
|
5174
5222
|
}
|
|
5175
5223
|
}
|
|
5224
|
+
|
|
5225
|
+
// packages/render/src/index.ts
|
|
5226
|
+
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
|
|
@@ -70,6 +70,15 @@ interface RenderResolvers {
|
|
|
70
70
|
* Options for HTML rendering
|
|
71
71
|
*/
|
|
72
72
|
interface RenderOptions {
|
|
73
|
+
/**
|
|
74
|
+
* Base URL used to resolve protocol-relative URLs (e.g., "//example.com/path").
|
|
75
|
+
* The protocol of this URL is inherited by protocol-relative references.
|
|
76
|
+
* Example: "https://scp-wiki.wikidot.com" or "http://scp-jp.wikidot.com"
|
|
77
|
+
* If not provided, protocol-relative URLs default to HTTPS.
|
|
78
|
+
*/
|
|
79
|
+
baseUrl?: string;
|
|
80
|
+
/** Wikitext settings controlling rendering behavior */
|
|
81
|
+
settings?: WikitextSettings;
|
|
73
82
|
/** Page context for resolving file paths, links, etc. */
|
|
74
83
|
page?: PageContext;
|
|
75
84
|
/** Pre-collected footnote elements from SyntaxTree.footnotes */
|
|
@@ -100,4 +109,6 @@ interface RenderOptions {
|
|
|
100
109
|
* Render a SyntaxTree to HTML string
|
|
101
110
|
*/
|
|
102
111
|
declare function renderToHtml(tree: SyntaxTree2, options?: RenderOptions): string;
|
|
103
|
-
|
|
112
|
+
import { WikitextMode, WikitextSettings as WikitextSettings3 } from "@wdprlib/ast";
|
|
113
|
+
import { createSettings, DEFAULT_SETTINGS } from "@wdprlib/ast";
|
|
114
|
+
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
|
|
@@ -70,6 +70,15 @@ interface RenderResolvers {
|
|
|
70
70
|
* Options for HTML rendering
|
|
71
71
|
*/
|
|
72
72
|
interface RenderOptions {
|
|
73
|
+
/**
|
|
74
|
+
* Base URL used to resolve protocol-relative URLs (e.g., "//example.com/path").
|
|
75
|
+
* The protocol of this URL is inherited by protocol-relative references.
|
|
76
|
+
* Example: "https://scp-wiki.wikidot.com" or "http://scp-jp.wikidot.com"
|
|
77
|
+
* If not provided, protocol-relative URLs default to HTTPS.
|
|
78
|
+
*/
|
|
79
|
+
baseUrl?: string;
|
|
80
|
+
/** Wikitext settings controlling rendering behavior */
|
|
81
|
+
settings?: WikitextSettings;
|
|
73
82
|
/** Page context for resolving file paths, links, etc. */
|
|
74
83
|
page?: PageContext;
|
|
75
84
|
/** Pre-collected footnote elements from SyntaxTree.footnotes */
|
|
@@ -100,4 +109,6 @@ interface RenderOptions {
|
|
|
100
109
|
* Render a SyntaxTree to HTML string
|
|
101
110
|
*/
|
|
102
111
|
declare function renderToHtml(tree: SyntaxTree2, options?: RenderOptions): string;
|
|
103
|
-
|
|
112
|
+
import { WikitextMode, WikitextSettings as WikitextSettings3 } from "@wdprlib/ast";
|
|
113
|
+
import { createSettings, DEFAULT_SETTINGS } from "@wdprlib/ast";
|
|
114
|
+
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));
|
|
@@ -4210,7 +4242,7 @@ var SANITIZE_CONFIG = {
|
|
|
4210
4242
|
"width"
|
|
4211
4243
|
]
|
|
4212
4244
|
},
|
|
4213
|
-
allowedSchemes: ["https"]
|
|
4245
|
+
allowedSchemes: ["https", "http"]
|
|
4214
4246
|
};
|
|
4215
4247
|
function findIframes(html) {
|
|
4216
4248
|
const doc = parseDocument(html);
|
|
@@ -4258,7 +4290,7 @@ function matchesAllowlistEntry(url, entry) {
|
|
|
4258
4290
|
}
|
|
4259
4291
|
return true;
|
|
4260
4292
|
}
|
|
4261
|
-
function validateAndSanitizeEmbed(content, allowlist) {
|
|
4293
|
+
function validateAndSanitizeEmbed(content, allowlist, baseUrl) {
|
|
4262
4294
|
const sanitized = sanitizeHtml(content.trim(), SANITIZE_CONFIG);
|
|
4263
4295
|
if (!sanitized.trim()) {
|
|
4264
4296
|
return null;
|
|
@@ -4274,11 +4306,16 @@ function validateAndSanitizeEmbed(content, allowlist) {
|
|
|
4274
4306
|
}
|
|
4275
4307
|
let url;
|
|
4276
4308
|
try {
|
|
4277
|
-
|
|
4309
|
+
if (src.startsWith("//")) {
|
|
4310
|
+
const base = baseUrl ?? "https://localhost";
|
|
4311
|
+
url = new URL(src, base);
|
|
4312
|
+
} else {
|
|
4313
|
+
url = new URL(src);
|
|
4314
|
+
}
|
|
4278
4315
|
} catch {
|
|
4279
4316
|
return null;
|
|
4280
4317
|
}
|
|
4281
|
-
if (url.protocol !== "https:") {
|
|
4318
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
4282
4319
|
return null;
|
|
4283
4320
|
}
|
|
4284
4321
|
if (allowlist !== null) {
|
|
@@ -4301,7 +4338,7 @@ function normalizeBooleanAttributes(html) {
|
|
|
4301
4338
|
}
|
|
4302
4339
|
function renderEmbedBlock(ctx, data) {
|
|
4303
4340
|
const allowlist = ctx.options.embedAllowlist !== undefined ? ctx.options.embedAllowlist : DEFAULT_EMBED_ALLOWLIST;
|
|
4304
|
-
const sanitized = validateAndSanitizeEmbed(data.contents, allowlist);
|
|
4341
|
+
const sanitized = validateAndSanitizeEmbed(data.contents, allowlist, ctx.options.baseUrl);
|
|
4305
4342
|
if (sanitized === null) {
|
|
4306
4343
|
ctx.push('<div class="error-block">Sorry, no match for the embedded content.</div>');
|
|
4307
4344
|
return;
|
|
@@ -4362,8 +4399,9 @@ function renderBibliographyCite(ctx, data) {
|
|
|
4362
4399
|
return;
|
|
4363
4400
|
}
|
|
4364
4401
|
const idSuffix = generateIdSuffix(data.label, counter);
|
|
4365
|
-
const id = `bibcite-${number}
|
|
4366
|
-
const
|
|
4402
|
+
const id = ctx.generateId(`bibcite-${number}-`, idSuffix);
|
|
4403
|
+
const bibitemId = ctx.generateId("bibitem-", number);
|
|
4404
|
+
const onclick = `WIKIDOT.page.utils.scrollToReference('${bibitemId}')`;
|
|
4367
4405
|
ctx.push(`<a href="javascript:;" class="bibcite" id="${id}" onclick="${escapeAttr(onclick)}">`);
|
|
4368
4406
|
ctx.push(String(number));
|
|
4369
4407
|
ctx.push("</a>");
|
|
@@ -4376,7 +4414,8 @@ function renderBibliographyBlock(ctx, data, renderElements2) {
|
|
|
4376
4414
|
ctx.push(`<div class="title">${escapeHtml(title)}</div>`);
|
|
4377
4415
|
let index = 1;
|
|
4378
4416
|
for (const entry of data.entries) {
|
|
4379
|
-
ctx.
|
|
4417
|
+
const itemId = ctx.generateId("bibitem-", index);
|
|
4418
|
+
ctx.push(`<div class="bibitem" id="${itemId}">`);
|
|
4380
4419
|
ctx.push(`${index}. `);
|
|
4381
4420
|
renderElements2(ctx, entry.value);
|
|
4382
4421
|
ctx.push("</div>");
|
|
@@ -4409,12 +4448,19 @@ function renderTocList(ctx, listData, depth) {
|
|
|
4409
4448
|
renderTocItem(ctx, item, depth);
|
|
4410
4449
|
}
|
|
4411
4450
|
}
|
|
4451
|
+
function rewriteTocAnchor(ctx, href) {
|
|
4452
|
+
const match = /^#toc(\d+)$/.exec(href);
|
|
4453
|
+
if (!match)
|
|
4454
|
+
return href;
|
|
4455
|
+
return `#${ctx.generateId("toc", Number(match[1]))}`;
|
|
4456
|
+
}
|
|
4412
4457
|
function renderTocItem(ctx, item, depth) {
|
|
4413
4458
|
if (item["item-type"] === "elements") {
|
|
4414
4459
|
for (const el of item.elements) {
|
|
4415
4460
|
const link = extractLinkText(el);
|
|
4416
4461
|
if (link) {
|
|
4417
|
-
|
|
4462
|
+
const href = rewriteTocAnchor(ctx, link.href);
|
|
4463
|
+
ctx.push(`<div style="margin-left: ${depth}em;"><a href="${escapeAttr(href)}">${escapeHtml(link.text)}</a></div>`);
|
|
4418
4464
|
}
|
|
4419
4465
|
}
|
|
4420
4466
|
} else if (item["item-type"] === "sub-list") {
|
|
@@ -4979,7 +5025,7 @@ function formatNumber(n) {
|
|
|
4979
5025
|
function renderToHtml(tree, options = {}) {
|
|
4980
5026
|
const ctx = new RenderContext(tree, options);
|
|
4981
5027
|
renderElements(ctx, tree.elements);
|
|
4982
|
-
if (tree.styles?.length) {
|
|
5028
|
+
if (ctx.settings.allowStyleElements && tree.styles?.length) {
|
|
4983
5029
|
for (const style of tree.styles) {
|
|
4984
5030
|
ctx.push(`<style>${escapeStyleContent(style)}</style>`);
|
|
4985
5031
|
}
|
|
@@ -5123,7 +5169,12 @@ function renderElement(ctx, element) {
|
|
|
5123
5169
|
break;
|
|
5124
5170
|
}
|
|
5125
5171
|
}
|
|
5172
|
+
|
|
5173
|
+
// packages/render/src/index.ts
|
|
5174
|
+
import { createSettings, DEFAULT_SETTINGS as DEFAULT_SETTINGS2 } from "@wdprlib/ast";
|
|
5126
5175
|
export {
|
|
5127
5176
|
renderToHtml,
|
|
5177
|
+
createSettings,
|
|
5178
|
+
DEFAULT_SETTINGS2 as DEFAULT_SETTINGS,
|
|
5128
5179
|
DEFAULT_EMBED_ALLOWLIST
|
|
5129
5180
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wdprlib/render",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
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.
|
|
42
|
+
"@wdprlib/ast": "1.1.0",
|
|
43
43
|
"domhandler": "^5.0.3",
|
|
44
44
|
"htmlparser2": "^10.0.0",
|
|
45
45
|
"sanitize-html": "^2.14.0",
|