prettier_print 0.1.0

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