asciidoctor-pdf 1.5.0.alpha.18 → 1.5.0.beta.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +45 -0
- data/NOTICE.adoc +14 -11
- data/README.adoc +76 -25
- data/asciidoctor-pdf.gemspec +2 -0
- data/data/themes/base-theme.yml +4 -0
- data/data/themes/default-theme.yml +17 -34
- data/data/themes/default-with-fallback-font-theme.yml +22 -0
- data/docs/theming-guide.adoc +957 -892
- data/lib/asciidoctor-pdf/asciidoctor_ext/image.rb +4 -4
- data/lib/asciidoctor-pdf/converter.rb +483 -357
- data/lib/asciidoctor-pdf/core_ext.rb +1 -0
- data/lib/asciidoctor-pdf/core_ext/hash.rb +5 -0
- data/lib/asciidoctor-pdf/formatted_text.rb +1 -0
- data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +3 -1
- data/lib/asciidoctor-pdf/formatted_text/parser.rb +24 -12
- data/lib/asciidoctor-pdf/formatted_text/parser.treetop +1 -1
- data/lib/asciidoctor-pdf/formatted_text/text_background_and_border_renderer.rb +45 -0
- data/lib/asciidoctor-pdf/formatted_text/transform.rb +44 -21
- data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +9 -11
- data/lib/asciidoctor-pdf/prawn_ext/images.rb +18 -7
- data/lib/asciidoctor-pdf/theme_loader.rb +94 -76
- data/lib/asciidoctor-pdf/version.rb +1 -1
- metadata +26 -3
@@ -7,3 +7,4 @@ require_relative 'formatted_text/inline_destination_marker'
|
|
7
7
|
require_relative 'formatted_text/inline_image_arranger'
|
8
8
|
require_relative 'formatted_text/inline_image_renderer'
|
9
9
|
require_relative 'formatted_text/inline_text_aligner'
|
10
|
+
require_relative 'formatted_text/text_background_and_border_renderer'
|
@@ -66,7 +66,9 @@ module InlineImageArranger
|
|
66
66
|
svg_obj = ::Prawn::SVG::Interface.new ::File.read(image_path), doc,
|
67
67
|
at: doc.bounds.top_left,
|
68
68
|
width: image_w,
|
69
|
-
fallback_font_name: doc.default_svg_font
|
69
|
+
fallback_font_name: doc.default_svg_font,
|
70
|
+
enable_web_requests: doc.allow_uri_read,
|
71
|
+
enable_file_requests_with_root: (::File.dirname image_path)
|
70
72
|
svg_size = image_w ? svg_obj.document.sizing :
|
71
73
|
# NOTE convert intrinsic dimensions to points; constrain to content width
|
72
74
|
(svg_obj.resize width: [(to_pt svg_obj.document.sizing.output_width, :px), available_w].min)
|
@@ -356,44 +356,44 @@ module Markup
|
|
356
356
|
r1 = SyntaxNode.new(input, (index-1)...index) if r1 == true
|
357
357
|
r0 = r1
|
358
358
|
else
|
359
|
-
if (match_len = has_terminal?('
|
359
|
+
if (match_len = has_terminal?('strong', false, index))
|
360
360
|
r2 = instantiate_node(SyntaxNode,input, index...(index + match_len))
|
361
361
|
@index += match_len
|
362
362
|
else
|
363
|
-
terminal_parse_failure('
|
363
|
+
terminal_parse_failure('strong')
|
364
364
|
r2 = nil
|
365
365
|
end
|
366
366
|
if r2
|
367
367
|
r2 = SyntaxNode.new(input, (index-1)...index) if r2 == true
|
368
368
|
r0 = r2
|
369
369
|
else
|
370
|
-
if (match_len = has_terminal?('
|
370
|
+
if (match_len = has_terminal?('em', false, index))
|
371
371
|
r3 = instantiate_node(SyntaxNode,input, index...(index + match_len))
|
372
372
|
@index += match_len
|
373
373
|
else
|
374
|
-
terminal_parse_failure('
|
374
|
+
terminal_parse_failure('em')
|
375
375
|
r3 = nil
|
376
376
|
end
|
377
377
|
if r3
|
378
378
|
r3 = SyntaxNode.new(input, (index-1)...index) if r3 == true
|
379
379
|
r0 = r3
|
380
380
|
else
|
381
|
-
if (match_len = has_terminal?('
|
381
|
+
if (match_len = has_terminal?('code', false, index))
|
382
382
|
r4 = instantiate_node(SyntaxNode,input, index...(index + match_len))
|
383
383
|
@index += match_len
|
384
384
|
else
|
385
|
-
terminal_parse_failure('
|
385
|
+
terminal_parse_failure('code')
|
386
386
|
r4 = nil
|
387
387
|
end
|
388
388
|
if r4
|
389
389
|
r4 = SyntaxNode.new(input, (index-1)...index) if r4 == true
|
390
390
|
r0 = r4
|
391
391
|
else
|
392
|
-
if (match_len = has_terminal?('
|
392
|
+
if (match_len = has_terminal?('color', false, index))
|
393
393
|
r5 = instantiate_node(SyntaxNode,input, index...(index + match_len))
|
394
394
|
@index += match_len
|
395
395
|
else
|
396
|
-
terminal_parse_failure('
|
396
|
+
terminal_parse_failure('color')
|
397
397
|
r5 = nil
|
398
398
|
end
|
399
399
|
if r5
|
@@ -422,11 +422,11 @@ module Markup
|
|
422
422
|
r7 = SyntaxNode.new(input, (index-1)...index) if r7 == true
|
423
423
|
r0 = r7
|
424
424
|
else
|
425
|
-
if (match_len = has_terminal?('
|
425
|
+
if (match_len = has_terminal?('button', false, index))
|
426
426
|
r8 = instantiate_node(SyntaxNode,input, index...(index + match_len))
|
427
427
|
@index += match_len
|
428
428
|
else
|
429
|
-
terminal_parse_failure('
|
429
|
+
terminal_parse_failure('button')
|
430
430
|
r8 = nil
|
431
431
|
end
|
432
432
|
if r8
|
@@ -455,8 +455,20 @@ module Markup
|
|
455
455
|
r10 = SyntaxNode.new(input, (index-1)...index) if r10 == true
|
456
456
|
r0 = r10
|
457
457
|
else
|
458
|
-
|
459
|
-
|
458
|
+
if (match_len = has_terminal?('del', false, index))
|
459
|
+
r11 = instantiate_node(SyntaxNode,input, index...(index + match_len))
|
460
|
+
@index += match_len
|
461
|
+
else
|
462
|
+
terminal_parse_failure('del')
|
463
|
+
r11 = nil
|
464
|
+
end
|
465
|
+
if r11
|
466
|
+
r11 = SyntaxNode.new(input, (index-1)...index) if r11 == true
|
467
|
+
r0 = r11
|
468
|
+
else
|
469
|
+
@index = i0
|
470
|
+
r0 = nil
|
471
|
+
end
|
460
472
|
end
|
461
473
|
end
|
462
474
|
end
|
@@ -51,7 +51,7 @@ grammar Markup
|
|
51
51
|
# QUESTION faster to do regex?
|
52
52
|
# QUESTION can we cut stuff we aren't using? what about supporting hr?
|
53
53
|
#'a' / 'b' / 'code' / 'color' / 'del' / 'em' / 'font' / 'i' / 'img' / 'link' / 'span' / 'strikethrough' / 'strong' / 'sub' / 'sup' / 'u'
|
54
|
-
'a' / '
|
54
|
+
'a' / 'strong' / 'em' / 'code' / 'color' / 'font' / 'span' / 'button' / 'sub' / 'sup' / 'del'
|
55
55
|
end
|
56
56
|
|
57
57
|
rule void_tag_name
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Asciidoctor::Pdf::FormattedText
|
2
|
+
module TextBackgroundAndBorderRenderer
|
3
|
+
module_function
|
4
|
+
|
5
|
+
# render_behind is called before the text is printed
|
6
|
+
def render_behind fragment
|
7
|
+
return if (pdf = fragment.document).scratch?
|
8
|
+
data = fragment.format_state
|
9
|
+
if (border_offset = data[:border_offset])
|
10
|
+
at = [fragment.left - border_offset, fragment.top + border_offset]
|
11
|
+
width = fragment.width + border_offset * 2
|
12
|
+
height = fragment.height + border_offset * 2
|
13
|
+
else
|
14
|
+
at = fragment.top_left
|
15
|
+
width = fragment.width
|
16
|
+
height = fragment.height
|
17
|
+
end
|
18
|
+
border_radius = data[:border_radius]
|
19
|
+
if (background_color = data[:background_color])
|
20
|
+
prev_fill_color = pdf.fill_color
|
21
|
+
pdf.fill_color background_color
|
22
|
+
if border_radius
|
23
|
+
pdf.fill_rounded_rectangle at, width, height, border_radius
|
24
|
+
else
|
25
|
+
pdf.fill_rectangle at, width, height
|
26
|
+
end
|
27
|
+
pdf.fill_color prev_fill_color
|
28
|
+
end
|
29
|
+
if (border_width = data[:border_width])
|
30
|
+
border_color = data[:border_color]
|
31
|
+
prev_stroke_color = pdf.stroke_color
|
32
|
+
prev_line_width = pdf.line_width
|
33
|
+
pdf.stroke_color border_color
|
34
|
+
pdf.line_width border_width
|
35
|
+
if border_radius
|
36
|
+
pdf.stroke_rounded_rectangle at, width, height, border_radius
|
37
|
+
else
|
38
|
+
pdf.stroke_rectangle at, width, height
|
39
|
+
end
|
40
|
+
pdf.stroke_color prev_stroke_color
|
41
|
+
pdf.line_width prev_line_width
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -20,21 +20,44 @@ class Transform
|
|
20
20
|
@merge_adjacent_text_nodes = options[:merge_adjacent_text_nodes]
|
21
21
|
# TODO add support for character spacing
|
22
22
|
if (theme = options[:theme])
|
23
|
-
@
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
23
|
+
@theme_settings = {
|
24
|
+
button: {
|
25
|
+
color: theme.button_font_color,
|
26
|
+
font: theme.button_font_family,
|
27
|
+
size: theme.button_font_size,
|
28
|
+
styles: to_styles(theme.button_font_style),
|
29
|
+
background_color: (button_bg_color = theme.button_background_color),
|
30
|
+
border_width: (button_border_width = theme.button_border_width),
|
31
|
+
border_color: button_border_width && (theme.button_border_color || theme.base_border_color),
|
32
|
+
border_offset: (button_bg_or_border = button_bg_color || button_border_width) && theme.button_border_offset,
|
33
|
+
border_radius: button_bg_or_border && theme.button_border_radius,
|
34
|
+
callback: button_bg_or_border && [TextBackgroundAndBorderRenderer],
|
35
|
+
}.compact,
|
36
|
+
code: {
|
37
|
+
color: theme.literal_font_color,
|
38
|
+
font: theme.literal_font_family,
|
39
|
+
size: theme.literal_font_size,
|
40
|
+
styles: to_styles(theme.literal_font_style),
|
41
|
+
background_color: (monospaced_bg_color = theme.literal_background_color),
|
42
|
+
border_width: (monospaced_border_width = theme.literal_border_width),
|
43
|
+
border_color: monospaced_border_width && (theme.literal_border_color || theme.base_border_color),
|
44
|
+
border_offset: (monospaced_bg_or_border = monospaced_bg_color || monospaced_border_width) && theme.literal_border_offset,
|
45
|
+
border_radius: monospaced_bg_or_border && theme.literal_border_radius,
|
46
|
+
callback: monospaced_bg_or_border && [TextBackgroundAndBorderRenderer],
|
47
|
+
}.compact,
|
48
|
+
link: {
|
49
|
+
color: theme.link_font_color,
|
50
|
+
font: theme.link_font_family,
|
51
|
+
size: theme.link_font_size,
|
52
|
+
styles: to_styles(theme.link_font_style, theme.link_text_decoration)
|
53
|
+
}.compact,
|
54
|
+
}
|
35
55
|
else
|
36
|
-
@
|
37
|
-
|
56
|
+
@theme_settings = {
|
57
|
+
button: { font: 'Courier', styles: [:bold].to_set },
|
58
|
+
code: { font: 'Courier', size: 0.9 },
|
59
|
+
link: { color: '0000FF' },
|
60
|
+
}
|
38
61
|
end
|
39
62
|
end
|
40
63
|
|
@@ -79,7 +102,7 @@ class Transform
|
|
79
102
|
image_format: attributes[:format],
|
80
103
|
# a zero-width space in the text will cause the image to be duplicated
|
81
104
|
text: (attributes[:alt].delete ZeroWidthSpace),
|
82
|
-
callback: InlineImageRenderer
|
105
|
+
callback: [InlineImageRenderer],
|
83
106
|
}
|
84
107
|
if (img_w = attributes[:width])
|
85
108
|
fragment[:image_width] = img_w
|
@@ -123,9 +146,9 @@ class Transform
|
|
123
146
|
styles << :bold
|
124
147
|
when :em
|
125
148
|
styles << :italic
|
126
|
-
when :code
|
127
|
-
# NOTE prefer old value, except for styles, which should be combined
|
128
|
-
fragment.update(@
|
149
|
+
when :code, :button
|
150
|
+
# NOTE prefer old value, except for styles and callback, which should be combined
|
151
|
+
fragment.update(@theme_settings[tag_name]) {|k, oval, nval| k == :styles ? oval.merge(nval) : (k == :callback ? oval.union(nval) : oval) }
|
129
152
|
when :color
|
130
153
|
if !fragment[:color]
|
131
154
|
if (rgb = attrs[:rgb])
|
@@ -168,7 +191,7 @@ class Transform
|
|
168
191
|
fragment[:width] = value
|
169
192
|
if (value = attrs[:align])
|
170
193
|
fragment[:align] = value.to_sym
|
171
|
-
fragment[:callback]
|
194
|
+
(fragment[:callback] ||= []) << InlineTextAligner
|
172
195
|
end
|
173
196
|
end
|
174
197
|
#if !fragment[:character_spacing] && (value = attrs[:character_spacing])
|
@@ -191,12 +214,12 @@ class Transform
|
|
191
214
|
if (type = attrs[:type])
|
192
215
|
fragment[:type] = type.to_sym
|
193
216
|
end
|
194
|
-
fragment[:callback]
|
217
|
+
(fragment[:callback] ||= []) << InlineDestinationMarker
|
195
218
|
visible = false
|
196
219
|
end
|
197
220
|
end
|
198
221
|
# NOTE prefer old value, except for styles, which should be combined
|
199
|
-
fragment.update(@
|
222
|
+
fragment.update(@theme_settings[:link]) {|k, oval, nval| k == :styles ? oval.merge(nval) : oval } if visible
|
200
223
|
when :sub
|
201
224
|
styles << :subscript
|
202
225
|
when :sup
|
@@ -39,7 +39,7 @@ module Extensions
|
|
39
39
|
|
40
40
|
# Returns the effective (writable) width of the page
|
41
41
|
#
|
42
|
-
# If inside a
|
42
|
+
# If inside a bounding box, returns width of box.
|
43
43
|
#
|
44
44
|
def effective_page_width
|
45
45
|
reference_bounds.width
|
@@ -748,20 +748,18 @@ module Extensions
|
|
748
748
|
nil
|
749
749
|
end
|
750
750
|
|
751
|
-
# Create a new page for the specified image. If the
|
752
|
-
#
|
753
|
-
# edges of the page (full coverage).
|
751
|
+
# Create a new page for the specified image. If the canvas option is true,
|
752
|
+
# the image is positioned relative to the boundaries of the page.
|
754
753
|
def image_page file, options = {}
|
755
754
|
start_new_page_discretely
|
756
|
-
|
757
|
-
|
758
|
-
|
759
|
-
end
|
755
|
+
image_page_number = page_number
|
756
|
+
if options.delete :canvas
|
757
|
+
canvas { image file, ({ position: :center, vposition: :center }.merge options) }
|
760
758
|
else
|
761
|
-
image file, fit: [bounds.width, bounds.height]
|
759
|
+
image file, (options.merge position: :center, vposition: :center, fit: [bounds.width, bounds.height])
|
762
760
|
end
|
763
|
-
#
|
764
|
-
go_to_page
|
761
|
+
# NOTE advance to new page just in case the image function threw off the cursor
|
762
|
+
go_to_page image_page_number
|
765
763
|
nil
|
766
764
|
end
|
767
765
|
|
@@ -9,10 +9,20 @@ module Images
|
|
9
9
|
|
10
10
|
# Dispatch to suitable image method in Prawn based on file extension.
|
11
11
|
def image file, opts = {}
|
12
|
-
# FIXME handle case when SVG is
|
13
|
-
if ::String === file && (file.downcase.end_with? '.svg')
|
14
|
-
opts[:
|
15
|
-
|
12
|
+
# FIXME handle case when SVG is an IO object
|
13
|
+
if ::String === file && (((opts = opts.dup).delete :format) == 'svg' || (file.downcase.end_with? '.svg'))
|
14
|
+
#opts[:enable_file_requests_with_root] = (::File.dirname file) unless opts.key? :enable_file_requests_with_root
|
15
|
+
#opts[:enable_web_requests] = allow_uri_read if !(opts.key? :enable_web_requests) && (respond_to? :allow_uri_read)
|
16
|
+
#opts[:fallback_font_name] = default_svg_font if !(opts.key? :fallback_font_name) && (respond_to? :default_svg_font)
|
17
|
+
if (opts.key? :fit) && (fit = opts.delete :fit) && !opts[:width] && !opts[:height]
|
18
|
+
svg (::File.read file), opts do |svg_doc|
|
19
|
+
max_width, max_height = fit
|
20
|
+
svg_doc.calculate_sizing requested_width: max_width if max_width && svg_doc.sizing.output_width != max_width
|
21
|
+
svg_doc.calculate_sizing requested_height: max_height if max_height && svg_doc.sizing.output_height > max_height
|
22
|
+
end
|
23
|
+
else
|
24
|
+
svg (::File.read file), opts
|
25
|
+
end
|
16
26
|
else
|
17
27
|
_initial_image file, opts
|
18
28
|
end
|
@@ -22,9 +32,9 @@ module Images
|
|
22
32
|
#
|
23
33
|
# Returns a Hash containing :width and :height keys that map to the image's
|
24
34
|
# intrinsic width and height values (in pixels)
|
25
|
-
def intrinsic_image_dimensions path
|
26
|
-
if
|
27
|
-
img_obj = ::Prawn::SVG::Interface.new ::File.read
|
35
|
+
def intrinsic_image_dimensions path, format
|
36
|
+
if format == 'svg'
|
37
|
+
img_obj = ::Prawn::SVG::Interface.new (::File.read path), self, {}
|
28
38
|
img_size = img_obj.document.sizing
|
29
39
|
{ width: img_size.output_width, height: img_size.output_height }
|
30
40
|
else
|
@@ -32,6 +42,7 @@ module Images
|
|
32
42
|
_, img_size = ::File.open(path, 'rb') {|fd| build_image_object fd }
|
33
43
|
{ width: img_size.width, height: img_size.height }
|
34
44
|
end
|
45
|
+
rescue
|
35
46
|
end
|
36
47
|
end
|
37
48
|
|
@@ -19,9 +19,9 @@ class ThemeLoader
|
|
19
19
|
DefaultThemePath = ::File.expand_path 'default-theme.yml', ThemesDir
|
20
20
|
BaseThemePath = ::File.expand_path 'base-theme.yml', ThemesDir
|
21
21
|
|
22
|
-
VariableRx = /\$([a-z0-9_]+)/
|
23
|
-
LoneVariableRx = /^\$([a-z0-9_]+)$/
|
24
|
-
HexColorEntryRx = /^(?<k>
|
22
|
+
VariableRx = /\$([a-z0-9_-]+)/
|
23
|
+
LoneVariableRx = /^\$([a-z0-9_-]+)$/
|
24
|
+
HexColorEntryRx = /^(?<k> *\p{Graph}+): +(?!null$)(?<q>["']?)(?<h>#)?(?<v>[a-f0-9]{3,6})\k<q> *(?:#.*)?$/
|
25
25
|
MultiplyDivideOpRx = /(-?\d+(?:\.\d+)?) +([*\/]) +(-?\d+(?:\.\d+)?)/
|
26
26
|
AddSubtractOpRx = /(-?\d+(?:\.\d+)?) +([+\-]) +(-?\d+(?:\.\d+)?)/
|
27
27
|
PrecisionFuncRx = /^(round|floor|ceil)\(/
|
@@ -35,7 +35,7 @@ class ThemeLoader
|
|
35
35
|
|
36
36
|
# A marker module for a normalized CMYK array
|
37
37
|
# Prevents normalizing CMYK value more than once
|
38
|
-
module
|
38
|
+
module CMYKColorValue
|
39
39
|
include ColorValue
|
40
40
|
def to_s
|
41
41
|
%([#{join ', '}])
|
@@ -43,89 +43,89 @@ class ThemeLoader
|
|
43
43
|
end
|
44
44
|
|
45
45
|
def self.resolve_theme_file theme_name = nil, theme_path = nil
|
46
|
-
|
47
|
-
|
48
|
-
if (theme_name.end_with? '.yml')
|
46
|
+
# if .yml extension is given, assume it's a path (don't append -theme.yml)
|
47
|
+
if ((theme_name ||= 'default').end_with? '.yml')
|
49
48
|
# FIXME restrict to jail!
|
50
|
-
|
51
|
-
theme_path
|
49
|
+
theme_file = ::File.expand_path theme_name, theme_path
|
50
|
+
theme_path ||= ::File.dirname theme_file
|
52
51
|
else
|
53
|
-
|
54
|
-
::File.expand_path %(#{theme_name}-theme.yml), (theme_path || ThemesDir)
|
52
|
+
theme_file = ::File.expand_path %(#{theme_name}-theme.yml), (theme_path || (theme_path = ThemesDir))
|
55
53
|
end
|
54
|
+
[theme_file, theme_path]
|
56
55
|
end
|
57
56
|
|
58
|
-
def self.resolve_theme_asset asset_path, theme_path
|
57
|
+
def self.resolve_theme_asset asset_path, theme_path
|
59
58
|
::File.expand_path asset_path, (theme_path || ThemesDir)
|
60
59
|
end
|
61
60
|
|
62
61
|
# NOTE base theme is loaded "as is" (no post-processing)
|
63
62
|
def self.load_base_theme
|
64
|
-
::OpenStruct.new
|
63
|
+
(::OpenStruct.new ::SafeYAML.load_file BaseThemePath).tap {|theme| theme.__dir__ = ThemesDir }
|
65
64
|
end
|
66
65
|
|
67
|
-
def self.load_theme theme_name = nil, theme_path = nil
|
68
|
-
|
69
|
-
|
70
|
-
|
66
|
+
def self.load_theme theme_name = nil, theme_path = nil
|
67
|
+
theme_file, theme_path = resolve_theme_file theme_name, theme_path
|
68
|
+
if theme_file == BaseThemePath
|
69
|
+
load_base_theme
|
70
|
+
else
|
71
|
+
theme_data = load_file theme_file, nil, theme_path
|
72
|
+
unless theme_file == DefaultThemePath
|
73
|
+
# QUESTION should we enforce any other fallback values?
|
74
|
+
theme_data.base_align ||= 'left'
|
75
|
+
theme_data.code_font_family ||= (theme_data.literal_font_family || 'Courier')
|
76
|
+
theme_data.conum_font_family ||= (theme_data.literal_font_family || 'Courier')
|
77
|
+
end
|
78
|
+
theme_data.__dir__ = theme_path
|
79
|
+
theme_data
|
71
80
|
end
|
72
|
-
|
73
|
-
theme_file == BaseThemePath ? theme_data : (load_file theme_file, theme_data, theme_path)
|
74
81
|
end
|
75
82
|
|
76
83
|
def self.load_file filename, theme_data = nil, theme_path = nil
|
77
|
-
|
78
|
-
|
79
|
-
[
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
84
|
+
data = ::File.read filename, encoding: ::Encoding::UTF_8
|
85
|
+
data = data.each_line.map {|l|
|
86
|
+
l.sub(HexColorEntryRx) { %(#{(m = $~)[:k]}: #{m[:h] || (m[:k].end_with? 'color') ? "'#{m[:v]}'" : m[:v]}) }
|
87
|
+
}.join unless filename == DefaultThemePath
|
88
|
+
yaml_data = ::SafeYAML.load data
|
89
|
+
if ::Hash === yaml_data && (yaml_data.key? 'extends')
|
90
|
+
if (extends = yaml_data.delete 'extends')
|
91
|
+
[*extends].each do |extend_file|
|
92
|
+
if extend_file == 'base'
|
93
|
+
theme_data = theme_data ? (::OpenStruct.new theme_data.to_h.merge load_base_theme.to_h) : load_base_theme
|
94
|
+
next
|
95
|
+
elsif extend_file == 'default' || extend_file == 'default-with-fallback-font'
|
96
|
+
extend_file, extend_theme_path = resolve_theme_file extend_file
|
97
|
+
elsif extend_file.start_with? './'
|
98
|
+
extend_file, extend_theme_path = resolve_theme_file extend_file, (::File.dirname filename)
|
99
|
+
else
|
100
|
+
extend_file, extend_theme_path = resolve_theme_file extend_file, theme_path
|
101
|
+
end
|
102
|
+
theme_data = load_file extend_file, theme_data, extend_theme_path
|
86
103
|
end
|
87
|
-
theme_data = load_file extend_file, theme_data, extend_theme_path
|
88
104
|
end
|
105
|
+
else
|
106
|
+
theme_data ||= (filename == DefaultThemePath ? nil : load_base_theme)
|
89
107
|
end
|
90
108
|
self.new.load yaml_data, theme_data, theme_path
|
91
109
|
end
|
92
110
|
|
93
111
|
def load hash, theme_data = nil, theme_path = nil
|
94
|
-
theme_data
|
95
|
-
return theme_data unless ::Hash === hash
|
96
|
-
base_code_font_family = theme_data.delete 'code_font_family'
|
97
|
-
base_conum_font_family = theme_data.delete 'conum_font_family'
|
98
|
-
hash.inject(theme_data) {|data, (key, val)| process_entry key, val, data }
|
99
|
-
# NOTE remap legacy running content keys (e.g., header_recto_content_left => header_recto_left_content)
|
100
|
-
%w(header_recto header_verso footer_recto footer_verso).each do |periphery_face|
|
101
|
-
%w(left center right).each do |align|
|
102
|
-
if (val = theme_data.delete %(#{periphery_face}_content_#{align}))
|
103
|
-
theme_data[%(#{periphery_face}_#{align}_content)] = val
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
theme_data.base_align ||= 'left'
|
108
|
-
theme_data.code_font_family ||= (theme_data.literal_font_family || base_code_font_family)
|
109
|
-
theme_data.conum_font_family ||= (theme_data.literal_font_family || base_conum_font_family)
|
110
|
-
# QUESTION should we do any other post-load calculations or defaults?
|
111
|
-
theme_data
|
112
|
+
::Hash === hash ? hash.reduce(theme_data || ::OpenStruct.new) {|data, (key, val)| process_entry key, val, data } : (theme_data || ::OpenStruct.new)
|
112
113
|
end
|
113
114
|
|
114
115
|
private
|
115
116
|
|
116
117
|
def process_entry key, val, data
|
117
|
-
if key.
|
118
|
+
key = key.tr '-', '_' if key.include? '-'
|
119
|
+
if key == 'font_catalog' || key == 'font_fallbacks'
|
118
120
|
data[key] = val
|
119
121
|
elsif key.start_with? 'admonition_icon_'
|
120
122
|
data[key] = (val || {}).map do |(key2, val2)|
|
121
123
|
[key2.to_sym, (key2.end_with? '_color') ? to_color(evaluate val2, data) : (evaluate val2, data)]
|
122
124
|
end.to_h
|
123
125
|
elsif ::Hash === val
|
124
|
-
val.each
|
125
|
-
process_entry %(#{key}_#{key2.tr '-', '_'}), val2, data
|
126
|
-
end
|
126
|
+
val.each {|subkey, subval| process_entry %(#{key}_#{subkey}), subval, data }
|
127
127
|
elsif key.end_with? '_color'
|
128
|
-
# QUESTION do we need to evaluate_math in this case?
|
128
|
+
# QUESTION do we really need to evaluate_math in this case?
|
129
129
|
data[key] = to_color(evaluate val, data)
|
130
130
|
elsif %(#{key.chomp '_'}_).include? '_content_'
|
131
131
|
data[key] = (expand_vars val.to_s, data).to_s
|
@@ -150,21 +150,27 @@ class ThemeLoader
|
|
150
150
|
def expand_vars expr, vars
|
151
151
|
if (idx = (expr.index '$'))
|
152
152
|
if idx == 0 && expr =~ LoneVariableRx
|
153
|
-
if
|
154
|
-
|
153
|
+
if (key = $1).include? '-'
|
154
|
+
key = key.tr '-', '_'
|
155
|
+
end
|
156
|
+
if vars.respond_to? key
|
157
|
+
vars[key]
|
155
158
|
else
|
156
159
|
logger.warn %(unknown variable reference in PDF theme: $#{$1})
|
157
160
|
expr
|
158
161
|
end
|
159
162
|
else
|
160
|
-
expr.gsub(VariableRx)
|
161
|
-
if
|
162
|
-
|
163
|
+
expr.gsub(VariableRx) do
|
164
|
+
if (key = $1).include? '-'
|
165
|
+
key = key.tr '-', '_'
|
166
|
+
end
|
167
|
+
if vars.respond_to? key
|
168
|
+
vars[key]
|
163
169
|
else
|
164
170
|
logger.warn %(unknown variable reference in PDF theme: $#{$1})
|
165
171
|
$&
|
166
172
|
end
|
167
|
-
|
173
|
+
end
|
168
174
|
end
|
169
175
|
else
|
170
176
|
expr
|
@@ -178,16 +184,24 @@ class ThemeLoader
|
|
178
184
|
# NOTE leave % as a string; handled by converter for now
|
179
185
|
expr = resolve_measurement_values(original = expr)
|
180
186
|
while true
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
187
|
+
if (expr.count '*/') > 0
|
188
|
+
result = expr.gsub(MultiplyDivideOpRx) { $1.to_f.send $2.to_sym, $3.to_f }
|
189
|
+
unchanged = (result == expr)
|
190
|
+
expr = result
|
191
|
+
break if unchanged
|
192
|
+
else
|
193
|
+
break
|
194
|
+
end
|
185
195
|
end
|
186
196
|
while true
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
197
|
+
if (expr.count '+-') > 0
|
198
|
+
result = expr.gsub(AddSubtractOpRx) { $1.to_f.send $2.to_sym, $3.to_f }
|
199
|
+
unchanged = (result == expr)
|
200
|
+
expr = result
|
201
|
+
break if unchanged
|
202
|
+
else
|
203
|
+
break
|
204
|
+
end
|
191
205
|
end
|
192
206
|
if (expr.end_with? ')') && expr =~ PrecisionFuncRx
|
193
207
|
op = $1
|
@@ -206,13 +220,6 @@ class ThemeLoader
|
|
206
220
|
when ColorValue
|
207
221
|
# already converted
|
208
222
|
return value
|
209
|
-
when ::String
|
210
|
-
if value == 'transparent'
|
211
|
-
# FIXME should we have a TransparentColorValue class?
|
212
|
-
return HexColorValue.new value
|
213
|
-
elsif value.length == 6
|
214
|
-
return HexColorValue.new value.upcase
|
215
|
-
end
|
216
223
|
when ::Array
|
217
224
|
case value.length
|
218
225
|
# CMYK value
|
@@ -231,7 +238,7 @@ class ThemeLoader
|
|
231
238
|
when [100, 100, 100, 100]
|
232
239
|
return HexColorValue.new '000000'
|
233
240
|
else
|
234
|
-
value.extend
|
241
|
+
value.extend CMYKColorValue
|
235
242
|
return value
|
236
243
|
end
|
237
244
|
# RGB value
|
@@ -241,9 +248,20 @@ class ThemeLoader
|
|
241
248
|
else
|
242
249
|
value = value.join
|
243
250
|
end
|
251
|
+
when ::String
|
252
|
+
if value == 'transparent'
|
253
|
+
# FIXME should we have a TransparentColorValue class?
|
254
|
+
return HexColorValue.new value
|
255
|
+
elsif value.length == 6
|
256
|
+
return HexColorValue.new value.upcase
|
257
|
+
end
|
258
|
+
when ::NilClass
|
259
|
+
return nil
|
244
260
|
else
|
245
|
-
# Unknown type; coerce to
|
246
|
-
value = value.to_s
|
261
|
+
# Unknown type (usually Integer); coerce to String
|
262
|
+
if (value = value.to_s).length == 6
|
263
|
+
return HexColorValue.new value.upcase
|
264
|
+
end
|
247
265
|
end
|
248
266
|
value = case value.length
|
249
267
|
when 6
|