@mrhenry/twig-tokenizer 0.1.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/LICENSE +24 -0
- package/package.json +10 -0
- package/src/code-points/code-points.js +54 -0
- package/src/code-points/ranges.js +161 -0
- package/src/errors.js +283 -0
- package/src/index.js +11 -0
- package/src/lexer.js +1766 -0
- package/src/operators.js +65 -0
- package/src/token-stream.js +188 -0
- package/src/token.js +356 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mr. Henry
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
This license applies only to the code in this repository.
|
|
24
|
+
Images are explicitly excluded.
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Named constants for the code points used by the Twig lexer.
|
|
4
|
+
*
|
|
5
|
+
* @module twig-tokenizer
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** \x00 */
|
|
9
|
+
export const NULL = 0x0000;
|
|
10
|
+
/** \t */
|
|
11
|
+
export const CHARACTER_TABULATION = 0x0009;
|
|
12
|
+
/** \n */
|
|
13
|
+
export const LINE_FEED = 0x000a;
|
|
14
|
+
/** \v */
|
|
15
|
+
export const LINE_TABULATION = 0x000b;
|
|
16
|
+
/** \f */
|
|
17
|
+
export const FORM_FEED = 0x000c;
|
|
18
|
+
/** \r */
|
|
19
|
+
export const CARRIAGE_RETURN = 0x000d;
|
|
20
|
+
/** \u20 */
|
|
21
|
+
export const SPACE = 0x0020;
|
|
22
|
+
|
|
23
|
+
/** " */
|
|
24
|
+
export const QUOTATION_MARK = 0x0022;
|
|
25
|
+
/** # */
|
|
26
|
+
export const NUMBER_SIGN = 0x0023;
|
|
27
|
+
/** ' */
|
|
28
|
+
export const APOSTROPHE = 0x0027;
|
|
29
|
+
/** ( */
|
|
30
|
+
export const LEFT_PARENTHESIS = 0x0028;
|
|
31
|
+
/** ) */
|
|
32
|
+
export const RIGHT_PARENTHESIS = 0x0029;
|
|
33
|
+
/** + */
|
|
34
|
+
export const PLUS_SIGN = 0x002b;
|
|
35
|
+
/** - */
|
|
36
|
+
export const HYPHEN_MINUS = 0x002d;
|
|
37
|
+
/** . */
|
|
38
|
+
export const FULL_STOP = 0x002e;
|
|
39
|
+
/** E */
|
|
40
|
+
export const LATIN_CAPITAL_LETTER_E = 0x0045;
|
|
41
|
+
/** [ */
|
|
42
|
+
export const LEFT_SQUARE_BRACKET = 0x005b;
|
|
43
|
+
/** \ */
|
|
44
|
+
export const REVERSE_SOLIDUS = 0x005c;
|
|
45
|
+
/** _ */
|
|
46
|
+
export const LOW_LINE = 0x005f;
|
|
47
|
+
/** e */
|
|
48
|
+
export const LATIN_SMALL_LETTER_E = 0x0065;
|
|
49
|
+
/** { */
|
|
50
|
+
export const LEFT_CURLY_BRACKET = 0x007b;
|
|
51
|
+
/** | */
|
|
52
|
+
export const VERTICAL_LINE = 0x007c;
|
|
53
|
+
/** } */
|
|
54
|
+
export const RIGHT_CURLY_BRACKET = 0x007d;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Predicates for classifying code points during lexing.
|
|
4
|
+
*
|
|
5
|
+
* @module twig-tokenizer
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
CARRIAGE_RETURN,
|
|
9
|
+
CHARACTER_TABULATION,
|
|
10
|
+
FORM_FEED,
|
|
11
|
+
LEFT_CURLY_BRACKET,
|
|
12
|
+
LEFT_PARENTHESIS,
|
|
13
|
+
LEFT_SQUARE_BRACKET,
|
|
14
|
+
LINE_FEED,
|
|
15
|
+
LINE_TABULATION,
|
|
16
|
+
NULL,
|
|
17
|
+
RIGHT_PARENTHESIS,
|
|
18
|
+
SPACE,
|
|
19
|
+
} from './code-points.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whether the code point is an ASCII decimal digit (`0` .. `9`).
|
|
23
|
+
*
|
|
24
|
+
* @param {number} search The code point to test.
|
|
25
|
+
* @returns {boolean}
|
|
26
|
+
*/
|
|
27
|
+
export function isDigitCodePoint(search) {
|
|
28
|
+
return search >= 0x0030 && search <= 0x0039;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Whether the code point is a hexadecimal digit (`0` .. `9`, `a` .. `f`, `A` .. `F`).
|
|
33
|
+
*
|
|
34
|
+
* @param {number} search The code point to test.
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
export function isHexDigitCodePoint(search) {
|
|
38
|
+
return (
|
|
39
|
+
(search >= 0x0030 && search <= 0x0039) ||
|
|
40
|
+
(search >= 0x0061 && search <= 0x0066) ||
|
|
41
|
+
(search >= 0x0041 && search <= 0x0046)
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether the code point is an octal digit (`0` .. `7`).
|
|
47
|
+
*
|
|
48
|
+
* @param {number} search The code point to test.
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
export function isOctalDigitCodePoint(search) {
|
|
52
|
+
return search >= 0x0030 && search <= 0x0037;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether the code point is an ASCII letter (`a` .. `z`, `A` .. `Z`).
|
|
57
|
+
*
|
|
58
|
+
* @param {number} search The code point to test.
|
|
59
|
+
* @returns {boolean}
|
|
60
|
+
*/
|
|
61
|
+
export function isAsciiLetterCodePoint(search) {
|
|
62
|
+
return (
|
|
63
|
+
(search >= 0x0061 && search <= 0x007a) ||
|
|
64
|
+
(search >= 0x0041 && search <= 0x005a)
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether the code point starts a Twig name.
|
|
70
|
+
*
|
|
71
|
+
* Mirrors the reference implementation's byte-oriented
|
|
72
|
+
* `[a-zA-Z_\x7f-\xff]`: in byte mode any UTF-8 byte in `\x7f-\xff` is an
|
|
73
|
+
* identifier start character, which corresponds to every code point from
|
|
74
|
+
* U+007F onward here (astral code points are matched via their surrogate
|
|
75
|
+
* halves in byte terms, and via the code point itself here).
|
|
76
|
+
*
|
|
77
|
+
* @param {number} search The code point to test.
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
export function isNameStartCodePoint(search) {
|
|
81
|
+
return (
|
|
82
|
+
(search >= 0x0041 && search <= 0x005a) ||
|
|
83
|
+
(search >= 0x0061 && search <= 0x007a) ||
|
|
84
|
+
search === 0x005f ||
|
|
85
|
+
search >= 0x007f
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether the code point continues a Twig name.
|
|
91
|
+
*
|
|
92
|
+
* Mirrors the byte-oriented `[a-zA-Z0-9_\x7f-\xff]`.
|
|
93
|
+
*
|
|
94
|
+
* @param {number} search The code point to test.
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
export function isNameCodePoint(search) {
|
|
98
|
+
return isNameStartCodePoint(search) || isDigitCodePoint(search);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Whether the code point is whitespace as matched by PCRE `\s` in byte mode.
|
|
103
|
+
*
|
|
104
|
+
* Space, tab, line feed, vertical tab, form feed and carriage return. JS
|
|
105
|
+
* `\s` also matches Unicode whitespace (NBSP, en/em spaces, U+2028/U+2029, …)
|
|
106
|
+
* that the reference implementation treats as identifier characters in byte
|
|
107
|
+
* mode; using this explicit set keeps the lexer byte-for-byte compatible
|
|
108
|
+
* with `src/Lexer.php`.
|
|
109
|
+
*
|
|
110
|
+
* @param {number} search The code point to test.
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
export function isWhitespaceCodePoint(search) {
|
|
114
|
+
return (
|
|
115
|
+
search === SPACE ||
|
|
116
|
+
search === CHARACTER_TABULATION ||
|
|
117
|
+
search === LINE_FEED ||
|
|
118
|
+
search === LINE_TABULATION ||
|
|
119
|
+
search === FORM_FEED ||
|
|
120
|
+
search === CARRIAGE_RETURN
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Whether the code point is stripped by PHP's default `ltrim()`/`rtrim()`.
|
|
126
|
+
*
|
|
127
|
+
* The default character list is `" \t\n\r\v\x00"`, which notably does *not*
|
|
128
|
+
* include the form feed `\f`.
|
|
129
|
+
*
|
|
130
|
+
* @param {number} search The code point to test.
|
|
131
|
+
* @returns {boolean}
|
|
132
|
+
*/
|
|
133
|
+
export function isPhpTrimCodePoint(search) {
|
|
134
|
+
return (
|
|
135
|
+
search === SPACE ||
|
|
136
|
+
search === CHARACTER_TABULATION ||
|
|
137
|
+
search === LINE_FEED ||
|
|
138
|
+
search === CARRIAGE_RETURN ||
|
|
139
|
+
search === LINE_TABULATION ||
|
|
140
|
+
search === NULL
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Whether the code point may follow an operator that ends with a letter.
|
|
146
|
+
*
|
|
147
|
+
* Mirrors the `(?=[\s()\[{])` lookahead of the reference implementation.
|
|
148
|
+
*
|
|
149
|
+
* @param {number} search The code point to test.
|
|
150
|
+
* @returns {boolean}
|
|
151
|
+
*/
|
|
152
|
+
export function isOperatorDelimiterCodePoint(search) {
|
|
153
|
+
return (
|
|
154
|
+
isWhitespaceCodePoint(search) ||
|
|
155
|
+
search === LEFT_PARENTHESIS ||
|
|
156
|
+
search === RIGHT_PARENTHESIS ||
|
|
157
|
+
search === LEFT_SQUARE_BRACKET ||
|
|
158
|
+
search === LEFT_CURLY_BRACKET
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
package/src/errors.js
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Error classes mirroring `Twig\Error\*`.
|
|
4
|
+
*
|
|
5
|
+
* @module twig-tokenizer
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A source context (template name + code).
|
|
10
|
+
*
|
|
11
|
+
* @typedef {object} Source
|
|
12
|
+
* @property {string} name The template name.
|
|
13
|
+
* @property {string} code The template source code.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Base class for all Twig errors.
|
|
18
|
+
*
|
|
19
|
+
* Mirrors `Twig\Error\Error`: the raw message is stored separately and the
|
|
20
|
+
* exposed message appends the template location (`in "name" at line N column M.`).
|
|
21
|
+
*/
|
|
22
|
+
export class TwigError extends Error {
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} message The raw error message.
|
|
25
|
+
* @param {number} [lineNumber] The 1-based template line, or -1.
|
|
26
|
+
* @param {Source|null} [source] The template source context.
|
|
27
|
+
* @param {Error|null} [previous] The previous (wrapped) exception.
|
|
28
|
+
* @param {number|null} [columnNumber] The 1-based column, if known.
|
|
29
|
+
*/
|
|
30
|
+
constructor(message, lineNumber = -1, source = null, previous = null, columnNumber = null) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = this.constructor.name;
|
|
33
|
+
/** @type {number} */
|
|
34
|
+
this.lineNumber = lineNumber;
|
|
35
|
+
/** @type {Source|null} */
|
|
36
|
+
this.source = source;
|
|
37
|
+
/** @type {Error|null} */
|
|
38
|
+
this.previous = previous;
|
|
39
|
+
/** @type {number|null} */
|
|
40
|
+
this.columnNumber = columnNumber;
|
|
41
|
+
/** @type {string} */
|
|
42
|
+
this.rawMessage = message;
|
|
43
|
+
this.updateRepresentation();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Rebuilds the formatted message with template location operatorInfo. */
|
|
47
|
+
updateRepresentation() {
|
|
48
|
+
let message = this.rawMessage;
|
|
49
|
+
const last = message.slice(-1);
|
|
50
|
+
const punctuation = last === '.' || last === '?' ? last : '';
|
|
51
|
+
if (punctuation) {
|
|
52
|
+
message = message.slice(0, -1);
|
|
53
|
+
}
|
|
54
|
+
if (this.source && this.source.name) {
|
|
55
|
+
message += ` in "${this.source.name}"`;
|
|
56
|
+
}
|
|
57
|
+
if (this.lineNumber > 0) {
|
|
58
|
+
message += ` at line ${this.lineNumber}`;
|
|
59
|
+
if (this.columnNumber !== null) {
|
|
60
|
+
message += ` column ${this.columnNumber}`;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (punctuation) {
|
|
64
|
+
message += punctuation;
|
|
65
|
+
}
|
|
66
|
+
this.message = message;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @returns {string} The raw message (without location).
|
|
71
|
+
*/
|
|
72
|
+
getRawMessage() {
|
|
73
|
+
return this.rawMessage;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The fully-qualified PHP class name this error maps to.
|
|
78
|
+
*
|
|
79
|
+
* @returns {string} e.g. `Twig\Error\SyntaxError`.
|
|
80
|
+
*/
|
|
81
|
+
getPhpClass() {
|
|
82
|
+
return `Twig\\Error\\${this.name.replace(/Error$/, 'Error')}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Formats the error like PHP's `sprintf('%s: %s', $e::class, $e->getMessage())`.
|
|
87
|
+
*
|
|
88
|
+
* @returns {string} `Class: message`.
|
|
89
|
+
*/
|
|
90
|
+
toPhpFormat() {
|
|
91
|
+
return `${this.getPhpClass()}: ${this.message}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @returns {number} The template line, or -1.
|
|
96
|
+
*/
|
|
97
|
+
getTemplateLine() {
|
|
98
|
+
return this.lineNumber;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Appends text to the raw message and rebuilds the formatted message.
|
|
103
|
+
*
|
|
104
|
+
* Mirrors `Twig\Error\Error::appendMessage()`.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} rawMessage
|
|
107
|
+
*/
|
|
108
|
+
appendMessage(rawMessage) {
|
|
109
|
+
this.rawMessage += rawMessage;
|
|
110
|
+
this.updateRepresentation();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {number} line Sets the template line.
|
|
115
|
+
*/
|
|
116
|
+
setTemplateLine(line) {
|
|
117
|
+
this.lineNumber = line;
|
|
118
|
+
this.updateRepresentation();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* @returns {Source|null} The source context.
|
|
123
|
+
*/
|
|
124
|
+
getSourceContext() {
|
|
125
|
+
return this.source;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @param {Source|null} source Sets the source context.
|
|
130
|
+
*/
|
|
131
|
+
setSourceContext(source) {
|
|
132
|
+
this.source = source;
|
|
133
|
+
this.updateRepresentation();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {number|null} columnNumber Sets the column.
|
|
138
|
+
*/
|
|
139
|
+
setTemplateColumn(columnNumber) {
|
|
140
|
+
this.columnNumber = columnNumber;
|
|
141
|
+
this.updateRepresentation();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Raised for lexer/parser errors, unknown tags or callables.
|
|
147
|
+
*/
|
|
148
|
+
export class SyntaxError extends TwigError {
|
|
149
|
+
/**
|
|
150
|
+
* @param {string} message
|
|
151
|
+
* @param {number} [lineNumber]
|
|
152
|
+
* @param {Source|null} [source]
|
|
153
|
+
* @param {number|null} [columnNumber]
|
|
154
|
+
*/
|
|
155
|
+
constructor(message, lineNumber = -1, source = null, columnNumber = null) {
|
|
156
|
+
super(message, lineNumber, source, null, columnNumber);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @override
|
|
161
|
+
* @returns {string}
|
|
162
|
+
*/
|
|
163
|
+
getPhpClass() {
|
|
164
|
+
return 'Twig\\Error\\SyntaxError';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Appends "Did you mean …?" suggestions to the message.
|
|
169
|
+
*
|
|
170
|
+
* Mirrors `Twig\Error\SyntaxError::addSuggestions()`, including its
|
|
171
|
+
* byte-oriented semantics: the Levenshtein distance and the length threshold
|
|
172
|
+
* are computed over the UTF-8 byte sequences (PHP `levenshtein()` /
|
|
173
|
+
* `strlen()`).
|
|
174
|
+
*
|
|
175
|
+
* @param {string} name The name that does not exist.
|
|
176
|
+
* @param {string[]} items The possible names.
|
|
177
|
+
*/
|
|
178
|
+
addSuggestions(name, items) {
|
|
179
|
+
const nameBytes = toBytes(name);
|
|
180
|
+
/** @type {Record<string, number>} */
|
|
181
|
+
const alternatives = {};
|
|
182
|
+
for (const item of items) {
|
|
183
|
+
const lev = byteLevenshtein(nameBytes, toBytes(item));
|
|
184
|
+
if (lev <= nameBytes.length / 3 || item.includes(name)) {
|
|
185
|
+
alternatives[item] = lev;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
const keys = Object.keys(alternatives);
|
|
189
|
+
if (!keys.length) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
keys.sort((a, b) => alternatives[a] - alternatives[b]);
|
|
193
|
+
this.appendMessage(` Did you mean "${keys.join('", "')}"?`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {string} value
|
|
199
|
+
* @returns {Uint8Array} The UTF-8 bytes of the string.
|
|
200
|
+
*/
|
|
201
|
+
function toBytes(value) {
|
|
202
|
+
return new TextEncoder().encode(value);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The Levenshtein distance between two byte sequences (mirrors PHP
|
|
207
|
+
* `levenshtein()`, which operates on bytes).
|
|
208
|
+
*
|
|
209
|
+
* @param {Uint8Array} a
|
|
210
|
+
* @param {Uint8Array} b
|
|
211
|
+
* @returns {number}
|
|
212
|
+
*/
|
|
213
|
+
function byteLevenshtein(a, b) {
|
|
214
|
+
if (a.length === 0) {
|
|
215
|
+
return b.length;
|
|
216
|
+
}
|
|
217
|
+
if (b.length === 0) {
|
|
218
|
+
return a.length;
|
|
219
|
+
}
|
|
220
|
+
const matrix = new Uint16Array((a.length + 1) * (b.length + 1));
|
|
221
|
+
for (let i = 0; i <= a.length; i++) {
|
|
222
|
+
matrix[i * (b.length + 1)] = i;
|
|
223
|
+
}
|
|
224
|
+
for (let j = 0; j <= b.length; j++) {
|
|
225
|
+
matrix[j] = j;
|
|
226
|
+
}
|
|
227
|
+
for (let i = 1; i <= a.length; i++) {
|
|
228
|
+
for (let j = 1; j <= b.length; j++) {
|
|
229
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
230
|
+
matrix[i * (b.length + 1) + j] = Math.min(
|
|
231
|
+
matrix[(i - 1) * (b.length + 1) + j] + 1,
|
|
232
|
+
matrix[i * (b.length + 1) + j - 1] + 1,
|
|
233
|
+
matrix[(i - 1) * (b.length + 1) + j - 1] + cost,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return matrix[a.length * (b.length + 1) + b.length];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Raised for errors during rendering.
|
|
242
|
+
*/
|
|
243
|
+
export class RuntimeError extends TwigError {
|
|
244
|
+
/**
|
|
245
|
+
* @param {string} message
|
|
246
|
+
* @param {number} [lineNumber]
|
|
247
|
+
* @param {Source|null} [source]
|
|
248
|
+
* @param {Error|null} [previous]
|
|
249
|
+
*/
|
|
250
|
+
constructor(message, lineNumber = -1, source = null, previous = null) {
|
|
251
|
+
super(message, lineNumber, source, previous);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @override
|
|
256
|
+
* @returns {string}
|
|
257
|
+
*/
|
|
258
|
+
getPhpClass() {
|
|
259
|
+
return 'Twig\\Error\\RuntimeError';
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Raised when a template cannot be found or read.
|
|
265
|
+
*/
|
|
266
|
+
export class LoaderError extends TwigError {
|
|
267
|
+
/**
|
|
268
|
+
* @param {string} message
|
|
269
|
+
* @param {number} [lineNumber]
|
|
270
|
+
* @param {Source|null} [source]
|
|
271
|
+
*/
|
|
272
|
+
constructor(message, lineNumber = -1, source = null) {
|
|
273
|
+
super(message, lineNumber, source);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @override
|
|
278
|
+
* @returns {string}
|
|
279
|
+
*/
|
|
280
|
+
getPhpClass() {
|
|
281
|
+
return 'Twig\\Error\\LoaderError';
|
|
282
|
+
}
|
|
283
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Public API of the `@mrhenry/twig-tokenizer` package.
|
|
4
|
+
*
|
|
5
|
+
* @module twig-tokenizer
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { Token, TokenType, typeToString, typeToEnglish } from './token.js';
|
|
9
|
+
export { TokenStream } from './token-stream.js';
|
|
10
|
+
export { Lexer, LexerState, Source, DEFAULT_LEXER_OPTIONS } from './lexer.js';
|
|
11
|
+
export { TwigError, SyntaxError, RuntimeError, LoaderError } from './errors.js';
|