asciidoctor-pdf 1.5.0.beta.8 → 1.5.0.rc.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 +49 -0
- data/LICENSE.adoc +1 -1
- data/NOTICE.adoc +1 -1
- data/README.adoc +43 -47
- data/asciidoctor-pdf.gemspec +5 -1
- data/bin/asciidoctor-pdf-optimize +1 -1
- data/data/themes/base-theme.yml +4 -3
- data/data/themes/default-theme.yml +10 -5
- data/docs/theming-guide.adoc +286 -22
- data/lib/asciidoctor-pdf.rb +1 -0
- data/lib/asciidoctor-pdf/converter.rb +1 -0
- data/lib/asciidoctor-pdf/version.rb +1 -0
- data/lib/asciidoctor/pdf.rb +13 -2
- data/lib/asciidoctor/pdf/converter.rb +3962 -3955
- data/lib/asciidoctor/pdf/ext.rb +9 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor.rb +1 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_block.rb +1 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_node.rb +1 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor/document.rb +1 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor/image.rb +18 -16
- data/lib/asciidoctor/pdf/ext/asciidoctor/list.rb +3 -2
- data/lib/asciidoctor/pdf/ext/asciidoctor/list_item.rb +2 -1
- data/lib/asciidoctor/pdf/ext/asciidoctor/logging_shim.rb +3 -4
- data/lib/asciidoctor/pdf/ext/asciidoctor/section.rb +8 -6
- data/lib/asciidoctor/pdf/ext/core.rb +2 -0
- data/lib/asciidoctor/pdf/ext/core/array.rb +1 -0
- data/lib/asciidoctor/pdf/ext/core/hash.rb +1 -0
- data/lib/asciidoctor/pdf/ext/core/numeric.rb +4 -3
- data/lib/asciidoctor/pdf/ext/core/object.rb +1 -0
- data/lib/asciidoctor/pdf/ext/core/quantifiable_stdout.rb +8 -1
- data/lib/asciidoctor/pdf/ext/core/regexp.rb +1 -0
- data/lib/asciidoctor/pdf/ext/core/string.rb +6 -7
- data/lib/asciidoctor/pdf/ext/pdf-core.rb +1 -0
- data/lib/asciidoctor/pdf/ext/pdf-core/page.rb +3 -4
- data/lib/asciidoctor/pdf/ext/pdf-core/pdf_object.rb +2 -1
- data/lib/asciidoctor/pdf/ext/prawn-svg.rb +1 -0
- data/lib/asciidoctor/pdf/ext/prawn-svg/interface.rb +11 -8
- data/lib/asciidoctor/pdf/ext/prawn-table.rb +2 -1
- data/lib/asciidoctor/pdf/ext/prawn-table/cell.rb +9 -10
- data/lib/asciidoctor/pdf/ext/prawn-table/cell/asciidoc.rb +62 -57
- data/lib/asciidoctor/pdf/ext/prawn-table/cell/text.rb +5 -3
- data/lib/asciidoctor/pdf/ext/prawn-templates.rb +1 -0
- data/lib/asciidoctor/pdf/ext/prawn.rb +1 -0
- data/lib/asciidoctor/pdf/ext/prawn/coderay_encoder.rb +73 -72
- data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +814 -818
- data/lib/asciidoctor/pdf/ext/prawn/font/afm.rb +4 -3
- data/lib/asciidoctor/pdf/ext/prawn/formatted_text/box.rb +2 -1
- data/lib/asciidoctor/pdf/ext/prawn/formatted_text/fragment.rb +7 -2
- data/lib/asciidoctor/pdf/ext/prawn/images.rb +45 -44
- data/lib/asciidoctor/pdf/ext/pygments.rb +34 -0
- data/lib/asciidoctor/pdf/ext/rouge.rb +1 -1
- data/lib/asciidoctor/pdf/ext/rouge/formatters/prawn.rb +181 -149
- data/lib/asciidoctor/pdf/ext/rouge/themes/asciidoctor_pdf_default.rb +1 -0
- data/lib/asciidoctor/pdf/formatted_text.rb +2 -0
- data/lib/asciidoctor/pdf/formatted_text/formatter.rb +35 -34
- data/lib/asciidoctor/pdf/formatted_text/fragment_position_renderer.rb +8 -7
- data/lib/asciidoctor/pdf/formatted_text/inline_destination_marker.rb +13 -14
- data/lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb +112 -133
- data/lib/asciidoctor/pdf/formatted_text/inline_image_renderer.rb +43 -41
- data/lib/asciidoctor/pdf/formatted_text/inline_text_aligner.rb +15 -14
- data/lib/asciidoctor/pdf/formatted_text/source_wrap.rb +43 -0
- data/lib/asciidoctor/pdf/formatted_text/text_background_and_border_renderer.rb +46 -37
- data/lib/asciidoctor/pdf/formatted_text/transform.rb +371 -352
- data/lib/asciidoctor/pdf/index_catalog.rb +99 -95
- data/lib/asciidoctor/pdf/measurements.rb +51 -48
- data/lib/asciidoctor/pdf/optimizer.rb +34 -31
- data/lib/asciidoctor/pdf/pdfmark.rb +34 -33
- data/lib/asciidoctor/pdf/roman_numeral.rb +80 -79
- data/lib/asciidoctor/pdf/sanitizer.rb +38 -37
- data/lib/asciidoctor/pdf/temporary_path.rb +10 -9
- data/lib/asciidoctor/pdf/text_transformer.rb +101 -100
- data/lib/asciidoctor/pdf/theme_loader.rb +258 -256
- data/lib/asciidoctor/pdf/version.rb +5 -4
- metadata +55 -6
- data/lib/asciidoctor/pdf/ext/rouge/themes/bw.rb +0 -39
- data/lib/asciidoctor/pdf/ext/ttfunk.rb +0 -9
@@ -1,21 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Asciidoctor::PDF::FormattedText
|
3
|
-
module InlineTextAligner
|
4
|
-
|
4
|
+
module InlineTextAligner
|
5
|
+
module_function
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
7
|
+
def render_behind fragment
|
8
|
+
document = fragment.document
|
9
|
+
text = fragment.text
|
10
|
+
x = fragment.left
|
11
|
+
y = fragment.baseline
|
12
|
+
align = fragment.format_state[:align]
|
13
|
+
if align == :center || align == :right
|
14
|
+
if (gap_width = fragment.width - (document.width_of text)) != 0
|
15
|
+
x += gap_width * (align == :center ? 0.5 : 1)
|
16
|
+
end
|
15
17
|
end
|
18
|
+
document.draw_text! text, at: [x, y]
|
19
|
+
fragment.conceal
|
16
20
|
end
|
17
|
-
document.draw_text! text, at: [x, y]
|
18
|
-
fragment.conceal
|
19
21
|
end
|
20
22
|
end
|
21
|
-
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Asciidoctor
|
4
|
+
module PDF
|
5
|
+
module FormattedText
|
6
|
+
module SourceWrap
|
7
|
+
NoBreakSpace = ?\u00a0
|
8
|
+
|
9
|
+
def wrap array
|
10
|
+
initialize_wrap array
|
11
|
+
stop = nil
|
12
|
+
highlight_line = nil
|
13
|
+
unconsumed = @arranger.unconsumed
|
14
|
+
if (linenum_fragment = unconsumed[0] || {})[:linenum]
|
15
|
+
linenum_spacer = { text: NoBreakSpace + (' ' * (linenum_fragment[:text].length - 1)) }
|
16
|
+
end
|
17
|
+
until stop
|
18
|
+
if linenum_spacer && (first_fragment = unconsumed[0])
|
19
|
+
if first_fragment[:linenum]
|
20
|
+
highlight_line = (second_fragment = unconsumed[1]) && (second_fragment[:highlight]) ? second_fragment.dup : nil
|
21
|
+
else
|
22
|
+
first_fragment[:text] = first_fragment[:text].lstrip
|
23
|
+
@arranger.unconsumed.unshift highlight_line if highlight_line
|
24
|
+
@arranger.unconsumed.unshift linenum_spacer.dup
|
25
|
+
end
|
26
|
+
end
|
27
|
+
@line_wrap.wrap_line document: @document, kerning: @kerning, width: available_width, arranger: @arranger, disable_wrap_by_char: @disable_wrap_by_char
|
28
|
+
if enough_height_for_this_line?
|
29
|
+
move_baseline_down
|
30
|
+
print_line
|
31
|
+
else
|
32
|
+
stop = true
|
33
|
+
end
|
34
|
+
stop ||= @single_line || @arranger.finished?
|
35
|
+
end
|
36
|
+
@text = @printed_lines.join ?\n
|
37
|
+
@everything_printed = @arranger.finished?
|
38
|
+
@arranger.unconsumed
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -1,46 +1,55 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Asciidoctor::Pdf::FormattedText
|
3
|
-
module TextBackgroundAndBorderRenderer
|
4
|
-
|
4
|
+
module TextBackgroundAndBorderRenderer
|
5
|
+
module_function
|
5
6
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
prev_fill_color = pdf.fill_color
|
22
|
-
pdf.fill_color background_color
|
23
|
-
if border_radius
|
24
|
-
pdf.fill_rounded_rectangle at, width, height, border_radius
|
7
|
+
DummyText = ?\u0000
|
8
|
+
|
9
|
+
# render_behind is called before the text is printed
|
10
|
+
def render_behind fragment
|
11
|
+
return if (pdf = fragment.document).scratch?
|
12
|
+
data = fragment.format_state
|
13
|
+
if data[:inline_block]
|
14
|
+
padding = (height = fragment.line_height) - fragment.height
|
15
|
+
at = [fragment.left, fragment.top + padding * 0.5]
|
16
|
+
width = data[:extend] ? (pdf.bounds.width - fragment.left) : fragment.width
|
17
|
+
fragment.conceal if fragment.text == DummyText
|
18
|
+
elsif (border_offset = data[:border_offset])
|
19
|
+
at = [fragment.left, fragment.top + border_offset]
|
20
|
+
width = fragment.width
|
21
|
+
height = fragment.height + border_offset * 2
|
25
22
|
else
|
26
|
-
|
23
|
+
at = fragment.top_left
|
24
|
+
width = fragment.width
|
25
|
+
height = fragment.height
|
27
26
|
end
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
pdf.
|
38
|
-
|
39
|
-
|
27
|
+
border_radius = data[:border_radius]
|
28
|
+
if (background_color = data[:background_color])
|
29
|
+
prev_fill_color = pdf.fill_color
|
30
|
+
pdf.fill_color background_color
|
31
|
+
if border_radius
|
32
|
+
pdf.fill_rounded_rectangle at, width, height, border_radius
|
33
|
+
else
|
34
|
+
pdf.fill_rectangle at, width, height
|
35
|
+
end
|
36
|
+
pdf.fill_color prev_fill_color
|
37
|
+
end
|
38
|
+
if (border_width = data[:border_width])
|
39
|
+
border_color = data[:border_color]
|
40
|
+
prev_stroke_color = pdf.stroke_color
|
41
|
+
prev_line_width = pdf.line_width
|
42
|
+
pdf.stroke_color border_color
|
43
|
+
pdf.line_width border_width
|
44
|
+
if border_radius
|
45
|
+
pdf.stroke_rounded_rectangle at, width, height, border_radius
|
46
|
+
else
|
47
|
+
pdf.stroke_rectangle at, width, height
|
48
|
+
end
|
49
|
+
pdf.stroke_color prev_stroke_color
|
50
|
+
pdf.line_width prev_line_width
|
40
51
|
end
|
41
|
-
|
42
|
-
pdf.line_width prev_line_width
|
52
|
+
nil
|
43
53
|
end
|
44
54
|
end
|
45
55
|
end
|
46
|
-
end
|
@@ -1,375 +1,394 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module Asciidoctor
|
3
|
-
module PDF
|
4
|
-
module FormattedText
|
5
|
-
class Transform
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
'font_family' => :font,
|
26
|
-
'font_size' => :size,
|
27
|
-
'text_decoration_color' => :text_decoration_color,
|
28
|
-
'text_decoration_width' => :text_decoration_width,
|
29
|
-
}
|
30
|
-
#DummyText = ?\u0000
|
4
|
+
module PDF
|
5
|
+
module FormattedText
|
6
|
+
class Transform
|
7
|
+
LF = ?\n
|
8
|
+
ZeroWidthSpace = ?\u200b
|
9
|
+
CharEntityTable = { amp: ?&, apos: ?', gt: ?>, lt: ?<, nbsp: ?\u00a0, quot: ?" }
|
10
|
+
CharRefRx = /&(?:(#{CharEntityTable.keys.join ?|})|#(?:(\d\d\d{0,4})|x([a-f\d][a-f\d][a-f\d]{0,3})));/
|
11
|
+
HexColorRx = /^#[a-fA-F0-9]{6}$/
|
12
|
+
TextDecorationTable = { 'underline' => :underline, 'line-through' => :strikethrough }
|
13
|
+
ThemeKeyToFragmentProperty = {
|
14
|
+
'background_color' => :background_color,
|
15
|
+
'border_color' => :border_color,
|
16
|
+
'border_offset' => :border_offset,
|
17
|
+
'border_radius' => :border_radius,
|
18
|
+
'border_width' => :border_width,
|
19
|
+
'font_color' => :color,
|
20
|
+
'font_family' => :font,
|
21
|
+
'font_size' => :size,
|
22
|
+
'text_decoration_color' => :text_decoration_color,
|
23
|
+
'text_decoration_width' => :text_decoration_width,
|
24
|
+
}
|
25
|
+
#DummyText = ?\u0000
|
31
26
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
27
|
+
def initialize options = {}
|
28
|
+
@merge_adjacent_text_nodes = options[:merge_adjacent_text_nodes]
|
29
|
+
# TODO: add support for character spacing
|
30
|
+
if (theme = options[:theme])
|
31
|
+
@theme_settings = {
|
32
|
+
button: {
|
33
|
+
color: theme.button_font_color,
|
34
|
+
font: theme.button_font_family,
|
35
|
+
size: theme.button_font_size,
|
36
|
+
styles: (to_styles theme.button_font_style),
|
37
|
+
background_color: (button_bg_color = theme.button_background_color),
|
38
|
+
border_width: (button_border_width = theme.button_border_width),
|
39
|
+
border_color: button_border_width && (theme.button_border_color || theme.base_border_color),
|
40
|
+
border_offset: (button_border_offset = (button_bg_or_border = button_bg_color || button_border_width) && theme.button_border_offset),
|
41
|
+
border_radius: button_bg_or_border && theme.button_border_radius,
|
42
|
+
align: button_border_offset && :center,
|
43
|
+
callback: button_bg_or_border && [TextBackgroundAndBorderRenderer],
|
44
|
+
}.compact,
|
45
|
+
code: {
|
46
|
+
color: theme.literal_font_color,
|
47
|
+
font: theme.literal_font_family,
|
48
|
+
size: theme.literal_font_size,
|
49
|
+
styles: (to_styles theme.literal_font_style),
|
50
|
+
background_color: (mono_bg_color = theme.literal_background_color),
|
51
|
+
border_width: (mono_border_width = theme.literal_border_width),
|
52
|
+
border_color: mono_border_width && (theme.literal_border_color || theme.base_border_color),
|
53
|
+
border_offset: (mono_border_offset = (mono_bg_or_border = mono_bg_color || mono_border_width) && theme.literal_border_offset),
|
54
|
+
border_radius: mono_bg_or_border && theme.literal_border_radius,
|
55
|
+
align: mono_border_offset && :center,
|
56
|
+
callback: mono_bg_or_border && [TextBackgroundAndBorderRenderer],
|
57
|
+
}.compact,
|
58
|
+
key: {
|
59
|
+
color: theme.key_font_color,
|
60
|
+
font: theme.key_font_family || theme.literal_font_family,
|
61
|
+
size: theme.key_font_size,
|
62
|
+
styles: (to_styles theme.key_font_style),
|
63
|
+
background_color: (key_bg_color = theme.key_background_color),
|
64
|
+
border_width: (key_border_width = theme.key_border_width),
|
65
|
+
border_color: key_border_width && (theme.key_border_color || theme.base_border_color),
|
66
|
+
border_offset: (key_border_offset = (key_bg_or_border = key_bg_color || key_border_width) && theme.key_border_offset),
|
67
|
+
border_radius: key_bg_or_border && theme.key_border_radius,
|
68
|
+
align: key_border_offset && :center,
|
69
|
+
callback: key_bg_or_border && [TextBackgroundAndBorderRenderer],
|
70
|
+
}.compact,
|
71
|
+
link: {
|
72
|
+
color: theme.link_font_color,
|
73
|
+
font: theme.link_font_family,
|
74
|
+
size: theme.link_font_size,
|
75
|
+
styles: (to_styles theme.link_font_style, theme.link_text_decoration),
|
76
|
+
text_decoration_color: theme.link_text_decoration_color,
|
77
|
+
text_decoration_width: theme.link_text_decoration_width,
|
78
|
+
}.compact,
|
79
|
+
mark: {
|
80
|
+
color: theme.mark_font_color,
|
81
|
+
styles: (to_styles theme.mark_font_style),
|
82
|
+
background_color: (mark_bg_color = theme.mark_background_color),
|
83
|
+
border_offset: (mark_border_offset = mark_bg_color && theme.mark_border_offset),
|
84
|
+
align: mark_border_offset && :center,
|
85
|
+
callback: mark_bg_color && [TextBackgroundAndBorderRenderer],
|
86
|
+
}.compact,
|
87
|
+
}
|
88
|
+
revise_roles = [].to_set
|
89
|
+
theme.each_pair.each_with_object @theme_settings do |(key, val), accum|
|
90
|
+
next unless (key = key.to_s).start_with? 'role_'
|
91
|
+
role, key = (key.slice 5, key.length).split '_', 2
|
92
|
+
if (prop = ThemeKeyToFragmentProperty[key])
|
93
|
+
(accum[role] ||= {})[prop] = val
|
94
|
+
#elsif key == 'font_kerning'
|
95
|
+
# unless (resolved_val = val == 'none' ? false : (val == 'normal' ? true : nil)).nil?
|
96
|
+
# (accum[role] ||= {})[:kerning] = resolved_val
|
97
|
+
# end
|
98
|
+
elsif key == 'font_style' || key == 'text_decoration'
|
99
|
+
revise_roles << role
|
100
|
+
end
|
101
|
+
end
|
102
|
+
revise_roles.each_with_object @theme_settings do |role, accum|
|
103
|
+
(accum[role] ||= {})[:styles] = to_styles theme[%(role_#{role}_font_style)], theme[%(role_#{role}_text_decoration)]
|
104
|
+
end
|
105
|
+
@theme_settings['line-through'] = { styles: [:strikethrough].to_set } unless @theme_settings.key? 'line-through'
|
106
|
+
@theme_settings['underline'] = { styles: [:underline].to_set } unless @theme_settings.key? 'underline'
|
107
|
+
unless @theme_settings.key? 'big'
|
108
|
+
if (base_font_size_large = theme.base_font_size_large)
|
109
|
+
@theme_settings['big'] = { size: %(#{(base_font_size_large / theme.base_font_size.to_f).round 4}em) }
|
110
|
+
else
|
111
|
+
@theme_settings['big'] = { size: '1.1667em' }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
unless @theme_settings.key? 'small'
|
115
|
+
if (base_font_size_small = theme.base_font_size_small)
|
116
|
+
@theme_settings['small'] = { size: %(#{(base_font_size_small / theme.base_font_size.to_f).round 4}em) }
|
117
|
+
else
|
118
|
+
@theme_settings['small'] = { size: '0.8333em' }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
else
|
122
|
+
@theme_settings = {
|
123
|
+
button: { font: 'Courier', styles: [:bold].to_set },
|
124
|
+
code: { font: 'Courier' },
|
125
|
+
key: { font: 'Courier', styles: [:italic].to_set },
|
126
|
+
link: { color: '0000FF' },
|
127
|
+
mark: { background_color: 'FFFF00', callback: [TextBackgroundAndBorderRenderer] },
|
128
|
+
'line-through' => { styles: [:strikethrough].to_set },
|
129
|
+
'underline' => { styles: [:underline].to_set },
|
130
|
+
'big' => { size: '1.667em' },
|
131
|
+
'small' => { size: '0.8333em' },
|
132
|
+
}
|
97
133
|
end
|
98
134
|
end
|
99
|
-
accum
|
100
|
-
end
|
101
|
-
revise_roles.reduce @theme_settings do |accum, role|
|
102
|
-
(accum[role] ||= {})[:styles] = to_styles theme[%(role_#{role}_font_style)], theme[%(role_#{role}_text_decoration)]
|
103
|
-
accum
|
104
|
-
end
|
105
|
-
@theme_settings['line-through'] = { styles: [:strikethrough].to_set } unless @theme_settings.key? 'line-through'
|
106
|
-
@theme_settings['underline'] = { styles: [:underline].to_set } unless @theme_settings.key? 'underline'
|
107
|
-
unless @theme_settings.key? 'big'
|
108
|
-
if (base_font_size_large = theme.base_font_size_large)
|
109
|
-
@theme_settings['big'] = { size: %(#{(base_font_size_large / theme.base_font_size.to_f).round 4}em) }
|
110
|
-
else
|
111
|
-
@theme_settings['big'] = { size: '1.1667em' }
|
112
|
-
end
|
113
|
-
end
|
114
|
-
unless @theme_settings.key? 'small'
|
115
|
-
if (base_font_size_small = theme.base_font_size_small)
|
116
|
-
@theme_settings['small'] = { size: %(#{(base_font_size_small / theme.base_font_size.to_f).round 4}em) }
|
117
|
-
else
|
118
|
-
@theme_settings['small'] = { size: '0.8333em' }
|
119
|
-
end
|
120
|
-
end
|
121
|
-
else
|
122
|
-
@theme_settings = {
|
123
|
-
button: { font: 'Courier', styles: [:bold].to_set },
|
124
|
-
code: { font: 'Courier' },
|
125
|
-
key: { font: 'Courier', styles: [:italic].to_set },
|
126
|
-
link: { color: '0000FF' },
|
127
|
-
mark: { background_color: 'FFFF00', callback: [TextBackgroundAndBorderRenderer] },
|
128
|
-
'line-through' => { styles: [:strikethrough].to_set },
|
129
|
-
'underline' => { styles: [:underline].to_set },
|
130
|
-
'big' => { size: '1.667em' },
|
131
|
-
'small' => { size: '0.8333em' },
|
132
|
-
}
|
133
|
-
end
|
134
|
-
end
|
135
135
|
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
136
|
+
def apply parsed, fragments = [], inherited = nil
|
137
|
+
previous_fragment_is_text = false
|
138
|
+
# NOTE we use each since using inject is slower than a manual loop
|
139
|
+
parsed.each do |node|
|
140
|
+
case node[:type]
|
141
|
+
when :element
|
142
|
+
# case 1: non-void element
|
143
|
+
if node.key? :pcdata
|
144
|
+
# NOTE skip element if it has no children
|
145
|
+
if (pcdata = node[:pcdata]).empty?
|
146
|
+
## NOTE handle an empty anchor element (i.e., <a ...></a>)
|
147
|
+
#if (tag_name = node[:name]) == :a
|
148
|
+
# seed = clone_fragment inherited, text: DummyText
|
149
|
+
# fragments << build_fragment(seed, tag_name, node[:attributes])
|
150
|
+
# previous_fragment_is_text = false
|
151
|
+
#end
|
152
|
+
else
|
153
|
+
tag_name = node[:name]
|
154
|
+
attributes = node[:attributes]
|
155
|
+
parent = clone_fragment inherited
|
156
|
+
# NOTE decorate child fragments with inherited properties from this element
|
157
|
+
apply pcdata, fragments, (build_fragment parent, tag_name, attributes)
|
158
|
+
previous_fragment_is_text = false
|
159
|
+
end
|
160
|
+
# case 2: void element
|
161
|
+
else
|
162
|
+
case node[:name]
|
163
|
+
when :br
|
164
|
+
if @merge_adjacent_text_nodes && previous_fragment_is_text
|
165
|
+
fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{LF}))
|
166
|
+
else
|
167
|
+
fragments << { text: LF }
|
168
|
+
end
|
169
|
+
previous_fragment_is_text = true
|
170
|
+
when :img
|
171
|
+
attributes = node[:attributes]
|
172
|
+
fragment = {
|
173
|
+
image_path: attributes[:tmp] == 'true' ? attributes[:src].extend(TemporaryPath) : attributes[:src],
|
174
|
+
image_format: attributes[:format],
|
175
|
+
# a zero-width space in the text will cause the image to be duplicated
|
176
|
+
text: (attributes[:alt].delete ZeroWidthSpace),
|
177
|
+
callback: [InlineImageRenderer],
|
178
|
+
}
|
179
|
+
if inherited && (link = inherited[:link])
|
180
|
+
fragment[:link] = link
|
181
|
+
end
|
182
|
+
if (img_w = attributes[:width])
|
183
|
+
fragment[:image_width] = img_w
|
184
|
+
end
|
185
|
+
if (img_fit = attributes[:fit])
|
186
|
+
fragment[:image_fit] = img_fit
|
187
|
+
end
|
188
|
+
fragments << fragment
|
189
|
+
previous_fragment_is_text = false
|
190
|
+
end
|
191
|
+
end
|
192
|
+
when :text
|
193
|
+
if @merge_adjacent_text_nodes && previous_fragment_is_text
|
194
|
+
fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{node[:value]}))
|
195
|
+
else
|
196
|
+
fragments << (clone_fragment inherited, text: node[:value])
|
197
|
+
end
|
198
|
+
previous_fragment_is_text = true
|
199
|
+
when :charref
|
200
|
+
if (ref_type = node[:reference_type]) == :name
|
201
|
+
text = CharEntityTable[node[:value]]
|
202
|
+
elsif ref_type == :decimal
|
203
|
+
# FIXME: AFM fonts do not include a thin space glyph; set fallback_fonts to allow glyph to be resolved
|
204
|
+
text = [node[:value]].pack 'U1'
|
205
|
+
else
|
206
|
+
# FIXME: AFM fonts do not include a thin space glyph; set fallback_fonts to allow glyph to be resolved
|
207
|
+
text = [(node[:value].to_i 16)].pack 'U1'
|
208
|
+
end
|
209
|
+
if @merge_adjacent_text_nodes && previous_fragment_is_text
|
210
|
+
fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{text}))
|
211
|
+
else
|
212
|
+
fragments << (clone_fragment inherited, text: text)
|
213
|
+
end
|
214
|
+
previous_fragment_is_text = true
|
215
|
+
end
|
159
216
|
end
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
217
|
+
fragments
|
218
|
+
end
|
219
|
+
|
220
|
+
def build_fragment fragment, tag_name, attrs = {}
|
221
|
+
styles = (fragment[:styles] ||= ::Set.new)
|
222
|
+
case tag_name
|
223
|
+
when :strong
|
224
|
+
styles << :bold
|
225
|
+
when :em
|
226
|
+
styles << :italic
|
227
|
+
when :button, :code, :key, :mark
|
228
|
+
update_fragment fragment, @theme_settings[tag_name]
|
229
|
+
when :color
|
230
|
+
if (rgb = attrs[:rgb])
|
231
|
+
case rgb.chr
|
232
|
+
when '#'
|
233
|
+
fragment[:color] = rgb.slice 1, rgb.length
|
234
|
+
when '['
|
235
|
+
# treat value as CMYK array (e.g., "[50, 100, 0, 0]")
|
236
|
+
fragment[:color] = rgb.slice(1, rgb.length).chomp(']').split(', ').map(&:to_i)
|
237
|
+
# ...or we could honor an rgb array too
|
238
|
+
#case (vals = rgb.slice(1, rgb.length).chomp(']').split(', ')).size
|
239
|
+
#when 4
|
240
|
+
# fragment[:color] = vals.map(&:to_i)
|
241
|
+
#when 3
|
242
|
+
# fragment[:color] = vals.map {|e| '%02X' % e.to_i }.join
|
243
|
+
#end
|
244
|
+
else
|
245
|
+
fragment[:color] = rgb
|
246
|
+
end
|
247
|
+
# QUESTION should we even support r,g,b and c,m,y,k as individual values?
|
248
|
+
elsif (r_val = attrs[:r]) && (g_val = attrs[:g]) && (b_val = attrs[:b])
|
249
|
+
fragment[:color] = [r_val, g_val, b_val].map {|e| '%02X' % e.to_i }.join
|
250
|
+
elsif (c_val = attrs[:c]) && (m_val = attrs[:m]) && (y_val = attrs[:y]) && (k_val = attrs[:k])
|
251
|
+
fragment[:color] = [c_val.to_i, m_val.to_i, y_val.to_i, k_val.to_i]
|
168
252
|
end
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
fragment = {
|
173
|
-
image_path: attributes[:tmp] == 'true' ? attributes[:src].extend(TemporaryPath) : attributes[:src],
|
174
|
-
image_format: attributes[:format],
|
175
|
-
# a zero-width space in the text will cause the image to be duplicated
|
176
|
-
text: (attributes[:alt].delete ZeroWidthSpace),
|
177
|
-
callback: [InlineImageRenderer],
|
178
|
-
}
|
179
|
-
if inherited && (link = inherited[:link])
|
180
|
-
fragment[:link] = link
|
253
|
+
when :font
|
254
|
+
if (value = attrs[:name])
|
255
|
+
fragment[:font] = value
|
181
256
|
end
|
182
|
-
if (
|
183
|
-
|
257
|
+
if (value = attrs[:size])
|
258
|
+
# FIXME: can we make this comparison more robust / accurate?
|
259
|
+
if (f_value = value.to_f).to_s == value || value.to_i.to_s == value
|
260
|
+
fragment[:size] = f_value
|
261
|
+
elsif value != '1em'
|
262
|
+
fragment[:size] = value
|
263
|
+
end
|
184
264
|
end
|
185
|
-
|
186
|
-
|
265
|
+
# NOTE width is used for font-based icons
|
266
|
+
if (value = attrs[:width])
|
267
|
+
fragment[:width] = value
|
268
|
+
fragment[:align] = :center
|
269
|
+
fragment[:callback] = (fragment[:callback] || []) | [InlineTextAligner]
|
270
|
+
end
|
271
|
+
#if (value = attrs[:character_spacing])
|
272
|
+
# fragment[:character_spacing] = value.to_f
|
273
|
+
#end
|
274
|
+
when :a
|
275
|
+
visible = true
|
276
|
+
# a element can have no attributes, so short-circuit if that's the case
|
277
|
+
unless attrs.empty?
|
278
|
+
# NOTE href, anchor, and name are mutually exclusive; nesting is not supported
|
279
|
+
if (value = attrs[:anchor])
|
280
|
+
fragment[:anchor] = value
|
281
|
+
elsif (value = attrs[:href])
|
282
|
+
fragment[:link] = (value.include? ';') ? (value.gsub CharRefRx do
|
283
|
+
$1 ? CharEntityTable[$1.to_sym] : [$2 ? $2.to_i : ($3.to_i 16)].pack('U1')
|
284
|
+
end) : value
|
285
|
+
elsif (value = attrs[:id] || attrs[:name])
|
286
|
+
# NOTE text is null character, which is used as placeholder text so Prawn doesn't drop fragment
|
287
|
+
fragment = { name: value, callback: [InlineDestinationMarker] }
|
288
|
+
if (type = attrs[:type])
|
289
|
+
fragment[:type] = type.to_sym
|
290
|
+
end
|
291
|
+
visible = nil
|
292
|
+
end
|
293
|
+
end
|
294
|
+
update_fragment fragment, @theme_settings[:link] if visible
|
295
|
+
when :sub
|
296
|
+
styles << :subscript
|
297
|
+
when :sup
|
298
|
+
styles << :superscript
|
299
|
+
when :del
|
300
|
+
styles << :strikethrough
|
301
|
+
when :span
|
302
|
+
# NOTE spaces in style value are superfluous for our purpose; split drops record after trailing ;
|
303
|
+
attrs[:style].tr(' ', '').split(';').each do |style|
|
304
|
+
pname, pvalue = style.split ':', 2
|
305
|
+
# TODO: text-transform
|
306
|
+
case pname
|
307
|
+
when 'color'
|
308
|
+
# TODO: check whether the value is a valid hex color?
|
309
|
+
case pvalue.length
|
310
|
+
when 6
|
311
|
+
fragment[:color] = pvalue
|
312
|
+
when 7
|
313
|
+
fragment[:color] = pvalue.slice 1, 6 if pvalue.start_with? '#'
|
314
|
+
end
|
315
|
+
# QUESTION should we support the 3 character form?
|
316
|
+
#when 3
|
317
|
+
# fragment[:color] = pvalue.each_char.map {|c| c * 2 }.join
|
318
|
+
#when 4
|
319
|
+
# fragment[:color] = pvalue.slice(1, 3).each_char.map {|c| c * 2 }.join if pvalue.start_with?('#')
|
320
|
+
when 'font-weight'
|
321
|
+
styles << :bold if pvalue == 'bold'
|
322
|
+
when 'font-style'
|
323
|
+
styles << :italic if pvalue == 'italic'
|
324
|
+
when 'align', 'text-align'
|
325
|
+
fragment[:align] = pvalue.to_sym
|
326
|
+
fragment[:callback] = (fragment[:callback] || []) | [InlineTextAligner]
|
327
|
+
when 'width'
|
328
|
+
# NOTE implicitly activates inline-block behavior
|
329
|
+
fragment[:width] = pvalue
|
330
|
+
when 'background-color' # background-color needed to support syntax highlighters
|
331
|
+
if (pvalue.start_with? '#') && (HexColorRx.match? pvalue)
|
332
|
+
fragment[:background_color] = pvalue.slice 1, pvalue.length
|
333
|
+
fragment[:callback] = [TextBackgroundAndBorderRenderer] | (fragment[:callback] || [])
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end if attrs.key? :style
|
187
337
|
end
|
338
|
+
# TODO: we could limit to select tags, but doesn't seem to really affect performance
|
339
|
+
attrs[:class].split.each do |class_name|
|
340
|
+
next unless @theme_settings.key? class_name
|
341
|
+
update_fragment fragment, @theme_settings[class_name]
|
342
|
+
if fragment[:background_color] || (fragment[:border_color] && fragment[:border_width])
|
343
|
+
fragment[:callback] = [TextBackgroundAndBorderRenderer] | (fragment[:callback] || [])
|
344
|
+
fragment[:align] = :center if fragment[:border_offset]
|
345
|
+
end
|
346
|
+
end if attrs.key? :class
|
347
|
+
fragment.delete :styles if styles.empty?
|
348
|
+
fragment[:callback] = (fragment[:callback] || []) | [InlineTextAligner] if fragment.key? :align
|
349
|
+
fragment
|
188
350
|
end
|
189
|
-
when :text
|
190
|
-
if @merge_adjacent_text_nodes && previous_fragment_is_text
|
191
|
-
fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{node[:value]}))
|
192
|
-
else
|
193
|
-
fragments << (clone_fragment inherited, text: node[:value])
|
194
|
-
end
|
195
|
-
previous_fragment_is_text = true
|
196
|
-
when :charref
|
197
|
-
if (ref_type = node[:reference_type]) == :name
|
198
|
-
text = CharEntityTable[node[:value]]
|
199
|
-
elsif ref_type == :decimal
|
200
|
-
# FIXME AFM fonts do not include a thin space glyph; set fallback_fonts to allow glyph to be resolved
|
201
|
-
text = [node[:value]].pack('U1')
|
202
|
-
else
|
203
|
-
# FIXME AFM fonts do not include a thin space glyph; set fallback_fonts to allow glyph to be resolved
|
204
|
-
text = [(node[:value].to_i 16)].pack('U1')
|
205
|
-
end
|
206
|
-
if @merge_adjacent_text_nodes && previous_fragment_is_text
|
207
|
-
fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{text}))
|
208
|
-
else
|
209
|
-
fragments << (clone_fragment inherited, text: text)
|
210
|
-
end
|
211
|
-
previous_fragment_is_text = true
|
212
|
-
end
|
213
|
-
end
|
214
|
-
fragments
|
215
|
-
end
|
216
351
|
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
when :button, :code, :key, :mark
|
225
|
-
fragment.update(@theme_settings[tag_name]) {|k, oval, nval| k == :styles ? (nval ? oval.merge(nval) : oval.clear) : (k == :callback ? oval.union(nval) : nval) }
|
226
|
-
when :color
|
227
|
-
if (rgb = attrs[:rgb])
|
228
|
-
case rgb.chr
|
229
|
-
when '#'
|
230
|
-
fragment[:color] = rgb[1..-1]
|
231
|
-
when '['
|
232
|
-
# treat value as CMYK array (e.g., "[50, 100, 0, 0]")
|
233
|
-
fragment[:color] = rgb[1..-1].chomp(']').split(', ').map(&:to_i)
|
234
|
-
# ...or we could honor an rgb array too
|
235
|
-
#case (vals = rgb[1..-1].chomp(']').split(', ')).size
|
236
|
-
#when 4
|
237
|
-
# fragment[:color] = vals.map(&:to_i)
|
238
|
-
#when 3
|
239
|
-
# fragment[:color] = vals.map {|e| '%02X' % e.to_i }.join
|
240
|
-
#end
|
241
|
-
else
|
242
|
-
fragment[:color] = rgb
|
243
|
-
end
|
244
|
-
# QUESTION should we even support r,g,b and c,m,y,k as individual values?
|
245
|
-
elsif (r_val = attrs[:r]) && (g_val = attrs[:g]) && (b_val = attrs[:b])
|
246
|
-
fragment[:color] = [r_val, g_val, b_val].map {|e| '%02X' % e.to_i }.join
|
247
|
-
elsif (c_val = attrs[:c]) && (m_val = attrs[:m]) && (y_val = attrs[:y]) && (k_val = attrs[:k])
|
248
|
-
fragment[:color] = [c_val.to_i, m_val.to_i, y_val.to_i, k_val.to_i]
|
249
|
-
end
|
250
|
-
when :font
|
251
|
-
if (value = attrs[:name])
|
252
|
-
fragment[:font] = value
|
253
|
-
end
|
254
|
-
if (value = attrs[:size])
|
255
|
-
# FIXME can we make this comparison more robust / accurate?
|
256
|
-
if %(#{f_value = value.to_f}) == value || %(#{value.to_i}) == value
|
257
|
-
fragment[:size] = f_value
|
258
|
-
elsif value != '1em'
|
259
|
-
fragment[:size] = value
|
260
|
-
end
|
261
|
-
end
|
262
|
-
if (value = attrs[:width])
|
263
|
-
fragment[:width] = value
|
264
|
-
if (value = attrs[:align])
|
265
|
-
fragment[:align] = value.to_sym
|
266
|
-
fragment[:callback] = ((fragment[:callback] ||= []) << InlineTextAligner).uniq
|
267
|
-
end
|
268
|
-
end
|
269
|
-
#if (value = attrs[:character_spacing])
|
270
|
-
# fragment[:character_spacing] = value.to_f
|
271
|
-
#end
|
272
|
-
when :a
|
273
|
-
visible = true
|
274
|
-
# a element can have no attributes, so short-circuit if that's the case
|
275
|
-
unless attrs.empty?
|
276
|
-
# NOTE href, anchor, and name are mutually exclusive; nesting is not supported
|
277
|
-
if (value = attrs[:anchor])
|
278
|
-
fragment[:anchor] = value
|
279
|
-
elsif (value = attrs[:href])
|
280
|
-
fragment[:link] = value.include?(';') ? value.gsub(CharRefRx) {
|
281
|
-
$1 ? CharEntityTable[$1.to_sym] : [$2 ? $2.to_i : ($3.to_i 16)].pack('U1')
|
282
|
-
} : value
|
283
|
-
elsif (value = attrs[:name])
|
284
|
-
# NOTE text is null character, which is used as placeholder text so Prawn doesn't drop fragment
|
285
|
-
fragment[:name] = value
|
286
|
-
if (type = attrs[:type])
|
287
|
-
fragment[:type] = type.to_sym
|
352
|
+
def clone_fragment fragment, append = nil
|
353
|
+
if fragment
|
354
|
+
fragment = fragment.dup
|
355
|
+
fragment[:styles] = fragment[:styles].dup if fragment.key? :styles
|
356
|
+
fragment[:callback] = fragment[:callback].dup if fragment.key? :callback
|
357
|
+
else
|
358
|
+
fragment = {}
|
288
359
|
end
|
289
|
-
fragment
|
290
|
-
|
360
|
+
fragment.update append if append
|
361
|
+
fragment
|
291
362
|
end
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
# NOTE spaces in style attribute value are superfluous, for our purpose; split drops record after trailing ;
|
302
|
-
attrs[:style].tr(' ', '').split(';').each do |style|
|
303
|
-
pname, pvalue = style.split(':', 2)
|
304
|
-
case pname
|
305
|
-
when 'color'
|
306
|
-
# QUESTION should we check whether the value is a valid hex color?
|
307
|
-
unless fragment[:color]
|
308
|
-
case pvalue.length
|
309
|
-
when 6
|
310
|
-
fragment[:color] = pvalue
|
311
|
-
when 7
|
312
|
-
fragment[:color] = pvalue.slice(1, 6) if pvalue.start_with?('#')
|
313
|
-
# QUESTION should we support the 3 character form?
|
314
|
-
#when 3
|
315
|
-
# fragment[:color] = pvalue.each_char.map {|c| c * 2 }.join
|
316
|
-
#when 4
|
317
|
-
# fragment[:color] = pvalue.slice(1, 3).each_char.map {|c| c * 2 }.join if pvalue.start_with?('#')
|
318
|
-
end
|
319
|
-
end
|
320
|
-
when 'font-weight'
|
321
|
-
if pvalue == 'bold'
|
322
|
-
styles << :bold
|
363
|
+
|
364
|
+
def to_styles font_style, text_decoration = nil
|
365
|
+
case font_style
|
366
|
+
when 'bold'
|
367
|
+
styles = [:bold].to_set
|
368
|
+
when 'italic'
|
369
|
+
styles = [:italic].to_set
|
370
|
+
when 'bold_italic'
|
371
|
+
styles = [:bold, :italic].to_set
|
323
372
|
end
|
324
|
-
|
325
|
-
|
326
|
-
|
373
|
+
if (style = TextDecorationTable[text_decoration])
|
374
|
+
styles ? (styles << style) : [style].to_set
|
375
|
+
else
|
376
|
+
styles
|
327
377
|
end
|
328
|
-
# TODO text-transform
|
329
378
|
end
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
379
|
+
|
380
|
+
def update_fragment fragment, props
|
381
|
+
fragment.update props do |k, oval, nval|
|
382
|
+
if k == :styles
|
383
|
+
nval ? (oval.merge nval) : oval.clear
|
384
|
+
elsif k == :callback
|
385
|
+
oval | nval
|
386
|
+
else
|
387
|
+
nval
|
388
|
+
end
|
389
|
+
end
|
338
390
|
end
|
339
391
|
end
|
340
|
-
end if attrs.key?(:class)
|
341
|
-
fragment.delete(:styles) if styles.empty?
|
342
|
-
fragment
|
343
|
-
end
|
344
|
-
|
345
|
-
def clone_fragment fragment, append = nil
|
346
|
-
if fragment
|
347
|
-
fragment = fragment.dup
|
348
|
-
fragment[:styles] = fragment[:styles].dup if fragment.key? :styles
|
349
|
-
fragment[:callback] = fragment[:callback].dup if fragment.key? :callback
|
350
|
-
else
|
351
|
-
fragment = {}
|
352
392
|
end
|
353
|
-
fragment.update append if append
|
354
|
-
fragment
|
355
393
|
end
|
356
|
-
|
357
|
-
def to_styles(font_style, text_decoration = nil)
|
358
|
-
case font_style
|
359
|
-
when 'bold'
|
360
|
-
styles = [:bold].to_set
|
361
|
-
when 'italic'
|
362
|
-
styles = [:italic].to_set
|
363
|
-
when 'bold_italic'
|
364
|
-
styles = [:bold, :italic].to_set
|
365
|
-
end
|
366
|
-
if (style = TextDecorationTable[text_decoration])
|
367
|
-
styles ? (styles << style) : [style].to_set
|
368
|
-
else
|
369
|
-
styles
|
370
|
-
end
|
371
|
-
end
|
372
|
-
end
|
373
|
-
end
|
374
|
-
end
|
375
394
|
end
|