zone5 0.0.0 → 1.0.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/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/cli/index.js +33 -0
- package/dist/cli/index2.js +238 -0
- package/dist/cli/index3.js +53 -0
- package/dist/cli/templates/.zone5.toml +4 -0
- package/dist/cli/templates/app.css +3 -0
- package/dist/cli/templates/layout.svelte +15 -0
- package/dist/cli/templates/layout.ts +1 -0
- package/dist/components/Zone5.svelte +153 -0
- package/dist/components/Zone5.svelte.d.ts +12 -0
- package/dist/components/Zone5Img.svelte +103 -0
- package/dist/components/Zone5Img.svelte.d.ts +10 -0
- package/dist/components/Zone5Lightbox.svelte +131 -0
- package/dist/components/Zone5Lightbox.svelte.d.ts +12 -0
- package/dist/components/Zone5Provider.svelte +68 -0
- package/dist/components/Zone5Provider.svelte.d.ts +9 -0
- package/dist/components/atoms/Button.svelte +40 -0
- package/dist/components/atoms/Button.svelte.d.ts +13 -0
- package/dist/components/atoms/CloseButton.svelte +18 -0
- package/dist/components/atoms/CloseButton.svelte.d.ts +9 -0
- package/dist/components/atoms/NextButton.svelte +19 -0
- package/dist/components/atoms/NextButton.svelte.d.ts +10 -0
- package/dist/components/atoms/PrevButton.svelte +19 -0
- package/dist/components/atoms/PrevButton.svelte.d.ts +10 -0
- package/dist/components/atoms/index.d.ts +4 -0
- package/dist/components/atoms/index.js +5 -0
- package/dist/components/constants.d.ts +17 -0
- package/dist/components/constants.js +17 -0
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.js +7 -0
- package/dist/components/portal.d.ts +4 -0
- package/dist/components/portal.js +26 -0
- package/dist/components/types.d.ts +7 -0
- package/dist/components/types.js +1 -0
- package/dist/config.d.ts +51 -0
- package/dist/config.js +56 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +4 -0
- package/dist/module.d.ts +19 -0
- package/dist/processor/blurhash.d.ts +7 -0
- package/dist/processor/blurhash.js +37 -0
- package/dist/processor/color.d.ts +5 -0
- package/dist/processor/color.js +32 -0
- package/dist/processor/config.d.ts +12 -0
- package/dist/processor/config.js +9 -0
- package/dist/processor/exif/converters.d.ts +7 -0
- package/dist/processor/exif/converters.js +38 -0
- package/dist/processor/exif/defaults.d.ts +17 -0
- package/dist/processor/exif/defaults.js +17 -0
- package/dist/processor/exif/exif.d.ts +34 -0
- package/dist/processor/exif/exif.js +43 -0
- package/dist/processor/exif/index.d.ts +1 -0
- package/dist/processor/exif/index.js +1 -0
- package/dist/processor/exif/types.d.ts +4 -0
- package/dist/processor/exif/types.js +1 -0
- package/dist/processor/file.d.ts +3 -0
- package/dist/processor/file.js +28 -0
- package/dist/processor/index.d.ts +27 -0
- package/dist/processor/index.js +70 -0
- package/dist/processor/test-data/canon-m6-22mm.jpg +0 -0
- package/dist/processor/test-data/iphone-15pro.jpg +0 -0
- package/dist/processor/test-data/nikon-z6iii-40mm.jpg +0 -0
- package/dist/processor/test-data/ricoh-gr-iiix.jpg +0 -0
- package/dist/processor/variants.d.ts +13 -0
- package/dist/processor/variants.js +83 -0
- package/dist/remark.d.ts +5 -0
- package/dist/remark.js +268 -0
- package/dist/stores/index.d.ts +1 -0
- package/dist/stores/index.js +1 -0
- package/dist/stores/registry.svelte.d.ts +22 -0
- package/dist/stores/registry.svelte.js +100 -0
- package/dist/test-setup.d.ts +9 -0
- package/dist/test-setup.js +25 -0
- package/dist/vite.d.ts +2 -0
- package/dist/vite.js +158 -0
- package/package.json +143 -5
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { BaseConfigType } from '../config.js';
|
|
2
|
+
import type { ProcessorConfig } from './config.js';
|
|
3
|
+
export interface GeneratedVariant {
|
|
4
|
+
width: number;
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function generateImageVariants(options: {
|
|
8
|
+
base: BaseConfigType;
|
|
9
|
+
processor: ProcessorConfig;
|
|
10
|
+
sourceFile: string;
|
|
11
|
+
clear?: boolean;
|
|
12
|
+
forceOverwrite?: boolean;
|
|
13
|
+
}): Promise<GeneratedVariant[]>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { SpanStatusCode, trace } from '@opentelemetry/api';
|
|
2
|
+
import { rm } from 'fs/promises';
|
|
3
|
+
import { join, parse } from 'path';
|
|
4
|
+
import sharp from 'sharp';
|
|
5
|
+
import { ensureDirectoryExists, fileExists, sourceFileHash } from './file.js';
|
|
6
|
+
const tracer = trace.getTracer('zone5-processor-variants');
|
|
7
|
+
const addDebugText = async (img, width, height) => {
|
|
8
|
+
const svg = `<svg height="100" width="300">
|
|
9
|
+
<text x="0" y="50" font-size="50" fill="#fff">${width}×${height}</text>
|
|
10
|
+
</svg>`;
|
|
11
|
+
return img.composite([{ input: Buffer.from(svg), gravity: sharp.gravity.center }]);
|
|
12
|
+
};
|
|
13
|
+
export async function generateImageVariants(options) {
|
|
14
|
+
return tracer.startActiveSpan('zone5.generateImageVariants', async (span) => {
|
|
15
|
+
try {
|
|
16
|
+
const { base, processor, sourceFile, clear = false, forceOverwrite = false } = options;
|
|
17
|
+
// Parse file path components
|
|
18
|
+
const { name: fileBasename, ext: fileExtension } = parse(sourceFile);
|
|
19
|
+
// Get source image metadata to check dimensions
|
|
20
|
+
const sourceImage = sharp(sourceFile);
|
|
21
|
+
const { width: sourceWidth } = await sourceImage.metadata();
|
|
22
|
+
// Filter out widths that would be wider than the source image
|
|
23
|
+
const validWidths = processor.variants.filter((width) => width <= sourceWidth);
|
|
24
|
+
span.setAttributes({
|
|
25
|
+
'zone5.sourceFile': sourceFile,
|
|
26
|
+
'zone5.sourceWidth': sourceWidth,
|
|
27
|
+
'zone5.validWidthsCount': validWidths.length,
|
|
28
|
+
'zone5.clear': clear,
|
|
29
|
+
'zone5.forceOverwrite': forceOverwrite,
|
|
30
|
+
});
|
|
31
|
+
// Create cache subdirectory
|
|
32
|
+
const sourceHash = sourceFileHash(base.root, sourceFile);
|
|
33
|
+
const cacheSubDir = join(base.cache, `${fileBasename}-${sourceHash}`);
|
|
34
|
+
if (clear) {
|
|
35
|
+
await rm(cacheSubDir, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
await ensureDirectoryExists(cacheSubDir);
|
|
38
|
+
// Generate variants for each valid width
|
|
39
|
+
const variants = [];
|
|
40
|
+
let generatedCount = 0;
|
|
41
|
+
for (const width of validWidths) {
|
|
42
|
+
const variantFilename = `${fileBasename}-${width}${fileExtension}`;
|
|
43
|
+
const variantPath = join(cacheSubDir, variantFilename);
|
|
44
|
+
// Check if variant already exists and should be overwritten
|
|
45
|
+
const variantExists = await fileExists(variantPath);
|
|
46
|
+
if (!variantExists || forceOverwrite) {
|
|
47
|
+
let img = sharp(sourceFile).gamma(processor.resize_gamma).resize(width, null, {
|
|
48
|
+
fit: 'inside',
|
|
49
|
+
kernel: processor.resize_kernel,
|
|
50
|
+
});
|
|
51
|
+
if (process.env.ZONE5_DEBUG) {
|
|
52
|
+
const { width: w, height: h } = await img.metadata();
|
|
53
|
+
const scale = w / width;
|
|
54
|
+
img = await addDebugText(img, width, Math.ceil(h * scale));
|
|
55
|
+
}
|
|
56
|
+
await img.toFile(variantPath);
|
|
57
|
+
generatedCount++;
|
|
58
|
+
}
|
|
59
|
+
variants.push({
|
|
60
|
+
width,
|
|
61
|
+
path: variantPath,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
span.setAttributes({
|
|
65
|
+
'zone5.variantsGenerated': generatedCount,
|
|
66
|
+
'zone5.variantsTotal': variants.length,
|
|
67
|
+
});
|
|
68
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
69
|
+
return variants;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
span.setStatus({
|
|
73
|
+
code: SpanStatusCode.ERROR,
|
|
74
|
+
message: error instanceof Error ? error.message : String(error),
|
|
75
|
+
});
|
|
76
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
span.end();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
package/dist/remark.d.ts
ADDED
package/dist/remark.js
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { compile } from 'svast-stringify/dist/main.es.js';
|
|
2
|
+
import { visit } from 'unist-util-visit';
|
|
3
|
+
/**
|
|
4
|
+
* Convert svast svelteComponent node to HTML using svast-stringify
|
|
5
|
+
*/
|
|
6
|
+
function svelteComponentToHTML(node) {
|
|
7
|
+
return compile({
|
|
8
|
+
type: 'root',
|
|
9
|
+
children: [node],
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Check if a paragraph is a newline (contains only whitespace)
|
|
14
|
+
*/
|
|
15
|
+
function isNewlineParagraph(node) {
|
|
16
|
+
return (node.type === 'paragraph' &&
|
|
17
|
+
node.children.length === 1 &&
|
|
18
|
+
node.children[0].type === 'text' &&
|
|
19
|
+
/^\s*$/.test(node.children[0].value));
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Build the expression value for the images property
|
|
23
|
+
*/
|
|
24
|
+
function buildImagesExpression(imageData) {
|
|
25
|
+
const imageExpressions = imageData.map((img) => `{...${img.key}, properties: {...${img.key}.properties, alt: ${JSON.stringify(img.alt)}, title: ${JSON.stringify(img.title)}}}`);
|
|
26
|
+
return '[' + imageExpressions.join(',') + ']';
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Create a Zone5 Svelte component node
|
|
30
|
+
*/
|
|
31
|
+
function createZone5Component(imageData, mode) {
|
|
32
|
+
const properties = [
|
|
33
|
+
{
|
|
34
|
+
type: 'svelteProperty',
|
|
35
|
+
name: 'images',
|
|
36
|
+
shorthand: 'none',
|
|
37
|
+
value: [
|
|
38
|
+
{
|
|
39
|
+
type: 'svelteDynamicContent',
|
|
40
|
+
expression: {
|
|
41
|
+
type: 'svelteExpression',
|
|
42
|
+
value: buildImagesExpression(imageData),
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
modifiers: [],
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
if (mode) {
|
|
50
|
+
properties.push({
|
|
51
|
+
type: 'svelteProperty',
|
|
52
|
+
name: 'mode',
|
|
53
|
+
shorthand: 'none',
|
|
54
|
+
value: [
|
|
55
|
+
{
|
|
56
|
+
type: 'text',
|
|
57
|
+
value: mode,
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
modifiers: [],
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
type: 'svelteComponent',
|
|
65
|
+
tagName: 'Zone5',
|
|
66
|
+
properties,
|
|
67
|
+
selfClosing: true,
|
|
68
|
+
children: [],
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Collect image data from Z5 images
|
|
73
|
+
*/
|
|
74
|
+
function collectImageData(images, existingKeys) {
|
|
75
|
+
return images.map((imageNode) => {
|
|
76
|
+
const importKey = generateImportKey(imageNode.url, existingKeys);
|
|
77
|
+
return {
|
|
78
|
+
key: importKey,
|
|
79
|
+
alt: imageNode.alt || '',
|
|
80
|
+
title: imageNode.title || undefined,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Generate a unique import key for an image URL
|
|
86
|
+
*/
|
|
87
|
+
function generateImportKey(url, existingKeys) {
|
|
88
|
+
const baseName = url
|
|
89
|
+
.split('/')
|
|
90
|
+
.pop()
|
|
91
|
+
?.replace(/\?z5$/, '')
|
|
92
|
+
.replace(/\.[^.]*$/, '')
|
|
93
|
+
.replace(/[^a-zA-Z0-9]/g, '_')
|
|
94
|
+
.replace(/^(\d)/, '_$1') || 'image';
|
|
95
|
+
let key = baseName;
|
|
96
|
+
let counter = 1;
|
|
97
|
+
while (existingKeys.has(key)) {
|
|
98
|
+
key = `${baseName}_${counter}`;
|
|
99
|
+
counter++;
|
|
100
|
+
}
|
|
101
|
+
existingKeys.add(key);
|
|
102
|
+
return key;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if a node is a Zone5 image (ends with ?z5)
|
|
106
|
+
*/
|
|
107
|
+
function isZ5Image(node) {
|
|
108
|
+
return node.type === 'image' && typeof node.url === 'string' && node.url.endsWith('?z5');
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Check if a paragraph contains only a single Z5 image
|
|
112
|
+
*/
|
|
113
|
+
function isZ5ImageParagraph(node) {
|
|
114
|
+
return node.type === 'paragraph' && node.children.length === 1 && isZ5Image(node.children[0]);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Check if a paragraph contains multiple consecutive Z5 images
|
|
118
|
+
*/
|
|
119
|
+
function isMultiZ5ImageParagraph(node) {
|
|
120
|
+
if (node.type !== 'paragraph')
|
|
121
|
+
return false;
|
|
122
|
+
const paragraph = node;
|
|
123
|
+
const z5Images = paragraph.children.filter((child) => isZ5Image(child));
|
|
124
|
+
return z5Images.length >= 2;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Extract the image from a Z5 image paragraph
|
|
128
|
+
*/
|
|
129
|
+
function getImageFromParagraph(node) {
|
|
130
|
+
if (isZ5ImageParagraph(node) && node.type === 'paragraph') {
|
|
131
|
+
return node.children[0];
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Find groups of consecutive Zone5 images
|
|
137
|
+
*/
|
|
138
|
+
function findConsecutiveImageGroups(children) {
|
|
139
|
+
const groups = [];
|
|
140
|
+
let currentGroup = [];
|
|
141
|
+
for (let i = 0; i < children.length; i++) {
|
|
142
|
+
const node = children[i];
|
|
143
|
+
if (isZ5ImageParagraph(node)) {
|
|
144
|
+
currentGroup.push(i);
|
|
145
|
+
}
|
|
146
|
+
else if (!isNewlineParagraph(node) && currentGroup.length > 0) {
|
|
147
|
+
// End of consecutive group (non-newline, non-image node)
|
|
148
|
+
if (currentGroup.length >= 2) {
|
|
149
|
+
groups.push([...currentGroup]);
|
|
150
|
+
}
|
|
151
|
+
currentGroup = [];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// Handle group at end of children
|
|
155
|
+
if (currentGroup.length >= 2) {
|
|
156
|
+
groups.push(currentGroup);
|
|
157
|
+
}
|
|
158
|
+
return groups;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Create a script element with TypeScript imports
|
|
162
|
+
*/
|
|
163
|
+
function createScriptElement(importMap) {
|
|
164
|
+
const imports = Object.entries(importMap)
|
|
165
|
+
.map(([key, url]) => `import ${key} from '${url}';`)
|
|
166
|
+
.join('\n');
|
|
167
|
+
// Use string concatenation to prevent the bundler from transforming the import path
|
|
168
|
+
const zone5Import = 'import { Zone5 } from ' + '"zone5/components"';
|
|
169
|
+
const lines = ['<script lang="ts">', zone5Import, imports, '</script>'];
|
|
170
|
+
return {
|
|
171
|
+
type: 'raw',
|
|
172
|
+
value: lines.join('\n'),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Remark plugin to process Zone5 images
|
|
177
|
+
*/
|
|
178
|
+
export const remarkZ5Images = () => {
|
|
179
|
+
return (tree, file) => {
|
|
180
|
+
const rootTree = tree;
|
|
181
|
+
const importMap = {};
|
|
182
|
+
const existingKeys = new Set();
|
|
183
|
+
const fm = file.data.fm;
|
|
184
|
+
const zone5mode = fm?.zone5mode;
|
|
185
|
+
// First, collect all Z5 images for the import map
|
|
186
|
+
visit(rootTree, 'image', (node) => {
|
|
187
|
+
if (isZ5Image(node)) {
|
|
188
|
+
const importKey = generateImportKey(node.url, existingKeys);
|
|
189
|
+
importMap[importKey] = node.url;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
// Reset keys for consistent grouping
|
|
193
|
+
existingKeys.clear();
|
|
194
|
+
visit(rootTree, 'root', (node) => {
|
|
195
|
+
// First, handle multi-image paragraphs (images on consecutive lines without blank lines)
|
|
196
|
+
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
197
|
+
const child = node.children[i];
|
|
198
|
+
if (isMultiZ5ImageParagraph(child) && child.type === 'paragraph') {
|
|
199
|
+
const paragraph = child;
|
|
200
|
+
const z5Images = paragraph.children.filter((ch) => isZ5Image(ch));
|
|
201
|
+
const imageData = collectImageData(z5Images, existingKeys);
|
|
202
|
+
const svelteComponent = createZone5Component(imageData, zone5mode);
|
|
203
|
+
// Replace the multi-image paragraph with the svelte component
|
|
204
|
+
node.children.splice(i, 1, svelteComponent);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Then handle consecutive single-image paragraphs
|
|
208
|
+
const groups = findConsecutiveImageGroups(node.children);
|
|
209
|
+
// Process groups in reverse order to maintain correct indices
|
|
210
|
+
for (let i = groups.length - 1; i >= 0; i--) {
|
|
211
|
+
const group = groups[i];
|
|
212
|
+
// Collect image nodes from the group
|
|
213
|
+
const imageNodes = group
|
|
214
|
+
.map((index) => getImageFromParagraph(node.children[index]))
|
|
215
|
+
.filter((img) => img !== null);
|
|
216
|
+
const imageData = collectImageData(imageNodes, existingKeys);
|
|
217
|
+
const svelteComponent = createZone5Component(imageData, zone5mode);
|
|
218
|
+
// Calculate how many nodes to remove (including newlines)
|
|
219
|
+
const startIndex = group[0];
|
|
220
|
+
const endIndex = group[group.length - 1];
|
|
221
|
+
const removeCount = endIndex - startIndex + 1;
|
|
222
|
+
// Count newline paragraphs between images
|
|
223
|
+
let actualRemoveCount = removeCount;
|
|
224
|
+
for (let j = startIndex + 1; j <= endIndex; j++) {
|
|
225
|
+
if (isNewlineParagraph(node.children[j])) {
|
|
226
|
+
actualRemoveCount++;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
node.children.splice(startIndex, actualRemoveCount, svelteComponent);
|
|
230
|
+
}
|
|
231
|
+
// Finally, handle remaining single Z5 images
|
|
232
|
+
for (let i = node.children.length - 1; i >= 0; i--) {
|
|
233
|
+
const child = node.children[i];
|
|
234
|
+
if (isZ5ImageParagraph(child) && child.type === 'paragraph') {
|
|
235
|
+
const imageNode = child.children[0];
|
|
236
|
+
const imageData = collectImageData([imageNode], existingKeys);
|
|
237
|
+
const svelteComponent = createZone5Component(imageData, zone5mode);
|
|
238
|
+
// Replace the single image paragraph with the svelte component
|
|
239
|
+
node.children.splice(i, 1, svelteComponent);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
// Store import map on the VFile's data
|
|
244
|
+
if (!file.data) {
|
|
245
|
+
file.data = {};
|
|
246
|
+
}
|
|
247
|
+
// Convert all svelteComponent nodes to HTML using svast-stringify
|
|
248
|
+
visit(rootTree, 'svelteComponent', (node, index, parent) => {
|
|
249
|
+
if (parent && typeof index === 'number') {
|
|
250
|
+
// Convert the svelteComponent node to HTML using svast-stringify
|
|
251
|
+
const componentHTML = svelteComponentToHTML(node);
|
|
252
|
+
// Create an HTML node with the stringified component
|
|
253
|
+
const htmlNode = {
|
|
254
|
+
type: 'html',
|
|
255
|
+
value: componentHTML,
|
|
256
|
+
};
|
|
257
|
+
// Replace the svelteComponent node with the HTML node
|
|
258
|
+
parent.children[index] = htmlNode;
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
// add script tag at beginning of markdown
|
|
262
|
+
if (Object.keys(importMap).length) {
|
|
263
|
+
// TODO: use existing script tag if any
|
|
264
|
+
const scriptNode = createScriptElement(importMap);
|
|
265
|
+
rootTree.children.unshift(scriptNode);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type Registry, default as registry } from './registry.svelte.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as registry } from './registry.svelte.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type Readable } from 'svelte/store';
|
|
2
|
+
import type { ImageData } from '../components/types';
|
|
3
|
+
export type Registry = Readable<{
|
|
4
|
+
images: ImageData[];
|
|
5
|
+
current: ImageData | null;
|
|
6
|
+
currentOffset: number | null;
|
|
7
|
+
offsets: Map<symbol, {
|
|
8
|
+
start: number;
|
|
9
|
+
count: number;
|
|
10
|
+
}>;
|
|
11
|
+
}> & {
|
|
12
|
+
register: (componentId: symbol, images: ImageData[]) => void;
|
|
13
|
+
remove: (componentId: symbol) => void;
|
|
14
|
+
clear: () => void;
|
|
15
|
+
setCurrent: (componentId: symbol, offset: number) => void;
|
|
16
|
+
findCurrent: (id: string) => boolean;
|
|
17
|
+
next: () => void;
|
|
18
|
+
prev: () => void;
|
|
19
|
+
clearCurrent: () => void;
|
|
20
|
+
};
|
|
21
|
+
declare const registry: Registry;
|
|
22
|
+
export default registry;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
2
|
+
import { get, writable } from 'svelte/store';
|
|
3
|
+
const store = writable({
|
|
4
|
+
images: [],
|
|
5
|
+
current: null,
|
|
6
|
+
currentOffset: null,
|
|
7
|
+
offsets: new Map(),
|
|
8
|
+
});
|
|
9
|
+
const mod = (n, m) => ((n % m) + m) % m;
|
|
10
|
+
const registry = {
|
|
11
|
+
subscribe: store.subscribe,
|
|
12
|
+
register: (componentId, images) => {
|
|
13
|
+
store.update((previous) => {
|
|
14
|
+
const offset = previous.offsets.get(componentId);
|
|
15
|
+
const start = offset?.start ?? previous.images.length;
|
|
16
|
+
const deleteCount = offset?.count ?? 0;
|
|
17
|
+
previous.offsets.set(componentId, { start, count: images.length });
|
|
18
|
+
return {
|
|
19
|
+
...previous,
|
|
20
|
+
images: previous.images.toSpliced(start, deleteCount, ...images),
|
|
21
|
+
current: null,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
},
|
|
25
|
+
remove: (componentId) => {
|
|
26
|
+
store.update((previous) => {
|
|
27
|
+
const offset = previous.offsets.get(componentId);
|
|
28
|
+
if (!offset) {
|
|
29
|
+
return previous;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
...previous,
|
|
33
|
+
images: previous.images.toSpliced(offset.start, offset.count),
|
|
34
|
+
current: null,
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
clear: () => {
|
|
39
|
+
store.set({ images: [], current: null, currentOffset: null, offsets: new SvelteMap() });
|
|
40
|
+
},
|
|
41
|
+
setCurrent: (componentId, offset) => {
|
|
42
|
+
const registered = get(store).offsets.get(componentId);
|
|
43
|
+
if (!registered) {
|
|
44
|
+
throw new Error(`no component registered under given key.`);
|
|
45
|
+
}
|
|
46
|
+
if (offset < 0 || offset >= registered.count) {
|
|
47
|
+
throw new Error(`offset not within registered image count for component`);
|
|
48
|
+
}
|
|
49
|
+
const newCurrentIndex = registered.start + offset;
|
|
50
|
+
store.update((current) => ({
|
|
51
|
+
...current,
|
|
52
|
+
current: current.images[newCurrentIndex],
|
|
53
|
+
currentOffset: newCurrentIndex,
|
|
54
|
+
}));
|
|
55
|
+
},
|
|
56
|
+
findCurrent: (id) => {
|
|
57
|
+
const value = get(store);
|
|
58
|
+
const index = value.images.findIndex((img) => img.id === id);
|
|
59
|
+
if (index >= 0 && value.currentOffset !== index) {
|
|
60
|
+
store.update((current) => ({
|
|
61
|
+
...current,
|
|
62
|
+
current: current.images[index],
|
|
63
|
+
currentOffset: index,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
return index >= 0;
|
|
67
|
+
},
|
|
68
|
+
next: () => {
|
|
69
|
+
const current = get(store);
|
|
70
|
+
if (current.currentOffset === null) {
|
|
71
|
+
throw new Error('can not call next with not current image');
|
|
72
|
+
}
|
|
73
|
+
const newCurrentIndex = mod(current.currentOffset + 1, current.images.length);
|
|
74
|
+
store.update((current) => ({
|
|
75
|
+
...current,
|
|
76
|
+
current: current.images[newCurrentIndex],
|
|
77
|
+
currentOffset: newCurrentIndex,
|
|
78
|
+
}));
|
|
79
|
+
},
|
|
80
|
+
prev: () => {
|
|
81
|
+
const current = get(store);
|
|
82
|
+
if (current.currentOffset === null) {
|
|
83
|
+
throw new Error('can not call prev with not current image');
|
|
84
|
+
}
|
|
85
|
+
const newCurrentIndex = mod(current.currentOffset - 1, current.images.length);
|
|
86
|
+
store.update((current) => ({
|
|
87
|
+
...current,
|
|
88
|
+
current: current.images[newCurrentIndex],
|
|
89
|
+
currentOffset: newCurrentIndex,
|
|
90
|
+
}));
|
|
91
|
+
},
|
|
92
|
+
clearCurrent: () => {
|
|
93
|
+
store.update((current) => ({
|
|
94
|
+
...current,
|
|
95
|
+
current: null,
|
|
96
|
+
currentOffset: null,
|
|
97
|
+
}));
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
export default registry;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Test setup file for Vitest
|
|
4
|
+
* Configures the environment for component testing
|
|
5
|
+
*/
|
|
6
|
+
// Mock ResizeObserver for jsdom
|
|
7
|
+
class ResizeObserverMock {
|
|
8
|
+
observe() { }
|
|
9
|
+
unobserve() { }
|
|
10
|
+
disconnect() { }
|
|
11
|
+
}
|
|
12
|
+
// Set up DOM environment
|
|
13
|
+
if (typeof global !== 'undefined') {
|
|
14
|
+
global.window = global.window || {};
|
|
15
|
+
global.ResizeObserver = ResizeObserverMock;
|
|
16
|
+
}
|
|
17
|
+
// Ensure we're in a browser-like environment
|
|
18
|
+
if (typeof window !== 'undefined' && typeof document !== 'undefined') {
|
|
19
|
+
// Add ResizeObserver polyfill for jsdom
|
|
20
|
+
if (!window.ResizeObserver) {
|
|
21
|
+
window.ResizeObserver = ResizeObserverMock;
|
|
22
|
+
}
|
|
23
|
+
// Browser environment is ready
|
|
24
|
+
console.log('Test environment: Browser (jsdom)');
|
|
25
|
+
}
|
package/dist/vite.d.ts
ADDED