@wdprlib/render 0.1.5 → 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/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
- return source.data;
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/${location.page}`;
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 `/${location.page}`;
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, "&#32;"));
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 = typeof data.link === "string" ? data.link : `/${data.link.page}`;
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
- return align === "left" ? "floatleft" : "floatright";
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
- ctx.push(`<li${renderListAttrs(item.attributes)}>`);
845
- renderElements(ctx, item.elements);
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: none"`;
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
- const id = data.name ? `equation-${data.name}` : `equation-${index}`;
3807
- ctx.push(`<div class="math-equation" id="${escapeAttr(id)}">`);
3808
- ctx.push(escapeHtml(data["latex-source"]));
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
- ctx.push(`<span class="math-inline">$`);
3813
- ctx.push(escapeHtml(data["latex-source"]));
3814
- ctx.push("$</span>");
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(`<a class="equation-ref" href="#${escapeAttr(id)}">`);
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("</a>");
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="http://www.wikidot.com/doc:modules" target="_blank">check available modules</a> and fix this page.</div>`);
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
- if (data.brackets) {
3996
- ctx.push("[");
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
- ctx.push(`<a class="bibcite" href="javascript:;">`);
3999
- ctx.push(escapeHtml(data.label));
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
- ctx.push(`<iframe src="${escapeAttr(src)}"${sandboxAttr} allowtransparency="true" frameborder="0" class="html-block-iframe"></iframe>`);
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
- renderElements(ctx, data.elements);
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("Unexpected token");
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
- const isTrue = result.success && result.value !== 0;
4535
- const elements = isTrue ? data.then : data.else;
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;