@streamscloud/kit 0.80.0 → 0.81.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.
@@ -12,7 +12,11 @@ export declare class HtmlHelper {
12
12
  */
13
13
  static trimElementValue(element: HTMLInputElement | HTMLTextAreaElement): boolean;
14
14
  static pasteIntoInput(value: string, element: HTMLInputElement | HTMLTextAreaElement): void;
15
- static sanitizeSvg(svg: string): string;
15
+ /**
16
+ * Sanitizes SVG markup and serializes it as XML, valid both inline in HTML and as a standalone `image/svg+xml` file.
17
+ * Returns null when the markup isn't a well-formed SVG document, or when there is no window.
18
+ */
19
+ static sanitizeSvg(svg: string): string | null;
16
20
  static sanitizeHtml(html: string): string;
17
21
  static stripHtml(html: string, lineBreaksReplacer?: string | null, trimResult?: boolean): string;
18
22
  }
@@ -1,4 +1,6 @@
1
1
  import dp from 'dompurify';
2
+ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
3
+ const isSvgRoot = (element) => element?.localName === 'svg' && element.namespaceURI === SVG_NAMESPACE;
2
4
  export class HtmlHelper {
3
5
  static clearSelection() {
4
6
  if (window.getSelection) {
@@ -66,11 +68,28 @@ export class HtmlHelper {
66
68
  }));
67
69
  element.focus();
68
70
  }
71
+ /**
72
+ * Sanitizes SVG markup and serializes it as XML, valid both inline in HTML and as a standalone `image/svg+xml` file.
73
+ * Returns null when the markup isn't a well-formed SVG document, or when there is no window.
74
+ */
69
75
  static sanitizeSvg(svg) {
70
76
  if (typeof window === 'undefined') {
71
- return svg;
77
+ return null;
78
+ }
79
+ const parsed = new DOMParser().parseFromString(svg, 'image/svg+xml');
80
+ if (!isSvgRoot(parsed.documentElement) || parsed.getElementsByTagName('parsererror').length) {
81
+ return null;
82
+ }
83
+ const sanitized = dp.sanitize(parsed.documentElement, {
84
+ USE_PROFILES: { svg: true, svgFilters: true },
85
+ ADD_ATTR: ['dominant-baseline'],
86
+ RETURN_DOM_FRAGMENT: true
87
+ });
88
+ const root = sanitized.firstElementChild;
89
+ if (!isSvgRoot(root)) {
90
+ return null;
72
91
  }
73
- return dp.sanitize(svg, { USE_PROFILES: { svg: true, svgFilters: true }, ADD_ATTR: ['dominant-baseline'] });
92
+ return new XMLSerializer().serializeToString(root);
74
93
  }
75
94
  static sanitizeHtml(html) {
76
95
  return HtmlSanitizer.sanitize(html);
@@ -5,9 +5,12 @@ let mounted = $state(false);
5
5
  onMount(() => {
6
6
  mounted = true;
7
7
  });
8
+ const sanitizedSvg = $derived(mounted ? HtmlHelper.sanitizeSvg(src) : null);
8
9
  </script>
9
10
 
10
- {#if src?.startsWith('<svg')}
11
+ {#if mounted && sanitizedSvg === null}
12
+ <i class="invalid-svg">Invalid SVG</i>
13
+ {:else}
11
14
  <div
12
15
  class="icon"
13
16
  class:icon--accent={color === 'accent'}
@@ -26,12 +29,10 @@ onMount(() => {
26
29
  class:icon--control-md={size === 'control-md'}
27
30
  class:icon--control-lg={size === 'control-lg'}
28
31
  class:icon--control-xl={size === 'control-xl'}>
29
- {#if mounted}
30
- {@html HtmlHelper.sanitizeSvg(src)}
32
+ {#if sanitizedSvg}
33
+ {@html sanitizedSvg}
31
34
  {/if}
32
35
  </div>
33
- {:else}
34
- <i class="invalid-svg">Invalid SVG</i>
35
36
  {/if}
36
37
 
37
38
  <!--
@@ -1,6 +1,6 @@
1
1
  import type { IconColor, IconSize } from './types';
2
2
  interface Props {
3
- /** Raw SVG string (must start with `<svg`) */
3
+ /** SVG markup, a well-formed `image/svg+xml` document (an XML declaration and DOCTYPE are allowed) */
4
4
  src: string;
5
5
  /** Predefined color preset applied to the icon */
6
6
  color?: IconColor | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.80.0",
3
+ "version": "0.81.0",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",