prawn-svg 0.27.1 → 0.28.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 78e98b74bfb10e36c2de7a051662546c98ddcfa5
4
- data.tar.gz: 9fe692aebb49298a976f9436141c9bf9f4e3b750
3
+ metadata.gz: 5f5f1b932d2ff278ac37b5957a18b57d51013345
4
+ data.tar.gz: 7502fc93ebe947b1955d478292bf71c7b5dd40c0
5
5
  SHA512:
6
- metadata.gz: bca8bd9a84bb372434d0a6aac7911c13e097fbdc30e788c9dad883a922237b384cc6f9786a3992193bd46ee47e562274bbe5e7f39a7ad6eba0a9311422e40c24
7
- data.tar.gz: 67af84522eb27d03b18afa2d9f6309b4a2588f411ddf123681dd69bf34992a85c2aab62946d87ab025bb12c66df3e2189b3543c5eba4e79c4de691003206696b
6
+ metadata.gz: 770335f6cd4fbff5a30d6d6ff965ff2148e66543342de66a320583f4f24ec040d341054749c619c7a9ef1491e1e0155158ba63ea407b440406791dec60fc57f2
7
+ data.tar.gz: 8e1d9b9a00984309065c1ffc93b7695aafb97d4fbc91c3b6b76d7cc9ee4fe106365b23672f7a87fe0edc97e2612a7d7d3adfe557d5aaa1af4a0c90fb15652717
data/README.md CHANGED
@@ -81,7 +81,7 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
81
81
  - `<switch>` and `<foreignObject>`, although prawn-svg cannot handle any data that is not SVG so `<foreignObject>`
82
82
  tags are always ignored.
83
83
 
84
- - properties: `clip-path`, `color`, `display`, `fill-opacity`, `fill`, `opacity`, `overflow`, `stroke`, `stroke-dasharray`, `stroke-linecap`, `stroke-opacity`, `stroke-width`
84
+ - properties: `clip-path`, `color`, `display`, `fill`, `fill-opacity`, `fill-rule`, `opacity`, `overflow`, `stroke`, `stroke-dasharray`, `stroke-linecap`, `stroke-opacity`, `stroke-width`
85
85
 
86
86
  - properties on lines, polylines, polygons and paths: `marker-end`, `marker-mid`, `marker-start`
87
87
 
@@ -101,12 +101,21 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
101
101
 
102
102
  ## CSS
103
103
 
104
- prawn-svg uses the css_parser gem to parse CSS <tt>&lt;style&gt;</tt> blocks. It only handles simple tag, class or id selectors; attribute and other advanced selectors are not supported by the gem.
104
+ prawn-svg supports CSS, both in `<style>` blocks and `style` attributes.
105
+
106
+ In CSS selectors you can use element names, IDs, classes, attributes (existence, `=`, `^=`, `$=`, `*=`, `~=`, `|=`)
107
+ and all combinators (` `, `>`, `+`, `~`).
108
+ The pseudo-classes `:first-child`, `:last-child` and `:nth-child(n)` (where n is a number) also work.
109
+
110
+ Pseudo-elements and the other pseudo-classes are not supported. Specificity ordering is
111
+ implemented, but `!important` is not.
105
112
 
106
113
  ## Not supported
107
114
 
108
115
  prawn-svg does not support radial gradients, patterns or filters.
109
116
 
117
+ It does not support text in the clip area, but you can clip shapes and text by any shape.
118
+
110
119
  ## Configuration
111
120
 
112
121
  ### Fonts
@@ -21,7 +21,9 @@ require 'prawn/svg/pathable'
21
21
  require 'prawn/svg/elements'
22
22
  require 'prawn/svg/extension'
23
23
  require 'prawn/svg/interface'
24
- require 'prawn/svg/css'
24
+ require 'prawn/svg/css/font_family_parser'
25
+ require 'prawn/svg/css/selector_parser'
26
+ require 'prawn/svg/css/stylesheets'
25
27
  require 'prawn/svg/ttf'
26
28
  require 'prawn/svg/font'
27
29
  require 'prawn/svg/document'
@@ -1,6 +1,6 @@
1
- class Prawn::SVG::CSS
2
- class << self
3
- def parse_font_family_string(string)
1
+ module Prawn::SVG::CSS
2
+ class FontFamilyParser
3
+ def self.parse(string)
4
4
  in_quote = nil
5
5
  in_escape = false
6
6
  current = nil
@@ -37,4 +37,3 @@ class Prawn::SVG::CSS
37
37
  end
38
38
  end
39
39
  end
40
-
@@ -0,0 +1,174 @@
1
+ module Prawn::SVG::CSS
2
+ class SelectorParser
3
+ def self.parse(selector)
4
+ tokens = tokenise_css_selector(selector) or return
5
+
6
+ result = [{}]
7
+ part = nil
8
+
9
+ tokens.each do |token|
10
+ case token
11
+ when Modifier
12
+ part = token.type
13
+ result.last[part] ||= part == :name ? "" : []
14
+ when Identifier
15
+ return unless part
16
+ result.last[part] << token.name
17
+ when Attribute
18
+ return unless ["=", "*=", "~=", "^=", "|=", "$=", nil].include?(token.operator)
19
+ (result.last[:attribute] ||= []) << [token.key, token.operator, token.value]
20
+ when Combinator
21
+ result << {combinator: token.type}
22
+ part = nil
23
+ end
24
+ end
25
+
26
+ result
27
+ end
28
+
29
+ private
30
+
31
+ VALID_CSS_IDENTIFIER_CHAR = /[a-zA-Z0-9_\u00a0-\uffff-]/
32
+ Identifier = Struct.new(:name)
33
+ Modifier = Struct.new(:type)
34
+ Combinator = Struct.new(:type)
35
+ Attribute = Struct.new(:key, :operator, :value)
36
+
37
+ def self.tokenise_css_selector(selector)
38
+ result = []
39
+ brackets = false
40
+ attribute = false
41
+ quote = false
42
+
43
+ selector.strip.chars do |char|
44
+ if brackets
45
+ result.last.name << char
46
+ brackets = false if char == ')'
47
+ elsif attribute
48
+ case attribute
49
+ when :pre_key
50
+ if VALID_CSS_IDENTIFIER_CHAR.match(char)
51
+ result.last.key = char
52
+ attribute = :key
53
+ elsif char != " " && char != "\t"
54
+ return
55
+ end
56
+
57
+ when :key
58
+ if VALID_CSS_IDENTIFIER_CHAR.match(char)
59
+ result.last.key << char
60
+ elsif char == "]"
61
+ attribute = nil
62
+ elsif "=*~^|$".include?(char)
63
+ result.last.operator = char
64
+ attribute = :operator
65
+ elsif char == " " || char == "\t"
66
+ attribute = :pre_operator
67
+ else
68
+ return
69
+ end
70
+
71
+ when :pre_operator
72
+ if "=*~^|$".include?(char)
73
+ result.last.operator = char
74
+ attribute = :operator
75
+ elsif char != " " && char != "\t"
76
+ return
77
+ end
78
+
79
+ when :operator
80
+ if "=*~^|$".include?(char)
81
+ result.last.operator << char
82
+ elsif char == " " || char == "\t"
83
+ attribute = :pre_value
84
+ elsif char == '"' || char == "'"
85
+ result.last.value = ''
86
+ attribute = char
87
+ else
88
+ result.last.value = char
89
+ attribute = :value
90
+ end
91
+
92
+ when :pre_value
93
+ if char == '"' || char == "'"
94
+ result.last.value = ''
95
+ attribute = char
96
+ elsif char != " " && char != "\t"
97
+ result.last.value = char
98
+ attribute = :value
99
+ end
100
+
101
+ when :value
102
+ if char == "]"
103
+ result.last.value = result.last.value.rstrip
104
+ attribute = nil
105
+ else
106
+ result.last.value << char
107
+ end
108
+
109
+ when '"', "'"
110
+ if char == "\\" && !quote
111
+ quote = true
112
+ elsif char == attribute && !quote
113
+ attribute = :post_string
114
+ else
115
+ quote = false
116
+ result.last.value << char
117
+ end
118
+
119
+ when :post_string
120
+ if char == "]"
121
+ attribute = nil
122
+ elsif char != " " && char != "\t"
123
+ return
124
+ end
125
+ end
126
+
127
+ elsif VALID_CSS_IDENTIFIER_CHAR.match(char)
128
+ case result.last
129
+ when Identifier
130
+ result.last.name << char
131
+ else
132
+ result << Modifier.new(:name) if !result.last.is_a?(Modifier)
133
+ result << Identifier.new(char)
134
+ end
135
+ else
136
+ case char
137
+ when "."
138
+ result << Modifier.new(:class)
139
+ when "#"
140
+ result << Modifier.new(:id)
141
+ when ":"
142
+ result << Modifier.new(:pseudo_class)
143
+ when " ", "\t"
144
+ result << Combinator.new(:descendant) unless result.last.is_a?(Combinator)
145
+ when ">"
146
+ result.pop if result.last == Combinator.new(:descendant)
147
+ result << Combinator.new(:child)
148
+ when "+"
149
+ result.pop if result.last == Combinator.new(:descendant)
150
+ result << Combinator.new(:adjacent)
151
+ when "~"
152
+ result.pop if result.last == Combinator.new(:descendant)
153
+ result << Combinator.new(:siblings)
154
+ when "*"
155
+ return unless result.empty? || result.last.is_a?(Combinator)
156
+ result << Modifier.new(:name)
157
+ result << Identifier.new("*")
158
+ when "(" # e.g. :nth-child(3n+4)
159
+ return unless result.last.is_a?(Identifier) && result[-2] && result[-2].is_a?(Modifier) && result[-2].type == :pseudo_class
160
+ result.last.name << "("
161
+ brackets = true
162
+ when "["
163
+ result << Attribute.new
164
+ attribute = :pre_key
165
+ else
166
+ return # unsupported Combinator
167
+ end
168
+ end
169
+ end
170
+
171
+ result unless brackets || attribute
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,125 @@
1
+ module Prawn::SVG::CSS
2
+ class Stylesheets
3
+ attr_reader :css_parser, :root, :media
4
+
5
+ def initialize(css_parser, root, media = :all)
6
+ @css_parser = css_parser
7
+ @root = root
8
+ @media = media
9
+ end
10
+
11
+ def load
12
+ load_style_elements
13
+ xpath_styles = gather_xpath_styles
14
+ associate_xpath_styles_with_elements(xpath_styles)
15
+ end
16
+
17
+ private
18
+
19
+ def load_style_elements
20
+ REXML::XPath.match(root, '//style').each do |source|
21
+ data = source.texts.map(&:value).join
22
+ css_parser.add_block!(data)
23
+ end
24
+ end
25
+
26
+ def gather_xpath_styles
27
+ xpath_styles = []
28
+ order = 0
29
+
30
+ css_parser.each_rule_set(media) do |rule_set, _|
31
+ declarations = []
32
+ rule_set.each_declaration { |*data| declarations << data }
33
+
34
+ rule_set.selectors.each do |selector_text|
35
+ selector = Prawn::SVG::CSS::SelectorParser.parse(selector_text)
36
+ xpath = css_selector_to_xpath(selector)
37
+ specificity = calculate_specificity(selector)
38
+ specificity << order
39
+ order += 1
40
+
41
+ xpath_styles << [xpath, declarations, specificity]
42
+ end
43
+ end
44
+
45
+ xpath_styles.sort_by(&:last)
46
+ end
47
+
48
+ def associate_xpath_styles_with_elements(xpath_styles)
49
+ element_styles = {}
50
+
51
+ xpath_styles.each do |xpath, declarations, _|
52
+ REXML::XPath.match(root, xpath).each do |element|
53
+ (element_styles[element] ||= []).concat declarations
54
+ end
55
+ end
56
+
57
+ element_styles
58
+ end
59
+
60
+ def xpath_quote(value)
61
+ %{"#{value.gsub('\\', '\\\\').gsub('"', '\\"')}"} if value
62
+ end
63
+
64
+ def css_selector_to_xpath(selector)
65
+ selector.map do |element|
66
+ pseudo_classes = Set.new(element[:pseudo_class])
67
+
68
+ result = case element[:combinator]
69
+ when :child
70
+ '/'
71
+ when :adjacent
72
+ pseudo_classes << 'first-child'
73
+ '/following-sibling::'
74
+ when :siblings
75
+ '/following-sibling::'
76
+ else
77
+ '//'
78
+ end
79
+
80
+ result << element[:name] if element[:name]
81
+ result << ((element[:class] || []).map { |name| "[contains(concat(' ',@class,' '), ' #{name} ')]" }.join)
82
+ result << ((element[:id] || []).map { |name| "[@id='#{name}']" }.join)
83
+
84
+ (element[:attribute] || []).each do |key, operator, value|
85
+ case operator
86
+ when nil
87
+ result << "[@#{key}]"
88
+ when "="
89
+ result << "[@#{key}=#{xpath_quote value}]"
90
+ when "^="
91
+ result << "[starts-with(@#{key}, #{xpath_quote value})]"
92
+ when "$="
93
+ result << "[substring(@#{key}, string-length(@#{key}) - #{value.length - 1}) = #{xpath_quote value})]"
94
+ when "*="
95
+ result << "[contains(@#{key}, #{xpath_quote value})]"
96
+ when "~="
97
+ result << "[contains(concat(' ',@#{key},' '), #{xpath_quote " #{value} "})]"
98
+ when "|="
99
+ result << "[contains(concat('-',@#{key},'-'), #{xpath_quote "-#{value}-"})]"
100
+ end
101
+ end
102
+
103
+ pseudo_classes.each do |pc|
104
+ case pc
105
+ when "first-child" then result << "[1]"
106
+ when "last-child" then result << "[last()]"
107
+ when /^nth-child\((\d+)\)$/ then result << "[#{$1}]"
108
+ end
109
+ end
110
+
111
+ result
112
+ end.join
113
+ end
114
+
115
+ def calculate_specificity(selector)
116
+ selector.reduce([0, 0, 0]) do |(a, b, c), element|
117
+ [
118
+ a + (element[:id] || []).length,
119
+ b + (element[:class] || []).length + (element[:attribute] || []).length + (element[:pseudo_class] || []).length,
120
+ c + (element[:name] && element[:name] != "*" ? 1 : 0)
121
+ ]
122
+ end
123
+ end
124
+ end
125
+ end
@@ -12,7 +12,8 @@ class Prawn::SVG::Document
12
12
  :fallback_font_name,
13
13
  :font_registry,
14
14
  :url_loader,
15
- :css_parser, :elements_by_id, :gradients
15
+ :elements_by_id, :gradients,
16
+ :element_styles
16
17
 
17
18
  def initialize(data, bounds, options, font_registry: nil, css_parser: CssParser::Parser.new)
18
19
  @root = REXML::Document.new(data).root
@@ -31,7 +32,6 @@ class Prawn::SVG::Document
31
32
  @gradients = {}
32
33
  @fallback_font_name = options.fetch(:fallback_font_name, DEFAULT_FALLBACK_FONT_NAME)
33
34
  @font_registry = font_registry
34
- @css_parser = css_parser
35
35
 
36
36
  @url_loader = Prawn::SVG::UrlLoader.new(
37
37
  enable_cache: options[:cache_images],
@@ -42,7 +42,7 @@ class Prawn::SVG::Document
42
42
  @sizing = Prawn::SVG::Calculators::DocumentSizing.new(bounds, @root.attributes)
43
43
  calculate_sizing(requested_width: options[:width], requested_height: options[:height])
44
44
 
45
- parse_style_elements
45
+ @element_styles = Prawn::SVG::CSS::Stylesheets.new(css_parser, root).load
46
46
 
47
47
  yield self if block_given?
48
48
  end
@@ -52,16 +52,4 @@ class Prawn::SVG::Document
52
52
  sizing.requested_height = requested_height
53
53
  sizing.calculate
54
54
  end
55
-
56
- private
57
-
58
- # <style> elements specified anywhere in the document apply to the entire
59
- # document. Because of this, we load all <style> elements before parsing
60
- # the rest of the document.
61
- def parse_style_elements
62
- REXML::XPath.match(root, '//style').each do |source|
63
- data = source.texts.map(&:value).join
64
- css_parser.add_block!(data)
65
- end
66
- end
67
55
  end
@@ -4,7 +4,7 @@ end
4
4
 
5
5
  require 'prawn/svg/elements/call_duplicator'
6
6
 
7
- %w(base depth_first_base root container viewport text text_component line polyline polygon circle ellipse rect path use image gradient marker ignored).each do |filename|
7
+ %w(base depth_first_base root container clip_path viewport text text_component line polyline polygon circle ellipse rect path use image gradient marker ignored).each do |filename|
8
8
  require "prawn/svg/elements/#{filename}"
9
9
  end
10
10
 
@@ -13,7 +13,7 @@ module Prawn::SVG::Elements
13
13
  g: Prawn::SVG::Elements::Container,
14
14
  symbol: Prawn::SVG::Elements::Container,
15
15
  defs: Prawn::SVG::Elements::Container,
16
- clipPath: Prawn::SVG::Elements::Container,
16
+ clipPath: Prawn::SVG::Elements::ClipPath,
17
17
  switch: Prawn::SVG::Elements::Container,
18
18
  svg: Prawn::SVG::Elements::Viewport,
19
19
  text: Prawn::SVG::Elements::Text,
@@ -157,14 +157,23 @@ class Prawn::SVG::Elements::Base
157
157
  end
158
158
 
159
159
  def apply_drawing_call
160
- if !state.disable_drawing && drawable?
161
- draw_types = PAINT_TYPES.select { |property| computed_properties.send(property) != 'none' }
160
+ return if state.disable_drawing || !drawable?
162
161
 
163
- if draw_types.empty?
164
- add_call_and_enter("end_path")
162
+ fill = computed_properties.fill != 'none'
163
+ stroke = computed_properties.stroke != 'none'
164
+
165
+ if fill
166
+ command = stroke ? 'fill_and_stroke' : 'fill'
167
+
168
+ if computed_properties.fill_rule == 'evenodd'
169
+ add_call_and_enter(command, {fill_rule: :even_odd})
165
170
  else
166
- add_call_and_enter(draw_types.join("_and_"))
171
+ add_call_and_enter(command)
167
172
  end
173
+ elsif stroke
174
+ add_call_and_enter('stroke')
175
+ else
176
+ add_call_and_enter('end_path')
168
177
  end
169
178
  end
170
179
 
@@ -207,32 +216,20 @@ class Prawn::SVG::Elements::Base
207
216
  end
208
217
 
209
218
  def extract_attributes_and_properties
210
- if @document && @document.css_parser
211
- tag_style = @document.css_parser.find_by_selector(source.name)
212
- id_style = @document.css_parser.find_by_selector("##{source.attributes["id"]}") if source.attributes["id"]
213
-
214
- if classes = source.attributes["class"]
215
- class_styles = classes.split(' ').collect do |class_name|
216
- @document.css_parser.find_by_selector(".#{class_name}")
217
- end
219
+ if styles = document.element_styles[source]
220
+ # TODO : implement !important, at the moment it's just ignored
221
+ styles.each do |name, value, _important|
222
+ @properties.set(name, value)
218
223
  end
219
-
220
- element_style = source.attributes['style']
221
-
222
- style = [tag_style, class_styles, id_style, element_style].flatten.collect do |s|
223
- s.nil? || s.strip == "" ? "" : "#{s}#{";" unless s.match(/;\s*\z/)}"
224
- end.join
225
- else
226
- style = source.attributes['style'] || ""
227
224
  end
228
225
 
226
+ @properties.load_hash(parse_css_declarations(source.attributes['style'] || ''))
227
+
229
228
  source.attributes.each do |name, value|
230
229
  # Properties#set returns nil if it's not a recognised property name
231
230
  @properties.set(name, value) or @attributes[name] = value
232
231
  end
233
232
 
234
- @properties.load_hash(parse_css_declarations(style))
235
-
236
233
  state.computed_properties.compute_properties(@properties)
237
234
  end
238
235