@autumnsgrove/groveengine 0.1.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 (219) hide show
  1. package/README.md +163 -0
  2. package/dist/auth/jwt.d.ts +14 -0
  3. package/dist/auth/jwt.js +109 -0
  4. package/dist/auth/session.d.ts +42 -0
  5. package/dist/auth/session.js +105 -0
  6. package/dist/components/admin/GutterManager.svelte +910 -0
  7. package/dist/components/admin/GutterManager.svelte.d.ts +15 -0
  8. package/dist/components/admin/MarkdownEditor.svelte +3114 -0
  9. package/dist/components/admin/MarkdownEditor.svelte.d.ts +43 -0
  10. package/dist/components/custom/CollapsibleSection.svelte +74 -0
  11. package/dist/components/custom/CollapsibleSection.svelte.d.ts +15 -0
  12. package/dist/components/custom/ContentWithGutter.svelte +646 -0
  13. package/dist/components/custom/ContentWithGutter.svelte.d.ts +19 -0
  14. package/dist/components/custom/GutterItem.svelte +201 -0
  15. package/dist/components/custom/GutterItem.svelte.d.ts +11 -0
  16. package/dist/components/custom/LeftGutter.svelte +271 -0
  17. package/dist/components/custom/LeftGutter.svelte.d.ts +17 -0
  18. package/dist/components/custom/MobileTOC.svelte +273 -0
  19. package/dist/components/custom/MobileTOC.svelte.d.ts +11 -0
  20. package/dist/components/custom/TableOfContents.svelte +163 -0
  21. package/dist/components/custom/TableOfContents.svelte.d.ts +11 -0
  22. package/dist/components/gallery/ImageGallery.svelte +681 -0
  23. package/dist/components/gallery/ImageGallery.svelte.d.ts +11 -0
  24. package/dist/components/gallery/Lightbox.svelte +107 -0
  25. package/dist/components/gallery/Lightbox.svelte.d.ts +19 -0
  26. package/dist/components/gallery/LightboxCaption.svelte +25 -0
  27. package/dist/components/gallery/LightboxCaption.svelte.d.ts +11 -0
  28. package/dist/components/gallery/ZoomableImage.svelte +163 -0
  29. package/dist/components/gallery/ZoomableImage.svelte.d.ts +17 -0
  30. package/dist/components/ui/Accordion.svelte +74 -0
  31. package/dist/components/ui/Accordion.svelte.d.ts +42 -0
  32. package/dist/components/ui/Badge.svelte +48 -0
  33. package/dist/components/ui/Badge.svelte.d.ts +26 -0
  34. package/dist/components/ui/Button.svelte +74 -0
  35. package/dist/components/ui/Button.svelte.d.ts +34 -0
  36. package/dist/components/ui/Card.svelte +102 -0
  37. package/dist/components/ui/Card.svelte.d.ts +46 -0
  38. package/dist/components/ui/Dialog.svelte +91 -0
  39. package/dist/components/ui/Dialog.svelte.d.ts +43 -0
  40. package/dist/components/ui/Input.svelte +81 -0
  41. package/dist/components/ui/Input.svelte.d.ts +35 -0
  42. package/dist/components/ui/Select.svelte +69 -0
  43. package/dist/components/ui/Select.svelte.d.ts +36 -0
  44. package/dist/components/ui/Sheet.svelte +98 -0
  45. package/dist/components/ui/Sheet.svelte.d.ts +45 -0
  46. package/dist/components/ui/Skeleton.svelte +31 -0
  47. package/dist/components/ui/Skeleton.svelte.d.ts +26 -0
  48. package/dist/components/ui/Table.svelte +59 -0
  49. package/dist/components/ui/Table.svelte.d.ts +44 -0
  50. package/dist/components/ui/Tabs.svelte +76 -0
  51. package/dist/components/ui/Tabs.svelte.d.ts +41 -0
  52. package/dist/components/ui/Textarea.svelte +81 -0
  53. package/dist/components/ui/Textarea.svelte.d.ts +35 -0
  54. package/dist/components/ui/Toast.svelte +18 -0
  55. package/dist/components/ui/Toast.svelte.d.ts +7 -0
  56. package/dist/components/ui/accordion/accordion-content.svelte +24 -0
  57. package/dist/components/ui/accordion/accordion-content.svelte.d.ts +4 -0
  58. package/dist/components/ui/accordion/accordion-item.svelte +12 -0
  59. package/dist/components/ui/accordion/accordion-item.svelte.d.ts +4 -0
  60. package/dist/components/ui/accordion/accordion-trigger.svelte +29 -0
  61. package/dist/components/ui/accordion/accordion-trigger.svelte.d.ts +7 -0
  62. package/dist/components/ui/accordion/index.d.ts +6 -0
  63. package/dist/components/ui/accordion/index.js +8 -0
  64. package/dist/components/ui/badge/badge.svelte +50 -0
  65. package/dist/components/ui/badge/badge.svelte.d.ts +60 -0
  66. package/dist/components/ui/badge/index.d.ts +2 -0
  67. package/dist/components/ui/badge/index.js +2 -0
  68. package/dist/components/ui/button/button.svelte +82 -0
  69. package/dist/components/ui/button/button.svelte.d.ts +132 -0
  70. package/dist/components/ui/button/index.d.ts +2 -0
  71. package/dist/components/ui/button/index.js +4 -0
  72. package/dist/components/ui/card/card-content.svelte +16 -0
  73. package/dist/components/ui/card/card-content.svelte.d.ts +5 -0
  74. package/dist/components/ui/card/card-description.svelte +16 -0
  75. package/dist/components/ui/card/card-description.svelte.d.ts +5 -0
  76. package/dist/components/ui/card/card-footer.svelte +16 -0
  77. package/dist/components/ui/card/card-footer.svelte.d.ts +5 -0
  78. package/dist/components/ui/card/card-header.svelte +16 -0
  79. package/dist/components/ui/card/card-header.svelte.d.ts +5 -0
  80. package/dist/components/ui/card/card-title.svelte +25 -0
  81. package/dist/components/ui/card/card-title.svelte.d.ts +8 -0
  82. package/dist/components/ui/card/card.svelte +20 -0
  83. package/dist/components/ui/card/card.svelte.d.ts +5 -0
  84. package/dist/components/ui/card/index.d.ts +7 -0
  85. package/dist/components/ui/card/index.js +9 -0
  86. package/dist/components/ui/dialog/dialog-content.svelte +38 -0
  87. package/dist/components/ui/dialog/dialog-content.svelte.d.ts +9 -0
  88. package/dist/components/ui/dialog/dialog-description.svelte +16 -0
  89. package/dist/components/ui/dialog/dialog-description.svelte.d.ts +4 -0
  90. package/dist/components/ui/dialog/dialog-footer.svelte +20 -0
  91. package/dist/components/ui/dialog/dialog-footer.svelte.d.ts +5 -0
  92. package/dist/components/ui/dialog/dialog-header.svelte +20 -0
  93. package/dist/components/ui/dialog/dialog-header.svelte.d.ts +5 -0
  94. package/dist/components/ui/dialog/dialog-overlay.svelte +19 -0
  95. package/dist/components/ui/dialog/dialog-overlay.svelte.d.ts +4 -0
  96. package/dist/components/ui/dialog/dialog-title.svelte +16 -0
  97. package/dist/components/ui/dialog/dialog-title.svelte.d.ts +4 -0
  98. package/dist/components/ui/dialog/index.d.ts +12 -0
  99. package/dist/components/ui/dialog/index.js +14 -0
  100. package/dist/components/ui/index.d.ts +26 -0
  101. package/dist/components/ui/index.js +29 -0
  102. package/dist/components/ui/input/index.d.ts +2 -0
  103. package/dist/components/ui/input/index.js +4 -0
  104. package/dist/components/ui/input/input.svelte +46 -0
  105. package/dist/components/ui/input/input.svelte.d.ts +13 -0
  106. package/dist/components/ui/select/index.d.ts +11 -0
  107. package/dist/components/ui/select/index.js +13 -0
  108. package/dist/components/ui/select/select-content.svelte +39 -0
  109. package/dist/components/ui/select/select-content.svelte.d.ts +7 -0
  110. package/dist/components/ui/select/select-group-heading.svelte +16 -0
  111. package/dist/components/ui/select/select-group-heading.svelte.d.ts +4 -0
  112. package/dist/components/ui/select/select-item.svelte +37 -0
  113. package/dist/components/ui/select/select-item.svelte.d.ts +4 -0
  114. package/dist/components/ui/select/select-scroll-down-button.svelte +19 -0
  115. package/dist/components/ui/select/select-scroll-down-button.svelte.d.ts +4 -0
  116. package/dist/components/ui/select/select-scroll-up-button.svelte +19 -0
  117. package/dist/components/ui/select/select-scroll-up-button.svelte.d.ts +4 -0
  118. package/dist/components/ui/select/select-separator.svelte +13 -0
  119. package/dist/components/ui/select/select-separator.svelte.d.ts +4 -0
  120. package/dist/components/ui/select/select-trigger.svelte +24 -0
  121. package/dist/components/ui/select/select-trigger.svelte.d.ts +4 -0
  122. package/dist/components/ui/separator/index.d.ts +2 -0
  123. package/dist/components/ui/separator/index.js +4 -0
  124. package/dist/components/ui/separator/separator.svelte +22 -0
  125. package/dist/components/ui/separator/separator.svelte.d.ts +4 -0
  126. package/dist/components/ui/sheet/index.d.ts +12 -0
  127. package/dist/components/ui/sheet/index.js +14 -0
  128. package/dist/components/ui/sheet/sheet-content.svelte +53 -0
  129. package/dist/components/ui/sheet/sheet-content.svelte.d.ts +62 -0
  130. package/dist/components/ui/sheet/sheet-description.svelte +16 -0
  131. package/dist/components/ui/sheet/sheet-description.svelte.d.ts +4 -0
  132. package/dist/components/ui/sheet/sheet-footer.svelte +20 -0
  133. package/dist/components/ui/sheet/sheet-footer.svelte.d.ts +5 -0
  134. package/dist/components/ui/sheet/sheet-header.svelte +20 -0
  135. package/dist/components/ui/sheet/sheet-header.svelte.d.ts +5 -0
  136. package/dist/components/ui/sheet/sheet-overlay.svelte +21 -0
  137. package/dist/components/ui/sheet/sheet-overlay.svelte.d.ts +6 -0
  138. package/dist/components/ui/sheet/sheet-title.svelte +16 -0
  139. package/dist/components/ui/sheet/sheet-title.svelte.d.ts +4 -0
  140. package/dist/components/ui/skeleton/index.d.ts +2 -0
  141. package/dist/components/ui/skeleton/index.js +4 -0
  142. package/dist/components/ui/skeleton/skeleton.svelte +17 -0
  143. package/dist/components/ui/skeleton/skeleton.svelte.d.ts +5 -0
  144. package/dist/components/ui/table/index.d.ts +9 -0
  145. package/dist/components/ui/table/index.js +11 -0
  146. package/dist/components/ui/table/table-body.svelte +16 -0
  147. package/dist/components/ui/table/table-body.svelte.d.ts +5 -0
  148. package/dist/components/ui/table/table-caption.svelte +16 -0
  149. package/dist/components/ui/table/table-caption.svelte.d.ts +5 -0
  150. package/dist/components/ui/table/table-cell.svelte +20 -0
  151. package/dist/components/ui/table/table-cell.svelte.d.ts +5 -0
  152. package/dist/components/ui/table/table-footer.svelte +16 -0
  153. package/dist/components/ui/table/table-footer.svelte.d.ts +5 -0
  154. package/dist/components/ui/table/table-head.svelte +23 -0
  155. package/dist/components/ui/table/table-head.svelte.d.ts +5 -0
  156. package/dist/components/ui/table/table-header.svelte +16 -0
  157. package/dist/components/ui/table/table-header.svelte.d.ts +5 -0
  158. package/dist/components/ui/table/table-row.svelte +23 -0
  159. package/dist/components/ui/table/table-row.svelte.d.ts +5 -0
  160. package/dist/components/ui/table/table.svelte +18 -0
  161. package/dist/components/ui/table/table.svelte.d.ts +5 -0
  162. package/dist/components/ui/tabs/index.d.ts +6 -0
  163. package/dist/components/ui/tabs/index.js +8 -0
  164. package/dist/components/ui/tabs/tabs-content.svelte +19 -0
  165. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
  166. package/dist/components/ui/tabs/tabs-list.svelte +19 -0
  167. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
  168. package/dist/components/ui/tabs/tabs-trigger.svelte +19 -0
  169. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  170. package/dist/components/ui/textarea/index.d.ts +2 -0
  171. package/dist/components/ui/textarea/index.js +4 -0
  172. package/dist/components/ui/textarea/textarea.svelte +24 -0
  173. package/dist/components/ui/textarea/textarea.svelte.d.ts +6 -0
  174. package/dist/components/ui/toast.d.ts +86 -0
  175. package/dist/components/ui/toast.js +99 -0
  176. package/dist/db/schema.sql +238 -0
  177. package/dist/index.d.ts +14 -0
  178. package/dist/index.js +20 -0
  179. package/dist/payments/index.d.ts +33 -0
  180. package/dist/payments/index.js +47 -0
  181. package/dist/payments/shop.d.ts +165 -0
  182. package/dist/payments/shop.js +588 -0
  183. package/dist/payments/stripe/client.d.ts +231 -0
  184. package/dist/payments/stripe/client.js +198 -0
  185. package/dist/payments/stripe/index.d.ts +18 -0
  186. package/dist/payments/stripe/index.js +17 -0
  187. package/dist/payments/stripe/provider.d.ts +50 -0
  188. package/dist/payments/stripe/provider.js +530 -0
  189. package/dist/payments/types.d.ts +355 -0
  190. package/dist/payments/types.js +7 -0
  191. package/dist/server/logger.d.ts +53 -0
  192. package/dist/server/logger.js +252 -0
  193. package/dist/styles/content.css +514 -0
  194. package/dist/styles/tokens.css +175 -0
  195. package/dist/utils/api.d.ts +20 -0
  196. package/dist/utils/api.js +109 -0
  197. package/dist/utils/cn.d.ts +15 -0
  198. package/dist/utils/cn.js +18 -0
  199. package/dist/utils/csrf.d.ts +22 -0
  200. package/dist/utils/csrf.js +72 -0
  201. package/dist/utils/debounce.d.ts +7 -0
  202. package/dist/utils/debounce.js +14 -0
  203. package/dist/utils/gallery.d.ts +66 -0
  204. package/dist/utils/gallery.js +181 -0
  205. package/dist/utils/gutter.d.ts +54 -0
  206. package/dist/utils/gutter.js +169 -0
  207. package/dist/utils/imageProcessor.d.ts +58 -0
  208. package/dist/utils/imageProcessor.js +205 -0
  209. package/dist/utils/json.d.ts +17 -0
  210. package/dist/utils/json.js +26 -0
  211. package/dist/utils/markdown.d.ts +101 -0
  212. package/dist/utils/markdown.js +947 -0
  213. package/dist/utils/sanitize.d.ts +25 -0
  214. package/dist/utils/sanitize.js +127 -0
  215. package/dist/utils/validation.d.ts +46 -0
  216. package/dist/utils/validation.js +169 -0
  217. package/dist/utils.d.ts +5 -0
  218. package/dist/utils.js +5 -0
  219. package/package.json +129 -0
@@ -0,0 +1,947 @@
1
+ import { marked } from "marked";
2
+ import matter from "gray-matter";
3
+ import mermaid from "mermaid";
4
+ import { sanitizeSVG, sanitizeMarkdown } from './sanitize.js';
5
+
6
+ // Configure Mermaid
7
+ mermaid.initialize({
8
+ startOnLoad: false,
9
+ theme: "default",
10
+ securityLevel: "strict",
11
+ });
12
+
13
+ // Configure marked renderer for GitHub-style code blocks
14
+ const renderer = new marked.Renderer();
15
+ renderer.code = function (token) {
16
+ // Handle both old (code, language) and new (token) API signatures
17
+ const code = typeof token === "string" ? token : token.text;
18
+ const language = typeof token === "string" ? arguments[1] : token.lang;
19
+
20
+ const lang = language || "text";
21
+
22
+ // Render markdown/md code blocks as formatted HTML (like GitHub)
23
+ if (lang === "markdown" || lang === "md") {
24
+ // Parse the markdown content and render it
25
+ const renderedContent = marked.parse(code);
26
+ // Escape the raw markdown for the copy button
27
+ const escapedCode = code
28
+ .replace(/&/g, "&")
29
+ .replace(/</g, "&lt;")
30
+ .replace(/>/g, "&gt;")
31
+ .replace(/"/g, "&quot;")
32
+ .replace(/'/g, "&#39;");
33
+
34
+ return `<div class="rendered-markdown-block">
35
+ <div class="rendered-markdown-header">
36
+ <span class="rendered-markdown-label">Markdown</span>
37
+ <button class="code-block-copy" aria-label="Copy markdown to clipboard" data-code="${escapedCode}">
38
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
39
+ <path d="M5.75 4.75H10.25V1.75H5.75V4.75ZM5.75 4.75H2.75V14.25H10.25V11.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
40
+ <rect x="5.75" y="4.75" width="7.5" height="9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
41
+ </svg>
42
+ <span class="copy-text">Copy</span>
43
+ </button>
44
+ </div>
45
+ <div class="rendered-markdown-content">
46
+ ${renderedContent}
47
+ </div>
48
+ </div>`;
49
+ }
50
+
51
+ const escapedCode = code
52
+ .replace(/&/g, "&amp;")
53
+ .replace(/</g, "&lt;")
54
+ .replace(/>/g, "&gt;")
55
+ .replace(/"/g, "&quot;")
56
+ .replace(/'/g, "&#39;");
57
+
58
+ return `<div class="code-block-wrapper">
59
+ <div class="code-block-header">
60
+ <span class="code-block-language">${lang}</span>
61
+ <button class="code-block-copy" aria-label="Copy code to clipboard" data-code="${escapedCode}">
62
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
63
+ <path d="M5.75 4.75H10.25V1.75H5.75V4.75ZM5.75 4.75H2.75V14.25H10.25V11.25" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
64
+ <rect x="5.75" y="4.75" width="7.5" height="9.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
65
+ </svg>
66
+ <span class="copy-text">Copy</span>
67
+ </button>
68
+ </div>
69
+ <pre><code class="language-${lang}">${escapedCode}</code></pre>
70
+ </div>`;
71
+ };
72
+
73
+ marked.setOptions({
74
+ renderer: renderer,
75
+ gfm: true,
76
+ breaks: false,
77
+ });
78
+
79
+ // Use Vite's import.meta.glob to load markdown files at build time
80
+ // This works in both dev and production (including Cloudflare Workers)
81
+ // Path is relative to project root - now using UserContent directory
82
+
83
+ // Posts - Using absolute path from project root for Cloudflare Pages compatibility
84
+ const modules = import.meta.glob("/UserContent/Posts/*.md", {
85
+ eager: true,
86
+ query: "?raw",
87
+ import: "default",
88
+ });
89
+
90
+ // Recipes
91
+ const recipeModules = import.meta.glob("../../../UserContent/Recipes/*.md", {
92
+ eager: true,
93
+ query: "?raw",
94
+ import: "default",
95
+ });
96
+
97
+ // About
98
+ const aboutModules = import.meta.glob("../../../UserContent/About/*.md", {
99
+ eager: true,
100
+ query: "?raw",
101
+ import: "default",
102
+ });
103
+
104
+ // Home
105
+ const homeModules = import.meta.glob("../../../UserContent/Home/*.md", {
106
+ eager: true,
107
+ query: "?raw",
108
+ import: "default",
109
+ });
110
+
111
+ // Contact
112
+ const contactModules = import.meta.glob("../../../UserContent/Contact/*.md", {
113
+ eager: true,
114
+ query: "?raw",
115
+ import: "default",
116
+ });
117
+
118
+ // Site config
119
+ const siteConfigModule = import.meta.glob(
120
+ "../../../UserContent/site-config.json",
121
+ {
122
+ eager: true,
123
+ },
124
+ );
125
+
126
+ /**
127
+ * Get the site configuration
128
+ * @returns {Object} Site configuration object
129
+ */
130
+ export function getSiteConfig() {
131
+ const entry = Object.entries(siteConfigModule)[0];
132
+ if (entry) {
133
+ return entry[1].default || entry[1];
134
+ }
135
+ return {
136
+ owner: { name: "Admin", email: "" },
137
+ site: { title: "The Grove", description: "", copyright: "AutumnsGrove" },
138
+ social: {},
139
+ };
140
+ }
141
+
142
+ // Load recipe metadata JSON files (step icons, etc.)
143
+ const recipeMetadataModules = import.meta.glob(
144
+ "../../../UserContent/Recipes/*/gutter/recipe.json",
145
+ {
146
+ eager: true,
147
+ },
148
+ );
149
+
150
+ // Load gutter manifest files for blog posts
151
+ const gutterManifestModules = import.meta.glob(
152
+ "../../../UserContent/Posts/*/gutter/manifest.json",
153
+ {
154
+ eager: true,
155
+ },
156
+ );
157
+
158
+ // Load gutter markdown content files
159
+ const gutterMarkdownModules = import.meta.glob(
160
+ "../../../UserContent/Posts/*/gutter/*.md",
161
+ {
162
+ eager: true,
163
+ query: "?raw",
164
+ import: "default",
165
+ },
166
+ );
167
+
168
+ // Load gutter image files
169
+ const gutterImageModules = import.meta.glob(
170
+ "../../../UserContent/Posts/*/gutter/*.{jpg,jpeg,png,gif,webp}",
171
+ {
172
+ eager: true,
173
+ query: "?url",
174
+ import: "default",
175
+ },
176
+ );
177
+
178
+ // Load about page gutter manifest files
179
+ const aboutGutterManifestModules = import.meta.glob(
180
+ "../../../UserContent/About/*/gutter/manifest.json",
181
+ {
182
+ eager: true,
183
+ },
184
+ );
185
+
186
+ // Load about page gutter markdown content files
187
+ const aboutGutterMarkdownModules = import.meta.glob(
188
+ "../../../UserContent/About/*/gutter/*.md",
189
+ {
190
+ eager: true,
191
+ query: "?raw",
192
+ import: "default",
193
+ },
194
+ );
195
+
196
+ // Load about page gutter image files
197
+ const aboutGutterImageModules = import.meta.glob(
198
+ "../../../UserContent/About/*/gutter/*.{jpg,jpeg,png,gif,webp}",
199
+ {
200
+ eager: true,
201
+ query: "?url",
202
+ import: "default",
203
+ },
204
+ );
205
+
206
+ // Load recipe gutter manifest files
207
+ const recipeGutterManifestModules = import.meta.glob(
208
+ "../../../UserContent/Recipes/*/gutter/manifest.json",
209
+ {
210
+ eager: true,
211
+ },
212
+ );
213
+
214
+ // Load recipe gutter markdown content files
215
+ const recipeGutterMarkdownModules = import.meta.glob(
216
+ "../../../UserContent/Recipes/*/gutter/*.md",
217
+ {
218
+ eager: true,
219
+ query: "?raw",
220
+ import: "default",
221
+ },
222
+ );
223
+
224
+ // Load recipe gutter image files
225
+ const recipeGutterImageModules = import.meta.glob(
226
+ "../../../UserContent/Recipes/*/gutter/*.{jpg,jpeg,png,gif,webp}",
227
+ {
228
+ eager: true,
229
+ query: "?url",
230
+ import: "default",
231
+ },
232
+ );
233
+
234
+ // Load home page gutter manifest files
235
+ const homeGutterManifestModules = import.meta.glob(
236
+ "../../../UserContent/Home/*/gutter/manifest.json",
237
+ {
238
+ eager: true,
239
+ },
240
+ );
241
+
242
+ // Load home page gutter markdown content files
243
+ const homeGutterMarkdownModules = import.meta.glob(
244
+ "../../../UserContent/Home/*/gutter/*.md",
245
+ {
246
+ eager: true,
247
+ query: "?raw",
248
+ import: "default",
249
+ },
250
+ );
251
+
252
+ // Load home page gutter image files
253
+ const homeGutterImageModules = import.meta.glob(
254
+ "../../../UserContent/Home/*/gutter/*.{jpg,jpeg,png,gif,webp}",
255
+ {
256
+ eager: true,
257
+ query: "?url",
258
+ import: "default",
259
+ },
260
+ );
261
+
262
+ // Load contact page gutter manifest files
263
+ const contactGutterManifestModules = import.meta.glob(
264
+ "../../../UserContent/Contact/*/gutter/manifest.json",
265
+ {
266
+ eager: true,
267
+ },
268
+ );
269
+
270
+ // Load contact page gutter markdown content files
271
+ const contactGutterMarkdownModules = import.meta.glob(
272
+ "../../../UserContent/Contact/*/gutter/*.md",
273
+ {
274
+ eager: true,
275
+ query: "?raw",
276
+ import: "default",
277
+ },
278
+ );
279
+
280
+ // Load contact page gutter image files
281
+ const contactGutterImageModules = import.meta.glob(
282
+ "../../../UserContent/Contact/*/gutter/*.{jpg,jpeg,png,gif,webp}",
283
+ {
284
+ eager: true,
285
+ query: "?url",
286
+ import: "default",
287
+ },
288
+ );
289
+
290
+ /**
291
+ * Validates if a string is a valid URL
292
+ * @param {string} urlString - The string to validate as a URL
293
+ * @returns {boolean} True if the string is a valid URL, false otherwise
294
+ */
295
+ function isValidUrl(urlString) {
296
+ try {
297
+ const url = new URL(urlString);
298
+ return url.protocol === "http:" || url.protocol === "https:";
299
+ } catch {
300
+ return false;
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Get all markdown posts from the posts directory
306
+ * @returns {Array} Array of post objects with metadata and slug
307
+ */
308
+ export function getAllPosts() {
309
+ try {
310
+ const posts = Object.entries(modules)
311
+ .map(([filepath, content]) => {
312
+ try {
313
+ // Extract slug from filepath: ../../../UserContent/Posts/example.md -> example
314
+ const slug = filepath.split("/").pop().replace(".md", "");
315
+ const { data } = matter(content);
316
+
317
+ return {
318
+ slug,
319
+ title: data.title || "Untitled",
320
+ date: data.date || new Date().toISOString(),
321
+ tags: data.tags || [],
322
+ description: data.description || "",
323
+ };
324
+ } catch (err) {
325
+ console.error(`Error processing post ${filepath}:`, err);
326
+ return null;
327
+ }
328
+ })
329
+ .filter(Boolean)
330
+ .sort((a, b) => new Date(b.date) - new Date(a.date));
331
+
332
+ return posts;
333
+ } catch (err) {
334
+ console.error("Error in getAllPosts:", err);
335
+ return [];
336
+ }
337
+ }
338
+
339
+ /**
340
+ * Get the latest (most recent) post with full content
341
+ * @returns {Object|null} The latest post object with content, or null if no posts exist
342
+ */
343
+ export function getLatestPost() {
344
+ const posts = getAllPosts();
345
+ if (posts.length === 0) {
346
+ return null;
347
+ }
348
+ // Get the full post content for the most recent post
349
+ return getPostBySlug(posts[0].slug);
350
+ }
351
+
352
+ /**
353
+ * Get all recipes from the recipes directory
354
+ * @returns {Array} Array of recipe objects with metadata and slug
355
+ */
356
+ export function getAllRecipes() {
357
+ try {
358
+ const recipes = Object.entries(recipeModules)
359
+ .map(([filepath, content]) => {
360
+ try {
361
+ // Extract slug from filepath: ../../../UserContent/Recipes/example.md -> example
362
+ const slug = filepath.split("/").pop().replace(".md", "");
363
+ const { data } = matter(content);
364
+
365
+ return {
366
+ slug,
367
+ title: data.title || "Untitled Recipe",
368
+ date: data.date || new Date().toISOString(),
369
+ tags: data.tags || [],
370
+ description: data.description || "",
371
+ };
372
+ } catch (err) {
373
+ console.error(`Error processing recipe ${filepath}:`, err);
374
+ return null;
375
+ }
376
+ })
377
+ .filter(Boolean)
378
+ .sort((a, b) => new Date(b.date) - new Date(a.date));
379
+
380
+ return recipes;
381
+ } catch (err) {
382
+ console.error("Error in getAllRecipes:", err);
383
+ return [];
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Get a single post by slug
389
+ * @param {string} slug - The post slug
390
+ * @returns {Object|null} Post object with content and metadata
391
+ */
392
+ export function getPostBySlug(slug) {
393
+ // Find the matching module by slug
394
+ const entry = Object.entries(modules).find(([filepath]) => {
395
+ const fileSlug = filepath.split("/").pop().replace(".md", "");
396
+ return fileSlug === slug;
397
+ });
398
+
399
+ if (!entry) {
400
+ return null;
401
+ }
402
+
403
+ const content = entry[1];
404
+
405
+ const { data, content: markdown } = matter(content);
406
+ let htmlContent = marked.parse(markdown);
407
+
408
+ // Process anchor tags in the HTML content
409
+ htmlContent = processAnchorTags(htmlContent);
410
+
411
+ // Extract headers for table of contents
412
+ const headers = extractHeaders(markdown);
413
+
414
+ // Get gutter content for this post
415
+ const gutterContent = getGutterContent(slug);
416
+
417
+ return {
418
+ slug,
419
+ title: data.title || "Untitled",
420
+ date: data.date || new Date().toISOString(),
421
+ tags: data.tags || [],
422
+ description: data.description || "",
423
+ content: htmlContent,
424
+ headers,
425
+ gutterContent,
426
+ };
427
+ }
428
+
429
+ /**
430
+ * Extract headers from markdown content for table of contents
431
+ * @param {string} markdown - The raw markdown content
432
+ * @returns {Array} Array of header objects with level, text, and id
433
+ */
434
+ export function extractHeaders(markdown) {
435
+ const headers = [];
436
+
437
+ // Remove fenced code blocks before extracting headers
438
+ // This prevents # comments inside code blocks from being treated as headers
439
+ const markdownWithoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, "");
440
+
441
+ const headerRegex = /^(#{1,6})\s+(.+)$/gm;
442
+
443
+ let match;
444
+ while ((match = headerRegex.exec(markdownWithoutCodeBlocks)) !== null) {
445
+ const level = match[1].length;
446
+ const text = match[2].trim();
447
+ // Create a slug-style ID from the header text
448
+ const id = text
449
+ .toLowerCase()
450
+ .replace(/[^\w\s-]/g, "")
451
+ .replace(/\s+/g, "-")
452
+ .replace(/-+/g, "-")
453
+ .trim();
454
+
455
+ headers.push({
456
+ level,
457
+ text,
458
+ id,
459
+ });
460
+ }
461
+
462
+ return headers;
463
+ }
464
+
465
+ /**
466
+ * Process anchor tags in HTML content
467
+ * Converts <!-- anchor:tagname --> comments to identifiable span elements
468
+ * @param {string} html - The HTML content
469
+ * @returns {string} HTML with anchor markers converted to spans
470
+ */
471
+ export function processAnchorTags(html) {
472
+ // Convert <!-- anchor:tagname --> to <span class="anchor-marker" data-anchor="tagname"></span>
473
+ // Supports alphanumeric characters, underscores, and hyphens in tag names
474
+ return html.replace(
475
+ /<!--\s*anchor:([\w-]+)\s*-->/g,
476
+ (match, tagname) =>
477
+ `<span class="anchor-marker" data-anchor="${tagname}"></span>`,
478
+ );
479
+ }
480
+
481
+ /**
482
+ * Get gutter content from specified modules
483
+ * @param {string} slug - The page/post slug
484
+ * @param {Object} manifestModules - The manifest modules to search
485
+ * @param {Object} markdownModules - The markdown modules to search
486
+ * @param {Object} imageModules - The image modules to search
487
+ * @returns {Array} Array of gutter items with content and position info
488
+ */
489
+ function getGutterContentFromModules(
490
+ slug,
491
+ manifestModules,
492
+ markdownModules,
493
+ imageModules,
494
+ ) {
495
+ // Find the manifest file for this page/post
496
+ const manifestEntry = Object.entries(manifestModules).find(([filepath]) => {
497
+ const parts = filepath.split("/");
498
+ const folder = parts[parts.length - 3]; // Get the folder name
499
+ return folder === slug;
500
+ });
501
+
502
+ if (!manifestEntry) {
503
+ return [];
504
+ }
505
+
506
+ const manifest = manifestEntry[1].default || manifestEntry[1];
507
+
508
+ if (!manifest.items || !Array.isArray(manifest.items)) {
509
+ return [];
510
+ }
511
+
512
+ // Process each gutter item
513
+ return manifest.items
514
+ .map((item) => {
515
+ if (item.type === "comment" || item.type === "markdown") {
516
+ // Find the markdown content file
517
+ const mdEntry = Object.entries(markdownModules).find(([filepath]) => {
518
+ return filepath.includes(`/${slug}/gutter/${item.file}`);
519
+ });
520
+
521
+ if (mdEntry) {
522
+ const markdownContent = mdEntry[1];
523
+ const htmlContent = marked.parse(markdownContent);
524
+
525
+ return {
526
+ ...item,
527
+ content: htmlContent,
528
+ };
529
+ }
530
+ } else if (item.type === "photo" || item.type === "image") {
531
+ // Check if file is an external URL
532
+ if (item.file && isValidUrl(item.file)) {
533
+ return {
534
+ ...item,
535
+ src: item.file,
536
+ };
537
+ }
538
+
539
+ // Find the local image file
540
+ const imgEntry = Object.entries(imageModules).find(([filepath]) => {
541
+ return filepath.includes(`/${slug}/gutter/${item.file}`);
542
+ });
543
+
544
+ if (imgEntry) {
545
+ return {
546
+ ...item,
547
+ src: imgEntry[1],
548
+ };
549
+ }
550
+ } else if (item.type === "emoji") {
551
+ // Emoji items can use URLs (local or CDN) or local files
552
+ if (item.url) {
553
+ // Direct URL (local path like /icons/instruction/mix.webp or CDN URL)
554
+ return {
555
+ ...item,
556
+ src: item.url,
557
+ };
558
+ } else if (item.file) {
559
+ // Local file in gutter directory
560
+ const imgEntry = Object.entries(imageModules).find(([filepath]) => {
561
+ return filepath.includes(`/${slug}/gutter/${item.file}`);
562
+ });
563
+
564
+ if (imgEntry) {
565
+ return {
566
+ ...item,
567
+ src: imgEntry[1],
568
+ };
569
+ }
570
+ }
571
+ return item;
572
+ } else if (item.type === "gallery") {
573
+ /**
574
+ * Process gallery items containing multiple images
575
+ *
576
+ * Galleries can contain:
577
+ * - External URLs (validated for http/https protocol)
578
+ * - Local files (resolved from the gutter directory)
579
+ *
580
+ * Images that fail to resolve (invalid URLs or missing files) are filtered out.
581
+ * If all images fail to resolve, the entire gallery item is excluded.
582
+ */
583
+ const originalImageCount = (item.images || []).length;
584
+ const images = (item.images || [])
585
+ .map((img) => {
586
+ // Check if it's an external URL
587
+ if (img.url) {
588
+ // Validate URL format to prevent malformed URLs from failing silently
589
+ if (!isValidUrl(img.url)) {
590
+ console.warn(
591
+ `Invalid URL in gallery for "${slug}": ${img.url}`,
592
+ );
593
+ return null;
594
+ }
595
+ return {
596
+ url: img.url,
597
+ alt: img.alt || "",
598
+ caption: img.caption || "",
599
+ };
600
+ }
601
+
602
+ // Otherwise, look for local file
603
+ if (img.file) {
604
+ const imgEntry = Object.entries(imageModules).find(
605
+ ([filepath]) => {
606
+ return filepath.includes(`/${slug}/gutter/${img.file}`);
607
+ },
608
+ );
609
+
610
+ if (imgEntry) {
611
+ return {
612
+ url: imgEntry[1],
613
+ alt: img.alt || "",
614
+ caption: img.caption || "",
615
+ };
616
+ } else {
617
+ console.warn(
618
+ `Local file not found in gallery for "${slug}": ${img.file}`,
619
+ );
620
+ }
621
+ }
622
+
623
+ return null;
624
+ })
625
+ .filter(Boolean);
626
+
627
+ if (images.length > 0) {
628
+ return {
629
+ ...item,
630
+ images,
631
+ };
632
+ } else if (originalImageCount > 0) {
633
+ // All images failed to resolve - log warning for debugging
634
+ console.warn(
635
+ `Gallery in "${slug}" has ${originalImageCount} image(s) defined but none could be resolved`,
636
+ );
637
+ }
638
+ }
639
+
640
+ return item;
641
+ })
642
+ .filter(
643
+ (item) =>
644
+ item.content || item.src || item.images || item.type === "emoji",
645
+ ); // Filter out items that weren't found
646
+ }
647
+
648
+ /**
649
+ * Get gutter content for a recipe by slug
650
+ * @param {string} slug - The recipe slug
651
+ * @returns {Array} Array of gutter items with content and position info
652
+ */
653
+ export function getRecipeGutterContent(slug) {
654
+ return getGutterContentFromModules(
655
+ slug,
656
+ recipeGutterManifestModules,
657
+ recipeGutterMarkdownModules,
658
+ recipeGutterImageModules,
659
+ );
660
+ }
661
+
662
+ /**
663
+ * Get gutter content for a blog post by slug
664
+ * @param {string} slug - The post slug
665
+ * @returns {Array} Array of gutter items with content and position info
666
+ */
667
+ export function getGutterContent(slug) {
668
+ return getGutterContentFromModules(
669
+ slug,
670
+ gutterManifestModules,
671
+ gutterMarkdownModules,
672
+ gutterImageModules,
673
+ );
674
+ }
675
+
676
+ /**
677
+ * Get gutter content for the home page
678
+ * @param {string} slug - The page slug (e.g., 'home')
679
+ * @returns {Array} Array of gutter items with content and position info
680
+ */
681
+ export function getHomeGutterContent(slug) {
682
+ return getGutterContentFromModules(
683
+ slug,
684
+ homeGutterManifestModules,
685
+ homeGutterMarkdownModules,
686
+ homeGutterImageModules,
687
+ );
688
+ }
689
+
690
+ /**
691
+ * Get gutter content for the contact page
692
+ * @param {string} slug - The page slug (e.g., 'contact')
693
+ * @returns {Array} Array of gutter items with content and position info
694
+ */
695
+ export function getContactGutterContent(slug) {
696
+ return getGutterContentFromModules(
697
+ slug,
698
+ contactGutterManifestModules,
699
+ contactGutterMarkdownModules,
700
+ contactGutterImageModules,
701
+ );
702
+ }
703
+
704
+ /**
705
+ * Get the home page content
706
+ * @returns {Object|null} Home page object with content, metadata, and galleries
707
+ */
708
+ export function getHomePage() {
709
+ try {
710
+ // Find the home.md file
711
+ const entry = Object.entries(homeModules).find(([filepath]) => {
712
+ return filepath.includes("home.md");
713
+ });
714
+
715
+ if (!entry) {
716
+ return null;
717
+ }
718
+
719
+ const content = entry[1];
720
+
721
+ const { data, content: markdown } = matter(content);
722
+ const htmlContent = sanitizeMarkdown(marked.parse(markdown));
723
+
724
+ // Extract headers for table of contents
725
+ const headers = extractHeaders(markdown);
726
+
727
+ // Get gutter content for the home page
728
+ const gutterContent = getHomeGutterContent("home");
729
+
730
+ return {
731
+ slug: "home",
732
+ title: data.title || "Home",
733
+ description: data.description || "",
734
+ hero: data.hero || null,
735
+ galleries: data.galleries || [],
736
+ content: htmlContent,
737
+ headers,
738
+ gutterContent,
739
+ };
740
+ } catch (err) {
741
+ console.error("Error in getHomePage:", err);
742
+ return null;
743
+ }
744
+ }
745
+
746
+ /**
747
+ * Get the contact page content
748
+ * @returns {Object|null} Contact page object with content and metadata
749
+ */
750
+ export function getContactPage() {
751
+ try {
752
+ // Find the contact.md file
753
+ const entry = Object.entries(contactModules).find(([filepath]) => {
754
+ return filepath.includes("contact.md");
755
+ });
756
+
757
+ if (!entry) {
758
+ return null;
759
+ }
760
+
761
+ const content = entry[1];
762
+
763
+ const { data, content: markdown } = matter(content);
764
+ const htmlContent = sanitizeMarkdown(marked.parse(markdown));
765
+
766
+ // Extract headers for table of contents
767
+ const headers = extractHeaders(markdown);
768
+
769
+ // Get gutter content for the contact page
770
+ const gutterContent = getContactGutterContent("contact");
771
+
772
+ return {
773
+ slug: "contact",
774
+ title: data.title || "Contact",
775
+ description: data.description || "",
776
+ content: htmlContent,
777
+ headers,
778
+ gutterContent,
779
+ };
780
+ } catch (err) {
781
+ console.error("Error in getContactPage:", err);
782
+ return null;
783
+ }
784
+ }
785
+
786
+ /**
787
+ * Get the about page content
788
+ * @returns {Object|null} About page object with content and metadata
789
+ */
790
+ export function getAboutPage() {
791
+ try {
792
+ // Find the about.md file
793
+ const entry = Object.entries(aboutModules).find(([filepath]) => {
794
+ return filepath.includes("about.md");
795
+ });
796
+
797
+ if (!entry) {
798
+ return null;
799
+ }
800
+
801
+ const content = entry[1];
802
+
803
+ const { data, content: markdown } = matter(content);
804
+ const htmlContent = sanitizeMarkdown(marked.parse(markdown));
805
+
806
+ // Extract headers for table of contents
807
+ const headers = extractHeaders(markdown);
808
+
809
+ // Get gutter content for the about page
810
+ const gutterContent = getAboutGutterContent("about");
811
+
812
+ return {
813
+ slug: "about",
814
+ title: data.title || "About",
815
+ date: data.date || new Date().toISOString(),
816
+ description: data.description || "",
817
+ content: htmlContent,
818
+ headers,
819
+ gutterContent,
820
+ };
821
+ } catch (err) {
822
+ console.error("Error in getAboutPage:", err);
823
+ return null;
824
+ }
825
+ }
826
+
827
+ /**
828
+ * Get gutter content for the about page
829
+ * @param {string} slug - The page slug (e.g., 'about')
830
+ * @returns {Array} Array of gutter items with content and position info
831
+ */
832
+ export function getAboutGutterContent(slug) {
833
+ return getGutterContentFromModules(
834
+ slug,
835
+ aboutGutterManifestModules,
836
+ aboutGutterMarkdownModules,
837
+ aboutGutterImageModules,
838
+ );
839
+ }
840
+
841
+ /**
842
+ * Get recipe metadata (step icons, etc.) for a recipe by slug
843
+ * @param {string} slug - The recipe slug
844
+ * @returns {Object|null} Recipe metadata with instruction icons
845
+ */
846
+ export function getRecipeSidecar(slug) {
847
+ // Find the recipe.json file in the gutter folder
848
+ // Expected path: ../../../UserContent/Recipes/{slug}/gutter/recipe.json
849
+ // parts[-3] extracts the recipe folder name from this path structure
850
+ const entry = Object.entries(recipeMetadataModules).find(([filepath]) => {
851
+ const parts = filepath.split("/");
852
+ const folder = parts[parts.length - 3]; // Get the recipe folder name
853
+ return folder === slug;
854
+ });
855
+
856
+ if (!entry) {
857
+ return null;
858
+ }
859
+
860
+ // The module is already parsed JSON
861
+ return entry[1].default || entry[1];
862
+ }
863
+
864
+ /**
865
+ * Get a single recipe by slug
866
+ * @param {string} slug - The recipe slug
867
+ * @returns {Object|null} Recipe object with content and metadata
868
+ */
869
+ export function getRecipeBySlug(slug) {
870
+ // Find the matching module by slug
871
+ const entry = Object.entries(recipeModules).find(([filepath]) => {
872
+ const fileSlug = filepath.split("/").pop().replace(".md", "");
873
+ return fileSlug === slug;
874
+ });
875
+
876
+ if (!entry) {
877
+ return null;
878
+ }
879
+
880
+ const content = entry[1];
881
+
882
+ const { data, content: markdown } = matter(content);
883
+
884
+ // Process Mermaid diagrams in the content
885
+ const processedContent = processMermaidDiagrams(markdown);
886
+ let htmlContent = marked.parse(processedContent);
887
+
888
+ // Process anchor tags in the HTML content
889
+ htmlContent = processAnchorTags(htmlContent);
890
+
891
+ // Extract headers for table of contents
892
+ const headers = extractHeaders(markdown);
893
+
894
+ // Get sidecar data if available
895
+ const sidecar = getRecipeSidecar(slug);
896
+
897
+ // Get gutter content for this recipe
898
+ const gutterContent = getRecipeGutterContent(slug);
899
+
900
+ return {
901
+ slug,
902
+ title: data.title || "Untitled Recipe",
903
+ date: data.date || new Date().toISOString(),
904
+ tags: data.tags || [],
905
+ description: data.description || "",
906
+ content: htmlContent,
907
+ headers,
908
+ gutterContent,
909
+ sidecar: sidecar,
910
+ };
911
+ }
912
+
913
+ /**
914
+ * Process Mermaid diagrams in markdown content
915
+ * @param {string} markdown - The markdown content
916
+ * @returns {string} Processed markdown with Mermaid diagrams
917
+ */
918
+ function processMermaidDiagrams(markdown) {
919
+ // Replace Mermaid code blocks with special divs that will be processed later
920
+ return markdown.replace(
921
+ /```mermaid\n([\s\S]*?)```/g,
922
+ (match, diagramCode) => {
923
+ const diagramId = "mermaid-" + Math.random().toString(36).substr(2, 9);
924
+ return `<div class="mermaid-container" id="${diagramId}" data-diagram="${encodeURIComponent(diagramCode.trim())}"></div>`;
925
+ },
926
+ );
927
+ }
928
+
929
+ /**
930
+ * Render Mermaid diagrams in the DOM
931
+ * This should be called after the content is mounted
932
+ */
933
+ export async function renderMermaidDiagrams() {
934
+ const containers = document.querySelectorAll(".mermaid-container");
935
+
936
+ for (const container of containers) {
937
+ try {
938
+ const diagramCode = decodeURIComponent(container.dataset.diagram);
939
+ const { svg } = await mermaid.render(container.id, diagramCode);
940
+ // Sanitize SVG output before injecting into DOM to prevent XSS
941
+ container.innerHTML = sanitizeSVG(svg);
942
+ } catch (error) {
943
+ console.error("Error rendering Mermaid diagram:", error);
944
+ container.innerHTML = '<p class="error">Error rendering diagram</p>';
945
+ }
946
+ }
947
+ }