@nette/neon 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +1204 -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 +438 -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 +313 -0
- package/license.md +21 -0
- package/package.json +60 -6
- package/readme.md +243 -0
- package/README.md +0 -3
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { ArrayItemNode, ValueNode } from "./nodes.js";
|
|
2
|
+
/** What a visitor returns to steer the walk; symbols, so that no other value means an action by accident. */
|
|
3
|
+
export const TraverseAction = Object.freeze({
|
|
4
|
+
SkipChildren: Symbol('SkipChildren'),
|
|
5
|
+
Stop: Symbol('Stop'),
|
|
6
|
+
Remove: Symbol('Remove'),
|
|
7
|
+
});
|
|
8
|
+
/**
|
|
9
|
+
* Walks the tree depth-first, calling enter() before the children of a node and leave() after them.
|
|
10
|
+
* The children of a node are taken when it is entered, so nodes inserted around the visited one later
|
|
11
|
+
* are not visited, and a child taken out of the node meanwhile is skipped; a node the visitor takes out
|
|
12
|
+
* of its place itself is neither walked into nor left. A replacement is visited from its children on.
|
|
13
|
+
* Stop ends the walk at once: no node is entered or left after it. The walk is not one atomic edit:
|
|
14
|
+
* the edits before a failure stay.
|
|
15
|
+
*/
|
|
16
|
+
export class Traverser {
|
|
17
|
+
enter;
|
|
18
|
+
leave;
|
|
19
|
+
#stopped = false;
|
|
20
|
+
/** The nodes being walked, from the first one, each with the parent it was entered under. */
|
|
21
|
+
#path = [];
|
|
22
|
+
constructor(enter, leave) {
|
|
23
|
+
this.enter = enter;
|
|
24
|
+
this.leave = leave;
|
|
25
|
+
}
|
|
26
|
+
static traverse(node, enter, leave) {
|
|
27
|
+
return new Traverser(enter, leave).visit(node);
|
|
28
|
+
}
|
|
29
|
+
/** Returns the node that stands in the place of the visited one afterwards, null if it was removed or taken out. */
|
|
30
|
+
visit(node) {
|
|
31
|
+
const level = this.#path.length;
|
|
32
|
+
this.#path.push([node, node.parent]);
|
|
33
|
+
try {
|
|
34
|
+
let result = this.enter?.(node);
|
|
35
|
+
const current = this.apply(node, result);
|
|
36
|
+
if (!current) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
this.#path[level] = [current, this.#path[level][1]];
|
|
40
|
+
if (!this.isInPlace(level)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
else if (this.#stopped) {
|
|
44
|
+
return current;
|
|
45
|
+
}
|
|
46
|
+
if (result !== TraverseAction.SkipChildren) {
|
|
47
|
+
for (const child of current.getChildren()) {
|
|
48
|
+
if ('getChildren' in child && child.parent === current) { // nodes only, tokens are not visited
|
|
49
|
+
this.visit(child);
|
|
50
|
+
if (this.#stopped) {
|
|
51
|
+
return current;
|
|
52
|
+
}
|
|
53
|
+
else if (!this.isInPlace(level)) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
result = this.leave?.(current);
|
|
60
|
+
return this.apply(current, result);
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
this.#path.pop();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** Whether the node at the level and the nodes above it still stand where they were entered. */
|
|
67
|
+
isInPlace(level) {
|
|
68
|
+
for (let i = level; i >= 0; i--) {
|
|
69
|
+
const [node, parent] = this.#path[i];
|
|
70
|
+
if (node.parent !== parent) {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
apply(node, result) {
|
|
77
|
+
if (result === TraverseAction.Stop) {
|
|
78
|
+
this.#stopped = true;
|
|
79
|
+
}
|
|
80
|
+
else if (result === TraverseAction.Remove) {
|
|
81
|
+
if (!(node instanceof ArrayItemNode)) {
|
|
82
|
+
throw new Error('Only an item can be removed; replace any other node.');
|
|
83
|
+
}
|
|
84
|
+
node.remove();
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
else if (result instanceof ValueNode) {
|
|
88
|
+
if (!(node instanceof ValueNode)) {
|
|
89
|
+
throw new Error('Only a value node can be replaced.');
|
|
90
|
+
}
|
|
91
|
+
node.replaceWith(result);
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
94
|
+
return node;
|
|
95
|
+
}
|
|
96
|
+
}
|
package/dist/utf8.d.ts
ADDED
package/dist/utf8.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { NeonError } from "./errors.js";
|
|
2
|
+
import { Cursor } from "./position.js";
|
|
3
|
+
const decoder = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
|
|
4
|
+
/** Decodes UTF-8 bytes; invalid ones fail with the position of the first invalid byte. */
|
|
5
|
+
export function decodeUtf8(bytes) {
|
|
6
|
+
try {
|
|
7
|
+
return decoder.decode(bytes);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
let valid = 0;
|
|
11
|
+
for (let n; (n = sequenceLength(bytes, valid)); valid += n) {
|
|
12
|
+
;
|
|
13
|
+
}
|
|
14
|
+
const prefix = decoder.decode(bytes.subarray(0, valid));
|
|
15
|
+
throw new NeonError('Invalid UTF-8 sequence.', new Cursor(prefix).at(prefix.length));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Ranges of the bytes of the well-formed sequences, by the first byte (Unicode table 3-7). */
|
|
19
|
+
const Sequences = [
|
|
20
|
+
[0x00, 0x7F, []],
|
|
21
|
+
[0xC2, 0xDF, [[0x80, 0xBF]]],
|
|
22
|
+
[0xE0, 0xE0, [[0xA0, 0xBF], [0x80, 0xBF]]],
|
|
23
|
+
[0xE1, 0xEC, [[0x80, 0xBF], [0x80, 0xBF]]],
|
|
24
|
+
[0xED, 0xED, [[0x80, 0x9F], [0x80, 0xBF]]],
|
|
25
|
+
[0xEE, 0xEF, [[0x80, 0xBF], [0x80, 0xBF]]],
|
|
26
|
+
[0xF0, 0xF0, [[0x90, 0xBF], [0x80, 0xBF], [0x80, 0xBF]]],
|
|
27
|
+
[0xF1, 0xF3, [[0x80, 0xBF], [0x80, 0xBF], [0x80, 0xBF]]],
|
|
28
|
+
[0xF4, 0xF4, [[0x80, 0x8F], [0x80, 0xBF], [0x80, 0xBF]]],
|
|
29
|
+
];
|
|
30
|
+
/** Length of the well-formed sequence at the offset, or 0. */
|
|
31
|
+
function sequenceLength(bytes, i) {
|
|
32
|
+
const first = bytes[i];
|
|
33
|
+
const sequence = first === undefined ? undefined : Sequences.find(([lo, hi]) => first >= lo && first <= hi);
|
|
34
|
+
if (!sequence) {
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
const rest = sequence[2];
|
|
38
|
+
return rest.every(([lo, hi], k) => {
|
|
39
|
+
const b = bytes[i + k + 1];
|
|
40
|
+
return b !== undefined && b >= lo && b <= hi;
|
|
41
|
+
})
|
|
42
|
+
? rest.length + 1
|
|
43
|
+
: 0;
|
|
44
|
+
}
|
package/dist/values.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/** Value of the neutral model mapped to JavaScript. */
|
|
2
|
+
export type NeonValue = null | boolean | number | bigint | string | NeonDate | Entity | NeonValue[] | {
|
|
3
|
+
[key: string]: NeonValue;
|
|
4
|
+
};
|
|
5
|
+
/** The value of an entity, `value(attributes)`; a chain is an entity whose value is Chain. */
|
|
6
|
+
export declare class Entity {
|
|
7
|
+
value: NeonValue;
|
|
8
|
+
attributes: NeonValue[] | {
|
|
9
|
+
[key: string]: NeonValue;
|
|
10
|
+
};
|
|
11
|
+
constructor(value: NeonValue, attributes?: NeonValue[] | {
|
|
12
|
+
[key: string]: NeonValue;
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/** The value of an entity that represents a chain; its attributes are the links. */
|
|
16
|
+
export declare const Chain = "!!chain";
|
|
17
|
+
/** A date as written: components and an offset, or none for a local date. */
|
|
18
|
+
export declare class NeonDate {
|
|
19
|
+
readonly year: number;
|
|
20
|
+
readonly month: number;
|
|
21
|
+
readonly day: number;
|
|
22
|
+
readonly hour: number | null;
|
|
23
|
+
readonly minute: number | null;
|
|
24
|
+
readonly second: number | null;
|
|
25
|
+
/** Digits of the fraction of a second as written, without the point. */
|
|
26
|
+
readonly fraction: string | null;
|
|
27
|
+
/** `+HH:MM` or `-HH:MM`, or null for a local date. */
|
|
28
|
+
readonly offset: string | null;
|
|
29
|
+
constructor(year: number, month: number, day: number, hour?: number | null, minute?: number | null, second?: number | null,
|
|
30
|
+
/** Digits of the fraction of a second as written, without the point. */
|
|
31
|
+
fraction?: string | null,
|
|
32
|
+
/** `+HH:MM` or `-HH:MM`, or null for a local date. */
|
|
33
|
+
offset?: string | null);
|
|
34
|
+
/** An instant as a date in UTC. */
|
|
35
|
+
static fromDate(date: Date): NeonDate;
|
|
36
|
+
/** Parses the text of a date literal; null if the text is not one, its components making no valid date included. */
|
|
37
|
+
static parse(text: string): NeonDate | null;
|
|
38
|
+
/** Converts the date to an instant; a local date needs the time zone offset in minutes or 'UTC'. */
|
|
39
|
+
toDate(zone?: 'UTC' | number): Date;
|
|
40
|
+
/** The date as the encoder writes it, without the fraction of a second. */
|
|
41
|
+
toString(): string;
|
|
42
|
+
/** Equality of components, including the presence of an offset and the written fraction. */
|
|
43
|
+
equals(other: NeonDate): boolean;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Equality of values between a value of the host and a decoded one. A number and a bigint of the
|
|
47
|
+
* same integer are equal; a plain object does not keep the order of integer-like keys, so with one the
|
|
48
|
+
* order does not count.
|
|
49
|
+
*/
|
|
50
|
+
export declare function valuesEqual(a: unknown, b: unknown): boolean;
|
|
51
|
+
/** Entries of an array of the host with canonical keys, whether they make a list, and whether their order is kept. */
|
|
52
|
+
export declare function entriesOf(value: unknown): {
|
|
53
|
+
entries: [string, unknown][];
|
|
54
|
+
list: boolean;
|
|
55
|
+
ordered: boolean;
|
|
56
|
+
} | null;
|
|
57
|
+
declare const Int64Max: bigint;
|
|
58
|
+
/** A number literal as a number or a bigint, or undefined if the text is not a number. */
|
|
59
|
+
export declare function evaluateNumber(text: string): number | bigint | undefined;
|
|
60
|
+
/** Value of a literal that is not a key. */
|
|
61
|
+
export declare function evaluateLiteral(text: string): NeonValue;
|
|
62
|
+
/** Canonical key of a key literal or a string key. */
|
|
63
|
+
export declare function canonicalKey(value: string | number | bigint, isFloat?: boolean): string;
|
|
64
|
+
/** Canonical key of a literal written as a key. */
|
|
65
|
+
export declare function literalKey(text: string): string;
|
|
66
|
+
/** Whether a canonical key is an integer key: what PHP converts to an integer array key. */
|
|
67
|
+
export declare function isIntegerKey(key: string): boolean;
|
|
68
|
+
export { Int64Max };
|
|
69
|
+
/** How deep arrays nest (a block, an inline array and attributes each count as one level). */
|
|
70
|
+
export declare const MaxDepth = 500;
|
|
71
|
+
/** Value of a string token. Throws EscapeError for an invalid escape sequence. */
|
|
72
|
+
export declare function evaluateString(text: string): string;
|
|
73
|
+
/** An invalid escape sequence; the parser reports it at the position of the string. */
|
|
74
|
+
export declare class EscapeError extends Error {
|
|
75
|
+
}
|
package/dist/values.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/** The value of an entity, `value(attributes)`; a chain is an entity whose value is Chain. */
|
|
2
|
+
export class Entity {
|
|
3
|
+
value;
|
|
4
|
+
attributes;
|
|
5
|
+
constructor(value, attributes = []) {
|
|
6
|
+
this.value = value;
|
|
7
|
+
this.attributes = attributes;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** The value of an entity that represents a chain; its attributes are the links. */
|
|
11
|
+
export const Chain = '!!chain';
|
|
12
|
+
/** A date as written: components and an offset, or none for a local date. */
|
|
13
|
+
export class NeonDate {
|
|
14
|
+
year;
|
|
15
|
+
month;
|
|
16
|
+
day;
|
|
17
|
+
hour;
|
|
18
|
+
minute;
|
|
19
|
+
second;
|
|
20
|
+
fraction;
|
|
21
|
+
offset;
|
|
22
|
+
constructor(year, month, day, hour = null, minute = null, second = null,
|
|
23
|
+
/** Digits of the fraction of a second as written, without the point. */
|
|
24
|
+
fraction = null,
|
|
25
|
+
/** `+HH:MM` or `-HH:MM`, or null for a local date. */
|
|
26
|
+
offset = null) {
|
|
27
|
+
this.year = year;
|
|
28
|
+
this.month = month;
|
|
29
|
+
this.day = day;
|
|
30
|
+
this.hour = hour;
|
|
31
|
+
this.minute = minute;
|
|
32
|
+
this.second = second;
|
|
33
|
+
this.fraction = fraction;
|
|
34
|
+
this.offset = offset;
|
|
35
|
+
const time = [hour, minute, second];
|
|
36
|
+
if (!inRange(year, 0, 9999) || !inRange(month, 1, 12) || !inRange(day, 1, daysInMonth(year, month))
|
|
37
|
+
|| (hour === null
|
|
38
|
+
? time.some((n) => n !== null) || fraction !== null || offset !== null
|
|
39
|
+
: !inRange(hour, 0, 23) || !inRange(minute, 0, 59) || !inRange(second, 0, 59))
|
|
40
|
+
|| (fraction !== null && !/^\d+$/.test(fraction))
|
|
41
|
+
|| (offset !== null && !/^[-+](?:(?:0\d|1[0-3]):[0-5]\d|14:00)$/.test(offset))) {
|
|
42
|
+
throw new RangeError('A component of the date is out of range.');
|
|
43
|
+
}
|
|
44
|
+
Object.freeze(this);
|
|
45
|
+
}
|
|
46
|
+
/** An instant as a date in UTC. */
|
|
47
|
+
static fromDate(date) {
|
|
48
|
+
const ms = date.getUTCMilliseconds();
|
|
49
|
+
return new NeonDate(date.getUTCFullYear(), date.getUTCMonth() + 1, date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds(), ms ? String(ms).padStart(3, '0').replace(/0+$/, '') : null, '+00:00');
|
|
50
|
+
}
|
|
51
|
+
/** Parses the text of a date literal; null if the text is not one, its components making no valid date included. */
|
|
52
|
+
static parse(text) {
|
|
53
|
+
const m = DatePattern.exec(text);
|
|
54
|
+
if (!m) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const [, year, month, day, hour, minute, second, fraction, zone] = m;
|
|
58
|
+
const num = (s) => s === undefined ? null : Number(s);
|
|
59
|
+
let offset = null;
|
|
60
|
+
if (zone === 'Z') {
|
|
61
|
+
offset = '+00:00';
|
|
62
|
+
}
|
|
63
|
+
else if (zone) {
|
|
64
|
+
const z = /^([-+])(\d\d?)(?::?(\d\d))?$/.exec(zone);
|
|
65
|
+
offset = z[1] + z[2].padStart(2, '0') + ':' + (z[3] ?? '00');
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return new NeonDate(Number(year), Number(month), Number(day), num(hour), num(minute), num(second), fraction || null, offset);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
if (e instanceof RangeError) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Converts the date to an instant; a local date needs the time zone offset in minutes or 'UTC'. */
|
|
78
|
+
toDate(zone) {
|
|
79
|
+
let offset;
|
|
80
|
+
if (this.offset) {
|
|
81
|
+
const sign = this.offset[0] === '-' ? -1 : 1;
|
|
82
|
+
offset = sign * (Number(this.offset.slice(1, 3)) * 60 + Number(this.offset.slice(4, 6)));
|
|
83
|
+
}
|
|
84
|
+
else if (zone === undefined) {
|
|
85
|
+
throw new TypeError('A local date needs a time zone to become an instant.');
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
offset = zone === 'UTC' ? 0 : zone;
|
|
89
|
+
}
|
|
90
|
+
const ms = Number((this.fraction ?? '').padEnd(3, '0').slice(0, 3));
|
|
91
|
+
const res = new Date(0);
|
|
92
|
+
res.setUTCFullYear(this.year, this.month - 1, this.day); // Date.UTC() would map years 0-99 to 1900-1999
|
|
93
|
+
res.setUTCHours(this.hour ?? 0, (this.minute ?? 0) - offset, this.second ?? 0, ms);
|
|
94
|
+
return res;
|
|
95
|
+
}
|
|
96
|
+
/** The date as the encoder writes it, without the fraction of a second. */
|
|
97
|
+
toString() {
|
|
98
|
+
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
99
|
+
let res = `${pad(this.year, 4)}-${pad(this.month)}-${pad(this.day)}`;
|
|
100
|
+
if (this.hour !== null) {
|
|
101
|
+
res += ` ${pad(this.hour)}:${pad(this.minute)}:${pad(this.second)}`;
|
|
102
|
+
}
|
|
103
|
+
if (this.offset) {
|
|
104
|
+
res += ' ' + this.offset.replace(':', '');
|
|
105
|
+
}
|
|
106
|
+
return res;
|
|
107
|
+
}
|
|
108
|
+
/** Equality of components, including the presence of an offset and the written fraction. */
|
|
109
|
+
equals(other) {
|
|
110
|
+
return this.year === other.year && this.month === other.month && this.day === other.day
|
|
111
|
+
&& this.hour === other.hour && this.minute === other.minute && this.second === other.second
|
|
112
|
+
&& this.fraction === other.fraction && this.offset === other.offset;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function daysInMonth(year, month) {
|
|
116
|
+
return month === 2
|
|
117
|
+
? (year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28)
|
|
118
|
+
: [4, 6, 9, 11].includes(month) ? 30 : 31;
|
|
119
|
+
}
|
|
120
|
+
function inRange(n, min, max) {
|
|
121
|
+
return Number.isInteger(n) && n >= min && n <= max;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Equality of values between a value of the host and a decoded one. A number and a bigint of the
|
|
125
|
+
* same integer are equal; a plain object does not keep the order of integer-like keys, so with one the
|
|
126
|
+
* order does not count.
|
|
127
|
+
*/
|
|
128
|
+
export function valuesEqual(a, b) {
|
|
129
|
+
if (typeof b === 'number' || typeof b === 'bigint') {
|
|
130
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
131
|
+
return a === b || (Number.isNaN(a) && Number.isNaN(b));
|
|
132
|
+
}
|
|
133
|
+
const [n, big] = typeof a === 'number' ? [a, b] : [b, a];
|
|
134
|
+
return typeof n === 'number' ? typeof big === 'bigint' && Number.isInteger(n) && BigInt(n) === big : n === big;
|
|
135
|
+
}
|
|
136
|
+
else if (b instanceof NeonDate) {
|
|
137
|
+
let date = a;
|
|
138
|
+
try {
|
|
139
|
+
date = a instanceof Date ? NeonDate.fromDate(a) : a;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// a Date out of the range of the model equals no date
|
|
143
|
+
}
|
|
144
|
+
return date instanceof NeonDate && b.equals(date);
|
|
145
|
+
}
|
|
146
|
+
else if (b instanceof Entity) {
|
|
147
|
+
return a instanceof Entity && valuesEqual(a.value, b.value) && valuesEqual(a.attributes, b.attributes);
|
|
148
|
+
}
|
|
149
|
+
else if (typeof b === 'object' && b !== null) {
|
|
150
|
+
const x = entriesOf(a);
|
|
151
|
+
const y = entriesOf(b);
|
|
152
|
+
if (!x || !y || x.list !== y.list || x.entries.length !== y.entries.length) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
else if (x.ordered && y.ordered) {
|
|
156
|
+
return x.entries.every(([key, v], i) => y.entries[i][0] === key && valuesEqual(v, y.entries[i][1]));
|
|
157
|
+
}
|
|
158
|
+
const map = new Map(y.entries);
|
|
159
|
+
return x.entries.every(([key, v]) => map.has(key) && valuesEqual(v, map.get(key)));
|
|
160
|
+
}
|
|
161
|
+
return a === b;
|
|
162
|
+
}
|
|
163
|
+
/** Entries of an array of the host with canonical keys, whether they make a list, and whether their order is kept. */
|
|
164
|
+
export function entriesOf(value) {
|
|
165
|
+
let entries;
|
|
166
|
+
if (Array.isArray(value)) {
|
|
167
|
+
return { entries: value.map((v, i) => [String(i), v]), list: true, ordered: true };
|
|
168
|
+
}
|
|
169
|
+
else if (value instanceof Map) {
|
|
170
|
+
entries = [...value].map(([key, v]) => [String(key), v]);
|
|
171
|
+
}
|
|
172
|
+
else if (typeof value === 'object' && value !== null && [Object.prototype, null].includes(Object.getPrototypeOf(value))) {
|
|
173
|
+
entries = Object.entries(value);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return { entries, list: entries.length > 0 && entries.every(([key], i) => key === String(i)), ordered: value instanceof Map };
|
|
179
|
+
}
|
|
180
|
+
const DatePattern = /^(\d{4})-(\d\d?)-(\d\d?)(?:(?:[Tt]| +)(\d\d?):(\d\d):(\d\d)(?:\.(\d*))? *(Z|[-+]\d\d?(?::?\d\d)?)?)?$/;
|
|
181
|
+
const SimpleTypes = {
|
|
182
|
+
true: true, True: true, TRUE: true, yes: true, Yes: true, YES: true,
|
|
183
|
+
false: false, False: false, FALSE: false, no: false, No: false, NO: false,
|
|
184
|
+
null: null, Null: null, NULL: null,
|
|
185
|
+
};
|
|
186
|
+
const DecimalPattern = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
|
|
187
|
+
const IntegerPattern = /^[+-]?\d+$/;
|
|
188
|
+
const BasePattern = /^0(?:x[0-9a-fA-F]+|o[0-7]+|b[01]+)$/;
|
|
189
|
+
const MaxSafe = BigInt(Number.MAX_SAFE_INTEGER);
|
|
190
|
+
const Int64Max = 2n ** 63n - 1n;
|
|
191
|
+
const Int64Min = -(2n ** 63n);
|
|
192
|
+
/** A number literal as a number or a bigint, or undefined if the text is not a number. */
|
|
193
|
+
export function evaluateNumber(text) {
|
|
194
|
+
if (IntegerPattern.test(text) || BasePattern.test(text)) {
|
|
195
|
+
return integer(BigInt(text.replace(/^\+/, '')));
|
|
196
|
+
}
|
|
197
|
+
else if (DecimalPattern.test(text)) {
|
|
198
|
+
return Number(text);
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
/** Value of a literal that is not a key. */
|
|
203
|
+
export function evaluateLiteral(text) {
|
|
204
|
+
if (Object.hasOwn(SimpleTypes, text)) {
|
|
205
|
+
return SimpleTypes[text];
|
|
206
|
+
}
|
|
207
|
+
return evaluateNumber(text) ?? NeonDate.parse(text) ?? text;
|
|
208
|
+
}
|
|
209
|
+
/** Canonical key of a key literal or a string key. */
|
|
210
|
+
export function canonicalKey(value, isFloat = false) {
|
|
211
|
+
if (typeof value === 'string') {
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
else if (typeof value === 'bigint' || (!isFloat && Number.isInteger(value))) {
|
|
215
|
+
return String(BigInt(value));
|
|
216
|
+
}
|
|
217
|
+
return floatKey(value);
|
|
218
|
+
}
|
|
219
|
+
/** Canonical key of a literal written as a key. */
|
|
220
|
+
export function literalKey(text) {
|
|
221
|
+
const num = evaluateNumber(text);
|
|
222
|
+
if (num === undefined) {
|
|
223
|
+
return text;
|
|
224
|
+
}
|
|
225
|
+
return canonicalKey(num, typeof num === 'number' && !IntegerPattern.test(text) && !BasePattern.test(text));
|
|
226
|
+
}
|
|
227
|
+
/** A float as PHP converts it to a string: 14 significant digits. */
|
|
228
|
+
function floatKey(f) {
|
|
229
|
+
if (Number.isNaN(f)) {
|
|
230
|
+
return 'NAN';
|
|
231
|
+
}
|
|
232
|
+
else if (!Number.isFinite(f)) {
|
|
233
|
+
return f > 0 ? 'INF' : '-INF';
|
|
234
|
+
}
|
|
235
|
+
else if (f === 0) {
|
|
236
|
+
return Object.is(f, -0) ? '-0' : '0';
|
|
237
|
+
}
|
|
238
|
+
const [mantissa, exponent] = f.toExponential(13).split('e');
|
|
239
|
+
const e = Number(exponent);
|
|
240
|
+
if (e >= -4 && e < 14) { // toPrecision() is positional in this range
|
|
241
|
+
const digits = f.toPrecision(14);
|
|
242
|
+
return digits.includes('.') ? digits.replace(/\.?0+$/, '') : digits;
|
|
243
|
+
}
|
|
244
|
+
let m = mantissa.replace(/0+$/, '').replace(/\.$/, '');
|
|
245
|
+
if (!m.includes('.')) {
|
|
246
|
+
m += '.0';
|
|
247
|
+
}
|
|
248
|
+
return `${m}E${e < 0 ? '-' : '+'}${Math.abs(e)}`;
|
|
249
|
+
}
|
|
250
|
+
/** Whether a canonical key is an integer key: what PHP converts to an integer array key. */
|
|
251
|
+
export function isIntegerKey(key) {
|
|
252
|
+
if (!/^(?:0|-?[1-9]\d*)$/.test(key)) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
const n = BigInt(key);
|
|
256
|
+
return n >= Int64Min && n <= Int64Max;
|
|
257
|
+
}
|
|
258
|
+
export { Int64Max };
|
|
259
|
+
/** How deep arrays nest (a block, an inline array and attributes each count as one level). */
|
|
260
|
+
export const MaxDepth = 500;
|
|
261
|
+
function integer(n) {
|
|
262
|
+
return n <= MaxSafe && n >= -MaxSafe ? Number(n) : n;
|
|
263
|
+
}
|
|
264
|
+
const Escapes = {
|
|
265
|
+
't': '\t', 'n': '\n', 'r': '\r', 'f': '\f', 'b': '\b', '"': '"', '\\': '\\', '/': '/', '_': String.fromCharCode(0xA0),
|
|
266
|
+
};
|
|
267
|
+
/** Value of a string token. Throws EscapeError for an invalid escape sequence. */
|
|
268
|
+
export function evaluateString(text) {
|
|
269
|
+
text = text.replaceAll('\r', '');
|
|
270
|
+
let res;
|
|
271
|
+
const multiline = /^('''|""")\n/.test(text);
|
|
272
|
+
if (multiline) {
|
|
273
|
+
const indent = /^...\n+([\t ]*)/.exec(text)[1];
|
|
274
|
+
res = text.slice(3, -3);
|
|
275
|
+
if (indent !== '') {
|
|
276
|
+
res = res.replaceAll('\n' + indent, '\n');
|
|
277
|
+
}
|
|
278
|
+
res = res.replace(/^\n/, '').replace(/\n[\t ]*$/, '');
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
res = text.slice(1, -1);
|
|
282
|
+
if (text[0] === '\'') {
|
|
283
|
+
res = res.replaceAll('\'\'', '\'');
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (text[0] === '\'') {
|
|
287
|
+
return res;
|
|
288
|
+
}
|
|
289
|
+
// the pattern of nette/neon 3.x, case-insensitive, so \U0041 and \uD801\UDC01 are one sequence each
|
|
290
|
+
return res.replace(/\\(?:ud[89ab][0-9a-f]{2}\\ud[c-f][0-9a-f]{2}|u[0-9a-f]{4}|[^\n])/gi, (seq) => {
|
|
291
|
+
const c = seq[1];
|
|
292
|
+
if (Object.hasOwn(Escapes, c)) {
|
|
293
|
+
return Escapes[c];
|
|
294
|
+
}
|
|
295
|
+
else if (c !== 'u' || seq.length < 6) {
|
|
296
|
+
throw new EscapeError(`Invalid escaping sequence ${seq}`);
|
|
297
|
+
}
|
|
298
|
+
else if (seq.length === 12) {
|
|
299
|
+
if (seq[7] !== 'u') {
|
|
300
|
+
throw new EscapeError(`Invalid UTF-8 sequence ${seq}`);
|
|
301
|
+
}
|
|
302
|
+
return String.fromCharCode(parseInt(seq.slice(2, 6), 16), parseInt(seq.slice(8), 16));
|
|
303
|
+
}
|
|
304
|
+
const code = parseInt(seq.slice(2), 16);
|
|
305
|
+
if (code >= 0xD800 && code <= 0xDFFF) {
|
|
306
|
+
throw new EscapeError(`Invalid UTF-8 sequence ${seq}`);
|
|
307
|
+
}
|
|
308
|
+
return String.fromCharCode(code);
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
/** An invalid escape sequence; the parser reports it at the position of the string. */
|
|
312
|
+
export class EscapeError extends Error {
|
|
313
|
+
}
|
package/license.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
===========
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 David Grudl (https://davidgrudl.com)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
|
7
|
+
software and associated documentation files (the "Software"), to deal in the Software
|
|
8
|
+
without restriction, including without limitation the rights to use, copy, modify,
|
|
9
|
+
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to the following
|
|
11
|
+
conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all copies
|
|
14
|
+
or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
17
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
18
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
|
|
20
|
+
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
|
|
21
|
+
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@nette/neon",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "NEON parser, encoder and lossless editor: change a value in a config file and keep its comments and formatting.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"neon",
|
|
7
|
+
"nette",
|
|
8
|
+
"parser",
|
|
9
|
+
"config",
|
|
10
|
+
"format-preserving",
|
|
11
|
+
"round-trip",
|
|
12
|
+
"yaml-alternative",
|
|
13
|
+
"cst"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"author": "David Grudl",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/nette/neon-js.git"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://neon.nette.org",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"import": "./dist/index.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"bin": {
|
|
31
|
+
"neon-lint": "bin/neon-lint.js"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"bin"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=22.0.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc -p tsconfig.build.json",
|
|
42
|
+
"prepare": "npm run build",
|
|
43
|
+
"spec": "node tests/fetch-spec.mjs",
|
|
44
|
+
"test": "tsc --noEmit && tsc --noEmit -p tsconfig.core.json && vitest run",
|
|
45
|
+
"smoke": "npm run build && node tests/package-smoke.mjs",
|
|
46
|
+
"lint": "eslint --cache",
|
|
47
|
+
"lint:fix": "eslint --cache --fix",
|
|
48
|
+
"verify-examples": "tsx examples/verify.ts"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@nette/eslint-plugin": "^0.3.1",
|
|
52
|
+
"@types/node": "^22.20.5",
|
|
53
|
+
"eslint": "^10.12.0",
|
|
54
|
+
"globals": "^17.13.0",
|
|
55
|
+
"tsx": "^4.23.15",
|
|
56
|
+
"typescript": "~6.0.3",
|
|
57
|
+
"typescript-eslint": "^8.71.0",
|
|
58
|
+
"vitest": "^5.0.3"
|
|
59
|
+
}
|
|
60
|
+
}
|