prawn-svg 0.27.1 → 0.31.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +5 -5
  2. data/.travis.yml +6 -4
  3. data/LICENSE +1 -1
  4. data/README.md +23 -9
  5. data/lib/prawn-svg.rb +7 -1
  6. data/lib/prawn/svg/attributes/opacity.rb +4 -4
  7. data/lib/prawn/svg/attributes/transform.rb +2 -44
  8. data/lib/prawn/svg/calculators/document_sizing.rb +2 -2
  9. data/lib/prawn/svg/{css.rb → css/font_family_parser.rb} +3 -4
  10. data/lib/prawn/svg/css/selector_parser.rb +174 -0
  11. data/lib/prawn/svg/css/stylesheets.rb +146 -0
  12. data/lib/prawn/svg/document.rb +3 -15
  13. data/lib/prawn/svg/elements.rb +4 -2
  14. data/lib/prawn/svg/elements/base.rb +26 -23
  15. data/lib/prawn/svg/elements/clip_path.rb +12 -0
  16. data/lib/prawn/svg/elements/container.rb +1 -3
  17. data/lib/prawn/svg/elements/gradient.rb +83 -25
  18. data/lib/prawn/svg/elements/image.rb +2 -2
  19. data/lib/prawn/svg/elements/path.rb +42 -29
  20. data/lib/prawn/svg/elements/root.rb +4 -1
  21. data/lib/prawn/svg/elements/text.rb +1 -1
  22. data/lib/prawn/svg/elements/text_component.rb +63 -14
  23. data/lib/prawn/svg/elements/use.rb +23 -7
  24. data/lib/prawn/svg/extensions/additional_gradient_transforms.rb +23 -0
  25. data/lib/prawn/svg/font_registry.rb +4 -3
  26. data/lib/prawn/svg/interface.rb +26 -2
  27. data/lib/prawn/svg/loaders/data.rb +1 -1
  28. data/lib/prawn/svg/loaders/file.rb +4 -2
  29. data/lib/prawn/svg/properties.rb +2 -0
  30. data/lib/prawn/svg/state.rb +6 -3
  31. data/lib/prawn/svg/transform_parser.rb +72 -0
  32. data/lib/prawn/svg/version.rb +1 -1
  33. data/prawn-svg.gemspec +3 -4
  34. data/spec/prawn/svg/attributes/opacity_spec.rb +85 -0
  35. data/spec/prawn/svg/attributes/transform_spec.rb +30 -35
  36. data/spec/prawn/svg/calculators/document_sizing_spec.rb +4 -4
  37. data/spec/prawn/svg/{css_spec.rb → css/font_family_parser_spec.rb} +3 -3
  38. data/spec/prawn/svg/css/selector_parser_spec.rb +33 -0
  39. data/spec/prawn/svg/css/stylesheets_spec.rb +142 -0
  40. data/spec/prawn/svg/document_spec.rb +0 -33
  41. data/spec/prawn/svg/elements/base_spec.rb +58 -2
  42. data/spec/prawn/svg/elements/gradient_spec.rb +79 -4
  43. data/spec/prawn/svg/elements/path_spec.rb +29 -17
  44. data/spec/prawn/svg/elements/text_spec.rb +74 -16
  45. data/spec/prawn/svg/font_registry_spec.rb +30 -0
  46. data/spec/prawn/svg/interface_spec.rb +33 -1
  47. data/spec/prawn/svg/loaders/data_spec.rb +8 -0
  48. data/spec/prawn/svg/transform_parser_spec.rb +94 -0
  49. data/spec/sample_output/{directory → .keep} +0 -0
  50. data/spec/sample_svg/double_opacity.svg +6 -0
  51. data/spec/sample_svg/gradient_transform.svg +19 -0
  52. data/spec/sample_svg/links.svg +18 -0
  53. data/spec/sample_svg/radgrad01-bounding.svg +26 -0
  54. data/spec/sample_svg/radgrad01.svg +26 -0
  55. data/spec/sample_svg/svg_fill.svg +5 -0
  56. data/spec/sample_svg/text-decoration.svg +4 -0
  57. data/spec/sample_svg/text_stroke.svg +41 -0
  58. data/spec/sample_svg/transform.svg +20 -0
  59. data/spec/sample_svg/use_disordered.svg +17 -0
  60. data/spec/sample_svg/warning-radioactive.svg +98 -0
  61. data/spec/spec_helper.rb +2 -2
  62. metadata +137 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 78e98b74bfb10e36c2de7a051662546c98ddcfa5
4
- data.tar.gz: 9fe692aebb49298a976f9436141c9bf9f4e3b750
2
+ SHA256:
3
+ metadata.gz: 9ed78a64dc94709fc5e53f1d6f521f93e3bf41196c29dde616c6139743c9c889
4
+ data.tar.gz: 7a0a5683370d3478e8b9717069e0f4012337aaf6250106065dfd5ad9b4458a91
5
5
  SHA512:
6
- metadata.gz: bca8bd9a84bb372434d0a6aac7911c13e097fbdc30e788c9dad883a922237b384cc6f9786a3992193bd46ee47e562274bbe5e7f39a7ad6eba0a9311422e40c24
7
- data.tar.gz: 67af84522eb27d03b18afa2d9f6309b4a2588f411ddf123681dd69bf34992a85c2aab62946d87ab025bb12c66df3e2189b3543c5eba4e79c4de691003206696b
6
+ metadata.gz: a58fd3f4508d486b3632f2e337f5e2fe97ef8c2baeeeda9b4f53f816ff18eb10b3e8879c98ea10c645becb29c8351a7addc6867c22abfeda77a7edf654607f44
7
+ data.tar.gz: 3bdbbc06f25405d3f51de30e81c39406f7ccf535e507bb36b43df3939d1a4931b763cb5308bde72674ab3f2ffcb55171b958f93a3b603bf5cf17a1bbd24d44b4
@@ -1,6 +1,8 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.1.10
4
- - 2.2.7
5
- - 2.3.3
6
- - 2.4.0
3
+ - 2.2.10
4
+ - 2.3.8
5
+ - 2.4.10
6
+ - 2.5.8
7
+ - 2.6.6
8
+ - 2.7.1
data/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License
2
2
 
3
- Copyright 2010-2013 Roger Nesbitt
3
+ Copyright 2010-2019 Roger Nesbitt
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -60,8 +60,8 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
60
60
  - <tt>&lt;path&gt;</tt> supports all commands defined in SVG 1.1, although the
61
61
  implementation of elliptical arc is a bit rough at the moment.
62
62
 
63
- - `<text>`, `<tspan>` and `<tref>` with attributes `x`, `y`, `dx`, `dy`, `rotate`, and with extra properties
64
- `text-anchor`, `font-size`, `font-family`, `font-weight`, `font-style`, `letter-spacing`
63
+ - `<text>`, `<tspan>` and `<tref>` with attributes `x`, `y`, `dx`, `dy`, `rotate`, 'textLength', 'lengthAdjust', and with extra properties
64
+ `text-anchor`, `text-decoration` (underline only), `font-size`, `font-family`, `font-weight`, `font-style`, `letter-spacing`
65
65
 
66
66
  - <tt>&lt;svg&gt;</tt>, <tt>&lt;g&gt;</tt> and <tt>&lt;symbol&gt;</tt>
67
67
 
@@ -76,12 +76,12 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
76
76
 
77
77
  - `<marker>`
78
78
 
79
- - `<linearGradient>` is implemented with Prawn 2.2.0+ (gradientTransform, spreadMethod and stop-opacity are unimplemented.)
79
+ - `<linearGradient>` and `<radialGradient>` are implemented on Prawn 2.2.0+ with attributes `gradientUnits` and `gradientTransform` (spreadMethod and stop-opacity are unimplemented.)
80
80
 
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
 
@@ -91,7 +91,7 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre
91
91
 
92
92
  - the <tt>preserveAspectRatio</tt> attribute on <tt>&lt;svg&gt;</tt>, <tt>&lt;image&gt;</tt> and `<marker>` elements
93
93
 
94
- - transform methods: <tt>translate()</tt>, <tt>rotate()</tt>, <tt>scale()</tt>, <tt>matrix()</tt>
94
+ - transform methods: `translate`, `translateX`, `translateY`, `rotate`, `scale`, `skewX`, `skewY`, `matrix`
95
95
 
96
96
  - colors: HTML standard names, <tt>#xxx</tt>, <tt>#xxxxxx</tt>, <tt>rgb(1, 2, 3)</tt>, <tt>rgb(1%, 2%, 3%)</tt>
97
97
 
@@ -101,11 +101,24 @@ 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
+ Warning: Ruby versions less than 2.6.0 have a bug in the REXML XPath implementation which means under some
111
+ conditions the `+` combinator will not pick up all matching elements. See `stylesheets_spec.rb` for an
112
+ explanation if you're stuck on an old version of Ruby.
113
+
114
+ Pseudo-elements and the other pseudo-classes are not supported. Specificity ordering is
115
+ implemented, but `!important` is not.
105
116
 
106
117
  ## Not supported
107
118
 
108
- prawn-svg does not support radial gradients, patterns or filters.
119
+ prawn-svg does not support hyperlinks, patterns or filters.
120
+
121
+ It does not support text in the clip area, but you can clip shapes and text by any shape.
109
122
 
110
123
  ## Configuration
111
124
 
@@ -122,5 +135,6 @@ Mac OS X and Debian Linux users. You can add to the font path:
122
135
 
123
136
  In your Gemfile, put `gem 'prawn-svg'` before `gem 'prawn-rails'` so that prawn-rails can see the prawn-svg extension.
124
137
 
125
- --
126
- Copyright Roger Nesbitt <roger@seriousorange.com>. MIT licence.
138
+ ## Licence
139
+
140
+ MIT licence. Copyright Roger Nesbitt.
@@ -10,6 +10,7 @@ require 'prawn/svg/calculators/arc_to_bezier_curve'
10
10
  require 'prawn/svg/calculators/aspect_ratio'
11
11
  require 'prawn/svg/calculators/document_sizing'
12
12
  require 'prawn/svg/calculators/pixels'
13
+ require 'prawn/svg/transform_parser'
13
14
  require 'prawn/svg/url_loader'
14
15
  require 'prawn/svg/loaders/data'
15
16
  require 'prawn/svg/loaders/file'
@@ -21,12 +22,17 @@ require 'prawn/svg/pathable'
21
22
  require 'prawn/svg/elements'
22
23
  require 'prawn/svg/extension'
23
24
  require 'prawn/svg/interface'
24
- require 'prawn/svg/css'
25
+ require 'prawn/svg/css/font_family_parser'
26
+ require 'prawn/svg/css/selector_parser'
27
+ require 'prawn/svg/css/stylesheets'
25
28
  require 'prawn/svg/ttf'
26
29
  require 'prawn/svg/font'
27
30
  require 'prawn/svg/document'
28
31
  require 'prawn/svg/state'
29
32
 
33
+ require 'prawn/svg/extensions/additional_gradient_transforms'
34
+ Prawn::Document.prepend Prawn::SVG::Extensions::AdditionalGradientTransforms
35
+
30
36
  module Prawn
31
37
  Svg = SVG # backwards compatibility
32
38
  end
@@ -1,13 +1,13 @@
1
1
  module Prawn::SVG::Attributes::Opacity
2
2
  def parse_opacity_attributes_and_call
3
3
  # We can't do nested opacities quite like the SVG requires, but this is close enough.
4
- fill_opacity = stroke_opacity = clamp(properties.opacity.to_f, 0, 1) if properties.opacity
4
+ opacity = clamp(properties.opacity.to_f, 0, 1) if properties.opacity
5
5
  fill_opacity = clamp(properties.fill_opacity.to_f, 0, 1) if properties.fill_opacity
6
6
  stroke_opacity = clamp(properties.stroke_opacity.to_f, 0, 1) if properties.stroke_opacity
7
7
 
8
- if fill_opacity || stroke_opacity
9
- state.fill_opacity *= fill_opacity || 1
10
- state.stroke_opacity *= stroke_opacity || 1
8
+ if opacity || fill_opacity || stroke_opacity
9
+ state.fill_opacity *= [opacity || 1, fill_opacity || 1].min
10
+ state.stroke_opacity *= [opacity || 1, stroke_opacity || 1].min
11
11
 
12
12
  add_call_and_enter 'transparent', state.fill_opacity, state.stroke_opacity
13
13
  end
@@ -2,49 +2,7 @@ module Prawn::SVG::Attributes::Transform
2
2
  def parse_transform_attribute_and_call
3
3
  return unless transform = attributes['transform']
4
4
 
5
- parse_css_method_calls(transform).each do |name, arguments|
6
- case name
7
- when 'translate'
8
- x, y = arguments
9
- add_call_and_enter name, x_pixels(x.to_f), -y_pixels(y.to_f)
10
-
11
- when 'rotate'
12
- r, x, y = arguments.collect {|a| a.to_f}
13
- case arguments.length
14
- when 1
15
- add_call_and_enter name, -r, :origin => [0, y('0')]
16
- when 3
17
- add_call_and_enter name, -r, :origin => [x(x), y(y)]
18
- else
19
- warnings << "transform 'rotate' must have either one or three arguments"
20
- end
21
-
22
- when 'scale'
23
- x_scale = arguments[0].to_f
24
- y_scale = (arguments[1] || x_scale).to_f
25
- add_call_and_enter "transformation_matrix", x_scale, 0, 0, y_scale, 0, 0
26
-
27
- when 'matrix'
28
- if arguments.length != 6
29
- warnings << "transform 'matrix' must have six arguments"
30
- else
31
- a, b, c, d, e, f = arguments.collect {|argument| argument.to_f}
32
- add_call_and_enter "transformation_matrix", a, -b, -c, d, x_pixels(e), -y_pixels(f)
33
- end
34
-
35
- else
36
- warnings << "Unknown transformation '#{name}'; ignoring"
37
- end
38
- end
39
- end
40
-
41
- private
42
-
43
- def parse_css_method_calls(string)
44
- string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
45
- name, argument_string = call
46
- arguments = argument_string.strip.split(/\s*[,\s]\s*/)
47
- [name, arguments]
48
- end
5
+ matrix = parse_transform_attribute(transform)
6
+ add_call_and_enter "transformation_matrix", *matrix unless matrix == [1, 0, 0, 1, 0, 0]
49
7
  end
50
8
  end
@@ -36,10 +36,10 @@ module Prawn::SVG::Calculators
36
36
  @x_offset, @y_offset, @viewport_width, @viewport_height = values.map {|value| value.to_f}
37
37
 
38
38
  if @viewport_width > 0 && @viewport_height > 0
39
- # If neither the width nor height was specified, use the entire space available
39
+ # If neither the width nor height was specified, use the entire width and the viewbox ratio
40
+ # to determine the height.
40
41
  if @output_width.nil? && @output_height.nil?
41
42
  @output_width = container_width
42
- @output_height = container_height
43
43
  end
44
44
 
45
45
  # If one of the output dimensions is missing, calculate it from the other one
@@ -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,146 @@
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
+ if 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
+ end
45
+
46
+ xpath_styles.sort_by(&:last)
47
+ end
48
+
49
+ def associate_xpath_styles_with_elements(xpath_styles)
50
+ element_styles = {}
51
+
52
+ xpath_styles.each do |xpath, declarations, _|
53
+ REXML::XPath.match(root, xpath).each do |element|
54
+ (element_styles[element] ||= []).concat declarations
55
+ end
56
+ end
57
+
58
+ element_styles
59
+ end
60
+
61
+ def xpath_quote(value)
62
+ %{"#{value.gsub('\\', '\\\\').gsub('"', '\\"')}"} if value
63
+ end
64
+
65
+ def css_selector_to_xpath(selector)
66
+ selector.map do |element|
67
+ pseudo_classes = Set.new(element[:pseudo_class])
68
+ require_function_name = false
69
+
70
+ result = case element[:combinator]
71
+ when :child
72
+ "/"
73
+ when :adjacent
74
+ pseudo_classes << 'first-child'
75
+ "/following-sibling::"
76
+ when :siblings
77
+ "/following-sibling::"
78
+ else
79
+ "//"
80
+ end
81
+
82
+ positions = []
83
+ pseudo_classes.each do |pc|
84
+ case pc
85
+ when "first-child" then positions << '1'
86
+ when "last-child" then positions << 'last()'
87
+ when /^nth-child\((\d+)\)$/ then positions << $1
88
+ end
89
+ end
90
+
91
+ if !positions.empty?
92
+ result << "*" unless require_function_name
93
+ require_function_name = true
94
+
95
+ logic = if positions.length == 1
96
+ positions.first
97
+ else
98
+ positions.map { |position| "position()=#{position}" }.join(" and ")
99
+ end
100
+
101
+ result << "[#{logic}]"
102
+ end
103
+
104
+ if require_function_name
105
+ result << "[name()=#{xpath_quote element[:name]}]" if element[:name]
106
+ else
107
+ result << (element[:name] || '*')
108
+ end
109
+
110
+ result << ((element[:class] || []).map { |name| "[contains(concat(' ',@class,' '), ' #{name} ')]" }.join)
111
+ result << ((element[:id] || []).map { |name| "[@id='#{name}']" }.join)
112
+
113
+ (element[:attribute] || []).each do |key, operator, value|
114
+ case operator
115
+ when nil
116
+ result << "[@#{key}]"
117
+ when "="
118
+ result << "[@#{key}=#{xpath_quote value}]"
119
+ when "^="
120
+ result << "[starts-with(@#{key}, #{xpath_quote value})]"
121
+ when "$="
122
+ result << "[substring(@#{key}, string-length(@#{key}) - #{value.length - 1}) = #{xpath_quote value})]"
123
+ when "*="
124
+ result << "[contains(@#{key}, #{xpath_quote value})]"
125
+ when "~="
126
+ result << "[contains(concat(' ',@#{key},' '), #{xpath_quote " #{value} "})]"
127
+ when "|="
128
+ result << "[contains(concat('-',@#{key},'-'), #{xpath_quote "-#{value}-"})]"
129
+ end
130
+ end
131
+
132
+ result
133
+ end.join
134
+ end
135
+
136
+ def calculate_specificity(selector)
137
+ selector.reduce([0, 0, 0]) do |(a, b, c), element|
138
+ [
139
+ a + (element[:id] || []).length,
140
+ b + (element[:class] || []).length + (element[:attribute] || []).length + (element[:pseudo_class] || []).length,
141
+ c + (element[:name] && element[:name] != "*" ? 1 : 0)
142
+ ]
143
+ end
144
+ end
145
+ end
146
+ end