@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
package/dist/scanner.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/** Reads tokens and trivia one by one. Returns null on an error, with errorAt set. */
|
|
2
|
+
export class Scanner {
|
|
3
|
+
s;
|
|
4
|
+
#pos = 0;
|
|
5
|
+
errorAt = -1;
|
|
6
|
+
constructor(s) {
|
|
7
|
+
this.s = s;
|
|
8
|
+
}
|
|
9
|
+
get position() {
|
|
10
|
+
return this.#pos;
|
|
11
|
+
}
|
|
12
|
+
next() {
|
|
13
|
+
const s = this.s;
|
|
14
|
+
const start = this.#pos;
|
|
15
|
+
const c = s[start];
|
|
16
|
+
if (c === undefined) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
let kind;
|
|
20
|
+
let triviaKind;
|
|
21
|
+
let end;
|
|
22
|
+
if (start === 0 && s.charCodeAt(0) === 0xFEFF) {
|
|
23
|
+
kind = 'Trivia';
|
|
24
|
+
triviaKind = 'ByteOrderMark';
|
|
25
|
+
end = 1;
|
|
26
|
+
}
|
|
27
|
+
else if (c === '\'' || c === '"') {
|
|
28
|
+
kind = 'String';
|
|
29
|
+
end = s.startsWith(c + c + c, start) && this.isLineEnding(start + 3)
|
|
30
|
+
? this.scanMultiline(start, c + c + c)
|
|
31
|
+
: -1;
|
|
32
|
+
if (end < 0) { // a triple without a closing line is read as single-line quotes
|
|
33
|
+
end = c === '\'' ? this.scanSingleQuoted(start) : this.scanDoubleQuoted(start);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
else if (c === '#') {
|
|
37
|
+
kind = 'Trivia';
|
|
38
|
+
triviaKind = 'Comment';
|
|
39
|
+
end = start + 1;
|
|
40
|
+
while (end < s.length && !this.isLineEnding(end)) {
|
|
41
|
+
end++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else if (this.isLineEnding(start)) {
|
|
45
|
+
kind = 'Trivia';
|
|
46
|
+
triviaKind = 'LineEnding';
|
|
47
|
+
end = start + (c === '\r' ? 2 : 1);
|
|
48
|
+
}
|
|
49
|
+
else if (this.isSpace(start)) {
|
|
50
|
+
kind = 'Trivia';
|
|
51
|
+
triviaKind = 'Whitespace';
|
|
52
|
+
end = start + 1;
|
|
53
|
+
while (this.isSpace(end)) {
|
|
54
|
+
end++;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else if (c in Punctuation) {
|
|
58
|
+
kind = Punctuation[c];
|
|
59
|
+
end = start + 1;
|
|
60
|
+
}
|
|
61
|
+
else if (c === ':' || c === '-') {
|
|
62
|
+
const next = s[start + 1];
|
|
63
|
+
const prev = s[start - 1];
|
|
64
|
+
if (next !== undefined && !this.isAny(start + 1, '"\',=[]{}()') && prev !== '"' && prev !== '\'') {
|
|
65
|
+
kind = 'Literal';
|
|
66
|
+
end = this.scanLiteral(start + 2); // the next character belongs to the start of the literal
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
kind = c === ':' ? 'Colon' : 'Dash';
|
|
70
|
+
end = start + 1;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (!this.isAny(start, '#"\',:=[]{}()`')) {
|
|
74
|
+
kind = 'Literal';
|
|
75
|
+
end = this.scanLiteral(start + 1);
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
end = -1;
|
|
79
|
+
kind = 'Trivia';
|
|
80
|
+
}
|
|
81
|
+
if (end < 0) {
|
|
82
|
+
this.errorAt = start;
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
this.#pos = end;
|
|
86
|
+
return { kind, triviaKind, start, end };
|
|
87
|
+
}
|
|
88
|
+
/** Is the character one of the given ones, a space, a tab, or a line ending? */
|
|
89
|
+
isAny(i, chars) {
|
|
90
|
+
const c = this.s[i];
|
|
91
|
+
return c === undefined || c === ' ' || c === '\t' || c === '\n' || c === '\r' || chars.includes(c);
|
|
92
|
+
}
|
|
93
|
+
isSpace(i) {
|
|
94
|
+
const c = this.s[i];
|
|
95
|
+
return c === ' ' || c === '\t' || (c === '\r' && this.s[i + 1] !== '\n');
|
|
96
|
+
}
|
|
97
|
+
isLineEnding(i) {
|
|
98
|
+
const c = this.s[i];
|
|
99
|
+
return c === '\n' || (c === '\r' && this.s[i + 1] === '\n');
|
|
100
|
+
}
|
|
101
|
+
scanLiteral(i) {
|
|
102
|
+
const s = this.s;
|
|
103
|
+
while (true) {
|
|
104
|
+
if (i < s.length && !this.isAny(i, ',:=]})(')) {
|
|
105
|
+
i++;
|
|
106
|
+
while (i < s.length && !this.isAny(i, ',:=]})(')) {
|
|
107
|
+
i++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
else if (s[i] === ':' && i + 1 < s.length && !this.isAny(i + 1, ',]})')) {
|
|
111
|
+
i++;
|
|
112
|
+
}
|
|
113
|
+
else if (this.isSpace(i)) {
|
|
114
|
+
let j = i + 1;
|
|
115
|
+
while (this.isSpace(j)) {
|
|
116
|
+
j++;
|
|
117
|
+
}
|
|
118
|
+
if (j < s.length && !this.isAny(j, '#,:=]})(')) {
|
|
119
|
+
i = j;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
return i;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
return i;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
scanSingleQuoted(start) {
|
|
131
|
+
const s = this.s;
|
|
132
|
+
let i = start + 1;
|
|
133
|
+
while (i < s.length) {
|
|
134
|
+
if (s[i] === '\'' && s[i + 1] === '\'') {
|
|
135
|
+
i += 2;
|
|
136
|
+
}
|
|
137
|
+
else if (s[i] === '\'' || s[i] === '\n') {
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
i++;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return s[i] === '\'' ? i + 1 : -1;
|
|
145
|
+
}
|
|
146
|
+
scanDoubleQuoted(start) {
|
|
147
|
+
const s = this.s;
|
|
148
|
+
let i = start + 1;
|
|
149
|
+
while (i < s.length) {
|
|
150
|
+
if (s[i] === '\\' && i + 1 < s.length && !this.isLineEnding(i + 1)) {
|
|
151
|
+
i += 2;
|
|
152
|
+
}
|
|
153
|
+
else if (s[i] === '"' || s[i] === '\\' || s[i] === '\n') {
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
i++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return s[i] === '"' ? i + 1 : -1;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Content runs to the first line after the first one that starts with optional spaces and the
|
|
164
|
+
* closing triple; only without such a line may the first line itself close an empty string.
|
|
165
|
+
*/
|
|
166
|
+
scanMultiline(start, triple) {
|
|
167
|
+
const s = this.s;
|
|
168
|
+
const first = s.indexOf('\n', start) + 1;
|
|
169
|
+
for (let i = s.indexOf('\n', first); i >= 0; i = s.indexOf('\n', i + 1)) {
|
|
170
|
+
const end = this.closingAt(i + 1, triple);
|
|
171
|
+
if (end >= 0) {
|
|
172
|
+
return end;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return this.closingAt(first, triple);
|
|
176
|
+
}
|
|
177
|
+
closingAt(i, triple) {
|
|
178
|
+
while (this.isSpace(i)) {
|
|
179
|
+
i++;
|
|
180
|
+
}
|
|
181
|
+
return this.s.startsWith(triple, i) ? i + 3 : -1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const Punctuation = {
|
|
185
|
+
',': 'Comma',
|
|
186
|
+
'=': 'Equals',
|
|
187
|
+
'[': 'OpenBracket',
|
|
188
|
+
']': 'CloseBracket',
|
|
189
|
+
'{': 'OpenBrace',
|
|
190
|
+
'}': 'CloseBrace',
|
|
191
|
+
'(': 'OpenParen',
|
|
192
|
+
')': 'CloseParen',
|
|
193
|
+
};
|
package/dist/token.d.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { forgetOrigin, setLeading, setParent, setTrailing } from './internal.ts';
|
|
2
|
+
import { type Node } from './nodes.ts';
|
|
3
|
+
export type TokenKind = 'String' | 'Literal' | 'Colon' | 'Equals' | 'Comma' | 'Dash' | 'OpenBracket' | 'CloseBracket' | 'OpenBrace' | 'CloseBrace' | 'OpenParen' | 'CloseParen' | 'EndOfFile';
|
|
4
|
+
export type TriviaKind = 'Whitespace' | 'LineEnding' | 'Comment' | 'ByteOrderMark';
|
|
5
|
+
/** Line and column are 1-based, offset 0-based; all in code points. utf16Offset indexes the JavaScript string. */
|
|
6
|
+
export interface Position {
|
|
7
|
+
readonly line: number;
|
|
8
|
+
readonly column: number;
|
|
9
|
+
readonly offset: number;
|
|
10
|
+
readonly utf16Offset: number;
|
|
11
|
+
}
|
|
12
|
+
/** Whitespace, a line ending, a comment or a byte order mark around a token. */
|
|
13
|
+
export declare class Trivia {
|
|
14
|
+
readonly kind: TriviaKind;
|
|
15
|
+
readonly text: string;
|
|
16
|
+
constructor(kind: TriviaKind, text: string);
|
|
17
|
+
static fromText(text: string): Trivia;
|
|
18
|
+
withText(text: string): Trivia;
|
|
19
|
+
is(...kinds: TriviaKind[]): boolean;
|
|
20
|
+
/** Text of a comment without # and trimmed, or empty string for other trivia. */
|
|
21
|
+
getCommentText(): string;
|
|
22
|
+
}
|
|
23
|
+
/** A token of the source with the trivia around it. */
|
|
24
|
+
export declare class Token {
|
|
25
|
+
#private;
|
|
26
|
+
/** Prefer Token.fromText(); the text must be one token of the kind and the trivia canonical. */
|
|
27
|
+
constructor(kind: TokenKind, text: string, leadingTrivia?: readonly Trivia[], trailingTrivia?: readonly Trivia[], origin?: Position | null);
|
|
28
|
+
/** Makes a token of the kind the scanner reads the text as. */
|
|
29
|
+
static fromText(text: string): Token;
|
|
30
|
+
/** A token changes its kind only by being replaced. */
|
|
31
|
+
get kind(): TokenKind;
|
|
32
|
+
/** Where the token stood in the source it was read from; null for a token made otherwise. A clone keeps it. */
|
|
33
|
+
get origin(): Position | null;
|
|
34
|
+
get text(): string;
|
|
35
|
+
get leadingTrivia(): readonly Trivia[];
|
|
36
|
+
get trailingTrivia(): readonly Trivia[];
|
|
37
|
+
get parent(): Node | null;
|
|
38
|
+
/** Replaces the text by another one of the same kind. */
|
|
39
|
+
setText(text: string): this;
|
|
40
|
+
/** Sets the leading trivia; in a document also checks them against the token before. */
|
|
41
|
+
setLeadingTrivia(trivia: readonly Trivia[]): this;
|
|
42
|
+
/** Sets the trailing trivia; in a document also checks them against the token after. */
|
|
43
|
+
setTrailingTrivia(trivia: readonly Trivia[]): this;
|
|
44
|
+
/** Replaces a trivia, found by identity, in the leading or trailing trivia. */
|
|
45
|
+
replaceTrivia(old: Trivia, replacement: Trivia): this;
|
|
46
|
+
is(...kinds: TokenKind[]): boolean;
|
|
47
|
+
/** A detached copy with the same text, trivia and origin. */
|
|
48
|
+
clone(): Token;
|
|
49
|
+
/** The current position, computed by a walk from the root; null in a detached subtree. */
|
|
50
|
+
getPosition(): Position | null;
|
|
51
|
+
/** The source text of the token with its trivia. */
|
|
52
|
+
toString(): string;
|
|
53
|
+
/** @internal Sets the leading trivia without the checks of the place, for an edit that moves trivia in steps. */
|
|
54
|
+
[setLeading](trivia: readonly Trivia[]): this;
|
|
55
|
+
/** @internal */
|
|
56
|
+
[setTrailing](trivia: readonly Trivia[]): this;
|
|
57
|
+
/** @internal For a token made from a text that is not the source of the document. */
|
|
58
|
+
[forgetOrigin](): void;
|
|
59
|
+
/** @internal */
|
|
60
|
+
[setParent](parent: Node | null): void;
|
|
61
|
+
}
|
|
62
|
+
/** @internal A token whose text and trivia are known to be valid: one the lexer reads or a clone. */
|
|
63
|
+
export declare function trustedToken(kind: TokenKind, text: string, leading: readonly Trivia[], trailing: readonly Trivia[], origin: Position | null): Token;
|
|
64
|
+
/** The token of the tree right before or after one. */
|
|
65
|
+
export declare function findNeighbour(token: Token, backward: boolean): Token | null;
|
|
66
|
+
/** The tokens of the tree before or after one, the nearest first, read lazily from the nodes around it. */
|
|
67
|
+
export declare function iterateNeighbours(token: Token, backward: boolean): Generator<Token>;
|
package/dist/token.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { forgetOrigin, record, setLeading, setParent, setTrailing } from "./internal.js";
|
|
2
|
+
import { Scanner } from "./scanner.js";
|
|
3
|
+
import { DocumentNode } from "./nodes.js";
|
|
4
|
+
import { Cursor } from "./position.js";
|
|
5
|
+
/** Whitespace, a line ending, a comment or a byte order mark around a token. */
|
|
6
|
+
export class Trivia {
|
|
7
|
+
kind;
|
|
8
|
+
text;
|
|
9
|
+
constructor(kind, text) {
|
|
10
|
+
this.kind = kind;
|
|
11
|
+
this.text = text;
|
|
12
|
+
if (classifyTrivia(text) !== kind) {
|
|
13
|
+
throw new TypeError(`${JSON.stringify(text)} is not ${kind}.`);
|
|
14
|
+
}
|
|
15
|
+
Object.freeze(this);
|
|
16
|
+
}
|
|
17
|
+
static fromText(text) {
|
|
18
|
+
const kind = classifyTrivia(text);
|
|
19
|
+
if (!kind) {
|
|
20
|
+
throw new TypeError(`${JSON.stringify(text)} is not one trivia.`);
|
|
21
|
+
}
|
|
22
|
+
return new Trivia(kind, text);
|
|
23
|
+
}
|
|
24
|
+
withText(text) {
|
|
25
|
+
const trivia = Trivia.fromText(text);
|
|
26
|
+
if (trivia.kind !== this.kind) {
|
|
27
|
+
throw new TypeError(`${JSON.stringify(text)} is not ${this.kind}.`);
|
|
28
|
+
}
|
|
29
|
+
return trivia;
|
|
30
|
+
}
|
|
31
|
+
is(...kinds) {
|
|
32
|
+
return kinds.includes(this.kind);
|
|
33
|
+
}
|
|
34
|
+
/** Text of a comment without # and trimmed, or empty string for other trivia. */
|
|
35
|
+
getCommentText() {
|
|
36
|
+
return this.kind === 'Comment' ? this.text.slice(1).trim() : '';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function classifyTrivia(text) {
|
|
40
|
+
if (/^(?:[ \t]|\r(?!\n))+$/.test(text)) {
|
|
41
|
+
return 'Whitespace';
|
|
42
|
+
}
|
|
43
|
+
else if (text === '\n' || text === '\r\n') {
|
|
44
|
+
return 'LineEnding';
|
|
45
|
+
}
|
|
46
|
+
else if (/^#[^\n]*$/.test(text) && text.isWellFormed()) {
|
|
47
|
+
return 'Comment';
|
|
48
|
+
}
|
|
49
|
+
else if (text === String.fromCharCode(0xFEFF)) {
|
|
50
|
+
return 'ByteOrderMark';
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/** A token of the source with the trivia around it. */
|
|
55
|
+
export class Token {
|
|
56
|
+
#kind;
|
|
57
|
+
#origin;
|
|
58
|
+
#text;
|
|
59
|
+
#leadingTrivia;
|
|
60
|
+
#trailingTrivia;
|
|
61
|
+
#parent = null;
|
|
62
|
+
/** Prefer Token.fromText(); the text must be one token of the kind and the trivia canonical. */
|
|
63
|
+
constructor(kind, text, leadingTrivia = [], trailingTrivia = [], origin = null) {
|
|
64
|
+
if (!trusted) {
|
|
65
|
+
checkText(kind, text);
|
|
66
|
+
checkTrivia(leadingTrivia, 'leading', kind === 'EndOfFile');
|
|
67
|
+
checkTrivia(trailingTrivia, 'trailing', false);
|
|
68
|
+
if (kind === 'EndOfFile' && trailingTrivia.length) {
|
|
69
|
+
throw new TypeError('EndOfFile has no trailing trivia.');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
this.#kind = kind;
|
|
73
|
+
this.#text = text;
|
|
74
|
+
this.#leadingTrivia = Object.freeze([...leadingTrivia]);
|
|
75
|
+
this.#trailingTrivia = Object.freeze([...trailingTrivia]);
|
|
76
|
+
this.#origin = origin;
|
|
77
|
+
}
|
|
78
|
+
/** Makes a token of the kind the scanner reads the text as. */
|
|
79
|
+
static fromText(text) {
|
|
80
|
+
const kind = text.isWellFormed() ? scanSingleToken(text) : null;
|
|
81
|
+
if (!kind) {
|
|
82
|
+
throw new TypeError(`${JSON.stringify(text)} is not one token.`);
|
|
83
|
+
}
|
|
84
|
+
return new Token(kind, text);
|
|
85
|
+
}
|
|
86
|
+
/** A token changes its kind only by being replaced. */
|
|
87
|
+
get kind() {
|
|
88
|
+
return this.#kind;
|
|
89
|
+
}
|
|
90
|
+
/** Where the token stood in the source it was read from; null for a token made otherwise. A clone keeps it. */
|
|
91
|
+
get origin() {
|
|
92
|
+
return this.#origin;
|
|
93
|
+
}
|
|
94
|
+
get text() {
|
|
95
|
+
return this.#text;
|
|
96
|
+
}
|
|
97
|
+
get leadingTrivia() {
|
|
98
|
+
return this.#leadingTrivia;
|
|
99
|
+
}
|
|
100
|
+
get trailingTrivia() {
|
|
101
|
+
return this.#trailingTrivia;
|
|
102
|
+
}
|
|
103
|
+
get parent() {
|
|
104
|
+
return this.#parent;
|
|
105
|
+
}
|
|
106
|
+
/** Replaces the text by another one of the same kind. */
|
|
107
|
+
setText(text) {
|
|
108
|
+
checkText(this.#kind, text);
|
|
109
|
+
const old = this.#text;
|
|
110
|
+
record(() => this.#text = old);
|
|
111
|
+
this.#text = text;
|
|
112
|
+
return this;
|
|
113
|
+
}
|
|
114
|
+
/** Sets the leading trivia; in a document also checks them against the token before. */
|
|
115
|
+
setLeadingTrivia(trivia) {
|
|
116
|
+
checkTrivia(trivia, 'leading', this.#kind === 'EndOfFile');
|
|
117
|
+
const [before] = neighbours(this);
|
|
118
|
+
if (before && trivia.length && !before.trailingTrivia.some((t) => t.kind === 'LineEnding')) {
|
|
119
|
+
throw new TypeError('Leading trivia must follow a line ending; on the same line they are the trailing trivia of the token before.');
|
|
120
|
+
}
|
|
121
|
+
else if (before && trivia[0]?.kind === 'ByteOrderMark') {
|
|
122
|
+
throw new TypeError('A byte order mark may only stand at the start of the input.');
|
|
123
|
+
}
|
|
124
|
+
return this[setLeading](trivia);
|
|
125
|
+
}
|
|
126
|
+
/** Sets the trailing trivia; in a document also checks them against the token after. */
|
|
127
|
+
setTrailingTrivia(trivia) {
|
|
128
|
+
checkTrivia(trivia, 'trailing', this.#kind === 'EndOfFile');
|
|
129
|
+
const [, after] = neighbours(this);
|
|
130
|
+
if (after && after.leadingTrivia.length && !trivia.some((t) => t.kind === 'LineEnding')) {
|
|
131
|
+
throw new TypeError('Trailing trivia without a line ending must not be followed by leading trivia.');
|
|
132
|
+
}
|
|
133
|
+
else if (after && after.kind !== 'EndOfFile' && trivia.at(-1)?.kind === 'Comment') {
|
|
134
|
+
throw new TypeError('A comment must be followed by a line ending.');
|
|
135
|
+
}
|
|
136
|
+
return this[setTrailing](trivia);
|
|
137
|
+
}
|
|
138
|
+
/** Replaces a trivia, found by identity, in the leading or trailing trivia. */
|
|
139
|
+
replaceTrivia(old, replacement) {
|
|
140
|
+
const replace = (list) => list.map((trivia) => trivia === old ? replacement : trivia);
|
|
141
|
+
if (this.#leadingTrivia.includes(old)) {
|
|
142
|
+
this.setLeadingTrivia(replace(this.#leadingTrivia));
|
|
143
|
+
}
|
|
144
|
+
else if (this.#trailingTrivia.includes(old)) {
|
|
145
|
+
this.setTrailingTrivia(replace(this.#trailingTrivia));
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
throw new TypeError('The trivia does not belong to the token.');
|
|
149
|
+
}
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
is(...kinds) {
|
|
153
|
+
return kinds.includes(this.#kind);
|
|
154
|
+
}
|
|
155
|
+
/** A detached copy with the same text, trivia and origin. */
|
|
156
|
+
clone() {
|
|
157
|
+
return trustedToken(this.#kind, this.#text, this.#leadingTrivia, this.#trailingTrivia, this.#origin);
|
|
158
|
+
}
|
|
159
|
+
/** The current position, computed by a walk from the root; null in a detached subtree. */
|
|
160
|
+
getPosition() {
|
|
161
|
+
let root = this.#parent;
|
|
162
|
+
while (root?.parent) {
|
|
163
|
+
root = root.parent;
|
|
164
|
+
}
|
|
165
|
+
if (!(root instanceof DocumentNode)) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
let text = '';
|
|
169
|
+
for (const token of root.getTokens()) {
|
|
170
|
+
if (token === this) {
|
|
171
|
+
for (const trivia of this.#leadingTrivia) {
|
|
172
|
+
text += trivia.text;
|
|
173
|
+
}
|
|
174
|
+
return new Cursor(text).at(text.length);
|
|
175
|
+
}
|
|
176
|
+
text += token.toString();
|
|
177
|
+
}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
/** The source text of the token with its trivia. */
|
|
181
|
+
toString() {
|
|
182
|
+
let res = '';
|
|
183
|
+
for (const trivia of this.#leadingTrivia) {
|
|
184
|
+
res += trivia.text;
|
|
185
|
+
}
|
|
186
|
+
res += this.#text;
|
|
187
|
+
for (const trivia of this.#trailingTrivia) {
|
|
188
|
+
res += trivia.text;
|
|
189
|
+
}
|
|
190
|
+
return res;
|
|
191
|
+
}
|
|
192
|
+
/** @internal Sets the leading trivia without the checks of the place, for an edit that moves trivia in steps. */
|
|
193
|
+
[setLeading](trivia) {
|
|
194
|
+
checkTrivia(trivia, 'leading', this.#kind === 'EndOfFile');
|
|
195
|
+
const old = this.#leadingTrivia;
|
|
196
|
+
record(() => this.#leadingTrivia = old);
|
|
197
|
+
this.#leadingTrivia = Object.freeze(uniqueTrivia(trivia));
|
|
198
|
+
return this;
|
|
199
|
+
}
|
|
200
|
+
/** @internal */
|
|
201
|
+
[setTrailing](trivia) {
|
|
202
|
+
checkTrivia(trivia, 'trailing', this.#kind === 'EndOfFile');
|
|
203
|
+
const old = this.#trailingTrivia;
|
|
204
|
+
record(() => this.#trailingTrivia = old);
|
|
205
|
+
this.#trailingTrivia = Object.freeze(uniqueTrivia(trivia));
|
|
206
|
+
return this;
|
|
207
|
+
}
|
|
208
|
+
/** @internal For a token made from a text that is not the source of the document. */
|
|
209
|
+
[forgetOrigin]() {
|
|
210
|
+
this.#origin = null;
|
|
211
|
+
}
|
|
212
|
+
/** @internal */
|
|
213
|
+
[setParent](parent) {
|
|
214
|
+
const old = this.#parent;
|
|
215
|
+
record(() => this.#parent = old);
|
|
216
|
+
this.#parent = parent;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
let trusted = false;
|
|
220
|
+
/** @internal A token whose text and trivia are known to be valid: one the lexer reads or a clone. */
|
|
221
|
+
export function trustedToken(kind, text, leading, trailing, origin) {
|
|
222
|
+
trusted = true;
|
|
223
|
+
try {
|
|
224
|
+
return new Token(kind, text, leading, trailing, origin);
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
trusted = false;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function checkText(kind, text) {
|
|
231
|
+
if (!text.isWellFormed() || (kind === 'EndOfFile' ? text !== '' : scanSingleToken(text) !== kind)) {
|
|
232
|
+
throw new TypeError(`${JSON.stringify(text)} is not one token of kind ${kind}.`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** The tokens before and after one in a document; none when it is not in a document. */
|
|
236
|
+
function neighbours(token) {
|
|
237
|
+
let root = token.parent;
|
|
238
|
+
while (root?.parent) {
|
|
239
|
+
root = root.parent;
|
|
240
|
+
}
|
|
241
|
+
return root instanceof DocumentNode ? [findNeighbour(token, true), findNeighbour(token, false)] : [null, null];
|
|
242
|
+
}
|
|
243
|
+
/** The token of the tree right before or after one. */
|
|
244
|
+
export function findNeighbour(token, backward) {
|
|
245
|
+
for (const neighbour of iterateNeighbours(token, backward)) {
|
|
246
|
+
return neighbour;
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
/** The tokens of the tree before or after one, the nearest first, read lazily from the nodes around it. */
|
|
251
|
+
export function* iterateNeighbours(token, backward) {
|
|
252
|
+
let child = token;
|
|
253
|
+
for (let node = token.parent; node; child = node, node = node.parent) {
|
|
254
|
+
const children = node.getChildren();
|
|
255
|
+
const i = children.indexOf(child);
|
|
256
|
+
for (const sibling of backward ? children.slice(0, i).reverse() : children.slice(i + 1)) {
|
|
257
|
+
yield* iterateTokens(sibling, backward);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function* iterateTokens(node, backward) {
|
|
262
|
+
if (node instanceof Token) {
|
|
263
|
+
yield node;
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
const children = node.getChildren();
|
|
267
|
+
for (const child of backward ? children.toReversed() : children) {
|
|
268
|
+
yield* iterateTokens(child, backward);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function checkTrivia(list, side, endOfFile) {
|
|
272
|
+
if (side === 'trailing' && endOfFile && list.length) {
|
|
273
|
+
throw new TypeError('EndOfFile has no trailing trivia.');
|
|
274
|
+
}
|
|
275
|
+
list.forEach((trivia, i) => {
|
|
276
|
+
const next = list[i + 1];
|
|
277
|
+
if (!(trivia instanceof Trivia)) {
|
|
278
|
+
throw new TypeError('Trivia expected.');
|
|
279
|
+
}
|
|
280
|
+
else if (trivia.kind === 'Whitespace' && next?.kind === 'Whitespace') {
|
|
281
|
+
throw new TypeError('Two whitespace trivia must not stand next to each other.');
|
|
282
|
+
}
|
|
283
|
+
else if (trivia.kind === 'ByteOrderMark' && (side !== 'leading' || i > 0)) {
|
|
284
|
+
throw new TypeError('A byte order mark may only be the first leading trivia.');
|
|
285
|
+
}
|
|
286
|
+
else if (trivia.kind === 'LineEnding' && side === 'trailing' && next) {
|
|
287
|
+
throw new TypeError('A line ending must be the last of the trailing trivia.');
|
|
288
|
+
}
|
|
289
|
+
else if (trivia.kind === 'Comment' && next?.kind !== 'LineEnding' && (next || (side === 'leading' && !endOfFile))) {
|
|
290
|
+
throw new TypeError('A comment must be followed by a line ending.');
|
|
291
|
+
}
|
|
292
|
+
else if (trivia.text.endsWith('\r') && next?.text.startsWith('\n')) {
|
|
293
|
+
throw new TypeError('A carriage return before a line feed would make one line ending of the two.');
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
/** A copy of the list where an instance stands once, so that replaceTrivia() finds the one meant. */
|
|
298
|
+
function uniqueTrivia(list) {
|
|
299
|
+
const seen = (new Set);
|
|
300
|
+
return list.map((trivia) => seen.has(trivia) ? new Trivia(trivia.kind, trivia.text) : (seen.add(trivia), trivia));
|
|
301
|
+
}
|
|
302
|
+
/** Kind of the text if the scanner reads it as exactly one token without trivia. */
|
|
303
|
+
function scanSingleToken(text) {
|
|
304
|
+
const scanner = new Scanner(text);
|
|
305
|
+
const token = scanner.next();
|
|
306
|
+
return token && token.end === text.length && token.kind !== 'Trivia'
|
|
307
|
+
? token.kind
|
|
308
|
+
: null;
|
|
309
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Node, ValueNode } from './nodes.ts';
|
|
2
|
+
/** What a visitor returns to steer the walk; symbols, so that no other value means an action by accident. */
|
|
3
|
+
export declare const TraverseAction: Readonly<{
|
|
4
|
+
readonly SkipChildren: symbol;
|
|
5
|
+
readonly Stop: symbol;
|
|
6
|
+
readonly Remove: symbol;
|
|
7
|
+
}>;
|
|
8
|
+
export type TraverseAction = typeof TraverseAction[keyof typeof TraverseAction];
|
|
9
|
+
/** What a visitor returns: nothing to go on, a node to take the place of the visited one, or an instruction. */
|
|
10
|
+
export type VisitResult = void | ValueNode | TraverseAction;
|
|
11
|
+
export type Visitor = (node: Node) => VisitResult;
|
|
12
|
+
/**
|
|
13
|
+
* Walks the tree depth-first, calling enter() before the children of a node and leave() after them.
|
|
14
|
+
* The children of a node are taken when it is entered, so nodes inserted around the visited one later
|
|
15
|
+
* are not visited, and a child taken out of the node meanwhile is skipped; a node the visitor takes out
|
|
16
|
+
* of its place itself is neither walked into nor left. A replacement is visited from its children on.
|
|
17
|
+
* Stop ends the walk at once: no node is entered or left after it. The walk is not one atomic edit:
|
|
18
|
+
* the edits before a failure stay.
|
|
19
|
+
*/
|
|
20
|
+
export declare class Traverser {
|
|
21
|
+
#private;
|
|
22
|
+
private readonly enter?;
|
|
23
|
+
private readonly leave?;
|
|
24
|
+
private constructor();
|
|
25
|
+
static traverse(node: Node, enter?: Visitor, leave?: Visitor): Node | null;
|
|
26
|
+
/** Returns the node that stands in the place of the visited one afterwards, null if it was removed or taken out. */
|
|
27
|
+
private visit;
|
|
28
|
+
/** Whether the node at the level and the nodes above it still stand where they were entered. */
|
|
29
|
+
private isInPlace;
|
|
30
|
+
private apply;
|
|
31
|
+
}
|