asciidoctor-pdf 1.5.0.alpha.8 → 1.5.0.alpha.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +9 -1
  3. data/Rakefile +1 -0
  4. data/data/fonts/{LICENSE-noto-fonts-2014-11-17 → LICENSE-noto-2015-06-05} +0 -0
  5. data/data/fonts/{mplus1mn-bolditalic-ascii.ttf → mplus1mn-bold_italic-ascii.ttf} +0 -0
  6. data/data/fonts/{mplus1p-regular-multilingual.ttf → mplus1p-regular-fallback.ttf} +0 -0
  7. data/data/fonts/notoserif-bold-subset.ttf +0 -0
  8. data/data/fonts/notoserif-bold_italic-subset.ttf +0 -0
  9. data/data/fonts/notoserif-italic-subset.ttf +0 -0
  10. data/data/fonts/notoserif-regular-subset.ttf +0 -0
  11. data/data/themes/base-theme.yml +90 -0
  12. data/data/themes/default-theme.yml +97 -92
  13. data/docs/theming-guide.adoc +93 -21
  14. data/lib/asciidoctor-pdf/converter.rb +419 -216
  15. data/lib/asciidoctor-pdf/core_ext/quantifiable_stdout.rb +20 -0
  16. data/lib/asciidoctor-pdf/formatted_text/formatter.rb +6 -5
  17. data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +3 -3
  18. data/lib/asciidoctor-pdf/formatted_text/transform.rb +36 -26
  19. data/lib/asciidoctor-pdf/pdf_core_ext.rb +1 -1
  20. data/lib/asciidoctor-pdf/pdf_core_ext/page.rb +22 -0
  21. data/lib/asciidoctor-pdf/prawn_ext.rb +1 -0
  22. data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +24 -4
  23. data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +73 -6
  24. data/lib/asciidoctor-pdf/prawn_ext/font/afm.rb +19 -0
  25. data/lib/asciidoctor-pdf/prawn_ext/formatted_text/fragment.rb +1 -1
  26. data/lib/asciidoctor-pdf/prawn_ext/images.rb +16 -0
  27. data/lib/asciidoctor-pdf/rouge_ext.rb +4 -0
  28. data/lib/asciidoctor-pdf/rouge_ext/css_theme.rb +14 -0
  29. data/lib/asciidoctor-pdf/rouge_ext/formatters/prawn.rb +121 -0
  30. data/lib/asciidoctor-pdf/rouge_ext/themes/pastie.rb +61 -0
  31. data/lib/asciidoctor-pdf/theme_loader.rb +32 -14
  32. data/lib/asciidoctor-pdf/version.rb +1 -1
  33. metadata +23 -15
  34. data/data/fonts/notoserif-bold-latin.ttf +0 -0
  35. data/data/fonts/notoserif-bolditalic-latin.ttf +0 -0
  36. data/data/fonts/notoserif-italic-latin.ttf +0 -0
  37. data/data/fonts/notoserif-regular-latin.ttf +0 -0
@@ -27,13 +27,11 @@ class Converter < ::Prawn::Document
27
27
 
28
28
  register_for 'pdf'
29
29
 
30
- def self.unicode_char number
31
- [number].pack 'U*'
32
- end
33
-
34
30
  # NOTE require_library doesn't support require_relative and we don't modify the load path for this gem
35
- CodeRayRequirePath = ::File.join((::File.dirname __FILE__), 'prawn_ext/coderay_encoder')
31
+ CodeRayRequirePath = ::File.join (::File.dirname __FILE__), 'prawn_ext/coderay_encoder'
32
+ RougeRequirePath = ::File.join (::File.dirname __FILE__), 'rouge_ext'
36
33
 
34
+ AsciidoctorVersion = ::Gem::Version.create ::Asciidoctor::VERSION
37
35
  AdmonitionIcons = {
38
36
  caution: { key: 'fa-fire', color: 'BF3400' },
39
37
  important: { key: 'fa-exclamation-circle', color: 'BF0000' },
@@ -42,27 +40,34 @@ class Converter < ::Prawn::Document
42
40
  warning: { key: 'fa-exclamation-triangle', color: 'BF6900' }
43
41
  }
44
42
  Alignments = [:left, :center, :right]
45
- IndentationRx = /^ +/
46
- TabSpaces = ' ' * 4
47
- NoBreakSpace = unicode_char 0x00a0
48
- NarrowSpace = unicode_char 0x2009
49
- NarrowNoBreakSpace = unicode_char 0x202f
50
- ZeroWidthSpace = unicode_char 0x200b
51
- HairSpace = unicode_char 0x200a
52
- DotLeaderDefault = %(. )
53
- EmDash = unicode_char 0x2014
54
- LowercaseGreekA = unicode_char 0x03b1
43
+ EOL = %(\n)
44
+ TAB = %(\t)
45
+ InnerIndent = %(\n )
46
+ # a no-break space is used to replace a leading space to prevent Prawn from trimming indentation
47
+ # a leading zero-width space can't be used as it gets dropped when calculating the line width
48
+ GuardedIndent = %(\u00a0)
49
+ GuardedInnerIndent = %(\n\u00a0)
50
+ TabRx = /\t/
51
+ TabIndentRx = /^\t+/
52
+ NoBreakSpace = %(\u00a0)
53
+ NarrowSpace = %(\u2009)
54
+ NarrowNoBreakSpace = %(\u202f)
55
+ ZeroWidthSpace = %(\u200b)
56
+ HairSpace = %(\u200a)
57
+ DotLeaderDefault = '. '
58
+ EmDash = %(\u2014)
59
+ LowercaseGreekA = %(\u03b1)
55
60
  Bullets = {
56
- disc: (unicode_char 0x2022),
57
- circle: (unicode_char 0x25e6),
58
- square: (unicode_char 0x25aa)
61
+ disc: %(\u2022),
62
+ circle: %(\u25e6),
63
+ square: %(\u25aa)
59
64
  }
60
65
  # NOTE Default theme font uses ballot boxes from FontAwesome
61
66
  BallotBox = {
62
- checked: (unicode_char 0x2611),
63
- unchecked: (unicode_char 0x2610)
67
+ checked: %(\u2611),
68
+ unchecked: %(\u2610)
64
69
  }
65
- IconSets = ['fa', 'fi', 'octicon', 'pf']
70
+ IconSets = ['fa', 'fi', 'octicon', 'pf'].to_set
66
71
  MeasurementRxt = '\\d+(?:\\.\\d+)?(?:in|cm|mm|pt|)'
67
72
  MeasurementPartsRx = /^(\d+(?:\.\d+)?)(in|mm|cm|pt|)$/
68
73
  PageSizeRx = /^(?:\[(#{MeasurementRxt}), ?(#{MeasurementRxt})\]|(#{MeasurementRxt})(?: x |x)(#{MeasurementRxt})|\S+)$/
@@ -70,6 +75,7 @@ class Converter < ::Prawn::Document
70
75
  CalloutExtractRx = /(?:(?:\/\/|#|--|;;) ?)?(\\)?<!?(--|)(\d+)\2>(?=(?: ?\\?<!?\2\d+\2>)*$)/
71
76
  ImageAttributeValueRx = /^image:{1,2}(.*?)\[(.*?)\]$/
72
77
  LineScanRx = /\n|.+/
78
+ SourceHighlighters = ['coderay', 'pygments', 'rouge'].to_set
73
79
 
74
80
  def initialize backend, opts
75
81
  super
@@ -78,6 +84,9 @@ class Converter < ::Prawn::Document
78
84
  #htmlsyntax 'xml'
79
85
  @list_numbers = []
80
86
  @list_bullets = []
87
+ @capabilities = {
88
+ expands_tabs: (::Asciidoctor::VERSION.start_with? '1.5.3.') || AsciidoctorVersion >= (::Gem::Version.create '1.5.3')
89
+ }
81
90
  end
82
91
 
83
92
  def convert node, name = nil, opts = {}
@@ -113,14 +122,19 @@ class Converter < ::Prawn::Document
113
122
  init_pdf doc
114
123
  # data-uri doesn't apply to PDF, so explicitly disable (is there a better place?)
115
124
  doc.attributes.delete 'data-uri'
125
+ # set default value for pagenums if not otherwise set
126
+ unless (doc.attribute_locked? 'pagenums') || ((doc.instance_variable_get :@attributes_modified).include? 'pagenums')
127
+ doc.attributes['pagenums'] = ''
128
+ end
116
129
  #assign_missing_section_ids doc
117
130
 
131
+ # NOTE the on_page_create callback is called within a float context
118
132
  on_page_create do
119
133
  # TODO implement as a watermark (on top)
120
134
  if @page_bg_image
121
135
  # FIXME implement fitting and centering for SVG
122
136
  # TODO implement image scaling (numeric value or "fit")
123
- float { canvas { image @page_bg_image, position: :center, fit: [bounds.width, bounds.height] } }
137
+ canvas { image @page_bg_image, position: :center, fit: [bounds.width, bounds.height] }
124
138
  elsif @page_bg_color && @page_bg_color != 'FFFFFF'
125
139
  fill_absolute_bounds @page_bg_color
126
140
  end
@@ -129,7 +143,8 @@ class Converter < ::Prawn::Document
129
143
  layout_cover_page :front, doc
130
144
  layout_title_page doc
131
145
 
132
- start_new_page
146
+ # NOTE a new page will already be started if the cover image is a PDF
147
+ start_new_page unless page_is_empty?
133
148
 
134
149
  toc_start_page_num = page_number
135
150
  num_toc_levels = (doc.attr 'toclevels', 2).to_i
@@ -148,17 +163,18 @@ class Converter < ::Prawn::Document
148
163
  font @theme.base_font_family, size: @theme.base_font_size
149
164
  convert_content_for_block doc
150
165
 
166
+ # NOTE delete orphaned page (a page was created but there was no additional content)
167
+ delete_page if page_is_empty?
168
+
151
169
  toc_page_nums = if include_toc
152
170
  layout_toc doc, num_toc_levels, toc_start_page_num, num_front_matter_pages
153
171
  else
154
172
  (0..-1)
155
173
  end
156
174
 
157
- # TODO enable pagenums by default
158
- if doc.attr? 'pagenums'
159
- layout_running_content :header, doc, skip: num_front_matter_pages
160
- layout_running_content :footer, doc, skip: num_front_matter_pages
161
- end
175
+ layout_running_content :header, doc, skip: num_front_matter_pages unless doc.noheader
176
+ layout_running_content :footer, doc, skip: num_front_matter_pages unless doc.nofooter
177
+
162
178
  add_outline doc, num_toc_levels, toc_page_nums, num_front_matter_pages
163
179
  catalog.data[:ViewerPreferences] = [:FitWindow]
164
180
 
@@ -178,19 +194,18 @@ class Converter < ::Prawn::Document
178
194
  theme = ThemeLoader.load_theme doc.attr('pdf-style'), (stylesdir = (doc.attr 'pdf-stylesdir'))
179
195
  @theme = theme
180
196
  pdf_opts = (build_pdf_options doc, theme)
197
+ # QUESTION should we preserve page options (otherwise, not readily available)
198
+ #@page_opts = { size: pdf_opts[:page_size], layout: pdf_opts[:page_layout] }
181
199
  ::Prawn::Document.instance_method(:initialize).bind(self).call pdf_opts
182
200
  # QUESTION should ThemeLoader register fonts?
183
201
  register_fonts theme.font_catalog, (doc.attr 'scripts', 'latin'), (doc.attr 'pdf-fontsdir', ThemeLoader::FontsDir)
184
- if (bg_image = theme.page_background_image) && bg_image != 'none'
185
- if ::File.readable?(bg_image = (ThemeLoader.resolve_theme_asset bg_image, stylesdir))
186
- @page_bg_image = bg_image
187
- else
188
- warn %(asciidoctor: WARNING: page background image #{bg_image} not found or readable)
189
- end
202
+ @page_bg_image = nil
203
+ if (bg_image = resolve_background_image doc, theme, 'page-background-image')
204
+ @page_bg_image = (bg_image == 'none' ? nil : bg_image)
190
205
  end
191
- @fallback_fonts = [*theme.font_fallbacks]
192
206
  @page_bg_color = resolve_theme_color :page_background_color, 'FFFFFF'
193
- @font_color = theme.base_font_color || '000000'
207
+ @fallback_fonts = [*theme.font_fallbacks]
208
+ @font_color = theme.base_font_color
194
209
  @text_transform = nil
195
210
  @stamps = {}
196
211
  init_scratch_prototype
@@ -202,8 +217,8 @@ class Converter < ::Prawn::Document
202
217
  #compress: true,
203
218
  #optimize_objects: true,
204
219
  info: (build_pdf_info doc),
205
- margin: (theme.page_margin || 36),
206
- page_layout: (theme.page_layout || :portrait).to_sym,
220
+ margin: theme.page_margin,
221
+ page_layout: theme.page_layout.to_sym,
207
222
  skip_page_creation: true,
208
223
  }
209
224
 
@@ -391,19 +406,24 @@ class Converter < ::Prawn::Document
391
406
  theme_margin :block, :top
392
407
  icons = node.document.attr? 'icons', 'font'
393
408
  label = icons ? (node.attr 'name').to_sym : node.caption.upcase
394
- shift_base = @theme.prose_margin_bottom || @theme.vertical_rhythm
409
+ # FIXME this shift stuff is a real hack until we have proper margin collapsing
410
+ shift_base = @theme.prose_margin_bottom
395
411
  #shift_top = icons ? (shift_base / 3.0) : 0
396
412
  #shift_bottom = icons ? ((shift_base * 2) / 3.0) : shift_base
397
413
  shift_top = shift_base / 3.0
398
414
  shift_bottom = (shift_base * 2) / 3.0
399
415
  keep_together do |box_height = nil|
400
416
  #theme_font :admonition do
417
+ # FIXME this is a fudge calculation for the icon width
401
418
  label_width = icons ? (bounds.width / 12.0) : (width_of label)
402
- # FIXME use padding from theme
403
- indent @theme.horizontal_rhythm, @theme.horizontal_rhythm do
419
+ abs_left = bounds.absolute_left
420
+ abs_right = bounds.absolute_right
421
+ pad_box @theme.admonition_padding do
422
+ left_padding = bounds.absolute_left - abs_left
423
+ right_padding = abs_right - bounds.absolute_right
404
424
  if box_height
405
425
  float do
406
- bounding_box [0, cursor], width: label_width + @theme.horizontal_rhythm, height: box_height do
426
+ bounding_box [0, cursor], width: label_width + right_padding, height: box_height do
407
427
  # IMPORTANT the label must fit in the alotted space or it shows up on another page!
408
428
  # QUESTION anyway to prevent text overflow in the case it doesn't fit?
409
429
  stroke_vertical_rule @theme.admonition_border_color, at: bounds.width
@@ -418,7 +438,7 @@ class Converter < ::Prawn::Document
418
438
  end
419
439
  end
420
440
  end
421
- indent label_width + @theme.horizontal_rhythm * 2 do
441
+ indent label_width + left_padding + right_padding do
422
442
  move_down shift_top
423
443
  layout_caption node.title if node.title?
424
444
  convert_content_for_block node
@@ -443,7 +463,7 @@ class Converter < ::Prawn::Document
443
463
  end
444
464
  end
445
465
  end
446
- pad_box [@theme.vertical_rhythm, @theme.horizontal_rhythm, 0, @theme.horizontal_rhythm] do
466
+ pad_box @theme.example_padding do
447
467
  theme_font :example do
448
468
  convert_content_for_block node
449
469
  end
@@ -472,17 +492,17 @@ class Converter < ::Prawn::Document
472
492
 
473
493
  def convert_quote_or_verse node
474
494
  add_dest_for_block node if node.id
475
- border_width = @theme.blockquote_border_width || 0
495
+ border_width = @theme.blockquote_border_width
476
496
  theme_margin :block, :top
477
497
  keep_together do |box_height = nil|
478
498
  start_cursor = cursor
479
- # FIXME use padding from theme
480
- pad_box [@theme.vertical_rhythm / 2.0, @theme.horizontal_rhythm, -(@theme.vertical_rhythm / 2.0), @theme.horizontal_rhythm + border_width / 2.0] do
499
+ pad_box @theme.blockquote_padding do
481
500
  theme_font :blockquote do
482
501
  if node.context == :quote
483
502
  convert_content_for_block node
484
503
  else # verse
485
- layout_prose node.content, preserve: true, normalize: false, align: :left
504
+ content = preserve_indentation node.content, (node.attr 'tabsize')
505
+ layout_prose content, normalize: false, align: :left
486
506
  end
487
507
  end
488
508
  theme_font :blockquote_cite do
@@ -517,7 +537,7 @@ class Converter < ::Prawn::Document
517
537
  end
518
538
  end
519
539
  end
520
- pad_box @theme.block_padding do
540
+ pad_box @theme.sidebar_padding do
521
541
  if node.title?
522
542
  theme_font :sidebar_title do
523
543
  # QUESTION should we allow margins of sidebar title to be customized?
@@ -527,8 +547,6 @@ class Converter < ::Prawn::Document
527
547
  theme_font :sidebar do
528
548
  convert_content_for_block node
529
549
  end
530
- # FIXME HACK compensate for margin bottom of sidebar content
531
- move_up(@theme.prose_margin_bottom || @theme.vertical_rhythm)
532
550
  end
533
551
  end
534
552
  theme_margin :block, :bottom
@@ -541,10 +559,10 @@ class Converter < ::Prawn::Document
541
559
  # NOTE this logic won't work for a colist nested inside a list item until Asciidoctor 1.5.3
542
560
  if (self_idx = node.parent.blocks.index node) && self_idx > 0 &&
543
561
  [:listing, :literal].include?(node.parent.blocks[self_idx - 1].context)
544
- move_up ((@theme.block_margin_bottom || @theme.vertical_rhythm) / 2.0)
562
+ move_up @theme.block_margin_bottom / 2.0
545
563
  # or we could do...
546
- #move_up (@theme.block_margin_bottom || @theme.vertical_rhythm)
547
- #move_down (@theme.caption_margin_inside * 2)
564
+ #move_up @theme.block_margin_bottom
565
+ #move_down @theme.caption_margin_inside * 2
548
566
  end
549
567
  end
550
568
  add_dest_for_block node if node.id
@@ -561,8 +579,8 @@ class Converter < ::Prawn::Document
561
579
  end
562
580
  @list_numbers.pop
563
581
  # correct bottom margin of last item
564
- list_margin_bottom = @theme.prose_margin_bottom || @theme.vertical_rhythm
565
- margin_bottom (list_margin_bottom - (@theme.outline_list_item_spacing || (list_margin_bottom / 2.0)))
582
+ list_margin_bottom = @theme.prose_margin_bottom
583
+ margin_bottom list_margin_bottom - @theme.outline_list_item_spacing
566
584
  end
567
585
 
568
586
  def convert_colist_item node
@@ -579,7 +597,7 @@ class Converter < ::Prawn::Document
579
597
 
580
598
  indent marker_width do
581
599
  convert_content_for_list_item node,
582
- margin_bottom: (@theme.outline_list_item_spacing || ((@theme.prose_margin_bottom || @theme.vertical_rhythm) / 2.0))
600
+ margin_bottom: @theme.outline_list_item_spacing
583
601
  end
584
602
  end
585
603
 
@@ -591,7 +609,7 @@ class Converter < ::Prawn::Document
591
609
  # FIXME extract ensure_space (or similar) method
592
610
  start_new_page if cursor < @theme.base_line_height_length * (terms.size + 1)
593
611
  terms.each do |term|
594
- layout_prose term.text, style: (@theme.description_list_term_font_style || :normal).to_sym, margin_top: 0, margin_bottom: (@theme.vertical_rhythm / 3.0), align: :left
612
+ layout_prose term.text, style: @theme.description_list_term_font_style.to_sym, margin_top: 0, margin_bottom: @theme.description_list_term_spacing, align: :left
595
613
  end
596
614
  if desc
597
615
  indent @theme.description_list_description_indent do
@@ -674,8 +692,8 @@ class Converter < ::Prawn::Document
674
692
  # However, don't leave gap at the bottom of a nested list
675
693
  unless complex || (::Asciidoctor::List === node.parent && node.parent.outline?)
676
694
  # correct bottom margin of last item
677
- list_margin_bottom = @theme.prose_margin_bottom || @theme.vertical_rhythm
678
- margin_bottom(list_margin_bottom - (@theme.outline_list_item_spacing || (list_margin_bottom / 2.0)))
695
+ list_margin_bottom = @theme.prose_margin_bottom
696
+ margin_bottom list_margin_bottom - @theme.outline_list_item_spacing
679
697
  end
680
698
  end
681
699
 
@@ -721,7 +739,7 @@ class Converter < ::Prawn::Document
721
739
  convert_content_for_list_item node
722
740
  else
723
741
  convert_content_for_list_item node,
724
- margin_bottom: (@theme.outline_list_item_spacing || ((@theme.prose_margin_bottom || @theme.vertical_rhythm) / 2.0))
742
+ margin_bottom: @theme.outline_list_item_spacing
725
743
  end
726
744
  end
727
745
 
@@ -741,9 +759,6 @@ class Converter < ::Prawn::Document
741
759
  if image_type == 'gif'
742
760
  valid_image = false
743
761
  warn %(asciidoctor: WARNING: GIF image format not supported. Please convert #{target} to PNG.)
744
- #elsif image_type == 'pdf'
745
- # import_page image_path
746
- # return
747
762
  end
748
763
 
749
764
  unless (image_path = resolve_image_path node, target) && (::File.readable? image_path)
@@ -751,9 +766,12 @@ class Converter < ::Prawn::Document
751
766
  warn %(asciidoctor: WARNING: image to embed not found or not readable: #{image_path || target})
752
767
  end
753
768
 
769
+ # NOTE import_page automatically advances to next page afterwards
770
+ return import_page image_path if image_type == 'pdf'
771
+
754
772
  # QUESTION if we advance to new page, shouldn't dest point there too?
755
773
  add_dest_for_block node if node.id
756
- position = ((node.attr 'align') || @theme.image_align || :left).to_sym
774
+ position = ((node.attr 'align') || @theme.image_align).to_sym
757
775
 
758
776
  unless valid_image
759
777
  theme_margin :block, :top
@@ -762,11 +780,7 @@ class Converter < ::Prawn::Document
762
780
  else
763
781
  alt_text = %([#{NoBreakSpace}#{node.attr 'alt'}#{NoBreakSpace}] | <em>#{target}</em>)
764
782
  end
765
- layout_prose alt_text,
766
- normalize: false,
767
- margin: 0,
768
- single_line: true,
769
- align: position
783
+ layout_prose alt_text, normalize: false, margin: 0, single_line: true, align: position
770
784
  layout_caption node, position: :bottom if node.title?
771
785
  theme_margin :block, :bottom
772
786
  return
@@ -774,34 +788,33 @@ class Converter < ::Prawn::Document
774
788
 
775
789
  theme_margin :block, :top
776
790
 
791
+ # NOTE image is scaled proportionally based on width (height is ignored)
777
792
  # TODO support cover (aka canvas) image layout using "canvas" (or "cover") role
778
- # NOTE height values are basically ignored (image is scaled proportionally based on width)
779
- width = if node.attr? 'pdfwidth'
780
- if (pdfwidth = node.attr 'pdfwidth').end_with? '%'
781
- (pdfwidth.to_f / 100) * bounds.width
782
- else
783
- str_to_pt pdfwidth
784
- end
785
- elsif node.attr? 'scaledwidth'
786
- ((node.attr 'scaledwidth').to_f / 100) * bounds.width
787
- elsif node.attr? 'width'
788
- # NOTE width is in pixels, so scale by 75%; restrict to max width of bounds.width
789
- [bounds.width, (node.attr 'width').to_f * 0.75].min
790
- end
793
+ width = resolve_explicit_width node.attributes, bounds.width
791
794
 
792
795
  case image_type
793
796
  when 'svg'
794
797
  begin
795
- svg_obj = ::Prawn::Svg::Interface.new (::IO.read image_path), self, position: position, width: width
796
- actual_w = svg_obj.document.sizing.output_width
797
- actual_h = svg_obj.document.sizing.output_height
798
- rel_left = { left: 0, right: (bounds.width - actual_w), center: ((bounds.width - actual_w) / 2.0) }[position]
798
+ svg_data = ::IO.read image_path
799
+ svg_obj = ::Prawn::Svg::Interface.new svg_data, self, position: position, width: width
800
+ svg_size = svg_obj.document.sizing
801
+ rendered_w = svg_size.output_width
802
+ if !width && (svg_obj.document.root.attributes.key? 'width')
803
+ # NOTE scale native width & height by 75% to convert px to pt; restrict width to bounds.width
804
+ if (adjusted_w = [bounds.width, rendered_w * 0.75].min) != rendered_w
805
+ # FIXME would be nice to have a resize/recalculate method; instead, just reconstruct
806
+ svg_obj = ::Prawn::Svg::Interface.new svg_data, self, position: position, width: (rendered_w = adjusted_w)
807
+ svg_size = svg_obj.document.sizing
808
+ end
809
+ end
810
+ # TODO shrink image to fit on a single page if height exceeds page height
811
+ rendered_h = svg_size.output_height
799
812
  # TODO layout SVG without using keep_together (since we know the dimensions already); always render caption
800
813
  keep_together do |box_height = nil|
801
814
  svg_obj.instance_variable_set :@prawn, self
802
815
  svg_obj.draw
803
816
  if box_height && (link = node.attr 'link')
804
- link_annotation [(abs_left = rel_left + bounds.absolute_left), y, (abs_left + actual_w), (y + actual_h)],
817
+ link_annotation [(abs_left = svg_obj.position[0] + bounds.absolute_left), y, (abs_left + rendered_w), (y + rendered_h)],
805
818
  Border: [0, 0, 0],
806
819
  A: { Type: :Action, S: :URI, URI: (str2pdfval link) }
807
820
  end
@@ -812,14 +825,14 @@ class Converter < ::Prawn::Document
812
825
  end
813
826
  else
814
827
  begin
815
- # FIXME temporary workaround to group caption & image
816
- # Prawn doesn't provide access to rendered width & height before placing image on page
817
828
  # FIXME this code really needs to be better organized!
829
+ # FIXME temporary workaround to group caption & image
830
+ # NOTE use low-level API to access intrinsic dimensions; build_image_object caches image data previously loaded
818
831
  image_obj, image_info = build_image_object image_path
819
832
  if width
820
833
  rendered_w, rendered_h = image_info.calc_image_dimensions width: width
821
834
  else
822
- # NOTE native size is in pixels, so scale by 75%; restrict to max width of bounds.width
835
+ # NOTE scale native width & height by 75% to convert px to pt; restrict width to bounds.width
823
836
  rendered_w = [bounds.width, image_info.width * 0.75].min
824
837
  rendered_h = (rendered_w * image_info.height) / image_info.width
825
838
  end
@@ -828,7 +841,7 @@ class Converter < ::Prawn::Document
828
841
  (@theme.caption_margin_inside + @theme.caption_margin_outside + @theme.base_line_height_length) : 0
829
842
  if rendered_h > (available_height = cursor - caption_height)
830
843
  start_new_page unless at_page_top?
831
- # NOTE shrink image so it fits on a single page
844
+ # NOTE shrink image so it fits on a single page if height exceeds page height
832
845
  if rendered_h > (available_height = cursor - caption_height)
833
846
  rendered_w = (rendered_w * available_height) / rendered_h
834
847
  rendered_h = available_height
@@ -862,18 +875,40 @@ class Converter < ::Prawn::Document
862
875
  # QUESTION can we avoid arranging fragments multiple times (conums & autofit) by eagerly preparing arranger?
863
876
  def convert_listing_or_literal node
864
877
  add_dest_for_block node if node.id
878
+
865
879
  # HACK disable built-in syntax highlighter; must be done before calling node.content!
866
- # NOTE the highlight sub is only set for coderay and pygments
867
- if node.style == 'source' && !scratch? && ((subs = node.subs).include? :highlight)
868
- highlighter = node.document.attr 'source-highlighter'
869
- # NOTE the source highlighter logic below handles the callouts and highlight subs
870
- prev_subs = subs.dup
871
- subs.delete_all :highlight, :callouts
880
+ if node.style == 'source' && node.attributes['language'] &&
881
+ (highlighter = node.document.attributes['source-highlighter']) &&
882
+ (SourceHighlighters.include? highlighter)
883
+ prev_subs = (subs = node.subs).dup
884
+ # NOTE the highlight sub is only set for coderay and pygments atm
885
+ highlight_idx = subs.index :highlight
886
+ # NOTE scratch? here only applies if listing block is nested inside another block
887
+ if scratch?
888
+ highlighter = nil
889
+ if highlight_idx
890
+ # switch the :highlight sub back to :specialcharacters
891
+ subs[highlight_idx] = :specialcharacters
892
+ else
893
+ prev_subs = nil
894
+ end
895
+ source_string = preserve_indentation node.content, (node.attr 'tabsize')
896
+ else
897
+ # NOTE the source highlighter logic below handles the callouts and highlight subs
898
+ if highlight_idx
899
+ subs.delete_all :highlight, :callouts
900
+ else
901
+ subs.delete_all :specialcharacters, :callouts
902
+ end
903
+ # the indent guard will be added by the source highlighter logic
904
+ source_string = preserve_indentation node.content, (node.attr 'tabsize'), false
905
+ end
872
906
  else
873
907
  highlighter = nil
874
908
  prev_subs = nil
909
+ source_string = preserve_indentation node.content, (node.attr 'tabsize')
875
910
  end
876
- source_string = preserve_indentation node.content
911
+
877
912
  source_chunks = case highlighter
878
913
  when 'coderay'
879
914
  Helpers.require_library CodeRayRequirePath, 'coderay' unless defined? ::Asciidoctor::Prawn::CodeRayEncoder
@@ -882,14 +917,22 @@ class Converter < ::Prawn::Document
882
917
  conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
883
918
  when 'pygments'
884
919
  Helpers.require_library 'pygments', 'pygments.rb' unless defined? ::Pygments
885
- source_string, conum_mapping = extract_conums source_string
886
920
  lexer = ::Pygments::Lexer[node.attr 'language', 'text', false] || ::Pygments::Lexer['text']
887
921
  pygments_config = { nowrap: true, noclasses: true, style: (node.document.attr 'pygments-style') || 'pastie' }
922
+ source_string, conum_mapping = extract_conums source_string
888
923
  result = lexer.highlight source_string, options: pygments_config
889
- fragments = text_formatter.format result
924
+ fragments = guard_indentation text_formatter.format result
925
+ conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
926
+ when 'rouge'
927
+ Helpers.require_library RougeRequirePath, 'rouge' unless defined? ::Rouge::Formatters::Prawn
928
+ lexer = ::Rouge::Lexer.find(node.attr 'language', 'text', false) || ::Rouge::Lexers::PlainText
929
+ formatter = (@rouge_formatter ||= ::Rouge::Formatters::Prawn.new theme: (node.document.attr 'rouge-style'))
930
+ source_string, conum_mapping = extract_conums source_string
931
+ # NOTE trailing endline is added to address https://github.com/jneen/rouge/issues/279
932
+ fragments = formatter.format (lexer.lex %(#{source_string}#{EOL})), line_numbers: (node.attr? 'linenums')
890
933
  conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
891
934
  else
892
- # NOTE only format if we detect a need
935
+ # NOTE only format if we detect a need (callouts or inline formatting)
893
936
  if source_string =~ BuiltInEntityCharOrTagRx
894
937
  text_formatter.format source_string
895
938
  else
@@ -948,6 +991,7 @@ class Converter < ::Prawn::Document
948
991
 
949
992
  pad_box @theme.code_padding do
950
993
  typeset_formatted_text source_chunks, (calc_line_metrics @theme.code_line_height),
994
+ # QUESTION should we require the code_font_color to be set?
951
995
  color: (@theme.code_font_color || @font_color),
952
996
  size: adjusted_font_size
953
997
  end
@@ -997,7 +1041,7 @@ class Converter < ::Prawn::Document
997
1041
  elsif text.include? EOL
998
1042
  text.split(EOL, -1).each_with_index do |line_in_fragment, idx|
999
1043
  line = (lines[line_num += 1] ||= []) unless idx == 0
1000
- line << fragment.merge(text: line_in_fragment) unless line_in_fragment.empty?
1044
+ line << (fragment.merge text: line_in_fragment) unless line_in_fragment.empty?
1001
1045
  end
1002
1046
  else
1003
1047
  line << fragment
@@ -1029,6 +1073,18 @@ class Converter < ::Prawn::Document
1029
1073
  glyph
1030
1074
  end
1031
1075
 
1076
+ # Adds guards to preserve indentation
1077
+ def guard_indentation fragments
1078
+ start_of_line = true
1079
+ fragments.each do |fragment|
1080
+ next if (text = fragment[:text]).empty?
1081
+ text[0] = GuardedIndent if start_of_line && (text.start_with? ' ')
1082
+ text.gsub! InnerIndent, GuardedInnerIndent if text.include? InnerIndent
1083
+ start_of_line = text.end_with? EOL
1084
+ end
1085
+ fragments
1086
+ end
1087
+
1032
1088
  def convert_table node
1033
1089
  add_dest_for_block node if node.id
1034
1090
  num_rows = 0
@@ -1036,12 +1092,15 @@ class Converter < ::Prawn::Document
1036
1092
  table_header = false
1037
1093
  theme = @theme
1038
1094
 
1039
- # NOTE use an explicit white background if no background color is set and table is nested inside a block
1040
- if (tbl_bg_color = resolve_theme_color :table_background_color, @page_bg_color)
1041
- tbl_bg_color = nil if tbl_bg_color == 'FFFFFF' && node.parent.context == :section
1042
- else
1043
- tbl_bg_color = 'FFFFFF' unless node.parent.context == :section
1044
- end
1095
+ tbl_bg_color = resolve_theme_color :table_background_color
1096
+ # QUESTION should we fallback to page background color? (which is never transparent)
1097
+ #tbl_bg_color = resolve_theme_color :table_background_color, @page_bg_color
1098
+ # ...and if so, should we try to be helpful and use @page_bg_color for tables nested in blocks?
1099
+ #unless tbl_bg_color
1100
+ # tbl_bg_color = @page_bg_color unless [:section, :document].include? node.parent.context
1101
+ #end
1102
+
1103
+ # NOTE emulate table bg color by using it as a fallback value for each element
1045
1104
  head_bg_color = resolve_theme_color :table_head_background_color, tbl_bg_color
1046
1105
  foot_bg_color = resolve_theme_color :table_foot_background_color, tbl_bg_color
1047
1106
  odd_row_bg_color = resolve_theme_color :table_odd_row_background_color, tbl_bg_color
@@ -1056,12 +1115,12 @@ class Converter < ::Prawn::Document
1056
1115
  rows.each do |cell|
1057
1116
  row_data << {
1058
1117
  content: (head_transform ? (transform_text cell.text, head_transform) : cell.text),
1059
- inline_format: [{ normalize: true }],
1118
+ inline_format: [normalize: true],
1060
1119
  background_color: head_bg_color,
1061
1120
  text_color: (theme.table_head_font_color || theme.table_font_color || @font_color),
1062
1121
  size: (theme.table_head_font_size || theme.table_font_size),
1063
1122
  font: (theme.table_head_font_family || theme.table_font_family),
1064
- font_style: (theme.table_head_font_style || :bold).to_sym,
1123
+ font_style: theme.table_head_font_style.to_sym,
1065
1124
  colspan: cell.colspan || 1,
1066
1125
  rowspan: cell.rowspan || 1,
1067
1126
  align: (cell.attr 'halign').to_sym,
@@ -1077,7 +1136,7 @@ class Converter < ::Prawn::Document
1077
1136
  rows.each do |cell|
1078
1137
  cell_data = {
1079
1138
  content: cell.text,
1080
- inline_format: [{ normalize: true }],
1139
+ inline_format: [normalize: true],
1081
1140
  text_color: (theme.table_body_font_color || @font_color),
1082
1141
  size: theme.table_font_size,
1083
1142
  font: theme.table_font_family,
@@ -1090,8 +1149,29 @@ class Converter < ::Prawn::Document
1090
1149
  case cell.style
1091
1150
  when :emphasis
1092
1151
  cell_data[:font_style] = :italic
1093
- when :strong, :header
1152
+ when :strong
1094
1153
  cell_data[:font_style] = :bold
1154
+ when :header
1155
+ unless defined? header_cell_data
1156
+ header_cell_data = {}
1157
+ {
1158
+ 'align' => :align,
1159
+ 'font_color' => :text_color,
1160
+ 'font_family' => :font,
1161
+ 'font_size' => :size,
1162
+ 'font_style' => :font_style
1163
+ }.each do |theme_key, key|
1164
+ if (val = theme[%(table_header_cell_#{theme_key})])
1165
+ header_cell_data[key] = val
1166
+ end
1167
+ end
1168
+ header_cell_data[:font_style] ||= :bold
1169
+ if (val = resolve_theme_color :table_header_cell_background_color)
1170
+ header_cell_data[:background_color] = val
1171
+ end
1172
+ end
1173
+
1174
+ cell_data.update header_cell_data unless header_cell_data.empty?
1095
1175
  when :monospaced
1096
1176
  cell_data[:font] = theme.literal_font_family
1097
1177
  if (size = theme.literal_font_size)
@@ -1107,8 +1187,6 @@ class Converter < ::Prawn::Document
1107
1187
  table_data << row_data
1108
1188
  end
1109
1189
 
1110
- column_widths = node.columns.map {|col| ((col.attr 'colpcwidth') * bounds.width) / 100.0 }
1111
-
1112
1190
  border = {}
1113
1191
  table_border_width = theme.table_border_width
1114
1192
  [:top, :bottom, :left, :right, :cols, :rows].each {|edge| border[edge] = table_border_width }
@@ -1134,6 +1212,13 @@ class Converter < ::Prawn::Document
1134
1212
  border[:top] = border[:right] = border[:bottom] = border[:left] = 0
1135
1213
  end
1136
1214
 
1215
+ if node.option? 'autowidth'
1216
+ column_widths = []
1217
+ else
1218
+ table_width = bounds.width * ((node.attr 'tablepcwidth') / 100.0)
1219
+ column_widths = node.columns.map {|col| ((col.attr 'colpcwidth') * table_width) / 100.0 }
1220
+ end
1221
+
1137
1222
  table_settings = {
1138
1223
  header: table_header,
1139
1224
  cell_style: {
@@ -1191,7 +1276,7 @@ class Converter < ::Prawn::Document
1191
1276
 
1192
1277
  def convert_thematic_break node
1193
1278
  theme_margin :thematic_break, :top
1194
- stroke_horizontal_rule @theme.thematic_break_border_color, line_width: @theme.thematic_break_border_width, line_style: (@theme.thematic_break_border_style || :solid).to_sym
1279
+ stroke_horizontal_rule @theme.thematic_break_border_color, line_width: @theme.thematic_break_border_width, line_style: @theme.thematic_break_border_style.to_sym
1195
1280
  theme_margin :thematic_break, :bottom
1196
1281
  end
1197
1282
 
@@ -1203,6 +1288,7 @@ class Converter < ::Prawn::Document
1203
1288
  nil
1204
1289
  end
1205
1290
 
1291
+ # NOTE to insert sequential page breaks, you must put {nbsp} between page breaks
1206
1292
  def convert_page_break node
1207
1293
  start_new_page unless at_page_top?
1208
1294
  end
@@ -1275,10 +1361,10 @@ class Converter < ::Prawn::Document
1275
1361
  def convert_inline_footnote node
1276
1362
  if (index = node.attr 'index')
1277
1363
  #text = node.document.footnotes.find {|fn| fn.index == index }.text
1278
- %(<sup>[#{index}: #{node.text}]</sup>)
1364
+ %( <color rgb="#999999">[#{index}: #{node.text}]</color>)
1279
1365
  elsif node.type == :xref
1280
1366
  # NOTE footnote reference not found
1281
- %(<sup><color rgb="FF0000">[#{node.text}]</color></sup>)
1367
+ %( <color rgb="FF0000">[#{node.text}]</color>)
1282
1368
  end
1283
1369
  end
1284
1370
 
@@ -1386,40 +1472,19 @@ class Converter < ::Prawn::Document
1386
1472
 
1387
1473
  # FIXME only create title page if doctype=book!
1388
1474
  def layout_title_page doc
1389
- return unless doc.header? && !doc.noheader && !doc.notitle
1475
+ return unless doc.header? && !doc.notitle
1390
1476
 
1391
1477
  prev_bg_image = @page_bg_image
1392
1478
  prev_bg_color = @page_bg_color
1393
- if (bg_image = (doc.attr 'title-background-image', @theme.title_page_background_image))
1394
- if bg_image == 'none'
1395
- @page_bg_image = nil
1396
- else
1397
- if bg_image =~ ImageAttributeValueRx
1398
- bg_image = $1
1399
- # QUESTION should we support width and height?
1400
- end
1401
1479
 
1402
- # NOTE resolve image relative to its origin
1403
- resolved_bg_image = if doc.attr? 'title-background-image'
1404
- resolve_image_path doc, bg_image
1405
- else
1406
- ThemeLoader.resolve_theme_asset bg_image, (doc.attr 'pdf-stylesdir')
1407
- end
1408
-
1409
- if resolved_bg_image && (::File.readable? resolved_bg_image)
1410
- @page_bg_image = resolved_bg_image
1411
- else
1412
- warn %(asciidoctor: WARNING: title page background image #{resolved_bg_image || bg_image} not found or readable)
1413
- bg_image = nil
1414
- end
1415
- end
1480
+ if (bg_image = resolve_background_image doc, @theme, 'title-page-background-image')
1481
+ @page_bg_image = (bg_image == 'none' ? nil : bg_image)
1416
1482
  end
1417
- if !bg_image && (bg_color = resolve_theme_color :title_page_background_color)
1483
+ if (bg_color = resolve_theme_color :title_page_background_color)
1418
1484
  @page_bg_color = bg_color
1419
- else
1420
- bg_color = nil
1421
1485
  end
1422
- start_new_page
1486
+ # NOTE a new page will already be started if the cover image is a PDF
1487
+ start_new_page unless page_is_empty?
1423
1488
  @page_bg_image = prev_bg_image if bg_image
1424
1489
  @page_bg_color = prev_bg_color if bg_color
1425
1490
 
@@ -1427,24 +1492,23 @@ class Converter < ::Prawn::Document
1427
1492
  font @theme.base_font_family, size: @theme.base_font_size
1428
1493
 
1429
1494
  # QUESTION allow aligment per element on title page?
1430
- title_align = (@theme.title_page_align || :center).to_sym
1495
+ title_align = @theme.title_page_align.to_sym
1431
1496
 
1432
- # FIXME rework image handling once fix for #134 is merged
1497
+ # TODO disallow .pdf as image type
1433
1498
  if (logo_image_path = (doc.attr 'title-logo-image', @theme.title_page_logo_image))
1434
- if logo_image_path =~ ImageAttributeValueRx
1499
+ if (logo_image_path.include? ':') && logo_image_path =~ ImageAttributeValueRx
1435
1500
  logo_image_path = $1
1436
- logo_image_attrs = AttributeList.new($2).parse(['alt', 'width', 'height'])
1501
+ logo_image_attrs = (AttributeList.new $2).parse ['alt', 'width', 'height']
1437
1502
  else
1438
1503
  logo_image_attrs = {}
1439
1504
  end
1440
1505
  # HACK quick fix to resolve image path relative to theme
1441
1506
  unless doc.attr? 'title-logo-image'
1442
- # FIXME use ThemeLoader.resolve_theme_asset once fix for #134 is merged
1443
- logo_image_path = ::File.expand_path logo_image_path, (doc.attr 'pdf-stylesdir', ThemeLoader::ThemesDir)
1507
+ logo_image_path = ThemeLoader.resolve_theme_asset logo_image_path, (doc.attr 'pdf-stylesdir')
1444
1508
  end
1445
1509
  logo_image_attrs['target'] = logo_image_path
1446
1510
  logo_image_attrs['align'] ||= (@theme.title_page_logo_align || title_align.to_s)
1447
- logo_image_top = (logo_image_attrs['top'] || @theme.title_page_logo_top || '10%')
1511
+ logo_image_top = (logo_image_attrs['top'] || @theme.title_page_logo_top)
1448
1512
  # FIXME delegate to method to convert page % to y value
1449
1513
  logo_image_top = [(page_height - page_height * (logo_image_top.to_i / 100.0)), bounds.absolute_top].min
1450
1514
  float do
@@ -1452,6 +1516,7 @@ class Converter < ::Prawn::Document
1452
1516
  # FIXME add API to Asciidoctor for creating blocks like this (extract from extensions module?)
1453
1517
  image_block = ::Asciidoctor::Block.new doc, :image, content_model: :empty, attributes: logo_image_attrs
1454
1518
  # FIXME prevent image from spilling to next page
1519
+ # QUESTION should we shave off margin top/bottom?
1455
1520
  convert_image image_block
1456
1521
  end
1457
1522
  end
@@ -1511,12 +1576,13 @@ class Converter < ::Prawn::Document
1511
1576
  # TODO turn processing of attribute with inline image a utility function in Asciidoctor
1512
1577
  # FIXME verify cover_image exists!
1513
1578
  if (cover_image = (doc.attr %(#{position}-cover-image)))
1514
- if cover_image =~ ImageAttributeValueRx
1579
+ if (cover_image.include? ':') && cover_image =~ ImageAttributeValueRx
1515
1580
  cover_image = resolve_image_path doc, $1
1516
1581
  end
1517
1582
  # QUESTION should we go to page 1 when position == :front?
1518
1583
  go_to_page page_count if position == :back
1519
1584
  if cover_image.downcase.end_with? '.pdf'
1585
+ # NOTE import_page automatically advances to next page afterwards
1520
1586
  import_page cover_image
1521
1587
  else
1522
1588
  image_page cover_image, canvas: true
@@ -1554,8 +1620,8 @@ class Converter < ::Prawn::Document
1554
1620
 
1555
1621
  # NOTE inline_format is true by default
1556
1622
  def layout_prose string, opts = {}
1557
- top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme.prose_margin_top || 0
1558
- bot_margin = margin || (opts.delete :margin_bottom) || @theme.prose_margin_bottom || @theme.vertical_rhythm
1623
+ top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme.prose_margin_top
1624
+ bot_margin = margin || (opts.delete :margin_bottom) || @theme.prose_margin_bottom
1559
1625
  if (transform = (opts.delete :text_transform) || @text_transform)
1560
1626
  string = transform_text string, transform
1561
1627
  end
@@ -1568,14 +1634,12 @@ class Converter < ::Prawn::Document
1568
1634
  string = %(<a anchor="#{anchor}">#{string}</a>)
1569
1635
  end
1570
1636
  end
1571
- # preserve leading space using non-breaking space chars
1572
- string = preserve_indentation string if opts.delete :preserve
1573
1637
  margin_top top_margin
1574
1638
  typeset_text string, calc_line_metrics((opts.delete :line_height) || @theme.base_line_height), {
1575
1639
  color: @font_color,
1576
1640
  # NOTE normalize makes endlines soft (replaces "\n" with ' ')
1577
- inline_format: [{ normalize: (opts.delete :normalize) != false }],
1578
- align: (@theme.base_align || :left).to_sym
1641
+ inline_format: [normalize: (opts.delete :normalize) != false],
1642
+ align: @theme.base_align.to_sym
1579
1643
  }.merge(opts)
1580
1644
  margin_bottom bot_margin
1581
1645
  end
@@ -1602,7 +1666,7 @@ class Converter < ::Prawn::Document
1602
1666
  layout_prose string, {
1603
1667
  margin_top: margin[:top],
1604
1668
  margin_bottom: margin[:bottom],
1605
- align: (@theme.caption_align || :left).to_sym,
1669
+ align: @theme.caption_align.to_sym,
1606
1670
  normalize: false
1607
1671
  }.merge(opts)
1608
1672
  if position == :top && @theme.caption_border_bottom_color
@@ -1627,7 +1691,7 @@ class Converter < ::Prawn::Document
1627
1691
  # QUESTION shouldn't we skip this whole method if num_levels == 0?
1628
1692
  if num_levels > 0
1629
1693
  theme_margin :toc, :top
1630
- line_metrics = calc_line_metrics @theme.toc_line_height || @theme.base_line_height
1694
+ line_metrics = calc_line_metrics @theme.toc_line_height
1631
1695
  dot_width = nil
1632
1696
  theme_font :toc do
1633
1697
  dot_width = width_of(@theme.toc_dot_leader_content || DotLeaderDefault)
@@ -1665,6 +1729,7 @@ class Converter < ::Prawn::Document
1665
1729
  # FIXME dots don't line up if width of page numbers differ
1666
1730
  typeset_formatted_text [
1667
1731
  { text: %(#{(@theme.toc_dot_leader_content || DotLeaderDefault) * num_dots}), color: toc_dot_color },
1732
+ # FIXME this spacing doesn't always work out
1668
1733
  { text: NoBreakSpace, size: (@font_size * 0.5) },
1669
1734
  { text: sect_page_num.to_s, anchor: sect_anchor, color: @font_color }], line_metrics, align: :right
1670
1735
  go_to_page end_page_number if start_page_number != end_page_number
@@ -1672,7 +1737,7 @@ class Converter < ::Prawn::Document
1672
1737
  end
1673
1738
  end
1674
1739
  if sect.level < num_levels
1675
- indent(@theme.toc_indent || @theme.outline_list_indent) do
1740
+ indent @theme.toc_indent do
1676
1741
  layout_toc_level sect.sections, num_levels, line_metrics, dot_width, num_front_matter_pages
1677
1742
  end
1678
1743
  end
@@ -1695,7 +1760,7 @@ class Converter < ::Prawn::Document
1695
1760
  num_pages = page_count - skip
1696
1761
 
1697
1762
  # FIXME probably need to treat doctypes differently
1698
- sections = doc.find_by(context: :section) {|sect| sect.level < 3 }
1763
+ sections = doc.find_by(context: :section) {|sect| sect.level < 3 } || []
1699
1764
 
1700
1765
  # index chapters and sections by the visual page number on which they start
1701
1766
  chapter_start_pages = {}
@@ -1726,49 +1791,52 @@ class Converter < ::Prawn::Document
1726
1791
  sections_by_page[num] = last_sect
1727
1792
  end
1728
1793
 
1729
- doctitle = doc.doctitle partition: true
1794
+ doctitle = doc.doctitle partition: true, use_fallback: true
1730
1795
  # NOTE set doctitle again so it's properly escaped
1731
1796
  doc.set_attr 'doctitle', doctitle.combined
1732
1797
  doc.set_attr 'document-title', doctitle.main
1733
1798
  doc.set_attr 'document-subtitle', doctitle.subtitle
1734
1799
  doc.set_attr 'page-count', num_pages
1735
1800
 
1801
+ fallback_footer_content = {
1802
+ recto: { right: '{page-number}' },
1803
+ verso: { left: '{page-number}' }
1804
+ }
1736
1805
  # TODO move this to a method so it can be reused; cache results
1737
1806
  content_dict = [:recto, :verso].inject({}) do |acc, side|
1738
1807
  side_content = {}
1739
1808
  Alignments.each do |align|
1740
1809
  if (val = @theme[%(#{position}_#{side}_content_#{align})])
1741
- if ImageAttributeValueRx =~ val &&
1810
+ # TODO support image URL (using resolve_image_path)
1811
+ if (val.include? ':') && val =~ ImageAttributeValueRx &&
1742
1812
  ::File.readable?(path = (ThemeLoader.resolve_theme_asset $1, (doc.attr 'pdf-stylesdir')))
1743
- attrs = AttributeList.new($2).parse
1744
- attrs['width'] = attrs['width'].to_f if attrs['width']
1745
- side_content[align] = { path: path, width: attrs['width'] }
1813
+ attrs = (AttributeList.new $2).parse
1814
+ width = resolve_explicit_width attrs, bounds.width
1815
+ # QUESTION should we lookup and scale intrinsic width if explicit width is not given?
1816
+ unless width
1817
+ width = [bounds.width, (intrinsic_image_dimensions path)[:width] * 0.75].min
1818
+ end
1819
+ side_content[align] = { path: path, width: width }
1746
1820
  else
1747
- side_content[align] ||= val
1821
+ side_content[align] = val
1748
1822
  end
1749
1823
  end
1750
1824
  end
1751
- if (acc[side] = side_content).empty? && @theme[%(footer_#{side}_content)] != 'none'
1752
- # NOTE set fallbacks if not explicitly disabled
1753
- case side
1754
- when :recto
1755
- acc[side] = { right: '{page-number}' }
1756
- when :verso
1757
- acc[side] = { left: '{page-number}' }
1758
- end
1825
+ # NOTE set fallbacks if not explicitly disabled
1826
+ if side_content.empty? && position == :footer && @theme[%(footer_#{side}_content)] != 'none'
1827
+ side_content = fallback_footer_content[side]
1759
1828
  end
1829
+
1830
+ acc[side] = side_content
1760
1831
  acc
1761
1832
  end
1762
1833
 
1763
- # QUESTION should we support footer_line_height?
1764
- #trim_line_metrics = calc_line_metrics @theme.base_line_height
1765
- trim_line_metrics = calc_line_metrics
1766
1834
  if position == :header
1835
+ trim_line_metrics = calc_line_metrics(@theme.header_line_height || @theme.base_line_height)
1767
1836
  trim_top = page_height
1768
1837
  # NOTE height is required atm
1769
1838
  trim_height = @theme.header_height || page_margin_top
1770
1839
  trim_padding = @theme.header_padding || [0, 0, 0, 0]
1771
- trim_content_height = trim_height - trim_padding[0] - trim_padding[2] - trim_line_metrics.padding_top
1772
1840
  trim_left = page_margin_left
1773
1841
  trim_width = page_width - trim_left - page_margin_right
1774
1842
  trim_font_color = @theme.header_font_color || @font_color
@@ -1776,13 +1844,13 @@ class Converter < ::Prawn::Document
1776
1844
  trim_border_width = @theme.header_border_width || @theme.base_border_width
1777
1845
  trim_border_style = (@theme.header_border_style || :solid).to_sym
1778
1846
  trim_border_color = resolve_theme_color :header_border_color
1779
- trim_valign = (@theme.header_valign || :center).to_sym
1780
- trim_img_valign = @theme.header_image_valign || trim_valign
1847
+ trim_valign = (@theme.header_vertical_align || :middle).to_sym
1848
+ trim_img_valign = @theme.header_image_vertical_align
1781
1849
  else
1850
+ trim_line_metrics = calc_line_metrics(@theme.footer_line_height || @theme.base_line_height)
1782
1851
  # NOTE height is required atm
1783
1852
  trim_top = trim_height = @theme.footer_height || page_margin_bottom
1784
1853
  trim_padding = @theme.footer_padding || [0, 0, 0, 0]
1785
- trim_content_height = trim_height - trim_padding[0] - trim_padding[2] - trim_line_metrics.padding_top
1786
1854
  trim_left = page_margin_left
1787
1855
  trim_width = page_width - trim_left - page_margin_right
1788
1856
  trim_font_color = @theme.footer_font_color || @font_color
@@ -1790,19 +1858,29 @@ class Converter < ::Prawn::Document
1790
1858
  trim_border_width = @theme.footer_border_width || @theme.base_border_width
1791
1859
  trim_border_style = (@theme.footer_border_style || :solid).to_sym
1792
1860
  trim_border_color = resolve_theme_color :footer_border_color
1793
- trim_valign = (@theme.footer_valign || :center).to_sym
1794
- trim_img_valign = @theme.footer_image_valign || trim_valign
1861
+ trim_valign = (@theme.footer_vertical_align || :middle).to_sym
1862
+ trim_img_valign = @theme.footer_image_vertical_align
1795
1863
  end
1796
1864
 
1797
- trim_stamp = %(#{position})
1865
+ trim_stamp = position.to_s
1798
1866
  trim_content_left = trim_left + trim_padding[3]
1867
+ trim_content_height = trim_height - trim_padding[0] - trim_padding[2] - trim_line_metrics.padding_top - trim_line_metrics.padding_bottom
1799
1868
  trim_content_width = trim_width - trim_padding[3] - trim_padding[1]
1800
1869
  trim_border_color = nil if trim_border_width == 0
1801
- if ['top', 'center', 'bottom'].include? trim_img_valign
1870
+ trim_valign = :center if trim_valign == :middle
1871
+ case trim_img_valign
1872
+ when nil
1873
+ trim_img_valign = trim_valign
1874
+ when 'middle'
1875
+ trim_img_valign = :center
1876
+ when 'top', 'center', 'bottom'
1802
1877
  trim_img_valign = trim_img_valign.to_sym
1803
1878
  end
1804
1879
 
1805
1880
  if trim_bg_color || trim_border_color
1881
+ # NOTE switch to first content page so stamp will get created properly (can't create on imported page)
1882
+ prev_page_number = page_number
1883
+ go_to_page start
1806
1884
  create_stamp trim_stamp do
1807
1885
  canvas do
1808
1886
  if trim_bg_color
@@ -1824,17 +1902,20 @@ class Converter < ::Prawn::Document
1824
1902
  end
1825
1903
  end
1826
1904
  @stamps[position] = true
1905
+ go_to_page prev_page_number
1827
1906
  end
1828
1907
 
1908
+ pagenums_enabled = doc.attr? 'pagenums'
1829
1909
  repeat (start..page_count), dynamic: true do
1830
1910
  # NOTE don't write on pages which are imported / inserts (otherwise we can get a corrupt PDF)
1831
1911
  next if page.imported_page?
1832
1912
  visual_pgnum = page_number - skip
1833
1913
  # FIXME we need to have a content setting for chapter pages
1834
1914
  content_by_alignment = content_dict[visual_pgnum.odd? ? :recto : :verso]
1835
- doc.set_attr 'page-number', visual_pgnum
1836
1915
  # TODO populate chapter-number
1837
1916
  # TODO populate numbered and unnumbered chapter and section titles
1917
+ # FIXME leave page-number attribute unset once we filter lines with unresolved attributes (see below)
1918
+ doc.set_attr 'page-number', (pagenums_enabled ? visual_pgnum : '')
1838
1919
  doc.set_attr 'chapter-title', (chapters_by_page[visual_pgnum] || '')
1839
1920
  doc.set_attr 'section-title', (sections_by_page[visual_pgnum] || '')
1840
1921
  doc.set_attr 'section-or-chapter-title', (sections_by_page[visual_pgnum] || chapters_by_page[visual_pgnum] || '')
@@ -1848,16 +1929,26 @@ class Converter < ::Prawn::Document
1848
1929
  # FIXME we need to have a content setting for chapter pages
1849
1930
  case (content = content_by_alignment[align])
1850
1931
  when ::Hash
1851
- # FIXME prevent image from overflowing the page
1852
- float do
1853
- # FIXME padding doesn't work when vposition is specified; how will padding bottom work?
1854
- #move_down trim_padding[0]
1855
- image content[:path], vposition: trim_img_valign, position: align, width: content[:width]
1932
+ # NOTE image placement respects padding; use negative image_vertical_align value to revert
1933
+ trim_v_padding = trim_padding[0] + trim_padding[2]
1934
+ # NOTE bounding_box is redundant if trim_v_padding is 0
1935
+ bounding_box [0, cursor - trim_padding[0]], width: bounds.width, height: (bounds.height - trim_v_padding) do
1936
+ # NOTE float ensures cursor position is restored and returns us to current page if we overrun
1937
+ float do
1938
+ #image content[:path], vposition: trim_img_valign, position: align, width: content[:width]
1939
+ # NOTE use :fit to prevent image from overflowing page (at the cost of scaling it)
1940
+ image content[:path], vposition: trim_img_valign, position: align, fit: [content[:width], bounds.height]
1941
+ end
1856
1942
  end
1857
1943
  when ::String
1858
- content = (content == '{page-number}' ? %(#{visual_pgnum}) : (doc.apply_subs content))
1859
- formatted_text_box parse_text(content, color: trim_font_color, inline_format: true),
1860
- at: [0, trim_content_height + trim_padding[2]],
1944
+ if content == '{page-number}'
1945
+ content = pagenums_enabled ? visual_pgnum.to_s : nil
1946
+ else
1947
+ # FIXME drop lines with unresolved attributes
1948
+ content = doc.apply_subs content
1949
+ end
1950
+ formatted_text_box parse_text(content, color: trim_font_color, inline_format: [normalize: true]),
1951
+ at: [0, trim_content_height + trim_padding[2] + trim_line_metrics.padding_bottom],
1861
1952
  height: trim_content_height,
1862
1953
  align: align,
1863
1954
  valign: trim_valign,
@@ -1884,7 +1975,7 @@ class Converter < ::Prawn::Document
1884
1975
 
1885
1976
  # title page (i)
1886
1977
  # TODO same conditional logic as in layout_title_page; consolidate
1887
- if doc.header? && !doc.noheader && !doc.notitle
1978
+ if doc.header? && !doc.notitle
1888
1979
  page_num_labels[0] = { P: ::PDF::Core::LiteralString.new(front_matter_counter.next!.to_s) }
1889
1980
  end
1890
1981
 
@@ -1935,11 +2026,18 @@ class Converter < ::Prawn::Document
1935
2026
  end
1936
2027
 
1937
2028
  def write pdf_doc, target
1938
- pdf_doc.render_file target
2029
+ if target.respond_to? :write
2030
+ require_relative 'core_ext/quantifiable_stdout' unless defined? ::QuantifiableStdout
2031
+ target = ::QuantifiableStdout.new STDOUT if target == STDOUT
2032
+ pdf_doc.render target
2033
+ else
2034
+ pdf_doc.render_file target
2035
+ # QUESTION restore attributes first?
2036
+ @pdfmarks.generate_file target if @pdfmarks
2037
+ end
1939
2038
  # write scratch document if debug is enabled (or perhaps DEBUG_STEPS env)
1940
2039
  #get_scratch_document.render_file 'scratch.pdf'
1941
- # QUESTION restore attributes first?
1942
- @pdfmarks.generate_file target if @pdfmarks
2040
+ nil
1943
2041
  end
1944
2042
 
1945
2043
  def register_fonts font_catalog, scripts = 'latin', fonts_dir
@@ -1988,18 +2086,22 @@ class Converter < ::Prawn::Document
1988
2086
  # Start a new page if y value is greater than remaining space on page.
1989
2087
  def margin y, position
1990
2088
  unless y == 0 || at_page_top?
1991
- if cursor <= y
1992
- @margin_box.move_past_bottom
1993
- else
2089
+ if cursor > y
1994
2090
  move_down y
2091
+ else
2092
+ # go to the next page
2093
+ # NOTE we don't use `move_down cursor` because we often have to check at_page_top?
2094
+ @margin_box.move_past_bottom
1995
2095
  end
1996
2096
  end
1997
2097
  end
1998
2098
 
1999
2099
  # Lookup margin for theme element and position, then delegate to margin method.
2000
- # If the margin value is not found, assume 0 for position = :top and $vertical_rhythm for position = :bottom.
2100
+ # If margin value is not found, assume:
2101
+ # - 0 when position = :top
2102
+ # - @theme.vertical_spacing when position = :bottom
2001
2103
  def theme_margin category, position
2002
- margin(@theme[%(#{category}_margin_#{position})] || (position == :bottom ? @theme.vertical_rhythm : 0), position)
2104
+ margin (@theme[%(#{category}_margin_#{position})] || (position == :bottom ? @theme.vertical_spacing : 0)), position
2003
2105
  end
2004
2106
 
2005
2107
  def theme_font category, opts = {}
@@ -2042,22 +2144,20 @@ class Converter < ::Prawn::Document
2042
2144
  arranger = arrange_fragments_by_line fragments
2043
2145
  adjusted_font_size = nil
2044
2146
  theme_font category do
2045
- # NOTE finalizing the line here generates fragments using current font settings
2147
+ # NOTE finalizing the line here generates fragments & calculates their widths using the current font settings
2148
+ # CAUTION it also removes zero-width spaces
2046
2149
  arranger.finalize_line
2047
2150
  actual_width = width_of_fragments arranger.fragments
2048
2151
  unless ::Array === (padding = @theme[%(#{category}_padding)])
2049
2152
  padding = [padding] * 4
2050
2153
  end
2051
- bounds.add_left_padding(p_left = padding[3] || 0)
2052
- bounds.add_right_padding(p_right = padding[1] || 0)
2053
- if actual_width > bounds.width
2054
- adjusted_font_size = ((bounds.width * font_size).to_f / actual_width).with_precision 4
2154
+ available_width = bounds.width - (padding[3] || 0) - (padding[1] || 0)
2155
+ if actual_width > available_width
2156
+ adjusted_font_size = ((available_width * font_size).to_f / actual_width).with_precision 4
2055
2157
  if (min = @theme[%(#{category}_font_size_min)] || @theme.base_font_size_min) && adjusted_font_size < min
2056
2158
  adjusted_font_size = min
2057
2159
  end
2058
2160
  end
2059
- bounds.subtract_left_padding p_left
2060
- bounds.subtract_right_padding p_right
2061
2161
  end
2062
2162
  adjusted_font_size
2063
2163
  end
@@ -2074,7 +2174,7 @@ class Converter < ::Prawn::Document
2074
2174
  by_line << fragment
2075
2175
  elsif txt.include? EOL
2076
2176
  txt.scan(LineScanRx) do |line|
2077
- by_line << fragment.merge(text: line)
2177
+ by_line << (line == EOL ? { text: EOL } : (fragment.merge text: line))
2078
2178
  end
2079
2179
  else
2080
2180
  by_line << fragment
@@ -2125,8 +2225,60 @@ class Converter < ::Prawn::Document
2125
2225
  (height_of string, leading: line_metrics.leading, final_gap: line_metrics.final_gap) + line_metrics.padding_top + line_metrics.padding_bottom
2126
2226
  end
2127
2227
 
2128
- def preserve_indentation string
2129
- string.gsub(IndentationRx) { NoBreakSpace * $&.length }
2228
+ def preserve_indentation string, tab_size = nil, guard_indent = true
2229
+ return '' unless string
2230
+ # expand tabs if they aren't already expanded, even if explicitly disabled
2231
+ # NOTE Asciidoctor >= 1.5.3 already replaces tabs if tabsize attribute is positive
2232
+ if ((tab_size = tab_size.to_i) < 1 || !@capabilities[:expands_tabs]) && (string.include? TAB)
2233
+ # Asciidoctor <= 1.5.2 already does tab replacement in some cases, so be consistent about tab size
2234
+ full_tab_space = ' ' * (tab_size = 4)
2235
+ result = []
2236
+ string.each_line do |line|
2237
+ if line.start_with? TAB
2238
+ # NOTE '+' operator is faster than interpolation in this case
2239
+ if guard_indent
2240
+ line.sub!(TabIndentRx) {|tabs| GuardedIndent + (full_tab_space * tabs.length).chop! }
2241
+ else
2242
+ line.sub!(TabIndentRx) {|tabs| full_tab_space * tabs.length }
2243
+ end
2244
+ leading_space = false
2245
+ # QUESTION should we check for EOL first?
2246
+ elsif line == EOL
2247
+ result << line
2248
+ next
2249
+ else
2250
+ leading_space = guard_indent && (line.start_with? ' ')
2251
+ end
2252
+
2253
+ if line.include? TAB
2254
+ # keep track of how many spaces were added to adjust offset in match data
2255
+ spaces_added = 0
2256
+ line.gsub!(TabRx) {
2257
+ # calculate how many spaces this tab represents, then replace tab with spaces
2258
+ if (offset = ($~.begin 0) + spaces_added) % tab_size == 0
2259
+ spaces_added += (tab_size - 1)
2260
+ full_tab_space
2261
+ else
2262
+ unless (spaces = tab_size - offset % tab_size) == 1
2263
+ spaces_added += (spaces - 1)
2264
+ end
2265
+ ' ' * spaces
2266
+ end
2267
+ }
2268
+ end
2269
+
2270
+ # NOTE we save time by adding indent guard per line while performing tab expansion
2271
+ line[0] = GuardedIndent if leading_space
2272
+ result << line
2273
+ end
2274
+ result.join
2275
+ else
2276
+ if guard_indent
2277
+ string[0] = GuardedIndent if string.start_with? ' '
2278
+ string.gsub! InnerIndent, GuardedInnerIndent if string.include? InnerIndent
2279
+ end
2280
+ string
2281
+ end
2130
2282
  end
2131
2283
 
2132
2284
  # If an id is provided or the node passed as the first argument has an id,
@@ -2209,6 +2361,57 @@ class Converter < ::Prawn::Document
2209
2361
  end
2210
2362
  end
2211
2363
 
2364
+ # Resolve the path to the background image either from a document attribute or theme key.
2365
+ #
2366
+ # Returns The string "none" if the background image value is none, otherwise the resolved
2367
+ # path to the image. If neither the document attribute or theme key are specified, or
2368
+ # the image path cannot be resolved, return nil.
2369
+ def resolve_background_image doc, theme, key
2370
+ if (bg_image = (doc_attr_val = (doc.attr key)) || theme[(key.tr '-', '_').to_sym])
2371
+ return bg_image if bg_image == 'none'
2372
+
2373
+ if (bg_image.include? ':') && bg_image =~ ImageAttributeValueRx
2374
+ # QUESTION should we support width and height in this case?
2375
+ bg_image = $1
2376
+ end
2377
+
2378
+ if (bg_image = doc_attr_val ? (resolve_image_path doc, bg_image) :
2379
+ (ThemeLoader.resolve_theme_asset bg_image, (doc.attr 'pdf-stylesdir')))
2380
+ if ::File.readable? bg_image
2381
+ bg_image
2382
+ else
2383
+ warn %(asciidoctor: WARNING: #{key.tr '-', ' '} #{bg_image} not found or readable)
2384
+ nil
2385
+ end
2386
+ end
2387
+ end
2388
+ end
2389
+
2390
+ # Resolves the explicit width as a PDF pt value, if specified.
2391
+ #
2392
+ # Resolves the explicit width, first considering the pdfwidth attribute, then
2393
+ # the scaledwidth attribute and finally the width attribute. If the specified
2394
+ # value is in pixels, the value is scaled by 75% to perform approximate
2395
+ # CSS px to PDF pt conversion. If the resolved width is larger than the
2396
+ # max_width, the max_width value is returned.
2397
+ #--
2398
+ # QUESTION should we enforce positive result?
2399
+ def resolve_explicit_width attrs, max_width = bounds.width
2400
+ if attrs.key? 'pdfwidth'
2401
+ if (pdfwidth = attrs['pdfwidth']).end_with? '%'
2402
+ (pdfwidth.to_f / 100) * max_width
2403
+ else
2404
+ str_to_pt pdfwidth
2405
+ end
2406
+ elsif attrs.key? 'scaledwidth'
2407
+ (attrs['scaledwidth'].to_f / 100) * max_width
2408
+ elsif attrs.key? 'width'
2409
+ # QUESTION should we honor percentage width value?
2410
+ # NOTE scale width down 75% to convert px to pt; restrict width to bounds.width
2411
+ [max_width, attrs['width'].to_f * 0.75].min
2412
+ end
2413
+ end
2414
+
2212
2415
  # QUESTION is there a better way to do this?
2213
2416
  # I suppose we could have @tmp_files as an instance variable on converter instead
2214
2417
  # It might be sufficient to delete temporary files once per conversion