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