From 4ec92b06d269c7a8367d071367cbe59c9c8fd425 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Sun, 24 May 2026 20:57:52 +0200
Subject: [PATCH 01/27] Implement coords-dom-01-f
---
GenerateReferencesCLI/cli.swift | 1 +
Source/Parser/SVG/SVGParser.swift | 8 +-
.../SVG/Scripting/SVGScriptRunner.swift | 286 ++++++++++++++++++
Tests/SVGViewTests/SVG11Tests.swift | 1 +
.../w3c/1.1F2/refs/coords-dom-01-f.ref | 50 +++
w3c-coverage.md | 12 +-
6 files changed, 351 insertions(+), 7 deletions(-)
create mode 100644 Source/Parser/SVG/Scripting/SVGScriptRunner.swift
create mode 100644 Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-01-f.ref
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index e2ea79f2..3d9a6837 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -17,6 +17,7 @@ struct cli: ParsableCommand {
"color-prop-05-t",
"coords-coord-01-t",
"coords-coord-02-t",
+ "coords-dom-01-f",
"coords-trans-01-b",
"coords-trans-02-t",
"coords-trans-03-t",
diff --git a/Source/Parser/SVG/SVGParser.swift b/Source/Parser/SVG/SVGParser.swift
index e799d44f..26a31967 100644
--- a/Source/Parser/SVG/SVGParser.swift
+++ b/Source/Parser/SVG/SVGParser.swift
@@ -32,12 +32,18 @@ public struct SVGParser {
static public func parse(xml: XMLElement?, settings: SVGSettings = .default) -> SVGNode? {
guard let xml = xml else { return nil }
- return parse(element: xml, parentContext: SVGRootContext(
+ let node = parse(element: xml, parentContext: SVGRootContext(
logger: settings.logger,
linker: settings.linker,
screen: SVGScreen.main(ppi: settings.ppi),
index: SVGIndex(element: xml),
defaultFontSize: settings.fontSize))
+
+ if let node {
+ SVGScriptRunner.executeIfNeeded(xmlRoot: xml, nodeRoot: node, logger: settings.logger)
+ }
+
+ return node
}
@available(*, deprecated, message: "Use parse(contentsOf:) function instead")
diff --git a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
new file mode 100644
index 00000000..945b9789
--- /dev/null
+++ b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
@@ -0,0 +1,286 @@
+import Foundation
+
+#if canImport(JavaScriptCore)
+import JavaScriptCore
+
+@objc protocol SVGJSDocumentExports: JSExport {
+ func getElementById(_ id: String) -> SVGJSElement?
+}
+
+@objc protocol SVGJSElementExports: JSExport {
+ var transform: SVGJSTransformListContainer? { get }
+ func setAttribute(_ name: String, _ value: String)
+}
+
+@objc protocol SVGJSTransformListContainerExports: JSExport {
+ var baseVal: SVGJSTransformList { get }
+}
+
+@objc protocol SVGJSTransformListExports: JSExport {
+ func getItem(_ index: Int) -> SVGJSTransform?
+}
+
+@objc protocol SVGJSTransformExports: JSExport {
+ var type: Int { get }
+ var matrix: SVGJSMatrix { get }
+ func setTranslate(_ tx: Double, _ ty: Double)
+ func setScale(_ sx: Double, _ sy: Double)
+ func setRotate(_ angle: Double, _ cx: Double, _ cy: Double)
+}
+
+@objc protocol SVGJSMatrixExports: JSExport {
+ var a: Double { get set }
+ var b: Double { get set }
+ var c: Double { get set }
+ var d: Double { get set }
+ var e: Double { get set }
+ var f: Double { get set }
+}
+
+@objcMembers
+final class SVGJSDocument: NSObject, SVGJSDocumentExports {
+ private let root: SVGNode
+
+ init(root: SVGNode) {
+ self.root = root
+ }
+
+ func getElementById(_ id: String) -> SVGJSElement? {
+ guard let node = root.getNode(byId: id) else {
+ return nil
+ }
+ return SVGJSElement(node: node)
+ }
+}
+
+@objcMembers
+final class SVGJSElement: NSObject, SVGJSElementExports {
+ private let node: SVGNode
+
+ init(node: SVGNode) {
+ self.node = node
+ }
+
+ var transform: SVGJSTransformListContainer? {
+ SVGJSTransformListContainer(node: node)
+ }
+
+ func setAttribute(_ name: String, _ value: String) {
+ if name == "fill", let shape = node as? SVGShape {
+ shape.fill = SVGHelper.parseColor(value, [:])
+ }
+ }
+}
+
+@objcMembers
+final class SVGJSTransformListContainer: NSObject, SVGJSTransformListContainerExports {
+ let baseVal: SVGJSTransformList
+
+ init(node: SVGNode) {
+ self.baseVal = SVGJSTransformList(node: node)
+ }
+}
+
+@objcMembers
+final class SVGJSTransformList: NSObject, SVGJSTransformListExports {
+ private let transform: SVGJSTransform
+
+ init(node: SVGNode) {
+ self.transform = SVGJSTransform(node: node)
+ }
+
+ func getItem(_ index: Int) -> SVGJSTransform? {
+ index == 0 ? transform : nil
+ }
+}
+
+@objcMembers
+final class SVGJSTransform: NSObject, SVGJSTransformExports {
+ static let svgTransformUnknown = 0
+ static let svgTransformMatrix = 1
+ static let svgTransformTranslate = 2
+ static let svgTransformScale = 3
+ static let svgTransformRotate = 4
+ static let svgTransformSkewX = 5
+ static let svgTransformSkewY = 6
+
+ private weak var node: SVGNode?
+ private(set) var matrixModel: SVGJSMatrix!
+ var components: (a: Double, b: Double, c: Double, d: Double, e: Double, f: Double)
+ var type: Int = svgTransformMatrix
+
+ var matrix: SVGJSMatrix {
+ matrixModel
+ }
+
+ init(node: SVGNode) {
+ self.node = node
+ let t = node.transform
+ self.components = (
+ a: Double(t.a),
+ b: Double(t.b),
+ c: Double(t.c),
+ d: Double(t.d),
+ e: Double(t.tx),
+ f: Double(t.ty)
+ )
+ super.init()
+ self.matrixModel = SVGJSMatrix(owner: self)
+ }
+
+ func setTranslate(_ tx: Double, _ ty: Double) {
+ type = Self.svgTransformTranslate
+ components = (a: 1, b: 0, c: 0, d: 1, e: tx, f: ty)
+ applyToNode()
+ }
+
+ func setScale(_ sx: Double, _ sy: Double) {
+ type = Self.svgTransformScale
+ components = (a: sx, b: 0, c: 0, d: sy, e: 0, f: 0)
+ applyToNode()
+ }
+
+ func setRotate(_ angle: Double, _ cx: Double, _ cy: Double) {
+ type = Self.svgTransformRotate
+ let rotation = CGAffineTransform.identity
+ .translatedBy(x: cx, y: cy)
+ .rotated(by: CGFloat(angle * .pi / 180.0))
+ .translatedBy(x: -cx, y: -cy)
+ components = (
+ a: Double(rotation.a),
+ b: Double(rotation.b),
+ c: Double(rotation.c),
+ d: Double(rotation.d),
+ e: Double(rotation.tx),
+ f: Double(rotation.ty)
+ )
+ applyToNode()
+ }
+
+ func setComponent(_ keyPath: WritableKeyPath<(a: Double, b: Double, c: Double, d: Double, e: Double, f: Double), Double>, _ value: Double) {
+ type = Self.svgTransformMatrix
+ components[keyPath: keyPath] = value
+ applyToNode()
+ }
+
+ func component(_ keyPath: KeyPath<(a: Double, b: Double, c: Double, d: Double, e: Double, f: Double), Double>) -> Double {
+ components[keyPath: keyPath]
+ }
+
+ private func applyToNode() {
+ node?.transform = CGAffineTransform(
+ a: CGFloat(components.a),
+ b: CGFloat(components.b),
+ c: CGFloat(components.c),
+ d: CGFloat(components.d),
+ tx: CGFloat(components.e),
+ ty: CGFloat(components.f)
+ )
+ }
+}
+
+@objcMembers
+final class SVGJSMatrix: NSObject, SVGJSMatrixExports {
+ private weak var owner: SVGJSTransform?
+
+ init(owner: SVGJSTransform) {
+ self.owner = owner
+ }
+
+ var a: Double {
+ get { owner?.component(\.a) ?? 1 }
+ set { owner?.setComponent(\.a, newValue) }
+ }
+
+ var b: Double {
+ get { owner?.component(\.b) ?? 0 }
+ set { owner?.setComponent(\.b, newValue) }
+ }
+
+ var c: Double {
+ get { owner?.component(\.c) ?? 0 }
+ set { owner?.setComponent(\.c, newValue) }
+ }
+
+ var d: Double {
+ get { owner?.component(\.d) ?? 1 }
+ set { owner?.setComponent(\.d, newValue) }
+ }
+
+ var e: Double {
+ get { owner?.component(\.e) ?? 0 }
+ set { owner?.setComponent(\.e, newValue) }
+ }
+
+ var f: Double {
+ get { owner?.component(\.f) ?? 0 }
+ set { owner?.setComponent(\.f, newValue) }
+ }
+}
+#endif
+
+enum SVGScriptRunner {
+
+ static func executeIfNeeded(xmlRoot: XMLElement, nodeRoot: SVGNode, logger: SVGLogger) {
+#if canImport(JavaScriptCore)
+ let scripts = collectScripts(from: xmlRoot)
+ guard !scripts.isEmpty else { return }
+
+ guard let context = JSContext() else {
+ logger.log(message: "Failed to create JavaScript context")
+ return
+ }
+
+ let document = SVGJSDocument(root: nodeRoot)
+ context.setObject(document, forKeyedSubscript: "document" as NSString)
+
+ let constants: [String: Int] = [
+ "SVG_TRANSFORM_UNKNOWN": SVGJSTransform.svgTransformUnknown,
+ "SVG_TRANSFORM_MATRIX": SVGJSTransform.svgTransformMatrix,
+ "SVG_TRANSFORM_TRANSLATE": SVGJSTransform.svgTransformTranslate,
+ "SVG_TRANSFORM_SCALE": SVGJSTransform.svgTransformScale,
+ "SVG_TRANSFORM_ROTATE": SVGJSTransform.svgTransformRotate,
+ "SVG_TRANSFORM_SKEWX": SVGJSTransform.svgTransformSkewX,
+ "SVG_TRANSFORM_SKEWY": SVGJSTransform.svgTransformSkewY,
+ ]
+ context.setObject(constants, forKeyedSubscript: "SVGTransform" as NSString)
+
+ context.exceptionHandler = { _, exception in
+ if let exception {
+ logger.log(message: "Script error: \(exception)")
+ }
+ }
+
+ for script in scripts {
+ _ = context.evaluateScript(script)
+ }
+#else
+ _ = xmlRoot
+ _ = nodeRoot
+ _ = logger
+#endif
+ }
+
+ private static func collectScripts(from root: XMLElement) -> [String] {
+ var result: [String] = []
+
+ func walk(_ element: XMLElement) {
+ if element.name == "script" {
+ let script = element.contents
+ .compactMap { ($0 as? XMLText)?.text }
+ .joined()
+ .trimmingCharacters(in: .whitespacesAndNewlines)
+ if !script.isEmpty {
+ result.append(script)
+ }
+ }
+
+ for child in element.contents.compactMap({ $0 as? XMLElement }) {
+ walk(child)
+ }
+ }
+
+ walk(root)
+ return result
+ }
+}
\ No newline at end of file
diff --git a/Tests/SVGViewTests/SVG11Tests.swift b/Tests/SVGViewTests/SVG11Tests.swift
index ea7d30bd..15075acf 100644
--- a/Tests/SVGViewTests/SVG11Tests.swift
+++ b/Tests/SVGViewTests/SVG11Tests.swift
@@ -21,6 +21,7 @@ struct SVG11Tests {
@Test func coordsCoord01T() async throws { try await compareToReference("coords-coord-01-t") }
@Test func coordsCoord02T() async throws { try await compareToReference("coords-coord-02-t") }
+ @Test func coordsDom01F() async throws { try await compareToReference("coords-dom-01-f") }
@Test func coordsTrans01B() async throws { try await compareToReference("coords-trans-01-b") }
@Test func coordsTrans02T() async throws { try await compareToReference("coords-trans-02-t") }
@Test func coordsTrans03T() async throws { try await compareToReference("coords-trans-03-t") }
diff --git a/Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-01-f.ref b/Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-01-f.ref
new file mode 100644
index 00000000..cca6f0b7
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-01-f.ref
@@ -0,0 +1,50 @@
+SVGViewport {
+ id: "svg-root",
+ viewBox: { width: 480, height: 360 },
+ scaling: "none",
+ contents: [
+ SVGDefs { },
+ SVGGroup {
+ id: "test-body-content",
+ contents: [
+ SVGGroup {
+ transform: [1, 0, 0, 1, 240, 180],
+ contents: [
+ SVGGroup {
+ id: "reference",
+ contents: [
+ SVGCircle { r: 40, fill: "red" }
+ ]
+ },
+ SVGGroup {
+ id: "g",
+ transform: [0, 1, -1, 0, 0, 0],
+ contents: [
+ SVGCircle { id: "c", r: 41, fill: "lime" }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ SVGGroup {
+ contents: [
+ SVGText {
+ id: "revision",
+ text: "$Revision: 1.8 $",
+ font: { name: "SVGFreeSansASCII,sans-serif", size: 32 },
+ fill: "black",
+ transform: [1, 0, 0, 1, 10, 340]
+ }
+ ]
+ },
+ SVGRect {
+ id: "test-frame",
+ x: 1,
+ y: 1,
+ width: 478,
+ height: 358,
+ stroke: { fill: "black" }
+ }
+ ]
+}
diff --git a/w3c-coverage.md b/w3c-coverage.md
index a1557ce2..28ddde8d 100644
--- a/w3c-coverage.md
+++ b/w3c-coverage.md
@@ -2,11 +2,11 @@
This page is automatically generated and shows actual coverage of the [W3C SVG Test Suite](https://github.com/web-platform-tests/wpt/tree/master/svg) by this `SVGView` implementation. Currently there are two standards supported: [SVG 1.1 (Second Edition)](https://www.w3.org/TR/SVG11/) and [SVG Tiny 1.2](https://www.w3.org/TR/SVGTiny12/).
- * [SVG 1.1 (Second Edition)](#svg-11-second-edition): `20.3%`
+ * [SVG 1.1 (Second Edition)](#svg-11-second-edition): `20.4%`
* [Animate](#animate-1): `0.0%`
* [Color](#color-1): `83.3%`
* [Conform](#conform-1): `0.0%`
- * [Coords](#coords-1): `71.8%`
+ * [Coords](#coords-1): `75.0%`
* [Extend](#extend-1): `0.0%`
* [Filters](#filters-1): `0.0%`
* [Fonts](#fonts-1): `0.0%`
@@ -51,7 +51,7 @@ This page is automatically generated and shows actual coverage of the [W3C SVG T
## [SVG 1.1 (Second Edition)](https://www.w3.org/TR/SVG11/)
-`20.3%` of tests covered (106/522). They can be splitted into following categories:
+`20.4%` of tests covered (107/522). They can be splitted into following categories:
### [Animate](https://www.w3.org/TR/SVG11/animate.html): `0.0%`
@@ -167,16 +167,16 @@ This page is automatically generated and shows actual coverage of the [W3C SVG T
|❌|[conform-viewers-03-f](Tests/SVGViewTests/w3c/1.1F2/svg/conform-viewers-03-f.svg)|
-### [Coords](https://www.w3.org/TR/SVG11/coords.html): `71.8%`
+### [Coords](https://www.w3.org/TR/SVG11/coords.html): `75.0%`
- (23/32) tests covered...
+ (24/32) tests covered...
|Status | Name|
|------|-------|
|✅|[coords-coord-01-t](Tests/SVGViewTests/w3c/1.1F2/svg/coords-coord-01-t.svg)|
|✅|[coords-coord-02-t](Tests/SVGViewTests/w3c/1.1F2/svg/coords-coord-02-t.svg)|
-|❌|[coords-dom-01-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-01-f.svg)|
+|✅|[coords-dom-01-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-01-f.svg)|
|❌|[coords-dom-02-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-02-f.svg)|
|❌|[coords-dom-03-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-03-f.svg)|
|❌|[coords-dom-04-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-04-f.svg)|
From e67b80df2cdaebaba3527aa42fd6e9c66480fa78 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Sun, 24 May 2026 21:08:26 +0200
Subject: [PATCH 02/27] Implement coords-dom-02-f
---
GenerateReferencesCLI/cli.swift | 1 +
Source/Model/Shapes/SVGCircle.swift | 2 +-
Tests/SVGViewTests/SVG11Tests.swift | 1 +
.../w3c/1.1F2/refs/coords-dom-02-f.ref | 50 +++++++++++++++++++
w3c-coverage.md | 12 ++---
5 files changed, 59 insertions(+), 7 deletions(-)
create mode 100644 Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-02-f.ref
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 3d9a6837..d2dc288e 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -18,6 +18,7 @@ struct cli: ParsableCommand {
"coords-coord-01-t",
"coords-coord-02-t",
"coords-dom-01-f",
+ "coords-dom-02-f",
"coords-trans-01-b",
"coords-trans-02-t",
"coords-trans-03-t",
diff --git a/Source/Model/Shapes/SVGCircle.swift b/Source/Model/Shapes/SVGCircle.swift
index 49f8e198..91fe1f1c 100644
--- a/Source/Model/Shapes/SVGCircle.swift
+++ b/Source/Model/Shapes/SVGCircle.swift
@@ -49,9 +49,9 @@ struct SVGCircleView: View {
public var body: some View {
Circle()
.applySVGStroke(stroke: model.stroke)
- .applyShapeAttributes(model: model)
.frame(width: 2 * model.r, height: 2 * model.r)
.position(x: model.cx, y: model.cy)
+ .applyShapeAttributes(model: model)
}
}
#endif
diff --git a/Tests/SVGViewTests/SVG11Tests.swift b/Tests/SVGViewTests/SVG11Tests.swift
index 15075acf..ecb77d5c 100644
--- a/Tests/SVGViewTests/SVG11Tests.swift
+++ b/Tests/SVGViewTests/SVG11Tests.swift
@@ -22,6 +22,7 @@ struct SVG11Tests {
@Test func coordsCoord01T() async throws { try await compareToReference("coords-coord-01-t") }
@Test func coordsCoord02T() async throws { try await compareToReference("coords-coord-02-t") }
@Test func coordsDom01F() async throws { try await compareToReference("coords-dom-01-f") }
+ @Test func coordsDom02F() async throws { try await compareToReference("coords-dom-02-f") }
@Test func coordsTrans01B() async throws { try await compareToReference("coords-trans-01-b") }
@Test func coordsTrans02T() async throws { try await compareToReference("coords-trans-02-t") }
@Test func coordsTrans03T() async throws { try await compareToReference("coords-trans-03-t") }
diff --git a/Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-02-f.ref b/Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-02-f.ref
new file mode 100644
index 00000000..9492479b
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-02-f.ref
@@ -0,0 +1,50 @@
+SVGViewport {
+ id: "svg-root",
+ viewBox: { width: 480, height: 360 },
+ scaling: "none",
+ contents: [
+ SVGDefs { },
+ SVGGroup {
+ id: "test-body-content",
+ contents: [
+ SVGGroup {
+ transform: [1, 0, 0, 1, 220, 160],
+ contents: [
+ SVGGroup {
+ id: "reference",
+ contents: [
+ SVGCircle { r: 41, fill: "red", transform: [2, 0, 0, 1, 20, 20] }
+ ]
+ },
+ SVGGroup {
+ id: "g",
+ transform: [2, 0, 0, 1, 20, 20],
+ contents: [
+ SVGCircle { id: "c", r: 41, fill: "lime" }
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ SVGGroup {
+ contents: [
+ SVGText {
+ id: "revision",
+ text: "$Revision: 1.9 $",
+ font: { name: "SVGFreeSansASCII,sans-serif", size: 32 },
+ fill: "black",
+ transform: [1, 0, 0, 1, 10, 340]
+ }
+ ]
+ },
+ SVGRect {
+ id: "test-frame",
+ x: 1,
+ y: 1,
+ width: 478,
+ height: 358,
+ stroke: { fill: "black" }
+ }
+ ]
+}
diff --git a/w3c-coverage.md b/w3c-coverage.md
index 28ddde8d..7d6a6ad8 100644
--- a/w3c-coverage.md
+++ b/w3c-coverage.md
@@ -2,11 +2,11 @@
This page is automatically generated and shows actual coverage of the [W3C SVG Test Suite](https://github.com/web-platform-tests/wpt/tree/master/svg) by this `SVGView` implementation. Currently there are two standards supported: [SVG 1.1 (Second Edition)](https://www.w3.org/TR/SVG11/) and [SVG Tiny 1.2](https://www.w3.org/TR/SVGTiny12/).
- * [SVG 1.1 (Second Edition)](#svg-11-second-edition): `20.4%`
+ * [SVG 1.1 (Second Edition)](#svg-11-second-edition): `20.6%`
* [Animate](#animate-1): `0.0%`
* [Color](#color-1): `83.3%`
* [Conform](#conform-1): `0.0%`
- * [Coords](#coords-1): `75.0%`
+ * [Coords](#coords-1): `78.1%`
* [Extend](#extend-1): `0.0%`
* [Filters](#filters-1): `0.0%`
* [Fonts](#fonts-1): `0.0%`
@@ -51,7 +51,7 @@ This page is automatically generated and shows actual coverage of the [W3C SVG T
## [SVG 1.1 (Second Edition)](https://www.w3.org/TR/SVG11/)
-`20.4%` of tests covered (107/522). They can be splitted into following categories:
+`20.6%` of tests covered (108/522). They can be splitted into following categories:
### [Animate](https://www.w3.org/TR/SVG11/animate.html): `0.0%`
@@ -167,17 +167,17 @@ This page is automatically generated and shows actual coverage of the [W3C SVG T
|❌|[conform-viewers-03-f](Tests/SVGViewTests/w3c/1.1F2/svg/conform-viewers-03-f.svg)|
-### [Coords](https://www.w3.org/TR/SVG11/coords.html): `75.0%`
+### [Coords](https://www.w3.org/TR/SVG11/coords.html): `78.1%`
- (24/32) tests covered...
+ (25/32) tests covered...
|Status | Name|
|------|-------|
|✅|[coords-coord-01-t](Tests/SVGViewTests/w3c/1.1F2/svg/coords-coord-01-t.svg)|
|✅|[coords-coord-02-t](Tests/SVGViewTests/w3c/1.1F2/svg/coords-coord-02-t.svg)|
|✅|[coords-dom-01-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-01-f.svg)|
-|❌|[coords-dom-02-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-02-f.svg)|
+|✅|[coords-dom-02-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-02-f.svg)|
|❌|[coords-dom-03-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-03-f.svg)|
|❌|[coords-dom-04-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-04-f.svg)|
|✅|[coords-trans-01-b](Tests/SVGViewTests/w3c/1.1F2/svg/coords-trans-01-b.svg)|
From 60cad6a4c60653f844b7bee197f1652c3b6a8666 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 09:04:35 +0200
Subject: [PATCH 03/27] Begin script support
---
GenerateReferencesCLI/cli.swift | 4 ++
Source/Model/Nodes/SVGGroup.swift | 2 +
Source/Model/Shapes/SVGRect.swift | 3 +-
.../SVG/Scripting/SVGScriptRunner.swift | 66 ++++++++++++++++++-
Source/UI/UIExtensions.swift | 3 +
Tests/SVGViewTests/BaseTestCase.swift | 11 ++--
Tests/SVGViewTests/SVGCustomTests.swift | 25 ++++++-
.../w3c/Custom/refs/script-gating-01.ref | 8 +++
.../w3c/Custom/refs/script-gating-02.ref | 8 +++
.../w3c/Custom/refs/script-gating-03.ref | 8 +++
.../w3c/Custom/refs/script-gating-04.ref | 8 +++
.../w3c/Custom/svg/script-gating-01.svg | 6 ++
.../w3c/Custom/svg/script-gating-02.svg | 6 ++
.../w3c/Custom/svg/script-gating-03.svg | 6 ++
.../w3c/Custom/svg/script-gating-04.svg | 6 ++
15 files changed, 162 insertions(+), 8 deletions(-)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-gating-01.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-gating-02.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-gating-03.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-gating-04.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-gating-01.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-gating-02.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-gating-03.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-gating-04.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index d2dc288e..1f78a2a1 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -155,6 +155,10 @@ struct cli: ParsableCommand {
"viewport-01",
"viewport-02",
"graph-01",
+ "script-gating-01",
+ "script-gating-02",
+ "script-gating-03",
+ "script-gating-04",
]
mutating func run() throws {
diff --git a/Source/Model/Nodes/SVGGroup.swift b/Source/Model/Nodes/SVGGroup.swift
index 4fef4e1b..b67c6478 100644
--- a/Source/Model/Nodes/SVGGroup.swift
+++ b/Source/Model/Nodes/SVGGroup.swift
@@ -46,9 +46,11 @@ public class SVGGroup: SVGNode {
#if !os(WASI) && !os(Linux)
override func draw(in context: CGContext) {
+ guard opaque else { return }
context.saveGState()
context.concatenate(transform)
for node in contents {
+ guard node.opaque else { continue }
node.draw(in: context)
}
context.restoreGState()
diff --git a/Source/Model/Shapes/SVGRect.swift b/Source/Model/Shapes/SVGRect.swift
index 36cc6b8c..6d59349c 100644
--- a/Source/Model/Shapes/SVGRect.swift
+++ b/Source/Model/Shapes/SVGRect.swift
@@ -50,13 +50,14 @@ public class SVGRect: SVGShape {
#if !os(WASI) && !os(Linux)
override func draw(in context: CGContext) {
+ guard opaque else { return }
guard let color = fill as? SVGColor else { return }
context.saveGState()
context.setFillColor(CGColor(
red: CGFloat(color.r) / 255,
green: CGFloat(color.g) / 255,
blue: CGFloat(color.b) / 255,
- alpha: CGFloat(color.opacity)
+ alpha: CGFloat(color.opacity * opacity)
))
context.fill(CGRect(x: x, y: y, width: width, height: height))
context.restoreGState()
diff --git a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
index 945b9789..82cc7579 100644
--- a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
+++ b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
@@ -66,8 +66,26 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
}
func setAttribute(_ name: String, _ value: String) {
- if name == "fill", let shape = node as? SVGShape {
- shape.fill = SVGHelper.parseColor(value, [:])
+ let normalizedName = name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
+ let normalizedValue = value.trimmingCharacters(in: .whitespacesAndNewlines)
+
+ switch normalizedName {
+ case "fill":
+ if let shape = node as? SVGShape {
+ shape.fill = SVGHelper.parseColor(normalizedValue, [:])
+ }
+ case "opacity":
+ if let parsedOpacity = Double(normalizedValue) {
+ node.opacity = min(max(parsedOpacity, 0), 1)
+ }
+ case "visibility":
+ node.opaque = normalizedValue.lowercased() != "hidden"
+ case "display":
+ node.opaque = normalizedValue.lowercased() != "none"
+ case "transform":
+ node.transform = SVGHelper.parseTransform(normalizedValue)
+ default:
+ break
}
}
}
@@ -221,6 +239,18 @@ final class SVGJSMatrix: NSObject, SVGJSMatrixExports {
enum SVGScriptRunner {
+ private static let supportedScriptMIMETypes: Set = [
+ "application/ecmascript",
+ "application/javascript",
+ "application/x-ecmascript",
+ "application/x-javascript",
+ "text/ecmascript",
+ "text/javascript",
+ "text/jscript",
+ ]
+
+ private static let defaultScriptMIMEType = "application/ecmascript"
+
static func executeIfNeeded(xmlRoot: XMLElement, nodeRoot: SVGNode, logger: SVGLogger) {
#if canImport(JavaScriptCore)
let scripts = collectScripts(from: xmlRoot)
@@ -264,8 +294,26 @@ enum SVGScriptRunner {
private static func collectScripts(from root: XMLElement) -> [String] {
var result: [String] = []
+ let explicitDefaultType = root.attributes["contentScriptType"]
+ let defaultScriptType: String? = {
+ if let explicitDefaultType {
+ return normalizedSupportedScriptType(explicitDefaultType)
+ }
+ return defaultScriptMIMEType
+ }()
+
func walk(_ element: XMLElement) {
if element.name == "script" {
+ let hasExplicitType = element.attributes["type"] != nil
+ let effectiveType: String? = {
+ if let scriptType = element.attributes["type"] {
+ return normalizedSupportedScriptType(scriptType)
+ }
+ return hasExplicitType ? nil : defaultScriptType
+ }()
+
+ guard effectiveType != nil else { return }
+
let script = element.contents
.compactMap { ($0 as? XMLText)?.text }
.joined()
@@ -283,4 +331,18 @@ enum SVGScriptRunner {
walk(root)
return result
}
+
+ private static func normalizedSupportedScriptType(_ rawValue: String) -> String? {
+ let trimmed = rawValue.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard !trimmed.isEmpty else { return nil }
+
+ let typeOnly = trimmed
+ .split(separator: ";", maxSplits: 1, omittingEmptySubsequences: true)
+ .first?
+ .trimmingCharacters(in: .whitespacesAndNewlines)
+ .lowercased()
+
+ guard let typeOnly else { return nil }
+ return supportedScriptMIMETypes.contains(typeOnly) ? typeOnly : nil
+ }
}
\ No newline at end of file
diff --git a/Source/UI/UIExtensions.swift b/Source/UI/UIExtensions.swift
index a35d9d21..b112bb85 100644
--- a/Source/UI/UIExtensions.swift
+++ b/Source/UI/UIExtensions.swift
@@ -57,6 +57,9 @@ extension View {
func applyNodeAttributes(model: SVGNode) -> some View {
self.opacity(model.opacity)
+ .applyIf(!model.opaque) {
+ $0.hidden()
+ }
.applyMask(mask: model.clip, absoluteNode: model)
.transformEffect(model.transform)
.applyIf(!model.gestures.isEmpty) {
diff --git a/Tests/SVGViewTests/BaseTestCase.swift b/Tests/SVGViewTests/BaseTestCase.swift
index 07362d10..78a23ebe 100644
--- a/Tests/SVGViewTests/BaseTestCase.swift
+++ b/Tests/SVGViewTests/BaseTestCase.swift
@@ -91,12 +91,15 @@ extension SVGTestHelper {
if #available(macOS 13, iOS 16, watchOS 9, *) {
let size = renderSize(for: node)
let png = await MainActor.run { () -> Data? in
- let renderer = ImageRenderer(content: SVGView(svg: node).frame(width: size.width, height: size.height))
+ let content = SVGView(svg: node)
+ .frame(width: size.width, height: size.height)
+ .background(Color.clear)
+ let renderer = ImageRenderer(content: content)
renderer.scale = 1.0
+ renderer.isOpaque = false
#if os(macOS)
- guard let nsImage = renderer.nsImage,
- let tiff = nsImage.tiffRepresentation,
- let rep = NSBitmapImageRep(data: tiff) else { return nil }
+ guard let cgImage = renderer.cgImage else { return nil }
+ let rep = NSBitmapImageRep(cgImage: cgImage)
return rep.representation(using: .png, properties: [:])
#elseif os(iOS)
return renderer.uiImage?.pngData()
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index dc454f19..db7407a7 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -10,15 +10,38 @@ struct SVGCustomTests: SVGTestHelper {
}
@Test func graph01() async throws {
+ // Baseline parsing snapshot for a large graph-like SVG fixture.
try await compareToReference("graph-01")
}
@Test func viewport01() async throws {
+ // Verifies viewport handling for the first custom viewport fixture.
try await compareToReference("viewport-01")
}
@Test func viewport02() async throws {
+ // Verifies viewport handling for the second custom viewport fixture.
try await compareToReference("viewport-02")
}
-}
\ No newline at end of file
+ @Test func scriptGating01() async throws {
+ // Unsupported script MIME type must be ignored, so the square stays blue.
+ try await compareToReference("script-gating-01")
+ }
+
+ @Test func scriptGating02() async throws {
+ // Unsupported root contentScriptType should prevent untyped script execution.
+ try await compareToReference("script-gating-02")
+ }
+
+ @Test func scriptGating03() async throws {
+ // Supported script MIME type should execute and turn the square red.
+ try await compareToReference("script-gating-03")
+ }
+
+ @Test func scriptGating04() async throws {
+ // Untyped script should execute with default script MIME handling.
+ try await compareToReference("script-gating-04")
+ }
+
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-gating-01.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-gating-01.ref
new file mode 100644
index 00000000..455f4961
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-gating-01.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "blue" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-gating-02.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-gating-02.ref
new file mode 100644
index 00000000..455f4961
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-gating-02.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "blue" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-gating-03.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-gating-03.ref
new file mode 100644
index 00000000..25151298
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-gating-03.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "red" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-gating-04.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-gating-04.ref
new file mode 100644
index 00000000..25151298
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-gating-04.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "red" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-gating-01.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-gating-01.svg
new file mode 100644
index 00000000..96f5f104
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-gating-01.svg
@@ -0,0 +1,6 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-gating-02.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-gating-02.svg
new file mode 100644
index 00000000..4b51109d
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-gating-02.svg
@@ -0,0 +1,6 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-gating-03.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-gating-03.svg
new file mode 100644
index 00000000..34b8a4ab
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-gating-03.svg
@@ -0,0 +1,6 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-gating-04.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-gating-04.svg
new file mode 100644
index 00000000..67d9b013
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-gating-04.svg
@@ -0,0 +1,6 @@
+
From 48f5ba2cb5de6cf4b48c2bee3f0286445d054709 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 09:11:04 +0200
Subject: [PATCH 04/27] Implement stroke and fill support in scripting engine
---
GenerateReferencesCLI/cli.swift | 2 +
.../SVG/Scripting/SVGScriptRunner.swift | 74 +++++++++++++++++++
Tests/SVGViewTests/SVGCustomTests.swift | 10 +++
.../w3c/Custom/refs/script-stroke-01.ref | 15 ++++
.../w3c/Custom/refs/script-stroke-02.ref | 15 ++++
.../w3c/Custom/svg/script-stroke-01.svg | 9 +++
.../w3c/Custom/svg/script-stroke-02.svg | 9 +++
7 files changed, 134 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-stroke-01.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-stroke-02.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-stroke-01.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-stroke-02.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 1f78a2a1..c03bf208 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -159,6 +159,8 @@ struct cli: ParsableCommand {
"script-gating-02",
"script-gating-03",
"script-gating-04",
+ "script-stroke-01",
+ "script-stroke-02",
]
mutating func run() throws {
diff --git a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
index 82cc7579..33bbb875 100644
--- a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
+++ b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
@@ -84,10 +84,84 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
node.opaque = normalizedValue.lowercased() != "none"
case "transform":
node.transform = SVGHelper.parseTransform(normalizedValue)
+ case "stroke":
+ setStroke(normalizedValue)
+ case "stroke-width":
+ setStrokeWidth(normalizedValue)
+ case "fill-opacity":
+ setFillOpacity(normalizedValue)
+ case "stroke-opacity":
+ setStrokeOpacity(normalizedValue)
default:
break
}
}
+
+ private func setStroke(_ value: String) {
+ guard let shape = node as? SVGShape else { return }
+
+ if value.lowercased() == "none" {
+ shape.stroke = nil
+ return
+ }
+
+ guard let color = SVGHelper.parseColor(value, [:]) else { return }
+ let current = shape.stroke
+ shape.stroke = SVGStroke(
+ fill: color,
+ width: current?.width ?? 1,
+ cap: current?.cap ?? .butt,
+ join: current?.join ?? .miter,
+ miterLimit: current?.miterLimit ?? 4,
+ dashes: current?.dashes ?? [],
+ offset: current?.offset ?? 0
+ )
+ }
+
+ private func setStrokeWidth(_ value: String) {
+ guard let shape = node as? SVGShape,
+ let width = SVGHelper.doubleFromString(value)
+ else { return }
+
+ let current = shape.stroke
+ shape.stroke = SVGStroke(
+ fill: current?.fill ?? SVGColor.black,
+ width: CGFloat(width),
+ cap: current?.cap ?? .butt,
+ join: current?.join ?? .miter,
+ miterLimit: current?.miterLimit ?? 4,
+ dashes: current?.dashes ?? [],
+ offset: current?.offset ?? 0
+ )
+ }
+
+ private func setFillOpacity(_ value: String) {
+ guard let shape = node as? SVGShape,
+ let opacity = SVGHelper.doubleFromString(value),
+ let fill = shape.fill
+ else { return }
+
+ let clamped = min(max(opacity, 0), 1)
+ shape.fill = fill.opacity(clamped)
+ }
+
+ private func setStrokeOpacity(_ value: String) {
+ guard let shape = node as? SVGShape,
+ let opacity = SVGHelper.doubleFromString(value),
+ let current = shape.stroke
+ else { return }
+
+ let clamped = min(max(opacity, 0), 1)
+ shape.stroke = SVGStroke(
+ fill: current.fill.opacity(clamped),
+ width: current.width,
+ cap: current.cap,
+ join: current.join,
+ miterLimit: current.miterLimit,
+ dashes: current.dashes,
+ offset: current.offset
+ )
+ }
}
@objcMembers
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index db7407a7..0471e827 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -44,4 +44,14 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-gating-04")
}
+ @Test func scriptStroke01() async throws {
+ // Unsupported script MIME type must not mutate stroke properties.
+ try await compareToReference("script-stroke-01")
+ }
+
+ @Test func scriptStroke02() async throws {
+ // Supported script MIME type should mutate stroke color/width/opacity.
+ try await compareToReference("script-stroke-02")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-01.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-01.ref
new file mode 100644
index 00000000..814b559b
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-01.ref
@@ -0,0 +1,15 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect {
+ id: "target",
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 100,
+ stroke: { fill: "blue", width: 2 }
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-02.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-02.ref
new file mode 100644
index 00000000..e8b7f6fd
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-02.ref
@@ -0,0 +1,15 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect {
+ id: "target",
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 100,
+ stroke: { fill: "50% red", width: 8 }
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-01.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-01.svg
new file mode 100644
index 00000000..e2499646
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-01.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-02.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-02.svg
new file mode 100644
index 00000000..212e1d53
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-02.svg
@@ -0,0 +1,9 @@
+
From 6b3046bb0847bf5ea126b68575ed8fc57d5fb80b Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 09:13:11 +0200
Subject: [PATCH 05/27] Implement stroke-dasharray and stroke-dashoffset in
script runner
---
GenerateReferencesCLI/cli.swift | 2 +
.../SVG/Scripting/SVGScriptRunner.swift | 49 +++++++++++++++++++
Tests/SVGViewTests/SVGCustomTests.swift | 10 ++++
.../w3c/Custom/refs/script-stroke-03.ref | 15 ++++++
.../w3c/Custom/refs/script-stroke-04.ref | 15 ++++++
.../w3c/Custom/svg/script-stroke-03.svg | 8 +++
.../w3c/Custom/svg/script-stroke-04.svg | 8 +++
7 files changed, 107 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-stroke-03.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-stroke-04.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-stroke-03.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-stroke-04.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index c03bf208..ecc41168 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -161,6 +161,8 @@ struct cli: ParsableCommand {
"script-gating-04",
"script-stroke-01",
"script-stroke-02",
+ "script-stroke-03",
+ "script-stroke-04",
]
mutating func run() throws {
diff --git a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
index 33bbb875..1f0006b3 100644
--- a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
+++ b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
@@ -92,6 +92,10 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
setFillOpacity(normalizedValue)
case "stroke-opacity":
setStrokeOpacity(normalizedValue)
+ case "stroke-dasharray":
+ setStrokeDashArray(normalizedValue)
+ case "stroke-dashoffset":
+ setStrokeDashOffset(normalizedValue)
default:
break
}
@@ -162,6 +166,51 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
offset: current.offset
)
}
+
+ private func setStrokeDashArray(_ value: String) {
+ guard let shape = node as? SVGShape,
+ let current = shape.stroke
+ else { return }
+
+ let dashes: [CGFloat]
+ if value.lowercased() == "none" {
+ dashes = []
+ } else {
+ let parts = value.components(separatedBy: CharacterSet(charactersIn: " ,"))
+ .filter { !$0.isEmpty }
+ dashes = parts.compactMap { token in
+ guard let parsed = SVGHelper.doubleFromString(token) else { return nil }
+ return CGFloat(parsed)
+ }
+ }
+
+ shape.stroke = SVGStroke(
+ fill: current.fill,
+ width: current.width,
+ cap: current.cap,
+ join: current.join,
+ miterLimit: current.miterLimit,
+ dashes: dashes,
+ offset: current.offset
+ )
+ }
+
+ private func setStrokeDashOffset(_ value: String) {
+ guard let shape = node as? SVGShape,
+ let current = shape.stroke,
+ let offset = SVGHelper.doubleFromString(value)
+ else { return }
+
+ shape.stroke = SVGStroke(
+ fill: current.fill,
+ width: current.width,
+ cap: current.cap,
+ join: current.join,
+ miterLimit: current.miterLimit,
+ dashes: current.dashes,
+ offset: CGFloat(offset)
+ )
+ }
}
@objcMembers
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index 0471e827..cabe743a 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -54,4 +54,14 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-stroke-02")
}
+ @Test func scriptStroke03() async throws {
+ // Unsupported script MIME type must not mutate stroke dash properties.
+ try await compareToReference("script-stroke-03")
+ }
+
+ @Test func scriptStroke04() async throws {
+ // Supported script MIME type should mutate stroke dasharray/dashoffset.
+ try await compareToReference("script-stroke-04")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-03.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-03.ref
new file mode 100644
index 00000000..814b559b
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-03.ref
@@ -0,0 +1,15 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect {
+ id: "target",
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 100,
+ stroke: { fill: "blue", width: 2 }
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-04.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-04.ref
new file mode 100644
index 00000000..9954b2ae
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-04.ref
@@ -0,0 +1,15 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect {
+ id: "target",
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 100,
+ stroke: { fill: "blue", width: 2, offset: 2, dashes: [6, 3] }
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-03.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-03.svg
new file mode 100644
index 00000000..7ae5a9c5
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-03.svg
@@ -0,0 +1,8 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-04.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-04.svg
new file mode 100644
index 00000000..cc584cce
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-04.svg
@@ -0,0 +1,8 @@
+
From 778b513f051207d9677dc3c7cef82f1ad66bb150 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 09:16:30 +0200
Subject: [PATCH 06/27] Implement linecap, linejoin and miterlimit in
ScriptRunner
---
GenerateReferencesCLI/cli.swift | 2 +
.../SVG/Scripting/SVGScriptRunner.swift | 75 +++++++++++++++++++
Tests/SVGViewTests/SVGCustomTests.swift | 10 +++
.../w3c/Custom/refs/script-stroke-05.ref | 15 ++++
.../w3c/Custom/refs/script-stroke-06.ref | 15 ++++
.../w3c/Custom/svg/script-stroke-05.svg | 9 +++
.../w3c/Custom/svg/script-stroke-06.svg | 9 +++
7 files changed, 135 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-stroke-05.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-stroke-06.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-stroke-05.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-stroke-06.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index ecc41168..9cf3a5c3 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -163,6 +163,8 @@ struct cli: ParsableCommand {
"script-stroke-02",
"script-stroke-03",
"script-stroke-04",
+ "script-stroke-05",
+ "script-stroke-06",
]
mutating func run() throws {
diff --git a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
index 1f0006b3..6fdfb9e7 100644
--- a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
+++ b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
@@ -96,6 +96,12 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
setStrokeDashArray(normalizedValue)
case "stroke-dashoffset":
setStrokeDashOffset(normalizedValue)
+ case "stroke-linecap":
+ setStrokeLineCap(normalizedValue)
+ case "stroke-linejoin":
+ setStrokeLineJoin(normalizedValue)
+ case "stroke-miterlimit":
+ setStrokeMiterLimit(normalizedValue)
default:
break
}
@@ -211,6 +217,75 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
offset: CGFloat(offset)
)
}
+
+ private func setStrokeLineCap(_ value: String) {
+ guard let shape = node as? SVGShape,
+ let current = shape.stroke
+ else { return }
+
+ let cap: CGLineCap
+ switch value.lowercased() {
+ case "round":
+ cap = .round
+ case "square":
+ cap = .square
+ default:
+ cap = .butt
+ }
+
+ shape.stroke = SVGStroke(
+ fill: current.fill,
+ width: current.width,
+ cap: cap,
+ join: current.join,
+ miterLimit: current.miterLimit,
+ dashes: current.dashes,
+ offset: current.offset
+ )
+ }
+
+ private func setStrokeLineJoin(_ value: String) {
+ guard let shape = node as? SVGShape,
+ let current = shape.stroke
+ else { return }
+
+ let join: CGLineJoin
+ switch value.lowercased() {
+ case "round":
+ join = .round
+ case "bevel":
+ join = .bevel
+ default:
+ join = .miter
+ }
+
+ shape.stroke = SVGStroke(
+ fill: current.fill,
+ width: current.width,
+ cap: current.cap,
+ join: join,
+ miterLimit: current.miterLimit,
+ dashes: current.dashes,
+ offset: current.offset
+ )
+ }
+
+ private func setStrokeMiterLimit(_ value: String) {
+ guard let shape = node as? SVGShape,
+ let current = shape.stroke,
+ let miterLimit = SVGHelper.doubleFromString(value)
+ else { return }
+
+ shape.stroke = SVGStroke(
+ fill: current.fill,
+ width: current.width,
+ cap: current.cap,
+ join: current.join,
+ miterLimit: CGFloat(miterLimit),
+ dashes: current.dashes,
+ offset: current.offset
+ )
+ }
}
@objcMembers
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index cabe743a..9ea4ca33 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -64,4 +64,14 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-stroke-04")
}
+ @Test func scriptStroke05() async throws {
+ // Unsupported script MIME type must not mutate cap/join/miterlimit.
+ try await compareToReference("script-stroke-05")
+ }
+
+ @Test func scriptStroke06() async throws {
+ // Supported script MIME type should mutate cap/join/miterlimit.
+ try await compareToReference("script-stroke-06")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-05.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-05.ref
new file mode 100644
index 00000000..ceea00ca
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-05.ref
@@ -0,0 +1,15 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect {
+ id: "target",
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 100,
+ stroke: { fill: "blue", width: 4 }
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-06.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-06.ref
new file mode 100644
index 00000000..6486cea0
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-stroke-06.ref
@@ -0,0 +1,15 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect {
+ id: "target",
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 100,
+ stroke: { fill: "blue", width: 4, cap: "round", join: "bevel", miterLimit: 7 }
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-05.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-05.svg
new file mode 100644
index 00000000..90eb866a
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-05.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-06.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-06.svg
new file mode 100644
index 00000000..7c1e0922
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-stroke-06.svg
@@ -0,0 +1,9 @@
+
From da38a27780cfd27079fa8424e4a1e035054318cf Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 09:22:19 +0200
Subject: [PATCH 07/27] Add fill-rule support in ScriptRunner
---
GenerateReferencesCLI/cli.swift | 2 ++
Source/Parser/SVG/Scripting/SVGScriptRunner.swift | 7 +++++++
Tests/SVGViewTests/SVGCustomTests.swift | 10 ++++++++++
.../w3c/Custom/refs/script-fillrule-01.ref | 12 ++++++++++++
.../w3c/Custom/refs/script-fillrule-02.ref | 13 +++++++++++++
.../w3c/Custom/svg/script-fillrule-01.svg | 6 ++++++
.../w3c/Custom/svg/script-fillrule-02.svg | 6 ++++++
7 files changed, 56 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-fillrule-01.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-fillrule-02.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-fillrule-01.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-fillrule-02.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 9cf3a5c3..4d7c3b8a 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -165,6 +165,8 @@ struct cli: ParsableCommand {
"script-stroke-04",
"script-stroke-05",
"script-stroke-06",
+ "script-fillrule-01",
+ "script-fillrule-02",
]
mutating func run() throws {
diff --git a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
index 6fdfb9e7..e3a90281 100644
--- a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
+++ b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
@@ -102,6 +102,8 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
setStrokeLineJoin(normalizedValue)
case "stroke-miterlimit":
setStrokeMiterLimit(normalizedValue)
+ case "fill-rule":
+ setFillRule(normalizedValue)
default:
break
}
@@ -286,6 +288,11 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
offset: current.offset
)
}
+
+ private func setFillRule(_ value: String) {
+ guard let path = node as? SVGPath else { return }
+ path.fillRule = value.lowercased() == "evenodd" ? .evenOdd : .winding
+ }
}
@objcMembers
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index 9ea4ca33..d218820d 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -74,4 +74,14 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-stroke-06")
}
+ @Test func scriptFillRule01() async throws {
+ // Unsupported script MIME type must not mutate path fill-rule.
+ try await compareToReference("script-fillrule-01")
+ }
+
+ @Test func scriptFillRule02() async throws {
+ // Supported script MIME type should mutate path fill-rule.
+ try await compareToReference("script-fillrule-02")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-fillrule-01.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-fillrule-01.ref
new file mode 100644
index 00000000..8ef13ed7
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-fillrule-01.ref
@@ -0,0 +1,12 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGPath {
+ id: "target",
+ path: "M10,10 H110 V110 H10 z M40,40 H80 V80 H40 z",
+ fill: "blue"
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-fillrule-02.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-fillrule-02.ref
new file mode 100644
index 00000000..fdbe3c04
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-fillrule-02.ref
@@ -0,0 +1,13 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGPath {
+ id: "target",
+ path: "M10,10 H110 V110 H10 z M40,40 H80 V80 H40 z",
+ fillRule: "evenodd",
+ fill: "blue"
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-fillrule-01.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-fillrule-01.svg
new file mode 100644
index 00000000..fa987354
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-fillrule-01.svg
@@ -0,0 +1,6 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-fillrule-02.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-fillrule-02.svg
new file mode 100644
index 00000000..86fe0c2e
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-fillrule-02.svg
@@ -0,0 +1,6 @@
+
From 04484930a3c16cbe2c0bfc2ab3b966d6c2584b21 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 09:26:16 +0200
Subject: [PATCH 08/27] Add support for currentColor in ScriptRunner
---
GenerateReferencesCLI/cli.swift | 3 +
Source/Model/Nodes/SVGNode.swift | 5 +-
Source/Model/Nodes/SVGShape.swift | 4 ++
.../SVG/Elements/SVGElementParser.swift | 3 +
.../SVG/Scripting/SVGScriptRunner.swift | 66 +++++++++++++++----
Tests/SVGViewTests/SVGCustomTests.swift | 15 +++++
.../Custom/refs/script-currentcolor-01.ref | 8 +++
.../Custom/refs/script-currentcolor-02.ref | 8 +++
.../Custom/refs/script-currentcolor-03.ref | 8 +++
.../w3c/Custom/svg/script-currentcolor-01.svg | 8 +++
.../w3c/Custom/svg/script-currentcolor-02.svg | 8 +++
.../w3c/Custom/svg/script-currentcolor-03.svg | 8 +++
12 files changed, 130 insertions(+), 14 deletions(-)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-01.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-02.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-03.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-01.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-02.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-03.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 4d7c3b8a..0d7d358b 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -167,6 +167,9 @@ struct cli: ParsableCommand {
"script-stroke-06",
"script-fillrule-01",
"script-fillrule-02",
+ "script-currentcolor-01",
+ "script-currentcolor-02",
+ "script-currentcolor-03",
]
mutating func run() throws {
diff --git a/Source/Model/Nodes/SVGNode.swift b/Source/Model/Nodes/SVGNode.swift
index f9602b4a..2edc4108 100644
--- a/Source/Model/Nodes/SVGNode.swift
+++ b/Source/Model/Nodes/SVGNode.swift
@@ -11,6 +11,7 @@ public class SVGNode: SerializableElement {
public var transform: CGAffineTransform = CGAffineTransform.identity
public var opaque: Bool
public var opacity: Double
+ public var currentColor: SVGColor?
public var clip: SVGNode?
public var mask: SVGNode?
public var id: String?
@@ -21,6 +22,7 @@ public class SVGNode: SerializableElement {
@Published public var transform: CGAffineTransform = CGAffineTransform.identity
@Published public var opaque: Bool
@Published public var opacity: Double
+ @Published public var currentColor: SVGColor?
@Published public var clip: SVGNode?
@Published public var mask: SVGNode?
@Published public var id: String?
@@ -30,10 +32,11 @@ public class SVGNode: SerializableElement {
#endif
- public init(transform: CGAffineTransform = .identity, opaque: Bool = true, opacity: Double = 1, clip: SVGNode? = nil, mask: SVGNode? = nil, id: String? = nil, markerStart: String? = nil, markerMid: String? = nil, markerEnd: String? = nil) {
+ public init(transform: CGAffineTransform = .identity, opaque: Bool = true, opacity: Double = 1, currentColor: SVGColor? = nil, clip: SVGNode? = nil, mask: SVGNode? = nil, id: String? = nil, markerStart: String? = nil, markerMid: String? = nil, markerEnd: String? = nil) {
self.transform = transform
self.opaque = opaque
self.opacity = opacity
+ self.currentColor = currentColor
self.clip = clip
self.mask = mask
self.id = id
diff --git a/Source/Model/Nodes/SVGShape.swift b/Source/Model/Nodes/SVGShape.swift
index e186de2b..5bd6f3c4 100644
--- a/Source/Model/Nodes/SVGShape.swift
+++ b/Source/Model/Nodes/SVGShape.swift
@@ -10,9 +10,13 @@ public class SVGShape: SVGNode {
#if os(WASI) || os(Linux)
public var fill: SVGPaint?
public var stroke: SVGStroke?
+ public var fillUsesCurrentColor: Bool = false
+ public var strokeUsesCurrentColor: Bool = false
#else
@Published public var fill: SVGPaint?
@Published public var stroke: SVGStroke?
+ @Published public var fillUsesCurrentColor: Bool = false
+ @Published public var strokeUsesCurrentColor: Bool = false
#endif
override func serialize(_ serializer: Serializer) {
diff --git a/Source/Parser/SVG/Elements/SVGElementParser.swift b/Source/Parser/SVG/Elements/SVGElementParser.swift
index 4b1d1b84..17999ef5 100644
--- a/Source/Parser/SVG/Elements/SVGElementParser.swift
+++ b/Source/Parser/SVG/Elements/SVGElementParser.swift
@@ -20,6 +20,9 @@ class SVGBaseElementParser: SVGElementParser {
let transform = SVGHelper.parseTransform(context.properties["transform"] ?? "")
node.transform = node.transform.concatenating(transform)
node.opacity = SVGHelper.parseOpacity(context.properties, "opacity")
+ if let colorValue = context.style("color") {
+ node.currentColor = SVGHelper.parseColor(colorValue, context.styles)
+ }
if let clipId = SVGHelper.parseUse(context.properties["clip-path"]),
let clipNode = context.index.element(by: clipId),
diff --git a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
index e3a90281..11eff615 100644
--- a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
+++ b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
@@ -71,9 +71,9 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
switch normalizedName {
case "fill":
- if let shape = node as? SVGShape {
- shape.fill = SVGHelper.parseColor(normalizedValue, [:])
- }
+ setFill(normalizedValue)
+ case "color":
+ setColor(normalizedValue)
case "opacity":
if let parsedOpacity = Double(normalizedValue) {
node.opacity = min(max(parsedOpacity, 0), 1)
@@ -109,25 +109,52 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
}
}
+ private func setColor(_ value: String) {
+ guard let color = SVGHelper.parseColor(value, [:]) else { return }
+ node.currentColor = color
+
+ guard let shape = node as? SVGShape else { return }
+ if shape.fillUsesCurrentColor {
+ shape.fill = color
+ }
+ if shape.strokeUsesCurrentColor {
+ let currentOpacity = (shape.stroke?.fill as? SVGColor)?.opacity ?? 1
+ replaceStrokeFill(for: shape, with: color.opacity(currentOpacity))
+ }
+ }
+
+ private func setFill(_ value: String) {
+ guard let shape = node as? SVGShape else { return }
+
+ if value.lowercased() == "currentcolor" {
+ shape.fillUsesCurrentColor = true
+ shape.fill = node.currentColor ?? SVGColor.black
+ return
+ }
+
+ shape.fillUsesCurrentColor = false
+ shape.fill = SVGHelper.parseColor(value, [:])
+ }
+
private func setStroke(_ value: String) {
guard let shape = node as? SVGShape else { return }
if value.lowercased() == "none" {
+ shape.strokeUsesCurrentColor = false
shape.stroke = nil
return
}
+ if value.lowercased() == "currentcolor" {
+ shape.strokeUsesCurrentColor = true
+ let currentOpacity = (shape.stroke?.fill as? SVGColor)?.opacity ?? 1
+ replaceStrokeFill(for: shape, with: (node.currentColor ?? SVGColor.black).opacity(currentOpacity))
+ return
+ }
+
guard let color = SVGHelper.parseColor(value, [:]) else { return }
- let current = shape.stroke
- shape.stroke = SVGStroke(
- fill: color,
- width: current?.width ?? 1,
- cap: current?.cap ?? .butt,
- join: current?.join ?? .miter,
- miterLimit: current?.miterLimit ?? 4,
- dashes: current?.dashes ?? [],
- offset: current?.offset ?? 0
- )
+ shape.strokeUsesCurrentColor = false
+ replaceStrokeFill(for: shape, with: color)
}
private func setStrokeWidth(_ value: String) {
@@ -293,6 +320,19 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
guard let path = node as? SVGPath else { return }
path.fillRule = value.lowercased() == "evenodd" ? .evenOdd : .winding
}
+
+ private func replaceStrokeFill(for shape: SVGShape, with fill: SVGPaint) {
+ let current = shape.stroke
+ shape.stroke = SVGStroke(
+ fill: fill,
+ width: current?.width ?? 1,
+ cap: current?.cap ?? .butt,
+ join: current?.join ?? .miter,
+ miterLimit: current?.miterLimit ?? 4,
+ dashes: current?.dashes ?? [],
+ offset: current?.offset ?? 0
+ )
+ }
}
@objcMembers
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index d218820d..4597d2c3 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -84,4 +84,19 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-fillrule-02")
}
+ @Test func scriptCurrentColor01() async throws {
+ // Unsupported script MIME type must not mutate fill via currentColor.
+ try await compareToReference("script-currentcolor-01")
+ }
+
+ @Test func scriptCurrentColor02() async throws {
+ // fill=currentColor then color=red should resolve to red.
+ try await compareToReference("script-currentcolor-02")
+ }
+
+ @Test func scriptCurrentColor03() async throws {
+ // color=lime then fill=currentColor should resolve to lime.
+ try await compareToReference("script-currentcolor-03")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-01.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-01.ref
new file mode 100644
index 00000000..455f4961
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-01.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "blue" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-02.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-02.ref
new file mode 100644
index 00000000..25151298
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-02.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "red" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-03.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-03.ref
new file mode 100644
index 00000000..36171b93
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-03.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "lime" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-01.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-01.svg
new file mode 100644
index 00000000..f878d43e
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-01.svg
@@ -0,0 +1,8 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-02.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-02.svg
new file mode 100644
index 00000000..ea4af714
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-02.svg
@@ -0,0 +1,8 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-03.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-03.svg
new file mode 100644
index 00000000..17a4f377
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-03.svg
@@ -0,0 +1,8 @@
+
From 3be9d3175e7682ffead897aad4e0394eaea15224 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 09:28:05 +0200
Subject: [PATCH 09/27] Harden currentcolor
---
GenerateReferencesCLI/cli.swift | 3 +++
Tests/SVGViewTests/SVGCustomTests.swift | 15 +++++++++++++++
.../w3c/Custom/refs/script-currentcolor-04.ref | 15 +++++++++++++++
.../w3c/Custom/refs/script-currentcolor-05.ref | 15 +++++++++++++++
.../w3c/Custom/refs/script-currentcolor-06.ref | 15 +++++++++++++++
.../w3c/Custom/svg/script-currentcolor-04.svg | 8 ++++++++
.../w3c/Custom/svg/script-currentcolor-05.svg | 8 ++++++++
.../w3c/Custom/svg/script-currentcolor-06.svg | 8 ++++++++
8 files changed, 87 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-04.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-05.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-06.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-04.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-05.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-06.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 0d7d358b..0b3c14bc 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -170,6 +170,9 @@ struct cli: ParsableCommand {
"script-currentcolor-01",
"script-currentcolor-02",
"script-currentcolor-03",
+ "script-currentcolor-04",
+ "script-currentcolor-05",
+ "script-currentcolor-06",
]
mutating func run() throws {
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index 4597d2c3..27d5375e 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -99,4 +99,19 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-03")
}
+ @Test func scriptCurrentColor04() async throws {
+ // Unsupported script MIME type must not mutate stroke via currentColor.
+ try await compareToReference("script-currentcolor-04")
+ }
+
+ @Test func scriptCurrentColor05() async throws {
+ // stroke=currentColor then color=red should resolve stroke to red.
+ try await compareToReference("script-currentcolor-05")
+ }
+
+ @Test func scriptCurrentColor06() async throws {
+ // color=lime then stroke=currentColor should resolve stroke to lime.
+ try await compareToReference("script-currentcolor-06")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-04.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-04.ref
new file mode 100644
index 00000000..88a55e86
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-04.ref
@@ -0,0 +1,15 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect {
+ id: "target",
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 100,
+ stroke: { fill: "blue", width: 6 }
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-05.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-05.ref
new file mode 100644
index 00000000..cef0d5ef
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-05.ref
@@ -0,0 +1,15 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect {
+ id: "target",
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 100,
+ stroke: { fill: "red", width: 6 }
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-06.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-06.ref
new file mode 100644
index 00000000..ab0a2da1
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-06.ref
@@ -0,0 +1,15 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect {
+ id: "target",
+ x: 10,
+ y: 10,
+ width: 100,
+ height: 100,
+ stroke: { fill: "lime", width: 6 }
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-04.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-04.svg
new file mode 100644
index 00000000..ffa73cff
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-04.svg
@@ -0,0 +1,8 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-05.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-05.svg
new file mode 100644
index 00000000..d9e1f28f
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-05.svg
@@ -0,0 +1,8 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-06.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-06.svg
new file mode 100644
index 00000000..7c04f3ee
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-06.svg
@@ -0,0 +1,8 @@
+
From ae64a3bcd1821e5f43dc8de6e45845fd5a1cbef4 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:13:56 +0200
Subject: [PATCH 10/27] Gitignore .vscode
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index a9969e90..50531b50 100644
--- a/.gitignore
+++ b/.gitignore
@@ -125,3 +125,4 @@ iOSInjectionProject/
# End of https://www.gitignore.io/api/swift,macos,carthage,cocoapods
test-output/
+/.vscode
From 6e53e867b5d60f1f45b9c53c5ec98d532ba69be1 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:16:38 +0200
Subject: [PATCH 11/27] Add currentColor runtime behavior and regression
controls
---
GenerateReferencesCLI/cli.swift | 2 +
.../Parser/SVG/Elements/SVGShapeParser.swift | 2 +
Source/Parser/SVG/SVGParser.swift | 5 ---
.../SVG/Scripting/SVGScriptRunner.swift | 40 ++++++++++++++-----
Tests/SVGViewTests/SVGCustomTests.swift | 10 +++++
.../w3c/1.1F2/refs/color-prop-05-t.ref | 2 +-
.../Custom/refs/script-currentcolor-07.ref | 13 ++++++
.../Custom/refs/script-currentcolor-08.ref | 13 ++++++
.../w3c/Custom/svg/script-currentcolor-07.svg | 8 ++++
.../w3c/Custom/svg/script-currentcolor-08.svg | 8 ++++
10 files changed, 87 insertions(+), 16 deletions(-)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-07.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-08.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-07.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-08.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 0b3c14bc..573fa339 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -173,6 +173,8 @@ struct cli: ParsableCommand {
"script-currentcolor-04",
"script-currentcolor-05",
"script-currentcolor-06",
+ "script-currentcolor-07",
+ "script-currentcolor-08",
]
mutating func run() throws {
diff --git a/Source/Parser/SVG/Elements/SVGShapeParser.swift b/Source/Parser/SVG/Elements/SVGShapeParser.swift
index 0e2e6ebe..a2029cd3 100644
--- a/Source/Parser/SVG/Elements/SVGShapeParser.swift
+++ b/Source/Parser/SVG/Elements/SVGShapeParser.swift
@@ -12,6 +12,8 @@ class SVGShapeParser: SVGBaseElementParser {
guard let locus = parseLocus(context: context) else { return nil }
locus.fill = SVGHelper.parseFill(context.styles, context.index)
locus.stroke = SVGHelper.parseStroke(context.styles, index: context.index)
+ locus.fillUsesCurrentColor = context.style("fill")?.lowercased() == "currentcolor"
+ locus.strokeUsesCurrentColor = context.style("stroke")?.lowercased() == "currentcolor"
return locus
}
diff --git a/Source/Parser/SVG/SVGParser.swift b/Source/Parser/SVG/SVGParser.swift
index 26a31967..b8d00ed0 100644
--- a/Source/Parser/SVG/SVGParser.swift
+++ b/Source/Parser/SVG/SVGParser.swift
@@ -112,11 +112,6 @@ public struct SVGParser {
}
}
- // TODO: it's a temporary solution. Need to create a correct style merging mechanics
- if styleDict["fill"] == "currentColor", let color = styleDict["color"] {
- styleDict["fill"] = color
- }
-
return styleDict
}
diff --git a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
index 11eff615..ee40fd3f 100644
--- a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
+++ b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
@@ -111,16 +111,7 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
private func setColor(_ value: String) {
guard let color = SVGHelper.parseColor(value, [:]) else { return }
- node.currentColor = color
-
- guard let shape = node as? SVGShape else { return }
- if shape.fillUsesCurrentColor {
- shape.fill = color
- }
- if shape.strokeUsesCurrentColor {
- let currentOpacity = (shape.stroke?.fill as? SVGColor)?.opacity ?? 1
- replaceStrokeFill(for: shape, with: color.opacity(currentOpacity))
- }
+ propagateCurrentColor(color, to: node)
}
private func setFill(_ value: String) {
@@ -333,6 +324,35 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
offset: current?.offset ?? 0
)
}
+
+ private func propagateCurrentColor(_ color: SVGColor, to node: SVGNode) {
+ node.currentColor = color
+ applyCurrentColorBindings(on: node)
+
+ if let group = node as? SVGGroup {
+ for child in group.contents {
+ propagateCurrentColor(color, to: child)
+ }
+ }
+
+ if let userSpaceNode = node as? SVGUserSpaceNode {
+ propagateCurrentColor(color, to: userSpaceNode.node)
+ }
+ }
+
+ private func applyCurrentColorBindings(on node: SVGNode) {
+ guard let shape = node as? SVGShape,
+ let currentColor = node.currentColor
+ else { return }
+
+ if shape.fillUsesCurrentColor {
+ shape.fill = currentColor
+ }
+ if shape.strokeUsesCurrentColor {
+ let currentOpacity = (shape.stroke?.fill as? SVGColor)?.opacity ?? 1
+ replaceStrokeFill(for: shape, with: currentColor.opacity(currentOpacity))
+ }
+ }
}
@objcMembers
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index 27d5375e..88581269 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -114,4 +114,14 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-06")
}
+ @Test func scriptCurrentColor07() async throws {
+ // Unsupported script MIME type must not mutate inherited currentColor fill.
+ try await compareToReference("script-currentcolor-07")
+ }
+
+ @Test func scriptCurrentColor08() async throws {
+ // Updating parent color should update descendant fill=currentColor.
+ try await compareToReference("script-currentcolor-08")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/1.1F2/refs/color-prop-05-t.ref b/Tests/SVGViewTests/w3c/1.1F2/refs/color-prop-05-t.ref
index a8e3ad7e..58a272cb 100644
--- a/Tests/SVGViewTests/w3c/1.1F2/refs/color-prop-05-t.ref
+++ b/Tests/SVGViewTests/w3c/1.1F2/refs/color-prop-05-t.ref
@@ -9,7 +9,7 @@ SVGViewport {
contents: [
SVGGroup {
contents: [
- SVGRect { x: 120, y: 60, width: 150, height: 150, fill: "lime" }
+ SVGRect { x: 120, y: 60, width: 150, height: 150, fill: "red" }
]
}
]
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-07.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-07.ref
new file mode 100644
index 00000000..b4c3fedb
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-07.ref
@@ -0,0 +1,13 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGGroup {
+ id: "container",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "blue" }
+ ]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-08.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-08.ref
new file mode 100644
index 00000000..20bb6b45
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-08.ref
@@ -0,0 +1,13 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGGroup {
+ id: "container",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "red" }
+ ]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-07.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-07.svg
new file mode 100644
index 00000000..cf2d8784
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-07.svg
@@ -0,0 +1,8 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-08.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-08.svg
new file mode 100644
index 00000000..825b3840
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-08.svg
@@ -0,0 +1,8 @@
+
From 2b001af31f0ff819bcc396cc1b1836db6853ce24 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:19:35 +0200
Subject: [PATCH 12/27] Harden currentColor inheritance and override precedence
---
GenerateReferencesCLI/cli.swift | 1 +
Source/Model/Nodes/SVGNode.swift | 2 ++
.../SVG/Elements/SVGElementParser.swift | 1 +
Source/Parser/SVG/SVGContext.swift | 6 ++++++
.../SVG/Scripting/SVGScriptRunner.swift | 19 ++++++++++++++-----
Tests/SVGViewTests/SVGCustomTests.swift | 5 +++++
.../Custom/refs/script-currentcolor-09.ref | 13 +++++++++++++
.../w3c/Custom/svg/script-currentcolor-09.svg | 8 ++++++++
8 files changed, 50 insertions(+), 5 deletions(-)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-09.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-09.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 573fa339..3c155956 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -175,6 +175,7 @@ struct cli: ParsableCommand {
"script-currentcolor-06",
"script-currentcolor-07",
"script-currentcolor-08",
+ "script-currentcolor-09",
]
mutating func run() throws {
diff --git a/Source/Model/Nodes/SVGNode.swift b/Source/Model/Nodes/SVGNode.swift
index 2edc4108..cec0d173 100644
--- a/Source/Model/Nodes/SVGNode.swift
+++ b/Source/Model/Nodes/SVGNode.swift
@@ -12,6 +12,7 @@ public class SVGNode: SerializableElement {
public var opaque: Bool
public var opacity: Double
public var currentColor: SVGColor?
+ public var hasExplicitCurrentColor: Bool = false
public var clip: SVGNode?
public var mask: SVGNode?
public var id: String?
@@ -23,6 +24,7 @@ public class SVGNode: SerializableElement {
@Published public var opaque: Bool
@Published public var opacity: Double
@Published public var currentColor: SVGColor?
+ @Published public var hasExplicitCurrentColor: Bool = false
@Published public var clip: SVGNode?
@Published public var mask: SVGNode?
@Published public var id: String?
diff --git a/Source/Parser/SVG/Elements/SVGElementParser.swift b/Source/Parser/SVG/Elements/SVGElementParser.swift
index 17999ef5..791ce84c 100644
--- a/Source/Parser/SVG/Elements/SVGElementParser.swift
+++ b/Source/Parser/SVG/Elements/SVGElementParser.swift
@@ -23,6 +23,7 @@ class SVGBaseElementParser: SVGElementParser {
if let colorValue = context.style("color") {
node.currentColor = SVGHelper.parseColor(colorValue, context.styles)
}
+ node.hasExplicitCurrentColor = context.hasElementStyle("color")
if let clipId = SVGHelper.parseUse(context.properties["clip-path"]),
let clipNode = context.index.element(by: clipId),
diff --git a/Source/Parser/SVG/SVGContext.swift b/Source/Parser/SVG/SVGContext.swift
index 6438e7ff..61f9dec9 100644
--- a/Source/Parser/SVG/SVGContext.swift
+++ b/Source/Parser/SVG/SVGContext.swift
@@ -61,6 +61,7 @@ class SVGNodeContext: SVGContext {
let element: XMLElement
let useIds: [String]
+ let elementStyles: [String:String]
let styles: [String:String]
let properties: [String:String]
@@ -71,6 +72,7 @@ class SVGNodeContext: SVGContext {
self.properties = element.attributes.filter { !SVGConstants.availableStyleAttributes.contains($0.key) }
let styleDict = SVGParser.getStyleAttributes(xml: element, index: root.index)
+ self.elementStyles = styleDict
self.styles = Self.mergeStyles(element: styleDict, parent: parentStyles)
}
@@ -109,6 +111,10 @@ class SVGNodeContext: SVGContext {
return styles[name]
}
+ func hasElementStyle(_ name: String) -> Bool {
+ elementStyles[name] != nil
+ }
+
func property(_ name: String) -> String? {
return properties[name]
}
diff --git a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
index ee40fd3f..05b927e9 100644
--- a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
+++ b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
@@ -111,7 +111,8 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
private func setColor(_ value: String) {
guard let color = SVGHelper.parseColor(value, [:]) else { return }
- propagateCurrentColor(color, to: node)
+ node.hasExplicitCurrentColor = true
+ propagateCurrentColor(color, to: node, forceCurrentNode: true)
}
private func setFill(_ value: String) {
@@ -325,18 +326,26 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
)
}
- private func propagateCurrentColor(_ color: SVGColor, to node: SVGNode) {
- node.currentColor = color
+ private func propagateCurrentColor(_ color: SVGColor, to node: SVGNode, forceCurrentNode: Bool = false) {
+ let resolvedColor: SVGColor
+ if node.hasExplicitCurrentColor && !forceCurrentNode {
+ resolvedColor = node.currentColor ?? color
+ node.currentColor = resolvedColor
+ } else {
+ node.currentColor = color
+ resolvedColor = color
+ }
+
applyCurrentColorBindings(on: node)
if let group = node as? SVGGroup {
for child in group.contents {
- propagateCurrentColor(color, to: child)
+ propagateCurrentColor(resolvedColor, to: child)
}
}
if let userSpaceNode = node as? SVGUserSpaceNode {
- propagateCurrentColor(color, to: userSpaceNode.node)
+ propagateCurrentColor(resolvedColor, to: userSpaceNode.node)
}
}
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index 88581269..e75014c3 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -124,4 +124,9 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-08")
}
+ @Test func scriptCurrentColor09() async throws {
+ // Child explicit color should override inherited parent color changes.
+ try await compareToReference("script-currentcolor-09")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-09.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-09.ref
new file mode 100644
index 00000000..25f20691
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-09.ref
@@ -0,0 +1,13 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGGroup {
+ id: "container",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "lime" }
+ ]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-09.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-09.svg
new file mode 100644
index 00000000..f6fbd00e
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-09.svg
@@ -0,0 +1,8 @@
+
From f15b1d1f96b71c97a3bd6d64e814374fca27fc5c Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:23:48 +0200
Subject: [PATCH 13/27] Add text currentColor parity and relink controls
---
GenerateReferencesCLI/cli.swift | 4 +
Source/Model/Nodes/SVGText.swift | 4 +
.../Parser/SVG/Elements/SVGTextParser.swift | 5 +-
.../SVG/Scripting/SVGScriptRunner.swift | 107 +++++++++++++-----
Tests/SVGViewTests/SVGCustomTests.swift | 20 ++++
.../Custom/refs/script-currentcolor-10.ref | 14 +++
.../Custom/refs/script-currentcolor-11.ref | 14 +++
.../Custom/refs/script-currentcolor-12.ref | 8 ++
.../Custom/refs/script-currentcolor-13.ref | 8 ++
.../w3c/Custom/svg/script-currentcolor-10.svg | 6 +
.../w3c/Custom/svg/script-currentcolor-11.svg | 6 +
.../w3c/Custom/svg/script-currentcolor-12.svg | 10 ++
.../w3c/Custom/svg/script-currentcolor-13.svg | 11 ++
13 files changed, 190 insertions(+), 27 deletions(-)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-10.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-11.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-12.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-13.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-10.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-11.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-12.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-13.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 3c155956..8254de35 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -176,6 +176,10 @@ struct cli: ParsableCommand {
"script-currentcolor-07",
"script-currentcolor-08",
"script-currentcolor-09",
+ "script-currentcolor-10",
+ "script-currentcolor-11",
+ "script-currentcolor-12",
+ "script-currentcolor-13",
]
mutating func run() throws {
diff --git a/Source/Model/Nodes/SVGText.swift b/Source/Model/Nodes/SVGText.swift
index 098cb99d..9d7b8cc6 100644
--- a/Source/Model/Nodes/SVGText.swift
+++ b/Source/Model/Nodes/SVGText.swift
@@ -17,12 +17,16 @@ public class SVGText: SVGNode {
public var font: SVGFont?
public var fill: SVGPaint?
public var stroke: SVGStroke?
+ public var fillUsesCurrentColor: Bool = false
+ public var strokeUsesCurrentColor: Bool = false
public var textAnchor: Anchor = .leading
#else
@Published public var text: String
@Published public var font: SVGFont?
@Published public var fill: SVGPaint?
@Published public var stroke: SVGStroke?
+ @Published public var fillUsesCurrentColor: Bool = false
+ @Published public var strokeUsesCurrentColor: Bool = false
@Published public var textAnchor: Anchor = .leading
#endif
diff --git a/Source/Parser/SVG/Elements/SVGTextParser.swift b/Source/Parser/SVG/Elements/SVGTextParser.swift
index 5cbbe6fd..02876d93 100644
--- a/Source/Parser/SVG/Elements/SVGTextParser.swift
+++ b/Source/Parser/SVG/Elements/SVGTextParser.swift
@@ -25,7 +25,10 @@ class SVGTextParser: SVGBaseElementParser {
if let textNode = context.element.contents.first as? XMLText {
let trimmed = textNode.text.trimmingCharacters(in: .whitespacesAndNewlines).processingWhitespaces()
- return SVGText(text: trimmed, font: font, fill: SVGHelper.parseFill(context.styles, context.index), stroke: SVGHelper.parseStroke(context.styles, index: context.index), textAnchor: textAnchor, transform: transform)
+ let text = SVGText(text: trimmed, font: font, fill: SVGHelper.parseFill(context.styles, context.index), stroke: SVGHelper.parseStroke(context.styles, index: context.index), textAnchor: textAnchor, transform: transform)
+ text.fillUsesCurrentColor = context.style("fill")?.lowercased() == "currentcolor"
+ text.strokeUsesCurrentColor = context.style("stroke")?.lowercased() == "currentcolor"
+ return text
}
return .none
}
diff --git a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
index 05b927e9..1668c6bc 100644
--- a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
+++ b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
@@ -116,37 +116,69 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
}
private func setFill(_ value: String) {
- guard let shape = node as? SVGShape else { return }
+ if let shape = node as? SVGShape {
+ if value.lowercased() == "currentcolor" {
+ shape.fillUsesCurrentColor = true
+ shape.fill = node.currentColor ?? SVGColor.black
+ return
+ }
- if value.lowercased() == "currentcolor" {
- shape.fillUsesCurrentColor = true
- shape.fill = node.currentColor ?? SVGColor.black
+ shape.fillUsesCurrentColor = false
+ shape.fill = SVGHelper.parseColor(value, [:])
return
}
- shape.fillUsesCurrentColor = false
- shape.fill = SVGHelper.parseColor(value, [:])
+ if let text = node as? SVGText {
+ if value.lowercased() == "currentcolor" {
+ text.fillUsesCurrentColor = true
+ text.fill = node.currentColor ?? SVGColor.black
+ return
+ }
+
+ text.fillUsesCurrentColor = false
+ text.fill = SVGHelper.parseColor(value, [:])
+ }
}
private func setStroke(_ value: String) {
- guard let shape = node as? SVGShape else { return }
+ if let shape = node as? SVGShape {
+ if value.lowercased() == "none" {
+ shape.strokeUsesCurrentColor = false
+ shape.stroke = nil
+ return
+ }
- if value.lowercased() == "none" {
+ if value.lowercased() == "currentcolor" {
+ shape.strokeUsesCurrentColor = true
+ let currentOpacity = (shape.stroke?.fill as? SVGColor)?.opacity ?? 1
+ replaceStrokeFill(for: shape, with: (node.currentColor ?? SVGColor.black).opacity(currentOpacity))
+ return
+ }
+
+ guard let color = SVGHelper.parseColor(value, [:]) else { return }
shape.strokeUsesCurrentColor = false
- shape.stroke = nil
+ replaceStrokeFill(for: shape, with: color)
return
}
- if value.lowercased() == "currentcolor" {
- shape.strokeUsesCurrentColor = true
- let currentOpacity = (shape.stroke?.fill as? SVGColor)?.opacity ?? 1
- replaceStrokeFill(for: shape, with: (node.currentColor ?? SVGColor.black).opacity(currentOpacity))
- return
- }
+ if let text = node as? SVGText {
+ if value.lowercased() == "none" {
+ text.strokeUsesCurrentColor = false
+ text.stroke = nil
+ return
+ }
- guard let color = SVGHelper.parseColor(value, [:]) else { return }
- shape.strokeUsesCurrentColor = false
- replaceStrokeFill(for: shape, with: color)
+ if value.lowercased() == "currentcolor" {
+ text.strokeUsesCurrentColor = true
+ let currentOpacity = (text.stroke?.fill as? SVGColor)?.opacity ?? 1
+ replaceStrokeFill(for: text, with: (node.currentColor ?? SVGColor.black).opacity(currentOpacity))
+ return
+ }
+
+ guard let color = SVGHelper.parseColor(value, [:]) else { return }
+ text.strokeUsesCurrentColor = false
+ replaceStrokeFill(for: text, with: color)
+ }
}
private func setStrokeWidth(_ value: String) {
@@ -326,6 +358,19 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
)
}
+ private func replaceStrokeFill(for text: SVGText, with fill: SVGPaint) {
+ let current = text.stroke
+ text.stroke = SVGStroke(
+ fill: fill,
+ width: current?.width ?? 1,
+ cap: current?.cap ?? .butt,
+ join: current?.join ?? .miter,
+ miterLimit: current?.miterLimit ?? 4,
+ dashes: current?.dashes ?? [],
+ offset: current?.offset ?? 0
+ )
+ }
+
private func propagateCurrentColor(_ color: SVGColor, to node: SVGNode, forceCurrentNode: Bool = false) {
let resolvedColor: SVGColor
if node.hasExplicitCurrentColor && !forceCurrentNode {
@@ -350,16 +395,26 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
}
private func applyCurrentColorBindings(on node: SVGNode) {
- guard let shape = node as? SVGShape,
- let currentColor = node.currentColor
- else { return }
+ guard let currentColor = node.currentColor else { return }
- if shape.fillUsesCurrentColor {
- shape.fill = currentColor
+ if let shape = node as? SVGShape {
+ if shape.fillUsesCurrentColor {
+ shape.fill = currentColor
+ }
+ if shape.strokeUsesCurrentColor {
+ let currentOpacity = (shape.stroke?.fill as? SVGColor)?.opacity ?? 1
+ replaceStrokeFill(for: shape, with: currentColor.opacity(currentOpacity))
+ }
}
- if shape.strokeUsesCurrentColor {
- let currentOpacity = (shape.stroke?.fill as? SVGColor)?.opacity ?? 1
- replaceStrokeFill(for: shape, with: currentColor.opacity(currentOpacity))
+
+ if let text = node as? SVGText {
+ if text.fillUsesCurrentColor {
+ text.fill = currentColor
+ }
+ if text.strokeUsesCurrentColor {
+ let currentOpacity = (text.stroke?.fill as? SVGColor)?.opacity ?? 1
+ replaceStrokeFill(for: text, with: currentColor.opacity(currentOpacity))
+ }
}
}
}
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index e75014c3..260d0c59 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -129,4 +129,24 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-09")
}
+ @Test func scriptCurrentColor10() async throws {
+ // Unsupported script MIME type must not update text currentColor fill.
+ try await compareToReference("script-currentcolor-10")
+ }
+
+ @Test func scriptCurrentColor11() async throws {
+ // Supported script MIME type should update text fill via currentColor.
+ try await compareToReference("script-currentcolor-11")
+ }
+
+ @Test func scriptCurrentColor12() async throws {
+ // Concrete fill should unlink currentColor so later color changes do not update it.
+ try await compareToReference("script-currentcolor-12")
+ }
+
+ @Test func scriptCurrentColor13() async throws {
+ // Setting fill back to currentColor should relink and follow later color updates.
+ try await compareToReference("script-currentcolor-13")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-10.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-10.ref
new file mode 100644
index 00000000..eff19562
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-10.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ fill: "blue",
+ transform: [1, 0, 0, 1, 10, 40]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-11.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-11.ref
new file mode 100644
index 00000000..9f951163
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-11.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ fill: "red",
+ transform: [1, 0, 0, 1, 10, 40]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-12.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-12.ref
new file mode 100644
index 00000000..455f4961
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-12.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "blue" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-13.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-13.ref
new file mode 100644
index 00000000..36171b93
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-13.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "lime" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-10.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-10.svg
new file mode 100644
index 00000000..eb977ba2
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-10.svg
@@ -0,0 +1,6 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-11.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-11.svg
new file mode 100644
index 00000000..2ab3f03c
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-11.svg
@@ -0,0 +1,6 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-12.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-12.svg
new file mode 100644
index 00000000..7e74e501
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-12.svg
@@ -0,0 +1,10 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-13.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-13.svg
new file mode 100644
index 00000000..00d57ef2
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-13.svg
@@ -0,0 +1,11 @@
+
From 9e96a3e8eab0e1c5cab20e93658ac32e8ba1ba51 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:25:05 +0200
Subject: [PATCH 14/27] Add text stroke currentColor relink controls
---
GenerateReferencesCLI/cli.swift | 3 +++
Tests/SVGViewTests/SVGCustomTests.swift | 15 +++++++++++++++
.../w3c/Custom/refs/script-currentcolor-14.ref | 14 ++++++++++++++
.../w3c/Custom/refs/script-currentcolor-15.ref | 14 ++++++++++++++
.../w3c/Custom/refs/script-currentcolor-16.ref | 14 ++++++++++++++
.../w3c/Custom/svg/script-currentcolor-14.svg | 9 +++++++++
.../w3c/Custom/svg/script-currentcolor-15.svg | 10 ++++++++++
.../w3c/Custom/svg/script-currentcolor-16.svg | 10 ++++++++++
8 files changed, 89 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-14.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-15.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-16.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-14.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-15.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-16.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 8254de35..8e704ab0 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -180,6 +180,9 @@ struct cli: ParsableCommand {
"script-currentcolor-11",
"script-currentcolor-12",
"script-currentcolor-13",
+ "script-currentcolor-14",
+ "script-currentcolor-15",
+ "script-currentcolor-16",
]
mutating func run() throws {
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index 260d0c59..ead36412 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -149,4 +149,19 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-13")
}
+ @Test func scriptCurrentColor14() async throws {
+ // Concrete stroke should unlink text stroke from currentColor updates.
+ try await compareToReference("script-currentcolor-14")
+ }
+
+ @Test func scriptCurrentColor15() async throws {
+ // Setting text stroke back to currentColor should relink to later color updates.
+ try await compareToReference("script-currentcolor-15")
+ }
+
+ @Test func scriptCurrentColor16() async throws {
+ // Stroke none should clear binding; setting currentColor later should relink using current color.
+ try await compareToReference("script-currentcolor-16")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-14.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-14.ref
new file mode 100644
index 00000000..f9de4128
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-14.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ stroke: { fill: "blue", width: 6 },
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-15.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-15.ref
new file mode 100644
index 00000000..cd8d2902
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-15.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ stroke: { fill: "lime", width: 6 },
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-16.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-16.ref
new file mode 100644
index 00000000..c9a62467
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-16.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ stroke: { fill: "lime" },
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-14.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-14.svg
new file mode 100644
index 00000000..bb956bcf
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-14.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-15.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-15.svg
new file mode 100644
index 00000000..61553b15
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-15.svg
@@ -0,0 +1,10 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-16.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-16.svg
new file mode 100644
index 00000000..9e6386d9
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-16.svg
@@ -0,0 +1,10 @@
+
From b80efe2e750308550a8698ecb645565ad8fed1f2 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:27:28 +0200
Subject: [PATCH 15/27] Add mixed-node currentColor inheritance controls
---
GenerateReferencesCLI/cli.swift | 3 +++
Tests/SVGViewTests/SVGCustomTests.swift | 15 ++++++++++++++
.../Custom/refs/script-currentcolor-17.ref | 20 +++++++++++++++++++
.../Custom/refs/script-currentcolor-18.ref | 20 +++++++++++++++++++
.../Custom/refs/script-currentcolor-19.ref | 20 +++++++++++++++++++
.../w3c/Custom/svg/script-currentcolor-17.svg | 10 ++++++++++
.../w3c/Custom/svg/script-currentcolor-18.svg | 13 ++++++++++++
.../w3c/Custom/svg/script-currentcolor-19.svg | 11 ++++++++++
8 files changed, 112 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-17.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-18.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-19.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-17.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-18.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-19.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 8e704ab0..09990da4 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -183,6 +183,9 @@ struct cli: ParsableCommand {
"script-currentcolor-14",
"script-currentcolor-15",
"script-currentcolor-16",
+ "script-currentcolor-17",
+ "script-currentcolor-18",
+ "script-currentcolor-19",
]
mutating func run() throws {
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index ead36412..b429c497 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -164,4 +164,19 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-16")
}
+ @Test func scriptCurrentColor17() async throws {
+ // Parent color updates should propagate to both shape and text children using currentColor.
+ try await compareToReference("script-currentcolor-17")
+ }
+
+ @Test func scriptCurrentColor18() async throws {
+ // Child text relinking to currentColor should bind to inherited parent color updates.
+ try await compareToReference("script-currentcolor-18")
+ }
+
+ @Test func scriptCurrentColor19() async throws {
+ // Explicit child text color override should remain stable while sibling shape tracks parent color.
+ try await compareToReference("script-currentcolor-19")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-17.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-17.ref
new file mode 100644
index 00000000..67e275c2
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-17.ref
@@ -0,0 +1,20 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGGroup {
+ id: "parent",
+ contents: [
+ SVGRect { id: "shape", x: 10, y: 10, width: 40, height: 40, fill: "red" },
+ SVGText {
+ id: "text",
+ text: "X",
+ font: { },
+ fill: "red",
+ transform: [1, 0, 0, 1, 10, 90]
+ }
+ ]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-18.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-18.ref
new file mode 100644
index 00000000..edd4a88e
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-18.ref
@@ -0,0 +1,20 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGGroup {
+ id: "parent",
+ contents: [
+ SVGRect { id: "shape", x: 10, y: 10, width: 40, height: 40, fill: "lime" },
+ SVGText {
+ id: "text",
+ text: "X",
+ font: { },
+ fill: "green",
+ transform: [1, 0, 0, 1, 10, 90]
+ }
+ ]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-19.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-19.ref
new file mode 100644
index 00000000..edd4a88e
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-19.ref
@@ -0,0 +1,20 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGGroup {
+ id: "parent",
+ contents: [
+ SVGRect { id: "shape", x: 10, y: 10, width: 40, height: 40, fill: "lime" },
+ SVGText {
+ id: "text",
+ text: "X",
+ font: { },
+ fill: "green",
+ transform: [1, 0, 0, 1, 10, 90]
+ }
+ ]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-17.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-17.svg
new file mode 100644
index 00000000..88dbee70
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-17.svg
@@ -0,0 +1,10 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-18.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-18.svg
new file mode 100644
index 00000000..819e1d7a
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-18.svg
@@ -0,0 +1,13 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-19.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-19.svg
new file mode 100644
index 00000000..43740974
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-19.svg
@@ -0,0 +1,11 @@
+
From 16ed2a5eda6be48da19e850b3f48f10efdcf451a Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:30:49 +0200
Subject: [PATCH 16/27] Add style source precedence currentColor controls
---
GenerateReferencesCLI/cli.swift | 3 +++
Tests/SVGViewTests/SVGCustomTests.swift | 15 +++++++++++++++
.../w3c/Custom/refs/script-currentcolor-20.ref | 8 ++++++++
.../w3c/Custom/refs/script-currentcolor-21.ref | 8 ++++++++
.../w3c/Custom/refs/script-currentcolor-22.ref | 14 ++++++++++++++
.../w3c/Custom/svg/script-currentcolor-20.svg | 8 ++++++++
.../w3c/Custom/svg/script-currentcolor-21.svg | 7 +++++++
.../w3c/Custom/svg/script-currentcolor-22.svg | 8 ++++++++
8 files changed, 71 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-20.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-21.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-22.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-20.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-21.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-22.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 09990da4..3be2c691 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -186,6 +186,9 @@ struct cli: ParsableCommand {
"script-currentcolor-17",
"script-currentcolor-18",
"script-currentcolor-19",
+ "script-currentcolor-20",
+ "script-currentcolor-21",
+ "script-currentcolor-22",
]
mutating func run() throws {
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index b429c497..bd7a2906 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -179,4 +179,19 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-19")
}
+ @Test func scriptCurrentColor20() async throws {
+ // Runtime fill=currentColor should override earlier concrete style fill and follow later color changes.
+ try await compareToReference("script-currentcolor-20")
+ }
+
+ @Test func scriptCurrentColor21() async throws {
+ // Inline style color should be the source color when runtime fill binds to currentColor.
+ try await compareToReference("script-currentcolor-21")
+ }
+
+ @Test func scriptCurrentColor22() async throws {
+ // Text runtime fill=currentColor should bind after parse-time style/attribute precedence resolution.
+ try await compareToReference("script-currentcolor-22")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-20.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-20.ref
new file mode 100644
index 00000000..36171b93
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-20.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "lime" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-21.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-21.ref
new file mode 100644
index 00000000..25151298
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-21.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "red" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-22.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-22.ref
new file mode 100644
index 00000000..776771f1
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-22.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ fill: "lime",
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-20.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-20.svg
new file mode 100644
index 00000000..d438af6c
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-20.svg
@@ -0,0 +1,8 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-21.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-21.svg
new file mode 100644
index 00000000..556519f9
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-21.svg
@@ -0,0 +1,7 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-22.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-22.svg
new file mode 100644
index 00000000..dd6e4b9d
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-22.svg
@@ -0,0 +1,8 @@
+
From 2fe2f5102c8b6a7d4bcfadf04b98ff6d85a3ec9a Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:32:40 +0200
Subject: [PATCH 17/27] Add opacity hardening for currentColor controls
---
GenerateReferencesCLI/cli.swift | 3 +
.../SVG/Scripting/SVGScriptRunner.swift | 60 ++++++++++++-------
Tests/SVGViewTests/SVGCustomTests.swift | 15 +++++
.../Custom/refs/script-currentcolor-23.ref | 8 +++
.../Custom/refs/script-currentcolor-24.ref | 14 +++++
.../Custom/refs/script-currentcolor-25.ref | 14 +++++
.../w3c/Custom/svg/script-currentcolor-23.svg | 8 +++
.../w3c/Custom/svg/script-currentcolor-24.svg | 8 +++
.../w3c/Custom/svg/script-currentcolor-25.svg | 11 ++++
9 files changed, 121 insertions(+), 20 deletions(-)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-23.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-24.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-25.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-23.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-24.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-25.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 3be2c691..d2ca2b5f 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -189,6 +189,9 @@ struct cli: ParsableCommand {
"script-currentcolor-20",
"script-currentcolor-21",
"script-currentcolor-22",
+ "script-currentcolor-23",
+ "script-currentcolor-24",
+ "script-currentcolor-25",
]
mutating func run() throws {
diff --git a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
index 1668c6bc..416d4282 100644
--- a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
+++ b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
@@ -199,31 +199,51 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
}
private func setFillOpacity(_ value: String) {
- guard let shape = node as? SVGShape,
- let opacity = SVGHelper.doubleFromString(value),
- let fill = shape.fill
- else { return }
-
+ guard let opacity = SVGHelper.doubleFromString(value) else { return }
let clamped = min(max(opacity, 0), 1)
- shape.fill = fill.opacity(clamped)
+
+ if let shape = node as? SVGShape,
+ let fill = shape.fill {
+ shape.fill = fill.opacity(clamped)
+ return
+ }
+
+ if let text = node as? SVGText,
+ let fill = text.fill {
+ text.fill = fill.opacity(clamped)
+ }
}
private func setStrokeOpacity(_ value: String) {
- guard let shape = node as? SVGShape,
- let opacity = SVGHelper.doubleFromString(value),
- let current = shape.stroke
- else { return }
-
+ guard let opacity = SVGHelper.doubleFromString(value) else { return }
let clamped = min(max(opacity, 0), 1)
- shape.stroke = SVGStroke(
- fill: current.fill.opacity(clamped),
- width: current.width,
- cap: current.cap,
- join: current.join,
- miterLimit: current.miterLimit,
- dashes: current.dashes,
- offset: current.offset
- )
+
+ if let shape = node as? SVGShape,
+ let current = shape.stroke {
+ shape.stroke = SVGStroke(
+ fill: current.fill.opacity(clamped),
+ width: current.width,
+ cap: current.cap,
+ join: current.join,
+ miterLimit: current.miterLimit,
+ dashes: current.dashes,
+ offset: current.offset
+ )
+ return
+ }
+
+ if let text = node as? SVGText,
+ let current = text.stroke {
+ text.stroke = SVGStroke(
+ fill: current.fill.opacity(clamped),
+ width: current.width,
+ cap: current.cap,
+ join: current.join,
+ miterLimit: current.miterLimit,
+ dashes: current.dashes,
+ offset: current.offset
+ )
+ }
}
private func setStrokeDashArray(_ value: String) {
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index bd7a2906..4123e57e 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -194,4 +194,19 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-22")
}
+ @Test func scriptCurrentColor23() async throws {
+ // Shape fill-opacity should be preserved while fill follows currentColor updates.
+ try await compareToReference("script-currentcolor-23")
+ }
+
+ @Test func scriptCurrentColor24() async throws {
+ // Text fill-opacity should be preserved while fill follows currentColor updates.
+ try await compareToReference("script-currentcolor-24")
+ }
+
+ @Test func scriptCurrentColor25() async throws {
+ // Text stroke-opacity should remain controllable across none -> currentColor relink.
+ try await compareToReference("script-currentcolor-25")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-23.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-23.ref
new file mode 100644
index 00000000..25151298
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-23.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "red" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-24.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-24.ref
new file mode 100644
index 00000000..c9c4cae7
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-24.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ fill: "red",
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-25.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-25.ref
new file mode 100644
index 00000000..760c633b
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-25.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ stroke: { fill: "40% red" },
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-23.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-23.svg
new file mode 100644
index 00000000..7662ecdf
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-23.svg
@@ -0,0 +1,8 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-24.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-24.svg
new file mode 100644
index 00000000..40d88cf6
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-24.svg
@@ -0,0 +1,8 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-25.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-25.svg
new file mode 100644
index 00000000..12bca29f
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-25.svg
@@ -0,0 +1,11 @@
+
From 071229eb30a3a7c03c69d878e6fef0dddc4a7671 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:35:57 +0200
Subject: [PATCH 18/27] Add currentColor mutation ordering controls
---
GenerateReferencesCLI/cli.swift | 3 +++
Tests/SVGViewTests/SVGCustomTests.swift | 15 ++++++++++++++
.../Custom/refs/script-currentcolor-26.ref | 8 ++++++++
.../Custom/refs/script-currentcolor-27.ref | 14 +++++++++++++
.../Custom/refs/script-currentcolor-28.ref | 20 +++++++++++++++++++
.../w3c/Custom/svg/script-currentcolor-26.svg | 11 ++++++++++
.../w3c/Custom/svg/script-currentcolor-27.svg | 12 +++++++++++
.../w3c/Custom/svg/script-currentcolor-28.svg | 18 +++++++++++++++++
8 files changed, 101 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-26.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-27.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-28.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-26.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-27.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-28.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index d2ca2b5f..ce57710f 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -192,6 +192,9 @@ struct cli: ParsableCommand {
"script-currentcolor-23",
"script-currentcolor-24",
"script-currentcolor-25",
+ "script-currentcolor-26",
+ "script-currentcolor-27",
+ "script-currentcolor-28",
]
mutating func run() throws {
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index 4123e57e..d6c71b3b 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -209,4 +209,19 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-25")
}
+ @Test func scriptCurrentColor26() async throws {
+ // Shape relink to currentColor should resume color tracking while preserving runtime fill-opacity.
+ try await compareToReference("script-currentcolor-26")
+ }
+
+ @Test func scriptCurrentColor27() async throws {
+ // Text stroke relink should restore currentColor tracking with latest runtime stroke-opacity.
+ try await compareToReference("script-currentcolor-27")
+ }
+
+ @Test func scriptCurrentColor28() async throws {
+ // Mixed descendants should preserve opacity while relinked nodes follow parent color updates.
+ try await compareToReference("script-currentcolor-28")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-26.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-26.ref
new file mode 100644
index 00000000..36171b93
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-26.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "lime" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-27.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-27.ref
new file mode 100644
index 00000000..569b8c42
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-27.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ stroke: { fill: "60% lime", width: 6 },
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-28.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-28.ref
new file mode 100644
index 00000000..7bbfa0c9
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-28.ref
@@ -0,0 +1,20 @@
+SVGViewport {
+ width: "140",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGGroup {
+ id: "group",
+ contents: [
+ SVGRect { id: "shape", x: 10, y: 10, width: 40, height: 100, fill: "lime" },
+ SVGText {
+ id: "text",
+ text: "X",
+ font: { },
+ fill: "lime",
+ transform: [1, 0, 0, 1, 70, 75]
+ }
+ ]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-26.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-26.svg
new file mode 100644
index 00000000..3cfbfe06
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-26.svg
@@ -0,0 +1,11 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-27.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-27.svg
new file mode 100644
index 00000000..d6e0fa8c
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-27.svg
@@ -0,0 +1,12 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-28.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-28.svg
new file mode 100644
index 00000000..6a73584c
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-28.svg
@@ -0,0 +1,18 @@
+
From a724a0c09fdc0d64617edac5b83060f990f06d7c Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:38:47 +0200
Subject: [PATCH 19/27] Add currentColor opacity clamping controls
---
GenerateReferencesCLI/cli.swift | 3 +++
Tests/SVGViewTests/SVGCustomTests.swift | 15 +++++++++++++++
.../w3c/Custom/refs/script-currentcolor-29.ref | 8 ++++++++
.../w3c/Custom/refs/script-currentcolor-30.ref | 14 ++++++++++++++
.../w3c/Custom/refs/script-currentcolor-31.ref | 14 ++++++++++++++
.../w3c/Custom/svg/script-currentcolor-29.svg | 8 ++++++++
.../w3c/Custom/svg/script-currentcolor-30.svg | 9 +++++++++
.../w3c/Custom/svg/script-currentcolor-31.svg | 12 ++++++++++++
8 files changed, 83 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-29.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-30.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-31.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-29.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-30.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-31.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index ce57710f..9c92768d 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -195,6 +195,9 @@ struct cli: ParsableCommand {
"script-currentcolor-26",
"script-currentcolor-27",
"script-currentcolor-28",
+ "script-currentcolor-29",
+ "script-currentcolor-30",
+ "script-currentcolor-31",
]
mutating func run() throws {
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index d6c71b3b..f4bd007c 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -224,4 +224,19 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-28")
}
+ @Test func scriptCurrentColor29() async throws {
+ // fill-opacity values above 1 should clamp while fill remains linked to currentColor updates.
+ try await compareToReference("script-currentcolor-29")
+ }
+
+ @Test func scriptCurrentColor30() async throws {
+ // Text fill-opacity below 0 should clamp and later in-range opacity should apply.
+ try await compareToReference("script-currentcolor-30")
+ }
+
+ @Test func scriptCurrentColor31() async throws {
+ // Text stroke-opacity should clamp around none -> currentColor relink and accept later valid updates.
+ try await compareToReference("script-currentcolor-31")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-29.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-29.ref
new file mode 100644
index 00000000..25151298
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-29.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "red" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-30.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-30.ref
new file mode 100644
index 00000000..c9c4cae7
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-30.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ fill: "red",
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-31.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-31.ref
new file mode 100644
index 00000000..db8f13db
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-31.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ stroke: { fill: "50% lime" },
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-29.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-29.svg
new file mode 100644
index 00000000..0438362e
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-29.svg
@@ -0,0 +1,8 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-30.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-30.svg
new file mode 100644
index 00000000..868c3e2e
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-30.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-31.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-31.svg
new file mode 100644
index 00000000..b93755a8
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-31.svg
@@ -0,0 +1,12 @@
+
From 1a2160bb8c0e3cca6d73521e560541d358011018 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:40:43 +0200
Subject: [PATCH 20/27] Add invalid opacity currentColor controls
---
GenerateReferencesCLI/cli.swift | 3 +++
Tests/SVGViewTests/SVGCustomTests.swift | 15 +++++++++++++++
.../w3c/Custom/refs/script-currentcolor-32.ref | 8 ++++++++
.../w3c/Custom/refs/script-currentcolor-33.ref | 14 ++++++++++++++
.../w3c/Custom/refs/script-currentcolor-34.ref | 14 ++++++++++++++
.../w3c/Custom/svg/script-currentcolor-32.svg | 9 +++++++++
.../w3c/Custom/svg/script-currentcolor-33.svg | 9 +++++++++
.../w3c/Custom/svg/script-currentcolor-34.svg | 11 +++++++++++
8 files changed, 83 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-32.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-33.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-34.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-32.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-33.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-34.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 9c92768d..a0518066 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -198,6 +198,9 @@ struct cli: ParsableCommand {
"script-currentcolor-29",
"script-currentcolor-30",
"script-currentcolor-31",
+ "script-currentcolor-32",
+ "script-currentcolor-33",
+ "script-currentcolor-34",
]
mutating func run() throws {
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index f4bd007c..f67d9cc9 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -239,4 +239,19 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-31")
}
+ @Test func scriptCurrentColor32() async throws {
+ // Invalid shape fill-opacity token should be ignored while currentColor updates still apply.
+ try await compareToReference("script-currentcolor-32")
+ }
+
+ @Test func scriptCurrentColor33() async throws {
+ // Invalid text fill-opacity token should be ignored after a valid opacity update.
+ try await compareToReference("script-currentcolor-33")
+ }
+
+ @Test func scriptCurrentColor34() async throws {
+ // Invalid text stroke-opacity token should be ignored across none -> currentColor relink.
+ try await compareToReference("script-currentcolor-34")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-32.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-32.ref
new file mode 100644
index 00000000..25151298
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-32.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 10, y: 10, width: 100, height: 100, fill: "red" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-33.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-33.ref
new file mode 100644
index 00000000..776771f1
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-33.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ fill: "lime",
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-34.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-34.ref
new file mode 100644
index 00000000..a73793bb
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-34.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ stroke: { fill: "red" },
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-32.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-32.svg
new file mode 100644
index 00000000..c07fe96e
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-32.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-33.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-33.svg
new file mode 100644
index 00000000..dbfa89ba
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-33.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-34.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-34.svg
new file mode 100644
index 00000000..87387eb4
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-34.svg
@@ -0,0 +1,11 @@
+
From 4b183998d415379425584a508d8ffa044a77c110 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:43:52 +0200
Subject: [PATCH 21/27] Add invalid color currentColor controls
---
GenerateReferencesCLI/cli.swift | 3 +++
Tests/SVGViewTests/SVGCustomTests.swift | 15 +++++++++++++++
.../w3c/Custom/refs/script-currentcolor-35.ref | 8 ++++++++
.../w3c/Custom/refs/script-currentcolor-36.ref | 14 ++++++++++++++
.../w3c/Custom/refs/script-currentcolor-37.ref | 14 ++++++++++++++
.../w3c/Custom/svg/script-currentcolor-35.svg | 9 +++++++++
.../w3c/Custom/svg/script-currentcolor-36.svg | 9 +++++++++
.../w3c/Custom/svg/script-currentcolor-37.svg | 11 +++++++++++
8 files changed, 83 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-35.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-36.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-37.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-35.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-36.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-37.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index a0518066..38bc0c16 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -201,6 +201,9 @@ struct cli: ParsableCommand {
"script-currentcolor-32",
"script-currentcolor-33",
"script-currentcolor-34",
+ "script-currentcolor-35",
+ "script-currentcolor-36",
+ "script-currentcolor-37",
]
mutating func run() throws {
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index f67d9cc9..e22ca68a 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -254,4 +254,19 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-34")
}
+ @Test func scriptCurrentColor35() async throws {
+ // Invalid shape color token should be ignored while later valid color updates still apply.
+ try await compareToReference("script-currentcolor-35")
+ }
+
+ @Test func scriptCurrentColor36() async throws {
+ // Invalid text color token should not block later fill-opacity and valid color updates.
+ try await compareToReference("script-currentcolor-36")
+ }
+
+ @Test func scriptCurrentColor37() async throws {
+ // Invalid text stroke color token should be ignored across none -> currentColor relink.
+ try await compareToReference("script-currentcolor-37")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-35.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-35.ref
new file mode 100644
index 00000000..2200198f
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-35.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 20, y: 20, width: 80, height: 80, fill: "red" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-36.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-36.ref
new file mode 100644
index 00000000..776771f1
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-36.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ fill: "lime",
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-37.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-37.ref
new file mode 100644
index 00000000..a73793bb
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-37.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ stroke: { fill: "red" },
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-35.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-35.svg
new file mode 100644
index 00000000..0855df8b
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-35.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-36.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-36.svg
new file mode 100644
index 00000000..7b708147
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-36.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-37.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-37.svg
new file mode 100644
index 00000000..8ea01ab7
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-37.svg
@@ -0,0 +1,11 @@
+
From 62e23713c49dbbc162826c5956c5efd43f02a2aa Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:45:52 +0200
Subject: [PATCH 22/27] Add currentColor edge color token controls
---
GenerateReferencesCLI/cli.swift | 3 +++
Tests/SVGViewTests/SVGCustomTests.swift | 15 +++++++++++++++
.../w3c/Custom/refs/script-currentcolor-38.ref | 8 ++++++++
.../w3c/Custom/refs/script-currentcolor-39.ref | 14 ++++++++++++++
.../w3c/Custom/refs/script-currentcolor-40.ref | 14 ++++++++++++++
.../w3c/Custom/svg/script-currentcolor-38.svg | 9 +++++++++
.../w3c/Custom/svg/script-currentcolor-39.svg | 9 +++++++++
.../w3c/Custom/svg/script-currentcolor-40.svg | 11 +++++++++++
8 files changed, 83 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-38.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-39.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-40.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-38.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-39.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-40.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 38bc0c16..4dc0949b 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -204,6 +204,9 @@ struct cli: ParsableCommand {
"script-currentcolor-35",
"script-currentcolor-36",
"script-currentcolor-37",
+ "script-currentcolor-38",
+ "script-currentcolor-39",
+ "script-currentcolor-40",
]
mutating func run() throws {
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index e22ca68a..70827c2d 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -269,4 +269,19 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-37")
}
+ @Test func scriptCurrentColor38() async throws {
+ // Empty color token should be ignored while a later valid color update applies.
+ try await compareToReference("script-currentcolor-38")
+ }
+
+ @Test func scriptCurrentColor39() async throws {
+ // Whitespace color token should not block later fill-opacity and valid color updates.
+ try await compareToReference("script-currentcolor-39")
+ }
+
+ @Test func scriptCurrentColor40() async throws {
+ // Quoted color token should be ignored across stroke none -> currentColor relink.
+ try await compareToReference("script-currentcolor-40")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-38.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-38.ref
new file mode 100644
index 00000000..2200198f
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-38.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 20, y: 20, width: 80, height: 80, fill: "red" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-39.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-39.ref
new file mode 100644
index 00000000..776771f1
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-39.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ fill: "lime",
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-40.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-40.ref
new file mode 100644
index 00000000..a73793bb
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-40.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ stroke: { fill: "red" },
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-38.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-38.svg
new file mode 100644
index 00000000..825235a5
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-38.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-39.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-39.svg
new file mode 100644
index 00000000..49531ad4
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-39.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-40.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-40.svg
new file mode 100644
index 00000000..1d7d8087
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-40.svg
@@ -0,0 +1,11 @@
+
From c62a294fd715a3e565ccf8cdfec15c04c00ae71e Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:47:49 +0200
Subject: [PATCH 23/27] Add malformed currentColor token controls
---
GenerateReferencesCLI/cli.swift | 3 +++
Tests/SVGViewTests/SVGCustomTests.swift | 15 +++++++++++++++
.../w3c/Custom/refs/script-currentcolor-41.ref | 8 ++++++++
.../w3c/Custom/refs/script-currentcolor-42.ref | 14 ++++++++++++++
.../w3c/Custom/refs/script-currentcolor-43.ref | 14 ++++++++++++++
.../w3c/Custom/svg/script-currentcolor-41.svg | 9 +++++++++
.../w3c/Custom/svg/script-currentcolor-42.svg | 9 +++++++++
.../w3c/Custom/svg/script-currentcolor-43.svg | 11 +++++++++++
8 files changed, 83 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-41.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-42.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-43.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-41.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-42.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-43.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 4dc0949b..b74f3715 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -207,6 +207,9 @@ struct cli: ParsableCommand {
"script-currentcolor-38",
"script-currentcolor-39",
"script-currentcolor-40",
+ "script-currentcolor-41",
+ "script-currentcolor-42",
+ "script-currentcolor-43",
]
mutating func run() throws {
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index 70827c2d..f47a574e 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -284,4 +284,19 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-40")
}
+ @Test func scriptCurrentColor41() async throws {
+ // Out-of-range rgb token should be ignored while a later valid color update applies.
+ try await compareToReference("script-currentcolor-41")
+ }
+
+ @Test func scriptCurrentColor42() async throws {
+ // Negative rgb token should not block later fill-opacity and valid color updates.
+ try await compareToReference("script-currentcolor-42")
+ }
+
+ @Test func scriptCurrentColor43() async throws {
+ // Malformed hex token should be ignored across stroke none -> currentColor relink.
+ try await compareToReference("script-currentcolor-43")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-41.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-41.ref
new file mode 100644
index 00000000..2200198f
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-41.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 20, y: 20, width: 80, height: 80, fill: "red" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-42.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-42.ref
new file mode 100644
index 00000000..776771f1
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-42.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ fill: "lime",
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-43.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-43.ref
new file mode 100644
index 00000000..a73793bb
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-43.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ stroke: { fill: "red" },
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-41.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-41.svg
new file mode 100644
index 00000000..e193ce54
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-41.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-42.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-42.svg
new file mode 100644
index 00000000..1cfc78ec
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-42.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-43.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-43.svg
new file mode 100644
index 00000000..2f80bb74
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-43.svg
@@ -0,0 +1,11 @@
+
From 48e9cb957fa0c1b42930bea852ca8e8939b70113 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:49:02 +0200
Subject: [PATCH 24/27] Add function-notation currentColor controls
---
GenerateReferencesCLI/cli.swift | 3 +++
Tests/SVGViewTests/SVGCustomTests.swift | 15 +++++++++++++++
.../w3c/Custom/refs/script-currentcolor-44.ref | 8 ++++++++
.../w3c/Custom/refs/script-currentcolor-45.ref | 14 ++++++++++++++
.../w3c/Custom/refs/script-currentcolor-46.ref | 14 ++++++++++++++
.../w3c/Custom/svg/script-currentcolor-44.svg | 9 +++++++++
.../w3c/Custom/svg/script-currentcolor-45.svg | 9 +++++++++
.../w3c/Custom/svg/script-currentcolor-46.svg | 11 +++++++++++
8 files changed, 83 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-44.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-45.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-46.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-44.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-45.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-46.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index b74f3715..6a1b4182 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -210,6 +210,9 @@ struct cli: ParsableCommand {
"script-currentcolor-41",
"script-currentcolor-42",
"script-currentcolor-43",
+ "script-currentcolor-44",
+ "script-currentcolor-45",
+ "script-currentcolor-46",
]
mutating func run() throws {
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index f47a574e..2603464c 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -299,4 +299,19 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-43")
}
+ @Test func scriptCurrentColor44() async throws {
+ // Malformed rgb argument count should be ignored while later valid color applies.
+ try await compareToReference("script-currentcolor-44")
+ }
+
+ @Test func scriptCurrentColor45() async throws {
+ // Out-of-range rgba alpha should not block later fill-opacity and valid color updates.
+ try await compareToReference("script-currentcolor-45")
+ }
+
+ @Test func scriptCurrentColor46() async throws {
+ // Malformed hsl argument count should be ignored across stroke none -> currentColor relink.
+ try await compareToReference("script-currentcolor-46")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-44.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-44.ref
new file mode 100644
index 00000000..2200198f
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-44.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 20, y: 20, width: 80, height: 80, fill: "red" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-45.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-45.ref
new file mode 100644
index 00000000..776771f1
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-45.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ fill: "lime",
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-46.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-46.ref
new file mode 100644
index 00000000..a73793bb
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-46.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ stroke: { fill: "red" },
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-44.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-44.svg
new file mode 100644
index 00000000..bb50a2db
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-44.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-45.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-45.svg
new file mode 100644
index 00000000..a7c51a1e
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-45.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-46.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-46.svg
new file mode 100644
index 00000000..dad79946
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-46.svg
@@ -0,0 +1,11 @@
+
From 571bed81a6ee6ab5e4c224210e578fdce8f3c23d Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 10:53:30 +0200
Subject: [PATCH 25/27] Add CSS-wide keyword currentColor controls
---
GenerateReferencesCLI/cli.swift | 3 +++
Tests/SVGViewTests/SVGCustomTests.swift | 15 +++++++++++++++
.../w3c/Custom/refs/script-currentcolor-47.ref | 8 ++++++++
.../w3c/Custom/refs/script-currentcolor-48.ref | 14 ++++++++++++++
.../w3c/Custom/refs/script-currentcolor-49.ref | 14 ++++++++++++++
.../w3c/Custom/svg/script-currentcolor-47.svg | 9 +++++++++
.../w3c/Custom/svg/script-currentcolor-48.svg | 9 +++++++++
.../w3c/Custom/svg/script-currentcolor-49.svg | 11 +++++++++++
8 files changed, 83 insertions(+)
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-47.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-48.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-49.ref
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-47.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-48.svg
create mode 100644 Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-49.svg
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 6a1b4182..de0b2d15 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -213,6 +213,9 @@ struct cli: ParsableCommand {
"script-currentcolor-44",
"script-currentcolor-45",
"script-currentcolor-46",
+ "script-currentcolor-47",
+ "script-currentcolor-48",
+ "script-currentcolor-49",
]
mutating func run() throws {
diff --git a/Tests/SVGViewTests/SVGCustomTests.swift b/Tests/SVGViewTests/SVGCustomTests.swift
index 2603464c..ca1a0f4c 100644
--- a/Tests/SVGViewTests/SVGCustomTests.swift
+++ b/Tests/SVGViewTests/SVGCustomTests.swift
@@ -314,4 +314,19 @@ struct SVGCustomTests: SVGTestHelper {
try await compareToReference("script-currentcolor-46")
}
+ @Test func scriptCurrentColor47() async throws {
+ // CSS-wide inherit keyword token should not block later fill-opacity and valid color updates.
+ try await compareToReference("script-currentcolor-47")
+ }
+
+ @Test func scriptCurrentColor48() async throws {
+ // CSS-wide initial keyword token should be ignored while later valid color applies.
+ try await compareToReference("script-currentcolor-48")
+ }
+
+ @Test func scriptCurrentColor49() async throws {
+ // CSS-wide unset keyword token should be ignored across stroke none -> currentColor relink.
+ try await compareToReference("script-currentcolor-49")
+ }
+
}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-47.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-47.ref
new file mode 100644
index 00000000..2200198f
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-47.ref
@@ -0,0 +1,8 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGRect { id: "target", x: 20, y: 20, width: 80, height: 80, fill: "red" }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-48.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-48.ref
new file mode 100644
index 00000000..776771f1
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-48.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ fill: "lime",
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-49.ref b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-49.ref
new file mode 100644
index 00000000..a73793bb
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/refs/script-currentcolor-49.ref
@@ -0,0 +1,14 @@
+SVGViewport {
+ width: "120",
+ height: "120",
+ scaling: "none",
+ contents: [
+ SVGText {
+ id: "target",
+ text: "X",
+ font: { },
+ stroke: { fill: "red" },
+ transform: [1, 0, 0, 1, 10, 70]
+ }
+ ]
+}
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-47.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-47.svg
new file mode 100644
index 00000000..299fdaf5
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-47.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-48.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-48.svg
new file mode 100644
index 00000000..9b0f4b26
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-48.svg
@@ -0,0 +1,9 @@
+
diff --git a/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-49.svg b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-49.svg
new file mode 100644
index 00000000..8c581fbb
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/Custom/svg/script-currentcolor-49.svg
@@ -0,0 +1,11 @@
+
From ca9c621a76d66ae8c8fe7c95dd9dd1fa8ec71705 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 12:55:29 +0200
Subject: [PATCH 26/27] Implment coords-dom-03-f
---
GenerateReferencesCLI/cli.swift | 1 +
.../SVG/Scripting/SVGScriptRunner.swift | 98 ++++++++++++++++---
Tests/SVGViewTests/SVG11Tests.swift | 1 +
.../w3c/1.1F2/refs/coords-dom-03-f.ref | 64 ++++++++++++
w3c-coverage.md | 6 +-
5 files changed, 153 insertions(+), 17 deletions(-)
create mode 100644 Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-03-f.ref
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index de0b2d15..9c894bbd 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -19,6 +19,7 @@ struct cli: ParsableCommand {
"coords-coord-02-t",
"coords-dom-01-f",
"coords-dom-02-f",
+ "coords-dom-03-f",
"coords-trans-01-b",
"coords-trans-02-t",
"coords-trans-03-t",
diff --git a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
index 416d4282..88f6db9e 100644
--- a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
+++ b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
@@ -4,11 +4,14 @@ import Foundation
import JavaScriptCore
@objc protocol SVGJSDocumentExports: JSExport {
+ var documentElement: SVGJSElement? { get }
func getElementById(_ id: String) -> SVGJSElement?
}
@objc protocol SVGJSElementExports: JSExport {
var transform: SVGJSTransformListContainer? { get }
+ func createSVGMatrix() -> SVGJSMatrix
+ func createSVGTransformFromMatrix(_ matrix: SVGJSMatrix) -> SVGJSTransform
func setAttribute(_ name: String, _ value: String)
}
@@ -18,6 +21,7 @@ import JavaScriptCore
@objc protocol SVGJSTransformListExports: JSExport {
func getItem(_ index: Int) -> SVGJSTransform?
+ func createSVGTransformFromMatrix(_ matrix: SVGJSMatrix) -> SVGJSTransform
}
@objc protocol SVGJSTransformExports: JSExport {
@@ -26,6 +30,7 @@ import JavaScriptCore
func setTranslate(_ tx: Double, _ ty: Double)
func setScale(_ sx: Double, _ sy: Double)
func setRotate(_ angle: Double, _ cx: Double, _ cy: Double)
+ func setMatrix(_ matrix: SVGJSMatrix)
}
@objc protocol SVGJSMatrixExports: JSExport {
@@ -45,6 +50,10 @@ final class SVGJSDocument: NSObject, SVGJSDocumentExports {
self.root = root
}
+ var documentElement: SVGJSElement? {
+ SVGJSElement(node: root)
+ }
+
func getElementById(_ id: String) -> SVGJSElement? {
guard let node = root.getNode(byId: id) else {
return nil
@@ -65,6 +74,14 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
SVGJSTransformListContainer(node: node)
}
+ func createSVGMatrix() -> SVGJSMatrix {
+ SVGJSMatrix()
+ }
+
+ func createSVGTransformFromMatrix(_ matrix: SVGJSMatrix) -> SVGJSTransform {
+ SVGJSTransform(components: matrix.componentsSnapshot())
+ }
+
func setAttribute(_ name: String, _ value: String) {
let normalizedName = name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let normalizedValue = value.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -459,6 +476,10 @@ final class SVGJSTransformList: NSObject, SVGJSTransformListExports {
func getItem(_ index: Int) -> SVGJSTransform? {
index == 0 ? transform : nil
}
+
+ func createSVGTransformFromMatrix(_ matrix: SVGJSMatrix) -> SVGJSTransform {
+ SVGJSTransform(components: matrix.componentsSnapshot())
+ }
}
@objcMembers
@@ -480,9 +501,9 @@ final class SVGJSTransform: NSObject, SVGJSTransformExports {
matrixModel
}
- init(node: SVGNode) {
+ init(node: SVGNode?) {
self.node = node
- let t = node.transform
+ let t = node?.transform ?? .identity
self.components = (
a: Double(t.a),
b: Double(t.b),
@@ -495,6 +516,13 @@ final class SVGJSTransform: NSObject, SVGJSTransformExports {
self.matrixModel = SVGJSMatrix(owner: self)
}
+ init(components: (a: Double, b: Double, c: Double, d: Double, e: Double, f: Double)) {
+ self.node = nil
+ self.components = components
+ super.init()
+ self.matrixModel = SVGJSMatrix(owner: self)
+ }
+
func setTranslate(_ tx: Double, _ ty: Double) {
type = Self.svgTransformTranslate
components = (a: 1, b: 0, c: 0, d: 1, e: tx, f: ty)
@@ -524,6 +552,12 @@ final class SVGJSTransform: NSObject, SVGJSTransformExports {
applyToNode()
}
+ func setMatrix(_ matrix: SVGJSMatrix) {
+ type = Self.svgTransformMatrix
+ components = matrix.componentsSnapshot()
+ applyToNode()
+ }
+
func setComponent(_ keyPath: WritableKeyPath<(a: Double, b: Double, c: Double, d: Double, e: Double, f: Double), Double>, _ value: Double) {
type = Self.svgTransformMatrix
components[keyPath: keyPath] = value
@@ -549,39 +583,75 @@ final class SVGJSTransform: NSObject, SVGJSTransformExports {
@objcMembers
final class SVGJSMatrix: NSObject, SVGJSMatrixExports {
private weak var owner: SVGJSTransform?
+ private var detachedComponents: (a: Double, b: Double, c: Double, d: Double, e: Double, f: Double)
+
+ override init() {
+ self.detachedComponents = (a: 1, b: 0, c: 0, d: 1, e: 0, f: 0)
+ super.init()
+ }
init(owner: SVGJSTransform) {
self.owner = owner
+ self.detachedComponents = (a: 1, b: 0, c: 0, d: 1, e: 0, f: 0)
+ }
+
+ func componentsSnapshot() -> (a: Double, b: Double, c: Double, d: Double, e: Double, f: Double) {
+ if let owner {
+ return (
+ a: owner.component(\.a),
+ b: owner.component(\.b),
+ c: owner.component(\.c),
+ d: owner.component(\.d),
+ e: owner.component(\.e),
+ f: owner.component(\.f)
+ )
+ }
+ return detachedComponents
+ }
+
+ private func setComponent(_ keyPath: WritableKeyPath<(a: Double, b: Double, c: Double, d: Double, e: Double, f: Double), Double>, _ value: Double) {
+ if let owner {
+ owner.setComponent(keyPath, value)
+ return
+ }
+ detachedComponents[keyPath: keyPath] = value
+ }
+
+ private func component(_ keyPath: KeyPath<(a: Double, b: Double, c: Double, d: Double, e: Double, f: Double), Double>) -> Double {
+ if let owner {
+ return owner.component(keyPath)
+ }
+ return detachedComponents[keyPath: keyPath]
}
var a: Double {
- get { owner?.component(\.a) ?? 1 }
- set { owner?.setComponent(\.a, newValue) }
+ get { component(\.a) }
+ set { setComponent(\.a, newValue) }
}
var b: Double {
- get { owner?.component(\.b) ?? 0 }
- set { owner?.setComponent(\.b, newValue) }
+ get { component(\.b) }
+ set { setComponent(\.b, newValue) }
}
var c: Double {
- get { owner?.component(\.c) ?? 0 }
- set { owner?.setComponent(\.c, newValue) }
+ get { component(\.c) }
+ set { setComponent(\.c, newValue) }
}
var d: Double {
- get { owner?.component(\.d) ?? 1 }
- set { owner?.setComponent(\.d, newValue) }
+ get { component(\.d) }
+ set { setComponent(\.d, newValue) }
}
var e: Double {
- get { owner?.component(\.e) ?? 0 }
- set { owner?.setComponent(\.e, newValue) }
+ get { component(\.e) }
+ set { setComponent(\.e, newValue) }
}
var f: Double {
- get { owner?.component(\.f) ?? 0 }
- set { owner?.setComponent(\.f, newValue) }
+ get { component(\.f) }
+ set { setComponent(\.f, newValue) }
}
}
#endif
diff --git a/Tests/SVGViewTests/SVG11Tests.swift b/Tests/SVGViewTests/SVG11Tests.swift
index ecb77d5c..8e6850b2 100644
--- a/Tests/SVGViewTests/SVG11Tests.swift
+++ b/Tests/SVGViewTests/SVG11Tests.swift
@@ -23,6 +23,7 @@ struct SVG11Tests {
@Test func coordsCoord02T() async throws { try await compareToReference("coords-coord-02-t") }
@Test func coordsDom01F() async throws { try await compareToReference("coords-dom-01-f") }
@Test func coordsDom02F() async throws { try await compareToReference("coords-dom-02-f") }
+ @Test func coordsDom03F() async throws { try await compareToReference("coords-dom-03-f") }
@Test func coordsTrans01B() async throws { try await compareToReference("coords-trans-01-b") }
@Test func coordsTrans02T() async throws { try await compareToReference("coords-trans-02-t") }
@Test func coordsTrans03T() async throws { try await compareToReference("coords-trans-03-t") }
diff --git a/Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-03-f.ref b/Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-03-f.ref
new file mode 100644
index 00000000..26430164
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-03-f.ref
@@ -0,0 +1,64 @@
+SVGViewport {
+ id: "svg-root",
+ viewBox: { width: 480, height: 360 },
+ scaling: "none",
+ contents: [
+ SVGDefs { },
+ SVGGroup {
+ id: "test-body-content",
+ contents: [
+ SVGText {
+ text: "Test that some methods taking an SVGMatrix take a copy of it",
+ font: { name: "SVGFreeSansASCII,sans-serif", size: 14 },
+ fill: "black",
+ transform: [1, 0, 0, 1, 10, 30]
+ },
+ SVGRect { id: "r1", x: 10, y: 50, width: 50, height: 50, fill: "lime" },
+ SVGRect { id: "r2", x: 10, y: 110, width: 50, height: 50, fill: "lime" },
+ SVGRect { id: "r3", x: 10, y: 170, width: 50, height: 50, fill: "lime" },
+ SVGGroup {
+ contents: [
+ SVGText {
+ text: "SVGTransformList.createSVGTransformFromMatrix()",
+ font: { name: "SVGFreeSansASCII,sans-serif", size: 10 },
+ fill: "black",
+ transform: [1, 0, 0, 1, 70, 80]
+ },
+ SVGText {
+ text: "SVGSVGElement.createSVGTransformFromMatrix()",
+ font: { name: "SVGFreeSansASCII,sans-serif", size: 10 },
+ fill: "black",
+ transform: [1, 0, 0, 1, 70, 140]
+ },
+ SVGText {
+ text: "SVGTransform.setMatrix()",
+ font: { name: "SVGFreeSansASCII,sans-serif", size: 10 },
+ fill: "black",
+ transform: [1, 0, 0, 1, 70, 200]
+ }
+ ]
+ },
+ SVGGroup { id: "g", transform: [3, 0, 0, 1, 0, 0] }
+ ]
+ },
+ SVGGroup {
+ contents: [
+ SVGText {
+ id: "revision",
+ text: "$Revision: 1.7 $",
+ font: { name: "SVGFreeSansASCII,sans-serif", size: 32 },
+ fill: "black",
+ transform: [1, 0, 0, 1, 10, 340]
+ }
+ ]
+ },
+ SVGRect {
+ id: "test-frame",
+ x: 1,
+ y: 1,
+ width: 478,
+ height: 358,
+ stroke: { fill: "black" }
+ }
+ ]
+}
diff --git a/w3c-coverage.md b/w3c-coverage.md
index 7d6a6ad8..fd1f8b31 100644
--- a/w3c-coverage.md
+++ b/w3c-coverage.md
@@ -167,10 +167,10 @@ This page is automatically generated and shows actual coverage of the [W3C SVG T
|❌|[conform-viewers-03-f](Tests/SVGViewTests/w3c/1.1F2/svg/conform-viewers-03-f.svg)|
-### [Coords](https://www.w3.org/TR/SVG11/coords.html): `78.1%`
+### [Coords](https://www.w3.org/TR/SVG11/coords.html): `81.3%`
- (25/32) tests covered...
+ (26/32) tests covered...
|Status | Name|
|------|-------|
@@ -178,7 +178,7 @@ This page is automatically generated and shows actual coverage of the [W3C SVG T
|✅|[coords-coord-02-t](Tests/SVGViewTests/w3c/1.1F2/svg/coords-coord-02-t.svg)|
|✅|[coords-dom-01-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-01-f.svg)|
|✅|[coords-dom-02-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-02-f.svg)|
-|❌|[coords-dom-03-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-03-f.svg)|
+|✅|[coords-dom-03-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-03-f.svg)|
|❌|[coords-dom-04-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-04-f.svg)|
|✅|[coords-trans-01-b](Tests/SVGViewTests/w3c/1.1F2/svg/coords-trans-01-b.svg)|
|✅|[coords-trans-02-t](Tests/SVGViewTests/w3c/1.1F2/svg/coords-trans-02-t.svg)|
From d0d1c7a5f72f515d8f970739e5bd0b0f4cf259d4 Mon Sep 17 00:00:00 2001
From: Mathias Amnell <104110+Amnell@users.noreply.github.com>
Date: Mon, 25 May 2026 14:26:04 +0200
Subject: [PATCH 27/27] Implement coords-dom-04-f DOM scripting support
---
GenerateReferencesCLI/cli.swift | 1 +
Source/Model/Nodes/SVGNode.swift | 5 +-
.../SVG/Elements/SVGElementParser.swift | 7 +
Source/Parser/SVG/SVGParserPrimitives.swift | 75 +++++++++
.../SVG/Scripting/SVGScriptRunner.swift | 146 +++++++++++++++++-
Tests/SVGViewTests/SVG11Tests.swift | 1 +
.../w3c/1.1F2/refs/coords-dom-04-f.ref | 131 ++++++++++++++++
w3c-coverage.md | 6 +-
8 files changed, 364 insertions(+), 8 deletions(-)
create mode 100644 Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-04-f.ref
diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift
index 9c894bbd..22067d42 100644
--- a/GenerateReferencesCLI/cli.swift
+++ b/GenerateReferencesCLI/cli.swift
@@ -20,6 +20,7 @@ struct cli: ParsableCommand {
"coords-dom-01-f",
"coords-dom-02-f",
"coords-dom-03-f",
+ "coords-dom-04-f",
"coords-trans-01-b",
"coords-trans-02-t",
"coords-trans-03-t",
diff --git a/Source/Model/Nodes/SVGNode.swift b/Source/Model/Nodes/SVGNode.swift
index cec0d173..1b20910b 100644
--- a/Source/Model/Nodes/SVGNode.swift
+++ b/Source/Model/Nodes/SVGNode.swift
@@ -9,6 +9,7 @@ public class SVGNode: SerializableElement {
#if os(WASI) || os(Linux)
public var transform: CGAffineTransform = CGAffineTransform.identity
+ public var scriptTransforms: [CGAffineTransform] = []
public var opaque: Bool
public var opacity: Double
public var currentColor: SVGColor?
@@ -21,6 +22,7 @@ public class SVGNode: SerializableElement {
public var markerEnd: String?
#else
@Published public var transform: CGAffineTransform = CGAffineTransform.identity
+ @Published public var scriptTransforms: [CGAffineTransform] = []
@Published public var opaque: Bool
@Published public var opacity: Double
@Published public var currentColor: SVGColor?
@@ -34,8 +36,9 @@ public class SVGNode: SerializableElement {
#endif
- public init(transform: CGAffineTransform = .identity, opaque: Bool = true, opacity: Double = 1, currentColor: SVGColor? = nil, clip: SVGNode? = nil, mask: SVGNode? = nil, id: String? = nil, markerStart: String? = nil, markerMid: String? = nil, markerEnd: String? = nil) {
+ public init(transform: CGAffineTransform = .identity, scriptTransforms: [CGAffineTransform] = [], opaque: Bool = true, opacity: Double = 1, currentColor: SVGColor? = nil, clip: SVGNode? = nil, mask: SVGNode? = nil, id: String? = nil, markerStart: String? = nil, markerMid: String? = nil, markerEnd: String? = nil) {
self.transform = transform
+ self.scriptTransforms = scriptTransforms
self.opaque = opaque
self.opacity = opacity
self.currentColor = currentColor
diff --git a/Source/Parser/SVG/Elements/SVGElementParser.swift b/Source/Parser/SVG/Elements/SVGElementParser.swift
index 791ce84c..03b5185e 100644
--- a/Source/Parser/SVG/Elements/SVGElementParser.swift
+++ b/Source/Parser/SVG/Elements/SVGElementParser.swift
@@ -19,7 +19,14 @@ class SVGBaseElementParser: SVGElementParser {
guard let node = doParse(context: context, delegate: delegate) else { return nil }
let transform = SVGHelper.parseTransform(context.properties["transform"] ?? "")
node.transform = node.transform.concatenating(transform)
+ node.scriptTransforms = SVGHelper.parseTransformOperations(context.properties["transform"] ?? "")
node.opacity = SVGHelper.parseOpacity(context.properties, "opacity")
+ if let visibility = context.style("visibility")?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(), visibility == "hidden" {
+ node.opaque = false
+ }
+ if let display = context.style("display")?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(), display == "none" {
+ node.opaque = false
+ }
if let colorValue = context.style("color") {
node.currentColor = SVGHelper.parseColor(colorValue, context.styles)
}
diff --git a/Source/Parser/SVG/SVGParserPrimitives.swift b/Source/Parser/SVG/SVGParserPrimitives.swift
index e1376b56..25aecf46 100644
--- a/Source/Parser/SVG/SVGParserPrimitives.swift
+++ b/Source/Parser/SVG/SVGParserPrimitives.swift
@@ -163,6 +163,81 @@ public class SVGHelper: NSObject {
return parseTransform(newAttributeString, transform: finalTransform)
}
+ static func parseTransformOperations(_ attributes: String, collectedOperations: [CGAffineTransform] = []) -> [CGAffineTransform] {
+ guard let matcher = SVGParserRegexHelper.getTransformAttributeMatcher() else {
+ return collectedOperations
+ }
+
+ let attributes = attributes.replacingOccurrences(of: "\n", with: "")
+ var updatedOperations = collectedOperations
+ let fullRange = NSRange(location: 0, length: attributes.count)
+
+ guard let matchedAttribute = matcher.firstMatch(in: attributes, options: .reportCompletion, range: fullRange) else {
+ return updatedOperations
+ }
+
+ let attributeName = (attributes as NSString).substring(with: matchedAttribute.range(at: 1))
+ let values = parseTransformValues((attributes as NSString).substring(with: matchedAttribute.range(at: 2)))
+ if values.isEmpty {
+ return updatedOperations
+ }
+
+ var operation = CGAffineTransform.identity
+ switch attributeName {
+ case "translate":
+ if let x = values[0].cgFloatValue {
+ var y: CGFloat = 0
+ if values.indices.contains(1) {
+ y = values[1].cgFloatValue ?? 0
+ }
+ operation = operation.translatedBy(x: x, y: y)
+ }
+ case "scale":
+ if let x = values[0].cgFloatValue {
+ var y = x
+ if values.indices.contains(1) {
+ y = values[1].cgFloatValue ?? x
+ }
+ operation = operation.scaledBy(x: x, y: y)
+ }
+ case "rotate":
+ if let angle = values[0].cgFloatValue {
+ if values.count == 1 {
+ operation = operation.rotated(by: angle.degreesToRadians)
+ } else if values.count == 3, let x = values[1].cgFloatValue, let y = values[2].cgFloatValue {
+ operation = operation.translatedBy(x: x, y: y).rotated(by: angle.degreesToRadians).translatedBy(x: -x, y: -y)
+ }
+ }
+ case "skewX":
+ if let x = values[0].cgFloatValue {
+ let v = tan((x * .pi) / 180.0)
+ operation = operation.shear(shx: v, shy: 0)
+ }
+ case "skewY":
+ if let y = values[0].cgFloatValue {
+ let v = tan((y * .pi) / 180.0)
+ operation = operation.shear(shx: 0, shy: v)
+ }
+ case "matrix":
+ if values.count != 6 {
+ return updatedOperations
+ }
+ if let a = values[0].cgFloatValue, let b = values[1].cgFloatValue,
+ let c = values[2].cgFloatValue, let d = values[3].cgFloatValue,
+ let tx = values[4].cgFloatValue, let ty = values[5].cgFloatValue {
+ operation = CGAffineTransform(a: a, b: b, c: c, d: d, tx: tx, ty: ty)
+ }
+ default:
+ break
+ }
+
+ updatedOperations.append(operation)
+ let rangeToRemove = NSRange(location: 0,
+ length: matchedAttribute.range.location + matchedAttribute.range.length)
+ let newAttributeString = (attributes as NSString).replacingCharacters(in: rangeToRemove, with: "")
+ return parseTransformOperations(newAttributeString, collectedOperations: updatedOperations)
+ }
+
static func parseTransformValues(_ values: String, collectedValues: [String] = []) -> [String] {
guard let matcher = SVGParserRegexHelper.getTransformMatcher() else {
return collectedValues
diff --git a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
index 88f6db9e..84490a86 100644
--- a/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
+++ b/Source/Parser/SVG/Scripting/SVGScriptRunner.swift
@@ -6,13 +6,17 @@ import JavaScriptCore
@objc protocol SVGJSDocumentExports: JSExport {
var documentElement: SVGJSElement? { get }
func getElementById(_ id: String) -> SVGJSElement?
+ func createElementNS(_ namespaceURI: String?, _ qualifiedName: String) -> SVGJSElement?
}
@objc protocol SVGJSElementExports: JSExport {
var transform: SVGJSTransformListContainer? { get }
+ var textContent: String { get set }
func createSVGMatrix() -> SVGJSMatrix
func createSVGTransformFromMatrix(_ matrix: SVGJSMatrix) -> SVGJSTransform
+ func appendChild(_ child: SVGJSElement) -> SVGJSElement?
func setAttribute(_ name: String, _ value: String)
+ func setAttributeNS(_ namespaceURI: String?, _ qualifiedName: String, _ value: String)
}
@objc protocol SVGJSTransformListContainerExports: JSExport {
@@ -22,6 +26,7 @@ import JavaScriptCore
@objc protocol SVGJSTransformListExports: JSExport {
func getItem(_ index: Int) -> SVGJSTransform?
func createSVGTransformFromMatrix(_ matrix: SVGJSMatrix) -> SVGJSTransform
+ func consolidate() -> SVGJSTransform?
}
@objc protocol SVGJSTransformExports: JSExport {
@@ -60,6 +65,25 @@ final class SVGJSDocument: NSObject, SVGJSDocumentExports {
}
return SVGJSElement(node: node)
}
+
+ func createElementNS(_ namespaceURI: String?, _ qualifiedName: String) -> SVGJSElement? {
+ _ = namespaceURI
+ let localName = qualifiedName
+ .split(separator: ":")
+ .last?
+ .lowercased() ?? qualifiedName.lowercased()
+
+ switch localName {
+ case "text":
+ return SVGJSElement(node: SVGText(text: ""))
+ case "rect":
+ return SVGJSElement(node: SVGRect())
+ case "g":
+ return SVGJSElement(node: SVGGroup(contents: []))
+ default:
+ return nil
+ }
+ }
}
@objcMembers
@@ -74,6 +98,15 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
SVGJSTransformListContainer(node: node)
}
+ var textContent: String {
+ get { (node as? SVGText)?.text ?? "" }
+ set {
+ if let textNode = node as? SVGText {
+ textNode.text = newValue
+ }
+ }
+ }
+
func createSVGMatrix() -> SVGJSMatrix {
SVGJSMatrix()
}
@@ -82,15 +115,33 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
SVGJSTransform(components: matrix.componentsSnapshot())
}
+ func appendChild(_ child: SVGJSElement) -> SVGJSElement? {
+ guard let group = node as? SVGGroup else {
+ return nil
+ }
+ group.contents.append(child.node)
+ return child
+ }
+
func setAttribute(_ name: String, _ value: String) {
let normalizedName = name.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
let normalizedValue = value.trimmingCharacters(in: .whitespacesAndNewlines)
switch normalizedName {
+ case "id":
+ node.id = normalizedValue
case "fill":
setFill(normalizedValue)
case "color":
setColor(normalizedValue)
+ case "x":
+ setX(normalizedValue)
+ case "y":
+ setY(normalizedValue)
+ case "width":
+ setWidth(normalizedValue)
+ case "height":
+ setHeight(normalizedValue)
case "opacity":
if let parsedOpacity = Double(normalizedValue) {
node.opacity = min(max(parsedOpacity, 0), 1)
@@ -101,6 +152,7 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
node.opaque = normalizedValue.lowercased() != "none"
case "transform":
node.transform = SVGHelper.parseTransform(normalizedValue)
+ node.scriptTransforms = SVGHelper.parseTransformOperations(normalizedValue)
case "stroke":
setStroke(normalizedValue)
case "stroke-width":
@@ -126,6 +178,47 @@ final class SVGJSElement: NSObject, SVGJSElementExports {
}
}
+ func setAttributeNS(_ namespaceURI: String?, _ qualifiedName: String, _ value: String) {
+ _ = namespaceURI
+ setAttribute(qualifiedName, value)
+ }
+
+ private func setX(_ value: String) {
+ guard let x = SVGHelper.doubleFromString(value) else { return }
+ if let rect = node as? SVGRect {
+ rect.x = CGFloat(x)
+ return
+ }
+ if let text = node as? SVGText {
+ text.transform.tx = CGFloat(x)
+ }
+ }
+
+ private func setY(_ value: String) {
+ guard let y = SVGHelper.doubleFromString(value) else { return }
+ if let rect = node as? SVGRect {
+ rect.y = CGFloat(y)
+ return
+ }
+ if let text = node as? SVGText {
+ text.transform.ty = CGFloat(y)
+ }
+ }
+
+ private func setWidth(_ value: String) {
+ guard let width = SVGHelper.doubleFromString(value),
+ let rect = node as? SVGRect
+ else { return }
+ rect.width = CGFloat(width)
+ }
+
+ private func setHeight(_ value: String) {
+ guard let height = SVGHelper.doubleFromString(value),
+ let rect = node as? SVGRect
+ else { return }
+ rect.height = CGFloat(height)
+ }
+
private func setColor(_ value: String) {
guard let color = SVGHelper.parseColor(value, [:]) else { return }
node.hasExplicitCurrentColor = true
@@ -467,19 +560,36 @@ final class SVGJSTransformListContainer: NSObject, SVGJSTransformListContainerEx
@objcMembers
final class SVGJSTransformList: NSObject, SVGJSTransformListExports {
- private let transform: SVGJSTransform
+ private var transforms: [SVGJSTransform]
init(node: SVGNode) {
- self.transform = SVGJSTransform(node: node)
+ if node.scriptTransforms.count > 1 {
+ self.transforms = node.scriptTransforms.map { SVGJSTransform(components: $0.svgComponents) }
+ } else {
+ self.transforms = [SVGJSTransform(node: node)]
+ }
}
func getItem(_ index: Int) -> SVGJSTransform? {
- index == 0 ? transform : nil
+ guard transforms.indices.contains(index) else { return nil }
+ return transforms[index]
}
func createSVGTransformFromMatrix(_ matrix: SVGJSMatrix) -> SVGJSTransform {
SVGJSTransform(components: matrix.componentsSnapshot())
}
+
+ func consolidate() -> SVGJSTransform? {
+ guard !transforms.isEmpty else {
+ return nil
+ }
+ let consolidated = transforms.reduce(CGAffineTransform.identity) { result, transform in
+ transform.affineTransform.concatenating(result)
+ }
+ let transform = SVGJSTransform(components: consolidated.svgComponents)
+ transforms = [transform]
+ return transform
+ }
}
@objcMembers
@@ -501,6 +611,17 @@ final class SVGJSTransform: NSObject, SVGJSTransformExports {
matrixModel
}
+ var affineTransform: CGAffineTransform {
+ CGAffineTransform(
+ a: CGFloat(components.a),
+ b: CGFloat(components.b),
+ c: CGFloat(components.c),
+ d: CGFloat(components.d),
+ tx: CGFloat(components.e),
+ ty: CGFloat(components.f)
+ )
+ }
+
init(node: SVGNode?) {
self.node = node
let t = node?.transform ?? .identity
@@ -654,6 +775,12 @@ final class SVGJSMatrix: NSObject, SVGJSMatrixExports {
set { setComponent(\.f, newValue) }
}
}
+
+private extension CGAffineTransform {
+ var svgComponents: (a: Double, b: Double, c: Double, d: Double, e: Double, f: Double) {
+ (a: Double(a), b: Double(b), c: Double(c), d: Double(d), e: Double(tx), f: Double(ty))
+ }
+}
#endif
enum SVGScriptRunner {
@@ -673,7 +800,8 @@ enum SVGScriptRunner {
static func executeIfNeeded(xmlRoot: XMLElement, nodeRoot: SVGNode, logger: SVGLogger) {
#if canImport(JavaScriptCore)
let scripts = collectScripts(from: xmlRoot)
- guard !scripts.isEmpty else { return }
+ let onLoadScript = normalizedOnLoadScript(from: xmlRoot)
+ guard !scripts.isEmpty || onLoadScript != nil else { return }
guard let context = JSContext() else {
logger.log(message: "Failed to create JavaScript context")
@@ -703,6 +831,10 @@ enum SVGScriptRunner {
for script in scripts {
_ = context.evaluateScript(script)
}
+
+ if let onLoadScript {
+ _ = context.evaluateScript(onLoadScript)
+ }
#else
_ = xmlRoot
_ = nodeRoot
@@ -764,4 +896,10 @@ enum SVGScriptRunner {
guard let typeOnly else { return nil }
return supportedScriptMIMETypes.contains(typeOnly) ? typeOnly : nil
}
+
+ private static func normalizedOnLoadScript(from root: XMLElement) -> String? {
+ let script = root.attributes["onload"]?.trimmingCharacters(in: .whitespacesAndNewlines)
+ guard let script, !script.isEmpty else { return nil }
+ return script
+ }
}
\ No newline at end of file
diff --git a/Tests/SVGViewTests/SVG11Tests.swift b/Tests/SVGViewTests/SVG11Tests.swift
index 8e6850b2..738f59e8 100644
--- a/Tests/SVGViewTests/SVG11Tests.swift
+++ b/Tests/SVGViewTests/SVG11Tests.swift
@@ -24,6 +24,7 @@ struct SVG11Tests {
@Test func coordsDom01F() async throws { try await compareToReference("coords-dom-01-f") }
@Test func coordsDom02F() async throws { try await compareToReference("coords-dom-02-f") }
@Test func coordsDom03F() async throws { try await compareToReference("coords-dom-03-f") }
+ @Test func coordsDom04F() async throws { try await compareToReference("coords-dom-04-f") }
@Test func coordsTrans01B() async throws { try await compareToReference("coords-trans-01-b") }
@Test func coordsTrans02T() async throws { try await compareToReference("coords-trans-02-t") }
@Test func coordsTrans03T() async throws { try await compareToReference("coords-trans-03-t") }
diff --git a/Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-04-f.ref b/Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-04-f.ref
new file mode 100644
index 00000000..1a5dc652
--- /dev/null
+++ b/Tests/SVGViewTests/w3c/1.1F2/refs/coords-dom-04-f.ref
@@ -0,0 +1,131 @@
+SVGViewport {
+ id: "svg-root",
+ viewBox: { width: 480, height: 360 },
+ scaling: "none",
+ contents: [
+ SVGDefs { },
+ SVGGroup {
+ id: "test-body-content",
+ contents: [
+ SVGDefs { },
+ SVGGroup {
+ transform: [1, 0, 0, 1, 20, -10],
+ contents: [
+ SVGGroup {
+ id: "subteststatus",
+ transform: [1, 0, 0, 1, 0, 40],
+ contents: [
+ SVGRect { id: "status", y: 5, width: 15, height: 15, fill: "lime" },
+ SVGText {
+ id: "scriptstatus",
+ text: "Scripting enabled",
+ font: { name: "SVGFreeSansASCII,sans-serif", size: 18 },
+ fill: "black",
+ transform: [1, 0, 0, 1, 20, 20]
+ },
+ SVGRect { y: 25, width: 15, height: 15, fill: "lime" },
+ SVGText {
+ text: "Passed subtest #1",
+ fill: "black",
+ transform: [1, 0, 0, 1, 20, 40]
+ },
+ SVGRect { y: 45, width: 15, height: 15, fill: "lime" },
+ SVGText {
+ text: "Passed subtest #2",
+ fill: "black",
+ transform: [1, 0, 0, 1, 20, 60]
+ },
+ SVGRect { y: 65, width: 15, height: 15, fill: "lime" },
+ SVGText {
+ text: "Passed subtest #3",
+ fill: "black",
+ transform: [1, 0, 0, 1, 20, 80]
+ },
+ SVGRect { y: 85, width: 15, height: 15, fill: "lime" },
+ SVGText {
+ text: "Passed subtest #4",
+ fill: "black",
+ transform: [1, 0, 0, 1, 20, 100]
+ },
+ SVGRect { y: 105, width: 15, height: 15, fill: "lime" },
+ SVGText {
+ text: "Passed subtest #5",
+ fill: "black",
+ transform: [1, 0, 0, 1, 20, 120]
+ },
+ SVGRect { y: 125, width: 15, height: 15, fill: "lime" },
+ SVGText {
+ text: "Passed subtest #6",
+ fill: "black",
+ transform: [1, 0, 0, 1, 20, 140]
+ },
+ SVGRect { y: 145, width: 15, height: 15, fill: "lime" },
+ SVGText {
+ text: "Passed subtest #7",
+ fill: "black",
+ transform: [1, 0, 0, 1, 20, 160]
+ },
+ SVGRect { y: 165, width: 15, height: 15, fill: "lime" },
+ SVGText {
+ text: "Passed subtest #8",
+ fill: "black",
+ transform: [1, 0, 0, 1, 20, 180]
+ },
+ SVGRect { y: 185, width: 15, height: 15, fill: "lime" },
+ SVGText {
+ text: "Passed subtest #9",
+ fill: "black",
+ transform: [1, 0, 0, 1, 20, 200]
+ },
+ SVGRect { y: 205, width: 15, height: 15, fill: "lime" },
+ SVGText {
+ text: "Passed subtest #10",
+ fill: "black",
+ transform: [1, 0, 0, 1, 20, 220]
+ },
+ SVGRect { y: 225, width: 15, height: 15, fill: "lime" },
+ SVGText {
+ text: "Passed subtest #11",
+ fill: "black",
+ transform: [1, 0, 0, 1, 20, 240]
+ },
+ SVGRect { y: 245, width: 15, height: 15, fill: "lime" },
+ SVGText {
+ text: "Passed subtest #12",
+ fill: "black",
+ transform: [1, 0, 0, 1, 20, 260]
+ }
+ ]
+ },
+ SVGPolyline {
+ id: "r",
+ points: [0, 0, 30, 40, 80, -20],
+ stroke: { fill: "green", width: 10 },
+ transform: [0, 1, -1, 0, 10, 10],
+ opaque: false
+ }
+ ]
+ }
+ ]
+ },
+ SVGGroup {
+ contents: [
+ SVGText {
+ id: "revision",
+ text: "$Revision: 1.5 $",
+ font: { name: "SVGFreeSansASCII,sans-serif", size: 32 },
+ fill: "black",
+ transform: [1, 0, 0, 1, 10, 340]
+ }
+ ]
+ },
+ SVGRect {
+ id: "test-frame",
+ x: 1,
+ y: 1,
+ width: 478,
+ height: 358,
+ stroke: { fill: "black" }
+ }
+ ]
+}
diff --git a/w3c-coverage.md b/w3c-coverage.md
index fd1f8b31..ca9bf51f 100644
--- a/w3c-coverage.md
+++ b/w3c-coverage.md
@@ -167,10 +167,10 @@ This page is automatically generated and shows actual coverage of the [W3C SVG T
|❌|[conform-viewers-03-f](Tests/SVGViewTests/w3c/1.1F2/svg/conform-viewers-03-f.svg)|
-### [Coords](https://www.w3.org/TR/SVG11/coords.html): `81.3%`
+### [Coords](https://www.w3.org/TR/SVG11/coords.html): `84.4%`
- (26/32) tests covered...
+ (27/32) tests covered...
|Status | Name|
|------|-------|
@@ -179,7 +179,7 @@ This page is automatically generated and shows actual coverage of the [W3C SVG T
|✅|[coords-dom-01-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-01-f.svg)|
|✅|[coords-dom-02-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-02-f.svg)|
|✅|[coords-dom-03-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-03-f.svg)|
-|❌|[coords-dom-04-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-04-f.svg)|
+|✅|[coords-dom-04-f](Tests/SVGViewTests/w3c/1.1F2/svg/coords-dom-04-f.svg)|
|✅|[coords-trans-01-b](Tests/SVGViewTests/w3c/1.1F2/svg/coords-trans-01-b.svg)|
|✅|[coords-trans-02-t](Tests/SVGViewTests/w3c/1.1F2/svg/coords-trans-02-t.svg)|
|✅|[coords-trans-03-t](Tests/SVGViewTests/w3c/1.1F2/svg/coords-trans-03-t.svg)|