asciidoctor-pdf 1.5.0.alpha.12 → 1.5.0.alpha.13

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +325 -0
  3. data/Gemfile +1 -1
  4. data/README.adoc +97 -43
  5. data/asciidoctor-pdf.gemspec +7 -6
  6. data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
  7. data/data/fonts/mplus1mn-bold_italic-ascii.ttf +0 -0
  8. data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
  9. data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
  10. data/data/fonts/notoserif-bold-subset.ttf +0 -0
  11. data/data/fonts/notoserif-bold_italic-subset.ttf +0 -0
  12. data/data/fonts/notoserif-italic-subset.ttf +0 -0
  13. data/data/fonts/notoserif-regular-subset.ttf +0 -0
  14. data/data/themes/default-theme.yml +22 -13
  15. data/docs/theming-guide.adoc +276 -110
  16. data/lib/asciidoctor-pdf/asciidoctor_ext/image.rb +7 -7
  17. data/lib/asciidoctor-pdf/asciidoctor_ext/list.rb +24 -1
  18. data/lib/asciidoctor-pdf/asciidoctor_ext/list_item.rb +3 -3
  19. data/lib/asciidoctor-pdf/asciidoctor_ext/section.rb +11 -3
  20. data/lib/asciidoctor-pdf/converter.rb +614 -401
  21. data/lib/asciidoctor-pdf/core_ext.rb +1 -0
  22. data/lib/asciidoctor-pdf/core_ext/array.rb +2 -2
  23. data/lib/asciidoctor-pdf/core_ext/numeric.rb +1 -1
  24. data/lib/asciidoctor-pdf/core_ext/ostruct.rb +10 -2
  25. data/lib/asciidoctor-pdf/core_ext/string.rb +11 -0
  26. data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +2 -2
  27. data/lib/asciidoctor-pdf/formatted_text/inline_image_renderer.rb +5 -5
  28. data/lib/asciidoctor-pdf/formatted_text/transform.rb +6 -4
  29. data/lib/asciidoctor-pdf/implicit_header_processor.rb +1 -1
  30. data/lib/asciidoctor-pdf/pdf_core_ext/page.rb +1 -1
  31. data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +3 -3
  32. data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +77 -28
  33. data/lib/asciidoctor-pdf/prawn_ext/font/afm.rb +5 -4
  34. data/lib/asciidoctor-pdf/prawn_ext/images.rb +1 -1
  35. data/lib/asciidoctor-pdf/roman_numeral.rb +11 -4
  36. data/lib/asciidoctor-pdf/rouge_ext/formatters/prawn.rb +9 -9
  37. data/lib/asciidoctor-pdf/theme_loader.rb +16 -3
  38. data/lib/asciidoctor-pdf/version.rb +1 -1
  39. metadata +18 -9
@@ -1,2 +1,3 @@
1
1
  require_relative 'core_ext/array'
2
2
  require_relative 'core_ext/numeric'
3
+ require_relative 'core_ext/string'
@@ -2,10 +2,10 @@ class Array
2
2
  if RUBY_VERSION < '2.1.0'
3
3
  def to_h
4
4
  Hash[to_a]
5
- end unless respond_to? :to_h
5
+ end unless method_defined? :to_h
6
6
  end
7
7
 
8
8
  def delete_all *entries
9
9
  entries.map {|entry| delete entry }.compact
10
- end unless respond_to? :delete_all
10
+ end unless method_defined? :delete_all
11
11
  end
@@ -7,5 +7,5 @@ class Numeric
7
7
  else
8
8
  self.truncate
9
9
  end
10
- end unless respond_to? :with_precision
10
+ end unless method_defined? :with_precision
11
11
  end
@@ -1,9 +1,17 @@
1
1
  class OpenStruct
2
2
  def [] key
3
3
  send key
4
- end unless respond_to? :[]
4
+ end unless method_defined? :[]
5
5
 
6
6
  def []= key, val
7
7
  send %(#{key}=), val
8
- end unless respond_to? :[]=
8
+ end unless method_defined? :[]=
9
9
  end if RUBY_ENGINE == 'rbx' || RUBY_VERSION < '2.0.0'
10
+
11
+ class OpenStruct
12
+ def delete key
13
+ begin
14
+ delete_field key
15
+ rescue ::NameError; end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ class String
2
+ def pred
3
+ begin
4
+ # integers
5
+ %(#{(Integer self) - 1})
6
+ rescue ::ArgumentError
7
+ # chars (upper alpha, lower alpha, lower greek)
8
+ ([65, 97, 945].include? ord) ? '0' : ([ord - 1].pack 'U*')
9
+ end
10
+ end unless method_defined? :pred
11
+ end
@@ -57,7 +57,7 @@ module InlineImageArranger
57
57
  end
58
58
 
59
59
  # TODO make helper method to calculate width and height of image
60
- if fragment[:image_type] == 'svg'
60
+ if fragment[:image_format] == 'svg'
61
61
  svg_obj = ::Prawn::Svg::Interface.new (::IO.read image_path), doc, at: doc.bounds.top_left, width: image_w, fallback_font_name: doc.default_svg_font
62
62
  if image_w
63
63
  fragment[:image_width] = svg_obj.document.sizing.output_width
@@ -69,7 +69,7 @@ module InlineImageArranger
69
69
  fragment[:image_obj] = svg_obj
70
70
  else
71
71
  # TODO cache image info based on path (Prawn cached based on SHA1 of content)
72
- image_obj, image_info = doc.build_image_object image_path
72
+ image_obj, image_info = ::File.open(image_path, 'rb') {|fd| doc.build_image_object fd }
73
73
  if image_w
74
74
  fragment[:image_width], fragment[:image_height] = image_info.calc_image_dimensions width: image_w
75
75
  else
@@ -24,12 +24,12 @@ module InlineImageRenderer
24
24
  fragment.top - ((fragment.height - data[:image_height]) / 2.0)
25
25
  end
26
26
  image_left = fragment.left + ((fragment.width - data[:image_width]) / 2.0)
27
- case data[:image_type]
27
+ case data[:image_format]
28
28
  when 'svg'
29
- # prawn-svg messes with the cursor; use float as a workaround
30
- pdf.float do
31
- data[:image_obj].tap {|obj| obj.options[:at] = [image_left, image_top] }.draw
32
- end
29
+ (image_obj = data[:image_obj]).options[:at] = [image_left, image_top]
30
+ # NOTE prawn-svg messes with the cursor; use float to workaround
31
+ # NOTE prawn-svg 0.24.0, 0.25.0, & 0.25.1 didn't restore font after call to draw (see mogest/prawn-svg#80)
32
+ pdf.float { image_obj.draw }
33
33
  else
34
34
  pdf.embed_image data[:image_obj], data[:image_info], at: [image_left, image_top], width: data[:image_width]
35
35
  end
@@ -2,7 +2,7 @@ module Asciidoctor
2
2
  module Pdf
3
3
  module FormattedText
4
4
  class Transform
5
- EOL = "\n"
5
+ LF = %(\n)
6
6
  CharEntityTable = {
7
7
  lt: '<',
8
8
  gt: '>',
@@ -72,16 +72,16 @@ class Transform
72
72
  case node[:name]
73
73
  when :br
74
74
  if @merge_adjacent_text_nodes && previous_fragment_is_text
75
- fragments << { text: %(#{fragments.pop[:text]}#{EOL}) }
75
+ fragments << { text: %(#{fragments.pop[:text]}#{LF}) }
76
76
  else
77
- fragments << { text: EOL }
77
+ fragments << { text: LF }
78
78
  end
79
79
  previous_fragment_is_text = true
80
80
  when :img
81
81
  attributes = node[:attributes]
82
82
  fragment = {
83
83
  image_path: attributes[:src],
84
- image_type: attributes[:type],
84
+ image_format: attributes[:format],
85
85
  image_tmp: (attributes[:tmp] == 'true'),
86
86
  text: attributes[:alt],
87
87
  callback: InlineImageRenderer
@@ -194,6 +194,8 @@ class Transform
194
194
  } : value
195
195
  end
196
196
  if !fragment[:name] && (value = attrs[:name])
197
+ # NOTE ZeroWidthSpace is used as placeholder text so Prawn doesn't drop fragment
198
+ #fragment[:text] = ZeroWidthSpace
197
199
  fragment[:name] = value
198
200
  fragment[:callback] = InlineDestinationMarker
199
201
  end
@@ -12,7 +12,7 @@ class ImplicitHeaderProcessor < ::Asciidoctor::Extensions::IncludeProcessor
12
12
  if (first_line = fd.readline) && (first_line.start_with? '= ')
13
13
  # HACK reset counters for each article for Editions
14
14
  if doc.attr? 'env', 'editions'
15
- doc.counters.each do |(counter_key, counter_val)|
15
+ doc.counters.each do |counter_key, counter_val|
16
16
  doc.attributes.delete counter_key
17
17
  end
18
18
  doc.counters.clear
@@ -16,7 +16,7 @@ class Page
16
16
  @content = document.ref({})
17
17
  dictionary.data[:Contents] << document.state.store[@content]
18
18
  document.renderer.open_graphics_state
19
- end unless respond_to? :new_content_stream
19
+ end unless method_defined? :new_content_stream
20
20
  end
21
21
  end
22
22
  end
@@ -68,7 +68,7 @@ class CodeRayEncoder < ::CodeRay::Encoders::Encoder
68
68
  value: '336600'
69
69
  }
70
70
 
71
- EOL = %(\n)
71
+ LF = %(\n)
72
72
  NoBreakSpace = %(\u00a0)
73
73
  InnerIndent = %(\n )
74
74
  GuardedIndent = %(\u00a0)
@@ -83,7 +83,7 @@ class CodeRayEncoder < ::CodeRay::Encoders::Encoder
83
83
  end
84
84
 
85
85
  def text_token text, kind
86
- if text == EOL
86
+ if text == LF
87
87
  @out << { text: text }
88
88
  @start_of_line = true
89
89
  # NOTE text is nil and kind is :error when CodeRay ends parsing on an error
@@ -99,7 +99,7 @@ class CodeRayEncoder < ::CodeRay::Encoders::Encoder
99
99
  # QUESTION should we default to no color?
100
100
  @out << { text: text, color: (COLORS[kind] || COLORS[@open.last] || COLORS[:default]) }
101
101
  end
102
- @start_of_line = text.end_with? EOL
102
+ @start_of_line = text.end_with? LF
103
103
  end
104
104
  end
105
105
 
@@ -11,6 +11,7 @@ module Extensions
11
11
 
12
12
  IconSets = ['fa', 'fi', 'octicon', 'pf'].to_set
13
13
  MeasurementValueRx = /(\d+|\d*\.\d+)(in|mm|cm|px|pt)?$/
14
+ InitialPageContent = %(q\n)
14
15
 
15
16
  # - :height is the height of a line
16
17
  # - :leading is spacing between adjacent lines
@@ -53,6 +54,20 @@ module Extensions
53
54
  reference_bounds.height
54
55
  end
55
56
 
57
+ # Set the margins for the current page.
58
+ #
59
+ def set_page_margin margin
60
+ # FIXME is there a cleaner way to set margins? does it make sense to override create_new_page?
61
+ apply_margin_options margin: margin
62
+ generate_margin_box
63
+ end
64
+
65
+ # Returns the margins for the current page as a 4 element array (top, right, bottom, left)
66
+ #
67
+ def page_margin
68
+ [page.margins[:top], page.margins[:right], page.margins[:bottom], page.margins[:left]]
69
+ end
70
+
56
71
  # Returns the width of the left margin for the current page
57
72
  #
58
73
  def page_margin_left
@@ -93,20 +108,38 @@ module Extensions
93
108
  page.dimensions[2] - bounds.absolute_right
94
109
  end
95
110
 
96
- # Returns whether the cursor is at the top of the page (i.e., margin box)
111
+ # Returns the side the current page is facing, :recto or :verso.
112
+ #
113
+ def page_side pgnum = nil
114
+ (recto_page? pgnum) ? :recto : :verso
115
+ end
116
+
117
+ # Returns whether the page is a recto page.
118
+ #
119
+ def recto_page? pgnum = nil
120
+ (pgnum || page_number).odd?
121
+ end
122
+
123
+ # Returns whether the page is a verso page.
124
+ #
125
+ def verso_page? pgnum = nil
126
+ (pgnum || page_number).even?
127
+ end
128
+
129
+ # Returns whether the cursor is at the top of the page (i.e., margin box).
97
130
  #
98
131
  def at_page_top?
99
132
  @y == @margin_box.absolute_top
100
133
  end
101
134
 
102
- # Returns whether the current page is empty (no content is written).
103
- # If at least one page has not yet been created, returns false.
135
+ # Returns whether the current page is empty (i.e., no content has been written).
136
+ # Returns false if a page has not yet been created.
104
137
  #
105
138
  def empty_page?
106
139
  # if we are at the page top, assume we didn't write anything to the page
107
140
  #at_page_top?
108
- # ...or use low-level check (initial value is "q\n")
109
- (page.content.stream || []).length <= 2 && page_number > 0
141
+ # ...or use more robust, low-level check (initial value of content is "q\n")
142
+ page_number > 0 && page.content.stream.filtered_stream == InitialPageContent
110
143
  end
111
144
  alias :page_is_empty? :empty_page?
112
145
 
@@ -119,17 +152,21 @@ module Extensions
119
152
  # Converts the specified float value to a pt value from the
120
153
  # specified unit of measurement (e.g., in, cm, mm, etc).
121
154
  def to_pt num, units
122
- case units
123
- when nil, 'pt'
155
+ if units.nil_or_empty?
124
156
  num
125
- when 'in'
126
- num * 72
127
- when 'mm'
128
- num * (72 / 25.4)
129
- when 'cm'
130
- num * (720 / 25.4)
131
- when 'px'
132
- num * 0.75
157
+ else
158
+ case units
159
+ when 'pt'
160
+ num
161
+ when 'in'
162
+ num * 72
163
+ when 'mm'
164
+ num * (72 / 25.4)
165
+ when 'cm'
166
+ num * (720 / 25.4)
167
+ when 'px'
168
+ num * 0.75
169
+ end
133
170
  end
134
171
  end
135
172
 
@@ -158,15 +195,6 @@ module Extensions
158
195
  dest_xyz 0, page_height, nil, (page_num ? state.pages[page_num - 1] : page)
159
196
  end
160
197
 
161
- # Text
162
-
163
- =begin
164
- # Draws a disc bullet as float text
165
- def draw_bullet
166
- float { text '•' }
167
- end
168
- =end
169
-
170
198
  # Fonts
171
199
 
172
200
  # Registers a new custom font described in the data parameter
@@ -474,7 +502,7 @@ module Extensions
474
502
  #
475
503
  def span_page_width_if verdict
476
504
  if verdict
477
- indent -bounds_margin_left, -bounds_margin_right do
505
+ indent(-bounds_margin_left, -bounds_margin_right) do
478
506
  yield
479
507
  end
480
508
  else
@@ -482,6 +510,20 @@ module Extensions
482
510
  end
483
511
  end
484
512
 
513
+ # A flowing version of the bounding_box. If the content runs to another page, the cursor starts
514
+ # at the top of the page instead of the original cursor position. Similar to span, except
515
+ # you can specify an absolute left position and pass additional options through to bounding_box.
516
+ #
517
+ def flow_bounding_box left = 0, opts = {}
518
+ original_y = self.y
519
+ canvas do
520
+ bounding_box [margin_box.absolute_left + left, margin_box.absolute_top], opts do
521
+ self.y = original_y
522
+ yield
523
+ end
524
+ end
525
+ end
526
+
485
527
  # Graphics
486
528
 
487
529
  # Fills the current bounding box with the specified fill color. Before
@@ -616,7 +658,12 @@ module Extensions
616
658
  pdf_store.pages.data[:Kids].pop
617
659
  pdf_store.pages.data[:Count] -= 1
618
660
  state.pages.pop
619
- go_to_page(pg - 1)
661
+ if pg > 1
662
+ go_to_page pg - 1
663
+ else
664
+ @page_number = 0
665
+ state.page = nil
666
+ end
620
667
  end
621
668
 
622
669
  # Import the specified page into the current document.
@@ -743,10 +790,12 @@ module Extensions
743
790
  scratch.font font_family, style: font_style, size: font_size do
744
791
  scratch.instance_exec(&block)
745
792
  end
793
+ # NOTE don't count excess if cursor exceeds writable area (due to padding)
794
+ partial_page_height = [effective_page_height, start_y - scratch.y].min
746
795
  scratch.bounds.subtract_left_padding left_padding if left_padding > 0
747
796
  scratch.bounds.subtract_right_padding right_padding if right_padding > 0
748
797
  whole_pages = scratch.page_number - start_page_number
749
- [(whole_pages * bounds.height + (start_y - scratch.y)), whole_pages, (start_y - scratch.y)]
798
+ [(whole_pages * bounds.height + partial_page_height), whole_pages, partial_page_height]
750
799
  end
751
800
 
752
801
  # Attempt to keep the objects generated in the block on the same page
@@ -783,7 +832,7 @@ module Extensions
783
832
  =begin
784
833
  def run_with_trial &block
785
834
  available_space = cursor
786
- whole_pages, remainder = dry_run(&block)
835
+ total_height, whole_pages, remainder = dry_run(&block)
787
836
  if whole_pages > 0 || remainder > available_space
788
837
  started_new_page = true
789
838
  else
@@ -1,4 +1,6 @@
1
1
  class Prawn::Font::AFM
2
+ undef_method :normalize_encoding
3
+
2
4
  # Patch normalize_encoding method to handle conversion more gracefully.
3
5
  #
4
6
  # Any valid utf-8 characters that cannot be encoded to windows-1252 are
@@ -9,11 +11,10 @@ class Prawn::Font::AFM
9
11
  rescue ::Encoding::UndefinedConversionError
10
12
  warn 'The following text could not be fully converted to the Windows-1252 character set:'
11
13
  warn %(#{text.gsub(/^/, '| ').rstrip})
12
- warn ''
13
- text.encode 'windows-1252', undef: :replace, replace: "\u00ac"
14
+ text.encode 'windows-1252', undef: :replace, replace: %(\u00ac)
14
15
  rescue ::Encoding::InvalidByteSequenceError
15
16
  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.)
17
+ %(Your document includes text which is not compatible with the Windows-1252 character set.
18
+ If you need full UTF-8 support, use TTF fonts instead of the built-in PDF (AFM) fonts.)
18
19
  end
19
20
  end
@@ -30,7 +30,7 @@ module Images
30
30
  { width: img_size.output_width, height: img_size.output_height }
31
31
  else
32
32
  # NOTE build_image_object caches image data previously loaded
33
- _, img_size = build_image_object path
33
+ _, img_size = ::File.open(path, 'rb') {|fd| build_image_object fd }
34
34
  { width: img_size.width, height: img_size.height }
35
35
  end
36
36
  end
@@ -62,7 +62,10 @@ class RomanNumeral
62
62
  end
63
63
 
64
64
  def to_r
65
- roman = RomanNumeral.int_to_roman @integer_value
65
+ if (int = @integer_value) < 1
66
+ return int.to_s
67
+ end
68
+ roman = RomanNumeral.int_to_roman int
66
69
  @letter_case == :lower ? roman.downcase : roman
67
70
  end
68
71
 
@@ -79,15 +82,19 @@ class RomanNumeral
79
82
  self
80
83
  end
81
84
 
85
+ def pred
86
+ RomanNumeral.new @integer_value - 1, @letter_case
87
+ end
88
+
82
89
  def self.int_to_roman value
83
- result = ''
90
+ result = []
84
91
  BaseDigits.keys.reverse.each do |ival|
85
92
  while value >= ival
86
93
  value -= ival
87
- result += BaseDigits[ival]
94
+ result << BaseDigits[ival]
88
95
  end
89
96
  end
90
- result
97
+ result.join
91
98
  end
92
99
 
93
100
  def self.roman_to_int value
@@ -5,7 +5,7 @@ module Formatters
5
5
  class Prawn < Formatter
6
6
  tag 'prawn'
7
7
 
8
- EOL = %(\n)
8
+ LF = %(\n)
9
9
  NoBreakSpace = %(\u00a0)
10
10
  InnerIndent = %(\n )
11
11
  GuardedIndent = %(\u00a0)
@@ -39,15 +39,15 @@ class Prawn < Formatter
39
39
  fragments = []
40
40
  fragments << (create_linenum_fragment linenum += 1)
41
41
  tokens.each do |tok, val|
42
- if val == EOL
43
- fragments << { text: EOL }
42
+ if val == LF
43
+ fragments << { text: LF }
44
44
  fragments << (create_linenum_fragment linenum += 1)
45
- elsif val.include? EOL
45
+ elsif val.include? LF
46
46
  base_fragment = create_fragment tok, val
47
47
  val.each_line do |line|
48
48
  fragments << (base_fragment.merge text: line)
49
49
  # NOTE append linenum fragment if there's a next line; only works if source doesn't have trailing endline
50
- if line.end_with? EOL
50
+ if line.end_with? LF
51
51
  fragments << (create_linenum_fragment linenum += 1)
52
52
  end
53
53
  end
@@ -58,7 +58,7 @@ class Prawn < Formatter
58
58
  # NOTE drop orphaned linenum fragment (due to trailing endline in source)
59
59
  fragments.pop if (last_fragment = fragments[-1]) && last_fragment[:linenum]
60
60
  # NOTE pad numbers that have less digits than the largest line number
61
- if (linenum_w = (linenum / 10) + 1) > 1
61
+ if (linenum_w = linenum.to_s.size) > 1
62
62
  # NOTE extra column is the trailing space after the line number
63
63
  linenum_w += 1
64
64
  fragments.each do |fragment|
@@ -70,13 +70,13 @@ class Prawn < Formatter
70
70
  start_of_line = true
71
71
  tokens.map do |tok, val|
72
72
  # match one or more consecutive endlines
73
- if val == EOL || (val == (EOL * val.length))
73
+ if val == LF || (val == (LF * val.length))
74
74
  start_of_line = true
75
75
  { text: val }
76
76
  else
77
77
  val[0] = GuardedIndent if start_of_line && (val.start_with? ' ')
78
78
  val.gsub! InnerIndent, GuardedInnerIndent if val.include? InnerIndent
79
- start_of_line = val.end_with? EOL
79
+ start_of_line = val.end_with? LF
80
80
  # NOTE this optimization assumes we don't support/use background colors
81
81
  val.rstrip.empty? ? { text: val } : (create_fragment tok, val)
82
82
  end
@@ -85,7 +85,7 @@ class Prawn < Formatter
85
85
  end
86
86
  end
87
87
 
88
- # TODO method could still be optimized (for instance, check if val is EOL or empty)
88
+ # TODO method could still be optimized (for instance, check if val is LF or empty)
89
89
  def create_fragment tok, val = nil
90
90
  fragment = val ? { text: val } : {}
91
91
  if (style_rules = @theme.style_for tok)