asciidoctor-pdf 1.5.0.alpha.13 → 1.5.0.alpha.14
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +131 -4
- data/Gemfile +8 -1
- data/README.adoc +199 -36
- data/Rakefile +1 -0
- data/asciidoctor-pdf.gemspec +4 -4
- data/data/themes/base-theme.yml +9 -2
- data/data/themes/default-theme.yml +34 -20
- data/docs/theming-guide.adoc +1147 -268
- data/lib/asciidoctor-pdf/asciidoctor_ext/image.rb +7 -1
- data/lib/asciidoctor-pdf/converter.rb +841 -409
- data/lib/asciidoctor-pdf/core_ext.rb +1 -0
- data/lib/asciidoctor-pdf/core_ext/numeric.rb +12 -8
- data/lib/asciidoctor-pdf/core_ext/object.rb +6 -0
- data/lib/asciidoctor-pdf/core_ext/string.rb +15 -0
- data/lib/asciidoctor-pdf/formatted_text/inline_destination_marker.rb +5 -0
- data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +75 -28
- data/lib/asciidoctor-pdf/formatted_text/inline_image_renderer.rb +3 -2
- data/lib/asciidoctor-pdf/formatted_text/transform.rb +97 -89
- data/lib/asciidoctor-pdf/index_catalog.rb +119 -0
- data/lib/asciidoctor-pdf/measurements.rb +58 -0
- data/lib/asciidoctor-pdf/pdf-core_ext.rb +1 -0
- data/lib/asciidoctor-pdf/{pdf_core_ext → pdf-core_ext}/page.rb +3 -7
- data/lib/asciidoctor-pdf/pdfmark.rb +33 -0
- data/lib/asciidoctor-pdf/prawn-svg_ext.rb +4 -0
- data/lib/asciidoctor-pdf/prawn-svg_ext/interface.rb +10 -0
- data/lib/asciidoctor-pdf/prawn-table_ext.rb +3 -0
- data/lib/asciidoctor-pdf/prawn-table_ext/cell/asciidoc.rb +69 -0
- data/lib/asciidoctor-pdf/prawn-table_ext/cell/text.rb +12 -0
- data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +89 -76
- data/lib/asciidoctor-pdf/prawn_ext/images.rb +0 -1
- data/lib/asciidoctor-pdf/roman_numeral.rb +1 -1
- data/lib/asciidoctor-pdf/rouge_ext/formatters/prawn.rb +51 -11
- data/lib/asciidoctor-pdf/rouge_ext/themes/pastie.rb +64 -59
- data/lib/asciidoctor-pdf/sanitizer.rb +53 -2
- data/lib/asciidoctor-pdf/theme_loader.rb +8 -21
- data/lib/asciidoctor-pdf/version.rb +1 -1
- metadata +19 -13
- data/docs/theme-schema.json +0 -114
- data/lib/asciidoctor-pdf/pdf_core_ext.rb +0 -2
- data/lib/asciidoctor-pdf/pdf_core_ext/pdf_object.rb +0 -25
- data/lib/asciidoctor-pdf/pdfmarks.rb +0 -35
@@ -1,5 +1,7 @@
|
|
1
1
|
module Asciidoctor
|
2
2
|
module Image
|
3
|
+
DataUriRx = /^data:image\/(?<fmt>png|jpe?g|gif|pdf|bmp|tiff);base64,(?<data>.*)$/
|
4
|
+
|
3
5
|
class << self
|
4
6
|
def format path, node = nil
|
5
7
|
(node && (node.attr 'format', nil, false)) || (::File.extname path).downcase[1..-1]
|
@@ -12,7 +14,11 @@ module Image
|
|
12
14
|
|
13
15
|
def target_and_format
|
14
16
|
image_path = inline? ? target : (attr 'target')
|
15
|
-
|
17
|
+
if (image_path.start_with? 'data:') && (m = DataUriRx.match image_path)
|
18
|
+
[(m[:data].extend ::Base64), m[:fmt]]
|
19
|
+
else
|
20
|
+
[image_path, (attr 'format', nil, false) || (::File.extname image_path).downcase[1..-1]]
|
21
|
+
end
|
16
22
|
end
|
17
23
|
end
|
18
24
|
end
|
@@ -1,20 +1,27 @@
|
|
1
1
|
# encoding: UTF-8
|
2
2
|
# TODO cleanup imports...decide what belongs in asciidoctor-pdf.rb
|
3
3
|
require 'prawn'
|
4
|
-
|
5
|
-
require 'prawn/
|
4
|
+
begin
|
5
|
+
require 'prawn/gmagick'
|
6
|
+
rescue LoadError
|
7
|
+
end unless defined? GMagick::Image
|
8
|
+
require_relative 'prawn-svg_ext'
|
9
|
+
require_relative 'prawn-table_ext'
|
6
10
|
require 'prawn/templates'
|
7
11
|
require_relative 'core_ext'
|
8
|
-
require_relative '
|
12
|
+
require_relative 'pdf-core_ext'
|
9
13
|
require_relative 'temporary_path'
|
14
|
+
require_relative 'measurements'
|
10
15
|
require_relative 'sanitizer'
|
11
16
|
require_relative 'prawn_ext'
|
12
17
|
require_relative 'formatted_text'
|
13
|
-
require_relative '
|
18
|
+
require_relative 'pdfmark'
|
14
19
|
require_relative 'asciidoctor_ext'
|
15
20
|
require_relative 'theme_loader'
|
16
21
|
require_relative 'roman_numeral'
|
22
|
+
require_relative 'index_catalog'
|
17
23
|
|
24
|
+
autoload :StringIO, 'stringio'
|
18
25
|
autoload :Tempfile, 'tempfile'
|
19
26
|
|
20
27
|
module Asciidoctor
|
@@ -38,7 +45,8 @@ class Converter < ::Prawn::Document
|
|
38
45
|
tip: { name: 'fa-lightbulb-o', stroke_color: '111111', size: 24 },
|
39
46
|
warning: { name: 'fa-exclamation-triangle', stroke_color: 'BF6900', size: 24 }
|
40
47
|
}
|
41
|
-
|
48
|
+
TextAlignmentNames = ['left', 'center', 'right', 'justify']
|
49
|
+
BlockAlignmentNames = ['left', 'center', 'right']
|
42
50
|
AlignmentTable = { '<' => :left, '=' => :center, '>' => :right }
|
43
51
|
ColumnPositions = [:left, :center, :right]
|
44
52
|
PageSides = [:recto, :verso]
|
@@ -57,8 +65,10 @@ class Converter < ::Prawn::Document
|
|
57
65
|
NarrowNoBreakSpace = %(\u202f)
|
58
66
|
ZeroWidthSpace = %(\u200b)
|
59
67
|
HairSpace = %(\u200a)
|
60
|
-
|
68
|
+
DummyText = %(\u0000)
|
69
|
+
DotLeaderTextDefault = '. '
|
61
70
|
EmDash = %(\u2014)
|
71
|
+
RightPointer = %(\u25ba)
|
62
72
|
LowercaseGreekA = %(\u03b1)
|
63
73
|
Bullets = {
|
64
74
|
disc: %(\u2022),
|
@@ -71,16 +81,20 @@ class Converter < ::Prawn::Document
|
|
71
81
|
unchecked: %(\u2610)
|
72
82
|
}
|
73
83
|
SimpleAttributeRefRx = /(?<!\\)\{\w+(?:[\-]\w+)*\}/
|
74
|
-
MeasurementRxt = '\\d+(?:\\.\\d+)?(?:in|cm|mm|
|
75
|
-
MeasurementPartsRx = /^(\d+(?:\.\d+)?)(in|mm|cm|
|
84
|
+
MeasurementRxt = '\\d+(?:\\.\\d+)?(?:in|cm|mm|p[txc])?'
|
85
|
+
MeasurementPartsRx = /^(\d+(?:\.\d+)?)(in|mm|cm|p[txc])?$/
|
76
86
|
PageSizeRx = /^(?:\[(#{MeasurementRxt}), ?(#{MeasurementRxt})\]|(#{MeasurementRxt})(?: x |x)(#{MeasurementRxt})|\S+)$/
|
77
87
|
# CalloutExtractRx synced from /lib/asciidoctor.rb of Asciidoctor core
|
78
88
|
CalloutExtractRx = /(?:(?:\/\/|#|--|;;) ?)?(\\)?<!?(--|)(\d+)\2> ?(?=(?:\\?<!?\2\d+\2> ?)*$)/
|
79
89
|
ImageAttributeValueRx = /^image:{1,2}(.*?)\[(.*?)\]$/
|
90
|
+
UriBreakCharsRx = /(?:\/|\?|&|#)(?!$)/
|
91
|
+
UriBreakCharRepl = %(\\&#{ZeroWidthSpace})
|
92
|
+
UriSchemeBoundaryRx = /(?<=:\/\/)/
|
80
93
|
LineScanRx = /\n|.+/
|
81
94
|
BlankLineRx = /\n[[:blank:]]*\n/
|
82
95
|
WhitespaceChars = %( \t\n)
|
83
96
|
SourceHighlighters = ['coderay', 'pygments', 'rouge'].to_set
|
97
|
+
PygmentsBgColorRx = /^\.highlight +{ *background: *#([^;]+);/
|
84
98
|
ViewportWidth = ::Module.new
|
85
99
|
|
86
100
|
def initialize backend, opts
|
@@ -134,6 +148,21 @@ class Converter < ::Prawn::Document
|
|
134
148
|
end
|
135
149
|
#assign_missing_section_ids doc
|
136
150
|
|
151
|
+
# promote anonymous preface (defined using preamble block) to preface section
|
152
|
+
# FIXME this should be done in core
|
153
|
+
if doc.doctype == 'book' && (blk_0 = doc.blocks[0]) && blk_0.context == :preamble &&
|
154
|
+
blk_0.title? && blk_0.blocks[0].style != 'abstract' && (blk_1 = doc.blocks[1]) && blk_1.context == :section
|
155
|
+
preface = Section.new doc, blk_1.level, false, attributes: { 1 => 'preface', 'style' => 'preface' }
|
156
|
+
preface.special = true
|
157
|
+
preface.sectname = 'preface'
|
158
|
+
preface.title = doc.attr 'preface-title', 'Preface'
|
159
|
+
# QUESTION should ID be generated from raw or converted title? core is not clear about this
|
160
|
+
preface.id = preface.generate_id
|
161
|
+
preface.blocks.replace blk_0.blocks.map {|b| b.parent = preface; b }
|
162
|
+
doc.blocks[0] = preface
|
163
|
+
blk_0 = blk_1 = preface = nil
|
164
|
+
end
|
165
|
+
|
137
166
|
# NOTE on_page_create is called within a float context
|
138
167
|
# NOTE on_page_create is not called for imported pages, front and back cover pages, and other image pages
|
139
168
|
on_page_create do
|
@@ -170,7 +199,7 @@ class Converter < ::Prawn::Document
|
|
170
199
|
#start_new_page if @ppbook && verso_page?
|
171
200
|
start_new_page if @media == 'prepress' && verso_page?
|
172
201
|
|
173
|
-
num_front_matter_pages = page_number - 1
|
202
|
+
num_front_matter_pages = (@index.start_page_number = page_number) - 1
|
174
203
|
font @theme.base_font_family, size: @theme.base_font_size, style: @theme.base_font_style.to_sym
|
175
204
|
doc.set_attr 'pdf-anchor', (doc_anchor = derive_anchor_from_id doc.id, 'top')
|
176
205
|
add_dest_for_block doc, doc_anchor
|
@@ -233,9 +262,11 @@ class Converter < ::Prawn::Document
|
|
233
262
|
@page_bg_color = resolve_theme_color :page_background_color, 'FFFFFF'
|
234
263
|
@fallback_fonts = [*theme.font_fallbacks]
|
235
264
|
@font_color = theme.base_font_color
|
265
|
+
@base_align = (align = doc.attr 'text-alignment') && (TextAlignmentNames.include? align) ? align : theme.base_align
|
236
266
|
@text_transform = nil
|
237
|
-
|
238
|
-
|
267
|
+
@index = IndexCatalog.new
|
268
|
+
# NOTE we have to init Pdfmark class here while we have reference to the doc
|
269
|
+
@pdfmark = (doc.attr? 'pdfmark') ? (Pdfmark.new doc) : nil
|
239
270
|
init_scratch_prototype
|
240
271
|
self
|
241
272
|
end
|
@@ -246,7 +277,7 @@ class Converter < ::Prawn::Document
|
|
246
277
|
#optimize_objects: true,
|
247
278
|
info: (build_pdf_info doc),
|
248
279
|
margin: theme.page_margin,
|
249
|
-
page_layout: theme.page_layout.to_sym,
|
280
|
+
page_layout: ((doc.attr 'pdf-page-layout') || theme.page_layout).to_sym,
|
250
281
|
skip_page_creation: true,
|
251
282
|
}
|
252
283
|
|
@@ -280,13 +311,8 @@ class Converter < ::Prawn::Document
|
|
280
311
|
# dimension cannot be less than 0
|
281
312
|
dim > 0 ? dim : break
|
282
313
|
elsif ::String === dim && (m = (MeasurementPartsRx.match dim))
|
283
|
-
|
284
|
-
|
285
|
-
# QUESTION should we make rounding a feature of the to_pt method?
|
286
|
-
if (val = val.round 4) == (i_val = val.to_i)
|
287
|
-
val = i_val
|
288
|
-
end
|
289
|
-
val
|
314
|
+
# NOTE truncate to max precision retained by PDF::Core
|
315
|
+
(to_pt m[1].to_f, m[2]).truncate_to_precision 4
|
290
316
|
else
|
291
317
|
break
|
292
318
|
end
|
@@ -303,30 +329,40 @@ class Converter < ::Prawn::Document
|
|
303
329
|
def build_pdf_info doc
|
304
330
|
info = {}
|
305
331
|
# FIXME use sanitize: :plain_text once available
|
306
|
-
info[:Title] =
|
332
|
+
info[:Title] = sanitize(doc.doctitle use_fallback: true).as_pdf
|
307
333
|
if doc.attr? 'authors'
|
308
|
-
info[:Author] =
|
334
|
+
info[:Author] = (doc.attr 'authors').as_pdf
|
309
335
|
end
|
310
336
|
if doc.attr? 'subject'
|
311
|
-
info[:Subject] =
|
337
|
+
info[:Subject] = (doc.attr 'subject').as_pdf
|
312
338
|
end
|
313
339
|
if doc.attr? 'keywords'
|
314
|
-
info[:Keywords] =
|
340
|
+
info[:Keywords] = (doc.attr 'keywords').as_pdf
|
315
341
|
end
|
316
342
|
if (doc.attr? 'publisher')
|
317
|
-
info[:Producer] =
|
343
|
+
info[:Producer] = (doc.attr 'publisher').as_pdf
|
318
344
|
end
|
319
|
-
info[:Creator] =
|
345
|
+
info[:Creator] = %(Asciidoctor PDF #{::Asciidoctor::Pdf::VERSION}, based on Prawn #{::Prawn::VERSION}).as_pdf
|
320
346
|
info[:Producer] ||= (info[:Author] || info[:Creator])
|
321
|
-
|
322
|
-
|
347
|
+
unless doc.attr? 'reproducible'
|
348
|
+
# NOTE since we don't track the creation date of the input file, we map the ModDate header to the last modified
|
349
|
+
# date of the input document and the CreationDate header to the date the PDF was produced by the converter.
|
350
|
+
info[:ModDate] = ::Time.parse(doc.attr 'docdatetime') rescue (now ||= ::Time.now)
|
351
|
+
info[:CreationDate] = ::Time.parse(doc.attr 'localdatetime') rescue (now ||= ::Time.now)
|
352
|
+
end
|
323
353
|
info
|
324
354
|
end
|
325
355
|
|
326
356
|
def convert_section sect, opts = {}
|
357
|
+
if sect.special && sect.sectname == 'abstract'
|
358
|
+
# HACK cheat a bit to hide this section from TOC; TOC should filter these sections
|
359
|
+
sect.context = :open
|
360
|
+
return convert_abstract sect
|
361
|
+
end
|
362
|
+
|
327
363
|
theme_font :heading, level: (hlevel = sect.level + 1) do
|
328
364
|
title = sect.numbered_title formal: true
|
329
|
-
align = (@theme[%(heading_h#{hlevel}_align)] || @theme.heading_align || @
|
365
|
+
align = (@theme[%(heading_h#{hlevel}_align)] || @theme.heading_align || @base_align).to_sym
|
330
366
|
type = nil
|
331
367
|
if sect.part_or_chapter?
|
332
368
|
if sect.chapter?
|
@@ -355,7 +391,7 @@ class Converter < ::Prawn::Document
|
|
355
391
|
end
|
356
392
|
end
|
357
393
|
|
358
|
-
convert_content_for_block sect
|
394
|
+
sect.special && sect.sectname == 'index' ? (convert_index_section sect) : (convert_content_for_block sect)
|
359
395
|
sect.set_attr 'pdf-page-end', page_number
|
360
396
|
end
|
361
397
|
|
@@ -363,13 +399,18 @@ class Converter < ::Prawn::Document
|
|
363
399
|
add_dest_for_block node if node.id
|
364
400
|
# QUESTION should we decouple styles from section titles?
|
365
401
|
theme_font :heading, level: (hlevel = node.level + 1) do
|
366
|
-
layout_heading node.title, align: (@theme[%(heading_h#{hlevel}_align)] || @theme.heading_align || @
|
402
|
+
layout_heading node.title, align: (@theme[%(heading_h#{hlevel}_align)] || @theme.heading_align || @base_align).to_sym
|
367
403
|
end
|
368
404
|
end
|
369
405
|
|
370
406
|
def convert_abstract node
|
371
407
|
add_dest_for_block node if node.id
|
372
408
|
pad_box @theme.abstract_padding do
|
409
|
+
if node.title?
|
410
|
+
theme_font :abstract_title do
|
411
|
+
layout_heading node.title, align: (@theme.abstract_title_align || @base_align).to_sym
|
412
|
+
end
|
413
|
+
end
|
373
414
|
theme_font :abstract do
|
374
415
|
prose_opts = { line_height: @theme.abstract_line_height }
|
375
416
|
# FIXME control more first_line_options using theme
|
@@ -399,11 +440,11 @@ class Converter < ::Prawn::Document
|
|
399
440
|
end
|
400
441
|
|
401
442
|
def convert_preamble node
|
402
|
-
#
|
403
|
-
|
404
|
-
|
405
|
-
convert_content_for_block node
|
443
|
+
# TODO find_by needs to support a depth argument
|
444
|
+
if (first_p = (node.find_by context: :paragraph)[0]) && first_p.parent == node
|
445
|
+
first_p.add_role 'lead'
|
406
446
|
end
|
447
|
+
convert_content_for_block node
|
407
448
|
end
|
408
449
|
|
409
450
|
# TODO add prose around image logic (use role to add special logic for headshot)
|
@@ -441,58 +482,134 @@ class Converter < ::Prawn::Document
|
|
441
482
|
end
|
442
483
|
end
|
443
484
|
|
444
|
-
# FIXME alignment of content is off
|
445
485
|
def convert_admonition node
|
446
486
|
add_dest_for_block node if node.id
|
447
487
|
theme_margin :block, :top
|
448
|
-
|
449
|
-
|
488
|
+
type = node.attr 'name'
|
489
|
+
label_align = (@theme.admonition_label_align || :center).to_sym
|
490
|
+
# TODO allow vertical_align to be a number
|
491
|
+
if (label_valign = (@theme.admonition_label_vertical_align || :middle).to_sym) == :middle
|
492
|
+
label_valign = :center
|
493
|
+
end
|
494
|
+
if (label_min_width = @theme.admonition_label_min_width)
|
495
|
+
label_min_width = label_min_width.to_f
|
496
|
+
end
|
497
|
+
icons = (node.document.attr? 'icons') ? (node.document.attr 'icons') : false
|
498
|
+
if icons == 'font' && !(node.attr? 'icon', nil, false)
|
499
|
+
icon_data = admonition_icon_data(label_text = type.to_sym)
|
500
|
+
label_width = label_min_width ? label_min_width : (icon_data[:size] * 1.5)
|
501
|
+
# NOTE icon_uri will consider icon attribute on node first, then type
|
502
|
+
elsif icons && ::File.readable?(icon_path = (node.icon_uri type))
|
503
|
+
icons = true
|
504
|
+
# TODO introduce @theme.admonition_image_width? or use size key from admonition_icon_<name>?
|
505
|
+
label_width = label_min_width ? label_min_width : 36.0
|
506
|
+
else
|
507
|
+
if icons
|
508
|
+
icons = false
|
509
|
+
warn %(asciidoctor: WARNING: admonition icon image not found or not readable: #{icon_path}) unless scratch?
|
510
|
+
end
|
511
|
+
label_text = node.caption
|
512
|
+
theme_font :admonition_label do
|
513
|
+
theme_font %(admonition_label_#{type}) do
|
514
|
+
if (transform = @text_transform) && transform != 'none'
|
515
|
+
label_text = transform_text label_text, transform
|
516
|
+
end
|
517
|
+
label_width = width_of label_text
|
518
|
+
label_width = label_min_width if label_min_width && label_min_width > label_width
|
519
|
+
end
|
520
|
+
end
|
521
|
+
end
|
522
|
+
unless ::Array === (cpad = @theme.admonition_padding)
|
523
|
+
cpad = ::Array.new 4, cpad
|
524
|
+
end
|
525
|
+
unless ::Array === (lpad = @theme.admonition_label_padding || cpad)
|
526
|
+
lpad = ::Array.new 4, lpad
|
527
|
+
end
|
450
528
|
# FIXME this shift stuff is a real hack until we have proper margin collapsing
|
451
529
|
shift_base = @theme.prose_margin_bottom
|
452
|
-
#shift_top = icons ? (shift_base / 3.0) : 0
|
453
|
-
#shift_bottom = icons ? ((shift_base * 2) / 3.0) : shift_base
|
454
530
|
shift_top = shift_base / 3.0
|
455
531
|
shift_bottom = (shift_base * 2) / 3.0
|
456
532
|
keep_together do |box_height = nil|
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
abs_right = bounds.absolute_right
|
462
|
-
pad_box @theme.admonition_padding do
|
463
|
-
left_padding = bounds.absolute_left - abs_left
|
464
|
-
right_padding = abs_right - bounds.absolute_right
|
465
|
-
if box_height
|
533
|
+
pad_box [0, cpad[1], 0, lpad[3]] do
|
534
|
+
if box_height
|
535
|
+
if (rule_color = @theme.admonition_column_rule_color) &&
|
536
|
+
(rule_width = @theme.admonition_column_rule_width || @theme.base_border_width) && rule_width > 0
|
466
537
|
float do
|
467
|
-
bounding_box [0, cursor], width: label_width +
|
538
|
+
bounding_box [0, cursor], width: label_width + lpad[1], height: box_height do
|
539
|
+
stroke_vertical_rule rule_color,
|
540
|
+
at: bounds.width,
|
541
|
+
line_style: (@theme.admonition_column_rule_style || :solid).to_sym,
|
542
|
+
line_width: rule_width
|
543
|
+
end
|
544
|
+
end
|
545
|
+
end
|
546
|
+
float do
|
547
|
+
bounding_box [0, cursor], width: label_width, height: box_height do
|
548
|
+
if icons == 'font'
|
549
|
+
# FIXME we're assume icon is a square
|
550
|
+
icon_size = fit_icon_to_bounds icon_data[:size]
|
551
|
+
# NOTE Prawn's vertical center is not reliable, so calculate it manually
|
552
|
+
if label_valign == :center
|
553
|
+
label_valign = :top
|
554
|
+
if (vcenter_pos = (box_height - icon_size) * 0.5) > 0
|
555
|
+
move_down vcenter_pos
|
556
|
+
end
|
557
|
+
end
|
558
|
+
icon icon_data[:name],
|
559
|
+
valign: label_valign,
|
560
|
+
align: label_align,
|
561
|
+
color: icon_data[:stroke_color],
|
562
|
+
size: icon_size
|
563
|
+
elsif icons
|
564
|
+
begin
|
565
|
+
image_obj, image_info = build_image_object icon_path
|
566
|
+
icon_aspect_ratio = image_info.width.fdiv image_info.height
|
567
|
+
# NOTE don't scale image up if smaller than label_width
|
568
|
+
icon_width = [(to_pt image_info.width, :px), label_width].min
|
569
|
+
if (icon_height = icon_width * (1 / icon_aspect_ratio)) > box_height
|
570
|
+
icon_width *= box_height / icon_height
|
571
|
+
icon_height = box_height
|
572
|
+
end
|
573
|
+
embed_image image_obj, image_info, width: icon_width, position: label_align, vposition: label_valign
|
574
|
+
rescue => e
|
575
|
+
# QUESTION should we show the label in this case?
|
576
|
+
warn %(asciidoctor: WARNING: could not embed admonition icon image: #{icon_path}; #{e.message})
|
577
|
+
end
|
578
|
+
else
|
468
579
|
# IMPORTANT the label must fit in the alotted space or it shows up on another page!
|
469
580
|
# QUESTION anyway to prevent text overflow in the case it doesn't fit?
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
581
|
+
theme_font :admonition_label do
|
582
|
+
theme_font %(admonition_label_#{type}) do
|
583
|
+
# NOTE Prawn's vertical center is not reliable, so calculate it manually
|
584
|
+
if label_valign == :center
|
585
|
+
label_valign = :top
|
586
|
+
if (vcenter_pos = (box_height - (height_of_typeset_text label_text, line_height: 1)) * 0.5) > 0
|
587
|
+
move_down vcenter_pos
|
588
|
+
end
|
589
|
+
end
|
590
|
+
@text_transform = nil # already applied to label
|
591
|
+
layout_prose label_text,
|
592
|
+
align: label_align,
|
593
|
+
valign: label_valign,
|
594
|
+
line_height: 1,
|
595
|
+
margin: 0,
|
596
|
+
inline_format: false
|
597
|
+
end
|
483
598
|
end
|
484
599
|
end
|
485
600
|
end
|
486
601
|
end
|
487
|
-
|
488
|
-
|
489
|
-
|
602
|
+
end
|
603
|
+
pad_box [cpad[0], 0, cpad[2], label_width + lpad[1] + cpad[3]] do
|
604
|
+
move_down shift_top
|
605
|
+
layout_caption node.title if node.title?
|
606
|
+
theme_font :admonition do
|
490
607
|
convert_content_for_block node
|
491
|
-
# FIXME HACK compensate for margin bottom of admonition content
|
492
|
-
move_up shift_bottom unless at_page_top?
|
493
608
|
end
|
609
|
+
# FIXME HACK compensate for margin bottom of admonition content
|
610
|
+
move_up shift_bottom unless at_page_top?
|
494
611
|
end
|
495
|
-
|
612
|
+
end
|
496
613
|
end
|
497
614
|
theme_margin :block, :bottom
|
498
615
|
end
|
@@ -519,18 +636,15 @@ class Converter < ::Prawn::Document
|
|
519
636
|
end
|
520
637
|
|
521
638
|
def convert_open node
|
522
|
-
|
523
|
-
when 'abstract'
|
639
|
+
if node.style == 'abstract'
|
524
640
|
convert_abstract node
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
add_dest_for_block node if node.id
|
530
|
-
convert_content_for_block node
|
531
|
-
end
|
641
|
+
elsif node.style == 'partintro' && node.blocks.size == 1 && node.blocks.first.style == 'abstract'
|
642
|
+
# TODO process block title and id
|
643
|
+
# TODO process abstract child even when partintro has multiple blocks
|
644
|
+
convert_abstract node.blocks.first
|
532
645
|
else
|
533
646
|
add_dest_for_block node if node.id
|
647
|
+
layout_caption node.title if node.title?
|
534
648
|
convert_content_for_block node
|
535
649
|
end
|
536
650
|
end
|
@@ -543,6 +657,7 @@ class Converter < ::Prawn::Document
|
|
543
657
|
keep_together do |box_height = nil|
|
544
658
|
start_page_number = page_number
|
545
659
|
start_cursor = cursor
|
660
|
+
caption_height = node.title? ? (layout_caption node) : 0
|
546
661
|
pad_box @theme.blockquote_padding do
|
547
662
|
theme_font :blockquote do
|
548
663
|
if node.context == :quote
|
@@ -559,8 +674,9 @@ class Converter < ::Prawn::Document
|
|
559
674
|
end
|
560
675
|
end
|
561
676
|
# FIXME we want to draw graphics before content, but box_height is not reliable when spanning pages
|
562
|
-
if
|
563
|
-
|
677
|
+
# FIXME border extends to bottom of content area if block terminates at bottom of page
|
678
|
+
if box_height && b_width > 0
|
679
|
+
page_spread = page_number - start_page_number + 1
|
564
680
|
end_cursor = cursor
|
565
681
|
go_to_page start_page_number
|
566
682
|
move_cursor_to start_cursor
|
@@ -573,9 +689,20 @@ class Converter < ::Prawn::Document
|
|
573
689
|
y_draw = cursor
|
574
690
|
b_height = page_spread - 1 == i ? (y_draw - end_cursor) : y_draw
|
575
691
|
end
|
692
|
+
# NOTE skip past caption if present
|
693
|
+
if caption_height > 0
|
694
|
+
if caption_height > cursor
|
695
|
+
caption_height -= cursor
|
696
|
+
next # keep skipping, caption is on next page
|
697
|
+
end
|
698
|
+
y_draw -= caption_height
|
699
|
+
b_height -= caption_height
|
700
|
+
caption_height = 0
|
701
|
+
end
|
702
|
+
# NOTE b_height is 0 when block terminates at bottom of page
|
576
703
|
bounding_box [0, y_draw], width: bounds.width, height: b_height do
|
577
704
|
stroke_vertical_rule b_color, line_width: b_width, at: b_width / 2.0
|
578
|
-
end
|
705
|
+
end unless b_height == 0
|
579
706
|
end
|
580
707
|
end
|
581
708
|
end
|
@@ -600,7 +727,7 @@ class Converter < ::Prawn::Document
|
|
600
727
|
if node.title?
|
601
728
|
theme_font :sidebar_title do
|
602
729
|
# QUESTION should we allow margins of sidebar title to be customized?
|
603
|
-
layout_heading node.title, align: (@theme.sidebar_title_align || @
|
730
|
+
layout_heading node.title, align: (@theme.sidebar_title_align || @base_align).to_sym, margin_top: 0
|
604
731
|
end
|
605
732
|
end
|
606
733
|
theme_font :sidebar do
|
@@ -685,6 +812,7 @@ class Converter < ::Prawn::Document
|
|
685
812
|
def convert_olist node
|
686
813
|
add_dest_for_block node if node.id
|
687
814
|
@list_numbers ||= []
|
815
|
+
# TODO move list_number resolve to a method
|
688
816
|
list_number = case node.style
|
689
817
|
when 'arabic'
|
690
818
|
'1'
|
@@ -824,120 +952,103 @@ class Converter < ::Prawn::Document
|
|
824
952
|
node.extend ::Asciidoctor::Image unless ::Asciidoctor::Image === node
|
825
953
|
target, image_format = node.target_and_format
|
826
954
|
|
827
|
-
if image_format == 'gif'
|
828
|
-
warn %(asciidoctor: WARNING: GIF image format not supported.
|
955
|
+
if image_format == 'gif' && !(defined? ::GMagick::Image)
|
956
|
+
warn %(asciidoctor: WARNING: GIF image format not supported. Install the prawn-gmagick gem or convert #{target} to PNG.) unless scratch?
|
829
957
|
image_path = false
|
958
|
+
elsif ::Base64 === target
|
959
|
+
image_path = target
|
830
960
|
elsif (image_path = resolve_image_path node, target, (opts.fetch :relative_to_imagesdir, true), image_format) &&
|
831
961
|
(::File.readable? image_path)
|
832
962
|
# NOTE import_page automatically advances to next page afterwards
|
833
963
|
# QUESTION should we add destination to top of imported page?
|
834
|
-
return import_page image_path if image_format == 'pdf'
|
964
|
+
return import_page image_path, replace: page_is_empty? if image_format == 'pdf'
|
835
965
|
else
|
836
966
|
warn %(asciidoctor: WARNING: image to embed not found or not readable: #{image_path || target}) unless scratch?
|
837
|
-
image_path = false
|
838
967
|
# QUESTION should we use alt text in this case?
|
839
968
|
return if image_format == 'pdf'
|
969
|
+
image_path = false
|
840
970
|
end
|
841
971
|
|
842
|
-
|
843
|
-
add_dest_for_block node if node.id
|
844
|
-
alignment = ((node.attr 'align', nil, false) || @theme.image_align).to_sym
|
972
|
+
theme_margin :block, :top unless (pinned = opts[:pinned])
|
845
973
|
|
846
|
-
|
974
|
+
return on_image_error :missing, node, target, opts unless image_path
|
847
975
|
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
|
854
|
-
|
855
|
-
layout_caption node, side: :bottom if node.title?
|
856
|
-
theme_margin :block, :bottom
|
857
|
-
return
|
858
|
-
end
|
976
|
+
# TODO move this calculation into a method, such as layout_caption node, side: :bottom, dry_run: true
|
977
|
+
caption_h = 0
|
978
|
+
dry_run do
|
979
|
+
move_down 0.0001 # hack to force top margin to be applied
|
980
|
+
# NOTE we assume caption fits on a single page, which seems reasonable
|
981
|
+
caption_h = layout_caption node, side: :bottom
|
982
|
+
end if node.title?
|
859
983
|
|
860
984
|
# TODO support cover (aka canvas) image layout using "canvas" (or "cover") role
|
861
|
-
width = resolve_explicit_width node.attributes, bounds.width, support_vw: true, use_fallback: true
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
866
|
-
overflow = 0
|
867
|
-
end
|
985
|
+
width = resolve_explicit_width node.attributes, (available_w = bounds.width), support_vw: true, use_fallback: true
|
986
|
+
# TODO add `to_pt page_width` method to ViewportWidth type
|
987
|
+
width = (width.to_f / 100) * page_width if (width_relative_to_page = ViewportWidth === width)
|
988
|
+
|
989
|
+
alignment = ((node.attr 'align', nil, false) || @theme.image_align).to_sym
|
868
990
|
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
873
|
-
|
991
|
+
begin
|
992
|
+
span_page_width_if width_relative_to_page do
|
993
|
+
if image_format == 'svg'
|
994
|
+
if ::Base64 === image_path
|
995
|
+
svg_data = ::Base64.decode64 image_path
|
996
|
+
file_request_root = false
|
997
|
+
else
|
998
|
+
svg_data = ::IO.read image_path
|
999
|
+
file_request_root = ::File.dirname image_path
|
1000
|
+
end
|
874
1001
|
svg_obj = ::Prawn::Svg::Interface.new svg_data, self,
|
875
1002
|
position: alignment,
|
876
1003
|
width: width,
|
877
|
-
fallback_font_name:
|
878
|
-
enable_web_requests: (
|
1004
|
+
fallback_font_name: default_svg_font,
|
1005
|
+
enable_web_requests: (node.document.attr? 'allow-uri-read'),
|
879
1006
|
# TODO enforce jail in safe mode
|
880
|
-
enable_file_requests_with_root:
|
1007
|
+
enable_file_requests_with_root: file_request_root
|
881
1008
|
rendered_w = (svg_size = svg_obj.document.sizing).output_width
|
882
1009
|
if !width && (svg_obj.document.root.attributes.key? 'width')
|
883
|
-
# NOTE scale native width & height
|
884
|
-
if (adjusted_w = [
|
885
|
-
|
886
|
-
svg_obj = ::Prawn::Svg::Interface.new svg_data, self,
|
887
|
-
position: alignment,
|
888
|
-
width: (rendered_w = adjusted_w),
|
889
|
-
fallback_font_name: fallback_font_name,
|
890
|
-
enable_web_requests: enable_web_requests,
|
891
|
-
enable_file_requests_with_root: file_request_root
|
892
|
-
svg_size = svg_obj.document.sizing
|
1010
|
+
# NOTE scale native width & height from px to pt and restrict width to available width
|
1011
|
+
if (adjusted_w = [available_w, (to_pt rendered_w, :px)].min) != rendered_w
|
1012
|
+
svg_size = svg_obj.resize width: (rendered_w = adjusted_w)
|
893
1013
|
end
|
894
1014
|
end
|
895
|
-
#
|
896
|
-
rendered_h = svg_size.output_height
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
link_annotation [(abs_left = svg_obj.position[0] + bounds.absolute_left), y, (abs_left + rendered_w), (y + rendered_h)],
|
904
|
-
Border: [0, 0, 0],
|
905
|
-
A: { Type: :Action, S: :URI, URI: (str2pdfval link) }
|
1015
|
+
# NOTE shrink image so it fits within available space; group image & caption
|
1016
|
+
if (rendered_h = svg_size.output_height) > (available_h = cursor - caption_h)
|
1017
|
+
unless pinned || at_page_top?
|
1018
|
+
start_new_page
|
1019
|
+
available_h = cursor - caption_h
|
1020
|
+
end
|
1021
|
+
if rendered_h > available_h
|
1022
|
+
rendered_w = (svg_size = svg_obj.resize height: (rendered_h = available_h)).output_width
|
906
1023
|
end
|
907
|
-
indent(*overflow) do
|
908
|
-
layout_caption node, side: :bottom
|
909
|
-
end if node.title?
|
910
1024
|
end
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
1025
|
+
add_dest_for_block node if node.id
|
1026
|
+
# NOTE workaround to fix Prawn not adding fill and stroke commands on page that only has an image;
|
1027
|
+
# breakage occurs when running content (stamps) are added to page
|
1028
|
+
update_colors if graphic_state.color_space.empty?
|
1029
|
+
# 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)
|
1030
|
+
# NOTE cursor advanced automatically
|
1031
|
+
svg_obj.draw
|
1032
|
+
if (link = node.attr 'link', nil, false)
|
1033
|
+
link_box = [(abs_left = svg_obj.position[0] + bounds.absolute_left), y, (abs_left + rendered_w), (y + rendered_h)]
|
1034
|
+
link_annotation link_box, Border: [0, 0, 0], A: { Type: :Action, S: :URI, URI: link.as_pdf }
|
1035
|
+
end
|
1036
|
+
else
|
916
1037
|
# FIXME this code really needs to be better organized!
|
917
|
-
# FIXME temporary workaround to group caption & image
|
918
1038
|
# NOTE use low-level API to access intrinsic dimensions; build_image_object caches image data previously loaded
|
919
|
-
image_obj, image_info = ::
|
920
|
-
|
921
|
-
|
922
|
-
|
923
|
-
|
924
|
-
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
# NOTE shrink image so it fits on a single page if height exceeds page height
|
933
|
-
if rendered_h > (available_height = cursor - caption_height)
|
934
|
-
rendered_w = (rendered_w * available_height) / rendered_h
|
935
|
-
rendered_h = available_height
|
936
|
-
# FIXME workaround to fix Prawn not adding fill and stroke commands
|
937
|
-
# on page that only has an image; breakage occurs when line numbers are added
|
938
|
-
# NOTE this no longer seems to be an issue
|
939
|
-
fill_color self.fill_color
|
940
|
-
stroke_color self.stroke_color
|
1039
|
+
image_obj, image_info = ::Base64 === image_path ?
|
1040
|
+
::StringIO.open((::Base64.decode64 image_path), 'rb') {|fd| build_image_object fd } :
|
1041
|
+
::File.open(image_path, 'rb') {|fd| build_image_object fd }
|
1042
|
+
# NOTE if width is not specified, scale native width & height from px to pt and restrict width to available width
|
1043
|
+
rendered_w, rendered_h = image_info.calc_image_dimensions width: (width || [available_w, (to_pt image_info.width, :px)].min)
|
1044
|
+
# NOTE shrink image so it fits within available space; group image & caption
|
1045
|
+
if rendered_h > (available_h = cursor - caption_h)
|
1046
|
+
unless pinned || at_page_top?
|
1047
|
+
start_new_page
|
1048
|
+
available_h = cursor - caption_h
|
1049
|
+
end
|
1050
|
+
if rendered_h > available_h
|
1051
|
+
rendered_w, rendered_h = image_info.calc_image_dimensions height: (rendered_h = available_h)
|
941
1052
|
end
|
942
1053
|
end
|
943
1054
|
# NOTE must calculate link position before embedding to get proper boundaries
|
@@ -945,25 +1056,100 @@ class Converter < ::Prawn::Document
|
|
945
1056
|
img_x, img_y = image_position rendered_w, rendered_h, position: alignment
|
946
1057
|
link_box = [img_x, (img_y - rendered_h), (img_x + rendered_w), img_y]
|
947
1058
|
end
|
948
|
-
|
949
|
-
if
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
1059
|
+
image_top = cursor
|
1060
|
+
add_dest_for_block node if node.id
|
1061
|
+
# NOTE workaround to fix Prawn not adding fill and stroke commands on page that only has an image;
|
1062
|
+
# breakage occurs when running content (stamps) are added to page
|
1063
|
+
update_colors if graphic_state.color_space.empty?
|
1064
|
+
# NOTE specify both width and height to avoid recalculation
|
1065
|
+
embed_image image_obj, image_info, width: rendered_w, height: rendered_h, position: alignment
|
1066
|
+
link_annotation link_box, Border: [0, 0, 0], A: { Type: :Action, S: :URI, URI: link.as_pdf } if link
|
1067
|
+
# NOTE Asciidoctor disables automatic advancement of cursor for raster images, so move cursor manually
|
1068
|
+
move_down rendered_h if cursor == image_top
|
956
1069
|
end
|
957
|
-
indent(*overflow) do
|
958
|
-
layout_caption node, side: :bottom
|
959
|
-
end if node.title?
|
960
1070
|
end
|
1071
|
+
layout_caption node, side: :bottom if node.title?
|
1072
|
+
theme_margin :block, :bottom unless pinned
|
1073
|
+
rescue => e
|
1074
|
+
on_image_error :exception, node, target, (opts.merge message: %(asciidoctor: WARNING: could not embed image: #{image_path}; #{e.message}))
|
961
1075
|
end
|
962
|
-
theme_margin :block, :bottom
|
963
1076
|
ensure
|
964
1077
|
unlink_tmp_file image_path if image_path
|
965
1078
|
end
|
966
1079
|
|
1080
|
+
def on_image_error reason, node, target, opts = {}
|
1081
|
+
warn opts[:message] if opts.key? :message
|
1082
|
+
alt_text = (link = node.attr 'link', nil, false) ?
|
1083
|
+
%(<a href="#{link}">[#{node.attr 'alt'}]</a> | <em>#{target}</em>) :
|
1084
|
+
%([#{node.attr 'alt'}] | <em>#{target}</em>)
|
1085
|
+
layout_prose alt_text,
|
1086
|
+
align: ((node.attr 'align', nil, false) || @theme.image_align).to_sym,
|
1087
|
+
margin: 0,
|
1088
|
+
normalize: false,
|
1089
|
+
single_line: true
|
1090
|
+
layout_caption node, side: :bottom if node.title?
|
1091
|
+
theme_margin :block, :bottom unless opts[:pinned]
|
1092
|
+
nil
|
1093
|
+
end
|
1094
|
+
|
1095
|
+
def convert_audio node
|
1096
|
+
add_dest_for_block node if node.id
|
1097
|
+
theme_margin :block, :top
|
1098
|
+
audio_path = node.media_uri(node.attr 'target')
|
1099
|
+
play_symbol = (node.document.attr? 'icons', 'font') ?
|
1100
|
+
%(<font name="fa">#{::Prawn::Icon::FontData.load(self, 'fa').unicode 'play'}</font>) : RightPointer
|
1101
|
+
layout_prose %(#{play_symbol}#{NoBreakSpace}<a href="#{audio_path}">#{audio_path}</a> <em>(audio)</em>), normalize: false, margin: 0, single_line: true
|
1102
|
+
layout_caption node, side: :bottom if node.title?
|
1103
|
+
theme_margin :block, :bottom
|
1104
|
+
end
|
1105
|
+
|
1106
|
+
def convert_video node
|
1107
|
+
case (poster = node.attr 'poster', nil, false)
|
1108
|
+
when 'youtube'
|
1109
|
+
video_path = %(https://www.youtube.com/watch?v=#{video_id = node.attr 'target'})
|
1110
|
+
# see http://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
|
1111
|
+
poster = (node.document.attr? 'allow-uri-read') ? %(https://img.youtube.com/vi/#{video_id}/maxresdefault.jpg) : nil
|
1112
|
+
type = 'YouTube video'
|
1113
|
+
when 'vimeo'
|
1114
|
+
video_path = %(https://vimeo.com/#{video_id = node.attr 'target'})
|
1115
|
+
if node.document.attr? 'allow-uri-read'
|
1116
|
+
if node.document.attr? 'cache-uri'
|
1117
|
+
Helpers.require_library 'open-uri/cached', 'open-uri-cached' unless defined? ::OpenURI::Cache
|
1118
|
+
else
|
1119
|
+
::OpenURI
|
1120
|
+
end
|
1121
|
+
poster = open(%(http://vimeo.com/api/v2/video/#{video_id}.xml), 'r') do |f|
|
1122
|
+
(/<thumbnail_large>(.*?)<\/thumbnail_large>/.match f.read)[1]
|
1123
|
+
end
|
1124
|
+
else
|
1125
|
+
poster = nil
|
1126
|
+
end
|
1127
|
+
type = 'Vimeo video'
|
1128
|
+
else
|
1129
|
+
video_path = node.media_uri(node.attr 'target')
|
1130
|
+
type = 'video'
|
1131
|
+
end
|
1132
|
+
|
1133
|
+
if poster.nil_or_empty?
|
1134
|
+
add_dest_for_block node if node.id
|
1135
|
+
theme_margin :block, :top
|
1136
|
+
play_symbol = (node.document.attr? 'icons', 'font') ?
|
1137
|
+
%(<font name="fa">#{::Prawn::Icon::FontData.load(self, 'fa').unicode 'play'}</font>) : RightPointer
|
1138
|
+
layout_prose %(#{play_symbol}#{NoBreakSpace}<a href="#{video_path}">#{video_path}</a> <em>(#{type})</em>), normalize: false, margin: 0, single_line: true
|
1139
|
+
layout_caption node, side: :bottom if node.title?
|
1140
|
+
theme_margin :block, :bottom
|
1141
|
+
else
|
1142
|
+
original_attributes = node.attributes.dup
|
1143
|
+
begin
|
1144
|
+
node.update_attributes 'target' => poster, 'link' => video_path
|
1145
|
+
#node.set_attr 'pdfwidth', '100%' unless (node.attr? 'width') || (node.attr? 'pdfwidth')
|
1146
|
+
convert_image node
|
1147
|
+
ensure
|
1148
|
+
node.attributes.replace original_attributes
|
1149
|
+
end
|
1150
|
+
end
|
1151
|
+
end
|
1152
|
+
|
967
1153
|
# QUESTION can we avoid arranging fragments multiple times (conums & autofit) by eagerly preparing arranger?
|
968
1154
|
def convert_listing_or_literal node
|
969
1155
|
add_dest_for_block node if node.id
|
@@ -1001,6 +1187,8 @@ class Converter < ::Prawn::Document
|
|
1001
1187
|
source_string = preserve_indentation node.content, (node.attr 'tabsize')
|
1002
1188
|
end
|
1003
1189
|
|
1190
|
+
bg_color_override = nil
|
1191
|
+
|
1004
1192
|
source_chunks = case highlighter
|
1005
1193
|
when 'coderay'
|
1006
1194
|
Helpers.require_library CodeRayRequirePath, 'coderay' unless defined? ::Asciidoctor::Prawn::CodeRayEncoder
|
@@ -1010,13 +1198,28 @@ class Converter < ::Prawn::Document
|
|
1010
1198
|
when 'pygments'
|
1011
1199
|
Helpers.require_library 'pygments', 'pygments.rb' unless defined? ::Pygments
|
1012
1200
|
lexer = ::Pygments::Lexer[node.attr 'language', 'text', false] || ::Pygments::Lexer['text']
|
1013
|
-
pygments_config = {
|
1201
|
+
pygments_config = {
|
1202
|
+
nowrap: true,
|
1203
|
+
noclasses: true,
|
1204
|
+
stripnl: false,
|
1205
|
+
style: style = (node.document.attr 'pygments-style') || 'pastie'
|
1206
|
+
}
|
1014
1207
|
# TODO enable once we support background color on spans
|
1015
1208
|
#if node.attr? 'highlight', nil, false
|
1016
1209
|
# unless (hl_lines = node.resolve_lines_to_highlight(node.attr 'highlight', nil, false)).empty?
|
1017
1210
|
# pygments_config[:hl_lines] = hl_lines * ' '
|
1018
1211
|
# end
|
1019
1212
|
#end
|
1213
|
+
# QUESTION should we treat white background as inherit?
|
1214
|
+
# QUESTION allow border color to be set by theme for highlighted block?
|
1215
|
+
if (node.document.attr? 'pygments-bgcolor')
|
1216
|
+
bg_color_override = node.document.attr 'pygments-bgcolor'
|
1217
|
+
elsif style == 'pastie'
|
1218
|
+
node.document.set_attr 'pygments-bgcolor', (bg_color_override = nil)
|
1219
|
+
else
|
1220
|
+
node.document.set_attr 'pygments-bgcolor',
|
1221
|
+
(bg_color_override = PygmentsBgColorRx =~ (::Pygments.css '.highlight', style: style) ? $1 : nil)
|
1222
|
+
end
|
1020
1223
|
source_string, conum_mapping = extract_conums source_string
|
1021
1224
|
# NOTE pygments.rb strips trailing whitespace; preserve it in case there are conums on last line
|
1022
1225
|
num_trailing_spaces = source_string.size - (source_string = source_string.rstrip).size if conum_mapping
|
@@ -1026,7 +1229,9 @@ class Converter < ::Prawn::Document
|
|
1026
1229
|
when 'rouge'
|
1027
1230
|
Helpers.require_library RougeRequirePath, 'rouge' unless defined? ::Rouge::Formatters::Prawn
|
1028
1231
|
lexer = ::Rouge::Lexer.find(node.attr 'language', 'text', false) || ::Rouge::Lexers::PlainText
|
1029
|
-
formatter = (@rouge_formatter ||= ::Rouge::Formatters::Prawn.new theme: (node.document.attr 'rouge-style'))
|
1232
|
+
formatter = (@rouge_formatter ||= ::Rouge::Formatters::Prawn.new theme: (node.document.attr 'rouge-style'), line_gap: @theme.code_line_gap)
|
1233
|
+
# QUESTION allow border color to be set by theme for highlighted block?
|
1234
|
+
bg_color_override = formatter.background_color
|
1030
1235
|
source_string, conum_mapping = extract_conums source_string
|
1031
1236
|
# NOTE trailing endline is added to address https://github.com/jneen/rouge/issues/279
|
1032
1237
|
fragments = formatter.format (lexer.lex %(#{source_string}#{LF})), line_numbers: (node.attr? 'linenums')
|
@@ -1057,7 +1262,7 @@ class Converter < ::Prawn::Document
|
|
1057
1262
|
# TODO move the multi-page logic to theme_fill_and_stroke_bounds
|
1058
1263
|
unless (b_width = @theme.code_border_width || 0) == 0
|
1059
1264
|
b_radius = (@theme.code_border_radius || 0) + b_width
|
1060
|
-
|
1265
|
+
b_gap_color = bg_color_override || @theme.code_background_color || @page_bg_color
|
1061
1266
|
end
|
1062
1267
|
remaining_height = box_height - caption_height
|
1063
1268
|
i = 0
|
@@ -1065,17 +1270,17 @@ class Converter < ::Prawn::Document
|
|
1065
1270
|
start_new_page if (started_new_page = i > 0)
|
1066
1271
|
fill_height = [remaining_height, cursor].min
|
1067
1272
|
bounding_box [0, cursor], width: bounds.width, height: fill_height do
|
1068
|
-
theme_fill_and_stroke_bounds :code
|
1273
|
+
theme_fill_and_stroke_bounds :code, background_color: bg_color_override
|
1069
1274
|
unless b_width == 0
|
1070
1275
|
indent b_radius, b_radius do
|
1071
1276
|
# dashed line to indicate continuation from previous page
|
1072
|
-
stroke_horizontal_rule
|
1277
|
+
stroke_horizontal_rule b_gap_color, line_width: b_width, line_style: :dashed
|
1073
1278
|
end if started_new_page
|
1074
1279
|
if remaining_height > fill_height
|
1075
1280
|
move_down fill_height
|
1076
1281
|
indent b_radius, b_radius do
|
1077
1282
|
# dashed line to indicate continuation on next page
|
1078
|
-
stroke_horizontal_rule
|
1283
|
+
stroke_horizontal_rule b_gap_color, line_width: b_width, line_style: :dashed
|
1079
1284
|
end
|
1080
1285
|
end
|
1081
1286
|
end
|
@@ -1207,11 +1412,13 @@ class Converter < ::Prawn::Document
|
|
1207
1412
|
even_row_bg_color = resolve_theme_color :table_even_row_background_color, tbl_bg_color
|
1208
1413
|
|
1209
1414
|
table_data = []
|
1210
|
-
node.rows[:head].each do |
|
1415
|
+
node.rows[:head].each do |row|
|
1211
1416
|
table_header = true
|
1212
|
-
head_transform = theme.table_head_text_transform
|
1417
|
+
if (head_transform = theme.table_head_text_transform)
|
1418
|
+
head_transform = nil if head_transform == 'none'
|
1419
|
+
end
|
1213
1420
|
row_data = []
|
1214
|
-
|
1421
|
+
row.each do |cell|
|
1215
1422
|
row_data << {
|
1216
1423
|
content: (head_transform ? (transform_text cell.text, head_transform) : cell.text),
|
1217
1424
|
inline_format: [normalize: true],
|
@@ -1223,15 +1430,15 @@ class Converter < ::Prawn::Document
|
|
1223
1430
|
colspan: cell.colspan || 1,
|
1224
1431
|
rowspan: cell.rowspan || 1,
|
1225
1432
|
align: (cell.attr 'halign', nil, false).to_sym,
|
1226
|
-
valign: (cell.attr 'valign', nil, false).to_sym
|
1433
|
+
valign: (val = cell.attr 'valign', nil, false) == 'middle' ? :center : val.to_sym
|
1227
1434
|
}
|
1228
1435
|
end
|
1229
1436
|
table_data << row_data
|
1230
1437
|
end
|
1231
1438
|
|
1232
|
-
(node.rows[:body] + node.rows[:foot]).each do |
|
1439
|
+
(node.rows[:body] + node.rows[:foot]).each do |row|
|
1233
1440
|
row_data = []
|
1234
|
-
|
1441
|
+
row.each do |cell|
|
1235
1442
|
cell_data = {
|
1236
1443
|
text_color: (theme.table_font_color || @font_color),
|
1237
1444
|
size: theme.table_font_size,
|
@@ -1250,25 +1457,22 @@ class Converter < ::Prawn::Document
|
|
1250
1457
|
unless defined? header_cell_data
|
1251
1458
|
header_cell_data = {}
|
1252
1459
|
[
|
1460
|
+
# TODO honor text_transform key
|
1253
1461
|
# QUESTION should we honor alignment set by col/cell spec? how can we tell?
|
1254
|
-
['align', :align, true],
|
1462
|
+
#['align', :align, true],
|
1255
1463
|
['font_color', :text_color, false],
|
1256
1464
|
['font_family', :font, false],
|
1257
1465
|
['font_size', :size, false],
|
1258
1466
|
['font_style', :font_style, true]
|
1259
1467
|
].each do |(theme_key, data_key, symbol_value)|
|
1260
|
-
if (val = theme[%(table_header_cell_#{theme_key})])
|
1468
|
+
if (val = theme[%(table_header_cell_#{theme_key})] || theme[%(table_head_#{theme_key})])
|
1261
1469
|
header_cell_data[data_key] = symbol_value ? val.to_sym : val
|
1262
1470
|
end
|
1263
1471
|
end
|
1264
|
-
|
1265
|
-
header_cell_data[:font_style] = val.to_sym
|
1266
|
-
end
|
1267
|
-
if (val = resolve_theme_color :table_header_cell_background_color)
|
1472
|
+
if (val = resolve_theme_color :table_header_cell_background_color, head_bg_color)
|
1268
1473
|
header_cell_data[:background_color] = val
|
1269
1474
|
end
|
1270
1475
|
end
|
1271
|
-
|
1272
1476
|
cell_data.update header_cell_data unless header_cell_data.empty?
|
1273
1477
|
when :monospaced
|
1274
1478
|
cell_data[:font] = theme.literal_font_family
|
@@ -1298,12 +1502,14 @@ class Converter < ::Prawn::Document
|
|
1298
1502
|
cell_data[:content] = preserve_indentation cell.text, (node.document.attr 'tabsize')
|
1299
1503
|
cell_data[:inline_format] = true
|
1300
1504
|
when :asciidoc
|
1301
|
-
|
1505
|
+
asciidoc_cell = ::Prawn::Table::Cell::AsciiDoc.new self,
|
1506
|
+
(cell_data.merge content: cell.inner_document, font_style: (val = theme.table_font_style) ? val.to_sym : nil)
|
1507
|
+
cell_data = { content: asciidoc_cell }
|
1302
1508
|
else
|
1303
1509
|
cell_data[:font_style] = (val = theme.table_font_style) ? val.to_sym : nil
|
1304
1510
|
end
|
1305
1511
|
unless cell_data.key? :content
|
1306
|
-
# NOTE effectively the same as calling cell.content
|
1512
|
+
# NOTE effectively the same as calling cell.content (should we use that instead?)
|
1307
1513
|
# TODO hard breaks not quite the same result as separate paragraphs; need custom cell impl
|
1308
1514
|
if (cell_text = cell.text).include? LF
|
1309
1515
|
cell_data[:content] = cell_text.split(BlankLineRx).map {|l| l.tr_s(WhitespaceChars, ' ') }.join(DoubleLF)
|
@@ -1318,10 +1524,17 @@ class Converter < ::Prawn::Document
|
|
1318
1524
|
table_data << row_data
|
1319
1525
|
end
|
1320
1526
|
|
1321
|
-
|
1527
|
+
# NOTE Prawn aborts if table data is empty, so ensure there's at least one row
|
1528
|
+
if table_data.empty?
|
1529
|
+
empty_row = []
|
1530
|
+
node.columns.each do
|
1531
|
+
empty_row << { content: '' }
|
1532
|
+
end
|
1533
|
+
table_data = [empty_row]
|
1534
|
+
end
|
1322
1535
|
|
1323
1536
|
border = {}
|
1324
|
-
table_border_color = theme.table_border_color
|
1537
|
+
table_border_color = theme.table_border_color || table_grid_color || theme.base_border_color
|
1325
1538
|
table_border_width = theme.table_border_width
|
1326
1539
|
table_grid_width = theme.table_grid_width || theme.table_border_width
|
1327
1540
|
[:top, :bottom, :left, :right].each {|edge| border[edge] = table_border_width }
|
@@ -1362,8 +1575,8 @@ class Converter < ::Prawn::Document
|
|
1362
1575
|
end
|
1363
1576
|
end
|
1364
1577
|
|
1365
|
-
if ((alignment = node.attr 'align', nil, false) && (
|
1366
|
-
(alignment = (node.roles &
|
1578
|
+
if ((alignment = node.attr 'align', nil, false) && (BlockAlignmentNames.include? alignment)) ||
|
1579
|
+
(alignment = (node.roles & BlockAlignmentNames).last)
|
1367
1580
|
alignment = alignment.to_sym
|
1368
1581
|
else
|
1369
1582
|
alignment = :left
|
@@ -1378,7 +1591,7 @@ class Converter < ::Prawn::Document
|
|
1378
1591
|
padding: theme.table_cell_padding,
|
1379
1592
|
border_width: 0,
|
1380
1593
|
# NOTE the border color of edges is set later
|
1381
|
-
border_color: theme.table_grid_color || theme.table_border_color
|
1594
|
+
border_color: theme.table_grid_color || theme.table_border_color || theme.base_border_color
|
1382
1595
|
},
|
1383
1596
|
width: table_width,
|
1384
1597
|
column_widths: column_widths,
|
@@ -1388,7 +1601,7 @@ class Converter < ::Prawn::Document
|
|
1388
1601
|
theme_margin :block, :top
|
1389
1602
|
|
1390
1603
|
table table_data, table_settings do
|
1391
|
-
# NOTE capture resolved table width
|
1604
|
+
# NOTE call width to capture resolved table width
|
1392
1605
|
table_width = width
|
1393
1606
|
@pdf.layout_table_caption node, table_width, alignment if node.title? && caption_side == :top
|
1394
1607
|
if grid == 'none' && frame == 'none'
|
@@ -1440,7 +1653,7 @@ class Converter < ::Prawn::Document
|
|
1440
1653
|
foot_row.font = theme.table_foot_font_family if theme.table_foot_font_family
|
1441
1654
|
foot_row.font_style = theme.table_foot_font_style.to_sym if theme.table_foot_font_style
|
1442
1655
|
# HACK we should do this transformation when creating the cell
|
1443
|
-
#if (foot_transform = theme.table_foot_text_transform)
|
1656
|
+
#if (foot_transform = theme.table_foot_text_transform) && foot_transform != 'none'
|
1444
1657
|
# foot_row.each {|c| c.content = (transform_text c.content, foot_transform) if c.content }
|
1445
1658
|
#end
|
1446
1659
|
end
|
@@ -1468,6 +1681,52 @@ class Converter < ::Prawn::Document
|
|
1468
1681
|
start_new_page unless at_page_top?
|
1469
1682
|
end
|
1470
1683
|
|
1684
|
+
def convert_index_section node
|
1685
|
+
unless @index.empty?
|
1686
|
+
space_needed_for_category = @theme.description_list_term_spacing + (2 * (height_of_typeset_text 'A'))
|
1687
|
+
column_box [0, cursor], columns: 2, width: bounds.width do
|
1688
|
+
@index.categories.each do |category|
|
1689
|
+
# NOTE cursor method always returns 0 inside column_box; breaks reference_bounds.move_past_bottom
|
1690
|
+
bounds.move_past_bottom if space_needed_for_category > y - reference_bounds.absolute_bottom
|
1691
|
+
layout_prose category.name,
|
1692
|
+
align: :left,
|
1693
|
+
inline_format: false,
|
1694
|
+
margin_top: 0,
|
1695
|
+
margin_bottom: @theme.description_list_term_spacing,
|
1696
|
+
style: @theme.description_list_term_font_style.to_sym
|
1697
|
+
category.terms.each do |term|
|
1698
|
+
convert_index_list_item term
|
1699
|
+
end
|
1700
|
+
if @theme.prose_margin_bottom > y - reference_bounds.absolute_bottom
|
1701
|
+
bounds.move_past_bottom
|
1702
|
+
else
|
1703
|
+
move_down @theme.prose_margin_bottom
|
1704
|
+
end
|
1705
|
+
end
|
1706
|
+
end
|
1707
|
+
end
|
1708
|
+
nil
|
1709
|
+
end
|
1710
|
+
|
1711
|
+
def convert_index_list_item term
|
1712
|
+
text = term.name
|
1713
|
+
unless term.container?
|
1714
|
+
if @media == 'screen'
|
1715
|
+
pagenums = term.dests.map {|dest| %(<a anchor="#{dest[:anchor]}">#{dest[:page]}</a>) }
|
1716
|
+
else
|
1717
|
+
pagenums = term.dests.uniq {|dest| dest[:page] }.map {|dest| dest[:page].to_s }
|
1718
|
+
end
|
1719
|
+
text = %(#{escape_xml text}, #{pagenums * ', '})
|
1720
|
+
end
|
1721
|
+
layout_prose text, align: :left, margin: 0
|
1722
|
+
|
1723
|
+
term.subterms.each do |subterm|
|
1724
|
+
indent @theme.description_list_description_indent do
|
1725
|
+
convert_index_list_item subterm
|
1726
|
+
end
|
1727
|
+
end unless term.leaf?
|
1728
|
+
end
|
1729
|
+
|
1471
1730
|
def convert_inline_anchor node
|
1472
1731
|
case node.type
|
1473
1732
|
when :link
|
@@ -1478,11 +1737,14 @@ class Converter < ::Prawn::Document
|
|
1478
1737
|
end
|
1479
1738
|
#attrs << %( title="#{node.attr 'title'}") if node.attr? 'title'
|
1480
1739
|
attrs << %( target="#{node.attr 'window'}") if node.attr? 'window', nil, false
|
1740
|
+
if (role = node.attr 'role', nil, false) && (role == 'bare' || ((role.split ' ').include? 'bare'))
|
1741
|
+
# QUESTION should we insert breakable chars into URI when building fragment instead?
|
1742
|
+
%(<a href="#{node.target}"#{attrs.join}>#{breakable_uri node.text}</a>)
|
1481
1743
|
# NOTE @media may not be initialized if method is called before convert phase
|
1482
|
-
|
1483
|
-
|
1484
|
-
# TODO allow style of
|
1485
|
-
%(<a href="#{target = node.target}"#{attrs.join}>#{node.text}</a> [<font size="0.85em">#{target}</font>])
|
1744
|
+
elsif (@media ||= node.document.attr 'media', 'screen') != 'screen' || (node.document.attr? 'show-link-uri')
|
1745
|
+
# QUESTION should we insert breakable chars into URI when building fragment instead?
|
1746
|
+
# TODO allow style of printed link to be controlled by theme
|
1747
|
+
%(<a href="#{target = node.target}"#{attrs.join}>#{node.text}</a> [<font size="0.85em">#{breakable_uri target}</font>])
|
1486
1748
|
else
|
1487
1749
|
%(<a href="#{node.target}"#{attrs.join}>#{node.text}</a>)
|
1488
1750
|
end
|
@@ -1511,12 +1773,10 @@ class Converter < ::Prawn::Document
|
|
1511
1773
|
end
|
1512
1774
|
when :ref
|
1513
1775
|
# NOTE destination is created inside callback registered by FormattedTextTransform#build_fragment
|
1514
|
-
|
1515
|
-
%(<a name="#{node.target}">#{ZeroWidthSpace}</a>)
|
1776
|
+
%(<a name="#{node.target}">#{DummyText}</a>)
|
1516
1777
|
when :bibref
|
1517
1778
|
# NOTE destination is created inside callback registered by FormattedTextTransform#build_fragment
|
1518
|
-
|
1519
|
-
%(<a name="#{target = node.target}">#{ZeroWidthSpace}</a>[#{target}])
|
1779
|
+
%(<a name="#{target = node.target}">#{DummyText}</a>[#{target}])
|
1520
1780
|
else
|
1521
1781
|
warn %(asciidoctor: WARNING: unknown anchor type: #{node.type.inspect})
|
1522
1782
|
end
|
@@ -1581,12 +1841,13 @@ class Converter < ::Prawn::Document
|
|
1581
1841
|
else
|
1582
1842
|
node.extend ::Asciidoctor::Image unless ::Asciidoctor::Image === node
|
1583
1843
|
target, image_format = node.target_and_format
|
1584
|
-
if image_format == 'gif'
|
1585
|
-
warn %(asciidoctor: WARNING: GIF image format not supported.
|
1844
|
+
if image_format == 'gif' && !(defined? ::GMagick::Image)
|
1845
|
+
warn %(asciidoctor: WARNING: GIF image format not supported. Install the prawn-gmagick gem or convert #{target} to PNG.) unless scratch?
|
1586
1846
|
img = %([#{node.attr 'alt'}])
|
1847
|
+
# NOTE an image with a data URI is handled using a temporary file
|
1587
1848
|
elsif (image_path = resolve_image_path node, target, true, image_format) && (::File.readable? image_path)
|
1588
|
-
width_attr = (
|
1589
|
-
img = %(<img src="#{image_path}" format="#{image_format}" alt="#{node.attr 'alt'}"#{width_attr} tmp="#{TemporaryPath === image_path}">)
|
1849
|
+
width_attr = (width = preresolve_explicit_width node.attributes) ? %( width="#{width}") : nil
|
1850
|
+
img = %(<img src="#{image_path}" format="#{image_format}" alt="[#{node.attr 'alt'}]"#{width_attr} tmp="#{TemporaryPath === image_path}">)
|
1590
1851
|
else
|
1591
1852
|
warn %(asciidoctor: WARNING: image to embed not found or not readable: #{image_path || target}) unless scratch?
|
1592
1853
|
img = %([#{node.attr 'alt'}])
|
@@ -1596,7 +1857,24 @@ class Converter < ::Prawn::Document
|
|
1596
1857
|
end
|
1597
1858
|
|
1598
1859
|
def convert_inline_indexterm node
|
1599
|
-
|
1860
|
+
# NOTE indexterms not supported if text gets substituted before PDF is initialized
|
1861
|
+
return '' unless instance_variable_defined? :@index
|
1862
|
+
if scratch?
|
1863
|
+
node.type == :visible ? node.text : ''
|
1864
|
+
else
|
1865
|
+
dest = {
|
1866
|
+
anchor: (anchor_name = %(__indexterm-#{node.object_id}))
|
1867
|
+
# NOTE page number is added in InlineDestinationMarker
|
1868
|
+
}
|
1869
|
+
anchor = %(<a name="#{anchor_name}" type="indexterm">#{DummyText}</a>)
|
1870
|
+
if node.type == :visible
|
1871
|
+
@index.store_primary_term(sanitize(visible_term = node.text), dest)
|
1872
|
+
%(#{anchor}#{visible_term})
|
1873
|
+
else
|
1874
|
+
@index.store_term((node.attr 'terms').map {|term| sanitize term }, dest)
|
1875
|
+
anchor
|
1876
|
+
end
|
1877
|
+
end
|
1600
1878
|
end
|
1601
1879
|
|
1602
1880
|
def convert_inline_kbd node
|
@@ -1651,7 +1929,7 @@ class Converter < ::Prawn::Document
|
|
1651
1929
|
end
|
1652
1930
|
|
1653
1931
|
# NOTE destination is created inside callback registered by FormattedTextTransform#build_fragment
|
1654
|
-
node.id ? %(<a name="#{node.id}">#{
|
1932
|
+
node.id ? %(<a name="#{node.id}">#{DummyText}</a>#{quoted_text}) : quoted_text
|
1655
1933
|
end
|
1656
1934
|
|
1657
1935
|
# FIXME only create title page if doctype=book!
|
@@ -1676,8 +1954,8 @@ class Converter < ::Prawn::Document
|
|
1676
1954
|
# IMPORTANT this is the first page created, so we need to set the base font
|
1677
1955
|
font @theme.base_font_family, size: @theme.base_font_size
|
1678
1956
|
|
1679
|
-
# QUESTION allow
|
1680
|
-
title_align = (@theme.title_page_align || @
|
1957
|
+
# QUESTION allow alignment per element on title page?
|
1958
|
+
title_align = (@theme.title_page_align || @base_align).to_sym
|
1681
1959
|
|
1682
1960
|
# TODO disallow .pdf as image type
|
1683
1961
|
if (logo_image_path = (doc.attr 'title-logo-image', @theme.title_page_logo_image))
|
@@ -1703,14 +1981,12 @@ class Converter < ::Prawn::Document
|
|
1703
1981
|
else
|
1704
1982
|
logo_image_top = bounds.absolute_top - effective_page_height * logo_image_top.to_f / 100.0
|
1705
1983
|
end
|
1706
|
-
|
1707
|
-
|
1708
|
-
|
1709
|
-
|
1710
|
-
|
1711
|
-
|
1712
|
-
convert_image image_block, relative_to_imagesdir: relative_to_imagesdir
|
1713
|
-
end
|
1984
|
+
initial_y, @y = @y, logo_image_top
|
1985
|
+
# FIXME add API to Asciidoctor for creating blocks like this (extract from extensions module?)
|
1986
|
+
image_block = ::Asciidoctor::Block.new doc, :image, content_model: :empty, attributes: logo_image_attrs
|
1987
|
+
# NOTE pinned option keeps image on same page
|
1988
|
+
convert_image image_block, relative_to_imagesdir: relative_to_imagesdir, pinned: true
|
1989
|
+
@y = initial_y
|
1714
1990
|
end
|
1715
1991
|
|
1716
1992
|
# TODO prevent content from spilling to next page
|
@@ -1745,20 +2021,23 @@ class Converter < ::Prawn::Document
|
|
1745
2021
|
end
|
1746
2022
|
if doc.attr? 'authors'
|
1747
2023
|
move_down (@theme.title_page_authors_margin_top || 0)
|
2024
|
+
# TODO provide an API in core to get authors as an array
|
2025
|
+
authors = (1..(doc.attr 'authorcount', 1).to_i).map {|idx|
|
2026
|
+
doc.attr(idx == 1 ? 'author' : %(author_#{idx}))
|
2027
|
+
} * (@theme.title_page_authors_delimiter || ', ')
|
1748
2028
|
theme_font :title_page_authors do
|
1749
|
-
|
1750
|
-
layout_prose((doc.attr 'authors'),
|
2029
|
+
layout_prose authors,
|
1751
2030
|
align: title_align,
|
1752
2031
|
margin: 0,
|
1753
|
-
normalize: false
|
2032
|
+
normalize: false
|
1754
2033
|
end
|
1755
2034
|
move_down (@theme.title_page_authors_margin_bottom || 0)
|
1756
2035
|
end
|
1757
2036
|
revision_info = [(doc.attr? 'revnumber') ? %(#{doc.attr 'version-label'} #{doc.attr 'revnumber'}) : nil, (doc.attr 'revdate')].compact
|
1758
2037
|
unless revision_info.empty?
|
1759
2038
|
move_down (@theme.title_page_revision_margin_top || 0)
|
2039
|
+
revision_text = revision_info * (@theme.title_page_revision_delimiter || ', ')
|
1760
2040
|
theme_font :title_page_revision do
|
1761
|
-
revision_text = revision_info * (@theme.title_page_revision_delimiter || ', ')
|
1762
2041
|
layout_prose revision_text,
|
1763
2042
|
align: title_align,
|
1764
2043
|
margin: 0,
|
@@ -1813,14 +2092,14 @@ class Converter < ::Prawn::Document
|
|
1813
2092
|
def layout_heading string, opts = {}
|
1814
2093
|
top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme.heading_margin_top
|
1815
2094
|
bot_margin = margin || (opts.delete :margin_bottom) || @theme.heading_margin_bottom
|
1816
|
-
if (transform = (opts.delete :text_transform) || @text_transform)
|
2095
|
+
if (transform = (opts.delete :text_transform) || @text_transform) && transform != 'none'
|
1817
2096
|
string = transform_text string, transform
|
1818
2097
|
end
|
1819
2098
|
margin_top top_margin
|
1820
2099
|
typeset_text string, calc_line_metrics((opts.delete :line_height) || @theme.heading_line_height), {
|
1821
2100
|
color: @font_color,
|
1822
2101
|
inline_format: true,
|
1823
|
-
align: @
|
2102
|
+
align: @base_align.to_sym
|
1824
2103
|
}.merge(opts)
|
1825
2104
|
margin_bottom bot_margin
|
1826
2105
|
end
|
@@ -1829,24 +2108,19 @@ class Converter < ::Prawn::Document
|
|
1829
2108
|
def layout_prose string, opts = {}
|
1830
2109
|
top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme.prose_margin_top
|
1831
2110
|
bot_margin = margin || (opts.delete :margin_bottom) || @theme.prose_margin_bottom
|
1832
|
-
if (transform = (opts.delete :text_transform) || @text_transform)
|
2111
|
+
if (transform = (opts.delete :text_transform) || @text_transform) && transform != 'none'
|
1833
2112
|
string = transform_text string, transform
|
1834
2113
|
end
|
2114
|
+
# NOTE used by extensions; ensures linked text gets formatted using the link styles
|
1835
2115
|
if (anchor = opts.delete :anchor)
|
1836
|
-
|
1837
|
-
if (link_color = opts.delete :link_color)
|
1838
|
-
# NOTE CMYK value gets flattened here, but is restored by formatted text parser
|
1839
|
-
string = %(<a anchor="#{anchor}"><color rgb="#{link_color}">#{string}</color></a>)
|
1840
|
-
else
|
1841
|
-
string = %(<a anchor="#{anchor}">#{string}</a>)
|
1842
|
-
end
|
2116
|
+
string = %(<a anchor="#{anchor}">#{string}</a>)
|
1843
2117
|
end
|
1844
2118
|
margin_top top_margin
|
1845
2119
|
typeset_text string, calc_line_metrics((opts.delete :line_height) || @theme.base_line_height), {
|
1846
2120
|
color: @font_color,
|
1847
2121
|
# NOTE normalize makes endlines soft (replaces "\n" with ' ')
|
1848
2122
|
inline_format: [normalize: (opts.delete :normalize) != false],
|
1849
|
-
align: @
|
2123
|
+
align: @base_align.to_sym
|
1850
2124
|
}.merge(opts)
|
1851
2125
|
margin_bottom bot_margin
|
1852
2126
|
end
|
@@ -1873,7 +2147,7 @@ class Converter < ::Prawn::Document
|
|
1873
2147
|
layout_prose string, {
|
1874
2148
|
margin_top: margin[:top],
|
1875
2149
|
margin_bottom: margin[:bottom],
|
1876
|
-
align: (@theme.caption_align || @
|
2150
|
+
align: (@theme.caption_align || @base_align).to_sym,
|
1877
2151
|
normalize: false
|
1878
2152
|
}.merge(opts)
|
1879
2153
|
if side == :top && @theme.caption_border_bottom_color
|
@@ -1907,17 +2181,33 @@ class Converter < ::Prawn::Document
|
|
1907
2181
|
go_to_page toc_page_number unless (page_number == toc_page_number) || scratch?
|
1908
2182
|
start_page_number = page_number
|
1909
2183
|
theme_font :heading, level: 2 do
|
1910
|
-
|
2184
|
+
theme_font :toc_title do
|
2185
|
+
toc_title_align = (@theme.toc_title_align || @theme.heading_h2_align || @theme.heading_align || @base_align).to_sym
|
2186
|
+
layout_heading((doc.attr 'toc-title'), align: toc_title_align)
|
2187
|
+
end
|
1911
2188
|
end
|
1912
|
-
# QUESTION
|
2189
|
+
# QUESTION should we skip this whole method if num_levels == 0?
|
1913
2190
|
if num_levels > 0
|
1914
|
-
|
1915
|
-
|
1916
|
-
|
1917
|
-
|
1918
|
-
|
2191
|
+
dot_leader = theme_font :toc do
|
2192
|
+
# TODO we could simplify by using nested theme_font :toc_dot_leader
|
2193
|
+
if (dot_leader_font_style = (@theme.toc_dot_leader_font_style || :normal).to_sym) != font_style
|
2194
|
+
font_style dot_leader_font_style
|
2195
|
+
end
|
2196
|
+
{
|
2197
|
+
font_color: @theme.toc_dot_leader_font_color || @font_color,
|
2198
|
+
font_style: dot_leader_font_style,
|
2199
|
+
levels: ((dot_leader_l = @theme.toc_dot_leader_levels) == 'none' ? ::Set.new :
|
2200
|
+
(dot_leader_l && dot_leader_l != 'all' ? dot_leader_l.to_s.split.map(&:to_i).to_set : (1..num_levels).to_set)),
|
2201
|
+
text: (dot_leader_text = @theme.toc_dot_leader_content || DotLeaderTextDefault),
|
2202
|
+
width: dot_leader_text.empty? ? 0 : (width_of dot_leader_text),
|
2203
|
+
# TODO spacer gives a little bit of room between dots and page number
|
2204
|
+
spacer: { text: NoBreakSpace, size: (spacer_font_size = @font_size * 0.25) },
|
2205
|
+
spacer_width: (width_of NoBreakSpace, size: spacer_font_size)
|
2206
|
+
}
|
1919
2207
|
end
|
1920
|
-
|
2208
|
+
line_metrics = calc_line_metrics @theme.toc_line_height
|
2209
|
+
theme_margin :toc, :top
|
2210
|
+
layout_toc_level doc.sections, num_levels, line_metrics, dot_leader, num_front_matter_pages
|
1921
2211
|
end
|
1922
2212
|
# NOTE range must be calculated relative to toc_page_number; absolute page number in scratch document is arbitrary
|
1923
2213
|
toc_page_numbers = (toc_page_number..(toc_page_number + (page_number - start_page_number)))
|
@@ -1925,51 +2215,75 @@ class Converter < ::Prawn::Document
|
|
1925
2215
|
toc_page_numbers
|
1926
2216
|
end
|
1927
2217
|
|
1928
|
-
def layout_toc_level sections, num_levels, line_metrics,
|
1929
|
-
|
2218
|
+
def layout_toc_level sections, num_levels, line_metrics, dot_leader, num_front_matter_pages = 0
|
2219
|
+
# NOTE font options aren't always reliable, so store size separately
|
2220
|
+
toc_font_info = theme_font :toc do
|
2221
|
+
{ font: font, size: @font_size }
|
2222
|
+
end
|
1930
2223
|
sections.each do |sect|
|
1931
2224
|
theme_font :toc, level: (sect.level + 1) do
|
1932
|
-
sect_title =
|
1933
|
-
|
1934
|
-
|
1935
|
-
|
1936
|
-
|
1937
|
-
|
1938
|
-
|
1939
|
-
|
1940
|
-
|
2225
|
+
sect_title = (transform = @text_transform) && transform != 'none' ?
|
2226
|
+
(transform_text sect.numbered_title, transform) : sect.numbered_title
|
2227
|
+
# NOTE only write section title (excluding dots and page number) if this is a dry run
|
2228
|
+
if scratch?
|
2229
|
+
# FIXME use layout_prose
|
2230
|
+
# NOTE must wrap title in empty anchor element in case links are styled with different font family / size
|
2231
|
+
typeset_text %(<a>#{sect_title}</a>), line_metrics, inline_format: true
|
2232
|
+
else
|
2233
|
+
pgnum_label = ((sect.attr 'pdf-page-start') - num_front_matter_pages).to_s
|
2234
|
+
start_page_number = page_number
|
2235
|
+
start_cursor = cursor
|
2236
|
+
# NOTE use low-level text formatter to add anchor overlay without styling text as link & force color
|
2237
|
+
sect_title_format_override = {
|
2238
|
+
anchor: (sect_anchor = sect.attr 'pdf-anchor'),
|
2239
|
+
color: @font_color,
|
2240
|
+
styles: ((@theme[%(toc_h#{sect.level + 1}_text_decoration)] || @theme.toc_text_decoration) == 'underline' ?
|
2241
|
+
(font_styles << :underline) : font_styles)
|
2242
|
+
}
|
2243
|
+
(sect_title_fragments = text_formatter.format sect_title).each do |fragment|
|
2244
|
+
fragment.update sect_title_format_override do |key, old_val, new_val|
|
2245
|
+
key == :styles ? (old_val.merge new_val) : new_val
|
2246
|
+
end
|
2247
|
+
end
|
2248
|
+
typeset_formatted_text sect_title_fragments, line_metrics
|
1941
2249
|
end_page_number = page_number
|
1942
2250
|
end_cursor = cursor
|
1943
2251
|
# TODO it would be convenient to have a cursor mark / placement utility that took page number into account
|
1944
2252
|
go_to_page start_page_number if start_page_number != end_page_number
|
1945
2253
|
move_cursor_to start_cursor
|
1946
|
-
|
1947
|
-
|
1948
|
-
|
1949
|
-
|
1950
|
-
|
1951
|
-
|
1952
|
-
|
1953
|
-
|
1954
|
-
|
1955
|
-
|
1956
|
-
|
1957
|
-
|
2254
|
+
if dot_leader[:width] > 0 && (dot_leader[:levels].include? sect.level)
|
2255
|
+
pgnum_label_font_settings = { color: @font_color, font: font_family, size: @font_size, styles: font_styles }
|
2256
|
+
pgnum_label_width = width_of pgnum_label
|
2257
|
+
sect_title_width = width_of sect_title, inline_format: true
|
2258
|
+
save_font do
|
2259
|
+
# NOTE the same font is used for dot leaders throughout toc
|
2260
|
+
set_font toc_font_info[:font], toc_font_info[:size]
|
2261
|
+
font_style dot_leader[:font_style]
|
2262
|
+
num_dots = ((bounds.width - sect_title_width - dot_leader[:spacer_width] - pgnum_label_width) / dot_leader[:width]).floor
|
2263
|
+
# FIXME dots don't line up in columns if width of page numbers differ
|
2264
|
+
typeset_formatted_text [
|
2265
|
+
{ text: (dot_leader[:text] * (num_dots < 0 ? 0 : num_dots)), color: dot_leader[:font_color] },
|
2266
|
+
dot_leader[:spacer],
|
2267
|
+
{ text: pgnum_label, anchor: sect_anchor }.merge(pgnum_label_font_settings)
|
2268
|
+
], line_metrics, align: :right
|
2269
|
+
end
|
2270
|
+
else
|
2271
|
+
typeset_formatted_text [{ text: pgnum_label, color: @font_color, anchor: sect_anchor }], line_metrics, align: :right
|
2272
|
+
end
|
2273
|
+
go_to_page end_page_number if page_number != end_page_number
|
1958
2274
|
move_cursor_to end_cursor
|
1959
2275
|
end
|
1960
2276
|
end
|
1961
|
-
|
1962
|
-
|
1963
|
-
|
1964
|
-
end
|
1965
|
-
end
|
2277
|
+
indent @theme.toc_indent do
|
2278
|
+
layout_toc_level sect.sections, num_levels, line_metrics, dot_leader, num_front_matter_pages
|
2279
|
+
end if sect.level < num_levels
|
1966
2280
|
end
|
1967
2281
|
end
|
1968
2282
|
|
1969
|
-
# Reduce icon
|
2283
|
+
# Reduce icon height to fit inside bounds.height. Icons will not render
|
1970
2284
|
# properly if they are larger than the current bounds.height.
|
1971
|
-
def
|
1972
|
-
(
|
2285
|
+
def fit_icon_to_bounds preferred_size = 24
|
2286
|
+
(max_height = bounds.height) < preferred_size ? max_height : preferred_size
|
1973
2287
|
end
|
1974
2288
|
|
1975
2289
|
def admonition_icon_data key
|
@@ -1985,37 +2299,76 @@ class Converter < ::Prawn::Document
|
|
1985
2299
|
# QUESTION should we short-circuit if setting not specified and if so, which setting?
|
1986
2300
|
return unless (periphery == :header && @theme.header_height) || (periphery == :footer && @theme.footer_height)
|
1987
2301
|
skip = opts[:skip] || 1
|
1988
|
-
|
2302
|
+
# NOTE find and advance to first non-imported content page to use as model page
|
2303
|
+
return unless (content_start_page = state.pages[skip..-1].index {|p| !p.imported_page? })
|
2304
|
+
content_start_page += (skip + 1)
|
1989
2305
|
num_pages = page_count - skip
|
2306
|
+
prev_page_number = page_number
|
2307
|
+
go_to_page content_start_page
|
1990
2308
|
|
1991
2309
|
# FIXME probably need to treat doctypes differently
|
1992
|
-
|
1993
|
-
|
1994
|
-
#
|
2310
|
+
is_book = doc.doctype == 'book'
|
2311
|
+
header = doc.header? ? doc.header : nil
|
2312
|
+
# TODO make this section threshold configurable (perhaps in theme?)
|
2313
|
+
sections = doc.find_by(context: :section) {|sect| sect.level < 3 && sect != header } || []
|
2314
|
+
|
2315
|
+
# FIXME we need a proper model for all this page counting
|
2316
|
+
# FIXME we make a big assumption that part & chapter start on new pages
|
2317
|
+
# index parts, chapters and sections by the visual page number on which they start
|
2318
|
+
part_start_pages = {}
|
1995
2319
|
chapter_start_pages = {}
|
1996
2320
|
section_start_pages = {}
|
2321
|
+
trailing_section_start_pages = {}
|
1997
2322
|
sections.each do |sect|
|
1998
|
-
|
1999
|
-
|
2323
|
+
page_num = (sect.attr 'pdf-page-start').to_i - skip
|
2324
|
+
if is_book && ((sect_is_part = sect.part?) || sect.chapter?)
|
2325
|
+
if sect_is_part
|
2326
|
+
part_start_pages[page_num] ||= (sect.numbered_title formal: true)
|
2327
|
+
else
|
2328
|
+
chapter_start_pages[page_num] ||= (sect.numbered_title formal: true)
|
2329
|
+
if sect.sectname == 'appendix' && !part_start_pages.empty?
|
2330
|
+
# FIXME need a better way to indicate that part has ended
|
2331
|
+
part_start_pages[page_num] = ''
|
2332
|
+
end
|
2333
|
+
end
|
2000
2334
|
else
|
2001
|
-
|
2335
|
+
sect_title = trailing_section_start_pages[page_num] = sect.numbered_title formal: true
|
2336
|
+
section_start_pages[page_num] ||= sect_title
|
2002
2337
|
end
|
2003
2338
|
end
|
2004
2339
|
|
2005
|
-
# index chapters and sections by the visual page number on which they appear
|
2340
|
+
# index parts, chapters, and sections by the visual page number on which they appear
|
2341
|
+
parts_by_page = {}
|
2006
2342
|
chapters_by_page = {}
|
2007
2343
|
sections_by_page = {}
|
2008
|
-
|
2344
|
+
# QUESTION should the default part be the doctitle?
|
2345
|
+
last_part = nil
|
2346
|
+
# QUESTION should we enforce that the preamble is preface?
|
2347
|
+
last_chap = is_book ? (doc.attr 'preface-title', 'Preface') : nil
|
2009
2348
|
last_sect = nil
|
2349
|
+
sect_search_threshold = 1
|
2010
2350
|
(1..num_pages).each do |num|
|
2351
|
+
if (part = part_start_pages[num])
|
2352
|
+
last_part = part
|
2353
|
+
end
|
2011
2354
|
if (chap = chapter_start_pages[num])
|
2012
2355
|
last_chap = chap
|
2013
2356
|
end
|
2014
2357
|
if (sect = section_start_pages[num])
|
2015
2358
|
last_sect = sect
|
2016
|
-
elsif chap
|
2359
|
+
elsif part || chap
|
2360
|
+
sect_search_threshold = num
|
2017
2361
|
last_sect = nil
|
2362
|
+
# NOTE we didn't find a section on this page; look back to find last section started
|
2363
|
+
elsif last_sect
|
2364
|
+
((sect_search_threshold)..(num - 1)).reverse_each do |prev|
|
2365
|
+
if (sect = trailing_section_start_pages[prev])
|
2366
|
+
last_sect = sect
|
2367
|
+
break
|
2368
|
+
end
|
2369
|
+
end
|
2018
2370
|
end
|
2371
|
+
parts_by_page[num] = last_part
|
2019
2372
|
chapters_by_page[num] = last_chap
|
2020
2373
|
sections_by_page[num] = last_sect
|
2021
2374
|
end
|
@@ -2026,35 +2379,8 @@ class Converter < ::Prawn::Document
|
|
2026
2379
|
doc.set_attr 'document-title', doctitle.main
|
2027
2380
|
doc.set_attr 'document-subtitle', doctitle.subtitle
|
2028
2381
|
doc.set_attr 'page-count', num_pages
|
2029
|
-
|
2030
|
-
|
2031
|
-
content_dict = PageSides.inject({}) do |acc, side|
|
2032
|
-
side_content = {}
|
2033
|
-
ColumnPositions.each do |position|
|
2034
|
-
if (val = @theme[%(#{periphery}_#{side}_#{position}_content)])
|
2035
|
-
# TODO support image URL (using resolve_image_path)
|
2036
|
-
if (val.include? ':') && val =~ ImageAttributeValueRx &&
|
2037
|
-
::File.readable?(path = (ThemeLoader.resolve_theme_asset $1, (doc.attr 'pdf-stylesdir')))
|
2038
|
-
attrs = (AttributeList.new $2).parse
|
2039
|
-
width = resolve_explicit_width attrs, bounds.width
|
2040
|
-
# QUESTION should we lookup and scale intrinsic width if explicit width is not given?
|
2041
|
-
unless width
|
2042
|
-
width = [bounds.width, (intrinsic_image_dimensions path)[:width] * 0.75].min
|
2043
|
-
end
|
2044
|
-
side_content[position] = { path: path, width: width }
|
2045
|
-
else
|
2046
|
-
side_content[position] = val
|
2047
|
-
end
|
2048
|
-
end
|
2049
|
-
end
|
2050
|
-
# NOTE set fallbacks if not explicitly disabled
|
2051
|
-
if side_content.empty? && periphery == :footer && @theme[%(footer_#{side}_content)] != 'none'
|
2052
|
-
side_content = { side == :recto ? :right : :left => '{page-number}' }
|
2053
|
-
end
|
2054
|
-
|
2055
|
-
acc[side] = side_content
|
2056
|
-
acc
|
2057
|
-
end
|
2382
|
+
allow_uri_read = doc.attr? 'allow-uri-read'
|
2383
|
+
svg_fallback_font = default_svg_font
|
2058
2384
|
|
2059
2385
|
if periphery == :header
|
2060
2386
|
trim_line_metrics = calc_line_metrics(@theme.header_line_height || @theme.base_line_height)
|
@@ -2115,26 +2441,29 @@ class Converter < ::Prawn::Document
|
|
2115
2441
|
|
2116
2442
|
colspec_dict = PageSides.inject({}) do |acc, side|
|
2117
2443
|
side_trim_content_width = trim_content_width[side]
|
2118
|
-
if (custom_colspecs = @theme[%(#{periphery}_#{side}_columns)])
|
2119
|
-
colspecs =
|
2120
|
-
|
2121
|
-
|
2122
|
-
|
2444
|
+
if (custom_colspecs = @theme[%(#{periphery}_#{side}_columns)] || @theme[%(#{periphery}_columns)])
|
2445
|
+
case (colspecs = (custom_colspecs.to_s.tr ',', ' ').split[0..2]).size
|
2446
|
+
when 3
|
2447
|
+
colspecs = { left: colspecs[0], center: colspecs[1], right: colspecs[2] }
|
2448
|
+
when 2
|
2449
|
+
colspecs = { left: colspecs[0], center: '0', right: colspecs[1] }
|
2450
|
+
when 0, 1
|
2451
|
+
colspecs = { left: '0', center: colspecs[0] || '100', right: '0' }
|
2452
|
+
end
|
2453
|
+
tot_width = 0
|
2123
2454
|
side_colspecs = colspecs.map {|col, spec|
|
2124
2455
|
if (alignment_char = spec.chr).to_i.to_s != alignment_char
|
2125
2456
|
alignment = AlignmentTable[alignment_char] || :left
|
2126
|
-
|
2457
|
+
rel_width = spec[1..-1].to_f
|
2127
2458
|
else
|
2128
2459
|
alignment = :left
|
2129
|
-
|
2460
|
+
rel_width = spec.to_f
|
2130
2461
|
end
|
2131
|
-
|
2132
|
-
|
2133
|
-
w = side_trim_content_width - cml_width
|
2134
|
-
end
|
2135
|
-
cml_width += w
|
2136
|
-
[col, { align: alignment, width: w, x: 0 }]
|
2462
|
+
tot_width += rel_width
|
2463
|
+
[col, { align: alignment, width: rel_width, x: 0 }]
|
2137
2464
|
}.to_h
|
2465
|
+
# QUESTION should we allow the columns to overlap (capping width at 100%)?
|
2466
|
+
side_colspecs.each {|_, colspec| colspec[:width] = (colspec[:width] / tot_width) * side_trim_content_width }
|
2138
2467
|
side_colspecs[:right][:x] = (side_colspecs[:center][:x] = side_colspecs[:left][:width]) + side_colspecs[:center][:width]
|
2139
2468
|
acc[side] = side_colspecs
|
2140
2469
|
else
|
@@ -2147,11 +2476,43 @@ class Converter < ::Prawn::Document
|
|
2147
2476
|
acc
|
2148
2477
|
end
|
2149
2478
|
|
2479
|
+
# TODO move this to a method so it can be reused; cache results
|
2480
|
+
content_dict = PageSides.inject({}) do |acc, side|
|
2481
|
+
side_content = {}
|
2482
|
+
ColumnPositions.each do |position|
|
2483
|
+
unless (val = @theme[%(#{periphery}_#{side}_#{position}_content)]).nil_or_empty?
|
2484
|
+
# TODO support image URL (using resolve_image_path)
|
2485
|
+
if (val.include? ':') && val =~ ImageAttributeValueRx &&
|
2486
|
+
::File.readable?(path = (ThemeLoader.resolve_theme_asset $1, (doc.attr 'pdf-stylesdir')))
|
2487
|
+
attrs = (AttributeList.new $2).parse
|
2488
|
+
col_width = colspec_dict[side][position][:width]
|
2489
|
+
if (fit = attrs['fit']) == 'contain'
|
2490
|
+
width = col_width
|
2491
|
+
else
|
2492
|
+
unless (width = resolve_explicit_width attrs, col_width)
|
2493
|
+
# QUESTION should we lookup and scale intrinsic width if explicit width is not given?
|
2494
|
+
# NOTE failure message will be reported later when image is rendered
|
2495
|
+
width = (to_pt intrinsic_image_dimensions(path)[:width], :px) rescue 0
|
2496
|
+
end
|
2497
|
+
width = col_width if fit == 'scale-down' && width > col_width
|
2498
|
+
end
|
2499
|
+
side_content[position] = { path: path, width: width, fit: !!fit }
|
2500
|
+
else
|
2501
|
+
side_content[position] = val
|
2502
|
+
end
|
2503
|
+
end
|
2504
|
+
end
|
2505
|
+
# NOTE set fallbacks if not explicitly disabled
|
2506
|
+
if side_content.empty? && periphery == :footer && @theme[%(footer_#{side}_content)] != 'none'
|
2507
|
+
side_content = { side == :recto ? :right : :left => '{page-number}' }
|
2508
|
+
end
|
2509
|
+
|
2510
|
+
acc[side] = side_content
|
2511
|
+
acc
|
2512
|
+
end
|
2513
|
+
|
2150
2514
|
stamps = {}
|
2151
2515
|
if trim_bg_color || trim_border_color
|
2152
|
-
# NOTE switch to first content page so stamp will get created properly (can't create on imported page)
|
2153
|
-
prev_page_number = page_number
|
2154
|
-
go_to_page start
|
2155
2516
|
PageSides.each do |side|
|
2156
2517
|
create_stamp trim_stamp_name[side] do
|
2157
2518
|
canvas do
|
@@ -2175,12 +2536,11 @@ class Converter < ::Prawn::Document
|
|
2175
2536
|
end
|
2176
2537
|
end
|
2177
2538
|
stamps[periphery] = true
|
2178
|
-
go_to_page prev_page_number
|
2179
2539
|
end
|
2180
2540
|
|
2181
2541
|
pagenums_enabled = doc.attr? 'pagenums'
|
2182
2542
|
attribute_missing_doc = doc.attr 'attribute-missing'
|
2183
|
-
repeat (
|
2543
|
+
repeat (content_start_page..page_count), dynamic: true do
|
2184
2544
|
# NOTE don't write on pages which are imported / inserts (otherwise we can get a corrupt PDF)
|
2185
2545
|
next if page.imported_page?
|
2186
2546
|
pgnum_label = page_number - skip
|
@@ -2191,8 +2551,9 @@ class Converter < ::Prawn::Document
|
|
2191
2551
|
colspec_by_position = colspec_dict[side]
|
2192
2552
|
# TODO populate chapter-number
|
2193
2553
|
# TODO populate numbered and unnumbered chapter and section titles
|
2194
|
-
|
2195
|
-
|
2554
|
+
doc.set_attr 'page-number', pgnum_label.to_s if pagenums_enabled
|
2555
|
+
# QUESTION should the fallback value be nil instead of empty string? or should we remove attribute if no value?
|
2556
|
+
doc.set_attr 'part-title', (parts_by_page[pgnum_label] || '')
|
2196
2557
|
doc.set_attr 'chapter-title', (chapters_by_page[pgnum_label] || '')
|
2197
2558
|
doc.set_attr 'section-title', (sections_by_page[pgnum_label] || '')
|
2198
2559
|
doc.set_attr 'section-or-chapter-title', (sections_by_page[pgnum_label] || chapters_by_page[pgnum_label] || '')
|
@@ -2214,12 +2575,37 @@ class Converter < ::Prawn::Document
|
|
2214
2575
|
float do
|
2215
2576
|
# NOTE bounding_box is redundant if trim_v_padding is 0
|
2216
2577
|
bounding_box [colspec[:x], cursor - trim_padding[0]], width: colspec[:width], height: (bounds.height - trim_v_padding) do
|
2217
|
-
|
2218
|
-
|
2219
|
-
|
2578
|
+
begin
|
2579
|
+
if (img_path = content[:path]).downcase.end_with? '.svg'
|
2580
|
+
svg_data = ::IO.read img_path
|
2581
|
+
svg_obj = ::Prawn::Svg::Interface.new svg_data, self,
|
2582
|
+
position: colspec[:align],
|
2583
|
+
vposition: trim_img_valign,
|
2584
|
+
width: content[:width],
|
2585
|
+
# TODO enforce jail in safe mode
|
2586
|
+
enable_file_requests_with_root: (::File.dirname img_path),
|
2587
|
+
enable_web_requests: allow_uri_read,
|
2588
|
+
fallback_font_name: svg_fallback_font
|
2589
|
+
if content[:fit] && svg_obj.document.sizing.output_height > (available_h = bounds.height)
|
2590
|
+
svg_obj.resize height: available_h
|
2591
|
+
end
|
2592
|
+
svg_obj.draw
|
2593
|
+
else
|
2594
|
+
img_opts = { position: colspec[:align], vposition: trim_img_valign }
|
2595
|
+
if content[:fit]
|
2596
|
+
img_opts[:fit] = [content[:width], bounds.height]
|
2597
|
+
else
|
2598
|
+
img_opts[:width] = content[:width]
|
2599
|
+
end
|
2600
|
+
image img_path, img_opts
|
2601
|
+
end
|
2602
|
+
rescue => e
|
2603
|
+
warn %(asciidoctor: WARNING: could not embed image in running content: #{img_path}; #{e.message})
|
2604
|
+
end
|
2220
2605
|
end
|
2221
2606
|
end
|
2222
2607
|
when ::String
|
2608
|
+
# NOTE minor optimization
|
2223
2609
|
if content == '{page-number}'
|
2224
2610
|
content = pagenums_enabled ? pgnum_label.to_s : nil
|
2225
2611
|
else
|
@@ -2248,19 +2634,23 @@ class Converter < ::Prawn::Document
|
|
2248
2634
|
end
|
2249
2635
|
end
|
2250
2636
|
end
|
2637
|
+
|
2638
|
+
go_to_page prev_page_number
|
2251
2639
|
nil
|
2252
2640
|
end
|
2253
2641
|
|
2254
2642
|
def add_outline doc, num_levels = 2, toc_page_nums = [], num_front_matter_pages = 0
|
2255
2643
|
front_matter_counter = RomanNumeral.new 0, :lower
|
2256
|
-
|
2644
|
+
pagenum_labels = {}
|
2257
2645
|
|
2258
|
-
num_front_matter_pages.times do
|
2259
|
-
|
2646
|
+
num_front_matter_pages.times do |n|
|
2647
|
+
pagenum_labels[n] = { P: (::PDF::Core::LiteralString.new front_matter_counter.next!.to_s) }
|
2260
2648
|
end
|
2261
2649
|
|
2262
|
-
#
|
2263
|
-
|
2650
|
+
# add labels for each content page, which is required for reader's page navigator to work correctly
|
2651
|
+
(num_front_matter_pages..(page_count - 1)).each_with_index do |n, i|
|
2652
|
+
pagenum_labels[n] = { P: (::PDF::Core::LiteralString.new %(#{i + 1})) }
|
2653
|
+
end
|
2264
2654
|
|
2265
2655
|
outline.define do
|
2266
2656
|
# FIXME use sanitize: :plain_text once available
|
@@ -2270,26 +2660,24 @@ class Converter < ::Prawn::Document
|
|
2270
2660
|
end
|
2271
2661
|
page title: (doc.attr 'toc-title'), destination: (document.dest_top toc_page_nums.first) if toc_page_nums.first
|
2272
2662
|
# QUESTION any way to get add_outline_level to invoke in the context of the outline?
|
2273
|
-
document.add_outline_level self, doc.sections, num_levels
|
2663
|
+
document.add_outline_level self, doc.sections, num_levels
|
2274
2664
|
end
|
2275
2665
|
|
2276
|
-
catalog.data[:PageLabels] = state.store.ref Nums:
|
2666
|
+
catalog.data[:PageLabels] = state.store.ref Nums: pagenum_labels.flatten
|
2277
2667
|
catalog.data[:PageMode] = :UseOutlines
|
2278
2668
|
nil
|
2279
2669
|
end
|
2280
2670
|
|
2281
|
-
#
|
2282
|
-
def add_outline_level outline, sections, num_levels
|
2671
|
+
# FIXME only nest inside root node if doctype=article
|
2672
|
+
def add_outline_level outline, sections, num_levels
|
2283
2673
|
sections.each do |sect|
|
2284
2674
|
sect_title = sanitize sect.numbered_title formal: true
|
2285
2675
|
sect_destination = sect.attr 'pdf-destination'
|
2286
|
-
sect_pgnum_label = (sect_pgnum = sect.attr 'pdf-page-start') - num_front_matter_pages
|
2287
|
-
page_num_labels[sect_pgnum - 1] = { P: ::PDF::Core::LiteralString.new(sect_pgnum_label.to_s) }
|
2288
2676
|
if (subsections = sect.sections).empty? || sect.level == num_levels
|
2289
2677
|
outline.page title: sect_title, destination: sect_destination
|
2290
2678
|
elsif sect.level < num_levels + 1
|
2291
2679
|
outline.section sect_title, { destination: sect_destination } do
|
2292
|
-
add_outline_level outline, subsections, num_levels
|
2680
|
+
add_outline_level outline, subsections, num_levels
|
2293
2681
|
end
|
2294
2682
|
end
|
2295
2683
|
end
|
@@ -2303,7 +2691,7 @@ class Converter < ::Prawn::Document
|
|
2303
2691
|
else
|
2304
2692
|
pdf_doc.render_file target
|
2305
2693
|
# QUESTION restore attributes first?
|
2306
|
-
@
|
2694
|
+
@pdfmark.generate_file target if @pdfmark
|
2307
2695
|
end
|
2308
2696
|
# write scratch document if debug is enabled (or perhaps DEBUG_STEPS env)
|
2309
2697
|
#get_scratch_document.render_file 'scratch.pdf'
|
@@ -2311,8 +2699,8 @@ class Converter < ::Prawn::Document
|
|
2311
2699
|
end
|
2312
2700
|
|
2313
2701
|
def register_fonts font_catalog, scripts = 'latin', fonts_dir
|
2314
|
-
(font_catalog || {}).each do |key,
|
2315
|
-
register_font key =>
|
2702
|
+
(font_catalog || {}).each do |key, styles|
|
2703
|
+
register_font key => styles.map {|style, path| [style.to_sym, (font_path path, fonts_dir)]}.to_h
|
2316
2704
|
end
|
2317
2705
|
|
2318
2706
|
# FIXME read kerning setting from theme!
|
@@ -2338,34 +2726,35 @@ class Converter < ::Prawn::Document
|
|
2338
2726
|
end
|
2339
2727
|
end
|
2340
2728
|
|
2341
|
-
def theme_fill_and_stroke_bounds category
|
2342
|
-
|
2343
|
-
|
2344
|
-
|
2729
|
+
def theme_fill_and_stroke_bounds category, opts = {}
|
2730
|
+
background_color = opts[:background_color] || @theme[%(#{category}_background_color)]
|
2731
|
+
fill_and_stroke_bounds background_color, @theme[%(#{category}_border_color)],
|
2732
|
+
line_width: @theme[%(#{category}_border_width)],
|
2733
|
+
radius: @theme[%(#{category}_border_radius)]
|
2345
2734
|
end
|
2346
2735
|
|
2347
2736
|
# Insert a top margin space unless cursor is at the top of the page.
|
2348
|
-
# Start a new page if
|
2349
|
-
def margin_top
|
2350
|
-
margin
|
2737
|
+
# Start a new page if n value is greater than remaining space on page.
|
2738
|
+
def margin_top n
|
2739
|
+
margin n, :top
|
2351
2740
|
end
|
2352
2741
|
|
2353
2742
|
# Insert a bottom margin space unless cursor is at the top of the page (not likely).
|
2354
|
-
# Start a new page if
|
2355
|
-
def margin_bottom
|
2356
|
-
margin
|
2743
|
+
# Start a new page if n value is greater than remaining space on page.
|
2744
|
+
def margin_bottom n
|
2745
|
+
margin n, :bottom
|
2357
2746
|
end
|
2358
2747
|
|
2359
2748
|
# Insert a margin space at the specified side unless cursor is at the top of the page.
|
2360
|
-
# Start a new page if
|
2361
|
-
def margin
|
2362
|
-
unless
|
2363
|
-
|
2364
|
-
|
2749
|
+
# Start a new page if n value is greater than remaining space on page.
|
2750
|
+
def margin n, side
|
2751
|
+
unless n == 0 || at_page_top?
|
2752
|
+
# NOTE use low-level cursor calculation to workaround cursor bug in column_box context
|
2753
|
+
if y - reference_bounds.absolute_bottom > n
|
2754
|
+
move_down n
|
2365
2755
|
else
|
2366
|
-
#
|
2367
|
-
|
2368
|
-
@margin_box.move_past_bottom
|
2756
|
+
# set cursor at top of next page
|
2757
|
+
reference_bounds.move_past_bottom
|
2369
2758
|
end
|
2370
2759
|
end
|
2371
2760
|
end
|
@@ -2379,6 +2768,8 @@ class Converter < ::Prawn::Document
|
|
2379
2768
|
end
|
2380
2769
|
|
2381
2770
|
def theme_font category, opts = {}
|
2771
|
+
result = nil
|
2772
|
+
# TODO inheriting from generic category should be an option
|
2382
2773
|
if (level = opts[:level])
|
2383
2774
|
family = @theme[%(#{category}_h#{level}_font_family)] || @theme[%(#{category}_font_family)] || @theme.base_font_family
|
2384
2775
|
size = @theme[%(#{category}_h#{level}_font_size)] || @theme[%(#{category}_font_size)] || @theme.base_font_size
|
@@ -2400,11 +2791,12 @@ class Converter < ::Prawn::Document
|
|
2400
2791
|
prev_transform, @text_transform = @text_transform, transform if transform
|
2401
2792
|
|
2402
2793
|
font family, size: size, style: (style && style.to_sym) do
|
2403
|
-
yield
|
2794
|
+
result = yield
|
2404
2795
|
end
|
2405
2796
|
|
2406
2797
|
@font_color = prev_color if color
|
2407
2798
|
@text_transform = prev_transform if transform
|
2799
|
+
result
|
2408
2800
|
end
|
2409
2801
|
|
2410
2802
|
# Calculate the font size (down to the minimum font size) that would allow
|
@@ -2414,24 +2806,26 @@ class Converter < ::Prawn::Document
|
|
2414
2806
|
# font size adjustment is necessary.
|
2415
2807
|
def theme_font_size_autofit fragments, category
|
2416
2808
|
arranger = arrange_fragments_by_line fragments
|
2417
|
-
adjusted_font_size = nil
|
2418
2809
|
theme_font category do
|
2419
2810
|
# NOTE finalizing the line here generates fragments & calculates their widths using the current font settings
|
2420
2811
|
# CAUTION it also removes zero-width spaces
|
2421
2812
|
arranger.finalize_line
|
2422
2813
|
actual_width = width_of_fragments arranger.fragments
|
2423
2814
|
unless ::Array === (padding = @theme[%(#{category}_padding)])
|
2424
|
-
padding =
|
2815
|
+
padding = ::Array.new 4, padding
|
2425
2816
|
end
|
2426
2817
|
available_width = bounds.width - (padding[3] || 0) - (padding[1] || 0)
|
2427
2818
|
if actual_width > available_width
|
2428
|
-
adjusted_font_size = ((available_width * font_size).to_f / actual_width).
|
2819
|
+
adjusted_font_size = ((available_width * font_size).to_f / actual_width).truncate_to_precision 4
|
2429
2820
|
if (min = @theme[%(#{category}_font_size_min)] || @theme.base_font_size_min) && adjusted_font_size < min
|
2430
|
-
|
2821
|
+
min
|
2822
|
+
else
|
2823
|
+
adjusted_font_size
|
2431
2824
|
end
|
2825
|
+
else
|
2826
|
+
nil
|
2432
2827
|
end
|
2433
2828
|
end
|
2434
|
-
adjusted_font_size
|
2435
2829
|
end
|
2436
2830
|
|
2437
2831
|
# Arrange fragments by line in an arranger and return an unfinalized arranger.
|
@@ -2558,7 +2952,7 @@ class Converter < ::Prawn::Document
|
|
2558
2952
|
# If value is nil, derive an anchor name from the default_value, if given.
|
2559
2953
|
def derive_anchor_from_id value, default_value = nil
|
2560
2954
|
if value
|
2561
|
-
value.ascii_only? ? value : %(0x#{::PDF::Core.string_to_hex value})
|
2955
|
+
value.ascii_only? ? value : %(0x#{::PDF::Core.string_to_hex value})
|
2562
2956
|
elsif default_value
|
2563
2957
|
%(__anchor-#{default_value})
|
2564
2958
|
end
|
@@ -2570,18 +2964,16 @@ class Converter < ::Prawn::Document
|
|
2570
2964
|
# specified, do nothing.
|
2571
2965
|
#
|
2572
2966
|
# If the node is a section, and the current y position is the top of the
|
2573
|
-
# page, set the position equal to the page height to improve the navigation
|
2967
|
+
# page, set the y position equal to the page height to improve the navigation
|
2968
|
+
# experience. If the current x position is at or inside the left margin, set
|
2969
|
+
# the x position equal to 0 (left edge of page) to improve the navigation
|
2574
2970
|
# experience.
|
2575
2971
|
def add_dest_for_block node, id = nil
|
2576
2972
|
if !scratch? && (id ||= node.id)
|
2577
|
-
|
2578
|
-
|
2973
|
+
dest_x = bounds.absolute_left.truncate_to_precision 4
|
2974
|
+
# QUESTION when content is aligned to left margin, should we keep precise x value or just use 0?
|
2579
2975
|
dest_x = 0 if dest_x <= page_margin_left
|
2580
|
-
dest_y =
|
2581
|
-
page_height
|
2582
|
-
else
|
2583
|
-
y
|
2584
|
-
end
|
2976
|
+
dest_y = at_page_top? && (node.context == :section || node.context == :document) ? page_height : y
|
2585
2977
|
# TODO find a way to store only the ref of the destination; look it up when we need it
|
2586
2978
|
node.set_attr 'pdf-destination', (node_dest = (dest_xyz dest_x, dest_y))
|
2587
2979
|
add_dest id, node_dest
|
@@ -2600,10 +2992,10 @@ class Converter < ::Prawn::Document
|
|
2600
2992
|
|
2601
2993
|
# Resolve the system path of the specified image path.
|
2602
2994
|
#
|
2603
|
-
# Resolve and normalize the absolute system path of the specified image
|
2604
|
-
#
|
2605
|
-
#
|
2606
|
-
#
|
2995
|
+
# Resolve and normalize the absolute system path of the specified image. If
|
2996
|
+
# the image_path argument is not specified, the path is read from the target
|
2997
|
+
# attribute of the specified document node. Resolve the path relative to the
|
2998
|
+
# imagesdir if the relative_to_imagesdir option is specified (default: true).
|
2607
2999
|
#
|
2608
3000
|
# If the target is a URI and the allow-uri-read attribute is set on the
|
2609
3001
|
# document, read the file contents to a temporary file and return the path to
|
@@ -2616,8 +3008,20 @@ class Converter < ::Prawn::Document
|
|
2616
3008
|
imagesdir = relative_to_imagesdir ? (resolve_imagesdir doc) : nil
|
2617
3009
|
image_path ||= node.attr 'target'
|
2618
3010
|
image_format ||= ::Asciidoctor::Image.format image_path, (::Asciidoctor::Image === node ? node : nil)
|
3011
|
+
# NOTE currently used for inline images
|
3012
|
+
if ::Base64 === image_path
|
3013
|
+
tmp_image = ::Tempfile.new ['image-', image_format && %(.#{image_format})]
|
3014
|
+
tmp_image.binmode unless image_format == 'svg'
|
3015
|
+
begin
|
3016
|
+
tmp_image.write(::Base64.decode64 image_path)
|
3017
|
+
tmp_image.path.extend TemporaryPath
|
3018
|
+
rescue
|
3019
|
+
nil
|
3020
|
+
ensure
|
3021
|
+
tmp_image.close
|
3022
|
+
end
|
2619
3023
|
# handle case when image is a URI
|
2620
|
-
|
3024
|
+
elsif (node.is_uri? image_path) || (imagesdir && (node.is_uri? imagesdir) &&
|
2621
3025
|
(image_path = (node.normalize_web_path image_path, imagesdir, false)))
|
2622
3026
|
unless doc.attr? 'allow-uri-read'
|
2623
3027
|
warn %(asciidoctor: WARNING: allow-uri-read is not enabled; cannot embed remote image: #{image_path}) unless scratch?
|
@@ -2631,15 +3035,13 @@ class Converter < ::Prawn::Document
|
|
2631
3035
|
tmp_image = ::Tempfile.new ['image-', image_format && %(.#{image_format})]
|
2632
3036
|
tmp_image.binmode if (binary = image_format != 'svg')
|
2633
3037
|
begin
|
2634
|
-
open(image_path, (binary ? 'rb' : 'r')) {|fd| tmp_image.write
|
2635
|
-
|
2636
|
-
tmp_image_path.extend TemporaryPath
|
3038
|
+
open(image_path, (binary ? 'rb' : 'r')) {|fd| tmp_image.write fd.read }
|
3039
|
+
tmp_image.path.extend TemporaryPath
|
2637
3040
|
rescue
|
2638
|
-
|
3041
|
+
nil
|
2639
3042
|
ensure
|
2640
3043
|
tmp_image.close
|
2641
3044
|
end
|
2642
|
-
tmp_image_path
|
2643
3045
|
# handle case when image is a local file
|
2644
3046
|
else
|
2645
3047
|
::File.expand_path(node.normalize_system_path image_path, imagesdir, nil, target_name: 'image')
|
@@ -2676,6 +3078,22 @@ class Converter < ::Prawn::Document
|
|
2676
3078
|
end
|
2677
3079
|
end
|
2678
3080
|
|
3081
|
+
# Resolves the explicit width as a PDF pt value if the value is specified in
|
3082
|
+
# absolute units, but defers resolving a percentage value until later.
|
3083
|
+
#
|
3084
|
+
# See resolve_explicit_width method for details about which attributes are considered.
|
3085
|
+
def preresolve_explicit_width attrs
|
3086
|
+
if attrs.key? 'pdfwidth'
|
3087
|
+
((width = attrs['pdfwidth']).end_with? '%') ? width : (str_to_pt width)
|
3088
|
+
elsif attrs.key? 'scaledwidth'
|
3089
|
+
# NOTE the parser automatically appends % if value is unitless
|
3090
|
+
((width = attrs['scaledwidth']).end_with? '%') ? width : (str_to_pt width)
|
3091
|
+
elsif attrs.key? 'width'
|
3092
|
+
# QUESTION should we honor percentage width value?
|
3093
|
+
to_pt attrs['width'].to_f, :px
|
3094
|
+
end
|
3095
|
+
end
|
3096
|
+
|
2679
3097
|
# Resolves the explicit width as a PDF pt value, if specified.
|
2680
3098
|
#
|
2681
3099
|
# Resolves the explicit width, first considering the pdfwidth attribute, then
|
@@ -2686,6 +3104,7 @@ class Converter < ::Prawn::Document
|
|
2686
3104
|
#--
|
2687
3105
|
# QUESTION should we enforce positive result?
|
2688
3106
|
def resolve_explicit_width attrs, max_width = bounds.width, opts = {}
|
3107
|
+
# QUESTION should we restrict width to max_width for pdfwidth?
|
2689
3108
|
if attrs.key? 'pdfwidth'
|
2690
3109
|
if (width = attrs['pdfwidth']).end_with? '%'
|
2691
3110
|
(width.to_f / 100) * max_width
|
@@ -2695,7 +3114,12 @@ class Converter < ::Prawn::Document
|
|
2695
3114
|
str_to_pt width
|
2696
3115
|
end
|
2697
3116
|
elsif attrs.key? 'scaledwidth'
|
2698
|
-
|
3117
|
+
# NOTE the parser automatically appends % if value is unitless
|
3118
|
+
if (width = attrs['scaledwidth']).end_with? '%'
|
3119
|
+
(width.to_f / 100) * max_width
|
3120
|
+
else
|
3121
|
+
str_to_pt width
|
3122
|
+
end
|
2699
3123
|
elsif opts[:use_fallback] && (width = @theme.image_width)
|
2700
3124
|
if width.end_with? '%'
|
2701
3125
|
(width.to_f / 100) * max_width
|
@@ -2706,8 +3130,7 @@ class Converter < ::Prawn::Document
|
|
2706
3130
|
end
|
2707
3131
|
elsif attrs.key? 'width'
|
2708
3132
|
# QUESTION should we honor percentage width value?
|
2709
|
-
|
2710
|
-
[max_width, attrs['width'].to_f * 0.75].min
|
3133
|
+
[max_width, (to_pt attrs['width'].to_f, :px)].min
|
2711
3134
|
end
|
2712
3135
|
end
|
2713
3136
|
|
@@ -2721,6 +3144,15 @@ class Converter < ::Prawn::Document
|
|
2721
3144
|
warn %(asciidoctor: WARNING: could not delete temporary image: #{path}; #{e.message})
|
2722
3145
|
end
|
2723
3146
|
|
3147
|
+
# NOTE assume URL is escaped (i.e., contains character references such as &)
|
3148
|
+
def breakable_uri uri
|
3149
|
+
scheme, address = uri.split UriSchemeBoundaryRx, 2
|
3150
|
+
address, scheme = scheme, address unless address
|
3151
|
+
address = address.gsub UriBreakCharsRx, UriBreakCharRepl
|
3152
|
+
address.slice!(-2) if address[-2] == ZeroWidthSpace
|
3153
|
+
%(#{scheme}#{address})
|
3154
|
+
end
|
3155
|
+
|
2724
3156
|
# QUESTION move to prawn/extensions.rb?
|
2725
3157
|
def init_scratch_prototype
|
2726
3158
|
# IMPORTANT don't set font before using Marshal, it causes serialization to fail
|