asciidoctor-pdf 1.6.2 → 2.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.adoc +224 -30
  4. data/NOTICE.adoc +16 -4
  5. data/README.adoc +207 -67
  6. data/asciidoctor-pdf.gemspec +3 -7
  7. data/data/fonts/ABOUT-mplus1mn-subset +1 -1
  8. data/data/fonts/ABOUT-mplus1p-subset +2 -2
  9. data/data/fonts/ABOUT-notosans-subset +26 -0
  10. data/data/fonts/ABOUT-notoserif-subset +1 -1
  11. data/data/fonts/{LICENSE-notoserif → LICENSE-noto} +0 -0
  12. data/data/fonts/mplus1mn-bold-subset.ttf +0 -0
  13. data/data/fonts/mplus1mn-bold_italic-subset.ttf +0 -0
  14. data/data/fonts/mplus1mn-italic-subset.ttf +0 -0
  15. data/data/fonts/mplus1mn-regular-subset.ttf +0 -0
  16. data/data/fonts/mplus1p-regular-fallback.ttf +0 -0
  17. data/data/fonts/notoemoji-subset.ttf +0 -0
  18. data/data/fonts/notosans-bold-subset.ttf +0 -0
  19. data/data/fonts/notosans-bold_italic-subset.ttf +0 -0
  20. data/data/fonts/notosans-italic-subset.ttf +0 -0
  21. data/data/fonts/notosans-regular-subset.ttf +0 -0
  22. data/data/fonts/notoserif-bold-subset.ttf +0 -0
  23. data/data/fonts/notoserif-bold_italic-subset.ttf +0 -0
  24. data/data/fonts/notoserif-italic-subset.ttf +0 -0
  25. data/data/fonts/notoserif-regular-subset.ttf +0 -0
  26. data/data/themes/base-theme.yml +18 -22
  27. data/data/themes/default-for-print-theme.yml +24 -0
  28. data/data/themes/default-for-print-with-fallback-font-theme.yml +3 -0
  29. data/data/themes/default-theme.yml +49 -54
  30. data/data/themes/default-with-fallback-font-theme.yml +2 -2
  31. data/data/themes/sans-with-fallback-font-theme.yml +10 -0
  32. data/docs/theming-guide.adoc +967 -344
  33. data/lib/asciidoctor/pdf/converter.rb +1691 -1478
  34. data/lib/asciidoctor/pdf/ext/asciidoctor/document.rb +18 -1
  35. data/lib/asciidoctor/pdf/ext/asciidoctor/image.rb +9 -15
  36. data/lib/asciidoctor/pdf/ext/asciidoctor/list.rb +6 -13
  37. data/lib/asciidoctor/pdf/ext/asciidoctor/section.rb +3 -16
  38. data/lib/asciidoctor/pdf/ext/asciidoctor.rb +1 -5
  39. data/lib/asciidoctor/pdf/ext/core/file.rb +1 -1
  40. data/lib/asciidoctor/pdf/ext/core/quantifiable_stdout.rb +1 -4
  41. data/lib/asciidoctor/pdf/ext/core/string.rb +2 -2
  42. data/lib/asciidoctor/pdf/ext/core.rb +1 -4
  43. data/lib/asciidoctor/pdf/ext/pdf-core/page.rb +8 -33
  44. data/lib/asciidoctor/pdf/ext/pdf-core.rb +0 -18
  45. data/lib/asciidoctor/pdf/ext/prawn/coderay_encoder.rb +5 -7
  46. data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +433 -329
  47. data/lib/asciidoctor/pdf/ext/prawn/font/afm.rb +0 -4
  48. data/lib/asciidoctor/pdf/ext/prawn/font_metric_cache.rb +1 -1
  49. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/arranger.rb +33 -3
  50. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/box.rb +20 -14
  51. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/fragment.rb +9 -3
  52. data/lib/asciidoctor/pdf/ext/prawn/images.rb +14 -16
  53. data/lib/asciidoctor/pdf/ext/prawn-svg/loaders/data.rb +6 -0
  54. data/lib/asciidoctor/pdf/ext/prawn-svg/loaders/web.rb +22 -0
  55. data/lib/asciidoctor/pdf/ext/prawn-svg/url_loader.rb +13 -0
  56. data/lib/asciidoctor/pdf/ext/prawn-svg.rb +5 -2
  57. data/lib/asciidoctor/pdf/ext/prawn-table/cell/asciidoc.rb +76 -20
  58. data/lib/asciidoctor/pdf/ext/prawn-table/cell/text.rb +39 -1
  59. data/lib/asciidoctor/pdf/ext/prawn-table/cell.rb +21 -15
  60. data/lib/asciidoctor/pdf/ext/prawn-table.rb +1 -1
  61. data/lib/asciidoctor/pdf/ext/pygments.rb +2 -2
  62. data/lib/asciidoctor/pdf/ext/rouge/formatters/prawn.rb +17 -20
  63. data/lib/asciidoctor/pdf/ext/rouge/themes/asciidoctor_pdf_default.rb +1 -0
  64. data/lib/asciidoctor/pdf/ext/rouge.rb +0 -1
  65. data/lib/asciidoctor/pdf/formatted_text/formatter.rb +2 -2
  66. data/lib/asciidoctor/pdf/formatted_text/inline_destination_marker.rb +8 -10
  67. data/lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb +69 -78
  68. data/lib/asciidoctor/pdf/formatted_text/inline_image_renderer.rb +7 -10
  69. data/lib/asciidoctor/pdf/formatted_text/inline_text_aligner.rb +2 -4
  70. data/lib/asciidoctor/pdf/formatted_text/parser.rb +53 -47
  71. data/lib/asciidoctor/pdf/formatted_text/parser.treetop +5 -7
  72. data/lib/asciidoctor/pdf/formatted_text/source_wrap.rb +14 -14
  73. data/lib/asciidoctor/pdf/formatted_text/text_background_and_border_renderer.rb +4 -7
  74. data/lib/asciidoctor/pdf/formatted_text/transform.rb +116 -109
  75. data/lib/asciidoctor/pdf/formatted_text.rb +0 -1
  76. data/lib/asciidoctor/pdf/index_catalog.rb +7 -11
  77. data/lib/asciidoctor/pdf/optimizer.rb +3 -5
  78. data/lib/asciidoctor/pdf/pdfmark.rb +16 -8
  79. data/lib/asciidoctor/pdf/roman_numeral.rb +4 -22
  80. data/lib/asciidoctor/pdf/sanitizer.rb +18 -13
  81. data/lib/asciidoctor/pdf/section_info_by_page.rb +24 -0
  82. data/lib/asciidoctor/pdf/theme_loader.rb +89 -79
  83. data/lib/asciidoctor/pdf/version.rb +1 -2
  84. data/lib/asciidoctor/pdf.rb +5 -2
  85. metadata +34 -64
  86. data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
  87. data/data/fonts/mplus1mn-bold_italic-ascii.ttf +0 -0
  88. data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
  89. data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
  90. data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_block.rb +0 -7
  91. data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_node.rb +0 -7
  92. data/lib/asciidoctor/pdf/ext/asciidoctor/list_item.rb +0 -18
  93. data/lib/asciidoctor/pdf/ext/asciidoctor/logging_shim.rb +0 -33
  94. data/lib/asciidoctor/pdf/ext/core/array.rb +0 -11
  95. data/lib/asciidoctor/pdf/ext/core/hash.rb +0 -7
  96. data/lib/asciidoctor/pdf/ext/core/regexp.rb +0 -5
  97. data/lib/asciidoctor/pdf/ext/pdf-core/pdf_object.rb +0 -8
  98. data/lib/asciidoctor-pdf/converter.rb +0 -3
  99. data/lib/asciidoctor-pdf/version.rb +0 -3
@@ -6,9 +6,9 @@ module Asciidoctor
6
6
  class Transform
7
7
  LF = ?\n
8
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}$/
9
+ CharEntityTable = { amp: '&', apos: ?', gt: '>', lt: '<', nbsp: ?\u00a0, quot: '"' }
10
+ CharRefRx = /&(?:(#{CharEntityTable.keys.join '|'})|#(?:(\d\d\d{0,4})|x(\h\h\h{0,3})));/
11
+ HexColorRx = /^#\h\h\h\h{0,3}$/
12
12
  TextDecorationTable = { 'underline' => :underline, 'line-through' => :strikethrough }
13
13
  ThemeKeyToFragmentProperty = {
14
14
  'background_color' => :background_color,
@@ -43,30 +43,30 @@ module Asciidoctor
43
43
  callback: button_bg_or_border && [TextBackgroundAndBorderRenderer],
44
44
  }.compact,
45
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,
46
+ color: theme.codespan_font_color,
47
+ font: theme.codespan_font_family,
48
+ size: theme.codespan_font_size,
49
+ styles: (to_styles theme.codespan_font_style),
50
+ background_color: (mono_bg_color = theme.codespan_background_color),
51
+ border_width: (mono_border_width = theme.codespan_border_width),
52
+ border_color: mono_border_width && (theme.codespan_border_color || theme.base_border_color),
53
+ border_offset: (mono_border_offset = (mono_bg_or_border = mono_bg_color || mono_border_width) && theme.codespan_border_offset),
54
+ border_radius: mono_bg_or_border && theme.codespan_border_radius,
55
55
  align: mono_border_offset && :center,
56
56
  callback: mono_bg_or_border && [TextBackgroundAndBorderRenderer],
57
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],
58
+ kbd: {
59
+ color: theme.kbd_font_color,
60
+ font: theme.kbd_font_family || theme.codespan_font_family,
61
+ size: theme.kbd_font_size,
62
+ styles: (to_styles theme.kbd_font_style),
63
+ background_color: (kbd_bg_color = theme.kbd_background_color),
64
+ border_width: (kbd_border_width = theme.kbd_border_width),
65
+ border_color: kbd_border_width && (theme.kbd_border_color || theme.base_border_color),
66
+ border_offset: (kbd_border_offset = (kbd_bg_or_border = kbd_bg_color || kbd_border_width) && theme.kbd_border_offset),
67
+ border_radius: kbd_bg_or_border && theme.kbd_border_radius,
68
+ align: kbd_border_offset && :center,
69
+ callback: kbd_bg_or_border && [TextBackgroundAndBorderRenderer],
70
70
  }.compact,
71
71
  link: {
72
72
  color: theme.link_font_color,
@@ -75,6 +75,10 @@ module Asciidoctor
75
75
  styles: (to_styles theme.link_font_style, theme.link_text_decoration),
76
76
  text_decoration_color: theme.link_text_decoration_color,
77
77
  text_decoration_width: theme.link_text_decoration_width,
78
+ background_color: (link_bg_color = theme.link_background_color),
79
+ border_offset: (link_border_offset = link_bg_color && theme.link_border_offset),
80
+ align: link_border_offset && :center,
81
+ callback: link_bg_color && [TextBackgroundAndBorderRenderer],
78
82
  }.compact,
79
83
  mark: {
80
84
  color: theme.mark_font_color,
@@ -84,6 +88,12 @@ module Asciidoctor
84
88
  align: mark_border_offset && :center,
85
89
  callback: mark_bg_color && [TextBackgroundAndBorderRenderer],
86
90
  }.compact,
91
+ menu: {
92
+ color: theme.menu_font_color,
93
+ font: theme.menu_font_family,
94
+ size: theme.menu_font_size,
95
+ styles: (to_styles theme.menu_font_style),
96
+ }.compact,
87
97
  }
88
98
  revise_roles = [].to_set
89
99
  theme.each_pair.each_with_object @theme_settings do |(key, val), accum|
@@ -106,14 +116,14 @@ module Asciidoctor
106
116
  @theme_settings['underline'] = { styles: [:underline].to_set } unless @theme_settings.key? 'underline'
107
117
  unless @theme_settings.key? 'big'
108
118
  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) }
119
+ @theme_settings['big'] = { size: %(#{(base_font_size_large / theme.base_font_size.to_f).round 5}em) }
110
120
  else
111
121
  @theme_settings['big'] = { size: '1.1667em' }
112
122
  end
113
123
  end
114
124
  unless @theme_settings.key? 'small'
115
125
  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) }
126
+ @theme_settings['small'] = { size: %(#{(base_font_size_small / theme.base_font_size.to_f).round 5}em) }
117
127
  else
118
128
  @theme_settings['small'] = { size: '0.8333em' }
119
129
  end
@@ -122,9 +132,10 @@ module Asciidoctor
122
132
  @theme_settings = {
123
133
  button: { font: 'Courier', styles: [:bold].to_set },
124
134
  code: { font: 'Courier' },
125
- key: { font: 'Courier', styles: [:italic].to_set },
135
+ kbd: { font: 'Courier', styles: [:italic].to_set },
126
136
  link: { color: '0000FF' },
127
137
  mark: { background_color: 'FFFF00', callback: [TextBackgroundAndBorderRenderer] },
138
+ menu: { styles: [:bold].to_set },
128
139
  'line-through' => { styles: [:strikethrough].to_set },
129
140
  'underline' => { styles: [:underline].to_set },
130
141
  'big' => { size: '1.667em' },
@@ -135,48 +146,53 @@ module Asciidoctor
135
146
 
136
147
  def apply parsed, fragments = [], inherited = nil
137
148
  previous_fragment_is_text = false
138
- # NOTE we use each since using inject is slower than a manual loop
149
+ # NOTE: we use each since using inject is slower than a manual loop
139
150
  parsed.each do |node|
140
151
  case node[:type]
141
152
  when :element
142
153
  # case 1: non-void element
143
154
  if node.key? :pcdata
144
- # NOTE skip element if it has no children
155
+ # NOTE: skip element if it has no children
145
156
  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
157
+ # QUESTION: should this be handled by the formatter after the transform is complete?
158
+ if previous_fragment_is_text && ((previous_fragment_text = fragments[-1][:text]).end_with? ' ')
159
+ fragments[-1][:text] = previous_fragment_text.chomp ' '
160
+ end
152
161
  else
153
162
  tag_name = node[:name]
154
163
  attributes = node[:attributes]
155
164
  parent = clone_fragment inherited
156
- # NOTE decorate child fragments with inherited properties from this element
165
+ # NOTE: decorate child fragments with inherited properties from this element
157
166
  apply pcdata, fragments, (build_fragment parent, tag_name, attributes)
158
167
  previous_fragment_is_text = false
159
168
  end
160
169
  # case 2: void element
161
170
  else
162
171
  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
172
  when :img
171
173
  attributes = node[:attributes]
172
174
  fragment = {
173
175
  image_path: attributes[:src],
174
176
  image_format: attributes[:format],
177
+ # a zero-width space in the text will cause the image to be duplicated
175
178
  # NOTE: add enclosing square brackets here to avoid errors in parsing
176
179
  text: %([#{attributes[:alt].delete ZeroWidthSpace}]),
177
- callback: [InlineImageRenderer],
178
180
  object_id: node.object_id, # used to deduplicate if fragment gets split up
179
181
  }
182
+ if inherited && (callback = inherited[:callback]) && (callback.include? TextBackgroundAndBorderRenderer)
183
+ # NOTE: if we keep InlineTextAligner, it needs to skip draw_text! for image fragment
184
+ fragment[:callback] = [TextBackgroundAndBorderRenderer, InlineImageRenderer]
185
+ fragment.update inherited.slice :border_color, :border_offset, :border_radius, :border_width, :background_color
186
+ else
187
+ fragment[:callback] = [InlineImageRenderer]
188
+ end
189
+ attributes[:class].split.each do |class_name|
190
+ next unless @theme_settings.key? class_name
191
+ update_fragment fragment, @theme_settings[class_name]
192
+ if fragment[:background_color] || (fragment[:border_color] && fragment[:border_width])
193
+ fragment[:callback] = [TextBackgroundAndBorderRenderer] | fragment[:callback]
194
+ end
195
+ end if attributes.key? :class
180
196
  if inherited && (link = inherited[:link])
181
197
  fragment[:link] = link
182
198
  end
@@ -188,15 +204,15 @@ module Asciidoctor
188
204
  end
189
205
  fragments << fragment
190
206
  previous_fragment_is_text = false
207
+ else # :br
208
+ if @merge_adjacent_text_nodes && previous_fragment_is_text
209
+ fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{LF}))
210
+ else
211
+ fragments << { text: LF }
212
+ end
213
+ previous_fragment_is_text = true
191
214
  end
192
215
  end
193
- when :text
194
- if @merge_adjacent_text_nodes && previous_fragment_is_text
195
- fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{node[:value]}))
196
- else
197
- fragments << (clone_fragment inherited, text: node[:value])
198
- end
199
- previous_fragment_is_text = true
200
216
  when :charref
201
217
  if (ref_type = node[:reference_type]) == :name
202
218
  text = CharEntityTable[node[:value]]
@@ -213,61 +229,54 @@ module Asciidoctor
213
229
  fragments << (clone_fragment inherited, text: text)
214
230
  end
215
231
  previous_fragment_is_text = true
232
+ else # :text
233
+ if @merge_adjacent_text_nodes && previous_fragment_is_text
234
+ fragments << (clone_fragment inherited, text: %(#{fragments.pop[:text]}#{node[:value]}))
235
+ else
236
+ fragments << (clone_fragment inherited, text: node[:value])
237
+ end
238
+ previous_fragment_is_text = true
216
239
  end
217
240
  end
218
241
  fragments
219
242
  end
220
243
 
221
- def build_fragment fragment, tag_name, attrs = {}
244
+ def build_fragment fragment, tag_name, attrs
222
245
  styles = (fragment[:styles] ||= ::Set.new)
223
246
  case tag_name
224
247
  when :strong
225
248
  styles << :bold
226
249
  when :em
227
250
  styles << :italic
228
- when :button, :code, :key, :mark
251
+ when :button, :code, :kbd, :mark, :menu
229
252
  update_fragment fragment, @theme_settings[tag_name]
230
- when :color
231
- if (rgb = attrs[:rgb])
232
- case rgb.chr
233
- when '#'
234
- fragment[:color] = rgb.slice 1, rgb.length
235
- when '['
236
- # treat value as CMYK array (e.g., "[50, 100, 0, 0]")
237
- fragment[:color] = rgb.slice(1, rgb.length).chomp(']').split(', ').map(&:to_i)
238
- # ...or we could honor an rgb array too
239
- #case (vals = rgb.slice(1, rgb.length).chomp(']').split(', ')).size
240
- #when 4
241
- # fragment[:color] = vals.map(&:to_i)
242
- #when 3
243
- # fragment[:color] = vals.map {|e| '%02X' % e.to_i }.join
244
- #end
245
- else
246
- fragment[:color] = rgb
247
- end
248
- # QUESTION should we even support r,g,b and c,m,y,k as individual values?
249
- elsif (r_val = attrs[:r]) && (g_val = attrs[:g]) && (b_val = attrs[:b])
250
- fragment[:color] = [r_val, g_val, b_val].map {|e| '%02X' % e.to_i }.join
251
- elsif (c_val = attrs[:c]) && (m_val = attrs[:m]) && (y_val = attrs[:y]) && (k_val = attrs[:k])
252
- fragment[:color] = [c_val.to_i, m_val.to_i, y_val.to_i, k_val.to_i]
253
- end
254
253
  when :font
255
254
  if (value = attrs[:name])
256
255
  fragment[:font] = value
257
256
  end
258
257
  if (value = attrs[:size])
259
- # FIXME: can we make this comparison more robust / accurate?
260
- if (f_value = value.to_f).to_s == value || value.to_i.to_s == value
261
- fragment[:size] = f_value
262
- elsif value != '1em'
263
- fragment[:size] = value
258
+ if value.end_with? 'em'
259
+ fragment[:size] = value unless value == '1em'
260
+ else
261
+ fragment[:size] = value.to_f
264
262
  end
265
263
  end
266
- # NOTE width is used for font-based icons
264
+ # NOTE: width is used for font-based icons
267
265
  if (value = attrs[:width])
268
266
  fragment[:width] = value
269
267
  fragment[:align] = :center
270
- fragment[:callback] = (fragment[:callback] || []) | [InlineTextAligner]
268
+ end
269
+ if (value = attrs[:color])
270
+ case value.chr
271
+ when '#' # hex string (e.g., #FF0000)
272
+ fragment[:color] = value.length == 7 ? (value.slice 1, 6) : (value.slice 1, 3).each_char.map {|c| c * 2 }.join if HexColorRx.match? value
273
+ when '[' # CMYK array (e.g., [50, 100, 0, 0])
274
+ fragment[:color] = ((((value.slice 1, value.length).chomp ']').split ', ', 4).each_with_object ::Array.new 4, 0).with_index do |(it, accum), idx|
275
+ accum[idx] = (ival = it.to_i) == (fval = it.to_f) ? ival : fval
276
+ end
277
+ else # assume a 6-character hex color (internal only)
278
+ fragment[:color] = value
279
+ end
271
280
  end
272
281
  #if (value = attrs[:character_spacing])
273
282
  # fragment[:character_spacing] = value.to_f
@@ -276,15 +285,15 @@ module Asciidoctor
276
285
  visible = true
277
286
  # a element can have no attributes, so short-circuit if that's the case
278
287
  unless attrs.empty?
279
- # NOTE href, anchor, and name are mutually exclusive; nesting is not supported
288
+ # NOTE: href, anchor, and name are mutually exclusive; nesting is not supported
280
289
  if (value = attrs[:anchor])
281
290
  fragment[:anchor] = value
282
291
  elsif (value = attrs[:href])
283
292
  fragment[:link] = (value.include? ';') ? (value.gsub CharRefRx do
284
293
  $1 ? CharEntityTable[$1.to_sym] : [$2 ? $2.to_i : ($3.to_i 16)].pack('U1')
285
294
  end) : value
286
- elsif (value = attrs[:id] || attrs[:name])
287
- # NOTE text is null character, which is used as placeholder text so Prawn doesn't drop fragment
295
+ elsif (value = attrs[:id])
296
+ # NOTE: text is null character, which is used as placeholder text so Prawn doesn't drop fragment
288
297
  fragment = { name: value, callback: [InlineDestinationMarker] }
289
298
  if (type = attrs[:type])
290
299
  fragment[:type] = type.to_sym
@@ -299,39 +308,29 @@ module Asciidoctor
299
308
  styles << :superscript
300
309
  when :del
301
310
  styles << :strikethrough
302
- when :span
303
- # NOTE spaces in style value are superfluous for our purpose; split drops record after trailing ;
311
+ else # :span
312
+ # NOTE: spaces in style value are superfluous for our purpose; split drops record after trailing ;
304
313
  attrs[:style].tr(' ', '').split(';').each do |style|
305
314
  pname, pvalue = style.split ':', 2
306
315
  # TODO: text-transform
307
316
  case pname
308
- when 'color'
309
- # TODO: check whether the value is a valid hex color?
310
- case pvalue.length
311
- when 6
312
- fragment[:color] = pvalue
313
- when 7
314
- fragment[:color] = pvalue.slice 1, 6 if pvalue.start_with? '#'
315
- end
316
- # QUESTION should we support the 3 character form?
317
- #when 3
318
- # fragment[:color] = pvalue.each_char.map {|c| c * 2 }.join
319
- #when 4
320
- # fragment[:color] = pvalue.slice(1, 3).each_char.map {|c| c * 2 }.join if pvalue.start_with?('#')
317
+ when 'color' # color needed to support syntax highlighters
318
+ fragment[:color] = pvalue.length == 7 ? (pvalue.slice 1, 6) : (pvalue.slice 1, 3).each_char.map {|c| c * 2 }.join if (pvalue.start_with? '#') && (HexColorRx.match? pvalue)
321
319
  when 'font-weight'
322
320
  styles << :bold if pvalue == 'bold'
323
321
  when 'font-style'
324
322
  styles << :italic if pvalue == 'italic'
325
323
  when 'align', 'text-align'
326
324
  fragment[:align] = pvalue.to_sym
327
- fragment[:callback] = (fragment[:callback] || []) | [InlineTextAligner]
328
325
  when 'width'
329
- # NOTE implicitly activates inline-block behavior
326
+ # NOTE: implicitly activates inline-block behavior
330
327
  fragment[:width] = pvalue
331
328
  when 'background-color' # background-color needed to support syntax highlighters
332
329
  if (pvalue.start_with? '#') && (HexColorRx.match? pvalue)
333
- fragment[:background_color] = pvalue.slice 1, pvalue.length
334
- fragment[:callback] = [TextBackgroundAndBorderRenderer] | (fragment[:callback] || [])
330
+ fragment[:background_color] = pvalue.length == 7 ? (pvalue.slice 1, 6) : (pvalue.slice 1, 3).each_char.map {|c| c * 2 }.join
331
+ # Q: is it possible that callback would already be set?
332
+ #fragment[:callback] = [TextBackgroundAndBorderRenderer] | (fragment[:callback] || [])
333
+ fragment[:callback] = [TextBackgroundAndBorderRenderer]
335
334
  end
336
335
  end
337
336
  end if attrs.key? :style
@@ -340,6 +339,7 @@ module Asciidoctor
340
339
  attrs[:class].split.each do |class_name|
341
340
  next unless @theme_settings.key? class_name
342
341
  update_fragment fragment, @theme_settings[class_name]
342
+ # NOTE: defer assignment of callback since we must look at combined styles of element and role
343
343
  if fragment[:background_color] || (fragment[:border_color] && fragment[:border_width])
344
344
  fragment[:callback] = [TextBackgroundAndBorderRenderer] | (fragment[:callback] || [])
345
345
  fragment[:align] = :center if fragment[:border_offset]
@@ -370,6 +370,8 @@ module Asciidoctor
370
370
  styles = [:italic].to_set
371
371
  when 'bold_italic'
372
372
  styles = [:bold, :italic].to_set
373
+ when 'normal_italic'
374
+ styles = [:normal, :italic].to_set
373
375
  end
374
376
  if (style = TextDecorationTable[text_decoration])
375
377
  styles ? (styles << style) : [style].to_set
@@ -382,9 +384,14 @@ module Asciidoctor
382
384
  fragment.update props do |k, oval, nval|
383
385
  case k
384
386
  when :styles
385
- nval ? (oval.merge nval) : oval.clear
386
- when :callback
387
- oval | nval
387
+ if nval
388
+ oval.subtract [:bold, :italic] if nval.delete? :normal
389
+ oval.merge nval
390
+ else
391
+ oval.clear
392
+ end
393
+ #when :callback
394
+ # oval | nval
388
395
  else
389
396
  nval
390
397
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'treetop'
4
- require 'set' unless defined? Set
5
4
  require_relative 'formatted_text/parser'
6
5
  require_relative 'formatted_text/transform'
7
6
  require_relative 'formatted_text/formatter'
@@ -20,7 +20,7 @@ module Asciidoctor
20
20
  %(__indexterm-#{@sequence += 1})
21
21
  end
22
22
 
23
- def store_term names, dest = nil
23
+ def store_term names, dest
24
24
  if (num_terms = names.size) > 2
25
25
  store_tertiary_term names[0], names[1], names[2], dest
26
26
  elsif num_terms == 2
@@ -40,8 +40,8 @@ module Asciidoctor
40
40
  (store_primary_term primary_name).store_term secondary_name, dest
41
41
  end
42
42
 
43
- def store_tertiary_term primary_name, secondary_name, tertiary_name, dest = nil
44
- store_dest dest if dest
43
+ def store_tertiary_term primary_name, secondary_name, tertiary_name, dest
44
+ store_dest dest
45
45
  (store_secondary_term primary_name, secondary_name).store_term tertiary_name, dest
46
46
  end
47
47
 
@@ -60,7 +60,7 @@ module Asciidoctor
60
60
 
61
61
  def link_dest_to_page anchor, physical_page_number
62
62
  if (dest = @dests[anchor])
63
- virtual_page_number = physical_page_number - (@start_page_number - 1)
63
+ virtual_page_number = (dest[:page_sortable] = physical_page_number) - (@start_page_number - 1)
64
64
  dest[:page] = (virtual_page_number < 1 ? (RomanNumeral.new physical_page_number, :lower) : virtual_page_number).to_s
65
65
  end
66
66
  end
@@ -70,7 +70,7 @@ module Asciidoctor
70
70
  end
71
71
 
72
72
  def categories
73
- @categories.empty? ? [] : @categories.values.sort
73
+ @categories.values.sort
74
74
  end
75
75
  end
76
76
 
@@ -83,16 +83,12 @@ module Asciidoctor
83
83
  @terms = {}
84
84
  end
85
85
 
86
- def store_term name, dest = nil
86
+ def store_term name, dest
87
87
  term = (@terms[name] ||= (IndexTerm.new name))
88
88
  term.add_dest dest if dest
89
89
  term
90
90
  end
91
91
 
92
- def find_term name
93
- @terms[name]
94
- end
95
-
96
92
  def terms
97
93
  @terms.empty? ? [] : @terms.values.sort
98
94
  end
@@ -118,7 +114,7 @@ module Asciidoctor
118
114
  end
119
115
 
120
116
  def dests
121
- @dests.select {|d| d.key? :page }.sort {|a, b| a[:page] <=> b[:page] }
117
+ @dests.select {|d| d.key? :page }.sort_by {|d| d[:page_sortable] }
122
118
  end
123
119
 
124
120
  def container?
@@ -6,8 +6,6 @@ require 'rghost/gs_alone'
6
6
  require 'tmpdir'
7
7
 
8
8
  RGhost::GSAlone.prepend (Module.new do
9
- WindowsRx = /win|ming/
10
-
11
9
  def initialize params, debug
12
10
  (@params = params.dup).push(*(@params.pop.split File::PATH_SEPARATOR))
13
11
  @debug = debug
@@ -60,9 +58,9 @@ module Asciidoctor
60
58
  inputs = target
61
59
  end
62
60
  (::RGhost::Convert.new inputs).to :pdf,
63
- filename: filename_tmp.to_s,
64
- quality: @quality,
65
- d: { Printed: false, CannotEmbedFontPolicy: '/Warning', CompatibilityLevel: @compatibility_level }
61
+ filename: filename_tmp.to_s,
62
+ quality: @quality,
63
+ d: { Printed: false, CannotEmbedFontPolicy: '/Warning', CompatibilityLevel: @compatibility_level }
66
64
  filename_o.binwrite filename_tmp.binread
67
65
  end
68
66
  nil
@@ -17,23 +17,31 @@ module Asciidoctor
17
17
  mod_date = (::Time.parse doc.attr 'docdatetime') rescue (now ||= ::Time.now)
18
18
  creation_date = (::Time.parse doc.attr 'localdatetime') rescue (now || ::Time.now)
19
19
  end
20
- # FIXME: use sanitize: :plain_text once available
20
+ if (doc.attribute_locked? 'author') && !(doc.attribute_locked? 'authors')
21
+ author = sanitize doc.attr 'author'
22
+ elsif doc.attr? 'authors'
23
+ author = sanitize doc.attr 'authors'
24
+ elsif doc.attr? 'author' # rubocop:disable Lint/DuplicateBranch
25
+ author = sanitize doc.attr 'author'
26
+ end
27
+ # see https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf
21
28
  <<~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}
29
+ [ /Title #{(sanitize doc.header? ? doc.doctitle : (doc.attr 'untitled-label')).to_pdf_object}
30
+ /Author #{author.to_pdf_object}
31
+ /Subject #{((doc.attr? 'subject') ? (sanitize doc.attr 'subject') : nil).to_pdf_object}
32
+ /Keywords #{((doc.attr? 'keywords') ? (sanitize doc.attr 'keywords') : nil).to_pdf_object}
26
33
  /ModDate #{mod_date.to_pdf_object}
27
34
  /CreationDate #{creation_date.to_pdf_object}
28
35
  /Creator (Asciidoctor PDF #{::Asciidoctor::PDF::VERSION}, based on Prawn #{::Prawn::VERSION})
29
- /Producer #{(doc.attr 'publisher').to_pdf_object}
36
+ /Producer #{((doc.attr? 'publisher') ? (sanitize doc.attr 'publisher') : nil).to_pdf_object}
30
37
  /DOCINFO pdfmark
31
38
  EOS
32
39
  end
33
40
 
34
41
  def generate_file pdf_file
35
- # QUESTION should we use the extension pdfmeta to be more clear?
36
- ::File.write %(#{pdf_file}mark), generate
42
+ # QUESTION: should we use the extension pdfmeta to be more clear?
43
+ ::File.write (pdfmark_file = %(#{pdf_file}mark)), generate
44
+ pdfmark_file
37
45
  end
38
46
  end
39
47
  end
@@ -48,15 +48,9 @@ module Asciidoctor
48
48
  1000 => 'M',
49
49
  }
50
50
 
51
- def initialize initial_value, letter_case = nil
52
- initial_value ||= 1
53
- if ::Integer === initial_value
54
- @integer_value = initial_value
55
- else
56
- @integer_value = RomanNumeral.roman_to_int initial_value
57
- letter_case = :lower if letter_case.nil? && initial_value.upcase != initial_value
58
- end
59
- @letter_case = letter_case.nil? ? :upper : letter_case
51
+ def initialize initial_value, letter_case
52
+ @integer_value = ::Integer === initial_value ? initial_value : (RomanNumeral.roman_to_int initial_value)
53
+ @letter_case = letter_case
60
54
  end
61
55
 
62
56
  def to_s
@@ -71,18 +65,6 @@ module Asciidoctor
71
65
  @letter_case == :lower ? roman.downcase : roman
72
66
  end
73
67
 
74
- def to_i
75
- @integer_value
76
- end
77
-
78
- def odd?
79
- to_i.odd?
80
- end
81
-
82
- def even?
83
- to_i.even?
84
- end
85
-
86
68
  def next
87
69
  RomanNumeral.new @integer_value + 1, @letter_case
88
70
  end
@@ -96,7 +78,7 @@ module Asciidoctor
96
78
  RomanNumeral.new @integer_value - 1, @letter_case
97
79
  end
98
80
 
99
- def empty?
81
+ def nil_or_empty?
100
82
  false
101
83
  end
102
84
 
@@ -4,29 +4,30 @@ module Asciidoctor
4
4
  module PDF
5
5
  module Sanitizer
6
6
  XMLSpecialChars = {
7
- '&lt;' => ?<,
8
- '&gt;' => ?>,
9
- '&amp;' => ?&,
7
+ '&lt;' => '<',
8
+ '&gt;' => '>',
9
+ '&amp;' => '&',
10
10
  }
11
- XMLSpecialCharsRx = /(?:#{XMLSpecialChars.keys.join ?|})/
11
+ XMLSpecialCharsRx = /(?:#{XMLSpecialChars.keys.join '|'})/
12
12
  InverseXMLSpecialChars = XMLSpecialChars.invert
13
13
  InverseXMLSpecialCharsRx = /[#{InverseXMLSpecialChars.keys.join}]/
14
14
  (BuiltInNamedEntities = {
15
- 'amp' => ?&,
15
+ 'amp' => '&',
16
16
  'apos' => ?',
17
- 'gt' => ?>,
18
- 'lt' => ?<,
17
+ 'gt' => '>',
18
+ 'lt' => '<',
19
19
  'nbsp' => ' ',
20
- 'quot' => ?",
21
- }).default = ??
20
+ 'quot' => '"',
21
+ }).default = '?'
22
22
  SanitizeXMLRx = /<[^>]+>/
23
- CharRefRx = /&(?:([a-z][a-z]+\d{0,2})|#(?:(\d\d\d{0,4})|x([a-f\d][a-f\d][a-f\d]{0,3})));/
23
+ CharRefRx = /&(?:amp;)?(?:([a-z][a-z]+\d{0,2})|#(?:(\d\d\d{0,4})|x(\h\h\h{0,3})));/
24
+ UnescapedAmpersandRx = /&(?!(?:[a-z][a-z]+\d{0,2}|#(?:\d\d\d{0,4}|x\h\h\h{0,3}));)/
24
25
 
25
26
  # Strip leading, trailing and repeating whitespace, remove XML tags and
26
27
  # resolve all entities in the specified string.
27
28
  #
28
- # FIXME move to a module so we can mix it in elsewhere
29
- # FIXME add option to control escaping entities, or a filter mechanism in general
29
+ # FIXME: move to a module so we can mix it in elsewhere
30
+ # FIXME: add option to control escaping entities, or a filter mechanism in general
30
31
  def sanitize string
31
32
  string = string.gsub SanitizeXMLRx, '' if string.include? '<'
32
33
  string = string.gsub(CharRefRx) { $1 ? BuiltInNamedEntities[$1] : ([$2 ? $2.to_i : ($3.to_i 16)].pack 'U1') } if string.include? '&'
@@ -37,8 +38,12 @@ module Asciidoctor
37
38
  string.gsub InverseXMLSpecialCharsRx, InverseXMLSpecialChars
38
39
  end
39
40
 
41
+ def escape_amp string
42
+ string.gsub UnescapedAmpersandRx, '&amp;'
43
+ end
44
+
40
45
  def encode_quotes string
41
- (string.include? ?") ? (string.gsub ?", '&quot;') : string
46
+ (string.include? '"') ? (string.gsub '"', '&quot;') : string
42
47
  end
43
48
  end
44
49
  end