@staticbolt/docs 0.0.1-beta.3 → 0.0.1-beta.5

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.
Files changed (117) hide show
  1. package/README.md +16 -19
  2. package/lib/index.d.mts +62 -35
  3. package/lib/mermaid/abnfDiagram-PAR5IXJI-Ibbj5eGO.js +1 -0
  4. package/lib/mermaid/architecture-7GRP2DOG-M7G3SWCO-BJAoKIRD.js +1 -0
  5. package/lib/mermaid/architectureDiagram-2PNWDENW-CI3fLWIN.js +36 -0
  6. package/lib/mermaid/blockDiagram-PUWGUBOA-DAfWpjz7.js +129 -0
  7. package/lib/mermaid/c4Diagram-3NLNHZP5-at-f4FDi.js +38 -0
  8. package/lib/mermaid/chunk-2NMSUPJH-DuxvnX1p.js +1 -0
  9. package/lib/mermaid/chunk-32MCUW2P-D8U-Oy6M.js +206 -0
  10. package/lib/mermaid/chunk-3NPNU5GP-8WYybYAh.js +1 -0
  11. package/lib/mermaid/chunk-3SMGFRGC-C0WC5bhP.js +1 -0
  12. package/lib/mermaid/chunk-4CAJECRU-C_QnaDal.js +15 -0
  13. package/lib/mermaid/chunk-4OADUZ5Q-DHE3k_pb.js +10 -0
  14. package/lib/mermaid/chunk-5IMINLNL-Cl3N81sR.js +1 -0
  15. package/lib/mermaid/chunk-5IQCK5RY-Df5Ppxew.js +3 -0
  16. package/lib/mermaid/chunk-5MVFYWFU-DgXg5u6h.js +1 -0
  17. package/lib/mermaid/chunk-AQ6EADP3-CZhslHi-.js +1 -0
  18. package/lib/mermaid/chunk-AZZRMDJM-y-xJBuMz.js +15 -0
  19. package/lib/mermaid/chunk-CLS4B6BI-BRLlisnG.js +168 -0
  20. package/lib/mermaid/chunk-DZP67EKU-9RpG5Rdc.js +70 -0
  21. package/lib/mermaid/chunk-EICP7K5X-8YS1Gtlr.js +1 -0
  22. package/lib/mermaid/chunk-GDY2UAB4-QtXfmDIW.js +1 -0
  23. package/lib/mermaid/chunk-H7VHZCWX-CwFRFlEn.js +1 -0
  24. package/lib/mermaid/chunk-IJBDOHL6-CgcssvZb.js +1 -0
  25. package/lib/mermaid/chunk-JFFVMJIA-Cx1_ZSMR.js +1 -0
  26. package/lib/mermaid/chunk-JQRUD6KW-Dbva2Z17.js +1 -0
  27. package/lib/mermaid/chunk-JZGDI5JK-BspJQ1vQ.js +1 -0
  28. package/lib/mermaid/chunk-KD6SPIPN-4bKl37hM.js +2 -0
  29. package/lib/mermaid/chunk-NF366YI6-D1lnbapv.js +231 -0
  30. package/lib/mermaid/chunk-NUDPKBEF-CL0e9XiQ.js +34 -0
  31. package/lib/mermaid/chunk-OJINMHTS-rKnCIFV0.js +1 -0
  32. package/lib/mermaid/chunk-Q6NGPIAH-CM9NYxWM.js +2 -0
  33. package/lib/mermaid/chunk-QJSWEUOL-kvKBktmc.js +133 -0
  34. package/lib/mermaid/chunk-R7JDMUKV-CSyCOfYj.js +1 -0
  35. package/lib/mermaid/chunk-RCXAM2GK-BeKcutEV.js +1 -0
  36. package/lib/mermaid/chunk-RT4MAWR3-Cv_8LyN5.js +1 -0
  37. package/lib/mermaid/chunk-SFMINX4W-BW2NSwFJ.js +1 -0
  38. package/lib/mermaid/chunk-SLDQ463B-D15xTdNW.js +88 -0
  39. package/lib/mermaid/chunk-STOV2HOB-GnExfzIX.js +1 -0
  40. package/lib/mermaid/chunk-T5OCTHI4-DdZvN-9i.js +1 -0
  41. package/lib/mermaid/chunk-TCEHX7R3-BXjgPiQF.js +1 -0
  42. package/lib/mermaid/chunk-VQC2EELJ-sGo2yn97.js +1 -0
  43. package/lib/mermaid/chunk-VRZKL3EH-BzaZ6KlD.js +1 -0
  44. package/lib/mermaid/chunk-VTHDQ7BV-BqUEu5Ml.js +1 -0
  45. package/lib/mermaid/chunk-VY2OSTJR-BYk2nUI_.js +2 -0
  46. package/lib/mermaid/chunk-WUBWJARI-Z8MI6quX.js +1 -0
  47. package/lib/mermaid/chunk-YBMQ5JGJ-DlZ1nCKo.js +2 -0
  48. package/lib/mermaid/chunk-YDMLL4PJ-BHgx6nXf.js +1 -0
  49. package/lib/mermaid/chunk-Z6V44ARX-CWGOZvke.js +62 -0
  50. package/lib/mermaid/chunk-ZJJ7VQJP-CZ7qJzqZ.js +340 -0
  51. package/lib/mermaid/chunk-ZNLK7HRR-B-wIfXTw.js +1 -0
  52. package/lib/mermaid/chunk-ZQN7UHIL-CNp35E78.js +1 -0
  53. package/lib/mermaid/chunk-ZS6VBONO-D-IFTJgn.js +175 -0
  54. package/lib/mermaid/chunk-ZUNWM646-DRKPDcQl.js +1 -0
  55. package/lib/mermaid/classDiagram-FGAMPOII-D1sf64vv.js +1 -0
  56. package/lib/mermaid/classDiagram-v2-AI5RJEK4-D1sf64vv.js +1 -0
  57. package/lib/mermaid/cose-bilkent-Q72C2OJ3-iX90typn.js +1 -0
  58. package/lib/mermaid/cynefin-OW5HDTMX-6OCTWOGG-6jDAiaRu.js +1 -0
  59. package/lib/mermaid/cynefinDiagram-2OOWUY4M-5ACKe62t.js +62 -0
  60. package/lib/mermaid/dagre-MPVFI544-BUrJjGwd.js +4 -0
  61. package/lib/mermaid/diagram-AE4NWXFW-B3Lkw8MS.js +24 -0
  62. package/lib/mermaid/diagram-BUJID5UG-p2ajLLgr.js +41 -0
  63. package/lib/mermaid/diagram-C44CZ5NI-CO0U1GX-.js +24 -0
  64. package/lib/mermaid/diagram-EYOLEXXS-DDoUndBV.js +30 -0
  65. package/lib/mermaid/diagram-P7B6PWOJ-C_dSNQyG.js +3 -0
  66. package/lib/mermaid/ebnfDiagram-FTPZ3PV6-DIwB5te0.js +1 -0
  67. package/lib/mermaid/erDiagram-H3ERR26V-CN4pCzIq.js +99 -0
  68. package/lib/mermaid/eventmodeling-NTZA5JFV-WKSWQHFS-Ruta89_N.js +1 -0
  69. package/lib/mermaid/flowDiagram-YHGXBVSY-CrSHzFwR.js +1 -0
  70. package/lib/mermaid/ganttDiagram-RWFTB7ED-C-rfEOn5.js +292 -0
  71. package/lib/mermaid/gitGraph-4MIJSDKK-JH2HKYUO-B76vihW5.js +1 -0
  72. package/lib/mermaid/gitGraphDiagram-MAR6GKOP-peAd7EYO.js +106 -0
  73. package/lib/mermaid/index.js +15 -0
  74. package/lib/mermaid/info-A6RAGUB7-3TX3OFWW-DykwOJ0Z.js +1 -0
  75. package/lib/mermaid/infoDiagram-LV547AFH-C46dOKDG.js +2 -0
  76. package/lib/mermaid/ishikawaDiagram-PUJQGNDS-DUmHXvO9.js +70 -0
  77. package/lib/mermaid/journeyDiagram-EW5QJT4W-CNAhUHsQ.js +139 -0
  78. package/lib/mermaid/kanban-definition-JSQERAZB-uOEoksHt.js +89 -0
  79. package/lib/mermaid/katex-CZ4GXH2S-CaEE-8Xf.js +257 -0
  80. package/lib/mermaid/mindmap-definition-VBJCLQLM-DZceCWFq.js +96 -0
  81. package/lib/mermaid/packet-AYTQ26CC-MKII6P3Y-XqJQX7OO.js +1 -0
  82. package/lib/mermaid/pegDiagram-7WILOQTJ-DMKA8PUf.js +1 -0
  83. package/lib/mermaid/pie-WAS4IAKB-6B4OPMJ5-DpoRuAye.js +1 -0
  84. package/lib/mermaid/pieDiagram-EFA7CBSB-BzO4xw8C.js +39 -0
  85. package/lib/mermaid/quadrantDiagram-WARVYA7P-DMQd2xKZ.js +7 -0
  86. package/lib/mermaid/radar-RG4KPBEZ-UPTR3XUL-BcwE6tO1.js +1 -0
  87. package/lib/mermaid/railroad-74A4TZTK-MEFUPFMS-B2Kwx3Uk.js +1 -0
  88. package/lib/mermaid/railroad-abnf-HS5TGJTU-R5YG2S5D-uBY3fYzG.js +1 -0
  89. package/lib/mermaid/railroad-ebnf-LZEXJU2U-G5QEX2L6-AhpzdU_3.js +1 -0
  90. package/lib/mermaid/railroad-peg-WCYAUIDC-H5I2ZRY2-CrsmP7-8.js +1 -0
  91. package/lib/mermaid/railroadDiagram-5NIQVNLQ-BGyZ628g.js +1 -0
  92. package/lib/mermaid/requirementDiagram-SXREGOKZ-CtCO7u9U.js +84 -0
  93. package/lib/mermaid/sankeyDiagram-VV74M2VP-CPXqmSKK.js +40 -0
  94. package/lib/mermaid/sequenceDiagram-52ZFIFDL-B1rSqWZp.js +162 -0
  95. package/lib/mermaid/sizeCapture-FGA5AGYP-BEqQzsxs.js +1 -0
  96. package/lib/mermaid/stateDiagram-3QAKPIVI-3KzWPUq3.js +1 -0
  97. package/lib/mermaid/stateDiagram-v2-HN2P4RDZ-dXASbYMy.js +1 -0
  98. package/lib/mermaid/swimlanes-QLVP42QD-CLpR90IY.js +1 -0
  99. package/lib/mermaid/swimlanesDiagram-JKPDBFCN-C2sgVGQE.js +8 -0
  100. package/lib/mermaid/timeline-definition-GS45XAXW-DLn0_RV5.js +120 -0
  101. package/lib/mermaid/treeView-Q6P3EWNA-ZRJGANNQ-u1bD1sZ1.js +1 -0
  102. package/lib/mermaid/treemap-WGGIJYW6-K64VN5AS-DrIaTBlj.js +1 -0
  103. package/lib/mermaid/vennDiagram-QIQU2ZRJ-FxbUp7-Z.js +34 -0
  104. package/lib/mermaid/wardley-WFR3VGLG-D67OWCCH-Br9hyuFK.js +1 -0
  105. package/lib/mermaid/wardleyDiagram-U6YNM66N-l-Kvr0Az.js +78 -0
  106. package/lib/mermaid/xychartDiagram-XJAZEDNM-O9bztWNh.js +7 -0
  107. package/lib/plugins.mjs +213 -162
  108. package/package.json +3 -10
  109. package/sources/parts/document/document.part.html +7 -7
  110. package/sources/parts/header/header.part.html +2 -2
  111. package/sources/parts/header/header.style.css +4 -2
  112. package/sources/parts/mermaid/mermaid.part.html +1 -1
  113. package/sources/parts/pagefind/pagefind.part.html +5 -3
  114. package/sources/parts/sidebar/sidebar.part.html +1 -1
  115. package/sources/parts/sidebar/sidebar.style.css +10 -0
  116. package/sources/parts/table-of-content/table-of-content-desktop.part.html +2 -2
  117. package/sources/scripts/mermaid.ts +3 -1
package/lib/plugins.mjs CHANGED
@@ -5,13 +5,12 @@ import { ExpressiveCodeBlock } from "expressive-code";
5
5
  import { toHtml } from "hast-util-to-html";
6
6
  import rehypeExpressiveCode, { createRenderer } from "rehype-expressive-code";
7
7
  import { createHash } from "node:crypto";
8
- import { cpSync, existsSync, globSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
8
+ import { cpSync, existsSync, globSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
10
  import yaml from "yaml";
11
11
  import { mkdir, readFile, writeFile } from "node:fs/promises";
12
12
  import { visit } from "unist-util-visit";
13
13
  import * as pagefind from "pagefind";
14
- import { selectAll } from "hast-util-select";
15
14
  //#region plugins/ec-cache.ts
16
15
  /**
17
16
  * Persistent cross-build caches for expressive-code rendering.
@@ -119,7 +118,7 @@ function cachedRehypeExpressiveCode(options) {
119
118
  * replaces the tag with a syntax highlighted code block. Use the `lang` attribute to set the language, and the other attributes
120
119
  * for the rest of the expressive-code metadata.
121
120
  */
122
- function HtmlCodeBlockPlugin(options = {}) {
121
+ function htmlCodeBlockPlugin(options = {}) {
123
122
  const tag = options.tagName ?? "code-block";
124
123
  let renderCache;
125
124
  let diskCache;
@@ -355,11 +354,40 @@ function HtmlCodeBlockPlugin(options = {}) {
355
354
  function isItem(item) {
356
355
  return "filePath" in item;
357
356
  }
358
- function normalizeTitle(string) {
357
+ /** Words `normalizeTitle` writes as listed, matched against the whole word. */
358
+ const ACRONYMS = [
359
+ "API",
360
+ "CDN",
361
+ "CLI",
362
+ "CSS",
363
+ "DOM",
364
+ "FAQ",
365
+ "HTML",
366
+ "HTTP",
367
+ "IIFE",
368
+ "JS",
369
+ "JSON",
370
+ "JSX",
371
+ "LSP",
372
+ "PWA",
373
+ "RSS",
374
+ "SEO",
375
+ "SVG",
376
+ "SVGO",
377
+ "TS",
378
+ "UI",
379
+ "URL",
380
+ "XML",
381
+ "YAML"
382
+ ];
383
+ /** Turns a file or directory name into a title: separators to spaces, every word capitalized, listed words as listed. */
384
+ function normalizeTitle(string, acronyms = ACRONYMS) {
359
385
  if (!string) return "";
360
- string = string.replace(/[-_]/g, " ");
361
- string = string.split(/\s+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join(" ");
362
- return string;
386
+ const fixed = new Map(acronyms.map((acronym) => [acronym.toLowerCase(), acronym]));
387
+ return string.replace(/[-_]/g, " ").split(/\s+/).map((word) => {
388
+ const lower = word.toLowerCase();
389
+ return fixed.get(lower) ?? word.charAt(0).toUpperCase() + lower.slice(1);
390
+ }).join(" ");
363
391
  }
364
392
  /** Escapes characters that would break out of a double-quoted HTML attribute value. */
365
393
  function escapeAttributeValue(value) {
@@ -409,6 +437,10 @@ function withInferredOrder(entries) {
409
437
  }
410
438
  return customizations;
411
439
  }
440
+ /** Whether a front matter key is read by the plugin itself and so must not reach the layout tag. */
441
+ function isConsumedKey(key) {
442
+ return key === "publish" || key.startsWith("sidebar.");
443
+ }
412
444
  function prepareOptions(options) {
413
445
  const include = options.include ?? ["pages/**/*.{html,md}"];
414
446
  const exclude = options.exclude ?? [];
@@ -417,11 +449,8 @@ function prepareOptions(options) {
417
449
  const icons = options.icons ?? "";
418
450
  const publish = options.publish ?? true;
419
451
  const collapsed = options.collapsed ?? true;
420
- const sidebarLayout = options.sidebarLayout ?? {};
421
- sidebarLayout.path ??= themeSidebarPart("sidebar.part.html");
422
- sidebarLayout.tag ??= path.basename(sidebarLayout.path).split(".")[1];
423
- sidebarLayout.sourceAttribute ??= "src";
424
- sidebarLayout.slotName ??= "sidebar";
452
+ const acronyms = [...ACRONYMS, ...options.acronyms ?? []];
453
+ const data = Object.fromEntries(Object.entries(options.data ?? {}).filter(([key]) => key !== "layout" && !isConsumedKey(key)));
425
454
  const directoryLayout = options.directoryLayout ?? {};
426
455
  directoryLayout.path ??= themeSidebarPart("directory.part.html");
427
456
  directoryLayout.tag ??= path.basename(directoryLayout.path).split(".")[1];
@@ -439,7 +468,8 @@ function prepareOptions(options) {
439
468
  icons,
440
469
  publish,
441
470
  collapsed,
442
- sidebarLayout,
471
+ acronyms,
472
+ data,
443
473
  directoryLayout,
444
474
  linkLayout
445
475
  };
@@ -500,51 +530,6 @@ function parseSidebarFrontmatter(file) {
500
530
  };
501
531
  }
502
532
  //#endregion
503
- //#region plugins/content-plugin/render-tree.ts
504
- /** `<icon>` source for a node, `undefined` when the project has no icons directory or the node no icon. */
505
- function iconSource(icons, icon) {
506
- if (!icons || !icon) return;
507
- return `${icons.replace(/\/$/, "")}/${icon}.icon.html`;
508
- }
509
- function renderDirectory(directory, items, { directoryLayout, customizations, icons }) {
510
- const customization = customizations[directory.realPath];
511
- const data = {
512
- title: customization?.label ?? directory.title,
513
- collapsed: customization?.collapsed ?? directory.collapsed,
514
- ...customization?.data,
515
- iconSrc: iconSource(icons, customization?.data?.icon)
516
- };
517
- return `<li class="sidebar-list-item">
518
- <${directoryLayout.tag} dataJson='${escapeAttributeValue(JSON.stringify(data))}' ${directoryLayout.sourceAttribute}="${directoryLayout.path}">
519
- <ul class="sidebar-list">${items}</ul>
520
- </${directoryLayout.tag}>
521
- </li>`;
522
- }
523
- function renderLink(link, { linkLayout, customizations, icons }) {
524
- const customization = customizations[link.realPath];
525
- const data = {
526
- title: customization?.label ?? link.title,
527
- fullTitle: link.pageTitle ?? link.title,
528
- href: encodeURIComponent(link.filePath),
529
- ...customization?.data,
530
- iconSrc: iconSource(icons, customization?.data?.icon)
531
- };
532
- return `<li class="sidebar-list-item">
533
- <${linkLayout.tag} dataJson='${escapeAttributeValue(JSON.stringify(data))}' ${linkLayout.sourceAttribute}="${linkLayout.path}"></${linkLayout.tag}>
534
- </li>`;
535
- }
536
- function renderTree(items, options) {
537
- let tree = "";
538
- for (const item of items) {
539
- if (isItem(item)) {
540
- tree += renderLink(item, options);
541
- continue;
542
- }
543
- tree += renderDirectory(item, renderTree(item.items, options), options);
544
- }
545
- return tree;
546
- }
547
- //#endregion
548
533
  //#region plugins/content-plugin/generate-tree.ts
549
534
  function getOrCreateDirectory(tree, title, realPath, collapsed) {
550
535
  let directory = tree.find((entry) => "items" in entry && entry.title === title);
@@ -604,13 +589,13 @@ function generateTree(options) {
604
589
  let realDirectoryPath = parts.slice(0, baseIndex + 1).join("/");
605
590
  for (const directory of directories) {
606
591
  realDirectoryPath = path.join(realDirectoryPath, directory);
607
- const title = normalizeTitle(directory);
592
+ const title = normalizeTitle(directory, options.acronyms);
608
593
  currentTree = getOrCreateDirectory(currentTree, title, realDirectoryPath, options.collapsed).items;
609
594
  }
610
595
  const fileNameWithoutExtension = path.parse(fileName).name;
611
- const title = fileNameWithoutExtension === "index" && directories.length === 0 ? "Home" : normalizeTitle(fileNameWithoutExtension);
596
+ const title = fileNameWithoutExtension === "index" && directories.length === 0 ? "Home" : normalizeTitle(fileNameWithoutExtension, options.acronyms);
612
597
  const pathAsLink = fileNameWithoutExtension === "index" ? path.dirname(file) : path.join(path.dirname(file), fileNameWithoutExtension);
613
- const href = path.relative(path.dirname(options.sidebarLayout.path), pathAsLink) + "/";
598
+ const href = this.resolver.aliasPath(pathAsLink + "/");
614
599
  currentTree.push({
615
600
  title,
616
601
  filePath: href,
@@ -620,48 +605,126 @@ function generateTree(options) {
620
605
  }
621
606
  applyOrdering(autoGenerated, options.customizations);
622
607
  return {
623
- tree: `<ul class="sidebar-list" slot="${options.sidebarLayout.slotName}">${renderTree(autoGenerated, options)}</ul>`,
608
+ tree: autoGenerated,
624
609
  notPublished
625
610
  };
626
611
  }
627
612
  //#endregion
613
+ //#region plugins/content-plugin/render-tree.ts
614
+ /** `<icon>` source for a node, `undefined` when the project has no icons directory or the node no icon. */
615
+ function iconSource(icons, icon) {
616
+ if (!icons || !icon) return;
617
+ return `${icons.replace(/\/$/, "")}/${icon}.icon.html`;
618
+ }
619
+ function renderDirectory(directory, items, { directoryLayout, customizations, icons }) {
620
+ const customization = customizations[directory.realPath];
621
+ const data = {
622
+ title: customization?.label ?? directory.title,
623
+ collapsed: customization?.collapsed ?? directory.collapsed,
624
+ ...customization?.data,
625
+ iconSrc: iconSource(icons, customization?.data?.icon)
626
+ };
627
+ return `<li class="sidebar-list-item">
628
+ <${directoryLayout.tag} dataJson='${escapeAttributeValue(JSON.stringify(data))}' ${directoryLayout.sourceAttribute}="${directoryLayout.path}">
629
+ <ul class="sidebar-list">${items}</ul>
630
+ </${directoryLayout.tag}>
631
+ </li>`;
632
+ }
633
+ function renderLink(link, { linkLayout, customizations, icons }) {
634
+ const customization = customizations[link.realPath];
635
+ const data = {
636
+ title: customization?.label ?? link.title,
637
+ fullTitle: link.pageTitle ?? link.title,
638
+ href: encodeURIComponent(link.filePath),
639
+ ...customization?.data,
640
+ iconSrc: iconSource(icons, customization?.data?.icon)
641
+ };
642
+ return `<li class="sidebar-list-item">
643
+ <${linkLayout.tag} dataJson='${escapeAttributeValue(JSON.stringify(data))}' ${linkLayout.sourceAttribute}="${linkLayout.path}"></${linkLayout.tag}>
644
+ </li>`;
645
+ }
646
+ function renderTree(items, options) {
647
+ let tree = "";
648
+ for (const item of items) {
649
+ if (isItem(item)) {
650
+ tree += renderLink(item, options);
651
+ continue;
652
+ }
653
+ tree += renderDirectory(item, renderTree(item.items, options), options);
654
+ }
655
+ return tree;
656
+ }
657
+ //#endregion
628
658
  //#region plugins/content-plugin/content-plugin.ts
629
- /**
630
- * Turns an alias, a package specifier, or an absolute path into the path the build works with: relative to the project root when
631
- * the file lives inside it, absolute otherwise.
632
- */
659
+ /** Turns an alias, a package specifier, or an absolute path into a path relative to the project root. */
633
660
  function locate(source) {
634
661
  const aliased = this.resolver.resolveAlias(source);
635
662
  if (aliased) return aliased;
636
663
  const resolved = path.isAbsolute(source) ? source : this.resolver.resolve(source, path.join(this.root, "index.html"))?.path;
637
- if (!resolved) return source;
638
- const relative = path.relative(this.root, resolved);
639
- return relative.startsWith("..") ? resolved : relative;
664
+ return resolved ? path.relative(this.root, resolved) : source;
665
+ }
666
+ /** The layout's empty `<slot name="sidebar">`, with the attributes around the name captured for the `slot` it forwards to. */
667
+ const slotRegex = /<slot\b([^>]*)\bname\s*=\s*["']sidebar["']([^>]*?)(\/>|>\s*<\/slot\s*>)/g;
668
+ /** The tree in place of the slot, sent to the same slot of the part below when the slot was forwarding to one. */
669
+ function fillSlot(content, tree) {
670
+ return content.replace(slotRegex, (_, before, after) => {
671
+ const target = /\bslot\s*=\s*["']([^"']*)["']/.exec(`${before} ${after}`)?.[1];
672
+ return `<ul class="sidebar-list"${target ? ` slot="${target}"` : ""}>${tree}</ul>`;
673
+ });
640
674
  }
641
675
  function contentPlugin(options) {
642
676
  const prepared = prepareOptions(options);
643
- const slotRegex = new RegExp(String.raw`<slot\s+name\s*=\s*['"]sidebar['"]\s*(\/>|>\s*<\/\s*slot\s*>)`, "g");
644
677
  let tree = "";
645
678
  let notPublished = /* @__PURE__ */ new Set();
646
- let sidebarRealPath = "";
679
+ /**
680
+ * The layouts this instance serves through `read`, each the file it is read from with the tree in its `sidebar` slot.
681
+ *
682
+ * @key served path, relative to the root
683
+ * @value the layout file it is a copy of
684
+ */
685
+ const served = /* @__PURE__ */ new Map();
686
+ const isInScope = (relativePath, root) => {
687
+ return path.isPathMatch(relativePath, {
688
+ include: prepared.include,
689
+ ignore: prepared.exclude,
690
+ root
691
+ });
692
+ };
693
+ const generate = (app) => {
694
+ const result = generateTree.call(app, prepared);
695
+ return {
696
+ tree: renderTree(result.tree, prepared),
697
+ notPublished: result.notPublished
698
+ };
699
+ };
647
700
  return {
648
701
  name: "content-plugin",
649
702
  setup() {
650
- for (const key in prepared.layouts) prepared.layouts[key] = locate.call(this, prepared.layouts[key]);
651
- prepared.sidebarLayout.path = locate.call(this, prepared.sidebarLayout.path);
652
- sidebarRealPath = realpathSync(path.resolve(this.root, prepared.sidebarLayout.path));
653
- const sidebarDirectory = path.dirname(path.resolve(this.root, prepared.sidebarLayout.path));
654
- const directoryLayoutPath = path.resolve(this.root, locate.call(this, prepared.directoryLayout.path));
655
- prepared.directoryLayout.path = path.relative(sidebarDirectory, directoryLayoutPath);
656
- const linkLayoutPath = path.resolve(this.root, locate.call(this, prepared.linkLayout.path));
657
- prepared.linkLayout.path = path.relative(sidebarDirectory, linkLayoutPath);
703
+ const instance = (this.pluginData.contentPluginInstances ?? 0) + 1;
704
+ this.pluginData.contentPluginInstances = instance;
705
+ for (const key in prepared.layouts) {
706
+ const original = locate.call(this, prepared.layouts[key]);
707
+ if (instance === 1) {
708
+ served.set(original, original);
709
+ continue;
710
+ }
711
+ const [name, ...rest] = path.basename(original).split(".");
712
+ const copy = path.join(path.dirname(original), [
713
+ name,
714
+ `content-${instance}`,
715
+ ...rest
716
+ ].join("."));
717
+ served.set(copy, original);
718
+ this.resolver.files.add(path.resolve(this.root, copy));
719
+ prepared.layouts[key] = copy;
720
+ }
658
721
  for (const pathKey in prepared.customizations) {
659
722
  const resolvedPath = this.resolver.resolveAlias(pathKey);
660
723
  if (!resolvedPath) continue;
661
724
  prepared.customizations[resolvedPath] = prepared.customizations[pathKey];
662
725
  delete prepared.customizations[pathKey];
663
726
  }
664
- ({tree, notPublished} = generateTree.call(this, prepared));
727
+ ({tree, notPublished} = generate(this));
665
728
  for (const file of notPublished) {
666
729
  this.entryPoints.delete(file);
667
730
  this.entryPoints.delete(`./${file}`);
@@ -672,37 +735,31 @@ function contentPlugin(options) {
672
735
  },
673
736
  read(absolutePath) {
674
737
  const relativePath = path.relative(this.root, absolutePath);
675
- if (path.basename(absolutePath) === path.basename(sidebarRealPath) && realpathSync(absolutePath) === sidebarRealPath) return readFileSync(absolutePath, "utf8").replace(slotRegex, tree);
676
- if (path.extname(relativePath) !== ".md") return;
677
- if (!path.isPathMatch(relativePath, {
678
- include: prepared.include,
679
- ignore: prepared.exclude,
680
- root: this.root
681
- })) return;
738
+ const original = served.get(relativePath);
739
+ if (original) return fillSlot(readFileSync(path.resolve(this.root, original), "utf8"), tree);
740
+ if (path.extname(relativePath) !== ".md" || !isInScope(relativePath, this.root)) return;
682
741
  const pageContent = readFileSync(absolutePath, "utf8");
683
742
  const frontMatterBlock = pageContent.match(/^---\n(.*?)\n---/s);
684
743
  const frontMatter = {
685
744
  layout: "default",
745
+ ...prepared.data,
686
746
  ...frontMatterBlock ? yaml.parse(frontMatterBlock[1]) : {}
687
747
  };
688
- const layoutPath = prepared.layouts[frontMatter.layout] ?? prepared.layouts.default;
689
- frontMatter.layout = path.relative(path.dirname(relativePath), layoutPath);
690
- for (const key in frontMatter) if (key === "publish" || key.startsWith("sidebar.")) delete frontMatter[key];
748
+ const layout = prepared.layouts[frontMatter.layout] ?? prepared.layouts.default;
749
+ frontMatter.layout = served.has(layout) ? path.relative(path.dirname(absolutePath), path.resolve(this.root, layout)) : layout;
750
+ for (const key in frontMatter) if (isConsumedKey(key)) delete frontMatter[key];
691
751
  const body = frontMatterBlock ? pageContent.slice(frontMatterBlock[0].length) : pageContent;
692
752
  return `---\n${yaml.stringify(frontMatter)}\n---\n${body}`;
693
753
  },
694
754
  onFileEvent(_, id) {
695
- if (!path.isPathMatch(id, {
696
- include: prepared.include,
697
- ignore: prepared.exclude,
698
- root: this.root
699
- })) return;
700
- const result = generateTree.call(this, prepared);
755
+ for (const [copy, original] of served) if (original === id && copy !== id) this.watcher?.emit("change", copy);
756
+ if (!isInScope(id, this.root)) return;
757
+ const result = generate(this);
701
758
  notPublished = result.notPublished;
702
759
  if (tree === result.tree) return;
703
760
  this.log.info("Re-generating sidebar tree");
704
761
  tree = result.tree;
705
- this.watcher?.emit("change", prepared.sidebarLayout.path);
762
+ for (const layout of served.keys()) this.watcher?.emit("change", layout);
706
763
  }
707
764
  };
708
765
  }
@@ -748,6 +805,7 @@ const leadingFrontmatter = /^---\r?\n.*?\r?\n---\r?\n/s;
748
805
  const leadingHtmlComments = /^(?:\s*<!--.*?-->\s*)+/s;
749
806
  const fenceDelimiter = /^ {0,3}(`{3,}|~{3,})/;
750
807
  const markdownLink = /\[([^\]]+)\]\([^)]*\)/g;
808
+ const markdownTag = /<markdown\s+src\s*=\s*["']([^"']+)["']\s*>\s*<\/markdown\s*>/g;
751
809
  /**
752
810
  * Writes `llms.txt` and `llms-full.txt`, plus a copy of every page's markdown beside its HTML.
753
811
  *
@@ -755,14 +813,15 @@ const markdownLink = /\[([^\]]+)\]\([^)]*\)/g;
755
813
  * needs no link-following. Nothing is curated by hand.
756
814
  */
757
815
  function llmsPlugin(options = {}) {
758
- const { siteUrl, title = "Documentation", summary } = options;
816
+ const { siteUrl, title = "Documentation", summary, include = ["pages/**/*.md"], exclude = [], output = "llms" } = options;
759
817
  return {
760
818
  name: "llms",
761
819
  async postBuild() {
762
820
  if (!siteUrl) return;
763
821
  const origin = siteUrl.replace(/\/+$/, "");
764
822
  const isBuilt = (file) => Boolean(this.findMetadata({ id: file }) ?? this.findMetadata({ id: `./${file}` }));
765
- const entries = await collectPages(this.root, isBuilt);
823
+ const resolveAlias = (source) => this.resolver.resolveAlias(source);
824
+ const entries = await collectPages(this.root, isBuilt, resolveAlias, include, exclude);
766
825
  if (entries.length === 0) return;
767
826
  for (const entry of entries) {
768
827
  const target = join(this.outdir, entry.route, "index.md");
@@ -787,27 +846,33 @@ function llmsPlugin(options = {}) {
787
846
  manifest.push(`- [${entry.title}](${url})${entry.description ? `: ${entry.description}` : ""}`);
788
847
  }
789
848
  }
790
- await writeFile(join(this.outdir, "llms.txt"), manifest.join("\n") + "\n", "utf8");
849
+ const manifestFile = `${output}.txt`;
850
+ const fullFile = `${output}-full.txt`;
851
+ await mkdir(dirname(join(this.outdir, manifestFile)), { recursive: true });
852
+ await writeFile(join(this.outdir, manifestFile), manifest.join("\n") + "\n", "utf8");
791
853
  const full = [[
792
854
  ...heading,
793
855
  "",
794
- `> Every document from ${origin}/llms.txt inlined in full, so no links need following.`
856
+ `> Every document from ${origin}/${manifestFile} inlined in full, so no links need following.`
795
857
  ].join("\n"), ...entries.map((entry) => {
796
858
  const source = `${origin}/${entry.route}${entry.route ? "/" : ""}index.md`;
797
859
  return `${demoteHeadings(entry.document, entry.title)}\n\nSource: ${source}`;
798
860
  })].join("\n\n---\n\n") + "\n";
799
- await writeFile(join(this.outdir, "llms-full.txt"), rewriteAliases(full, origin), "utf8");
861
+ await writeFile(join(this.outdir, fullFile), rewriteAliases(full, origin), "utf8");
800
862
  const kilobytes = Math.round(Buffer.byteLength(full) / 1024);
801
- this.log.success(`Wrote llms.txt (${entries.length} links) and llms-full.txt (${kilobytes} KB).`);
863
+ this.log.success(`Wrote ${manifestFile} (${entries.length} links) and ${fullFile} (${kilobytes} KB).`);
802
864
  }
803
865
  };
804
866
  }
805
867
  /** Pages the build produced, so anything the markdown plugin dropped (`publish: false`) stays out of the llms files too. */
806
- async function collectPages(root, isBuilt) {
807
- const files = globSync("pages/**/*.md", { cwd: root }).map((file) => file.replaceAll("\\", "/")).filter((file) => isBuilt(file)).toSorted(byDepthThenPath);
868
+ async function collectPages(root, isBuilt, resolveAlias, include, exclude) {
869
+ const files = globSync(include, {
870
+ cwd: root,
871
+ exclude
872
+ }).map((file) => file.replaceAll("\\", "/")).filter((file) => isBuilt(file)).toSorted(byDepthThenPath);
808
873
  const entries = [];
809
874
  for (const file of files) {
810
- const document = clean(await readFile(join(root, file), "utf8"));
875
+ const document = await inlineMarkdownTags(clean(await readFile(join(root, file), "utf8")), file, root, resolveAlias);
811
876
  const route = file.replace(/^pages\//, "").replace(/\.md$/, "").replace(/(^|\/)index$/, "");
812
877
  entries.push({
813
878
  title: firstHeading(document) ?? normalizeTitle(route.split("/").pop() ?? "Home"),
@@ -818,6 +883,22 @@ async function collectPages(root, isBuilt) {
818
883
  }
819
884
  return entries;
820
885
  }
886
+ /**
887
+ * Replaces every `<markdown src>` tag with the file it points at, so a page that only embeds another document reads as that
888
+ * document. Aliases resolve through the app, other paths from the page's directory. A tag whose file cannot be read stays as is.
889
+ */
890
+ async function inlineMarkdownTags(document, file, root, resolveAlias) {
891
+ for (const [tag, source] of document.matchAll(markdownTag)) {
892
+ const resolved = resolveAlias(source) ?? join(dirname(file), source);
893
+ try {
894
+ const content = clean(await readFile(join(root, resolved), "utf8"));
895
+ document = document.replace(tag, () => content);
896
+ } catch {
897
+ continue;
898
+ }
899
+ }
900
+ return document;
901
+ }
821
902
  /** Groups by the first path segment, so the sections mirror the sidebar. Root pages come first, under "Overview". */
822
903
  function groupByFolder(entries) {
823
904
  const sections = /* @__PURE__ */ new Map();
@@ -890,6 +971,13 @@ function byDepthThenPath(a, b) {
890
971
  }
891
972
  //#endregion
892
973
  //#region plugins/mermaid-fence.ts
974
+ /** The sole `<code>` child of a `<pre>`, ignoring the whitespace some serialisers insert, or nothing when the shape differs. */
975
+ function onlyCodeChild(node) {
976
+ const children = node.children.filter((child) => !(child.type === "text" && child.value.trim() === ""));
977
+ const [child] = children;
978
+ if (children.length !== 1 || child.type !== "element" || child.tagName !== "code") return;
979
+ return child;
980
+ }
893
981
  /**
894
982
  * Rehype plugin: converts fenced mermaid code blocks into <pre class="mermaid"> elements.
895
983
  *
@@ -900,60 +988,21 @@ function byDepthThenPath(a, b) {
900
988
  const rehypeMermaidFence = () => {
901
989
  return (tree) => {
902
990
  visit(tree, "element", (node) => {
903
- if (!isPreElement(node)) return;
904
- const codeElement = findSingleCodeChild(node);
905
- if (codeElement === null) return;
906
- if (!hasMermaidLanguageClass(codeElement)) return;
907
- const mermaidSource = extractTextContent(codeElement);
908
- if (mermaidSource === null) return;
991
+ if (node.tagName !== "pre") return;
992
+ const code = onlyCodeChild(node);
993
+ if (!code) return;
994
+ const className = code.properties?.className;
995
+ if (!(Array.isArray(className) ? className : [className]).some((name) => String(name) === "language-mermaid")) return;
996
+ const text = code.children.filter((child) => child.type === "text");
997
+ if (text.length === 0) return;
909
998
  node.properties = { className: ["mermaid"] };
910
999
  node.children = [{
911
1000
  type: "text",
912
- value: mermaidSource
1001
+ value: text.map((child) => child.value).join("")
913
1002
  }];
914
1003
  });
915
1004
  };
916
1005
  };
917
- function isPreElement(node) {
918
- return node.tagName === "pre";
919
- }
920
- /** Returns the sole <code> child of a <pre>, or null if the structure doesn't match (e.g. multiple children, wrong tag). */
921
- function findSingleCodeChild(preNode) {
922
- const nonWhitespaceChildren = [];
923
- for (const child of preNode.children) {
924
- if (child.type === "text" && child.value.trim() === "") continue;
925
- nonWhitespaceChildren.push(child);
926
- }
927
- if (nonWhitespaceChildren.length !== 1) return null;
928
- const [onlyChild] = nonWhitespaceChildren;
929
- if (onlyChild.type !== "element") return null;
930
- if (onlyChild.tagName !== "code") return null;
931
- return onlyChild;
932
- }
933
- /**
934
- * Returns true when the element's className list contains "language-mermaid". Handles both string and array forms that hast
935
- * allows.
936
- */
937
- function hasMermaidLanguageClass(codeElement) {
938
- const rawClassName = codeElement.properties?.className;
939
- if (rawClassName === void 0 || rawClassName === null) return false;
940
- const classNames = Array.isArray(rawClassName) ? rawClassName : [String(rawClassName)];
941
- for (const className of classNames) if (className === "language-mermaid") return true;
942
- return false;
943
- }
944
- /**
945
- * Concatenates all text-node descendants of an element. Returns null when there are no text nodes (empty code block is valid →
946
- * ""). A <code> block produced by remark-rehype always has exactly one Text child, but we handle edge cases defensively.
947
- */
948
- function extractTextContent(element) {
949
- let content = "";
950
- let found = false;
951
- for (const child of element.children) if (child.type === "text") {
952
- content += child.value;
953
- found = true;
954
- }
955
- return found ? content : null;
956
- }
957
1006
  //#endregion
958
1007
  //#region plugins/obsidian-comments.ts
959
1008
  const commentPattern = /(?<!\\)%%[\s\S]*?(?<!\\)%%/g;
@@ -1072,7 +1121,10 @@ const remarkObsidianTags = () => {
1072
1121
  };
1073
1122
  //#endregion
1074
1123
  //#region plugins/pagefind-plugin.ts
1075
- function pagefindPlugin({ glob = "**/*.html" } = {}) {
1124
+ function pagefindPlugin(options = {}) {
1125
+ const root = options.root ?? ".";
1126
+ const glob = options.glob ?? "**/*.html";
1127
+ const output = options.output ?? join(root, "pagefind");
1076
1128
  return {
1077
1129
  name: "pagefind",
1078
1130
  async postBuild() {
@@ -1081,7 +1133,7 @@ function pagefindPlugin({ glob = "**/*.html" } = {}) {
1081
1133
  const { index } = await pagefind.createIndex();
1082
1134
  if (!index) throw new Error("Failed to create Pagefind index");
1083
1135
  await index.addDirectory({
1084
- path: this.outdir,
1136
+ path: join(this.outdir, root),
1085
1137
  glob
1086
1138
  });
1087
1139
  const { files } = await index.getFiles();
@@ -1096,7 +1148,7 @@ function pagefindPlugin({ glob = "**/*.html" } = {}) {
1096
1148
  ]);
1097
1149
  for (const file of files) {
1098
1150
  if (exclude.has(file.path)) continue;
1099
- const targetPath = join(this.outdir, "pagefind", file.path);
1151
+ const targetPath = join(this.outdir, output, file.path);
1100
1152
  await mkdir(dirname(targetPath), { recursive: true });
1101
1153
  await writeFile(targetPath, file.content, "utf8");
1102
1154
  }
@@ -1108,18 +1160,17 @@ function pagefindPlugin({ glob = "**/*.html" } = {}) {
1108
1160
  //#region plugins/wrap-table.ts
1109
1161
  function wrapTables() {
1110
1162
  return (tree) => {
1111
- const tables = selectAll("table", tree);
1112
- for (const table of tables) visit(tree, table, (node, index, parent) => {
1113
- if (!parent || typeof index !== "number") return;
1114
- const wrapper = {
1163
+ visit(tree, "element", (node, index, parent) => {
1164
+ if (node.tagName !== "table" || !parent || typeof index !== "number") return;
1165
+ parent.children[index] = {
1115
1166
  type: "element",
1116
1167
  tagName: "div",
1117
1168
  properties: { class: "table-wrapper" },
1118
1169
  children: [node]
1119
1170
  };
1120
- parent.children[index] = wrapper;
1171
+ return "skip";
1121
1172
  });
1122
1173
  };
1123
1174
  }
1124
1175
  //#endregion
1125
- export { HtmlCodeBlockPlugin, PersistentCache, cachedRehypeExpressiveCode, contentPlugin, ecCachePlugin, ecCacheSalt, htmlWithTypeScriptScripts, katexFontsPlugin, llmsPlugin, normalizeTitle, pagefindPlugin, pluginCollapsible, rehypeMermaidFence, remarkObsidianComments, remarkObsidianTags, sha1, wrapTables };
1176
+ export { PersistentCache, cachedRehypeExpressiveCode, contentPlugin, ecCachePlugin, ecCacheSalt, htmlCodeBlockPlugin, htmlWithTypeScriptScripts, katexFontsPlugin, llmsPlugin, normalizeTitle, pagefindPlugin, pluginCollapsible, rehypeMermaidFence, remarkObsidianComments, remarkObsidianTags, sha1, wrapTables };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@staticbolt/docs",
3
- "version": "0.0.1-beta.3",
3
+ "version": "0.0.1-beta.5",
4
4
  "description": "Documentation theme for staticbolt: layout parts, styles, and plugins shared by every docs site.",
5
5
  "author": "Ahmed ALABSI",
6
6
  "license": "MIT",
@@ -14,6 +14,7 @@
14
14
  "types": "./lib/ec.config.d.mts",
15
15
  "default": "./lib/ec-config.mjs"
16
16
  },
17
+ "./mermaid": "./lib/mermaid/index.js",
17
18
  "./parts/*": "./sources/parts/*",
18
19
  "./styles/*": "./sources/styles/*",
19
20
  "./scripts/*": "./sources/scripts/*",
@@ -51,13 +52,7 @@
51
52
  ],
52
53
  "peerDependencies": {
53
54
  "@staticbolt/core": "beta",
54
- "@staticview/ui": ">=1.0.0-beta.0",
55
- "mermaid": "^11.15.0"
56
- },
57
- "peerDependenciesMeta": {
58
- "mermaid": {
59
- "optional": true
60
- }
55
+ "@staticview/ui": ">=1.0.0-beta.0"
61
56
  },
62
57
  "dependencies": {
63
58
  "@expressive-code/core": "^0.42.0",
@@ -66,7 +61,6 @@
66
61
  "@pagefind/component-ui": "^1.5.2",
67
62
  "@xt0rted/expressive-code-file-icons": "^1.0.0",
68
63
  "expressive-code": "^0.42.0",
69
- "hast-util-select": "^6.0.2",
70
64
  "hast-util-to-html": "^9.0.5",
71
65
  "pagefind": "^1.5.2",
72
66
  "rehype-expressive-code": "^0.42.0",
@@ -75,7 +69,6 @@
75
69
  "yaml": "^2.9.0"
76
70
  },
77
71
  "overrides": {
78
- "expressive-code": "^0.42.0",
79
72
  "@expressive-code/core": "^0.42.0"
80
73
  },
81
74
  "devDependencies": {