@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.js
CHANGED
|
@@ -367,17 +367,43 @@ class RenderContext {
|
|
|
367
367
|
_footnoteIndex = 0;
|
|
368
368
|
_equationIndex = 0;
|
|
369
369
|
_htmlBlockIndex = 0;
|
|
370
|
+
_bibciteCounter = 0;
|
|
370
371
|
options;
|
|
371
372
|
footnotes;
|
|
372
373
|
styles;
|
|
373
374
|
htmlBlocks;
|
|
374
375
|
tocElements;
|
|
376
|
+
bibliographyMap;
|
|
377
|
+
bibliographyEntries;
|
|
375
378
|
constructor(tree, options = {}) {
|
|
376
379
|
this.options = options;
|
|
377
380
|
this.footnotes = options.footnotes ?? tree.footnotes ?? [];
|
|
378
381
|
this.styles = tree.styles ?? [];
|
|
379
382
|
this.htmlBlocks = tree["html-blocks"] ?? [];
|
|
380
383
|
this.tocElements = tree["table-of-contents"] ?? [];
|
|
384
|
+
this.bibliographyMap = new Map;
|
|
385
|
+
this.bibliographyEntries = [];
|
|
386
|
+
this.buildBibliographyMap(tree.elements);
|
|
387
|
+
}
|
|
388
|
+
buildBibliographyMap(elements) {
|
|
389
|
+
for (const el of elements) {
|
|
390
|
+
if (el.element === "bibliography-block") {
|
|
391
|
+
const data = el.data;
|
|
392
|
+
for (const entry of data.entries) {
|
|
393
|
+
if (!this.bibliographyMap.has(entry.key_string)) {
|
|
394
|
+
const index = this.bibliographyMap.size + 1;
|
|
395
|
+
this.bibliographyMap.set(entry.key_string, index);
|
|
396
|
+
this.bibliographyEntries.push(entry);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if ("data" in el && el.data && typeof el.data === "object") {
|
|
401
|
+
const data = el.data;
|
|
402
|
+
if ("elements" in data && Array.isArray(data.elements)) {
|
|
403
|
+
this.buildBibliographyMap(data.elements);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
381
407
|
}
|
|
382
408
|
push(html) {
|
|
383
409
|
this.chunks.push(html);
|
|
@@ -400,15 +426,24 @@ class RenderContext {
|
|
|
400
426
|
nextHtmlBlockIndex() {
|
|
401
427
|
return this._htmlBlockIndex++;
|
|
402
428
|
}
|
|
429
|
+
nextBibciteCounter() {
|
|
430
|
+
return ++this._bibciteCounter;
|
|
431
|
+
}
|
|
403
432
|
get page() {
|
|
404
433
|
return this.options.page;
|
|
405
434
|
}
|
|
406
435
|
resolveImageSource(source) {
|
|
436
|
+
const pageName = this.page?.pageName;
|
|
407
437
|
switch (source.type) {
|
|
408
|
-
case "url":
|
|
409
|
-
|
|
438
|
+
case "url": {
|
|
439
|
+
const url = source.data;
|
|
440
|
+
if (url.startsWith("/") && !url.startsWith("//")) {
|
|
441
|
+
return `/local--files${url}`;
|
|
442
|
+
}
|
|
443
|
+
return url;
|
|
444
|
+
}
|
|
410
445
|
case "file1":
|
|
411
|
-
return `/local--files/${source.data.file}`;
|
|
446
|
+
return pageName ? `/local--files/${pageName}/${source.data.file}` : `/local--files/${source.data.file}`;
|
|
412
447
|
case "file2":
|
|
413
448
|
return `/local--files/${source.data.page}/${source.data.file}`;
|
|
414
449
|
case "file3":
|
|
@@ -419,10 +454,34 @@ class RenderContext {
|
|
|
419
454
|
if (typeof location === "string") {
|
|
420
455
|
return location;
|
|
421
456
|
}
|
|
457
|
+
const page = location.page;
|
|
458
|
+
if (page.startsWith("//")) {
|
|
459
|
+
return page.toLowerCase();
|
|
460
|
+
}
|
|
461
|
+
const hashIdx = page.indexOf("#");
|
|
462
|
+
if (hashIdx !== -1) {
|
|
463
|
+
let pagePart = page.slice(0, hashIdx);
|
|
464
|
+
const anchor = page.slice(hashIdx);
|
|
465
|
+
if (pagePart.endsWith("/")) {
|
|
466
|
+
pagePart = pagePart.slice(0, -1);
|
|
467
|
+
}
|
|
468
|
+
return `/${pagePart.toLowerCase()}${anchor.toLowerCase()}`;
|
|
469
|
+
}
|
|
470
|
+
const normalizedPage = this.normalizePageName(page);
|
|
471
|
+
const safePage = normalizedPage.startsWith("/") ? normalizedPage.slice(1) : normalizedPage;
|
|
422
472
|
if (location.site) {
|
|
423
|
-
return `https://${location.site}.wikidot.com/${
|
|
473
|
+
return `https://${location.site}.wikidot.com/${safePage}`;
|
|
474
|
+
}
|
|
475
|
+
return `/${safePage}`;
|
|
476
|
+
}
|
|
477
|
+
normalizePageName(page) {
|
|
478
|
+
let normalized = page.toLowerCase();
|
|
479
|
+
normalized = normalized.replace(/:\s+/g, ":");
|
|
480
|
+
normalized = normalized.replace(/\s+/g, "-").trim();
|
|
481
|
+
if (!normalized.startsWith("/")) {
|
|
482
|
+
normalized = normalized.replace(/\//g, "-");
|
|
424
483
|
}
|
|
425
|
-
return
|
|
484
|
+
return normalized;
|
|
426
485
|
}
|
|
427
486
|
renderAttributes(attributes) {
|
|
428
487
|
const safe = sanitizeAttributes(attributes);
|
|
@@ -517,6 +576,9 @@ function renderStringContainer(ctx, type, attributes, elements) {
|
|
|
517
576
|
ctx.push("</span>");
|
|
518
577
|
break;
|
|
519
578
|
case "div":
|
|
579
|
+
if (elements.length === 0 && Object.keys(attributes).length === 0) {
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
520
582
|
ctx.push(`<div${renderAttrs(attributes)}>`);
|
|
521
583
|
renderElements(ctx, elements);
|
|
522
584
|
ctx.push("</div>");
|
|
@@ -636,7 +698,7 @@ function renderRaw(ctx, data) {
|
|
|
636
698
|
if (data === "")
|
|
637
699
|
return;
|
|
638
700
|
ctx.push(`<span style="white-space: pre-wrap;">`);
|
|
639
|
-
ctx.push(escapeHtml(data));
|
|
701
|
+
ctx.push(escapeHtml(data).replace(/ /g, " "));
|
|
640
702
|
ctx.push("</span>");
|
|
641
703
|
}
|
|
642
704
|
function renderEmail(ctx, email) {
|
|
@@ -658,6 +720,19 @@ function renderLink(ctx, data) {
|
|
|
658
720
|
href = "#invalid-url";
|
|
659
721
|
}
|
|
660
722
|
const attrs = [`href="${escapeAttr(href)}"`];
|
|
723
|
+
if (data.type === "page" && typeof data.link === "object") {
|
|
724
|
+
const page = data.link.page;
|
|
725
|
+
const isSpecialPage = page.startsWith("//") || page.includes(":") || page.includes("#/");
|
|
726
|
+
if (!isSpecialPage) {
|
|
727
|
+
const hashIdx = page.indexOf("#");
|
|
728
|
+
const pageToCheck = hashIdx !== -1 ? page.slice(0, hashIdx) : page;
|
|
729
|
+
const pageExists = ctx.page?.pageExists;
|
|
730
|
+
const exists = pageExists ? pageExists(pageToCheck) : false;
|
|
731
|
+
if (!exists) {
|
|
732
|
+
attrs.push(`class="newpage"`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
661
736
|
if (data.target) {
|
|
662
737
|
const targetMap = {
|
|
663
738
|
"new-tab": "_blank",
|
|
@@ -751,7 +826,16 @@ function renderImage(ctx, data) {
|
|
|
751
826
|
const imgTag = `<img ${imgAttrs.join(" ")} />`;
|
|
752
827
|
let output = imgTag;
|
|
753
828
|
if (data.link) {
|
|
754
|
-
let href
|
|
829
|
+
let href;
|
|
830
|
+
if (typeof data.link === "string") {
|
|
831
|
+
if (!data.link.startsWith("/") && !data.link.startsWith("#") && !data.link.startsWith("http://") && !data.link.startsWith("https://")) {
|
|
832
|
+
href = `/${data.link}`;
|
|
833
|
+
} else {
|
|
834
|
+
href = data.link;
|
|
835
|
+
}
|
|
836
|
+
} else {
|
|
837
|
+
href = `/${data.link.page}`;
|
|
838
|
+
}
|
|
755
839
|
if (isDangerousUrl(href)) {
|
|
756
840
|
href = "#invalid-url";
|
|
757
841
|
}
|
|
@@ -768,7 +852,16 @@ function renderImage(ctx, data) {
|
|
|
768
852
|
}
|
|
769
853
|
function getAlignmentClass(align, isFloat) {
|
|
770
854
|
if (isFloat) {
|
|
771
|
-
|
|
855
|
+
switch (align) {
|
|
856
|
+
case "left":
|
|
857
|
+
return "floatleft";
|
|
858
|
+
case "right":
|
|
859
|
+
return "floatright";
|
|
860
|
+
case "center":
|
|
861
|
+
return "floatcenter";
|
|
862
|
+
default:
|
|
863
|
+
return `float${align}`;
|
|
864
|
+
}
|
|
772
865
|
}
|
|
773
866
|
switch (align) {
|
|
774
867
|
case "left":
|
|
@@ -797,7 +890,87 @@ function getFilenameFromSource(source) {
|
|
|
797
890
|
}
|
|
798
891
|
|
|
799
892
|
// packages/render/src/elements/list.ts
|
|
893
|
+
function trimTextElements(elements) {
|
|
894
|
+
if (elements.length === 0)
|
|
895
|
+
return elements;
|
|
896
|
+
let start = 0;
|
|
897
|
+
let end = elements.length;
|
|
898
|
+
while (start < end) {
|
|
899
|
+
const el = elements[start];
|
|
900
|
+
if (el.element === "text" && typeof el.data === "string" && el.data.trim() === "") {
|
|
901
|
+
start++;
|
|
902
|
+
} else {
|
|
903
|
+
break;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
while (end > start) {
|
|
907
|
+
const el = elements[end - 1];
|
|
908
|
+
if (el.element === "text" && typeof el.data === "string" && el.data.trim() === "") {
|
|
909
|
+
end--;
|
|
910
|
+
} else {
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
return elements.slice(start, end);
|
|
915
|
+
}
|
|
916
|
+
function isLiCloseTextParagraph(el) {
|
|
917
|
+
if (el.element !== "container")
|
|
918
|
+
return false;
|
|
919
|
+
const data = el.data;
|
|
920
|
+
if (data.type !== "paragraph")
|
|
921
|
+
return false;
|
|
922
|
+
const texts = data.elements.filter((e) => e.element === "text").map((e) => e.data);
|
|
923
|
+
const combined = texts.join("").trim();
|
|
924
|
+
return combined === "[[/li]]";
|
|
925
|
+
}
|
|
926
|
+
function renderNoMarkerElements(ctx, elements) {
|
|
927
|
+
const trimmed = trimTextElements(elements);
|
|
928
|
+
if (trimmed.length === 0)
|
|
929
|
+
return;
|
|
930
|
+
const paragraphIndices = [];
|
|
931
|
+
for (let i = 0;i < trimmed.length; i++) {
|
|
932
|
+
const el = trimmed[i];
|
|
933
|
+
if (el.element === "container" && el.data.type === "paragraph") {
|
|
934
|
+
paragraphIndices.push(i);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (paragraphIndices.length === 0) {
|
|
938
|
+
renderElements(ctx, trimmed);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
const firstParagraphIdx = paragraphIndices[0];
|
|
942
|
+
const lastParagraphIdx = paragraphIndices[paragraphIndices.length - 1];
|
|
943
|
+
for (let i = 0;i < trimmed.length; i++) {
|
|
944
|
+
const el = trimmed[i];
|
|
945
|
+
if (el.element === "container" && el.data.type === "paragraph") {
|
|
946
|
+
const data = el.data;
|
|
947
|
+
if (i === firstParagraphIdx) {
|
|
948
|
+
renderElements(ctx, data.elements);
|
|
949
|
+
} else if (i === lastParagraphIdx && isLiCloseTextParagraph(el)) {
|
|
950
|
+
renderElements(ctx, data.elements);
|
|
951
|
+
} else {
|
|
952
|
+
ctx.push("<p>");
|
|
953
|
+
renderElements(ctx, data.elements);
|
|
954
|
+
ctx.push("</p>");
|
|
955
|
+
}
|
|
956
|
+
} else {
|
|
957
|
+
renderElement(ctx, el);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
800
961
|
function renderList(ctx, data) {
|
|
962
|
+
const hasContent = data.items.some((item) => {
|
|
963
|
+
if (item["item-type"] === "sub-list")
|
|
964
|
+
return true;
|
|
965
|
+
if (item["item-type"] === "elements") {
|
|
966
|
+
const trimmed = trimTextElements(item.elements);
|
|
967
|
+
return trimmed.length > 0;
|
|
968
|
+
}
|
|
969
|
+
return false;
|
|
970
|
+
});
|
|
971
|
+
if (!hasContent) {
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
801
974
|
const tag = data.type === "numbered" ? "ol" : "ul";
|
|
802
975
|
ctx.push(`<${tag}${renderListAttrs(data.attributes)}>`);
|
|
803
976
|
const items = data.items;
|
|
@@ -805,8 +978,14 @@ function renderList(ctx, data) {
|
|
|
805
978
|
while (i < items.length) {
|
|
806
979
|
const item = items[i];
|
|
807
980
|
if (item["item-type"] === "elements") {
|
|
808
|
-
|
|
809
|
-
|
|
981
|
+
const hasNoMarker = item.attributes._noMarker === "true";
|
|
982
|
+
const styleAttr = hasNoMarker ? ' style="list-style: none"' : "";
|
|
983
|
+
ctx.push(`<li${renderListAttrs(item.attributes)}${styleAttr}>`);
|
|
984
|
+
if (hasNoMarker) {
|
|
985
|
+
renderNoMarkerElements(ctx, item.elements);
|
|
986
|
+
} else {
|
|
987
|
+
renderElements(ctx, trimTextElements(item.elements));
|
|
988
|
+
}
|
|
810
989
|
while (i + 1 < items.length && items[i + 1]["item-type"] === "sub-list") {
|
|
811
990
|
i++;
|
|
812
991
|
const subItem = items[i];
|
|
@@ -3724,7 +3903,7 @@ function renderTabView(ctx, tabs) {
|
|
|
3724
3903
|
ctx.push(`<div class="yui-content">`);
|
|
3725
3904
|
for (let i = 0;i < tabs.length; i++) {
|
|
3726
3905
|
const tab = tabs[i];
|
|
3727
|
-
const displayStyle = i === 0 ? "" : ` style="display:
|
|
3906
|
+
const displayStyle = i === 0 ? "" : ` style="display:none"`;
|
|
3728
3907
|
ctx.push(`<div id="wiki-tab-0-${i}"${displayStyle}>`);
|
|
3729
3908
|
renderElements(ctx, tab.elements);
|
|
3730
3909
|
ctx.push("</div>");
|
|
@@ -3743,8 +3922,6 @@ function renderFootnoteRef(ctx, index) {
|
|
|
3743
3922
|
ctx.push("</sup>");
|
|
3744
3923
|
}
|
|
3745
3924
|
function renderFootnoteBlock(ctx, data) {
|
|
3746
|
-
if (data.hide)
|
|
3747
|
-
return;
|
|
3748
3925
|
if (ctx.footnotes.length === 0)
|
|
3749
3926
|
return;
|
|
3750
3927
|
const title = data.title ?? "Footnotes";
|
|
@@ -3762,26 +3939,81 @@ function renderFootnoteBlock(ctx, data) {
|
|
|
3762
3939
|
}
|
|
3763
3940
|
|
|
3764
3941
|
// packages/render/src/elements/math.ts
|
|
3942
|
+
import temml from "temml";
|
|
3943
|
+
function needsAlignedWrapper(latex) {
|
|
3944
|
+
if (/\\begin\s*\{/.test(latex)) {
|
|
3945
|
+
return false;
|
|
3946
|
+
}
|
|
3947
|
+
const withoutEscaped = latex.replace(/\\&/g, "");
|
|
3948
|
+
return withoutEscaped.includes("&");
|
|
3949
|
+
}
|
|
3950
|
+
function renderLatexToMathML(latex, displayMode) {
|
|
3951
|
+
try {
|
|
3952
|
+
let processedLatex = latex;
|
|
3953
|
+
if (displayMode && needsAlignedWrapper(latex)) {
|
|
3954
|
+
processedLatex = `\\begin{aligned}
|
|
3955
|
+
${latex}
|
|
3956
|
+
\\end{aligned}`;
|
|
3957
|
+
}
|
|
3958
|
+
return temml.renderToString(processedLatex, {
|
|
3959
|
+
displayMode,
|
|
3960
|
+
throwOnError: false,
|
|
3961
|
+
annotate: false
|
|
3962
|
+
});
|
|
3963
|
+
} catch {
|
|
3964
|
+
return "";
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3765
3967
|
function renderMath(ctx, data) {
|
|
3766
3968
|
const index = ctx.nextEquationIndex() + 1;
|
|
3969
|
+
const latex = data["latex-source"];
|
|
3970
|
+
const mathml = renderLatexToMathML(latex, true);
|
|
3971
|
+
const id = data.name ? `equation-${data.name}` : `equation-${index}`;
|
|
3972
|
+
const dataName = data.name ? ` data-name="${escapeAttr(data.name)}"` : "";
|
|
3973
|
+
ctx.push(`<div class="math-block" id="${escapeAttr(id)}"${dataName}>`);
|
|
3767
3974
|
if (data.name) {
|
|
3768
3975
|
ctx.push(`<span class="equation-number">(${index})</span>`);
|
|
3769
3976
|
}
|
|
3770
|
-
|
|
3771
|
-
ctx.push(
|
|
3772
|
-
ctx.push(
|
|
3977
|
+
ctx.push(`<code class="math-source" hidden aria-hidden="true">`);
|
|
3978
|
+
ctx.push(escapeHtml(latex));
|
|
3979
|
+
ctx.push(`</code>`);
|
|
3980
|
+
ctx.push(`<span class="math-render">`);
|
|
3981
|
+
if (mathml) {
|
|
3982
|
+
ctx.push(mathml);
|
|
3983
|
+
} else {
|
|
3984
|
+
ctx.push(`<span class="math-error">`);
|
|
3985
|
+
ctx.push(escapeHtml(latex));
|
|
3986
|
+
ctx.push(`</span>`);
|
|
3987
|
+
}
|
|
3988
|
+
ctx.push(`</span>`);
|
|
3773
3989
|
ctx.push("</div>");
|
|
3774
3990
|
}
|
|
3775
3991
|
function renderMathInline(ctx, data) {
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
ctx.push(
|
|
3992
|
+
const latex = data["latex-source"];
|
|
3993
|
+
const mathml = renderLatexToMathML(latex, false);
|
|
3994
|
+
ctx.push(`<span class="math-inline">`);
|
|
3995
|
+
ctx.push(`<code class="math-source" hidden aria-hidden="true">`);
|
|
3996
|
+
ctx.push(escapeHtml(latex));
|
|
3997
|
+
ctx.push(`</code>`);
|
|
3998
|
+
ctx.push(`<span class="math-render">`);
|
|
3999
|
+
if (mathml) {
|
|
4000
|
+
ctx.push(mathml);
|
|
4001
|
+
} else {
|
|
4002
|
+
ctx.push(`<span class="math-error">$`);
|
|
4003
|
+
ctx.push(escapeHtml(latex));
|
|
4004
|
+
ctx.push(`$</span>`);
|
|
4005
|
+
}
|
|
4006
|
+
ctx.push(`</span>`);
|
|
4007
|
+
ctx.push("</span>");
|
|
3779
4008
|
}
|
|
3780
4009
|
function renderEquationRef(ctx, name) {
|
|
3781
4010
|
const id = `equation-${name}`;
|
|
3782
|
-
ctx.push(`<
|
|
4011
|
+
ctx.push(`<span class="eref" data-target="${escapeAttr(id)}">`);
|
|
4012
|
+
ctx.push(`<a class="eref-link" href="#${escapeAttr(id)}">`);
|
|
3783
4013
|
ctx.push(escapeHtml(name));
|
|
3784
|
-
ctx.push(
|
|
4014
|
+
ctx.push(`</a>`);
|
|
4015
|
+
ctx.push(`<span class="eref-tooltip" aria-hidden="true"></span>`);
|
|
4016
|
+
ctx.push("</span>");
|
|
3785
4017
|
}
|
|
3786
4018
|
|
|
3787
4019
|
// packages/render/src/elements/module/backlinks.ts
|
|
@@ -3838,7 +4070,7 @@ function renderListPages(ctx, _data) {
|
|
|
3838
4070
|
function renderModule(ctx, data) {
|
|
3839
4071
|
switch (data.module) {
|
|
3840
4072
|
case "unknown":
|
|
3841
|
-
ctx.push(`<div class="error-block">[[module <em>${data.name}</em>]] No such module, please <a href="
|
|
4073
|
+
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>`);
|
|
3842
4074
|
break;
|
|
3843
4075
|
case "backlinks":
|
|
3844
4076
|
renderBacklinks(ctx, data);
|
|
@@ -3926,8 +4158,148 @@ function renderGitlabSnippet(ctx, snippetId) {
|
|
|
3926
4158
|
ctx.push(`<script src="https://gitlab.com/snippets/${escapeAttr(snippetId)}.js"></script>`);
|
|
3927
4159
|
}
|
|
3928
4160
|
|
|
4161
|
+
// packages/render/src/elements/embed-block.ts
|
|
4162
|
+
import DOMPurify from "dompurify";
|
|
4163
|
+
import { JSDOM } from "jsdom";
|
|
4164
|
+
var BOOLEAN_ATTRIBUTES = [
|
|
4165
|
+
"allowfullscreen",
|
|
4166
|
+
"async",
|
|
4167
|
+
"autofocus",
|
|
4168
|
+
"autoplay",
|
|
4169
|
+
"checked",
|
|
4170
|
+
"controls",
|
|
4171
|
+
"default",
|
|
4172
|
+
"defer",
|
|
4173
|
+
"disabled",
|
|
4174
|
+
"formnovalidate",
|
|
4175
|
+
"hidden",
|
|
4176
|
+
"ismap",
|
|
4177
|
+
"loop",
|
|
4178
|
+
"multiple",
|
|
4179
|
+
"muted",
|
|
4180
|
+
"novalidate",
|
|
4181
|
+
"open",
|
|
4182
|
+
"readonly",
|
|
4183
|
+
"required",
|
|
4184
|
+
"reversed",
|
|
4185
|
+
"selected"
|
|
4186
|
+
];
|
|
4187
|
+
var DEFAULT_EMBED_ALLOWLIST = [
|
|
4188
|
+
{ host: "*.youtube.com", pathPrefix: "/embed/" },
|
|
4189
|
+
{ host: "*.youtube-nocookie.com", pathPrefix: "/embed/" },
|
|
4190
|
+
{ host: "player.vimeo.com", pathPrefix: "/video/" },
|
|
4191
|
+
{ host: "*.google.com", pathPrefix: "/maps/embed" },
|
|
4192
|
+
{ host: "calendar.google.com", pathPrefix: "/calendar/embed" },
|
|
4193
|
+
{ host: "open.spotify.com", pathPrefix: "/embed/" },
|
|
4194
|
+
{ host: "w.soundcloud.com", pathPrefix: "/player/" },
|
|
4195
|
+
{ host: "codepen.io" }
|
|
4196
|
+
];
|
|
4197
|
+
var window = new JSDOM("").window;
|
|
4198
|
+
var purify = DOMPurify(window);
|
|
4199
|
+
purify.addHook("uponSanitizeAttribute", (_node, data) => {
|
|
4200
|
+
if (data.attrName === "src" && data.attrValue) {
|
|
4201
|
+
if (!data.attrValue.toLowerCase().startsWith("https://")) {
|
|
4202
|
+
data.attrValue = "";
|
|
4203
|
+
data.forceKeepAttr = false;
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
});
|
|
4207
|
+
var DOMPURIFY_CONFIG = {
|
|
4208
|
+
ALLOWED_TAGS: ["iframe"],
|
|
4209
|
+
ADD_ATTR: ["allow", "allowfullscreen", "frameborder", "loading", "referrerpolicy", "sandbox"],
|
|
4210
|
+
FORBID_ATTR: ["srcdoc", "onload", "onerror", "onclick"]
|
|
4211
|
+
};
|
|
4212
|
+
function matchesHostPattern(hostname, pattern) {
|
|
4213
|
+
const lowerHostname = hostname.toLowerCase();
|
|
4214
|
+
const lowerPattern = pattern.toLowerCase();
|
|
4215
|
+
if (lowerPattern.startsWith("*.")) {
|
|
4216
|
+
const base = lowerPattern.slice(2);
|
|
4217
|
+
return lowerHostname === base || lowerHostname.endsWith("." + base);
|
|
4218
|
+
}
|
|
4219
|
+
return lowerHostname === lowerPattern;
|
|
4220
|
+
}
|
|
4221
|
+
function matchesAllowlistEntry(url, entry) {
|
|
4222
|
+
if (!matchesHostPattern(url.hostname, entry.host)) {
|
|
4223
|
+
return false;
|
|
4224
|
+
}
|
|
4225
|
+
if (entry.pathPrefix) {
|
|
4226
|
+
const pathLower = url.pathname.toLowerCase();
|
|
4227
|
+
const prefixLower = entry.pathPrefix.toLowerCase();
|
|
4228
|
+
if (!pathLower.startsWith(prefixLower)) {
|
|
4229
|
+
return false;
|
|
4230
|
+
}
|
|
4231
|
+
if (!prefixLower.endsWith("/")) {
|
|
4232
|
+
const remainder = pathLower.slice(prefixLower.length);
|
|
4233
|
+
if (remainder && !/^[/?#]/.test(remainder)) {
|
|
4234
|
+
return false;
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
return true;
|
|
4239
|
+
}
|
|
4240
|
+
function validateAndSanitizeEmbed(content, allowlist) {
|
|
4241
|
+
const sanitized = purify.sanitize(content.trim(), {
|
|
4242
|
+
...DOMPURIFY_CONFIG,
|
|
4243
|
+
RETURN_TRUSTED_TYPE: false
|
|
4244
|
+
});
|
|
4245
|
+
if (!sanitized.trim()) {
|
|
4246
|
+
return null;
|
|
4247
|
+
}
|
|
4248
|
+
const dom = new JSDOM(sanitized);
|
|
4249
|
+
const iframes = dom.window.document.querySelectorAll("iframe");
|
|
4250
|
+
if (iframes.length !== 1) {
|
|
4251
|
+
return null;
|
|
4252
|
+
}
|
|
4253
|
+
const iframe = iframes[0];
|
|
4254
|
+
const src = iframe.getAttribute("src")?.trim();
|
|
4255
|
+
if (!src) {
|
|
4256
|
+
return null;
|
|
4257
|
+
}
|
|
4258
|
+
let url;
|
|
4259
|
+
try {
|
|
4260
|
+
url = new URL(src);
|
|
4261
|
+
} catch {
|
|
4262
|
+
return null;
|
|
4263
|
+
}
|
|
4264
|
+
if (url.protocol !== "https:") {
|
|
4265
|
+
return null;
|
|
4266
|
+
}
|
|
4267
|
+
if (allowlist !== null) {
|
|
4268
|
+
const matched = allowlist.some((entry) => matchesAllowlistEntry(url, entry));
|
|
4269
|
+
if (!matched) {
|
|
4270
|
+
return null;
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
return sanitized;
|
|
4274
|
+
}
|
|
4275
|
+
function normalizeBooleanAttributes(html) {
|
|
4276
|
+
let result = html;
|
|
4277
|
+
for (const attr of BOOLEAN_ATTRIBUTES) {
|
|
4278
|
+
const standalonePattern = new RegExp(`\\s${attr}(?=\\s|>|/>)`, "gi");
|
|
4279
|
+
result = result.replace(standalonePattern, ` ${attr}="${attr}"`);
|
|
4280
|
+
const emptyValuePattern = new RegExp(`\\s${attr}=""`, "gi");
|
|
4281
|
+
result = result.replace(emptyValuePattern, ` ${attr}="${attr}"`);
|
|
4282
|
+
}
|
|
4283
|
+
return result;
|
|
4284
|
+
}
|
|
4285
|
+
function renderEmbedBlock(ctx, data) {
|
|
4286
|
+
const allowlist = ctx.options.embedAllowlist !== undefined ? ctx.options.embedAllowlist : DEFAULT_EMBED_ALLOWLIST;
|
|
4287
|
+
const sanitized = validateAndSanitizeEmbed(data.contents, allowlist);
|
|
4288
|
+
if (sanitized === null) {
|
|
4289
|
+
ctx.push('<div class="error-block">Sorry, no match for the embedded content.</div>');
|
|
4290
|
+
return;
|
|
4291
|
+
}
|
|
4292
|
+
const normalized = normalizeBooleanAttributes(sanitized);
|
|
4293
|
+
ctx.push(normalized);
|
|
4294
|
+
}
|
|
4295
|
+
|
|
3929
4296
|
// packages/render/src/elements/user.ts
|
|
3930
4297
|
function renderUser(ctx, data) {
|
|
4298
|
+
const normalized = data.name.toLowerCase().trim();
|
|
4299
|
+
if (normalized === "anonymous") {
|
|
4300
|
+
ctx.push("Anonymous");
|
|
4301
|
+
return;
|
|
4302
|
+
}
|
|
3931
4303
|
const resolved = ctx.options.resolvers?.user?.(data.name) ?? null;
|
|
3932
4304
|
if (resolved === null) {
|
|
3933
4305
|
ctx.push(escapeHtml(data.name));
|
|
@@ -3937,9 +4309,10 @@ function renderUser(ctx, data) {
|
|
|
3937
4309
|
const hrefAttr = resolved.url ? ` href="${escapeAttr(resolved.url)}"` : "";
|
|
3938
4310
|
const showAvatar = data["show-avatar"] && resolved.url && resolved.avatarUrl;
|
|
3939
4311
|
if (showAvatar) {
|
|
4312
|
+
const styleAttr = resolved.karmaUrl ? ` style="background-image:url(${escapeAttr(resolved.karmaUrl)})"` : "";
|
|
3940
4313
|
ctx.push(`<span class="printuser avatarhover">`);
|
|
3941
4314
|
ctx.push(`<a${hrefAttr}>`);
|
|
3942
|
-
ctx.push(`<img class="small" src="${escapeAttr(resolved.avatarUrl)}" alt="${escapeAttr(displayName)}" />`);
|
|
4315
|
+
ctx.push(`<img class="small" src="${escapeAttr(resolved.avatarUrl)}" alt="${escapeAttr(displayName)}"${styleAttr} />`);
|
|
3943
4316
|
ctx.push("</a>");
|
|
3944
4317
|
ctx.push(`<a${hrefAttr}>`);
|
|
3945
4318
|
ctx.push(escapeHtml(displayName));
|
|
@@ -3955,23 +4328,43 @@ function renderUser(ctx, data) {
|
|
|
3955
4328
|
}
|
|
3956
4329
|
|
|
3957
4330
|
// packages/render/src/elements/bibliography.ts
|
|
4331
|
+
function generateIdSuffix(label, counter) {
|
|
4332
|
+
let h = 2166136261;
|
|
4333
|
+
const input = label + counter;
|
|
4334
|
+
for (let i = 0;i < input.length; i++) {
|
|
4335
|
+
h ^= input.charCodeAt(i);
|
|
4336
|
+
h = Math.imul(h, 16777619);
|
|
4337
|
+
}
|
|
4338
|
+
return (h >>> 0).toString(16).slice(0, 6);
|
|
4339
|
+
}
|
|
3958
4340
|
function renderBibliographyCite(ctx, data) {
|
|
3959
|
-
|
|
3960
|
-
|
|
4341
|
+
const number = ctx.bibliographyMap.get(data.label);
|
|
4342
|
+
const counter = ctx.nextBibciteCounter();
|
|
4343
|
+
if (number === undefined) {
|
|
4344
|
+
ctx.push(escapeHtml(data.label));
|
|
4345
|
+
return;
|
|
3961
4346
|
}
|
|
3962
|
-
|
|
3963
|
-
|
|
4347
|
+
const idSuffix = generateIdSuffix(data.label, counter);
|
|
4348
|
+
const id = `bibcite-${number}-${idSuffix}`;
|
|
4349
|
+
const onclick = `WIKIDOT.page.utils.scrollToReference('bibitem-${number}')`;
|
|
4350
|
+
ctx.push(`<a href="javascript:;" class="bibcite" id="${id}" onclick="${escapeAttr(onclick)}">`);
|
|
4351
|
+
ctx.push(String(number));
|
|
3964
4352
|
ctx.push("</a>");
|
|
3965
|
-
if (data.brackets) {
|
|
3966
|
-
ctx.push("]");
|
|
3967
|
-
}
|
|
3968
4353
|
}
|
|
3969
|
-
function renderBibliographyBlock(ctx, data) {
|
|
4354
|
+
function renderBibliographyBlock(ctx, data, renderElements2) {
|
|
3970
4355
|
if (data.hide)
|
|
3971
4356
|
return;
|
|
3972
4357
|
const title = data.title ?? "Bibliography";
|
|
3973
4358
|
ctx.push(`<div class="bibitems">`);
|
|
3974
4359
|
ctx.push(`<div class="title">${escapeHtml(title)}</div>`);
|
|
4360
|
+
let index = 1;
|
|
4361
|
+
for (const entry of data.entries) {
|
|
4362
|
+
ctx.push(`<div class="bibitem" id="bibitem-${index}">`);
|
|
4363
|
+
ctx.push(`${index}. `);
|
|
4364
|
+
renderElements2(ctx, entry.value);
|
|
4365
|
+
ctx.push("</div>");
|
|
4366
|
+
index++;
|
|
4367
|
+
}
|
|
3975
4368
|
ctx.push("</div>");
|
|
3976
4369
|
}
|
|
3977
4370
|
|
|
@@ -4074,17 +4467,61 @@ function renderHtmlBlock(ctx, data) {
|
|
|
4074
4467
|
const src = callbackUrl || generateDefaultUrl(pageName, data.contents);
|
|
4075
4468
|
const sandbox = ctx.options.htmlBlockSandbox;
|
|
4076
4469
|
const sandboxAttr = sandbox ? ` sandbox="${escapeAttr(sandbox)}"` : "";
|
|
4077
|
-
|
|
4470
|
+
const styleAttr = data.style ? ` style="${escapeAttr(sanitizeStyleValue(data.style))}"` : "";
|
|
4471
|
+
ctx.push(`<p><iframe src="${escapeAttr(src)}"${sandboxAttr} allowtransparency="true" frameborder="0" class="html-block-iframe"${styleAttr}></iframe></p>`);
|
|
4078
4472
|
}
|
|
4079
4473
|
|
|
4080
4474
|
// packages/render/src/elements/include.ts
|
|
4081
4475
|
function renderInclude(ctx, data) {
|
|
4476
|
+
if (data.elements.length === 0) {
|
|
4477
|
+
const pageName = data.location.page.toLowerCase();
|
|
4478
|
+
const encodedPageName = pageName.replace(/[^a-z0-9\-_:/]/g, (c) => encodeURIComponent(c));
|
|
4479
|
+
const safePath = encodedPageName.startsWith("/") ? encodedPageName.slice(1) : encodedPageName;
|
|
4480
|
+
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>`);
|
|
4481
|
+
return;
|
|
4482
|
+
}
|
|
4082
4483
|
renderElements(ctx, data.elements);
|
|
4083
4484
|
}
|
|
4084
4485
|
|
|
4085
4486
|
// packages/render/src/elements/iftags.ts
|
|
4487
|
+
function evaluateIfTagsCondition(condition, pageTags) {
|
|
4488
|
+
const pageTagSet = new Set(pageTags.map((t) => t.toLowerCase()));
|
|
4489
|
+
const tokens = condition.split(/\s+/).filter(Boolean);
|
|
4490
|
+
if (tokens.length === 0) {
|
|
4491
|
+
return false;
|
|
4492
|
+
}
|
|
4493
|
+
const required = [];
|
|
4494
|
+
const excluded = [];
|
|
4495
|
+
const optional = [];
|
|
4496
|
+
for (const token of tokens) {
|
|
4497
|
+
if (token.startsWith("+")) {
|
|
4498
|
+
required.push(token.slice(1).toLowerCase());
|
|
4499
|
+
} else if (token.startsWith("-")) {
|
|
4500
|
+
excluded.push(token.slice(1).toLowerCase());
|
|
4501
|
+
} else {
|
|
4502
|
+
optional.push(token.toLowerCase());
|
|
4503
|
+
}
|
|
4504
|
+
}
|
|
4505
|
+
for (const tag of required) {
|
|
4506
|
+
if (!pageTagSet.has(tag))
|
|
4507
|
+
return false;
|
|
4508
|
+
}
|
|
4509
|
+
for (const tag of excluded) {
|
|
4510
|
+
if (pageTagSet.has(tag))
|
|
4511
|
+
return false;
|
|
4512
|
+
}
|
|
4513
|
+
if (optional.length > 0) {
|
|
4514
|
+
const hasAnyOptional = optional.some((tag) => pageTagSet.has(tag));
|
|
4515
|
+
if (!hasAnyOptional)
|
|
4516
|
+
return false;
|
|
4517
|
+
}
|
|
4518
|
+
return true;
|
|
4519
|
+
}
|
|
4086
4520
|
function renderIfTags(ctx, data) {
|
|
4087
|
-
|
|
4521
|
+
const pageTags = ctx.page?.tags ?? [];
|
|
4522
|
+
if (evaluateIfTagsCondition(data.condition, pageTags)) {
|
|
4523
|
+
renderElements(ctx, data.elements);
|
|
4524
|
+
}
|
|
4088
4525
|
}
|
|
4089
4526
|
|
|
4090
4527
|
// packages/render/src/elements/color.ts
|
|
@@ -4259,7 +4696,7 @@ class ExprParser {
|
|
|
4259
4696
|
parse() {
|
|
4260
4697
|
const result = this.parseOr();
|
|
4261
4698
|
if (this.current().kind !== "EOF") {
|
|
4262
|
-
throw new Error("
|
|
4699
|
+
throw new Error("too many values in the stack");
|
|
4263
4700
|
}
|
|
4264
4701
|
return result;
|
|
4265
4702
|
}
|
|
@@ -4372,7 +4809,7 @@ class ExprParser {
|
|
|
4372
4809
|
}
|
|
4373
4810
|
if (kind === "PLUS") {
|
|
4374
4811
|
this.advance();
|
|
4375
|
-
return this.parseUnary();
|
|
4812
|
+
return +this.parseUnary();
|
|
4376
4813
|
}
|
|
4377
4814
|
return this.parsePrimary();
|
|
4378
4815
|
}
|
|
@@ -4495,8 +4932,11 @@ function renderIf(ctx, data) {
|
|
|
4495
4932
|
}
|
|
4496
4933
|
function renderIfExpr(ctx, data) {
|
|
4497
4934
|
const result = evaluateExpression(data.expression);
|
|
4498
|
-
|
|
4499
|
-
|
|
4935
|
+
if (!result.success) {
|
|
4936
|
+
ctx.pushEscaped(`run-time error: ${result.error}`);
|
|
4937
|
+
return;
|
|
4938
|
+
}
|
|
4939
|
+
const elements = result.value !== 0 ? data.then : data.else;
|
|
4500
4940
|
renderBranchElements(ctx, elements);
|
|
4501
4941
|
}
|
|
4502
4942
|
function renderBranchElements(ctx, elements) {
|
|
@@ -4594,7 +5034,7 @@ function renderElement(ctx, element) {
|
|
|
4594
5034
|
renderBibliographyCite(ctx, element.data);
|
|
4595
5035
|
break;
|
|
4596
5036
|
case "bibliography-block":
|
|
4597
|
-
renderBibliographyBlock(ctx, element.data);
|
|
5037
|
+
renderBibliographyBlock(ctx, element.data, renderElements);
|
|
4598
5038
|
break;
|
|
4599
5039
|
case "table-of-contents":
|
|
4600
5040
|
renderTableOfContents(ctx, element.data);
|
|
@@ -4611,6 +5051,9 @@ function renderElement(ctx, element) {
|
|
|
4611
5051
|
case "embed":
|
|
4612
5052
|
renderEmbed(ctx, element.data);
|
|
4613
5053
|
break;
|
|
5054
|
+
case "embed-block":
|
|
5055
|
+
renderEmbedBlock(ctx, element.data);
|
|
5056
|
+
break;
|
|
4614
5057
|
case "user":
|
|
4615
5058
|
renderUser(ctx, element.data);
|
|
4616
5059
|
break;
|
|
@@ -4664,5 +5107,6 @@ function renderElement(ctx, element) {
|
|
|
4664
5107
|
}
|
|
4665
5108
|
}
|
|
4666
5109
|
export {
|
|
4667
|
-
renderToHtml
|
|
5110
|
+
renderToHtml,
|
|
5111
|
+
DEFAULT_EMBED_ALLOWLIST
|
|
4668
5112
|
};
|