From 574bf5271f4050db7a3bc01d99337fe33494a610 Mon Sep 17 00:00:00 2001 From: Mathias Amnell <104110+Amnell@users.noreply.github.com> Date: Tue, 26 May 2026 10:38:14 +0200 Subject: [PATCH 1/4] Implement struct-cond-02-t --- GenerateReferencesCLI/cli.swift | 1 + Tests/SVGViewTests/SVG11Tests.swift | 1 + .../w3c/1.1F2/refs/struct-cond-02-t.ref | 62 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 Tests/SVGViewTests/w3c/1.1F2/refs/struct-cond-02-t.ref diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift index a2ed0c21..4db1ef0d 100644 --- a/GenerateReferencesCLI/cli.swift +++ b/GenerateReferencesCLI/cli.swift @@ -115,6 +115,7 @@ struct cli: ParsableCommand { "shapes-rect-06-f", "shapes-rect-07-f", "struct-cond-01-t", + "struct-cond-02-t", "struct-cond-03-t", "struct-defs-01-t", "struct-frag-01-t", diff --git a/Tests/SVGViewTests/SVG11Tests.swift b/Tests/SVGViewTests/SVG11Tests.swift index b02242ba..3e9b5bf4 100644 --- a/Tests/SVGViewTests/SVG11Tests.swift +++ b/Tests/SVGViewTests/SVG11Tests.swift @@ -161,6 +161,7 @@ struct SVG11Tests { var dir: String { "1.1F2" } @Test func structCond01T() async throws { try await compareToReference("struct-cond-01-t") } + @Test func structCond02T() async throws { try await compareToReference("struct-cond-02-t") } @Test func structCond03T() async throws { try await compareToReference("struct-cond-03-t") } @Test func structDefs01T() async throws { try await compareToReference("struct-defs-01-t") } @Test func structFrag01T() async throws { try await compareToReference("struct-frag-01-t") } diff --git a/Tests/SVGViewTests/w3c/1.1F2/refs/struct-cond-02-t.ref b/Tests/SVGViewTests/w3c/1.1F2/refs/struct-cond-02-t.ref new file mode 100644 index 00000000..6f082427 --- /dev/null +++ b/Tests/SVGViewTests/w3c/1.1F2/refs/struct-cond-02-t.ref @@ -0,0 +1,62 @@ +SVGViewport { + id: "svg-root", + viewBox: { width: 480, height: 360 }, + scaling: "none", + contents: [ + SVGDefs { }, + SVGGroup { + id: "test-body-content", + contents: [ + SVGGroup { + contents: [ + SVGGroup { + contents: [ + SVGGroup { + contents: [ + SVGText { + text: "Why can't they just speak English ?", + font: { + name: "Arial, Tahoma, Verdana, 'Arial Unicode MS', Code2000", + size: 24 + }, + fill: "black", + transform: [1, 0, 0, 1, 20, 220] + }, + SVGText { + text: "English (US)", + font: { + name: "Arial, Tahoma, Verdana, 'Arial Unicode MS', Code2000", + size: 24 + }, + fill: "black", + transform: [1, 0, 0, 1, 230, 150] + } + ] + } + ] + } + ] + } + ] + }, + SVGGroup { + contents: [ + SVGText { + id: "revision", + text: "$Revision: 1.6 $", + 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" } + } + ] +} From d25bc11a67f0580e1837d42e93afa52eeae8e3cb Mon Sep 17 00:00:00 2001 From: Mathias Amnell <104110+Amnell@users.noreply.github.com> Date: Tue, 26 May 2026 10:47:02 +0200 Subject: [PATCH 2/4] Implement struct-cond-overview-02-f --- GenerateReferencesCLI/cli.swift | 1 + .../SVG/Elements/SVGElementParser.swift | 58 ++++++++++++++++++ .../SVG/Elements/SVGStructureParsers.swift | 50 +--------------- Tests/SVGViewTests/SVG11Tests.swift | 1 + .../1.1F2/refs/struct-cond-overview-02-f.ref | 59 +++++++++++++++++++ w3c-coverage.md | 2 +- 6 files changed, 121 insertions(+), 50 deletions(-) create mode 100644 Tests/SVGViewTests/w3c/1.1F2/refs/struct-cond-overview-02-f.ref diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift index 4db1ef0d..02796981 100644 --- a/GenerateReferencesCLI/cli.swift +++ b/GenerateReferencesCLI/cli.swift @@ -117,6 +117,7 @@ struct cli: ParsableCommand { "struct-cond-01-t", "struct-cond-02-t", "struct-cond-03-t", + "struct-cond-overview-02-f", "struct-defs-01-t", "struct-frag-01-t", "struct-frag-06-t", diff --git a/Source/Parser/SVG/Elements/SVGElementParser.swift b/Source/Parser/SVG/Elements/SVGElementParser.swift index 03b5185e..390f864f 100644 --- a/Source/Parser/SVG/Elements/SVGElementParser.swift +++ b/Source/Parser/SVG/Elements/SVGElementParser.swift @@ -15,7 +15,29 @@ protocol SVGElementParser { class SVGBaseElementParser: SVGElementParser { + /// SVG 1.1 feature strings that SVGView supports for conditional processing. + static let supportedConditionalFeatures: Set = [ + "http://www.w3.org/TR/SVG11/feature#SVG-static", + "http://www.w3.org/TR/SVG11/feature#CoreAttribute", + "http://www.w3.org/TR/SVG11/feature#Structure", + "http://www.w3.org/TR/SVG11/feature#BasicStructure", + "http://www.w3.org/TR/SVG11/feature#ConditionalProcessing", + "http://www.w3.org/TR/SVG11/feature#Shape", + "http://www.w3.org/TR/SVG11/feature#BasicText", + "http://www.w3.org/TR/SVG11/feature#PaintAttribute", + "http://www.w3.org/TR/SVG11/feature#BasicPaintAttribute", + "http://www.w3.org/TR/SVG11/feature#OpacityAttribute", + "http://www.w3.org/TR/SVG11/feature#GraphicsAttribute", + "http://www.w3.org/TR/SVG11/feature#BasicGraphicsAttribute", + "http://www.w3.org/TR/SVG11/feature#Gradient", + "http://www.w3.org/TR/SVG11/feature#Marker", + "http://www.w3.org/TR/SVG11/feature#Image", + ] + func parse(context: SVGNodeContext, delegate: (XMLElement) -> SVGNode?) -> SVGNode? { + guard Self.conditionalAttributesMet(attributes: context.properties) else { + return nil + } guard let node = doParse(context: context, delegate: delegate) else { return nil } let transform = SVGHelper.parseTransform(context.properties["transform"] ?? "") node.transform = node.transform.concatenating(transform) @@ -58,4 +80,40 @@ class SVGBaseElementParser: SVGElementParser { return SVGUserSpaceNode.UserSpace.objectBoundingBox } + static func conditionalAttributesMet(attributes: [String: String]) -> Bool { + // requiredExtensions: SVGView supports no extensions — any non-empty value fails. + if let extensions = attributes["requiredExtensions"], !extensions.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return false + } + + // requiredFeatures: every whitespace-separated URI must be supported. + if let features = attributes["requiredFeatures"] { + let required = features.split(whereSeparator: { $0.isWhitespace }).map(String.init) + if !required.allSatisfy({ supportedConditionalFeatures.contains($0) }) { + return false + } + } + + // systemLanguage: at least one listed language must match the current locale. + if let languages = attributes["systemLanguage"] { + let currentLanguage: String + if #available(macOS 13, iOS 16, watchOS 9, *) { + currentLanguage = Locale.current.language.languageCode?.identifier ?? "" + } else { + currentLanguage = (Locale.current as NSLocale).languageCode + } + + let codes = languages + .split(separator: ",") + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() } + .filter { !$0.isEmpty } + let normalizedCurrent = currentLanguage.lowercased() + if !codes.contains(where: { $0.hasPrefix(normalizedCurrent) || normalizedCurrent.hasPrefix($0) }) { + return false + } + } + + return true + } + } diff --git a/Source/Parser/SVG/Elements/SVGStructureParsers.swift b/Source/Parser/SVG/Elements/SVGStructureParsers.swift index fcb2685b..d09d3151 100644 --- a/Source/Parser/SVG/Elements/SVGStructureParsers.swift +++ b/Source/Parser/SVG/Elements/SVGStructureParsers.swift @@ -151,25 +151,6 @@ class SVGMarkerParser: SVGBaseElementParser { /// met. Subsequent children are ignored. class SVGSwitchParser: SVGBaseElementParser { - /// SVG 1.1 feature strings that SVGView supports. - private static let supportedFeatures: Set = [ - "http://www.w3.org/TR/SVG11/feature#SVG-static", - "http://www.w3.org/TR/SVG11/feature#CoreAttribute", - "http://www.w3.org/TR/SVG11/feature#Structure", - "http://www.w3.org/TR/SVG11/feature#BasicStructure", - "http://www.w3.org/TR/SVG11/feature#ConditionalProcessing", - "http://www.w3.org/TR/SVG11/feature#Shape", - "http://www.w3.org/TR/SVG11/feature#BasicText", - "http://www.w3.org/TR/SVG11/feature#PaintAttribute", - "http://www.w3.org/TR/SVG11/feature#BasicPaintAttribute", - "http://www.w3.org/TR/SVG11/feature#OpacityAttribute", - "http://www.w3.org/TR/SVG11/feature#GraphicsAttribute", - "http://www.w3.org/TR/SVG11/feature#BasicGraphicsAttribute", - "http://www.w3.org/TR/SVG11/feature#Gradient", - "http://www.w3.org/TR/SVG11/feature#Marker", - "http://www.w3.org/TR/SVG11/feature#Image", - ] - override func doParse(context: SVGNodeContext, delegate: (XMLElement) -> SVGNode?) -> SVGNode? { let children = context.element.contents.compactMap { $0 as? XMLElement } for child in children { @@ -182,35 +163,6 @@ class SVGSwitchParser: SVGBaseElementParser { } private func conditionsMet(for element: XMLElement) -> Bool { - let attrs = element.attributes - - // requiredExtensions: SVGView supports no extensions — any non-empty value skips the child - if let extensions = attrs["requiredExtensions"], !extensions.trimmingCharacters(in: .whitespaces).isEmpty { - return false - } - - // requiredFeatures: every space-separated feature URI must be in our supported set - if let features = attrs["requiredFeatures"] { - let required = features.split(separator: " ").map(String.init) - if !required.allSatisfy({ Self.supportedFeatures.contains($0) }) { - return false - } - } - - // systemLanguage: at least one comma-separated BCP-47 tag must prefix-match the current locale - if let languages = attrs["systemLanguage"] { - let currentLang: String - if #available(macOS 13, iOS 16, watchOS 9, *) { - currentLang = Locale.current.language.languageCode?.identifier ?? "" - } else { - currentLang = (Locale.current as NSLocale).languageCode - } - let codes = languages.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces) } - if !codes.contains(where: { $0.hasPrefix(currentLang) || currentLang.hasPrefix($0) }) { - return false - } - } - - return true + Self.conditionalAttributesMet(attributes: element.attributes) } } diff --git a/Tests/SVGViewTests/SVG11Tests.swift b/Tests/SVGViewTests/SVG11Tests.swift index 3e9b5bf4..d00b16ac 100644 --- a/Tests/SVGViewTests/SVG11Tests.swift +++ b/Tests/SVGViewTests/SVG11Tests.swift @@ -163,6 +163,7 @@ struct SVG11Tests { @Test func structCond01T() async throws { try await compareToReference("struct-cond-01-t") } @Test func structCond02T() async throws { try await compareToReference("struct-cond-02-t") } @Test func structCond03T() async throws { try await compareToReference("struct-cond-03-t") } + @Test func structCondOverview02F() async throws { try await compareToReference("struct-cond-overview-02-f") } @Test func structDefs01T() async throws { try await compareToReference("struct-defs-01-t") } @Test func structFrag01T() async throws { try await compareToReference("struct-frag-01-t") } @Test func structFrag06T() async throws { try await compareToReference("struct-frag-06-t") } diff --git a/Tests/SVGViewTests/w3c/1.1F2/refs/struct-cond-overview-02-f.ref b/Tests/SVGViewTests/w3c/1.1F2/refs/struct-cond-overview-02-f.ref new file mode 100644 index 00000000..c8e292b8 --- /dev/null +++ b/Tests/SVGViewTests/w3c/1.1F2/refs/struct-cond-overview-02-f.ref @@ -0,0 +1,59 @@ +SVGViewport { + id: "svg-root", + viewBox: { width: 480, height: 360 }, + scaling: "none", + contents: [ + SVGDefs { }, + SVGGroup { + id: "test-body-content", + contents: [ + SVGRect { width: 100, height: 100, fill: "blue" }, + SVGRect { x: 200, width: 100, height: 100, fill: "blue" }, + SVGRect { y: 120, width: 100, height: 100, fill: "blue" }, + SVGRect { x: 200, y: 120, width: 100, height: 100, fill: "blue" }, + SVGRect { y: 240, width: 100, height: 100, fill: "blue" }, + SVGRect { x: 200, y: 240, width: 100, height: 100, fill: "blue" } + ] + }, + SVGGroup { + contents: [ + SVGText { + id: "revision", + text: "$Revision: 1.4 $", + 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" } + }, + SVGGroup { + id: "draft-watermark", + contents: [ + SVGRect { + x: 1, + y: 1, + width: 478, + height: 20, + fill: "red", + stroke: { fill: "black" } + }, + SVGText { + text: "DRAFT", + font: { name: "SVGFreeSansASCII,sans-serif", size: 20, weight: "bold" }, + textAnchor: "middle", + fill: "white", + stroke: { fill: "black", width: 0.5 }, + transform: [1, 0, 0, 1, 240, 18] + } + ] + } + ] +} diff --git a/w3c-coverage.md b/w3c-coverage.md index 658228a7..5fa7d29a 100644 --- a/w3c-coverage.md +++ b/w3c-coverage.md @@ -583,7 +583,7 @@ This page is automatically generated and shows actual coverage of the [W3C SVG T |✅|[struct-cond-01-t](Tests/SVGViewTests/w3c/1.1F2/svg/struct-cond-01-t.svg)| |❌|[struct-cond-02-t](Tests/SVGViewTests/w3c/1.1F2/svg/struct-cond-02-t.svg)| |✅|[struct-cond-03-t](Tests/SVGViewTests/w3c/1.1F2/svg/struct-cond-03-t.svg)| -|❌|[struct-cond-overview-02-f](Tests/SVGViewTests/w3c/1.1F2/svg/struct-cond-overview-02-f.svg)| +|✅|[struct-cond-overview-02-f](Tests/SVGViewTests/w3c/1.1F2/svg/struct-cond-overview-02-f.svg)| |❌|[struct-cond-overview-03-f](Tests/SVGViewTests/w3c/1.1F2/svg/struct-cond-overview-03-f.svg)| |❌|[struct-cond-overview-04-f](Tests/SVGViewTests/w3c/1.1F2/svg/struct-cond-overview-04-f.svg)| |❌|[struct-cond-overview-05-f](Tests/SVGViewTests/w3c/1.1F2/svg/struct-cond-overview-05-f.svg)| From 95964392d8b4d79d6030c500ecd6c21b29acb7bd Mon Sep 17 00:00:00 2001 From: Mathias Amnell <104110+Amnell@users.noreply.github.com> Date: Tue, 26 May 2026 10:58:07 +0200 Subject: [PATCH 3/4] Fix struct-defs-01-t rendering --- Source/Model/Nodes/SVGDefs.swift | 8 +++++++- Source/Model/Nodes/SVGNode.swift | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/Model/Nodes/SVGDefs.swift b/Source/Model/Nodes/SVGDefs.swift index 525c76be..c187a8a7 100644 --- a/Source/Model/Nodes/SVGDefs.swift +++ b/Source/Model/Nodes/SVGDefs.swift @@ -5,5 +5,11 @@ import SwiftUI import Combine #endif -public class SVGDefs: SVGGroup {} +public class SVGDefs: SVGGroup { + #if !os(WASI) && !os(Linux) + override func draw(in context: CGContext) { + // defines reusable content and must not be rendered directly. + } + #endif +} diff --git a/Source/Model/Nodes/SVGNode.swift b/Source/Model/Nodes/SVGNode.swift index 1b20910b..442e7a85 100644 --- a/Source/Model/Nodes/SVGNode.swift +++ b/Source/Model/Nodes/SVGNode.swift @@ -109,6 +109,8 @@ extension SVGNode { switch self { case let model as SVGViewport: SVGViewportView(model: model) + case is SVGDefs: + EmptyView() case let model as SVGGroup: model.contentView() case let model as SVGRect: From c14657ae23979fc14d07944a7839b7a8462e2236 Mon Sep 17 00:00:00 2001 From: Mathias Amnell <104110+Amnell@users.noreply.github.com> Date: Tue, 26 May 2026 11:16:05 +0200 Subject: [PATCH 4/4] Implement text-align-01-b --- GenerateReferencesCLI/cli.swift | 1 + Tests/SVGViewTests/SVG11Tests.swift | 7 ++ .../w3c/1.1F2/refs/text-align-01-b.ref | 110 ++++++++++++++++++ 3 files changed, 118 insertions(+) create mode 100644 Tests/SVGViewTests/w3c/1.1F2/refs/text-align-01-b.ref diff --git a/GenerateReferencesCLI/cli.swift b/GenerateReferencesCLI/cli.swift index 02796981..c050d012 100644 --- a/GenerateReferencesCLI/cli.swift +++ b/GenerateReferencesCLI/cli.swift @@ -129,6 +129,7 @@ struct cli: ParsableCommand { "styling-css-01-b", "styling-pres-01-t", "types-basic-01-f", + "text-align-01-b", ] static let v12Refs: [String] = [ diff --git a/Tests/SVGViewTests/SVG11Tests.swift b/Tests/SVGViewTests/SVG11Tests.swift index d00b16ac..c6692953 100644 --- a/Tests/SVGViewTests/SVG11Tests.swift +++ b/Tests/SVGViewTests/SVG11Tests.swift @@ -188,4 +188,11 @@ struct SVG11Tests { @Test func typesBasic01F() async throws { try await compareToReference("types-basic-01-f") } } + + @Suite("Text") + struct Text: SVGTestHelper { + var dir: String { "1.1F2" } + + @Test func textAlign01B() async throws { try await compareToReference("text-align-01-b") } + } } diff --git a/Tests/SVGViewTests/w3c/1.1F2/refs/text-align-01-b.ref b/Tests/SVGViewTests/w3c/1.1F2/refs/text-align-01-b.ref new file mode 100644 index 00000000..91e5d760 --- /dev/null +++ b/Tests/SVGViewTests/w3c/1.1F2/refs/text-align-01-b.ref @@ -0,0 +1,110 @@ +SVGViewport { + id: "svg-root", + viewBox: { width: 480, height: 360 }, + scaling: "none", + contents: [ + SVGDefs { }, + SVGGroup { + id: "test-body-content", + contents: [ + SVGText { + text: "Test 'text-anchor' (horizontal)", + font: { name: "SVGFreeSansASCII,sans-serif", size: 34 }, + fill: "black", + transform: [1, 0, 0, 1, 5, 40] + }, + SVGGroup { + id: "text-anchor", + contents: [ + SVGGroup { + transform: [1, 0, 0, 1, 230, 130], + contents: [ + SVGLine { + x2: 50, + fill: "black", + stroke: { fill: "black" } + }, + SVGCircle { r: 3, fill: "black" }, + SVGText { + text: "text-anchor:none", + font: { name: "SVGFreeSansASCII,sans-serif", size: 30 }, + fill: "fuchsia" + } + ] + }, + SVGGroup { + transform: [1, 0, 0, 1, 230, 180], + contents: [ + SVGLine { + x2: 50, + fill: "black", + stroke: { fill: "black" } + }, + SVGCircle { r: 3, fill: "black" }, + SVGText { + text: "text-anchor:start", + font: { name: "SVGFreeSansASCII,sans-serif", size: 30 }, + fill: "fuchsia" + } + ] + }, + SVGGroup { + transform: [1, 0, 0, 1, 230, 230], + contents: [ + SVGLine { + x1: -25, + x2: 25, + fill: "black", + stroke: { fill: "black" } + }, + SVGCircle { r: 3, fill: "black" }, + SVGText { + text: "text-anchor:middle", + font: { name: "SVGFreeSansASCII,sans-serif", size: 30 }, + textAnchor: "middle", + fill: "green" + } + ] + }, + SVGGroup { + transform: [1, 0, 0, 1, 230, 280], + contents: [ + SVGLine { + x1: -50, + fill: "black", + stroke: { fill: "black" } + }, + SVGCircle { r: 3, fill: "black" }, + SVGText { + text: "text-anchor:end", + font: { name: "SVGFreeSansASCII,sans-serif", size: 30 }, + textAnchor: "end", + fill: "blue" + } + ] + } + ] + } + ] + }, + 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" } + } + ] +}