@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,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Gutter Utilities
|
|
3
|
+
*
|
|
4
|
+
* This module provides common utilities for gutter content positioning
|
|
5
|
+
* and anchor resolution. Used by ContentWithGutter component and related
|
|
6
|
+
* functionality across the site.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parse anchor string to determine anchor type and value
|
|
11
|
+
* @param {string} anchor - The anchor string from manifest
|
|
12
|
+
* @returns {Object} Object with type and value properties
|
|
13
|
+
*/
|
|
14
|
+
export function parseAnchor(anchor) {
|
|
15
|
+
if (!anchor) {
|
|
16
|
+
return { type: 'none', value: null };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Check for paragraph anchor: "paragraph:N"
|
|
20
|
+
const paragraphMatch = anchor.match(/^paragraph:(\d+)$/);
|
|
21
|
+
if (paragraphMatch) {
|
|
22
|
+
return { type: 'paragraph', value: parseInt(paragraphMatch[1], 10) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Check for tag anchor: "anchor:tagname" (supports alphanumeric, underscores, and hyphens)
|
|
26
|
+
const tagMatch = anchor.match(/^anchor:([\w-]+)$/);
|
|
27
|
+
if (tagMatch) {
|
|
28
|
+
return { type: 'tag', value: tagMatch[1] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check for header anchor: "## Header Text"
|
|
32
|
+
const headerMatch = anchor.match(/^(#{1,6})\s+(.+)$/);
|
|
33
|
+
if (headerMatch) {
|
|
34
|
+
return { type: 'header', value: anchor };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Unknown format - treat as header for backwards compatibility
|
|
38
|
+
return { type: 'header', value: anchor };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate a unique key for an anchor (used for grouping and positioning)
|
|
43
|
+
* @param {string} anchor - The anchor string
|
|
44
|
+
* @param {Array} headers - Array of header objects with id and text
|
|
45
|
+
* @returns {string} A unique key for the anchor
|
|
46
|
+
*/
|
|
47
|
+
export function getAnchorKey(anchor, headers = []) {
|
|
48
|
+
const parsed = parseAnchor(anchor);
|
|
49
|
+
switch (parsed.type) {
|
|
50
|
+
case 'header': {
|
|
51
|
+
const headerText = anchor.replace(/^#+\s*/, '');
|
|
52
|
+
const header = headers.find(h => h.text === headerText);
|
|
53
|
+
return header ? `header:${header.id}` : `header:${anchor}`;
|
|
54
|
+
}
|
|
55
|
+
case 'paragraph':
|
|
56
|
+
return `paragraph:${parsed.value}`;
|
|
57
|
+
case 'tag':
|
|
58
|
+
return `tag:${parsed.value}`;
|
|
59
|
+
default:
|
|
60
|
+
return `unknown:${anchor}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get all unique anchors from items (preserving order)
|
|
66
|
+
* @param {Array} items - Array of gutter items
|
|
67
|
+
* @returns {Array} Array of unique anchor strings
|
|
68
|
+
*/
|
|
69
|
+
export function getUniqueAnchors(items) {
|
|
70
|
+
if (!items) return [];
|
|
71
|
+
const seen = new Set();
|
|
72
|
+
const anchors = [];
|
|
73
|
+
for (const item of items) {
|
|
74
|
+
if (item.anchor && !seen.has(item.anchor)) {
|
|
75
|
+
seen.add(item.anchor);
|
|
76
|
+
anchors.push(item.anchor);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return anchors;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get display label for an anchor (used in overflow section)
|
|
84
|
+
* @param {string} anchor - The anchor string
|
|
85
|
+
* @returns {string} Human-readable label for the anchor
|
|
86
|
+
*/
|
|
87
|
+
export function getAnchorLabel(anchor) {
|
|
88
|
+
const parsed = parseAnchor(anchor);
|
|
89
|
+
switch (parsed.type) {
|
|
90
|
+
case 'header':
|
|
91
|
+
return anchor.replace(/^#+\s*/, '');
|
|
92
|
+
case 'paragraph':
|
|
93
|
+
return `Paragraph ${parsed.value}`;
|
|
94
|
+
case 'tag':
|
|
95
|
+
return `Tag: ${parsed.value}`;
|
|
96
|
+
default:
|
|
97
|
+
return anchor;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get items that match a specific anchor
|
|
103
|
+
* @param {Array} items - Array of gutter items
|
|
104
|
+
* @param {string} anchor - The anchor to match
|
|
105
|
+
* @returns {Array} Items matching the anchor
|
|
106
|
+
*/
|
|
107
|
+
export function getItemsForAnchor(items, anchor) {
|
|
108
|
+
if (!items) return [];
|
|
109
|
+
return items.filter(item => item.anchor === anchor);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get items that don't have a valid anchor (orphan items shown at top)
|
|
114
|
+
* @param {Array} items - Array of gutter items
|
|
115
|
+
* @param {Array} headers - Array of header objects
|
|
116
|
+
* @returns {Array} Items without valid anchors
|
|
117
|
+
*/
|
|
118
|
+
export function getOrphanItems(items, headers = []) {
|
|
119
|
+
if (!items) return [];
|
|
120
|
+
return items.filter(item => {
|
|
121
|
+
if (!item.anchor) return true;
|
|
122
|
+
const parsed = parseAnchor(item.anchor);
|
|
123
|
+
if (parsed.type === 'header') {
|
|
124
|
+
const headerText = item.anchor.replace(/^#+\s*/, '');
|
|
125
|
+
return !headers.find(h => h.text === headerText);
|
|
126
|
+
}
|
|
127
|
+
// Paragraph and tag anchors are valid if they have values
|
|
128
|
+
return parsed.type === 'none';
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Find the DOM element for an anchor within a content element
|
|
134
|
+
* @param {string} anchor - The anchor string
|
|
135
|
+
* @param {HTMLElement} contentEl - The content container element
|
|
136
|
+
* @param {Array} headers - Array of header objects
|
|
137
|
+
* @returns {HTMLElement|null} The DOM element or null if not found
|
|
138
|
+
*/
|
|
139
|
+
export function findAnchorElement(anchor, contentEl, headers = []) {
|
|
140
|
+
if (!contentEl) return null;
|
|
141
|
+
|
|
142
|
+
const parsed = parseAnchor(anchor);
|
|
143
|
+
|
|
144
|
+
switch (parsed.type) {
|
|
145
|
+
case 'header': {
|
|
146
|
+
const headerText = anchor.replace(/^#+\s*/, '');
|
|
147
|
+
const header = headers.find(h => h.text === headerText);
|
|
148
|
+
if (header) {
|
|
149
|
+
return document.getElementById(header.id);
|
|
150
|
+
}
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
case 'paragraph': {
|
|
154
|
+
// Select only direct child paragraphs to avoid counting paragraphs
|
|
155
|
+
// inside blockquotes, list items, etc.
|
|
156
|
+
const paragraphs = contentEl.querySelectorAll(':scope > p');
|
|
157
|
+
const index = parsed.value - 1; // Convert to 0-based index
|
|
158
|
+
if (index >= 0 && index < paragraphs.length) {
|
|
159
|
+
return paragraphs[index];
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
case 'tag': {
|
|
164
|
+
return contentEl.querySelector(`[data-anchor="${parsed.value}"]`);
|
|
165
|
+
}
|
|
166
|
+
default:
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side image processing utility
|
|
3
|
+
* Handles WebP conversion, quality adjustment, EXIF stripping, and hash generation
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Calculate SHA-256 hash of file for duplicate detection
|
|
7
|
+
* @param {File|Blob} file - The file to hash
|
|
8
|
+
* @returns {Promise<string>} Hex string of the hash
|
|
9
|
+
*/
|
|
10
|
+
export function calculateFileHash(file: File | Blob): Promise<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Process an image: convert to WebP, adjust quality, strip EXIF
|
|
13
|
+
* Drawing to canvas automatically strips EXIF data including GPS
|
|
14
|
+
*
|
|
15
|
+
* @param {File} file - Original image file
|
|
16
|
+
* @param {Object} options - Processing options
|
|
17
|
+
* @param {number} options.quality - Quality 0-100 (default 80)
|
|
18
|
+
* @param {boolean} options.convertToWebP - Convert to WebP format (default true)
|
|
19
|
+
* @param {boolean} options.fullResolution - Skip resizing (default false)
|
|
20
|
+
* @returns {Promise<{ blob: Blob, width: number, height: number, originalSize: number, processedSize: number }>}
|
|
21
|
+
*/
|
|
22
|
+
export function processImage(file: File, options?: {
|
|
23
|
+
quality: number;
|
|
24
|
+
convertToWebP: boolean;
|
|
25
|
+
fullResolution: boolean;
|
|
26
|
+
}): Promise<{
|
|
27
|
+
blob: Blob;
|
|
28
|
+
width: number;
|
|
29
|
+
height: number;
|
|
30
|
+
originalSize: number;
|
|
31
|
+
processedSize: number;
|
|
32
|
+
}>;
|
|
33
|
+
/**
|
|
34
|
+
* Generate a date-based path for organizing uploads
|
|
35
|
+
* Format: photos/YYYY/MM/DD/
|
|
36
|
+
* @returns {string} Date-based folder path
|
|
37
|
+
*/
|
|
38
|
+
export function generateDatePath(): string;
|
|
39
|
+
/**
|
|
40
|
+
* Generate a clean filename from original name
|
|
41
|
+
* @param {string} originalName - Original filename
|
|
42
|
+
* @param {boolean} useWebP - Whether to use .webp extension
|
|
43
|
+
* @returns {string} Sanitized filename
|
|
44
|
+
*/
|
|
45
|
+
export function sanitizeFilename(originalName: string, useWebP?: boolean): string;
|
|
46
|
+
/**
|
|
47
|
+
* Format bytes to human-readable string
|
|
48
|
+
* @param {number} bytes - Size in bytes
|
|
49
|
+
* @returns {string} Formatted size
|
|
50
|
+
*/
|
|
51
|
+
export function formatBytes(bytes: number): string;
|
|
52
|
+
/**
|
|
53
|
+
* Calculate compression ratio
|
|
54
|
+
* @param {number} original - Original size in bytes
|
|
55
|
+
* @param {number} processed - Processed size in bytes
|
|
56
|
+
* @returns {string} Percentage saved
|
|
57
|
+
*/
|
|
58
|
+
export function compressionRatio(original: number, processed: number): string;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side image processing utility
|
|
3
|
+
* Handles WebP conversion, quality adjustment, EXIF stripping, and hash generation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Calculate SHA-256 hash of file for duplicate detection
|
|
8
|
+
* @param {File|Blob} file - The file to hash
|
|
9
|
+
* @returns {Promise<string>} Hex string of the hash
|
|
10
|
+
*/
|
|
11
|
+
export async function calculateFileHash(file) {
|
|
12
|
+
const buffer = await file.arrayBuffer();
|
|
13
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
|
|
14
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
15
|
+
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load an image from a File object
|
|
20
|
+
* @param {File} file - Image file
|
|
21
|
+
* @returns {Promise<HTMLImageElement>}
|
|
22
|
+
*/
|
|
23
|
+
function loadImage(file) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
const img = new Image();
|
|
26
|
+
img.onload = () => {
|
|
27
|
+
URL.revokeObjectURL(img.src);
|
|
28
|
+
resolve(img);
|
|
29
|
+
};
|
|
30
|
+
img.onerror = () => {
|
|
31
|
+
URL.revokeObjectURL(img.src);
|
|
32
|
+
reject(new Error('Failed to load image'));
|
|
33
|
+
};
|
|
34
|
+
img.src = URL.createObjectURL(file);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Calculate dimensions maintaining aspect ratio
|
|
40
|
+
* @param {number} width - Original width
|
|
41
|
+
* @param {number} height - Original height
|
|
42
|
+
* @param {number} maxDimension - Maximum dimension (width or height)
|
|
43
|
+
* @returns {{ width: number, height: number }}
|
|
44
|
+
*/
|
|
45
|
+
function calculateDimensions(width, height, maxDimension) {
|
|
46
|
+
if (width <= maxDimension && height <= maxDimension) {
|
|
47
|
+
return { width, height };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ratio = Math.min(maxDimension / width, maxDimension / height);
|
|
51
|
+
return {
|
|
52
|
+
width: Math.round(width * ratio),
|
|
53
|
+
height: Math.round(height * ratio)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get max dimension based on quality setting
|
|
59
|
+
* Higher quality = larger max dimension
|
|
60
|
+
* @param {number} quality - Quality 0-100
|
|
61
|
+
* @returns {number} Max dimension in pixels
|
|
62
|
+
*/
|
|
63
|
+
function getMaxDimensionForQuality(quality) {
|
|
64
|
+
if (quality >= 90) return 4096;
|
|
65
|
+
if (quality >= 70) return 2560;
|
|
66
|
+
if (quality >= 50) return 1920;
|
|
67
|
+
if (quality >= 30) return 1280;
|
|
68
|
+
return 960;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Process an image: convert to WebP, adjust quality, strip EXIF
|
|
73
|
+
* Drawing to canvas automatically strips EXIF data including GPS
|
|
74
|
+
*
|
|
75
|
+
* @param {File} file - Original image file
|
|
76
|
+
* @param {Object} options - Processing options
|
|
77
|
+
* @param {number} options.quality - Quality 0-100 (default 80)
|
|
78
|
+
* @param {boolean} options.convertToWebP - Convert to WebP format (default true)
|
|
79
|
+
* @param {boolean} options.fullResolution - Skip resizing (default false)
|
|
80
|
+
* @returns {Promise<{ blob: Blob, width: number, height: number, originalSize: number, processedSize: number }>}
|
|
81
|
+
*/
|
|
82
|
+
export async function processImage(file, options = {}) {
|
|
83
|
+
const {
|
|
84
|
+
quality = 80,
|
|
85
|
+
convertToWebP = true,
|
|
86
|
+
fullResolution = false
|
|
87
|
+
} = options;
|
|
88
|
+
|
|
89
|
+
// For GIFs, return original to preserve animation
|
|
90
|
+
if (file.type === 'image/gif') {
|
|
91
|
+
return {
|
|
92
|
+
blob: file,
|
|
93
|
+
width: 0,
|
|
94
|
+
height: 0,
|
|
95
|
+
originalSize: file.size,
|
|
96
|
+
processedSize: file.size,
|
|
97
|
+
skipped: true,
|
|
98
|
+
reason: 'GIF preserved for animation'
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const img = await loadImage(file);
|
|
103
|
+
const originalSize = file.size;
|
|
104
|
+
|
|
105
|
+
// Calculate target dimensions
|
|
106
|
+
let targetWidth = img.naturalWidth;
|
|
107
|
+
let targetHeight = img.naturalHeight;
|
|
108
|
+
|
|
109
|
+
if (!fullResolution) {
|
|
110
|
+
const maxDim = getMaxDimensionForQuality(quality);
|
|
111
|
+
const dims = calculateDimensions(img.naturalWidth, img.naturalHeight, maxDim);
|
|
112
|
+
targetWidth = dims.width;
|
|
113
|
+
targetHeight = dims.height;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Create canvas and draw image (this strips EXIF data)
|
|
117
|
+
const canvas = document.createElement('canvas');
|
|
118
|
+
canvas.width = targetWidth;
|
|
119
|
+
canvas.height = targetHeight;
|
|
120
|
+
|
|
121
|
+
const ctx = canvas.getContext('2d');
|
|
122
|
+
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
|
|
123
|
+
|
|
124
|
+
// Convert to blob
|
|
125
|
+
const mimeType = convertToWebP ? 'image/webp' : file.type;
|
|
126
|
+
const qualityDecimal = quality / 100;
|
|
127
|
+
|
|
128
|
+
const blob = await new Promise((resolve) => {
|
|
129
|
+
canvas.toBlob(resolve, mimeType, qualityDecimal);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
blob,
|
|
134
|
+
width: targetWidth,
|
|
135
|
+
height: targetHeight,
|
|
136
|
+
originalSize,
|
|
137
|
+
processedSize: blob.size,
|
|
138
|
+
skipped: false
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Generate a date-based path for organizing uploads
|
|
144
|
+
* Format: photos/YYYY/MM/DD/
|
|
145
|
+
* @returns {string} Date-based folder path
|
|
146
|
+
*/
|
|
147
|
+
export function generateDatePath() {
|
|
148
|
+
const now = new Date();
|
|
149
|
+
const year = now.getFullYear();
|
|
150
|
+
const month = String(now.getMonth() + 1).padStart(2, '0');
|
|
151
|
+
const day = String(now.getDate()).padStart(2, '0');
|
|
152
|
+
return `photos/${year}/${month}/${day}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Generate a clean filename from original name
|
|
157
|
+
* @param {string} originalName - Original filename
|
|
158
|
+
* @param {boolean} useWebP - Whether to use .webp extension
|
|
159
|
+
* @returns {string} Sanitized filename
|
|
160
|
+
*/
|
|
161
|
+
export function sanitizeFilename(originalName, useWebP = true) {
|
|
162
|
+
// Get base name without extension
|
|
163
|
+
const lastDot = originalName.lastIndexOf('.');
|
|
164
|
+
const baseName = lastDot > 0 ? originalName.substring(0, lastDot) : originalName;
|
|
165
|
+
const originalExt = lastDot > 0 ? originalName.substring(lastDot + 1).toLowerCase() : '';
|
|
166
|
+
|
|
167
|
+
// Sanitize the base name
|
|
168
|
+
const sanitized = baseName
|
|
169
|
+
.toLowerCase()
|
|
170
|
+
.replace(/[^a-z0-9]/g, '-')
|
|
171
|
+
.replace(/-+/g, '-')
|
|
172
|
+
.replace(/^-|-$/g, '')
|
|
173
|
+
.substring(0, 100); // Limit length
|
|
174
|
+
|
|
175
|
+
// Add timestamp for uniqueness
|
|
176
|
+
const timestamp = Date.now().toString(36);
|
|
177
|
+
|
|
178
|
+
// Determine extension
|
|
179
|
+
const ext = useWebP && originalExt !== 'gif' ? 'webp' : originalExt;
|
|
180
|
+
|
|
181
|
+
return `${sanitized}-${timestamp}.${ext}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Format bytes to human-readable string
|
|
186
|
+
* @param {number} bytes - Size in bytes
|
|
187
|
+
* @returns {string} Formatted size
|
|
188
|
+
*/
|
|
189
|
+
export function formatBytes(bytes) {
|
|
190
|
+
if (bytes < 1024) return bytes + ' B';
|
|
191
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
192
|
+
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Calculate compression ratio
|
|
197
|
+
* @param {number} original - Original size in bytes
|
|
198
|
+
* @param {number} processed - Processed size in bytes
|
|
199
|
+
* @returns {string} Percentage saved
|
|
200
|
+
*/
|
|
201
|
+
export function compressionRatio(original, processed) {
|
|
202
|
+
if (original <= 0) return '0%';
|
|
203
|
+
const saved = ((original - processed) / original) * 100;
|
|
204
|
+
return saved > 0 ? `-${saved.toFixed(0)}%` : `+${Math.abs(saved).toFixed(0)}%`;
|
|
205
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON utility functions
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Safely parse JSON with fallback for corrupted or missing data.
|
|
6
|
+
* Prevents crashes when parsing malformed JSON from external sources.
|
|
7
|
+
*
|
|
8
|
+
* @param {string|null|undefined} str - JSON string to parse
|
|
9
|
+
* @param {*} fallback - Fallback value if parsing fails (default: [])
|
|
10
|
+
* @returns {*} Parsed value or fallback
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* safeJsonParse('["a","b"]', []) // Returns ['a', 'b']
|
|
14
|
+
* safeJsonParse('invalid', []) // Returns []
|
|
15
|
+
* safeJsonParse(null, {}) // Returns {}
|
|
16
|
+
*/
|
|
17
|
+
export function safeJsonParse(str: string | null | undefined, fallback?: any): any;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON utility functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Safely parse JSON with fallback for corrupted or missing data.
|
|
7
|
+
* Prevents crashes when parsing malformed JSON from external sources.
|
|
8
|
+
*
|
|
9
|
+
* @param {string|null|undefined} str - JSON string to parse
|
|
10
|
+
* @param {*} fallback - Fallback value if parsing fails (default: [])
|
|
11
|
+
* @returns {*} Parsed value or fallback
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* safeJsonParse('["a","b"]', []) // Returns ['a', 'b']
|
|
15
|
+
* safeJsonParse('invalid', []) // Returns []
|
|
16
|
+
* safeJsonParse(null, {}) // Returns {}
|
|
17
|
+
*/
|
|
18
|
+
export function safeJsonParse(str, fallback = []) {
|
|
19
|
+
if (!str) return fallback;
|
|
20
|
+
try {
|
|
21
|
+
return JSON.parse(str);
|
|
22
|
+
} catch (e) {
|
|
23
|
+
console.warn('Failed to parse JSON:', e.message);
|
|
24
|
+
return fallback;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the site configuration
|
|
3
|
+
* @returns {Object} Site configuration object
|
|
4
|
+
*/
|
|
5
|
+
export function getSiteConfig(): Object;
|
|
6
|
+
/**
|
|
7
|
+
* Get all markdown posts from the posts directory
|
|
8
|
+
* @returns {Array} Array of post objects with metadata and slug
|
|
9
|
+
*/
|
|
10
|
+
export function getAllPosts(): any[];
|
|
11
|
+
/**
|
|
12
|
+
* Get the latest (most recent) post with full content
|
|
13
|
+
* @returns {Object|null} The latest post object with content, or null if no posts exist
|
|
14
|
+
*/
|
|
15
|
+
export function getLatestPost(): Object | null;
|
|
16
|
+
/**
|
|
17
|
+
* Get all recipes from the recipes directory
|
|
18
|
+
* @returns {Array} Array of recipe objects with metadata and slug
|
|
19
|
+
*/
|
|
20
|
+
export function getAllRecipes(): any[];
|
|
21
|
+
/**
|
|
22
|
+
* Get a single post by slug
|
|
23
|
+
* @param {string} slug - The post slug
|
|
24
|
+
* @returns {Object|null} Post object with content and metadata
|
|
25
|
+
*/
|
|
26
|
+
export function getPostBySlug(slug: string): Object | null;
|
|
27
|
+
/**
|
|
28
|
+
* Extract headers from markdown content for table of contents
|
|
29
|
+
* @param {string} markdown - The raw markdown content
|
|
30
|
+
* @returns {Array} Array of header objects with level, text, and id
|
|
31
|
+
*/
|
|
32
|
+
export function extractHeaders(markdown: string): any[];
|
|
33
|
+
/**
|
|
34
|
+
* Process anchor tags in HTML content
|
|
35
|
+
* Converts <!-- anchor:tagname --> comments to identifiable span elements
|
|
36
|
+
* @param {string} html - The HTML content
|
|
37
|
+
* @returns {string} HTML with anchor markers converted to spans
|
|
38
|
+
*/
|
|
39
|
+
export function processAnchorTags(html: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Get gutter content for a recipe by slug
|
|
42
|
+
* @param {string} slug - The recipe slug
|
|
43
|
+
* @returns {Array} Array of gutter items with content and position info
|
|
44
|
+
*/
|
|
45
|
+
export function getRecipeGutterContent(slug: string): any[];
|
|
46
|
+
/**
|
|
47
|
+
* Get gutter content for a blog post by slug
|
|
48
|
+
* @param {string} slug - The post slug
|
|
49
|
+
* @returns {Array} Array of gutter items with content and position info
|
|
50
|
+
*/
|
|
51
|
+
export function getGutterContent(slug: string): any[];
|
|
52
|
+
/**
|
|
53
|
+
* Get gutter content for the home page
|
|
54
|
+
* @param {string} slug - The page slug (e.g., 'home')
|
|
55
|
+
* @returns {Array} Array of gutter items with content and position info
|
|
56
|
+
*/
|
|
57
|
+
export function getHomeGutterContent(slug: string): any[];
|
|
58
|
+
/**
|
|
59
|
+
* Get gutter content for the contact page
|
|
60
|
+
* @param {string} slug - The page slug (e.g., 'contact')
|
|
61
|
+
* @returns {Array} Array of gutter items with content and position info
|
|
62
|
+
*/
|
|
63
|
+
export function getContactGutterContent(slug: string): any[];
|
|
64
|
+
/**
|
|
65
|
+
* Get the home page content
|
|
66
|
+
* @returns {Object|null} Home page object with content, metadata, and galleries
|
|
67
|
+
*/
|
|
68
|
+
export function getHomePage(): Object | null;
|
|
69
|
+
/**
|
|
70
|
+
* Get the contact page content
|
|
71
|
+
* @returns {Object|null} Contact page object with content and metadata
|
|
72
|
+
*/
|
|
73
|
+
export function getContactPage(): Object | null;
|
|
74
|
+
/**
|
|
75
|
+
* Get the about page content
|
|
76
|
+
* @returns {Object|null} About page object with content and metadata
|
|
77
|
+
*/
|
|
78
|
+
export function getAboutPage(): Object | null;
|
|
79
|
+
/**
|
|
80
|
+
* Get gutter content for the about page
|
|
81
|
+
* @param {string} slug - The page slug (e.g., 'about')
|
|
82
|
+
* @returns {Array} Array of gutter items with content and position info
|
|
83
|
+
*/
|
|
84
|
+
export function getAboutGutterContent(slug: string): any[];
|
|
85
|
+
/**
|
|
86
|
+
* Get recipe metadata (step icons, etc.) for a recipe by slug
|
|
87
|
+
* @param {string} slug - The recipe slug
|
|
88
|
+
* @returns {Object|null} Recipe metadata with instruction icons
|
|
89
|
+
*/
|
|
90
|
+
export function getRecipeSidecar(slug: string): Object | null;
|
|
91
|
+
/**
|
|
92
|
+
* Get a single recipe by slug
|
|
93
|
+
* @param {string} slug - The recipe slug
|
|
94
|
+
* @returns {Object|null} Recipe object with content and metadata
|
|
95
|
+
*/
|
|
96
|
+
export function getRecipeBySlug(slug: string): Object | null;
|
|
97
|
+
/**
|
|
98
|
+
* Render Mermaid diagrams in the DOM
|
|
99
|
+
* This should be called after the content is mounted
|
|
100
|
+
*/
|
|
101
|
+
export function renderMermaidDiagrams(): Promise<void>;
|