asciidoctor-pdf 1.5.0.alpha.8 → 1.5.0.alpha.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.adoc +9 -1
- data/Rakefile +1 -0
- data/data/fonts/{LICENSE-noto-fonts-2014-11-17 → LICENSE-noto-2015-06-05} +0 -0
- data/data/fonts/{mplus1mn-bolditalic-ascii.ttf → mplus1mn-bold_italic-ascii.ttf} +0 -0
- data/data/fonts/{mplus1p-regular-multilingual.ttf → mplus1p-regular-fallback.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 +90 -0
- data/data/themes/default-theme.yml +97 -92
- data/docs/theming-guide.adoc +93 -21
- data/lib/asciidoctor-pdf/converter.rb +419 -216
- data/lib/asciidoctor-pdf/core_ext/quantifiable_stdout.rb +20 -0
- data/lib/asciidoctor-pdf/formatted_text/formatter.rb +6 -5
- data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +3 -3
- data/lib/asciidoctor-pdf/formatted_text/transform.rb +36 -26
- data/lib/asciidoctor-pdf/pdf_core_ext.rb +1 -1
- data/lib/asciidoctor-pdf/pdf_core_ext/page.rb +22 -0
- data/lib/asciidoctor-pdf/prawn_ext.rb +1 -0
- data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +24 -4
- data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +73 -6
- data/lib/asciidoctor-pdf/prawn_ext/font/afm.rb +19 -0
- data/lib/asciidoctor-pdf/prawn_ext/formatted_text/fragment.rb +1 -1
- data/lib/asciidoctor-pdf/prawn_ext/images.rb +16 -0
- data/lib/asciidoctor-pdf/rouge_ext.rb +4 -0
- data/lib/asciidoctor-pdf/rouge_ext/css_theme.rb +14 -0
- data/lib/asciidoctor-pdf/rouge_ext/formatters/prawn.rb +121 -0
- data/lib/asciidoctor-pdf/rouge_ext/themes/pastie.rb +61 -0
- data/lib/asciidoctor-pdf/theme_loader.rb +32 -14
- data/lib/asciidoctor-pdf/version.rb +1 -1
- metadata +23 -15
- data/data/fonts/notoserif-bold-latin.ttf +0 -0
- data/data/fonts/notoserif-bolditalic-latin.ttf +0 -0
- data/data/fonts/notoserif-italic-latin.ttf +0 -0
- data/data/fonts/notoserif-regular-latin.ttf +0 -0
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
# A delegator that allows the size method to be used on the STDOUT object.
|
4
|
+
#
|
5
|
+
# The size of the content written to STDOUT cannot be measured normally. This
|
6
|
+
# class wraps the STDOUT object so the cumulative size of the content passed to
|
7
|
+
# the write method (while wrapped in this decorator) can be measured.
|
8
|
+
class QuantifiableStdout < SimpleDelegator
|
9
|
+
attr_reader :size
|
10
|
+
|
11
|
+
def initialize delegate
|
12
|
+
@size = 0
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def write content
|
17
|
+
@size += content.bytesize
|
18
|
+
super
|
19
|
+
end
|
20
|
+
end
|
@@ -3,6 +3,7 @@ module Pdf
|
|
3
3
|
module FormattedText
|
4
4
|
class Formatter
|
5
5
|
FormattingSnifferPattern = /[<&]/
|
6
|
+
EOL = "\n"
|
6
7
|
|
7
8
|
def initialize options = {}
|
8
9
|
@parser = MarkupParser.new
|
@@ -10,14 +11,14 @@ class Formatter
|
|
10
11
|
end
|
11
12
|
|
12
13
|
def format string, *args
|
13
|
-
options = args
|
14
|
-
string = string.tr_s(
|
14
|
+
options = args[0] || {}
|
15
|
+
string = string.tr_s(EOL, ' ') if options[:normalize]
|
15
16
|
return [text: string] unless string.match(FormattingSnifferPattern)
|
16
|
-
if (parsed = @parser.parse(string))
|
17
|
+
if (parsed = @parser.parse(string))
|
18
|
+
@transform.apply(parsed.content)
|
19
|
+
else
|
17
20
|
warn %(Failed to parse formatted text: #{string})
|
18
21
|
[text: string]
|
19
|
-
else
|
20
|
-
@transform.apply(parsed.content)
|
21
22
|
end
|
22
23
|
end
|
23
24
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Asciidoctor::Pdf::FormattedText
|
2
2
|
module InlineImageArranger
|
3
|
-
#ImagePlaceholderChar =
|
3
|
+
#ImagePlaceholderChar = %(\u00a0)
|
4
4
|
ImagePlaceholderChar = '.'
|
5
5
|
begin
|
6
6
|
require 'thread_safe' unless defined? ::ThreadSafe
|
@@ -9,7 +9,7 @@ module InlineImageArranger
|
|
9
9
|
PlaceholderWidthCache = {}
|
10
10
|
end
|
11
11
|
|
12
|
-
if
|
12
|
+
if respond_to? :prepend
|
13
13
|
def wrap fragments
|
14
14
|
arrange_images fragments
|
15
15
|
super
|
@@ -115,7 +115,7 @@ module InlineImageArranger
|
|
115
115
|
end
|
116
116
|
end
|
117
117
|
|
118
|
-
if
|
118
|
+
if respond_to? :prepend
|
119
119
|
class ::Prawn::Text::Formatted::Box
|
120
120
|
prepend InlineImageArranger
|
121
121
|
end
|
@@ -10,7 +10,7 @@ class Transform
|
|
10
10
|
:quot => '"',
|
11
11
|
:apos => '\''
|
12
12
|
}
|
13
|
-
#ZeroWidthSpace =
|
13
|
+
#ZeroWidthSpace = %(\u200b)
|
14
14
|
|
15
15
|
def initialize(options = {})
|
16
16
|
@merge_adjacent_text_nodes = options[:merge_adjacent_text_nodes]
|
@@ -19,12 +19,23 @@ class Transform
|
|
19
19
|
@monospaced_font_color = theme.literal_font_color
|
20
20
|
@monospaced_font_family = theme.literal_font_family
|
21
21
|
@monospaced_font_size = theme.literal_font_size
|
22
|
+
case theme.literal_font_style
|
23
|
+
when 'bold'
|
24
|
+
@monospaced_font_style = [:bold]
|
25
|
+
when 'italic'
|
26
|
+
@monospaced_font_style = [:italic]
|
27
|
+
when 'bold_italic'
|
28
|
+
@monospaced_font_style = [:bold, :italic]
|
29
|
+
else
|
30
|
+
@monospaced_font_style = nil
|
31
|
+
end
|
22
32
|
#@monospaced_letter_spacing = theme.literal_letter_spacing
|
23
33
|
else
|
24
34
|
@link_font_color = '0000FF'
|
25
35
|
@monospaced_font_color = nil
|
26
36
|
@monospaced_font_family = 'Courier'
|
27
37
|
@monospaced_font_size = 0.9
|
38
|
+
@monospaced_font_style = nil
|
28
39
|
#@monospaced_letter_spacing = -0.1
|
29
40
|
end
|
30
41
|
end
|
@@ -34,7 +45,7 @@ class Transform
|
|
34
45
|
fragments = []
|
35
46
|
previous_fragment_is_text = false
|
36
47
|
# NOTE we use each since using inject is slower than a manual loop
|
37
|
-
parsed.each
|
48
|
+
parsed.each do |node|
|
38
49
|
case node[:type]
|
39
50
|
when :element
|
40
51
|
# case 1: non-void element
|
@@ -42,10 +53,10 @@ class Transform
|
|
42
53
|
if pcdata.size > 0
|
43
54
|
tag_name = node[:name]
|
44
55
|
attributes = node[:attributes]
|
45
|
-
fragments << apply(pcdata).map
|
56
|
+
fragments << apply(pcdata).map do |fragment|
|
46
57
|
# decorate child fragments with styles from this element
|
47
58
|
build_fragment(fragment, tag_name, attributes)
|
48
|
-
|
59
|
+
end
|
49
60
|
previous_fragment_is_text = false
|
50
61
|
# NOTE skip element if it has no children
|
51
62
|
#else
|
@@ -94,7 +105,7 @@ class Transform
|
|
94
105
|
if (name = node[:name])
|
95
106
|
text = NamedEntityTable[name]
|
96
107
|
else
|
97
|
-
#
|
108
|
+
# FIXME AFM fonts do not include a thin space glyph; set fallback_fonts to allow glyph to be resolved
|
98
109
|
text = [node[:number]].pack('U*')
|
99
110
|
end
|
100
111
|
# NOTE the remaining logic is shared with :text
|
@@ -105,13 +116,11 @@ class Transform
|
|
105
116
|
end
|
106
117
|
previous_fragment_is_text = true
|
107
118
|
end
|
108
|
-
|
119
|
+
end
|
109
120
|
fragments.flatten
|
110
121
|
end
|
111
122
|
|
112
|
-
def build_fragment(fragment, tag_name
|
113
|
-
# QUESTION should we short-circuit if tag_name is nil?
|
114
|
-
#return { text: fragment } unless tag_name
|
123
|
+
def build_fragment(fragment, tag_name, attrs = {})
|
115
124
|
styles = (fragment[:styles] ||= ::Set.new)
|
116
125
|
case tag_name
|
117
126
|
when :strong
|
@@ -129,6 +138,9 @@ class Transform
|
|
129
138
|
if @monospaced_font_color
|
130
139
|
fragment[:color] ||= @monospaced_font_color
|
131
140
|
end
|
141
|
+
if @monospaced_font_style
|
142
|
+
styles.merge @monospaced_font_style
|
143
|
+
end
|
132
144
|
when :color
|
133
145
|
if !fragment[:color]
|
134
146
|
if (rgb = attrs[:rgb])
|
@@ -177,9 +189,9 @@ class Transform
|
|
177
189
|
if !fragment[:link] && (value = attrs[:href])
|
178
190
|
fragment[:link] = value
|
179
191
|
end
|
180
|
-
if !fragment[:local] && (value = attrs[:local])
|
181
|
-
|
182
|
-
end
|
192
|
+
#if !fragment[:local] && (value = attrs[:local])
|
193
|
+
# fragment[:local] = value
|
194
|
+
#end
|
183
195
|
if !fragment[:name] && (value = attrs[:name])
|
184
196
|
fragment[:name] = value
|
185
197
|
fragment[:callback] = InlineDestinationMarker
|
@@ -196,33 +208,31 @@ class Transform
|
|
196
208
|
when :span
|
197
209
|
# span logic with normal style parsing
|
198
210
|
if (inline_styles = attrs[:style])
|
199
|
-
|
211
|
+
# NOTE for our purposes, spaces inside the style attribute are superfluous
|
212
|
+
# NOTE split will ignore record after trailing ;
|
213
|
+
inline_styles.tr(' ', '').split(';').each do |style|
|
200
214
|
pname, pvalue = style.split(':', 2)
|
201
215
|
case pname
|
202
216
|
when 'color'
|
203
|
-
|
217
|
+
unless fragment[:color]
|
218
|
+
pvalue = pvalue[1..-1] if pvalue.start_with? '#'
|
219
|
+
#pvalue = pvalue.each_char.map {|c| c * 2 }.join if pvalue.size == 3
|
220
|
+
fragment[:color] = pvalue
|
221
|
+
end
|
204
222
|
when 'font-weight'
|
205
|
-
if pvalue
|
223
|
+
if pvalue == 'bold'
|
206
224
|
styles << :bold
|
207
225
|
end
|
208
226
|
when 'font-style'
|
209
|
-
if pvalue
|
227
|
+
if pvalue == 'italic'
|
210
228
|
styles << :italic
|
211
229
|
end
|
230
|
+
# TODO text-transform
|
212
231
|
end
|
213
232
|
end
|
214
233
|
end
|
215
|
-
|
216
|
-
# quicker span logic that only honors font color
|
217
|
-
#if !fragment[:color] && (value = attrs[:style]) && value.start_with?('color:')
|
218
|
-
# if value.include?(';')
|
219
|
-
# value = value.split(';').first
|
220
|
-
# end
|
221
|
-
# fragment[:color] = value[6..-1].tr(' #', '')
|
222
|
-
#end
|
223
234
|
end
|
224
|
-
|
225
|
-
#fragment.delete(:styles) if styles.empty?
|
235
|
+
fragment.delete(:styles) if styles.empty?
|
226
236
|
fragment
|
227
237
|
end
|
228
238
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module PDF
|
2
|
+
module Core
|
3
|
+
class Page
|
4
|
+
# Restore the new_content_stream method from PDF::Core::Page
|
5
|
+
#
|
6
|
+
# The prawn-templates gem relies on the new_content_stream method on
|
7
|
+
# PDF::Core::Page, which was removed in pdf-core 0.3.1. prawn-templates is
|
8
|
+
# used for importing a single-page PDF into the current document.
|
9
|
+
#
|
10
|
+
# see https://github.com/prawnpdf/pdf-core/commit/67f9a08a03bcfcc5a24cf76b135c218d3d3ab05d
|
11
|
+
def new_content_stream
|
12
|
+
return if in_stamp_stream?
|
13
|
+
unless ::Array === dictionary.data[:Contents]
|
14
|
+
dictionary.data[:Contents] = [content]
|
15
|
+
end
|
16
|
+
@content = document.ref({})
|
17
|
+
dictionary.data[:Contents] << document.state.store[@content]
|
18
|
+
document.renderer.open_graphics_state
|
19
|
+
end unless respond_to? :new_content_stream
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -68,18 +68,38 @@ class CodeRayEncoder < ::CodeRay::Encoders::Encoder
|
|
68
68
|
value: '336600'
|
69
69
|
}
|
70
70
|
|
71
|
-
EOL =
|
71
|
+
EOL = %(\n)
|
72
|
+
NoBreakSpace = %(\u00a0)
|
73
|
+
InnerIndent = %(\n )
|
74
|
+
GuardedIndent = %(\u00a0)
|
75
|
+
GuardedInnerIndent = %(\n\u00a0)
|
72
76
|
|
73
77
|
def setup options
|
74
78
|
super
|
75
79
|
@out = []
|
76
80
|
@open = []
|
81
|
+
# NOTE tracks whether text token begins at the start of a line
|
82
|
+
@start_of_line = true
|
77
83
|
end
|
78
84
|
|
79
85
|
def text_token text, kind
|
80
|
-
|
81
|
-
|
82
|
-
|
86
|
+
if text == EOL
|
87
|
+
@out << { text: text }
|
88
|
+
@start_of_line = true
|
89
|
+
else
|
90
|
+
# NOTE add guard character to prevent Prawn from trimming indentation
|
91
|
+
text[0] = GuardedIndent if @start_of_line && (text.start_with? ' ')
|
92
|
+
text.gsub! InnerIndent, GuardedInnerIndent if text.include? InnerIndent
|
93
|
+
|
94
|
+
# NOTE this optimization assumes we don't support/use background colors
|
95
|
+
if text.rstrip.empty?
|
96
|
+
@out << { text: text }
|
97
|
+
else
|
98
|
+
# QUESTION should we default to no color?
|
99
|
+
@out << { text: text, color: (COLORS[kind] || COLORS[@open.last] || COLORS[:default]) }
|
100
|
+
end
|
101
|
+
@start_of_line = text.end_with? EOL
|
102
|
+
end
|
83
103
|
end
|
84
104
|
|
85
105
|
def begin_group kind
|
@@ -1,3 +1,5 @@
|
|
1
|
+
Prawn::Font::AFM.instance_variable_set :@hide_m17n_warning, true
|
2
|
+
|
1
3
|
module Asciidoctor
|
2
4
|
module Prawn
|
3
5
|
module Extensions
|
@@ -82,6 +84,23 @@ module Extensions
|
|
82
84
|
@y == @margin_box.absolute_top
|
83
85
|
end
|
84
86
|
|
87
|
+
# Returns whether the current page is empty (no content is written).
|
88
|
+
# If at least one page has not yet been created, returns false.
|
89
|
+
#
|
90
|
+
def empty_page?
|
91
|
+
# if we are at the page top, assume we didn't write anything to the page
|
92
|
+
#at_page_top?
|
93
|
+
# ...or use low-level check (initial value is "q\n")
|
94
|
+
(page.content.stream || []).length <= 2 && page_number > 0
|
95
|
+
end
|
96
|
+
alias :page_is_empty? :empty_page?
|
97
|
+
|
98
|
+
# Returns whether the current page is the last page in the document.
|
99
|
+
#
|
100
|
+
def last_page?
|
101
|
+
page_number == page_count
|
102
|
+
end
|
103
|
+
|
85
104
|
# Converts the specified float value to a pt value from the
|
86
105
|
# specified unit of measurement (e.g., in, cm, mm, etc).
|
87
106
|
def to_pt num, units
|
@@ -562,13 +581,45 @@ module Extensions
|
|
562
581
|
|
563
582
|
# Pages
|
564
583
|
|
584
|
+
# Deletes the current page and move the cursor
|
585
|
+
# to the previous page.
|
586
|
+
def delete_page
|
587
|
+
pg = page_number
|
588
|
+
pdf_store = state.store
|
589
|
+
pdf_objs = pdf_store.instance_variable_get :@objects
|
590
|
+
pdf_ids = pdf_store.instance_variable_get :@identifiers
|
591
|
+
page_id = pdf_store.object_id_for_page pg
|
592
|
+
content_id = page.content.identifier
|
593
|
+
[page_id, content_id].each do |key|
|
594
|
+
pdf_objs.delete key
|
595
|
+
pdf_ids.delete key
|
596
|
+
end
|
597
|
+
pdf_store.pages.data[:Kids].pop
|
598
|
+
pdf_store.pages.data[:Count] -= 1
|
599
|
+
state.pages.pop
|
600
|
+
go_to_page(pg - 1)
|
601
|
+
end
|
602
|
+
|
565
603
|
# Import the specified page into the current document.
|
566
604
|
#
|
567
605
|
def import_page file
|
568
|
-
|
606
|
+
prev_page_layout = page.layout
|
607
|
+
prev_page_size = page.size
|
569
608
|
state.compress = false if state.compress # can't use compression if using template
|
609
|
+
prev_text_rendering_mode = @text_rendering_mode
|
610
|
+
# NOTE use functionality provided by prawn-templates
|
570
611
|
start_new_page_discretely template: file
|
571
|
-
|
612
|
+
# prawn-templates sets text_rendering_mode to :unknown, which breaks running content; revert
|
613
|
+
@text_rendering_mode = prev_text_rendering_mode
|
614
|
+
if last_page?
|
615
|
+
# NOTE set page size & layout explicitly in case imported page differs
|
616
|
+
# I'm not sure it's right to start a new page here, but unfortunately there's no other
|
617
|
+
# way atm to prevent the size & layout of the imported page from affecting subsequent pages
|
618
|
+
start_new_page size: prev_page_size, layout: prev_page_layout
|
619
|
+
else
|
620
|
+
go_to_page page_number + 1
|
621
|
+
end
|
622
|
+
nil
|
572
623
|
end
|
573
624
|
|
574
625
|
# Create a new page for the specified image. If the
|
@@ -583,21 +634,33 @@ module Extensions
|
|
583
634
|
else
|
584
635
|
image file, fit: [bounds.width, bounds.height]
|
585
636
|
end
|
637
|
+
# FIXME shouldn't this be `go_to_page prev_page_number + 1`?
|
586
638
|
go_to_page page_count
|
639
|
+
nil
|
587
640
|
end
|
588
641
|
|
589
642
|
# Perform an operation (such as creating a new page) without triggering the on_page_create callback
|
590
643
|
#
|
591
644
|
def perform_discretely
|
592
645
|
if (saved_callback = state.on_page_create_callback)
|
646
|
+
# equivalent to calling `on_page_create`
|
593
647
|
state.on_page_create_callback = nil
|
594
648
|
yield
|
649
|
+
# equivalent to calling `on_page_create &saved_callback`
|
595
650
|
state.on_page_create_callback = saved_callback
|
596
651
|
else
|
597
652
|
yield
|
598
653
|
end
|
599
654
|
end
|
600
655
|
|
656
|
+
#def advance_or_start_new_page options = {}
|
657
|
+
# if last_page?
|
658
|
+
# start_new_page options
|
659
|
+
# else
|
660
|
+
# go_to_page page_number + 1
|
661
|
+
# end
|
662
|
+
#end
|
663
|
+
|
601
664
|
# Start a new page without triggering the on_page_create callback
|
602
665
|
#
|
603
666
|
def start_new_page_discretely options = {}
|
@@ -646,14 +709,18 @@ module Extensions
|
|
646
709
|
scratch = get_scratch_document
|
647
710
|
scratch.start_new_page
|
648
711
|
start_page_number = scratch.page_number
|
649
|
-
# QUESTION is it enough to just set the padding or do we need to clone the bounds?
|
650
|
-
default_bounds = scratch.bounds
|
651
|
-
scratch.bounds = bounds.deep_copy.tap {|b| b.instance_variable_set :@document, scratch }
|
652
712
|
start_y = scratch.y
|
713
|
+
if (left_padding = bounds.total_left_padding) > 0
|
714
|
+
scratch.bounds.add_left_padding left_padding
|
715
|
+
end
|
716
|
+
if (right_padding = bounds.total_right_padding) > 0
|
717
|
+
scratch.bounds.add_right_padding right_padding
|
718
|
+
end
|
653
719
|
scratch.font font_family, style: font_style, size: font_size do
|
654
720
|
scratch.instance_exec(&block)
|
655
721
|
end
|
656
|
-
scratch.bounds
|
722
|
+
scratch.bounds.subtract_left_padding left_padding if left_padding > 0
|
723
|
+
scratch.bounds.subtract_right_padding right_padding if right_padding > 0
|
657
724
|
whole_pages = scratch.page_number - start_page_number
|
658
725
|
[(whole_pages * bounds.height + (start_y - scratch.y)), whole_pages, (start_y - scratch.y)]
|
659
726
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class Prawn::Font::AFM
|
2
|
+
# Patch normalize_encoding method to handle conversion more gracefully.
|
3
|
+
#
|
4
|
+
# Any valid utf-8 characters that cannot be encoded to windows-1252 are
|
5
|
+
# replaced with the logic "not" symbol and a warning is issued identifying
|
6
|
+
# the text that cannot be converted.
|
7
|
+
def normalize_encoding text
|
8
|
+
text.encode 'windows-1252'
|
9
|
+
rescue ::Encoding::UndefinedConversionError
|
10
|
+
warn 'The following text could not be fully converted to the Windows-1252 character set:'
|
11
|
+
warn %(#{text.gsub(/^/, '| ').rstrip})
|
12
|
+
warn ''
|
13
|
+
text.encode 'windows-1252', undef: :replace, replace: "\u00ac"
|
14
|
+
rescue ::Encoding::InvalidByteSequenceError
|
15
|
+
raise Prawn::Errors::IncompatibleStringEncoding,
|
16
|
+
%(Your document includes text that's not compatible with the Windows-1252 character set.
|
17
|
+
If you need full UTF-8 support, use TTF fonts instead of PDF's built-in (AFM) fonts\n.)
|
18
|
+
end
|
19
|
+
end
|