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