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.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +35 -4
- data/Gemfile +1 -1
- data/README.adoc +33 -3
- data/asciidoctor-pdf.gemspec +6 -5
- data/data/themes/base-theme.yml +3 -1
- data/data/themes/default-theme.yml +5 -5
- data/docs/theming-guide.adoc +86 -37
- data/lib/asciidoctor-pdf.rb +1 -1
- data/lib/asciidoctor-pdf/converter.rb +265 -109
- data/lib/asciidoctor-pdf/core_ext/object.rb +1 -1
- data/lib/asciidoctor-pdf/core_ext/string.rb +1 -1
- data/lib/asciidoctor-pdf/formatted_text/inline_destination_marker.rb +1 -1
- data/lib/asciidoctor-pdf/formatted_text/transform.rb +12 -3
- data/lib/asciidoctor-pdf/pdf-core_ext.rb +1 -0
- data/lib/asciidoctor-pdf/pdf-core_ext/page.rb +8 -1
- data/lib/asciidoctor-pdf/pdf-core_ext/pdf_object.rb +6 -0
- data/lib/asciidoctor-pdf/prawn-table_ext/cell/text.rb +1 -0
- data/lib/asciidoctor-pdf/prawn-templates_ext.rb +5 -0
- data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +1 -1
- data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +6 -7
- data/lib/asciidoctor-pdf/rouge_ext/formatters/prawn.rb +8 -5
- data/lib/asciidoctor-pdf/theme_loader.rb +3 -4
- data/lib/asciidoctor-pdf/ttfunk_ext.rb +8 -0
- data/lib/asciidoctor-pdf/version.rb +1 -1
- metadata +17 -8
data/lib/asciidoctor-pdf.rb
CHANGED
@@ -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
|
-
|
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 (
|
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 =
|
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
|
-
|
216
|
-
|
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
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
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
|
-
|
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
|
-
|
325
|
-
|
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 =
|
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.
|
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
|
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
|
-
|
722
|
-
|
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
|
-
|
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 =
|
774
|
-
|
775
|
-
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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 =
|
921
|
-
start_position = -marker_width + -(
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
1201
|
-
|
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.
|
1226
|
-
result = lexer.highlight source_string, options:
|
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})),
|
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
|
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
|
-
|
1412
|
-
|
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
|
1458
|
-
|
1562
|
+
unless header_cell_data_cache
|
1563
|
+
header_cell_data_cache = {}
|
1459
1564
|
[
|
1460
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
1513
|
-
|
1514
|
-
|
1515
|
-
cell_data[:content] = cell_text.split
|
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
|
-
|
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
|
-
[:
|
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
|
-
|
1658
|
+
border_width[:rows] = 0
|
1548
1659
|
when 'rows'
|
1549
|
-
|
1660
|
+
border_width[:cols] = 0
|
1550
1661
|
else # none
|
1551
|
-
|
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
|
-
|
1669
|
+
border_width[:left] = border_width[:right] = 0
|
1559
1670
|
when 'sides'
|
1560
|
-
|
1671
|
+
border_width[:top] = border_width[:bottom] = 0
|
1561
1672
|
else # none
|
1562
|
-
|
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)
|
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
|
-
|
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
|
-
|
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).
|
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 = [
|
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).
|
1619
|
-
|
1620
|
-
|
1621
|
-
|
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 =
|
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 =
|
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 =
|
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 =
|
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
|
-
|
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 = %(#{
|
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 : (
|
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 : (
|
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: (
|
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 =
|
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)
|
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 (
|
2806
|
+
if (level = sect.level) == num_levels || !sect.sections?
|
2677
2807
|
outline.page title: sect_title, destination: sect_destination
|
2678
|
-
elsif
|
2808
|
+
elsif level <= num_levels
|
2679
2809
|
outline.section sect_title, { destination: sect_destination } do
|
2680
|
-
add_outline_level outline,
|
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
|
-
|
3061
|
+
# NOTE '+' operator is faster than interpolation
|
3062
|
+
line.sub!(TabIndentRx) { GuardedIndent + (full_tab_space * $&.length).chop! }
|
2907
3063
|
else
|
2908
|
-
line.sub!(TabIndentRx) {
|
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?
|