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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions GenerateReferencesCLI/cli.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,9 @@ struct cli: ParsableCommand {
"shapes-rect-06-f",
"shapes-rect-07-f",
"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",
Expand All @@ -127,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] = [
Expand Down
8 changes: 7 additions & 1 deletion Source/Model/Nodes/SVGDefs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
// <defs> defines reusable content and must not be rendered directly.
}
#endif
}

2 changes: 2 additions & 0 deletions Source/Model/Nodes/SVGNode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
58 changes: 58 additions & 0 deletions Source/Parser/SVG/Elements/SVGElementParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,29 @@ protocol SVGElementParser {

class SVGBaseElementParser: SVGElementParser {

/// SVG 1.1 feature strings that SVGView supports for conditional processing.
static let supportedConditionalFeatures: Set<String> = [
"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)
Expand Down Expand Up @@ -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
}

}
50 changes: 1 addition & 49 deletions Source/Parser/SVG/Elements/SVGStructureParsers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = [
"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 {
Expand All @@ -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)
}
}
9 changes: 9 additions & 0 deletions Tests/SVGViewTests/SVG11Tests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,9 @@ 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 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") }
Expand All @@ -186,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") }
}
}
62 changes: 62 additions & 0 deletions Tests/SVGViewTests/w3c/1.1F2/refs/struct-cond-02-t.ref
Original file line number Diff line number Diff line change
@@ -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" }
}
]
}
59 changes: 59 additions & 0 deletions Tests/SVGViewTests/w3c/1.1F2/refs/struct-cond-overview-02-f.ref
Original file line number Diff line number Diff line change
@@ -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]
}
]
}
]
}
Loading