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.
Files changed (36) hide show
  1. package/README.md +40 -0
  2. package/package.json +1 -1
  3. package/src/cli.js +26 -1
  4. package/src/main.js +24 -10
  5. package/src/parser.js +1 -1
  6. package/src/parsers/html.js +345 -0
  7. package/.github/LIQUID_SPEC.md +0 -101
  8. package/.vscode/settings.json +0 -10
  9. package/schema/intermediary.json +0 -147
  10. package/test/basic.js +0 -9
  11. package/test/templates/article.liquid +0 -9
  12. package/test/templates/button.liquid +0 -3
  13. package/test/templates/complex-nested.liquid +0 -11
  14. package/test/templates/complex.liquid +0 -4
  15. package/test/templates/conditional-bool-attr.liquid +0 -4
  16. package/test/templates/conditional-nonbool-attr.liquid +0 -4
  17. package/test/templates/conditional-unless-attr.liquid +0 -4
  18. package/test/templates/conditional-unless-nonbool-attr.liquid +0 -4
  19. package/test/templates/conditional.liquid +0 -6
  20. package/test/templates/dynamic-class.liquid +0 -1
  21. package/test/templates/greeting.json +0 -74
  22. package/test/templates/image.liquid +0 -5
  23. package/test/templates/link.json +0 -31
  24. package/test/templates/mixed.liquid +0 -5
  25. package/test/templates/nested-attr-conditionals.liquid +0 -1
  26. package/test/templates/nested-comprehensive.liquid +0 -51
  27. package/test/templates/nested-conditionals.liquid +0 -9
  28. package/test/templates/nested-if-flatten.liquid +0 -6
  29. package/test/templates/render-comprehensive.liquid +0 -21
  30. package/test/templates/render-conditional.liquid +0 -6
  31. package/test/templates/render-expressions.liquid +0 -3
  32. package/test/templates/render-test.liquid +0 -7
  33. package/test/templates/root-text.liquid +0 -2
  34. package/test/templates/test-attr-cond.liquid +0 -1
  35. package/test/templates/text-nodes.liquid +0 -5
  36. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lui-templates",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "transform html templates into lui components",
5
5
  "repository": {
6
6
  "type": "git",
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
- const result = await lui_templates(path, {
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
- const expression = generate(name, parsed, lui_imports, component_imports);
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
- result.unshift(`import {${
68
- list_generate([...lui_imports], 0)
69
- }} from ${JSON.stringify(options.lui_name)};`);
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
+ }
@@ -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
@@ -1,10 +0,0 @@
1
- {
2
- "json.schemas": [
3
- {
4
- "fileMatch": [
5
- "/test/templates/*.json"
6
- ],
7
- "url": "/schema/intermediary.json"
8
- }
9
- ]
10
- }
@@ -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,9 +0,0 @@
1
- <article>
2
- <h1>{{ title }}</h1>
3
- {% if author %}
4
- Written by {{ author }}
5
- {% endif %}
6
- <div>
7
- {{ content }}
8
- </div>
9
- </article>
@@ -1,3 +0,0 @@
1
- <button type=submit class="btn-primary" style='color:red' disabled>
2
- {{ label }}
3
- </button>
@@ -1,11 +0,0 @@
1
- <div>
2
- {% if level1 %}
3
- <h1>Level 1</h1>
4
- {% if level2 %}
5
- <h2>Level 2</h2>
6
- {% if level3 %}
7
- <h3>Level 3</h3>
8
- {% endif %}
9
- {% endif %}
10
- {% endif %}
11
- </div>
@@ -1,4 +0,0 @@
1
- <div title="Test Title">
2
- <h1>Welcome {{ user }}!</h1>
3
- <p>This is a test paragraph.</p>
4
- </div>
@@ -1,4 +0,0 @@
1
- <button
2
- {% if isDisabled %}disabled{% endif %}
3
- class="btn"
4
- >Submit</button>
@@ -1,4 +0,0 @@
1
- <button
2
- {% if customClass %}class="{{ customClass }}"{% endif %}
3
- type="submit"
4
- >Submit</button>
@@ -1,4 +0,0 @@
1
- <button
2
- {% unless isEnabled %}disabled{% endunless %}
3
- class="btn"
4
- >Submit</button>
@@ -1,4 +0,0 @@
1
- <button
2
- {% unless hideClass %}class="btn-primary"{% endunless %}
3
- type="submit"
4
- >Submit</button>
@@ -1,6 +0,0 @@
1
- <div>
2
- {% if showGreeting %}
3
- <h1>Hello {{ name }}!</h1>
4
- {% endif %}
5
- <p>Always visible</p>
6
- </div>
@@ -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
- }
@@ -1,5 +0,0 @@
1
- <div>
2
- <img src="{{ imageUrl }}" alt="{{ altText }}">
3
- <br>
4
- <p>{{ caption }}</p>
5
- </div>
@@ -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,5 +0,0 @@
1
- <div>
2
- <h1>{{ title }}</h1>
3
- <p>Static text and {{ dynamic }} content</p>
4
- <a href="{{ url }}">Link to {{ destination }}</a>
5
- </div>
@@ -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,9 +0,0 @@
1
- <div>
2
- {% if outer %}
3
- <h1>Outer is true</h1>
4
- {% if inner %}
5
- <p>Both outer and inner are true</p>
6
- {% endif %}
7
- {% endif %}
8
- <span>Always visible</span>
9
- </div>
@@ -1,6 +0,0 @@
1
- {% if A %}
2
- {% if B %}
3
- <p>1</p>
4
- <p>2</p>
5
- {% endif %}
6
- {% endif %}
@@ -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,6 +0,0 @@
1
- <div>
2
- {% if showButton %}
3
- {% render 'button', label: buttonLabel %}
4
- {% endif %}
5
- {% render 'icon', name: 'check' %}
6
- </div>
@@ -1,3 +0,0 @@
1
- <div>
2
- {% render 'button', label: user.name, count: items.length %}
3
- </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,2 +0,0 @@
1
- Hello {{ name }}!
2
- <p>Welcome</p>
@@ -1 +0,0 @@
1
- <button class="btn {% if isActive %}active{% endif %}">Test</button>
@@ -1,5 +0,0 @@
1
- <div>
2
- Hello {{ name }}!
3
- <p>Welcome</p>
4
- Goodbye
5
- </div>
@@ -1,5 +0,0 @@
1
- <div>
2
- {% unless hideContent %}
3
- <p>Visible content</p>
4
- {% endunless %}
5
- </div>