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