asciidoctor-pdf 1.5.0.alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.adoc +22 -0
  3. data/NOTICE.adoc +76 -0
  4. data/README.adoc +263 -0
  5. data/Rakefile +78 -0
  6. data/bin/asciidoctor-pdf +15 -0
  7. data/bin/optimize-pdf +63 -0
  8. data/data/fonts/LICENSE-liberation-fonts-2.00.1 +102 -0
  9. data/data/fonts/LICENSE-mplus-testflight-58 +16 -0
  10. data/data/fonts/LICENSE-noto-fonts-2014-01-30 +201 -0
  11. data/data/fonts/liberationmono-bold-latin.ttf +0 -0
  12. data/data/fonts/liberationmono-bolditalic-latin.ttf +0 -0
  13. data/data/fonts/liberationmono-italic-latin.ttf +0 -0
  14. data/data/fonts/liberationmono-regular-latin.ttf +0 -0
  15. data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
  16. data/data/fonts/mplus1mn-bolditalic-ascii.ttf +0 -0
  17. data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
  18. data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
  19. data/data/fonts/mplus1p-bold-latin.ttf +0 -0
  20. data/data/fonts/mplus1p-light-latin.ttf +0 -0
  21. data/data/fonts/mplus1p-regular-latin.ttf +0 -0
  22. data/data/fonts/mplus1p-regular-multilingual.ttf +0 -0
  23. data/data/fonts/notoserif-bold-latin.ttf +0 -0
  24. data/data/fonts/notoserif-bolditalic-latin.ttf +0 -0
  25. data/data/fonts/notoserif-italic-latin.ttf +0 -0
  26. data/data/fonts/notoserif-regular-latin.ttf +0 -0
  27. data/data/themes/asciidoctor-theme.yml +174 -0
  28. data/data/themes/default-theme.yml +182 -0
  29. data/examples/chronicles.adoc +429 -0
  30. data/examples/chronicles.pdf +0 -0
  31. data/examples/example-pdf-screenshot.png +0 -0
  32. data/examples/example.adoc +27 -0
  33. data/examples/example.pdf +0 -0
  34. data/examples/sample-title-logo.jpg +0 -0
  35. data/examples/wolpertinger.jpg +0 -0
  36. data/lib/asciidoctor-pdf.rb +3 -0
  37. data/lib/asciidoctor-pdf/asciidoctor_ext.rb +1 -0
  38. data/lib/asciidoctor-pdf/asciidoctor_ext/section.rb +26 -0
  39. data/lib/asciidoctor-pdf/converter.rb +1365 -0
  40. data/lib/asciidoctor-pdf/core_ext/array.rb +5 -0
  41. data/lib/asciidoctor-pdf/core_ext/ostruct.rb +9 -0
  42. data/lib/asciidoctor-pdf/implicit_header_processor.rb +59 -0
  43. data/lib/asciidoctor-pdf/pdfmarks.rb +30 -0
  44. data/lib/asciidoctor-pdf/prawn_ext.rb +3 -0
  45. data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +94 -0
  46. data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +529 -0
  47. data/lib/asciidoctor-pdf/prawn_ext/formatted_text/formatter.rb +29 -0
  48. data/lib/asciidoctor-pdf/prawn_ext/formatted_text/parser.rb +1012 -0
  49. data/lib/asciidoctor-pdf/prawn_ext/formatted_text/parser.treetop +115 -0
  50. data/lib/asciidoctor-pdf/prawn_ext/formatted_text/transform.rb +178 -0
  51. data/lib/asciidoctor-pdf/roman_numeral.rb +107 -0
  52. data/lib/asciidoctor-pdf/theme_loader.rb +103 -0
  53. data/lib/asciidoctor-pdf/version.rb +5 -0
  54. metadata +248 -0
@@ -0,0 +1,5 @@
1
+ class Array
2
+ def to_h
3
+ Hash[to_a]
4
+ end unless respond_to? :to_h
5
+ end
@@ -0,0 +1,9 @@
1
+ class OpenStruct
2
+ def [] key
3
+ send key
4
+ end
5
+
6
+ def []= key, val
7
+ send %(#{key}=), val
8
+ end
9
+ end if RUBY_VERSION < '2.0.0'
@@ -0,0 +1,59 @@
1
+ require 'asciidoctor/extensions'
2
+
3
+ module Asciidoctor
4
+ module Pdf
5
+ # An include processor that skips the implicit author line below
6
+ # the document title within include documents.
7
+ class ImplicitHeaderProcessor < ::Asciidoctor::Extensions::IncludeProcessor
8
+ def process doc, reader, target, attributes
9
+ return reader unless File.exist? target
10
+ ::File.open target, 'r' do |fd|
11
+ # FIXME handle case where doc id is specified above title
12
+ if (first_line = fd.readline) && (first_line.start_with? '= ')
13
+ # HACK reset counters for each article for Editions
14
+ if doc.attr? 'env', 'editions'
15
+ doc.counters.each do |(counter_key, counter_val)|
16
+ doc.attributes.delete counter_key
17
+ end
18
+ doc.counters.clear
19
+ end
20
+ if (second_line = fd.readline)
21
+ if AuthorInfoLineRx =~ second_line
22
+ # FIXME temporary hack to set author and e-mail attributes; this should handle all attributes in header!
23
+ author = [$1, $2, $3].compact * ' '
24
+ email = $4
25
+ reader.push_include fd.readlines, target, target, 3, attributes unless fd.eof?
26
+ reader.push_include first_line, target, target, 1, attributes
27
+ lines = [%(:author: #{author})]
28
+ lines << %(:email: #{email}) if email
29
+ reader.push_include lines, target, target, 2, attributes
30
+ else
31
+ lines = [second_line]
32
+ lines += fd.readlines unless fd.eof?
33
+ reader.push_include lines, target, target, 2, attributes
34
+ reader.push_include first_line, target, target, 1, attributes
35
+ end
36
+ else
37
+ reader.push_include first_line, target, target, 1, attributes
38
+ end
39
+ else
40
+ lines = [first_line]
41
+ lines += fd.readlines unless fd.eof?
42
+ reader.push_include lines, target, target, 1, attributes
43
+ end
44
+ end
45
+ reader
46
+ end
47
+
48
+ def handles? target
49
+ # FIXME should not require this hack to skip processing bio
50
+ !(target.end_with? 'bio.adoc') && ((target.end_with? '.adoc') || (target.end_with? '.asciidoc'))
51
+ end
52
+
53
+ # FIXME this method shouldn't be required
54
+ def update_config config
55
+ (@config ||= {}).update config
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,30 @@
1
+ module Asciidoctor
2
+ module Pdf
3
+ class Pdfmarks
4
+ def initialize doc
5
+ @doc = doc
6
+ end
7
+
8
+ def generate
9
+ current_datetime = ::DateTime.now.strftime '%Y%m%d%H%M%S'
10
+ doc = @doc
11
+ content = <<-EOS
12
+ [ /Title (#{doc.doctitle sanitize: true, use_fallback: true})
13
+ /Author (#{doc.attr 'authors'})
14
+ /Subject (#{doc.attr 'subject'})
15
+ /Keywords (#{doc.attr 'keywords'})
16
+ /ModDate (D:#{current_datetime})
17
+ /CreationDate (D:#{current_datetime})
18
+ /Creator (Asciidoctor PDF #{::Asciidoctor::Pdf::VERSION}, based on Prawn #{::Prawn::VERSION})
19
+ /Producer (#{doc.attr 'publisher'})
20
+ /DOCINFO pdfmark
21
+ EOS
22
+ content
23
+ end
24
+
25
+ def generate_file pdf_file
26
+ ::IO.write %(#{pdf_file}marks), generate
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,3 @@
1
+ # the following modules / classes are organized under the Asciidoctor::Prawn namespace
2
+ require_relative 'prawn_ext/extensions'
3
+ require_relative 'prawn_ext/formatted_text/formatter'
@@ -0,0 +1,94 @@
1
+ ######################################################################
2
+ #
3
+ # This file was copied from Prawn (manual/syntax_highlight.rb) and
4
+ # modified for use with Asciidoctor PDF.
5
+ #
6
+ # Prawn is free software: you can redistribute it and/or modify
7
+ # it under the terms of the GNU General Public License as published by
8
+ # the Free Software Foundation, either version 3 of the License, or
9
+ # (at your option) any later version.
10
+ #
11
+ # Prawn is distributed in the hope that it will be useful,
12
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ # GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License
17
+ # along with Prawn. If not, see <http://www.gnu.org/licenses/>.
18
+ #
19
+ # Copyright (C) Felipe Doria
20
+ # Copyright (C) 2014 OpenDevise Inc. and the Asciidoctor Project
21
+ #
22
+ ######################################################################
23
+
24
+ require 'coderay'
25
+
26
+ # Registers a to_prawn method with CodeRay. The method returns an array of hashes to be
27
+ # used with Prawn::Text.formatted_text(array).
28
+ #
29
+ # Usage:
30
+ #
31
+ # CodeRay.scan(string, :ruby).to_prawn
32
+ #
33
+ module Asciidoctor
34
+ module Prawn
35
+ class CodeRayEncoder < ::CodeRay::Encoders::Encoder
36
+ register_for :to_prawn
37
+
38
+ # Manni theme from Pygments
39
+ COLORS = {
40
+ default: '333333',
41
+
42
+ annotation: '9999FF',
43
+ attribute_name: '4F9FCF',
44
+ attribute_value: 'D44950',
45
+ class: '00AA88',
46
+ class_variable: '003333',
47
+ color: 'FF6600',
48
+ comment: '999999',
49
+ constant: '336600',
50
+ directive: '006699',
51
+ doctype: '009999',
52
+ instance_variable: '003333',
53
+ integer: 'FF6600',
54
+ entity: '999999',
55
+ float: 'FF6600',
56
+ function: 'CC00FF',
57
+ important: '9999FF',
58
+ inline_delimiter: 'EF804F',
59
+ instance_variable: '003333',
60
+ key: '006699',
61
+ keyword: '006699',
62
+ method: 'CC00FF',
63
+ namespace: '00CCFF',
64
+ predefined_type: '007788',
65
+ regexp: '33AAAA',
66
+ string: 'CC3300',
67
+ symbol: 'FFCC33',
68
+ tag: '2F6F9F',
69
+ type: '007788',
70
+ value: '336600'
71
+ }
72
+
73
+ def setup(options)
74
+ super
75
+ @out = []
76
+ @open = []
77
+ end
78
+
79
+ def text_token(text, kind)
80
+ color = COLORS[kind] || COLORS[@open.last] || COLORS[:default]
81
+
82
+ @out << {:text => text, :color => color}
83
+ end
84
+
85
+ def begin_group(kind)
86
+ @open << kind
87
+ end
88
+
89
+ def end_group(kind)
90
+ @open.pop
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,529 @@
1
+ module Asciidoctor
2
+ module Prawn
3
+ module Extensions
4
+ include ::Prawn::Measurements
5
+
6
+ # - :height is the height of a line
7
+ # - :leading is spacing between adjacent lines
8
+ # - :padding_top is half line spacing, plus any line_gap in the font
9
+ # - :padding_bottom is half line spacing
10
+ # - :final_gap determines whether a gap is added below the last line
11
+ LineMetrics = ::Struct.new :height, :leading, :padding_top, :padding_bottom, :final_gap
12
+
13
+ # Core
14
+
15
+ # Retrieves the catalog reference data for the PDF.
16
+ #
17
+ def catalog
18
+ state.store.root
19
+ end
20
+
21
+ # Measurements
22
+
23
+ # Returns the width of the current page from edge-to-edge
24
+ #
25
+ def page_width
26
+ page.dimensions[2]
27
+ end
28
+
29
+ # Returns the height of the current page from edge-to-edge
30
+ #
31
+ def page_height
32
+ page.dimensions[3]
33
+ end
34
+
35
+ # Returns the width of the left margin for the current page
36
+ #
37
+ def left_margin
38
+ page.margins[:left]
39
+ end
40
+
41
+ # Returns the width of the right margin for the current page
42
+ #
43
+ def right_margin
44
+ page.margins[:right]
45
+ end
46
+
47
+ # Returns whether the cursor is at the top of the page (i.e., margin box)
48
+ #
49
+ def at_page_top?
50
+ @y == @margin_box.absolute_top
51
+ end
52
+
53
+ # Destinations
54
+
55
+ # Generates a destination object that resolves to the top of the page
56
+ # specified by the page_num parameter or the current page if no page number
57
+ # is provided. The destination preserves the user's zoom level unlike
58
+ # the destinations generated by the outline builder.
59
+ #
60
+ def dest_top page_num = nil
61
+ dest_xyz 0, page_height, nil, (page_num ? state.pages[page_num - 1] : page)
62
+ end
63
+
64
+ # Text
65
+
66
+ =begin
67
+ # Draws a disc bullet as float text
68
+ def draw_bullet
69
+ float { text '•' }
70
+ end
71
+ =end
72
+
73
+ # Fonts
74
+
75
+ # Registers a new custom font described in the data parameter
76
+ # after converting the font name to a String.
77
+ #
78
+ # Example:
79
+ #
80
+ # register_font GillSans: {
81
+ # normal: 'assets/fonts/GillSans.ttf',
82
+ # bold: 'assets/fonts/GillSans-Bold.ttf',
83
+ # italic: 'assets/fonts/GillSans-Italic.ttf',
84
+ # }
85
+ #
86
+ def register_font data
87
+ font_families.update data.inject({}) {|accum, (key, val)| accum[key.to_s] = val; accum }
88
+ end
89
+
90
+ # Enhances the built-in font method to allow the font
91
+ # size to be specified as the second option.
92
+ #
93
+ def font name = nil, options = {}
94
+ if !name.nil? && ((size = options).is_a? ::Numeric)
95
+ options = { size: size }
96
+ end
97
+ super name, options
98
+ end
99
+
100
+ # Retrieves the current font name (i.e., family).
101
+ #
102
+ def font_family
103
+ font.options[:family]
104
+ end
105
+
106
+ alias :font_name :font_family
107
+
108
+ # Retrieves the current font info (family, style, size) as a Hash
109
+ #
110
+ def font_info
111
+ { family: font.options[:family], style: font.options[:style] || :normal, size: @font_size }
112
+ end
113
+
114
+ # Sets the font style for the scope of the block to which this method
115
+ # yields. If the style is nil and no block is given, return the current
116
+ # font style.
117
+ #
118
+ def font_style style = nil
119
+ if block_given?
120
+ font font.options[:family], style: style do
121
+ yield
122
+ end
123
+ elsif style.nil?
124
+ font.options[:style] || :normal
125
+ else
126
+ font font.options[:family], style: style
127
+ end
128
+ end
129
+
130
+ # Applies points as a scale factor of the current font if the value provided
131
+ # is less than or equal to 1 or it's a string (e.g., 1.1em), then delegates to the super
132
+ # implementation to carry out the built-in functionality.
133
+ #
134
+ #--
135
+ # QUESTION should we round the result?
136
+ def font_size points = nil
137
+ return @font_size unless points
138
+ #if points.is_a? String
139
+ # # QUESTION should we round?
140
+ # points = (@font_size * (points.chop.to_f / 100.0)).round
141
+ # warn points
142
+ #elsif points <= 1
143
+ # points = (@font_size * points)
144
+ #end
145
+ if points <= 1
146
+ points = (@font_size * points)
147
+ end
148
+ super points
149
+ end
150
+
151
+ def calc_line_metrics line_height = 1, font = self.font, font_size = self.font_size
152
+ line_height_length = line_height * font_size
153
+ leading = line_height_length - font_size
154
+ half_leading = leading / 2
155
+ padding_top = half_leading + font.line_gap
156
+ padding_bottom = half_leading
157
+ LineMetrics.new line_height_length, leading, padding_top, padding_bottom, false
158
+ end
159
+
160
+ =begin
161
+ # these line metrics attempted to figure out a correction based on the reported height and the font_size
162
+ # however, it only works for some fonts, and breaks down for fonts like NotoSerif
163
+ def calc_line_metrics line_height = 1, font = self.font, font_size = self.font_size
164
+ line_height_length = font_size * line_height
165
+ line_gap = line_height_length - font_size
166
+ correction = font.height - font_size
167
+ leading = line_gap - correction
168
+ shift = (font.line_gap + correction + line_gap) / 2
169
+ final_gap = font.line_gap != 0
170
+ LineMetrics.new line_height_length, leading, shift, shift, final_gap
171
+ end
172
+ =end
173
+
174
+ # Parse the text into an array of fragments using the text formatter.
175
+ def parse_text string, options = {}
176
+ return [] if string.nil?
177
+
178
+ options = options.dup
179
+ if (format_option = options.delete :inline_format)
180
+ format_option = [] unless format_option.is_a? ::Array
181
+ fragments = self.text_formatter.format string, *format_option
182
+ else
183
+ fragments = [{text: string}]
184
+ end
185
+
186
+ if (color = options.delete :color)
187
+ fragments.map do |fragment|
188
+ fragment[:color] ? fragment : fragment.merge(color: color)
189
+ end
190
+ else
191
+ fragments
192
+ end
193
+ end
194
+
195
+ # Performs the same work as text except that the first_line_options
196
+ # are applied to the first line of text renderered.
197
+ def text_with_formatted_first_line string, first_line_options, opts
198
+ fragments = parse_text string, opts
199
+ opts = opts.merge document: self
200
+ box = ::Prawn::Text::Formatted::Box.new fragments, (opts.merge single_line: true)
201
+ remaining_fragments = box.render dry_run: true
202
+ # HACK prawn removes the color from remaining_fragments, so we have to explicitly restore
203
+ if (color = opts[:color])
204
+ remaining_fragments.each {|fragment| fragment[:color] ||= color }
205
+ end
206
+ # FIXME merge options more intelligently so as not to clobber other styles in set
207
+ fragments = fragments.map {|fragment| fragment.merge first_line_options }
208
+ fill_formatted_text_box fragments, (opts.merge single_line: true)
209
+ if remaining_fragments.size > 0
210
+ # as of Prawn 1.2.1, we have to handle the line gap after the first line manually
211
+ move_down opts[:leading]
212
+ remaining_fragments = fill_formatted_text_box remaining_fragments, opts
213
+ draw_remaining_formatted_text_on_new_pages remaining_fragments, opts
214
+ end
215
+ end
216
+
217
+ # Cursor
218
+
219
+ # Short-circuits the call to the built-in move_up operation
220
+ # when n is 0.
221
+ #
222
+ def move_up n
223
+ super unless n == 0
224
+ end
225
+
226
+ # Short-circuits the call to the built-in move_down operation
227
+ # when n is 0.
228
+ #
229
+ def move_down n
230
+ super unless n == 0
231
+ end
232
+
233
+ # Bounds
234
+
235
+ # Overrides the built-in pad operation to allow for asymmetric paddings.
236
+ #
237
+ # Example:
238
+ #
239
+ # pad 20, 10 do
240
+ # text 'A paragraph with twice as much top padding as bottom padding.'
241
+ # end
242
+ #
243
+ def pad top, bottom = nil
244
+ move_down top
245
+ yield
246
+ move_down(bottom || top)
247
+ end
248
+
249
+ # Combines the built-in pad and indent operations into a single method.
250
+ #
251
+ # Padding may be specified as an array of four values, or as a single value.
252
+ # The single value is used as the padding around all four sides of the box.
253
+ #
254
+ # If padding is nil, this method simply yields to the block and returns.
255
+ #
256
+ # Example:
257
+ #
258
+ # pad_box 20 do
259
+ # text 'A paragraph inside a blox with even padding on all sides.'
260
+ # end
261
+ #
262
+ # pad_box [10, 10, 10, 20] do
263
+ # text 'An indented paragraph inside a box with equal padding on all sides.'
264
+ # end
265
+ #
266
+ def pad_box padding
267
+ if padding
268
+ # TODO implement shorthand combinations like in CSS
269
+ p_top, p_right, p_bottom, p_left = (padding.is_a? ::Array) ? padding : ([padding] * 4)
270
+ begin
271
+ # inlined logic
272
+ move_down p_top
273
+ bounds.add_left_padding p_left
274
+ bounds.add_right_padding p_right
275
+ yield
276
+ move_down p_bottom
277
+ ensure
278
+ bounds.subtract_left_padding p_left
279
+ bounds.subtract_right_padding p_right
280
+ end
281
+ else
282
+ yield
283
+ end
284
+
285
+ # alternate, delegated logic
286
+ #pad padding[0], padding[2] do
287
+ # indent padding[1], padding[3] do
288
+ # yield
289
+ # end
290
+ #end
291
+ end
292
+
293
+ # Stretch the current bounds to the left and right edges of the current page.
294
+ #
295
+ def use_page_width_if verdict
296
+ if verdict
297
+ indent(-bounds.absolute_left, bounds.absolute_right - page_width) do
298
+ yield
299
+ end
300
+ else
301
+ yield
302
+ end
303
+ end
304
+
305
+ # Graphics
306
+
307
+ # Fills the current bounding box with the specified fill color. Before
308
+ # returning from this method, the original fill color on the document is
309
+ # restored.
310
+ #
311
+ def fill_bounds f_color = fill_color
312
+ unless f_color.nil? || f_color.to_s == 'transparent'
313
+ original_fill_color = fill_color
314
+ fill_color f_color
315
+ fill_rectangle bounds.top_left, bounds.width, bounds.height
316
+ fill_color original_fill_color
317
+ end
318
+ end
319
+
320
+ # Fills the current bounds using the specified fill color and strokes the
321
+ # bounds using the specified stroke color. Sets the line with if specified
322
+ # in the options. Before returning from this method, the original fill
323
+ # color, stroke color and line width on the document are restored.
324
+ #
325
+ def fill_and_stroke_bounds f_color = fill_color, s_color = stroke_color, options = {}
326
+ no_fill = (f_color.nil? || f_color.to_s == 'transparent')
327
+ no_stroke = (s_color.nil? || s_color.to_s == 'transparent')
328
+ return if no_fill && no_stroke
329
+ save_graphics_state do
330
+ radius = options[:radius] || 0
331
+
332
+ # fill
333
+ unless no_fill
334
+ fill_color f_color
335
+ fill_rounded_rectangle bounds.top_left, bounds.width, bounds.height, radius
336
+ end
337
+
338
+ # stroke
339
+ unless no_stroke
340
+ stroke_color s_color
341
+ line_width options[:line_width] || 0.5
342
+ # FIXME think about best way to indicate dashed borders
343
+ #if options.has_key? :dash_width
344
+ # dash options[:dash_width], space: options[:dash_space] || 1
345
+ #end
346
+ stroke_rounded_rectangle bounds.top_left, bounds.width, bounds.height, radius
347
+ #undash if options.has_key? :dash_width
348
+ end
349
+ end
350
+ end
351
+
352
+ # Fills and, optionally, strokes the current bounds using the fill and
353
+ # stroke color specified, then yields to the block. The only_if option can
354
+ # be used to conditionally disable this behavior.
355
+ #
356
+ def shade_box color, line_color = nil, options = {}
357
+ if (!options.has_key? :only_if) || options[:only_if]
358
+ # FIXME could use save_graphics_state here
359
+ previous_fill_color = current_fill_color
360
+ fill_color color
361
+ fill_rectangle [bounds.left, bounds.top], bounds.right, bounds.top - bounds.bottom
362
+ fill_color previous_fill_color
363
+ if line_color
364
+ line_width 0.5
365
+ previous_stroke_color = current_stroke_color
366
+ stroke_color line_color
367
+ stroke_bounds
368
+ stroke_color previous_stroke_color
369
+ end
370
+ end
371
+ yield
372
+ end
373
+
374
+ # A compliment to the stroke_horizontal_rule method, strokes a
375
+ # vertical line using the current bounds. The width of the line
376
+ # can be specified using the line_width option. The horizontal (x)
377
+ # position can be specified using the at option.
378
+ #
379
+ def stroke_vertical_rule s_color = stroke_color, options = {}
380
+ save_graphics_state do
381
+ line_width options[:line_width] || 0.5
382
+ stroke_color s_color
383
+ stroke_vertical_line bounds.bottom, bounds.top, at: (options[:at] || 0)
384
+ end
385
+ end
386
+
387
+ # Strokes a horizontal line using the current bounds. The width of the line
388
+ # can be specified using the line_width option.
389
+ #
390
+ def stroke_horizontal_rule s_color = stroke_color, options = {}
391
+ save_graphics_state do
392
+ line_width options[:line_width] || 0.5
393
+ stroke_color s_color
394
+ stroke_horizontal_line bounds.left, bounds.right
395
+ end
396
+ end
397
+
398
+ # Pages
399
+
400
+ # Import the specified page into the current document.
401
+ #
402
+ def import_page file
403
+ prev_page_number = page_number
404
+ state.compress = false if state.compress # can't use compression if using template
405
+ start_new_page_discretely template: file
406
+ go_to_page prev_page_number + 1
407
+ end
408
+
409
+ # Create a new page for the specified image. If the
410
+ # canvas option is true, the image is stretched to the
411
+ # edges of the page (full coverage).
412
+ def image_page file, options = {}
413
+ start_new_page_discretely
414
+ if options[:canvas]
415
+ canvas do
416
+ image file, width: bounds.width, height: bounds.height
417
+ end
418
+ else
419
+ image file, fit: [bounds.width, bounds.height]
420
+ end
421
+ go_to_page page_count
422
+ end
423
+
424
+ # Perform an operation (such as creating a new page) without triggering the on_page_create callback
425
+ #
426
+ def perform_discretely
427
+ if (saved_callback = state.on_page_create_callback)
428
+ state.on_page_create_callback = nil
429
+ yield
430
+ state.on_page_create_callback = saved_callback
431
+ else
432
+ yield
433
+ end
434
+ end
435
+
436
+ # Start a new page without triggering the on_page_create callback
437
+ #
438
+ def start_new_page_discretely options = {}
439
+ perform_discretely do
440
+ start_new_page options
441
+ end
442
+ end
443
+
444
+ # Grouping
445
+
446
+ # Conditional group operation
447
+ #
448
+ def group_if verdict
449
+ if verdict
450
+ state.optimize_objects = false # optimize objects breaks group
451
+ group { yield }
452
+ else
453
+ yield
454
+ end
455
+ end
456
+
457
+ def get_scratch_document
458
+ # marshal if not using transaction feature
459
+ #Marshal.load Marshal.dump @prototype
460
+
461
+ # use cached instance, tests show it's faster
462
+ #@prototype ||= ::Prawn::Document.new
463
+ @scratch ||= if defined? @prototype
464
+ scratch = Marshal.load Marshal.dump @prototype
465
+ scratch.instance_variable_set(:@prototype, @prototype)
466
+ # TODO set scratch number on scratch document
467
+ scratch
468
+ else
469
+ warn 'asciidoctor: WARNING: no scratch prototype available; instantiating fresh scratch document'
470
+ ::Prawn::Document.new
471
+ end
472
+ end
473
+
474
+ def is_scratch?
475
+ !!state.store.info.data[:Scratch]
476
+ end
477
+ alias :scratch? :is_scratch?
478
+
479
+ # TODO document me
480
+ def dry_run &block
481
+ scratch = get_scratch_document
482
+ scratch.start_new_page
483
+ start_y = scratch.y
484
+ scratch.font font_family, style: font_style, size: font_size do
485
+ scratch.instance_exec(&block)
486
+ end
487
+ start_y - scratch.y
488
+ #height_of_content = start_y - scratch.y
489
+ #scratch.render_file 'scratch.pdf'
490
+ #height_of_content
491
+ end
492
+
493
+ # Attempt to keep the objects generated in the block on the same page
494
+ #
495
+ # TODO short-circuit nested usage
496
+ def keep_together &block
497
+ available_space = cursor
498
+ if (height_of_content = dry_run(&block)) > available_space
499
+ started_new_page = true
500
+ start_new_page
501
+ else
502
+ started_new_page = false
503
+ end
504
+ yield height_of_content, started_new_page
505
+ end
506
+
507
+ # Attempt to keep the objects generated in the block on the same page
508
+ # if the verdict parameter is true.
509
+ #
510
+ def keep_together_if verdict, &block
511
+ if verdict
512
+ keep_together(&block)
513
+ else
514
+ yield
515
+ end
516
+ end
517
+
518
+ def run_with_trial &block
519
+ available_space = cursor
520
+ if (height_of_content = dry_run(&block)) > available_space
521
+ started_new_page = true
522
+ else
523
+ started_new_page = false
524
+ end
525
+ yield height_of_content, started_new_page
526
+ end
527
+ end
528
+ end
529
+ end