asciidoctor-pdf 1.5.0.beta.2 → 1.5.0.beta.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +41 -0
  3. data/README.adoc +233 -18
  4. data/asciidoctor-pdf.gemspec +5 -2
  5. data/data/fonts/ABOUT-mplus1mn-subset +2 -0
  6. data/data/fonts/ABOUT-mplus1p-subset +1 -0
  7. data/data/fonts/ABOUT-notoserif-subset +1 -0
  8. data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
  9. data/data/fonts/mplus1mn-bold-subset.ttf +0 -0
  10. data/data/fonts/mplus1mn-bold_italic-ascii.ttf +0 -0
  11. data/data/fonts/mplus1mn-bold_italic-subset.ttf +0 -0
  12. data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
  13. data/data/fonts/mplus1mn-italic-subset.ttf +0 -0
  14. data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
  15. data/data/fonts/mplus1mn-regular-subset.ttf +0 -0
  16. data/data/fonts/mplus1p-regular-fallback.ttf +0 -0
  17. data/data/fonts/notoserif-bold-subset.ttf +0 -0
  18. data/data/fonts/notoserif-bold_italic-subset.ttf +0 -0
  19. data/data/fonts/notoserif-italic-subset.ttf +0 -0
  20. data/data/fonts/notoserif-regular-subset.ttf +0 -0
  21. data/data/themes/base-theme.yml +5 -2
  22. data/data/themes/default-theme.yml +5 -1
  23. data/docs/theming-guide.adoc +176 -53
  24. data/lib/asciidoctor-pdf/converter.rb +342 -238
  25. data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +1 -1
  26. data/lib/asciidoctor-pdf/formatted_text/parser.rb +16 -4
  27. data/lib/asciidoctor-pdf/formatted_text/parser.treetop +1 -1
  28. data/lib/asciidoctor-pdf/formatted_text/transform.rb +22 -6
  29. data/lib/asciidoctor-pdf/implicit_header_processor.rb +1 -1
  30. data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +3 -3
  31. data/lib/asciidoctor-pdf/prawn_ext/images.rb +3 -3
  32. data/lib/asciidoctor-pdf/sanitizer.rb +1 -1
  33. data/lib/asciidoctor-pdf/theme_loader.rb +54 -31
  34. data/lib/asciidoctor-pdf/version.rb +1 -1
  35. metadata +47 -5
@@ -39,8 +39,8 @@ class Converter < ::Prawn::Document
39
39
  register_for 'pdf'
40
40
 
41
41
  # NOTE require_library doesn't support require_relative and we don't modify the load path for this gem
42
- CodeRayRequirePath = ::File.join((::File.dirname __FILE__), 'prawn_ext/coderay_encoder')
43
- RougeRequirePath = ::File.join((::File.dirname __FILE__), 'rouge_ext')
42
+ CodeRayRequirePath = ::File.join __dir__, 'prawn_ext/coderay_encoder'
43
+ RougeRequirePath = ::File.join __dir__, 'rouge_ext'
44
44
 
45
45
  AsciidoctorVersion = ::Gem::Version.create ::Asciidoctor::VERSION
46
46
  AdmonitionIcons = {
@@ -101,6 +101,7 @@ class Converter < ::Prawn::Document
101
101
  UriSchemeBoundaryRx = /(?<=:\/\/)/
102
102
  LineScanRx = /\n|.+/
103
103
  BlankLineRx = /\n{2,}/
104
+ CjkLineBreakRx = /(?=[\u3000\u30a0-\u30ff\u3040-\u309f\p{Han}\uff00-\uffef])/
104
105
  WhitespaceChars = ' ' + TAB + LF
105
106
  SourceHighlighters = ['coderay', 'pygments', 'rouge'].to_set
106
107
  PygmentsBgColorRx = /^\.highlight +{ *background: *#([^;]+);/
@@ -121,7 +122,6 @@ class Converter < ::Prawn::Document
121
122
  doc.attributes['data-uri'] = ((doc.instance_variable_get :@attribute_overrides) || {})['data-uri'] = ''
122
123
  end
123
124
  @capabilities = {
124
- expands_tabs: (::Asciidoctor::VERSION.start_with? '1.5.3.') || AsciidoctorVersion >= (::Gem::Version.create '1.5.3'),
125
125
  special_sectnums: AsciidoctorVersion >= (::Gem::Version.create '1.5.7'),
126
126
  syntax_highlighter: AsciidoctorVersion >= (::Gem::Version.create '2.0.0'),
127
127
  }
@@ -196,12 +196,12 @@ class Converter < ::Prawn::Document
196
196
  theme_font :heading, level: 1 do
197
197
  layout_heading doc.doctitle, align: (@theme.heading_h1_align || :center).to_sym, level: 1
198
198
  end
199
- toc_start = @y
200
199
  end
200
+ toc_start = @y
201
201
  end
202
202
 
203
203
  # NOTE font must be set before toc dry run to ensure dry run size is accurate
204
- font @theme.base_font_family, size: @theme.base_font_size, style: (@theme.base_font_style || :normal).to_sym
204
+ font @theme.base_font_family, size: @root_font_size, style: (@theme.base_font_style || :normal).to_sym
205
205
 
206
206
  num_toc_levels = (doc.attr 'toclevels', 2).to_i
207
207
  if (insert_toc = (doc.attr? 'toc') && doc.sections?)
@@ -222,9 +222,7 @@ class Converter < ::Prawn::Document
222
222
  end
223
223
  end
224
224
 
225
- # FIXME only apply to book doctype once title and toc are moved to start page when using article doctype
226
- #start_new_page if @ppbook && verso_page?
227
- start_new_page if @media == 'prepress' && verso_page?
225
+ start_new_page if @ppbook && verso_page?
228
226
 
229
227
  if insert_title_page
230
228
  body_offset = (body_start_page_number = page_number) - 1
@@ -278,9 +276,16 @@ class Converter < ::Prawn::Document
278
276
  end
279
277
 
280
278
  add_outline doc, (doc.attr 'outlinelevels', num_toc_levels).to_i, toc_page_nums, num_front_matter_pages[1]
281
- # TODO allow document (or theme) to override initial view magnification
282
- # NOTE add 1 to page height to force initial scroll to 0; a nil value also seems to work
283
- catalog.data[:OpenAction] = dest_fit_horizontally((page_height + 1), state.pages[0]) if state.pages.size > 0
279
+ if state.pages.size > 0 && (initial_zoom = @theme.page_initial_zoom)
280
+ case initial_zoom.to_sym
281
+ when :Fit
282
+ catalog.data[:OpenAction] = dest_fit state.pages[0]
283
+ when :FitV
284
+ catalog.data[:OpenAction] = dest_fit_vertically 0, state.pages[0]
285
+ when :FitH
286
+ catalog.data[:OpenAction] = dest_fit_horizontally page_height, state.pages[0]
287
+ end
288
+ end
284
289
  catalog.data[:ViewerPreferences] = { DisplayDocTitle: true }
285
290
 
286
291
  layout_cover_page doc, :back
@@ -294,11 +299,10 @@ class Converter < ::Prawn::Document
294
299
 
295
300
  # TODO only allow method to be called once (or we need a reset)
296
301
  def init_pdf doc
297
- @allow_uri_read = doc.attr? 'allow-uri-read'
298
302
  pdf_opts = build_pdf_options doc, (theme = load_theme doc)
299
303
  # QUESTION should page options be preserved? (otherwise, not readily available)
300
304
  #@page_opts = { size: pdf_opts[:page_size], layout: pdf_opts[:page_layout] }
301
- ::Prawn::Document.instance_method(:initialize).bind(self).call pdf_opts
305
+ ((::Prawn::Document.instance_method :initialize).bind self).call pdf_opts
302
306
  renderer.min_version PDFVersions[doc.attr 'pdf-version']
303
307
  @page_margin_by_side = { recto: page_margin, verso: page_margin }
304
308
  if (@media = doc.attr 'media', 'screen') == 'prepress'
@@ -313,12 +317,13 @@ class Converter < ::Prawn::Document
313
317
  # NOTE prepare scratch document to use page margin from recto side (which has same width as verso side)
314
318
  set_page_margin page_margin_recto unless page_margin_recto == page_margin
315
319
  else
316
- @ppbook = false
320
+ @ppbook = nil
317
321
  end
318
322
  # QUESTION should ThemeLoader handle registering fonts instead?
319
- register_fonts theme.font_catalog, (doc.attr 'scripts', 'latin'), (doc.attr 'pdf-fontsdir', ThemeLoader::FontsDir)
323
+ register_fonts theme.font_catalog, (doc.attr 'pdf-fontsdir', ThemeLoader::FontsDir)
320
324
  default_kerning theme.base_font_kerning != 'none'
321
325
  @fallback_fonts = [*theme.font_fallbacks]
326
+ @allow_uri_read = doc.attr? 'allow-uri-read'
322
327
  if (bg_image = resolve_background_image doc, theme, 'page-background-image') && bg_image[0]
323
328
  @page_bg_image = { verso: bg_image, recto: bg_image }
324
329
  else
@@ -331,8 +336,10 @@ class Converter < ::Prawn::Document
331
336
  @page_bg_image[:recto] = bg_image[0] && bg_image
332
337
  end
333
338
  @page_bg_color = resolve_theme_color :page_background_color, 'FFFFFF'
339
+ @root_font_size = theme.base_font_size || 12
334
340
  @font_color = theme.base_font_color || '000000'
335
341
  @base_align = (align = doc.attr 'text-align') && (TextAlignmentNames.include? align) ? align : theme.base_align
342
+ @cjk_line_breaks = doc.attr? 'scripts', 'cjk'
336
343
  @text_transform = nil
337
344
  @list_numerals = []
338
345
  @list_bullets = []
@@ -351,18 +358,17 @@ class Converter < ::Prawn::Document
351
358
  def load_theme doc
352
359
  @theme ||= begin
353
360
  if (theme = doc.options[:pdf_theme])
354
- @themesdir = theme.__dir__ || (doc.attr 'pdf-themesdir') || (doc.attr 'pdf-stylesdir')
361
+ @themesdir = ::File.expand_path theme.__dir__ || (doc.attr 'pdf-themesdir') || (doc.attr 'pdf-stylesdir') || ::Dir.pwd
355
362
  elsif (theme_name = (doc.attr 'pdf-theme') || (doc.attr 'pdf-style'))
356
- theme = ThemeLoader.load_theme theme_name, (theme_dir = (doc.attr 'pdf-themesdir') || (doc.attr 'pdf-stylesdir'))
363
+ theme = ThemeLoader.load_theme theme_name, (user_themesdir = (doc.attr 'pdf-themesdir') || (doc.attr 'pdf-stylesdir'))
357
364
  @themesdir = theme.__dir__
358
365
  else
359
- theme = ThemeLoader.load_theme
360
- @themesdir = theme.__dir__
366
+ @themesdir = (theme = ThemeLoader.load_theme).__dir__
361
367
  end
362
368
  theme
363
369
  rescue
364
- if theme_dir
365
- message = %(could not locate or load the pdf theme `#{theme_name}' in #{::File.absolute_path theme_dir})
370
+ if user_themesdir
371
+ message = %(could not locate or load the pdf theme `#{theme_name}' in #{user_themesdir})
366
372
  else
367
373
  message = %(could not locate or load the built-in pdf theme `#{theme_name}')
368
374
  end
@@ -457,18 +463,10 @@ class Converter < ::Prawn::Document
457
463
  info = {}
458
464
  # FIXME use sanitize: :plain_text once available
459
465
  info[:Title] = sanitize(doc.doctitle use_fallback: true).as_pdf
460
- if doc.attr? 'authors'
461
- info[:Author] = (doc.attr 'authors').as_pdf
462
- end
463
- if doc.attr? 'subject'
464
- info[:Subject] = (doc.attr 'subject').as_pdf
465
- end
466
- if doc.attr? 'keywords'
467
- info[:Keywords] = (doc.attr 'keywords').as_pdf
468
- end
469
- if (doc.attr? 'publisher')
470
- info[:Producer] = (doc.attr 'publisher').as_pdf
471
- end
466
+ info[:Author] = (doc.attr 'authors').as_pdf if doc.attr? 'authors'
467
+ info[:Subject] = (doc.attr 'subject').as_pdf if doc.attr? 'subject'
468
+ info[:Keywords] = (doc.attr 'keywords').as_pdf if doc.attr? 'keywords'
469
+ info[:Producer] = (doc.attr 'publisher').as_pdf if doc.attr? 'publisher'
472
470
  info[:Creator] = %(Asciidoctor PDF #{::Asciidoctor::PDF::VERSION}, based on Prawn #{::Prawn::VERSION}).as_pdf
473
471
  info[:Producer] ||= (info[:Author] || info[:Creator])
474
472
  unless doc.attr? 'reproducible'
@@ -487,14 +485,16 @@ class Converter < ::Prawn::Document
487
485
  if @media == 'prepress' && (next_page_margin = @page_margin_by_side[page_side]) != page_margin
488
486
  set_page_margin next_page_margin
489
487
  end
488
+ if @page_bg_color && @page_bg_color != 'FFFFFF'
489
+ tare = true
490
+ fill_absolute_bounds @page_bg_color
491
+ end
490
492
  # TODO implement as a watermark (on top)
491
493
  if (bg_image = @page_bg_image[page_side])
494
+ tare = true
492
495
  canvas { image bg_image[0], ({ position: :center, vposition: :center }.merge bg_image[1]) }
493
- page.tare_content_stream
494
- elsif @page_bg_color && @page_bg_color != 'FFFFFF'
495
- fill_absolute_bounds @page_bg_color
496
- page.tare_content_stream
497
496
  end
497
+ page.tare_content_stream if tare
498
498
  end
499
499
 
500
500
  def convert_section sect, opts = {}
@@ -511,14 +511,24 @@ class Converter < ::Prawn::Document
511
511
  if sect.part_or_chapter?
512
512
  if sect.chapter?
513
513
  type = :chapter
514
- start_new_chapter sect
514
+ if @theme.heading_chapter_break_before == 'auto'
515
+ start_new_chapter sect if @theme.heading_part_break_after == 'always' && sect == sect.parent.sections[0]
516
+ else
517
+ start_new_chapter sect
518
+ end
515
519
  else
516
520
  type = :part
517
- start_new_part sect
521
+ start_new_part sect unless @theme.heading_part_break_before == 'auto'
518
522
  end
519
- else
520
- # FIXME smarter calculation here!!
521
- start_new_page unless at_page_top? || cursor > (height_of title) + @theme.heading_margin_top + @theme.heading_margin_bottom + ((@theme.base_line_height_length || (font_size * @theme.base_line_height)) * 1.5)
523
+ end
524
+ unless at_page_top?
525
+ # FIXME this height doesn't account for impact of text transform or inline formatting
526
+ heading_height =
527
+ (height_of_typeset_text title, line_height: (@theme[%(heading_h#{hlevel}_line_height)] || @theme.heading_line_height)) +
528
+ (@theme[%(heading_h#{hlevel}_margin_top)] || @theme.heading_margin_top || 0) +
529
+ (@theme[%(heading_h#{hlevel}_margin_bottom)] || @theme.heading_margin_bottom || 0)
530
+ heading_height += (@theme.heading_min_height_after || 0) if sect.blocks?
531
+ start_new_page unless cursor > heading_height
522
532
  end
523
533
  # QUESTION should we store pdf-page-start, pdf-anchor & pdf-destination in internal map?
524
534
  sect.set_attr 'pdf-page-start', (start_pgnum = page_number)
@@ -567,13 +577,11 @@ class Converter < ::Prawn::Document
567
577
  def convert_abstract node
568
578
  add_dest_for_block node if node.id
569
579
  pad_box @theme.abstract_padding do
570
- if node.title?
571
- theme_font :abstract_title do
572
- layout_heading node.title, align: (@theme.abstract_title_align || @base_align).to_sym
573
- end
574
- end
580
+ theme_font :abstract_title do
581
+ layout_heading node.title, align: (@theme.abstract_title_align || @base_align).to_sym, margin_top: (@theme.heading_margin_top || 0), margin_bottom: (@theme.heading_margin_bottom || 0)
582
+ end if node.title?
575
583
  theme_font :abstract do
576
- prose_opts = { line_height: @theme.abstract_line_height, align: (@theme.abstract_align || @base_align).to_sym }
584
+ prose_opts = { line_height: @theme.abstract_line_height, align: (initial_alignment = (@theme.abstract_align || @base_align).to_sym) }
577
585
  if (text_indent = @theme.prose_text_indent)
578
586
  prose_opts[:indent_paragraphs] = text_indent
579
587
  end
@@ -587,14 +595,21 @@ class Converter < ::Prawn::Document
587
595
  # FIXME is playback necessary here?
588
596
  child.document.playback_attributes child.attributes
589
597
  if child.context == :paragraph
598
+ if (alignment = resolve_alignment_from_role child.roles)
599
+ prose_opts[:align] = alignment
600
+ end
590
601
  layout_prose child.content, prose_opts
591
602
  prose_opts.delete :first_line_options
603
+ prose_opts[:align] = initial_alignment
592
604
  else
593
605
  # FIXME this could do strange things if the wrong kind of content shows up
594
606
  convert_content_for_block child
595
607
  end
596
608
  end
597
609
  elsif node.content_model != :compound && (string = node.content)
610
+ if (alignment = resolve_alignment_from_role node.roles)
611
+ prose_opts[:align] = alignment
612
+ end
598
613
  layout_prose string, prose_opts
599
614
  end
600
615
  end
@@ -657,29 +672,30 @@ class Converter < ::Prawn::Document
657
672
  if (label_min_width = @theme.admonition_label_min_width)
658
673
  label_min_width = label_min_width.to_f
659
674
  end
660
- icons = ((doc = node.document).attr? 'icons') ? (doc.attr 'icons') : false
675
+ icons = ((doc = node.document).attr? 'icons') ? (doc.attr 'icons') : nil
661
676
  if (data_uri_enabled = doc.attr? 'data-uri')
662
677
  doc.remove_attr 'data-uri'
663
678
  end
664
679
  if icons == 'font' && !(node.attr? 'icon', nil, false)
665
680
  icon_data = admonition_icon_data(label_text = type.to_sym)
666
- label_width = label_min_width ? label_min_width : (icon_data[:size] * 1.5)
681
+ label_width = label_min_width ? label_min_width : ((icon_size = icon_data[:size] || 24) * 1.5)
667
682
  # NOTE icon_uri will consider icon attribute on node first, then type
668
- elsif icons && ::File.readable?(icon_path = (node.icon_uri type))
683
+ # QUESTION should we use resolve_image_path here?
684
+ elsif icons && (icon_path = node.icon_uri type) &&
685
+ (icon_path = node.normalize_system_path icon_path, nil, nil, target_name: 'admonition icon') &&
686
+ (::File.readable? icon_path)
669
687
  icons = true
670
688
  # TODO introduce @theme.admonition_image_width? or use size key from admonition_icon_<name>?
671
689
  label_width = label_min_width ? label_min_width : 36.0
672
690
  else
673
691
  if icons
674
- icons = false
675
- logger.warn %(admonition icon image not found or not readable: #{icon_path}) unless scratch?
692
+ icons = nil
693
+ logger.warn %(admonition icon not found or not readable: #{icon_path}) unless scratch?
676
694
  end
677
695
  label_text = node.caption
678
696
  theme_font :admonition_label do
679
697
  theme_font %(admonition_label_#{type}) do
680
- if (transform = @text_transform)
681
- label_text = transform_text label_text, transform
682
- end
698
+ label_text = transform_text label_text, @text_transform if @text_transform
683
699
  label_width = rendered_width_of_string label_text
684
700
  label_width = label_min_width if label_min_width && label_min_width > label_width
685
701
  end
@@ -715,7 +731,7 @@ class Converter < ::Prawn::Document
715
731
  bounding_box [0, cursor], width: label_width, height: box_height do
716
732
  if icons == 'font'
717
733
  # FIXME we're assume icon is a square
718
- icon_size = fit_icon_to_bounds icon_data[:size]
734
+ icon_size = fit_icon_to_bounds icon_size
719
735
  # NOTE Prawn's vertical center is not reliable, so calculate it manually
720
736
  if label_valign == :center
721
737
  label_valign = :top
@@ -731,7 +747,7 @@ class Converter < ::Prawn::Document
731
747
  elsif icons
732
748
  if (::Asciidoctor::Image.format icon_path) == 'svg'
733
749
  begin
734
- svg_obj = ::Prawn::SVG::Interface.new ::File.read(icon_path), self,
750
+ svg_obj = ::Prawn::SVG::Interface.new ::File.read(icon_path, mode: 'r:UTF-8'), self,
735
751
  position: label_align,
736
752
  vposition: label_valign,
737
753
  width: label_width,
@@ -746,7 +762,7 @@ class Converter < ::Prawn::Document
746
762
  end
747
763
  svg_obj.draw
748
764
  rescue
749
- logger.warn %(could not embed admonition icon image: #{icon_path}; #{$!.message})
765
+ logger.warn %(could not embed admonition icon: #{icon_path}; #{$!.message})
750
766
  end
751
767
  else
752
768
  begin
@@ -761,7 +777,7 @@ class Converter < ::Prawn::Document
761
777
  embed_image image_obj, image_info, width: icon_width, position: label_align, vposition: label_valign
762
778
  rescue
763
779
  # QUESTION should we show the label in this case?
764
- logger.warn %(could not embed admonition icon image: #{icon_path}; #{$!.message})
780
+ logger.warn %(could not embed admonition icon: #{icon_path}; #{$!.message})
765
781
  end
766
782
  end
767
783
  else
@@ -807,15 +823,60 @@ class Converter < ::Prawn::Document
807
823
  def convert_example node
808
824
  add_dest_for_block node if node.id
809
825
  theme_margin :block, :top
826
+ caption_height = 0
827
+ dry_run do
828
+ move_down 1 # hack to force top margin to be applied
829
+ caption_height = (layout_caption node) - 1
830
+ end if node.title?
810
831
  keep_together do |box_height = nil|
811
832
  push_scratch node.document if scratch?
812
- caption_height = node.title? ? (layout_caption node) : 0
813
833
  if box_height
834
+ # FIXME due to the calculation error logged in #789, we must advance page even when content is split across pages
835
+ advance_page if box_height > cursor && !at_page_top?
836
+ layout_caption node
814
837
  float do
815
- bounding_box [0, cursor], width: bounds.width, height: box_height - caption_height do
816
- theme_fill_and_stroke_bounds :example
838
+ # TODO move the multi-page logic to theme_fill_and_stroke_bounds
839
+ if (b_width = @theme.example_border_width || 0) > 0 && (b_color = @theme.example_border_color)
840
+ if b_color == @page_bg_color # let page background cut into example background
841
+ b_gap_color, b_shift = @page_bg_color, b_width
842
+ elsif (b_gap_color = @theme.example_background_color) && b_gap_color != b_color
843
+ b_shift = 0
844
+ else # let page background cut into border
845
+ b_gap_color, b_shift = @page_bg_color, 0
846
+ end
847
+ else # let page background cut into sidebar background
848
+ b_width = 0.5 if b_width == 0
849
+ b_shift, b_gap_color = b_width * 0.5, @page_bg_color
850
+ end
851
+ b_radius = (@theme.example_border_radius || 0) + b_width
852
+ initial_page, remaining_height = true, box_height - caption_height
853
+ while remaining_height > 0
854
+ advance_page unless initial_page
855
+ fragment_height = [(available_height = cursor), remaining_height].min
856
+ bounding_box [0, available_height], width: bounds.width, height: fragment_height do
857
+ theme_fill_and_stroke_bounds :example
858
+ unless b_width == 0
859
+ indent b_radius, b_radius do
860
+ move_down b_shift
861
+ # dashed line to indicate continuation from previous page; swell line to cover background
862
+ stroke_horizontal_rule b_gap_color, line_width: b_width * 1.2, line_style: :dashed
863
+ move_up b_shift
864
+ end unless initial_page
865
+ if remaining_height > fragment_height
866
+ move_down fragment_height - b_shift
867
+ indent b_radius, b_radius do
868
+ # dashed line to indicate continuation to next page; swell line to cover background
869
+ stroke_horizontal_rule b_gap_color, line_width: b_width * 1.2, line_style: :dashed
870
+ end
871
+ end
872
+ end
873
+ end
874
+ remaining_height -= fragment_height
875
+ initial_page = false
817
876
  end
818
877
  end
878
+ else
879
+ move_down caption_height
819
880
  end
820
881
  pad_box @theme.example_padding do
821
882
  theme_font :example do
@@ -856,13 +917,13 @@ class Converter < ::Prawn::Document
856
917
  if node.context == :quote
857
918
  convert_content_for_block node
858
919
  else # verse
859
- content = preserve_indentation node.content, (node.attr 'tabsize')
920
+ content = guard_indentation node.content
860
921
  layout_prose content, normalize: false, align: :left
861
922
  end
862
923
  end
863
924
  if node.attr? 'attribution', nil, false
864
925
  theme_font :blockquote_cite do
865
- layout_prose %(#{EmDash} #{[(node.attr 'attribution'), (node.attr 'citetitle', nil, false)].compact * ', '}), align: :left, normalize: false
926
+ layout_prose %(#{EmDash} #{[(node.attr 'attribution'), (node.attr 'citetitle', nil, false)].compact.join ', '}), align: :left, normalize: false
866
927
  end
867
928
  end
868
929
  end
@@ -957,12 +1018,10 @@ class Converter < ::Prawn::Document
957
1018
  end
958
1019
  end
959
1020
  pad_box @theme.sidebar_padding do
960
- if node.title?
961
- theme_font :sidebar_title do
962
- # QUESTION should we allow margins of sidebar title to be customized?
963
- layout_heading node.title, align: (@theme.sidebar_title_align || @base_align).to_sym, margin_top: 0
964
- end
965
- end
1021
+ theme_font :sidebar_title do
1022
+ # QUESTION should we allow margins of sidebar title to be customized?
1023
+ layout_heading node.title, align: (@theme.sidebar_title_align || @base_align).to_sym, margin_top: 0, margin_bottom: (@theme.heading_margin_bottom || 0)
1024
+ end if node.title?
966
1025
  theme_font :sidebar do
967
1026
  convert_content_for_block node
968
1027
  end
@@ -989,10 +1048,9 @@ class Converter < ::Prawn::Document
989
1048
  @list_numerals ||= []
990
1049
  @list_numerals << 1
991
1050
  #stroke_horizontal_rule @theme.caption_border_bottom_color
992
- line_metrics = calc_line_metrics @theme.base_line_height
993
- node.items.each_with_index do |item, idx|
994
- # FIXME extract to an ensure_space (or similar) method; simplify
995
- advance_page if cursor < (line_metrics.height + line_metrics.leading + line_metrics.padding_top) + 1
1051
+ line_metrics = theme_font :conum do calc_line_metrics @theme.base_line_height end
1052
+ node.items.each do |item|
1053
+ allocate_space_for_list_item line_metrics
996
1054
  convert_colist_item item
997
1055
  end
998
1056
  @list_numerals.pop
@@ -1033,11 +1091,11 @@ class Converter < ::Prawn::Document
1033
1091
  # and advance to the next page if so (similar to logic for section titles)
1034
1092
  layout_caption node.title if node.title?
1035
1093
 
1094
+ line_metrics = calc_line_metrics @theme.base_line_height
1036
1095
  node.items.each do |terms, desc|
1037
1096
  terms = [*terms]
1038
- # NOTE don't orphan the terms, allow for at least one line of content
1039
- # FIXME extract ensure_space (or similar) method
1040
- advance_page if cursor < (@theme.base_line_height_length || (font_size * @theme.base_line_height)) * (terms.size + 1)
1097
+ # NOTE don't orphan the terms (keep together terms and at least one line of content)
1098
+ allocate_space_for_list_item line_metrics, (terms.size + 1), ((@theme.description_list_term_spacing || 0) + 0.05)
1041
1099
  terms.each do |term|
1042
1100
  # FIXME layout_prose should pass style downward when parsing formatted text
1043
1101
  #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
@@ -1167,8 +1225,7 @@ class Converter < ::Prawn::Document
1167
1225
  end
1168
1226
  indent list_indent do
1169
1227
  node.items.each do |item|
1170
- # FIXME extract to an ensure_space (or similar) method; simplify
1171
- advance_page if cursor < (line_metrics.height + line_metrics.leading + line_metrics.padding_top)
1228
+ allocate_space_for_list_item line_metrics
1172
1229
  convert_outline_list_item item, node, opts
1173
1230
  end
1174
1231
  end
@@ -1267,11 +1324,19 @@ class Converter < ::Prawn::Document
1267
1324
  convert_content_for_block desc
1268
1325
  end
1269
1326
  else
1270
- layout_prose node.text, opts if node.text?
1327
+ if (primary_text = node.text).nil_or_empty?
1328
+ layout_prose DummyText, opts unless node.blocks?
1329
+ else
1330
+ layout_prose primary_text, opts
1331
+ end
1271
1332
  convert_content_for_block node
1272
1333
  end
1273
1334
  end
1274
1335
 
1336
+ def allocate_space_for_list_item line_metrics, number = 1, additional_gap = 0
1337
+ advance_page if !at_page_top? && cursor < (line_metrics.height + line_metrics.leading + line_metrics.padding_top + additional_gap) * number
1338
+ end
1339
+
1275
1340
  def convert_image node, opts = {}
1276
1341
  node.extend ::Asciidoctor::Image unless ::Asciidoctor::Image === node
1277
1342
  target, image_format = node.target_and_format
@@ -1282,15 +1347,17 @@ class Converter < ::Prawn::Document
1282
1347
  elsif ::Base64 === target
1283
1348
  image_path = target
1284
1349
  elsif (image_path = resolve_image_path node, target, (opts.fetch :relative_to_imagesdir, true), image_format)
1285
- if ::File.readable? image_path
1286
- # NOTE import_page automatically advances to next page afterwards
1287
- # QUESTION should we add destination to top of imported page?
1288
- return import_page image_path, replace: page.empty? if image_format == 'pdf'
1289
- elsif image_format == 'pdf'
1290
- logger.warn %(pdf to insert not found or not readable: #{image_path}) unless scratch?
1291
- # QUESTION should we use alt text in this case?
1350
+ if image_format == 'pdf'
1351
+ if ::File.readable? image_path
1352
+ # NOTE import_page automatically advances to next page afterwards
1353
+ # QUESTION should we add destination to top of imported page?
1354
+ import_page image_path, page: [(node.attr 'page').to_i, 1].max, replace: page.empty?
1355
+ else
1356
+ # QUESTION should we use alt text in this case?
1357
+ logger.warn %(pdf to insert not found or not readable: #{image_path})
1358
+ end
1292
1359
  return
1293
- else
1360
+ elsif !(::File.readable? image_path)
1294
1361
  logger.warn %(image to embed not found or not readable: #{image_path}) unless scratch?
1295
1362
  image_path = nil
1296
1363
  end
@@ -1306,9 +1373,9 @@ class Converter < ::Prawn::Document
1306
1373
  # TODO move this calculation into a method, such as layout_caption node, side: :bottom, dry_run: true
1307
1374
  caption_h = 0
1308
1375
  dry_run do
1309
- move_down 0.0001 # hack to force top margin to be applied
1376
+ move_down 1 # hack to force top margin to be applied
1310
1377
  # NOTE we assume caption fits on a single page, which seems reasonable
1311
- caption_h = layout_caption node, side: :bottom
1378
+ caption_h = (layout_caption node, side: :bottom) - 1
1312
1379
  end if node.title?
1313
1380
 
1314
1381
  # TODO support cover (aka canvas) image layout using "canvas" (or "cover") role
@@ -1316,7 +1383,7 @@ class Converter < ::Prawn::Document
1316
1383
  # TODO add `to_pt page_width` method to ViewportWidth type
1317
1384
  width = (width.to_f / 100) * page_width if ViewportWidth === width
1318
1385
 
1319
- alignment = ((node.attr 'align', nil, false) || @theme.image_align).to_sym
1386
+ alignment = ((node.attr 'align', nil, false) || @theme.image_align || :left).to_sym
1320
1387
  align_to_page = node.option? 'align-to-page'
1321
1388
 
1322
1389
  begin
@@ -1326,7 +1393,7 @@ class Converter < ::Prawn::Document
1326
1393
  svg_data = ::Base64.decode64 image_path
1327
1394
  file_request_root = false
1328
1395
  else
1329
- svg_data = ::File.read image_path
1396
+ svg_data = ::File.read image_path, mode: 'r:UTF-8'
1330
1397
  file_request_root = ::File.dirname image_path
1331
1398
  end
1332
1399
  svg_obj = ::Prawn::SVG::Interface.new svg_data, self,
@@ -1353,6 +1420,7 @@ class Converter < ::Prawn::Document
1353
1420
  end
1354
1421
  end
1355
1422
  image_y = y
1423
+ image_cursor = cursor
1356
1424
  add_dest_for_block node if node.id
1357
1425
  # NOTE workaround to fix Prawn not adding fill and stroke commands on page that only has an image;
1358
1426
  # breakage occurs when running content (stamps) are added to page
@@ -1361,6 +1429,7 @@ class Converter < ::Prawn::Document
1361
1429
  # NOTE prawn-svg 0.24.0, 0.25.0, & 0.25.1 didn't restore font after call to draw (see mogest/prawn-svg#80)
1362
1430
  # NOTE cursor advances automatically
1363
1431
  svg_obj.draw
1432
+ draw_image_border image_cursor, rendered_w, rendered_h, alignment unless node.role? 'noborder'
1364
1433
  if (link = node.attr 'link', nil, false)
1365
1434
  add_link_to_image link, { width: rendered_w, height: rendered_h }, position: alignment, y: image_y
1366
1435
  end
@@ -1383,6 +1452,7 @@ class Converter < ::Prawn::Document
1383
1452
  end
1384
1453
  end
1385
1454
  image_y = y
1455
+ image_cursor = cursor
1386
1456
  add_dest_for_block node if node.id
1387
1457
  # NOTE workaround to fix Prawn not adding fill and stroke commands on page that only has an image;
1388
1458
  # breakage occurs when running content (stamps) are added to page
@@ -1390,6 +1460,7 @@ class Converter < ::Prawn::Document
1390
1460
  update_colors if graphic_state.color_space.empty?
1391
1461
  # NOTE specify both width and height to avoid recalculation
1392
1462
  embed_image image_obj, image_info, width: rendered_w, height: rendered_h, position: alignment
1463
+ draw_image_border image_cursor, rendered_w, rendered_h, alignment unless node.role? 'noborder'
1393
1464
  if (link = node.attr 'link', nil, false)
1394
1465
  add_link_to_image link, { width: rendered_w, height: rendered_h }, position: alignment, y: image_y
1395
1466
  end
@@ -1406,6 +1477,22 @@ class Converter < ::Prawn::Document
1406
1477
  unlink_tmp_file image_path if image_path
1407
1478
  end
1408
1479
 
1480
+ def draw_image_border top, w, h, alignment
1481
+ if (b_width = @theme.image_border_width || 0) > 0 && @theme.image_border_color
1482
+ if (@theme.image_border_fit || 'content') == 'auto'
1483
+ bb_width = bounds.width
1484
+ elsif alignment == :center
1485
+ bb_x = (bounds.width - w) * 0.5
1486
+ elsif alignment == :right
1487
+ bb_x = bounds.width - w
1488
+ end
1489
+ bounding_box [(bb_x || 0), top], width: (bb_width || w), height: h, position: alignment do
1490
+ theme_fill_and_stroke_bounds :image, background_color: 'transparent'
1491
+ end
1492
+ true
1493
+ end
1494
+ end
1495
+
1409
1496
  def on_image_error reason, node, target, opts = {}
1410
1497
  logger.warn opts[:message] if opts.key? :message
1411
1498
  alt_text = (link = node.attr 'link', nil, false) ?
@@ -1482,7 +1569,7 @@ class Converter < ::Prawn::Document
1482
1569
  # HACK disable built-in syntax highlighter; must be done before calling node.content!
1483
1570
  if node.style == 'source' && node.attributes['language'] &&
1484
1571
  (highlighter = node.document.attributes['source-highlighter']) && (SourceHighlighters.include? highlighter) &&
1485
- (@capabilities[:syntax_highlighter] ? node.document.syntax_highlighter.highlight? : true)
1572
+ (@capabilities[:syntax_highlighter] ? (syntax_hl = node.document.syntax_highlighter) && syntax_hl.highlight? : true)
1486
1573
  case highlighter
1487
1574
  when 'coderay'
1488
1575
  unless defined? ::Asciidoctor::Prawn::CodeRayEncoder
@@ -1509,7 +1596,7 @@ class Converter < ::Prawn::Document
1509
1596
  else
1510
1597
  prev_subs = nil
1511
1598
  end
1512
- source_string = preserve_indentation node.content, (node.attr 'tabsize')
1599
+ source_string = guard_indentation node.content
1513
1600
  else
1514
1601
  # NOTE the source highlighter logic below handles the callouts and highlight subs
1515
1602
  if highlight_idx
@@ -1518,12 +1605,12 @@ class Converter < ::Prawn::Document
1518
1605
  subs.delete_all :specialcharacters, :callouts
1519
1606
  end
1520
1607
  # the indent guard will be added by the source highlighter logic
1521
- source_string = preserve_indentation node.content, (node.attr 'tabsize'), false
1608
+ source_string = node.content || ''
1522
1609
  end
1523
1610
  else
1524
1611
  highlighter = nil
1525
1612
  prev_subs = nil
1526
- source_string = preserve_indentation node.content, (node.attr 'tabsize')
1613
+ source_string = guard_indentation node.content
1527
1614
  end
1528
1615
 
1529
1616
  bg_color_override = nil
@@ -1551,7 +1638,7 @@ class Converter < ::Prawn::Document
1551
1638
  # TODO enable once we support background color on spans
1552
1639
  #if node.attr? 'highlight', nil, false
1553
1640
  # unless (hl_lines = node.resolve_lines_to_highlight(node.attr 'highlight', nil, false)).empty?
1554
- # pygments_config[:hl_lines] = hl_lines * ' '
1641
+ # pygments_config[:hl_lines] = hl_lines.join ' '
1555
1642
  # end
1556
1643
  #end
1557
1644
  # QUESTION should we treat white background as inherit?
@@ -1576,7 +1663,7 @@ class Converter < ::Prawn::Document
1576
1663
  end
1577
1664
  fragments = text_formatter.format result
1578
1665
  fragments = restore_conums fragments, conum_mapping, num_trailing_spaces, linenums if conum_mapping
1579
- fragments = guard_indentation fragments
1666
+ fragments = guard_indentation_in_fragments fragments
1580
1667
  when 'rouge'
1581
1668
  if (srclang = node.attr 'language', nil, false)
1582
1669
  if srclang.include? '?'
@@ -1685,7 +1772,7 @@ class Converter < ::Prawn::Document
1685
1772
  else
1686
1773
  line
1687
1774
  end
1688
- } * LF
1775
+ }.join LF
1689
1776
  conum_mapping = nil if conum_mapping.empty?
1690
1777
  [string, conum_mapping]
1691
1778
  end
@@ -1720,10 +1807,11 @@ class Converter < ::Prawn::Document
1720
1807
  # append conums to appropriate lines, then flatten to an array of fragments
1721
1808
  lines.flat_map.with_index do |line, cur_line_num|
1722
1809
  last_line = cur_line_num == last_line_num
1723
- line.unshift text: %(#{(cur_line_num + linenums).to_s.rjust pad_size} ), color: linenum_color if linenums
1810
+ # NOTE use ::String.new to ensure string is not frozen
1811
+ line.unshift text: (::String.new %(#{(cur_line_num + linenums).to_s.rjust pad_size} )), color: linenum_color if linenums
1724
1812
  if (conums = conum_mapping.delete cur_line_num)
1725
1813
  line << { text: ' ' * num_trailing_spaces } if last_line && num_trailing_spaces > 0
1726
- conum_text = conums.map {|num| conum_glyph num } * ' '
1814
+ conum_text = conums.map {|num| conum_glyph num }.join ' '
1727
1815
  line << (conum_color ? { text: conum_text, color: conum_color } : { text: conum_text })
1728
1816
  end
1729
1817
  line << { text: LF } unless last_line
@@ -1735,18 +1823,6 @@ class Converter < ::Prawn::Document
1735
1823
  @conum_glyphs[number - 1]
1736
1824
  end
1737
1825
 
1738
- # Adds guards to preserve indentation
1739
- def guard_indentation fragments
1740
- start_of_line = true
1741
- fragments.each do |fragment|
1742
- next if (text = fragment[:text]).empty?
1743
- text[0] = GuardedIndent if start_of_line && (text.start_with? ' ')
1744
- text.gsub! InnerIndent, GuardedInnerIndent if text.include? InnerIndent
1745
- start_of_line = text.end_with? LF
1746
- end
1747
- fragments
1748
- end
1749
-
1750
1826
  def convert_table node
1751
1827
  add_dest_for_block node if node.id
1752
1828
  # TODO we could skip a lot of the logic below when num_rows == 0
@@ -1780,7 +1856,7 @@ class Converter < ::Prawn::Document
1780
1856
  row_data = []
1781
1857
  row.each do |cell|
1782
1858
  row_data << {
1783
- content: (head_transform ? (transform_text cell.text, head_transform) : cell.text),
1859
+ content: (head_transform ? (transform_text cell.text.strip, head_transform) : cell.text.strip),
1784
1860
  inline_format: [normalize: true],
1785
1861
  background_color: head_bg_color,
1786
1862
  text_color: (theme.table_head_font_color || theme.table_font_color || @font_color),
@@ -1853,7 +1929,7 @@ class Converter < ::Prawn::Document
1853
1929
  cell_line_metrics = calc_line_metrics theme.base_line_height
1854
1930
  when :literal
1855
1931
  # FIXME core should not substitute in this case
1856
- cell_data[:content] = preserve_indentation((cell.instance_variable_get :@text), (node.document.attr 'tabsize'))
1932
+ cell_data[:content] = guard_indentation cell.instance_variable_get :@text
1857
1933
  # NOTE the absence of the inline_format option implies it's disabled
1858
1934
  # QUESTION should we use literal_font_*, code_font_*, or introduce another category?
1859
1935
  cell_data[:font] = theme.code_font_family
@@ -1865,7 +1941,7 @@ class Converter < ::Prawn::Document
1865
1941
  end
1866
1942
  cell_line_metrics = calc_line_metrics theme.code_line_height
1867
1943
  when :verse
1868
- cell_data[:content] = preserve_indentation cell.text, (node.document.attr 'tabsize')
1944
+ cell_data[:content] = guard_indentation cell.text
1869
1945
  cell_data[:inline_format] = true
1870
1946
  cell_line_metrics = calc_line_metrics theme.base_line_height
1871
1947
  when :asciidoc
@@ -1889,7 +1965,9 @@ class Converter < ::Prawn::Document
1889
1965
  #cell_data[:final_gap] = cell_line_metrics.final_gap
1890
1966
  end
1891
1967
  unless cell_data.key? :content
1892
- if (cell_text = cell_transform ? (transform_text cell.text, cell_transform) : cell.text).include? LF
1968
+ cell_text = cell.text.strip
1969
+ cell_text = transform_text cell_text if cell_transform
1970
+ if cell_text.include? LF
1893
1971
  # NOTE effectively the same as calling cell.content (should we use that instead?)
1894
1972
  # FIXME hard breaks not quite the same result as separate paragraphs; need custom cell impl here
1895
1973
  cell_data[:content] = (cell_text.split BlankLineRx).map {|l| l.tr_s WhitespaceChars, ' ' }.join DoubleLF
@@ -1899,6 +1977,7 @@ class Converter < ::Prawn::Document
1899
1977
  cell_data[:inline_format] = [normalize: true]
1900
1978
  end
1901
1979
  end
1980
+ cell_data[:background_color] = (node.document.attr 'cellbgcolor')[1..-1] if node.document.attr? 'cellbgcolor'
1902
1981
  row_data << cell_data
1903
1982
  end
1904
1983
  table_data << row_data
@@ -1952,7 +2031,7 @@ class Converter < ::Prawn::Document
1952
2031
 
1953
2032
  if node.option? 'autowidth'
1954
2033
  table_width = (node.attr? 'width', nil, false) ? bounds.width * ((node.attr 'tablepcwidth') / 100.0) :
1955
- ((node.has_role? 'spread') ? bounds.width : nil)
2034
+ (((node.has_role? 'stretch') || (node.has_role? 'spread')) ? bounds.width : nil)
1956
2035
  column_widths = []
1957
2036
  else
1958
2037
  table_width = bounds.width * ((node.attr 'tablepcwidth') / 100.0)
@@ -2156,7 +2235,7 @@ class Converter < ::Prawn::Document
2156
2235
  else
2157
2236
  pagenums = consolidate_ranges term.dests.uniq {|dest| dest[:page] }.map {|dest| dest[:page].to_s }
2158
2237
  end
2159
- text = %(#{text}, #{pagenums * ', '})
2238
+ text = %(#{text}, #{pagenums.join ', '})
2160
2239
  end
2161
2240
  layout_prose text, align: :left, margin: 0
2162
2241
 
@@ -2264,18 +2343,6 @@ class Converter < ::Prawn::Document
2264
2343
  icon_set = node.attr 'set', (node.document.attr 'icon-set', 'fa'), false
2265
2344
  end
2266
2345
  icon_set = 'fa' unless IconSets.include? icon_set
2267
- if node.attr? 'size', nil, false
2268
- case (size = node.attr 'size')
2269
- when 'lg'
2270
- size_attr = %( size="1.333em")
2271
- when 'fw'
2272
- size_attr = %( width="1em" align="center")
2273
- else
2274
- size_attr = %( size="#{size.sub 'x', 'em'}")
2275
- end
2276
- else
2277
- size_attr = ''
2278
- end
2279
2346
  if icon_set == 'fa'
2280
2347
  # legacy name from Font Awesome < 5
2281
2348
  if (remapped_icon_name = resolve_legacy_icon_name icon_name)
@@ -2296,8 +2363,21 @@ class Converter < ::Prawn::Document
2296
2363
  glyph = (icon_font_data icon_set).unicode icon_name rescue nil
2297
2364
  end
2298
2365
  if glyph
2366
+ if node.attr? 'size', nil, false
2367
+ case (size = node.attr 'size')
2368
+ when 'lg'
2369
+ size_attr = %( size="1.333em")
2370
+ when 'fw'
2371
+ size_attr = %( width="1em" align="center")
2372
+ else
2373
+ size_attr = %( size="#{size.sub 'x', 'em'}")
2374
+ end
2375
+ else
2376
+ size_attr = ''
2377
+ end
2378
+ class_attr = node.role? ? %( class="#{node.role}") : ''
2299
2379
  # TODO support rotate and flip attributes
2300
- %(<font name="#{icon_set}"#{size_attr}>#{glyph}</font>)
2380
+ %(<font name="#{icon_set}"#{size_attr}#{class_attr}>#{glyph}</font>)
2301
2381
  else
2302
2382
  logger.warn %(#{icon_name} is not a valid icon name in the #{icon_set} icon set)
2303
2383
  %([#{node.attr 'alt'}])
@@ -2365,7 +2445,7 @@ class Converter < ::Prawn::Document
2365
2445
  menu = node.attr 'menu'
2366
2446
  caret = (load_theme node.document).menu_caret_content || %( \u203a )
2367
2447
  if !(submenus = node.attr 'submenus').empty?
2368
- %(<strong>#{[menu, *submenus, (node.attr 'menuitem')] * caret}</strong>)
2448
+ %(<strong>#{[menu, *submenus, (node.attr 'menuitem')].join caret}</strong>)
2369
2449
  elsif (menuitem = node.attr 'menuitem')
2370
2450
  %(<strong>#{menu}#{caret}#{menuitem}</strong>)
2371
2451
  else
@@ -2389,6 +2469,8 @@ class Converter < ::Prawn::Document
2389
2469
  open, close, is_tag = [?\u201c, ?\u201d, false]
2390
2470
  when :single
2391
2471
  open, close, is_tag = [?\u2018, ?\u2019, false]
2472
+ when :mark
2473
+ open, close, is_tag = ['<mark>', '</mark>', true]
2392
2474
  #when :asciimath, :latexmath
2393
2475
  else
2394
2476
  open, close, is_tag = [nil, nil, false]
@@ -2413,8 +2495,8 @@ class Converter < ::Prawn::Document
2413
2495
 
2414
2496
  # NOTE a new page may have already been started at this point, so decide what to do with it
2415
2497
  if page.empty?
2416
- page.reset_content if (recycle = @ppbook ? verso_page? : true)
2417
- elsif @ppbook && verso_page?
2498
+ page.reset_content if (recycle = @ppbook ? recto_page? : true)
2499
+ elsif @ppbook && page_number > 0 && recto_page?
2418
2500
  start_new_page
2419
2501
  end
2420
2502
 
@@ -2432,23 +2514,22 @@ class Converter < ::Prawn::Document
2432
2514
  @page_bg_color = prev_bg_color if bg_color
2433
2515
 
2434
2516
  # IMPORTANT this is the first page created, so we need to set the base font
2435
- font @theme.base_font_family, size: @theme.base_font_size
2517
+ font @theme.base_font_family, size: @root_font_size
2436
2518
 
2437
2519
  # QUESTION allow alignment per element on title page?
2438
2520
  title_align = (@theme.title_page_align || @base_align).to_sym
2439
2521
 
2440
2522
  # TODO disallow .pdf as image type
2441
- if (logo_image_path = (doc.attr 'title-logo-image', @theme.title_page_logo_image))
2523
+ if (logo_image_path = (doc.attr 'title-logo-image') || (logo_image_from_theme = @theme.title_page_logo_image))
2442
2524
  if (logo_image_path.include? ':') && logo_image_path =~ ImageAttributeValueRx
2443
- logo_image_path = $1
2444
2525
  logo_image_attrs = (AttributeList.new $2).parse ['alt', 'width', 'height']
2445
2526
  relative_to_imagesdir = true
2527
+ logo_image_path = logo_image_from_theme ? (ThemeLoader.resolve_theme_asset (sub_attributes_discretely doc, $1), @themesdir) : $1
2446
2528
  else
2447
2529
  logo_image_attrs = {}
2448
2530
  relative_to_imagesdir = false
2531
+ logo_image_path = ThemeLoader.resolve_theme_asset (sub_attributes_discretely doc, logo_image_path), @themesdir if logo_image_from_theme
2449
2532
  end
2450
- # HACK quick fix to resolve image path relative to theme
2451
- logo_image_path = ThemeLoader.resolve_theme_asset logo_image_path, @themesdir unless doc.attr? 'title-logo-image'
2452
2533
  logo_image_attrs['target'] = logo_image_path
2453
2534
  logo_image_attrs['align'] ||= (@theme.title_page_logo_align || title_align.to_s)
2454
2535
  # QUESTION should we allow theme to turn logo image off?
@@ -2509,7 +2590,7 @@ class Converter < ::Prawn::Document
2509
2590
  # TODO provide an API in core to get authors as an array
2510
2591
  authors = (1..(doc.attr 'authorcount', 1).to_i).map {|idx|
2511
2592
  doc.attr(idx == 1 ? 'author' : %(author_#{idx}))
2512
- } * (@theme.title_page_authors_delimiter || ', ')
2593
+ }.join (@theme.title_page_authors_delimiter || ', ')
2513
2594
  theme_font :title_page_authors do
2514
2595
  layout_prose authors,
2515
2596
  align: title_align,
@@ -2522,7 +2603,10 @@ class Converter < ::Prawn::Document
2522
2603
  revision_info = [(doc.attr? 'revnumber') ? %(#{doc.attr 'version-label'} #{doc.attr 'revnumber'}) : nil, (doc.attr 'revdate')].compact
2523
2604
  unless revision_info.empty?
2524
2605
  move_down(@theme.title_page_revision_margin_top || 0)
2525
- revision_text = revision_info * (@theme.title_page_revision_delimiter || ', ')
2606
+ revision_text = revision_info.join (@theme.title_page_revision_delimiter || ', ')
2607
+ if (revremark = doc.attr 'revremark')
2608
+ revision_text = %(#{revision_text}: #{revremark})
2609
+ end
2526
2610
  indent (@theme.title_page_revision_margin_left || 0), (@theme.title_page_revision_margin_right || 0) do
2527
2611
  theme_font :title_page_revision do
2528
2612
  layout_prose revision_text,
@@ -2555,7 +2639,7 @@ class Converter < ::Prawn::Document
2555
2639
 
2556
2640
  go_to_page page_count if face == :back
2557
2641
  if image_path.downcase.end_with? '.pdf'
2558
- import_page image_path, advance: face != :back
2642
+ import_page image_path, page: [((image_attrs || {})['page']).to_i, 1].max, advance: face != :back
2559
2643
  else
2560
2644
  image_opts = resolve_image_options image_path, image_attrs, background: true, format: image_format
2561
2645
  image_page image_path, (image_opts.merge canvas: true)
@@ -2578,16 +2662,26 @@ class Converter < ::Prawn::Document
2578
2662
  alias start_new_part start_new_chapter
2579
2663
  alias layout_part_title layout_chapter_title
2580
2664
 
2581
- # QUESTION why doesn't layout_heading set the font??
2665
+ # NOTE layout_heading doesn't set the theme font because it's used for various types of headings
2582
2666
  # QUESTION why doesn't layout_heading accept a node?
2583
2667
  def layout_heading string, opts = {}
2584
- top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme[%(heading_h#{opts[:level]}_margin_top)] || @theme.heading_margin_top
2585
- bot_margin = margin || (opts.delete :margin_bottom) || @theme[%(heading_h#{opts[:level]}_margin_bottom)] || @theme.heading_margin_bottom
2668
+ hlevel = opts[:level]
2669
+ unless (top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top))
2670
+ if at_page_top?
2671
+ if hlevel && (top_margin = @theme[%(heading_h#{hlevel}_margin_page_top)] || @theme.heading_margin_page_top || 0) > 0
2672
+ move_down top_margin
2673
+ end
2674
+ top_margin = 0
2675
+ else
2676
+ top_margin = (hlevel ? @theme[%(heading_h#{hlevel}_margin_top)] : nil) || @theme.heading_margin_top
2677
+ end
2678
+ end
2679
+ bot_margin = margin || (opts.delete :margin_bottom) || (hlevel ? @theme[%(heading_h#{hlevel}_margin_bottom)] : nil) || @theme.heading_margin_bottom
2586
2680
  if (transform = resolve_text_transform opts)
2587
2681
  string = transform_text string, transform
2588
2682
  end
2589
2683
  margin_top top_margin
2590
- typeset_text string, calc_line_metrics((opts.delete :line_height) || @theme[%(heading_h#{opts[:level]}_line_height)] || @theme.heading_line_height || @theme.base_line_height), {
2684
+ typeset_text string, calc_line_metrics((opts.delete :line_height) || (hlevel ? @theme[%(heading_h#{hlevel}_line_height)] : nil) || @theme.heading_line_height || @theme.base_line_height), {
2591
2685
  color: @font_color,
2592
2686
  inline_format: true,
2593
2687
  align: @base_align.to_sym
@@ -2697,7 +2791,7 @@ class Converter < ::Prawn::Document
2697
2791
  theme_font :heading, level: 2 do
2698
2792
  theme_font :toc_title do
2699
2793
  toc_title_align = (@theme.toc_title_align || @theme.heading_h2_align || @theme.heading_align || @base_align).to_sym
2700
- layout_heading toc_title, align: toc_title_align
2794
+ layout_heading toc_title, align: toc_title_align, level: 2
2701
2795
  end
2702
2796
  end
2703
2797
  end
@@ -2737,7 +2831,7 @@ class Converter < ::Prawn::Document
2737
2831
  end
2738
2832
  sections.each do |sect|
2739
2833
  theme_font :toc, level: (sect.level + 1) do
2740
- sect_title = (transform = @text_transform) ? (transform_text sect.numbered_title, transform) : sect.numbered_title
2834
+ sect_title = @text_transform ? (transform_text sect.numbered_title, @text_transform) : sect.numbered_title
2741
2835
  # NOTE only write section title (excluding dots and page number) if this is a dry run
2742
2836
  if scratch?
2743
2837
  # FIXME use layout_prose
@@ -2910,7 +3004,6 @@ class Converter < ::Prawn::Document
2910
3004
  doc.set_attr 'page-count', num_pages
2911
3005
 
2912
3006
  pagenums_enabled = doc.attr? 'pagenums'
2913
- attribute_missing_doc = doc.attr 'attribute-missing'
2914
3007
  case @media == 'prepress' ? 'physical' : (doc.attr 'pdf-folio-placement')
2915
3008
  when 'physical'
2916
3009
  folio_basis, invert_folio = :physical, false
@@ -2990,16 +3083,8 @@ class Converter < ::Prawn::Document
2990
3083
  if content == '{page-number}'
2991
3084
  content = pagenums_enabled ? pgnum_label.to_s : nil
2992
3085
  else
2993
- # FIXME get apply_subs to handle drop-line w/o a warning
2994
- doc.set_attr 'attribute-missing', 'skip' unless attribute_missing_doc == 'skip'
2995
- if (content = doc.apply_subs content).include? '{'
2996
- # NOTE must use &#123; in place of {, not \{, to escape attribute reference
2997
- content = content.split(LF).delete_if {|line| SimpleAttributeRefRx.match? line } * LF
2998
- end
2999
- doc.set_attr 'attribute-missing', attribute_missing_doc unless attribute_missing_doc == 'skip'
3000
- if (transform = @text_transform) && transform != 'none'
3001
- content = transform_text content, @text_transform
3002
- end
3086
+ content = apply_subs_discretely doc, content, drop_lines_with_unresolved_attributes: true
3087
+ content = transform_text content, @text_transform if @text_transform
3003
3088
  end
3004
3089
  formatted_text_box parse_text(content, color: @font_color, inline_format: [normalize: true]),
3005
3090
  at: [left, bounds.top - trim_styles[:padding][0] - trim_styles[:content_offset] + (trim_styles[:valign] == :center ? font.descender * 0.5 : 0)],
@@ -3075,7 +3160,7 @@ class Converter < ::Prawn::Document
3075
3160
  trim_styles[:img_valign] = trim_styles[:img_valign].to_sym
3076
3161
  end
3077
3162
 
3078
- colspec_dict = PageSides.inject({}) do |acc, side|
3163
+ colspec_dict = PageSides.reduce({}) do |acc, side|
3079
3164
  side_trim_content_width = trim_content_width[side]
3080
3165
  if (custom_colspecs = @theme[%(#{periphery}_#{side}_columns)] || @theme[%(#{periphery}_columns)])
3081
3166
  case (colspecs = (custom_colspecs.to_s.tr ',', ' ').split[0..2]).size
@@ -3112,7 +3197,7 @@ class Converter < ::Prawn::Document
3112
3197
  acc
3113
3198
  end
3114
3199
 
3115
- content_dict = PageSides.inject({}) do |acc, side|
3200
+ content_dict = PageSides.reduce({}) do |acc, side|
3116
3201
  side_content = {}
3117
3202
  ColumnPositions.each do |position|
3118
3203
  unless (val = @theme[%(#{periphery}_#{side}_#{position}_content)]).nil_or_empty?
@@ -3231,9 +3316,24 @@ class Converter < ::Prawn::Document
3231
3316
  nil
3232
3317
  end
3233
3318
 
3234
- def register_fonts font_catalog, scripts = 'latin', fonts_dir
3235
- (font_catalog || {}).each do |key, styles|
3236
- register_font key => styles.map {|style, path| [style.to_sym, (font_path path, fonts_dir)]}.to_h
3319
+ def register_fonts font_catalog, fonts_dir
3320
+ return unless font_catalog
3321
+ dirs = (fonts_dir.split ::File::PATH_SEPARATOR, -1).map do |dir|
3322
+ dir.empty? || dir == 'GEM_FONTS_DIR' ? ThemeLoader::FontsDir : dir
3323
+ end
3324
+ font_catalog.each do |key, styles|
3325
+ styles = styles.reduce({}) do |accum, (style, path)|
3326
+ found = dirs.find do |dir|
3327
+ resolved_font_path = font_path path, dir
3328
+ if ::File.readable? resolved_font_path
3329
+ accum[style.to_sym] = resolved_font_path
3330
+ true
3331
+ end
3332
+ end
3333
+ raise ::Errno::ENOENT, %(#{path} not found in #{fonts_dir}) unless found
3334
+ accum
3335
+ end
3336
+ register_font key => styles
3237
3337
  end
3238
3338
  end
3239
3339
 
@@ -3273,25 +3373,28 @@ class Converter < ::Prawn::Document
3273
3373
  radius: @theme[%(#{category}_border_radius)]
3274
3374
  end
3275
3375
 
3276
- # Insert a top margin space unless cursor is at the top of the page.
3277
- # Start a new page if n value is greater than remaining space on page.
3278
- def margin_top n
3279
- margin n, :top
3376
+ # Insert a top margin equal to amount if cursor is not at the top of the
3377
+ # page. Start a new page instead if amount is greater than the remaining
3378
+ # space on the page.
3379
+ def margin_top amount
3380
+ margin amount, :top
3280
3381
  end
3281
3382
 
3282
- # Insert a bottom margin space unless cursor is at the top of the page (not likely).
3283
- # Start a new page if n value is greater than remaining space on page.
3284
- def margin_bottom n
3285
- margin n, :bottom
3383
+ # Insert a bottom margin equal to amount unless cursor is at the top of the
3384
+ # page (not likely). Start a new page instead if amount is greater than the
3385
+ # remaining space on the page.
3386
+ def margin_bottom amount
3387
+ margin amount, :bottom
3286
3388
  end
3287
3389
 
3288
- # Insert a margin space at the specified side unless cursor is at the top of the page.
3289
- # Start a new page if n value is greater than remaining space on page.
3290
- def margin n, side
3291
- unless (n || 0) == 0 || at_page_top?
3390
+ # Insert a margin at the specified side if the cursor is not at the top of
3391
+ # the page. Start a new page if amount is greater than the remaining space on
3392
+ # the page.
3393
+ def margin amount, side
3394
+ unless (amount || 0) == 0 || at_page_top?
3292
3395
  # NOTE use low-level cursor calculation to workaround cursor bug in column_box context
3293
- if y - reference_bounds.absolute_bottom > n
3294
- move_down n
3396
+ if y - reference_bounds.absolute_bottom > amount
3397
+ move_down amount
3295
3398
  else
3296
3399
  # set cursor at top of next page
3297
3400
  reference_bounds.move_past_bottom
@@ -3312,8 +3415,8 @@ class Converter < ::Prawn::Document
3312
3415
  # TODO inheriting from generic category should be an option
3313
3416
  if opts.key? :level
3314
3417
  level = opts[:level]
3315
- family = @theme[%(#{category}_h#{level}_font_family)] || @theme[%(#{category}_font_family)] || @theme.base_font_family
3316
- size = @theme[%(#{category}_h#{level}_font_size)] || @theme[%(#{category}_font_size)] || @theme.base_font_size
3418
+ family = @theme[%(#{category}_h#{level}_font_family)] || @theme[%(#{category}_font_family)] || @theme.base_font_family || font_family
3419
+ size = @theme[%(#{category}_h#{level}_font_size)] || @theme[%(#{category}_font_size)] || @root_font_size
3317
3420
  style = @theme[%(#{category}_h#{level}_font_style)] || @theme[%(#{category}_font_style)]
3318
3421
  color = @theme[%(#{category}_h#{level}_font_color)] || @theme[%(#{category}_font_color)]
3319
3422
  # NOTE global text_transform is not currently supported
@@ -3437,6 +3540,7 @@ class Converter < ::Prawn::Document
3437
3540
  def typeset_text string, line_metrics, opts = {}
3438
3541
  move_down line_metrics.padding_top
3439
3542
  opts = { leading: line_metrics.leading, final_gap: line_metrics.final_gap }.merge opts
3543
+ string = string.gsub CjkLineBreakRx, ZeroWidthSpace if @cjk_line_breaks
3440
3544
  if (first_line_opts = opts.delete :first_line_options)
3441
3545
  # TODO good candidate for Prawn enhancement!
3442
3546
  text_with_formatted_first_line string, first_line_opts, opts
@@ -3458,62 +3562,27 @@ class Converter < ::Prawn::Document
3458
3562
  (height_of string, leading: line_metrics.leading, final_gap: line_metrics.final_gap) + line_metrics.padding_top + line_metrics.padding_bottom
3459
3563
  end
3460
3564
 
3461
- def preserve_indentation string, tab_size = nil, guard_indent = true
3462
- return '' unless string
3463
- # expand tabs if they aren't already expanded, even if explicitly disabled
3464
- # NOTE Asciidoctor >= 1.5.3 already replaces tabs if tabsize attribute is positive
3465
- if ((tab_size = tab_size.to_i) < 1 || !@capabilities[:expands_tabs]) && (string.include? TAB)
3466
- # Asciidoctor <= 1.5.2 already does tab replacement in some cases, so be consistent about tab size
3467
- full_tab_space = ' ' * (tab_size = 4)
3468
- result = []
3469
- string.each_line do |line|
3470
- if line.start_with? TAB
3471
- if guard_indent
3472
- # NOTE '+' operator is faster than interpolation
3473
- line.sub!(TabIndentRx) { GuardedIndent + (full_tab_space * $&.length).chop! }
3474
- else
3475
- line.sub!(TabIndentRx) { full_tab_space * $&.length }
3476
- end
3477
- leading_space = false
3478
- # QUESTION should we check for LF first?
3479
- elsif line == LF
3480
- result << line
3481
- next
3482
- else
3483
- leading_space = guard_indent && (line.start_with? ' ')
3484
- end
3485
-
3486
- if line.include? TAB
3487
- # keep track of how many spaces were added to adjust offset in match data
3488
- spaces_added = 0
3489
- line.gsub!(TabRx) {
3490
- # calculate how many spaces this tab represents, then replace tab with spaces
3491
- if (offset = ($~.begin 0) + spaces_added) % tab_size == 0
3492
- spaces_added += (tab_size - 1)
3493
- full_tab_space
3494
- else
3495
- unless (spaces = tab_size - offset % tab_size) == 1
3496
- spaces_added += (spaces - 1)
3497
- end
3498
- ' ' * spaces
3499
- end
3500
- }
3501
- end
3502
-
3503
- # NOTE we save time by adding indent guard per line while performing tab expansion
3504
- line[0] = GuardedIndent if leading_space
3505
- result << line
3506
- end
3507
- result.join
3508
- else
3509
- if guard_indent
3510
- string[0] = GuardedIndent if string.start_with? ' '
3511
- string.gsub! InnerIndent, GuardedInnerIndent if string.include? InnerIndent
3512
- end
3565
+ def guard_indentation string
3566
+ if string
3567
+ string[0] = GuardedIndent if string.start_with? ' '
3568
+ string.gsub! InnerIndent, GuardedInnerIndent if string.include? InnerIndent
3513
3569
  string
3570
+ else
3571
+ ''
3514
3572
  end
3515
3573
  end
3516
3574
 
3575
+ def guard_indentation_in_fragments fragments
3576
+ start_of_line = true
3577
+ fragments.each do |fragment|
3578
+ next if (text = fragment[:text]).empty?
3579
+ text[0] = GuardedIndent if start_of_line && (text.start_with? ' ')
3580
+ text.gsub! InnerIndent, GuardedInnerIndent if text.include? InnerIndent
3581
+ start_of_line = text.end_with? LF
3582
+ end
3583
+ fragments
3584
+ end
3585
+
3517
3586
  # Derive a PDF-safe, ASCII-only anchor name from the given value.
3518
3587
  # Encodes value into hex if it contains characters outside the ASCII range.
3519
3588
  # If value is nil, derive an anchor name from the default_value, if given.
@@ -3619,7 +3688,7 @@ class Converter < ::Prawn::Document
3619
3688
  end
3620
3689
  # handle case when image is a local file
3621
3690
  else
3622
- ::File.expand_path(node.normalize_system_path image_path, imagesdir, nil, target_name: 'image')
3691
+ node.normalize_system_path image_path, imagesdir, nil, target_name: 'image'
3623
3692
  end
3624
3693
  end
3625
3694
 
@@ -3634,10 +3703,17 @@ class Converter < ::Prawn::Document
3634
3703
  return []
3635
3704
  elsif (image_path.include? ':') && image_path =~ ImageAttributeValueRx
3636
3705
  image_attrs = (AttributeList.new $2).parse ['alt', 'width']
3706
+ if from_theme
3707
+ # TODO support remote image when loaded from theme
3708
+ image_path = ThemeLoader.resolve_theme_asset (sub_attributes_discretely doc, $1), @themesdir
3709
+ else
3710
+ image_path = resolve_image_path doc, $1, true, (image_format = image_attrs['format'])
3711
+ end
3712
+ elsif from_theme
3637
3713
  # TODO support remote image when loaded from theme
3638
- image_path = from_theme ? (ThemeLoader.resolve_theme_asset $1, @themesdir) : (resolve_image_path doc, $1, true, (image_format = image_attrs['format']))
3714
+ image_path = ThemeLoader.resolve_theme_asset (sub_attributes_discretely doc, image_path), @themesdir
3639
3715
  else
3640
- image_path = from_theme ? (ThemeLoader.resolve_theme_asset image_path, @themesdir) : (resolve_image_path doc, image_path, false)
3716
+ image_path = resolve_image_path doc, image_path, false
3641
3717
  end
3642
3718
 
3643
3719
  return unless image_path
@@ -3835,6 +3911,34 @@ class Converter < ::Prawn::Document
3835
3911
  logger.warn %(could not delete temporary image: #{path}; #{$!.message})
3836
3912
  end
3837
3913
 
3914
+ def apply_subs_discretely doc, value, opts = {}
3915
+ imagesdir = doc.attr 'imagesdir'
3916
+ doc.set_attr 'imagesdir', @themesdir
3917
+ # FIXME get sub_attributes to handle drop-line w/o a warning
3918
+ doc.set_attr 'attribute-missing', 'skip' unless (attribute_missing = doc.attr 'attribute-missing') == 'skip'
3919
+ value = value.gsub '\{', '\\\\\\{' if (escaped_attr_ref = value.include? '\{')
3920
+ value = doc.apply_subs value
3921
+ if opts[:drop_lines_with_unresolved_attributes] && (value.include? '{')
3922
+ value = (value.split LF).delete_if {|line| SimpleAttributeRefRx.match? line }.join LF
3923
+ end
3924
+ value = value.gsub '\{', '{' if escaped_attr_ref
3925
+ doc.set_attr 'attribute-missing', attribute_missing unless attribute_missing == 'skip'
3926
+ if imagesdir
3927
+ doc.set_attr 'imagesdir', imagesdir
3928
+ else
3929
+ # NOTE remove_attr not defined until Asciidoctor 1.5.6
3930
+ doc.attributes.delete 'imagesdir'
3931
+ end
3932
+ value
3933
+ end
3934
+
3935
+ def sub_attributes_discretely doc, value
3936
+ doc.set_attr 'attribute-missing', 'skip' unless (attribute_missing = doc.attr 'attribute-missing') == 'skip'
3937
+ value = doc.apply_subs value
3938
+ doc.set_attr 'attribute-missing', attribute_missing unless attribute_missing == 'skip'
3939
+ value
3940
+ end
3941
+
3838
3942
  # NOTE assume URL is escaped (i.e., contains character references such as &amp;)
3839
3943
  def breakable_uri uri
3840
3944
  scheme, address = uri.split UriSchemeBoundaryRx, 2
@@ -3850,7 +3954,7 @@ class Converter < ::Prawn::Document
3850
3954
  def consolidate_ranges nums
3851
3955
  if nums.size > 1
3852
3956
  prev = nil
3853
- nums.inject([]) {|accum, num|
3957
+ nums.reduce([]) {|accum, num|
3854
3958
  if prev && (prev.to_i + 1) == num.to_i
3855
3959
  accum[-1][1] = num
3856
3960
  else