asciidoctor-pdf 1.5.0.beta.1 → 1.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +277 -2
- data/LICENSE.adoc +1 -1
- data/NOTICE.adoc +1 -1
- data/README.adoc +486 -292
- data/asciidoctor-pdf.gemspec +12 -11
- data/bin/asciidoctor-pdf +2 -6
- data/bin/asciidoctor-pdf-optimize +20 -0
- data/data/fonts/ABOUT-mplus1mn-subset +26 -0
- data/data/fonts/ABOUT-mplus1p-subset +26 -0
- data/data/fonts/ABOUT-notoemoji-subset +3 -0
- data/data/fonts/ABOUT-notoserif-subset +26 -0
- data/data/fonts/{LICENSE-mplus-testflight-58 → LICENSE-mplus} +2 -2
- data/data/fonts/{LICENSE-noto-2015-06-05 → LICENSE-notoserif} +0 -0
- data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-bold-subset.ttf +0 -0
- data/data/fonts/mplus1mn-bold_italic-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-bold_italic-subset.ttf +0 -0
- data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-italic-subset.ttf +0 -0
- data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
- data/data/fonts/mplus1mn-regular-subset.ttf +0 -0
- data/data/fonts/mplus1p-regular-fallback.ttf +0 -0
- data/data/fonts/notoemoji-subset.ttf +0 -0
- data/data/fonts/notoserif-bold-subset.ttf +0 -0
- data/data/fonts/notoserif-bold_italic-subset.ttf +0 -0
- data/data/fonts/notoserif-italic-subset.ttf +0 -0
- data/data/fonts/notoserif-regular-subset.ttf +0 -0
- data/data/themes/base-theme.yml +22 -4
- data/data/themes/default-theme.yml +59 -29
- data/data/themes/default-with-fallback-font-theme.yml +4 -17
- data/docs/theming-guide.adoc +1647 -167
- data/lib/asciidoctor/pdf/converter.rb +4489 -0
- data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/abstract_block.rb +2 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_node.rb +7 -0
- data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/document.rb +2 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor/image.rb +35 -0
- data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/list.rb +4 -2
- data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/list_item.rb +3 -1
- data/lib/asciidoctor/pdf/ext/asciidoctor/logging_shim.rb +33 -0
- data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/section.rb +9 -6
- data/lib/asciidoctor/pdf/ext/asciidoctor.rb +11 -0
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/array.rb +6 -0
- data/lib/asciidoctor/pdf/ext/core/file.rb +9 -0
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/hash.rb +2 -0
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/numeric.rb +5 -3
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/object.rb +3 -1
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/quantifiable_stdout.rb +9 -1
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/regexp.rb +2 -0
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/string.rb +9 -13
- data/lib/asciidoctor/pdf/ext/core.rb +10 -0
- data/lib/asciidoctor/pdf/ext/pdf-core/page.rb +54 -0
- data/lib/asciidoctor/pdf/ext/pdf-core/pdf_object.rb +8 -0
- data/lib/asciidoctor/pdf/ext/pdf-core.rb +4 -0
- data/lib/asciidoctor/pdf/ext/prawn/coderay_encoder.rb +117 -0
- data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +922 -0
- data/lib/{asciidoctor-pdf/prawn_ext → asciidoctor/pdf/ext/prawn}/font/afm.rb +14 -10
- data/lib/asciidoctor/pdf/ext/prawn/font_metric_cache.rb +9 -0
- data/lib/asciidoctor/pdf/ext/prawn/formatted_text/box.rb +66 -0
- data/lib/{asciidoctor-pdf/prawn_ext → asciidoctor/pdf/ext/prawn}/formatted_text/fragment.rb +16 -12
- data/lib/asciidoctor/pdf/ext/prawn/images.rb +54 -0
- data/lib/asciidoctor/pdf/ext/prawn-svg/interface.rb +14 -0
- data/lib/{asciidoctor-pdf/prawn-svg_ext.rb → asciidoctor/pdf/ext/prawn-svg.rb} +3 -1
- data/lib/asciidoctor/pdf/ext/prawn-table/cell/asciidoc.rb +76 -0
- data/lib/{asciidoctor-pdf/prawn-table_ext → asciidoctor/pdf/ext/prawn-table}/cell/text.rb +6 -3
- data/lib/{asciidoctor-pdf/prawn-table_ext → asciidoctor/pdf/ext/prawn-table}/cell.rb +10 -10
- data/lib/asciidoctor/pdf/ext/prawn-table.rb +6 -0
- data/lib/{asciidoctor-pdf/prawn-templates_ext.rb → asciidoctor/pdf/ext/prawn-templates.rb} +2 -0
- data/lib/asciidoctor/pdf/ext/prawn.rb +9 -0
- data/lib/asciidoctor/pdf/ext/pygments.rb +34 -0
- data/lib/asciidoctor/pdf/ext/rouge/formatters/prawn.rb +208 -0
- data/lib/{asciidoctor-pdf/rouge_ext → asciidoctor/pdf/ext/rouge}/themes/asciidoctor_pdf_default.rb +2 -0
- data/lib/asciidoctor/pdf/ext/rouge.rb +5 -0
- data/lib/asciidoctor/pdf/ext.rb +9 -0
- data/lib/asciidoctor/pdf/formatted_text/formatter.rb +43 -0
- data/lib/asciidoctor/pdf/formatted_text/fragment_position_renderer.rb +14 -0
- data/lib/asciidoctor/pdf/formatted_text/inline_destination_marker.rb +21 -0
- data/lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb +134 -0
- data/lib/asciidoctor/pdf/formatted_text/inline_image_renderer.rb +51 -0
- data/lib/asciidoctor/pdf/formatted_text/inline_text_aligner.rb +22 -0
- data/lib/{asciidoctor-pdf → asciidoctor/pdf}/formatted_text/parser.rb +31 -7
- data/lib/{asciidoctor-pdf → asciidoctor/pdf}/formatted_text/parser.treetop +3 -4
- data/lib/asciidoctor/pdf/formatted_text/source_wrap.rb +43 -0
- data/lib/asciidoctor/pdf/formatted_text/text_background_and_border_renderer.rb +55 -0
- data/lib/asciidoctor/pdf/formatted_text/transform.rb +394 -0
- data/lib/{asciidoctor-pdf → asciidoctor/pdf}/formatted_text.rb +4 -0
- data/lib/asciidoctor/pdf/index_catalog.rb +133 -0
- data/lib/asciidoctor/pdf/measurements.rb +62 -0
- data/lib/asciidoctor/pdf/optimizer.rb +44 -0
- data/lib/asciidoctor/pdf/pdfmark.rb +41 -0
- data/lib/asciidoctor/pdf/roman_numeral.rb +128 -0
- data/lib/asciidoctor/pdf/sanitizer.rb +45 -0
- data/lib/asciidoctor/pdf/text_transformer.rb +116 -0
- data/lib/asciidoctor/pdf/theme_loader.rb +305 -0
- data/lib/asciidoctor/pdf/version.rb +8 -1
- data/lib/asciidoctor/pdf.rb +15 -1
- data/lib/asciidoctor-pdf/converter.rb +2 -3824
- data/lib/asciidoctor-pdf/version.rb +3 -6
- data/lib/asciidoctor-pdf.rb +3 -4
- metadata +130 -85
- data/lib/asciidoctor-pdf/asciidoctor_ext/image.rb +0 -24
- data/lib/asciidoctor-pdf/asciidoctor_ext/logging_shim.rb +0 -25
- data/lib/asciidoctor-pdf/asciidoctor_ext.rb +0 -8
- data/lib/asciidoctor-pdf/core_ext/ostruct.rb +0 -8
- data/lib/asciidoctor-pdf/core_ext.rb +0 -6
- data/lib/asciidoctor-pdf/formatted_text/formatter.rb +0 -40
- data/lib/asciidoctor-pdf/formatted_text/inline_destination_marker.rb +0 -21
- data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +0 -160
- data/lib/asciidoctor-pdf/formatted_text/inline_image_renderer.rb +0 -46
- data/lib/asciidoctor-pdf/formatted_text/inline_text_aligner.rb +0 -20
- data/lib/asciidoctor-pdf/formatted_text/text_background_and_border_renderer.rb +0 -45
- data/lib/asciidoctor-pdf/formatted_text/transform.rb +0 -294
- data/lib/asciidoctor-pdf/implicit_header_processor.rb +0 -63
- data/lib/asciidoctor-pdf/index_catalog.rb +0 -127
- data/lib/asciidoctor-pdf/measurements.rb +0 -58
- data/lib/asciidoctor-pdf/pdf-core_ext/page.rb +0 -25
- data/lib/asciidoctor-pdf/pdf-core_ext/pdf_object.rb +0 -6
- data/lib/asciidoctor-pdf/pdf-core_ext.rb +0 -2
- data/lib/asciidoctor-pdf/pdfmark.rb +0 -33
- data/lib/asciidoctor-pdf/prawn-svg_ext/interface.rb +0 -10
- data/lib/asciidoctor-pdf/prawn-table_ext/cell/asciidoc.rb +0 -69
- data/lib/asciidoctor-pdf/prawn-table_ext.rb +0 -4
- data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +0 -115
- data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +0 -904
- data/lib/asciidoctor-pdf/prawn_ext/images.rb +0 -51
- data/lib/asciidoctor-pdf/prawn_ext.rb +0 -5
- data/lib/asciidoctor-pdf/roman_numeral.rb +0 -126
- data/lib/asciidoctor-pdf/rouge_ext/formatters/prawn.rb +0 -175
- data/lib/asciidoctor-pdf/rouge_ext/themes/bw.rb +0 -38
- data/lib/asciidoctor-pdf/rouge_ext.rb +0 -4
- data/lib/asciidoctor-pdf/sanitizer.rb +0 -101
- data/lib/asciidoctor-pdf/temporary_path.rb +0 -13
- data/lib/asciidoctor-pdf/theme_loader.rb +0 -280
- data/lib/asciidoctor-pdf/ttfunk_ext.rb +0 -8
@@ -0,0 +1,922 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Prawn::Font::AFM.instance_variable_set :@hide_m17n_warning, true
|
4
|
+
|
5
|
+
require 'prawn/icon'
|
6
|
+
|
7
|
+
Prawn::Icon::Compatibility.send :prepend, (::Module.new { def warning *args; end })
|
8
|
+
|
9
|
+
module Asciidoctor
|
10
|
+
module Prawn
|
11
|
+
module Extensions
|
12
|
+
include ::Asciidoctor::PDF::Measurements
|
13
|
+
include ::Asciidoctor::PDF::Sanitizer
|
14
|
+
include ::Asciidoctor::PDF::TextTransformer
|
15
|
+
|
16
|
+
FontAwesomeIconSets = %w(fab far fas)
|
17
|
+
IconSets = %w(fab far fas fi pf).to_set
|
18
|
+
IconSetPrefixes = IconSets.map {|it| it + '-' }
|
19
|
+
InitialPageContent = %(q\n)
|
20
|
+
(FontStyleToSet = {
|
21
|
+
bold: [:bold].to_set,
|
22
|
+
italic: [:italic].to_set,
|
23
|
+
bold_italic: [:bold, :italic].to_set,
|
24
|
+
}).default = ::Set.new
|
25
|
+
# NOTE must use a visible char for placeholder or else Prawn won't reserve space for the fragment
|
26
|
+
PlaceholderChar = ?\u2063
|
27
|
+
|
28
|
+
# - :height is the height of a line
|
29
|
+
# - :leading is spacing between adjacent lines
|
30
|
+
# - :padding_top is half line spacing, plus any line_gap in the font
|
31
|
+
# - :padding_bottom is half line spacing
|
32
|
+
# - :final_gap determines whether a gap is added below the last line
|
33
|
+
LineMetrics = ::Struct.new :height, :leading, :padding_top, :padding_bottom, :final_gap
|
34
|
+
|
35
|
+
# Core
|
36
|
+
|
37
|
+
# Retrieves the catalog reference data for the PDF.
|
38
|
+
#
|
39
|
+
def catalog
|
40
|
+
state.store.root
|
41
|
+
end
|
42
|
+
|
43
|
+
# Retrieves the compatiblity version of the PDF.
|
44
|
+
#
|
45
|
+
def min_version
|
46
|
+
state.version
|
47
|
+
end
|
48
|
+
|
49
|
+
# Measurements
|
50
|
+
|
51
|
+
# Returns the width of the current page from edge-to-edge
|
52
|
+
#
|
53
|
+
def page_width
|
54
|
+
page.dimensions[2]
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the effective (writable) width of the page
|
58
|
+
#
|
59
|
+
# If inside a bounding box, returns width of box.
|
60
|
+
#
|
61
|
+
def effective_page_width
|
62
|
+
reference_bounds.width
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns the height of the current page from edge-to-edge
|
66
|
+
#
|
67
|
+
def page_height
|
68
|
+
page.dimensions[3]
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the effective (writable) height of the page
|
72
|
+
#
|
73
|
+
# If inside a fixed-height bounding box, returns width of box.
|
74
|
+
#
|
75
|
+
def effective_page_height
|
76
|
+
reference_bounds.height
|
77
|
+
end
|
78
|
+
|
79
|
+
# Set the margins for the current page.
|
80
|
+
#
|
81
|
+
def set_page_margin margin
|
82
|
+
# FIXME: is there a cleaner way to set margins? does it make sense to override create_new_page?
|
83
|
+
apply_margin_options margin: margin
|
84
|
+
generate_margin_box
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns the margins for the current page as a 4 element array (top, right, bottom, left)
|
88
|
+
#
|
89
|
+
def page_margin
|
90
|
+
[page.margins[:top], page.margins[:right], page.margins[:bottom], page.margins[:left]]
|
91
|
+
end
|
92
|
+
|
93
|
+
# Returns the width of the left margin for the current page
|
94
|
+
#
|
95
|
+
def page_margin_left
|
96
|
+
page.margins[:left]
|
97
|
+
end
|
98
|
+
# deprecated
|
99
|
+
alias left_margin page_margin_left
|
100
|
+
|
101
|
+
# Returns the width of the right margin for the current page
|
102
|
+
#
|
103
|
+
def page_margin_right
|
104
|
+
page.margins[:right]
|
105
|
+
end
|
106
|
+
# deprecated
|
107
|
+
alias right_margin page_margin_right
|
108
|
+
|
109
|
+
# Returns the width of the top margin for the current page
|
110
|
+
#
|
111
|
+
def page_margin_top
|
112
|
+
page.margins[:top]
|
113
|
+
end
|
114
|
+
|
115
|
+
# Returns the width of the bottom margin for the current page
|
116
|
+
#
|
117
|
+
def page_margin_bottom
|
118
|
+
page.margins[:bottom]
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns the total left margin (to the page edge) for the current bounds.
|
122
|
+
#
|
123
|
+
def bounds_margin_left
|
124
|
+
bounds.absolute_left
|
125
|
+
end
|
126
|
+
|
127
|
+
# Returns the total right margin (to the page edge) for the current bounds.
|
128
|
+
#
|
129
|
+
def bounds_margin_right
|
130
|
+
page.dimensions[2] - bounds.absolute_right
|
131
|
+
end
|
132
|
+
|
133
|
+
# Returns the side the current page is facing, :recto or :verso.
|
134
|
+
#
|
135
|
+
def page_side pgnum = nil, invert = nil
|
136
|
+
if invert
|
137
|
+
(recto_page? pgnum) ? :verso : :recto
|
138
|
+
else
|
139
|
+
(recto_page? pgnum) ? :recto : :verso
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Returns whether the page is a recto page.
|
144
|
+
#
|
145
|
+
def recto_page? pgnum = nil
|
146
|
+
(pgnum || page_number).odd?
|
147
|
+
end
|
148
|
+
|
149
|
+
# Returns whether the page is a verso page.
|
150
|
+
#
|
151
|
+
def verso_page? pgnum = nil
|
152
|
+
(pgnum || page_number).even?
|
153
|
+
end
|
154
|
+
|
155
|
+
# Returns whether the cursor is at the top of the page (i.e., margin box).
|
156
|
+
#
|
157
|
+
def at_page_top?
|
158
|
+
@y == @margin_box.absolute_top
|
159
|
+
end
|
160
|
+
|
161
|
+
# Returns whether the current page is the last page in the document.
|
162
|
+
#
|
163
|
+
def last_page?
|
164
|
+
page_number == page_count
|
165
|
+
end
|
166
|
+
|
167
|
+
# Destinations
|
168
|
+
|
169
|
+
# Generates a destination object that resolves to the top of the page
|
170
|
+
# specified by the page_num parameter or the current page if no page number
|
171
|
+
# is provided. The destination preserves the user's zoom level unlike
|
172
|
+
# the destinations generated by the outline builder.
|
173
|
+
#
|
174
|
+
def dest_top page_num = nil
|
175
|
+
dest_xyz 0, page_height, nil, (page_num ? state.pages[page_num - 1] : page)
|
176
|
+
end
|
177
|
+
|
178
|
+
# Fonts
|
179
|
+
|
180
|
+
# Registers a new custom font described in the data parameter
|
181
|
+
# after converting the font name to a String.
|
182
|
+
#
|
183
|
+
# Example:
|
184
|
+
#
|
185
|
+
# register_font Roboto: {
|
186
|
+
# normal: 'fonts/roboto-normal.ttf',
|
187
|
+
# italic: 'fonts/roboto-italic.ttf',
|
188
|
+
# bold: 'fonts/roboto-bold.ttf',
|
189
|
+
# bold_italic: 'fonts/roboto-bold_italic.ttf'
|
190
|
+
# }
|
191
|
+
#
|
192
|
+
def register_font data
|
193
|
+
font_families.update data.each_with_object({}) {|(key, val), accum| accum[key.to_s] = val }
|
194
|
+
end
|
195
|
+
|
196
|
+
# Enhances the built-in font method to allow the font
|
197
|
+
# size to be specified as the second option and to
|
198
|
+
# lazily load font-based icons.
|
199
|
+
#
|
200
|
+
def font name = nil, options = {}
|
201
|
+
if name
|
202
|
+
options = { size: options } if ::Numeric === options
|
203
|
+
if IconSets.include? name
|
204
|
+
::Prawn::Icon::FontData.load self, name
|
205
|
+
options = options.reject {|k| k == :style } if options.key? :style
|
206
|
+
end
|
207
|
+
end
|
208
|
+
super
|
209
|
+
end
|
210
|
+
|
211
|
+
# Retrieves the current font name (i.e., family).
|
212
|
+
#
|
213
|
+
def font_family
|
214
|
+
font.options[:family]
|
215
|
+
end
|
216
|
+
|
217
|
+
alias font_name font_family
|
218
|
+
|
219
|
+
# Retrieves the current font info (family, style, size) as a Hash
|
220
|
+
#
|
221
|
+
def font_info
|
222
|
+
{ family: font.options[:family], style: (font.options[:style] || :normal), size: @font_size }
|
223
|
+
end
|
224
|
+
|
225
|
+
# Sets the font style for the scope of the block to which this method
|
226
|
+
# yields. If the style is nil and no block is given, return the current
|
227
|
+
# font style.
|
228
|
+
#
|
229
|
+
def font_style style = nil
|
230
|
+
if block_given?
|
231
|
+
font font.options[:family], style: style do
|
232
|
+
yield
|
233
|
+
end
|
234
|
+
elsif style
|
235
|
+
font font.options[:family], style: style
|
236
|
+
else
|
237
|
+
font.options[:style] || :normal
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
# Applies points as a scale factor of the current font if the value provided
|
242
|
+
# is less than or equal to 1 or it's a string (e.g., 1.1em), then delegates to the super
|
243
|
+
# implementation to carry out the built-in functionality.
|
244
|
+
#
|
245
|
+
#--
|
246
|
+
# QUESTION should we round the result?
|
247
|
+
def font_size points = nil
|
248
|
+
return @font_size unless points
|
249
|
+
if points == 1
|
250
|
+
super @font_size
|
251
|
+
elsif String === points
|
252
|
+
if points.end_with? 'rem'
|
253
|
+
super @root_font_size * points.to_f
|
254
|
+
elsif points.end_with? 'em'
|
255
|
+
super @font_size * points.to_f
|
256
|
+
elsif points.end_with? '%'
|
257
|
+
super @font_size * (points.to_f / 100)
|
258
|
+
else
|
259
|
+
super points.to_f
|
260
|
+
end
|
261
|
+
# FIXME: HACK assume em value
|
262
|
+
elsif points < 1
|
263
|
+
super @font_size * points
|
264
|
+
else
|
265
|
+
super points
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def resolve_font_style styles
|
270
|
+
if styles.include? :bold
|
271
|
+
(styles.include? :italic) ? :bold_italic : :bold
|
272
|
+
elsif styles.include? :italic
|
273
|
+
:italic
|
274
|
+
else
|
275
|
+
:normal
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Retreives the collection of font styles from the given font style key,
|
280
|
+
# which defaults to the current font style.
|
281
|
+
#
|
282
|
+
def font_styles style = font_style
|
283
|
+
FontStyleToSet[style].dup
|
284
|
+
end
|
285
|
+
|
286
|
+
# Apply the font settings (family, size, styles and character spacing) from
|
287
|
+
# the fragment to the document, then yield to the block.
|
288
|
+
#
|
289
|
+
# The original font settings are restored before this method returns.
|
290
|
+
#
|
291
|
+
def fragment_font fragment
|
292
|
+
f_info = font_info
|
293
|
+
f_family = fragment[:font] || f_info[:family]
|
294
|
+
f_size = fragment[:size] || f_info[:size]
|
295
|
+
if (f_styles = fragment[:styles])
|
296
|
+
f_style = resolve_font_style f_styles
|
297
|
+
else
|
298
|
+
f_style = :normal
|
299
|
+
end
|
300
|
+
|
301
|
+
if (c_spacing = fragment[:character_spacing])
|
302
|
+
character_spacing c_spacing do
|
303
|
+
font f_family, size: f_size, style: f_style do
|
304
|
+
yield
|
305
|
+
end
|
306
|
+
end
|
307
|
+
else
|
308
|
+
font f_family, size: f_size, style: f_style do
|
309
|
+
yield
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# Override width of string to check for placeholder char, which uses character spacing to control width
|
315
|
+
#
|
316
|
+
def width_of_string string, options = {}
|
317
|
+
string == PlaceholderChar ? @character_spacing : super
|
318
|
+
end
|
319
|
+
|
320
|
+
def icon_font_data family
|
321
|
+
::Prawn::Icon::FontData.load self, family
|
322
|
+
end
|
323
|
+
|
324
|
+
def resolve_legacy_icon_name name
|
325
|
+
::Prawn::Icon::Compatibility::SHIMS[%(fa-#{name})]
|
326
|
+
end
|
327
|
+
|
328
|
+
def calc_line_metrics line_height = 1, font = self.font, font_size = self.font_size
|
329
|
+
line_height_length = line_height * font_size
|
330
|
+
leading = line_height_length - font_size
|
331
|
+
half_leading = leading / 2
|
332
|
+
padding_top = half_leading + font.line_gap
|
333
|
+
padding_bottom = half_leading
|
334
|
+
LineMetrics.new line_height_length, leading, padding_top, padding_bottom, false
|
335
|
+
end
|
336
|
+
|
337
|
+
=begin
|
338
|
+
# these line metrics attempted to figure out a correction based on the reported height and the font_size
|
339
|
+
# however, it only works for some fonts, and breaks down for fonts like Noto Serif
|
340
|
+
def calc_line_metrics line_height = 1, font = self.font, font_size = self.font_size
|
341
|
+
line_height_length = font_size * line_height
|
342
|
+
line_gap = line_height_length - font_size
|
343
|
+
correction = font.height - font_size
|
344
|
+
leading = line_gap - correction
|
345
|
+
shift = (font.line_gap + correction + line_gap) / 2
|
346
|
+
final_gap = font.line_gap != 0
|
347
|
+
LineMetrics.new line_height_length, leading, shift, shift, final_gap
|
348
|
+
end
|
349
|
+
=end
|
350
|
+
|
351
|
+
# Parse the text into an array of fragments using the text formatter.
|
352
|
+
def parse_text string, options = {}
|
353
|
+
return [] if string.nil?
|
354
|
+
|
355
|
+
options = options.dup
|
356
|
+
if (format_option = options.delete :inline_format)
|
357
|
+
format_option = [] unless ::Array === format_option
|
358
|
+
fragments = text_formatter.format string, *format_option
|
359
|
+
else
|
360
|
+
fragments = [text: string]
|
361
|
+
end
|
362
|
+
|
363
|
+
if (color = options.delete :color)
|
364
|
+
fragments.map do |fragment|
|
365
|
+
fragment[:color] ? fragment : fragment.merge(color: color)
|
366
|
+
end
|
367
|
+
else
|
368
|
+
fragments
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# NOTE override built-in draw_indented_formatted_line to insert leading before second line
|
373
|
+
def draw_indented_formatted_line string, opts
|
374
|
+
result = super
|
375
|
+
unless @no_text_printed || @all_text_printed
|
376
|
+
# as of Prawn 1.2.1, we have to handle the line gap after the first line manually
|
377
|
+
move_down opts[:leading]
|
378
|
+
end
|
379
|
+
result
|
380
|
+
end
|
381
|
+
|
382
|
+
# Performs the same work as Prawn::Text.text except that the first_line_opts are applied to the first line of text
|
383
|
+
# renderered. It's necessary to use low-level APIs in this method so we only style the first line and not the
|
384
|
+
# remaining lines (which is the default behavior in Prawn).
|
385
|
+
def text_with_formatted_first_line string, first_line_opts, opts
|
386
|
+
color = opts.delete :color
|
387
|
+
fragments = parse_text string, opts
|
388
|
+
# NOTE the low-level APIs we're using don't recognize the :styles option, so we must resolve
|
389
|
+
if (styles = opts.delete :styles)
|
390
|
+
opts[:style] = resolve_font_style styles
|
391
|
+
end
|
392
|
+
if (first_line_styles = first_line_opts.delete :styles)
|
393
|
+
first_line_opts[:style] = resolve_font_style first_line_styles
|
394
|
+
end
|
395
|
+
first_line_color = (first_line_opts.delete :color) || color
|
396
|
+
opts = opts.merge document: self
|
397
|
+
# QUESTION should we merge more carefully here? (hand-select keys?)
|
398
|
+
first_line_opts = opts.merge(first_line_opts).merge single_line: true
|
399
|
+
box = ::Prawn::Text::Formatted::Box.new fragments, first_line_opts
|
400
|
+
# NOTE get remaining_fragments before we add color to fragments on first line
|
401
|
+
if (text_indent = opts.delete :indent_paragraphs)
|
402
|
+
remaining_fragments = indent text_indent do
|
403
|
+
box.render dry_run: true
|
404
|
+
end
|
405
|
+
else
|
406
|
+
remaining_fragments = box.render dry_run: true
|
407
|
+
end
|
408
|
+
# NOTE color must be applied per-fragment
|
409
|
+
fragments.each {|fragment| fragment[:color] ||= first_line_color } if first_line_color
|
410
|
+
if text_indent
|
411
|
+
indent text_indent do
|
412
|
+
fill_formatted_text_box fragments, first_line_opts
|
413
|
+
end
|
414
|
+
else
|
415
|
+
fill_formatted_text_box fragments, first_line_opts
|
416
|
+
end
|
417
|
+
unless remaining_fragments.empty?
|
418
|
+
# NOTE color must be applied per-fragment
|
419
|
+
remaining_fragments.each {|fragment| fragment[:color] ||= color } if color
|
420
|
+
# as of Prawn 1.2.1, we have to handle the line gap after the first line manually
|
421
|
+
move_down opts[:leading]
|
422
|
+
remaining_fragments = fill_formatted_text_box remaining_fragments, opts
|
423
|
+
draw_remaining_formatted_text_on_new_pages remaining_fragments, opts
|
424
|
+
end
|
425
|
+
end
|
426
|
+
|
427
|
+
# Apply the text transform to the specified text.
|
428
|
+
#
|
429
|
+
# Supported transform values are "uppercase", "lowercase", or "none" (passed
|
430
|
+
# as either a String or a Symbol). When the uppercase transform is applied to
|
431
|
+
# the text, it correctly uppercases visible text while leaving markup and
|
432
|
+
# named character entities unchanged. The none transform returns the text
|
433
|
+
# unmodified.
|
434
|
+
#
|
435
|
+
def transform_text text, transform
|
436
|
+
case transform
|
437
|
+
when :uppercase, 'uppercase'
|
438
|
+
uppercase_pcdata text
|
439
|
+
when :lowercase, 'lowercase'
|
440
|
+
lowercase_pcdata text
|
441
|
+
when :capitalize, 'capitalize'
|
442
|
+
capitalize_words_pcdata text
|
443
|
+
else
|
444
|
+
text
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
def hyphenate_text text, hyphenator
|
449
|
+
hyphenate_words_pcdata text, hyphenator
|
450
|
+
end
|
451
|
+
|
452
|
+
# Cursor
|
453
|
+
|
454
|
+
# Short-circuits the call to the built-in move_up operation
|
455
|
+
# when n is 0.
|
456
|
+
#
|
457
|
+
def move_up n
|
458
|
+
super unless n == 0
|
459
|
+
end
|
460
|
+
|
461
|
+
# Override built-in move_text_position method to prevent Prawn from advancing
|
462
|
+
# to next page if image doesn't fit before rendering image.
|
463
|
+
#--
|
464
|
+
# NOTE could use :at option when calling image/embed_image instead
|
465
|
+
def move_text_position h; end
|
466
|
+
|
467
|
+
# Short-circuits the call to the built-in move_down operation
|
468
|
+
# when n is 0.
|
469
|
+
#
|
470
|
+
def move_down n
|
471
|
+
super unless n == 0
|
472
|
+
end
|
473
|
+
|
474
|
+
# Bounds
|
475
|
+
|
476
|
+
# Overrides the built-in pad operation to allow for asymmetric paddings.
|
477
|
+
#
|
478
|
+
# Example:
|
479
|
+
#
|
480
|
+
# pad 20, 10 do
|
481
|
+
# text 'A paragraph with twice as much top padding as bottom padding.'
|
482
|
+
# end
|
483
|
+
#
|
484
|
+
def pad top, bottom = nil
|
485
|
+
move_down top
|
486
|
+
yield
|
487
|
+
move_down(bottom || top)
|
488
|
+
end
|
489
|
+
|
490
|
+
# Combines the built-in pad and indent operations into a single method.
|
491
|
+
#
|
492
|
+
# Padding may be specified as an array of four values, or as a single value.
|
493
|
+
# The single value is used as the padding around all four sides of the box.
|
494
|
+
#
|
495
|
+
# If padding is nil, this method simply yields to the block and returns.
|
496
|
+
#
|
497
|
+
# Example:
|
498
|
+
#
|
499
|
+
# pad_box 20 do
|
500
|
+
# text 'A paragraph inside a blox with even padding on all sides.'
|
501
|
+
# end
|
502
|
+
#
|
503
|
+
# pad_box [10, 10, 10, 20] do
|
504
|
+
# text 'An indented paragraph inside a box with equal padding on all sides.'
|
505
|
+
# end
|
506
|
+
#
|
507
|
+
def pad_box padding
|
508
|
+
if padding
|
509
|
+
# TODO: implement shorthand combinations like in CSS
|
510
|
+
p_top, p_right, p_bottom, p_left = ::Array === padding ? padding : (::Array.new 4, padding)
|
511
|
+
begin
|
512
|
+
# logic is intentionally inlined
|
513
|
+
move_down p_top
|
514
|
+
bounds.add_left_padding p_left
|
515
|
+
bounds.add_right_padding p_right
|
516
|
+
yield
|
517
|
+
# NOTE support negative bottom padding for use with quote block
|
518
|
+
if p_bottom < 0
|
519
|
+
# QUESTION should we return to previous page if top of page is reached?
|
520
|
+
p_bottom < cursor - reference_bounds.top ? (move_cursor_to reference_bounds.top) : (move_down p_bottom)
|
521
|
+
else
|
522
|
+
p_bottom < cursor ? (move_down p_bottom) : reference_bounds.move_past_bottom
|
523
|
+
end
|
524
|
+
ensure
|
525
|
+
bounds.subtract_left_padding p_left
|
526
|
+
bounds.subtract_right_padding p_right
|
527
|
+
end
|
528
|
+
else
|
529
|
+
yield
|
530
|
+
end
|
531
|
+
|
532
|
+
# alternate, delegated logic
|
533
|
+
#pad padding[0], padding[2] do
|
534
|
+
# indent padding[1], padding[3] do
|
535
|
+
# yield
|
536
|
+
# end
|
537
|
+
#end
|
538
|
+
end
|
539
|
+
|
540
|
+
def inflate_indent value
|
541
|
+
(::Array === value ? (value.slice 0, 2) : (::Array.new 2, value)).map(&:to_f)
|
542
|
+
end
|
543
|
+
|
544
|
+
# TODO: memoize the result
|
545
|
+
def inflate_padding padding
|
546
|
+
padding = [*(padding || 0)].slice 0, 4
|
547
|
+
case padding.size
|
548
|
+
when 1
|
549
|
+
[padding[0], padding[0], padding[0], padding[0]]
|
550
|
+
when 2
|
551
|
+
[padding[0], padding[1], padding[0], padding[1]]
|
552
|
+
when 3
|
553
|
+
[padding[0], padding[1], padding[2], padding[1]]
|
554
|
+
else
|
555
|
+
padding
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
# Stretch the current bounds to the left and right edges of the current page
|
560
|
+
# while yielding the specified block if the verdict argument is true.
|
561
|
+
# Otherwise, simply yield the specified block.
|
562
|
+
#
|
563
|
+
def span_page_width_if verdict
|
564
|
+
if verdict
|
565
|
+
indent(-bounds_margin_left, -bounds_margin_right) do
|
566
|
+
yield
|
567
|
+
end
|
568
|
+
else
|
569
|
+
yield
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
# A flowing version of the bounding_box. If the content runs to another page, the cursor starts
|
574
|
+
# at the top of the page instead of the original cursor position. Similar to span, except
|
575
|
+
# you can specify an absolute left position and pass additional options through to bounding_box.
|
576
|
+
#
|
577
|
+
def flow_bounding_box left = 0, opts = {}
|
578
|
+
original_y = y
|
579
|
+
# QUESTION should preserving original_x be an option?
|
580
|
+
original_x = bounds.absolute_left - margin_box.absolute_left
|
581
|
+
canvas do
|
582
|
+
bounding_box [margin_box.absolute_left + original_x + left, margin_box.absolute_top], opts do
|
583
|
+
self.y = original_y
|
584
|
+
yield
|
585
|
+
end
|
586
|
+
end
|
587
|
+
end
|
588
|
+
|
589
|
+
# Graphics
|
590
|
+
|
591
|
+
# Fills the current bounding box with the specified fill color. Before
|
592
|
+
# returning from this method, the original fill color on the document is
|
593
|
+
# restored.
|
594
|
+
def fill_bounds f_color = fill_color
|
595
|
+
if f_color && f_color != 'transparent'
|
596
|
+
prev_fill_color = fill_color
|
597
|
+
fill_color f_color
|
598
|
+
fill_rectangle bounds.top_left, bounds.width, bounds.height
|
599
|
+
fill_color prev_fill_color
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
# Fills the absolute bounding box with the specified fill color. Before
|
604
|
+
# returning from this method, the original fill color on the document is
|
605
|
+
# restored.
|
606
|
+
def fill_absolute_bounds f_color = fill_color
|
607
|
+
canvas { fill_bounds f_color }
|
608
|
+
end
|
609
|
+
|
610
|
+
# Fills the current bounds using the specified fill color and strokes the
|
611
|
+
# bounds using the specified stroke color. Sets the line with if specified
|
612
|
+
# in the options. Before returning from this method, the original fill
|
613
|
+
# color, stroke color and line width on the document are restored.
|
614
|
+
#
|
615
|
+
def fill_and_stroke_bounds f_color = fill_color, s_color = stroke_color, options = {}
|
616
|
+
no_fill = !f_color || f_color == 'transparent'
|
617
|
+
no_stroke = !s_color || s_color == 'transparent' || options[:line_width] == 0
|
618
|
+
return if no_fill && no_stroke
|
619
|
+
save_graphics_state do
|
620
|
+
radius = options[:radius] || 0
|
621
|
+
|
622
|
+
# fill
|
623
|
+
unless no_fill
|
624
|
+
fill_color f_color
|
625
|
+
fill_rounded_rectangle bounds.top_left, bounds.width, bounds.height, radius
|
626
|
+
end
|
627
|
+
|
628
|
+
# stroke
|
629
|
+
unless no_stroke
|
630
|
+
stroke_color s_color
|
631
|
+
line_width(options[:line_width] || 0.5)
|
632
|
+
# FIXME: think about best way to indicate dashed borders
|
633
|
+
#if options.has_key? :dash_width
|
634
|
+
# dash options[:dash_width], space: options[:dash_space] || 1
|
635
|
+
#end
|
636
|
+
stroke_rounded_rectangle bounds.top_left, bounds.width, bounds.height, radius
|
637
|
+
#undash if options.has_key? :dash_width
|
638
|
+
end
|
639
|
+
end
|
640
|
+
end
|
641
|
+
|
642
|
+
# Fills and, optionally, strokes the current bounds using the fill and
|
643
|
+
# stroke color specified, then yields to the block. The only_if option can
|
644
|
+
# be used to conditionally disable this behavior.
|
645
|
+
#
|
646
|
+
def shade_box color, line_color = nil, options = {}
|
647
|
+
if (!options.key? :only_if) || options[:only_if]
|
648
|
+
# FIXME: could use save_graphics_state here
|
649
|
+
previous_fill_color = current_fill_color
|
650
|
+
fill_color color
|
651
|
+
fill_rectangle [bounds.left, bounds.top], bounds.right, bounds.top - bounds.bottom
|
652
|
+
fill_color previous_fill_color
|
653
|
+
if line_color
|
654
|
+
line_width 0.5
|
655
|
+
previous_stroke_color = current_stroke_color
|
656
|
+
stroke_color line_color
|
657
|
+
stroke_bounds
|
658
|
+
stroke_color previous_stroke_color
|
659
|
+
end
|
660
|
+
end
|
661
|
+
yield
|
662
|
+
end
|
663
|
+
|
664
|
+
# Strokes a horizontal line using the current bounds. The width of the line
|
665
|
+
# can be specified using the line_width option. The offset from the cursor
|
666
|
+
# can be set using the at option.
|
667
|
+
#
|
668
|
+
def stroke_horizontal_rule rule_color = stroke_color, options = {}
|
669
|
+
rule_y = cursor - (options[:at] || 0)
|
670
|
+
rule_style = options[:line_style]
|
671
|
+
rule_width = options[:line_width] || 0.5
|
672
|
+
rule_x_start = bounds.left
|
673
|
+
rule_x_end = bounds.right
|
674
|
+
rule_inked = false
|
675
|
+
save_graphics_state do
|
676
|
+
line_width rule_width
|
677
|
+
stroke_color rule_color
|
678
|
+
case rule_style
|
679
|
+
when :dashed
|
680
|
+
dash rule_width * 4
|
681
|
+
when :dotted
|
682
|
+
dash rule_width
|
683
|
+
when :double
|
684
|
+
stroke_horizontal_line rule_x_start, rule_x_end, at: (rule_y + rule_width)
|
685
|
+
stroke_horizontal_line rule_x_start, rule_x_end, at: (rule_y - rule_width)
|
686
|
+
rule_inked = true
|
687
|
+
end if rule_style
|
688
|
+
stroke_horizontal_line rule_x_start, rule_x_end, at: rule_y unless rule_inked
|
689
|
+
end
|
690
|
+
end
|
691
|
+
|
692
|
+
# A compliment to the stroke_horizontal_rule method, strokes a
|
693
|
+
# vertical line using the current bounds. The width of the line
|
694
|
+
# can be specified using the line_width option. The horizontal (x)
|
695
|
+
# position can be specified using the at option.
|
696
|
+
#
|
697
|
+
def stroke_vertical_rule rule_color = stroke_color, options = {}
|
698
|
+
rule_x = options[:at] || 0
|
699
|
+
rule_y_from = bounds.top
|
700
|
+
rule_y_to = bounds.bottom
|
701
|
+
rule_style = options[:line_style]
|
702
|
+
rule_width = options[:line_width] || 0.5
|
703
|
+
save_graphics_state do
|
704
|
+
line_width rule_width
|
705
|
+
stroke_color rule_color
|
706
|
+
case rule_style
|
707
|
+
when :dashed
|
708
|
+
dash rule_width * 4
|
709
|
+
when :dotted
|
710
|
+
dash rule_width
|
711
|
+
when :double
|
712
|
+
stroke_vertical_line rule_y_from, rule_y_to, at: (rule_x - rule_width)
|
713
|
+
rule_x += rule_width
|
714
|
+
end if rule_style
|
715
|
+
stroke_vertical_line rule_y_from, rule_y_to, at: rule_x
|
716
|
+
end
|
717
|
+
end
|
718
|
+
|
719
|
+
# Pages
|
720
|
+
|
721
|
+
# Deletes the current page and move the cursor
|
722
|
+
# to the previous page.
|
723
|
+
def delete_page
|
724
|
+
pg = page_number
|
725
|
+
pdf_store = state.store
|
726
|
+
pdf_objs = pdf_store.instance_variable_get :@objects
|
727
|
+
pdf_ids = pdf_store.instance_variable_get :@identifiers
|
728
|
+
page_id = pdf_store.object_id_for_page pg
|
729
|
+
content_id = page.content.identifier
|
730
|
+
[page_id, content_id].each do |key|
|
731
|
+
pdf_objs.delete key
|
732
|
+
pdf_ids.delete key
|
733
|
+
end
|
734
|
+
pdf_store.pages.data[:Kids].pop
|
735
|
+
pdf_store.pages.data[:Count] -= 1
|
736
|
+
state.pages.pop
|
737
|
+
if pg > 1
|
738
|
+
go_to_page pg - 1
|
739
|
+
else
|
740
|
+
@page_number = 0
|
741
|
+
state.page = nil
|
742
|
+
end
|
743
|
+
end
|
744
|
+
|
745
|
+
# Import the specified page into the current document.
|
746
|
+
#
|
747
|
+
# By default, advance to the next page afterwards, creating it if necessary.
|
748
|
+
# This behavior can be disabled by passing the option `advance: false`.
|
749
|
+
# However, due to how page creation works in Prawn, understand that advancing
|
750
|
+
# to the next page is necessary to prevent the size & layout of the imported
|
751
|
+
# page from affecting a newly created page.
|
752
|
+
def import_page file, opts = {}
|
753
|
+
prev_page_layout = page.layout
|
754
|
+
prev_page_size = page.size
|
755
|
+
state.compress = false if state.compress # can't use compression if using template
|
756
|
+
prev_text_rendering_mode = (defined? @text_rendering_mode) ? @text_rendering_mode : nil
|
757
|
+
delete_page if opts[:replace]
|
758
|
+
# NOTE use functionality provided by prawn-templates
|
759
|
+
start_new_page_discretely template: file, template_page: opts[:page]
|
760
|
+
# prawn-templates sets text_rendering_mode to :unknown, which breaks running content; revert
|
761
|
+
@text_rendering_mode = prev_text_rendering_mode
|
762
|
+
yield if block_given?
|
763
|
+
if opts.fetch :advance, true
|
764
|
+
# NOTE set page size & layout explicitly in case imported page differs
|
765
|
+
# I'm not sure it's right to start a new page here, but unfortunately there's no other
|
766
|
+
# way atm to prevent the size & layout of the imported page from affecting subsequent pages
|
767
|
+
advance_page size: prev_page_size, layout: prev_page_layout
|
768
|
+
end
|
769
|
+
nil
|
770
|
+
end
|
771
|
+
|
772
|
+
# Create a new page for the specified image. If the canvas option is true,
|
773
|
+
# the image is positioned relative to the boundaries of the page.
|
774
|
+
def image_page file, options = {}
|
775
|
+
start_new_page_discretely
|
776
|
+
image_page_number = page_number
|
777
|
+
if options.delete :canvas
|
778
|
+
canvas { image file, ({ position: :center, vposition: :center }.merge options) }
|
779
|
+
else
|
780
|
+
image file, (options.merge position: :center, vposition: :center, fit: [bounds.width, bounds.height])
|
781
|
+
end
|
782
|
+
# NOTE advance to newly created page just in case the image function threw off the cursor
|
783
|
+
go_to_page image_page_number
|
784
|
+
nil
|
785
|
+
end
|
786
|
+
|
787
|
+
# Perform an operation (such as creating a new page) without triggering the on_page_create callback
|
788
|
+
#
|
789
|
+
def perform_discretely
|
790
|
+
if (saved_callback = state.on_page_create_callback)
|
791
|
+
# equivalent to calling `on_page_create`
|
792
|
+
state.on_page_create_callback = nil
|
793
|
+
yield
|
794
|
+
# equivalent to calling `on_page_create &saved_callback`
|
795
|
+
state.on_page_create_callback = saved_callback
|
796
|
+
else
|
797
|
+
yield
|
798
|
+
end
|
799
|
+
end
|
800
|
+
|
801
|
+
# This method is a smarter version of start_new_page. It calls start_new_page
|
802
|
+
# if the current page is the last page of the document. Otherwise, it simply
|
803
|
+
# advances to the next existing page.
|
804
|
+
def advance_page opts = {}
|
805
|
+
last_page? ? (start_new_page opts) : (go_to_page page_number + 1)
|
806
|
+
end
|
807
|
+
|
808
|
+
# Start a new page without triggering the on_page_create callback
|
809
|
+
#
|
810
|
+
def start_new_page_discretely options = {}
|
811
|
+
perform_discretely { start_new_page options }
|
812
|
+
end
|
813
|
+
|
814
|
+
# Grouping
|
815
|
+
|
816
|
+
# Conditional group operation
|
817
|
+
#
|
818
|
+
def group_if verdict
|
819
|
+
if verdict
|
820
|
+
state.optimize_objects = false # optimize_objects breaks group
|
821
|
+
group { yield }
|
822
|
+
else
|
823
|
+
yield
|
824
|
+
end
|
825
|
+
end
|
826
|
+
|
827
|
+
def get_scratch_document
|
828
|
+
# marshal if not using transaction feature
|
829
|
+
#Marshal.load Marshal.dump @prototype
|
830
|
+
|
831
|
+
# use cached instance, tests show it's faster
|
832
|
+
#@prototype ||= ::Prawn::Document.new
|
833
|
+
@scratch ||= if defined? @prototype # rubocop:disable Naming/MemoizedInstanceVariableName
|
834
|
+
scratch = Marshal.load Marshal.dump @prototype
|
835
|
+
scratch.instance_variable_set :@prototype, @prototype
|
836
|
+
scratch.instance_variable_set :@tmp_files, @tmp_files
|
837
|
+
# TODO: set scratch number on scratch document
|
838
|
+
scratch
|
839
|
+
else
|
840
|
+
logger.warn 'no scratch prototype available; instantiating fresh scratch document'
|
841
|
+
::Prawn::Document.new
|
842
|
+
end
|
843
|
+
end
|
844
|
+
|
845
|
+
def scratch?
|
846
|
+
(@_label ||= (state.store.info.data[:Scratch] ? :scratch : :primary)) == :scratch
|
847
|
+
rescue
|
848
|
+
false # NOTE this method may get called before the state is initialized
|
849
|
+
end
|
850
|
+
alias is_scratch? scratch?
|
851
|
+
|
852
|
+
def dry_run &block
|
853
|
+
scratch = get_scratch_document
|
854
|
+
# QUESTION should we use scratch.advance_page instead?
|
855
|
+
scratch.start_new_page
|
856
|
+
start_page_number = scratch.page_number
|
857
|
+
start_y = scratch.y
|
858
|
+
scratch_bounds = scratch.bounds
|
859
|
+
original_x = scratch_bounds.absolute_left
|
860
|
+
original_width = scratch_bounds.width
|
861
|
+
scratch_bounds.instance_variable_set :@x, bounds.absolute_left
|
862
|
+
scratch_bounds.instance_variable_set :@width, bounds.width
|
863
|
+
scratch.font font_family, style: font_style, size: font_size do
|
864
|
+
scratch.instance_exec(&block)
|
865
|
+
end
|
866
|
+
# NOTE don't count excess if cursor exceeds writable area (due to padding)
|
867
|
+
full_page_height = scratch.effective_page_height
|
868
|
+
partial_page_height = [full_page_height, start_y - scratch.y].min
|
869
|
+
scratch_bounds.instance_variable_set :@x, original_x
|
870
|
+
scratch_bounds.instance_variable_set :@width, original_width
|
871
|
+
whole_pages = scratch.page_number - start_page_number
|
872
|
+
[(whole_pages * full_page_height + partial_page_height), whole_pages, partial_page_height]
|
873
|
+
end
|
874
|
+
|
875
|
+
# Attempt to keep the objects generated in the block on the same page
|
876
|
+
#
|
877
|
+
# TODO: short-circuit nested usage
|
878
|
+
def keep_together &block
|
879
|
+
available_space = cursor
|
880
|
+
total_height, = dry_run(&block)
|
881
|
+
# NOTE technically, if we're at the page top, we don't even need to do the
|
882
|
+
# dry run, except several uses of this method rely on the calculated height
|
883
|
+
if total_height > available_space && !at_page_top? && total_height <= effective_page_height
|
884
|
+
advance_page
|
885
|
+
started_new_page = true
|
886
|
+
else
|
887
|
+
started_new_page = false
|
888
|
+
end
|
889
|
+
|
890
|
+
# HACK: yield doesn't work here on JRuby (at least not when called from AsciidoctorJ)
|
891
|
+
#yield remainder, started_new_page
|
892
|
+
instance_exec(total_height, started_new_page, &block)
|
893
|
+
end
|
894
|
+
|
895
|
+
# Attempt to keep the objects generated in the block on the same page
|
896
|
+
# if the verdict parameter is true.
|
897
|
+
#
|
898
|
+
def keep_together_if verdict, &block
|
899
|
+
if verdict
|
900
|
+
keep_together(&block)
|
901
|
+
else
|
902
|
+
yield
|
903
|
+
end
|
904
|
+
end
|
905
|
+
|
906
|
+
=begin
|
907
|
+
def run_with_trial &block
|
908
|
+
available_space = cursor
|
909
|
+
total_height, whole_pages, remainder = dry_run(&block)
|
910
|
+
if whole_pages > 0 || remainder > available_space
|
911
|
+
started_new_page = true
|
912
|
+
else
|
913
|
+
started_new_page = false
|
914
|
+
end
|
915
|
+
# HACK yield doesn't work here on JRuby (at least not when called from AsciidoctorJ)
|
916
|
+
#yield remainder, started_new_page
|
917
|
+
instance_exec(remainder, started_new_page, &block)
|
918
|
+
end
|
919
|
+
=end
|
920
|
+
end
|
921
|
+
end
|
922
|
+
end
|