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.
- checksums.yaml +5 -5
- data/.travis.yml +6 -4
- data/LICENSE +1 -1
- data/README.md +23 -9
- data/lib/prawn-svg.rb +7 -1
- data/lib/prawn/svg/attributes/opacity.rb +4 -4
- data/lib/prawn/svg/attributes/transform.rb +2 -44
- data/lib/prawn/svg/calculators/document_sizing.rb +2 -2
- data/lib/prawn/svg/{css.rb → css/font_family_parser.rb} +3 -4
- data/lib/prawn/svg/css/selector_parser.rb +174 -0
- data/lib/prawn/svg/css/stylesheets.rb +146 -0
- data/lib/prawn/svg/document.rb +3 -15
- data/lib/prawn/svg/elements.rb +4 -2
- data/lib/prawn/svg/elements/base.rb +26 -23
- data/lib/prawn/svg/elements/clip_path.rb +12 -0
- data/lib/prawn/svg/elements/container.rb +1 -3
- data/lib/prawn/svg/elements/gradient.rb +83 -25
- data/lib/prawn/svg/elements/image.rb +2 -2
- data/lib/prawn/svg/elements/path.rb +42 -29
- data/lib/prawn/svg/elements/root.rb +4 -1
- data/lib/prawn/svg/elements/text.rb +1 -1
- data/lib/prawn/svg/elements/text_component.rb +63 -14
- data/lib/prawn/svg/elements/use.rb +23 -7
- data/lib/prawn/svg/extensions/additional_gradient_transforms.rb +23 -0
- data/lib/prawn/svg/font_registry.rb +4 -3
- data/lib/prawn/svg/interface.rb +26 -2
- data/lib/prawn/svg/loaders/data.rb +1 -1
- data/lib/prawn/svg/loaders/file.rb +4 -2
- data/lib/prawn/svg/properties.rb +2 -0
- data/lib/prawn/svg/state.rb +6 -3
- data/lib/prawn/svg/transform_parser.rb +72 -0
- data/lib/prawn/svg/version.rb +1 -1
- data/prawn-svg.gemspec +3 -4
- data/spec/prawn/svg/attributes/opacity_spec.rb +85 -0
- data/spec/prawn/svg/attributes/transform_spec.rb +30 -35
- data/spec/prawn/svg/calculators/document_sizing_spec.rb +4 -4
- data/spec/prawn/svg/{css_spec.rb → css/font_family_parser_spec.rb} +3 -3
- data/spec/prawn/svg/css/selector_parser_spec.rb +33 -0
- data/spec/prawn/svg/css/stylesheets_spec.rb +142 -0
- data/spec/prawn/svg/document_spec.rb +0 -33
- data/spec/prawn/svg/elements/base_spec.rb +58 -2
- data/spec/prawn/svg/elements/gradient_spec.rb +79 -4
- data/spec/prawn/svg/elements/path_spec.rb +29 -17
- data/spec/prawn/svg/elements/text_spec.rb +74 -16
- data/spec/prawn/svg/font_registry_spec.rb +30 -0
- data/spec/prawn/svg/interface_spec.rb +33 -1
- data/spec/prawn/svg/loaders/data_spec.rb +8 -0
- data/spec/prawn/svg/transform_parser_spec.rb +94 -0
- data/spec/sample_output/{directory → .keep} +0 -0
- data/spec/sample_svg/double_opacity.svg +6 -0
- data/spec/sample_svg/gradient_transform.svg +19 -0
- data/spec/sample_svg/links.svg +18 -0
- data/spec/sample_svg/radgrad01-bounding.svg +26 -0
- data/spec/sample_svg/radgrad01.svg +26 -0
- data/spec/sample_svg/svg_fill.svg +5 -0
- data/spec/sample_svg/text-decoration.svg +4 -0
- data/spec/sample_svg/text_stroke.svg +41 -0
- data/spec/sample_svg/transform.svg +20 -0
- data/spec/sample_svg/use_disordered.svg +17 -0
- data/spec/sample_svg/warning-radioactive.svg +98 -0
- data/spec/spec_helper.rb +2 -2
- metadata +137 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9ed78a64dc94709fc5e53f1d6f521f93e3bf41196c29dde616c6139743c9c889
|
4
|
+
data.tar.gz: 7a0a5683370d3478e8b9717069e0f4012337aaf6250106065dfd5ad9b4458a91
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a58fd3f4508d486b3632f2e337f5e2fe97ef8c2baeeeda9b4f53f816ff18eb10b3e8879c98ea10c645becb29c8351a7addc6867c22abfeda77a7edf654607f44
|
7
|
+
data.tar.gz: 3bdbbc06f25405d3f51de30e81c39406f7ccf535e507bb36b43df3939d1a4931b763cb5308bde72674ab3f2ffcb55171b958f93a3b603bf5cf17a1bbd24d44b4
|
data/.travis.yml
CHANGED
data/LICENSE
CHANGED
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><path></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><svg></tt>, <tt><g></tt> and <tt><symbol></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>`
|
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><svg></tt>, <tt><image></tt> and `<marker>` elements
|
93
93
|
|
94
|
-
- transform methods:
|
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
|
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
|
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
|
-
|
138
|
+
## Licence
|
139
|
+
|
140
|
+
MIT licence. Copyright Roger Nesbitt.
|
data/lib/prawn-svg.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
6
|
-
|
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
|
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
|
-
|
2
|
-
class
|
3
|
-
def
|
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
|