asciidoctor-pdf 2.0.0.beta.2 → 2.0.0.rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +30 -0
- data/README.adoc +79 -1103
- data/docs/theming-guide.adoc +1 -1
- data/lib/asciidoctor/pdf/converter.rb +2062 -2064
- data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_block.rb +31 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor.rb +1 -0
- data/lib/asciidoctor/pdf/ext/prawn/document/column_box.rb +9 -5
- data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +22 -20
- data/lib/asciidoctor/pdf/version.rb +1 -1
- metadata +3 -2
@@ -156,30 +156,6 @@ module Asciidoctor
|
|
156
156
|
::Asciidoctor::Inline === node ? result : self
|
157
157
|
end
|
158
158
|
|
159
|
-
def traverse node, opts = {}
|
160
|
-
# NOTE: need to reconfigure document to use scratch converter in scratch document
|
161
|
-
if self == (prev_converter = node.document.converter)
|
162
|
-
prev_converter = nil
|
163
|
-
else
|
164
|
-
node.document.instance_variable_set :@converter, self
|
165
|
-
end
|
166
|
-
if node.blocks?
|
167
|
-
node.content
|
168
|
-
elsif node.content_model != :compound && (string = node.content)
|
169
|
-
prose_opts = opts.merge hyphenate: true, margin_bottom: 0
|
170
|
-
if (bottom_gutter = @bottom_gutters[-1][node])
|
171
|
-
prose_opts[:bottom_gutter] = bottom_gutter
|
172
|
-
end
|
173
|
-
ink_prose string, prose_opts
|
174
|
-
end
|
175
|
-
ensure
|
176
|
-
node.document.instance_variable_set :@converter, prev_converter if prev_converter
|
177
|
-
end
|
178
|
-
|
179
|
-
def log severity, message = nil, &block
|
180
|
-
logger.send severity, message, &block unless scratch?
|
181
|
-
end
|
182
|
-
|
183
159
|
def convert_document doc
|
184
160
|
doc.promote_preface_block
|
185
161
|
init_pdf doc
|
@@ -195,15 +171,18 @@ module Asciidoctor
|
|
195
171
|
# NOTE: a new page will already be started (page_number = 2) if the front cover image is a PDF
|
196
172
|
ink_cover_page doc, :front
|
197
173
|
has_front_cover = page_number > marked_page_number
|
198
|
-
has_title_page =
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
174
|
+
if (has_title_page = (title_page_on = doc.doctype == 'book' || (doc.attr? 'title-page')) && (start_title_page doc))
|
175
|
+
# NOTE: the base font must be set before any content is written to the main or scratch document
|
176
|
+
font @theme.base_font_family, size: @root_font_size, style: @theme.base_font_style
|
177
|
+
ink_title_page doc
|
178
|
+
start_new_page
|
179
|
+
else
|
180
|
+
@page_margin_by_side[:cover] = @page_margin_by_side[:recto] if @media == 'prepress' && page_number == 0
|
181
|
+
start_new_page unless page&.empty? # rubocop:disable Lint/SafeNavigationWithEmpty
|
182
|
+
# NOTE: the base font must be set before any content is written to the main or scratch document
|
183
|
+
# this method is called inside ink_title_page if the title page is active
|
184
|
+
font @theme.base_font_family, size: @root_font_size, style: @theme.base_font_style
|
185
|
+
end
|
207
186
|
|
208
187
|
unless title_page_on
|
209
188
|
body_start_page_number = page_number
|
@@ -217,14 +196,14 @@ module Asciidoctor
|
|
217
196
|
indent_section do
|
218
197
|
toc_num_levels = (doc.attr 'toclevels', 2).to_i
|
219
198
|
if (insert_toc = (doc.attr? 'toc') && !((toc_placement = doc.attr 'toc-placement') == 'macro' || toc_placement == 'preamble') && doc.sections?)
|
220
|
-
|
199
|
+
start_toc_page doc, toc_placement if title_page_on
|
221
200
|
add_dest_for_block doc, id: 'toc', y: (at_page_top? ? page_height : nil)
|
222
201
|
@toc_extent = allocate_toc doc, toc_num_levels, cursor, title_page_on
|
223
202
|
else
|
224
203
|
@toc_extent = nil
|
225
204
|
end
|
226
205
|
|
227
|
-
start_new_page if @ppbook && verso_page? && !(((next_block = doc.
|
206
|
+
start_new_page if @ppbook && verso_page? && !(((next_block = doc.first_child)&.context == :preamble ? next_block.first_child : next_block)&.option? 'nonfacing')
|
228
207
|
|
229
208
|
if title_page_on
|
230
209
|
zero_page_offset = has_front_cover ? 1 : 0
|
@@ -383,7 +362,7 @@ module Asciidoctor
|
|
383
362
|
@ppbook = nil
|
384
363
|
end
|
385
364
|
# QUESTION: should ThemeLoader handle registering fonts instead?
|
386
|
-
register_fonts theme.font_catalog, (doc.attr 'pdf-fontsdir', 'GEM_FONTS_DIR'
|
365
|
+
register_fonts theme.font_catalog, ((doc.attr 'pdf-fontsdir')&.sub '{docdir}', (doc.attr 'docdir')) || 'GEM_FONTS_DIR'
|
387
366
|
default_kerning theme.base_font_kerning != 'none'
|
388
367
|
@fallback_fonts = Array theme.font_fallbacks
|
389
368
|
@allow_uri_read = doc.attr? 'allow-uri-read'
|
@@ -405,12 +384,13 @@ module Asciidoctor
|
|
405
384
|
@font_scale = 1
|
406
385
|
@font_color = theme.base_font_color
|
407
386
|
@text_decoration_width = theme.base_text_decoration_width
|
408
|
-
@base_text_align = (
|
387
|
+
@base_text_align = (text_align = doc.attr 'text-align') && (TextAlignmentNames.include? text_align) ? text_align : theme.base_text_align
|
409
388
|
@base_line_height = theme.base_line_height
|
410
389
|
@cjk_line_breaks = doc.attr? 'scripts', 'cjk'
|
411
|
-
if (hyphen_lang = doc.attr 'hyphens')
|
390
|
+
if (hyphen_lang = (doc.attr 'hyphens') ||
|
391
|
+
(((doc.attribute_locked? 'hyphens') || ((doc.instance_variable_get :@attributes_modified).include? 'hyphens')) ? nil : @theme.base_hyphens)) &&
|
412
392
|
((defined? ::Text::Hyphen::VERSION) || !(Helpers.require_library 'text/hyphen', 'text-hyphen', :warn).nil?)
|
413
|
-
hyphen_lang = doc.attr 'lang' if hyphen_lang.empty?
|
393
|
+
hyphen_lang = doc.attr 'lang' if !(::String === hyphen_lang) || hyphen_lang.empty?
|
414
394
|
hyphen_lang = 'en_us' if hyphen_lang.nil_or_empty? || hyphen_lang == 'en'
|
415
395
|
hyphen_lang = (hyphen_lang.tr '-', '_').downcase
|
416
396
|
@hyphenator = ::Text::Hyphen.new language: hyphen_lang
|
@@ -434,75 +414,10 @@ module Asciidoctor
|
|
434
414
|
if (@optimize = doc.attr 'optimize')
|
435
415
|
@optimize = nil unless (defined? ::Asciidoctor::PDF::Optimizer) || !(Helpers.require_library OptimizerRequirePath, 'rghost', :warn).nil? # rubocop:disable Style/SoleNestedConditional
|
436
416
|
end
|
437
|
-
|
417
|
+
allocate_scratch_prototype
|
438
418
|
self
|
439
419
|
end
|
440
420
|
|
441
|
-
def load_theme doc
|
442
|
-
@theme ||= begin # rubocop:disable Naming/MemoizedInstanceVariableName
|
443
|
-
if (theme = doc.options[:pdf_theme])
|
444
|
-
theme = theme.dup
|
445
|
-
@themesdir = ::File.expand_path theme.__dir__ || (doc.attr 'pdf-themesdir') || ::Dir.pwd
|
446
|
-
elsif (theme_name = doc.attr 'pdf-theme')
|
447
|
-
theme = ThemeLoader.load_theme theme_name, (user_themesdir = doc.attr 'pdf-themesdir')
|
448
|
-
@themesdir = theme.__dir__
|
449
|
-
else
|
450
|
-
@themesdir = (theme = ThemeLoader.load_theme).__dir__
|
451
|
-
end
|
452
|
-
prepare_theme theme
|
453
|
-
rescue
|
454
|
-
if user_themesdir
|
455
|
-
message = %(could not locate or load the pdf theme `#{theme_name}' in #{user_themesdir})
|
456
|
-
else
|
457
|
-
message = %(could not locate or load the built-in pdf theme `#{theme_name}')
|
458
|
-
end
|
459
|
-
message += %( because of #{$!.class} #{$!.message})
|
460
|
-
log :error, (message.sub %r/$/, '; reverting to default theme')
|
461
|
-
@themesdir = (theme = ThemeLoader.load_theme).__dir__
|
462
|
-
prepare_theme theme
|
463
|
-
end
|
464
|
-
end
|
465
|
-
|
466
|
-
def prepare_theme theme
|
467
|
-
theme.base_font_color ||= '000000'
|
468
|
-
theme.base_font_size ||= 12
|
469
|
-
theme.base_font_style = theme.base_font_style&.to_sym || :normal
|
470
|
-
theme.page_numbering_start_at ||= 'body'
|
471
|
-
theme.running_content_start_at ||= 'body'
|
472
|
-
theme.heading_margin_page_top ||= 0
|
473
|
-
theme.heading_margin_top ||= 0
|
474
|
-
theme.heading_margin_bottom ||= 0
|
475
|
-
theme.prose_text_indent ||= 0
|
476
|
-
theme.prose_text_indent_inner ||= 0
|
477
|
-
theme.prose_margin_bottom ||= 0
|
478
|
-
theme.block_margin_bottom ||= 0
|
479
|
-
theme.list_indent ||= 0
|
480
|
-
theme.list_item_spacing ||= 0
|
481
|
-
theme.description_list_term_spacing ||= 0
|
482
|
-
theme.description_list_description_indent ||= 0
|
483
|
-
theme.table_border_color ||= (theme.base_border_color || '000000')
|
484
|
-
theme.table_border_width ||= 0.5
|
485
|
-
theme.thematic_break_border_color ||= (theme.base_border_color || '000000')
|
486
|
-
theme.image_border_width ||= 0
|
487
|
-
theme.code_linenum_font_color ||= '999999'
|
488
|
-
theme.callout_list_margin_top_after_code ||= 0
|
489
|
-
theme.role_unresolved_font_color ||= 'FF0000'
|
490
|
-
theme.footnotes_item_spacing ||= 0
|
491
|
-
theme.index_columns ||= 2
|
492
|
-
theme.index_column_gap ||= theme.base_font_size
|
493
|
-
theme.kbd_separator ||= '+'
|
494
|
-
theme.title_page_authors_delimiter ||= ', '
|
495
|
-
theme.title_page_revision_delimiter ||= ', '
|
496
|
-
theme.toc_indent ||= 0
|
497
|
-
theme.toc_hanging_indent ||= 0
|
498
|
-
if ::Array === (quotes = theme.quotes)
|
499
|
-
TypographicQuotes.each_with_index {|char, idx| quotes[idx] ||= char }
|
500
|
-
else
|
501
|
-
theme.quotes = TypographicQuotes
|
502
|
-
end
|
503
|
-
theme
|
504
|
-
end
|
505
|
-
|
506
421
|
def build_pdf_options doc, theme
|
507
422
|
case (page_margin = (doc.attr 'pdf-page-margin') || theme.page_margin)
|
508
423
|
when ::Array
|
@@ -620,35 +535,86 @@ module Asciidoctor
|
|
620
535
|
info
|
621
536
|
end
|
622
537
|
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
538
|
+
def load_theme doc
|
539
|
+
@theme ||= begin # rubocop:disable Naming/MemoizedInstanceVariableName
|
540
|
+
if (theme = doc.options[:pdf_theme])
|
541
|
+
theme = theme.dup
|
542
|
+
@themesdir = ::File.expand_path theme.__dir__ ||
|
543
|
+
(user_themesdir = ((doc.attr 'pdf-themesdir')&.sub '{docdir}', (doc.attr 'docdir')) || ::Dir.pwd)
|
544
|
+
elsif (theme_name = doc.attr 'pdf-theme')
|
545
|
+
theme = ThemeLoader.load_theme theme_name, (user_themesdir = (doc.attr 'pdf-themesdir')&.sub '{docdir}', (doc.attr 'docdir'))
|
546
|
+
@themesdir = theme.__dir__
|
547
|
+
else
|
548
|
+
@themesdir = (theme = ThemeLoader.load_theme).__dir__
|
549
|
+
end
|
550
|
+
prepare_theme theme
|
551
|
+
rescue
|
552
|
+
if user_themesdir
|
553
|
+
message = %(could not locate or load the pdf theme `#{theme_name}' in #{user_themesdir})
|
554
|
+
else
|
555
|
+
message = %(could not locate or load the built-in pdf theme `#{theme_name}')
|
556
|
+
end
|
557
|
+
message += %( because of #{$!.class} #{$!.message})
|
558
|
+
log :error, (message.sub %r/$/, '; reverting to default theme')
|
559
|
+
@themesdir = (theme = ThemeLoader.load_theme).__dir__
|
560
|
+
prepare_theme theme
|
629
561
|
end
|
630
|
-
|
631
|
-
|
632
|
-
|
562
|
+
end
|
563
|
+
|
564
|
+
def prepare_theme theme
|
565
|
+
theme.base_font_color ||= '000000'
|
566
|
+
theme.base_font_size ||= 12
|
567
|
+
theme.base_font_style = theme.base_font_style&.to_sym || :normal
|
568
|
+
theme.page_numbering_start_at ||= 'body'
|
569
|
+
theme.running_content_start_at ||= 'body'
|
570
|
+
theme.heading_margin_page_top ||= 0
|
571
|
+
theme.heading_margin_top ||= 0
|
572
|
+
theme.heading_margin_bottom ||= 0
|
573
|
+
theme.prose_text_indent ||= 0
|
574
|
+
theme.prose_text_indent_inner ||= 0
|
575
|
+
theme.prose_margin_bottom ||= 0
|
576
|
+
theme.block_margin_bottom ||= 0
|
577
|
+
theme.list_indent ||= 0
|
578
|
+
theme.list_item_spacing ||= 0
|
579
|
+
theme.description_list_term_spacing ||= 0
|
580
|
+
theme.description_list_description_indent ||= 0
|
581
|
+
theme.table_border_color ||= (theme.base_border_color || '000000')
|
582
|
+
theme.table_border_width ||= 0.5
|
583
|
+
theme.thematic_break_border_color ||= (theme.base_border_color || '000000')
|
584
|
+
theme.image_border_width ||= 0
|
585
|
+
theme.code_linenum_font_color ||= '999999'
|
586
|
+
theme.callout_list_margin_top_after_code ||= 0
|
587
|
+
theme.role_unresolved_font_color ||= 'FF0000'
|
588
|
+
theme.footnotes_item_spacing ||= 0
|
589
|
+
theme.index_columns ||= 2
|
590
|
+
theme.index_column_gap ||= theme.base_font_size
|
591
|
+
theme.kbd_separator ||= '+'
|
592
|
+
theme.title_page_authors_delimiter ||= ', '
|
593
|
+
theme.title_page_revision_delimiter ||= ', '
|
594
|
+
theme.toc_indent ||= 0
|
595
|
+
theme.toc_hanging_indent ||= 0
|
596
|
+
if ::Array === (quotes = theme.quotes)
|
597
|
+
TypographicQuotes.each_with_index {|char, idx| quotes[idx] ||= char }
|
598
|
+
else
|
599
|
+
theme.quotes = TypographicQuotes
|
633
600
|
end
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
601
|
+
theme
|
602
|
+
end
|
603
|
+
|
604
|
+
def indent_section
|
605
|
+
if (values = @section_indent)
|
606
|
+
indent(values[0], values[1]) { yield }
|
607
|
+
else
|
608
|
+
yield
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
def outdent_section enabled = true
|
613
|
+
if enabled && (values = @section_indent)
|
614
|
+
indent(-values[0], -values[1]) { yield }
|
615
|
+
else
|
616
|
+
yield
|
650
617
|
end
|
651
|
-
page.tare_content_stream if tare
|
652
618
|
end
|
653
619
|
|
654
620
|
def convert_section sect, _opts = {}
|
@@ -657,7 +623,7 @@ module Asciidoctor
|
|
657
623
|
sect.context = :open
|
658
624
|
return convert_abstract sect
|
659
625
|
elsif (index_section = sectname == 'index') && @index.empty?
|
660
|
-
sect.
|
626
|
+
sect.remove
|
661
627
|
return
|
662
628
|
end
|
663
629
|
|
@@ -668,10 +634,10 @@ module Asciidoctor
|
|
668
634
|
title = %(#{title}\n<em class="subtitle">#{subtitle}</em>)
|
669
635
|
end
|
670
636
|
hlevel = sect.level.next
|
671
|
-
|
637
|
+
text_align = (@theme[%(heading_h#{hlevel}_text_align)] || @theme.heading_text_align || @base_text_align).to_sym
|
672
638
|
chapterlike = !(part = sectname == 'part') && (sectname == 'chapter' || (sect.document.doctype == 'book' && sect.level == 1))
|
673
639
|
hidden = sect.option? 'notitle'
|
674
|
-
hopts = { align:
|
640
|
+
hopts = { align: text_align, level: hlevel, part: part, chapterlike: chapterlike, outdent: !(part || chapterlike) }
|
675
641
|
if part
|
676
642
|
unless @theme.heading_part_break_before == 'auto'
|
677
643
|
started_new = true
|
@@ -684,7 +650,7 @@ module Asciidoctor
|
|
684
650
|
start_new_chapter sect
|
685
651
|
end
|
686
652
|
end
|
687
|
-
arrange_heading sect, title, hopts unless hidden || started_new || at_page_top? ||
|
653
|
+
arrange_heading sect, title, hopts unless hidden || started_new || at_page_top? || sect.empty?
|
688
654
|
# QUESTION: should we store pdf-page-start, pdf-anchor & pdf-destination in internal map?
|
689
655
|
sect.set_attr 'pdf-page-start', (start_pgnum = page_number)
|
690
656
|
# QUESTION: should we just assign the section this generated id?
|
@@ -710,58 +676,14 @@ module Asciidoctor
|
|
710
676
|
sect.set_attr 'pdf-page-end', page_number
|
711
677
|
end
|
712
678
|
|
713
|
-
def indent_section
|
714
|
-
if (values = @section_indent)
|
715
|
-
indent(values[0], values[1]) { yield }
|
716
|
-
else
|
717
|
-
yield
|
718
|
-
end
|
719
|
-
end
|
720
|
-
|
721
|
-
def outdent_section enabled = true
|
722
|
-
if enabled && (values = @section_indent)
|
723
|
-
indent(-values[0], -values[1]) { yield }
|
724
|
-
else
|
725
|
-
yield
|
726
|
-
end
|
727
|
-
end
|
728
|
-
|
729
|
-
# QUESTION: if a footnote ref appears in a separate chapter, should the footnote def be duplicated?
|
730
|
-
def ink_footnotes node
|
731
|
-
return if (fns = (doc = node.document).footnotes - @rendered_footnotes).empty?
|
732
|
-
theme_margin :block, :bottom if node.context == :document || node == node.document.blocks[-1]
|
733
|
-
theme_margin :footnotes, :top
|
734
|
-
with_dry_run do |extent|
|
735
|
-
if (single_page_height = extent&.single_page_height) && (delta = cursor - single_page_height - 0.0001) > 0
|
736
|
-
move_down delta
|
737
|
-
end
|
738
|
-
theme_font :footnotes do
|
739
|
-
(title = doc.attr 'footnotes-title') && (ink_caption title, category: :footnotes)
|
740
|
-
item_spacing = @theme.footnotes_item_spacing
|
741
|
-
index_offset = @rendered_footnotes.length
|
742
|
-
sect_xreftext = node.context == :section && (node.xreftext node.document.attr 'xrefstyle')
|
743
|
-
fns.each do |fn|
|
744
|
-
label = (index = fn.index) - index_offset
|
745
|
-
if sect_xreftext
|
746
|
-
fn.singleton_class.send :attr_accessor, :label unless fn.respond_to? :label=
|
747
|
-
fn.label = %(#{label} - #{sect_xreftext})
|
748
|
-
end
|
749
|
-
ink_prose %(<a id="_footnotedef_#{index}">#{DummyText}</a>[<a anchor="_footnoteref_#{index}">#{label}</a>] #{fn.text}), margin_bottom: item_spacing, hyphenate: true
|
750
|
-
end
|
751
|
-
@rendered_footnotes += fns if extent
|
752
|
-
end
|
753
|
-
end
|
754
|
-
nil
|
755
|
-
end
|
756
|
-
|
757
679
|
def convert_floating_title node
|
758
680
|
title = node.title
|
759
681
|
hlevel = node.level.next
|
760
|
-
unless (
|
761
|
-
|
682
|
+
unless (text_align = resolve_text_align_from_role node.roles)
|
683
|
+
text_align = (@theme[%(heading_h#{hlevel}_text_align)] || @theme.heading_text_align || @base_text_align).to_sym
|
762
684
|
end
|
763
|
-
hopts = { align:
|
764
|
-
arrange_heading node, title, hopts unless at_page_top? || node
|
685
|
+
hopts = { align: text_align, level: hlevel, outdent: node.parent.context == :section }
|
686
|
+
arrange_heading node, title, hopts unless at_page_top? || node.last_child?
|
765
687
|
add_dest_for_block node if node.id
|
766
688
|
# QUESTION: should we decouple styles from section titles?
|
767
689
|
theme_font :heading, level: hlevel do
|
@@ -769,26 +691,84 @@ module Asciidoctor
|
|
769
691
|
end
|
770
692
|
end
|
771
693
|
|
772
|
-
def
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
|
784
|
-
|
785
|
-
|
786
|
-
|
787
|
-
|
788
|
-
|
789
|
-
|
790
|
-
|
791
|
-
|
694
|
+
def convert_index_section node
|
695
|
+
space_needed_for_category = @theme.description_list_term_spacing + (2 * (height_of_typeset_text 'A'))
|
696
|
+
pagenum_sequence_style = node.document.attr 'index-pagenum-sequence-style'
|
697
|
+
column_box [0, cursor], columns: @theme.index_columns, width: bounds.width, reflow_margins: true, spacer: @theme.index_column_gap do
|
698
|
+
@index.categories.each do |category|
|
699
|
+
bounds.move_past_bottom if space_needed_for_category > cursor
|
700
|
+
ink_prose category.name,
|
701
|
+
align: :left,
|
702
|
+
inline_format: false,
|
703
|
+
margin_bottom: @theme.description_list_term_spacing,
|
704
|
+
style: @theme.description_list_term_font_style&.to_sym
|
705
|
+
category.terms.each {|term| convert_index_list_item term, pagenum_sequence_style }
|
706
|
+
@theme.prose_margin_bottom > cursor ? bounds.move_past_bottom : (move_down @theme.prose_margin_bottom)
|
707
|
+
end
|
708
|
+
end
|
709
|
+
nil
|
710
|
+
end
|
711
|
+
|
712
|
+
def convert_index_list_item term, pagenum_sequence_style = nil
|
713
|
+
text = escape_xml term.name
|
714
|
+
unless term.container?
|
715
|
+
if @media == 'screen'
|
716
|
+
case pagenum_sequence_style
|
717
|
+
when 'page'
|
718
|
+
pagenums = term.dests.uniq {|dest| dest[:page] }.map {|dest| %(<a anchor="#{dest[:anchor]}">#{dest[:page]}</a>) }
|
719
|
+
when 'range'
|
720
|
+
first_anchor_per_page = term.dests.each_with_object({}) {|dest, accum| accum[dest[:page]] ||= dest[:anchor] }
|
721
|
+
pagenums = (consolidate_ranges first_anchor_per_page.keys).map do |range|
|
722
|
+
anchor = first_anchor_per_page[(range.include? '-') ? (range.partition '-')[0] : range]
|
723
|
+
%(<a anchor="#{anchor}">#{range}</a>)
|
724
|
+
end
|
725
|
+
else # term
|
726
|
+
pagenums = term.dests.map {|dest| %(<a anchor="#{dest[:anchor]}">#{dest[:page]}</a>) }
|
727
|
+
end
|
728
|
+
else
|
729
|
+
pagenums = consolidate_ranges term.dests.map {|dest| dest[:page] }.uniq
|
730
|
+
end
|
731
|
+
text = %(#{text}, #{pagenums.join ', '})
|
732
|
+
end
|
733
|
+
subterm_indent = @theme.description_list_description_indent
|
734
|
+
ink_prose text, align: :left, margin: 0, hanging_indent: subterm_indent * 2
|
735
|
+
indent subterm_indent do
|
736
|
+
term.subterms.each do |subterm|
|
737
|
+
convert_index_list_item subterm, pagenum_sequence_style
|
738
|
+
end
|
739
|
+
end unless term.leaf?
|
740
|
+
end
|
741
|
+
|
742
|
+
def convert_preamble node
|
743
|
+
# FIXME: core should not be promoting paragraph to preamble if there are no sections
|
744
|
+
if (first_block = node.first_child)&.context == :paragraph && node.document.sections? && !first_block.role?
|
745
|
+
first_block.role = 'lead'
|
746
|
+
end
|
747
|
+
traverse node
|
748
|
+
theme_margin :block, :bottom, (next_enclosed_block node)
|
749
|
+
convert_toc node, placement: 'preamble'
|
750
|
+
end
|
751
|
+
|
752
|
+
def convert_abstract node
|
753
|
+
add_dest_for_block node if node.id
|
754
|
+
outdent_section do
|
755
|
+
pad_box @theme.abstract_padding do
|
756
|
+
theme_font :abstract_title do
|
757
|
+
ink_prose node.title, align: (@theme.abstract_title_text_align || @base_text_align).to_sym, margin_top: @theme.heading_margin_top, margin_bottom: @theme.heading_margin_bottom, line_height: (@theme.heading_line_height || @theme.base_line_height)
|
758
|
+
end if node.title?
|
759
|
+
theme_font :abstract do
|
760
|
+
prose_opts = { align: (@theme.abstract_text_align || @base_text_align).to_sym, hyphenate: true }
|
761
|
+
if (text_indent = @theme.prose_text_indent) > 0
|
762
|
+
prose_opts[:indent_paragraphs] = text_indent
|
763
|
+
end
|
764
|
+
# FIXME: allow theme to control more first line options
|
765
|
+
if (line1_font_style = @theme.abstract_first_line_font_style&.to_sym) && line1_font_style != font_style
|
766
|
+
case line1_font_style
|
767
|
+
when :normal
|
768
|
+
first_line_options = { styles: [] }
|
769
|
+
when :normal_italic
|
770
|
+
first_line_options = { styles: [:italic] }
|
771
|
+
else
|
792
772
|
first_line_options = { styles: [font_style, line1_font_style] }
|
793
773
|
end
|
794
774
|
end
|
@@ -801,12 +781,12 @@ module Asciidoctor
|
|
801
781
|
prose_opts[:first_line_options] = first_line_options if first_line_options
|
802
782
|
# FIXME: make this cleaner!!
|
803
783
|
if node.blocks?
|
804
|
-
last_block =
|
805
|
-
blocks.each do |child|
|
784
|
+
last_block = node.last_child
|
785
|
+
node.blocks.each do |child|
|
806
786
|
if child.context == :paragraph
|
807
787
|
child.document.playback_attributes child.attributes
|
808
788
|
prose_opts[:margin_bottom] = 0 if child == last_block
|
809
|
-
ink_prose child.content, ((
|
789
|
+
ink_prose child.content, ((text_align = resolve_text_align_from_role child.roles) ? (prose_opts.merge align: text_align) : prose_opts.dup)
|
810
790
|
prose_opts.delete :first_line_options
|
811
791
|
prose_opts.delete :margin_bottom
|
812
792
|
else
|
@@ -815,8 +795,8 @@ module Asciidoctor
|
|
815
795
|
end
|
816
796
|
end
|
817
797
|
elsif node.content_model != :compound && (string = node.content)
|
818
|
-
if (
|
819
|
-
prose_opts[:align] =
|
798
|
+
if (text_align = resolve_text_align_from_role node.roles)
|
799
|
+
prose_opts[:align] = text_align
|
820
800
|
end
|
821
801
|
ink_prose string, (prose_opts.merge margin_bottom: 0)
|
822
802
|
end
|
@@ -827,27 +807,16 @@ module Asciidoctor
|
|
827
807
|
theme_margin :block, :bottom, (next_enclosed_block node)
|
828
808
|
end
|
829
809
|
|
830
|
-
def convert_preamble node
|
831
|
-
# FIXME: core should not be promoting paragraph to preamble if there are no sections
|
832
|
-
if node.blocks? && (first_block = node.blocks[0]).context == :paragraph && node.document.sections? && !first_block.role?
|
833
|
-
first_block.role = 'lead'
|
834
|
-
end
|
835
|
-
traverse node
|
836
|
-
theme_margin :block, :bottom, (next_enclosed_block node)
|
837
|
-
convert_toc node, placement: 'preamble'
|
838
|
-
end
|
839
|
-
|
840
810
|
def convert_paragraph node
|
841
811
|
add_dest_for_block node if node.id
|
842
812
|
|
843
813
|
prose_opts = { margin_bottom: 0, hyphenate: true }
|
844
|
-
if (
|
845
|
-
prose_opts[:align] =
|
814
|
+
if (text_align = resolve_text_align_from_role (roles = node.roles), query_theme: true, remove_predefined: true)
|
815
|
+
prose_opts[:align] = text_align
|
846
816
|
end
|
847
817
|
role_keys = roles.map {|role| %(role_#{role}).to_sym } unless roles.empty?
|
848
818
|
if (text_indent = @theme.prose_text_indent) > 0 ||
|
849
|
-
((text_indent = @theme.prose_text_indent_inner) > 0 &&
|
850
|
-
(self_idx = (siblings = node.parent.blocks).index node) > 0 && siblings[self_idx - 1].context == :paragraph)
|
819
|
+
((text_indent = @theme.prose_text_indent_inner) > 0 && node.previous_sibling&.context == :paragraph)
|
851
820
|
prose_opts[:indent_paragraphs] = text_indent
|
852
821
|
end
|
853
822
|
if (bottom_gutter = @bottom_gutters[-1][node])
|
@@ -877,7 +846,7 @@ module Asciidoctor
|
|
877
846
|
|
878
847
|
def convert_admonition node
|
879
848
|
type = node.attr 'name'
|
880
|
-
|
849
|
+
label_text_align = @theme.admonition_label_text_align&.to_sym || :center
|
881
850
|
# TODO: allow vertical_align to be a number
|
882
851
|
if (label_valign = @theme.admonition_label_vertical_align&.to_sym || :middle) == :middle
|
883
852
|
label_valign = :center
|
@@ -893,14 +862,14 @@ module Asciidoctor
|
|
893
862
|
icon_size = icon_data[:size] || 24
|
894
863
|
label_width = label_min_width || (icon_size * 1.5)
|
895
864
|
elsif (icon_path = has_icon || !(icon_path = (@theme[%(admonition_icon_#{type})] || {})[:image]) ?
|
896
|
-
(
|
865
|
+
(get_icon_image_path node, type) :
|
897
866
|
(ThemeLoader.resolve_theme_asset (apply_subs_discretely doc, icon_path, subs: [:attributes]), @themesdir)) &&
|
898
867
|
(::File.readable? icon_path)
|
899
868
|
icons = true
|
900
869
|
# TODO: introduce @theme.admonition_image_width? or use size key from admonition_icon_<name>?
|
901
870
|
label_width = label_min_width || 36.0
|
902
871
|
else
|
903
|
-
log :warn, %(admonition icon image#{has_icon ? '' : ' for ' + type.upcase} not found or not readable: #{icon_path || (
|
872
|
+
log :warn, %(admonition icon image#{has_icon ? '' : ' for ' + type.upcase} not found or not readable: #{icon_path || (get_icon_image_path node, type, false)})
|
904
873
|
end
|
905
874
|
end
|
906
875
|
unless icons
|
@@ -927,7 +896,7 @@ module Asciidoctor
|
|
927
896
|
advance_page unless first_page
|
928
897
|
rule_segment_height = start_cursor = cursor
|
929
898
|
rule_segment_height -= last_page.cursor if last_page
|
930
|
-
bounding_box [
|
899
|
+
bounding_box [bounds.left, start_cursor], width: label_width + lpad[1], height: rule_segment_height do
|
931
900
|
stroke_vertical_rule rule_color, at: bounds.right, line_style: rule_style, line_width: rule_width
|
932
901
|
end
|
933
902
|
end
|
@@ -935,7 +904,7 @@ module Asciidoctor
|
|
935
904
|
end
|
936
905
|
float do
|
937
906
|
adjusted_font_size = nil
|
938
|
-
bounding_box [
|
907
|
+
bounding_box [bounds.left, cursor], width: label_width, height: label_height do
|
939
908
|
if icons == 'font'
|
940
909
|
# FIXME: we assume icon is square
|
941
910
|
icon_size = fit_icon_to_bounds icon_size
|
@@ -948,14 +917,14 @@ module Asciidoctor
|
|
948
917
|
end
|
949
918
|
icon icon_data[:name],
|
950
919
|
valign: label_valign,
|
951
|
-
align:
|
920
|
+
align: label_text_align,
|
952
921
|
color: (icon_data[:stroke_color] || font_color),
|
953
922
|
size: icon_size
|
954
923
|
elsif icons
|
955
924
|
if (::Asciidoctor::Image.format icon_path) == 'svg'
|
956
925
|
begin
|
957
926
|
svg_obj = ::Prawn::SVG::Interface.new (::File.read icon_path, mode: 'r:UTF-8'), self,
|
958
|
-
position:
|
927
|
+
position: label_text_align,
|
959
928
|
vposition: label_valign,
|
960
929
|
width: label_width,
|
961
930
|
height: label_height,
|
@@ -981,7 +950,7 @@ module Asciidoctor
|
|
981
950
|
if (icon_height = icon_width * (1 / icon_aspect_ratio)) > label_height
|
982
951
|
icon_width *= label_height / icon_height
|
983
952
|
end
|
984
|
-
embed_image image_obj, image_info, width: icon_width, position:
|
953
|
+
embed_image image_obj, image_info, width: icon_width, position: label_text_align, vposition: label_valign
|
985
954
|
rescue
|
986
955
|
log :warn, %(could not embed admonition icon image: #{icon_path}; #{$!.message})
|
987
956
|
icons = nil
|
@@ -1012,7 +981,7 @@ module Asciidoctor
|
|
1012
981
|
end
|
1013
982
|
@text_transform = nil # already applied to label
|
1014
983
|
ink_prose label_text,
|
1015
|
-
align:
|
984
|
+
align: label_text_align,
|
1016
985
|
valign: label_valign,
|
1017
986
|
line_height: 1,
|
1018
987
|
margin: 0,
|
@@ -1035,12 +1004,167 @@ module Asciidoctor
|
|
1035
1004
|
theme_margin :block, :bottom, (next_enclosed_block node)
|
1036
1005
|
end
|
1037
1006
|
|
1007
|
+
# QUESTION: can we avoid arranging fragments multiple times (conums & autofit) by eagerly preparing arranger?
|
1008
|
+
def convert_code node
|
1009
|
+
extensions = []
|
1010
|
+
source_chunks = bg_color_override = font_color_override = adjusted_font_size = nil
|
1011
|
+
theme_font :code do
|
1012
|
+
# HACK: disable built-in syntax highlighter; must be done before calling node.content!
|
1013
|
+
if node.style == 'source' && (highlighter = (syntax_hl = node.document.syntax_highlighter)&.highlight? && syntax_hl.name)
|
1014
|
+
case highlighter
|
1015
|
+
when 'coderay'
|
1016
|
+
Helpers.require_library CodeRayRequirePath, 'coderay' unless defined? ::Asciidoctor::Prawn::CodeRayEncoder
|
1017
|
+
when 'pygments'
|
1018
|
+
Helpers.require_library PygmentsRequirePath, 'pygments.rb' unless defined? ::Pygments::Ext::BlockStyles
|
1019
|
+
when 'rouge'
|
1020
|
+
Helpers.require_library RougeRequirePath, 'rouge' unless defined? ::Rouge::Formatters::Prawn
|
1021
|
+
else
|
1022
|
+
highlighter = nil
|
1023
|
+
end
|
1024
|
+
prev_subs = (subs = node.subs).dup
|
1025
|
+
callouts_enabled = subs.include? :callouts
|
1026
|
+
highlight_idx = subs.index :highlight
|
1027
|
+
# NOTE: scratch? here only applies if listing block is nested inside another block
|
1028
|
+
if !highlighter || scratch?
|
1029
|
+
highlighter = nil
|
1030
|
+
if highlight_idx
|
1031
|
+
# switch the :highlight sub back to :specialcharacters
|
1032
|
+
subs[highlight_idx] = :specialcharacters
|
1033
|
+
else
|
1034
|
+
prev_subs = nil
|
1035
|
+
end
|
1036
|
+
source_string = guard_indentation node.content
|
1037
|
+
elsif highlight_idx
|
1038
|
+
# NOTE: the source highlighter logic below handles the highlight and callouts subs
|
1039
|
+
subs.replace subs - [:highlight, :callouts]
|
1040
|
+
# NOTE: indentation guards will be added by the source highlighter logic
|
1041
|
+
source_string = expand_tabs node.content
|
1042
|
+
else
|
1043
|
+
highlighter = prev_subs = nil
|
1044
|
+
source_string = guard_indentation node.content
|
1045
|
+
end
|
1046
|
+
else
|
1047
|
+
highlighter = nil
|
1048
|
+
source_string = guard_indentation node.content
|
1049
|
+
end
|
1050
|
+
|
1051
|
+
case highlighter
|
1052
|
+
when 'coderay'
|
1053
|
+
source_string, conum_mapping = extract_conums source_string if callouts_enabled
|
1054
|
+
srclang = node.attr 'language', 'text'
|
1055
|
+
begin
|
1056
|
+
::CodeRay::Scanners[(srclang = (srclang.start_with? 'html+') ? (srclang.slice 5, srclang.length).to_sym : srclang.to_sym)]
|
1057
|
+
rescue ::ArgumentError
|
1058
|
+
srclang = :text
|
1059
|
+
end
|
1060
|
+
fragments = (::CodeRay.scan source_string, srclang).to_prawn
|
1061
|
+
source_chunks = conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
|
1062
|
+
when 'pygments'
|
1063
|
+
unless (style = (node.document.attr 'pygments-style')) && (::Pygments::Ext::BlockStyles.available? style)
|
1064
|
+
style = 'pastie'
|
1065
|
+
end
|
1066
|
+
# QUESTION: allow border color to be set by theme for highlighted block?
|
1067
|
+
pg_block_styles = ::Pygments::Ext::BlockStyles.for style
|
1068
|
+
bg_color_override = pg_block_styles[:background_color]
|
1069
|
+
font_color_override = pg_block_styles[:font_color]
|
1070
|
+
if source_string.empty?
|
1071
|
+
source_chunks = []
|
1072
|
+
else
|
1073
|
+
lexer = (::Pygments::Lexer.find_by_alias node.attr 'language', 'text') || (::Pygments::Lexer.find_by_mimetype 'text/plain')
|
1074
|
+
lexer_opts = { nowrap: true, noclasses: true, stripnl: false, style: style }
|
1075
|
+
lexer_opts[:startinline] = !(node.option? 'mixed') if lexer.name == 'PHP'
|
1076
|
+
source_string, conum_mapping = extract_conums source_string if callouts_enabled
|
1077
|
+
# NOTE: highlight can return nil if something goes wrong; fallback to encoded source string if this happens
|
1078
|
+
result = (lexer.highlight source_string, options: lexer_opts) || (node.apply_subs source_string, [:specialcharacters])
|
1079
|
+
if node.attr? 'highlight'
|
1080
|
+
if (highlight_lines = node.resolve_lines_to_highlight source_string, (node.attr 'highlight')).empty?
|
1081
|
+
highlight_lines = nil
|
1082
|
+
else
|
1083
|
+
pg_highlight_bg_color = pg_block_styles[:highlight_background_color]
|
1084
|
+
highlight_lines = highlight_lines.map {|linenum| [linenum, pg_highlight_bg_color] }.to_h
|
1085
|
+
end
|
1086
|
+
end
|
1087
|
+
if (node.option? 'linenums') || (node.attr? 'linenums')
|
1088
|
+
linenums = (node.attr 'start', 1).to_i
|
1089
|
+
postprocess = true
|
1090
|
+
extensions << FormattedText::SourceWrap
|
1091
|
+
elsif conum_mapping || highlight_lines
|
1092
|
+
postprocess = true
|
1093
|
+
end
|
1094
|
+
fragments = text_formatter.format result
|
1095
|
+
fragments = restore_conums fragments, conum_mapping, linenums, highlight_lines if postprocess
|
1096
|
+
source_chunks = guard_indentation_in_fragments fragments
|
1097
|
+
end
|
1098
|
+
when 'rouge'
|
1099
|
+
formatter = (@rouge_formatter ||= ::Rouge::Formatters::Prawn.new theme: (node.document.attr 'rouge-style'), line_gap: @theme.code_line_gap, highlight_background_color: @theme.code_highlight_background_color)
|
1100
|
+
# QUESTION: allow border color to be set by theme for highlighted block?
|
1101
|
+
bg_color_override = formatter.background_color
|
1102
|
+
if source_string.empty?
|
1103
|
+
source_chunks = []
|
1104
|
+
else
|
1105
|
+
if (node.option? 'linenums') || (node.attr? 'linenums')
|
1106
|
+
formatter_opts = { line_numbers: true, start_line: (node.attr 'start', 1).to_i }
|
1107
|
+
extensions << FormattedText::SourceWrap
|
1108
|
+
else
|
1109
|
+
formatter_opts = {}
|
1110
|
+
end
|
1111
|
+
if (srclang = node.attr 'language')
|
1112
|
+
if srclang.include? '?'
|
1113
|
+
if (lexer = ::Rouge::Lexer.find_fancy srclang) && lexer.tag == 'php' && !(node.option? 'mixed') && !((lexer_opts = lexer.options).key? 'start_inline')
|
1114
|
+
lexer = lexer.class.new lexer_opts.merge 'start_inline' => true
|
1115
|
+
end
|
1116
|
+
elsif (lexer = ::Rouge::Lexer.find srclang)
|
1117
|
+
lexer = lexer.new start_inline: true if lexer.tag == 'php' && !(node.option? 'mixed')
|
1118
|
+
end
|
1119
|
+
end
|
1120
|
+
lexer ||= ::Rouge::Lexers::PlainText
|
1121
|
+
source_string, conum_mapping = extract_conums source_string if callouts_enabled
|
1122
|
+
if (node.attr? 'highlight') && !(hl_lines = (node.resolve_lines_to_highlight source_string, (node.attr 'highlight'))).empty?
|
1123
|
+
formatter_opts[:highlight_lines] = hl_lines.map {|linenum| [linenum, true] }.to_h
|
1124
|
+
end
|
1125
|
+
fragments = formatter.format (lexer.lex source_string), formatter_opts rescue [text: source_string]
|
1126
|
+
source_chunks = conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
|
1127
|
+
end
|
1128
|
+
else
|
1129
|
+
# NOTE: only format if we detect a need (callouts or inline formatting)
|
1130
|
+
source_chunks = (XMLMarkupRx.match? source_string) ? (text_formatter.format source_string) : [text: source_string]
|
1131
|
+
end
|
1132
|
+
node.subs.replace prev_subs if prev_subs
|
1133
|
+
adjusted_font_size = ((node.option? 'autofit') || (node.document.attr? 'autofit-option')) ? (compute_autofit_font_size source_chunks, :code) : nil
|
1134
|
+
end
|
1135
|
+
|
1136
|
+
caption_below = @theme.code_caption_end&.to_sym == :bottom
|
1137
|
+
arrange_block node do |extent|
|
1138
|
+
add_dest_for_block node if node.id
|
1139
|
+
tare_first_page_content_stream do
|
1140
|
+
theme_fill_and_stroke_block :code, extent, background_color: bg_color_override, caption_node: caption_below ? nil : node
|
1141
|
+
end
|
1142
|
+
pad_box @theme.code_padding, node do
|
1143
|
+
theme_font :code do
|
1144
|
+
typeset_formatted_text source_chunks, (calc_line_metrics @base_line_height),
|
1145
|
+
color: (font_color_override || @theme.code_font_color || @font_color),
|
1146
|
+
size: adjusted_font_size,
|
1147
|
+
bottom_gutter: @bottom_gutters[-1][node],
|
1148
|
+
extensions: extensions.empty? ? nil : extensions
|
1149
|
+
end
|
1150
|
+
end
|
1151
|
+
end
|
1152
|
+
# TODO: add protection against the bottom caption being widowed
|
1153
|
+
ink_caption node, category: :code, end: :bottom if caption_below
|
1154
|
+
theme_margin :block, :bottom, (next_enclosed_block node)
|
1155
|
+
end
|
1156
|
+
|
1157
|
+
alias convert_listing convert_code
|
1158
|
+
alias convert_literal convert_code
|
1159
|
+
alias convert_listing_or_literal convert_code
|
1160
|
+
|
1038
1161
|
def convert_example node
|
1039
1162
|
return convert_open node if node.option? 'collapsible'
|
1163
|
+
caption_bottom = @theme.example_caption_end&.to_sym == :bottom
|
1040
1164
|
arrange_block node do |extent|
|
1041
|
-
add_dest_for_block node if node.id
|
1165
|
+
add_dest_for_block node if node.id
|
1042
1166
|
tare_first_page_content_stream do
|
1043
|
-
theme_fill_and_stroke_block :example, extent, caption_node: node
|
1167
|
+
theme_fill_and_stroke_block :example, extent, caption_node: caption_bottom ? nil : node
|
1044
1168
|
end
|
1045
1169
|
pad_box @theme.example_padding, node do
|
1046
1170
|
theme_font :example do
|
@@ -1048,6 +1172,8 @@ module Asciidoctor
|
|
1048
1172
|
end
|
1049
1173
|
end
|
1050
1174
|
end
|
1175
|
+
# TODO: add protection against the bottom caption being widowed
|
1176
|
+
ink_caption node, category: :example, end: :bottom if caption_bottom
|
1051
1177
|
theme_margin :block, :bottom, (next_enclosed_block node)
|
1052
1178
|
end
|
1053
1179
|
|
@@ -1097,7 +1223,7 @@ module Asciidoctor
|
|
1097
1223
|
advance_page unless first_page
|
1098
1224
|
b_height = start_cursor = cursor
|
1099
1225
|
b_height -= last_page.cursor if last_page
|
1100
|
-
bounding_box [
|
1226
|
+
bounding_box [bounds.left, start_cursor], width: bounds.width, height: b_height do
|
1101
1227
|
stroke_vertical_rule b_color, line_width: b_left_width, at: b_left_width * 0.5
|
1102
1228
|
end
|
1103
1229
|
end
|
@@ -1118,7 +1244,7 @@ module Asciidoctor
|
|
1118
1244
|
end
|
1119
1245
|
end
|
1120
1246
|
if attribution
|
1121
|
-
|
1247
|
+
theme_margin :block, :bottom
|
1122
1248
|
theme_font %(#{category}_cite) do
|
1123
1249
|
attribution_parts = citetitle ? [attribution, citetitle] : [attribution]
|
1124
1250
|
ink_prose %(#{EmDash} #{attribution_parts.join ', '}), align: :left, normalize: false, margin_bottom: 0
|
@@ -1152,7 +1278,7 @@ module Asciidoctor
|
|
1152
1278
|
end
|
1153
1279
|
|
1154
1280
|
def convert_colist node
|
1155
|
-
|
1281
|
+
if !at_page_top? && ((prev_context = node.previous_sibling&.context) == :listing || prev_context == :literal)
|
1156
1282
|
margin_top @theme.callout_list_margin_top_after_code
|
1157
1283
|
end
|
1158
1284
|
add_dest_for_block node if node.id
|
@@ -1160,8 +1286,8 @@ module Asciidoctor
|
|
1160
1286
|
last_item = node.items[-1]
|
1161
1287
|
item_spacing = @theme.callout_list_item_spacing || @theme.list_item_spacing
|
1162
1288
|
item_opts = { margin_bottom: item_spacing, normalize_line_height: true }
|
1163
|
-
if (
|
1164
|
-
item_opts[:align] =
|
1289
|
+
if (item_text_align = (resolve_text_align_from_role node.roles) || @theme.list_text_align&.to_sym)
|
1290
|
+
item_opts[:align] = item_text_align
|
1165
1291
|
end
|
1166
1292
|
theme_font :callout_list do
|
1167
1293
|
line_metrics = theme_font(:conum) { calc_line_metrics @base_line_height }
|
@@ -1181,7 +1307,7 @@ module Asciidoctor
|
|
1181
1307
|
theme_font :conum do
|
1182
1308
|
marker_width = rendered_width_of_string %(#{marker = conum_glyph index}x)
|
1183
1309
|
float do
|
1184
|
-
bounding_box [
|
1310
|
+
bounding_box [bounds.left, cursor], width: marker_width do
|
1185
1311
|
ink_prose marker, align: :center, inline_format: false, margin: 0
|
1186
1312
|
end
|
1187
1313
|
end
|
@@ -1379,13 +1505,13 @@ module Asciidoctor
|
|
1379
1505
|
ink_caption node, category: :list, labeled: false if node.title?
|
1380
1506
|
|
1381
1507
|
opts = {}
|
1382
|
-
if (
|
1383
|
-
opts[:align] =
|
1508
|
+
if (text_align = resolve_text_align_from_role node.roles)
|
1509
|
+
opts[:align] = text_align
|
1384
1510
|
elsif node.style == 'bibliography'
|
1385
1511
|
opts[:align] = :left
|
1386
|
-
elsif (
|
1512
|
+
elsif (text_align = @theme.list_text_align&.to_sym) # rubocop:disable Lint/DuplicateBranch
|
1387
1513
|
# NOTE: theme setting only affects alignment of list text (not nested blocks)
|
1388
|
-
opts[:align] =
|
1514
|
+
opts[:align] = text_align
|
1389
1515
|
end
|
1390
1516
|
|
1391
1517
|
line_metrics = calc_line_metrics @base_line_height
|
@@ -1469,7 +1595,7 @@ module Asciidoctor
|
|
1469
1595
|
character_spacing_correction = 0.5 if (rendered_width_of_char 'x', character_spacing: -0.5) == marker_gap
|
1470
1596
|
end
|
1471
1597
|
marker_height = height_of_typeset_text marker, line_height: marker_style[:line_height], single_line: true
|
1472
|
-
start_position = -marker_width
|
1598
|
+
start_position = bounds.left - marker_width - marker_gap + character_spacing_correction
|
1473
1599
|
float do
|
1474
1600
|
advance_page if @media == 'prepress' && cursor < marker_height
|
1475
1601
|
flow_bounding_box position: start_position, width: marker_width do
|
@@ -1498,30 +1624,18 @@ module Asciidoctor
|
|
1498
1624
|
traverse_list_item node, list_type, opts
|
1499
1625
|
end
|
1500
1626
|
|
1501
|
-
def
|
1502
|
-
|
1503
|
-
|
1504
|
-
|
1505
|
-
if
|
1506
|
-
|
1507
|
-
|
1508
|
-
|
1509
|
-
|
1510
|
-
|
1511
|
-
ink_prose DummyText, opts unless node.blocks?
|
1512
|
-
else
|
1513
|
-
ink_prose primary_text, (opts.merge hyphenate: true)
|
1627
|
+
def convert_image node, opts = {}
|
1628
|
+
target, image_format = (node.extend ::Asciidoctor::Image).target_and_format
|
1629
|
+
|
1630
|
+
unless image_format == 'pdf'
|
1631
|
+
if (float_to = node.attr 'float') && ((BlockFloatNames.include? float_to) ? float_to : (float_to = nil))
|
1632
|
+
alignment = float_to.to_sym
|
1633
|
+
elsif (alignment = node.attr 'align')
|
1634
|
+
alignment = (BlockAlignmentNames.include? alignment) ? alignment.to_sym : :left
|
1635
|
+
elsif !(alignment = node.roles.reverse.find {|r| BlockAlignmentNames.include? r }&.to_sym)
|
1636
|
+
alignment = @theme.image_align&.to_sym || :left
|
1514
1637
|
end
|
1515
|
-
traverse node
|
1516
1638
|
end
|
1517
|
-
end
|
1518
|
-
|
1519
|
-
def allocate_space_for_list_item line_metrics
|
1520
|
-
advance_page if !at_page_top? && cursor < line_metrics.height + line_metrics.leading + line_metrics.padding_top
|
1521
|
-
end
|
1522
|
-
|
1523
|
-
def convert_image node, opts = {}
|
1524
|
-
target, image_format = (node.extend ::Asciidoctor::Image).target_and_format
|
1525
1639
|
|
1526
1640
|
if image_format == 'gif' && !(defined? ::GMagick::Image)
|
1527
1641
|
log :warn, %(GIF image format not supported. Install the prawn-gmagick gem or convert #{target} to PNG.)
|
@@ -1570,15 +1684,8 @@ module Asciidoctor
|
|
1570
1684
|
end
|
1571
1685
|
end
|
1572
1686
|
|
1573
|
-
return on_image_error :missing, node, target, opts unless image_path
|
1687
|
+
return on_image_error :missing, node, target, (opts.merge align: alignment) unless image_path
|
1574
1688
|
|
1575
|
-
if (float_to = node.attr 'float') && ((BlockFloatNames.include? float_to) ? float_to : (float_to = nil))
|
1576
|
-
alignment = float_to.to_sym
|
1577
|
-
elsif (alignment = node.attr 'align')
|
1578
|
-
alignment = (BlockAlignmentNames.include? alignment) ? alignment.to_sym : :left
|
1579
|
-
else
|
1580
|
-
alignment = (resolve_text_align_from_role node.roles) || @theme.image_align&.to_sym || :left
|
1581
|
-
end
|
1582
1689
|
# TODO: support cover (aka canvas) image layout using "canvas" (or "cover") role
|
1583
1690
|
width = resolve_explicit_width node.attributes, bounds_width: (available_w = bounds.width), support_vw: true, use_fallback: true, constrain_to_bounds: true
|
1584
1691
|
# TODO: add `to_pt page_width` method to ViewportWidth type
|
@@ -1635,7 +1742,7 @@ module Asciidoctor
|
|
1635
1742
|
svg_obj.document.warnings.each do |img_warning|
|
1636
1743
|
log :warn, %(problem encountered in image: #{image_path}; #{img_warning})
|
1637
1744
|
end unless scratch?
|
1638
|
-
draw_image_border image_cursor, rendered_w, rendered_h, alignment unless node.role? && (node.has_role? 'noborder')
|
1745
|
+
draw_image_border image_cursor, rendered_w, rendered_h, alignment unless pinned || (node.role? && (node.has_role? 'noborder'))
|
1639
1746
|
if (link = node.attr 'link')
|
1640
1747
|
add_link_to_image link, { width: rendered_w, height: rendered_h }, position: alignment, y: image_y
|
1641
1748
|
end
|
@@ -1664,7 +1771,7 @@ module Asciidoctor
|
|
1664
1771
|
image_cursor = cursor
|
1665
1772
|
# NOTE: specify both width and height to avoid recalculation
|
1666
1773
|
embed_image image_obj, image_info, width: rendered_w, height: rendered_h, position: alignment
|
1667
|
-
draw_image_border image_cursor, rendered_w, rendered_h, alignment unless node.role? && (node.has_role? 'noborder')
|
1774
|
+
draw_image_border image_cursor, rendered_w, rendered_h, alignment unless pinned || (node.role? && (node.has_role? 'noborder'))
|
1668
1775
|
if (link = node.attr 'link')
|
1669
1776
|
add_link_to_image link, { width: rendered_w, height: rendered_h }, position: alignment, y: image_y
|
1670
1777
|
end
|
@@ -1682,65 +1789,8 @@ module Asciidoctor
|
|
1682
1789
|
end
|
1683
1790
|
rescue => e
|
1684
1791
|
raise if ::StopIteration === e
|
1685
|
-
on_image_error :exception, node, target, (opts.merge message: %(could not embed image: #{image_path}; #{e.message}#{::Prawn::Errors::UnsupportedImageType === e && !(defined? ::GMagick::Image) ? '; install prawn-gmagick gem to add support' : ''}))
|
1686
|
-
end
|
1687
|
-
end
|
1688
|
-
|
1689
|
-
def supports_float_wrapping? node
|
1690
|
-
node.context == :paragraph
|
1691
|
-
end
|
1692
|
-
|
1693
|
-
def init_float_box _node, block_width, block_height, float_to
|
1694
|
-
gap = ::Array === (gap = @theme.image_float_gap) ? gap.dup : [gap, gap]
|
1695
|
-
float_w = block_width + (gap[0] ||= 12)
|
1696
|
-
float_h = block_height + (gap[1] ||= 6)
|
1697
|
-
box_l = bounds.left + (float_to == 'right' ? 0 : float_w)
|
1698
|
-
box_t = cursor + block_height
|
1699
|
-
box_w = bounds.width - float_w
|
1700
|
-
box_r = box_l + box_w
|
1701
|
-
box_h = [box_t, float_h].min
|
1702
|
-
box_b = box_t - box_h
|
1703
|
-
move_cursor_to box_t
|
1704
|
-
@float_box = { page: page_number, top: box_t, right: box_r, bottom: box_b, left: box_l, width: box_w, height: box_h, gap: gap }
|
1705
|
-
end
|
1706
|
-
|
1707
|
-
def draw_image_border top, w, h, alignment
|
1708
|
-
if (Array @theme.image_border_width).any? {|it| it&.> 0 } && (@theme.image_border_color || @theme.base_border_color)
|
1709
|
-
if (@theme.image_border_fit || 'content') == 'auto'
|
1710
|
-
bb_width = bounds.width
|
1711
|
-
elsif alignment == :center
|
1712
|
-
bb_x = (bounds.width - w) * 0.5
|
1713
|
-
elsif alignment == :right
|
1714
|
-
bb_x = bounds.width - w
|
1715
|
-
end
|
1716
|
-
bounding_box [(bb_x || 0), top], width: (bb_width || w), height: h, position: alignment do
|
1717
|
-
theme_fill_and_stroke_bounds :image
|
1718
|
-
end
|
1719
|
-
true
|
1720
|
-
end
|
1721
|
-
end
|
1722
|
-
|
1723
|
-
def on_image_error _reason, node, target, opts
|
1724
|
-
log :warn, opts[:message] if opts.key? :message
|
1725
|
-
alt_text_vars = { alt: (node.attr 'alt'), target: target }
|
1726
|
-
alt_text_template = @theme.image_alt_content || '%{link}[%{alt}]%{/link} | <em>%{target}</em>' # rubocop:disable Style/FormatStringToken
|
1727
|
-
return if alt_text_template.empty?
|
1728
|
-
if (link = node.attr 'link')
|
1729
|
-
alt_text_vars[:link] = %(<a href="#{link}">)
|
1730
|
-
alt_text_vars[:'/link'] = '</a>'
|
1731
|
-
else
|
1732
|
-
alt_text_vars[:link] = ''
|
1733
|
-
alt_text_vars[:'/link'] = ''
|
1792
|
+
on_image_error :exception, node, target, (opts.merge align: alignment, message: %(could not embed image: #{image_path}; #{e.message}#{::Prawn::Errors::UnsupportedImageType === e && !(defined? ::GMagick::Image) ? '; install prawn-gmagick gem to add support' : ''}))
|
1734
1793
|
end
|
1735
|
-
theme_font :image_alt do
|
1736
|
-
alignment = (alignment = node.attr 'align') ?
|
1737
|
-
((BlockAlignmentNames.include? alignment) ? alignment.to_sym : :left) :
|
1738
|
-
(resolve_text_align_from_role node.roles) || (@theme.image_align&.to_sym || :left)
|
1739
|
-
ink_prose alt_text_template % alt_text_vars, align: alignment, margin: 0, normalize: false, single_line: true
|
1740
|
-
end
|
1741
|
-
ink_caption node, category: :image, end: :bottom if node.title?
|
1742
|
-
theme_margin :block, :bottom, (next_enclosed_block node) unless opts[:pinned]
|
1743
|
-
nil
|
1744
1794
|
end
|
1745
1795
|
|
1746
1796
|
def convert_audio node
|
@@ -1789,158 +1839,28 @@ module Asciidoctor
|
|
1789
1839
|
end
|
1790
1840
|
end
|
1791
1841
|
|
1792
|
-
#
|
1793
|
-
def
|
1794
|
-
|
1795
|
-
|
1796
|
-
|
1797
|
-
# HACK: disable built-in syntax highlighter; must be done before calling node.content!
|
1798
|
-
if node.style == 'source' && (highlighter = (syntax_hl = node.document.syntax_highlighter)&.highlight? && syntax_hl.name)
|
1799
|
-
case highlighter
|
1800
|
-
when 'coderay'
|
1801
|
-
Helpers.require_library CodeRayRequirePath, 'coderay' unless defined? ::Asciidoctor::Prawn::CodeRayEncoder
|
1802
|
-
when 'pygments'
|
1803
|
-
Helpers.require_library PygmentsRequirePath, 'pygments.rb' unless defined? ::Pygments::Ext::BlockStyles
|
1804
|
-
when 'rouge'
|
1805
|
-
Helpers.require_library RougeRequirePath, 'rouge' unless defined? ::Rouge::Formatters::Prawn
|
1806
|
-
else
|
1807
|
-
highlighter = nil
|
1808
|
-
end
|
1809
|
-
prev_subs = (subs = node.subs).dup
|
1810
|
-
callouts_enabled = subs.include? :callouts
|
1811
|
-
highlight_idx = subs.index :highlight
|
1812
|
-
# NOTE: scratch? here only applies if listing block is nested inside another block
|
1813
|
-
if !highlighter || scratch?
|
1814
|
-
highlighter = nil
|
1815
|
-
if highlight_idx
|
1816
|
-
# switch the :highlight sub back to :specialcharacters
|
1817
|
-
subs[highlight_idx] = :specialcharacters
|
1818
|
-
else
|
1819
|
-
prev_subs = nil
|
1820
|
-
end
|
1821
|
-
source_string = guard_indentation node.content
|
1822
|
-
elsif highlight_idx
|
1823
|
-
# NOTE: the source highlighter logic below handles the highlight and callouts subs
|
1824
|
-
subs.replace subs - [:highlight, :callouts]
|
1825
|
-
# NOTE: indentation guards will be added by the source highlighter logic
|
1826
|
-
source_string = expand_tabs node.content
|
1827
|
-
else
|
1828
|
-
highlighter = prev_subs = nil
|
1829
|
-
source_string = guard_indentation node.content
|
1830
|
-
end
|
1831
|
-
else
|
1832
|
-
highlighter = nil
|
1833
|
-
source_string = guard_indentation node.content
|
1834
|
-
end
|
1835
|
-
|
1836
|
-
case highlighter
|
1837
|
-
when 'coderay'
|
1838
|
-
source_string, conum_mapping = extract_conums source_string if callouts_enabled
|
1839
|
-
srclang = node.attr 'language', 'text'
|
1840
|
-
begin
|
1841
|
-
::CodeRay::Scanners[(srclang = (srclang.start_with? 'html+') ? (srclang.slice 5, srclang.length).to_sym : srclang.to_sym)]
|
1842
|
-
rescue ::ArgumentError
|
1843
|
-
srclang = :text
|
1844
|
-
end
|
1845
|
-
fragments = (::CodeRay.scan source_string, srclang).to_prawn
|
1846
|
-
source_chunks = conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
|
1847
|
-
when 'pygments'
|
1848
|
-
unless (style = (node.document.attr 'pygments-style')) && (::Pygments::Ext::BlockStyles.available? style)
|
1849
|
-
style = 'pastie'
|
1850
|
-
end
|
1851
|
-
# QUESTION: allow border color to be set by theme for highlighted block?
|
1852
|
-
pg_block_styles = ::Pygments::Ext::BlockStyles.for style
|
1853
|
-
bg_color_override = pg_block_styles[:background_color]
|
1854
|
-
font_color_override = pg_block_styles[:font_color]
|
1855
|
-
if source_string.empty?
|
1856
|
-
source_chunks = []
|
1857
|
-
else
|
1858
|
-
lexer = (::Pygments::Lexer.find_by_alias node.attr 'language', 'text') || (::Pygments::Lexer.find_by_mimetype 'text/plain')
|
1859
|
-
lexer_opts = { nowrap: true, noclasses: true, stripnl: false, style: style }
|
1860
|
-
lexer_opts[:startinline] = !(node.option? 'mixed') if lexer.name == 'PHP'
|
1861
|
-
source_string, conum_mapping = extract_conums source_string if callouts_enabled
|
1862
|
-
# NOTE: highlight can return nil if something goes wrong; fallback to encoded source string if this happens
|
1863
|
-
result = (lexer.highlight source_string, options: lexer_opts) || (node.apply_subs source_string, [:specialcharacters])
|
1864
|
-
if node.attr? 'highlight'
|
1865
|
-
if (highlight_lines = node.resolve_lines_to_highlight source_string, (node.attr 'highlight')).empty?
|
1866
|
-
highlight_lines = nil
|
1867
|
-
else
|
1868
|
-
pg_highlight_bg_color = pg_block_styles[:highlight_background_color]
|
1869
|
-
highlight_lines = highlight_lines.map {|linenum| [linenum, pg_highlight_bg_color] }.to_h
|
1870
|
-
end
|
1871
|
-
end
|
1872
|
-
if (node.option? 'linenums') || (node.attr? 'linenums')
|
1873
|
-
linenums = (node.attr 'start', 1).to_i
|
1874
|
-
postprocess = true
|
1875
|
-
extensions << FormattedText::SourceWrap
|
1876
|
-
elsif conum_mapping || highlight_lines
|
1877
|
-
postprocess = true
|
1878
|
-
end
|
1879
|
-
fragments = text_formatter.format result
|
1880
|
-
fragments = restore_conums fragments, conum_mapping, linenums, highlight_lines if postprocess
|
1881
|
-
source_chunks = guard_indentation_in_fragments fragments
|
1882
|
-
end
|
1883
|
-
when 'rouge'
|
1884
|
-
formatter = (@rouge_formatter ||= ::Rouge::Formatters::Prawn.new theme: (node.document.attr 'rouge-style'), line_gap: @theme.code_line_gap, highlight_background_color: @theme.code_highlight_background_color)
|
1885
|
-
# QUESTION: allow border color to be set by theme for highlighted block?
|
1886
|
-
bg_color_override = formatter.background_color
|
1887
|
-
if source_string.empty?
|
1888
|
-
source_chunks = []
|
1889
|
-
else
|
1890
|
-
if (node.option? 'linenums') || (node.attr? 'linenums')
|
1891
|
-
formatter_opts = { line_numbers: true, start_line: (node.attr 'start', 1).to_i }
|
1892
|
-
extensions << FormattedText::SourceWrap
|
1893
|
-
else
|
1894
|
-
formatter_opts = {}
|
1895
|
-
end
|
1896
|
-
if (srclang = node.attr 'language')
|
1897
|
-
if srclang.include? '?'
|
1898
|
-
if (lexer = ::Rouge::Lexer.find_fancy srclang) && lexer.tag == 'php' && !(node.option? 'mixed') && !((lexer_opts = lexer.options).key? 'start_inline')
|
1899
|
-
lexer = lexer.class.new lexer_opts.merge 'start_inline' => true
|
1900
|
-
end
|
1901
|
-
elsif (lexer = ::Rouge::Lexer.find srclang)
|
1902
|
-
lexer = lexer.new start_inline: true if lexer.tag == 'php' && !(node.option? 'mixed')
|
1903
|
-
end
|
1904
|
-
end
|
1905
|
-
lexer ||= ::Rouge::Lexers::PlainText
|
1906
|
-
source_string, conum_mapping = extract_conums source_string if callouts_enabled
|
1907
|
-
if (node.attr? 'highlight') && !(hl_lines = (node.resolve_lines_to_highlight source_string, (node.attr 'highlight'))).empty?
|
1908
|
-
formatter_opts[:highlight_lines] = hl_lines.map {|linenum| [linenum, true] }.to_h
|
1909
|
-
end
|
1910
|
-
fragments = formatter.format (lexer.lex source_string), formatter_opts rescue [text: source_string]
|
1911
|
-
source_chunks = conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
|
1912
|
-
end
|
1913
|
-
else
|
1914
|
-
# NOTE: only format if we detect a need (callouts or inline formatting)
|
1915
|
-
source_chunks = (XMLMarkupRx.match? source_string) ? (text_formatter.format source_string) : [text: source_string]
|
1842
|
+
# NOTE: to insert sequential page breaks, you must put {nbsp} between page breaks
|
1843
|
+
def convert_page_break node
|
1844
|
+
if (page_layout = node.attr 'page-layout').nil_or_empty?
|
1845
|
+
unless node.role? && (page_layout = (node.roles.map(&:to_sym) & PageLayouts)[-1])
|
1846
|
+
page_layout = nil
|
1916
1847
|
end
|
1917
|
-
|
1918
|
-
|
1848
|
+
elsif !(PageLayouts.include? (page_layout = page_layout.to_sym))
|
1849
|
+
page_layout = nil
|
1919
1850
|
end
|
1920
1851
|
|
1921
|
-
|
1922
|
-
|
1923
|
-
|
1924
|
-
|
1925
|
-
end
|
1926
|
-
pad_box @theme.code_padding, node do
|
1927
|
-
theme_font :code do
|
1928
|
-
typeset_formatted_text source_chunks, (calc_line_metrics @base_line_height),
|
1929
|
-
color: (font_color_override || @theme.code_font_color || @font_color),
|
1930
|
-
size: adjusted_font_size,
|
1931
|
-
bottom_gutter: @bottom_gutters[-1][node],
|
1932
|
-
extensions: extensions.empty? ? nil : extensions
|
1933
|
-
end
|
1852
|
+
if at_page_top?
|
1853
|
+
if page_layout && page_layout != page.layout && page.empty?
|
1854
|
+
delete_current_page
|
1855
|
+
advance_page layout: page_layout
|
1934
1856
|
end
|
1857
|
+
elsif page_layout
|
1858
|
+
advance_page layout: page_layout
|
1859
|
+
else
|
1860
|
+
advance_page
|
1935
1861
|
end
|
1936
|
-
|
1937
|
-
theme_margin :block, :bottom, (next_enclosed_block node)
|
1938
1862
|
end
|
1939
1863
|
|
1940
|
-
alias convert_listing convert_code
|
1941
|
-
alias convert_literal convert_code
|
1942
|
-
alias convert_listing_or_literal convert_code
|
1943
|
-
|
1944
1864
|
def convert_pass node
|
1945
1865
|
theme_font :code do
|
1946
1866
|
typeset_formatted_text [text: (guard_indentation node.content), color: @theme.base_font_color], (calc_line_metrics @base_line_height)
|
@@ -1963,100 +1883,14 @@ module Asciidoctor
|
|
1963
1883
|
theme_margin :block, :bottom, (next_enclosed_block node)
|
1964
1884
|
end
|
1965
1885
|
|
1966
|
-
|
1967
|
-
|
1968
|
-
|
1969
|
-
|
1970
|
-
|
1971
|
-
|
1972
|
-
|
1973
|
-
|
1974
|
-
if line.include? '<'
|
1975
|
-
line = line.gsub CalloutExtractRx do
|
1976
|
-
# honor the escape
|
1977
|
-
if $1 == ?\\
|
1978
|
-
$&.sub $1, ''
|
1979
|
-
else
|
1980
|
-
(conum_mapping[line_num] ||= []) << ($3 == '.' ? (auto_num += 1) : $3.to_i)
|
1981
|
-
''
|
1982
|
-
end
|
1983
|
-
end
|
1984
|
-
# NOTE: use first position to store space that precedes conums
|
1985
|
-
if (conum_mapping.key? line_num) && (line.end_with? ' ')
|
1986
|
-
trimmed_line = line.rstrip
|
1987
|
-
conum_mapping[line_num].unshift line.slice trimmed_line.length, line.length
|
1988
|
-
line = trimmed_line
|
1989
|
-
end
|
1990
|
-
end
|
1991
|
-
line
|
1992
|
-
end.join LF
|
1993
|
-
conum_mapping = nil if conum_mapping.empty?
|
1994
|
-
[string, conum_mapping]
|
1995
|
-
end
|
1996
|
-
|
1997
|
-
# Restore the conums into the Array of formatted text fragments
|
1998
|
-
#--
|
1999
|
-
# QUESTION: can this be done more efficiently?
|
2000
|
-
# QUESTION: can we reuse arrange_fragments_by_line?
|
2001
|
-
def restore_conums fragments, conum_mapping, linenums = nil, highlight_lines = nil
|
2002
|
-
lines = []
|
2003
|
-
line_num = 0
|
2004
|
-
# reorganize the fragments into an array of lines
|
2005
|
-
fragments.each do |fragment|
|
2006
|
-
line = (lines[line_num] ||= [])
|
2007
|
-
if (text = fragment[:text]) == LF
|
2008
|
-
lines[line_num += 1] ||= []
|
2009
|
-
elsif text.include? LF
|
2010
|
-
(text.split LF, -1).each_with_index do |line_in_fragment, idx|
|
2011
|
-
line = (lines[line_num += 1] ||= []) unless idx == 0
|
2012
|
-
line << (fragment.merge text: line_in_fragment) unless line_in_fragment.empty?
|
2013
|
-
end
|
2014
|
-
else
|
2015
|
-
line << fragment
|
2016
|
-
end
|
2017
|
-
end
|
2018
|
-
conum_font_color = @theme.conum_font_color
|
2019
|
-
if (conum_font_name = @theme.conum_font_family) == font_name
|
2020
|
-
conum_font_name = nil
|
2021
|
-
end
|
2022
|
-
last_line_num = lines.size - 1
|
2023
|
-
if linenums
|
2024
|
-
pad_size = (last_line_num + 1).to_s.length
|
2025
|
-
linenum_color = @theme.code_linenum_font_color
|
2026
|
-
end
|
2027
|
-
# append conums to appropriate lines, then flatten to an array of fragments
|
2028
|
-
lines.flat_map.with_index do |line, cur_line_num|
|
2029
|
-
last_line = cur_line_num == last_line_num
|
2030
|
-
visible_line_num = cur_line_num + (linenums || 1)
|
2031
|
-
if highlight_lines && (highlight_bg_color = highlight_lines[visible_line_num])
|
2032
|
-
line.unshift text: DummyText, background_color: highlight_bg_color, highlight: true, inline_block: true, extend: true, width: 0, callback: [FormattedText::TextBackgroundAndBorderRenderer]
|
2033
|
-
end
|
2034
|
-
line.unshift text: %(#{visible_line_num.to_s.rjust pad_size} ), linenum: visible_line_num, color: linenum_color if linenums
|
2035
|
-
if conum_mapping && (conums = conum_mapping.delete cur_line_num)
|
2036
|
-
line << { text: conums.shift } if ::String === conums[0]
|
2037
|
-
conum_text = conums.map {|num| conum_glyph num }.join ' '
|
2038
|
-
conum_fragment = { text: conum_text }
|
2039
|
-
conum_fragment[:color] = conum_font_color if conum_font_color
|
2040
|
-
conum_fragment[:font] = conum_font_name if conum_font_name
|
2041
|
-
line << conum_fragment
|
2042
|
-
end
|
2043
|
-
line << { text: LF } unless last_line
|
2044
|
-
line
|
2045
|
-
end
|
2046
|
-
end
|
2047
|
-
|
2048
|
-
def conum_glyph number
|
2049
|
-
@conum_glyphs[number - 1]
|
2050
|
-
end
|
2051
|
-
|
2052
|
-
def convert_table node
|
2053
|
-
if !at_page_top? && ((unbreakable = node.option? 'unbreakable') || ((node.option? 'breakable') && (node.id || node.title?)))
|
2054
|
-
(table_container = Block.new (table_dup = node.dup), :open) << table_dup
|
2055
|
-
if unbreakable
|
2056
|
-
table_dup.remove_attr 'unbreakable-option'
|
2057
|
-
table_container.set_attr 'unbreakable-option'
|
2058
|
-
else
|
2059
|
-
table_dup.remove_attr 'breakable-option'
|
1886
|
+
def convert_table node
|
1887
|
+
if !at_page_top? && ((unbreakable = node.option? 'unbreakable') || ((node.option? 'breakable') && (node.id || node.title?)))
|
1888
|
+
(table_container = Block.new (table_dup = node.dup), :open) << table_dup
|
1889
|
+
if unbreakable
|
1890
|
+
table_dup.remove_attr 'unbreakable-option'
|
1891
|
+
table_container.set_attr 'unbreakable-option'
|
1892
|
+
else
|
1893
|
+
table_dup.remove_attr 'breakable-option'
|
2060
1894
|
end
|
2061
1895
|
table_container.id, table_dup.id = table_dup.id, nil
|
2062
1896
|
if table_dup.title?
|
@@ -2428,10 +2262,7 @@ module Asciidoctor
|
|
2428
2262
|
return if @toc_extent
|
2429
2263
|
is_macro = (placement = opts[:placement] || 'macro') == 'macro'
|
2430
2264
|
if ((doc = node.document).attr? 'toc-placement', placement) && (doc.attr? 'toc') && doc.sections?
|
2431
|
-
if (is_book = doc.doctype == 'book')
|
2432
|
-
start_new_page unless at_page_top?
|
2433
|
-
start_new_page if @ppbook && verso_page? && !(is_macro && (node.option? 'nonfacing'))
|
2434
|
-
end
|
2265
|
+
start_toc_page node, placement if (is_book = doc.doctype == 'book')
|
2435
2266
|
add_dest_for_block node, id: (node.id || 'toc') if is_macro
|
2436
2267
|
toc_extent = @toc_extent = allocate_toc doc, (doc.attr 'toclevels', 2).to_i, cursor, (title_page_on = is_book || (doc.attr? 'title-page'))
|
2437
2268
|
@index.start_page_number = toc_extent.to.page + 1 if title_page_on && @theme.page_numbering_start_at == 'after-toc'
|
@@ -2443,74 +2274,42 @@ module Asciidoctor
|
|
2443
2274
|
nil
|
2444
2275
|
end
|
2445
2276
|
|
2446
|
-
|
2447
|
-
|
2448
|
-
if (
|
2449
|
-
|
2450
|
-
page_layout = nil
|
2451
|
-
end
|
2452
|
-
elsif !(PageLayouts.include? (page_layout = page_layout.to_sym))
|
2453
|
-
page_layout = nil
|
2454
|
-
end
|
2455
|
-
|
2456
|
-
if at_page_top?
|
2457
|
-
if page_layout && page_layout != page.layout && page.empty?
|
2458
|
-
delete_current_page
|
2459
|
-
advance_page layout: page_layout
|
2460
|
-
end
|
2461
|
-
elsif page_layout
|
2462
|
-
advance_page layout: page_layout
|
2277
|
+
def traverse node, opts = {}
|
2278
|
+
# NOTE: need to reconfigure document to use scratch converter in scratch document
|
2279
|
+
if self == (prev_converter = node.document.converter)
|
2280
|
+
prev_converter = nil
|
2463
2281
|
else
|
2464
|
-
|
2282
|
+
node.document.instance_variable_set :@converter, self
|
2465
2283
|
end
|
2466
|
-
|
2467
|
-
|
2468
|
-
|
2469
|
-
|
2470
|
-
|
2471
|
-
|
2472
|
-
@index.categories.each do |category|
|
2473
|
-
bounds.move_past_bottom if space_needed_for_category > cursor
|
2474
|
-
ink_prose category.name,
|
2475
|
-
align: :left,
|
2476
|
-
inline_format: false,
|
2477
|
-
margin_bottom: @theme.description_list_term_spacing,
|
2478
|
-
style: @theme.description_list_term_font_style&.to_sym
|
2479
|
-
category.terms.each {|term| convert_index_list_item term, pagenum_sequence_style }
|
2480
|
-
@theme.prose_margin_bottom > cursor ? bounds.move_past_bottom : (move_down @theme.prose_margin_bottom)
|
2284
|
+
if node.blocks?
|
2285
|
+
node.content
|
2286
|
+
elsif node.content_model != :compound && (string = node.content)
|
2287
|
+
prose_opts = opts.merge hyphenate: true, margin_bottom: 0
|
2288
|
+
if (bottom_gutter = @bottom_gutters[-1][node])
|
2289
|
+
prose_opts[:bottom_gutter] = bottom_gutter
|
2481
2290
|
end
|
2291
|
+
ink_prose string, prose_opts
|
2482
2292
|
end
|
2483
|
-
|
2293
|
+
ensure
|
2294
|
+
node.document.instance_variable_set :@converter, prev_converter if prev_converter
|
2484
2295
|
end
|
2485
2296
|
|
2486
|
-
def
|
2487
|
-
|
2488
|
-
|
2489
|
-
|
2490
|
-
|
2491
|
-
|
2492
|
-
|
2493
|
-
|
2494
|
-
|
2495
|
-
|
2496
|
-
|
2497
|
-
%(<a anchor="#{anchor}">#{range}</a>)
|
2498
|
-
end
|
2499
|
-
else # term
|
2500
|
-
pagenums = term.dests.map {|dest| %(<a anchor="#{dest[:anchor]}">#{dest[:page]}</a>) }
|
2501
|
-
end
|
2297
|
+
def traverse_list_item node, list_type, opts = {}
|
2298
|
+
if list_type == :dlist # qanda
|
2299
|
+
terms, desc = node
|
2300
|
+
terms.each {|term| ink_prose %(<em>#{term.text}</em>), (opts.merge margin_bottom: @theme.description_list_term_spacing) }
|
2301
|
+
if desc
|
2302
|
+
ink_prose desc.text, (opts.merge hyphenate: true) if desc.text?
|
2303
|
+
traverse desc
|
2304
|
+
end
|
2305
|
+
else
|
2306
|
+
if (primary_text = node.text).nil_or_empty?
|
2307
|
+
ink_prose DummyText, opts unless node.blocks?
|
2502
2308
|
else
|
2503
|
-
|
2309
|
+
ink_prose primary_text, (opts.merge hyphenate: true)
|
2504
2310
|
end
|
2505
|
-
|
2311
|
+
traverse node
|
2506
2312
|
end
|
2507
|
-
subterm_indent = @theme.description_list_description_indent
|
2508
|
-
ink_prose text, align: :left, margin: 0, hanging_indent: subterm_indent * 2
|
2509
|
-
indent subterm_indent do
|
2510
|
-
term.subterms.each do |subterm|
|
2511
|
-
convert_index_list_item subterm, pagenum_sequence_style
|
2512
|
-
end
|
2513
|
-
end unless term.leaf?
|
2514
2313
|
end
|
2515
2314
|
|
2516
2315
|
def convert_inline_anchor node
|
@@ -2792,193 +2591,118 @@ module Asciidoctor
|
|
2792
2591
|
node.id ? %(<a id="#{node.id}">#{DummyText}</a>#{quoted_text}) : quoted_text
|
2793
2592
|
end
|
2794
2593
|
|
2795
|
-
#
|
2796
|
-
|
2797
|
-
|
2594
|
+
# If an id is provided or the node passed as the first argument has an id,
|
2595
|
+
# add a named destination to the document equivalent to the node id at the
|
2596
|
+
# current y position. If the node does not have an id and an id is not
|
2597
|
+
# specified, do nothing.
|
2598
|
+
#
|
2599
|
+
# If the node is a section, and the current y position is the top of the
|
2600
|
+
# page, set the y position equal to the page height to improve the navigation
|
2601
|
+
# experience. If the current x position is at or inside the left margin, set
|
2602
|
+
# the x position equal to 0 (left edge of page) to improve the navigation
|
2603
|
+
# experience.
|
2604
|
+
def add_dest_for_block node, id: nil, y: nil
|
2605
|
+
if !scratch? && (id ||= node.id)
|
2606
|
+
dest_x = bounds.absolute_left.truncate 4
|
2607
|
+
# QUESTION: when content is aligned to left margin, should we keep precise x value or just use 0?
|
2608
|
+
dest_x = 0 if dest_x <= page_margin_left
|
2609
|
+
unless (dest_y = y)
|
2610
|
+
dest_y = @y
|
2611
|
+
dest_y += [page_height - dest_y, -@theme.block_anchor_top.to_f].min
|
2612
|
+
end
|
2613
|
+
# TODO: find a way to store only the ref of the destination; look it up when we need it
|
2614
|
+
node.set_attr 'pdf-destination', (node_dest = (dest_xyz dest_x, dest_y))
|
2615
|
+
add_dest id, node_dest
|
2616
|
+
end
|
2617
|
+
nil
|
2618
|
+
end
|
2798
2619
|
|
2799
|
-
|
2800
|
-
if
|
2801
|
-
|
2802
|
-
|
2803
|
-
|
2620
|
+
def add_outline doc, num_levels, toc_page_nums, num_front_matter_pages, has_front_cover
|
2621
|
+
if ::String === num_levels
|
2622
|
+
if num_levels.include? ':'
|
2623
|
+
num_levels, expand_levels = num_levels.split ':', 2
|
2624
|
+
num_levels = num_levels.empty? ? (doc.attr 'toclevels', 2).to_i : num_levels.to_i
|
2625
|
+
expand_levels = expand_levels.to_i
|
2626
|
+
else
|
2627
|
+
num_levels = expand_levels = num_levels.to_i
|
2628
|
+
end
|
2629
|
+
else
|
2630
|
+
expand_levels = num_levels
|
2804
2631
|
end
|
2632
|
+
front_matter_counter = RomanNumeral.new 0, :lower
|
2633
|
+
pagenum_labels = {}
|
2805
2634
|
|
2806
|
-
|
2807
|
-
|
2808
|
-
prev_bg_color = @page_bg_color
|
2809
|
-
if (bg_image = resolve_background_image doc, @theme, 'title-page-background-image')
|
2810
|
-
@page_bg_image[side] = bg_image[0] && bg_image
|
2635
|
+
num_front_matter_pages.times do |n|
|
2636
|
+
pagenum_labels[n] = { P: (::PDF::Core::LiteralString.new front_matter_counter.next!.to_s) }
|
2811
2637
|
end
|
2812
|
-
|
2813
|
-
|
2638
|
+
|
2639
|
+
# add labels for each content page, which is required for reader's page navigator to work correctly
|
2640
|
+
(num_front_matter_pages..(page_count - 1)).each_with_index do |n, i|
|
2641
|
+
pagenum_labels[n] = { P: (::PDF::Core::LiteralString.new (i + 1).to_s) }
|
2814
2642
|
end
|
2815
|
-
recycle ? float { init_page self } : start_new_page
|
2816
|
-
@page_bg_image[side] = prev_bg_image if bg_image
|
2817
|
-
@page_bg_color = prev_bg_color if bg_color
|
2818
2643
|
|
2819
|
-
|
2820
|
-
|
2644
|
+
unless toc_page_nums.none? || (toc_title = doc.attr 'toc-title').nil_or_empty?
|
2645
|
+
toc_section = insert_toc_section doc, toc_title, toc_page_nums
|
2646
|
+
end
|
2821
2647
|
|
2822
|
-
|
2823
|
-
|
2648
|
+
outline.define do
|
2649
|
+
initial_pagenum = has_front_cover ? 2 : 1
|
2650
|
+
# FIXME: use sanitize: :plain_text on Document#doctitle once available
|
2651
|
+
if document.page_count >= initial_pagenum && (outline_title = doc.attr 'outline-title') &&
|
2652
|
+
(outline_title.empty? ? (outline_title = document.resolve_doctitle doc) : outline_title)
|
2653
|
+
page title: (document.sanitize outline_title), destination: (document.dest_top initial_pagenum)
|
2654
|
+
end
|
2655
|
+
# QUESTION: is there any way to get add_outline_level to invoke in the context of the outline?
|
2656
|
+
document.add_outline_level self, doc.sections, num_levels, expand_levels
|
2657
|
+
end if doc.attr? 'outline'
|
2824
2658
|
|
2825
|
-
|
2826
|
-
|
2827
|
-
|
2828
|
-
|
2829
|
-
|
2830
|
-
|
2831
|
-
|
2832
|
-
|
2833
|
-
|
2834
|
-
|
2659
|
+
toc_section&.remove
|
2660
|
+
|
2661
|
+
catalog.data[:PageLabels] = state.store.ref Nums: pagenum_labels.flatten
|
2662
|
+
primary_page_mode, secondary_page_mode = PageModes[(doc.attr 'pdf-page-mode') || @theme.page_mode]
|
2663
|
+
catalog.data[:PageMode] = primary_page_mode
|
2664
|
+
catalog.data[:NonFullScreenPageMode] = secondary_page_mode if secondary_page_mode
|
2665
|
+
nil
|
2666
|
+
end
|
2667
|
+
|
2668
|
+
def add_outline_level outline, sections, num_levels, expand_levels
|
2669
|
+
sections.each do |sect|
|
2670
|
+
next if (num_levels_for_sect = (sect.attr 'outlinelevels', num_levels).to_i) < (level = sect.level) ||
|
2671
|
+
((sect.option? 'notitle') && sect == sect.document.last_child && sect.empty?)
|
2672
|
+
sect_title = sanitize sect.numbered_title formal: true
|
2673
|
+
sect_destination = sect.attr 'pdf-destination'
|
2674
|
+
if level < num_levels_for_sect && sect.sections?
|
2675
|
+
outline.section sect_title, destination: sect_destination, closed: expand_levels < 1 do
|
2676
|
+
add_outline_level outline, sect.sections, num_levels_for_sect, (expand_levels - 1)
|
2835
2677
|
end
|
2836
2678
|
else
|
2837
|
-
|
2838
|
-
relative_to_imagesdir = false
|
2839
|
-
if logo_image_from_theme
|
2840
|
-
logo_image_path = apply_subs_discretely doc, logo_image_path, subs: [:attributes]
|
2841
|
-
logo_image_path = ThemeLoader.resolve_theme_asset logo_image_path, @themesdir unless doc.is_uri? logo_image_path
|
2842
|
-
end
|
2843
|
-
end
|
2844
|
-
if (::Asciidoctor::Image.target_and_format logo_image_path)[1] == 'pdf'
|
2845
|
-
log :error, %(PDF format not supported for title page logo image: #{logo_image_path})
|
2846
|
-
else
|
2847
|
-
logo_image_attrs['target'] = logo_image_path
|
2848
|
-
# NOTE: at the very least, title_align will be a valid alignment value
|
2849
|
-
logo_image_attrs['align'] = [(logo_image_attrs.delete 'align'), @theme.title_page_logo_align, title_align.to_s].find {|val| (BlockAlignmentNames.include? val) }
|
2850
|
-
if (logo_image_top = logo_image_attrs['top'] || @theme.title_page_logo_top)
|
2851
|
-
initial_y, @y = @y, (resolve_top logo_image_top)
|
2852
|
-
end
|
2853
|
-
# NOTE: pinned option keeps image on same page
|
2854
|
-
indent (@theme.title_page_logo_margin_left || 0), (@theme.title_page_logo_margin_right || 0) do
|
2855
|
-
# FIXME: add API to Asciidoctor for creating blocks outside of extensions
|
2856
|
-
convert_image (::Asciidoctor::Block.new doc, :image, content_model: :empty, attributes: logo_image_attrs), relative_to_imagesdir: relative_to_imagesdir, pinned: true
|
2857
|
-
end
|
2858
|
-
@y = initial_y if initial_y
|
2859
|
-
end
|
2860
|
-
end
|
2861
|
-
|
2862
|
-
# TODO: prevent content from spilling to next page
|
2863
|
-
theme_font :title_page do
|
2864
|
-
if (title_top = @theme.title_page_title_top)
|
2865
|
-
@y = resolve_top title_top
|
2866
|
-
end
|
2867
|
-
unless @theme.title_page_title_display == 'none'
|
2868
|
-
doctitle = doc.doctitle partition: true
|
2869
|
-
move_down @theme.title_page_title_margin_top || 0
|
2870
|
-
indent (@theme.title_page_title_margin_left || 0), (@theme.title_page_title_margin_right || 0) do
|
2871
|
-
theme_font :title_page_title do
|
2872
|
-
ink_prose doctitle.main, align: title_align, margin: 0
|
2873
|
-
end
|
2874
|
-
end
|
2875
|
-
move_down @theme.title_page_title_margin_bottom || 0
|
2876
|
-
end
|
2877
|
-
if @theme.title_page_subtitle_display != 'none' && (subtitle = (doctitle || (doc.doctitle partition: true)).subtitle)
|
2878
|
-
move_down @theme.title_page_subtitle_margin_top || 0
|
2879
|
-
indent (@theme.title_page_subtitle_margin_left || 0), (@theme.title_page_subtitle_margin_right || 0) do
|
2880
|
-
theme_font :title_page_subtitle do
|
2881
|
-
ink_prose subtitle, align: title_align, margin: 0
|
2882
|
-
end
|
2883
|
-
end
|
2884
|
-
move_down @theme.title_page_subtitle_margin_bottom || 0
|
2885
|
-
end
|
2886
|
-
if @theme.title_page_authors_display != 'none' && (doc.attr? 'authors')
|
2887
|
-
move_down @theme.title_page_authors_margin_top || 0
|
2888
|
-
indent (@theme.title_page_authors_margin_left || 0), (@theme.title_page_authors_margin_right || 0) do
|
2889
|
-
generic_authors_content = @theme.title_page_authors_content
|
2890
|
-
authors_content = {
|
2891
|
-
name_only: @theme.title_page_authors_content_name_only || generic_authors_content,
|
2892
|
-
with_email: @theme.title_page_authors_content_with_email || generic_authors_content,
|
2893
|
-
with_url: @theme.title_page_authors_content_with_url || generic_authors_content,
|
2894
|
-
}
|
2895
|
-
authors = doc.authors.map.with_index do |author, idx|
|
2896
|
-
with_author doc, author, idx == 0 do
|
2897
|
-
author_content_key = (url = doc.attr 'url') ? ((url.start_with? 'mailto:') ? :with_email : :with_url) : :name_only
|
2898
|
-
if (author_content = authors_content[author_content_key])
|
2899
|
-
apply_subs_discretely doc, author_content, drop_lines_with_unresolved_attributes: true, imagesdir: @themesdir
|
2900
|
-
else
|
2901
|
-
doc.attr 'author'
|
2902
|
-
end
|
2903
|
-
end
|
2904
|
-
end.join @theme.title_page_authors_delimiter
|
2905
|
-
theme_font :title_page_authors do
|
2906
|
-
ink_prose authors, align: title_align, margin: 0, normalize: true
|
2907
|
-
end
|
2908
|
-
end
|
2909
|
-
move_down @theme.title_page_authors_margin_bottom || 0
|
2910
|
-
end
|
2911
|
-
unless @theme.title_page_revision_display == 'none' || (revision_info = [(doc.attr? 'revnumber') ? %(#{doc.attr 'version-label'} #{doc.attr 'revnumber'}) : nil, (doc.attr 'revdate')].compact).empty?
|
2912
|
-
move_down @theme.title_page_revision_margin_top || 0
|
2913
|
-
revision_text = revision_info.join @theme.title_page_revision_delimiter
|
2914
|
-
if (revremark = doc.attr 'revremark')
|
2915
|
-
revision_text = %(#{revision_text}: #{revremark})
|
2916
|
-
end
|
2917
|
-
indent (@theme.title_page_revision_margin_left || 0), (@theme.title_page_revision_margin_right || 0) do
|
2918
|
-
theme_font :title_page_revision do
|
2919
|
-
ink_prose revision_text, align: title_align, margin: 0, normalize: false
|
2920
|
-
end
|
2921
|
-
end
|
2922
|
-
move_down @theme.title_page_revision_margin_bottom || 0
|
2679
|
+
outline.page title: sect_title, destination: sect_destination
|
2923
2680
|
end
|
2924
2681
|
end
|
2925
|
-
|
2926
|
-
ink_prose DummyText, margin: 0, line_height: 1, normalize: false if page.empty?
|
2927
|
-
true
|
2928
2682
|
end
|
2929
2683
|
|
2930
|
-
def
|
2931
|
-
|
2932
|
-
|
2933
|
-
|
2934
|
-
go_to_page page_count if face == :back
|
2935
|
-
start_new_page_discretely
|
2936
|
-
# NOTE: open graphics state to prevent page from being reused
|
2937
|
-
open_graphics_state if face == :front
|
2938
|
-
return
|
2939
|
-
elsif image_path == '~'
|
2940
|
-
@page_margin_by_side[:cover] = @page_margin_by_side[:recto] if @media == 'prepress'
|
2941
|
-
return
|
2942
|
-
end
|
2943
|
-
|
2944
|
-
go_to_page page_count if face == :back
|
2945
|
-
if image_opts[:format] == 'pdf'
|
2946
|
-
import_page image_path, (image_opts.merge advance: face != :back, advance_if_missing: false)
|
2947
|
-
else
|
2948
|
-
begin
|
2949
|
-
image_page image_path, image_opts
|
2950
|
-
rescue
|
2951
|
-
log :warn, %(could not embed #{face} cover image: #{image_path}; #{$!.message})
|
2952
|
-
end
|
2953
|
-
end
|
2684
|
+
def apply_subs_discretely doc, value, opts = {}
|
2685
|
+
if (imagesdir = opts[:imagesdir])
|
2686
|
+
imagesdir_to_restore = doc.attr 'imagesdir'
|
2687
|
+
doc.set_attr 'imagesdir', imagesdir
|
2954
2688
|
end
|
2955
|
-
|
2956
|
-
|
2957
|
-
|
2958
|
-
|
2959
|
-
|
2960
|
-
|
2961
|
-
|
2962
|
-
|
2963
|
-
|
2964
|
-
|
2965
|
-
|
2966
|
-
|
2967
|
-
(first_page_num.next..page_count).each do |num|
|
2968
|
-
go_to_page num
|
2969
|
-
stamp 'foreground-image' unless page.imported_page?
|
2689
|
+
# FIXME: get sub_attributes to handle drop-line w/o a warning
|
2690
|
+
doc.set_attr 'attribute-missing', 'skip' unless (attribute_missing = doc.attr 'attribute-missing') == 'skip'
|
2691
|
+
value = value.gsub '\{', '\\\\\\{' if (escaped_attr_ref = value.include? '\{')
|
2692
|
+
value = (subs = opts[:subs]) ? (doc.apply_subs value, subs) : (doc.apply_subs value)
|
2693
|
+
value = (value.split LF).delete_if {|line| SimpleAttributeRefRx.match? line }.join LF if opts[:drop_lines_with_unresolved_attributes] && (value.include? '{')
|
2694
|
+
value = value.gsub '\{', '{' if escaped_attr_ref
|
2695
|
+
doc.set_attr 'attribute-missing', attribute_missing unless attribute_missing == 'skip'
|
2696
|
+
if imagesdir
|
2697
|
+
if imagesdir_to_restore
|
2698
|
+
doc.set_attr 'imagesdir', imagesdir_to_restore
|
2699
|
+
else
|
2700
|
+
doc.remove_attr 'imagesdir'
|
2970
2701
|
end
|
2971
2702
|
end
|
2703
|
+
value
|
2972
2704
|
end
|
2973
2705
|
|
2974
|
-
def start_new_chapter chapter
|
2975
|
-
start_new_page unless at_page_top?
|
2976
|
-
# TODO: must call update_colors before advancing to next page if start_new_page is called in ink_chapter_title
|
2977
|
-
start_new_page if @ppbook && verso_page? && !(chapter.option? 'nonfacing')
|
2978
|
-
end
|
2979
|
-
|
2980
|
-
alias start_new_part start_new_chapter
|
2981
|
-
|
2982
2706
|
# Position the cursor for where to ink the specified section title or discrete heading node.
|
2983
2707
|
#
|
2984
2708
|
# This method computes whether there is enough room on the page to prevent the specified node
|
@@ -3001,12 +2725,10 @@ module Asciidoctor
|
|
3001
2725
|
end
|
3002
2726
|
if page == start_page
|
3003
2727
|
page.tare_content_stream
|
3004
|
-
orphaned = stop_if_first_page_empty
|
3005
|
-
node.context == :section ? (traverse node) : (convert (siblings = node.parent.blocks)[(siblings.index node) + 1])
|
3006
|
-
end
|
2728
|
+
orphaned = stop_if_first_page_empty { node.context == :section ? (traverse node) : (convert node.next_sibling) }
|
3007
2729
|
end
|
3008
2730
|
end
|
3009
|
-
|
2731
|
+
advance_page if orphaned
|
3010
2732
|
else
|
3011
2733
|
theme_font :heading, level: (hlevel = opts[:level]) do
|
3012
2734
|
h_padding_t, h_padding_r, h_padding_b, h_padding_l = expand_padding_value @theme[%(heading_h#{hlevel}_padding)]
|
@@ -3015,176 +2737,179 @@ module Asciidoctor
|
|
3015
2737
|
heading_h = (height_of_typeset_text title) +
|
3016
2738
|
(@theme[%(heading_h#{hlevel}_margin_top)] || @theme.heading_margin_top) +
|
3017
2739
|
(@theme[%(heading_h#{hlevel}_margin_bottom)] || @theme.heading_margin_bottom) + h_padding_t + h_padding_b
|
3018
|
-
if (min_height_after = @theme.heading_min_height_after) &&
|
3019
|
-
(node.context == :section ? node.blocks? : node != node.parent.blocks[-1])
|
2740
|
+
if (min_height_after = @theme.heading_min_height_after) && (node.context == :section ? node.blocks? : !node.last_child?)
|
3020
2741
|
heading_h += min_height_after
|
3021
2742
|
end
|
3022
2743
|
cursor >= heading_h
|
3023
2744
|
end
|
3024
|
-
|
2745
|
+
advance_page unless h_fits
|
3025
2746
|
end
|
3026
2747
|
end
|
3027
2748
|
nil
|
3028
2749
|
end
|
3029
2750
|
|
3030
|
-
|
3031
|
-
|
2751
|
+
# NOTE: only used when tabsize attribute is not specified
|
2752
|
+
# tabs must always be replaced with spaces in order for the indentation guards to work
|
2753
|
+
def expand_tabs string
|
2754
|
+
if string.nil_or_empty?
|
2755
|
+
''
|
2756
|
+
elsif string.include? TAB
|
2757
|
+
full_tab_space = ' ' * (tab_size = 4)
|
2758
|
+
(string.split LF, -1).map do |line|
|
2759
|
+
if line.empty? || !(tab_idx = line.index TAB)
|
2760
|
+
line
|
2761
|
+
else
|
2762
|
+
if tab_idx == 0
|
2763
|
+
leading_tabs = 0
|
2764
|
+
line.each_byte do |b|
|
2765
|
+
break unless b == 9
|
2766
|
+
leading_tabs += 1
|
2767
|
+
end
|
2768
|
+
line = %(#{full_tab_space * leading_tabs}#{rest = line.slice leading_tabs, line.length})
|
2769
|
+
next line unless rest.include? TAB
|
2770
|
+
end
|
2771
|
+
# keeps track of how many spaces were added to adjust offset in match data
|
2772
|
+
spaces_added = 0
|
2773
|
+
idx = 0
|
2774
|
+
result = ''
|
2775
|
+
line.each_char do |c|
|
2776
|
+
if c == TAB
|
2777
|
+
# calculate how many spaces this tab represents, then replace tab with spaces
|
2778
|
+
if (offset = idx + spaces_added) % tab_size == 0
|
2779
|
+
spaces_added += (tab_size - 1)
|
2780
|
+
result += full_tab_space
|
2781
|
+
else
|
2782
|
+
unless (spaces = tab_size - offset % tab_size) == 1
|
2783
|
+
spaces_added += (spaces - 1)
|
2784
|
+
end
|
2785
|
+
result += (' ' * spaces)
|
2786
|
+
end
|
2787
|
+
else
|
2788
|
+
result += c
|
2789
|
+
end
|
2790
|
+
idx += 1
|
2791
|
+
end
|
2792
|
+
result
|
2793
|
+
end
|
2794
|
+
end.join LF
|
2795
|
+
else
|
2796
|
+
string
|
2797
|
+
end
|
3032
2798
|
end
|
3033
2799
|
|
3034
|
-
|
3035
|
-
|
3036
|
-
|
3037
|
-
|
2800
|
+
# Extract callout marks from string, indexed by 0-based line number
|
2801
|
+
# Return an Array with the processed string as the first argument
|
2802
|
+
# and the mapping of lines to conums as the second.
|
2803
|
+
def extract_conums string
|
2804
|
+
conum_mapping = {}
|
2805
|
+
auto_num = 0
|
2806
|
+
string = (string.split LF).map.with_index do |line, line_num|
|
2807
|
+
# FIXME: we get extra spaces before numbers if more than one on a line
|
2808
|
+
if line.include? '<'
|
2809
|
+
line = line.gsub CalloutExtractRx do
|
2810
|
+
# honor the escape
|
2811
|
+
if $1 == ?\\
|
2812
|
+
$&.sub $1, ''
|
2813
|
+
else
|
2814
|
+
(conum_mapping[line_num] ||= []) << ($3 == '.' ? (auto_num += 1) : $3.to_i)
|
2815
|
+
''
|
2816
|
+
end
|
2817
|
+
end
|
2818
|
+
# NOTE: use first position to store space that precedes conums
|
2819
|
+
if (conum_mapping.key? line_num) && (line.end_with? ' ')
|
2820
|
+
trimmed_line = line.rstrip
|
2821
|
+
conum_mapping[line_num].unshift line.slice trimmed_line.length, line.length
|
2822
|
+
line = trimmed_line
|
2823
|
+
end
|
2824
|
+
end
|
2825
|
+
line
|
2826
|
+
end.join LF
|
2827
|
+
conum_mapping = nil if conum_mapping.empty?
|
2828
|
+
[string, conum_mapping]
|
3038
2829
|
end
|
3039
2830
|
|
3040
|
-
#
|
3041
|
-
|
3042
|
-
|
3043
|
-
|
3044
|
-
|
3045
|
-
|
3046
|
-
|
3047
|
-
|
3048
|
-
|
2831
|
+
# Restore the conums into the Array of formatted text fragments
|
2832
|
+
#--
|
2833
|
+
# QUESTION: can this be done more efficiently?
|
2834
|
+
# QUESTION: can we reuse arrange_fragments_by_line?
|
2835
|
+
def restore_conums fragments, conum_mapping, linenums = nil, highlight_lines = nil
|
2836
|
+
lines = []
|
2837
|
+
line_num = 0
|
2838
|
+
# reorganize the fragments into an array of lines
|
2839
|
+
fragments.each do |fragment|
|
2840
|
+
line = (lines[line_num] ||= [])
|
2841
|
+
if (text = fragment[:text]) == LF
|
2842
|
+
lines[line_num += 1] ||= []
|
2843
|
+
elsif text.include? LF
|
2844
|
+
(text.split LF, -1).each_with_index do |line_in_fragment, idx|
|
2845
|
+
line = (lines[line_num += 1] ||= []) unless idx == 0
|
2846
|
+
line << (fragment.merge text: line_in_fragment) unless line_in_fragment.empty?
|
3049
2847
|
end
|
3050
|
-
top_margin = 0
|
3051
2848
|
else
|
3052
|
-
|
2849
|
+
line << fragment
|
3053
2850
|
end
|
3054
2851
|
end
|
3055
|
-
|
3056
|
-
if (
|
3057
|
-
|
2852
|
+
conum_font_color = @theme.conum_font_color
|
2853
|
+
if (conum_font_name = @theme.conum_font_family) == font_name
|
2854
|
+
conum_font_name = nil
|
3058
2855
|
end
|
3059
|
-
|
3060
|
-
|
3061
|
-
|
3062
|
-
|
3063
|
-
|
3064
|
-
|
3065
|
-
|
3066
|
-
|
3067
|
-
|
3068
|
-
|
3069
|
-
|
3070
|
-
typeset_text string, (calc_line_metrics (opts.delete :line_height) || @base_line_height), {
|
3071
|
-
color: @font_color,
|
3072
|
-
inline_format: inline_format_opts,
|
3073
|
-
align: @base_text_align.to_sym,
|
3074
|
-
}.merge(opts)
|
2856
|
+
last_line_num = lines.size - 1
|
2857
|
+
if linenums
|
2858
|
+
pad_size = (last_line_num + 1).to_s.length
|
2859
|
+
linenum_color = @theme.code_linenum_font_color
|
2860
|
+
end
|
2861
|
+
# append conums to appropriate lines, then flatten to an array of fragments
|
2862
|
+
lines.flat_map.with_index do |line, cur_line_num|
|
2863
|
+
last_line = cur_line_num == last_line_num
|
2864
|
+
visible_line_num = cur_line_num + (linenums || 1)
|
2865
|
+
if highlight_lines && (highlight_bg_color = highlight_lines[visible_line_num])
|
2866
|
+
line.unshift text: DummyText, background_color: highlight_bg_color, highlight: true, inline_block: true, extend: true, width: 0, callback: [FormattedText::TextBackgroundAndBorderRenderer]
|
3075
2867
|
end
|
3076
|
-
|
3077
|
-
|
3078
|
-
|
3079
|
-
|
3080
|
-
|
3081
|
-
|
3082
|
-
|
2868
|
+
line.unshift text: %(#{visible_line_num.to_s.rjust pad_size} ), linenum: visible_line_num, color: linenum_color if linenums
|
2869
|
+
if conum_mapping && (conums = conum_mapping.delete cur_line_num)
|
2870
|
+
line << { text: conums.shift } if ::String === conums[0]
|
2871
|
+
conum_text = conums.map {|num| conum_glyph num }.join ' '
|
2872
|
+
conum_fragment = { text: conum_text }
|
2873
|
+
conum_fragment[:color] = conum_font_color if conum_font_color
|
2874
|
+
conum_fragment[:font] = conum_font_name if conum_font_name
|
2875
|
+
line << conum_fragment
|
3083
2876
|
end
|
3084
|
-
|
2877
|
+
line << { text: LF } unless last_line
|
2878
|
+
line
|
3085
2879
|
end
|
3086
2880
|
end
|
3087
2881
|
|
3088
|
-
|
3089
|
-
|
3090
|
-
@float_box = para_font_descender = para_font_size = end_cursor = nil
|
3091
|
-
if role_keys
|
3092
|
-
line_metrics = theme_font_cascade role_keys do
|
3093
|
-
para_font_descender = font.descender
|
3094
|
-
para_font_size = font_size
|
3095
|
-
calc_line_metrics @base_line_height
|
3096
|
-
end
|
3097
|
-
else
|
3098
|
-
para_font_descender = font.descender
|
3099
|
-
para_font_size = font_size
|
3100
|
-
line_metrics = calc_line_metrics @base_line_height
|
3101
|
-
end
|
3102
|
-
# allocate the space of at least one empty line below block
|
3103
|
-
line_height_length = line_metrics.height + line_metrics.leading + line_metrics.padding_top
|
3104
|
-
start_page_number = float_box[:page]
|
3105
|
-
start_cursor = cursor
|
3106
|
-
block_bottom = (float_box_bottom = float_box[:bottom]) + float_box[:gap][1]
|
3107
|
-
# use :at to incorporate padding top from line metrics
|
3108
|
-
# use :final_gap to incorporate padding bottom from line metrics
|
3109
|
-
# use :draw_text_callback to track end cursor (requires applying :final_gap to result manually)
|
3110
|
-
prose_opts.update \
|
3111
|
-
at: [float_box[:left], start_cursor - line_metrics.padding_top],
|
3112
|
-
width: float_box[:width],
|
3113
|
-
height: [cursor, float_box[:height] - (float_box[:top] - start_cursor) + line_height_length].min,
|
3114
|
-
final_gap: para_font_descender + line_metrics.padding_bottom,
|
3115
|
-
draw_text_callback: (proc do |text, opts|
|
3116
|
-
draw_text! text, opts
|
3117
|
-
end_cursor = opts[:at][1] # does not include :final_gap value
|
3118
|
-
end)
|
3119
|
-
overflow_text = role_keys ?
|
3120
|
-
theme_font_cascade(role_keys) { ink_prose node.content, prose_opts } :
|
3121
|
-
(ink_prose node.content, prose_opts)
|
3122
|
-
move_cursor_to end_cursor -= prose_opts[:final_gap] if end_cursor # ink_prose with :height does not move cursor
|
3123
|
-
if overflow_text.empty?
|
3124
|
-
if block_next && (supports_float_wrapping? block_next)
|
3125
|
-
insert_margin_bottom.call
|
3126
|
-
@float_box = float_box if page_number == start_page_number && cursor > start_cursor - prose_opts[:height]
|
3127
|
-
elsif end_cursor > block_bottom
|
3128
|
-
move_cursor_to block_bottom
|
3129
|
-
theme_margin :block, :bottom, block_next
|
3130
|
-
else
|
3131
|
-
insert_margin_bottom.call
|
3132
|
-
end
|
3133
|
-
else
|
3134
|
-
overflow_prose_opts = { align: prose_opts[:align] || @base_text_align.to_sym }
|
3135
|
-
unless end_cursor
|
3136
|
-
overflow_prose_opts[:indent_paragraphs] = prose_opts[:indent_paragraphs]
|
3137
|
-
move_cursor_to float_box_bottom if start_cursor > float_box_bottom
|
3138
|
-
end
|
3139
|
-
role_keys ?
|
3140
|
-
theme_font_cascade(role_keys) { typeset_formatted_text overflow_text, line_metrics, overflow_prose_opts } :
|
3141
|
-
(typeset_formatted_text overflow_text, line_metrics, overflow_prose_opts)
|
3142
|
-
insert_margin_bottom.call
|
3143
|
-
end
|
2882
|
+
def fallback_svg_font_name
|
2883
|
+
@theme.svg_fallback_font_family || @theme.svg_font_family || @theme.base_font_family
|
3144
2884
|
end
|
3145
2885
|
|
3146
|
-
#
|
3147
|
-
|
3148
|
-
|
3149
|
-
|
3150
|
-
|
3151
|
-
string
|
3152
|
-
end
|
3153
|
-
string = hyphenate_text string, @hyphenator if (opts.delete :hyphenate) && (defined? @hyphenator)
|
3154
|
-
# NOTE: used by extensions; ensures linked text gets formatted using the link styles
|
3155
|
-
if (anchor = opts.delete :anchor)
|
3156
|
-
string = anchor == true ? %(<a>#{string}</a>) : %(<a anchor="#{anchor}">#{string}</a>)
|
3157
|
-
end
|
3158
|
-
margin_top top_margin
|
3159
|
-
# NOTE: normalize makes endlines soft (replaces "\n" with ' ')
|
3160
|
-
inline_format_opts = { normalize: (opts.delete :normalize) != false }
|
3161
|
-
if (styles = opts.delete :styles)
|
3162
|
-
inline_format_opts[:inherited] = {
|
3163
|
-
styles: styles,
|
3164
|
-
text_decoration_color: (opts.delete :text_decoration_color),
|
3165
|
-
text_decoration_width: (opts.delete :text_decoration_width),
|
3166
|
-
}.compact
|
2886
|
+
# Add an indentation guard at the start of indented lines.
|
2887
|
+
# Expand tabs to spaces if tabs are present
|
2888
|
+
def guard_indentation string
|
2889
|
+
unless (string = expand_tabs string).empty?
|
2890
|
+
string[0] = GuardedIndent if string.start_with? ' '
|
2891
|
+
string.gsub! InnerIndent, GuardedInnerIndent if string.include? InnerIndent
|
3167
2892
|
end
|
3168
|
-
|
3169
|
-
color: @font_color,
|
3170
|
-
inline_format: [inline_format_opts],
|
3171
|
-
align: @base_text_align.to_sym,
|
3172
|
-
}.merge(opts)
|
3173
|
-
margin_bottom bot_margin
|
3174
|
-
result
|
2893
|
+
string
|
3175
2894
|
end
|
3176
2895
|
|
3177
|
-
def
|
3178
|
-
|
3179
|
-
|
3180
|
-
|
2896
|
+
def guard_indentation_in_fragments fragments
|
2897
|
+
start_of_line = true
|
2898
|
+
fragments.each do |fragment|
|
2899
|
+
next if (text = fragment[:text]).empty?
|
2900
|
+
if start_of_line && (text.start_with? ' ')
|
2901
|
+
fragment[:text] = GuardedIndent + (((text = text.slice 1, text.length).include? InnerIndent) ? (text.gsub InnerIndent, GuardedInnerIndent) : text)
|
2902
|
+
elsif text.include? InnerIndent
|
2903
|
+
fragment[:text] = text.gsub InnerIndent, GuardedInnerIndent
|
2904
|
+
end
|
2905
|
+
start_of_line = text.end_with? LF
|
3181
2906
|
end
|
3182
|
-
|
3183
|
-
|
3184
|
-
|
3185
|
-
|
3186
|
-
|
3187
|
-
|
2907
|
+
fragments
|
2908
|
+
end
|
2909
|
+
|
2910
|
+
def height_of_typeset_text string, opts = {}
|
2911
|
+
line_metrics = (calc_line_metrics opts[:line_height] || @base_line_height)
|
2912
|
+
(height_of string, leading: line_metrics.leading, final_gap: line_metrics.final_gap) + line_metrics.padding_top + (opts[:single_line] ? 0 : line_metrics.padding_bottom)
|
3188
2913
|
end
|
3189
2914
|
|
3190
2915
|
# Render the caption in the current document. If the dry_run option is true, return the height.
|
@@ -3277,7 +3002,7 @@ module Asciidoctor
|
|
3277
3002
|
end
|
3278
3003
|
unless scratch? || !(bg_color = @theme[%(#{category_caption}_background_color)] || @theme.caption_background_color)
|
3279
3004
|
caption_height = height_of_typeset_text string
|
3280
|
-
fill_at = [
|
3005
|
+
fill_at = [bounds.left, cursor + (margin[:top] || 0)]
|
3281
3006
|
float { bounding_box(fill_at, width: container_width, height: caption_height) { fill_bounds bg_color } }
|
3282
3007
|
end
|
3283
3008
|
indent(*indent_by) do
|
@@ -3299,389 +3024,148 @@ module Asciidoctor
|
|
3299
3024
|
ink_caption node, category: :table, end: end_, block_align: table_alignment, block_width: table_width, max_width: max_width
|
3300
3025
|
end
|
3301
3026
|
|
3302
|
-
def
|
3303
|
-
|
3304
|
-
to_page = nil
|
3305
|
-
extent = dry_run onto: self do
|
3306
|
-
to_page = (ink_toc doc, toc_num_levels, toc_start_page_number, toc_start_cursor).end
|
3307
|
-
margin_bottom @theme.block_margin_bottom unless title_page_on
|
3308
|
-
end
|
3309
|
-
# NOTE: patch for custom converters that allocate extra TOC pages without actually creating them
|
3310
|
-
if to_page > extent.to.page
|
3311
|
-
extent.to.page = to_page
|
3312
|
-
extent.to.cursor = bounds.height
|
3313
|
-
end
|
3314
|
-
# NOTE: reserve pages for the toc; leaves cursor on page after last page in toc
|
3315
|
-
if title_page_on
|
3316
|
-
extent.each_page { start_new_page }
|
3317
|
-
else
|
3318
|
-
extent.each_page {|first_page| start_new_page unless first_page }
|
3319
|
-
move_cursor_to extent.to.cursor
|
3320
|
-
end
|
3321
|
-
extent
|
3027
|
+
def ink_chapter_title node, title, opts = {}
|
3028
|
+
ink_general_heading node, title, (opts.merge outdent: true)
|
3322
3029
|
end
|
3323
3030
|
|
3324
|
-
|
3325
|
-
node.sections
|
3326
|
-
end
|
3031
|
+
alias ink_part_title ink_chapter_title
|
3327
3032
|
|
3328
|
-
|
3329
|
-
|
3330
|
-
|
3331
|
-
|
3332
|
-
|
3333
|
-
|
3334
|
-
|
3335
|
-
|
3336
|
-
|
3033
|
+
def ink_cover_page doc, face
|
3034
|
+
image_path, image_opts = resolve_background_image doc, @theme, %(#{face}-cover-image), theme_key: %(cover_#{face}_image).to_sym, symbolic_paths: ['', '~']
|
3035
|
+
if image_path
|
3036
|
+
if image_path.empty?
|
3037
|
+
go_to_page page_count if face == :back
|
3038
|
+
start_new_page_discretely
|
3039
|
+
# NOTE: open graphics state to prevent page from being reused
|
3040
|
+
open_graphics_state if face == :front
|
3041
|
+
return
|
3042
|
+
elsif image_path == '~'
|
3043
|
+
@page_margin_by_side[:cover] = @page_margin_by_side[:recto] if @media == 'prepress'
|
3044
|
+
return
|
3337
3045
|
end
|
3338
|
-
|
3339
|
-
|
3340
|
-
|
3341
|
-
|
3342
|
-
|
3343
|
-
|
3344
|
-
|
3046
|
+
|
3047
|
+
go_to_page page_count if face == :back
|
3048
|
+
if image_opts[:format] == 'pdf'
|
3049
|
+
import_page image_path, (image_opts.merge advance: face != :back, advance_if_missing: false)
|
3050
|
+
else
|
3051
|
+
begin
|
3052
|
+
image_page image_path, image_opts
|
3053
|
+
rescue
|
3054
|
+
log :warn, %(could not embed #{face} cover image: #{image_path}; #{$!.message})
|
3345
3055
|
end
|
3346
|
-
font_size @theme.toc_dot_leader_font_size
|
3347
|
-
{
|
3348
|
-
font_color: @theme.toc_dot_leader_font_color || @font_color,
|
3349
|
-
font_style: dot_leader_font_style,
|
3350
|
-
font_size: font_size,
|
3351
|
-
levels: ((dot_leader_l = @theme.toc_dot_leader_levels) == 'none' ? ::Set.new :
|
3352
|
-
(dot_leader_l && dot_leader_l != 'all' ? dot_leader_l.to_s.split.map(&:to_i).to_set : (0..num_levels).to_set)),
|
3353
|
-
text: (dot_leader_text = @theme.toc_dot_leader_content || DotLeaderTextDefault),
|
3354
|
-
width: dot_leader_text.empty? ? 0 : (rendered_width_of_string dot_leader_text),
|
3355
|
-
# TODO: spacer gives a little bit of room between dots and page number
|
3356
|
-
spacer: { text: NoBreakSpace, size: (spacer_font_size = @font_size * 0.25) },
|
3357
|
-
spacer_width: (rendered_width_of_char NoBreakSpace, size: spacer_font_size),
|
3358
|
-
}
|
3359
3056
|
end
|
3360
|
-
theme_margin :toc, :top
|
3361
|
-
ink_toc_level (get_entries_for_toc doc), num_levels, dot_leader, num_front_matter_pages
|
3362
3057
|
end
|
3363
|
-
# NOTE: range must be calculated relative to toc_page_number; absolute page number in scratch document is arbitrary
|
3364
|
-
toc_page_numbers = (toc_page_number..(toc_page_number + (page_number - start_page_number)))
|
3365
|
-
go_to_page page_count unless scratch?
|
3366
|
-
toc_page_numbers
|
3367
3058
|
end
|
3368
3059
|
|
3369
|
-
|
3370
|
-
|
3371
|
-
|
3372
|
-
|
3373
|
-
|
3374
|
-
|
3375
|
-
|
3376
|
-
|
3377
|
-
|
3378
|
-
|
3379
|
-
|
3380
|
-
|
3381
|
-
|
3382
|
-
|
3383
|
-
|
3384
|
-
|
3385
|
-
|
3386
|
-
|
3387
|
-
|
3388
|
-
end
|
3389
|
-
else
|
3390
|
-
if !(physical_pgnum = entry.attr 'pdf-page-start') &&
|
3391
|
-
(target_page_ref = (get_dest entry_anchor)&.first) &&
|
3392
|
-
(target_page_idx = state.pages.index {|candidate| candidate.dictionary == target_page_ref })
|
3393
|
-
physical_pgnum = target_page_idx + 1
|
3394
|
-
end
|
3395
|
-
if physical_pgnum
|
3396
|
-
virtual_pgnum = physical_pgnum - num_front_matter_pages
|
3397
|
-
pgnum_label = (virtual_pgnum < 1 ? (RomanNumeral.new physical_pgnum, :lower) : virtual_pgnum).to_s
|
3398
|
-
else
|
3399
|
-
pgnum_label = '?'
|
3400
|
-
end
|
3401
|
-
start_page_number = page_number
|
3402
|
-
start_cursor = cursor
|
3403
|
-
start_dots = nil
|
3404
|
-
entry_title_inherited = (apply_text_decoration ::Set.new, :toc, entry_level).merge anchor: entry_anchor, color: @font_color
|
3405
|
-
# NOTE: use text formatter to add anchor overlay to avoid using inline format with synthetic anchor tag
|
3406
|
-
entry_title_fragments = text_formatter.format entry_title, inherited: entry_title_inherited
|
3407
|
-
line_metrics = calc_line_metrics @base_line_height
|
3408
|
-
indent 0, pgnum_label_placeholder_width do
|
3409
|
-
(entry_title_fragments[-1][:callback] ||= []) << (last_fragment_pos = ::Asciidoctor::PDF::FormattedText::FragmentPositionRenderer.new)
|
3410
|
-
typeset_formatted_text entry_title_fragments, line_metrics, hanging_indent: hanging_indent, normalize_line_height: true
|
3411
|
-
start_dots = last_fragment_pos.right + hanging_indent
|
3412
|
-
last_fragment_cursor = last_fragment_pos.top + line_metrics.padding_top
|
3413
|
-
start_cursor = last_fragment_cursor if last_fragment_pos.page_number > start_page_number || (start_cursor - last_fragment_cursor) > line_metrics.height
|
3414
|
-
end
|
3415
|
-
end_cursor = cursor
|
3416
|
-
move_cursor_to start_cursor
|
3417
|
-
# NOTE: we're guaranteed to be on the same page as the final line of the entry
|
3418
|
-
if dot_leader[:width] > 0 && (dot_leader[:levels].include? entry_level.pred)
|
3419
|
-
pgnum_label_width = rendered_width_of_string pgnum_label
|
3420
|
-
pgnum_label_font_settings = { color: @font_color, font: font_family, size: @font_size, styles: font_styles }
|
3421
|
-
save_font do
|
3422
|
-
# NOTE: the same font is used for dot leaders throughout toc
|
3423
|
-
set_font toc_font_info[:font], dot_leader[:font_size]
|
3424
|
-
font_style dot_leader[:font_style]
|
3425
|
-
num_dots = [((bounds.width - start_dots - dot_leader[:spacer_width] - pgnum_label_width) / dot_leader[:width]).floor, 0].max
|
3426
|
-
# FIXME: dots don't line up in columns if width of page numbers differ
|
3427
|
-
typeset_formatted_text [
|
3428
|
-
{ text: dot_leader[:text] * num_dots, color: dot_leader[:font_color] },
|
3429
|
-
dot_leader[:spacer],
|
3430
|
-
({ text: pgnum_label, anchor: entry_anchor }.merge pgnum_label_font_settings),
|
3431
|
-
], line_metrics, align: :right
|
3432
|
-
end
|
3433
|
-
else
|
3434
|
-
typeset_formatted_text [{ text: pgnum_label, color: @font_color, anchor: entry_anchor }], line_metrics, align: :right
|
3060
|
+
# QUESTION: if a footnote ref appears in a separate chapter, should the footnote def be duplicated?
|
3061
|
+
def ink_footnotes node
|
3062
|
+
return if (fns = (doc = node.document).footnotes - @rendered_footnotes).empty?
|
3063
|
+
theme_margin :block, :bottom if node.context == :document || node == node.document.last_child
|
3064
|
+
theme_margin :footnotes, :top
|
3065
|
+
with_dry_run do |extent|
|
3066
|
+
if (single_page_height = extent&.single_page_height) && (delta = cursor - single_page_height - 0.0001) > 0
|
3067
|
+
move_down delta
|
3068
|
+
end
|
3069
|
+
theme_font :footnotes do
|
3070
|
+
(title = doc.attr 'footnotes-title') && (ink_caption title, category: :footnotes)
|
3071
|
+
item_spacing = @theme.footnotes_item_spacing
|
3072
|
+
index_offset = @rendered_footnotes.length
|
3073
|
+
sect_xreftext = node.context == :section && (node.xreftext node.document.attr 'xrefstyle')
|
3074
|
+
fns.each do |fn|
|
3075
|
+
label = (index = fn.index) - index_offset
|
3076
|
+
if sect_xreftext
|
3077
|
+
fn.singleton_class.send :attr_accessor, :label unless fn.respond_to? :label=
|
3078
|
+
fn.label = %(#{label} - #{sect_xreftext})
|
3435
3079
|
end
|
3436
|
-
|
3080
|
+
ink_prose %(<a id="_footnotedef_#{index}">#{DummyText}</a>[<a anchor="_footnoteref_#{index}">#{label}</a>] #{fn.text}), margin_bottom: item_spacing, hyphenate: true
|
3437
3081
|
end
|
3082
|
+
@rendered_footnotes += fns if extent
|
3438
3083
|
end
|
3439
|
-
indent @theme.toc_indent do
|
3440
|
-
ink_toc_level (get_entries_for_toc entry), num_levels_for_entry, dot_leader, num_front_matter_pages
|
3441
|
-
end if num_levels_for_entry >= entry_level
|
3442
3084
|
end
|
3085
|
+
nil
|
3443
3086
|
end
|
3444
3087
|
|
3445
|
-
|
3446
|
-
|
3447
|
-
def fit_icon_to_bounds preferred_size
|
3448
|
-
(max_height = bounds.height) < preferred_size ? max_height : preferred_size
|
3088
|
+
def ink_general_heading _node, title, opts = {}
|
3089
|
+
ink_heading title, opts
|
3449
3090
|
end
|
3450
3091
|
|
3451
|
-
|
3452
|
-
|
3453
|
-
|
3454
|
-
|
3455
|
-
|
3456
|
-
|
3457
|
-
|
3092
|
+
# NOTE: ink_heading doesn't set the theme font because it's used for various types of headings
|
3093
|
+
def ink_heading string, opts = {}
|
3094
|
+
if (h_level = opts[:level])
|
3095
|
+
h_category = %(heading_h#{h_level})
|
3096
|
+
end
|
3097
|
+
unless (top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top))
|
3098
|
+
if at_page_top?
|
3099
|
+
if h_category && (top_margin = @theme[%(#{h_category}_margin_page_top)] || @theme.heading_margin_page_top) > 0
|
3100
|
+
move_down top_margin
|
3458
3101
|
end
|
3102
|
+
top_margin = 0
|
3459
3103
|
else
|
3460
|
-
|
3104
|
+
top_margin = (h_category ? @theme[%(#{h_category}_margin_top)] : nil) || @theme.heading_margin_top
|
3461
3105
|
end
|
3462
|
-
else
|
3463
|
-
(icon_data = AdmonitionIcons[key] || {})[:name] ||= AdmonitionIcons[:note][:name]
|
3464
3106
|
end
|
3465
|
-
|
3466
|
-
|
3467
|
-
|
3468
|
-
|
3469
|
-
|
3470
|
-
|
3471
|
-
|
3472
|
-
|
3473
|
-
|
3474
|
-
|
3475
|
-
|
3476
|
-
|
3477
|
-
|
3478
|
-
# FIXME: probably need to treat doctypes differently
|
3479
|
-
is_book = doc.doctype == 'book'
|
3480
|
-
header = doc.header? ? doc.header : nil
|
3481
|
-
sectlevels = (@theme[%(#{periphery}_sectlevels)] || 2).to_i
|
3482
|
-
sections = doc.find_by(context: :section) {|sect| sect.level <= sectlevels && sect != header }
|
3483
|
-
toc_title = (doc.attr 'toc-title').to_s if (toc_page_nums = @toc_extent&.page_range)
|
3484
|
-
disable_on_pages = @disable_running_content[periphery]
|
3485
|
-
|
3486
|
-
title_method = TitleStyles[@theme[%(#{periphery}_title_style)]]
|
3487
|
-
# FIXME: we need a proper model for all this page counting
|
3488
|
-
# FIXME: we make a big assumption that part & chapter start on new pages
|
3489
|
-
# index parts, chapters and sections by the physical page number on which they start
|
3490
|
-
part_start_pages = {}
|
3491
|
-
chapter_start_pages = {}
|
3492
|
-
section_start_pages = {}
|
3493
|
-
trailing_section_start_pages = {}
|
3494
|
-
sections.each do |sect|
|
3495
|
-
pgnum = (sect.attr 'pdf-page-start').to_i
|
3496
|
-
if is_book && ((sect_is_part = sect.sectname == 'part') || sect.level == 1)
|
3497
|
-
if sect_is_part
|
3498
|
-
part_start_pages[pgnum] ||= sect
|
3107
|
+
bot_margin = margin || (opts.delete :margin_bottom) || (h_category ? @theme[%(#{h_category}_margin_bottom)] : nil) || @theme.heading_margin_bottom
|
3108
|
+
if (transform = resolve_text_transform opts)
|
3109
|
+
string = transform_text string, transform
|
3110
|
+
end
|
3111
|
+
outdent_section opts.delete :outdent do
|
3112
|
+
margin_top top_margin
|
3113
|
+
start_cursor = cursor
|
3114
|
+
start_page_number = page_number
|
3115
|
+
pad_box h_category ? @theme[%(#{h_category}_padding)] : nil do
|
3116
|
+
# QUESTION: should we move inherited styles to typeset_text?
|
3117
|
+
if (inherited = apply_text_decoration font_styles, :heading, h_level).empty?
|
3118
|
+
inline_format_opts = true
|
3499
3119
|
else
|
3500
|
-
|
3501
|
-
# FIXME: need a better way to indicate that part has ended
|
3502
|
-
part_start_pages[pgnum] = '' if sect.sectname == 'appendix' && !part_start_pages.empty?
|
3120
|
+
inline_format_opts = [{ inherited: inherited }]
|
3503
3121
|
end
|
3504
|
-
|
3505
|
-
|
3506
|
-
|
3507
|
-
|
3508
|
-
|
3509
|
-
|
3510
|
-
# index parts, chapters, and sections by the physical page number on which they appear
|
3511
|
-
parts_by_page = SectionInfoByPage.new title_method
|
3512
|
-
chapters_by_page = SectionInfoByPage.new title_method
|
3513
|
-
sections_by_page = SectionInfoByPage.new title_method
|
3514
|
-
# QUESTION: should the default part be the doctitle?
|
3515
|
-
last_part = nil
|
3516
|
-
# QUESTION: should we enforce that the preamble is a preface?
|
3517
|
-
last_chap = is_book ? :pre : nil
|
3518
|
-
last_sect = nil
|
3519
|
-
sect_search_threshold = 1
|
3520
|
-
(1..num_pages).each do |pgnum|
|
3521
|
-
if (part = part_start_pages[pgnum])
|
3522
|
-
last_part = part
|
3523
|
-
last_chap = nil
|
3524
|
-
last_sect = nil
|
3525
|
-
end
|
3526
|
-
if (chap = chapter_start_pages[pgnum])
|
3527
|
-
last_chap = chap
|
3528
|
-
last_sect = nil
|
3122
|
+
typeset_text string, (calc_line_metrics (opts.delete :line_height) || @base_line_height), {
|
3123
|
+
color: @font_color,
|
3124
|
+
inline_format: inline_format_opts,
|
3125
|
+
align: @base_text_align.to_sym,
|
3126
|
+
}.merge(opts)
|
3529
3127
|
end
|
3530
|
-
if
|
3531
|
-
|
3532
|
-
|
3533
|
-
|
3534
|
-
|
3535
|
-
elsif last_sect
|
3536
|
-
(sect_search_threshold..(pgnum - 1)).reverse_each do |prev|
|
3537
|
-
if (sect = trailing_section_start_pages[prev])
|
3538
|
-
last_sect = sect
|
3539
|
-
break
|
3128
|
+
if h_category && @theme[%(#{h_category}_border_width)] &&
|
3129
|
+
(@theme[%(#{h_category}_border_color)] || @theme.base_border_color) && page_number == start_page_number
|
3130
|
+
float do
|
3131
|
+
bounding_box [bounds.left, start_cursor], width: bounds.width, height: start_cursor - cursor do
|
3132
|
+
theme_fill_and_stroke_bounds h_category
|
3540
3133
|
end
|
3541
3134
|
end
|
3542
3135
|
end
|
3543
|
-
|
3544
|
-
if toc_page_nums&.cover? pgnum
|
3545
|
-
if is_book
|
3546
|
-
chapters_by_page[pgnum] = toc_title
|
3547
|
-
sections_by_page[pgnum] = nil
|
3548
|
-
else
|
3549
|
-
chapters_by_page[pgnum] = nil
|
3550
|
-
sections_by_page[pgnum] = section_start_pages[pgnum] || toc_title
|
3551
|
-
end
|
3552
|
-
toc_page_nums = nil if toc_page_nums.end == pgnum
|
3553
|
-
elsif last_chap == :pre
|
3554
|
-
chapters_by_page[pgnum] = pgnum < body_start_page_number ? doc.doctitle : (doc.attr 'preface-title', 'Preface')
|
3555
|
-
sections_by_page[pgnum] = last_sect
|
3556
|
-
else
|
3557
|
-
chapters_by_page[pgnum] = last_chap
|
3558
|
-
sections_by_page[pgnum] = last_sect
|
3559
|
-
end
|
3136
|
+
margin_bottom bot_margin
|
3560
3137
|
end
|
3138
|
+
end
|
3561
3139
|
|
3562
|
-
|
3563
|
-
|
3564
|
-
|
3565
|
-
|
3566
|
-
|
3567
|
-
|
3568
|
-
|
3569
|
-
pagenums_enabled = doc.attr? 'pagenums'
|
3570
|
-
periphery_layout_cache = {}
|
3571
|
-
# NOTE: this block is invoked during PDF generation, after #write -> #render_file and thus after #convert_document
|
3572
|
-
repeat (content_start_page_number..num_pages), dynamic: true do
|
3573
|
-
pgnum = page_number
|
3574
|
-
# NOTE: don't write on pages which are imported / inserts (otherwise we can get a corrupt PDF)
|
3575
|
-
next if page.imported_page? || (disable_on_pages.include? pgnum)
|
3576
|
-
virtual_pgnum = pgnum - skip_pagenums
|
3577
|
-
pgnum_label = (virtual_pgnum < 1 ? (RomanNumeral.new pgnum, :lower) : virtual_pgnum).to_s
|
3578
|
-
side = page_side((@folio_placement[:basis] == :physical ? pgnum : virtual_pgnum), @folio_placement[:inverted])
|
3579
|
-
doc.set_attr 'page-layout', page.layout.to_s
|
3580
|
-
|
3581
|
-
# NOTE: running content is cached per page layout
|
3582
|
-
# QUESTION: should allocation be per side?
|
3583
|
-
trim_styles, colspec_dict, content_dict, stamp_names = allocate_running_content_layout doc, page, periphery, periphery_layout_cache
|
3584
|
-
# FIXME: we need to have a content setting for chapter pages
|
3585
|
-
content_by_position, colspec_by_position = content_dict[side], colspec_dict[side]
|
3586
|
-
|
3587
|
-
doc.set_attr 'page-number', pgnum_label if pagenums_enabled
|
3588
|
-
# QUESTION: should the fallback value be nil instead of empty string? or should we remove attribute if no value?
|
3589
|
-
doc.set_attr 'part-title', ((part_info = parts_by_page[pgnum])[:title] || '')
|
3590
|
-
if (part_numeral = part_info[:numeral])
|
3591
|
-
doc.set_attr 'part-numeral', part_numeral
|
3592
|
-
else
|
3593
|
-
doc.remove_attr 'part-numeral'
|
3594
|
-
end
|
3595
|
-
doc.set_attr 'chapter-title', ((chap_info = chapters_by_page[pgnum])[:title] || '')
|
3596
|
-
if (chap_numeral = chap_info[:numeral])
|
3597
|
-
doc.set_attr 'chapter-numeral', chap_numeral
|
3598
|
-
else
|
3599
|
-
doc.remove_attr 'chapter-numeral'
|
3600
|
-
end
|
3601
|
-
doc.set_attr 'section-title', ((sect_info = sections_by_page[pgnum])[:title] || '')
|
3602
|
-
doc.set_attr 'section-or-chapter-title', (sect_info[:title] || chap_info[:title] || '')
|
3603
|
-
|
3604
|
-
stamp stamp_names[side] if stamp_names
|
3605
|
-
|
3606
|
-
theme_font periphery do
|
3607
|
-
canvas do
|
3608
|
-
bounding_box [trim_styles[:content_left][side], trim_styles[:top][side]], width: trim_styles[:content_width][side], height: trim_styles[:height] do
|
3609
|
-
if trim_styles[:column_rule_color] && (trim_column_rule_width = trim_styles[:column_rule_width]) > 0
|
3610
|
-
trim_column_rule_spacing = trim_styles[:column_rule_spacing]
|
3611
|
-
else
|
3612
|
-
trim_column_rule_width = nil
|
3613
|
-
end
|
3614
|
-
prev_position = nil
|
3615
|
-
ColumnPositions.each do |position|
|
3616
|
-
next unless (content = content_by_position[position])
|
3617
|
-
next unless (colspec = colspec_by_position[position])[:width] > 0
|
3618
|
-
left, colwidth = colspec[:x], colspec[:width]
|
3619
|
-
if trim_column_rule_width && colwidth < bounds.width
|
3620
|
-
if (trim_column_rule = prev_position)
|
3621
|
-
left += (trim_column_rule_spacing * 0.5)
|
3622
|
-
colwidth -= trim_column_rule_spacing
|
3623
|
-
else
|
3624
|
-
colwidth -= (trim_column_rule_spacing * 0.5)
|
3625
|
-
end
|
3626
|
-
end
|
3627
|
-
# FIXME: we need to have a content setting for chapter pages
|
3628
|
-
if ::Array === content
|
3629
|
-
redo_with_content = nil
|
3630
|
-
# NOTE: float ensures cursor position is restored and returns us to current page if we overrun
|
3631
|
-
float do
|
3632
|
-
# NOTE: bounding_box is redundant if both vertical padding and border width are 0
|
3633
|
-
bounding_box [left, bounds.top - trim_styles[:padding][side][0] - trim_styles[:content_offset]], width: colwidth, height: trim_styles[:content_height][side] do
|
3634
|
-
# NOTE: image vposition respects padding; use negative image_vertical_align value to revert
|
3635
|
-
image_opts = content[1].merge position: colspec[:align], vposition: trim_styles[:img_valign]
|
3636
|
-
begin
|
3637
|
-
image_info = image content[0], image_opts
|
3638
|
-
if (image_link = content[2])
|
3639
|
-
image_info = { width: image_info.scaled_width, height: image_info.scaled_height } unless image_opts[:format] == 'svg'
|
3640
|
-
add_link_to_image image_link, image_info, image_opts
|
3641
|
-
end
|
3642
|
-
rescue
|
3643
|
-
redo_with_content = image_opts[:alt]
|
3644
|
-
log :warn, %(could not embed image in running content: #{content[0]}; #{$!.message})
|
3645
|
-
end
|
3646
|
-
end
|
3647
|
-
end
|
3648
|
-
if redo_with_content
|
3649
|
-
content_by_position[position] = redo_with_content
|
3650
|
-
redo
|
3651
|
-
end
|
3652
|
-
else
|
3653
|
-
theme_font %(#{periphery}_#{side}_#{position}) do
|
3654
|
-
# NOTE: minor optimization
|
3655
|
-
if content == '{page-number}'
|
3656
|
-
content = pagenums_enabled ? pgnum_label : nil
|
3657
|
-
else
|
3658
|
-
content = apply_subs_discretely doc, content, drop_lines_with_unresolved_attributes: true, imagesdir: @themesdir
|
3659
|
-
content = transform_text content, @text_transform if @text_transform
|
3660
|
-
end
|
3661
|
-
formatted_text_box (parse_text content, inline_format: [normalize: true]),
|
3662
|
-
at: [left, bounds.top - trim_styles[:padding][side][0] - trim_styles[:content_offset] + ((Array trim_styles[:valign])[0] == :center ? font.descender * 0.5 : 0)],
|
3663
|
-
color: @font_color,
|
3664
|
-
width: colwidth,
|
3665
|
-
height: trim_styles[:prose_content_height][side],
|
3666
|
-
align: colspec[:align],
|
3667
|
-
valign: trim_styles[:valign],
|
3668
|
-
leading: trim_styles[:line_metrics].leading,
|
3669
|
-
final_gap: false,
|
3670
|
-
overflow: :truncate
|
3671
|
-
end
|
3672
|
-
end
|
3673
|
-
bounding_box [colspec[:x], bounds.top - trim_styles[:padding][side][0] - trim_styles[:content_offset]], width: colspec[:width], height: trim_styles[:content_height][side] do
|
3674
|
-
stroke_vertical_rule trim_styles[:column_rule_color], at: bounds.left, line_style: trim_styles[:column_rule_style], line_width: trim_column_rule_width
|
3675
|
-
end if trim_column_rule
|
3676
|
-
prev_position = position
|
3677
|
-
end
|
3678
|
-
end
|
3679
|
-
end
|
3680
|
-
end
|
3140
|
+
# NOTE: inline_format is true by default
|
3141
|
+
def ink_prose string, opts = {}
|
3142
|
+
top_margin = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || 0
|
3143
|
+
bot_margin = margin || (opts.delete :margin_bottom) || @theme.prose_margin_bottom
|
3144
|
+
if (transform = resolve_text_transform opts)
|
3145
|
+
string = transform_text string, transform
|
3681
3146
|
end
|
3682
|
-
|
3683
|
-
|
3684
|
-
|
3147
|
+
string = hyphenate_text string, @hyphenator if (opts.delete :hyphenate) && (defined? @hyphenator)
|
3148
|
+
# NOTE: used by extensions; ensures linked text gets formatted using the link styles
|
3149
|
+
if (anchor = opts.delete :anchor)
|
3150
|
+
string = anchor == true ? %(<a>#{string}</a>) : %(<a anchor="#{anchor}">#{string}</a>)
|
3151
|
+
end
|
3152
|
+
margin_top top_margin
|
3153
|
+
# NOTE: normalize makes endlines soft (replaces "\n" with ' ')
|
3154
|
+
inline_format_opts = { normalize: (opts.delete :normalize) != false }
|
3155
|
+
if (styles = opts.delete :styles)
|
3156
|
+
inline_format_opts[:inherited] = {
|
3157
|
+
styles: styles,
|
3158
|
+
text_decoration_color: (opts.delete :text_decoration_color),
|
3159
|
+
text_decoration_width: (opts.delete :text_decoration_width),
|
3160
|
+
}.compact
|
3161
|
+
end
|
3162
|
+
result = typeset_text string, (calc_line_metrics (opts.delete :line_height) || @base_line_height), {
|
3163
|
+
color: @font_color,
|
3164
|
+
inline_format: [inline_format_opts],
|
3165
|
+
align: @base_text_align.to_sym,
|
3166
|
+
}.merge(opts)
|
3167
|
+
margin_bottom bot_margin
|
3168
|
+
result
|
3685
3169
|
end
|
3686
3170
|
|
3687
3171
|
def allocate_running_content_layout doc, page, periphery, cache
|
@@ -3852,191 +3336,890 @@ module Asciidoctor
|
|
3852
3336
|
end
|
3853
3337
|
end
|
3854
3338
|
end
|
3855
|
-
|
3856
|
-
[trim_styles, colspec_dict, content_dict, stamp_names]
|
3339
|
+
|
3340
|
+
[trim_styles, colspec_dict, content_dict, stamp_names]
|
3341
|
+
end
|
3342
|
+
end
|
3343
|
+
|
3344
|
+
# TODO: delegate to ink_page_header and ink_page_footer per page
|
3345
|
+
def ink_running_content periphery, doc, skip = [1, 1], body_start_page_number = 1
|
3346
|
+
skip_pages, skip_pagenums = skip
|
3347
|
+
# NOTE: find and advance to first non-imported content page to use as model page
|
3348
|
+
return unless (content_start_page_number = state.pages[skip_pages..-1].index {|it| !it.imported_page? })
|
3349
|
+
content_start_page_number += (skip_pages + 1)
|
3350
|
+
num_pages = page_count
|
3351
|
+
prev_page_number = page_number
|
3352
|
+
go_to_page content_start_page_number
|
3353
|
+
|
3354
|
+
# FIXME: probably need to treat doctypes differently
|
3355
|
+
is_book = doc.doctype == 'book'
|
3356
|
+
header = doc.header? ? doc.header : nil
|
3357
|
+
sectlevels = (@theme[%(#{periphery}_sectlevels)] || 2).to_i
|
3358
|
+
sections = doc.find_by(context: :section) {|sect| sect.level <= sectlevels && sect != header }
|
3359
|
+
toc_title = (doc.attr 'toc-title').to_s if (toc_page_nums = @toc_extent&.page_range)
|
3360
|
+
disable_on_pages = @disable_running_content[periphery]
|
3361
|
+
|
3362
|
+
title_method = TitleStyles[@theme[%(#{periphery}_title_style)]]
|
3363
|
+
# FIXME: we need a proper model for all this page counting
|
3364
|
+
# FIXME: we make a big assumption that part & chapter start on new pages
|
3365
|
+
# index parts, chapters and sections by the physical page number on which they start
|
3366
|
+
part_start_pages = {}
|
3367
|
+
chapter_start_pages = {}
|
3368
|
+
section_start_pages = {}
|
3369
|
+
trailing_section_start_pages = {}
|
3370
|
+
sections.each do |sect|
|
3371
|
+
pgnum = (sect.attr 'pdf-page-start').to_i
|
3372
|
+
if is_book && ((sect_is_part = sect.sectname == 'part') || sect.level == 1)
|
3373
|
+
if sect_is_part
|
3374
|
+
part_start_pages[pgnum] ||= sect
|
3375
|
+
else
|
3376
|
+
chapter_start_pages[pgnum] ||= sect
|
3377
|
+
# FIXME: need a better way to indicate that part has ended
|
3378
|
+
part_start_pages[pgnum] = '' if sect.sectname == 'appendix' && !part_start_pages.empty?
|
3379
|
+
end
|
3380
|
+
else
|
3381
|
+
trailing_section_start_pages[pgnum] = sect
|
3382
|
+
section_start_pages[pgnum] ||= sect
|
3383
|
+
end
|
3384
|
+
end
|
3385
|
+
|
3386
|
+
# index parts, chapters, and sections by the physical page number on which they appear
|
3387
|
+
parts_by_page = SectionInfoByPage.new title_method
|
3388
|
+
chapters_by_page = SectionInfoByPage.new title_method
|
3389
|
+
sections_by_page = SectionInfoByPage.new title_method
|
3390
|
+
# QUESTION: should the default part be the doctitle?
|
3391
|
+
last_part = nil
|
3392
|
+
# QUESTION: should we enforce that the preamble is a preface?
|
3393
|
+
last_chap = is_book ? :pre : nil
|
3394
|
+
last_sect = nil
|
3395
|
+
sect_search_threshold = 1
|
3396
|
+
(1..num_pages).each do |pgnum|
|
3397
|
+
if (part = part_start_pages[pgnum])
|
3398
|
+
last_part = part
|
3399
|
+
last_chap = nil
|
3400
|
+
last_sect = nil
|
3401
|
+
end
|
3402
|
+
if (chap = chapter_start_pages[pgnum])
|
3403
|
+
last_chap = chap
|
3404
|
+
last_sect = nil
|
3405
|
+
end
|
3406
|
+
if (sect = section_start_pages[pgnum])
|
3407
|
+
last_sect = sect
|
3408
|
+
elsif part || chap
|
3409
|
+
sect_search_threshold = pgnum
|
3410
|
+
# NOTE: we didn't find a section on this page; look back to find last section started
|
3411
|
+
elsif last_sect
|
3412
|
+
(sect_search_threshold..(pgnum - 1)).reverse_each do |prev|
|
3413
|
+
if (sect = trailing_section_start_pages[prev])
|
3414
|
+
last_sect = sect
|
3415
|
+
break
|
3416
|
+
end
|
3417
|
+
end
|
3418
|
+
end
|
3419
|
+
parts_by_page[pgnum] = last_part
|
3420
|
+
if toc_page_nums&.cover? pgnum
|
3421
|
+
if is_book
|
3422
|
+
chapters_by_page[pgnum] = toc_title
|
3423
|
+
sections_by_page[pgnum] = nil
|
3424
|
+
else
|
3425
|
+
chapters_by_page[pgnum] = nil
|
3426
|
+
sections_by_page[pgnum] = section_start_pages[pgnum] || toc_title
|
3427
|
+
end
|
3428
|
+
toc_page_nums = nil if toc_page_nums.end == pgnum
|
3429
|
+
elsif last_chap == :pre
|
3430
|
+
chapters_by_page[pgnum] = pgnum < body_start_page_number ? doc.doctitle : (doc.attr 'preface-title', 'Preface')
|
3431
|
+
sections_by_page[pgnum] = last_sect
|
3432
|
+
else
|
3433
|
+
chapters_by_page[pgnum] = last_chap
|
3434
|
+
sections_by_page[pgnum] = last_sect
|
3435
|
+
end
|
3436
|
+
end
|
3437
|
+
|
3438
|
+
doctitle = resolve_doctitle doc, true
|
3439
|
+
# NOTE: set doctitle again so it's properly escaped
|
3440
|
+
doc.set_attr 'doctitle', doctitle.combined
|
3441
|
+
doc.set_attr 'document-title', doctitle.main
|
3442
|
+
doc.set_attr 'document-subtitle', doctitle.subtitle
|
3443
|
+
doc.set_attr 'page-count', (num_pages - skip_pagenums)
|
3444
|
+
|
3445
|
+
pagenums_enabled = doc.attr? 'pagenums'
|
3446
|
+
periphery_layout_cache = {}
|
3447
|
+
# NOTE: this block is invoked during PDF generation, after #write -> #render_file and thus after #convert_document
|
3448
|
+
repeat (content_start_page_number..num_pages), dynamic: true do
|
3449
|
+
pgnum = page_number
|
3450
|
+
# NOTE: don't write on pages which are imported / inserts (otherwise we can get a corrupt PDF)
|
3451
|
+
next if page.imported_page? || (disable_on_pages.include? pgnum)
|
3452
|
+
virtual_pgnum = pgnum - skip_pagenums
|
3453
|
+
pgnum_label = (virtual_pgnum < 1 ? (RomanNumeral.new pgnum, :lower) : virtual_pgnum).to_s
|
3454
|
+
side = page_side((@folio_placement[:basis] == :physical ? pgnum : virtual_pgnum), @folio_placement[:inverted])
|
3455
|
+
doc.set_attr 'page-layout', page.layout.to_s
|
3456
|
+
|
3457
|
+
# NOTE: running content is cached per page layout
|
3458
|
+
# QUESTION: should allocation be per side?
|
3459
|
+
trim_styles, colspec_dict, content_dict, stamp_names = allocate_running_content_layout doc, page, periphery, periphery_layout_cache
|
3460
|
+
# FIXME: we need to have a content setting for chapter pages
|
3461
|
+
content_by_position, colspec_by_position = content_dict[side], colspec_dict[side]
|
3462
|
+
|
3463
|
+
doc.set_attr 'page-number', pgnum_label if pagenums_enabled
|
3464
|
+
# QUESTION: should the fallback value be nil instead of empty string? or should we remove attribute if no value?
|
3465
|
+
doc.set_attr 'part-title', ((part_info = parts_by_page[pgnum])[:title] || '')
|
3466
|
+
if (part_numeral = part_info[:numeral])
|
3467
|
+
doc.set_attr 'part-numeral', part_numeral
|
3468
|
+
else
|
3469
|
+
doc.remove_attr 'part-numeral'
|
3470
|
+
end
|
3471
|
+
doc.set_attr 'chapter-title', ((chap_info = chapters_by_page[pgnum])[:title] || '')
|
3472
|
+
if (chap_numeral = chap_info[:numeral])
|
3473
|
+
doc.set_attr 'chapter-numeral', chap_numeral
|
3474
|
+
else
|
3475
|
+
doc.remove_attr 'chapter-numeral'
|
3476
|
+
end
|
3477
|
+
doc.set_attr 'section-title', ((sect_info = sections_by_page[pgnum])[:title] || '')
|
3478
|
+
doc.set_attr 'section-or-chapter-title', (sect_info[:title] || chap_info[:title] || '')
|
3479
|
+
|
3480
|
+
stamp stamp_names[side] if stamp_names
|
3481
|
+
|
3482
|
+
theme_font periphery do
|
3483
|
+
canvas do
|
3484
|
+
bounding_box [trim_styles[:content_left][side], trim_styles[:top][side]], width: trim_styles[:content_width][side], height: trim_styles[:height] do
|
3485
|
+
if trim_styles[:column_rule_color] && (trim_column_rule_width = trim_styles[:column_rule_width]) > 0
|
3486
|
+
trim_column_rule_spacing = trim_styles[:column_rule_spacing]
|
3487
|
+
else
|
3488
|
+
trim_column_rule_width = nil
|
3489
|
+
end
|
3490
|
+
prev_position = nil
|
3491
|
+
ColumnPositions.each do |position|
|
3492
|
+
next unless (content = content_by_position[position])
|
3493
|
+
next unless (colspec = colspec_by_position[position])[:width] > 0
|
3494
|
+
left, colwidth = colspec[:x], colspec[:width]
|
3495
|
+
if trim_column_rule_width && colwidth < bounds.width
|
3496
|
+
if (trim_column_rule = prev_position)
|
3497
|
+
left += (trim_column_rule_spacing * 0.5)
|
3498
|
+
colwidth -= trim_column_rule_spacing
|
3499
|
+
else
|
3500
|
+
colwidth -= (trim_column_rule_spacing * 0.5)
|
3501
|
+
end
|
3502
|
+
end
|
3503
|
+
# FIXME: we need to have a content setting for chapter pages
|
3504
|
+
if ::Array === content
|
3505
|
+
redo_with_content = nil
|
3506
|
+
# NOTE: float ensures cursor position is restored and returns us to current page if we overrun
|
3507
|
+
float do
|
3508
|
+
# NOTE: bounding_box is redundant if both vertical padding and border width are 0
|
3509
|
+
bounding_box [left, bounds.top - trim_styles[:padding][side][0] - trim_styles[:content_offset]], width: colwidth, height: trim_styles[:content_height][side] do
|
3510
|
+
# NOTE: image vposition respects padding; use negative image_vertical_align value to revert
|
3511
|
+
image_opts = content[1].merge position: colspec[:align], vposition: trim_styles[:img_valign]
|
3512
|
+
begin
|
3513
|
+
image_info = image content[0], image_opts
|
3514
|
+
if (image_link = content[2])
|
3515
|
+
image_info = { width: image_info.scaled_width, height: image_info.scaled_height } unless image_opts[:format] == 'svg'
|
3516
|
+
add_link_to_image image_link, image_info, image_opts
|
3517
|
+
end
|
3518
|
+
rescue
|
3519
|
+
redo_with_content = image_opts[:alt]
|
3520
|
+
log :warn, %(could not embed image in running content: #{content[0]}; #{$!.message})
|
3521
|
+
end
|
3522
|
+
end
|
3523
|
+
end
|
3524
|
+
if redo_with_content
|
3525
|
+
content_by_position[position] = redo_with_content
|
3526
|
+
redo
|
3527
|
+
end
|
3528
|
+
else
|
3529
|
+
theme_font %(#{periphery}_#{side}_#{position}) do
|
3530
|
+
# NOTE: minor optimization
|
3531
|
+
if content == '{page-number}'
|
3532
|
+
content = pagenums_enabled ? pgnum_label : nil
|
3533
|
+
else
|
3534
|
+
content = apply_subs_discretely doc, content, drop_lines_with_unresolved_attributes: true, imagesdir: @themesdir
|
3535
|
+
content = transform_text content, @text_transform if @text_transform
|
3536
|
+
end
|
3537
|
+
formatted_text_box (parse_text content, inline_format: [normalize: true]),
|
3538
|
+
at: [left, bounds.top - trim_styles[:padding][side][0] - trim_styles[:content_offset] + ((Array trim_styles[:valign])[0] == :center ? font.descender * 0.5 : 0)],
|
3539
|
+
color: @font_color,
|
3540
|
+
width: colwidth,
|
3541
|
+
height: trim_styles[:prose_content_height][side],
|
3542
|
+
align: colspec[:align],
|
3543
|
+
valign: trim_styles[:valign],
|
3544
|
+
leading: trim_styles[:line_metrics].leading,
|
3545
|
+
final_gap: false,
|
3546
|
+
overflow: :truncate
|
3547
|
+
end
|
3548
|
+
end
|
3549
|
+
bounding_box [colspec[:x], bounds.top - trim_styles[:padding][side][0] - trim_styles[:content_offset]], width: colspec[:width], height: trim_styles[:content_height][side] do
|
3550
|
+
stroke_vertical_rule trim_styles[:column_rule_color], at: bounds.left, line_style: trim_styles[:column_rule_style], line_width: trim_column_rule_width
|
3551
|
+
end if trim_column_rule
|
3552
|
+
prev_position = position
|
3553
|
+
end
|
3554
|
+
end
|
3555
|
+
end
|
3556
|
+
end
|
3557
|
+
end
|
3558
|
+
|
3559
|
+
go_to_page prev_page_number
|
3560
|
+
nil
|
3561
|
+
end
|
3562
|
+
|
3563
|
+
def ink_title_page doc
|
3564
|
+
# QUESTION: allow alignment per element on title page?
|
3565
|
+
title_text_align = (@theme.title_page_text_align || @base_text_align).to_sym
|
3566
|
+
|
3567
|
+
if @theme.title_page_logo_display != 'none' && (logo_image_path = (doc.attr 'title-logo-image') || (logo_image_from_theme = @theme.title_page_logo_image))
|
3568
|
+
if (logo_image_path.include? ':') && logo_image_path =~ ImageAttributeValueRx
|
3569
|
+
logo_image_attrs = (AttributeList.new $2).parse %w(alt width height)
|
3570
|
+
if logo_image_from_theme
|
3571
|
+
relative_to_imagesdir = false
|
3572
|
+
logo_image_path = apply_subs_discretely doc, $1, subs: [:attributes]
|
3573
|
+
logo_image_path = ThemeLoader.resolve_theme_asset logo_image_path, @themesdir unless doc.is_uri? logo_image_path
|
3574
|
+
else
|
3575
|
+
relative_to_imagesdir = true
|
3576
|
+
logo_image_path = $1
|
3577
|
+
end
|
3578
|
+
else
|
3579
|
+
logo_image_attrs = {}
|
3580
|
+
relative_to_imagesdir = false
|
3581
|
+
if logo_image_from_theme
|
3582
|
+
logo_image_path = apply_subs_discretely doc, logo_image_path, subs: [:attributes]
|
3583
|
+
logo_image_path = ThemeLoader.resolve_theme_asset logo_image_path, @themesdir unless doc.is_uri? logo_image_path
|
3584
|
+
end
|
3585
|
+
end
|
3586
|
+
if (::Asciidoctor::Image.target_and_format logo_image_path)[1] == 'pdf'
|
3587
|
+
log :error, %(PDF format not supported for title page logo image: #{logo_image_path})
|
3588
|
+
else
|
3589
|
+
logo_image_attrs['target'] = logo_image_path
|
3590
|
+
# NOTE: at the very least, title_text_align will be a valid alignment value
|
3591
|
+
logo_image_attrs['align'] = [(logo_image_attrs.delete 'align'), @theme.title_page_logo_align, title_text_align.to_s].find {|val| (BlockAlignmentNames.include? val) }
|
3592
|
+
if (logo_image_top = logo_image_attrs['top'] || @theme.title_page_logo_top)
|
3593
|
+
initial_y, @y = @y, (resolve_top logo_image_top)
|
3594
|
+
end
|
3595
|
+
# NOTE: pinned option keeps image on same page
|
3596
|
+
indent (@theme.title_page_logo_margin_left || 0), (@theme.title_page_logo_margin_right || 0) do
|
3597
|
+
# FIXME: add API to Asciidoctor for creating blocks outside of extensions
|
3598
|
+
convert_image (::Asciidoctor::Block.new doc, :image, content_model: :empty, attributes: logo_image_attrs), relative_to_imagesdir: relative_to_imagesdir, pinned: true
|
3599
|
+
end
|
3600
|
+
@y = initial_y if initial_y
|
3601
|
+
end
|
3602
|
+
end
|
3603
|
+
|
3604
|
+
# TODO: prevent content from spilling to next page
|
3605
|
+
theme_font :title_page do
|
3606
|
+
if (title_top = @theme.title_page_title_top)
|
3607
|
+
@y = resolve_top title_top
|
3608
|
+
end
|
3609
|
+
unless @theme.title_page_title_display == 'none'
|
3610
|
+
doctitle = doc.doctitle partition: true
|
3611
|
+
move_down @theme.title_page_title_margin_top || 0
|
3612
|
+
indent (@theme.title_page_title_margin_left || 0), (@theme.title_page_title_margin_right || 0) do
|
3613
|
+
theme_font :title_page_title do
|
3614
|
+
ink_prose doctitle.main, align: title_text_align, margin: 0
|
3615
|
+
end
|
3616
|
+
end
|
3617
|
+
move_down @theme.title_page_title_margin_bottom || 0
|
3618
|
+
end
|
3619
|
+
if @theme.title_page_subtitle_display != 'none' && (subtitle = (doctitle || (doc.doctitle partition: true)).subtitle)
|
3620
|
+
move_down @theme.title_page_subtitle_margin_top || 0
|
3621
|
+
indent (@theme.title_page_subtitle_margin_left || 0), (@theme.title_page_subtitle_margin_right || 0) do
|
3622
|
+
theme_font :title_page_subtitle do
|
3623
|
+
ink_prose subtitle, align: title_text_align, margin: 0
|
3624
|
+
end
|
3625
|
+
end
|
3626
|
+
move_down @theme.title_page_subtitle_margin_bottom || 0
|
3627
|
+
end
|
3628
|
+
if @theme.title_page_authors_display != 'none' && (doc.attr? 'authors')
|
3629
|
+
move_down @theme.title_page_authors_margin_top || 0
|
3630
|
+
indent (@theme.title_page_authors_margin_left || 0), (@theme.title_page_authors_margin_right || 0) do
|
3631
|
+
generic_authors_content = @theme.title_page_authors_content
|
3632
|
+
authors_content = {
|
3633
|
+
name_only: @theme.title_page_authors_content_name_only || generic_authors_content,
|
3634
|
+
with_email: @theme.title_page_authors_content_with_email || generic_authors_content,
|
3635
|
+
with_url: @theme.title_page_authors_content_with_url || generic_authors_content,
|
3636
|
+
}
|
3637
|
+
authors = doc.authors.map.with_index do |author, idx|
|
3638
|
+
with_author doc, author, idx == 0 do
|
3639
|
+
author_content_key = (url = doc.attr 'url') ? ((url.start_with? 'mailto:') ? :with_email : :with_url) : :name_only
|
3640
|
+
if (author_content = authors_content[author_content_key])
|
3641
|
+
apply_subs_discretely doc, author_content, drop_lines_with_unresolved_attributes: true, imagesdir: @themesdir
|
3642
|
+
else
|
3643
|
+
doc.attr 'author'
|
3644
|
+
end
|
3645
|
+
end
|
3646
|
+
end.join @theme.title_page_authors_delimiter
|
3647
|
+
theme_font :title_page_authors do
|
3648
|
+
ink_prose authors, align: title_text_align, margin: 0, normalize: true
|
3649
|
+
end
|
3650
|
+
end
|
3651
|
+
move_down @theme.title_page_authors_margin_bottom || 0
|
3652
|
+
end
|
3653
|
+
unless @theme.title_page_revision_display == 'none' || (revision_info = [(doc.attr? 'revnumber') ? %(#{doc.attr 'version-label'} #{doc.attr 'revnumber'}) : nil, (doc.attr 'revdate')].compact).empty?
|
3654
|
+
move_down @theme.title_page_revision_margin_top || 0
|
3655
|
+
revision_text = revision_info.join @theme.title_page_revision_delimiter
|
3656
|
+
if (revremark = doc.attr 'revremark')
|
3657
|
+
revision_text = %(#{revision_text}: #{revremark})
|
3658
|
+
end
|
3659
|
+
indent (@theme.title_page_revision_margin_left || 0), (@theme.title_page_revision_margin_right || 0) do
|
3660
|
+
theme_font :title_page_revision do
|
3661
|
+
ink_prose revision_text, align: title_text_align, margin: 0, normalize: false
|
3662
|
+
end
|
3663
|
+
end
|
3664
|
+
move_down @theme.title_page_revision_margin_bottom || 0
|
3665
|
+
end
|
3666
|
+
end
|
3667
|
+
end
|
3668
|
+
|
3669
|
+
def allocate_toc doc, toc_num_levels, toc_start_cursor, title_page_on
|
3670
|
+
toc_start_page_number = page_number
|
3671
|
+
to_page = nil
|
3672
|
+
extent = dry_run onto: self do
|
3673
|
+
to_page = (ink_toc doc, toc_num_levels, toc_start_page_number, toc_start_cursor).end
|
3674
|
+
theme_margin :block, :bottom unless title_page_on
|
3675
|
+
end
|
3676
|
+
# NOTE: patch for custom converters that allocate extra TOC pages without actually creating them
|
3677
|
+
if to_page > extent.to.page
|
3678
|
+
extent.to.page = to_page
|
3679
|
+
extent.to.cursor = bounds.height
|
3680
|
+
end
|
3681
|
+
# NOTE: reserve pages for the toc; leaves cursor on page after last page in toc
|
3682
|
+
if title_page_on
|
3683
|
+
extent.each_page { start_new_page }
|
3684
|
+
else
|
3685
|
+
extent.each_page {|first_page| start_new_page unless first_page }
|
3686
|
+
move_cursor_to extent.to.cursor
|
3687
|
+
end
|
3688
|
+
extent
|
3689
|
+
end
|
3690
|
+
|
3691
|
+
def get_entries_for_toc node
|
3692
|
+
node.sections
|
3693
|
+
end
|
3694
|
+
|
3695
|
+
# NOTE: num_front_matter_pages not used during a dry run
|
3696
|
+
def ink_toc doc, num_levels, toc_page_number, start_cursor, num_front_matter_pages = 0
|
3697
|
+
go_to_page toc_page_number unless (page_number == toc_page_number) || scratch?
|
3698
|
+
start_page_number = page_number
|
3699
|
+
move_cursor_to start_cursor
|
3700
|
+
unless (toc_title = doc.attr 'toc-title').nil_or_empty?
|
3701
|
+
theme_font_cascade [[:heading, level: 2], :toc_title] do
|
3702
|
+
toc_title_text_align = (@theme.toc_title_text_align || @theme.heading_h2_text_align || @theme.heading_text_align || @base_text_align).to_sym
|
3703
|
+
ink_general_heading doc, toc_title, align: toc_title_text_align, level: 2, outdent: true, role: :toctitle
|
3704
|
+
end
|
3705
|
+
end
|
3706
|
+
# QUESTION: should we skip this whole method if num_levels < 0?
|
3707
|
+
unless num_levels < 0
|
3708
|
+
dot_leader = theme_font :toc do
|
3709
|
+
# TODO: we could simplify by using nested theme_font :toc_dot_leader
|
3710
|
+
if (dot_leader_font_style = @theme.toc_dot_leader_font_style&.to_sym || :normal) != font_style
|
3711
|
+
font_style dot_leader_font_style
|
3712
|
+
end
|
3713
|
+
font_size @theme.toc_dot_leader_font_size
|
3714
|
+
{
|
3715
|
+
font_color: @theme.toc_dot_leader_font_color || @font_color,
|
3716
|
+
font_style: dot_leader_font_style,
|
3717
|
+
font_size: font_size,
|
3718
|
+
levels: ((dot_leader_l = @theme.toc_dot_leader_levels) == 'none' ? ::Set.new :
|
3719
|
+
(dot_leader_l && dot_leader_l != 'all' ? dot_leader_l.to_s.split.map(&:to_i).to_set : (0..num_levels).to_set)),
|
3720
|
+
text: (dot_leader_text = @theme.toc_dot_leader_content || DotLeaderTextDefault),
|
3721
|
+
width: dot_leader_text.empty? ? 0 : (rendered_width_of_string dot_leader_text),
|
3722
|
+
# TODO: spacer gives a little bit of room between dots and page number
|
3723
|
+
spacer: { text: NoBreakSpace, size: (spacer_font_size = @font_size * 0.25) },
|
3724
|
+
spacer_width: (rendered_width_of_char NoBreakSpace, size: spacer_font_size),
|
3725
|
+
}
|
3726
|
+
end
|
3727
|
+
theme_margin :toc, :top
|
3728
|
+
ink_toc_level (get_entries_for_toc doc), num_levels, dot_leader, num_front_matter_pages
|
3729
|
+
end
|
3730
|
+
# NOTE: range must be calculated relative to toc_page_number; absolute page number in scratch document is arbitrary
|
3731
|
+
toc_page_numbers = (toc_page_number..(toc_page_number + (page_number - start_page_number)))
|
3732
|
+
go_to_page page_count unless scratch?
|
3733
|
+
toc_page_numbers
|
3734
|
+
end
|
3735
|
+
|
3736
|
+
def ink_toc_level entries, num_levels, dot_leader, num_front_matter_pages
|
3737
|
+
# NOTE: font options aren't always reliable, so store size separately
|
3738
|
+
toc_font_info = theme_font :toc do
|
3739
|
+
{ font: font, size: @font_size }
|
3740
|
+
end
|
3741
|
+
hanging_indent = @theme.toc_hanging_indent
|
3742
|
+
entries.each do |entry|
|
3743
|
+
next if (num_levels_for_entry = (entry.attr 'toclevels', num_levels).to_i) < (entry_level = entry.level + 1).pred ||
|
3744
|
+
!(entry_anchor = (entry.attr 'pdf-anchor') || entry.id) ||
|
3745
|
+
((entry.option? 'notitle') && entry == entry.document.last_child && entry.empty?)
|
3746
|
+
theme_font :toc, level: entry_level do
|
3747
|
+
entry_title = entry.context == :section ? entry.numbered_title : (entry.title? ? entry.title : (entry.xreftext 'basic'))
|
3748
|
+
next if entry_title.empty?
|
3749
|
+
entry_title = transform_text entry_title, @text_transform if @text_transform
|
3750
|
+
pgnum_label_placeholder_width = rendered_width_of_string '0' * @toc_max_pagenum_digits
|
3751
|
+
# NOTE: only write title (excluding dots and page number) if this is a dry run
|
3752
|
+
if scratch?
|
3753
|
+
indent 0, pgnum_label_placeholder_width do
|
3754
|
+
# NOTE: must wrap title in empty anchor element in case links are styled with different font family / size
|
3755
|
+
ink_prose entry_title, anchor: true, normalize: false, hanging_indent: hanging_indent, normalize_line_height: true, margin: 0
|
3756
|
+
end
|
3757
|
+
else
|
3758
|
+
if !(physical_pgnum = entry.attr 'pdf-page-start') &&
|
3759
|
+
(target_page_ref = (get_dest entry_anchor)&.first) &&
|
3760
|
+
(target_page_idx = state.pages.index {|candidate| candidate.dictionary == target_page_ref })
|
3761
|
+
physical_pgnum = target_page_idx + 1
|
3762
|
+
end
|
3763
|
+
if physical_pgnum
|
3764
|
+
virtual_pgnum = physical_pgnum - num_front_matter_pages
|
3765
|
+
pgnum_label = (virtual_pgnum < 1 ? (RomanNumeral.new physical_pgnum, :lower) : virtual_pgnum).to_s
|
3766
|
+
else
|
3767
|
+
pgnum_label = '?'
|
3768
|
+
end
|
3769
|
+
start_page_number = page_number
|
3770
|
+
start_cursor = cursor
|
3771
|
+
start_dots = nil
|
3772
|
+
entry_title_inherited = (apply_text_decoration ::Set.new, :toc, entry_level).merge anchor: entry_anchor, color: @font_color
|
3773
|
+
# NOTE: use text formatter to add anchor overlay to avoid using inline format with synthetic anchor tag
|
3774
|
+
entry_title_fragments = text_formatter.format entry_title, inherited: entry_title_inherited
|
3775
|
+
line_metrics = calc_line_metrics @base_line_height
|
3776
|
+
indent 0, pgnum_label_placeholder_width do
|
3777
|
+
(entry_title_fragments[-1][:callback] ||= []) << (last_fragment_pos = ::Asciidoctor::PDF::FormattedText::FragmentPositionRenderer.new)
|
3778
|
+
typeset_formatted_text entry_title_fragments, line_metrics, hanging_indent: hanging_indent, normalize_line_height: true
|
3779
|
+
start_dots = last_fragment_pos.right + hanging_indent
|
3780
|
+
last_fragment_cursor = last_fragment_pos.top + line_metrics.padding_top
|
3781
|
+
start_cursor = last_fragment_cursor if last_fragment_pos.page_number > start_page_number || (start_cursor - last_fragment_cursor) > line_metrics.height
|
3782
|
+
end
|
3783
|
+
end_cursor = cursor
|
3784
|
+
move_cursor_to start_cursor
|
3785
|
+
# NOTE: we're guaranteed to be on the same page as the final line of the entry
|
3786
|
+
if dot_leader[:width] > 0 && (dot_leader[:levels].include? entry_level.pred)
|
3787
|
+
pgnum_label_width = rendered_width_of_string pgnum_label
|
3788
|
+
pgnum_label_font_settings = { color: @font_color, font: font_family, size: @font_size, styles: font_styles }
|
3789
|
+
save_font do
|
3790
|
+
# NOTE: the same font is used for dot leaders throughout toc
|
3791
|
+
set_font toc_font_info[:font], dot_leader[:font_size]
|
3792
|
+
font_style dot_leader[:font_style]
|
3793
|
+
num_dots = [((bounds.width - start_dots - dot_leader[:spacer_width] - pgnum_label_width) / dot_leader[:width]).floor, 0].max
|
3794
|
+
# FIXME: dots don't line up in columns if width of page numbers differ
|
3795
|
+
typeset_formatted_text [
|
3796
|
+
{ text: dot_leader[:text] * num_dots, color: dot_leader[:font_color] },
|
3797
|
+
dot_leader[:spacer],
|
3798
|
+
({ text: pgnum_label, anchor: entry_anchor }.merge pgnum_label_font_settings),
|
3799
|
+
], line_metrics, align: :right
|
3800
|
+
end
|
3801
|
+
else
|
3802
|
+
typeset_formatted_text [{ text: pgnum_label, color: @font_color, anchor: entry_anchor }], line_metrics, align: :right
|
3803
|
+
end
|
3804
|
+
move_cursor_to end_cursor
|
3805
|
+
end
|
3806
|
+
end
|
3807
|
+
indent @theme.toc_indent do
|
3808
|
+
ink_toc_level (get_entries_for_toc entry), num_levels_for_entry, dot_leader, num_front_matter_pages
|
3809
|
+
end if num_levels_for_entry >= entry_level
|
3810
|
+
end
|
3811
|
+
end
|
3812
|
+
|
3813
|
+
# Sends the specified message to the log unless this method is called from the scratch document
|
3814
|
+
def log severity, message = nil, &block
|
3815
|
+
logger.send severity, message, &block unless scratch?
|
3816
|
+
end
|
3817
|
+
|
3818
|
+
# Insert a margin at the specified side if the cursor is not at the top of
|
3819
|
+
# the page. Start a new page if amount is greater than the remaining space on
|
3820
|
+
# the page.
|
3821
|
+
def margin amount, _side
|
3822
|
+
if (amount || 0) == 0 || at_page_top?
|
3823
|
+
0
|
3824
|
+
elsif cursor > amount
|
3825
|
+
move_down amount
|
3826
|
+
amount
|
3827
|
+
else
|
3828
|
+
# move cursor to top of next page
|
3829
|
+
bounds.move_past_bottom
|
3830
|
+
0
|
3831
|
+
end
|
3832
|
+
end
|
3833
|
+
|
3834
|
+
# Insert a bottom margin equal to amount unless cursor is at the top of the
|
3835
|
+
# page (not likely). Start a new page instead if amount is greater than the
|
3836
|
+
# remaining space on the page.
|
3837
|
+
def margin_bottom amount
|
3838
|
+
margin amount, :bottom
|
3839
|
+
end
|
3840
|
+
|
3841
|
+
# Insert a top margin equal to amount if cursor is not at the top of the
|
3842
|
+
# page. Start a new page instead if amount is greater than the remaining
|
3843
|
+
# space on the page.
|
3844
|
+
def margin_top amount
|
3845
|
+
margin amount, :top
|
3846
|
+
end
|
3847
|
+
|
3848
|
+
def next_enclosed_block block, descend: false
|
3849
|
+
return if (context = block.context) == :document
|
3850
|
+
parent_context = (parent = block.parent).context
|
3851
|
+
if (list_item = context == :list_item)
|
3852
|
+
return block.first_child if descend && block.blocks?
|
3853
|
+
siblings = parent.items
|
3854
|
+
else
|
3855
|
+
siblings = parent.blocks
|
3856
|
+
end
|
3857
|
+
siblings = siblings.flatten if parent_context == :dlist
|
3858
|
+
if block != siblings[-1]
|
3859
|
+
(self_idx = siblings.index block) && siblings[self_idx + 1]
|
3860
|
+
elsif parent_context == :list_item || (parent_context == :open && parent.style != 'abstract') || parent_context == :section
|
3861
|
+
next_enclosed_block parent
|
3862
|
+
elsif list_item && (grandparent = parent.parent).context == :list_item
|
3863
|
+
next_enclosed_block grandparent
|
3864
|
+
end
|
3865
|
+
end
|
3866
|
+
|
3867
|
+
def register_fonts font_catalog, fonts_dir
|
3868
|
+
return unless font_catalog
|
3869
|
+
dirs = (fonts_dir.split ValueSeparatorRx, -1).map do |dir|
|
3870
|
+
dir == 'GEM_FONTS_DIR' || dir.empty? ? ThemeLoader::FontsDir : dir
|
3871
|
+
end
|
3872
|
+
font_catalog.each do |key, styles|
|
3873
|
+
styles = styles.each_with_object({}) do |(style, path), accum|
|
3874
|
+
found = dirs.any? do |dir|
|
3875
|
+
resolved_font_path = font_path path, dir
|
3876
|
+
accum[style.to_sym] = resolved_font_path if ::File.readable? resolved_font_path
|
3877
|
+
end
|
3878
|
+
raise ::Errno::ENOENT, ((File.absolute_path? path) ? %(#{path} not found) : %(#{path} not found in #{fonts_dir.gsub ValueSeparatorRx, ' or '})) unless found
|
3879
|
+
end
|
3880
|
+
register_font key => styles
|
3881
|
+
end
|
3882
|
+
end
|
3883
|
+
|
3884
|
+
# Compute the rendered width of a char, taking fallback fonts into account
|
3885
|
+
def rendered_width_of_char char, opts = {}
|
3886
|
+
unless @fallback_fonts.empty? || (font.glyph_present? char)
|
3887
|
+
@fallback_fonts.each do |fallback_font|
|
3888
|
+
font fallback_font do
|
3889
|
+
return width_of_string char, opts if font.glyph_present? char
|
3890
|
+
end
|
3891
|
+
end
|
3892
|
+
end
|
3893
|
+
width_of_string char, opts
|
3894
|
+
end
|
3895
|
+
|
3896
|
+
# Compute the rendered width of a string, taking fallback fonts into account
|
3897
|
+
def rendered_width_of_string str, opts = {}
|
3898
|
+
opts = opts.merge kerning: default_kerning?
|
3899
|
+
if str.length == 1
|
3900
|
+
rendered_width_of_char str, opts
|
3901
|
+
elsif (chars = str.each_char).all? {|char| font.glyph_present? char }
|
3902
|
+
width_of_string str, opts
|
3903
|
+
else
|
3904
|
+
char_widths = chars.map {|char| rendered_width_of_char char, opts }
|
3905
|
+
char_widths.sum + (char_widths.length * character_spacing)
|
3906
|
+
end
|
3907
|
+
end
|
3908
|
+
|
3909
|
+
# Resolve the path and sizing of the background image either from a document attribute or theme key.
|
3910
|
+
#
|
3911
|
+
# Returns the argument list for the image method if the document attribute or theme key is found. Otherwise,
|
3912
|
+
# nothing. The first argument in the argument list is the image path. If that value is nil, the background
|
3913
|
+
# image is disabled. The second argument is the options hash to specify the dimensions, such as width and fit.
|
3914
|
+
def resolve_background_image doc, theme, key, opts = {}
|
3915
|
+
if ::String === key
|
3916
|
+
theme_key = opts.delete :theme_key
|
3917
|
+
image_path = (doc.attr key) || (from_theme = theme[theme_key || (key.tr '-', '_').to_sym])
|
3918
|
+
else
|
3919
|
+
image_path = from_theme = theme[key]
|
3920
|
+
end
|
3921
|
+
symbolic_paths = opts.delete :symbolic_paths
|
3922
|
+
if image_path
|
3923
|
+
if symbolic_paths&.include? image_path
|
3924
|
+
return [image_path, {}]
|
3925
|
+
elsif image_path == 'none'
|
3926
|
+
return []
|
3927
|
+
elsif (image_path.include? ':') && image_path =~ ImageAttributeValueRx
|
3928
|
+
image_attrs = (AttributeList.new $2).parse %w(alt width)
|
3929
|
+
if from_theme
|
3930
|
+
image_path = apply_subs_discretely doc, $1, subs: [:attributes]
|
3931
|
+
image_relative_to = @themesdir
|
3932
|
+
else
|
3933
|
+
image_path = $1
|
3934
|
+
image_relative_to = true
|
3935
|
+
end
|
3936
|
+
elsif from_theme
|
3937
|
+
image_path = apply_subs_discretely doc, image_path, subs: [:attributes]
|
3938
|
+
image_relative_to = @themesdir
|
3939
|
+
end
|
3940
|
+
|
3941
|
+
image_path, image_format = ::Asciidoctor::Image.target_and_format image_path, image_attrs
|
3942
|
+
image_path = resolve_image_path doc, image_path, image_format, image_relative_to
|
3943
|
+
|
3944
|
+
return unless image_path
|
3945
|
+
|
3946
|
+
unless ::File.readable? image_path
|
3947
|
+
log :warn, %(#{key.to_s.tr '-_', ' '} not found or readable: #{image_path})
|
3948
|
+
return
|
3949
|
+
end
|
3950
|
+
|
3951
|
+
if image_format == 'pdf'
|
3952
|
+
[image_path, page: [((image_attrs || {})['page']).to_i, 1].max, format: image_format]
|
3953
|
+
else
|
3954
|
+
[image_path, (resolve_image_options image_path, image_format, image_attrs, (({ background: true, container_size: [page_width, page_height] }.merge opts)))]
|
3955
|
+
end
|
3956
|
+
end
|
3957
|
+
end
|
3958
|
+
|
3959
|
+
def resolve_doctitle doc, partition = nil
|
3960
|
+
if doc.header?
|
3961
|
+
doc.doctitle partition: partition
|
3962
|
+
elsif partition
|
3963
|
+
::Asciidoctor::Document::Title.new (doc.attr 'untitled-label'), separator: (doc.attr 'title-separator')
|
3964
|
+
else
|
3965
|
+
doc.attr 'untitled-label'
|
3966
|
+
end
|
3967
|
+
end
|
3968
|
+
|
3969
|
+
# Resolves the explicit width, if specified, as a PDF pt value.
|
3970
|
+
#
|
3971
|
+
# Resolves the explicit width, first considering the pdfwidth attribute, then the scaledwidth
|
3972
|
+
# attribute, then the theme default (if enabled by the :use_fallback option), and finally the
|
3973
|
+
# width attribute. If the specified value is in pixels, the value is scaled by 75% to perform
|
3974
|
+
# approximate CSS px to PDF pt conversion. If the value is a percentage, and the
|
3975
|
+
# bounds_width option is given, the percentage of the bounds_width value is returned.
|
3976
|
+
# Otherwise, the percentage width is returned.
|
3977
|
+
#--
|
3978
|
+
# QUESTION: should we enforce positive result?
|
3979
|
+
def resolve_explicit_width attrs, opts = {}
|
3980
|
+
bounds_width = opts[:bounds_width]
|
3981
|
+
# QUESTION: should we restrict width to bounds_width for pdfwidth?
|
3982
|
+
if attrs.key? 'pdfwidth'
|
3983
|
+
if (width = attrs['pdfwidth']).end_with? '%'
|
3984
|
+
bounds_width ? (width.to_f / 100) * bounds_width : width
|
3985
|
+
elsif opts[:support_vw] && (width.end_with? 'vw')
|
3986
|
+
(width.chomp 'vw').extend ViewportWidth
|
3987
|
+
else
|
3988
|
+
str_to_pt width
|
3989
|
+
end
|
3990
|
+
elsif attrs.key? 'scaledwidth'
|
3991
|
+
# NOTE: the parser automatically appends % if value is unitless
|
3992
|
+
if (width = attrs['scaledwidth']).end_with? '%'
|
3993
|
+
bounds_width ? (width.to_f / 100) * bounds_width : width
|
3994
|
+
else
|
3995
|
+
str_to_pt width
|
3996
|
+
end
|
3997
|
+
elsif opts[:use_fallback] && (width = @theme.image_width)
|
3998
|
+
if ::Numeric === width
|
3999
|
+
width
|
4000
|
+
elsif (width = width.to_s).end_with? '%'
|
4001
|
+
bounds_width ? (width.to_f / 100) * bounds_width : bounds_width
|
4002
|
+
elsif opts[:support_vw] && (width.end_with? 'vw')
|
4003
|
+
(width.chomp 'vw').extend ViewportWidth
|
4004
|
+
else
|
4005
|
+
str_to_pt width
|
4006
|
+
end
|
4007
|
+
elsif attrs.key? 'width'
|
4008
|
+
if (width = attrs['width']).end_with? '%'
|
4009
|
+
width = (width.to_f / 100) * bounds_width if bounds_width
|
4010
|
+
elsif DigitsRx.match? width
|
4011
|
+
width = to_pt width.to_f, :px
|
4012
|
+
else
|
4013
|
+
return
|
4014
|
+
end
|
4015
|
+
bounds_width && opts[:constrain_to_bounds] ? [bounds_width, width].min : width
|
4016
|
+
end
|
4017
|
+
end
|
4018
|
+
|
4019
|
+
def resolve_image_options image_path, image_format, image_attrs, opts = {}
|
4020
|
+
if image_format == 'svg'
|
4021
|
+
image_opts = {
|
4022
|
+
enable_file_requests_with_root: (::File.dirname image_path),
|
4023
|
+
enable_web_requests: allow_uri_read ? (method :load_open_uri).to_proc : false,
|
4024
|
+
cache_images: cache_uri,
|
4025
|
+
fallback_font_name: fallback_svg_font_name,
|
4026
|
+
format: 'svg',
|
4027
|
+
}
|
4028
|
+
else
|
4029
|
+
image_opts = {}
|
4030
|
+
end
|
4031
|
+
container_size = opts[:container_size]
|
4032
|
+
if image_attrs
|
4033
|
+
if (alt = image_attrs['alt'])
|
4034
|
+
image_opts[:alt] = %([#{alt}])
|
4035
|
+
end
|
4036
|
+
if (background = opts[:background]) && (image_pos = image_attrs['position']) && (image_pos = resolve_background_position image_pos, nil)
|
4037
|
+
image_opts.update image_pos
|
4038
|
+
end
|
4039
|
+
if (image_fit = image_attrs['fit'] || (background ? 'contain' : nil))
|
4040
|
+
image_fit = 'contain' if image_format == 'svg' && image_fit == 'fill'
|
4041
|
+
container_width, container_height = container_size
|
4042
|
+
case image_fit
|
4043
|
+
when 'none'
|
4044
|
+
if (image_width = resolve_explicit_width image_attrs, bounds_width: container_width)
|
4045
|
+
image_opts[:width] = image_width
|
4046
|
+
end
|
4047
|
+
when 'scale-down'
|
4048
|
+
# NOTE: if width and height aren't set in SVG, real width and height are computed after stretching viewbox to fit page
|
4049
|
+
if (image_width = resolve_explicit_width image_attrs, bounds_width: container_width)
|
4050
|
+
if image_width > container_width
|
4051
|
+
image_opts[:fit] = container_size
|
4052
|
+
else
|
4053
|
+
image_size = intrinsic_image_dimensions image_path, image_format
|
4054
|
+
if image_width * (image_size[:height].to_f / image_size[:width]) > container_height
|
4055
|
+
image_opts[:fit] = container_size
|
4056
|
+
else
|
4057
|
+
image_opts[:width] = image_width
|
4058
|
+
end
|
4059
|
+
end
|
4060
|
+
else
|
4061
|
+
image_size = intrinsic_image_dimensions image_path, image_format
|
4062
|
+
image_opts[:fit] = container_size if image_size[:width] > container_width || image_size[:height] > container_height
|
4063
|
+
end
|
4064
|
+
when 'cover'
|
4065
|
+
# QUESTION: should we take explicit width into account?
|
4066
|
+
image_size = intrinsic_image_dimensions image_path, image_format
|
4067
|
+
if container_width * (image_size[:height].to_f / image_size[:width]) < container_height
|
4068
|
+
image_opts[:height] = container_height
|
4069
|
+
else
|
4070
|
+
image_opts[:width] = container_width
|
4071
|
+
end
|
4072
|
+
when 'fill'
|
4073
|
+
image_opts[:width] = container_width
|
4074
|
+
image_opts[:height] = container_height
|
4075
|
+
else # 'contain'
|
4076
|
+
image_opts[:fit] = container_size
|
4077
|
+
end
|
4078
|
+
elsif (image_width = resolve_explicit_width image_attrs, bounds_width: container_size[0])
|
4079
|
+
image_opts[:width] = image_width
|
4080
|
+
else # default to fit=contain if sizing is not specified
|
4081
|
+
image_opts[:fit] = container_size
|
4082
|
+
end
|
4083
|
+
else
|
4084
|
+
image_opts[:fit] = container_size
|
3857
4085
|
end
|
4086
|
+
image_opts
|
3858
4087
|
end
|
3859
4088
|
|
3860
|
-
|
3861
|
-
|
3862
|
-
|
3863
|
-
|
3864
|
-
|
3865
|
-
|
3866
|
-
|
3867
|
-
|
3868
|
-
|
4089
|
+
# Resolve the system path of the specified image path.
|
4090
|
+
#
|
4091
|
+
# Resolve and normalize the absolute system path of the specified image,
|
4092
|
+
# taking into account the imagesdir attribute. If an image path is not
|
4093
|
+
# specified, the path is read from the target attribute of the specified
|
4094
|
+
# document node.
|
4095
|
+
#
|
4096
|
+
# If the target is a URI and the allow-uri-read attribute is set on the
|
4097
|
+
# document, read the file contents to a temporary file and return the path to
|
4098
|
+
# the temporary file. If the target is a URI and the allow-uri-read attribute
|
4099
|
+
# is not set, or the URI cannot be read, this method returns a nil value.
|
4100
|
+
#
|
4101
|
+
# When a temporary file is used, the file is stored in @tmp_files to be cleaned up after conversion.
|
4102
|
+
def resolve_image_path node, image_path, image_format, relative_to = true
|
4103
|
+
doc = node.document
|
4104
|
+
if relative_to == true
|
4105
|
+
imagesdir = nil if (imagesdir = doc.attr 'imagesdir').nil_or_empty? || imagesdir == '.' || imagesdir == './'
|
3869
4106
|
else
|
3870
|
-
|
3871
|
-
end
|
3872
|
-
front_matter_counter = RomanNumeral.new 0, :lower
|
3873
|
-
pagenum_labels = {}
|
3874
|
-
|
3875
|
-
num_front_matter_pages.times do |n|
|
3876
|
-
pagenum_labels[n] = { P: (::PDF::Core::LiteralString.new front_matter_counter.next!.to_s) }
|
3877
|
-
end
|
3878
|
-
|
3879
|
-
# add labels for each content page, which is required for reader's page navigator to work correctly
|
3880
|
-
(num_front_matter_pages..(page_count - 1)).each_with_index do |n, i|
|
3881
|
-
pagenum_labels[n] = { P: (::PDF::Core::LiteralString.new (i + 1).to_s) }
|
3882
|
-
end
|
3883
|
-
|
3884
|
-
unless toc_page_nums.none? || (toc_title = doc.attr 'toc-title').nil_or_empty?
|
3885
|
-
toc_section = insert_toc_section doc, toc_title, toc_page_nums
|
4107
|
+
imagesdir = relative_to
|
3886
4108
|
end
|
3887
|
-
|
3888
|
-
|
3889
|
-
|
3890
|
-
|
3891
|
-
|
3892
|
-
|
3893
|
-
|
3894
|
-
|
3895
|
-
|
3896
|
-
|
3897
|
-
|
3898
|
-
|
3899
|
-
|
3900
|
-
|
3901
|
-
|
3902
|
-
|
3903
|
-
|
3904
|
-
|
3905
|
-
|
3906
|
-
|
3907
|
-
|
3908
|
-
def add_outline_level outline, sections, num_levels, expand_levels
|
3909
|
-
sections.each do |sect|
|
3910
|
-
next if (num_levels_for_sect = (sect.attr 'outlinelevels', num_levels).to_i) < (level = sect.level)
|
3911
|
-
sect_title = sanitize sect.numbered_title formal: true
|
3912
|
-
sect_destination = sect.attr 'pdf-destination'
|
3913
|
-
if level < num_levels_for_sect && sect.sections?
|
3914
|
-
outline.section sect_title, destination: sect_destination, closed: expand_levels < 1 do
|
3915
|
-
add_outline_level outline, sect.sections, num_levels_for_sect, (expand_levels - 1)
|
3916
|
-
end
|
3917
|
-
else
|
3918
|
-
outline.page title: sect_title, destination: sect_destination
|
4109
|
+
# NOTE: base64 logic currently used for inline images
|
4110
|
+
if ::Base64 === image_path
|
4111
|
+
return @tmp_files[image_path] if @tmp_files.key? image_path
|
4112
|
+
tmp_image = ::Tempfile.create %W(image- .#{image_format})
|
4113
|
+
tmp_image.binmode unless image_format == 'svg'
|
4114
|
+
tmp_image.write ::Base64.decode64 image_path
|
4115
|
+
tmp_image.close
|
4116
|
+
@tmp_files[image_path] = tmp_image.path
|
4117
|
+
# NOTE: this will catch a classloader resource path on JRuby (e.g., uri:classloader:/path/to/image)
|
4118
|
+
elsif ::File.absolute_path? image_path
|
4119
|
+
::File.absolute_path image_path
|
4120
|
+
elsif !(is_uri = node.is_uri? image_path) && imagesdir && (::File.absolute_path? imagesdir)
|
4121
|
+
::File.absolute_path image_path, imagesdir
|
4122
|
+
# handle case when image is a URI
|
4123
|
+
elsif is_uri || (imagesdir && (node.is_uri? imagesdir) && (image_path = node.normalize_web_path image_path, imagesdir, false))
|
4124
|
+
if !allow_uri_read
|
4125
|
+
log :warn, %(cannot embed remote image: #{image_path} (allow-uri-read attribute not enabled))
|
4126
|
+
return
|
4127
|
+
elsif @tmp_files.key? image_path
|
4128
|
+
return @tmp_files[image_path]
|
3919
4129
|
end
|
3920
|
-
|
3921
|
-
|
3922
|
-
|
3923
|
-
|
3924
|
-
|
3925
|
-
|
3926
|
-
|
3927
|
-
|
3928
|
-
|
3929
|
-
|
3930
|
-
|
3931
|
-
|
3932
|
-
insert_idx = 0
|
4130
|
+
tmp_image = ::Tempfile.create ['image-', image_format && %(.#{image_format})]
|
4131
|
+
tmp_image.binmode if (binary = image_format != 'svg')
|
4132
|
+
begin
|
4133
|
+
load_open_uri.open_uri(image_path, (binary ? 'rb' : 'r')) {|fd| tmp_image.write fd.read }
|
4134
|
+
tmp_image.close
|
4135
|
+
@tmp_files[image_path] = tmp_image.path
|
4136
|
+
rescue
|
4137
|
+
@tmp_files[image_path] = nil
|
4138
|
+
log :warn, %(could not retrieve remote image: #{image_path}; #{$!.message})
|
4139
|
+
tmp_image.close
|
4140
|
+
unlink_tmp_file tmp_image.path
|
4141
|
+
nil
|
3933
4142
|
end
|
3934
|
-
|
4143
|
+
# handle case when image is a local file
|
3935
4144
|
else
|
3936
|
-
|
3937
|
-
toc_level = doc.sections[0].level
|
3938
|
-
insert_idx = 0
|
3939
|
-
toc_dest = dest_top toc_page_nums.first
|
4145
|
+
node.normalize_system_path image_path, imagesdir, nil, target_name: 'image'
|
3940
4146
|
end
|
3941
|
-
toc_section = Section.new grandparent_section, toc_level, false, attributes: { 'pdf-destination' => toc_dest }
|
3942
|
-
toc_section.title = toc_title
|
3943
|
-
grandparent_section.blocks.insert insert_idx, toc_section
|
3944
|
-
toc_section
|
3945
4147
|
end
|
3946
4148
|
|
3947
|
-
def
|
3948
|
-
if
|
3949
|
-
|
3950
|
-
|
3951
|
-
|
3952
|
-
|
3953
|
-
|
3954
|
-
|
3955
|
-
if (quality = @optimize)
|
3956
|
-
if quality.include? ','
|
3957
|
-
quality, compliance = quality.split ',', 2
|
3958
|
-
elsif quality.include? '/'
|
3959
|
-
quality, compliance = nil, quality
|
4149
|
+
def resolve_text_align_from_role roles, query_theme: false, remove_predefined: false
|
4150
|
+
if (align_role = roles.reverse.find {|r| TextAlignmentRoles.include? r })
|
4151
|
+
roles.replace roles - TextAlignmentRoles if remove_predefined
|
4152
|
+
(align_role.slice 5, align_role.length).to_sym
|
4153
|
+
elsif query_theme
|
4154
|
+
roles.reverse.each do |role|
|
4155
|
+
if (text_align = @theme[%(role_#{role}_text_align)])
|
4156
|
+
return text_align.to_sym
|
3960
4157
|
end
|
3961
|
-
(Optimizer.new quality, pdf_doc.min_version, compliance).optimize_file target
|
3962
4158
|
end
|
3963
|
-
|
3964
|
-
end
|
3965
|
-
if !ENV['KEEP_ARTIFACTS']
|
3966
|
-
remove_tmp_files
|
3967
|
-
elsif to_file
|
3968
|
-
scratch_target = (target.slice 0, target.length - (target_ext = ::File.extname target).length) + '-scratch' + target_ext
|
3969
|
-
scratch.render_file scratch_target
|
4159
|
+
nil
|
3970
4160
|
end
|
3971
|
-
clear_scratch
|
3972
|
-
nil
|
3973
4161
|
end
|
3974
4162
|
|
3975
|
-
|
3976
|
-
|
3977
|
-
|
3978
|
-
|
3979
|
-
|
3980
|
-
|
3981
|
-
|
3982
|
-
|
3983
|
-
|
3984
|
-
|
3985
|
-
|
3986
|
-
|
4163
|
+
# Deprecated
|
4164
|
+
alias resolve_alignment_from_role resolve_text_align_from_role
|
4165
|
+
|
4166
|
+
def stamp_foreground_image doc, has_front_cover
|
4167
|
+
pages = state.pages
|
4168
|
+
if (first_page = (has_front_cover ? (pages.slice 1, pages.size) : pages).find {|it| !it.imported_page? }) &&
|
4169
|
+
(first_page_num = (pages.index first_page) + 1) &&
|
4170
|
+
(fg_image = resolve_background_image doc, @theme, 'page-foreground-image') && fg_image[0]
|
4171
|
+
go_to_page first_page_num
|
4172
|
+
create_stamp 'foreground-image' do
|
4173
|
+
canvas { image fg_image[0], ({ position: :center, vposition: :center }.merge fg_image[1]) }
|
4174
|
+
end
|
4175
|
+
stamp 'foreground-image'
|
4176
|
+
(first_page_num.next..page_count).each do |num|
|
4177
|
+
go_to_page num
|
4178
|
+
stamp 'foreground-image' unless page.imported_page?
|
3987
4179
|
end
|
3988
|
-
register_font key => styles
|
3989
4180
|
end
|
3990
4181
|
end
|
3991
4182
|
|
3992
|
-
def
|
3993
|
-
|
3994
|
-
|
4183
|
+
def start_new_chapter chapter
|
4184
|
+
start_new_page unless at_page_top?
|
4185
|
+
# TODO: must call update_colors before advancing to next page if start_new_page is called in ink_chapter_title
|
4186
|
+
start_new_page if @ppbook && verso_page? && !(chapter.option? 'nonfacing')
|
3995
4187
|
end
|
3996
4188
|
|
3997
|
-
|
3998
|
-
@theme.svg_fallback_font_family || @theme.svg_font_family || @theme.base_font_family
|
3999
|
-
end
|
4189
|
+
alias start_new_part start_new_chapter
|
4000
4190
|
|
4001
|
-
|
4002
|
-
|
4003
|
-
|
4004
|
-
styles: (styles << text_decoration_style),
|
4005
|
-
text_decoration_color: level && @theme[%(#{category}_h#{level}_text_decoration_color)] || @theme[%(#{category}_text_decoration_color)],
|
4006
|
-
text_decoration_width: level && @theme[%(#{category}_h#{level}_text_decoration_width)] || @theme[%(#{category}_text_decoration_width)],
|
4007
|
-
}.compact
|
4008
|
-
else
|
4009
|
-
styles.empty? ? {} : { styles: styles }
|
4010
|
-
end
|
4011
|
-
end
|
4191
|
+
# Returns a Boolean indicating whether the title page was created
|
4192
|
+
def start_title_page doc
|
4193
|
+
return unless doc.header? && !doc.notitle && @theme.title_page != false
|
4012
4194
|
|
4013
|
-
|
4014
|
-
if
|
4015
|
-
|
4016
|
-
elsif
|
4017
|
-
|
4195
|
+
# NOTE: a new page may have already been started at this point, so decide what to do with it
|
4196
|
+
if page.empty?
|
4197
|
+
page.reset_content if (recycle = @ppbook ? recto_page? : true)
|
4198
|
+
elsif @ppbook && page_number > 0 && recto_page?
|
4199
|
+
start_new_page
|
4200
|
+
end
|
4201
|
+
side = page_side (recycle ? nil : page_number + 1), @folio_placement[:inverted]
|
4202
|
+
prev_bg_image = @page_bg_image[side]
|
4203
|
+
prev_bg_color = @page_bg_color
|
4204
|
+
if (bg_image = resolve_background_image doc, @theme, 'title-page-background-image')
|
4205
|
+
@page_bg_image[side] = bg_image[0] && bg_image
|
4018
4206
|
end
|
4019
|
-
|
4020
|
-
|
4021
|
-
# QUESTION: should we pass a category as an argument?
|
4022
|
-
# QUESTION: should we make this a method on the theme ostruct? (e.g., @theme.resolve_color key, fallback)
|
4023
|
-
def resolve_theme_color key, fallback_color = nil, transparent_color = fallback_color
|
4024
|
-
if (color = @theme[key])
|
4025
|
-
color == 'transparent' ? transparent_color : color
|
4026
|
-
else
|
4027
|
-
fallback_color
|
4207
|
+
if (bg_color = resolve_theme_color :title_page_background_color)
|
4208
|
+
@page_bg_color = bg_color
|
4028
4209
|
end
|
4210
|
+
recycle ? float { init_page self } : start_new_page
|
4211
|
+
@page_bg_image[side] = prev_bg_image if bg_image
|
4212
|
+
@page_bg_color = prev_bg_color if bg_color
|
4213
|
+
true
|
4029
4214
|
end
|
4030
4215
|
|
4031
|
-
def
|
4032
|
-
|
4216
|
+
def start_toc_page node, placement
|
4217
|
+
start_new_page unless at_page_top?
|
4218
|
+
start_new_page if @ppbook && verso_page? && !(placement == 'macro' && (node.option? 'nonfacing'))
|
4033
4219
|
end
|
4034
4220
|
|
4035
|
-
def
|
4036
|
-
|
4037
|
-
line_width: @theme[%(#{category}_border_width)],
|
4038
|
-
line_style: (@theme[%(#{category}_border_style)]&.to_sym || :solid),
|
4039
|
-
radius: @theme[%(#{category}_border_radius)]
|
4221
|
+
def supports_float_wrapping? node
|
4222
|
+
node.context == :paragraph
|
4040
4223
|
end
|
4041
4224
|
|
4042
4225
|
def theme_fill_and_stroke_block category, extent, opts = {}
|
@@ -4079,7 +4262,7 @@ module Asciidoctor
|
|
4079
4262
|
advance_page unless first_page
|
4080
4263
|
chunk_height = start_cursor = cursor
|
4081
4264
|
chunk_height -= last_page.cursor if last_page
|
4082
|
-
bounding_box [
|
4265
|
+
bounding_box [bounds.left, start_cursor], width: bounds.width, height: chunk_height do
|
4083
4266
|
theme_fill_and_stroke_bounds category, background_color: bg_color
|
4084
4267
|
unless first_page
|
4085
4268
|
indent b_radius, b_radius do
|
@@ -4100,46 +4283,11 @@ module Asciidoctor
|
|
4100
4283
|
nil
|
4101
4284
|
end
|
4102
4285
|
|
4103
|
-
|
4104
|
-
|
4105
|
-
|
4106
|
-
|
4107
|
-
|
4108
|
-
end
|
4109
|
-
|
4110
|
-
# Insert a bottom margin equal to amount unless cursor is at the top of the
|
4111
|
-
# page (not likely). Start a new page instead if amount is greater than the
|
4112
|
-
# remaining space on the page.
|
4113
|
-
def margin_bottom amount
|
4114
|
-
margin amount, :bottom
|
4115
|
-
end
|
4116
|
-
|
4117
|
-
# Insert a margin at the specified side if the cursor is not at the top of
|
4118
|
-
# the page. Start a new page if amount is greater than the remaining space on
|
4119
|
-
# the page.
|
4120
|
-
def margin amount, _side
|
4121
|
-
if (amount || 0) == 0 || at_page_top?
|
4122
|
-
0
|
4123
|
-
# NOTE: use low-level cursor calculation to workaround cursor bug in column_box context
|
4124
|
-
elsif y - reference_bounds.absolute_bottom > amount
|
4125
|
-
move_down amount
|
4126
|
-
amount
|
4127
|
-
else
|
4128
|
-
# set cursor at top of next page
|
4129
|
-
reference_bounds.move_past_bottom
|
4130
|
-
0
|
4131
|
-
end
|
4132
|
-
end
|
4133
|
-
|
4134
|
-
# Lookup margin for theme element and side, then delegate to margin method.
|
4135
|
-
# If margin value is not found, assume 0.
|
4136
|
-
def theme_margin category, side, node = true
|
4137
|
-
if node
|
4138
|
-
category = :block if node != true && node.context == :section
|
4139
|
-
margin (@theme[%(#{category}_margin_#{side})] || 0), side
|
4140
|
-
else
|
4141
|
-
0
|
4142
|
-
end
|
4286
|
+
def theme_fill_and_stroke_bounds category, opts = {}
|
4287
|
+
fill_and_stroke_bounds opts[:background_color], @theme[%(#{category}_border_color)] || @theme.base_border_color,
|
4288
|
+
line_width: @theme[%(#{category}_border_width)],
|
4289
|
+
line_style: (@theme[%(#{category}_border_style)]&.to_sym || :solid),
|
4290
|
+
radius: @theme[%(#{category}_border_radius)]
|
4143
4291
|
end
|
4144
4292
|
|
4145
4293
|
def theme_font category, opts = {}
|
@@ -4198,87 +4346,15 @@ module Asciidoctor
|
|
4198
4346
|
end
|
4199
4347
|
end
|
4200
4348
|
|
4201
|
-
#
|
4202
|
-
#
|
4203
|
-
|
4204
|
-
|
4205
|
-
|
4206
|
-
|
4207
|
-
arranger = arrange_fragments_by_line fragments
|
4208
|
-
# NOTE: finalizing the line here generates fragments & calculates their widths using the current font settings
|
4209
|
-
# NOTE: it also removes zero-width spaces
|
4210
|
-
arranger.finalize_line
|
4211
|
-
actual_width = width_of_fragments arranger.fragments
|
4212
|
-
padding = expand_padding_value @theme[%(#{category}_padding)]
|
4213
|
-
if actual_width > (available_width = bounds.width - padding[3].to_f - padding[1].to_f)
|
4214
|
-
adjusted_font_size = ((available_width * font_size).to_f / actual_width).truncate 4
|
4215
|
-
if (min = @theme[%(#{category}_font_size_min)] || @theme.base_font_size_min) && adjusted_font_size < min
|
4216
|
-
min
|
4217
|
-
else
|
4218
|
-
adjusted_font_size
|
4219
|
-
end
|
4220
|
-
end
|
4221
|
-
end
|
4222
|
-
|
4223
|
-
# Arrange fragments by line in an arranger and return an unfinalized arranger.
|
4224
|
-
#
|
4225
|
-
# Finalizing the arranger is deferred since it must be done in the context of
|
4226
|
-
# the global font settings you want applied to each fragment.
|
4227
|
-
def arrange_fragments_by_line fragments, _opts = {}
|
4228
|
-
arranger = ::Prawn::Text::Formatted::Arranger.new self
|
4229
|
-
by_line = arranger.consumed = []
|
4230
|
-
fragments.each do |fragment|
|
4231
|
-
if (text = fragment[:text]) == LF || !(text.include? LF)
|
4232
|
-
by_line << fragment
|
4233
|
-
else
|
4234
|
-
text.scan LineScanRx do |line|
|
4235
|
-
by_line << (line == LF ? { text: LF } : (fragment.merge text: line))
|
4236
|
-
end
|
4237
|
-
end
|
4238
|
-
end
|
4239
|
-
arranger
|
4240
|
-
end
|
4241
|
-
|
4242
|
-
# Calculate the width that is needed to print all the
|
4243
|
-
# fragments without wrapping any lines.
|
4244
|
-
#
|
4245
|
-
# This method assumes endlines are represented as discrete entries in the
|
4246
|
-
# fragments array.
|
4247
|
-
def width_of_fragments fragments
|
4248
|
-
line_widths = [0]
|
4249
|
-
fragments.each do |fragment|
|
4250
|
-
if fragment.text == LF
|
4251
|
-
line_widths << 0
|
4252
|
-
else
|
4253
|
-
line_widths[-1] += fragment.width
|
4254
|
-
end
|
4255
|
-
end
|
4256
|
-
line_widths.max
|
4257
|
-
end
|
4258
|
-
|
4259
|
-
# Compute the rendered width of a string, taking fallback fonts into account
|
4260
|
-
def rendered_width_of_string str, opts = {}
|
4261
|
-
opts = opts.merge kerning: default_kerning?
|
4262
|
-
if str.length == 1
|
4263
|
-
rendered_width_of_char str, opts
|
4264
|
-
elsif (chars = str.each_char).all? {|char| font.glyph_present? char }
|
4265
|
-
width_of_string str, opts
|
4349
|
+
# Lookup margin for theme element and side, then delegate to margin method.
|
4350
|
+
# If margin value is not found, assume 0.
|
4351
|
+
def theme_margin category, side, node = true
|
4352
|
+
if node
|
4353
|
+
category = :block if node != true && node.context == :section
|
4354
|
+
margin (@theme[%(#{category}_margin_#{side})] || 0), side
|
4266
4355
|
else
|
4267
|
-
|
4268
|
-
char_widths.sum + (char_widths.length * character_spacing)
|
4269
|
-
end
|
4270
|
-
end
|
4271
|
-
|
4272
|
-
# Compute the rendered width of a char, taking fallback fonts into account
|
4273
|
-
def rendered_width_of_char char, opts = {}
|
4274
|
-
unless @fallback_fonts.empty? || (font.glyph_present? char)
|
4275
|
-
@fallback_fonts.each do |fallback_font|
|
4276
|
-
font fallback_font do
|
4277
|
-
return width_of_string char, opts if font.glyph_present? char
|
4278
|
-
end
|
4279
|
-
end
|
4356
|
+
0
|
4280
4357
|
end
|
4281
|
-
width_of_string char, opts
|
4282
4358
|
end
|
4283
4359
|
|
4284
4360
|
# TODO: document me, esp the first line formatting functionality
|
@@ -4286,7 +4362,7 @@ module Asciidoctor
|
|
4286
4362
|
opts = { leading: line_metrics.leading, final_gap: line_metrics.final_gap }.merge opts
|
4287
4363
|
string = string.gsub CjkLineBreakRx, ZeroWidthSpace if @cjk_line_breaks
|
4288
4364
|
return text_box string, opts if opts[:height]
|
4289
|
-
|
4365
|
+
opts[:initial_gap] = line_metrics.padding_top
|
4290
4366
|
if (hanging_indent = (opts.delete :hanging_indent) || 0) > 0
|
4291
4367
|
indent hanging_indent do
|
4292
4368
|
text string, (opts.merge indent_paragraphs: -hanging_indent)
|
@@ -4302,8 +4378,7 @@ module Asciidoctor
|
|
4302
4378
|
|
4303
4379
|
# QUESTION: combine with typeset_text?
|
4304
4380
|
def typeset_formatted_text fragments, line_metrics, opts = {}
|
4305
|
-
|
4306
|
-
opts = { leading: line_metrics.leading, final_gap: line_metrics.final_gap }.merge opts
|
4381
|
+
opts = { leading: line_metrics.leading, initial_gap: line_metrics.padding_top, final_gap: line_metrics.final_gap }.merge opts
|
4307
4382
|
if (hanging_indent = (opts.delete :hanging_indent) || 0) > 0
|
4308
4383
|
indent hanging_indent do
|
4309
4384
|
formatted_text fragments, (opts.merge indent_paragraphs: -hanging_indent)
|
@@ -4314,94 +4389,67 @@ module Asciidoctor
|
|
4314
4389
|
move_down line_metrics.padding_bottom
|
4315
4390
|
end
|
4316
4391
|
|
4317
|
-
def
|
4318
|
-
|
4319
|
-
|
4320
|
-
|
4321
|
-
|
4322
|
-
# NOTE: only used when tabsize attribute is not specified
|
4323
|
-
# tabs must always be replaced with spaces in order for the indentation guards to work
|
4324
|
-
def expand_tabs string
|
4325
|
-
if string.nil_or_empty?
|
4326
|
-
''
|
4327
|
-
elsif string.include? TAB
|
4328
|
-
full_tab_space = ' ' * (tab_size = 4)
|
4329
|
-
(string.split LF, -1).map do |line|
|
4330
|
-
if line.empty? || !(tab_idx = line.index TAB)
|
4331
|
-
line
|
4332
|
-
else
|
4333
|
-
if tab_idx == 0
|
4334
|
-
leading_tabs = 0
|
4335
|
-
line.each_byte do |b|
|
4336
|
-
break unless b == 9
|
4337
|
-
leading_tabs += 1
|
4338
|
-
end
|
4339
|
-
line = %(#{full_tab_space * leading_tabs}#{rest = line.slice leading_tabs, line.length})
|
4340
|
-
next line unless rest.include? TAB
|
4341
|
-
end
|
4342
|
-
# keeps track of how many spaces were added to adjust offset in match data
|
4343
|
-
spaces_added = 0
|
4344
|
-
idx = 0
|
4345
|
-
result = ''
|
4346
|
-
line.each_char do |c|
|
4347
|
-
if c == TAB
|
4348
|
-
# calculate how many spaces this tab represents, then replace tab with spaces
|
4349
|
-
if (offset = idx + spaces_added) % tab_size == 0
|
4350
|
-
spaces_added += (tab_size - 1)
|
4351
|
-
result += full_tab_space
|
4352
|
-
else
|
4353
|
-
unless (spaces = tab_size - offset % tab_size) == 1
|
4354
|
-
spaces_added += (spaces - 1)
|
4355
|
-
end
|
4356
|
-
result += (' ' * spaces)
|
4357
|
-
end
|
4358
|
-
else
|
4359
|
-
result += c
|
4360
|
-
end
|
4361
|
-
idx += 1
|
4362
|
-
end
|
4363
|
-
result
|
4364
|
-
end
|
4365
|
-
end.join LF
|
4392
|
+
def write pdf_doc, target
|
4393
|
+
if target.respond_to? :write
|
4394
|
+
target = ::QuantifiableStdout.new $stdout if target == $stdout
|
4395
|
+
pdf_doc.render target
|
4366
4396
|
else
|
4367
|
-
|
4397
|
+
pdf_doc.render_file target
|
4398
|
+
# QUESTION: restore attributes first?
|
4399
|
+
@pdfmark&.generate_file target
|
4400
|
+
if (quality = @optimize)
|
4401
|
+
if quality.include? ','
|
4402
|
+
quality, compliance = quality.split ',', 2
|
4403
|
+
elsif quality.include? '/'
|
4404
|
+
quality, compliance = nil, quality
|
4405
|
+
end
|
4406
|
+
(Optimizer.new quality, pdf_doc.min_version, compliance).optimize_file target
|
4407
|
+
end
|
4408
|
+
to_file = true
|
4409
|
+
end
|
4410
|
+
if !ENV['KEEP_ARTIFACTS']
|
4411
|
+
remove_tmp_files
|
4412
|
+
elsif to_file
|
4413
|
+
scratch_target = (target.slice 0, target.length - (target_ext = ::File.extname target).length) + '-scratch' + target_ext
|
4414
|
+
scratch.render_file scratch_target
|
4368
4415
|
end
|
4416
|
+
clear_scratch
|
4417
|
+
nil
|
4369
4418
|
end
|
4370
4419
|
|
4371
|
-
#
|
4372
|
-
|
4373
|
-
|
4374
|
-
|
4375
|
-
|
4376
|
-
|
4420
|
+
# Deprecated method names
|
4421
|
+
alias layout_footnotes ink_footnotes
|
4422
|
+
alias layout_title_page ink_title_page
|
4423
|
+
alias layout_cover_page ink_cover_page
|
4424
|
+
alias layout_chapter_title ink_chapter_title
|
4425
|
+
alias layout_part_title ink_part_title
|
4426
|
+
alias layout_general_heading ink_general_heading
|
4427
|
+
alias layout_heading ink_heading
|
4428
|
+
alias layout_prose ink_prose
|
4429
|
+
alias layout_caption ink_caption
|
4430
|
+
alias layout_table_caption ink_table_caption
|
4431
|
+
alias layout_toc ink_toc
|
4432
|
+
alias layout_toc_level ink_toc_level
|
4433
|
+
alias layout_running_content ink_running_content
|
4434
|
+
|
4435
|
+
# intercepts "class CustomPDFConverter < (Asciidoctor::Converter.for 'pdf')"
|
4436
|
+
def self.method_added method_sym
|
4437
|
+
if (method_name = method_sym.to_s).start_with? 'layout_'
|
4438
|
+
alias_method %(ink_#{method_name.slice 7, method_name.length}).to_sym, method_sym
|
4439
|
+
elsif method_name == 'convert_listing_or_literal' || method_name == 'convert_code'
|
4440
|
+
alias_method :convert_listing, method_sym
|
4441
|
+
alias_method :convert_literal, method_sym
|
4377
4442
|
end
|
4378
|
-
string
|
4379
4443
|
end
|
4380
4444
|
|
4381
|
-
|
4382
|
-
|
4383
|
-
|
4384
|
-
|
4385
|
-
|
4386
|
-
fragment[:text] = GuardedIndent + (((text = text.slice 1, text.length).include? InnerIndent) ? (text.gsub InnerIndent, GuardedInnerIndent) : text)
|
4387
|
-
elsif text.include? InnerIndent
|
4388
|
-
fragment[:text] = text.gsub InnerIndent, GuardedInnerIndent
|
4389
|
-
end
|
4390
|
-
start_of_line = text.end_with? LF
|
4391
|
-
end
|
4392
|
-
fragments
|
4445
|
+
# intercepts "(Asciidoctor::Converter.for 'pdf').prepend CustomConverterExtensions"
|
4446
|
+
def self.prepend *mods
|
4447
|
+
super
|
4448
|
+
mods.each {|mod| (mod.instance_methods false).each {|method| method_added method } }
|
4449
|
+
self
|
4393
4450
|
end
|
4394
4451
|
|
4395
|
-
|
4396
|
-
# Encodes value into hex if it contains characters outside the ASCII range.
|
4397
|
-
# If value is nil, derive an anchor name from the default_value, if given.
|
4398
|
-
def derive_anchor_from_id value, default_value = nil
|
4399
|
-
if value
|
4400
|
-
value.ascii_only? ? value : %(0x#{::PDF::Core.string_to_hex value})
|
4401
|
-
else
|
4402
|
-
%(__anchor-#{default_value})
|
4403
|
-
end
|
4404
|
-
end
|
4452
|
+
private
|
4405
4453
|
|
4406
4454
|
def add_dest_for_top doc
|
4407
4455
|
unless (top_page = doc.attr 'pdf-page-start') > page_count
|
@@ -4414,302 +4462,359 @@ module Asciidoctor
|
|
4414
4462
|
nil
|
4415
4463
|
end
|
4416
4464
|
|
4417
|
-
|
4418
|
-
|
4419
|
-
|
4420
|
-
|
4421
|
-
|
4422
|
-
|
4423
|
-
|
4424
|
-
|
4425
|
-
|
4426
|
-
|
4427
|
-
|
4428
|
-
|
4429
|
-
|
4430
|
-
|
4431
|
-
|
4432
|
-
|
4433
|
-
|
4434
|
-
|
4465
|
+
def add_link_to_image uri, image_info, image_opts
|
4466
|
+
image_width = image_info[:width]
|
4467
|
+
image_height = image_info[:height]
|
4468
|
+
|
4469
|
+
case image_opts[:position]
|
4470
|
+
when :center
|
4471
|
+
image_x = bounds.left_side + (bounds.width - image_width) * 0.5
|
4472
|
+
when :right
|
4473
|
+
image_x = bounds.right_side - image_width
|
4474
|
+
else # :left, nil
|
4475
|
+
image_x = bounds.left_side
|
4476
|
+
end
|
4477
|
+
|
4478
|
+
case image_opts[:vposition]
|
4479
|
+
when :top
|
4480
|
+
image_y = bounds.absolute_top
|
4481
|
+
when :center
|
4482
|
+
image_y = bounds.absolute_top - (bounds.height - image_height) * 0.5
|
4483
|
+
when :bottom
|
4484
|
+
image_y = bounds.absolute_bottom + image_height
|
4485
|
+
else
|
4486
|
+
image_y = y - image_opts[:vposition]
|
4487
|
+
end unless (image_y = image_opts[:y])
|
4488
|
+
|
4489
|
+
link_annotation [image_x, (image_y - image_height), (image_x + image_width), image_y], Border: [0, 0, 0], A: { Type: :Action, S: :URI, URI: uri.as_pdf }
|
4490
|
+
end
|
4491
|
+
|
4492
|
+
def admonition_icon_data key
|
4493
|
+
if (icon_data = @theme[%(admonition_icon_#{key})])
|
4494
|
+
icon_data = (AdmonitionIcons[key] || {}).merge icon_data
|
4495
|
+
if (icon_name = icon_data[:name])
|
4496
|
+
unless icon_name.start_with?(*IconSetPrefixes)
|
4497
|
+
log(:info) { %(#{key} admonition in theme uses icon from deprecated fa icon set; use fas, far, or fab instead) }
|
4498
|
+
icon_data[:name] = %(fa-#{icon_name}) unless icon_name.start_with? 'fa-'
|
4499
|
+
end
|
4500
|
+
else
|
4501
|
+
icon_data[:name] = AdmonitionIcons[:note][:name]
|
4435
4502
|
end
|
4436
|
-
|
4437
|
-
|
4438
|
-
add_dest id, node_dest
|
4503
|
+
else
|
4504
|
+
(icon_data = AdmonitionIcons[key] || {})[:name] ||= AdmonitionIcons[:note][:name]
|
4439
4505
|
end
|
4440
|
-
|
4506
|
+
icon_data
|
4441
4507
|
end
|
4442
4508
|
|
4443
|
-
def
|
4444
|
-
if
|
4445
|
-
|
4446
|
-
|
4447
|
-
|
4509
|
+
def allocate_space_for_list_item line_metrics
|
4510
|
+
advance_page if !at_page_top? && cursor < line_metrics.height + line_metrics.leading + line_metrics.padding_top
|
4511
|
+
end
|
4512
|
+
|
4513
|
+
def apply_text_decoration styles, category, level = nil
|
4514
|
+
if (text_decoration_style = TextDecorationStyleTable[level && @theme[%(#{category}_h#{level}_text_decoration)] || @theme[%(#{category}_text_decoration)]])
|
4515
|
+
{
|
4516
|
+
styles: (styles << text_decoration_style),
|
4517
|
+
text_decoration_color: level && @theme[%(#{category}_h#{level}_text_decoration_color)] || @theme[%(#{category}_text_decoration_color)],
|
4518
|
+
text_decoration_width: level && @theme[%(#{category}_h#{level}_text_decoration_width)] || @theme[%(#{category}_text_decoration_width)],
|
4519
|
+
}.compact
|
4448
4520
|
else
|
4449
|
-
|
4521
|
+
styles.empty? ? {} : { styles: styles }
|
4450
4522
|
end
|
4451
4523
|
end
|
4452
4524
|
|
4453
|
-
|
4454
|
-
|
4455
|
-
|
4456
|
-
|
4457
|
-
|
4458
|
-
|
4459
|
-
|
4460
|
-
|
4525
|
+
# Arrange fragments by line in an arranger and return an unfinalized arranger.
|
4526
|
+
#
|
4527
|
+
# Finalizing the arranger is deferred since it must be done in the context of
|
4528
|
+
# the global font settings you want applied to each fragment.
|
4529
|
+
def arrange_fragments_by_line fragments, _opts = {}
|
4530
|
+
arranger = ::Prawn::Text::Formatted::Arranger.new self
|
4531
|
+
by_line = arranger.consumed = []
|
4532
|
+
fragments.each do |fragment|
|
4533
|
+
if (text = fragment[:text]) == LF || !(text.include? LF)
|
4534
|
+
by_line << fragment
|
4535
|
+
else
|
4536
|
+
text.scan LineScanRx do |line|
|
4537
|
+
by_line << (line == LF ? { text: LF } : (fragment.merge text: line))
|
4461
4538
|
end
|
4462
4539
|
end
|
4463
|
-
nil
|
4464
4540
|
end
|
4541
|
+
arranger
|
4465
4542
|
end
|
4466
4543
|
|
4467
|
-
#
|
4468
|
-
|
4544
|
+
# NOTE: assume URL is escaped (i.e., contains character references such as &)
|
4545
|
+
def breakable_uri uri
|
4546
|
+
scheme, address = uri.split UriSchemeBoundaryRx, 2
|
4547
|
+
address, scheme = scheme, address unless address
|
4548
|
+
unless address.nil_or_empty?
|
4549
|
+
address = address.gsub UriBreakCharsRx, UriBreakCharRepl
|
4550
|
+
# NOTE: require at least two characters after a break
|
4551
|
+
address.slice!(-2) if address[-2] == ZeroWidthSpace
|
4552
|
+
end
|
4553
|
+
%(#{scheme}#{address})
|
4554
|
+
end
|
4469
4555
|
|
4470
|
-
#
|
4471
|
-
|
4472
|
-
|
4473
|
-
|
4556
|
+
# Calculate the font size (down to the minimum font size) that would allow
|
4557
|
+
# all the specified fragments to fit in the available width without wrapping lines.
|
4558
|
+
#
|
4559
|
+
# Return the calculated font size if an adjustment is necessary or nil if no
|
4560
|
+
# font size adjustment is necessary.
|
4561
|
+
def compute_autofit_font_size fragments, category
|
4562
|
+
arranger = arrange_fragments_by_line fragments
|
4563
|
+
# NOTE: finalizing the line here generates fragments & calculates their widths using the current font settings
|
4564
|
+
# NOTE: it also removes zero-width spaces
|
4565
|
+
arranger.finalize_line
|
4566
|
+
actual_width = width_of_fragments arranger.fragments
|
4567
|
+
padding = expand_padding_value @theme[%(#{category}_padding)]
|
4568
|
+
if actual_width > (available_width = bounds.width - padding[3].to_f - padding[1].to_f)
|
4569
|
+
adjusted_font_size = ((available_width * font_size).to_f / actual_width).truncate 4
|
4570
|
+
if (min = @theme[%(#{category}_font_size_min)] || @theme.base_font_size_min) && adjusted_font_size < min
|
4571
|
+
min
|
4572
|
+
else
|
4573
|
+
adjusted_font_size
|
4574
|
+
end
|
4575
|
+
end
|
4576
|
+
end
|
4577
|
+
|
4578
|
+
def consolidate_ranges nums
|
4579
|
+
if nums.size > 1
|
4580
|
+
prev = nil
|
4581
|
+
nums.each_with_object [] do |num, accum|
|
4582
|
+
if prev && (prev.to_i + 1) == num.to_i
|
4583
|
+
accum[-1][1] = num
|
4584
|
+
else
|
4585
|
+
accum << [num]
|
4586
|
+
end
|
4587
|
+
prev = num
|
4588
|
+
end.map {|range| range.join '-' }
|
4474
4589
|
else
|
4475
|
-
|
4590
|
+
nums
|
4476
4591
|
end
|
4477
4592
|
end
|
4478
4593
|
|
4479
|
-
|
4480
|
-
|
4481
|
-
|
4482
|
-
|
4483
|
-
#
|
4484
|
-
#
|
4485
|
-
#
|
4486
|
-
|
4487
|
-
|
4488
|
-
|
4489
|
-
|
4490
|
-
|
4491
|
-
|
4492
|
-
|
4493
|
-
|
4494
|
-
|
4495
|
-
|
4496
|
-
|
4497
|
-
|
4498
|
-
|
4499
|
-
|
4500
|
-
|
4501
|
-
|
4502
|
-
@tmp_files[image_path] = tmp_image.path
|
4503
|
-
# NOTE: this will catch a classloader resource path on JRuby (e.g., uri:classloader:/path/to/image)
|
4504
|
-
elsif ::File.absolute_path? image_path
|
4505
|
-
::File.absolute_path image_path
|
4506
|
-
elsif !(is_uri = node.is_uri? image_path) && imagesdir && (::File.absolute_path? imagesdir)
|
4507
|
-
::File.absolute_path image_path, imagesdir
|
4508
|
-
# handle case when image is a URI
|
4509
|
-
elsif is_uri || (imagesdir && (node.is_uri? imagesdir) && (image_path = node.normalize_web_path image_path, imagesdir, false))
|
4510
|
-
if !allow_uri_read
|
4511
|
-
log :warn, %(cannot embed remote image: #{image_path} (allow-uri-read attribute not enabled))
|
4512
|
-
return
|
4513
|
-
elsif @tmp_files.key? image_path
|
4514
|
-
return @tmp_files[image_path]
|
4594
|
+
def conum_glyph number
|
4595
|
+
@conum_glyphs[number - 1]
|
4596
|
+
end
|
4597
|
+
|
4598
|
+
# Derive a PDF-safe, ASCII-only anchor name from the given value.
|
4599
|
+
# Encodes value into hex if it contains characters outside the ASCII range.
|
4600
|
+
# If value is nil, derive an anchor name from the default_value, if given.
|
4601
|
+
def derive_anchor_from_id value, default_value = nil
|
4602
|
+
if value
|
4603
|
+
value.ascii_only? ? value : %(0x#{::PDF::Core.string_to_hex value})
|
4604
|
+
else
|
4605
|
+
%(__anchor-#{default_value})
|
4606
|
+
end
|
4607
|
+
end
|
4608
|
+
|
4609
|
+
def draw_image_border top, w, h, alignment
|
4610
|
+
if (Array @theme.image_border_width).any? {|it| it&.> 0 } && (@theme.image_border_color || @theme.base_border_color)
|
4611
|
+
if (@theme.image_border_fit || 'content') == 'auto'
|
4612
|
+
bb_width = bounds.width
|
4613
|
+
elsif alignment == :center
|
4614
|
+
bb_x = (bounds.width - w) * 0.5
|
4615
|
+
elsif alignment == :right
|
4616
|
+
bb_x = bounds.width - w
|
4515
4617
|
end
|
4516
|
-
|
4517
|
-
|
4518
|
-
begin
|
4519
|
-
load_open_uri.open_uri(image_path, (binary ? 'rb' : 'r')) {|fd| tmp_image.write fd.read }
|
4520
|
-
tmp_image.close
|
4521
|
-
@tmp_files[image_path] = tmp_image.path
|
4522
|
-
rescue
|
4523
|
-
@tmp_files[image_path] = nil
|
4524
|
-
log :warn, %(could not retrieve remote image: #{image_path}; #{$!.message})
|
4525
|
-
tmp_image.close
|
4526
|
-
unlink_tmp_file tmp_image.path
|
4527
|
-
nil
|
4618
|
+
bounding_box [(bb_x || 0), top], width: (bb_width || w), height: h, position: alignment do
|
4619
|
+
theme_fill_and_stroke_bounds :image
|
4528
4620
|
end
|
4529
|
-
|
4530
|
-
else
|
4531
|
-
node.normalize_system_path image_path, imagesdir, nil, target_name: 'image'
|
4621
|
+
true
|
4532
4622
|
end
|
4533
4623
|
end
|
4534
4624
|
|
4535
|
-
|
4536
|
-
|
4537
|
-
|
4625
|
+
# Reduce icon height to fit inside bounds.height. Icons will not render
|
4626
|
+
# properly if they are larger than the current bounds.height.
|
4627
|
+
def fit_icon_to_bounds preferred_size
|
4628
|
+
(max_height = bounds.height) < preferred_size ? max_height : preferred_size
|
4629
|
+
end
|
4630
|
+
|
4631
|
+
def font_path font_file, fonts_dir
|
4632
|
+
# resolve relative to built-in font dir unless path is absolute
|
4633
|
+
::File.absolute_path font_file, fonts_dir
|
4634
|
+
end
|
4635
|
+
|
4636
|
+
def generate_manname_section node
|
4637
|
+
title = node.attr 'manname-title', 'Name'
|
4638
|
+
if (next_section_title = node.sections[0]&.title) && next_section_title.upcase == next_section_title
|
4639
|
+
title = title.upcase
|
4538
4640
|
end
|
4641
|
+
sect = Section.new node, 1
|
4642
|
+
sect.sectname = 'section'
|
4643
|
+
sect.id = node.attr 'manname-id'
|
4644
|
+
sect.title = title
|
4645
|
+
sect << (Block.new sect, :paragraph, source: %(#{node.attr 'manname'} - #{node.attr 'manpurpose'}), subs: :normal)
|
4646
|
+
sect
|
4647
|
+
end
|
4648
|
+
|
4649
|
+
def get_char code
|
4650
|
+
(code.start_with? '\u') ? ([((code.slice 2, code.length).to_i 16)].pack 'U1') : code
|
4651
|
+
end
|
4652
|
+
|
4653
|
+
def get_icon_image_path node, type, resolve = true
|
4654
|
+
doc = node.document
|
4655
|
+
doc.remove_attr 'data-uri' if (data_uri_enabled = doc.attr? 'data-uri')
|
4539
4656
|
# NOTE: icon_uri will consider icon attribute on node first, then type
|
4540
4657
|
icon_path, icon_format = ::Asciidoctor::Image.target_and_format node.icon_uri type
|
4541
4658
|
doc.set_attr 'data-uri', '' if data_uri_enabled
|
4542
4659
|
resolve ? (resolve_image_path node, icon_path, icon_format, nil) : icon_path
|
4543
4660
|
end
|
4544
4661
|
|
4545
|
-
|
4546
|
-
|
4547
|
-
|
4548
|
-
|
4549
|
-
|
4550
|
-
|
4551
|
-
|
4552
|
-
|
4553
|
-
|
4554
|
-
|
4555
|
-
|
4662
|
+
def init_float_box _node, block_width, block_height, float_to
|
4663
|
+
gap = ::Array === (gap = @theme.image_float_gap) ? gap.dup : [gap, gap]
|
4664
|
+
float_w = block_width + (gap[0] ||= 12)
|
4665
|
+
float_h = block_height + (gap[1] ||= 6)
|
4666
|
+
box_l = bounds.left + (float_to == 'right' ? 0 : float_w)
|
4667
|
+
box_t = cursor + block_height
|
4668
|
+
box_w = bounds.width - float_w
|
4669
|
+
box_r = box_l + box_w
|
4670
|
+
box_h = [box_t, float_h].min
|
4671
|
+
box_b = box_t - box_h
|
4672
|
+
move_cursor_to box_t
|
4673
|
+
@float_box = { page: page_number, top: box_t, right: box_r, bottom: box_b, left: box_l, width: box_w, height: box_h, gap: gap }
|
4674
|
+
end
|
4675
|
+
|
4676
|
+
# NOTE: init_page is called within a float context; this will suppress prawn-svg messing with the cursor
|
4677
|
+
# NOTE: init_page is not called for imported pages, front and back cover pages, and other image pages
|
4678
|
+
def init_page *_args
|
4679
|
+
next_page_side = page_side nil, @folio_placement[:inverted]
|
4680
|
+
if @media == 'prepress' && (next_page_margin = @page_margin_by_side[page_number == 1 ? :cover : next_page_side]) != page_margin
|
4681
|
+
set_page_margin next_page_margin
|
4556
4682
|
end
|
4557
|
-
|
4558
|
-
|
4559
|
-
|
4560
|
-
|
4561
|
-
|
4562
|
-
|
4563
|
-
|
4564
|
-
|
4565
|
-
|
4566
|
-
|
4567
|
-
|
4683
|
+
unless @page_bg_color == 'FFFFFF'
|
4684
|
+
tare = true
|
4685
|
+
fill_absolute_bounds @page_bg_color
|
4686
|
+
end
|
4687
|
+
if (bg_image_path, bg_image_opts = @page_bg_image[next_page_side])
|
4688
|
+
tare = true
|
4689
|
+
begin
|
4690
|
+
if bg_image_opts[:format] == 'pdf'
|
4691
|
+
# NOTE: pages that use PDF for the background do not support a background color or running content
|
4692
|
+
# IMPORTANT: the background PDF must have the same dimensions as the current PDF
|
4693
|
+
import_page bg_image_path, (bg_image_opts.merge replace: true, advance: false, advance_if_missing: false)
|
4568
4694
|
else
|
4569
|
-
|
4570
|
-
image_relative_to = true
|
4695
|
+
canvas { image bg_image_path, ({ position: :center, vposition: :center }.merge bg_image_opts) }
|
4571
4696
|
end
|
4572
|
-
|
4573
|
-
|
4574
|
-
|
4697
|
+
rescue
|
4698
|
+
facing_page_side = (PageSides - [next_page_side])[0]
|
4699
|
+
@page_bg_image[facing_page_side] = nil if @page_bg_image[facing_page_side] == @page_bg_image[next_page_side]
|
4700
|
+
@page_bg_image[next_page_side] = nil
|
4701
|
+
log :warn, %(could not embed page background image: #{bg_image_path}; #{$!.message})
|
4575
4702
|
end
|
4703
|
+
end
|
4704
|
+
page.tare_content_stream if tare
|
4705
|
+
end
|
4576
4706
|
|
4577
|
-
|
4578
|
-
|
4579
|
-
|
4580
|
-
|
4581
|
-
|
4582
|
-
|
4583
|
-
|
4584
|
-
return
|
4707
|
+
def ink_paragraph_in_float_box node, float_box, prose_opts, role_keys, block_next, insert_margin_bottom
|
4708
|
+
@float_box = para_font_descender = para_font_size = end_cursor = nil
|
4709
|
+
if role_keys
|
4710
|
+
line_metrics = theme_font_cascade role_keys do
|
4711
|
+
para_font_descender = font.descender
|
4712
|
+
para_font_size = font_size
|
4713
|
+
calc_line_metrics @base_line_height
|
4585
4714
|
end
|
4586
|
-
|
4587
|
-
|
4588
|
-
|
4715
|
+
else
|
4716
|
+
para_font_descender = font.descender
|
4717
|
+
para_font_size = font_size
|
4718
|
+
line_metrics = calc_line_metrics @base_line_height
|
4719
|
+
end
|
4720
|
+
# allocate the space of at least one empty line below block
|
4721
|
+
line_height_length = line_metrics.height + line_metrics.leading + line_metrics.padding_top
|
4722
|
+
start_page_number = float_box[:page]
|
4723
|
+
start_cursor = cursor
|
4724
|
+
block_bottom = (float_box_bottom = float_box[:bottom]) + float_box[:gap][1]
|
4725
|
+
# use :at to incorporate padding top from line metrics since text_box method does not apply it
|
4726
|
+
# use :final_gap to incorporate padding bottom from line metrics
|
4727
|
+
# use :draw_text_callback to track end cursor (requires applying :final_gap to result manually)
|
4728
|
+
prose_opts.update \
|
4729
|
+
at: [float_box[:left], start_cursor - line_metrics.padding_top],
|
4730
|
+
width: float_box[:width],
|
4731
|
+
height: [cursor, float_box[:height] - (float_box[:top] - start_cursor) + line_height_length].min,
|
4732
|
+
final_gap: para_font_descender + line_metrics.padding_bottom,
|
4733
|
+
draw_text_callback: (proc do |text, opts|
|
4734
|
+
draw_text! text, opts
|
4735
|
+
end_cursor = opts[:at][1] # does not include :final_gap value
|
4736
|
+
end)
|
4737
|
+
overflow_text = role_keys ?
|
4738
|
+
theme_font_cascade(role_keys) { ink_prose node.content, prose_opts } :
|
4739
|
+
(ink_prose node.content, prose_opts)
|
4740
|
+
move_cursor_to end_cursor -= prose_opts[:final_gap] if end_cursor # ink_prose with :height does not move cursor
|
4741
|
+
if overflow_text.empty?
|
4742
|
+
if block_next && (supports_float_wrapping? block_next)
|
4743
|
+
insert_margin_bottom.call
|
4744
|
+
@float_box = float_box if page_number == start_page_number && cursor > start_cursor - prose_opts[:height]
|
4745
|
+
elsif end_cursor > block_bottom
|
4746
|
+
move_cursor_to block_bottom
|
4747
|
+
theme_margin :block, :bottom, block_next
|
4589
4748
|
else
|
4590
|
-
|
4749
|
+
insert_margin_bottom.call
|
4750
|
+
end
|
4751
|
+
else
|
4752
|
+
overflow_prose_opts = { align: prose_opts[:align] || @base_text_align.to_sym }
|
4753
|
+
unless end_cursor
|
4754
|
+
overflow_prose_opts[:indent_paragraphs] = prose_opts[:indent_paragraphs]
|
4755
|
+
move_cursor_to float_box_bottom if start_cursor > float_box_bottom
|
4591
4756
|
end
|
4757
|
+
role_keys ?
|
4758
|
+
theme_font_cascade(role_keys) { typeset_formatted_text overflow_text, line_metrics, overflow_prose_opts } :
|
4759
|
+
(typeset_formatted_text overflow_text, line_metrics, overflow_prose_opts)
|
4760
|
+
insert_margin_bottom.call
|
4592
4761
|
end
|
4593
4762
|
end
|
4594
4763
|
|
4595
|
-
def
|
4596
|
-
if
|
4597
|
-
|
4598
|
-
|
4599
|
-
|
4600
|
-
|
4601
|
-
|
4602
|
-
|
4603
|
-
|
4764
|
+
def insert_toc_section doc, toc_title, toc_page_nums
|
4765
|
+
if (doc.attr? 'toc-placement', 'macro') && (toc_node = (doc.find_by context: :toc)[0])
|
4766
|
+
if (parent_section = toc_node.parent).context == :section
|
4767
|
+
grandparent_section = parent_section.parent
|
4768
|
+
toc_level = parent_section.level
|
4769
|
+
insert_idx = (grandparent_section.blocks.index parent_section) + 1
|
4770
|
+
else
|
4771
|
+
grandparent_section = doc
|
4772
|
+
toc_level = doc.sections[0].level
|
4773
|
+
insert_idx = 0
|
4774
|
+
end
|
4775
|
+
toc_dest = toc_node.attr 'pdf-destination'
|
4604
4776
|
else
|
4605
|
-
|
4777
|
+
grandparent_section = doc
|
4778
|
+
toc_level = doc.sections[0].level
|
4779
|
+
insert_idx = 0
|
4780
|
+
toc_dest = dest_top toc_page_nums.first
|
4606
4781
|
end
|
4607
|
-
|
4608
|
-
|
4609
|
-
|
4610
|
-
|
4611
|
-
|
4612
|
-
|
4613
|
-
|
4614
|
-
|
4615
|
-
|
4616
|
-
|
4617
|
-
|
4618
|
-
|
4619
|
-
|
4620
|
-
|
4621
|
-
|
4622
|
-
|
4623
|
-
|
4624
|
-
|
4625
|
-
|
4626
|
-
|
4627
|
-
|
4628
|
-
|
4629
|
-
image_size = intrinsic_image_dimensions image_path, image_format
|
4630
|
-
if image_width * (image_size[:height].to_f / image_size[:width]) > container_height
|
4631
|
-
image_opts[:fit] = container_size
|
4632
|
-
else
|
4633
|
-
image_opts[:width] = image_width
|
4634
|
-
end
|
4635
|
-
end
|
4636
|
-
else
|
4637
|
-
image_size = intrinsic_image_dimensions image_path, image_format
|
4638
|
-
image_opts[:fit] = container_size if image_size[:width] > container_width || image_size[:height] > container_height
|
4639
|
-
end
|
4640
|
-
when 'cover'
|
4641
|
-
# QUESTION: should we take explicit width into account?
|
4642
|
-
image_size = intrinsic_image_dimensions image_path, image_format
|
4643
|
-
if container_width * (image_size[:height].to_f / image_size[:width]) < container_height
|
4644
|
-
image_opts[:height] = container_height
|
4645
|
-
else
|
4646
|
-
image_opts[:width] = container_width
|
4647
|
-
end
|
4648
|
-
when 'fill'
|
4649
|
-
image_opts[:width] = container_width
|
4650
|
-
image_opts[:height] = container_height
|
4651
|
-
else # 'contain'
|
4652
|
-
image_opts[:fit] = container_size
|
4653
|
-
end
|
4654
|
-
elsif (image_width = resolve_explicit_width image_attrs, bounds_width: container_size[0])
|
4655
|
-
image_opts[:width] = image_width
|
4656
|
-
else # default to fit=contain if sizing is not specified
|
4657
|
-
image_opts[:fit] = container_size
|
4658
|
-
end
|
4782
|
+
toc_section = Section.new grandparent_section, toc_level, false, attributes: { 'pdf-destination' => toc_dest }
|
4783
|
+
toc_section.title = toc_title
|
4784
|
+
grandparent_section.blocks.insert insert_idx, toc_section
|
4785
|
+
toc_section
|
4786
|
+
end
|
4787
|
+
|
4788
|
+
def load_open_uri
|
4789
|
+
if @cache_uri && !(defined? ::OpenURI::Cache) && (Helpers.require_library 'open-uri/cached', 'open-uri-cached', :warn).nil?
|
4790
|
+
# disable URI caching if library fails to load
|
4791
|
+
@cache_uri = false
|
4792
|
+
end
|
4793
|
+
::OpenURI
|
4794
|
+
end
|
4795
|
+
|
4796
|
+
def on_image_error _reason, node, target, opts
|
4797
|
+
log :warn, opts[:message] if opts.key? :message
|
4798
|
+
alt_text_vars = { alt: (node.attr 'alt'), target: target }
|
4799
|
+
alt_text_template = @theme.image_alt_content || '%{link}[%{alt}]%{/link} | <em>%{target}</em>' # rubocop:disable Style/FormatStringToken
|
4800
|
+
return if alt_text_template.empty?
|
4801
|
+
if (link = node.attr 'link')
|
4802
|
+
alt_text_vars[:link] = %(<a href="#{link}">)
|
4803
|
+
alt_text_vars[:'/link'] = '</a>'
|
4659
4804
|
else
|
4660
|
-
|
4805
|
+
alt_text_vars[:link] = ''
|
4806
|
+
alt_text_vars[:'/link'] = ''
|
4661
4807
|
end
|
4662
|
-
|
4808
|
+
theme_font :image_alt do
|
4809
|
+
ink_prose alt_text_template % alt_text_vars, align: opts[:align], margin: 0, normalize: false, single_line: true
|
4810
|
+
end
|
4811
|
+
ink_caption node, category: :image, end: :bottom if node.title?
|
4812
|
+
theme_margin :block, :bottom, (next_enclosed_block node) unless opts[:pinned]
|
4813
|
+
nil
|
4663
4814
|
end
|
4664
4815
|
|
4665
|
-
|
4666
|
-
|
4667
|
-
# Resolves the explicit width, first considering the pdfwidth attribute, then the scaledwidth
|
4668
|
-
# attribute, then the theme default (if enabled by the :use_fallback option), and finally the
|
4669
|
-
# width attribute. If the specified value is in pixels, the value is scaled by 75% to perform
|
4670
|
-
# approximate CSS px to PDF pt conversion. If the value is a percentage, and the
|
4671
|
-
# bounds_width option is given, the percentage of the bounds_width value is returned.
|
4672
|
-
# Otherwise, the percentage width is returned.
|
4673
|
-
#--
|
4674
|
-
# QUESTION: should we enforce positive result?
|
4675
|
-
def resolve_explicit_width attrs, opts = {}
|
4676
|
-
bounds_width = opts[:bounds_width]
|
4677
|
-
# QUESTION: should we restrict width to bounds_width for pdfwidth?
|
4678
|
-
if attrs.key? 'pdfwidth'
|
4679
|
-
if (width = attrs['pdfwidth']).end_with? '%'
|
4680
|
-
bounds_width ? (width.to_f / 100) * bounds_width : width
|
4681
|
-
elsif opts[:support_vw] && (width.end_with? 'vw')
|
4682
|
-
(width.chomp 'vw').extend ViewportWidth
|
4683
|
-
else
|
4684
|
-
str_to_pt width
|
4685
|
-
end
|
4686
|
-
elsif attrs.key? 'scaledwidth'
|
4687
|
-
# NOTE: the parser automatically appends % if value is unitless
|
4688
|
-
if (width = attrs['scaledwidth']).end_with? '%'
|
4689
|
-
bounds_width ? (width.to_f / 100) * bounds_width : width
|
4690
|
-
else
|
4691
|
-
str_to_pt width
|
4692
|
-
end
|
4693
|
-
elsif opts[:use_fallback] && (width = @theme.image_width)
|
4694
|
-
if ::Numeric === width
|
4695
|
-
width
|
4696
|
-
elsif (width = width.to_s).end_with? '%'
|
4697
|
-
bounds_width ? (width.to_f / 100) * bounds_width : bounds_width
|
4698
|
-
elsif opts[:support_vw] && (width.end_with? 'vw')
|
4699
|
-
(width.chomp 'vw').extend ViewportWidth
|
4700
|
-
else
|
4701
|
-
str_to_pt width
|
4702
|
-
end
|
4703
|
-
elsif attrs.key? 'width'
|
4704
|
-
if (width = attrs['width']).end_with? '%'
|
4705
|
-
width = (width.to_f / 100) * bounds_width if bounds_width
|
4706
|
-
elsif DigitsRx.match? width
|
4707
|
-
width = to_pt width.to_f, :px
|
4708
|
-
else
|
4709
|
-
return
|
4710
|
-
end
|
4711
|
-
bounds_width && opts[:constrain_to_bounds] ? [bounds_width, width].min : width
|
4712
|
-
end
|
4816
|
+
def remove_tmp_files
|
4817
|
+
@tmp_files.reject! {|_, path| path ? (unlink_tmp_file path) : true }
|
4713
4818
|
end
|
4714
4819
|
|
4715
4820
|
def resolve_background_position value, default_value = {}
|
@@ -4744,128 +4849,50 @@ module Asciidoctor
|
|
4744
4849
|
end
|
4745
4850
|
end
|
4746
4851
|
|
4747
|
-
def
|
4748
|
-
|
4749
|
-
page_height * (1 - (val.to_f / 100))
|
4750
|
-
elsif val.end_with? '%'
|
4751
|
-
@y - effective_page_height * (val.to_f / 100)
|
4752
|
-
else
|
4753
|
-
@y - (str_to_pt val)
|
4754
|
-
end
|
4852
|
+
def resolve_font_kerning keyword
|
4853
|
+
FontKerningTable[keyword]
|
4755
4854
|
end
|
4756
4855
|
|
4757
|
-
def
|
4758
|
-
|
4759
|
-
|
4760
|
-
|
4761
|
-
|
4762
|
-
|
4763
|
-
doc.set_attr 'attribute-missing', 'skip' unless (attribute_missing = doc.attr 'attribute-missing') == 'skip'
|
4764
|
-
value = value.gsub '\{', '\\\\\\{' if (escaped_attr_ref = value.include? '\{')
|
4765
|
-
value = (subs = opts[:subs]) ? (doc.apply_subs value, subs) : (doc.apply_subs value)
|
4766
|
-
value = (value.split LF).delete_if {|line| SimpleAttributeRefRx.match? line }.join LF if opts[:drop_lines_with_unresolved_attributes] && (value.include? '{')
|
4767
|
-
value = value.gsub '\{', '{' if escaped_attr_ref
|
4768
|
-
doc.set_attr 'attribute-missing', attribute_missing unless attribute_missing == 'skip'
|
4769
|
-
if imagesdir
|
4770
|
-
if imagesdir_to_restore
|
4771
|
-
doc.set_attr 'imagesdir', imagesdir_to_restore
|
4856
|
+
def resolve_pagenums val
|
4857
|
+
pgnums = []
|
4858
|
+
((val.include? ',') ? (val.split ',') : (val.split ';')).each do |entry|
|
4859
|
+
if entry.include? '..'
|
4860
|
+
from, _, to = entry.partition '..'
|
4861
|
+
pgnums += ([from.to_i, 1].max..[to.to_i, 1].max).to_a
|
4772
4862
|
else
|
4773
|
-
|
4863
|
+
pgnums << entry.to_i
|
4774
4864
|
end
|
4775
4865
|
end
|
4776
|
-
|
4866
|
+
|
4867
|
+
pgnums
|
4777
4868
|
end
|
4778
4869
|
|
4779
|
-
def
|
4780
|
-
|
4781
|
-
|
4782
|
-
|
4783
|
-
|
4784
|
-
siblings = parent.items
|
4870
|
+
def resolve_top val
|
4871
|
+
if val.end_with? 'vh'
|
4872
|
+
page_height * (1 - (val.to_f / 100))
|
4873
|
+
elsif val.end_with? '%'
|
4874
|
+
@y - effective_page_height * (val.to_f / 100)
|
4785
4875
|
else
|
4786
|
-
|
4787
|
-
end
|
4788
|
-
siblings = siblings.flatten if parent_context == :dlist
|
4789
|
-
if block != siblings[-1]
|
4790
|
-
(self_idx = siblings.index block) && siblings[self_idx + 1]
|
4791
|
-
elsif parent_context == :list_item || (parent_context == :open && parent.style != 'abstract') || parent_context == :section
|
4792
|
-
next_enclosed_block parent
|
4793
|
-
elsif list_item && (grandparent = parent.parent).context == :list_item
|
4794
|
-
next_enclosed_block grandparent
|
4876
|
+
@y - (str_to_pt val)
|
4795
4877
|
end
|
4796
4878
|
end
|
4797
4879
|
|
4798
|
-
|
4799
|
-
|
4800
|
-
|
4801
|
-
|
4802
|
-
|
4803
|
-
alias layout_part_title ink_part_title
|
4804
|
-
alias layout_general_heading ink_general_heading
|
4805
|
-
alias layout_heading ink_heading
|
4806
|
-
alias layout_prose ink_prose
|
4807
|
-
alias layout_caption ink_caption
|
4808
|
-
alias layout_table_caption ink_table_caption
|
4809
|
-
alias layout_toc ink_toc
|
4810
|
-
alias layout_toc_level ink_toc_level
|
4811
|
-
alias layout_running_content ink_running_content
|
4812
|
-
|
4813
|
-
# intercepts "class CustomPDFConverter < (Asciidoctor::Converter.for 'pdf')"
|
4814
|
-
def self.method_added method_sym
|
4815
|
-
if (method_name = method_sym.to_s).start_with? 'layout_'
|
4816
|
-
alias_method %(ink_#{method_name.slice 7, method_name.length}).to_sym, method_sym
|
4817
|
-
elsif method_name == 'convert_listing_or_literal' || method_name == 'convert_code'
|
4818
|
-
alias_method :convert_listing, method_sym
|
4819
|
-
alias_method :convert_literal, method_sym
|
4880
|
+
def resolve_text_transform key, use_fallback = true
|
4881
|
+
if (transform = ::Hash === key ? (key.delete :text_transform) : @theme[key])
|
4882
|
+
transform == 'none' ? nil : transform
|
4883
|
+
elsif use_fallback
|
4884
|
+
@text_transform
|
4820
4885
|
end
|
4821
4886
|
end
|
4822
4887
|
|
4823
|
-
#
|
4824
|
-
|
4825
|
-
|
4826
|
-
|
4827
|
-
|
4828
|
-
end
|
4829
|
-
|
4830
|
-
private
|
4831
|
-
|
4832
|
-
def add_link_to_image uri, image_info, image_opts
|
4833
|
-
image_width = image_info[:width]
|
4834
|
-
image_height = image_info[:height]
|
4835
|
-
|
4836
|
-
case image_opts[:position]
|
4837
|
-
when :center
|
4838
|
-
image_x = bounds.left_side + (bounds.width - image_width) * 0.5
|
4839
|
-
when :right
|
4840
|
-
image_x = bounds.right_side - image_width
|
4841
|
-
else # :left, nil
|
4842
|
-
image_x = bounds.left_side
|
4843
|
-
end
|
4844
|
-
|
4845
|
-
case image_opts[:vposition]
|
4846
|
-
when :top
|
4847
|
-
image_y = bounds.absolute_top
|
4848
|
-
when :center
|
4849
|
-
image_y = bounds.absolute_top - (bounds.height - image_height) * 0.5
|
4850
|
-
when :bottom
|
4851
|
-
image_y = bounds.absolute_bottom + image_height
|
4888
|
+
# QUESTION: should we pass a category as an argument?
|
4889
|
+
# QUESTION: should we make this a method on the theme ostruct? (e.g., @theme.resolve_color key, fallback)
|
4890
|
+
def resolve_theme_color key, fallback_color = nil, transparent_color = fallback_color
|
4891
|
+
if (color = @theme[key])
|
4892
|
+
color == 'transparent' ? transparent_color : color
|
4852
4893
|
else
|
4853
|
-
|
4854
|
-
end unless (image_y = image_opts[:y])
|
4855
|
-
|
4856
|
-
link_annotation [image_x, (image_y - image_height), (image_x + image_width), image_y], Border: [0, 0, 0], A: { Type: :Action, S: :URI, URI: uri.as_pdf }
|
4857
|
-
end
|
4858
|
-
|
4859
|
-
def load_open_uri
|
4860
|
-
if @cache_uri && !(defined? ::OpenURI::Cache) && (Helpers.require_library 'open-uri/cached', 'open-uri-cached', :warn).nil?
|
4861
|
-
# disable URI caching if library fails to load
|
4862
|
-
@cache_uri = false
|
4894
|
+
fallback_color
|
4863
4895
|
end
|
4864
|
-
::OpenURI
|
4865
|
-
end
|
4866
|
-
|
4867
|
-
def remove_tmp_files
|
4868
|
-
@tmp_files.reject! {|_, path| path ? (unlink_tmp_file path) : true }
|
4869
4896
|
end
|
4870
4897
|
|
4871
4898
|
def unlink_tmp_file path
|
@@ -4876,6 +4903,23 @@ module Asciidoctor
|
|
4876
4903
|
false
|
4877
4904
|
end
|
4878
4905
|
|
4906
|
+
# Calculate the width that is needed to print all the
|
4907
|
+
# fragments without wrapping any lines.
|
4908
|
+
#
|
4909
|
+
# This method assumes endlines are represented as discrete entries in the
|
4910
|
+
# fragments array.
|
4911
|
+
def width_of_fragments fragments
|
4912
|
+
line_widths = [0]
|
4913
|
+
fragments.each do |fragment|
|
4914
|
+
if fragment.text == LF
|
4915
|
+
line_widths << 0
|
4916
|
+
else
|
4917
|
+
line_widths[-1] += fragment.width
|
4918
|
+
end
|
4919
|
+
end
|
4920
|
+
line_widths.max
|
4921
|
+
end
|
4922
|
+
|
4879
4923
|
# Promotes author to primary author attributes around block; restores original attributes after block executes
|
4880
4924
|
def with_author doc, author, primary
|
4881
4925
|
doc.remove_attr 'url' if (original_url = doc.attr 'url')
|
@@ -4908,53 +4952,7 @@ module Asciidoctor
|
|
4908
4952
|
result
|
4909
4953
|
end
|
4910
4954
|
|
4911
|
-
|
4912
|
-
def breakable_uri uri
|
4913
|
-
scheme, address = uri.split UriSchemeBoundaryRx, 2
|
4914
|
-
address, scheme = scheme, address unless address
|
4915
|
-
unless address.nil_or_empty?
|
4916
|
-
address = address.gsub UriBreakCharsRx, UriBreakCharRepl
|
4917
|
-
# NOTE: require at least two characters after a break
|
4918
|
-
address.slice!(-2) if address[-2] == ZeroWidthSpace
|
4919
|
-
end
|
4920
|
-
%(#{scheme}#{address})
|
4921
|
-
end
|
4922
|
-
|
4923
|
-
def consolidate_ranges nums
|
4924
|
-
if nums.size > 1
|
4925
|
-
prev = nil
|
4926
|
-
nums.each_with_object [] do |num, accum|
|
4927
|
-
if prev && (prev.to_i + 1) == num.to_i
|
4928
|
-
accum[-1][1] = num
|
4929
|
-
else
|
4930
|
-
accum << [num]
|
4931
|
-
end
|
4932
|
-
prev = num
|
4933
|
-
end.map {|range| range.join '-' }
|
4934
|
-
else
|
4935
|
-
nums
|
4936
|
-
end
|
4937
|
-
end
|
4938
|
-
|
4939
|
-
def resolve_pagenums val
|
4940
|
-
pgnums = []
|
4941
|
-
((val.include? ',') ? (val.split ',') : (val.split ';')).each do |entry|
|
4942
|
-
if entry.include? '..'
|
4943
|
-
from, _, to = entry.partition '..'
|
4944
|
-
pgnums += ([from.to_i, 1].max..[to.to_i, 1].max).to_a
|
4945
|
-
else
|
4946
|
-
pgnums << entry.to_i
|
4947
|
-
end
|
4948
|
-
end
|
4949
|
-
|
4950
|
-
pgnums
|
4951
|
-
end
|
4952
|
-
|
4953
|
-
def get_char code
|
4954
|
-
(code.start_with? '\u') ? ([((code.slice 2, code.length).to_i 16)].pack 'U1') : code
|
4955
|
-
end
|
4956
|
-
|
4957
|
-
def create_prototype
|
4955
|
+
def create_scratch_prototype
|
4958
4956
|
@label = :scratch
|
4959
4957
|
@save_state = nil
|
4960
4958
|
@scratch_depth = 0
|
@@ -4971,7 +4969,7 @@ module Asciidoctor
|
|
4971
4969
|
end
|
4972
4970
|
|
4973
4971
|
def init_scratch originator
|
4974
|
-
@
|
4972
|
+
@scratch_prototype = originator.instance_variable_get :@scratch_prototype
|
4975
4973
|
@tmp_files = originator.instance_variable_get :@tmp_files
|
4976
4974
|
text_formatter.scratch = true
|
4977
4975
|
self
|
@@ -4996,7 +4994,7 @@ module Asciidoctor
|
|
4996
4994
|
|
4997
4995
|
def clear_scratch
|
4998
4996
|
@scratch_depth = 0
|
4999
|
-
@save_state = @
|
4997
|
+
@save_state = @scratch_prototype = @scratch = nil
|
5000
4998
|
end
|
5001
4999
|
end
|
5002
5000
|
end
|