asciidoctor-pdf 2.0.0.alpha.2 → 2.0.0.beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +185 -83
  3. data/README.adoc +31 -126
  4. data/data/fonts/ABOUT-mplus1p-subset +1 -0
  5. data/data/fonts/ABOUT-notosans-subset +1 -0
  6. data/data/fonts/ABOUT-notoserif-subset +1 -0
  7. data/data/fonts/mplus1mn-bold-subset.ttf +0 -0
  8. data/data/fonts/mplus1mn-bold_italic-subset.ttf +0 -0
  9. data/data/fonts/mplus1mn-italic-subset.ttf +0 -0
  10. data/data/fonts/mplus1mn-regular-subset.ttf +0 -0
  11. data/data/fonts/mplus1p-regular-fallback.ttf +0 -0
  12. data/data/fonts/notosans-bold-subset.ttf +0 -0
  13. data/data/fonts/notosans-bold_italic-subset.ttf +0 -0
  14. data/data/fonts/notosans-italic-subset.ttf +0 -0
  15. data/data/fonts/notosans-regular-subset.ttf +0 -0
  16. data/data/fonts/notoserif-bold-subset.ttf +0 -0
  17. data/data/fonts/notoserif-bold_italic-subset.ttf +0 -0
  18. data/data/fonts/notoserif-italic-subset.ttf +0 -0
  19. data/data/fonts/notoserif-regular-subset.ttf +0 -0
  20. data/data/themes/base-theme.yml +10 -20
  21. data/data/themes/default-for-print-theme.yml +4 -4
  22. data/data/themes/default-for-print-with-fallback-font-theme.yml +2 -3
  23. data/data/themes/default-for-print-with-font-fallbacks-theme.yml +3 -0
  24. data/data/themes/{sans-with-fallback-font-theme.yml → default-sans-theme.yml} +1 -1
  25. data/data/themes/default-sans-with-font-fallbacks-theme.yml +3 -0
  26. data/data/themes/default-theme.yml +5 -4
  27. data/data/themes/default-with-fallback-font-theme.yml +2 -9
  28. data/data/themes/default-with-font-fallbacks-theme.yml +9 -0
  29. data/docs/theming-guide.adoc +5 -6050
  30. data/lib/asciidoctor/pdf/converter.rb +505 -294
  31. data/lib/asciidoctor/pdf/ext/pdf-core/page.rb +8 -0
  32. data/lib/asciidoctor/pdf/ext/prawn/document/column_box.rb +16 -0
  33. data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +110 -58
  34. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/box.rb +14 -0
  35. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/indented_paragraph_wrap.rb +39 -0
  36. data/lib/asciidoctor/pdf/ext/prawn-table/cell/asciidoc.rb +3 -10
  37. data/lib/asciidoctor/pdf/ext/prawn.rb +2 -0
  38. data/lib/asciidoctor/pdf/formatted_text/source_wrap.rb +7 -2
  39. data/lib/asciidoctor/pdf/nopngmagick.rb +3 -0
  40. data/lib/asciidoctor/pdf/optimizer.rb +12 -5
  41. data/lib/asciidoctor/pdf/text_transformer.rb +14 -0
  42. data/lib/asciidoctor/pdf/theme_loader.rb +9 -3
  43. data/lib/asciidoctor/pdf/version.rb +1 -1
  44. metadata +9 -3
@@ -14,6 +14,14 @@ class PDF::Core::Page
14
14
  content.stream.filtered_stream == (@tare_content_stream ||= InitialPageContent) && document.page_number > 0
15
15
  end
16
16
 
17
+ # Flags this page as imported.
18
+ #
19
+ def imported
20
+ @imported_page = true
21
+ end
22
+
23
+ alias imported_page imported
24
+
17
25
  # Reset the content of the page.
18
26
  # Note that this method may leave behind an orphaned background image.
19
27
  def reset_content
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ Prawn::Document::ColumnBox.prepend (Module.new do
4
+ def absolute_bottom
5
+ stretchy? ? @parent.absolute_bottom : super
6
+ end
7
+
8
+ def move_past_bottom *_args
9
+ initial_page = @document.page
10
+ super
11
+ if (page = @document.page) != initial_page && page.margins != initial_page.margins
12
+ @document.bounds = self.class.new @document, @parent, (margin_box = @document.margin_box).absolute_top_left,
13
+ columns: @columns, reflow_margins: true, spacer: @spacer, width: margin_box.width
14
+ end
15
+ end
16
+ end)
@@ -103,7 +103,7 @@ module Asciidoctor
103
103
  NewPageRequiredError = ::Class.new ::StopIteration
104
104
 
105
105
  InhibitNewPageProc = proc do |pdf|
106
- pdf.delete_page
106
+ pdf.delete_current_page
107
107
  raise NewPageRequiredError
108
108
  end
109
109
 
@@ -111,7 +111,7 @@ module Asciidoctor
111
111
 
112
112
  DetectEmptyFirstPageProc = proc do |delegate, pdf|
113
113
  if pdf.state.pages[pdf.page_number - 2].empty?
114
- pdf.delete_page
114
+ pdf.delete_current_page
115
115
  raise NewPageRequiredError
116
116
  end
117
117
  delegate.call pdf if (pdf.state.on_page_create_callback = delegate)
@@ -252,6 +252,15 @@ module Asciidoctor
252
252
  @y == @margin_box.absolute_top
253
253
  end
254
254
 
255
+ # Prevents at_page_top? from returning true while yielding to the specified block.
256
+ #
257
+ def conceal_page_top
258
+ margin_box.instance_variable_set :@y, (old_top = margin_box.absolute_top) + 0.0001
259
+ yield
260
+ ensure
261
+ margin_box.instance_variable_set :@y, old_top
262
+ end
263
+
255
264
  # Returns whether the current page is the last page in the document.
256
265
  #
257
266
  def last_page?
@@ -269,6 +278,20 @@ module Asciidoctor
269
278
  dest_xyz 0, page_height, nil, (page_num ? state.pages[page_num - 1] : page)
270
279
  end
271
280
 
281
+ # Gets the destination registered for the specified name. The return value
282
+ # matches that which was passed to the add_dest method.
283
+ #
284
+ def get_dest name, node = dests.data
285
+ node.children.each do |child|
286
+ if ::PDF::Core::NameTree::Value === child
287
+ return child.value.data if child.name == name
288
+ elsif (found = get_dest name, child)
289
+ return found
290
+ end
291
+ end
292
+ nil
293
+ end
294
+
272
295
  # Fonts
273
296
 
274
297
  # Registers a new custom font described in the data parameter
@@ -396,20 +419,11 @@ module Asciidoctor
396
419
  def parse_text string, options = {}
397
420
  return [] if string.nil?
398
421
 
399
- options = options.dup
400
- if (format_option = options.delete :inline_format)
422
+ if (format_option = options[:inline_format])
401
423
  format_option = [] unless ::Array === format_option
402
- fragments = text_formatter.format string, *format_option
403
- else
404
- fragments = [text: string]
405
- end
406
-
407
- if (color = options.delete :color)
408
- fragments.map do |fragment|
409
- fragment[:color] ? fragment : (fragment.merge color: color)
410
- end
424
+ text_formatter.format string, *format_option
411
425
  else
412
- fragments
426
+ [text: string]
413
427
  end
414
428
  end
415
429
 
@@ -421,7 +435,8 @@ module Asciidoctor
421
435
  @no_text_printed = box.nothing_printed?
422
436
  @all_text_printed = box.everything_printed?
423
437
 
424
- if ((defined? @final_gap) && @final_gap) || (options[:first_line] && !(@no_text_printed || @all_text_printed))
438
+ if ((defined? @final_gap) && @final_gap) ||
439
+ (options[:first_line] && (options[:final_gap] || !(@no_text_printed || @all_text_printed)))
425
440
  self.y -= box.height + box.line_gap + box.leading
426
441
  else
427
442
  self.y -= box.height
@@ -439,31 +454,50 @@ module Asciidoctor
439
454
  # renderered. It's necessary to use low-level APIs in this method so we only style the first line and not the
440
455
  # remaining lines (which is the default behavior in Prawn).
441
456
  def text_with_formatted_first_line string, first_line_options, options
442
- color = options.delete :color
457
+ if (first_line_font_color = first_line_options.delete :color)
458
+ other_lines_font_color, options[:color] = options[:color], first_line_font_color
459
+ end
443
460
  fragments = parse_text string, options
444
461
  # NOTE: the low-level APIs we're using don't recognize the :styles option, so we must resolve
445
- # NOTE: disabled until we have a need for it
462
+ # NOTE: disabled until we have a need for it; currently handled in convert_abstract
446
463
  #if (styles = options.delete :styles)
447
464
  # options[:style] = resolve_font_style styles
448
465
  #end
449
466
  if (first_line_styles = first_line_options.delete :styles)
450
467
  first_line_options[:style] = resolve_font_style first_line_styles
451
468
  end
452
- first_line_color = (first_line_options.delete :color) || color
469
+ first_line_text_transform = first_line_options.delete :text_transform
453
470
  options = options.merge document: self
471
+ text_indent = options.delete :indent_paragraphs
454
472
  # QUESTION: should we merge more carefully here? (hand-select keys?)
455
473
  first_line_options = (options.merge first_line_options).merge single_line: true, first_line: true
456
474
  box = ::Prawn::Text::Formatted::Box.new fragments, first_line_options
457
- # NOTE: get remaining_fragments before we add color to fragments on first line
458
- if (text_indent = options.delete :indent_paragraphs)
475
+ if text_indent
459
476
  remaining_fragments = indent text_indent do
460
477
  box.render dry_run: true
461
478
  end
462
479
  else
463
480
  remaining_fragments = box.render dry_run: true
464
481
  end
465
- # NOTE: color must be applied per-fragment
466
- fragments.each {|fragment| fragment[:color] ||= first_line_color }
482
+ if first_line_text_transform
483
+ # NOTE: applying text transform here could alter the wrapping, so we need to isolate first line and shrink to fit
484
+ first_line_text = (box.instance_variable_get :@printed_lines)[0]
485
+ unless first_line_text == fragments[0][:text]
486
+ original_fragments, fragments = fragments, []
487
+ original_fragments.reduce '' do |traced, fragment|
488
+ fragments << fragment
489
+ # NOTE: we could just do a length comparison here
490
+ if (traced += fragment[:text]).start_with? first_line_text
491
+ fragment[:text] = fragment[:text][0...-(traced.length - first_line_text.length)]
492
+ break
493
+ end
494
+ traced
495
+ end
496
+ end
497
+ fragments.each {|fragment| fragment[:text] = transform_text fragment[:text], first_line_text_transform }
498
+ first_line_options[:overflow] = :shrink_to_fit
499
+ first_line_options[:final_gap] = first_line_options[:force_justify] = true unless remaining_fragments.empty?
500
+ end
467
501
  if text_indent
468
502
  indent text_indent do
469
503
  fill_formatted_text_box fragments, first_line_options
@@ -472,8 +506,7 @@ module Asciidoctor
472
506
  fill_formatted_text_box fragments, first_line_options
473
507
  end
474
508
  unless remaining_fragments.empty?
475
- # NOTE: color must be applied per-fragment
476
- remaining_fragments.each {|fragment| fragment[:color] ||= color }
509
+ options[:color] = other_lines_font_color if first_line_font_color
477
510
  remaining_fragments = fill_formatted_text_box remaining_fragments, options
478
511
  draw_remaining_formatted_text_on_new_pages remaining_fragments, options
479
512
  end
@@ -495,6 +528,8 @@ module Asciidoctor
495
528
  lowercase_pcdata text
496
529
  when :capitalize, 'capitalize'
497
530
  capitalize_words_pcdata text
531
+ when :smallcaps, 'smallcaps'
532
+ smallcaps_pcdata text
498
533
  else
499
534
  text
500
535
  end
@@ -538,17 +573,20 @@ module Asciidoctor
538
573
  # Example:
539
574
  #
540
575
  # pad_box 20 do
541
- # text 'A paragraph inside a blox with even padding on all sides.'
576
+ # text 'A paragraph inside a blox with even padding from all edges.'
577
+ # end
578
+ #
579
+ # pad_box [10, 5] do
580
+ # text 'A paragraph inside a box with different padding from ends and sides.'
542
581
  # end
543
582
  #
544
- # pad_box [10, 10, 10, 20] do
545
- # text 'An indented paragraph inside a box with equal padding on all sides.'
583
+ # pad_box [5, 10, 15, 20] do
584
+ # text 'A paragraph inside a box with different padding from each edge.'
546
585
  # end
547
586
  #
548
587
  def pad_box padding, node = nil
549
588
  if padding
550
- # TODO: implement shorthand combinations like in CSS
551
- p_top, p_right, p_bottom, p_left = ::Array === padding ? padding : (::Array.new 4, padding)
589
+ p_top, p_right, p_bottom, p_left = expand_padding_value padding
552
590
  # logic is intentionally inlined
553
591
  begin
554
592
  if node && ((last_block = node).content_model != :compound || (last_block = node.blocks[-1])&.context == :paragraph)
@@ -560,8 +598,8 @@ module Asciidoctor
560
598
  bounds.add_left_padding p_left
561
599
  bounds.add_right_padding p_right
562
600
  yield
563
- cursor > p_bottom ? (move_down p_bottom) : reference_bounds.move_past_bottom unless at_page_top?
564
601
  ensure
602
+ cursor > p_bottom ? (move_down p_bottom) : reference_bounds.move_past_bottom unless at_page_top?
565
603
  @bottom_gutters.pop
566
604
  bounds.subtract_left_padding p_left
567
605
  bounds.subtract_right_padding p_right
@@ -576,26 +614,25 @@ module Asciidoctor
576
614
  end
577
615
 
578
616
  def expand_padding_value shorthand
579
- unless (padding = (@side_area_shorthand_cache ||= {})[shorthand])
580
- if ::Array === shorthand
581
- case shorthand.size
617
+ (@edge_shorthand_cache ||= ::Hash.new do |store, key|
618
+ if ::Array === key
619
+ case key.size
582
620
  when 1
583
- padding = [shorthand[0], shorthand[0], shorthand[0], shorthand[0]]
621
+ value = [(value0 = key[0] || 0), value0, value0, value0]
584
622
  when 2
585
- padding = [shorthand[0], shorthand[1], shorthand[0], shorthand[1]]
623
+ value = [(value0 = key[0] || 0), (value1 = key[1] || 0), value0, value1]
586
624
  when 3
587
- padding = [shorthand[0], shorthand[1], shorthand[2], shorthand[1]]
625
+ value = [key[0] || 0, (value1 = key[1] || 0), key[2] || 0, value1]
588
626
  when 4
589
- padding = shorthand
627
+ value = key.map {|it| it || 0 }
590
628
  else
591
- padding = shorthand.slice 0, 4
629
+ value = (key.slice 0, 4).map {|it| it || 0 }
592
630
  end
593
631
  else
594
- padding = ::Array.new 4, (shorthand || 0)
632
+ value = [(value0 = key || 0), value0, value0, value0]
595
633
  end
596
- @side_area_shorthand_cache[shorthand] = padding
597
- end
598
- padding.dup
634
+ store[key] = value
635
+ end)[shorthand]
599
636
  end
600
637
 
601
638
  alias expand_margin_value expand_padding_value
@@ -700,7 +737,8 @@ module Asciidoctor
700
737
  def fill_and_stroke_bounds f_color = fill_color, s_color = stroke_color, options = {}
701
738
  no_fill = !f_color || f_color == 'transparent'
702
739
  if ::Array === (s_width = options[:line_width] || 0)
703
- s_width_max = s_width.map(&:to_i).max
740
+ s_width = [s_width[0], s_width[1], s_width[0], s_width[1]] if s_width.size == 2
741
+ s_width_max = (s_width = s_width.map {|it| it || 0 }).max
704
742
  radius = 0
705
743
  else
706
744
  radius = options[:radius] || 0
@@ -718,13 +756,19 @@ module Asciidoctor
718
756
 
719
757
  # stroke
720
758
  if s_width_max
721
- if (s_width_end = s_width[0] || 0) > 0
722
- stroke_horizontal_rule s_color, line_width: s_width_end, line_style: options[:line_style]
723
- stroke_horizontal_rule s_color, line_width: s_width_end, line_style: options[:line_style], at: bounds.height
759
+ s_width_top, s_width_right, s_width_bottom, s_width_left = s_width
760
+ projection_top, projection_right, projection_bottom, projection_left = s_width.map {|it| it * 0.5 }
761
+ if s_width_top > 0
762
+ stroke_horizontal_rule s_color, line_width: s_width_top, line_style: options[:line_style], left_projection: projection_left, right_projection: projection_right
763
+ end
764
+ if s_width_right > 0
765
+ stroke_vertical_rule s_color, line_width: s_width_right, line_style: options[:line_style], at: bounds.width, top_projection: projection_top, bottom_projection: projection_bottom
724
766
  end
725
- if (s_width_side = s_width[1] || 0) > 0
726
- stroke_vertical_rule s_color, line_width: s_width_side, line_style: options[:line_style]
727
- stroke_vertical_rule s_color, line_width: s_width_side, line_style: options[:line_style], at: bounds.width
767
+ if s_width_bottom > 0
768
+ stroke_horizontal_rule s_color, line_width: s_width_bottom, line_style: options[:line_style], at: bounds.height, left_projection: projection_left, right_projection: projection_right
769
+ end
770
+ if s_width_left > 0
771
+ stroke_vertical_rule s_color, line_width: s_width_left, line_style: options[:line_style], top_projection: projection_top, bottom_projection: projection_bottom
728
772
  end
729
773
  else
730
774
  stroke_color s_color
@@ -759,8 +803,8 @@ module Asciidoctor
759
803
  rule_y = cursor - (options[:at] || 0)
760
804
  rule_style = options[:line_style]
761
805
  rule_width = options[:line_width] || 0.5
762
- rule_x_start = bounds.left
763
- rule_x_end = bounds.right
806
+ rule_x_start = bounds.left - (options[:left_projection] || 0)
807
+ rule_x_end = bounds.right - (options[:right_projection] || 0)
764
808
  save_graphics_state do
765
809
  stroke_color rule_color
766
810
  case rule_style
@@ -790,8 +834,8 @@ module Asciidoctor
790
834
  #
791
835
  def stroke_vertical_rule rule_color = stroke_color, options = {}
792
836
  rule_x = options[:at] || 0
793
- rule_y_from = bounds.top
794
- rule_y_to = bounds.bottom
837
+ rule_y_from = bounds.top + (options[:top_projection] || 0)
838
+ rule_y_to = bounds.bottom - (options[:bottom_projection] || 0)
795
839
  rule_style = options[:line_style]
796
840
  rule_width = options[:line_width] || 0.5
797
841
  save_graphics_state do
@@ -814,7 +858,7 @@ module Asciidoctor
814
858
 
815
859
  # Deletes the current page and move the cursor
816
860
  # to the previous page.
817
- def delete_page
861
+ def delete_current_page
818
862
  pg = page_number
819
863
  pdf_store = state.store
820
864
  content_id = page.content.identifier
@@ -848,7 +892,7 @@ module Asciidoctor
848
892
  prev_page_size = page.size
849
893
  state.compress = false if state.compress # can't use compression if using template
850
894
  prev_text_rendering_mode = (defined? @text_rendering_mode) ? @text_rendering_mode : nil
851
- delete_page if options[:replace]
895
+ delete_current_page if options[:replace]
852
896
  # NOTE: use functionality provided by prawn-templates
853
897
  start_new_page_discretely template: file, template_page: options[:page]
854
898
  # prawn-templates sets text_rendering_mode to :unknown, which breaks running content; revert
@@ -860,11 +904,11 @@ module Asciidoctor
860
904
  # way atm to prevent the size & layout of the imported page from affecting subsequent pages
861
905
  advance_page size: prev_page_size, layout: prev_page_layout if options.fetch :advance, true
862
906
  elsif options.fetch :advance_if_missing, true
863
- delete_page
907
+ delete_current_page
864
908
  # NOTE: see previous comment
865
909
  advance_page size: prev_page_size, layout: prev_page_layout
866
910
  else
867
- delete_page
911
+ delete_current_page
868
912
  end
869
913
  nil
870
914
  end
@@ -993,7 +1037,15 @@ module Asciidoctor
993
1037
  state.on_page_create_callback = delegate
994
1038
  end
995
1039
 
996
- # NOTE: only used in dry_run since that's when DetectEmptyFirstPage is active
1040
+ # This method delegates to the provided block, then tares (i.e., resets) the content stream of
1041
+ # the initial page.
1042
+ #
1043
+ # The purpose of this method is to ink content while making it appear as though the page is
1044
+ # empty. This technique allows the caller to detect whether any subsequent content was written
1045
+ # to the page following the content inked by the block. It's often used to keep the title of a
1046
+ # content block with the block's first child.
1047
+ #
1048
+ # NOTE: this method should only used inside dry_run since that's when DetectEmptyFirstPage is active
997
1049
  def tare_first_page_content_stream
998
1050
  return yield unless DetectEmptyFirstPage === (delegate = state.on_page_create_callback)
999
1051
  on_page_create_called = nil
@@ -4,13 +4,18 @@ Prawn::Text::Formatted::Box.prepend (Module.new do
4
4
  include Asciidoctor::Logging
5
5
 
6
6
  def initialize formatted_text, options = {}
7
+ if (color = options[:color]) && !formatted_text.empty?
8
+ formatted_text = formatted_text.map {|fragment| fragment[:color] ? fragment : (fragment.merge color: color) }
9
+ end
7
10
  super
8
11
  formatted_text[0][:normalize_line_height] = true if options[:normalize_line_height] && !formatted_text.empty?
9
12
  options[:extensions]&.each {|extension| extend extension }
13
+ extend Prawn::Text::Formatted::IndentedParagraphWrap if (@indent_paragraphs = options[:indent_paragraphs])
10
14
  if (bottom_gutter = options[:bottom_gutter]) && bottom_gutter > 0
11
15
  @bottom_gutter = bottom_gutter
12
16
  extend Prawn::Text::Formatted::ProtectBottomGutter
13
17
  end
18
+ @force_justify = options[:force_justify]
14
19
  end
15
20
 
16
21
  def draw_fragment_overlay_styles fragment
@@ -71,6 +76,15 @@ Prawn::Text::Formatted::Box.prepend (Module.new do
71
76
  end
72
77
  end
73
78
 
79
+ # Override method to force text justification when :force_justify option is set (typically for rendering a single line)
80
+ def word_spacing_for_this_line
81
+ if @align == :justify && (@force_justify || (@line_wrap.space_count > 0 && !@line_wrap.paragraph_finished?))
82
+ (available_width - @line_wrap.width) / @line_wrap.space_count
83
+ else
84
+ 0
85
+ end
86
+ end
87
+
74
88
  # Override method in super class to provide support for a tuple consisting of alignment and offset
75
89
  def process_vertical_alignment text
76
90
  return super if Symbol === (valign = @vertical_align)
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prawn::Text::Formatted
4
+ module IndentedParagraphWrap
5
+ # Override Prawn::Text::Formatted::Box#wrap method to add support for :indent_paragraphs to (formatted_)text_box.
6
+ def wrap array
7
+ initialize_wrap array
8
+ stop = nil
9
+ until stop
10
+ if (first_line_indent = @indent_paragraphs) && @printed_lines.empty?
11
+ @width -= first_line_indent
12
+ stop = @document.indent(first_line_indent) { wrap_and_print_line }
13
+ @width += first_line_indent
14
+ else
15
+ stop = wrap_and_print_line
16
+ end
17
+ end
18
+ @text = @printed_lines.join ?\n
19
+ @everything_printed = @arranger.finished?
20
+ @arranger.unconsumed
21
+ end
22
+
23
+ def wrap_and_print_line
24
+ @line_wrap.wrap_line \
25
+ document: @document,
26
+ kerning: @kerning,
27
+ width: @width,
28
+ arranger: @arranger,
29
+ disable_wrap_by_char: @disable_wrap_by_char
30
+ if enough_height_for_this_line?
31
+ move_baseline_down
32
+ print_line
33
+ @single_line || @arranger.finished?
34
+ else
35
+ true
36
+ end
37
+ end
38
+ end
39
+ end
@@ -41,17 +41,10 @@ module Prawn
41
41
  extent = @pdf.dry_run keep_together: true, single_page: true do
42
42
  push_scratch parent_doc
43
43
  doc.catalog[:footnotes] = parent_doc.catalog[:footnotes]
44
- if padding_y > 0
45
- move_down padding_y
46
- #elsif at_page_top?
47
- else
48
- # TODO: encapsulate this logic to force top margin to be applied
49
- margin_box.instance_variable_set :@y, margin_box.absolute_top + 0.0001
50
- end
51
44
  # NOTE: we should be able to use cell.max_width, but returns 0 in some conditions (like when colspan > 1)
52
45
  indent cell.padding_left, bounds.width - cell.width + cell.padding_right do
53
- # TODO: truncate margin bottom of last block
54
- traverse cell.content
46
+ move_down padding_y if padding_y > 0
47
+ conceal_page_top { traverse cell.content }
55
48
  end
56
49
  pop_scratch parent_doc
57
50
  doc.catalog[:footnotes] = parent_doc.catalog[:footnotes]
@@ -99,7 +92,7 @@ module Prawn
99
92
  apply_font_properties { pdf.traverse content }
100
93
  if (extra_pages = pdf.page_number - start_page) > 0
101
94
  logger.error %(the table cell on page #{start_page} has been truncated; Asciidoctor PDF does not support table cell content that exceeds the height of a single page) unless extra_pages == 1 && pdf.page.empty?
102
- extra_pages.times { pdf.delete_page }
95
+ extra_pages.times { pdf.delete_current_page }
103
96
  end
104
97
  nil
105
98
  end
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # the following are organized under the Asciidoctor::Prawn namespace
4
+ require_relative 'prawn/document/column_box'
4
5
  require_relative 'prawn/font_metric_cache'
5
6
  require_relative 'prawn/font/afm'
6
7
  require_relative 'prawn/images'
7
8
  require_relative 'prawn/formatted_text/arranger'
8
9
  require_relative 'prawn/formatted_text/box'
9
10
  require_relative 'prawn/formatted_text/fragment'
11
+ require_relative 'prawn/formatted_text/indented_paragraph_wrap'
10
12
  require_relative 'prawn/formatted_text/protect_bottom_gutter'
11
13
  require_relative 'prawn/extensions'
@@ -24,14 +24,19 @@ module Asciidoctor
24
24
  @arranger.unconsumed.unshift highlight_line if highlight_line
25
25
  @arranger.unconsumed.unshift linenum_spacer.dup
26
26
  end
27
- @line_wrap.wrap_line document: @document, kerning: @kerning, width: available_width, arranger: @arranger, disable_wrap_by_char: @disable_wrap_by_char
27
+ @line_wrap.wrap_line \
28
+ document: @document,
29
+ kerning: @kerning,
30
+ width: @width,
31
+ arranger: @arranger,
32
+ disable_wrap_by_char: @disable_wrap_by_char
28
33
  if enough_height_for_this_line?
29
34
  move_baseline_down
30
35
  print_line
36
+ stop = @arranger.finished?
31
37
  else
32
38
  stop = true
33
39
  end
34
- stop ||= @arranger.finished?
35
40
  end
36
41
  @text = @printed_lines.join ?\n
37
42
  @everything_printed = @arranger.finished?
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ Prawn.image_handler.register! Prawn::Images::PNG if defined? GMagick::Image
@@ -40,9 +40,10 @@ module Asciidoctor
40
40
  attr_reader :quality
41
41
  attr_reader :compatibility_level
42
42
 
43
- def initialize quality = 'default', compatibility_level = '1.4'
43
+ def initialize quality = 'default', compatibility_level = '1.4', compliance = 'PDF'
44
44
  @quality = QUALITY_NAMES[quality]
45
45
  @compatibility_level = compatibility_level
46
+ @compliance = compliance
46
47
  if (gs_path = ::ENV['GS'])
47
48
  ::RGhost::Config::GS[:path] = gs_path
48
49
  end
@@ -57,10 +58,16 @@ module Asciidoctor
57
58
  else
58
59
  inputs = target
59
60
  end
60
- (::RGhost::Convert.new inputs).to :pdf,
61
- filename: filename_tmp.to_s,
62
- quality: @quality,
63
- d: { Printed: false, CannotEmbedFontPolicy: '/Warning', CompatibilityLevel: @compatibility_level }
61
+ d = { Printed: false, CannotEmbedFontPolicy: '/Warning', CompatibilityLevel: @compatibility_level }
62
+ case @compliance
63
+ when 'PDF/A', 'PDF/A-1', 'PDF/A-2', 'PDF/A-3'
64
+ d[:PDFA] = ((@compliance.split '-', 2)[1] || 1).to_i
65
+ d[:ShowAnnots] = false
66
+ when 'PDF/X', 'PDF/X-1', 'PDF/X-3'
67
+ d[:PDFX] = true
68
+ d[:ShowAnnots] = false
69
+ end
70
+ (::RGhost::Convert.new inputs).to :pdf, filename: filename_tmp.to_s, quality: @quality, d: d
64
71
  filename_o.binwrite filename_tmp.binread
65
72
  end
66
73
  nil
@@ -10,6 +10,12 @@ module Asciidoctor
10
10
  WordRx = /\p{Word}+/
11
11
  Hyphen = '-'
12
12
  SoftHyphen = ?\u00ad
13
+ LowerAlphaChars = 'a-z'
14
+ # NOTE: use more widely-supported ғ instead of ꜰ as replacement for F
15
+ # NOTE: use more widely-supported ǫ instead of ꞯ as replacement for Q
16
+ # NOTE: use more widely-supported s (lowercase latin "s") instead of ꜱ as replacement for S
17
+ # NOTE: in small caps, x (lowercase latin "x") remains unchanged
18
+ SmallCapsChars = 'ᴀʙᴄᴅᴇғɢʜɪᴊᴋʟᴍɴoᴘǫʀsᴛᴜᴠᴡxʏᴢ'
13
19
 
14
20
  def capitalize_words_pcdata string
15
21
  if XMLMarkupRx.match? string
@@ -50,6 +56,14 @@ module Asciidoctor
50
56
  string.upcase
51
57
  end
52
58
  end
59
+
60
+ def smallcaps_pcdata string
61
+ if XMLMarkupRx.match? string
62
+ string.gsub(PCDATAFilterRx) { $2 ? ($2.tr LowerAlphaChars, SmallCapsChars) : $1 }
63
+ else
64
+ string.tr LowerAlphaChars, SmallCapsChars
65
+ end
66
+ end
53
67
  end
54
68
  end
55
69
  end
@@ -15,9 +15,10 @@ module Asciidoctor
15
15
  BaseThemePath = ::File.join ThemesDir, 'base-theme.yml'
16
16
  BundledThemeNames = (::Dir.children ThemesDir).map {|it| it.slice 0, it.length - 10 }
17
17
  DeprecatedCategoryKeys = { 'blockquote' => 'quote', 'key' => 'kbd', 'literal' => 'codespan', 'outline_list' => 'list' }
18
- AmbiguousAlignKeys = %w(base heading heading_h1 heading_h2 heading_h3 heading_h4 heading_h5 heading_h6 title_page abstract abstract_title admonition_label sidebar_title toc_title).each_with_object({}) do |prefix, accum|
18
+ DeprecatedKeys = %w(base heading heading_h1 heading_h2 heading_h3 heading_h4 heading_h5 heading_h6 title_page abstract abstract_title admonition_label sidebar_title toc_title).each_with_object({ 'table_caption_side' => 'table_caption_end' }) do |prefix, accum|
19
19
  accum[%(#{prefix}_align)] = %(#{prefix}_text_align)
20
20
  end
21
+ PaddingBottomHackKeys = %w(example_padding quote_padding sidebar_padding verse_padding)
21
22
 
22
23
  VariableRx = /\$([a-z0-9_-]+)/
23
24
  LoneVariableRx = /^\$([a-z0-9_-]+)$/
@@ -170,9 +171,14 @@ module Asciidoctor
170
171
  val.each do |subkey, subval|
171
172
  process_entry %(#{key}_#{key == 'role' || !(subkey.include? '-') ? subkey : (subkey.tr '-', '_')}), subval, data
172
173
  end
173
- elsif (rekey = AmbiguousAlignKeys[key]) ||
174
+ elsif (rekey = DeprecatedKeys[key]) ||
174
175
  ((key.start_with? 'role_') && (key.end_with? '_align') && (rekey = key.sub RoleAlignKeyRx, '_text_align'))
175
176
  data[rekey] = evaluate val, data
177
+ elsif PaddingBottomHackKeys.include? key
178
+ val = evaluate val, data
179
+ # normalize padding hacks for themes designed before the converter had smart margins
180
+ val[2] = val[0] if ::Array === val && val[0].to_f >= 0 && val[2].to_f <= 0
181
+ data[key] = val
176
182
  # QUESTION: do we really need to evaluate_math in this case?
177
183
  elsif key.end_with? '_color'
178
184
  if key == 'table_border_color'
@@ -216,7 +222,7 @@ module Asciidoctor
216
222
  var = var.tr '-', '_' if var.include? '-'
217
223
  if (vars.respond_to? var) ||
218
224
  DeprecatedCategoryKeys.any? {|old, new| (var.start_with? old + '_') && (vars.respond_to? (replace = new + (var.slice old.length, var.length))) && (var = replace) } ||
219
- ((replace = AmbiguousAlignKeys[var]) && (vars.respond_to? replace) && (var = replace))
225
+ ((replace = DeprecatedKeys[var]) && (vars.respond_to? replace) && (var = replace))
220
226
  vars[var]
221
227
  else
222
228
  logger.warn %(unknown variable reference in PDF theme: #{ref})
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Asciidoctor
4
4
  module PDF
5
- VERSION = '2.0.0.alpha.2'
5
+ VERSION = '2.0.0.beta.2'
6
6
  end
7
7
  end