@svgedit/svgcanvas 7.1.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.
@@ -0,0 +1,40 @@
1
+ /* eslint-env node */
2
+ // This rollup script is run by the command:
3
+ // 'npm run build'
4
+
5
+ import rimraf from 'rimraf'
6
+ import babel from '@rollup/plugin-babel'
7
+ import { nodeResolve } from '@rollup/plugin-node-resolve'
8
+ import commonjs from '@rollup/plugin-commonjs'
9
+ import { terser } from 'rollup-plugin-terser'
10
+ // import progress from 'rollup-plugin-progress';
11
+ import filesize from 'rollup-plugin-filesize'
12
+
13
+ const dest = ['dist/svgcanvas']
14
+
15
+ // remove existing distribution
16
+ rimraf('./dist', () => console.info('recreating dist'))
17
+
18
+ // config for svgedit core module
19
+ const config = [{
20
+ input: ['./svgcanvas.js'],
21
+ output: [
22
+ {
23
+ format: 'es',
24
+ inlineDynamicImports: true,
25
+ sourcemap: true,
26
+ file: 'dist/svgcanvas.js'
27
+ }
28
+ ],
29
+ plugins: [
30
+ nodeResolve({
31
+ browser: true,
32
+ preferBuiltins: false
33
+ }),
34
+ commonjs(),
35
+ babel({ babelHelpers: 'bundled', exclude: [/\/core-js\//] }), // exclude core-js to avoid circular dependencies.
36
+ terser({ keep_fnames: true }), // keep_fnames is needed to avoid an error when calling extensions.
37
+ filesize()
38
+ ]
39
+ }]
40
+ export default config
package/sanitize.js ADDED
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Tools for SVG sanitization.
3
+ * @module sanitize
4
+ * @license MIT
5
+ *
6
+ * @copyright 2010 Alexis Deveria, 2010 Jeff Schiller
7
+ */
8
+
9
+ import { getReverseNS, NS } from './namespaces.js'
10
+ import { getHref, setHref, getUrlFromAttr } from './utilities.js'
11
+
12
+ const REVERSE_NS = getReverseNS()
13
+
14
+ // Todo: Split out into core attributes, presentation attributes, etc. so consistent
15
+ /**
16
+ * This defines which elements and attributes that we support (or at least
17
+ * don't remove).
18
+ * @type {PlainObject}
19
+ */
20
+ /* eslint-disable max-len */
21
+ const svgGenericWhiteList = ['class', 'id', 'display', 'transform', 'style']
22
+ const svgWhiteList_ = {
23
+ // SVG Elements
24
+ a: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', 'opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'xlink:href', 'xlink:title'],
25
+ circle: ['clip-path', 'clip-rule', 'cx', 'cy', 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', 'opacity', 'r', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage'],
26
+ clipPath: ['clipPathUnits'],
27
+ defs: [],
28
+ desc: [],
29
+ ellipse: ['clip-path', 'clip-rule', 'cx', 'cy', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', 'opacity', 'requiredFeatures', 'rx', 'ry', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage'],
30
+ feBlend: ['in', 'in2'],
31
+ feColorMatrix: ['in', 'type', 'value', 'result', 'values'],
32
+ feComposite: ['in', 'operator', 'result', 'in2'],
33
+ feFlood: ['flood-color', 'in', 'result', 'flood-opacity'],
34
+ feGaussianBlur: ['color-interpolation-filters', 'in', 'requiredFeatures', 'stdDeviation', 'result'],
35
+ feMerge: [],
36
+ feMergeNode: ['in'],
37
+ feMorphology: ['in', 'operator', 'radius'],
38
+ feOffset: ['dx', 'in', 'dy', 'result'],
39
+ filter: ['color-interpolation-filters', 'filterRes', 'filterUnits', 'height', 'primitiveUnits', 'requiredFeatures', 'width', 'x', 'xlink:href', 'y'],
40
+ foreignObject: ['font-size', 'height', 'opacity', 'requiredFeatures', 'width', 'x', 'y'],
41
+ g: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'font-family', 'font-size', 'font-style', 'font-weight', 'text-anchor'],
42
+ image: ['clip-path', 'clip-rule', 'filter', 'height', 'mask', 'opacity', 'requiredFeatures', 'systemLanguage', 'width', 'x', 'xlink:href', 'xlink:title', 'y'],
43
+ line: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'x1', 'x2', 'y1', 'y2'],
44
+ linearGradient: ['gradientTransform', 'gradientUnits', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'x1', 'x2', 'xlink:href', 'y1', 'y2'],
45
+ marker: ['markerHeight', 'markerUnits', 'markerWidth', 'orient', 'preserveAspectRatio', 'refX', 'refY', 'se_type', 'systemLanguage', 'viewBox'],
46
+ mask: ['height', 'maskContentUnits', 'maskUnits', 'width', 'x', 'y'],
47
+ metadata: [],
48
+ path: ['clip-path', 'clip-rule', 'd', 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage'],
49
+ pattern: ['height', 'patternContentUnits', 'patternTransform', 'patternUnits', 'requiredFeatures', 'systemLanguage', 'viewBox', 'width', 'x', 'xlink:href', 'y'],
50
+ polygon: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'points', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'sides', 'shape', 'edge', 'point', 'starRadiusMultiplier', 'r', 'radialshift', 'r2', 'orient', 'cx', 'cy'],
51
+ polyline: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'points', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'se:connector'],
52
+ radialGradient: ['cx', 'cy', 'fx', 'fy', 'gradientTransform', 'gradientUnits', 'r', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'xlink:href'],
53
+ rect: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'height', 'mask', 'opacity', 'requiredFeatures', 'rx', 'ry', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'width', 'x', 'y'],
54
+ stop: ['offset', 'requiredFeatures', 'stop-opacity', 'systemLanguage', 'stop-color', 'gradientUnits', 'gradientTransform'],
55
+ style: ['type'],
56
+ svg: ['clip-path', 'clip-rule', 'enable-background', 'filter', 'height', 'mask', 'preserveAspectRatio', 'requiredFeatures', 'systemLanguage', 'version', 'viewBox', 'width', 'x', 'xmlns', 'xmlns:se', 'xmlns:xlink', 'xmlns:oi', 'oi:animations', 'y', 'stroke-linejoin', 'fill-rule', 'aria-label', 'stroke-width', 'fill-rule', 'xml:space'],
57
+ switch: ['requiredFeatures', 'systemLanguage'],
58
+ symbol: ['fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'opacity', 'overflow', 'preserveAspectRatio', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'viewBox', 'width', 'height'],
59
+ text: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'mask', 'opacity', 'requiredFeatures', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'text-anchor', 'letter-spacing', 'word-spacing', 'text-decoration', 'textLength', 'lengthAdjust', 'x', 'xml:space', 'y'],
60
+ textPath: ['method', 'requiredFeatures', 'spacing', 'startOffset', 'systemLanguage', 'xlink:href'],
61
+ title: [],
62
+ tspan: ['clip-path', 'clip-rule', 'dx', 'dy', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'font-family', 'font-size', 'font-style', 'font-weight', 'mask', 'opacity', 'requiredFeatures', 'rotate', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'systemLanguage', 'text-anchor', 'textLength', 'x', 'xml:space', 'y'],
63
+ use: ['clip-path', 'clip-rule', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'height', 'mask', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'width', 'x', 'xlink:href', 'y', 'overflow'],
64
+
65
+ // MathML Elements
66
+ annotation: ['encoding'],
67
+ 'annotation-xml': ['encoding'],
68
+ maction: ['actiontype', 'other', 'selection'],
69
+ math: ['xmlns'],
70
+ menclose: ['notation'],
71
+ merror: [],
72
+ mfrac: ['linethickness'],
73
+ mi: ['mathvariant'],
74
+ mmultiscripts: [],
75
+ mn: [],
76
+ mo: ['fence', 'lspace', 'maxsize', 'minsize', 'rspace', 'stretchy'],
77
+ mover: [],
78
+ mpadded: ['lspace', 'width', 'height', 'depth', 'voffset'],
79
+ mphantom: [],
80
+ mprescripts: [],
81
+ mroot: [],
82
+ mrow: ['xlink:href', 'xlink:type', 'xmlns:xlink'],
83
+ mspace: ['depth', 'height', 'width'],
84
+ msqrt: [],
85
+ mstyle: ['displaystyle', 'mathbackground', 'mathcolor', 'mathvariant', 'scriptlevel'],
86
+ msub: [],
87
+ msubsup: [],
88
+ msup: [],
89
+ mtable: ['align', 'columnalign', 'columnlines', 'columnspacing', 'displaystyle', 'equalcolumns', 'equalrows', 'frame', 'rowalign', 'rowlines', 'rowspacing', 'width'],
90
+ mtd: ['columnalign', 'columnspan', 'rowalign', 'rowspan'],
91
+ mtext: [],
92
+ mtr: ['columnalign', 'rowalign'],
93
+ munder: [],
94
+ munderover: [],
95
+ none: [],
96
+ semantics: []
97
+ }
98
+ /* eslint-enable max-len */
99
+
100
+ // add generic attributes to all elements of the whitelist
101
+ Object.keys(svgWhiteList_).forEach((element) => { svgWhiteList_[element] = [...svgWhiteList_[element], ...svgGenericWhiteList] })
102
+
103
+ // Produce a Namespace-aware version of svgWhitelist
104
+ const svgWhiteListNS_ = {}
105
+ Object.entries(svgWhiteList_).forEach(([elt, atts]) => {
106
+ const attNS = {}
107
+ Object.entries(atts).forEach(([_i, att]) => {
108
+ if (att.includes(':')) {
109
+ const v = att.split(':')
110
+ attNS[v[1]] = NS[(v[0]).toUpperCase()]
111
+ } else {
112
+ attNS[att] = att === 'xmlns' ? NS.XMLNS : null
113
+ }
114
+ })
115
+ svgWhiteListNS_[elt] = attNS
116
+ })
117
+
118
+ /**
119
+ * Sanitizes the input node and its children.
120
+ * It only keeps what is allowed from our whitelist defined above.
121
+ * @function module:sanitize.sanitizeSvg
122
+ * @param {Text|Element} node - The DOM element to be checked (we'll also check its children) or text node to be cleaned up
123
+ * @returns {void}
124
+ */
125
+ export const sanitizeSvg = (node) => {
126
+ // Cleanup text nodes
127
+ if (node.nodeType === 3) { // 3 === TEXT_NODE
128
+ // Trim whitespace
129
+ node.nodeValue = node.nodeValue.trim()
130
+ // Remove if empty
131
+ if (!node.nodeValue.length) {
132
+ node.remove()
133
+ }
134
+ }
135
+
136
+ // We only care about element nodes.
137
+ // Automatically return for all non-element nodes, such as comments, etc.
138
+ if (node.nodeType !== 1) { // 1 == ELEMENT_NODE
139
+ return
140
+ }
141
+
142
+ const doc = node.ownerDocument
143
+ const parent = node.parentNode
144
+ // can parent ever be null here? I think the root node's parent is the document...
145
+ if (!doc || !parent) {
146
+ return
147
+ }
148
+
149
+ const allowedAttrs = svgWhiteList_[node.nodeName]
150
+ const allowedAttrsNS = svgWhiteListNS_[node.nodeName]
151
+ // if this element is supported, sanitize it
152
+ if (typeof allowedAttrs !== 'undefined') {
153
+ const seAttrs = []
154
+ let i = node.attributes.length
155
+ while (i--) {
156
+ // if the attribute is not in our whitelist, then remove it
157
+ const attr = node.attributes.item(i)
158
+ const attrName = attr.nodeName
159
+ const attrLocalName = attr.localName
160
+ const attrNsURI = attr.namespaceURI
161
+ // Check that an attribute with the correct localName in the correct namespace is on
162
+ // our whitelist or is a namespace declaration for one of our allowed namespaces
163
+ if (attrNsURI !== allowedAttrsNS[attrLocalName] && attrNsURI !== NS.XMLNS &&
164
+ !(attrNsURI === NS.XMLNS && REVERSE_NS[attr.value])) {
165
+ // Bypassing the whitelist to allow se: and oi: prefixes
166
+ // We can add specific namepaces on demand for now.
167
+ // Is there a more appropriate way to do this?
168
+ if (attrName.startsWith('se:') || attrName.startsWith('oi:') || attrName.startsWith('data-')) {
169
+ // We should bypass the namespace aswell
170
+ const seAttrNS = (attrName.startsWith('se:')) ? NS.SE : ((attrName.startsWith('oi:')) ? NS.OI : null)
171
+ seAttrs.push([attrName, attr.value, seAttrNS])
172
+ } else {
173
+ console.warn(`sanitizeSvg: attribute ${attrName} in element ${node.nodeName} not in whitelist is removed`)
174
+ node.removeAttributeNS(attrNsURI, attrLocalName)
175
+ }
176
+ }
177
+
178
+ // For the style attribute, rewrite it in terms of XML presentational attributes
179
+ if (attrName === 'style') {
180
+ const props = attr.value.split(';')
181
+ let p = props.length
182
+ while (p--) {
183
+ const [name, val] = props[p].split(':')
184
+ const styleAttrName = (name || '').trim()
185
+ const styleAttrVal = (val || '').trim()
186
+ // Now check that this attribute is supported
187
+ if (allowedAttrs.includes(styleAttrName)) {
188
+ node.setAttribute(styleAttrName, styleAttrVal)
189
+ }
190
+ }
191
+ node.removeAttribute('style')
192
+ }
193
+ }
194
+
195
+ Object.values(seAttrs).forEach(([att, val, ns]) => {
196
+ node.setAttributeNS(ns, att, val)
197
+ })
198
+
199
+ // for some elements that have a xlink:href, ensure the URI refers to a local element
200
+ // (but not for links)
201
+ const href = getHref(node)
202
+ if (href &&
203
+ ['filter', 'linearGradient', 'pattern',
204
+ 'radialGradient', 'textPath', 'use'].includes(node.nodeName) && href[0] !== '#') {
205
+ // remove the attribute (but keep the element)
206
+ setHref(node, '')
207
+ console.warn(`sanitizeSvg: attribute href in element ${node.nodeName} pointing to a non-local reference (${href}) is removed`)
208
+ node.removeAttributeNS(NS.XLINK, 'href')
209
+ }
210
+
211
+ // Safari crashes on a <use> without a xlink:href, so we just remove the node here
212
+ if (node.nodeName === 'use' && !getHref(node)) {
213
+ console.warn(`sanitizeSvg: element ${node.nodeName} without a xlink:href is removed`)
214
+ node.remove()
215
+ return
216
+ }
217
+ // if the element has attributes pointing to a non-local reference,
218
+ // need to remove the attribute
219
+ Object.values(['clip-path', 'fill', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke'], (attr) => {
220
+ let val = node.getAttribute(attr)
221
+ if (val) {
222
+ val = getUrlFromAttr(val)
223
+ // simply check for first character being a '#'
224
+ if (val && val[0] !== '#') {
225
+ node.setAttribute(attr, '')
226
+ console.warn(`sanitizeSvg: attribute ${attr} in element ${node.nodeName} pointing to a non-local reference (${val}) is removed`)
227
+ node.removeAttribute(attr)
228
+ }
229
+ }
230
+ })
231
+
232
+ // recurse to children
233
+ i = node.childNodes.length
234
+ while (i--) { sanitizeSvg(node.childNodes.item(i)) }
235
+ // else (element not supported), remove it
236
+ } else {
237
+ // remove all children from this node and insert them before this node
238
+ // TODO: in the case of animation elements this will hardly ever be correct
239
+ console.warn(`sanitizeSvg: element ${node.nodeName} not supported is removed`)
240
+ const children = []
241
+ while (node.hasChildNodes()) {
242
+ children.push(parent.insertBefore(node.firstChild, node))
243
+ }
244
+
245
+ // remove this node from the document altogether
246
+ node.remove()
247
+
248
+ // call sanitizeSvg on each of those children
249
+ let i = children.length
250
+ while (i--) { sanitizeSvg(children[i]) }
251
+ }
252
+ }