asciidoctor-pdf 1.5.0.beta.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.adoc +277 -2
- data/LICENSE.adoc +1 -1
- data/NOTICE.adoc +1 -1
- data/README.adoc +486 -292
- data/asciidoctor-pdf.gemspec +12 -11
- data/bin/asciidoctor-pdf +2 -6
- data/bin/asciidoctor-pdf-optimize +20 -0
- data/data/fonts/ABOUT-mplus1mn-subset +26 -0
- data/data/fonts/ABOUT-mplus1p-subset +26 -0
- data/data/fonts/ABOUT-notoemoji-subset +3 -0
- data/data/fonts/ABOUT-notoserif-subset +26 -0
- data/data/fonts/{LICENSE-mplus-testflight-58 → LICENSE-mplus} +2 -2
- data/data/fonts/{LICENSE-noto-2015-06-05 → LICENSE-notoserif} +0 -0
- data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-bold-subset.ttf +0 -0
- data/data/fonts/mplus1mn-bold_italic-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-bold_italic-subset.ttf +0 -0
- data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-italic-subset.ttf +0 -0
- data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
- data/data/fonts/mplus1mn-regular-subset.ttf +0 -0
- data/data/fonts/mplus1p-regular-fallback.ttf +0 -0
- data/data/fonts/notoemoji-subset.ttf +0 -0
- data/data/fonts/notoserif-bold-subset.ttf +0 -0
- data/data/fonts/notoserif-bold_italic-subset.ttf +0 -0
- data/data/fonts/notoserif-italic-subset.ttf +0 -0
- data/data/fonts/notoserif-regular-subset.ttf +0 -0
- data/data/themes/base-theme.yml +22 -4
- data/data/themes/default-theme.yml +59 -29
- data/data/themes/default-with-fallback-font-theme.yml +4 -17
- data/docs/theming-guide.adoc +1647 -167
- data/lib/asciidoctor/pdf/converter.rb +4489 -0
- data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/abstract_block.rb +2 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_node.rb +7 -0
- data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/document.rb +2 -0
- data/lib/asciidoctor/pdf/ext/asciidoctor/image.rb +35 -0
- data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/list.rb +4 -2
- data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/list_item.rb +3 -1
- data/lib/asciidoctor/pdf/ext/asciidoctor/logging_shim.rb +33 -0
- data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/section.rb +9 -6
- data/lib/asciidoctor/pdf/ext/asciidoctor.rb +11 -0
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/array.rb +6 -0
- data/lib/asciidoctor/pdf/ext/core/file.rb +9 -0
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/hash.rb +2 -0
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/numeric.rb +5 -3
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/object.rb +3 -1
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/quantifiable_stdout.rb +9 -1
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/regexp.rb +2 -0
- data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/string.rb +9 -13
- data/lib/asciidoctor/pdf/ext/core.rb +10 -0
- data/lib/asciidoctor/pdf/ext/pdf-core/page.rb +54 -0
- data/lib/asciidoctor/pdf/ext/pdf-core/pdf_object.rb +8 -0
- data/lib/asciidoctor/pdf/ext/pdf-core.rb +4 -0
- data/lib/asciidoctor/pdf/ext/prawn/coderay_encoder.rb +117 -0
- data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +922 -0
- data/lib/{asciidoctor-pdf/prawn_ext → asciidoctor/pdf/ext/prawn}/font/afm.rb +14 -10
- data/lib/asciidoctor/pdf/ext/prawn/font_metric_cache.rb +9 -0
- data/lib/asciidoctor/pdf/ext/prawn/formatted_text/box.rb +66 -0
- data/lib/{asciidoctor-pdf/prawn_ext → asciidoctor/pdf/ext/prawn}/formatted_text/fragment.rb +16 -12
- data/lib/asciidoctor/pdf/ext/prawn/images.rb +54 -0
- data/lib/asciidoctor/pdf/ext/prawn-svg/interface.rb +14 -0
- data/lib/{asciidoctor-pdf/prawn-svg_ext.rb → asciidoctor/pdf/ext/prawn-svg.rb} +3 -1
- data/lib/asciidoctor/pdf/ext/prawn-table/cell/asciidoc.rb +76 -0
- data/lib/{asciidoctor-pdf/prawn-table_ext → asciidoctor/pdf/ext/prawn-table}/cell/text.rb +6 -3
- data/lib/{asciidoctor-pdf/prawn-table_ext → asciidoctor/pdf/ext/prawn-table}/cell.rb +10 -10
- data/lib/asciidoctor/pdf/ext/prawn-table.rb +6 -0
- data/lib/{asciidoctor-pdf/prawn-templates_ext.rb → asciidoctor/pdf/ext/prawn-templates.rb} +2 -0
- data/lib/asciidoctor/pdf/ext/prawn.rb +9 -0
- data/lib/asciidoctor/pdf/ext/pygments.rb +34 -0
- data/lib/asciidoctor/pdf/ext/rouge/formatters/prawn.rb +208 -0
- data/lib/{asciidoctor-pdf/rouge_ext → asciidoctor/pdf/ext/rouge}/themes/asciidoctor_pdf_default.rb +2 -0
- data/lib/asciidoctor/pdf/ext/rouge.rb +5 -0
- data/lib/asciidoctor/pdf/ext.rb +9 -0
- data/lib/asciidoctor/pdf/formatted_text/formatter.rb +43 -0
- data/lib/asciidoctor/pdf/formatted_text/fragment_position_renderer.rb +14 -0
- data/lib/asciidoctor/pdf/formatted_text/inline_destination_marker.rb +21 -0
- data/lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb +134 -0
- data/lib/asciidoctor/pdf/formatted_text/inline_image_renderer.rb +51 -0
- data/lib/asciidoctor/pdf/formatted_text/inline_text_aligner.rb +22 -0
- data/lib/{asciidoctor-pdf → asciidoctor/pdf}/formatted_text/parser.rb +31 -7
- data/lib/{asciidoctor-pdf → asciidoctor/pdf}/formatted_text/parser.treetop +3 -4
- data/lib/asciidoctor/pdf/formatted_text/source_wrap.rb +43 -0
- data/lib/asciidoctor/pdf/formatted_text/text_background_and_border_renderer.rb +55 -0
- data/lib/asciidoctor/pdf/formatted_text/transform.rb +394 -0
- data/lib/{asciidoctor-pdf → asciidoctor/pdf}/formatted_text.rb +4 -0
- data/lib/asciidoctor/pdf/index_catalog.rb +133 -0
- data/lib/asciidoctor/pdf/measurements.rb +62 -0
- data/lib/asciidoctor/pdf/optimizer.rb +44 -0
- data/lib/asciidoctor/pdf/pdfmark.rb +41 -0
- data/lib/asciidoctor/pdf/roman_numeral.rb +128 -0
- data/lib/asciidoctor/pdf/sanitizer.rb +45 -0
- data/lib/asciidoctor/pdf/text_transformer.rb +116 -0
- data/lib/asciidoctor/pdf/theme_loader.rb +305 -0
- data/lib/asciidoctor/pdf/version.rb +8 -1
- data/lib/asciidoctor/pdf.rb +15 -1
- data/lib/asciidoctor-pdf/converter.rb +2 -3824
- data/lib/asciidoctor-pdf/version.rb +3 -6
- data/lib/asciidoctor-pdf.rb +3 -4
- metadata +130 -85
- data/lib/asciidoctor-pdf/asciidoctor_ext/image.rb +0 -24
- data/lib/asciidoctor-pdf/asciidoctor_ext/logging_shim.rb +0 -25
- data/lib/asciidoctor-pdf/asciidoctor_ext.rb +0 -8
- data/lib/asciidoctor-pdf/core_ext/ostruct.rb +0 -8
- data/lib/asciidoctor-pdf/core_ext.rb +0 -6
- data/lib/asciidoctor-pdf/formatted_text/formatter.rb +0 -40
- data/lib/asciidoctor-pdf/formatted_text/inline_destination_marker.rb +0 -21
- data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +0 -160
- data/lib/asciidoctor-pdf/formatted_text/inline_image_renderer.rb +0 -46
- data/lib/asciidoctor-pdf/formatted_text/inline_text_aligner.rb +0 -20
- data/lib/asciidoctor-pdf/formatted_text/text_background_and_border_renderer.rb +0 -45
- data/lib/asciidoctor-pdf/formatted_text/transform.rb +0 -294
- data/lib/asciidoctor-pdf/implicit_header_processor.rb +0 -63
- data/lib/asciidoctor-pdf/index_catalog.rb +0 -127
- data/lib/asciidoctor-pdf/measurements.rb +0 -58
- data/lib/asciidoctor-pdf/pdf-core_ext/page.rb +0 -25
- data/lib/asciidoctor-pdf/pdf-core_ext/pdf_object.rb +0 -6
- data/lib/asciidoctor-pdf/pdf-core_ext.rb +0 -2
- data/lib/asciidoctor-pdf/pdfmark.rb +0 -33
- data/lib/asciidoctor-pdf/prawn-svg_ext/interface.rb +0 -10
- data/lib/asciidoctor-pdf/prawn-table_ext/cell/asciidoc.rb +0 -69
- data/lib/asciidoctor-pdf/prawn-table_ext.rb +0 -4
- data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +0 -115
- data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +0 -904
- data/lib/asciidoctor-pdf/prawn_ext/images.rb +0 -51
- data/lib/asciidoctor-pdf/prawn_ext.rb +0 -5
- data/lib/asciidoctor-pdf/roman_numeral.rb +0 -126
- data/lib/asciidoctor-pdf/rouge_ext/formatters/prawn.rb +0 -175
- data/lib/asciidoctor-pdf/rouge_ext/themes/bw.rb +0 -38
- data/lib/asciidoctor-pdf/rouge_ext.rb +0 -4
- data/lib/asciidoctor-pdf/sanitizer.rb +0 -101
- data/lib/asciidoctor-pdf/temporary_path.rb +0 -13
- data/lib/asciidoctor-pdf/theme_loader.rb +0 -280
- data/lib/asciidoctor-pdf/ttfunk_ext.rb +0 -8
@@ -1,10 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Prawn::Font::AFM
|
4
|
+
include ::Asciidoctor::Logging
|
5
|
+
|
2
6
|
FALLBACK_CHARS = {
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
7
|
+
?\u200b => '',
|
8
|
+
?\u202f => ?\u00a0,
|
9
|
+
?\u2009 => ' ',
|
10
|
+
?\u25e6 => '-',
|
11
|
+
?\u25aa => ?\u00b7,
|
8
12
|
}
|
9
13
|
|
10
14
|
remove_method :normalize_encoding
|
@@ -17,12 +21,12 @@ class Prawn::Font::AFM
|
|
17
21
|
def normalize_encoding text
|
18
22
|
text.encode 'windows-1252', fallback: FALLBACK_CHARS
|
19
23
|
rescue ::Encoding::UndefinedConversionError
|
20
|
-
warn
|
21
|
-
|
22
|
-
text.encode 'windows-1252', undef: :replace, replace:
|
24
|
+
logger.warn %(The following text could not be fully converted to the Windows-1252 character set:
|
25
|
+
#{text.gsub(/^/, '| ').rstrip}) if logger.info? && !@document.scratch?
|
26
|
+
text.encode 'windows-1252', undef: :replace, replace: ?\u00ac
|
23
27
|
rescue ::Encoding::InvalidByteSequenceError
|
24
28
|
raise Prawn::Errors::IncompatibleStringEncoding,
|
25
|
-
|
26
|
-
If you need full UTF-8 support, use TTF fonts instead of the built-in PDF (AFM) fonts.
|
29
|
+
'Your document includes text which is not compatible with the Windows-1252 character set.
|
30
|
+
If you need full UTF-8 support, use TTF fonts instead of the built-in PDF (AFM) fonts.'
|
27
31
|
end
|
28
32
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Prawn::Text::Formatted::Box.prepend (Module.new do
|
4
|
+
include ::Asciidoctor::Logging
|
5
|
+
|
6
|
+
def draw_fragment_overlay_styles fragment
|
7
|
+
if (underline = (styles = fragment.styles).include? :underline) || (styles.include? :strikethrough)
|
8
|
+
(doc = fragment.document).save_graphics_state do
|
9
|
+
if (text_decoration_width = (fs = fragment.format_state)[:text_decoration_width] || doc.text_decoration_width)
|
10
|
+
doc.line_width = text_decoration_width
|
11
|
+
end
|
12
|
+
if (text_decoration_color = fs[:text_decoration_color])
|
13
|
+
doc.stroke_color = text_decoration_color
|
14
|
+
end
|
15
|
+
underline ? (doc.stroke_line fragment.underline_points) : (doc.stroke_line fragment.strikethrough_points)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def find_font_for_this_glyph char, current_font, fallback_fonts_to_check, original_font = current_font
|
21
|
+
(doc = @document).font current_font
|
22
|
+
if fallback_fonts_to_check.empty?
|
23
|
+
if logger.info? && !doc.scratch?
|
24
|
+
fonts_checked = @fallback_fonts.dup.unshift original_font
|
25
|
+
missing_chars = (doc.instance_variable_defined? :@missing_chars) ?
|
26
|
+
(doc.instance_variable_get :@missing_chars) : (doc.instance_variable_set :@missing_chars, {})
|
27
|
+
previous_fonts_checked = (missing_chars[char] ||= [])
|
28
|
+
if previous_fonts_checked.empty? && !(previous_fonts_checked.include? fonts_checked)
|
29
|
+
logger.warn %(Could not locate the character `#{char}' in the following fonts: #{fonts_checked.join ', '})
|
30
|
+
previous_fonts_checked << fonts_checked
|
31
|
+
end
|
32
|
+
end
|
33
|
+
current_font
|
34
|
+
elsif doc.font.glyph_present? char
|
35
|
+
current_font
|
36
|
+
else
|
37
|
+
find_font_for_this_glyph char, fallback_fonts_to_check.shift, fallback_fonts_to_check, original_font
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def process_vertical_alignment text
|
42
|
+
return super if ::Symbol === (valign = @vertical_align)
|
43
|
+
|
44
|
+
return if defined? @vertical_alignment_processed
|
45
|
+
@vertical_alignment_processed = true
|
46
|
+
|
47
|
+
valign, offset = valign
|
48
|
+
|
49
|
+
if valign == :top
|
50
|
+
@at[1] -= offset
|
51
|
+
return
|
52
|
+
end
|
53
|
+
|
54
|
+
wrap text
|
55
|
+
h = height
|
56
|
+
|
57
|
+
case valign
|
58
|
+
when :center
|
59
|
+
@at[1] -= (@height - h + @descender) * 0.5 + offset
|
60
|
+
when :bottom
|
61
|
+
@at[1] -= (@height - h) + offset
|
62
|
+
end
|
63
|
+
|
64
|
+
@height = h
|
65
|
+
end
|
66
|
+
end)
|
@@ -1,7 +1,6 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
module Fragment
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Prawn::Text::Formatted::Fragment.prepend (Module.new do
|
5
4
|
attr_reader :document
|
6
5
|
|
7
6
|
# Prevent fragment from being written by discarding the text.
|
@@ -9,6 +8,12 @@ module Fragment
|
|
9
8
|
@text = ''
|
10
9
|
end
|
11
10
|
|
11
|
+
# Don't strip soft hyphens when repacking unretrieved fragments
|
12
|
+
def include_trailing_white_space!
|
13
|
+
@format_state.delete :normalized_soft_hyphen
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
12
17
|
# Modify the built-in ascender write method to allow an override value to be
|
13
18
|
# specified using the format_state hash.
|
14
19
|
def ascender= val
|
@@ -23,14 +28,13 @@ module Fragment
|
|
23
28
|
|
24
29
|
def width
|
25
30
|
if (val = format_state[:width])
|
26
|
-
(val.end_with? 'em') ? val.to_f * @document.font_size : val
|
31
|
+
val = (val.end_with? 'em') ? val.to_f * @document.font_size : (@document.str_to_pt val) if ::String === val
|
27
32
|
else
|
28
|
-
super
|
33
|
+
val = super
|
29
34
|
end
|
35
|
+
if (border_offset = format_state[:border_offset])
|
36
|
+
val += border_offset * 2
|
37
|
+
end
|
38
|
+
val
|
30
39
|
end
|
31
|
-
end
|
32
|
-
|
33
|
-
::Prawn::Text::Formatted::Fragment.prepend Fragment
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
40
|
+
end)
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Asciidoctor
|
4
|
+
module Prawn
|
5
|
+
module Images
|
6
|
+
class << self
|
7
|
+
def extended base
|
8
|
+
base.class.__send__ :alias_method, :_initial_image, :image
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# Dispatch to suitable image method in Prawn based on file extension.
|
13
|
+
def image file, opts = {}
|
14
|
+
# FIXME: handle case when SVG is an IO object
|
15
|
+
if ::String === file && (((opts = opts.dup).delete :format) == 'svg' || (file.downcase.end_with? '.svg'))
|
16
|
+
#opts[:enable_file_requests_with_root] = (::File.dirname file) unless opts.key? :enable_file_requests_with_root
|
17
|
+
#opts[:enable_web_requests] = allow_uri_read if !(opts.key? :enable_web_requests) && (respond_to? :allow_uri_read)
|
18
|
+
#opts[:cache_images] = cache_uri if !(opts.key? :cache_images) && (respond_to? :cache_uri)
|
19
|
+
#opts[:fallback_font_name] = fallback_svg_font_name if !(opts.key? :fallback_font_name) && (respond_to? :fallback_svg_font_name)
|
20
|
+
if (opts.key? :fit) && (fit = opts.delete :fit) && !opts[:width] && !opts[:height]
|
21
|
+
svg (::File.read file, mode: 'r:UTF-8'), opts do |svg_doc|
|
22
|
+
max_width, max_height = fit
|
23
|
+
svg_doc.calculate_sizing requested_width: max_width if max_width && svg_doc.sizing.output_width != max_width
|
24
|
+
svg_doc.calculate_sizing requested_height: max_height if max_height && svg_doc.sizing.output_height > max_height
|
25
|
+
end
|
26
|
+
else
|
27
|
+
svg (::File.read file, mode: 'r:UTF-8'), opts
|
28
|
+
end
|
29
|
+
else
|
30
|
+
_initial_image file, opts
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Retrieve the intrinsic image dimensions for the specified path.
|
35
|
+
#
|
36
|
+
# Returns a Hash containing :width and :height keys that map to the image's
|
37
|
+
# intrinsic width and height values (in pixels)
|
38
|
+
def intrinsic_image_dimensions path, format
|
39
|
+
if format == 'svg'
|
40
|
+
img_obj = ::Prawn::SVG::Interface.new (::File.read path, mode: 'r:UTF-8'), self, {}
|
41
|
+
img_size = img_obj.document.sizing
|
42
|
+
{ width: img_size.output_width, height: img_size.output_height }
|
43
|
+
else
|
44
|
+
# NOTE build_image_object caches image data previously loaded
|
45
|
+
_, img_size = ::File.open(path, 'rb') {|fd| build_image_object fd }
|
46
|
+
{ width: img_size.width, height: img_size.height }
|
47
|
+
end
|
48
|
+
rescue
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
::Prawn::Document.extensions << Images
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prawn
|
4
|
+
module SVG
|
5
|
+
class Interface
|
6
|
+
def resize opts = {}
|
7
|
+
sizing = document.sizing
|
8
|
+
sizing.requested_width = opts[:width]
|
9
|
+
sizing.requested_height = opts[:height]
|
10
|
+
sizing.calculate
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end unless Prawn::SVG::Interface.method_defined? :resize
|
@@ -1,4 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'prawn-svg' unless defined? Prawn::SVG::Interface
|
2
|
-
require_relative 'prawn-
|
4
|
+
require_relative 'prawn-svg/interface'
|
3
5
|
# NOTE disable system fonts since they're non-portable
|
4
6
|
Prawn::SVG::Interface.font_path.clear
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prawn
|
4
|
+
class Table
|
5
|
+
class Cell
|
6
|
+
class AsciiDoc < Cell
|
7
|
+
attr_accessor :align
|
8
|
+
attr_accessor :valign
|
9
|
+
|
10
|
+
def initialize pdf, opts = {}
|
11
|
+
@font_options = {}
|
12
|
+
super pdf, [], opts
|
13
|
+
end
|
14
|
+
|
15
|
+
def font_style= val
|
16
|
+
@font_options[:style] = val
|
17
|
+
end
|
18
|
+
|
19
|
+
def text_color= val
|
20
|
+
@font_options[:color] = val
|
21
|
+
end
|
22
|
+
|
23
|
+
def size= val
|
24
|
+
@font_options[:size] = val
|
25
|
+
end
|
26
|
+
|
27
|
+
def font= val
|
28
|
+
@font_options[:family] = val
|
29
|
+
end
|
30
|
+
|
31
|
+
# NOTE: automatic image sizing only works if cell has fixed width
|
32
|
+
def dry_run
|
33
|
+
cell = self
|
34
|
+
max_height = nil
|
35
|
+
height, = @pdf.dry_run do
|
36
|
+
max_height = bounds.height
|
37
|
+
# NOTE: we should be able to use cell.max_width, but returns 0 in some conditions (like when colspan > 1)
|
38
|
+
indent cell.padding_left, bounds.width - cell.width + cell.padding_right do
|
39
|
+
# HACK: force margin_top to be applied
|
40
|
+
move_down 0.0001
|
41
|
+
# TODO: truncate margin bottom of last block
|
42
|
+
traverse cell.content
|
43
|
+
end
|
44
|
+
end
|
45
|
+
# FIXME: prawn-table doesn't support cell taller than a single page
|
46
|
+
[max_height, height - 0.0001].min
|
47
|
+
end
|
48
|
+
|
49
|
+
def natural_content_width
|
50
|
+
# QUESTION can we get a better estimate of the natural width?
|
51
|
+
@natural_content_width ||= (@pdf.bounds.width - padding_left - padding_right)
|
52
|
+
end
|
53
|
+
|
54
|
+
def natural_content_height
|
55
|
+
# NOTE when natural_content_height is called, we already know max width
|
56
|
+
@natural_content_height ||= dry_run
|
57
|
+
end
|
58
|
+
|
59
|
+
def draw_content
|
60
|
+
pdf = @pdf
|
61
|
+
# NOTE draw_bounded_content automatically adds FPTolerance to width and height
|
62
|
+
pdf.bounds.instance_variable_set :@width, spanned_content_width
|
63
|
+
# NOTE we've already reserved the space, so just let the box stretch to the bottom of the page to avoid overflow
|
64
|
+
pdf.bounds.instance_variable_set :@height, pdf.y
|
65
|
+
if @valign != :top && (excess_y = spanned_content_height - natural_content_height) > 0
|
66
|
+
pdf.move_down(@valign == :center ? (excess_y.fdiv 2) : excess_y)
|
67
|
+
end
|
68
|
+
# TODO: apply horizontal alignment (right now must use alignment on content block)
|
69
|
+
# QUESTION inherit table cell font properties?
|
70
|
+
pdf.traverse content
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -1,12 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Prawn::Table::Cell::Text
|
2
4
|
# Override draw_content method to drop cursor advancement
|
3
5
|
remove_method :draw_content
|
4
6
|
def draw_content
|
5
7
|
with_font do
|
6
8
|
with_text_color do
|
7
|
-
(text_box
|
8
|
-
|
9
|
-
|
9
|
+
(text_box \
|
10
|
+
width: spanned_content_width + FPTolerance,
|
11
|
+
height: spanned_content_height + FPTolerance,
|
12
|
+
at: [0, @pdf.cursor]).render
|
10
13
|
end
|
11
14
|
end
|
12
15
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
class Prawn::Table::Cell
|
2
4
|
remove_method :draw_borders
|
3
5
|
# Draws borders around the cell. Borders are centered on the bounds of
|
@@ -5,10 +7,10 @@ class Prawn::Table::Cell
|
|
5
7
|
# setting appropriate padding to ensure the border does not overlap with
|
6
8
|
# cell content.
|
7
9
|
#
|
8
|
-
def draw_borders
|
10
|
+
def draw_borders pt
|
9
11
|
x, y = pt
|
10
12
|
|
11
|
-
@pdf.mask
|
13
|
+
@pdf.mask :line_width, :stroke_color do
|
12
14
|
@borders.each do |border|
|
13
15
|
idx = { top: 0, right: 1, bottom: 2, left: 3 }[border]
|
14
16
|
border_color = @border_colors[idx]
|
@@ -21,15 +23,13 @@ class Prawn::Table::Cell
|
|
21
23
|
# of the corner, so that the corners end up square.
|
22
24
|
from, to = case border
|
23
25
|
when :top
|
24
|
-
[[x, y], [x+width, y]]
|
26
|
+
[[x, y], [x + width, y]]
|
25
27
|
when :bottom
|
26
|
-
[[x, y-height], [x+width, y-height]]
|
28
|
+
[[x, y - height], [x + width, y - height]]
|
27
29
|
when :left
|
28
|
-
[[x, y + (border_top_width / 2.0)],
|
29
|
-
[x, y - height - (border_bottom_width / 2.0)]]
|
30
|
+
[[x, y + (border_top_width / 2.0)], [x, y - height - (border_bottom_width / 2.0)]]
|
30
31
|
when :right
|
31
|
-
[[x+width, y + (border_top_width / 2.0)],
|
32
|
-
[x+width, y - height - (border_bottom_width / 2.0)]]
|
32
|
+
[[x + width, y + (border_top_width / 2.0)], [x + width, y - height - (border_bottom_width / 2.0)]]
|
33
33
|
end
|
34
34
|
|
35
35
|
case border_line
|
@@ -47,11 +47,11 @@ class Prawn::Table::Cell
|
|
47
47
|
if border_color == 'transparent'
|
48
48
|
@pdf.stroke_color = '000000'
|
49
49
|
@pdf.transparent 0 do
|
50
|
-
@pdf.stroke_line
|
50
|
+
@pdf.stroke_line from, to
|
51
51
|
end
|
52
52
|
else
|
53
53
|
@pdf.stroke_color = border_color
|
54
|
-
@pdf.stroke_line
|
54
|
+
@pdf.stroke_line from, to
|
55
55
|
end
|
56
56
|
@pdf.undash
|
57
57
|
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# the following are organized under the Asciidoctor::Prawn namespace
|
4
|
+
require_relative 'prawn/font_metric_cache'
|
5
|
+
require_relative 'prawn/font/afm'
|
6
|
+
require_relative 'prawn/images'
|
7
|
+
require_relative 'prawn/formatted_text/box'
|
8
|
+
require_relative 'prawn/formatted_text/fragment'
|
9
|
+
require_relative 'prawn/extensions'
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pygments.rb'
|
4
|
+
|
5
|
+
module Pygments
|
6
|
+
module Ext
|
7
|
+
module BlockStyles
|
8
|
+
BlockSelectorRx = /^\.highlight *\{([^}]+?)\}/
|
9
|
+
HighlightBackgroundColorRx = /^\.highlight +\.hll +{ *background(?:-color)?: *#([a-fA-F0-9]{6})/
|
10
|
+
HexColorRx = /^#[a-fA-F0-9]{6}$/
|
11
|
+
|
12
|
+
@cache = ::Hash.new do |cache, key|
|
13
|
+
styles = {}
|
14
|
+
if BlockSelectorRx =~ (css = ::Pygments.css '.highlight', style: key)
|
15
|
+
($1.strip.split ';').each do |style|
|
16
|
+
pname, pval = (style.split ':', 2).map(&:strip)
|
17
|
+
if pname == 'background' || pname == 'background-color'
|
18
|
+
styles[:background_color] = pval.slice 1, pval.length if HexColorRx.match? pval
|
19
|
+
elsif pname == 'color'
|
20
|
+
styles[:font_color] = pval.slice 1, pval.length if HexColorRx.match? pval
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
styles[:highlight_background_color] = $1 if HighlightBackgroundColorRx =~ css
|
25
|
+
@cache = cache.merge key => styles
|
26
|
+
styles
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.for style
|
30
|
+
@cache[style]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rouge
|
4
|
+
module Formatters
|
5
|
+
# Transforms a token stream into an array of
|
6
|
+
# formatted text fragments for use with Prawn.
|
7
|
+
class Prawn < Formatter
|
8
|
+
tag 'prawn'
|
9
|
+
|
10
|
+
Tokens = ::Rouge::Token::Tokens
|
11
|
+
LineOrientedTokens = [
|
12
|
+
::Rouge::Token::Tokens::Generic::Inserted,
|
13
|
+
::Rouge::Token::Tokens::Generic::Deleted,
|
14
|
+
::Rouge::Token::Tokens::Generic::Heading,
|
15
|
+
::Rouge::Token::Tokens::Generic::Subheading,
|
16
|
+
]
|
17
|
+
|
18
|
+
LF = ?\n
|
19
|
+
DummyText = ?\u0000
|
20
|
+
NoBreakSpace = ?\u00a0
|
21
|
+
InnerIndent = %(#{LF} )
|
22
|
+
GuardedIndent = NoBreakSpace
|
23
|
+
GuardedInnerIndent = %(#{LF}#{NoBreakSpace})
|
24
|
+
BoldStyle = [:bold].to_set
|
25
|
+
ItalicStyle = [:italic].to_set
|
26
|
+
BoldItalicStyle = [:bold, :italic].to_set
|
27
|
+
UnderlineStyle = [:underline].to_set
|
28
|
+
|
29
|
+
def initialize opts = {}
|
30
|
+
unless ::Rouge::Theme === (theme = opts[:theme])
|
31
|
+
unless theme && (theme = ::Rouge::Theme.find theme)
|
32
|
+
theme = ::Rouge::Themes::AsciidoctorPDFDefault
|
33
|
+
end
|
34
|
+
theme = theme.new
|
35
|
+
end
|
36
|
+
@theme = theme
|
37
|
+
@normalized_colors = {}
|
38
|
+
@background_colorizer = BackgroundColorizer.new line_gap: opts[:line_gap]
|
39
|
+
@linenum_fragment_base = create_fragment Tokens::Generic::Lineno
|
40
|
+
@highlight_line_fragment = create_highlight_line_fragment opts[:highlight_background_color]
|
41
|
+
end
|
42
|
+
|
43
|
+
def background_color
|
44
|
+
@background_color ||= (normalize_color (@theme.style_for Tokens::Text).bg)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Override format method so fragments don't get flatted to a string
|
48
|
+
# and to add an options Hash.
|
49
|
+
def format tokens, opts = {}
|
50
|
+
stream tokens, opts
|
51
|
+
end
|
52
|
+
|
53
|
+
def stream tokens, opts = {}
|
54
|
+
line_numbers = opts[:line_numbers]
|
55
|
+
highlight_lines = opts[:highlight_lines]
|
56
|
+
if line_numbers || highlight_lines
|
57
|
+
linenum = (linenum = opts[:start_line] || 1) > 0 ? linenum : 1
|
58
|
+
fragments = []
|
59
|
+
line_numbers ? (fragments << (create_linenum_fragment linenum)) : (start_of_line = true)
|
60
|
+
fragments << @highlight_line_fragment.dup if highlight_lines && highlight_lines[linenum]
|
61
|
+
tokens.each do |tok, val|
|
62
|
+
if val == LF
|
63
|
+
fragments << { text: LF }
|
64
|
+
linenum += 1
|
65
|
+
line_numbers ? (fragments << (create_linenum_fragment linenum)) : (start_of_line = true)
|
66
|
+
fragments << @highlight_line_fragment.dup if highlight_lines && highlight_lines[linenum]
|
67
|
+
elsif val.include? LF
|
68
|
+
# NOTE we assume if the fragment ends in a line feed, the intention was to match a line-oriented form
|
69
|
+
line_oriented = val.end_with? LF
|
70
|
+
base_fragment = create_fragment tok, val
|
71
|
+
val.each_line do |line|
|
72
|
+
if start_of_line
|
73
|
+
line[0] = GuardedIndent if line.start_with? ' '
|
74
|
+
start_of_line = nil
|
75
|
+
end
|
76
|
+
fragments << (line_oriented ? (base_fragment.merge text: line, inline_block: true) : (base_fragment.merge text: line))
|
77
|
+
next unless line.end_with? LF
|
78
|
+
# NOTE eagerly append linenum fragment or line highlight if there's a next line
|
79
|
+
linenum += 1
|
80
|
+
line_numbers ? (fragments << (create_linenum_fragment linenum)) : (start_of_line = true)
|
81
|
+
fragments << @highlight_line_fragment.dup if highlight_lines && highlight_lines[linenum]
|
82
|
+
end
|
83
|
+
else
|
84
|
+
if start_of_line
|
85
|
+
val[0] = GuardedIndent if val.start_with? ' '
|
86
|
+
start_of_line = nil
|
87
|
+
end
|
88
|
+
fragments << (create_fragment tok, val)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
# NOTE pad numbers that have less digits than the largest line number
|
92
|
+
# FIXME we could store these fragments so we don't have find them again
|
93
|
+
if line_numbers && (linenum_w = linenum.to_s.length) > 1
|
94
|
+
# NOTE extra column is the trailing space after the line number
|
95
|
+
linenum_w += 1
|
96
|
+
fragments.each do |fragment|
|
97
|
+
fragment[:text] = (fragment[:text].rjust linenum_w, NoBreakSpace).to_s if fragment[:linenum]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
fragments
|
101
|
+
else
|
102
|
+
start_of_line = true
|
103
|
+
tokens.map do |tok, val|
|
104
|
+
# match one or more consecutive endlines
|
105
|
+
if val == LF || (val == (LF * val.length))
|
106
|
+
start_of_line = true
|
107
|
+
{ text: val }
|
108
|
+
else
|
109
|
+
val[0] = GuardedIndent if start_of_line && (val.start_with? ' ')
|
110
|
+
val.gsub! InnerIndent, GuardedInnerIndent if val.include? InnerIndent
|
111
|
+
# QUESTION do we need the call to create_fragment if val contains only spaces? consider bg
|
112
|
+
#fragment = create_fragment tok, val
|
113
|
+
fragment = val.rstrip.empty? ? { text: val } : (create_fragment tok, val)
|
114
|
+
# NOTE we assume if the fragment ends in a line feed, the intention was to match a line-oriented form
|
115
|
+
fragment[:inline_block] = true if (start_of_line = val.end_with? LF)
|
116
|
+
fragment
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# TODO: method could still be optimized (for instance, check if val is LF or empty)
|
123
|
+
def create_fragment tok, val = nil
|
124
|
+
fragment = val ? { text: val } : {}
|
125
|
+
if (style_rules = @theme.style_for tok)
|
126
|
+
if (bg = normalize_color style_rules.bg) && bg != background_color
|
127
|
+
fragment[:background_color] = bg
|
128
|
+
fragment[:callback] = @background_colorizer
|
129
|
+
if LineOrientedTokens.include? tok
|
130
|
+
fragment[:inline_block] = true unless style_rules[:inline_block] == false
|
131
|
+
fragment[:extend] = true unless style_rules[:extend] == false
|
132
|
+
else
|
133
|
+
fragment[:inline_block] = true if style_rules[:inline_block]
|
134
|
+
fragment[:extend] = true if style_rules[:extend]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
if (fg = normalize_color style_rules.fg)
|
138
|
+
fragment[:color] = fg
|
139
|
+
end
|
140
|
+
if style_rules[:bold]
|
141
|
+
fragment[:styles] = style_rules[:italic] ? BoldItalicStyle.dup : BoldStyle.dup
|
142
|
+
elsif style_rules[:italic]
|
143
|
+
fragment[:styles] = ItalicStyle.dup
|
144
|
+
end
|
145
|
+
if style_rules[:underline]
|
146
|
+
if fragment.key? :styles
|
147
|
+
fragment[:styles] << UnderlineStyle[0]
|
148
|
+
else
|
149
|
+
fragment[:styles] = UnderlineStyle.dup
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
fragment
|
154
|
+
end
|
155
|
+
|
156
|
+
def create_linenum_fragment linenum
|
157
|
+
@linenum_fragment_base.merge text: %(#{linenum} ), linenum: linenum
|
158
|
+
end
|
159
|
+
|
160
|
+
def create_highlight_line_fragment bg_color
|
161
|
+
{
|
162
|
+
background_color: (bg_color || 'FFFFCC'),
|
163
|
+
callback: @background_colorizer,
|
164
|
+
extend: true,
|
165
|
+
highlight: true,
|
166
|
+
inline_block: true,
|
167
|
+
text: DummyText,
|
168
|
+
width: 0,
|
169
|
+
}
|
170
|
+
end
|
171
|
+
|
172
|
+
def normalize_color raw
|
173
|
+
return unless raw
|
174
|
+
if (normalized = @normalized_colors[raw])
|
175
|
+
normalized
|
176
|
+
else
|
177
|
+
normalized = (raw.start_with? '#') ? (raw.slice 1, raw.length) : raw
|
178
|
+
normalized = normalized.each_char.map {|c| c * 2 }.join if normalized.length == 3
|
179
|
+
@normalized_colors[raw] = normalized
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
class BackgroundColorizer
|
184
|
+
def initialize opts = {}
|
185
|
+
@line_gap = opts[:line_gap] || 0
|
186
|
+
end
|
187
|
+
|
188
|
+
def render_behind fragment
|
189
|
+
pdf = fragment.document
|
190
|
+
data = fragment.format_state
|
191
|
+
prev_fill_color = pdf.fill_color
|
192
|
+
pdf.fill_color data[:background_color]
|
193
|
+
if data[:inline_block]
|
194
|
+
fragment_width = data[:extend] ? pdf.bounds.width - fragment.left : fragment.width
|
195
|
+
v_gap = @line_gap
|
196
|
+
else
|
197
|
+
fragment_width = fragment.width
|
198
|
+
v_gap = 0
|
199
|
+
end
|
200
|
+
pdf.fill_rectangle [fragment.left, fragment.top + v_gap * 0.5], fragment_width, (fragment.height + v_gap)
|
201
|
+
pdf.fill_color prev_fill_color
|
202
|
+
fragment.conceal if fragment.text == DummyText
|
203
|
+
nil
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|