@annotorious/annotorious 3.8.2 → 3.8.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@annotorious/annotorious",
3
- "version": "3.8.2",
3
+ "version": "3.8.4",
4
4
  "description": "Add image annotation functionality to any web page with a few lines of JavaScript",
5
5
  "author": "Rainer Simon",
6
6
  "license": "BSD-3-Clause",
@@ -46,16 +46,16 @@
46
46
  "@sveltejs/vite-plugin-svelte": "^3.1.2",
47
47
  "@tsconfig/svelte": "^5.0.8",
48
48
  "@types/rbush": "^4.0.0",
49
- "jsdom": "^29.1.0",
49
+ "jsdom": "^29.1.1",
50
50
  "svelte": "^4.2.20",
51
- "svelte-preprocess": "^6.0.3",
51
+ "svelte-preprocess": "^6.0.5",
52
52
  "typescript": "^5.9.3",
53
53
  "vite": "^5.4.21",
54
54
  "vite-plugin-dts": "^4.5.4",
55
- "vitest": "^3.2.4"
55
+ "vitest": "^3.2.6"
56
56
  },
57
57
  "dependencies": {
58
- "@annotorious/core": "3.8.2",
58
+ "@annotorious/core": "3.8.4",
59
59
  "dequal": "^2.0.3",
60
60
  "rbush": "^4.0.1",
61
61
  "simplify-js": "^1.2.4",
@@ -33,6 +33,22 @@ export const W3CImageFormat = (
33
33
  return { parse, serialize }
34
34
  }
35
35
 
36
+ const isSupportedSelector = (selector: any): boolean =>
37
+ selector?.type === 'SvgSelector' ||
38
+ selector?.type === 'FragmentSelector' ||
39
+ isFragmentSelector(selector);
40
+
41
+ const isSupportedTarget = (target: any): boolean => {
42
+ if (typeof target === 'string')
43
+ return isFragmentSelector(target); // Plain string fragment selector
44
+
45
+ const selector = Array.isArray(target.selector)
46
+ ? target.selector.find(isSupportedSelector)
47
+ : target.selector;
48
+
49
+ return isSupportedSelector(selector);
50
+ }
51
+
36
52
  export const parseW3CImageAnnotation = (
37
53
  annotation: W3CAnnotation,
38
54
  opts: W3CImageFormatAdapterOpts = { strict: true, invertY: false }
@@ -49,8 +65,15 @@ export const parseW3CImageAnnotation = (
49
65
 
50
66
  const bodies = parseW3CBodies(body || [], annotationId);
51
67
 
52
- const w3cTarget = Array.isArray(annotation.target)
53
- ? annotation.target[0] : annotation.target;
68
+ const w3cTarget = Array.isArray(annotation.target)
69
+ ? annotation.target.find(isSupportedTarget)
70
+ : annotation.target;
71
+
72
+ if (!w3cTarget) {
73
+ return {
74
+ error: Error(`Unsupported target(s): ${JSON.stringify(w3cTarget)}`)
75
+ };
76
+ }
54
77
 
55
78
  const w3cSelector =
56
79
  typeof w3cTarget === 'string' ? w3cTarget :
@@ -11,6 +11,10 @@ export interface FragmentSelector {
11
11
 
12
12
  }
13
13
 
14
+ const number = '-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?';
15
+
16
+ const MAX_FRAGMENT_LENGTH = 512; // ReDoS guard
17
+
14
18
  export const isFragmentSelector = (
15
19
  selector: any
16
20
  ): boolean => {
@@ -18,10 +22,15 @@ export const isFragmentSelector = (
18
22
  return true;
19
23
 
20
24
  if (typeof selector === 'string') {
25
+ if (selector.length > MAX_FRAGMENT_LENGTH) return false;
26
+
21
27
  const hashIndex = selector.indexOf('#');
22
28
  if (hashIndex < 0) return false;
23
29
 
24
- const xywh = /#xywh(?:=(?:pixel:|percent:)?)(.+?),(.+?),(.+?),(.+)$/i;
30
+ const xywh = new RegExp(
31
+ `#xywh=((?:pixel|percent):)?(${number}),(${number}),(${number}),(${number})$`,
32
+ 'i');
33
+
25
34
  return xywh.test(selector);
26
35
  }
27
36
 
@@ -35,8 +44,13 @@ export const parseFragmentSelector = (
35
44
  const fragment =
36
45
  typeof fragmentOrSelector === 'string' ? fragmentOrSelector : fragmentOrSelector.value;
37
46
 
38
- const regex = /(xywh)=(?:(pixel|percent):)?:?(.+?),(.+?),(.+?),(.+)*/g;
39
- const matches = [...fragment.matchAll(regex)][0];
47
+ if (fragment.length > MAX_FRAGMENT_LENGTH) throw new Error('Fragment too long: ' + fragment);
48
+
49
+ const regex = new RegExp(
50
+ `(xywh)=((?:pixel|percent))?:?(${number}),(${number}),(${number}),(${number})$`,
51
+ );
52
+
53
+ const matches = regex.exec(fragment);
40
54
 
41
55
  if (!matches) throw new Error('Not a MediaFragment: ' + fragment);
42
56
 
@@ -37,7 +37,13 @@ export const insertSVGNamespace = (originalDoc: Document): Element => {
37
37
  export const parseSVGXML = (value: string): Element => {
38
38
  const parser = new DOMParser();
39
39
 
40
- const doc = parser.parseFromString(value, 'image/svg+xml');
40
+ // Parser assumes an <svg /> root element, but W3C spec also
41
+ // allows just the shape element, without SVG doc.
42
+ const wrapped = value.trimStart().startsWith('<svg')
43
+ ? value
44
+ : `<svg xmlns="${SVG_NAMESPACE}">${value}</svg>`;
45
+
46
+ const doc = parser.parseFromString(wrapped, 'image/svg+xml');
41
47
 
42
48
  // SVG needs a namespace declaration - check if it's set or insert if not
43
49
  const isPrefixDeclared = doc.lookupPrefix(SVG_NAMESPACE); // SVG declared via prefix
@@ -163,7 +163,6 @@ const parsePathCommands = (d: string) => {
163
163
  const commands: PathCommand[] = [];
164
164
  const cleanPath = d.replace(/,/g, ' ').trim();
165
165
 
166
- // Updated regex to include H and V commands
167
166
  const commandRegex = /([MmLlHhVvCcZz])\s*([^MmLlHhVvCcZz]*)/g;
168
167
  let match;
169
168
 
@@ -171,12 +170,33 @@ const parsePathCommands = (d: string) => {
171
170
  const [, commandLetter, argsString] = match;
172
171
  const args = argsString.trim() === '' ? [] :
173
172
  argsString.trim().split(/\s+/).map(Number).filter(n => !isNaN(n));
174
-
175
- commands.push({
176
- type: commandLetter,
177
- args
178
- });
173
+
174
+ const step = argStep(commandLetter);
175
+
176
+ if (step === 0 || args.length <= step) {
177
+ // Z or single-instance command
178
+ commands.push({ type: commandLetter, args });
179
+ } else {
180
+ // Repeated commands
181
+ for (let i = 0; i < args.length; i += step) {
182
+ const impliedType = i === 0 ? commandLetter
183
+ : commandLetter === 'M' ? 'L'
184
+ : commandLetter === 'm' ? 'l'
185
+ : commandLetter;
186
+ commands.push({ type: impliedType, args: args.slice(i, i + step) });
187
+ }
188
+ }
179
189
  }
180
190
 
181
191
  return commands;
192
+ }
193
+
194
+ const argStep = (cmd: string): number => {
195
+ switch (cmd.toUpperCase()) {
196
+ case 'M': case 'L': return 2;
197
+ case 'H': case 'V': return 1;
198
+ case 'C': return 6;
199
+ case 'Z': return 0;
200
+ default: return 2;
201
+ }
182
202
  }