soml-lang 0.0.1 → 0.0.2
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/lib/soml/duration.rb +14 -36
- data/lib/soml/edit.rb +42 -23
- data/lib/soml/error.rb +27 -17
- data/lib/soml/format.rb +14 -4
- data/lib/soml/generator.rb +33 -23
- data/lib/soml/parser.rb +172 -200
- data/lib/soml/tree.rb +195 -64
- data/lib/soml/version.rb +1 -1
- data/lib/soml.rb +49 -43
- data/readme.md +114 -60
- data/sig/soml.rbs +98 -2
- metadata +4 -2
data/readme.md
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
## Highlights
|
|
9
9
|
|
|
10
|
-
- Strict: every rule in the specification
|
|
10
|
+
- Strict: enforces every rule in the specification, with the same error messages as the JavaScript reference implementation
|
|
11
11
|
- Exact: an int is an `Integer`, a float is a `Float`, an instant is a `Time` exact to the nanosecond, and a duration is an exact count of nanoseconds
|
|
12
|
-
- Canonical: `SOML.generate(value, canonical: true)` writes the one canonical form, so equal values give equal text
|
|
12
|
+
- Canonical: `SOML.generate(value, canonical: true)` writes the one canonical form, so equal values give equal text
|
|
13
13
|
- Familiar: the API of `json`, with `parse`, `load_file`, `generate`, `symbolize_names:`, and `freeze:`
|
|
14
|
-
- Lossless: `SOML.format`
|
|
15
|
-
- Safe: linear time and memory on every input
|
|
14
|
+
- Lossless: `SOML.format` and `SOML.edit` keep comments, member order, and spelling
|
|
15
|
+
- Safe: linear time and memory on every input, no stack overflow on deep input, and every document error is a `SOML::ParseError`
|
|
16
16
|
- Ractor-safe, with no dependencies
|
|
17
17
|
|
|
18
18
|
## Install
|
|
@@ -27,7 +27,7 @@ Or in a `Gemfile`:
|
|
|
27
27
|
gem 'soml-lang'
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Requires Ruby 4.0 or later.
|
|
30
|
+
[On RubyGems](https://rubygems.org/gems/soml-lang). Requires Ruby 4.0 or later.
|
|
31
31
|
|
|
32
32
|
## Usage
|
|
33
33
|
|
|
@@ -71,7 +71,7 @@ config[:postgres][:host]
|
|
|
71
71
|
#=> "db.internal"
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
Write a file in binary mode, so that Windows does not
|
|
74
|
+
Write a file in binary mode, so that Windows does not write CRLF, which SOML does not allow:
|
|
75
75
|
|
|
76
76
|
```ruby
|
|
77
77
|
File.binwrite('config.soml', SOML.generate(config))
|
|
@@ -91,7 +91,7 @@ File.binwrite('config.soml', SOML.generate(config))
|
|
|
91
91
|
| array | `Array` | |
|
|
92
92
|
| object | `Hash` with `String` keys, in document order, or `Symbol` keys with `symbolize_names: true` | `Symbol` keys |
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
`3` and `3.0` keep their types in both directions:
|
|
95
95
|
|
|
96
96
|
```ruby
|
|
97
97
|
SOML.parse("a: 3\nb: 3.0")
|
|
@@ -102,14 +102,14 @@ SOML.generate({a: 3, b: 3.0})
|
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
> [!IMPORTANT]
|
|
105
|
-
> Ruby
|
|
105
|
+
> In Ruby, `3 == 3.0`, so `SOML.parse('a: 3') == SOML.parse('a: 3.0')` is true. Use `eql?` to also compare the types.
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
Two things always hold:
|
|
108
108
|
|
|
109
109
|
- `SOML.parse(SOML.generate(value))` equals `value`, for a value made of the types `parse` returns. (A `Time` comes back in UTC, a `Symbol` as a `String`, and `-0.0` as `0.0`.)
|
|
110
|
-
- `SOML.generate(SOML.parse(text), canonical: true)` is the canonical form of `text
|
|
110
|
+
- `SOML.generate(SOML.parse(text), canonical: true)` is the canonical form of `text`.
|
|
111
111
|
|
|
112
|
-
An instant is a `Time` in UTC, because the offset is
|
|
112
|
+
An instant is a `Time` in UTC, because the offset is not part of its value:
|
|
113
113
|
|
|
114
114
|
```ruby
|
|
115
115
|
SOML.parse('a: 2026-09-19T14:00:00.123456789+02:00')['a']
|
|
@@ -120,11 +120,11 @@ SOML.parse('a: 2026-09-19T14:00:00.123456789+02:00')['a']
|
|
|
120
120
|
|
|
121
121
|
### SOML.parse(source, symbolize_names: false, freeze: false)
|
|
122
122
|
|
|
123
|
-
Parse a document. Returns a `Hash` or an `Array
|
|
123
|
+
Parse a document. Returns a `Hash` or an `Array`.
|
|
124
124
|
|
|
125
|
-
`source` is a `String`, or an object with `to_str`. A UTF-8, binary, or US-ASCII string is read as UTF-8
|
|
125
|
+
`source` is a `String`, or an object with `to_str`. A UTF-8, binary, or US-ASCII string is read as UTF-8. A string in another encoding, such as UTF-16, is converted to UTF-8 first, as `JSON.parse` does.
|
|
126
126
|
|
|
127
|
-
Raises `SOML::ParseError` when the document is not valid,
|
|
127
|
+
Raises `SOML::ParseError` when the document is not valid (including invalid UTF-8), `TypeError` when `source` is not a string, and `EncodingError` when it does not convert to UTF-8.
|
|
128
128
|
|
|
129
129
|
#### symbolize_names
|
|
130
130
|
|
|
@@ -157,9 +157,9 @@ Ractor.shareable?(config)
|
|
|
157
157
|
|
|
158
158
|
Read and parse a file. `path` is a `String`, or an object with `to_path`, such as a `Pathname`. The options are those of `SOML.parse`.
|
|
159
159
|
|
|
160
|
-
The file is read in binary mode, so that a text-mode conversion cannot hide a carriage return
|
|
160
|
+
The file is read in binary mode, so that a text-mode conversion cannot hide a carriage return.
|
|
161
161
|
|
|
162
|
-
Raises `SOML::ParseError` when the document is not valid, and a `SystemCallError`, such as `Errno::ENOENT`, when the file cannot be read. The error names the file, so that an editor or a terminal can open it
|
|
162
|
+
Raises `SOML::ParseError` when the document is not valid, and a `SystemCallError`, such as `Errno::ENOENT`, when the file cannot be read. The error names the file, so that an editor or a terminal can open it:
|
|
163
163
|
|
|
164
164
|
```ruby
|
|
165
165
|
SOML.load_file('config.soml')
|
|
@@ -168,9 +168,7 @@ SOML.load_file('config.soml')
|
|
|
168
168
|
|
|
169
169
|
### SOML.generate(value, canonical: false)
|
|
170
170
|
|
|
171
|
-
Write a `Hash` or an `Array` as SOML. Members keep the order of their hash, nesting
|
|
172
|
-
|
|
173
|
-
Everything but the order of members follows canonical form, so the text is canonical form when the keys are already sorted. Use `canonical: true` when the text is hashed, signed, or compared.
|
|
171
|
+
Write a `Hash` or an `Array` as SOML. Members keep the order of their hash, nesting uses braces and tabs, and the text ends with one line feed. Everything but the member order follows canonical form. Use `canonical: true` when the text is hashed, signed, or compared.
|
|
174
172
|
|
|
175
173
|
```ruby
|
|
176
174
|
SOML.generate({name: 'api', ports: [80, 443], description: 'The edge service'})
|
|
@@ -179,14 +177,14 @@ SOML.generate({name: 'api', ports: [80, 443], description: 'The edge service'})
|
|
|
179
177
|
|
|
180
178
|
Raises `SOML::GenerateError` for a value that cannot be written:
|
|
181
179
|
|
|
182
|
-
- a top-level value that is not a `Hash` or an `Array
|
|
183
|
-
- `Float::NAN
|
|
180
|
+
- a top-level value that is not a `Hash` or an `Array`
|
|
181
|
+
- `Float::NAN` (use `nil` for a missing value)
|
|
184
182
|
- an `Integer` outside the 64-bit range
|
|
185
|
-
- a `Time` outside the years 0001 to 9999 in UTC, or
|
|
186
|
-
- a string or key with a carriage return, or
|
|
187
|
-
- a key that is not a `String` or a `Symbol`,
|
|
183
|
+
- a `Time` outside the years 0001 to 9999 in UTC, or finer than a nanosecond
|
|
184
|
+
- a string or key with a carriage return, or that is not valid UTF-8 and does not convert to it
|
|
185
|
+
- a key that is not a `String` or a `Symbol`, and two keys that are the same string, such as `:a` and `'a'`
|
|
188
186
|
- a circular reference, and nesting deeper than 100 levels
|
|
189
|
-
- an object of any other class, such as a `Date` (write it as a string), a `Rational`, or an `ActiveSupport::Duration` (use a `SOML::Duration`). A `Struct`, `Data`, `Set`, or `Pathname` is not converted, because it would read back as another class
|
|
187
|
+
- an object of any other class, such as a `Date` (write it as a string), a `Rational`, or an `ActiveSupport::Duration` (use a `SOML::Duration`). A `Struct`, `Data`, `Set`, or `Pathname` is not converted, because it would read back as another class. The message names the conversion to use, such as `to_h`.
|
|
190
188
|
|
|
191
189
|
The message says where the value is:
|
|
192
190
|
|
|
@@ -200,7 +198,7 @@ SOML.generate({servers: [{ratio: Float::NAN}]})
|
|
|
200
198
|
Type: `Boolean`\
|
|
201
199
|
Default: `false`
|
|
202
200
|
|
|
203
|
-
Whether to write canonical form, with the members of each object sorted by key.
|
|
201
|
+
Whether to write canonical form, with the members of each object sorted by key. Two equal values give the same canonical text.
|
|
204
202
|
|
|
205
203
|
```ruby
|
|
206
204
|
SOML.generate({name: 'api', ports: [80, 443], description: 'The edge service'}, canonical: true)
|
|
@@ -211,7 +209,7 @@ SOML.generate({name: 'api', ports: [80, 443], description: 'The edge service'},
|
|
|
211
209
|
|
|
212
210
|
Format a document. Returns the document with its layout normalized, ending with one line feed.
|
|
213
211
|
|
|
214
|
-
The layout follows the [formatter](https://github.com/soml-lang/soml/blob/main/spec.md#formatting) in the specification: one tab per level, every member and item on its own line with no commas, and no trailing whitespace or runs of blank lines. A container
|
|
212
|
+
The layout follows the [formatter](https://github.com/soml-lang/soml/blob/main/spec.md#formatting) in the specification: one tab per level, every member and item on its own line with no commas, and no trailing whitespace or runs of blank lines. A container on one line stays on one line, as in `ports: [80, 443]`. Put a line break anywhere inside it to split it. Comments, member order, block strings, and the spelling of every value stay as they are.
|
|
215
213
|
|
|
216
214
|
`source` is read as `SOML.parse` reads it, with the same errors.
|
|
217
215
|
|
|
@@ -223,19 +221,19 @@ SOML.format("pool: {\nmin: 2, max: 16}")
|
|
|
223
221
|
#=> "pool: {\n\tmin: 2\n\tmax: 16\n}\n"
|
|
224
222
|
```
|
|
225
223
|
|
|
226
|
-
In detail
|
|
224
|
+
In detail:
|
|
227
225
|
|
|
228
|
-
- A block comment
|
|
229
|
-
- One space follows each
|
|
230
|
-
- A value on the line after its `key:` moves up to that line, unless a comment comes between them. Then it stays on its own line, one level deeper than the key
|
|
231
|
-
- A block string begins on the line after its key,
|
|
226
|
+
- A block comment before a value on the same line, as in `1, /* note */ 2`, stays in front of that value.
|
|
227
|
+
- One space follows each `:`.
|
|
228
|
+
- A value on the line after its `key:` moves up to that line, unless a comment comes between them. Then it stays on its own line, one level deeper than the key.
|
|
229
|
+
- A block string begins on the line after its key, indented to that line. Its lines are not otherwise changed.
|
|
232
230
|
- There are no blank lines at the start of the file or directly inside brackets.
|
|
233
231
|
|
|
234
|
-
### SOML.edit(source, path, value)
|
|
232
|
+
### SOML.edit(source, path, value) and SOML.edit(source, path, delete: true)
|
|
235
233
|
|
|
236
|
-
Change one value in a document, and keep everything else as it is written: comments, member order, layout, and
|
|
234
|
+
Change one value in a document, and keep everything else as it is written: comments, member order, layout, and spelling. For tools that update a config file, such as a dependency bumper or a `set` command.
|
|
237
235
|
|
|
238
|
-
The value at `path` is replaced, or added when it does not exist
|
|
236
|
+
The value at `path` is replaced, or added when it does not exist. `delete: true` removes it instead. Only the changed part of the text is rewritten, so a formatted document stays formatted.
|
|
239
237
|
|
|
240
238
|
```ruby
|
|
241
239
|
SOML.edit("name: 'api' # The service\nport: 8080\n", ['port'], 9090)
|
|
@@ -254,28 +252,28 @@ Read and write the file in binary mode, as for `SOML.generate`:
|
|
|
254
252
|
File.binwrite('config.soml', SOML.edit(File.binread('config.soml'), ['postgres', 'port'], 5433))
|
|
255
253
|
```
|
|
256
254
|
|
|
257
|
-
- A new value is written as `SOML.generate` writes it
|
|
255
|
+
- A new value is written as `SOML.generate` writes it (keeping member order), at the indentation of its line. So `0xFF` replaced by `255` is written `255`. In a one-line container, it is written on one line too, so `[1, 2]` with a new item `{a: 3}` becomes `[1, 2, {a: 3}]`.
|
|
258
256
|
- A new member goes after the last member of its object. Missing objects on the way are created.
|
|
259
|
-
- A new item can be added at the end of an array,
|
|
260
|
-
- A new member or item goes on its own line without a comma, after the comments that the one before it owns (see below)
|
|
261
|
-
- A removed member or item takes the comments it owns, which
|
|
262
|
-
- A removed member or item takes its comma with it. When
|
|
257
|
+
- A new item can be added at the end of an array, at the index that is its length.
|
|
258
|
+
- A new member or item goes on its own line without a comma, after the comments that the one before it owns (see below). The commas of the others stay as they are. When something follows the one before it on the same line, such as the `}` of `{a: 1}`, it goes on that line after a comma, as in `{a: 1, b: 2}`. In an empty `[]` or `{}`, it goes on its own line, unless that container is inside a one-line container, so `a: [1, []]` becomes `a: [1, [2]]`.
|
|
259
|
+
- A removed member or item takes the comments it owns, which are the ones `SOML.format` keeps with it: the comments after it on its line (also after its comma), and the block comments before it on its line. So removing `2` from `[1, /* note */ 2]` gives `[1]`. A comment on a line of its own stays. Its lines are removed when nothing else is on them, including a line that a block comment after it continues onto. Removing every item of a container closes it up to `[]` or `{}`, unless a comment is left inside.
|
|
260
|
+
- A removed member or item takes its comma with it. When it is the last one without a comma after it, it takes the comma directly before it instead, so `[1, 2]` becomes `[1]`.
|
|
263
261
|
- Removing the only member of a document without braces leaves `{}`.
|
|
264
262
|
|
|
265
|
-
Comments inside a
|
|
263
|
+
Comments inside a replaced or removed value are removed with it. In a layout that `SOML.format` never writes, such as an item after a closing block string delimiter on its line, a new item can be indented differently from its neighbors, but the document is always valid.
|
|
266
264
|
|
|
267
|
-
Removing a value that does not exist (a missing member, a missing object or array on the way, or an index at or past the end of its array) changes nothing, so
|
|
265
|
+
Removing a value that does not exist (a missing member, a missing object or array on the way, or an index at or past the end of its array) changes nothing, so it is safe to remove the same path twice. Use `SOML.edit(source, path, delete: true).b == source.b` to tell whether something was removed. The `.b` is needed because a source read in binary mode is not `==` to the UTF-8 result when it has non-ASCII characters.
|
|
268
266
|
|
|
269
267
|
The result is parsed before it is returned, so a bug in `SOML.edit` raises a `RuntimeError` rather than returning a broken document.
|
|
270
268
|
|
|
271
269
|
Raises:
|
|
272
270
|
|
|
273
271
|
- `SOML::ParseError` when `source` is not a valid document, the same as `SOML.parse`
|
|
274
|
-
- `TypeError` when `source` is not a string, when `path` is not a non-empty array of keys and
|
|
275
|
-
- `IndexError` when a value is set at an index past the end of its array, or at an index under a value
|
|
276
|
-
- `SOML::GenerateError` for a value or a key that `SOML.generate` cannot write, and when the change would nest
|
|
277
|
-
- `ArgumentError` when both a value and `delete: true` are given, or neither
|
|
278
|
-
- `EncodingError`
|
|
272
|
+
- `TypeError` when `source` is not a string, when `path` is not a member node, a value node, or a non-empty array of keys and indexes, when it leads through a scalar, or when it has a key where an array is or an index where an object is
|
|
273
|
+
- `IndexError` when a value is set at an index past the end of its array, or at an index under a missing value
|
|
274
|
+
- `SOML::GenerateError` for a value or a key that `SOML.generate` cannot write, and when the change would nest more than 100 levels deep. Its path is the place in the document.
|
|
275
|
+
- `ArgumentError` when both a value and `delete: true` are given, or neither, or when `path` is the root node or a node that is not in `source`
|
|
276
|
+
- `EncodingError` as for `SOML.parse`
|
|
279
277
|
|
|
280
278
|
```ruby
|
|
281
279
|
SOML.edit('ports: [80]', ['ports', 5], 443)
|
|
@@ -284,9 +282,9 @@ SOML.edit('ports: [80]', ['ports', 5], 443)
|
|
|
284
282
|
|
|
285
283
|
#### path
|
|
286
284
|
|
|
287
|
-
Type: `Array<String | Symbol | Integer
|
|
285
|
+
Type: `Array<String | Symbol | Integer>`, or a node from `SOML.parse_tree`
|
|
288
286
|
|
|
289
|
-
The keys and array indexes that lead to the value, such as `['servers', 0, 'port']
|
|
287
|
+
The keys and array indexes that lead to the value, such as `['servers', 0, 'port']`, or a member or value node from `SOML.parse_tree(source)` (see [Autofixes](#autofixes)).
|
|
290
288
|
|
|
291
289
|
#### value
|
|
292
290
|
|
|
@@ -513,18 +511,18 @@ SOML.generate({timeout: SOML::Duration.from_seconds(5.minutes.to_r)})
|
|
|
513
511
|
|
|
514
512
|
## Benchmark
|
|
515
513
|
|
|
516
|
-
|
|
514
|
+
CPU time to read and write the shared [benchmark fixtures](https://github.com/soml-lang/soml/blob/main/benchmark/readme.md), which hold the same data in SOML, JSON, and TOML, on an Apple M-series machine with Ruby 4.0 and YJIT. The numbers are rough. Lower is better.
|
|
517
515
|
|
|
518
|
-
| | this gem | `
|
|
516
|
+
| | this gem | `toml-rb` | `json` | `oj` |
|
|
519
517
|
|---|---|---|---|---|
|
|
520
|
-
| Read `service` (3 KB) | 78 µs |
|
|
521
|
-
| Read `platform` (35 KB) | 0.61 ms |
|
|
522
|
-
| Read `earthquakes` (733 KB) | 14 ms |
|
|
523
|
-
| Write `service` | 51 µs |
|
|
524
|
-
| Write `platform` | 0.34 ms | 0.
|
|
525
|
-
| Write `earthquakes` | 15 ms | 2.1 ms | 3.2 ms |
|
|
518
|
+
| Read `service` (3 KB) | 78 µs | 174 µs | 10 µs | 11 µs |
|
|
519
|
+
| Read `platform` (35 KB) | 0.61 ms | 1.2 ms | 0.10 ms | 0.07 ms |
|
|
520
|
+
| Read `earthquakes` (733 KB) | 14 ms | 51 ms | 2.2 ms | 2.8 ms |
|
|
521
|
+
| Write `service` | 51 µs | 105 µs | 8 µs | 8 µs |
|
|
522
|
+
| Write `platform` | 0.34 ms | 0.65 ms | 0.06 ms | 0.03 ms |
|
|
523
|
+
| Write `earthquakes` | 15 ms | 26 ms | 2.1 ms | 3.2 ms |
|
|
526
524
|
|
|
527
|
-
`
|
|
525
|
+
`toml-rb` is pure Ruby, like this gem, and `json` and `oj` are C extensions. The script also compares `tomlib`, `perfect_toml`, and `tomlrb`, and times `SOML.format`, `SOML.edit`, and `SOML.parse_tree` on each fixture:
|
|
528
526
|
|
|
529
527
|
```sh
|
|
530
528
|
bundle config set --local with benchmark
|
|
@@ -532,11 +530,67 @@ bundle install
|
|
|
532
530
|
bundle exec ruby --yjit bench/benchmark.rb
|
|
533
531
|
```
|
|
534
532
|
|
|
533
|
+
## Syntax tree
|
|
534
|
+
|
|
535
|
+
`SOML.parse_tree(source)` returns a deeply frozen `SOML::Tree` with `source` (a UTF-8 copy), `body` (an object or array), `tokens`, and `comments`. It accepts the same inputs and raises the same errors as `SOML.parse`, without freezing the caller's string. Only valid documents are supported; there is no recovery from incomplete editor input.
|
|
536
|
+
|
|
537
|
+
```ruby
|
|
538
|
+
tree = SOML.parse_tree('port: 0x1F90 # Default')
|
|
539
|
+
node = tree.body.members.first.value
|
|
540
|
+
|
|
541
|
+
node.kind #=> :integer
|
|
542
|
+
node.value #=> 8080
|
|
543
|
+
node.radix #=> 16
|
|
544
|
+
tree.source.byteslice(node.start...node.finish) #=> "0x1F90"
|
|
545
|
+
tree.position(node.start) #=> [1, 7]
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
Every node, token, and comment has `start` and exclusive `finish` UTF-8 byte offsets into `tree.source`. Use `byteslice` to read spans. Collections include their brackets; a bare root object spans its first key through its last value. `position(byte_offset)` returns one-based `[line, column]`, counting Unicode characters, including tabs as one character. It accepts character boundaries through EOF, raising `TypeError` for non-integers and `ArgumentError` for invalid offsets. Unlike tree spans, `ParseError#offset` counts characters.
|
|
549
|
+
|
|
550
|
+
`each_node` yields the body and its descendants in source order, parents before children and member keys before values; without a block it returns an Enumerator. Tokens and comments are separate, each in source order. Node constructors are internal.
|
|
551
|
+
|
|
552
|
+
| Class under `SOML::Tree` | `type` | Fields besides `start` and `finish` |
|
|
553
|
+
|---|---|---|
|
|
554
|
+
| `ObjectNode` | `:object` | `members`, `braced` |
|
|
555
|
+
| `ArrayNode` | `:array` | `elements` |
|
|
556
|
+
| `MemberNode` | `:member` | `key`, `value` (a node) |
|
|
557
|
+
| `KeyNode` | `:key` | `value` (decoded string), `style` (`:bare`, `:literal`, or `:escaped`) |
|
|
558
|
+
| `ScalarNode` | `:scalar` | `kind`, `value` (decoded), `block`, `style`, `radix` |
|
|
559
|
+
| `Comment` | `:line` or `:block` | `value` (text without delimiters) |
|
|
560
|
+
| `Token` | See below | `value` (original source text) |
|
|
561
|
+
|
|
562
|
+
Scalar kinds are `:string`, `:integer`, `:float`, `:boolean`, `:null`, `:instant`, and `:duration`, with the same Ruby values as `SOML.parse`. Strings have `style: :literal` or `:escaped` and a `block` boolean; integers have `radix: 2`, `8`, `10`, or `16`. Otherwise, `style` and `radix` are `nil`, and `block` is `false`.
|
|
563
|
+
|
|
564
|
+
### Syntax highlighting
|
|
565
|
+
|
|
566
|
+
Token types are `:punctuator` (`{}[]:,`), `:bare_key`, `:string`, `:integer`, `:float`, `:keyword` (`true`, `false`, `null`), `:instant`, and `:duration`. Quoted keys and whole block strings are string tokens; escapes stay within strings. Infinities are floats. Use key nodes to distinguish quoted keys from string values.
|
|
567
|
+
|
|
568
|
+
```ruby
|
|
569
|
+
(tree.tokens + tree.comments).sort_by(&:start).each do |token|
|
|
570
|
+
text = tree.source.byteslice(token.start...token.finish)
|
|
571
|
+
# Highlight text using token.type, preserving whitespace between spans.
|
|
572
|
+
end
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
Tokens never overlap and exclude whitespace and comments. When rendering HTML, escape all source text, including comments and gaps.
|
|
576
|
+
|
|
577
|
+
### Autofixes
|
|
578
|
+
|
|
579
|
+
`SOML.edit` accepts a member or non-root value node in place of a path, using its existing comment and layout rules. Members select their value for replacement and the whole member for deletion. Keys, tokens, comments, and the root collection are not accepted.
|
|
580
|
+
|
|
581
|
+
```ruby
|
|
582
|
+
source = "port: 0x1F90 # Default\n"
|
|
583
|
+
node = SOML.parse_tree(source).body.members.first.value
|
|
584
|
+
SOML.edit(source, node, 9090)
|
|
585
|
+
#=> "port: 9090 # Default\n"
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
Nodes must come from the same source text. Reparse after editing before selecting another node. For spelling fixes, apply non-overlapping span replacements from the end backwards to `tree.source.b`, restore UTF-8 with `force_encoding(Encoding::UTF_8)`, then validate with `SOML.parse`.
|
|
589
|
+
|
|
535
590
|
## Limits
|
|
536
591
|
|
|
537
592
|
- **Nesting is limited to 100 levels**, as the specification requires, in reading and in writing. Every array and object is one level, including the document's own collection.
|
|
538
|
-
- **The gem name.** The
|
|
539
|
-
- **The syntax tree is not public yet.** `SOML.format` and `SOML.edit` use one, and the [JavaScript](https://github.com/soml-lang/soml-javascript), [Python](https://github.com/soml-lang/soml-python), and [Rust](https://github.com/soml-lang/soml-rust) implementations have a public one, for tools such as linters.
|
|
593
|
+
- **The gem name.** The gem is [`soml-lang`](https://rubygems.org/gems/soml-lang), because `soml` on RubyGems is an unrelated format. It also defines `module SOML`, so the two gems cannot be loaded together.
|
|
540
594
|
|
|
541
595
|
## Development
|
|
542
596
|
|
data/sig/soml.rbs
CHANGED
|
@@ -17,15 +17,21 @@ module SOML
|
|
|
17
17
|
# Write a hash or an array as SOML, keeping the member order of each hash, or as canonical form with `canonical: true`.
|
|
18
18
|
def self.generate: (Hash[untyped, untyped] | Array[untyped] value, ?canonical: boolish) -> String
|
|
19
19
|
|
|
20
|
+
# Parse a document into a read-only syntax tree for linters and other tools.
|
|
21
|
+
def self.parse_tree: (string source) -> Tree
|
|
22
|
+
|
|
20
23
|
# Format a document, keeping its comments, member order, and spelling.
|
|
21
24
|
def self.format: (string source) -> String
|
|
22
25
|
|
|
23
26
|
# A key in an object or an index in an array.
|
|
24
27
|
type path_segment = String | Symbol | Integer
|
|
25
28
|
|
|
29
|
+
# What `edit` changes: the keys and indexes that lead to a value, or a member or a non-root value node from `parse_tree` of the same source.
|
|
30
|
+
type edit_target = Array[path_segment] | Tree::MemberNode | Tree::ObjectNode | Tree::ArrayNode | Tree::ScalarNode
|
|
31
|
+
|
|
26
32
|
# Change one value in a document, and keep everything else as it is written.
|
|
27
|
-
def self.edit: (string source,
|
|
28
|
-
| (string source,
|
|
33
|
+
def self.edit: (string source, edit_target path, untyped value, ?delete: false) -> String
|
|
34
|
+
| (string source, edit_target path, delete: true) -> String
|
|
29
35
|
|
|
30
36
|
# Whether a key can be written without quotes, as a bare key. A key is a `String` or a `Symbol`, so anything else gives false.
|
|
31
37
|
def self.bare_key?: (untyped key) -> bool
|
|
@@ -140,4 +146,94 @@ module SOML
|
|
|
140
146
|
|
|
141
147
|
def deconstruct_keys: (Array[Symbol]? keys) -> { nanoseconds: Integer }
|
|
142
148
|
end
|
|
149
|
+
|
|
150
|
+
# A read-only syntax tree, from `parse_tree`. Every offset is a UTF-8 byte offset into `source`, and `finish` is exclusive.
|
|
151
|
+
class Tree
|
|
152
|
+
# A node of the tree.
|
|
153
|
+
type node = ObjectNode | ArrayNode | MemberNode | KeyNode | ScalarNode
|
|
154
|
+
|
|
155
|
+
# A `# ...` comment or a `/* ... */` comment, with its text without the delimiters.
|
|
156
|
+
class Comment < Struct[untyped]
|
|
157
|
+
attr_reader type: :line | :block
|
|
158
|
+
attr_reader start: Integer
|
|
159
|
+
attr_reader finish: Integer
|
|
160
|
+
attr_reader value: String
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# A lexical token, with its source text.
|
|
164
|
+
class Token < Struct[untyped]
|
|
165
|
+
attr_reader type: :punctuator | :bare_key | :string | :integer | :float | :keyword | :instant | :duration
|
|
166
|
+
attr_reader start: Integer
|
|
167
|
+
attr_reader finish: Integer
|
|
168
|
+
attr_reader value: String
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# An object, written with braces or, only at the top level, without them.
|
|
172
|
+
class ObjectNode < Struct[untyped]
|
|
173
|
+
attr_reader members: Array[MemberNode]
|
|
174
|
+
attr_reader braced: bool
|
|
175
|
+
attr_reader start: Integer
|
|
176
|
+
attr_reader finish: Integer
|
|
177
|
+
def type: () -> :object
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
class ArrayNode < Struct[untyped]
|
|
181
|
+
attr_reader elements: Array[ObjectNode | ArrayNode | ScalarNode]
|
|
182
|
+
attr_reader start: Integer
|
|
183
|
+
attr_reader finish: Integer
|
|
184
|
+
def type: () -> :array
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# A `key: value` member.
|
|
188
|
+
class MemberNode < Struct[untyped]
|
|
189
|
+
attr_reader key: KeyNode
|
|
190
|
+
attr_reader value: ObjectNode | ArrayNode | ScalarNode
|
|
191
|
+
attr_reader start: Integer
|
|
192
|
+
attr_reader finish: Integer
|
|
193
|
+
def type: () -> :member
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# A key, with its decoded string and how it is written.
|
|
197
|
+
class KeyNode < Struct[untyped]
|
|
198
|
+
attr_reader value: String
|
|
199
|
+
attr_reader start: Integer
|
|
200
|
+
attr_reader finish: Integer
|
|
201
|
+
attr_reader style: :bare | :literal | :escaped
|
|
202
|
+
def type: () -> :key
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# A value that is not a collection, with its decoded value and how it is written.
|
|
206
|
+
class ScalarNode < Struct[untyped]
|
|
207
|
+
attr_reader block: bool
|
|
208
|
+
attr_reader start: Integer
|
|
209
|
+
attr_reader finish: Integer
|
|
210
|
+
attr_reader kind: :string | :integer | :float | :boolean | :null | :instant | :duration
|
|
211
|
+
attr_reader value: String | Integer | Float | bool | nil | Time | Duration
|
|
212
|
+
attr_reader style: (:literal | :escaped)?
|
|
213
|
+
attr_reader radix: (2 | 8 | 10 | 16)?
|
|
214
|
+
def type: () -> :scalar
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# The source, in UTF-8.
|
|
218
|
+
attr_reader source: String
|
|
219
|
+
|
|
220
|
+
# The document's collection.
|
|
221
|
+
attr_reader body: ObjectNode | ArrayNode
|
|
222
|
+
|
|
223
|
+
# Every comment, in source order.
|
|
224
|
+
attr_reader comments: Array[Comment]
|
|
225
|
+
|
|
226
|
+
# Every lexical token, in source order.
|
|
227
|
+
attr_reader tokens: Array[Token]
|
|
228
|
+
|
|
229
|
+
# `lean` is internal, for `format` and `edit`.
|
|
230
|
+
def initialize: (string source, ?lean: boolish) -> void
|
|
231
|
+
|
|
232
|
+
# Yield the body and all its descendants in source order.
|
|
233
|
+
def each_node: () { (node) -> void } -> self
|
|
234
|
+
| () -> Enumerator[node, self]
|
|
235
|
+
|
|
236
|
+
# The 1-based line and column of a byte offset in `source`.
|
|
237
|
+
def position: (Integer byte_offset) -> [Integer, Integer]
|
|
238
|
+
end
|
|
143
239
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: soml-lang
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sindre Sorhus
|
|
@@ -11,7 +11,8 @@ date: 1980-01-02 00:00:00.000000000 Z
|
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: A strict parser and a generator for SOML that keeps member order or writes
|
|
13
13
|
canonical form, with exact integers, instants, and durations, and an error message
|
|
14
|
-
that says what is wrong and where.
|
|
14
|
+
that says what is wrong and where. With a formatter, an editor that keeps comments
|
|
15
|
+
and layout, and a syntax tree for linters.
|
|
15
16
|
email:
|
|
16
17
|
- sindresorhus@gmail.com
|
|
17
18
|
executables: []
|
|
@@ -37,6 +38,7 @@ licenses:
|
|
|
37
38
|
metadata:
|
|
38
39
|
source_code_uri: https://github.com/soml-lang/soml-ruby
|
|
39
40
|
bug_tracker_uri: https://github.com/soml-lang/soml-ruby/issues
|
|
41
|
+
changelog_uri: https://github.com/soml-lang/soml-ruby/releases
|
|
40
42
|
rubygems_mfa_required: 'true'
|
|
41
43
|
rdoc_options: []
|
|
42
44
|
require_paths:
|