asciidoctor-pdf 1.5.0.alpha.1
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 +7 -0
- data/LICENSE.adoc +22 -0
- data/NOTICE.adoc +76 -0
- data/README.adoc +263 -0
- data/Rakefile +78 -0
- data/bin/asciidoctor-pdf +15 -0
- data/bin/optimize-pdf +63 -0
- data/data/fonts/LICENSE-liberation-fonts-2.00.1 +102 -0
- data/data/fonts/LICENSE-mplus-testflight-58 +16 -0
- data/data/fonts/LICENSE-noto-fonts-2014-01-30 +201 -0
- data/data/fonts/liberationmono-bold-latin.ttf +0 -0
- data/data/fonts/liberationmono-bolditalic-latin.ttf +0 -0
- data/data/fonts/liberationmono-italic-latin.ttf +0 -0
- data/data/fonts/liberationmono-regular-latin.ttf +0 -0
- data/data/fonts/mplus1mn-bold-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-bolditalic-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-italic-ascii.ttf +0 -0
- data/data/fonts/mplus1mn-regular-ascii-conums.ttf +0 -0
- data/data/fonts/mplus1p-bold-latin.ttf +0 -0
- data/data/fonts/mplus1p-light-latin.ttf +0 -0
- data/data/fonts/mplus1p-regular-latin.ttf +0 -0
- data/data/fonts/mplus1p-regular-multilingual.ttf +0 -0
- 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
- data/data/themes/asciidoctor-theme.yml +174 -0
- data/data/themes/default-theme.yml +182 -0
- data/examples/chronicles.adoc +429 -0
- data/examples/chronicles.pdf +0 -0
- data/examples/example-pdf-screenshot.png +0 -0
- data/examples/example.adoc +27 -0
- data/examples/example.pdf +0 -0
- data/examples/sample-title-logo.jpg +0 -0
- data/examples/wolpertinger.jpg +0 -0
- data/lib/asciidoctor-pdf.rb +3 -0
- data/lib/asciidoctor-pdf/asciidoctor_ext.rb +1 -0
- data/lib/asciidoctor-pdf/asciidoctor_ext/section.rb +26 -0
- data/lib/asciidoctor-pdf/converter.rb +1365 -0
- data/lib/asciidoctor-pdf/core_ext/array.rb +5 -0
- data/lib/asciidoctor-pdf/core_ext/ostruct.rb +9 -0
- data/lib/asciidoctor-pdf/implicit_header_processor.rb +59 -0
- data/lib/asciidoctor-pdf/pdfmarks.rb +30 -0
- data/lib/asciidoctor-pdf/prawn_ext.rb +3 -0
- data/lib/asciidoctor-pdf/prawn_ext/coderay_encoder.rb +94 -0
- data/lib/asciidoctor-pdf/prawn_ext/extensions.rb +529 -0
- data/lib/asciidoctor-pdf/prawn_ext/formatted_text/formatter.rb +29 -0
- data/lib/asciidoctor-pdf/prawn_ext/formatted_text/parser.rb +1012 -0
- data/lib/asciidoctor-pdf/prawn_ext/formatted_text/parser.treetop +115 -0
- data/lib/asciidoctor-pdf/prawn_ext/formatted_text/transform.rb +178 -0
- data/lib/asciidoctor-pdf/roman_numeral.rb +107 -0
- data/lib/asciidoctor-pdf/theme_loader.rb +103 -0
- data/lib/asciidoctor-pdf/version.rb +5 -0
- metadata +248 -0
Binary file
|
Binary file
|
@@ -0,0 +1,27 @@
|
|
1
|
+
= Document Title
|
2
|
+
Doc Writer <doc@example.com>
|
3
|
+
:doctype: book
|
4
|
+
:source-highlighter: coderay
|
5
|
+
:listing-caption: Listing
|
6
|
+
|
7
|
+
A simple http://asciidoc.org[AsciiDoc] document.
|
8
|
+
|
9
|
+
== Introduction
|
10
|
+
|
11
|
+
A paragraph followed by a simple list with square bullets.
|
12
|
+
|
13
|
+
[square]
|
14
|
+
* item 1
|
15
|
+
* item 2
|
16
|
+
|
17
|
+
Here's how you say "`Hello, World!`" in Prawn:
|
18
|
+
|
19
|
+
.Create a basic PDF document using Prawn
|
20
|
+
[source,ruby]
|
21
|
+
----
|
22
|
+
require 'prawn'
|
23
|
+
|
24
|
+
Prawn::Document.generate 'example.pdf' do
|
25
|
+
text 'Hello, World!'
|
26
|
+
end
|
27
|
+
----
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
require_relative 'asciidoctor_ext/section'
|
@@ -0,0 +1,26 @@
|
|
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
|
+
%(Chapter #{@cached_numbered_title})
|
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 respond_to? :numbered_title
|
22
|
+
|
23
|
+
def chapter?
|
24
|
+
@document.doctype == 'book' && @level == 1 || (@special && @level == 0)
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,1365 @@
|
|
1
|
+
# TODO cleanup imports...decide what belongs in asciidoctor-pdf.rb
|
2
|
+
require_relative 'core_ext/array'
|
3
|
+
require 'prawn'
|
4
|
+
require 'prawn-svg'
|
5
|
+
require 'prawn/table'
|
6
|
+
require 'prawn/templates'
|
7
|
+
require_relative 'prawn_ext'
|
8
|
+
require_relative 'pdfmarks'
|
9
|
+
require_relative 'asciidoctor_ext'
|
10
|
+
require_relative 'implicit_header_processor'
|
11
|
+
require_relative 'theme_loader'
|
12
|
+
require_relative 'roman_numeral'
|
13
|
+
|
14
|
+
Asciidoctor::Extensions.register :pdf do
|
15
|
+
include_processor Asciidoctor::Pdf::ImplicitHeaderProcessor if @document.backend == 'pdf'
|
16
|
+
end
|
17
|
+
|
18
|
+
module Asciidoctor
|
19
|
+
module Pdf
|
20
|
+
class Converter < ::Prawn::Document
|
21
|
+
include ::Asciidoctor::Converter
|
22
|
+
include ::Asciidoctor::Writer
|
23
|
+
include ::Asciidoctor::Prawn::Extensions
|
24
|
+
|
25
|
+
register_for 'pdf'
|
26
|
+
|
27
|
+
def self.unicode_char number
|
28
|
+
[number].pack 'U*'
|
29
|
+
end
|
30
|
+
|
31
|
+
IndentationRx = /^ +/
|
32
|
+
TabSpaces = ' ' * 4
|
33
|
+
NoBreakSpace = unicode_char 0x00a0
|
34
|
+
NarrowNoBreakSpace = unicode_char 0x202f
|
35
|
+
HairSpace = unicode_char 0x200a
|
36
|
+
DotLeader = %(#{HairSpace}.)
|
37
|
+
EmDash = unicode_char 0x2014
|
38
|
+
LowercaseGreekA = unicode_char 0x03b1
|
39
|
+
AdmonitionIcons = {
|
40
|
+
note: (unicode_char 0xf0eb)
|
41
|
+
}
|
42
|
+
Bullets = {
|
43
|
+
disc: (unicode_char 0x2022),
|
44
|
+
circle: (unicode_char 0x25e6),
|
45
|
+
square: (unicode_char 0x25aa)
|
46
|
+
}
|
47
|
+
BuiltInEntityChars = {
|
48
|
+
'<' => '<',
|
49
|
+
'>' => '>',
|
50
|
+
'&' => '&'
|
51
|
+
}
|
52
|
+
BuiltInEntityCharsRx = /(?:#{BuiltInEntityChars.keys * '|'})/
|
53
|
+
ImageAttributeValueRx = /^image:{1,2}(.*?)\[(.*?)\]$/
|
54
|
+
|
55
|
+
def initialize backend, opts
|
56
|
+
super
|
57
|
+
basebackend 'html'
|
58
|
+
outfilesuffix '.pdf'
|
59
|
+
#htmlsyntax 'xml'
|
60
|
+
@list_numbers = []
|
61
|
+
@list_bullets = []
|
62
|
+
end
|
63
|
+
|
64
|
+
def convert node, name = nil
|
65
|
+
method_name = %(convert_#{name ||= node.node_name})
|
66
|
+
result = nil
|
67
|
+
if respond_to? method_name
|
68
|
+
# NOTE we prepend the prefix "convert_" to avoid conflict with Prawn methods
|
69
|
+
result = send method_name, node
|
70
|
+
else
|
71
|
+
# TODO delegate to convert_method_missing
|
72
|
+
warn %(asciidoctor: WARNING: conversion missing in backend #{@backend} for #{name})
|
73
|
+
end
|
74
|
+
# NOTE inline nodes generate pseudo-HTML strings; the remainder write directly to PDF object
|
75
|
+
(node.is_a? ::Asciidoctor::Inline) ? result : self
|
76
|
+
end
|
77
|
+
|
78
|
+
def convert_content_for_block node, opts = {}
|
79
|
+
if self != (prev_converter = node.document.converter)
|
80
|
+
node.document.instance_variable_set :@converter, self
|
81
|
+
else
|
82
|
+
prev_converter = nil
|
83
|
+
end
|
84
|
+
if node.blocks?
|
85
|
+
node.content
|
86
|
+
elsif node.content_model != :compound && (string = node.content)
|
87
|
+
# TODO this content could be catched on repeat invocations!
|
88
|
+
layout_prose string, opts
|
89
|
+
end
|
90
|
+
node.document.instance_variable_set :@converter, prev_converter if prev_converter
|
91
|
+
end
|
92
|
+
|
93
|
+
def convert_document doc
|
94
|
+
init_pdf doc
|
95
|
+
# data-uri doesn't apply to PDF, so explicitly disable (is there a better place?)
|
96
|
+
doc.attributes.delete 'data-uri'
|
97
|
+
|
98
|
+
# TODO implement page_background_image as alternative and/or page_watermark_image
|
99
|
+
if (bg_color = @theme.page_background_color) && !(['transparent', 'FFFFFF'].include? bg_color.to_s)
|
100
|
+
on_page_create do
|
101
|
+
canvas do
|
102
|
+
fill_bounds bg_color.to_s
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
layout_cover_page :front, doc
|
108
|
+
layout_title_page doc
|
109
|
+
|
110
|
+
start_new_page
|
111
|
+
font @theme.base_font_family, size: @theme.base_font_size
|
112
|
+
convert_content_for_block doc
|
113
|
+
|
114
|
+
num_toc_levels = (doc.attr 'toclevels', 2).to_i
|
115
|
+
toc_page_nums = if doc.attr? 'toc'
|
116
|
+
layout_toc doc, num_toc_levels
|
117
|
+
else
|
118
|
+
(0..-1)
|
119
|
+
end
|
120
|
+
|
121
|
+
# TODO enable pagenums by default (perhaps upstream?)
|
122
|
+
stamp_page_numbers skip: (toc_page_nums.to_a.size + 1) if doc.attr 'pagenums'
|
123
|
+
add_outline doc, num_toc_levels, toc_page_nums
|
124
|
+
catalog.data[:ViewerPreferences] = [:FitWindow]
|
125
|
+
|
126
|
+
layout_cover_page :back, doc
|
127
|
+
|
128
|
+
# NOTE we have to init pdfmarks here while we have a reference to the doc
|
129
|
+
@pdfmarks = Pdfmarks.new doc
|
130
|
+
end
|
131
|
+
|
132
|
+
# NOTE embedded only makes sense if perhaps we are building
|
133
|
+
# on an existing Prawn::Document instance; for now, just treat
|
134
|
+
# it the same as a full document.
|
135
|
+
alias :convert_embedded :convert_document
|
136
|
+
|
137
|
+
# TODO only allow method to be called once (or we need a reset)
|
138
|
+
def init_pdf doc
|
139
|
+
theme = ThemeLoader.load_theme doc.attr('pdf-style'), doc.attr('pdf-stylesdir')
|
140
|
+
pdf_opts = (build_pdf_options doc, theme)
|
141
|
+
::Prawn::Document.instance_method(:initialize).bind(self).call pdf_opts
|
142
|
+
# QUESTION should ThemeLoader register fonts?
|
143
|
+
register_fonts theme.font_catalog, (doc.attr 'scripts', 'latin')
|
144
|
+
@theme = theme
|
145
|
+
@font_color = theme.base_font_color
|
146
|
+
init_scratch_prototype
|
147
|
+
self
|
148
|
+
end
|
149
|
+
|
150
|
+
def build_pdf_options doc, theme
|
151
|
+
pdf_opts = {
|
152
|
+
#compress: true,
|
153
|
+
#optimize_objects: true,
|
154
|
+
info: (build_pdf_info doc),
|
155
|
+
margin: (theme.page_margin || 36),
|
156
|
+
page_layout: (theme.page_layout || :portrait).to_sym,
|
157
|
+
page_size: (theme.page_size || 'LETTER').upcase,
|
158
|
+
skip_page_creation: true,
|
159
|
+
}
|
160
|
+
# FIXME fix the namespace for FormattedTextFormatter
|
161
|
+
pdf_opts[:text_formatter] ||= ::Asciidoctor::Prawn::FormattedTextFormatter.new theme: theme
|
162
|
+
pdf_opts
|
163
|
+
end
|
164
|
+
|
165
|
+
def build_pdf_info doc
|
166
|
+
info = {}
|
167
|
+
# TODO create helper method for creating literal PDF string
|
168
|
+
info[:Title] = ::PDF::Core::LiteralString.new(doc.doctitle sanitize: true, use_fallback: true)
|
169
|
+
if doc.attr? 'authors'
|
170
|
+
info[:Author] = ::PDF::Core::LiteralString.new(doc.attr 'authors')
|
171
|
+
end
|
172
|
+
if doc.attr? 'subject'
|
173
|
+
info[:Subject] = ::PDF::Core::LiteralString.new(doc.attr 'subject')
|
174
|
+
end
|
175
|
+
if doc.attr? 'keywords'
|
176
|
+
info[:Keywords] = ::PDF::Core::LiteralString.new(doc.attr 'keywords')
|
177
|
+
end
|
178
|
+
if (doc.attr? 'publisher')
|
179
|
+
info[:Producer] = ::PDF::Core::LiteralString.new(doc.attr 'publisher')
|
180
|
+
end
|
181
|
+
info[:Creator] = ::PDF::Core::LiteralString.new %(Asciidoctor PDF #{::Asciidoctor::Pdf::VERSION}, based on Prawn #{::Prawn::VERSION})
|
182
|
+
info[:Producer] ||= (info[:Author] || info[:Creator])
|
183
|
+
# FIXME use docdate attribute
|
184
|
+
info[:ModDate] = info[:CreationDate] = ::Time.now
|
185
|
+
info
|
186
|
+
end
|
187
|
+
|
188
|
+
def convert_section sect, opts = {}
|
189
|
+
heading_level = sect.level + 1
|
190
|
+
theme_font :heading, level: heading_level do
|
191
|
+
title = sect.numbered_title formal: true
|
192
|
+
unless at_page_top?
|
193
|
+
if sect.chapter?
|
194
|
+
start_new_chapter sect
|
195
|
+
# FIXME smarter calculation here!!
|
196
|
+
elsif cursor < (height_of title) + @theme.heading_margin_top + @theme.heading_margin_bottom + @theme.base_line_height_length * 1.5
|
197
|
+
start_new_page
|
198
|
+
end
|
199
|
+
end
|
200
|
+
# QUESTION should we store page_start & destination in internal map?
|
201
|
+
sect.set_attr 'page_start', page_number
|
202
|
+
dest_y = at_page_top? ? page_height : y
|
203
|
+
sect.set_attr 'destination', (sect_destination = (dest_xyz 0, dest_y))
|
204
|
+
add_dest sect.id, sect_destination
|
205
|
+
sect.chapter? ? (layout_chapter_title sect, title) : (layout_heading title)
|
206
|
+
end
|
207
|
+
|
208
|
+
convert_content_for_block sect
|
209
|
+
sect.set_attr 'page_end', page_number
|
210
|
+
end
|
211
|
+
|
212
|
+
def convert_floating_title node
|
213
|
+
theme_font :heading, level: (node.level + 1) do
|
214
|
+
layout_heading node.title
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def convert_abstract node
|
219
|
+
pad_box @theme.abstract_padding do
|
220
|
+
theme_font :abstract do
|
221
|
+
# FIXME control first_line_options using theme
|
222
|
+
prose_opts = { line_height: @theme.abstract_line_height, first_line_options: { styles: [font_style, :bold] } }
|
223
|
+
# FIXME make this cleaner!!
|
224
|
+
if node.blocks?
|
225
|
+
node.blocks.each do |child|
|
226
|
+
# FIXME is playback necessary here?
|
227
|
+
child.document.playback_attributes child.attributes
|
228
|
+
if child.context == :paragraph
|
229
|
+
layout_prose child.content, prose_opts
|
230
|
+
prose_opts.delete :first_line_options
|
231
|
+
else
|
232
|
+
# FIXME this could do strange things if the wrong kind of content shows up
|
233
|
+
convert_content_for_block child
|
234
|
+
end
|
235
|
+
end
|
236
|
+
elsif node.content_model != :compound && (string = node.content)
|
237
|
+
layout_prose string, prose_opts
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
# QUESTION should we be adding margin below the abstract??
|
242
|
+
#move_down @theme.block_margin_bottom
|
243
|
+
#theme_margin :block, :bottom
|
244
|
+
end
|
245
|
+
|
246
|
+
def convert_preamble node
|
247
|
+
# FIXME should only use lead for first paragraph
|
248
|
+
# add lead role to first paragraph then delegate to convert_content_for_block
|
249
|
+
theme_font :lead do
|
250
|
+
convert_content_for_block node
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# TODO add prose around image logic (use role to add special logic for headshot)
|
255
|
+
def convert_paragraph node
|
256
|
+
is_lead = false
|
257
|
+
prose_opts = {}
|
258
|
+
node.roles.each do |role|
|
259
|
+
case role
|
260
|
+
when 'text-left'
|
261
|
+
prose_opts[:align] = :left
|
262
|
+
when 'text-right'
|
263
|
+
prose_opts[:align] = :right
|
264
|
+
when 'text-justify'
|
265
|
+
prose_opts[:align] = :justify
|
266
|
+
when 'lead'
|
267
|
+
is_lead = true
|
268
|
+
#when 'signature'
|
269
|
+
# prose_opts[:size] = @theme.base_font_size_small
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
if is_lead
|
274
|
+
theme_font :lead do
|
275
|
+
layout_prose node.content, prose_opts
|
276
|
+
end
|
277
|
+
else
|
278
|
+
layout_prose node.content, prose_opts
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
# FIXME alignment of content is off
|
283
|
+
def convert_admonition node
|
284
|
+
#move_down @theme.block_margin_top unless at_page_top?
|
285
|
+
theme_margin :block, :top
|
286
|
+
keep_together do |box_height = nil|
|
287
|
+
#theme_font :admonition do
|
288
|
+
label = node.caption.upcase
|
289
|
+
label_width = width_of label
|
290
|
+
# FIXME use padding from theme
|
291
|
+
indent @theme.horizontal_rhythm, @theme.horizontal_rhythm do
|
292
|
+
if box_height
|
293
|
+
float do
|
294
|
+
bounding_box [0, cursor], width: label_width + @theme.horizontal_rhythm, height: box_height do
|
295
|
+
# IMPORTANT the label must fit in the alotted space or it shows up on another page!
|
296
|
+
# QUESTION anyway to prevent text overflow in the case it doesn't fit?
|
297
|
+
stroke_vertical_rule @theme.admonition_border_color, at: bounds.width
|
298
|
+
# HACK make title in this location look right
|
299
|
+
label_margin_top = node.title? ? @theme.caption_margin_inside : 0
|
300
|
+
layout_prose label, valign: :center, style: :bold, line_height: 1, margin_top: label_margin_top, margin_bottom: 0
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
indent label_width + @theme.horizontal_rhythm * 2 do
|
305
|
+
layout_caption node.title if node.title?
|
306
|
+
convert_content_for_block node
|
307
|
+
# HACK compensate for margin bottom of admonition content
|
308
|
+
move_up(@theme.prose_margin_bottom || @theme.vertical_rhythm)
|
309
|
+
end
|
310
|
+
end
|
311
|
+
#end
|
312
|
+
end
|
313
|
+
#move_down @theme.block_margin_bottom
|
314
|
+
theme_margin :block, :bottom
|
315
|
+
end
|
316
|
+
|
317
|
+
def convert_example node
|
318
|
+
#move_down @theme.block_margin_top unless at_page_top?
|
319
|
+
theme_margin :block, :top
|
320
|
+
keep_together do |box_height = nil|
|
321
|
+
caption_height = node.title? ? (layout_caption node) : 0
|
322
|
+
if box_height
|
323
|
+
float do
|
324
|
+
bounding_box [0, cursor], width: bounds.width, height: box_height - caption_height do
|
325
|
+
theme_fill_and_stroke_bounds :example
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
pad_box [@theme.vertical_rhythm, @theme.horizontal_rhythm, 0, @theme.horizontal_rhythm] do
|
330
|
+
theme_font :example do
|
331
|
+
convert_content_for_block node
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
#move_down @theme.block_margin_bottom
|
336
|
+
theme_margin :block, :bottom
|
337
|
+
end
|
338
|
+
|
339
|
+
def convert_open node
|
340
|
+
case node.style
|
341
|
+
when 'abstract'
|
342
|
+
convert_abstract node
|
343
|
+
when 'partintro'
|
344
|
+
# FIXME cuts off any content beyond first paragraph!!
|
345
|
+
if node.blocks.size == 1 && node.blocks.first.style == 'abstract'
|
346
|
+
convert_abstract node.blocks.first
|
347
|
+
else
|
348
|
+
convert_content_for_block node
|
349
|
+
end
|
350
|
+
else
|
351
|
+
convert_content_for_block node
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def convert_quote_or_verse node
|
356
|
+
border_width = @theme.blockquote_border_width
|
357
|
+
#move_down @theme.block_margin_top unless at_page_top?
|
358
|
+
theme_margin :block, :top
|
359
|
+
keep_together do |box_height = nil|
|
360
|
+
start_cursor = cursor
|
361
|
+
# FIXME use padding from theme
|
362
|
+
pad_box [@theme.vertical_rhythm / 2.0, @theme.horizontal_rhythm, -(@theme.vertical_rhythm / 2.0), @theme.horizontal_rhythm + border_width / 2.0] do
|
363
|
+
theme_font :blockquote do
|
364
|
+
if node.context == :quote
|
365
|
+
convert_content_for_block node
|
366
|
+
else # verse
|
367
|
+
layout_prose node.content, preserve: true, normalize: false, align: :left
|
368
|
+
end
|
369
|
+
end
|
370
|
+
theme_font :blockquote_cite do
|
371
|
+
if node.attr? 'attribution'
|
372
|
+
layout_prose %(#{EmDash} #{[(node.attr 'attribution'), (node.attr 'citetitle')].compact * ', '}), align: :left, normalize: false
|
373
|
+
end
|
374
|
+
end
|
375
|
+
end
|
376
|
+
if box_height
|
377
|
+
# QUESTION should we use bounding_box + stroke_vertical_rule instead?
|
378
|
+
save_graphics_state do
|
379
|
+
stroke_color @theme.blockquote_border_color
|
380
|
+
line_width border_width
|
381
|
+
stroke_vertical_line cursor, start_cursor, at: border_width / 2.0
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
#move_down @theme.block_margin_bottom
|
386
|
+
theme_margin :block, :bottom
|
387
|
+
end
|
388
|
+
|
389
|
+
alias :convert_quote :convert_quote_or_verse
|
390
|
+
alias :convert_verse :convert_quote_or_verse
|
391
|
+
|
392
|
+
def convert_sidebar node
|
393
|
+
#move_down @theme.block_margin_top unless at_page_top?
|
394
|
+
theme_margin :block, :top
|
395
|
+
keep_together do |box_height = nil|
|
396
|
+
if box_height
|
397
|
+
float do
|
398
|
+
bounding_box [0, cursor], width: bounds.width, height: box_height do
|
399
|
+
theme_fill_and_stroke_bounds :sidebar
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
403
|
+
pad_box @theme.block_padding do
|
404
|
+
if node.title?
|
405
|
+
theme_font :sidebar_title do
|
406
|
+
# QUESTION should we allow margins of sidebar title to be customized?
|
407
|
+
layout_heading node.title, align: @theme.sidebar_title_align.to_sym, margin_top: 0
|
408
|
+
end
|
409
|
+
end
|
410
|
+
theme_font :sidebar do
|
411
|
+
convert_content_for_block node
|
412
|
+
end
|
413
|
+
# HACK compensate for margin bottom of sidebar content
|
414
|
+
move_up(@theme.prose_margin_bottom || @theme.vertical_rhythm)
|
415
|
+
end
|
416
|
+
end
|
417
|
+
#move_down @theme.block_margin_bottom
|
418
|
+
theme_margin :block, :bottom
|
419
|
+
end
|
420
|
+
|
421
|
+
def convert_colist node
|
422
|
+
# HACK undo the margin below the listing
|
423
|
+
move_up ((@theme.block_margin_bottom || @theme.vertical_rhythm) * 0.5)
|
424
|
+
@list_numbers ||= []
|
425
|
+
# FIXME move \u2460 to constant (or theme setting)
|
426
|
+
@list_numbers << %(\u2460)
|
427
|
+
#stroke_horizontal_rule @theme.caption_border_bottom_color
|
428
|
+
# HACK fudge spacing around colist a bit; each item is shifted up by this amount (see convert_list_item)
|
429
|
+
move_down ((@theme.prose_margin_bottom || @theme.vertical_rhythm) * 0.5)
|
430
|
+
convert_outline_list node
|
431
|
+
@list_numbers.pop
|
432
|
+
end
|
433
|
+
|
434
|
+
def convert_dlist node
|
435
|
+
node.items.each do |terms, desc|
|
436
|
+
terms = [*terms]
|
437
|
+
# NOTE don't orphan the terms, allow for at least one line of content
|
438
|
+
# FIXME extract ensure_space (or similar) method
|
439
|
+
start_new_page if cursor < @theme.base_line_height_length * (terms.size + 1)
|
440
|
+
terms.each do |term|
|
441
|
+
layout_prose term.text, style: @theme.description_list_term_font_style.to_sym, margin_top: 0, margin_bottom: (@theme.vertical_rhythm / 3.0), align: :left
|
442
|
+
end
|
443
|
+
if desc
|
444
|
+
indent @theme.description_list_description_indent do
|
445
|
+
convert_content_for_list_item desc
|
446
|
+
end
|
447
|
+
end
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
def convert_olist node
|
452
|
+
@list_numbers ||= []
|
453
|
+
list_number = case node.style
|
454
|
+
when 'arabic'
|
455
|
+
'1'
|
456
|
+
when 'decimal'
|
457
|
+
'01'
|
458
|
+
when 'loweralpha'
|
459
|
+
'a'
|
460
|
+
when 'upperalpha'
|
461
|
+
'A'
|
462
|
+
when 'lowerroman'
|
463
|
+
RomanNumeral.new 'i'
|
464
|
+
when 'upperroman'
|
465
|
+
RomanNumeral.new 'I'
|
466
|
+
when 'lowergreek'
|
467
|
+
LowercaseGreekA
|
468
|
+
else
|
469
|
+
'1'
|
470
|
+
end
|
471
|
+
if (skip = (node.attr 'start', 1).to_i - 1) > 0
|
472
|
+
skip.times { list_number = list_number.next }
|
473
|
+
end
|
474
|
+
@list_numbers << list_number
|
475
|
+
convert_outline_list node
|
476
|
+
@list_numbers.pop
|
477
|
+
end
|
478
|
+
|
479
|
+
# TODO implement checklist
|
480
|
+
def convert_ulist node
|
481
|
+
bullet_type = if (style = node.style)
|
482
|
+
case style
|
483
|
+
when 'bibliography'
|
484
|
+
:square
|
485
|
+
else
|
486
|
+
style.to_sym
|
487
|
+
end
|
488
|
+
else
|
489
|
+
case (node.level % 3)
|
490
|
+
when 1
|
491
|
+
:disc
|
492
|
+
when 2
|
493
|
+
:circle
|
494
|
+
when 0
|
495
|
+
:square
|
496
|
+
end
|
497
|
+
end
|
498
|
+
@list_bullets << Bullets[bullet_type]
|
499
|
+
convert_outline_list node
|
500
|
+
@list_bullets.pop
|
501
|
+
end
|
502
|
+
|
503
|
+
def convert_outline_list node
|
504
|
+
indent @theme.outline_list_indent do
|
505
|
+
node.items.each do |item|
|
506
|
+
convert_list_item item
|
507
|
+
end
|
508
|
+
end
|
509
|
+
# NOTE children will provide the necessary bottom margin
|
510
|
+
end
|
511
|
+
|
512
|
+
def convert_list_item node
|
513
|
+
# HACK quick hack to tighten items on colist
|
514
|
+
if node.parent.context == :colist
|
515
|
+
move_up ((@theme.prose_margin_bottom || @theme.vertical_rhythm) * 0.5)
|
516
|
+
end
|
517
|
+
|
518
|
+
# NOTE we need at least one line of content, so move down if we don't have it
|
519
|
+
# FIXME extract ensure_space (or similar) method
|
520
|
+
start_new_page if cursor < @theme.base_line_height_length
|
521
|
+
|
522
|
+
# TODO move this to a draw_bullet method
|
523
|
+
float do
|
524
|
+
bounding_box [-@theme.outline_list_indent, cursor], width: @theme.outline_list_indent do
|
525
|
+
label = case node.parent.context
|
526
|
+
when :ulist
|
527
|
+
@list_bullets.last
|
528
|
+
when :olist
|
529
|
+
@list_numbers << (index = @list_numbers.pop).next
|
530
|
+
%(#{index}.)
|
531
|
+
when :colist
|
532
|
+
@list_numbers << (index = @list_numbers.pop).next
|
533
|
+
# FIXME cleaner way to do numbers in colist; need more room around number
|
534
|
+
theme_font :conum do
|
535
|
+
# QUESTION should this be align: :left or :center?
|
536
|
+
layout_prose index, align: :left, line_height: @theme.conum_line_height, inline_format: false, margin: 0
|
537
|
+
end
|
538
|
+
next # short circuit label
|
539
|
+
end
|
540
|
+
layout_prose label, align: :center, normalize: false, inline_format: false, margin: 0
|
541
|
+
end
|
542
|
+
end
|
543
|
+
convert_content_for_list_item node
|
544
|
+
end
|
545
|
+
|
546
|
+
def convert_content_for_list_item node
|
547
|
+
if node.text?
|
548
|
+
opts = {}
|
549
|
+
opts[:align] = :left if node.parent.style == 'bibliography'
|
550
|
+
layout_prose node.text, opts
|
551
|
+
end
|
552
|
+
convert_content_for_block node
|
553
|
+
end
|
554
|
+
|
555
|
+
def convert_image node
|
556
|
+
#move_down @theme.block_margin_top unless at_page_top?
|
557
|
+
theme_margin :block, :top
|
558
|
+
target = node.attr 'target'
|
559
|
+
#if target.end_with? '.pdf'
|
560
|
+
# import_page target
|
561
|
+
# return
|
562
|
+
#end
|
563
|
+
|
564
|
+
# FIXME use normalize_path here!
|
565
|
+
image_path = File.join((node.attr 'docdir'), (node.attr 'imagesdir') || '', target)
|
566
|
+
# TODO extension should be an attribute on an image node
|
567
|
+
image_type = File.extname(image_path)[1..-1]
|
568
|
+
width = if node.attr? 'scaledwidth'
|
569
|
+
((node.attr 'scaledwidth').to_f / 100.0) * bounds.width
|
570
|
+
elsif image_type == 'svg'
|
571
|
+
bounds.width
|
572
|
+
elsif node.attr? 'width'
|
573
|
+
(node.attr 'width').to_f
|
574
|
+
else
|
575
|
+
bounds.width * (@theme.image_scaled_width_default || 0.75)
|
576
|
+
end
|
577
|
+
height = nil
|
578
|
+
position = ((node.attr 'align') || @theme.image_align_default || :left).to_sym
|
579
|
+
case image_type
|
580
|
+
when 'svg'
|
581
|
+
keep_together do
|
582
|
+
# HACK prawn-svg can't seem to center, so do it manually for now
|
583
|
+
left = case position
|
584
|
+
when :left
|
585
|
+
0
|
586
|
+
when :right
|
587
|
+
bounds.width - width
|
588
|
+
when :center
|
589
|
+
((bounds.width - width) / 2.0).floor
|
590
|
+
end
|
591
|
+
svg IO.read(image_path), at: [left, cursor], width: width, position: position
|
592
|
+
layout_caption node, position: :bottom if node.title?
|
593
|
+
end
|
594
|
+
else
|
595
|
+
begin
|
596
|
+
# FIXME temporary workaround to group caption & image
|
597
|
+
# Prawn doesn't provide access to rendered width and height before placing the
|
598
|
+
# image on the page
|
599
|
+
image_obj, image_info = build_image_object node.image_uri image_path
|
600
|
+
rendered_w, rendered_h = image_info.calc_image_dimensions width: width
|
601
|
+
caption_height = node.title? ?
|
602
|
+
(@theme.caption_margin_inside + @theme.caption_margin_outside + @theme.base_line_height_length) : 0
|
603
|
+
if cursor < rendered_h + caption_height
|
604
|
+
start_new_page
|
605
|
+
if cursor < rendered_h + caption_height
|
606
|
+
height = (cursor - caption_height).floor
|
607
|
+
width = ((rendered_w * height) / rendered_h).floor
|
608
|
+
# FIXME workaround to fix Prawn not adding fill and stroke commands
|
609
|
+
# on page that only has an image; breakage occurs when line numbers are added
|
610
|
+
fill_color self.fill_color
|
611
|
+
stroke_color self.stroke_color
|
612
|
+
end
|
613
|
+
end
|
614
|
+
embed_image image_obj, image_info, width: width, height: height, position: position
|
615
|
+
rescue => e
|
616
|
+
warn %(asciidoctor: WARNING: could not embed image; #{e.message})
|
617
|
+
return
|
618
|
+
end
|
619
|
+
layout_caption node, position: :bottom if node.title?
|
620
|
+
end
|
621
|
+
#move_down @theme.block_margin_bottom
|
622
|
+
theme_margin :block, :bottom
|
623
|
+
end
|
624
|
+
|
625
|
+
def convert_listing_or_literal node
|
626
|
+
# HACK disable built-in syntax highlighter; must be done before calling node.content!
|
627
|
+
if (node.style == 'source')
|
628
|
+
node.subs.delete :highlight
|
629
|
+
end
|
630
|
+
# FIXME highlighter freaks out about the non-breaking space characters
|
631
|
+
source_string = prepare_verbatim node.content
|
632
|
+
source_chunks = if node.context == :listing && (node.attr? 'language') && (node.attr? 'source-highlighter')
|
633
|
+
case node.attr 'source-highlighter'
|
634
|
+
when 'coderay'
|
635
|
+
# FIXME use autoload here!
|
636
|
+
require_relative 'prawn_ext/coderay_encoder' unless defined? ::Asciidoctor::Prawn::CodeRayEncoder
|
637
|
+
(::CodeRay.scan source_string, (node.attr 'language', 'text').to_sym).to_prawn
|
638
|
+
when 'pygments'
|
639
|
+
# FIXME use autoload here!
|
640
|
+
require 'pygments.rb' unless defined? ::Pygments
|
641
|
+
# FIXME if lexer is nil, we don't escape specialchars!
|
642
|
+
if (lexer = ::Pygments::Lexer[(node.attr 'language')])
|
643
|
+
pygments_config = { nowrap: true, noclasses: true, style: ((node.document.attr 'pygments-style') || 'pastie') }
|
644
|
+
result = lexer.highlight(source_string, options: pygments_config)
|
645
|
+
result = result.gsub(/(?: <span style="font-style: italic">(?:\/\/|#) <(?<num>\d+)><\/span>| <(?<num>\d+)>)$/) {
|
646
|
+
# FIXME move \u2460 to constant (or theme setting)
|
647
|
+
num = %(\u2460)
|
648
|
+
(($~[:num]).to_i - 1).times { num = num.next }
|
649
|
+
if (conum_color = @theme.conum_font_color)
|
650
|
+
%( <color rgb="#{conum_color}">#{num}</color>)
|
651
|
+
end
|
652
|
+
}
|
653
|
+
text_formatter.format result
|
654
|
+
end
|
655
|
+
end
|
656
|
+
end
|
657
|
+
source_chunks ||= [{ text: source_string }]
|
658
|
+
|
659
|
+
#move_down @theme.block_margin_top unless at_page_top?
|
660
|
+
theme_margin :block, :top
|
661
|
+
|
662
|
+
keep_together do |box_height = nil|
|
663
|
+
caption_height = node.title? ? (layout_caption node) : 0
|
664
|
+
theme_font :code do
|
665
|
+
if box_height
|
666
|
+
float do
|
667
|
+
bounding_box [0, cursor], width: bounds.width, height: box_height - caption_height do
|
668
|
+
theme_fill_and_stroke_bounds :code
|
669
|
+
end
|
670
|
+
end
|
671
|
+
end
|
672
|
+
|
673
|
+
pad_box @theme.code_padding do
|
674
|
+
typeset_formatted_text source_chunks, (calc_line_metrics @theme.code_line_height), color: @theme.code_font_color
|
675
|
+
end
|
676
|
+
end
|
677
|
+
end
|
678
|
+
stroke_horizontal_rule @theme.caption_border_bottom_color if node.title? && @theme.caption_border_bottom_color
|
679
|
+
|
680
|
+
#move_down @theme.block_margin_bottom
|
681
|
+
theme_margin :block, :bottom
|
682
|
+
end
|
683
|
+
|
684
|
+
alias :convert_listing :convert_listing_or_literal
|
685
|
+
alias :convert_literal :convert_listing_or_literal
|
686
|
+
|
687
|
+
def convert_table node
|
688
|
+
num_rows = 0
|
689
|
+
num_cols = node.columns.size
|
690
|
+
table_header = false
|
691
|
+
|
692
|
+
table_data = []
|
693
|
+
node.rows[:head].each do |rows|
|
694
|
+
table_header = true
|
695
|
+
num_rows += 1
|
696
|
+
row_data = []
|
697
|
+
rows.each do |cell|
|
698
|
+
row_data << {
|
699
|
+
content: cell.text,
|
700
|
+
text_color: (@theme.table_head_font_color || @font_color),
|
701
|
+
inline_format: true,
|
702
|
+
font_style: :bold,
|
703
|
+
colspan: cell.colspan || 1,
|
704
|
+
rowspan: cell.rowspan || 1,
|
705
|
+
align: (cell.attr 'halign').to_sym,
|
706
|
+
valign: (cell.attr 'valign').to_sym
|
707
|
+
}
|
708
|
+
end
|
709
|
+
table_data << row_data
|
710
|
+
end
|
711
|
+
|
712
|
+
node.rows[:body].each do |rows|
|
713
|
+
num_rows += 1
|
714
|
+
row_data = []
|
715
|
+
rows.each do |cell|
|
716
|
+
cell_data = {
|
717
|
+
content: cell.text,
|
718
|
+
text_color: (@theme.table_body_font_color || @font_color),
|
719
|
+
inline_format: true,
|
720
|
+
colspan: cell.colspan || 1,
|
721
|
+
rowspan: cell.rowspan || 1,
|
722
|
+
align: (cell.attr 'halign').to_sym,
|
723
|
+
valign: (cell.attr 'valign').to_sym
|
724
|
+
}
|
725
|
+
case cell.style
|
726
|
+
when :emphasis
|
727
|
+
cell_data[:font_style] = :italic
|
728
|
+
when :strong, :header
|
729
|
+
cell_data[:font_style] = :bold
|
730
|
+
when :monospaced
|
731
|
+
cell_data[:font] = @theme.literal_font_family
|
732
|
+
if (size = @theme.literal_font_size)
|
733
|
+
cell_data[:size] = size
|
734
|
+
end
|
735
|
+
if (color = @theme.literal_font_color)
|
736
|
+
cell_data[:text_color] = color
|
737
|
+
end
|
738
|
+
# TODO finish me
|
739
|
+
end
|
740
|
+
row_data << cell_data
|
741
|
+
end
|
742
|
+
table_data << row_data
|
743
|
+
end
|
744
|
+
|
745
|
+
# TODO support footer row
|
746
|
+
|
747
|
+
column_widths = node.columns.map {|col| ((col.attr 'colpcwidth') * bounds.width) / 100.0 }
|
748
|
+
|
749
|
+
border = {}
|
750
|
+
table_border_width = @theme.table_border_width
|
751
|
+
[:top, :bottom, :left, :right, :cols, :rows].each {|edge| border[edge] = table_border_width }
|
752
|
+
|
753
|
+
frame = (node.attr 'frame') || 'all'
|
754
|
+
grid = (node.attr 'grid') || 'all'
|
755
|
+
|
756
|
+
case grid
|
757
|
+
when 'cols'
|
758
|
+
border[:rows] = 0
|
759
|
+
when 'rows'
|
760
|
+
border[:cols] = 0
|
761
|
+
when 'none'
|
762
|
+
border[:rows] = border[:cols] = 0
|
763
|
+
end
|
764
|
+
|
765
|
+
case frame
|
766
|
+
when 'topbot'
|
767
|
+
border[:left] = border[:right] = 0
|
768
|
+
when 'sides'
|
769
|
+
border[:top] = border[:bottom] = 0
|
770
|
+
when 'none'
|
771
|
+
border[:top] = border[:right] = border[:bottom] = border[:left] = 0
|
772
|
+
end
|
773
|
+
|
774
|
+
table_settings = {
|
775
|
+
header: table_header,
|
776
|
+
cell_style: {
|
777
|
+
padding: @theme.table_cell_padding,
|
778
|
+
border_width: 0,
|
779
|
+
border_color: @theme.table_border_color
|
780
|
+
},
|
781
|
+
column_widths: column_widths,
|
782
|
+
row_colors: ['FFFFFF', @theme.table_background_color_alt]
|
783
|
+
}
|
784
|
+
|
785
|
+
theme_margin :block, :top
|
786
|
+
layout_caption node if node.title?
|
787
|
+
|
788
|
+
table table_data, table_settings do
|
789
|
+
if grid == 'none' && frame == 'none'
|
790
|
+
if table_header
|
791
|
+
rows(0).border_bottom_width = 1.5
|
792
|
+
end
|
793
|
+
else
|
794
|
+
# apply the grid setting first across all cells
|
795
|
+
cells.border_width = [border[:rows], border[:cols], border[:rows], border[:cols]]
|
796
|
+
|
797
|
+
if table_header
|
798
|
+
rows(0).border_bottom_width = 1.5
|
799
|
+
end
|
800
|
+
|
801
|
+
# top edge of table
|
802
|
+
rows(0).border_top_width = border[:top]
|
803
|
+
# right edge of table
|
804
|
+
columns(num_cols - 1).border_right_width = border[:right]
|
805
|
+
# bottom edge of table
|
806
|
+
rows(num_rows - 1).border_bottom_width = border[:bottom]
|
807
|
+
# left edge of table
|
808
|
+
columns(0).border_left_width = border[:left]
|
809
|
+
end
|
810
|
+
end
|
811
|
+
theme_margin :block, :bottom
|
812
|
+
end
|
813
|
+
|
814
|
+
def convert_thematic_break node
|
815
|
+
#move_down @theme.thematic_break_margin_top
|
816
|
+
theme_margin :thematic_break, :top
|
817
|
+
stroke_horizontal_rule @theme.thematic_break_border_color, line_width: @theme.thematic_break_border_width
|
818
|
+
#move_down @theme.thematic_break_margin_bottom
|
819
|
+
theme_margin :thematic_break, :bottom
|
820
|
+
end
|
821
|
+
|
822
|
+
# deprecated
|
823
|
+
alias :convert_horizontal_rule :convert_thematic_break
|
824
|
+
|
825
|
+
# NOTE can't alias to start_new_page since methods have different arity
|
826
|
+
def convert_page_break node
|
827
|
+
start_new_page unless at_page_top?
|
828
|
+
end
|
829
|
+
|
830
|
+
def convert_inline_anchor node
|
831
|
+
target = node.target
|
832
|
+
case node.type
|
833
|
+
when :xref
|
834
|
+
refid = (node.attr 'refid') || target
|
835
|
+
# NOTE we lookup text in converter because DocBook doesn't need this logic
|
836
|
+
if (text = node.text || (node.document.references[:ids][refid] || %([#{refid}])))
|
837
|
+
# FIXME shouldn't target be refid? logic seems confused here
|
838
|
+
%(<link anchor="#{target}">#{text}</link>)
|
839
|
+
# FIXME hack for bibliography references
|
840
|
+
# should be able to reenable once we parse inline destinations
|
841
|
+
else
|
842
|
+
%((see [#{refid}]))
|
843
|
+
end
|
844
|
+
when :ref
|
845
|
+
#%(<a id="#{target}"></a>)
|
846
|
+
''
|
847
|
+
when :bibref
|
848
|
+
#%(<a id="#{target}"></a>[#{target}])
|
849
|
+
%([#{target}])
|
850
|
+
when :link
|
851
|
+
attrs = []
|
852
|
+
#attrs << %( id="#{node.id}") if node.id
|
853
|
+
if (role = node.role)
|
854
|
+
attrs << %( class="#{role}")
|
855
|
+
end
|
856
|
+
#attrs << %( title="#{node.attr 'title'}") if node.attr? 'title'
|
857
|
+
attrs << %( target="#{node.attr 'window'}") if node.attr? 'window'
|
858
|
+
if (node.document.attr? 'showlinks') && !(node.has_role? 'bare')
|
859
|
+
# TODO cleanup look, perhaps put target in smaller text
|
860
|
+
%(<link href="#{target}"#{attrs.join}>#{node.text}</a> (#{target}))
|
861
|
+
else
|
862
|
+
%(<link href="#{target}"#{attrs.join}>#{node.text}</a>)
|
863
|
+
end
|
864
|
+
else
|
865
|
+
warn %(asciidoctor: WARNING: unknown anchor type: #{node.type.inspect})
|
866
|
+
end
|
867
|
+
end
|
868
|
+
|
869
|
+
def convert_inline_break node
|
870
|
+
%(#{node.text}<br>)
|
871
|
+
end
|
872
|
+
|
873
|
+
def convert_inline_button node
|
874
|
+
%(<b>[#{NarrowNoBreakSpace}#{node.text}#{NarrowNoBreakSpace}]</b>)
|
875
|
+
end
|
876
|
+
|
877
|
+
def convert_inline_footnote node
|
878
|
+
if (index = node.attr 'index')
|
879
|
+
#text = node.document.footnotes.find {|fn| fn.index == index }.text
|
880
|
+
%( [#{node.text}])
|
881
|
+
elsif node.type == :xref
|
882
|
+
%( <color rgb="FF0000">[#{node.text}]</color>)
|
883
|
+
end
|
884
|
+
end
|
885
|
+
|
886
|
+
def convert_inline_kbd node
|
887
|
+
if (keys = node.attr 'keys').size == 1
|
888
|
+
%(<code>#{keys[0]}</code>)
|
889
|
+
else
|
890
|
+
keys.map {|key| %(<code>#{key}</code>+) }.join.chop
|
891
|
+
end
|
892
|
+
end
|
893
|
+
|
894
|
+
def convert_inline_menu node
|
895
|
+
menu = node.attr 'menu'
|
896
|
+
if !(submenus = node.attr 'submenus').empty?
|
897
|
+
%(<strong>#{[menu, *submenus, (node.attr 'menuitem')] * ' | '}</strong>)
|
898
|
+
elsif (menuitem = node.attr 'menuitem')
|
899
|
+
%(<strong>#{menu} | #{menuitem}</strong>)
|
900
|
+
else
|
901
|
+
%(<strong>#{menu}</strong>)
|
902
|
+
end
|
903
|
+
end
|
904
|
+
|
905
|
+
def convert_inline_quoted node
|
906
|
+
case node.type
|
907
|
+
when :emphasis
|
908
|
+
open, close, is_tag = ['<em>', '</em>', true]
|
909
|
+
when :strong
|
910
|
+
open, close, is_tag = ['<strong>', '</strong>', true]
|
911
|
+
when :monospaced
|
912
|
+
open, close, is_tag = ['<code>', '</code>', true]
|
913
|
+
when :superscript
|
914
|
+
open, close, is_tag = ['<sup>', '</sup>', true]
|
915
|
+
when :subscript
|
916
|
+
open, close, is_tag = ['<sub>', '</sub>', true]
|
917
|
+
when :double
|
918
|
+
open, close, is_tag = ['“', '”', false]
|
919
|
+
when :single
|
920
|
+
open, close, is_tag = ['‘', '’', false]
|
921
|
+
#when :asciimath, :latexmath
|
922
|
+
else
|
923
|
+
open, close, is_tag = [nil, nil, false]
|
924
|
+
end
|
925
|
+
|
926
|
+
if (role = node.role)
|
927
|
+
if is_tag
|
928
|
+
quoted_text = %(#{open.chop} class="#{role}">#{node.text}#{close})
|
929
|
+
else
|
930
|
+
quoted_text = %(<span class="#{role}">#{open}#{node.text}#{close}</span>)
|
931
|
+
end
|
932
|
+
else
|
933
|
+
quoted_text = %(#{open}#{node.text}#{close})
|
934
|
+
end
|
935
|
+
|
936
|
+
node.id ? %(<a id="#{node.id}"></a>#{quoted_text}) : quoted_text
|
937
|
+
end
|
938
|
+
|
939
|
+
def layout_title_page doc
|
940
|
+
return unless doc.header? && !doc.noheader && !doc.notitle
|
941
|
+
|
942
|
+
start_new_page
|
943
|
+
# IMPORTANT this is the first page created, so we need to set the base font
|
944
|
+
font @theme.base_font_family, size: @theme.base_font_size
|
945
|
+
|
946
|
+
# TODO treat title-logo like front and back cover images
|
947
|
+
if doc.attr? 'title-logo'
|
948
|
+
# FIXME theme setting
|
949
|
+
move_down @theme.vertical_rhythm * 2
|
950
|
+
# FIXME add API to Asciidoctor for creating blocks like this (extract from extensions module?)
|
951
|
+
image = ::Asciidoctor::Block.new doc, :image, content_model: :empty
|
952
|
+
attrs = { 'target' => (doc.attr 'title-logo'), 'align' => 'center' }
|
953
|
+
image.update_attributes attrs
|
954
|
+
convert_image image
|
955
|
+
# FIXME theme setting
|
956
|
+
move_down @theme.vertical_rhythm * 4
|
957
|
+
end
|
958
|
+
|
959
|
+
# FIXME only create title page if doctype=book!
|
960
|
+
# FIXME honor subtitle!
|
961
|
+
theme_font :heading, level: 1 do
|
962
|
+
layout_heading doc.doctitle, align: :center
|
963
|
+
end
|
964
|
+
# FIXME theme setting
|
965
|
+
move_down @theme.vertical_rhythm
|
966
|
+
if doc.attr? 'authors'
|
967
|
+
layout_prose doc.attr('authors'), align: :center, margin_top: 0, margin_bottom: @theme.vertical_rhythm / 2.0, normalize: false
|
968
|
+
end
|
969
|
+
layout_prose [(doc.attr? 'revnumber') ? %(#{doc.attr 'version-label'} #{doc.attr 'revnumber'}) : nil, (doc.attr 'revdate')].compact * "\n", align: :center, margin_top: @theme.vertical_rhythm * 5, margin_bottom: 0, normalize: false
|
970
|
+
end
|
971
|
+
|
972
|
+
def layout_cover_page position, doc
|
973
|
+
# TODO turn processing of attribute with inline image a utility function in Asciidoctor
|
974
|
+
if (cover_image = (doc.attr %(#{position}-cover-image)))
|
975
|
+
if cover_image =~ ImageAttributeValueRx
|
976
|
+
cover_image = %(#{resolve_imagesdir doc}#{$1})
|
977
|
+
end
|
978
|
+
# QUESTION should we go to page 1 when position == :front?
|
979
|
+
go_to_page page_count if position == :back
|
980
|
+
image_page cover_image, canvas: true
|
981
|
+
end
|
982
|
+
end
|
983
|
+
|
984
|
+
# NOTE can't alias to start_new_page since methods have different arity
|
985
|
+
# NOTE only called if not at page top
|
986
|
+
def start_new_chapter section
|
987
|
+
start_new_page
|
988
|
+
end
|
989
|
+
|
990
|
+
def layout_chapter_title node, title
|
991
|
+
layout_heading title
|
992
|
+
end
|
993
|
+
|
994
|
+
# QUESTION why doesn't layout_heading set the font??
|
995
|
+
def layout_heading string, opts = {}
|
996
|
+
margin_top = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme.heading_margin_top
|
997
|
+
margin_bottom = margin || (opts.delete :margin_bottom) || @theme.heading_margin_bottom
|
998
|
+
#move_down margin_top
|
999
|
+
self.margin_top margin_top
|
1000
|
+
typeset_text string, calc_line_metrics((opts.delete :line_height) || @theme.heading_line_height), {
|
1001
|
+
color: @font_color,
|
1002
|
+
inline_format: true,
|
1003
|
+
align: :left
|
1004
|
+
}.merge(opts)
|
1005
|
+
#move_down margin_bottom
|
1006
|
+
self.margin_bottom margin_bottom
|
1007
|
+
end
|
1008
|
+
|
1009
|
+
# NOTE inline_format is true by default
|
1010
|
+
def layout_prose string, opts = {}
|
1011
|
+
margin_top = (margin = (opts.delete :margin)) || (opts.delete :margin_top) || @theme.prose_margin_top || 0
|
1012
|
+
margin_bottom = margin || (opts.delete :margin_bottom) || @theme.prose_margin_bottom || @theme.vertical_rhythm
|
1013
|
+
if (anchor = opts.delete :anchor)
|
1014
|
+
# FIXME won't work if inline_format is true; should instead pass through as attribute w/ link color set
|
1015
|
+
if (link_color = opts.delete :link_color)
|
1016
|
+
string = %(<link anchor="#{anchor}"><color rgb="#{link_color}">#{string}</color></link>)
|
1017
|
+
else
|
1018
|
+
string = %(<link anchor="#{anchor}">#{string}</link>)
|
1019
|
+
end
|
1020
|
+
end
|
1021
|
+
if opts.delete :preserve
|
1022
|
+
# preserve leading space using non-breaking space chars
|
1023
|
+
string = string.gsub(IndentationRx) { NoBreakSpace * $&.length }
|
1024
|
+
end
|
1025
|
+
#move_down margin_top
|
1026
|
+
self.margin_top margin_top
|
1027
|
+
typeset_text string, calc_line_metrics((opts.delete :line_height) || @theme.base_line_height), {
|
1028
|
+
color: @font_color,
|
1029
|
+
# NOTE normalize makes endlines soft (replaces "\n" with ' ')
|
1030
|
+
inline_format: [{ normalize: (opts.delete :normalize) != false }],
|
1031
|
+
align: (@theme.base_align || :justify).to_sym
|
1032
|
+
}.merge(opts)
|
1033
|
+
#move_down margin_bottom
|
1034
|
+
self.margin_bottom margin_bottom
|
1035
|
+
end
|
1036
|
+
|
1037
|
+
# Render the caption and return the height of the rendered content
|
1038
|
+
# QUESTION should layout_caption check for title? and return 0 if false?
|
1039
|
+
# TODO allow margin to be zeroed
|
1040
|
+
def layout_caption subject, opts = {}
|
1041
|
+
mark = { cursor: cursor, page_number: page_number }
|
1042
|
+
case subject
|
1043
|
+
when ::String
|
1044
|
+
string = subject
|
1045
|
+
when ::Asciidoctor::AbstractBlock
|
1046
|
+
string = subject.title? ? subject.captioned_title : nil
|
1047
|
+
else
|
1048
|
+
return 0
|
1049
|
+
end
|
1050
|
+
theme_font :caption do
|
1051
|
+
if (position = (opts.delete :position) || :top) == :top
|
1052
|
+
margin = { top: @theme.caption_margin_outside, bottom: @theme.caption_margin_inside }
|
1053
|
+
else
|
1054
|
+
margin = { top: @theme.caption_margin_inside, bottom: @theme.caption_margin_outside }
|
1055
|
+
end
|
1056
|
+
layout_prose string, {
|
1057
|
+
margin_top: margin[:top],
|
1058
|
+
margin_bottom: margin[:bottom],
|
1059
|
+
align: (@theme.caption_align || :left).to_sym,
|
1060
|
+
normalize: false
|
1061
|
+
}.merge(opts)
|
1062
|
+
if position == :top && @theme.caption_border_bottom_color
|
1063
|
+
stroke_horizontal_rule @theme.caption_border_bottom_color
|
1064
|
+
# HACK move down slightly so line isn't covered by filled area (half width of line)
|
1065
|
+
move_down 0.25
|
1066
|
+
end
|
1067
|
+
end
|
1068
|
+
# NOTE we assume we don't clear more than one page
|
1069
|
+
if page_number > mark[:page_number]
|
1070
|
+
mark[:cursor] + (bounds.top - cursor)
|
1071
|
+
else
|
1072
|
+
mark[:cursor] - cursor
|
1073
|
+
end
|
1074
|
+
end
|
1075
|
+
|
1076
|
+
def layout_toc doc, num_levels = 2, toc_page_number = 2
|
1077
|
+
go_to_page toc_page_number - 1
|
1078
|
+
start_new_page
|
1079
|
+
theme_font :heading, level: 2 do
|
1080
|
+
layout_heading doc.attr('toc-title')
|
1081
|
+
end
|
1082
|
+
line_metrics = calc_line_metrics @theme.base_line_height
|
1083
|
+
dot_width = width_of DotLeader
|
1084
|
+
if num_levels > 0
|
1085
|
+
layout_toc_level doc.sections, num_levels, line_metrics, dot_width
|
1086
|
+
end
|
1087
|
+
toc_page_numbers = (toc_page_number..page_number)
|
1088
|
+
go_to_page page_count - 1
|
1089
|
+
toc_page_numbers
|
1090
|
+
end
|
1091
|
+
|
1092
|
+
def layout_toc_level sections, num_levels, line_metrics, dot_width
|
1093
|
+
sections.each do |sect|
|
1094
|
+
sect_title = sect.numbered_title
|
1095
|
+
sect_page_num = (sect.attr 'page_start') - 1
|
1096
|
+
# NOTE we do some cursor hacking so the dots don't affect vertical alignment
|
1097
|
+
start_cursor = cursor
|
1098
|
+
typeset_text %(<link anchor="#{sect.id}">#{sect_title}</link>), line_metrics, inline_format: true
|
1099
|
+
end_cursor = cursor
|
1100
|
+
move_cursor_to start_cursor
|
1101
|
+
num_dots = ((bounds.width - (width_of %(#{sect_title} #{sect_page_num}), inline_format: true)) / dot_width).floor
|
1102
|
+
typeset_formatted_text [text: %(#{DotLeader * num_dots} #{sect_page_num}), anchor: sect.id], line_metrics, align: :right
|
1103
|
+
move_cursor_to end_cursor
|
1104
|
+
if sect.level < num_levels
|
1105
|
+
indent @theme.horizontal_rhythm do
|
1106
|
+
layout_toc_level sect.sections, num_levels, line_metrics, dot_width
|
1107
|
+
end
|
1108
|
+
end
|
1109
|
+
end
|
1110
|
+
end
|
1111
|
+
|
1112
|
+
def stamp_page_numbers opts = {}
|
1113
|
+
skip = opts[:skip] || 1
|
1114
|
+
start = skip + 1
|
1115
|
+
pattern = page_number_pattern
|
1116
|
+
repeat (start..page_count), dynamic: true do
|
1117
|
+
# don't stamp pages which are imported / inserts
|
1118
|
+
next if page.imported_page?
|
1119
|
+
case (align = (page_number - skip).odd? ? :left : :right)
|
1120
|
+
when :left
|
1121
|
+
page_number_label = pattern[:left] % [page_number - skip]
|
1122
|
+
when :right
|
1123
|
+
page_number_label = pattern[:right] % [page_number - skip]
|
1124
|
+
end
|
1125
|
+
theme_font :footer do
|
1126
|
+
canvas do
|
1127
|
+
if @theme.footer_border_color && @theme.footer_border_color != 'transparent'
|
1128
|
+
save_graphics_state do
|
1129
|
+
line_width @theme.base_border_width
|
1130
|
+
stroke_color @theme.footer_border_color
|
1131
|
+
stroke_horizontal_line left_margin, bounds.width - right_margin, at: (page.margins[:bottom] / 2.0 + @theme.vertical_rhythm / 2.0)
|
1132
|
+
end
|
1133
|
+
end
|
1134
|
+
indent left_margin, right_margin do
|
1135
|
+
formatted_text_box [text: page_number_label, color: @theme.footer_font_color], at: [0, (page.margins[:bottom] / 2.0)], align: align
|
1136
|
+
end
|
1137
|
+
end
|
1138
|
+
end
|
1139
|
+
end
|
1140
|
+
end
|
1141
|
+
|
1142
|
+
def page_number_pattern
|
1143
|
+
{ left: '%s', right: '%s' }
|
1144
|
+
end
|
1145
|
+
|
1146
|
+
# FIXME we are assuming we always have exactly one title page
|
1147
|
+
def add_outline doc, num_levels = 2, toc_page_nums = (0..-1)
|
1148
|
+
front_matter_counter = RomanNumeral.new 0, :lower
|
1149
|
+
|
1150
|
+
page_num_labels = {}
|
1151
|
+
|
1152
|
+
# title page (i)
|
1153
|
+
page_num_labels[0] = { P: ::PDF::Core::LiteralString.new(front_matter_counter.next!.to_s) }
|
1154
|
+
|
1155
|
+
# toc pages (ii..?)
|
1156
|
+
toc_page_nums.each do
|
1157
|
+
page_num_labels[front_matter_counter.to_i] = { P: ::PDF::Core::LiteralString.new(front_matter_counter.next!.to_s) }
|
1158
|
+
end
|
1159
|
+
|
1160
|
+
# credits page
|
1161
|
+
#page_num_labels[front_matter_counter.to_i] = { P: ::PDF::Core::LiteralString.new(front_matter_counter.next!.to_s) }
|
1162
|
+
|
1163
|
+
# number of front matter pages aside from the document title to skip in page number index
|
1164
|
+
numbering_offset = front_matter_counter.to_i - 1
|
1165
|
+
|
1166
|
+
outline.define do
|
1167
|
+
if (doctitle = (doc.doctitle sanitize: true, use_fallback: true))
|
1168
|
+
page title: doctitle, destination: (document.dest_top 1)
|
1169
|
+
end
|
1170
|
+
if doc.attr? 'toc'
|
1171
|
+
page title: doc.attr('toc-title'), destination: (document.dest_top toc_page_nums.first)
|
1172
|
+
end
|
1173
|
+
#page title: 'Credits', destination: (document.dest_top toc_page_nums.first + 1)
|
1174
|
+
# QUESTION any way to get add_outline_level to invoke in the context of the outline?
|
1175
|
+
document.add_outline_level self, doc.sections, num_levels, page_num_labels, numbering_offset
|
1176
|
+
end
|
1177
|
+
|
1178
|
+
catalog.data[:PageLabels] = state.store.ref Nums: page_num_labels.flatten
|
1179
|
+
catalog.data[:PageMode] = :UseOutlines
|
1180
|
+
nil
|
1181
|
+
end
|
1182
|
+
|
1183
|
+
# TODO only nest inside root node if doctype=article
|
1184
|
+
def add_outline_level outline, sections, num_levels, page_num_labels, numbering_offset
|
1185
|
+
sections.each do |sect|
|
1186
|
+
sect_title = sanitize(sect.numbered_title formal: true)
|
1187
|
+
sect_destination = sect.attr 'destination'
|
1188
|
+
sect_page_num = (sect.attr 'page_start') - 1
|
1189
|
+
page_num_labels[sect_page_num + numbering_offset] = { P: ::PDF::Core::LiteralString.new(sect_page_num.to_s) }
|
1190
|
+
if (subsections = sect.sections).empty? || sect.level == num_levels
|
1191
|
+
outline.page title: sect_title, destination: sect_destination
|
1192
|
+
elsif sect.level < num_levels + 1
|
1193
|
+
outline.section sect_title, { destination: sect_destination } do
|
1194
|
+
add_outline_level outline, subsections, num_levels, page_num_labels, numbering_offset
|
1195
|
+
end
|
1196
|
+
end
|
1197
|
+
end
|
1198
|
+
end
|
1199
|
+
|
1200
|
+
def write pdf_doc, target
|
1201
|
+
pdf_doc.render_file target
|
1202
|
+
#@prototype.render_file 'scratch.pdf'
|
1203
|
+
# QUESTION restore attributes first?
|
1204
|
+
@pdfmarks.generate_file target if @pdfmarks
|
1205
|
+
end
|
1206
|
+
|
1207
|
+
def register_fonts font_catalog, scripts = 'latin'
|
1208
|
+
(font_catalog || {}).each do |key, font_styles|
|
1209
|
+
register_font key => font_styles.map {|style, path| [style.to_sym, (font_path path)]}.to_h
|
1210
|
+
end
|
1211
|
+
|
1212
|
+
@fallback_fonts ||= []
|
1213
|
+
# FIXME read kerning setting from theme!
|
1214
|
+
default_kerning true
|
1215
|
+
end
|
1216
|
+
|
1217
|
+
# FIXME move to static method on ThemeLoader
|
1218
|
+
def font_path font_file
|
1219
|
+
# resolve relative to built-in font dir unless path is absolute
|
1220
|
+
::File.absolute_path font_file, ThemeLoader::FontsDir
|
1221
|
+
end
|
1222
|
+
|
1223
|
+
def theme_fill_and_stroke_bounds category
|
1224
|
+
fill_and_stroke_bounds @theme[%(#{category}_background_color)], @theme[%(#{category}_border_color)], {
|
1225
|
+
line_width: @theme[%(#{category}_border_width)],
|
1226
|
+
radius: @theme[%(#{category}_border_radius)]
|
1227
|
+
}
|
1228
|
+
end
|
1229
|
+
|
1230
|
+
# Insert a top margin space unless cursor is at the top of the page.
|
1231
|
+
# Start a new page if y value is greater than remaining space on page.
|
1232
|
+
def margin_top y
|
1233
|
+
margin y, :top
|
1234
|
+
end
|
1235
|
+
|
1236
|
+
# Insert a bottom margin space unless cursor is at the top of the page (not likely).
|
1237
|
+
# Start a new page if y value is greater than remaining space on page.
|
1238
|
+
def margin_bottom y
|
1239
|
+
margin y, :bottom
|
1240
|
+
end
|
1241
|
+
|
1242
|
+
# Insert a margin space of type position unless cursor is at the top of the page.
|
1243
|
+
# Start a new page if y value is greater than remaining space on page.
|
1244
|
+
def margin y, position
|
1245
|
+
unless y == 0 || at_page_top?
|
1246
|
+
if cursor <= y
|
1247
|
+
@margin_box.move_past_bottom
|
1248
|
+
else
|
1249
|
+
move_down y
|
1250
|
+
end
|
1251
|
+
end
|
1252
|
+
end
|
1253
|
+
|
1254
|
+
# Lookup margin for theme element and position, then delegate to margin method.
|
1255
|
+
# If the margin value is not found, assume 0 for position = :top and $vertical_rhythm for position = :bottom.
|
1256
|
+
def theme_margin category, position
|
1257
|
+
margin(@theme[%(#{category}_margin_#{position})] || (position == :bottom ? @theme.vertical_rhythm : 0), position)
|
1258
|
+
end
|
1259
|
+
|
1260
|
+
def theme_font category, opts = {}
|
1261
|
+
# QUESTION should we fallback to base_font_* or just leave current setting?
|
1262
|
+
family = @theme[%(#{category}_font_family)] || @theme.base_font_family
|
1263
|
+
|
1264
|
+
if (level = opts[:level])
|
1265
|
+
size = @theme[%(#{category}_font_size_h#{level})] || @theme.base_font_size
|
1266
|
+
else
|
1267
|
+
size = @theme[%(#{category}_font_size)] || @theme.base_font_size
|
1268
|
+
end
|
1269
|
+
|
1270
|
+
style = (@theme[%(#{category}_font_style)] || :normal).to_sym
|
1271
|
+
|
1272
|
+
if level
|
1273
|
+
color = @theme[%(#{category}_font_color_h#{level})] || @theme[%(#{category}_font_color)]
|
1274
|
+
else
|
1275
|
+
color = @theme[%(#{category}_font_color)]
|
1276
|
+
end
|
1277
|
+
|
1278
|
+
if color
|
1279
|
+
prev_color = @font_color
|
1280
|
+
@font_color = color
|
1281
|
+
end
|
1282
|
+
font family, size: size, style: style do
|
1283
|
+
yield
|
1284
|
+
end
|
1285
|
+
if color
|
1286
|
+
@font_color = prev_color
|
1287
|
+
end
|
1288
|
+
end
|
1289
|
+
|
1290
|
+
# TODO document me, esp the first line formatting functionality
|
1291
|
+
def typeset_text string, line_metrics, opts = {}
|
1292
|
+
move_down line_metrics.padding_top
|
1293
|
+
opts = { leading: line_metrics.leading, final_gap: line_metrics.final_gap }.merge opts
|
1294
|
+
if (first_line_opts = opts.delete :first_line_options)
|
1295
|
+
# TODO good candidate for Prawn enhancement!
|
1296
|
+
text_with_formatted_first_line string, first_line_opts, opts
|
1297
|
+
else
|
1298
|
+
text string, opts
|
1299
|
+
end
|
1300
|
+
move_down line_metrics.padding_bottom
|
1301
|
+
end
|
1302
|
+
|
1303
|
+
# QUESTION combine with typeset_text?
|
1304
|
+
def typeset_formatted_text fragments, line_metrics, opts = {}
|
1305
|
+
move_down line_metrics.padding_top
|
1306
|
+
formatted_text fragments, { leading: line_metrics.leading, final_gap: line_metrics.final_gap }.merge(opts)
|
1307
|
+
move_down line_metrics.padding_bottom
|
1308
|
+
end
|
1309
|
+
|
1310
|
+
def height_of_typeset_text string, opts = {}
|
1311
|
+
line_metrics = (calc_line_metrics opts[:line_height] || @theme.base_line_height)
|
1312
|
+
(height_of string, leading: line_metrics.leading, final_gap: line_metrics.final_gap) + line_metrics.padding_top + line_metrics.padding_bottom
|
1313
|
+
end
|
1314
|
+
|
1315
|
+
def prepare_verbatim string
|
1316
|
+
string.gsub(BuiltInEntityCharsRx, BuiltInEntityChars)
|
1317
|
+
.gsub(IndentationRx) { NoBreakSpace * $&.length }
|
1318
|
+
end
|
1319
|
+
|
1320
|
+
# Remove all HTML tags and resolve all entities in a string
|
1321
|
+
# FIXME add option to control escaping entities, or a filter mechanism in general
|
1322
|
+
def sanitize string
|
1323
|
+
string.gsub(/<[^>]+>/, '')
|
1324
|
+
.gsub(/&#(\d{2,4});/) { [$1.to_i].pack('U*') }
|
1325
|
+
.gsub('<', '<').gsub('>', '>').gsub('&', '&')
|
1326
|
+
.tr_s(' ', ' ')
|
1327
|
+
.strip
|
1328
|
+
end
|
1329
|
+
|
1330
|
+
def resolve_imagesdir doc
|
1331
|
+
@imagesdir ||= begin
|
1332
|
+
imagesdir = (doc.attr 'imagesdir', '.').chomp '/'
|
1333
|
+
imagesdir = imagesdir == '.' ? nil : %(#{imagesdir}/)
|
1334
|
+
end
|
1335
|
+
end
|
1336
|
+
|
1337
|
+
# QUESTION move to prawn/extensions.rb?
|
1338
|
+
def init_scratch_prototype
|
1339
|
+
# IMPORTANT don't set font before using Marshal, it causes serialization to fail
|
1340
|
+
@prototype = ::Marshal.load ::Marshal.dump self
|
1341
|
+
@prototype.state.store.info.data[:Scratch] = true
|
1342
|
+
# we're now starting a new page each time, so no need to do it here
|
1343
|
+
#@prototype.start_new_page if @prototype.page_number == 0
|
1344
|
+
end
|
1345
|
+
|
1346
|
+
=begin
|
1347
|
+
def create_stamps
|
1348
|
+
create_stamp 'masthead' do
|
1349
|
+
canvas do
|
1350
|
+
save_graphics_state do
|
1351
|
+
stroke_color '000000'
|
1352
|
+
x_margin = mm2pt 20
|
1353
|
+
y_margin = mm2pt 15
|
1354
|
+
stroke_horizontal_line x_margin, bounds.right - x_margin, at: bounds.top - y_margin
|
1355
|
+
stroke_horizontal_line x_margin, bounds.right - x_margin, at: y_margin
|
1356
|
+
end
|
1357
|
+
end
|
1358
|
+
end
|
1359
|
+
|
1360
|
+
@stamps_initialized = true
|
1361
|
+
end
|
1362
|
+
=end
|
1363
|
+
end
|
1364
|
+
end
|
1365
|
+
end
|