sevgi-standard 0.93.1 → 0.95.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Sevgi Standard
2
2
 
3
- Provides SVG element, attribute, and conformance data used by Sevgi validation.
3
+ SVG element, attribute, and conformance data used by Sevgi validation.
4
4
 
5
- See the root [README](../README.md) and the documentation site for usage.
5
+ ## Install
6
+
7
+ ```sh
8
+ gem install sevgi-standard
9
+ ```
10
+
11
+ ## Require
12
+
13
+ ```ruby
14
+ require "sevgi/standard"
15
+ ```
16
+
17
+ ## Example
18
+
19
+ ```ruby
20
+ Sevgi::Standard.element?(:rect)
21
+ Sevgi::Standard.attribute?(:width)
22
+ ```
23
+
24
+ ## Ruby compatibility
25
+
26
+ Requires Ruby 3.4.0 or newer. CI verifies Ruby 3.4.0 and the current development Ruby from `.ruby-version`.
27
+
28
+ ## Native prerequisites
29
+
30
+ None beyond Ruby and this gem's Ruby dependencies.
31
+
32
+ ## Links
33
+
34
+ - Documentation: https://sevgi.roktas.dev
35
+ - API documentation: https://www.rubydoc.info/gems/sevgi-standard
36
+ - Source: https://github.com/roktas/sevgi/tree/main/standard
37
+ - Changelog: https://github.com/roktas/sevgi/blob/main/CHANGELOG.md
@@ -14,11 +14,14 @@ module Sevgi
14
14
  attr_reader :spec
15
15
 
16
16
  # Builds a validator for one SVG element.
17
- # @param element [Symbol] SVG element name
17
+ # @param element [String, Symbol] SVG element name
18
18
  # @return [void]
19
+ # @raise [Sevgi::ArgumentError] when element is not a valid public name
19
20
  # @raise [Sevgi::InvalidElementsError] when the element is unknown
20
21
  # @raise [Sevgi::PanicError] when the element model is missing or unimplemented
21
22
  def initialize(element)
23
+ element = Name.normalize!(element, context: "element")
24
+
22
25
  InvalidElementsError.(element) unless (@spec = Specification[@element = element])
23
26
 
24
27
  PanicError.("No model specified: #{element}") unless spec[:model]
@@ -30,10 +33,11 @@ module Sevgi
30
33
  end
31
34
 
32
35
  # Validates one usage of the configured SVG element.
33
- # @param attributes [Array<Symbol>, nil] attribute names used by the element
36
+ # @param attributes [Array<String, Symbol>, String, Symbol, nil] attribute names used by the element
34
37
  # @param cdata [String, nil] character data content
35
- # @param elements [Array<Symbol>, nil] child element names
38
+ # @param elements [Array<String, Symbol>, String, Symbol, nil] child element names
36
39
  # @return [Boolean] true when the usage conforms
40
+ # @raise [Sevgi::ArgumentError] when any name is not a valid public name
37
41
  # @raise [Sevgi::ValidationError] when the usage violates the standard data
38
42
  def call(attributes: nil, cdata: nil, elements: nil)
39
43
  if attributes
@@ -50,21 +54,39 @@ module Sevgi
50
54
  @cache = {}
51
55
 
52
56
  # Validates one SVG element usage, using cached element validators.
53
- # @param element [Symbol] SVG element name
54
- # @param attributes [Array<Symbol>, nil] attribute names used by the element
57
+ # @param element [String, Symbol] SVG element name
58
+ # @param attributes [Array<String, Symbol>, nil] attribute names used by the element
55
59
  # @param cdata [String, nil] character data content
56
- # @param elements [Array<Symbol>, nil] child element names
60
+ # @param elements [Array<String, Symbol>, nil] child element names
57
61
  # @return [Boolean] true when the usage conforms or the element is ignored
62
+ # @raise [Sevgi::ArgumentError] when any name is not a valid public name
58
63
  # @raise [Sevgi::ValidationError] when the usage violates the standard data
59
64
  # @raise [Sevgi::PanicError] when the standard data refers to an invalid model
60
65
  def self.call(element, attributes: nil, cdata: nil, elements: nil)
66
+ element = Name.normalize!(element, context: "element")
67
+ attributes = Name.list!(attributes, context: "attribute")
68
+ elements = Name.list!(elements, context: "element") || []
69
+
61
70
  Element.ignore?(element) or
62
- (@cache[element] ||= new(element)).call(
63
- attributes: Attribute.concerns(attributes),
64
- elements: Element.concerns(elements || []),
65
- cdata:
66
- )
71
+ validate(element, attributes:, elements:, cdata:)
72
+ end
73
+
74
+ def self.validate(element, attributes:, elements:, cdata:)
75
+ validator = @cache[element] || new(element)
76
+ result = validator.call(**arguments(attributes, elements, cdata))
77
+ @cache[element] ||= validator
78
+ result
67
79
  end
80
+
81
+ def self.arguments(attributes, elements, cdata)
82
+ {
83
+ attributes: Attribute.concerns(attributes),
84
+ elements: Element.concerns(elements),
85
+ cdata:
86
+ }
87
+ end
88
+
89
+ private_class_method :arguments, :validate
68
90
  end
69
91
 
70
92
  private_constant :Conform, :Model
@@ -483,7 +483,6 @@ module Sevgi
483
483
  Deprecated: %i[
484
484
  accent-height
485
485
  alphabetic
486
- amplitude
487
486
  arabic-form
488
487
  ascent
489
488
  attributeType
@@ -492,12 +491,29 @@ module Sevgi
492
491
  ]
493
492
  )
494
493
 
494
+ RESERVED_NAMESPACE_PREFIXES = %w[xlink: xml:].freeze
495
+ private_constant :RESERVED_NAMESPACE_PREFIXES
496
+
497
+ # Reports whether an attribute name should be ignored by standard validation.
498
+ # @param attribute [String, Symbol, Object] candidate attribute name
499
+ # @return [Boolean] true for valid private, data, xmlns, or foreign namespaced attributes
495
500
  def ignore?(attribute)
496
- attribute.start_with?("_") ||
497
- attribute == :xmlns ||
498
- attribute.start_with?("data-") ||
499
- (attribute.to_s.include?(":") && !attribute.start_with?("xlink:") && !attribute.start_with?("xml:"))
501
+ return false unless attribute.is_a?(::String) || attribute.is_a?(::Symbol)
502
+
503
+ name = attribute.to_s
504
+
505
+ Name.valid?(name) && ignored_name?(name)
500
506
  end
507
+
508
+ def ignored_name?(name)
509
+ name.start_with?("_") || name == "xmlns" || name.start_with?("data-") || foreign_namespace?(name)
510
+ end
511
+
512
+ def foreign_namespace?(name)
513
+ name.include?(":") && RESERVED_NAMESPACE_PREFIXES.none? { name.start_with?(it) }
514
+ end
515
+
516
+ private_class_method :foreign_namespace?, :ignored_name?
501
517
  end
502
518
  end
503
519
  end
@@ -8,6 +8,7 @@ module Sevgi
8
8
  animate
9
9
  animateMotion
10
10
  animateTransform
11
+ discard
11
12
  mpath
12
13
  set
13
14
  ],
@@ -146,7 +147,6 @@ module Sevgi
146
147
  linearGradient
147
148
  pattern
148
149
  radialGradient
149
- solidcolor
150
150
  ],
151
151
 
152
152
  Renderable: %i[
@@ -265,8 +265,15 @@ module Sevgi
265
265
  ]
266
266
  )
267
267
 
268
+ # Reports whether an element name should be ignored by standard validation.
269
+ # @param element [String, Symbol, Object] candidate element name
270
+ # @return [Boolean] true for valid private or namespaced element names that Sevgi should not validate
268
271
  def ignore?(element)
269
- (name = element.to_s).include?(":") || name.start_with?("_")
272
+ return false unless element.is_a?(::String) || element.is_a?(::Symbol)
273
+
274
+ name = element.to_s
275
+
276
+ Name.valid?(name) && (name.include?(":") || name.start_with?("_"))
270
277
  end
271
278
  end
272
279
  end
@@ -4,12 +4,44 @@ require_relative "data/color"
4
4
 
5
5
  module Sevgi
6
6
  module Standard
7
+ # Defensive copy helper for public standard-data snapshots.
8
+ # @api private
9
+ module Snapshot
10
+ # Returns a recursively independent copy of a value.
11
+ # @param value [Object] value to copy
12
+ # @return [Object] copied value
13
+ def self.copy(value)
14
+ case value
15
+ when ::Hash
16
+ hash(value)
17
+ when ::Array
18
+ value.map { copy(it) }
19
+ when ::Set
20
+ Set[*value.map { copy(it) }]
21
+ else
22
+ duplicate(value)
23
+ end
24
+ end
25
+
26
+ def self.hash(value) = value.to_h { |key, item| [key, copy(item)] }
27
+
28
+ def self.duplicate(value)
29
+ value.dup
30
+ rescue ::TypeError
31
+ value
32
+ end
33
+
34
+ private_class_method :duplicate, :hash
35
+ end
36
+
37
+ private_constant :Snapshot
38
+
7
39
  # Shared set operations for SVG standard data lists.
8
40
  # @api private
9
41
  module Common
10
42
  # Returns every known name in this data list.
11
- # @return [Set<Symbol>]
12
- def all = @all ||= Set[*data.values.flatten.uniq.sort]
43
+ # @return [Set<Symbol>] mutation-isolated set snapshot
44
+ def all = Snapshot.copy(@all ||= Set[*data.values.flatten.uniq.sort])
13
45
 
14
46
  # Checks whether a name belongs to a group.
15
47
  # @param name [Symbol] item name
@@ -24,9 +56,13 @@ module Sevgi
24
56
  def pick(names, *groups) = names.select { |name| groups.any? { is?(name, it) } }
25
57
 
26
58
  # Returns all names or names from selected groups.
27
- # @param groups [Array<Symbol>] group names
28
- # @return [Set<Symbol>] selected names
29
- def set(*groups) = groups.empty? ? all : Set[*data.values_at(*groups).flatten.compact.uniq.sort]
59
+ # @param groups [Array<String, Symbol>] group names
60
+ # @return [Set<Symbol>] mutation-isolated selected-name snapshot
61
+ def set(*groups)
62
+ return all if groups.empty?
63
+
64
+ Set[*data.values_at(*groups!(groups)).flatten.uniq.sort]
65
+ end
30
66
 
31
67
  # Removes names that belong to any requested group.
32
68
  # @param names [Array<Symbol>] names to filter
@@ -34,6 +70,16 @@ module Sevgi
34
70
  # @return [Array<Symbol>] filtered names
35
71
  def unpick(names, *groups) = names.reject { |name| groups.any? { is?(name, it) } }
36
72
 
73
+ def groups!(groups)
74
+ groups = groups.map { Name.normalize!(it, context: "group") }
75
+ unknown = groups.reject { data.key?(it) }
76
+ ArgumentError.("Unknown SVG group(s): #{unknown.map(&:inspect).join(", ")}") unless unknown.empty?
77
+
78
+ groups
79
+ end
80
+
81
+ private :groups!
82
+
37
83
  # Removes ignored names from a list.
38
84
  # @param names [Array<Symbol>, nil] names to filter
39
85
  # @return [Array<Symbol>, nil] names that should be validated
@@ -74,8 +120,8 @@ module Sevgi
74
120
 
75
121
  # Returns expanded standard specification data for one element.
76
122
  # @param name [Symbol] SVG element name
77
- # @return [Hash, nil] expanded specification data
78
- def [](name) = expand(name)
123
+ # @return [Hash, nil] mutation-isolated expanded specification snapshot
124
+ def [](name) = Snapshot.copy(expand(name))
79
125
 
80
126
  # Reports whether a name is a data group name.
81
127
  # @param name [Symbol] element or group name
@@ -83,33 +129,42 @@ module Sevgi
83
129
  def group?(name) = /[[:upper:]]/.match?(name[0])
84
130
 
85
131
  # Checks whether an element uses one of the requested content models.
86
- # @param name [Symbol] SVG element name
87
- # @param models [Array<Symbol>] model names to match
132
+ # @param name [String, Symbol] SVG element name
133
+ # @param models [Array<String, Symbol>] model names to match
88
134
  # @return [Boolean]
89
- def model?(name, *models) = models.any? { (self[name] || {})[:model] == it }
135
+ # @raise [Sevgi::ArgumentError] when any name is not a valid public name
136
+ def model?(name, *models)
137
+ name = Name.normalize!(name, context: "element")
138
+ models = models.map { Name.normalize!(it, context: "model") }
139
+
140
+ models.any? { (self[name] || {})[:model] == it }
141
+ end
90
142
 
91
143
  private
92
144
 
93
145
  @spec = {}
94
146
 
95
147
  def expand(name)
96
- @spec[name] ||= if data[name]
97
- data[name].transform_values { |value| value.is_a?(::Array) ? value.dup : value }.tap do |spec|
98
- expand_names(spec[:elements], Element.data)
99
- expand_names(spec[:attributes], Attribute.data)
148
+ return unless data.key?(name)
149
+
150
+ @spec[name] ||= data
151
+ .fetch(name)
152
+ .transform_values { |value| value.is_a?(::Array) ? value.dup : value }
153
+ .tap do |spec|
154
+ expand_names(spec[:elements], Element.send(:data))
155
+ expand_names(spec[:attributes], Attribute.send(:data))
100
156
  end
101
- end
102
157
  end
103
158
 
104
159
  def expand_names(names, list)
105
160
  return unless names
106
161
 
107
- names.replace(names.map { |name| group?(name) ? list[name] : name }.flatten)
162
+ names.replace(names.flat_map { |name| group?(name) ? list[name] : name }.uniq)
108
163
  end
109
164
 
110
165
  # For testing purposes
111
166
 
112
- def charge = data.keys { expand(it) }
167
+ def charge = data.each_key { expand(it) }
113
168
 
114
169
  def flush = (@spec = {})
115
170
  end
@@ -2,6 +2,50 @@
2
2
 
3
3
  module Sevgi
4
4
  module Standard
5
+ # Normalizes public SVG standard names at API boundaries.
6
+ # @api private
7
+ module Name
8
+ PATTERN = /\A[A-Za-z_][A-Za-z0-9_.-]*(?::[A-Za-z_][A-Za-z0-9_.-]*)?\z/
9
+ private_constant :PATTERN
10
+
11
+ # Reports whether a string is a supported SVG-style name token.
12
+ # @param name [String] candidate name
13
+ # @return [Boolean]
14
+ def self.valid?(name) = !!PATTERN.match?(name)
15
+
16
+ # Normalizes one public name to the registry symbol form.
17
+ # @param value [String, Symbol] public name
18
+ # @param context [String] name context for error messages
19
+ # @return [Symbol] normalized name
20
+ # @raise [Sevgi::ArgumentError] when value is not a String or Symbol
21
+ # @raise [Sevgi::ArgumentError] when value is not a valid SVG-style name
22
+ def self.normalize!(value, context:)
23
+ case value
24
+ when ::String, ::Symbol
25
+ text = value.to_s
26
+ ArgumentError.("Invalid SVG #{context} name: #{value.inspect}") unless valid?(text)
27
+
28
+ text.to_sym
29
+ else
30
+ ArgumentError.("SVG #{context} name must be a String or Symbol: #{value.inspect}")
31
+ end
32
+ end
33
+
34
+ # Normalizes a list of public names.
35
+ # @param values [Array<String, Symbol>, String, Symbol, nil] public names
36
+ # @param context [String] name context for error messages
37
+ # @return [Array<Symbol>, nil] normalized names
38
+ # @raise [Sevgi::ArgumentError] when any value is not a valid public name
39
+ def self.list!(values, context:)
40
+ return if values.nil?
41
+
42
+ values = [values] unless values.is_a?(::Array)
43
+ values.map { normalize!(it, context:) }
44
+ end
45
+ end
46
+
47
+ private_constant :Name
48
+
5
49
  # Low-level import and lookup helper for static SVG standard data.
6
50
  # @api private
7
51
  module List
@@ -32,6 +76,7 @@ module Sevgi
32
76
 
33
77
  class << self
34
78
  attr_reader :data
79
+ private :data
35
80
  end
36
81
  end
37
82
  end
@@ -103,7 +103,7 @@ module Sevgi
103
103
  UnmetConditionError.(element, "Exactly one FilterLightSource element as first required")
104
104
  end
105
105
 
106
- if !(unallowed = Element.unpick(elements[1..], :Descriptive)).empty? && !unallowed.empty?
106
+ unless (unallowed = Element.unpick(elements[1..], :Descriptive)).empty?
107
107
  UnallowedElementsError.(element, unallowed)
108
108
  end
109
109
  end
@@ -3,6 +3,6 @@
3
3
  module Sevgi
4
4
  module Standard
5
5
  # Current version of the Sevgi standard gem.
6
- VERSION = "0.93.1"
6
+ VERSION = "0.95.0"
7
7
  end
8
8
  end
@@ -12,53 +12,67 @@ require_relative "standard/version"
12
12
 
13
13
  module Sevgi
14
14
  # SVG standard data and validation helpers.
15
+ # The registry is a Sevgi compatibility set based on SVG 2 plus the split-out SVG modules and legacy entries already
16
+ # modeled in the bundled specifications. Supported element names must have matching specification data; abandoned or
17
+ # unspecified proposal entries are not exposed as supported elements.
15
18
  module Standard
16
19
  extend self
17
20
 
18
21
  # @overload attributes(*groups)
19
22
  # Returns SVG attributes, optionally restricted to one or more attribute groups.
20
- # @param groups [Array<Symbol>] attribute group names
21
- # @return [Set<Symbol>] attribute names
23
+ # @param groups [Array<String, Symbol>] attribute group names
24
+ # @return [Set<Symbol>] mutation-isolated attribute-name snapshot
22
25
  def attributes(...) = Attribute.set(...)
23
26
 
24
27
  # Reports whether an attribute name is recognized by the SVG standard data.
25
- # @param name [Object] attribute name
28
+ # @param name [String, Symbol] attribute name
26
29
  # @return [Boolean]
27
- def attribute?(name) = name.respond_to?(:to_sym) && Attribute.all.include?(name.to_sym)
30
+ # @raise [Sevgi::ArgumentError] when name is not a String or Symbol
31
+ # @raise [Sevgi::ArgumentError] when name is not a valid SVG-style name
32
+ def attribute?(name) = Attribute.all.include?(Name.normalize!(name, context: "attribute"))
28
33
 
29
34
  # @overload conform(element, attributes: nil, cdata: nil, elements: nil)
30
35
  # Validates an SVG element usage against the standard data.
31
- # @param element [Symbol] SVG element name
32
- # @param attributes [Array<Symbol>, nil] attribute names used by the element
36
+ # @param element [String, Symbol] SVG element name
37
+ # @param attributes [Array<String, Symbol>, String, Symbol, nil] attribute names used by the element
33
38
  # @param cdata [String, nil] character data content
34
- # @param elements [Array<Symbol>, nil] child element names
39
+ # @param elements [Array<String, Symbol>, String, Symbol, nil] child element names
35
40
  # @return [Boolean] true when the usage conforms
41
+ # @raise [Sevgi::ArgumentError] when any name is not a String or Symbol
42
+ # @raise [Sevgi::ArgumentError] when any name is not a valid SVG-style name
36
43
  # @raise [Sevgi::ValidationError] when the usage violates the standard data
37
44
  # @raise [Sevgi::PanicError] when the standard data refers to an invalid model
38
45
  def conform(...) = Conform.(...)
39
46
 
40
47
  # @overload elements(*groups)
41
48
  # Returns SVG elements, optionally restricted to one or more element groups.
42
- # @param groups [Array<Symbol>] element group names
43
- # @return [Set<Symbol>] element names
49
+ # @param groups [Array<String, Symbol>] element group names
50
+ # @return [Set<Symbol>] mutation-isolated element-name snapshot
44
51
  def elements(...) = Element.set(...)
45
52
 
46
53
  # Reports whether an element name is recognized by the SVG standard data.
47
- # @param name [Object] element name
54
+ # @param name [String, Symbol] element name
48
55
  # @return [Boolean]
49
- def element?(name) = name.respond_to?(:to_sym) && Element.all.include?(name.to_sym)
56
+ # @raise [Sevgi::ArgumentError] when name is not a String or Symbol
57
+ # @raise [Sevgi::ArgumentError] when name is not a valid SVG-style name
58
+ def element?(name) = Element.all.include?(Name.normalize!(name, context: "element"))
50
59
 
51
60
  # @overload model?(name, *models)
52
61
  # Checks whether an element uses one of the requested content models.
53
- # @param name [Symbol] SVG element name
54
- # @param models [Array<Symbol>] model names to match
62
+ # @param name [String, Symbol] SVG element name
63
+ # @param models [Array<String, Symbol>] model names to match
55
64
  # @return [Boolean]
65
+ # @raise [Sevgi::ArgumentError] when any name is not a String or Symbol
66
+ # @raise [Sevgi::ArgumentError] when any name is not a valid SVG-style name
56
67
  def model?(...) = Specification.model?(...)
57
68
 
58
69
  # Returns the expanded standard contract for an SVG element.
59
- # @param name [Object] SVG element name
60
- # @return [Hash, nil] expanded specification data, or nil when name is invalid or unknown
61
- def specification(name) = name.respond_to?(:to_sym) ? Specification[name.to_sym] : nil
70
+ # The returned hash and nested arrays are mutation-isolated snapshots; changing them does not alter the registry.
71
+ # @param name [String, Symbol] SVG element name
72
+ # @return [Hash, nil] expanded specification snapshot, or nil when name is unknown
73
+ # @raise [Sevgi::ArgumentError] when name is not a String or Symbol
74
+ # @raise [Sevgi::ArgumentError] when name is not a valid SVG-style name
75
+ def specification(name) = Specification[Name.normalize!(name, context: "element")]
62
76
 
63
77
  alias [] specification
64
78
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sevgi-standard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.93.1
4
+ version: 0.95.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Recai Oktaş
@@ -15,20 +15,22 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.93.1
18
+ version: 0.95.0
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.93.1
25
+ version: 0.95.0
26
26
  description: Validates elements and attributes according to the SVG specification.
27
27
  email: roktas@gmail.com
28
28
  executables: []
29
29
  extensions: []
30
30
  extra_rdoc_files: []
31
31
  files:
32
+ - CHANGELOG.md
33
+ - LICENSE
32
34
  - README.md
33
35
  - lib/sevgi/standard.rb
34
36
  - lib/sevgi/standard/conform.rb
@@ -56,14 +58,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
56
58
  requirements:
57
59
  - - ">="
58
60
  - !ruby/object:Gem::Version
59
- version: 3.4.0.pre.preview1
61
+ version: 3.4.0
60
62
  required_rubygems_version: !ruby/object:Gem::Requirement
61
63
  requirements:
62
64
  - - ">="
63
65
  - !ruby/object:Gem::Version
64
66
  version: '0'
65
67
  requirements: []
66
- rubygems_version: 4.0.10
68
+ rubygems_version: 4.0.11
67
69
  specification_version: 4
68
70
  summary: SVG Validation for the Sevgi toolkit.
69
71
  test_files: []