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.
- 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
|