prawnx 0.1.1
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 +7 -0
- data/COPYING +2 -0
- data/GPLv2 +339 -0
- data/GPLv3 +674 -0
- data/LICENSE +65 -0
- data/LICENSE-prawn-svg +21 -0
- data/README.md +107 -0
- data/data/fonts/Courier-Bold.afm +342 -0
- data/data/fonts/Courier-BoldOblique.afm +342 -0
- data/data/fonts/Courier-Oblique.afm +342 -0
- data/data/fonts/Courier.afm +342 -0
- data/data/fonts/Helvetica-Bold.afm +2827 -0
- data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
- data/data/fonts/Helvetica-Oblique.afm +3051 -0
- data/data/fonts/Helvetica.afm +3051 -0
- data/data/fonts/MustRead.html +19 -0
- data/data/fonts/Symbol.afm +213 -0
- data/data/fonts/Times-Bold.afm +2588 -0
- data/data/fonts/Times-BoldItalic.afm +2384 -0
- data/data/fonts/Times-Italic.afm +2667 -0
- data/data/fonts/Times-Roman.afm +2419 -0
- data/data/fonts/ZapfDingbats.afm +225 -0
- data/lib/pdf/core.rb +4 -0
- data/lib/prawn/measurement_extensions.rb +4 -0
- data/lib/prawn/svg/attributes/clip_path.rb +46 -0
- data/lib/prawn/svg/attributes/mask.rb +16 -0
- data/lib/prawn/svg/attributes/opacity.rb +30 -0
- data/lib/prawn/svg/attributes/space.rb +10 -0
- data/lib/prawn/svg/attributes/stroke.rb +50 -0
- data/lib/prawn/svg/attributes/transform.rb +10 -0
- data/lib/prawn/svg/attributes.rb +6 -0
- data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
- data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
- data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
- data/lib/prawn/svg/calculators/path_length.rb +168 -0
- data/lib/prawn/svg/calculators/pixels.rb +57 -0
- data/lib/prawn/svg/color.rb +240 -0
- data/lib/prawn/svg/css/font_face_parser.rb +72 -0
- data/lib/prawn/svg/css/font_family_parser.rb +39 -0
- data/lib/prawn/svg/css/font_parser.rb +46 -0
- data/lib/prawn/svg/css/selector_parser.rb +178 -0
- data/lib/prawn/svg/css/stylesheets.rb +233 -0
- data/lib/prawn/svg/css/values_parser.rb +68 -0
- data/lib/prawn/svg/document.rb +170 -0
- data/lib/prawn/svg/elements/anchor.rb +10 -0
- data/lib/prawn/svg/elements/base.rb +312 -0
- data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
- data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
- data/lib/prawn/svg/elements/circle.rb +25 -0
- data/lib/prawn/svg/elements/clip_path.rb +26 -0
- data/lib/prawn/svg/elements/container.rb +10 -0
- data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
- data/lib/prawn/svg/elements/ellipse.rb +22 -0
- data/lib/prawn/svg/elements/gradient.rb +184 -0
- data/lib/prawn/svg/elements/ignored.rb +5 -0
- data/lib/prawn/svg/elements/image.rb +177 -0
- data/lib/prawn/svg/elements/line.rb +27 -0
- data/lib/prawn/svg/elements/marker.rb +72 -0
- data/lib/prawn/svg/elements/mask.rb +65 -0
- data/lib/prawn/svg/elements/path.rb +294 -0
- data/lib/prawn/svg/elements/pattern.rb +150 -0
- data/lib/prawn/svg/elements/polygon.rb +25 -0
- data/lib/prawn/svg/elements/polyline.rb +23 -0
- data/lib/prawn/svg/elements/rect.rb +64 -0
- data/lib/prawn/svg/elements/root.rb +22 -0
- data/lib/prawn/svg/elements/switch.rb +92 -0
- data/lib/prawn/svg/elements/text.rb +99 -0
- data/lib/prawn/svg/elements/text_component.rb +346 -0
- data/lib/prawn/svg/elements/text_node.rb +308 -0
- data/lib/prawn/svg/elements/text_path.rb +153 -0
- data/lib/prawn/svg/elements/use.rb +142 -0
- data/lib/prawn/svg/elements/viewport.rb +29 -0
- data/lib/prawn/svg/elements.rb +46 -0
- data/lib/prawn/svg/extension.rb +25 -0
- data/lib/prawn/svg/font.rb +20 -0
- data/lib/prawn/svg/font_metrics.rb +144 -0
- data/lib/prawn/svg/font_registry.rb +315 -0
- data/lib/prawn/svg/funciri.rb +14 -0
- data/lib/prawn/svg/gradient_renderer.rb +332 -0
- data/lib/prawn/svg/gradients.rb +53 -0
- data/lib/prawn/svg/interface.rb +72 -0
- data/lib/prawn/svg/length.rb +43 -0
- data/lib/prawn/svg/link_renderer.rb +40 -0
- data/lib/prawn/svg/loaders/data.rb +17 -0
- data/lib/prawn/svg/loaders/file.rb +111 -0
- data/lib/prawn/svg/loaders/web.rb +24 -0
- data/lib/prawn/svg/paint.rb +67 -0
- data/lib/prawn/svg/pathable.rb +153 -0
- data/lib/prawn/svg/pattern_renderer.rb +112 -0
- data/lib/prawn/svg/pdfmatrix.rb +37 -0
- data/lib/prawn/svg/percentage.rb +24 -0
- data/lib/prawn/svg/properties.rb +329 -0
- data/lib/prawn/svg/renderer.rb +249 -0
- data/lib/prawn/svg/state.rb +24 -0
- data/lib/prawn/svg/transform_parser.rb +78 -0
- data/lib/prawn/svg/ttc.rb +35 -0
- data/lib/prawn/svg/ttf.rb +95 -0
- data/lib/prawn/svg/url_loader.rb +46 -0
- data/lib/prawn/svg/version.rb +5 -0
- data/lib/prawn/table.rb +4 -0
- data/lib/prawn-svg.rb +53 -0
- data/lib/prawn.rb +10 -0
- data/lib/prawnx/annotations.rb +21 -0
- data/lib/prawnx/document/bounding_box.rb +193 -0
- data/lib/prawnx/document/column_box.rb +91 -0
- data/lib/prawnx/document/internals.rb +144 -0
- data/lib/prawnx/document/span.rb +25 -0
- data/lib/prawnx/document.rb +253 -0
- data/lib/prawnx/encoding.rb +29 -0
- data/lib/prawnx/errors.rb +37 -0
- data/lib/prawnx/font.rb +214 -0
- data/lib/prawnx/fonts/afm.rb +171 -0
- data/lib/prawnx/fonts/otf.rb +50 -0
- data/lib/prawnx/fonts/ttf.rb +410 -0
- data/lib/prawnx/graphic_state.rb +41 -0
- data/lib/prawnx/graphics/color.rb +148 -0
- data/lib/prawnx/graphics/patterns.rb +100 -0
- data/lib/prawnx/graphics/style.rb +171 -0
- data/lib/prawnx/graphics.rb +208 -0
- data/lib/prawnx/grid.rb +216 -0
- data/lib/prawnx/images/image.rb +244 -0
- data/lib/prawnx/images/png_decoder.rb +65 -0
- data/lib/prawnx/images.rb +115 -0
- data/lib/prawnx/measurement_extensions.rb +40 -0
- data/lib/prawnx/measurements.rb +62 -0
- data/lib/prawnx/outline.rb +146 -0
- data/lib/prawnx/page.rb +125 -0
- data/lib/prawnx/page_geometry.rb +27 -0
- data/lib/prawnx/pdf/outline_node.rb +53 -0
- data/lib/prawnx/pdf/reference.rb +31 -0
- data/lib/prawnx/pdf/writer.rb +105 -0
- data/lib/prawnx/pdf.rb +66 -0
- data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
- data/lib/prawnx/pdf_a.rb +130 -0
- data/lib/prawnx/pdf_core.rb +25 -0
- data/lib/prawnx/repeater.rb +79 -0
- data/lib/prawnx/security.rb +108 -0
- data/lib/prawnx/stamp.rb +74 -0
- data/lib/prawnx/table/cell/kinds.rb +246 -0
- data/lib/prawnx/table/cell.rb +331 -0
- data/lib/prawnx/table/cells.rb +208 -0
- data/lib/prawnx/table.rb +296 -0
- data/lib/prawnx/text/box.rb +16 -0
- data/lib/prawnx/text/formatted/arranger.rb +217 -0
- data/lib/prawnx/text/formatted/box.rb +312 -0
- data/lib/prawnx/text/formatted/fragment.rb +205 -0
- data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
- data/lib/prawnx/text/formatted/parser.rb +149 -0
- data/lib/prawnx/text/formatted/wrap.rb +103 -0
- data/lib/prawnx/text.rb +281 -0
- data/lib/prawnx/transformation_stack.rb +43 -0
- data/lib/prawnx/true_type/cff_subset.rb +157 -0
- data/lib/prawnx/true_type/file.rb +371 -0
- data/lib/prawnx/true_type/open_type_layout.rb +189 -0
- data/lib/prawnx/true_type/simple_subsets.rb +94 -0
- data/lib/prawnx/true_type/subset.rb +187 -0
- data/lib/prawnx/version.rb +7 -0
- data/lib/prawnx/view.rb +28 -0
- data/lib/prawnx.rb +102 -0
- metadata +251 -0
|
@@ -0,0 +1,178 @@
|
|
|
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
|
+
|
|
17
|
+
result.last[part] << token.name
|
|
18
|
+
when Attribute
|
|
19
|
+
return unless ['=', '*=', '~=', '^=', '|=', '$=', nil].include?(token.operator)
|
|
20
|
+
|
|
21
|
+
(result.last[:attribute] ||= []) << [token.key, token.operator, token.value]
|
|
22
|
+
when Combinator
|
|
23
|
+
result << { combinator: token.type }
|
|
24
|
+
part = nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
VALID_CSS_IDENTIFIER_CHAR = /[a-zA-Z0-9_\u00a0-\uffff-]/.freeze
|
|
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 = String.new(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 = String.new(char)
|
|
64
|
+
attribute = :operator
|
|
65
|
+
elsif [' ', "\t"].include?(char)
|
|
66
|
+
attribute = :pre_operator
|
|
67
|
+
else
|
|
68
|
+
return
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
when :pre_operator
|
|
72
|
+
if '=*~^|$'.include?(char)
|
|
73
|
+
result.last.operator = String.new(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 [' ', "\t"].include?(char)
|
|
83
|
+
attribute = :pre_value
|
|
84
|
+
elsif ['"', "'"].include?(char)
|
|
85
|
+
result.last.value = +''
|
|
86
|
+
attribute = String.new(char)
|
|
87
|
+
else
|
|
88
|
+
result.last.value = String.new(char)
|
|
89
|
+
attribute = :value
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
when :pre_value
|
|
93
|
+
if ['"', "'"].include?(char)
|
|
94
|
+
result.last.value = +''
|
|
95
|
+
attribute = String.new(char)
|
|
96
|
+
elsif char != ' ' && char != "\t"
|
|
97
|
+
result.last.value = String.new(char)
|
|
98
|
+
attribute = :value
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
when :value
|
|
102
|
+
if char == ']'
|
|
103
|
+
result.last.value = String.new(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) unless 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
|
+
|
|
157
|
+
result << Modifier.new(:name)
|
|
158
|
+
result << Identifier.new('*')
|
|
159
|
+
when '(' # e.g. :nth-child(3n+4)
|
|
160
|
+
unless result.last.is_a?(Identifier) && result[-2] && result[-2].is_a?(Modifier) && result[-2].type == :pseudo_class
|
|
161
|
+
return
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
result.last.name << '('
|
|
165
|
+
brackets = true
|
|
166
|
+
when '['
|
|
167
|
+
result << Attribute.new
|
|
168
|
+
attribute = :pre_key
|
|
169
|
+
else
|
|
170
|
+
return # unsupported Combinator
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
result unless brackets || attribute
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
module Prawn::SVG::CSS
|
|
2
|
+
class Stylesheets
|
|
3
|
+
attr_reader :css_parser, :root, :media, :font_face_rules
|
|
4
|
+
|
|
5
|
+
def initialize(css_parser, root, media = :all, url_loader: nil, warnings: [])
|
|
6
|
+
@css_parser = css_parser
|
|
7
|
+
@root = root
|
|
8
|
+
@media = media
|
|
9
|
+
@warnings = warnings
|
|
10
|
+
@font_face_rules = []
|
|
11
|
+
|
|
12
|
+
@has_url_loader = !url_loader.nil?
|
|
13
|
+
install_url_loader_on_css_parser(url_loader) if url_loader
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def load
|
|
17
|
+
load_style_elements
|
|
18
|
+
xpath_styles = gather_xpath_styles
|
|
19
|
+
associate_xpath_styles_with_elements(xpath_styles)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def install_url_loader_on_css_parser(url_loader)
|
|
25
|
+
warnings = @warnings
|
|
26
|
+
|
|
27
|
+
css_parser.define_singleton_method(:load_uri!) do |uri, options = {}, _deprecated = nil|
|
|
28
|
+
uri = Addressable::URI.parse(uri) unless uri.respond_to?(:scheme)
|
|
29
|
+
import_url = uri.to_s
|
|
30
|
+
|
|
31
|
+
begin
|
|
32
|
+
return unless circular_reference_check(import_url)
|
|
33
|
+
|
|
34
|
+
src = url_loader.load(import_url)
|
|
35
|
+
add_block!(src, options) if src
|
|
36
|
+
rescue CssParser::CircularReferenceError
|
|
37
|
+
# Silently ignore circular @import references
|
|
38
|
+
rescue Prawn::SVG::UrlLoader::Error => e
|
|
39
|
+
warnings << "Failed to load @import CSS from #{import_url}: #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def load_style_elements
|
|
45
|
+
REXML::XPath.match(root, '//style').each do |source|
|
|
46
|
+
data = source.texts.map(&:value).join
|
|
47
|
+
|
|
48
|
+
if @has_url_loader
|
|
49
|
+
css_parser.add_block!(data, base_uri: '')
|
|
50
|
+
else
|
|
51
|
+
css_parser.add_block!(data)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def gather_xpath_styles
|
|
57
|
+
xpath_styles = []
|
|
58
|
+
order = 0
|
|
59
|
+
|
|
60
|
+
css_parser.each_rule_set(media) do |rule_set, _|
|
|
61
|
+
declarations = []
|
|
62
|
+
rule_set.each_declaration { |*data| declarations << data }
|
|
63
|
+
|
|
64
|
+
rule_set.selectors.each do |selector_text|
|
|
65
|
+
if selector_text.strip == '@font-face'
|
|
66
|
+
@font_face_rules << declarations
|
|
67
|
+
next
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
next unless (selector = Prawn::SVG::CSS::SelectorParser.parse(selector_text))
|
|
71
|
+
|
|
72
|
+
xpath = css_selector_to_xpath(selector)
|
|
73
|
+
specificity = calculate_specificity(selector)
|
|
74
|
+
specificity << order
|
|
75
|
+
order += 1
|
|
76
|
+
|
|
77
|
+
xpath_styles << [xpath, declarations, specificity]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
xpath_styles.sort_by(&:last)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def associate_xpath_styles_with_elements(xpath_styles)
|
|
85
|
+
element_styles = {}
|
|
86
|
+
|
|
87
|
+
xpath_styles.each do |xpath, declarations, _|
|
|
88
|
+
REXML::XPath.match(root, xpath).each do |element|
|
|
89
|
+
(element_styles[element] ||= []).concat declarations
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
element_styles
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def xpath_quote(value)
|
|
97
|
+
%("#{value.gsub('\\', '\\\\').gsub('"', '\\"')}") if value
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
RECOGNIZED_PSEUDO_CLASSES = %w[first-child last-child link visited hover active focus].freeze
|
|
101
|
+
|
|
102
|
+
def css_selector_to_xpath(selector)
|
|
103
|
+
selector.map do |element|
|
|
104
|
+
pseudo_classes = Set.new(element[:pseudo_class])
|
|
105
|
+
require_function_name = false
|
|
106
|
+
never_match = false
|
|
107
|
+
|
|
108
|
+
result = case element[:combinator]
|
|
109
|
+
when :child
|
|
110
|
+
+'/'
|
|
111
|
+
when :adjacent
|
|
112
|
+
pseudo_classes << 'first-child'
|
|
113
|
+
+'/following-sibling::'
|
|
114
|
+
when :siblings
|
|
115
|
+
+'/following-sibling::'
|
|
116
|
+
else
|
|
117
|
+
+'//'
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
positions = []
|
|
121
|
+
lang_predicates = []
|
|
122
|
+
href_predicates = []
|
|
123
|
+
pseudo_classes.each do |pc|
|
|
124
|
+
case pc
|
|
125
|
+
when 'first-child' then positions << '1'
|
|
126
|
+
when 'last-child' then positions << 'last()'
|
|
127
|
+
when /^nth-child\((.+)\)$/ then positions << parse_nth_child_expression($1)
|
|
128
|
+
when /^lang\((.+)\)$/ then lang_predicates << $1
|
|
129
|
+
when 'link' then href_predicates << true
|
|
130
|
+
when 'visited', 'hover', 'active', 'focus' then never_match = true
|
|
131
|
+
else
|
|
132
|
+
never_match = true unless RECOGNIZED_PSEUDO_CLASSES.include?(pc)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
unless positions.empty?
|
|
137
|
+
result << '*' unless require_function_name
|
|
138
|
+
require_function_name = true
|
|
139
|
+
|
|
140
|
+
logic = if positions.length == 1
|
|
141
|
+
positions.first
|
|
142
|
+
else
|
|
143
|
+
positions.map { |position| "position()=#{position}" }.join(' and ')
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
result << "[#{logic}]"
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
if require_function_name
|
|
150
|
+
result << "[name()=#{xpath_quote element[:name]}]" if element[:name]
|
|
151
|
+
else
|
|
152
|
+
result << (element[:name] || '*')
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
result << ((element[:class] || []).map { |name| "[contains(concat(' ',@class,' '), ' #{name} ')]" }.join)
|
|
156
|
+
result << ((element[:id] || []).map { |name| "[@id='#{name}']" }.join)
|
|
157
|
+
|
|
158
|
+
(element[:attribute] || []).each do |key, operator, value|
|
|
159
|
+
case operator
|
|
160
|
+
when nil
|
|
161
|
+
result << "[@#{key}]"
|
|
162
|
+
when '='
|
|
163
|
+
result << "[@#{key}=#{xpath_quote value}]"
|
|
164
|
+
when '^='
|
|
165
|
+
result << "[starts-with(@#{key}, #{xpath_quote value})]"
|
|
166
|
+
when '$='
|
|
167
|
+
result << "[substring(@#{key}, string-length(@#{key}) - #{value.length - 1}) = #{xpath_quote value}]"
|
|
168
|
+
when '*='
|
|
169
|
+
result << "[contains(@#{key}, #{xpath_quote value})]"
|
|
170
|
+
when '~='
|
|
171
|
+
result << "[contains(concat(' ',@#{key},' '), #{xpath_quote " #{value} "})]"
|
|
172
|
+
when '|='
|
|
173
|
+
result << "[contains(concat('-',@#{key},'-'), #{xpath_quote "-#{value}-"})]"
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
lang_predicates.each do |lang|
|
|
178
|
+
result << "[lang(#{xpath_quote lang})]"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
result << '[@href or @xlink:href]' unless href_predicates.empty?
|
|
182
|
+
|
|
183
|
+
result << '[false()]' if never_match
|
|
184
|
+
|
|
185
|
+
result
|
|
186
|
+
end.join
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def parse_nth_child_expression(expr)
|
|
190
|
+
expr = expr.strip
|
|
191
|
+
|
|
192
|
+
return '(position() mod 2 = 1)' if expr == 'odd'
|
|
193
|
+
return '(position() mod 2 = 0)' if expr == 'even'
|
|
194
|
+
return expr if expr.match?(/\A\d+\z/)
|
|
195
|
+
|
|
196
|
+
match = expr.match(/\A(-?\d*)n\s*([+-]\s*\d+)?\z/)
|
|
197
|
+
return 'false()' unless match
|
|
198
|
+
|
|
199
|
+
a = case match[1]
|
|
200
|
+
when '', '+' then 1
|
|
201
|
+
when '-' then -1
|
|
202
|
+
else match[1].to_i
|
|
203
|
+
end
|
|
204
|
+
b = match[2] ? match[2].gsub(/\s/, '').to_i : 0
|
|
205
|
+
|
|
206
|
+
if a.zero?
|
|
207
|
+
b.to_s
|
|
208
|
+
elsif a.positive?
|
|
209
|
+
if b <= 0
|
|
210
|
+
"(position() mod #{a} = #{b % a} and position() >= #{[b, 1].max})"
|
|
211
|
+
else
|
|
212
|
+
"(position() mod #{a} = #{b % a} and position() >= #{b})"
|
|
213
|
+
end
|
|
214
|
+
elsif b <= 0
|
|
215
|
+
# a is negative: matches positions b, b+a, b+2a, ... while > 0
|
|
216
|
+
# i.e. position() <= b and (position() - b) mod |a| = 0
|
|
217
|
+
'false()'
|
|
218
|
+
else
|
|
219
|
+
"((position() - #{b}) mod #{a.abs} = 0 and position() >= 1 and position() <= #{b})"
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def calculate_specificity(selector)
|
|
224
|
+
selector.reduce([0, 0, 0]) do |(a, b, c), element|
|
|
225
|
+
[
|
|
226
|
+
a + (element[:id] || []).length,
|
|
227
|
+
b + (element[:class] || []).length + (element[:attribute] || []).length + (element[:pseudo_class] || []).length,
|
|
228
|
+
c + (element[:name] && element[:name] != '*' ? 1 : 0)
|
|
229
|
+
]
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Prawn::SVG::CSS
|
|
2
|
+
class ValuesParser
|
|
3
|
+
class << self
|
|
4
|
+
def parse(values)
|
|
5
|
+
result = []
|
|
6
|
+
|
|
7
|
+
while values
|
|
8
|
+
value, remainder = parse_next(values)
|
|
9
|
+
break unless value
|
|
10
|
+
|
|
11
|
+
result << value
|
|
12
|
+
values = remainder
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
result
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def parse_next(values)
|
|
21
|
+
values = values.strip
|
|
22
|
+
return if values.empty?
|
|
23
|
+
|
|
24
|
+
if (matches = values.match(/\A([a-z-]+)\(\s*(.+)/i))
|
|
25
|
+
parse_function_call(matches[1].downcase, matches[2])
|
|
26
|
+
else
|
|
27
|
+
values.split(/\s+/, 2)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Note this does not support space-separated arguments.
|
|
32
|
+
# I don't think CSS 2 has any, but in case it does here is the place to add them.
|
|
33
|
+
def parse_function_call(name, rest)
|
|
34
|
+
arguments = []
|
|
35
|
+
in_quote = nil
|
|
36
|
+
in_escape = false
|
|
37
|
+
current = +''
|
|
38
|
+
|
|
39
|
+
rest.each_char.with_index do |char, index|
|
|
40
|
+
if in_escape
|
|
41
|
+
current << char
|
|
42
|
+
in_escape = false
|
|
43
|
+
elsif %w[" '].include?(char)
|
|
44
|
+
if in_quote == char
|
|
45
|
+
in_quote = nil
|
|
46
|
+
elsif in_quote.nil?
|
|
47
|
+
in_quote = char
|
|
48
|
+
else
|
|
49
|
+
current << char
|
|
50
|
+
end
|
|
51
|
+
elsif char == '\\'
|
|
52
|
+
in_escape = true
|
|
53
|
+
elsif in_quote.nil? && char == ','
|
|
54
|
+
arguments << current.strip
|
|
55
|
+
current = +''
|
|
56
|
+
elsif in_quote.nil? && char == ')'
|
|
57
|
+
arguments << current.strip
|
|
58
|
+
return [[name, arguments], rest[index + 1..]]
|
|
59
|
+
else
|
|
60
|
+
current << char
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
[rest, nil]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
require 'tempfile'
|
|
2
|
+
|
|
3
|
+
class Prawn::SVG::Document
|
|
4
|
+
Error = Class.new(StandardError)
|
|
5
|
+
InvalidSVGData = Class.new(Error)
|
|
6
|
+
|
|
7
|
+
DEFAULT_FALLBACK_FONT_NAME = 'Times-Roman'.freeze
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
attr_accessor :enable_web_requests_warned
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# An +Array+ of warnings that occurred while parsing the SVG data.
|
|
14
|
+
attr_reader :warnings
|
|
15
|
+
|
|
16
|
+
attr_reader :root,
|
|
17
|
+
:sizing,
|
|
18
|
+
:fallback_font_name,
|
|
19
|
+
:font_registry,
|
|
20
|
+
:url_loader,
|
|
21
|
+
:elements_by_id, :gradients,
|
|
22
|
+
:element_styles,
|
|
23
|
+
:color_mode,
|
|
24
|
+
:external_svg_cache
|
|
25
|
+
|
|
26
|
+
def initialize(data, bounds, options, font_registry: nil, css_parser: CssParser::Parser.new, attribute_overrides: {})
|
|
27
|
+
begin
|
|
28
|
+
@root = REXML::Document.new(data).root or raise_parse_error(data)
|
|
29
|
+
rescue REXML::ParseException => e
|
|
30
|
+
raise_parse_error(data, e.message)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
@warnings = []
|
|
34
|
+
@options = options
|
|
35
|
+
@elements_by_id = {}
|
|
36
|
+
@gradients = Prawn::SVG::Gradients.new(self)
|
|
37
|
+
@external_svg_cache = {}
|
|
38
|
+
@fallback_font_name = options.fetch(:fallback_font_name, DEFAULT_FALLBACK_FONT_NAME)
|
|
39
|
+
@font_registry = font_registry
|
|
40
|
+
@color_mode = load_color_mode
|
|
41
|
+
@font_face_tempfiles = []
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@url_loader = Prawn::SVG::UrlLoader.new(
|
|
45
|
+
enable_cache: options[:cache_images],
|
|
46
|
+
# PrawnX: no web request unless asked for, since an SVG could otherwise make the server fetch any URL.
|
|
47
|
+
enable_web: options.fetch(:enable_web_requests, false),
|
|
48
|
+
enable_file_with_root: options[:enable_file_requests_with_root]
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
attributes = @root.attributes.dup
|
|
52
|
+
attribute_overrides.each { |key, value| attributes.add(REXML::Attribute.new(key, value)) }
|
|
53
|
+
|
|
54
|
+
@sizing = Prawn::SVG::Calculators::DocumentSizing.new(bounds, attributes)
|
|
55
|
+
calculate_sizing(requested_width: options[:width], requested_height: options[:height])
|
|
56
|
+
|
|
57
|
+
stylesheets = Prawn::SVG::CSS::Stylesheets.new(css_parser, root, url_loader: url_loader, warnings: warnings)
|
|
58
|
+
@element_styles = stylesheets.load
|
|
59
|
+
process_font_face_rules(stylesheets.font_face_rules)
|
|
60
|
+
|
|
61
|
+
yield self if block_given?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def calculate_sizing(requested_width: nil, requested_height: nil)
|
|
65
|
+
sizing.requested_width = requested_width
|
|
66
|
+
sizing.requested_height = requested_height
|
|
67
|
+
sizing.calculate
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def with_sizing(temporary_sizing)
|
|
71
|
+
original = @sizing
|
|
72
|
+
@sizing = temporary_sizing
|
|
73
|
+
yield
|
|
74
|
+
ensure
|
|
75
|
+
@sizing = original
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private
|
|
79
|
+
|
|
80
|
+
def process_font_face_rules(rules)
|
|
81
|
+
return unless font_registry
|
|
82
|
+
|
|
83
|
+
rules.each do |declarations|
|
|
84
|
+
decl_hash = {}
|
|
85
|
+
declarations.each { |name, value, _| decl_hash[name] = value }
|
|
86
|
+
|
|
87
|
+
family = unquote_css_string(decl_hash['font-family'])
|
|
88
|
+
next unless family
|
|
89
|
+
|
|
90
|
+
src = decl_hash['src']
|
|
91
|
+
next unless src
|
|
92
|
+
|
|
93
|
+
weight = decl_hash['font-weight']
|
|
94
|
+
style = decl_hash['font-style']
|
|
95
|
+
stretch = decl_hash['font-stretch']
|
|
96
|
+
|
|
97
|
+
load_font_face_source(family, weight, style, stretch, src)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def load_font_face_source(family, weight, style, stretch, src)
|
|
102
|
+
sources = Prawn::SVG::CSS::FontFaceParser.parse_src(src)
|
|
103
|
+
|
|
104
|
+
sources.detect do |source|
|
|
105
|
+
case source[:type]
|
|
106
|
+
when :local
|
|
107
|
+
load_local_font_face(family, weight, style, stretch, source[:name])
|
|
108
|
+
when :url
|
|
109
|
+
load_url_font_face(family, weight, style, stretch, source)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def load_local_font_face(family, weight, style, stretch, local_name)
|
|
115
|
+
font_data = font_registry.find_local_font_data(local_name, weight, style, stretch)
|
|
116
|
+
return unless font_data
|
|
117
|
+
|
|
118
|
+
font_registry.register_font_face(family, weight, style, stretch, font_data)
|
|
119
|
+
true
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def load_url_font_face(family, weight, style, stretch, source)
|
|
123
|
+
return if source[:format] && !Prawn::SVG::CSS::FontFaceParser::SUPPORTED_FORMATS.include?(source[:format])
|
|
124
|
+
|
|
125
|
+
data = url_loader.load(source[:url])
|
|
126
|
+
tempfile = Tempfile.new(['prawn-svg-font', '.ttf'])
|
|
127
|
+
tempfile.binmode
|
|
128
|
+
tempfile.write(data)
|
|
129
|
+
tempfile.close
|
|
130
|
+
@font_face_tempfiles << tempfile
|
|
131
|
+
|
|
132
|
+
font_registry.register_font_face(family, weight, style, stretch, tempfile.path)
|
|
133
|
+
true
|
|
134
|
+
rescue Prawn::SVG::UrlLoader::Error => e
|
|
135
|
+
warnings << "Failed to load @font-face font from #{source[:url]}: #{e.message}"
|
|
136
|
+
false
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def unquote_css_string(value)
|
|
140
|
+
return unless value
|
|
141
|
+
|
|
142
|
+
value = value.strip
|
|
143
|
+
if (value.start_with?('"') && value.end_with?('"')) || (value.start_with?("'") && value.end_with?("'"))
|
|
144
|
+
value[1..-2]
|
|
145
|
+
else
|
|
146
|
+
value
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def load_color_mode
|
|
151
|
+
case @options[:color_mode]
|
|
152
|
+
when nil, :rgb then :rgb
|
|
153
|
+
when :cmyk then :cmyk
|
|
154
|
+
else
|
|
155
|
+
raise ArgumentError, ':color_mode must be set to :rgb (default) or :cmyk'
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def raise_parse_error(data, exception_message = nil)
|
|
160
|
+
message = 'The data supplied is not a valid SVG document.'
|
|
161
|
+
|
|
162
|
+
if data.respond_to?(:end_with?) && data.end_with?('.svg')
|
|
163
|
+
message += " It looks like you've supplied a filename instead; use File.read(filename) to get the data from the file before you pass it to prawn-svg."
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
message += "\n#{exception_message}" if exception_message
|
|
167
|
+
|
|
168
|
+
raise InvalidSVGData, message
|
|
169
|
+
end
|
|
170
|
+
end
|