prawn-manual_builder 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d4873e82c2906833aa5480dcd264453e839b62a67254e8bd6c43c361430be8b
4
- data.tar.gz: 4958041c344dcb46dae8142b07872995d3e649bd9e5bc94befc7f476a5bd45b3
3
+ metadata.gz: cfd5c78fb9db44ad2832807a23ee19813304f3e2d484744a713d2a60eee651ec
4
+ data.tar.gz: 9863d4dd0e1767293ff0f32bd41496a85db40b9289dfbb1763f734cb2ffbe530
5
5
  SHA512:
6
- metadata.gz: c29745b0d6d5e43c81907857eb695017cce649d30b9905e0baffe52cf8385fa4073bdaad5021c936f57b87196cf9288659351ef4f9e63268b2f35c0d364f8e86
7
- data.tar.gz: d1a4eff4362d6e9a4fbe5a644cbf385c8aaf66d56baf2eb0a1e3cc3d1330f1ec791125906cb2f62dcecf616394861b066cf0838e76f3e95ac66d9318cd6724c9
6
+ metadata.gz: b3e5ef3376d39d2ab8f11862aaeb4c0dca74d46cd774ae34301227b26b4ea27ee3e3657c1ec2aacbcedd6d0e1e33be5c6ebce4aa71014ce41ff596638a0d8354
7
+ data.tar.gz: 43965e339ab569298c301cf484547d6afa47953158372b94ee45b224b58a3874ed57f35ab19991d4c6eb8cd19b71e92eab4e8f61bf8f2fcfe5db34c22acc79fd
checksums.yaml.gz.sig ADDED
@@ -0,0 +1,2 @@
1
+ Y*6_�ޏ��_Y-˫�: +�@�i�T�~��s������<6"�
2
  ����ɟQ �T�NN����
3
+ K�$�<��4h���~����5�Ek�C�NE_�H�+b�悰�}��d;L�+ʤ�?����:��.�vjnY�*Nfn)�#/j�@�
Binary file
Binary file
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