prawn-manual_builder 0.3.1 → 0.4.0
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 +4 -4
- checksums.yaml.gz.sig +2 -0
- data/data/fonts/DejaVuSans-Bold.ttf +0 -0
- data/data/fonts/DejaVuSans-BoldOblique.ttf +0 -0
- data/data/fonts/DejaVuSans-Oblique.ttf +0 -0
- data/data/fonts/DejaVuSans.ttf +0 -0
- data/data/fonts/Jigmo.ttf +0 -0
- data/data/fonts/Jigmo2.ttf +0 -0
- data/data/fonts/Jigmo3.ttf +0 -0
- data/data/fonts/Panic+Sans.dfont +0 -0
- data/data/fonts/iosevka-po-bold.ttf +0 -0
- data/data/fonts/iosevka-po-regular.ttf +0 -0
- data/lib/prawn/manual_builder/chapter.rb +329 -0
- data/lib/prawn/manual_builder/manual.rb +172 -0
- data/lib/prawn/manual_builder/part.rb +78 -0
- data/lib/prawn/manual_builder/peritext.rb +40 -0
- data/lib/prawn/manual_builder/section.rb +22 -0
- data/lib/prawn/manual_builder/syntax_highlight.rb +96 -44
- data/lib/prawn/manual_builder/text_renderer.rb +152 -0
- data/lib/prawn/manual_builder/version.rb +7 -0
- data/lib/prawn/manual_builder.rb +31 -22
- data.tar.gz.sig +0 -0
- metadata +63 -22
- metadata.gz.sig +1 -0
- data/data/fonts/Dustismo_Roman.ttf +0 -0
- data/data/fonts/gkai00mp.ttf +0 -0
- data/lib/prawn/manual_builder/example.rb +0 -394
- data/lib/prawn/manual_builder/example_file.rb +0 -128
- data/lib/prawn/manual_builder/example_package.rb +0 -56
- data/lib/prawn/manual_builder/example_section.rb +0 -47
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cfd5c78fb9db44ad2832807a23ee19813304f3e2d484744a713d2a60eee651ec
|
4
|
+
data.tar.gz: 9863d4dd0e1767293ff0f32bd41496a85db40b9289dfbb1763f734cb2ffbe530
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b3e5ef3376d39d2ab8f11862aaeb4c0dca74d46cd774ae34301227b26b4ea27ee3e3657c1ec2aacbcedd6d0e1e33be5c6ebce4aa71014ce41ff596638a0d8354
|
7
|
+
data.tar.gz: 43965e339ab569298c301cf484547d6afa47953158372b94ee45b224b58a3874ed57f35ab19991d4c6eb8cd19b71e92eab4e8f61bf8f2fcfe5db34c22acc79fd
|
checksums.yaml.gz.sig
ADDED
Binary file
|
Binary file
|
Binary file
|
data/data/fonts/DejaVuSans.ttf
CHANGED
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,329 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'prism'
|
4
|
+
require_relative 'part'
|
5
|
+
require_relative 'text_renderer'
|
6
|
+
require_relative "syntax_highlight"
|
7
|
+
|
8
|
+
module Prawn
|
9
|
+
module ManualBuilder
|
10
|
+
class Chapter < Part
|
11
|
+
def initialize(&block)
|
12
|
+
super
|
13
|
+
|
14
|
+
if block
|
15
|
+
instance_eval(&block)
|
16
|
+
else
|
17
|
+
warn "Chapter defined in #{__FILE__} has no content"
|
18
|
+
end
|
19
|
+
|
20
|
+
self.auto_render = true
|
21
|
+
at_exit do
|
22
|
+
if self.auto_render
|
23
|
+
execute_example
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Chapter DSL
|
29
|
+
def title(title = NOT_SET)
|
30
|
+
if title == NOT_SET
|
31
|
+
@title
|
32
|
+
else
|
33
|
+
@title = title
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def text(&block)
|
38
|
+
if !block_given?
|
39
|
+
@text
|
40
|
+
else
|
41
|
+
@text = block
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def example(source = NOT_SET, axes: false, new_page: false, eval: true, standalone: false, &block)
|
46
|
+
if source == NOT_SET && !block_given?
|
47
|
+
@example
|
48
|
+
elsif source != NOT_SET && block_given?
|
49
|
+
raise ArgumentError, "Example can't be specified both as a block and as a string"
|
50
|
+
else
|
51
|
+
if source != NOT_SET
|
52
|
+
@example = source
|
53
|
+
else
|
54
|
+
@example = block
|
55
|
+
end
|
56
|
+
@example_axes = axes
|
57
|
+
@eval_example = eval
|
58
|
+
@standalone_example = standalone
|
59
|
+
@new_page_example = new_page
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def eval_example?
|
64
|
+
@eval_example
|
65
|
+
end
|
66
|
+
|
67
|
+
def standalone_example?
|
68
|
+
@standalone_example
|
69
|
+
end
|
70
|
+
|
71
|
+
def new_page_example?
|
72
|
+
@new_page_example
|
73
|
+
end
|
74
|
+
|
75
|
+
def example_axes?
|
76
|
+
@example_axes
|
77
|
+
end
|
78
|
+
|
79
|
+
def render(doc)
|
80
|
+
doc.start_new_page(margin: PAGE_MARGIN)
|
81
|
+
@page_number = doc.page_number
|
82
|
+
|
83
|
+
chapter_header(doc)
|
84
|
+
|
85
|
+
inner_box(doc) do
|
86
|
+
TextRenderer.new(doc, &text).render
|
87
|
+
end
|
88
|
+
|
89
|
+
example_source(doc)
|
90
|
+
|
91
|
+
if eval_example? && !standalone_example?
|
92
|
+
eval_example(doc)
|
93
|
+
end
|
94
|
+
|
95
|
+
unless eval_example?
|
96
|
+
standalone_example(doc)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def to_s
|
101
|
+
super[-2, 0] = " path: #{path}"
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def example_source_code
|
107
|
+
case example
|
108
|
+
when Proc
|
109
|
+
block_source_line = example.source_location.last
|
110
|
+
source = File.read(path)
|
111
|
+
parse_result = Prism.parse(source)
|
112
|
+
|
113
|
+
block =
|
114
|
+
Prism::Pattern.new("CallNode[name: :example, block: BlockNode]")
|
115
|
+
.scan(parse_result.value)
|
116
|
+
.find { |node| node.block.location.start_line == block_source_line }
|
117
|
+
&.block
|
118
|
+
|
119
|
+
return '' unless block
|
120
|
+
|
121
|
+
source.byteslice(block.opening_loc.end_offset, block.closing_loc.start_offset - block.opening_loc.end_offset)
|
122
|
+
.then { _1.gsub(/^#{_1.scan(/^[ \t]*(?=\S)/).min}/, '') } # Remove indentation
|
123
|
+
when String
|
124
|
+
example
|
125
|
+
else
|
126
|
+
''
|
127
|
+
end
|
128
|
+
.sub(/(?<=\A)([ \t]*\r?\n)*/, '') # Remove empty lines at the beginning
|
129
|
+
.rstrip # Remove trailing whitespace
|
130
|
+
end
|
131
|
+
|
132
|
+
def chapter_header(doc)
|
133
|
+
raise "Title is not set" unless title
|
134
|
+
|
135
|
+
header_options = {
|
136
|
+
leading: 6,
|
137
|
+
final_gap: false,
|
138
|
+
}
|
139
|
+
header_text = [
|
140
|
+
{ text: title, font: HEADER_FONT, size: HEADER_FONT_SIZE, color: DARK_GRAY },
|
141
|
+
]
|
142
|
+
|
143
|
+
if manual.root_path && example
|
144
|
+
rel_path = Pathname.new(path).relative_path_from(manual.root_path)
|
145
|
+
|
146
|
+
header_text.concat(
|
147
|
+
[
|
148
|
+
{ text: "\n" },
|
149
|
+
{ text: "#{rel_path.dirname}/", color: BROWN, font: 'Iosevka', size: HEADER_FONT_SIZE * 0.75 },
|
150
|
+
{ text: "#{rel_path.basename}", color: ORANGE, font: 'Iosevka', size: HEADER_FONT_SIZE * 0.75 },
|
151
|
+
]
|
152
|
+
)
|
153
|
+
end
|
154
|
+
|
155
|
+
TextRenderer.new(doc) do
|
156
|
+
header_with_bg(header_text, header_options)
|
157
|
+
end.render
|
158
|
+
end
|
159
|
+
|
160
|
+
def example_source(doc)
|
161
|
+
return if example_source_code.empty?
|
162
|
+
|
163
|
+
doc.font(CODE_FONT, size: CODE_FONT_SIZE) do
|
164
|
+
colored_box(doc, SyntaxHighlight.new(example_source_code).to_prawn, fill_color: DARK_GRAY)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
def eval_example(doc)
|
169
|
+
old_new_page_callback = doc.new_page_callback
|
170
|
+
crossed_page = false
|
171
|
+
doc.new_page_callback = lambda do |doc|
|
172
|
+
# Reset bounding box on new page
|
173
|
+
doc.bounds = Prawn::Document::BoundingBox.new(
|
174
|
+
doc,
|
175
|
+
nil,
|
176
|
+
[0, doc.page.dimensions[3]],
|
177
|
+
width: doc.page.dimensions[2],
|
178
|
+
height: doc.page.dimensions[3]
|
179
|
+
)
|
180
|
+
setup_example_area(doc)
|
181
|
+
crossed_page = true
|
182
|
+
end
|
183
|
+
|
184
|
+
preserving_doc_settings(doc) do
|
185
|
+
doc.save_graphics_state do
|
186
|
+
doc.bounding_box(
|
187
|
+
[-doc.bounds.absolute_left, doc.cursor],
|
188
|
+
width: doc.page.dimensions[2],
|
189
|
+
height: doc.y
|
190
|
+
) do
|
191
|
+
doc.start_new_page if new_page_example?
|
192
|
+
setup_example_area(doc) unless crossed_page
|
193
|
+
|
194
|
+
begin
|
195
|
+
if example.is_a?(Proc)
|
196
|
+
doc.instance_eval(&example)
|
197
|
+
else
|
198
|
+
doc.instance_eval(example, path)
|
199
|
+
end
|
200
|
+
rescue => e
|
201
|
+
puts "Error evaluating example: #{e.message}"
|
202
|
+
puts
|
203
|
+
puts "---- Source: ----"
|
204
|
+
puts
|
205
|
+
puts example
|
206
|
+
puts
|
207
|
+
puts "---- Backtrace: ----"
|
208
|
+
puts e.backtrace
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
ensure
|
214
|
+
doc.new_page_callback = old_new_page_callback
|
215
|
+
end
|
216
|
+
|
217
|
+
def setup_example_area(doc)
|
218
|
+
doc.save_graphics_state do
|
219
|
+
line_width = PAGE_MARGIN
|
220
|
+
text_size = PAGE_MARGIN * 0.6
|
221
|
+
doc.stroke_color(GRAY)
|
222
|
+
doc.line_width(line_width)
|
223
|
+
|
224
|
+
doc.stroke_rectangle([line_width / 2, doc.bounds.top - line_width / 2], doc.bounds.width - line_width, doc.bounds.height - line_width)
|
225
|
+
|
226
|
+
# We're reseting fonts for the example so we have to add it again
|
227
|
+
example_title_font = '_ManualExampleTitle'
|
228
|
+
doc.font_families.update(
|
229
|
+
example_title_font => {
|
230
|
+
normal: "#{Prawn::ManualBuilder::DATADIR}/fonts/DejaVuSans-Bold.ttf",
|
231
|
+
bold: "#{Prawn::ManualBuilder::DATADIR}/fonts/DejaVuSans-Bold.ttf",
|
232
|
+
})
|
233
|
+
example_title = 'Example Output'
|
234
|
+
text = [{text: example_title, font: example_title_font, size: text_size, styles: [:bold], color: 'ffffff'}]
|
235
|
+
h = doc.height_of_formatted(text, { final_gap: false })
|
236
|
+
|
237
|
+
doc.bounding_box(
|
238
|
+
[line_width, doc.bounds.top - (line_width - h) / 2 * 1.25],
|
239
|
+
width: doc.bounds.width - line_width * 2
|
240
|
+
) do
|
241
|
+
doc.formatted_text(text, align: :right)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
doc.bounds = Prawn::Document::BoundingBox.new(
|
246
|
+
doc,
|
247
|
+
doc.bounds,
|
248
|
+
[PAGE_MARGIN, doc.bounds.top - PAGE_MARGIN],
|
249
|
+
width: doc.bounds.width - PAGE_MARGIN * 2,
|
250
|
+
height: doc.bounds.height - PAGE_MARGIN * 2
|
251
|
+
)
|
252
|
+
doc.move_cursor_to(doc.bounds.height)
|
253
|
+
|
254
|
+
doc.stroke_axis if example_axes?
|
255
|
+
end
|
256
|
+
|
257
|
+
# Used to generate the url for the example files
|
258
|
+
MANUAL_URL = "https://github.com/prawnpdf/prawn/tree/master/manual"
|
259
|
+
|
260
|
+
|
261
|
+
# Renders a box with the link for the example file
|
262
|
+
def standalone_example(doc)
|
263
|
+
url = "#{MANUAL_URL}/#{Pathname(path).relative_path_from(manual.root_path)}"
|
264
|
+
|
265
|
+
reason = [
|
266
|
+
{ text: "This code snippet was not evaluated inline. "\
|
267
|
+
"You may see its output by running the "\
|
268
|
+
"example file located here:\n",
|
269
|
+
color: DARK_GRAY, font: 'DejaVu', size: 11 },
|
270
|
+
{ text: url.gsub('/', "/#{Prawn::Text::ZWSP}"), color: BLUE, link: url, font: 'DejaVu', size: 11 }
|
271
|
+
]
|
272
|
+
|
273
|
+
colored_box(
|
274
|
+
doc, reason,
|
275
|
+
fill_color: LIGHT_GOLD,
|
276
|
+
stroke_color: DARK_GOLD,
|
277
|
+
leading: LEADING * 3
|
278
|
+
)
|
279
|
+
end
|
280
|
+
|
281
|
+
def execute_example
|
282
|
+
if standalone_example?
|
283
|
+
if self.example.is_a?(String)
|
284
|
+
eval(example, TOPLEVEL_BINDING)
|
285
|
+
else
|
286
|
+
Object.new.instance_eval(&example)
|
287
|
+
end
|
288
|
+
else
|
289
|
+
Prawn::Document.generate("example.pdf") do |doc|
|
290
|
+
if self.example.is_a?(String)
|
291
|
+
doc.instance_eval(example, path)
|
292
|
+
else
|
293
|
+
doc.instance_eval(&example)
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def preserving_doc_settings(doc)
|
300
|
+
old_settings = {
|
301
|
+
font: doc.instance_variable_get(:@font),
|
302
|
+
font_size: doc.instance_variable_get(:@font_size),
|
303
|
+
leading: doc.default_leading,
|
304
|
+
text_direction: doc.text_direction,
|
305
|
+
line_width: doc.line_width,
|
306
|
+
cap_style: doc.cap_style,
|
307
|
+
join_style: doc.join_style,
|
308
|
+
fill_color: doc.fill_color,
|
309
|
+
stroke_color: doc.stroke_color,
|
310
|
+
font_families: doc.font_families.dup,
|
311
|
+
}
|
312
|
+
doc.instance_variable_set(:@font_families, nil)
|
313
|
+
doc.font('Helvetica', size: 12)
|
314
|
+
|
315
|
+
yield
|
316
|
+
ensure
|
317
|
+
doc.font_families.replace(old_settings[:font_families])
|
318
|
+
doc.set_font(old_settings[:font], size: old_settings[:font_size])
|
319
|
+
doc.default_leading = old_settings[:leading]
|
320
|
+
doc.text_direction = old_settings[:text_direction]
|
321
|
+
doc.line_width = old_settings[:line_width]
|
322
|
+
doc.cap_style = old_settings[:cap_style]
|
323
|
+
doc.join_style = old_settings[:join_style]
|
324
|
+
doc.fill_color(old_settings[:fill_color])
|
325
|
+
doc.stroke_color(old_settings[:stroke_color])
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
require 'prawn'
|
5
|
+
|
6
|
+
require_relative 'part'
|
7
|
+
require_relative 'section'
|
8
|
+
require_relative 'peritext'
|
9
|
+
require_relative 'chapter'
|
10
|
+
|
11
|
+
module Prawn
|
12
|
+
module ManualBuilder
|
13
|
+
class Manual
|
14
|
+
def initialize(root_path, document_options = {}, &block)
|
15
|
+
@root_path = Pathname(root_path)
|
16
|
+
@document_options = document_options
|
17
|
+
@content = []
|
18
|
+
@container = self
|
19
|
+
instance_eval(&block) if block
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :root_path, :content
|
23
|
+
|
24
|
+
def generate(filename = nil)
|
25
|
+
doc = Prawn::Document.new({ skip_page_creation: true, margin: PAGE_MARGIN }.merge(@document_options)) do
|
26
|
+
jigmo_file = "#{Prawn::ManualBuilder::DATADIR}/fonts/Jigmo.ttf"
|
27
|
+
jigmo2_file = "#{Prawn::ManualBuilder::DATADIR}/fonts/Jigmo2.ttf"
|
28
|
+
jigmo3_file = "#{Prawn::ManualBuilder::DATADIR}/fonts/Jigmo3.ttf"
|
29
|
+
dejavu_file = "#{Prawn::ManualBuilder::DATADIR}/fonts/DejaVuSans.ttf"
|
30
|
+
dejavu_bold_file = "#{Prawn::ManualBuilder::DATADIR}/fonts/DejaVuSans-Bold.ttf"
|
31
|
+
dejavu_italic_file = "#{Prawn::ManualBuilder::DATADIR}/fonts/DejaVuSans-Oblique.ttf"
|
32
|
+
dejavu_bold_italic_file = "#{Prawn::ManualBuilder::DATADIR}/fonts/DejaVuSans-BoldOblique.ttf"
|
33
|
+
iosevka_file = "#{Prawn::ManualBuilder::DATADIR}/fonts/iosevka-po-regular.ttf"
|
34
|
+
iosevka_bold_file = "#{Prawn::ManualBuilder::DATADIR}/fonts/iosevka-po-bold.ttf"
|
35
|
+
font_families.update(
|
36
|
+
'Jigmo' => { normal: jigmo_file },
|
37
|
+
'Jigmo2' => { normal: jigmo2_file },
|
38
|
+
'Jigmo3' => { normal: jigmo3_file },
|
39
|
+
'DejaVu' => {
|
40
|
+
normal: dejavu_file,
|
41
|
+
bold: dejavu_bold_file,
|
42
|
+
italic: dejavu_italic_file,
|
43
|
+
bold_italic: dejavu_bold_italic_file,
|
44
|
+
},
|
45
|
+
'Iosevka' => {
|
46
|
+
normal: iosevka_file,
|
47
|
+
bold: iosevka_bold_file,
|
48
|
+
}
|
49
|
+
)
|
50
|
+
|
51
|
+
class << self
|
52
|
+
attr_accessor :new_page_callback
|
53
|
+
|
54
|
+
def start_new_page(options = {})
|
55
|
+
super
|
56
|
+
|
57
|
+
if new_page_callback
|
58
|
+
new_page_callback.call(self)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
render_content(doc)
|
65
|
+
build_outline(doc)
|
66
|
+
if filename
|
67
|
+
doc.render_file(filename)
|
68
|
+
else
|
69
|
+
doc.render
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
protected
|
74
|
+
|
75
|
+
attr_reader :content
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def title(title = NOT_SET)
|
80
|
+
if title == NOT_SET
|
81
|
+
@title
|
82
|
+
else
|
83
|
+
@title = title
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Groups chapters into sections. This is primarily used to structure manual outline.
|
88
|
+
def section(title)
|
89
|
+
section = Section.new(title)
|
90
|
+
@container.content << section
|
91
|
+
parent_container = @container
|
92
|
+
@container = section
|
93
|
+
yield
|
94
|
+
ensure
|
95
|
+
@container = parent_container
|
96
|
+
end
|
97
|
+
|
98
|
+
# Adds manual content. Chapter usually produces some content and is added to the document outline.
|
99
|
+
def chapter(relative_path)
|
100
|
+
part = load_part(relative_path)
|
101
|
+
@container.content << part
|
102
|
+
end
|
103
|
+
|
104
|
+
def part(relative_path)
|
105
|
+
part = load_part(relative_path)
|
106
|
+
@container.content << part
|
107
|
+
end
|
108
|
+
|
109
|
+
def load_part(relative_path)
|
110
|
+
part_path = File.join(root_path, "#{relative_path}.rb")
|
111
|
+
if File.exist?(part_path)
|
112
|
+
part = eval(File.read(part_path), TOPLEVEL_BINDING, part_path)
|
113
|
+
if part.is_a?(Part)
|
114
|
+
part.auto_render = false
|
115
|
+
part.path = part_path
|
116
|
+
part.manual = self
|
117
|
+
else
|
118
|
+
raise ArgumentError, "#{relative_path} doesn't evaluate to a Part object"
|
119
|
+
end
|
120
|
+
|
121
|
+
part
|
122
|
+
else
|
123
|
+
raise ArgumentError, "#{relative_path} not found"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def render_content(doc)
|
128
|
+
content.each do |part|
|
129
|
+
render_part(doc, part)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def render_part(doc, part)
|
134
|
+
case part
|
135
|
+
when Section
|
136
|
+
part.content.each do |subpart|
|
137
|
+
render_part(doc, subpart)
|
138
|
+
end
|
139
|
+
when Chapter, Peritext
|
140
|
+
part.render(doc)
|
141
|
+
else
|
142
|
+
raise ArgumentError, "Unknown part type #{part.class}"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def build_outline(doc)
|
147
|
+
doc.outline.section(title) do
|
148
|
+
content.each do |part|
|
149
|
+
add_part_to_outline(doc, part)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def add_part_to_outline(doc, part)
|
155
|
+
case part
|
156
|
+
when Section
|
157
|
+
doc.outline.section(part.title, destination: part.page_number) do
|
158
|
+
part.content.each do |subpart|
|
159
|
+
add_part_to_outline(doc, subpart)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
when Chapter
|
163
|
+
doc.outline.page(title: part.title, destination: part.page_number)
|
164
|
+
when Peritext
|
165
|
+
# Skip
|
166
|
+
else
|
167
|
+
raise ArgumentError, "Unknown part type #{part.class}"
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prawn
|
4
|
+
module ManualBuilder
|
5
|
+
class Part
|
6
|
+
attr_accessor :auto_render
|
7
|
+
attr_accessor :manual
|
8
|
+
attr_accessor :path
|
9
|
+
attr_reader :page_number
|
10
|
+
|
11
|
+
def render(doc)
|
12
|
+
raise NotImplementedError
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def colored_box(doc, box_text, options={})
|
18
|
+
options = {
|
19
|
+
fill_color: DARK_GRAY,
|
20
|
+
stroke_color: nil,
|
21
|
+
text_color: LIGHT_GRAY,
|
22
|
+
leading: LEADING
|
23
|
+
}.merge(options)
|
24
|
+
|
25
|
+
text_options = {
|
26
|
+
leading: options[:leading],
|
27
|
+
fallback_fonts: ["DejaVu", "Jigmo", "Jigmo2", "Jigmo3"]
|
28
|
+
}
|
29
|
+
|
30
|
+
box_height = 0
|
31
|
+
|
32
|
+
doc.bounding_box(
|
33
|
+
[INNER_MARGIN + RHYTHM, doc.cursor],
|
34
|
+
width: doc.bounds.width - (INNER_MARGIN + RHYTHM) * 2
|
35
|
+
) do
|
36
|
+
box_height = doc.height_of_formatted(box_text, text_options)
|
37
|
+
end
|
38
|
+
|
39
|
+
if box_height > doc.cursor - doc.bounds.bottom
|
40
|
+
doc.start_new_page
|
41
|
+
doc.move_down(INNER_MARGIN)
|
42
|
+
end
|
43
|
+
|
44
|
+
doc.bounding_box(
|
45
|
+
[INNER_MARGIN + RHYTHM, doc.cursor],
|
46
|
+
width: doc.bounds.width - (INNER_MARGIN + RHYTHM) * 2
|
47
|
+
) do
|
48
|
+
box_height = doc.height_of_formatted(box_text, text_options)
|
49
|
+
|
50
|
+
doc.fill_color(options[:fill_color])
|
51
|
+
doc.stroke_color(options[:stroke_color] || options[:fill_color])
|
52
|
+
doc.fill_and_stroke_rounded_rectangle(
|
53
|
+
[doc.bounds.left - RHYTHM, doc.cursor],
|
54
|
+
doc.bounds.left + doc.bounds.right + RHYTHM * 2,
|
55
|
+
box_height + RHYTHM * 2,
|
56
|
+
5
|
57
|
+
)
|
58
|
+
doc.fill_color(BLACK)
|
59
|
+
doc.stroke_color(BLACK)
|
60
|
+
|
61
|
+
doc.pad(RHYTHM) do
|
62
|
+
doc.formatted_text(box_text, text_options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
doc.move_down(RHYTHM * 2)
|
67
|
+
end
|
68
|
+
|
69
|
+
def inner_box(doc, &block)
|
70
|
+
doc.bounding_box(
|
71
|
+
[INNER_MARGIN, doc.cursor],
|
72
|
+
width: doc.bounds.width - INNER_MARGIN * 2,
|
73
|
+
&block
|
74
|
+
)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'prism'
|
4
|
+
require_relative 'part'
|
5
|
+
require_relative 'text_renderer'
|
6
|
+
|
7
|
+
module Prawn
|
8
|
+
module ManualBuilder
|
9
|
+
class Peritext < Part
|
10
|
+
|
11
|
+
def initialize(&block)
|
12
|
+
super
|
13
|
+
|
14
|
+
if block
|
15
|
+
instance_eval(&block)
|
16
|
+
else
|
17
|
+
warn "Peritext defined in #{__FILE__} has no content"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# DSL
|
22
|
+
def text(&block)
|
23
|
+
if !block_given?
|
24
|
+
@text
|
25
|
+
else
|
26
|
+
@text = block
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def render(doc)
|
31
|
+
doc.start_new_page(margin: PAGE_MARGIN)
|
32
|
+
@page_number = doc.page_number
|
33
|
+
|
34
|
+
inner_box(doc) do
|
35
|
+
TextRenderer.new(doc, &text).render
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Prawn
|
4
|
+
module ManualBuilder
|
5
|
+
class Section
|
6
|
+
def initialize(title)
|
7
|
+
@title = title
|
8
|
+
@content = []
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :title, :content
|
12
|
+
|
13
|
+
def render(doc)
|
14
|
+
# Do nothing
|
15
|
+
end
|
16
|
+
|
17
|
+
def page_number
|
18
|
+
content.find { _1.respond_to?(:page_number) }&.page_number
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|