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