@autumnsgrove/groveengine 0.1.0 → 0.3.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 (61) hide show
  1. package/dist/components/admin/GutterManager.svelte +1 -2
  2. package/dist/components/admin/MarkdownEditor.svelte +1 -2
  3. package/dist/components/custom/InternalsPostViewer.svelte +95 -0
  4. package/dist/components/custom/InternalsPostViewer.svelte.d.ts +13 -0
  5. package/dist/components/ui/index.d.ts +0 -12
  6. package/dist/components/ui/index.js +2 -13
  7. package/dist/components/ui/select/select-separator.svelte +2 -3
  8. package/dist/components/ui/select/select-separator.svelte.d.ts +1 -1
  9. package/dist/utils/markdown.d.ts +130 -66
  10. package/dist/utils/markdown.js +482 -568
  11. package/package.json +2 -1
  12. package/dist/components/ui/Badge.svelte +0 -48
  13. package/dist/components/ui/Badge.svelte.d.ts +0 -26
  14. package/dist/components/ui/Button.svelte +0 -74
  15. package/dist/components/ui/Button.svelte.d.ts +0 -34
  16. package/dist/components/ui/Card.svelte +0 -102
  17. package/dist/components/ui/Card.svelte.d.ts +0 -46
  18. package/dist/components/ui/Input.svelte +0 -81
  19. package/dist/components/ui/Input.svelte.d.ts +0 -35
  20. package/dist/components/ui/Skeleton.svelte +0 -31
  21. package/dist/components/ui/Skeleton.svelte.d.ts +0 -26
  22. package/dist/components/ui/Textarea.svelte +0 -81
  23. package/dist/components/ui/Textarea.svelte.d.ts +0 -35
  24. package/dist/components/ui/badge/badge.svelte +0 -50
  25. package/dist/components/ui/badge/badge.svelte.d.ts +0 -60
  26. package/dist/components/ui/badge/index.d.ts +0 -2
  27. package/dist/components/ui/badge/index.js +0 -2
  28. package/dist/components/ui/button/button.svelte +0 -82
  29. package/dist/components/ui/button/button.svelte.d.ts +0 -132
  30. package/dist/components/ui/button/index.d.ts +0 -2
  31. package/dist/components/ui/button/index.js +0 -4
  32. package/dist/components/ui/card/card-content.svelte +0 -16
  33. package/dist/components/ui/card/card-content.svelte.d.ts +0 -5
  34. package/dist/components/ui/card/card-description.svelte +0 -16
  35. package/dist/components/ui/card/card-description.svelte.d.ts +0 -5
  36. package/dist/components/ui/card/card-footer.svelte +0 -16
  37. package/dist/components/ui/card/card-footer.svelte.d.ts +0 -5
  38. package/dist/components/ui/card/card-header.svelte +0 -16
  39. package/dist/components/ui/card/card-header.svelte.d.ts +0 -5
  40. package/dist/components/ui/card/card-title.svelte +0 -25
  41. package/dist/components/ui/card/card-title.svelte.d.ts +0 -8
  42. package/dist/components/ui/card/card.svelte +0 -20
  43. package/dist/components/ui/card/card.svelte.d.ts +0 -5
  44. package/dist/components/ui/card/index.d.ts +0 -7
  45. package/dist/components/ui/card/index.js +0 -9
  46. package/dist/components/ui/input/index.d.ts +0 -2
  47. package/dist/components/ui/input/index.js +0 -4
  48. package/dist/components/ui/input/input.svelte +0 -46
  49. package/dist/components/ui/input/input.svelte.d.ts +0 -13
  50. package/dist/components/ui/separator/index.d.ts +0 -2
  51. package/dist/components/ui/separator/index.js +0 -4
  52. package/dist/components/ui/separator/separator.svelte +0 -22
  53. package/dist/components/ui/separator/separator.svelte.d.ts +0 -4
  54. package/dist/components/ui/skeleton/index.d.ts +0 -2
  55. package/dist/components/ui/skeleton/index.js +0 -4
  56. package/dist/components/ui/skeleton/skeleton.svelte +0 -17
  57. package/dist/components/ui/skeleton/skeleton.svelte.d.ts +0 -5
  58. package/dist/components/ui/textarea/index.d.ts +0 -2
  59. package/dist/components/ui/textarea/index.js +0 -4
  60. package/dist/components/ui/textarea/textarea.svelte +0 -24
  61. package/dist/components/ui/textarea/textarea.svelte.d.ts +0 -6
@@ -76,217 +76,6 @@ marked.setOptions({
76
76
  breaks: false,
77
77
  });
78
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
79
  /**
291
80
  * Validates if a string is a valid URL
292
81
  * @param {string} urlString - The string to validate as a URL
@@ -301,131 +90,6 @@ function isValidUrl(urlString) {
301
90
  }
302
91
  }
303
92
 
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
93
  /**
430
94
  * Extract headers from markdown content for table of contents
431
95
  * @param {string} markdown - The raw markdown content
@@ -479,14 +143,95 @@ export function processAnchorTags(html) {
479
143
  }
480
144
 
481
145
  /**
482
- * Get gutter content from specified modules
146
+ * Process Mermaid diagrams in markdown content
147
+ * @param {string} markdown - The markdown content
148
+ * @returns {string} Processed markdown with Mermaid diagrams
149
+ */
150
+ export function processMermaidDiagrams(markdown) {
151
+ // Replace Mermaid code blocks with special divs that will be processed later
152
+ return markdown.replace(
153
+ /```mermaid\n([\s\S]*?)```/g,
154
+ (match, diagramCode) => {
155
+ const diagramId = "mermaid-" + Math.random().toString(36).substr(2, 9);
156
+ return `<div class="mermaid-container" id="${diagramId}" data-diagram="${encodeURIComponent(diagramCode.trim())}"></div>`;
157
+ },
158
+ );
159
+ }
160
+
161
+ /**
162
+ * Render Mermaid diagrams in the DOM
163
+ * This should be called after the content is mounted
164
+ */
165
+ export async function renderMermaidDiagrams() {
166
+ const containers = document.querySelectorAll(".mermaid-container");
167
+
168
+ for (const container of containers) {
169
+ try {
170
+ const diagramCode = decodeURIComponent(container.dataset.diagram);
171
+ const { svg } = await mermaid.render(container.id, diagramCode);
172
+ // Sanitize SVG output before injecting into DOM to prevent XSS
173
+ container.innerHTML = sanitizeSVG(svg);
174
+ } catch (error) {
175
+ console.error("Error rendering Mermaid diagram:", error);
176
+ container.innerHTML = '<p class="error">Error rendering diagram</p>';
177
+ }
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Parse markdown content and convert to HTML
183
+ * @param {string} markdownContent - The raw markdown content (may include frontmatter)
184
+ * @returns {Object} Object with data (frontmatter), content (HTML), headers, and raw markdown
185
+ */
186
+ export function parseMarkdownContent(markdownContent) {
187
+ const { data, content: markdown } = matter(markdownContent);
188
+
189
+ // Process Mermaid diagrams in the content
190
+ const processedContent = processMermaidDiagrams(markdown);
191
+ let htmlContent = marked.parse(processedContent);
192
+
193
+ // Process anchor tags in the HTML content
194
+ htmlContent = processAnchorTags(htmlContent);
195
+
196
+ // Extract headers for table of contents
197
+ const headers = extractHeaders(markdown);
198
+
199
+ return {
200
+ data,
201
+ content: htmlContent,
202
+ headers,
203
+ rawMarkdown: markdown,
204
+ };
205
+ }
206
+
207
+ /**
208
+ * Parse markdown content with sanitization (for user-facing pages like home, about, contact)
209
+ * @param {string} markdownContent - The raw markdown content (may include frontmatter)
210
+ * @returns {Object} Object with data (frontmatter), content (sanitized HTML), headers
211
+ */
212
+ export function parseMarkdownContentSanitized(markdownContent) {
213
+ const { data, content: markdown } = matter(markdownContent);
214
+ const htmlContent = sanitizeMarkdown(marked.parse(markdown));
215
+ const headers = extractHeaders(markdown);
216
+
217
+ return {
218
+ data,
219
+ content: htmlContent,
220
+ headers,
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Get gutter content from provided modules
226
+ * This is a utility function that processes gutter manifests, markdown, and images
227
+ *
483
228
  * @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
229
+ * @param {Object} manifestModules - The manifest modules (from import.meta.glob)
230
+ * @param {Object} markdownModules - The markdown modules (from import.meta.glob)
231
+ * @param {Object} imageModules - The image modules (from import.meta.glob)
487
232
  * @returns {Array} Array of gutter items with content and position info
488
233
  */
489
- function getGutterContentFromModules(
234
+ export function processGutterContent(
490
235
  slug,
491
236
  manifestModules,
492
237
  markdownModules,
@@ -646,302 +391,471 @@ function getGutterContentFromModules(
646
391
  }
647
392
 
648
393
  /**
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
394
+ * Process a list of markdown files into post/recipe objects
395
+ *
396
+ * @param {Object} modules - The modules from import.meta.glob (filepath -> content)
397
+ * @returns {Array} Array of post/content objects with metadata and slug
652
398
  */
653
- export function getRecipeGutterContent(slug) {
654
- return getGutterContentFromModules(
655
- slug,
656
- recipeGutterManifestModules,
657
- recipeGutterMarkdownModules,
658
- recipeGutterImageModules,
659
- );
660
- }
399
+ export function processMarkdownModules(modules) {
400
+ try {
401
+ const items = Object.entries(modules)
402
+ .map(([filepath, content]) => {
403
+ try {
404
+ // Extract slug from filepath: /path/to/Posts/example.md -> example
405
+ const slug = filepath.split("/").pop().replace(".md", "");
406
+ const { data } = matter(content);
661
407
 
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
- }
408
+ return {
409
+ slug,
410
+ title: data.title || "Untitled",
411
+ date: data.date || new Date().toISOString(),
412
+ tags: data.tags || [],
413
+ description: data.description || "",
414
+ };
415
+ } catch (err) {
416
+ console.error(`Error processing file ${filepath}:`, err);
417
+ return null;
418
+ }
419
+ })
420
+ .filter(Boolean)
421
+ .sort((a, b) => new Date(b.date) - new Date(a.date));
675
422
 
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
- );
423
+ return items;
424
+ } catch (err) {
425
+ console.error("Error in processMarkdownModules:", err);
426
+ return [];
427
+ }
688
428
  }
689
429
 
690
430
  /**
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
431
+ * Get a single item by slug from modules
432
+ *
433
+ * @param {string} slug - The item slug
434
+ * @param {Object} modules - The modules from import.meta.glob (filepath -> content)
435
+ * @param {Object} options - Optional configuration
436
+ * @param {Object} options.gutterModules - Gutter modules { manifest, markdown, images }
437
+ * @param {Object} options.sidecarModules - Sidecar/metadata modules (for recipes)
438
+ * @returns {Object|null} Item object with content and metadata
694
439
  */
695
- export function getContactGutterContent(slug) {
696
- return getGutterContentFromModules(
440
+ export function getItemBySlug(slug, modules, options = {}) {
441
+ // Find the matching module by slug
442
+ const entry = Object.entries(modules).find(([filepath]) => {
443
+ const fileSlug = filepath.split("/").pop().replace(".md", "");
444
+ return fileSlug === slug;
445
+ });
446
+
447
+ if (!entry) {
448
+ return null;
449
+ }
450
+
451
+ const rawContent = entry[1];
452
+ const { data, content, headers } = parseMarkdownContent(rawContent);
453
+
454
+ // Build the result object
455
+ const result = {
697
456
  slug,
698
- contactGutterManifestModules,
699
- contactGutterMarkdownModules,
700
- contactGutterImageModules,
701
- );
457
+ title: data.title || "Untitled",
458
+ date: data.date || new Date().toISOString(),
459
+ tags: data.tags || [],
460
+ description: data.description || "",
461
+ content,
462
+ headers,
463
+ };
464
+
465
+ // Process gutter content if provided
466
+ if (options.gutterModules) {
467
+ const { manifest, markdown, images } = options.gutterModules;
468
+ result.gutterContent = processGutterContent(slug, manifest, markdown, images);
469
+ }
470
+
471
+ // Process sidecar/metadata if provided (for recipes)
472
+ if (options.sidecarModules) {
473
+ const sidecarEntry = Object.entries(options.sidecarModules).find(([filepath]) => {
474
+ const parts = filepath.split("/");
475
+ const folder = parts[parts.length - 3]; // Get the folder name
476
+ return folder === slug;
477
+ });
478
+
479
+ if (sidecarEntry) {
480
+ result.sidecar = sidecarEntry[1].default || sidecarEntry[1];
481
+ }
482
+ }
483
+
484
+ return result;
702
485
  }
703
486
 
704
487
  /**
705
- * Get the home page content
706
- * @returns {Object|null} Home page object with content, metadata, and galleries
488
+ * Get a page (home, about, contact) by filename from modules
489
+ * Uses sanitization for security
490
+ *
491
+ * @param {string} filename - The filename to look for (e.g., "home.md", "about.md")
492
+ * @param {Object} modules - The modules from import.meta.glob (filepath -> content)
493
+ * @param {Object} options - Optional configuration
494
+ * @param {Object} options.gutterModules - Gutter modules { manifest, markdown, images }
495
+ * @param {string} options.slug - Override slug (defaults to filename without .md)
496
+ * @returns {Object|null} Page object with content and metadata
707
497
  */
708
- export function getHomePage() {
498
+ export function getPageByFilename(filename, modules, options = {}) {
709
499
  try {
710
- // Find the home.md file
711
- const entry = Object.entries(homeModules).find(([filepath]) => {
712
- return filepath.includes("home.md");
500
+ // Find the matching file
501
+ const entry = Object.entries(modules).find(([filepath]) => {
502
+ return filepath.includes(filename);
713
503
  });
714
504
 
715
505
  if (!entry) {
716
506
  return null;
717
507
  }
718
508
 
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");
509
+ const rawContent = entry[1];
510
+ const { data, content, headers } = parseMarkdownContentSanitized(rawContent);
511
+ const slug = options.slug || filename.replace(".md", "");
729
512
 
730
- return {
731
- slug: "home",
732
- title: data.title || "Home",
513
+ // Build the result object
514
+ const result = {
515
+ slug,
516
+ title: data.title || slug.charAt(0).toUpperCase() + slug.slice(1),
733
517
  description: data.description || "",
734
- hero: data.hero || null,
735
- galleries: data.galleries || [],
736
- content: htmlContent,
518
+ content,
737
519
  headers,
738
- gutterContent,
739
520
  };
521
+
522
+ // Add optional fields from frontmatter
523
+ if (data.date) result.date = data.date;
524
+ if (data.hero) result.hero = data.hero;
525
+ if (data.galleries) result.galleries = data.galleries;
526
+
527
+ // Process gutter content if provided
528
+ if (options.gutterModules) {
529
+ const { manifest, markdown, images } = options.gutterModules;
530
+ result.gutterContent = processGutterContent(slug, manifest, markdown, images);
531
+ }
532
+
533
+ return result;
740
534
  } catch (err) {
741
- console.error("Error in getHomePage:", err);
535
+ console.error(`Error in getPageByFilename for ${filename}:`, err);
742
536
  return null;
743
537
  }
744
538
  }
745
539
 
746
540
  /**
747
- * Get the contact page content
748
- * @returns {Object|null} Contact page object with content and metadata
541
+ * Get site configuration from a config module
542
+ *
543
+ * @param {Object} configModule - The config module from import.meta.glob
544
+ * @returns {Object} Site configuration object
749
545
  */
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
- });
546
+ export function getSiteConfigFromModule(configModule) {
547
+ const entry = Object.entries(configModule)[0];
548
+ if (entry) {
549
+ return entry[1].default || entry[1];
550
+ }
551
+ return {
552
+ owner: { name: "Admin", email: "" },
553
+ site: { title: "The Grove", description: "", copyright: "AutumnsGrove" },
554
+ social: {},
555
+ };
556
+ }
756
557
 
757
- if (!entry) {
758
- return null;
759
- }
558
+ /**
559
+ * Create a configured content loader with all functions bound to the provided modules
560
+ * This is the main factory function for creating a content loader in the consuming app
561
+ *
562
+ * @param {Object} config - Configuration object with all required modules
563
+ * @param {Object} config.posts - Post modules from import.meta.glob
564
+ * @param {Object} config.recipes - Recipe modules from import.meta.glob
565
+ * @param {Object} config.about - About page modules from import.meta.glob
566
+ * @param {Object} config.home - Home page modules from import.meta.glob
567
+ * @param {Object} config.contact - Contact page modules from import.meta.glob
568
+ * @param {Object} config.siteConfig - Site config module from import.meta.glob
569
+ * @param {Object} config.postGutter - Post gutter modules { manifest, markdown, images }
570
+ * @param {Object} config.recipeGutter - Recipe gutter modules { manifest, markdown, images }
571
+ * @param {Object} config.recipeMetadata - Recipe metadata modules from import.meta.glob
572
+ * @param {Object} config.aboutGutter - About gutter modules { manifest, markdown, images }
573
+ * @param {Object} config.homeGutter - Home gutter modules { manifest, markdown, images }
574
+ * @param {Object} config.contactGutter - Contact gutter modules { manifest, markdown, images }
575
+ * @returns {Object} Object with all content loader functions
576
+ */
577
+ export function createContentLoader(config) {
578
+ const {
579
+ posts = {},
580
+ recipes = {},
581
+ about = {},
582
+ home = {},
583
+ contact = {},
584
+ siteConfig = {},
585
+ postGutter = {},
586
+ recipeGutter = {},
587
+ recipeMetadata = {},
588
+ aboutGutter = {},
589
+ homeGutter = {},
590
+ contactGutter = {},
591
+ } = config;
760
592
 
761
- const content = entry[1];
593
+ return {
594
+ /**
595
+ * Get all posts with metadata
596
+ */
597
+ getAllPosts() {
598
+ return processMarkdownModules(posts);
599
+ },
762
600
 
763
- const { data, content: markdown } = matter(content);
764
- const htmlContent = sanitizeMarkdown(marked.parse(markdown));
601
+ /**
602
+ * Get all recipes with metadata
603
+ */
604
+ getAllRecipes() {
605
+ return processMarkdownModules(recipes);
606
+ },
765
607
 
766
- // Extract headers for table of contents
767
- const headers = extractHeaders(markdown);
608
+ /**
609
+ * Get the latest (most recent) post with full content
610
+ */
611
+ getLatestPost() {
612
+ const allPosts = processMarkdownModules(posts);
613
+ if (allPosts.length === 0) {
614
+ return null;
615
+ }
616
+ return this.getPostBySlug(allPosts[0].slug);
617
+ },
768
618
 
769
- // Get gutter content for the contact page
770
- const gutterContent = getContactGutterContent("contact");
619
+ /**
620
+ * Get a single post by slug
621
+ */
622
+ getPostBySlug(slug) {
623
+ return getItemBySlug(slug, posts, {
624
+ gutterModules: postGutter.manifest ? postGutter : undefined,
625
+ });
626
+ },
771
627
 
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
- }
628
+ /**
629
+ * Get a single recipe by slug
630
+ */
631
+ getRecipeBySlug(slug) {
632
+ return getItemBySlug(slug, recipes, {
633
+ gutterModules: recipeGutter.manifest ? recipeGutter : undefined,
634
+ sidecarModules: recipeMetadata,
635
+ });
636
+ },
785
637
 
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
- });
638
+ /**
639
+ * Get the home page content
640
+ */
641
+ getHomePage() {
642
+ return getPageByFilename("home.md", home, {
643
+ gutterModules: homeGutter.manifest ? homeGutter : undefined,
644
+ slug: "home",
645
+ });
646
+ },
796
647
 
797
- if (!entry) {
798
- return null;
799
- }
648
+ /**
649
+ * Get the about page content
650
+ */
651
+ getAboutPage() {
652
+ return getPageByFilename("about.md", about, {
653
+ gutterModules: aboutGutter.manifest ? aboutGutter : undefined,
654
+ slug: "about",
655
+ });
656
+ },
657
+
658
+ /**
659
+ * Get the contact page content
660
+ */
661
+ getContactPage() {
662
+ return getPageByFilename("contact.md", contact, {
663
+ gutterModules: contactGutter.manifest ? contactGutter : undefined,
664
+ slug: "contact",
665
+ });
666
+ },
667
+
668
+ /**
669
+ * Get the site configuration
670
+ */
671
+ getSiteConfig() {
672
+ return getSiteConfigFromModule(siteConfig);
673
+ },
674
+
675
+ /**
676
+ * Get gutter content for a post
677
+ */
678
+ getGutterContent(slug) {
679
+ if (!postGutter.manifest) return [];
680
+ return processGutterContent(
681
+ slug,
682
+ postGutter.manifest,
683
+ postGutter.markdown || {},
684
+ postGutter.images || {},
685
+ );
686
+ },
687
+
688
+ /**
689
+ * Get gutter content for a recipe
690
+ */
691
+ getRecipeGutterContent(slug) {
692
+ if (!recipeGutter.manifest) return [];
693
+ return processGutterContent(
694
+ slug,
695
+ recipeGutter.manifest,
696
+ recipeGutter.markdown || {},
697
+ recipeGutter.images || {},
698
+ );
699
+ },
700
+
701
+ /**
702
+ * Get gutter content for the home page
703
+ */
704
+ getHomeGutterContent(slug) {
705
+ if (!homeGutter.manifest) return [];
706
+ return processGutterContent(
707
+ slug,
708
+ homeGutter.manifest,
709
+ homeGutter.markdown || {},
710
+ homeGutter.images || {},
711
+ );
712
+ },
800
713
 
801
- const content = entry[1];
714
+ /**
715
+ * Get gutter content for the about page
716
+ */
717
+ getAboutGutterContent(slug) {
718
+ if (!aboutGutter.manifest) return [];
719
+ return processGutterContent(
720
+ slug,
721
+ aboutGutter.manifest,
722
+ aboutGutter.markdown || {},
723
+ aboutGutter.images || {},
724
+ );
725
+ },
802
726
 
803
- const { data, content: markdown } = matter(content);
804
- const htmlContent = sanitizeMarkdown(marked.parse(markdown));
727
+ /**
728
+ * Get gutter content for the contact page
729
+ */
730
+ getContactGutterContent(slug) {
731
+ if (!contactGutter.manifest) return [];
732
+ return processGutterContent(
733
+ slug,
734
+ contactGutter.manifest,
735
+ contactGutter.markdown || {},
736
+ contactGutter.images || {},
737
+ );
738
+ },
805
739
 
806
- // Extract headers for table of contents
807
- const headers = extractHeaders(markdown);
740
+ /**
741
+ * Get recipe sidecar/metadata by slug
742
+ */
743
+ getRecipeSidecar(slug) {
744
+ const entry = Object.entries(recipeMetadata).find(([filepath]) => {
745
+ const parts = filepath.split("/");
746
+ const folder = parts[parts.length - 3];
747
+ return folder === slug;
748
+ });
749
+
750
+ if (!entry) {
751
+ return null;
752
+ }
753
+
754
+ return entry[1].default || entry[1];
755
+ },
756
+ };
757
+ }
758
+
759
+ /**
760
+ * Registry for site-specific content loaders
761
+ * Sites must register their content loaders using registerContentLoader()
762
+ */
763
+ let contentLoader = null;
764
+
765
+ /**
766
+ * Register a content loader for the site
767
+ * This should be called by the consuming site to provide access to content
768
+ * @param {Object} loader - Object with getAllPosts, getSiteConfig, getLatestPost functions
769
+ */
770
+ export function registerContentLoader(loader) {
771
+ contentLoader = loader;
772
+ }
808
773
 
809
- // Get gutter content for the about page
810
- const gutterContent = getAboutGutterContent("about");
774
+ /**
775
+ * Get all blog posts
776
+ * @returns {Array} Array of post objects
777
+ */
778
+ export function getAllPosts() {
779
+ if (!contentLoader || !contentLoader.getAllPosts) {
780
+ console.warn('getAllPosts: No content loader registered. Call registerContentLoader() in your site.');
781
+ return [];
782
+ }
783
+ return contentLoader.getAllPosts();
784
+ }
811
785
 
786
+ /**
787
+ * Get site configuration
788
+ * @returns {Object} Site config object
789
+ */
790
+ export function getSiteConfig() {
791
+ if (!contentLoader || !contentLoader.getSiteConfig) {
792
+ console.warn('getSiteConfig: No content loader registered. Call registerContentLoader() in your site.');
812
793
  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,
794
+ owner: { name: "Admin", email: "" },
795
+ site: { title: "GroveEngine Site", description: "", copyright: "" },
796
+ social: {},
820
797
  };
821
- } catch (err) {
822
- console.error("Error in getAboutPage:", err);
823
- return null;
824
798
  }
799
+ return contentLoader.getSiteConfig();
825
800
  }
826
801
 
827
802
  /**
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
803
+ * Get the latest post
804
+ * @returns {Object|null} Latest post or null
831
805
  */
832
- export function getAboutGutterContent(slug) {
833
- return getGutterContentFromModules(
834
- slug,
835
- aboutGutterManifestModules,
836
- aboutGutterMarkdownModules,
837
- aboutGutterImageModules,
838
- );
806
+ export function getLatestPost() {
807
+ if (!contentLoader || !contentLoader.getLatestPost) {
808
+ console.warn('getLatestPost: No content loader registered. Call registerContentLoader() in your site.');
809
+ return null;
810
+ }
811
+ return contentLoader.getLatestPost();
839
812
  }
840
813
 
841
814
  /**
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
815
+ * Get home page content
816
+ * @returns {Object|null} Home page data or null
845
817
  */
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) {
818
+ export function getHomePage() {
819
+ if (!contentLoader || !contentLoader.getHomePage) {
820
+ console.warn('getHomePage: No content loader registered. Call registerContentLoader() in your site.');
857
821
  return null;
858
822
  }
859
-
860
- // The module is already parsed JSON
861
- return entry[1].default || entry[1];
823
+ return contentLoader.getHomePage();
862
824
  }
863
825
 
864
826
  /**
865
- * Get a single recipe by slug
866
- * @param {string} slug - The recipe slug
867
- * @returns {Object|null} Recipe object with content and metadata
827
+ * Get a post by its slug
828
+ * @param {string} slug - The post slug
829
+ * @returns {Object|null} Post object or null
868
830
  */
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) {
831
+ export function getPostBySlug(slug) {
832
+ if (!contentLoader || !contentLoader.getPostBySlug) {
833
+ console.warn('getPostBySlug: No content loader registered. Call registerContentLoader() in your site.');
877
834
  return null;
878
835
  }
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
- };
836
+ return contentLoader.getPostBySlug(slug);
911
837
  }
912
838
 
913
839
  /**
914
- * Process Mermaid diagrams in markdown content
915
- * @param {string} markdown - The markdown content
916
- * @returns {string} Processed markdown with Mermaid diagrams
840
+ * Get about page content
841
+ * @returns {Object|null} About page data or null
917
842
  */
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
- );
843
+ export function getAboutPage() {
844
+ if (!contentLoader || !contentLoader.getAboutPage) {
845
+ console.warn('getAboutPage: No content loader registered. Call registerContentLoader() in your site.');
846
+ return null;
847
+ }
848
+ return contentLoader.getAboutPage();
927
849
  }
928
850
 
929
851
  /**
930
- * Render Mermaid diagrams in the DOM
931
- * This should be called after the content is mounted
852
+ * Get contact page content
853
+ * @returns {Object|null} Contact page data or null
932
854
  */
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
- }
855
+ export function getContactPage() {
856
+ if (!contentLoader || !contentLoader.getContactPage) {
857
+ console.warn('getContactPage: No content loader registered. Call registerContentLoader() in your site.');
858
+ return null;
946
859
  }
860
+ return contentLoader.getContactPage();
947
861
  }