lui-templates 0.1.0 → 0.2.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/README.md +40 -0
- package/package.json +1 -1
- package/src/cli.js +26 -1
- package/src/main.js +24 -10
- package/src/parser.js +1 -1
- package/src/parsers/html.js +345 -0
- package/.github/LIQUID_SPEC.md +0 -101
- package/.vscode/settings.json +0 -10
- package/schema/intermediary.json +0 -147
- package/test/basic.js +0 -9
- package/test/templates/article.liquid +0 -9
- package/test/templates/button.liquid +0 -3
- package/test/templates/complex-nested.liquid +0 -11
- package/test/templates/complex.liquid +0 -4
- package/test/templates/conditional-bool-attr.liquid +0 -4
- package/test/templates/conditional-nonbool-attr.liquid +0 -4
- package/test/templates/conditional-unless-attr.liquid +0 -4
- package/test/templates/conditional-unless-nonbool-attr.liquid +0 -4
- package/test/templates/conditional.liquid +0 -6
- package/test/templates/dynamic-class.liquid +0 -1
- package/test/templates/greeting.json +0 -74
- package/test/templates/image.liquid +0 -5
- package/test/templates/link.json +0 -31
- package/test/templates/mixed.liquid +0 -5
- package/test/templates/nested-attr-conditionals.liquid +0 -1
- package/test/templates/nested-comprehensive.liquid +0 -51
- package/test/templates/nested-conditionals.liquid +0 -9
- package/test/templates/nested-if-flatten.liquid +0 -6
- package/test/templates/render-comprehensive.liquid +0 -21
- package/test/templates/render-conditional.liquid +0 -6
- package/test/templates/render-expressions.liquid +0 -3
- package/test/templates/render-test.liquid +0 -7
- package/test/templates/root-text.liquid +0 -2
- package/test/templates/test-attr-cond.liquid +0 -1
- package/test/templates/text-nodes.liquid +0 -5
- package/test/templates/unless.liquid +0 -5
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Here are some templating languages I think about supporting:
|
|
|
13
13
|
- [x] [Liquid](https://shopify.github.io/liquid/) (proof of concept)
|
|
14
14
|
- [ ] [Handlebars](https://handlebarsjs.com/)
|
|
15
15
|
- [ ] [Knockout](https://knockoutjs.com/documentation/introduction.html)
|
|
16
|
+
- [x] Plain HTML (for static templates without dynamic content)
|
|
16
17
|
- [x] Raw JSON (as intermediary format, see test/templates/greeting.json)
|
|
17
18
|
|
|
18
19
|
## Usage
|
|
@@ -21,6 +22,10 @@ Here are some templating languages I think about supporting:
|
|
|
21
22
|
npx lui-templates src/templates/greeting.liquid > src/components/greeting.js
|
|
22
23
|
```
|
|
23
24
|
|
|
25
|
+
```sh
|
|
26
|
+
npx lui-templates src/templates/simple.html > src/components/simple.js
|
|
27
|
+
```
|
|
28
|
+
|
|
24
29
|
```sh
|
|
25
30
|
npx lui-templates --help
|
|
26
31
|
```
|
|
@@ -80,6 +85,41 @@ You may have it in your `.gitignore` to prevent duplication.
|
|
|
80
85
|
|
|
81
86
|
... And did I mention that this file is generated? 🎉
|
|
82
87
|
|
|
88
|
+
## HTML Example
|
|
89
|
+
|
|
90
|
+
For static templates without dynamic content, you can use plain HTML:
|
|
91
|
+
|
|
92
|
+
### `src/templates/simple.html`
|
|
93
|
+
|
|
94
|
+
```html
|
|
95
|
+
<div>
|
|
96
|
+
<h1>Hello World</h1>
|
|
97
|
+
<p>This is a simple HTML test.</p>
|
|
98
|
+
</div>
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Generated `src/components/simple.js`
|
|
102
|
+
|
|
103
|
+
```js
|
|
104
|
+
// generated by lui-templates
|
|
105
|
+
|
|
106
|
+
import {
|
|
107
|
+
hook_dom,
|
|
108
|
+
node_dom,
|
|
109
|
+
} from "lui";
|
|
110
|
+
|
|
111
|
+
export default function Simple() {
|
|
112
|
+
hook_dom("div");
|
|
113
|
+
|
|
114
|
+
return [
|
|
115
|
+
node_dom("h1[innerText=Hello World]"),
|
|
116
|
+
node_dom("p[innerText=This is a simple HTML test.]"),
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The HTML parser supports all standard HTML elements and attributes but does not include dynamic content like variables or conditionals. For dynamic content, use the Liquid parser instead.
|
|
122
|
+
|
|
83
123
|
## Interface of `lui_templates(path[, {options}])`
|
|
84
124
|
|
|
85
125
|
### `path`
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -13,7 +13,9 @@ if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
|
|
|
13
13
|
Input: The template file or directory containing the templates
|
|
14
14
|
Options:
|
|
15
15
|
-e, --externs <path> File unknown components are imported from
|
|
16
|
+
-g, --global Assume window.lui is defined
|
|
16
17
|
-h, --help Show this help message
|
|
18
|
+
--ssr '{"prop": ...}' Render to html using lui-ssr (must be installed)
|
|
17
19
|
--version Show version information
|
|
18
20
|
|
|
19
21
|
Example:
|
|
@@ -38,6 +40,8 @@ if (args[0].startsWith('-')) {
|
|
|
38
40
|
|
|
39
41
|
const path = args.shift();
|
|
40
42
|
let arg_externs = './externs.js';
|
|
43
|
+
let arg_global = false;
|
|
44
|
+
let arg_ssr = null;
|
|
41
45
|
|
|
42
46
|
while (args.length > 0) {
|
|
43
47
|
const arg = args.shift();
|
|
@@ -46,14 +50,35 @@ while (args.length > 0) {
|
|
|
46
50
|
case '--externs':
|
|
47
51
|
arg_externs = args.shift();
|
|
48
52
|
break;
|
|
53
|
+
case '--ssr':
|
|
54
|
+
arg_ssr = JSON.parse(args.shift());
|
|
55
|
+
case '-g':
|
|
56
|
+
case '--global':
|
|
57
|
+
arg_global = true;
|
|
58
|
+
break;
|
|
49
59
|
default:
|
|
50
60
|
console.error(`Unknown argument: ${arg}, see --help`);
|
|
51
61
|
process.exit(1);
|
|
52
62
|
}
|
|
53
63
|
}
|
|
54
64
|
|
|
55
|
-
|
|
65
|
+
let result = await lui_templates(path, {
|
|
56
66
|
components_name: arg_externs,
|
|
67
|
+
lui_global: arg_global,
|
|
57
68
|
});
|
|
58
69
|
|
|
70
|
+
if (arg_ssr) {
|
|
71
|
+
const {default: lui_ssr} = await import('lui-ssr');
|
|
72
|
+
const name = result.match(/function\s+(\w+)\s*\(/)?.[1];
|
|
73
|
+
|
|
74
|
+
result += `
|
|
75
|
+
lui.init(() => {
|
|
76
|
+
return [
|
|
77
|
+
lui.node(${name}, ${JSON.stringify(arg_ssr)}),
|
|
78
|
+
];
|
|
79
|
+
});`;
|
|
80
|
+
|
|
81
|
+
result = lui_ssr(result)();
|
|
82
|
+
}
|
|
83
|
+
|
|
59
84
|
console.log(result);
|
package/src/main.js
CHANGED
|
@@ -21,6 +21,7 @@ const version = JSON.parse(
|
|
|
21
21
|
|
|
22
22
|
const options_default = {
|
|
23
23
|
lui_name: 'lui',
|
|
24
|
+
lui_global: false,
|
|
24
25
|
components_name: './externs.js',
|
|
25
26
|
};
|
|
26
27
|
|
|
@@ -31,6 +32,11 @@ export default async function lui_templates(path, options = {}) {
|
|
|
31
32
|
};
|
|
32
33
|
const is_directory = (await stat(path)).isDirectory();
|
|
33
34
|
|
|
35
|
+
if (
|
|
36
|
+
is_directory &&
|
|
37
|
+
options.lui_global
|
|
38
|
+
) throw new Error('with global setting, only a single template is allowed');
|
|
39
|
+
|
|
34
40
|
const paths = is_directory
|
|
35
41
|
? (await readdir(path)).map(name => `${path}/${name}`)
|
|
36
42
|
: [path];
|
|
@@ -44,29 +50,37 @@ export default async function lui_templates(path, options = {}) {
|
|
|
44
50
|
const result = [];
|
|
45
51
|
|
|
46
52
|
for (const [name, parsed] of all_parsed) {
|
|
47
|
-
|
|
53
|
+
let expression = generate(name, parsed, lui_imports, component_imports);
|
|
54
|
+
|
|
55
|
+
if (!options.lui_global) {
|
|
56
|
+
if (!is_directory) expression = 'default ' + expression;
|
|
57
|
+
expression = 'export ' + expression;
|
|
58
|
+
}
|
|
48
59
|
|
|
49
|
-
result.push(
|
|
50
|
-
'export ' +
|
|
51
|
-
(is_directory ? '' : 'default ') +
|
|
52
|
-
expression
|
|
53
|
-
);
|
|
60
|
+
result.push(expression);
|
|
54
61
|
}
|
|
55
62
|
|
|
56
63
|
// write component_imports to the top of the file
|
|
57
64
|
for (const [name] of all_parsed) {
|
|
58
65
|
component_imports.delete(name);
|
|
59
66
|
}
|
|
60
|
-
if (component_imports.size > 0) {
|
|
67
|
+
if (component_imports.size > 0 && !options.lui_global) {
|
|
61
68
|
result.unshift(`import {${
|
|
62
69
|
list_generate([...component_imports], 0)
|
|
63
70
|
}} from ${JSON.stringify(options.components_name)};`);
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
// write lui_imports to the top of the file
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
if (options.lui_global) {
|
|
75
|
+
result.unshift(`const {${
|
|
76
|
+
list_generate([...lui_imports], 0)
|
|
77
|
+
}} = ${options.lui_name};`);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
result.unshift(`import {${
|
|
81
|
+
list_generate([...lui_imports], 0)
|
|
82
|
+
}} from ${JSON.stringify(options.lui_name)};`);
|
|
83
|
+
}
|
|
70
84
|
|
|
71
85
|
result.unshift('// generated by lui-templates ' + version);
|
|
72
86
|
|
package/src/parser.js
CHANGED
|
@@ -47,7 +47,7 @@ export function html_attr_to_dom(attr) {
|
|
|
47
47
|
return html_attr_to_dom_map.get(attr) || attr;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const html_whitespaces = ' \n\r\t\f\v'.split('');
|
|
50
|
+
export const html_whitespaces = ' \n\r\t\f\v'.split('');
|
|
51
51
|
export const html_is_whitespace = char => html_whitespaces.includes(char);
|
|
52
52
|
|
|
53
53
|
const html_self_closing = 'img,input,br,hr'.split(',');
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NODE_TYPE_ELEMENT,
|
|
3
|
+
VALUE_TYPE_STATIC,
|
|
4
|
+
} from '../constants.js';
|
|
5
|
+
import {
|
|
6
|
+
html_is_self_closing,
|
|
7
|
+
html_is_whitespace,
|
|
8
|
+
html_attr_to_dom,
|
|
9
|
+
html_whitespaces,
|
|
10
|
+
} from '../parser.js';
|
|
11
|
+
|
|
12
|
+
const TOKEN_HTML_START = 0;
|
|
13
|
+
const TOKEN_HTML_END = 1;
|
|
14
|
+
const TOKEN_TEXT = 2;
|
|
15
|
+
|
|
16
|
+
export default async function parse_html(src, path) {
|
|
17
|
+
const tokenizer = new Tokenizer(src, path);
|
|
18
|
+
const tokens = tokenizer.parse_nodes();
|
|
19
|
+
const nodes = build_nodes(tokens, 0, tokens.length);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
inputs: [],
|
|
23
|
+
transformations: [],
|
|
24
|
+
effects: [],
|
|
25
|
+
nodes,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function error(message, obj) {
|
|
30
|
+
console.error(`Error in ${obj.path}:${obj.line}:${obj.column}: ${message}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class Tokenizer {
|
|
35
|
+
constructor(src, path) {
|
|
36
|
+
this.src = src;
|
|
37
|
+
this.path = path;
|
|
38
|
+
this.line = 1;
|
|
39
|
+
this.column = 0;
|
|
40
|
+
this.index = 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
char_current() {
|
|
44
|
+
return this.src.charAt(this.index);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
chars_match(chars) {
|
|
48
|
+
return this.src.slice(this.index, this.index + chars.length) === chars;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
char_step() {
|
|
52
|
+
const char = this.char_current();
|
|
53
|
+
if (char == null) error('Unexpected end of input', this);
|
|
54
|
+
this.index++;
|
|
55
|
+
if (char === '\n') {
|
|
56
|
+
this.line++;
|
|
57
|
+
this.column = 0;
|
|
58
|
+
}
|
|
59
|
+
else this.column++;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
chars_step(n) {
|
|
63
|
+
for (let i = 0; i < n; i++) {
|
|
64
|
+
this.char_step();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
chars_consume(chars) {
|
|
69
|
+
if (!this.chars_match(chars)) error(`Expected "${chars}"`, this);
|
|
70
|
+
this.chars_step(chars.length);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
chars_consume_until(limit, desc) {
|
|
74
|
+
const index_end = this.src.indexOf(limit, this.index);
|
|
75
|
+
if (index_end === -1) error(`Unclosed ${desc}`, this);
|
|
76
|
+
const value = this.src.slice(this.index, index_end);
|
|
77
|
+
this.chars_step(index_end - this.index);
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
chars_consume_until_one(limits, desc) {
|
|
82
|
+
let value = '';
|
|
83
|
+
while (this.index < this.src.length) {
|
|
84
|
+
const char = this.char_current();
|
|
85
|
+
if (limits.includes(char)) return value;
|
|
86
|
+
value += char;
|
|
87
|
+
this.char_step();
|
|
88
|
+
}
|
|
89
|
+
error(`Unclosed ${desc}`, this);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
chars_skip_whitespace() {
|
|
93
|
+
while (
|
|
94
|
+
this.index < this.src.length &&
|
|
95
|
+
html_is_whitespace(this.char_current())
|
|
96
|
+
) {
|
|
97
|
+
this.char_step();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
parse_nodes() {
|
|
102
|
+
const tokens = [];
|
|
103
|
+
|
|
104
|
+
while (this.index < this.src.length) {
|
|
105
|
+
const char = this.char_current();
|
|
106
|
+
const position = {
|
|
107
|
+
path: this.path,
|
|
108
|
+
line: this.line,
|
|
109
|
+
column: this.column,
|
|
110
|
+
};
|
|
111
|
+
// text nodes
|
|
112
|
+
if (char !== '<') {
|
|
113
|
+
// we do this manually since text can also end at EOF
|
|
114
|
+
const index_lt = this.src.indexOf('<', this.index);
|
|
115
|
+
const value = this.src.slice(
|
|
116
|
+
this.index,
|
|
117
|
+
index_lt > -1
|
|
118
|
+
? index_lt
|
|
119
|
+
: this.src.length
|
|
120
|
+
);
|
|
121
|
+
this.chars_step(value.length);
|
|
122
|
+
tokens.push({
|
|
123
|
+
type: TOKEN_TEXT,
|
|
124
|
+
...position,
|
|
125
|
+
value,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
// comment
|
|
129
|
+
else if (this.chars_match('<!--')) {
|
|
130
|
+
this.chars_consume_until('-->', 'comment');
|
|
131
|
+
}
|
|
132
|
+
// end tag
|
|
133
|
+
else if (this.chars_match('</')) {
|
|
134
|
+
this.chars_consume('</');
|
|
135
|
+
|
|
136
|
+
const tag_name = this.chars_consume_until('>', 'end tag');
|
|
137
|
+
if (!tag_name) error('empty end tag', position);
|
|
138
|
+
if (tag_name !== tag_name.trim()) error('spaces in end tag', position);
|
|
139
|
+
|
|
140
|
+
this.chars_consume('>');
|
|
141
|
+
|
|
142
|
+
tokens.push({
|
|
143
|
+
type: TOKEN_HTML_END,
|
|
144
|
+
...position,
|
|
145
|
+
tag_name,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
// start tag
|
|
149
|
+
else {
|
|
150
|
+
tokens.push(
|
|
151
|
+
...this.parse_html_start(position)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return tokens;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
parse_html_start(position) {
|
|
160
|
+
this.chars_consume('<');
|
|
161
|
+
const tag_name = this.chars_consume_until_one(
|
|
162
|
+
['/', '>', ...html_whitespaces],
|
|
163
|
+
'tag'
|
|
164
|
+
);
|
|
165
|
+
if (!tag_name) error('empty start tag or spaces before tag name', position);
|
|
166
|
+
|
|
167
|
+
const props = this.parse_attributes();
|
|
168
|
+
|
|
169
|
+
this.chars_skip_whitespace();
|
|
170
|
+
const has_self_closing_slash = this.char_current() === '/';
|
|
171
|
+
if (has_self_closing_slash) {
|
|
172
|
+
this.chars_consume('/');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
this.chars_consume('>');
|
|
176
|
+
|
|
177
|
+
const start_token = {
|
|
178
|
+
type: TOKEN_HTML_START,
|
|
179
|
+
...position,
|
|
180
|
+
tag_name,
|
|
181
|
+
props,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
if (
|
|
185
|
+
has_self_closing_slash ||
|
|
186
|
+
html_is_self_closing(tag_name)
|
|
187
|
+
) {
|
|
188
|
+
return [
|
|
189
|
+
start_token,
|
|
190
|
+
{
|
|
191
|
+
type: TOKEN_HTML_END,
|
|
192
|
+
...position,
|
|
193
|
+
tag_name,
|
|
194
|
+
}
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return [start_token];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
parse_attributes() {
|
|
202
|
+
const props = {};
|
|
203
|
+
|
|
204
|
+
while (this.index < this.src.length) {
|
|
205
|
+
this.chars_skip_whitespace();
|
|
206
|
+
|
|
207
|
+
if (['/', '>'].includes(this.char_current())) return props;
|
|
208
|
+
|
|
209
|
+
const name = this.chars_consume_until_one(
|
|
210
|
+
['=', '/', '>', ...html_whitespaces],
|
|
211
|
+
'attribute name'
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
if (!name) error('attribute name missing', this);
|
|
215
|
+
|
|
216
|
+
this.chars_skip_whitespace();
|
|
217
|
+
|
|
218
|
+
let value = true;
|
|
219
|
+
if (this.char_current() === '=') {
|
|
220
|
+
this.chars_consume('=');
|
|
221
|
+
|
|
222
|
+
this.chars_skip_whitespace();
|
|
223
|
+
|
|
224
|
+
const quote = this.char_current();
|
|
225
|
+
if (quote === '"' || quote === "'") {
|
|
226
|
+
this.chars_consume(quote);
|
|
227
|
+
value = this.chars_consume_until(quote, 'attribute value');
|
|
228
|
+
this.chars_consume(quote);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
value = this.chars_consume_until_one(
|
|
232
|
+
['/', '>', ...html_whitespaces],
|
|
233
|
+
'attribute value'
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
props[html_attr_to_dom(name)] = {
|
|
239
|
+
type: VALUE_TYPE_STATIC,
|
|
240
|
+
data: value,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
error('Unclosed tag', this);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function build_nodes(tokens, index, index_end) {
|
|
249
|
+
const nodes = [];
|
|
250
|
+
for (; index < index_end; index++) {
|
|
251
|
+
const token = tokens[index];
|
|
252
|
+
switch (token.type) {
|
|
253
|
+
case TOKEN_HTML_START: {
|
|
254
|
+
const {tag_name, props} = token;
|
|
255
|
+
const index_start = ++index;
|
|
256
|
+
|
|
257
|
+
// find matching end tag index
|
|
258
|
+
let depth = 1;
|
|
259
|
+
loop: for (; index < index_end; index++) {
|
|
260
|
+
const token = tokens[index];
|
|
261
|
+
switch (token.type) {
|
|
262
|
+
case TOKEN_HTML_START:
|
|
263
|
+
if (token.tag_name === tag_name) depth++;
|
|
264
|
+
break;
|
|
265
|
+
case TOKEN_HTML_END:
|
|
266
|
+
if (
|
|
267
|
+
token.tag_name === tag_name &&
|
|
268
|
+
--depth === 0
|
|
269
|
+
) {
|
|
270
|
+
break loop;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (depth > 0) error(`Unclosed tag <${tag_name}>`, token);
|
|
275
|
+
|
|
276
|
+
const children = build_nodes(tokens, index_start, index);
|
|
277
|
+
|
|
278
|
+
if (
|
|
279
|
+
children.length > 0 &&
|
|
280
|
+
children[0].is_wrapper
|
|
281
|
+
) {
|
|
282
|
+
props.innerText = children.shift().props.innerText;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
nodes.push({
|
|
286
|
+
is_wrapper: false,
|
|
287
|
+
type: NODE_TYPE_ELEMENT,
|
|
288
|
+
tag: tag_name,
|
|
289
|
+
props,
|
|
290
|
+
children,
|
|
291
|
+
});
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
case TOKEN_HTML_END:
|
|
295
|
+
error(`Unexpected closing tag </${token.tag_name}>`, token);
|
|
296
|
+
case TOKEN_TEXT: {
|
|
297
|
+
let {value} = token;
|
|
298
|
+
if (!value) break;
|
|
299
|
+
|
|
300
|
+
const value_trimmed_start = value.trimStart();
|
|
301
|
+
// if first node
|
|
302
|
+
if (nodes.length === 0) {
|
|
303
|
+
if (!value_trimmed_start) break;
|
|
304
|
+
value = value_trimmed_start;
|
|
305
|
+
}
|
|
306
|
+
// if between other nodes and empty
|
|
307
|
+
else if (
|
|
308
|
+
index_end - index > 1 &&
|
|
309
|
+
!value_trimmed_start
|
|
310
|
+
) {
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
// if not first but has leading whitespace
|
|
314
|
+
else if (value !== value_trimmed_start) {
|
|
315
|
+
value = ' ' + value_trimmed_start;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const value_trimmed_end = value.trimEnd();
|
|
319
|
+
// if last node
|
|
320
|
+
if (index_end - index === 1) {
|
|
321
|
+
if (!value_trimmed_end) break;
|
|
322
|
+
value = value_trimmed_end;
|
|
323
|
+
}
|
|
324
|
+
// if not last but has trailing whitespace
|
|
325
|
+
else if (value !== value_trimmed_end) {
|
|
326
|
+
value = value_trimmed_end + ' ';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
nodes.push({
|
|
330
|
+
is_wrapper: true,
|
|
331
|
+
type: NODE_TYPE_ELEMENT,
|
|
332
|
+
tag: 'span',
|
|
333
|
+
props: {
|
|
334
|
+
innerText: {
|
|
335
|
+
type: VALUE_TYPE_STATIC,
|
|
336
|
+
data: value,
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
children: [],
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return nodes;
|
|
345
|
+
}
|
package/.github/LIQUID_SPEC.md
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# Liquid Template Syntax Specification
|
|
2
|
-
|
|
3
|
-
This document describes the Liquid template syntax used in this project. AI agents and contributors should follow these specifications when working with Liquid templates.
|
|
4
|
-
|
|
5
|
-
## Syntax Overview
|
|
6
|
-
|
|
7
|
-
Liquid uses two types of delimiters:
|
|
8
|
-
|
|
9
|
-
### 1. Output (Expressions): `{{ }}`
|
|
10
|
-
Used for outputting variables and expressions:
|
|
11
|
-
```liquid
|
|
12
|
-
{{ variable }}
|
|
13
|
-
{{ user.name }}
|
|
14
|
-
{{ "Hello " + name }}
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
### 2. Tags (Commands): `{% %}`
|
|
18
|
-
Used for logic, control flow, and special commands:
|
|
19
|
-
```liquid
|
|
20
|
-
{% if condition %}
|
|
21
|
-
Content
|
|
22
|
-
{% endif %}
|
|
23
|
-
|
|
24
|
-
{% unless condition %}
|
|
25
|
-
Content
|
|
26
|
-
{% endunless %}
|
|
27
|
-
|
|
28
|
-
{% for item in items %}
|
|
29
|
-
{{ item }}
|
|
30
|
-
{% endfor %}
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
## Supported Commands
|
|
34
|
-
|
|
35
|
-
### Control Flow Commands
|
|
36
|
-
- `{% if condition %}...{% endif %}` - Conditional rendering
|
|
37
|
-
- `{% unless condition %}...{% endunless %}` - Inverse conditional rendering
|
|
38
|
-
- `{% for item in collection %}...{% endfor %}` - Loop over collections
|
|
39
|
-
|
|
40
|
-
### Special Commands
|
|
41
|
-
- `{% comment %}...{% endcomment %}` - Comments (not rendered)
|
|
42
|
-
- `{% raw %}...{% endraw %}` - Raw content (no processing)
|
|
43
|
-
- `{% render 'component', prop: value %}` - Render a component
|
|
44
|
-
|
|
45
|
-
## Component Rendering
|
|
46
|
-
|
|
47
|
-
The `{% render %}` command is used to instantiate components:
|
|
48
|
-
|
|
49
|
-
```liquid
|
|
50
|
-
{% render 'button', label: 'Submit', type: 'primary' %}
|
|
51
|
-
{% render 'user-card', name: user.name, age: 25 %}
|
|
52
|
-
{% render 'components/ui/icon', name: 'check', size: 24 %}
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### Syntax
|
|
56
|
-
```liquid
|
|
57
|
-
{% render 'path/to/component', prop1: value1, prop2: value2 %}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### Rules
|
|
61
|
-
1. **Path**: The component path (string). Only the last segment is used (e.g., `'ui/button'` → `Button`)
|
|
62
|
-
2. **Component Name**: Converted to PascalCase (e.g., `'user-card'` → `UserCard`)
|
|
63
|
-
3. **Props**: Key-value pairs separated by commas
|
|
64
|
-
4. **Values**: Can be:
|
|
65
|
-
- String literals: `'text'` or `"text"`
|
|
66
|
-
- Numbers: `42`, `3.14`
|
|
67
|
-
- Booleans: `true`, `false`
|
|
68
|
-
- Variables: `userName`, `count`
|
|
69
|
-
- Expressions: `user.name`, `items.length`
|
|
70
|
-
|
|
71
|
-
### Examples
|
|
72
|
-
|
|
73
|
-
Simple with static values:
|
|
74
|
-
```liquid
|
|
75
|
-
{% render 'button', label: 'Click me', disabled: false %}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
With variables:
|
|
79
|
-
```liquid
|
|
80
|
-
{% render 'input', name: fieldName, value: fieldValue %}
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
With expressions:
|
|
84
|
-
```liquid
|
|
85
|
-
{% render 'user-card', name: user.name, count: items.length %}
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
Within conditionals:
|
|
89
|
-
```liquid
|
|
90
|
-
{% if showProfile %}
|
|
91
|
-
{% render 'profile', user: currentUser %}
|
|
92
|
-
{% endif %}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## Important Notes for AI Agents
|
|
96
|
-
|
|
97
|
-
1. **Always use `{% %}` for the render command**, not `{{ }}`
|
|
98
|
-
2. The render command is a **tag/command**, not an expression
|
|
99
|
-
3. Component names are automatically converted from kebab-case to PascalCase
|
|
100
|
-
4. Only simple variable identifiers are registered as component inputs
|
|
101
|
-
5. Complex expressions (e.g., `user.name`) are passed through but not registered as inputs
|
package/.vscode/settings.json
DELETED
package/schema/intermediary.json
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://json-schema.org/draft-07/schema",
|
|
3
|
-
"$id": "https://l3p3.de/shr/schema/lui-templates.intermediary.json",
|
|
4
|
-
"$title": "Intermediary format for templates",
|
|
5
|
-
"$description": "All templates should be parsed into this format before being turned into a component. You can also pass a template.json directly, following this schema.",
|
|
6
|
-
|
|
7
|
-
"type": "object",
|
|
8
|
-
"properties": {
|
|
9
|
-
"inputs": {
|
|
10
|
-
"$description": "list of component prop definitions",
|
|
11
|
-
"type": "array",
|
|
12
|
-
"items": {"$ref": "#/$defs/input"}
|
|
13
|
-
},
|
|
14
|
-
"transformations": {
|
|
15
|
-
"$description": "list of transformations",
|
|
16
|
-
"type": "array",
|
|
17
|
-
"items": {"$ref": "#/$defs/transformation"}
|
|
18
|
-
},
|
|
19
|
-
"effects": {
|
|
20
|
-
"$description": "list of effects",
|
|
21
|
-
"type": "array",
|
|
22
|
-
"items": {"$ref": "#/$defs/effect"}
|
|
23
|
-
},
|
|
24
|
-
"nodes": {
|
|
25
|
-
"$description": "list of contained nodes",
|
|
26
|
-
"type": "array",
|
|
27
|
-
"items": {"$ref": "#/$defs/node"}
|
|
28
|
-
}
|
|
29
|
-
},
|
|
30
|
-
"required": ["inputs", "transformations", "effects", "nodes"],
|
|
31
|
-
|
|
32
|
-
"$defs": {
|
|
33
|
-
"value": {
|
|
34
|
-
"$description": "expression representing a runtime value",
|
|
35
|
-
"oneOf": [
|
|
36
|
-
{
|
|
37
|
-
"$description": "static value",
|
|
38
|
-
"type": "object",
|
|
39
|
-
"properties": {
|
|
40
|
-
"type": {"const": 0},
|
|
41
|
-
"data": {"type": ["string", "number", "boolean", "null"]}
|
|
42
|
-
},
|
|
43
|
-
"required": ["type", "data"]
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
"$description": "field reference",
|
|
47
|
-
"type": "object",
|
|
48
|
-
"properties": {
|
|
49
|
-
"type": {"const": 1},
|
|
50
|
-
"data": {"type": "string"}
|
|
51
|
-
},
|
|
52
|
-
"required": ["type", "data"]
|
|
53
|
-
},
|
|
54
|
-
{
|
|
55
|
-
"$description": "string concatenation",
|
|
56
|
-
"type": "object",
|
|
57
|
-
"properties": {
|
|
58
|
-
"type": {"const": 2},
|
|
59
|
-
"data": {
|
|
60
|
-
"type": "array",
|
|
61
|
-
"items": {"$ref": "#/$defs/value"}
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
"required": ["type", "data"]
|
|
65
|
-
}
|
|
66
|
-
]
|
|
67
|
-
},
|
|
68
|
-
"props": {
|
|
69
|
-
"$description": "map of arbitrary property keys and their assigned values",
|
|
70
|
-
"type": "object",
|
|
71
|
-
"additionalProperties": {"$ref": "#/$defs/value"}
|
|
72
|
-
},
|
|
73
|
-
"input": {
|
|
74
|
-
"$description": "component prop definition",
|
|
75
|
-
"type": "object",
|
|
76
|
-
"properties": {
|
|
77
|
-
"name": {"type": "string"},
|
|
78
|
-
"fallback": {"type": ["string", "number", "boolean", "null"]}
|
|
79
|
-
},
|
|
80
|
-
"required": ["name"]
|
|
81
|
-
},
|
|
82
|
-
"transformation": {
|
|
83
|
-
"$description": "transformation from a list of fields into a single field",
|
|
84
|
-
"type": "object",
|
|
85
|
-
"properties": {}
|
|
86
|
-
},
|
|
87
|
-
"effect": {
|
|
88
|
-
"$description": "effect to be run depending on field values",
|
|
89
|
-
"type": "object",
|
|
90
|
-
"properties": {}
|
|
91
|
-
},
|
|
92
|
-
"node": {
|
|
93
|
-
"$description": "node in the template tree",
|
|
94
|
-
"oneOf": [
|
|
95
|
-
{
|
|
96
|
-
"$description": "single component",
|
|
97
|
-
"type": "object",
|
|
98
|
-
"properties": {
|
|
99
|
-
"type": {"const": 0},
|
|
100
|
-
"component": {"type": "string"},
|
|
101
|
-
"props": {"$ref": "#/$defs/props"},
|
|
102
|
-
"children": {
|
|
103
|
-
"type": "array",
|
|
104
|
-
"items": {"$ref": "#/$defs/node"}
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
"required": ["type", "component", "props", "children"]
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
"$description": "html element",
|
|
111
|
-
"type": "object",
|
|
112
|
-
"properties": {
|
|
113
|
-
"type": {"const": 1},
|
|
114
|
-
"tag": {"type": "string"},
|
|
115
|
-
"props": {"$ref": "#/$defs/props"},
|
|
116
|
-
"children": {
|
|
117
|
-
"type": "array",
|
|
118
|
-
"items": {"$ref": "#/$defs/node"}
|
|
119
|
-
}
|
|
120
|
-
},
|
|
121
|
-
"required": ["type", "tag", "props", "children"]
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
"$description": "if statement",
|
|
125
|
-
"type": "object",
|
|
126
|
-
"properties": {
|
|
127
|
-
"type": {"const": 2},
|
|
128
|
-
"condition": {"$ref": "#/$defs/value"},
|
|
129
|
-
"child": {"$ref": "#/$defs/node"}
|
|
130
|
-
},
|
|
131
|
-
"required": ["type", "condition", "child"]
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
"$description": "map component",
|
|
135
|
-
"type": "object",
|
|
136
|
-
"properties": {
|
|
137
|
-
"type": {"const": 3},
|
|
138
|
-
"from": {"type": "string"},
|
|
139
|
-
"component": {"type": "string"},
|
|
140
|
-
"props": {"$ref": "#/$defs/props"}
|
|
141
|
-
},
|
|
142
|
-
"required": ["type", "from", "component", "props"]
|
|
143
|
-
}
|
|
144
|
-
]
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
package/test/basic.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import lui_templates from '../src/main.js';
|
|
2
|
-
|
|
3
|
-
const test = async () => {
|
|
4
|
-
const file = process.argv[2]; // Optional: pass specific file via argv
|
|
5
|
-
const options = file ? { file } : {};
|
|
6
|
-
const result = await lui_templates('templates', options);
|
|
7
|
-
console.log(result);
|
|
8
|
-
}
|
|
9
|
-
test();
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<button class="button-{{ color }} btn-{{ size }}">{{ label }}</button>
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://l3p3.de/shr/schema/lui-templates.intermediary.json",
|
|
3
|
-
"inputs": [
|
|
4
|
-
{
|
|
5
|
-
"name": "name",
|
|
6
|
-
"fallback": "unknown"
|
|
7
|
-
}
|
|
8
|
-
],
|
|
9
|
-
"transformations": [],
|
|
10
|
-
"effects": [],
|
|
11
|
-
"nodes": [
|
|
12
|
-
{
|
|
13
|
-
"type": 1,
|
|
14
|
-
"tag": "div",
|
|
15
|
-
"props": {
|
|
16
|
-
"title": {
|
|
17
|
-
"type": 0,
|
|
18
|
-
"data": "Just a test"
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
"children": [
|
|
22
|
-
{
|
|
23
|
-
"type": 2,
|
|
24
|
-
"condition": {
|
|
25
|
-
"type": 1,
|
|
26
|
-
"data": "name"
|
|
27
|
-
},
|
|
28
|
-
"child": {
|
|
29
|
-
"type": 1,
|
|
30
|
-
"tag": "h1",
|
|
31
|
-
"props": {
|
|
32
|
-
"innerText": {
|
|
33
|
-
"type": 2,
|
|
34
|
-
"data": [
|
|
35
|
-
{
|
|
36
|
-
"type": 0,
|
|
37
|
-
"data": "Hello "
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
"type": 1,
|
|
41
|
-
"data": "name"
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
"type": 0,
|
|
45
|
-
"data": "!"
|
|
46
|
-
}
|
|
47
|
-
]
|
|
48
|
-
}
|
|
49
|
-
},
|
|
50
|
-
"children": []
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
"type": 1,
|
|
55
|
-
"tag": "p",
|
|
56
|
-
"props": {},
|
|
57
|
-
"children": [
|
|
58
|
-
{
|
|
59
|
-
"type": 0,
|
|
60
|
-
"component": "Link",
|
|
61
|
-
"props": {
|
|
62
|
-
"text": {
|
|
63
|
-
"type": 0,
|
|
64
|
-
"data": "Click me!"
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
"children": []
|
|
68
|
-
}
|
|
69
|
-
]
|
|
70
|
-
}
|
|
71
|
-
]
|
|
72
|
-
}
|
|
73
|
-
]
|
|
74
|
-
}
|
package/test/templates/link.json
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://l3p3.de/shr/schema/lui-templates.intermediary.json",
|
|
3
|
-
"inputs": [
|
|
4
|
-
{
|
|
5
|
-
"name": "href",
|
|
6
|
-
"fallback": "#"
|
|
7
|
-
},
|
|
8
|
-
{
|
|
9
|
-
"name": "text"
|
|
10
|
-
}
|
|
11
|
-
],
|
|
12
|
-
"transformations": [],
|
|
13
|
-
"effects": [],
|
|
14
|
-
"nodes": [
|
|
15
|
-
{
|
|
16
|
-
"type": 1,
|
|
17
|
-
"tag": "a",
|
|
18
|
-
"props": {
|
|
19
|
-
"href": {
|
|
20
|
-
"type": 1,
|
|
21
|
-
"data": "href"
|
|
22
|
-
},
|
|
23
|
-
"innerText": {
|
|
24
|
-
"type": 1,
|
|
25
|
-
"data": "text"
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
"children": []
|
|
29
|
-
}
|
|
30
|
-
]
|
|
31
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<button {% if showClass %}class="{% if primary %}btn-primary{% endif %}"{% endif %}>Click</button>
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
<div>
|
|
2
|
-
<!-- Test 1: Nested conditionals around nodes -->
|
|
3
|
-
{% if outerCondition %}
|
|
4
|
-
{% if innerCondition %}
|
|
5
|
-
<p>Both conditions true</p>
|
|
6
|
-
<span>Second element</span>
|
|
7
|
-
{% endif %}
|
|
8
|
-
{% endif %}
|
|
9
|
-
|
|
10
|
-
<!-- Test 2: Nested conditionals around text nodes -->
|
|
11
|
-
{% if showText %}
|
|
12
|
-
{% if emphasize %}
|
|
13
|
-
Strong text here!
|
|
14
|
-
{% endif %}
|
|
15
|
-
{% endif %}
|
|
16
|
-
|
|
17
|
-
<!-- Test 3: Triple nesting -->
|
|
18
|
-
{% if level1 %}
|
|
19
|
-
{% if level2 %}
|
|
20
|
-
{% if level3 %}
|
|
21
|
-
<p>All three levels true</p>
|
|
22
|
-
{% endif %}
|
|
23
|
-
{% endif %}
|
|
24
|
-
{% endif %}
|
|
25
|
-
|
|
26
|
-
<!-- Test 4: Mixed if and unless -->
|
|
27
|
-
{% if enabled %}
|
|
28
|
-
{% unless disabled %}
|
|
29
|
-
<button>Click me</button>
|
|
30
|
-
{% endunless %}
|
|
31
|
-
{% endif %}
|
|
32
|
-
|
|
33
|
-
<!-- Test 5: Regular conditional attributes (should work as before) -->
|
|
34
|
-
<input {% if isRequired %}required{% endif %} type="text">
|
|
35
|
-
|
|
36
|
-
<!-- Test 6: Nested conditionals in attribute values -->
|
|
37
|
-
<div {% if hasClass %}class="{% if isPrimary %}primary{% endif %} btn"{% endif %}>Button</div>
|
|
38
|
-
|
|
39
|
-
<!-- Test 7: Complex nested in attribute value -->
|
|
40
|
-
<button class="btn {% if isActive %}active{% endif %} {% if isDisabled %}disabled{% endif %}">Multi-class</button>
|
|
41
|
-
|
|
42
|
-
<!-- Test 8: Conditional boolean attribute with nested condition -->
|
|
43
|
-
<button {% if outerCheck %}{% if innerCheck %}disabled{% endif %}{% endif %}>Nested boolean attr</button>
|
|
44
|
-
|
|
45
|
-
<!-- Test 9: Text with variables inside nested conditionals -->
|
|
46
|
-
{% if showGreeting %}
|
|
47
|
-
{% if showName %}
|
|
48
|
-
Hello {{ userName }}!
|
|
49
|
-
{% endif %}
|
|
50
|
-
{% endif %}
|
|
51
|
-
</div>
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
<div>
|
|
2
|
-
<!-- Simple render with static values -->
|
|
3
|
-
{% render 'button', label: 'Submit', type: 'primary' %}
|
|
4
|
-
|
|
5
|
-
<!-- Render with variables -->
|
|
6
|
-
{% render 'input', name: fieldName, value: fieldValue %}
|
|
7
|
-
|
|
8
|
-
<!-- Render with complex expressions -->
|
|
9
|
-
{% render 'user-card', name: user.name, age: user.age %}
|
|
10
|
-
|
|
11
|
-
<!-- Render with nested paths (only last part is used) -->
|
|
12
|
-
{% render 'components/ui/icon', name: 'check', size: 24 %}
|
|
13
|
-
|
|
14
|
-
<!-- Render with boolean and number literals -->
|
|
15
|
-
{% render 'checkbox', checked: true, count: 5 %}
|
|
16
|
-
|
|
17
|
-
<!-- Render within conditionals -->
|
|
18
|
-
{% if showProfile %}
|
|
19
|
-
{% render 'profile', user: currentUser %}
|
|
20
|
-
{% endif %}
|
|
21
|
-
</div>
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
<div>
|
|
2
|
-
{% render 'button', label: 'Click me', color: 'blue' %}
|
|
3
|
-
{% render 'components/nested/icon', name: iconName, size: 24 %}
|
|
4
|
-
<p>Some text</p>
|
|
5
|
-
{% render 'link', href: url, text: linkText %}
|
|
6
|
-
{% render 'card', title: 'Test', enabled: true, count: 5 %}
|
|
7
|
-
</div>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<button class="btn {% if isActive %}active{% endif %}">Test</button>
|