prawn-svg 0.27.1 → 0.31.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.
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