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 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