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