oppen 0.1.0 → 0.9.1

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: 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