@autumnsgrove/groveengine 0.6.2 → 0.6.4
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/LICENSE +378 -0
- package/dist/auth/jwt.d.ts +10 -4
- package/dist/auth/jwt.js +18 -4
- package/dist/auth/session.d.ts +22 -15
- package/dist/auth/session.js +35 -16
- package/dist/components/admin/GutterManager.svelte +81 -139
- package/dist/components/admin/GutterManager.svelte.d.ts +6 -6
- package/dist/components/admin/MarkdownEditor.svelte +80 -23
- package/dist/components/admin/MarkdownEditor.svelte.d.ts +14 -8
- package/dist/components/admin/composables/useAmbientSounds.svelte.d.ts +52 -2
- package/dist/components/admin/composables/useAmbientSounds.svelte.js +38 -4
- package/dist/components/admin/composables/useCommandPalette.svelte.d.ts +80 -10
- package/dist/components/admin/composables/useCommandPalette.svelte.js +45 -5
- package/dist/components/admin/composables/useDraftManager.svelte.d.ts +76 -14
- package/dist/components/admin/composables/useDraftManager.svelte.js +44 -10
- package/dist/components/admin/composables/useEditorTheme.svelte.d.ts +168 -2
- package/dist/components/admin/composables/useEditorTheme.svelte.js +40 -7
- package/dist/components/admin/composables/useSlashCommands.svelte.d.ts +94 -22
- package/dist/components/admin/composables/useSlashCommands.svelte.js +58 -9
- package/dist/components/admin/composables/useSnippets.svelte.d.ts +51 -2
- package/dist/components/admin/composables/useSnippets.svelte.js +35 -3
- package/dist/components/admin/composables/useWritingSession.svelte.d.ts +64 -6
- package/dist/components/admin/composables/useWritingSession.svelte.js +42 -5
- package/dist/components/custom/ContentWithGutter.svelte +53 -23
- package/dist/components/custom/ContentWithGutter.svelte.d.ts +6 -14
- package/dist/components/custom/GutterItem.svelte +1 -1
- package/dist/components/custom/LeftGutter.svelte +43 -13
- package/dist/components/custom/LeftGutter.svelte.d.ts +6 -6
- package/dist/config/ai-models.js +1 -1
- package/dist/groveauth/client.js +11 -11
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -2
- package/dist/server/logger.d.ts +74 -26
- package/dist/server/logger.js +133 -184
- package/dist/server/services/cache.js +1 -10
- package/dist/ui/components/charts/ActivityOverview.svelte +14 -3
- package/dist/ui/components/charts/ActivityOverview.svelte.d.ts +10 -7
- package/dist/ui/components/charts/RepoBreakdown.svelte +9 -3
- package/dist/ui/components/charts/RepoBreakdown.svelte.d.ts +12 -11
- package/dist/ui/components/charts/Sparkline.svelte +18 -7
- package/dist/ui/components/charts/Sparkline.svelte.d.ts +21 -2
- package/dist/ui/components/gallery/ImageGallery.svelte +12 -8
- package/dist/ui/components/gallery/ImageGallery.svelte.d.ts +2 -2
- package/dist/ui/components/gallery/Lightbox.svelte +5 -2
- package/dist/ui/components/gallery/ZoomableImage.svelte +8 -5
- package/dist/ui/components/primitives/accordion/index.d.ts +1 -1
- package/dist/ui/components/primitives/input/input.svelte.d.ts +1 -1
- package/dist/ui/components/primitives/tabs/index.d.ts +1 -1
- package/dist/ui/components/primitives/textarea/textarea.svelte.d.ts +1 -1
- package/dist/ui/components/ui/Button.svelte +5 -0
- package/dist/ui/components/ui/Button.svelte.d.ts +4 -1
- package/dist/ui/components/ui/Input.svelte +4 -0
- package/dist/ui/components/ui/Input.svelte.d.ts +3 -1
- package/dist/ui/components/ui/Logo.svelte +86 -0
- package/dist/ui/components/ui/Logo.svelte.d.ts +25 -0
- package/dist/ui/components/ui/LogoLoader.svelte +71 -0
- package/dist/ui/components/ui/LogoLoader.svelte.d.ts +9 -0
- package/dist/ui/components/ui/index.d.ts +2 -0
- package/dist/ui/components/ui/index.js +2 -0
- package/dist/ui/tailwind.preset.js +8 -8
- package/dist/utils/api.js +2 -1
- package/dist/utils/debounce.d.ts +4 -3
- package/dist/utils/debounce.js +10 -6
- package/dist/utils/gallery.d.ts +58 -32
- package/dist/utils/gallery.js +111 -129
- package/dist/utils/gutter.d.ts +47 -26
- package/dist/utils/gutter.js +116 -124
- package/dist/utils/imageProcessor.d.ts +66 -19
- package/dist/utils/imageProcessor.js +31 -10
- package/dist/utils/index.d.ts +11 -11
- package/dist/utils/index.js +4 -3
- package/dist/utils/json.js +1 -1
- package/dist/utils/markdown.d.ts +183 -103
- package/dist/utils/markdown.js +517 -678
- package/dist/utils/sanitize.d.ts +22 -12
- package/dist/utils/sanitize.js +268 -282
- package/dist/utils/validation.js +4 -3
- package/package.json +23 -23
- package/static/fonts/alagard.ttf +0 -0
package/dist/utils/markdown.js
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
import { marked } from "marked";
|
|
2
2
|
import matter from "gray-matter";
|
|
3
|
-
import { sanitizeMarkdown } from
|
|
4
|
-
|
|
3
|
+
import { sanitizeMarkdown } from "./sanitize.js";
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// Marked Configuration
|
|
6
|
+
// ============================================================================
|
|
5
7
|
// Configure marked renderer for GitHub-style code blocks
|
|
6
8
|
const renderer = new marked.Renderer();
|
|
7
9
|
renderer.code = function (token) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
.replace(/'/g, "'");
|
|
25
|
-
|
|
26
|
-
return `<div class="rendered-markdown-block">
|
|
10
|
+
// Handle both old (code, language) and new (token) API signatures
|
|
11
|
+
const code = typeof token === "string" ? token : token.text;
|
|
12
|
+
const language = typeof token === "string" ? arguments[1] : token.lang;
|
|
13
|
+
const lang = language || "text";
|
|
14
|
+
// Render markdown/md code blocks as formatted HTML (like GitHub)
|
|
15
|
+
if (lang === "markdown" || lang === "md") {
|
|
16
|
+
// Parse the markdown content and render it
|
|
17
|
+
const renderedContent = marked.parse(code, { async: false });
|
|
18
|
+
// Escape the raw markdown for the copy button
|
|
19
|
+
const escapedCode = code
|
|
20
|
+
.replace(/&/g, "&")
|
|
21
|
+
.replace(/</g, "<")
|
|
22
|
+
.replace(/>/g, ">")
|
|
23
|
+
.replace(/"/g, """)
|
|
24
|
+
.replace(/'/g, "'");
|
|
25
|
+
return `<div class="rendered-markdown-block">
|
|
27
26
|
<div class="rendered-markdown-header">
|
|
28
27
|
<span class="rendered-markdown-label">Markdown</span>
|
|
29
28
|
<button class="code-block-copy" aria-label="Copy markdown to clipboard" data-code="${escapedCode}">
|
|
@@ -38,16 +37,14 @@ renderer.code = function (token) {
|
|
|
38
37
|
${renderedContent}
|
|
39
38
|
</div>
|
|
40
39
|
</div>`;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return `<div class="code-block-wrapper">
|
|
40
|
+
}
|
|
41
|
+
const escapedCode = code
|
|
42
|
+
.replace(/&/g, "&")
|
|
43
|
+
.replace(/</g, "<")
|
|
44
|
+
.replace(/>/g, ">")
|
|
45
|
+
.replace(/"/g, """)
|
|
46
|
+
.replace(/'/g, "'");
|
|
47
|
+
return `<div class="code-block-wrapper">
|
|
51
48
|
<div class="code-block-header">
|
|
52
49
|
<span class="code-block-language">${lang}</span>
|
|
53
50
|
<button class="code-block-copy" aria-label="Copy code to clipboard" data-code="${escapedCode}">
|
|
@@ -61,780 +58,622 @@ renderer.code = function (token) {
|
|
|
61
58
|
<pre><code class="language-${lang}">${escapedCode}</code></pre>
|
|
62
59
|
</div>`;
|
|
63
60
|
};
|
|
64
|
-
|
|
65
61
|
marked.setOptions({
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
renderer: renderer,
|
|
63
|
+
gfm: true,
|
|
64
|
+
breaks: false,
|
|
69
65
|
});
|
|
70
|
-
|
|
66
|
+
// ============================================================================
|
|
67
|
+
// Utility Functions
|
|
68
|
+
// ============================================================================
|
|
71
69
|
/**
|
|
72
70
|
* Validates if a string is a valid URL
|
|
73
|
-
* @param {string} urlString - The string to validate as a URL
|
|
74
|
-
* @returns {boolean} True if the string is a valid URL, false otherwise
|
|
75
71
|
*/
|
|
76
72
|
function isValidUrl(urlString) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
73
|
+
try {
|
|
74
|
+
const url = new URL(urlString);
|
|
75
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
83
80
|
}
|
|
84
|
-
|
|
85
81
|
/**
|
|
86
82
|
* Extract headers from markdown content for table of contents
|
|
87
|
-
* @param {string} markdown - The raw markdown content
|
|
88
|
-
* @returns {Array} Array of header objects with level, text, and id
|
|
89
83
|
*/
|
|
90
84
|
export function extractHeaders(markdown) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
id,
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return headers;
|
|
85
|
+
const headers = [];
|
|
86
|
+
// Remove fenced code blocks before extracting headers
|
|
87
|
+
// This prevents # comments inside code blocks from being treated as headers
|
|
88
|
+
const markdownWithoutCodeBlocks = markdown.replace(/```[\s\S]*?```/g, "");
|
|
89
|
+
const headerRegex = /^(#{1,6})\s+(.+)$/gm;
|
|
90
|
+
let match;
|
|
91
|
+
while ((match = headerRegex.exec(markdownWithoutCodeBlocks)) !== null) {
|
|
92
|
+
const level = match[1].length;
|
|
93
|
+
const text = match[2].trim();
|
|
94
|
+
// Create a slug-style ID from the header text
|
|
95
|
+
const id = text
|
|
96
|
+
.toLowerCase()
|
|
97
|
+
.replace(/[^\w\s-]/g, "")
|
|
98
|
+
.replace(/\s+/g, "-")
|
|
99
|
+
.replace(/-+/g, "-")
|
|
100
|
+
.trim();
|
|
101
|
+
headers.push({
|
|
102
|
+
level,
|
|
103
|
+
text,
|
|
104
|
+
id,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return headers;
|
|
119
108
|
}
|
|
120
|
-
|
|
121
109
|
/**
|
|
122
110
|
* Process anchor tags in HTML content
|
|
123
111
|
* Converts <!-- anchor:tagname --> comments to identifiable span elements
|
|
124
|
-
* @param {string} html - The HTML content
|
|
125
|
-
* @returns {string} HTML with anchor markers converted to spans
|
|
126
112
|
*/
|
|
127
113
|
export function processAnchorTags(html) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
/<!--\s*anchor:([\w-]+)\s*-->/g,
|
|
132
|
-
(match, tagname) =>
|
|
133
|
-
`<span class="anchor-marker" data-anchor="${tagname}"></span>`,
|
|
134
|
-
);
|
|
114
|
+
// Convert <!-- anchor:tagname --> to <span class="anchor-marker" data-anchor="tagname"></span>
|
|
115
|
+
// Supports alphanumeric characters, underscores, and hyphens in tag names
|
|
116
|
+
return html.replace(/<!--\s*anchor:([\w-]+)\s*-->/g, (_match, tagname) => `<span class="anchor-marker" data-anchor="${tagname}"></span>`);
|
|
135
117
|
}
|
|
136
|
-
|
|
137
118
|
/**
|
|
138
119
|
* Parse markdown content and convert to HTML
|
|
139
|
-
* @param {string} markdownContent - The raw markdown content (may include frontmatter)
|
|
140
|
-
* @returns {Object} Object with data (frontmatter), content (HTML), headers, and raw markdown
|
|
141
120
|
*/
|
|
142
121
|
export function parseMarkdownContent(markdownContent) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
content: htmlContent,
|
|
156
|
-
headers,
|
|
157
|
-
rawMarkdown: markdown,
|
|
158
|
-
};
|
|
122
|
+
const { data, content: markdown } = matter(markdownContent);
|
|
123
|
+
let htmlContent = marked.parse(markdown, { async: false });
|
|
124
|
+
// Process anchor tags in the HTML content
|
|
125
|
+
htmlContent = processAnchorTags(htmlContent);
|
|
126
|
+
// Extract headers for table of contents
|
|
127
|
+
const headers = extractHeaders(markdown);
|
|
128
|
+
return {
|
|
129
|
+
data: data,
|
|
130
|
+
content: htmlContent,
|
|
131
|
+
headers,
|
|
132
|
+
rawMarkdown: markdown,
|
|
133
|
+
};
|
|
159
134
|
}
|
|
160
|
-
|
|
161
135
|
/**
|
|
162
136
|
* Parse markdown content with sanitization (for user-facing pages like home, about, contact)
|
|
163
|
-
* @param {string} markdownContent - The raw markdown content (may include frontmatter)
|
|
164
|
-
* @returns {Object} Object with data (frontmatter), content (sanitized HTML), headers
|
|
165
137
|
*/
|
|
166
138
|
export function parseMarkdownContentSanitized(markdownContent) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
};
|
|
139
|
+
const { data, content: markdown } = matter(markdownContent);
|
|
140
|
+
const htmlContent = sanitizeMarkdown(marked.parse(markdown, { async: false }));
|
|
141
|
+
const headers = extractHeaders(markdown);
|
|
142
|
+
return {
|
|
143
|
+
data: data,
|
|
144
|
+
content: htmlContent,
|
|
145
|
+
headers,
|
|
146
|
+
};
|
|
176
147
|
}
|
|
177
|
-
|
|
178
148
|
/**
|
|
179
149
|
* Get gutter content from provided modules
|
|
180
150
|
* This is a utility function that processes gutter manifests, markdown, and images
|
|
181
|
-
*
|
|
182
|
-
* @param {string} slug - The page/post slug
|
|
183
|
-
* @param {Object} manifestModules - The manifest modules (from import.meta.glob)
|
|
184
|
-
* @param {Object} markdownModules - The markdown modules (from import.meta.glob)
|
|
185
|
-
* @param {Object} imageModules - The image modules (from import.meta.glob)
|
|
186
|
-
* @returns {Array} Array of gutter items with content and position info
|
|
187
151
|
*/
|
|
188
|
-
export function processGutterContent(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const htmlContent = marked.parse(markdownContent);
|
|
223
|
-
|
|
224
|
-
return {
|
|
225
|
-
...item,
|
|
226
|
-
content: htmlContent,
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
} else if (item.type === "photo" || item.type === "image") {
|
|
230
|
-
// Check if file is an external URL
|
|
231
|
-
if (item.file && isValidUrl(item.file)) {
|
|
232
|
-
return {
|
|
233
|
-
...item,
|
|
234
|
-
src: item.file,
|
|
235
|
-
};
|
|
152
|
+
export function processGutterContent(slug, manifestModules, markdownModules, imageModules) {
|
|
153
|
+
// Find the manifest file for this page/post
|
|
154
|
+
const manifestEntry = Object.entries(manifestModules).find(([filepath]) => {
|
|
155
|
+
const parts = filepath.split("/");
|
|
156
|
+
const folder = parts[parts.length - 3]; // Get the folder name
|
|
157
|
+
return folder === slug;
|
|
158
|
+
});
|
|
159
|
+
if (!manifestEntry) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
const manifestData = manifestEntry[1];
|
|
163
|
+
const manifest = "default" in manifestData ? manifestData.default : manifestData;
|
|
164
|
+
if (!manifest.items || !Array.isArray(manifest.items)) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
// Process each gutter item
|
|
168
|
+
return manifest.items
|
|
169
|
+
.map((item) => {
|
|
170
|
+
// Destructure to separate images from other properties
|
|
171
|
+
// This ensures proper type handling when spreading
|
|
172
|
+
const { images: rawImages, ...baseItem } = item;
|
|
173
|
+
if (item.type === "comment" || item.type === "markdown") {
|
|
174
|
+
// Find the markdown content file
|
|
175
|
+
const mdEntry = Object.entries(markdownModules).find(([filepath]) => {
|
|
176
|
+
return filepath.includes(`/${slug}/gutter/${item.file}`);
|
|
177
|
+
});
|
|
178
|
+
if (mdEntry) {
|
|
179
|
+
const markdownContent = mdEntry[1];
|
|
180
|
+
const htmlContent = marked.parse(markdownContent, { async: false });
|
|
181
|
+
return {
|
|
182
|
+
...baseItem,
|
|
183
|
+
content: htmlContent,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
236
186
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
187
|
+
else if (item.type === "photo" || item.type === "image") {
|
|
188
|
+
// Check if file is an external URL
|
|
189
|
+
if (item.file && isValidUrl(item.file)) {
|
|
190
|
+
return {
|
|
191
|
+
...baseItem,
|
|
192
|
+
src: item.file,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
// Find the local image file
|
|
196
|
+
const imgEntry = Object.entries(imageModules).find(([filepath]) => {
|
|
197
|
+
return filepath.includes(`/${slug}/gutter/${item.file}`);
|
|
198
|
+
});
|
|
199
|
+
if (imgEntry) {
|
|
200
|
+
return {
|
|
201
|
+
...baseItem,
|
|
202
|
+
src: imgEntry[1],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
248
205
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
206
|
+
else if (item.type === "emoji") {
|
|
207
|
+
// Emoji items can use URLs (local or CDN) or local files
|
|
208
|
+
if (item.url) {
|
|
209
|
+
// Direct URL (local path like /icons/instruction/mix.webp or CDN URL)
|
|
210
|
+
return {
|
|
211
|
+
...baseItem,
|
|
212
|
+
src: item.url,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
else if (item.file) {
|
|
216
|
+
// Local file in gutter directory
|
|
217
|
+
const imgEntry = Object.entries(imageModules).find(([filepath]) => {
|
|
218
|
+
return filepath.includes(`/${slug}/gutter/${item.file}`);
|
|
219
|
+
});
|
|
220
|
+
if (imgEntry) {
|
|
221
|
+
return {
|
|
222
|
+
...baseItem,
|
|
223
|
+
src: imgEntry[1],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return baseItem;
|
|
269
228
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
229
|
+
else if (item.type === "gallery") {
|
|
230
|
+
/**
|
|
231
|
+
* Process gallery items containing multiple images
|
|
232
|
+
*
|
|
233
|
+
* Galleries can contain:
|
|
234
|
+
* - External URLs (validated for http/https protocol)
|
|
235
|
+
* - Local files (resolved from the gutter directory)
|
|
236
|
+
*
|
|
237
|
+
* Images that fail to resolve (invalid URLs or missing files) are filtered out.
|
|
238
|
+
* If all images fail to resolve, the entire gallery item is excluded.
|
|
239
|
+
*/
|
|
240
|
+
const originalImageCount = (rawImages || []).length;
|
|
241
|
+
const images = (rawImages || [])
|
|
242
|
+
.map((img) => {
|
|
243
|
+
// Check if it's an external URL
|
|
244
|
+
if (img.url) {
|
|
245
|
+
// Validate URL format to prevent malformed URLs from failing silently
|
|
246
|
+
if (!isValidUrl(img.url)) {
|
|
247
|
+
console.warn(`Invalid URL in gallery for "${slug}": ${img.url}`);
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
return {
|
|
251
|
+
url: img.url,
|
|
252
|
+
alt: img.alt || "",
|
|
253
|
+
caption: img.caption || "",
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
// Otherwise, look for local file
|
|
257
|
+
if (img.file) {
|
|
258
|
+
const imgEntry = Object.entries(imageModules).find(([filepath]) => {
|
|
259
|
+
return filepath.includes(`/${slug}/gutter/${img.file}`);
|
|
260
|
+
});
|
|
261
|
+
if (imgEntry) {
|
|
262
|
+
return {
|
|
263
|
+
url: imgEntry[1],
|
|
264
|
+
alt: img.alt || "",
|
|
265
|
+
caption: img.caption || "",
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
console.warn(`Local file not found in gallery for "${slug}": ${img.file}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
292
272
|
return null;
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
alt: img.alt || "",
|
|
297
|
-
caption: img.caption || "",
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Otherwise, look for local file
|
|
302
|
-
if (img.file) {
|
|
303
|
-
const imgEntry = Object.entries(imageModules).find(
|
|
304
|
-
([filepath]) => {
|
|
305
|
-
return filepath.includes(`/${slug}/gutter/${img.file}`);
|
|
306
|
-
},
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
if (imgEntry) {
|
|
273
|
+
})
|
|
274
|
+
.filter((img) => img !== null);
|
|
275
|
+
if (images.length > 0) {
|
|
310
276
|
return {
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
caption: img.caption || "",
|
|
277
|
+
...baseItem,
|
|
278
|
+
images,
|
|
314
279
|
};
|
|
315
|
-
} else {
|
|
316
|
-
console.warn(
|
|
317
|
-
`Local file not found in gallery for "${slug}": ${img.file}`,
|
|
318
|
-
);
|
|
319
|
-
}
|
|
320
280
|
}
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
if (images.length > 0) {
|
|
327
|
-
return {
|
|
328
|
-
...item,
|
|
329
|
-
images,
|
|
330
|
-
};
|
|
331
|
-
} else if (originalImageCount > 0) {
|
|
332
|
-
// All images failed to resolve - log warning for debugging
|
|
333
|
-
console.warn(
|
|
334
|
-
`Gallery in "${slug}" has ${originalImageCount} image(s) defined but none could be resolved`,
|
|
335
|
-
);
|
|
281
|
+
else if (originalImageCount > 0) {
|
|
282
|
+
// All images failed to resolve - log warning for debugging
|
|
283
|
+
console.warn(`Gallery in "${slug}" has ${originalImageCount} image(s) defined but none could be resolved`);
|
|
284
|
+
}
|
|
336
285
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
return item;
|
|
286
|
+
return baseItem;
|
|
340
287
|
})
|
|
341
|
-
|
|
342
|
-
(item) =>
|
|
343
|
-
item.content || item.src || item.images || item.type === "emoji",
|
|
344
|
-
); // Filter out items that weren't found
|
|
288
|
+
.filter((item) => item !== null && (!!item.content || !!item.src || !!item.images || item.type === "emoji"));
|
|
345
289
|
}
|
|
346
|
-
|
|
347
290
|
/**
|
|
348
291
|
* Process a list of markdown files into post/recipe objects
|
|
349
|
-
*
|
|
350
|
-
* @param {Object} modules - The modules from import.meta.glob (filepath -> content)
|
|
351
|
-
* @returns {Array} Array of post/content objects with metadata and slug
|
|
352
292
|
*/
|
|
353
293
|
export function processMarkdownModules(modules) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
294
|
+
try {
|
|
295
|
+
const items = Object.entries(modules)
|
|
296
|
+
.map(([filepath, content]) => {
|
|
297
|
+
try {
|
|
298
|
+
// Extract slug from filepath: /path/to/Posts/example.md -> example
|
|
299
|
+
const filename = filepath.split("/").pop();
|
|
300
|
+
if (!filename)
|
|
301
|
+
return null;
|
|
302
|
+
const slug = filename.replace(".md", "");
|
|
303
|
+
const { data } = matter(content);
|
|
304
|
+
return {
|
|
305
|
+
slug,
|
|
306
|
+
title: data.title || "Untitled",
|
|
307
|
+
date: data.date || new Date().toISOString(),
|
|
308
|
+
tags: data.tags || [],
|
|
309
|
+
description: data.description || "",
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
catch (err) {
|
|
313
|
+
console.error(`Error processing file ${filepath}:`, err);
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
})
|
|
317
|
+
.filter((item) => item !== null)
|
|
318
|
+
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
|
|
319
|
+
return items;
|
|
320
|
+
}
|
|
321
|
+
catch (err) {
|
|
322
|
+
console.error("Error in processMarkdownModules:", err);
|
|
323
|
+
return [];
|
|
324
|
+
}
|
|
382
325
|
}
|
|
383
|
-
|
|
384
326
|
/**
|
|
385
327
|
* Get a single item by slug from modules
|
|
386
|
-
*
|
|
387
|
-
* @param {string} slug - The item slug
|
|
388
|
-
* @param {Object} modules - The modules from import.meta.glob (filepath -> content)
|
|
389
|
-
* @param {Object} options - Optional configuration
|
|
390
|
-
* @param {Object} options.gutterModules - Gutter modules { manifest, markdown, images }
|
|
391
|
-
* @param {Object} options.sidecarModules - Sidecar/metadata modules (for recipes)
|
|
392
|
-
* @returns {Object|null} Item object with content and metadata
|
|
393
328
|
*/
|
|
394
329
|
export function getItemBySlug(slug, modules, options = {}) {
|
|
395
|
-
|
|
396
|
-
const entry = Object.entries(modules).find(([filepath]) => {
|
|
397
|
-
const fileSlug = filepath.split("/").pop().replace(".md", "");
|
|
398
|
-
return fileSlug === slug;
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
if (!entry) {
|
|
402
|
-
return null;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
const rawContent = entry[1];
|
|
406
|
-
const { data, content, headers } = parseMarkdownContent(rawContent);
|
|
407
|
-
|
|
408
|
-
// Build the result object
|
|
409
|
-
const result = {
|
|
410
|
-
slug,
|
|
411
|
-
title: data.title || "Untitled",
|
|
412
|
-
date: data.date || new Date().toISOString(),
|
|
413
|
-
tags: data.tags || [],
|
|
414
|
-
description: data.description || "",
|
|
415
|
-
content,
|
|
416
|
-
headers,
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
// Process gutter content if provided
|
|
420
|
-
if (options.gutterModules) {
|
|
421
|
-
const { manifest, markdown, images } = options.gutterModules;
|
|
422
|
-
result.gutterContent = processGutterContent(slug, manifest, markdown, images);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Process sidecar/metadata if provided (for recipes)
|
|
426
|
-
if (options.sidecarModules) {
|
|
427
|
-
const sidecarEntry = Object.entries(options.sidecarModules).find(([filepath]) => {
|
|
428
|
-
const parts = filepath.split("/");
|
|
429
|
-
const folder = parts[parts.length - 3]; // Get the folder name
|
|
430
|
-
return folder === slug;
|
|
431
|
-
});
|
|
432
|
-
|
|
433
|
-
if (sidecarEntry) {
|
|
434
|
-
result.sidecar = sidecarEntry[1].default || sidecarEntry[1];
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return result;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Get a page (home, about, contact) by filename from modules
|
|
443
|
-
* Uses sanitization for security
|
|
444
|
-
*
|
|
445
|
-
* @param {string} filename - The filename to look for (e.g., "home.md", "about.md")
|
|
446
|
-
* @param {Object} modules - The modules from import.meta.glob (filepath -> content)
|
|
447
|
-
* @param {Object} options - Optional configuration
|
|
448
|
-
* @param {Object} options.gutterModules - Gutter modules { manifest, markdown, images }
|
|
449
|
-
* @param {string} options.slug - Override slug (defaults to filename without .md)
|
|
450
|
-
* @returns {Object|null} Page object with content and metadata
|
|
451
|
-
*/
|
|
452
|
-
export function getPageByFilename(filename, modules, options = {}) {
|
|
453
|
-
try {
|
|
454
|
-
// Find the matching file
|
|
330
|
+
// Find the matching module by slug
|
|
455
331
|
const entry = Object.entries(modules).find(([filepath]) => {
|
|
456
|
-
|
|
332
|
+
const filename = filepath.split("/").pop();
|
|
333
|
+
if (!filename)
|
|
334
|
+
return false;
|
|
335
|
+
const fileSlug = filename.replace(".md", "");
|
|
336
|
+
return fileSlug === slug;
|
|
457
337
|
});
|
|
458
|
-
|
|
459
338
|
if (!entry) {
|
|
460
|
-
|
|
339
|
+
return null;
|
|
461
340
|
}
|
|
462
|
-
|
|
463
341
|
const rawContent = entry[1];
|
|
464
|
-
const { data, content, headers } =
|
|
465
|
-
const slug = options.slug || filename.replace(".md", "");
|
|
466
|
-
|
|
342
|
+
const { data, content, headers } = parseMarkdownContent(rawContent);
|
|
467
343
|
// Build the result object
|
|
468
344
|
const result = {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
345
|
+
slug,
|
|
346
|
+
title: data.title || "Untitled",
|
|
347
|
+
date: data.date || new Date().toISOString(),
|
|
348
|
+
tags: data.tags || [],
|
|
349
|
+
description: data.description || "",
|
|
350
|
+
content,
|
|
351
|
+
headers,
|
|
474
352
|
};
|
|
475
|
-
|
|
476
|
-
// Add optional fields from frontmatter
|
|
477
|
-
if (data.date) result.date = data.date;
|
|
478
|
-
if (data.hero) result.hero = data.hero;
|
|
479
|
-
if (data.galleries) result.galleries = data.galleries;
|
|
480
|
-
|
|
481
353
|
// Process gutter content if provided
|
|
482
|
-
if (options.gutterModules) {
|
|
483
|
-
|
|
484
|
-
|
|
354
|
+
if (options.gutterModules?.manifest) {
|
|
355
|
+
const { manifest, markdown = {}, images = {} } = options.gutterModules;
|
|
356
|
+
result.gutterContent = processGutterContent(slug, manifest, markdown, images);
|
|
357
|
+
}
|
|
358
|
+
// Process sidecar/metadata if provided (for recipes)
|
|
359
|
+
if (options.sidecarModules) {
|
|
360
|
+
const sidecarEntry = Object.entries(options.sidecarModules).find(([filepath]) => {
|
|
361
|
+
const parts = filepath.split("/");
|
|
362
|
+
const folder = parts[parts.length - 3]; // Get the folder name
|
|
363
|
+
return folder === slug;
|
|
364
|
+
});
|
|
365
|
+
if (sidecarEntry) {
|
|
366
|
+
const sidecarData = sidecarEntry[1];
|
|
367
|
+
result.sidecar = typeof sidecarData === "object" && sidecarData !== null && "default" in sidecarData
|
|
368
|
+
? sidecarData.default
|
|
369
|
+
: sidecarData;
|
|
370
|
+
}
|
|
485
371
|
}
|
|
486
|
-
|
|
487
372
|
return result;
|
|
488
|
-
} catch (err) {
|
|
489
|
-
console.error(`Error in getPageByFilename for ${filename}:`, err);
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
492
373
|
}
|
|
493
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Get a page (home, about, contact) by filename from modules
|
|
376
|
+
* Uses sanitization for security
|
|
377
|
+
*/
|
|
378
|
+
export function getPageByFilename(filename, modules, options = {}) {
|
|
379
|
+
try {
|
|
380
|
+
// Find the matching file
|
|
381
|
+
const entry = Object.entries(modules).find(([filepath]) => {
|
|
382
|
+
return filepath.includes(filename);
|
|
383
|
+
});
|
|
384
|
+
if (!entry) {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
const rawContent = entry[1];
|
|
388
|
+
const { data, content, headers } = parseMarkdownContentSanitized(rawContent);
|
|
389
|
+
const slug = options.slug || filename.replace(".md", "");
|
|
390
|
+
// Build the result object
|
|
391
|
+
const result = {
|
|
392
|
+
slug,
|
|
393
|
+
title: data.title || slug.charAt(0).toUpperCase() + slug.slice(1),
|
|
394
|
+
description: data.description || "",
|
|
395
|
+
content,
|
|
396
|
+
headers,
|
|
397
|
+
};
|
|
398
|
+
// Add optional fields from frontmatter
|
|
399
|
+
if (data.date)
|
|
400
|
+
result.date = data.date;
|
|
401
|
+
if (data.hero)
|
|
402
|
+
result.hero = data.hero;
|
|
403
|
+
if (data.galleries)
|
|
404
|
+
result.galleries = data.galleries;
|
|
405
|
+
// Process gutter content if provided
|
|
406
|
+
if (options.gutterModules?.manifest) {
|
|
407
|
+
const { manifest, markdown = {}, images = {} } = options.gutterModules;
|
|
408
|
+
result.gutterContent = processGutterContent(slug, manifest, markdown, images);
|
|
409
|
+
}
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
console.error(`Error in getPageByFilename for ${filename}:`, err);
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
494
417
|
/**
|
|
495
418
|
* Get site configuration from a config module
|
|
496
|
-
*
|
|
497
|
-
* @param {Object} configModule - The config module from import.meta.glob
|
|
498
|
-
* @returns {Object} Site configuration object
|
|
499
419
|
*/
|
|
500
420
|
export function getSiteConfigFromModule(configModule) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
421
|
+
const entry = Object.entries(configModule)[0];
|
|
422
|
+
if (entry) {
|
|
423
|
+
const config = entry[1];
|
|
424
|
+
return "default" in config ? config.default : config;
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
owner: { name: "Admin", email: "" },
|
|
428
|
+
site: { title: "The Grove", description: "", copyright: "AutumnsGrove" },
|
|
429
|
+
social: {},
|
|
430
|
+
};
|
|
510
431
|
}
|
|
511
|
-
|
|
512
432
|
/**
|
|
513
433
|
* Create a configured content loader with all functions bound to the provided modules
|
|
514
434
|
* This is the main factory function for creating a content loader in the consuming app
|
|
515
|
-
*
|
|
516
|
-
* @param {Object} config - Configuration object with all required modules
|
|
517
|
-
* @param {Object} config.posts - Post modules from import.meta.glob
|
|
518
|
-
* @param {Object} config.recipes - Recipe modules from import.meta.glob
|
|
519
|
-
* @param {Object} config.about - About page modules from import.meta.glob
|
|
520
|
-
* @param {Object} config.home - Home page modules from import.meta.glob
|
|
521
|
-
* @param {Object} config.contact - Contact page modules from import.meta.glob
|
|
522
|
-
* @param {Object} config.siteConfig - Site config module from import.meta.glob
|
|
523
|
-
* @param {Object} config.postGutter - Post gutter modules { manifest, markdown, images }
|
|
524
|
-
* @param {Object} config.recipeGutter - Recipe gutter modules { manifest, markdown, images }
|
|
525
|
-
* @param {Object} config.recipeMetadata - Recipe metadata modules from import.meta.glob
|
|
526
|
-
* @param {Object} config.aboutGutter - About gutter modules { manifest, markdown, images }
|
|
527
|
-
* @param {Object} config.homeGutter - Home gutter modules { manifest, markdown, images }
|
|
528
|
-
* @param {Object} config.contactGutter - Contact gutter modules { manifest, markdown, images }
|
|
529
|
-
* @returns {Object} Object with all content loader functions
|
|
530
435
|
*/
|
|
531
436
|
export function createContentLoader(config) {
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
);
|
|
666
|
-
},
|
|
667
|
-
|
|
668
|
-
/**
|
|
669
|
-
* Get gutter content for the about page
|
|
670
|
-
*/
|
|
671
|
-
getAboutGutterContent(slug) {
|
|
672
|
-
if (!aboutGutter.manifest) return [];
|
|
673
|
-
return processGutterContent(
|
|
674
|
-
slug,
|
|
675
|
-
aboutGutter.manifest,
|
|
676
|
-
aboutGutter.markdown || {},
|
|
677
|
-
aboutGutter.images || {},
|
|
678
|
-
);
|
|
679
|
-
},
|
|
680
|
-
|
|
681
|
-
/**
|
|
682
|
-
* Get gutter content for the contact page
|
|
683
|
-
*/
|
|
684
|
-
getContactGutterContent(slug) {
|
|
685
|
-
if (!contactGutter.manifest) return [];
|
|
686
|
-
return processGutterContent(
|
|
687
|
-
slug,
|
|
688
|
-
contactGutter.manifest,
|
|
689
|
-
contactGutter.markdown || {},
|
|
690
|
-
contactGutter.images || {},
|
|
691
|
-
);
|
|
692
|
-
},
|
|
693
|
-
|
|
694
|
-
/**
|
|
695
|
-
* Get recipe sidecar/metadata by slug
|
|
696
|
-
*/
|
|
697
|
-
getRecipeSidecar(slug) {
|
|
698
|
-
const entry = Object.entries(recipeMetadata).find(([filepath]) => {
|
|
699
|
-
const parts = filepath.split("/");
|
|
700
|
-
const folder = parts[parts.length - 3];
|
|
701
|
-
return folder === slug;
|
|
702
|
-
});
|
|
703
|
-
|
|
704
|
-
if (!entry) {
|
|
705
|
-
return null;
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
return entry[1].default || entry[1];
|
|
709
|
-
},
|
|
710
|
-
};
|
|
437
|
+
const { posts = {}, recipes = {}, about = {}, home = {}, contact = {}, siteConfig = {}, postGutter = {}, recipeGutter = {}, recipeMetadata = {}, aboutGutter = {}, homeGutter = {}, contactGutter = {}, } = config;
|
|
438
|
+
const loader = {
|
|
439
|
+
/**
|
|
440
|
+
* Get all posts with metadata
|
|
441
|
+
*/
|
|
442
|
+
getAllPosts() {
|
|
443
|
+
return processMarkdownModules(posts);
|
|
444
|
+
},
|
|
445
|
+
/**
|
|
446
|
+
* Get all recipes with metadata
|
|
447
|
+
*/
|
|
448
|
+
getAllRecipes() {
|
|
449
|
+
return processMarkdownModules(recipes);
|
|
450
|
+
},
|
|
451
|
+
/**
|
|
452
|
+
* Get the latest (most recent) post with full content
|
|
453
|
+
*/
|
|
454
|
+
getLatestPost() {
|
|
455
|
+
const allPosts = processMarkdownModules(posts);
|
|
456
|
+
if (allPosts.length === 0) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
return loader.getPostBySlug(allPosts[0].slug);
|
|
460
|
+
},
|
|
461
|
+
/**
|
|
462
|
+
* Get a single post by slug
|
|
463
|
+
*/
|
|
464
|
+
getPostBySlug(slug) {
|
|
465
|
+
return getItemBySlug(slug, posts, {
|
|
466
|
+
gutterModules: postGutter.manifest ? postGutter : undefined,
|
|
467
|
+
});
|
|
468
|
+
},
|
|
469
|
+
/**
|
|
470
|
+
* Get a single recipe by slug
|
|
471
|
+
*/
|
|
472
|
+
getRecipeBySlug(slug) {
|
|
473
|
+
return getItemBySlug(slug, recipes, {
|
|
474
|
+
gutterModules: recipeGutter.manifest ? recipeGutter : undefined,
|
|
475
|
+
sidecarModules: recipeMetadata,
|
|
476
|
+
});
|
|
477
|
+
},
|
|
478
|
+
/**
|
|
479
|
+
* Get the home page content
|
|
480
|
+
*/
|
|
481
|
+
getHomePage() {
|
|
482
|
+
return getPageByFilename("home.md", home, {
|
|
483
|
+
gutterModules: homeGutter.manifest ? homeGutter : undefined,
|
|
484
|
+
slug: "home",
|
|
485
|
+
});
|
|
486
|
+
},
|
|
487
|
+
/**
|
|
488
|
+
* Get the about page content
|
|
489
|
+
*/
|
|
490
|
+
getAboutPage() {
|
|
491
|
+
return getPageByFilename("about.md", about, {
|
|
492
|
+
gutterModules: aboutGutter.manifest ? aboutGutter : undefined,
|
|
493
|
+
slug: "about",
|
|
494
|
+
});
|
|
495
|
+
},
|
|
496
|
+
/**
|
|
497
|
+
* Get the contact page content
|
|
498
|
+
*/
|
|
499
|
+
getContactPage() {
|
|
500
|
+
return getPageByFilename("contact.md", contact, {
|
|
501
|
+
gutterModules: contactGutter.manifest ? contactGutter : undefined,
|
|
502
|
+
slug: "contact",
|
|
503
|
+
});
|
|
504
|
+
},
|
|
505
|
+
/**
|
|
506
|
+
* Get the site configuration
|
|
507
|
+
*/
|
|
508
|
+
getSiteConfig() {
|
|
509
|
+
return getSiteConfigFromModule(siteConfig);
|
|
510
|
+
},
|
|
511
|
+
/**
|
|
512
|
+
* Get gutter content for a post
|
|
513
|
+
*/
|
|
514
|
+
getGutterContent(slug) {
|
|
515
|
+
if (!postGutter.manifest)
|
|
516
|
+
return [];
|
|
517
|
+
return processGutterContent(slug, postGutter.manifest, postGutter.markdown || {}, postGutter.images || {});
|
|
518
|
+
},
|
|
519
|
+
/**
|
|
520
|
+
* Get gutter content for a recipe
|
|
521
|
+
*/
|
|
522
|
+
getRecipeGutterContent(slug) {
|
|
523
|
+
if (!recipeGutter.manifest)
|
|
524
|
+
return [];
|
|
525
|
+
return processGutterContent(slug, recipeGutter.manifest, recipeGutter.markdown || {}, recipeGutter.images || {});
|
|
526
|
+
},
|
|
527
|
+
/**
|
|
528
|
+
* Get gutter content for the home page
|
|
529
|
+
*/
|
|
530
|
+
getHomeGutterContent(slug) {
|
|
531
|
+
if (!homeGutter.manifest)
|
|
532
|
+
return [];
|
|
533
|
+
return processGutterContent(slug, homeGutter.manifest, homeGutter.markdown || {}, homeGutter.images || {});
|
|
534
|
+
},
|
|
535
|
+
/**
|
|
536
|
+
* Get gutter content for the about page
|
|
537
|
+
*/
|
|
538
|
+
getAboutGutterContent(slug) {
|
|
539
|
+
if (!aboutGutter.manifest)
|
|
540
|
+
return [];
|
|
541
|
+
return processGutterContent(slug, aboutGutter.manifest, aboutGutter.markdown || {}, aboutGutter.images || {});
|
|
542
|
+
},
|
|
543
|
+
/**
|
|
544
|
+
* Get gutter content for the contact page
|
|
545
|
+
*/
|
|
546
|
+
getContactGutterContent(slug) {
|
|
547
|
+
if (!contactGutter.manifest)
|
|
548
|
+
return [];
|
|
549
|
+
return processGutterContent(slug, contactGutter.manifest, contactGutter.markdown || {}, contactGutter.images || {});
|
|
550
|
+
},
|
|
551
|
+
/**
|
|
552
|
+
* Get recipe sidecar/metadata by slug
|
|
553
|
+
*/
|
|
554
|
+
getRecipeSidecar(slug) {
|
|
555
|
+
const entry = Object.entries(recipeMetadata).find(([filepath]) => {
|
|
556
|
+
const parts = filepath.split("/");
|
|
557
|
+
const folder = parts[parts.length - 3];
|
|
558
|
+
return folder === slug;
|
|
559
|
+
});
|
|
560
|
+
if (!entry) {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
const data = entry[1];
|
|
564
|
+
return typeof data === "object" && data !== null && "default" in data
|
|
565
|
+
? data.default
|
|
566
|
+
: data;
|
|
567
|
+
},
|
|
568
|
+
};
|
|
569
|
+
return loader;
|
|
711
570
|
}
|
|
712
|
-
|
|
571
|
+
// ============================================================================
|
|
572
|
+
// Global Content Loader Registry
|
|
573
|
+
// ============================================================================
|
|
713
574
|
/**
|
|
714
575
|
* Registry for site-specific content loaders
|
|
715
576
|
* Sites must register their content loaders using registerContentLoader()
|
|
716
577
|
*/
|
|
717
578
|
let contentLoader = null;
|
|
718
|
-
|
|
719
579
|
/**
|
|
720
580
|
* Register a content loader for the site
|
|
721
581
|
* This should be called by the consuming site to provide access to content
|
|
722
|
-
* @param {Object} loader - Object with getAllPosts, getSiteConfig, getLatestPost functions
|
|
723
582
|
*/
|
|
724
583
|
export function registerContentLoader(loader) {
|
|
725
|
-
|
|
584
|
+
contentLoader = loader;
|
|
726
585
|
}
|
|
727
|
-
|
|
728
586
|
/**
|
|
729
587
|
* Get all blog posts
|
|
730
|
-
* @returns {Array} Array of post objects
|
|
731
588
|
*/
|
|
732
589
|
export function getAllPosts() {
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
590
|
+
if (!contentLoader || !contentLoader.getAllPosts) {
|
|
591
|
+
console.warn("getAllPosts: No content loader registered. Call registerContentLoader() in your site.");
|
|
592
|
+
return [];
|
|
593
|
+
}
|
|
594
|
+
return contentLoader.getAllPosts();
|
|
738
595
|
}
|
|
739
|
-
|
|
740
596
|
/**
|
|
741
597
|
* Get site configuration
|
|
742
|
-
* @returns {Object} Site config object
|
|
743
598
|
*/
|
|
744
599
|
export function getSiteConfig() {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
600
|
+
if (!contentLoader || !contentLoader.getSiteConfig) {
|
|
601
|
+
console.warn("getSiteConfig: No content loader registered. Call registerContentLoader() in your site.");
|
|
602
|
+
return {
|
|
603
|
+
owner: { name: "Admin", email: "" },
|
|
604
|
+
site: { title: "GroveEngine Site", description: "", copyright: "" },
|
|
605
|
+
social: {},
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
return contentLoader.getSiteConfig();
|
|
754
609
|
}
|
|
755
|
-
|
|
756
610
|
/**
|
|
757
611
|
* Get the latest post
|
|
758
|
-
* @returns {Object|null} Latest post or null
|
|
759
612
|
*/
|
|
760
613
|
export function getLatestPost() {
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
614
|
+
if (!contentLoader || !contentLoader.getLatestPost) {
|
|
615
|
+
console.warn("getLatestPost: No content loader registered. Call registerContentLoader() in your site.");
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
return contentLoader.getLatestPost();
|
|
766
619
|
}
|
|
767
|
-
|
|
768
620
|
/**
|
|
769
621
|
* Get home page content
|
|
770
|
-
* @returns {Object|null} Home page data or null
|
|
771
622
|
*/
|
|
772
623
|
export function getHomePage() {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
624
|
+
if (!contentLoader || !contentLoader.getHomePage) {
|
|
625
|
+
console.warn("getHomePage: No content loader registered. Call registerContentLoader() in your site.");
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
return contentLoader.getHomePage();
|
|
778
629
|
}
|
|
779
|
-
|
|
780
630
|
/**
|
|
781
631
|
* Get a post by its slug
|
|
782
|
-
* @param {string} slug - The post slug
|
|
783
|
-
* @returns {Object|null} Post object or null
|
|
784
632
|
*/
|
|
785
633
|
export function getPostBySlug(slug) {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
634
|
+
if (!contentLoader || !contentLoader.getPostBySlug) {
|
|
635
|
+
console.warn("getPostBySlug: No content loader registered. Call registerContentLoader() in your site.");
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
return contentLoader.getPostBySlug(slug);
|
|
791
639
|
}
|
|
792
|
-
|
|
793
640
|
/**
|
|
794
641
|
* Get about page content
|
|
795
|
-
* @returns {Object|null} About page data or null
|
|
796
642
|
*/
|
|
797
643
|
export function getAboutPage() {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
644
|
+
if (!contentLoader || !contentLoader.getAboutPage) {
|
|
645
|
+
console.warn("getAboutPage: No content loader registered. Call registerContentLoader() in your site.");
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
return contentLoader.getAboutPage();
|
|
803
649
|
}
|
|
804
|
-
|
|
805
650
|
/**
|
|
806
651
|
* Get contact page content
|
|
807
|
-
* @returns {Object|null} Contact page data or null
|
|
808
652
|
*/
|
|
809
653
|
export function getContactPage() {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
654
|
+
if (!contentLoader || !contentLoader.getContactPage) {
|
|
655
|
+
console.warn("getContactPage: No content loader registered. Call registerContentLoader() in your site.");
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
return contentLoader.getContactPage();
|
|
815
659
|
}
|
|
816
|
-
|
|
817
660
|
/**
|
|
818
661
|
* Get all recipes
|
|
819
|
-
* @returns {Array} Array of recipe objects
|
|
820
662
|
*/
|
|
821
663
|
export function getAllRecipes() {
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
664
|
+
if (!contentLoader || !contentLoader.getAllRecipes) {
|
|
665
|
+
console.warn("getAllRecipes: No content loader registered. Call registerContentLoader() in your site.");
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
return contentLoader.getAllRecipes();
|
|
827
669
|
}
|
|
828
|
-
|
|
829
670
|
/**
|
|
830
671
|
* Get a recipe by its slug
|
|
831
|
-
* @param {string} slug - The recipe slug
|
|
832
|
-
* @returns {Object|null} Recipe object or null
|
|
833
672
|
*/
|
|
834
673
|
export function getRecipeBySlug(slug) {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
674
|
+
if (!contentLoader || !contentLoader.getRecipeBySlug) {
|
|
675
|
+
console.warn("getRecipeBySlug: No content loader registered. Call registerContentLoader() in your site.");
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
return contentLoader.getRecipeBySlug(slug);
|
|
840
679
|
}
|