soml-lang 0.0.2 → 0.0.4
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.
- package/distribution/edit.d.ts +56 -0
- package/distribution/edit.js +536 -0
- package/distribution/error.d.ts +19 -2
- package/distribution/error.js +20 -3
- package/distribution/format.d.ts +48 -4
- package/distribution/format.js +209 -49
- package/distribution/index.d.ts +6 -3
- package/distribution/index.js +7 -2
- package/distribution/parse.d.ts +69 -6
- package/distribution/parse.js +603 -227
- package/distribution/shared.d.ts +58 -4
- package/distribution/shared.js +93 -11
- package/distribution/stringify.d.ts +88 -8
- package/distribution/stringify.js +173 -59
- package/distribution/tree.d.ts +111 -9
- package/distribution/tree.js +214 -95
- package/package.json +4 -3
- package/readme.md +256 -50
package/distribution/tree.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isBareKeyCharacter, findNumberEnd, findLineEnd, findBlockStringEnd, } from "./shared.js";
|
|
1
|
+
import { parseWithTimes, } from "./parse.js";
|
|
2
|
+
import { isBareKeyCharacter, isSpace, findNumberEnd, findLineEnd, findBlockStringEnd, LF, DOUBLE_QUOTE, HASH, SINGLE_QUOTE, ASTERISK, COMMA, SLASH, BACKSLASH, OPEN_BRACKET, CLOSE_BRACKET, OPEN_BRACE, CLOSE_BRACE, } from "./shared.js";
|
|
3
3
|
/**
|
|
4
|
-
The properties of each node type that hold its child nodes, in source order
|
|
4
|
+
The properties of each node type that hold its child nodes, in source order, for tools that walk the tree, such as an ESLint language plugin.
|
|
5
5
|
|
|
6
6
|
@example
|
|
7
7
|
```
|
|
@@ -20,8 +20,7 @@ export const visitorKeys = Object.freeze({
|
|
|
20
20
|
Object: Object.freeze(['members']),
|
|
21
21
|
Member: Object.freeze(['key', 'value']),
|
|
22
22
|
Array: Object.freeze(['elements']),
|
|
23
|
-
Key: Object.freeze([
|
|
24
|
-
KeySegment: Object.freeze([]),
|
|
23
|
+
Key: Object.freeze([]),
|
|
25
24
|
String: Object.freeze([]),
|
|
26
25
|
Integer: Object.freeze([]),
|
|
27
26
|
Float: Object.freeze([]),
|
|
@@ -31,31 +30,16 @@ export const visitorKeys = Object.freeze({
|
|
|
31
30
|
Duration: Object.freeze([]),
|
|
32
31
|
});
|
|
33
32
|
/* eslint-enable @typescript-eslint/naming-convention */
|
|
34
|
-
const TAB = 0x09;
|
|
35
|
-
const LF = 0x0A;
|
|
36
|
-
const SPACE = 0x20;
|
|
37
|
-
const DOUBLE_QUOTE = 0x22;
|
|
38
|
-
const HASH = 0x23;
|
|
39
|
-
const SINGLE_QUOTE = 0x27;
|
|
40
|
-
const ASTERISK = 0x2A;
|
|
41
|
-
const COMMA = 0x2C;
|
|
42
|
-
const DOT = 0x2E;
|
|
43
|
-
const SLASH = 0x2F;
|
|
44
|
-
const BACKSLASH = 0x5C;
|
|
45
|
-
const OPEN_BRACKET = 0x5B;
|
|
46
|
-
const CLOSE_BRACKET = 0x5D;
|
|
47
|
-
const OPEN_BRACE = 0x7B;
|
|
48
|
-
const CLOSE_BRACE = 0x7D;
|
|
49
|
-
const WHITESPACE = new Set([SPACE, TAB, LF]);
|
|
50
33
|
/*
|
|
51
34
|
`infinity` and `-infinity` are read as numbers, because they are floats.
|
|
52
35
|
*/
|
|
53
36
|
const KEYWORDS = ['true', 'false', 'null'];
|
|
54
|
-
|
|
37
|
+
// A map rather than an object, so that a property added to `Object.prototype` is never read as an entry.
|
|
38
|
+
const RADIX = new Map([['x', 16], ['o', 8], ['b', 2]]);
|
|
55
39
|
/**
|
|
56
|
-
Parse a document into a syntax tree, for tools such as linters and formatters.
|
|
40
|
+
Parse a document into a syntax tree, for tools such as linters and formatters. Returns a `Document` node, which also holds every token and comment.
|
|
57
41
|
|
|
58
|
-
Every node, token, and comment has a `range` and a `loc
|
|
42
|
+
Every node, token, and comment has a `range`, which is `[start, end]` as UTF-16 offsets into `text`, and a `loc`, which is `{start: {line, column}, end: {line, column}}`, with a 1-based line and a 0-based column in UTF-16 code units, as in ESTree. A `ParseError` counts its column differently, for people to read, so use its `offset` to find the position in the tree. A scalar node or a key shares its `range` and `loc` with its token, and other nodes, except `Document`, share the positions in `loc` with their first and last token, so treat them as read-only.
|
|
59
43
|
|
|
60
44
|
@param text - The document.
|
|
61
45
|
@returns The root node, which also holds every token and comment.
|
|
@@ -80,21 +64,26 @@ export function parseTree(text) {
|
|
|
80
64
|
throw new TypeError(`Expected a string, got ${text === null ? 'null' : typeof text}`);
|
|
81
65
|
}
|
|
82
66
|
// Validates the whole document, so the builder below can assume valid input.
|
|
83
|
-
|
|
67
|
+
parseWithTimes(text);
|
|
84
68
|
return new TreeBuilder(text).build();
|
|
85
69
|
}
|
|
86
70
|
/*
|
|
87
|
-
The value of one scalar, read by the parser itself so that the tree and `parse()` cannot disagree. A scalar's validity does not depend on its context, so it parses on its own inside an array.
|
|
71
|
+
The value of one scalar, read by the parser itself so that the tree and `parse()` cannot disagree. A scalar's validity does not depend on its context, so it parses on its own inside an array. An instant or a duration is a `Time`.
|
|
88
72
|
*/
|
|
89
73
|
function readScalar(raw) {
|
|
90
|
-
return
|
|
74
|
+
return parseWithTimes(`[${raw}]`)[0];
|
|
91
75
|
}
|
|
76
|
+
/*
|
|
77
|
+
Every node is an object literal with the same property order, `type`, its own fields, `range`, and `loc`, rather than one generic function that spreads the fields, which is several times slower. A node that is one token, such as a scalar or a key, shares the token's `range` and `loc`.
|
|
78
|
+
*/
|
|
92
79
|
class TreeBuilder {
|
|
93
80
|
#source;
|
|
94
81
|
#index = 0;
|
|
95
82
|
#tokens = [];
|
|
96
83
|
#comments = [];
|
|
97
84
|
#lineStarts = [0];
|
|
85
|
+
// The line of the last offset looked up. Offsets mostly arrive in order, so it is the best first guess for the next one.
|
|
86
|
+
#line = 0;
|
|
98
87
|
constructor(source) {
|
|
99
88
|
this.#source = source;
|
|
100
89
|
for (let index = source.indexOf('\n'); index !== -1; index = source.indexOf('\n', index + 1)) {
|
|
@@ -109,29 +98,40 @@ class TreeBuilder {
|
|
|
109
98
|
*/
|
|
110
99
|
#position(offset) {
|
|
111
100
|
const lineStarts = this.#lineStarts;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
101
|
+
// The binary search runs only when the offset is not on the line of the last one or the line after it, which makes building the tree about twice as fast.
|
|
102
|
+
if (this.#isOnLine(offset, this.#line + 1)) {
|
|
103
|
+
this.#line++;
|
|
104
|
+
}
|
|
105
|
+
else if (!this.#isOnLine(offset, this.#line)) {
|
|
106
|
+
let low = 0;
|
|
107
|
+
let high = lineStarts.length - 1;
|
|
108
|
+
while (low < high) {
|
|
109
|
+
const middle = Math.ceil((low + high) / 2);
|
|
110
|
+
if (lineStarts[middle] <= offset) {
|
|
111
|
+
low = middle;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
high = middle - 1;
|
|
115
|
+
}
|
|
121
116
|
}
|
|
117
|
+
this.#line = low;
|
|
122
118
|
}
|
|
123
|
-
return { line:
|
|
119
|
+
return { line: this.#line + 1, column: offset - lineStarts[this.#line] };
|
|
124
120
|
}
|
|
125
|
-
#
|
|
126
|
-
|
|
121
|
+
#isOnLine(offset, line) {
|
|
122
|
+
const lineStarts = this.#lineStarts;
|
|
123
|
+
return line < lineStarts.length && lineStarts[line] <= offset && (line + 1 === lineStarts.length || lineStarts[line + 1] > offset);
|
|
124
|
+
}
|
|
125
|
+
#location(start, end) {
|
|
126
|
+
return { start: this.#position(start), end: this.#position(end) };
|
|
127
|
+
}
|
|
128
|
+
#token(type, start, end) {
|
|
129
|
+
const token = {
|
|
127
130
|
type,
|
|
128
|
-
|
|
131
|
+
value: this.#source.slice(start, end),
|
|
129
132
|
range: [start, end],
|
|
130
|
-
loc:
|
|
133
|
+
loc: this.#location(start, end),
|
|
131
134
|
};
|
|
132
|
-
}
|
|
133
|
-
#token(type, start, end) {
|
|
134
|
-
const token = this.#node(type, start, end, { value: this.#source.slice(start, end) });
|
|
135
135
|
this.#tokens.push(token);
|
|
136
136
|
return token;
|
|
137
137
|
}
|
|
@@ -144,48 +144,73 @@ class TreeBuilder {
|
|
|
144
144
|
for (;;) {
|
|
145
145
|
const code = this.#code();
|
|
146
146
|
const start = this.#index;
|
|
147
|
-
if (
|
|
147
|
+
if (code === LF || isSpace(code)) {
|
|
148
148
|
this.#index++;
|
|
149
149
|
}
|
|
150
150
|
else if (code === HASH) {
|
|
151
151
|
this.#index = findLineEnd(source, start);
|
|
152
|
-
this.#
|
|
152
|
+
this.#comment('Line', start, this.#index, source.slice(start + 1, this.#index));
|
|
153
153
|
}
|
|
154
154
|
else if (code === SLASH && this.#code(start + 1) === ASTERISK) {
|
|
155
155
|
this.#index = source.indexOf('*/', start + 2) + 2;
|
|
156
|
-
this.#
|
|
156
|
+
this.#comment('Block', start, this.#index, source.slice(start + 2, this.#index - 2));
|
|
157
157
|
}
|
|
158
158
|
else {
|
|
159
159
|
return;
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
|
+
#comment(type, start, end, value) {
|
|
164
|
+
this.#comments.push({
|
|
165
|
+
type,
|
|
166
|
+
value,
|
|
167
|
+
range: [start, end],
|
|
168
|
+
loc: this.#location(start, end),
|
|
169
|
+
});
|
|
170
|
+
}
|
|
163
171
|
#bareObject() {
|
|
164
172
|
const members = [];
|
|
165
173
|
do {
|
|
166
174
|
members.push(this.#member());
|
|
167
175
|
this.#skipTrivia();
|
|
168
176
|
} while (this.#index < this.#source.length);
|
|
169
|
-
|
|
177
|
+
const first = members[0];
|
|
178
|
+
const last = members.at(-1);
|
|
179
|
+
return {
|
|
180
|
+
type: 'Object',
|
|
181
|
+
members,
|
|
182
|
+
braced: false,
|
|
183
|
+
range: [first.range[0], last.range[1]],
|
|
184
|
+
loc: spanLocation(first, last),
|
|
185
|
+
};
|
|
170
186
|
}
|
|
171
187
|
#object() {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
188
|
+
const { items: members, opening, closing } = this.#items(CLOSE_BRACE, () => this.#member());
|
|
189
|
+
return {
|
|
190
|
+
type: 'Object',
|
|
191
|
+
members,
|
|
192
|
+
braced: true,
|
|
193
|
+
range: [opening.range[0], closing.range[1]],
|
|
194
|
+
loc: spanLocation(opening, closing),
|
|
195
|
+
};
|
|
175
196
|
}
|
|
176
197
|
#array() {
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
198
|
+
const { items: elements, opening, closing } = this.#items(CLOSE_BRACKET, () => this.#value());
|
|
199
|
+
return {
|
|
200
|
+
type: 'Array',
|
|
201
|
+
elements,
|
|
202
|
+
range: [opening.range[0], closing.range[1]],
|
|
203
|
+
loc: spanLocation(opening, closing),
|
|
204
|
+
};
|
|
180
205
|
}
|
|
181
206
|
/*
|
|
182
|
-
The items between an opening bracket and its `
|
|
207
|
+
The items between an opening bracket and its `closingCode` bracket, which is consumed too. Items are separated by a comma, a line break, or both, so a comma token may be absent. The parser has already checked the separators, including that a comma is on the line of the item before it.
|
|
183
208
|
*/
|
|
184
|
-
#items(
|
|
209
|
+
#items(closingCode, parseItem) {
|
|
185
210
|
const items = [];
|
|
186
|
-
this.#punctuator();
|
|
211
|
+
const opening = this.#punctuator();
|
|
187
212
|
this.#skipTrivia();
|
|
188
|
-
while (this.#code() !==
|
|
213
|
+
while (this.#code() !== closingCode) {
|
|
189
214
|
items.push(parseItem());
|
|
190
215
|
this.#skipTrivia();
|
|
191
216
|
if (this.#code() !== COMMA) {
|
|
@@ -194,36 +219,45 @@ class TreeBuilder {
|
|
|
194
219
|
this.#punctuator();
|
|
195
220
|
this.#skipTrivia();
|
|
196
221
|
}
|
|
197
|
-
this.#punctuator();
|
|
198
|
-
return items;
|
|
222
|
+
return { items, opening, closing: this.#punctuator() };
|
|
199
223
|
}
|
|
200
224
|
#member() {
|
|
201
225
|
const key = this.#key();
|
|
202
226
|
this.#punctuator(); // `:`
|
|
203
227
|
this.#skipTrivia();
|
|
204
228
|
const value = this.#value();
|
|
205
|
-
return
|
|
229
|
+
return {
|
|
230
|
+
type: 'Member',
|
|
231
|
+
key,
|
|
232
|
+
value,
|
|
233
|
+
range: [key.range[0], value.range[1]],
|
|
234
|
+
loc: spanLocation(key, value),
|
|
235
|
+
};
|
|
206
236
|
}
|
|
207
237
|
#key() {
|
|
208
|
-
const segments = [this.#keySegment()];
|
|
209
|
-
while (this.#code() === DOT) {
|
|
210
|
-
this.#punctuator();
|
|
211
|
-
segments.push(this.#keySegment());
|
|
212
|
-
}
|
|
213
|
-
return this.#node('Key', segments[0].range[0], segments.at(-1).range[1], { segments });
|
|
214
|
-
}
|
|
215
|
-
#keySegment() {
|
|
216
238
|
const start = this.#index;
|
|
217
239
|
const code = this.#code();
|
|
218
240
|
if (code === SINGLE_QUOTE || code === DOUBLE_QUOTE) {
|
|
219
|
-
const { value, style } = this.#singleLineString();
|
|
220
|
-
return
|
|
241
|
+
const { value, style, token: { range, loc } } = this.#singleLineString();
|
|
242
|
+
return {
|
|
243
|
+
type: 'Key',
|
|
244
|
+
value,
|
|
245
|
+
style,
|
|
246
|
+
range,
|
|
247
|
+
loc,
|
|
248
|
+
};
|
|
221
249
|
}
|
|
222
250
|
while (isBareKeyCharacter(this.#code())) {
|
|
223
251
|
this.#index++;
|
|
224
252
|
}
|
|
225
|
-
const
|
|
226
|
-
return
|
|
253
|
+
const { value, range, loc } = this.#token('BareKey', start, this.#index);
|
|
254
|
+
return {
|
|
255
|
+
type: 'Key',
|
|
256
|
+
value,
|
|
257
|
+
style: 'bare',
|
|
258
|
+
range,
|
|
259
|
+
loc,
|
|
260
|
+
};
|
|
227
261
|
}
|
|
228
262
|
#value() {
|
|
229
263
|
const code = this.#code();
|
|
@@ -238,16 +272,30 @@ class TreeBuilder {
|
|
|
238
272
|
if (this.#code(start + 1) === code && this.#code(start + 2) === code) {
|
|
239
273
|
return this.#blockString(code);
|
|
240
274
|
}
|
|
241
|
-
const { value, style } = this.#singleLineString();
|
|
242
|
-
return
|
|
275
|
+
const { value, style, token: { range, loc } } = this.#singleLineString();
|
|
276
|
+
return {
|
|
277
|
+
type: 'String',
|
|
278
|
+
value,
|
|
279
|
+
style,
|
|
280
|
+
block: false,
|
|
281
|
+
range,
|
|
282
|
+
loc,
|
|
283
|
+
};
|
|
243
284
|
}
|
|
244
|
-
|
|
285
|
+
// The document is valid, so a value that starts with a keyword is that keyword, as a number cannot start with a letter other than the `i` of `infinity`.
|
|
286
|
+
const keyword = KEYWORDS.find(word => this.#source.startsWith(word, start));
|
|
245
287
|
if (keyword !== undefined) {
|
|
246
288
|
this.#index += keyword.length;
|
|
247
|
-
this.#token('Keyword', start, this.#index);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
289
|
+
const { range, loc } = this.#token('Keyword', start, this.#index);
|
|
290
|
+
if (keyword === 'null') {
|
|
291
|
+
return { type: 'Null', range, loc };
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
type: 'Boolean',
|
|
295
|
+
value: keyword === 'true',
|
|
296
|
+
range,
|
|
297
|
+
loc,
|
|
298
|
+
};
|
|
251
299
|
}
|
|
252
300
|
return this.#numberOrTime();
|
|
253
301
|
}
|
|
@@ -270,8 +318,8 @@ class TreeBuilder {
|
|
|
270
318
|
this.#index = end + 1;
|
|
271
319
|
const token = this.#token('String', start, this.#index);
|
|
272
320
|
return quote === SINGLE_QUOTE
|
|
273
|
-
? { value: source.slice(start + 1, end), style: 'literal' }
|
|
274
|
-
: { value: readScalar(token.value), style: 'escaped' };
|
|
321
|
+
? { value: source.slice(start + 1, end), style: 'literal', token }
|
|
322
|
+
: { value: readScalar(token.value), style: 'escaped', token };
|
|
275
323
|
}
|
|
276
324
|
/*
|
|
277
325
|
The block ends at the first line whose first non-whitespace content is a run of exactly as many quotes as the opening delimiter.
|
|
@@ -285,12 +333,15 @@ class TreeBuilder {
|
|
|
285
333
|
}
|
|
286
334
|
const closing = findBlockStringEnd(source, source.indexOf('\n', start) + 1, quote, delimiterLength);
|
|
287
335
|
this.#index = closing.delimiterStart + delimiterLength;
|
|
288
|
-
const
|
|
289
|
-
return
|
|
290
|
-
|
|
336
|
+
const { value, range, loc } = this.#token('String', start, this.#index);
|
|
337
|
+
return {
|
|
338
|
+
type: 'String',
|
|
339
|
+
value: readScalar(value),
|
|
291
340
|
style: quote === SINGLE_QUOTE ? 'literal' : 'escaped',
|
|
292
341
|
block: true,
|
|
293
|
-
|
|
342
|
+
range,
|
|
343
|
+
loc,
|
|
344
|
+
};
|
|
294
345
|
}
|
|
295
346
|
#numberOrTime() {
|
|
296
347
|
const start = this.#index;
|
|
@@ -299,16 +350,53 @@ class TreeBuilder {
|
|
|
299
350
|
const raw = this.#source.slice(start, end);
|
|
300
351
|
const value = readScalar(raw);
|
|
301
352
|
if (typeof value === 'bigint') {
|
|
302
|
-
this.#token('Integer', start, end);
|
|
303
|
-
return
|
|
353
|
+
const { range, loc } = this.#token('Integer', start, end);
|
|
354
|
+
return {
|
|
355
|
+
type: 'Integer',
|
|
356
|
+
value,
|
|
357
|
+
radix: RADIX.get(raw.charAt(1)) ?? 10,
|
|
358
|
+
range,
|
|
359
|
+
loc,
|
|
360
|
+
};
|
|
304
361
|
}
|
|
305
362
|
if (typeof value === 'number') {
|
|
306
|
-
this.#token('Float', start, end);
|
|
307
|
-
return
|
|
363
|
+
const { range, loc } = this.#token('Float', start, end);
|
|
364
|
+
return {
|
|
365
|
+
type: 'Float',
|
|
366
|
+
value,
|
|
367
|
+
range,
|
|
368
|
+
loc,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
const time = value;
|
|
372
|
+
const { range, loc } = this.#token(time.type, start, end);
|
|
373
|
+
let temporal;
|
|
374
|
+
if (time.type === 'Duration') {
|
|
375
|
+
// A getter, so that the `Temporal` object is made when `value` is first read.
|
|
376
|
+
const node = {
|
|
377
|
+
type: 'Duration',
|
|
378
|
+
get value() {
|
|
379
|
+
temporal ??= time.toTemporal();
|
|
380
|
+
return temporal;
|
|
381
|
+
},
|
|
382
|
+
negative: raw.startsWith('-'),
|
|
383
|
+
parts: readDurationParts(raw),
|
|
384
|
+
range,
|
|
385
|
+
loc,
|
|
386
|
+
};
|
|
387
|
+
return node;
|
|
308
388
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
389
|
+
// A getter, so that the `Temporal` object is made when `value` is first read.
|
|
390
|
+
const node = {
|
|
391
|
+
type: 'Instant',
|
|
392
|
+
get value() {
|
|
393
|
+
temporal ??= time.toTemporal();
|
|
394
|
+
return temporal;
|
|
395
|
+
},
|
|
396
|
+
range,
|
|
397
|
+
loc,
|
|
398
|
+
};
|
|
399
|
+
return node;
|
|
312
400
|
}
|
|
313
401
|
build() {
|
|
314
402
|
this.#skipTrivia();
|
|
@@ -324,10 +412,41 @@ class TreeBuilder {
|
|
|
324
412
|
body = this.#bareObject();
|
|
325
413
|
}
|
|
326
414
|
this.#skipTrivia();
|
|
327
|
-
return
|
|
415
|
+
return {
|
|
416
|
+
type: 'Document',
|
|
328
417
|
body,
|
|
329
418
|
tokens: this.#tokens,
|
|
330
419
|
comments: this.#comments,
|
|
331
|
-
|
|
420
|
+
range: [0, this.#source.length],
|
|
421
|
+
loc: this.#location(0, this.#source.length),
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/*
|
|
426
|
+
The location from the start of `first` to the end of `last`, which shares their positions rather than looking them up again. A node that ends far from where it starts, such as a large object, would otherwise move the line cache in `#position` back and forth.
|
|
427
|
+
*/
|
|
428
|
+
function spanLocation(first, last) {
|
|
429
|
+
return { start: first.loc.start, end: last.loc.end };
|
|
430
|
+
}
|
|
431
|
+
/*
|
|
432
|
+
The parts of a duration that is already valid. A part is a number, which has no letters, and then a unit, which has only lowercase letters.
|
|
433
|
+
*/
|
|
434
|
+
function readDurationParts(raw) {
|
|
435
|
+
// From `a` to `z`.
|
|
436
|
+
const isUnitCharacter = (index) => raw.charCodeAt(index) >= 0x61 && raw.charCodeAt(index) <= 0x7A;
|
|
437
|
+
const parts = [];
|
|
438
|
+
let index = raw.startsWith('-') ? 1 : 0;
|
|
439
|
+
while (index < raw.length) {
|
|
440
|
+
let unitStart = index;
|
|
441
|
+
while (!isUnitCharacter(unitStart)) {
|
|
442
|
+
unitStart++;
|
|
443
|
+
}
|
|
444
|
+
let unitEnd = unitStart;
|
|
445
|
+
while (unitEnd < raw.length && isUnitCharacter(unitEnd)) {
|
|
446
|
+
unitEnd++;
|
|
447
|
+
}
|
|
448
|
+
parts.push({ number: raw.slice(index, unitStart), unit: raw.slice(unitStart, unitEnd) });
|
|
449
|
+
index = unitEnd;
|
|
332
450
|
}
|
|
451
|
+
return parts;
|
|
333
452
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soml-lang",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Reference parser,
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "Reference parser, serializer, and formatter for SOML, a config format for humans",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": "som-lang/soml-javascript",
|
|
6
7
|
"author": "Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)",
|
|
7
8
|
"type": "module",
|
|
8
9
|
"exports": {
|
|
@@ -16,7 +17,7 @@
|
|
|
16
17
|
"scripts": {
|
|
17
18
|
"build": "tsc --project tsconfig.build.json",
|
|
18
19
|
"prepack": "npm run build",
|
|
19
|
-
"test": "xo && tsc && node --test",
|
|
20
|
+
"test": "xo && tsc && node --test 'test/*.ts'",
|
|
20
21
|
"bench": "node bench/index.ts"
|
|
21
22
|
},
|
|
22
23
|
"files": [
|