@wdprlib/render 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -44,10 +44,15 @@ var __export = (target, all) => {
44
44
  var exports_src = {};
45
45
  __export(exports_src, {
46
46
  renderToHtml: () => renderToHtml,
47
+ createSettings: () => import_ast3.createSettings,
48
+ DEFAULT_SETTINGS: () => import_ast3.DEFAULT_SETTINGS,
47
49
  DEFAULT_EMBED_ALLOWLIST: () => DEFAULT_EMBED_ALLOWLIST
48
50
  });
49
51
  module.exports = __toCommonJS(exports_src);
50
52
 
53
+ // packages/render/src/context.ts
54
+ var import_ast = require("@wdprlib/ast");
55
+
51
56
  // packages/render/src/escape.ts
52
57
  function escapeHtml(text) {
53
58
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -418,6 +423,8 @@ class RenderContext {
418
423
  _equationIndex = 0;
419
424
  _htmlBlockIndex = 0;
420
425
  _bibciteCounter = 0;
426
+ _idSuffix;
427
+ settings;
421
428
  options;
422
429
  footnotes;
423
430
  styles;
@@ -426,6 +433,8 @@ class RenderContext {
426
433
  bibliographyMap;
427
434
  bibliographyEntries;
428
435
  constructor(tree, options = {}) {
436
+ this.settings = options.settings ?? import_ast.DEFAULT_SETTINGS;
437
+ this._idSuffix = this.settings.useTrueIds ? null : Math.random().toString(16).slice(2, 8);
429
438
  this.options = options;
430
439
  this.footnotes = options.footnotes ?? tree.footnotes ?? [];
431
440
  this.styles = tree.styles ?? [];
@@ -479,6 +488,18 @@ class RenderContext {
479
488
  nextBibciteCounter() {
480
489
  return ++this._bibciteCounter;
481
490
  }
491
+ generateId(prefix, index) {
492
+ if (this._idSuffix === null) {
493
+ return `${prefix}${index}`;
494
+ }
495
+ return `${prefix}${index}-${this._idSuffix}`;
496
+ }
497
+ generateFixedId(name) {
498
+ if (this._idSuffix === null) {
499
+ return name;
500
+ }
501
+ return `${name}-${this._idSuffix}`;
502
+ }
482
503
  get page() {
483
504
  return this.options.page;
484
505
  }
@@ -488,15 +509,23 @@ class RenderContext {
488
509
  case "url": {
489
510
  const url = source.data;
490
511
  if (url.startsWith("/") && !url.startsWith("//")) {
512
+ if (!this.settings.allowLocalPaths)
513
+ return null;
491
514
  return `/local--files${url}`;
492
515
  }
493
516
  return url;
494
517
  }
495
518
  case "file1":
519
+ if (!this.settings.allowLocalPaths)
520
+ return null;
496
521
  return pageName ? `/local--files/${pageName}/${source.data.file}` : `/local--files/${source.data.file}`;
497
522
  case "file2":
523
+ if (!this.settings.allowLocalPaths)
524
+ return null;
498
525
  return `/local--files/${source.data.page}/${source.data.file}`;
499
526
  case "file3":
527
+ if (!this.settings.allowLocalPaths)
528
+ return null;
500
529
  return `/local--files/${source.data.site}/${source.data.page}/${source.data.file}`;
501
530
  }
502
531
  }
@@ -548,28 +577,28 @@ class RenderContext {
548
577
  }
549
578
 
550
579
  // packages/render/src/elements/container.ts
551
- var import_ast = require("@wdprlib/ast");
580
+ var import_ast2 = require("@wdprlib/ast");
552
581
  function renderContainer(ctx, data) {
553
582
  const { type, attributes, elements } = data;
554
- if (import_ast.isHeaderType(type)) {
583
+ if (import_ast2.isHeaderType(type)) {
555
584
  renderHeader(ctx, type.header.level, type.header["has-toc"], attributes, elements);
556
585
  return;
557
586
  }
558
- if (import_ast.isAlignType(type)) {
587
+ if (import_ast2.isAlignType(type)) {
559
588
  ctx.push(`<div style="text-align: ${type.align};">`);
560
589
  renderElements(ctx, elements);
561
590
  ctx.push("</div>");
562
591
  return;
563
592
  }
564
- if (import_ast.isStringContainerType(type)) {
593
+ if (import_ast2.isStringContainerType(type)) {
565
594
  renderStringContainer(ctx, type, attributes, elements);
566
595
  }
567
596
  }
568
597
  function renderHeader(ctx, level, hasToc, attributes, elements) {
569
598
  const tag = `h${level}`;
570
599
  if (hasToc) {
571
- const tocId = ctx.nextTocIndex();
572
- ctx.push(`<${tag} id="toc${tocId}"${renderAttrs(attributes)}>`);
600
+ const tocId = ctx.generateId("toc", ctx.nextTocIndex());
601
+ ctx.push(`<${tag} id="${tocId}"${renderAttrs(attributes)}>`);
573
602
  } else {
574
603
  ctx.push(`<${tag}${renderAttrs(attributes)}>`);
575
604
  }
@@ -855,6 +884,8 @@ function renderAnchorName(ctx, name) {
855
884
  // packages/render/src/elements/image.ts
856
885
  function renderImage(ctx, data) {
857
886
  let src = ctx.resolveImageSource(data.source);
887
+ if (src === null)
888
+ return;
858
889
  if (isDangerousUrl(src)) {
859
890
  src = "#invalid-url";
860
891
  }
@@ -3939,7 +3970,7 @@ function fnv1aHash(input, hexLen) {
3939
3970
  function renderTabView(ctx, tabs) {
3940
3971
  const labelString = tabs.map((t) => t.label).join("");
3941
3972
  const hash = md5Hash(labelString);
3942
- const widgetId = `wiki-tabview-${hash}`;
3973
+ const widgetId = ctx.generateFixedId(`wiki-tabview-${hash}`);
3943
3974
  ctx.push(`<div id="${widgetId}" class="yui-navset">`);
3944
3975
  ctx.push(`<ul class="yui-nav">`);
3945
3976
  for (let i = 0;i < tabs.length; i++) {
@@ -3954,7 +3985,8 @@ function renderTabView(ctx, tabs) {
3954
3985
  for (let i = 0;i < tabs.length; i++) {
3955
3986
  const tab = tabs[i];
3956
3987
  const displayStyle = i === 0 ? "" : ` style="display:none"`;
3957
- ctx.push(`<div id="wiki-tab-0-${i}"${displayStyle}>`);
3988
+ const tabId = ctx.generateId("wiki-tab-0-", i);
3989
+ ctx.push(`<div id="${tabId}"${displayStyle}>`);
3958
3990
  renderElements(ctx, tab.elements);
3959
3991
  ctx.push("</div>");
3960
3992
  }
@@ -3967,8 +3999,9 @@ function md5Hash(input) {
3967
3999
 
3968
4000
  // packages/render/src/elements/footnote.ts
3969
4001
  function renderFootnoteRef(ctx, index) {
4002
+ const id = ctx.generateId("footnoteref-", index);
3970
4003
  ctx.push(`<sup class="footnoteref">`);
3971
- ctx.push(`<a id="footnoteref-${index}" href="javascript:;" class="footnoteref">${index}</a>`);
4004
+ ctx.push(`<a id="${id}" href="javascript:;" class="footnoteref">${index}</a>`);
3972
4005
  ctx.push("</sup>");
3973
4006
  }
3974
4007
  function renderFootnoteBlock(ctx, data) {
@@ -3980,7 +4013,8 @@ function renderFootnoteBlock(ctx, data) {
3980
4013
  for (let i = 0;i < ctx.footnotes.length; i++) {
3981
4014
  const index = i + 1;
3982
4015
  const elements = ctx.footnotes[i] ?? [];
3983
- ctx.push(`<div class="footnote-footer" id="footnote-${index}">`);
4016
+ const fnId = ctx.generateId("footnote-", index);
4017
+ ctx.push(`<div class="footnote-footer" id="${fnId}">`);
3984
4018
  ctx.push(`<a href="javascript:;">${index}</a>. `);
3985
4019
  renderElements(ctx, elements);
3986
4020
  ctx.push("</div>");
@@ -4018,7 +4052,7 @@ function renderMath(ctx, data) {
4018
4052
  const index = ctx.nextEquationIndex() + 1;
4019
4053
  const latex = data["latex-source"];
4020
4054
  const mathml = renderLatexToMathML(latex, true);
4021
- const id = data.name ? `equation-${data.name}` : `equation-${index}`;
4055
+ const id = data.name ? ctx.generateId("equation-", data.name) : ctx.generateId("equation-", index);
4022
4056
  const dataName = data.name ? ` data-name="${escapeAttr(data.name)}"` : "";
4023
4057
  ctx.push(`<div class="math-block" id="${escapeAttr(id)}"${dataName}>`);
4024
4058
  if (data.name) {
@@ -4057,7 +4091,7 @@ function renderMathInline(ctx, data) {
4057
4091
  ctx.push("</span>");
4058
4092
  }
4059
4093
  function renderEquationRef(ctx, name) {
4060
- const id = `equation-${name}`;
4094
+ const id = ctx.generateId("equation-", name);
4061
4095
  ctx.push(`<span class="eref" data-target="${escapeAttr(id)}">`);
4062
4096
  ctx.push(`<a class="eref-link" href="#${escapeAttr(id)}">`);
4063
4097
  ctx.push(escapeHtml(name));
@@ -4260,7 +4294,7 @@ var SANITIZE_CONFIG = {
4260
4294
  "width"
4261
4295
  ]
4262
4296
  },
4263
- allowedSchemes: ["https"]
4297
+ allowedSchemes: ["https", "http"]
4264
4298
  };
4265
4299
  function findIframes(html) {
4266
4300
  const doc = import_htmlparser2.parseDocument(html);
@@ -4308,7 +4342,7 @@ function matchesAllowlistEntry(url, entry) {
4308
4342
  }
4309
4343
  return true;
4310
4344
  }
4311
- function validateAndSanitizeEmbed(content, allowlist) {
4345
+ function validateAndSanitizeEmbed(content, allowlist, baseUrl) {
4312
4346
  const sanitized = import_sanitize_html.default(content.trim(), SANITIZE_CONFIG);
4313
4347
  if (!sanitized.trim()) {
4314
4348
  return null;
@@ -4324,11 +4358,16 @@ function validateAndSanitizeEmbed(content, allowlist) {
4324
4358
  }
4325
4359
  let url;
4326
4360
  try {
4327
- url = new URL(src);
4361
+ if (src.startsWith("//")) {
4362
+ const base = baseUrl ?? "https://localhost";
4363
+ url = new URL(src, base);
4364
+ } else {
4365
+ url = new URL(src);
4366
+ }
4328
4367
  } catch {
4329
4368
  return null;
4330
4369
  }
4331
- if (url.protocol !== "https:") {
4370
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
4332
4371
  return null;
4333
4372
  }
4334
4373
  if (allowlist !== null) {
@@ -4351,7 +4390,7 @@ function normalizeBooleanAttributes(html) {
4351
4390
  }
4352
4391
  function renderEmbedBlock(ctx, data) {
4353
4392
  const allowlist = ctx.options.embedAllowlist !== undefined ? ctx.options.embedAllowlist : DEFAULT_EMBED_ALLOWLIST;
4354
- const sanitized = validateAndSanitizeEmbed(data.contents, allowlist);
4393
+ const sanitized = validateAndSanitizeEmbed(data.contents, allowlist, ctx.options.baseUrl);
4355
4394
  if (sanitized === null) {
4356
4395
  ctx.push('<div class="error-block">Sorry, no match for the embedded content.</div>');
4357
4396
  return;
@@ -4412,8 +4451,9 @@ function renderBibliographyCite(ctx, data) {
4412
4451
  return;
4413
4452
  }
4414
4453
  const idSuffix = generateIdSuffix(data.label, counter);
4415
- const id = `bibcite-${number}-${idSuffix}`;
4416
- const onclick = `WIKIDOT.page.utils.scrollToReference('bibitem-${number}')`;
4454
+ const id = ctx.generateId(`bibcite-${number}-`, idSuffix);
4455
+ const bibitemId = ctx.generateId("bibitem-", number);
4456
+ const onclick = `WIKIDOT.page.utils.scrollToReference('${bibitemId}')`;
4417
4457
  ctx.push(`<a href="javascript:;" class="bibcite" id="${id}" onclick="${escapeAttr(onclick)}">`);
4418
4458
  ctx.push(String(number));
4419
4459
  ctx.push("</a>");
@@ -4426,7 +4466,8 @@ function renderBibliographyBlock(ctx, data, renderElements2) {
4426
4466
  ctx.push(`<div class="title">${escapeHtml(title)}</div>`);
4427
4467
  let index = 1;
4428
4468
  for (const entry of data.entries) {
4429
- ctx.push(`<div class="bibitem" id="bibitem-${index}">`);
4469
+ const itemId = ctx.generateId("bibitem-", index);
4470
+ ctx.push(`<div class="bibitem" id="${itemId}">`);
4430
4471
  ctx.push(`${index}. `);
4431
4472
  renderElements2(ctx, entry.value);
4432
4473
  ctx.push("</div>");
@@ -4459,12 +4500,19 @@ function renderTocList(ctx, listData, depth) {
4459
4500
  renderTocItem(ctx, item, depth);
4460
4501
  }
4461
4502
  }
4503
+ function rewriteTocAnchor(ctx, href) {
4504
+ const match = /^#toc(\d+)$/.exec(href);
4505
+ if (!match)
4506
+ return href;
4507
+ return `#${ctx.generateId("toc", Number(match[1]))}`;
4508
+ }
4462
4509
  function renderTocItem(ctx, item, depth) {
4463
4510
  if (item["item-type"] === "elements") {
4464
4511
  for (const el of item.elements) {
4465
4512
  const link = extractLinkText(el);
4466
4513
  if (link) {
4467
- ctx.push(`<div style="margin-left: ${depth}em;"><a href="${escapeHtml(link.href)}">${escapeHtml(link.text)}</a></div>`);
4514
+ const href = rewriteTocAnchor(ctx, link.href);
4515
+ ctx.push(`<div style="margin-left: ${depth}em;"><a href="${escapeAttr(href)}">${escapeHtml(link.text)}</a></div>`);
4468
4516
  }
4469
4517
  }
4470
4518
  } else if (item["item-type"] === "sub-list") {
@@ -5029,7 +5077,7 @@ function formatNumber(n) {
5029
5077
  function renderToHtml(tree, options = {}) {
5030
5078
  const ctx = new RenderContext(tree, options);
5031
5079
  renderElements(ctx, tree.elements);
5032
- if (tree.styles?.length) {
5080
+ if (ctx.settings.allowStyleElements && tree.styles?.length) {
5033
5081
  for (const style of tree.styles) {
5034
5082
  ctx.push(`<style>${escapeStyleContent(style)}</style>`);
5035
5083
  }
@@ -5173,3 +5221,6 @@ function renderElement(ctx, element) {
5173
5221
  break;
5174
5222
  }
5175
5223
  }
5224
+
5225
+ // packages/render/src/index.ts
5226
+ var import_ast3 = require("@wdprlib/ast");
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { SyntaxTree as SyntaxTree2 } from "@wdprlib/ast";
2
- import { Element } from "@wdprlib/ast";
2
+ import { Element, WikitextSettings } from "@wdprlib/ast";
3
3
  /**
4
4
  * Allowlist entry for embed content validation
5
5
  * Each entry specifies a host pattern and optional path prefix
@@ -70,6 +70,15 @@ interface RenderResolvers {
70
70
  * Options for HTML rendering
71
71
  */
72
72
  interface RenderOptions {
73
+ /**
74
+ * Base URL used to resolve protocol-relative URLs (e.g., "//example.com/path").
75
+ * The protocol of this URL is inherited by protocol-relative references.
76
+ * Example: "https://scp-wiki.wikidot.com" or "http://scp-jp.wikidot.com"
77
+ * If not provided, protocol-relative URLs default to HTTPS.
78
+ */
79
+ baseUrl?: string;
80
+ /** Wikitext settings controlling rendering behavior */
81
+ settings?: WikitextSettings;
73
82
  /** Page context for resolving file paths, links, etc. */
74
83
  page?: PageContext;
75
84
  /** Pre-collected footnote elements from SyntaxTree.footnotes */
@@ -100,4 +109,6 @@ interface RenderOptions {
100
109
  * Render a SyntaxTree to HTML string
101
110
  */
102
111
  declare function renderToHtml(tree: SyntaxTree2, options?: RenderOptions): string;
103
- export { renderToHtml, ResolvedUser, RenderResolvers, RenderOptions, PageContext, DEFAULT_EMBED_ALLOWLIST };
112
+ import { WikitextMode, WikitextSettings as WikitextSettings3 } from "@wdprlib/ast";
113
+ import { createSettings, DEFAULT_SETTINGS } from "@wdprlib/ast";
114
+ export { renderToHtml, createSettings, WikitextSettings3 as WikitextSettings, WikitextMode, ResolvedUser, RenderResolvers, RenderOptions, PageContext, DEFAULT_SETTINGS, DEFAULT_EMBED_ALLOWLIST };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { SyntaxTree as SyntaxTree2 } from "@wdprlib/ast";
2
- import { Element } from "@wdprlib/ast";
2
+ import { Element, WikitextSettings } from "@wdprlib/ast";
3
3
  /**
4
4
  * Allowlist entry for embed content validation
5
5
  * Each entry specifies a host pattern and optional path prefix
@@ -70,6 +70,15 @@ interface RenderResolvers {
70
70
  * Options for HTML rendering
71
71
  */
72
72
  interface RenderOptions {
73
+ /**
74
+ * Base URL used to resolve protocol-relative URLs (e.g., "//example.com/path").
75
+ * The protocol of this URL is inherited by protocol-relative references.
76
+ * Example: "https://scp-wiki.wikidot.com" or "http://scp-jp.wikidot.com"
77
+ * If not provided, protocol-relative URLs default to HTTPS.
78
+ */
79
+ baseUrl?: string;
80
+ /** Wikitext settings controlling rendering behavior */
81
+ settings?: WikitextSettings;
73
82
  /** Page context for resolving file paths, links, etc. */
74
83
  page?: PageContext;
75
84
  /** Pre-collected footnote elements from SyntaxTree.footnotes */
@@ -100,4 +109,6 @@ interface RenderOptions {
100
109
  * Render a SyntaxTree to HTML string
101
110
  */
102
111
  declare function renderToHtml(tree: SyntaxTree2, options?: RenderOptions): string;
103
- export { renderToHtml, ResolvedUser, RenderResolvers, RenderOptions, PageContext, DEFAULT_EMBED_ALLOWLIST };
112
+ import { WikitextMode, WikitextSettings as WikitextSettings3 } from "@wdprlib/ast";
113
+ import { createSettings, DEFAULT_SETTINGS } from "@wdprlib/ast";
114
+ export { renderToHtml, createSettings, WikitextSettings3 as WikitextSettings, WikitextMode, ResolvedUser, RenderResolvers, RenderOptions, PageContext, DEFAULT_SETTINGS, DEFAULT_EMBED_ALLOWLIST };
package/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ // packages/render/src/context.ts
2
+ import { DEFAULT_SETTINGS } from "@wdprlib/ast";
3
+
1
4
  // packages/render/src/escape.ts
2
5
  function escapeHtml(text) {
3
6
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -368,6 +371,8 @@ class RenderContext {
368
371
  _equationIndex = 0;
369
372
  _htmlBlockIndex = 0;
370
373
  _bibciteCounter = 0;
374
+ _idSuffix;
375
+ settings;
371
376
  options;
372
377
  footnotes;
373
378
  styles;
@@ -376,6 +381,8 @@ class RenderContext {
376
381
  bibliographyMap;
377
382
  bibliographyEntries;
378
383
  constructor(tree, options = {}) {
384
+ this.settings = options.settings ?? DEFAULT_SETTINGS;
385
+ this._idSuffix = this.settings.useTrueIds ? null : Math.random().toString(16).slice(2, 8);
379
386
  this.options = options;
380
387
  this.footnotes = options.footnotes ?? tree.footnotes ?? [];
381
388
  this.styles = tree.styles ?? [];
@@ -429,6 +436,18 @@ class RenderContext {
429
436
  nextBibciteCounter() {
430
437
  return ++this._bibciteCounter;
431
438
  }
439
+ generateId(prefix, index) {
440
+ if (this._idSuffix === null) {
441
+ return `${prefix}${index}`;
442
+ }
443
+ return `${prefix}${index}-${this._idSuffix}`;
444
+ }
445
+ generateFixedId(name) {
446
+ if (this._idSuffix === null) {
447
+ return name;
448
+ }
449
+ return `${name}-${this._idSuffix}`;
450
+ }
432
451
  get page() {
433
452
  return this.options.page;
434
453
  }
@@ -438,15 +457,23 @@ class RenderContext {
438
457
  case "url": {
439
458
  const url = source.data;
440
459
  if (url.startsWith("/") && !url.startsWith("//")) {
460
+ if (!this.settings.allowLocalPaths)
461
+ return null;
441
462
  return `/local--files${url}`;
442
463
  }
443
464
  return url;
444
465
  }
445
466
  case "file1":
467
+ if (!this.settings.allowLocalPaths)
468
+ return null;
446
469
  return pageName ? `/local--files/${pageName}/${source.data.file}` : `/local--files/${source.data.file}`;
447
470
  case "file2":
471
+ if (!this.settings.allowLocalPaths)
472
+ return null;
448
473
  return `/local--files/${source.data.page}/${source.data.file}`;
449
474
  case "file3":
475
+ if (!this.settings.allowLocalPaths)
476
+ return null;
450
477
  return `/local--files/${source.data.site}/${source.data.page}/${source.data.file}`;
451
478
  }
452
479
  }
@@ -518,8 +545,8 @@ function renderContainer(ctx, data) {
518
545
  function renderHeader(ctx, level, hasToc, attributes, elements) {
519
546
  const tag = `h${level}`;
520
547
  if (hasToc) {
521
- const tocId = ctx.nextTocIndex();
522
- ctx.push(`<${tag} id="toc${tocId}"${renderAttrs(attributes)}>`);
548
+ const tocId = ctx.generateId("toc", ctx.nextTocIndex());
549
+ ctx.push(`<${tag} id="${tocId}"${renderAttrs(attributes)}>`);
523
550
  } else {
524
551
  ctx.push(`<${tag}${renderAttrs(attributes)}>`);
525
552
  }
@@ -805,6 +832,8 @@ function renderAnchorName(ctx, name) {
805
832
  // packages/render/src/elements/image.ts
806
833
  function renderImage(ctx, data) {
807
834
  let src = ctx.resolveImageSource(data.source);
835
+ if (src === null)
836
+ return;
808
837
  if (isDangerousUrl(src)) {
809
838
  src = "#invalid-url";
810
839
  }
@@ -3889,7 +3918,7 @@ function fnv1aHash(input, hexLen) {
3889
3918
  function renderTabView(ctx, tabs) {
3890
3919
  const labelString = tabs.map((t) => t.label).join("");
3891
3920
  const hash = md5Hash(labelString);
3892
- const widgetId = `wiki-tabview-${hash}`;
3921
+ const widgetId = ctx.generateFixedId(`wiki-tabview-${hash}`);
3893
3922
  ctx.push(`<div id="${widgetId}" class="yui-navset">`);
3894
3923
  ctx.push(`<ul class="yui-nav">`);
3895
3924
  for (let i = 0;i < tabs.length; i++) {
@@ -3904,7 +3933,8 @@ function renderTabView(ctx, tabs) {
3904
3933
  for (let i = 0;i < tabs.length; i++) {
3905
3934
  const tab = tabs[i];
3906
3935
  const displayStyle = i === 0 ? "" : ` style="display:none"`;
3907
- ctx.push(`<div id="wiki-tab-0-${i}"${displayStyle}>`);
3936
+ const tabId = ctx.generateId("wiki-tab-0-", i);
3937
+ ctx.push(`<div id="${tabId}"${displayStyle}>`);
3908
3938
  renderElements(ctx, tab.elements);
3909
3939
  ctx.push("</div>");
3910
3940
  }
@@ -3917,8 +3947,9 @@ function md5Hash(input) {
3917
3947
 
3918
3948
  // packages/render/src/elements/footnote.ts
3919
3949
  function renderFootnoteRef(ctx, index) {
3950
+ const id = ctx.generateId("footnoteref-", index);
3920
3951
  ctx.push(`<sup class="footnoteref">`);
3921
- ctx.push(`<a id="footnoteref-${index}" href="javascript:;" class="footnoteref">${index}</a>`);
3952
+ ctx.push(`<a id="${id}" href="javascript:;" class="footnoteref">${index}</a>`);
3922
3953
  ctx.push("</sup>");
3923
3954
  }
3924
3955
  function renderFootnoteBlock(ctx, data) {
@@ -3930,7 +3961,8 @@ function renderFootnoteBlock(ctx, data) {
3930
3961
  for (let i = 0;i < ctx.footnotes.length; i++) {
3931
3962
  const index = i + 1;
3932
3963
  const elements = ctx.footnotes[i] ?? [];
3933
- ctx.push(`<div class="footnote-footer" id="footnote-${index}">`);
3964
+ const fnId = ctx.generateId("footnote-", index);
3965
+ ctx.push(`<div class="footnote-footer" id="${fnId}">`);
3934
3966
  ctx.push(`<a href="javascript:;">${index}</a>. `);
3935
3967
  renderElements(ctx, elements);
3936
3968
  ctx.push("</div>");
@@ -3968,7 +4000,7 @@ function renderMath(ctx, data) {
3968
4000
  const index = ctx.nextEquationIndex() + 1;
3969
4001
  const latex = data["latex-source"];
3970
4002
  const mathml = renderLatexToMathML(latex, true);
3971
- const id = data.name ? `equation-${data.name}` : `equation-${index}`;
4003
+ const id = data.name ? ctx.generateId("equation-", data.name) : ctx.generateId("equation-", index);
3972
4004
  const dataName = data.name ? ` data-name="${escapeAttr(data.name)}"` : "";
3973
4005
  ctx.push(`<div class="math-block" id="${escapeAttr(id)}"${dataName}>`);
3974
4006
  if (data.name) {
@@ -4007,7 +4039,7 @@ function renderMathInline(ctx, data) {
4007
4039
  ctx.push("</span>");
4008
4040
  }
4009
4041
  function renderEquationRef(ctx, name) {
4010
- const id = `equation-${name}`;
4042
+ const id = ctx.generateId("equation-", name);
4011
4043
  ctx.push(`<span class="eref" data-target="${escapeAttr(id)}">`);
4012
4044
  ctx.push(`<a class="eref-link" href="#${escapeAttr(id)}">`);
4013
4045
  ctx.push(escapeHtml(name));
@@ -4210,7 +4242,7 @@ var SANITIZE_CONFIG = {
4210
4242
  "width"
4211
4243
  ]
4212
4244
  },
4213
- allowedSchemes: ["https"]
4245
+ allowedSchemes: ["https", "http"]
4214
4246
  };
4215
4247
  function findIframes(html) {
4216
4248
  const doc = parseDocument(html);
@@ -4258,7 +4290,7 @@ function matchesAllowlistEntry(url, entry) {
4258
4290
  }
4259
4291
  return true;
4260
4292
  }
4261
- function validateAndSanitizeEmbed(content, allowlist) {
4293
+ function validateAndSanitizeEmbed(content, allowlist, baseUrl) {
4262
4294
  const sanitized = sanitizeHtml(content.trim(), SANITIZE_CONFIG);
4263
4295
  if (!sanitized.trim()) {
4264
4296
  return null;
@@ -4274,11 +4306,16 @@ function validateAndSanitizeEmbed(content, allowlist) {
4274
4306
  }
4275
4307
  let url;
4276
4308
  try {
4277
- url = new URL(src);
4309
+ if (src.startsWith("//")) {
4310
+ const base = baseUrl ?? "https://localhost";
4311
+ url = new URL(src, base);
4312
+ } else {
4313
+ url = new URL(src);
4314
+ }
4278
4315
  } catch {
4279
4316
  return null;
4280
4317
  }
4281
- if (url.protocol !== "https:") {
4318
+ if (url.protocol !== "https:" && url.protocol !== "http:") {
4282
4319
  return null;
4283
4320
  }
4284
4321
  if (allowlist !== null) {
@@ -4301,7 +4338,7 @@ function normalizeBooleanAttributes(html) {
4301
4338
  }
4302
4339
  function renderEmbedBlock(ctx, data) {
4303
4340
  const allowlist = ctx.options.embedAllowlist !== undefined ? ctx.options.embedAllowlist : DEFAULT_EMBED_ALLOWLIST;
4304
- const sanitized = validateAndSanitizeEmbed(data.contents, allowlist);
4341
+ const sanitized = validateAndSanitizeEmbed(data.contents, allowlist, ctx.options.baseUrl);
4305
4342
  if (sanitized === null) {
4306
4343
  ctx.push('<div class="error-block">Sorry, no match for the embedded content.</div>');
4307
4344
  return;
@@ -4362,8 +4399,9 @@ function renderBibliographyCite(ctx, data) {
4362
4399
  return;
4363
4400
  }
4364
4401
  const idSuffix = generateIdSuffix(data.label, counter);
4365
- const id = `bibcite-${number}-${idSuffix}`;
4366
- const onclick = `WIKIDOT.page.utils.scrollToReference('bibitem-${number}')`;
4402
+ const id = ctx.generateId(`bibcite-${number}-`, idSuffix);
4403
+ const bibitemId = ctx.generateId("bibitem-", number);
4404
+ const onclick = `WIKIDOT.page.utils.scrollToReference('${bibitemId}')`;
4367
4405
  ctx.push(`<a href="javascript:;" class="bibcite" id="${id}" onclick="${escapeAttr(onclick)}">`);
4368
4406
  ctx.push(String(number));
4369
4407
  ctx.push("</a>");
@@ -4376,7 +4414,8 @@ function renderBibliographyBlock(ctx, data, renderElements2) {
4376
4414
  ctx.push(`<div class="title">${escapeHtml(title)}</div>`);
4377
4415
  let index = 1;
4378
4416
  for (const entry of data.entries) {
4379
- ctx.push(`<div class="bibitem" id="bibitem-${index}">`);
4417
+ const itemId = ctx.generateId("bibitem-", index);
4418
+ ctx.push(`<div class="bibitem" id="${itemId}">`);
4380
4419
  ctx.push(`${index}. `);
4381
4420
  renderElements2(ctx, entry.value);
4382
4421
  ctx.push("</div>");
@@ -4409,12 +4448,19 @@ function renderTocList(ctx, listData, depth) {
4409
4448
  renderTocItem(ctx, item, depth);
4410
4449
  }
4411
4450
  }
4451
+ function rewriteTocAnchor(ctx, href) {
4452
+ const match = /^#toc(\d+)$/.exec(href);
4453
+ if (!match)
4454
+ return href;
4455
+ return `#${ctx.generateId("toc", Number(match[1]))}`;
4456
+ }
4412
4457
  function renderTocItem(ctx, item, depth) {
4413
4458
  if (item["item-type"] === "elements") {
4414
4459
  for (const el of item.elements) {
4415
4460
  const link = extractLinkText(el);
4416
4461
  if (link) {
4417
- ctx.push(`<div style="margin-left: ${depth}em;"><a href="${escapeHtml(link.href)}">${escapeHtml(link.text)}</a></div>`);
4462
+ const href = rewriteTocAnchor(ctx, link.href);
4463
+ ctx.push(`<div style="margin-left: ${depth}em;"><a href="${escapeAttr(href)}">${escapeHtml(link.text)}</a></div>`);
4418
4464
  }
4419
4465
  }
4420
4466
  } else if (item["item-type"] === "sub-list") {
@@ -4979,7 +5025,7 @@ function formatNumber(n) {
4979
5025
  function renderToHtml(tree, options = {}) {
4980
5026
  const ctx = new RenderContext(tree, options);
4981
5027
  renderElements(ctx, tree.elements);
4982
- if (tree.styles?.length) {
5028
+ if (ctx.settings.allowStyleElements && tree.styles?.length) {
4983
5029
  for (const style of tree.styles) {
4984
5030
  ctx.push(`<style>${escapeStyleContent(style)}</style>`);
4985
5031
  }
@@ -5123,7 +5169,12 @@ function renderElement(ctx, element) {
5123
5169
  break;
5124
5170
  }
5125
5171
  }
5172
+
5173
+ // packages/render/src/index.ts
5174
+ import { createSettings, DEFAULT_SETTINGS as DEFAULT_SETTINGS2 } from "@wdprlib/ast";
5126
5175
  export {
5127
5176
  renderToHtml,
5177
+ createSettings,
5178
+ DEFAULT_SETTINGS2 as DEFAULT_SETTINGS,
5128
5179
  DEFAULT_EMBED_ALLOWLIST
5129
5180
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wdprlib/render",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "HTML renderer for Wikidot markup",
5
5
  "keywords": [
6
6
  "html",
@@ -39,7 +39,7 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@wdprlib/ast": "1.0.0",
42
+ "@wdprlib/ast": "1.1.0",
43
43
  "domhandler": "^5.0.3",
44
44
  "htmlparser2": "^10.0.0",
45
45
  "sanitize-html": "^2.14.0",