asciidoctor-pdf 1.5.0.alpha.14 → 1.5.0.alpha.15

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,3 @@
1
- require 'asciidoctor'
1
+ require 'asciidoctor' unless defined? Asciidoctor::VERSION
2
2
  require_relative 'asciidoctor-pdf/version'
3
3
  require_relative 'asciidoctor-pdf/converter'
@@ -1,13 +1,14 @@
1
1
  # encoding: UTF-8
2
2
  # TODO cleanup imports...decide what belongs in asciidoctor-pdf.rb
3
3
  require 'prawn'
4
+ require_relative 'ttfunk_ext'
4
5
  begin
5
6
  require 'prawn/gmagick'
6
7
  rescue LoadError
7
8
  end unless defined? GMagick::Image
8
9
  require_relative 'prawn-svg_ext'
9
10
  require_relative 'prawn-table_ext'
10
- require 'prawn/templates'
11
+ require_relative 'prawn-templates_ext'
11
12
  require_relative 'core_ext'
12
13
  require_relative 'pdf-core_ext'
13
14
  require_relative 'temporary_path'
@@ -49,6 +50,7 @@ class Converter < ::Prawn::Document
49
50
  BlockAlignmentNames = ['left', 'center', 'right']
50
51
  AlignmentTable = { '<' => :left, '=' => :center, '>' => :right }
51
52
  ColumnPositions = [:left, :center, :right]
53
+ PageLayouts = [:portrait, :landscape]
52
54
  PageSides = [:recto, :verso]
53
55
  LF = %(\n)
54
56
  DoubleLF = %(\n\n)
@@ -73,7 +75,8 @@ class Converter < ::Prawn::Document
73
75
  Bullets = {
74
76
  disc: %(\u2022),
75
77
  circle: %(\u25e6),
76
- square: %(\u25aa)
78
+ square: %(\u25aa),
79
+ none: ''
77
80
  }
78
81
  # NOTE Default theme font uses ballot boxes from FontAwesome
79
82
  BallotBox = {
@@ -187,7 +190,7 @@ class Converter < ::Prawn::Document
187
190
  start_new_page unless page_is_empty?
188
191
 
189
192
  num_toc_levels = (doc.attr 'toclevels', 2).to_i
190
- if (include_toc = doc.attr? 'toc')
193
+ if (insert_toc = (doc.attr? 'toc') && doc.sections?)
191
194
  start_new_page if @ppbook && verso_page?
192
195
  toc_page_nums = page_number
193
196
  dry_run { toc_page_nums = layout_toc doc, num_toc_levels, toc_page_nums }
@@ -209,11 +212,15 @@ class Converter < ::Prawn::Document
209
212
  # QUESTION should we delete page if document is empty? (leaving no pages?)
210
213
  delete_page if page_is_empty? && page_count > 1
211
214
 
212
- toc_page_nums = include_toc ? (layout_toc doc, num_toc_levels, toc_page_nums.first, num_front_matter_pages) : []
215
+ toc_page_nums = insert_toc ? (layout_toc doc, num_toc_levels, toc_page_nums.first, num_front_matter_pages) : []
213
216
 
214
217
  if page_count > num_front_matter_pages
215
- layout_running_content :header, doc, skip: num_front_matter_pages unless doc.noheader
216
- layout_running_content :footer, doc, skip: num_front_matter_pages unless doc.nofooter
218
+ unless doc.noheader || @theme.header_height.to_f.zero?
219
+ layout_running_content :header, doc, skip: num_front_matter_pages
220
+ end
221
+ unless doc.nofooter || @theme.footer_height.to_f.zero?
222
+ layout_running_content :footer, doc, skip: num_front_matter_pages
223
+ end
217
224
  end
218
225
 
219
226
  add_outline doc, num_toc_levels, toc_page_nums, num_front_matter_pages
@@ -272,14 +279,28 @@ class Converter < ::Prawn::Document
272
279
  end
273
280
 
274
281
  def build_pdf_options doc, theme
275
- pdf_opts = {
276
- #compress: true,
277
- #optimize_objects: true,
278
- info: (build_pdf_info doc),
279
- margin: theme.page_margin,
280
- page_layout: ((doc.attr 'pdf-page-layout') || theme.page_layout).to_sym,
281
- skip_page_creation: true,
282
- }
282
+ case (page_margin = (doc.attr 'pdf-page-margin') || theme.page_margin)
283
+ when ::String
284
+ if page_margin.empty?
285
+ page_margin = nil
286
+ elsif (page_margin.start_with? '[') && (page_margin.end_with? ']')
287
+ if (page_margin = page_margin[1...-1].rstrip).empty?
288
+ page_margin = [0]
289
+ else
290
+ if (page_margin = page_margin.split ',', -1).length > 4
291
+ page_margin = page_margin[0..3]
292
+ end
293
+ page_margin = page_margin.map {|v| str_to_pt v.rstrip }
294
+ end
295
+ else
296
+ page_margin = [(str_to_pt page_margin)]
297
+ end
298
+ when ::Array
299
+ page_margin = page_margin[0..3] if page_margin.length > 4
300
+ page_margin = page_margin.map {|v| ::Numeric === v ? v : (str_to_pt v.to_s) }
301
+ else
302
+ page_margin = nil
303
+ end
283
304
 
284
305
  page_size = if (doc.attr? 'pdf-page-size') && (m = PageSizeRx.match(doc.attr 'pdf-page-size'))
285
306
  # e.g, [8.5in, 11in]
@@ -319,10 +340,21 @@ class Converter < ::Prawn::Document
319
340
  end
320
341
  end
321
342
 
322
- pdf_opts[:page_size] = (page_size || 'A4')
343
+ if (page_layout = (doc.attr 'pdf-page-layout') || theme.page_layout).nil_or_empty? ||
344
+ !(PageLayouts.include?(page_layout = page_layout.to_sym))
345
+ page_layout = nil
346
+ end
323
347
 
324
- pdf_opts[:text_formatter] ||= FormattedText::Formatter.new theme: theme
325
- pdf_opts
348
+ {
349
+ #compress: true,
350
+ #optimize_objects: true,
351
+ margin: (page_margin || 36),
352
+ page_size: (page_size || 'A4'),
353
+ page_layout: (page_layout || :portrait),
354
+ info: (build_pdf_info doc),
355
+ skip_page_creation: true,
356
+ text_formatter: (FormattedText::Formatter.new theme: theme)
357
+ }
326
358
  end
327
359
 
328
360
  # FIXME PdfMarks should use the PDF info result
@@ -514,7 +546,7 @@ class Converter < ::Prawn::Document
514
546
  if (transform = @text_transform) && transform != 'none'
515
547
  label_text = transform_text label_text, transform
516
548
  end
517
- label_width = width_of label_text
549
+ label_width = rendered_width_of_string label_text
518
550
  label_width = label_min_width if label_min_width && label_min_width > label_width
519
551
  end
520
552
  end
@@ -638,10 +670,10 @@ class Converter < ::Prawn::Document
638
670
  def convert_open node
639
671
  if node.style == 'abstract'
640
672
  convert_abstract node
641
- elsif node.style == 'partintro' && node.blocks.size == 1 && node.blocks.first.style == 'abstract'
673
+ elsif node.style == 'partintro' && node.blocks.size == 1 && node.blocks[0].style == 'abstract'
642
674
  # TODO process block title and id
643
675
  # TODO process abstract child even when partintro has multiple blocks
644
- convert_abstract node.blocks.first
676
+ convert_abstract node.blocks[0]
645
677
  else
646
678
  add_dest_for_block node if node.id
647
679
  layout_caption node.title if node.title?
@@ -717,9 +749,47 @@ class Converter < ::Prawn::Document
717
749
  theme_margin :block, :top
718
750
  keep_together do |box_height = nil|
719
751
  if box_height
752
+ # FIXME due to the calculation error logged in #789, we must advance page even when content is split across pages
753
+ advance_page if box_height > cursor && !at_page_top?
720
754
  float do
721
- bounding_box [0, cursor], width: bounds.width, height: box_height do
722
- theme_fill_and_stroke_bounds :sidebar
755
+ # TODO move the multi-page logic to theme_fill_and_stroke_bounds
756
+ if (b_width = @theme.sidebar_border_width || 0) > 0 && (b_color = @theme.sidebar_border_color)
757
+ if b_color == @page_bg_color # let page background cut into sidebar background
758
+ b_gap_color, b_shift = @page_bg_color, b_width
759
+ elsif (b_gap_color = @theme.sidebar_background_color) && b_gap_color != b_color
760
+ b_shift = 0
761
+ else # let page background cut into border
762
+ b_gap_color, b_shift = @page_bg_color, 0
763
+ end
764
+ else # let page background cut into sidebar background
765
+ b_width = 0.5 if b_width == 0
766
+ b_shift, b_gap_color = b_width * 0.5, @page_bg_color
767
+ end
768
+ b_radius = (@theme.sidebar_border_radius || 0) + b_width
769
+ initial_page, remaining_height = true, box_height
770
+ while remaining_height > 0
771
+ advance_page unless initial_page
772
+ fragment_height = [(available_height = cursor), remaining_height].min
773
+ bounding_box [0, available_height], width: bounds.width, height: fragment_height do
774
+ theme_fill_and_stroke_bounds :sidebar
775
+ unless b_width == 0
776
+ indent b_radius, b_radius do
777
+ move_down b_shift
778
+ # dashed line to indicate continuation from previous page; swell line to cover background
779
+ stroke_horizontal_rule b_gap_color, line_width: b_width * 1.2, line_style: :dashed
780
+ move_up b_shift
781
+ end unless initial_page
782
+ if remaining_height > fragment_height
783
+ move_down fragment_height - b_shift
784
+ indent b_radius, b_radius do
785
+ # dashed line to indicate continuation to next page; swell line to cover background
786
+ stroke_horizontal_rule b_gap_color, line_width: b_width * 1.2, line_style: :dashed
787
+ end
788
+ end
789
+ end
790
+ end
791
+ remaining_height -= fragment_height
792
+ initial_page = false
723
793
  end
724
794
  end
725
795
  end
@@ -760,7 +830,7 @@ class Converter < ::Prawn::Document
760
830
  line_metrics = calc_line_metrics @theme.base_line_height
761
831
  node.items.each_with_index do |item, idx|
762
832
  # FIXME extract to an ensure_space (or similar) method; simplify
763
- start_new_page if cursor < (line_metrics.height + line_metrics.leading + line_metrics.padding_top)
833
+ advance_page if cursor < (line_metrics.height + line_metrics.leading + line_metrics.padding_top)
764
834
  convert_colist_item item
765
835
  end
766
836
  @list_numbers.pop
@@ -770,13 +840,15 @@ class Converter < ::Prawn::Document
770
840
  end
771
841
 
772
842
  def convert_colist_item node
773
- marker_width = width_of %(#{conum_glyph 1}x)
774
-
775
- float do
776
- bounding_box [0, cursor], width: marker_width do
777
- @list_numbers << (index = @list_numbers.pop).next
778
- theme_font :conum do
779
- layout_prose index, align: :center, line_height: @theme.conum_line_height, inline_format: false, margin: 0
843
+ marker_width = nil
844
+ theme_font :conum do
845
+ marker_width = rendered_width_of_string %(#{conum_glyph 1}x)
846
+ float do
847
+ bounding_box [0, cursor], width: marker_width do
848
+ @list_numbers << (index = @list_numbers.pop).next
849
+ theme_font :conum do
850
+ layout_prose index, align: :center, line_height: @theme.conum_line_height, inline_format: false, margin: 0
851
+ end
780
852
  end
781
853
  end
782
854
  end
@@ -797,7 +869,7 @@ class Converter < ::Prawn::Document
797
869
  terms = [*terms]
798
870
  # NOTE don't orphan the terms, allow for at least one line of content
799
871
  # FIXME extract ensure_space (or similar) method
800
- start_new_page if cursor < @theme.base_line_height_length * (terms.size + 1)
872
+ advance_page if cursor < @theme.base_line_height_length * (terms.size + 1)
801
873
  terms.each do |term|
802
874
  layout_prose term.text, style: @theme.description_list_term_font_style.to_sym, margin_top: 0, margin_bottom: @theme.description_list_term_spacing, align: :left
803
875
  end
@@ -850,8 +922,15 @@ class Converter < ::Prawn::Document
850
922
  case style
851
923
  when 'bibliography'
852
924
  :square
925
+ when 'unstyled', 'no-bullet'
926
+ nil
853
927
  else
854
- style.to_sym
928
+ if Bullets.key?(candidate = style.to_sym)
929
+ candidate
930
+ else
931
+ warn %(asciidoctor: WARNING: unknown unordered list style: #{candidate})
932
+ :disc
933
+ end
855
934
  end
856
935
  else
857
936
  case node.outline_level
@@ -878,10 +957,21 @@ class Converter < ::Prawn::Document
878
957
  complex = false
879
958
  # ...or if we want to give all items in the list the same treatment
880
959
  #complex = node.items.find(&:complex?) ? true : false
881
- indent @theme.outline_list_indent do
960
+ if node.context == :ulist && !@list_bullets[-1]
961
+ if node.style == 'unstyled'
962
+ # unstyled takes away all indentation
963
+ list_indent = 0
964
+ elsif (list_indent = @theme.outline_list_indent) > 0
965
+ # no-bullet aligns text with left-hand side of bullet position (as though there's no bullet)
966
+ list_indent = [list_indent - (rendered_width_of_string %(\u2022x)), 0].max
967
+ end
968
+ else
969
+ list_indent = @theme.outline_list_indent
970
+ end
971
+ indent list_indent do
882
972
  node.items.each do |item|
883
973
  # FIXME extract to an ensure_space (or similar) method; simplify
884
- start_new_page if cursor < (line_metrics.height + line_metrics.leading + line_metrics.padding_top)
974
+ advance_page if cursor < (line_metrics.height + line_metrics.leading + line_metrics.padding_top)
885
975
  convert_outline_list_item item, item.complex?
886
976
  end
887
977
  end
@@ -898,7 +988,7 @@ class Converter < ::Prawn::Document
898
988
  # TODO move this to a draw_bullet (or draw_marker) method
899
989
  case (list_type = node.parent.context)
900
990
  when :ulist
901
- marker = @list_bullets.last
991
+ marker = @list_bullets[-1]
902
992
  if marker == :checkbox
903
993
  if node.attr? 'checkbox', nil, false
904
994
  marker = BallotBox[(node.attr? 'checked', nil, false) ? :checked : :unchecked]
@@ -917,8 +1007,8 @@ class Converter < ::Prawn::Document
917
1007
  end
918
1008
 
919
1009
  if marker
920
- marker_width = width_of marker
921
- start_position = -marker_width + -(width_of 'x')
1010
+ marker_width = rendered_width_of_string marker
1011
+ start_position = -marker_width + -(rendered_width_of_char 'x')
922
1012
  float do
923
1013
  flow_bounding_box start_position, width: marker_width do
924
1014
  layout_prose marker,
@@ -1015,7 +1105,7 @@ class Converter < ::Prawn::Document
1015
1105
  # NOTE shrink image so it fits within available space; group image & caption
1016
1106
  if (rendered_h = svg_size.output_height) > (available_h = cursor - caption_h)
1017
1107
  unless pinned || at_page_top?
1018
- start_new_page
1108
+ advance_page
1019
1109
  available_h = cursor - caption_h
1020
1110
  end
1021
1111
  if rendered_h > available_h
@@ -1044,7 +1134,7 @@ class Converter < ::Prawn::Document
1044
1134
  # NOTE shrink image so it fits within available space; group image & caption
1045
1135
  if rendered_h > (available_h = cursor - caption_h)
1046
1136
  unless pinned || at_page_top?
1047
- start_new_page
1137
+ advance_page
1048
1138
  available_h = cursor - caption_h
1049
1139
  end
1050
1140
  if rendered_h > available_h
@@ -1193,17 +1283,24 @@ class Converter < ::Prawn::Document
1193
1283
  when 'coderay'
1194
1284
  Helpers.require_library CodeRayRequirePath, 'coderay' unless defined? ::Asciidoctor::Prawn::CodeRayEncoder
1195
1285
  source_string, conum_mapping = extract_conums source_string
1196
- fragments = (::CodeRay.scan source_string, (node.attr 'language', 'text', false).to_sym).to_prawn
1286
+ srclang = node.attr 'language', 'text', false
1287
+ begin
1288
+ ::CodeRay::Scanners[(srclang = (srclang.start_with? 'html+') ? srclang[5..-1].to_sym : srclang.to_sym)]
1289
+ rescue ::ArgumentError
1290
+ srclang = :text
1291
+ end
1292
+ fragments = (::CodeRay.scan source_string, srclang).to_prawn
1197
1293
  conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
1198
1294
  when 'pygments'
1199
1295
  Helpers.require_library 'pygments', 'pygments.rb' unless defined? ::Pygments
1200
- lexer = ::Pygments::Lexer[node.attr 'language', 'text', false] || ::Pygments::Lexer['text']
1201
- pygments_config = {
1296
+ lexer = ::Pygments::Lexer.find_by_alias(node.attr 'language', 'text', false) || ::Pygments::Lexer.find_by_mimetype('text/plain')
1297
+ lexer_opts = {
1202
1298
  nowrap: true,
1203
1299
  noclasses: true,
1204
1300
  stripnl: false,
1205
- style: style = (node.document.attr 'pygments-style') || 'pastie'
1301
+ style: (style = (node.document.attr 'pygments-style') || 'pastie')
1206
1302
  }
1303
+ lexer_opts[:startinline] = !(node.option? 'mixed') if lexer.name == 'PHP'
1207
1304
  # TODO enable once we support background color on spans
1208
1305
  #if node.attr? 'highlight', nil, false
1209
1306
  # unless (hl_lines = node.resolve_lines_to_highlight(node.attr 'highlight', nil, false)).empty?
@@ -1222,21 +1319,23 @@ class Converter < ::Prawn::Document
1222
1319
  end
1223
1320
  source_string, conum_mapping = extract_conums source_string
1224
1321
  # NOTE pygments.rb strips trailing whitespace; preserve it in case there are conums on last line
1225
- num_trailing_spaces = source_string.size - (source_string = source_string.rstrip).size if conum_mapping
1226
- result = lexer.highlight source_string, options: pygments_config
1322
+ num_trailing_spaces = source_string.length - (source_string = source_string.rstrip).length if conum_mapping
1323
+ result = lexer.highlight source_string, options: lexer_opts
1227
1324
  fragments = guard_indentation text_formatter.format result
1228
1325
  conum_mapping ? (restore_conums fragments, conum_mapping, num_trailing_spaces) : fragments
1229
1326
  when 'rouge'
1230
1327
  Helpers.require_library RougeRequirePath, 'rouge' unless defined? ::Rouge::Formatters::Prawn
1231
1328
  lexer = ::Rouge::Lexer.find(node.attr 'language', 'text', false) || ::Rouge::Lexers::PlainText
1329
+ lexer_opts = lexer.tag == 'php' ? { start_inline: !(node.option? 'mixed') } : {}
1232
1330
  formatter = (@rouge_formatter ||= ::Rouge::Formatters::Prawn.new theme: (node.document.attr 'rouge-style'), line_gap: @theme.code_line_gap)
1331
+ formatter_opts = (node.attr? 'linenums') ? { line_numbers: true, start_line: (node.attr 'start').to_i } : {}
1233
1332
  # QUESTION allow border color to be set by theme for highlighted block?
1234
1333
  bg_color_override = formatter.background_color
1235
1334
  source_string, conum_mapping = extract_conums source_string
1236
1335
  # NOTE trailing endline is added to address https://github.com/jneen/rouge/issues/279
1237
- fragments = formatter.format (lexer.lex %(#{source_string}#{LF})), line_numbers: (node.attr? 'linenums')
1336
+ fragments = formatter.format (lexer.lex %(#{source_string}#{LF}), lexer_opts), formatter_opts
1238
1337
  # NOTE cleanup trailing endline (handled in rouge_ext/formatters/prawn instead)
1239
- #fragments.last[:text] == LF ? fragments.pop : fragments.last[:text].chop!
1338
+ #fragments[-1][:text] == LF ? fragments.pop : fragments[-1][:text].chop!
1240
1339
  conum_mapping ? (restore_conums fragments, conum_mapping) : fragments
1241
1340
  else
1242
1341
  # NOTE only format if we detect a need (callouts or inline formatting)
@@ -1408,8 +1507,12 @@ class Converter < ::Prawn::Document
1408
1507
  # NOTE emulate table bg color by using it as a fallback value for each element
1409
1508
  head_bg_color = resolve_theme_color :table_head_background_color, tbl_bg_color
1410
1509
  foot_bg_color = resolve_theme_color :table_foot_background_color, tbl_bg_color
1411
- odd_row_bg_color = resolve_theme_color :table_odd_row_background_color, tbl_bg_color
1412
- even_row_bg_color = resolve_theme_color :table_even_row_background_color, tbl_bg_color
1510
+ body_bg_color = resolve_theme_color :table_body_background_color,
1511
+ # table_odd_row_background_color is deprecated
1512
+ (resolve_theme_color :table_odd_row_background_color, tbl_bg_color)
1513
+ body_stripe_bg_color = resolve_theme_color :table_body_stripe_background_color,
1514
+ # table_even_row_background_color is deprecated
1515
+ (resolve_theme_color :table_even_row_background_color, tbl_bg_color)
1413
1516
 
1414
1517
  table_data = []
1415
1518
  node.rows[:head].each do |row|
@@ -1436,6 +1539,7 @@ class Converter < ::Prawn::Document
1436
1539
  table_data << row_data
1437
1540
  end
1438
1541
 
1542
+ header_cell_data_cache = nil
1439
1543
  (node.rows[:body] + node.rows[:foot]).each do |row|
1440
1544
  row_data = []
1441
1545
  row.each do |cell|
@@ -1448,31 +1552,35 @@ class Converter < ::Prawn::Document
1448
1552
  align: (cell.attr 'halign', nil, false).to_sym,
1449
1553
  valign: (val = cell.attr 'valign', nil, false) == 'middle' ? :center : val.to_sym
1450
1554
  }
1555
+ cell_transform = nil
1451
1556
  case cell.style
1452
1557
  when :emphasis
1453
1558
  cell_data[:font_style] = :italic
1454
1559
  when :strong
1455
1560
  cell_data[:font_style] = :bold
1456
1561
  when :header
1457
- unless defined? header_cell_data
1458
- header_cell_data = {}
1562
+ unless header_cell_data_cache
1563
+ header_cell_data_cache = {}
1459
1564
  [
1460
- # TODO honor text_transform key
1461
- # QUESTION should we honor alignment set by col/cell spec? how can we tell?
1462
- #['align', :align, true],
1565
+ #['align', :align, true], # QUESTION should we honor alignment set by col/cell spec? how can we tell?
1463
1566
  ['font_color', :text_color, false],
1464
1567
  ['font_family', :font, false],
1465
1568
  ['font_size', :size, false],
1466
- ['font_style', :font_style, true]
1569
+ ['font_style', :font_style, true],
1570
+ ['text_transform', :text_transform, true]
1467
1571
  ].each do |(theme_key, data_key, symbol_value)|
1468
1572
  if (val = theme[%(table_header_cell_#{theme_key})] || theme[%(table_head_#{theme_key})])
1469
- header_cell_data[data_key] = symbol_value ? val.to_sym : val
1573
+ header_cell_data_cache[data_key] = symbol_value ? val.to_sym : val
1470
1574
  end
1471
1575
  end
1472
1576
  if (val = resolve_theme_color :table_header_cell_background_color, head_bg_color)
1473
- header_cell_data[:background_color] = val
1577
+ header_cell_data_cache[:background_color] = val
1474
1578
  end
1475
1579
  end
1580
+ header_cell_data = header_cell_data_cache.dup
1581
+ if (cell_transform = header_cell_data.delete :text_transform) == 'none'
1582
+ cell_transform = nil
1583
+ end
1476
1584
  cell_data.update header_cell_data unless header_cell_data.empty?
1477
1585
  when :monospaced
1478
1586
  cell_data[:font] = theme.literal_font_family
@@ -1509,10 +1617,10 @@ class Converter < ::Prawn::Document
1509
1617
  cell_data[:font_style] = (val = theme.table_font_style) ? val.to_sym : nil
1510
1618
  end
1511
1619
  unless cell_data.key? :content
1512
- # NOTE effectively the same as calling cell.content (should we use that instead?)
1513
- # TODO hard breaks not quite the same result as separate paragraphs; need custom cell impl
1514
- if (cell_text = cell.text).include? LF
1515
- cell_data[:content] = cell_text.split(BlankLineRx).map {|l| l.tr_s(WhitespaceChars, ' ') }.join(DoubleLF)
1620
+ if (cell_text = cell_transform ? (transform_text cell.text, cell_transform) : cell.text).include? LF
1621
+ # NOTE effectively the same as calling cell.content (should we use that instead?)
1622
+ # FIXME hard breaks not quite the same result as separate paragraphs; need custom cell impl here
1623
+ cell_data[:content] = (cell_text.split BlankLineRx).map {|l| l.tr_s WhitespaceChars, ' ' }.join DoubleLF
1516
1624
  cell_data[:inline_format] = true
1517
1625
  else
1518
1626
  cell_data[:content] = cell_text
@@ -1533,33 +1641,36 @@ class Converter < ::Prawn::Document
1533
1641
  table_data = [empty_row]
1534
1642
  end
1535
1643
 
1536
- border = {}
1537
- table_border_color = theme.table_border_color || table_grid_color || theme.base_border_color
1644
+ border_width = {}
1645
+ table_border_color = theme.table_border_color || theme.table_grid_color || theme.base_border_color
1646
+ table_border_style = (theme.table_border_style || :solid).to_sym
1538
1647
  table_border_width = theme.table_border_width
1648
+ [:top, :bottom, :left, :right].each {|edge| border_width[edge] = table_border_width }
1649
+ table_grid_color = theme.table_grid_color || table_border_color
1650
+ table_grid_style = (theme.table_grid_style || table_border_style).to_sym
1539
1651
  table_grid_width = theme.table_grid_width || theme.table_border_width
1540
- [:top, :bottom, :left, :right].each {|edge| border[edge] = table_border_width }
1541
- [:cols, :rows].each {|edge| border[edge] = table_grid_width }
1652
+ [:cols, :rows].each {|edge| border_width[edge] = table_grid_width }
1542
1653
 
1543
1654
  case (grid = node.attr 'grid', 'all', false)
1544
1655
  when 'all'
1545
1656
  # keep inner borders
1546
1657
  when 'cols'
1547
- border[:rows] = 0
1658
+ border_width[:rows] = 0
1548
1659
  when 'rows'
1549
- border[:cols] = 0
1660
+ border_width[:cols] = 0
1550
1661
  else # none
1551
- border[:rows] = border[:cols] = 0
1662
+ border_width[:rows] = border_width[:cols] = 0
1552
1663
  end
1553
1664
 
1554
1665
  case (frame = node.attr 'frame', 'all', false)
1555
1666
  when 'all'
1556
1667
  # keep outer borders
1557
1668
  when 'topbot'
1558
- border[:left] = border[:right] = 0
1669
+ border_width[:left] = border_width[:right] = 0
1559
1670
  when 'sides'
1560
- border[:top] = border[:bottom] = 0
1671
+ border_width[:top] = border_width[:bottom] = 0
1561
1672
  else # none
1562
- border[:top] = border[:right] = border[:bottom] = border[:left] = 0
1673
+ border_width[:top] = border_width[:right] = border_width[:bottom] = border_width[:left] = 0
1563
1674
  end
1564
1675
 
1565
1676
  if node.option? 'autowidth'
@@ -1576,7 +1687,7 @@ class Converter < ::Prawn::Document
1576
1687
  end
1577
1688
 
1578
1689
  if ((alignment = node.attr 'align', nil, false) && (BlockAlignmentNames.include? alignment)) ||
1579
- (alignment = (node.roles & BlockAlignmentNames).last)
1690
+ (alignment = (node.roles & BlockAlignmentNames)[-1])
1580
1691
  alignment = alignment.to_sym
1581
1692
  else
1582
1693
  alignment = :left
@@ -1588,16 +1699,29 @@ class Converter < ::Prawn::Document
1588
1699
  header: table_header,
1589
1700
  position: alignment,
1590
1701
  cell_style: {
1591
- padding: theme.table_cell_padding,
1702
+ # NOTE the border color and style of the outer frame is set later
1703
+ border_color: table_grid_color,
1704
+ border_lines: [table_grid_style],
1705
+ # NOTE the border width is set later
1592
1706
  border_width: 0,
1593
- # NOTE the border color of edges is set later
1594
- border_color: theme.table_grid_color || theme.table_border_color || theme.base_border_color
1707
+ padding: theme.table_cell_padding
1595
1708
  },
1596
1709
  width: table_width,
1597
- column_widths: column_widths,
1598
- row_colors: [odd_row_bg_color, even_row_bg_color]
1710
+ column_widths: column_widths
1599
1711
  }
1600
1712
 
1713
+ # QUESTION should we support nth; should we support sequence of roles?
1714
+ case node.attr 'stripes', 'even', false
1715
+ when 'all'
1716
+ table_settings[:row_colors] = [body_stripe_bg_color]
1717
+ when 'even'
1718
+ table_settings[:row_colors] = [body_bg_color, body_stripe_bg_color]
1719
+ when 'odd'
1720
+ table_settings[:row_colors] = [body_stripe_bg_color, body_bg_color]
1721
+ else # none
1722
+ table_settings[:row_colors] = [body_bg_color]
1723
+ end
1724
+
1601
1725
  theme_margin :block, :top
1602
1726
 
1603
1727
  table table_data, table_settings do
@@ -1606,40 +1730,46 @@ class Converter < ::Prawn::Document
1606
1730
  @pdf.layout_table_caption node, table_width, alignment if node.title? && caption_side == :top
1607
1731
  if grid == 'none' && frame == 'none'
1608
1732
  if table_header
1609
- # FIXME allow header border bottom width to be set by theme
1610
- rows(0).border_bottom_width = 1.5
1733
+ # FIXME allow header border bottom width and style to be set by theme
1734
+ rows(0).tap do |r|
1735
+ r.border_bottom_line, r.border_bottom_width = :solid, 1.25
1736
+ # QUESTION should we use the table border color for the bottom border color of the header row?
1737
+ #r.border_bottom_color, r.border_bottom_line, r.border_bottom_width = table_border_color, :solid, 1.25
1738
+ end
1611
1739
  end
1612
1740
  else
1613
1741
  # apply the grid setting first across all cells
1614
- cells.border_width = [border[:rows], border[:cols], border[:rows], border[:cols]]
1742
+ cells.border_width = [border_width[:rows], border_width[:cols], border_width[:rows], border_width[:cols]]
1615
1743
 
1616
1744
  if table_header
1617
- # FIXME allow header border bottom width to be set by theme
1618
- rows(0).border_bottom_width = 1.5
1619
- # QUESTION should we use the table border color for the bottom border color of the header row?
1620
- #rows(0).border_bottom_color = table_border_color
1621
- #rows(1).border_top_width = 0 if row_length > 1
1745
+ # FIXME allow header border bottom width and style to be set by theme
1746
+ rows(0).tap do |r|
1747
+ r.border_bottom_line, r.border_bottom_width = :solid, 1.25
1748
+ # QUESTION should we use the table border color for the bottom border color of the header row?
1749
+ #r.border_bottom_color, r.border_bottom_line, r.border_bottom_width = table_border_color, :solid, 1.25
1750
+ end
1751
+ rows(1).tap do |r|
1752
+ r.border_top_line, r.border_top_width = :solid, 1.25
1753
+ # QUESTION should we use the table border color for the top border color of the first row?
1754
+ #r.border_top_color, r.border_top_line, r.border_top_width = table_border_color, :solid, 1.25
1755
+ end if num_rows > 1
1622
1756
  end
1623
1757
 
1624
1758
  # top edge of table
1625
1759
  rows(0).tap do |r|
1626
- r.border_top_width = border[:top]
1627
- r.border_top_color = table_border_color
1760
+ r.border_top_color, r.border_top_line, r.border_top_width = table_border_color, table_border_style, border_width[:top]
1628
1761
  end
1629
1762
  # right edge of table
1630
1763
  columns(num_cols - 1).tap do |r|
1631
- r.border_right_width = border[:right]
1632
- r.border_right_color = table_border_color
1764
+ r.border_right_color, r.border_right_line, r.border_right_width = table_border_color, table_border_style, border_width[:right]
1633
1765
  end
1634
1766
  # bottom edge of table
1635
1767
  rows(num_rows - 1).tap do |r|
1636
- r.border_bottom_width = border[:bottom]
1637
- r.border_bottom_color = table_border_color
1768
+ r.border_bottom_color, r.border_bottom_line, r.border_bottom_width = table_border_color, table_border_style, border_width[:bottom]
1638
1769
  end
1639
1770
  # left edge of table
1640
1771
  columns(0).tap do |r|
1641
- r.border_left_width = border[:left]
1642
- r.border_left_color = table_border_color
1772
+ r.border_left_color, r.border_left_line, r.border_left_width = table_border_color, table_border_style, border_width[:left]
1643
1773
  end
1644
1774
  end
1645
1775
 
@@ -1678,7 +1808,7 @@ class Converter < ::Prawn::Document
1678
1808
 
1679
1809
  # NOTE to insert sequential page breaks, you must put {nbsp} between page breaks
1680
1810
  def convert_page_break node
1681
- start_new_page unless at_page_top?
1811
+ advance_page unless at_page_top?
1682
1812
  end
1683
1813
 
1684
1814
  def convert_index_section node
@@ -1709,14 +1839,14 @@ class Converter < ::Prawn::Document
1709
1839
  end
1710
1840
 
1711
1841
  def convert_index_list_item term
1712
- text = term.name
1842
+ text = escape_xml term.name
1713
1843
  unless term.container?
1714
1844
  if @media == 'screen'
1715
1845
  pagenums = term.dests.map {|dest| %(<a anchor="#{dest[:anchor]}">#{dest[:page]}</a>) }
1716
1846
  else
1717
1847
  pagenums = term.dests.uniq {|dest| dest[:page] }.map {|dest| dest[:page].to_s }
1718
1848
  end
1719
- text = %(#{escape_xml text}, #{pagenums * ', '})
1849
+ text = %(#{text}, #{pagenums * ', '})
1720
1850
  end
1721
1851
  layout_prose text, align: :left, margin: 0
1722
1852
 
@@ -1788,6 +1918,7 @@ class Converter < ::Prawn::Document
1788
1918
 
1789
1919
  def convert_inline_button node
1790
1920
  %(<strong>[#{NarrowNoBreakSpace}#{node.text}#{NarrowNoBreakSpace}]</strong>)
1921
+ #%(<strong>[#{NoBreakSpace}#{node.text}#{NoBreakSpace}]</strong>)
1791
1922
  end
1792
1923
 
1793
1924
  def convert_inline_callout node
@@ -2197,12 +2328,12 @@ class Converter < ::Prawn::Document
2197
2328
  font_color: @theme.toc_dot_leader_font_color || @font_color,
2198
2329
  font_style: dot_leader_font_style,
2199
2330
  levels: ((dot_leader_l = @theme.toc_dot_leader_levels) == 'none' ? ::Set.new :
2200
- (dot_leader_l && dot_leader_l != 'all' ? dot_leader_l.to_s.split.map(&:to_i).to_set : (1..num_levels).to_set)),
2331
+ (dot_leader_l && dot_leader_l != 'all' ? dot_leader_l.to_s.split.map(&:to_i).to_set : (0..num_levels).to_set)),
2201
2332
  text: (dot_leader_text = @theme.toc_dot_leader_content || DotLeaderTextDefault),
2202
- width: dot_leader_text.empty? ? 0 : (width_of dot_leader_text),
2333
+ width: dot_leader_text.empty? ? 0 : (rendered_width_of_string dot_leader_text),
2203
2334
  # TODO spacer gives a little bit of room between dots and page number
2204
2335
  spacer: { text: NoBreakSpace, size: (spacer_font_size = @font_size * 0.25) },
2205
- spacer_width: (width_of NoBreakSpace, size: spacer_font_size)
2336
+ spacer_width: (rendered_width_of_char NoBreakSpace, size: spacer_font_size)
2206
2337
  }
2207
2338
  end
2208
2339
  line_metrics = calc_line_metrics @theme.toc_line_height
@@ -2253,7 +2384,8 @@ class Converter < ::Prawn::Document
2253
2384
  move_cursor_to start_cursor
2254
2385
  if dot_leader[:width] > 0 && (dot_leader[:levels].include? sect.level)
2255
2386
  pgnum_label_font_settings = { color: @font_color, font: font_family, size: @font_size, styles: font_styles }
2256
- pgnum_label_width = width_of pgnum_label
2387
+ pgnum_label_width = rendered_width_of_string pgnum_label
2388
+ # WARNING width_of is not accurate if string must use characters from fallback font
2257
2389
  sect_title_width = width_of sect_title, inline_format: true
2258
2390
  save_font do
2259
2391
  # NOTE the same font is used for dot leaders throughout toc
@@ -2296,8 +2428,6 @@ class Converter < ::Prawn::Document
2296
2428
 
2297
2429
  # TODO delegate to layout_page_header and layout_page_footer per page
2298
2430
  def layout_running_content periphery, doc, opts = {}
2299
- # QUESTION should we short-circuit if setting not specified and if so, which setting?
2300
- return unless (periphery == :header && @theme.header_height) || (periphery == :footer && @theme.footer_height)
2301
2431
  skip = opts[:skip] || 1
2302
2432
  # NOTE find and advance to first non-imported content page to use as model page
2303
2433
  return unless (content_start_page = state.pages[skip..-1].index {|p| !p.imported_page? })
@@ -2658,7 +2788,7 @@ class Converter < ::Prawn::Document
2658
2788
  # FIXME link to title page if there's a cover page (skip cover page and ensuing blank page)
2659
2789
  page title: doctitle, destination: (document.dest_top 1)
2660
2790
  end
2661
- page title: (doc.attr 'toc-title'), destination: (document.dest_top toc_page_nums.first) if toc_page_nums.first
2791
+ page title: (doc.attr 'toc-title'), destination: (document.dest_top toc_page_nums.first) unless toc_page_nums.none?
2662
2792
  # QUESTION any way to get add_outline_level to invoke in the context of the outline?
2663
2793
  document.add_outline_level self, doc.sections, num_levels
2664
2794
  end
@@ -2673,11 +2803,11 @@ class Converter < ::Prawn::Document
2673
2803
  sections.each do |sect|
2674
2804
  sect_title = sanitize sect.numbered_title formal: true
2675
2805
  sect_destination = sect.attr 'pdf-destination'
2676
- if (subsections = sect.sections).empty? || sect.level == num_levels
2806
+ if (level = sect.level) == num_levels || !sect.sections?
2677
2807
  outline.page title: sect_title, destination: sect_destination
2678
- elsif sect.level < num_levels + 1
2808
+ elsif level <= num_levels
2679
2809
  outline.section sect_title, { destination: sect_destination } do
2680
- add_outline_level outline, subsections, num_levels
2810
+ add_outline_level outline, sect.sections, num_levels
2681
2811
  end
2682
2812
  end
2683
2813
  end
@@ -2866,6 +2996,32 @@ class Converter < ::Prawn::Document
2866
2996
  line_widths.max
2867
2997
  end
2868
2998
 
2999
+ # Compute the rendered width of a string, taking fallback fonts into account
3000
+ def rendered_width_of_string str, opts = {}
3001
+ if str.length == 1
3002
+ rendered_width_of_char str, opts
3003
+ elsif (chars = str.each_char).all? {|char| font.glyph_present? char }
3004
+ width_of_string str, opts
3005
+ else
3006
+ char_widths = chars.map {|char| rendered_width_of_char char, opts }
3007
+ char_widths.reduce(&:+) + (char_widths.length * character_spacing)
3008
+ end
3009
+ end
3010
+
3011
+ # Compute the rendered width of a char, taking fallback fonts into account
3012
+ def rendered_width_of_char char, opts = {}
3013
+ if @fallback_fonts.empty? || (font.glyph_present? char)
3014
+ width_of_string char, opts
3015
+ else
3016
+ @fallback_fonts.each do |fallback_font|
3017
+ font fallback_font do
3018
+ return width_of_string char, opts if font.glyph_present? char
3019
+ end
3020
+ end
3021
+ width_of_string char, opts
3022
+ end
3023
+ end
3024
+
2869
3025
  # TODO document me, esp the first line formatting functionality
2870
3026
  def typeset_text string, line_metrics, opts = {}
2871
3027
  move_down line_metrics.padding_top
@@ -2901,11 +3057,11 @@ class Converter < ::Prawn::Document
2901
3057
  result = []
2902
3058
  string.each_line do |line|
2903
3059
  if line.start_with? TAB
2904
- # NOTE '+' operator is faster than interpolation in this case
2905
3060
  if guard_indent
2906
- line.sub!(TabIndentRx) {|tabs| GuardedIndent + (full_tab_space * tabs.length).chop! }
3061
+ # NOTE '+' operator is faster than interpolation
3062
+ line.sub!(TabIndentRx) { GuardedIndent + (full_tab_space * $&.length).chop! }
2907
3063
  else
2908
- line.sub!(TabIndentRx) {|tabs| full_tab_space * tabs.length }
3064
+ line.sub!(TabIndentRx) { full_tab_space * $&.length }
2909
3065
  end
2910
3066
  leading_space = false
2911
3067
  # QUESTION should we check for LF first?