@autumnsgrove/groveengine 0.6.2 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +378 -0
  2. package/dist/auth/jwt.d.ts +10 -4
  3. package/dist/auth/jwt.js +18 -4
  4. package/dist/auth/session.d.ts +22 -15
  5. package/dist/auth/session.js +35 -16
  6. package/dist/components/admin/GutterManager.svelte +81 -139
  7. package/dist/components/admin/GutterManager.svelte.d.ts +6 -6
  8. package/dist/components/admin/MarkdownEditor.svelte +80 -23
  9. package/dist/components/admin/MarkdownEditor.svelte.d.ts +14 -8
  10. package/dist/components/admin/composables/useAmbientSounds.svelte.d.ts +52 -2
  11. package/dist/components/admin/composables/useAmbientSounds.svelte.js +38 -4
  12. package/dist/components/admin/composables/useCommandPalette.svelte.d.ts +80 -10
  13. package/dist/components/admin/composables/useCommandPalette.svelte.js +45 -5
  14. package/dist/components/admin/composables/useDraftManager.svelte.d.ts +76 -14
  15. package/dist/components/admin/composables/useDraftManager.svelte.js +44 -10
  16. package/dist/components/admin/composables/useEditorTheme.svelte.d.ts +168 -2
  17. package/dist/components/admin/composables/useEditorTheme.svelte.js +40 -7
  18. package/dist/components/admin/composables/useSlashCommands.svelte.d.ts +94 -22
  19. package/dist/components/admin/composables/useSlashCommands.svelte.js +58 -9
  20. package/dist/components/admin/composables/useSnippets.svelte.d.ts +51 -2
  21. package/dist/components/admin/composables/useSnippets.svelte.js +35 -3
  22. package/dist/components/admin/composables/useWritingSession.svelte.d.ts +64 -6
  23. package/dist/components/admin/composables/useWritingSession.svelte.js +42 -5
  24. package/dist/components/custom/ContentWithGutter.svelte +53 -23
  25. package/dist/components/custom/ContentWithGutter.svelte.d.ts +6 -14
  26. package/dist/components/custom/GutterItem.svelte +1 -1
  27. package/dist/components/custom/LeftGutter.svelte +43 -13
  28. package/dist/components/custom/LeftGutter.svelte.d.ts +6 -6
  29. package/dist/config/ai-models.js +1 -1
  30. package/dist/groveauth/client.js +11 -11
  31. package/dist/index.d.ts +3 -1
  32. package/dist/index.js +2 -2
  33. package/dist/server/logger.d.ts +74 -26
  34. package/dist/server/logger.js +133 -184
  35. package/dist/server/services/cache.js +1 -10
  36. package/dist/ui/components/charts/ActivityOverview.svelte +14 -3
  37. package/dist/ui/components/charts/ActivityOverview.svelte.d.ts +10 -7
  38. package/dist/ui/components/charts/RepoBreakdown.svelte +9 -3
  39. package/dist/ui/components/charts/RepoBreakdown.svelte.d.ts +12 -11
  40. package/dist/ui/components/charts/Sparkline.svelte +18 -7
  41. package/dist/ui/components/charts/Sparkline.svelte.d.ts +21 -2
  42. package/dist/ui/components/gallery/ImageGallery.svelte +12 -8
  43. package/dist/ui/components/gallery/ImageGallery.svelte.d.ts +2 -2
  44. package/dist/ui/components/gallery/Lightbox.svelte +5 -2
  45. package/dist/ui/components/gallery/ZoomableImage.svelte +8 -5
  46. package/dist/ui/components/primitives/accordion/index.d.ts +1 -1
  47. package/dist/ui/components/primitives/input/input.svelte.d.ts +1 -1
  48. package/dist/ui/components/primitives/tabs/index.d.ts +1 -1
  49. package/dist/ui/components/primitives/textarea/textarea.svelte.d.ts +1 -1
  50. package/dist/ui/components/ui/Button.svelte +5 -0
  51. package/dist/ui/components/ui/Button.svelte.d.ts +4 -1
  52. package/dist/ui/components/ui/Input.svelte +4 -0
  53. package/dist/ui/components/ui/Input.svelte.d.ts +3 -1
  54. package/dist/ui/components/ui/Logo.svelte +86 -0
  55. package/dist/ui/components/ui/Logo.svelte.d.ts +25 -0
  56. package/dist/ui/components/ui/LogoLoader.svelte +71 -0
  57. package/dist/ui/components/ui/LogoLoader.svelte.d.ts +9 -0
  58. package/dist/ui/components/ui/index.d.ts +2 -0
  59. package/dist/ui/components/ui/index.js +2 -0
  60. package/dist/ui/tailwind.preset.js +8 -8
  61. package/dist/utils/api.js +2 -1
  62. package/dist/utils/debounce.d.ts +4 -3
  63. package/dist/utils/debounce.js +10 -6
  64. package/dist/utils/gallery.d.ts +58 -32
  65. package/dist/utils/gallery.js +111 -129
  66. package/dist/utils/gutter.d.ts +47 -26
  67. package/dist/utils/gutter.js +116 -124
  68. package/dist/utils/imageProcessor.d.ts +66 -19
  69. package/dist/utils/imageProcessor.js +31 -10
  70. package/dist/utils/index.d.ts +11 -11
  71. package/dist/utils/index.js +4 -3
  72. package/dist/utils/json.js +1 -1
  73. package/dist/utils/markdown.d.ts +183 -103
  74. package/dist/utils/markdown.js +517 -678
  75. package/dist/utils/sanitize.d.ts +22 -12
  76. package/dist/utils/sanitize.js +268 -282
  77. package/dist/utils/validation.js +4 -3
  78. package/package.json +23 -23
  79. package/static/fonts/alagard.ttf +0 -0
@@ -5,165 +5,157 @@
5
5
  * and anchor resolution. Used by ContentWithGutter component and related
6
6
  * functionality across the site.
7
7
  */
8
-
9
8
  /**
10
9
  * 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
10
+ * @param anchor - The anchor string from manifest
11
+ * @returns Object with type and value properties
13
12
  */
14
13
  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 };
14
+ if (!anchor) {
15
+ return { type: "none", value: null };
16
+ }
17
+ // Check for paragraph anchor: "paragraph:N"
18
+ const paragraphMatch = anchor.match(/^paragraph:(\d+)$/);
19
+ if (paragraphMatch) {
20
+ return { type: "paragraph", value: parseInt(paragraphMatch[1], 10) };
21
+ }
22
+ // Check for tag anchor: "anchor:tagname" (supports alphanumeric, underscores, and hyphens)
23
+ const tagMatch = anchor.match(/^anchor:([\w-]+)$/);
24
+ if (tagMatch) {
25
+ return { type: "tag", value: tagMatch[1] };
26
+ }
27
+ // Check for header anchor: "## Header Text"
28
+ const headerMatch = anchor.match(/^(#{1,6})\s+(.+)$/);
29
+ if (headerMatch) {
30
+ return { type: "header", value: anchor };
31
+ }
32
+ // Unknown format - treat as header for backwards compatibility
33
+ return { type: "header", value: anchor };
39
34
  }
40
-
41
35
  /**
42
36
  * 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
37
+ * @param anchor - The anchor string
38
+ * @param headers - Array of header objects with id and text
39
+ * @returns A unique key for the anchor
46
40
  */
47
41
  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}`;
42
+ const parsed = parseAnchor(anchor);
43
+ switch (parsed.type) {
44
+ case "header": {
45
+ const headerText = anchor.replace(/^#+\s*/, "");
46
+ const header = headers.find((h) => h.text === headerText);
47
+ return header ? `header:${header.id}` : `header:${anchor}`;
48
+ }
49
+ case "paragraph":
50
+ return `paragraph:${parsed.value}`;
51
+ case "tag":
52
+ return `tag:${parsed.value}`;
53
+ default:
54
+ return `unknown:${anchor}`;
54
55
  }
55
- case 'paragraph':
56
- return `paragraph:${parsed.value}`;
57
- case 'tag':
58
- return `tag:${parsed.value}`;
59
- default:
60
- return `unknown:${anchor}`;
61
- }
62
56
  }
63
-
64
57
  /**
65
58
  * Get all unique anchors from items (preserving order)
66
- * @param {Array} items - Array of gutter items
67
- * @returns {Array} Array of unique anchor strings
59
+ * @param items - Array of gutter items
60
+ * @returns Array of unique anchor strings
68
61
  */
69
62
  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);
63
+ if (!items)
64
+ return [];
65
+ const seen = new Set();
66
+ const anchors = [];
67
+ for (const item of items) {
68
+ if (item.anchor && !seen.has(item.anchor)) {
69
+ seen.add(item.anchor);
70
+ anchors.push(item.anchor);
71
+ }
77
72
  }
78
- }
79
- return anchors;
73
+ return anchors;
80
74
  }
81
-
82
75
  /**
83
76
  * 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
77
+ * @param anchor - The anchor string
78
+ * @returns Human-readable label for the anchor
86
79
  */
87
80
  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
- }
81
+ const parsed = parseAnchor(anchor);
82
+ switch (parsed.type) {
83
+ case "header":
84
+ return anchor.replace(/^#+\s*/, "");
85
+ case "paragraph":
86
+ return `Paragraph ${parsed.value}`;
87
+ case "tag":
88
+ return `Tag: ${parsed.value}`;
89
+ default:
90
+ return anchor;
91
+ }
99
92
  }
100
-
101
93
  /**
102
94
  * 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
95
+ * @param items - Array of gutter items
96
+ * @param anchor - The anchor to match
97
+ * @returns Items matching the anchor
106
98
  */
107
99
  export function getItemsForAnchor(items, anchor) {
108
- if (!items) return [];
109
- return items.filter(item => item.anchor === anchor);
100
+ if (!items)
101
+ return [];
102
+ return items.filter((item) => item.anchor === anchor);
110
103
  }
111
-
112
104
  /**
113
105
  * 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
106
+ * @param items - Array of gutter items
107
+ * @param headers - Array of header objects
108
+ * @returns Items without valid anchors
117
109
  */
118
110
  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
- });
111
+ if (!items)
112
+ return [];
113
+ return items.filter((item) => {
114
+ if (!item.anchor)
115
+ return true;
116
+ const parsed = parseAnchor(item.anchor);
117
+ if (parsed.type === "header") {
118
+ const headerText = item.anchor.replace(/^#+\s*/, "");
119
+ return !headers.find((h) => h.text === headerText);
120
+ }
121
+ // Paragraph and tag anchors are valid if they have values
122
+ return parsed.type === "none";
123
+ });
130
124
  }
131
-
132
125
  /**
133
126
  * 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
127
+ * @param anchor - The anchor string
128
+ * @param contentEl - The content container element
129
+ * @param headers - Array of header objects
130
+ * @returns The DOM element or null if not found
138
131
  */
139
132
  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}"]`);
133
+ if (!contentEl)
134
+ return null;
135
+ const parsed = parseAnchor(anchor);
136
+ switch (parsed.type) {
137
+ case "header": {
138
+ const headerText = anchor.replace(/^#+\s*/, "");
139
+ const header = headers.find((h) => h.text === headerText);
140
+ if (header) {
141
+ return document.getElementById(header.id);
142
+ }
143
+ return null;
144
+ }
145
+ case "paragraph": {
146
+ // Select only direct child paragraphs to avoid counting paragraphs
147
+ // inside blockquotes, list items, etc.
148
+ const paragraphs = contentEl.querySelectorAll(":scope > p");
149
+ const index = parsed.value - 1; // Convert to 0-based index
150
+ if (index >= 0 && index < paragraphs.length) {
151
+ return paragraphs[index];
152
+ }
153
+ return null;
154
+ }
155
+ case "tag": {
156
+ return contentEl.querySelector(`[data-anchor="${parsed.value}"]`);
157
+ }
158
+ default:
159
+ return null;
165
160
  }
166
- default:
167
- return null;
168
- }
169
161
  }
@@ -8,28 +8,31 @@
8
8
  * @returns {Promise<string>} Hex string of the hash
9
9
  */
10
10
  export function calculateFileHash(file: File | Blob): Promise<string>;
11
+ /**
12
+ * @typedef {Object} ProcessedImageResult
13
+ * @property {Blob} blob - Processed image blob
14
+ * @property {number} width - Image width
15
+ * @property {number} height - Image height
16
+ * @property {number} originalSize - Original file size
17
+ * @property {number} processedSize - Processed file size
18
+ * @property {boolean} [skipped] - Whether processing was skipped
19
+ * @property {string} [reason] - Reason for skipping
20
+ */
21
+ /**
22
+ * @typedef {Object} ProcessImageOptions
23
+ * @property {number} [quality] - Quality 0-100 (default 80)
24
+ * @property {boolean} [convertToWebP] - Convert to WebP format (default true)
25
+ * @property {boolean} [fullResolution] - Skip resizing (default false)
26
+ */
11
27
  /**
12
28
  * Process an image: convert to WebP, adjust quality, strip EXIF
13
29
  * Drawing to canvas automatically strips EXIF data including GPS
14
30
  *
15
31
  * @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
- }>;
32
+ * @param {ProcessImageOptions} options - Processing options
33
+ * @returns {Promise<ProcessedImageResult>}
34
+ */
35
+ export function processImage(file: File, options?: ProcessImageOptions): Promise<ProcessedImageResult>;
33
36
  /**
34
37
  * Generate a date-based path for organizing uploads
35
38
  * Format: photos/YYYY/MM/DD/
@@ -37,12 +40,12 @@ export function processImage(file: File, options?: {
37
40
  */
38
41
  export function generateDatePath(): string;
39
42
  /**
40
- * Generate a clean filename from original name
43
+ * Generate a clean filename from original name for image files
41
44
  * @param {string} originalName - Original filename
42
45
  * @param {boolean} useWebP - Whether to use .webp extension
43
46
  * @returns {string} Sanitized filename
44
47
  */
45
- export function sanitizeFilename(originalName: string, useWebP?: boolean): string;
48
+ export function sanitizeImageFilename(originalName: string, useWebP?: boolean): string;
46
49
  /**
47
50
  * Format bytes to human-readable string
48
51
  * @param {number} bytes - Size in bytes
@@ -56,3 +59,47 @@ export function formatBytes(bytes: number): string;
56
59
  * @returns {string} Percentage saved
57
60
  */
58
61
  export function compressionRatio(original: number, processed: number): string;
62
+ export type ProcessedImageResult = {
63
+ /**
64
+ * - Processed image blob
65
+ */
66
+ blob: Blob;
67
+ /**
68
+ * - Image width
69
+ */
70
+ width: number;
71
+ /**
72
+ * - Image height
73
+ */
74
+ height: number;
75
+ /**
76
+ * - Original file size
77
+ */
78
+ originalSize: number;
79
+ /**
80
+ * - Processed file size
81
+ */
82
+ processedSize: number;
83
+ /**
84
+ * - Whether processing was skipped
85
+ */
86
+ skipped?: boolean | undefined;
87
+ /**
88
+ * - Reason for skipping
89
+ */
90
+ reason?: string | undefined;
91
+ };
92
+ export type ProcessImageOptions = {
93
+ /**
94
+ * - Quality 0-100 (default 80)
95
+ */
96
+ quality?: number | undefined;
97
+ /**
98
+ * - Convert to WebP format (default true)
99
+ */
100
+ convertToWebP?: boolean | undefined;
101
+ /**
102
+ * - Skip resizing (default false)
103
+ */
104
+ fullResolution?: boolean | undefined;
105
+ };
@@ -68,16 +68,31 @@ function getMaxDimensionForQuality(quality) {
68
68
  return 960;
69
69
  }
70
70
 
71
+ /**
72
+ * @typedef {Object} ProcessedImageResult
73
+ * @property {Blob} blob - Processed image blob
74
+ * @property {number} width - Image width
75
+ * @property {number} height - Image height
76
+ * @property {number} originalSize - Original file size
77
+ * @property {number} processedSize - Processed file size
78
+ * @property {boolean} [skipped] - Whether processing was skipped
79
+ * @property {string} [reason] - Reason for skipping
80
+ */
81
+
82
+ /**
83
+ * @typedef {Object} ProcessImageOptions
84
+ * @property {number} [quality] - Quality 0-100 (default 80)
85
+ * @property {boolean} [convertToWebP] - Convert to WebP format (default true)
86
+ * @property {boolean} [fullResolution] - Skip resizing (default false)
87
+ */
88
+
71
89
  /**
72
90
  * Process an image: convert to WebP, adjust quality, strip EXIF
73
91
  * Drawing to canvas automatically strips EXIF data including GPS
74
92
  *
75
93
  * @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 }>}
94
+ * @param {ProcessImageOptions} options - Processing options
95
+ * @returns {Promise<ProcessedImageResult>}
81
96
  */
82
97
  export async function processImage(file, options = {}) {
83
98
  const {
@@ -119,14 +134,20 @@ export async function processImage(file, options = {}) {
119
134
  canvas.height = targetHeight;
120
135
 
121
136
  const ctx = canvas.getContext('2d');
137
+ if (!ctx) {
138
+ throw new Error('Failed to get canvas 2d context');
139
+ }
122
140
  ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
123
141
 
124
142
  // Convert to blob
125
143
  const mimeType = convertToWebP ? 'image/webp' : file.type;
126
144
  const qualityDecimal = quality / 100;
127
145
 
128
- const blob = await new Promise((resolve) => {
129
- canvas.toBlob(resolve, mimeType, qualityDecimal);
146
+ const blob = await new Promise((resolve, reject) => {
147
+ canvas.toBlob((b) => {
148
+ if (b) resolve(b);
149
+ else reject(new Error('Failed to create blob'));
150
+ }, mimeType, qualityDecimal);
130
151
  });
131
152
 
132
153
  return {
@@ -134,7 +155,7 @@ export async function processImage(file, options = {}) {
134
155
  width: targetWidth,
135
156
  height: targetHeight,
136
157
  originalSize,
137
- processedSize: blob.size,
158
+ processedSize: /** @type {Blob} */ (blob).size,
138
159
  skipped: false
139
160
  };
140
161
  }
@@ -153,12 +174,12 @@ export function generateDatePath() {
153
174
  }
154
175
 
155
176
  /**
156
- * Generate a clean filename from original name
177
+ * Generate a clean filename from original name for image files
157
178
  * @param {string} originalName - Original filename
158
179
  * @param {boolean} useWebP - Whether to use .webp extension
159
180
  * @returns {string} Sanitized filename
160
181
  */
161
- export function sanitizeFilename(originalName, useWebP = true) {
182
+ export function sanitizeImageFilename(originalName, useWebP = true) {
162
183
  // Get base name without extension
163
184
  const lastDot = originalName.lastIndexOf('.');
164
185
  const baseName = lastDot > 0 ? originalName.substring(0, lastDot) : originalName;
@@ -1,11 +1,11 @@
1
- export * from "./api.js";
2
- export * from "./cn.js";
3
- export * from "./csrf.js";
4
- export * from "./debounce.js";
5
- export * from "./gallery.js";
6
- export * from "./gutter.js";
7
- export * from "./imageProcessor.js";
8
- export * from "./json.js";
9
- export * from "./markdown.js";
10
- export * from "./sanitize.js";
11
- export * from "./validation.js";
1
+ export * from './api.js';
2
+ export * from './cn.js';
3
+ export * from './csrf.js';
4
+ export * from './debounce.js';
5
+ export { parseImageFilename, getImageTitle, getImageDate, searchImages, filterImagesByDateRange, filterImagesByTags, filterImagesByCategory, getAvailableYears, getAvailableCategories, type GalleryImage, type ImageTag, type ParsedImageMetadata } from './gallery.js';
6
+ export { parseAnchor, getAnchorKey, getUniqueAnchors, getAnchorLabel, getItemsForAnchor, getOrphanItems, findAnchorElement, type AnchorType, type ParsedAnchor, type Header, type GutterItem } from './gutter.js';
7
+ export * from './imageProcessor.js';
8
+ export * from './json.js';
9
+ export * from './markdown.js';
10
+ export * from './sanitize.js';
11
+ export * from './validation.js';
@@ -1,12 +1,13 @@
1
1
  // Utils barrel export
2
2
  // Re-exports all utility functions from the utils module
3
-
4
3
  export * from './api.js';
5
4
  export * from './cn.js';
6
5
  export * from './csrf.js';
7
6
  export * from './debounce.js';
8
- export * from './gallery.js';
9
- export * from './gutter.js';
7
+ // Gallery - explicit exports to avoid ambiguity
8
+ export { parseImageFilename, getImageTitle, getImageDate, searchImages, filterImagesByDateRange, filterImagesByTags, filterImagesByCategory, getAvailableYears, getAvailableCategories } from './gallery.js';
9
+ // Gutter - explicit exports to avoid ambiguity
10
+ export { parseAnchor, getAnchorKey, getUniqueAnchors, getAnchorLabel, getItemsForAnchor, getOrphanItems, findAnchorElement } from './gutter.js';
10
11
  export * from './imageProcessor.js';
11
12
  export * from './json.js';
12
13
  export * from './markdown.js';
@@ -20,7 +20,7 @@ export function safeJsonParse(str, fallback = []) {
20
20
  try {
21
21
  return JSON.parse(str);
22
22
  } catch (e) {
23
- console.warn('Failed to parse JSON:', e.message);
23
+ console.warn('Failed to parse JSON:', e instanceof Error ? e.message : String(e));
24
24
  return fallback;
25
25
  }
26
26
  }