asciidoctor-pdf 1.6.1 → 2.0.0.alpha.2

Sign up to get free protection for your applications and to get access to all the features.
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