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

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/NOTICE.adoc +2 -2
  3. data/README.adoc +127 -128
  4. data/Rakefile +5 -4
  5. data/bin/asciidoctor-pdf +15 -2
  6. data/data/fonts/notoserif-regular-latin.ttf +0 -0
  7. data/data/themes/default-theme.yml +15 -13
  8. data/docs/theme-schema.json +114 -0
  9. data/docs/theming-guide.adoc +386 -132
  10. data/lib/asciidoctor-pdf/asciidoctor_ext.rb +2 -0
  11. data/lib/asciidoctor-pdf/asciidoctor_ext/image.rb +18 -0
  12. data/lib/asciidoctor-pdf/converter.rb +377 -221
  13. data/lib/asciidoctor-pdf/core_ext.rb +2 -0
  14. data/lib/asciidoctor-pdf/core_ext/array.rb +10 -4
  15. data/lib/asciidoctor-pdf/core_ext/numeric.rb +11 -0
  16. data/lib/asciidoctor-pdf/core_ext/ostruct.rb +1 -1
  17. data/lib/asciidoctor-pdf/formatted_text.rb +8 -0
  18. data/lib/asciidoctor-pdf/{prawn_ext/formatted_text → formatted_text}/formatter.rb +6 -9
  19. data/lib/asciidoctor-pdf/formatted_text/inline_destination_marker.rb +16 -0
  20. data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +125 -0
  21. data/lib/asciidoctor-pdf/formatted_text/inline_image_renderer.rb +45 -0
  22. data/lib/asciidoctor-pdf/{prawn_ext/formatted_text → formatted_text}/parser.rb +252 -218
  23. data/lib/asciidoctor-pdf/{prawn_ext/formatted_text → formatted_text}/parser.treetop +18 -9
  24. data/lib/asciidoctor-pdf/{prawn_ext/formatted_text → formatted_text}/transform.rb +80 -69
  25. data/lib/asciidoctor-pdf/prawn_ext.rb +2 -2
  26. data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +164 -35
  27. data/lib/asciidoctor-pdf/prawn_ext/formatted_text/fragment.rb +37 -0
  28. data/lib/asciidoctor-pdf/prawn_ext/images.rb +11 -9
  29. data/lib/asciidoctor-pdf/temporary_path.rb +9 -0
  30. data/lib/asciidoctor-pdf/theme_loader.rb +40 -33
  31. data/lib/asciidoctor-pdf/version.rb +1 -1
  32. metadata +30 -14
@@ -1,3 +1,5 @@
1
+ # NOTE these are all candidates for inclusion in Asciidoctor core
1
2
  require_relative 'asciidoctor_ext/section'
2
3
  require_relative 'asciidoctor_ext/list'
3
4
  require_relative 'asciidoctor_ext/list_item'
5
+ require_relative 'asciidoctor_ext/image'
@@ -0,0 +1,18 @@
1
+ module Asciidoctor
2
+ module Image
3
+ class << self
4
+ def image_type path
5
+ (::File.extname path).downcase[1..-1]
6
+ end
7
+ end
8
+
9
+ def image_type
10
+ ::File.extname(inline? ? target : (attr 'target')).downcase[1..-1]
11
+ end
12
+
13
+ def target_with_image_type
14
+ image_path = inline? ? (target) : (attr 'target')
15
+ [image_path, (::File.extname image_path).downcase[1..-1]]
16
+ end
17
+ end
18
+ end
@@ -1,14 +1,16 @@
1
1
  # encoding: UTF-8
2
2
  # TODO cleanup imports...decide what belongs in asciidoctor-pdf.rb
3
- require_relative 'core_ext/array'
4
3
  require 'prawn'
5
4
  require 'prawn-svg'
6
5
  require 'prawn/table'
7
6
  require 'prawn/templates'
8
7
  require 'prawn/icon'
8
+ require_relative 'core_ext'
9
9
  require_relative 'pdf_core_ext'
10
+ require_relative 'temporary_path'
10
11
  require_relative 'sanitizer'
11
12
  require_relative 'prawn_ext'
13
+ require_relative 'formatted_text'
12
14
  require_relative 'pdfmarks'
13
15
  require_relative 'asciidoctor_ext'
14
16
  require_relative 'theme_loader'
@@ -55,9 +57,19 @@ class Converter < ::Prawn::Document
55
57
  circle: (unicode_char 0x25e6),
56
58
  square: (unicode_char 0x25aa)
57
59
  }
60
+ # NOTE Default theme font uses ballot boxes from FontAwesome
61
+ BallotBox = {
62
+ checked: (unicode_char 0x2611),
63
+ unchecked: (unicode_char 0x2610)
64
+ }
65
+ IconSets = ['fa', 'fi', 'octicon', 'pf']
66
+ MeasurementRxt = '\\d+(?:\\.\\d+)?(?:in|cm|mm|pt|)'
67
+ MeasurementPartsRx = /^(\d+(?:\.\d+)?)(in|mm|cm|pt|)$/
68
+ PageSizeRx = /^(?:\[(#{MeasurementRxt}), ?(#{MeasurementRxt})\]|(#{MeasurementRxt})(?: x |x)(#{MeasurementRxt})|\S+)$/
58
69
  # CalloutExtractRx synced from /lib/asciidoctor.rb of Asciidoctor core
59
70
  CalloutExtractRx = /(?:(?:\/\/|#|--|;;) ?)?(\\)?<!?(--|)(\d+)\2>(?=(?: ?\\?<!?\2\d+\2>)*$)/
60
71
  ImageAttributeValueRx = /^image:{1,2}(.*?)\[(.*?)\]$/
72
+ LineScanRx = /\n|.+/
61
73
 
62
74
  def initialize backend, opts
63
75
  super
@@ -91,7 +103,7 @@ class Converter < ::Prawn::Document
91
103
  if node.blocks?
92
104
  node.content
93
105
  elsif node.content_model != :compound && (string = node.content)
94
- # TODO this content could be catched on repeat invocations!
106
+ # TODO this content could be cached on repeat invocations!
95
107
  layout_prose string, opts
96
108
  end
97
109
  node.document.instance_variable_set :@converter, prev_converter if prev_converter
@@ -109,10 +121,10 @@ class Converter < ::Prawn::Document
109
121
  # FIXME implement fitting and centering for SVG
110
122
  # TODO implement image scaling (numeric value or "fit")
111
123
  float { canvas { image @page_bg_image, position: :center, fit: [bounds.width, bounds.height] } }
112
- elsif @page_bg_color
124
+ elsif @page_bg_color && @page_bg_color != 'FFFFFF'
113
125
  fill_absolute_bounds @page_bg_color
114
126
  end
115
- end
127
+ end if respond_to? :on_page_create
116
128
 
117
129
  layout_cover_page :front, doc
118
130
  layout_title_page doc
@@ -121,12 +133,12 @@ class Converter < ::Prawn::Document
121
133
 
122
134
  toc_start_page_num = page_number
123
135
  num_toc_levels = (doc.attr 'toclevels', 2).to_i
124
- if doc.attr? 'toc'
136
+ if (include_toc = doc.attr? 'toc')
125
137
  toc_page_nums = ()
126
138
  dry_run do
127
139
  toc_page_nums = layout_toc doc, num_toc_levels, 1
128
140
  end
129
- # reserve pages for the toc
141
+ # NOTE reserve pages for the toc
130
142
  toc_page_nums.each do
131
143
  start_new_page
132
144
  end
@@ -136,7 +148,7 @@ class Converter < ::Prawn::Document
136
148
  font @theme.base_font_family, size: @theme.base_font_size
137
149
  convert_content_for_block doc
138
150
 
139
- toc_page_nums = if doc.attr? 'toc'
151
+ toc_page_nums = if include_toc
140
152
  layout_toc doc, num_toc_levels, toc_start_page_num, num_front_matter_pages
141
153
  else
142
154
  (0..-1)
@@ -177,9 +189,7 @@ class Converter < ::Prawn::Document
177
189
  end
178
190
  end
179
191
  @fallback_fonts = [*theme.font_fallbacks]
180
- if ['FFFFFF', 'transparent'].include?(@page_bg_color = theme.page_background_color)
181
- @page_bg_color = nil
182
- end
192
+ @page_bg_color = resolve_theme_color :page_background_color, 'FFFFFF'
183
193
  @font_color = theme.base_font_color || '000000'
184
194
  @text_transform = nil
185
195
  @stamps = {}
@@ -197,37 +207,39 @@ class Converter < ::Prawn::Document
197
207
  skip_page_creation: true,
198
208
  }
199
209
 
200
- if doc.attr? 'pdf-page-size'
201
- page_size = ::YAML.safe_load(doc.attr 'pdf-page-size')
210
+ page_size = if (doc.attr? 'pdf-page-size') && (m = PageSizeRx.match(doc.attr 'pdf-page-size'))
211
+ # e.g, [8.5in, 11in]
212
+ if m[1]
213
+ [m[1], m[2]]
214
+ # e.g, 8.5in x 11in
215
+ elsif m[3]
216
+ [m[3], m[4]]
217
+ # e.g, A4
218
+ else
219
+ m[0]
220
+ end
202
221
  else
203
- page_size = theme.page_size
222
+ theme.page_size
204
223
  end
205
224
 
206
- pdf_opts[:page_size] = case page_size
225
+ page_size = case page_size
207
226
  when ::String
227
+ # TODO extract helper method to check for named page size
208
228
  if ::PDF::Core::PageGeometry::SIZES.key?(page_size = page_size.upcase)
209
229
  page_size
210
- else
211
- 'LETTER'
212
230
  end
213
231
  when ::Array
214
- page_size.fill(0..1) {|i| page_size[i] || 0 }.map {|d|
215
- if ::Numeric === d
216
- break if d == 0
217
- d
218
- elsif ::String === d && (m = /^(\d*(?:\.\d+)?)(in|mm|cm|pt)$/.match d)
219
- val = m[1].to_f
220
- val = case m[2]
221
- when 'in'
222
- val * 72
223
- when 'mm'
224
- val * (72 / 25.4)
225
- when 'cm'
226
- val * (720 / 25.4)
227
- when 'pt'
228
- val
229
- end
232
+ unless page_size.size == 2
233
+ page_size = page_size[0..1].fill(0..1) {|i| page_size[i] || 0}
234
+ end
235
+ page_size.map do |dim|
236
+ if ::Numeric === dim
237
+ # dimension cannot be less than 0
238
+ dim > 0 ? dim : break
239
+ elsif ::String === dim && (m = (MeasurementPartsRx.match dim))
240
+ val = to_pt m[1].to_f, m[2]
230
241
  # NOTE 4 is the max practical precision in PDFs
242
+ # QUESTION should we make rounding a feature of the to_pt method?
231
243
  if (val = val.round 4) == (i_val = val.to_i)
232
244
  val = i_val
233
245
  end
@@ -235,13 +247,12 @@ class Converter < ::Prawn::Document
235
247
  else
236
248
  break
237
249
  end
238
- }
250
+ end
239
251
  end
240
252
 
241
- pdf_opts[:page_size] ||= 'LETTER'
253
+ pdf_opts[:page_size] = (page_size || 'LETTER')
242
254
 
243
- # FIXME fix the namespace for FormattedTextFormatter
244
- pdf_opts[:text_formatter] ||= ::Asciidoctor::Prawn::FormattedTextFormatter.new theme: theme
255
+ pdf_opts[:text_formatter] ||= FormattedText::Formatter.new theme: theme
245
256
  pdf_opts
246
257
  end
247
258
 
@@ -328,7 +339,6 @@ class Converter < ::Prawn::Document
328
339
  end
329
340
  end
330
341
  # QUESTION should we be adding margin below the abstract??
331
- #move_down @theme.block_margin_bottom
332
342
  #theme_margin :block, :bottom
333
343
  end
334
344
 
@@ -353,6 +363,8 @@ class Converter < ::Prawn::Document
353
363
  prose_opts[:align] = :right
354
364
  when 'text-justify'
355
365
  prose_opts[:align] = :justify
366
+ when 'text-center'
367
+ prose_opts[:align] = :center
356
368
  when 'lead'
357
369
  is_lead = true
358
370
  #when 'signature'
@@ -376,7 +388,6 @@ class Converter < ::Prawn::Document
376
388
  # FIXME alignment of content is off
377
389
  def convert_admonition node
378
390
  add_dest_for_block node if node.id
379
- #move_down @theme.block_margin_top unless at_page_top?
380
391
  theme_margin :block, :top
381
392
  icons = node.document.attr? 'icons', 'font'
382
393
  label = icons ? (node.attr 'name').to_sym : node.caption.upcase
@@ -417,13 +428,11 @@ class Converter < ::Prawn::Document
417
428
  end
418
429
  #end
419
430
  end
420
- #move_down @theme.block_margin_bottom
421
431
  theme_margin :block, :bottom
422
432
  end
423
433
 
424
434
  def convert_example node
425
435
  add_dest_for_block node if node.id
426
- #move_down @theme.block_margin_top unless at_page_top?
427
436
  theme_margin :block, :top
428
437
  keep_together do |box_height = nil|
429
438
  caption_height = node.title? ? (layout_caption node) : 0
@@ -440,7 +449,6 @@ class Converter < ::Prawn::Document
440
449
  end
441
450
  end
442
451
  end
443
- #move_down @theme.block_margin_bottom
444
452
  theme_margin :block, :bottom
445
453
  end
446
454
 
@@ -465,7 +473,6 @@ class Converter < ::Prawn::Document
465
473
  def convert_quote_or_verse node
466
474
  add_dest_for_block node if node.id
467
475
  border_width = @theme.blockquote_border_width || 0
468
- #move_down @theme.block_margin_top unless at_page_top?
469
476
  theme_margin :block, :top
470
477
  keep_together do |box_height = nil|
471
478
  start_cursor = cursor
@@ -493,7 +500,6 @@ class Converter < ::Prawn::Document
493
500
  end
494
501
  end
495
502
  end
496
- #move_down @theme.block_margin_bottom
497
503
  theme_margin :block, :bottom
498
504
  end
499
505
 
@@ -502,7 +508,6 @@ class Converter < ::Prawn::Document
502
508
 
503
509
  def convert_sidebar node
504
510
  add_dest_for_block node if node.id
505
- #move_down @theme.block_margin_top unless at_page_top?
506
511
  theme_margin :block, :top
507
512
  keep_together do |box_height = nil|
508
513
  if box_height
@@ -526,19 +531,21 @@ class Converter < ::Prawn::Document
526
531
  move_up(@theme.prose_margin_bottom || @theme.vertical_rhythm)
527
532
  end
528
533
  end
529
- #move_down @theme.block_margin_bottom
530
534
  theme_margin :block, :bottom
531
535
  end
532
536
 
533
537
  def convert_colist node
534
538
  # HACK undo the margin below previous listing or literal block
535
539
  # TODO allow this to be set using colist_margin_top
536
- unless at_page_top? || (self_idx = node.parent.blocks.index node) == 0 ||
537
- ![:listing, :literal].include?(node.parent.blocks[self_idx - 1].context)
538
- move_up ((@theme.block_margin_bottom || @theme.vertical_rhythm) / 2.0)
539
- # or we could do...
540
- #move_up (@theme.block_margin_bottom || @theme.vertical_rhythm)
541
- #move_down (@theme.caption_margin_inside * 2)
540
+ unless at_page_top?
541
+ # NOTE this logic won't work for a colist nested inside a list item until Asciidoctor 1.5.3
542
+ if (self_idx = node.parent.blocks.index node) && self_idx > 0 &&
543
+ [:listing, :literal].include?(node.parent.blocks[self_idx - 1].context)
544
+ move_up ((@theme.block_margin_bottom || @theme.vertical_rhythm) / 2.0)
545
+ # or we could do...
546
+ #move_up (@theme.block_margin_bottom || @theme.vertical_rhythm)
547
+ #move_down (@theme.caption_margin_inside * 2)
548
+ end
542
549
  end
543
550
  add_dest_for_block node if node.id
544
551
  @list_numbers ||= []
@@ -584,7 +591,7 @@ class Converter < ::Prawn::Document
584
591
  # FIXME extract ensure_space (or similar) method
585
592
  start_new_page if cursor < @theme.base_line_height_length * (terms.size + 1)
586
593
  terms.each do |term|
587
- layout_prose term.text, style: @theme.description_list_term_font_style.to_sym, margin_top: 0, margin_bottom: (@theme.vertical_rhythm / 3.0), align: :left
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
588
595
  end
589
596
  if desc
590
597
  indent @theme.description_list_description_indent do
@@ -623,27 +630,30 @@ class Converter < ::Prawn::Document
623
630
  @list_numbers.pop
624
631
  end
625
632
 
626
- # TODO implement checklist
627
633
  def convert_ulist node
628
634
  add_dest_for_block node if node.id
629
- bullet_type = if (style = node.style)
630
- case style
631
- when 'bibliography'
632
- :square
633
- else
634
- style.to_sym
635
- end
635
+ if node.option? 'checklist'
636
+ @list_bullets << :checkbox
636
637
  else
637
- case (node.level % 3)
638
- when 1
639
- :disc
640
- when 2
641
- :circle
642
- when 0
643
- :square
638
+ bullet_type = if (style = node.style)
639
+ case style
640
+ when 'bibliography'
641
+ :square
642
+ else
643
+ style.to_sym
644
+ end
645
+ else
646
+ case (node.level % 3)
647
+ when 1
648
+ :disc
649
+ when 2
650
+ :circle
651
+ when 0
652
+ :square
653
+ end
644
654
  end
655
+ @list_bullets << Bullets[bullet_type]
645
656
  end
646
- @list_bullets << Bullets[bullet_type]
647
657
  convert_outline_list node
648
658
  @list_bullets.pop
649
659
  end
@@ -671,28 +681,39 @@ class Converter < ::Prawn::Document
671
681
 
672
682
  def convert_outline_list_item node, complex = false
673
683
  # TODO move this to a draw_bullet (or draw_marker) method
674
- marker = case (list_type = node.parent.context)
684
+ case (list_type = node.parent.context)
675
685
  when :ulist
676
- @list_bullets.last
686
+ marker = @list_bullets.last
687
+ if marker == :checkbox
688
+ if node.attr? 'checkbox'
689
+ marker = BallotBox[(node.attr? 'checked') ? :checked : :unchecked]
690
+ else
691
+ # QUESTION should we remove marker indent in this case?
692
+ marker = nil
693
+ end
694
+ end
677
695
  when :olist
678
696
  @list_numbers << (index = @list_numbers.pop).next
679
- %(#{index}.)
697
+ marker = %(#{index}.)
680
698
  else
681
699
  warn %(asciidoctor: WARNING: unknown list type #{list_type.inspect})
682
- Bullets[:disc]
700
+ marker = Bullets[:disc]
683
701
  end
684
702
 
685
- marker_width = width_of marker
686
- start_position = -marker_width + -(width_of 'x')
687
- float do
688
- bounding_box [start_position, cursor], width: marker_width do
689
- layout_prose marker,
690
- align: :right,
691
- normalize: false,
692
- inline_format: false,
693
- margin: 0,
694
- character_spacing: -0.5,
695
- single_line: true
703
+ if marker
704
+ marker_width = width_of marker
705
+ start_position = -marker_width + -(width_of 'x')
706
+ float do
707
+ bounding_box [start_position, cursor], width: marker_width do
708
+ layout_prose marker,
709
+ align: :right,
710
+ color: (@theme.outline_list_marker_font_color || @font_color),
711
+ normalize: false,
712
+ inline_format: false,
713
+ margin: 0,
714
+ character_spacing: -0.5,
715
+ single_line: true
716
+ end
696
717
  end
697
718
  end
698
719
 
@@ -713,14 +734,13 @@ class Converter < ::Prawn::Document
713
734
  end
714
735
 
715
736
  def convert_image node
737
+ node.extend ::Asciidoctor::Image unless ::Asciidoctor::Image === node
716
738
  valid_image = true
717
- target = node.attr 'target'
718
- # TODO file extension should be an attribute on an image node
719
- image_type = (::File.extname target)[1..-1].downcase
739
+ target, image_type = node.target_with_image_type
720
740
 
721
741
  if image_type == 'gif'
722
742
  valid_image = false
723
- warn %(asciidoctor: WARNING: GIF image format not supported. Please convert the image #{target} to PNG.)
743
+ warn %(asciidoctor: WARNING: GIF image format not supported. Please convert #{target} to PNG.)
724
744
  #elsif image_type == 'pdf'
725
745
  # import_page image_path
726
746
  # return
@@ -731,11 +751,22 @@ class Converter < ::Prawn::Document
731
751
  warn %(asciidoctor: WARNING: image to embed not found or not readable: #{image_path || target})
732
752
  end
733
753
 
754
+ # QUESTION if we advance to new page, shouldn't dest point there too?
734
755
  add_dest_for_block node if node.id
756
+ position = ((node.attr 'align') || @theme.image_align || :left).to_sym
735
757
 
736
758
  unless valid_image
737
759
  theme_margin :block, :top
738
- layout_prose %(#{node.attr 'alt'} | #{target}), normalize: false, margin: 0, single_line: true
760
+ if (link = node.attr 'link')
761
+ alt_text = %(<a href="#{link}">[#{NoBreakSpace}#{node.attr 'alt'}#{NoBreakSpace}]</a> | <em>#{target}</em>)
762
+ else
763
+ alt_text = %([#{NoBreakSpace}#{node.attr 'alt'}#{NoBreakSpace}] | <em>#{target}</em>)
764
+ end
765
+ layout_prose alt_text,
766
+ normalize: false,
767
+ margin: 0,
768
+ single_line: true,
769
+ align: position
739
770
  layout_caption node, position: :bottom if node.title?
740
771
  theme_margin :block, :bottom
741
772
  return
@@ -744,37 +775,35 @@ class Converter < ::Prawn::Document
744
775
  theme_margin :block, :top
745
776
 
746
777
  # TODO support cover (aka canvas) image layout using "canvas" (or "cover") role
747
- width = if node.attr? 'scaledwidth'
748
- ((node.attr 'scaledwidth').to_f / 100.0) * bounds.width
749
- elsif image_type == 'svg'
750
- bounds.width
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
751
787
  elsif node.attr? 'width'
752
- (node.attr 'width').to_f
753
- else
754
- bounds.width * (@theme.image_scaled_width_default || 0.75)
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
755
790
  end
756
- position = ((node.attr 'align') || @theme.image_align_default || :left).to_sym
791
+
757
792
  case image_type
758
793
  when 'svg'
759
- # NOTE prawn-svg can't position, so we have to do it manually (file issue?)
760
- left = case position
761
- when :left
762
- 0
763
- when :right
764
- bounds.width - width
765
- when :center
766
- ((bounds.width - width) / 2.0).floor
767
- end
768
794
  begin
769
- img_data = ::IO.read image_path
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]
799
+ # TODO layout SVG without using keep_together (since we know the dimensions already); always render caption
770
800
  keep_together do |box_height = nil|
801
+ svg_obj.instance_variable_set :@prawn, self
802
+ svg_obj.draw
771
803
  if box_height && (link = node.attr 'link')
772
- result = svg img_data, at: [left, cursor], width: width
773
- link_annotation [(abs_left = left + bounds.absolute_left), y, (abs_left + width), (y + result[:height])],
774
- Border: [0, 0, 0],
775
- A: { Type: :Action, S: :URI, URI: (str2pdfval link) }
776
- else
777
- svg img_data, at: [left, cursor], width: width
804
+ link_annotation [(abs_left = rel_left + bounds.absolute_left), y, (abs_left + actual_w), (y + actual_h)],
805
+ Border: [0, 0, 0],
806
+ A: { Type: :Action, S: :URI, URI: (str2pdfval link) }
778
807
  end
779
808
  layout_caption node, position: :bottom if node.title?
780
809
  end
@@ -784,32 +813,37 @@ class Converter < ::Prawn::Document
784
813
  else
785
814
  begin
786
815
  # FIXME temporary workaround to group caption & image
787
- # Prawn doesn't provide access to rendered width and height before placing the
788
- # image on the page
816
+ # Prawn doesn't provide access to rendered width & height before placing image on page
789
817
  # FIXME this code really needs to be better organized!
790
818
  image_obj, image_info = build_image_object image_path
791
- rendered_w, rendered_h = image_info.calc_image_dimensions width: width
819
+ if width
820
+ rendered_w, rendered_h = image_info.calc_image_dimensions width: width
821
+ else
822
+ # NOTE native size is in pixels, so scale by 75%; restrict to max width of bounds.width
823
+ rendered_w = [bounds.width, image_info.width * 0.75].min
824
+ rendered_h = (rendered_w * image_info.height) / image_info.width
825
+ end
792
826
  # TODO move this calculation into a method
793
827
  caption_height = node.title? ?
794
828
  (@theme.caption_margin_inside + @theme.caption_margin_outside + @theme.base_line_height_length) : 0
795
- height = nil
796
- if cursor < rendered_h + caption_height
797
- start_new_page
798
- if cursor < rendered_h + caption_height
799
- height = (cursor - caption_height).floor
800
- width = ((rendered_w * height) / rendered_h).floor
829
+ if rendered_h > (available_height = cursor - caption_height)
830
+ start_new_page unless at_page_top?
831
+ # NOTE shrink image so it fits on a single page
832
+ if rendered_h > (available_height = cursor - caption_height)
833
+ rendered_w = (rendered_w * available_height) / rendered_h
834
+ rendered_h = available_height
801
835
  # FIXME workaround to fix Prawn not adding fill and stroke commands
802
836
  # on page that only has an image; breakage occurs when line numbers are added
803
837
  fill_color self.fill_color
804
838
  stroke_color self.stroke_color
805
839
  end
806
840
  end
841
+ # NOTE must calculate link position before embedding to get proper boundaries
807
842
  if (link = node.attr 'link')
808
- actual_w, actual_h = [(width || rendered_w), (height || rendered_h)]
809
- img_x, img_y = image_position actual_w, actual_h, position: position
810
- link_box = [img_x, (img_y - actual_h), img_x + actual_w, img_y]
843
+ img_x, img_y = image_position rendered_w, rendered_h, position: position
844
+ link_box = [img_x, (img_y - rendered_h), (img_x + rendered_w), img_y]
811
845
  end
812
- embed_image image_obj, image_info, width: width, height: height, position: position
846
+ embed_image image_obj, image_info, width: rendered_w, position: position
813
847
  if link
814
848
  link_annotation link_box,
815
849
  Border: [0, 0, 0],
@@ -825,7 +859,7 @@ class Converter < ::Prawn::Document
825
859
  unlink_tmp_file image_path
826
860
  end
827
861
 
828
- # TODO shrink text if it's too wide to fit in the bounding box
862
+ # QUESTION can we avoid arranging fragments multiple times (conums & autofit) by eagerly preparing arranger?
829
863
  def convert_listing_or_literal node
830
864
  add_dest_for_block node if node.id
831
865
  # HACK disable built-in syntax highlighter; must be done before calling node.content!
@@ -834,13 +868,11 @@ class Converter < ::Prawn::Document
834
868
  highlighter = node.document.attr 'source-highlighter'
835
869
  # NOTE the source highlighter logic below handles the callouts and highlight subs
836
870
  prev_subs = subs.dup
837
- subs.delete :callouts
838
- subs.delete :highlight
871
+ subs.delete_all :highlight, :callouts
839
872
  else
840
873
  highlighter = nil
841
874
  prev_subs = nil
842
875
  end
843
- # FIXME source highlighter freaks out about the non-breaking space characters; does it?
844
876
  source_string = preserve_indentation node.content
845
877
  source_chunks = case highlighter
846
878
  when 'coderay'
@@ -858,28 +890,55 @@ class Converter < ::Prawn::Document
858
890
  conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
859
891
  else
860
892
  # NOTE only format if we detect a need
861
- (source_string =~ BuiltInEntityCharOrTagRx) ? (text_formatter.format source_string) : [{ text: source_string }]
893
+ if source_string =~ BuiltInEntityCharOrTagRx
894
+ text_formatter.format source_string
895
+ else
896
+ [{ text: source_string }]
897
+ end
862
898
  end
863
899
 
864
900
  node.subs.replace prev_subs if prev_subs
865
901
 
866
- #move_down @theme.block_margin_top unless at_page_top?
867
902
  theme_margin :block, :top
868
903
 
904
+ if (node.option? 'autofit') || (node.document.attr? 'autofit-option')
905
+ adjusted_font_size = theme_font_size_autofit source_chunks, :code
906
+ else
907
+ adjusted_font_size = nil
908
+ end
909
+
869
910
  keep_together do |box_height = nil|
870
911
  caption_height = node.title? ? (layout_caption node) : 0
871
912
  theme_font :code do
872
913
  if box_height
873
914
  float do
874
- # FIXME don't use border / border radius at page boundaries
875
- # TODO move this logic to theme_fill_and_stroke_bounds
915
+ # TODO move the multi-page logic to theme_fill_and_stroke_bounds
916
+ unless (b_width = @theme.code_border_width || 0) == 0
917
+ b_radius = (@theme.code_border_radius || 0) + b_width
918
+ bg_color = @theme.code_background_color || @page_bg_color
919
+ end
876
920
  remaining_height = box_height - caption_height
877
921
  i = 0
878
922
  while remaining_height > 0
879
- start_new_page if i > 0
923
+ start_new_page if (new_page_started = i > 0)
880
924
  fill_height = [remaining_height, cursor].min
881
925
  bounding_box [0, cursor], width: bounds.width, height: fill_height do
882
926
  theme_fill_and_stroke_bounds :code
927
+ unless b_width == 0
928
+ if new_page_started
929
+ indent b_radius, b_radius do
930
+ # dashed line to indicate continuation from previous page
931
+ stroke_horizontal_rule bg_color, line_width: b_width, line_style: :dashed
932
+ end
933
+ end
934
+ if remaining_height > fill_height
935
+ move_down fill_height
936
+ indent b_radius, b_radius do
937
+ # dashed line to indicate continuation on next page
938
+ stroke_horizontal_rule bg_color, line_width: b_width, line_style: :dashed
939
+ end
940
+ end
941
+ end
883
942
  end
884
943
  remaining_height -= fill_height
885
944
  i += 1
@@ -888,13 +947,14 @@ class Converter < ::Prawn::Document
888
947
  end
889
948
 
890
949
  pad_box @theme.code_padding do
891
- typeset_formatted_text source_chunks, (calc_line_metrics @theme.code_line_height), color: @theme.code_font_color
950
+ typeset_formatted_text source_chunks, (calc_line_metrics @theme.code_line_height),
951
+ color: (@theme.code_font_color || @font_color),
952
+ size: adjusted_font_size
892
953
  end
893
954
  end
894
955
  end
895
956
  stroke_horizontal_rule @theme.caption_border_bottom_color if node.title? && @theme.caption_border_bottom_color
896
957
 
897
- #move_down @theme.block_margin_bottom
898
958
  theme_margin :block, :bottom
899
959
  end
900
960
 
@@ -923,6 +983,9 @@ class Converter < ::Prawn::Document
923
983
  end
924
984
 
925
985
  # Restore the conums into the Array of formatted text fragments
986
+ #--
987
+ # QUESTION can this be done more efficiently?
988
+ # QUESTION can we reuse arrange_fragments_by_line?
926
989
  def restore_conums fragments, conum_mapping
927
990
  lines = []
928
991
  line_num = 0
@@ -943,16 +1006,16 @@ class Converter < ::Prawn::Document
943
1006
  conum_color = @theme.conum_font_color
944
1007
  last_line_num = lines.size - 1
945
1008
  # append conums to appropriate lines, then flatten to an array of fragments
946
- lines.flat_map.with_index do |line, line_num|
947
- if (conums = conum_mapping.delete line_num)
1009
+ lines.flat_map.with_index do |line, cur_line_num|
1010
+ if (conums = conum_mapping.delete cur_line_num)
948
1011
  conums = conums.map {|num| conum_glyph num }
949
1012
  # ensure there's at least one space between content and conum(s)
950
1013
  if line.size > 0 && (end_text = line.last[:text]) && !(end_text.end_with? ' ')
951
1014
  line.last[:text] = %(#{end_text} )
952
1015
  end
953
- line << { text: (conums * ' '), color: conum_color }
1016
+ line << (conum_color ? { text: (conums * ' '), color: conum_color } : { text: (conums * ' ') })
954
1017
  end
955
- line << { text: EOL } unless line_num == last_line_num
1018
+ line << { text: EOL } unless cur_line_num == last_line_num
956
1019
  line
957
1020
  end
958
1021
  end
@@ -973,30 +1036,16 @@ class Converter < ::Prawn::Document
973
1036
  table_header = false
974
1037
  theme = @theme
975
1038
 
976
- # FIXME this is a mess!
977
- unless (page_bg_color = theme.page_background_color) && page_bg_color != 'transparent'
978
- page_bg_color = nil
979
- end
980
-
981
- unless (bg_color = theme.table_background_color) && bg_color != 'transparent'
982
- bg_color = page_bg_color
983
- end
984
-
985
- unless (head_bg_color = theme.table_head_background_color) && head_bg_color != 'transparent'
986
- head_bg_color = bg_color
987
- end
988
-
989
- unless (foot_bg_color = theme.table_foot_background_color) && foot_bg_color != 'transparent'
990
- foot_bg_color = bg_color
991
- end
992
-
993
- unless (odd_row_bg_color = theme.table_odd_row_background_color) && odd_row_bg_color != 'transparent'
994
- odd_row_bg_color = bg_color
995
- end
996
-
997
- unless (even_row_bg_color = theme.table_even_row_background_color) && even_row_bg_color != 'transparent'
998
- even_row_bg_color = bg_color
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
999
1044
  end
1045
+ head_bg_color = resolve_theme_color :table_head_background_color, tbl_bg_color
1046
+ foot_bg_color = resolve_theme_color :table_foot_background_color, tbl_bg_color
1047
+ odd_row_bg_color = resolve_theme_color :table_odd_row_background_color, tbl_bg_color
1048
+ even_row_bg_color = resolve_theme_color :table_even_row_background_color, tbl_bg_color
1000
1049
 
1001
1050
  table_data = []
1002
1051
  node.rows[:head].each do |rows|
@@ -1141,16 +1190,19 @@ class Converter < ::Prawn::Document
1141
1190
  end
1142
1191
 
1143
1192
  def convert_thematic_break node
1144
- #move_down @theme.thematic_break_margin_top
1145
1193
  theme_margin :thematic_break, :top
1146
- stroke_horizontal_rule @theme.thematic_break_border_color, line_width: @theme.thematic_break_border_width
1147
- #move_down @theme.thematic_break_margin_bottom
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
1148
1195
  theme_margin :thematic_break, :bottom
1149
1196
  end
1150
1197
 
1151
1198
  # deprecated
1152
1199
  alias :convert_horizontal_rule :convert_thematic_break
1153
1200
 
1201
+ # NOTE manual placement not yet possible, so return nil
1202
+ def convert_toc node
1203
+ nil
1204
+ end
1205
+
1154
1206
  def convert_page_break node
1155
1207
  start_new_page unless at_page_top?
1156
1208
  end
@@ -1208,7 +1260,7 @@ class Converter < ::Prawn::Document
1208
1260
  end
1209
1261
 
1210
1262
  def convert_inline_button node
1211
- %(<b>[#{NarrowNoBreakSpace}#{node.text}#{NarrowNoBreakSpace}]</b>)
1263
+ %(<strong>[#{NarrowNoBreakSpace}#{node.text}#{NarrowNoBreakSpace}]</strong>)
1212
1264
  end
1213
1265
 
1214
1266
  def convert_inline_callout node
@@ -1216,20 +1268,65 @@ class Converter < ::Prawn::Document
1216
1268
  # NOTE CMYK value gets flattened here, but is restored by formatted text parser
1217
1269
  %(<color rgb="#{conum_color}">#{conum_glyph node.text.to_i}</color>)
1218
1270
  else
1219
- node.text
1271
+ conum_glyph node.text.to_i
1220
1272
  end
1221
1273
  end
1222
1274
 
1223
1275
  def convert_inline_footnote node
1224
1276
  if (index = node.attr 'index')
1225
1277
  #text = node.document.footnotes.find {|fn| fn.index == index }.text
1226
- %( [#{node.text}])
1278
+ %(<sup>[#{index}: #{node.text}]</sup>)
1227
1279
  elsif node.type == :xref
1228
1280
  # NOTE footnote reference not found
1229
- %( <color rgb="FF0000">[#{node.text}]</color>)
1281
+ %(<sup><color rgb="FF0000">[#{node.text}]</color></sup>)
1230
1282
  end
1231
1283
  end
1232
1284
 
1285
+ def convert_inline_image node
1286
+ img = nil
1287
+ if node.type == 'icon'
1288
+ if node.document.attr? 'icons', 'font'
1289
+ if (icon_name = node.target).include? '@'
1290
+ icon_name, icon_set = icon_name.split '@', 2
1291
+ else
1292
+ icon_set = node.attr 'set', (node.document.attr 'icon-set', 'fa')
1293
+ end
1294
+ icon_set = 'fa' unless IconSets.include? icon_set
1295
+ if node.attr? 'size'
1296
+ size = (size = (node.attr 'size')) == 'lg' ? '1.3333em' : (size.sub 'x', 'em')
1297
+ size_attr = %( size="#{size}")
1298
+ else
1299
+ size_attr = nil
1300
+ end
1301
+ @icon_font_data ||= ::Prawn::Icon::FontData.load self, icon_set
1302
+ begin
1303
+ # TODO support rotate and flip attributes; support fw (full-width) size
1304
+ img = %(<font name="#{icon_set}"#{size_attr}>#{@icon_font_data.unicode icon_name}</font>)
1305
+ rescue
1306
+ warn %(asciidoctor: WARNING: #{icon_name} is not a valid icon name in the #{icon_set} icon set)
1307
+ end
1308
+ end
1309
+ else
1310
+ node.extend ::Asciidoctor::Image unless ::Asciidoctor::Image === node
1311
+ target, image_type = node.target_with_image_type
1312
+ valid = true
1313
+ if image_type == 'gif'
1314
+ warn %(asciidoctor: WARNING: GIF image format not supported. Please convert #{target} to PNG.) unless scratch?
1315
+ valid = false
1316
+ end
1317
+ unless (image_path = resolve_image_path node, target) && (::File.readable? image_path)
1318
+ warn %(asciidoctor: WARNING: image to embed not found or not readable: #{image_path || target}) unless scratch?
1319
+ valid = false
1320
+ end
1321
+ if valid
1322
+ width_attr = (node.attr? 'width') ? %( width="#{node.attr 'width'}") : nil
1323
+ img = %(<img src="#{image_path}" type="#{image_type}" alt="#{node.attr 'alt'}"#{width_attr} tmp="#{TemporaryPath === image_path}">)
1324
+ end
1325
+ end
1326
+ img ||= %([#{node.attr 'alt'}])
1327
+ (node.attr? 'link') ? %(<a href="#{node.attr 'link'}">#{img}</a>) : img
1328
+ end
1329
+
1233
1330
  def convert_inline_indexterm node
1234
1331
  node.type == :visible ? node.text : nil
1235
1332
  end
@@ -1317,7 +1414,7 @@ class Converter < ::Prawn::Document
1317
1414
  end
1318
1415
  end
1319
1416
  end
1320
- if !bg_image && (bg_color = @theme.title_page_background_color) && bg_color != 'transparent'
1417
+ if !bg_image && (bg_color = resolve_theme_color :title_page_background_color)
1321
1418
  @page_bg_color = bg_color
1322
1419
  else
1323
1420
  bg_color = nil
@@ -1419,7 +1516,7 @@ class Converter < ::Prawn::Document
1419
1516
  end
1420
1517
  # QUESTION should we go to page 1 when position == :front?
1421
1518
  go_to_page page_count if position == :back
1422
- if (::File.extname cover_image) == '.pdf'
1519
+ if cover_image.downcase.end_with? '.pdf'
1423
1520
  import_page cover_image
1424
1521
  else
1425
1522
  image_page cover_image, canvas: true
@@ -1441,26 +1538,24 @@ class Converter < ::Prawn::Document
1441
1538
 
1442
1539
  # QUESTION why doesn't layout_heading set the font??
1443
1540
  def layout_heading string, opts = {}
1444
- margin_top = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme.heading_margin_top
1445
- margin_bottom = margin || (opts.delete :margin_bottom) || @theme.heading_margin_bottom
1541
+ top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme.heading_margin_top
1542
+ bot_margin = margin || (opts.delete :margin_bottom) || @theme.heading_margin_bottom
1446
1543
  if (transform = (opts.delete :text_transform) || @text_transform)
1447
1544
  string = transform_text string, transform
1448
1545
  end
1449
- #move_down margin_top
1450
- self.margin_top margin_top
1546
+ margin_top top_margin
1451
1547
  typeset_text string, calc_line_metrics((opts.delete :line_height) || @theme.heading_line_height), {
1452
1548
  color: @font_color,
1453
1549
  inline_format: true,
1454
1550
  align: :left
1455
1551
  }.merge(opts)
1456
- #move_down margin_bottom
1457
- self.margin_bottom margin_bottom
1552
+ margin_bottom bot_margin
1458
1553
  end
1459
1554
 
1460
1555
  # NOTE inline_format is true by default
1461
1556
  def layout_prose string, opts = {}
1462
1557
  top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme.prose_margin_top || 0
1463
- bottom_margin = margin || (opts.delete :margin_bottom) || @theme.prose_margin_bottom || @theme.vertical_rhythm
1558
+ bot_margin = margin || (opts.delete :margin_bottom) || @theme.prose_margin_bottom || @theme.vertical_rhythm
1464
1559
  if (transform = (opts.delete :text_transform) || @text_transform)
1465
1560
  string = transform_text string, transform
1466
1561
  end
@@ -1482,7 +1577,7 @@ class Converter < ::Prawn::Document
1482
1577
  inline_format: [{ normalize: (opts.delete :normalize) != false }],
1483
1578
  align: (@theme.base_align || :left).to_sym
1484
1579
  }.merge(opts)
1485
- margin_bottom bottom_margin
1580
+ margin_bottom bot_margin
1486
1581
  end
1487
1582
 
1488
1583
  # Render the caption and return the height of the rendered content
@@ -1545,7 +1640,7 @@ class Converter < ::Prawn::Document
1545
1640
  end
1546
1641
 
1547
1642
  def layout_toc_level sections, num_levels, line_metrics, dot_width, num_front_matter_pages = 0
1548
- toc_dot_color = @theme.toc_dot_leader_color
1643
+ toc_dot_color = @theme.toc_dot_leader_font_color || @theme.toc_font_color || @font_color
1549
1644
  sections.each do |sect|
1550
1645
  theme_font :toc, level: (sect.level + 1) do
1551
1646
  sect_title = @text_transform ? (transform_text sect.numbered_title, @text_transform) : sect.numbered_title
@@ -1566,6 +1661,7 @@ class Converter < ::Prawn::Document
1566
1661
  spacer_width = (width_of NoBreakSpace) * 0.75
1567
1662
  # FIXME this calculation will be wrong if a style is set per level
1568
1663
  num_dots = ((bounds.width - (width_of %(#{sect_title}#{sect_page_num}), inline_format: true) - spacer_width) / dot_width).floor
1664
+ num_dots = 0 if num_dots < 0
1569
1665
  # FIXME dots don't line up if width of page numbers differ
1570
1666
  typeset_formatted_text [
1571
1667
  { text: %(#{(@theme.toc_dot_leader_content || DotLeaderDefault) * num_dots}), color: toc_dot_color },
@@ -1675,10 +1771,11 @@ class Converter < ::Prawn::Document
1675
1771
  trim_content_height = trim_height - trim_padding[0] - trim_padding[2] - trim_line_metrics.padding_top
1676
1772
  trim_left = page_margin_left
1677
1773
  trim_width = page_width - trim_left - page_margin_right
1678
- trim_font_color = @theme.header_font_color
1679
- trim_bg_color = @theme.header_background_color
1774
+ trim_font_color = @theme.header_font_color || @font_color
1775
+ trim_bg_color = resolve_theme_color :header_background_color
1680
1776
  trim_border_width = @theme.header_border_width || @theme.base_border_width
1681
- trim_border_color = @theme.header_border_color
1777
+ trim_border_style = (@theme.header_border_style || :solid).to_sym
1778
+ trim_border_color = resolve_theme_color :header_border_color
1682
1779
  trim_valign = (@theme.header_valign || :center).to_sym
1683
1780
  trim_img_valign = @theme.header_image_valign || trim_valign
1684
1781
  else
@@ -1688,10 +1785,11 @@ class Converter < ::Prawn::Document
1688
1785
  trim_content_height = trim_height - trim_padding[0] - trim_padding[2] - trim_line_metrics.padding_top
1689
1786
  trim_left = page_margin_left
1690
1787
  trim_width = page_width - trim_left - page_margin_right
1691
- trim_font_color = @theme.footer_font_color
1692
- trim_bg_color = @theme.footer_background_color
1788
+ trim_font_color = @theme.footer_font_color || @font_color
1789
+ trim_bg_color = resolve_theme_color :footer_background_color
1693
1790
  trim_border_width = @theme.footer_border_width || @theme.base_border_width
1694
- trim_border_color = @theme.footer_border_color
1791
+ trim_border_style = (@theme.footer_border_style || :solid).to_sym
1792
+ trim_border_color = resolve_theme_color :footer_border_color
1695
1793
  trim_valign = (@theme.footer_valign || :center).to_sym
1696
1794
  trim_img_valign = @theme.footer_image_valign || trim_valign
1697
1795
  end
@@ -1699,9 +1797,7 @@ class Converter < ::Prawn::Document
1699
1797
  trim_stamp = %(#{position})
1700
1798
  trim_content_left = trim_left + trim_padding[3]
1701
1799
  trim_content_width = trim_width - trim_padding[3] - trim_padding[1]
1702
- # NOTE FFFFFF is meaningful value for background and border, so don't scrap it
1703
- trim_bg_color = nil if trim_bg_color == 'transparent'
1704
- trim_border_color = nil if trim_border_color == 'transparent' || trim_border_width == 0
1800
+ trim_border_color = nil if trim_border_width == 0
1705
1801
  if ['top', 'center', 'bottom'].include? trim_img_valign
1706
1802
  trim_img_valign = trim_img_valign.to_sym
1707
1803
  end
@@ -1715,15 +1811,14 @@ class Converter < ::Prawn::Document
1715
1811
  if trim_border_color
1716
1812
  # TODO stroke_horizontal_rule should support :at
1717
1813
  move_down bounds.height if position == :header
1718
- stroke_horizontal_rule trim_border_color, line_width: trim_border_width
1814
+ stroke_horizontal_rule trim_border_color, line_width: trim_border_width, line_style: trim_border_style
1719
1815
  end
1720
1816
  end
1721
1817
  else
1722
1818
  bounding_box [trim_left, trim_top], width: trim_width, height: trim_height do
1723
1819
  # TODO stroke_horizontal_rule should support :at
1724
1820
  move_down bounds.height if position == :header
1725
- stroke_horizontal_rule trim_border_color, line_width: trim_border_width
1726
- move_up bounds.height if position == :header
1821
+ stroke_horizontal_rule trim_border_color, line_width: trim_border_width, line_style: trim_border_style
1727
1822
  end
1728
1823
  end
1729
1824
  end
@@ -1736,7 +1831,7 @@ class Converter < ::Prawn::Document
1736
1831
  next if page.imported_page?
1737
1832
  visual_pgnum = page_number - skip
1738
1833
  # FIXME we need to have a content setting for chapter pages
1739
- content_by_alignment = content_dict[side = visual_pgnum.odd? ? :recto : :verso]
1834
+ content_by_alignment = content_dict[visual_pgnum.odd? ? :recto : :verso]
1740
1835
  doc.set_attr 'page-number', visual_pgnum
1741
1836
  # TODO populate chapter-number
1742
1837
  # TODO populate numbered and unnumbered chapter and section titles
@@ -1841,7 +1936,8 @@ class Converter < ::Prawn::Document
1841
1936
 
1842
1937
  def write pdf_doc, target
1843
1938
  pdf_doc.render_file target
1844
- #@prototype.render_file 'scratch.pdf'
1939
+ # write scratch document if debug is enabled (or perhaps DEBUG_STEPS env)
1940
+ #get_scratch_document.render_file 'scratch.pdf'
1845
1941
  # QUESTION restore attributes first?
1846
1942
  @pdfmarks.generate_file target if @pdfmarks
1847
1943
  end
@@ -1860,6 +1956,16 @@ class Converter < ::Prawn::Document
1860
1956
  ::File.absolute_path font_file, fonts_dir
1861
1957
  end
1862
1958
 
1959
+ # QUESTION should we pass a category as an argument?
1960
+ # QUESTION should we make this a method on the theme ostruct? (e.g., @theme.resolve_color key, fallback)
1961
+ def resolve_theme_color key, fallback_color = nil
1962
+ if (color = @theme[key.to_s]) && color != 'transparent'
1963
+ color
1964
+ else
1965
+ fallback_color
1966
+ end
1967
+ end
1968
+
1863
1969
  def theme_fill_and_stroke_bounds category
1864
1970
  fill_and_stroke_bounds @theme[%(#{category}_background_color)], @theme[%(#{category}_border_color)],
1865
1971
  line_width: @theme[%(#{category}_border_width)],
@@ -1927,6 +2033,73 @@ class Converter < ::Prawn::Document
1927
2033
  @text_transform = prev_transform if transform
1928
2034
  end
1929
2035
 
2036
+ # Calculate the font size (down to the minimum font size) that would allow
2037
+ # all the specified fragments to fit in the available width without wrapping lines.
2038
+ #
2039
+ # Return the calculated font size if an adjustment is necessary or nil if no
2040
+ # font size adjustment is necessary.
2041
+ def theme_font_size_autofit fragments, category
2042
+ arranger = arrange_fragments_by_line fragments
2043
+ adjusted_font_size = nil
2044
+ theme_font category do
2045
+ # NOTE finalizing the line here generates fragments using current font settings
2046
+ arranger.finalize_line
2047
+ actual_width = width_of_fragments arranger.fragments
2048
+ unless ::Array === (padding = @theme[%(#{category}_padding)])
2049
+ padding = [padding] * 4
2050
+ 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
2055
+ if (min = @theme[%(#{category}_font_size_min)] || @theme.base_font_size_min) && adjusted_font_size < min
2056
+ adjusted_font_size = min
2057
+ end
2058
+ end
2059
+ bounds.subtract_left_padding p_left
2060
+ bounds.subtract_right_padding p_right
2061
+ end
2062
+ adjusted_font_size
2063
+ end
2064
+
2065
+ # Arrange fragments by line in an arranger and return an unfinalized arranger.
2066
+ #
2067
+ # Finalizing the arranger is deferred since it must be done in the context of
2068
+ # the global font settings you want applied to each fragment.
2069
+ def arrange_fragments_by_line fragments, opts = {}
2070
+ arranger = ::Prawn::Text::Formatted::Arranger.new self
2071
+ by_line = arranger.consumed = []
2072
+ fragments.each do |fragment|
2073
+ if (txt = fragment[:text]) == EOL
2074
+ by_line << fragment
2075
+ elsif txt.include? EOL
2076
+ txt.scan(LineScanRx) do |line|
2077
+ by_line << fragment.merge(text: line)
2078
+ end
2079
+ else
2080
+ by_line << fragment
2081
+ end
2082
+ end
2083
+ arranger
2084
+ end
2085
+
2086
+ # Calculate the width that is needed to print all the
2087
+ # fragments without wrapping any lines.
2088
+ #
2089
+ # This method assumes endlines are represented as discrete entries in the
2090
+ # fragments array.
2091
+ def width_of_fragments fragments
2092
+ line_widths = [0]
2093
+ fragments.each do |fragment|
2094
+ if fragment.text == EOL
2095
+ line_widths << 0
2096
+ else
2097
+ line_widths[-1] += fragment.width
2098
+ end
2099
+ end
2100
+ line_widths.max
2101
+ end
2102
+
1930
2103
  # TODO document me, esp the first line formatting functionality
1931
2104
  def typeset_text string, line_metrics, opts = {}
1932
2105
  move_down line_metrics.padding_top
@@ -2001,17 +2174,18 @@ class Converter < ::Prawn::Document
2001
2174
  # the temporary file. If the target is a URI and the allow-uri-read attribute
2002
2175
  # is not set, or the URI cannot be read, this method returns a nil value.
2003
2176
  #
2004
- # When a temporary file is used, the file descriptor is assigned to the
2005
- # @tmp_file instance variable of the return string.
2177
+ # When a temporary file is used, the TemporaryPath type is mixed into the path string.
2006
2178
  def resolve_image_path node, image_path = nil, image_type = nil
2007
2179
  imagesdir = resolve_imagesdir(doc = node.document)
2008
2180
  image_path ||= (node.attr 'target', nil, false)
2009
- image_type ||= (::File.extname image_path)[1..-1]
2181
+ image_type ||= ::Asciidoctor::Image.image_type image_path
2010
2182
  # handle case when image is a URI
2011
2183
  if (node.is_uri? image_path) || (imagesdir && (node.is_uri? imagesdir) &&
2012
2184
  (image_path = (node.normalize_web_path image_path, image_base_uri, false)))
2013
2185
  unless doc.attr? 'allow-uri-read'
2014
- warn %(asciidoctor: WARNING: allow-uri-read is not enabled; cannot embed remote image: #{image_path})
2186
+ unless scratch?
2187
+ warn %(asciidoctor: WARNING: allow-uri-read is not enabled; cannot embed remote image: #{image_path})
2188
+ end
2015
2189
  return
2016
2190
  end
2017
2191
  if doc.attr? 'cache-uri'
@@ -2022,7 +2196,7 @@ class Converter < ::Prawn::Document
2022
2196
  begin
2023
2197
  open(image_path, (binary ? 'rb' : 'r')) {|fd| tmp_image.write(fd.read) }
2024
2198
  tmp_image_path = tmp_image.path
2025
- tmp_image_path.instance_variable_set :@tmp_file, tmp_image
2199
+ tmp_image_path.extend TemporaryPath
2026
2200
  rescue
2027
2201
  tmp_image_path = nil
2028
2202
  ensure
@@ -2037,11 +2211,9 @@ class Converter < ::Prawn::Document
2037
2211
 
2038
2212
  # QUESTION is there a better way to do this?
2039
2213
  # I suppose we could have @tmp_files as an instance variable on converter instead
2040
- def unlink_tmp_file holder
2041
- if (tmp_file = (holder.instance_variable_get :@tmp_file))
2042
- tmp_file.unlink
2043
- holder.remove_instance_variable :@tmp_file
2044
- end
2214
+ # It might be sufficient to delete temporary files once per conversion
2215
+ def unlink_tmp_file path
2216
+ path.unlink if TemporaryPath === path
2045
2217
  end
2046
2218
 
2047
2219
  # QUESTION move to prawn/extensions.rb?
@@ -2064,22 +2236,6 @@ class Converter < ::Prawn::Document
2064
2236
  end
2065
2237
  end
2066
2238
  end
2067
-
2068
- def create_stamps
2069
- create_stamp 'masthead' do
2070
- canvas do
2071
- save_graphics_state do
2072
- stroke_color '000000'
2073
- x_margin = mm2pt 20
2074
- y_margin = mm2pt 15
2075
- stroke_horizontal_line x_margin, bounds.right - x_margin, at: bounds.top - y_margin
2076
- stroke_horizontal_line x_margin, bounds.right - x_margin, at: y_margin
2077
- end
2078
- end
2079
- end
2080
-
2081
- @stamps_initialized = true
2082
- end
2083
2239
  =end
2084
2240
  end
2085
2241
  end