@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.
- package/dist/components/admin/GutterManager.svelte +1 -2
- package/dist/components/admin/MarkdownEditor.svelte +1 -2
- package/dist/components/custom/InternalsPostViewer.svelte +95 -0
- package/dist/components/custom/InternalsPostViewer.svelte.d.ts +13 -0
- package/dist/components/ui/index.d.ts +0 -12
- package/dist/components/ui/index.js +2 -13
- package/dist/components/ui/select/select-separator.svelte +2 -3
- package/dist/components/ui/select/select-separator.svelte.d.ts +1 -1
- package/dist/utils/markdown.d.ts +130 -66
- package/dist/utils/markdown.js +482 -568
- package/package.json +2 -1
- package/dist/components/ui/Badge.svelte +0 -48
- package/dist/components/ui/Badge.svelte.d.ts +0 -26
- package/dist/components/ui/Button.svelte +0 -74
- package/dist/components/ui/Button.svelte.d.ts +0 -34
- package/dist/components/ui/Card.svelte +0 -102
- package/dist/components/ui/Card.svelte.d.ts +0 -46
- package/dist/components/ui/Input.svelte +0 -81
- package/dist/components/ui/Input.svelte.d.ts +0 -35
- package/dist/components/ui/Skeleton.svelte +0 -31
- package/dist/components/ui/Skeleton.svelte.d.ts +0 -26
- package/dist/components/ui/Textarea.svelte +0 -81
- package/dist/components/ui/Textarea.svelte.d.ts +0 -35
- package/dist/components/ui/badge/badge.svelte +0 -50
- package/dist/components/ui/badge/badge.svelte.d.ts +0 -60
- package/dist/components/ui/badge/index.d.ts +0 -2
- package/dist/components/ui/badge/index.js +0 -2
- package/dist/components/ui/button/button.svelte +0 -82
- package/dist/components/ui/button/button.svelte.d.ts +0 -132
- package/dist/components/ui/button/index.d.ts +0 -2
- package/dist/components/ui/button/index.js +0 -4
- package/dist/components/ui/card/card-content.svelte +0 -16
- package/dist/components/ui/card/card-content.svelte.d.ts +0 -5
- package/dist/components/ui/card/card-description.svelte +0 -16
- package/dist/components/ui/card/card-description.svelte.d.ts +0 -5
- package/dist/components/ui/card/card-footer.svelte +0 -16
- package/dist/components/ui/card/card-footer.svelte.d.ts +0 -5
- package/dist/components/ui/card/card-header.svelte +0 -16
- package/dist/components/ui/card/card-header.svelte.d.ts +0 -5
- package/dist/components/ui/card/card-title.svelte +0 -25
- package/dist/components/ui/card/card-title.svelte.d.ts +0 -8
- package/dist/components/ui/card/card.svelte +0 -20
- package/dist/components/ui/card/card.svelte.d.ts +0 -5
- package/dist/components/ui/card/index.d.ts +0 -7
- package/dist/components/ui/card/index.js +0 -9
- package/dist/components/ui/input/index.d.ts +0 -2
- package/dist/components/ui/input/index.js +0 -4
- package/dist/components/ui/input/input.svelte +0 -46
- package/dist/components/ui/input/input.svelte.d.ts +0 -13
- package/dist/components/ui/separator/index.d.ts +0 -2
- package/dist/components/ui/separator/index.js +0 -4
- package/dist/components/ui/separator/separator.svelte +0 -22
- package/dist/components/ui/separator/separator.svelte.d.ts +0 -4
- package/dist/components/ui/skeleton/index.d.ts +0 -2
- package/dist/components/ui/skeleton/index.js +0 -4
- package/dist/components/ui/skeleton/skeleton.svelte +0 -17
- package/dist/components/ui/skeleton/skeleton.svelte.d.ts +0 -5
- package/dist/components/ui/textarea/index.d.ts +0 -2
- package/dist/components/ui/textarea/index.js +0 -4
- package/dist/components/ui/textarea/textarea.svelte +0 -24
- package/dist/components/ui/textarea/textarea.svelte.d.ts +0 -6
package/dist/utils/markdown.js
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
485
|
-
* @param {Object} markdownModules - The markdown modules
|
|
486
|
-
* @param {Object} imageModules - The image modules
|
|
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
|
|
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
|
-
*
|
|
650
|
-
*
|
|
651
|
-
* @
|
|
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
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
678
|
-
|
|
679
|
-
|
|
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
|
|
692
|
-
*
|
|
693
|
-
* @
|
|
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
|
|
696
|
-
|
|
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
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
|
706
|
-
*
|
|
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
|
|
498
|
+
export function getPageByFilename(filename, modules, options = {}) {
|
|
709
499
|
try {
|
|
710
|
-
// Find the
|
|
711
|
-
const entry = Object.entries(
|
|
712
|
-
return filepath.includes(
|
|
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
|
|
720
|
-
|
|
721
|
-
const
|
|
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
|
-
|
|
731
|
-
|
|
732
|
-
|
|
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
|
-
|
|
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(
|
|
535
|
+
console.error(`Error in getPageByFilename for ${filename}:`, err);
|
|
742
536
|
return null;
|
|
743
537
|
}
|
|
744
538
|
}
|
|
745
539
|
|
|
746
540
|
/**
|
|
747
|
-
* Get
|
|
748
|
-
*
|
|
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
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
758
|
-
|
|
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
|
-
|
|
593
|
+
return {
|
|
594
|
+
/**
|
|
595
|
+
* Get all posts with metadata
|
|
596
|
+
*/
|
|
597
|
+
getAllPosts() {
|
|
598
|
+
return processMarkdownModules(posts);
|
|
599
|
+
},
|
|
762
600
|
|
|
763
|
-
|
|
764
|
-
|
|
601
|
+
/**
|
|
602
|
+
* Get all recipes with metadata
|
|
603
|
+
*/
|
|
604
|
+
getAllRecipes() {
|
|
605
|
+
return processMarkdownModules(recipes);
|
|
606
|
+
},
|
|
765
607
|
|
|
766
|
-
|
|
767
|
-
|
|
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
|
-
|
|
770
|
-
|
|
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
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
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
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
|
|
798
|
-
|
|
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
|
-
|
|
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
|
-
|
|
804
|
-
|
|
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
|
-
|
|
807
|
-
|
|
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
|
-
|
|
810
|
-
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
|
829
|
-
* @
|
|
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
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
|
843
|
-
* @
|
|
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
|
|
847
|
-
|
|
848
|
-
|
|
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
|
|
866
|
-
* @param {string} slug - The
|
|
867
|
-
* @returns {Object|null}
|
|
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
|
|
870
|
-
|
|
871
|
-
|
|
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
|
-
*
|
|
915
|
-
* @
|
|
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
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
*
|
|
931
|
-
*
|
|
852
|
+
* Get contact page content
|
|
853
|
+
* @returns {Object|null} Contact page data or null
|
|
932
854
|
*/
|
|
933
|
-
export
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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
|
}
|