@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
@@ -0,0 +1,98 @@
1
+ //#region src/util.d.ts
2
+ /**
3
+ * Converts a string to a URL-safe slug.
4
+ *
5
+ * Normalizes unicode, lowercases, trims, and replaces non-alphanumeric
6
+ * characters with hyphens.
7
+ */
8
+ declare function slugify(input: string): string;
9
+ /**
10
+ * Strips a `YYYY-MM-DD` prefix from a filename stem.
11
+ *
12
+ * @param name - Filename stem without extension.
13
+ * @returns The extracted ISO date and the remaining name, or just `rest` if no prefix found.
14
+ */
15
+ declare function splitDatePrefix(name: string): {
16
+ date?: string;
17
+ rest: string;
18
+ };
19
+ /**
20
+ * Strips a numeric order prefix (up to 4 digits) from a filename or directory stem.
21
+ *
22
+ * @param name - Filename stem without extension, e.g. `"01-getting-started"`.
23
+ * @returns The extracted order number and the remaining name.
24
+ */
25
+ declare function splitOrderPrefix(name: string): {
26
+ order?: number;
27
+ rest: string;
28
+ };
29
+ /**
30
+ * Converts a slug or filename stem to a human-readable title.
31
+ *
32
+ * @param segment - Slug segment, e.g. `"getting-started"`.
33
+ * @returns Title-cased string, e.g. `"Getting Started"`.
34
+ */
35
+ declare function humanize(segment: string): string;
36
+ /**
37
+ * Joins a base path and a slug into a clean URL path.
38
+ *
39
+ * Ensures exactly one leading slash and removes duplicate slashes.
40
+ */
41
+ declare function joinPath(base: string, slug: string): string;
42
+ /**
43
+ * Resolves a site-relative path to a full absolute URL.
44
+ *
45
+ * Returns the path unchanged if it already starts with `http(s)://`.
46
+ *
47
+ * @param siteUrl - Canonical site origin, e.g. `https://example.com`.
48
+ * @param path - Absolute or relative URL path.
49
+ */
50
+ declare function absoluteUrl(siteUrl: string, path: string): string;
51
+ /**
52
+ * Estimates reading time in minutes, assuming 200 words per minute.
53
+ * Always returns at least 1.
54
+ */
55
+ declare function readingTimeMinutes(markdown: string): number;
56
+ /**
57
+ * Extracts a plain-text excerpt from Markdown, stripping code blocks,
58
+ * headings, and inline markup.
59
+ *
60
+ * @param markdown - Raw Markdown source.
61
+ * @param max - Maximum character length before truncation. Defaults to `180`.
62
+ */
63
+ declare function deriveExcerpt(markdown: string, max?: number): string;
64
+ /**
65
+ * Formats an ISO 8601 date string for display.
66
+ *
67
+ * @param iso - ISO date string, e.g. `"2024-01-15"`.
68
+ * @param locale - BCP 47 locale. Defaults to `"en-US"`.
69
+ * @returns Formatted string like `"January 15, 2024"`, or the original string if invalid.
70
+ */
71
+ declare function formatDate(iso: string, locale?: string): string;
72
+ /**
73
+ * Parses a semver string from a filename stem, stripping any leading `v`.
74
+ *
75
+ * @param name - Filename stem, e.g. `"v1.2.3"` or `"1.0.0-rc.1"`.
76
+ * @returns Normalized version string, or `undefined` if not a valid version.
77
+ */
78
+ declare function parseVersion(name: string): string | undefined;
79
+ /**
80
+ * Compares two semver-like version strings numerically.
81
+ *
82
+ * @returns Negative if `a < b`, positive if `a > b`, zero if equal.
83
+ */
84
+ declare function compareVersions(a: string, b: string): number;
85
+ /**
86
+ * Escapes a string for safe inclusion in XML/HTML attribute values and text nodes.
87
+ */
88
+ declare function escapeXml(value: string): string;
89
+ /**
90
+ * Serializes a JSON-LD object to a string safe for inline `<script>` tags.
91
+ *
92
+ * Escapes `<`, `>`, `&`, and Unicode line/paragraph separators to prevent
93
+ * XSS and parser errors.
94
+ */
95
+ declare function serializeJsonLd(data: unknown): string;
96
+ //#endregion
97
+ export { absoluteUrl, compareVersions, deriveExcerpt, escapeXml, formatDate, humanize, joinPath, parseVersion, readingTimeMinutes, serializeJsonLd, slugify, splitDatePrefix, splitOrderPrefix };
98
+ //# sourceMappingURL=util.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.d.mts","names":[],"sources":["../src/util.ts"],"mappings":";;AAMA;;;;AAAqC;iBAArB,OAAA,CAAQ,KAAa;;;;;;;iBAkBrB,eAAA,CAAgB,IAAA;EAAiB,IAAA;EAAe,IAAA;AAAA;;;;;;;iBAchD,gBAAA,CAAiB,IAAA;EAC/B,KAAA;EACA,IAAA;AAAA;;AAasC;AAYxC;;;;iBAZgB,QAAA,CAAS,OAAe;AA2BxC;;;;AAAyD;AAAzD,iBAfgB,QAAA,CAAS,IAAA,UAAc,IAAY;;;;AA0BA;AAYnD;;;;iBAvBgB,WAAA,CAAY,OAAA,UAAiB,IAAY;AA6CzD;;;;AAAA,iBAlCgB,kBAAA,CAAmB,QAAgB;AAoDnD;;;;AAAyC;AAUzC;;AAVA,iBAxCgB,aAAA,CAAc,QAAA,UAAkB,GAAS;;AAkDL;AAwBpD;;;;AAAuC;iBApDvB,UAAA,CAAW,GAAA,UAAa,MAAgB;;;;AAmEX;;;iBAjD7B,YAAA,CAAa,IAAY;;;;;;iBAUzB,eAAA,CAAgB,CAAA,UAAW,CAAS;;;;iBAwBpC,SAAA,CAAU,KAAa;;;;;;;iBAevB,eAAA,CAAgB,IAAa"}
package/dist/util.mjs ADDED
@@ -0,0 +1,171 @@
1
+ //#region src/util.ts
2
+ /**
3
+ * Converts a string to a URL-safe slug.
4
+ *
5
+ * Normalizes unicode, lowercases, trims, and replaces non-alphanumeric
6
+ * characters with hyphens.
7
+ */
8
+ function slugify(input) {
9
+ return input.normalize("NFKD").replace(/[̀-ͯ]/g, "").toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
10
+ }
11
+ const DATE_PREFIX_RE = /^(\d{4})-(\d{2})-(\d{2})[-_.](.+)$/;
12
+ /**
13
+ * Strips a `YYYY-MM-DD` prefix from a filename stem.
14
+ *
15
+ * @param name - Filename stem without extension.
16
+ * @returns The extracted ISO date and the remaining name, or just `rest` if no prefix found.
17
+ */
18
+ function splitDatePrefix(name) {
19
+ const m = DATE_PREFIX_RE.exec(name);
20
+ if (!m) return { rest: name };
21
+ return {
22
+ date: `${m[1]}-${m[2]}-${m[3]}`,
23
+ rest: m[4]
24
+ };
25
+ }
26
+ const ORDER_PREFIX_RE = /^(\d{1,4})[-_.](.+)$/;
27
+ /**
28
+ * Strips a numeric order prefix (up to 4 digits) from a filename or directory stem.
29
+ *
30
+ * @param name - Filename stem without extension, e.g. `"01-getting-started"`.
31
+ * @returns The extracted order number and the remaining name.
32
+ */
33
+ function splitOrderPrefix(name) {
34
+ const m = ORDER_PREFIX_RE.exec(name);
35
+ if (!m) return { rest: name };
36
+ return {
37
+ order: Number(m[1]),
38
+ rest: m[2]
39
+ };
40
+ }
41
+ /**
42
+ * Converts a slug or filename stem to a human-readable title.
43
+ *
44
+ * @param segment - Slug segment, e.g. `"getting-started"`.
45
+ * @returns Title-cased string, e.g. `"Getting Started"`.
46
+ */
47
+ function humanize(segment) {
48
+ return segment.replace(/[-_]+/g, " ").replace(/\b\w/g, (ch) => ch.toUpperCase()).trim();
49
+ }
50
+ /**
51
+ * Joins a base path and a slug into a clean URL path.
52
+ *
53
+ * Ensures exactly one leading slash and removes duplicate slashes.
54
+ */
55
+ function joinPath(base, slug) {
56
+ const left = base.replace(/\/+$/, "");
57
+ const right = slug.replace(/^\/+/, "");
58
+ return `${left.startsWith("/") ? left : `/${left}`}/${right}`.replace(/([^:])\/{2,}/g, "$1/");
59
+ }
60
+ /**
61
+ * Resolves a site-relative path to a full absolute URL.
62
+ *
63
+ * Returns the path unchanged if it already starts with `http(s)://`.
64
+ *
65
+ * @param siteUrl - Canonical site origin, e.g. `https://example.com`.
66
+ * @param path - Absolute or relative URL path.
67
+ */
68
+ function absoluteUrl(siteUrl, path) {
69
+ if (/^https?:\/\//i.test(path)) return path;
70
+ return `${siteUrl.replace(/\/+$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
71
+ }
72
+ /**
73
+ * Estimates reading time in minutes, assuming 200 words per minute.
74
+ * Always returns at least 1.
75
+ */
76
+ function readingTimeMinutes(markdown) {
77
+ const words = markdown.trim().split(/\s+/).filter(Boolean).length;
78
+ return Math.max(1, Math.round(words / 200));
79
+ }
80
+ /**
81
+ * Extracts a plain-text excerpt from Markdown, stripping code blocks,
82
+ * headings, and inline markup.
83
+ *
84
+ * @param markdown - Raw Markdown source.
85
+ * @param max - Maximum character length before truncation. Defaults to `180`.
86
+ */
87
+ function deriveExcerpt(markdown, max = 180) {
88
+ const text = markdown.replace(/```[\s\S]*?```/g, " ").replace(/`[^`]*`/g, " ").replace(/^#{1,6}\s.*$/gm, " ").replace(/!\[[^\]]*\]\([^)]*\)/g, " ").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*_>#~]/g, " ").replace(/\s+/g, " ").replace(/\s+([.,;:!?])/g, "$1").trim();
89
+ if (text.length <= max) return text;
90
+ return `${text.slice(0, max).replace(/\s+\S*$/, "")}…`;
91
+ }
92
+ /**
93
+ * Formats an ISO 8601 date string for display.
94
+ *
95
+ * @param iso - ISO date string, e.g. `"2024-01-15"`.
96
+ * @param locale - BCP 47 locale. Defaults to `"en-US"`.
97
+ * @returns Formatted string like `"January 15, 2024"`, or the original string if invalid.
98
+ */
99
+ function formatDate(iso, locale = "en-US") {
100
+ const d = new Date(iso);
101
+ if (Number.isNaN(d.getTime())) return iso;
102
+ return d.toLocaleDateString(locale, {
103
+ year: "numeric",
104
+ month: "long",
105
+ day: "numeric"
106
+ });
107
+ }
108
+ const VERSION_RE = /^v?(\d+(?:\.\d+)*(?:[-.][\w.]+)?)$/i;
109
+ /**
110
+ * Parses a semver string from a filename stem, stripping any leading `v`.
111
+ *
112
+ * @param name - Filename stem, e.g. `"v1.2.3"` or `"1.0.0-rc.1"`.
113
+ * @returns Normalized version string, or `undefined` if not a valid version.
114
+ */
115
+ function parseVersion(name) {
116
+ const m = VERSION_RE.exec(name);
117
+ return m ? m[1] : void 0;
118
+ }
119
+ /**
120
+ * Compares two semver-like version strings numerically.
121
+ *
122
+ * @returns Negative if `a < b`, positive if `a > b`, zero if equal.
123
+ */
124
+ function compareVersions(a, b) {
125
+ const partsA = a.split(/[-.]/);
126
+ const partsB = b.split(/[-.]/);
127
+ const len = Math.max(partsA.length, partsB.length);
128
+ for (let i = 0; i < len; i++) {
129
+ const ai = partsA[i] ?? "";
130
+ const bi = partsB[i] ?? "";
131
+ const na = Number(ai);
132
+ const nb = Number(bi);
133
+ if (!Number.isNaN(na) && !Number.isNaN(nb)) {
134
+ if (na !== nb) return na - nb;
135
+ } else if (ai !== bi) {
136
+ if (ai === "") return 1;
137
+ if (bi === "") return -1;
138
+ return ai < bi ? -1 : 1;
139
+ }
140
+ }
141
+ return 0;
142
+ }
143
+ /**
144
+ * Escapes a string for safe inclusion in XML/HTML attribute values and text nodes.
145
+ */
146
+ function escapeXml(value) {
147
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
148
+ }
149
+ /**
150
+ * Serializes a JSON-LD object to a string safe for inline `<script>` tags.
151
+ *
152
+ * Escapes `<`, `>`, `&`, and Unicode line/paragraph separators to prevent
153
+ * XSS and parser errors.
154
+ */
155
+ function serializeJsonLd(data) {
156
+ return JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
157
+ }
158
+ /** Formats an ISO 8601 date string as RFC 822 (used in RSS feeds). */
159
+ function rfc822(date) {
160
+ const d = new Date(date);
161
+ return (isNaN(d.getTime()) ? /* @__PURE__ */ new Date() : d).toUTCString();
162
+ }
163
+ /** Formats an ISO 8601 date string as `YYYY-MM-DD` (used in sitemaps). */
164
+ function isoDate(date) {
165
+ const d = new Date(date);
166
+ return (isNaN(d.getTime()) ? /* @__PURE__ */ new Date() : d).toISOString().slice(0, 10);
167
+ }
168
+ //#endregion
169
+ export { absoluteUrl, compareVersions, deriveExcerpt, escapeXml, formatDate, humanize, isoDate, joinPath, parseVersion, readingTimeMinutes, rfc822, serializeJsonLd, slugify, splitDatePrefix, splitOrderPrefix };
170
+
171
+ //# sourceMappingURL=util.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.mjs","names":[],"sources":["../src/util.ts"],"sourcesContent":["/**\n * Converts a string to a URL-safe slug.\n *\n * Normalizes unicode, lowercases, trims, and replaces non-alphanumeric\n * characters with hyphens.\n */\nexport function slugify(input: string): string {\n return input\n .normalize(\"NFKD\")\n .replace(/[̀-ͯ]/g, \"\")\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n\nconst DATE_PREFIX_RE = /^(\\d{4})-(\\d{2})-(\\d{2})[-_.](.+)$/;\n\n/**\n * Strips a `YYYY-MM-DD` prefix from a filename stem.\n *\n * @param name - Filename stem without extension.\n * @returns The extracted ISO date and the remaining name, or just `rest` if no prefix found.\n */\nexport function splitDatePrefix(name: string): { date?: string; rest: string } {\n const m = DATE_PREFIX_RE.exec(name);\n if (!m) return { rest: name };\n return { date: `${m[1]}-${m[2]}-${m[3]}`, rest: m[4]! };\n}\n\nconst ORDER_PREFIX_RE = /^(\\d{1,4})[-_.](.+)$/;\n\n/**\n * Strips a numeric order prefix (up to 4 digits) from a filename or directory stem.\n *\n * @param name - Filename stem without extension, e.g. `\"01-getting-started\"`.\n * @returns The extracted order number and the remaining name.\n */\nexport function splitOrderPrefix(name: string): {\n order?: number;\n rest: string;\n} {\n const m = ORDER_PREFIX_RE.exec(name);\n if (!m) return { rest: name };\n return { order: Number(m[1]), rest: m[2]! };\n}\n\n/**\n * Converts a slug or filename stem to a human-readable title.\n *\n * @param segment - Slug segment, e.g. `\"getting-started\"`.\n * @returns Title-cased string, e.g. `\"Getting Started\"`.\n */\nexport function humanize(segment: string): string {\n return segment\n .replace(/[-_]+/g, \" \")\n .replace(/\\b\\w/g, (ch) => ch.toUpperCase())\n .trim();\n}\n\n/**\n * Joins a base path and a slug into a clean URL path.\n *\n * Ensures exactly one leading slash and removes duplicate slashes.\n */\nexport function joinPath(base: string, slug: string): string {\n const left = base.replace(/\\/+$/, \"\");\n const right = slug.replace(/^\\/+/, \"\");\n const prefix = left.startsWith(\"/\") ? left : `/${left}`;\n return `${prefix}/${right}`.replace(/([^:])\\/{2,}/g, \"$1/\");\n}\n\n/**\n * Resolves a site-relative path to a full absolute URL.\n *\n * Returns the path unchanged if it already starts with `http(s)://`.\n *\n * @param siteUrl - Canonical site origin, e.g. `https://example.com`.\n * @param path - Absolute or relative URL path.\n */\nexport function absoluteUrl(siteUrl: string, path: string): string {\n if (/^https?:\\/\\//i.test(path)) return path;\n const origin = siteUrl.replace(/\\/+$/, \"\");\n const rel = path.startsWith(\"/\") ? path : `/${path}`;\n return `${origin}${rel}`;\n}\n\n/**\n * Estimates reading time in minutes, assuming 200 words per minute.\n * Always returns at least 1.\n */\nexport function readingTimeMinutes(markdown: string): number {\n const words = markdown.trim().split(/\\s+/).filter(Boolean).length;\n return Math.max(1, Math.round(words / 200));\n}\n\n/**\n * Extracts a plain-text excerpt from Markdown, stripping code blocks,\n * headings, and inline markup.\n *\n * @param markdown - Raw Markdown source.\n * @param max - Maximum character length before truncation. Defaults to `180`.\n */\nexport function deriveExcerpt(markdown: string, max = 180): string {\n const text = markdown\n .replace(/```[\\s\\S]*?```/g, \" \")\n .replace(/`[^`]*`/g, \" \")\n .replace(/^#{1,6}\\s.*$/gm, \" \")\n .replace(/!\\[[^\\]]*\\]\\([^)]*\\)/g, \" \")\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, \"$1\")\n .replace(/[*_>#~]/g, \" \")\n .replace(/\\s+/g, \" \")\n .replace(/\\s+([.,;:!?])/g, \"$1\")\n .trim();\n if (text.length <= max) return text;\n return `${text.slice(0, max).replace(/\\s+\\S*$/, \"\")}…`;\n}\n\n/**\n * Formats an ISO 8601 date string for display.\n *\n * @param iso - ISO date string, e.g. `\"2024-01-15\"`.\n * @param locale - BCP 47 locale. Defaults to `\"en-US\"`.\n * @returns Formatted string like `\"January 15, 2024\"`, or the original string if invalid.\n */\nexport function formatDate(iso: string, locale = \"en-US\"): string {\n const d = new Date(iso);\n if (Number.isNaN(d.getTime())) return iso;\n return d.toLocaleDateString(locale, {\n year: \"numeric\",\n month: \"long\",\n day: \"numeric\",\n });\n}\n\nconst VERSION_RE = /^v?(\\d+(?:\\.\\d+)*(?:[-.][\\w.]+)?)$/i;\n\n/**\n * Parses a semver string from a filename stem, stripping any leading `v`.\n *\n * @param name - Filename stem, e.g. `\"v1.2.3\"` or `\"1.0.0-rc.1\"`.\n * @returns Normalized version string, or `undefined` if not a valid version.\n */\nexport function parseVersion(name: string): string | undefined {\n const m = VERSION_RE.exec(name);\n return m ? m[1] : undefined;\n}\n\n/**\n * Compares two semver-like version strings numerically.\n *\n * @returns Negative if `a < b`, positive if `a > b`, zero if equal.\n */\nexport function compareVersions(a: string, b: string): number {\n const partsA = a.split(/[-.]/);\n const partsB = b.split(/[-.]/);\n const len = Math.max(partsA.length, partsB.length);\n for (let i = 0; i < len; i++) {\n const ai = partsA[i] ?? \"\";\n const bi = partsB[i] ?? \"\";\n const na = Number(ai);\n const nb = Number(bi);\n const bothNumeric = !Number.isNaN(na) && !Number.isNaN(nb);\n if (bothNumeric) {\n if (na !== nb) return na - nb;\n } else if (ai !== bi) {\n if (ai === \"\") return 1;\n if (bi === \"\") return -1;\n return ai < bi ? -1 : 1;\n }\n }\n return 0;\n}\n\n/**\n * Escapes a string for safe inclusion in XML/HTML attribute values and text nodes.\n */\nexport function escapeXml(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&apos;\");\n}\n\n/**\n * Serializes a JSON-LD object to a string safe for inline `<script>` tags.\n *\n * Escapes `<`, `>`, `&`, and Unicode line/paragraph separators to prevent\n * XSS and parser errors.\n */\nexport function serializeJsonLd(data: unknown): string {\n return JSON.stringify(data)\n .replace(/</g, \"\\\\u003c\")\n .replace(/>/g, \"\\\\u003e\")\n .replace(/&/g, \"\\\\u0026\")\n .replace(/\\u2028/g, \"\\\\u2028\")\n .replace(/\\u2029/g, \"\\\\u2029\");\n}\n\n/** Formats an ISO 8601 date string as RFC 822 (used in RSS feeds). */\nexport function rfc822(date: string): string {\n const d = new Date(date);\n return (isNaN(d.getTime()) ? new Date() : d).toUTCString();\n}\n\n/** Formats an ISO 8601 date string as `YYYY-MM-DD` (used in sitemaps). */\nexport function isoDate(date: string): string {\n const d = new Date(date);\n return (isNaN(d.getTime()) ? new Date() : d).toISOString().slice(0, 10);\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,106 @@
1
+ {
2
+ "name": "@prudentbird/voxx-core",
3
+ "version": "1.0.0",
4
+ "description": "The portable content engine behind Voxx: markdown + frontmatter -> SEO-ready blogs, docs, and changelogs.",
5
+ "license": "MIT",
6
+ "author": "prudentbird <me@prudentbird.com> (https://prudentbird.com)",
7
+ "homepage": "https://github.com/prudentbird/voxx#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/prudentbird/voxx.git",
11
+ "directory": "packages/core"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/prudentbird/voxx/issues"
15
+ },
16
+ "keywords": [
17
+ "cms",
18
+ "markdown",
19
+ "blog",
20
+ "docs",
21
+ "changelog",
22
+ "static-site",
23
+ "seo",
24
+ "rss",
25
+ "llms-txt",
26
+ "effect"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "type": "module",
32
+ "engines": {
33
+ "node": ">=20"
34
+ },
35
+ "files": [
36
+ "dist",
37
+ "theme",
38
+ "voxx.schema.json",
39
+ "LICENSE"
40
+ ],
41
+ "exports": {
42
+ ".": {
43
+ "import": {
44
+ "types": "./dist/index.d.mts",
45
+ "default": "./dist/index.mjs"
46
+ },
47
+ "require": {
48
+ "types": "./dist/index.d.cts",
49
+ "default": "./dist/index.cjs"
50
+ }
51
+ },
52
+ "./effect": {
53
+ "import": {
54
+ "types": "./dist/effect.d.mts",
55
+ "default": "./dist/effect.mjs"
56
+ },
57
+ "require": {
58
+ "types": "./dist/effect.d.cts",
59
+ "default": "./dist/effect.cjs"
60
+ }
61
+ },
62
+ "./theme/voxx.css": "./theme/voxx.css",
63
+ "./theme/demo-globals.css": "./theme/demo-globals.css",
64
+ "./voxx.schema.json": "./voxx.schema.json",
65
+ "./package.json": "./package.json"
66
+ },
67
+ "dependencies": {
68
+ "@effect/platform": "^0.96.1",
69
+ "@effect/platform-node": "^0.107.0",
70
+ "@shikijs/rehype": "^4.2.0",
71
+ "effect": "^3.21.3",
72
+ "gray-matter": "^4.0.3",
73
+ "hast-util-to-string": "^3.0.1",
74
+ "rehype-autolink-headings": "^7.1.0",
75
+ "rehype-raw": "^7.0.0",
76
+ "rehype-slug": "^6.0.0",
77
+ "rehype-stringify": "^10.0.1",
78
+ "remark-gfm": "^4.0.1",
79
+ "remark-parse": "^11.0.0",
80
+ "remark-rehype": "^11.1.2",
81
+ "shiki": "^4.2.0",
82
+ "unified": "^11.0.5",
83
+ "unist-util-visit": "^5.1.0"
84
+ },
85
+ "devDependencies": {
86
+ "@types/hast": "^3.0.4",
87
+ "@types/mdast": "^4.0.4",
88
+ "@types/node": "^25.9.3",
89
+ "@typescript/native-preview": "7.0.0-dev.20260610.1",
90
+ "eslint": "^10.0.2",
91
+ "jiti": "^2.7.0",
92
+ "tsdown": "^0.22.2",
93
+ "typescript": "^6.0.3",
94
+ "vitest": "^4.1.8",
95
+ "@voxx/typescript-config": "0.0.1",
96
+ "@voxx/eslint-config": "0.0.1"
97
+ },
98
+ "scripts": {
99
+ "build": "tsdown && node ./scripts/gen-schema.ts",
100
+ "dev": "tsdown --watch",
101
+ "lint": "eslint .",
102
+ "lint:fix": "eslint --fix .",
103
+ "typecheck": "tsgo --pretty --noEmit",
104
+ "test": "vitest run"
105
+ }
106
+ }
@@ -0,0 +1,61 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ :root {
8
+ --background: #ffffff;
9
+ --foreground: #09090b;
10
+ --muted: #f4f4f5;
11
+ --muted-foreground: #71717a;
12
+ --border: #e4e4e7;
13
+ --input: #e4e4e7;
14
+ --primary: #18181b;
15
+ --primary-foreground: #fafafa;
16
+ --accent: #f4f4f5;
17
+ --accent-foreground: #18181b;
18
+ --ring: #18181b;
19
+ --radius: 0.5rem;
20
+ }
21
+
22
+ .dark {
23
+ --background: #09090b;
24
+ --foreground: #fafafa;
25
+ --muted: #18181b;
26
+ --muted-foreground: #a1a1aa;
27
+ --border: #27272a;
28
+ --input: #27272a;
29
+ --primary: #fafafa;
30
+ --primary-foreground: #18181b;
31
+ --accent: #27272a;
32
+ --accent-foreground: #fafafa;
33
+ --ring: #d4d4d8;
34
+ }
35
+
36
+ @media (prefers-color-scheme: dark) {
37
+ :root:not(.light) {
38
+ --background: #09090b;
39
+ --foreground: #fafafa;
40
+ --muted: #18181b;
41
+ --muted-foreground: #a1a1aa;
42
+ --border: #27272a;
43
+ --input: #27272a;
44
+ --primary: #fafafa;
45
+ --primary-foreground: #18181b;
46
+ --accent: #27272a;
47
+ --accent-foreground: #fafafa;
48
+ --ring: #d4d4d8;
49
+ }
50
+ }
51
+
52
+ html {
53
+ scroll-behavior: smooth;
54
+ }
55
+
56
+ body {
57
+ margin: 0;
58
+ background: var(--background);
59
+ color: var(--foreground);
60
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
61
+ }