asciidoctor-pdf 1.5.0.beta.1 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +277 -2
- data/LICENSE.adoc +1 -1
- data/NOTICE.adoc +1 -1
- data/README.adoc +486 -292
- data/asciidoctor-pdf.gemspec +12 -11
- data/bin/asciidoctor-pdf +2 -6
- data/bin/asciidoctor-pdf-optimize +20 -0
- data/data/fonts/ABOUT-mplus1mn-subset +26 -0
- data/data/fonts/ABOUT-mplus1p-subset +26 -0
- data/data/fonts/ABOUT-notoemoji-subset +3 -0
- data/data/fonts/ABOUT-notoserif-subset +26 -0
- data/data/fonts/{LICENSE-mplus-testflight-58 → LICENSE-mplus} +2 -2
- data/data/fonts/{LICENSE-noto-2015-06-05 → LICENSE-notoserif} +0 -0
- data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-bold-subset.ttf +0 -0
- data/data/fonts/mplus1mn-bold_italic-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-bold_italic-subset.ttf +0 -0
- data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-italic-subset.ttf +0 -0
- data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
- data/data/fonts/mplus1mn-regular-subset.ttf +0 -0
- data/data/fonts/mplus1p-regular-fallback.ttf +0 -0
- data/data/fonts/notoemoji-subset.ttf +0 -0
- data/data/fonts/notoserif-bold-subset.ttf +0 -0
- data/data/fonts/notoserif-bold_italic-subset.ttf +0 -0
- data/data/fonts/notoserif-italic-subset.ttf +0 -0
- data/data/fonts/notoserif-regular-subset.ttf +0 -0
- data/data/themes/base-theme.yml +22 -4
- data/data/themes/default-theme.yml +59 -29
- data/data/themes/default-with-fallback-font-theme.yml +4 -17
- data/docs/theming-guide.adoc +1647 -167
- data/lib/asciidoctor/pdf/converter.rb +4489 -0
- data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/abstract_block.rb +2 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_node.rb +7 -0
- data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/document.rb +2 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor/image.rb +35 -0
- data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/list.rb +4 -2
- data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/list_item.rb +3 -1
- data/lib/asciidoctor/pdf/ext/asciidoctor/logging_shim.rb +33 -0
- data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/section.rb +9 -6
- data/lib/asciidoctor/pdf/ext/asciidoctor.rb +11 -0
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/array.rb +6 -0
- data/lib/asciidoctor/pdf/ext/core/file.rb +9 -0
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/hash.rb +2 -0
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/numeric.rb +5 -3
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/object.rb +3 -1
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/quantifiable_stdout.rb +9 -1
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/regexp.rb +2 -0
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/string.rb +9 -13
- data/lib/asciidoctor/pdf/ext/core.rb +10 -0
- data/lib/asciidoctor/pdf/ext/pdf-core/page.rb +54 -0
- data/lib/asciidoctor/pdf/ext/pdf-core/pdf_object.rb +8 -0
- data/lib/asciidoctor/pdf/ext/pdf-core.rb +4 -0
- data/lib/asciidoctor/pdf/ext/prawn/coderay_encoder.rb +117 -0
- data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +922 -0
- data/lib/{asciidoctor-pdf/prawn_ext → asciidoctor/pdf/ext/prawn}/font/afm.rb +14 -10
- data/lib/asciidoctor/pdf/ext/prawn/font_metric_cache.rb +9 -0
- data/lib/asciidoctor/pdf/ext/prawn/formatted_text/box.rb +66 -0
- data/lib/{asciidoctor-pdf/prawn_ext → asciidoctor/pdf/ext/prawn}/formatted_text/fragment.rb +16 -12
- data/lib/asciidoctor/pdf/ext/prawn/images.rb +54 -0
- data/lib/asciidoctor/pdf/ext/prawn-svg/interface.rb +14 -0
- data/lib/{asciidoctor-pdf/prawn-svg_ext.rb → asciidoctor/pdf/ext/prawn-svg.rb} +3 -1
- data/lib/asciidoctor/pdf/ext/prawn-table/cell/asciidoc.rb +76 -0
- data/lib/{asciidoctor-pdf/prawn-table_ext → asciidoctor/pdf/ext/prawn-table}/cell/text.rb +6 -3
- data/lib/{asciidoctor-pdf/prawn-table_ext → asciidoctor/pdf/ext/prawn-table}/cell.rb +10 -10
- data/lib/asciidoctor/pdf/ext/prawn-table.rb +6 -0
- data/lib/{asciidoctor-pdf/prawn-templates_ext.rb → asciidoctor/pdf/ext/prawn-templates.rb} +2 -0
- data/lib/asciidoctor/pdf/ext/prawn.rb +9 -0
- data/lib/asciidoctor/pdf/ext/pygments.rb +34 -0
- data/lib/asciidoctor/pdf/ext/rouge/formatters/prawn.rb +208 -0
- data/lib/{asciidoctor-pdf/rouge_ext → asciidoctor/pdf/ext/rouge}/themes/asciidoctor_pdf_default.rb +2 -0
- data/lib/asciidoctor/pdf/ext/rouge.rb +5 -0
- data/lib/asciidoctor/pdf/ext.rb +9 -0
- data/lib/asciidoctor/pdf/formatted_text/formatter.rb +43 -0
- data/lib/asciidoctor/pdf/formatted_text/fragment_position_renderer.rb +14 -0
- data/lib/asciidoctor/pdf/formatted_text/inline_destination_marker.rb +21 -0
- data/lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb +134 -0
- data/lib/asciidoctor/pdf/formatted_text/inline_image_renderer.rb +51 -0
- data/lib/asciidoctor/pdf/formatted_text/inline_text_aligner.rb +22 -0
- data/lib/{asciidoctor-pdf → asciidoctor/pdf}/formatted_text/parser.rb +31 -7
- data/lib/{asciidoctor-pdf → asciidoctor/pdf}/formatted_text/parser.treetop +3 -4
- data/lib/asciidoctor/pdf/formatted_text/source_wrap.rb +43 -0
- data/lib/asciidoctor/pdf/formatted_text/text_background_and_border_renderer.rb +55 -0
- data/lib/asciidoctor/pdf/formatted_text/transform.rb +394 -0
- data/lib/{asciidoctor-pdf → asciidoctor/pdf}/formatted_text.rb +4 -0
- data/lib/asciidoctor/pdf/index_catalog.rb +133 -0
- data/lib/asciidoctor/pdf/measurements.rb +62 -0
- data/lib/asciidoctor/pdf/optimizer.rb +44 -0
- data/lib/asciidoctor/pdf/pdfmark.rb +41 -0
- data/lib/asciidoctor/pdf/roman_numeral.rb +128 -0
- data/lib/asciidoctor/pdf/sanitizer.rb +45 -0
- data/lib/asciidoctor/pdf/text_transformer.rb +116 -0
- data/lib/asciidoctor/pdf/theme_loader.rb +305 -0
- data/lib/asciidoctor/pdf/version.rb +8 -1
- data/lib/asciidoctor/pdf.rb +15 -1
- data/lib/asciidoctor-pdf/converter.rb +2 -3824
- data/lib/asciidoctor-pdf/version.rb +3 -6
- data/lib/asciidoctor-pdf.rb +3 -4
- metadata +130 -85
- data/lib/asciidoctor-pdf/asciidoctor_ext/image.rb +0 -24
- data/lib/asciidoctor-pdf/asciidoctor_ext/logging_shim.rb +0 -25
- data/lib/asciidoctor-pdf/asciidoctor_ext.rb +0 -8
- data/lib/asciidoctor-pdf/core_ext/ostruct.rb +0 -8
- data/lib/asciidoctor-pdf/core_ext.rb +0 -6
- data/lib/asciidoctor-pdf/formatted_text/formatter.rb +0 -40
- data/lib/asciidoctor-pdf/formatted_text/inline_destination_marker.rb +0 -21
- data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +0 -160
- data/lib/asciidoctor-pdf/formatted_text/inline_image_renderer.rb +0 -46
- data/lib/asciidoctor-pdf/formatted_text/inline_text_aligner.rb +0 -20
- data/lib/asciidoctor-pdf/formatted_text/text_background_and_border_renderer.rb +0 -45
- data/lib/asciidoctor-pdf/formatted_text/transform.rb +0 -294
- data/lib/asciidoctor-pdf/implicit_header_processor.rb +0 -63
- data/lib/asciidoctor-pdf/index_catalog.rb +0 -127
- data/lib/asciidoctor-pdf/measurements.rb +0 -58
- data/lib/asciidoctor-pdf/pdf-core_ext/page.rb +0 -25
- data/lib/asciidoctor-pdf/pdf-core_ext/pdf_object.rb +0 -6
- data/lib/asciidoctor-pdf/pdf-core_ext.rb +0 -2
- data/lib/asciidoctor-pdf/pdfmark.rb +0 -33
- data/lib/asciidoctor-pdf/prawn-svg_ext/interface.rb +0 -10
- data/lib/asciidoctor-pdf/prawn-table_ext/cell/asciidoc.rb +0 -69
- data/lib/asciidoctor-pdf/prawn-table_ext.rb +0 -4
- data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +0 -115
- data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +0 -904
- data/lib/asciidoctor-pdf/prawn_ext/images.rb +0 -51
- data/lib/asciidoctor-pdf/prawn_ext.rb +0 -5
- data/lib/asciidoctor-pdf/roman_numeral.rb +0 -126
- data/lib/asciidoctor-pdf/rouge_ext/formatters/prawn.rb +0 -175
- data/lib/asciidoctor-pdf/rouge_ext/themes/bw.rb +0 -38
- data/lib/asciidoctor-pdf/rouge_ext.rb +0 -4
- data/lib/asciidoctor-pdf/sanitizer.rb +0 -101
- data/lib/asciidoctor-pdf/temporary_path.rb +0 -13
- data/lib/asciidoctor-pdf/theme_loader.rb +0 -280
- data/lib/asciidoctor-pdf/ttfunk_ext.rb +0 -8
@@ -0,0 +1,394 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Asciidoctor
|
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
|
26
|
+
|
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
|
+
}
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
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[: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
|
216
|
+
end
|
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]
|
252
|
+
end
|
253
|
+
when :font
|
254
|
+
if (value = attrs[:name])
|
255
|
+
fragment[:font] = value
|
256
|
+
end
|
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
|
264
|
+
end
|
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
|
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
|
350
|
+
end
|
351
|
+
|
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 = {}
|
359
|
+
end
|
360
|
+
fragment.update append if append
|
361
|
+
fragment
|
362
|
+
end
|
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
|
372
|
+
end
|
373
|
+
if (style = TextDecorationTable[text_decoration])
|
374
|
+
styles ? (styles << style) : [style].to_set
|
375
|
+
else
|
376
|
+
styles
|
377
|
+
end
|
378
|
+
end
|
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
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
@@ -1,10 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'treetop'
|
2
4
|
require 'set' unless defined? Set
|
3
5
|
require_relative 'formatted_text/parser'
|
4
6
|
require_relative 'formatted_text/transform'
|
5
7
|
require_relative 'formatted_text/formatter'
|
8
|
+
require_relative 'formatted_text/fragment_position_renderer'
|
6
9
|
require_relative 'formatted_text/inline_destination_marker'
|
7
10
|
require_relative 'formatted_text/inline_image_arranger'
|
8
11
|
require_relative 'formatted_text/inline_image_renderer'
|
9
12
|
require_relative 'formatted_text/inline_text_aligner'
|
13
|
+
require_relative 'formatted_text/source_wrap'
|
10
14
|
require_relative 'formatted_text/text_background_and_border_renderer'
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Asciidoctor
|
4
|
+
module PDF
|
5
|
+
class IndexCatalog
|
6
|
+
include ::Asciidoctor::PDF::TextTransformer
|
7
|
+
|
8
|
+
LeadingAlphaRx = /^\p{Alpha}/
|
9
|
+
|
10
|
+
attr_accessor :start_page_number
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@categories = {}
|
14
|
+
@start_page_number = 1
|
15
|
+
@dests = {}
|
16
|
+
@sequence = 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def next_anchor_name
|
20
|
+
%(__indexterm-#{@sequence += 1})
|
21
|
+
end
|
22
|
+
|
23
|
+
def store_term names, dest = nil
|
24
|
+
if (num_terms = names.size) > 2
|
25
|
+
store_tertiary_term names[0], names[1], names[2], dest
|
26
|
+
elsif num_terms == 2
|
27
|
+
store_secondary_term names[0], names[1], dest
|
28
|
+
elsif num_terms == 1
|
29
|
+
store_primary_term names[0], dest
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def store_primary_term name, dest = nil
|
34
|
+
store_dest dest if dest
|
35
|
+
(init_category uppercase_mb name.chr).store_term name, dest
|
36
|
+
end
|
37
|
+
|
38
|
+
def store_secondary_term primary_name, secondary_name, dest = nil
|
39
|
+
store_dest dest if dest
|
40
|
+
(store_primary_term primary_name).store_term secondary_name, dest
|
41
|
+
end
|
42
|
+
|
43
|
+
def store_tertiary_term primary_name, secondary_name, tertiary_name, dest = nil
|
44
|
+
store_dest dest if dest
|
45
|
+
(store_secondary_term primary_name, secondary_name).store_term tertiary_name, dest
|
46
|
+
end
|
47
|
+
|
48
|
+
def init_category name
|
49
|
+
name = '@' unless LeadingAlphaRx.match? name
|
50
|
+
@categories[name] ||= IndexTermCategory.new name
|
51
|
+
end
|
52
|
+
|
53
|
+
def find_category name
|
54
|
+
@categories[name]
|
55
|
+
end
|
56
|
+
|
57
|
+
def store_dest dest
|
58
|
+
@dests[dest[:anchor]] = dest
|
59
|
+
end
|
60
|
+
|
61
|
+
def link_dest_to_page anchor, physical_page_number
|
62
|
+
if (dest = @dests[anchor])
|
63
|
+
virtual_page_number = physical_page_number - (@start_page_number - 1)
|
64
|
+
dest[:page] = (virtual_page_number < 1 ? (RomanNumeral.new physical_page_number, :lower) : virtual_page_number).to_s
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def empty?
|
69
|
+
@categories.empty?
|
70
|
+
end
|
71
|
+
|
72
|
+
def categories
|
73
|
+
@categories.empty? ? [] : @categories.values.sort
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class IndexTermGroup
|
78
|
+
include Comparable
|
79
|
+
attr_reader :name
|
80
|
+
|
81
|
+
def initialize name
|
82
|
+
@name = name
|
83
|
+
@terms = {}
|
84
|
+
end
|
85
|
+
|
86
|
+
def store_term name, dest = nil
|
87
|
+
term = (@terms[name] ||= (IndexTerm.new name))
|
88
|
+
term.add_dest dest if dest
|
89
|
+
term
|
90
|
+
end
|
91
|
+
|
92
|
+
def find_term name
|
93
|
+
@terms[name]
|
94
|
+
end
|
95
|
+
|
96
|
+
def terms
|
97
|
+
@terms.empty? ? [] : @terms.values.sort
|
98
|
+
end
|
99
|
+
|
100
|
+
def <=> other
|
101
|
+
(val = @name.casecmp other.name) == 0 ? @name <=> other.name : val
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
class IndexTermCategory < IndexTermGroup; end
|
106
|
+
|
107
|
+
class IndexTerm < IndexTermGroup
|
108
|
+
def initialize name
|
109
|
+
super
|
110
|
+
@dests = ::Set.new
|
111
|
+
end
|
112
|
+
|
113
|
+
alias subterms terms
|
114
|
+
|
115
|
+
def add_dest dest
|
116
|
+
@dests << dest
|
117
|
+
self
|
118
|
+
end
|
119
|
+
|
120
|
+
def dests
|
121
|
+
@dests.select {|d| d.key? :page }.sort {|a, b| a[:page] <=> b[:page] }
|
122
|
+
end
|
123
|
+
|
124
|
+
def container?
|
125
|
+
@dests.empty? || @dests.none? {|d| d.key? :page }
|
126
|
+
end
|
127
|
+
|
128
|
+
def leaf?
|
129
|
+
@terms.empty?
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Asciidoctor
|
4
|
+
module PDF
|
5
|
+
module Measurements
|
6
|
+
MeasurementValueRx = /(\d+|\d*\.\d+)(in|mm|cm|p[txc])?$/
|
7
|
+
InsetMeasurementValueRx = /(?<=^| |\()(-?\d+(?:\.\d+)?)(in|mm|cm|p[txc])(?=$| |\))/
|
8
|
+
MeasurementValueHintRx = /\d(in|mm|cm|p[txc])/
|
9
|
+
|
10
|
+
# Convert the specified string value to a pt value from the
|
11
|
+
# specified unit of measurement (e.g., in, cm, mm, etc).
|
12
|
+
# If the unit of measurement is not recognized, assume pt.
|
13
|
+
#
|
14
|
+
# Examples:
|
15
|
+
#
|
16
|
+
# 0.5in => 36.0
|
17
|
+
# 100px => 75.0
|
18
|
+
# 72blah => 72.0
|
19
|
+
#
|
20
|
+
def str_to_pt val
|
21
|
+
MeasurementValueRx =~ val ? (to_pt $1.to_f, $2) : val.to_f
|
22
|
+
end
|
23
|
+
|
24
|
+
# Converts the specified float value to a pt value from the
|
25
|
+
# specified unit of measurement (e.g., in, cm, mm, etc).
|
26
|
+
# Raises an argument error if the unit of measurement is not recognized.
|
27
|
+
def to_pt num, units
|
28
|
+
units = units.to_s if ::Symbol === units
|
29
|
+
if units.nil_or_empty?
|
30
|
+
num
|
31
|
+
else
|
32
|
+
case units
|
33
|
+
when 'pt'
|
34
|
+
num
|
35
|
+
when 'in'
|
36
|
+
num * 72
|
37
|
+
when 'mm'
|
38
|
+
num * (72 / 25.4)
|
39
|
+
when 'cm'
|
40
|
+
num * (720 / 25.4)
|
41
|
+
when 'px'
|
42
|
+
# assuming canvas of 96 dpi
|
43
|
+
num * 0.75
|
44
|
+
when 'pc'
|
45
|
+
num * 12
|
46
|
+
else
|
47
|
+
raise ::ArgumentError, %(unknown unit of measurement: #{units})
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Resolve measurement values in the string to PDF points.
|
53
|
+
def resolve_measurement_values str
|
54
|
+
if MeasurementValueHintRx.match? str
|
55
|
+
str.gsub(InsetMeasurementValueRx) { to_pt $1.to_f, $2 }
|
56
|
+
else
|
57
|
+
str
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'rghost'
|
5
|
+
require 'tmpdir'
|
6
|
+
|
7
|
+
module Asciidoctor
|
8
|
+
module PDF
|
9
|
+
class Optimizer
|
10
|
+
(QUALITY_NAMES = {
|
11
|
+
'default' => :default,
|
12
|
+
'screen' => :screen,
|
13
|
+
'ebook' => :ebook,
|
14
|
+
'printer' => :printer,
|
15
|
+
'prepress' => :prepress,
|
16
|
+
}).default = :default
|
17
|
+
|
18
|
+
def initialize quality = 'default', compatibility_level = '1.4'
|
19
|
+
@quality = QUALITY_NAMES[quality]
|
20
|
+
@compatibility_level = compatibility_level
|
21
|
+
end
|
22
|
+
|
23
|
+
def generate_file target
|
24
|
+
::Dir::Tmpname.create ['asciidoctor-pdf-', '.pdf'] do |tmpfile|
|
25
|
+
filename = Pathname.new target
|
26
|
+
filename_o = Pathname.new tmpfile
|
27
|
+
pdfmark = filename.sub_ext '.pdfmark'
|
28
|
+
inputs = pdfmark.file? ? [target, pdfmark.to_s] : target
|
29
|
+
(::RGhost::Convert.new inputs).to :pdf,
|
30
|
+
filename: filename_o.to_s,
|
31
|
+
quality: @quality,
|
32
|
+
d: { Printed: false, CannotEmbedFontPolicy: '/Warning', CompatibilityLevel: @compatibility_level }
|
33
|
+
begin
|
34
|
+
filename_o.rename target
|
35
|
+
rescue ::Errno::EXDEV
|
36
|
+
filename.binwrite filename_o.binread
|
37
|
+
filename_o.unlink
|
38
|
+
end
|
39
|
+
end
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Asciidoctor
|
4
|
+
module PDF
|
5
|
+
class Pdfmark
|
6
|
+
include ::Asciidoctor::PDF::Sanitizer
|
7
|
+
|
8
|
+
def initialize doc
|
9
|
+
@doc = doc
|
10
|
+
end
|
11
|
+
|
12
|
+
def generate
|
13
|
+
doc = @doc
|
14
|
+
if doc.attr? 'reproducible'
|
15
|
+
mod_date = creation_date = (::Time.at 0).utc
|
16
|
+
else
|
17
|
+
mod_date = (::Time.parse doc.attr 'docdatetime') rescue (now ||= ::Time.now)
|
18
|
+
creation_date = (::Time.parse doc.attr 'localdatetime') rescue (now || ::Time.now)
|
19
|
+
end
|
20
|
+
# FIXME: use sanitize: :plain_text once available
|
21
|
+
content = <<~EOS
|
22
|
+
[ /Title #{(sanitize doc.doctitle use_fallback: true).to_pdf_object}
|
23
|
+
/Author #{(doc.attr 'authors').to_pdf_object}
|
24
|
+
/Subject #{(doc.attr 'subject').to_pdf_object}
|
25
|
+
/Keywords #{(doc.attr 'keywords').to_pdf_object}
|
26
|
+
/ModDate #{mod_date.to_pdf_object}
|
27
|
+
/CreationDate #{creation_date.to_pdf_object}
|
28
|
+
/Creator (Asciidoctor PDF #{::Asciidoctor::PDF::VERSION}, based on Prawn #{::Prawn::VERSION})
|
29
|
+
/Producer #{(doc.attr 'publisher').to_pdf_object}
|
30
|
+
/DOCINFO pdfmark
|
31
|
+
EOS
|
32
|
+
content
|
33
|
+
end
|
34
|
+
|
35
|
+
def generate_file pdf_file
|
36
|
+
# QUESTION should we use the extension pdfmeta to be more clear?
|
37
|
+
::File.write %(#{pdf_file}mark), generate
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|