asciidoctor-pdf 1.5.0.alpha.16 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. checksums.yaml +5 -5
  2. data/.yardopts +12 -0
  3. data/CHANGELOG.adoc +415 -1
  4. data/LICENSE.adoc +1 -1
  5. data/NOTICE.adoc +14 -11
  6. data/README.adoc +647 -222
  7. data/asciidoctor-pdf.gemspec +47 -44
  8. data/bin/asciidoctor-pdf +5 -9
  9. data/bin/asciidoctor-pdf-optimize +20 -0
  10. data/data/fonts/ABOUT-mplus1mn-subset +26 -0
  11. data/data/fonts/ABOUT-mplus1p-subset +26 -0
  12. data/data/fonts/ABOUT-notoemoji-subset +3 -0
  13. data/data/fonts/ABOUT-notoserif-subset +26 -0
  14. data/data/fonts/{LICENSE-mplus-testflight-58 → LICENSE-mplus} +2 -2
  15. data/data/fonts/{LICENSE-noto-2015-06-05 → LICENSE-notoserif} +0 -0
  16. data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
  17. data/data/fonts/mplus1mn-bold-subset.ttf +0 -0
  18. data/data/fonts/mplus1mn-bold_italic-ascii.ttf +0 -0
  19. data/data/fonts/mplus1mn-bold_italic-subset.ttf +0 -0
  20. data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
  21. data/data/fonts/mplus1mn-italic-subset.ttf +0 -0
  22. data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
  23. data/data/fonts/mplus1mn-regular-subset.ttf +0 -0
  24. data/data/fonts/mplus1p-regular-fallback.ttf +0 -0
  25. data/data/fonts/notoemoji-subset.ttf +0 -0
  26. data/data/fonts/notoserif-bold-subset.ttf +0 -0
  27. data/data/fonts/notoserif-bold_italic-subset.ttf +0 -0
  28. data/data/fonts/notoserif-italic-subset.ttf +0 -0
  29. data/data/fonts/notoserif-regular-subset.ttf +0 -0
  30. data/data/themes/base-theme.yml +26 -4
  31. data/data/themes/default-theme.yml +76 -60
  32. data/data/themes/default-with-fallback-font-theme.yml +9 -0
  33. data/docs/theming-guide.adoc +2731 -922
  34. data/lib/asciidoctor/pdf/converter.rb +4489 -0
  35. data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_block.rb +7 -0
  36. data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_node.rb +7 -0
  37. data/lib/asciidoctor/pdf/ext/asciidoctor/document.rb +5 -0
  38. data/lib/asciidoctor/pdf/ext/asciidoctor/image.rb +35 -0
  39. data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/list.rb +4 -2
  40. data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/list_item.rb +3 -1
  41. data/lib/asciidoctor/pdf/ext/asciidoctor/logging_shim.rb +33 -0
  42. data/lib/asciidoctor/pdf/ext/asciidoctor/section.rb +45 -0
  43. data/lib/asciidoctor/pdf/ext/asciidoctor.rb +11 -0
  44. data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/array.rb +6 -6
  45. data/lib/asciidoctor/pdf/ext/core/file.rb +9 -0
  46. data/lib/asciidoctor/pdf/ext/core/hash.rb +7 -0
  47. data/lib/asciidoctor/pdf/ext/core/numeric.rb +26 -0
  48. data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/object.rb +3 -1
  49. data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/quantifiable_stdout.rb +9 -1
  50. data/lib/asciidoctor/pdf/ext/core/regexp.rb +5 -0
  51. data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/string.rb +9 -13
  52. data/lib/asciidoctor/pdf/ext/core.rb +10 -0
  53. data/lib/asciidoctor/pdf/ext/pdf-core/page.rb +54 -0
  54. data/lib/asciidoctor/pdf/ext/pdf-core/pdf_object.rb +8 -0
  55. data/lib/asciidoctor/pdf/ext/pdf-core.rb +4 -0
  56. data/lib/asciidoctor/pdf/ext/prawn/coderay_encoder.rb +117 -0
  57. data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +922 -0
  58. data/lib/{asciidoctor-pdf/prawn_ext → asciidoctor/pdf/ext/prawn}/font/afm.rb +14 -10
  59. data/lib/asciidoctor/pdf/ext/prawn/font_metric_cache.rb +9 -0
  60. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/box.rb +66 -0
  61. data/lib/{asciidoctor-pdf/prawn_ext → asciidoctor/pdf/ext/prawn}/formatted_text/fragment.rb +21 -18
  62. data/lib/asciidoctor/pdf/ext/prawn/images.rb +54 -0
  63. data/lib/asciidoctor/pdf/ext/prawn-svg/interface.rb +14 -0
  64. data/lib/asciidoctor/pdf/ext/prawn-svg.rb +6 -0
  65. data/lib/asciidoctor/pdf/ext/prawn-table/cell/asciidoc.rb +76 -0
  66. data/lib/{asciidoctor-pdf/prawn-table_ext → asciidoctor/pdf/ext/prawn-table}/cell/text.rb +6 -3
  67. data/lib/asciidoctor/pdf/ext/prawn-table/cell.rb +60 -0
  68. data/lib/asciidoctor/pdf/ext/prawn-table.rb +6 -0
  69. data/lib/{asciidoctor-pdf/prawn-templates_ext.rb → asciidoctor/pdf/ext/prawn-templates.rb} +2 -0
  70. data/lib/asciidoctor/pdf/ext/prawn.rb +9 -0
  71. data/lib/asciidoctor/pdf/ext/pygments.rb +34 -0
  72. data/lib/asciidoctor/pdf/ext/rouge/formatters/prawn.rb +208 -0
  73. data/lib/{asciidoctor-pdf/rouge_ext/themes/pastie.rb → asciidoctor/pdf/ext/rouge/themes/asciidoctor_pdf_default.rb} +7 -5
  74. data/lib/asciidoctor/pdf/ext/rouge.rb +5 -0
  75. data/lib/asciidoctor/pdf/ext.rb +9 -0
  76. data/lib/asciidoctor/pdf/formatted_text/formatter.rb +43 -0
  77. data/lib/asciidoctor/pdf/formatted_text/fragment_position_renderer.rb +14 -0
  78. data/lib/asciidoctor/pdf/formatted_text/inline_destination_marker.rb +21 -0
  79. data/lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb +134 -0
  80. data/lib/asciidoctor/pdf/formatted_text/inline_image_renderer.rb +51 -0
  81. data/lib/asciidoctor/pdf/formatted_text/inline_text_aligner.rb +22 -0
  82. data/lib/{asciidoctor-pdf → asciidoctor/pdf}/formatted_text/parser.rb +175 -53
  83. data/lib/{asciidoctor-pdf → asciidoctor/pdf}/formatted_text/parser.treetop +20 -14
  84. data/lib/asciidoctor/pdf/formatted_text/source_wrap.rb +43 -0
  85. data/lib/asciidoctor/pdf/formatted_text/text_background_and_border_renderer.rb +55 -0
  86. data/lib/asciidoctor/pdf/formatted_text/transform.rb +394 -0
  87. data/lib/{asciidoctor-pdf → asciidoctor/pdf}/formatted_text.rb +6 -0
  88. data/lib/asciidoctor/pdf/index_catalog.rb +133 -0
  89. data/lib/asciidoctor/pdf/measurements.rb +62 -0
  90. data/lib/asciidoctor/pdf/optimizer.rb +44 -0
  91. data/lib/asciidoctor/pdf/pdfmark.rb +41 -0
  92. data/lib/asciidoctor/pdf/roman_numeral.rb +128 -0
  93. data/lib/asciidoctor/pdf/sanitizer.rb +45 -0
  94. data/lib/asciidoctor/pdf/text_transformer.rb +116 -0
  95. data/lib/asciidoctor/pdf/theme_loader.rb +305 -0
  96. data/lib/asciidoctor/pdf/version.rb +8 -0
  97. data/lib/asciidoctor/pdf.rb +15 -0
  98. data/lib/asciidoctor-pdf/converter.rb +2 -3343
  99. data/lib/asciidoctor-pdf/version.rb +3 -5
  100. data/lib/asciidoctor-pdf.rb +3 -3
  101. metadata +242 -128
  102. data/Gemfile +0 -22
  103. data/Rakefile +0 -81
  104. data/lib/asciidoctor-pdf/asciidoctor_ext/image.rb +0 -24
  105. data/lib/asciidoctor-pdf/asciidoctor_ext/section.rb +0 -34
  106. data/lib/asciidoctor-pdf/asciidoctor_ext.rb +0 -5
  107. data/lib/asciidoctor-pdf/core_ext/numeric.rb +0 -15
  108. data/lib/asciidoctor-pdf/core_ext/ostruct.rb +0 -17
  109. data/lib/asciidoctor-pdf/core_ext.rb +0 -4
  110. data/lib/asciidoctor-pdf/formatted_text/formatter.rb +0 -27
  111. data/lib/asciidoctor-pdf/formatted_text/inline_destination_marker.rb +0 -21
  112. data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +0 -172
  113. data/lib/asciidoctor-pdf/formatted_text/inline_image_renderer.rb +0 -46
  114. data/lib/asciidoctor-pdf/formatted_text/transform.rb +0 -261
  115. data/lib/asciidoctor-pdf/implicit_header_processor.rb +0 -63
  116. data/lib/asciidoctor-pdf/index_catalog.rb +0 -119
  117. data/lib/asciidoctor-pdf/measurements.rb +0 -58
  118. data/lib/asciidoctor-pdf/pdf-core_ext/page.rb +0 -25
  119. data/lib/asciidoctor-pdf/pdf-core_ext/pdf_object.rb +0 -6
  120. data/lib/asciidoctor-pdf/pdf-core_ext.rb +0 -2
  121. data/lib/asciidoctor-pdf/pdfmark.rb +0 -33
  122. data/lib/asciidoctor-pdf/prawn-svg_ext/interface.rb +0 -10
  123. data/lib/asciidoctor-pdf/prawn-svg_ext.rb +0 -4
  124. data/lib/asciidoctor-pdf/prawn-table_ext/cell/asciidoc.rb +0 -69
  125. data/lib/asciidoctor-pdf/prawn-table_ext.rb +0 -3
  126. data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +0 -115
  127. data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +0 -863
  128. data/lib/asciidoctor-pdf/prawn_ext/images.rb +0 -40
  129. data/lib/asciidoctor-pdf/prawn_ext.rb +0 -5
  130. data/lib/asciidoctor-pdf/roman_numeral.rb +0 -114
  131. data/lib/asciidoctor-pdf/rouge_ext/css_theme.rb +0 -15
  132. data/lib/asciidoctor-pdf/rouge_ext/formatters/prawn.rb +0 -164
  133. data/lib/asciidoctor-pdf/rouge_ext.rb +0 -4
  134. data/lib/asciidoctor-pdf/sanitizer.rb +0 -88
  135. data/lib/asciidoctor-pdf/temporary_path.rb +0 -13
  136. data/lib/asciidoctor-pdf/theme_loader.rb +0 -247
  137. 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,8 +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'
12
+ require_relative 'formatted_text/inline_text_aligner'
13
+ require_relative 'formatted_text/source_wrap'
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