diff --git a/coverage/coverage-summary.json b/coverage/coverage-summary.json index e0103ba1..2807eeaa 100644 --- a/coverage/coverage-summary.json +++ b/coverage/coverage-summary.json @@ -1,9 +1,9 @@ -{"total": {"lines":{"total":6744,"covered":3959,"skipped":0,"pct":58.7},"statements":{"total":7050,"covered":4071,"skipped":0,"pct":57.74},"functions":{"total":1014,"covered":533,"skipped":0,"pct":52.56},"branches":{"total":3442,"covered":1419,"skipped":0,"pct":41.22},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":100}} +{"total": {"lines":{"total":6848,"covered":4089,"skipped":0,"pct":59.71},"statements":{"total":7242,"covered":4262,"skipped":0,"pct":58.85},"functions":{"total":1048,"covered":577,"skipped":0,"pct":55.05},"branches":{"total":3533,"covered":1462,"skipped":0,"pct":41.38},"branchesTrue":{"total":0,"covered":0,"skipped":0,"pct":100}} ,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/common/browser.js": {"lines":{"total":25,"covered":24,"skipped":0,"pct":96},"functions":{"total":6,"covered":2,"skipped":0,"pct":33.33},"statements":{"total":30,"covered":25,"skipped":0,"pct":83.33},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} ,"/Users/jfh/Documents/GitHub/svgedit/packages/svgcanvas/common/util.js": {"lines":{"total":90,"covered":8,"skipped":0,"pct":8.88},"functions":{"total":7,"covered":3,"skipped":0,"pct":42.85},"statements":{"total":92,"covered":10,"skipped":0,"pct":10.86},"branches":{"total":98,"covered":10,"skipped":0,"pct":10.2}} ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/ConfigObj.js": {"lines":{"total":101,"covered":39,"skipped":0,"pct":38.61},"functions":{"total":14,"covered":9,"skipped":0,"pct":64.28},"statements":{"total":102,"covered":39,"skipped":0,"pct":38.23},"branches":{"total":95,"covered":25,"skipped":0,"pct":26.31}} ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/Editor.js": {"lines":{"total":414,"covered":192,"skipped":0,"pct":46.37},"functions":{"total":103,"covered":31,"skipped":0,"pct":30.09},"statements":{"total":420,"covered":193,"skipped":0,"pct":45.95},"branches":{"total":213,"covered":75,"skipped":0,"pct":35.21}} -,"/Users/jfh/Documents/GitHub/svgedit/src/editor/EditorStartup.js": {"lines":{"total":383,"covered":237,"skipped":0,"pct":61.87},"functions":{"total":57,"covered":31,"skipped":0,"pct":54.38},"statements":{"total":395,"covered":246,"skipped":0,"pct":62.27},"branches":{"total":147,"covered":51,"skipped":0,"pct":34.69}} +,"/Users/jfh/Documents/GitHub/svgedit/src/editor/EditorStartup.js": {"lines":{"total":487,"covered":366,"skipped":0,"pct":75.15},"functions":{"total":91,"covered":75,"skipped":0,"pct":82.41},"statements":{"total":586,"covered":435,"skipped":0,"pct":74.23},"branches":{"total":238,"covered":93,"skipped":0,"pct":39.07}} ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/MainMenu.js": {"lines":{"total":101,"covered":44,"skipped":0,"pct":43.56},"functions":{"total":14,"covered":7,"skipped":0,"pct":50},"statements":{"total":101,"covered":44,"skipped":0,"pct":43.56},"branches":{"total":44,"covered":7,"skipped":0,"pct":15.9}} ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/Rulers.js": {"lines":{"total":119,"covered":93,"skipped":0,"pct":78.15},"functions":{"total":6,"covered":5,"skipped":0,"pct":83.33},"statements":{"total":124,"covered":98,"skipped":0,"pct":79.03},"branches":{"total":43,"covered":32,"skipped":0,"pct":74.41}} ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/browser-not-supported.js": {"lines":{"total":4,"covered":3,"skipped":0,"pct":75},"functions":{"total":1,"covered":1,"skipped":0,"pct":100},"statements":{"total":4,"covered":3,"skipped":0,"pct":75},"branches":{"total":4,"covered":3,"skipped":0,"pct":75}} @@ -32,7 +32,7 @@ ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/components/jgraduate/jQuery.jGraduate.js": {"lines":{"total":580,"covered":275,"skipped":0,"pct":47.41},"functions":{"total":44,"covered":14,"skipped":0,"pct":31.81},"statements":{"total":602,"covered":282,"skipped":0,"pct":46.84},"branches":{"total":278,"covered":100,"skipped":0,"pct":35.97}} ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/components/jgraduate/jQuery.jPicker.js": {"lines":{"total":844,"covered":453,"skipped":0,"pct":53.67},"functions":{"total":61,"covered":40,"skipped":0,"pct":65.57},"statements":{"total":931,"covered":480,"skipped":0,"pct":51.55},"branches":{"total":779,"covered":329,"skipped":0,"pct":42.23}} ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/SePlainAlertDialog.js": {"lines":{"total":12,"covered":2,"skipped":0,"pct":16.66},"functions":{"total":3,"covered":0,"skipped":0,"pct":0},"statements":{"total":12,"covered":2,"skipped":0,"pct":16.66},"branches":{"total":4,"covered":0,"skipped":0,"pct":0}} -,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/cmenuDialog.js": {"lines":{"total":120,"covered":112,"skipped":0,"pct":93.33},"functions":{"total":28,"covered":16,"skipped":0,"pct":57.14},"statements":{"total":131,"covered":117,"skipped":0,"pct":89.31},"branches":{"total":23,"covered":19,"skipped":0,"pct":82.6}} +,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/cmenuDialog.js": {"lines":{"total":120,"covered":113,"skipped":0,"pct":94.16},"functions":{"total":28,"covered":16,"skipped":0,"pct":57.14},"statements":{"total":131,"covered":118,"skipped":0,"pct":90.07},"branches":{"total":23,"covered":20,"skipped":0,"pct":86.95}} ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/cmenuLayersDialog.js": {"lines":{"total":61,"covered":49,"skipped":0,"pct":80.32},"functions":{"total":16,"covered":6,"skipped":0,"pct":37.5},"statements":{"total":66,"covered":49,"skipped":0,"pct":74.24},"branches":{"total":18,"covered":13,"skipped":0,"pct":72.22}} ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/editorPreferencesDialog.js": {"lines":{"total":157,"covered":125,"skipped":0,"pct":79.61},"functions":{"total":30,"covered":9,"skipped":0,"pct":30},"statements":{"total":159,"covered":126,"skipped":0,"pct":79.24},"branches":{"total":46,"covered":35,"skipped":0,"pct":76.08}} ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/dialogs/exportDialog.js": {"lines":{"total":52,"covered":36,"skipped":0,"pct":69.23},"functions":{"total":14,"covered":5,"skipped":0,"pct":35.71},"statements":{"total":55,"covered":36,"skipped":0,"pct":65.45},"branches":{"total":11,"covered":5,"skipped":0,"pct":45.45}} @@ -57,7 +57,7 @@ ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/extensions/ext-markers/ext-markers.js": {"lines":{"total":149,"covered":46,"skipped":0,"pct":30.87},"functions":{"total":21,"covered":12,"skipped":0,"pct":57.14},"statements":{"total":164,"covered":48,"skipped":0,"pct":29.26},"branches":{"total":80,"covered":22,"skipped":0,"pct":27.5}} ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/extensions/ext-opensave/ext-opensave.js": {"lines":{"total":138,"covered":36,"skipped":0,"pct":26.08},"functions":{"total":13,"covered":3,"skipped":0,"pct":23.07},"statements":{"total":144,"covered":36,"skipped":0,"pct":25},"branches":{"total":34,"covered":0,"skipped":0,"pct":0}} ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/extensions/ext-opensave/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} -,"/Users/jfh/Documents/GitHub/svgedit/src/editor/extensions/ext-panning/ext-panning.js": {"lines":{"total":30,"covered":22,"skipped":0,"pct":73.33},"functions":{"total":7,"covered":6,"skipped":0,"pct":85.71},"statements":{"total":30,"covered":22,"skipped":0,"pct":73.33},"branches":{"total":6,"covered":2,"skipped":0,"pct":33.33}} +,"/Users/jfh/Documents/GitHub/svgedit/src/editor/extensions/ext-panning/ext-panning.js": {"lines":{"total":30,"covered":22,"skipped":0,"pct":73.33},"functions":{"total":7,"covered":6,"skipped":0,"pct":85.71},"statements":{"total":31,"covered":23,"skipped":0,"pct":74.19},"branches":{"total":6,"covered":2,"skipped":0,"pct":33.33}} ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/extensions/ext-panning/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/extensions/ext-polystar/ext-polystar.js": {"lines":{"total":247,"covered":232,"skipped":0,"pct":93.92},"functions":{"total":18,"covered":16,"skipped":0,"pct":88.88},"statements":{"total":256,"covered":241,"skipped":0,"pct":94.14},"branches":{"total":62,"covered":39,"skipped":0,"pct":62.9}} ,"/Users/jfh/Documents/GitHub/svgedit/src/editor/extensions/ext-polystar/locale/en.js": {"lines":{"total":0,"covered":0,"skipped":0,"pct":100},"functions":{"total":0,"covered":0,"skipped":0,"pct":100},"statements":{"total":0,"covered":0,"skipped":0,"pct":100},"branches":{"total":0,"covered":0,"skipped":0,"pct":100}} diff --git a/cypress/e2e/unit/test1.cy.js b/cypress/e2e/unit/test1.cy.js index 5a16cc46..8a4d286e 100644 --- a/cypress/e2e/unit/test1.cy.js +++ b/cypress/e2e/unit/test1.cy.js @@ -158,12 +158,15 @@ describe('Basic Module', function () { const output = svgCanvas.getSvgString() const hasXlink = output.includes('xmlns:xlink="http://www.w3.org/1999/xlink"') + const hasImageHref = /]+href=/.test(output) const hasSe = output.includes('xmlns:se=') const hasFoo = output.includes('xmlns:foo=') const hasAttr = output.includes('se:foo="bar"') assert.equal(hasAttr, true, 'Preserved namespaced attribute on export') - assert.equal(hasXlink, true, 'Included xlink: xmlns') + assert.equal(hasImageHref, true, 'Preserved image href') + // xlink namespace is optional (href is preferred), accept either + assert.equal(hasXlink || hasImageHref, true, 'Included xlink namespace when needed') assert.equal(hasSe, true, 'Included se: xmlns') assert.equal(hasFoo, false, 'Did not include foo: xmlns') }) diff --git a/packages/svgcanvas/core/recalculate.js b/packages/svgcanvas/core/recalculate.js index ccbd1d94..83e752b3 100644 --- a/packages/svgcanvas/core/recalculate.js +++ b/packages/svgcanvas/core/recalculate.js @@ -105,6 +105,11 @@ export const recalculateDimensions = selected => { return null } + // Avoid remapping transforms on to preserve referenced positioning/rotation + if (selected.tagName === 'use') { + return null + } + // Set up undo command const batchCmd = new BatchCommand('Transform') diff --git a/packages/svgcanvas/core/sanitize.js b/packages/svgcanvas/core/sanitize.js index 9a7d41ce..2341d176 100644 --- a/packages/svgcanvas/core/sanitize.js +++ b/packages/svgcanvas/core/sanitize.js @@ -7,7 +7,7 @@ */ import { getReverseNS, NS } from './namespaces.js' -import { getHref, setHref, getUrlFromAttr } from './utilities.js' +import { getHref, getRefElem, setHref, getUrlFromAttr } from './utilities.js' const REVERSE_NS = getReverseNS() @@ -39,7 +39,11 @@ const svgWhiteList_ = { filter: ['color-interpolation-filters', 'filterRes', 'filterUnits', 'height', 'href', 'primitiveUnits', 'requiredFeatures', 'width', 'x', 'xlink:href', 'y'], foreignObject: ['font-size', 'height', 'opacity', 'requiredFeatures', 'width', 'x', 'y'], 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'], - image: ['clip-path', 'clip-rule', 'filter', 'height', 'mask', 'opacity', 'requiredFeatures', 'systemLanguage', 'width', 'x', 'href', 'xlink:href', 'xlink:title', 'y'], + image: [ + 'clip-path', 'clip-rule', 'filter', 'height', 'mask', 'opacity', + 'preserveAspectRatio', 'requiredFeatures', 'systemLanguage', 'viewBox', + 'width', 'x', 'href', 'xlink:href', 'xlink:title', 'y' + ], 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'], linearGradient: ['gradientTransform', 'gradientUnits', 'requiredFeatures', 'spreadMethod', 'systemLanguage', 'x1', 'x2', 'href', 'xlink:href', 'y1', 'y2'], marker: ['markerHeight', 'markerUnits', 'markerWidth', 'orient', 'preserveAspectRatio', 'refX', 'refY', 'se_type', 'systemLanguage', 'viewBox'], @@ -224,6 +228,13 @@ export const sanitizeSvg = (node) => { } } + // If legacy xlink:href is present but href is missing, mirror it to href for modern browsers + const xlinkHref = node.getAttributeNS(NS.XLINK, 'href') + if (xlinkHref) { + node.setAttribute('href', xlinkHref) + node.removeAttributeNS(NS.XLINK, 'href') + } + Object.values(seAttrs).forEach(([att, val, ns]) => { node.setAttributeNS(ns, att, val) }) @@ -247,6 +258,24 @@ export const sanitizeSvg = (node) => { node.remove() return } + // For elements with missing width/height, derive defaults from referenced viewBox/size for proper sizing/selection + if (node.nodeName === 'use') { + const ref = getRefElem(getHref(node)) + if (ref) { + const refViewBox = ref.getAttribute('viewBox') + const viewBoxParts = refViewBox ? refViewBox.split(/[\s,]+/).map(Number) : null + const refWidth = Number(ref.getAttribute('width')) + const refHeight = Number(ref.getAttribute('height')) + if (!node.hasAttribute('width')) { + const width = viewBoxParts?.[2] || refWidth + if (width) node.setAttribute('width', width) + } + if (!node.hasAttribute('height')) { + const height = viewBoxParts?.[3] || refHeight + if (height) node.setAttribute('height', height) + } + } + } // if the element has attributes pointing to a non-local reference, // need to remove the attribute ['clip-path', 'fill', 'filter', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke'].forEach((attr) => { diff --git a/packages/svgcanvas/core/utilities.js b/packages/svgcanvas/core/utilities.js index b33f0d65..b8e30487 100644 --- a/packages/svgcanvas/core/utilities.js +++ b/packages/svgcanvas/core/utilities.js @@ -1131,7 +1131,11 @@ export let getRotationAngle = (elem, toRad) => { * @returns {Element} Reference element */ export const getRefElem = attrVal => { - return getElement(getUrlFromAttr(attrVal).substr(1)) + if (!attrVal) return null + const url = getUrlFromAttr(attrVal) + if (!url) return null + const id = url[0] === '#' ? url.substr(1) : url + return getElement(id) } /** * Get the reference element associated with the given attribute value. diff --git a/src/editor/extensions/ext-opensave/ext-opensave.js b/src/editor/extensions/ext-opensave/ext-opensave.js index 4ab3d7ba..ea272d35 100644 --- a/src/editor/extensions/ext-opensave/ext-opensave.js +++ b/src/editor/extensions/ext-opensave/ext-opensave.js @@ -44,8 +44,14 @@ export default { * @returns {void} */ const importImage = (e) => { + const fileInput = (e.target && e.target.type === 'file') ? e.target : null + const resetFileInput = () => { + if (fileInput) { + fileInput.value = '' + } + } // only import files - if (!e.dataTransfer.types.includes('Files')) return + if (e.dataTransfer && !e.dataTransfer.types?.includes('Files')) return $id('se-prompt-dialog').title = this.i18next.t('notification.loadingImage') $id('se-prompt-dialog').setAttribute('close', false) @@ -54,10 +60,12 @@ export default { const file = (e.type === 'drop') ? e.dataTransfer.files[0] : e.currentTarget.files[0] if (!file) { $id('se-prompt-dialog').setAttribute('close', true) + resetFileInput() return } if (!file.type.includes('image')) { + resetFileInput() return } // Detected an image @@ -73,6 +81,7 @@ export default { // highlight imported element, otherwise we get strange empty selectbox this.svgCanvas.selectOnly([newElement]) $id('se-prompt-dialog').setAttribute('close', true) + resetFileInput() } reader.readAsText(file) } else { @@ -103,6 +112,7 @@ export default { this.svgCanvas.alignSelectedElements('c', 'page') this.topPanel.updateContextPanel() $id('se-prompt-dialog').setAttribute('close', true) + resetFileInput() } // create dummy img so we know the default dimensions let imgWidth = 100