oppen 0.1.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3566ebc351b729f845e192fb3687bfae4a874879b3741de2e1c43fb97946aa23
4
- data.tar.gz: 9499a00567a1ad0d7f7ffc551573888513eb952945a83d90022a499d31248828
3
+ metadata.gz: 81c1832fc106f4f4622738ad55ea231c66bdb40a96523d9913b3426781aed296
4
+ data.tar.gz: 9c5797979a36d529317bf31cdc85a158309c26f5a0c298f8cd1e577a83f4386a
5
5
  SHA512:
6
- metadata.gz: 0f1348d6dac1afc87a495f2e395ed63a28ae42a722c235f1fe33fc317f1e13b0cbe5d7cae51c67a446755a7f26382c9b7555b80e394e9bdc161e620c2fbfde44
7
- data.tar.gz: 430164e1995f18e4c75cddd59e6e39f6669c9312d01d29cb9670298b3459fb345ccd1a22742728b951859c248e628e0b1b968968ad00f025ea070173816fb476
6
+ metadata.gz: e6a37e27b916fc83323a4b4b387c3990265094ac0c326b5770328ce8d43977b421c8cc0b2092b3c8a877d619ee56578abee1c0f5b2af9390c12f696211eb6446
7
+ data.tar.gz: 2548a7c7cd71299b8779714537916cf603f72a57bf2d02cf0ebcad163bafe7d53299e833cae07ada8a8cc30ddf2ba4e7ef19df6556656c2c920a90478c734ce4
data/README.md CHANGED
@@ -1,30 +1,74 @@
1
1
  # Oppen's Pretty Printer
2
2
  [![CI badge]][CI]
3
3
  [![Docs latest badge]][Docs latest]
4
- <!-- [![rubygems.org badge]][rubygems.org] -->
4
+ [![rubygems.org badge]][rubygems.org]
5
5
 
6
6
  [CI badge]: https://github.com/Faveod/oppen-ruby/actions/workflows/test.yml/badge.svg
7
7
  [CI]: https://github.com/Faveod/oppen-ruby/actions/workflows/test.yml
8
8
  [Docs latest badge]: https://github.com/Faveod/oppen-ruby/actions/workflows/docs.yml/badge.svg
9
9
  [Docs latest]: https://faveod.github.io/oppen-ruby/
10
+ [rubygems.org badge]: https://img.shields.io/gem/v/oppen?label=rubygems.org
11
+ [rubygems.org]: https://rubygems.org/gems/oppen
10
12
 
11
13
  An implementation of the pretty printing algorithm described by
12
14
  [Derek C. Oppen](https://dl.acm.org/doi/pdf/10.1145/357114.357115).
13
15
 
14
- > [!WARNING]
16
+ We also provide an API similar to
17
+ [`ruby/prettyprint`](https://github.com/ruby/prettyprint), which we call
18
+ `Wadler`, in reference to Philip Wadler's paper, [_A prettier
19
+ printer_](https://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf),
20
+ the basis for `prettyprint`. This can be really helpful if you decide to
21
+ transition from `ruby/prettyprint` to this gem.
22
+
23
+ `Wadler` is implemented on top of `Oppen`, and it provides more options than
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]
15
30
  > This is still under development.
16
31
 
17
- ## Difference with the original algorithm
32
+ ## Usage
33
+
34
+ > [!WARNING]
35
+ > Lands when the APIs are stable.
36
+
37
+ ## Oppen vs Wadler
38
+
39
+ `Wadler` calls `Oppen` under the hood, so it's not a separate implementation,
40
+ and it's not calling ruby's `prettyprint`.
41
+
42
+ 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.
45
+
46
+ That being said, both APIs in this gem can achieve the same results, especially
47
+ on consistent and inconsistent breaking.
48
+
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
54
+ what we expect when writing Ruby code. All correspondences with the algorithm
55
+ as described in Oppen's paper are noted in the comments of classes and methods.
56
+ 1. We do not raise exceptions when we overflow the margin. The only exceptions
57
+ that we raise indicate a bug in the implementation. Please report them.
58
+
59
+ ### Difference with `ruby/prettyprint`
18
60
 
19
- We decided to diverge from Oppen's original algorithm to provide a more
20
- idiomatic Ruby experience, in one particular aspect: exceptions/errors: we do
21
- not raise exceptions when we overflow the margin.
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.
22
65
 
23
- The only exceptions that we raise indicate a bug in the implementation. Please
24
- report them.
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.
25
69
 
26
70
  ## Related projects
27
71
 
28
- 1. [Python implementation](https://github.com/stevej2608/oppen-pretty-printer)
29
- as a library.
30
- 1. [rustc implementation](https://doc.rust-lang.org/nightly/nightly-rustc/rustc_ast_pretty/pp/index.html)
72
+ 1. [`ruby/prettyprint`](https://github.com/ruby/prettyprint)
73
+ 1. [rustc implementation](https://doc.rust-lang.org/nightly/nightly-rustc/rustc_ast_pretty/pp/index.html)
74
+ 1. [`stevej2608/oppen-pretty-printer`](https://github.com/stevej2608/oppen-pretty-printer) as a library.
@@ -21,31 +21,47 @@ module Oppen
21
21
  # IO element that builds the output.
22
22
  attr_reader :buffer
23
23
 
24
+ # Config containing customization flags
25
+ attr_reader :config
26
+
27
+ # Callable that generate spaces
28
+ attr_reader :genspace
29
+
24
30
  # Array representing the stack of PrintStackEntries.
25
31
  attr_reader :items
26
32
 
27
33
  # Delimiter between lines in output
28
34
  attr_reader :new_line
29
35
 
30
- # Page margin (Called length in the original paper).
31
- attr_reader :margin
36
+ # Maximum allowed width for printing (Called length in the original paper).
37
+ attr_reader :width
32
38
 
33
39
  # Current available space (Called index in the original paper).
34
40
  #
35
41
  # @return [Integer] Current available space (Called index in the original paper).
36
42
  attr_reader :space
37
43
 
38
- def initialize(margin, new_line)
39
- @buffer = StringIO.new
44
+ def initialize(width, new_line, config, space, out)
45
+ @buffer = out
46
+ @config = config
47
+ @genspace =
48
+ if space.respond_to?(:call)
49
+ raise ArgumentError, 'space argument must be a Proc of arity 1' \
50
+ if space.to_proc.arity != 1
51
+
52
+ space
53
+ else
54
+ ->(n) { space * n }
55
+ end
40
56
  @items = []
41
57
  @new_line = new_line
42
- @margin = margin
43
- @space = margin
58
+ @width = width
59
+ @space = width
44
60
  end
45
61
 
46
62
  # Returns the output of the print stack
47
63
  #
48
- # @return [StringIO]
64
+ # @return [String]
49
65
  def output
50
66
  buffer.string
51
67
  end
@@ -55,39 +71,47 @@ module Oppen
55
71
  # @note Called Print in the original paper.
56
72
  #
57
73
  # @param token [Token]
58
- # @param token_length [Integer]
74
+ # @param token_width [Integer]
59
75
  #
60
76
  # @return [Nil]
61
- def print(token, token_length)
77
+ def print(token, token_width)
62
78
  case token
63
79
  in Token::Begin
64
- handle_begin token, token_length
80
+ handle_begin token, token_width
65
81
  in Token::End
66
82
  handle_end
67
83
  in Token::Break
68
- handle_break token, token_length
84
+ handle_break token, token_width
69
85
  in Token::String
70
- handle_string token, token_length
86
+ handle_string token, token_width
71
87
  end
72
88
  end
73
89
 
74
90
  # Handle Begin Token.
75
91
  #
76
92
  # @param token [Token]
77
- # @param token_length [Integer]
93
+ # @param token_width [Integer]
78
94
  #
79
95
  # @return [Nil]
80
96
  #
81
97
  # @see Token::Begin
82
- def handle_begin(token, token_length)
83
- if token_length > space
98
+ def handle_begin(token, token_width)
99
+ if token_width > space
84
100
  type =
85
101
  if token.break_type == Token::BreakType::CONSISTENT
86
102
  Token::BreakType::CONSISTENT
87
103
  else
88
104
  Token::BreakType::INCONSISTENT
89
105
  end
90
- push PrintStackEntry.new space - token.offset, type
106
+ if config&.indent_anchor == Config::IndentAnchor::ON_BEGIN
107
+ indent = token.offset
108
+ if !items.empty?
109
+ indent += top.offset
110
+ end
111
+ else
112
+ indent = space - token.offset
113
+ end
114
+ push PrintStackEntry.new indent, type
91
115
  else
92
116
  push PrintStackEntry.new 0, Token::BreakType::FITS
93
117
  end
@@ -105,27 +129,41 @@ module Oppen
105
129
  # Handle Break Token.
106
130
  #
107
131
  # @param token [Token]
108
- # @param token_length [Integer]
132
+ # @param token_width [Integer]
109
133
  #
110
134
  # @return [Nil]
111
135
  #
112
136
  # @see Token::Break
113
- def handle_break(token, token_length)
137
+ def handle_break(token, token_width)
114
138
  block = top
115
139
  case block.break_type
116
140
  in Token::BreakType::FITS
117
- @space -= token.blank_space
118
- indent token.blank_space
141
+ @space -= token.width
142
+ write token
119
143
  in Token::BreakType::CONSISTENT
120
144
  @space = block.offset - token.offset
121
- print_new_line margin - space
145
+ indent =
146
+ if config&.indent_anchor == Config::IndentAnchor::ON_BEGIN
147
+ token.offset
148
+ else
149
+ width - space
150
+ end
151
+ write token.line_continuation
152
+ print_new_line indent
122
153
  in Token::BreakType::INCONSISTENT
123
- if token_length > space
154
+ if token_width > space
124
155
  @space = block.offset - token.offset
125
- print_new_line margin - space
156
+ indent =
157
+ if config&.indent_anchor == Config::IndentAnchor::ON_BEGIN
158
+ token.offset
159
+ else
160
+ width - space
161
+ end
162
+ write token.line_continuation
163
+ print_new_line indent
126
164
  else
127
- @space -= token.blank_space
128
- indent token.blank_space
165
+ @space -= token.width
166
+ write token
129
167
  end
130
168
  end
131
169
  end
@@ -133,14 +171,14 @@ module Oppen
133
171
  # Handle String Token.
134
172
  #
135
173
  # @param token [Token]
136
- # @param token_length [Integer]
174
+ # @param token_width [Integer]
137
175
  #
138
176
  # @return [Nil]
139
177
  #
140
178
  # @see Token::String
141
- def handle_string(token, token_length)
142
- @space = [0, space - token_length].max
143
- write token.value
179
+ def handle_string(token, token_width)
180
+ @space = [0, space - token_width].max
181
+ write token
144
182
  end
145
183
 
146
184
  # Push a PrintStackEntry into the stack.
@@ -183,16 +221,21 @@ module Oppen
183
221
  # @return [Nil]
184
222
  def print_new_line(amount)
185
223
  write new_line
186
- indent amount
224
+ if config&.indent_anchor == Config::IndentAnchor::ON_BEGIN
225
+ @space = width - top.offset - amount
226
+ indent width - space
227
+ else
228
+ indent amount
229
+ end
187
230
  end
188
231
 
189
232
  # Write a string to the output.
190
233
  #
191
- # @param string [String]
234
+ # @param obj [Object]
192
235
  #
193
236
  # @return [Nil]
194
- def write(string)
195
- buffer.write(string)
237
+ def write(obj)
238
+ buffer.write(obj.to_s)
196
239
  end
197
240
 
198
241
  # Add indentation by `amount`.
@@ -203,7 +246,7 @@ module Oppen
203
246
  #
204
247
  # @return [Nil]
205
248
  def indent(amount)
206
- write ' ' * amount
249
+ write genspace.call(amount)
207
250
  end
208
251
  end
209
252
  end
data/lib/oppen/printer.rb CHANGED
@@ -4,11 +4,13 @@ require 'stringio'
4
4
 
5
5
  require_relative 'scan_stack'
6
6
  require_relative 'print_stack'
7
+ require_relative 'utils'
7
8
 
8
9
  # Oppen.
9
10
  module Oppen
10
11
  # Oppen pretty-printer.
11
12
  class Printer
13
+ attr_reader :config
12
14
  # Ring buffer left index.
13
15
  #
14
16
  # @note Called left as well in the original paper.
@@ -47,25 +49,44 @@ module Oppen
47
49
  # @note Called token in the original paper.
48
50
  attr_reader :tokens
49
51
 
50
- # @note Called PrettyPrintInit in the original paper.
52
+ # Some description
51
53
  #
52
- # @param margin [Integer] maximum line width desired.
54
+ # @example
55
+ # "This is a string" # => and this is a comment
56
+ # out = Oppen::Wadler.new (margin: 13) # Hawn
57
+ # # Baliz
58
+ #
59
+ # @example
60
+ # "This is a string" # => and this is a comment
61
+ # # var = 12
62
+ #
63
+ # @param width [Integer] maximum line width desired.
53
64
  # @param new_line [String] the delimiter between lines.
54
- def initialize(margin, new_line)
65
+ # @param config [Config]
66
+ # @param space [String, Proc] could be a String or a callable.
67
+ # If it's a string, spaces will be generated with the the
68
+ # lambda `->(n){ n * space }`, where `n` is the number of columns
69
+ # to indent.
70
+ # If it's a callable, it will receive `n` and it needs to return
71
+ # a string.
72
+ # @param out [Object] should have a write and string method
73
+ def initialize(width, new_line, config = Config.oppen,
74
+ space = ' ', out = StringIO.new)
55
75
  # Maximum size if the stacks
56
- n = 3 * margin
76
+ n = 3 * width
57
77
 
78
+ @config = config
58
79
  @left = 0
59
80
  @left_total = 1
60
- @print_stack = PrintStack.new margin, new_line
81
+ @print_stack = PrintStack.new width, new_line, config, space, out
61
82
  @right = 0
62
83
  @right_total = 1
63
- @scan_stack = ScanStack.new n
84
+ @scan_stack = ScanStack.new n, config
64
85
  @size = Array.new n
65
86
  @tokens = Array.new n
66
87
  end
67
88
 
68
- # @return [StringIO]
89
+ # @return [String]
69
90
  def output
70
91
  print_stack.output
71
92
  end
@@ -137,6 +158,11 @@ module Oppen
137
158
  tokens[right] = token
138
159
  size[right] = -1
139
160
  scan_stack.push right
161
+ if config&.eager_print? &&
162
+ (!scan_stack.empty? && right_total - left_total < print_stack.space)
163
+ check_stack 0
164
+ advance_left tokens[left], size[left]
165
+ end
140
166
  end
141
167
  end
142
168
 
@@ -158,7 +184,7 @@ module Oppen
158
184
  scan_stack.push right
159
185
  tokens[right] = token
160
186
  size[right] = -right_total
161
- @right_total += token.blank_space
187
+ @right_total += token.width
162
188
  end
163
189
 
164
190
  # Handle String Token.
@@ -168,12 +194,12 @@ module Oppen
168
194
  # @see Token::String
169
195
  def handle_string(token)
170
196
  if scan_stack.empty?
171
- print_stack.print token, token.length
197
+ print_stack.print token, token.width
172
198
  else
173
199
  advance_right
174
200
  tokens[right] = token
175
- size[right] = token.length
176
- @right_total += token.length
201
+ size[right] = token.width
202
+ @right_total += token.width
177
203
  check_stream
178
204
  end
179
205
  end
@@ -204,7 +230,10 @@ module Oppen
204
230
  @right = (right + 1) % scan_stack.length
205
231
  return if right != left
206
232
 
207
- raise 'Token queue full'
233
+ raise 'Token queue full' if !config.upsize_stack?
234
+
235
+ @size, = Utils.upsize_circular_array(@size, @left)
236
+ @tokens, @left, @right = Utils.upsize_circular_array(@tokens, @left)
208
237
  end
209
238
 
210
239
  # Advances the `left` pointer and lets the print stack
@@ -213,16 +242,16 @@ module Oppen
213
242
  # @note Called AdvanceLeft as well in the original paper.
214
243
  #
215
244
  # @return [Nil]
216
- def advance_left(token, token_length)
217
- return if token_length.negative?
245
+ def advance_left(token, token_width)
246
+ return if token_width.negative?
218
247
 
219
- print_stack.print token, token_length
248
+ print_stack.print token, token_width
220
249
 
221
250
  case token
222
251
  when Token::Break
223
- @left_total += token.blank_space
252
+ @left_total += token.width
224
253
  when Token::String
225
- @left_total += token_length
254
+ @left_total += token_width
226
255
  end
227
256
 
228
257
  return if left == right
@@ -239,7 +268,7 @@ module Oppen
239
268
  # @param depth [Integer] depth of the group
240
269
  #
241
270
  # @return [Nil]
242
- def check_stack(depth) # rubocop:disable Metrics/AbcSize
271
+ def check_stack(depth)
243
272
  return if scan_stack.empty?
244
273
 
245
274
  x = scan_stack.top
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'utils'
4
+
3
5
  # Oppen.
4
6
  module Oppen
5
7
  # A fixed-size stack that can be popped from top and bottom.
6
8
  class ScanStack
7
- def initialize(size)
9
+ def initialize(size, config)
8
10
  @bottom = 0
11
+ @config = config
9
12
  @empty = true
10
13
  @stack = Array.new(size)
11
14
  @top = 0
@@ -68,7 +71,9 @@ module Oppen
68
71
  else
69
72
  @top = increment(@top)
70
73
  if @top == @bottom
71
- raise 'Stack full'
74
+ raise 'Stack full' if !@config.upsize_stack?
75
+
76
+ @stack, @bottom, @top = Utils.upsize_circular_array(@stack, @bottom)
72
77
  end
73
78
  end
74
79
  @stack[@top] = value
data/lib/oppen/token.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  # Oppen.
4
4
  module Oppen
5
5
  # Token.
6
- module Token
6
+ class Token
7
7
  # BreakType.
8
8
  #
9
9
  # FITS => No break is needed (the block fits on the line).
@@ -18,43 +18,67 @@ module Oppen
18
18
  CONSISTENT = 2
19
19
  end
20
20
 
21
+ # Default token width
22
+ # @return [Integer]
23
+ def width = 0
24
+
21
25
  # String Token.
22
- class String
26
+ class String < Token
23
27
  # @return [String] String value.
24
28
  attr_reader :value
29
+ # @return [Integer]
30
+ attr_reader :width
25
31
 
26
- def initialize(value)
32
+ def initialize(value, width: value.length)
27
33
  @value = value
34
+ @width = width
35
+ super()
28
36
  end
29
37
 
30
- # @return [Integer]
31
- def length
32
- value.length
33
- end
38
+ # @return [String]
39
+ def to_s = value
34
40
  end
35
41
 
36
42
  # Break Token.
37
- class Break
38
- # @return [Integer] Number of blank spaces.
39
- attr_reader :blank_space
43
+ class Break < Token
44
+ # @return [String] If a new line is needed display this string before the new line
45
+ attr_reader :line_continuation
40
46
  # @return [Integer] Indentation.
41
47
  attr_reader :offset
48
+ # @return [String] Break strings.
49
+ attr_reader :str
50
+ # @return [Integer]
51
+ attr_reader :width
42
52
 
43
- def initialize(blank_space: 1, offset: 0)
44
- @blank_space = blank_space
53
+ def initialize(str = ' ', width: str.length, line_continuation: '', offset: 0)
54
+ raise ArgumentError, 'line_continuation cannot be nil' if line_continuation.nil?
55
+
56
+ @line_continuation = line_continuation
45
57
  @offset = offset
58
+ @str = str
59
+ @width = width
60
+ super()
46
61
  end
62
+
63
+ # @return [String]
64
+ def to_s = str
47
65
  end
48
66
 
49
67
  # Distinguished instance of Break which forces a line break.
50
68
  class LineBreak < Break
51
- def initialize(offset: 0)
52
- super(blank_space: 9999, offset:)
69
+ # Mock string that represents an infinite string to force new line.
70
+ class LineBreakString
71
+ # @return [Integer]
72
+ def length = 999_999
73
+ end
74
+
75
+ def initialize(line_continuation: '', offset: 0)
76
+ super(LineBreakString.new, line_continuation:, offset:)
53
77
  end
54
78
  end
55
79
 
56
80
  # Begin Token.
57
- class Begin
81
+ class Begin < Token
58
82
  # @return [BreakType]
59
83
  attr_reader :break_type
60
84
  # @return [Integer] Indentation.
@@ -63,16 +87,17 @@ module Oppen
63
87
  def initialize(break_type: BreakType::INCONSISTENT, offset: 2)
64
88
  @offset = offset
65
89
  @break_type = break_type
90
+ super()
66
91
  end
67
92
  end
68
93
 
69
94
  # End Token
70
- class End
95
+ class End < Token
71
96
  nil
72
97
  end
73
98
 
74
99
  # EOF Token
75
- class EOF
100
+ class EOF < Token
76
101
  nil
77
102
  end
78
103
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Oppen.
4
+ module Oppen
5
+ # Utils.
6
+ module Utils
7
+ # Rotates circular array and triples its size.
8
+ # @param arr [Array]
9
+ # @param offset [Integer] Rotation amount
10
+ #
11
+ # @return [Array(Array, Integer, Integer)] upsized array, lhs, rhs
12
+ def self.upsize_circular_array(arr, offset)
13
+ size = arr.size
14
+ arr = arr.rotate(offset)
15
+ arr.fill(nil, size, 2 * size)
16
+ [arr, 0, size]
17
+ end
18
+ end
19
+ end
data/lib/oppen/version.rb CHANGED
@@ -5,5 +5,5 @@ module Oppen
5
5
  # Oppen version
6
6
  #
7
7
  # @return [String] current version
8
- VERSION = '0.1.0' # managed by release.sh
8
+ VERSION = '0.9.1' # managed by release.sh
9
9
  end
data/lib/oppen.rb CHANGED
@@ -5,21 +5,148 @@ require_relative 'oppen/print_stack'
5
5
  require_relative 'oppen/scan_stack'
6
6
  require_relative 'oppen/token'
7
7
  require_relative 'oppen/version'
8
+ require_relative 'wadler/print'
8
9
 
9
10
  # Oppen.
10
11
  module Oppen
11
12
  # Entry point of the pretty printer.
12
13
  #
13
- # @param tokens [Array[Token]] the list of tokens to be printed
14
- # @param margin [Integer] maximum line width desired
14
+ # @param config [Config]
15
+ # @param space [String, Proc] could be a String or a callable.
16
+ # If it's a string, spaces will be generated with the the
17
+ # lambda `->(n){ n * space }`, where `n` is the number of columns
18
+ # to indent.
19
+ # If it's a callable, it will receive `n` and it needs to return
20
+ # a string.
15
21
  # @param new_line [String] the delimiter between lines
22
+ # @param out [Object] should have a write and string method
23
+ # @param tokens [Array[Token]] the list of tokens to be printed
24
+ # @param width [Integer] maximum line width desired
16
25
  #
17
- # @return [StringIO] output of the pretty printer
18
- def self.print(tokens: [], margin: 80, new_line: "\n")
19
- printer = Printer.new margin, new_line
26
+ # @return [String] output of the pretty printer
27
+ def self.print(config: Config.oppen, space: ' ',
28
+ new_line: "\n", out: StringIO.new, tokens: [], width: 80)
29
+ printer = Printer.new width, new_line, config, space, out
20
30
  tokens.each do |token|
21
31
  printer.print token
22
32
  end
23
33
  printer.output
24
34
  end
35
+
36
+ # Config.
37
+ class Config
38
+ # IndentAnchor.
39
+ #
40
+ # ON_BREAK => anchor on break position (as in Oppen's original paper)
41
+ # ON_BEGIN => anchor on begin block position
42
+ module IndentAnchor
43
+ # @return [Integer]
44
+ ON_BREAK = 0
45
+ # @return [Integer]
46
+ ON_BEGIN = 1
47
+ end
48
+
49
+ attr_accessor :indent_anchor
50
+
51
+ def initialize(indent_anchor: IndentAnchor::ON_BREAK, eager_print: false, upsize_stack: false)
52
+ @indent_anchor = indent_anchor
53
+ @eager_print = eager_print
54
+ @upsize_stack = upsize_stack
55
+ end
56
+
57
+ # Print groups eagerly
58
+ #
59
+ # @example
60
+ # out = Oppen::Wadler.new (width: 13)
61
+ # out.group {
62
+ # out.group {
63
+ # out.text 'abc'
64
+ # out.breakable
65
+ # out.text 'def'
66
+ # }
67
+ # out.group {
68
+ # out.text 'ghi'
69
+ # out.breakable
70
+ # out.text 'jkl'
71
+ # }
72
+ # }
73
+ # out.output
74
+ #
75
+ # # eager_print: false
76
+ # # =>
77
+ # # abc
78
+ # # defghi jkl
79
+ # #
80
+ # # eager_print: true
81
+ # # =>
82
+ # # abc defghi
83
+ # # jkl
84
+ #
85
+ # @return [Boolean]
86
+ def eager_print? = @eager_print
87
+
88
+ def upsize_stack? = @upsize_stack
89
+
90
+ # Default config for Oppen usage
91
+ # @return [Config]
92
+ def self.oppen
93
+ new
94
+ end
95
+
96
+ # Default config for Wadler usage
97
+ # @return [Config]
98
+ def self.wadler(eager_print: true, upsize_stack: true)
99
+ new(indent_anchor: IndentAnchor::ON_BEGIN, eager_print:, upsize_stack:)
100
+ end
101
+ end
102
+
103
+ # @param value [String]
104
+ # @param width [Integer] token width that defaults to value.length
105
+ #
106
+ # @return [Oppen::Token::String] a new String token
107
+ def self.string(value, width: value.length)
108
+ Token::String.new(value, width:)
109
+ end
110
+
111
+ # @param str [String]
112
+ # @param line_continuation [String] If a new line is needed display this string before the new line
113
+ # @param offset [Integer]
114
+ # @param width [Integer] token width that defaults to str.length
115
+ #
116
+ # @return [Oppen::Token::Break] a new Break token
117
+ def self.break(str = ' ', line_continuation: '', offset: 0, width: str.length)
118
+ Token::Break.new(str, width:, line_continuation:, offset:)
119
+ end
120
+
121
+ # @param line_continuation [String] If a new line is needed display this string before the new line
122
+ # @param offset [Integer]
123
+ #
124
+ # @return [Oppen::Token::LineBreak] a new LineBreak token
125
+ def self.line_break(line_continuation: '', offset: 0)
126
+ Token::LineBreak.new(line_continuation:, offset:)
127
+ end
128
+
129
+ # @param offset [Integer]
130
+ #
131
+ # @return [Oppen::Token::Begin] a new consistent Begin token
132
+ def self.begin_consistent(offset: 2)
133
+ Token::Begin.new(break_type: Token::BreakType::CONSISTENT, offset:)
134
+ end
135
+
136
+ # @param offset [Integer]
137
+ #
138
+ # @return [Oppen::Token::Begin] a new inconsistent Begin token
139
+ def self.begin_inconsistent(offset: 2)
140
+ Token::Begin.new(break_type: Token::BreakType::INCONSISTENT, offset:)
141
+ end
142
+
143
+ # @return [Oppen::Token::End] a new End token
144
+ def self.end
145
+ Token::End.new
146
+ end
147
+
148
+ # @return [Oppen::Token::EOF] a new EOF token
149
+ def self.eof
150
+ Token::EOF.new
151
+ end
25
152
  end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Oppen.
4
+ module Oppen
5
+ # Wadler.
6
+ class Wadler
7
+ attr_reader :config
8
+ attr_reader :current_indent
9
+ attr_reader :space
10
+ attr_reader :new_line
11
+ attr_reader :out
12
+ attr_reader :tokens
13
+ attr_reader :width
14
+
15
+ # @param config [Oppen::Config]
16
+ # @param space [String, Proc] could be a String or a callable.
17
+ # If it's a string, spaces will be generated with the the
18
+ # lambda `->(n){ n * space }`, where `n` is the number of columns
19
+ # to indent.
20
+ # If it's a callable, it will receive `n` and it needs to return
21
+ # a string.
22
+ # @param new_line [String]
23
+ # @param out [Object] should have a write and string method
24
+ # @param width [Integer]
25
+ def initialize(config: Config.wadler, space: ' ',
26
+ new_line: "\n", out: StringIO.new, width: 80)
27
+ @config = config
28
+ @current_indent = 0
29
+ @space = space
30
+ @width = width
31
+ @new_line = new_line
32
+ @out = out
33
+ @tokens = []
34
+ end
35
+
36
+ # @return [String]
37
+ def output
38
+ if !tokens.last.is_a? Oppen::Token::EOF
39
+ tokens << Oppen.eof
40
+ end
41
+ Oppen.print(tokens:, new_line:, config:, space:, out:, width:)
42
+ end
43
+
44
+ # @param indent [Integer] group indentation
45
+ # @param open_obj [String] group opening delimiter
46
+ # @param close_obj [String] group closing delimiter
47
+ # @param break_type [Oppen::Token::BreakType] group breaking type
48
+ #
49
+ # @yield the block of text in a group
50
+ #
51
+ # @return [Nil]
52
+ def group(indent = 0, open_obj = '', close_obj = '',
53
+ break_type = Oppen::Token::BreakType::CONSISTENT)
54
+ raise ArgumentError, "#{open_obj.nil? ? 'open_obj' : 'close_obj'} cannot be nil" \
55
+ if open_obj.nil? || close_obj.nil?
56
+
57
+ tokens <<
58
+ case break_type
59
+ in Oppen::Token::BreakType::CONSISTENT
60
+ Oppen.begin_consistent(offset: indent)
61
+ in Oppen::Token::BreakType::INCONSISTENT
62
+ Oppen.begin_inconsistent(offset: indent)
63
+ end
64
+
65
+ if !open_obj.empty?
66
+ self.break
67
+ text(open_obj)
68
+ end
69
+
70
+ yield
71
+
72
+ if !close_obj.empty?
73
+ self.break
74
+ text(close_obj)
75
+ end
76
+
77
+ tokens << Oppen.end
78
+ end
79
+
80
+ # @param indent [Integer] nest indentation
81
+ # @param open_obj [String] nest opening delimiter
82
+ # @param close_obj [String] nest closing delimiter
83
+ # @param break_type [Oppen::Token::BreakType] nest breaking type
84
+ #
85
+ # @return [Nil]
86
+ def nest(indent, open_obj = '', close_obj = '',
87
+ break_type = Oppen::Token::BreakType::CONSISTENT)
88
+ raise ArgumentError, "#{open_obj.nil? ? 'open_obj' : 'close_obj'} cannot be nil" \
89
+ if open_obj.nil? || close_obj.nil?
90
+
91
+ @current_indent += indent
92
+
93
+ if !open_obj.empty?
94
+ text(open_obj)
95
+ self.break
96
+ end
97
+
98
+ begin
99
+ yield
100
+ ensure
101
+ @current_indent -= indent
102
+ end
103
+
104
+ return if close_obj.empty?
105
+
106
+ self.break
107
+ text(close_obj)
108
+ end
109
+
110
+ # @param value [String]
111
+ #
112
+ # @return [Nil]
113
+ def text(value, width: value.length)
114
+ tokens << Oppen.string(value, width:)
115
+ end
116
+
117
+ # @param str [String]
118
+ # @param line_continuation [String] If a new line is needed display this string before the new line
119
+ #
120
+ # @return [Nil]
121
+ def breakable(str = ' ', width: str.length, line_continuation: '')
122
+ tokens << Oppen.break(str, width:, line_continuation:, offset: current_indent)
123
+ end
124
+
125
+ # @param line_continuation [String] If a new line is needed display this string before the new line
126
+ #
127
+ # @return [Nil]
128
+ def break(line_continuation: '')
129
+ tokens << Oppen.line_break(line_continuation:, offset: current_indent)
130
+ end
131
+ end
132
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oppen
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amine Mike El Maalouf <amine.el-maalouf@epita.fr>
@@ -9,25 +9,24 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2024-09-13 00:00:00.000000000 Z
12
+ date: 2024-10-22 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Implementation of the Oppen's pretty printing algorithm
15
15
  email:
16
- executables:
17
- - main.rb
16
+ executables: []
18
17
  extensions: []
19
18
  extra_rdoc_files: []
20
19
  files:
21
20
  - LICENSE
22
21
  - README.md
23
- - bin/main.rb
24
- - bin/repl.rb
25
22
  - lib/oppen.rb
26
23
  - lib/oppen/print_stack.rb
27
24
  - lib/oppen/printer.rb
28
25
  - lib/oppen/scan_stack.rb
29
26
  - lib/oppen/token.rb
27
+ - lib/oppen/utils.rb
30
28
  - lib/oppen/version.rb
29
+ - lib/wadler/print.rb
31
30
  homepage: http://github.com/Faveod/oppen-ruby
32
31
  licenses:
33
32
  - MIT
data/bin/main.rb DELETED
@@ -1,24 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- lib = File.expand_path('lib', __dir__)
5
- $LOAD_PATH.unshift(lib) if !$LOAD_PATH.include?(lib)
6
-
7
- require 'oppen'
8
-
9
- list = [
10
- Oppen::Token::Begin.new,
11
- Oppen::Token::String.new('XXXXXXXXXX'),
12
- Oppen::Token::Break.new,
13
- Oppen::Token::String.new('+'),
14
- Oppen::Token::Break.new,
15
- Oppen::Token::String.new('YYYYYYYYYY'),
16
- Oppen::Token::Break.new,
17
- Oppen::Token::String.new('+'),
18
- Oppen::Token::Break.new,
19
- Oppen::Token::String.new('ZZZZZZZZZZ'),
20
- Oppen::Token::End.new,
21
- Oppen::Token::EOF.new,
22
- ]
23
-
24
- puts Oppen.print tokens: list, line_width: 25
data/bin/repl.rb DELETED
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'colored'
4
-
5
- # Fancy
6
- WELCOME = <<-'YGM'
7
- __ __ ___ __ __ ____ ____ ___ ___ ___ ____ ____ ___ ___ ___
8
- | | | / \ | | || \ / | / _]| | || \ / || | | / _]
9
- | | || || | || D )| __| / [_ | _ _ || _ || o || _ _ | / [_
10
- | ~ || O || | || / | | || _]| \_/ || | || || \_/ || _]
11
- |___, || || : || \ | |_ || [_ | | || | || _ || | || [_
12
- | || || || . \| ______ ______ ______ __ || |
13
- |____/ \___/ \__,_||__|\_||___ /\ == \ /\ ___\ /\ == \ /\ \ ___||_____|
14
- \ \ __< \ \ __\ \ \ _-/ \ \ \____
15
- \ \_\ \_\ \ \_____\ \ \_\ \ \_____\
16
- \/_/ /_/ \/_____/ \/_/ \/_____/
17
- YGM
18
-
19
- puts WELCOME.green