oppen 0.9.8 → 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: '008545ae6d86a7415e434ef51a19872fbf5b768db232d70bbac025667e002604'
4
- data.tar.gz: a989a6e8d422c363e7ebca36fc10dac4ad030f145afc2b4bc152c9cff5bdfe49
3
+ metadata.gz: e53fd2f0875228c91da4aaa2259d7b1bf18e0561284513e57181cb6c260ba610
4
+ data.tar.gz: 82c1776d841d6eef2d2071fc923b4864e23e2e68e079fa6caec36b7cca23ffbc
5
5
  SHA512:
6
- metadata.gz: 9a3e32d3a05c638bd26a9e170d737186c9635eeb3c54844869b108c8252fdafa942a05a76a1e3d3d25d5e93a47d076c280f7346c1c313b5c7159a8f3d1166ef6
7
- data.tar.gz: 1f3c076ea8b298f5a772d9c209c4bc64087f6093576e41e2ffb6289b5d0aba08c1b3ed9d3022ebb14b7d7fe4c083804e60219735bce5c00f96785e9dede2bb69
6
+ metadata.gz: 87f3293a723b93c82532045aafe0dd699b6d7ac754893d51ff338448550187f27d75d7bea12fd4035be3f765de660605c56ac431460785387b47b6955b2805bb
7
+ data.tar.gz: 9928e9536e2fb193150fb32d000abf5e5b0017cbbecab050b2b064f14847f72b24ac573ea4779d2fe0bd2f5ac4f5b1ef04a7b1341bdc36e9b74bd652aa3b43ce
data/README.md CHANGED
@@ -22,16 +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
- A few examples of the API usage can be found in [examples/](examples/README.md).
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.
35
31
 
36
32
  ## Oppen vs Wadler
37
33
 
@@ -39,34 +35,109 @@ A few examples of the API usage can be found in [examples/](examples/README.md).
39
35
  and it's not calling ruby's `prettyprint`.
40
36
 
41
37
  Both implementations have their use cases:
42
- 1. Oppen gives more control over tokens sent to the printer.
43
- 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.
44
40
 
45
41
  That being said, both APIs in this gem can achieve the same results, especially
46
42
  on consistent and inconsistent breaking.
47
43
 
48
- ## Noteworthy details
49
-
50
- ### Difference with Oppen's original algorithm
51
-
52
- 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
53
127
  what we expect when writing Ruby code. All correspondences with the algorithm
54
128
  as described in Oppen's paper are noted in the comments of classes and methods.
55
129
  1. We do not raise exceptions when we overflow the margin. The only exceptions
56
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`).
57
137
 
58
- ### Difference with `ruby/prettyprint`
59
-
60
- Oppen's algorithm and `ruby/prettyprint` do not have the same starting positions
61
- for a group's indentation. That's why you need to pay particular attention to
62
- calls for `nest`; you might want to decrease them by `1` if you care about keeping
63
- the same behavior.
64
-
65
- This is what we do in our test suite to verify the correspondence of the `Wadler`
66
- API and the `ruby/prettyprint`. We decided to shift the burden to the user because
67
- 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).
68
139
 
69
- ## Related projects
140
+ ## Related Projects
70
141
 
71
142
  1. [`ruby/prettyprint`](https://github.com/ruby/prettyprint)
72
143
  1. [rustc implementation](https://doc.rust-lang.org/nightly/nightly-rustc/rustc_ast_pretty/pp/index.html)
data/lib/oppen/mixins.rb CHANGED
@@ -24,15 +24,14 @@ module Oppen
24
24
 
25
25
  # @return [String]
26
26
  def tokens_to_wadler(tokens, base_indent: 0, printer_name: 'out', width: tokens.length * 3)
27
- printer = Oppen::Wadler.new(width: width)
28
- printer.base_indent(base_indent)
29
- indent = 2
27
+ printer = Oppen::Wadler.new(base_indent:, indent: 2, width:)
30
28
 
31
29
  handle_break_token = ->(token) {
32
30
  if token.offset.positive?
33
- printer.text "#{printer_name}.nest(#{token.offset}, '', '') {"
34
- printer.nest_open indent
35
- printer.break
31
+ printer
32
+ .text("#{printer_name}.nest(indent: #{token.offset}) {")
33
+ .nest_open
34
+ .break
36
35
  end
37
36
 
38
37
  printer.text(
@@ -45,11 +44,7 @@ module Oppen
45
44
  end,
46
45
  )
47
46
 
48
- if token.offset.positive?
49
- printer.nest_close indent
50
- printer.break
51
- printer.text '}'
52
- end
47
+ printer.nest_close.break.text '}' if token.offset.positive?
53
48
  }
54
49
 
55
50
  tokens.each_with_index do |token, idx|
@@ -59,17 +54,17 @@ module Oppen
59
54
  in Token::Break
60
55
  handle_break_token.(token)
61
56
  in Token::Begin
62
- printer.text "#{printer_name}.group(#{token.offset}, '', '', #{token.break_type.inspect}) {"
63
- printer.nest_open indent
57
+ printer
58
+ .text("#{printer_name}.group(#{token.break_type.inspect}, indent: #{token.offset}) {")
59
+ .nest_open
64
60
  in Token::End
65
- printer.nest_close indent
66
- printer.break
67
- printer.text '}'
61
+ printer.nest_close.break.text '}'
68
62
  in Token::EOF
69
63
  nil
70
64
  end
71
65
  printer.break if !tokens[idx + 1].is_a?(Token::End)
72
66
  end
67
+
73
68
  printer.output
74
69
  end
75
70
  end
@@ -81,7 +81,7 @@ module Oppen
81
81
  in Token::End
82
82
  handle_end
83
83
  in Token::Break
84
- handle_break token, token_width, trim_on_break: trim_on_break
84
+ handle_break token, token_width, trim_on_break:
85
85
  in Token::String
86
86
  handle_string token, token_width
87
87
  end
data/lib/oppen/printer.rb CHANGED
@@ -293,7 +293,7 @@ module Oppen
293
293
  end
294
294
  trim_on_break ||= 0
295
295
 
296
- print_stack.print(token, token_width, trim_on_break: trim_on_break)
296
+ print_stack.print(token, token_width, trim_on_break:)
297
297
 
298
298
  case token
299
299
  when Token::Break
data/lib/oppen/token.rb CHANGED
@@ -74,7 +74,7 @@ module Oppen
74
74
  end
75
75
 
76
76
  def initialize(line_continuation: '', offset: 0)
77
- super(LineBreakString.new, line_continuation: line_continuation, offset: offset)
77
+ super(LineBreakString.new, line_continuation:, offset:)
78
78
  end
79
79
  end
80
80
 
data/lib/oppen/version.rb CHANGED
@@ -6,5 +6,5 @@ module Oppen
6
6
  #
7
7
  # @return [String]
8
8
  # current version
9
- VERSION = '0.9.8' # managed by release.sh
9
+ VERSION = '1.0.0' # managed by release.sh
10
10
  end
data/lib/oppen.rb CHANGED
@@ -64,8 +64,8 @@ module Oppen
64
64
  # config = Oppen::Config.new(indent_anchor: :end_of_previous_line)
65
65
  # out = Oppen::Wadler.new config:, width: 13
66
66
  # out.text 'And she said:'
67
- # out.group(4) {
68
- # out.group(4) {
67
+ # out.group(indent: 4) {
68
+ # out.group(indent: 4) {
69
69
  # out.break
70
70
  # out.text 'Hello, World!'
71
71
  # }
@@ -80,8 +80,8 @@ module Oppen
80
80
  # config = Oppen::Config.new(indent_anchor: :current_offset)
81
81
  # out = Oppen::Wadler.new config:, width: 13
82
82
  # out.text 'And she said:'
83
- # out.group(4) {
84
- # out.group(4) {
83
+ # out.group(indent: 4) {
84
+ # out.group(indent: 4) {
85
85
  # out.break
86
86
  # out.text 'Hello, World!'
87
87
  # }
@@ -123,7 +123,7 @@ module Oppen
123
123
  # #
124
124
  # # eager_print: true =>
125
125
  # # abc defghi
126
- # # jkl
126
+ # # jkl
127
127
  #
128
128
  # @return [Boolean]
129
129
  def eager_print? = @eager_print
@@ -154,10 +154,10 @@ module Oppen
154
154
  # @return [Config]
155
155
  def self.wadler(eager_print: true, trim_trailing_whitespaces: true, upsize_stack: true)
156
156
  new(
157
- eager_print: eager_print,
157
+ eager_print:,
158
158
  indent_anchor: :current_offset,
159
- trim_trailing_whitespaces: trim_trailing_whitespaces,
160
- upsize_stack: upsize_stack,
159
+ trim_trailing_whitespaces:,
160
+ upsize_stack:,
161
161
  )
162
162
  end
163
163
  end
@@ -172,7 +172,7 @@ module Oppen
172
172
  # @return [Token::String]
173
173
  # a new String token.
174
174
  def self.string(value, width: value.length)
175
- Token::String.new(value, width: width)
175
+ Token::String.new(value, width:)
176
176
  end
177
177
 
178
178
  # @return [Token::Whitespace] a new Whitespace token.
@@ -196,7 +196,7 @@ module Oppen
196
196
  #
197
197
  # @see Wadler#break example on `line_continuation`.
198
198
  def self.break(str = ' ', line_continuation: '', offset: 0, width: str.length)
199
- Token::Break.new(str, width: width, line_continuation: line_continuation, offset: offset)
199
+ Token::Break.new(str, width:, line_continuation:, offset:)
200
200
  end
201
201
 
202
202
  # @param line_continuation [String]
@@ -209,7 +209,7 @@ module Oppen
209
209
  #
210
210
  # @see Wadler#break example on `line_continuation`.
211
211
  def self.line_break(line_continuation: '', offset: 0)
212
- Token::LineBreak.new(line_continuation: line_continuation, offset: offset)
212
+ Token::LineBreak.new(line_continuation:, offset:)
213
213
  end
214
214
 
215
215
  # In a consistent group, the presence of a new line inside the group will
@@ -232,7 +232,7 @@ module Oppen
232
232
  #
233
233
  # @see Wadler#group
234
234
  def self.begin_consistent(offset: 2)
235
- Token::Begin.new(break_type: :consistent, offset: offset)
235
+ Token::Begin.new(break_type: :consistent, offset:)
236
236
  end
237
237
 
238
238
  # In an inconsistent group, the presence of a new line inside the group will
@@ -251,7 +251,7 @@ module Oppen
251
251
  #
252
252
  # @see Wadler#group
253
253
  def self.begin_inconsistent(offset: 2)
254
- Token::Begin.new(break_type: :inconsistent, offset: offset)
254
+ Token::Begin.new(break_type: :inconsistent, offset:)
255
255
  end
256
256
 
257
257
  # @return [Token::End] a new End token.
data/lib/wadler/print.rb CHANGED
@@ -18,7 +18,7 @@ module Oppen
18
18
  attr_reader :out
19
19
  # @return [Proc]
20
20
  # space generator, a callable.
21
- attr_reader :space
21
+ attr_reader :space_gen
22
22
  # @return [Array<Token>]
23
23
  # the tokens list that is being built.
24
24
  attr_reader :tokens
@@ -29,13 +29,17 @@ module Oppen
29
29
  # maximum line width.
30
30
  attr_reader :width
31
31
 
32
+ # @param base_indent [Integer]
33
+ # the starting indentation level for the whole printer.
32
34
  # @param config [Config]
33
35
  # to customize the printer's behavior.
36
+ # @param indent [Integer]
37
+ # the default indentation amount for {group} and {nest}.
34
38
  # @param new_line [String]
35
39
  # the new line String.
36
40
  # @param out [Object]
37
41
  # the output string buffer. It should have a `write` and `string` methods.
38
- # @param space [String, Proc]
42
+ # @param space_gen [String, Proc]
39
43
  # indentation string or a string generator.
40
44
  # - If a `String`, spaces will be generated with the the lambda
41
45
  # `->(n){ space * n }`, where `n` is the number of columns to indent.
@@ -44,27 +48,26 @@ module Oppen
44
48
  # @param width [Integer] maximum line width desired.
45
49
  #
46
50
  # @see Token::Whitespace
47
- def initialize(config: Config.wadler, new_line: "\n",
48
- out: StringIO.new, space: ' ',
51
+ def initialize(base_indent: 0, config: Config.wadler, indent: 0, new_line: "\n",
52
+ out: StringIO.new, space_gen: ' ',
49
53
  whitespace: ' ', width: 80)
50
54
  @config = config
51
- @current_indent = 0
52
- @space = space
53
- @width = width
55
+ @current_indent = base_indent
56
+ @indent = indent
54
57
  @new_line = new_line
55
58
  @out = out
59
+ @space_gen = space_gen
56
60
  @tokens = []
57
61
  @whitespace = whitespace
62
+ @width = width
58
63
  end
59
64
 
60
65
  # Add missing {Token::Begin}, {Token::End} or {Token::EOF}.
61
66
  #
62
67
  # @return [Nil]
63
68
  def add_missing_begin_and_end
64
- if !tokens.first.is_a? Token::Begin
65
- tokens.unshift Oppen.begin_consistent(offset: 0)
66
- tokens << Oppen.end
67
- end
69
+ tokens.unshift Oppen.begin_consistent(offset: 0)
70
+ tokens << Oppen.end
68
71
  tokens << Oppen.eof if !tokens.last.is_a?(Oppen::Token::EOF)
69
72
  end
70
73
 
@@ -74,12 +77,12 @@ module Oppen
74
77
  def output
75
78
  add_missing_begin_and_end
76
79
  Oppen.print(
77
- tokens: tokens,
78
- new_line: new_line,
79
- config: config,
80
- space: space,
81
- out: out,
82
- width: width,
80
+ tokens:,
81
+ new_line:,
82
+ config:,
83
+ space: space_gen,
84
+ out:,
85
+ width:,
83
86
  )
84
87
  end
85
88
 
@@ -96,13 +99,11 @@ module Oppen
96
99
  #
97
100
  # @example
98
101
  # out = Oppen::Wadler.new
99
- # out.group {
100
- # out.text('Hello World!')
101
- # }
102
+ # out.text('Hello World!')
102
103
  # out.show_print_commands(out_name: 'out')
103
104
  #
104
105
  # # =>
105
- # # out.group(0, "", "", :consistent) {
106
+ # # out.group(:consistent, indent: 0) {
106
107
  # # out.text("Hello World!", width: 12)
107
108
  # # }
108
109
  #
@@ -114,26 +115,57 @@ module Oppen
114
115
 
115
116
  # Create a new group.
116
117
  #
117
- # @param indent [Integer]
118
+ # @param indent [Integer]
118
119
  # indentation.
119
- # @param open_obj [String]
120
- # opening delimiter.
121
- # @param close_obj [String]
122
- # closing delimiter.
120
+ # @param delim [Nil|String|Symbol|Array<Nil, String, Symbol>]
121
+ # delimiters, to be printed at the start and the end of the group:
122
+ # - If it's nil, nothing will be printed
123
+ # - If it's a Strings or a Symbol, it will be printed at both positions.
124
+ # - If it's an Array of many items, the first two elements will be used
125
+ # for the start and end of the group.
123
126
  # @param break_type [Token::BreakType]
124
127
  # break type.
125
128
  #
126
129
  # @yield
127
130
  # the block of text in a group.
128
131
  #
129
- # @example
132
+ # @example 1 String Delimiter
130
133
  # out = Oppen::Wadler.new
131
- # out.text 'a'
132
- # out.group(2, '{', '}') {
133
- # out.break
134
- # out.text 'b'
135
- # }
136
- # out.output
134
+ # out
135
+ # .text('a')
136
+ # .group(indent: 2, delim: '|') {
137
+ # out.break.text 'b'
138
+ # }
139
+ # puts out.output
140
+ #
141
+ # # =>
142
+ # # a
143
+ # # |
144
+ # # b
145
+ # # |
146
+ #
147
+ # @example 1 Delimiter in Array
148
+ # out = Oppen::Wadler.new
149
+ # out
150
+ # .text('a')
151
+ # .group(indent: 2, delim: ['|']) {
152
+ # out.break.text 'b'
153
+ # }
154
+ # puts out.output
155
+ #
156
+ # # =>
157
+ # # a
158
+ # # |
159
+ # # b
160
+ #
161
+ # @example 2 Delimiters
162
+ # out = Oppen::Wadler.new
163
+ # out
164
+ # .text('a')
165
+ # .group(indent: 2, delim: %i[{ }]) {
166
+ # out.break.text 'b'
167
+ # }
168
+ # puts out.output
137
169
  #
138
170
  # # =>
139
171
  # # a
@@ -143,14 +175,10 @@ module Oppen
143
175
  #
144
176
  # @example Consistent Breaking
145
177
  # out = Oppen::Wadler.new
146
- # out.group(0, '', '', :consistent) {
147
- # out.text 'a'
148
- # out.break
149
- # out.text 'b'
150
- # out.breakable
151
- # out.text 'c'
178
+ # out.group(:consistent) {
179
+ # out.text('a').break.text('b').breakable.text('c')
152
180
  # }
153
- # out.output
181
+ # puts out.output
154
182
  #
155
183
  # # =>
156
184
  # # a
@@ -159,27 +187,26 @@ module Oppen
159
187
  #
160
188
  # @example Inconsistent Breaking
161
189
  # out = Oppen::Wadler.new
162
- # out.group(0, '', '', :inconsistent) {
163
- # out.text 'a'
164
- # out.break
165
- # out.text 'b'
166
- # out.breakable
167
- # out.text 'c'
190
+ # out.group(:inconsistent) {
191
+ # out.text('a').break.text('b').breakable.text('c')
168
192
  # }
169
- # out.output
193
+ # puts out.output
170
194
  #
171
195
  # # =>
172
196
  # # a
173
197
  # # b c
174
198
  #
175
- # @return [Nil]
199
+ # @return [self]
176
200
  #
177
201
  # @see Oppen.begin_consistent
178
202
  # @see Oppen.begin_inconsistent
179
- def group(indent = 0, open_obj = '', close_obj = '',
180
- break_type = :consistent)
181
- raise ArgumentError, "#{open_obj.nil? ? 'open_obj' : 'close_obj'} cannot be nil" \
182
- if open_obj.nil? || close_obj.nil?
203
+ def group(break_type = :consistent, delim: nil, indent: @indent)
204
+ lft, rgt =
205
+ case delim
206
+ in nil then ['', '']
207
+ in String | Symbol then [delim, delim]
208
+ in Array then delim.values_at(0, 1).map(&:to_s)
209
+ end
183
210
 
184
211
  tokens <<
185
212
  case break_type
@@ -189,19 +216,31 @@ module Oppen
189
216
  Oppen.begin_inconsistent(offset: indent)
190
217
  end
191
218
 
192
- if !open_obj.empty?
219
+ if !lft.empty?
193
220
  self.break
194
- text(open_obj)
221
+ text lft
195
222
  end
196
223
 
197
224
  yield
198
225
 
199
- if !close_obj.empty?
226
+ if !rgt.empty?
200
227
  self.break
201
- text(close_obj)
228
+ text rgt
202
229
  end
203
230
 
204
231
  tokens << Oppen.end
232
+
233
+ self
234
+ end
235
+
236
+ # An alias for `group(:consistent, ...)`
237
+ def consistent(...)
238
+ group(:consistent, ...)
239
+ end
240
+
241
+ # An alias for `group(:inconsistent, ...)`
242
+ def inconsistent(...)
243
+ group(:inconsistent, ...)
205
244
  end
206
245
 
207
246
  # Create a new non-strict {group}.
@@ -218,24 +257,24 @@ module Oppen
218
257
  # @note a {nest} will not forcibly indent its content if the break type of
219
258
  # the enclosing {group} is `:inconsistent`.
220
259
  #
221
- # @param indent [Integer]
260
+ # @param delim [Nil|String|Symbol|Array<Nil, String, Symbol>]
261
+ # delimiters, to be printed at the start and the end of the group:
262
+ # - `nil` is always the empty string.
263
+ # - If it's a Strings or a Symbol, it will be printed at both positions.
264
+ # - If it's an Array of many items, the first two elements will be used
265
+ # for the start and end of the group.
266
+ # @param indent [Integer]
222
267
  # indentation.
223
- # @param open_obj [String]
224
- # opening delimiter. A {break} is implicitly slipped after it if it's not empty.
225
- # @param close_obj [String]
226
- # closing delimiter. A {break} is implicitly slipped before it if it's not empty.
227
268
  #
228
269
  # @yield
229
270
  # the block of text in a nest.
230
271
  #
231
272
  # @example
232
273
  # out = Oppen::Wadler.new
233
- # out.nest(2, '{', '}') {
234
- # out.text 'a'
235
- # out.break
236
- # out.text 'b'
274
+ # out.nest(delim: %i[{ }], indent: 2) {
275
+ # out.text('a').break.text('b')
237
276
  # }
238
- # out.output
277
+ # puts out.output
239
278
  #
240
279
  # # =>
241
280
  # # {
@@ -243,15 +282,19 @@ module Oppen
243
282
  # # b
244
283
  # # }
245
284
  #
246
- # @return [Nil]
247
- def nest(indent, open_obj = '', close_obj = '')
248
- raise ArgumentError, "#{open_obj.nil? ? 'open_obj' : 'close_obj'} cannot be nil" \
249
- if open_obj.nil? || close_obj.nil?
285
+ # @return [self]
286
+ def nest(delim: nil, indent: @indent)
287
+ lft, rgt =
288
+ case delim
289
+ in nil then ['', '']
290
+ in String | Symbol then [delim, delim]
291
+ in Array then delim.values_at(0, 1).map(&:to_s)
292
+ end
250
293
 
251
294
  @current_indent += indent
252
295
 
253
- if !open_obj.empty?
254
- text(open_obj)
296
+ if !lft.empty?
297
+ text lft
255
298
  self.break
256
299
  end
257
300
 
@@ -261,10 +304,12 @@ module Oppen
261
304
  @current_indent -= indent
262
305
  end
263
306
 
264
- return if close_obj.empty?
307
+ if !rgt.empty?
308
+ self.break
309
+ text rgt
310
+ end
265
311
 
266
- self.break
267
- text(close_obj)
312
+ self
268
313
  end
269
314
 
270
315
  # Create a new text element.
@@ -272,7 +317,7 @@ module Oppen
272
317
  # @param value [String]
273
318
  # the value of the token.
274
319
  #
275
- # @return [Nil]
320
+ # @return [self]
276
321
  def text(value, width: value.length)
277
322
  if config.trim_trailing_whitespaces? && value.match(/((?:#{Regexp.escape(whitespace)})+)\z/)
278
323
  match = Regexp.last_match(1)
@@ -282,8 +327,9 @@ module Oppen
282
327
  end
283
328
  tokens << Oppen.whitespace(match)
284
329
  else
285
- tokens << Oppen.string(value, width: width)
330
+ tokens << Oppen.string(value, width:)
286
331
  end
332
+ self
287
333
  end
288
334
 
289
335
  # Create a new breakable element.
@@ -295,11 +341,12 @@ module Oppen
295
341
  # @param width [Integer]
296
342
  # the width of the token.
297
343
  #
298
- # @return [Nil]
344
+ # @return [self]
299
345
  #
300
346
  # @see Wadler#break example on `line_continuation`.
301
347
  def breakable(str = ' ', line_continuation: '', width: str.length)
302
- tokens << Oppen.break(str, width: width, line_continuation: line_continuation, offset: current_indent)
348
+ tokens << Oppen.break(str, width:, line_continuation:, offset: current_indent)
349
+ self
303
350
  end
304
351
 
305
352
  # Create a new break element.
@@ -314,55 +361,424 @@ module Oppen
314
361
  # out.text 'b'
315
362
  # out.break line_continuation: '#'
316
363
  # out.text 'c'
317
- # out.output
364
+ # puts out.output
318
365
  #
319
366
  # # =>
320
367
  # # a
321
368
  # # b#
322
369
  # # c
323
370
  #
324
- # @return [Nil]
371
+ # @return [self]
325
372
  def break(line_continuation: '')
326
- tokens << Oppen.line_break(line_continuation: line_continuation, offset: current_indent)
373
+ tokens << Oppen.line_break(line_continuation:, offset: current_indent)
374
+ self
375
+ end
376
+
377
+ # A convenient way to avoid breaking chains of calls.
378
+ #
379
+ # @example
380
+ # out
381
+ # .do { fn_call(fn_arg) }
382
+ # .breakable
383
+ # .text('=')
384
+ # .breakable
385
+ # .do { fn_call(fn_arg) }
386
+ #
387
+ # @yield to execute the passed block
388
+ #
389
+ # @return [self]
390
+ def do
391
+ yield
392
+ self
327
393
  end
328
394
 
329
- # @!group Helpers
395
+ # A means to wrap a piece of code in several ways.
396
+ #
397
+ # @example
398
+ # out
399
+ # .wrap {
400
+ # # all printing instructions here will be deferred.
401
+ # # they will be executed in `when` blocks by calling the `wrapped`.
402
+ # out.text(...)
403
+ # # ...
404
+ # } # This is "wrapped".
405
+ # .when(cond1){ |wrapped|
406
+ # # when cond1 is true you execute this block.
407
+ # out.text("before wrapped")
408
+ # # call the wrapped
409
+ # wrapped.call
410
+ # # and continue printing
411
+ # out.text("after wrapped)
412
+ # }
413
+ # .when(cond2){ |wrapped|
414
+ # # and you cand define many conditions.
415
+ # }
416
+ # .end
417
+ #
418
+ # @example Calling `end` is not needed if there's another call after the last `when`:
419
+ # out
420
+ # .wrap{...} # This is "wrapped".
421
+ # .when(cond1){ |wrapped| ... }
422
+ # .when(cond2){ |wrapped| ... }
423
+ # .text('foo')
424
+ #
425
+ # @return [Wrap]
426
+ def wrap(&blk)
427
+ Wrap.new(blk)
428
+ end
330
429
 
331
- # Set a base indenetaion level for the printer.
430
+ # Produce a separated list.
332
431
  #
333
- # @param indent [Integer]
334
- # the amount of indentation.
432
+ # @example Consistent Breaking
433
+ # puts out.separate((1..3).map(&:to_s), ',') { |i| out.text i}
335
434
  #
336
- # @return [Nil]
337
- def base_indent(indent = 0)
338
- @current_indent = indent if !indent.nil?
435
+ # # =>
436
+ # # 1,
437
+ # # 2,
438
+ # # 3
439
+ #
440
+ # @example Inconsistent Breaking
441
+ # puts out.separate((1..3).map(&:to_s), ',', break_type: :inconsistent) { |i| out.text i}
442
+ #
443
+ # # =>
444
+ # # 1, 2,
445
+ # # 3
446
+ #
447
+ # @param args [String]
448
+ # a list of values.
449
+ # @param sep [String]
450
+ # a separator.
451
+ # @param breakable [String|Nil]
452
+ # adds a `breakable` after the separator.
453
+ # @param break_pos [Symbol]
454
+ # whether to break :before or :after the seraparator.
455
+ # @param break_type [Symbol|Nil]
456
+ # whether the break is :consistent or :inconsistent.
457
+ # If nil is given, the tokens will not be surrounded by a group.
458
+ # @param indent [Boolean|Integer]
459
+ # - If `true`, indent by @indent.
460
+ # - If an 'Integer', indent by its value.
461
+ # @param force_break [Boolean]
462
+ # adds a `break` after the separator.
463
+ # @param line_continuation [String]
464
+ # string to display before new line.
465
+ #
466
+ # @yield to execute the passed block.
467
+ #
468
+ # @return [self]
469
+ def separate(args, sep, breakable: ' ', break_pos: :after,
470
+ break_type: nil, indent: false,
471
+ force_break: false, line_continuation: '')
472
+ if args.is_a?(Enumerator) ? args.count == 1 : args.length == 1
473
+ yield(*args[0])
474
+ return self
475
+ end
476
+
477
+ first = true
478
+ wrap {
479
+ wrap {
480
+ args&.each do |*as|
481
+ if first
482
+ breakable '' if !line_continuation.empty? && break_pos == :after
483
+ first = false
484
+ elsif break_pos == :after
485
+ text sep
486
+ breakable(breakable, line_continuation:) if breakable && !force_break
487
+ self.break(line_continuation:) if force_break
488
+ else
489
+ breakable(breakable, line_continuation:) if breakable && !force_break
490
+ self.break(line_continuation:) if force_break
491
+ text sep
492
+ end
493
+ yield(*as)
494
+ end
495
+ }
496
+ .when(break_type) { |body|
497
+ group(break_type, indent: 0) {
498
+ body.()
499
+ }
500
+ }
501
+ .end
502
+ }
503
+ .when(indent) { |body|
504
+ nest(indent: indent.is_a?(Integer) ? indent : @indent) {
505
+ body.()
506
+ }
507
+ }.end
508
+ breakable('', line_continuation:) if !line_continuation.empty? && !break_type
509
+
510
+ self
511
+ end
512
+
513
+ # A shorhand for `text ' '`.
514
+ #
515
+ # @return [self]
516
+ def space
517
+ text ' '
518
+ end
519
+
520
+ # Surround a block with +lft+ and +rgt+
521
+ #
522
+ # @param lft [String] lft
523
+ # left surrounding string.
524
+ # @param rgt [String] rgt
525
+ # right surrounding string.
526
+ #
527
+ # @yield the passed block to be surrounded with `lft` and `rgt`.
528
+ #
529
+ # @option opts [Boolean] :group (true)
530
+ # whether to create a group enclosing `lft`, `rgt`, and the passed block.
531
+ # @option opts [Boolean] :indent (@indent)
532
+ # whether to indent the passed block.
533
+ # @option opts [String] :lft_breakable ('')
534
+ # left breakable string.
535
+ # @option opts [Boolean] :lft_can_break (true)
536
+ # injects `break` or `breakable` only if true;
537
+ # i.e. `lft_breakable` will be ignored if false.
538
+ # @option opts [Boolean] :lft_force_break (false)
539
+ # force break instead of using `lft_breakable`.
540
+ # @option opts [String] :rgt_breakable ('')
541
+ # right breakable string.
542
+ # @option opts [Boolean] :rgt_can_break (true)
543
+ # injects `break` or `breakable` only if true.
544
+ # i.e. `rgt_breakable` will be ignored if false.
545
+ # @option opts [Boolean] :rgt_force_break (false)
546
+ # force break instead of using `rgt_breakable`.
547
+ #
548
+ # @return [self]
549
+ def surround(lft, rgt, **opts)
550
+ group = opts.fetch(:group, true)
551
+ group_open(break_type: :inconsistent) if group
552
+
553
+ text lft if lft
554
+
555
+ indent = opts.fetch(:indent, @indent)
556
+ nest_open(indent:)
557
+
558
+ lft_breakable = opts.fetch(:lft_breakable, '')
559
+ lft_can_break = opts.fetch(:lft_can_break, true)
560
+ lft_force_break = opts.fetch(:lft_force_break, false)
561
+ if lft && lft_can_break
562
+ if lft_force_break
563
+ self.break
564
+ else
565
+ breakable lft_breakable
566
+ end
567
+ end
568
+
569
+ if block_given?
570
+ yield
571
+ end
572
+
573
+ nest_close
574
+
575
+ rgt_breakable = opts.fetch(:rgt_breakable, '')
576
+ rgt_can_break = opts.fetch(:rgt_can_break, true)
577
+ rgt_force_break = opts.fetch(:rgt_force_break, false)
578
+ if rgt
579
+ if rgt_can_break
580
+ if rgt_force_break
581
+ self.break
582
+ else
583
+ breakable rgt_breakable
584
+ end
585
+ end
586
+ text rgt
587
+ end
588
+
589
+ group_close if group
590
+
591
+ self
592
+ end
593
+
594
+ # @!group Convenience Methods Built On {separate}
595
+
596
+ # Separate args into lines.
597
+ #
598
+ # This is a wrapper around {separate} where `breakable: true`.
599
+ #
600
+ # @see [separate]
601
+ def lines(*args, **kwargs, &)
602
+ separate(*args, **kwargs.merge(force_break: true), &)
603
+ end
604
+
605
+ # Concatenates args.
606
+ #
607
+ # This is a wrapper around {separate} where `breakable: false`.
608
+ #
609
+ # @see [separate]
610
+ def concat(*args, **kwargs, &)
611
+ separate(*args, **kwargs.merge(breakable: false), &)
612
+ end
613
+
614
+ # @!endgroup
615
+ # @!group Convenience Methods Built On {surround}
616
+
617
+ # YARD doesn't drop into blocks, so we can't use metaprogramming
618
+ # to generate all these functions, so we're copy-pastring.
619
+
620
+ # {surround} with `< >`. New lines can appear after and before the delimiters.
621
+ #
622
+ # @param padding [String] ('')
623
+ # Passed to `lft_breakable` and `rgt_breakable`.
624
+ #
625
+ # @return [self]
626
+ def angles(padding: '', **kwargs, &block)
627
+ surround(
628
+ '<', '>',
629
+ **kwargs.merge(lft_breakable: padding, rgt_breakable: padding),
630
+ &block
631
+ )
632
+ end
633
+
634
+ # {surround} with `< >`. New lines cannot appear after and before the delimiters.
635
+ #
636
+ # @return [self]
637
+ def angles_break_both(**kwargs, &)
638
+ angles(**kwargs.merge(lft_force_break: true, rgt_force_break: true), &)
639
+ end
640
+
641
+ # {surround} with `< >`. New lines will appear after and before the delimiters.
642
+ #
643
+ # @return [self]
644
+ def angles_break_none(**kwargs, &)
645
+ angles(**kwargs.merge(lft_can_break: false, rgt_can_break: false), &)
646
+ end
647
+
648
+ # {surround} with `{ }`. New lines can appear after and before the delimiters.
649
+ #
650
+ # @param padding [String] ('')
651
+ # Passed to `lft_breakable` and `rgt_breakable`.
652
+ #
653
+ # @return [self]
654
+ def braces(padding: '', **kwargs, &block)
655
+ surround(
656
+ '{', '}',
657
+ **kwargs.merge(lft_breakable: padding, rgt_breakable: padding),
658
+ &block
659
+ )
660
+ end
661
+
662
+ # {surround} with `{ }`. New lines cannot appear after and before the delimiters.
663
+ #
664
+ # @return [self]
665
+ def braces_break_both(**kwargs, &)
666
+ braces(**kwargs.merge(lft_force_break: true, rgt_force_break: true), &)
667
+ end
668
+
669
+ # {surround} with `{ }`. New lines will appear after and before the delimiters.
670
+ #
671
+ # @return [self]
672
+ def braces_break_none(**kwargs, &)
673
+ braces(**kwargs.merge(lft_can_break: false, rgt_can_break: false), &)
674
+ end
675
+
676
+ # {surround} with `[ ]`. New lines can appear after and before the delimiters.
677
+ #
678
+ # @param padding [String] ('')
679
+ # Passed to `lft_breakable` and `rgt_breakable`.
680
+ #
681
+ # @return [self]
682
+ def brackets(padding: '', **kwargs, &block)
683
+ surround(
684
+ '[', ']',
685
+ **kwargs.merge(lft_breakable: padding, rgt_breakable: padding),
686
+ &block
687
+ )
688
+ end
689
+
690
+ # {surround} with `[ ]`. New lines cannot appear after and before the delimiters.
691
+ #
692
+ # @return [self]
693
+ def brackets_break_both(**kwargs, &)
694
+ brackets(**kwargs.merge(lft_force_break: true, rgt_force_break: true), &)
695
+ end
696
+
697
+ # {surround} with `[ ]`. New lines will appear after and before the delimiters.
698
+ #
699
+ # @return [self]
700
+ def brackets_break_none(**kwargs, &)
701
+ brackets(**kwargs.merge(lft_can_break: false, rgt_can_break: false), &)
702
+ end
703
+
704
+ # {surround} with `( )`. New lines can appear after and before the delimiters.
705
+ #
706
+ # @param padding [String] ('')
707
+ # Passed to `lft_breakable` and `rgt_breakable`.
708
+ #
709
+ # @return [self]
710
+ def parens(padding: '', **kwargs, &block)
711
+ surround(
712
+ '(', ')',
713
+ **kwargs.merge(lft_breakable: padding, rgt_breakable: padding),
714
+ &block
715
+ )
716
+ end
717
+
718
+ # {surround} with `( )`. New lines cannot appear after and before the delimiters.
719
+ #
720
+ # @return [self]
721
+ def parens_break_both(**kwargs, &)
722
+ parens(**kwargs.merge(lft_force_break: true, rgt_force_break: true), &)
723
+ end
724
+
725
+ # {surround} with `( )`. New lines will appear after and before the delimiters.
726
+ #
727
+ # @return [self]
728
+ def parens_break_none(**kwargs, &)
729
+ parens(**kwargs.merge(lft_can_break: false, rgt_can_break: false), &)
730
+ end
731
+
732
+ # {surround} with `` ` ` ``. New lines cannot appear after and before the delimiters
733
+ # unless you specify it with `rgt_can_break` and `lft_can_break`.
734
+ #
735
+ # @return [self]
736
+ def backticks(**kwargs, &)
737
+ surround('`', '`', lft_can_break: false, rgt_can_break: false, **kwargs, &)
738
+ end
739
+
740
+ # {surround} with `" "`. New lines cannot appear after and before the delimiters
741
+ # unless you specify it with `rgt_can_break` and `lft_can_break`.
742
+ #
743
+ # @return [self]
744
+ def quote_double(**kwargs, &)
745
+ surround('"', '"', lft_can_break: false, rgt_can_break: false, **kwargs, &)
746
+ end
747
+
748
+ # {surround} with `' '`. New lines cannot appear after and before the delimiters
749
+ # unless you specify it with `rgt_can_break` and `lft_can_break`.
750
+ #
751
+ # @return [self]
752
+ def quote_single(**kwargs, &)
753
+ surround("'", "'", lft_can_break: false, rgt_can_break: false, **kwargs, &)
339
754
  end
340
755
 
341
756
  # Open a consistent group.
342
757
  #
343
- # @param inconsistent [Boolean]
344
- # whether the break type of the group should be inconsistent.
345
- # @param indent [Integer]
758
+ # @param break_type [Symbol]
759
+ # `:consistent` or `:inconsistent`
760
+ # @param indent [Integer]
346
761
  # the amount of indentation of the group.
347
762
  #
348
- # @return [Nil]
763
+ # @return [self]
349
764
  #
350
765
  # @see Oppen.begin_consistent
351
766
  # @see Oppen.begin_inconsistent
352
- def group_open(inconsistent: false, indent: 0)
353
- tokens <<
354
- if inconsistent
355
- Oppen.begin_inconsistent(offset: indent)
356
- else
357
- Oppen.begin_consistent(offset: indent)
358
- end
767
+ def group_open(break_type: :consistent, indent: 0)
768
+ if %i[consistent inconsistent].none?(break_type)
769
+ raise ArgumentError, '%s is not a valid type. Choose one: :consistent or :inconsistent'
770
+ end
771
+
772
+ tokens << Oppen.send(:"begin_#{break_type}", offset: indent)
773
+ self
359
774
  end
360
775
 
361
776
  # Close a group.
362
777
  #
363
- # @return [Nil]
364
- def group_close(_)
778
+ # @return [self]
779
+ def group_close
365
780
  tokens << Oppen.end
781
+ self
366
782
  end
367
783
 
368
784
  # Open a consistent group and add indent amount.
@@ -370,8 +786,8 @@ module Oppen
370
786
  # @param indent [Integer]
371
787
  # the amount of indentation of the group.
372
788
  #
373
- # @return [Nil]
374
- def indent_open(indent)
789
+ # @return [self]
790
+ def indent_open(indent: @indent)
375
791
  @current_indent += indent
376
792
  group_open
377
793
  end
@@ -381,10 +797,10 @@ module Oppen
381
797
  # @param indent [Integer]
382
798
  # the amount of indentation of the group.
383
799
  #
384
- # @return [Nil]
385
- def indent_close(group, indent)
800
+ # @return [self]
801
+ def indent_close(indent: @indent)
386
802
  @current_indent -= indent
387
- group_close(group)
803
+ group_close
388
804
  end
389
805
 
390
806
  # Open a nest by adding indent.
@@ -392,9 +808,10 @@ module Oppen
392
808
  # @param indent [Integer]
393
809
  # the amount of indentation of the nest.
394
810
  #
395
- # @return [Nil]
396
- def nest_open(indent)
811
+ # @return [self]
812
+ def nest_open(indent: @indent)
397
813
  @current_indent += indent
814
+ self
398
815
  end
399
816
 
400
817
  # Close a nest by subtracting indent.
@@ -402,11 +819,43 @@ module Oppen
402
819
  # @param indent [Integer]
403
820
  # the amount of indentation of the nest.
404
821
  #
405
- # @return [Nil]
406
- def nest_close(indent)
822
+ # @return [self]
823
+ def nest_close(indent: @indent)
407
824
  @current_indent -= indent
825
+ self
408
826
  end
409
827
 
410
828
  # @!endgroup
829
+
830
+ # Helper class to allow conditional printing.
831
+ class Wrap
832
+ def initialize(blk)
833
+ @wrapped = blk
834
+ @wrapper = nil
835
+ end
836
+
837
+ # Conditional.
838
+ def when(cond, &blk)
839
+ if cond
840
+ @wrapper = blk
841
+ end
842
+ self
843
+ end
844
+
845
+ # Flush.
846
+ def end
847
+ @wrapper ? @wrapper.(@wrapped) : @wrapped.()
848
+ end
849
+
850
+ # To re-enable chaining.
851
+ def method_missing(meth, ...)
852
+ self.end.send(meth, ...)
853
+ end
854
+
855
+ # To re-enable chaining.
856
+ def respond_to_missing?(meth, include_private)
857
+ self.end.respond_to_missing?(meth, include_private)
858
+ end
859
+ end
411
860
  end
412
861
  end
metadata CHANGED
@@ -1,18 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oppen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.8
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amine Mike El Maalouf <amine.el-maalouf@epita.fr>
8
8
  - Firas al-Khalil <firas.alkhalil@faveod.com>
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-12-30 00:00:00.000000000 Z
12
+ date: 2025-01-21 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Implementation of the Oppen's pretty printing algorithm
15
- email:
15
+ email:
16
16
  executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
@@ -31,7 +31,7 @@ homepage: http://github.com/Faveod/oppen-ruby
31
31
  licenses:
32
32
  - MIT
33
33
  metadata: {}
34
- post_install_message:
34
+ post_install_message:
35
35
  rdoc_options: []
36
36
  require_paths:
37
37
  - lib
@@ -39,7 +39,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
39
39
  requirements:
40
40
  - - ">="
41
41
  - !ruby/object:Gem::Version
42
- version: '3.0'
42
+ version: '3.1'
43
43
  required_rubygems_version: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -47,7 +47,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
47
47
  version: '0'
48
48
  requirements: []
49
49
  rubygems_version: 3.4.19
50
- signing_key:
50
+ signing_key:
51
51
  specification_version: 4
52
52
  summary: Pretty-printing library
53
53
  test_files: []