@wdprlib/render 0.1.4 → 1.0.0-rc.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/README.md +58 -0
- package/dist/index.cjs +496 -39
- package/dist/index.d.cts +31 -1
- package/dist/index.d.ts +31 -1
- package/dist/index.js +483 -39
- package/package.json +9 -2
package/dist/index.cjs
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
var import_node_module = require("node:module");
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
2
4
|
var __defProp = Object.defineProperty;
|
|
3
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
6
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __toESM = (mod, isNodeMode, target) => {
|
|
9
|
+
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
10
|
+
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
11
|
+
for (let key of __getOwnPropNames(mod))
|
|
12
|
+
if (!__hasOwnProp.call(to, key))
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: () => mod[key],
|
|
15
|
+
enumerable: true
|
|
16
|
+
});
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
6
19
|
var __moduleCache = /* @__PURE__ */ new WeakMap;
|
|
7
20
|
var __toCommonJS = (from) => {
|
|
8
21
|
var entry = __moduleCache.get(from), desc;
|
|
@@ -30,7 +43,8 @@ var __export = (target, all) => {
|
|
|
30
43
|
// packages/render/src/index.ts
|
|
31
44
|
var exports_src = {};
|
|
32
45
|
__export(exports_src, {
|
|
33
|
-
renderToHtml: () => renderToHtml
|
|
46
|
+
renderToHtml: () => renderToHtml,
|
|
47
|
+
DEFAULT_EMBED_ALLOWLIST: () => DEFAULT_EMBED_ALLOWLIST
|
|
34
48
|
});
|
|
35
49
|
module.exports = __toCommonJS(exports_src);
|
|
36
50
|
|
|
@@ -403,17 +417,43 @@ class RenderContext {
|
|
|
403
417
|
_footnoteIndex = 0;
|
|
404
418
|
_equationIndex = 0;
|
|
405
419
|
_htmlBlockIndex = 0;
|
|
420
|
+
_bibciteCounter = 0;
|
|
406
421
|
options;
|
|
407
422
|
footnotes;
|
|
408
423
|
styles;
|
|
409
424
|
htmlBlocks;
|
|
410
425
|
tocElements;
|
|
426
|
+
bibliographyMap;
|
|
427
|
+
bibliographyEntries;
|
|
411
428
|
constructor(tree, options = {}) {
|
|
412
429
|
this.options = options;
|
|
413
430
|
this.footnotes = options.footnotes ?? tree.footnotes ?? [];
|
|
414
431
|
this.styles = tree.styles ?? [];
|
|
415
432
|
this.htmlBlocks = tree["html-blocks"] ?? [];
|
|
416
433
|
this.tocElements = tree["table-of-contents"] ?? [];
|
|
434
|
+
this.bibliographyMap = new Map;
|
|
435
|
+
this.bibliographyEntries = [];
|
|
436
|
+
this.buildBibliographyMap(tree.elements);
|
|
437
|
+
}
|
|
438
|
+
buildBibliographyMap(elements) {
|
|
439
|
+
for (const el of elements) {
|
|
440
|
+
if (el.element === "bibliography-block") {
|
|
441
|
+
const data = el.data;
|
|
442
|
+
for (const entry of data.entries) {
|
|
443
|
+
if (!this.bibliographyMap.has(entry.key_string)) {
|
|
444
|
+
const index = this.bibliographyMap.size + 1;
|
|
445
|
+
this.bibliographyMap.set(entry.key_string, index);
|
|
446
|
+
this.bibliographyEntries.push(entry);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if ("data" in el && el.data && typeof el.data === "object") {
|
|
451
|
+
const data = el.data;
|
|
452
|
+
if ("elements" in data && Array.isArray(data.elements)) {
|
|
453
|
+
this.buildBibliographyMap(data.elements);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
417
457
|
}
|
|
418
458
|
push(html) {
|
|
419
459
|
this.chunks.push(html);
|
|
@@ -436,15 +476,24 @@ class RenderContext {
|
|
|
436
476
|
nextHtmlBlockIndex() {
|
|
437
477
|
return this._htmlBlockIndex++;
|
|
438
478
|
}
|
|
479
|
+
nextBibciteCounter() {
|
|
480
|
+
return ++this._bibciteCounter;
|
|
481
|
+
}
|
|
439
482
|
get page() {
|
|
440
483
|
return this.options.page;
|
|
441
484
|
}
|
|
442
485
|
resolveImageSource(source) {
|
|
486
|
+
const pageName = this.page?.pageName;
|
|
443
487
|
switch (source.type) {
|
|
444
|
-
case "url":
|
|
445
|
-
|
|
488
|
+
case "url": {
|
|
489
|
+
const url = source.data;
|
|
490
|
+
if (url.startsWith("/") && !url.startsWith("//")) {
|
|
491
|
+
return `/local--files${url}`;
|
|
492
|
+
}
|
|
493
|
+
return url;
|
|
494
|
+
}
|
|
446
495
|
case "file1":
|
|
447
|
-
return `/local--files/${source.data.file}`;
|
|
496
|
+
return pageName ? `/local--files/${pageName}/${source.data.file}` : `/local--files/${source.data.file}`;
|
|
448
497
|
case "file2":
|
|
449
498
|
return `/local--files/${source.data.page}/${source.data.file}`;
|
|
450
499
|
case "file3":
|
|
@@ -455,10 +504,34 @@ class RenderContext {
|
|
|
455
504
|
if (typeof location === "string") {
|
|
456
505
|
return location;
|
|
457
506
|
}
|
|
507
|
+
const page = location.page;
|
|
508
|
+
if (page.startsWith("//")) {
|
|
509
|
+
return page.toLowerCase();
|
|
510
|
+
}
|
|
511
|
+
const hashIdx = page.indexOf("#");
|
|
512
|
+
if (hashIdx !== -1) {
|
|
513
|
+
let pagePart = page.slice(0, hashIdx);
|
|
514
|
+
const anchor = page.slice(hashIdx);
|
|
515
|
+
if (pagePart.endsWith("/")) {
|
|
516
|
+
pagePart = pagePart.slice(0, -1);
|
|
517
|
+
}
|
|
518
|
+
return `/${pagePart.toLowerCase()}${anchor.toLowerCase()}`;
|
|
519
|
+
}
|
|
520
|
+
const normalizedPage = this.normalizePageName(page);
|
|
521
|
+
const safePage = normalizedPage.startsWith("/") ? normalizedPage.slice(1) : normalizedPage;
|
|
458
522
|
if (location.site) {
|
|
459
|
-
return `https://${location.site}.wikidot.com/${
|
|
523
|
+
return `https://${location.site}.wikidot.com/${safePage}`;
|
|
524
|
+
}
|
|
525
|
+
return `/${safePage}`;
|
|
526
|
+
}
|
|
527
|
+
normalizePageName(page) {
|
|
528
|
+
let normalized = page.toLowerCase();
|
|
529
|
+
normalized = normalized.replace(/:\s+/g, ":");
|
|
530
|
+
normalized = normalized.replace(/\s+/g, "-").trim();
|
|
531
|
+
if (!normalized.startsWith("/")) {
|
|
532
|
+
normalized = normalized.replace(/\//g, "-");
|
|
460
533
|
}
|
|
461
|
-
return
|
|
534
|
+
return normalized;
|
|
462
535
|
}
|
|
463
536
|
renderAttributes(attributes) {
|
|
464
537
|
const safe = sanitizeAttributes(attributes);
|
|
@@ -553,6 +626,9 @@ function renderStringContainer(ctx, type, attributes, elements) {
|
|
|
553
626
|
ctx.push("</span>");
|
|
554
627
|
break;
|
|
555
628
|
case "div":
|
|
629
|
+
if (elements.length === 0 && Object.keys(attributes).length === 0) {
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
556
632
|
ctx.push(`<div${renderAttrs(attributes)}>`);
|
|
557
633
|
renderElements(ctx, elements);
|
|
558
634
|
ctx.push("</div>");
|
|
@@ -672,7 +748,7 @@ function renderRaw(ctx, data) {
|
|
|
672
748
|
if (data === "")
|
|
673
749
|
return;
|
|
674
750
|
ctx.push(`<span style="white-space: pre-wrap;">`);
|
|
675
|
-
ctx.push(escapeHtml(data));
|
|
751
|
+
ctx.push(escapeHtml(data).replace(/ /g, " "));
|
|
676
752
|
ctx.push("</span>");
|
|
677
753
|
}
|
|
678
754
|
function renderEmail(ctx, email) {
|
|
@@ -694,6 +770,19 @@ function renderLink(ctx, data) {
|
|
|
694
770
|
href = "#invalid-url";
|
|
695
771
|
}
|
|
696
772
|
const attrs = [`href="${escapeAttr(href)}"`];
|
|
773
|
+
if (data.type === "page" && typeof data.link === "object") {
|
|
774
|
+
const page = data.link.page;
|
|
775
|
+
const isSpecialPage = page.startsWith("//") || page.includes(":") || page.includes("#/");
|
|
776
|
+
if (!isSpecialPage) {
|
|
777
|
+
const hashIdx = page.indexOf("#");
|
|
778
|
+
const pageToCheck = hashIdx !== -1 ? page.slice(0, hashIdx) : page;
|
|
779
|
+
const pageExists = ctx.page?.pageExists;
|
|
780
|
+
const exists = pageExists ? pageExists(pageToCheck) : false;
|
|
781
|
+
if (!exists) {
|
|
782
|
+
attrs.push(`class="newpage"`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
697
786
|
if (data.target) {
|
|
698
787
|
const targetMap = {
|
|
699
788
|
"new-tab": "_blank",
|
|
@@ -787,7 +876,16 @@ function renderImage(ctx, data) {
|
|
|
787
876
|
const imgTag = `<img ${imgAttrs.join(" ")} />`;
|
|
788
877
|
let output = imgTag;
|
|
789
878
|
if (data.link) {
|
|
790
|
-
let href
|
|
879
|
+
let href;
|
|
880
|
+
if (typeof data.link === "string") {
|
|
881
|
+
if (!data.link.startsWith("/") && !data.link.startsWith("#") && !data.link.startsWith("http://") && !data.link.startsWith("https://")) {
|
|
882
|
+
href = `/${data.link}`;
|
|
883
|
+
} else {
|
|
884
|
+
href = data.link;
|
|
885
|
+
}
|
|
886
|
+
} else {
|
|
887
|
+
href = `/${data.link.page}`;
|
|
888
|
+
}
|
|
791
889
|
if (isDangerousUrl(href)) {
|
|
792
890
|
href = "#invalid-url";
|
|
793
891
|
}
|
|
@@ -804,7 +902,16 @@ function renderImage(ctx, data) {
|
|
|
804
902
|
}
|
|
805
903
|
function getAlignmentClass(align, isFloat) {
|
|
806
904
|
if (isFloat) {
|
|
807
|
-
|
|
905
|
+
switch (align) {
|
|
906
|
+
case "left":
|
|
907
|
+
return "floatleft";
|
|
908
|
+
case "right":
|
|
909
|
+
return "floatright";
|
|
910
|
+
case "center":
|
|
911
|
+
return "floatcenter";
|
|
912
|
+
default:
|
|
913
|
+
return `float${align}`;
|
|
914
|
+
}
|
|
808
915
|
}
|
|
809
916
|
switch (align) {
|
|
810
917
|
case "left":
|
|
@@ -833,7 +940,87 @@ function getFilenameFromSource(source) {
|
|
|
833
940
|
}
|
|
834
941
|
|
|
835
942
|
// packages/render/src/elements/list.ts
|
|
943
|
+
function trimTextElements(elements) {
|
|
944
|
+
if (elements.length === 0)
|
|
945
|
+
return elements;
|
|
946
|
+
let start = 0;
|
|
947
|
+
let end = elements.length;
|
|
948
|
+
while (start < end) {
|
|
949
|
+
const el = elements[start];
|
|
950
|
+
if (el.element === "text" && typeof el.data === "string" && el.data.trim() === "") {
|
|
951
|
+
start++;
|
|
952
|
+
} else {
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
while (end > start) {
|
|
957
|
+
const el = elements[end - 1];
|
|
958
|
+
if (el.element === "text" && typeof el.data === "string" && el.data.trim() === "") {
|
|
959
|
+
end--;
|
|
960
|
+
} else {
|
|
961
|
+
break;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
return elements.slice(start, end);
|
|
965
|
+
}
|
|
966
|
+
function isLiCloseTextParagraph(el) {
|
|
967
|
+
if (el.element !== "container")
|
|
968
|
+
return false;
|
|
969
|
+
const data = el.data;
|
|
970
|
+
if (data.type !== "paragraph")
|
|
971
|
+
return false;
|
|
972
|
+
const texts = data.elements.filter((e) => e.element === "text").map((e) => e.data);
|
|
973
|
+
const combined = texts.join("").trim();
|
|
974
|
+
return combined === "[[/li]]";
|
|
975
|
+
}
|
|
976
|
+
function renderNoMarkerElements(ctx, elements) {
|
|
977
|
+
const trimmed = trimTextElements(elements);
|
|
978
|
+
if (trimmed.length === 0)
|
|
979
|
+
return;
|
|
980
|
+
const paragraphIndices = [];
|
|
981
|
+
for (let i = 0;i < trimmed.length; i++) {
|
|
982
|
+
const el = trimmed[i];
|
|
983
|
+
if (el.element === "container" && el.data.type === "paragraph") {
|
|
984
|
+
paragraphIndices.push(i);
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
if (paragraphIndices.length === 0) {
|
|
988
|
+
renderElements(ctx, trimmed);
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
const firstParagraphIdx = paragraphIndices[0];
|
|
992
|
+
const lastParagraphIdx = paragraphIndices[paragraphIndices.length - 1];
|
|
993
|
+
for (let i = 0;i < trimmed.length; i++) {
|
|
994
|
+
const el = trimmed[i];
|
|
995
|
+
if (el.element === "container" && el.data.type === "paragraph") {
|
|
996
|
+
const data = el.data;
|
|
997
|
+
if (i === firstParagraphIdx) {
|
|
998
|
+
renderElements(ctx, data.elements);
|
|
999
|
+
} else if (i === lastParagraphIdx && isLiCloseTextParagraph(el)) {
|
|
1000
|
+
renderElements(ctx, data.elements);
|
|
1001
|
+
} else {
|
|
1002
|
+
ctx.push("<p>");
|
|
1003
|
+
renderElements(ctx, data.elements);
|
|
1004
|
+
ctx.push("</p>");
|
|
1005
|
+
}
|
|
1006
|
+
} else {
|
|
1007
|
+
renderElement(ctx, el);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
836
1011
|
function renderList(ctx, data) {
|
|
1012
|
+
const hasContent = data.items.some((item) => {
|
|
1013
|
+
if (item["item-type"] === "sub-list")
|
|
1014
|
+
return true;
|
|
1015
|
+
if (item["item-type"] === "elements") {
|
|
1016
|
+
const trimmed = trimTextElements(item.elements);
|
|
1017
|
+
return trimmed.length > 0;
|
|
1018
|
+
}
|
|
1019
|
+
return false;
|
|
1020
|
+
});
|
|
1021
|
+
if (!hasContent) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
837
1024
|
const tag = data.type === "numbered" ? "ol" : "ul";
|
|
838
1025
|
ctx.push(`<${tag}${renderListAttrs(data.attributes)}>`);
|
|
839
1026
|
const items = data.items;
|
|
@@ -841,8 +1028,14 @@ function renderList(ctx, data) {
|
|
|
841
1028
|
while (i < items.length) {
|
|
842
1029
|
const item = items[i];
|
|
843
1030
|
if (item["item-type"] === "elements") {
|
|
844
|
-
|
|
845
|
-
|
|
1031
|
+
const hasNoMarker = item.attributes._noMarker === "true";
|
|
1032
|
+
const styleAttr = hasNoMarker ? ' style="list-style: none"' : "";
|
|
1033
|
+
ctx.push(`<li${renderListAttrs(item.attributes)}${styleAttr}>`);
|
|
1034
|
+
if (hasNoMarker) {
|
|
1035
|
+
renderNoMarkerElements(ctx, item.elements);
|
|
1036
|
+
} else {
|
|
1037
|
+
renderElements(ctx, trimTextElements(item.elements));
|
|
1038
|
+
}
|
|
846
1039
|
while (i + 1 < items.length && items[i + 1]["item-type"] === "sub-list") {
|
|
847
1040
|
i++;
|
|
848
1041
|
const subItem = items[i];
|
|
@@ -3760,7 +3953,7 @@ function renderTabView(ctx, tabs) {
|
|
|
3760
3953
|
ctx.push(`<div class="yui-content">`);
|
|
3761
3954
|
for (let i = 0;i < tabs.length; i++) {
|
|
3762
3955
|
const tab = tabs[i];
|
|
3763
|
-
const displayStyle = i === 0 ? "" : ` style="display:
|
|
3956
|
+
const displayStyle = i === 0 ? "" : ` style="display:none"`;
|
|
3764
3957
|
ctx.push(`<div id="wiki-tab-0-${i}"${displayStyle}>`);
|
|
3765
3958
|
renderElements(ctx, tab.elements);
|
|
3766
3959
|
ctx.push("</div>");
|
|
@@ -3779,8 +3972,6 @@ function renderFootnoteRef(ctx, index) {
|
|
|
3779
3972
|
ctx.push("</sup>");
|
|
3780
3973
|
}
|
|
3781
3974
|
function renderFootnoteBlock(ctx, data) {
|
|
3782
|
-
if (data.hide)
|
|
3783
|
-
return;
|
|
3784
3975
|
if (ctx.footnotes.length === 0)
|
|
3785
3976
|
return;
|
|
3786
3977
|
const title = data.title ?? "Footnotes";
|
|
@@ -3798,26 +3989,81 @@ function renderFootnoteBlock(ctx, data) {
|
|
|
3798
3989
|
}
|
|
3799
3990
|
|
|
3800
3991
|
// packages/render/src/elements/math.ts
|
|
3992
|
+
var import_temml = __toESM(require("temml"));
|
|
3993
|
+
function needsAlignedWrapper(latex) {
|
|
3994
|
+
if (/\\begin\s*\{/.test(latex)) {
|
|
3995
|
+
return false;
|
|
3996
|
+
}
|
|
3997
|
+
const withoutEscaped = latex.replace(/\\&/g, "");
|
|
3998
|
+
return withoutEscaped.includes("&");
|
|
3999
|
+
}
|
|
4000
|
+
function renderLatexToMathML(latex, displayMode) {
|
|
4001
|
+
try {
|
|
4002
|
+
let processedLatex = latex;
|
|
4003
|
+
if (displayMode && needsAlignedWrapper(latex)) {
|
|
4004
|
+
processedLatex = `\\begin{aligned}
|
|
4005
|
+
${latex}
|
|
4006
|
+
\\end{aligned}`;
|
|
4007
|
+
}
|
|
4008
|
+
return import_temml.default.renderToString(processedLatex, {
|
|
4009
|
+
displayMode,
|
|
4010
|
+
throwOnError: false,
|
|
4011
|
+
annotate: false
|
|
4012
|
+
});
|
|
4013
|
+
} catch {
|
|
4014
|
+
return "";
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
3801
4017
|
function renderMath(ctx, data) {
|
|
3802
4018
|
const index = ctx.nextEquationIndex() + 1;
|
|
4019
|
+
const latex = data["latex-source"];
|
|
4020
|
+
const mathml = renderLatexToMathML(latex, true);
|
|
4021
|
+
const id = data.name ? `equation-${data.name}` : `equation-${index}`;
|
|
4022
|
+
const dataName = data.name ? ` data-name="${escapeAttr(data.name)}"` : "";
|
|
4023
|
+
ctx.push(`<div class="math-block" id="${escapeAttr(id)}"${dataName}>`);
|
|
3803
4024
|
if (data.name) {
|
|
3804
4025
|
ctx.push(`<span class="equation-number">(${index})</span>`);
|
|
3805
4026
|
}
|
|
3806
|
-
|
|
3807
|
-
ctx.push(
|
|
3808
|
-
ctx.push(
|
|
4027
|
+
ctx.push(`<code class="math-source" hidden aria-hidden="true">`);
|
|
4028
|
+
ctx.push(escapeHtml(latex));
|
|
4029
|
+
ctx.push(`</code>`);
|
|
4030
|
+
ctx.push(`<span class="math-render">`);
|
|
4031
|
+
if (mathml) {
|
|
4032
|
+
ctx.push(mathml);
|
|
4033
|
+
} else {
|
|
4034
|
+
ctx.push(`<span class="math-error">`);
|
|
4035
|
+
ctx.push(escapeHtml(latex));
|
|
4036
|
+
ctx.push(`</span>`);
|
|
4037
|
+
}
|
|
4038
|
+
ctx.push(`</span>`);
|
|
3809
4039
|
ctx.push("</div>");
|
|
3810
4040
|
}
|
|
3811
4041
|
function renderMathInline(ctx, data) {
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
ctx.push(
|
|
4042
|
+
const latex = data["latex-source"];
|
|
4043
|
+
const mathml = renderLatexToMathML(latex, false);
|
|
4044
|
+
ctx.push(`<span class="math-inline">`);
|
|
4045
|
+
ctx.push(`<code class="math-source" hidden aria-hidden="true">`);
|
|
4046
|
+
ctx.push(escapeHtml(latex));
|
|
4047
|
+
ctx.push(`</code>`);
|
|
4048
|
+
ctx.push(`<span class="math-render">`);
|
|
4049
|
+
if (mathml) {
|
|
4050
|
+
ctx.push(mathml);
|
|
4051
|
+
} else {
|
|
4052
|
+
ctx.push(`<span class="math-error">$`);
|
|
4053
|
+
ctx.push(escapeHtml(latex));
|
|
4054
|
+
ctx.push(`$</span>`);
|
|
4055
|
+
}
|
|
4056
|
+
ctx.push(`</span>`);
|
|
4057
|
+
ctx.push("</span>");
|
|
3815
4058
|
}
|
|
3816
4059
|
function renderEquationRef(ctx, name) {
|
|
3817
4060
|
const id = `equation-${name}`;
|
|
3818
|
-
ctx.push(`<
|
|
4061
|
+
ctx.push(`<span class="eref" data-target="${escapeAttr(id)}">`);
|
|
4062
|
+
ctx.push(`<a class="eref-link" href="#${escapeAttr(id)}">`);
|
|
3819
4063
|
ctx.push(escapeHtml(name));
|
|
3820
|
-
ctx.push(
|
|
4064
|
+
ctx.push(`</a>`);
|
|
4065
|
+
ctx.push(`<span class="eref-tooltip" aria-hidden="true"></span>`);
|
|
4066
|
+
ctx.push("</span>");
|
|
3821
4067
|
}
|
|
3822
4068
|
|
|
3823
4069
|
// packages/render/src/elements/module/backlinks.ts
|
|
@@ -3874,7 +4120,7 @@ function renderListPages(ctx, _data) {
|
|
|
3874
4120
|
function renderModule(ctx, data) {
|
|
3875
4121
|
switch (data.module) {
|
|
3876
4122
|
case "unknown":
|
|
3877
|
-
ctx.push(`<div class="error-block">[[module <em>${data.name}</em>]] No such module, please <a href="
|
|
4123
|
+
ctx.push(`<div class="error-block">[[module <em>${data.name}</em>]] No such module, please <a href="https://www.wikidot.com/doc:modules" target="_blank" rel="noopener noreferrer">check available modules</a> and fix this page.</div>`);
|
|
3878
4124
|
break;
|
|
3879
4125
|
case "backlinks":
|
|
3880
4126
|
renderBacklinks(ctx, data);
|
|
@@ -3962,8 +4208,148 @@ function renderGitlabSnippet(ctx, snippetId) {
|
|
|
3962
4208
|
ctx.push(`<script src="https://gitlab.com/snippets/${escapeAttr(snippetId)}.js"></script>`);
|
|
3963
4209
|
}
|
|
3964
4210
|
|
|
4211
|
+
// packages/render/src/elements/embed-block.ts
|
|
4212
|
+
var import_dompurify = __toESM(require("dompurify"));
|
|
4213
|
+
var import_jsdom = require("jsdom");
|
|
4214
|
+
var BOOLEAN_ATTRIBUTES = [
|
|
4215
|
+
"allowfullscreen",
|
|
4216
|
+
"async",
|
|
4217
|
+
"autofocus",
|
|
4218
|
+
"autoplay",
|
|
4219
|
+
"checked",
|
|
4220
|
+
"controls",
|
|
4221
|
+
"default",
|
|
4222
|
+
"defer",
|
|
4223
|
+
"disabled",
|
|
4224
|
+
"formnovalidate",
|
|
4225
|
+
"hidden",
|
|
4226
|
+
"ismap",
|
|
4227
|
+
"loop",
|
|
4228
|
+
"multiple",
|
|
4229
|
+
"muted",
|
|
4230
|
+
"novalidate",
|
|
4231
|
+
"open",
|
|
4232
|
+
"readonly",
|
|
4233
|
+
"required",
|
|
4234
|
+
"reversed",
|
|
4235
|
+
"selected"
|
|
4236
|
+
];
|
|
4237
|
+
var DEFAULT_EMBED_ALLOWLIST = [
|
|
4238
|
+
{ host: "*.youtube.com", pathPrefix: "/embed/" },
|
|
4239
|
+
{ host: "*.youtube-nocookie.com", pathPrefix: "/embed/" },
|
|
4240
|
+
{ host: "player.vimeo.com", pathPrefix: "/video/" },
|
|
4241
|
+
{ host: "*.google.com", pathPrefix: "/maps/embed" },
|
|
4242
|
+
{ host: "calendar.google.com", pathPrefix: "/calendar/embed" },
|
|
4243
|
+
{ host: "open.spotify.com", pathPrefix: "/embed/" },
|
|
4244
|
+
{ host: "w.soundcloud.com", pathPrefix: "/player/" },
|
|
4245
|
+
{ host: "codepen.io" }
|
|
4246
|
+
];
|
|
4247
|
+
var window = new import_jsdom.JSDOM("").window;
|
|
4248
|
+
var purify = import_dompurify.default(window);
|
|
4249
|
+
purify.addHook("uponSanitizeAttribute", (_node, data) => {
|
|
4250
|
+
if (data.attrName === "src" && data.attrValue) {
|
|
4251
|
+
if (!data.attrValue.toLowerCase().startsWith("https://")) {
|
|
4252
|
+
data.attrValue = "";
|
|
4253
|
+
data.forceKeepAttr = false;
|
|
4254
|
+
}
|
|
4255
|
+
}
|
|
4256
|
+
});
|
|
4257
|
+
var DOMPURIFY_CONFIG = {
|
|
4258
|
+
ALLOWED_TAGS: ["iframe"],
|
|
4259
|
+
ADD_ATTR: ["allow", "allowfullscreen", "frameborder", "loading", "referrerpolicy", "sandbox"],
|
|
4260
|
+
FORBID_ATTR: ["srcdoc", "onload", "onerror", "onclick"]
|
|
4261
|
+
};
|
|
4262
|
+
function matchesHostPattern(hostname, pattern) {
|
|
4263
|
+
const lowerHostname = hostname.toLowerCase();
|
|
4264
|
+
const lowerPattern = pattern.toLowerCase();
|
|
4265
|
+
if (lowerPattern.startsWith("*.")) {
|
|
4266
|
+
const base = lowerPattern.slice(2);
|
|
4267
|
+
return lowerHostname === base || lowerHostname.endsWith("." + base);
|
|
4268
|
+
}
|
|
4269
|
+
return lowerHostname === lowerPattern;
|
|
4270
|
+
}
|
|
4271
|
+
function matchesAllowlistEntry(url, entry) {
|
|
4272
|
+
if (!matchesHostPattern(url.hostname, entry.host)) {
|
|
4273
|
+
return false;
|
|
4274
|
+
}
|
|
4275
|
+
if (entry.pathPrefix) {
|
|
4276
|
+
const pathLower = url.pathname.toLowerCase();
|
|
4277
|
+
const prefixLower = entry.pathPrefix.toLowerCase();
|
|
4278
|
+
if (!pathLower.startsWith(prefixLower)) {
|
|
4279
|
+
return false;
|
|
4280
|
+
}
|
|
4281
|
+
if (!prefixLower.endsWith("/")) {
|
|
4282
|
+
const remainder = pathLower.slice(prefixLower.length);
|
|
4283
|
+
if (remainder && !/^[/?#]/.test(remainder)) {
|
|
4284
|
+
return false;
|
|
4285
|
+
}
|
|
4286
|
+
}
|
|
4287
|
+
}
|
|
4288
|
+
return true;
|
|
4289
|
+
}
|
|
4290
|
+
function validateAndSanitizeEmbed(content, allowlist) {
|
|
4291
|
+
const sanitized = purify.sanitize(content.trim(), {
|
|
4292
|
+
...DOMPURIFY_CONFIG,
|
|
4293
|
+
RETURN_TRUSTED_TYPE: false
|
|
4294
|
+
});
|
|
4295
|
+
if (!sanitized.trim()) {
|
|
4296
|
+
return null;
|
|
4297
|
+
}
|
|
4298
|
+
const dom = new import_jsdom.JSDOM(sanitized);
|
|
4299
|
+
const iframes = dom.window.document.querySelectorAll("iframe");
|
|
4300
|
+
if (iframes.length !== 1) {
|
|
4301
|
+
return null;
|
|
4302
|
+
}
|
|
4303
|
+
const iframe = iframes[0];
|
|
4304
|
+
const src = iframe.getAttribute("src")?.trim();
|
|
4305
|
+
if (!src) {
|
|
4306
|
+
return null;
|
|
4307
|
+
}
|
|
4308
|
+
let url;
|
|
4309
|
+
try {
|
|
4310
|
+
url = new URL(src);
|
|
4311
|
+
} catch {
|
|
4312
|
+
return null;
|
|
4313
|
+
}
|
|
4314
|
+
if (url.protocol !== "https:") {
|
|
4315
|
+
return null;
|
|
4316
|
+
}
|
|
4317
|
+
if (allowlist !== null) {
|
|
4318
|
+
const matched = allowlist.some((entry) => matchesAllowlistEntry(url, entry));
|
|
4319
|
+
if (!matched) {
|
|
4320
|
+
return null;
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
return sanitized;
|
|
4324
|
+
}
|
|
4325
|
+
function normalizeBooleanAttributes(html) {
|
|
4326
|
+
let result = html;
|
|
4327
|
+
for (const attr of BOOLEAN_ATTRIBUTES) {
|
|
4328
|
+
const standalonePattern = new RegExp(`\\s${attr}(?=\\s|>|/>)`, "gi");
|
|
4329
|
+
result = result.replace(standalonePattern, ` ${attr}="${attr}"`);
|
|
4330
|
+
const emptyValuePattern = new RegExp(`\\s${attr}=""`, "gi");
|
|
4331
|
+
result = result.replace(emptyValuePattern, ` ${attr}="${attr}"`);
|
|
4332
|
+
}
|
|
4333
|
+
return result;
|
|
4334
|
+
}
|
|
4335
|
+
function renderEmbedBlock(ctx, data) {
|
|
4336
|
+
const allowlist = ctx.options.embedAllowlist !== undefined ? ctx.options.embedAllowlist : DEFAULT_EMBED_ALLOWLIST;
|
|
4337
|
+
const sanitized = validateAndSanitizeEmbed(data.contents, allowlist);
|
|
4338
|
+
if (sanitized === null) {
|
|
4339
|
+
ctx.push('<div class="error-block">Sorry, no match for the embedded content.</div>');
|
|
4340
|
+
return;
|
|
4341
|
+
}
|
|
4342
|
+
const normalized = normalizeBooleanAttributes(sanitized);
|
|
4343
|
+
ctx.push(normalized);
|
|
4344
|
+
}
|
|
4345
|
+
|
|
3965
4346
|
// packages/render/src/elements/user.ts
|
|
3966
4347
|
function renderUser(ctx, data) {
|
|
4348
|
+
const normalized = data.name.toLowerCase().trim();
|
|
4349
|
+
if (normalized === "anonymous") {
|
|
4350
|
+
ctx.push("Anonymous");
|
|
4351
|
+
return;
|
|
4352
|
+
}
|
|
3967
4353
|
const resolved = ctx.options.resolvers?.user?.(data.name) ?? null;
|
|
3968
4354
|
if (resolved === null) {
|
|
3969
4355
|
ctx.push(escapeHtml(data.name));
|
|
@@ -3973,9 +4359,10 @@ function renderUser(ctx, data) {
|
|
|
3973
4359
|
const hrefAttr = resolved.url ? ` href="${escapeAttr(resolved.url)}"` : "";
|
|
3974
4360
|
const showAvatar = data["show-avatar"] && resolved.url && resolved.avatarUrl;
|
|
3975
4361
|
if (showAvatar) {
|
|
4362
|
+
const styleAttr = resolved.karmaUrl ? ` style="background-image:url(${escapeAttr(resolved.karmaUrl)})"` : "";
|
|
3976
4363
|
ctx.push(`<span class="printuser avatarhover">`);
|
|
3977
4364
|
ctx.push(`<a${hrefAttr}>`);
|
|
3978
|
-
ctx.push(`<img class="small" src="${escapeAttr(resolved.avatarUrl)}" alt="${escapeAttr(displayName)}" />`);
|
|
4365
|
+
ctx.push(`<img class="small" src="${escapeAttr(resolved.avatarUrl)}" alt="${escapeAttr(displayName)}"${styleAttr} />`);
|
|
3979
4366
|
ctx.push("</a>");
|
|
3980
4367
|
ctx.push(`<a${hrefAttr}>`);
|
|
3981
4368
|
ctx.push(escapeHtml(displayName));
|
|
@@ -3991,23 +4378,43 @@ function renderUser(ctx, data) {
|
|
|
3991
4378
|
}
|
|
3992
4379
|
|
|
3993
4380
|
// packages/render/src/elements/bibliography.ts
|
|
4381
|
+
function generateIdSuffix(label, counter) {
|
|
4382
|
+
let h = 2166136261;
|
|
4383
|
+
const input = label + counter;
|
|
4384
|
+
for (let i = 0;i < input.length; i++) {
|
|
4385
|
+
h ^= input.charCodeAt(i);
|
|
4386
|
+
h = Math.imul(h, 16777619);
|
|
4387
|
+
}
|
|
4388
|
+
return (h >>> 0).toString(16).slice(0, 6);
|
|
4389
|
+
}
|
|
3994
4390
|
function renderBibliographyCite(ctx, data) {
|
|
3995
|
-
|
|
3996
|
-
|
|
4391
|
+
const number = ctx.bibliographyMap.get(data.label);
|
|
4392
|
+
const counter = ctx.nextBibciteCounter();
|
|
4393
|
+
if (number === undefined) {
|
|
4394
|
+
ctx.push(escapeHtml(data.label));
|
|
4395
|
+
return;
|
|
3997
4396
|
}
|
|
3998
|
-
|
|
3999
|
-
|
|
4397
|
+
const idSuffix = generateIdSuffix(data.label, counter);
|
|
4398
|
+
const id = `bibcite-${number}-${idSuffix}`;
|
|
4399
|
+
const onclick = `WIKIDOT.page.utils.scrollToReference('bibitem-${number}')`;
|
|
4400
|
+
ctx.push(`<a href="javascript:;" class="bibcite" id="${id}" onclick="${escapeAttr(onclick)}">`);
|
|
4401
|
+
ctx.push(String(number));
|
|
4000
4402
|
ctx.push("</a>");
|
|
4001
|
-
if (data.brackets) {
|
|
4002
|
-
ctx.push("]");
|
|
4003
|
-
}
|
|
4004
4403
|
}
|
|
4005
|
-
function renderBibliographyBlock(ctx, data) {
|
|
4404
|
+
function renderBibliographyBlock(ctx, data, renderElements2) {
|
|
4006
4405
|
if (data.hide)
|
|
4007
4406
|
return;
|
|
4008
4407
|
const title = data.title ?? "Bibliography";
|
|
4009
4408
|
ctx.push(`<div class="bibitems">`);
|
|
4010
4409
|
ctx.push(`<div class="title">${escapeHtml(title)}</div>`);
|
|
4410
|
+
let index = 1;
|
|
4411
|
+
for (const entry of data.entries) {
|
|
4412
|
+
ctx.push(`<div class="bibitem" id="bibitem-${index}">`);
|
|
4413
|
+
ctx.push(`${index}. `);
|
|
4414
|
+
renderElements2(ctx, entry.value);
|
|
4415
|
+
ctx.push("</div>");
|
|
4416
|
+
index++;
|
|
4417
|
+
}
|
|
4011
4418
|
ctx.push("</div>");
|
|
4012
4419
|
}
|
|
4013
4420
|
|
|
@@ -4110,17 +4517,61 @@ function renderHtmlBlock(ctx, data) {
|
|
|
4110
4517
|
const src = callbackUrl || generateDefaultUrl(pageName, data.contents);
|
|
4111
4518
|
const sandbox = ctx.options.htmlBlockSandbox;
|
|
4112
4519
|
const sandboxAttr = sandbox ? ` sandbox="${escapeAttr(sandbox)}"` : "";
|
|
4113
|
-
|
|
4520
|
+
const styleAttr = data.style ? ` style="${escapeAttr(sanitizeStyleValue(data.style))}"` : "";
|
|
4521
|
+
ctx.push(`<p><iframe src="${escapeAttr(src)}"${sandboxAttr} allowtransparency="true" frameborder="0" class="html-block-iframe"${styleAttr}></iframe></p>`);
|
|
4114
4522
|
}
|
|
4115
4523
|
|
|
4116
4524
|
// packages/render/src/elements/include.ts
|
|
4117
4525
|
function renderInclude(ctx, data) {
|
|
4526
|
+
if (data.elements.length === 0) {
|
|
4527
|
+
const pageName = data.location.page.toLowerCase();
|
|
4528
|
+
const encodedPageName = pageName.replace(/[^a-z0-9\-_:/]/g, (c) => encodeURIComponent(c));
|
|
4529
|
+
const safePath = encodedPageName.startsWith("/") ? encodedPageName.slice(1) : encodedPageName;
|
|
4530
|
+
ctx.push(`<div class="error-block"><p>Included page "${escapeHtml(pageName)}" does not exist (<a href="/${escapeAttr(safePath)}/edit/true">create it now</a>)</p></div>`);
|
|
4531
|
+
return;
|
|
4532
|
+
}
|
|
4118
4533
|
renderElements(ctx, data.elements);
|
|
4119
4534
|
}
|
|
4120
4535
|
|
|
4121
4536
|
// packages/render/src/elements/iftags.ts
|
|
4537
|
+
function evaluateIfTagsCondition(condition, pageTags) {
|
|
4538
|
+
const pageTagSet = new Set(pageTags.map((t) => t.toLowerCase()));
|
|
4539
|
+
const tokens = condition.split(/\s+/).filter(Boolean);
|
|
4540
|
+
if (tokens.length === 0) {
|
|
4541
|
+
return false;
|
|
4542
|
+
}
|
|
4543
|
+
const required = [];
|
|
4544
|
+
const excluded = [];
|
|
4545
|
+
const optional = [];
|
|
4546
|
+
for (const token of tokens) {
|
|
4547
|
+
if (token.startsWith("+")) {
|
|
4548
|
+
required.push(token.slice(1).toLowerCase());
|
|
4549
|
+
} else if (token.startsWith("-")) {
|
|
4550
|
+
excluded.push(token.slice(1).toLowerCase());
|
|
4551
|
+
} else {
|
|
4552
|
+
optional.push(token.toLowerCase());
|
|
4553
|
+
}
|
|
4554
|
+
}
|
|
4555
|
+
for (const tag of required) {
|
|
4556
|
+
if (!pageTagSet.has(tag))
|
|
4557
|
+
return false;
|
|
4558
|
+
}
|
|
4559
|
+
for (const tag of excluded) {
|
|
4560
|
+
if (pageTagSet.has(tag))
|
|
4561
|
+
return false;
|
|
4562
|
+
}
|
|
4563
|
+
if (optional.length > 0) {
|
|
4564
|
+
const hasAnyOptional = optional.some((tag) => pageTagSet.has(tag));
|
|
4565
|
+
if (!hasAnyOptional)
|
|
4566
|
+
return false;
|
|
4567
|
+
}
|
|
4568
|
+
return true;
|
|
4569
|
+
}
|
|
4122
4570
|
function renderIfTags(ctx, data) {
|
|
4123
|
-
|
|
4571
|
+
const pageTags = ctx.page?.tags ?? [];
|
|
4572
|
+
if (evaluateIfTagsCondition(data.condition, pageTags)) {
|
|
4573
|
+
renderElements(ctx, data.elements);
|
|
4574
|
+
}
|
|
4124
4575
|
}
|
|
4125
4576
|
|
|
4126
4577
|
// packages/render/src/elements/color.ts
|
|
@@ -4295,7 +4746,7 @@ class ExprParser {
|
|
|
4295
4746
|
parse() {
|
|
4296
4747
|
const result = this.parseOr();
|
|
4297
4748
|
if (this.current().kind !== "EOF") {
|
|
4298
|
-
throw new Error("
|
|
4749
|
+
throw new Error("too many values in the stack");
|
|
4299
4750
|
}
|
|
4300
4751
|
return result;
|
|
4301
4752
|
}
|
|
@@ -4408,7 +4859,7 @@ class ExprParser {
|
|
|
4408
4859
|
}
|
|
4409
4860
|
if (kind === "PLUS") {
|
|
4410
4861
|
this.advance();
|
|
4411
|
-
return this.parseUnary();
|
|
4862
|
+
return +this.parseUnary();
|
|
4412
4863
|
}
|
|
4413
4864
|
return this.parsePrimary();
|
|
4414
4865
|
}
|
|
@@ -4531,8 +4982,11 @@ function renderIf(ctx, data) {
|
|
|
4531
4982
|
}
|
|
4532
4983
|
function renderIfExpr(ctx, data) {
|
|
4533
4984
|
const result = evaluateExpression(data.expression);
|
|
4534
|
-
|
|
4535
|
-
|
|
4985
|
+
if (!result.success) {
|
|
4986
|
+
ctx.pushEscaped(`run-time error: ${result.error}`);
|
|
4987
|
+
return;
|
|
4988
|
+
}
|
|
4989
|
+
const elements = result.value !== 0 ? data.then : data.else;
|
|
4536
4990
|
renderBranchElements(ctx, elements);
|
|
4537
4991
|
}
|
|
4538
4992
|
function renderBranchElements(ctx, elements) {
|
|
@@ -4630,7 +5084,7 @@ function renderElement(ctx, element) {
|
|
|
4630
5084
|
renderBibliographyCite(ctx, element.data);
|
|
4631
5085
|
break;
|
|
4632
5086
|
case "bibliography-block":
|
|
4633
|
-
renderBibliographyBlock(ctx, element.data);
|
|
5087
|
+
renderBibliographyBlock(ctx, element.data, renderElements);
|
|
4634
5088
|
break;
|
|
4635
5089
|
case "table-of-contents":
|
|
4636
5090
|
renderTableOfContents(ctx, element.data);
|
|
@@ -4647,6 +5101,9 @@ function renderElement(ctx, element) {
|
|
|
4647
5101
|
case "embed":
|
|
4648
5102
|
renderEmbed(ctx, element.data);
|
|
4649
5103
|
break;
|
|
5104
|
+
case "embed-block":
|
|
5105
|
+
renderEmbedBlock(ctx, element.data);
|
|
5106
|
+
break;
|
|
4650
5107
|
case "user":
|
|
4651
5108
|
renderUser(ctx, element.data);
|
|
4652
5109
|
break;
|