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 +4 -4
- data/README.md +55 -11
- data/lib/oppen/print_stack.rb +77 -34
- data/lib/oppen/printer.rb +47 -18
- data/lib/oppen/scan_stack.rb +7 -2
- data/lib/oppen/token.rb +42 -17
- data/lib/oppen/utils.rb +19 -0
- data/lib/oppen/version.rb +1 -1
- data/lib/oppen.rb +132 -5
- data/lib/wadler/print.rb +132 -0
- metadata +5 -6
- data/bin/main.rb +0 -24
- data/bin/repl.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 81c1832fc106f4f4622738ad55ea231c66bdb40a96523d9913b3426781aed296
|
4
|
+
data.tar.gz: 9c5797979a36d529317bf31cdc85a158309c26f5a0c298f8cd1e577a83f4386a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
|
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
|
-
##
|
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
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
24
|
-
|
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. [
|
29
|
-
|
30
|
-
1. [
|
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.
|
data/lib/oppen/print_stack.rb
CHANGED
@@ -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
|
-
#
|
31
|
-
attr_reader :
|
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(
|
39
|
-
@buffer =
|
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
|
-
@
|
43
|
-
@space =
|
58
|
+
@width = width
|
59
|
+
@space = width
|
44
60
|
end
|
45
61
|
|
46
62
|
# Returns the output of the print stack
|
47
63
|
#
|
48
|
-
# @return [
|
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
|
74
|
+
# @param token_width [Integer]
|
59
75
|
#
|
60
76
|
# @return [Nil]
|
61
|
-
def print(token,
|
77
|
+
def print(token, token_width)
|
62
78
|
case token
|
63
79
|
in Token::Begin
|
64
|
-
handle_begin token,
|
80
|
+
handle_begin token, token_width
|
65
81
|
in Token::End
|
66
82
|
handle_end
|
67
83
|
in Token::Break
|
68
|
-
handle_break token,
|
84
|
+
handle_break token, token_width
|
69
85
|
in Token::String
|
70
|
-
handle_string token,
|
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
|
93
|
+
# @param token_width [Integer]
|
78
94
|
#
|
79
95
|
# @return [Nil]
|
80
96
|
#
|
81
97
|
# @see Token::Begin
|
82
|
-
def handle_begin(token,
|
83
|
-
if
|
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
|
-
|
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
|
132
|
+
# @param token_width [Integer]
|
109
133
|
#
|
110
134
|
# @return [Nil]
|
111
135
|
#
|
112
136
|
# @see Token::Break
|
113
|
-
def handle_break(token,
|
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.
|
118
|
-
|
141
|
+
@space -= token.width
|
142
|
+
write token
|
119
143
|
in Token::BreakType::CONSISTENT
|
120
144
|
@space = block.offset - token.offset
|
121
|
-
|
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
|
154
|
+
if token_width > space
|
124
155
|
@space = block.offset - token.offset
|
125
|
-
|
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.
|
128
|
-
|
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
|
174
|
+
# @param token_width [Integer]
|
137
175
|
#
|
138
176
|
# @return [Nil]
|
139
177
|
#
|
140
178
|
# @see Token::String
|
141
|
-
def handle_string(token,
|
142
|
-
@space = [0, space -
|
143
|
-
write token
|
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
|
-
|
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
|
234
|
+
# @param obj [Object]
|
192
235
|
#
|
193
236
|
# @return [Nil]
|
194
|
-
def write(
|
195
|
-
buffer.write(
|
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
|
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
|
-
#
|
52
|
+
# Some description
|
51
53
|
#
|
52
|
-
# @
|
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
|
-
|
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 *
|
76
|
+
n = 3 * width
|
57
77
|
|
78
|
+
@config = config
|
58
79
|
@left = 0
|
59
80
|
@left_total = 1
|
60
|
-
@print_stack = PrintStack.new
|
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 [
|
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.
|
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.
|
197
|
+
print_stack.print token, token.width
|
172
198
|
else
|
173
199
|
advance_right
|
174
200
|
tokens[right] = token
|
175
|
-
size[right] = token.
|
176
|
-
@right_total += token.
|
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,
|
217
|
-
return if
|
245
|
+
def advance_left(token, token_width)
|
246
|
+
return if token_width.negative?
|
218
247
|
|
219
|
-
print_stack.print token,
|
248
|
+
print_stack.print token, token_width
|
220
249
|
|
221
250
|
case token
|
222
251
|
when Token::Break
|
223
|
-
@left_total += token.
|
252
|
+
@left_total += token.width
|
224
253
|
when Token::String
|
225
|
-
@left_total +=
|
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)
|
271
|
+
def check_stack(depth)
|
243
272
|
return if scan_stack.empty?
|
244
273
|
|
245
274
|
x = scan_stack.top
|
data/lib/oppen/scan_stack.rb
CHANGED
@@ -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
|
-
|
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 [
|
31
|
-
def
|
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 [
|
39
|
-
attr_reader :
|
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(
|
44
|
-
|
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
|
-
|
52
|
-
|
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
|
data/lib/oppen/utils.rb
ADDED
@@ -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
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
|
14
|
-
# @param
|
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 [
|
18
|
-
def self.print(
|
19
|
-
|
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
|
data/lib/wadler/print.rb
ADDED
@@ -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
|
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-
|
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
|