asciidoctor-pdf 1.5.0.beta.7 → 1.5.0.beta.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,6 +14,7 @@ require_relative 'ext/pdf-core'
14
14
  require_relative 'temporary_path'
15
15
  require_relative 'measurements'
16
16
  require_relative 'sanitizer'
17
+ require_relative 'text_transformer'
17
18
  require_relative 'ext/prawn'
18
19
  require_relative 'formatted_text'
19
20
  require_relative 'pdfmark'
@@ -53,12 +54,14 @@ class Converter < ::Prawn::Document
53
54
  }
54
55
  TextAlignmentNames = ['justify', 'left', 'center', 'right']
55
56
  TextAlignmentRoles = ['text-justify', 'text-left', 'text-center', 'text-right']
57
+ TextDecorationStyleTable = { 'underline' => :underline, 'line-through' => :strikethrough }
56
58
  BlockAlignmentNames = ['left', 'center', 'right']
57
59
  AlignmentTable = { '<' => :left, '=' => :center, '>' => :right }
58
60
  ColumnPositions = [:left, :center, :right]
59
61
  PageLayouts = [:portrait, :landscape]
60
62
  PageSides = [:recto, :verso]
61
63
  (PDFVersions = { '1.3' => 1.3, '1.4' => 1.4, '1.5' => 1.5, '1.6' => 1.6, '1.7' => 1.7 }).default = 1.4
64
+ AuthorAttributeNames = ['author', 'authorinitials', 'firstname', 'middlename', 'lastname', 'email']
62
65
  LF = ?\n
63
66
  DoubleLF = LF * 2
64
67
  TAB = ?\t
@@ -106,6 +109,7 @@ class Converter < ::Prawn::Document
106
109
  CjkLineBreakRx = /(?=[\u3000\u30a0-\u30ff\u3040-\u309f\p{Han}\uff00-\uffef])/
107
110
  WhitespaceChars = ' ' + TAB + LF
108
111
  ValueSeparatorRx = /;|,/
112
+ HexColorRx = /^#[a-fA-F0-9]{6}$/
109
113
  SourceHighlighters = ['coderay', 'pygments', 'rouge'].to_set
110
114
  PygmentsBgColorRx = /^\.highlight +{ *background: *#([^;]+);/
111
115
  ViewportWidth = ::Module.new
@@ -125,16 +129,15 @@ class Converter < ::Prawn::Document
125
129
  doc.attributes['data-uri'] = ((doc.instance_variable_get :@attribute_overrides) || {})['data-uri'] = ''
126
130
  end
127
131
  @capabilities = {
128
- honors_literal_cell_style: AsciidoctorVersion >= (::Gem::Version.create '1.5.6'),
129
132
  special_sectnums: AsciidoctorVersion >= (::Gem::Version.create '1.5.7'),
130
133
  syntax_highlighter: AsciidoctorVersion >= (::Gem::Version.create '2.0.0'),
131
134
  }
135
+ @initial_instance_variables = [:@initial_instance_variables] + instance_variables
132
136
  end
133
137
 
134
138
  def convert node, name = nil, opts = {}
135
139
  method_name = %(convert_#{name ||= node.node_name})
136
140
  if respond_to? method_name
137
- # NOTE we prepend the prefix "convert_" to avoid conflict with Prawn methods
138
141
  result = send method_name, node
139
142
  else
140
143
  # TODO delegate to convert_method_missing
@@ -154,7 +157,7 @@ class Converter < ::Prawn::Document
154
157
  node.content
155
158
  elsif node.content_model != :compound && (string = node.content)
156
159
  # TODO this content could be cached on repeat invocations!
157
- layout_prose string, opts
160
+ layout_prose string, (opts.merge hyphenate: true)
158
161
  end
159
162
  node.document.instance_variable_set :@converter, prev_converter if prev_converter
160
163
  end
@@ -259,7 +262,10 @@ class Converter < ::Prawn::Document
259
262
 
260
263
  # NOTE delete orphaned page (a page was created but there was no additional content)
261
264
  # QUESTION should we delete page if document is empty? (leaving no pages?)
262
- delete_page if page.empty? && page_count > 1
265
+ if page_count > 1
266
+ go_to_page page_count unless last_page?
267
+ delete_page if page.empty?
268
+ end
263
269
 
264
270
  toc_page_nums = @toc_extent ? (layout_toc doc, toc_num_levels, @toc_extent[:page_nums].first, @toc_extent[:start_y], num_front_matter_pages[1]) : []
265
271
  end
@@ -273,6 +279,7 @@ class Converter < ::Prawn::Document
273
279
  end
274
280
  end
275
281
 
282
+ catalog.data[:PageMode] = :FullScreen if (doc.attr 'pdf-page-mode', @theme.page_mode) == 'fullscreen'
276
283
  add_outline doc, (doc.attr 'outlinelevels', toc_num_levels), toc_page_nums, num_front_matter_pages[1], has_front_cover
277
284
  if state.pages.size > 0 && (initial_zoom = @theme.page_initial_zoom)
278
285
  case initial_zoom.to_sym
@@ -296,8 +303,8 @@ class Converter < ::Prawn::Document
296
303
  # it the same as a full document.
297
304
  alias convert_embedded convert_document
298
305
 
299
- # TODO only allow method to be called once (or we need a reset)
300
306
  def init_pdf doc
307
+ (instance_variables - @initial_instance_variables).each {|ivar| remove_instance_variable ivar } if state
301
308
  pdf_opts = build_pdf_options doc, (theme = load_theme doc)
302
309
  # QUESTION should page options be preserved? (otherwise, not readily available)
303
310
  #@page_opts = { size: pdf_opts[:page_size], layout: pdf_opts[:page_layout] }
@@ -323,6 +330,7 @@ class Converter < ::Prawn::Document
323
330
  default_kerning theme.base_font_kerning != 'none'
324
331
  @fallback_fonts = [*theme.font_fallbacks]
325
332
  @allow_uri_read = doc.attr? 'allow-uri-read'
333
+ @cache_uri = doc.attr? 'cache-uri'
326
334
  if (bg_image = resolve_background_image doc, theme, 'page-background-image') && bg_image[0]
327
335
  @page_bg_image = { verso: bg_image, recto: bg_image }
328
336
  else
@@ -339,6 +347,14 @@ class Converter < ::Prawn::Document
339
347
  @font_color = theme.base_font_color || '000000'
340
348
  @base_align = (align = doc.attr 'text-align') && (TextAlignmentNames.include? align) ? align : theme.base_align
341
349
  @cjk_line_breaks = doc.attr? 'scripts', 'cjk'
350
+ if (hyphen_lang = doc.attr 'hyphens')
351
+ hyphen_lang = doc.attr 'lang' if hyphen_lang.empty?
352
+ hyphen_lang = 'en_us' if hyphen_lang.nil_or_empty? || hyphen_lang == 'en'
353
+ hyphen_lang = (hyphen_lang.tr '-', '_').downcase
354
+ if (defined? ::Text::Hyphen) || !(Helpers.require_library 'text/hyphen', 'text-hyphen', :warn).nil?
355
+ @hyphenator = ::Text::Hyphen.new language: hyphen_lang
356
+ end
357
+ end
342
358
  @text_transform = nil
343
359
  @list_numerals = []
344
360
  @list_bullets = []
@@ -470,13 +486,16 @@ class Converter < ::Prawn::Document
470
486
  info[:Subject] = (doc.attr 'subject').as_pdf if doc.attr? 'subject'
471
487
  info[:Keywords] = (doc.attr 'keywords').as_pdf if doc.attr? 'keywords'
472
488
  info[:Producer] = (doc.attr 'publisher').as_pdf if doc.attr? 'publisher'
473
- info[:Creator] = %(Asciidoctor PDF #{::Asciidoctor::PDF::VERSION}, based on Prawn #{::Prawn::VERSION}).as_pdf
474
- info[:Producer] ||= (info[:Author] || info[:Creator])
475
- unless doc.attr? 'reproducible'
489
+ if doc.attr? 'reproducible'
490
+ info[:Creator] = 'Asciidoctor PDF, based on Prawn'.as_pdf
491
+ info[:Producer] ||= (info[:Author] || info[:Creator])
492
+ else
493
+ info[:Creator] = %(Asciidoctor PDF #{::Asciidoctor::PDF::VERSION}, based on Prawn #{::Prawn::VERSION}).as_pdf
494
+ info[:Producer] ||= (info[:Author] || info[:Creator])
476
495
  # NOTE since we don't track the creation date of the input file, we map the ModDate header to the last modified
477
496
  # date of the input document and the CreationDate header to the date the PDF was produced by the converter.
478
- info[:ModDate] = ::Time.parse(doc.attr 'docdatetime') rescue (now ||= ::Time.now)
479
- info[:CreationDate] = ::Time.parse(doc.attr 'localdatetime') rescue (now ||= ::Time.now)
497
+ info[:ModDate] = (::Time.parse doc.attr 'docdatetime') rescue (now ||= ::Time.now)
498
+ info[:CreationDate] = (::Time.parse doc.attr 'localdatetime') rescue (now ||= ::Time.now)
480
499
  end
481
500
  info
482
501
  end
@@ -580,7 +599,7 @@ class Converter < ::Prawn::Document
580
599
  (title = doc.attr 'footnotes-title') && (layout_caption title, category: :footnotes)
581
600
  item_spacing = @theme.footnotes_item_spacing || 0
582
601
  fns.each do |fn|
583
- layout_prose %(<a name="_footnotedef_#{index = fn.index}">#{DummyText}</a>[<a anchor="_footnoteref_#{index}">#{index}</a>] #{fn.text}), margin_bottom: item_spacing
602
+ layout_prose %(<a name="_footnotedef_#{index = fn.index}">#{DummyText}</a>[<a anchor="_footnoteref_#{index}">#{index}</a>] #{fn.text}), margin_bottom: item_spacing, hyphenate: true
584
603
  end
585
604
  @footnotes += fns
586
605
  end
@@ -603,8 +622,8 @@ class Converter < ::Prawn::Document
603
622
  layout_prose node.title, align: (@theme.abstract_title_align || @base_align).to_sym, margin_top: (@theme.heading_margin_top || 0), margin_bottom: (@theme.heading_margin_bottom || 0), line_height: @theme.heading_line_height
604
623
  end if node.title?
605
624
  theme_font :abstract do
606
- prose_opts = { line_height: @theme.abstract_line_height, align: (initial_alignment = (@theme.abstract_align || @base_align).to_sym) }
607
- if (text_indent = @theme.prose_text_indent)
625
+ prose_opts = { line_height: @theme.abstract_line_height, align: (@theme.abstract_align || @base_align).to_sym, hyphenate: true }
626
+ if (text_indent = @theme.prose_text_indent || 0) > 0
608
627
  prose_opts[:indent_paragraphs] = text_indent
609
628
  end
610
629
  # FIXME control more first_line_options using theme
@@ -617,20 +636,16 @@ class Converter < ::Prawn::Document
617
636
  # FIXME is playback necessary here?
618
637
  child.document.playback_attributes child.attributes
619
638
  if child.context == :paragraph
620
- if (alignment = resolve_alignment_from_role child.roles)
621
- prose_opts[:align] = alignment
622
- end
623
- layout_prose child.content, prose_opts
639
+ layout_prose child.content, ((align = resolve_alignment_from_role child.roles) ? (prose_opts.merge align: align) : prose_opts.dup)
624
640
  prose_opts.delete :first_line_options
625
- prose_opts[:align] = initial_alignment
626
641
  else
627
642
  # FIXME this could do strange things if the wrong kind of content shows up
628
643
  convert_content_for_block child
629
644
  end
630
645
  end
631
646
  elsif node.content_model != :compound && (string = node.content)
632
- if (alignment = resolve_alignment_from_role node.roles)
633
- prose_opts[:align] = alignment
647
+ if (align = resolve_alignment_from_role node.roles)
648
+ prose_opts[:align] = align
634
649
  end
635
650
  layout_prose string, prose_opts
636
651
  end
@@ -651,13 +666,13 @@ class Converter < ::Prawn::Document
651
666
 
652
667
  def convert_paragraph node
653
668
  add_dest_for_block node if node.id
654
- prose_opts = { margin_bottom: 0 }
669
+ prose_opts = { margin_bottom: 0, hyphenate: true }
655
670
  lead = (roles = node.roles).include? 'lead'
656
671
  if (align = resolve_alignment_from_role roles)
657
672
  prose_opts[:align] = align
658
673
  end
659
674
 
660
- if (text_indent = @theme.prose_text_indent)
675
+ if (text_indent = @theme.prose_text_indent || 0) > 0
661
676
  prose_opts[:indent_paragraphs] = text_indent
662
677
  end
663
678
 
@@ -675,11 +690,10 @@ class Converter < ::Prawn::Document
675
690
 
676
691
  if (margin_inner_val = @theme.prose_margin_inner) &&
677
692
  (next_block = (siblings = node.parent.blocks)[(siblings.index node) + 1]) && next_block.context == :paragraph
678
- margin_bottom_val = margin_inner_val
693
+ margin_bottom margin_inner_val
679
694
  else
680
- margin_bottom_val = @theme.prose_margin_bottom
695
+ margin_bottom @theme.prose_margin_bottom
681
696
  end
682
- margin_bottom margin_bottom_val
683
697
  end
684
698
 
685
699
  def convert_admonition node
@@ -752,7 +766,7 @@ class Converter < ::Prawn::Document
752
766
  float do
753
767
  bounding_box [0, cursor], width: label_width, height: box_height do
754
768
  if icons == 'font'
755
- # FIXME we're assume icon is a square
769
+ # FIXME we assume icon is square
756
770
  icon_size = fit_icon_to_bounds icon_size
757
771
  # NOTE Prawn's vertical center is not reliable, so calculate it manually
758
772
  if label_valign == :center
@@ -776,7 +790,8 @@ class Converter < ::Prawn::Document
776
790
  height: box_height,
777
791
  fallback_font_name: fallback_svg_font_name,
778
792
  enable_web_requests: allow_uri_read,
779
- enable_file_requests_with_root: (::File.dirname icon_path)
793
+ enable_file_requests_with_root: (::File.dirname icon_path),
794
+ cache_images: cache_uri
780
795
  if (icon_height = (svg_size = svg_obj.document.sizing).output_height) > box_height
781
796
  icon_width = (svg_obj.resize height: (icon_height = box_height)).output_width
782
797
  else
@@ -829,7 +844,7 @@ class Converter < ::Prawn::Document
829
844
  end
830
845
  pad_box [cpad[0], 0, cpad[2], label_width + lpad[1] + cpad[3]] do
831
846
  move_down shift_top
832
- layout_caption node.title if node.title?
847
+ layout_caption node.title, category: :admonition if node.title?
833
848
  theme_font :admonition do
834
849
  convert_content_for_block node
835
850
  end
@@ -941,7 +956,7 @@ class Converter < ::Prawn::Document
941
956
  convert_content_for_block node
942
957
  else # verse
943
958
  content = guard_indentation node.content
944
- layout_prose content, normalize: false, align: :left
959
+ layout_prose content, normalize: false, align: :left, hyphenate: true
945
960
  end
946
961
  end
947
962
  if node.attr? 'attribution', nil, false
@@ -1134,7 +1149,7 @@ class Converter < ::Prawn::Document
1134
1149
  if (term_font_styles = font_styles).empty?
1135
1150
  term_inline_format = true
1136
1151
  else
1137
- term_inline_format = [normalize: false, inherited: { styles: term_font_styles }]
1152
+ term_inline_format = [inherited: { styles: term_font_styles }]
1138
1153
  end
1139
1154
  term_line_metrics = calc_line_metrics @theme.description_list_term_line_height || @theme.base_line_height
1140
1155
  term_padding = [term_line_metrics.padding_top, 10, (@theme.prose_margin_bottom || 0) * 0.5 + term_line_metrics.padding_bottom, 10]
@@ -1408,14 +1423,14 @@ class Converter < ::Prawn::Document
1408
1423
  terms, desc = node
1409
1424
  [*terms].each {|term| layout_prose %(<em>#{term.text}</em>), (opts.merge margin_top: 0, margin_bottom: @theme.description_list_term_spacing) }
1410
1425
  if desc
1411
- layout_prose desc.text, opts if desc.text?
1426
+ layout_prose desc.text, (opts.merge hyphenate: true) if desc.text?
1412
1427
  convert_content_for_block desc
1413
1428
  end
1414
1429
  else
1415
1430
  if (primary_text = node.text).nil_or_empty?
1416
1431
  layout_prose DummyText, opts unless node.blocks?
1417
1432
  else
1418
- layout_prose primary_text, opts
1433
+ layout_prose primary_text, (opts.merge hyphenate: true)
1419
1434
  end
1420
1435
  convert_content_for_block node
1421
1436
  end
@@ -1495,7 +1510,8 @@ class Converter < ::Prawn::Document
1495
1510
  width: width,
1496
1511
  fallback_font_name: fallback_svg_font_name,
1497
1512
  enable_web_requests: allow_uri_read,
1498
- enable_file_requests_with_root: file_request_root
1513
+ enable_file_requests_with_root: file_request_root,
1514
+ cache_images: cache_uri
1499
1515
  rendered_w = (svg_size = svg_obj.document.sizing).output_width
1500
1516
  if !width && (svg_obj.document.root.attributes.key? 'width')
1501
1517
  # NOTE scale native width & height from px to pt and restrict width to available width
@@ -1565,7 +1581,7 @@ class Converter < ::Prawn::Document
1565
1581
  layout_caption node, category: :image, side: :bottom if node.title?
1566
1582
  theme_margin :block, :bottom unless pinned
1567
1583
  rescue
1568
- on_image_error :exception, node, target, (opts.merge message: %(could not embed image: #{image_path}; #{$!.message}))
1584
+ on_image_error :exception, node, target, (opts.merge message: %(could not embed image: #{image_path}; #{$!.message}#{::Prawn::Errors::UnsupportedImageType === $! ? '; install prawn-gmagick gem to add support' : ''}))
1569
1585
  end
1570
1586
  ensure
1571
1587
  unlink_tmp_file image_path if image_path
@@ -1631,7 +1647,7 @@ class Converter < ::Prawn::Document
1631
1647
  when 'vimeo'
1632
1648
  video_path = %(https://vimeo.com/#{video_id = node.attr 'target'})
1633
1649
  if allow_uri_read
1634
- if node.document.attr? 'cache-uri'
1650
+ if cache_uri
1635
1651
  Helpers.require_library 'open-uri/cached', 'open-uri-cached' unless defined? ::OpenURI::Cache
1636
1652
  else
1637
1653
  ::OpenURI
@@ -1950,8 +1966,10 @@ class Converter < ::Prawn::Document
1950
1966
  head_transform = resolve_text_transform :table_head_text_transform, nil
1951
1967
  row_data = []
1952
1968
  row.each do |cell|
1969
+ cell_text = head_transform ? (transform_text cell.text.strip, head_transform) : cell.text.strip
1970
+ cell_text = hyphenate_text cell_text, @hyphenator if defined? @hyphenator
1953
1971
  row_data << {
1954
- content: (head_transform ? (transform_text cell.text.strip, head_transform) : cell.text.strip),
1972
+ content: cell_text,
1955
1973
  inline_format: [normalize: true],
1956
1974
  background_color: head_bg_color,
1957
1975
  text_color: (theme.table_head_font_color || theme.table_font_color || @font_color),
@@ -2023,7 +2041,8 @@ class Converter < ::Prawn::Document
2023
2041
  end
2024
2042
  cell_line_metrics = calc_line_metrics theme.base_line_height
2025
2043
  when :literal
2026
- cell_data[:content] = @capabilities[:honors_literal_cell_style] ? (guard_indentation cell.text) : (guard_indentation cell.instance_variable_get :@text)
2044
+ # NOTE we want the raw AsciiDoc in this case
2045
+ cell_data[:content] = guard_indentation cell.instance_variable_get :@text
2027
2046
  # NOTE the absence of the inline_format option implies it's disabled
2028
2047
  # QUESTION should we use literal_font_*, code_font_*, or introduce another category?
2029
2048
  cell_data[:font] = theme.code_font_family
@@ -2061,6 +2080,8 @@ class Converter < ::Prawn::Document
2061
2080
  unless cell_data.key? :content
2062
2081
  cell_text = cell.text.strip
2063
2082
  cell_text = transform_text cell_text if cell_transform
2083
+ cell_text = hyphenate_text cell_text, @hyphenator if defined? @hyphenator
2084
+ cell_text = cell_text.gsub CjkLineBreakRx, ZeroWidthSpace if @cjk_line_breaks
2064
2085
  if cell_text.include? LF
2065
2086
  # NOTE effectively the same as calling cell.content (should we use that instead?)
2066
2087
  # FIXME hard breaks not quite the same result as separate paragraphs; need custom cell impl here
@@ -2072,8 +2093,11 @@ class Converter < ::Prawn::Document
2072
2093
  end
2073
2094
  end
2074
2095
  if node.document.attr? 'cellbgcolor'
2075
- cell_bg_color = node.document.attr 'cellbgcolor'
2076
- cell_data[:background_color] = cell_bg_color == 'transparent' ? body_bg_color : (cell_bg_color.slice 1, cell_bg_color.length)
2096
+ if (cell_bg_color = node.document.attr 'cellbgcolor') == 'transparent'
2097
+ cell_data[:background_color] = body_bg_color
2098
+ elsif (cell_bg_color.start_with? '#') && (HexColorRx.match? cell_bg_color)
2099
+ cell_data[:background_color] = cell_bg_color.slice 1, cell_bg_color.length
2100
+ end
2077
2101
  end
2078
2102
  row_data << cell_data
2079
2103
  end
@@ -2134,7 +2158,7 @@ class Converter < ::Prawn::Document
2134
2158
  table_width = bounds.width * ((node.attr 'tablepcwidth') / 100.0)
2135
2159
  column_widths = node.columns.map {|col| ((col.attr 'colpcwidth') * table_width) / 100.0 }
2136
2160
  # NOTE until Asciidoctor 1.5.4, colpcwidth values didn't always add up to 100%; use last column to compensate
2137
- unless column_widths.empty? || (width_delta = table_width - column_widths.reduce(:+)) == 0
2161
+ unless column_widths.empty? || (width_delta = table_width - column_widths.sum) == 0
2138
2162
  column_widths[-1] += width_delta
2139
2163
  end
2140
2164
  end
@@ -2276,6 +2300,7 @@ class Converter < ::Prawn::Document
2276
2300
  start_new_page unless at_page_top?
2277
2301
  start_new_page if @ppbook && verso_page? && !(node.option? 'nonfacing')
2278
2302
  end
2303
+ add_dest_for_block node, (derive_anchor_from_id node.id, 'toc')
2279
2304
  allocate_toc doc, (doc.attr 'toclevels', 2).to_i, @y, (is_book || (doc.attr? 'title-page'))
2280
2305
  end
2281
2306
  nil
@@ -2340,8 +2365,7 @@ class Converter < ::Prawn::Document
2340
2365
  end
2341
2366
  text = %(#{text}, #{pagenums.join ', '})
2342
2367
  end
2343
- layout_prose text, align: :left, margin: 0, normalize_line_height: true
2344
-
2368
+ layout_prose text, align: :left, margin: 0, normalize_line_height: true, hanging_indent: @theme.description_list_description_indent * 2
2345
2369
  term.subterms.each do |subterm|
2346
2370
  indent @theme.description_list_description_indent do
2347
2371
  convert_index_list_item subterm
@@ -2442,11 +2466,14 @@ class Converter < ::Prawn::Document
2442
2466
  if node.document.attr? 'icons', 'font'
2443
2467
  if (icon_name = node.target).include? '@'
2444
2468
  icon_name, icon_set = icon_name.split '@', 2
2469
+ explicit_icon_set = true
2470
+ elsif (icon_set = node.attr 'set', nil, false)
2471
+ explicit_icon_set = true
2445
2472
  else
2446
- icon_set = node.attr 'set', (node.document.attr 'icon-set', 'fa'), false
2473
+ icon_set = node.document.attr 'icon-set', 'fa'
2447
2474
  end
2448
- icon_set = 'fa' unless IconSets.include? icon_set
2449
- if icon_set == 'fa'
2475
+ if icon_set == 'fa' || !(IconSets.include? icon_set)
2476
+ icon_set = 'fa'
2450
2477
  # legacy name from Font Awesome < 5
2451
2478
  if (remapped_icon_name = resolve_legacy_icon_name icon_name)
2452
2479
  requested_icon_name = icon_name
@@ -2465,6 +2492,10 @@ class Converter < ::Prawn::Document
2465
2492
  else
2466
2493
  glyph = (icon_font_data icon_set).unicode icon_name rescue nil
2467
2494
  end
2495
+ unless glyph || explicit_icon_set || !icon_name.start_with?(*IconSetPrefixes)
2496
+ icon_set, icon_name = icon_name.split '-', 2
2497
+ glyph = (icon_font_data icon_set).unicode icon_name rescue nil
2498
+ end
2468
2499
  if glyph
2469
2500
  if node.attr? 'size', nil, false
2470
2501
  case (size = node.attr 'size')
@@ -2637,34 +2668,22 @@ class Converter < ::Prawn::Document
2637
2668
  if (logo_align = [(logo_image_attrs.delete 'align'), @theme.title_page_logo_align, title_align.to_s].find {|val| (BlockAlignmentNames.include? val) })
2638
2669
  logo_image_attrs['align'] = logo_align
2639
2670
  end
2640
- # QUESTION should we allow theme to turn logo image off?
2641
- logo_image_top = logo_image_attrs['top'] || @theme.title_page_logo_top || '10%'
2642
- # FIXME delegate to method to convert page % to y value
2643
- if logo_image_top.end_with? 'vh'
2644
- logo_image_top = page_height - page_height * logo_image_top.to_f / 100.0
2645
- else
2646
- logo_image_top = bounds.absolute_top - effective_page_height * logo_image_top.to_f / 100.0
2671
+ if (logo_image_top = logo_image_attrs['top'] || @theme.title_page_logo_top)
2672
+ initial_y, @y = @y, (resolve_top logo_image_top)
2647
2673
  end
2648
- initial_y, @y = @y, logo_image_top
2649
2674
  # FIXME add API to Asciidoctor for creating blocks like this (extract from extensions module?)
2650
2675
  image_block = ::Asciidoctor::Block.new doc, :image, content_model: :empty, attributes: logo_image_attrs
2651
2676
  # NOTE pinned option keeps image on same page
2652
2677
  indent (@theme.title_page_logo_margin_left || 0), (@theme.title_page_logo_margin_right || 0) do
2653
2678
  convert_image image_block, relative_to_imagesdir: relative_to_imagesdir, pinned: true
2654
2679
  end
2655
- @y = initial_y
2680
+ @y = initial_y if initial_y
2656
2681
  end
2657
2682
 
2658
2683
  # TODO prevent content from spilling to next page
2659
2684
  theme_font :title_page do
2660
2685
  if (title_top = @theme.title_page_title_top)
2661
- if title_top.end_with? 'vh'
2662
- title_top = page_height - page_height * title_top.to_f / 100.0
2663
- else
2664
- title_top = bounds.absolute_top - effective_page_height * title_top.to_f / 100.0
2665
- end
2666
- # FIXME delegate to method to convert page % to y value
2667
- @y = title_top
2686
+ @y = resolve_top title_top
2668
2687
  end
2669
2688
  unless @theme.title_page_title_display == 'none'
2670
2689
  doctitle = doc.doctitle partition: true
@@ -2694,9 +2713,22 @@ class Converter < ::Prawn::Document
2694
2713
  if @theme.title_page_authors_display != 'none' && (doc.attr? 'authors')
2695
2714
  move_down(@theme.title_page_authors_margin_top || 0)
2696
2715
  indent (@theme.title_page_authors_margin_left || 0), (@theme.title_page_authors_margin_right || 0) do
2716
+ authors_content = @theme.title_page_authors_content
2717
+ authors_content = {
2718
+ name_only: @theme.title_page_authors_content_name_only || authors_content,
2719
+ with_email: @theme.title_page_authors_content_with_email || authors_content,
2720
+ with_url: @theme.title_page_authors_content_with_url || authors_content,
2721
+ }
2697
2722
  # TODO provide an API in core to get authors as an array
2698
2723
  authors = (1..(doc.attr 'authorcount', 1).to_i).map {|idx|
2699
- doc.attr(idx == 1 ? 'author' : %(author_#{idx}))
2724
+ promote_author doc, idx do
2725
+ author_content_key = (url = doc.attr 'url') ? ((url.start_with? 'mailto:') ? :with_email : :with_url) : :name_only
2726
+ if (author_content = authors_content[author_content_key])
2727
+ apply_subs_discretely doc, author_content
2728
+ else
2729
+ doc.attr 'author'
2730
+ end
2731
+ end
2700
2732
  }.join (@theme.title_page_authors_delimiter || ', ')
2701
2733
  theme_font :title_page_authors do
2702
2734
  layout_prose authors,
@@ -2806,9 +2838,15 @@ class Converter < ::Prawn::Document
2806
2838
  end
2807
2839
  outdent_section(opts.delete :outdent) do
2808
2840
  margin_top top_margin
2841
+ # QUESTION should we move inherited styles to typeset_text?
2842
+ if (inherited = apply_text_decoration ::Set.new, :heading, hlevel).empty?
2843
+ inline_format_opts = true
2844
+ else
2845
+ inline_format_opts = [{ inherited: inherited }]
2846
+ end
2809
2847
  typeset_text string, calc_line_metrics((opts.delete :line_height) || (hlevel ? @theme[%(heading_h#{hlevel}_line_height)] : nil) || @theme.heading_line_height || @theme.base_line_height), {
2810
2848
  color: @font_color,
2811
- inline_format: true,
2849
+ inline_format: inline_format_opts,
2812
2850
  align: @base_align.to_sym
2813
2851
  }.merge(opts)
2814
2852
  margin_bottom bot_margin
@@ -2822,6 +2860,7 @@ class Converter < ::Prawn::Document
2822
2860
  if (transform = resolve_text_transform opts)
2823
2861
  string = transform_text string, transform
2824
2862
  end
2863
+ string = hyphenate_text string, @hyphenator if (opts.delete :hyphenate) && (defined? @hyphenator)
2825
2864
  # NOTE used by extensions; ensures linked text gets formatted using the link styles
2826
2865
  if (anchor = opts.delete :anchor)
2827
2866
  string = %(<a anchor="#{anchor}">#{string}</a>)
@@ -2885,7 +2924,8 @@ class Converter < ::Prawn::Document
2885
2924
  margin_bottom: margin[:bottom],
2886
2925
  align: (@theme[%(#{category_caption}_align)] || @theme.caption_align || @base_align).to_sym,
2887
2926
  normalize: false,
2888
- normalize_line_height: true
2927
+ normalize_line_height: true,
2928
+ hyphenate: true,
2889
2929
  }.merge(opts)
2890
2930
  if side == :top && (bb_color = @theme[%(#{category_caption}_border_bottom_color)] || @theme.caption_border_bottom_color)
2891
2931
  stroke_horizontal_rule bb_color
@@ -2990,31 +3030,25 @@ class Converter < ::Prawn::Document
2990
3030
  sections.each do |sect|
2991
3031
  theme_font :toc, level: (sect.level + 1) do
2992
3032
  sect_title = ZeroWidthSpace + (@text_transform ? (transform_text sect.numbered_title, @text_transform) : sect.numbered_title)
3033
+ hanging_indent = @theme.toc_hanging_indent || 0
2993
3034
  # NOTE only write section title (excluding dots and page number) if this is a dry run
2994
3035
  if scratch?
2995
3036
  # FIXME use layout_prose
2996
3037
  # NOTE must wrap title in empty anchor element in case links are styled with different font family / size
2997
- typeset_text %(<a>#{sect_title}</a>), line_metrics, inline_format: true
3038
+ typeset_text %(<a>#{sect_title}</a>), line_metrics, inline_format: true, hanging_indent: hanging_indent
2998
3039
  else
2999
3040
  pgnum_label = ((sect.attr 'pdf-page-start') - num_front_matter_pages).to_s
3000
3041
  start_page_number = page_number
3001
3042
  start_cursor = cursor
3002
3043
  start_dots = nil
3003
- # NOTE use low-level text formatter to add anchor overlay without styling text as link & force color
3004
- sect_title_format_override = {
3005
- anchor: (sect_anchor = sect.attr 'pdf-anchor'),
3006
- color: @font_color,
3007
- styles: ((@theme[%(toc_h#{sect.level + 1}_text_decoration)] || @theme.toc_text_decoration) == 'underline' ?
3008
- (font_styles << :underline) : font_styles)
3009
- }
3010
- (sect_title_fragments = text_formatter.format sect_title).each do |fragment|
3011
- fragment.update(sect_title_format_override) {|k, oval, nval| k == :styles ? (oval.merge nval) : oval }
3012
- end
3044
+ sect_title_inherited = (apply_text_decoration ::Set.new, :toc, sect.level.next).merge anchor: (sect_anchor = sect.attr 'pdf-anchor'), color: @font_color
3045
+ # NOTE use text formatter to add anchor overlay to avoid using inline format with synthetic anchor tag
3046
+ sect_title_fragments = text_formatter.format sect_title, inherited: sect_title_inherited
3013
3047
  pgnum_label_width = rendered_width_of_string pgnum_label
3014
3048
  indent 0, pgnum_label_width do
3015
3049
  sect_title_fragments[-1][:callback] = (last_fragment_pos = ::Asciidoctor::PDF::FormattedText::FragmentPositionRenderer.new)
3016
- typeset_formatted_text sect_title_fragments, line_metrics
3017
- start_dots = last_fragment_pos.right
3050
+ typeset_formatted_text sect_title_fragments, line_metrics, hanging_indent: hanging_indent
3051
+ start_dots = last_fragment_pos.right + hanging_indent
3018
3052
  last_fragment_cursor = last_fragment_pos.top + line_metrics.padding_top
3019
3053
  # NOTE this will be incorrect if wrapped line is all monospace
3020
3054
  start_cursor = last_fragment_cursor if start_cursor - last_fragment_cursor > line_metrics.height
@@ -3456,21 +3490,24 @@ class Converter < ::Prawn::Document
3456
3490
  pagenum_labels[n] = { P: (::PDF::Core::LiteralString.new %(#{i + 1})) }
3457
3491
  end
3458
3492
 
3493
+ unless toc_page_nums.none? || (toc_title = doc.attr 'toc-title').nil_or_empty?
3494
+ toc_section = insert_toc_section doc, toc_title, toc_page_nums
3495
+ end
3496
+
3459
3497
  outline.define do
3460
3498
  initial_pagenum = has_front_cover ? 2 : 1
3461
3499
  # FIXME use sanitize: :plain_text once available
3462
3500
  if document.page_count >= initial_pagenum && (doctitle = doc.header? ? doc.doctitle : (doc.attr 'untitled-label'))
3463
3501
  page title: (document.sanitize doctitle), destination: (document.dest_top has_front_cover ? 2 : 1)
3464
3502
  end
3465
- unless toc_page_nums.none? || (toc_title = doc.attr 'toc-title').nil_or_empty?
3466
- page title: toc_title, destination: (document.dest_top toc_page_nums.first)
3467
- end
3468
3503
  # QUESTION is there any way to get add_outline_level to invoke in the context of the outline?
3469
3504
  document.add_outline_level self, doc.sections, num_levels, expand_levels
3470
3505
  end
3471
3506
 
3507
+ toc_section.parent.blocks.delete toc_section if toc_section
3508
+
3472
3509
  catalog.data[:PageLabels] = state.store.ref Nums: pagenum_labels.flatten
3473
- catalog.data[:PageMode] = :UseOutlines
3510
+ catalog.data[((doc.attr 'pdf-page-mode') || @theme.page_mode) == 'fullscreen' ? :NonFullScreenPageMode : :PageMode] = :UseOutlines
3474
3511
  nil
3475
3512
  end
3476
3513
 
@@ -3488,6 +3525,30 @@ class Converter < ::Prawn::Document
3488
3525
  end
3489
3526
  end
3490
3527
 
3528
+ def insert_toc_section doc, toc_title, toc_page_nums
3529
+ if (doc.attr? 'toc-placement', 'macro') && (toc_node = (doc.find_by context: :toc)[0])
3530
+ if (parent_section = toc_node.parent).context == :section
3531
+ grandparent_section = parent_section.parent
3532
+ toc_level = parent_section.level
3533
+ insert_idx = (grandparent_section.blocks.index parent_section) + 1
3534
+ else
3535
+ parent_section = grandparent_section = doc
3536
+ toc_level = doc.sections[0].level
3537
+ insert_idx = 0
3538
+ end
3539
+ toc_dest = toc_node.attr 'pdf-destination'
3540
+ else
3541
+ parent_section = grandparent_section = doc
3542
+ toc_level = doc.sections[0].level
3543
+ insert_idx = 0
3544
+ toc_dest = dest_top toc_page_nums.first
3545
+ end
3546
+ toc_section = Section.new grandparent_section, toc_level, false, { attributes: { 'pdf-destination' => toc_dest } }
3547
+ toc_section.title = toc_title
3548
+ grandparent_section.blocks.insert insert_idx, toc_section
3549
+ toc_section
3550
+ end
3551
+
3491
3552
  def write pdf_doc, target
3492
3553
  if target.respond_to? :write
3493
3554
  require_relative 'ext/core/quantifiable_stdout' unless defined? ::QuantifiableStdout
@@ -3537,6 +3598,19 @@ class Converter < ::Prawn::Document
3537
3598
  end
3538
3599
 
3539
3600
  attr_reader :allow_uri_read
3601
+ attr_reader :cache_uri
3602
+
3603
+ def apply_text_decoration styles, category, level = nil
3604
+ if (text_decoration_style = TextDecorationStyleTable[(level && @theme[%(#{category}_h#{level}_text_decoration)]) || @theme[%(#{category}_text_decoration)]])
3605
+ {
3606
+ styles: (styles << text_decoration_style),
3607
+ text_decoration_color: (level && @theme[%(#{category}_h#{level}_text_decoration_color)]) || @theme[%(#{category}_text_decoration_color)],
3608
+ text_decoration_width: (level && @theme[%(#{category}_h#{level}_text_decoration_width)]) || @theme[%(#{category}_text_decoration_width)],
3609
+ }.compact
3610
+ else
3611
+ styles.empty? ? {} : { styles: styles }
3612
+ end
3613
+ end
3540
3614
 
3541
3615
  def resolve_text_transform key, use_fallback = true
3542
3616
  if (transform = ::Hash === key ? (key.delete :text_transform) : @theme[key.to_s])
@@ -3708,7 +3782,7 @@ class Converter < ::Prawn::Document
3708
3782
  width_of_string str, opts
3709
3783
  else
3710
3784
  char_widths = chars.map {|char| rendered_width_of_char char, opts }
3711
- char_widths.reduce(&:+) + (char_widths.length * character_spacing)
3785
+ char_widths.sum + (char_widths.length * character_spacing)
3712
3786
  end
3713
3787
  end
3714
3788
 
@@ -3731,7 +3805,11 @@ class Converter < ::Prawn::Document
3731
3805
  move_down line_metrics.padding_top
3732
3806
  opts = { leading: line_metrics.leading, final_gap: line_metrics.final_gap }.merge opts
3733
3807
  string = string.gsub CjkLineBreakRx, ZeroWidthSpace if @cjk_line_breaks
3734
- if (first_line_opts = opts.delete :first_line_options)
3808
+ if (hanging_indent = (opts.delete :hanging_indent) || 0) > 0
3809
+ indent hanging_indent do
3810
+ text string, (opts.merge indent_paragraphs: -hanging_indent)
3811
+ end
3812
+ elsif (first_line_opts = opts.delete :first_line_options)
3735
3813
  # TODO good candidate for Prawn enhancement!
3736
3814
  text_with_formatted_first_line string, first_line_opts, opts
3737
3815
  else
@@ -3743,7 +3821,14 @@ class Converter < ::Prawn::Document
3743
3821
  # QUESTION combine with typeset_text?
3744
3822
  def typeset_formatted_text fragments, line_metrics, opts = {}
3745
3823
  move_down line_metrics.padding_top
3746
- formatted_text fragments, { leading: line_metrics.leading, final_gap: line_metrics.final_gap }.merge(opts)
3824
+ opts = { leading: line_metrics.leading, final_gap: line_metrics.final_gap }.merge opts
3825
+ if (hanging_indent = (opts.delete :hanging_indent) || 0) > 0
3826
+ indent hanging_indent do
3827
+ formatted_text fragments, (opts.merge indent_paragraphs: -hanging_indent)
3828
+ end
3829
+ else
3830
+ formatted_text fragments, opts
3831
+ end
3747
3832
  move_down line_metrics.padding_bottom
3748
3833
  end
3749
3834
 
@@ -3915,7 +4000,7 @@ class Converter < ::Prawn::Document
3915
4000
  logger.warn %(allow-uri-read is not enabled; cannot embed remote image: #{image_path}) unless scratch?
3916
4001
  return
3917
4002
  end
3918
- if doc.attr? 'cache-uri'
4003
+ if cache_uri
3919
4004
  Helpers.require_library 'open-uri/cached', 'open-uri-cached' unless defined? ::OpenURI::Cache
3920
4005
  else
3921
4006
  ::OpenURI
@@ -3926,6 +4011,7 @@ class Converter < ::Prawn::Document
3926
4011
  open(image_path, (binary ? 'rb' : 'r')) {|fd| tmp_image.write fd.read }
3927
4012
  tmp_image.path.extend TemporaryPath
3928
4013
  rescue
4014
+ logger.warn %(could not retrieve remote image: #{image_path}; #{$!.message}) unless scratch?
3929
4015
  nil
3930
4016
  ensure
3931
4017
  tmp_image.close
@@ -3976,6 +4062,7 @@ class Converter < ::Prawn::Document
3976
4062
  image_opts = {
3977
4063
  enable_file_requests_with_root: (::File.dirname image_path),
3978
4064
  enable_web_requests: allow_uri_read,
4065
+ cache_images: cache_uri,
3979
4066
  fallback_font_name: fallback_svg_font_name,
3980
4067
  format: 'svg',
3981
4068
  }
@@ -4122,6 +4209,16 @@ class Converter < ::Prawn::Document
4122
4209
  end
4123
4210
  end
4124
4211
 
4212
+ def resolve_top val
4213
+ if val.end_with? 'vh'
4214
+ page_height * (1 - (val.to_f / 100))
4215
+ elsif val.end_with? '%'
4216
+ @y - effective_page_height * (val.to_f / 100)
4217
+ else
4218
+ @y - (str_to_pt val)
4219
+ end
4220
+ end
4221
+
4125
4222
  def add_link_to_image uri, image_info, image_opts
4126
4223
  image_width = image_info[:width]
4127
4224
  image_height = image_info[:height]
@@ -4173,8 +4270,7 @@ class Converter < ::Prawn::Document
4173
4270
  if imagesdir
4174
4271
  doc.set_attr 'imagesdir', imagesdir
4175
4272
  else
4176
- # NOTE remove_attr not defined until Asciidoctor 1.5.6
4177
- doc.attributes.delete 'imagesdir'
4273
+ doc.remove_attr 'imagesdir'
4178
4274
  end
4179
4275
  value
4180
4276
  end
@@ -4186,6 +4282,44 @@ class Converter < ::Prawn::Document
4186
4282
  value
4187
4283
  end
4188
4284
 
4285
+ def promote_author doc, idx = 1
4286
+ doc.remove_attr 'url' if (original_url = doc.attr 'url')
4287
+ email = nil
4288
+ if idx > 1
4289
+ original_attrs = AuthorAttributeNames.reduce({}) do |accum, name|
4290
+ accum[name] = doc.attr name
4291
+ if (val = doc.attr %(#{name}_#{idx}))
4292
+ doc.set_attr name, val
4293
+ # NOTE email holds url as well
4294
+ email = val if name == 'email'
4295
+ else
4296
+ doc.remove_attr name
4297
+ end
4298
+ accum
4299
+ end
4300
+ doc.set_attr 'url', ((email.include? '@') ? %(mailto:#{email}) : email) if email
4301
+ result = yield
4302
+ original_attrs.each do |name, val|
4303
+ if val
4304
+ doc.set_attr name, val
4305
+ else
4306
+ doc.remove_attr name
4307
+ end
4308
+ end
4309
+ else
4310
+ if (email = doc.attr 'email')
4311
+ doc.set_attr 'url', ((email.include? '@') ? %(mailto:#{email}) : email)
4312
+ end
4313
+ result = yield
4314
+ end
4315
+ if original_url
4316
+ doc.set_attr 'url', original_url
4317
+ elsif email
4318
+ doc.remove_attr 'url'
4319
+ end
4320
+ result
4321
+ end
4322
+
4189
4323
  # NOTE assume URL is escaped (i.e., contains character references such as &amp;)
4190
4324
  def breakable_uri uri
4191
4325
  scheme, address = uri.split UriSchemeBoundaryRx, 2