@nette/neon 0.0.0-stage → 1.0.0
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/bin/neon-lint.js +4 -0
- package/dist/builder.d.ts +28 -0
- package/dist/builder.js +76 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +172 -0
- package/dist/dumper.d.ts +5 -0
- package/dist/dumper.js +75 -0
- package/dist/editing.d.ts +41 -0
- package/dist/editing.js +1188 -0
- package/dist/encoder.d.ts +16 -0
- package/dist/encoder.js +292 -0
- package/dist/errors.d.ts +8 -0
- package/dist/errors.js +14 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +14 -0
- package/dist/internal.d.ts +11 -0
- package/dist/internal.js +32 -0
- package/dist/layout.d.ts +14 -0
- package/dist/layout.js +105 -0
- package/dist/lexer.d.ts +8 -0
- package/dist/lexer.js +59 -0
- package/dist/neon.d.ts +19 -0
- package/dist/neon.js +22 -0
- package/dist/nodes.d.ts +227 -0
- package/dist/nodes.js +635 -0
- package/dist/parser.d.ts +3 -0
- package/dist/parser.js +404 -0
- package/dist/position.d.ts +8 -0
- package/dist/position.js +40 -0
- package/dist/printer.d.ts +8 -0
- package/dist/printer.js +40 -0
- package/dist/scanner.d.ts +30 -0
- package/dist/scanner.js +193 -0
- package/dist/token.d.ts +67 -0
- package/dist/token.js +309 -0
- package/dist/traverser.d.ts +31 -0
- package/dist/traverser.js +96 -0
- package/dist/utf8.d.ts +2 -0
- package/dist/utf8.js +44 -0
- package/dist/values.d.ts +75 -0
- package/dist/values.js +316 -0
- package/license.md +21 -0
- package/package.json +60 -6
- package/readme.md +242 -0
- package/README.md +0 -3
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface EncodeOptions {
|
|
2
|
+
/** Writes arrays as blocks, one item per line; a function is asked for every array, with the keys to it. */
|
|
3
|
+
blockMode?: boolean | BlockFilter;
|
|
4
|
+
/** Indentation of nested blocks and of the lines of multiline strings. */
|
|
5
|
+
indentation?: string;
|
|
6
|
+
/** @internal Writes multiline strings in """, for a document where ''' would close an earlier string. */
|
|
7
|
+
doubleQuotedMultiline?: boolean;
|
|
8
|
+
}
|
|
9
|
+
/** Decides whether the array is written as a block. */
|
|
10
|
+
export type BlockFilter = (value: unknown, path: readonly string[]) => boolean;
|
|
11
|
+
/** Writes a value as NEON. */
|
|
12
|
+
export declare function encode(value: unknown, options?: EncodeOptions): string;
|
|
13
|
+
/** Whether the string has to be quoted. */
|
|
14
|
+
export declare function needsQuotes(s: string): boolean;
|
|
15
|
+
/** Shortest round-trip digits, positional for exponents -4 to 16; null for INF and NAN. */
|
|
16
|
+
export declare function formatFloat(f: number): string | null;
|
package/dist/encoder.js
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { NeonError } from "./errors.js";
|
|
2
|
+
import { Scanner } from "./scanner.js";
|
|
3
|
+
import { Chain, Entity, evaluateLiteral, isIntegerKey, MaxDepth, NeonDate } from "./values.js";
|
|
4
|
+
/** Writes a value as NEON. */
|
|
5
|
+
export function encode(value, options = {}) {
|
|
6
|
+
return new Encoder(options).encode(value);
|
|
7
|
+
}
|
|
8
|
+
class Encoder {
|
|
9
|
+
#stack = [];
|
|
10
|
+
#arrays = [];
|
|
11
|
+
#path = [];
|
|
12
|
+
indentation;
|
|
13
|
+
doubleQuoted;
|
|
14
|
+
blockMode;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.indentation = options.indentation ?? '\t';
|
|
17
|
+
this.doubleQuoted = options.doubleQuotedMultiline ?? false;
|
|
18
|
+
this.blockMode = options.blockMode ?? false;
|
|
19
|
+
if (typeof this.indentation !== 'string' || !/^[\t ]+$/.test(this.indentation)) {
|
|
20
|
+
throw new TypeError('The indentation is a non-empty run of spaces and tabs.');
|
|
21
|
+
}
|
|
22
|
+
else if (typeof this.blockMode !== 'boolean' && typeof this.blockMode !== 'function') {
|
|
23
|
+
throw new TypeError('The blockMode is a boolean or a function.');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
encode(value) {
|
|
27
|
+
const entries = this.entries(value);
|
|
28
|
+
if (!entries || !this.isBlock(value)) {
|
|
29
|
+
return this.inline(value);
|
|
30
|
+
}
|
|
31
|
+
else if (!entries.length) {
|
|
32
|
+
return isMapLike(value) ? '{}' : '[]';
|
|
33
|
+
}
|
|
34
|
+
return this.block(value, entries);
|
|
35
|
+
}
|
|
36
|
+
block(container, entries) {
|
|
37
|
+
let res = '';
|
|
38
|
+
this.enter(container, true);
|
|
39
|
+
for (const [key, value, hidden] of withHiddenKeys(entries)) {
|
|
40
|
+
this.#path.push(key);
|
|
41
|
+
const prefix = hidden ? '-' : this.key(key) + ':';
|
|
42
|
+
const nested = this.entries(value);
|
|
43
|
+
if (nested?.length && this.isBlock(value)) {
|
|
44
|
+
const text = indent(this.block(value, nested), this.indentation);
|
|
45
|
+
res += prefix + '\n' + text + (text.endsWith('\n\n') ? '' : '\n');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
res += prefix + ' ' + this.inline(value) + '\n';
|
|
49
|
+
}
|
|
50
|
+
this.#path.pop();
|
|
51
|
+
}
|
|
52
|
+
this.leave();
|
|
53
|
+
return res;
|
|
54
|
+
}
|
|
55
|
+
isBlock(value) {
|
|
56
|
+
return typeof this.blockMode === 'function' ? this.blockMode(value, [...this.#path]) : this.blockMode;
|
|
57
|
+
}
|
|
58
|
+
inline(value) {
|
|
59
|
+
if (value instanceof Entity) {
|
|
60
|
+
this.enter(value);
|
|
61
|
+
const res = value.value === Chain ? this.chain(value) : this.entity(value);
|
|
62
|
+
this.leave();
|
|
63
|
+
return res;
|
|
64
|
+
}
|
|
65
|
+
const entries = this.entries(value);
|
|
66
|
+
if (entries) {
|
|
67
|
+
const [open, close] = Array.isArray(value) ? ['[', ']'] : ['{', '}'];
|
|
68
|
+
return open + this.items(value) + close;
|
|
69
|
+
}
|
|
70
|
+
return this.scalar(value);
|
|
71
|
+
}
|
|
72
|
+
entity(entity, link = false) {
|
|
73
|
+
const head = this.inline(entity.value);
|
|
74
|
+
if (entity.value instanceof Entity) {
|
|
75
|
+
this.fail('the value of an entity cannot be an entity or a chain');
|
|
76
|
+
}
|
|
77
|
+
else if (link && /^['"[{]/.test(head)) {
|
|
78
|
+
this.fail('a link of a chain after the first needs a head written as a literal');
|
|
79
|
+
}
|
|
80
|
+
return head + '(' + this.items(entity.attributes) + ')';
|
|
81
|
+
}
|
|
82
|
+
chain(chain) {
|
|
83
|
+
const links = chain.attributes;
|
|
84
|
+
if (!Array.isArray(links) || links.length < 2 || !links.every((link) => link instanceof Entity && link.value !== Chain)) {
|
|
85
|
+
this.fail('a chain must hold a list of two or more entities');
|
|
86
|
+
}
|
|
87
|
+
return links.map((link, i) => {
|
|
88
|
+
this.enter(link);
|
|
89
|
+
const res = this.entity(link, i > 0);
|
|
90
|
+
this.leave();
|
|
91
|
+
return res;
|
|
92
|
+
}).join('');
|
|
93
|
+
}
|
|
94
|
+
items(container) {
|
|
95
|
+
this.enter(container, true);
|
|
96
|
+
const res = withHiddenKeys(this.entries(container) ?? this.fail('attributes must be an array or an object'))
|
|
97
|
+
.map(([key, value, hidden]) => {
|
|
98
|
+
this.#path.push(key);
|
|
99
|
+
const text = (hidden ? '' : this.key(key) + ': ') + this.inline(value);
|
|
100
|
+
this.#path.pop();
|
|
101
|
+
return text;
|
|
102
|
+
})
|
|
103
|
+
.join(', ');
|
|
104
|
+
this.leave();
|
|
105
|
+
return res;
|
|
106
|
+
}
|
|
107
|
+
key(key) {
|
|
108
|
+
return isIntegerKey(key) ? key : this.string(key);
|
|
109
|
+
}
|
|
110
|
+
scalar(value) {
|
|
111
|
+
if (value === null) {
|
|
112
|
+
return 'null';
|
|
113
|
+
}
|
|
114
|
+
else if (typeof value === 'boolean') {
|
|
115
|
+
return String(value);
|
|
116
|
+
}
|
|
117
|
+
else if (typeof value === 'bigint') {
|
|
118
|
+
return value.toString();
|
|
119
|
+
}
|
|
120
|
+
else if (typeof value === 'number') {
|
|
121
|
+
return Number.isSafeInteger(value) && !Object.is(value, -0)
|
|
122
|
+
? String(value) // cannot be told from an integer; beyond 2^53 a number is a float, integers are bigint
|
|
123
|
+
: formatFloat(value) ?? this.fail('INF and NAN cannot be encoded to NEON', false);
|
|
124
|
+
}
|
|
125
|
+
else if (typeof value === 'string') {
|
|
126
|
+
if (!value.isWellFormed()) {
|
|
127
|
+
this.fail('a string with a lone surrogate');
|
|
128
|
+
}
|
|
129
|
+
return this.string(value);
|
|
130
|
+
}
|
|
131
|
+
else if (value instanceof NeonDate) {
|
|
132
|
+
return value.toString();
|
|
133
|
+
}
|
|
134
|
+
else if (value instanceof Date) {
|
|
135
|
+
return formatDate(value);
|
|
136
|
+
}
|
|
137
|
+
this.fail(`${typeof value === 'object' ? 'an instance of ' + value.constructor?.name : typeof value} cannot be encoded`);
|
|
138
|
+
}
|
|
139
|
+
string(s) {
|
|
140
|
+
if (s.includes('\n')) {
|
|
141
|
+
return this.multiline(s);
|
|
142
|
+
}
|
|
143
|
+
else if (!needsQuotes(s)) {
|
|
144
|
+
return s;
|
|
145
|
+
}
|
|
146
|
+
else if (/[\x00-\x08\x0B-\x1F]/.test(s)) { // eslint-disable-line no-control-regex
|
|
147
|
+
return jsonString(s);
|
|
148
|
+
}
|
|
149
|
+
return '\'' + s.replaceAll('\'', '\'\'') + '\'';
|
|
150
|
+
}
|
|
151
|
+
multiline(s) {
|
|
152
|
+
let delimiter = '\'\'\'';
|
|
153
|
+
let content = s;
|
|
154
|
+
const firstLine = /^\n*([^\n]*)/.exec(s)[1];
|
|
155
|
+
if (this.doubleQuoted || /[\x00-\x08\x0B-\x1F]/.test(s) || /(?:^|\n)[\t ]*'''/.test(s) || /^[\t ]/.test(firstLine)) { // eslint-disable-line no-control-regex
|
|
156
|
+
delimiter = '"""';
|
|
157
|
+
content = jsonString(s).slice(1, -1)
|
|
158
|
+
.replace(/\\(.)/g, (seq, c) => ({ 'n': '\n', 't': '\t', '"': '"' })[c] ?? seq)
|
|
159
|
+
.replaceAll('"""', '""\\"')
|
|
160
|
+
.replace(/^(\n*)([\t ]+)/, (_, newlines, space) => newlines + space.replaceAll(' ', '\\u0020').replaceAll('\t', '\\t'));
|
|
161
|
+
}
|
|
162
|
+
return delimiter + '\n' + indent(content, this.indentation) + '\n' + delimiter;
|
|
163
|
+
}
|
|
164
|
+
/** Entries of an array, a Map or a plain object; null for anything else. */
|
|
165
|
+
entries(value) {
|
|
166
|
+
if (Array.isArray(value)) {
|
|
167
|
+
const res = [];
|
|
168
|
+
for (let i = 0; i < value.length; i++) {
|
|
169
|
+
if (!(i in value)) {
|
|
170
|
+
this.#path.push(String(i));
|
|
171
|
+
this.fail('a sparse array cannot be encoded');
|
|
172
|
+
}
|
|
173
|
+
res.push([String(i), value[i]]);
|
|
174
|
+
}
|
|
175
|
+
return res;
|
|
176
|
+
}
|
|
177
|
+
else if (value instanceof Map) {
|
|
178
|
+
const keys = (new Set);
|
|
179
|
+
return [...value].map(([key, v]) => {
|
|
180
|
+
if (typeof key !== 'string' && typeof key !== 'number') {
|
|
181
|
+
this.fail('a key of a Map must be a string or a number');
|
|
182
|
+
}
|
|
183
|
+
else if (keys.has(String(key))) {
|
|
184
|
+
this.fail(`the keys of a Map give the key '${String(key)}' twice`);
|
|
185
|
+
}
|
|
186
|
+
keys.add(String(key));
|
|
187
|
+
return [String(key), v];
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
else if (isPlainObject(value)) {
|
|
191
|
+
return Object.keys(value).map((key) => [key, value[key]]);
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
/** Goes into a container, an array being one level of nesting. */
|
|
196
|
+
enter(container, array = false) {
|
|
197
|
+
if (this.#stack.includes(container)) {
|
|
198
|
+
this.fail('a cycle cannot be encoded');
|
|
199
|
+
}
|
|
200
|
+
else if (array && this.#arrays.length >= MaxDepth) {
|
|
201
|
+
this.fail(`arrays nested deeper than ${MaxDepth} levels`);
|
|
202
|
+
}
|
|
203
|
+
this.#stack.push(container);
|
|
204
|
+
if (array) {
|
|
205
|
+
this.#arrays.push(container);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
leave() {
|
|
209
|
+
if (this.#arrays.at(-1) === this.#stack.pop()) {
|
|
210
|
+
this.#arrays.pop();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
fail(reason, withPath = true) {
|
|
214
|
+
const at = withPath && this.#path.length ? ` at ${this.#path.join('.')}` : '';
|
|
215
|
+
throw new NeonError(withPath ? `Cannot encode: ${reason}${at}` : reason);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** Marks the keys that decoding would assign anyway. */
|
|
219
|
+
function withHiddenKeys(entries) {
|
|
220
|
+
let counter = 0n;
|
|
221
|
+
let hiding = true;
|
|
222
|
+
return entries.map(([key, value]) => {
|
|
223
|
+
const integer = isIntegerKey(key);
|
|
224
|
+
const hidden = hiding && integer && BigInt(key) === counter;
|
|
225
|
+
if (integer && hiding) {
|
|
226
|
+
const k = BigInt(key);
|
|
227
|
+
hiding = k === counter;
|
|
228
|
+
counter = k + 1n > counter ? k + 1n : counter;
|
|
229
|
+
}
|
|
230
|
+
return [key, value, hidden];
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/** Whether the string has to be quoted. */
|
|
234
|
+
export function needsQuotes(s) {
|
|
235
|
+
const token = new Scanner(s).next();
|
|
236
|
+
return !token || token.kind !== 'Literal' || token.end !== s.length
|
|
237
|
+
|| /[\x00-\x1F]/.test(s) // eslint-disable-line no-control-regex
|
|
238
|
+
|| /^[+\-.]?\d/.test(s)
|
|
239
|
+
|| /^(?:true|false|yes|no|on|off|null)$/i.test(s)
|
|
240
|
+
|| evaluateLiteral(s) !== s;
|
|
241
|
+
}
|
|
242
|
+
function jsonString(s) {
|
|
243
|
+
return JSON.stringify(s).replaceAll('\u2028', '\\u2028').replaceAll('\u2029', '\\u2029');
|
|
244
|
+
}
|
|
245
|
+
/** Shortest round-trip digits, positional for exponents -4 to 16; null for INF and NAN. */
|
|
246
|
+
export function formatFloat(f) {
|
|
247
|
+
if (!Number.isFinite(f)) {
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
else if (f === 0) {
|
|
251
|
+
return Object.is(f, -0) ? '-0.0' : '0.0';
|
|
252
|
+
}
|
|
253
|
+
const [mantissa, exponent] = Math.abs(f).toExponential().split('e');
|
|
254
|
+
const digits = mantissa.replace('.', '');
|
|
255
|
+
const e = Number(exponent);
|
|
256
|
+
const sign = f < 0 ? '-' : '';
|
|
257
|
+
if (e >= -4 && e < 17) {
|
|
258
|
+
if (e < 0) {
|
|
259
|
+
return sign + '0.' + '0'.repeat(-e - 1) + digits;
|
|
260
|
+
}
|
|
261
|
+
const whole = digits.slice(0, e + 1).padEnd(e + 1, '0');
|
|
262
|
+
const fraction = digits.slice(e + 1);
|
|
263
|
+
return sign + whole + '.' + (fraction || '0');
|
|
264
|
+
}
|
|
265
|
+
return sign + (mantissa.includes('.') ? mantissa : mantissa + '.0') + 'e' + (e < 0 ? '-' : '+') + Math.abs(e);
|
|
266
|
+
}
|
|
267
|
+
/** An instant, written in UTC. */
|
|
268
|
+
function formatDate(date) {
|
|
269
|
+
if (Number.isNaN(date.getTime())) {
|
|
270
|
+
throw new NeonError('Cannot encode: an invalid Date');
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
return NeonDate.fromDate(date).toString();
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
throw new NeonError('Cannot encode: a Date out of the years 0 to 9999');
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** Prefixes every non-empty line. */
|
|
280
|
+
function indent(text, indentation) {
|
|
281
|
+
return text.split('\n').map((line) => line === '' ? line : indentation + line).join('\n'); // a regex would also split at CR and U+2028
|
|
282
|
+
}
|
|
283
|
+
function isPlainObject(value) {
|
|
284
|
+
if (typeof value !== 'object' || value === null) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
const proto = Object.getPrototypeOf(value);
|
|
288
|
+
return proto === Object.prototype || proto === null;
|
|
289
|
+
}
|
|
290
|
+
function isMapLike(value) {
|
|
291
|
+
return value instanceof Map || isPlainObject(value);
|
|
292
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Position } from './token.ts';
|
|
2
|
+
/** Error of the scanner, the parser or the evaluation of values. */
|
|
3
|
+
export declare class NeonError extends Error {
|
|
4
|
+
readonly position: Position | null;
|
|
5
|
+
name: string;
|
|
6
|
+
constructor(message: string, position?: Position | null);
|
|
7
|
+
toString(): string;
|
|
8
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Error of the scanner, the parser or the evaluation of values. */
|
|
2
|
+
export class NeonError extends Error {
|
|
3
|
+
position;
|
|
4
|
+
name = 'NeonError';
|
|
5
|
+
constructor(message, position = null) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.position = position;
|
|
8
|
+
}
|
|
9
|
+
toString() {
|
|
10
|
+
return this.position
|
|
11
|
+
? `${this.message} on line ${this.position.line} at column ${this.position.column}`
|
|
12
|
+
: this.message;
|
|
13
|
+
}
|
|
14
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { Builder } from './builder.ts';
|
|
2
|
+
export { dump } from './dumper.ts';
|
|
3
|
+
export { CommentPolicy } from './editing.ts';
|
|
4
|
+
export type { PathSegment } from './editing.ts';
|
|
5
|
+
export { Layout } from './layout.ts';
|
|
6
|
+
export type { EncodeOptions } from './encoder.ts';
|
|
7
|
+
export { NeonError } from './errors.ts';
|
|
8
|
+
export { decode, encode, Neon } from './neon.ts';
|
|
9
|
+
export { tokenize } from './lexer.ts';
|
|
10
|
+
export { ArrayItemNode, BlockArrayNode, DocumentNode, EntityChainNode, EntityNode, InlineArrayNode, LiteralNode, Node, StringNode, ValueNode, } from './nodes.ts';
|
|
11
|
+
export { parse } from './parser.ts';
|
|
12
|
+
export { locateAll, print, printText } from './printer.ts';
|
|
13
|
+
export { Token, Trivia } from './token.ts';
|
|
14
|
+
export { TraverseAction, Traverser } from './traverser.ts';
|
|
15
|
+
export type { Visitor, VisitResult } from './traverser.ts';
|
|
16
|
+
export type { Position, TokenKind, TriviaKind } from './token.ts';
|
|
17
|
+
export { decodeUtf8 } from './utf8.ts';
|
|
18
|
+
export { Chain, Entity, NeonDate } from './values.ts';
|
|
19
|
+
export type { NeonValue } from './values.ts';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { Builder } from "./builder.js";
|
|
2
|
+
export { dump } from "./dumper.js";
|
|
3
|
+
export { CommentPolicy } from "./editing.js";
|
|
4
|
+
export { Layout } from "./layout.js";
|
|
5
|
+
export { NeonError } from "./errors.js";
|
|
6
|
+
export { decode, encode, Neon } from "./neon.js";
|
|
7
|
+
export { tokenize } from "./lexer.js";
|
|
8
|
+
export { ArrayItemNode, BlockArrayNode, DocumentNode, EntityChainNode, EntityNode, InlineArrayNode, LiteralNode, Node, StringNode, ValueNode, } from "./nodes.js";
|
|
9
|
+
export { parse } from "./parser.js";
|
|
10
|
+
export { locateAll, print, printText } from "./printer.js";
|
|
11
|
+
export { Token, Trivia } from "./token.js";
|
|
12
|
+
export { TraverseAction, Traverser } from "./traverser.js";
|
|
13
|
+
export { decodeUtf8 } from "./utf8.js";
|
|
14
|
+
export { Chain, Entity, NeonDate } from "./values.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Keys of the members that only the tree itself may call. They are not exported from the package. */
|
|
2
|
+
export declare const setParent: unique symbol;
|
|
3
|
+
export declare const setItems: unique symbol;
|
|
4
|
+
export declare const setSlots: unique symbol;
|
|
5
|
+
export declare const setLeading: unique symbol;
|
|
6
|
+
export declare const setTrailing: unique symbol;
|
|
7
|
+
export declare const forgetOrigin: unique symbol;
|
|
8
|
+
/** Notes how to undo a write of the tree, while an atomic() operation runs. */
|
|
9
|
+
export declare function record(undo: () => void): void;
|
|
10
|
+
/** Runs an operation; if it throws, every write it made is undone, so a refused operation changes nothing. */
|
|
11
|
+
export declare function atomic<T>(operation: () => T): T;
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Keys of the members that only the tree itself may call. They are not exported from the package. */
|
|
2
|
+
export const setParent = Symbol('setParent');
|
|
3
|
+
export const setItems = Symbol('setItems');
|
|
4
|
+
export const setSlots = Symbol('setSlots');
|
|
5
|
+
export const setLeading = Symbol('setLeading');
|
|
6
|
+
export const setTrailing = Symbol('setTrailing');
|
|
7
|
+
export const forgetOrigin = Symbol('forgetOrigin');
|
|
8
|
+
let journal = null;
|
|
9
|
+
/** Notes how to undo a write of the tree, while an atomic() operation runs. */
|
|
10
|
+
export function record(undo) {
|
|
11
|
+
journal?.push(undo);
|
|
12
|
+
}
|
|
13
|
+
/** Runs an operation; if it throws, every write it made is undone, so a refused operation changes nothing. */
|
|
14
|
+
export function atomic(operation) {
|
|
15
|
+
if (journal) {
|
|
16
|
+
return operation();
|
|
17
|
+
}
|
|
18
|
+
const list = journal = [];
|
|
19
|
+
try {
|
|
20
|
+
return operation();
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
journal = null;
|
|
24
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
25
|
+
list[i]();
|
|
26
|
+
}
|
|
27
|
+
throw e;
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
journal = null;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/layout.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ArrayItemNode, type Node } from './nodes.ts';
|
|
2
|
+
import { type Token } from './token.ts';
|
|
3
|
+
/** Layout of new text, derived from the neighbourhood of the place of an edit. */
|
|
4
|
+
export declare class Layout {
|
|
5
|
+
/** The difference of indentation between a nested block and its parent. */
|
|
6
|
+
static unit(at: Node): string;
|
|
7
|
+
/**
|
|
8
|
+
* The indentation of the block the item is in. It is the indentation of the line of the item, except
|
|
9
|
+
* for the first item of a block opened on the line of a dash, which stands after the dash.
|
|
10
|
+
*/
|
|
11
|
+
static indentation(item: ArrayItemNode): string;
|
|
12
|
+
/** The line ending of the nearest preceding token, else the first of the document, else LF. */
|
|
13
|
+
static lineEnding(at: Node | Token): string;
|
|
14
|
+
}
|
package/dist/layout.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { ArrayItemNode, BlockArrayNode, DocumentNode } from "./nodes.js";
|
|
2
|
+
import { iterateNeighbours } from "./token.js";
|
|
3
|
+
/** Layout of new text, derived from the neighbourhood of the place of an edit. */
|
|
4
|
+
export class Layout {
|
|
5
|
+
/** The difference of indentation between a nested block and its parent. */
|
|
6
|
+
static unit(at) {
|
|
7
|
+
for (const candidate of candidates(at)) {
|
|
8
|
+
const unit = unitOf(candidate);
|
|
9
|
+
if (unit) {
|
|
10
|
+
return unit;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return '\t';
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The indentation of the block the item is in. It is the indentation of the line of the item, except
|
|
17
|
+
* for the first item of a block opened on the line of a dash, which stands after the dash.
|
|
18
|
+
*/
|
|
19
|
+
static indentation(item) {
|
|
20
|
+
const block = item.parent;
|
|
21
|
+
const owner = block?.parent;
|
|
22
|
+
if (block instanceof BlockArrayNode && owner instanceof ArrayItemNode && owner.bullet
|
|
23
|
+
&& !owner.bullet.trailingTrivia.some((t) => t.kind === 'LineEnding')) {
|
|
24
|
+
const second = block.items[1];
|
|
25
|
+
if (second) {
|
|
26
|
+
return second.getIndentation();
|
|
27
|
+
}
|
|
28
|
+
// as the parser decides: a tab if the next line within the block starts with one
|
|
29
|
+
const outer = Layout.indentation(owner);
|
|
30
|
+
const next = block.getTokens().slice(1).find((token) => token.leadingTrivia.some((t) => t.kind === 'Whitespace')
|
|
31
|
+
&& !!token.leadingTrivia.length);
|
|
32
|
+
const nextIndent = next?.leadingTrivia.at(-1)?.text ?? '';
|
|
33
|
+
return outer + (nextIndent.startsWith(outer + '\t') ? '\t' : ' ');
|
|
34
|
+
}
|
|
35
|
+
return item.getIndentation();
|
|
36
|
+
}
|
|
37
|
+
/** The line ending of the nearest preceding token, else the first of the document, else LF. */
|
|
38
|
+
static lineEnding(at) {
|
|
39
|
+
const root = rootOf(at);
|
|
40
|
+
if (!root) {
|
|
41
|
+
return '\n';
|
|
42
|
+
}
|
|
43
|
+
const token = at instanceof ArrayItemNode || 'getFirstToken' in at ? at.getFirstToken() : at;
|
|
44
|
+
const first = root.getFirstToken();
|
|
45
|
+
const find = (list) => {
|
|
46
|
+
for (const token of list) {
|
|
47
|
+
const trivia = [...token.trailingTrivia, ...token.leadingTrivia].find((t) => t.kind === 'LineEnding');
|
|
48
|
+
if (trivia) {
|
|
49
|
+
return trivia.text;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
54
|
+
return find(iterateNeighbours(token, true)) ?? find([first]) ?? find(iterateNeighbours(first, false)) ?? '\n';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** The blocks to take the unit from: the siblings, the nearest first, then the ancestors, then the whole document. */
|
|
58
|
+
function* candidates(at) {
|
|
59
|
+
const block = at instanceof BlockArrayNode ? at : at.findAncestor(BlockArrayNode);
|
|
60
|
+
if (block) {
|
|
61
|
+
const item = at instanceof ArrayItemNode ? at : at.findAncestor(ArrayItemNode);
|
|
62
|
+
const index = item ? Math.max(block.items.indexOf(item), 0) : 0;
|
|
63
|
+
for (let distance = 0; distance < block.items.length; distance++) {
|
|
64
|
+
for (const i of distance ? [index - distance, index + distance] : [index]) {
|
|
65
|
+
const value = block.items[i]?.value;
|
|
66
|
+
if (value instanceof BlockArrayNode) {
|
|
67
|
+
yield value;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (let node = block; node; node = node.parent) {
|
|
73
|
+
if (node instanceof BlockArrayNode) {
|
|
74
|
+
yield node;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const root = rootOf(at);
|
|
78
|
+
if (root) {
|
|
79
|
+
yield* root.find(BlockArrayNode);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Unit of a nested block, or null for a block opened on the line of a dash or not nested. */
|
|
83
|
+
function unitOf(block) {
|
|
84
|
+
const owner = block.parent;
|
|
85
|
+
if (!(owner instanceof ArrayItemNode)) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const head = owner.colon ?? owner.bullet;
|
|
89
|
+
if (!head?.trailingTrivia.some((t) => t.kind === 'LineEnding')) {
|
|
90
|
+
return null; // opened on the line of the dash
|
|
91
|
+
}
|
|
92
|
+
const outer = owner.getIndentation();
|
|
93
|
+
const inner = block.items[0].getIndentation();
|
|
94
|
+
return inner.length > outer.length && inner.startsWith(outer) ? inner.slice(outer.length) : null;
|
|
95
|
+
}
|
|
96
|
+
function rootOf(at) {
|
|
97
|
+
let node = at.parent;
|
|
98
|
+
if (at instanceof DocumentNode) {
|
|
99
|
+
return at;
|
|
100
|
+
}
|
|
101
|
+
while (node?.parent) {
|
|
102
|
+
node = node.parent;
|
|
103
|
+
}
|
|
104
|
+
return node instanceof DocumentNode ? node : null;
|
|
105
|
+
}
|
package/dist/lexer.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Token } from './token.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Splits the input into tokens with their trivia attached. The last token is EndOfFile.
|
|
4
|
+
* Printing all tokens in order gives back the input.
|
|
5
|
+
*/
|
|
6
|
+
export declare function tokenize(input: string): Token[];
|
|
7
|
+
/** Text quoted by 'Unexpected': cut to 40 code points, a line ending counting as one, shown as <new line>. */
|
|
8
|
+
export declare function quote(text: string): string;
|
package/dist/lexer.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { NeonError } from "./errors.js";
|
|
2
|
+
import { Scanner } from "./scanner.js";
|
|
3
|
+
import { Cursor } from "./position.js";
|
|
4
|
+
import { Trivia, trustedToken } from "./token.js";
|
|
5
|
+
/**
|
|
6
|
+
* Splits the input into tokens with their trivia attached. The last token is EndOfFile.
|
|
7
|
+
* Printing all tokens in order gives back the input.
|
|
8
|
+
*/
|
|
9
|
+
export function tokenize(input) {
|
|
10
|
+
checkWellFormed(input);
|
|
11
|
+
const scanner = new Scanner(input);
|
|
12
|
+
const cursor = new Cursor(input);
|
|
13
|
+
const tokens = [];
|
|
14
|
+
let pending = []; // trivia not yet attached
|
|
15
|
+
let last = null;
|
|
16
|
+
const flush = () => {
|
|
17
|
+
if (last) {
|
|
18
|
+
tokens.push(trustedToken(last.kind, last.text, last.leading, last.trailing, last.origin));
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
while (true) {
|
|
22
|
+
const item = scanner.next();
|
|
23
|
+
if (!item) {
|
|
24
|
+
if (scanner.errorAt >= 0) {
|
|
25
|
+
throw new NeonError(`Unexpected '${quote(input.slice(scanner.errorAt))}'`, cursor.at(scanner.errorAt));
|
|
26
|
+
}
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
const text = input.slice(item.start, item.end);
|
|
30
|
+
if (item.kind === 'Trivia') {
|
|
31
|
+
const trivia = new Trivia(item.triviaKind, text);
|
|
32
|
+
// trailing trivia of the last token run up to and including the first line ending
|
|
33
|
+
if (last && !pending.length && !last.trailing.some((t) => t.kind === 'LineEnding')) {
|
|
34
|
+
last.trailing.push(trivia);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
pending.push(trivia);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
flush();
|
|
42
|
+
last = { kind: item.kind, text, leading: pending, trailing: [], origin: cursor.at(item.start) };
|
|
43
|
+
pending = [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
flush();
|
|
47
|
+
tokens.push(trustedToken('EndOfFile', '', pending, [], cursor.at(input.length)));
|
|
48
|
+
return tokens;
|
|
49
|
+
}
|
|
50
|
+
/** Text quoted by 'Unexpected': cut to 40 code points, a line ending counting as one, shown as <new line>. */
|
|
51
|
+
export function quote(text) {
|
|
52
|
+
return Array.from(text.replaceAll('\r\n', '\n')).slice(0, 40).join('').replaceAll('\n', '<new line>');
|
|
53
|
+
}
|
|
54
|
+
function checkWellFormed(input) {
|
|
55
|
+
if (!input.isWellFormed()) {
|
|
56
|
+
const i = input.search(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/);
|
|
57
|
+
throw new NeonError('Invalid UTF-8 sequence.', new Cursor(input).at(i));
|
|
58
|
+
}
|
|
59
|
+
}
|
package/dist/neon.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type BlockFilter, type EncodeOptions } from './encoder.ts';
|
|
2
|
+
import type { Node } from './nodes.ts';
|
|
3
|
+
import { parse } from './parser.ts';
|
|
4
|
+
import { type NeonValue } from './values.ts';
|
|
5
|
+
/** Parses NEON and returns its value. */
|
|
6
|
+
export declare function decode(input: string): NeonValue;
|
|
7
|
+
/**
|
|
8
|
+
* Writes a value as NEON. In block mode, arrays are written one item per line; a function
|
|
9
|
+
* `blockMode(value, path)` is asked for every array and writes it as a block if true.
|
|
10
|
+
*/
|
|
11
|
+
export declare function encode(value: unknown, blockMode?: boolean | BlockFilter | EncodeOptions, indentation?: string): string;
|
|
12
|
+
/** The facade, read as `Neon.decode(text)` like in PHP. */
|
|
13
|
+
export declare const Neon: {
|
|
14
|
+
readonly decode: typeof decode;
|
|
15
|
+
readonly encode: typeof encode;
|
|
16
|
+
readonly parse: typeof parse;
|
|
17
|
+
readonly print: (node: Node) => string;
|
|
18
|
+
readonly Chain: "!!chain";
|
|
19
|
+
};
|
package/dist/neon.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { encode as encodeValue } from "./encoder.js";
|
|
2
|
+
import { parse } from "./parser.js";
|
|
3
|
+
import { Chain } from "./values.js";
|
|
4
|
+
/** Parses NEON and returns its value. */
|
|
5
|
+
export function decode(input) {
|
|
6
|
+
return parse(input).toValue();
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Writes a value as NEON. In block mode, arrays are written one item per line; a function
|
|
10
|
+
* `blockMode(value, path)` is asked for every array and writes it as a block if true.
|
|
11
|
+
*/
|
|
12
|
+
export function encode(value, blockMode = false, indentation = '\t') {
|
|
13
|
+
return encodeValue(value, typeof blockMode === 'object' ? blockMode : { blockMode, indentation });
|
|
14
|
+
}
|
|
15
|
+
/** The facade, read as `Neon.decode(text)` like in PHP. */
|
|
16
|
+
export const Neon = {
|
|
17
|
+
decode,
|
|
18
|
+
encode,
|
|
19
|
+
parse,
|
|
20
|
+
print: (node) => node.print(),
|
|
21
|
+
Chain,
|
|
22
|
+
};
|