deep-cover 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 00a1a34b5a290562fb05bfeb843246435583938dd7c27a2bce393a2842f98263
4
- data.tar.gz: 7a051b58218d644f64b3b54b39bbe73162d09971673b31c27e9a57153155b703
3
+ metadata.gz: 621b28ff48f341de43e6968478e56ad4a19f3fd5857938589a70a811aaa5fbc0
4
+ data.tar.gz: 96188d3620f46b1b84013ea31fb74238baa5096b91d6674312addff99d36a665
5
5
  SHA512:
6
- metadata.gz: 01f05fb8adac63000082f7dc1a868470be5fd547f92d21dc6e726e4f3d069dff0ae896633c1de071e3bae7c7324cf7501866d0255e94de5fb7280f0cd2b4b51e
7
- data.tar.gz: 1ee9a7394325b8f098da4cc7e7f1beb8681a6142d458aed51ecf14a9b9033d4afd3b251a6d4684ff5f4a2cf5eb97ccc734350631ad73a9ab755a4d0e9cb9d19e
6
+ metadata.gz: 3cd1be3fb4a64e36728781248c10cc3f199a00151ae08b0aec33a9f35914981deaa9fbc22f53f5084cdb19d67f8ab35530f469fe8ca858983fdb8d67379d1f2b
7
+ data.tar.gz: c26c90179da7bad93c2fe605b4c231221c8cb2d1b136aa17253c90cb1319b9dcd5b91aa53744e98f5fa9cc12dd8e5263fa7f657885aef830ee66c982cc092996
data/.pryrc ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH << './lib' << './core_gem/lib'
4
+ require 'deep_cover'
5
+
6
+ Pry.config.hooks.add_hook(:when_started, :set_context) do |binding, options, pry|
7
+ if binding.eval('self').class == Object # true when starting `pry`
8
+ # false when called from binding.pry
9
+ pry.input = StringIO.new('cd DeepCover')
10
+ end
11
+ end
@@ -6,6 +6,7 @@ AllCops:
6
6
  Exclude:
7
7
  - '**/spec/char_cover/**/*'
8
8
  - '**/spec/code_fixtures/**/*'
9
+ - '**/profile/fixtures/**/*'
9
10
  - '**/spec/samples/**/*'
10
11
  - '**/spec/specs_tools.rb'
11
12
  - 'bin/**/*'
@@ -16,7 +17,7 @@ Gemspec/RequiredRubyVersion:
16
17
  # Yes, we still support ruby 2.1...
17
18
  Enabled: false
18
19
 
19
- Layout/AlignHash:
20
+ Layout/HashAlignment:
20
21
  Enabled: false
21
22
 
22
23
  Layout/ClosingParenthesisIndentation:
@@ -34,16 +35,16 @@ Layout/EmptyLines:
34
35
  Layout/EmptyLineBetweenDefs:
35
36
  NumberOfEmptyLines: [1, 2]
36
37
 
37
- Layout/IndentFirstArgument:
38
+ Layout/FirstArgumentIndentation:
38
39
  IndentationWidth: 4
39
40
 
40
- Layout/IndentFirstArrayElement:
41
+ Layout/FirstArrayElementIndentation:
41
42
  EnforcedStyle: align_brackets
42
43
 
43
- Layout/IndentFirstHashElement:
44
+ Layout/FirstHashElementIndentation:
44
45
  EnforcedStyle: align_braces
45
46
 
46
- Layout/IndentHeredoc:
47
+ Layout/HeredocIndentation:
47
48
  Enabled: false
48
49
 
49
50
  Layout/MultilineArrayBraceLayout:
@@ -79,7 +80,7 @@ Metrics/CyclomaticComplexity:
79
80
  Enabled: false
80
81
 
81
82
  # Really, you aim for less than that, but we won't bug you unless you reach 150
82
- Metrics/LineLength:
83
+ Layout/LineLength:
83
84
  IgnoreCopDirectives: true
84
85
  Max: 150
85
86
 
@@ -175,7 +176,8 @@ Style/GlobalVars:
175
176
  Enabled: false
176
177
 
177
178
  Style/FormatStringToken:
178
- EnforcedStyle: template
179
+ Enabled: false
180
+ # EnforcedStyle: template
179
181
 
180
182
  Style/MutableConstant:
181
183
  Enabled: false # TODO: - Remove me one asap
@@ -187,9 +189,9 @@ Naming/PredicateName:
187
189
  NamePrefix:
188
190
  - is_
189
191
  - have_
190
- NamePrefixBlacklist:
192
+ ForbiddenPrefixes:
191
193
  - have_
192
- NameWhitelist:
194
+ AllowedMethods:
193
195
  - is_statement
194
196
  - is_child_statement
195
197
 
@@ -229,6 +231,21 @@ Style/NilComparison:
229
231
  Style/NonNilCheck:
230
232
  Enabled: false
231
233
 
234
+ Style/HashTransformKeys:
235
+ Enabled: true
236
+
237
+ Style/HashTransformValues:
238
+ Enabled: true
239
+
240
+ Style/HashEachMethods:
241
+ Enabled: true
242
+
243
+ Lint/StructNewOverride:
244
+ Enabled: true
245
+
246
+ Lint/RaiseException:
247
+ Enabled: true
248
+
232
249
  Lint/EmptyWhen:
233
250
  Enabled: false
234
251
 
@@ -273,7 +290,7 @@ Gemspec/RubyVersionGlobalsUsage:
273
290
  Lint/BooleanSymbol:
274
291
  Enabled: false
275
292
 
276
- Naming/UncommunicativeMethodParamName:
293
+ Naming/MethodParameterName:
277
294
  Exclude:
278
295
  - core_gem/spec/**/*
279
296
 
@@ -2,6 +2,7 @@ sudo: false
2
2
  language: ruby
3
3
  rvm:
4
4
  - ruby-head
5
+ - 2.7.1
5
6
  - 2.6.0
6
7
  - 2.5.0
7
8
  - 2.4.1
data/README.md CHANGED
@@ -80,7 +80,7 @@ Do the appropriate of the installation of the gem, then follow the steps that co
80
80
  spec.add_development_dependency 'deep-cover', '~> 0.7'
81
81
 
82
82
  # otherwise if using a Gemfile, add this to it and then run `bundle install`
83
- gem 'deep-cover', '~> 0.7', group: :test, require: false
83
+ gem 'deep-cover', '~> 0.7', group: :test
84
84
 
85
85
  # otherwise just run:
86
86
  gem install deep-cover
@@ -106,6 +106,9 @@ Note, this is a bit slower and may cause issues in your tests if your use relati
106
106
 
107
107
  Typically, you want to insert that line **at the very top** of `test/test_helper.rb` or `spec/spec_helper.rb` . If `deep-cover` is required after your code, then it won't be able to detect the coverage.
108
108
 
109
+ Note that if some of your tests run by launching another process, that process will have to `require 'deep-cover'` also. For example you could insert `require 'deep-cover' if ENV['DEEP_COVER']` at the beginning of `lib/my_awesome_gem.rb`, before all the `require_relative 'my_awesome_gem/fabulous_core_part_1'`, ...
110
+ Note that the environment variable `DEEP_COVER` is set by `deep-cover exec` or `DeepCover.start`.
111
+
109
112
  2. Create a config file (optional)
110
113
 
111
114
  You may want to create a config file `.deep_cover.rb` at the root of your project, where you can set the config as you wish.
@@ -40,7 +40,7 @@ Gem::Specification.new do |spec|
40
40
  # About every single release breaks something
41
41
  # Ruby 2.1 is no longer supported
42
42
  if RUBY_VERSION >= '2.3.0'
43
- spec.add_development_dependency 'rubocop', '~> 0.74.0'
43
+ spec.add_development_dependency 'rubocop', '~> 0.81.0'
44
44
  spec.add_development_dependency 'rubocop-performance'
45
45
  end
46
46
  end
@@ -0,0 +1,4537 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adapted from: https://github.com/asciidoctor/asciidoctor-pdf/blob/master/lib/asciidoctor/pdf/converter.rb
4
+ if true
5
+ module Asciidoctor
6
+ module Converter; end
7
+ module Logging; end
8
+ module Writer; end
9
+ module Prawn
10
+ module Extensions
11
+ end
12
+ end
13
+ end
14
+ module Prawn
15
+ class Document
16
+ def self.register_for(*)
17
+ end
18
+ end
19
+ end
20
+ else
21
+ require_relative 'formatted_text'
22
+ require_relative 'index_catalog'
23
+ require_relative 'pdfmark'
24
+ require_relative 'roman_numeral'
25
+ require_relative 'section_info_by_page'
26
+ end
27
+
28
+ autoload :StringIO, 'stringio'
29
+ autoload :Tempfile, 'tempfile'
30
+
31
+ module Asciidoctor
32
+ module PDF
33
+ class Converter < ::Prawn::Document
34
+ include ::Asciidoctor::Converter
35
+ include ::Asciidoctor::Logging
36
+ include ::Asciidoctor::Writer
37
+ include ::Asciidoctor::Prawn::Extensions
38
+
39
+ register_for 'pdf'
40
+
41
+ attr_reader :allow_uri_read
42
+
43
+ attr_reader :cache_uri
44
+
45
+ attr_reader :theme
46
+
47
+ attr_reader :text_decoration_width
48
+
49
+ # NOTE require_library doesn't support require_relative and we don't modify the load path for this gem
50
+ CodeRayRequirePath = ::File.join __dir__, 'ext/prawn/coderay_encoder'
51
+ RougeRequirePath = ::File.join __dir__, 'ext/rouge'
52
+ PygmentsRequirePath = ::File.join __dir__, 'ext/pygments'
53
+ OptimizerRequirePath = ::File.join __dir__, 'optimizer'
54
+
55
+ AdmonitionIcons = {
56
+ caution: { name: 'fas-fire', stroke_color: 'BF3400', size: 24 },
57
+ important: { name: 'fas-exclamation-circle', stroke_color: 'BF0000', size: 24 },
58
+ note: { name: 'fas-info-circle', stroke_color: '19407C', size: 24 },
59
+ tip: { name: 'far-lightbulb', stroke_color: '111111', size: 24 },
60
+ warning: { name: 'fas-exclamation-triangle', stroke_color: 'BF6900', size: 24 },
61
+ }
62
+ TextAlignmentNames = %w(justify left center right)
63
+ TextAlignmentRoles = %w(text-justify text-left text-center text-right)
64
+ TextDecorationStyleTable = { 'underline' => :underline, 'line-through' => :strikethrough }
65
+ FontKerningTable = { 'normal' => true, 'none' => false }
66
+ BlockAlignmentNames = %w(left center right)
67
+ AlignmentTable = { '<' => :left, '=' => :center, '>' => :right }
68
+ ColumnPositions = [:left, :center, :right]
69
+ PageLayouts = [:portrait, :landscape]
70
+ (PageModes = {
71
+ 'fullscreen' => [:FullScreen, :UseOutlines],
72
+ 'fullscreen none' => [:FullScreen, :UseNone],
73
+ 'fullscreen outline' => [:FullScreen, :UseOutlines],
74
+ 'fullscreen thumbs' => [:FullScreen, :UseThumbs],
75
+ 'none' => :UseNone,
76
+ 'outline' => :UseOutlines,
77
+ 'thumbs' => :UseThumbs,
78
+ }).default = :UseOutlines
79
+ PageSides = [:recto, :verso]
80
+ (PDFVersions = { '1.3' => 1.3, '1.4' => 1.4, '1.5' => 1.5, '1.6' => 1.6, '1.7' => 1.7 }).default = 1.4
81
+ AuthorAttributeNames = %w(author authorinitials firstname middlename lastname email)
82
+ LF = ?\n
83
+ DoubleLF = LF * 2
84
+ TAB = ?\t
85
+ InnerIndent = LF + ' '
86
+ # a no-break space is used to replace a leading space to prevent Prawn from trimming indentation
87
+ # a leading zero-width space can't be used as it gets dropped when calculating the line width
88
+ GuardedIndent = ?\u00a0
89
+ GuardedInnerIndent = LF + GuardedIndent
90
+ TabRx = /\t/
91
+ TabIndentRx = /^\t+/
92
+ NoBreakSpace = ?\u00a0
93
+ ZeroWidthSpace = ?\u200b
94
+ DummyText = ?\u0000
95
+ DotLeaderTextDefault = '. '
96
+ EmDash = ?\u2014
97
+ RightPointer = ?\u25ba
98
+ LowercaseGreekA = ?\u03b1
99
+ Bullets = {
100
+ disc: ?\u2022,
101
+ circle: ?\u25e6,
102
+ square: ?\u25aa,
103
+ none: '',
104
+ }
105
+ # NOTE Default theme font uses ballot boxes from FontAwesome
106
+ BallotBox = {
107
+ checked: ?\u2611,
108
+ unchecked: ?\u2610,
109
+ }
110
+ ConumSets = {
111
+ 'circled' => (?\u2460..?\u2473).to_a,
112
+ 'filled' => (?\u2776..?\u277f).to_a + (?\u24eb..?\u24f4).to_a,
113
+ }
114
+ SimpleAttributeRefRx = /(?<!\\)\{\w+(?:[\-]\w+)*\}/
115
+ MeasurementRxt = '\\d+(?:\\.\\d+)?(?:in|cm|mm|p[txc])?'
116
+ MeasurementPartsRx = /^(\d+(?:\.\d+)?)(in|mm|cm|p[txc])?$/
117
+ PageSizeRx = /^(?:\[(#{MeasurementRxt}), ?(#{MeasurementRxt})\]|(#{MeasurementRxt})(?: x |x)(#{MeasurementRxt})|\S+)$/
118
+ CalloutExtractRx = /(?:(?:\/\/|#|--|;;) ?)?(\\)?<!?(|--)(\d+|\.)\2> ?(?=(?:\\?<!?\2(?:\d+|\.)\2>)*$)/
119
+ ImageAttributeValueRx = /^image:{1,2}(.*?)\[(.*?)\]$/
120
+ StopPunctRx = /[.!?;:]$/
121
+ UriBreakCharsRx = /(?:\/|\?|&amp;|#)(?!$)/
122
+ UriBreakCharRepl = %(\\&#{ZeroWidthSpace})
123
+ UriSchemeBoundaryRx = /(?<=:\/\/)/
124
+ LineScanRx = /\n|.+/
125
+ BlankLineRx = /\n{2,}/
126
+ CjkLineBreakRx = /(?=[\u3000\u30a0-\u30ff\u3040-\u309f\p{Han}\uff00-\uffef])/
127
+ WhitespaceChars = ' ' + TAB + LF
128
+ ValueSeparatorRx = /;|,/
129
+ HexColorRx = /^#[a-fA-F0-9]{6}$/
130
+ VimeoThumbnailRx = /<thumbnail_large>(.*?)<\/thumbnail_large>/
131
+ SourceHighlighters = %w(coderay pygments rouge).to_set
132
+ ViewportWidth = ::Module.new
133
+ (TitleStyles = {
134
+ 'toc' => [:numbered_title],
135
+ 'basic' => [:title],
136
+ }).default = [:numbered_title, formal: true]
137
+
138
+ def initialize backend, opts
139
+ super
140
+ basebackend 'html'
141
+ filetype 'pdf'
142
+ htmlsyntax 'html'
143
+ outfilesuffix '.pdf'
144
+ if (doc = opts[:document])
145
+ # NOTE enabling data-uri forces Asciidoctor Diagram to produce absolute image paths
146
+ doc.attributes['data-uri'] = (doc.instance_variable_get :@attribute_overrides)['data-uri'] = ''
147
+ end
148
+ @initial_instance_variables = [:@initial_instance_variables] + instance_variables
149
+ end
150
+
151
+ def convert node, name = nil, _opts = {}
152
+ method_name = %(convert_#{name ||= node.node_name})
153
+ if respond_to? method_name
154
+ result = send method_name, node
155
+ else
156
+ # TODO: delegate to convert_method_missing
157
+ logger.warn %(missing convert handler for #{name} node in #{@backend} backend) unless scratch?
158
+ end
159
+ # NOTE: inline nodes generate pseudo-HTML strings; the remainder write directly to PDF object
160
+ ::Asciidoctor::Inline === node ? result : self
161
+ end
162
+
163
+ def traverse node, opts = {}
164
+ # NOTE converter instance in scratch document gets duplicated; must be rewired to this one
165
+ if self == (prev_converter = node.document.converter)
166
+ prev_converter = nil
167
+ else
168
+ node.document.instance_variable_set :@converter, self
169
+ end
170
+ if node.blocks?
171
+ node.content
172
+ elsif node.content_model != :compound && (string = node.content)
173
+ # TODO: this content could be cached on repeat invocations!
174
+ layout_prose string, (opts.merge hyphenate: true)
175
+ end
176
+ ensure
177
+ node.document.instance_variable_set :@converter, prev_converter if prev_converter
178
+ end
179
+
180
+ def convert_document doc
181
+ init_pdf doc
182
+ # set default value for outline and pagenums if not otherwise set
183
+ doc.attributes['outline'] = '' unless (doc.attribute_locked? 'outline') || ((doc.instance_variable_get :@attributes_modified).include? 'outline')
184
+ doc.attributes['pagenums'] = '' unless (doc.attribute_locked? 'pagenums') || ((doc.instance_variable_get :@attributes_modified).include? 'pagenums')
185
+ #assign_missing_section_ids doc
186
+
187
+ # promote anonymous preface (defined using preamble block) to preface section
188
+ # FIXME: this should be done in core
189
+ if doc.doctype == 'book' && (blk0 = doc.blocks[0]) && blk0.context == :preamble && blk0.title? &&
190
+ !blk0.title.nil_or_empty? && blk0.blocks[0].style != 'abstract' && (blk1 = doc.blocks[1]) && blk1.context == :section
191
+ preface = Section.new doc, blk1.level, false, attributes: { 1 => 'preface', 'style' => 'preface' }
192
+ preface.special = true
193
+ preface.sectname = 'preface'
194
+ preface.title = blk0.instance_variable_get :@title
195
+ # QUESTION should ID be generated from raw or converted title? core is not clear about this
196
+ preface.id = preface.generate_id
197
+ preface.blocks.replace blk0.blocks.map {|b| b.parent = preface; b } # rubocop:disable Style/Semicolon
198
+ doc.blocks[0] = preface
199
+ blk0 = blk1 = preface = nil # rubocop:disable Lint/UselessAssignment
200
+ end
201
+
202
+ on_page_create(&(method :init_page))
203
+
204
+ marked_page_number = page_number
205
+ # NOTE: a new page will already be started (page_number = 2) if the front cover image is a PDF
206
+ layout_cover_page doc, :front
207
+ has_front_cover = page_number > marked_page_number
208
+
209
+ if (use_title_page = doc.doctype == 'book' || (doc.attr? 'title-page'))
210
+ layout_title_page doc
211
+ has_title_page = page_number == (has_front_cover ? 2 : 1)
212
+ end
213
+
214
+ @page_margin_by_side[:cover] = @page_margin_by_side[:recto] if @media == 'prepress' && page_number == 0
215
+
216
+ # NOTE: font must be set before content is written to the main or scratch document
217
+ start_new_page unless page.empty?
218
+ font @theme.base_font_family, size: @root_font_size, style: (@theme.base_font_style || :normal).to_sym
219
+
220
+ unless use_title_page
221
+ body_start_page_number = page_number
222
+ theme_font :heading, level: 1 do
223
+ layout_heading doc.doctitle, align: (@theme.heading_h1_align || :center).to_sym, level: 1
224
+ end if doc.header? && !doc.notitle
225
+ end
226
+
227
+ num_front_matter_pages = toc_page_nums = toc_num_levels = nil
228
+
229
+ indent_section do
230
+ toc_num_levels = (doc.attr 'toclevels', 2).to_i
231
+ if (insert_toc = (doc.attr? 'toc') && !(doc.attr? 'toc-placement', 'macro') && doc.sections?)
232
+ start_new_page if @ppbook && verso_page?
233
+ add_dest_for_block doc, 'toc'
234
+ allocate_toc doc, toc_num_levels, @y, use_title_page
235
+ else
236
+ @toc_extent = nil
237
+ end
238
+
239
+ start_new_page if @ppbook && verso_page?
240
+
241
+ if use_title_page
242
+ zero_page_offset = has_front_cover ? 1 : 0
243
+ first_page_offset = has_title_page ? zero_page_offset.next : zero_page_offset
244
+ body_offset = (body_start_page_number = page_number) - 1
245
+ if ::Integer === (running_content_start_at = @theme.running_content_start_at || 'body')
246
+ running_content_body_offset = body_offset + [running_content_start_at.pred, 1].max
247
+ running_content_start_at = 'body'
248
+ else
249
+ running_content_body_offset = body_offset
250
+ running_content_start_at = 'toc' if running_content_start_at == 'title' && !has_title_page
251
+ running_content_start_at = 'body' if running_content_start_at == 'toc' && !insert_toc
252
+ end
253
+ if ::Integer === (page_numbering_start_at = @theme.page_numbering_start_at || 'body')
254
+ page_numbering_body_offset = body_offset + [page_numbering_start_at.pred, 1].max
255
+ page_numbering_start_at = 'body'
256
+ else
257
+ page_numbering_body_offset = body_offset
258
+ page_numbering_start_at = 'toc' if page_numbering_start_at == 'title' && !has_title_page
259
+ page_numbering_start_at = 'body' if page_numbering_start_at == 'toc' && !insert_toc
260
+ end
261
+ front_matter_sig = [running_content_start_at, page_numbering_start_at]
262
+ # table values are number of pages to skip before starting running content and page numbering, respectively
263
+ num_front_matter_pages = {
264
+ %w(title title) => [zero_page_offset, zero_page_offset],
265
+ %w(title toc) => [zero_page_offset, first_page_offset],
266
+ %w(title body) => [zero_page_offset, page_numbering_body_offset],
267
+ %w(toc title) => [first_page_offset, zero_page_offset],
268
+ %w(toc toc) => [first_page_offset, first_page_offset],
269
+ %w(toc body) => [first_page_offset, page_numbering_body_offset],
270
+ %w(body title) => [running_content_body_offset, zero_page_offset],
271
+ %w(body toc) => [running_content_body_offset, first_page_offset],
272
+ }[front_matter_sig] || [running_content_body_offset, page_numbering_body_offset]
273
+ else
274
+ body_offset = body_start_page_number - 1
275
+ if ::Integer === (running_content_start_at = @theme.running_content_start_at || 'body')
276
+ running_content_body_offset = body_offset + [running_content_start_at.pred, 1].max
277
+ else
278
+ running_content_body_offset = body_offset
279
+ end
280
+ if ::Integer === (page_numbering_start_at = @theme.page_numbering_start_at || 'body')
281
+ page_numbering_body_offset = body_offset + [page_numbering_start_at.pred, 1].max
282
+ else
283
+ page_numbering_body_offset = body_offset
284
+ end
285
+ num_front_matter_pages = [running_content_body_offset, page_numbering_body_offset]
286
+ end
287
+
288
+ @index.start_page_number = num_front_matter_pages[1] + 1
289
+ doc.set_attr 'pdf-anchor', (doc_anchor = derive_anchor_from_id doc.id, 'top')
290
+ add_dest_for_block doc, doc_anchor
291
+
292
+ convert_section generate_manname_section doc if doc.doctype == 'manpage' && (doc.attr? 'manpurpose')
293
+
294
+ traverse doc
295
+
296
+ # NOTE: for a book, these are leftover footnotes; for an article this is everything
297
+ outdent_section { layout_footnotes doc }
298
+
299
+ toc_page_nums = @toc_extent ? (layout_toc doc, toc_num_levels, @toc_extent[:page_nums].first, @toc_extent[:start_y], num_front_matter_pages[1]) : []
300
+
301
+ # NOTE: delete orphaned page (a page was created but there was no additional content)
302
+ # QUESTION should we delete page if document is empty? (leaving no pages?)
303
+ delete_page if page_count > 1 && page.empty?
304
+ end
305
+
306
+ unless page_count < body_start_page_number
307
+ layout_running_content :header, doc, skip: num_front_matter_pages, body_start_page_number: body_start_page_number unless doc.noheader || @theme.header_height.to_f == 0
308
+ layout_running_content :footer, doc, skip: num_front_matter_pages, body_start_page_number: body_start_page_number unless doc.nofooter || @theme.footer_height.to_f == 0
309
+ end
310
+
311
+ add_outline doc, (doc.attr 'outlinelevels', toc_num_levels), toc_page_nums, num_front_matter_pages[1], has_front_cover
312
+ if !state.pages.empty? && (initial_zoom = @theme.page_initial_zoom)
313
+ case initial_zoom.to_sym
314
+ when :Fit
315
+ catalog.data[:OpenAction] = dest_fit state.pages[0]
316
+ when :FitV
317
+ catalog.data[:OpenAction] = dest_fit_vertically 0, state.pages[0]
318
+ when :FitH
319
+ catalog.data[:OpenAction] = dest_fit_horizontally page_height, state.pages[0]
320
+ end
321
+ end
322
+ catalog.data[:ViewerPreferences] = { DisplayDocTitle: true }
323
+
324
+ stamp_foreground_image doc, has_front_cover
325
+ layout_cover_page doc, :back
326
+ nil
327
+ end
328
+
329
+ # NOTE: embedded only makes sense if perhaps we are building
330
+ # on an existing Prawn::Document instance; for now, just treat
331
+ # it the same as a full document.
332
+ alias convert_embedded convert_document
333
+
334
+ def init_pdf doc
335
+ (instance_variables - @initial_instance_variables).each {|ivar| remove_instance_variable ivar } if state
336
+ pdf_opts = build_pdf_options doc, (theme = load_theme doc)
337
+ # QUESTION should page options be preserved? (otherwise, not readily available)
338
+ #@page_opts = { size: pdf_opts[:page_size], layout: pdf_opts[:page_layout] }
339
+ ((::Prawn::Document.instance_method :initialize).bind self).call pdf_opts
340
+ renderer.min_version (@pdf_version = PDFVersions[doc.attr 'pdf-version'])
341
+ @page_margin_by_side = { recto: page_margin, verso: page_margin, cover: page_margin }
342
+ if (@media = doc.attr 'media', 'screen') == 'prepress'
343
+ @ppbook = doc.doctype == 'book'
344
+ page_margin_recto = @page_margin_by_side[:recto]
345
+ if (page_margin_outer = theme.page_margin_outer)
346
+ page_margin_recto[1] = @page_margin_by_side[:verso][3] = page_margin_outer
347
+ end
348
+ if (page_margin_inner = theme.page_margin_inner)
349
+ page_margin_recto[3] = @page_margin_by_side[:verso][1] = page_margin_inner
350
+ end
351
+ # NOTE: prepare scratch document to use page margin from recto side (which has same width as verso side)
352
+ set_page_margin page_margin_recto unless page_margin_recto == page_margin
353
+ else
354
+ @ppbook = nil
355
+ end
356
+ # QUESTION should ThemeLoader handle registering fonts instead?
357
+ register_fonts theme.font_catalog, (doc.attr 'pdf-fontsdir', 'GEM_FONTS_DIR')
358
+ default_kerning theme.base_font_kerning != 'none'
359
+ @fallback_fonts = [*theme.font_fallbacks]
360
+ @allow_uri_read = doc.attr? 'allow-uri-read'
361
+ @cache_uri = doc.attr? 'cache-uri'
362
+ @tmp_files = {}
363
+ if (bg_image = resolve_background_image doc, theme, 'page-background-image') && bg_image[0]
364
+ @page_bg_image = { verso: bg_image, recto: bg_image }
365
+ else
366
+ @page_bg_image = { verso: nil, recto: nil }
367
+ end
368
+ if (bg_image = resolve_background_image doc, theme, 'page-background-image-verso')
369
+ @page_bg_image[:verso] = bg_image[0] && bg_image
370
+ end
371
+ if (bg_image = resolve_background_image doc, theme, 'page-background-image-recto')
372
+ @page_bg_image[:recto] = bg_image[0] && bg_image
373
+ end
374
+ @page_bg_color = resolve_theme_color :page_background_color, 'FFFFFF'
375
+ @root_font_size = theme.base_font_size || 12
376
+ @font_color = theme.base_font_color || '000000'
377
+ @text_decoration_width = theme.base_text_decoration_width
378
+ @base_align = (align = doc.attr 'text-align') && (TextAlignmentNames.include? align) ? align : theme.base_align
379
+ @cjk_line_breaks = doc.attr? 'scripts', 'cjk'
380
+ if (hyphen_lang = doc.attr 'hyphens') &&
381
+ ((defined? ::Text::Hyphen::VERSION) || !(Helpers.require_library 'text/hyphen', 'text-hyphen', :warn).nil?)
382
+ hyphen_lang = doc.attr 'lang' if hyphen_lang.empty?
383
+ hyphen_lang = 'en_us' if hyphen_lang.nil_or_empty? || hyphen_lang == 'en'
384
+ hyphen_lang = (hyphen_lang.tr '-', '_').downcase
385
+ @hyphenator = ::Text::Hyphen.new language: hyphen_lang
386
+ end
387
+ @text_transform = nil
388
+ @list_numerals = []
389
+ @list_bullets = []
390
+ @rendered_footnotes = []
391
+ @conum_glyphs = ConumSets[@theme.conum_glyphs || 'circled'] || (@theme.conum_glyphs.split ',').map {|r|
392
+ from, to = r.rstrip.split '-', 2
393
+ to ? ((get_char from)..(get_char to)).to_a : [(get_char from)]
394
+ }.flatten
395
+ @section_indent = (val = @theme.section_indent) && (expand_indent_value val)
396
+ @toc_max_pagenum_digits = (doc.attr 'toc-max-pagenum-digits', 3).to_i
397
+ @disable_running_content = {}
398
+ @index = IndexCatalog.new
399
+ # NOTE: we have to init Pdfmark class here while we have reference to the doc
400
+ @pdfmark = (doc.attr? 'pdfmark') ? (Pdfmark.new doc) : nil
401
+ @optimize = doc.attr 'optimize'
402
+ init_scratch_prototype
403
+ self
404
+ end
405
+
406
+ def load_theme doc
407
+ @theme ||= begin # rubocop:disable Naming/MemoizedInstanceVariableName
408
+ if (theme = doc.options[:pdf_theme])
409
+ @themesdir = ::File.expand_path theme.__dir__ || (doc.attr 'pdf-themesdir') || (doc.attr 'pdf-stylesdir') || ::Dir.pwd
410
+ elsif (theme_name = (doc.attr 'pdf-theme') || (doc.attr 'pdf-style'))
411
+ theme = ThemeLoader.load_theme theme_name, (user_themesdir = (doc.attr 'pdf-themesdir') || (doc.attr 'pdf-stylesdir'))
412
+ @themesdir = theme.__dir__
413
+ else
414
+ @themesdir = (theme = ThemeLoader.load_theme).__dir__
415
+ end
416
+ theme
417
+ rescue
418
+ if user_themesdir
419
+ message = %(could not locate or load the pdf theme `#{theme_name}' in #{user_themesdir})
420
+ else
421
+ message = %(could not locate or load the built-in pdf theme `#{theme_name}')
422
+ end
423
+ message += %( because of #{$!.class} #{$!.message}) unless ::SystemCallError === $!
424
+ logger.error %(#{message}; reverting to default theme)
425
+ @themesdir = (theme = ThemeLoader.load_theme).__dir__
426
+ theme
427
+ end
428
+ end
429
+
430
+ def build_pdf_options doc, theme
431
+ case (page_margin = (doc.attr 'pdf-page-margin') || theme.page_margin)
432
+ when ::Array
433
+ if page_margin.empty?
434
+ page_margin = nil
435
+ else
436
+ page_margin = page_margin.slice 0, 4 if page_margin.length > 4
437
+ page_margin = page_margin.map {|v| ::Numeric === v ? v : (str_to_pt v.to_s) }
438
+ end
439
+ when ::Numeric
440
+ page_margin = [page_margin]
441
+ when ::String
442
+ if page_margin.empty?
443
+ page_margin = nil
444
+ elsif (page_margin.start_with? '[') && (page_margin.end_with? ']')
445
+ if (page_margin = (page_margin.slice 1, page_margin.length - 2).rstrip).empty?
446
+ page_margin = nil
447
+ else
448
+ if (page_margin = page_margin.split ',', -1).length > 4
449
+ page_margin = page_margin.slice 0, 4
450
+ end
451
+ page_margin = page_margin.map {|v| str_to_pt v.rstrip }
452
+ end
453
+ else
454
+ page_margin = [(str_to_pt page_margin)]
455
+ end
456
+ else
457
+ page_margin = nil
458
+ end
459
+
460
+ if (doc.attr? 'pdf-page-size') && PageSizeRx =~ (doc.attr 'pdf-page-size')
461
+ # e.g, [8.5in, 11in]
462
+ if $1
463
+ page_size = [$1, $2]
464
+ # e.g, 8.5in x 11in
465
+ elsif $3
466
+ page_size = [$3, $4]
467
+ # e.g, A4
468
+ else
469
+ page_size = $&
470
+ end
471
+ else
472
+ page_size = theme.page_size
473
+ end
474
+
475
+ case page_size
476
+ when ::String
477
+ # TODO: extract helper method to check for named page size
478
+ page_size = page_size.upcase
479
+ page_size = nil unless ::PDF::Core::PageGeometry::SIZES.key? page_size
480
+ when ::Array
481
+ page_size = (page_size.slice 0, 2).fill(0..1) {|i| page_size[i] || 0 } unless page_size.size == 2
482
+ page_size = page_size.map do |dim|
483
+ if ::Numeric === dim
484
+ # dimension cannot be less than 0
485
+ dim > 0 ? dim : break
486
+ elsif ::String === dim && MeasurementPartsRx =~ dim
487
+ # NOTE: truncate to max precision retained by PDF::Core
488
+ (to_pt $1.to_f, $2).truncate 4
489
+ else
490
+ break
491
+ end
492
+ end
493
+ else
494
+ page_size = nil
495
+ end
496
+
497
+ if (page_layout = (doc.attr 'pdf-page-layout') || theme.page_layout).nil_or_empty? ||
498
+ !PageLayouts.include?(page_layout = page_layout.to_sym)
499
+ page_layout = nil
500
+ end
501
+
502
+ {
503
+ margin: (page_margin || 36),
504
+ page_size: (page_size || 'A4'),
505
+ page_layout: (page_layout || :portrait),
506
+ info: (build_pdf_info doc),
507
+ compress: (doc.attr? 'compress'),
508
+ skip_page_creation: true,
509
+ text_formatter: (FormattedText::Formatter.new theme: theme),
510
+ }
511
+ end
512
+
513
+ # FIXME: Pdfmark should use the PDF info result
514
+ def build_pdf_info doc
515
+ info = {}
516
+ if (doctitle = doc.header? ? doc.doctitle : (doc.attr 'untitled-label'))
517
+ info[:Title] = (sanitize doctitle).as_pdf
518
+ end
519
+ info[:Author] = (sanitize doc.attr 'authors').as_pdf if doc.attr? 'authors'
520
+ info[:Subject] = (sanitize doc.attr 'subject').as_pdf if doc.attr? 'subject'
521
+ info[:Keywords] = (sanitize doc.attr 'keywords').as_pdf if doc.attr? 'keywords'
522
+ info[:Producer] = (sanitize doc.attr 'publisher').as_pdf if doc.attr? 'publisher'
523
+ if doc.attr? 'reproducible'
524
+ info[:Creator] = 'Asciidoctor PDF, based on Prawn'.as_pdf
525
+ info[:Producer] ||= (info[:Author] || info[:Creator])
526
+ else
527
+ info[:Creator] = %(Asciidoctor PDF #{::Asciidoctor::PDF::VERSION}, based on Prawn #{::Prawn::VERSION}).as_pdf
528
+ info[:Producer] ||= (info[:Author] || info[:Creator])
529
+ # NOTE: since we don't track the creation date of the input file, we map the ModDate header to the last modified
530
+ # date of the input document and the CreationDate header to the date the PDF was produced by the converter.
531
+ info[:ModDate] = (::Time.parse doc.attr 'docdatetime') rescue (now ||= ::Time.now)
532
+ info[:CreationDate] = (::Time.parse doc.attr 'localdatetime') rescue (now || ::Time.now)
533
+ end
534
+ info
535
+ end
536
+
537
+ # # NOTE: init_page is called within a float context
538
+ # # NOTE: init_page is not called for imported pages, front and back cover pages, and other image pages
539
+ # def init_page *_args
540
+ # # NOTE: we assume in prepress that physical page number reflects page side
541
+ # if @media == 'prepress' &&
542
+ # (next_page_margin = @page_margin_by_side[page_number == 1 ? :cover : page_side]) != page_margin
543
+ # set_page_margin next_page_margin
544
+ # end
545
+ # unless @page_bg_color == 'FFFFFF'
546
+ # tare = true
547
+ # fill_absolute_bounds @page_bg_color
548
+ # end
549
+ # if (bg_image = @page_bg_image[page_side])
550
+ # tare = true
551
+ # # NOTE: float is necessary since prawn-svg may mess with cursor position
552
+ # float { canvas { image bg_image[0], ({ position: :center, vposition: :center }.merge bg_image[1]) } }
553
+ # end
554
+ # page.tare_content_stream if tare
555
+ # end
556
+
557
+ # def convert_section sect, _opts = {}
558
+ # if sect.sectname == 'abstract'
559
+ # # HACK: cheat a bit to hide this section from TOC; TOC should filter these sections
560
+ # sect.context = :open
561
+ # return convert_abstract sect
562
+ # elsif (index_section = sect.sectname == 'index')
563
+ # return if @index.empty?
564
+ # end
565
+
566
+ # type = nil
567
+ # title = sect.numbered_title formal: true
568
+ # sep = (sect.attr 'separator', nil, false) || (sect.document.attr 'title-separator') || ''
569
+ # if !sep.empty? && title.include?(sep = %(#{sep} ))
570
+ # title, _, subtitle = title.rpartition sep
571
+ # title = %(#{title}\n<em class="subtitle">#{subtitle}</em>)
572
+ # end
573
+ # theme_font :heading, level: (hlevel = sect.level + 1) do
574
+ # align = (@theme[%(heading_h#{hlevel}_align)] || @theme.heading_align || @base_align).to_sym
575
+ # if sect.part_or_chapter?
576
+ # if sect.chapter?
577
+ # type = :chapter
578
+ # if @theme.heading_chapter_break_before == 'auto'
579
+ # start_new_chapter sect if @theme.heading_part_break_after == 'always' && sect == sect.parent.sections[0]
580
+ # else
581
+ # start_new_chapter sect
582
+ # end
583
+ # else
584
+ # type = :part
585
+ # start_new_part sect unless @theme.heading_part_break_before == 'auto'
586
+ # end
587
+ # end
588
+ # unless at_page_top?
589
+ # # FIXME: this height doesn't account for impact of text transform or inline formatting
590
+ # heading_height =
591
+ # (height_of_typeset_text title, line_height: (@theme[%(heading_h#{hlevel}_line_height)] || @theme.heading_line_height)) +
592
+ # (@theme[%(heading_h#{hlevel}_margin_top)] || @theme.heading_margin_top || 0) +
593
+ # (@theme[%(heading_h#{hlevel}_margin_bottom)] || @theme.heading_margin_bottom || 0)
594
+ # heading_height += (@theme.heading_min_height_after || 0) if sect.blocks?
595
+ # start_new_page unless cursor > heading_height
596
+ # end
597
+ # # QUESTION should we store pdf-page-start, pdf-anchor & pdf-destination in internal map?
598
+ # sect.set_attr 'pdf-page-start', (start_pgnum = page_number)
599
+ # # QUESTION should we just assign the section this generated id?
600
+ # # NOTE: section must have pdf-anchor in order to be listed in the TOC
601
+ # sect.set_attr 'pdf-anchor', (sect_anchor = derive_anchor_from_id sect.id, %(#{start_pgnum}-#{y.ceil}))
602
+ # add_dest_for_block sect, sect_anchor
603
+ # if type == :part
604
+ # layout_part_title sect, title, align: align, level: hlevel
605
+ # elsif type == :chapter
606
+ # layout_chapter_title sect, title, align: align, level: hlevel
607
+ # else
608
+ # layout_heading title, align: align, level: hlevel, outdent: true
609
+ # end
610
+ # end
611
+
612
+ # if index_section
613
+ # outdent_section { convert_index_section sect }
614
+ # else
615
+ # traverse sect
616
+ # end
617
+ # outdent_section { layout_footnotes sect } if type == :chapter
618
+ # sect.set_attr 'pdf-page-end', page_number
619
+ # end
620
+
621
+ # def indent_section
622
+ # if (values = @section_indent)
623
+ # indent(values[0], values[1]) { yield }
624
+ # else
625
+ # yield
626
+ # end
627
+ # end
628
+
629
+ # def outdent_section enabled = true
630
+ # if enabled && (values = @section_indent)
631
+ # indent(-values[0], -values[1]) { yield }
632
+ # else
633
+ # yield
634
+ # end
635
+ # end
636
+
637
+ # # QUESTION if a footnote ref appears in a separate chapter, should the footnote def be duplicated?
638
+ # def layout_footnotes node
639
+ # return if (fns = (doc = node.document).footnotes - @rendered_footnotes).empty?
640
+ # theme_margin :footnotes, :top
641
+ # theme_font :footnotes do
642
+ # (title = doc.attr 'footnotes-title') && (layout_caption title, category: :footnotes)
643
+ # item_spacing = @theme.footnotes_item_spacing || 0
644
+ # index_offset = @rendered_footnotes.length
645
+ # sect_xreftext = node.context == :section && (node.xreftext node.document.attr 'xrefstyle')
646
+ # fns.each do |fn|
647
+ # label = (index = fn.index) - index_offset
648
+ # if sect_xreftext
649
+ # fn.singleton_class.send :attr_accessor, :label unless fn.respond_to? :label=
650
+ # fn.label = %(#{label} - #{sect_xreftext})
651
+ # end
652
+ # layout_prose %(<a id="_footnotedef_#{index}">#{DummyText}</a>[<a anchor="_footnoteref_#{index}">#{label}</a>] #{fn.text}), margin_bottom: item_spacing, hyphenate: true
653
+ # end
654
+ # @rendered_footnotes += fns
655
+ # end
656
+ # nil
657
+ # end
658
+
659
+ # def convert_floating_title node
660
+ # add_dest_for_block node if node.id
661
+ # hlevel = node.level.next
662
+ # unless (align = resolve_alignment_from_role node.roles)
663
+ # align = (@theme[%(heading_h#{hlevel}_align)] || @theme.heading_align || @base_align).to_sym
664
+ # end
665
+ # # QUESTION should we decouple styles from section titles?
666
+ # theme_font :heading, level: hlevel do
667
+ # layout_heading node.title, align: align, level: hlevel, outdent: (node.parent.context == :section)
668
+ # end
669
+ # end
670
+
671
+ # def convert_abstract node
672
+ # add_dest_for_block node if node.id
673
+ # outdent_section do
674
+ # pad_box @theme.abstract_padding do
675
+ # theme_font :abstract_title do
676
+ # layout_prose node.title, align: (@theme.abstract_title_align || @base_align).to_sym, margin_top: (@theme.heading_margin_top || 0), margin_bottom: (@theme.heading_margin_bottom || 0), line_height: @theme.heading_line_height
677
+ # end if node.title?
678
+ # theme_font :abstract do
679
+ # prose_opts = { line_height: @theme.abstract_line_height, align: (@theme.abstract_align || @base_align).to_sym, hyphenate: true }
680
+ # if (text_indent = @theme.prose_text_indent || 0) > 0
681
+ # prose_opts[:indent_paragraphs] = text_indent
682
+ # end
683
+ # # FIXME: allow theme to control more first line options
684
+ # if (line1_font_style = @theme.abstract_first_line_font_style) && line1_font_style.to_sym != font_style
685
+ # first_line_options = { styles: [font_style, line1_font_style.to_sym] }
686
+ # end
687
+ # if (line1_font_color = @theme.abstract_first_line_font_color)
688
+ # (first_line_options ||= {})[:color] = line1_font_color
689
+ # end
690
+ # prose_opts[:first_line_options] = first_line_options if first_line_options
691
+ # # FIXME: make this cleaner!!
692
+ # if node.blocks?
693
+ # node.blocks.each do |child|
694
+ # if child.context == :paragraph
695
+ # child.document.playback_attributes child.attributes
696
+ # layout_prose child.content, ((align = resolve_alignment_from_role child.roles) ? (prose_opts.merge align: align) : prose_opts.dup)
697
+ # prose_opts.delete :first_line_options
698
+ # else
699
+ # # FIXME: this could do strange things if the wrong kind of content shows up
700
+ # child.convert
701
+ # end
702
+ # end
703
+ # elsif node.content_model != :compound && (string = node.content)
704
+ # if (align = resolve_alignment_from_role node.roles)
705
+ # prose_opts[:align] = align
706
+ # end
707
+ # layout_prose string, prose_opts
708
+ # end
709
+ # end
710
+ # end
711
+ # # QUESTION should we be adding margin below the abstract??
712
+ # #theme_margin :block, :bottom
713
+ # end
714
+ # end
715
+
716
+ # def convert_preamble node
717
+ # # FIXME: core should not be promoting paragraph to preamble if there are no sections
718
+ # if node.blocks? && (first_block = node.blocks[0]).context == :paragraph && node.document.sections?
719
+ # first_block.add_role 'lead' unless first_block.role?
720
+ # end
721
+ # traverse node
722
+ # end
723
+
724
+ # def convert_paragraph node
725
+ # add_dest_for_block node if node.id
726
+ # prose_opts = { margin_bottom: 0, hyphenate: true }
727
+ # lead = (roles = node.roles).include? 'lead'
728
+ # if (align = resolve_alignment_from_role roles)
729
+ # prose_opts[:align] = align
730
+ # end
731
+
732
+ # if (text_indent = @theme.prose_text_indent || 0) > 0
733
+ # prose_opts[:indent_paragraphs] = text_indent
734
+ # end
735
+
736
+ # # TODO: check if we're within one line of the bottom of the page
737
+ # # and advance to the next page if so (similar to logic for section titles)
738
+ # layout_caption node.title if node.title?
739
+
740
+ # if lead
741
+ # theme_font :lead do
742
+ # layout_prose node.content, prose_opts
743
+ # end
744
+ # else
745
+ # layout_prose node.content, prose_opts
746
+ # end
747
+
748
+ # if (margin_inner_val = @theme.prose_margin_inner) &&
749
+ # (next_block = (siblings = node.parent.blocks)[(siblings.index node) + 1]) && next_block.context == :paragraph
750
+ # margin_bottom margin_inner_val
751
+ # else
752
+ # margin_bottom @theme.prose_margin_bottom
753
+ # end
754
+ # end
755
+
756
+ # def convert_admonition node
757
+ # add_dest_for_block node if node.id
758
+ # theme_margin :block, :top
759
+ # type = node.attr 'name'
760
+ # label_align = (@theme.admonition_label_align || :center).to_sym
761
+ # # TODO: allow vertical_align to be a number
762
+ # if (label_valign = (@theme.admonition_label_vertical_align || :middle).to_sym) == :middle
763
+ # label_valign = :center
764
+ # end
765
+ # if (label_min_width = @theme.admonition_label_min_width)
766
+ # label_min_width = label_min_width.to_f
767
+ # end
768
+ # icons = ((doc = node.document).attr? 'icons') ? (doc.attr 'icons') : nil
769
+ # if (data_uri_enabled = doc.attr? 'data-uri')
770
+ # doc.remove_attr 'data-uri'
771
+ # end
772
+ # if icons == 'font' && !(node.attr? 'icon', nil, false)
773
+ # label_text = type.to_sym
774
+ # icon_data = admonition_icon_data label_text
775
+ # label_width = label_min_width || ((icon_size = icon_data[:size] || 24) * 1.5)
776
+ # # NOTE: icon_uri will consider icon attribute on node first, then type
777
+ # # QUESTION should we use resolve_image_path here?
778
+ # elsif icons && (icon_path = node.icon_uri type) &&
779
+ # (icon_path = node.normalize_system_path icon_path, nil, nil, target_name: 'admonition icon') &&
780
+ # (::File.readable? icon_path)
781
+ # icons = true
782
+ # # TODO: introduce @theme.admonition_image_width? or use size key from admonition_icon_<name>?
783
+ # label_width = label_min_width || 36.0
784
+ # else
785
+ # if icons
786
+ # icons = nil
787
+ # logger.warn %(admonition icon not found or not readable: #{icon_path}) unless scratch?
788
+ # end
789
+ # label_text = node.caption
790
+ # theme_font :admonition_label do
791
+ # theme_font %(admonition_label_#{type}) do
792
+ # label_text = transform_text label_text, @text_transform if @text_transform
793
+ # label_width = rendered_width_of_string label_text
794
+ # label_width = label_min_width if label_min_width && label_min_width > label_width
795
+ # end
796
+ # end
797
+ # end
798
+ # doc.set_attr 'data-uri', '' if data_uri_enabled
799
+ # unless ::Array === (cpad = @theme.admonition_padding)
800
+ # cpad = ::Array.new 4, cpad
801
+ # end
802
+ # unless ::Array === (lpad = @theme.admonition_label_padding || cpad)
803
+ # lpad = ::Array.new 4, lpad
804
+ # end
805
+ # # FIXME: this shift stuff is a real hack until we have proper margin collapsing
806
+ # shift_base = @theme.prose_margin_bottom
807
+ # shift_top = shift_base / 3.0
808
+ # shift_bottom = (shift_base * 2) / 3.0
809
+ # keep_together do |box_height = nil|
810
+ # push_scratch doc if scratch?
811
+ # theme_fill_and_stroke_block :admonition, box_height if box_height
812
+ # pad_box [0, cpad[1], 0, lpad[3]] do
813
+ # if box_height
814
+ # label_height = [box_height, cursor].min
815
+ # if (rule_color = @theme.admonition_column_rule_color) &&
816
+ # (rule_width = @theme.admonition_column_rule_width || @theme.base_border_width) && rule_width > 0
817
+ # float do
818
+ # rule_height = box_height
819
+ # while rule_height > 0
820
+ # rule_segment_height = [rule_height, cursor].min
821
+ # bounding_box [0, cursor], width: label_width + lpad[1], height: rule_segment_height do
822
+ # stroke_vertical_rule rule_color,
823
+ # at: bounds.right,
824
+ # line_style: (@theme.admonition_column_rule_style || :solid).to_sym,
825
+ # line_width: rule_width
826
+ # end
827
+ # advance_page if (rule_height -= rule_segment_height) > 0
828
+ # end
829
+ # end
830
+ # end
831
+ # float do
832
+ # bounding_box [0, cursor], width: label_width, height: label_height do
833
+ # if icons == 'font'
834
+ # # FIXME: we assume icon is square
835
+ # icon_size = fit_icon_to_bounds icon_size
836
+ # # NOTE: Prawn's vertical center is not reliable, so calculate it manually
837
+ # if label_valign == :center
838
+ # label_valign = :top
839
+ # if (vcenter_pos = (label_height - icon_size) * 0.5) > 0
840
+ # move_down vcenter_pos
841
+ # end
842
+ # end
843
+ # icon icon_data[:name],
844
+ # valign: label_valign,
845
+ # align: label_align,
846
+ # color: icon_data[:stroke_color],
847
+ # size: icon_size
848
+ # elsif icons
849
+ # if (::Asciidoctor::Image.format icon_path) == 'svg'
850
+ # begin
851
+ # svg_obj = ::Prawn::SVG::Interface.new ::File.read(icon_path, mode: 'r:UTF-8'), self,
852
+ # position: label_align,
853
+ # vposition: label_valign,
854
+ # width: label_width,
855
+ # height: label_height,
856
+ # fallback_font_name: fallback_svg_font_name,
857
+ # enable_web_requests: allow_uri_read ? (method :load_open_uri).to_proc : false,
858
+ # enable_file_requests_with_root: (::File.dirname icon_path),
859
+ # cache_images: cache_uri
860
+ # if (icon_height = (svg_size = svg_obj.document.sizing).output_height) > label_height
861
+ # icon_width = (svg_obj.resize height: (icon_height = label_height)).output_width
862
+ # else
863
+ # icon_width = svg_size.output_width
864
+ # end
865
+ # svg_obj.draw
866
+ # svg_obj.document.warnings.each do |icon_warning|
867
+ # logger.warn %(problem encountered in image: #{icon_path}; #{icon_warning})
868
+ # end unless scratch?
869
+ # rescue
870
+ # logger.warn %(could not embed admonition icon: #{icon_path}; #{$!.message}) unless scratch?
871
+ # end
872
+ # else
873
+ # begin
874
+ # image_obj, image_info = ::File.open(icon_path, 'rb') {|fd| build_image_object fd }
875
+ # icon_aspect_ratio = image_info.width.fdiv image_info.height
876
+ # # NOTE: don't scale image up if smaller than label_width
877
+ # icon_width = [(to_pt image_info.width, :px), label_width].min
878
+ # if (icon_height = icon_width * (1 / icon_aspect_ratio)) > label_height
879
+ # icon_width *= label_height / icon_height
880
+ # icon_height = label_height # rubocop:disable Lint/UselessAssignment
881
+ # end
882
+ # embed_image image_obj, image_info, width: icon_width, position: label_align, vposition: label_valign
883
+ # rescue
884
+ # # QUESTION should we show the label in this case?
885
+ # logger.warn %(could not embed admonition icon: #{icon_path}; #{$!.message}) unless scratch?
886
+ # end
887
+ # end
888
+ # else
889
+ # # IMPORTANT the label must fit in the alotted space or it shows up on another page!
890
+ # # QUESTION anyway to prevent text overflow in the case it doesn't fit?
891
+ # theme_font :admonition_label do
892
+ # theme_font %(admonition_label_#{type}) do
893
+ # # NOTE: Prawn's vertical center is not reliable, so calculate it manually
894
+ # if label_valign == :center
895
+ # label_valign = :top
896
+ # if (vcenter_pos = (label_height - (height_of_typeset_text label_text, line_height: 1)) * 0.5) > 0
897
+ # move_down vcenter_pos
898
+ # end
899
+ # end
900
+ # @text_transform = nil # already applied to label
901
+ # layout_prose label_text,
902
+ # align: label_align,
903
+ # valign: label_valign,
904
+ # line_height: 1,
905
+ # margin: 0,
906
+ # inline_format: false
907
+ # end
908
+ # end
909
+ # end
910
+ # end
911
+ # end
912
+ # end
913
+ # pad_box [cpad[0], 0, cpad[2], label_width + lpad[1] + cpad[3]] do
914
+ # move_down shift_top
915
+ # layout_caption node.title, category: :admonition if node.title?
916
+ # theme_font :admonition do
917
+ # traverse node
918
+ # end
919
+ # # FIXME: HACK compensate for margin bottom of admonition content
920
+ # move_up shift_bottom unless at_page_top?
921
+ # end
922
+ # end
923
+ # pop_scratch doc if scratch?
924
+ # end
925
+ # theme_margin :block, :bottom
926
+ # end
927
+
928
+ # def convert_example node
929
+ # add_dest_for_block node if node.id
930
+ # theme_margin :block, :top
931
+ # caption_height = node.title? ? (layout_caption node, category: :example, dry_run: true) : 0
932
+ # keep_together do |box_height = nil|
933
+ # push_scratch node.document if scratch?
934
+ # if box_height
935
+ # theme_fill_and_stroke_block :example, box_height, caption_node: node
936
+ # else
937
+ # move_down caption_height
938
+ # end
939
+ # pad_box @theme.example_padding do
940
+ # theme_font :example do
941
+ # traverse node
942
+ # end
943
+ # end
944
+ # pop_scratch node.document if scratch?
945
+ # end
946
+ # theme_margin :block, :bottom
947
+ # end
948
+
949
+ # def convert_open node
950
+ # if node.style == 'abstract'
951
+ # convert_abstract node
952
+ # else
953
+ # doc = node.document
954
+ # keep_together_if node.option? 'unbreakable' do
955
+ # push_scratch doc if scratch?
956
+ # add_dest_for_block node if node.id
957
+ # layout_caption node.title if node.title?
958
+ # traverse node
959
+ # pop_scratch doc if scratch?
960
+ # end
961
+ # end
962
+ # end
963
+
964
+ # def convert_quote_or_verse node
965
+ # add_dest_for_block node if node.id
966
+ # theme_margin :block, :top
967
+ # category = node.context == :quote ? :blockquote : :verse
968
+ # # NOTE: b_width and b_left_width are mutually exclusive
969
+ # unless (b_left_width = @theme[%(#{category}_border_left_width)]) && b_left_width > 0
970
+ # b_left_width = nil
971
+ # if (b_width = @theme[%(#{category}_border_width)])
972
+ # b_width = nil unless b_width > 0
973
+ # end
974
+ # end
975
+ # keep_together do |box_height = nil|
976
+ # push_scratch node.document if scratch?
977
+ # theme_fill_and_stroke_block category, box_height, border_width: b_width if box_height && (b_width || @theme[%(#{category}_background_color)])
978
+ # start_page_number = page_number
979
+ # start_cursor = cursor
980
+ # caption_height = node.title? ? (layout_caption node, category: category) : 0
981
+ # pad_box @theme[%(#{category}_padding)] do
982
+ # theme_font category do
983
+ # if category == :blockquote
984
+ # traverse node
985
+ # else # verse
986
+ # content = guard_indentation node.content
987
+ # layout_prose content, normalize: false, align: :left, hyphenate: true
988
+ # end
989
+ # end
990
+ # if node.attr? 'attribution', nil, false
991
+ # theme_font %(#{category}_cite) do
992
+ # layout_prose %(#{EmDash} #{[(node.attr 'attribution'), (node.attr 'citetitle', nil, false)].compact.join ', '}), align: :left, normalize: false
993
+ # end
994
+ # end
995
+ # end
996
+ # # FIXME: we want to draw graphics before content, but box_height is not reliable when spanning pages
997
+ # # FIXME: border extends to bottom of content area if block terminates at bottom of page
998
+ # if box_height && b_left_width
999
+ # b_color = @theme[%(#{category}_border_color)]
1000
+ # page_spread = page_number - start_page_number + 1
1001
+ # end_cursor = cursor
1002
+ # go_to_page start_page_number
1003
+ # move_cursor_to start_cursor
1004
+ # page_spread.times do |i|
1005
+ # if i == 0
1006
+ # y_draw = cursor
1007
+ # b_height = page_spread > 1 ? y_draw : (y_draw - end_cursor)
1008
+ # else
1009
+ # bounds.move_past_bottom
1010
+ # y_draw = cursor
1011
+ # b_height = page_spread - 1 == i ? (y_draw - end_cursor) : y_draw
1012
+ # end
1013
+ # # NOTE: skip past caption if present
1014
+ # if caption_height > 0
1015
+ # if caption_height > cursor
1016
+ # caption_height -= cursor
1017
+ # next # keep skipping, caption is on next page
1018
+ # end
1019
+ # y_draw -= caption_height
1020
+ # b_height -= caption_height
1021
+ # caption_height = 0
1022
+ # end
1023
+ # # NOTE: b_height is 0 when block terminates at bottom of page
1024
+ # next if b_height == 0
1025
+ # bounding_box [0, y_draw], width: bounds.width, height: b_height do
1026
+ # stroke_vertical_rule b_color, line_width: b_left_width, at: b_left_width * 0.5
1027
+ # end
1028
+ # end
1029
+ # end
1030
+ # pop_scratch node.document if scratch?
1031
+ # end
1032
+ # theme_margin :block, :bottom
1033
+ # end
1034
+
1035
+ # alias convert_quote convert_quote_or_verse
1036
+ # alias convert_verse convert_quote_or_verse
1037
+
1038
+ # def convert_sidebar node
1039
+ # add_dest_for_block node if node.id
1040
+ # theme_margin :block, :top
1041
+ # keep_together do |box_height = nil|
1042
+ # push_scratch node.document if scratch?
1043
+ # theme_fill_and_stroke_block :sidebar, box_height if box_height
1044
+ # pad_box @theme.sidebar_padding do
1045
+ # theme_font :sidebar_title do
1046
+ # # QUESTION should we allow margins of sidebar title to be customized?
1047
+ # layout_prose node.title, align: (@theme.sidebar_title_align || @base_align).to_sym, margin_top: 0, margin_bottom: (@theme.heading_margin_bottom || 0), line_height: @theme.heading_line_height
1048
+ # end if node.title?
1049
+ # theme_font :sidebar do
1050
+ # traverse node
1051
+ # end
1052
+ # end
1053
+ # pop_scratch node.document if scratch?
1054
+ # end
1055
+ # theme_margin :block, :bottom
1056
+ # end
1057
+
1058
+ # def convert_colist node
1059
+ # # HACK: undo the margin below previous listing or literal block
1060
+ # # TODO: allow this to be set using colist_margin_top
1061
+ # if (self_idx = node.parent.blocks.index node) && self_idx > 0 &&
1062
+ # [:listing, :literal].include?(node.parent.blocks[self_idx - 1].context)
1063
+ # move_up @theme.block_margin_bottom - @theme.outline_list_item_spacing
1064
+ # end unless at_page_top?
1065
+ # add_dest_for_block node if node.id
1066
+ # @list_numerals << 1
1067
+ # #stroke_horizontal_rule @theme.caption_border_bottom_color
1068
+ # line_metrics = theme_font(:conum) { calc_line_metrics @theme.base_line_height }
1069
+ # node.items.each do |item|
1070
+ # allocate_space_for_list_item line_metrics
1071
+ # convert_colist_item item
1072
+ # end
1073
+ # @list_numerals.pop
1074
+ # # correct bottom margin of last item
1075
+ # list_margin_bottom = @theme.prose_margin_bottom
1076
+ # margin_bottom list_margin_bottom - @theme.outline_list_item_spacing
1077
+ # end
1078
+
1079
+ # def convert_colist_item node
1080
+ # marker_width = nil
1081
+ # @list_numerals << (index = @list_numerals.pop).next
1082
+ # theme_font :conum do
1083
+ # marker_width = rendered_width_of_string %(#{marker = conum_glyph index}x)
1084
+ # float do
1085
+ # bounding_box [0, cursor], width: marker_width do
1086
+ # theme_font :conum do
1087
+ # layout_prose marker, align: :center, line_height: @theme.conum_line_height, inline_format: false, margin: 0
1088
+ # end
1089
+ # end
1090
+ # end
1091
+ # end
1092
+
1093
+ # indent marker_width do
1094
+ # traverse_list_item node, :colist, margin_bottom: @theme.outline_list_item_spacing, normalize_line_height: true
1095
+ # end
1096
+ # end
1097
+
1098
+ # def convert_dlist node
1099
+ # add_dest_for_block node if node.id
1100
+
1101
+ # case (style = node.style)
1102
+ # when 'unordered', 'ordered'
1103
+ # if style == 'unordered'
1104
+ # list_style = :ulist
1105
+ # (markers = @list_bullets) << :disc
1106
+ # else
1107
+ # list_style = :olist
1108
+ # (markers = @list_numerals) << 1
1109
+ # end
1110
+ # list = List.new node.parent, list_style
1111
+ # stack_subject = node.has_role? 'stack'
1112
+ # subject_stop = node.attr 'subject-stop', (stack_subject ? nil : ':'), false
1113
+ # node.items.each do |subjects, dd|
1114
+ # subject = [*subjects].first.text
1115
+ # if dd
1116
+ # list_item_text = %(+++<strong>#{subject}#{(StopPunctRx.match? sanitize subject) ? '' : subject_stop}</strong>#{dd.text? ? "#{stack_subject ? '<br>' : ' '}#{dd.text}" : ''}+++)
1117
+ # list_item = ListItem.new list, list_item_text
1118
+ # dd.blocks.each {|it| list_item << it } if dd.block?
1119
+ # else
1120
+ # list_item = ListItem.new list, %(+++<strong>#{subject}</strong>+++)
1121
+ # end
1122
+ # list << list_item
1123
+ # end
1124
+ # convert_outline_list list
1125
+ # markers.pop
1126
+ # when 'horizontal'
1127
+ # table_data = []
1128
+ # term_padding = desc_padding = term_line_metrics = term_inline_format = term_kerning = nil
1129
+ # max_term_width = 0
1130
+ # theme_font :description_list_term do
1131
+ # term_inline_format = (term_font_styles = font_styles).empty? ? true : [inherited: { styles: term_font_styles }]
1132
+ # term_line_metrics = calc_line_metrics @theme.description_list_term_line_height || @theme.base_line_height
1133
+ # term_padding = [term_line_metrics.padding_top, 10, (@theme.prose_margin_bottom || 0) * 0.5 + term_line_metrics.padding_bottom, 10]
1134
+ # desc_padding = [0, 10, (@theme.prose_margin_bottom || 0) * 0.5, 10]
1135
+ # term_kerning = default_kerning?
1136
+ # end
1137
+ # node.items.each do |terms, desc|
1138
+ # term_text = terms.map(&:text).join ?\n
1139
+ # if (term_width = width_of term_text, inline_format: term_inline_format, kerning: term_kerning) > max_term_width
1140
+ # max_term_width = term_width
1141
+ # end
1142
+ # row_data = [{
1143
+ # text_color: @font_color,
1144
+ # kerning: term_kerning,
1145
+ # content: term_text,
1146
+ # inline_format: term_inline_format,
1147
+ # padding: term_padding,
1148
+ # leading: term_line_metrics.leading,
1149
+ # # FIXME: prawn-table doesn't have support for final_gap option
1150
+ # #final_gap: term_line_metrics.final_gap,
1151
+ # valign: :top,
1152
+ # }]
1153
+ # if desc
1154
+ # desc_container = Block.new desc, :open
1155
+ # desc_container << (Block.new desc_container, :paragraph, source: (desc.instance_variable_get :@text), subs: :default) if desc.text?
1156
+ # desc.blocks.each {|b| desc_container << b } if desc.block?
1157
+ # row_data << {
1158
+ # content: (::Prawn::Table::Cell::AsciiDoc.new self, content: desc_container, text_color: @font_color, padding: desc_padding, valign: :top),
1159
+ # }
1160
+ # else
1161
+ # row_data << {}
1162
+ # end
1163
+ # table_data << row_data
1164
+ # end
1165
+ # max_term_width += (term_padding[1] + term_padding[3])
1166
+ # term_column_width = [max_term_width, bounds.width * 0.5].min
1167
+ # table table_data, position: :left, cell_style: { border_width: 0 }, column_widths: [term_column_width] do
1168
+ # @pdf.layout_table_caption node if node.title?
1169
+ # end
1170
+ # margin_bottom (@theme.prose_margin_bottom || 0) * 0.5
1171
+ # when 'qanda'
1172
+ # @list_numerals << '1'
1173
+ # convert_outline_list node
1174
+ # @list_numerals.pop
1175
+ # else
1176
+ # # TODO: check if we're within one line of the bottom of the page
1177
+ # # and advance to the next page if so (similar to logic for section titles)
1178
+ # layout_caption node.title, category: :description_list if node.title?
1179
+
1180
+ # term_line_height = @theme.description_list_term_line_height || @theme.base_line_height
1181
+ # line_metrics = theme_font(:description_list_term) { calc_line_metrics term_line_height }
1182
+ # node.items.each do |terms, desc|
1183
+ # # NOTE: don't orphan the terms (keep together terms and at least one line of content)
1184
+ # allocate_space_for_list_item line_metrics, (terms.size + 1), ((@theme.description_list_term_spacing || 0) + 0.05)
1185
+ # theme_font :description_list_term do
1186
+ # if (term_font_styles = font_styles).empty?
1187
+ # term_font_styles = nil
1188
+ # end
1189
+ # terms.each do |term|
1190
+ # # QUESTION should we pass down styles in other calls to layout_prose
1191
+ # layout_prose term.text, margin_top: 0, margin_bottom: @theme.description_list_term_spacing, align: :left, line_height: term_line_height, normalize_line_height: true, styles: term_font_styles
1192
+ # end
1193
+ # end
1194
+ # indent(@theme.description_list_description_indent || 0) do
1195
+ # traverse_list_item desc, :dlist_desc, normalize_line_height: true
1196
+ # end if desc
1197
+ # end
1198
+ # end
1199
+ # end
1200
+
1201
+ # def convert_olist node
1202
+ # add_dest_for_block node if node.id
1203
+ # # TODO: move list_numeral resolve to a method
1204
+ # case node.style
1205
+ # when 'arabic'
1206
+ # list_numeral = 1
1207
+ # when 'decimal'
1208
+ # list_numeral = '01'
1209
+ # when 'loweralpha'
1210
+ # list_numeral = 'a'
1211
+ # when 'upperalpha'
1212
+ # list_numeral = 'A'
1213
+ # when 'lowerroman'
1214
+ # list_numeral = RomanNumeral.new 'i'
1215
+ # when 'upperroman'
1216
+ # list_numeral = RomanNumeral.new 'I'
1217
+ # when 'lowergreek'
1218
+ # list_numeral = LowercaseGreekA
1219
+ # when 'unstyled', 'unnumbered', 'no-bullet'
1220
+ # list_numeral = nil
1221
+ # when 'none'
1222
+ # list_numeral = ''
1223
+ # else
1224
+ # list_numeral = 1
1225
+ # end
1226
+ # if list_numeral && list_numeral != '' &&
1227
+ # (start = (node.attr 'start', nil, false) || ((node.option? 'reversed') ? node.items.size : nil))
1228
+ # if (start = start.to_i) > 1
1229
+ # (start - 1).times { list_numeral = list_numeral.next }
1230
+ # elsif start < 1 && !(::String === list_numeral)
1231
+ # (start - 1).abs.times { list_numeral = list_numeral.pred }
1232
+ # end
1233
+ # end
1234
+ # @list_numerals << list_numeral
1235
+ # convert_outline_list node
1236
+ # @list_numerals.pop
1237
+ # end
1238
+
1239
+ # def convert_ulist node
1240
+ # add_dest_for_block node if node.id
1241
+ # # TODO: move bullet_type to method on List (or helper method)
1242
+ # if node.option? 'checklist'
1243
+ # @list_bullets << :checkbox
1244
+ # else
1245
+ # if (style = node.style)
1246
+ # case style
1247
+ # when 'bibliography'
1248
+ # bullet_type = :square
1249
+ # when 'unstyled', 'no-bullet'
1250
+ # bullet_type = nil
1251
+ # else
1252
+ # candidate = style.to_sym
1253
+ # if Bullets.key? candidate
1254
+ # bullet_type = candidate
1255
+ # else
1256
+ # logger.warn %(unknown unordered list style: #{candidate}) unless scratch?
1257
+ # bullet_type = :disc
1258
+ # end
1259
+ # end
1260
+ # else
1261
+ # case node.outline_level
1262
+ # when 1
1263
+ # bullet_type = :disc
1264
+ # when 2
1265
+ # bullet_type = :circle
1266
+ # else
1267
+ # bullet_type = :square
1268
+ # end
1269
+ # end
1270
+ # @list_bullets << bullet_type
1271
+ # end
1272
+ # convert_outline_list node
1273
+ # @list_bullets.pop
1274
+ # end
1275
+
1276
+ # def convert_outline_list node
1277
+ # # TODO: check if we're within one line of the bottom of the page
1278
+ # # and advance to the next page if so (similar to logic for section titles)
1279
+ # layout_caption node.title, category: :outline_list if node.title?
1280
+
1281
+ # opts = {}
1282
+ # if (align = resolve_alignment_from_role node.roles)
1283
+ # opts[:align] = align
1284
+ # elsif node.style == 'bibliography'
1285
+ # opts[:align] = :left
1286
+ # elsif (align = @theme.outline_list_text_align)
1287
+ # # NOTE: theme setting only affects alignment of list text (not nested blocks)
1288
+ # opts[:align] = align.to_sym
1289
+ # end
1290
+
1291
+ # line_metrics = calc_line_metrics @theme.base_line_height
1292
+ # complex = false
1293
+ # # ...or if we want to give all items in the list the same treatment
1294
+ # #complex = node.items.find(&:compound?) ? true : false
1295
+ # if (node.context == :ulist && !@list_bullets[-1]) || (node.context == :olist && !@list_numerals[-1])
1296
+ # if node.style == 'unstyled'
1297
+ # # unstyled takes away all indentation
1298
+ # list_indent = 0
1299
+ # elsif (list_indent = @theme.outline_list_indent || 0) > 0
1300
+ # # no-bullet aligns text with left-hand side of bullet position (as though there's no bullet)
1301
+ # list_indent = [list_indent - (rendered_width_of_string %(#{node.context == :ulist ? ?\u2022 : '1.'}x)), 0].max
1302
+ # end
1303
+ # else
1304
+ # list_indent = @theme.outline_list_indent || 0
1305
+ # end
1306
+ # indent list_indent do
1307
+ # node.items.each do |item|
1308
+ # allocate_space_for_list_item line_metrics
1309
+ # convert_outline_list_item item, node, opts
1310
+ # end
1311
+ # end
1312
+ # # NOTE: Children will provide the necessary bottom margin if last item is complex.
1313
+ # # However, don't leave gap at the bottom if list is nested in an outline list
1314
+ # unless complex || (node.nested? && node.parent.parent.outline?)
1315
+ # # correct bottom margin of last item
1316
+ # margin_bottom((@theme.prose_margin_bottom || 0) - (@theme.outline_list_item_spacing || 0))
1317
+ # end
1318
+ # end
1319
+
1320
+ # def convert_outline_list_item node, list, opts = {}
1321
+ # # TODO: move this to a draw_bullet (or draw_marker) method
1322
+ # marker_style = {}
1323
+ # marker_style[:font_color] = @theme.outline_list_marker_font_color || @font_color
1324
+ # marker_style[:font_family] = font_family
1325
+ # marker_style[:font_size] = font_size
1326
+ # marker_style[:line_height] = @theme.base_line_height
1327
+ # case (list_type = list.context)
1328
+ # when :dlist
1329
+ # # NOTE: list.style is 'qanda'
1330
+ # complex = node[1]&.compound?
1331
+ # @list_numerals << (index = @list_numerals.pop).next
1332
+ # marker = %(#{index}.)
1333
+ # when :olist
1334
+ # complex = node.compound?
1335
+ # if (index = @list_numerals.pop)
1336
+ # if index == ''
1337
+ # marker = ''
1338
+ # else
1339
+ # marker = %(#{index}.)
1340
+ # dir = (node.parent.option? 'reversed') ? :pred : :next
1341
+ # @list_numerals << (index.public_send dir)
1342
+ # end
1343
+ # end
1344
+ # else # :ulist
1345
+ # complex = node.compound?
1346
+ # if (marker_type = @list_bullets[-1])
1347
+ # if marker_type == :checkbox
1348
+ # # QUESTION should we remove marker indent if not a checkbox?
1349
+ # if node.attr? 'checkbox', nil, false
1350
+ # marker_type = (node.attr? 'checked', nil, false) ? :checked : :unchecked
1351
+ # marker = @theme[%(ulist_marker_#{marker_type}_content)] || BallotBox[marker_type]
1352
+ # end
1353
+ # else
1354
+ # marker = @theme[%(ulist_marker_#{marker_type}_content)] || Bullets[marker_type]
1355
+ # end
1356
+ # [:font_color, :font_family, :font_size, :line_height].each do |prop|
1357
+ # marker_style[prop] = @theme[%(ulist_marker_#{marker_type}_#{prop})] || @theme[%(ulist_marker_#{prop})] || marker_style[prop]
1358
+ # end if marker
1359
+ # end
1360
+ # end
1361
+
1362
+ # if marker
1363
+ # if marker_style[:font_family] == 'fa'
1364
+ # logger.info 'deprecated fa icon set found in theme; use fas, far, or fab instead' unless scratch?
1365
+ # marker_style[:font_family] = FontAwesomeIconSets.find {|candidate| (icon_font_data candidate).yaml[candidate].value? marker } || 'fas'
1366
+ # end
1367
+ # marker_gap = rendered_width_of_char 'x'
1368
+ # font marker_style[:font_family], size: marker_style[:font_size] do
1369
+ # marker_width = rendered_width_of_string marker
1370
+ # marker_height = height_of_typeset_text marker, line_height: marker_style[:line_height], single_line: true
1371
+ # start_position = -marker_width + -marker_gap
1372
+ # float do
1373
+ # start_new_page if @media == 'prepress' && cursor < marker_height
1374
+ # flow_bounding_box start_position, width: marker_width do
1375
+ # layout_prose marker,
1376
+ # align: :right,
1377
+ # character_spacing: -0.5,
1378
+ # color: marker_style[:font_color],
1379
+ # inline_format: false,
1380
+ # line_height: marker_style[:line_height],
1381
+ # margin: 0,
1382
+ # normalize: false,
1383
+ # single_line: true
1384
+ # end
1385
+ # end
1386
+ # end
1387
+ # end
1388
+
1389
+ # if complex
1390
+ # traverse_list_item node, list_type, (opts.merge normalize_line_height: true)
1391
+ # else
1392
+ # traverse_list_item node, list_type, (opts.merge margin_bottom: @theme.outline_list_item_spacing, normalize_line_height: true)
1393
+ # end
1394
+ # end
1395
+
1396
+ # def traverse_list_item node, list_type, opts = {}
1397
+ # if list_type == :dlist # qanda
1398
+ # terms, desc = node
1399
+ # terms.each {|term| layout_prose %(<em>#{term.text}</em>), (opts.merge margin_top: 0, margin_bottom: @theme.description_list_term_spacing) }
1400
+ # if desc
1401
+ # layout_prose desc.text, (opts.merge hyphenate: true) if desc.text?
1402
+ # traverse desc
1403
+ # end
1404
+ # else
1405
+ # if (primary_text = node.text).nil_or_empty?
1406
+ # layout_prose DummyText, opts unless node.blocks?
1407
+ # else
1408
+ # layout_prose primary_text, (opts.merge hyphenate: true)
1409
+ # end
1410
+ # traverse node
1411
+ # end
1412
+ # end
1413
+
1414
+ # def allocate_space_for_list_item line_metrics, number = 1, additional_gap = 0
1415
+ # advance_page if !at_page_top? && cursor < (line_metrics.height + line_metrics.leading + line_metrics.padding_top + additional_gap) * number
1416
+ # end
1417
+
1418
+ # def convert_image node, opts = {}
1419
+ # node.extend ::Asciidoctor::Image unless ::Asciidoctor::Image === node
1420
+ # target, image_format = node.target_and_format
1421
+
1422
+ # if image_format == 'gif' && !(defined? ::GMagick::Image)
1423
+ # logger.warn %(GIF image format not supported. Install the prawn-gmagick gem or convert #{target} to PNG.) unless scratch?
1424
+ # image_path = nil
1425
+ # elsif ::Base64 === target
1426
+ # image_path = target
1427
+ # elsif (image_path = resolve_image_path node, target, image_format, (opts.fetch :relative_to_imagesdir, true))
1428
+ # if image_format == 'pdf'
1429
+ # if ::File.readable? image_path
1430
+ # if (id = node.id)
1431
+ # add_dest_block = proc do
1432
+ # node.set_attr 'pdf-destination', (node_dest = dest_top)
1433
+ # add_dest id, node_dest
1434
+ # end
1435
+ # end
1436
+ # # NOTE: import_page automatically advances to next page afterwards
1437
+ # # QUESTION should we add destination to top of imported page?
1438
+ # if (pgnums = node.attr 'pages', nil, false)
1439
+ # (resolve_pagenums pgnums).each_with_index do |pgnum, idx|
1440
+ # if idx == 0
1441
+ # import_page image_path, page: pgnum, replace: page.empty?, &add_dest_block
1442
+ # else
1443
+ # import_page image_path, page: pgnum, replace: true
1444
+ # end
1445
+ # end
1446
+ # else
1447
+ # import_page image_path, page: [(node.attr 'page', nil, 1).to_i, 1].max, replace: page.empty?, &add_dest_block
1448
+ # end
1449
+ # return
1450
+ # else
1451
+ # logger.warn %(pdf to insert not found or not readable: #{image_path}) unless scratch?
1452
+ # image_path = nil
1453
+ # end
1454
+ # elsif !(::File.readable? image_path)
1455
+ # logger.warn %(image to embed not found or not readable: #{image_path}) unless scratch?
1456
+ # image_path = nil
1457
+ # end
1458
+ # end
1459
+
1460
+ # theme_margin :block, :top unless (pinned = opts[:pinned])
1461
+
1462
+ # return on_image_error :missing, node, target, opts unless image_path
1463
+
1464
+ # alignment = ((node.attr 'align', nil, false) || (resolve_alignment_from_role node.roles) || @theme.image_align || :left).to_sym
1465
+ # # TODO: support cover (aka canvas) image layout using "canvas" (or "cover") role
1466
+ # width = resolve_explicit_width node.attributes, bounds_width: (available_w = bounds.width), support_vw: true, use_fallback: true, constrain_to_bounds: true
1467
+ # # TODO: add `to_pt page_width` method to ViewportWidth type
1468
+ # width = (width.to_f / 100) * page_width if ViewportWidth === width
1469
+
1470
+ # # NOTE: if width is not set explicitly and max-width is fit-content, caption height may not be accurate
1471
+ # caption_h = node.title? ? (layout_caption node, category: :image, side: :bottom, block_align: alignment, block_width: width, max_width: @theme.image_caption_max_width, dry_run: true) : 0
1472
+
1473
+ # align_to_page = node.option? 'align-to-page'
1474
+
1475
+ # begin
1476
+ # rendered_w = nil
1477
+ # span_page_width_if align_to_page do
1478
+ # if image_format == 'svg'
1479
+ # if ::Base64 === image_path
1480
+ # svg_data = ::Base64.decode64 image_path
1481
+ # file_request_root = false
1482
+ # else
1483
+ # svg_data = ::File.read image_path, mode: 'r:UTF-8'
1484
+ # file_request_root = ::File.dirname image_path
1485
+ # end
1486
+ # svg_obj = ::Prawn::SVG::Interface.new svg_data, self,
1487
+ # position: alignment,
1488
+ # width: width,
1489
+ # fallback_font_name: fallback_svg_font_name,
1490
+ # enable_web_requests: allow_uri_read ? (method :load_open_uri).to_proc : false,
1491
+ # enable_file_requests_with_root: file_request_root,
1492
+ # cache_images: cache_uri
1493
+ # rendered_w = (svg_size = svg_obj.document.sizing).output_width
1494
+ # if !width && (svg_obj.document.root.attributes.key? 'width')
1495
+ # # NOTE: scale native width & height from px to pt and restrict width to available width
1496
+ # if (adjusted_w = [available_w, (to_pt rendered_w, :px)].min) != rendered_w
1497
+ # svg_size = svg_obj.resize width: (rendered_w = adjusted_w)
1498
+ # end
1499
+ # end
1500
+ # # NOTE: shrink image so it fits within available space; group image & caption
1501
+ # if (rendered_h = svg_size.output_height) > (available_h = cursor - caption_h)
1502
+ # unless pinned || at_page_top?
1503
+ # advance_page
1504
+ # available_h = cursor - caption_h
1505
+ # end
1506
+ # rendered_w = (svg_obj.resize height: (rendered_h = available_h)).output_width if rendered_h > available_h
1507
+ # end
1508
+ # image_y = y
1509
+ # image_cursor = cursor
1510
+ # add_dest_for_block node if node.id
1511
+ # # NOTE: workaround to fix Prawn not adding fill and stroke commands on page that only has an image;
1512
+ # # breakage occurs when running content (stamps) are added to page
1513
+ # # seems to be resolved as of Prawn 2.2.2
1514
+ # #update_colors if graphic_state.color_space.empty?
1515
+ # # NOTE: prawn-svg 0.24.0, 0.25.0, & 0.25.1 didn't restore font after call to draw (see mogest/prawn-svg#80)
1516
+ # # NOTE: cursor advances automatically
1517
+ # svg_obj.draw
1518
+ # svg_obj.document.warnings.each do |img_warning|
1519
+ # logger.warn %(problem encountered in image: #{image_path}; #{img_warning})
1520
+ # end unless scratch?
1521
+ # draw_image_border image_cursor, rendered_w, rendered_h, alignment unless node.role? && (node.has_role? 'noborder')
1522
+ # if (link = node.attr 'link', nil, false)
1523
+ # add_link_to_image link, { width: rendered_w, height: rendered_h }, position: alignment, y: image_y
1524
+ # end
1525
+ # else
1526
+ # # FIXME: this code really needs to be better organized!
1527
+ # # NOTE: use low-level API to access intrinsic dimensions; build_image_object caches image data previously loaded
1528
+ # image_obj, image_info = ::Base64 === image_path ?
1529
+ # ::StringIO.open((::Base64.decode64 image_path), 'rb') {|fd| build_image_object fd } :
1530
+ # ::File.open(image_path, 'rb') {|fd| build_image_object fd }
1531
+ # # NOTE: if width is not specified, scale native width & height from px to pt and restrict width to available width
1532
+ # rendered_w, rendered_h = image_info.calc_image_dimensions width: (width || [available_w, (to_pt image_info.width, :px)].min)
1533
+ # # NOTE: shrink image so it fits within available space; group image & caption
1534
+ # if rendered_h > (available_h = cursor - caption_h)
1535
+ # unless pinned || at_page_top?
1536
+ # advance_page
1537
+ # available_h = cursor - caption_h
1538
+ # end
1539
+ # rendered_w, rendered_h = image_info.calc_image_dimensions height: available_h if rendered_h > available_h
1540
+ # end
1541
+ # image_y = y
1542
+ # image_cursor = cursor
1543
+ # add_dest_for_block node if node.id
1544
+ # # NOTE: workaround to fix Prawn not adding fill and stroke commands on page that only has an image;
1545
+ # # breakage occurs when running content (stamps) are added to page
1546
+ # # seems to be resolved as of Prawn 2.2.2
1547
+ # #update_colors if graphic_state.color_space.empty?
1548
+ # # NOTE: specify both width and height to avoid recalculation
1549
+ # embed_image image_obj, image_info, width: rendered_w, height: rendered_h, position: alignment
1550
+ # draw_image_border image_cursor, rendered_w, rendered_h, alignment unless node.role? && (node.has_role? 'noborder')
1551
+ # if (link = node.attr 'link', nil, false)
1552
+ # add_link_to_image link, { width: rendered_w, height: rendered_h }, position: alignment, y: image_y
1553
+ # end
1554
+ # # NOTE: Asciidoctor disables automatic advancement of cursor for raster images, so move cursor manually
1555
+ # move_down rendered_h if y == image_y
1556
+ # end
1557
+ # end
1558
+ # layout_caption node, category: :image, side: :bottom, block_align: alignment, block_width: rendered_w, max_width: @theme.image_caption_max_width if node.title?
1559
+ # theme_margin :block, :bottom unless pinned
1560
+ # rescue
1561
+ # on_image_error :exception, node, target, (opts.merge message: %(could not embed image: #{image_path}; #{$!.message}#{::Prawn::Errors::UnsupportedImageType === $! && !(defined? ::GMagick::Image) ? '; install prawn-gmagick gem to add support' : ''}))
1562
+ # end
1563
+ # end
1564
+
1565
+ # def draw_image_border top, w, h, alignment
1566
+ # if (@theme.image_border_width || 0) > 0 && @theme.image_border_color
1567
+ # if (@theme.image_border_fit || 'content') == 'auto'
1568
+ # bb_width = bounds.width
1569
+ # elsif alignment == :center
1570
+ # bb_x = (bounds.width - w) * 0.5
1571
+ # elsif alignment == :right
1572
+ # bb_x = bounds.width - w
1573
+ # end
1574
+ # bounding_box [(bb_x || 0), top], width: (bb_width || w), height: h, position: alignment do
1575
+ # theme_fill_and_stroke_bounds :image, background_color: nil
1576
+ # end
1577
+ # true
1578
+ # end
1579
+ # end
1580
+
1581
+ # def on_image_error _reason, node, target, opts = {}
1582
+ # logger.warn opts[:message] if (opts.key? :message) && !scratch?
1583
+ # alt_text_vars = { alt: (node.attr 'alt'), target: target }
1584
+ # alt_text_template = @theme.image_alt_content || '%{link}[%{alt}]%{/link} | <em>%{target}</em>'
1585
+ # return if alt_text_template.empty?
1586
+ # if (link = node.attr 'link', nil, false)
1587
+ # alt_text_vars[:link] = %(<a href="#{link}">)
1588
+ # alt_text_vars[:'/link'] = '</a>'
1589
+ # else
1590
+ # alt_text_vars[:link] = ''
1591
+ # alt_text_vars[:'/link'] = ''
1592
+ # end
1593
+ # theme_font :image_alt do
1594
+ # layout_prose alt_text_template % alt_text_vars,
1595
+ # align: ((node.attr 'align', nil, false) || @theme.image_align).to_sym,
1596
+ # margin: 0,
1597
+ # normalize: false,
1598
+ # single_line: true
1599
+ # end
1600
+ # layout_caption node, category: :image, side: :bottom if node.title?
1601
+ # theme_margin :block, :bottom unless opts[:pinned]
1602
+ # nil
1603
+ # end
1604
+
1605
+ # def convert_audio node
1606
+ # add_dest_for_block node if node.id
1607
+ # theme_margin :block, :top
1608
+ # audio_path = node.media_uri node.attr 'target'
1609
+ # play_symbol = (node.document.attr? 'icons', 'font') ? %(<font name="fas">#{(icon_font_data 'fas').unicode 'play'}</font>) : RightPointer
1610
+ # layout_prose %(#{play_symbol}#{NoBreakSpace}<a href="#{audio_path}">#{audio_path}</a> <em>(audio)</em>), normalize: false, margin: 0, single_line: true
1611
+ # layout_caption node, side: :bottom if node.title?
1612
+ # theme_margin :block, :bottom
1613
+ # end
1614
+
1615
+ # def convert_video node
1616
+ # case (poster = node.attr 'poster', nil, false)
1617
+ # when 'youtube'
1618
+ # video_path = %(https://www.youtube.com/watch?v=#{video_id = node.attr 'target'})
1619
+ # # see http://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
1620
+ # poster = allow_uri_read ? %(https://img.youtube.com/vi/#{video_id}/maxresdefault.jpg) : nil
1621
+ # type = 'YouTube video'
1622
+ # when 'vimeo'
1623
+ # video_path = %(https://vimeo.com/#{video_id = node.attr 'target'})
1624
+ # if allow_uri_read
1625
+ # poster = load_open_uri.open_uri %(http://vimeo.com/api/v2/video/#{video_id}.xml), 'r' do |f|
1626
+ # VimeoThumbnailRx =~ f.read && $1
1627
+ # end
1628
+ # end
1629
+ # type = 'Vimeo video'
1630
+ # else
1631
+ # video_path = node.media_uri node.attr 'target'
1632
+ # type = 'video'
1633
+ # end
1634
+
1635
+ # if poster.nil_or_empty?
1636
+ # add_dest_for_block node if node.id
1637
+ # theme_margin :block, :top
1638
+ # play_symbol = (node.document.attr? 'icons', 'font') ? %(<font name="fas">#{(icon_font_data 'fas').unicode 'play'}</font>) : RightPointer
1639
+ # layout_prose %(#{play_symbol}#{NoBreakSpace}<a href="#{video_path}">#{video_path}</a> <em>(#{type})</em>), normalize: false, margin: 0, single_line: true
1640
+ # layout_caption node, side: :bottom if node.title?
1641
+ # theme_margin :block, :bottom
1642
+ # else
1643
+ # original_attributes = node.attributes.dup
1644
+ # begin
1645
+ # node.update_attributes 'target' => poster, 'link' => video_path
1646
+ # #node.set_attr 'pdfwidth', '100%' unless (node.attr? 'width') || (node.attr? 'pdfwidth')
1647
+ # convert_image node
1648
+ # ensure
1649
+ # node.attributes.replace original_attributes
1650
+ # end
1651
+ # end
1652
+ # end
1653
+
1654
+ # # QUESTION can we avoid arranging fragments multiple times (conums & autofit) by eagerly preparing arranger?
1655
+ # def convert_listing_or_literal node
1656
+ # add_dest_for_block node if node.id
1657
+ # wrap_ext = source_chunks = bg_color_override = font_color_override = adjusted_font_size = nil
1658
+ # theme_font :code do
1659
+ # # HACK: disable built-in syntax highlighter; must be done before calling node.content!
1660
+ # if node.style == 'source' && (highlighter = (syntax_hl = node.document.syntax_highlighter) && syntax_hl.highlight? && syntax_hl.name)
1661
+ # case highlighter
1662
+ # when 'coderay'
1663
+ # unless defined? ::Asciidoctor::Prawn::CodeRayEncoder
1664
+ # highlighter = nil if (Helpers.require_library CodeRayRequirePath, 'coderay', :warn).nil?
1665
+ # end
1666
+ # when 'pygments'
1667
+ # unless defined? ::Pygments::Ext::BlockStyles
1668
+ # highlighter = nil if (Helpers.require_library PygmentsRequirePath, 'pygments.rb', :warn).nil?
1669
+ # end
1670
+ # when 'rouge'
1671
+ # unless defined? ::Rouge::Formatters::Prawn
1672
+ # highlighter = nil if (Helpers.require_library RougeRequirePath, 'rouge', :warn).nil?
1673
+ # end
1674
+ # end
1675
+ # prev_subs = (subs = node.subs).dup
1676
+ # # NOTE: the highlight sub is only set for coderay, rouge, and pygments atm
1677
+ # highlight_idx = subs.index :highlight
1678
+ # # NOTE: scratch? here only applies if listing block is nested inside another block
1679
+ # if !highlighter || scratch?
1680
+ # highlighter = nil
1681
+ # if highlight_idx
1682
+ # # switch the :highlight sub back to :specialcharacters
1683
+ # subs[highlight_idx] = :specialcharacters
1684
+ # else
1685
+ # prev_subs = nil
1686
+ # end
1687
+ # source_string = guard_indentation node.content
1688
+ # else
1689
+ # # NOTE: the source highlighter logic below handles the callouts and highlight subs
1690
+ # if highlight_idx
1691
+ # subs.delete_all :highlight, :callouts
1692
+ # else
1693
+ # subs.delete_all :specialcharacters, :callouts
1694
+ # end
1695
+ # # NOTE: indentation guards will be added by the source highlighter logic
1696
+ # source_string = expand_tabs node.content
1697
+ # end
1698
+ # else
1699
+ # highlighter = nil
1700
+ # source_string = guard_indentation node.content
1701
+ # end
1702
+
1703
+ # case highlighter
1704
+ # when 'coderay'
1705
+ # source_string, conum_mapping = extract_conums source_string
1706
+ # srclang = node.attr 'language', 'text', false
1707
+ # begin
1708
+ # ::CodeRay::Scanners[(srclang = (srclang.start_with? 'html+') ? (srclang.slice 5, srclang.length).to_sym : srclang.to_sym)]
1709
+ # rescue ::ArgumentError
1710
+ # srclang = :text
1711
+ # end
1712
+ # fragments = (::CodeRay.scan source_string, srclang).to_prawn
1713
+ # source_chunks = conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
1714
+ # when 'pygments'
1715
+ # style = (node.document.attr 'pygments-style') || 'pastie'
1716
+ # # QUESTION allow border color to be set by theme for highlighted block?
1717
+ # pg_block_styles = ::Pygments::Ext::BlockStyles.for style
1718
+ # bg_color_override = pg_block_styles[:background_color]
1719
+ # font_color_override = pg_block_styles[:font_color]
1720
+ # if source_string.empty?
1721
+ # source_chunks = []
1722
+ # else
1723
+ # lexer = (::Pygments::Lexer.find_by_alias node.attr 'language', 'text', false) || (::Pygments::Lexer.find_by_mimetype 'text/plain')
1724
+ # lexer_opts = { nowrap: true, noclasses: true, stripnl: false, style: style }
1725
+ # lexer_opts[:startinline] = !(node.option? 'mixed') if lexer.name == 'PHP'
1726
+ # source_string, conum_mapping = extract_conums source_string
1727
+ # # NOTE: highlight can return nil if something goes wrong; fallback to encoded source string if this happens
1728
+ # result = (lexer.highlight source_string, options: lexer_opts) || (node.apply_subs source_string, [:specialcharacters])
1729
+ # if node.attr? 'highlight', nil, false
1730
+ # if (highlight_lines = (node.method :resolve_lines_to_highlight).arity > 1 ?
1731
+ # (node.resolve_lines_to_highlight source_string, (node.attr 'highlight')) :
1732
+ # (node.resolve_lines_to_highlight node.attr 'highlight')).empty?
1733
+ # highlight_lines = nil
1734
+ # else
1735
+ # pg_highlight_bg_color = pg_block_styles[:highlight_background_color]
1736
+ # highlight_lines = highlight_lines.map {|linenum| [linenum, pg_highlight_bg_color] }.to_h
1737
+ # end
1738
+ # end
1739
+ # if node.attr? 'linenums'
1740
+ # linenums = (node.attr 'start', 1, false).to_i
1741
+ # @theme.code_linenum_font_color ||= '999999'
1742
+ # postprocess = true
1743
+ # wrap_ext = FormattedText::SourceWrap
1744
+ # elsif conum_mapping || highlight_lines
1745
+ # postprocess = true
1746
+ # end
1747
+ # fragments = text_formatter.format result
1748
+ # fragments = restore_conums fragments, conum_mapping, linenums, highlight_lines if postprocess
1749
+ # source_chunks = guard_indentation_in_fragments fragments
1750
+ # end
1751
+ # when 'rouge'
1752
+ # formatter = (@rouge_formatter ||= ::Rouge::Formatters::Prawn.new theme: (node.document.attr 'rouge-style'), line_gap: @theme.code_line_gap, highlight_background_color: @theme.code_highlight_background_color)
1753
+ # # QUESTION allow border color to be set by theme for highlighted block?
1754
+ # bg_color_override = formatter.background_color
1755
+ # if source_string.empty?
1756
+ # source_chunks = []
1757
+ # else
1758
+ # if node.attr? 'linenums'
1759
+ # formatter_opts = { line_numbers: true, start_line: (node.attr 'start', 1, false).to_i }
1760
+ # wrap_ext = FormattedText::SourceWrap
1761
+ # else
1762
+ # formatter_opts = {}
1763
+ # end
1764
+ # if (srclang = node.attr 'language', nil, false)
1765
+ # if srclang.include? '?'
1766
+ # if (lexer = ::Rouge::Lexer.find_fancy srclang)
1767
+ # unless lexer.tag != 'php' || (node.option? 'mixed') || ((lexer_opts = lexer.options).key? 'start_inline')
1768
+ # lexer = lexer.class.new lexer_opts.merge 'start_inline' => true
1769
+ # end
1770
+ # end
1771
+ # elsif (lexer = ::Rouge::Lexer.find srclang)
1772
+ # lexer = lexer.new start_inline: true if lexer.tag == 'php' && !(node.option? 'mixed')
1773
+ # end
1774
+ # end
1775
+ # lexer ||= ::Rouge::Lexers::PlainText
1776
+ # source_string, conum_mapping = extract_conums source_string
1777
+ # if node.attr? 'highlight', nil, false
1778
+ # unless (hl_lines = (node.method :resolve_lines_to_highlight).arity > 1 ?
1779
+ # (node.resolve_lines_to_highlight source_string, (node.attr 'highlight')) :
1780
+ # (node.resolve_lines_to_highlight node.attr 'highlight')).empty?
1781
+ # formatter_opts[:highlight_lines] = hl_lines.map {|linenum| [linenum, true] }.to_h
1782
+ # end
1783
+ # end
1784
+ # fragments = formatter.format (lexer.lex source_string), formatter_opts
1785
+ # source_chunks = conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
1786
+ # end
1787
+ # else
1788
+ # # NOTE: only format if we detect a need (callouts or inline formatting)
1789
+ # source_chunks = (XMLMarkupRx.match? source_string) ? (text_formatter.format source_string) : [text: source_string]
1790
+ # end
1791
+ # node.subs.replace prev_subs if prev_subs
1792
+ # adjusted_font_size = ((node.option? 'autofit') || (node.document.attr? 'autofit-option')) ? (compute_autofit_font_size source_chunks, :code) : nil
1793
+ # end
1794
+
1795
+ # theme_margin :block, :top
1796
+
1797
+ # keep_together do |box_height = nil|
1798
+ # caption_height = node.title? ? (layout_caption node, category: :code) : 0
1799
+ # theme_font :code do
1800
+ # theme_fill_and_stroke_block :code, (box_height - caption_height), background_color: bg_color_override, split_from_top: false if box_height
1801
+ # pad_box @theme.code_padding do
1802
+ # ::Prawn::Text::Formatted::Box.extensions << wrap_ext if wrap_ext
1803
+ # typeset_formatted_text source_chunks, (calc_line_metrics @theme.code_line_height || @theme.base_line_height),
1804
+ # color: (font_color_override || @theme.code_font_color || @font_color),
1805
+ # size: adjusted_font_size
1806
+ # ::Prawn::Text::Formatted::Box.extensions.pop if wrap_ext
1807
+ # end
1808
+ # end
1809
+ # end
1810
+
1811
+ # stroke_horizontal_rule @theme.caption_border_bottom_color if node.title? && @theme.caption_border_bottom_color
1812
+
1813
+ # theme_margin :block, :bottom
1814
+ # end
1815
+
1816
+ # alias convert_listing convert_listing_or_literal
1817
+ # alias convert_literal convert_listing_or_literal
1818
+
1819
+ # def convert_pass node
1820
+ # node = node.dup
1821
+ # (subs = node.subs.dup).unshift :specialcharacters
1822
+ # node.instance_variable_set :@subs, subs.uniq
1823
+ # convert_listing_or_literal node
1824
+ # end
1825
+
1826
+ # alias convert_stem convert_listing_or_literal
1827
+
1828
+ # # Extract callout marks from string, indexed by 0-based line number
1829
+ # # Return an Array with the processed string as the first argument
1830
+ # # and the mapping of lines to conums as the second.
1831
+ # def extract_conums string
1832
+ # conum_mapping = {}
1833
+ # auto_num = 0
1834
+ # string = string.split(LF).map.with_index {|line, line_num|
1835
+ # # FIXME: we get extra spaces before numbers if more than one on a line
1836
+ # if line.include? '<'
1837
+ # line = line.gsub CalloutExtractRx do
1838
+ # # honor the escape
1839
+ # if $1 == ?\\
1840
+ # $&.sub $1, ''
1841
+ # else
1842
+ # (conum_mapping[line_num] ||= []) << ($3 == '.' ? (auto_num += 1) : $3.to_i)
1843
+ # ''
1844
+ # end
1845
+ # end
1846
+ # # NOTE use first position to store space that precedes conums
1847
+ # if (conum_mapping.key? line_num) && (line.end_with? ' ')
1848
+ # trimmed_line = line.rstrip
1849
+ # conum_mapping[line_num].unshift line.slice trimmed_line.length, line.length
1850
+ # line = trimmed_line
1851
+ # end
1852
+ # end
1853
+ # line
1854
+ # }.join LF
1855
+ # conum_mapping = nil if conum_mapping.empty?
1856
+ # [string, conum_mapping]
1857
+ # end
1858
+
1859
+ # # Restore the conums into the Array of formatted text fragments
1860
+ # #--
1861
+ # # QUESTION can this be done more efficiently?
1862
+ # # QUESTION can we reuse arrange_fragments_by_line?
1863
+ # def restore_conums fragments, conum_mapping, linenums = nil, highlight_lines = nil
1864
+ # lines = []
1865
+ # line_num = 0
1866
+ # # reorganize the fragments into an array of lines
1867
+ # fragments.each do |fragment|
1868
+ # line = (lines[line_num] ||= [])
1869
+ # if (text = fragment[:text]) == LF
1870
+ # lines[line_num += 1] ||= []
1871
+ # elsif text.include? LF
1872
+ # text.split(LF, -1).each_with_index do |line_in_fragment, idx|
1873
+ # line = (lines[line_num += 1] ||= []) unless idx == 0
1874
+ # line << (fragment.merge text: line_in_fragment) unless line_in_fragment.empty?
1875
+ # end
1876
+ # else
1877
+ # line << fragment
1878
+ # end
1879
+ # end
1880
+ # conum_font_color = @theme.conum_font_color
1881
+ # if (conum_font_name = @theme.conum_font_family) == font_name
1882
+ # conum_font_name = nil
1883
+ # end
1884
+ # last_line_num = lines.size - 1
1885
+ # if linenums
1886
+ # pad_size = (last_line_num + 1).to_s.length
1887
+ # linenum_color = @theme.code_linenum_font_color
1888
+ # end
1889
+ # # append conums to appropriate lines, then flatten to an array of fragments
1890
+ # lines.flat_map.with_index do |line, cur_line_num|
1891
+ # last_line = cur_line_num == last_line_num
1892
+ # visible_line_num = cur_line_num + (linenums || 1)
1893
+ # if highlight_lines && (highlight_bg_color = highlight_lines[visible_line_num])
1894
+ # line.unshift text: DummyText, background_color: highlight_bg_color, highlight: true, inline_block: true, extend: true, width: 0, callback: [FormattedText::TextBackgroundAndBorderRenderer]
1895
+ # end
1896
+ # line.unshift text: %(#{visible_line_num.to_s.rjust pad_size} ), linenum: visible_line_num, color: linenum_color if linenums
1897
+ # if conum_mapping && (conums = conum_mapping.delete cur_line_num)
1898
+ # line << { text: conums.shift } if ::String === conums[0]
1899
+ # conum_text = conums.map {|num| conum_glyph num }.join ' '
1900
+ # conum_fragment = { text: conum_text }
1901
+ # conum_fragment[:color] = conum_font_color if conum_font_color
1902
+ # conum_fragment[:font] = conum_font_name if conum_font_name
1903
+ # line << conum_fragment
1904
+ # end
1905
+ # line << { text: LF } unless last_line
1906
+ # line
1907
+ # end
1908
+ # end
1909
+
1910
+ # def conum_glyph number
1911
+ # @conum_glyphs[number - 1]
1912
+ # end
1913
+
1914
+ # def convert_table node
1915
+ # add_dest_for_block node if node.id
1916
+ # # TODO: we could skip a lot of the logic below when num_rows == 0
1917
+ # num_rows = node.attr 'rowcount'
1918
+ # num_cols = node.columns.size
1919
+ # table_header_size = false
1920
+ # theme = @theme
1921
+
1922
+ # tbl_bg_color = resolve_theme_color :table_background_color
1923
+ # # QUESTION should we fallback to page background color? (which is never transparent)
1924
+ # #tbl_bg_color = resolve_theme_color :table_background_color, @page_bg_color
1925
+ # # ...and if so, should we try to be helpful and use @page_bg_color for tables nested in blocks?
1926
+ # #unless tbl_bg_color
1927
+ # # tbl_bg_color = @page_bg_color unless [:section, :document].include? node.parent.context
1928
+ # #end
1929
+
1930
+ # # NOTE: emulate table bg color by using it as a fallback value for each element
1931
+ # head_bg_color = resolve_theme_color :table_head_background_color, tbl_bg_color
1932
+ # foot_bg_color = resolve_theme_color :table_foot_background_color, tbl_bg_color
1933
+ # body_bg_color = resolve_theme_color :table_body_background_color, tbl_bg_color
1934
+ # body_stripe_bg_color = resolve_theme_color :table_body_stripe_background_color, tbl_bg_color
1935
+
1936
+ # base_header_cell_data = nil
1937
+ # header_cell_line_metrics = nil
1938
+
1939
+ # table_data = []
1940
+ # theme_font :table do
1941
+ # head_rows = node.rows[:head]
1942
+ # body_rows = node.rows[:body]
1943
+ # #if (hrows = node.attr 'hrows', false, nil) && (shift_rows = hrows.to_i - head_rows.size) > 0
1944
+ # # head_rows = head_rows.dup
1945
+ # # body_rows = body_rows.dup
1946
+ # # shift_rows.times { head_rows << body_rows.shift unless body_rows.empty? }
1947
+ # #end
1948
+ # theme_font :table_head do
1949
+ # table_header_size = head_rows.size
1950
+ # head_font_info = font_info
1951
+ # head_line_metrics = calc_line_metrics theme.base_line_height
1952
+ # head_cell_padding = theme.table_head_cell_padding || theme.table_cell_padding
1953
+ # head_cell_padding = ::Array === head_cell_padding && head_cell_padding.size == 4 ? head_cell_padding.dup : (expand_padding_value head_cell_padding)
1954
+ # head_cell_padding[0] += head_line_metrics.padding_top
1955
+ # head_cell_padding[2] += head_line_metrics.padding_bottom
1956
+ # # QUESTION why doesn't text transform inherit from table?
1957
+ # head_transform = resolve_text_transform :table_head_text_transform, nil
1958
+ # base_cell_data = {
1959
+ # inline_format: [normalize: true],
1960
+ # background_color: head_bg_color,
1961
+ # text_color: @font_color,
1962
+ # size: head_font_info[:size],
1963
+ # font: head_font_info[:family],
1964
+ # font_style: head_font_info[:style],
1965
+ # kerning: default_kerning?,
1966
+ # padding: head_cell_padding,
1967
+ # leading: head_line_metrics.leading,
1968
+ # # TODO: patch prawn-table to pass through final_gap option
1969
+ # #final_gap: head_line_metrics.final_gap,
1970
+ # }
1971
+ # head_rows.each do |row|
1972
+ # table_data << (row.map do |cell|
1973
+ # cell_text = head_transform ? (transform_text cell.text.strip, head_transform) : cell.text.strip
1974
+ # cell_text = hyphenate_text cell_text, @hyphenator if defined? @hyphenator
1975
+ # base_cell_data.merge \
1976
+ # content: cell_text,
1977
+ # colspan: cell.colspan || 1,
1978
+ # align: (cell.attr 'halign', nil, false).to_sym,
1979
+ # valign: (val = cell.attr 'valign', nil, false) == 'middle' ? :center : val.to_sym
1980
+ # end)
1981
+ # end
1982
+ # end unless head_rows.empty?
1983
+
1984
+ # base_cell_data = {
1985
+ # font: (body_font_info = font_info)[:family],
1986
+ # font_style: body_font_info[:style],
1987
+ # size: body_font_info[:size],
1988
+ # kerning: default_kerning?,
1989
+ # text_color: @font_color,
1990
+ # }
1991
+ # body_cell_line_metrics = calc_line_metrics theme.base_line_height
1992
+ # (body_rows + node.rows[:foot]).each do |row|
1993
+ # table_data << (row.map do |cell|
1994
+ # cell_data = base_cell_data.merge \
1995
+ # colspan: cell.colspan || 1,
1996
+ # rowspan: cell.rowspan || 1,
1997
+ # align: (cell.attr 'halign', nil, false).to_sym,
1998
+ # valign: (val = cell.attr 'valign', nil, false) == 'middle' ? :center : val.to_sym
1999
+ # cell_line_metrics = body_cell_line_metrics
2000
+ # case cell.style
2001
+ # when :emphasis
2002
+ # cell_data[:font_style] = :italic
2003
+ # when :strong
2004
+ # cell_data[:font_style] = :bold
2005
+ # when :header
2006
+ # unless base_header_cell_data
2007
+ # theme_font :table_head do
2008
+ # theme_font :table_header_cell do
2009
+ # header_cell_font_info = font_info
2010
+ # base_header_cell_data = {
2011
+ # text_color: @font_color,
2012
+ # font: header_cell_font_info[:family],
2013
+ # size: header_cell_font_info[:size],
2014
+ # font_style: header_cell_font_info[:style],
2015
+ # text_transform: @text_transform,
2016
+ # }
2017
+ # header_cell_line_metrics = calc_line_metrics theme.base_line_height
2018
+ # end
2019
+ # end
2020
+ # if (val = resolve_theme_color :table_header_cell_background_color, head_bg_color)
2021
+ # base_header_cell_data[:background_color] = val
2022
+ # end
2023
+ # end
2024
+ # cell_data.update base_header_cell_data
2025
+ # cell_transform = cell_data.delete :text_transform
2026
+ # cell_line_metrics = header_cell_line_metrics
2027
+ # when :monospaced
2028
+ # cell_data.delete :font_style
2029
+ # theme_font :literal do
2030
+ # mono_cell_font_info = font_info
2031
+ # cell_data[:font] = mono_cell_font_info[:family]
2032
+ # cell_data[:size] = mono_cell_font_info[:size]
2033
+ # cell_data[:text_color] = @font_color
2034
+ # cell_line_metrics = calc_line_metrics theme.base_line_height
2035
+ # end
2036
+ # when :literal
2037
+ # # NOTE: we want the raw AsciiDoc in this case
2038
+ # cell_data[:content] = guard_indentation cell.instance_variable_get :@text
2039
+ # # NOTE: the absence of the inline_format option implies it's disabled
2040
+ # cell_data.delete :font_style
2041
+ # # QUESTION should we use literal_font_*, code_font_*, or introduce another category?
2042
+ # theme_font :code do
2043
+ # literal_cell_font_info = font_info
2044
+ # cell_data[:font] = literal_cell_font_info[:family]
2045
+ # cell_data[:size] = literal_cell_font_info[:size]
2046
+ # cell_data[:text_color] = @font_color
2047
+ # cell_line_metrics = calc_line_metrics theme.base_line_height
2048
+ # end
2049
+ # when :asciidoc
2050
+ # cell_data.delete :kerning
2051
+ # cell_data.delete :font_style
2052
+ # cell_line_metrics = nil
2053
+ # asciidoc_cell = ::Prawn::Table::Cell::AsciiDoc.new self,
2054
+ # (cell_data.merge content: cell.inner_document, font_style: (val = theme.table_font_style) ? val.to_sym : nil, padding: theme.table_cell_padding)
2055
+ # cell_data = { content: asciidoc_cell }
2056
+ # end
2057
+ # if cell_line_metrics
2058
+ # cell_padding = ::Array === (cell_padding = theme.table_cell_padding) && cell_padding.size == 4 ? cell_padding.dup : (expand_padding_value cell_padding)
2059
+ # cell_padding[0] += cell_line_metrics.padding_top
2060
+ # cell_padding[2] += cell_line_metrics.padding_bottom
2061
+ # cell_data[:leading] = cell_line_metrics.leading
2062
+ # # TODO: patch prawn-table to pass through final_gap option
2063
+ # #cell_data[:final_gap] = cell_line_metrics.final_gap
2064
+ # cell_data[:padding] = cell_padding
2065
+ # end
2066
+ # unless cell_data.key? :content
2067
+ # cell_text = cell.text.strip
2068
+ # cell_text = transform_text cell_text, cell_transform if cell_transform
2069
+ # cell_text = hyphenate_text cell_text, @hyphenator if defined? @hyphenator
2070
+ # cell_text = cell_text.gsub CjkLineBreakRx, ZeroWidthSpace if @cjk_line_breaks
2071
+ # if cell_text.include? DoubleLF
2072
+ # # FIXME: hard breaks not quite the same result as separate paragraphs; need custom cell impl here
2073
+ # cell_data[:content] = (cell_text.split BlankLineRx).map {|l| l.tr_s WhitespaceChars, ' ' }.join DoubleLF
2074
+ # cell_data[:inline_format] = true
2075
+ # else
2076
+ # cell_data[:content] = cell_text
2077
+ # cell_data[:inline_format] = [normalize: true]
2078
+ # end
2079
+ # end
2080
+ # if node.document.attr? 'cellbgcolor'
2081
+ # if (cell_bg_color = node.document.attr 'cellbgcolor') == 'transparent'
2082
+ # cell_data[:background_color] = body_bg_color
2083
+ # elsif (cell_bg_color.start_with? '#') && (HexColorRx.match? cell_bg_color)
2084
+ # cell_data[:background_color] = cell_bg_color.slice 1, cell_bg_color.length
2085
+ # end
2086
+ # end
2087
+ # cell_data
2088
+ # end)
2089
+ # end
2090
+ # end
2091
+
2092
+ # # NOTE: Prawn aborts if table data is empty, so ensure there's at least one row
2093
+ # if table_data.empty?
2094
+ # logger.warn message_with_context 'no rows found in table', source_location: node.source_location
2095
+ # table_data << ::Array.new([node.columns.size, 1].max) { { content: '' } }
2096
+ # end
2097
+
2098
+ # border_width = {}
2099
+ # table_border_color = theme.table_border_color || theme.table_grid_color || theme.base_border_color
2100
+ # table_border_style = (theme.table_border_style || :solid).to_sym
2101
+ # table_border_width = theme.table_border_width
2102
+ # if table_header_size
2103
+ # head_border_bottom_color = theme.table_head_border_bottom_color || table_border_color
2104
+ # head_border_bottom_style = (theme.table_head_border_bottom_style || table_border_style).to_sym
2105
+ # head_border_bottom_width = theme.table_head_border_bottom_width || table_border_width
2106
+ # end
2107
+ # [:top, :bottom, :left, :right].each {|edge| border_width[edge] = table_border_width }
2108
+ # table_grid_color = theme.table_grid_color || table_border_color
2109
+ # table_grid_style = (theme.table_grid_style || table_border_style).to_sym
2110
+ # table_grid_width = theme.table_grid_width || theme.table_border_width
2111
+ # [:cols, :rows].each {|edge| border_width[edge] = table_grid_width }
2112
+
2113
+ # case (grid = node.attr 'grid', 'all', 'table-grid')
2114
+ # when 'all'
2115
+ # # keep inner borders
2116
+ # when 'cols'
2117
+ # border_width[:rows] = 0
2118
+ # when 'rows'
2119
+ # border_width[:cols] = 0
2120
+ # else # none
2121
+ # border_width[:rows] = border_width[:cols] = 0
2122
+ # end
2123
+
2124
+ # case (frame = node.attr 'frame', 'all', 'table-frame')
2125
+ # when 'all'
2126
+ # # keep outer borders
2127
+ # when 'topbot', 'ends'
2128
+ # border_width[:left] = border_width[:right] = 0
2129
+ # when 'sides'
2130
+ # border_width[:top] = border_width[:bottom] = 0
2131
+ # else # none
2132
+ # border_width[:top] = border_width[:right] = border_width[:bottom] = border_width[:left] = 0
2133
+ # end
2134
+
2135
+ # if node.option? 'autowidth'
2136
+ # table_width = (node.attr? 'width', nil, false) ? bounds.width * ((node.attr 'tablepcwidth') / 100.0) :
2137
+ # (((node.has_role? 'stretch') || (node.has_role? 'spread')) ? bounds.width : nil)
2138
+ # column_widths = []
2139
+ # else
2140
+ # table_width = bounds.width * ((node.attr 'tablepcwidth') / 100.0)
2141
+ # column_widths = node.columns.map {|col| ((col.attr 'colpcwidth') * table_width) / 100.0 }
2142
+ # end
2143
+
2144
+ # if ((alignment = node.attr 'align', nil, false) && (BlockAlignmentNames.include? alignment)) ||
2145
+ # (alignment = (node.roles & BlockAlignmentNames)[-1])
2146
+ # alignment = alignment.to_sym
2147
+ # else
2148
+ # alignment = (theme.table_align || :left).to_sym
2149
+ # end
2150
+
2151
+ # caption_side = (theme.table_caption_side || :top).to_sym
2152
+ # caption_max_width = theme.table_caption_max_width || 'fit-content'
2153
+
2154
+ # table_settings = {
2155
+ # header: table_header_size,
2156
+ # # NOTE: position is handled by this method
2157
+ # position: :left,
2158
+ # cell_style: {
2159
+ # # NOTE: the border color and style of the outer frame is set later
2160
+ # border_color: table_grid_color,
2161
+ # border_lines: [table_grid_style],
2162
+ # # NOTE: the border width is set later
2163
+ # border_width: 0,
2164
+ # },
2165
+ # width: table_width,
2166
+ # column_widths: column_widths,
2167
+ # }
2168
+
2169
+ # # QUESTION should we support nth; should we support sequence of roles?
2170
+ # case node.attr 'stripes', nil, 'table-stripes'
2171
+ # when 'all'
2172
+ # table_settings[:row_colors] = [body_stripe_bg_color]
2173
+ # when 'even'
2174
+ # table_settings[:row_colors] = [body_bg_color, body_stripe_bg_color]
2175
+ # when 'odd'
2176
+ # table_settings[:row_colors] = [body_stripe_bg_color, body_bg_color]
2177
+ # else # none
2178
+ # table_settings[:row_colors] = [body_bg_color]
2179
+ # end
2180
+
2181
+ # theme_margin :block, :top
2182
+
2183
+ # left_padding = right_padding = nil
2184
+ # table table_data, table_settings do
2185
+ # # NOTE: call width to capture resolved table width
2186
+ # table_width = width
2187
+ # @pdf.layout_table_caption node, alignment, table_width, caption_max_width if node.title? && caption_side == :top
2188
+ # # NOTE align using padding instead of bounding_box as prawn-table does
2189
+ # # using a bounding_box across pages mangles the margin box of subsequent pages
2190
+ # if alignment != :left && table_width != (this_bounds = @pdf.bounds).width
2191
+ # if alignment == :center
2192
+ # left_padding = right_padding = (this_bounds.width - width) * 0.5
2193
+ # this_bounds.add_left_padding left_padding
2194
+ # this_bounds.add_right_padding right_padding
2195
+ # else # :right
2196
+ # left_padding = this_bounds.width - width
2197
+ # this_bounds.add_left_padding left_padding
2198
+ # end
2199
+ # end
2200
+ # if grid == 'none' && frame == 'none'
2201
+ # rows(table_header_size).tap do |r|
2202
+ # r.border_bottom_color = head_border_bottom_color
2203
+ # r.border_bottom_line = head_border_bottom_style
2204
+ # r.border_bottom_width = head_border_bottom_width
2205
+ # end if table_header_size
2206
+ # else
2207
+ # # apply the grid setting first across all cells
2208
+ # cells.border_width = [border_width[:rows], border_width[:cols], border_width[:rows], border_width[:cols]]
2209
+
2210
+ # if table_header_size
2211
+ # rows(table_header_size - 1).tap do |r|
2212
+ # r.border_bottom_color = head_border_bottom_color
2213
+ # r.border_bottom_line = head_border_bottom_style
2214
+ # r.border_bottom_width = head_border_bottom_width
2215
+ # end
2216
+ # rows(table_header_size).tap do |r|
2217
+ # r.border_top_color = head_border_bottom_color
2218
+ # r.border_top_line = head_border_bottom_style
2219
+ # r.border_top_width = head_border_bottom_width
2220
+ # end if num_rows > table_header_size
2221
+ # end
2222
+
2223
+ # # top edge of table
2224
+ # rows(0).tap do |r|
2225
+ # r.border_top_color, r.border_top_line, r.border_top_width = table_border_color, table_border_style, border_width[:top]
2226
+ # end
2227
+ # # right edge of table
2228
+ # columns(num_cols - 1).tap do |r|
2229
+ # r.border_right_color, r.border_right_line, r.border_right_width = table_border_color, table_border_style, border_width[:right]
2230
+ # end
2231
+ # # bottom edge of table
2232
+ # rows(num_rows - 1).tap do |r|
2233
+ # r.border_bottom_color, r.border_bottom_line, r.border_bottom_width = table_border_color, table_border_style, border_width[:bottom]
2234
+ # end
2235
+ # # left edge of table
2236
+ # columns(0).tap do |r|
2237
+ # r.border_left_color, r.border_left_line, r.border_left_width = table_border_color, table_border_style, border_width[:left]
2238
+ # end
2239
+ # end
2240
+
2241
+ # # QUESTION should cell padding be configurable for foot row cells?
2242
+ # unless node.rows[:foot].empty?
2243
+ # foot_row = row num_rows.pred
2244
+ # foot_row.background_color = foot_bg_color
2245
+ # # FIXME: find a way to do this when defining the cells
2246
+ # foot_row.text_color = theme.table_foot_font_color if theme.table_foot_font_color
2247
+ # foot_row.size = theme.table_foot_font_size if theme.table_foot_font_size
2248
+ # foot_row.font = theme.table_foot_font_family if theme.table_foot_font_family
2249
+ # foot_row.font_style = theme.table_foot_font_style.to_sym if theme.table_foot_font_style
2250
+ # # HACK: we should do this transformation when creating the cell
2251
+ # #if (foot_transform = resolve_text_transform :table_foot_text_transform, nil)
2252
+ # # foot_row.each {|c| c.content = (transform_text c.content, foot_transform) if c.content }
2253
+ # #end
2254
+ # end
2255
+ # end
2256
+ # if left_padding
2257
+ # bounds.subtract_left_padding left_padding
2258
+ # bounds.subtract_right_padding right_padding if right_padding
2259
+ # end
2260
+ # layout_table_caption node, alignment, table_width, caption_max_width, caption_side if node.title? && caption_side == :bottom
2261
+ # theme_margin :block, :bottom
2262
+ # end
2263
+
2264
+ # def convert_thematic_break _node
2265
+ # theme_margin :thematic_break, :top
2266
+ # stroke_horizontal_rule @theme.thematic_break_border_color, line_width: @theme.thematic_break_border_width, line_style: (@theme.thematic_break_border_style || :solid).to_sym
2267
+ # theme_margin :thematic_break, :bottom
2268
+ # end
2269
+
2270
+ # def convert_toc node
2271
+ # if ((doc = node.document).attr? 'toc-placement', 'macro') && doc.sections?
2272
+ # if (is_book = doc.doctype == 'book')
2273
+ # start_new_page unless at_page_top?
2274
+ # start_new_page if @ppbook && verso_page? && !(node.option? 'nonfacing')
2275
+ # end
2276
+ # add_dest_for_block node, (node.id || 'toc')
2277
+ # allocate_toc doc, (doc.attr 'toclevels', 2).to_i, @y, (is_book || (doc.attr? 'title-page'))
2278
+ # @disable_running_content[:header] = (@disable_running_content[:header] || ::Set.new) + @toc_extent[:page_nums] if node.option? 'noheader'
2279
+ # @disable_running_content[:footer] = (@disable_running_content[:footer] || ::Set.new) + @toc_extent[:page_nums] if node.option? 'nofooter'
2280
+ # end
2281
+ # nil
2282
+ # end
2283
+
2284
+ # # NOTE to insert sequential page breaks, you must put {nbsp} between page breaks
2285
+ # def convert_page_break node
2286
+ # if (page_layout = node.attr 'page-layout').nil_or_empty?
2287
+ # unless node.role? && (page_layout = (node.roles.map(&:to_sym) & PageLayouts)[-1])
2288
+ # page_layout = nil
2289
+ # end
2290
+ # elsif !PageLayouts.include?(page_layout = page_layout.to_sym)
2291
+ # page_layout = nil
2292
+ # end
2293
+
2294
+ # if at_page_top?
2295
+ # if page_layout && page_layout != page.layout && page.empty?
2296
+ # delete_page
2297
+ # advance_page layout: page_layout
2298
+ # end
2299
+ # elsif page_layout
2300
+ # advance_page layout: page_layout
2301
+ # else
2302
+ # advance_page
2303
+ # end
2304
+ # end
2305
+
2306
+ # def convert_index_section _node
2307
+ # space_needed_for_category = @theme.description_list_term_spacing + (2 * (height_of_typeset_text 'A'))
2308
+ # column_box [0, cursor], columns: 2, width: bounds.width, reflow_margins: true do
2309
+ # @index.categories.each do |category|
2310
+ # # NOTE cursor method always returns 0 inside column_box; breaks reference_bounds.move_past_bottom
2311
+ # bounds.move_past_bottom if space_needed_for_category > y - reference_bounds.absolute_bottom
2312
+ # layout_prose category.name,
2313
+ # align: :left,
2314
+ # inline_format: false,
2315
+ # margin_top: 0,
2316
+ # margin_bottom: @theme.description_list_term_spacing,
2317
+ # style: @theme.description_list_term_font_style.to_sym
2318
+ # category.terms.each do |term|
2319
+ # convert_index_list_item term
2320
+ # end
2321
+ # if @theme.prose_margin_bottom > y - reference_bounds.absolute_bottom
2322
+ # bounds.move_past_bottom
2323
+ # else
2324
+ # move_down @theme.prose_margin_bottom
2325
+ # end
2326
+ # end
2327
+ # end
2328
+ # nil
2329
+ # end
2330
+
2331
+ # def convert_index_list_item term
2332
+ # text = escape_xml term.name
2333
+ # unless term.container?
2334
+ # if @media == 'screen'
2335
+ # pagenums = term.dests.map {|dest| %(<a anchor="#{dest[:anchor]}">#{dest[:page]}</a>) }
2336
+ # else
2337
+ # pagenums = consolidate_ranges term.dests.uniq {|dest| dest[:page] }.map {|dest| dest[:page].to_s }
2338
+ # end
2339
+ # text = %(#{text}, #{pagenums.join ', '})
2340
+ # end
2341
+ # subterm_indent = @theme.description_list_description_indent
2342
+ # layout_prose text, align: :left, margin: 0, normalize_line_height: true, hanging_indent: subterm_indent * 2
2343
+ # indent subterm_indent do
2344
+ # term.subterms.each do |subterm|
2345
+ # convert_index_list_item subterm
2346
+ # end
2347
+ # end unless term.leaf?
2348
+ # end
2349
+
2350
+ # def convert_inline_anchor node
2351
+ # doc = node.document
2352
+ # target = node.target
2353
+ # case node.type
2354
+ # when :link
2355
+ # attrs = []
2356
+ # #attrs << %( id="#{node.id}") if node.id
2357
+ # if (role = node.role)
2358
+ # attrs << %( class="#{role}")
2359
+ # end
2360
+ # #attrs << %( title="#{node.attr 'title'}") if node.attr? 'title'
2361
+ # attrs << %( target="#{node.attr 'window'}") if node.attr? 'window', nil, false
2362
+ # if (@media ||= doc.attr 'media', 'screen') != 'screen' && (target.start_with? 'mailto:') && (doc.attr? 'hide-uri-scheme')
2363
+ # bare_target = target.slice 7, target.length
2364
+ # node.add_role 'bare' if (text = node.text) == bare_target
2365
+ # else
2366
+ # bare_target = target
2367
+ # text = node.text
2368
+ # end
2369
+ # if (role = node.attr 'role', nil, false) && (role == 'bare' || ((role.split ' ').include? 'bare'))
2370
+ # # QUESTION should we insert breakable chars into URI when building fragment instead?
2371
+ # %(<a href="#{target}"#{attrs.join}>#{breakable_uri text}</a>)
2372
+ # # NOTE @media may not be initialized if method is called before convert phase
2373
+ # elsif @media != 'screen' || (doc.attr? 'show-link-uri')
2374
+ # # QUESTION should we insert breakable chars into URI when building fragment instead?
2375
+ # # TODO: allow style of printed link to be controlled by theme
2376
+ # %(<a href="#{target}"#{attrs.join}>#{text}</a> [<font size="0.85em">#{breakable_uri bare_target}</font>&#93;)
2377
+ # else
2378
+ # %(<a href="#{target}"#{attrs.join}>#{text}</a>)
2379
+ # end
2380
+ # when :xref
2381
+ # # NOTE non-nil path indicates this is an inter-document xref that's not included in current document
2382
+ # if (path = node.attributes['path'])
2383
+ # # NOTE we don't use local as that doesn't work on the web
2384
+ # # NOTE for the fragment to work in most viewers, it must be #page=<N> <= document this!
2385
+ # %(<a href="#{target}">#{node.text || path}</a>)
2386
+ # elsif (refid = node.attributes['refid'])
2387
+ # unless (text = node.text)
2388
+ # refs = doc.catalog[:refs]
2389
+ # if ::Asciidoctor::AbstractNode === (ref = refs[refid])
2390
+ # text = ref.xreftext node.attr 'xrefstyle', nil, true
2391
+ # end
2392
+ # end
2393
+ # %(<a anchor="#{derive_anchor_from_id refid}">#{text || "[#{refid}]"}</a>).gsub ']', '&#93;'
2394
+ # else
2395
+ # %(<a anchor="#{doc.attr 'pdf-anchor'}">#{node.text || '[^top&#93;'}</a>)
2396
+ # end
2397
+ # when :ref
2398
+ # # NOTE destination is created inside callback registered by FormattedTextTransform#build_fragment
2399
+ # %(<a id="#{node.id}">#{DummyText}</a>)
2400
+ # when :bibref
2401
+ # # NOTE destination is created inside callback registered by FormattedTextTransform#build_fragment
2402
+ # # NOTE technically node.text should be node.reftext, but subs have already been applied to text
2403
+ # reftext = (reftext = node.reftext) ? %([#{reftext}]) : %([#{node.id}])
2404
+ # %(<a id="#{node.id}">#{DummyText}</a>#{reftext})
2405
+ # else
2406
+ # logger.warn %(unknown anchor type: #{node.type.inspect}) unless scratch?
2407
+ # end
2408
+ # end
2409
+
2410
+ # def convert_inline_break node
2411
+ # %(#{node.text}<br>)
2412
+ # end
2413
+
2414
+ # def convert_inline_button node
2415
+ # %(<button>#{((load_theme node.document).button_content || '%s').sub '%s', node.text}</button>)
2416
+ # end
2417
+
2418
+ # def convert_inline_callout node
2419
+ # if (conum_font_family = @theme.conum_font_family) != font_name
2420
+ # result = %(<font name="#{conum_font_family}">#{conum_glyph node.text.to_i}</font>)
2421
+ # else
2422
+ # result = conum_glyph node.text.to_i
2423
+ # end
2424
+ # if (conum_font_color = @theme.conum_font_color)
2425
+ # # NOTE CMYK value gets flattened here, but is restored by formatted text parser
2426
+ # result = %(<color rgb="#{conum_font_color}">#{result}</font>)
2427
+ # end
2428
+ # result
2429
+ # end
2430
+
2431
+ # def convert_inline_footnote node
2432
+ # if (index = node.attr 'index') && (fn = node.document.footnotes.find {|candidate| candidate.index == index })
2433
+ # anchor = node.type == :xref ? '' : %(<a id="_footnoteref_#{index}">#{DummyText}</a>)
2434
+ # label = (@rendered_footnotes.include? fn) ? fn.label : (index - @rendered_footnotes.length)
2435
+ # %(#{anchor}<sup>[<a anchor="_footnotedef_#{index}">#{label}</a>]</sup>)
2436
+ # elsif node.type == :xref
2437
+ # # NOTE footnote reference not found
2438
+ # %( <color rgb="FF0000">[#{node.text}]</color>)
2439
+ # end
2440
+ # end
2441
+
2442
+ # def convert_inline_icon node
2443
+ # if node.document.attr? 'icons', 'font'
2444
+ # if (icon_name = node.target).include? '@'
2445
+ # icon_name, icon_set = icon_name.split '@', 2
2446
+ # explicit_icon_set = true
2447
+ # elsif (icon_set = node.attr 'set', nil, false)
2448
+ # explicit_icon_set = true
2449
+ # else
2450
+ # icon_set = node.document.attr 'icon-set', 'fa'
2451
+ # end
2452
+ # if icon_set == 'fa' || !(IconSets.include? icon_set)
2453
+ # icon_set = 'fa'
2454
+ # # legacy name from Font Awesome < 5
2455
+ # if (remapped_icon_name = resolve_legacy_icon_name icon_name)
2456
+ # requested_icon_name = icon_name
2457
+ # icon_set, icon_name = remapped_icon_name.split '-', 2
2458
+ # glyph = (icon_font_data icon_set).unicode icon_name
2459
+ # logger.info { %(#{requested_icon_name} icon found in deprecated fa icon set; using #{icon_name} from #{icon_set} icon set instead) } unless scratch?
2460
+ # # new name in Font Awesome >= 5 (but document is configured to use fa icon set)
2461
+ # else
2462
+ # font_data = nil
2463
+ # if (resolved_icon_set = FontAwesomeIconSets.find {|candidate| (font_data = icon_font_data candidate).unicode icon_name rescue nil })
2464
+ # icon_set = resolved_icon_set
2465
+ # glyph = font_data.unicode icon_name
2466
+ # logger.info { %(#{icon_name} icon not found in deprecated fa icon set; using match found in #{resolved_icon_set} icon set instead) } unless scratch?
2467
+ # end
2468
+ # end
2469
+ # else
2470
+ # glyph = (icon_font_data icon_set).unicode icon_name rescue nil
2471
+ # end
2472
+ # unless glyph || explicit_icon_set || !icon_name.start_with?(*IconSetPrefixes)
2473
+ # icon_set, icon_name = icon_name.split '-', 2
2474
+ # glyph = (icon_font_data icon_set).unicode icon_name rescue nil
2475
+ # end
2476
+ # if glyph
2477
+ # if node.attr? 'size', nil, false
2478
+ # case (size = node.attr 'size')
2479
+ # when 'lg'
2480
+ # size_attr = ' size="1.333em"'
2481
+ # when 'fw'
2482
+ # size_attr = ' width="1em"'
2483
+ # else
2484
+ # size_attr = %( size="#{size.sub 'x', 'em'}")
2485
+ # end
2486
+ # else
2487
+ # size_attr = ''
2488
+ # end
2489
+ # class_attr = node.role? ? %( class="#{node.role}") : ''
2490
+ # # TODO: support rotate and flip attributes
2491
+ # %(<font name="#{icon_set}"#{size_attr}#{class_attr}>#{glyph}</font>)
2492
+ # else
2493
+ # logger.warn %(#{icon_name} is not a valid icon name in the #{icon_set} icon set) unless scratch?
2494
+ # %([#{node.attr 'alt'}])
2495
+ # end
2496
+ # else
2497
+ # %([#{node.attr 'alt'}])
2498
+ # end
2499
+ # end
2500
+
2501
+ # def convert_inline_image node
2502
+ # if node.type == 'icon'
2503
+ # convert_inline_icon node
2504
+ # else
2505
+ # node.extend ::Asciidoctor::Image unless ::Asciidoctor::Image === node
2506
+ # target, image_format = node.target_and_format
2507
+ # if image_format == 'gif' && !(defined? ::GMagick::Image)
2508
+ # logger.warn %(GIF image format not supported. Install the prawn-gmagick gem or convert #{target} to PNG.) unless scratch?
2509
+ # img = %([#{node.attr 'alt'}])
2510
+ # # NOTE an image with a data URI is handled using a temporary file
2511
+ # elsif (image_path = resolve_image_path node, target, image_format, true)
2512
+ # if ::File.readable? image_path
2513
+ # width_attr = (width = resolve_explicit_width node.attributes) ? %( width="#{width}") : ''
2514
+ # fit_attr = (fit = node.attr 'fit', nil, false) ? %( fit="#{fit}") : ''
2515
+ # img = %(<img src="#{image_path}" format="#{image_format}" alt="[#{encode_quotes node.attr 'alt'}]"#{width_attr}#{fit_attr}>)
2516
+ # else
2517
+ # logger.warn %(image to embed not found or not readable: #{image_path}) unless scratch?
2518
+ # img = %([#{node.attr 'alt'}])
2519
+ # end
2520
+ # else
2521
+ # img = %([#{node.attr 'alt'}])
2522
+ # end
2523
+ # (node.attr? 'link', nil, false) ? %(<a href="#{node.attr 'link'}">#{img}</a>) : img
2524
+ # end
2525
+ # end
2526
+
2527
+ # def convert_inline_indexterm node
2528
+ # # NOTE indexterms not supported if text gets substituted before PDF is initialized
2529
+ # if !(defined? @index)
2530
+ # ''
2531
+ # elsif scratch?
2532
+ # node.type == :visible ? node.text : ''
2533
+ # else
2534
+ # # NOTE page number (:page key) is added by InlineDestinationMarker
2535
+ # dest = { anchor: (anchor_name = @index.next_anchor_name) }
2536
+ # anchor = %(<a id="#{anchor_name}" type="indexterm">#{DummyText}</a>)
2537
+ # if node.type == :visible
2538
+ # visible_term = node.text
2539
+ # @index.store_primary_term (sanitize visible_term), dest
2540
+ # %(#{anchor}#{visible_term})
2541
+ # else
2542
+ # @index.store_term((node.attr 'terms').map {|term| sanitize term }, dest)
2543
+ # anchor
2544
+ # end
2545
+ # end
2546
+ # end
2547
+
2548
+ # def convert_inline_kbd node
2549
+ # if (keys = node.attr 'keys').size == 1
2550
+ # %(<key>#{keys[0]}</key>)
2551
+ # else
2552
+ # keys.map {|key| %(<key>#{key}</key>) }.join (load_theme node.document).key_separator || '+'
2553
+ # end
2554
+ # end
2555
+
2556
+ # def convert_inline_menu node
2557
+ # menu = node.attr 'menu'
2558
+ # caret = (load_theme node.document).menu_caret_content || %( \u203a )
2559
+ # if !(submenus = node.attr 'submenus').empty?
2560
+ # %(<strong>#{[menu, *submenus, (node.attr 'menuitem')].join caret}</strong>)
2561
+ # elsif (menuitem = node.attr 'menuitem')
2562
+ # %(<strong>#{menu}#{caret}#{menuitem}</strong>)
2563
+ # else
2564
+ # %(<strong>#{menu}</strong>)
2565
+ # end
2566
+ # end
2567
+
2568
+ # def convert_inline_quoted node
2569
+ # case node.type
2570
+ # when :emphasis
2571
+ # open, close, is_tag = ['<em>', '</em>', true]
2572
+ # when :strong
2573
+ # open, close, is_tag = ['<strong>', '</strong>', true]
2574
+ # when :monospaced, :asciimath, :latexmath
2575
+ # open, close, is_tag = ['<code>', '</code>', true]
2576
+ # when :superscript
2577
+ # open, close, is_tag = ['<sup>', '</sup>', true]
2578
+ # when :subscript
2579
+ # open, close, is_tag = ['<sub>', '</sub>', true]
2580
+ # when :double
2581
+ # open, close, is_tag = ['&#8220;', '&#8221;', false]
2582
+ # when :single
2583
+ # open, close, is_tag = ['&#8216;', '&#8217;', false]
2584
+ # when :mark
2585
+ # open, close, is_tag = ['<mark>', '</mark>', true]
2586
+ # else
2587
+ # open, close, is_tag = [nil, nil, false]
2588
+ # end
2589
+
2590
+ # inner_text = node.text
2591
+
2592
+ # if (role = node.role)
2593
+ # if (text_transform = (load_theme node.document)[%(role_#{role}_text_transform)])
2594
+ # inner_text = transform_text inner_text, text_transform
2595
+ # end
2596
+ # quoted_text = is_tag ? %(#{open.chop} class="#{role}">#{inner_text}#{close}) : %(<span class="#{role}">#{open}#{inner_text}#{close}</span>)
2597
+ # else
2598
+ # quoted_text = %(#{open}#{inner_text}#{close})
2599
+ # end
2600
+
2601
+ # # NOTE destination is created inside callback registered by FormattedTextTransform#build_fragment
2602
+ # node.id ? %(<a id="#{node.id}">#{DummyText}</a>#{quoted_text}) : quoted_text
2603
+ # end
2604
+
2605
+ # def layout_title_page doc
2606
+ # return unless doc.header? && !doc.notitle
2607
+
2608
+ # # NOTE a new page may have already been started at this point, so decide what to do with it
2609
+ # if page.empty?
2610
+ # page.reset_content if (recycle = @ppbook ? recto_page? : true)
2611
+ # elsif @ppbook && page_number > 0 && recto_page?
2612
+ # start_new_page
2613
+ # end
2614
+
2615
+ # side = recycle ? page_side : (page_side page_number + 1)
2616
+ # prev_bg_image = @page_bg_image[side]
2617
+ # prev_bg_color = @page_bg_color
2618
+ # if (bg_image = resolve_background_image doc, @theme, 'title-page-background-image')
2619
+ # @page_bg_image[side] = bg_image[0] && bg_image
2620
+ # end
2621
+ # if (bg_color = resolve_theme_color :title_page_background_color)
2622
+ # @page_bg_color = bg_color
2623
+ # end
2624
+ # recycle ? (init_page self) : start_new_page
2625
+ # @page_bg_image[side] = prev_bg_image if bg_image
2626
+ # @page_bg_color = prev_bg_color if bg_color
2627
+
2628
+ # # IMPORTANT this is the first page created, so we need to set the base font
2629
+ # font @theme.base_font_family, size: @root_font_size
2630
+
2631
+ # # QUESTION allow alignment per element on title page?
2632
+ # title_align = (@theme.title_page_align || @base_align).to_sym
2633
+
2634
+ # # FIXME: disallow .pdf as image type
2635
+ # if @theme.title_page_logo_display != 'none' && (logo_image_path = (doc.attr 'title-logo-image') || (logo_image_from_theme = @theme.title_page_logo_image))
2636
+ # if (logo_image_path.include? ':') && logo_image_path =~ ImageAttributeValueRx
2637
+ # logo_image_attrs = (AttributeList.new $2).parse %w(alt width height)
2638
+ # if logo_image_from_theme
2639
+ # relative_to_imagesdir = false
2640
+ # logo_image_path = sub_attributes_discretely doc, $1
2641
+ # logo_image_path = ThemeLoader.resolve_theme_asset logo_image_path, @themesdir unless doc.is_uri? logo_image_path
2642
+ # else
2643
+ # relative_to_imagesdir = true
2644
+ # logo_image_path = $1
2645
+ # end
2646
+ # else
2647
+ # logo_image_attrs = {}
2648
+ # relative_to_imagesdir = false
2649
+ # if logo_image_from_theme
2650
+ # logo_image_path = sub_attributes_discretely doc, logo_image_path
2651
+ # logo_image_path = ThemeLoader.resolve_theme_asset logo_image_path, @themesdir unless doc.is_uri? logo_image_path
2652
+ # end
2653
+ # end
2654
+ # logo_image_attrs['target'] = logo_image_path
2655
+ # if (logo_align = [(logo_image_attrs.delete 'align'), @theme.title_page_logo_align, title_align.to_s].find {|val| (BlockAlignmentNames.include? val) })
2656
+ # logo_image_attrs['align'] = logo_align
2657
+ # end
2658
+ # if (logo_image_top = logo_image_attrs['top'] || @theme.title_page_logo_top)
2659
+ # initial_y, @y = @y, (resolve_top logo_image_top)
2660
+ # end
2661
+ # # FIXME: add API to Asciidoctor for creating blocks like this (extract from extensions module?)
2662
+ # image_block = ::Asciidoctor::Block.new doc, :image, content_model: :empty, attributes: logo_image_attrs
2663
+ # # NOTE pinned option keeps image on same page
2664
+ # indent (@theme.title_page_logo_margin_left || 0), (@theme.title_page_logo_margin_right || 0) do
2665
+ # convert_image image_block, relative_to_imagesdir: relative_to_imagesdir, pinned: true
2666
+ # end
2667
+ # @y = initial_y if initial_y
2668
+ # end
2669
+
2670
+ # # TODO: prevent content from spilling to next page
2671
+ # theme_font :title_page do
2672
+ # if (title_top = @theme.title_page_title_top)
2673
+ # @y = resolve_top title_top
2674
+ # end
2675
+ # unless @theme.title_page_title_display == 'none'
2676
+ # doctitle = doc.doctitle partition: true
2677
+ # move_down(@theme.title_page_title_margin_top || 0)
2678
+ # indent (@theme.title_page_title_margin_left || 0), (@theme.title_page_title_margin_right || 0) do
2679
+ # theme_font :title_page_title do
2680
+ # layout_prose doctitle.main,
2681
+ # align: title_align,
2682
+ # margin: 0,
2683
+ # line_height: @theme.title_page_title_line_height
2684
+ # end
2685
+ # end
2686
+ # move_down(@theme.title_page_title_margin_bottom || 0)
2687
+ # end
2688
+ # if @theme.title_page_subtitle_display != 'none' && (subtitle = (doctitle || (doc.doctitle partition: true)).subtitle)
2689
+ # move_down(@theme.title_page_subtitle_margin_top || 0)
2690
+ # indent (@theme.title_page_subtitle_margin_left || 0), (@theme.title_page_subtitle_margin_right || 0) do
2691
+ # theme_font :title_page_subtitle do
2692
+ # layout_prose subtitle,
2693
+ # align: title_align,
2694
+ # margin: 0,
2695
+ # line_height: @theme.title_page_subtitle_line_height
2696
+ # end
2697
+ # end
2698
+ # move_down(@theme.title_page_subtitle_margin_bottom || 0)
2699
+ # end
2700
+ # if @theme.title_page_authors_display != 'none' && (doc.attr? 'authors')
2701
+ # move_down(@theme.title_page_authors_margin_top || 0)
2702
+ # indent (@theme.title_page_authors_margin_left || 0), (@theme.title_page_authors_margin_right || 0) do
2703
+ # generic_authors_content = @theme.title_page_authors_content
2704
+ # authors_content = {
2705
+ # name_only: @theme.title_page_authors_content_name_only || generic_authors_content,
2706
+ # with_email: @theme.title_page_authors_content_with_email || generic_authors_content,
2707
+ # with_url: @theme.title_page_authors_content_with_url || generic_authors_content,
2708
+ # }
2709
+ # # TODO: provide an API in core to get authors as an array
2710
+ # authors = (1..(doc.attr 'authorcount', 1).to_i).map {|idx|
2711
+ # promote_author doc, idx do
2712
+ # author_content_key = (url = doc.attr 'url') ? ((url.start_with? 'mailto:') ? :with_email : :with_url) : :name_only
2713
+ # if (author_content = authors_content[author_content_key])
2714
+ # apply_subs_discretely doc, author_content, drop_lines_with_unresolved_attributes: true
2715
+ # else
2716
+ # doc.attr 'author'
2717
+ # end
2718
+ # end
2719
+ # }.join (@theme.title_page_authors_delimiter || ', ')
2720
+ # theme_font :title_page_authors do
2721
+ # layout_prose authors,
2722
+ # align: title_align,
2723
+ # margin: 0,
2724
+ # normalize: true
2725
+ # end
2726
+ # end
2727
+ # move_down(@theme.title_page_authors_margin_bottom || 0)
2728
+ # end
2729
+ # unless @theme.title_page_revision_display == 'none' || (revision_info = [(doc.attr? 'revnumber') ? %(#{doc.attr 'version-label'} #{doc.attr 'revnumber'}) : nil, (doc.attr 'revdate')].compact).empty?
2730
+ # move_down(@theme.title_page_revision_margin_top || 0)
2731
+ # revision_text = revision_info.join (@theme.title_page_revision_delimiter || ', ')
2732
+ # if (revremark = doc.attr 'revremark')
2733
+ # revision_text = %(#{revision_text}: #{revremark})
2734
+ # end
2735
+ # indent (@theme.title_page_revision_margin_left || 0), (@theme.title_page_revision_margin_right || 0) do
2736
+ # theme_font :title_page_revision do
2737
+ # layout_prose revision_text,
2738
+ # align: title_align,
2739
+ # margin: 0,
2740
+ # normalize: false
2741
+ # end
2742
+ # end
2743
+ # move_down(@theme.title_page_revision_margin_bottom || 0)
2744
+ # end
2745
+ # end
2746
+
2747
+ # layout_prose DummyText, margin: 0, line_height: 1, normalize: false if page.empty?
2748
+ # end
2749
+
2750
+ # def layout_cover_page doc, face
2751
+ # bg_image = resolve_background_image doc, @theme, %(#{face}-cover-image), theme_key: %(cover_#{face}_image).to_sym, symbolic_paths: ['', '~']
2752
+ # if bg_image && bg_image[0]
2753
+ # image_path, image_opts = bg_image
2754
+ # if image_path.empty?
2755
+ # go_to_page page_count if face == :back
2756
+ # start_new_page_discretely
2757
+ # # NOTE open graphics state to prevent page from being reused
2758
+ # open_graphics_state if face == :front
2759
+ # return
2760
+ # elsif image_path == '~'
2761
+ # @page_margin_by_side[:cover] = @page_margin_by_side[:recto] if @media == 'prepress'
2762
+ # return
2763
+ # end
2764
+
2765
+ # go_to_page page_count if face == :back
2766
+ # if image_opts[:format] == 'pdf'
2767
+ # import_page image_path, (image_opts.merge advance: face != :back)
2768
+ # else
2769
+ # image_page image_path, (image_opts.merge canvas: true)
2770
+ # end
2771
+ # end
2772
+ # end
2773
+
2774
+ # def stamp_foreground_image doc, has_front_cover
2775
+ # pages = state.pages
2776
+ # if (first_page = (has_front_cover ? (pages.slice 1, pages.size) : pages).find {|it| !it.imported_page? }) &&
2777
+ # (first_page_num = (pages.index first_page) + 1) &&
2778
+ # (fg_image = resolve_background_image doc, @theme, 'page-foreground-image') && fg_image[0]
2779
+ # go_to_page first_page_num
2780
+ # create_stamp 'foreground-image' do
2781
+ # canvas { image fg_image[0], ({ position: :center, vposition: :center }.merge fg_image[1]) }
2782
+ # end
2783
+ # stamp 'foreground-image'
2784
+ # (first_page_num.next..page_count).each do |num|
2785
+ # go_to_page num
2786
+ # stamp 'foreground-image' unless page.imported_page?
2787
+ # end
2788
+ # end
2789
+ # end
2790
+
2791
+ # def start_new_chapter chapter
2792
+ # start_new_page unless at_page_top?
2793
+ # # TODO: must call update_colors before advancing to next page if start_new_page is called in layout_chapter_title
2794
+ # start_new_page if @ppbook && verso_page? && !(chapter.option? 'nonfacing')
2795
+ # end
2796
+
2797
+ # alias start_new_part start_new_chapter
2798
+
2799
+ # def layout_chapter_title _node, title, opts = {}
2800
+ # layout_heading title, (opts.merge outdent: true)
2801
+ # end
2802
+
2803
+ # alias layout_part_title layout_chapter_title
2804
+
2805
+ # # NOTE layout_heading doesn't set the theme font because it's used for various types of headings
2806
+ # # QUESTION why doesn't layout_heading accept a node?
2807
+ # def layout_heading string, opts = {}
2808
+ # hlevel = opts[:level]
2809
+ # unless (top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top))
2810
+ # if at_page_top?
2811
+ # if hlevel && (top_margin = @theme[%(heading_h#{hlevel}_margin_page_top)] || @theme.heading_margin_page_top || 0) > 0
2812
+ # move_down top_margin
2813
+ # end
2814
+ # top_margin = 0
2815
+ # else
2816
+ # top_margin = (hlevel ? @theme[%(heading_h#{hlevel}_margin_top)] : nil) || @theme.heading_margin_top
2817
+ # end
2818
+ # end
2819
+ # bot_margin = margin || (opts.delete :margin_bottom) || (hlevel ? @theme[%(heading_h#{hlevel}_margin_bottom)] : nil) || @theme.heading_margin_bottom
2820
+ # if (transform = resolve_text_transform opts)
2821
+ # string = transform_text string, transform
2822
+ # end
2823
+ # outdent_section opts.delete :outdent do
2824
+ # margin_top top_margin
2825
+ # # QUESTION should we move inherited styles to typeset_text?
2826
+ # if (inherited = apply_text_decoration font_styles, :heading, hlevel).empty?
2827
+ # inline_format_opts = true
2828
+ # else
2829
+ # inline_format_opts = [{ inherited: inherited }]
2830
+ # end
2831
+ # typeset_text string, calc_line_metrics((opts.delete :line_height) || (hlevel ? @theme[%(heading_h#{hlevel}_line_height)] : nil) || @theme.heading_line_height || @theme.base_line_height), {
2832
+ # color: @font_color,
2833
+ # inline_format: inline_format_opts,
2834
+ # align: @base_align.to_sym,
2835
+ # }.merge(opts)
2836
+ # margin_bottom bot_margin
2837
+ # end
2838
+ # end
2839
+
2840
+ # # NOTE inline_format is true by default
2841
+ # def layout_prose string, opts = {}
2842
+ # top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme.prose_margin_top
2843
+ # bot_margin = margin || (opts.delete :margin_bottom) || @theme.prose_margin_bottom
2844
+ # if (transform = resolve_text_transform opts)
2845
+ # string = transform_text string, transform
2846
+ # end
2847
+ # string = hyphenate_text string, @hyphenator if (opts.delete :hyphenate) && (defined? @hyphenator)
2848
+ # # NOTE used by extensions; ensures linked text gets formatted using the link styles
2849
+ # if (anchor = opts.delete :anchor)
2850
+ # string = %(<a anchor="#{anchor}">#{string}</a>)
2851
+ # end
2852
+ # margin_top top_margin
2853
+ # string = ZeroWidthSpace + string if opts.delete :normalize_line_height
2854
+ # # NOTE normalize makes endlines soft (replaces "\n" with ' ')
2855
+ # inline_format_opts = { normalize: (opts.delete :normalize) != false }
2856
+ # if (styles = opts.delete :styles)
2857
+ # inline_format_opts[:inherited] = { styles: styles }
2858
+ # end
2859
+ # typeset_text string, calc_line_metrics((opts.delete :line_height) || @theme.base_line_height), {
2860
+ # color: @font_color,
2861
+ # inline_format: [inline_format_opts],
2862
+ # align: @base_align.to_sym,
2863
+ # }.merge(opts)
2864
+ # margin_bottom bot_margin
2865
+ # end
2866
+
2867
+ # def generate_manname_section node
2868
+ # title = node.attr 'manname-title', 'Name'
2869
+ # if (next_section = node.sections[0]) && (next_section_title = next_section.title) == next_section_title.upcase
2870
+ # title = title.upcase
2871
+ # end
2872
+ # sect = Section.new node, 1
2873
+ # sect.sectname = 'section'
2874
+ # sect.id = node.attr 'manname-id'
2875
+ # sect.title = title
2876
+ # sect << (Block.new sect, :paragraph, source: %(#{node.attr 'manname'} - #{node.attr 'manpurpose'}), subs: :normal)
2877
+ # sect
2878
+ # end
2879
+
2880
+ # # Render the caption and return the height of the rendered content
2881
+ # #
2882
+ # # The subject argument can either be a String or an AbstractNode. If
2883
+ # # subject is an AbstractNode, only call this method if the node has a
2884
+ # # title (i.e., subject.title? return true).
2885
+ # #--
2886
+ # # TODO: allow margin to be zeroed
2887
+ # def layout_caption subject, opts = {}
2888
+ # if opts.delete :dry_run
2889
+ # height = nil
2890
+ # dry_run do
2891
+ # move_down 0.001 # HACK: force top margin to be applied
2892
+ # height = layout_caption subject, opts
2893
+ # end
2894
+ # return height
2895
+ # end
2896
+ # mark = { cursor: cursor, page_number: page_number }
2897
+ # case subject
2898
+ # when ::String
2899
+ # string = subject
2900
+ # when ::Asciidoctor::AbstractBlock
2901
+ # string = subject.captioned_title
2902
+ # else
2903
+ # raise ArgumentError, 'invalid subject'
2904
+ # end
2905
+ # category_caption = (category = opts[:category]) ? %(#{category}_caption) : 'caption'
2906
+ # container_width = bounds.width
2907
+ # block_align = opts.delete :block_align
2908
+ # if (align = @theme[%(#{category_caption}_align)] || @theme.caption_align)
2909
+ # align = align == 'inherit' ? (block_align || @base_align) : align.to_sym
2910
+ # else
2911
+ # align = @base_align.to_sym
2912
+ # end
2913
+ # if (text_align = @theme[%(#{category_caption}_text_align)] || @theme.caption_text_align)
2914
+ # text_align = text_align == 'inherit' ? align : text_align.to_sym
2915
+ # else
2916
+ # text_align = align
2917
+ # end
2918
+ # indent_by = [0, 0]
2919
+ # block_width = opts.delete :block_width
2920
+ # if (max_width = opts.delete :max_width) && max_width != 'none'
2921
+ # if max_width.start_with? 'fit-content'
2922
+ # if max_width.end_with? 't', '()'
2923
+ # max_width = block_width || container_width
2924
+ # else
2925
+ # max_width = (block_width || container_width) * (max_width.slice 12, max_width.length - 1).to_f / 100.0
2926
+ # end
2927
+ # else
2928
+ # max_width = [max_width.to_f / 100 * bounds.width, bounds.width].min if ::String === max_width && (max_width.end_with? '%')
2929
+ # block_align = align
2930
+ # end
2931
+ # if (remainder = container_width - max_width) > 0
2932
+ # case block_align
2933
+ # when :right
2934
+ # indent_by = [remainder, 0]
2935
+ # when :center
2936
+ # indent_by = [(side_margin = remainder * 0.5), side_margin]
2937
+ # else # :left, nil
2938
+ # indent_by = [0, remainder]
2939
+ # end
2940
+ # end
2941
+ # end
2942
+ # theme_font :caption do
2943
+ # theme_font category_caption do
2944
+ # caption_margin_outside = @theme[%(#{category_caption}_margin_outside)] || @theme.caption_margin_outside
2945
+ # caption_margin_inside = @theme[%(#{category_caption}_margin_inside)] || @theme.caption_margin_inside
2946
+ # if (side = (opts.delete :side) || :top) == :top
2947
+ # margin = { top: caption_margin_outside, bottom: caption_margin_inside }
2948
+ # else
2949
+ # margin = { top: caption_margin_inside, bottom: caption_margin_outside }
2950
+ # end
2951
+ # indent(*indent_by) do
2952
+ # layout_prose string, {
2953
+ # margin_top: margin[:top],
2954
+ # margin_bottom: margin[:bottom],
2955
+ # align: text_align,
2956
+ # normalize: false,
2957
+ # normalize_line_height: true,
2958
+ # hyphenate: true,
2959
+ # }.merge(opts)
2960
+ # end
2961
+ # if side == :top && (bb_color = @theme[%(#{category_caption}_border_bottom_color)] || @theme.caption_border_bottom_color)
2962
+ # stroke_horizontal_rule bb_color
2963
+ # # FIXME: HACK move down slightly so line isn't covered by filled area (half width of line)
2964
+ # move_down 0.25
2965
+ # end
2966
+ # end
2967
+ # end
2968
+ # # NOTE we assume we don't clear more than one page
2969
+ # if page_number > mark[:page_number]
2970
+ # mark[:cursor] + (bounds.top - cursor)
2971
+ # else
2972
+ # mark[:cursor] - cursor
2973
+ # end
2974
+ # end
2975
+
2976
+ # # Render the caption for a table and return the height of the rendered content
2977
+ # def layout_table_caption node, table_alignment = :left, table_width = nil, max_width = nil, side = :top
2978
+ # layout_caption node, category: :table, side: side, block_align: table_alignment, block_width: table_width, max_width: max_width
2979
+ # end
2980
+
2981
+ # def allocate_toc doc, toc_num_levels, toc_start_y, use_title_page
2982
+ # toc_page_nums = page_number
2983
+ # toc_end = nil
2984
+ # dry_run do
2985
+ # toc_page_nums = layout_toc doc, toc_num_levels, toc_page_nums, toc_start_y
2986
+ # move_down @theme.block_margin_bottom unless use_title_page
2987
+ # toc_end = @y
2988
+ # end
2989
+ # # NOTE reserve pages for the toc; leaves cursor on page after last page in toc
2990
+ # if use_title_page
2991
+ # toc_page_nums.each { start_new_page }
2992
+ # else
2993
+ # (toc_page_nums.size - 1).times { start_new_page }
2994
+ # @y = toc_end
2995
+ # end
2996
+ # @toc_extent = { page_nums: toc_page_nums, start_y: toc_start_y }
2997
+ # end
2998
+
2999
+ # # NOTE num_front_matter_pages is not used during a dry run
3000
+ # def layout_toc doc, num_levels = 2, toc_page_number = 2, start_y = nil, num_front_matter_pages = 0
3001
+ # go_to_page toc_page_number unless (page_number == toc_page_number) || scratch?
3002
+ # start_page_number = page_number
3003
+ # @y = start_y if start_y
3004
+ # unless (toc_title = doc.attr 'toc-title').nil_or_empty?
3005
+ # theme_font :heading, level: 2 do
3006
+ # theme_font :toc_title do
3007
+ # toc_title_align = (@theme.toc_title_align || @theme.heading_h2_align || @theme.heading_align || @base_align).to_sym
3008
+ # layout_heading toc_title, align: toc_title_align, level: 2, outdent: true
3009
+ # end
3010
+ # end
3011
+ # end
3012
+ # # QUESTION should we skip this whole method if num_levels < 0?
3013
+ # unless num_levels < 0
3014
+ # dot_leader = theme_font :toc do
3015
+ # # TODO: we could simplify by using nested theme_font :toc_dot_leader
3016
+ # if (dot_leader_font_style = (@theme.toc_dot_leader_font_style || :normal).to_sym) != font_style
3017
+ # font_style dot_leader_font_style
3018
+ # end
3019
+ # {
3020
+ # font_color: @theme.toc_dot_leader_font_color || @font_color,
3021
+ # font_style: dot_leader_font_style,
3022
+ # levels: ((dot_leader_l = @theme.toc_dot_leader_levels) == 'none' ? ::Set.new :
3023
+ # (dot_leader_l && dot_leader_l != 'all' ? dot_leader_l.to_s.split.map(&:to_i).to_set : (0..num_levels).to_set)),
3024
+ # text: (dot_leader_text = @theme.toc_dot_leader_content || DotLeaderTextDefault),
3025
+ # width: dot_leader_text.empty? ? 0 : (rendered_width_of_string dot_leader_text),
3026
+ # # TODO: spacer gives a little bit of room between dots and page number
3027
+ # spacer: { text: NoBreakSpace, size: (spacer_font_size = @font_size * 0.25) },
3028
+ # spacer_width: (rendered_width_of_char NoBreakSpace, size: spacer_font_size),
3029
+ # }
3030
+ # end
3031
+ # line_metrics = calc_line_metrics @theme.toc_line_height
3032
+ # theme_margin :toc, :top
3033
+ # layout_toc_level doc.sections, num_levels, line_metrics, dot_leader, num_front_matter_pages
3034
+ # end
3035
+ # # NOTE range must be calculated relative to toc_page_number; absolute page number in scratch document is arbitrary
3036
+ # toc_page_numbers = (toc_page_number..(toc_page_number + (page_number - start_page_number)))
3037
+ # go_to_page page_count unless scratch?
3038
+ # toc_page_numbers
3039
+ # end
3040
+
3041
+ # def layout_toc_level sections, num_levels, line_metrics, dot_leader, num_front_matter_pages = 0
3042
+ # # NOTE font options aren't always reliable, so store size separately
3043
+ # toc_font_info = theme_font :toc do
3044
+ # { font: font, size: @font_size }
3045
+ # end
3046
+ # hanging_indent = @theme.toc_hanging_indent || 0
3047
+ # sections.each do |sect|
3048
+ # next if (num_levels_for_sect = (sect.attr 'toclevels', num_levels, false).to_i) < sect.level
3049
+ # theme_font :toc, level: (sect.level + 1) do
3050
+ # sect_title = ZeroWidthSpace + (@text_transform ? (transform_text sect.numbered_title, @text_transform) : sect.numbered_title)
3051
+ # pgnum_label_placeholder_width = rendered_width_of_string '0' * @toc_max_pagenum_digits
3052
+ # # NOTE only write section title (excluding dots and page number) if this is a dry run
3053
+ # if scratch?
3054
+ # indent 0, pgnum_label_placeholder_width do
3055
+ # # FIXME: use layout_prose
3056
+ # # NOTE must wrap title in empty anchor element in case links are styled with different font family / size
3057
+ # typeset_text %(<a>#{sect_title}</a>), line_metrics, inline_format: true, hanging_indent: hanging_indent
3058
+ # end
3059
+ # else
3060
+ # physical_pgnum = sect.attr 'pdf-page-start'
3061
+ # virtual_pgnum = physical_pgnum - num_front_matter_pages
3062
+ # pgnum_label = (virtual_pgnum < 1 ? (RomanNumeral.new physical_pgnum, :lower) : virtual_pgnum).to_s
3063
+ # start_page_number = page_number
3064
+ # start_cursor = cursor
3065
+ # start_dots = nil
3066
+ # sect_title_inherited = (apply_text_decoration ::Set.new, :toc, sect.level.next).merge anchor: (sect_anchor = sect.attr 'pdf-anchor'), color: @font_color
3067
+ # # NOTE use text formatter to add anchor overlay to avoid using inline format with synthetic anchor tag
3068
+ # sect_title_fragments = text_formatter.format sect_title, inherited: sect_title_inherited
3069
+ # indent 0, pgnum_label_placeholder_width do
3070
+ # sect_title_fragments[-1][:callback] = (last_fragment_pos = ::Asciidoctor::PDF::FormattedText::FragmentPositionRenderer.new)
3071
+ # typeset_formatted_text sect_title_fragments, line_metrics, hanging_indent: hanging_indent
3072
+ # start_dots = last_fragment_pos.right + hanging_indent
3073
+ # last_fragment_cursor = last_fragment_pos.top + line_metrics.padding_top
3074
+ # # NOTE this will be incorrect if wrapped line is all monospace
3075
+ # if (last_fragment_page_number = last_fragment_pos.page_number) > start_page_number ||
3076
+ # (start_cursor - last_fragment_cursor) > line_metrics.height
3077
+ # start_page_number = last_fragment_page_number
3078
+ # start_cursor = last_fragment_cursor
3079
+ # end
3080
+ # end
3081
+ # end_page_number = page_number
3082
+ # end_cursor = cursor
3083
+ # # TODO: it would be convenient to have a cursor mark / placement utility that took page number into account
3084
+ # go_to_page start_page_number if start_page_number != end_page_number
3085
+ # move_cursor_to start_cursor
3086
+ # if dot_leader[:width] > 0 && (dot_leader[:levels].include? sect.level)
3087
+ # pgnum_label_width = rendered_width_of_string pgnum_label
3088
+ # pgnum_label_font_settings = { color: @font_color, font: font_family, size: @font_size, styles: font_styles }
3089
+ # save_font do
3090
+ # # NOTE the same font is used for dot leaders throughout toc
3091
+ # set_font toc_font_info[:font], toc_font_info[:size]
3092
+ # font_style dot_leader[:font_style]
3093
+ # num_dots = ((bounds.width - start_dots - dot_leader[:spacer_width] - pgnum_label_width) / dot_leader[:width]).floor
3094
+ # # FIXME: dots don't line up in columns if width of page numbers differ
3095
+ # typeset_formatted_text [
3096
+ # { text: (dot_leader[:text] * (num_dots < 0 ? 0 : num_dots)), color: dot_leader[:font_color] },
3097
+ # dot_leader[:spacer],
3098
+ # { text: pgnum_label, anchor: sect_anchor }.merge(pgnum_label_font_settings),
3099
+ # ], line_metrics, align: :right
3100
+ # end
3101
+ # else
3102
+ # typeset_formatted_text [{ text: pgnum_label, color: @font_color, anchor: sect_anchor }], line_metrics, align: :right
3103
+ # end
3104
+ # go_to_page end_page_number if page_number != end_page_number
3105
+ # move_cursor_to end_cursor
3106
+ # end
3107
+ # end
3108
+ # indent @theme.toc_indent do
3109
+ # layout_toc_level sect.sections, num_levels_for_sect, line_metrics, dot_leader, num_front_matter_pages
3110
+ # end if num_levels_for_sect > sect.level
3111
+ # end
3112
+ # end
3113
+
3114
+ # # Reduce icon height to fit inside bounds.height. Icons will not render
3115
+ # # properly if they are larger than the current bounds.height.
3116
+ # def fit_icon_to_bounds preferred_size = 24
3117
+ # (max_height = bounds.height) < preferred_size ? max_height : preferred_size
3118
+ # end
3119
+
3120
+ # def admonition_icon_data key
3121
+ # if (icon_data = @theme[%(admonition_icon_#{key})])
3122
+ # icon_data = (AdmonitionIcons[key] || {}).merge icon_data
3123
+ # if (icon_name = icon_data[:name])
3124
+ # unless icon_name.start_with?(*IconSetPrefixes)
3125
+ # logger.info { %(#{key} admonition in theme uses icon from deprecated fa icon set; use fas, far, or fab instead) } unless scratch?
3126
+ # icon_data[:name] = %(fa-#{icon_name}) unless icon_name.start_with? 'fa-'
3127
+ # end
3128
+ # end
3129
+ # icon_data
3130
+ # else
3131
+ # AdmonitionIcons[key]
3132
+ # end
3133
+ # end
3134
+
3135
+ # # TODO: delegate to layout_page_header and layout_page_footer per page
3136
+ # def layout_running_content periphery, doc, opts = {}
3137
+ # skip, skip_pagenums = opts[:skip] || [1, 1]
3138
+ # body_start_page_number = opts[:body_start_page_number] || 1
3139
+ # # NOTE find and advance to first non-imported content page to use as model page
3140
+ # return unless (content_start_page = state.pages[skip..-1].index {|it| !it.imported_page? })
3141
+ # content_start_page += (skip + 1)
3142
+ # num_pages = page_count
3143
+ # prev_page_number = page_number
3144
+ # go_to_page content_start_page
3145
+
3146
+ # # FIXME: probably need to treat doctypes differently
3147
+ # is_book = doc.doctype == 'book'
3148
+ # header = doc.header? ? doc.header : nil
3149
+ # sectlevels = (@theme[%(#{periphery}_sectlevels)] || 2).to_i
3150
+ # sections = doc.find_by(context: :section) {|sect| sect.level <= sectlevels && sect != header } || []
3151
+ # if (toc_page_nums = @toc_extent && @toc_extent[:page_nums])
3152
+ # toc_title = (doc.attr 'toc-title') || ''
3153
+ # end
3154
+ # disable_on_pages = @disable_running_content[periphery]
3155
+
3156
+ # title_method = TitleStyles[@theme[%(#{periphery}_title_style)]]
3157
+ # # FIXME: we need a proper model for all this page counting
3158
+ # # FIXME: we make a big assumption that part & chapter start on new pages
3159
+ # # index parts, chapters and sections by the physical page number on which they start
3160
+ # part_start_pages = {}
3161
+ # chapter_start_pages = {}
3162
+ # section_start_pages = {}
3163
+ # trailing_section_start_pages = {}
3164
+ # sections.each do |sect|
3165
+ # pgnum = (sect.attr 'pdf-page-start').to_i
3166
+ # if is_book && ((sect_is_part = sect.part?) || sect.chapter?)
3167
+ # if sect_is_part
3168
+ # part_start_pages[pgnum] ||= sect
3169
+ # else
3170
+ # chapter_start_pages[pgnum] ||= sect
3171
+ # # FIXME: need a better way to indicate that part has ended
3172
+ # part_start_pages[pgnum] = '' if sect.sectname == 'appendix' && !part_start_pages.empty?
3173
+ # end
3174
+ # else
3175
+ # trailing_section_start_pages[pgnum] = sect
3176
+ # section_start_pages[pgnum] ||= sect
3177
+ # end
3178
+ # end
3179
+
3180
+ # # index parts, chapters, and sections by the physical page number on which they appear
3181
+ # parts_by_page = SectionInfoByPage.new title_method
3182
+ # chapters_by_page = SectionInfoByPage.new title_method
3183
+ # sections_by_page = SectionInfoByPage.new title_method
3184
+ # # QUESTION should the default part be the doctitle?
3185
+ # last_part = nil
3186
+ # # QUESTION should we enforce that the preamble is a preface?
3187
+ # last_chap = is_book ? :pre : nil
3188
+ # last_sect = nil
3189
+ # sect_search_threshold = 1
3190
+ # (1..num_pages).each do |pgnum|
3191
+ # if (part = part_start_pages[pgnum])
3192
+ # last_part = part
3193
+ # last_chap = nil
3194
+ # last_sect = nil
3195
+ # end
3196
+ # if (chap = chapter_start_pages[pgnum])
3197
+ # last_chap = chap
3198
+ # last_sect = nil
3199
+ # end
3200
+ # if (sect = section_start_pages[pgnum])
3201
+ # last_sect = sect
3202
+ # elsif part || chap
3203
+ # sect_search_threshold = pgnum
3204
+ # # NOTE we didn't find a section on this page; look back to find last section started
3205
+ # elsif last_sect
3206
+ # (sect_search_threshold..(pgnum - 1)).reverse_each do |prev|
3207
+ # if (sect = trailing_section_start_pages[prev])
3208
+ # last_sect = sect
3209
+ # break
3210
+ # end
3211
+ # end
3212
+ # end
3213
+ # parts_by_page[pgnum] = last_part
3214
+ # if toc_page_nums && (toc_page_nums.cover? pgnum)
3215
+ # if is_book
3216
+ # chapters_by_page[pgnum] = toc_title
3217
+ # sections_by_page[pgnum] = nil
3218
+ # else
3219
+ # chapters_by_page[pgnum] = nil
3220
+ # sections_by_page[pgnum] = section_start_pages[pgnum] || toc_title
3221
+ # end
3222
+ # toc_page_nums = nil if toc_page_nums.end == pgnum
3223
+ # elsif last_chap == :pre
3224
+ # chapters_by_page[pgnum] = pgnum < body_start_page_number ? doc.doctitle : (is_book ? (doc.attr 'preface-title', 'Preface') : nil)
3225
+ # sections_by_page[pgnum] = last_sect
3226
+ # else
3227
+ # chapters_by_page[pgnum] = last_chap
3228
+ # sections_by_page[pgnum] = last_sect
3229
+ # end
3230
+ # end
3231
+
3232
+ # doctitle = doc.doctitle partition: true, use_fallback: true
3233
+ # # NOTE set doctitle again so it's properly escaped
3234
+ # doc.set_attr 'doctitle', doctitle.combined
3235
+ # doc.set_attr 'document-title', doctitle.main
3236
+ # doc.set_attr 'document-subtitle', doctitle.subtitle
3237
+ # doc.set_attr 'page-count', (num_pages - skip_pagenums)
3238
+
3239
+ # pagenums_enabled = doc.attr? 'pagenums'
3240
+ # case @media == 'prepress' ? 'physical' : (doc.attr 'pdf-folio-placement')
3241
+ # when 'physical'
3242
+ # folio_basis, invert_folio = :physical, false
3243
+ # when 'physical-inverted'
3244
+ # folio_basis, invert_folio = :physical, true
3245
+ # when 'virtual-inverted'
3246
+ # folio_basis, invert_folio = :virtual, true
3247
+ # else
3248
+ # folio_basis, invert_folio = :virtual, false
3249
+ # end
3250
+ # periphery_layout_cache = {}
3251
+ # # NOTE: this block is invoked during PDF generation, after convert_document has returned
3252
+ # repeat (content_start_page..num_pages), dynamic: true do
3253
+ # pgnum = page_number
3254
+ # # NOTE: don't write on pages which are imported / inserts (otherwise we can get a corrupt PDF)
3255
+ # next if page.imported_page? || (disable_on_pages && (disable_on_pages.include? pgnum))
3256
+ # virtual_pgnum = pgnum - skip_pagenums
3257
+ # pgnum_label = (virtual_pgnum < 1 ? (RomanNumeral.new pgnum, :lower) : virtual_pgnum).to_s
3258
+ # side = page_side((folio_basis == :physical ? pgnum : virtual_pgnum), invert_folio)
3259
+ # doc.set_attr 'page-layout', page.layout.to_s
3260
+
3261
+ # # NOTE: running content is cached per page layout
3262
+ # # QUESTION: should allocation be per side?
3263
+ # trim_styles, colspec_dict, content_dict, stamp_names = allocate_running_content_layout doc, page, periphery, periphery_layout_cache
3264
+ # # FIXME: we need to have a content setting for chapter pages
3265
+ # content_by_position, colspec_by_position = content_dict[side], colspec_dict[side]
3266
+
3267
+ # doc.set_attr 'page-number', pgnum_label if pagenums_enabled
3268
+ # # QUESTION should the fallback value be nil instead of empty string? or should we remove attribute if no value?
3269
+ # doc.set_attr 'part-title', ((part_info = parts_by_page[pgnum])[:title] || '')
3270
+ # if (part_numeral = part_info[:numeral])
3271
+ # doc.set_attr 'part-numeral', part_numeral
3272
+ # else
3273
+ # doc.remove_attr 'part-numeral'
3274
+ # end
3275
+ # doc.set_attr 'chapter-title', ((chap_info = chapters_by_page[pgnum])[:title] || '')
3276
+ # if (chap_numeral = chap_info[:numeral])
3277
+ # doc.set_attr 'chapter-numeral', chap_numeral
3278
+ # else
3279
+ # doc.remove_attr 'chapter-numeral'
3280
+ # end
3281
+ # doc.set_attr 'section-title', ((sect_info = sections_by_page[pgnum])[:title] || '')
3282
+ # doc.set_attr 'section-or-chapter-title', (sect_info[:title] || chap_info[:title] || '')
3283
+
3284
+ # stamp stamp_names[side] if stamp_names
3285
+
3286
+ # theme_font periphery do
3287
+ # canvas do
3288
+ # bounding_box [trim_styles[:content_left][side], trim_styles[:top][side]], width: trim_styles[:content_width][side], height: trim_styles[:height] do
3289
+ # if (trim_column_rule_width = trim_styles[:column_rule_width]) > 0
3290
+ # trim_column_rule_spacing = trim_styles[:column_rule_spacing]
3291
+ # else
3292
+ # trim_column_rule_width = nil
3293
+ # end
3294
+ # prev_position = nil
3295
+ # ColumnPositions.each do |position|
3296
+ # next unless (content = content_by_position[position])
3297
+ # next unless (colspec = colspec_by_position[position])[:width] > 0
3298
+ # left, colwidth = colspec[:x], colspec[:width]
3299
+ # if trim_column_rule_width && colwidth < bounds.width
3300
+ # if (trim_column_rule = prev_position)
3301
+ # left += (trim_column_rule_spacing * 0.5)
3302
+ # colwidth -= trim_column_rule_spacing
3303
+ # else
3304
+ # colwidth -= (trim_column_rule_spacing * 0.5)
3305
+ # end
3306
+ # end
3307
+ # # FIXME: we need to have a content setting for chapter pages
3308
+ # case content
3309
+ # when ::Array
3310
+ # # NOTE float ensures cursor position is restored and returns us to current page if we overrun
3311
+ # float do
3312
+ # # NOTE bounding_box is redundant if both vertical padding and border width are 0
3313
+ # bounding_box [left, bounds.top - trim_styles[:padding][side][0] - trim_styles[:content_offset]], width: colwidth, height: trim_styles[:content_height][side] do
3314
+ # # NOTE image vposition respects padding; use negative image_vertical_align value to revert
3315
+ # image_opts = content[1].merge position: colspec[:align], vposition: trim_styles[:img_valign]
3316
+ # begin
3317
+ # image_info = image content[0], image_opts
3318
+ # if (image_link = content[2])
3319
+ # image_info = { width: image_info.scaled_width, height: image_info.scaled_height } unless image_opts[:format] == 'svg'
3320
+ # add_link_to_image image_link, image_info, image_opts
3321
+ # end
3322
+ # rescue
3323
+ # logger.warn %(could not embed image in running content: #{content[0]}; #{$!.message})
3324
+ # end
3325
+ # end
3326
+ # end
3327
+ # when ::String
3328
+ # theme_font %(#{periphery}_#{side}_#{position}) do
3329
+ # # NOTE minor optimization
3330
+ # if content == '{page-number}'
3331
+ # content = pagenums_enabled ? pgnum_label : nil
3332
+ # else
3333
+ # content = apply_subs_discretely doc, content, drop_lines_with_unresolved_attributes: true
3334
+ # content = transform_text content, @text_transform if @text_transform
3335
+ # end
3336
+ # formatted_text_box parse_text(content, color: @font_color, inline_format: [normalize: true]),
3337
+ # at: [left, bounds.top - trim_styles[:padding][side][0] - trim_styles[:content_offset] + ((Array trim_styles[:valign])[0] == :center ? font.descender * 0.5 : 0)],
3338
+ # width: colwidth,
3339
+ # height: trim_styles[:prose_content_height][side],
3340
+ # align: colspec[:align],
3341
+ # valign: trim_styles[:valign],
3342
+ # leading: trim_styles[:line_metrics].leading,
3343
+ # final_gap: false,
3344
+ # overflow: :truncate
3345
+ # end
3346
+ # end
3347
+ # bounding_box [colspec[:x], bounds.top - trim_styles[:padding][side][0] - trim_styles[:content_offset]], width: colspec[:width], height: trim_styles[:content_height][side] do
3348
+ # stroke_vertical_rule trim_styles[:column_rule_color], at: bounds.left, line_style: trim_styles[:column_rule_style], line_width: trim_column_rule_width
3349
+ # end if trim_column_rule
3350
+ # prev_position = position
3351
+ # end
3352
+ # end
3353
+ # end
3354
+ # end
3355
+ # end
3356
+
3357
+ # go_to_page prev_page_number
3358
+ # nil
3359
+ # end
3360
+
3361
+ # def allocate_running_content_layout doc, page, periphery, cache
3362
+ # cache[layout = page.layout] ||= begin
3363
+ # page_margin_recto = @page_margin_by_side[:recto]
3364
+ # trim_margin_recto = @theme[%(#{periphery}_recto_margin)] || @theme[%(#{periphery}_margin)] || [0, 'inherit', 0, 'inherit']
3365
+ # trim_margin_recto = (expand_margin_value trim_margin_recto).map.with_index {|v, i| i.odd? && v == 'inherit' ? page_margin_recto[i] : v.to_f }
3366
+ # trim_content_margin_recto = @theme[%(#{periphery}_recto_content_margin)] || @theme[%(#{periphery}_content_margin)] || [0, 'inherit', 0, 'inherit']
3367
+ # trim_content_margin_recto = (expand_margin_value trim_content_margin_recto).map.with_index {|v, i| i.odd? && v == 'inherit' ? page_margin_recto[i] - trim_margin_recto[i] : v.to_f }
3368
+ # if (trim_padding_recto = @theme[%(#{periphery}_recto_padding)] || @theme[%(#{periphery}_padding)])
3369
+ # trim_padding_recto = (expand_margin_value trim_padding_recto).map.with_index {|v, i| v + trim_content_margin_recto[i] }
3370
+ # else
3371
+ # trim_padding_recto = trim_content_margin_recto
3372
+ # end
3373
+ # page_margin_verso = @page_margin_by_side[:verso]
3374
+ # trim_margin_verso = @theme[%(#{periphery}_verso_margin)] || @theme[%(#{periphery}_margin)] || [0, 'inherit', 0, 'inherit']
3375
+ # trim_margin_verso = (expand_margin_value trim_margin_verso).map.with_index {|v, i| i.odd? && v == 'inherit' ? page_margin_verso[i] : v.to_f }
3376
+ # trim_content_margin_verso = @theme[%(#{periphery}_verso_content_margin)] || @theme[%(#{periphery}_content_margin)] || [0, 'inherit', 0, 'inherit']
3377
+ # trim_content_margin_verso = (expand_margin_value trim_content_margin_verso).map.with_index {|v, i| i.odd? && v == 'inherit' ? page_margin_verso[i] - trim_margin_verso[i] : v.to_f }
3378
+ # if (trim_padding_verso = @theme[%(#{periphery}_verso_padding)] || @theme[%(#{periphery}_padding)])
3379
+ # trim_padding_verso = (expand_margin_value trim_padding_verso).map.with_index {|v, i| v + trim_content_margin_verso[i] }
3380
+ # else
3381
+ # trim_padding_verso = trim_content_margin_verso
3382
+ # end
3383
+ # valign, valign_offset = @theme[%(#{periphery}_vertical_align)]
3384
+ # if (valign = (valign || :middle).to_sym) == :middle
3385
+ # valign = :center
3386
+ # end
3387
+ # trim_styles = {
3388
+ # line_metrics: (trim_line_metrics = calc_line_metrics @theme[%(#{periphery}_line_height)] || @theme.base_line_height),
3389
+ # # NOTE we've already verified this property is set
3390
+ # height: (trim_height = @theme[%(#{periphery}_height)]),
3391
+ # bg_color: (resolve_theme_color %(#{periphery}_background_color).to_sym),
3392
+ # border_color: (trim_border_color = resolve_theme_color %(#{periphery}_border_color).to_sym),
3393
+ # border_style: (@theme[%(#{periphery}_border_style)] || :solid).to_sym,
3394
+ # border_width: (trim_border_width = trim_border_color ? @theme[%(#{periphery}_border_width)] || @theme.base_border_width || 0 : 0),
3395
+ # column_rule_color: (trim_column_rule_color = resolve_theme_color %(#{periphery}_column_rule_color).to_sym),
3396
+ # column_rule_style: (@theme[%(#{periphery}_column_rule_style)] || :solid).to_sym,
3397
+ # column_rule_width: (trim_column_rule_color ? @theme[%(#{periphery}_column_rule_width)] || 0 : 0),
3398
+ # column_rule_spacing: (@theme[%(#{periphery}_column_rule_spacing)] || 0),
3399
+ # valign: valign_offset ? [valign, valign_offset] : valign,
3400
+ # img_valign: @theme[%(#{periphery}_image_vertical_align)],
3401
+ # top: {
3402
+ # recto: periphery == :header ? page_height - trim_margin_recto[0] : trim_height + trim_margin_recto[2],
3403
+ # verso: periphery == :header ? page_height - trim_margin_verso[0] : trim_height + trim_margin_verso[2],
3404
+ # },
3405
+ # left: {
3406
+ # recto: (trim_left_recto = trim_margin_recto[3]),
3407
+ # verso: (trim_left_verso = trim_margin_verso[3]),
3408
+ # },
3409
+ # width: {
3410
+ # recto: (trim_width_recto = page_width - trim_left_recto - trim_margin_recto[1]),
3411
+ # verso: (trim_width_verso = page_width - trim_left_verso - trim_margin_verso[1]),
3412
+ # },
3413
+ # padding: {
3414
+ # recto: trim_padding_recto,
3415
+ # verso: trim_padding_verso,
3416
+ # },
3417
+ # content_left: {
3418
+ # recto: trim_left_recto + trim_padding_recto[3],
3419
+ # verso: trim_left_verso + trim_padding_verso[3],
3420
+ # },
3421
+ # content_width: (trim_content_width = {
3422
+ # recto: trim_width_recto - trim_padding_recto[1] - trim_padding_recto[3],
3423
+ # verso: trim_width_verso - trim_padding_verso[1] - trim_padding_verso[3],
3424
+ # }),
3425
+ # content_height: (trim_content_height = {
3426
+ # recto: trim_height - trim_padding_recto[0] - trim_padding_recto[2] - (trim_border_width * 0.5),
3427
+ # verso: trim_height - trim_padding_verso[0] - trim_padding_verso[2] - (trim_border_width * 0.5),
3428
+ # }),
3429
+ # prose_content_height: {
3430
+ # recto: trim_content_height[:recto] - trim_line_metrics.padding_top - trim_line_metrics.padding_bottom,
3431
+ # verso: trim_content_height[:verso] - trim_line_metrics.padding_top - trim_line_metrics.padding_bottom,
3432
+ # },
3433
+ # # NOTE content offset adjusts y position to account for border
3434
+ # content_offset: (periphery == :footer ? trim_border_width * 0.5 : 0),
3435
+ # }
3436
+ # case trim_styles[:img_valign]
3437
+ # when nil
3438
+ # trim_styles[:img_valign] = valign
3439
+ # when 'middle'
3440
+ # trim_styles[:img_valign] = :center
3441
+ # when 'top', 'center', 'bottom'
3442
+ # trim_styles[:img_valign] = trim_styles[:img_valign].to_sym
3443
+ # end
3444
+
3445
+ # if (trim_bg_image_recto = resolve_background_image doc, @theme, %(#{periphery}_background_image).to_sym, container_size: [trim_width_recto, trim_height]) && trim_bg_image_recto[0]
3446
+ # trim_bg_image = { recto: trim_bg_image_recto }
3447
+ # if trim_width_recto == trim_width_verso
3448
+ # trim_bg_image[:verso] = trim_bg_image_recto
3449
+ # else
3450
+ # trim_bg_image[:verso] = resolve_background_image doc, @theme, %(#{periphery}_background_image).to_sym, container_size: [trim_width_verso, trim_height]
3451
+ # end
3452
+ # end
3453
+
3454
+ # colspec_dict = PageSides.each_with_object({}) do |side, acc|
3455
+ # side_trim_content_width = trim_content_width[side]
3456
+ # if (custom_colspecs = @theme[%(#{periphery}_#{side}_columns)] || @theme[%(#{periphery}_columns)])
3457
+ # case (colspecs = (custom_colspecs.to_s.tr ',', ' ').split).size
3458
+ # when 0, 1
3459
+ # colspecs = { left: '0', center: colspecs[0] || '100', right: '0' }
3460
+ # when 2
3461
+ # colspecs = { left: colspecs[0], center: '0', right: colspecs[1] }
3462
+ # else # 3
3463
+ # colspecs = { left: colspecs[0], center: colspecs[1], right: colspecs[2] }
3464
+ # end
3465
+ # tot_width = 0
3466
+ # side_colspecs = colspecs.map {|col, spec|
3467
+ # if (alignment_char = spec.chr).to_i.to_s != alignment_char
3468
+ # alignment = AlignmentTable[alignment_char] || :left
3469
+ # rel_width = (spec.slice 1, spec.length).to_f
3470
+ # else
3471
+ # alignment = :left
3472
+ # rel_width = spec.to_f
3473
+ # end
3474
+ # tot_width += rel_width
3475
+ # [col, { align: alignment, width: rel_width, x: 0 }]
3476
+ # }.to_h
3477
+ # # QUESTION should we allow the columns to overlap (capping width at 100%)?
3478
+ # side_colspecs.each {|_, colspec| colspec[:width] = (colspec[:width] / tot_width) * side_trim_content_width }
3479
+ # side_colspecs[:right][:x] = (side_colspecs[:center][:x] = side_colspecs[:left][:width]) + side_colspecs[:center][:width]
3480
+ # acc[side] = side_colspecs
3481
+ # else
3482
+ # acc[side] = {
3483
+ # left: { align: :left, width: side_trim_content_width, x: 0 },
3484
+ # center: { align: :center, width: side_trim_content_width, x: 0 },
3485
+ # right: { align: :right, width: side_trim_content_width, x: 0 },
3486
+ # }
3487
+ # end
3488
+ # end
3489
+
3490
+ # content_dict = PageSides.each_with_object({}) do |side, acc|
3491
+ # side_content = {}
3492
+ # ColumnPositions.each do |position|
3493
+ # unless (val = @theme[%(#{periphery}_#{side}_#{position}_content)]).nil_or_empty?
3494
+ # if (val.include? ':') && val =~ ImageAttributeValueRx
3495
+ # attrlist = $2
3496
+ # image_attrs = (AttributeList.new attrlist).parse %w(alt width)
3497
+ # image_path, image_format = ::Asciidoctor::Image.target_and_format $1, image_attrs
3498
+ # if (image_path = resolve_image_path doc, image_path, image_format, @themesdir) && (::File.readable? image_path)
3499
+ # image_opts = resolve_image_options image_path, image_format, image_attrs, container_size: [colspec_dict[side][position][:width], trim_content_height[side]]
3500
+ # side_content[position] = [image_path, image_opts, image_attrs['link']]
3501
+ # else
3502
+ # # NOTE allows inline image handler to report invalid reference and replace with alt text
3503
+ # side_content[position] = %(image:#{image_path}[#{attrlist}])
3504
+ # end
3505
+ # else
3506
+ # side_content[position] = val
3507
+ # end
3508
+ # end
3509
+ # end
3510
+
3511
+ # acc[side] = side_content
3512
+ # end
3513
+
3514
+ # if (trim_bg_color = trim_styles[:bg_color]) || trim_bg_image || trim_border_width > 0
3515
+ # stamp_names = { recto: %(#{layout}_#{periphery}_recto), verso: %(#{layout}_#{periphery}_verso) }
3516
+ # PageSides.each do |side|
3517
+ # create_stamp stamp_names[side] do
3518
+ # canvas do
3519
+ # bounding_box [trim_styles[:left][side], trim_styles[:top][side]], width: trim_styles[:width][side], height: trim_height do
3520
+ # fill_bounds trim_bg_color if trim_bg_color
3521
+ # # NOTE: must draw line before image or SVG will cause border to disappear
3522
+ # stroke_horizontal_rule trim_styles[:border_color], line_width: trim_border_width, line_style: trim_styles[:border_style], at: (periphery == :header ? bounds.height : 0) if trim_border_width > 0
3523
+ # image trim_bg_image[side][0], ({ position: :center, vposition: :center }.merge trim_bg_image[side][1]) if trim_bg_image
3524
+ # end
3525
+ # end
3526
+ # end
3527
+ # end
3528
+ # end
3529
+
3530
+ # [trim_styles, colspec_dict, content_dict, stamp_names]
3531
+ # end
3532
+ # end
3533
+
3534
+ # def add_outline doc, num_levels = 2, toc_page_nums = [], num_front_matter_pages = 0, has_front_cover = false
3535
+ # if ::String === num_levels
3536
+ # if num_levels.include? ':'
3537
+ # num_levels, expand_levels = num_levels.split ':', 2
3538
+ # num_levels = num_levels.empty? ? (doc.attr 'toclevels', 2).to_i : num_levels.to_i
3539
+ # expand_levels = expand_levels.to_i
3540
+ # else
3541
+ # num_levels = expand_levels = num_levels.to_i
3542
+ # end
3543
+ # else
3544
+ # expand_levels = num_levels
3545
+ # end
3546
+ # front_matter_counter = RomanNumeral.new 0, :lower
3547
+ # pagenum_labels = {}
3548
+
3549
+ # num_front_matter_pages.times do |n|
3550
+ # pagenum_labels[n] = { P: (::PDF::Core::LiteralString.new front_matter_counter.next!.to_s) }
3551
+ # end
3552
+
3553
+ # # add labels for each content page, which is required for reader's page navigator to work correctly
3554
+ # (num_front_matter_pages..(page_count - 1)).each_with_index do |n, i|
3555
+ # pagenum_labels[n] = { P: (::PDF::Core::LiteralString.new (i + 1).to_s) }
3556
+ # end
3557
+
3558
+ # unless toc_page_nums.none? || (toc_title = doc.attr 'toc-title').nil_or_empty?
3559
+ # toc_section = insert_toc_section doc, toc_title, toc_page_nums
3560
+ # end
3561
+
3562
+ # outline.define do
3563
+ # initial_pagenum = has_front_cover ? 2 : 1
3564
+ # # FIXME: use sanitize: :plain_text once available
3565
+ # if document.page_count >= initial_pagenum && (doctitle = doc.header? ? doc.doctitle : (doc.attr 'untitled-label'))
3566
+ # page title: (document.sanitize doctitle), destination: (document.dest_top has_front_cover ? 2 : 1)
3567
+ # end
3568
+ # # QUESTION is there any way to get add_outline_level to invoke in the context of the outline?
3569
+ # document.add_outline_level self, doc.sections, num_levels, expand_levels
3570
+ # end if doc.attr? 'outline'
3571
+
3572
+ # toc_section.parent.blocks.delete toc_section if toc_section
3573
+
3574
+ # catalog.data[:PageLabels] = state.store.ref Nums: pagenum_labels.flatten
3575
+ # primary_page_mode, secondary_page_mode = PageModes[(doc.attr 'pdf-page-mode') || @theme.page_mode]
3576
+ # catalog.data[:PageMode] = primary_page_mode
3577
+ # catalog.data[:NonFullScreenPageMode] = secondary_page_mode if secondary_page_mode
3578
+ # nil
3579
+ # end
3580
+
3581
+ # def add_outline_level outline, sections, num_levels, expand_levels
3582
+ # sections.each do |sect|
3583
+ # sect_title = sanitize sect.numbered_title formal: true
3584
+ # sect_destination = sect.attr 'pdf-destination'
3585
+ # if (level = sect.level) == num_levels || !sect.sections?
3586
+ # outline.page title: sect_title, destination: sect_destination
3587
+ # elsif level <= num_levels
3588
+ # outline.section sect_title, destination: sect_destination, closed: expand_levels < 1 do
3589
+ # add_outline_level outline, sect.sections, num_levels, (expand_levels - 1)
3590
+ # end
3591
+ # end
3592
+ # end
3593
+ # end
3594
+
3595
+ # def insert_toc_section doc, toc_title, toc_page_nums
3596
+ # if (doc.attr? 'toc-placement', 'macro') && (toc_node = (doc.find_by context: :toc)[0])
3597
+ # if (parent_section = toc_node.parent).context == :section
3598
+ # grandparent_section = parent_section.parent
3599
+ # toc_level = parent_section.level
3600
+ # insert_idx = (grandparent_section.blocks.index parent_section) + 1
3601
+ # else
3602
+ # grandparent_section = doc
3603
+ # toc_level = doc.sections[0].level
3604
+ # insert_idx = 0
3605
+ # end
3606
+ # toc_dest = toc_node.attr 'pdf-destination'
3607
+ # else
3608
+ # grandparent_section = doc
3609
+ # toc_level = doc.sections[0].level
3610
+ # insert_idx = 0
3611
+ # toc_dest = dest_top toc_page_nums.first
3612
+ # end
3613
+ # toc_section = Section.new grandparent_section, toc_level, false, attributes: { 'pdf-destination' => toc_dest }
3614
+ # toc_section.title = toc_title
3615
+ # grandparent_section.blocks.insert insert_idx, toc_section
3616
+ # toc_section
3617
+ # end
3618
+
3619
+ # def write pdf_doc, target
3620
+ # if target.respond_to? :write
3621
+ # target = ::QuantifiableStdout.new STDOUT if target == STDOUT
3622
+ # pdf_doc.render target
3623
+ # else
3624
+ # pdf_doc.render_file target
3625
+ # # QUESTION restore attributes first?
3626
+ # @pdfmark&.generate_file target
3627
+ # (Optimizer.new @optimize, pdf_doc.min_version).generate_file target if @optimize && ((defined? ::Asciidoctor::PDF::Optimizer) || !(Helpers.require_library OptimizerRequirePath, 'rghost', :warn).nil?)
3628
+ # to_file = true
3629
+ # end
3630
+ # if !ENV['KEEP_ARTIFACTS']
3631
+ # remove_tmp_files
3632
+ # elsif to_file
3633
+ # scratch_target = (target.slice 0, target.length - (target_ext = ::File.extname target).length) + '-scratch' + target_ext
3634
+ # get_scratch_document.render_file scratch_target
3635
+ # end
3636
+ # clear_scratch
3637
+ # nil
3638
+ # end
3639
+
3640
+ # def register_fonts font_catalog, fonts_dir
3641
+ # return unless font_catalog
3642
+ # dirs = (fonts_dir.split ValueSeparatorRx, -1).map do |dir|
3643
+ # dir == 'GEM_FONTS_DIR' || dir.empty? ? ThemeLoader::FontsDir : dir
3644
+ # end
3645
+ # font_catalog.each do |key, styles|
3646
+ # styles = styles.each_with_object({}) do |(style, path), accum|
3647
+ # found = dirs.find do |dir|
3648
+ # resolved_font_path = font_path path, dir
3649
+ # if ::File.readable? resolved_font_path
3650
+ # accum[style.to_sym] = resolved_font_path
3651
+ # true
3652
+ # end
3653
+ # end
3654
+ # raise ::Errno::ENOENT, ((File.absolute_path? path) ? %(#{path} not found) : %(#{path} not found in #{fonts_dir.gsub ValueSeparatorRx, ' or '})) unless found
3655
+ # end
3656
+ # register_font key => styles
3657
+ # end
3658
+ # end
3659
+
3660
+ # def font_path font_file, fonts_dir
3661
+ # # resolve relative to built-in font dir unless path is absolute
3662
+ # ::File.absolute_path font_file, fonts_dir
3663
+ # end
3664
+
3665
+ # def fallback_svg_font_name
3666
+ # @theme.svg_fallback_font_family || @theme.svg_font_family || @theme.base_font_family
3667
+ # end
3668
+
3669
+ # def apply_text_decoration styles, category, level = nil
3670
+ # if (text_decoration_style = TextDecorationStyleTable[(level && @theme[%(#{category}_h#{level}_text_decoration)]) || @theme[%(#{category}_text_decoration)]])
3671
+ # {
3672
+ # styles: (styles << text_decoration_style),
3673
+ # text_decoration_color: (level && @theme[%(#{category}_h#{level}_text_decoration_color)]) || @theme[%(#{category}_text_decoration_color)],
3674
+ # text_decoration_width: (level && @theme[%(#{category}_h#{level}_text_decoration_width)]) || @theme[%(#{category}_text_decoration_width)],
3675
+ # }.compact
3676
+ # else
3677
+ # styles.empty? ? {} : { styles: styles }
3678
+ # end
3679
+ # end
3680
+
3681
+ # def resolve_text_transform key, use_fallback = true
3682
+ # if (transform = ::Hash === key ? (key.delete :text_transform) : @theme[key.to_s])
3683
+ # transform == 'none' ? nil : transform
3684
+ # elsif use_fallback
3685
+ # @text_transform
3686
+ # end
3687
+ # end
3688
+
3689
+ # # QUESTION should we pass a category as an argument?
3690
+ # # QUESTION should we make this a method on the theme ostruct? (e.g., @theme.resolve_color key, fallback)
3691
+ # def resolve_theme_color key, fallback_color = nil
3692
+ # if (color = @theme[key.to_s]) && color != 'transparent'
3693
+ # color
3694
+ # else
3695
+ # fallback_color
3696
+ # end
3697
+ # end
3698
+
3699
+ # def resolve_font_kerning keyword, fallback = default_kerning?
3700
+ # keyword && (FontKerningTable.key? keyword) ? FontKerningTable[keyword] : fallback
3701
+ # end
3702
+
3703
+ # def theme_fill_and_stroke_bounds category, opts = {}
3704
+ # bg_color = (opts.key? :background_color) ? opts[:background_color] : @theme[%(#{category}_background_color)]
3705
+ # fill_and_stroke_bounds bg_color, @theme[%(#{category}_border_color)],
3706
+ # line_width: (@theme[%(#{category}_border_width)] || 0),
3707
+ # line_style: (@theme[%(#{category}_border_style)] || :solid).to_sym,
3708
+ # radius: @theme[%(#{category}_border_radius)]
3709
+ # end
3710
+
3711
+ # def theme_fill_and_stroke_block category, block_height, opts = {}
3712
+ # if (b_width = (opts.key? :border_width) ? opts[:border_width] : @theme[%(#{category}_border_width)])
3713
+ # b_width = nil unless b_width > 0
3714
+ # end
3715
+ # if (bg_color = opts[:background_color] || @theme[%(#{category}_background_color)]) == 'transparent'
3716
+ # bg_color = nil
3717
+ # end
3718
+ # unless b_width || bg_color
3719
+ # (node = opts[:caption_node]) && node.title? && (layout_caption node, category: category)
3720
+ # return
3721
+ # end
3722
+ # if (b_color = @theme[%(#{category}_border_color)]) == 'transparent'
3723
+ # b_color = @page_bg_color
3724
+ # end
3725
+ # b_radius = (@theme[%(#{category}_border_radius)] || 0) + (b_width || 0)
3726
+ # if b_width && b_color
3727
+ # if b_color == @page_bg_color # let page background cut into block background
3728
+ # b_gap_color, b_shift = @page_bg_color, (b_width * 0.5)
3729
+ # elsif (b_gap_color = bg_color) && b_gap_color != b_color
3730
+ # b_shift = 0
3731
+ # else # let page background cut into border
3732
+ # b_gap_color, b_shift = @page_bg_color, 0
3733
+ # end
3734
+ # else # let page background cut into block background
3735
+ # b_shift, b_gap_color = (b_width ||= 0.5) * 0.5, @page_bg_color
3736
+ # end
3737
+ # # FIXME: due to the calculation error logged in #789, we must advance page even when content is split across pages
3738
+ # advance_page if (opts.fetch :split_from_top, true) && block_height > cursor && !at_page_top?
3739
+ # caption_height = (node = opts[:caption_node]) && node.title? ? (layout_caption node, category: category) : 0
3740
+ # float do
3741
+ # remaining_height = block_height - caption_height
3742
+ # initial_page = true
3743
+ # while remaining_height > 0
3744
+ # advance_page unless initial_page
3745
+ # chunk_height = [(available_height = cursor), remaining_height].min
3746
+ # bounding_box [0, available_height], width: bounds.width, height: chunk_height do
3747
+ # theme_fill_and_stroke_bounds category, background_color: bg_color
3748
+ # # NOTE b_width is always set; if no border is set, split indicator is cut into background
3749
+ # indent b_radius, b_radius do
3750
+ # # dashed line indicates continuation from previous page; swell line slightly to cover background
3751
+ # stroke_horizontal_rule b_gap_color, line_width: b_width * 1.2, line_style: :dashed, at: b_shift
3752
+ # end unless initial_page
3753
+ # if remaining_height > chunk_height
3754
+ # move_down chunk_height - b_shift
3755
+ # indent b_radius, b_radius do
3756
+ # # dashed line indicates continuation from previous page; swell line slightly to cover background
3757
+ # stroke_horizontal_rule b_gap_color, line_width: b_width * 1.2, line_style: :dashed
3758
+ # end
3759
+ # end
3760
+ # end
3761
+ # initial_page = false
3762
+ # remaining_height -= chunk_height
3763
+ # end
3764
+ # end
3765
+ # end
3766
+
3767
+ # # Insert a top margin equal to amount if cursor is not at the top of the
3768
+ # # page. Start a new page instead if amount is greater than the remaining
3769
+ # # space on the page.
3770
+ # def margin_top amount
3771
+ # margin amount, :top
3772
+ # end
3773
+
3774
+ # # Insert a bottom margin equal to amount unless cursor is at the top of the
3775
+ # # page (not likely). Start a new page instead if amount is greater than the
3776
+ # # remaining space on the page.
3777
+ # def margin_bottom amount
3778
+ # margin amount, :bottom
3779
+ # end
3780
+
3781
+ # # Insert a margin at the specified side if the cursor is not at the top of
3782
+ # # the page. Start a new page if amount is greater than the remaining space on
3783
+ # # the page.
3784
+ # def margin amount, _side
3785
+ # unless (amount || 0) == 0 || at_page_top?
3786
+ # # NOTE use low-level cursor calculation to workaround cursor bug in column_box context
3787
+ # if y - reference_bounds.absolute_bottom > amount
3788
+ # move_down amount
3789
+ # else
3790
+ # # set cursor at top of next page
3791
+ # reference_bounds.move_past_bottom
3792
+ # end
3793
+ # end
3794
+ # end
3795
+
3796
+ # # Lookup margin for theme element and side, then delegate to margin method.
3797
+ # # If margin value is not found, assume:
3798
+ # # - 0 when side == :top
3799
+ # # - @theme.vertical_spacing when side == :bottom
3800
+ # def theme_margin category, side
3801
+ # margin((@theme[%(#{category}_margin_#{side})] || (side == :bottom ? @theme.vertical_spacing : 0)), side)
3802
+ # end
3803
+
3804
+ # def theme_font category, opts = {}
3805
+ # result = nil
3806
+ # # TODO: inheriting from generic category should be an option
3807
+ # if opts.key? :level
3808
+ # hlevel_category = %(#{category}_h#{opts[:level]})
3809
+ # family = @theme[%(#{hlevel_category}_font_family)] || @theme[%(#{category}_font_family)] || @theme.base_font_family || font_family
3810
+ # size = @theme[%(#{hlevel_category}_font_size)] || @theme[%(#{category}_font_size)] || @root_font_size
3811
+ # style = @theme[%(#{hlevel_category}_font_style)] || @theme[%(#{category}_font_style)]
3812
+ # color = @theme[%(#{hlevel_category}_font_color)] || @theme[%(#{category}_font_color)]
3813
+ # kerning = resolve_font_kerning @theme[%(#{hlevel_category}_font_kerning)] || @theme[%(#{category}_font_kerning)], nil
3814
+ # # NOTE global text_transform is not currently supported
3815
+ # transform = @theme[%(#{hlevel_category}_text_transform)] || @theme[%(#{category}_text_transform)]
3816
+ # else
3817
+ # inherited_font = font_info
3818
+ # family = @theme[%(#{category}_font_family)] || inherited_font[:family]
3819
+ # size = @theme[%(#{category}_font_size)] || inherited_font[:size]
3820
+ # style = @theme[%(#{category}_font_style)] || inherited_font[:style]
3821
+ # color = @theme[%(#{category}_font_color)]
3822
+ # kerning = resolve_font_kerning @theme[%(#{category}_font_kerning)], nil
3823
+ # # NOTE global text_transform is not currently supported
3824
+ # transform = @theme[%(#{category}_text_transform)]
3825
+ # end
3826
+
3827
+ # prev_color, @font_color = @font_color, color if color
3828
+ # prev_kerning, self.default_kerning = default_kerning?, kerning unless kerning.nil?
3829
+ # prev_transform, @text_transform = @text_transform, (transform == 'none' ? nil : transform) if transform
3830
+
3831
+ # font family, size: size, style: (style && style.to_sym) do
3832
+ # result = yield
3833
+ # end
3834
+
3835
+ # @font_color = prev_color if color
3836
+ # default_kerning prev_kerning unless kerning.nil?
3837
+ # @text_transform = prev_transform if transform
3838
+ # result
3839
+ # end
3840
+
3841
+ # # Calculate the font size (down to the minimum font size) that would allow
3842
+ # # all the specified fragments to fit in the available width without wrapping lines.
3843
+ # #
3844
+ # # Return the calculated font size if an adjustment is necessary or nil if no
3845
+ # # font size adjustment is necessary.
3846
+ # def compute_autofit_font_size fragments, category
3847
+ # arranger = arrange_fragments_by_line fragments
3848
+ # # NOTE finalizing the line here generates fragments & calculates their widths using the current font settings
3849
+ # # CAUTION it also removes zero-width spaces
3850
+ # arranger.finalize_line
3851
+ # actual_width = width_of_fragments arranger.fragments
3852
+ # unless ::Array === (padding = @theme[%(#{category}_padding)])
3853
+ # padding = ::Array.new 4, padding
3854
+ # end
3855
+ # available_width = bounds.width - (padding[3] || 0) - (padding[1] || 0)
3856
+ # if actual_width > available_width
3857
+ # adjusted_font_size = ((available_width * font_size).to_f / actual_width).truncate 4
3858
+ # if (min = @theme[%(#{category}_font_size_min)] || @theme.base_font_size_min) && adjusted_font_size < min
3859
+ # min
3860
+ # else
3861
+ # adjusted_font_size
3862
+ # end
3863
+ # end
3864
+ # end
3865
+
3866
+ # # Arrange fragments by line in an arranger and return an unfinalized arranger.
3867
+ # #
3868
+ # # Finalizing the arranger is deferred since it must be done in the context of
3869
+ # # the global font settings you want applied to each fragment.
3870
+ # def arrange_fragments_by_line fragments, _opts = {}
3871
+ # arranger = ::Prawn::Text::Formatted::Arranger.new self
3872
+ # by_line = arranger.consumed = []
3873
+ # fragments.each do |fragment|
3874
+ # if (text = fragment[:text]) == LF
3875
+ # by_line << fragment
3876
+ # elsif text.include? LF
3877
+ # text.scan LineScanRx do |line|
3878
+ # by_line << (line == LF ? { text: LF } : (fragment.merge text: line))
3879
+ # end
3880
+ # else
3881
+ # by_line << fragment
3882
+ # end
3883
+ # end
3884
+ # arranger
3885
+ # end
3886
+
3887
+ # # Calculate the width that is needed to print all the
3888
+ # # fragments without wrapping any lines.
3889
+ # #
3890
+ # # This method assumes endlines are represented as discrete entries in the
3891
+ # # fragments array.
3892
+ # def width_of_fragments fragments
3893
+ # line_widths = [0]
3894
+ # fragments.each do |fragment|
3895
+ # if fragment.text == LF
3896
+ # line_widths << 0
3897
+ # else
3898
+ # line_widths[-1] += fragment.width
3899
+ # end
3900
+ # end
3901
+ # line_widths.max
3902
+ # end
3903
+
3904
+ # # Compute the rendered width of a string, taking fallback fonts into account
3905
+ # def rendered_width_of_string str, opts = {}
3906
+ # opts = opts.merge kerning: default_kerning?
3907
+ # if str.length == 1
3908
+ # rendered_width_of_char str, opts
3909
+ # elsif (chars = str.each_char).all? {|char| font.glyph_present? char }
3910
+ # width_of_string str, opts
3911
+ # else
3912
+ # char_widths = chars.map {|char| rendered_width_of_char char, opts }
3913
+ # char_widths.sum + (char_widths.length * character_spacing)
3914
+ # end
3915
+ # end
3916
+
3917
+ # # Compute the rendered width of a char, taking fallback fonts into account
3918
+ # def rendered_width_of_char char, opts = {}
3919
+ # unless @fallback_fonts.empty? || (font.glyph_present? char)
3920
+ # @fallback_fonts.each do |fallback_font|
3921
+ # font fallback_font do
3922
+ # return width_of_string char, opts if font.glyph_present? char
3923
+ # end
3924
+ # end
3925
+ # end
3926
+ # width_of_string char, opts
3927
+ # end
3928
+
3929
+ # # TODO: document me, esp the first line formatting functionality
3930
+ # def typeset_text string, line_metrics, opts = {}
3931
+ # move_down line_metrics.padding_top
3932
+ # opts = { leading: line_metrics.leading, final_gap: line_metrics.final_gap }.merge opts
3933
+ # string = string.gsub CjkLineBreakRx, ZeroWidthSpace if @cjk_line_breaks
3934
+ # if (hanging_indent = (opts.delete :hanging_indent) || 0) > 0
3935
+ # indent hanging_indent do
3936
+ # text string, (opts.merge indent_paragraphs: -hanging_indent)
3937
+ # end
3938
+ # elsif (first_line_opts = opts.delete :first_line_options)
3939
+ # # TODO: good candidate for Prawn enhancement!
3940
+ # text_with_formatted_first_line string, first_line_opts, opts
3941
+ # else
3942
+ # text string, opts
3943
+ # end
3944
+ # move_down line_metrics.padding_bottom
3945
+ # end
3946
+
3947
+ # # QUESTION combine with typeset_text?
3948
+ # def typeset_formatted_text fragments, line_metrics, opts = {}
3949
+ # move_down line_metrics.padding_top
3950
+ # opts = { leading: line_metrics.leading, final_gap: line_metrics.final_gap }.merge opts
3951
+ # if (hanging_indent = (opts.delete :hanging_indent) || 0) > 0
3952
+ # indent hanging_indent do
3953
+ # formatted_text fragments, (opts.merge indent_paragraphs: -hanging_indent)
3954
+ # end
3955
+ # else
3956
+ # formatted_text fragments, opts
3957
+ # end
3958
+ # move_down line_metrics.padding_bottom
3959
+ # end
3960
+
3961
+ # def height_of_typeset_text string, opts = {}
3962
+ # line_metrics = (calc_line_metrics opts[:line_height] || @theme.base_line_height)
3963
+ # (height_of string, leading: line_metrics.leading, final_gap: line_metrics.final_gap) + line_metrics.padding_top + (opts[:single_line] ? 0 : line_metrics.padding_bottom)
3964
+ # end
3965
+
3966
+ # # NOTE only used when tabsize attribute is not specified
3967
+ # # tabs must always be replaced with spaces in order for the indentation guards to work
3968
+ # def expand_tabs string
3969
+ # if string.nil_or_empty?
3970
+ # ''
3971
+ # elsif string.include? TAB
3972
+ # full_tab_space = ' ' * (tab_size = 4)
3973
+ # (string.split LF, -1).map {|line|
3974
+ # if line.empty?
3975
+ # line
3976
+ # elsif (tab_idx = line.index TAB)
3977
+ # if tab_idx == 0
3978
+ # leading_tabs = 0
3979
+ # line.each_byte do |b|
3980
+ # break unless b == 9
3981
+ # leading_tabs += 1
3982
+ # end
3983
+ # line = %(#{full_tab_space * leading_tabs}#{rest = line.slice leading_tabs, line.length})
3984
+ # next line unless rest.include? TAB
3985
+ # end
3986
+ # # keeps track of how many spaces were added to adjust offset in match data
3987
+ # spaces_added = 0
3988
+ # idx = 0
3989
+ # result = ''
3990
+ # line.each_char do |c|
3991
+ # if c == TAB
3992
+ # # calculate how many spaces this tab represents, then replace tab with spaces
3993
+ # if (offset = idx + spaces_added) % tab_size == 0
3994
+ # spaces_added += (tab_size - 1)
3995
+ # result += full_tab_space
3996
+ # else
3997
+ # unless (spaces = tab_size - offset % tab_size) == 1
3998
+ # spaces_added += (spaces - 1)
3999
+ # end
4000
+ # result += (' ' * spaces)
4001
+ # end
4002
+ # else
4003
+ # result += c
4004
+ # end
4005
+ # idx += 1
4006
+ # end
4007
+ # result
4008
+ # else
4009
+ # line
4010
+ # end
4011
+ # }.join LF
4012
+ # else
4013
+ # string
4014
+ # end
4015
+ # end
4016
+
4017
+ # # Add an indentation guard at the start of indented lines.
4018
+ # # Expand tabs to spaces if tabs are present
4019
+ # def guard_indentation string
4020
+ # unless (string = expand_tabs string).empty?
4021
+ # string[0] = GuardedIndent if string.start_with? ' '
4022
+ # string.gsub! InnerIndent, GuardedInnerIndent if string.include? InnerIndent
4023
+ # end
4024
+ # string
4025
+ # end
4026
+
4027
+ # def guard_indentation_in_fragments fragments
4028
+ # start_of_line = true
4029
+ # fragments.each do |fragment|
4030
+ # next if (text = fragment[:text]).empty?
4031
+ # if start_of_line && (text.start_with? ' ')
4032
+ # fragment[:text] = GuardedIndent + (((text = text.slice 1, text.length).include? InnerIndent) ? (text.gsub InnerIndent, GuardedInnerIndent) : text)
4033
+ # elsif text.include? InnerIndent
4034
+ # fragment[:text] = text.gsub InnerIndent, GuardedInnerIndent
4035
+ # end
4036
+ # start_of_line = text.end_with? LF
4037
+ # end
4038
+ # fragments
4039
+ # end
4040
+
4041
+ # # Derive a PDF-safe, ASCII-only anchor name from the given value.
4042
+ # # Encodes value into hex if it contains characters outside the ASCII range.
4043
+ # # If value is nil, derive an anchor name from the default_value, if given.
4044
+ # def derive_anchor_from_id value, default_value = nil
4045
+ # if value
4046
+ # value.ascii_only? ? value : %(0x#{::PDF::Core.string_to_hex value})
4047
+ # else
4048
+ # %(__anchor-#{default_value})
4049
+ # end
4050
+ # end
4051
+
4052
+ # # If an id is provided or the node passed as the first argument has an id,
4053
+ # # add a named destination to the document equivalent to the node id at the
4054
+ # # current y position. If the node does not have an id and an id is not
4055
+ # # specified, do nothing.
4056
+ # #
4057
+ # # If the node is a section, and the current y position is the top of the
4058
+ # # page, set the y position equal to the page height to improve the navigation
4059
+ # # experience. If the current x position is at or inside the left margin, set
4060
+ # # the x position equal to 0 (left edge of page) to improve the navigation
4061
+ # # experience.
4062
+ # def add_dest_for_block node, id = nil
4063
+ # if !scratch? && (id ||= node.id)
4064
+ # dest_x = bounds.absolute_left.truncate 4
4065
+ # # QUESTION when content is aligned to left margin, should we keep precise x value or just use 0?
4066
+ # dest_x = 0 if dest_x <= page_margin_left
4067
+ # dest_y = at_page_top? && (node.context == :section || node.context == :document) ? page_height : y
4068
+ # # TODO: find a way to store only the ref of the destination; look it up when we need it
4069
+ # node.set_attr 'pdf-destination', (node_dest = (dest_xyz dest_x, dest_y))
4070
+ # add_dest id, node_dest
4071
+ # end
4072
+ # nil
4073
+ # end
4074
+
4075
+ # def resolve_alignment_from_role roles
4076
+ # if (align_role = roles.reverse.find {|r| TextAlignmentRoles.include? r })
4077
+ # (align_role.slice 5, align_role.length).to_sym
4078
+ # end
4079
+ # end
4080
+
4081
+ # # QUESTION is this method still necessary?
4082
+ # def resolve_imagesdir doc
4083
+ # if (imagesdir = doc.attr 'imagesdir').nil_or_empty? || (imagesdir = imagesdir.chomp '/') == '.'
4084
+ # nil
4085
+ # else
4086
+ # imagesdir
4087
+ # end
4088
+ # end
4089
+
4090
+ # # Resolve the system path of the specified image path.
4091
+ # #
4092
+ # # Resolve and normalize the absolute system path of the specified image,
4093
+ # # taking into account the imagesdir attribute. If an image path is not
4094
+ # # specified, the path is read from the target attribute of the specified
4095
+ # # document node.
4096
+ # #
4097
+ # # If the target is a URI and the allow-uri-read attribute is set on the
4098
+ # # document, read the file contents to a temporary file and return the path to
4099
+ # # the temporary file. If the target is a URI and the allow-uri-read attribute
4100
+ # # is not set, or the URI cannot be read, this method returns a nil value.
4101
+ # #
4102
+ # # When a temporary file is used, the file is stored in @tmp_files to be cleaned up after conversion.
4103
+ # def resolve_image_path node, image_path, image_format, relative_to = true
4104
+ # doc = node.document
4105
+ # imagesdir = relative_to == true ? (resolve_imagesdir doc) : relative_to
4106
+ # #image_format ||= ::Asciidoctor::Image.format image_path, (::Asciidoctor::Image === node ? node.attributes : nil)
4107
+ # # NOTE base64 logic currently used for inline images
4108
+ # if ::Base64 === image_path
4109
+ # return @tmp_files[image_path] if @tmp_files.key? image_path
4110
+ # tmp_image = ::Tempfile.create ['image-', image_format && %(.#{image_format})]
4111
+ # tmp_image.binmode unless image_format == 'svg'
4112
+ # begin
4113
+ # tmp_image.write ::Base64.decode64 image_path
4114
+ # tmp_image.close
4115
+ # @tmp_files[image_path] = tmp_image.path
4116
+ # rescue
4117
+ # @tmp_files[image_path] = nil
4118
+ # tmp_image.close
4119
+ # unlink_tmp_file tmp_image.path
4120
+ # nil
4121
+ # end
4122
+ # # handle case when image is a URI
4123
+ # elsif (node.is_uri? image_path) ||
4124
+ # (imagesdir && (node.is_uri? imagesdir) && (image_path = node.normalize_web_path image_path, imagesdir, false))
4125
+ # unless allow_uri_read
4126
+ # logger.warn %(allow-uri-read is not enabled; cannot embed remote image: #{image_path}) unless scratch?
4127
+ # return
4128
+ # end
4129
+ # return @tmp_files[image_path] if @tmp_files.key? image_path
4130
+ # tmp_image = ::Tempfile.create ['image-', image_format && %(.#{image_format})]
4131
+ # tmp_image.binmode if (binary = image_format != 'svg')
4132
+ # begin
4133
+ # load_open_uri.open_uri(image_path, (binary ? 'rb' : 'r')) {|fd| tmp_image.write fd.read }
4134
+ # tmp_image.close
4135
+ # @tmp_files[image_path] = tmp_image.path
4136
+ # rescue
4137
+ # @tmp_files[image_path] = nil
4138
+ # logger.warn %(could not retrieve remote image: #{image_path}; #{$!.message}) unless scratch?
4139
+ # tmp_image.close
4140
+ # unlink_tmp_file tmp_image.path
4141
+ # nil
4142
+ # end
4143
+ # # handle case when image is a local file
4144
+ # else
4145
+ # node.normalize_system_path image_path, imagesdir, nil, target_name: 'image'
4146
+ # end
4147
+ # end
4148
+
4149
+ # # Resolve the path and sizing of the background image either from a document attribute or theme key.
4150
+ # #
4151
+ # # Returns the argument list for the image method if the document attribute or theme key is found. Otherwise,
4152
+ # # nothing. The first argument in the argument list is the image path. If that value is nil, the background
4153
+ # # image is disabled. The second argument is the options hash to specify the dimensions, such as width and fit.
4154
+ # def resolve_background_image doc, theme, key, opts = {}
4155
+ # if ::String === key
4156
+ # theme_key = opts.delete :theme_key
4157
+ # image_path = (doc.attr key) || (from_theme = theme[theme_key || (key.tr '-', '_').to_sym])
4158
+ # else
4159
+ # image_path = from_theme = theme[key]
4160
+ # end
4161
+ # symbolic_paths = opts.delete :symbolic_paths
4162
+ # if image_path
4163
+ # if symbolic_paths && (symbolic_paths.include? image_path)
4164
+ # return [image_path, {}]
4165
+ # elsif image_path == 'none'
4166
+ # return []
4167
+ # elsif (image_path.include? ':') && image_path =~ ImageAttributeValueRx
4168
+ # image_attrs = (AttributeList.new $2).parse %w(alt width)
4169
+ # if from_theme
4170
+ # image_path = sub_attributes_discretely doc, $1
4171
+ # image_relative_to = @themesdir
4172
+ # else
4173
+ # image_path = $1
4174
+ # image_relative_to = true
4175
+ # end
4176
+ # elsif from_theme
4177
+ # image_path = sub_attributes_discretely doc, image_path
4178
+ # image_relative_to = @themesdir
4179
+ # end
4180
+
4181
+ # image_path, image_format = ::Asciidoctor::Image.target_and_format image_path, image_attrs
4182
+ # image_path = resolve_image_path doc, image_path, image_format, image_relative_to
4183
+
4184
+ # return unless image_path
4185
+
4186
+ # unless ::File.readable? image_path
4187
+ # logger.warn %(#{key.to_s.tr '-_', ' '} not found or readable: #{image_path})
4188
+ # return
4189
+ # end
4190
+
4191
+ # if image_format == 'pdf'
4192
+ # [image_path, page: [((image_attrs || {})['page']).to_i, 1].max, format: image_format]
4193
+ # else
4194
+ # [image_path, (resolve_image_options image_path, image_format, image_attrs, (opts.merge background: true))]
4195
+ # end
4196
+ # end
4197
+ # end
4198
+
4199
+ # def resolve_image_options image_path, image_format, image_attrs, opts = {}
4200
+ # if image_format == 'svg'
4201
+ # image_opts = {
4202
+ # enable_file_requests_with_root: (::File.dirname image_path),
4203
+ # enable_web_requests: allow_uri_read ? (method :load_open_uri).to_proc : false,
4204
+ # cache_images: cache_uri,
4205
+ # fallback_font_name: fallback_svg_font_name,
4206
+ # format: 'svg',
4207
+ # }
4208
+ # else
4209
+ # image_opts = {}
4210
+ # end
4211
+ # background = opts[:background]
4212
+ # container_size = opts[:container_size] || (background ? [page_width, page_height] : [bounds.width, bounds.height])
4213
+ # if image_attrs
4214
+ # if background && (image_pos = image_attrs['position']) && (image_pos = resolve_background_position image_pos, nil)
4215
+ # image_opts.update image_pos
4216
+ # end
4217
+ # if (image_fit = image_attrs['fit'] || (background ? 'contain' : nil))
4218
+ # image_fit = 'contain' if image_format == 'svg' && image_fit == 'fill'
4219
+ # container_width, container_height = container_size
4220
+ # case image_fit
4221
+ # when 'none'
4222
+ # if (image_width = resolve_explicit_width image_attrs, bounds_width: container_width)
4223
+ # image_opts[:width] = image_width
4224
+ # end
4225
+ # when 'scale-down'
4226
+ # # NOTE if width and height aren't set in SVG, real width and height are computed after stretching viewbox to fit page
4227
+ # if (image_width = resolve_explicit_width image_attrs, bounds_width: container_width) && image_width > container_width
4228
+ # image_opts[:fit] = container_size
4229
+ # elsif (image_size = intrinsic_image_dimensions image_path, image_format) &&
4230
+ # (image_width ? image_width * (image_size[:height].to_f / image_size[:width]) > container_height : (to_pt image_size[:width], :px) > container_width || (to_pt image_size[:height], :px) > container_height)
4231
+ # image_opts[:fit] = container_size
4232
+ # elsif image_width
4233
+ # image_opts[:width] = image_width
4234
+ # end
4235
+ # when 'cover'
4236
+ # # QUESTION should we take explicit width into account?
4237
+ # if (image_size = intrinsic_image_dimensions image_path, image_format)
4238
+ # if container_width * (image_size[:height].to_f / image_size[:width]) < container_height
4239
+ # image_opts[:height] = container_height
4240
+ # else
4241
+ # image_opts[:width] = container_width
4242
+ # end
4243
+ # end
4244
+ # when 'fill'
4245
+ # image_opts[:width] = container_width
4246
+ # image_opts[:height] = container_height
4247
+ # else # when 'contain'
4248
+ # image_opts[:fit] = container_size
4249
+ # end
4250
+ # elsif (image_width = resolve_explicit_width image_attrs, bounds_width: container_size[0])
4251
+ # image_opts[:width] = image_width
4252
+ # else # default to fit=contain if sizing is not specified
4253
+ # image_opts[:fit] = container_size
4254
+ # end
4255
+ # else
4256
+ # image_opts[:fit] = container_size
4257
+ # end
4258
+ # image_opts
4259
+ # end
4260
+
4261
+ # # Resolves the explicit width, if specified, as a PDF pt value.
4262
+ # #
4263
+ # # Resolves the explicit width, first considering the pdfwidth attribute, then the scaledwidth
4264
+ # # attribute, then the theme default (if enabled by the :use_fallback option), and finally the
4265
+ # # width attribute. If the specified value is in pixels, the value is scaled by 75% to perform
4266
+ # # approximate CSS px to PDF pt conversion. If the value is a percentage, and the
4267
+ # # bounds_width option is given, the percentage of the bounds_width value is returned.
4268
+ # # Otherwise, the percentage width is returned.
4269
+ # #--
4270
+ # # QUESTION should we enforce positive result?
4271
+ # def resolve_explicit_width attrs, opts = {}
4272
+ # bounds_width = opts[:bounds_width]
4273
+ # # QUESTION should we restrict width to bounds_width for pdfwidth?
4274
+ # if attrs.key? 'pdfwidth'
4275
+ # if (width = attrs['pdfwidth']).end_with? '%'
4276
+ # bounds_width ? (width.to_f / 100) * bounds_width : width
4277
+ # elsif opts[:support_vw] && (width.end_with? 'vw')
4278
+ # (width.chomp 'vw').extend ViewportWidth
4279
+ # else
4280
+ # str_to_pt width
4281
+ # end
4282
+ # elsif attrs.key? 'scaledwidth'
4283
+ # # NOTE the parser automatically appends % if value is unitless
4284
+ # if (width = attrs['scaledwidth']).end_with? '%'
4285
+ # bounds_width ? (width.to_f / 100) * bounds_width : width
4286
+ # else
4287
+ # str_to_pt width
4288
+ # end
4289
+ # elsif opts[:use_fallback] && (width = @theme.image_width)
4290
+ # if ::Numeric === width
4291
+ # width
4292
+ # elsif (width = width.to_s).end_with? '%'
4293
+ # bounds_width ? (width.to_f / 100) * bounds_width : bounds_width
4294
+ # elsif opts[:support_vw] && (width.end_with? 'vw')
4295
+ # (width.chomp 'vw').extend ViewportWidth
4296
+ # else
4297
+ # str_to_pt width
4298
+ # end
4299
+ # elsif attrs.key? 'width'
4300
+ # if (width = attrs['width']).end_with? '%'
4301
+ # width = (width.to_f / 100) * bounds_width if bounds_width
4302
+ # else
4303
+ # width = to_pt width.to_f, :px
4304
+ # end
4305
+ # bounds_width && opts[:constrain_to_bounds] ? [bounds_width, width].min : width
4306
+ # end
4307
+ # end
4308
+
4309
+ # def resolve_background_position value, default_value = {}
4310
+ # if value.include? ' '
4311
+ # result = {}
4312
+ # center = nil
4313
+ # (value.split ' ', 2).each do |keyword|
4314
+ # if keyword == 'left' || keyword == 'right'
4315
+ # result[:position] = keyword.to_sym
4316
+ # elsif keyword == 'top' || keyword == 'bottom'
4317
+ # result[:vposition] = keyword.to_sym
4318
+ # elsif keyword == 'center'
4319
+ # center = true
4320
+ # end
4321
+ # end
4322
+ # if center
4323
+ # result[:position] ||= :center
4324
+ # result[:vposition] ||= :center
4325
+ # result
4326
+ # elsif (result.key? :position) && (result.key? :vposition)
4327
+ # result
4328
+ # else
4329
+ # default_value
4330
+ # end
4331
+ # elsif value == 'left' || value == 'right' || value == 'center'
4332
+ # { position: value.to_sym, vposition: :center }
4333
+ # elsif value == 'top' || value == 'bottom'
4334
+ # { position: :center, vposition: value.to_sym }
4335
+ # else
4336
+ # default_value
4337
+ # end
4338
+ # end
4339
+
4340
+ # def resolve_top val
4341
+ # if val.end_with? 'vh'
4342
+ # page_height * (1 - (val.to_f / 100))
4343
+ # elsif val.end_with? '%'
4344
+ # @y - effective_page_height * (val.to_f / 100)
4345
+ # else
4346
+ # @y - (str_to_pt val)
4347
+ # end
4348
+ # end
4349
+
4350
+ # def add_link_to_image uri, image_info, image_opts
4351
+ # image_width = image_info[:width]
4352
+ # image_height = image_info[:height]
4353
+
4354
+ # case image_opts[:position]
4355
+ # when :center
4356
+ # image_x = bounds.left_side + (bounds.width - image_width) * 0.5
4357
+ # when :right
4358
+ # image_x = bounds.right_side - image_width
4359
+ # else # :left or not set
4360
+ # image_x = bounds.left_side
4361
+ # end
4362
+
4363
+ # case image_opts[:vposition]
4364
+ # when :top
4365
+ # image_y = bounds.absolute_top
4366
+ # when :center
4367
+ # image_y = bounds.absolute_top - (bounds.height - image_height) * 0.5
4368
+ # when :bottom
4369
+ # image_y = bounds.absolute_bottom + image_height
4370
+ # else
4371
+ # image_y = y
4372
+ # end unless (image_y = image_opts[:y])
4373
+
4374
+ # link_annotation [image_x, (image_y - image_height), (image_x + image_width), image_y], Border: [0, 0, 0], A: { Type: :Action, S: :URI, URI: uri.as_pdf }
4375
+ # end
4376
+
4377
+ # def load_open_uri
4378
+ # if @cache_uri && !(defined? ::OpenURI::Cache)
4379
+ # if (Helpers.require_library 'open-uri/cached', 'open-uri-cached', :warn).nil?
4380
+ # @cache_uri = false # disable since it failed to load
4381
+ # end
4382
+ # end
4383
+ # ::OpenURI
4384
+ # end
4385
+
4386
+ # def remove_tmp_files
4387
+ # @tmp_files.reject! {|_, path| path ? (unlink_tmp_file path) : true }
4388
+ # end
4389
+
4390
+ # def unlink_tmp_file path
4391
+ # ::File.unlink path if ::File.exist? path
4392
+ # true
4393
+ # rescue
4394
+ # logger.warn %(could not delete temporary file: #{path}; #{$!.message}) unless scratch?
4395
+ # false
4396
+ # end
4397
+
4398
+ # def apply_subs_discretely doc, value, opts = {}
4399
+ # imagesdir = doc.attr 'imagesdir'
4400
+ # doc.set_attr 'imagesdir', @themesdir
4401
+ # # FIXME: get sub_attributes to handle drop-line w/o a warning
4402
+ # doc.set_attr 'attribute-missing', 'skip' unless (attribute_missing = doc.attr 'attribute-missing') == 'skip'
4403
+ # value = value.gsub '\{', '\\\\\\{' if (escaped_attr_ref = value.include? '\{')
4404
+ # value = doc.apply_subs value
4405
+ # value = (value.split LF).delete_if {|line| SimpleAttributeRefRx.match? line }.join LF if opts[:drop_lines_with_unresolved_attributes] && (value.include? '{')
4406
+ # value = value.gsub '\{', '{' if escaped_attr_ref
4407
+ # doc.set_attr 'attribute-missing', attribute_missing unless attribute_missing == 'skip'
4408
+ # if imagesdir
4409
+ # doc.set_attr 'imagesdir', imagesdir
4410
+ # else
4411
+ # doc.remove_attr 'imagesdir'
4412
+ # end
4413
+ # value
4414
+ # end
4415
+
4416
+ # def sub_attributes_discretely doc, value
4417
+ # doc.set_attr 'attribute-missing', 'skip' unless (attribute_missing = doc.attr 'attribute-missing') == 'skip'
4418
+ # value = doc.apply_subs value, [:attributes]
4419
+ # doc.set_attr 'attribute-missing', attribute_missing unless attribute_missing == 'skip'
4420
+ # value
4421
+ # end
4422
+
4423
+ # def promote_author doc, idx = 1
4424
+ # doc.remove_attr 'url' if (original_url = doc.attr 'url')
4425
+ # email = nil
4426
+ # if idx > 1
4427
+ # original_attrs = AuthorAttributeNames.each_with_object({}) do |name, accum|
4428
+ # accum[name] = doc.attr name
4429
+ # if (val = doc.attr %(#{name}_#{idx}))
4430
+ # doc.set_attr name, val
4431
+ # # NOTE email holds url as well
4432
+ # email = val if name == 'email'
4433
+ # else
4434
+ # doc.remove_attr name
4435
+ # end
4436
+ # end
4437
+ # doc.set_attr 'url', ((email.include? '@') ? %(mailto:#{email}) : email) if email
4438
+ # result = yield
4439
+ # original_attrs.each {|name, val| val ? (doc.set_attr name, val) : (doc.remove_attr name) }
4440
+ # else
4441
+ # if (email = doc.attr 'email')
4442
+ # doc.set_attr 'url', ((email.include? '@') ? %(mailto:#{email}) : email)
4443
+ # end
4444
+ # result = yield
4445
+ # end
4446
+ # if original_url
4447
+ # doc.set_attr 'url', original_url
4448
+ # elsif email
4449
+ # doc.remove_attr 'url'
4450
+ # end
4451
+ # result
4452
+ # end
4453
+
4454
+ # # NOTE assume URL is escaped (i.e., contains character references such as &amp;)
4455
+ # def breakable_uri uri
4456
+ # scheme, address = uri.split UriSchemeBoundaryRx, 2
4457
+ # address, scheme = scheme, address unless address
4458
+ # unless address.nil_or_empty?
4459
+ # address = address.gsub UriBreakCharsRx, UriBreakCharRepl
4460
+ # # NOTE require at least two characters after a break
4461
+ # address.slice!(-2) if address[-2] == ZeroWidthSpace
4462
+ # end
4463
+ # %(#{scheme}#{address})
4464
+ # end
4465
+
4466
+ # def consolidate_ranges nums
4467
+ # if nums.size > 1
4468
+ # prev = nil
4469
+ # nums.each_with_object([]) {|num, accum|
4470
+ # if prev && (prev.to_i + 1) == num.to_i
4471
+ # accum[-1][1] = num
4472
+ # else
4473
+ # accum << [num]
4474
+ # end
4475
+ # prev = num
4476
+ # }.map {|range| range.join '-' }
4477
+ # else
4478
+ # nums
4479
+ # end
4480
+ # end
4481
+
4482
+ # def resolve_pagenums val
4483
+ # pgnums = []
4484
+ # ((val.include? ',') ? (val.split ',') : (val.split ';')).each do |entry|
4485
+ # if entry.include? '..'
4486
+ # from, _, to = entry.partition '..'
4487
+ # pgnums += ([from.to_i, 1].max..[to.to_i, 1].max).to_a
4488
+ # else
4489
+ # pgnums << entry.to_i
4490
+ # end
4491
+ # end
4492
+
4493
+ # pgnums
4494
+ # end
4495
+
4496
+ # def get_char code
4497
+ # (code.start_with? '\u') ? ([((code.slice 2, code.length).to_i 16)].pack 'U1') : code
4498
+ # end
4499
+
4500
+ # # QUESTION move to prawn/extensions.rb?
4501
+ # def init_scratch_prototype
4502
+ # @save_state = nil
4503
+ # @scratch_depth = 0
4504
+ # # NOTE don't need background image in scratch document; can cause marshal error anyway
4505
+ # saved_page_bg_image, @page_bg_image = @page_bg_image, { verso: nil, recto: nil }
4506
+ # # IMPORTANT don't set font before using Marshal, it causes serialization to fail
4507
+ # @prototype = ::Marshal.load ::Marshal.dump self
4508
+ # @page_bg_image = saved_page_bg_image
4509
+ # @prototype.state.store.info.data[:Scratch] = @prototype.text_formatter.scratch = true
4510
+ # # NOTE we're now starting a new page each time, so no need to do it here
4511
+ # #@prototype.start_new_page if @prototype.page_number == 0
4512
+ # end
4513
+
4514
+ # def push_scratch doc
4515
+ # if (@scratch_depth += 1) == 1
4516
+ # @save_state = {
4517
+ # catalog: {}.tap {|accum| doc.catalog.each {|k, v| accum[k] = v.dup } },
4518
+ # attributes: doc.attributes.dup,
4519
+ # }
4520
+ # end
4521
+ # end
4522
+
4523
+ # def pop_scratch doc
4524
+ # if (@scratch_depth -= 1) == 0
4525
+ # doc.catalog.replace @save_state[:catalog]
4526
+ # doc.attributes.replace @save_state[:attributes]
4527
+ # @save_state = nil
4528
+ # end
4529
+ # end
4530
+
4531
+ # def clear_scratch
4532
+ # @scratch_depth = 0
4533
+ # @save_state = @prototype = @scratch = nil
4534
+ # end
4535
+ end
4536
+ end
4537
+ end