asciidoctor-pdf 1.5.0.alpha.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.adoc +22 -0
- data/NOTICE.adoc +76 -0
- data/README.adoc +263 -0
- data/Rakefile +78 -0
- data/bin/asciidoctor-pdf +15 -0
- data/bin/optimize-pdf +63 -0
- data/data/fonts/LICENSE-liberation-fonts-2.00.1 +102 -0
- data/data/fonts/LICENSE-mplus-testflight-58 +16 -0
- data/data/fonts/LICENSE-noto-fonts-2014-01-30 +201 -0
- data/data/fonts/liberationmono-bold-latin.ttf +0 -0
- data/data/fonts/liberationmono-bolditalic-latin.ttf +0 -0
- data/data/fonts/liberationmono-italic-latin.ttf +0 -0
- data/data/fonts/liberationmono-regular-latin.ttf +0 -0
- data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-bolditalic-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
- data/data/fonts/mplus1p-bold-latin.ttf +0 -0
- data/data/fonts/mplus1p-light-latin.ttf +0 -0
- data/data/fonts/mplus1p-regular-latin.ttf +0 -0
- data/data/fonts/mplus1p-regular-multilingual.ttf +0 -0
- data/data/fonts/notoserif-bold-latin.ttf +0 -0
- data/data/fonts/notoserif-bolditalic-latin.ttf +0 -0
- data/data/fonts/notoserif-italic-latin.ttf +0 -0
- data/data/fonts/notoserif-regular-latin.ttf +0 -0
- data/data/themes/asciidoctor-theme.yml +174 -0
- data/data/themes/default-theme.yml +182 -0
- data/examples/chronicles.adoc +429 -0
- data/examples/chronicles.pdf +0 -0
- data/examples/example-pdf-screenshot.png +0 -0
- data/examples/example.adoc +27 -0
- data/examples/example.pdf +0 -0
- data/examples/sample-title-logo.jpg +0 -0
- data/examples/wolpertinger.jpg +0 -0
- data/lib/asciidoctor-pdf.rb +3 -0
- data/lib/asciidoctor-pdf/asciidoctor_ext.rb +1 -0
- data/lib/asciidoctor-pdf/asciidoctor_ext/section.rb +26 -0
- data/lib/asciidoctor-pdf/converter.rb +1365 -0
- data/lib/asciidoctor-pdf/core_ext/array.rb +5 -0
- data/lib/asciidoctor-pdf/core_ext/ostruct.rb +9 -0
- data/lib/asciidoctor-pdf/implicit_header_processor.rb +59 -0
- data/lib/asciidoctor-pdf/pdfmarks.rb +30 -0
- data/lib/asciidoctor-pdf/prawn_ext.rb +3 -0
- data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +94 -0
- data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +529 -0
- data/lib/asciidoctor-pdf/prawn_ext/formatted_text/formatter.rb +29 -0
- data/lib/asciidoctor-pdf/prawn_ext/formatted_text/parser.rb +1012 -0
- data/lib/asciidoctor-pdf/prawn_ext/formatted_text/parser.treetop +115 -0
- data/lib/asciidoctor-pdf/prawn_ext/formatted_text/transform.rb +178 -0
- data/lib/asciidoctor-pdf/roman_numeral.rb +107 -0
- data/lib/asciidoctor-pdf/theme_loader.rb +103 -0
- data/lib/asciidoctor-pdf/version.rb +5 -0
- metadata +248 -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,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
|