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.
- package/distribution/error.d.ts +32 -0
- package/distribution/error.js +139 -0
- package/distribution/format.d.ts +18 -0
- package/distribution/format.js +243 -0
- package/distribution/index.d.ts +5 -0
- package/distribution/index.js +5 -0
- package/distribution/parse.d.ts +68 -0
- package/distribution/parse.js +1171 -0
- package/distribution/shared.d.ts +48 -0
- package/distribution/shared.js +203 -0
- package/distribution/stringify.d.ts +35 -0
- package/distribution/stringify.js +359 -0
- package/distribution/tree.d.ts +153 -0
- package/distribution/tree.js +333 -0
- package/package.json +33 -34
- package/readme.md +90 -10
- package/index.d.ts +0 -151
- package/index.js +0 -3
- package/source/error.js +0 -130
- package/source/parse.js +0 -1574
- package/source/shared.js +0 -166
- package/source/stringify.js +0 -439
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
A position in the source: a 1-based line, and a 0-based column in UTF-16 code units, as in ESTree.
|
|
3
|
+
*/
|
|
4
|
+
export type Position = {
|
|
5
|
+
readonly line: number;
|
|
6
|
+
readonly column: number;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
Where a node, token, or comment is in the source.
|
|
10
|
+
*/
|
|
11
|
+
export type SourceLocation = {
|
|
12
|
+
readonly start: Position;
|
|
13
|
+
readonly end: Position;
|
|
14
|
+
};
|
|
15
|
+
type Located<Type extends string> = {
|
|
16
|
+
readonly type: Type;
|
|
17
|
+
/**
|
|
18
|
+
The start and end, as UTF-16 offsets into the text.
|
|
19
|
+
*/
|
|
20
|
+
readonly range: readonly [start: number, end: number];
|
|
21
|
+
readonly loc: SourceLocation;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
A token. `value` is its source text.
|
|
25
|
+
|
|
26
|
+
A `Punctuator` is one of `{`, `}`, `[`, `]`, `:`, `,`, and `.`. A `Keyword` is `true`, `false`, or `null`. `infinity` and `-infinity` are `Float` tokens.
|
|
27
|
+
*/
|
|
28
|
+
export type Token = Located<'Punctuator' | 'BareKey' | 'String' | 'Integer' | 'Float' | 'Keyword' | 'Instant' | 'Duration'> & {
|
|
29
|
+
readonly value: string;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
A comment. `value` is its text without its delimiters.
|
|
33
|
+
*/
|
|
34
|
+
export type Comment = Located<'Line' | 'Block'> & {
|
|
35
|
+
readonly value: string;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
The root of a tree.
|
|
39
|
+
*/
|
|
40
|
+
export type DocumentNode = Located<'Document'> & {
|
|
41
|
+
readonly body: ObjectNode | ArrayNode;
|
|
42
|
+
/**
|
|
43
|
+
Every token, in source order, without comments.
|
|
44
|
+
*/
|
|
45
|
+
readonly tokens: readonly Token[];
|
|
46
|
+
/**
|
|
47
|
+
Every comment, in source order.
|
|
48
|
+
*/
|
|
49
|
+
readonly comments: readonly Comment[];
|
|
50
|
+
};
|
|
51
|
+
export type ObjectNode = Located<'Object'> & {
|
|
52
|
+
readonly members: readonly MemberNode[];
|
|
53
|
+
/**
|
|
54
|
+
`false` only for a top-level object written without braces.
|
|
55
|
+
*/
|
|
56
|
+
readonly braced: boolean;
|
|
57
|
+
};
|
|
58
|
+
export type MemberNode = Located<'Member'> & {
|
|
59
|
+
readonly key: KeyNode;
|
|
60
|
+
readonly value: ValueNode;
|
|
61
|
+
};
|
|
62
|
+
export type ArrayNode = Located<'Array'> & {
|
|
63
|
+
readonly elements: readonly ValueNode[];
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
A key, with more than one segment when it is dotted.
|
|
67
|
+
*/
|
|
68
|
+
export type KeyNode = Located<'Key'> & {
|
|
69
|
+
readonly segments: readonly KeySegmentNode[];
|
|
70
|
+
};
|
|
71
|
+
export type KeySegmentNode = Located<'KeySegment'> & {
|
|
72
|
+
/**
|
|
73
|
+
The decoded key.
|
|
74
|
+
*/
|
|
75
|
+
readonly value: string;
|
|
76
|
+
readonly style: 'bare' | 'literal' | 'escaped';
|
|
77
|
+
};
|
|
78
|
+
export type StringNode = Located<'String'> & {
|
|
79
|
+
/**
|
|
80
|
+
The decoded string.
|
|
81
|
+
*/
|
|
82
|
+
readonly value: string;
|
|
83
|
+
readonly style: 'literal' | 'escaped';
|
|
84
|
+
/**
|
|
85
|
+
Whether it is a block string.
|
|
86
|
+
*/
|
|
87
|
+
readonly block: boolean;
|
|
88
|
+
};
|
|
89
|
+
export type IntegerNode = Located<'Integer'> & {
|
|
90
|
+
readonly value: bigint;
|
|
91
|
+
readonly radix: 2 | 8 | 10 | 16;
|
|
92
|
+
};
|
|
93
|
+
export type FloatNode = Located<'Float'> & {
|
|
94
|
+
readonly value: number;
|
|
95
|
+
};
|
|
96
|
+
export type BooleanNode = Located<'Boolean'> & {
|
|
97
|
+
readonly value: boolean;
|
|
98
|
+
};
|
|
99
|
+
export type NullNode = Located<'Null'>;
|
|
100
|
+
export type InstantNode = Located<'Instant'> & {
|
|
101
|
+
readonly value: Temporal.Instant;
|
|
102
|
+
};
|
|
103
|
+
export type DurationNode = Located<'Duration'> & {
|
|
104
|
+
readonly value: Temporal.Duration;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
A node that is a value.
|
|
108
|
+
*/
|
|
109
|
+
export type ValueNode = ObjectNode | ArrayNode | StringNode | IntegerNode | FloatNode | BooleanNode | NullNode | InstantNode | DurationNode;
|
|
110
|
+
/**
|
|
111
|
+
Any node in a tree.
|
|
112
|
+
*/
|
|
113
|
+
export type Node = DocumentNode | MemberNode | KeyNode | KeySegmentNode | ValueNode;
|
|
114
|
+
/**
|
|
115
|
+
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.
|
|
116
|
+
|
|
117
|
+
@example
|
|
118
|
+
```
|
|
119
|
+
import {visitorKeys} from 'soml-lang';
|
|
120
|
+
|
|
121
|
+
visitorKeys.Member;
|
|
122
|
+
//=> ['key', 'value']
|
|
123
|
+
|
|
124
|
+
visitorKeys.Integer;
|
|
125
|
+
//=> []
|
|
126
|
+
```
|
|
127
|
+
*/
|
|
128
|
+
export declare const visitorKeys: Readonly<Record<Node['type'], readonly string[]>>;
|
|
129
|
+
/**
|
|
130
|
+
Parse a document into a syntax tree, for tools such as linters and formatters.
|
|
131
|
+
|
|
132
|
+
Every node, token, and comment has a `range` and a `loc`.
|
|
133
|
+
|
|
134
|
+
@param text - The document.
|
|
135
|
+
@returns The root node, which also holds every token and comment.
|
|
136
|
+
@throws {ParseError} When the document is not valid, the same as `parse()`.
|
|
137
|
+
@throws {TypeError} When `text` is not a string.
|
|
138
|
+
|
|
139
|
+
@example
|
|
140
|
+
```
|
|
141
|
+
import {parseTree} from 'soml-lang';
|
|
142
|
+
|
|
143
|
+
const tree = parseTree('port: 8080 # The default');
|
|
144
|
+
|
|
145
|
+
tree.body.members[0].value;
|
|
146
|
+
//=> {type: 'Integer', value: 8080n, radix: 10, range: [6, 10], loc: {…}}
|
|
147
|
+
|
|
148
|
+
tree.comments[0];
|
|
149
|
+
//=> {type: 'Line', value: ' The default', range: [11, 24], loc: {…}}
|
|
150
|
+
```
|
|
151
|
+
*/
|
|
152
|
+
export declare function parseTree(text: string): DocumentNode;
|
|
153
|
+
export {};
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { parse } from "./parse.js";
|
|
2
|
+
import { isBareKeyCharacter, findNumberEnd, findLineEnd, findBlockStringEnd, } from "./shared.js";
|
|
3
|
+
/**
|
|
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
|
+
|
|
6
|
+
@example
|
|
7
|
+
```
|
|
8
|
+
import {visitorKeys} from 'soml-lang';
|
|
9
|
+
|
|
10
|
+
visitorKeys.Member;
|
|
11
|
+
//=> ['key', 'value']
|
|
12
|
+
|
|
13
|
+
visitorKeys.Integer;
|
|
14
|
+
//=> []
|
|
15
|
+
```
|
|
16
|
+
*/
|
|
17
|
+
/* eslint-disable @typescript-eslint/naming-convention -- The keys are node types. */
|
|
18
|
+
export const visitorKeys = Object.freeze({
|
|
19
|
+
Document: Object.freeze(['body']),
|
|
20
|
+
Object: Object.freeze(['members']),
|
|
21
|
+
Member: Object.freeze(['key', 'value']),
|
|
22
|
+
Array: Object.freeze(['elements']),
|
|
23
|
+
Key: Object.freeze(['segments']),
|
|
24
|
+
KeySegment: Object.freeze([]),
|
|
25
|
+
String: Object.freeze([]),
|
|
26
|
+
Integer: Object.freeze([]),
|
|
27
|
+
Float: Object.freeze([]),
|
|
28
|
+
Boolean: Object.freeze([]),
|
|
29
|
+
Null: Object.freeze([]),
|
|
30
|
+
Instant: Object.freeze([]),
|
|
31
|
+
Duration: Object.freeze([]),
|
|
32
|
+
});
|
|
33
|
+
/* 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
|
+
/*
|
|
51
|
+
`infinity` and `-infinity` are read as numbers, because they are floats.
|
|
52
|
+
*/
|
|
53
|
+
const KEYWORDS = ['true', 'false', 'null'];
|
|
54
|
+
const RADIX = { x: 16, o: 8, b: 2 };
|
|
55
|
+
/**
|
|
56
|
+
Parse a document into a syntax tree, for tools such as linters and formatters.
|
|
57
|
+
|
|
58
|
+
Every node, token, and comment has a `range` and a `loc`.
|
|
59
|
+
|
|
60
|
+
@param text - The document.
|
|
61
|
+
@returns The root node, which also holds every token and comment.
|
|
62
|
+
@throws {ParseError} When the document is not valid, the same as `parse()`.
|
|
63
|
+
@throws {TypeError} When `text` is not a string.
|
|
64
|
+
|
|
65
|
+
@example
|
|
66
|
+
```
|
|
67
|
+
import {parseTree} from 'soml-lang';
|
|
68
|
+
|
|
69
|
+
const tree = parseTree('port: 8080 # The default');
|
|
70
|
+
|
|
71
|
+
tree.body.members[0].value;
|
|
72
|
+
//=> {type: 'Integer', value: 8080n, radix: 10, range: [6, 10], loc: {…}}
|
|
73
|
+
|
|
74
|
+
tree.comments[0];
|
|
75
|
+
//=> {type: 'Line', value: ' The default', range: [11, 24], loc: {…}}
|
|
76
|
+
```
|
|
77
|
+
*/
|
|
78
|
+
export function parseTree(text) {
|
|
79
|
+
if (typeof text !== 'string') {
|
|
80
|
+
throw new TypeError(`Expected a string, got ${text === null ? 'null' : typeof text}`);
|
|
81
|
+
}
|
|
82
|
+
// Validates the whole document, so the builder below can assume valid input.
|
|
83
|
+
parse(text);
|
|
84
|
+
return new TreeBuilder(text).build();
|
|
85
|
+
}
|
|
86
|
+
/*
|
|
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.
|
|
88
|
+
*/
|
|
89
|
+
function readScalar(raw) {
|
|
90
|
+
return parse(`[${raw}]`)[0];
|
|
91
|
+
}
|
|
92
|
+
class TreeBuilder {
|
|
93
|
+
#source;
|
|
94
|
+
#index = 0;
|
|
95
|
+
#tokens = [];
|
|
96
|
+
#comments = [];
|
|
97
|
+
#lineStarts = [0];
|
|
98
|
+
constructor(source) {
|
|
99
|
+
this.#source = source;
|
|
100
|
+
for (let index = source.indexOf('\n'); index !== -1; index = source.indexOf('\n', index + 1)) {
|
|
101
|
+
this.#lineStarts.push(index + 1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
#code(offset = this.#index) {
|
|
105
|
+
return this.#source.charCodeAt(offset);
|
|
106
|
+
}
|
|
107
|
+
/*
|
|
108
|
+
1-based line, and 0-based column in UTF-16 code units, as in ESTree.
|
|
109
|
+
*/
|
|
110
|
+
#position(offset) {
|
|
111
|
+
const lineStarts = this.#lineStarts;
|
|
112
|
+
let low = 0;
|
|
113
|
+
let high = lineStarts.length - 1;
|
|
114
|
+
while (low < high) {
|
|
115
|
+
const middle = Math.ceil((low + high) / 2);
|
|
116
|
+
if (lineStarts[middle] <= offset) {
|
|
117
|
+
low = middle;
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
high = middle - 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { line: low + 1, column: offset - lineStarts[low] };
|
|
124
|
+
}
|
|
125
|
+
#node(type, start, end, fields) {
|
|
126
|
+
return {
|
|
127
|
+
type,
|
|
128
|
+
...fields,
|
|
129
|
+
range: [start, end],
|
|
130
|
+
loc: { start: this.#position(start), end: this.#position(end) },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
#token(type, start, end) {
|
|
134
|
+
const token = this.#node(type, start, end, { value: this.#source.slice(start, end) });
|
|
135
|
+
this.#tokens.push(token);
|
|
136
|
+
return token;
|
|
137
|
+
}
|
|
138
|
+
#punctuator() {
|
|
139
|
+
this.#index++;
|
|
140
|
+
return this.#token('Punctuator', this.#index - 1, this.#index);
|
|
141
|
+
}
|
|
142
|
+
#skipTrivia() {
|
|
143
|
+
const source = this.#source;
|
|
144
|
+
for (;;) {
|
|
145
|
+
const code = this.#code();
|
|
146
|
+
const start = this.#index;
|
|
147
|
+
if (WHITESPACE.has(code)) {
|
|
148
|
+
this.#index++;
|
|
149
|
+
}
|
|
150
|
+
else if (code === HASH) {
|
|
151
|
+
this.#index = findLineEnd(source, start);
|
|
152
|
+
this.#comments.push(this.#node('Line', start, this.#index, { value: source.slice(start + 1, this.#index) }));
|
|
153
|
+
}
|
|
154
|
+
else if (code === SLASH && this.#code(start + 1) === ASTERISK) {
|
|
155
|
+
this.#index = source.indexOf('*/', start + 2) + 2;
|
|
156
|
+
this.#comments.push(this.#node('Block', start, this.#index, { value: source.slice(start + 2, this.#index - 2) }));
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
#bareObject() {
|
|
164
|
+
const members = [];
|
|
165
|
+
do {
|
|
166
|
+
members.push(this.#member());
|
|
167
|
+
this.#skipTrivia();
|
|
168
|
+
} while (this.#index < this.#source.length);
|
|
169
|
+
return this.#node('Object', members[0].range[0], members.at(-1).range[1], { members, braced: false });
|
|
170
|
+
}
|
|
171
|
+
#object() {
|
|
172
|
+
const start = this.#index;
|
|
173
|
+
const members = this.#items(CLOSE_BRACE, () => this.#member());
|
|
174
|
+
return this.#node('Object', start, this.#index, { members, braced: true });
|
|
175
|
+
}
|
|
176
|
+
#array() {
|
|
177
|
+
const start = this.#index;
|
|
178
|
+
const elements = this.#items(CLOSE_BRACKET, () => this.#value());
|
|
179
|
+
return this.#node('Array', start, this.#index, { elements });
|
|
180
|
+
}
|
|
181
|
+
/*
|
|
182
|
+
The items between an opening bracket and its `closing` bracket, which is consumed too. The parser has already checked the commas.
|
|
183
|
+
*/
|
|
184
|
+
#items(closing, parseItem) {
|
|
185
|
+
const items = [];
|
|
186
|
+
this.#punctuator();
|
|
187
|
+
this.#skipTrivia();
|
|
188
|
+
while (this.#code() !== closing) {
|
|
189
|
+
items.push(parseItem());
|
|
190
|
+
this.#skipTrivia();
|
|
191
|
+
if (this.#code() !== COMMA) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
this.#punctuator();
|
|
195
|
+
this.#skipTrivia();
|
|
196
|
+
}
|
|
197
|
+
this.#punctuator();
|
|
198
|
+
return items;
|
|
199
|
+
}
|
|
200
|
+
#member() {
|
|
201
|
+
const key = this.#key();
|
|
202
|
+
this.#punctuator(); // `:`
|
|
203
|
+
this.#skipTrivia();
|
|
204
|
+
const value = this.#value();
|
|
205
|
+
return this.#node('Member', key.range[0], value.range[1], { key, value });
|
|
206
|
+
}
|
|
207
|
+
#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
|
+
const start = this.#index;
|
|
217
|
+
const code = this.#code();
|
|
218
|
+
if (code === SINGLE_QUOTE || code === DOUBLE_QUOTE) {
|
|
219
|
+
const { value, style } = this.#singleLineString();
|
|
220
|
+
return this.#node('KeySegment', start, this.#index, { value, style });
|
|
221
|
+
}
|
|
222
|
+
while (isBareKeyCharacter(this.#code())) {
|
|
223
|
+
this.#index++;
|
|
224
|
+
}
|
|
225
|
+
const token = this.#token('BareKey', start, this.#index);
|
|
226
|
+
return this.#node('KeySegment', start, this.#index, { value: token.value, style: 'bare' });
|
|
227
|
+
}
|
|
228
|
+
#value() {
|
|
229
|
+
const code = this.#code();
|
|
230
|
+
if (code === OPEN_BRACE) {
|
|
231
|
+
return this.#object();
|
|
232
|
+
}
|
|
233
|
+
if (code === OPEN_BRACKET) {
|
|
234
|
+
return this.#array();
|
|
235
|
+
}
|
|
236
|
+
const start = this.#index;
|
|
237
|
+
if (code === SINGLE_QUOTE || code === DOUBLE_QUOTE) {
|
|
238
|
+
if (this.#code(start + 1) === code && this.#code(start + 2) === code) {
|
|
239
|
+
return this.#blockString(code);
|
|
240
|
+
}
|
|
241
|
+
const { value, style } = this.#singleLineString();
|
|
242
|
+
return this.#node('String', start, this.#index, { value, style, block: false });
|
|
243
|
+
}
|
|
244
|
+
const keyword = KEYWORDS.find(word => this.#source.startsWith(word, start) && !isBareKeyCharacter(this.#code(start + word.length)));
|
|
245
|
+
if (keyword !== undefined) {
|
|
246
|
+
this.#index += keyword.length;
|
|
247
|
+
this.#token('Keyword', start, this.#index);
|
|
248
|
+
return keyword === 'null'
|
|
249
|
+
? this.#node('Null', start, this.#index, {})
|
|
250
|
+
: this.#node('Boolean', start, this.#index, { value: keyword === 'true' });
|
|
251
|
+
}
|
|
252
|
+
return this.#numberOrTime();
|
|
253
|
+
}
|
|
254
|
+
/*
|
|
255
|
+
`'...'` ends at the next `'`. `"..."` ends at the next `"` that is not escaped, and `\` always starts a two-character escape or a `\u{…}`, which holds no quote.
|
|
256
|
+
*/
|
|
257
|
+
#singleLineString() {
|
|
258
|
+
const source = this.#source;
|
|
259
|
+
const start = this.#index;
|
|
260
|
+
const quote = this.#code();
|
|
261
|
+
let end = start + 1;
|
|
262
|
+
if (quote === SINGLE_QUOTE) {
|
|
263
|
+
end = source.indexOf('\'', end);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
while (this.#code(end) !== DOUBLE_QUOTE) {
|
|
267
|
+
end += this.#code(end) === BACKSLASH ? 2 : 1;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
this.#index = end + 1;
|
|
271
|
+
const token = this.#token('String', start, this.#index);
|
|
272
|
+
return quote === SINGLE_QUOTE
|
|
273
|
+
? { value: source.slice(start + 1, end), style: 'literal' }
|
|
274
|
+
: { value: readScalar(token.value), style: 'escaped' };
|
|
275
|
+
}
|
|
276
|
+
/*
|
|
277
|
+
The block ends at the first line whose first non-whitespace content is a run of exactly as many quotes as the opening delimiter.
|
|
278
|
+
*/
|
|
279
|
+
#blockString(quote) {
|
|
280
|
+
const source = this.#source;
|
|
281
|
+
const start = this.#index;
|
|
282
|
+
let delimiterLength = 0;
|
|
283
|
+
while (this.#code(start + delimiterLength) === quote) {
|
|
284
|
+
delimiterLength++;
|
|
285
|
+
}
|
|
286
|
+
const closing = findBlockStringEnd(source, source.indexOf('\n', start) + 1, quote, delimiterLength);
|
|
287
|
+
this.#index = closing.delimiterStart + delimiterLength;
|
|
288
|
+
const token = this.#token('String', start, this.#index);
|
|
289
|
+
return this.#node('String', start, this.#index, {
|
|
290
|
+
value: readScalar(token.value),
|
|
291
|
+
style: quote === SINGLE_QUOTE ? 'literal' : 'escaped',
|
|
292
|
+
block: true,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
#numberOrTime() {
|
|
296
|
+
const start = this.#index;
|
|
297
|
+
const end = findNumberEnd(this.#source, start);
|
|
298
|
+
this.#index = end;
|
|
299
|
+
const raw = this.#source.slice(start, end);
|
|
300
|
+
const value = readScalar(raw);
|
|
301
|
+
if (typeof value === 'bigint') {
|
|
302
|
+
this.#token('Integer', start, end);
|
|
303
|
+
return this.#node('Integer', start, end, { value, radix: RADIX[raw.charAt(1)] ?? 10 });
|
|
304
|
+
}
|
|
305
|
+
if (typeof value === 'number') {
|
|
306
|
+
this.#token('Float', start, end);
|
|
307
|
+
return this.#node('Float', start, end, { value });
|
|
308
|
+
}
|
|
309
|
+
const type = value instanceof Temporal.Instant ? 'Instant' : 'Duration';
|
|
310
|
+
this.#token(type, start, end);
|
|
311
|
+
return this.#node(type, start, end, { value });
|
|
312
|
+
}
|
|
313
|
+
build() {
|
|
314
|
+
this.#skipTrivia();
|
|
315
|
+
const code = this.#code();
|
|
316
|
+
let body;
|
|
317
|
+
if (code === OPEN_BRACE) {
|
|
318
|
+
body = this.#object();
|
|
319
|
+
}
|
|
320
|
+
else if (code === OPEN_BRACKET) {
|
|
321
|
+
body = this.#array();
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
body = this.#bareObject();
|
|
325
|
+
}
|
|
326
|
+
this.#skipTrivia();
|
|
327
|
+
return this.#node('Document', 0, this.#source.length, {
|
|
328
|
+
body,
|
|
329
|
+
tokens: this.#tokens,
|
|
330
|
+
comments: this.#comments,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "soml-lang",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Reference parser
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Reference parser, canonical serializer, and formatter for SOML, a config format for humans",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"exports": {
|
|
9
|
-
"types": "./index.d.ts",
|
|
10
|
-
"default": "./index.js"
|
|
9
|
+
"types": "./distribution/index.d.ts",
|
|
10
|
+
"default": "./distribution/index.js"
|
|
11
11
|
},
|
|
12
12
|
"sideEffects": false,
|
|
13
13
|
"engines": {
|
|
14
|
-
"node": ">=
|
|
14
|
+
"node": ">=22"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"
|
|
18
|
-
"
|
|
17
|
+
"build": "tsc --project tsconfig.build.json",
|
|
18
|
+
"prepack": "npm run build",
|
|
19
|
+
"test": "xo && tsc && node --test",
|
|
20
|
+
"bench": "node bench/index.ts"
|
|
19
21
|
},
|
|
20
22
|
"files": [
|
|
21
|
-
"
|
|
22
|
-
"index.d.ts",
|
|
23
|
-
"source"
|
|
23
|
+
"distribution"
|
|
24
24
|
],
|
|
25
25
|
"keywords": [
|
|
26
26
|
"config",
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
"yaml"
|
|
36
36
|
],
|
|
37
37
|
"devDependencies": {
|
|
38
|
+
"@sindresorhus/tsconfig": "^8.1.0",
|
|
39
|
+
"@types/node": "^26.6.4",
|
|
38
40
|
"json5": "^2.2.3",
|
|
39
41
|
"smol-toml": "^1.9.0",
|
|
40
42
|
"typescript": "^6.0.3",
|
|
@@ -50,19 +52,12 @@
|
|
|
50
52
|
{
|
|
51
53
|
"rules": {
|
|
52
54
|
"unicorn/prefer-code-point": "off",
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"unicorn/no-declarations-before-early-exit": "off",
|
|
60
|
-
"unicorn/prefer-global-number-constants": "off",
|
|
61
|
-
"unicorn/no-break-in-nested-loop": "off",
|
|
62
|
-
"unicorn/prefer-else-if": "off",
|
|
63
|
-
"jsdoc/require-param-description": "off",
|
|
64
|
-
"jsdoc/require-param-type": "off",
|
|
65
|
-
"jsdoc/require-param": "off"
|
|
55
|
+
"@typescript-eslint/switch-exhaustiveness-check": [
|
|
56
|
+
"error",
|
|
57
|
+
{
|
|
58
|
+
"considerDefaultExhaustiveForUnions": true
|
|
59
|
+
}
|
|
60
|
+
]
|
|
66
61
|
}
|
|
67
62
|
},
|
|
68
63
|
{
|
|
@@ -71,20 +66,24 @@
|
|
|
71
66
|
],
|
|
72
67
|
"rules": {
|
|
73
68
|
"node-test/no-conditional-assertion": "off",
|
|
74
|
-
"node-test/no-conditional-tests": "off",
|
|
75
69
|
"node-test/no-import-test-files": "off",
|
|
76
|
-
"node-test/no-compound-assertion": "off",
|
|
77
|
-
"node-test/prefer-assert-throws": "off",
|
|
78
70
|
"node-test/require-assertion": "off",
|
|
79
|
-
"no-bitwise": "off",
|
|
80
|
-
"unicorn/prefer-unicode-code-point-escapes": "off",
|
|
81
|
-
"@stylistic/object-curly-newline": "off",
|
|
82
|
-
"regexp/no-control-character": "off",
|
|
83
|
-
"no-control-regex": "off",
|
|
84
71
|
"unicorn/max-nested-calls": "off",
|
|
85
|
-
"
|
|
86
|
-
|
|
87
|
-
|
|
72
|
+
"@typescript-eslint/no-floating-promises": [
|
|
73
|
+
"error",
|
|
74
|
+
{
|
|
75
|
+
"allowForKnownSafeCalls": [
|
|
76
|
+
{
|
|
77
|
+
"from": "package",
|
|
78
|
+
"package": "node:test",
|
|
79
|
+
"name": [
|
|
80
|
+
"test",
|
|
81
|
+
"suite"
|
|
82
|
+
]
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
]
|
|
88
87
|
}
|
|
89
88
|
}
|
|
90
89
|
]
|