asciidoctor-pdf 1.6.1 → 2.0.0.alpha.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.adoc +273 -31
  4. data/NOTICE.adoc +16 -4
  5. data/README.adoc +208 -68
  6. data/asciidoctor-pdf.gemspec +3 -7
  7. data/data/fonts/ABOUT-mplus1mn-subset +1 -1
  8. data/data/fonts/ABOUT-mplus1p-subset +2 -2
  9. data/data/fonts/ABOUT-notosans-subset +26 -0
  10. data/data/fonts/ABOUT-notoserif-subset +1 -1
  11. data/data/fonts/{LICENSE-notoserif → LICENSE-noto} +0 -0
  12. data/data/fonts/mplus1mn-bold-subset.ttf +0 -0
  13. data/data/fonts/mplus1mn-bold_italic-subset.ttf +0 -0
  14. data/data/fonts/mplus1mn-italic-subset.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/notoemoji-subset.ttf +0 -0
  18. data/data/fonts/notosans-bold-subset.ttf +0 -0
  19. data/data/fonts/notosans-bold_italic-subset.ttf +0 -0
  20. data/data/fonts/notosans-italic-subset.ttf +0 -0
  21. data/data/fonts/notosans-regular-subset.ttf +0 -0
  22. data/data/fonts/notoserif-bold-subset.ttf +0 -0
  23. data/data/fonts/notoserif-bold_italic-subset.ttf +0 -0
  24. data/data/fonts/notoserif-italic-subset.ttf +0 -0
  25. data/data/fonts/notoserif-regular-subset.ttf +0 -0
  26. data/data/themes/base-theme.yml +21 -24
  27. data/data/themes/default-for-print-theme.yml +24 -0
  28. data/data/themes/default-for-print-with-fallback-font-theme.yml +3 -0
  29. data/data/themes/default-theme.yml +55 -59
  30. data/data/themes/default-with-fallback-font-theme.yml +2 -2
  31. data/data/themes/sans-with-fallback-font-theme.yml +10 -0
  32. data/docs/theming-guide.adoc +977 -352
  33. data/lib/asciidoctor/pdf/converter.rb +1853 -1566
  34. data/lib/asciidoctor/pdf/ext/asciidoctor/document.rb +22 -1
  35. data/lib/asciidoctor/pdf/ext/asciidoctor/image.rb +9 -15
  36. data/lib/asciidoctor/pdf/ext/asciidoctor/list.rb +6 -13
  37. data/lib/asciidoctor/pdf/ext/asciidoctor/section.rb +3 -16
  38. data/lib/asciidoctor/pdf/ext/asciidoctor.rb +1 -5
  39. data/lib/asciidoctor/pdf/ext/core/file.rb +1 -1
  40. data/lib/asciidoctor/pdf/ext/core/quantifiable_stdout.rb +1 -4
  41. data/lib/asciidoctor/pdf/ext/core/string.rb +2 -2
  42. data/lib/asciidoctor/pdf/ext/core.rb +1 -4
  43. data/lib/asciidoctor/pdf/ext/pdf-core/page.rb +8 -33
  44. data/lib/asciidoctor/pdf/ext/pdf-core.rb +0 -16
  45. data/lib/asciidoctor/pdf/ext/prawn/coderay_encoder.rb +5 -7
  46. data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +489 -331
  47. data/lib/asciidoctor/pdf/ext/prawn/font/afm.rb +0 -4
  48. data/lib/asciidoctor/pdf/ext/prawn/font_metric_cache.rb +1 -1
  49. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/arranger.rb +33 -3
  50. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/box.rb +25 -14
  51. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/fragment.rb +9 -3
  52. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/protect_bottom_gutter.rb +13 -0
  53. data/lib/asciidoctor/pdf/ext/prawn/images.rb +20 -18
  54. data/lib/asciidoctor/pdf/ext/prawn-svg/loaders/data.rb +6 -0
  55. data/lib/asciidoctor/pdf/ext/prawn-svg/loaders/web.rb +22 -0
  56. data/lib/asciidoctor/pdf/ext/prawn-svg/url_loader.rb +13 -0
  57. data/lib/asciidoctor/pdf/ext/prawn-svg.rb +5 -2
  58. data/lib/asciidoctor/pdf/ext/prawn-table/cell/asciidoc.rb +76 -20
  59. data/lib/asciidoctor/pdf/ext/prawn-table/cell/text.rb +39 -1
  60. data/lib/asciidoctor/pdf/ext/prawn-table/cell.rb +21 -15
  61. data/lib/asciidoctor/pdf/ext/prawn-table.rb +1 -1
  62. data/lib/asciidoctor/pdf/ext/prawn.rb +1 -0
  63. data/lib/asciidoctor/pdf/ext/pygments.rb +2 -2
  64. data/lib/asciidoctor/pdf/ext/rouge/formatters/prawn.rb +17 -20
  65. data/lib/asciidoctor/pdf/ext/rouge/themes/asciidoctor_pdf_default.rb +1 -0
  66. data/lib/asciidoctor/pdf/ext/rouge.rb +0 -1
  67. data/lib/asciidoctor/pdf/formatted_text/formatter.rb +2 -2
  68. data/lib/asciidoctor/pdf/formatted_text/inline_destination_marker.rb +8 -10
  69. data/lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb +69 -78
  70. data/lib/asciidoctor/pdf/formatted_text/inline_image_renderer.rb +7 -10
  71. data/lib/asciidoctor/pdf/formatted_text/inline_text_aligner.rb +2 -4
  72. data/lib/asciidoctor/pdf/formatted_text/parser.rb +53 -47
  73. data/lib/asciidoctor/pdf/formatted_text/parser.treetop +5 -7
  74. data/lib/asciidoctor/pdf/formatted_text/source_wrap.rb +14 -14
  75. data/lib/asciidoctor/pdf/formatted_text/text_background_and_border_renderer.rb +4 -7
  76. data/lib/asciidoctor/pdf/formatted_text/transform.rb +122 -110
  77. data/lib/asciidoctor/pdf/formatted_text.rb +0 -1
  78. data/lib/asciidoctor/pdf/index_catalog.rb +7 -11
  79. data/lib/asciidoctor/pdf/nogmagick.rb +6 -0
  80. data/lib/asciidoctor/pdf/optimizer.rb +3 -5
  81. data/lib/asciidoctor/pdf/pdfmark.rb +16 -8
  82. data/lib/asciidoctor/pdf/roman_numeral.rb +4 -22
  83. data/lib/asciidoctor/pdf/sanitizer.rb +18 -13
  84. data/lib/asciidoctor/pdf/section_info_by_page.rb +24 -0
  85. data/lib/asciidoctor/pdf/theme_loader.rb +100 -80
  86. data/lib/asciidoctor/pdf/version.rb +1 -2
  87. data/lib/asciidoctor/pdf.rb +5 -2
  88. metadata +36 -64
  89. data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
  90. data/data/fonts/mplus1mn-bold_italic-ascii.ttf +0 -0
  91. data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
  92. data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
  93. data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_block.rb +0 -7
  94. data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_node.rb +0 -7
  95. data/lib/asciidoctor/pdf/ext/asciidoctor/list_item.rb +0 -18
  96. data/lib/asciidoctor/pdf/ext/asciidoctor/logging_shim.rb +0 -33
  97. data/lib/asciidoctor/pdf/ext/core/array.rb +0 -11
  98. data/lib/asciidoctor/pdf/ext/core/hash.rb +0 -7
  99. data/lib/asciidoctor/pdf/ext/core/regexp.rb +0 -5
  100. data/lib/asciidoctor/pdf/ext/pdf-core/pdf_object.rb +0 -8
  101. data/lib/asciidoctor-pdf/converter.rb +0 -3
  102. data/lib/asciidoctor-pdf/version.rb +0 -3
@@ -4,7 +4,7 @@ Prawn::Font::AFM.instance_variable_set :@hide_m17n_warning, true
4
4
 
5
5
  require 'prawn/icon'
6
6
 
7
- Prawn::Icon::Compatibility.send :prepend, (::Module.new { def warning *args; end })
7
+ Prawn::Icon::Compatibility.send :prepend, (::Module.new { def warning *_args; end })
8
8
 
9
9
  module Asciidoctor
10
10
  module Prawn
@@ -22,7 +22,7 @@ module Asciidoctor
22
22
  italic: [:italic].to_set,
23
23
  bold_italic: [:bold, :italic].to_set,
24
24
  }).default = ::Set.new
25
- # NOTE must use a visible char for placeholder or else Prawn won't reserve space for the fragment
25
+ # NOTE: must use a visible char for placeholder or else Prawn won't reserve space for the fragment
26
26
  PlaceholderChar = ?\u2063
27
27
 
28
28
  # - :height is the height of a line
@@ -32,6 +32,91 @@ module Asciidoctor
32
32
  # - :final_gap determines whether a gap is added below the last line
33
33
  LineMetrics = ::Struct.new :height, :leading, :padding_top, :padding_bottom, :final_gap
34
34
 
35
+ Position = ::Struct.new :page, :cursor
36
+
37
+ Extent = ::Struct.new :current, :from, :to do
38
+ def initialize current_page, current_cursor, start_page, start_cursor, end_page, end_cursor
39
+ self.from = self.current = Position.new current_page, current_cursor
40
+ self.from = Position.new start_page, start_cursor unless start_page == current_page && start_cursor == current_cursor
41
+ self.to = Position.new end_page, end_cursor
42
+ end
43
+
44
+ def each_page
45
+ from.page.upto to.page do |pgnum|
46
+ yield pgnum == from.page && from, pgnum == to.page && to, pgnum
47
+ end
48
+ end
49
+
50
+ def single_page?
51
+ from.page == to.page
52
+ end
53
+
54
+ def single_page_height
55
+ single_page? ? from.cursor - to.cursor : nil
56
+ end
57
+
58
+ def page_range
59
+ (from.page..to.page)
60
+ end
61
+ end
62
+
63
+ ScratchExtent = ::Struct.new :from, :to do
64
+ def initialize start_page, start_cursor, end_page, end_cursor
65
+ self.from = Position.new start_page, start_cursor
66
+ self.to = Position.new end_page, end_cursor
67
+ end
68
+
69
+ def position_onto pdf, keep_together = nil
70
+ current_page = pdf.page_number
71
+ current_cursor = pdf.cursor
72
+ from_page = current_page + (advance_by = from.page - 1)
73
+ to_page = current_page + (to.page - 1)
74
+ if advance_by > 0
75
+ advance_by.times { pdf.advance_page }
76
+ elsif keep_together && single_page? && !(try_to_fit_on_previous current_cursor)
77
+ pdf.advance_page
78
+ from_page += 1
79
+ to_page += 1
80
+ end
81
+ Extent.new current_page, current_cursor, from_page, from.cursor, to_page, to.cursor
82
+ end
83
+
84
+ def single_page?
85
+ from.page == to.page
86
+ end
87
+
88
+ def single_page_height
89
+ single_page? ? from.cursor - to.cursor : nil
90
+ end
91
+
92
+ def try_to_fit_on_previous reference_cursor
93
+ if (height = from.cursor - to.cursor) <= reference_cursor
94
+ from.cursor = reference_cursor
95
+ to.cursor = reference_cursor - height
96
+ true
97
+ else
98
+ false
99
+ end
100
+ end
101
+ end
102
+
103
+ NewPageRequiredError = ::Class.new ::StopIteration
104
+
105
+ InhibitNewPageProc = proc do |pdf|
106
+ pdf.delete_page
107
+ raise NewPageRequiredError
108
+ end
109
+
110
+ DetectEmptyFirstPage = ::Module.new
111
+
112
+ DetectEmptyFirstPageProc = proc do |delegate, pdf|
113
+ if pdf.state.pages[pdf.page_number - 2].empty?
114
+ pdf.delete_page
115
+ raise NewPageRequiredError
116
+ end
117
+ delegate.call pdf if (pdf.state.on_page_create_callback = delegate)
118
+ end
119
+
35
120
  # Core
36
121
 
37
122
  # Retrieves the catalog reference data for the PDF.
@@ -54,14 +139,6 @@ module Asciidoctor
54
139
  page.dimensions[2]
55
140
  end
56
141
 
57
- # Returns the effective (writable) width of the page
58
- #
59
- # If inside a bounding box, returns width of box.
60
- #
61
- def effective_page_width
62
- reference_bounds.width
63
- end
64
-
65
142
  # Returns the height of the current page from edge-to-edge
66
143
  #
67
144
  def page_height
@@ -70,13 +147,13 @@ module Asciidoctor
70
147
 
71
148
  # Returns the effective (writable) height of the page
72
149
  #
73
- # If inside a fixed-height bounding box, returns width of box.
150
+ # If inside a fixed-height bounding box, returns height of box.
74
151
  #
75
152
  def effective_page_height
76
153
  reference_bounds.height
77
154
  end
78
155
 
79
- # workaround for https://github.com/prawnpdf/prawn/issues/1121
156
+ # remove once fixed upstream; see https://github.com/prawnpdf/prawn/pull/1122
80
157
  def generate_margin_box
81
158
  page_w, page_h = (page = state.page).dimensions.slice 2, 2
82
159
  page_m = page.margins
@@ -108,7 +185,7 @@ module Asciidoctor
108
185
  # Returns the margins for the current page as a 4 element array (top, right, bottom, left)
109
186
  #
110
187
  def page_margin
111
- [page.margins[:top], page.margins[:right], page.margins[:bottom], page.margins[:left]]
188
+ [page_margin_top, page_margin_right, page_margin_bottom, page_margin_left]
112
189
  end
113
190
 
114
191
  # Returns the width of the left margin for the current page
@@ -116,16 +193,12 @@ module Asciidoctor
116
193
  def page_margin_left
117
194
  page.margins[:left]
118
195
  end
119
- # deprecated
120
- alias left_margin page_margin_left
121
196
 
122
197
  # Returns the width of the right margin for the current page
123
198
  #
124
199
  def page_margin_right
125
200
  page.margins[:right]
126
201
  end
127
- # deprecated
128
- alias right_margin page_margin_right
129
202
 
130
203
  # Returns the width of the top margin for the current page
131
204
  #
@@ -157,7 +230,7 @@ module Asciidoctor
157
230
  if invert
158
231
  (recto_page? pgnum) ? :verso : :recto
159
232
  else
160
- (recto_page? pgnum) ? :recto : :verso
233
+ (verso_page? pgnum) ? :verso : :recto
161
234
  end
162
235
  end
163
236
 
@@ -243,16 +316,10 @@ module Asciidoctor
243
316
  { family: font.options[:family], style: (font.options[:style] || :normal), size: @font_size }
244
317
  end
245
318
 
246
- # Sets the font style for the scope of the block to which this method
247
- # yields. If the style is nil and no block is given, return the current
248
- # font style.
319
+ # Set the font style on the document, if a style is given, otherwise return the current font style.
249
320
  #
250
321
  def font_style style = nil
251
- if block_given?
252
- font font.options[:family], style: style do
253
- yield
254
- end
255
- elsif style
322
+ if style
256
323
  font font.options[:family], style: style
257
324
  else
258
325
  font.options[:style] || :normal
@@ -264,12 +331,10 @@ module Asciidoctor
264
331
  # implementation to carry out the built-in functionality.
265
332
  #
266
333
  #--
267
- # QUESTION should we round the result?
334
+ # QUESTION: should we round the result?
268
335
  def font_size points = nil
269
336
  return @font_size unless points
270
- if points == 1
271
- super @font_size
272
- elsif String === points
337
+ if ::String === points
273
338
  if points.end_with? 'rem'
274
339
  super @root_font_size * points.to_f
275
340
  elsif points.end_with? 'em'
@@ -279,8 +344,8 @@ module Asciidoctor
279
344
  else
280
345
  super points.to_f
281
346
  end
282
- # FIXME: HACK assume em value
283
- elsif points < 1
347
+ # NOTE: assume em value (since a font size of 1 is extremely unlikely)
348
+ elsif points <= 1
284
349
  super @font_size * points
285
350
  else
286
351
  super points
@@ -304,37 +369,9 @@ module Asciidoctor
304
369
  FontStyleToSet[style].dup
305
370
  end
306
371
 
307
- # Apply the font settings (family, size, styles and character spacing) from
308
- # the fragment to the document, then yield to the block.
309
- #
310
- # The original font settings are restored before this method returns.
311
- #
312
- def fragment_font fragment
313
- f_info = font_info
314
- f_family = fragment[:font] || f_info[:family]
315
- f_size = fragment[:size] || f_info[:size]
316
- if (f_styles = fragment[:styles])
317
- f_style = resolve_font_style f_styles
318
- else
319
- f_style = :normal
320
- end
321
-
322
- if (c_spacing = fragment[:character_spacing])
323
- character_spacing c_spacing do
324
- font f_family, size: f_size, style: f_style do
325
- yield
326
- end
327
- end
328
- else
329
- font f_family, size: f_size, style: f_style do
330
- yield
331
- end
332
- end
333
- end
334
-
335
372
  # Override width of string to check for placeholder char, which uses character spacing to control width
336
373
  #
337
- def width_of_string string, options = {}
374
+ def width_of_string string, options
338
375
  string == PlaceholderChar ? @character_spacing : super
339
376
  end
340
377
 
@@ -346,7 +383,7 @@ module Asciidoctor
346
383
  ::Prawn::Icon::Compatibility::SHIMS[%(fa-#{name})]
347
384
  end
348
385
 
349
- def calc_line_metrics line_height = 1, font = self.font, font_size = self.font_size
386
+ def calc_line_metrics line_height, font = self.font, font_size = self.font_size
350
387
  line_height_length = line_height * font_size
351
388
  leading = line_height_length - font_size
352
389
  half_leading = leading / 2
@@ -355,20 +392,6 @@ module Asciidoctor
355
392
  LineMetrics.new line_height_length, leading, padding_top, padding_bottom, false
356
393
  end
357
394
 
358
- =begin
359
- # these line metrics attempted to figure out a correction based on the reported height and the font_size
360
- # however, it only works for some fonts, and breaks down for fonts like Noto Serif
361
- def calc_line_metrics line_height = 1, font = self.font, font_size = self.font_size
362
- line_height_length = font_size * line_height
363
- line_gap = line_height_length - font_size
364
- correction = font.height - font_size
365
- leading = line_gap - correction
366
- shift = (font.line_gap + correction + line_gap) / 2
367
- final_gap = font.line_gap != 0
368
- LineMetrics.new line_height_length, leading, shift, shift, final_gap
369
- end
370
- =end
371
-
372
395
  # Parse the text into an array of fragments using the text formatter.
373
396
  def parse_text string, options = {}
374
397
  return [] if string.nil?
@@ -383,22 +406,22 @@ module Asciidoctor
383
406
 
384
407
  if (color = options.delete :color)
385
408
  fragments.map do |fragment|
386
- fragment[:color] ? fragment : fragment.merge(color: color)
409
+ fragment[:color] ? fragment : (fragment.merge color: color)
387
410
  end
388
411
  else
389
412
  fragments
390
413
  end
391
414
  end
392
415
 
393
- # NOTE override built-in fill_formatted_text_box to insert leading before second line when :first_line is true
394
- def fill_formatted_text_box text, opts
395
- merge_text_box_positioning_options opts
396
- box = ::Prawn::Text::Formatted::Box.new text, opts
416
+ # NOTE: override built-in fill_formatted_text_box to insert leading before second line when :first_line is true
417
+ def fill_formatted_text_box text, options
418
+ merge_text_box_positioning_options options
419
+ box = ::Prawn::Text::Formatted::Box.new text, options
397
420
  remaining_text = box.render
398
421
  @no_text_printed = box.nothing_printed?
399
422
  @all_text_printed = box.everything_printed?
400
423
 
401
- if @final_gap || (opts[:first_line] && !(@no_text_printed || @all_text_printed))
424
+ if ((defined? @final_gap) && @final_gap) || (options[:first_line] && !(@no_text_printed || @all_text_printed))
402
425
  self.y -= box.height + box.line_gap + box.leading
403
426
  else
404
427
  self.y -= box.height
@@ -407,51 +430,52 @@ module Asciidoctor
407
430
  remaining_text
408
431
  end
409
432
 
410
- # NOTE override built-in draw_indented_formatted_line to set first_line flag
411
- def draw_indented_formatted_line string, opts
412
- super string, (opts.merge first_line: true)
433
+ # NOTE: override built-in draw_indented_formatted_line to set first_line flag
434
+ def draw_indented_formatted_line string, options
435
+ super string, (options.merge first_line: true)
413
436
  end
414
437
 
415
- # Performs the same work as Prawn::Text.text except that the first_line_opts are applied to the first line of text
438
+ # Performs the same work as Prawn::Text.text except that the first_line_options are applied to the first line of text
416
439
  # renderered. It's necessary to use low-level APIs in this method so we only style the first line and not the
417
440
  # remaining lines (which is the default behavior in Prawn).
418
- def text_with_formatted_first_line string, first_line_opts, opts
419
- color = opts.delete :color
420
- fragments = parse_text string, opts
421
- # NOTE the low-level APIs we're using don't recognize the :styles option, so we must resolve
422
- if (styles = opts.delete :styles)
423
- opts[:style] = resolve_font_style styles
424
- end
425
- if (first_line_styles = first_line_opts.delete :styles)
426
- first_line_opts[:style] = resolve_font_style first_line_styles
427
- end
428
- first_line_color = (first_line_opts.delete :color) || color
429
- opts = opts.merge document: self
430
- # QUESTION should we merge more carefully here? (hand-select keys?)
431
- first_line_opts = opts.merge(first_line_opts).merge single_line: true, first_line: true
432
- box = ::Prawn::Text::Formatted::Box.new fragments, first_line_opts
433
- # NOTE get remaining_fragments before we add color to fragments on first line
434
- if (text_indent = opts.delete :indent_paragraphs)
441
+ def text_with_formatted_first_line string, first_line_options, options
442
+ color = options.delete :color
443
+ fragments = parse_text string, options
444
+ # 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
446
+ #if (styles = options.delete :styles)
447
+ # options[:style] = resolve_font_style styles
448
+ #end
449
+ if (first_line_styles = first_line_options.delete :styles)
450
+ first_line_options[:style] = resolve_font_style first_line_styles
451
+ end
452
+ first_line_color = (first_line_options.delete :color) || color
453
+ options = options.merge document: self
454
+ # QUESTION: should we merge more carefully here? (hand-select keys?)
455
+ first_line_options = (options.merge first_line_options).merge single_line: true, first_line: true
456
+ 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)
435
459
  remaining_fragments = indent text_indent do
436
460
  box.render dry_run: true
437
461
  end
438
462
  else
439
463
  remaining_fragments = box.render dry_run: true
440
464
  end
441
- # NOTE color must be applied per-fragment
442
- fragments.each {|fragment| fragment[:color] ||= first_line_color } if first_line_color
465
+ # NOTE: color must be applied per-fragment
466
+ fragments.each {|fragment| fragment[:color] ||= first_line_color }
443
467
  if text_indent
444
468
  indent text_indent do
445
- fill_formatted_text_box fragments, first_line_opts
469
+ fill_formatted_text_box fragments, first_line_options
446
470
  end
447
471
  else
448
- fill_formatted_text_box fragments, first_line_opts
472
+ fill_formatted_text_box fragments, first_line_options
449
473
  end
450
474
  unless remaining_fragments.empty?
451
- # NOTE color must be applied per-fragment
452
- remaining_fragments.each {|fragment| fragment[:color] ||= color } if color
453
- remaining_fragments = fill_formatted_text_box remaining_fragments, opts
454
- draw_remaining_formatted_text_on_new_pages remaining_fragments, opts
475
+ # NOTE: color must be applied per-fragment
476
+ remaining_fragments.each {|fragment| fragment[:color] ||= color }
477
+ remaining_fragments = fill_formatted_text_box remaining_fragments, options
478
+ draw_remaining_formatted_text_on_new_pages remaining_fragments, options
455
479
  end
456
480
  end
457
481
 
@@ -492,7 +516,7 @@ module Asciidoctor
492
516
  # Override built-in move_text_position method to prevent Prawn from advancing
493
517
  # to next page if image doesn't fit before rendering image.
494
518
  #--
495
- # NOTE could use :at option when calling image/embed_image instead
519
+ # NOTE: could use :at option when calling image/embed_image instead
496
520
  def move_text_position h; end
497
521
 
498
522
  # Short-circuits the call to the built-in move_down operation
@@ -504,21 +528,7 @@ module Asciidoctor
504
528
 
505
529
  # Bounds
506
530
 
507
- # Overrides the built-in pad operation to allow for asymmetric paddings.
508
- #
509
- # Example:
510
- #
511
- # pad 20, 10 do
512
- # text 'A paragraph with twice as much top padding as bottom padding.'
513
- # end
514
- #
515
- def pad top, bottom = nil
516
- move_down top
517
- yield
518
- move_down(bottom || top)
519
- end
520
-
521
- # Combines the built-in pad and indent operations into a single method.
531
+ # Augments the built-in pad method by adding support for specifying padding on all four sizes.
522
532
  #
523
533
  # Padding may be specified as an array of four values, or as a single value.
524
534
  # The single value is used as the padding around all four sides of the box.
@@ -535,55 +545,102 @@ module Asciidoctor
535
545
  # text 'An indented paragraph inside a box with equal padding on all sides.'
536
546
  # end
537
547
  #
538
- def pad_box padding
548
+ def pad_box padding, node = nil
539
549
  if padding
540
550
  # TODO: implement shorthand combinations like in CSS
541
551
  p_top, p_right, p_bottom, p_left = ::Array === padding ? padding : (::Array.new 4, padding)
552
+ # logic is intentionally inlined
542
553
  begin
543
- # logic is intentionally inlined
554
+ if node && ((last_block = node).content_model != :compound || (last_block = node.blocks[-1])&.context == :paragraph)
555
+ @bottom_gutters << { last_block => p_bottom }
556
+ else
557
+ @bottom_gutters << {}
558
+ end
544
559
  move_down p_top
545
560
  bounds.add_left_padding p_left
546
561
  bounds.add_right_padding p_right
547
562
  yield
548
- # NOTE support negative bottom padding for use with quote block
549
- if p_bottom < 0
550
- # QUESTION should we return to previous page if top of page is reached?
551
- p_bottom < cursor - reference_bounds.top ? (move_cursor_to reference_bounds.top) : (move_down p_bottom)
552
- else
553
- p_bottom < cursor ? (move_down p_bottom) : reference_bounds.move_past_bottom
554
- end
563
+ cursor > p_bottom ? (move_down p_bottom) : reference_bounds.move_past_bottom unless at_page_top?
555
564
  ensure
565
+ @bottom_gutters.pop
556
566
  bounds.subtract_left_padding p_left
557
567
  bounds.subtract_right_padding p_right
558
568
  end
559
569
  else
560
570
  yield
561
571
  end
562
-
563
- # alternate, delegated logic
564
- #pad padding[0], padding[2] do
565
- # indent padding[1], padding[3] do
566
- # yield
567
- # end
568
- #end
569
572
  end
570
573
 
571
- def inflate_indent value
574
+ def expand_indent_value value
572
575
  (::Array === value ? (value.slice 0, 2) : (::Array.new 2, value)).map(&:to_f)
573
576
  end
574
577
 
575
- # TODO: memoize the result
576
- def inflate_padding padding
577
- padding = (Array padding || 0).slice 0, 4
578
- case padding.size
579
- when 1
580
- [padding[0], padding[0], padding[0], padding[0]]
581
- when 2
582
- [padding[0], padding[1], padding[0], padding[1]]
583
- when 3
584
- [padding[0], padding[1], padding[2], padding[1]]
578
+ def expand_padding_value shorthand
579
+ unless (padding = (@side_area_shorthand_cache ||= {})[shorthand])
580
+ if ::Array === shorthand
581
+ case shorthand.size
582
+ when 1
583
+ padding = [shorthand[0], shorthand[0], shorthand[0], shorthand[0]]
584
+ when 2
585
+ padding = [shorthand[0], shorthand[1], shorthand[0], shorthand[1]]
586
+ when 3
587
+ padding = [shorthand[0], shorthand[1], shorthand[2], shorthand[1]]
588
+ when 4
589
+ padding = shorthand
590
+ else
591
+ padding = shorthand.slice 0, 4
592
+ end
593
+ else
594
+ padding = ::Array.new 4, (shorthand || 0)
595
+ end
596
+ @side_area_shorthand_cache[shorthand] = padding
597
+ end
598
+ padding.dup
599
+ end
600
+
601
+ alias expand_margin_value expand_padding_value
602
+
603
+ def expand_grid_values shorthand, default = nil
604
+ if ::Array === shorthand
605
+ case shorthand.size
606
+ when 1
607
+ [(value0 = shorthand[0] || default), value0]
608
+ when 2
609
+ shorthand.map {|it| it || default }
610
+ when 4
611
+ if Asciidoctor::PDF::ThemeLoader::CMYKColorValue === shorthand
612
+ [shorthand, shorthand]
613
+ else
614
+ (shorthand.slice 0, 2).map {|it| it || default }
615
+ end
616
+ else
617
+ (shorthand.slice 0, 2).map {|it| it || default }
618
+ end
585
619
  else
586
- padding
620
+ [(value0 = shorthand || default), value0]
621
+ end
622
+ end
623
+
624
+ def expand_rect_values shorthand, default = nil
625
+ if ::Array === shorthand
626
+ case shorthand.size
627
+ when 1
628
+ [(value0 = shorthand[0] || default), value0, value0, value0]
629
+ when 2
630
+ [(value0 = shorthand[0] || default), (value1 = shorthand[1] || default), value0, value1]
631
+ when 3
632
+ [shorthand[0] || default, (value1 = shorthand[1] || default), shorthand[2] || default, value1]
633
+ when 4
634
+ if Asciidoctor::PDF::ThemeLoader::CMYKColorValue === shorthand
635
+ [shorthand, shorthand, shorthand, shorthand]
636
+ else
637
+ shorthand.map {|it| it || default }
638
+ end
639
+ else
640
+ (shorthand.slice 0, 4).map {|it| it || default }
641
+ end
642
+ else
643
+ [(value0 = shorthand || default), value0, value0, value0]
587
644
  end
588
645
  end
589
646
 
@@ -601,16 +658,15 @@ module Asciidoctor
601
658
  end
602
659
  end
603
660
 
604
- # A flowing version of the bounding_box. If the content runs to another page, the cursor starts
605
- # at the top of the page instead of the original cursor position. Similar to span, except
606
- # you can specify an absolute left position and pass additional options through to bounding_box.
661
+ # A flowing version of bounding_box. If the content runs to another page, the cursor starts at
662
+ # the top of the page instead of from the original cursor position. Similar to span, except
663
+ # the :position option is limited to a numeric value and additional options are passed through
664
+ # to bounding_box.
607
665
  #
608
- def flow_bounding_box left = 0, opts = {}
609
- original_y = y
610
- # QUESTION should preserving original_x be an option?
611
- original_x = bounds.absolute_left - margin_box.absolute_left
666
+ def flow_bounding_box options = {}
667
+ original_y, original_x = y, bounds.absolute_left
612
668
  canvas do
613
- bounding_box [margin_box.absolute_left + original_x + left, margin_box.absolute_top], opts do
669
+ bounding_box [original_x + (options.delete :position).to_f, @margin_box.absolute_top], options do
614
670
  self.y = original_y
615
671
  yield
616
672
  end
@@ -622,19 +678,17 @@ module Asciidoctor
622
678
  # Fills the current bounding box with the specified fill color. Before
623
679
  # returning from this method, the original fill color on the document is
624
680
  # restored.
625
- def fill_bounds f_color = fill_color
626
- if f_color && f_color != 'transparent'
627
- prev_fill_color = fill_color
628
- fill_color f_color
629
- fill_rectangle bounds.top_left, bounds.width, bounds.height
630
- fill_color prev_fill_color
631
- end
681
+ def fill_bounds f_color
682
+ prev_fill_color = fill_color
683
+ fill_color f_color
684
+ fill_rectangle bounds.top_left, bounds.width, bounds.height
685
+ fill_color prev_fill_color
632
686
  end
633
687
 
634
688
  # Fills the absolute bounding box with the specified fill color. Before
635
689
  # returning from this method, the original fill color on the document is
636
690
  # restored.
637
- def fill_absolute_bounds f_color = fill_color
691
+ def fill_absolute_bounds f_color
638
692
  canvas { fill_bounds f_color }
639
693
  end
640
694
 
@@ -645,53 +699,58 @@ module Asciidoctor
645
699
  #
646
700
  def fill_and_stroke_bounds f_color = fill_color, s_color = stroke_color, options = {}
647
701
  no_fill = !f_color || f_color == 'transparent'
648
- no_stroke = !s_color || s_color == 'transparent' || options[:line_width] == 0
702
+ if ::Array === (s_width = options[:line_width] || 0)
703
+ s_width_max = s_width.map(&:to_i).max
704
+ radius = 0
705
+ else
706
+ radius = options[:radius] || 0
707
+ end
708
+ no_stroke = !s_color || s_color == 'transparent' || (s_width_max || s_width) == 0
649
709
  return if no_fill && no_stroke
650
710
  save_graphics_state do
651
- radius = options[:radius] || 0
652
-
653
711
  # fill
654
712
  unless no_fill
655
713
  fill_color f_color
656
714
  fill_rounded_rectangle bounds.top_left, bounds.width, bounds.height, radius
657
715
  end
658
716
 
717
+ next if no_stroke
718
+
659
719
  # stroke
660
- unless no_stroke
720
+ 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
724
+ 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
728
+ end
729
+ else
661
730
  stroke_color s_color
662
- line_width(options[:line_width] || 0.5)
663
- # FIXME: think about best way to indicate dashed borders
664
- #if options.has_key? :dash_width
665
- # dash options[:dash_width], space: options[:dash_space] || 1
666
- #end
731
+ case options[:line_style]
732
+ when :dashed
733
+ line_width s_width
734
+ dash s_width * 4
735
+ when :dotted
736
+ line_width s_width
737
+ dash s_width
738
+ when :double
739
+ single_line_width = s_width / 3.0
740
+ line_width single_line_width
741
+ inner_line_offset = single_line_width * 2
742
+ inner_top_left = [bounds.left + inner_line_offset, bounds.top - inner_line_offset]
743
+ stroke_rounded_rectangle bounds.top_left, bounds.width, bounds.height, radius
744
+ stroke_rounded_rectangle inner_top_left, bounds.width - (inner_line_offset * 2), bounds.height - (inner_line_offset * 2), radius
745
+ next
746
+ else # :solid
747
+ line_width s_width
748
+ end
667
749
  stroke_rounded_rectangle bounds.top_left, bounds.width, bounds.height, radius
668
- #undash if options.has_key? :dash_width
669
750
  end
670
751
  end
671
752
  end
672
753
 
673
- # Fills and, optionally, strokes the current bounds using the fill and
674
- # stroke color specified, then yields to the block. The only_if option can
675
- # be used to conditionally disable this behavior.
676
- #
677
- def shade_box color, line_color = nil, options = {}
678
- if (!options.key? :only_if) || options[:only_if]
679
- # FIXME: could use save_graphics_state here
680
- previous_fill_color = current_fill_color
681
- fill_color color
682
- fill_rectangle [bounds.left, bounds.top], bounds.right, bounds.top - bounds.bottom
683
- fill_color previous_fill_color
684
- if line_color
685
- line_width 0.5
686
- previous_stroke_color = current_stroke_color
687
- stroke_color line_color
688
- stroke_bounds
689
- stroke_color previous_stroke_color
690
- end
691
- end
692
- yield
693
- end
694
-
695
754
  # Strokes a horizontal line using the current bounds. The width of the line
696
755
  # can be specified using the line_width option. The offset from the cursor
697
756
  # can be set using the at option.
@@ -702,21 +761,25 @@ module Asciidoctor
702
761
  rule_width = options[:line_width] || 0.5
703
762
  rule_x_start = bounds.left
704
763
  rule_x_end = bounds.right
705
- rule_inked = false
706
764
  save_graphics_state do
707
- line_width rule_width
708
765
  stroke_color rule_color
709
766
  case rule_style
710
767
  when :dashed
768
+ line_width rule_width
711
769
  dash rule_width * 4
712
770
  when :dotted
771
+ line_width rule_width
713
772
  dash rule_width
714
773
  when :double
715
- stroke_horizontal_line rule_x_start, rule_x_end, at: (rule_y + rule_width)
716
- stroke_horizontal_line rule_x_start, rule_x_end, at: (rule_y - rule_width)
717
- rule_inked = true
718
- end if rule_style
719
- stroke_horizontal_line rule_x_start, rule_x_end, at: rule_y unless rule_inked
774
+ single_rule_width = rule_width / 3.0
775
+ line_width single_rule_width
776
+ stroke_horizontal_line rule_x_start, rule_x_end, at: (rule_y + single_rule_width)
777
+ stroke_horizontal_line rule_x_start, rule_x_end, at: (rule_y - single_rule_width)
778
+ next
779
+ else # :solid
780
+ line_width rule_width
781
+ end
782
+ stroke_horizontal_line rule_x_start, rule_x_end, at: rule_y
720
783
  end
721
784
  end
722
785
 
@@ -754,14 +817,14 @@ module Asciidoctor
754
817
  def delete_page
755
818
  pg = page_number
756
819
  pdf_store = state.store
757
- pdf_objs = pdf_store.instance_variable_get :@objects
758
- pdf_ids = pdf_store.instance_variable_get :@identifiers
759
- page_id = pdf_store.object_id_for_page pg
760
820
  content_id = page.content.identifier
761
- [page_id, content_id].each do |key|
762
- pdf_objs.delete key
763
- pdf_ids.delete key
764
- end
821
+ page_ref = page.dictionary
822
+ (prune_dests = proc do |node|
823
+ node.children.delete_if {|it| ::PDF::Core::NameTree::Node === it ? prune_dests[it] : it.value.data[0] == page_ref }
824
+ false
825
+ end)[dests.data]
826
+ # NOTE: cannot delete objects and IDs, otherwise references get corrupted; so just reset the value
827
+ (pdf_store.instance_variable_get :@objects)[content_id] = ::PDF::Core::Reference.new content_id, {}
765
828
  pdf_store.pages.data[:Kids].pop
766
829
  pdf_store.pages.data[:Count] -= 1
767
830
  state.pages.pop
@@ -780,60 +843,63 @@ module Asciidoctor
780
843
  # However, due to how page creation works in Prawn, understand that advancing
781
844
  # to the next page is necessary to prevent the size & layout of the imported
782
845
  # page from affecting a newly created page.
783
- def import_page file, opts = {}
846
+ def import_page file, options = {}
784
847
  prev_page_layout = page.layout
785
848
  prev_page_size = page.size
786
849
  state.compress = false if state.compress # can't use compression if using template
787
850
  prev_text_rendering_mode = (defined? @text_rendering_mode) ? @text_rendering_mode : nil
788
- delete_page if opts[:replace]
789
- # NOTE use functionality provided by prawn-templates
790
- start_new_page_discretely template: file, template_page: opts[:page]
851
+ delete_page if options[:replace]
852
+ # NOTE: use functionality provided by prawn-templates
853
+ start_new_page_discretely template: file, template_page: options[:page]
791
854
  # prawn-templates sets text_rendering_mode to :unknown, which breaks running content; revert
792
855
  @text_rendering_mode = prev_text_rendering_mode
793
- yield if block_given?
794
- if opts.fetch :advance, true
795
- # NOTE set page size & layout explicitly in case imported page differs
856
+ if page.imported_page?
857
+ yield if block_given?
858
+ # NOTE: set page size & layout explicitly in case imported page differs
796
859
  # I'm not sure it's right to start a new page here, but unfortunately there's no other
797
860
  # way atm to prevent the size & layout of the imported page from affecting subsequent pages
861
+ advance_page size: prev_page_size, layout: prev_page_layout if options.fetch :advance, true
862
+ elsif options.fetch :advance_if_missing, true
863
+ delete_page
864
+ # NOTE: see previous comment
798
865
  advance_page size: prev_page_size, layout: prev_page_layout
866
+ else
867
+ delete_page
799
868
  end
800
869
  nil
801
870
  end
802
871
 
803
- # Create a new page for the specified image. If the canvas option is true,
804
- # the image is positioned relative to the boundaries of the page.
872
+ # Create a new page for the specified image.
873
+ #
874
+ # The image is positioned relative to the boundaries of the page.
805
875
  def image_page file, options = {}
806
876
  start_new_page_discretely
807
- image_page_number = page_number
808
- if options.delete :canvas
809
- canvas { image file, ({ position: :center, vposition: :center }.merge options) }
810
- else
811
- image file, (options.merge position: :center, vposition: :center, fit: [bounds.width, bounds.height])
877
+ ex = nil
878
+ float do
879
+ canvas do
880
+ image file, ({ position: :center, vposition: :center }.merge options)
881
+ rescue
882
+ ex = $!
883
+ end
812
884
  end
813
- # NOTE advance to newly created page just in case the image function threw off the cursor
814
- go_to_page image_page_number
885
+ raise ex if ex
815
886
  nil
816
887
  end
817
888
 
818
889
  # Perform an operation (such as creating a new page) without triggering the on_page_create callback
819
890
  #
820
891
  def perform_discretely
821
- if (saved_callback = state.on_page_create_callback)
822
- # equivalent to calling `on_page_create`
823
- state.on_page_create_callback = nil
824
- yield
825
- # equivalent to calling `on_page_create &saved_callback`
826
- state.on_page_create_callback = saved_callback
827
- else
828
- yield
829
- end
892
+ state.on_page_create_callback = nil if (saved_callback = state.on_page_create_callback) != InhibitNewPageProc
893
+ yield
894
+ ensure
895
+ state.on_page_create_callback = saved_callback
830
896
  end
831
897
 
832
898
  # This method is a smarter version of start_new_page. It calls start_new_page
833
899
  # if the current page is the last page of the document. Otherwise, it simply
834
900
  # advances to the next existing page.
835
- def advance_page opts = {}
836
- last_page? ? (start_new_page opts) : (go_to_page page_number + 1)
901
+ def advance_page options = {}
902
+ last_page? ? (start_new_page options) : (go_to_page page_number + 1)
837
903
  end
838
904
 
839
905
  # Start a new page without triggering the on_page_create callback
@@ -844,115 +910,207 @@ module Asciidoctor
844
910
 
845
911
  # Grouping
846
912
 
847
- # Conditional group operation
848
- #
849
- def group_if verdict
850
- if verdict
851
- state.optimize_objects = false # optimize_objects breaks group
852
- group { yield }
853
- else
854
- yield
855
- end
913
+ def allocate_prototype
914
+ @prototype = create_prototype { ::Marshal.load ::Marshal.dump self }
856
915
  end
857
916
 
858
- def get_scratch_document
859
- # marshal if not using transaction feature
860
- #Marshal.load Marshal.dump @prototype
861
-
862
- # use cached instance, tests show it's faster
863
- #@prototype ||= ::Prawn::Document.new
864
- @scratch ||= if defined? @prototype # rubocop:disable Naming/MemoizedInstanceVariableName
865
- scratch = Marshal.load Marshal.dump @prototype
866
- scratch.instance_variable_set :@prototype, @prototype
867
- scratch.instance_variable_set :@tmp_files, @tmp_files
868
- # TODO: set scratch number on scratch document
869
- scratch
870
- else
871
- logger.warn 'no scratch prototype available; instantiating fresh scratch document'
872
- ::Prawn::Document.new
873
- end
917
+ def scratch
918
+ @scratch ||= ((Marshal.load Marshal.dump @prototype).send :init_scratch, self)
874
919
  end
875
920
 
876
921
  def scratch?
877
- (@_label ||= (state.store.info.data[:Scratch] ? :scratch : :primary)) == :scratch
878
- rescue
879
- false # NOTE this method may get called before the state is initialized
880
- end
881
- alias is_scratch? scratch?
882
-
883
- def dry_run &block
884
- scratch = get_scratch_document
885
- # QUESTION should we use scratch.advance_page instead?
886
- scratch.start_new_page
887
- start_page_number = scratch.page_number
888
- start_y = scratch.y
889
- scratch_bounds = scratch.bounds
890
- original_x = scratch_bounds.absolute_left
891
- original_width = scratch_bounds.width
892
- scratch_bounds.instance_variable_set :@x, bounds.absolute_left
893
- scratch_bounds.instance_variable_set :@width, bounds.width
894
- scratch.font font_family, style: font_style, size: font_size do
895
- scratch.instance_exec(&block)
896
- end
897
- # NOTE don't count excess if cursor exceeds writable area (due to padding)
898
- full_page_height = scratch.effective_page_height
899
- partial_page_height = [full_page_height, start_y - scratch.y].min
900
- scratch_bounds.instance_variable_set :@x, original_x
901
- scratch_bounds.instance_variable_set :@width, original_width
902
- whole_pages = scratch.page_number - start_page_number
903
- [(whole_pages * full_page_height + partial_page_height), whole_pages, partial_page_height]
922
+ @label == :scratch
904
923
  end
905
924
 
906
925
  def with_dry_run &block
907
- total_height, = dry_run(&block)
908
- instance_exec total_height, &block
926
+ yield dry_run(&block).position_onto self, cursor
909
927
  end
910
928
 
911
- # Attempt to keep the objects generated in the block on the same page
929
+ # Yields to the specified block multiple times, first to determine where to render the content
930
+ # so it fits properly, then once more, this time providing access to the content's extent, to
931
+ # ink the content in the primary document.
912
932
  #
913
- # TODO: short-circuit nested usage
914
- def keep_together &block
915
- available_space = cursor
916
- total_height, = dry_run(&block)
917
- # NOTE technically, if we're at the page top, we don't even need to do the
918
- # dry run, except several uses of this method rely on the calculated height
919
- if total_height > available_space && !at_page_top? && total_height <= effective_page_height
920
- advance_page
921
- started_new_page = true
922
- else
923
- started_new_page = false
933
+ # This method yields to the specified block in a scratch document by calling dry_run to
934
+ # determine where the content should start in the primary document. In the process, it also
935
+ # computes the extent of the content. It then returns to the primary document and yields to
936
+ # the block again, this time passing in the extent of the content. The extent can be used to
937
+ # draw a border and/or background under the content before inking it.
938
+ #
939
+ # This method is intended to enclose the conversion of a single content block, such as a
940
+ # sidebar or example block. The arrange logic attempts to keep unbreakable content on the same
941
+ # page, keeps the top caption pinned to the top of the content, computes the extent of the
942
+ # content for the purpose of drawing a border and/or background underneath it, and ensures
943
+ # that the extent does not begin near the bottom of a page if the first line of content
944
+ # doesn't fit. If unbreakable content does not fit on a single page, the content is treated as
945
+ # breakable.
946
+ #
947
+ # The block passed to this method should use advance_page to move to the next page rather than
948
+ # start_new_page. Using start_new_page can mangle the calculation of content block's extent.
949
+ #
950
+ def arrange_block node, &block
951
+ keep_together = (node.option? 'unbreakable') && !at_page_top?
952
+ doc = node.document
953
+ block_for_scratch = proc do
954
+ push_scratch doc
955
+ instance_exec(&block)
956
+ ensure
957
+ pop_scratch doc
924
958
  end
959
+ extent = dry_run keep_together: keep_together, onto: [self, keep_together], &block_for_scratch
960
+ scratch? ? block_for_scratch.call : (yield extent)
961
+ end
925
962
 
926
- # HACK: yield doesn't work here on JRuby (at least not when called from AsciidoctorJ)
927
- #yield remainder, started_new_page
928
- instance_exec(total_height, started_new_page, &block)
963
+ # This method installs an on_page_create_callback that stops processing if the first page is
964
+ # exceeded while yielding to the specified block. If the content fits on a single page, the
965
+ # processing is not stopped. The purpose of this method is to determine if the content fits on
966
+ # a single page.
967
+ #
968
+ # Returns a Boolean indicating whether the content fits on a single page.
969
+ def perform_on_single_page
970
+ saved_callback, state.on_page_create_callback = state.on_page_create_callback, InhibitNewPageProc
971
+ yield
972
+ false
973
+ rescue NewPageRequiredError
974
+ true
975
+ ensure
976
+ state.on_page_create_callback = saved_callback
929
977
  end
930
978
 
931
- # Attempt to keep the objects generated in the block on the same page
932
- # if the verdict parameter is true.
979
+ # This method installs an on_page_create_callback that stops processing if a new page is
980
+ # created without writing content to the first page while yielding to the specified block. If
981
+ # any content is written to the first page, processing is not stopped. The purpose of this
982
+ # method is to check whether any content fits on the remaining space on the current page.
933
983
  #
934
- def keep_together_if verdict, &block
935
- if verdict
936
- keep_together(&block)
937
- else
984
+ # Returns a Boolean indicating whether any content is written on the first page.
985
+ def stop_if_first_page_empty
986
+ delegate = state.on_page_create_callback
987
+ state.on_page_create_callback = DetectEmptyFirstPageProc.curry[delegate].extend DetectEmptyFirstPage
988
+ yield
989
+ false
990
+ rescue NewPageRequiredError
991
+ true
992
+ ensure
993
+ state.on_page_create_callback = delegate
994
+ end
995
+
996
+ # NOTE: only used in dry_run since that's when DetectEmptyFirstPage is active
997
+ def tare_first_page_content_stream
998
+ return yield unless DetectEmptyFirstPage === (delegate = state.on_page_create_callback)
999
+ on_page_create_called = nil
1000
+ state.on_page_create_callback = proc do |pdf|
1001
+ on_page_create_called = true
1002
+ pdf.state.pages[pdf.page_number - 2].tare_content_stream
1003
+ delegate.call pdf
1004
+ end
1005
+ begin
938
1006
  yield
1007
+ ensure
1008
+ page.tare_content_stream unless on_page_create_called
1009
+ state.on_page_create_callback = delegate
939
1010
  end
940
1011
  end
941
1012
 
942
- =begin
943
- def run_with_trial &block
944
- available_space = cursor
945
- total_height, whole_pages, remainder = dry_run(&block)
946
- if whole_pages > 0 || remainder > available_space
947
- started_new_page = true
948
- else
949
- started_new_page = false
1013
+ # Yields to the specified block within the context of a scratch document up to three times to
1014
+ # acertain the extent of the content block.
1015
+ #
1016
+ # The purpose of this method is two-fold. First, it works out the position where the rendered
1017
+ # content should start in the calling document. Then, it precomputes the extent of the content
1018
+ # starting from that position.
1019
+ #
1020
+ # This method returns the content's extent (the range from the start page and cursor to the
1021
+ # end page and cursor) as a ScratchExtent object or, if the onto keyword parameter is
1022
+ # specified, an Extent object. A ScratchExtent always starts the page range at 1. When the
1023
+ # ScratchExtent is positioned onto the primary document using ScratchExtent#position_onto,
1024
+ # that's when the cursor may be advanced to the next page.
1025
+ #
1026
+ # This method performs all work in a scratch document (or documents). It begins by starting a
1027
+ # new page in the scratch document, first creating the scratch document if necessary. It then
1028
+ # applies all the settings from the main document to the scratch document that impact
1029
+ # rendering. This includes the bounds, the cursor position, and the font settings.
1030
+ #
1031
+ # From this point, the number of attempts the method makes is determined by the value of the
1032
+ # keep_together keyword parameter. If the value is true (or the parent document is inhibiting
1033
+ # page creation), it starts from the top of the page, yields to the block, and tries to fit
1034
+ # the content on the current page. If the content fits, it computes and returns the
1035
+ # ScratchExtent (or Extent). If the content does not fit, it first checks if this scenario
1036
+ # should stop the operation. If the parent document is inhibiting page creation, it bubbles
1037
+ # the error. If the single_page keyword argument is :enforce, it raises a CannotFit error. If
1038
+ # the single_page keyword argument is true, it returns a ScratchExtent (or Extent) that
1039
+ # represents a full page. If none of those conditions are met, it restarts with the
1040
+ # keep_together parameter unset.
1041
+ #
1042
+ # If the keep_together parameter is not true, the method tries to render the content in the
1043
+ # scratch document from the location of the cursor in the main document. If the cursor is at
1044
+ # the top of the page, no special conditions are applied (this is the last attempt). The
1045
+ # content is rendered and the extent is computed based on where the content ended up (minus a
1046
+ # trailing empty page). If the cursor is not at the top of the page, the method renders the
1047
+ # content while listening for a page creation event before any content is written. If a new
1048
+ # page is created, and no content is written on the first page, the method restarts with the
1049
+ # cursor at the top of the page.
1050
+ #
1051
+ # Note that if the block has content that itself requires a dry run, that nested dry run will
1052
+ # be performed in a separate scratch document.
1053
+ #
1054
+ # opts - A Hash of options that configure the dry run computation:
1055
+ # :keep_together - A Boolean indicating whether an attempt should be made to keep the
1056
+ # content on the same page (optional, default: false).
1057
+ # :single_page - A Boolean indicating whether the operation should stop if the content
1058
+ # exceeds the height of a single page.
1059
+ # :onto - The document onto which to position the scratch extent. If this option is
1060
+ # set, the method returns an Extent instead of a ScratchExtent (optional, default: nil)
1061
+ # :pages_advanced - The number of pages the content has been advanced during this
1062
+ # operation (internal only) (optional, default: 0)
1063
+ #
1064
+ # Returns an Extent or ScratchExtent object that describes the bounds of the content block.
1065
+ def dry_run keep_together: nil, pages_advanced: 0, single_page: nil, onto: nil, &block
1066
+ (scratch_pdf = scratch).start_new_page
1067
+ scratch_bounds = scratch_pdf.bounds
1068
+ restore_bounds = [:@total_left_padding, :@total_right_padding, :@width, :@x].each_with_object({}) do |name, accum|
1069
+ accum[name] = scratch_bounds.instance_variable_get name
1070
+ scratch_bounds.instance_variable_set name, (bounds.instance_variable_get name)
1071
+ end
1072
+ scratch_pdf.move_cursor_to cursor unless (scratch_start_at_top = keep_together || pages_advanced > 0 || at_page_top?)
1073
+ scratch_start_cursor = scratch_pdf.cursor
1074
+ scratch_start_page = scratch_pdf.page_number
1075
+ inhibit_new_page = state.on_page_create_callback == InhibitNewPageProc
1076
+ restart = nil
1077
+ scratch_pdf.font font_family, size: font_size, style: font_style do
1078
+ prev_font_scale, scratch_pdf.font_scale = scratch_pdf.font_scale, font_scale
1079
+ if keep_together || inhibit_new_page
1080
+ if (restart = scratch_pdf.perform_on_single_page { scratch_pdf.instance_exec(&block) })
1081
+ # NOTE: propogate NewPageRequiredError from nested block, which is rendered in separate scratch document
1082
+ raise NewPageRequiredError if inhibit_new_page
1083
+ if single_page
1084
+ raise ::Prawn::Errors::CannotFit if single_page == :enforce
1085
+ # single_page and onto are mutually exclusive
1086
+ return ScratchExtent.new scratch_start_page, scratch_start_cursor, scratch_start_page, 0
1087
+ end
1088
+ end
1089
+ elsif scratch_start_at_top
1090
+ scratch_pdf.instance_exec(&block)
1091
+ elsif (restart = scratch_pdf.stop_if_first_page_empty { scratch_pdf.instance_exec(&block) })
1092
+ pages_advanced += 1
1093
+ end
1094
+ ensure
1095
+ scratch_pdf.font_scale = prev_font_scale
1096
+ end
1097
+ return dry_run pages_advanced: pages_advanced, onto: onto, &block if restart
1098
+ scratch_end_page = scratch_pdf.page_number - scratch_start_page + (scratch_start_page = 1)
1099
+ if pages_advanced > 0
1100
+ scratch_start_page += pages_advanced
1101
+ scratch_end_page += pages_advanced
1102
+ end
1103
+ scratch_end_cursor = scratch_pdf.cursor
1104
+ # NOTE: drop trailing blank page and move cursor to end of previous page
1105
+ if scratch_end_page > scratch_start_page && scratch_pdf.at_page_top?
1106
+ scratch_end_page -= 1
1107
+ scratch_end_cursor = 0
950
1108
  end
951
- # HACK yield doesn't work here on JRuby (at least not when called from AsciidoctorJ)
952
- #yield remainder, started_new_page
953
- instance_exec(remainder, started_new_page, &block)
1109
+ extent = ScratchExtent.new scratch_start_page, scratch_start_cursor, scratch_end_page, scratch_end_cursor
1110
+ onto ? extent.position_onto(*onto) : extent
1111
+ ensure
1112
+ restore_bounds.each {|name, val| scratch_bounds.instance_variable_set name, val }
954
1113
  end
955
- =end
956
1114
  end
957
1115
  end
958
1116
  end