@autumnsgrove/groveengine 0.1.0 → 0.1.1

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.
@@ -1,29 +1,3 @@
1
- /**
2
- * Get the site configuration
3
- * @returns {Object} Site configuration object
4
- */
5
- export function getSiteConfig(): Object;
6
- /**
7
- * Get all markdown posts from the posts directory
8
- * @returns {Array} Array of post objects with metadata and slug
9
- */
10
- export function getAllPosts(): any[];
11
- /**
12
- * Get the latest (most recent) post with full content
13
- * @returns {Object|null} The latest post object with content, or null if no posts exist
14
- */
15
- export function getLatestPost(): Object | null;
16
- /**
17
- * Get all recipes from the recipes directory
18
- * @returns {Array} Array of recipe objects with metadata and slug
19
- */
20
- export function getAllRecipes(): any[];
21
- /**
22
- * Get a single post by slug
23
- * @param {string} slug - The post slug
24
- * @returns {Object|null} Post object with content and metadata
25
- */
26
- export function getPostBySlug(slug: string): Object | null;
27
1
  /**
28
2
  * Extract headers from markdown content for table of contents
29
3
  * @param {string} markdown - The raw markdown content
@@ -38,64 +12,112 @@ export function extractHeaders(markdown: string): any[];
38
12
  */
39
13
  export function processAnchorTags(html: string): string;
40
14
  /**
41
- * Get gutter content for a recipe by slug
42
- * @param {string} slug - The recipe slug
43
- * @returns {Array} Array of gutter items with content and position info
44
- */
45
- export function getRecipeGutterContent(slug: string): any[];
46
- /**
47
- * Get gutter content for a blog post by slug
48
- * @param {string} slug - The post slug
49
- * @returns {Array} Array of gutter items with content and position info
15
+ * Process Mermaid diagrams in markdown content
16
+ * @param {string} markdown - The markdown content
17
+ * @returns {string} Processed markdown with Mermaid diagrams
50
18
  */
51
- export function getGutterContent(slug: string): any[];
19
+ export function processMermaidDiagrams(markdown: string): string;
52
20
  /**
53
- * Get gutter content for the home page
54
- * @param {string} slug - The page slug (e.g., 'home')
55
- * @returns {Array} Array of gutter items with content and position info
56
- */
57
- export function getHomeGutterContent(slug: string): any[];
58
- /**
59
- * Get gutter content for the contact page
60
- * @param {string} slug - The page slug (e.g., 'contact')
61
- * @returns {Array} Array of gutter items with content and position info
62
- */
63
- export function getContactGutterContent(slug: string): any[];
64
- /**
65
- * Get the home page content
66
- * @returns {Object|null} Home page object with content, metadata, and galleries
21
+ * Render Mermaid diagrams in the DOM
22
+ * This should be called after the content is mounted
67
23
  */
68
- export function getHomePage(): Object | null;
24
+ export function renderMermaidDiagrams(): Promise<void>;
69
25
  /**
70
- * Get the contact page content
71
- * @returns {Object|null} Contact page object with content and metadata
26
+ * Parse markdown content and convert to HTML
27
+ * @param {string} markdownContent - The raw markdown content (may include frontmatter)
28
+ * @returns {Object} Object with data (frontmatter), content (HTML), headers, and raw markdown
72
29
  */
73
- export function getContactPage(): Object | null;
30
+ export function parseMarkdownContent(markdownContent: string): Object;
74
31
  /**
75
- * Get the about page content
76
- * @returns {Object|null} About page object with content and metadata
32
+ * Parse markdown content with sanitization (for user-facing pages like home, about, contact)
33
+ * @param {string} markdownContent - The raw markdown content (may include frontmatter)
34
+ * @returns {Object} Object with data (frontmatter), content (sanitized HTML), headers
77
35
  */
78
- export function getAboutPage(): Object | null;
36
+ export function parseMarkdownContentSanitized(markdownContent: string): Object;
79
37
  /**
80
- * Get gutter content for the about page
81
- * @param {string} slug - The page slug (e.g., 'about')
38
+ * Get gutter content from provided modules
39
+ * This is a utility function that processes gutter manifests, markdown, and images
40
+ *
41
+ * @param {string} slug - The page/post slug
42
+ * @param {Object} manifestModules - The manifest modules (from import.meta.glob)
43
+ * @param {Object} markdownModules - The markdown modules (from import.meta.glob)
44
+ * @param {Object} imageModules - The image modules (from import.meta.glob)
82
45
  * @returns {Array} Array of gutter items with content and position info
83
46
  */
84
- export function getAboutGutterContent(slug: string): any[];
85
- /**
86
- * Get recipe metadata (step icons, etc.) for a recipe by slug
87
- * @param {string} slug - The recipe slug
88
- * @returns {Object|null} Recipe metadata with instruction icons
89
- */
90
- export function getRecipeSidecar(slug: string): Object | null;
91
- /**
92
- * Get a single recipe by slug
93
- * @param {string} slug - The recipe slug
94
- * @returns {Object|null} Recipe object with content and metadata
95
- */
96
- export function getRecipeBySlug(slug: string): Object | null;
97
- /**
98
- * Render Mermaid diagrams in the DOM
99
- * This should be called after the content is mounted
47
+ export function processGutterContent(slug: string, manifestModules: Object, markdownModules: Object, imageModules: Object): any[];
48
+ /**
49
+ * Process a list of markdown files into post/recipe objects
50
+ *
51
+ * @param {Object} modules - The modules from import.meta.glob (filepath -> content)
52
+ * @returns {Array} Array of post/content objects with metadata and slug
53
+ */
54
+ export function processMarkdownModules(modules: Object): any[];
55
+ /**
56
+ * Get a single item by slug from modules
57
+ *
58
+ * @param {string} slug - The item slug
59
+ * @param {Object} modules - The modules from import.meta.glob (filepath -> content)
60
+ * @param {Object} options - Optional configuration
61
+ * @param {Object} options.gutterModules - Gutter modules { manifest, markdown, images }
62
+ * @param {Object} options.sidecarModules - Sidecar/metadata modules (for recipes)
63
+ * @returns {Object|null} Item object with content and metadata
64
+ */
65
+ export function getItemBySlug(slug: string, modules: Object, options?: {
66
+ gutterModules: Object;
67
+ sidecarModules: Object;
68
+ }): Object | null;
69
+ /**
70
+ * Get a page (home, about, contact) by filename from modules
71
+ * Uses sanitization for security
72
+ *
73
+ * @param {string} filename - The filename to look for (e.g., "home.md", "about.md")
74
+ * @param {Object} modules - The modules from import.meta.glob (filepath -> content)
75
+ * @param {Object} options - Optional configuration
76
+ * @param {Object} options.gutterModules - Gutter modules { manifest, markdown, images }
77
+ * @param {string} options.slug - Override slug (defaults to filename without .md)
78
+ * @returns {Object|null} Page object with content and metadata
79
+ */
80
+ export function getPageByFilename(filename: string, modules: Object, options?: {
81
+ gutterModules: Object;
82
+ slug: string;
83
+ }): Object | null;
84
+ /**
85
+ * Get site configuration from a config module
86
+ *
87
+ * @param {Object} configModule - The config module from import.meta.glob
88
+ * @returns {Object} Site configuration object
100
89
  */
101
- export function renderMermaidDiagrams(): Promise<void>;
90
+ export function getSiteConfigFromModule(configModule: Object): Object;
91
+ /**
92
+ * Create a configured content loader with all functions bound to the provided modules
93
+ * This is the main factory function for creating a content loader in the consuming app
94
+ *
95
+ * @param {Object} config - Configuration object with all required modules
96
+ * @param {Object} config.posts - Post modules from import.meta.glob
97
+ * @param {Object} config.recipes - Recipe modules from import.meta.glob
98
+ * @param {Object} config.about - About page modules from import.meta.glob
99
+ * @param {Object} config.home - Home page modules from import.meta.glob
100
+ * @param {Object} config.contact - Contact page modules from import.meta.glob
101
+ * @param {Object} config.siteConfig - Site config module from import.meta.glob
102
+ * @param {Object} config.postGutter - Post gutter modules { manifest, markdown, images }
103
+ * @param {Object} config.recipeGutter - Recipe gutter modules { manifest, markdown, images }
104
+ * @param {Object} config.recipeMetadata - Recipe metadata modules from import.meta.glob
105
+ * @param {Object} config.aboutGutter - About gutter modules { manifest, markdown, images }
106
+ * @param {Object} config.homeGutter - Home gutter modules { manifest, markdown, images }
107
+ * @param {Object} config.contactGutter - Contact gutter modules { manifest, markdown, images }
108
+ * @returns {Object} Object with all content loader functions
109
+ */
110
+ export function createContentLoader(config: {
111
+ posts: Object;
112
+ recipes: Object;
113
+ about: Object;
114
+ home: Object;
115
+ contact: Object;
116
+ siteConfig: Object;
117
+ postGutter: Object;
118
+ recipeGutter: Object;
119
+ recipeMetadata: Object;
120
+ aboutGutter: Object;
121
+ homeGutter: Object;
122
+ contactGutter: Object;
123
+ }): Object;
@@ -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,367 @@ 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
- }
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
- }
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);
675
407
 
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
- }
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));
689
422
 
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
- );
423
+ return items;
424
+ } catch (err) {
425
+ console.error("Error in processMarkdownModules:", err);
426
+ return [];
427
+ }
702
428
  }
703
429
 
704
430
  /**
705
- * Get the home page content
706
- * @returns {Object|null} Home page object with content, metadata, and galleries
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
707
439
  */
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
- });
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
+ });
714
446
 
715
- if (!entry) {
716
- return null;
717
- }
447
+ if (!entry) {
448
+ return null;
449
+ }
718
450
 
719
- const content = entry[1];
451
+ const rawContent = entry[1];
452
+ const { data, content, headers } = parseMarkdownContent(rawContent);
720
453
 
721
- const { data, content: markdown } = matter(content);
722
- const htmlContent = sanitizeMarkdown(marked.parse(markdown));
454
+ // Build the result object
455
+ const result = {
456
+ slug,
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
+ };
723
464
 
724
- // Extract headers for table of contents
725
- const headers = extractHeaders(markdown);
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
+ }
726
470
 
727
- // Get gutter content for the home page
728
- const gutterContent = getHomeGutterContent("home");
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
+ });
729
478
 
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;
479
+ if (sidecarEntry) {
480
+ result.sidecar = sidecarEntry[1].default || sidecarEntry[1];
481
+ }
743
482
  }
483
+
484
+ return result;
744
485
  }
745
486
 
746
487
  /**
747
- * Get the contact page content
748
- * @returns {Object|null} Contact page object with content and metadata
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
749
497
  */
750
- export function getContactPage() {
498
+ export function getPageByFilename(filename, modules, options = {}) {
751
499
  try {
752
- // Find the contact.md file
753
- const entry = Object.entries(contactModules).find(([filepath]) => {
754
- return filepath.includes("contact.md");
500
+ // Find the matching file
501
+ const entry = Object.entries(modules).find(([filepath]) => {
502
+ return filepath.includes(filename);
755
503
  });
756
504
 
757
505
  if (!entry) {
758
506
  return null;
759
507
  }
760
508
 
761
- const content = entry[1];
509
+ const rawContent = entry[1];
510
+ const { data, content, headers } = parseMarkdownContentSanitized(rawContent);
511
+ const slug = options.slug || filename.replace(".md", "");
762
512
 
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",
513
+ // Build the result object
514
+ const result = {
515
+ slug,
516
+ title: data.title || slug.charAt(0).toUpperCase() + slug.slice(1),
775
517
  description: data.description || "",
776
- content: htmlContent,
518
+ content,
777
519
  headers,
778
- gutterContent,
779
520
  };
780
- } catch (err) {
781
- console.error("Error in getContactPage:", err);
782
- return null;
783
- }
784
- }
785
521
 
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
- });
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;
796
526
 
797
- if (!entry) {
798
- return null;
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);
799
531
  }
800
532
 
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
- };
533
+ return result;
821
534
  } catch (err) {
822
- console.error("Error in getAboutPage:", err);
535
+ console.error(`Error in getPageByFilename for ${filename}:`, err);
823
536
  return null;
824
537
  }
825
538
  }
826
539
 
827
540
  /**
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
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
831
545
  */
832
- export function getAboutGutterContent(slug) {
833
- return getGutterContentFromModules(
834
- slug,
835
- aboutGutterManifestModules,
836
- aboutGutterMarkdownModules,
837
- aboutGutterImageModules,
838
- );
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
+ };
839
556
  }
840
557
 
841
558
  /**
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
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
845
576
  */
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
- });
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;
855
592
 
856
- if (!entry) {
857
- return null;
858
- }
593
+ return {
594
+ /**
595
+ * Get all posts with metadata
596
+ */
597
+ getAllPosts() {
598
+ return processMarkdownModules(posts);
599
+ },
859
600
 
860
- // The module is already parsed JSON
861
- return entry[1].default || entry[1];
862
- }
601
+ /**
602
+ * Get all recipes with metadata
603
+ */
604
+ getAllRecipes() {
605
+ return processMarkdownModules(recipes);
606
+ },
863
607
 
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
- });
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
+ },
875
618
 
876
- if (!entry) {
877
- return null;
878
- }
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
+ },
879
627
 
880
- const content = entry[1];
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
+ },
881
637
 
882
- const { data, content: markdown } = matter(content);
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
+ },
883
647
 
884
- // Process Mermaid diagrams in the content
885
- const processedContent = processMermaidDiagrams(markdown);
886
- let htmlContent = marked.parse(processedContent);
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
+ },
887
657
 
888
- // Process anchor tags in the HTML content
889
- htmlContent = processAnchorTags(htmlContent);
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
+ },
890
667
 
891
- // Extract headers for table of contents
892
- const headers = extractHeaders(markdown);
668
+ /**
669
+ * Get the site configuration
670
+ */
671
+ getSiteConfig() {
672
+ return getSiteConfigFromModule(siteConfig);
673
+ },
893
674
 
894
- // Get sidecar data if available
895
- const sidecar = getRecipeSidecar(slug);
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
+ },
896
687
 
897
- // Get gutter content for this recipe
898
- const gutterContent = getRecipeGutterContent(slug);
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
+ },
899
700
 
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
- }
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
+ },
912
713
 
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>`;
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
+ );
925
725
  },
926
- );
927
- }
928
726
 
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");
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
+ },
935
739
 
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
- }
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
+ };
947
757
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autumnsgrove/groveengine",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Multi-tenant blog engine for Grove Platform. Features gutter annotations, markdown editing, magic code auth, and Cloudflare Workers deployment.",
5
5
  "author": "AutumnsGrove",
6
6
  "license": "MIT",