syntax_tree 0.1.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.
@@ -0,0 +1,1132 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # This class implements a pretty printing algorithm. It finds line breaks and
4
+ # nice indentations for grouped structure.
5
+ #
6
+ # By default, the class assumes that primitive elements are strings and each
7
+ # byte in the strings is a single column in width. But it can be used for other
8
+ # situations by giving suitable arguments for some methods:
9
+ #
10
+ # * newline object and space generation block for PrettyPrint.new
11
+ # * optional width argument for PrettyPrint#text
12
+ # * PrettyPrint#breakable
13
+ #
14
+ # There are several candidate uses:
15
+ # * text formatting using proportional fonts
16
+ # * multibyte characters which has columns different to number of bytes
17
+ # * non-string formatting
18
+ #
19
+ # == Usage
20
+ #
21
+ # To use this module, you will need to generate a tree of print nodes that
22
+ # represent indentation and newline behavior before it gets sent to the printer.
23
+ # Each node has different semantics, depending on the desired output.
24
+ #
25
+ # The most basic node is a Text node. This represents plain text content that
26
+ # cannot be broken up even if it doesn't fit on one line. You would create one
27
+ # of those with the text method, as in:
28
+ #
29
+ # PrettyPrint.format { |q| q.text('my content') }
30
+ #
31
+ # No matter what the desired output width is, the output for the snippet above
32
+ # will always be the same.
33
+ #
34
+ # If you want to allow the printer to break up the content on the space
35
+ # character when there isn't enough width for the full string on the same line,
36
+ # you can use the Breakable and Group nodes. For example:
37
+ #
38
+ # PrettyPrint.format do |q|
39
+ # q.group do
40
+ # q.text('my')
41
+ # q.breakable
42
+ # q.text('content')
43
+ # end
44
+ # end
45
+ #
46
+ # Now, if everything fits on one line (depending on the maximum width specified)
47
+ # then it will be the same output as the first example. If, however, there is
48
+ # not enough room on the line, then you will get two lines of output, one for
49
+ # the first string and one for the second.
50
+ #
51
+ # There are other nodes for the print tree as well, described in the
52
+ # documentation below. They control alignment, indentation, conditional
53
+ # formatting, and more.
54
+ #
55
+ # == Bugs
56
+ # * Box based formatting?
57
+ #
58
+ # Report any bugs at http://bugs.ruby-lang.org
59
+ #
60
+ # == References
61
+ # Christian Lindig, Strictly Pretty, March 2000,
62
+ # https://lindig.github.io/papers/strictly-pretty-2000.pdf
63
+ #
64
+ # Philip Wadler, A prettier printer, March 1998,
65
+ # https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf
66
+ #
67
+ # == Author
68
+ # Tanaka Akira <akr@fsij.org>
69
+ #
70
+ class PrettyPrint
71
+ # A node in the print tree that represents aligning nested nodes to a certain
72
+ # prefix width or string.
73
+ class Align
74
+ attr_reader :indent, :contents
75
+
76
+ def initialize(indent:, contents: [])
77
+ @indent = indent
78
+ @contents = contents
79
+ end
80
+
81
+ def pretty_print(q)
82
+ q.group(2, "align([", "])") do
83
+ q.seplist(contents) { |content| q.pp(content) }
84
+ end
85
+ end
86
+ end
87
+
88
+ # A node in the print tree that represents a place in the buffer that the
89
+ # content can be broken onto multiple lines.
90
+ class Breakable
91
+ attr_reader :separator, :width
92
+
93
+ def initialize(
94
+ separator = " ",
95
+ width = separator.length,
96
+ force: false,
97
+ indent: true
98
+ )
99
+ @separator = separator
100
+ @width = width
101
+ @force = force
102
+ @indent = indent
103
+ end
104
+
105
+ def force?
106
+ @force
107
+ end
108
+
109
+ def indent?
110
+ @indent
111
+ end
112
+
113
+ def pretty_print(q)
114
+ q.text("breakable")
115
+
116
+ attributes =
117
+ [("force=true" if force?), ("indent=false" unless indent?)].compact
118
+
119
+ if attributes.any?
120
+ q.text("(")
121
+ q.seplist(attributes, -> { q.text(", ") }) do |attribute|
122
+ q.text(attribute)
123
+ end
124
+ q.text(")")
125
+ end
126
+ end
127
+ end
128
+
129
+ # A node in the print tree that forces the surrounding group to print out in
130
+ # the "break" mode as opposed to the "flat" mode. Useful for when you need to
131
+ # force a newline into a group.
132
+ class BreakParent
133
+ def pretty_print(q)
134
+ q.text("break-parent")
135
+ end
136
+ end
137
+
138
+ # A node in the print tree that represents a group of items which the printer
139
+ # should try to fit onto one line. This is the basic command to tell the
140
+ # printer when to break. Groups are usually nested, and the printer will try
141
+ # to fit everything on one line, but if it doesn't fit it will break the
142
+ # outermost group first and try again. It will continue breaking groups until
143
+ # everything fits (or there are no more groups to break).
144
+ class Group
145
+ attr_reader :depth, :contents
146
+
147
+ def initialize(depth, contents: [])
148
+ @depth = depth
149
+ @contents = contents
150
+ @break = false
151
+ end
152
+
153
+ def break
154
+ @break = true
155
+ end
156
+
157
+ def break?
158
+ @break
159
+ end
160
+
161
+ def pretty_print(q)
162
+ q.group(2, "group([", "])") do
163
+ q.seplist(contents) { |content| q.pp(content) }
164
+ end
165
+ end
166
+ end
167
+
168
+ # A node in the print tree that represents printing one thing if the
169
+ # surrounding group node is broken and another thing if the surrounding group
170
+ # node is flat.
171
+ class IfBreak
172
+ attr_reader :break_contents, :flat_contents
173
+
174
+ def initialize(break_contents: [], flat_contents: [])
175
+ @break_contents = break_contents
176
+ @flat_contents = flat_contents
177
+ end
178
+
179
+ def pretty_print(q)
180
+ q.group(2, "if-break(", ")") do
181
+ q.breakable("")
182
+ q.group(2, "[", "],") do
183
+ q.seplist(break_contents) { |content| q.pp(content) }
184
+ end
185
+ q.breakable
186
+ q.group(2, "[", "]") do
187
+ q.seplist(flat_contents) { |content| q.pp(content) }
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ # A node in the print tree that is a variant of the Align node that indents
194
+ # its contents by one level.
195
+ class Indent
196
+ attr_reader :contents
197
+
198
+ def initialize(contents: [])
199
+ @contents = contents
200
+ end
201
+
202
+ def pretty_print(q)
203
+ q.group(2, "indent([", "])") do
204
+ q.seplist(contents) { |content| q.pp(content) }
205
+ end
206
+ end
207
+ end
208
+
209
+ # A node in the print tree that has its own special buffer for implementing
210
+ # content that should flush before any newline.
211
+ #
212
+ # Useful for implementating trailing content, as it's not always practical to
213
+ # constantly check where the line ends to avoid accidentally printing some
214
+ # content after a line suffix node.
215
+ class LineSuffix
216
+ attr_reader :contents
217
+
218
+ def initialize(contents: [])
219
+ @contents = contents
220
+ end
221
+
222
+ def pretty_print(q)
223
+ q.group(2, "line-suffix([", "])") do
224
+ q.seplist(contents) { |content| q.pp(content) }
225
+ end
226
+ end
227
+ end
228
+
229
+ # A node in the print tree that represents plain content that cannot be broken
230
+ # up (by default this assumes strings, but it can really be anything).
231
+ class Text
232
+ attr_reader :objects, :width
233
+
234
+ def initialize
235
+ @objects = []
236
+ @width = 0
237
+ end
238
+
239
+ def add(object: "", width: object.length)
240
+ @objects << object
241
+ @width += width
242
+ end
243
+
244
+ def pretty_print(q)
245
+ q.group(2, "text([", "])") do
246
+ q.seplist(objects) { |object| q.pp(object) }
247
+ end
248
+ end
249
+ end
250
+
251
+ # A node in the print tree that represents trimming all of the indentation of
252
+ # the current line, in the rare case that you need to ignore the indentation
253
+ # that you've already created. This node should be placed after a Breakable.
254
+ class Trim
255
+ def pretty_print(q)
256
+ q.text("trim")
257
+ end
258
+ end
259
+
260
+ # When building up the contents in the output buffer, it's convenient to be
261
+ # able to trim trailing whitespace before newlines. If the output object is a
262
+ # string or array or strings, then we can do this with some gsub calls. If
263
+ # not, then this effectively just wraps the output object and forwards on
264
+ # calls to <<.
265
+ module Buffer
266
+ # This is the default output buffer that provides a base implementation of
267
+ # trim! that does nothing. It's effectively a wrapper around whatever output
268
+ # object was given to the format command.
269
+ class DefaultBuffer
270
+ attr_reader :output
271
+
272
+ def initialize(output = [])
273
+ @output = output
274
+ end
275
+
276
+ def <<(object)
277
+ @output << object
278
+ end
279
+
280
+ def trim!
281
+ 0
282
+ end
283
+ end
284
+
285
+ # This is an output buffer that wraps a string output object. It provides a
286
+ # trim! method that trims off trailing whitespace from the string using
287
+ # gsub!.
288
+ class StringBuffer < DefaultBuffer
289
+ def initialize(output = "".dup)
290
+ super(output)
291
+ end
292
+
293
+ def trim!
294
+ length = output.length
295
+ output.gsub!(/[\t ]*\z/, "")
296
+ length - output.length
297
+ end
298
+ end
299
+
300
+ # This is an output buffer that wraps an array output object. It provides a
301
+ # trim! method that trims off trailing whitespace from the last element in
302
+ # the array if it's an unfrozen string using the same method as the
303
+ # StringBuffer.
304
+ class ArrayBuffer < DefaultBuffer
305
+ def initialize(output = [])
306
+ super(output)
307
+ end
308
+
309
+ def trim!
310
+ return 0 if output.empty?
311
+
312
+ trimmed = 0
313
+
314
+ while output.any? && output.last.is_a?(String) &&
315
+ output.last.match?(/\A[\t ]*\z/)
316
+ trimmed += output.pop.length
317
+ end
318
+
319
+ if output.any? && output.last.is_a?(String) && !output.last.frozen?
320
+ length = output.last.length
321
+ output.last.gsub!(/[\t ]*\z/, "")
322
+ trimmed += length - output.last.length
323
+ end
324
+
325
+ trimmed
326
+ end
327
+ end
328
+
329
+ # This is a switch for building the correct output buffer wrapper class for
330
+ # the given output object.
331
+ def self.for(output)
332
+ case output
333
+ when String
334
+ StringBuffer.new(output)
335
+ when Array
336
+ ArrayBuffer.new(output)
337
+ else
338
+ DefaultBuffer.new(output)
339
+ end
340
+ end
341
+ end
342
+
343
+ # PrettyPrint::SingleLine is used by PrettyPrint.singleline_format
344
+ #
345
+ # It is passed to be similar to a PrettyPrint object itself, by responding to
346
+ # all of the same print tree node builder methods, as well as the #flush
347
+ # method.
348
+ #
349
+ # The significant difference here is that there are no line breaks in the
350
+ # output. If an IfBreak node is used, only the flat contents are printed.
351
+ # LineSuffix nodes are printed at the end of the buffer when #flush is called.
352
+ class SingleLine
353
+ # The output object. It stores rendered text and should respond to <<.
354
+ attr_reader :output
355
+
356
+ # The current array of contents that the print tree builder methods should
357
+ # append to.
358
+ attr_reader :target
359
+
360
+ # A buffer output that wraps any calls to line_suffix that will be flushed
361
+ # at the end of printing.
362
+ attr_reader :line_suffixes
363
+
364
+ # Create a PrettyPrint::SingleLine object
365
+ #
366
+ # Arguments:
367
+ # * +output+ - String (or similar) to store rendered text. Needs to respond
368
+ # to '<<'.
369
+ # * +maxwidth+ - Argument position expected to be here for compatibility.
370
+ # This argument is a noop.
371
+ # * +newline+ - Argument position expected to be here for compatibility.
372
+ # This argument is a noop.
373
+ def initialize(output, maxwidth = nil, newline = nil)
374
+ @output = Buffer.for(output)
375
+ @target = @output
376
+ @line_suffixes = Buffer::ArrayBuffer.new
377
+ end
378
+
379
+ # Flushes the line suffixes onto the output buffer.
380
+ def flush
381
+ line_suffixes.output.each { |doc| output << doc }
382
+ end
383
+
384
+ # --------------------------------------------------------------------------
385
+ # Markers node builders
386
+ # --------------------------------------------------------------------------
387
+
388
+ # Appends +separator+ to the text to be output. By default +separator+ is
389
+ # ' '
390
+ #
391
+ # The +width+, +indent+, and +force+ arguments are here for compatibility.
392
+ # They are all noop arguments.
393
+ def breakable(
394
+ separator = " ",
395
+ width = separator.length,
396
+ indent: nil,
397
+ force: nil
398
+ )
399
+ target << separator
400
+ end
401
+
402
+ # Here for compatibility, does nothing.
403
+ def break_parent
404
+ end
405
+
406
+ # Appends +separator+ to the output buffer. +width+ is a noop here for
407
+ # compatibility.
408
+ def fill_breakable(separator = " ", width = separator.length)
409
+ target << separator
410
+ end
411
+
412
+ # Immediately trims the output buffer.
413
+ def trim
414
+ target.trim!
415
+ end
416
+
417
+ # ----------------------------------------------------------------------------
418
+ # Container node builders
419
+ # ----------------------------------------------------------------------------
420
+
421
+ # Opens a block for grouping objects to be pretty printed.
422
+ #
423
+ # Arguments:
424
+ # * +indent+ - noop argument. Present for compatibility.
425
+ # * +open_obj+ - text appended before the &block. Default is ''
426
+ # * +close_obj+ - text appended after the &block. Default is ''
427
+ # * +open_width+ - noop argument. Present for compatibility.
428
+ # * +close_width+ - noop argument. Present for compatibility.
429
+ def group(
430
+ indent = nil,
431
+ open_object = "",
432
+ close_object = "",
433
+ open_width = nil,
434
+ close_width = nil
435
+ )
436
+ target << open_object
437
+ yield
438
+ target << close_object
439
+ end
440
+
441
+ # A class that wraps the ability to call #if_flat. The contents of the
442
+ # #if_flat block are executed immediately, so effectively this class and the
443
+ # #if_break method that triggers it are unnecessary, but they're here to
444
+ # maintain compatibility.
445
+ class IfBreakBuilder
446
+ def if_flat
447
+ yield
448
+ end
449
+ end
450
+
451
+ # Effectively unnecessary, but here for compatibility.
452
+ def if_break
453
+ IfBreakBuilder.new
454
+ end
455
+
456
+ # A noop that immediately yields.
457
+ def indent
458
+ yield
459
+ end
460
+
461
+ # Changes the target output buffer to the line suffix output buffer which
462
+ # will get flushed at the end of printing.
463
+ def line_suffix
464
+ previous_target, @target = @target, line_suffixes
465
+ yield
466
+ @target = previous_target
467
+ end
468
+
469
+ # Takes +indent+ arg, but does nothing with it.
470
+ #
471
+ # Yields to a block.
472
+ def nest(indent)
473
+ yield
474
+ end
475
+
476
+ # Add +object+ to the text to be output.
477
+ #
478
+ # +width+ argument is here for compatibility. It is a noop argument.
479
+ def text(object = "", width = nil)
480
+ target << object
481
+ end
482
+ end
483
+
484
+ # This object represents the current level of indentation within the printer.
485
+ # It has the ability to generate new levels of indentation through the #align
486
+ # and #indent methods.
487
+ class IndentLevel
488
+ IndentPart = Object.new
489
+ DedentPart = Object.new
490
+
491
+ StringAlignPart = Struct.new(:n)
492
+ NumberAlignPart = Struct.new(:n)
493
+
494
+ attr_reader :genspace, :value, :length, :queue, :root
495
+
496
+ def initialize(
497
+ genspace:,
498
+ value: genspace.call(0),
499
+ length: 0,
500
+ queue: [],
501
+ root: nil
502
+ )
503
+ @genspace = genspace
504
+ @value = value
505
+ @length = length
506
+ @queue = queue
507
+ @root = root
508
+ end
509
+
510
+ # This can accept a whole lot of different kinds of objects, due to the
511
+ # nature of the flexibility of the Align node.
512
+ def align(n)
513
+ case n
514
+ when NilClass
515
+ self
516
+ when String
517
+ indent(StringAlignPart.new(n))
518
+ else
519
+ indent(n < 0 ? DedentPart : NumberAlignPart.new(n))
520
+ end
521
+ end
522
+
523
+ def indent(part = IndentPart)
524
+ next_value = genspace.call(0)
525
+ next_length = 0
526
+ next_queue = (part == DedentPart ? queue[0...-1] : [*queue, part])
527
+
528
+ last_spaces = 0
529
+
530
+ add_spaces = ->(count) do
531
+ next_value << genspace.call(count)
532
+ next_length += count
533
+ end
534
+
535
+ flush_spaces = -> do
536
+ add_spaces[last_spaces] if last_spaces > 0
537
+ last_spaces = 0
538
+ end
539
+
540
+ next_queue.each do |part|
541
+ case part
542
+ when IndentPart
543
+ flush_spaces.call
544
+ add_spaces.call(2)
545
+ when StringAlignPart
546
+ flush_spaces.call
547
+ next_value += part.n
548
+ next_length += part.n.length
549
+ when NumberAlignPart
550
+ last_spaces += part.n
551
+ end
552
+ end
553
+
554
+ flush_spaces.call
555
+
556
+ IndentLevel.new(
557
+ genspace: genspace,
558
+ value: next_value,
559
+ length: next_length,
560
+ queue: next_queue,
561
+ root: root
562
+ )
563
+ end
564
+ end
565
+
566
+ # When printing, you can optionally specify the value that should be used
567
+ # whenever a group needs to be broken onto multiple lines. In this case the
568
+ # default is \n.
569
+ DEFAULT_NEWLINE = "\n"
570
+
571
+ # When generating spaces after a newline for indentation, by default we
572
+ # generate one space per character needed for indentation. You can change this
573
+ # behavior (for instance to use tabs) by passing a different genspace
574
+ # procedure.
575
+ DEFAULT_GENSPACE = ->(n) { " " * n }
576
+
577
+ # There are two modes in printing, break and flat. When we're in break mode,
578
+ # any lines will use their newline, any if-breaks will use their break
579
+ # contents, etc.
580
+ MODE_BREAK = 1
581
+
582
+ # This is another print mode much like MODE_BREAK. When we're in flat mode, we
583
+ # attempt to print everything on one line until we either hit a broken group,
584
+ # a forced line, or the maximum width.
585
+ MODE_FLAT = 2
586
+
587
+ # This is a convenience method which is same as follows:
588
+ #
589
+ # begin
590
+ # q = PrettyPrint.new(output, maxwidth, newline, &genspace)
591
+ # ...
592
+ # q.flush
593
+ # output
594
+ # end
595
+ #
596
+ def self.format(
597
+ output = "".dup,
598
+ maxwidth = 80,
599
+ newline = DEFAULT_NEWLINE,
600
+ genspace = DEFAULT_GENSPACE
601
+ )
602
+ q = new(output, maxwidth, newline, &genspace)
603
+ yield q
604
+ q.flush
605
+ output
606
+ end
607
+
608
+ # This is similar to PrettyPrint::format but the result has no breaks.
609
+ #
610
+ # +maxwidth+, +newline+ and +genspace+ are ignored.
611
+ #
612
+ # The invocation of +breakable+ in the block doesn't break a line and is
613
+ # treated as just an invocation of +text+.
614
+ #
615
+ def self.singleline_format(
616
+ output = "".dup,
617
+ maxwidth = nil,
618
+ newline = nil,
619
+ genspace = nil
620
+ )
621
+ q = SingleLine.new(output)
622
+ yield q
623
+ output
624
+ end
625
+
626
+ # The output object. It represents the final destination of the contents of
627
+ # the print tree. It should respond to <<.
628
+ #
629
+ # This defaults to "".dup
630
+ attr_reader :output
631
+
632
+ # This is an output buffer that wraps the output object and provides
633
+ # additional functionality depending on its type.
634
+ #
635
+ # This defaults to Buffer::StringBuffer.new("".dup)
636
+ attr_reader :buffer
637
+
638
+ # The maximum width of a line, before it is separated in to a newline
639
+ #
640
+ # This defaults to 80, and should be an Integer
641
+ attr_reader :maxwidth
642
+
643
+ # The value that is appended to +output+ to add a new line.
644
+ #
645
+ # This defaults to "\n", and should be String
646
+ attr_reader :newline
647
+
648
+ # An object that responds to call that takes one argument, of an Integer, and
649
+ # returns the corresponding number of spaces.
650
+ #
651
+ # By default this is: ->(n) { ' ' * n }
652
+ attr_reader :genspace
653
+
654
+ # The stack of groups that are being printed.
655
+ attr_reader :groups
656
+
657
+ # The current array of contents that calls to methods that generate print tree
658
+ # nodes will append to.
659
+ attr_reader :target
660
+
661
+ # Creates a buffer for pretty printing.
662
+ #
663
+ # +output+ is an output target. If it is not specified, '' is assumed. It
664
+ # should have a << method which accepts the first argument +obj+ of
665
+ # PrettyPrint#text, the first argument +separator+ of PrettyPrint#breakable,
666
+ # the first argument +newline+ of PrettyPrint.new, and the result of a given
667
+ # block for PrettyPrint.new.
668
+ #
669
+ # +maxwidth+ specifies maximum line length. If it is not specified, 80 is
670
+ # assumed. However actual outputs may overflow +maxwidth+ if long
671
+ # non-breakable texts are provided.
672
+ #
673
+ # +newline+ is used for line breaks. "\n" is used if it is not specified.
674
+ #
675
+ # The block is used to generate spaces. ->(n) { ' ' * n } is used if it is not
676
+ # given.
677
+ def initialize(
678
+ output = "".dup,
679
+ maxwidth = 80,
680
+ newline = DEFAULT_NEWLINE,
681
+ &genspace
682
+ )
683
+ @output = output
684
+ @buffer = Buffer.for(output)
685
+ @maxwidth = maxwidth
686
+ @newline = newline
687
+ @genspace = genspace || DEFAULT_GENSPACE
688
+ reset
689
+ end
690
+
691
+ # Returns the group most recently added to the stack.
692
+ #
693
+ # Contrived example:
694
+ # out = ""
695
+ # => ""
696
+ # q = PrettyPrint.new(out)
697
+ # => #<PrettyPrint:0x0>
698
+ # q.group {
699
+ # q.text q.current_group.inspect
700
+ # q.text q.newline
701
+ # q.group(q.current_group.depth + 1) {
702
+ # q.text q.current_group.inspect
703
+ # q.text q.newline
704
+ # q.group(q.current_group.depth + 1) {
705
+ # q.text q.current_group.inspect
706
+ # q.text q.newline
707
+ # q.group(q.current_group.depth + 1) {
708
+ # q.text q.current_group.inspect
709
+ # q.text q.newline
710
+ # }
711
+ # }
712
+ # }
713
+ # }
714
+ # => 284
715
+ # puts out
716
+ # #<PrettyPrint::Group:0x0 @depth=1>
717
+ # #<PrettyPrint::Group:0x0 @depth=2>
718
+ # #<PrettyPrint::Group:0x0 @depth=3>
719
+ # #<PrettyPrint::Group:0x0 @depth=4>
720
+ def current_group
721
+ groups.last
722
+ end
723
+
724
+ # Flushes all of the generated print tree onto the output buffer, then clears
725
+ # the generated tree from memory.
726
+ def flush
727
+ # First, get the root group, since we placed one at the top to begin with.
728
+ doc = groups.first
729
+
730
+ # This represents how far along the current line we are. It gets reset
731
+ # back to 0 when we encounter a newline.
732
+ position = 0
733
+
734
+ # This is our command stack. A command consists of a triplet of an
735
+ # indentation level, the mode (break or flat), and a doc node.
736
+ commands = [[IndentLevel.new(genspace: genspace), MODE_BREAK, doc]]
737
+
738
+ # This is a small optimization boolean. It keeps track of whether or not
739
+ # when we hit a group node we should check if it fits on the same line.
740
+ should_remeasure = false
741
+
742
+ # This is a separate command stack that includes the same kind of triplets
743
+ # as the commands variable. It is used to keep track of things that should
744
+ # go at the end of printed lines once the other doc nodes are
745
+ # accounted for. Typically this is used to implement comments.
746
+ line_suffixes = []
747
+
748
+ # This is a linear stack instead of a mutually recursive call defined on
749
+ # the individual doc nodes for efficiency.
750
+ while commands.any?
751
+ indent, mode, doc = commands.pop
752
+
753
+ case doc
754
+ when Text
755
+ doc.objects.each { |object| buffer << object }
756
+ position += doc.width
757
+ when Array
758
+ doc.reverse_each { |part| commands << [indent, mode, part] }
759
+ when Indent
760
+ commands << [indent.indent, mode, doc.contents]
761
+ when Align
762
+ commands << [indent.align(doc.indent), mode, doc.contents]
763
+ when Trim
764
+ position -= buffer.trim!
765
+ when Group
766
+ if mode == MODE_FLAT && !should_remeasure
767
+ commands <<
768
+ [indent, doc.break? ? MODE_BREAK : MODE_FLAT, doc.contents]
769
+ else
770
+ should_remeasure = false
771
+ next_cmd = [indent, MODE_FLAT, doc.contents]
772
+
773
+ if !doc.break? && fits?(next_cmd, commands, maxwidth - position)
774
+ commands << next_cmd
775
+ else
776
+ commands << [indent, MODE_BREAK, doc.contents]
777
+ end
778
+ end
779
+ when IfBreak
780
+ if mode == MODE_BREAK
781
+ commands << [indent, mode, doc.break_contents] if doc.break_contents
782
+ elsif mode == MODE_FLAT
783
+ commands << [indent, mode, doc.flat_contents] if doc.flat_contents
784
+ end
785
+ when LineSuffix
786
+ line_suffixes << [indent, mode, doc.contents]
787
+ when Breakable
788
+ if mode == MODE_FLAT
789
+ if doc.force?
790
+ # This line was forced into the output even if we were in flat mode,
791
+ # so we need to tell the next group that no matter what, it needs to
792
+ # remeasure because the previous measurement didn't accurately
793
+ # capture the entire expression (this is necessary for nested
794
+ # groups).
795
+ should_remeasure = true
796
+ else
797
+ buffer << doc.separator
798
+ position += doc.width
799
+ next
800
+ end
801
+ end
802
+
803
+ # If there are any commands in the line suffix buffer, then we're going
804
+ # to flush them now, as we are about to add a newline.
805
+ if line_suffixes.any?
806
+ commands << [indent, mode, doc]
807
+ commands += line_suffixes.reverse
808
+ line_suffixes = []
809
+ next
810
+ end
811
+
812
+ if !doc.indent?
813
+ buffer << newline
814
+
815
+ if indent.root
816
+ buffer << indent.root.value
817
+ position = indent.root.length
818
+ else
819
+ position = 0
820
+ end
821
+ else
822
+ position -= buffer.trim!
823
+ buffer << newline
824
+ buffer << indent.value
825
+ position = indent.length
826
+ end
827
+ when BreakParent
828
+ # do nothing
829
+ else
830
+ # Special case where the user has defined some way to get an extra doc
831
+ # node that we don't explicitly support into the list. In this case
832
+ # we're going to assume it's 0-width and just append it to the output
833
+ # buffer.
834
+ #
835
+ # This is useful behavior for putting marker nodes into the list so that
836
+ # you can know how things are getting mapped before they get printed.
837
+ buffer << doc
838
+ end
839
+
840
+ if commands.empty? && line_suffixes.any?
841
+ commands += line_suffixes.reverse
842
+ line_suffixes = []
843
+ end
844
+ end
845
+
846
+ # Reset the group stack and target array so that this pretty printer object
847
+ # can continue to be used before calling flush again if desired.
848
+ reset
849
+ end
850
+
851
+ # ----------------------------------------------------------------------------
852
+ # Markers node builders
853
+ # ----------------------------------------------------------------------------
854
+
855
+ # This says "you can break a line here if necessary", and a +width+\-column
856
+ # text +separator+ is inserted if a line is not broken at the point.
857
+ #
858
+ # If +separator+ is not specified, ' ' is used.
859
+ #
860
+ # If +width+ is not specified, +separator.length+ is used. You will have to
861
+ # specify this when +separator+ is a multibyte character, for example.
862
+ #
863
+ # By default, if the surrounding group is broken and a newline is inserted,
864
+ # the printer will indent the subsequent line up to the current level of
865
+ # indentation. You can disable this behavior with the +indent+ argument if
866
+ # that's not desired (rare).
867
+ #
868
+ # By default, when you insert a Breakable into the print tree, it only breaks
869
+ # the surrounding group when the group's contents cannot fit onto the
870
+ # remaining space of the current line. You can force it to break the
871
+ # surrounding group instead if you always want the newline with the +force+
872
+ # argument.
873
+ def breakable(
874
+ separator = " ",
875
+ width = separator.length,
876
+ indent: true,
877
+ force: false
878
+ )
879
+ doc = Breakable.new(separator, width, indent: indent, force: force)
880
+
881
+ target << doc
882
+ break_parent if force
883
+
884
+ doc
885
+ end
886
+
887
+ # This inserts a BreakParent node into the print tree which forces the
888
+ # surrounding and all parent group nodes to break.
889
+ def break_parent
890
+ doc = BreakParent.new
891
+ target << doc
892
+
893
+ groups.reverse_each do |group|
894
+ break if group.break?
895
+ group.break
896
+ end
897
+
898
+ doc
899
+ end
900
+
901
+ # This is similar to #breakable except the decision to break or not is
902
+ # determined individually.
903
+ #
904
+ # Two #fill_breakable under a group may cause 4 results:
905
+ # (break,break), (break,non-break), (non-break,break), (non-break,non-break).
906
+ # This is different to #breakable because two #breakable under a group
907
+ # may cause 2 results: (break,break), (non-break,non-break).
908
+ #
909
+ # The text +separator+ is inserted if a line is not broken at this point.
910
+ #
911
+ # If +separator+ is not specified, ' ' is used.
912
+ #
913
+ # If +width+ is not specified, +separator.length+ is used. You will have to
914
+ # specify this when +separator+ is a multibyte character, for example.
915
+ def fill_breakable(separator = " ", width = separator.length)
916
+ group { breakable(separator, width) }
917
+ end
918
+
919
+ # This inserts a Trim node into the print tree which, when printed, will clear
920
+ # all whitespace at the end of the output buffer. This is useful for the rare
921
+ # case where you need to delete printed indentation and force the next node
922
+ # to start at the beginning of the line.
923
+ def trim
924
+ doc = Trim.new
925
+ target << doc
926
+
927
+ doc
928
+ end
929
+
930
+ # ----------------------------------------------------------------------------
931
+ # Container node builders
932
+ # ----------------------------------------------------------------------------
933
+
934
+ # Groups line break hints added in the block. The line break hints are all to
935
+ # be used or not.
936
+ #
937
+ # If +indent+ is specified, the method call is regarded as nested by
938
+ # nest(indent) { ... }.
939
+ #
940
+ # If +open_object+ is specified, <tt>text(open_object, open_width)</tt> is
941
+ # called before grouping. If +close_object+ is specified,
942
+ # <tt>text(close_object, close_width)</tt> is called after grouping.
943
+ def group(
944
+ indent = 0,
945
+ open_object = "",
946
+ close_object = "",
947
+ open_width = open_object.length,
948
+ close_width = close_object.length
949
+ )
950
+ text(open_object, open_width) if open_object != ""
951
+
952
+ doc = Group.new(groups.last.depth + 1)
953
+ groups << doc
954
+ target << doc
955
+
956
+ with_target(doc.contents) do
957
+ if indent != 0
958
+ nest(indent) { yield }
959
+ else
960
+ yield
961
+ end
962
+ end
963
+
964
+ groups.pop
965
+ text(close_object, close_width) if close_object != ""
966
+
967
+ doc
968
+ end
969
+
970
+ # A small DSL-like object used for specifying the alternative contents to be
971
+ # printed if the surrounding group doesn't break for an IfBreak node.
972
+ class IfBreakBuilder
973
+ attr_reader :builder, :if_break
974
+
975
+ def initialize(builder, if_break)
976
+ @builder = builder
977
+ @if_break = if_break
978
+ end
979
+
980
+ def if_flat(&block)
981
+ builder.with_target(if_break.flat_contents, &block)
982
+ end
983
+ end
984
+
985
+ # Inserts an IfBreak node with the contents of the block being added to its
986
+ # list of nodes that should be printed if the surrounding node breaks. If it
987
+ # doesn't, then you can specify the contents to be printed with the #if_flat
988
+ # method used on the return object from this method. For example,
989
+ #
990
+ # q.if_break { q.text('do') }.if_flat { q.text('{') }
991
+ #
992
+ # In the example above, if the surrounding group is broken it will print 'do'
993
+ # and if it is not it will print '{'.
994
+ def if_break
995
+ doc = IfBreak.new
996
+ target << doc
997
+
998
+ with_target(doc.break_contents) { yield }
999
+ IfBreakBuilder.new(self, doc)
1000
+ end
1001
+
1002
+ # Very similar to the #nest method, this indents the nested content by one
1003
+ # level by inserting an Indent node into the print tree. The contents of the
1004
+ # node are determined by the block.
1005
+ def indent
1006
+ doc = Indent.new
1007
+ target << doc
1008
+
1009
+ with_target(doc.contents) { yield }
1010
+ doc
1011
+ end
1012
+
1013
+ # Inserts a LineSuffix node into the print tree. The contents of the node are
1014
+ # determined by the block.
1015
+ def line_suffix
1016
+ doc = LineSuffix.new
1017
+ target << doc
1018
+
1019
+ with_target(doc.contents) { yield }
1020
+ doc
1021
+ end
1022
+
1023
+ # Increases left margin after newline with +indent+ for line breaks added in
1024
+ # the block.
1025
+ def nest(indent)
1026
+ doc = Align.new(indent: indent)
1027
+ target << doc
1028
+
1029
+ with_target(doc.contents) { yield }
1030
+ doc
1031
+ end
1032
+
1033
+ # This adds +object+ as a text of +width+ columns in width.
1034
+ #
1035
+ # If +width+ is not specified, object.length is used.
1036
+ def text(object = "", width = object.length)
1037
+ doc = target.last
1038
+
1039
+ unless Text === doc
1040
+ doc = Text.new
1041
+ target << doc
1042
+ end
1043
+
1044
+ doc.add(object: object, width: width)
1045
+ doc
1046
+ end
1047
+
1048
+ # ----------------------------------------------------------------------------
1049
+ # Internal APIs
1050
+ # ----------------------------------------------------------------------------
1051
+
1052
+ # A convenience method used by a lot of the print tree node builders that
1053
+ # temporarily changes the target that the builders will append to.
1054
+ def with_target(target)
1055
+ previous_target, @target = @target, target
1056
+ yield
1057
+ @target = previous_target
1058
+ end
1059
+
1060
+ private
1061
+
1062
+ # This method returns a boolean as to whether or not the remaining commands
1063
+ # fit onto the remaining space on the current line. If we finish printing
1064
+ # all of the commands or if we hit a newline, then we return true. Otherwise
1065
+ # if we continue printing past the remaining space, we return false.
1066
+ def fits?(next_command, rest_commands, remaining)
1067
+ # This is the index in the remaining commands that we've handled so far.
1068
+ # We reverse through the commands and add them to the stack if we've run
1069
+ # out of nodes to handle.
1070
+ rest_index = rest_commands.length
1071
+
1072
+ # This is our stack of commands, very similar to the commands list in the
1073
+ # print method.
1074
+ commands = [next_command]
1075
+
1076
+ # This is our output buffer, really only necessary to keep track of
1077
+ # because we could encounter a Trim doc node that would actually add
1078
+ # remaining space.
1079
+ fit_buffer = buffer.class.new
1080
+
1081
+ while remaining >= 0
1082
+ if commands.empty?
1083
+ return true if rest_index == 0
1084
+
1085
+ rest_index -= 1
1086
+ commands << rest_commands[rest_index]
1087
+ next
1088
+ end
1089
+
1090
+ indent, mode, doc = commands.pop
1091
+
1092
+ case doc
1093
+ when Text
1094
+ doc.objects.each { |object| fit_buffer << object }
1095
+ remaining -= doc.width
1096
+ when Array
1097
+ doc.reverse_each { |part| commands << [indent, mode, part] }
1098
+ when Indent
1099
+ commands << [indent.indent, mode, doc.contents]
1100
+ when Align
1101
+ commands << [indent.align(doc.indent), mode, doc.contents]
1102
+ when Trim
1103
+ remaining += fit_buffer.trim!
1104
+ when Group
1105
+ commands << [indent, doc.break? ? MODE_BREAK : mode, doc.contents]
1106
+ when IfBreak
1107
+ if mode == MODE_BREAK
1108
+ commands << [indent, mode, doc.break_contents] if doc.break_contents
1109
+ else
1110
+ commands << [indent, mode, doc.flat_contents] if doc.flat_contents
1111
+ end
1112
+ when Breakable
1113
+ if mode == MODE_FLAT && !doc.force?
1114
+ fit_buffer << doc.separator
1115
+ remaining -= doc.width
1116
+ next
1117
+ end
1118
+
1119
+ return true
1120
+ end
1121
+ end
1122
+
1123
+ false
1124
+ end
1125
+
1126
+ # Resets the group stack and target array so that this pretty printer object
1127
+ # can continue to be used before calling flush again if desired.
1128
+ def reset
1129
+ @groups = [Group.new(0)]
1130
+ @target = @groups.last.contents
1131
+ end
1132
+ end