vectory 0.13.1 → 0.13.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f48a547c1520b19d88d77ad41c2e2967da41896163f49ef39492f6f6e73b1af
4
- data.tar.gz: cb36e0382b283f7ad50816a5ce5ff5b6a80736a44f8a062db57dd8ce836385f2
3
+ metadata.gz: f57a3fbf7c5b0f3c7660e32dbd1f6361d149ee27a9eef68418ced42ab0d4416b
4
+ data.tar.gz: 0e6dcda6b904e457b75ee3091ee27f91c6790da056f289003f289f585a66148b
5
5
  SHA512:
6
- metadata.gz: 11759d94c1a88218d39f5e4561a661c1c0bee4f371d9c369571d95c34e141dadfb308646c890fc08ddef5a5484fe381e5303b07fc153985b0989fd249e85d8c8
7
- data.tar.gz: 1a0151957b311956558f7935e548e92d2678d283238eb66445ec7203916b3fba3753f93140affb5145b732df6f8cd3b75746a2985f9801875a2bf4688f021ad3
6
+ metadata.gz: bdd45abe6b12a83b16d04a63fca16fa3052552909ac1a16de73623706d0b2ee8dd84a958baec8e60e14c6d4b4037c6832ec55abddda3acb94b971cd5bbbbe206
7
+ data.tar.gz: 9761d2d5b4169f87fbbc46555ac74ab8d3d43ad80260aadd07bc6a77aa7d0c6ea564854bef42f70b02a383a1647610a1770af8d200a43881a91001386bec2f3d
@@ -0,0 +1,138 @@
1
+ require "leptris"
2
+
3
+ module Vectory
4
+ # Disambiguates element ids across multiple SVG documents embedded in one
5
+ # output (the multi-svg collision problem): ids that appear in more than
6
+ # one SVG get a per-document suffix, and every reference form is rewritten
7
+ # to match — `url(#id)` in presentation attributes and `<style>` blocks,
8
+ # `href`/`xlink:href` fragment references, and the `id` attributes
9
+ # themselves.
10
+ #
11
+ # This is the string-in/string-out counterpart of SvgMapping's document
12
+ # processing: content in, processed content out, same as SvgMapping's
13
+ # "serialize on entry" contract. Ported from metanorma-standoc's
14
+ # svg_uniqueids cleanup (lib/metanorma/cleanup/image.rb), where it was
15
+ # written against Nokogiri inline.
16
+ #
17
+ # Usage — N embedded SVGs, disambiguate by position:
18
+ #
19
+ # dupes = Vectory::SvgIdDisambiguator.duplicate_ids(svgs)
20
+ # svgs.map.with_index { |svg, i| Vectory::SvgIdDisambiguator.rewrite(svg, "_inject_#{i}", dupes) }
21
+ #
22
+ class SvgIdDisambiguator
23
+ # SVG elements whose ids are referenced through presentation attributes
24
+ # and CSS rather than href: the tag maps to the attributes that carry
25
+ # the reference (nil = the tag name itself is the CSS property).
26
+ IRI_TAG_PROPERTIES_MAP = {
27
+ "clipPath" => ["clip-path"],
28
+ "color-profile" => nil,
29
+ "cursor" => nil,
30
+ "filter" => nil,
31
+ "linearGradient" => ["fill", "stroke"],
32
+ "marker" => ["marker", "marker-end", "marker-mid", "marker-start"],
33
+ "mask" => nil,
34
+ "pattern" => ["fill", "stroke"],
35
+ "radialGradient" => ["fill", "stroke"],
36
+ }.freeze
37
+
38
+ URL_RE = %r{url\("?#([a-zA-Z][\w:.-]*)"?\)}.freeze
39
+ private_constant :URL_RE
40
+
41
+ class << self
42
+ # Ids that appear more than once across the given SVG contents.
43
+ #
44
+ # @param contents [Array<String>] SVG document contents
45
+ # @return [Hash{String => Integer}] id => occurrence count (count >= 2)
46
+ def duplicate_ids(contents)
47
+ contents.flat_map { |c| id_values(Leptris::XML.parse(c)) }
48
+ .group_by(&:itself).transform_values(&:count)
49
+ .delete_if { |_, v| v < 2 }
50
+ end
51
+
52
+ # Suffix every id in +duplicate_ids+ with +suffix+ inside +content+,
53
+ # rewriting all reference forms to match. Ids absent from the map are
54
+ # left untouched.
55
+ #
56
+ # @param content [String] SVG document content
57
+ # @param suffix [String] the full disambiguator, separator included
58
+ # (e.g. "_inject_0")
59
+ # @param duplicate_ids [Hash{String => Integer}] from .duplicate_ids
60
+ # @return [String] the rewritten content
61
+ def rewrite(content, suffix, duplicate_ids)
62
+ doc = Leptris::XML.parse(content)
63
+ svg_roots(doc).each { |svg| rewrite_svg(svg, suffix, duplicate_ids) }
64
+ doc.to_xml
65
+ end
66
+
67
+ private
68
+
69
+ def svg_roots(doc)
70
+ root = doc.root
71
+ if root&.name == "svg"
72
+ [root]
73
+ else
74
+ # SVGs embedded in a host document
75
+ doc.xpath("//*[local-name()='svg']")
76
+ end
77
+ end
78
+
79
+ def id_values(doc)
80
+ doc.xpath("//*").filter_map { |e| e["id"] if e["id"] }
81
+ end
82
+
83
+ def rewrite_svg(svg, suffix, ids)
84
+ id_elems = svg.xpath(".//*[@id]")
85
+ id_elems = [svg] + id_elems if svg["id"]
86
+ iri_properties = iri_properties(id_elems)
87
+ ([svg] + svg.xpath("//*")).each { |e| rewrite_element(e, iri_properties, suffix, ids) }
88
+ end
89
+
90
+ def rewrite_element(elem, iri_properties, suffix, ids)
91
+ if elem.name == "style"
92
+ elem.children = rewrite_urls(elem.text, suffix, ids)
93
+ return
94
+ end
95
+
96
+ # All attribute reads AND writes go through attribute_nodes: mixing
97
+ # in the elem[...] accessors after an id write desyncs the native
98
+ # attribute layer (leptris-ruby#526).
99
+ nodes = elem.attribute_nodes || []
100
+ return if nodes.empty?
101
+
102
+ iri_properties.each do |p|
103
+ node = nodes.find { |a| a.name == p } or next
104
+ node.value = rewrite_urls(node.value, suffix, ids)
105
+ end
106
+ nodes.each do |a|
107
+ case a.name
108
+ when "id"
109
+ a.value = "#{a.value}#{suffix}" if ids[a.value]
110
+ when "xlink:href", "href"
111
+ v = a.value.to_s.strip
112
+ a.value = "#{a.value}#{suffix}" if v.start_with?("#") && ids[v.sub(/\A#/, "")]
113
+ end
114
+ end
115
+ end
116
+
117
+ # The CSS-referenceable properties present among the id-bearing
118
+ # elements, plus "style" when any exist (standoc svg_iri_properties).
119
+ def iri_properties(id_elems)
120
+ iri_tag_names = id_elems.filter_map do |e|
121
+ e.name if IRI_TAG_PROPERTIES_MAP.key?(e.name)
122
+ end.uniq
123
+ iri_properties = iri_tag_names.flat_map do |t|
124
+ IRI_TAG_PROPERTIES_MAP[t] || [t]
125
+ end.uniq
126
+ return [] if iri_properties.empty?
127
+
128
+ iri_properties + ["style"]
129
+ end
130
+
131
+ def rewrite_urls(text, suffix, ids)
132
+ text.gsub(URL_RE) do |x|
133
+ ids[$1] ? "url(##{$1}#{suffix})" : x
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vectory
4
- VERSION = "0.13.1"
4
+ VERSION = "0.13.2"
5
5
  end
data/lib/vectory.rb CHANGED
@@ -37,6 +37,7 @@ module Vectory
37
37
  autoload :Svg, "vectory/svg"
38
38
  autoload :SvgMapping, "vectory/svg_mapping"
39
39
  autoload :SvgDocument, "vectory/svg_document"
40
+ autoload :SvgIdDisambiguator, "vectory/svg_id_disambiguator"
40
41
  autoload :FileMagic, "vectory/file_magic"
41
42
  end
42
43
 
data/vectory.gemspec CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
21
21
  spec.executables = ["vectory"]
22
22
  spec.require_paths = ["lib"]
23
23
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
24
- f.match(%r{^(bin|spec)/})
24
+ f.match(%r{^(bin|spec|TODO\.[^/]+)/})
25
25
  end
26
26
  spec.test_files = `git ls-files -- {spec}/*`.split("\n")
27
27
  spec.required_ruby_version = ">= 3.1.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vectory
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.1
4
+ version: 0.13.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-17 00:00:00.000000000 Z
11
+ date: 2026-10-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64
@@ -168,6 +168,7 @@ files:
168
168
  - lib/vectory/ps.rb
169
169
  - lib/vectory/svg.rb
170
170
  - lib/vectory/svg_document.rb
171
+ - lib/vectory/svg_id_disambiguator.rb
171
172
  - lib/vectory/svg_mapping.rb
172
173
  - lib/vectory/utils.rb
173
174
  - lib/vectory/vector.rb