Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# SVG-Edit CHANGES

## 7.4.1
- Fix: parent transform iteration and undo/redo for grouped elements
- Fix: gradient inheritance, clipPath translation, blur filters, layer operations
- Tests: add Playwright regression tests for 11 GitHub issues
- Build: ensure coverage instrumentation for e2e tests

## 7.4.0
- Scripts: adapt `build` and `publish` for root-managed builds/publishes across workspaces.
- Docs: Update release/publish instructions to reflect workspace versioning and the new `scripts/version-bump.mjs` helper.
Expand Down
72 changes: 59 additions & 13 deletions coverage/coverage-summary.json

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions docs/ReleaseInstructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
- Confirm `CHANGES.md` has been updated.
- Run the full release checks (`npm run test-build` → tests, docs, and build); it exits on failure.
- Ask before creating a release commit and tag (defaults to `v<version>`); declining aborts the publish.
- Publish all workspaces and the root package together.
- Publish all workspaces first, then the root package.

You will need to be a member of the npm group to do this step.

544 changes: 277 additions & 267 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "svgedit",
"version": "7.4.0",
"version": "7.4.1",
"description": "Powerful SVG-Editor for your browser ",
"main": "dist/editor/Editor.js",
"module": "dist/editor/Editor.js",
Expand Down Expand Up @@ -82,34 +82,34 @@
]
},
"dependencies": {
"@svgedit/svgcanvas": "workspace:*",
"@svgedit/svgcanvas": "7.4.1",
"browser-fs-access": "0.38.0",
"elix": "15.0.1",
"i18next": "25.7.1",
"jspdf": "3.0.4",
"i18next": "25.7.4",
"jspdf": "4.0.0",
"pathseg": "1.2.1",
"svg2pdf.js": "2.6.0"
"svg2pdf.js": "2.7.0"
},
"devDependencies": {
"@playwright/test": "^1.57.0",
"@rollup/plugin-dynamic-import-vars": "2.1.5",
"@vitest/coverage-v8": "^4.0.15",
"@vitest/coverage-v8": "^4.0.16",
"jamilih": "0.63.1",
"jsdoc": "4.0.5",
"jsdom": "^27.2.0",
"jsdom": "^27.4.0",
"npm-run-all": "4.1.5",
"nyc": "17.1.0",
"open-cli": "8.0.0",
"remark-cli": "12.0.1",
"remark-lint-ordered-list-marker-value": "4.0.1",
"rimraf": "6.1.2",
"standard": "17.1.2",
"vite": "^7.2.6",
"vite": "^7.3.1",
"vite-plugin-istanbul": "^7.2.1",
"vite-plugin-string": "^1.2.3",
"vitest": "^4.0.15"
"vitest": "^4.0.16"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.53.3"
"@rollup/rollup-linux-x64-gnu": "4.55.1"
}
}
2 changes: 1 addition & 1 deletion packages/react-test/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@svgedit/react-test",
"version": "7.4.0",
"version": "7.4.1",
"description": "",
"main": "dist/index.js",
"scripts": {
Expand Down
141 changes: 104 additions & 37 deletions packages/svgcanvas/common/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,60 +8,127 @@

const NSSVG = 'http://www.w3.org/2000/svg'

const { userAgent } = navigator

// Note: Browser sniffing should only be used if no other detection method is possible
const isWebkit_ = userAgent.includes('AppleWebKit')
const isGecko_ = userAgent.includes('Gecko/')
const isChrome_ = userAgent.includes('Chrome/')
const isMac_ = userAgent.includes('Macintosh')

// text character positioning (for IE9 and now Chrome)
const supportsGoodTextCharPos_ = (function () {
const svgroot = document.createElementNS(NSSVG, 'svg')
const svgContent = document.createElementNS(NSSVG, 'svg')
document.documentElement.append(svgroot)
svgContent.setAttribute('x', 5)
svgroot.append(svgContent)
const text = document.createElementNS(NSSVG, 'text')
text.textContent = 'a'
svgContent.append(text)
try { // Chrome now fails here
const pos = text.getStartPositionOfChar(0).x
return (pos === 0)
} catch (err) {
return false
} finally {
svgroot.remove()
/**
* Browser capabilities and detection object.
* Uses modern feature detection and lazy evaluation patterns.
*/
class BrowserDetector {
#userAgent = navigator.userAgent
#cachedResults = new Map()

/**
* Detects if the browser is WebKit-based
* @returns {boolean}
*/
get isWebkit () {
if (!this.#cachedResults.has('isWebkit')) {
this.#cachedResults.set('isWebkit', this.#userAgent.includes('AppleWebKit'))
}
return this.#cachedResults.get('isWebkit')
}

/**
* Detects if the browser is Gecko-based
* @returns {boolean}
*/
get isGecko () {
if (!this.#cachedResults.has('isGecko')) {
this.#cachedResults.set('isGecko', this.#userAgent.includes('Gecko/'))
}
return this.#cachedResults.get('isGecko')
}

/**
* Detects if the browser is Chrome
* @returns {boolean}
*/
get isChrome () {
if (!this.#cachedResults.has('isChrome')) {
this.#cachedResults.set('isChrome', this.#userAgent.includes('Chrome/'))
}
return this.#cachedResults.get('isChrome')
}

/**
* Detects if the platform is macOS
* @returns {boolean}
*/
get isMac () {
if (!this.#cachedResults.has('isMac')) {
this.#cachedResults.set('isMac', this.#userAgent.includes('Macintosh'))
}
return this.#cachedResults.get('isMac')
}

/**
* Tests if the browser supports accurate text character positioning
* @returns {boolean}
*/
get supportsGoodTextCharPos () {
if (!this.#cachedResults.has('supportsGoodTextCharPos')) {
this.#cachedResults.set('supportsGoodTextCharPos', this.#testTextCharPos())
}
return this.#cachedResults.get('supportsGoodTextCharPos')
}
}())

// Public API
/**
* Private method to test text character positioning support
* @returns {boolean}
*/
#testTextCharPos () {
const svgroot = document.createElementNS(NSSVG, 'svg')
const svgContent = document.createElementNS(NSSVG, 'svg')
document.documentElement.append(svgroot)
svgContent.setAttribute('x', 5)
svgroot.append(svgContent)
const text = document.createElementNS(NSSVG, 'text')
text.textContent = 'a'
svgContent.append(text)

try {
const pos = text.getStartPositionOfChar(0).x
return pos === 0
} catch (err) {
return false
} finally {
svgroot.remove()
}
}
}

// Create singleton instance
const browser = new BrowserDetector()

// Export as functions for backward compatibility
/**
* @function module:browser.isWebkit
* @returns {boolean}
*/
export const isWebkit = () => isWebkit_
*/
export const isWebkit = () => browser.isWebkit

/**
* @function module:browser.isGecko
* @returns {boolean}
*/
export const isGecko = () => isGecko_
*/
export const isGecko = () => browser.isGecko

/**
* @function module:browser.isChrome
* @returns {boolean}
*/
export const isChrome = () => isChrome_
*/
export const isChrome = () => browser.isChrome

/**
* @function module:browser.isMac
* @returns {boolean}
*/
export const isMac = () => isMac_
*/
export const isMac = () => browser.isMac

/**
* @function module:browser.supportsGoodTextCharPos
* @returns {boolean}
*/
export const supportsGoodTextCharPos = () => supportsGoodTextCharPos_
*/
export const supportsGoodTextCharPos = () => browser.supportsGoodTextCharPos

// Export browser instance for direct access
export default browser
151 changes: 151 additions & 0 deletions packages/svgcanvas/common/logger.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* Centralized logging utility for SVGCanvas.
* Provides configurable log levels and the ability to disable logging in production.
* @module logger
* @license MIT
*/

/**
* Log levels in order of severity
* @enum {number}
*/
export const LogLevel = {
NONE: 0,
ERROR: 1,
WARN: 2,
INFO: 3,
DEBUG: 4
}

/**
* Logger configuration
* @type {Object}
*/
const config = {
currentLevel: LogLevel.WARN,
enabled: true,
prefix: '[SVGCanvas]'
}

/**
* Set the logging level
* @param {LogLevel} level - The log level to set
* @returns {void}
*/
export const setLogLevel = (level) => {
if (Object.values(LogLevel).includes(level)) {
config.currentLevel = level
}
}

/**
* Enable or disable logging
* @param {boolean} enabled - Whether logging should be enabled
* @returns {void}
*/
export const setLoggingEnabled = (enabled) => {
config.enabled = Boolean(enabled)
}

/**
* Set the log prefix
* @param {string} prefix - The prefix to use for log messages
* @returns {void}
*/
export const setLogPrefix = (prefix) => {
config.prefix = String(prefix)
}

/**
* Format a log message with prefix and context
* @param {string} message - The log message
* @param {string} [context=''] - Optional context information
* @returns {string} Formatted message
*/
const formatMessage = (message, context = '') => {
const contextStr = context ? ` [${context}]` : ''
return `${config.prefix}${contextStr} ${message}`
}

/**
* Log an error message
* @param {string} message - The error message
* @param {Error|any} [error] - Optional error object or additional data
* @param {string} [context=''] - Optional context (e.g., module name)
* @returns {void}
*/
export const error = (message, error, context = '') => {
if (!config.enabled || config.currentLevel < LogLevel.ERROR) return

console.error(formatMessage(message, context))
if (error) {
console.error(error)
}
}

/**
* Log a warning message
* @param {string} message - The warning message
* @param {any} [data] - Optional additional data
* @param {string} [context=''] - Optional context (e.g., module name)
* @returns {void}
*/
export const warn = (message, data, context = '') => {
if (!config.enabled || config.currentLevel < LogLevel.WARN) return

console.warn(formatMessage(message, context))
if (data !== undefined) {
console.warn(data)
}
}

/**
* Log an info message
* @param {string} message - The info message
* @param {any} [data] - Optional additional data
* @param {string} [context=''] - Optional context (e.g., module name)
* @returns {void}
*/
export const info = (message, data, context = '') => {
if (!config.enabled || config.currentLevel < LogLevel.INFO) return

console.info(formatMessage(message, context))
if (data !== undefined) {
console.info(data)
}
}

/**
* Log a debug message
* @param {string} message - The debug message
* @param {any} [data] - Optional additional data
* @param {string} [context=''] - Optional context (e.g., module name)
* @returns {void}
*/
export const debug = (message, data, context = '') => {
if (!config.enabled || config.currentLevel < LogLevel.DEBUG) return

console.debug(formatMessage(message, context))
if (data !== undefined) {
console.debug(data)
}
}

/**
* Get current logger configuration
* @returns {Object} Current configuration
*/
export const getConfig = () => ({ ...config })

// Default export as namespace
export default {
LogLevel,
setLogLevel,
setLoggingEnabled,
setLogPrefix,
error,
warn,
info,
debug,
getConfig
}
Loading