@prudentbird/voxx-core 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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +42 -0
  3. package/dist/_virtual/_rolldown/runtime.cjs +23 -0
  4. package/dist/config.cjs +151 -0
  5. package/dist/config.cjs.map +1 -0
  6. package/dist/config.d.cts +122 -0
  7. package/dist/config.d.cts.map +1 -0
  8. package/dist/config.d.mts +122 -0
  9. package/dist/config.d.mts.map +1 -0
  10. package/dist/config.mjs +149 -0
  11. package/dist/config.mjs.map +1 -0
  12. package/dist/content.cjs +147 -0
  13. package/dist/content.cjs.map +1 -0
  14. package/dist/content.d.cts +41 -0
  15. package/dist/content.d.cts.map +1 -0
  16. package/dist/content.d.mts +41 -0
  17. package/dist/content.d.mts.map +1 -0
  18. package/dist/content.mjs +145 -0
  19. package/dist/content.mjs.map +1 -0
  20. package/dist/dev.cjs +82 -0
  21. package/dist/dev.cjs.map +1 -0
  22. package/dist/dev.d.cts +24 -0
  23. package/dist/dev.d.cts.map +1 -0
  24. package/dist/dev.d.mts +24 -0
  25. package/dist/dev.d.mts.map +1 -0
  26. package/dist/dev.mjs +82 -0
  27. package/dist/dev.mjs.map +1 -0
  28. package/dist/effect.cjs +23 -0
  29. package/dist/effect.d.cts +8 -0
  30. package/dist/effect.d.mts +8 -0
  31. package/dist/effect.mjs +8 -0
  32. package/dist/errors.cjs +20 -0
  33. package/dist/errors.cjs.map +1 -0
  34. package/dist/errors.d.cts +45 -0
  35. package/dist/errors.d.cts.map +1 -0
  36. package/dist/errors.d.mts +45 -0
  37. package/dist/errors.d.mts.map +1 -0
  38. package/dist/errors.mjs +16 -0
  39. package/dist/errors.mjs.map +1 -0
  40. package/dist/feeds.cjs +97 -0
  41. package/dist/feeds.cjs.map +1 -0
  42. package/dist/feeds.d.cts +49 -0
  43. package/dist/feeds.d.cts.map +1 -0
  44. package/dist/feeds.d.mts +49 -0
  45. package/dist/feeds.d.mts.map +1 -0
  46. package/dist/feeds.mjs +94 -0
  47. package/dist/feeds.mjs.map +1 -0
  48. package/dist/frontmatter.cjs +22 -0
  49. package/dist/frontmatter.cjs.map +1 -0
  50. package/dist/frontmatter.d.cts +30 -0
  51. package/dist/frontmatter.d.cts.map +1 -0
  52. package/dist/frontmatter.d.mts +30 -0
  53. package/dist/frontmatter.d.mts.map +1 -0
  54. package/dist/frontmatter.mjs +20 -0
  55. package/dist/frontmatter.mjs.map +1 -0
  56. package/dist/index.cjs +90 -0
  57. package/dist/index.cjs.map +1 -0
  58. package/dist/index.d.cts +50 -0
  59. package/dist/index.d.cts.map +1 -0
  60. package/dist/index.d.mts +50 -0
  61. package/dist/index.d.mts.map +1 -0
  62. package/dist/index.mjs +59 -0
  63. package/dist/index.mjs.map +1 -0
  64. package/dist/llms.cjs +81 -0
  65. package/dist/llms.cjs.map +1 -0
  66. package/dist/llms.d.cts +43 -0
  67. package/dist/llms.d.cts.map +1 -0
  68. package/dist/llms.d.mts +43 -0
  69. package/dist/llms.d.mts.map +1 -0
  70. package/dist/llms.mjs +78 -0
  71. package/dist/llms.mjs.map +1 -0
  72. package/dist/nav.cjs +50 -0
  73. package/dist/nav.cjs.map +1 -0
  74. package/dist/nav.d.cts +16 -0
  75. package/dist/nav.d.cts.map +1 -0
  76. package/dist/nav.d.mts +16 -0
  77. package/dist/nav.d.mts.map +1 -0
  78. package/dist/nav.mjs +50 -0
  79. package/dist/nav.mjs.map +1 -0
  80. package/dist/render.cjs +152 -0
  81. package/dist/render.cjs.map +1 -0
  82. package/dist/render.d.cts +29 -0
  83. package/dist/render.d.cts.map +1 -0
  84. package/dist/render.d.mts +29 -0
  85. package/dist/render.d.mts.map +1 -0
  86. package/dist/render.mjs +143 -0
  87. package/dist/render.mjs.map +1 -0
  88. package/dist/schema.cjs +78 -0
  89. package/dist/schema.cjs.map +1 -0
  90. package/dist/schema.d.cts +93 -0
  91. package/dist/schema.d.cts.map +1 -0
  92. package/dist/schema.d.mts +93 -0
  93. package/dist/schema.d.mts.map +1 -0
  94. package/dist/schema.mjs +77 -0
  95. package/dist/schema.mjs.map +1 -0
  96. package/dist/seo.cjs +77 -0
  97. package/dist/seo.cjs.map +1 -0
  98. package/dist/seo.d.cts +15 -0
  99. package/dist/seo.d.cts.map +1 -0
  100. package/dist/seo.d.mts +15 -0
  101. package/dist/seo.d.mts.map +1 -0
  102. package/dist/seo.mjs +77 -0
  103. package/dist/seo.mjs.map +1 -0
  104. package/dist/types.cjs +45 -0
  105. package/dist/types.cjs.map +1 -0
  106. package/dist/types.d.cts +138 -0
  107. package/dist/types.d.cts.map +1 -0
  108. package/dist/types.d.mts +138 -0
  109. package/dist/types.d.mts.map +1 -0
  110. package/dist/types.mjs +45 -0
  111. package/dist/types.mjs.map +1 -0
  112. package/dist/util.cjs +185 -0
  113. package/dist/util.cjs.map +1 -0
  114. package/dist/util.d.cts +98 -0
  115. package/dist/util.d.cts.map +1 -0
  116. package/dist/util.d.mts +98 -0
  117. package/dist/util.d.mts.map +1 -0
  118. package/dist/util.mjs +171 -0
  119. package/dist/util.mjs.map +1 -0
  120. package/package.json +106 -0
  121. package/theme/demo-globals.css +61 -0
  122. package/theme/voxx.css +915 -0
  123. package/voxx.schema.json +186 -0
package/dist/llms.mjs ADDED
@@ -0,0 +1,78 @@
1
+ import { absoluteUrl } from "./util.mjs";
2
+ //#region src/llms.ts
3
+ const SECTION_HEADING = {
4
+ blog: "Posts",
5
+ docs: "Pages",
6
+ changelog: "Releases"
7
+ };
8
+ const oneLine = (s) => s.replace(/\s*\n\s*/g, " ").trim();
9
+ const escLinkText = (s) => oneLine(s).replace(/([[\]])/g, "\\$1");
10
+ const escHeading = (s) => oneLine(s);
11
+ /**
12
+ * Returns the default section heading for a content type.
13
+ *
14
+ * @param type - Content type from the Voxx config.
15
+ * @returns Human-readable heading, e.g. `"Posts"`, `"Pages"`, `"Releases"`.
16
+ */
17
+ function sectionHeading(type) {
18
+ return SECTION_HEADING[type];
19
+ }
20
+ /**
21
+ * Renders a `llms.txt` file from one or more labeled post sections.
22
+ *
23
+ * @param sections - Ordered list of headings with their associated posts.
24
+ * @param config - Resolved Voxx config (used for site title and description).
25
+ * @returns `llms.txt` Markdown string.
26
+ */
27
+ function renderLlmsTxtSections(sections, config) {
28
+ const lines = [`# ${config.site.title}`, ""];
29
+ if (config.site.description) lines.push(`> ${config.site.description}`, "");
30
+ for (const section of sections) {
31
+ lines.push(`## ${section.heading}`, "");
32
+ for (const p of section.posts) {
33
+ const link = absoluteUrl(config.site.url, p.url);
34
+ const summary = p.description ?? p.excerpt;
35
+ const title = escLinkText(p.title);
36
+ const safeSummary = summary ? oneLine(summary) : "";
37
+ lines.push(`- [${title}](${link})${safeSummary ? `: ${safeSummary}` : ""}`);
38
+ }
39
+ lines.push("");
40
+ }
41
+ return lines.join("\n");
42
+ }
43
+ /**
44
+ * Renders a `llms.txt` index linking all posts with one-line summaries.
45
+ *
46
+ * @param posts - All posts to include.
47
+ * @param config - Resolved Voxx config.
48
+ * @returns `llms.txt` Markdown string.
49
+ */
50
+ function renderLlmsTxt(posts, config) {
51
+ return renderLlmsTxtSections([{
52
+ heading: SECTION_HEADING[config.content.type],
53
+ posts
54
+ }], config);
55
+ }
56
+ /**
57
+ * Renders an `llms-full.txt` file containing the complete Markdown content
58
+ * of every post, separated by `---` dividers.
59
+ *
60
+ * @param posts - All posts to include.
61
+ * @param config - Resolved Voxx config.
62
+ * @returns Full-content `llms-full.txt` string.
63
+ */
64
+ function renderLlmsFull(posts, config) {
65
+ const out = [`# ${config.site.title}`, ""];
66
+ if (config.site.description) out.push(config.site.description, "");
67
+ for (const p of posts) {
68
+ const link = absoluteUrl(config.site.url, p.url);
69
+ out.push("---", "", `# ${escHeading(p.title)}`, "", `Source: ${link}`, `Date: ${p.date}`);
70
+ if (p.tags.length) out.push(`Tags: ${p.tags.join(", ")}`);
71
+ out.push("", p.content.trim(), "");
72
+ }
73
+ return out.join("\n");
74
+ }
75
+ //#endregion
76
+ export { renderLlmsFull, renderLlmsTxt, renderLlmsTxtSections, sectionHeading };
77
+
78
+ //# sourceMappingURL=llms.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"llms.mjs","names":[],"sources":["../src/llms.ts"],"sourcesContent":["import { absoluteUrl } from \"./util\";\nimport type { ContentType, Post, VoxxConfig } from \"./types\";\n\nconst SECTION_HEADING: Record<ContentType, string> = {\n blog: \"Posts\",\n docs: \"Pages\",\n changelog: \"Releases\",\n};\n\nconst oneLine = (s: string) => s.replace(/\\s*\\n\\s*/g, \" \").trim();\nconst escLinkText = (s: string) => oneLine(s).replace(/([[\\]])/g, \"\\\\$1\");\nconst escHeading = (s: string) => oneLine(s);\n\n/** A titled group of posts for use in `renderLlmsTxtSections`. */\nexport interface LlmsSection {\n heading: string;\n posts: Post[];\n}\n\n/**\n * Returns the default section heading for a content type.\n *\n * @param type - Content type from the Voxx config.\n * @returns Human-readable heading, e.g. `\"Posts\"`, `\"Pages\"`, `\"Releases\"`.\n */\nexport function sectionHeading(type: ContentType): string {\n return SECTION_HEADING[type];\n}\n\n/**\n * Renders a `llms.txt` file from one or more labeled post sections.\n *\n * @param sections - Ordered list of headings with their associated posts.\n * @param config - Resolved Voxx config (used for site title and description).\n * @returns `llms.txt` Markdown string.\n */\nexport function renderLlmsTxtSections(\n sections: LlmsSection[],\n config: VoxxConfig,\n): string {\n const lines: string[] = [`# ${config.site.title}`, \"\"];\n if (config.site.description) lines.push(`> ${config.site.description}`, \"\");\n for (const section of sections) {\n lines.push(`## ${section.heading}`, \"\");\n for (const p of section.posts) {\n const link = absoluteUrl(config.site.url, p.url);\n const summary = p.description ?? p.excerpt;\n const title = escLinkText(p.title);\n const safeSummary = summary ? oneLine(summary) : \"\";\n lines.push(\n `- [${title}](${link})${safeSummary ? `: ${safeSummary}` : \"\"}`,\n );\n }\n lines.push(\"\");\n }\n return lines.join(\"\\n\");\n}\n\n/**\n * Renders a `llms.txt` index linking all posts with one-line summaries.\n *\n * @param posts - All posts to include.\n * @param config - Resolved Voxx config.\n * @returns `llms.txt` Markdown string.\n */\nexport function renderLlmsTxt(posts: Post[], config: VoxxConfig): string {\n return renderLlmsTxtSections(\n [{ heading: SECTION_HEADING[config.content.type], posts }],\n config,\n );\n}\n\n/**\n * Renders an `llms-full.txt` file containing the complete Markdown content\n * of every post, separated by `---` dividers.\n *\n * @param posts - All posts to include.\n * @param config - Resolved Voxx config.\n * @returns Full-content `llms-full.txt` string.\n */\nexport function renderLlmsFull(posts: Post[], config: VoxxConfig): string {\n const out: string[] = [`# ${config.site.title}`, \"\"];\n if (config.site.description) out.push(config.site.description, \"\");\n for (const p of posts) {\n const link = absoluteUrl(config.site.url, p.url);\n out.push(\n \"---\",\n \"\",\n `# ${escHeading(p.title)}`,\n \"\",\n `Source: ${link}`,\n `Date: ${p.date}`,\n );\n if (p.tags.length) out.push(`Tags: ${p.tags.join(\", \")}`);\n out.push(\"\", p.content.trim(), \"\");\n }\n return out.join(\"\\n\");\n}\n"],"mappings":";;AAGA,MAAM,kBAA+C;CACnD,MAAM;CACN,MAAM;CACN,WAAW;AACb;AAEA,MAAM,WAAW,MAAc,EAAE,QAAQ,aAAa,GAAG,CAAC,CAAC,KAAK;AAChE,MAAM,eAAe,MAAc,QAAQ,CAAC,CAAC,CAAC,QAAQ,YAAY,MAAM;AACxE,MAAM,cAAc,MAAc,QAAQ,CAAC;;;;;;;AAc3C,SAAgB,eAAe,MAA2B;CACxD,OAAO,gBAAgB;AACzB;;;;;;;;AASA,SAAgB,sBACd,UACA,QACQ;CACR,MAAM,QAAkB,CAAC,KAAK,OAAO,KAAK,SAAS,EAAE;CACrD,IAAI,OAAO,KAAK,aAAa,MAAM,KAAK,KAAK,OAAO,KAAK,eAAe,EAAE;CAC1E,KAAK,MAAM,WAAW,UAAU;EAC9B,MAAM,KAAK,MAAM,QAAQ,WAAW,EAAE;EACtC,KAAK,MAAM,KAAK,QAAQ,OAAO;GAC7B,MAAM,OAAO,YAAY,OAAO,KAAK,KAAK,EAAE,GAAG;GAC/C,MAAM,UAAU,EAAE,eAAe,EAAE;GACnC,MAAM,QAAQ,YAAY,EAAE,KAAK;GACjC,MAAM,cAAc,UAAU,QAAQ,OAAO,IAAI;GACjD,MAAM,KACJ,MAAM,MAAM,IAAI,KAAK,GAAG,cAAc,KAAK,gBAAgB,IAC7D;EACF;EACA,MAAM,KAAK,EAAE;CACf;CACA,OAAO,MAAM,KAAK,IAAI;AACxB;;;;;;;;AASA,SAAgB,cAAc,OAAe,QAA4B;CACvE,OAAO,sBACL,CAAC;EAAE,SAAS,gBAAgB,OAAO,QAAQ;EAAO;CAAM,CAAC,GACzD,MACF;AACF;;;;;;;;;AAUA,SAAgB,eAAe,OAAe,QAA4B;CACxE,MAAM,MAAgB,CAAC,KAAK,OAAO,KAAK,SAAS,EAAE;CACnD,IAAI,OAAO,KAAK,aAAa,IAAI,KAAK,OAAO,KAAK,aAAa,EAAE;CACjE,KAAK,MAAM,KAAK,OAAO;EACrB,MAAM,OAAO,YAAY,OAAO,KAAK,KAAK,EAAE,GAAG;EAC/C,IAAI,KACF,OACA,IACA,KAAK,WAAW,EAAE,KAAK,KACvB,IACA,WAAW,QACX,SAAS,EAAE,MACb;EACA,IAAI,EAAE,KAAK,QAAQ,IAAI,KAAK,SAAS,EAAE,KAAK,KAAK,IAAI,GAAG;EACxD,IAAI,KAAK,IAAI,EAAE,QAAQ,KAAK,GAAG,EAAE;CACnC;CACA,OAAO,IAAI,KAAK,IAAI;AACtB"}
package/dist/nav.cjs ADDED
@@ -0,0 +1,50 @@
1
+ const require_util = require("./util.cjs");
2
+ //#region src/nav.ts
3
+ /**
4
+ * Builds a sidebar navigation tree from an ordered list of docs posts.
5
+ *
6
+ * Directory segments become category nodes; index files promote their
7
+ * title and URL onto the parent node.
8
+ *
9
+ * @param posts - Posts sorted by `getPostsEffect` (order-prefix aware).
10
+ * @returns Top-level `NavNode` array suitable for a sidebar component.
11
+ */
12
+ function buildNavTree(posts) {
13
+ const root = {
14
+ title: "",
15
+ children: []
16
+ };
17
+ const nodes = /* @__PURE__ */ new Map();
18
+ const nodeFor = (path) => {
19
+ if (path.length === 0) return root;
20
+ const key = path.join("/");
21
+ let node = nodes.get(key);
22
+ if (!node) {
23
+ node = {
24
+ title: require_util.humanize(path[path.length - 1]),
25
+ children: []
26
+ };
27
+ nodes.set(key, node);
28
+ nodeFor(path.slice(0, -1)).children.push(node);
29
+ }
30
+ return node;
31
+ };
32
+ for (const post of posts) {
33
+ if (post.path.length === 0) {
34
+ root.children.push({
35
+ title: post.title,
36
+ url: post.url,
37
+ children: []
38
+ });
39
+ continue;
40
+ }
41
+ const node = nodeFor(post.path);
42
+ node.title = post.title;
43
+ node.url = post.url;
44
+ }
45
+ return root.children;
46
+ }
47
+ //#endregion
48
+ exports.buildNavTree = buildNavTree;
49
+
50
+ //# sourceMappingURL=nav.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nav.cjs","names":["humanize"],"sources":["../src/nav.ts"],"sourcesContent":["import type { NavNode, Post } from \"./types\";\nimport { humanize } from \"./util\";\n\n/**\n * Builds a sidebar navigation tree from an ordered list of docs posts.\n *\n * Directory segments become category nodes; index files promote their\n * title and URL onto the parent node.\n *\n * @param posts - Posts sorted by `getPostsEffect` (order-prefix aware).\n * @returns Top-level `NavNode` array suitable for a sidebar component.\n */\nexport function buildNavTree(posts: Post[]): NavNode[] {\n const root: NavNode = { title: \"\", children: [] };\n const nodes = new Map<string, NavNode>();\n\n const nodeFor = (path: string[]): NavNode => {\n if (path.length === 0) return root;\n const key = path.join(\"/\");\n let node = nodes.get(key);\n if (!node) {\n node = { title: humanize(path[path.length - 1]!), children: [] };\n nodes.set(key, node);\n nodeFor(path.slice(0, -1)).children.push(node);\n }\n return node;\n };\n\n for (const post of posts) {\n if (post.path.length === 0) {\n root.children.push({ title: post.title, url: post.url, children: [] });\n continue;\n }\n const node = nodeFor(post.path);\n node.title = post.title;\n node.url = post.url;\n }\n\n return root.children;\n}\n"],"mappings":";;;;;;;;;;;AAYA,SAAgB,aAAa,OAA0B;CACrD,MAAM,OAAgB;EAAE,OAAO;EAAI,UAAU,CAAC;CAAE;CAChD,MAAM,wBAAQ,IAAI,IAAqB;CAEvC,MAAM,WAAW,SAA4B;EAC3C,IAAI,KAAK,WAAW,GAAG,OAAO;EAC9B,MAAM,MAAM,KAAK,KAAK,GAAG;EACzB,IAAI,OAAO,MAAM,IAAI,GAAG;EACxB,IAAI,CAAC,MAAM;GACT,OAAO;IAAE,OAAOA,aAAAA,SAAS,KAAK,KAAK,SAAS,EAAG;IAAG,UAAU,CAAC;GAAE;GAC/D,MAAM,IAAI,KAAK,IAAI;GACnB,QAAQ,KAAK,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI;EAC/C;EACA,OAAO;CACT;CAEA,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,KAAK,KAAK,WAAW,GAAG;GAC1B,KAAK,SAAS,KAAK;IAAE,OAAO,KAAK;IAAO,KAAK,KAAK;IAAK,UAAU,CAAC;GAAE,CAAC;GACrE;EACF;EACA,MAAM,OAAO,QAAQ,KAAK,IAAI;EAC9B,KAAK,QAAQ,KAAK;EAClB,KAAK,MAAM,KAAK;CAClB;CAEA,OAAO,KAAK;AACd"}
package/dist/nav.d.cts ADDED
@@ -0,0 +1,16 @@
1
+ import { NavNode, Post } from "./types.cjs";
2
+
3
+ //#region src/nav.d.ts
4
+ /**
5
+ * Builds a sidebar navigation tree from an ordered list of docs posts.
6
+ *
7
+ * Directory segments become category nodes; index files promote their
8
+ * title and URL onto the parent node.
9
+ *
10
+ * @param posts - Posts sorted by `getPostsEffect` (order-prefix aware).
11
+ * @returns Top-level `NavNode` array suitable for a sidebar component.
12
+ */
13
+ declare function buildNavTree(posts: Post[]): NavNode[];
14
+ //#endregion
15
+ export { buildNavTree };
16
+ //# sourceMappingURL=nav.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nav.d.cts","names":[],"sources":["../src/nav.ts"],"mappings":";;;;;AAYA;;;;;;;iBAAgB,YAAA,CAAa,KAAA,EAAO,IAAA,KAAS,OAAO"}
package/dist/nav.d.mts ADDED
@@ -0,0 +1,16 @@
1
+ import { NavNode, Post } from "./types.mjs";
2
+
3
+ //#region src/nav.d.ts
4
+ /**
5
+ * Builds a sidebar navigation tree from an ordered list of docs posts.
6
+ *
7
+ * Directory segments become category nodes; index files promote their
8
+ * title and URL onto the parent node.
9
+ *
10
+ * @param posts - Posts sorted by `getPostsEffect` (order-prefix aware).
11
+ * @returns Top-level `NavNode` array suitable for a sidebar component.
12
+ */
13
+ declare function buildNavTree(posts: Post[]): NavNode[];
14
+ //#endregion
15
+ export { buildNavTree };
16
+ //# sourceMappingURL=nav.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nav.d.mts","names":[],"sources":["../src/nav.ts"],"mappings":";;;;;AAYA;;;;;;;iBAAgB,YAAA,CAAa,KAAA,EAAO,IAAA,KAAS,OAAO"}
package/dist/nav.mjs ADDED
@@ -0,0 +1,50 @@
1
+ import { humanize } from "./util.mjs";
2
+ //#region src/nav.ts
3
+ /**
4
+ * Builds a sidebar navigation tree from an ordered list of docs posts.
5
+ *
6
+ * Directory segments become category nodes; index files promote their
7
+ * title and URL onto the parent node.
8
+ *
9
+ * @param posts - Posts sorted by `getPostsEffect` (order-prefix aware).
10
+ * @returns Top-level `NavNode` array suitable for a sidebar component.
11
+ */
12
+ function buildNavTree(posts) {
13
+ const root = {
14
+ title: "",
15
+ children: []
16
+ };
17
+ const nodes = /* @__PURE__ */ new Map();
18
+ const nodeFor = (path) => {
19
+ if (path.length === 0) return root;
20
+ const key = path.join("/");
21
+ let node = nodes.get(key);
22
+ if (!node) {
23
+ node = {
24
+ title: humanize(path[path.length - 1]),
25
+ children: []
26
+ };
27
+ nodes.set(key, node);
28
+ nodeFor(path.slice(0, -1)).children.push(node);
29
+ }
30
+ return node;
31
+ };
32
+ for (const post of posts) {
33
+ if (post.path.length === 0) {
34
+ root.children.push({
35
+ title: post.title,
36
+ url: post.url,
37
+ children: []
38
+ });
39
+ continue;
40
+ }
41
+ const node = nodeFor(post.path);
42
+ node.title = post.title;
43
+ node.url = post.url;
44
+ }
45
+ return root.children;
46
+ }
47
+ //#endregion
48
+ export { buildNavTree };
49
+
50
+ //# sourceMappingURL=nav.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nav.mjs","names":[],"sources":["../src/nav.ts"],"sourcesContent":["import type { NavNode, Post } from \"./types\";\nimport { humanize } from \"./util\";\n\n/**\n * Builds a sidebar navigation tree from an ordered list of docs posts.\n *\n * Directory segments become category nodes; index files promote their\n * title and URL onto the parent node.\n *\n * @param posts - Posts sorted by `getPostsEffect` (order-prefix aware).\n * @returns Top-level `NavNode` array suitable for a sidebar component.\n */\nexport function buildNavTree(posts: Post[]): NavNode[] {\n const root: NavNode = { title: \"\", children: [] };\n const nodes = new Map<string, NavNode>();\n\n const nodeFor = (path: string[]): NavNode => {\n if (path.length === 0) return root;\n const key = path.join(\"/\");\n let node = nodes.get(key);\n if (!node) {\n node = { title: humanize(path[path.length - 1]!), children: [] };\n nodes.set(key, node);\n nodeFor(path.slice(0, -1)).children.push(node);\n }\n return node;\n };\n\n for (const post of posts) {\n if (post.path.length === 0) {\n root.children.push({ title: post.title, url: post.url, children: [] });\n continue;\n }\n const node = nodeFor(post.path);\n node.title = post.title;\n node.url = post.url;\n }\n\n return root.children;\n}\n"],"mappings":";;;;;;;;;;;AAYA,SAAgB,aAAa,OAA0B;CACrD,MAAM,OAAgB;EAAE,OAAO;EAAI,UAAU,CAAC;CAAE;CAChD,MAAM,wBAAQ,IAAI,IAAqB;CAEvC,MAAM,WAAW,SAA4B;EAC3C,IAAI,KAAK,WAAW,GAAG,OAAO;EAC9B,MAAM,MAAM,KAAK,KAAK,GAAG;EACzB,IAAI,OAAO,MAAM,IAAI,GAAG;EACxB,IAAI,CAAC,MAAM;GACT,OAAO;IAAE,OAAO,SAAS,KAAK,KAAK,SAAS,EAAG;IAAG,UAAU,CAAC;GAAE;GAC/D,MAAM,IAAI,KAAK,IAAI;GACnB,QAAQ,KAAK,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,IAAI;EAC/C;EACA,OAAO;CACT;CAEA,KAAK,MAAM,QAAQ,OAAO;EACxB,IAAI,KAAK,KAAK,WAAW,GAAG;GAC1B,KAAK,SAAS,KAAK;IAAE,OAAO,KAAK;IAAO,KAAK,KAAK;IAAK,UAAU,CAAC;GAAE,CAAC;GACrE;EACF;EACA,MAAM,OAAO,QAAQ,KAAK,IAAI;EAC9B,KAAK,QAAQ,KAAK;EAClB,KAAK,MAAM,KAAK;CAClB;CAEA,OAAO,KAAK;AACd"}
@@ -0,0 +1,152 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ const require_errors = require("./errors.cjs");
3
+ let effect = require("effect");
4
+ let unified = require("unified");
5
+ let remark_parse = require("remark-parse");
6
+ remark_parse = require_runtime.__toESM(remark_parse, 1);
7
+ let remark_gfm = require("remark-gfm");
8
+ remark_gfm = require_runtime.__toESM(remark_gfm, 1);
9
+ let remark_rehype = require("remark-rehype");
10
+ remark_rehype = require_runtime.__toESM(remark_rehype, 1);
11
+ let rehype_raw = require("rehype-raw");
12
+ rehype_raw = require_runtime.__toESM(rehype_raw, 1);
13
+ let rehype_slug = require("rehype-slug");
14
+ rehype_slug = require_runtime.__toESM(rehype_slug, 1);
15
+ let rehype_autolink_headings = require("rehype-autolink-headings");
16
+ rehype_autolink_headings = require_runtime.__toESM(rehype_autolink_headings, 1);
17
+ let rehype_stringify = require("rehype-stringify");
18
+ rehype_stringify = require_runtime.__toESM(rehype_stringify, 1);
19
+ let _shikijs_rehype_core = require("@shikijs/rehype/core");
20
+ _shikijs_rehype_core = require_runtime.__toESM(_shikijs_rehype_core, 1);
21
+ let shiki = require("shiki");
22
+ let unist_util_visit = require("unist-util-visit");
23
+ let hast_util_to_string = require("hast-util-to-string");
24
+ //#region src/render.ts
25
+ function rehypeCollectToc() {
26
+ return (tree, file) => {
27
+ const toc = [];
28
+ (0, unist_util_visit.visit)(tree, "element", (node) => {
29
+ if (node.tagName !== "h2" && node.tagName !== "h3") return;
30
+ const id = node.properties?.["id"];
31
+ if (typeof id !== "string") return;
32
+ toc.push({
33
+ id,
34
+ text: (0, hast_util_to_string.toString)(node),
35
+ depth: node.tagName === "h2" ? 2 : 3
36
+ });
37
+ });
38
+ file.data["toc"] = toc;
39
+ };
40
+ }
41
+ const EXTERNAL_RE = /^https?:\/\//i;
42
+ const ABSOLUTE_RE = /^(?:[a-z][a-z0-9+.-]*:|\/|#|\?)/i;
43
+ const ASSET_PROPS = ["src", "poster"];
44
+ function resolveRelativePath(base, rel) {
45
+ const segments = [...base.split("/"), ...rel.split("/")].filter((s) => s !== "" && s !== ".");
46
+ const out = [];
47
+ for (const segment of segments) if (segment === "..") out.pop();
48
+ else out.push(segment);
49
+ return `/${out.join("/")}`;
50
+ }
51
+ function rehypeExternalLinks() {
52
+ return (tree) => {
53
+ (0, unist_util_visit.visit)(tree, "element", (node) => {
54
+ if (node.tagName !== "a") return;
55
+ const href = node.properties?.["href"];
56
+ if (typeof href !== "string" || !EXTERNAL_RE.test(href)) return;
57
+ node.properties["target"] = "_blank";
58
+ node.properties["rel"] = "noreferrer";
59
+ });
60
+ };
61
+ }
62
+ function rehypeResolveAssets(base) {
63
+ return (tree) => {
64
+ (0, unist_util_visit.visit)(tree, "element", (node) => {
65
+ for (const prop of ASSET_PROPS) {
66
+ const value = node.properties?.[prop];
67
+ if (typeof value !== "string" || value === "") continue;
68
+ if (ABSOLUTE_RE.test(value)) continue;
69
+ node.properties[prop] = resolveRelativePath(base, value);
70
+ }
71
+ });
72
+ };
73
+ }
74
+ function shikiOptions(codeTheme) {
75
+ const parts = codeTheme.split(/[,\s]+/).filter(Boolean);
76
+ if (parts.length >= 2) return {
77
+ themes: {
78
+ light: parts[0],
79
+ dark: parts[1]
80
+ },
81
+ defaultColor: false
82
+ };
83
+ return { theme: parts[0] ?? "github-dark" };
84
+ }
85
+ const BASE_LANGS = [
86
+ "ts",
87
+ "tsx",
88
+ "js",
89
+ "jsx",
90
+ "json",
91
+ "bash",
92
+ "css",
93
+ "html",
94
+ "md",
95
+ "python",
96
+ "go",
97
+ "rust"
98
+ ];
99
+ const highlighters = /* @__PURE__ */ new Map();
100
+ function getHighlighter(codeTheme) {
101
+ let cached = highlighters.get(codeTheme);
102
+ if (!cached) {
103
+ const opts = shikiOptions(codeTheme);
104
+ cached = (0, shiki.createHighlighter)({
105
+ themes: "themes" in opts ? [opts.themes.light, opts.themes.dark] : [opts.theme],
106
+ langs: BASE_LANGS
107
+ });
108
+ highlighters.set(codeTheme, cached);
109
+ }
110
+ return cached;
111
+ }
112
+ const FENCE_LANG_RE = /^[ \t]*(?:`{3,}|~{3,})[ \t]*([A-Za-z0-9_+-]+)/gm;
113
+ async function ensureLanguages(highlighter, markdown) {
114
+ const loaded = new Set(highlighter.getLoadedLanguages());
115
+ const wanted = /* @__PURE__ */ new Set();
116
+ for (const match of markdown.matchAll(FENCE_LANG_RE)) {
117
+ const lang = match[1].toLowerCase();
118
+ if (!loaded.has(lang) && lang in shiki.bundledLanguages) wanted.add(lang);
119
+ }
120
+ for (const lang of wanted) try {
121
+ await highlighter.loadLanguage(lang);
122
+ } catch {}
123
+ }
124
+ /**
125
+ * Converts Markdown to HTML with syntax highlighting, heading slugs,
126
+ * autolinked headings, and a collected table of contents.
127
+ *
128
+ * @param markdown - Raw Markdown source.
129
+ * @param config - Voxx config (used for `theme.codeTheme`).
130
+ * @param opts - Optional `assetBase` for resolving relative image paths.
131
+ */
132
+ const renderMarkdownEffect = (markdown, config, opts = {}) => effect.Effect.tryPromise({
133
+ try: async () => {
134
+ const highlighter = await getHighlighter(config.theme.codeTheme);
135
+ await ensureLanguages(highlighter, markdown);
136
+ const processor = (0, unified.unified)().use(remark_parse.default).use(remark_gfm.default).use(remark_rehype.default, { allowDangerousHtml: true }).use(rehype_raw.default).use(rehype_slug.default).use(rehype_autolink_headings.default, { behavior: "wrap" }).use(rehypeExternalLinks).use(_shikijs_rehype_core.default, highlighter, shikiOptions(config.theme.codeTheme));
137
+ if (opts.assetBase) processor.use(rehypeResolveAssets, opts.assetBase);
138
+ const file = await processor.use(rehypeCollectToc).use(rehype_stringify.default).process(markdown);
139
+ return {
140
+ html: String(file),
141
+ toc: file.data["toc"] ?? []
142
+ };
143
+ },
144
+ catch: (cause) => new require_errors.RenderError({
145
+ message: `Failed to render markdown: ${String(cause)}`,
146
+ cause
147
+ })
148
+ });
149
+ //#endregion
150
+ exports.renderMarkdownEffect = renderMarkdownEffect;
151
+
152
+ //# sourceMappingURL=render.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.cjs","names":["bundledLanguages","Effect","remarkParse","remarkGfm","remarkRehype","rehypeRaw","rehypeSlug","rehypeAutolinkHeadings","rehypeShikiFromHighlighter","rehypeStringify","RenderError"],"sources":["../src/render.ts"],"sourcesContent":["import { Effect } from \"effect\";\nimport { unified } from \"unified\";\nimport remarkParse from \"remark-parse\";\nimport remarkGfm from \"remark-gfm\";\nimport remarkRehype from \"remark-rehype\";\nimport rehypeRaw from \"rehype-raw\";\nimport rehypeSlug from \"rehype-slug\";\nimport rehypeAutolinkHeadings from \"rehype-autolink-headings\";\nimport rehypeStringify from \"rehype-stringify\";\nimport rehypeShikiFromHighlighter from \"@shikijs/rehype/core\";\nimport {\n createHighlighter,\n bundledLanguages,\n type BundledLanguage,\n type Highlighter,\n} from \"shiki\";\nimport { visit } from \"unist-util-visit\";\nimport { toString } from \"hast-util-to-string\";\nimport type { Root } from \"hast\";\nimport { RenderError } from \"./errors\";\nimport type { TocItem, VoxxConfig } from \"./types\";\n\n/** Result returned by the Markdown renderer. */\nexport interface RenderResult {\n /** Rendered HTML string. */\n html: string;\n /** Extracted `h2`/`h3` headings for the table of contents. */\n toc: TocItem[];\n}\n\n/** Options passed to the Markdown renderer. */\nexport interface RenderOptions {\n /** URL prefix used to resolve relative `src` and `poster` attributes. */\n assetBase?: string;\n}\n\nfunction rehypeCollectToc() {\n return (tree: Root, file: { data: Record<string, unknown> }) => {\n const toc: TocItem[] = [];\n visit(tree, \"element\", (node) => {\n if (node.tagName !== \"h2\" && node.tagName !== \"h3\") return;\n const id = node.properties?.[\"id\"];\n if (typeof id !== \"string\") return;\n toc.push({\n id,\n text: toString(node),\n depth: node.tagName === \"h2\" ? 2 : 3,\n });\n });\n file.data[\"toc\"] = toc;\n };\n}\n\nconst EXTERNAL_RE = /^https?:\\/\\//i;\nconst ABSOLUTE_RE = /^(?:[a-z][a-z0-9+.-]*:|\\/|#|\\?)/i;\nconst ASSET_PROPS = [\"src\", \"poster\"] as const;\n\nfunction resolveRelativePath(base: string, rel: string): string {\n const segments = [...base.split(\"/\"), ...rel.split(\"/\")].filter(\n (s) => s !== \"\" && s !== \".\",\n );\n const out: string[] = [];\n for (const segment of segments) {\n if (segment === \"..\") out.pop();\n else out.push(segment);\n }\n return `/${out.join(\"/\")}`;\n}\n\nfunction rehypeExternalLinks() {\n return (tree: Root) => {\n visit(tree, \"element\", (node) => {\n if (node.tagName !== \"a\") return;\n const href = node.properties?.[\"href\"];\n if (typeof href !== \"string\" || !EXTERNAL_RE.test(href)) return;\n node.properties[\"target\"] = \"_blank\";\n node.properties[\"rel\"] = \"noreferrer\";\n });\n };\n}\n\nfunction rehypeResolveAssets(base: string) {\n return (tree: Root) => {\n visit(tree, \"element\", (node) => {\n for (const prop of ASSET_PROPS) {\n const value = node.properties?.[prop];\n if (typeof value !== \"string\" || value === \"\") continue;\n if (ABSOLUTE_RE.test(value)) continue;\n node.properties[prop] = resolveRelativePath(base, value);\n }\n });\n };\n}\n\ntype ShikiOptions =\n | { themes: { light: string; dark: string }; defaultColor: false }\n | { theme: string };\n\nfunction shikiOptions(codeTheme: string): ShikiOptions {\n const parts = codeTheme.split(/[,\\s]+/).filter(Boolean);\n if (parts.length >= 2) {\n return {\n themes: { light: parts[0]!, dark: parts[1]! },\n defaultColor: false as const,\n };\n }\n return { theme: parts[0] ?? \"github-dark\" };\n}\n\nconst BASE_LANGS: BundledLanguage[] = [\n \"ts\",\n \"tsx\",\n \"js\",\n \"jsx\",\n \"json\",\n \"bash\",\n \"css\",\n \"html\",\n \"md\",\n \"python\",\n \"go\",\n \"rust\",\n];\n\nconst highlighters = new Map<string, Promise<Highlighter>>();\n\nfunction getHighlighter(codeTheme: string): Promise<Highlighter> {\n let cached = highlighters.get(codeTheme);\n if (!cached) {\n const opts = shikiOptions(codeTheme);\n const themes =\n \"themes\" in opts\n ? [opts.themes.light, opts.themes.dark]\n : [(opts as { theme: string }).theme];\n cached = createHighlighter({ themes, langs: BASE_LANGS });\n highlighters.set(codeTheme, cached);\n }\n return cached;\n}\n\nconst FENCE_LANG_RE = /^[ \\t]*(?:`{3,}|~{3,})[ \\t]*([A-Za-z0-9_+-]+)/gm;\n\nasync function ensureLanguages(\n highlighter: Highlighter,\n markdown: string,\n): Promise<void> {\n const loaded = new Set(highlighter.getLoadedLanguages());\n const wanted = new Set<BundledLanguage>();\n for (const match of markdown.matchAll(FENCE_LANG_RE)) {\n const lang = match[1]!.toLowerCase();\n if (!loaded.has(lang) && lang in bundledLanguages) {\n wanted.add(lang as BundledLanguage);\n }\n }\n for (const lang of wanted) {\n try {\n await highlighter.loadLanguage(lang);\n } catch {}\n }\n}\n\n/**\n * Converts Markdown to HTML with syntax highlighting, heading slugs,\n * autolinked headings, and a collected table of contents.\n *\n * @param markdown - Raw Markdown source.\n * @param config - Voxx config (used for `theme.codeTheme`).\n * @param opts - Optional `assetBase` for resolving relative image paths.\n */\nexport const renderMarkdownEffect = (\n markdown: string,\n config: VoxxConfig,\n opts: RenderOptions = {},\n) =>\n Effect.tryPromise({\n try: async (): Promise<RenderResult> => {\n const highlighter = await getHighlighter(config.theme.codeTheme);\n await ensureLanguages(highlighter, markdown);\n const processor = unified()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkRehype, { allowDangerousHtml: true })\n .use(rehypeRaw)\n .use(rehypeSlug)\n .use(rehypeAutolinkHeadings, { behavior: \"wrap\" })\n .use(rehypeExternalLinks)\n .use(\n rehypeShikiFromHighlighter,\n highlighter,\n shikiOptions(config.theme.codeTheme),\n );\n if (opts.assetBase) processor.use(rehypeResolveAssets, opts.assetBase);\n const file = await processor\n .use(rehypeCollectToc)\n .use(rehypeStringify)\n .process(markdown);\n return {\n html: String(file),\n toc: (file.data[\"toc\"] as TocItem[] | undefined) ?? [],\n };\n },\n catch: (cause) =>\n new RenderError({\n message: `Failed to render markdown: ${String(cause)}`,\n cause,\n }),\n });\n"],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { RenderError } from "./errors.cjs";
2
+ import { TocItem, VoxxConfig } from "./types.cjs";
3
+ import { Effect } from "effect";
4
+
5
+ //#region src/render.d.ts
6
+ /** Result returned by the Markdown renderer. */
7
+ interface RenderResult {
8
+ /** Rendered HTML string. */
9
+ html: string;
10
+ /** Extracted `h2`/`h3` headings for the table of contents. */
11
+ toc: TocItem[];
12
+ }
13
+ /** Options passed to the Markdown renderer. */
14
+ interface RenderOptions {
15
+ /** URL prefix used to resolve relative `src` and `poster` attributes. */
16
+ assetBase?: string;
17
+ }
18
+ /**
19
+ * Converts Markdown to HTML with syntax highlighting, heading slugs,
20
+ * autolinked headings, and a collected table of contents.
21
+ *
22
+ * @param markdown - Raw Markdown source.
23
+ * @param config - Voxx config (used for `theme.codeTheme`).
24
+ * @param opts - Optional `assetBase` for resolving relative image paths.
25
+ */
26
+ declare const renderMarkdownEffect: (markdown: string, config: VoxxConfig, opts?: RenderOptions) => Effect.Effect<RenderResult, RenderError, never>;
27
+ //#endregion
28
+ export { RenderResult, renderMarkdownEffect };
29
+ //# sourceMappingURL=render.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.cts","names":[],"sources":["../src/render.ts"],"mappings":";;;;;;UAuBiB,YAAA;EAAA;EAEf,IAAA;;EAEA,GAAA,EAAK,OAAO;AAAA;;UAIG,aAAA;EAJH;EAMZ,SAAS;AAAA;;;;AAAA;AAwIX;;;;cAAa,oBAAA,GACX,QAAA,UACA,MAAA,EAAQ,UAAA,EACR,IAAA,GAAM,aAAA,KAAkB,MAAA,CAAA,MAAA,CAAA,YAAA,EAAA,WAAA"}
@@ -0,0 +1,29 @@
1
+ import { RenderError } from "./errors.mjs";
2
+ import { TocItem, VoxxConfig } from "./types.mjs";
3
+ import { Effect } from "effect";
4
+
5
+ //#region src/render.d.ts
6
+ /** Result returned by the Markdown renderer. */
7
+ interface RenderResult {
8
+ /** Rendered HTML string. */
9
+ html: string;
10
+ /** Extracted `h2`/`h3` headings for the table of contents. */
11
+ toc: TocItem[];
12
+ }
13
+ /** Options passed to the Markdown renderer. */
14
+ interface RenderOptions {
15
+ /** URL prefix used to resolve relative `src` and `poster` attributes. */
16
+ assetBase?: string;
17
+ }
18
+ /**
19
+ * Converts Markdown to HTML with syntax highlighting, heading slugs,
20
+ * autolinked headings, and a collected table of contents.
21
+ *
22
+ * @param markdown - Raw Markdown source.
23
+ * @param config - Voxx config (used for `theme.codeTheme`).
24
+ * @param opts - Optional `assetBase` for resolving relative image paths.
25
+ */
26
+ declare const renderMarkdownEffect: (markdown: string, config: VoxxConfig, opts?: RenderOptions) => Effect.Effect<RenderResult, RenderError, never>;
27
+ //#endregion
28
+ export { RenderResult, renderMarkdownEffect };
29
+ //# sourceMappingURL=render.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.mts","names":[],"sources":["../src/render.ts"],"mappings":";;;;;;UAuBiB,YAAA;EAAA;EAEf,IAAA;;EAEA,GAAA,EAAK,OAAO;AAAA;;UAIG,aAAA;EAJH;EAMZ,SAAS;AAAA;;;;AAAA;AAwIX;;;;cAAa,oBAAA,GACX,QAAA,UACA,MAAA,EAAQ,UAAA,EACR,IAAA,GAAM,aAAA,KAAkB,MAAA,CAAA,MAAA,CAAA,YAAA,EAAA,WAAA"}
@@ -0,0 +1,143 @@
1
+ import { RenderError } from "./errors.mjs";
2
+ import { Effect } from "effect";
3
+ import { unified } from "unified";
4
+ import remarkParse from "remark-parse";
5
+ import remarkGfm from "remark-gfm";
6
+ import remarkRehype from "remark-rehype";
7
+ import rehypeRaw from "rehype-raw";
8
+ import rehypeSlug from "rehype-slug";
9
+ import rehypeAutolinkHeadings from "rehype-autolink-headings";
10
+ import rehypeStringify from "rehype-stringify";
11
+ import rehypeShikiFromHighlighter from "@shikijs/rehype/core";
12
+ import { bundledLanguages, createHighlighter } from "shiki";
13
+ import { visit } from "unist-util-visit";
14
+ import { toString } from "hast-util-to-string";
15
+ //#region src/render.ts
16
+ function rehypeCollectToc() {
17
+ return (tree, file) => {
18
+ const toc = [];
19
+ visit(tree, "element", (node) => {
20
+ if (node.tagName !== "h2" && node.tagName !== "h3") return;
21
+ const id = node.properties?.["id"];
22
+ if (typeof id !== "string") return;
23
+ toc.push({
24
+ id,
25
+ text: toString(node),
26
+ depth: node.tagName === "h2" ? 2 : 3
27
+ });
28
+ });
29
+ file.data["toc"] = toc;
30
+ };
31
+ }
32
+ const EXTERNAL_RE = /^https?:\/\//i;
33
+ const ABSOLUTE_RE = /^(?:[a-z][a-z0-9+.-]*:|\/|#|\?)/i;
34
+ const ASSET_PROPS = ["src", "poster"];
35
+ function resolveRelativePath(base, rel) {
36
+ const segments = [...base.split("/"), ...rel.split("/")].filter((s) => s !== "" && s !== ".");
37
+ const out = [];
38
+ for (const segment of segments) if (segment === "..") out.pop();
39
+ else out.push(segment);
40
+ return `/${out.join("/")}`;
41
+ }
42
+ function rehypeExternalLinks() {
43
+ return (tree) => {
44
+ visit(tree, "element", (node) => {
45
+ if (node.tagName !== "a") return;
46
+ const href = node.properties?.["href"];
47
+ if (typeof href !== "string" || !EXTERNAL_RE.test(href)) return;
48
+ node.properties["target"] = "_blank";
49
+ node.properties["rel"] = "noreferrer";
50
+ });
51
+ };
52
+ }
53
+ function rehypeResolveAssets(base) {
54
+ return (tree) => {
55
+ visit(tree, "element", (node) => {
56
+ for (const prop of ASSET_PROPS) {
57
+ const value = node.properties?.[prop];
58
+ if (typeof value !== "string" || value === "") continue;
59
+ if (ABSOLUTE_RE.test(value)) continue;
60
+ node.properties[prop] = resolveRelativePath(base, value);
61
+ }
62
+ });
63
+ };
64
+ }
65
+ function shikiOptions(codeTheme) {
66
+ const parts = codeTheme.split(/[,\s]+/).filter(Boolean);
67
+ if (parts.length >= 2) return {
68
+ themes: {
69
+ light: parts[0],
70
+ dark: parts[1]
71
+ },
72
+ defaultColor: false
73
+ };
74
+ return { theme: parts[0] ?? "github-dark" };
75
+ }
76
+ const BASE_LANGS = [
77
+ "ts",
78
+ "tsx",
79
+ "js",
80
+ "jsx",
81
+ "json",
82
+ "bash",
83
+ "css",
84
+ "html",
85
+ "md",
86
+ "python",
87
+ "go",
88
+ "rust"
89
+ ];
90
+ const highlighters = /* @__PURE__ */ new Map();
91
+ function getHighlighter(codeTheme) {
92
+ let cached = highlighters.get(codeTheme);
93
+ if (!cached) {
94
+ const opts = shikiOptions(codeTheme);
95
+ cached = createHighlighter({
96
+ themes: "themes" in opts ? [opts.themes.light, opts.themes.dark] : [opts.theme],
97
+ langs: BASE_LANGS
98
+ });
99
+ highlighters.set(codeTheme, cached);
100
+ }
101
+ return cached;
102
+ }
103
+ const FENCE_LANG_RE = /^[ \t]*(?:`{3,}|~{3,})[ \t]*([A-Za-z0-9_+-]+)/gm;
104
+ async function ensureLanguages(highlighter, markdown) {
105
+ const loaded = new Set(highlighter.getLoadedLanguages());
106
+ const wanted = /* @__PURE__ */ new Set();
107
+ for (const match of markdown.matchAll(FENCE_LANG_RE)) {
108
+ const lang = match[1].toLowerCase();
109
+ if (!loaded.has(lang) && lang in bundledLanguages) wanted.add(lang);
110
+ }
111
+ for (const lang of wanted) try {
112
+ await highlighter.loadLanguage(lang);
113
+ } catch {}
114
+ }
115
+ /**
116
+ * Converts Markdown to HTML with syntax highlighting, heading slugs,
117
+ * autolinked headings, and a collected table of contents.
118
+ *
119
+ * @param markdown - Raw Markdown source.
120
+ * @param config - Voxx config (used for `theme.codeTheme`).
121
+ * @param opts - Optional `assetBase` for resolving relative image paths.
122
+ */
123
+ const renderMarkdownEffect = (markdown, config, opts = {}) => Effect.tryPromise({
124
+ try: async () => {
125
+ const highlighter = await getHighlighter(config.theme.codeTheme);
126
+ await ensureLanguages(highlighter, markdown);
127
+ const processor = unified().use(remarkParse).use(remarkGfm).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw).use(rehypeSlug).use(rehypeAutolinkHeadings, { behavior: "wrap" }).use(rehypeExternalLinks).use(rehypeShikiFromHighlighter, highlighter, shikiOptions(config.theme.codeTheme));
128
+ if (opts.assetBase) processor.use(rehypeResolveAssets, opts.assetBase);
129
+ const file = await processor.use(rehypeCollectToc).use(rehypeStringify).process(markdown);
130
+ return {
131
+ html: String(file),
132
+ toc: file.data["toc"] ?? []
133
+ };
134
+ },
135
+ catch: (cause) => new RenderError({
136
+ message: `Failed to render markdown: ${String(cause)}`,
137
+ cause
138
+ })
139
+ });
140
+ //#endregion
141
+ export { renderMarkdownEffect };
142
+
143
+ //# sourceMappingURL=render.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.mjs","names":[],"sources":["../src/render.ts"],"sourcesContent":["import { Effect } from \"effect\";\nimport { unified } from \"unified\";\nimport remarkParse from \"remark-parse\";\nimport remarkGfm from \"remark-gfm\";\nimport remarkRehype from \"remark-rehype\";\nimport rehypeRaw from \"rehype-raw\";\nimport rehypeSlug from \"rehype-slug\";\nimport rehypeAutolinkHeadings from \"rehype-autolink-headings\";\nimport rehypeStringify from \"rehype-stringify\";\nimport rehypeShikiFromHighlighter from \"@shikijs/rehype/core\";\nimport {\n createHighlighter,\n bundledLanguages,\n type BundledLanguage,\n type Highlighter,\n} from \"shiki\";\nimport { visit } from \"unist-util-visit\";\nimport { toString } from \"hast-util-to-string\";\nimport type { Root } from \"hast\";\nimport { RenderError } from \"./errors\";\nimport type { TocItem, VoxxConfig } from \"./types\";\n\n/** Result returned by the Markdown renderer. */\nexport interface RenderResult {\n /** Rendered HTML string. */\n html: string;\n /** Extracted `h2`/`h3` headings for the table of contents. */\n toc: TocItem[];\n}\n\n/** Options passed to the Markdown renderer. */\nexport interface RenderOptions {\n /** URL prefix used to resolve relative `src` and `poster` attributes. */\n assetBase?: string;\n}\n\nfunction rehypeCollectToc() {\n return (tree: Root, file: { data: Record<string, unknown> }) => {\n const toc: TocItem[] = [];\n visit(tree, \"element\", (node) => {\n if (node.tagName !== \"h2\" && node.tagName !== \"h3\") return;\n const id = node.properties?.[\"id\"];\n if (typeof id !== \"string\") return;\n toc.push({\n id,\n text: toString(node),\n depth: node.tagName === \"h2\" ? 2 : 3,\n });\n });\n file.data[\"toc\"] = toc;\n };\n}\n\nconst EXTERNAL_RE = /^https?:\\/\\//i;\nconst ABSOLUTE_RE = /^(?:[a-z][a-z0-9+.-]*:|\\/|#|\\?)/i;\nconst ASSET_PROPS = [\"src\", \"poster\"] as const;\n\nfunction resolveRelativePath(base: string, rel: string): string {\n const segments = [...base.split(\"/\"), ...rel.split(\"/\")].filter(\n (s) => s !== \"\" && s !== \".\",\n );\n const out: string[] = [];\n for (const segment of segments) {\n if (segment === \"..\") out.pop();\n else out.push(segment);\n }\n return `/${out.join(\"/\")}`;\n}\n\nfunction rehypeExternalLinks() {\n return (tree: Root) => {\n visit(tree, \"element\", (node) => {\n if (node.tagName !== \"a\") return;\n const href = node.properties?.[\"href\"];\n if (typeof href !== \"string\" || !EXTERNAL_RE.test(href)) return;\n node.properties[\"target\"] = \"_blank\";\n node.properties[\"rel\"] = \"noreferrer\";\n });\n };\n}\n\nfunction rehypeResolveAssets(base: string) {\n return (tree: Root) => {\n visit(tree, \"element\", (node) => {\n for (const prop of ASSET_PROPS) {\n const value = node.properties?.[prop];\n if (typeof value !== \"string\" || value === \"\") continue;\n if (ABSOLUTE_RE.test(value)) continue;\n node.properties[prop] = resolveRelativePath(base, value);\n }\n });\n };\n}\n\ntype ShikiOptions =\n | { themes: { light: string; dark: string }; defaultColor: false }\n | { theme: string };\n\nfunction shikiOptions(codeTheme: string): ShikiOptions {\n const parts = codeTheme.split(/[,\\s]+/).filter(Boolean);\n if (parts.length >= 2) {\n return {\n themes: { light: parts[0]!, dark: parts[1]! },\n defaultColor: false as const,\n };\n }\n return { theme: parts[0] ?? \"github-dark\" };\n}\n\nconst BASE_LANGS: BundledLanguage[] = [\n \"ts\",\n \"tsx\",\n \"js\",\n \"jsx\",\n \"json\",\n \"bash\",\n \"css\",\n \"html\",\n \"md\",\n \"python\",\n \"go\",\n \"rust\",\n];\n\nconst highlighters = new Map<string, Promise<Highlighter>>();\n\nfunction getHighlighter(codeTheme: string): Promise<Highlighter> {\n let cached = highlighters.get(codeTheme);\n if (!cached) {\n const opts = shikiOptions(codeTheme);\n const themes =\n \"themes\" in opts\n ? [opts.themes.light, opts.themes.dark]\n : [(opts as { theme: string }).theme];\n cached = createHighlighter({ themes, langs: BASE_LANGS });\n highlighters.set(codeTheme, cached);\n }\n return cached;\n}\n\nconst FENCE_LANG_RE = /^[ \\t]*(?:`{3,}|~{3,})[ \\t]*([A-Za-z0-9_+-]+)/gm;\n\nasync function ensureLanguages(\n highlighter: Highlighter,\n markdown: string,\n): Promise<void> {\n const loaded = new Set(highlighter.getLoadedLanguages());\n const wanted = new Set<BundledLanguage>();\n for (const match of markdown.matchAll(FENCE_LANG_RE)) {\n const lang = match[1]!.toLowerCase();\n if (!loaded.has(lang) && lang in bundledLanguages) {\n wanted.add(lang as BundledLanguage);\n }\n }\n for (const lang of wanted) {\n try {\n await highlighter.loadLanguage(lang);\n } catch {}\n }\n}\n\n/**\n * Converts Markdown to HTML with syntax highlighting, heading slugs,\n * autolinked headings, and a collected table of contents.\n *\n * @param markdown - Raw Markdown source.\n * @param config - Voxx config (used for `theme.codeTheme`).\n * @param opts - Optional `assetBase` for resolving relative image paths.\n */\nexport const renderMarkdownEffect = (\n markdown: string,\n config: VoxxConfig,\n opts: RenderOptions = {},\n) =>\n Effect.tryPromise({\n try: async (): Promise<RenderResult> => {\n const highlighter = await getHighlighter(config.theme.codeTheme);\n await ensureLanguages(highlighter, markdown);\n const processor = unified()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkRehype, { allowDangerousHtml: true })\n .use(rehypeRaw)\n .use(rehypeSlug)\n .use(rehypeAutolinkHeadings, { behavior: \"wrap\" })\n .use(rehypeExternalLinks)\n .use(\n rehypeShikiFromHighlighter,\n highlighter,\n shikiOptions(config.theme.codeTheme),\n );\n if (opts.assetBase) processor.use(rehypeResolveAssets, opts.assetBase);\n const file = await processor\n .use(rehypeCollectToc)\n .use(rehypeStringify)\n .process(markdown);\n return {\n html: String(file),\n toc: (file.data[\"toc\"] as TocItem[] | undefined) ?? [],\n };\n },\n catch: (cause) =>\n new RenderError({\n message: `Failed to render markdown: ${String(cause)}`,\n cause,\n }),\n });\n"],"mappings":"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"}