asciidoctor-pdf 1.5.0.alpha.12 → 1.5.0.alpha.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +325 -0
  3. data/Gemfile +1 -1
  4. data/README.adoc +97 -43
  5. data/asciidoctor-pdf.gemspec +7 -6
  6. data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
  7. data/data/fonts/mplus1mn-bold_italic-ascii.ttf +0 -0
  8. data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
  9. data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
  10. data/data/fonts/notoserif-bold-subset.ttf +0 -0
  11. data/data/fonts/notoserif-bold_italic-subset.ttf +0 -0
  12. data/data/fonts/notoserif-italic-subset.ttf +0 -0
  13. data/data/fonts/notoserif-regular-subset.ttf +0 -0
  14. data/data/themes/default-theme.yml +22 -13
  15. data/docs/theming-guide.adoc +276 -110
  16. data/lib/asciidoctor-pdf/asciidoctor_ext/image.rb +7 -7
  17. data/lib/asciidoctor-pdf/asciidoctor_ext/list.rb +24 -1
  18. data/lib/asciidoctor-pdf/asciidoctor_ext/list_item.rb +3 -3
  19. data/lib/asciidoctor-pdf/asciidoctor_ext/section.rb +11 -3
  20. data/lib/asciidoctor-pdf/converter.rb +614 -401
  21. data/lib/asciidoctor-pdf/core_ext.rb +1 -0
  22. data/lib/asciidoctor-pdf/core_ext/array.rb +2 -2
  23. data/lib/asciidoctor-pdf/core_ext/numeric.rb +1 -1
  24. data/lib/asciidoctor-pdf/core_ext/ostruct.rb +10 -2
  25. data/lib/asciidoctor-pdf/core_ext/string.rb +11 -0
  26. data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +2 -2
  27. data/lib/asciidoctor-pdf/formatted_text/inline_image_renderer.rb +5 -5
  28. data/lib/asciidoctor-pdf/formatted_text/transform.rb +6 -4
  29. data/lib/asciidoctor-pdf/implicit_header_processor.rb +1 -1
  30. data/lib/asciidoctor-pdf/pdf_core_ext/page.rb +1 -1
  31. data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +3 -3
  32. data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +77 -28
  33. data/lib/asciidoctor-pdf/prawn_ext/font/afm.rb +5 -4
  34. data/lib/asciidoctor-pdf/prawn_ext/images.rb +1 -1
  35. data/lib/asciidoctor-pdf/roman_numeral.rb +11 -4
  36. data/lib/asciidoctor-pdf/rouge_ext/formatters/prawn.rb +9 -9
  37. data/lib/asciidoctor-pdf/theme_loader.rb +16 -3
  38. data/lib/asciidoctor-pdf/version.rb +1 -1
  39. metadata +18 -9
@@ -1,18 +1,18 @@
1
1
  module Asciidoctor
2
2
  module Image
3
3
  class << self
4
- def image_type path
5
- (::File.extname path).downcase[1..-1]
4
+ def format path, node = nil
5
+ (node && (node.attr 'format', nil, false)) || (::File.extname path).downcase[1..-1]
6
6
  end
7
7
  end
8
8
 
9
- def image_type
10
- ::File.extname(inline? ? target : (attr 'target')).downcase[1..-1]
9
+ def format
10
+ (attr 'format', nil, false) || ::File.extname(inline? ? target : (attr 'target')).downcase[1..-1]
11
11
  end
12
12
 
13
- def target_with_image_type
14
- image_path = inline? ? (target) : (attr 'target')
15
- [image_path, (::File.extname image_path).downcase[1..-1]]
13
+ def target_and_format
14
+ image_path = inline? ? target : (attr 'target')
15
+ [image_path, (attr 'format', nil, false) || (::File.extname image_path).downcase[1..-1]]
16
16
  end
17
17
  end
18
18
  end
@@ -5,5 +5,28 @@ class Asciidoctor::List
5
5
  # Return true if this list is an outline list. Otherwise, return false.
6
6
  def outline?
7
7
  @context == :ulist || @context == :olist
8
- end unless respond_to? :outline?
8
+ end unless method_defined? :outline?
9
+
10
+ # Check whether this list is nested inside the item of another list.
11
+ #
12
+ # Return true if the parent of this list is a list item. Otherwise, return false.
13
+ def nested?
14
+ Asciidoctor::ListItem === @parent
15
+ end unless method_defined? :nested?
16
+
17
+ # Get the level of this list within the broader outline list (unordered or ordered) structure.
18
+ #
19
+ # This method differs from the level property in that it considers all outline list ancestors.
20
+ # It's important for selecting the marker for an unordered list.
21
+ #
22
+ # Return the 1-based level of this list within the outline list structure.
23
+ def outline_level
24
+ l = 1
25
+ ancestor = self
26
+ # FIXME does not cross out of AsciiDoc table cell
27
+ while (ancestor = ancestor.parent)
28
+ l += 1 if Asciidoctor::List === ancestor && ancestor.outline?
29
+ end
30
+ l
31
+ end unless method_defined? :outline_level
9
32
  end
@@ -5,12 +5,12 @@ class Asciidoctor::ListItem
5
5
  # Return false if the list item contains no blocks or it contains a nested outline list. Otherwise, return true.
6
6
  def complex?
7
7
  !simple?
8
- end unless respond_to? :complex?
8
+ end unless method_defined? :complex?
9
9
 
10
10
  # Check whether this list item has simple content (i.e., no nested blocks aside from an outline list).
11
11
  #
12
12
  # Return true if the list item contains no blocks or it contains a nested outline list. Otherwise, return false.
13
13
  def simple?
14
- @blocks.empty? || (@blocks.size == 1 && ::Asciidoctor::List === (blk = @blocks[0]) && blk.outline?)
15
- end unless respond_to? :simple?
14
+ @blocks.empty? || (@blocks.size == 1 && Asciidoctor::List === (blk = @blocks[0]) && blk.outline?)
15
+ end unless method_defined? :simple?
16
16
  end
@@ -18,9 +18,17 @@ class Asciidoctor::Section
18
18
  end
19
19
  end
20
20
  opts[:formal] ? @cached_formal_numbered_title : @cached_numbered_title
21
- end unless respond_to? :numbered_title
21
+ end unless method_defined? :numbered_title
22
+
23
+ def part?
24
+ @document.doctype == 'book' && @level == 0 && !@special
25
+ end unless method_defined? :part?
22
26
 
23
27
  def chapter?
24
- @document.doctype == 'book' && @level == 1 || (@special && @level == 0)
25
- end
28
+ @document.doctype == 'book' && (@level == 1 || (@special && @level == 0))
29
+ end unless method_defined? :chapter?
30
+
31
+ def part_or_chapter?
32
+ @document.doctype == 'book' && @level < 2
33
+ end unless method_defined? :part_or_chapter?
26
34
  end
@@ -38,9 +38,12 @@ class Converter < ::Prawn::Document
38
38
  tip: { name: 'fa-lightbulb-o', stroke_color: '111111', size: 24 },
39
39
  warning: { name: 'fa-exclamation-triangle', stroke_color: 'BF6900', size: 24 }
40
40
  }
41
- Alignments = [:left, :center, :right]
42
41
  AlignmentNames = ['left', 'center', 'right']
43
- EOL = %(\n)
42
+ AlignmentTable = { '<' => :left, '=' => :center, '>' => :right }
43
+ ColumnPositions = [:left, :center, :right]
44
+ PageSides = [:recto, :verso]
45
+ LF = %(\n)
46
+ DoubleLF = %(\n\n)
44
47
  TAB = %(\t)
45
48
  InnerIndent = %(\n )
46
49
  # a no-break space is used to replace a leading space to prevent Prawn from trimming indentation
@@ -67,14 +70,18 @@ class Converter < ::Prawn::Document
67
70
  checked: %(\u2611),
68
71
  unchecked: %(\u2610)
69
72
  }
70
- MeasurementRxt = '\\d+(?:\\.\\d+)?(?:in|cm|mm|pt|)'
71
- MeasurementPartsRx = /^(\d+(?:\.\d+)?)(in|mm|cm|pt|)$/
73
+ SimpleAttributeRefRx = /(?<!\\)\{\w+(?:[\-]\w+)*\}/
74
+ MeasurementRxt = '\\d+(?:\\.\\d+)?(?:in|cm|mm|pt|px)?'
75
+ MeasurementPartsRx = /^(\d+(?:\.\d+)?)(in|mm|cm|pt|px)?$/
72
76
  PageSizeRx = /^(?:\[(#{MeasurementRxt}), ?(#{MeasurementRxt})\]|(#{MeasurementRxt})(?: x |x)(#{MeasurementRxt})|\S+)$/
73
77
  # CalloutExtractRx synced from /lib/asciidoctor.rb of Asciidoctor core
74
- CalloutExtractRx = /(?:(?:\/\/|#|--|;;) ?)?(\\)?<!?(--|)(\d+)\2>(?=(?: ?\\?<!?\2\d+\2>)*$)/
78
+ CalloutExtractRx = /(?:(?:\/\/|#|--|;;) ?)?(\\)?<!?(--|)(\d+)\2> ?(?=(?:\\?<!?\2\d+\2> ?)*$)/
75
79
  ImageAttributeValueRx = /^image:{1,2}(.*?)\[(.*?)\]$/
76
80
  LineScanRx = /\n|.+/
81
+ BlankLineRx = /\n[[:blank:]]*\n/
82
+ WhitespaceChars = %( \t\n)
77
83
  SourceHighlighters = ['coderay', 'pygments', 'rouge'].to_set
84
+ ViewportWidth = ::Module.new
78
85
 
79
86
  def initialize backend, opts
80
87
  super
@@ -127,8 +134,13 @@ class Converter < ::Prawn::Document
127
134
  end
128
135
  #assign_missing_section_ids doc
129
136
 
130
- # NOTE the on_page_create callback is called within a float context
137
+ # NOTE on_page_create is called within a float context
138
+ # NOTE on_page_create is not called for imported pages, front and back cover pages, and other image pages
131
139
  on_page_create do
140
+ # NOTE we assume here that physical page number reflects page side
141
+ if @media == 'prepress' && (next_page_margin = @page_margin_by_side[page_side]) != page_margin
142
+ set_page_margin next_page_margin
143
+ end
132
144
  # TODO implement as a watermark (on top)
133
145
  if @page_bg_image
134
146
  # FIXME implement fitting and centering for SVG
@@ -145,31 +157,30 @@ class Converter < ::Prawn::Document
145
157
  # NOTE a new page will already be started if the cover image is a PDF
146
158
  start_new_page unless page_is_empty?
147
159
 
148
- toc_start_page_num = page_number
149
160
  num_toc_levels = (doc.attr 'toclevels', 2).to_i
150
161
  if (include_toc = doc.attr? 'toc')
151
- toc_page_nums = ()
152
- dry_run do
153
- toc_page_nums = layout_toc doc, num_toc_levels, 1
154
- end
155
- # NOTE reserve pages for the toc
156
- toc_page_nums.each do
157
- start_new_page
158
- end
162
+ start_new_page if @ppbook && verso_page?
163
+ toc_page_nums = page_number
164
+ dry_run { toc_page_nums = layout_toc doc, num_toc_levels, toc_page_nums }
165
+ # NOTE reserve pages for the toc; leaves cursor on page after last page in toc
166
+ toc_page_nums.each { start_new_page }
159
167
  end
160
168
 
169
+ # FIXME only apply to book doctype once title and toc are moved to start page when using article doctype
170
+ #start_new_page if @ppbook && verso_page?
171
+ start_new_page if @media == 'prepress' && verso_page?
172
+
161
173
  num_front_matter_pages = page_number - 1
162
174
  font @theme.base_font_family, size: @theme.base_font_size, style: @theme.base_font_style.to_sym
175
+ doc.set_attr 'pdf-anchor', (doc_anchor = derive_anchor_from_id doc.id, 'top')
176
+ add_dest_for_block doc, doc_anchor
163
177
  convert_content_for_block doc
164
178
 
165
179
  # NOTE delete orphaned page (a page was created but there was no additional content)
166
- delete_page if page_is_empty?
180
+ # QUESTION should we delete page if document is empty? (leaving no pages?)
181
+ delete_page if page_is_empty? && page_count > 1
167
182
 
168
- toc_page_nums = if include_toc
169
- layout_toc doc, num_toc_levels, toc_start_page_num, num_front_matter_pages
170
- else
171
- (0..-1)
172
- end
183
+ toc_page_nums = include_toc ? (layout_toc doc, num_toc_levels, toc_page_nums.first, num_front_matter_pages) : []
173
184
 
174
185
  if page_count > num_front_matter_pages
175
186
  layout_running_content :header, doc, skip: num_front_matter_pages unless doc.noheader
@@ -192,23 +203,37 @@ class Converter < ::Prawn::Document
192
203
 
193
204
  # TODO only allow method to be called once (or we need a reset)
194
205
  def init_pdf doc
195
- theme = ThemeLoader.load_theme doc.attr('pdf-style'), doc.attr('pdf-stylesdir')
196
- @theme = theme
197
- pdf_opts = (build_pdf_options doc, theme)
198
- # QUESTION should we preserve page options (otherwise, not readily available)
206
+ @theme = theme = ThemeLoader.load_theme((doc.attr 'pdf-style'), (doc.attr 'pdf-stylesdir'))
207
+ pdf_opts = build_pdf_options doc, theme
208
+ # QUESTION should page options be preserved (otherwise, not readily available)
199
209
  #@page_opts = { size: pdf_opts[:page_size], layout: pdf_opts[:page_layout] }
200
210
  ::Prawn::Document.instance_method(:initialize).bind(self).call pdf_opts
211
+ @page_margin_by_side = { recto: page_margin, verso: page_margin }
212
+ if (@media = doc.attr 'media', 'screen') == 'prepress'
213
+ @ppbook = doc.doctype == 'book'
214
+ page_margin_recto = @page_margin_by_side[:recto]
215
+ if (page_margin_outer = theme.page_margin_outer)
216
+ page_margin_recto[1] = @page_margin_by_side[:verso][3] = page_margin_outer
217
+ end
218
+ if (page_margin_inner = theme.page_margin_inner)
219
+ page_margin_recto[3] = @page_margin_by_side[:verso][1] = page_margin_inner
220
+ end
221
+ # NOTE prepare scratch document to use page margin from recto side
222
+ set_page_margin page_margin_recto unless page_margin_recto == page_margin
223
+ else
224
+ @ppbook = false
225
+ end
201
226
  # QUESTION should ThemeLoader register fonts?
202
227
  register_fonts theme.font_catalog, (doc.attr 'scripts', 'latin'), (doc.attr 'pdf-fontsdir', ThemeLoader::FontsDir)
203
- @page_bg_image = nil
204
- if (bg_image = resolve_background_image doc, theme, 'page-background-image')
205
- @page_bg_image = (bg_image == 'none' ? nil : bg_image)
228
+ if (bg_image = resolve_background_image doc, theme, 'page-background-image') && bg_image != 'none'
229
+ @page_bg_image = bg_image
230
+ else
231
+ @page_bg_image = nil
206
232
  end
207
233
  @page_bg_color = resolve_theme_color :page_background_color, 'FFFFFF'
208
234
  @fallback_fonts = [*theme.font_fallbacks]
209
235
  @font_color = theme.base_font_color
210
236
  @text_transform = nil
211
- @stamps = {}
212
237
  # NOTE we have to init pdfmarks here while we have a reference to the doc
213
238
  @pdfmarks = (doc.attr? 'pdfmarks') ? (Pdfmarks.new doc) : nil
214
239
  init_scratch_prototype
@@ -299,37 +324,46 @@ class Converter < ::Prawn::Document
299
324
  end
300
325
 
301
326
  def convert_section sect, opts = {}
302
- theme_font :heading, level: (h_level = sect.level + 1) do
327
+ theme_font :heading, level: (hlevel = sect.level + 1) do
303
328
  title = sect.numbered_title formal: true
304
- align = (@theme[%(heading_h#{h_level}_align)] || @theme.heading_align || :left).to_sym
305
- unless at_page_top?
329
+ align = (@theme[%(heading_h#{hlevel}_align)] || @theme.heading_align || @theme.base_align).to_sym
330
+ type = nil
331
+ if sect.part_or_chapter?
306
332
  if sect.chapter?
333
+ type = :chapter
307
334
  start_new_chapter sect
308
- # FIXME smarter calculation here!!
309
- elsif cursor < (height_of title) + @theme.heading_margin_top + @theme.heading_margin_bottom + @theme.base_line_height_length * 1.5
310
- start_new_page
335
+ else
336
+ type = :part
337
+ start_new_part sect
311
338
  end
339
+ else
340
+ # FIXME smarter calculation here!!
341
+ start_new_page unless at_page_top? || cursor > (height_of title) + @theme.heading_margin_top + @theme.heading_margin_bottom + (@theme.base_line_height_length * 1.5)
312
342
  end
313
- # QUESTION should we store page_start & destination in internal map?
314
- # TODO ideally, this attribute should be pdf-page-start
315
- sect.set_attr 'page_start', page_number
316
- # NOTE auto-generate an anchor if one doesn't exist so TOC works
343
+ # QUESTION should we store pdf-page-start, pdf-anchor & pdf-destination in internal map?
344
+ sect.set_attr 'pdf-page-start', (start_pgnum = page_number)
317
345
  # QUESTION should we just assign the section this generated id?
318
- sect.set_attr 'anchor', (sect_anchor = sect.id || %(__autoid-#{page_number}-#{y.ceil}))
346
+ # NOTE section must have pdf-anchor in order to be listed in the TOC
347
+ sect.set_attr 'pdf-anchor', (sect_anchor = derive_anchor_from_id sect.id, %(#{start_pgnum}-#{y.ceil}))
319
348
  add_dest_for_block sect, sect_anchor
320
- sect.chapter? ? (layout_chapter_title sect, title, align: align) : (layout_heading title, align: align)
349
+ if type == :part
350
+ layout_part_title sect, title, align: align
351
+ elsif type == :chapter
352
+ layout_chapter_title sect, title, align: align
353
+ else
354
+ layout_heading title, align: align
355
+ end
321
356
  end
322
357
 
323
358
  convert_content_for_block sect
324
- # TODO ideally, this attribute should be pdf-page-end
325
- sect.set_attr 'page_end', page_number
359
+ sect.set_attr 'pdf-page-end', page_number
326
360
  end
327
361
 
328
362
  def convert_floating_title node
329
363
  add_dest_for_block node if node.id
330
364
  # QUESTION should we decouple styles from section titles?
331
- theme_font :heading, level: (h_level = node.level + 1) do
332
- layout_heading node.title, align: (@theme[%(heading_h#{h_level}_align)] || @theme.heading_align || :left).to_sym
365
+ theme_font :heading, level: (hlevel = node.level + 1) do
366
+ layout_heading node.title, align: (@theme[%(heading_h#{hlevel}_align)] || @theme.heading_align || @theme.base_align).to_sym
333
367
  end
334
368
  end
335
369
 
@@ -455,7 +489,7 @@ class Converter < ::Prawn::Document
455
489
  layout_caption node.title if node.title?
456
490
  convert_content_for_block node
457
491
  # FIXME HACK compensate for margin bottom of admonition content
458
- move_up shift_bottom
492
+ move_up shift_bottom unless at_page_top?
459
493
  end
460
494
  end
461
495
  #end
@@ -489,7 +523,6 @@ class Converter < ::Prawn::Document
489
523
  when 'abstract'
490
524
  convert_abstract node
491
525
  when 'partintro'
492
- # FIXME cuts off any content beyond first paragraph!!
493
526
  if node.blocks.size == 1 && node.blocks.first.style == 'abstract'
494
527
  convert_abstract node.blocks.first
495
528
  else
@@ -504,9 +537,11 @@ class Converter < ::Prawn::Document
504
537
 
505
538
  def convert_quote_or_verse node
506
539
  add_dest_for_block node if node.id
507
- border_width = @theme.blockquote_border_width
508
540
  theme_margin :block, :top
541
+ b_width = @theme.blockquote_border_width
542
+ b_color = @theme.blockquote_border_color
509
543
  keep_together do |box_height = nil|
544
+ start_page_number = page_number
510
545
  start_cursor = cursor
511
546
  pad_box @theme.blockquote_padding do
512
547
  theme_font :blockquote do
@@ -517,18 +552,30 @@ class Converter < ::Prawn::Document
517
552
  layout_prose content, normalize: false, align: :left
518
553
  end
519
554
  end
520
- theme_font :blockquote_cite do
521
- if node.attr? 'attribution'
522
- layout_prose %(#{EmDash} #{[(node.attr 'attribution'), (node.attr 'citetitle')].compact * ', '}), align: :left, normalize: false
555
+ if node.attr? 'attribution', nil, false
556
+ theme_font :blockquote_cite do
557
+ layout_prose %(#{EmDash} #{[(node.attr 'attribution'), (node.attr 'citetitle', nil, false)].compact * ', '}), align: :left, normalize: false
523
558
  end
524
559
  end
525
560
  end
561
+ # FIXME we want to draw graphics before content, but box_height is not reliable when spanning pages
526
562
  if box_height
527
- # QUESTION should we use bounding_box + stroke_vertical_rule instead?
528
- save_graphics_state do
529
- stroke_color @theme.blockquote_border_color
530
- line_width border_width
531
- stroke_vertical_line cursor, start_cursor, at: border_width / 2.0
563
+ page_spread = (end_page_number = page_number) - start_page_number + 1
564
+ end_cursor = cursor
565
+ go_to_page start_page_number
566
+ move_cursor_to start_cursor
567
+ page_spread.times do |i|
568
+ if i == 0
569
+ y_draw = cursor
570
+ b_height = page_spread > 1 ? y_draw : (y_draw - end_cursor)
571
+ else
572
+ bounds.move_past_bottom
573
+ y_draw = cursor
574
+ b_height = page_spread - 1 == i ? (y_draw - end_cursor) : y_draw
575
+ end
576
+ bounding_box [0, y_draw], width: bounds.width, height: b_height do
577
+ stroke_vertical_rule b_color, line_width: b_width, at: b_width / 2.0
578
+ end
532
579
  end
533
580
  end
534
581
  end
@@ -553,7 +600,7 @@ class Converter < ::Prawn::Document
553
600
  if node.title?
554
601
  theme_font :sidebar_title do
555
602
  # QUESTION should we allow margins of sidebar title to be customized?
556
- layout_heading node.title, align: @theme.sidebar_title_align.to_sym, margin_top: 0
603
+ layout_heading node.title, align: (@theme.sidebar_title_align || @theme.base_align).to_sym, margin_top: 0
557
604
  end
558
605
  end
559
606
  theme_font :sidebar do
@@ -608,8 +655,7 @@ class Converter < ::Prawn::Document
608
655
  end
609
656
 
610
657
  indent marker_width do
611
- convert_content_for_list_item node,
612
- margin_bottom: @theme.outline_list_item_spacing
658
+ convert_content_for_list_item node, margin_bottom: @theme.outline_list_item_spacing
613
659
  end
614
660
  end
615
661
 
@@ -657,8 +703,9 @@ class Converter < ::Prawn::Document
657
703
  else
658
704
  '1'
659
705
  end
660
- if (skip = (node.attr 'start', 1).to_i - 1) > 0
661
- skip.times { list_number = list_number.next }
706
+ # TODO support start values < 1 (issue #498)
707
+ if (start = ((node.attr 'start', nil, false) || ((node.option? 'reversed') ? node.items.size : 1)).to_i) > 1
708
+ (start - 1).times { list_number = list_number.next }
662
709
  end
663
710
  @list_numbers << list_number
664
711
  convert_outline_list node
@@ -667,6 +714,7 @@ class Converter < ::Prawn::Document
667
714
 
668
715
  def convert_ulist node
669
716
  add_dest_for_block node if node.id
717
+ # TODO move bullet_type to method on List (or helper method)
670
718
  if node.option? 'checklist'
671
719
  @list_bullets << :checkbox
672
720
  else
@@ -678,12 +726,12 @@ class Converter < ::Prawn::Document
678
726
  style.to_sym
679
727
  end
680
728
  else
681
- case (node.level % 3)
729
+ case node.outline_level
682
730
  when 1
683
731
  :disc
684
732
  when 2
685
733
  :circle
686
- when 0
734
+ else
687
735
  :square
688
736
  end
689
737
  end
@@ -710,8 +758,8 @@ class Converter < ::Prawn::Document
710
758
  end
711
759
  end
712
760
  # NOTE Children will provide the necessary bottom margin if last item is complex.
713
- # However, don't leave gap at the bottom of a nested list
714
- unless complex || (::Asciidoctor::List === node.parent && node.parent.outline?)
761
+ # However, don't leave gap at the bottom if list is nested in an outline list
762
+ unless complex || (node.nested? && node.parent.parent.outline?)
715
763
  # correct bottom margin of last item
716
764
  list_margin_bottom = @theme.prose_margin_bottom
717
765
  margin_bottom list_margin_bottom - @theme.outline_list_item_spacing
@@ -724,15 +772,16 @@ class Converter < ::Prawn::Document
724
772
  when :ulist
725
773
  marker = @list_bullets.last
726
774
  if marker == :checkbox
727
- if node.attr? 'checkbox'
728
- marker = BallotBox[(node.attr? 'checked') ? :checked : :unchecked]
775
+ if node.attr? 'checkbox', nil, false
776
+ marker = BallotBox[(node.attr? 'checked', nil, false) ? :checked : :unchecked]
729
777
  else
730
778
  # QUESTION should we remove marker indent in this case?
731
779
  marker = nil
732
780
  end
733
781
  end
734
782
  when :olist
735
- @list_numbers << (index = @list_numbers.pop).next
783
+ dir = (node.parent.option? 'reversed') ? :pred : :next
784
+ @list_numbers << ((index = @list_numbers.pop).public_send dir)
736
785
  marker = %(#{index}.)
737
786
  else
738
787
  warn %(asciidoctor: WARNING: unknown list type #{list_type.inspect})
@@ -743,7 +792,7 @@ class Converter < ::Prawn::Document
743
792
  marker_width = width_of marker
744
793
  start_position = -marker_width + -(width_of 'x')
745
794
  float do
746
- bounding_box [start_position, cursor], width: marker_width do
795
+ flow_bounding_box start_position, width: marker_width do
747
796
  layout_prose marker,
748
797
  align: :right,
749
798
  color: (@theme.outline_list_marker_font_color || @font_color),
@@ -759,8 +808,7 @@ class Converter < ::Prawn::Document
759
808
  if complex
760
809
  convert_content_for_list_item node
761
810
  else
762
- convert_content_for_list_item node,
763
- margin_bottom: @theme.outline_list_item_spacing
811
+ convert_content_for_list_item node, margin_bottom: @theme.outline_list_item_spacing
764
812
  end
765
813
  end
766
814
 
@@ -772,63 +820,75 @@ class Converter < ::Prawn::Document
772
820
  convert_content_for_block node
773
821
  end
774
822
 
775
- def convert_image node
823
+ def convert_image node, opts = {}
776
824
  node.extend ::Asciidoctor::Image unless ::Asciidoctor::Image === node
777
- valid_image = true
778
- target, image_type = node.target_with_image_type
779
-
780
- if image_type == 'gif'
781
- valid_image = false
782
- warn %(asciidoctor: WARNING: GIF image format not supported. Please convert #{target} to PNG.)
783
- end
784
-
785
- unless (image_path = resolve_image_path node, target) && (::File.readable? image_path)
786
- valid_image = false
787
- warn %(asciidoctor: WARNING: image to embed not found or not readable: #{image_path || target})
825
+ target, image_format = node.target_and_format
826
+
827
+ if image_format == 'gif'
828
+ warn %(asciidoctor: WARNING: GIF image format not supported. Please convert #{target} to PNG.) unless scratch?
829
+ image_path = false
830
+ elsif (image_path = resolve_image_path node, target, (opts.fetch :relative_to_imagesdir, true), image_format) &&
831
+ (::File.readable? image_path)
832
+ # NOTE import_page automatically advances to next page afterwards
833
+ # QUESTION should we add destination to top of imported page?
834
+ return import_page image_path if image_format == 'pdf'
835
+ else
836
+ warn %(asciidoctor: WARNING: image to embed not found or not readable: #{image_path || target}) unless scratch?
837
+ image_path = false
838
+ # QUESTION should we use alt text in this case?
839
+ return if image_format == 'pdf'
788
840
  end
789
841
 
790
- # NOTE import_page automatically advances to next page afterwards
791
- return import_page image_path if image_type == 'pdf'
792
-
793
842
  # QUESTION if we advance to new page, shouldn't dest point there too?
794
843
  add_dest_for_block node if node.id
795
- position = ((node.attr 'align') || @theme.image_align).to_sym
844
+ alignment = ((node.attr 'align', nil, false) || @theme.image_align).to_sym
796
845
 
797
- unless valid_image
798
- theme_margin :block, :top
799
- if (link = node.attr 'link')
846
+ theme_margin :block, :top
847
+
848
+ unless image_path
849
+ if (link = node.attr 'link', nil, false)
800
850
  alt_text = %(<a href="#{link}">[#{NoBreakSpace}#{node.attr 'alt'}#{NoBreakSpace}]</a> | <em>#{target}</em>)
801
851
  else
802
852
  alt_text = %([#{NoBreakSpace}#{node.attr 'alt'}#{NoBreakSpace}] | <em>#{target}</em>)
803
853
  end
804
- layout_prose alt_text, normalize: false, margin: 0, single_line: true, align: position
805
- layout_caption node, position: :bottom if node.title?
854
+ layout_prose alt_text, normalize: false, margin: 0, single_line: true, align: alignment
855
+ layout_caption node, side: :bottom if node.title?
806
856
  theme_margin :block, :bottom
807
857
  return
808
858
  end
809
859
 
810
- theme_margin :block, :top
811
-
812
860
  # TODO support cover (aka canvas) image layout using "canvas" (or "cover") role
813
- width = resolve_explicit_width node.attributes, bounds.width
814
- if (width_relative_to_page = (node.attr? 'pdfwidth') && ((node.attr 'pdfwidth').end_with? 'vw'))
861
+ width = resolve_explicit_width node.attributes, bounds.width, support_vw: true, use_fallback: true
862
+ if (width_relative_to_page = ViewportWidth === width)
863
+ width = (width.to_f / 100) * page_width
815
864
  overflow = [bounds_margin_left, bounds_margin_right]
816
865
  else
817
866
  overflow = 0
818
867
  end
819
868
 
820
869
  span_page_width_if width_relative_to_page do
821
- case image_type
870
+ case image_format
822
871
  when 'svg'
823
872
  begin
824
873
  svg_data = ::IO.read image_path
825
- svg_obj = ::Prawn::Svg::Interface.new svg_data, self, position: position, width: width, fallback_font_name: default_svg_font, enable_file_requests_with_root: (::File.dirname image_path)
874
+ svg_obj = ::Prawn::Svg::Interface.new svg_data, self,
875
+ position: alignment,
876
+ width: width,
877
+ fallback_font_name: (fallback_font_name = default_svg_font),
878
+ enable_web_requests: (enable_web_requests = node.document.attr? 'allow-uri-read'),
879
+ # TODO enforce jail in safe mode
880
+ enable_file_requests_with_root: (file_request_root = ::File.dirname image_path)
826
881
  rendered_w = (svg_size = svg_obj.document.sizing).output_width
827
882
  if !width && (svg_obj.document.root.attributes.key? 'width')
828
883
  # NOTE scale native width & height by 75% to convert px to pt; restrict width to bounds.width
829
884
  if (adjusted_w = [bounds.width, rendered_w * 0.75].min) != rendered_w
830
- # FIXME would be nice to have a resize/recalculate method; instead, just reconstruct
831
- svg_obj = ::Prawn::Svg::Interface.new svg_data, self, position: position, width: (rendered_w = adjusted_w), fallback_font_name: default_svg_font, enable_file_requests_with_root: (::File.dirname image_path)
885
+ # FIXME would be nice to have a resize method (available as of prawn-svg 0.25.2); for now, just reconstruct
886
+ svg_obj = ::Prawn::Svg::Interface.new svg_data, self,
887
+ position: alignment,
888
+ width: (rendered_w = adjusted_w),
889
+ fallback_font_name: fallback_font_name,
890
+ enable_web_requests: enable_web_requests,
891
+ enable_file_requests_with_root: file_request_root
832
892
  svg_size = svg_obj.document.sizing
833
893
  end
834
894
  end
@@ -837,14 +897,15 @@ class Converter < ::Prawn::Document
837
897
  # TODO layout SVG without using keep_together (since we know the dimensions already); always render caption
838
898
  keep_together do |box_height = nil|
839
899
  svg_obj.instance_variable_set :@prawn, self
900
+ # NOTE prawn-svg 0.24.0, 0.25.0, & 0.25.1 didn't restore font after call to draw (see mogest/prawn-svg#80)
840
901
  svg_obj.draw
841
- if box_height && (link = node.attr 'link')
902
+ if box_height && (link = node.attr 'link', nil, false)
842
903
  link_annotation [(abs_left = svg_obj.position[0] + bounds.absolute_left), y, (abs_left + rendered_w), (y + rendered_h)],
843
904
  Border: [0, 0, 0],
844
905
  A: { Type: :Action, S: :URI, URI: (str2pdfval link) }
845
906
  end
846
- indent *overflow do
847
- layout_caption node, position: :bottom
907
+ indent(*overflow) do
908
+ layout_caption node, side: :bottom
848
909
  end if node.title?
849
910
  end
850
911
  rescue => e
@@ -855,7 +916,7 @@ class Converter < ::Prawn::Document
855
916
  # FIXME this code really needs to be better organized!
856
917
  # FIXME temporary workaround to group caption & image
857
918
  # NOTE use low-level API to access intrinsic dimensions; build_image_object caches image data previously loaded
858
- image_obj, image_info = build_image_object image_path
919
+ image_obj, image_info = ::File.open(image_path, 'rb') {|fd| build_image_object fd }
859
920
  if width
860
921
  rendered_w, rendered_h = image_info.calc_image_dimensions width: width
861
922
  else
@@ -880,11 +941,11 @@ class Converter < ::Prawn::Document
880
941
  end
881
942
  end
882
943
  # NOTE must calculate link position before embedding to get proper boundaries
883
- if (link = node.attr 'link')
884
- img_x, img_y = image_position rendered_w, rendered_h, position: position
944
+ if (link = node.attr 'link', nil, false)
945
+ img_x, img_y = image_position rendered_w, rendered_h, position: alignment
885
946
  link_box = [img_x, (img_y - rendered_h), (img_x + rendered_w), img_y]
886
947
  end
887
- embed_image image_obj, image_info, width: rendered_w, position: position
948
+ embed_image image_obj, image_info, width: rendered_w, position: alignment
888
949
  if link
889
950
  link_annotation link_box,
890
951
  Border: [0, 0, 0],
@@ -893,14 +954,14 @@ class Converter < ::Prawn::Document
893
954
  rescue => e
894
955
  warn %(asciidoctor: WARNING: could not embed image: #{image_path}; #{e.message})
895
956
  end
896
- indent *overflow do
897
- layout_caption node, position: :bottom
957
+ indent(*overflow) do
958
+ layout_caption node, side: :bottom
898
959
  end if node.title?
899
960
  end
900
961
  end
901
962
  theme_margin :block, :bottom
902
963
  ensure
903
- unlink_tmp_file image_path
964
+ unlink_tmp_file image_path if image_path
904
965
  end
905
966
 
906
967
  # QUESTION can we avoid arranging fragments multiple times (conums & autofit) by eagerly preparing arranger?
@@ -949,7 +1010,7 @@ class Converter < ::Prawn::Document
949
1010
  when 'pygments'
950
1011
  Helpers.require_library 'pygments', 'pygments.rb' unless defined? ::Pygments
951
1012
  lexer = ::Pygments::Lexer[node.attr 'language', 'text', false] || ::Pygments::Lexer['text']
952
- pygments_config = { nowrap: true, noclasses: true, style: (node.document.attr 'pygments-style') || 'pastie' }
1013
+ pygments_config = { nowrap: true, noclasses: true, stripnl: false, style: (node.document.attr 'pygments-style') || 'pastie' }
953
1014
  # TODO enable once we support background color on spans
954
1015
  #if node.attr? 'highlight', nil, false
955
1016
  # unless (hl_lines = node.resolve_lines_to_highlight(node.attr 'highlight', nil, false)).empty?
@@ -957,16 +1018,20 @@ class Converter < ::Prawn::Document
957
1018
  # end
958
1019
  #end
959
1020
  source_string, conum_mapping = extract_conums source_string
1021
+ # NOTE pygments.rb strips trailing whitespace; preserve it in case there are conums on last line
1022
+ num_trailing_spaces = source_string.size - (source_string = source_string.rstrip).size if conum_mapping
960
1023
  result = lexer.highlight source_string, options: pygments_config
961
1024
  fragments = guard_indentation text_formatter.format result
962
- conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
1025
+ conum_mapping ? (restore_conums fragments, conum_mapping, num_trailing_spaces) : fragments
963
1026
  when 'rouge'
964
1027
  Helpers.require_library RougeRequirePath, 'rouge' unless defined? ::Rouge::Formatters::Prawn
965
1028
  lexer = ::Rouge::Lexer.find(node.attr 'language', 'text', false) || ::Rouge::Lexers::PlainText
966
1029
  formatter = (@rouge_formatter ||= ::Rouge::Formatters::Prawn.new theme: (node.document.attr 'rouge-style'))
967
1030
  source_string, conum_mapping = extract_conums source_string
968
1031
  # NOTE trailing endline is added to address https://github.com/jneen/rouge/issues/279
969
- fragments = formatter.format (lexer.lex %(#{source_string}#{EOL})), line_numbers: (node.attr? 'linenums')
1032
+ fragments = formatter.format (lexer.lex %(#{source_string}#{LF})), line_numbers: (node.attr? 'linenums')
1033
+ # NOTE cleanup trailing endline (handled in rouge_ext/formatters/prawn instead)
1034
+ #fragments.last[:text] == LF ? fragments.pop : fragments.last[:text].chop!
970
1035
  conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
971
1036
  else
972
1037
  # NOTE only format if we detect a need (callouts or inline formatting)
@@ -979,13 +1044,10 @@ class Converter < ::Prawn::Document
979
1044
 
980
1045
  node.subs.replace prev_subs if prev_subs
981
1046
 
982
- theme_margin :block, :top
1047
+ adjusted_font_size = ((node.option? 'autofit') || (node.document.attr? 'autofit-option')) ?
1048
+ (theme_font_size_autofit source_chunks, :code) : nil
983
1049
 
984
- if (node.option? 'autofit') || (node.document.attr? 'autofit-option')
985
- adjusted_font_size = theme_font_size_autofit source_chunks, :code
986
- else
987
- adjusted_font_size = nil
988
- end
1050
+ theme_margin :block, :top
989
1051
 
990
1052
  keep_together do |box_height = nil|
991
1053
  caption_height = node.title? ? (layout_caption node) : 0
@@ -1000,17 +1062,15 @@ class Converter < ::Prawn::Document
1000
1062
  remaining_height = box_height - caption_height
1001
1063
  i = 0
1002
1064
  while remaining_height > 0
1003
- start_new_page if (new_page_started = i > 0)
1065
+ start_new_page if (started_new_page = i > 0)
1004
1066
  fill_height = [remaining_height, cursor].min
1005
1067
  bounding_box [0, cursor], width: bounds.width, height: fill_height do
1006
1068
  theme_fill_and_stroke_bounds :code
1007
1069
  unless b_width == 0
1008
- if new_page_started
1009
- indent b_radius, b_radius do
1010
- # dashed line to indicate continuation from previous page
1011
- stroke_horizontal_rule bg_color, line_width: b_width, line_style: :dashed
1012
- end
1013
- end
1070
+ indent b_radius, b_radius do
1071
+ # dashed line to indicate continuation from previous page
1072
+ stroke_horizontal_rule bg_color, line_width: b_width, line_style: :dashed
1073
+ end if started_new_page
1014
1074
  if remaining_height > fill_height
1015
1075
  move_down fill_height
1016
1076
  indent b_radius, b_radius do
@@ -1047,18 +1107,22 @@ class Converter < ::Prawn::Document
1047
1107
  # and the mapping of lines to conums as the second.
1048
1108
  def extract_conums string
1049
1109
  conum_mapping = {}
1050
- string = string.split(EOL).map.with_index {|line, line_num|
1110
+ string = string.split(LF).map.with_index {|line, line_num|
1051
1111
  # FIXME we get extra spaces before numbers if more than one on a line
1052
- line.gsub(CalloutExtractRx) {
1053
- # honor the escape
1054
- if $1 == '\\'
1055
- $&.sub '\\', ''
1056
- else
1057
- (conum_mapping[line_num] ||= []) << $3.to_i
1058
- ''
1059
- end
1060
- }
1061
- } * EOL
1112
+ if line.include? '<'
1113
+ line.gsub(CalloutExtractRx) {
1114
+ # honor the escape
1115
+ if $1 == '\\'
1116
+ $&.sub '\\', ''
1117
+ else
1118
+ (conum_mapping[line_num] ||= []) << $3.to_i
1119
+ ''
1120
+ end
1121
+ }
1122
+ else
1123
+ line
1124
+ end
1125
+ } * LF
1062
1126
  conum_mapping = nil if conum_mapping.empty?
1063
1127
  [string, conum_mapping]
1064
1128
  end
@@ -1067,16 +1131,16 @@ class Converter < ::Prawn::Document
1067
1131
  #--
1068
1132
  # QUESTION can this be done more efficiently?
1069
1133
  # QUESTION can we reuse arrange_fragments_by_line?
1070
- def restore_conums fragments, conum_mapping
1134
+ def restore_conums fragments, conum_mapping, num_trailing_spaces = 0
1071
1135
  lines = []
1072
1136
  line_num = 0
1073
1137
  # reorganize the fragments into an array of lines
1074
1138
  fragments.each do |fragment|
1075
1139
  line = (lines[line_num] ||= [])
1076
- if (text = fragment[:text]) == EOL
1140
+ if (text = fragment[:text]) == LF
1077
1141
  line_num += 1
1078
- elsif text.include? EOL
1079
- text.split(EOL, -1).each_with_index do |line_in_fragment, idx|
1142
+ elsif text.include? LF
1143
+ text.split(LF, -1).each_with_index do |line_in_fragment, idx|
1080
1144
  line = (lines[line_num += 1] ||= []) unless idx == 0
1081
1145
  line << (fragment.merge text: line_in_fragment) unless line_in_fragment.empty?
1082
1146
  end
@@ -1088,15 +1152,13 @@ class Converter < ::Prawn::Document
1088
1152
  last_line_num = lines.size - 1
1089
1153
  # append conums to appropriate lines, then flatten to an array of fragments
1090
1154
  lines.flat_map.with_index do |line, cur_line_num|
1155
+ last_line = cur_line_num == last_line_num
1091
1156
  if (conums = conum_mapping.delete cur_line_num)
1092
- conums = conums.map {|num| conum_glyph num }
1093
- # ensure there's at least one space between content and conum(s)
1094
- if line.size > 0 && (end_text = line.last[:text]) && !(end_text.end_with? ' ')
1095
- line.last[:text] = %(#{end_text} )
1096
- end
1097
- line << (conum_color ? { text: (conums * ' '), color: conum_color } : { text: (conums * ' ') })
1157
+ line << { text: ' ' * num_trailing_spaces } if last_line && num_trailing_spaces > 0
1158
+ conum_text = conums.map {|num| conum_glyph num } * ' '
1159
+ line << (conum_color ? { text: conum_text, color: conum_color } : { text: conum_text })
1098
1160
  end
1099
- line << { text: EOL } unless cur_line_num == last_line_num
1161
+ line << { text: LF } unless last_line
1100
1162
  line
1101
1163
  end
1102
1164
  end
@@ -1117,14 +1179,15 @@ class Converter < ::Prawn::Document
1117
1179
  next if (text = fragment[:text]).empty?
1118
1180
  text[0] = GuardedIndent if start_of_line && (text.start_with? ' ')
1119
1181
  text.gsub! InnerIndent, GuardedInnerIndent if text.include? InnerIndent
1120
- start_of_line = text.end_with? EOL
1182
+ start_of_line = text.end_with? LF
1121
1183
  end
1122
1184
  fragments
1123
1185
  end
1124
1186
 
1125
1187
  def convert_table node
1126
1188
  add_dest_for_block node if node.id
1127
- num_rows = 0
1189
+ # TODO we could skip a lot of the logic below when num_rows == 0
1190
+ num_rows = node.attr 'rowcount'
1128
1191
  num_cols = node.columns.size
1129
1192
  table_header = false
1130
1193
  theme = @theme
@@ -1147,7 +1210,6 @@ class Converter < ::Prawn::Document
1147
1210
  node.rows[:head].each do |rows|
1148
1211
  table_header = true
1149
1212
  head_transform = theme.table_head_text_transform
1150
- num_rows += 1
1151
1213
  row_data = []
1152
1214
  rows.each do |cell|
1153
1215
  row_data << {
@@ -1157,32 +1219,28 @@ class Converter < ::Prawn::Document
1157
1219
  text_color: (theme.table_head_font_color || theme.table_font_color || @font_color),
1158
1220
  size: (theme.table_head_font_size || theme.table_font_size),
1159
1221
  font: (theme.table_head_font_family || theme.table_font_family),
1160
- font_style: theme.table_head_font_style.to_sym,
1222
+ font_style: (val = theme.table_head_font_style || theme.table_font_style) ? val.to_sym : nil,
1161
1223
  colspan: cell.colspan || 1,
1162
1224
  rowspan: cell.rowspan || 1,
1163
- align: (cell.attr 'halign').to_sym,
1164
- valign: (cell.attr 'valign').to_sym
1225
+ align: (cell.attr 'halign', nil, false).to_sym,
1226
+ valign: (cell.attr 'valign', nil, false).to_sym
1165
1227
  }
1166
1228
  end
1167
1229
  table_data << row_data
1168
1230
  end
1169
1231
 
1170
1232
  (node.rows[:body] + node.rows[:foot]).each do |rows|
1171
- num_rows += 1
1172
1233
  row_data = []
1173
1234
  rows.each do |cell|
1174
1235
  cell_data = {
1175
- content: cell.text,
1176
- inline_format: [normalize: true],
1177
1236
  text_color: (theme.table_font_color || @font_color),
1178
1237
  size: theme.table_font_size,
1179
1238
  font: theme.table_font_family,
1180
1239
  colspan: cell.colspan || 1,
1181
1240
  rowspan: cell.rowspan || 1,
1182
- align: (cell.attr 'halign').to_sym,
1183
- valign: (cell.attr 'valign').to_sym
1241
+ align: (cell.attr 'halign', nil, false).to_sym,
1242
+ valign: (val = cell.attr 'valign', nil, false) == 'middle' ? :center : val.to_sym
1184
1243
  }
1185
- cell_data[:valign] = :center if cell_data[:valign] == :middle
1186
1244
  case cell.style
1187
1245
  when :emphasis
1188
1246
  cell_data[:font_style] = :italic
@@ -1191,18 +1249,21 @@ class Converter < ::Prawn::Document
1191
1249
  when :header
1192
1250
  unless defined? header_cell_data
1193
1251
  header_cell_data = {}
1194
- {
1195
- 'align' => :align,
1196
- 'font_color' => :text_color,
1197
- 'font_family' => :font,
1198
- 'font_size' => :size,
1199
- 'font_style' => :font_style
1200
- }.each do |theme_key, key|
1252
+ [
1253
+ # QUESTION should we honor alignment set by col/cell spec? how can we tell?
1254
+ ['align', :align, true],
1255
+ ['font_color', :text_color, false],
1256
+ ['font_family', :font, false],
1257
+ ['font_size', :size, false],
1258
+ ['font_style', :font_style, true]
1259
+ ].each do |(theme_key, data_key, symbol_value)|
1201
1260
  if (val = theme[%(table_header_cell_#{theme_key})])
1202
- header_cell_data[key] = val
1261
+ header_cell_data[data_key] = symbol_value ? val.to_sym : val
1203
1262
  end
1204
1263
  end
1205
- header_cell_data[:font_style] ||= :bold
1264
+ unless (header_cell_data.key? :font_style) || !(val = theme.table_head_font_style)
1265
+ header_cell_data[:font_style] = val.to_sym
1266
+ end
1206
1267
  if (val = resolve_theme_color :table_header_cell_background_color)
1207
1268
  header_cell_data[:background_color] = val
1208
1269
  end
@@ -1211,19 +1272,54 @@ class Converter < ::Prawn::Document
1211
1272
  cell_data.update header_cell_data unless header_cell_data.empty?
1212
1273
  when :monospaced
1213
1274
  cell_data[:font] = theme.literal_font_family
1214
- if (size = theme.literal_font_size)
1215
- cell_data[:size] = size
1275
+ if (val = theme.literal_font_size)
1276
+ cell_data[:size] = val
1277
+ end
1278
+ if (val = theme.literal_font_color)
1279
+ cell_data[:text_color] = val
1280
+ end
1281
+ # TODO need to also add top and bottom padding from line metrics
1282
+ #cell_data[:leading] = (calc_line_metrics theme.base_line_height).leading
1283
+ when :literal
1284
+ # FIXME core should not substitute in this case
1285
+ cell_data[:content] = preserve_indentation((cell.instance_variable_get :@text), (node.document.attr 'tabsize'))
1286
+ cell_data[:inline_format] = false
1287
+ # QUESTION should we use literal_font_*, code_font_*, or introduce another category?
1288
+ cell_data[:font] = theme.code_font_family
1289
+ if (val = theme.code_font_size)
1290
+ cell_data[:size] = val
1291
+ end
1292
+ if (val = theme.code_font_color)
1293
+ cell_data[:text_color] = val
1216
1294
  end
1217
- if (color = theme.literal_font_color)
1218
- cell_data[:text_color] = color
1295
+ # TODO need to also add top and bottom padding from line metrics
1296
+ #cell_data[:leading] = (calc_line_metrics theme.code_line_height).leading
1297
+ when :verse
1298
+ cell_data[:content] = preserve_indentation cell.text, (node.document.attr 'tabsize')
1299
+ cell_data[:inline_format] = true
1300
+ when :asciidoc
1301
+ # TODO finish me
1302
+ else
1303
+ cell_data[:font_style] = (val = theme.table_font_style) ? val.to_sym : nil
1304
+ end
1305
+ unless cell_data.key? :content
1306
+ # NOTE effectively the same as calling cell.content
1307
+ # TODO hard breaks not quite the same result as separate paragraphs; need custom cell impl
1308
+ if (cell_text = cell.text).include? LF
1309
+ cell_data[:content] = cell_text.split(BlankLineRx).map {|l| l.tr_s(WhitespaceChars, ' ') }.join(DoubleLF)
1310
+ cell_data[:inline_format] = true
1311
+ else
1312
+ cell_data[:content] = cell_text
1313
+ cell_data[:inline_format] = [normalize: true]
1219
1314
  end
1220
- # TODO finish me
1221
1315
  end
1222
1316
  row_data << cell_data
1223
1317
  end
1224
1318
  table_data << row_data
1225
1319
  end
1226
1320
 
1321
+ table_data = [[{ content: '' }]] if table_data.empty?
1322
+
1227
1323
  border = {}
1228
1324
  table_border_color = theme.table_border_color
1229
1325
  table_border_width = theme.table_border_width
@@ -1231,63 +1327,70 @@ class Converter < ::Prawn::Document
1231
1327
  [:top, :bottom, :left, :right].each {|edge| border[edge] = table_border_width }
1232
1328
  [:cols, :rows].each {|edge| border[edge] = table_grid_width }
1233
1329
 
1234
- frame = (node.attr 'frame') || 'all'
1235
- grid = (node.attr 'grid') || 'all'
1236
-
1237
- case grid
1330
+ case (grid = node.attr 'grid', 'all', false)
1331
+ when 'all'
1332
+ # keep inner borders
1238
1333
  when 'cols'
1239
1334
  border[:rows] = 0
1240
1335
  when 'rows'
1241
1336
  border[:cols] = 0
1242
- when 'none'
1337
+ else # none
1243
1338
  border[:rows] = border[:cols] = 0
1244
1339
  end
1245
1340
 
1246
- case frame
1341
+ case (frame = node.attr 'frame', 'all', false)
1342
+ when 'all'
1343
+ # keep outer borders
1247
1344
  when 'topbot'
1248
1345
  border[:left] = border[:right] = 0
1249
1346
  when 'sides'
1250
1347
  border[:top] = border[:bottom] = 0
1251
- when 'none'
1348
+ else # none
1252
1349
  border[:top] = border[:right] = border[:bottom] = border[:left] = 0
1253
1350
  end
1254
1351
 
1255
1352
  if node.option? 'autowidth'
1353
+ table_width = (node.attr? 'width', nil, false) ? bounds.width * ((node.attr 'tablepcwidth') / 100.0) :
1354
+ ((node.has_role? 'spread') ? bounds.width : nil)
1256
1355
  column_widths = []
1257
1356
  else
1258
1357
  table_width = bounds.width * ((node.attr 'tablepcwidth') / 100.0)
1259
- even_column_pct = 100.0 / node.columns.size
1260
- column_widths = node.columns.map {|col| ((col.attr 'colpcwidth', even_column_pct) * table_width) / 100.0 }
1261
- # NOTE Asciidoctor core doesn't always add colpcwidth values up to 100%
1262
- unless column_widths.empty? || (width_delta = table_width - column_widths.reduce(:+)).zero?
1358
+ column_widths = node.columns.map {|col| ((col.attr 'colpcwidth') * table_width) / 100.0 }
1359
+ # NOTE until Asciidoctor 1.5.4, colpcwidth values didn't always add up to 100%; use last column to compensate
1360
+ unless column_widths.empty? || (width_delta = table_width - column_widths.reduce(:+)) == 0
1263
1361
  column_widths[-1] += width_delta
1264
1362
  end
1265
1363
  end
1266
1364
 
1267
- if ((position = node.attr 'align') && (AlignmentNames.include? position)) ||
1268
- (position = (node.roles & AlignmentNames).last)
1269
- position = position.to_sym
1365
+ if ((alignment = node.attr 'align', nil, false) && (AlignmentNames.include? alignment)) ||
1366
+ (alignment = (node.roles & AlignmentNames).last)
1367
+ alignment = alignment.to_sym
1270
1368
  else
1271
- position = :left
1369
+ alignment = :left
1272
1370
  end
1273
1371
 
1372
+ caption_side = (theme.table_caption_side || :top).to_sym
1373
+
1274
1374
  table_settings = {
1275
1375
  header: table_header,
1276
- position: position,
1376
+ position: alignment,
1277
1377
  cell_style: {
1278
1378
  padding: theme.table_cell_padding,
1279
1379
  border_width: 0,
1280
1380
  # NOTE the border color of edges is set later
1281
1381
  border_color: theme.table_grid_color || theme.table_border_color
1282
1382
  },
1383
+ width: table_width,
1283
1384
  column_widths: column_widths,
1284
1385
  row_colors: [odd_row_bg_color, even_row_bg_color]
1285
1386
  }
1286
1387
 
1287
1388
  theme_margin :block, :top
1288
- layout_caption node if node.title?
1289
1389
 
1290
1390
  table table_data, table_settings do
1391
+ # NOTE capture resolved table width
1392
+ table_width = width
1393
+ @pdf.layout_table_caption node, table_width, alignment if node.title? && caption_side == :top
1291
1394
  if grid == 'none' && frame == 'none'
1292
1395
  if table_header
1293
1396
  # FIXME allow header border bottom width to be set by theme
@@ -1342,6 +1445,7 @@ class Converter < ::Prawn::Document
1342
1445
  #end
1343
1446
  end
1344
1447
  end
1448
+ layout_table_caption node, table_width, alignment, :bottom if node.title? && caption_side == :bottom
1345
1449
  theme_margin :block, :bottom
1346
1450
  end
1347
1451
 
@@ -1373,30 +1477,36 @@ class Converter < ::Prawn::Document
1373
1477
  attrs << %( class="#{role}")
1374
1478
  end
1375
1479
  #attrs << %( title="#{node.attr 'title'}") if node.attr? 'title'
1376
- attrs << %( target="#{node.attr 'window'}") if node.attr? 'window'
1377
- if (((doc = node.document).attr? 'media', 'print') || (doc.attr? 'show-link-uri')) && !(node.has_role? 'bare')
1480
+ attrs << %( target="#{node.attr 'window'}") if node.attr? 'window', nil, false
1481
+ # NOTE @media may not be initialized if method is called before convert phase
1482
+ if ((@media ||= node.document.attr 'media', 'screen') != 'screen' || (node.document.attr? 'show-link-uri')) &&
1483
+ !(node.has_role? 'bare')
1378
1484
  # TODO allow style of visible link to be controlled by theme
1379
- %(<a href="#{target = node.target}"#{attrs.join}>#{node.text}</a> <font size="0.9em">[#{target}]</font>)
1485
+ %(<a href="#{target = node.target}"#{attrs.join}>#{node.text}</a> [<font size="0.85em">#{target}</font>])
1380
1486
  else
1381
1487
  %(<a href="#{node.target}"#{attrs.join}>#{node.text}</a>)
1382
1488
  end
1383
1489
  when :xref
1384
- # NOTE the presence of path indicates an inter-document xref
1385
- if (path = node.attributes['path'])
1490
+ # NOTE non-nil path indicates this is an inter-document xref that's not included in current document
1491
+ if (path = node.attr 'path', nil, false)
1386
1492
  # NOTE we don't use local as that doesn't work on the web
1387
- # NOTE for the fragment to work in most viewers, it must be #page=<N>
1493
+ # NOTE for the fragment to work in most viewers, it must be #page=<N> <= document this!
1388
1494
  %(<a href="#{node.target}">#{node.text || path}</a>)
1389
1495
  else
1390
- refid = node.attributes['refid']
1391
- # NOTE reference table is not comprehensive (we don't catalog all inline anchors)
1392
- if (reftext = node.document.references[:ids][refid])
1393
- %(<a anchor="#{refid}">#{node.text || reftext}</a>)
1496
+ if (refid = node.attr 'refid', nil, false)
1497
+ anchor = derive_anchor_from_id refid
1498
+ # NOTE reference table is not comprehensive (we don't yet catalog all inline anchors)
1499
+ if (reftext = node.document.references[:ids][refid])
1500
+ %(<a anchor="#{anchor}">#{node.text || reftext}</a>)
1501
+ else
1502
+ # NOTE we don't yet catalog all inline anchors, so we can't warn here (maybe after conversion is complete)
1503
+ #source = $VERBOSE ? %( in source:\n#{node.parent.lines * "\n"}) : nil
1504
+ #warn %(asciidoctor: WARNING: reference '#{refid}' not found#{source})
1505
+ #%[(see #{node.text || %([#{refid}])})]
1506
+ %(<a anchor="#{anchor}">#{node.text || "[#{refid}]"}</a>)
1507
+ end
1394
1508
  else
1395
- # NOTE we don't catalog all inline anchors, so we can't warn here (maybe once conversion is complete)
1396
- #source = $VERBOSE ? %( in source:\n#{node.parent.lines * "\n"}) : nil
1397
- #warn %(asciidoctor: WARNING: reference '#{refid}' not found#{source})
1398
- #%[(see #{node.text || %([#{refid}])})]
1399
- %(<a anchor="#{refid}">#{node.text || "[#{refid}]"}</a>)
1509
+ %(<a anchor="#{node.document.attr 'pdf-anchor'}">#{node.text || '[^top]'}</a>)
1400
1510
  end
1401
1511
  end
1402
1512
  when :ref
@@ -1439,48 +1549,50 @@ class Converter < ::Prawn::Document
1439
1549
  end
1440
1550
  end
1441
1551
 
1552
+ def convert_inline_icon node
1553
+ if node.document.attr? 'icons', 'font'
1554
+ if (icon_name = node.target).include? '@'
1555
+ icon_name, icon_set = icon_name.split '@', 2
1556
+ else
1557
+ icon_set = node.attr 'set', (node.document.attr 'icon-set', 'fa'), false
1558
+ end
1559
+ icon_set = 'fa' unless IconSets.include? icon_set
1560
+ if node.attr? 'size', nil, false
1561
+ size = (size = (node.attr 'size')) == 'lg' ? '1.3333em' : (size.sub 'x', 'em')
1562
+ size_attr = %( size="#{size}")
1563
+ else
1564
+ size_attr = nil
1565
+ end
1566
+ begin
1567
+ # TODO support rotate and flip attributes; support fw (full-width) size
1568
+ %(<font name="#{icon_set}"#{size_attr}>#{::Prawn::Icon::FontData.load(self, icon_set).unicode icon_name}</font>)
1569
+ rescue
1570
+ warn %(asciidoctor: WARNING: #{icon_name} is not a valid icon name in the #{icon_set} icon set)
1571
+ %([#{node.attr 'alt'}])
1572
+ end
1573
+ else
1574
+ %([#{node.attr 'alt'}])
1575
+ end
1576
+ end
1577
+
1442
1578
  def convert_inline_image node
1443
- img = nil
1444
1579
  if node.type == 'icon'
1445
- if node.document.attr? 'icons', 'font'
1446
- if (icon_name = node.target).include? '@'
1447
- icon_name, icon_set = icon_name.split '@', 2
1448
- else
1449
- icon_set = node.attr 'set', (node.document.attr 'icon-set', 'fa')
1450
- end
1451
- icon_set = 'fa' unless IconSets.include? icon_set
1452
- if node.attr? 'size'
1453
- size = (size = (node.attr 'size')) == 'lg' ? '1.3333em' : (size.sub 'x', 'em')
1454
- size_attr = %( size="#{size}")
1455
- else
1456
- size_attr = nil
1457
- end
1458
- begin
1459
- # TODO support rotate and flip attributes; support fw (full-width) size
1460
- img = %(<font name="#{icon_set}"#{size_attr}>#{::Prawn::Icon::FontData.load(self, icon_set).unicode icon_name}</font>)
1461
- rescue
1462
- warn %(asciidoctor: WARNING: #{icon_name} is not a valid icon name in the #{icon_set} icon set)
1463
- end
1464
- end
1580
+ convert_inline_icon node
1465
1581
  else
1466
1582
  node.extend ::Asciidoctor::Image unless ::Asciidoctor::Image === node
1467
- target, image_type = node.target_with_image_type
1468
- valid = true
1469
- if image_type == 'gif'
1583
+ target, image_format = node.target_and_format
1584
+ if image_format == 'gif'
1470
1585
  warn %(asciidoctor: WARNING: GIF image format not supported. Please convert #{target} to PNG.) unless scratch?
1471
- valid = false
1472
- end
1473
- unless (image_path = resolve_image_path node, target) && (::File.readable? image_path)
1586
+ img = %([#{node.attr 'alt'}])
1587
+ elsif (image_path = resolve_image_path node, target, true, image_format) && (::File.readable? image_path)
1588
+ width_attr = (node.attr? 'width', nil, false) ? %( width="#{node.attr 'width'}") : nil
1589
+ img = %(<img src="#{image_path}" format="#{image_format}" alt="#{node.attr 'alt'}"#{width_attr} tmp="#{TemporaryPath === image_path}">)
1590
+ else
1474
1591
  warn %(asciidoctor: WARNING: image to embed not found or not readable: #{image_path || target}) unless scratch?
1475
- valid = false
1476
- end
1477
- if valid
1478
- width_attr = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : nil
1479
- img = %(<img src="#{image_path}" type="#{image_type}" alt="#{node.attr 'alt'}"#{width_attr} tmp="#{TemporaryPath === image_path}">)
1592
+ img = %([#{node.attr 'alt'}])
1480
1593
  end
1594
+ (node.attr? 'link', nil, false) ? %(<a href="#{node.attr 'link'}">#{img}</a>) : img
1481
1595
  end
1482
- img ||= %([#{node.attr 'alt'}])
1483
- (node.attr? 'link') ? %(<a href="#{node.attr 'link'}">#{img}</a>) : img
1484
1596
  end
1485
1597
 
1486
1598
  def convert_inline_indexterm node
@@ -1538,6 +1650,7 @@ class Converter < ::Prawn::Document
1538
1650
  quoted_text = %(#{open}#{node.text}#{close})
1539
1651
  end
1540
1652
 
1653
+ # NOTE destination is created inside callback registered by FormattedTextTransform#build_fragment
1541
1654
  node.id ? %(<a name="#{node.id}">#{ZeroWidthSpace}</a>#{quoted_text}) : quoted_text
1542
1655
  end
1543
1656
 
@@ -1556,6 +1669,7 @@ class Converter < ::Prawn::Document
1556
1669
  end
1557
1670
  # NOTE a new page will already be started if the cover image is a PDF
1558
1671
  start_new_page unless page_is_empty?
1672
+ start_new_page if @ppbook && verso_page?
1559
1673
  @page_bg_image = prev_bg_image if bg_image
1560
1674
  @page_bg_color = prev_bg_color if bg_color
1561
1675
 
@@ -1563,15 +1677,17 @@ class Converter < ::Prawn::Document
1563
1677
  font @theme.base_font_family, size: @theme.base_font_size
1564
1678
 
1565
1679
  # QUESTION allow aligment per element on title page?
1566
- title_align = @theme.title_page_align.to_sym
1680
+ title_align = (@theme.title_page_align || @theme.base_align).to_sym
1567
1681
 
1568
1682
  # TODO disallow .pdf as image type
1569
1683
  if (logo_image_path = (doc.attr 'title-logo-image', @theme.title_page_logo_image))
1570
1684
  if (logo_image_path.include? ':') && logo_image_path =~ ImageAttributeValueRx
1571
1685
  logo_image_path = $1
1572
1686
  logo_image_attrs = (AttributeList.new $2).parse ['alt', 'width', 'height']
1687
+ relative_to_imagesdir = true
1573
1688
  else
1574
1689
  logo_image_attrs = {}
1690
+ relative_to_imagesdir = false
1575
1691
  end
1576
1692
  # HACK quick fix to resolve image path relative to theme
1577
1693
  unless doc.attr? 'title-logo-image'
@@ -1579,16 +1695,21 @@ class Converter < ::Prawn::Document
1579
1695
  end
1580
1696
  logo_image_attrs['target'] = logo_image_path
1581
1697
  logo_image_attrs['align'] ||= (@theme.title_page_logo_align || title_align.to_s)
1582
- logo_image_top = (logo_image_attrs['top'] || @theme.title_page_logo_top)
1698
+ # QUESTION should we allow theme to turn logo image off?
1699
+ logo_image_top = logo_image_attrs['top'] || @theme.title_page_logo_top || '10%'
1583
1700
  # FIXME delegate to method to convert page % to y value
1584
- logo_image_top = [(page_height - page_height * (logo_image_top.to_f / 100.0)), bounds.absolute_top].min
1701
+ if logo_image_top.end_with? 'vh'
1702
+ logo_image_top = page_height - page_height * logo_image_top.to_f / 100.0
1703
+ else
1704
+ logo_image_top = bounds.absolute_top - effective_page_height * logo_image_top.to_f / 100.0
1705
+ end
1585
1706
  float do
1586
1707
  @y = logo_image_top
1587
1708
  # FIXME add API to Asciidoctor for creating blocks like this (extract from extensions module?)
1588
1709
  image_block = ::Asciidoctor::Block.new doc, :image, content_model: :empty, attributes: logo_image_attrs
1589
1710
  # FIXME prevent image from spilling to next page
1590
1711
  # QUESTION should we shave off margin top/bottom?
1591
- convert_image image_block
1712
+ convert_image image_block, relative_to_imagesdir: relative_to_imagesdir
1592
1713
  end
1593
1714
  end
1594
1715
 
@@ -1596,8 +1717,13 @@ class Converter < ::Prawn::Document
1596
1717
  theme_font :title_page do
1597
1718
  doctitle = doc.doctitle partition: true
1598
1719
  if (title_top = @theme.title_page_title_top)
1720
+ if title_top.end_with? 'vh'
1721
+ title_top = page_height - page_height * title_top.to_f / 100.0
1722
+ else
1723
+ title_top = bounds.absolute_top - effective_page_height * title_top.to_f / 100.0
1724
+ end
1599
1725
  # FIXME delegate to method to convert page % to y value
1600
- @y = [(page_height - page_height * (title_top.to_f / 100.0)), bounds.absolute_top].min
1726
+ @y = title_top
1601
1727
  end
1602
1728
  move_down (@theme.title_page_title_margin_top || 0)
1603
1729
  theme_font :title_page_title do
@@ -1621,10 +1747,10 @@ class Converter < ::Prawn::Document
1621
1747
  move_down (@theme.title_page_authors_margin_top || 0)
1622
1748
  theme_font :title_page_authors do
1623
1749
  # TODO add support for author delimiter
1624
- layout_prose doc.attr('authors'),
1750
+ layout_prose((doc.attr 'authors'),
1625
1751
  align: title_align,
1626
1752
  margin: 0,
1627
- normalize: false
1753
+ normalize: false)
1628
1754
  end
1629
1755
  move_down (@theme.title_page_authors_margin_bottom || 0)
1630
1756
  end
@@ -1643,37 +1769,47 @@ class Converter < ::Prawn::Document
1643
1769
  end
1644
1770
  end
1645
1771
 
1646
- def layout_cover_page position, doc
1772
+ def layout_cover_page face, doc
1647
1773
  # TODO turn processing of attribute with inline image a utility function in Asciidoctor
1648
- # FIXME verify cover_image exists!
1649
- if (cover_image = (doc.attr %(#{position}-cover-image)))
1774
+ if (cover_image = (doc.attr %(#{face}-cover-image)))
1650
1775
  if (cover_image.include? ':') && cover_image =~ ImageAttributeValueRx
1776
+ # TODO support explicit image format
1651
1777
  cover_image = resolve_image_path doc, $1
1778
+ else
1779
+ cover_image = resolve_image_path doc, cover_image, false
1652
1780
  end
1653
- # QUESTION should we go to page 1 when position == :front?
1654
- go_to_page page_count if position == :back
1655
- if cover_image.downcase.end_with? '.pdf'
1656
- # NOTE import_page automatically advances to next page afterwards
1657
- import_page cover_image, advance: position != :back
1781
+
1782
+ if ::File.readable? cover_image
1783
+ go_to_page page_count if face == :back
1784
+ if cover_image.downcase.end_with? '.pdf'
1785
+ # NOTE import_page automatically advances to next page afterwards (can we change this behavior?)
1786
+ import_page cover_image, advance: face != :back
1787
+ else
1788
+ image_page cover_image, canvas: true
1789
+ end
1658
1790
  else
1659
- image_page cover_image, canvas: true
1791
+ warn %(asciidoctor: WARNING: #{face} cover image not found or readable: #{cover_image})
1660
1792
  end
1661
1793
  end
1662
1794
  ensure
1663
- unlink_tmp_file cover_image
1795
+ unlink_tmp_file cover_image if cover_image
1664
1796
  end
1665
1797
 
1666
- # NOTE can't alias to start_new_page since methods have different arity
1667
- # NOTE only called if not at page top
1668
- def start_new_chapter section
1669
- start_new_page
1798
+ def start_new_chapter chapter
1799
+ start_new_page unless at_page_top?
1800
+ # TODO must call update_colors before advancing to next page if start_new_page is called in layout_chapter_title
1801
+ start_new_page if @ppbook && verso_page? && !(chapter.option? 'nonfacing')
1670
1802
  end
1671
1803
 
1672
1804
  def layout_chapter_title node, title, opts = {}
1673
1805
  layout_heading title, opts
1674
1806
  end
1675
1807
 
1808
+ alias :start_new_part :start_new_chapter
1809
+ alias :layout_part_title :layout_chapter_title
1810
+
1676
1811
  # QUESTION why doesn't layout_heading set the font??
1812
+ # QUESTION why doesn't layout_heading accept a node?
1677
1813
  def layout_heading string, opts = {}
1678
1814
  top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme.heading_margin_top
1679
1815
  bot_margin = margin || (opts.delete :margin_bottom) || @theme.heading_margin_bottom
@@ -1684,7 +1820,7 @@ class Converter < ::Prawn::Document
1684
1820
  typeset_text string, calc_line_metrics((opts.delete :line_height) || @theme.heading_line_height), {
1685
1821
  color: @font_color,
1686
1822
  inline_format: true,
1687
- align: :left
1823
+ align: @theme.base_align.to_sym
1688
1824
  }.merge(opts)
1689
1825
  margin_bottom bot_margin
1690
1826
  end
@@ -1729,7 +1865,7 @@ class Converter < ::Prawn::Document
1729
1865
  return 0
1730
1866
  end
1731
1867
  theme_font :caption do
1732
- if (position = (opts.delete :position) || :top) == :top
1868
+ if (side = (opts.delete :side) || :top) == :top
1733
1869
  margin = { top: @theme.caption_margin_outside, bottom: @theme.caption_margin_inside }
1734
1870
  else
1735
1871
  margin = { top: @theme.caption_margin_inside, bottom: @theme.caption_margin_outside }
@@ -1737,10 +1873,10 @@ class Converter < ::Prawn::Document
1737
1873
  layout_prose string, {
1738
1874
  margin_top: margin[:top],
1739
1875
  margin_bottom: margin[:bottom],
1740
- align: @theme.caption_align.to_sym,
1876
+ align: (@theme.caption_align || @theme.base_align).to_sym,
1741
1877
  normalize: false
1742
1878
  }.merge(opts)
1743
- if position == :top && @theme.caption_border_bottom_color
1879
+ if side == :top && @theme.caption_border_bottom_color
1744
1880
  stroke_horizontal_rule @theme.caption_border_bottom_color
1745
1881
  # FIXME HACK move down slightly so line isn't covered by filled area (half width of line)
1746
1882
  move_down 0.25
@@ -1754,11 +1890,24 @@ class Converter < ::Prawn::Document
1754
1890
  end
1755
1891
  end
1756
1892
 
1893
+ # Render the caption for a table and return the height of the rendered content
1894
+ def layout_table_caption node, width, alignment = :left, side = :top
1895
+ # QUESTION should we confine width of title to width of table?
1896
+ if alignment == :left || (excess = bounds.width - width) == 0
1897
+ layout_caption node, side: side
1898
+ else
1899
+ indent excess * (alignment == :center ? 0.5 : 1) do
1900
+ layout_caption node, side: side
1901
+ end
1902
+ end
1903
+ end
1904
+
1905
+ # NOTE num_front_matter_pages is not used during a dry run
1757
1906
  def layout_toc doc, num_levels = 2, toc_page_number = 2, num_front_matter_pages = 0
1758
1907
  go_to_page toc_page_number unless (page_number == toc_page_number) || scratch?
1759
1908
  start_page_number = page_number
1760
1909
  theme_font :heading, level: 2 do
1761
- layout_heading doc.attr('toc-title'), align: (@theme.toc_title_align || :left).to_sym
1910
+ layout_heading((doc.attr 'toc-title'), align: (@theme.toc_title_align || @theme.base_align).to_sym)
1762
1911
  end
1763
1912
  # QUESTION shouldn't we skip this whole method if num_levels == 0?
1764
1913
  if num_levels > 0
@@ -1786,7 +1935,7 @@ class Converter < ::Prawn::Document
1786
1935
  start_cursor = cursor
1787
1936
  # NOTE CMYK value gets flattened here, but is restored by formatted text parser
1788
1937
  # FIXME use layout_prose
1789
- typeset_text %(<a anchor="#{sect_anchor = (sect.attr 'anchor') || sect.id}"><color rgb="#{@font_color}">#{sect_title}</color></a>), line_metrics, inline_format: true
1938
+ typeset_text %(<a anchor="#{sect_anchor = sect.attr 'pdf-anchor'}"><color rgb="#{@font_color}">#{sect_title}</color></a>), line_metrics, inline_format: true
1790
1939
  # we only write the label if this is a dry run
1791
1940
  unless scratch?
1792
1941
  end_page_number = page_number
@@ -1794,17 +1943,17 @@ class Converter < ::Prawn::Document
1794
1943
  # TODO it would be convenient to have a cursor mark / placement utility that took page number into account
1795
1944
  go_to_page start_page_number if start_page_number != end_page_number
1796
1945
  move_cursor_to start_cursor
1797
- sect_page_num = (sect.attr 'page_start') - num_front_matter_pages
1946
+ sect_pgnum_label = (sect.attr 'pdf-page-start') - num_front_matter_pages
1798
1947
  spacer_width = (width_of NoBreakSpace) * 0.75
1799
1948
  # FIXME this calculation will be wrong if a style is set per level
1800
- num_dots = ((bounds.width - (width_of %(#{sect_title}#{sect_page_num}), inline_format: true) - spacer_width) / dot_width).floor
1949
+ num_dots = ((bounds.width - (width_of %(#{sect_title}#{sect_pgnum_label}), inline_format: true) - spacer_width) / dot_width).floor
1801
1950
  num_dots = 0 if num_dots < 0
1802
1951
  # FIXME dots don't line up if width of page numbers differ
1803
1952
  typeset_formatted_text [
1804
1953
  { text: %(#{(@theme.toc_dot_leader_content || DotLeaderDefault) * num_dots}), color: toc_dot_color },
1805
- # FIXME this spacing doesn't always work out
1954
+ # FIXME this spacing doesn't always work out; should we use graphics instead?
1806
1955
  { text: NoBreakSpace, size: (@font_size * 0.5) },
1807
- { text: sect_page_num.to_s, anchor: sect_anchor, color: @font_color }], line_metrics, align: :right
1956
+ { text: sect_pgnum_label.to_s, anchor: sect_anchor, color: @font_color }], line_metrics, align: :right
1808
1957
  go_to_page end_page_number if start_page_number != end_page_number
1809
1958
  move_cursor_to end_cursor
1810
1959
  end
@@ -1832,9 +1981,9 @@ class Converter < ::Prawn::Document
1832
1981
  end
1833
1982
 
1834
1983
  # TODO delegate to layout_page_header and layout_page_footer per page
1835
- def layout_running_content position, doc, opts = {}
1984
+ def layout_running_content periphery, doc, opts = {}
1836
1985
  # QUESTION should we short-circuit if setting not specified and if so, which setting?
1837
- return unless (position == :header && @theme.header_height) || (position == :footer && @theme.footer_height)
1986
+ return unless (periphery == :header && @theme.header_height) || (periphery == :footer && @theme.footer_height)
1838
1987
  skip = opts[:skip] || 1
1839
1988
  start = skip + 1
1840
1989
  num_pages = page_count - skip
@@ -1847,9 +1996,9 @@ class Converter < ::Prawn::Document
1847
1996
  section_start_pages = {}
1848
1997
  sections.each do |sect|
1849
1998
  if sect.chapter?
1850
- chapter_start_pages[(sect.attr 'page_start').to_i - skip] ||= (sect.numbered_title formal: true)
1999
+ chapter_start_pages[(sect.attr 'pdf-page-start').to_i - skip] ||= (sect.numbered_title formal: true)
1851
2000
  else
1852
- section_start_pages[(sect.attr 'page_start').to_i - skip] ||= (sect.numbered_title formal: true)
2001
+ section_start_pages[(sect.attr 'pdf-page-start').to_i - skip] ||= (sect.numbered_title formal: true)
1853
2002
  end
1854
2003
  end
1855
2004
 
@@ -1878,15 +2027,11 @@ class Converter < ::Prawn::Document
1878
2027
  doc.set_attr 'document-subtitle', doctitle.subtitle
1879
2028
  doc.set_attr 'page-count', num_pages
1880
2029
 
1881
- fallback_footer_content = {
1882
- recto: { right: '{page-number}' },
1883
- verso: { left: '{page-number}' }
1884
- }
1885
2030
  # TODO move this to a method so it can be reused; cache results
1886
- content_dict = [:recto, :verso].inject({}) do |acc, side|
2031
+ content_dict = PageSides.inject({}) do |acc, side|
1887
2032
  side_content = {}
1888
- Alignments.each do |align|
1889
- if (val = @theme[%(#{position}_#{side}_content_#{align})])
2033
+ ColumnPositions.each do |position|
2034
+ if (val = @theme[%(#{periphery}_#{side}_#{position}_content)])
1890
2035
  # TODO support image URL (using resolve_image_path)
1891
2036
  if (val.include? ':') && val =~ ImageAttributeValueRx &&
1892
2037
  ::File.readable?(path = (ThemeLoader.resolve_theme_asset $1, (doc.attr 'pdf-stylesdir')))
@@ -1896,30 +2041,27 @@ class Converter < ::Prawn::Document
1896
2041
  unless width
1897
2042
  width = [bounds.width, (intrinsic_image_dimensions path)[:width] * 0.75].min
1898
2043
  end
1899
- side_content[align] = { path: path, width: width }
2044
+ side_content[position] = { path: path, width: width }
1900
2045
  else
1901
- side_content[align] = val
2046
+ side_content[position] = val
1902
2047
  end
1903
2048
  end
1904
2049
  end
1905
2050
  # NOTE set fallbacks if not explicitly disabled
1906
- if side_content.empty? && position == :footer && @theme[%(footer_#{side}_content)] != 'none'
1907
- side_content = fallback_footer_content[side]
2051
+ if side_content.empty? && periphery == :footer && @theme[%(footer_#{side}_content)] != 'none'
2052
+ side_content = { side == :recto ? :right : :left => '{page-number}' }
1908
2053
  end
1909
2054
 
1910
2055
  acc[side] = side_content
1911
2056
  acc
1912
2057
  end
1913
2058
 
1914
- if position == :header
2059
+ if periphery == :header
1915
2060
  trim_line_metrics = calc_line_metrics(@theme.header_line_height || @theme.base_line_height)
1916
2061
  trim_top = page_height
1917
2062
  # NOTE height is required atm
1918
2063
  trim_height = @theme.header_height || page_margin_top
1919
2064
  trim_padding = @theme.header_padding || [0, 0, 0, 0]
1920
- trim_left = page_margin_left
1921
- trim_width = page_width - trim_left - page_margin_right
1922
- trim_font_color = @theme.header_font_color || @font_color
1923
2065
  trim_bg_color = resolve_theme_color :header_background_color
1924
2066
  trim_border_width = @theme.header_border_width || @theme.base_border_width
1925
2067
  trim_border_style = (@theme.header_border_style || :solid).to_sym
@@ -1931,9 +2073,6 @@ class Converter < ::Prawn::Document
1931
2073
  # NOTE height is required atm
1932
2074
  trim_top = trim_height = @theme.footer_height || page_margin_bottom
1933
2075
  trim_padding = @theme.footer_padding || [0, 0, 0, 0]
1934
- trim_left = page_margin_left
1935
- trim_width = page_width - trim_left - page_margin_right
1936
- trim_font_color = @theme.footer_font_color || @font_color
1937
2076
  trim_bg_color = resolve_theme_color :footer_background_color
1938
2077
  trim_border_width = @theme.footer_border_width || @theme.base_border_width
1939
2078
  trim_border_style = (@theme.footer_border_style || :solid).to_sym
@@ -1942,10 +2081,27 @@ class Converter < ::Prawn::Document
1942
2081
  trim_img_valign = @theme.footer_image_vertical_align
1943
2082
  end
1944
2083
 
1945
- trim_stamp = position.to_s
1946
- trim_content_left = trim_left + trim_padding[3]
2084
+ trim_stamp_name = {
2085
+ recto: %(#{periphery}_recto),
2086
+ verso: %(#{periphery}_verso)
2087
+ }
2088
+ trim_left = {
2089
+ recto: @page_margin_by_side[:recto][3],
2090
+ verso: @page_margin_by_side[:verso][3]
2091
+ }
2092
+ trim_width = {
2093
+ recto: page_width - trim_left[:recto] - @page_margin_by_side[:recto][1],
2094
+ verso: page_width - trim_left[:verso] - @page_margin_by_side[:verso][1]
2095
+ }
2096
+ trim_content_left = {
2097
+ recto: trim_left[:recto] + trim_padding[3],
2098
+ verso: trim_left[:verso] + trim_padding[3]
2099
+ }
2100
+ trim_content_width = {
2101
+ recto: trim_width[:recto] - trim_padding[3] - trim_padding[1],
2102
+ verso: trim_width[:verso] - trim_padding[3] - trim_padding[1]
2103
+ }
1947
2104
  trim_content_height = trim_height - trim_padding[0] - trim_padding[2] - trim_line_metrics.padding_top - trim_line_metrics.padding_bottom
1948
- trim_content_width = trim_width - trim_padding[3] - trim_padding[1]
1949
2105
  trim_border_color = nil if trim_border_width == 0
1950
2106
  trim_valign = :center if trim_valign == :middle
1951
2107
  case trim_img_valign
@@ -1957,130 +2113,164 @@ class Converter < ::Prawn::Document
1957
2113
  trim_img_valign = trim_img_valign.to_sym
1958
2114
  end
1959
2115
 
2116
+ colspec_dict = PageSides.inject({}) do |acc, side|
2117
+ side_trim_content_width = trim_content_width[side]
2118
+ if (custom_colspecs = @theme[%(#{periphery}_#{side}_columns)])
2119
+ colspecs = %w(<40% =20% >40%)
2120
+ (custom_colspecs.tr ',', ' ').split[0..2].each_with_index {|c, idx| colspecs[idx] = c }
2121
+ colspecs = { left: colspecs[0], center: colspecs[1], right: colspecs[2] }
2122
+ cml_width = 0
2123
+ side_colspecs = colspecs.map {|col, spec|
2124
+ if (alignment_char = spec.chr).to_i.to_s != alignment_char
2125
+ alignment = AlignmentTable[alignment_char] || :left
2126
+ pcwidth = spec[1..-1].to_f
2127
+ else
2128
+ alignment = :left
2129
+ pcwidth = spec.to_f
2130
+ end
2131
+ # QUESTION should we allow the columns to overlap (capping width at 100%)?
2132
+ if (w = side_trim_content_width * (pcwidth / 100.0)) + cml_width > side_trim_content_width
2133
+ w = side_trim_content_width - cml_width
2134
+ end
2135
+ cml_width += w
2136
+ [col, { align: alignment, width: w, x: 0 }]
2137
+ }.to_h
2138
+ side_colspecs[:right][:x] = (side_colspecs[:center][:x] = side_colspecs[:left][:width]) + side_colspecs[:center][:width]
2139
+ acc[side] = side_colspecs
2140
+ else
2141
+ acc[side] = {
2142
+ left: { align: :left, width: side_trim_content_width, x: 0 },
2143
+ center: { align: :center, width: side_trim_content_width, x: 0 },
2144
+ right: { align: :right, width: side_trim_content_width, x: 0 }
2145
+ }
2146
+ end
2147
+ acc
2148
+ end
2149
+
2150
+ stamps = {}
1960
2151
  if trim_bg_color || trim_border_color
1961
2152
  # NOTE switch to first content page so stamp will get created properly (can't create on imported page)
1962
2153
  prev_page_number = page_number
1963
2154
  go_to_page start
1964
- create_stamp trim_stamp do
1965
- canvas do
1966
- if trim_bg_color
1967
- bounding_box [0, trim_top], width: bounds.width, height: trim_height do
1968
- fill_bounds trim_bg_color
1969
- if trim_border_color
2155
+ PageSides.each do |side|
2156
+ create_stamp trim_stamp_name[side] do
2157
+ canvas do
2158
+ if trim_bg_color
2159
+ bounding_box [0, trim_top], width: bounds.width, height: trim_height do
2160
+ fill_bounds trim_bg_color
2161
+ if trim_border_color
2162
+ # TODO stroke_horizontal_rule should support :at
2163
+ move_down bounds.height if periphery == :header
2164
+ stroke_horizontal_rule trim_border_color, line_width: trim_border_width, line_style: trim_border_style
2165
+ end
2166
+ end
2167
+ else
2168
+ bounding_box [trim_left[side], trim_top], width: trim_width[side], height: trim_height do
1970
2169
  # TODO stroke_horizontal_rule should support :at
1971
- move_down bounds.height if position == :header
2170
+ move_down bounds.height if periphery == :header
1972
2171
  stroke_horizontal_rule trim_border_color, line_width: trim_border_width, line_style: trim_border_style
1973
2172
  end
1974
2173
  end
1975
- else
1976
- bounding_box [trim_left, trim_top], width: trim_width, height: trim_height do
1977
- # TODO stroke_horizontal_rule should support :at
1978
- move_down bounds.height if position == :header
1979
- stroke_horizontal_rule trim_border_color, line_width: trim_border_width, line_style: trim_border_style
1980
- end
1981
2174
  end
1982
2175
  end
1983
2176
  end
1984
- @stamps[position] = true
2177
+ stamps[periphery] = true
1985
2178
  go_to_page prev_page_number
1986
2179
  end
1987
2180
 
1988
2181
  pagenums_enabled = doc.attr? 'pagenums'
2182
+ attribute_missing_doc = doc.attr 'attribute-missing'
1989
2183
  repeat (start..page_count), dynamic: true do
1990
2184
  # NOTE don't write on pages which are imported / inserts (otherwise we can get a corrupt PDF)
1991
2185
  next if page.imported_page?
1992
- visual_pgnum = page_number - skip
2186
+ pgnum_label = page_number - skip
2187
+ # QUESTION should we respect physical page number or just look at the content page number?
2188
+ side = page_side pgnum_label
1993
2189
  # FIXME we need to have a content setting for chapter pages
1994
- content_by_alignment = content_dict[visual_pgnum.odd? ? :recto : :verso]
2190
+ content_by_position = content_dict[side]
2191
+ colspec_by_position = colspec_dict[side]
1995
2192
  # TODO populate chapter-number
1996
2193
  # TODO populate numbered and unnumbered chapter and section titles
1997
2194
  # FIXME leave page-number attribute unset once we filter lines with unresolved attributes (see below)
1998
- doc.set_attr 'page-number', (pagenums_enabled ? visual_pgnum : '')
1999
- doc.set_attr 'chapter-title', (chapters_by_page[visual_pgnum] || '')
2000
- doc.set_attr 'section-title', (sections_by_page[visual_pgnum] || '')
2001
- doc.set_attr 'section-or-chapter-title', (sections_by_page[visual_pgnum] || chapters_by_page[visual_pgnum] || '')
2195
+ doc.set_attr 'page-number', (pagenums_enabled ? pgnum_label : '')
2196
+ doc.set_attr 'chapter-title', (chapters_by_page[pgnum_label] || '')
2197
+ doc.set_attr 'section-title', (sections_by_page[pgnum_label] || '')
2198
+ doc.set_attr 'section-or-chapter-title', (sections_by_page[pgnum_label] || chapters_by_page[pgnum_label] || '')
2002
2199
 
2003
- stamp trim_stamp if @stamps[position]
2200
+ stamp trim_stamp_name[side] if stamps[periphery]
2004
2201
 
2005
- theme_font position do
2202
+ theme_font periphery do
2006
2203
  canvas do
2007
- bounding_box [trim_content_left, trim_top], width: trim_content_width, height: trim_height do
2008
- Alignments.each do |align|
2204
+ bounding_box [trim_content_left[side], trim_top], width: trim_content_width[side], height: trim_height do
2205
+ ColumnPositions.each do |position|
2206
+ next unless (content = content_by_position[position])
2207
+ next unless (colspec = colspec_by_position[position])[:width] > 0
2009
2208
  # FIXME we need to have a content setting for chapter pages
2010
- case (content = content_by_alignment[align])
2209
+ case content
2011
2210
  when ::Hash
2012
- # NOTE image placement respects padding; use negative image_vertical_align value to revert
2211
+ # NOTE image vposition respects padding; use negative image_vertical_align value to revert
2013
2212
  trim_v_padding = trim_padding[0] + trim_padding[2]
2014
2213
  # NOTE float ensures cursor position is restored and returns us to current page if we overrun
2015
2214
  float do
2016
2215
  # NOTE bounding_box is redundant if trim_v_padding is 0
2017
- bounding_box [0, cursor - trim_padding[0]], width: bounds.width, height: (bounds.height - trim_v_padding) do
2018
- #image content[:path], vposition: trim_img_valign, position: align, width: content[:width]
2216
+ bounding_box [colspec[:x], cursor - trim_padding[0]], width: colspec[:width], height: (bounds.height - trim_v_padding) do
2217
+ #image content[:path], vposition: trim_img_valign, position: colspec[:align], width: content[:width]
2019
2218
  # NOTE use :fit to prevent image from overflowing page (at the cost of scaling it)
2020
- image content[:path], vposition: trim_img_valign, position: align, fit: [content[:width], bounds.height]
2219
+ image content[:path], vposition: trim_img_valign, position: colspec[:align], fit: [content[:width], bounds.height]
2021
2220
  end
2022
2221
  end
2023
2222
  when ::String
2024
2223
  if content == '{page-number}'
2025
- content = pagenums_enabled ? visual_pgnum.to_s : nil
2224
+ content = pagenums_enabled ? pgnum_label.to_s : nil
2026
2225
  else
2027
- # FIXME drop lines with unresolved attributes
2028
- content = doc.apply_subs content
2226
+ # FIXME get apply_subs to handle drop-line w/o a warning
2227
+ doc.set_attr 'attribute-missing', 'skip' unless attribute_missing_doc == 'skip'
2228
+ if (content = doc.apply_subs content).include? '{'
2229
+ # NOTE must use &#123; in place of {, not \{, to escape attribute reference
2230
+ content = content.split(LF).delete_if {|line| SimpleAttributeRefRx =~ line } * LF
2231
+ end
2232
+ doc.set_attr 'attribute-missing', attribute_missing_doc unless attribute_missing_doc == 'skip'
2233
+ end
2234
+ theme_font %(#{periphery}_#{side}_#{position}) do
2235
+ formatted_text_box parse_text(content, color: @font_color, inline_format: [normalize: true]),
2236
+ at: [colspec[:x], trim_content_height + trim_padding[2] + trim_line_metrics.padding_bottom],
2237
+ width: colspec[:width],
2238
+ height: trim_content_height,
2239
+ align: colspec[:align],
2240
+ valign: trim_valign,
2241
+ leading: trim_line_metrics.leading,
2242
+ final_gap: false,
2243
+ overflow: :truncate
2029
2244
  end
2030
- formatted_text_box parse_text(content, color: trim_font_color, inline_format: [normalize: true]),
2031
- at: [0, trim_content_height + trim_padding[2] + trim_line_metrics.padding_bottom],
2032
- height: trim_content_height,
2033
- align: align,
2034
- valign: trim_valign,
2035
- leading: trim_line_metrics.leading,
2036
- final_gap: false,
2037
- overflow: :truncate
2038
2245
  end
2039
2246
  end
2040
2247
  end
2041
2248
  end
2042
2249
  end
2043
2250
  end
2251
+ nil
2044
2252
  end
2045
2253
 
2046
- # FIXME we are assuming we always have exactly one title page
2047
- def add_outline doc, num_levels = 2, toc_page_nums = (0..-1), num_front_matter_pages = 0
2254
+ def add_outline doc, num_levels = 2, toc_page_nums = [], num_front_matter_pages = 0
2048
2255
  front_matter_counter = RomanNumeral.new 0, :lower
2049
-
2050
2256
  page_num_labels = {}
2051
2257
 
2052
- # FIXME account for cover page
2053
- # cover page (i)
2054
- #front_matter_counter.next!
2055
-
2056
- # title page (i)
2057
- # TODO same conditional logic as in layout_title_page; consolidate
2058
- if doc.header? && !doc.notitle
2059
- page_num_labels[0] = { P: ::PDF::Core::LiteralString.new(front_matter_counter.next!.to_s) }
2060
- end
2061
-
2062
- # toc pages (ii..?)
2063
- toc_page_nums.each do
2258
+ num_front_matter_pages.times do
2064
2259
  page_num_labels[front_matter_counter.to_i] = { P: ::PDF::Core::LiteralString.new(front_matter_counter.next!.to_s) }
2065
2260
  end
2066
2261
 
2067
- # credits page
2068
- #page_num_labels[front_matter_counter.to_i] = { P: ::PDF::Core::LiteralString.new(front_matter_counter.next!.to_s) }
2069
-
2070
- # number of front matter pages aside from the document title to skip in page number index
2071
- numbering_offset = front_matter_counter.to_i - 1
2262
+ # placeholder for first page of content, in case it's not the destination of an outline entry
2263
+ page_num_labels[front_matter_counter.to_i] = { P: ::PDF::Core::LiteralString.new('1') }
2072
2264
 
2073
2265
  outline.define do
2074
2266
  # FIXME use sanitize: :plain_text once available
2075
2267
  if (doctitle = document.sanitize(doc.doctitle use_fallback: true))
2268
+ # FIXME link to title page if there's a cover page (skip cover page and ensuing blank page)
2076
2269
  page title: doctitle, destination: (document.dest_top 1)
2077
2270
  end
2078
- if doc.attr? 'toc'
2079
- page title: doc.attr('toc-title'), destination: (document.dest_top toc_page_nums.first)
2080
- end
2081
- #page title: 'Credits', destination: (document.dest_top toc_page_nums.first + 1)
2271
+ page title: (doc.attr 'toc-title'), destination: (document.dest_top toc_page_nums.first) if toc_page_nums.first
2082
2272
  # QUESTION any way to get add_outline_level to invoke in the context of the outline?
2083
- document.add_outline_level self, doc.sections, num_levels, page_num_labels, numbering_offset, num_front_matter_pages
2273
+ document.add_outline_level self, doc.sections, num_levels, page_num_labels, num_front_matter_pages
2084
2274
  end
2085
2275
 
2086
2276
  catalog.data[:PageLabels] = state.store.ref Nums: page_num_labels.flatten
@@ -2089,17 +2279,17 @@ class Converter < ::Prawn::Document
2089
2279
  end
2090
2280
 
2091
2281
  # TODO only nest inside root node if doctype=article
2092
- def add_outline_level outline, sections, num_levels, page_num_labels, numbering_offset, num_front_matter_pages
2282
+ def add_outline_level outline, sections, num_levels, page_num_labels, num_front_matter_pages
2093
2283
  sections.each do |sect|
2094
2284
  sect_title = sanitize sect.numbered_title formal: true
2095
2285
  sect_destination = sect.attr 'pdf-destination'
2096
- sect_page_num = (sect.attr 'page_start') - num_front_matter_pages
2097
- page_num_labels[sect_page_num + numbering_offset] = { P: ::PDF::Core::LiteralString.new(sect_page_num.to_s) }
2286
+ sect_pgnum_label = (sect_pgnum = sect.attr 'pdf-page-start') - num_front_matter_pages
2287
+ page_num_labels[sect_pgnum - 1] = { P: ::PDF::Core::LiteralString.new(sect_pgnum_label.to_s) }
2098
2288
  if (subsections = sect.sections).empty? || sect.level == num_levels
2099
2289
  outline.page title: sect_title, destination: sect_destination
2100
2290
  elsif sect.level < num_levels + 1
2101
2291
  outline.section sect_title, { destination: sect_destination } do
2102
- add_outline_level outline, subsections, num_levels, page_num_labels, numbering_offset, num_front_matter_pages
2292
+ add_outline_level outline, subsections, num_levels, page_num_labels, num_front_matter_pages
2103
2293
  end
2104
2294
  end
2105
2295
  end
@@ -2166,9 +2356,9 @@ class Converter < ::Prawn::Document
2166
2356
  margin y, :bottom
2167
2357
  end
2168
2358
 
2169
- # Insert a margin space of type position unless cursor is at the top of the page.
2359
+ # Insert a margin space at the specified side unless cursor is at the top of the page.
2170
2360
  # Start a new page if y value is greater than remaining space on page.
2171
- def margin y, position
2361
+ def margin y, side
2172
2362
  unless y == 0 || at_page_top?
2173
2363
  if cursor > y
2174
2364
  move_down y
@@ -2180,12 +2370,12 @@ class Converter < ::Prawn::Document
2180
2370
  end
2181
2371
  end
2182
2372
 
2183
- # Lookup margin for theme element and position, then delegate to margin method.
2373
+ # Lookup margin for theme element and side, then delegate to margin method.
2184
2374
  # If margin value is not found, assume:
2185
- # - 0 when position = :top
2186
- # - @theme.vertical_spacing when position = :bottom
2187
- def theme_margin category, position
2188
- margin (@theme[%(#{category}_margin_#{position})] || (position == :bottom ? @theme.vertical_spacing : 0)), position
2375
+ # - 0 when side == :top
2376
+ # - @theme.vertical_spacing when side == :bottom
2377
+ def theme_margin category, side
2378
+ margin (@theme[%(#{category}_margin_#{side})] || (side == :bottom ? @theme.vertical_spacing : 0)), side
2189
2379
  end
2190
2380
 
2191
2381
  def theme_font category, opts = {}
@@ -2206,12 +2396,10 @@ class Converter < ::Prawn::Document
2206
2396
  transform = @theme[%(#{category}_text_transform)]
2207
2397
  end
2208
2398
 
2209
- style = style.to_sym if style
2210
-
2211
2399
  prev_color, @font_color = @font_color, color if color
2212
2400
  prev_transform, @text_transform = @text_transform, transform if transform
2213
2401
 
2214
- font family, size: size, style: style do
2402
+ font family, size: size, style: (style && style.to_sym) do
2215
2403
  yield
2216
2404
  end
2217
2405
 
@@ -2254,11 +2442,11 @@ class Converter < ::Prawn::Document
2254
2442
  arranger = ::Prawn::Text::Formatted::Arranger.new self
2255
2443
  by_line = arranger.consumed = []
2256
2444
  fragments.each do |fragment|
2257
- if (txt = fragment[:text]) == EOL
2445
+ if (txt = fragment[:text]) == LF
2258
2446
  by_line << fragment
2259
- elsif txt.include? EOL
2447
+ elsif txt.include? LF
2260
2448
  txt.scan(LineScanRx) do |line|
2261
- by_line << (line == EOL ? { text: EOL } : (fragment.merge text: line))
2449
+ by_line << (line == LF ? { text: LF } : (fragment.merge text: line))
2262
2450
  end
2263
2451
  else
2264
2452
  by_line << fragment
@@ -2275,7 +2463,7 @@ class Converter < ::Prawn::Document
2275
2463
  def width_of_fragments fragments
2276
2464
  line_widths = [0]
2277
2465
  fragments.each do |fragment|
2278
- if fragment.text == EOL
2466
+ if fragment.text == LF
2279
2467
  line_widths << 0
2280
2468
  else
2281
2469
  line_widths[-1] += fragment.width
@@ -2326,8 +2514,8 @@ class Converter < ::Prawn::Document
2326
2514
  line.sub!(TabIndentRx) {|tabs| full_tab_space * tabs.length }
2327
2515
  end
2328
2516
  leading_space = false
2329
- # QUESTION should we check for EOL first?
2330
- elsif line == EOL
2517
+ # QUESTION should we check for LF first?
2518
+ elsif line == LF
2331
2519
  result << line
2332
2520
  next
2333
2521
  else
@@ -2365,6 +2553,17 @@ class Converter < ::Prawn::Document
2365
2553
  end
2366
2554
  end
2367
2555
 
2556
+ # Derive a PDF-safe, ASCII-only anchor name from the given value.
2557
+ # Encodes value into hex if it contains characters outside the ASCII range.
2558
+ # If value is nil, derive an anchor name from the default_value, if given.
2559
+ def derive_anchor_from_id value, default_value = nil
2560
+ if value
2561
+ value.ascii_only? ? value : %(0x#{::PDF::Core.string_to_hex value})
2562
+ elsif default_value
2563
+ %(__anchor-#{default_value})
2564
+ end
2565
+ end
2566
+
2368
2567
  # If an id is provided or the node passed as the first argument has an id,
2369
2568
  # add a named destination to the document equivalent to the node id at the
2370
2569
  # current y position. If the node does not have an id and an id is not
@@ -2378,7 +2577,7 @@ class Converter < ::Prawn::Document
2378
2577
  # QUESTION should we set precise x value of destination or just 0?
2379
2578
  dest_x = bounds.absolute_left.round 2
2380
2579
  dest_x = 0 if dest_x <= page_margin_left
2381
- dest_y = if node.context == :section && at_page_top?
2580
+ dest_y = if at_page_top? && (node.context == :section || node.context == :document)
2382
2581
  page_height
2383
2582
  else
2384
2583
  y
@@ -2412,17 +2611,16 @@ class Converter < ::Prawn::Document
2412
2611
  # is not set, or the URI cannot be read, this method returns a nil value.
2413
2612
  #
2414
2613
  # When a temporary file is used, the TemporaryPath type is mixed into the path string.
2415
- def resolve_image_path node, image_path = nil, image_type = nil
2416
- imagesdir = resolve_imagesdir(doc = node.document)
2417
- image_path ||= (node.attr 'target', nil, false)
2418
- image_type ||= ::Asciidoctor::Image.image_type image_path
2614
+ def resolve_image_path node, image_path = nil, relative_to_imagesdir = true, image_format = nil
2615
+ doc = node.document
2616
+ imagesdir = relative_to_imagesdir ? (resolve_imagesdir doc) : nil
2617
+ image_path ||= node.attr 'target'
2618
+ image_format ||= ::Asciidoctor::Image.format image_path, (::Asciidoctor::Image === node ? node : nil)
2419
2619
  # handle case when image is a URI
2420
2620
  if (node.is_uri? image_path) || (imagesdir && (node.is_uri? imagesdir) &&
2421
2621
  (image_path = (node.normalize_web_path image_path, imagesdir, false)))
2422
2622
  unless doc.attr? 'allow-uri-read'
2423
- unless scratch?
2424
- warn %(asciidoctor: WARNING: allow-uri-read is not enabled; cannot embed remote image: #{image_path})
2425
- end
2623
+ warn %(asciidoctor: WARNING: allow-uri-read is not enabled; cannot embed remote image: #{image_path}) unless scratch?
2426
2624
  return
2427
2625
  end
2428
2626
  if doc.attr? 'cache-uri'
@@ -2430,8 +2628,8 @@ class Converter < ::Prawn::Document
2430
2628
  else
2431
2629
  ::OpenURI
2432
2630
  end
2433
- tmp_image = ::Tempfile.new ['image-', %(.#{image_type})]
2434
- tmp_image.binmode if (binary = image_type != 'svg')
2631
+ tmp_image = ::Tempfile.new ['image-', image_format && %(.#{image_format})]
2632
+ tmp_image.binmode if (binary = image_format != 'svg')
2435
2633
  begin
2436
2634
  open(image_path, (binary ? 'rb' : 'r')) {|fd| tmp_image.write(fd.read) }
2437
2635
  tmp_image_path = tmp_image.path
@@ -2459,15 +2657,19 @@ class Converter < ::Prawn::Document
2459
2657
 
2460
2658
  if (bg_image.include? ':') && bg_image =~ ImageAttributeValueRx
2461
2659
  # QUESTION should we support width and height in this case?
2660
+ # TODO support explicit format
2462
2661
  bg_image = $1
2662
+ relative_to_imagesdir = true
2663
+ else
2664
+ relative_to_imagesdir = false
2463
2665
  end
2464
2666
 
2465
- if (bg_image = doc_attr_val ? (resolve_image_path doc, bg_image) :
2667
+ if (bg_image = doc_attr_val ? (resolve_image_path doc, bg_image, relative_to_imagesdir) :
2466
2668
  (ThemeLoader.resolve_theme_asset bg_image, (doc.attr 'pdf-stylesdir')))
2467
2669
  if ::File.readable? bg_image
2468
2670
  bg_image
2469
2671
  else
2470
- warn %(asciidoctor: WARNING: #{key.tr '-', ' '} #{bg_image} not found or readable)
2672
+ warn %(asciidoctor: WARNING: #{key.tr '-', ' '} not found or readable: #{bg_image})
2471
2673
  nil
2472
2674
  end
2473
2675
  end
@@ -2483,20 +2685,28 @@ class Converter < ::Prawn::Document
2483
2685
  # max_width, the max_width value is returned.
2484
2686
  #--
2485
2687
  # QUESTION should we enforce positive result?
2486
- def resolve_explicit_width attrs, max_width = bounds.width
2688
+ def resolve_explicit_width attrs, max_width = bounds.width, opts = {}
2487
2689
  if attrs.key? 'pdfwidth'
2488
- if (pdfwidth = attrs['pdfwidth']).end_with? '%'
2489
- (pdfwidth.to_f / 100) * max_width
2490
- elsif pdfwidth.end_with? 'vw'
2491
- (pdfwidth.to_f / 100) * page_width
2690
+ if (width = attrs['pdfwidth']).end_with? '%'
2691
+ (width.to_f / 100) * max_width
2692
+ elsif opts[:support_vw] && (width.end_with? 'vw')
2693
+ (width.chomp 'vw').extend ViewportWidth
2492
2694
  else
2493
- str_to_pt pdfwidth
2695
+ str_to_pt width
2494
2696
  end
2495
2697
  elsif attrs.key? 'scaledwidth'
2496
2698
  (attrs['scaledwidth'].to_f / 100) * max_width
2699
+ elsif opts[:use_fallback] && (width = @theme.image_width)
2700
+ if width.end_with? '%'
2701
+ (width.to_f / 100) * max_width
2702
+ elsif opts[:support_vw] && (width.end_with? 'vw')
2703
+ (width.chomp 'vw').extend ViewportWidth
2704
+ else
2705
+ str_to_pt width
2706
+ end
2497
2707
  elsif attrs.key? 'width'
2498
2708
  # QUESTION should we honor percentage width value?
2499
- # NOTE scale width down 75% to convert px to pt; restrict width to bounds.width
2709
+ # NOTE scale width down 75% to convert px to pt; restrict width to max width
2500
2710
  [max_width, attrs['width'].to_f * 0.75].min
2501
2711
  end
2502
2712
  end
@@ -2507,6 +2717,8 @@ class Converter < ::Prawn::Document
2507
2717
  # NOTE Ruby 1.9 will sometimes delete a tmp file before the process exits
2508
2718
  def unlink_tmp_file path
2509
2719
  path.unlink if TemporaryPath === path && path.exist?
2720
+ rescue => e
2721
+ warn %(asciidoctor: WARNING: could not delete temporary image: #{path}; #{e.message})
2510
2722
  end
2511
2723
 
2512
2724
  # QUESTION move to prawn/extensions.rb?
@@ -2514,11 +2726,12 @@ class Converter < ::Prawn::Document
2514
2726
  # IMPORTANT don't set font before using Marshal, it causes serialization to fail
2515
2727
  @prototype = ::Marshal.load ::Marshal.dump self
2516
2728
  @prototype.state.store.info.data[:Scratch] = true
2517
- # we're now starting a new page each time, so no need to do it here
2729
+ # NOTE we're now starting a new page each time, so no need to do it here
2518
2730
  #@prototype.start_new_page if @prototype.page_number == 0
2519
2731
  end
2520
2732
 
2521
2733
  =begin
2734
+ # TODO could assign pdf-anchor attributes here too
2522
2735
  def assign_missing_section_ids doc
2523
2736
  unless doc.attr? 'sectids'
2524
2737
  doc.attributes['sectids'] = ''