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.
- 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
|