asciidoctor-pdf 1.5.0.alpha.16 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. checksums.yaml +5 -5
  2. data/.yardopts +12 -0
  3. data/CHANGELOG.adoc +415 -1
  4. data/LICENSE.adoc +1 -1
  5. data/NOTICE.adoc +14 -11
  6. data/README.adoc +647 -222
  7. data/asciidoctor-pdf.gemspec +47 -44
  8. data/bin/asciidoctor-pdf +5 -9
  9. data/bin/asciidoctor-pdf-optimize +20 -0
  10. data/data/fonts/ABOUT-mplus1mn-subset +26 -0
  11. data/data/fonts/ABOUT-mplus1p-subset +26 -0
  12. data/data/fonts/ABOUT-notoemoji-subset +3 -0
  13. data/data/fonts/ABOUT-notoserif-subset +26 -0
  14. data/data/fonts/{LICENSE-mplus-testflight-58 → LICENSE-mplus} +2 -2
  15. data/data/fonts/{LICENSE-noto-2015-06-05 → LICENSE-notoserif} +0 -0
  16. data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
  17. data/data/fonts/mplus1mn-bold-subset.ttf +0 -0
  18. data/data/fonts/mplus1mn-bold_italic-ascii.ttf +0 -0
  19. data/data/fonts/mplus1mn-bold_italic-subset.ttf +0 -0
  20. data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
  21. data/data/fonts/mplus1mn-italic-subset.ttf +0 -0
  22. data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
  23. data/data/fonts/mplus1mn-regular-subset.ttf +0 -0
  24. data/data/fonts/mplus1p-regular-fallback.ttf +0 -0
  25. data/data/fonts/notoemoji-subset.ttf +0 -0
  26. data/data/fonts/notoserif-bold-subset.ttf +0 -0
  27. data/data/fonts/notoserif-bold_italic-subset.ttf +0 -0
  28. data/data/fonts/notoserif-italic-subset.ttf +0 -0
  29. data/data/fonts/notoserif-regular-subset.ttf +0 -0
  30. data/data/themes/base-theme.yml +26 -4
  31. data/data/themes/default-theme.yml +76 -60
  32. data/data/themes/default-with-fallback-font-theme.yml +9 -0
  33. data/docs/theming-guide.adoc +2731 -922
  34. data/lib/asciidoctor/pdf/converter.rb +4489 -0
  35. data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_block.rb +7 -0
  36. data/lib/asciidoctor/pdf/ext/asciidoctor/abstract_node.rb +7 -0
  37. data/lib/asciidoctor/pdf/ext/asciidoctor/document.rb +5 -0
  38. data/lib/asciidoctor/pdf/ext/asciidoctor/image.rb +35 -0
  39. data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/list.rb +4 -2
  40. data/lib/{asciidoctor-pdf/asciidoctor_ext → asciidoctor/pdf/ext/asciidoctor}/list_item.rb +3 -1
  41. data/lib/asciidoctor/pdf/ext/asciidoctor/logging_shim.rb +33 -0
  42. data/lib/asciidoctor/pdf/ext/asciidoctor/section.rb +45 -0
  43. data/lib/asciidoctor/pdf/ext/asciidoctor.rb +11 -0
  44. data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/array.rb +6 -6
  45. data/lib/asciidoctor/pdf/ext/core/file.rb +9 -0
  46. data/lib/asciidoctor/pdf/ext/core/hash.rb +7 -0
  47. data/lib/asciidoctor/pdf/ext/core/numeric.rb +26 -0
  48. data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/object.rb +3 -1
  49. data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/quantifiable_stdout.rb +9 -1
  50. data/lib/asciidoctor/pdf/ext/core/regexp.rb +5 -0
  51. data/lib/{asciidoctor-pdf/core_ext → asciidoctor/pdf/ext/core}/string.rb +9 -13
  52. data/lib/asciidoctor/pdf/ext/core.rb +10 -0
  53. data/lib/asciidoctor/pdf/ext/pdf-core/page.rb +54 -0
  54. data/lib/asciidoctor/pdf/ext/pdf-core/pdf_object.rb +8 -0
  55. data/lib/asciidoctor/pdf/ext/pdf-core.rb +4 -0
  56. data/lib/asciidoctor/pdf/ext/prawn/coderay_encoder.rb +117 -0
  57. data/lib/asciidoctor/pdf/ext/prawn/extensions.rb +922 -0
  58. data/lib/{asciidoctor-pdf/prawn_ext → asciidoctor/pdf/ext/prawn}/font/afm.rb +14 -10
  59. data/lib/asciidoctor/pdf/ext/prawn/font_metric_cache.rb +9 -0
  60. data/lib/asciidoctor/pdf/ext/prawn/formatted_text/box.rb +66 -0
  61. data/lib/{asciidoctor-pdf/prawn_ext → asciidoctor/pdf/ext/prawn}/formatted_text/fragment.rb +21 -18
  62. data/lib/asciidoctor/pdf/ext/prawn/images.rb +54 -0
  63. data/lib/asciidoctor/pdf/ext/prawn-svg/interface.rb +14 -0
  64. data/lib/asciidoctor/pdf/ext/prawn-svg.rb +6 -0
  65. data/lib/asciidoctor/pdf/ext/prawn-table/cell/asciidoc.rb +76 -0
  66. data/lib/{asciidoctor-pdf/prawn-table_ext → asciidoctor/pdf/ext/prawn-table}/cell/text.rb +6 -3
  67. data/lib/asciidoctor/pdf/ext/prawn-table/cell.rb +60 -0
  68. data/lib/asciidoctor/pdf/ext/prawn-table.rb +6 -0
  69. data/lib/{asciidoctor-pdf/prawn-templates_ext.rb → asciidoctor/pdf/ext/prawn-templates.rb} +2 -0
  70. data/lib/asciidoctor/pdf/ext/prawn.rb +9 -0
  71. data/lib/asciidoctor/pdf/ext/pygments.rb +34 -0
  72. data/lib/asciidoctor/pdf/ext/rouge/formatters/prawn.rb +208 -0
  73. data/lib/{asciidoctor-pdf/rouge_ext/themes/pastie.rb → asciidoctor/pdf/ext/rouge/themes/asciidoctor_pdf_default.rb} +7 -5
  74. data/lib/asciidoctor/pdf/ext/rouge.rb +5 -0
  75. data/lib/asciidoctor/pdf/ext.rb +9 -0
  76. data/lib/asciidoctor/pdf/formatted_text/formatter.rb +43 -0
  77. data/lib/asciidoctor/pdf/formatted_text/fragment_position_renderer.rb +14 -0
  78. data/lib/asciidoctor/pdf/formatted_text/inline_destination_marker.rb +21 -0
  79. data/lib/asciidoctor/pdf/formatted_text/inline_image_arranger.rb +134 -0
  80. data/lib/asciidoctor/pdf/formatted_text/inline_image_renderer.rb +51 -0
  81. data/lib/asciidoctor/pdf/formatted_text/inline_text_aligner.rb +22 -0
  82. data/lib/{asciidoctor-pdf → asciidoctor/pdf}/formatted_text/parser.rb +175 -53
  83. data/lib/{asciidoctor-pdf → asciidoctor/pdf}/formatted_text/parser.treetop +20 -14
  84. data/lib/asciidoctor/pdf/formatted_text/source_wrap.rb +43 -0
  85. data/lib/asciidoctor/pdf/formatted_text/text_background_and_border_renderer.rb +55 -0
  86. data/lib/asciidoctor/pdf/formatted_text/transform.rb +394 -0
  87. data/lib/{asciidoctor-pdf → asciidoctor/pdf}/formatted_text.rb +6 -0
  88. data/lib/asciidoctor/pdf/index_catalog.rb +133 -0
  89. data/lib/asciidoctor/pdf/measurements.rb +62 -0
  90. data/lib/asciidoctor/pdf/optimizer.rb +44 -0
  91. data/lib/asciidoctor/pdf/pdfmark.rb +41 -0
  92. data/lib/asciidoctor/pdf/roman_numeral.rb +128 -0
  93. data/lib/asciidoctor/pdf/sanitizer.rb +45 -0
  94. data/lib/asciidoctor/pdf/text_transformer.rb +116 -0
  95. data/lib/asciidoctor/pdf/theme_loader.rb +305 -0
  96. data/lib/asciidoctor/pdf/version.rb +8 -0
  97. data/lib/asciidoctor/pdf.rb +15 -0
  98. data/lib/asciidoctor-pdf/converter.rb +2 -3343
  99. data/lib/asciidoctor-pdf/version.rb +3 -5
  100. data/lib/asciidoctor-pdf.rb +3 -3
  101. metadata +242 -128
  102. data/Gemfile +0 -22
  103. data/Rakefile +0 -81
  104. data/lib/asciidoctor-pdf/asciidoctor_ext/image.rb +0 -24
  105. data/lib/asciidoctor-pdf/asciidoctor_ext/section.rb +0 -34
  106. data/lib/asciidoctor-pdf/asciidoctor_ext.rb +0 -5
  107. data/lib/asciidoctor-pdf/core_ext/numeric.rb +0 -15
  108. data/lib/asciidoctor-pdf/core_ext/ostruct.rb +0 -17
  109. data/lib/asciidoctor-pdf/core_ext.rb +0 -4
  110. data/lib/asciidoctor-pdf/formatted_text/formatter.rb +0 -27
  111. data/lib/asciidoctor-pdf/formatted_text/inline_destination_marker.rb +0 -21
  112. data/lib/asciidoctor-pdf/formatted_text/inline_image_arranger.rb +0 -172
  113. data/lib/asciidoctor-pdf/formatted_text/inline_image_renderer.rb +0 -46
  114. data/lib/asciidoctor-pdf/formatted_text/transform.rb +0 -261
  115. data/lib/asciidoctor-pdf/implicit_header_processor.rb +0 -63
  116. data/lib/asciidoctor-pdf/index_catalog.rb +0 -119
  117. data/lib/asciidoctor-pdf/measurements.rb +0 -58
  118. data/lib/asciidoctor-pdf/pdf-core_ext/page.rb +0 -25
  119. data/lib/asciidoctor-pdf/pdf-core_ext/pdf_object.rb +0 -6
  120. data/lib/asciidoctor-pdf/pdf-core_ext.rb +0 -2
  121. data/lib/asciidoctor-pdf/pdfmark.rb +0 -33
  122. data/lib/asciidoctor-pdf/prawn-svg_ext/interface.rb +0 -10
  123. data/lib/asciidoctor-pdf/prawn-svg_ext.rb +0 -4
  124. data/lib/asciidoctor-pdf/prawn-table_ext/cell/asciidoc.rb +0 -69
  125. data/lib/asciidoctor-pdf/prawn-table_ext.rb +0 -3
  126. data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +0 -115
  127. data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +0 -863
  128. data/lib/asciidoctor-pdf/prawn_ext/images.rb +0 -40
  129. data/lib/asciidoctor-pdf/prawn_ext.rb +0 -5
  130. data/lib/asciidoctor-pdf/roman_numeral.rb +0 -114
  131. data/lib/asciidoctor-pdf/rouge_ext/css_theme.rb +0 -15
  132. data/lib/asciidoctor-pdf/rouge_ext/formatters/prawn.rb +0 -164
  133. data/lib/asciidoctor-pdf/rouge_ext.rb +0 -4
  134. data/lib/asciidoctor-pdf/sanitizer.rb +0 -88
  135. data/lib/asciidoctor-pdf/temporary_path.rb +0 -13
  136. data/lib/asciidoctor-pdf/theme_loader.rb +0 -247
  137. data/lib/asciidoctor-pdf/ttfunk_ext.rb +0 -8
@@ -1,34 +0,0 @@
1
- class Asciidoctor::Section
2
- def numbered_title opts = {}
3
- unless (@cached_numbered_title ||= nil)
4
- if (slevel = (@level == 0 && @special ? 1 : @level)) == 0
5
- @is_numbered = false
6
- @cached_numbered_title = @cached_formal_numbered_title = title
7
- elsif @numbered && !@caption && slevel <= (@document.attr 'sectnumlevels', 3).to_i
8
- @is_numbered = true
9
- @cached_numbered_title = %(#{sectnum} #{title})
10
- @cached_formal_numbered_title = if slevel == 1 && @document.doctype == 'book'
11
- %(#{@document.attr 'chapter-label', 'Chapter'} #{@cached_numbered_title}).lstrip
12
- else
13
- @cached_numbered_title
14
- end
15
- else
16
- @is_numbered = false
17
- @cached_numbered_title = @cached_formal_numbered_title = captioned_title
18
- end
19
- end
20
- opts[:formal] ? @cached_formal_numbered_title : @cached_numbered_title
21
- end unless method_defined? :numbered_title
22
-
23
- def part?
24
- @document.doctype == 'book' && @level == 0 && !@special
25
- end unless method_defined? :part?
26
-
27
- def chapter?
28
- @document.doctype == 'book' && (@level == 1 || (@special && @level == 0))
29
- end unless method_defined? :chapter?
30
-
31
- def part_or_chapter?
32
- @document.doctype == 'book' && @level < 2
33
- end unless method_defined? :part_or_chapter?
34
- end
@@ -1,5 +0,0 @@
1
- # NOTE these are all candidates for inclusion in Asciidoctor core
2
- require_relative 'asciidoctor_ext/section'
3
- require_relative 'asciidoctor_ext/list'
4
- require_relative 'asciidoctor_ext/list_item'
5
- require_relative 'asciidoctor_ext/image'
@@ -1,15 +0,0 @@
1
- class Numeric
2
- if (instance_method :truncate).arity == 0
3
- def truncate_to_precision precision
4
- if (precision = precision.to_i) > 0
5
- factor = 10 ** precision
6
- (self * factor).truncate.fdiv factor
7
- else
8
- truncate
9
- end
10
- end
11
- else
12
- # use native method in Ruby >= 2.4
13
- alias :truncate_to_precision :truncate
14
- end unless method_defined? :truncate_to_precision
15
- end
@@ -1,17 +0,0 @@
1
- class OpenStruct
2
- def [] key
3
- send key
4
- end unless method_defined? :[]
5
-
6
- def []= key, val
7
- send %(#{key}=), val
8
- end unless method_defined? :[]=
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
@@ -1,4 +0,0 @@
1
- require_relative 'core_ext/object'
2
- require_relative 'core_ext/array'
3
- require_relative 'core_ext/numeric'
4
- require_relative 'core_ext/string'
@@ -1,27 +0,0 @@
1
- module Asciidoctor
2
- module Pdf
3
- module FormattedText
4
- class Formatter
5
- FormattingSnifferPattern = /[<&]/
6
- WHITESPACE = " \t\n"
7
-
8
- def initialize options = {}
9
- @parser = MarkupParser.new
10
- @transform = Transform.new merge_adjacent_text_nodes: true, theme: options[:theme]
11
- end
12
-
13
- def format string, *args
14
- options = args[0] || {}
15
- string = string.tr_s(WHITESPACE, ' ') if options[:normalize]
16
- return [text: string] unless string.match(FormattingSnifferPattern)
17
- if (parsed = @parser.parse(string))
18
- @transform.apply(parsed.content)
19
- else
20
- warn %(Failed to parse formatted text: #{string})
21
- [text: string]
22
- end
23
- end
24
- end
25
- end
26
- end
27
- end
@@ -1,21 +0,0 @@
1
- module Asciidoctor::Pdf::FormattedText
2
- module InlineDestinationMarker
3
- module_function
4
-
5
- # render_behind is called before the text is printed
6
- def render_behind fragment
7
- unless (pdf = fragment.document).scratch?
8
- if (name = fragment.format_state[:name])
9
- if fragment.format_state[:type] == :indexterm
10
- (pdf.instance_variable_get :@index).link_dest_to_page name, pdf.page_number
11
- end
12
- # get precise position of the reference (x, y)
13
- dest_rect = fragment.absolute_bounding_box
14
- pdf.add_dest name, (pdf.dest_xyz dest_rect[0], dest_rect[-1])
15
- # prevent any text from being written
16
- fragment.conceal
17
- end
18
- end
19
- end
20
- end
21
- end
@@ -1,172 +0,0 @@
1
- module Asciidoctor::Pdf::FormattedText
2
- module InlineImageArranger
3
- include ::Asciidoctor::Pdf::Measurements
4
-
5
- ImagePlaceholderChar = '.'
6
- begin
7
- require 'thread_safe' unless defined? ::ThreadSafe
8
- PlaceholderWidthCache = ::ThreadSafe::Cache.new
9
- rescue
10
- PlaceholderWidthCache = {}
11
- end
12
- TemporaryPath = ::Asciidoctor::Pdf::TemporaryPath
13
-
14
- if respond_to? :prepend
15
- def wrap fragments
16
- arrange_images fragments
17
- super
18
- end
19
- else
20
- class << self
21
- def extended base
22
- base.class.__send__ :alias_method, :_initial_wrap, :wrap
23
- end
24
- end
25
-
26
- def wrap fragments
27
- arrange_images fragments
28
- _initial_wrap fragments
29
- end
30
- end
31
-
32
- # Iterates over the fragments that represent inline images and prepares the
33
- # image data to be embedded into the document.
34
- #
35
- # This method populates the image_width, image_height, image_obj and
36
- # image_info (PNG only) keys on the fragment. The text is replaced with
37
- # placeholder text that will be used to reserve enough room in the line to
38
- # fit the image.
39
- #
40
- # The image height is scaled down to 75% of the specified width (px to pt
41
- # conversion). If the image height is more than 1.5x the height of the
42
- # surrounding line of text, the font size and line metrics of the fragment
43
- # are modified to fit the image in the line.
44
- #
45
- # If this is the scratch document, the image renderer callback is removed so
46
- # that the image is not embedded.
47
- #
48
- # When this method is called, the cursor is positioned at start of where this
49
- # group of fragments will be written (offset by the leading padding).
50
- #
51
- # This method is called each time the set of fragments overflow to another
52
- # page, so it's necessary to short-circuit if that case is detected.
53
- def arrange_images fragments
54
- doc = @document
55
- return if (raw_image_fragments = fragments.select {|f| (f.key? :image_path) && !(f.key? :image_obj) }).empty?
56
- scratch = doc.scratch?
57
- available_w = doc.bounds.width
58
- #available_h = doc.bounds.height
59
- # NOTE try to fit image within bounds if cursor is within 1% of top of page
60
- # QUESTION are we considering the right boundaries here?
61
- available_h ||= (doc.cursor / (available_h = doc.bounds.height) >= 0.99 ? doc.cursor : available_h)
62
- raw_image_fragments.each do |fragment|
63
- drop = scratch
64
- begin
65
- image_path = fragment[:image_path]
66
-
67
- # NOTE only attempt to convert an unresolved (i.e., String) value
68
- if ::String === (image_w = fragment[:image_width])
69
- image_w = [available_w, (image_w.end_with? '%') ? (image_w.to_f / 100 * available_w) : image_w.to_f].min
70
- end
71
-
72
- # TODO make helper method to calculate width and height of image
73
- if fragment[:image_format] == 'svg'
74
- svg_obj = ::Prawn::Svg::Interface.new ::IO.read(image_path), doc,
75
- at: doc.bounds.top_left,
76
- width: image_w,
77
- fallback_font_name: doc.default_svg_font
78
- svg_size = image_w ? svg_obj.document.sizing :
79
- # NOTE convert intrinsic dimensions to points; constrain to content width
80
- (svg_obj.resize width: [(to_pt svg_obj.document.sizing.output_width, :px), available_w].min)
81
- # NOTE the best we can do is make the image fit within full height of bounds
82
- if (image_h = svg_size.output_height) > available_h
83
- image_w = (svg_size = svg_obj.resize height: (image_h = available_h)).output_width
84
- else
85
- image_w = svg_size.output_width
86
- end
87
- fragment[:image_obj] = svg_obj
88
- else
89
- # TODO cache image info based on path (Prawn caches based on SHA1 of content)
90
- # NOTE image_obj is constrained to image_width by renderer
91
- image_obj, image_info = ::File.open(image_path, 'rb') {|fd| doc.build_image_object fd }
92
- if image_w
93
- if image_w == image_info.width
94
- image_h = image_info.height.to_f
95
- else
96
- image_h = image_w * (image_info.height.fdiv image_info.width)
97
- end
98
- else
99
- # NOTE convert intrinsic dimensions to points; constrain to content width
100
- if (image_w = to_pt image_info.width, :px) > available_w
101
- image_h = (image_w = available_w) * (image_info.height.fdiv image_info.width)
102
- else
103
- image_h = to_pt image_info.height, :px
104
- end
105
- end
106
- # NOTE the best we can do is make the image fit within full height of bounds
107
- image_w = (image_h = available_h) * (image_info.width.fdiv image_info.height) if image_h > available_h
108
- fragment[:image_obj] = image_obj
109
- fragment[:image_info] = image_info
110
- end
111
-
112
- spacer_w = nil
113
- doc.fragment_font fragment do
114
- # NOTE if image height exceeds line height by more than 1.5x, increase the line height
115
- # FIXME we could really use a nicer API from Prawn here; this is an ugly hack
116
- if (f_height = image_h) > (line_font = doc.font).height * 1.5
117
- # align with descender (equivalent to vertical-align: bottom in CSS)
118
- fragment[:ascender] = f_height - (fragment[:descender] = line_font.descender)
119
- doc.font_size(fragment[:size] = f_height * (doc.font_size / line_font.height))
120
- # align with baseline (roughly equivalent to vertical-align: baseline in CSS)
121
- #fragment[:ascender] = f_height
122
- #fragment[:descender] = 0
123
- #doc.font_size(fragment[:size] = (f_height + line_font.descender) * (doc.font_size / line_font.height))
124
- fragment[:line_height_increased] = true
125
- end
126
-
127
- unless (spacer_w = PlaceholderWidthCache[f_info = doc.font_info])
128
- spacer_w = PlaceholderWidthCache[f_info] = doc.width_of ImagePlaceholderChar
129
- end
130
- end
131
-
132
- # NOTE make room for image by repeating image placeholder character, using character spacing to fine-tune
133
- # NOTE image_width is constrained to available_w, so we don't have to check for overflow
134
- spacer_cnt, remainder = image_w.divmod spacer_w
135
- if spacer_cnt > 0
136
- fragment[:text] = ImagePlaceholderChar * spacer_cnt
137
- fragment[:character_spacing] = remainder.fdiv spacer_cnt if remainder > 0
138
- else
139
- fragment[:text] = ImagePlaceholderChar
140
- fragment[:character_spacing] = -(spacer_w - remainder)
141
- end
142
-
143
- # FIXME we could use a nicer API from Prawn here that lets us reserve a fragment width without text
144
- #fragment[:image_width] = fragment[:width] = image_w
145
- fragment[:image_width] = image_w
146
- fragment[:image_height] = image_h
147
- rescue => e
148
- warn %(asciidoctor: WARNING: could not embed image: #{image_path}; #{e.message})
149
- drop = true # delegate to cleanup logic in ensure block
150
- ensure
151
- # NOTE skip rendering image in scratch document or if image can't be loaded
152
- if drop
153
- fragment.delete :callback
154
- fragment.delete :image_info
155
- # NOTE retain key to indicate we've visited fragment already
156
- fragment[:image_obj] = nil
157
- # NOTE in main document, temporary image path is unlinked by renderer
158
- ::File.unlink image_path if TemporaryPath === image_path && image_path.exist?
159
- end
160
- end
161
- end
162
- end
163
- end
164
-
165
- if respond_to? :prepend
166
- class ::Prawn::Text::Formatted::Box
167
- prepend InlineImageArranger
168
- end
169
- else
170
- ::Prawn::Text::Formatted::Box.extensions << InlineImageArranger
171
- end
172
- end
@@ -1,46 +0,0 @@
1
- module Asciidoctor::Pdf::FormattedText
2
- module InlineImageRenderer
3
- TemporaryPath = ::Asciidoctor::Pdf::TemporaryPath
4
- module_function
5
-
6
- # Embeds the image object in this fragment into the document in place of the
7
- # text that was previously used to reserve space for the image in the line.
8
- #
9
- # If the image height is less than 1.5x the height of the surrounding text,
10
- # it is centered vertically in the line. If the image height is greater, then
11
- # the image is aligned to the bottom of the text.
12
- #
13
- # Note that render_behind is called before the text is printed.
14
- #
15
- # This handler is only used on the main document (not the scratch document).
16
- #
17
- def render_behind fragment
18
- pdf = fragment.document
19
- data = fragment.format_state
20
- image_top = if data.key? :line_height_increased
21
- # align image to bottom of line (differs from fragment.top by descender value)
22
- fragment.bottom + data[:image_height]
23
- else
24
- # center image in line
25
- fragment.top - ((fragment.height - data[:image_height]) / 2.0)
26
- end
27
- image_left = fragment.left + ((fragment.width - data[:image_width]) / 2.0)
28
- case data[:image_format]
29
- when 'svg'
30
- (image_obj = data[:image_obj]).options[:at] = [image_left, image_top]
31
- # NOTE prawn-svg messes with the cursor; use float to workaround
32
- # 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)
33
- pdf.float { image_obj.draw }
34
- else
35
- pdf.embed_image data[:image_obj], data[:image_info], at: [image_left, image_top], width: data[:image_width], height: data[:image_height]
36
- end
37
- # ...or use the public interface, loading the image again
38
- #pdf.image data[:image_path], at: [image_left, image_top], width: data[:image_width]
39
-
40
- # prevent any text from being written
41
- fragment.conceal
42
- ensure
43
- ::File.unlink data[:image_path] if TemporaryPath === data[:image_path] && data[:image_path].exist?
44
- end
45
- end
46
- end
@@ -1,261 +0,0 @@
1
- module Asciidoctor
2
- module Pdf
3
- module FormattedText
4
- class Transform
5
- LF = %(\n)
6
- CharEntityTable = {
7
- lt: '<',
8
- gt: '>',
9
- amp: '&',
10
- quot: '"',
11
- apos: '\''
12
- }
13
- CharRefRx = /&(?:#(\d{2,6})|(#{CharEntityTable.keys * '|'}));/
14
- TextDecorationTable = { 'underline' => :underline, 'line-through' => :strikethrough }
15
- #DummyText = %(\u0000)
16
-
17
- def initialize(options = {})
18
- @merge_adjacent_text_nodes = options[:merge_adjacent_text_nodes]
19
- # TODO add support for character spacing
20
- if (theme = options[:theme])
21
- @link_font_settings = {
22
- color: theme.link_font_color,
23
- font: theme.link_font_family,
24
- size: theme.link_font_size,
25
- styles: to_styles(theme.link_font_style, theme.link_text_decoration)
26
- }.select! {|_, val| val }
27
- @monospaced_font_settings = {
28
- color: theme.literal_font_color,
29
- font: theme.literal_font_family,
30
- size: theme.literal_font_size,
31
- styles: to_styles(theme.literal_font_style)
32
- }.select! {|_, val| val }
33
- else
34
- @link_font_settings = { color: '0000FF' }
35
- @monospaced_font_settings = { font: 'Courier', size: 0.9 }
36
- end
37
- end
38
-
39
- # FIXME pass styles downwards to child elements rather than decorating on way out of hierarchy
40
- def apply(parsed)
41
- fragments = []
42
- previous_fragment_is_text = false
43
- # NOTE we use each since using inject is slower than a manual loop
44
- parsed.each do |node|
45
- case node[:type]
46
- when :element
47
- # case 1: non-void element
48
- if node.key?(:pcdata)
49
- unless (pcdata = node[:pcdata]).empty?
50
- tag_name = node[:name]
51
- attributes = node[:attributes]
52
- # NOTE decorate child fragments with styles from this element
53
- fragments << apply(pcdata).map {|fragment| build_fragment(fragment, tag_name, attributes) }
54
- previous_fragment_is_text = false
55
- # NOTE skip element if it has no children
56
- #else
57
- # # NOTE handle an empty anchor element (i.e., <a ...></a>)
58
- # if (tag_name = node[:name]) == :a
59
- # fragments << build_fragment({ text: DummyText }, tag_name, node[:attributes])
60
- # previous_fragment_is_text = false
61
- # end
62
- end
63
- # case 2: void element
64
- else
65
- case node[:name]
66
- when :br
67
- if @merge_adjacent_text_nodes && previous_fragment_is_text
68
- fragments << { text: %(#{fragments.pop[:text]}#{LF}) }
69
- else
70
- fragments << { text: LF }
71
- end
72
- previous_fragment_is_text = true
73
- when :img
74
- attributes = node[:attributes]
75
- fragment = {
76
- image_path: attributes[:tmp] == 'true' ? attributes[:src].extend(TemporaryPath) : attributes[:src],
77
- image_format: attributes[:format],
78
- text: attributes[:alt],
79
- callback: InlineImageRenderer
80
- }
81
- if (img_w = attributes[:width])
82
- fragment[:image_width] = img_w
83
- end
84
- fragments << fragment
85
- previous_fragment_is_text = false
86
- end
87
- end
88
- when :text
89
- text = node[:value]
90
- # NOTE the remaining logic is shared with :entity
91
- if @merge_adjacent_text_nodes && previous_fragment_is_text
92
- fragments << { text: %(#{fragments.pop[:text]}#{text}) }
93
- else
94
- fragments << { text: text }
95
- end
96
- previous_fragment_is_text = true
97
- when :entity
98
- if (name = node[:name])
99
- text = CharEntityTable[name]
100
- else
101
- # FIXME AFM fonts do not include a thin space glyph; set fallback_fonts to allow glyph to be resolved
102
- text = [node[:number]].pack('U*')
103
- end
104
- # NOTE the remaining logic is shared with :text
105
- if @merge_adjacent_text_nodes && previous_fragment_is_text
106
- fragments << { text: %(#{fragments.pop[:text]}#{text}) }
107
- else
108
- fragments << { text: text }
109
- end
110
- previous_fragment_is_text = true
111
- end
112
- end
113
- fragments.flatten
114
- end
115
-
116
- def build_fragment(fragment, tag_name, attrs = {})
117
- styles = (fragment[:styles] ||= ::Set.new)
118
- case tag_name
119
- when :strong
120
- styles << :bold
121
- when :em
122
- styles << :italic
123
- when :code
124
- # NOTE prefer old value, except for styles, which should be combined
125
- fragment.update(@monospaced_font_settings) {|k, old_v, new_v| k == :styles ? old_v.merge(new_v) : old_v }
126
- when :color
127
- if !fragment[:color]
128
- if (rgb = attrs[:rgb])
129
- case rgb.chr
130
- when '#'
131
- fragment[:color] = rgb[1..-1]
132
- when '['
133
- # treat value as CMYK array (e.g., "[50, 100, 0, 0]")
134
- fragment[:color] = rgb[1..-1].chomp(']').split(', ').map(&:to_i)
135
- # ...or we could honor an rgb array too
136
- #case (vals = rgb[1..-1].chomp(']').split(', ')).size
137
- #when 4
138
- # fragment[:color] = vals.map(&:to_i)
139
- #when 3
140
- # fragment[:color] = vals.map {|e| '%02X' % e.to_i }.join
141
- #end
142
- else
143
- fragment[:color] = rgb
144
- end
145
- # QUESTION should we even support r,g,b and c,m,y,k as individual values?
146
- elsif (r_val = attrs[:r]) && (g_val = attrs[:g]) && (b_val = attrs[:b])
147
- fragment[:color] = [r_val, g_val, b_val].map {|e| '%02X' % e.to_i }.join
148
- elsif (c_val = attrs[:c]) && (m_val = attrs[:m]) && (y_val = attrs[:y]) && (k_val = attrs[:k])
149
- fragment[:color] = [c_val.to_i, m_val.to_i, y_val.to_i, k_val.to_i]
150
- end
151
- end
152
- when :font
153
- if !fragment[:font] && (value = attrs[:name])
154
- fragment[:font] = value
155
- end
156
- if !fragment[:size] && (value = attrs[:size])
157
- # FIXME can we make this comparison more robust / accurate?
158
- if %(#{f_value = value.to_f}) == value || %(#{value.to_i}) == value
159
- fragment[:size] = f_value
160
- elsif value != '1em'
161
- fragment[:size] = value
162
- end
163
- end
164
- #if !fragment[:character_spacing] && (value = attrs[:character_spacing])
165
- # fragment[:character_spacing] = value.to_f
166
- #end
167
- when :a
168
- visible = true
169
- # a element can have no attributes, so short-circuit if that's the case
170
- unless attrs.empty?
171
- # NOTE href, anchor, and name are mutually exclusive; nesting is not supported
172
- if (value = attrs[:anchor])
173
- fragment[:anchor] = value
174
- elsif (value = attrs[:href])
175
- fragment[:link] = value.include?(';') ? value.gsub(CharRefRx) {
176
- $2 ? CharEntityTable[$2.to_sym] : [$1.to_i].pack('U*')
177
- } : value
178
- elsif (value = attrs[:name])
179
- # NOTE text is null character, which is used as placeholder text so Prawn doesn't drop fragment
180
- fragment[:name] = value
181
- if (type = attrs[:type])
182
- fragment[:type] = type.to_sym
183
- end
184
- fragment[:callback] = InlineDestinationMarker
185
- visible = false
186
- end
187
- end
188
- # NOTE prefer old value, except for styles, which should be combined
189
- fragment.update(@link_font_settings) {|k, old_v, new_v| k == :styles ? old_v.merge(new_v) : old_v } if visible
190
- when :sub
191
- styles << :subscript
192
- when :sup
193
- styles << :superscript
194
- when :del
195
- styles << :strikethrough
196
- when :span
197
- # NOTE spaces in style attribute value are superfluous, for our purpose; split drops record after trailing ;
198
- attrs[:style].tr(' ', '').split(';').each do |style|
199
- pname, pvalue = style.split(':', 2)
200
- case pname
201
- when 'color'
202
- # QUESTION should we check whether the value is a valid hex color?
203
- unless fragment[:color]
204
- case pvalue.length
205
- when 6
206
- fragment[:color] = pvalue
207
- when 7
208
- fragment[:color] = pvalue.slice(1, 6) if pvalue.start_with?('#')
209
- # QUESTION should we support the 3 character form?
210
- #when 3
211
- # fragment[:color] = pvalue.each_char.map {|c| c * 2 }.join
212
- #when 4
213
- # fragment[:color] = pvalue.slice(1, 3).each_char.map {|c| c * 2 }.join if pvalue.start_with?('#')
214
- end
215
- end
216
- when 'font-weight'
217
- if pvalue == 'bold'
218
- styles << :bold
219
- end
220
- when 'font-style'
221
- if pvalue == 'italic'
222
- styles << :italic
223
- end
224
- # TODO text-transform
225
- end
226
- end if attrs.key?(:style)
227
- end
228
- # TODO we could limit to select tags, but doesn't seem to really affect performance
229
- attrs[:class].split.each do |class_name|
230
- case class_name
231
- when 'underline'
232
- styles << :underline
233
- when 'line-through'
234
- styles << :strikethrough
235
- end
236
- end if attrs.key?(:class)
237
- fragment.delete(:styles) if styles.empty?
238
- fragment
239
- end
240
-
241
- def to_styles(font_style, text_decoration = nil)
242
- case font_style
243
- when 'bold'
244
- styles = [:bold].to_set
245
- when 'italic'
246
- styles = [:italic].to_set
247
- when 'bold_italic'
248
- styles = [:bold, :italic].to_set
249
- else
250
- styles = nil
251
- end
252
- if (style = TextDecorationTable[text_decoration])
253
- styles ? (styles << style) : [style].to_set
254
- else
255
- styles
256
- end
257
- end
258
- end
259
- end
260
- end
261
- end
@@ -1,63 +0,0 @@
1
- require 'asciidoctor/extensions'
2
-
3
- module Asciidoctor
4
- module Pdf
5
- # An include processor that skips the implicit author line below
6
- # the document title within include documents.
7
- class ImplicitHeaderProcessor < ::Asciidoctor::Extensions::IncludeProcessor
8
- def process doc, reader, target, attributes
9
- return reader unless File.exist? target
10
- ::File.open target, 'r' do |fd|
11
- # FIXME handle case where doc id is specified above title
12
- if (first_line = fd.readline) && (first_line.start_with? '= ')
13
- # HACK reset counters for each article for Editions
14
- if doc.attr? 'env', 'editions'
15
- doc.counters.each do |counter_key, counter_val|
16
- doc.attributes.delete counter_key
17
- end
18
- doc.counters.clear
19
- end
20
- if (second_line = fd.readline)
21
- if AuthorInfoLineRx =~ second_line
22
- # FIXME temporary hack to set author and e-mail attributes; this should handle all attributes in header!
23
- author = [$1, $2, $3].compact * ' '
24
- email = $4
25
- reader.push_include fd.readlines, target, target, 3, attributes unless fd.eof?
26
- reader.push_include first_line, target, target, 1, attributes
27
- lines = [%(:author: #{author})]
28
- lines << %(:email: #{email}) if email
29
- reader.push_include lines, target, target, 2, attributes
30
- else
31
- lines = [second_line]
32
- lines += fd.readlines unless fd.eof?
33
- reader.push_include lines, target, target, 2, attributes
34
- reader.push_include first_line, target, target, 1, attributes
35
- end
36
- else
37
- reader.push_include first_line, target, target, 1, attributes
38
- end
39
- else
40
- lines = [first_line]
41
- lines += fd.readlines unless fd.eof?
42
- reader.push_include lines, target, target, 1, attributes
43
- end
44
- end
45
- reader
46
- end
47
-
48
- def handles? target
49
- # FIXME should not require this hack to skip processing bio
50
- !(target.end_with? 'bio.adoc') && ((target.end_with? '.adoc') || (target.end_with? '.asciidoc'))
51
- end
52
-
53
- # FIXME this method shouldn't be required
54
- def update_config config
55
- (@config ||= {}).update config
56
- end
57
- end
58
- end
59
- end
60
-
61
- Asciidoctor::Extensions.register :pdf do
62
- include_processor Asciidoctor::Pdf::ImplicitHeaderProcessor if @document.backend == 'pdf'
63
- end