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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +9 -1
  3. data/Rakefile +1 -0
  4. data/data/fonts/{LICENSE-noto-fonts-2014-11-17 → LICENSE-noto-2015-06-05} +0 -0
  5. data/data/fonts/{mplus1mn-bolditalic-ascii.ttf → mplus1mn-bold_italic-ascii.ttf} +0 -0
  6. data/data/fonts/{mplus1p-regular-multilingual.ttf → mplus1p-regular-fallback.ttf} +0 -0
  7. data/data/fonts/notoserif-bold-subset.ttf +0 -0
  8. data/data/fonts/notoserif-bold_italic-subset.ttf +0 -0
  9. data/data/fonts/notoserif-italic-subset.ttf +0 -0
  10. data/data/fonts/notoserif-regular-subset.ttf +0 -0
  11. data/data/themes/base-theme.yml +90 -0
  12. data/data/themes/default-theme.yml +97 -92
  13. data/docs/theming-guide.adoc +93 -21
  14. data/lib/asciidoctor-pdf/converter.rb +419 -216
  15. data/lib/asciidoctor-pdf/core_ext/quantifiable_stdout.rb +20 -0
  16. data/lib/asciidoctor-pdf/formatted_text/formatter.rb +6 -5
  17. data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +3 -3
  18. data/lib/asciidoctor-pdf/formatted_text/transform.rb +36 -26
  19. data/lib/asciidoctor-pdf/pdf_core_ext.rb +1 -1
  20. data/lib/asciidoctor-pdf/pdf_core_ext/page.rb +22 -0
  21. data/lib/asciidoctor-pdf/prawn_ext.rb +1 -0
  22. data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +24 -4
  23. data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +73 -6
  24. data/lib/asciidoctor-pdf/prawn_ext/font/afm.rb +19 -0
  25. data/lib/asciidoctor-pdf/prawn_ext/formatted_text/fragment.rb +1 -1
  26. data/lib/asciidoctor-pdf/prawn_ext/images.rb +16 -0
  27. data/lib/asciidoctor-pdf/rouge_ext.rb +4 -0
  28. data/lib/asciidoctor-pdf/rouge_ext/css_theme.rb +14 -0
  29. data/lib/asciidoctor-pdf/rouge_ext/formatters/prawn.rb +121 -0
  30. data/lib/asciidoctor-pdf/rouge_ext/themes/pastie.rb +61 -0
  31. data/lib/asciidoctor-pdf/theme_loader.rb +32 -14
  32. data/lib/asciidoctor-pdf/version.rb +1 -1
  33. metadata +23 -15
  34. data/data/fonts/notoserif-bold-latin.ttf +0 -0
  35. data/data/fonts/notoserif-bolditalic-latin.ttf +0 -0
  36. data/data/fonts/notoserif-italic-latin.ttf +0 -0
  37. 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.first || {}
14
- string = string.tr_s("\n", ' ') if options[:normalize]
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)) == nil
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 = [0x00a0].pack 'U*'
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 ::RUBY_MIN_VERSION_2
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 ::RUBY_MIN_VERSION_2
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 = [0x200b].pack 'U*'
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 {|node|
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 {|fragment|
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
- # NOTE AFM fonts do not include a thin space glyph; set fallback_fonts to allow glyph to be resolved
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 = nil, attrs = {})
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
- fragment[:local] = value
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
- inline_styles.rstrip.chomp(';').split(';').each do |style|
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
- fragment[:color] = pvalue.tr(' #', '') unless fragment[:color]
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.lstrip == 'bold'
223
+ if pvalue == 'bold'
206
224
  styles << :bold
207
225
  end
208
226
  when 'font-style'
209
- if pvalue.lstrip == 'italic'
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
- # QUESTION should we remove styles if empty? Need test
225
- #fragment.delete(:styles) if styles.empty?
235
+ fragment.delete(:styles) if styles.empty?
226
236
  fragment
227
237
  end
228
238
  end
@@ -1,2 +1,2 @@
1
- # the following modules / classes are organized under the Asciidoctor::PdfCore namespace
2
1
  require_relative 'pdf_core_ext/pdf_object'
2
+ require_relative 'pdf_core_ext/page'
@@ -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
@@ -1,4 +1,5 @@
1
1
  # the following are organized under the Asciidoctor::Prawn namespace
2
+ require_relative 'prawn_ext/font/afm'
2
3
  require_relative 'prawn_ext/images'
3
4
  require_relative 'prawn_ext/formatted_text/fragment'
4
5
  require_relative 'prawn_ext/extensions'
@@ -68,18 +68,38 @@ class CodeRayEncoder < ::CodeRay::Encoders::Encoder
68
68
  value: '336600'
69
69
  }
70
70
 
71
- EOL = "\n"
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
- color = COLORS[kind] || COLORS[@open.last] || COLORS[:default]
81
-
82
- @out << (text == EOL ? { :text => text } : { :text => text, :color => color })
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
- prev_page_number = page_number
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
- go_to_page prev_page_number + 1
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 = default_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