asciidoctor-pdf 1.6.2 → 2.0.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/CHANGELOG.adoc +224 -30
  4. data/NOTICE.adoc +16 -4
  5. data/README.adoc +207 -67
  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 +18 -22
  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 +49 -54
  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 +967 -344
  33. data/lib/asciidoctor/pdf/converter.rb +1691 -1478
  34. data/lib/asciidoctor/pdf/ext/asciidoctor/document.rb +18 -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 -18
  45. data/lib/asciidoctor/pdf/ext/prawn/coderay_encoder.rb +5 -7
  46. data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +433 -329
  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 +20 -14
  51. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/fragment.rb +9 -3
  52. data/lib/asciidoctor/pdf/ext/prawn/images.rb +14 -16
  53. data/lib/asciidoctor/pdf/ext/prawn-svg/loaders/data.rb +6 -0
  54. data/lib/asciidoctor/pdf/ext/prawn-svg/loaders/web.rb +22 -0
  55. data/lib/asciidoctor/pdf/ext/prawn-svg/url_loader.rb +13 -0
  56. data/lib/asciidoctor/pdf/ext/prawn-svg.rb +5 -2
  57. data/lib/asciidoctor/pdf/ext/prawn-table/cell/asciidoc.rb +76 -20
  58. data/lib/asciidoctor/pdf/ext/prawn-table/cell/text.rb +39 -1
  59. data/lib/asciidoctor/pdf/ext/prawn-table/cell.rb +21 -15
  60. data/lib/asciidoctor/pdf/ext/prawn-table.rb +1 -1
  61. data/lib/asciidoctor/pdf/ext/pygments.rb +2 -2
  62. data/lib/asciidoctor/pdf/ext/rouge/formatters/prawn.rb +17 -20
  63. data/lib/asciidoctor/pdf/ext/rouge/themes/asciidoctor_pdf_default.rb +1 -0
  64. data/lib/asciidoctor/pdf/ext/rouge.rb +0 -1
  65. data/lib/asciidoctor/pdf/formatted_text/formatter.rb +2 -2
  66. data/lib/asciidoctor/pdf/formatted_text/inline_destination_marker.rb +8 -10
  67. data/lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb +69 -78
  68. data/lib/asciidoctor/pdf/formatted_text/inline_image_renderer.rb +7 -10
  69. data/lib/asciidoctor/pdf/formatted_text/inline_text_aligner.rb +2 -4
  70. data/lib/asciidoctor/pdf/formatted_text/parser.rb +53 -47
  71. data/lib/asciidoctor/pdf/formatted_text/parser.treetop +5 -7
  72. data/lib/asciidoctor/pdf/formatted_text/source_wrap.rb +14 -14
  73. data/lib/asciidoctor/pdf/formatted_text/text_background_and_border_renderer.rb +4 -7
  74. data/lib/asciidoctor/pdf/formatted_text/transform.rb +116 -109
  75. data/lib/asciidoctor/pdf/formatted_text.rb +0 -1
  76. data/lib/asciidoctor/pdf/index_catalog.rb +7 -11
  77. data/lib/asciidoctor/pdf/optimizer.rb +3 -5
  78. data/lib/asciidoctor/pdf/pdfmark.rb +16 -8
  79. data/lib/asciidoctor/pdf/roman_numeral.rb +4 -22
  80. data/lib/asciidoctor/pdf/sanitizer.rb +18 -13
  81. data/lib/asciidoctor/pdf/section_info_by_page.rb +24 -0
  82. data/lib/asciidoctor/pdf/theme_loader.rb +89 -79
  83. data/lib/asciidoctor/pdf/version.rb +1 -2
  84. data/lib/asciidoctor/pdf.rb +5 -2
  85. metadata +34 -64
  86. data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
  87. data/data/fonts/mplus1mn-bold_italic-ascii.ttf +0 -0
  88. data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
  89. data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
  90. data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_block.rb +0 -7
  91. data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_node.rb +0 -7
  92. data/lib/asciidoctor/pdf/ext/asciidoctor/list_item.rb +0 -18
  93. data/lib/asciidoctor/pdf/ext/asciidoctor/logging_shim.rb +0 -33
  94. data/lib/asciidoctor/pdf/ext/core/array.rb +0 -11
  95. data/lib/asciidoctor/pdf/ext/core/hash.rb +0 -7
  96. data/lib/asciidoctor/pdf/ext/core/regexp.rb +0 -5
  97. data/lib/asciidoctor/pdf/ext/pdf-core/pdf_object.rb +0 -8
  98. data/lib/asciidoctor-pdf/converter.rb +0 -3
  99. 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.
@@ -545,13 +555,7 @@ module Asciidoctor
545
555
  bounds.add_left_padding p_left
546
556
  bounds.add_right_padding p_right
547
557
  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
558
+ cursor > p_bottom ? (move_down p_bottom) : reference_bounds.move_past_bottom unless at_page_top?
555
559
  ensure
556
560
  bounds.subtract_left_padding p_left
557
561
  bounds.subtract_right_padding p_right
@@ -559,34 +563,37 @@ module Asciidoctor
559
563
  else
560
564
  yield
561
565
  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
566
  end
570
567
 
571
- def inflate_indent value
568
+ def expand_indent_value value
572
569
  (::Array === value ? (value.slice 0, 2) : (::Array.new 2, value)).map(&:to_f)
573
570
  end
574
571
 
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]]
585
- else
586
- padding
572
+ def expand_padding_value shorthand
573
+ unless (padding = (@side_area_shorthand_cache ||= {})[shorthand])
574
+ if ::Array === shorthand
575
+ case shorthand.size
576
+ when 1
577
+ padding = [shorthand[0], shorthand[0], shorthand[0], shorthand[0]]
578
+ when 2
579
+ padding = [shorthand[0], shorthand[1], shorthand[0], shorthand[1]]
580
+ when 3
581
+ padding = [shorthand[0], shorthand[1], shorthand[2], shorthand[1]]
582
+ when 4
583
+ padding = shorthand
584
+ else
585
+ padding = shorthand.slice 0, 4
586
+ end
587
+ else
588
+ padding = ::Array.new 4, (shorthand || 0)
589
+ end
590
+ @side_area_shorthand_cache[shorthand] = padding
587
591
  end
592
+ padding.dup
588
593
  end
589
594
 
595
+ alias expand_margin_value expand_padding_value
596
+
590
597
  # Stretch the current bounds to the left and right edges of the current page
591
598
  # while yielding the specified block if the verdict argument is true.
592
599
  # Otherwise, simply yield the specified block.
@@ -601,16 +608,15 @@ module Asciidoctor
601
608
  end
602
609
  end
603
610
 
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.
611
+ # A flowing version of bounding_box. If the content runs to another page, the cursor starts at
612
+ # the top of the page instead of from the original cursor position. Similar to span, except
613
+ # the :position option is limited to a numeric value and additional options are passed through
614
+ # to bounding_box.
607
615
  #
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
616
+ def flow_bounding_box options = {}
617
+ original_y, original_x = y, bounds.absolute_left
612
618
  canvas do
613
- bounding_box [margin_box.absolute_left + original_x + left, margin_box.absolute_top], opts do
619
+ bounding_box [original_x + (options.delete :position).to_f, @margin_box.absolute_top], options do
614
620
  self.y = original_y
615
621
  yield
616
622
  end
@@ -622,19 +628,17 @@ module Asciidoctor
622
628
  # Fills the current bounding box with the specified fill color. Before
623
629
  # returning from this method, the original fill color on the document is
624
630
  # 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
631
+ def fill_bounds f_color
632
+ prev_fill_color = fill_color
633
+ fill_color f_color
634
+ fill_rectangle bounds.top_left, bounds.width, bounds.height
635
+ fill_color prev_fill_color
632
636
  end
633
637
 
634
638
  # Fills the absolute bounding box with the specified fill color. Before
635
639
  # returning from this method, the original fill color on the document is
636
640
  # restored.
637
- def fill_absolute_bounds f_color = fill_color
641
+ def fill_absolute_bounds f_color
638
642
  canvas { fill_bounds f_color }
639
643
  end
640
644
 
@@ -645,53 +649,58 @@ module Asciidoctor
645
649
  #
646
650
  def fill_and_stroke_bounds f_color = fill_color, s_color = stroke_color, options = {}
647
651
  no_fill = !f_color || f_color == 'transparent'
648
- no_stroke = !s_color || s_color == 'transparent' || options[:line_width] == 0
652
+ if ::Array === (s_width = options[:line_width] || 0)
653
+ s_width_max = s_width.map(&:to_i).max
654
+ radius = 0
655
+ else
656
+ radius = options[:radius] || 0
657
+ end
658
+ no_stroke = !s_color || s_color == 'transparent' || (s_width_max || s_width) == 0
649
659
  return if no_fill && no_stroke
650
660
  save_graphics_state do
651
- radius = options[:radius] || 0
652
-
653
661
  # fill
654
662
  unless no_fill
655
663
  fill_color f_color
656
664
  fill_rounded_rectangle bounds.top_left, bounds.width, bounds.height, radius
657
665
  end
658
666
 
667
+ next if no_stroke
668
+
659
669
  # stroke
660
- unless no_stroke
670
+ if s_width_max
671
+ if (s_width_end = s_width[0] || 0) > 0
672
+ stroke_horizontal_rule s_color, line_width: s_width_end, line_style: options[:line_style]
673
+ stroke_horizontal_rule s_color, line_width: s_width_end, line_style: options[:line_style], at: bounds.height
674
+ end
675
+ if (s_width_side = s_width[1] || 0) > 0
676
+ stroke_vertical_rule s_color, line_width: s_width_side, line_style: options[:line_style]
677
+ stroke_vertical_rule s_color, line_width: s_width_side, line_style: options[:line_style], at: bounds.width
678
+ end
679
+ else
661
680
  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
681
+ case options[:line_style]
682
+ when :dashed
683
+ line_width s_width
684
+ dash s_width * 4
685
+ when :dotted
686
+ line_width s_width
687
+ dash s_width
688
+ when :double
689
+ single_line_width = s_width / 3.0
690
+ line_width single_line_width
691
+ inner_line_offset = single_line_width * 2
692
+ inner_top_left = [bounds.left + inner_line_offset, bounds.top - inner_line_offset]
693
+ stroke_rounded_rectangle bounds.top_left, bounds.width, bounds.height, radius
694
+ stroke_rounded_rectangle inner_top_left, bounds.width - (inner_line_offset * 2), bounds.height - (inner_line_offset * 2), radius
695
+ next
696
+ else # :solid
697
+ line_width s_width
698
+ end
667
699
  stroke_rounded_rectangle bounds.top_left, bounds.width, bounds.height, radius
668
- #undash if options.has_key? :dash_width
669
700
  end
670
701
  end
671
702
  end
672
703
 
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
704
  # Strokes a horizontal line using the current bounds. The width of the line
696
705
  # can be specified using the line_width option. The offset from the cursor
697
706
  # can be set using the at option.
@@ -702,21 +711,25 @@ module Asciidoctor
702
711
  rule_width = options[:line_width] || 0.5
703
712
  rule_x_start = bounds.left
704
713
  rule_x_end = bounds.right
705
- rule_inked = false
706
714
  save_graphics_state do
707
- line_width rule_width
708
715
  stroke_color rule_color
709
716
  case rule_style
710
717
  when :dashed
718
+ line_width rule_width
711
719
  dash rule_width * 4
712
720
  when :dotted
721
+ line_width rule_width
713
722
  dash rule_width
714
723
  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
724
+ single_rule_width = rule_width / 3.0
725
+ line_width single_rule_width
726
+ stroke_horizontal_line rule_x_start, rule_x_end, at: (rule_y + single_rule_width)
727
+ stroke_horizontal_line rule_x_start, rule_x_end, at: (rule_y - single_rule_width)
728
+ next
729
+ else # :solid
730
+ line_width rule_width
731
+ end
732
+ stroke_horizontal_line rule_x_start, rule_x_end, at: rule_y
720
733
  end
721
734
  end
722
735
 
@@ -754,14 +767,9 @@ module Asciidoctor
754
767
  def delete_page
755
768
  pg = page_number
756
769
  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
770
  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
771
+ # NOTE: cannot delete objects and IDs, otherwise references get corrupted; so just reset the value
772
+ (pdf_store.instance_variable_get :@objects)[content_id] = ::PDF::Core::Reference.new content_id, {}
765
773
  pdf_store.pages.data[:Kids].pop
766
774
  pdf_store.pages.data[:Count] -= 1
767
775
  state.pages.pop
@@ -780,60 +788,63 @@ module Asciidoctor
780
788
  # However, due to how page creation works in Prawn, understand that advancing
781
789
  # to the next page is necessary to prevent the size & layout of the imported
782
790
  # page from affecting a newly created page.
783
- def import_page file, opts = {}
791
+ def import_page file, options = {}
784
792
  prev_page_layout = page.layout
785
793
  prev_page_size = page.size
786
794
  state.compress = false if state.compress # can't use compression if using template
787
795
  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]
796
+ delete_page if options[:replace]
797
+ # NOTE: use functionality provided by prawn-templates
798
+ start_new_page_discretely template: file, template_page: options[:page]
791
799
  # prawn-templates sets text_rendering_mode to :unknown, which breaks running content; revert
792
800
  @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
801
+ if page.imported_page?
802
+ yield if block_given?
803
+ # NOTE: set page size & layout explicitly in case imported page differs
796
804
  # I'm not sure it's right to start a new page here, but unfortunately there's no other
797
805
  # way atm to prevent the size & layout of the imported page from affecting subsequent pages
806
+ advance_page size: prev_page_size, layout: prev_page_layout if options.fetch :advance, true
807
+ elsif options.fetch :advance_if_missing, true
808
+ delete_page
809
+ # NOTE: see previous comment
798
810
  advance_page size: prev_page_size, layout: prev_page_layout
811
+ else
812
+ delete_page
799
813
  end
800
814
  nil
801
815
  end
802
816
 
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.
817
+ # Create a new page for the specified image.
818
+ #
819
+ # The image is positioned relative to the boundaries of the page.
805
820
  def image_page file, options = {}
806
821
  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])
822
+ ex = nil
823
+ float do
824
+ canvas do
825
+ image file, ({ position: :center, vposition: :center }.merge options)
826
+ rescue
827
+ ex = $!
828
+ end
812
829
  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
830
+ raise ex if ex
815
831
  nil
816
832
  end
817
833
 
818
834
  # Perform an operation (such as creating a new page) without triggering the on_page_create callback
819
835
  #
820
836
  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
837
+ state.on_page_create_callback = nil if (saved_callback = state.on_page_create_callback) != InhibitNewPageProc
838
+ yield
839
+ ensure
840
+ state.on_page_create_callback = saved_callback
830
841
  end
831
842
 
832
843
  # This method is a smarter version of start_new_page. It calls start_new_page
833
844
  # if the current page is the last page of the document. Otherwise, it simply
834
845
  # advances to the next existing page.
835
- def advance_page opts = {}
836
- last_page? ? (start_new_page opts) : (go_to_page page_number + 1)
846
+ def advance_page options = {}
847
+ last_page? ? (start_new_page options) : (go_to_page page_number + 1)
837
848
  end
838
849
 
839
850
  # Start a new page without triggering the on_page_create callback
@@ -844,115 +855,208 @@ module Asciidoctor
844
855
 
845
856
  # Grouping
846
857
 
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
858
+ def allocate_prototype
859
+ @prototype = create_prototype { ::Marshal.load ::Marshal.dump self }
856
860
  end
857
861
 
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
862
+ def scratch
863
+ @scratch ||= ((Marshal.load Marshal.dump @prototype).send :init_scratch, self)
874
864
  end
875
865
 
876
866
  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
867
+ @label == :scratch
880
868
  end
881
869
  alias is_scratch? scratch?
882
870
 
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]
904
- end
905
-
906
871
  def with_dry_run &block
907
- total_height, = dry_run(&block)
908
- instance_exec total_height, &block
872
+ yield dry_run(&block).position_onto self, cursor
909
873
  end
910
874
 
911
- # Attempt to keep the objects generated in the block on the same page
875
+ # Yields to the specified block multiple times, first to determine where to render the content
876
+ # so it fits properly, then once more, this time providing access to the content's extent, to
877
+ # ink the content in the primary document.
912
878
  #
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
879
+ # This method yields to the specified block in a scratch document by calling dry_run to
880
+ # determine where the content should start in the primary document. In the process, it also
881
+ # computes the extent of the content. It then returns to the primary document and yields to
882
+ # the block again, this time passing in the extent of the content. The extent can be used to
883
+ # draw a border and/or background under the content before inking it.
884
+ #
885
+ # This method is intended to enclose the conversion of a single content block, such as a
886
+ # sidebar or example block. The arrange logic attempts to keep unbreakable content on the same
887
+ # page, keeps the top caption pinned to the top of the content, computes the extent of the
888
+ # content for the purpose of drawing a border and/or background underneath it, and ensures
889
+ # that the extent does not begin near the bottom of a page if the first line of content
890
+ # doesn't fit. If unbreakable content does not fit on a single page, the content is treated as
891
+ # breakable.
892
+ #
893
+ # The block passed to this method should use advance_page to move to the next page rather than
894
+ # start_new_page. Using start_new_page can mangle the calculation of content block's extent.
895
+ #
896
+ def arrange_block node, &block
897
+ keep_together = (node.option? 'unbreakable') && !at_page_top?
898
+ doc = node.document
899
+ block_for_scratch = proc do
900
+ push_scratch doc
901
+ instance_exec(&block)
902
+ ensure
903
+ pop_scratch doc
924
904
  end
905
+ extent = dry_run keep_together: keep_together, onto: [self, keep_together], &block_for_scratch
906
+ scratch? ? block_for_scratch.call : (yield extent)
907
+ end
925
908
 
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)
909
+ # This method installs an on_page_create_callback that stops processing if the first page is
910
+ # exceeded while yielding to the specified block. If the content fits on a single page, the
911
+ # processing is not stopped. The purpose of this method is to determine if the content fits on
912
+ # a single page.
913
+ #
914
+ # Returns a Boolean indicating whether the content fits on a single page.
915
+ def perform_on_single_page
916
+ saved_callback, state.on_page_create_callback = state.on_page_create_callback, InhibitNewPageProc
917
+ yield
918
+ false
919
+ rescue NewPageRequiredError
920
+ true
921
+ ensure
922
+ state.on_page_create_callback = saved_callback
929
923
  end
930
924
 
931
- # Attempt to keep the objects generated in the block on the same page
932
- # if the verdict parameter is true.
925
+ # This method installs an on_page_create_callback that stops processing if a new page is
926
+ # created without writing content to the first page while yielding to the specified block. If
927
+ # any content is written to the first page, processing is not stopped. The purpose of this
928
+ # method is to check whether any content fits on the remaining space on the current page.
933
929
  #
934
- def keep_together_if verdict, &block
935
- if verdict
936
- keep_together(&block)
937
- else
930
+ # Returns a Boolean indicating whether any content is written on the first page.
931
+ def stop_if_first_page_empty
932
+ delegate = state.on_page_create_callback
933
+ state.on_page_create_callback = DetectEmptyFirstPageProc.curry[delegate].extend DetectEmptyFirstPage
934
+ yield
935
+ false
936
+ rescue NewPageRequiredError
937
+ true
938
+ ensure
939
+ state.on_page_create_callback = delegate
940
+ end
941
+
942
+ # NOTE: only used in dry_run since that's when DetectEmptyFirstPage is active
943
+ def tare_first_page_content_stream
944
+ return yield unless DetectEmptyFirstPage === (delegate = state.on_page_create_callback)
945
+ on_page_create_called = nil
946
+ state.on_page_create_callback = proc do |pdf|
947
+ on_page_create_called = true
948
+ pdf.state.pages[pdf.page_number - 2].tare_content_stream
949
+ delegate.call pdf
950
+ end
951
+ begin
938
952
  yield
953
+ ensure
954
+ page.tare_content_stream unless on_page_create_called
955
+ state.on_page_create_callback = delegate
939
956
  end
940
957
  end
941
958
 
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
959
+ # Yields to the specified block within the context of a scratch document up to three times to
960
+ # acertain the extent of the content block.
961
+ #
962
+ # The purpose of this method is two-fold. First, it works out the position where the rendered
963
+ # content should start in the calling document. Then, it precomputes the extent of the content
964
+ # starting from that position.
965
+ #
966
+ # This method returns the content's extent (the range from the start page and cursor to the
967
+ # end page and cursor) as a ScratchExtent object or, if the onto keyword parameter is
968
+ # specified, an Extent object. A ScratchExtent always starts the page range at 1. When the
969
+ # ScratchExtent is positioned onto the primary document using ScratchExtent#position_onto,
970
+ # that's when the cursor may be advanced to the next page.
971
+ #
972
+ # This method performs all work in a scratch document (or documents). It begins by starting a
973
+ # new page in the scratch document, first creating the scratch document if necessary. It then
974
+ # applies all the settings from the main document to the scratch document that impact
975
+ # rendering. This includes the bounds, the cursor position, and the font settings.
976
+ #
977
+ # From this point, the number of attempts the method makes is determined by the value of the
978
+ # keep_together keyword parameter. If the value is true (or the parent document is inhibiting
979
+ # page creation), it starts from the top of the page, yields to the block, and tries to fit
980
+ # the content on the current page. If the content fits, it computes and returns the
981
+ # ScratchExtent (or Extent). If the content does not fit, it first checks if this scenario
982
+ # should stop the operation. If the parent document is inhibiting page creation, it bubbles
983
+ # the error. If the single_page keyword argument is :enforce, it raises a CannotFit error. If
984
+ # the single_page keyword argument is true, it returns a ScratchExtent (or Extent) that
985
+ # represents a full page. If none of those conditions are met, it restarts with the
986
+ # keep_together parameter unset.
987
+ #
988
+ # If the keep_together parameter is not true, the method tries to render the content in the
989
+ # scratch document from the location of the cursor in the main document. If the cursor is at
990
+ # the top of the page, no special conditions are applied (this is the last attempt). The
991
+ # content is rendered and the extent is computed based on where the content ended up (minus a
992
+ # trailing empty page). If the cursor is not at the top of the page, the method renders the
993
+ # content while listening for a page creation event before any content is written. If a new
994
+ # page is created, and no content is written on the first page, the method restarts with the
995
+ # cursor at the top of the page.
996
+ #
997
+ # Note that if the block has content that itself requires a dry run, that nested dry run will
998
+ # be performed in a separate scratch document.
999
+ #
1000
+ # opts - A Hash of options that configure the dry run computation:
1001
+ # :keep_together - A Boolean indicating whether an attempt should be made to keep the
1002
+ # content on the same page (optional, default: false).
1003
+ # :single_page - A Boolean indicating whether the operation should stop if the content
1004
+ # exceeds the height of a single page.
1005
+ # :onto - The document onto which to position the scratch extent. If this option is
1006
+ # set, the method returns an Extent instead of a ScratchExtent (optional, default: nil)
1007
+ # :pages_advanced - The number of pages the content has been advanced during this
1008
+ # operation (internal only) (optional, default: 0)
1009
+ #
1010
+ # Returns an Extent or ScratchExtent object that describes the bounds of the content block.
1011
+ def dry_run keep_together: nil, pages_advanced: 0, single_page: nil, onto: nil, &block
1012
+ (scratch_pdf = scratch).start_new_page
1013
+ scratch_bounds = scratch_pdf.bounds
1014
+ restore_bounds = [:@total_left_padding, :@total_right_padding, :@width, :@x].each_with_object({}) do |name, accum|
1015
+ accum[name] = scratch_bounds.instance_variable_get name
1016
+ scratch_bounds.instance_variable_set name, (bounds.instance_variable_get name)
1017
+ end
1018
+ scratch_pdf.move_cursor_to cursor unless (scratch_start_at_top = keep_together || pages_advanced > 0 || at_page_top?)
1019
+ scratch_start_cursor = scratch_pdf.cursor
1020
+ scratch_start_page = scratch_pdf.page_number
1021
+ inhibit_new_page = state.on_page_create_callback == InhibitNewPageProc
1022
+ restart = nil
1023
+ scratch_pdf.font font_family, size: font_size, style: font_style do
1024
+ prev_font_scale, scratch_pdf.font_scale = scratch_pdf.font_scale, font_scale
1025
+ if keep_together || inhibit_new_page
1026
+ if (restart = scratch_pdf.perform_on_single_page { scratch_pdf.instance_exec(&block) })
1027
+ # NOTE: propogate NewPageRequiredError from nested block, which is rendered in separate scratch document
1028
+ raise NewPageRequiredError if inhibit_new_page
1029
+ if single_page
1030
+ raise ::Prawn::Errors::CannotFit if single_page == :enforce
1031
+ # single_page and onto are mutually exclusive
1032
+ return ScratchExtent.new scratch_start_page, scratch_start_cursor, scratch_start_page, 0
1033
+ end
1034
+ end
1035
+ elsif scratch_start_at_top
1036
+ scratch_pdf.instance_exec(&block)
1037
+ elsif (restart = scratch_pdf.stop_if_first_page_empty { scratch_pdf.instance_exec(&block) })
1038
+ pages_advanced += 1
1039
+ end
1040
+ ensure
1041
+ scratch_pdf.font_scale = prev_font_scale
1042
+ end
1043
+ return dry_run pages_advanced: pages_advanced, onto: onto, &block if restart
1044
+ scratch_end_page = scratch_pdf.page_number - scratch_start_page + (scratch_start_page = 1)
1045
+ if pages_advanced > 0
1046
+ scratch_start_page += pages_advanced
1047
+ scratch_end_page += pages_advanced
1048
+ end
1049
+ scratch_end_cursor = scratch_pdf.cursor
1050
+ # NOTE: drop trailing blank page and move cursor to end of previous page
1051
+ if scratch_end_page > scratch_start_page && scratch_pdf.at_page_top?
1052
+ scratch_end_page -= 1
1053
+ scratch_end_cursor = 0
950
1054
  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)
1055
+ extent = ScratchExtent.new scratch_start_page, scratch_start_cursor, scratch_end_page, scratch_end_cursor
1056
+ onto ? extent.position_onto(*onto) : extent
1057
+ ensure
1058
+ restore_bounds.each {|name, val| scratch_bounds.instance_variable_set name, val }
954
1059
  end
955
- =end
956
1060
  end
957
1061
  end
958
1062
  end