oppen 0.9.7 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b656616fe07cf5b29b8dfa43f0b72ed6dbf80c56a9ca6790745a8db27f058ed7
4
- data.tar.gz: 3d46ddc3a703d029ac22460ac504cf2928f95d804d32e0266f0de7ffbbfd3359
3
+ metadata.gz: e53fd2f0875228c91da4aaa2259d7b1bf18e0561284513e57181cb6c260ba610
4
+ data.tar.gz: 82c1776d841d6eef2d2071fc923b4864e23e2e68e079fa6caec36b7cca23ffbc
5
5
  SHA512:
6
- metadata.gz: 7b69f0365a507dc9fca546cbfea064593a48d0d85140cd894ed2ff256e13c4c0e772bf9e0d2b29f9eefd9121c8b3adf03da93137c15b3c9570f6a7e49ed435b2
7
- data.tar.gz: aa608fa2238dc8eea6bf3ce5d03f10e6f8059eda98c02d1e53fa06f9ac9301b03b42e54b38ed4f945c28766c4959b441cc190b76f0f176a72c7f52967096258a
6
+ metadata.gz: 87f3293a723b93c82532045aafe0dd699b6d7ac754893d51ff338448550187f27d75d7bea12fd4035be3f765de660605c56ac431460785387b47b6955b2805bb
7
+ data.tar.gz: 9928e9536e2fb193150fb32d000abf5e5b0017cbbecab050b2b064f14847f72b24ac573ea4779d2fe0bd2f5ac4f5b1ef04a7b1341bdc36e9b74bd652aa3b43ce
data/README.md CHANGED
@@ -22,17 +22,12 @@ transition from `ruby/prettyprint` to this gem.
22
22
 
23
23
  `Wadler` is implemented on top of `Oppen`, and it provides more options than
24
24
  `ruby/prettyprint`, notably:
25
- 1. Consistent and inconsistent breaking.
26
- 1. Explicit breaking, which is achievable in `ruby/prettyprint` with some
27
- monkeypatching.
28
-
29
- > [!CAUTION]
30
- > This is still under development.
31
-
32
- ## Usage
33
-
34
- > [!WARNING]
35
- > Lands when the APIs are stable.
25
+ 1. [Consistent](examples/wadler_group/consistent.rb) and [inconsistent](examples/wadler_group/inconsistent.rb) breaking.
26
+ 1. [Explicit breaking](examples/wadler_break_and_breakable/break.rb), which is achievable in `ruby/prettyprint` with some monkeypatching.
27
+ 1. [Trimming of trailing whitespaces](examples/oppen_and_wadler_customization/whitespace.rb).
28
+ 1. [Display a `String` on line break](examples/wadler_break_and_breakable/line_continuation.rb).
29
+ 1. A bunch of helper methods to simplify common patterns like [surrounding](examples/wadler_utils/surround.rb) or
30
+ [separating](examples/wadler_utils/surround.rb) tokens.
36
31
 
37
32
  ## Oppen vs Wadler
38
33
 
@@ -40,34 +35,109 @@ monkeypatching.
40
35
  and it's not calling ruby's `prettyprint`.
41
36
 
42
37
  Both implementations have their use cases:
43
- 1. Oppen gives more control over tokens sent to the printer.
44
- 1. Wadler gives a more _"functional"_ API, which is far nicer to work with.
38
+ - Oppen gives more control over tokens sent to the printer.
39
+ - Wadler gives a more _"functional"_ API, which is far nicer to work with.
45
40
 
46
41
  That being said, both APIs in this gem can achieve the same results, especially
47
42
  on consistent and inconsistent breaking.
48
43
 
49
- ## Noteworthy details
50
-
51
- ### Difference with Oppen's original algorithm
52
-
53
- 1. We took liberty to rename functions to make the API more modern and closer to
44
+ ## Oppen's API Example
45
+
46
+ ```ruby
47
+ tokens = [
48
+ Oppen.begin_inconsistent,
49
+ Oppen.string('Hello'),
50
+ Oppen.break(', '),
51
+ Oppen.string('World!'),
52
+ Oppen.line_break,
53
+ Oppen.string('How are you doing?'),
54
+ Oppen.end,
55
+ Oppen.eof,
56
+ ]
57
+
58
+ puts Oppen.print(tokens:)
59
+ # Hello, World!
60
+ # How are you doing?
61
+ ```
62
+
63
+ ## Wadler's API Example
64
+
65
+ ```ruby
66
+ out = Oppen::Wadler.new(width: 20)
67
+
68
+ out.group(indent: 2) {
69
+ out.group {
70
+ out.text('def').breakable.text('foo')
71
+ }
72
+ out.parens_break_none {
73
+ out.separate(%w[bar baz bat qux], ',', break_type: :inconsistent) { |param|
74
+ out.text(param)
75
+ }
76
+ }
77
+ }
78
+ out.group(indent: 2) {
79
+ out
80
+ .break
81
+ .nest(indent: 2) {
82
+ out
83
+ .text('puts')
84
+ .breakable(line_continuation: ' \\')
85
+ .text('42')
86
+ }
87
+ }
88
+ out.break.text('end')
89
+
90
+ puts out.output
91
+ # def foo(bar, baz,
92
+ # bat, qux)
93
+ # puts \
94
+ # 42
95
+ # end
96
+ ```
97
+
98
+ ## More Examples
99
+
100
+ An easy way to add colors to the output on the terminal is wrap `oppen` and expose your own vocabulary:
101
+
102
+ ```ruby
103
+ require 'colored'
104
+ class ColoredTty
105
+ KW_PALETTE = { Hello: :red, World: :green }.freeze
106
+ def initialize(...) = @out = Oppen::Wadler.new(...)
107
+ def breakable(...) = @out.breakable(...) && self
108
+ def keyword(value, width: value.length) = @out.text(value.send(KW_PALETTE[value.to_sym] || :white), width:) && self
109
+ def output = @out.output
110
+ def text(...) = @out.text(...) && self
111
+ end
112
+
113
+ out = ColoredTty.new(width: 12)
114
+ out.keyword('Hello').breakable.text('World')
115
+
116
+ puts out.output
117
+ # \e[31mHello\e[0m World
118
+ ```
119
+
120
+ The same idea can be applied an adapted to make an HTML printer; all you need to take care of is the correct width of the text to preserve the width of the text and get an output identical to that of the tty colored printer.
121
+
122
+ Check out the [examples/](examples/README.md) folder for more details on how to use the Oppen and Wadler APIs.
123
+
124
+ ## Difference With Oppen's Original Algorithm
125
+
126
+ 1. We took the liberty to rename functions to make the API more modern and closer to
54
127
  what we expect when writing Ruby code. All correspondences with the algorithm
55
128
  as described in Oppen's paper are noted in the comments of classes and methods.
56
129
  1. We do not raise exceptions when we overflow the margin. The only exceptions
57
130
  that we raise indicate a bug in the implementation. Please report them.
131
+ 1. The stacks described by the algorithm do not have a fixed size in our
132
+ implementation: we upsize them when they are full.
133
+ 1. We can optionally trim trailing whitespaces (this feature is on by default for the `Wadler` API).
134
+ 1. We added support for an additional new line anchors, see [examples/configs/indent_anchor.rb](examples/configs/indent_anchor.rb).
135
+ 1. We added support for eager printing of `groups`; see [examples/configs/eager_print.rb](examples/configs/eager_print.rb).
136
+ 1. We introduced a new token (`Whitespace`) and added more customizations to one of the originals (`Break`).
58
137
 
59
- ### Difference with `ruby/prettyprint`
60
-
61
- Oppen's algorithm and `ruby/prettyprint` do not have the same starting positions
62
- for a group's indentation. That's why you need to pay particular attention to
63
- calls for `nest`; you might want to decrease them by `1` if you care about keeping
64
- the same behavior.
65
-
66
- This is what we do in our test suite to verify the correspondence of the `Wadler`
67
- API and the `ruby/prettyprint`. We decided to shift the burden to the user because
68
- we think that the deicision taken by `ruby/prettyprint` does not suit us.
138
+ For more insight on how Oppen's algorithm works, check out [docs/oppen_algorithm.md](docs/oppen_algorithm.md).
69
139
 
70
- ## Related projects
140
+ ## Related Projects
71
141
 
72
142
  1. [`ruby/prettyprint`](https://github.com/ruby/prettyprint)
73
143
  1. [rustc implementation](https://doc.rust-lang.org/nightly/nightly-rustc/rustc_ast_pretty/pp/index.html)
data/lib/oppen/mixins.rb CHANGED
@@ -4,70 +4,68 @@ module Oppen
4
4
  # Mixins.
5
5
  module Mixins
6
6
  # Rotates circular array and triples its size.
7
- # This method is not for public use.
8
7
  #
9
- # @param arr [Array]
10
- # @param offset [Integer] Rotation amount
8
+ # @!visibility private
9
+ # @note This method is not for public use.
11
10
  #
12
- # @return [Array<Array, Integer, Integer>] upsized array, lhs, rhs
11
+ # @param arr [Array]
12
+ # the circular array.
13
+ # @param offset [Integer]
14
+ # rotation amount.
15
+ #
16
+ # @return [Array<Array, Integer, Integer>]
17
+ # upsized array, lhs, rhs.
13
18
  def upsize_circular_array(arr, offset)
14
19
  size = arr.size
15
- arr = arr.rotate(offset)
16
- arr.fill(nil, size, 2 * size)
20
+ arr = arr.rotate offset
21
+ arr.fill nil, size, 2 * size
17
22
  [arr, 0, size]
18
23
  end
19
24
 
20
- # Convert a list of tokens to its wadler representation.
21
- #
22
- # @param tokens [Array[Token]]
23
- # @param base_indent [Integer]
24
- # @param printer_name [String]
25
- #
26
25
  # @return [String]
27
- def tokens_to_wadler(tokens, base_indent: 0, printer_name: 'out')
28
- nb_spaces = base_indent
29
- out = StringIO.new
26
+ def tokens_to_wadler(tokens, base_indent: 0, printer_name: 'out', width: tokens.length * 3)
27
+ printer = Oppen::Wadler.new(base_indent:, indent: 2, width:)
30
28
 
31
- write = ->(txt) {
32
- out << (' ' * nb_spaces) << txt << "\n"
33
- }
34
- display_break_token = ->(token) {
29
+ handle_break_token = ->(token) {
35
30
  if token.offset.positive?
36
- write.("#{printer_name}.nest(#{token.offset}, \"\", \"\") {")
37
- nb_spaces += 2
31
+ printer
32
+ .text("#{printer_name}.nest(indent: #{token.offset}) {")
33
+ .nest_open
34
+ .break
38
35
  end
39
36
 
40
- case token
41
- in Token::LineBreak
42
- write.("#{printer_name}.break(line_continuation: #{token.line_continuation.inspect})")
43
- in Token::Break
44
- write.("#{printer_name}.breakable(#{token.str.inspect}, width: #{token.width}, " \
45
- "line_continuation: #{token.line_continuation.inspect})")
46
- end
37
+ printer.text(
38
+ case token
39
+ in Token::LineBreak
40
+ "#{printer_name}.break(line_continuation: #{token.line_continuation.inspect})"
41
+ in Token::Break
42
+ "#{printer_name}.breakable(#{token.str.inspect}, width: #{token.width}, " \
43
+ "line_continuation: #{token.line_continuation.inspect})"
44
+ end,
45
+ )
47
46
 
48
- if token.offset.positive?
49
- nb_spaces -= 2
50
- write.('}')
51
- end
47
+ printer.nest_close.break.text '}' if token.offset.positive?
52
48
  }
53
49
 
54
- tokens.each do |token|
50
+ tokens.each_with_index do |token, idx|
55
51
  case token
56
52
  in Token::String
57
- write.("#{printer_name}.text(#{token.value.inspect}, width: #{token.width})")
53
+ printer.text "#{printer_name}.text(#{token.value.inspect}, width: #{token.width})"
58
54
  in Token::Break
59
- display_break_token.(token)
55
+ handle_break_token.(token)
60
56
  in Token::Begin
61
- write.("#{printer_name}.group(#{token.offset}, \"\", \"\", #{token.break_type_name}) {")
62
- nb_spaces += 2
57
+ printer
58
+ .text("#{printer_name}.group(#{token.break_type.inspect}, indent: #{token.offset}) {")
59
+ .nest_open
63
60
  in Token::End
64
- nb_spaces -= 2
65
- write.('}')
61
+ printer.nest_close.break.text '}'
66
62
  in Token::EOF
67
- write.('') # new line
63
+ nil
68
64
  end
65
+ printer.break if !tokens[idx + 1].is_a?(Token::End)
69
66
  end
70
- out.string
67
+
68
+ printer.output
71
69
  end
72
70
  end
73
71
  end
@@ -2,15 +2,16 @@
2
2
 
3
3
  # Oppen.
4
4
  module Oppen
5
- # Class that represents a stack that builds an output string
6
- # using the values of the tokens that were pushed into it.
5
+ # A stack of {Token}s.
7
6
  class PrintStack
8
- # Class that represents an item in the print stack.
7
+ # An item in the print stack.
9
8
  class PrintStackEntry
10
- # @return [Integer] Indentation level.
11
- attr_reader :offset
12
- # @return [Token::BreakType] (Called break in the original paper).
9
+ # @return [Token::BreakType]
10
+ # Called `break` in the original paper.
13
11
  attr_reader :break_type
12
+ # @return [Integer]
13
+ # Indentation level.
14
+ attr_reader :offset
14
15
 
15
16
  def initialize(offset, break_type)
16
17
  @offset = offset
@@ -18,34 +19,26 @@ module Oppen
18
19
  end
19
20
  end
20
21
 
21
- # IO element that builds the output.
22
+ # IO sink for the output.
22
23
  attr_reader :buffer
23
-
24
- # Config containing customization flags
24
+ # The printer's configuration, altering its behavior.
25
25
  attr_reader :config
26
-
27
- # Callable that generate spaces
26
+ # Space generator, a callable.
28
27
  attr_reader :genspace
29
-
30
- # Array representing the stack of PrintStackEntries.
28
+ # The stack of PrintStackEntries.
31
29
  attr_reader :items
32
-
33
- # Delimiter between lines in output
30
+ # Delimiter between lines.
34
31
  attr_reader :new_line
35
-
36
- # Maximum allowed width for printing (Called length in the original paper).
37
- attr_reader :width
38
-
39
- # Current available space (Called index in the original paper).
40
- #
41
- # @return [Integer] Current available space (Called index in the original paper).
32
+ # Current available space (`index` in the original paper).
42
33
  attr_reader :space
34
+ # Maximum allowed width for printing (`length` in the original paper).
35
+ attr_reader :width
43
36
 
44
37
  def initialize(width, new_line, config, space, out)
45
38
  @buffer = out
46
39
  @config = config
47
40
  @genspace =
48
- if space.respond_to?(:call)
41
+ if space.respond_to? :call
49
42
  raise ArgumentError, 'space argument must be a Proc of arity 1' \
50
43
  if space.to_proc.arity != 1
51
44
 
@@ -53,28 +46,32 @@ module Oppen
53
46
  else
54
47
  ->(n) { space * n }
55
48
  end
56
- @indent = 0
49
+ @indent = 0 # the amount of indentation to display on the next non empty new line.
57
50
  @items = []
58
51
  @new_line = new_line
59
52
  @width = width
60
53
  @space = width
61
54
  end
62
55
 
63
- # Returns the output of the print stack
56
+ # The final pretty-printed output.
64
57
  #
65
58
  # @return [String]
59
+ # The output of the print stack.
66
60
  def output
67
- buffer.truncate(buffer.pos)
61
+ buffer.truncate buffer.pos
68
62
  buffer.string
69
63
  end
70
64
 
71
- # Core method responsible for building the print stack and the output string.
65
+ # Core method responsible for building the print stack and the output
66
+ # string.
72
67
  #
73
- # @note Called Print in the original paper.
68
+ # @note Called `Print` in the original paper.
74
69
  #
75
- # @param token [Token]
76
- # @param token_width [Integer]
77
- # @param trim_on_break [Integer] Number of trailing whitespace characters to trim.
70
+ # @param token [Token]
71
+ # @param token_width [Integer]
72
+ # @param trim_on_break [Integer]
73
+ # number of trailing whitespace characters to trim. If zero, no
74
+ # character will be trimmed.
78
75
  #
79
76
  # @return [Nil]
80
77
  def print(token, token_width, trim_on_break: 0)
@@ -90,23 +87,21 @@ module Oppen
90
87
  end
91
88
  end
92
89
 
93
- # Handle Begin Token.
90
+ # Handle {Token::Begin}.
94
91
  #
95
- # @param token [Token]
92
+ # @param token [Token]
96
93
  # @param token_width [Integer]
97
94
  #
98
95
  # @return [Nil]
99
- #
100
- # @see Token::Begin
101
96
  def handle_begin(token, token_width)
102
97
  if token_width > space
103
98
  type =
104
- if token.break_type == Token::BreakType::CONSISTENT
105
- Token::BreakType::CONSISTENT
99
+ if token.break_type == :consistent
100
+ :consistent
106
101
  else
107
- Token::BreakType::INCONSISTENT
102
+ :inconsistent
108
103
  end
109
- if config&.indent_anchor == Config::IndentAnchor::ON_BEGIN
104
+ if config&.indent_anchor == :current_offset
110
105
  indent = token.offset
111
106
  if !items.empty?
112
107
  indent += top.offset
@@ -116,55 +111,54 @@ module Oppen
116
111
  end
117
112
  push PrintStackEntry.new indent, type
118
113
  else
119
- push PrintStackEntry.new 0, Token::BreakType::FITS
114
+ push PrintStackEntry.new 0, :fits
120
115
  end
121
116
  end
122
117
 
123
- # Handle End Token.
118
+ # Handle {Token::End}.
124
119
  #
125
120
  # @return [Nil]
126
- #
127
- # @see Token::End
128
121
  def handle_end
129
122
  pop
130
123
  end
131
124
 
132
- # Handle Break Token.
125
+ # Handle {Token::Break}.
133
126
  #
134
- # @param token [Token]
135
- # @param token_width [Integer]
136
- # @param trim_on_break [Integer] Number of trailing whitespace characters to trim.
127
+ # @param token [Token::Break]
128
+ # @param token_width [Integer]
129
+ # @param trim_on_break [Integer]
130
+ # number of trailing whitespace characters to trim.
131
+ # 0 = none.
137
132
  #
138
133
  # @return [Nil]
139
- #
140
- # @see Token::Break
141
134
  def handle_break(token, token_width, trim_on_break: 0)
142
135
  block = top
143
136
  case block.break_type
144
- in Token::BreakType::FITS
137
+ in :fits
138
+ # No new line is needed (the block fits on the line).
145
139
  @space -= token.width
146
140
  write token
147
- in Token::BreakType::CONSISTENT
141
+ in :consistent
148
142
  @space = block.offset - token.offset
149
143
  indent =
150
- if config&.indent_anchor == Config::IndentAnchor::ON_BEGIN
144
+ if config&.indent_anchor == :current_offset
151
145
  token.offset
152
146
  else
153
147
  width - space
154
148
  end
155
- erase(trim_on_break)
149
+ erase trim_on_break
156
150
  write token.line_continuation
157
151
  print_new_line indent
158
- in Token::BreakType::INCONSISTENT
152
+ in :inconsistent
159
153
  if token_width > space
160
154
  @space = block.offset - token.offset
161
155
  indent =
162
- if config&.indent_anchor == Config::IndentAnchor::ON_BEGIN
156
+ if config&.indent_anchor == :current_offset
163
157
  token.offset
164
158
  else
165
159
  width - space
166
160
  end
167
- erase(trim_on_break)
161
+ erase trim_on_break
168
162
  write token.line_continuation
169
163
  print_new_line indent
170
164
  else
@@ -174,14 +168,12 @@ module Oppen
174
168
  end
175
169
  end
176
170
 
177
- # Handle String Token.
171
+ # Handle {Token::String}.
178
172
  #
179
- # @param token [Token]
173
+ # @param token [Token::String]
180
174
  # @param token_width [Integer]
181
175
  #
182
176
  # @return [Nil]
183
- #
184
- # @see Token::String
185
177
  def handle_string(token, token_width)
186
178
  return if token.value.empty?
187
179
 
@@ -193,16 +185,16 @@ module Oppen
193
185
  write token
194
186
  end
195
187
 
196
- # Push a PrintStackEntry into the stack.
188
+ # Push a {PrintStackEntry} into the stack.
197
189
  #
198
190
  # @param print_stack_entry [PrintStackEntry]
199
191
  #
200
192
  # @return [Nil]
201
193
  def push(print_stack_entry)
202
- items.append(print_stack_entry)
194
+ items.append print_stack_entry
203
195
  end
204
196
 
205
- # Pop a PrintStackEntry from the stack.
197
+ # Pop a {PrintStackEntry} from the stack.
206
198
  #
207
199
  # @return [PrintStackEntry]
208
200
  def pop
@@ -226,14 +218,15 @@ module Oppen
226
218
 
227
219
  # Add a new line to the output.
228
220
  #
229
- # @note Called PrintNewLine as well in the original paper.
221
+ # @note Called `PrintNewLine` as well in the original paper.
230
222
  #
231
- # @param amount [Integer] indentation amount.
223
+ # @param amount [Integer]
224
+ # indentation amount.
232
225
  #
233
226
  # @return [Nil]
234
227
  def print_new_line(amount)
235
228
  write new_line
236
- if config&.indent_anchor == Config::IndentAnchor::ON_BEGIN
229
+ if config&.indent_anchor == :current_offset
237
230
  @space = width - top.offset - amount
238
231
  @indent = width - space
239
232
  else
@@ -247,7 +240,7 @@ module Oppen
247
240
  #
248
241
  # @return [Nil]
249
242
  def write(obj)
250
- buffer.write(obj.to_s)
243
+ buffer.write obj.to_s
251
244
  end
252
245
 
253
246
  # Erase the last `count` characters.
@@ -264,7 +257,7 @@ module Oppen
264
257
 
265
258
  # Add indentation by `amount`.
266
259
  #
267
- # @note Called Indent as well in the original paper.
260
+ # @note Called `Indent` as well in the original paper.
268
261
  #
269
262
  # @param amount [Integer]
270
263
  #