template-sluz 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +237 -0
- package/package.json +30 -0
- package/src/sluz.js +716 -0
package/README.md
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
# ⚡ Sluz templating system
|
|
2
|
+
|
|
3
|
+
A minimalistic JavaScript templating engine with Smarty-like syntax. Zero dependencies, ESM-only.
|
|
4
|
+
|
|
5
|
+
## 📦 Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install template-sluz
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 🚀 Quick Start
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
import Sluz from 'template-sluz';
|
|
15
|
+
|
|
16
|
+
const sluz = new Sluz();
|
|
17
|
+
sluz.assign('name', 'Scott');
|
|
18
|
+
sluz.assign('user', { first: 'Jason', last: 'Doolis', age: 43 });
|
|
19
|
+
|
|
20
|
+
console.log(sluz.parse('Hello {$name}')); // Hello Scott
|
|
21
|
+
console.log(sluz.parse('{$user.first} {$user.last}')); // Jason Doolis
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 🌐 Browser Usage
|
|
27
|
+
|
|
28
|
+
Load Sluz in the browser with `<script type="module">`.
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<script type="module">
|
|
32
|
+
import Sluz from './path/to/sluz.js';
|
|
33
|
+
const sluz = new Sluz();
|
|
34
|
+
|
|
35
|
+
sluz.assign('user', { name: 'Alice', role: 'admin' });
|
|
36
|
+
document.getElementById('body').innerHTML = sluz.parse("Welcome {$user.name} you are {$user.role}");
|
|
37
|
+
</script>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 📝 Variables
|
|
43
|
+
|
|
44
|
+
Variables are inserted with `{$varname}`. Dotted paths resolve nested objects and arrays.
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
sluz.assign('person', { name: { first: 'Jane' }, colors: ['red', 'green'] });
|
|
48
|
+
|
|
49
|
+
sluz.parse('{$person.name.first}'); // Jane
|
|
50
|
+
sluz.parse('{$person.colors.0}'); // red
|
|
51
|
+
sluz.parse('{$missing}'); // '' (empty string)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### assign()
|
|
55
|
+
|
|
56
|
+
Accepts key/value pairs or a single object:
|
|
57
|
+
|
|
58
|
+
```js
|
|
59
|
+
sluz.assign('color', 'blue'); // Scalar
|
|
60
|
+
sluz.assign('size', ['small', 'medium', 'large']); // Array
|
|
61
|
+
sluz.assign('info', { color: 'yellow', age: 43 }); // Hash
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 📖 API Reference
|
|
67
|
+
|
|
68
|
+
### `new Sluz()`
|
|
69
|
+
|
|
70
|
+
Creates a new template engine instance.
|
|
71
|
+
|
|
72
|
+
### `assign(key, value)` / `assign(object)`
|
|
73
|
+
|
|
74
|
+
Sets template variables. Accepts:
|
|
75
|
+
- Key/value pairs: `sluz.assign('name', 'Scott')`
|
|
76
|
+
- A single object: `sluz.assign('info', { name: 'Scott', age: 43 })`
|
|
77
|
+
|
|
78
|
+
### `parse(string)`
|
|
79
|
+
|
|
80
|
+
Parses a template string with the current variables and returns the rendered output.
|
|
81
|
+
|
|
82
|
+
### `registerModifier(name, fn)`
|
|
83
|
+
|
|
84
|
+
Registers a custom modifier function. The function receives the variable value as the first argument, followed by any user-supplied arguments from the template.
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
sluz.registerModifier('truncate', (s, n) => String(s).slice(0, n));
|
|
88
|
+
// Template: {$name|truncate:3}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 🔧 Modifiers
|
|
94
|
+
|
|
95
|
+
Modifiers transform variable output using pipe (`|`) syntax. Arguments follow a colon (`:`), multiple arguments are comma-separated.
|
|
96
|
+
|
|
97
|
+
### Built-in modifiers
|
|
98
|
+
|
|
99
|
+
| Modifier | Description | Example |
|
|
100
|
+
|------------|------------------------------------------|----------------------------------------------|
|
|
101
|
+
| `upper` | Uppercase string | `{$name\|upper}` |
|
|
102
|
+
| `lower` | Lowercase string | `{$name\|lower}` |
|
|
103
|
+
| `ucfirst` | Capitalize first character | `{$name\|ucfirst}` |
|
|
104
|
+
| `trim` | Trim whitespace | `{$name\|trim}` |
|
|
105
|
+
| `length` | String length | `{$name\|length}` |
|
|
106
|
+
| `substr` | Substring `(start[, length])` | `{$name\|substr:0,3}` |
|
|
107
|
+
| `replace` | Replace all occurrences | `{$name\|replace:"old","new"}` |
|
|
108
|
+
| `join` | Join array with separator | `{$items\|join:", "}` |
|
|
109
|
+
| `count` | Count array keys / object keys / truthy | `{$items\|count}` |
|
|
110
|
+
| `first` | First element of array / first character | `{$items\|first}` |
|
|
111
|
+
| `last` | Last element of array / last character | `{$items\|last}` |
|
|
112
|
+
|
|
113
|
+
### Default values
|
|
114
|
+
|
|
115
|
+
The `default:` modifier returns a fallback when the variable is empty (undefined, null, or empty string):
|
|
116
|
+
|
|
117
|
+
```js
|
|
118
|
+
sluz.parse('{$name|default:"N/A"}'); // Scott (unchanged)
|
|
119
|
+
sluz.parse('{$zero|default:"123"}'); // 0 (zero is not empty)
|
|
120
|
+
sluz.parse('{$missing|default:"N/A"}'); // N/A
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Chained modifiers
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
sluz.parse('{$name|upper|substr:0,2}'); // SC
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Custom modifiers
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
sluz.registerModifier('greet', name => `Howdy, ${name}!`);
|
|
133
|
+
sluz.parse('{$name|greet}'); // Howdy, Scott!
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 🔢 Expressions & Math
|
|
139
|
+
|
|
140
|
+
Wrap any JavaScript expression in braces for evaluation:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
sluz.parse('{$count + 10}'); // 17
|
|
144
|
+
sluz.parse('{($count * 3) - 5}'); // 16
|
|
145
|
+
sluz.parse('{$count > 5}'); // true
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 🔀 Conditionals: `{if}` / `{elseif}` / `{else}` / `{/if}`
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
sluz.parse('{if $admin}Welcome admin{/if}');
|
|
154
|
+
sluz.parse('{if $count > 5}Big{else}Small{/if}');
|
|
155
|
+
sluz.parse('{if $age < 21}Minor{elseif $age < 65}Adult{else}Senior{/if}');
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Supports `&&`, `||`, `!`, parentheses, and comparison operators (`==`, `!=`, `<`, `>`, `<=`, `>=`).
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 🔄 Loops: `{foreach}` / `{/foreach}`
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
// Simple iteration
|
|
166
|
+
{foreach $items as $x}{$x} {/foreach}
|
|
167
|
+
|
|
168
|
+
// Key/value iteration (index => value for arrays, key => value for objects)
|
|
169
|
+
{foreach $items as $idx => $x}[{$idx}]: {$x} {/foreach}
|
|
170
|
+
|
|
171
|
+
// Works with objects
|
|
172
|
+
{foreach $user as $key => $val}{$key}: {$val} {/foreach}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Foreach magic variables
|
|
176
|
+
|
|
177
|
+
Available inside loops:
|
|
178
|
+
|
|
179
|
+
| Variable | Description |
|
|
180
|
+
|----------------------|----------------------|
|
|
181
|
+
| `$__FOREACH_FIRST` | 1 on first iteration |
|
|
182
|
+
| `$__FOREACH_LAST` | 1 on last iteration |
|
|
183
|
+
| `$__FOREACH_INDEX` | 0-based index |
|
|
184
|
+
|
|
185
|
+
```js
|
|
186
|
+
{foreach $items as $x}
|
|
187
|
+
{if $__FOREACH_FIRST}>>> {/if}
|
|
188
|
+
{$x}
|
|
189
|
+
{if $__FOREACH_LAST} <<<{/if}
|
|
190
|
+
{/foreach}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 📄 Literal Blocks
|
|
196
|
+
|
|
197
|
+
`{literal}...{/literal}` bypasses template parsing, outputting content verbatim:
|
|
198
|
+
|
|
199
|
+
```js
|
|
200
|
+
sluz.parse('{literal}function foo() { .. }{/literal}');
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 💬 Comments
|
|
206
|
+
|
|
207
|
+
`{* ... *}` comments are stripped from output.
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
sluz.parse('Kitten{* favorite animal *}'); // Kitten
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## ⚠️ Error Handling
|
|
216
|
+
|
|
217
|
+
Syntax errors throw `SluzError` with a descriptive message and error code:
|
|
218
|
+
|
|
219
|
+
```js
|
|
220
|
+
import Sluz, { SluzError } from 'template-sluz';
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
sluz.parse('{foo');
|
|
224
|
+
} catch (e) {
|
|
225
|
+
console.log(e.code); // 45821
|
|
226
|
+
console.log(e.message); // Template::Sluz error #45821: Unclosed tag ...
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
| Error Code | Description |
|
|
231
|
+
|-----------|--------------------------------|
|
|
232
|
+
| `45821` | Unclosed tag |
|
|
233
|
+
| `48724` | Missing comment close `*}` |
|
|
234
|
+
| `73467` | Unknown block type |
|
|
235
|
+
| `18933` | Unknown tag / invalid eval |
|
|
236
|
+
| `47204` | Unknown modifier function |
|
|
237
|
+
| `95320` | If/else parsing error |
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "template-sluz",
|
|
3
|
+
"version": "0.9.2",
|
|
4
|
+
"description": "A minimalistic JavaScript templating engine with Smarty-like syntax",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/sluz.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/sluz.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"template",
|
|
19
|
+
"smarty",
|
|
20
|
+
"templating",
|
|
21
|
+
"sluz"
|
|
22
|
+
],
|
|
23
|
+
"license": "GPL-3.0-or-later",
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"vitest": "^3.2.6"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/sluz.js
ADDED
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
class SluzError extends Error {
|
|
2
|
+
constructor(msg, code) {
|
|
3
|
+
super(`Template::Sluz error #${code}: ${msg}`);
|
|
4
|
+
this.code = code;
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const ESCAPE_RE = {
|
|
9
|
+
'\\': '\\\\', '.': '\\.', '*': '\\*', '+': '\\+', '?': '\\?',
|
|
10
|
+
'(': '\\(', ')': '\\)', '[': '\\[', ']': '\\]', '{': '\\{', '}': '\\}',
|
|
11
|
+
'|': '\\|', '^': '\\^', '$': '\\$',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function escapeRegex(s) {
|
|
15
|
+
return s.replace(/[\\.*+?()\[\]{}|^$]/g, ch => ESCAPE_RE[ch]);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default class Sluz {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.tplVars = {};
|
|
21
|
+
this.varPrefix = 'sluz_pfx';
|
|
22
|
+
this.modifiers = new Map();
|
|
23
|
+
this.charPos = -1;
|
|
24
|
+
this._sourceStr = '';
|
|
25
|
+
|
|
26
|
+
this._registerBuiltins();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_registerBuiltins() {
|
|
30
|
+
this.modifiers.set('count', v => {
|
|
31
|
+
if (Array.isArray(v)) return v.length;
|
|
32
|
+
if (v && typeof v === 'object') return Object.keys(v).length;
|
|
33
|
+
return v != null ? 1 : 0;
|
|
34
|
+
});
|
|
35
|
+
this.modifiers.set('ucfirst', s => {
|
|
36
|
+
s = String(s);
|
|
37
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
38
|
+
});
|
|
39
|
+
this.modifiers.set('upper', s => String(s).toUpperCase());
|
|
40
|
+
this.modifiers.set('lower', s => String(s).toLowerCase());
|
|
41
|
+
this.modifiers.set('substr', (s, start, len) => {
|
|
42
|
+
s = String(s);
|
|
43
|
+
start = Number(start);
|
|
44
|
+
return len !== undefined ? s.slice(start, start + Number(len)) : s.slice(start);
|
|
45
|
+
});
|
|
46
|
+
this.modifiers.set('trim', s => String(s).trim());
|
|
47
|
+
this.modifiers.set('replace', (s, search, replacement) => String(s).replaceAll(search, replacement));
|
|
48
|
+
this.modifiers.set('length', s => String(s).length);
|
|
49
|
+
this.modifiers.set('join', (arr, sep = ',') => Array.prototype.join.call(arr, sep));
|
|
50
|
+
this.modifiers.set('first', arr => Array.isArray(arr) ? arr[0] : String(arr)[0]);
|
|
51
|
+
this.modifiers.set('last', arr => Array.isArray(arr) ? arr[arr.length - 1] : String(arr).slice(-1));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string|Object} first
|
|
56
|
+
* @param {*} [second]
|
|
57
|
+
*/
|
|
58
|
+
assign(first, second) {
|
|
59
|
+
// Batch-assign: pass a single object to assign all its keys at once
|
|
60
|
+
if (arguments.length === 1 && typeof first === 'object' && !Array.isArray(first) && first !== null) {
|
|
61
|
+
for (const [k, v] of Object.entries(first)) {
|
|
62
|
+
this.tplVars[k] = v;
|
|
63
|
+
}
|
|
64
|
+
// Key-value pair: assign('name', 'value')
|
|
65
|
+
} else if (arguments.length % 2 === 0) {
|
|
66
|
+
for (let i = 0; i < arguments.length; i += 2) {
|
|
67
|
+
this.tplVars[arguments[i]] = arguments[i + 1];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
registerModifier(name, fn) {
|
|
73
|
+
this.modifiers.set(name, fn);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// This is where all the real work is done.
|
|
77
|
+
// We break the input string into blocks based on { }
|
|
78
|
+
// Then we loop through those blocks doing variable replacement as needed
|
|
79
|
+
parse(str) {
|
|
80
|
+
this._sourceStr = str;
|
|
81
|
+
const blocks = this._getBlocks(str);
|
|
82
|
+
return this._processBlocks(blocks);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// -------------------------------------------------------------------
|
|
86
|
+
// Tokenizer
|
|
87
|
+
// -------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @param {string} str
|
|
91
|
+
* @returns {Array<[string, number]>}
|
|
92
|
+
*/
|
|
93
|
+
// Split a template string into an array of [text, endIndex] blocks,
|
|
94
|
+
// separating literal HTML from {tags} and handling nested if/foreach/literal.
|
|
95
|
+
_getBlocks(str) {
|
|
96
|
+
const slen = str.length;
|
|
97
|
+
let start = 0;
|
|
98
|
+
let i;
|
|
99
|
+
const blocks = [];
|
|
100
|
+
|
|
101
|
+
// Fast-forward to the first '{' so we don't scan plain text char-by-char
|
|
102
|
+
let z = str.indexOf('{');
|
|
103
|
+
if (z < 0) z = slen;
|
|
104
|
+
|
|
105
|
+
for (i = z; i < slen; i++) {
|
|
106
|
+
const char = str[i];
|
|
107
|
+
let isOpen = char === '{';
|
|
108
|
+
const isClosed = char === '}';
|
|
109
|
+
|
|
110
|
+
// Skip plain-text runs in one jump instead of character-by-character
|
|
111
|
+
if (!isOpen && !isClosed) {
|
|
112
|
+
const nextOpen = str.indexOf('{', i);
|
|
113
|
+
const nextClose = str.indexOf('}', i);
|
|
114
|
+
const nOpen = nextOpen < 0 ? slen : nextOpen;
|
|
115
|
+
const nClose = nextClose < 0 ? slen : nextClose;
|
|
116
|
+
i = (nOpen < nClose ? nOpen : nClose) - 1;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const hasLen = start !== i;
|
|
121
|
+
let isComment = false;
|
|
122
|
+
|
|
123
|
+
// Disambiguate '{' used in template tags from literal '{' surrounded by whitespace
|
|
124
|
+
if (isOpen) {
|
|
125
|
+
const prevC = i > 0 ? str[i - 1] : ' ';
|
|
126
|
+
const nextC = i + 1 < slen ? str[i + 1] : ' ';
|
|
127
|
+
const chk = prevC + char + nextC;
|
|
128
|
+
if (/^\s[\{\}]\s$/.test(chk)) isOpen = false;
|
|
129
|
+
if (nextC === '*') isComment = true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Push the text before this opening tag as a literal block
|
|
133
|
+
if (isOpen && hasLen) {
|
|
134
|
+
blocks.push([str.slice(start, i), i]);
|
|
135
|
+
start = i;
|
|
136
|
+
} else if (isClosed) {
|
|
137
|
+
// Find the full tag (or block) from start to the matching '}'
|
|
138
|
+
const len = i - start + 1;
|
|
139
|
+
let block = str.slice(start, start + len);
|
|
140
|
+
const openTagMatch = block.match(/^\{(if|foreach|literal)\b/);
|
|
141
|
+
// For block tags (if/foreach/literal), scan for the matching close tag
|
|
142
|
+
if (openTagMatch) {
|
|
143
|
+
const ot = openTagMatch[1];
|
|
144
|
+
const closeTag = `{/${ot}}`;
|
|
145
|
+
for (let j = i + 1; j < slen; j++) {
|
|
146
|
+
if (str[j] === '}') {
|
|
147
|
+
const tmp = str.slice(start, j + 1);
|
|
148
|
+
const oc = (tmp.match(new RegExp(`\\{${escapeRegex(ot)}\\b`, 'g')) || []).length;
|
|
149
|
+
const cc = (tmp.match(new RegExp(`\\{\\/${escapeRegex(ot)}\\}`, 'g')) || []).length;
|
|
150
|
+
if (oc === cc) {
|
|
151
|
+
block = tmp;
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (block.length) blocks.push([block, i]);
|
|
159
|
+
start += block.length;
|
|
160
|
+
i = start;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Handle {* comment *} — swallow everything up to the closing *}
|
|
164
|
+
if (isComment) {
|
|
165
|
+
const end = this._findEndingTag(str.slice(start), '{*', '*}');
|
|
166
|
+
if (end < 0) {
|
|
167
|
+
const [line, col] = this._getCharLocation(i);
|
|
168
|
+
throw new SluzError(`Missing closing <code>*}</code> for comment on line #${line}`, 48724);
|
|
169
|
+
}
|
|
170
|
+
start += end + 2;
|
|
171
|
+
i = start;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Push any remaining text after the last tag as a literal block
|
|
176
|
+
if (start < slen) {
|
|
177
|
+
blocks.push([str.slice(start), i]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Strip the leading newline from blocks that follow {if}/{for} so the
|
|
181
|
+
// rendered HTML doesn't have an extra blank line after control tags
|
|
182
|
+
let prevIsIf = false;
|
|
183
|
+
for (let bi = 0; bi < blocks.length; bi++) {
|
|
184
|
+
const bstr = blocks[bi][0];
|
|
185
|
+
const curIsIf = /^\{if\b/.test(bstr) || /^\{for/.test(bstr);
|
|
186
|
+
if (prevIsIf) {
|
|
187
|
+
let shouldStrip = 1;
|
|
188
|
+
const foreachMatch = blocks[bi - 1][0].match(/^\{foreach .+?\}([\s\S]*)\{\/foreach\}$/);
|
|
189
|
+
if (foreachMatch) {
|
|
190
|
+
shouldStrip = foreachMatch[1].endsWith('\n') ? 1 : 0;
|
|
191
|
+
}
|
|
192
|
+
if (shouldStrip) {
|
|
193
|
+
blocks[bi][0] = this._ltrimOne(bstr, '\n');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
prevIsIf = curIsIf;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return blocks;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Walk the parsed blocks and reassemble the final HTML.
|
|
203
|
+
// Blocks starting with '{' are template tags processed by _processBlock;
|
|
204
|
+
// everything else is literal text appended as-is.
|
|
205
|
+
_processBlocks(blocks) {
|
|
206
|
+
let html = '';
|
|
207
|
+
for (const x of blocks) {
|
|
208
|
+
const block = x[0];
|
|
209
|
+
if (!block.length) continue;
|
|
210
|
+
if (block[0] === '{') {
|
|
211
|
+
html += this._processBlock(block, x[1]);
|
|
212
|
+
} else {
|
|
213
|
+
html += block;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return html;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Dispatch a single {tag} string to the appropriate handler.
|
|
220
|
+
// Tries each known tag type in order: variable, if, foreach, literal,
|
|
221
|
+
// expression, then falls back to an error for unclosed tags.
|
|
222
|
+
_processBlock(str, charPos) {
|
|
223
|
+
this.charPos = charPos;
|
|
224
|
+
|
|
225
|
+
// {$var} or {$var|modifier:param} or {$var|modifier:$param}
|
|
226
|
+
let varMatch;
|
|
227
|
+
if (str.includes('|')) {
|
|
228
|
+
// Variable with modifiers - allow $ in content for variable parameters
|
|
229
|
+
varMatch = str.match(/^\{\$([\w|.'";\t :,!@#%^&*?_\-/$]+)\}$/);
|
|
230
|
+
} else {
|
|
231
|
+
// Simple variable - don't allow $ to avoid matching expressions like {$a * $b}
|
|
232
|
+
varMatch = str.match(/^\{\$([\w|.'";\t :,!@#%^&*?_\-/]+)\}$/);
|
|
233
|
+
}
|
|
234
|
+
if (str.startsWith('{$') && varMatch) {
|
|
235
|
+
return this._variableBlock(varMatch[1]);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// {if condition}...{/if}
|
|
239
|
+
if (str.startsWith('{if ') && str.endsWith('{/if}')) {
|
|
240
|
+
return this._ifBlock(str);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// {foreach $array as $key => $value}...{/foreach}
|
|
244
|
+
const foreachMatch = str.match(/^\{foreach (\$\w[\w.]*) as \$(\w+)(?: => \$(\w+))?\}([\s\S]*)\{\/foreach\}$/);
|
|
245
|
+
if (str.startsWith('{foreach ') && foreachMatch) {
|
|
246
|
+
return this._foreachBlock(foreachMatch[1], foreachMatch[2], foreachMatch[3], foreachMatch[4]);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// {literal}raw content{/literal} — returned verbatim
|
|
250
|
+
if (str.startsWith('{literal}')) {
|
|
251
|
+
const m = str.match(/^\{literal\}([\s\S]*)\{\/literal\}$/);
|
|
252
|
+
if (m) return m[1];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// { foo } — whitespace-padded content without expression markers, return verbatim
|
|
256
|
+
if (/^\{\s+.*\s+\}$/.test(str) && !/["\d\$\(]/.test(str)) {
|
|
257
|
+
return str;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Fallback: treat anything inside { } as an expression
|
|
261
|
+
const exprMatch = str.match(/^\{(.+)}$/s);
|
|
262
|
+
if (exprMatch) {
|
|
263
|
+
return this._expressionBlock(str, exprMatch[1]);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// No closing brace — this is a parse error
|
|
267
|
+
if (!str.endsWith('}')) {
|
|
268
|
+
const [line, col] = this._getCharLocation(this.charPos);
|
|
269
|
+
throw new SluzError(`Unclosed tag <code>${str}</code> on line #${line}`, 45821);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return str;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// -------------------------------------------------------------------
|
|
276
|
+
// Block handlers
|
|
277
|
+
// -------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
_variableBlock(str) {
|
|
280
|
+
const pipeParts = this._splitRespectingQuotes(str, '|');
|
|
281
|
+
const key = pipeParts[0];
|
|
282
|
+
const modStr = pipeParts.slice(1).join('|');
|
|
283
|
+
|
|
284
|
+
if (modStr) {
|
|
285
|
+
const tmp = this._arrayDive(key, this.tplVars);
|
|
286
|
+
const isDefault = modStr.includes('default:');
|
|
287
|
+
const isNothing = this._isNothing(tmp);
|
|
288
|
+
|
|
289
|
+
if (isNothing && isDefault) {
|
|
290
|
+
const dval = modStr.replace(/^.*?default:/, '');
|
|
291
|
+
const [ret] = this._peval(dval);
|
|
292
|
+
if (ret !== undefined) return ret;
|
|
293
|
+
return '';
|
|
294
|
+
} else if (!isNothing && isDefault) {
|
|
295
|
+
return String(this._arrayDive(key, this.tplVars) ?? '');
|
|
296
|
+
} else {
|
|
297
|
+
let pre = this._arrayDive(key, this.tplVars) ?? '';
|
|
298
|
+
const parts = this._splitRespectingQuotes(modStr, '|');
|
|
299
|
+
for (const p of parts) {
|
|
300
|
+
const colonIdx = this._findFirstColonOutsideQuotes(p);
|
|
301
|
+
const func = colonIdx >= 0 ? p.slice(0, colonIdx) : p;
|
|
302
|
+
const paramStr = colonIdx >= 0 ? p.slice(colonIdx + 1) : '';
|
|
303
|
+
const params = [pre];
|
|
304
|
+
|
|
305
|
+
if (paramStr.length) {
|
|
306
|
+
const commaLimbs = this._splitRespectingQuotes(paramStr, ',');
|
|
307
|
+
for (const limb of commaLimbs) {
|
|
308
|
+
const [v] = this._peval(limb);
|
|
309
|
+
params.push(v);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const fn = this.modifiers.get(func);
|
|
314
|
+
if (!fn) {
|
|
315
|
+
const [line, col] = this._getCharLocation(this.charPos);
|
|
316
|
+
throw new SluzError(`Unknown function call <code>${func}</code> on line #${line}`, 47204);
|
|
317
|
+
}
|
|
318
|
+
pre = fn(...params);
|
|
319
|
+
}
|
|
320
|
+
return pre;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const ret = this._arrayDive(str, this.tplVars);
|
|
325
|
+
if (Array.isArray(ret)) return 'ARRAY';
|
|
326
|
+
if (ret && typeof ret === 'object') return 'HASH';
|
|
327
|
+
if (ret != null) return ret;
|
|
328
|
+
return '';
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Evaluate an {if}/{elseif}/{else} chain.
|
|
332
|
+
// Simple blocks (no {else}) use a fast regex path; complex ones go through
|
|
333
|
+
// tokenization. The first matching condition renders its payload.
|
|
334
|
+
_ifBlock(str) {
|
|
335
|
+
// True when the block has no {else} or {elseif}, so we can use a simple regex
|
|
336
|
+
const isSimple = !str.includes('{else', 7);
|
|
337
|
+
let rules = [];
|
|
338
|
+
|
|
339
|
+
if (isSimple) {
|
|
340
|
+
const m = str.match(/\{if (.+?)\}([\s\S]*)\{\/if\}/s);
|
|
341
|
+
if (m) {
|
|
342
|
+
rules = [[m[1], this._ltrimOne(m[2], '\n')]];
|
|
343
|
+
}
|
|
344
|
+
} else {
|
|
345
|
+
const toks = this._getTokens(str);
|
|
346
|
+
rules = this._ifRulesFromTokens(toks);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let ret = '';
|
|
350
|
+
for (const [cond, payload] of rules) {
|
|
351
|
+
const test = this._convertVars(cond);
|
|
352
|
+
const [res] = this._peval(test);
|
|
353
|
+
if (res) {
|
|
354
|
+
const inBlocks = this._getBlocks(payload);
|
|
355
|
+
ret += this._processBlocks(inBlocks);
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return ret;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
_foreachBlock(srcExpr, keyVar, valVar, payload) {
|
|
363
|
+
const convSrc = this._convertVars(srcExpr);
|
|
364
|
+
payload = this._ltrimOne(payload, '\n');
|
|
365
|
+
const blocks = this._getBlocks(payload);
|
|
366
|
+
|
|
367
|
+
const [src] = this._peval(convSrc);
|
|
368
|
+
|
|
369
|
+
let iterable;
|
|
370
|
+
if (src == null) {
|
|
371
|
+
iterable = [];
|
|
372
|
+
} else if (Array.isArray(src) || (typeof src === 'object' && src !== null)) {
|
|
373
|
+
iterable = src;
|
|
374
|
+
} else {
|
|
375
|
+
iterable = [src];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const save = { ...this.tplVars };
|
|
379
|
+
let ret = '';
|
|
380
|
+
let idx = 0;
|
|
381
|
+
|
|
382
|
+
if (Array.isArray(iterable)) {
|
|
383
|
+
const last = iterable.length - 1;
|
|
384
|
+
for (let i = 0; i <= last; i++) {
|
|
385
|
+
this.tplVars.__FOREACH_FIRST = idx === 0 ? 1 : 0;
|
|
386
|
+
this.tplVars.__FOREACH_LAST = idx === last ? 1 : 0;
|
|
387
|
+
this.tplVars.__FOREACH_INDEX = idx;
|
|
388
|
+
if (valVar !== undefined) {
|
|
389
|
+
this.tplVars[keyVar] = i;
|
|
390
|
+
this.tplVars[valVar] = iterable[i];
|
|
391
|
+
} else {
|
|
392
|
+
this.tplVars[keyVar] = iterable[i];
|
|
393
|
+
}
|
|
394
|
+
ret += this._processBlocks(blocks);
|
|
395
|
+
idx++;
|
|
396
|
+
}
|
|
397
|
+
} else if (typeof iterable === 'object' && iterable !== null) {
|
|
398
|
+
const keys = Object.keys(iterable);
|
|
399
|
+
const last = keys.length - 1;
|
|
400
|
+
for (let i = 0; i <= last; i++) {
|
|
401
|
+
const k = keys[i];
|
|
402
|
+
this.tplVars.__FOREACH_FIRST = idx === 0 ? 1 : 0;
|
|
403
|
+
this.tplVars.__FOREACH_LAST = idx === last ? 1 : 0;
|
|
404
|
+
this.tplVars.__FOREACH_INDEX = idx;
|
|
405
|
+
if (valVar !== undefined) {
|
|
406
|
+
this.tplVars[keyVar] = k;
|
|
407
|
+
this.tplVars[valVar] = iterable[k];
|
|
408
|
+
} else {
|
|
409
|
+
this.tplVars[keyVar] = iterable[k];
|
|
410
|
+
}
|
|
411
|
+
ret += this._processBlocks(blocks);
|
|
412
|
+
idx++;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
this.tplVars = save;
|
|
417
|
+
return ret;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
_expressionBlock(str, inner) {
|
|
421
|
+
if (!/["\d\$\(]/.test(str)) {
|
|
422
|
+
const [line, col] = this._getCharLocation(this.charPos);
|
|
423
|
+
throw new SluzError(`Unknown block type <code>${str}</code> on line #${line}`, 73467);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const after = this._convertVars(inner);
|
|
427
|
+
const [ret, err] = this._peval(after);
|
|
428
|
+
|
|
429
|
+
const valid = ret !== undefined && ret !== null && typeof ret !== 'object';
|
|
430
|
+
|
|
431
|
+
if (err || !valid) {
|
|
432
|
+
const [line, col] = this._getCharLocation(this.charPos);
|
|
433
|
+
throw new SluzError(`Unknown tag <code>${str}</code> on line #${line}`, 18933);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return ret;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// -------------------------------------------------------------------
|
|
440
|
+
// Variable / eval engine
|
|
441
|
+
// -------------------------------------------------------------------
|
|
442
|
+
|
|
443
|
+
_convertVars(str) {
|
|
444
|
+
str = String(str);
|
|
445
|
+
if (!str.includes('$')) return str;
|
|
446
|
+
return str.replace(/\$\w[\w.]*/g, match => {
|
|
447
|
+
const parts = match.slice(1).split('.');
|
|
448
|
+
const first = parts.shift();
|
|
449
|
+
let res = `__S.sluz_pfx_${first}`;
|
|
450
|
+
for (const p of parts) {
|
|
451
|
+
res += /^\d+$/.test(p) ? `[${p}]` : `.${p}`;
|
|
452
|
+
}
|
|
453
|
+
return res;
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
_microOptimize(str) {
|
|
458
|
+
if (/^-?\d+(?:\.\d+)?$/.test(str)) return str;
|
|
459
|
+
if (!str.length) return undefined;
|
|
460
|
+
|
|
461
|
+
const first = str[0];
|
|
462
|
+
const last = str[str.length - 1];
|
|
463
|
+
|
|
464
|
+
if (first === "'" && last === "'") {
|
|
465
|
+
const tmp = str.slice(1, -1);
|
|
466
|
+
if (!tmp.includes("'")) return tmp;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (first === '"' && last === '"') {
|
|
470
|
+
const tmp = str.slice(1, -1);
|
|
471
|
+
if (!tmp.includes('$') && !tmp.includes('"')) return tmp;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (/^\w+$/.test(str) && Object.prototype.hasOwnProperty.call(this.tplVars, str)) {
|
|
475
|
+
return this.tplVars[str];
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const bareNot = str.match(/^!(\w+)$/);
|
|
479
|
+
if (bareNot && Object.prototype.hasOwnProperty.call(this.tplVars, bareNot[1])) {
|
|
480
|
+
return !this.tplVars[bareNot[1]];
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return undefined;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Safely evaluate a template expression string at runtime.
|
|
487
|
+
// Returns [value, 0] on success or [undefined, -1] on error.
|
|
488
|
+
_peval(str) {
|
|
489
|
+
// Smarty uses === for equality but JS triple-equals would reject
|
|
490
|
+
// different types, so soften it to == for template compatibility
|
|
491
|
+
str = str.replace(/===/g, '==');
|
|
492
|
+
// Quick path: if the expression is a plain literal or simple reference,
|
|
493
|
+
// resolve it without invoking the Function constructor
|
|
494
|
+
const opt = this._microOptimize(str);
|
|
495
|
+
if (opt !== undefined) return [opt, 0];
|
|
496
|
+
|
|
497
|
+
// Convert template variable references ($foo) to __S_prefix_foo lookups
|
|
498
|
+
const code = this._convertVars(str);
|
|
499
|
+
// Build a Function that receives the variable scope object and any
|
|
500
|
+
// registered custom modifier functions as parameters
|
|
501
|
+
const fnNames = [...this.modifiers.keys()];
|
|
502
|
+
const fn = new Function('__S', ...fnNames, `"use strict"; return (${code})`);
|
|
503
|
+
|
|
504
|
+
// Build the scope object with prefixed keys so $foo maps to __S_foo
|
|
505
|
+
const __S = {};
|
|
506
|
+
for (const [k, v] of Object.entries(this.tplVars)) {
|
|
507
|
+
__S[`${this.varPrefix}_${k}`] = v;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const fns = fnNames.map(n => this.modifiers.get(n));
|
|
511
|
+
try {
|
|
512
|
+
return [fn(__S, ...fns), 0];
|
|
513
|
+
} catch {
|
|
514
|
+
return [undefined, -1];
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// -------------------------------------------------------------------
|
|
519
|
+
// Helpers
|
|
520
|
+
// -------------------------------------------------------------------
|
|
521
|
+
|
|
522
|
+
_arrayDive(needle, haystack) {
|
|
523
|
+
if (needle == null || haystack == null) return undefined;
|
|
524
|
+
|
|
525
|
+
if (Object.prototype.hasOwnProperty.call(haystack, needle)) {
|
|
526
|
+
return haystack[needle];
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const parts = needle.split('.');
|
|
530
|
+
let arr = haystack;
|
|
531
|
+
|
|
532
|
+
for (const elem of parts) {
|
|
533
|
+
if (arr == null) return undefined;
|
|
534
|
+
if (Array.isArray(arr)) {
|
|
535
|
+
if (!/^\d+$/.test(elem) || elem >= arr.length) return undefined;
|
|
536
|
+
arr = arr[elem];
|
|
537
|
+
} else if (typeof arr === 'object') {
|
|
538
|
+
if (!Object.prototype.hasOwnProperty.call(arr, elem)) return undefined;
|
|
539
|
+
arr = arr[elem];
|
|
540
|
+
} else {
|
|
541
|
+
return undefined;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return arr;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
_ltrimOne(str, char) {
|
|
548
|
+
if (str && str[0] === char) return str.slice(1);
|
|
549
|
+
return str;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
_findEndingTag(haystack, openTag, closeTag) {
|
|
553
|
+
let pos = haystack.indexOf(closeTag);
|
|
554
|
+
if (pos < 0) return -1;
|
|
555
|
+
|
|
556
|
+
let substr = haystack.slice(0, pos);
|
|
557
|
+
const openRe = new RegExp(escapeRegex(openTag), 'g');
|
|
558
|
+
let openCount = (substr.match(openRe) || []).length;
|
|
559
|
+
if (openCount === 1) return pos;
|
|
560
|
+
|
|
561
|
+
const closeLen = closeTag.length;
|
|
562
|
+
let offset = pos + closeLen;
|
|
563
|
+
|
|
564
|
+
for (let n = 0; n < 5; n++) {
|
|
565
|
+
pos = haystack.indexOf(closeTag, offset);
|
|
566
|
+
if (pos < 0) return -1;
|
|
567
|
+
substr = haystack.slice(0, pos + 2);
|
|
568
|
+
openCount = (substr.match(openRe) || []).length;
|
|
569
|
+
const closeCount = (substr.match(new RegExp(escapeRegex(closeTag), 'g')) || []).length;
|
|
570
|
+
if (openCount === closeCount) return pos;
|
|
571
|
+
offset = pos + closeLen;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return -1;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
_getTokens(str) {
|
|
578
|
+
return str.split(/({[^}]+})/).filter(t => t.length);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
_isIfToken(str) {
|
|
582
|
+
if (str === '{else}') return 1;
|
|
583
|
+
if (str === '{/if}') return 1;
|
|
584
|
+
const m = str.match(/^\{(?:if|elseif)\s+(.+?)\}$/);
|
|
585
|
+
if (m) return m[1];
|
|
586
|
+
return '';
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
_ifRulesFromTokens(toks) {
|
|
590
|
+
const num = toks.length;
|
|
591
|
+
let nested = 0;
|
|
592
|
+
const tmp = new Array(num);
|
|
593
|
+
|
|
594
|
+
for (let i = 0; i < num; i++) {
|
|
595
|
+
const item = toks[i];
|
|
596
|
+
if (/^\{if\b/.test(item)) nested++;
|
|
597
|
+
if (item === '{/if}') nested--;
|
|
598
|
+
|
|
599
|
+
let yes = 0;
|
|
600
|
+
if (nested === 1) {
|
|
601
|
+
yes = this._isIfToken(item) || 0;
|
|
602
|
+
if (item === '{/if}') yes = 0;
|
|
603
|
+
}
|
|
604
|
+
tmp[i] = yes;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
tmp[num - 1] = 1;
|
|
608
|
+
|
|
609
|
+
const conds = [];
|
|
610
|
+
for (let i = 0; i < num; i++) {
|
|
611
|
+
if (tmp[i]) {
|
|
612
|
+
const test = this._isIfToken(toks[i]);
|
|
613
|
+
if (i !== num - 1) conds.push(test);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
let str = '';
|
|
618
|
+
const payloads = [];
|
|
619
|
+
let first = true;
|
|
620
|
+
for (let i = 0; i < num; i++) {
|
|
621
|
+
if (tmp[i]) {
|
|
622
|
+
if (!first) payloads.push(str);
|
|
623
|
+
first = false;
|
|
624
|
+
str = '';
|
|
625
|
+
} else {
|
|
626
|
+
str += toks[i];
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (conds.length !== payloads.length) {
|
|
631
|
+
throw new SluzError(`Error parsing {if} conditions`, 95320);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const ret = [];
|
|
635
|
+
for (let i = 0; i < conds.length; i++) {
|
|
636
|
+
ret.push([conds[i], payloads[i]]);
|
|
637
|
+
}
|
|
638
|
+
return ret;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// -------------------------------------------------------------------
|
|
642
|
+
// Quote-aware splitting
|
|
643
|
+
// -------------------------------------------------------------------
|
|
644
|
+
|
|
645
|
+
_splitRespectingQuotes(str, delimiter) {
|
|
646
|
+
const parts = [];
|
|
647
|
+
let current = '';
|
|
648
|
+
let inQuote = null;
|
|
649
|
+
|
|
650
|
+
for (let i = 0; i < str.length; i++) {
|
|
651
|
+
const ch = str[i];
|
|
652
|
+
|
|
653
|
+
if (inQuote) {
|
|
654
|
+
current += ch;
|
|
655
|
+
if (ch === inQuote) inQuote = null;
|
|
656
|
+
} else if (ch === "'" || ch === '"') {
|
|
657
|
+
current += ch;
|
|
658
|
+
inQuote = ch;
|
|
659
|
+
} else if (ch === delimiter) {
|
|
660
|
+
parts.push(current);
|
|
661
|
+
current = '';
|
|
662
|
+
} else {
|
|
663
|
+
current += ch;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (current.length) parts.push(current);
|
|
668
|
+
return parts;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
_findFirstColonOutsideQuotes(str) {
|
|
672
|
+
let inQuote = null;
|
|
673
|
+
for (let i = 0; i < str.length; i++) {
|
|
674
|
+
const ch = str[i];
|
|
675
|
+
if ((ch === "'" || ch === '"') && inQuote === null) {
|
|
676
|
+
inQuote = ch;
|
|
677
|
+
} else if (ch === inQuote) {
|
|
678
|
+
inQuote = null;
|
|
679
|
+
} else if (ch === ':' && inQuote === null) {
|
|
680
|
+
return i;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return -1;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
_isNothing(v) {
|
|
687
|
+
if (v === undefined || v === null) return true;
|
|
688
|
+
if (typeof v === 'object') return false;
|
|
689
|
+
return String(v).length === 0 && v !== '0';
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// -------------------------------------------------------------------
|
|
693
|
+
// Error handling
|
|
694
|
+
// -------------------------------------------------------------------
|
|
695
|
+
|
|
696
|
+
_getCharLocation(pos) {
|
|
697
|
+
const str = this._sourceStr;
|
|
698
|
+
if (pos < 0 || !str) return [-1, -1];
|
|
699
|
+
|
|
700
|
+
let line = 1;
|
|
701
|
+
let col = 0;
|
|
702
|
+
for (let i = 0; i < str.length; i++) {
|
|
703
|
+
col++;
|
|
704
|
+
if (str[i] === '\n') {
|
|
705
|
+
line++;
|
|
706
|
+
col = 0;
|
|
707
|
+
}
|
|
708
|
+
if (pos === i) return [line, col];
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (pos === str.length) return [line, col];
|
|
712
|
+
return [-1, -1];
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export { SluzError };
|