@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.
Files changed (219) hide show
  1. package/README.md +163 -0
  2. package/dist/auth/jwt.d.ts +14 -0
  3. package/dist/auth/jwt.js +109 -0
  4. package/dist/auth/session.d.ts +42 -0
  5. package/dist/auth/session.js +105 -0
  6. package/dist/components/admin/GutterManager.svelte +910 -0
  7. package/dist/components/admin/GutterManager.svelte.d.ts +15 -0
  8. package/dist/components/admin/MarkdownEditor.svelte +3114 -0
  9. package/dist/components/admin/MarkdownEditor.svelte.d.ts +43 -0
  10. package/dist/components/custom/CollapsibleSection.svelte +74 -0
  11. package/dist/components/custom/CollapsibleSection.svelte.d.ts +15 -0
  12. package/dist/components/custom/ContentWithGutter.svelte +646 -0
  13. package/dist/components/custom/ContentWithGutter.svelte.d.ts +19 -0
  14. package/dist/components/custom/GutterItem.svelte +201 -0
  15. package/dist/components/custom/GutterItem.svelte.d.ts +11 -0
  16. package/dist/components/custom/LeftGutter.svelte +271 -0
  17. package/dist/components/custom/LeftGutter.svelte.d.ts +17 -0
  18. package/dist/components/custom/MobileTOC.svelte +273 -0
  19. package/dist/components/custom/MobileTOC.svelte.d.ts +11 -0
  20. package/dist/components/custom/TableOfContents.svelte +163 -0
  21. package/dist/components/custom/TableOfContents.svelte.d.ts +11 -0
  22. package/dist/components/gallery/ImageGallery.svelte +681 -0
  23. package/dist/components/gallery/ImageGallery.svelte.d.ts +11 -0
  24. package/dist/components/gallery/Lightbox.svelte +107 -0
  25. package/dist/components/gallery/Lightbox.svelte.d.ts +19 -0
  26. package/dist/components/gallery/LightboxCaption.svelte +25 -0
  27. package/dist/components/gallery/LightboxCaption.svelte.d.ts +11 -0
  28. package/dist/components/gallery/ZoomableImage.svelte +163 -0
  29. package/dist/components/gallery/ZoomableImage.svelte.d.ts +17 -0
  30. package/dist/components/ui/Accordion.svelte +74 -0
  31. package/dist/components/ui/Accordion.svelte.d.ts +42 -0
  32. package/dist/components/ui/Badge.svelte +48 -0
  33. package/dist/components/ui/Badge.svelte.d.ts +26 -0
  34. package/dist/components/ui/Button.svelte +74 -0
  35. package/dist/components/ui/Button.svelte.d.ts +34 -0
  36. package/dist/components/ui/Card.svelte +102 -0
  37. package/dist/components/ui/Card.svelte.d.ts +46 -0
  38. package/dist/components/ui/Dialog.svelte +91 -0
  39. package/dist/components/ui/Dialog.svelte.d.ts +43 -0
  40. package/dist/components/ui/Input.svelte +81 -0
  41. package/dist/components/ui/Input.svelte.d.ts +35 -0
  42. package/dist/components/ui/Select.svelte +69 -0
  43. package/dist/components/ui/Select.svelte.d.ts +36 -0
  44. package/dist/components/ui/Sheet.svelte +98 -0
  45. package/dist/components/ui/Sheet.svelte.d.ts +45 -0
  46. package/dist/components/ui/Skeleton.svelte +31 -0
  47. package/dist/components/ui/Skeleton.svelte.d.ts +26 -0
  48. package/dist/components/ui/Table.svelte +59 -0
  49. package/dist/components/ui/Table.svelte.d.ts +44 -0
  50. package/dist/components/ui/Tabs.svelte +76 -0
  51. package/dist/components/ui/Tabs.svelte.d.ts +41 -0
  52. package/dist/components/ui/Textarea.svelte +81 -0
  53. package/dist/components/ui/Textarea.svelte.d.ts +35 -0
  54. package/dist/components/ui/Toast.svelte +18 -0
  55. package/dist/components/ui/Toast.svelte.d.ts +7 -0
  56. package/dist/components/ui/accordion/accordion-content.svelte +24 -0
  57. package/dist/components/ui/accordion/accordion-content.svelte.d.ts +4 -0
  58. package/dist/components/ui/accordion/accordion-item.svelte +12 -0
  59. package/dist/components/ui/accordion/accordion-item.svelte.d.ts +4 -0
  60. package/dist/components/ui/accordion/accordion-trigger.svelte +29 -0
  61. package/dist/components/ui/accordion/accordion-trigger.svelte.d.ts +7 -0
  62. package/dist/components/ui/accordion/index.d.ts +6 -0
  63. package/dist/components/ui/accordion/index.js +8 -0
  64. package/dist/components/ui/badge/badge.svelte +50 -0
  65. package/dist/components/ui/badge/badge.svelte.d.ts +60 -0
  66. package/dist/components/ui/badge/index.d.ts +2 -0
  67. package/dist/components/ui/badge/index.js +2 -0
  68. package/dist/components/ui/button/button.svelte +82 -0
  69. package/dist/components/ui/button/button.svelte.d.ts +132 -0
  70. package/dist/components/ui/button/index.d.ts +2 -0
  71. package/dist/components/ui/button/index.js +4 -0
  72. package/dist/components/ui/card/card-content.svelte +16 -0
  73. package/dist/components/ui/card/card-content.svelte.d.ts +5 -0
  74. package/dist/components/ui/card/card-description.svelte +16 -0
  75. package/dist/components/ui/card/card-description.svelte.d.ts +5 -0
  76. package/dist/components/ui/card/card-footer.svelte +16 -0
  77. package/dist/components/ui/card/card-footer.svelte.d.ts +5 -0
  78. package/dist/components/ui/card/card-header.svelte +16 -0
  79. package/dist/components/ui/card/card-header.svelte.d.ts +5 -0
  80. package/dist/components/ui/card/card-title.svelte +25 -0
  81. package/dist/components/ui/card/card-title.svelte.d.ts +8 -0
  82. package/dist/components/ui/card/card.svelte +20 -0
  83. package/dist/components/ui/card/card.svelte.d.ts +5 -0
  84. package/dist/components/ui/card/index.d.ts +7 -0
  85. package/dist/components/ui/card/index.js +9 -0
  86. package/dist/components/ui/dialog/dialog-content.svelte +38 -0
  87. package/dist/components/ui/dialog/dialog-content.svelte.d.ts +9 -0
  88. package/dist/components/ui/dialog/dialog-description.svelte +16 -0
  89. package/dist/components/ui/dialog/dialog-description.svelte.d.ts +4 -0
  90. package/dist/components/ui/dialog/dialog-footer.svelte +20 -0
  91. package/dist/components/ui/dialog/dialog-footer.svelte.d.ts +5 -0
  92. package/dist/components/ui/dialog/dialog-header.svelte +20 -0
  93. package/dist/components/ui/dialog/dialog-header.svelte.d.ts +5 -0
  94. package/dist/components/ui/dialog/dialog-overlay.svelte +19 -0
  95. package/dist/components/ui/dialog/dialog-overlay.svelte.d.ts +4 -0
  96. package/dist/components/ui/dialog/dialog-title.svelte +16 -0
  97. package/dist/components/ui/dialog/dialog-title.svelte.d.ts +4 -0
  98. package/dist/components/ui/dialog/index.d.ts +12 -0
  99. package/dist/components/ui/dialog/index.js +14 -0
  100. package/dist/components/ui/index.d.ts +26 -0
  101. package/dist/components/ui/index.js +29 -0
  102. package/dist/components/ui/input/index.d.ts +2 -0
  103. package/dist/components/ui/input/index.js +4 -0
  104. package/dist/components/ui/input/input.svelte +46 -0
  105. package/dist/components/ui/input/input.svelte.d.ts +13 -0
  106. package/dist/components/ui/select/index.d.ts +11 -0
  107. package/dist/components/ui/select/index.js +13 -0
  108. package/dist/components/ui/select/select-content.svelte +39 -0
  109. package/dist/components/ui/select/select-content.svelte.d.ts +7 -0
  110. package/dist/components/ui/select/select-group-heading.svelte +16 -0
  111. package/dist/components/ui/select/select-group-heading.svelte.d.ts +4 -0
  112. package/dist/components/ui/select/select-item.svelte +37 -0
  113. package/dist/components/ui/select/select-item.svelte.d.ts +4 -0
  114. package/dist/components/ui/select/select-scroll-down-button.svelte +19 -0
  115. package/dist/components/ui/select/select-scroll-down-button.svelte.d.ts +4 -0
  116. package/dist/components/ui/select/select-scroll-up-button.svelte +19 -0
  117. package/dist/components/ui/select/select-scroll-up-button.svelte.d.ts +4 -0
  118. package/dist/components/ui/select/select-separator.svelte +13 -0
  119. package/dist/components/ui/select/select-separator.svelte.d.ts +4 -0
  120. package/dist/components/ui/select/select-trigger.svelte +24 -0
  121. package/dist/components/ui/select/select-trigger.svelte.d.ts +4 -0
  122. package/dist/components/ui/separator/index.d.ts +2 -0
  123. package/dist/components/ui/separator/index.js +4 -0
  124. package/dist/components/ui/separator/separator.svelte +22 -0
  125. package/dist/components/ui/separator/separator.svelte.d.ts +4 -0
  126. package/dist/components/ui/sheet/index.d.ts +12 -0
  127. package/dist/components/ui/sheet/index.js +14 -0
  128. package/dist/components/ui/sheet/sheet-content.svelte +53 -0
  129. package/dist/components/ui/sheet/sheet-content.svelte.d.ts +62 -0
  130. package/dist/components/ui/sheet/sheet-description.svelte +16 -0
  131. package/dist/components/ui/sheet/sheet-description.svelte.d.ts +4 -0
  132. package/dist/components/ui/sheet/sheet-footer.svelte +20 -0
  133. package/dist/components/ui/sheet/sheet-footer.svelte.d.ts +5 -0
  134. package/dist/components/ui/sheet/sheet-header.svelte +20 -0
  135. package/dist/components/ui/sheet/sheet-header.svelte.d.ts +5 -0
  136. package/dist/components/ui/sheet/sheet-overlay.svelte +21 -0
  137. package/dist/components/ui/sheet/sheet-overlay.svelte.d.ts +6 -0
  138. package/dist/components/ui/sheet/sheet-title.svelte +16 -0
  139. package/dist/components/ui/sheet/sheet-title.svelte.d.ts +4 -0
  140. package/dist/components/ui/skeleton/index.d.ts +2 -0
  141. package/dist/components/ui/skeleton/index.js +4 -0
  142. package/dist/components/ui/skeleton/skeleton.svelte +17 -0
  143. package/dist/components/ui/skeleton/skeleton.svelte.d.ts +5 -0
  144. package/dist/components/ui/table/index.d.ts +9 -0
  145. package/dist/components/ui/table/index.js +11 -0
  146. package/dist/components/ui/table/table-body.svelte +16 -0
  147. package/dist/components/ui/table/table-body.svelte.d.ts +5 -0
  148. package/dist/components/ui/table/table-caption.svelte +16 -0
  149. package/dist/components/ui/table/table-caption.svelte.d.ts +5 -0
  150. package/dist/components/ui/table/table-cell.svelte +20 -0
  151. package/dist/components/ui/table/table-cell.svelte.d.ts +5 -0
  152. package/dist/components/ui/table/table-footer.svelte +16 -0
  153. package/dist/components/ui/table/table-footer.svelte.d.ts +5 -0
  154. package/dist/components/ui/table/table-head.svelte +23 -0
  155. package/dist/components/ui/table/table-head.svelte.d.ts +5 -0
  156. package/dist/components/ui/table/table-header.svelte +16 -0
  157. package/dist/components/ui/table/table-header.svelte.d.ts +5 -0
  158. package/dist/components/ui/table/table-row.svelte +23 -0
  159. package/dist/components/ui/table/table-row.svelte.d.ts +5 -0
  160. package/dist/components/ui/table/table.svelte +18 -0
  161. package/dist/components/ui/table/table.svelte.d.ts +5 -0
  162. package/dist/components/ui/tabs/index.d.ts +6 -0
  163. package/dist/components/ui/tabs/index.js +8 -0
  164. package/dist/components/ui/tabs/tabs-content.svelte +19 -0
  165. package/dist/components/ui/tabs/tabs-content.svelte.d.ts +4 -0
  166. package/dist/components/ui/tabs/tabs-list.svelte +19 -0
  167. package/dist/components/ui/tabs/tabs-list.svelte.d.ts +4 -0
  168. package/dist/components/ui/tabs/tabs-trigger.svelte +19 -0
  169. package/dist/components/ui/tabs/tabs-trigger.svelte.d.ts +4 -0
  170. package/dist/components/ui/textarea/index.d.ts +2 -0
  171. package/dist/components/ui/textarea/index.js +4 -0
  172. package/dist/components/ui/textarea/textarea.svelte +24 -0
  173. package/dist/components/ui/textarea/textarea.svelte.d.ts +6 -0
  174. package/dist/components/ui/toast.d.ts +86 -0
  175. package/dist/components/ui/toast.js +99 -0
  176. package/dist/db/schema.sql +238 -0
  177. package/dist/index.d.ts +14 -0
  178. package/dist/index.js +20 -0
  179. package/dist/payments/index.d.ts +33 -0
  180. package/dist/payments/index.js +47 -0
  181. package/dist/payments/shop.d.ts +165 -0
  182. package/dist/payments/shop.js +588 -0
  183. package/dist/payments/stripe/client.d.ts +231 -0
  184. package/dist/payments/stripe/client.js +198 -0
  185. package/dist/payments/stripe/index.d.ts +18 -0
  186. package/dist/payments/stripe/index.js +17 -0
  187. package/dist/payments/stripe/provider.d.ts +50 -0
  188. package/dist/payments/stripe/provider.js +530 -0
  189. package/dist/payments/types.d.ts +355 -0
  190. package/dist/payments/types.js +7 -0
  191. package/dist/server/logger.d.ts +53 -0
  192. package/dist/server/logger.js +252 -0
  193. package/dist/styles/content.css +514 -0
  194. package/dist/styles/tokens.css +175 -0
  195. package/dist/utils/api.d.ts +20 -0
  196. package/dist/utils/api.js +109 -0
  197. package/dist/utils/cn.d.ts +15 -0
  198. package/dist/utils/cn.js +18 -0
  199. package/dist/utils/csrf.d.ts +22 -0
  200. package/dist/utils/csrf.js +72 -0
  201. package/dist/utils/debounce.d.ts +7 -0
  202. package/dist/utils/debounce.js +14 -0
  203. package/dist/utils/gallery.d.ts +66 -0
  204. package/dist/utils/gallery.js +181 -0
  205. package/dist/utils/gutter.d.ts +54 -0
  206. package/dist/utils/gutter.js +169 -0
  207. package/dist/utils/imageProcessor.d.ts +58 -0
  208. package/dist/utils/imageProcessor.js +205 -0
  209. package/dist/utils/json.d.ts +17 -0
  210. package/dist/utils/json.js +26 -0
  211. package/dist/utils/markdown.d.ts +101 -0
  212. package/dist/utils/markdown.js +947 -0
  213. package/dist/utils/sanitize.d.ts +25 -0
  214. package/dist/utils/sanitize.js +127 -0
  215. package/dist/utils/validation.d.ts +46 -0
  216. package/dist/utils/validation.js +169 -0
  217. package/dist/utils.d.ts +5 -0
  218. package/dist/utils.js +5 -0
  219. 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>;