asciidoctor-pdf 1.5.0.alpha.1

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