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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + + + X + + + 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 @@ + + + + X + + + 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 @@ + + + + X + + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + + + X + + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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 @@ + + X + + 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)|