lui-templates 0.0.6

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 (34) hide show
  1. package/.vscode/settings.json +10 -0
  2. package/README.md +103 -0
  3. package/package.json +32 -0
  4. package/schema/intermediary.json +147 -0
  5. package/src/cli.js +59 -0
  6. package/src/constants.js +8 -0
  7. package/src/generator.js +276 -0
  8. package/src/main.js +74 -0
  9. package/src/parser.js +57 -0
  10. package/src/parsers/json.js +9 -0
  11. package/src/parsers/liquid.js +1194 -0
  12. package/test/basic.js +9 -0
  13. package/test/templates/article.liquid +9 -0
  14. package/test/templates/button.liquid +3 -0
  15. package/test/templates/complex-nested.liquid +11 -0
  16. package/test/templates/complex.liquid +4 -0
  17. package/test/templates/conditional-bool-attr.liquid +4 -0
  18. package/test/templates/conditional-nonbool-attr.liquid +4 -0
  19. package/test/templates/conditional-unless-attr.liquid +4 -0
  20. package/test/templates/conditional-unless-nonbool-attr.liquid +4 -0
  21. package/test/templates/conditional.liquid +6 -0
  22. package/test/templates/dynamic-class.liquid +1 -0
  23. package/test/templates/greeting.json +74 -0
  24. package/test/templates/image.liquid +5 -0
  25. package/test/templates/link.json +31 -0
  26. package/test/templates/mixed.liquid +5 -0
  27. package/test/templates/nested-attr-conditionals.liquid +1 -0
  28. package/test/templates/nested-comprehensive.liquid +51 -0
  29. package/test/templates/nested-conditionals.liquid +9 -0
  30. package/test/templates/nested-if-flatten.liquid +6 -0
  31. package/test/templates/root-text.liquid +2 -0
  32. package/test/templates/test-attr-cond.liquid +1 -0
  33. package/test/templates/text-nodes.liquid +5 -0
  34. package/test/templates/unless.liquid +5 -0
@@ -0,0 +1,10 @@
1
+ {
2
+ "json.schemas": [
3
+ {
4
+ "fileMatch": [
5
+ "/test/templates/*.json"
6
+ ],
7
+ "url": "/schema/intermediary.json"
8
+ }
9
+ ]
10
+ }
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # lui-templates
2
+
3
+ [lui](https://github.com/L3P3/lui) is quite simple to use -- as long as you are a JavaScript developer.
4
+
5
+ This tool lets you write lui components without writing JavaScript but via template files. 🎉
6
+
7
+ ## Supported formats
8
+
9
+ Here are some templating languages I think about supporting:
10
+
11
+ - [ ] [pug](https://pugjs.org/api/getting-started.html)
12
+ - [ ] [Haml](https://haml.info/)
13
+ - [x] [Liquid](https://shopify.github.io/liquid/) (proof of concept)
14
+ - [ ] [Handlebars](https://handlebarsjs.com/)
15
+ - [ ] [Knockout](https://knockoutjs.com/documentation/introduction.html)
16
+ - [x] Raw JSON (as intermediary format, see test/templates/greeting.json)
17
+
18
+ ## Usage
19
+
20
+ ```sh
21
+ npx lui-templates src/templates/greeting.liquid > src/components/greeting.js
22
+ ```
23
+
24
+ ```sh
25
+ npx lui-templates --help
26
+ ```
27
+
28
+ ## Example
29
+
30
+ ### `src/templates/greeting.liquid`
31
+
32
+ ```liquid
33
+ <h1>Hello {{ name }}!</h1>
34
+ ```
35
+
36
+ ### `src/main.js`
37
+
38
+ ```js
39
+ import { init, node } from 'lui';
40
+
41
+ import Greeting from './components/greeting.js';
42
+
43
+ init(() => {
44
+ return [
45
+ node(Greeting, { name: 'World' }),
46
+ ];
47
+ });
48
+ ```
49
+
50
+ ### `build.js`
51
+
52
+ ```js
53
+ import lui_templates from 'lui-templates';
54
+
55
+ const code = await lui_templates('src/templates/Greeting.liquid');
56
+ await fs.writeFile('src/generated/Greeting.js', code, 'utf8');
57
+
58
+ await bundleApp('src/main.js'); // or whatever
59
+ ```
60
+
61
+ ### `src/generated/greeting.js` (generated)
62
+
63
+ The generated component is looking like this:
64
+
65
+ ```js
66
+ // generated by lui-templates
67
+
68
+ import { hook_dom } from "lui";
69
+
70
+ export default function Greeting({ name }) {
71
+ hook_dom("h1", {
72
+ innerText: `Hello ${name}!`,
73
+ });
74
+
75
+ return null;
76
+ }
77
+ ```
78
+
79
+ You may have it in your `.gitignore` to prevent duplication.
80
+
81
+ ... And did I mention that this file is generated? 🎉
82
+
83
+ ## Interface of `lui_templates(path[, {options}])`
84
+
85
+ ### `path`
86
+
87
+ The template file to read. Can be in one of the [supported formats](#supported-formats), determined by its ending. If it is a directory, all contained templates will be read.
88
+
89
+ ### option `lui_name`
90
+
91
+ The name of the lui module to import from. Defaults to `lui`.
92
+
93
+ ### option `components_name`
94
+
95
+ The name of the module to import unknown components from. Defaults to `./externs.js`.
96
+
97
+ ### return value
98
+
99
+ The returned ES module imports the needed methods and exports the component(s).
100
+
101
+ For a single template file, the default export is the component.
102
+
103
+ For a directory, each component is exported by its name.
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "lui-templates",
3
+ "version": "0.0.6",
4
+ "description": "transform html templates into lui components",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/l3p3/lui-templates"
8
+ },
9
+ "bugs": {
10
+ "url": "https://github.com/l3p3/lui-templates/issues"
11
+ },
12
+ "main": "./src/main.js",
13
+ "module": "./src/main.js",
14
+ "exports": {
15
+ ".": "./src/main.js"
16
+ },
17
+ "type": "module",
18
+ "scripts": {
19
+ "test": "cd test && node basic.js"
20
+ },
21
+ "bin": "./src/cli.js",
22
+ "keywords": [
23
+ "lui",
24
+ "templates",
25
+ "liquid",
26
+ "framework",
27
+ "html",
28
+ "tiny"
29
+ ],
30
+ "author": "L3P3 <dev@l3p3.de> (https://l3p3.de)",
31
+ "license": "Zlib"
32
+ }
@@ -0,0 +1,147 @@
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/src/cli.js ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ readFileSync,
5
+ } from 'fs';
6
+
7
+ import lui_templates from './main.js';
8
+
9
+ const args = process.argv.slice(2);
10
+
11
+ if (args.length === 0 || args[0] === '-h' || args[0] === '--help') {
12
+ console.error(`Usage: lui-templates input [options]
13
+ Input: The template file or directory containing the templates
14
+ Options:
15
+ -e, --externs <path> File unknown components are imported from
16
+ -h, --help Show this help message
17
+ --version Show version information
18
+
19
+ Example:
20
+ lui-templates ./templates/button.liquid > ./components/button.js
21
+ `);
22
+ process.exit(0);
23
+ }
24
+ if (args[0] === '--version') {
25
+ const { version } = JSON.parse(
26
+ readFileSync(
27
+ new URL('../package.json', import.meta.url)
28
+ )
29
+ );
30
+ console.error(`lui-templates version ${version}`);
31
+ process.exit(0);
32
+ }
33
+
34
+ if (args[0].startsWith('-')) {
35
+ console.error('Must first specify the input, see --help');
36
+ process.exit(1);
37
+ }
38
+
39
+ const path = args.shift();
40
+ let arg_externs = './externs.js';
41
+
42
+ while (args.length > 0) {
43
+ const arg = args.shift();
44
+ switch (arg) {
45
+ case '-e':
46
+ case '--externs':
47
+ arg_externs = args.shift();
48
+ break;
49
+ default:
50
+ console.error(`Unknown argument: ${arg}, see --help`);
51
+ process.exit(1);
52
+ }
53
+ }
54
+
55
+ const result = await lui_templates(path, {
56
+ components_name: arg_externs,
57
+ });
58
+
59
+ console.log(result);
@@ -0,0 +1,8 @@
1
+ export const NODE_TYPE_COMPONENT = 0;
2
+ export const NODE_TYPE_ELEMENT = 1;
3
+ export const NODE_TYPE_IF = 2;
4
+ export const NODE_TYPE_MAP = 3;
5
+
6
+ export const VALUE_TYPE_STATIC = 0;
7
+ export const VALUE_TYPE_FIELD = 1;
8
+ export const VALUE_TYPE_STRING_CONCAT = 2;
@@ -0,0 +1,276 @@
1
+ import {
2
+ NODE_TYPE_COMPONENT,
3
+ NODE_TYPE_ELEMENT,
4
+ NODE_TYPE_IF,
5
+ NODE_TYPE_MAP,
6
+ VALUE_TYPE_FIELD,
7
+ VALUE_TYPE_STATIC,
8
+ VALUE_TYPE_STRING_CONCAT,
9
+ } from './constants.js';
10
+
11
+ export function generate(name, parsed, lui_imports, component_imports) {
12
+ const {effects, inputs, transformations} = parsed;
13
+ let {nodes} = parsed;
14
+
15
+ const body = [];
16
+
17
+ // TODO: transformations
18
+ // stuff like `const variable2 = variable1 + 1`
19
+
20
+ if (nodes.length === 1 && nodes[0].type === NODE_TYPE_ELEMENT) {
21
+ const [node] = nodes;
22
+ lui_imports.add('hook_dom');
23
+ body.push(`hook_dom("${
24
+ descriptor_generate(node)
25
+ }"${
26
+ attrs_generate(Object.entries(node.props), 1)
27
+ });`);
28
+ nodes = node.children;
29
+ }
30
+
31
+ // TODO: effects
32
+ // stuff like when variable1 is updated, function doSomething is invoked
33
+
34
+ if (nodes.length === 0) {
35
+ body.push('return null;');
36
+ } else {
37
+ body.push(`return [${
38
+ childs_generate(nodes, 1, lui_imports, component_imports)
39
+ }];`);
40
+ }
41
+
42
+ return `function ${name}(${
43
+ inputs_generate(inputs)
44
+ }) {\n\t${
45
+ body.join('\n\n\t')
46
+ }\n}`;
47
+ }
48
+
49
+ /**
50
+ formats name for a component
51
+ @param {string} name some-name
52
+ @returns {string} SomeName
53
+ */
54
+ export function name_format(name) {
55
+ name =
56
+ name.charAt(0).toUpperCase() +
57
+ name.slice(1)
58
+ .replaceAll(/[\s_]+/g, '-')
59
+ .replace(
60
+ /-([a-z])/g,
61
+ (_, char) => char.toUpperCase()
62
+ );
63
+ assert_identifier(name);
64
+ return name;
65
+ }
66
+
67
+ function inputs_generate(inputs) {
68
+ if (inputs.length === 0) return '';
69
+
70
+ const declarations = inputs.map(input => {
71
+ assert_identifier(input.name);
72
+ if (input.fallback !== undefined) {
73
+ return `${input.name} = ${JSON.stringify(input.fallback)}`;
74
+ }
75
+ return input.name;
76
+ });
77
+
78
+ return `{${list_generate(declarations, 0)}}`;
79
+ }
80
+
81
+ function descriptor_generate(node) {
82
+ if (node.tag.includes('[')) throw new SyntaxError(`Tag must not contain "[": ${node.tag}`);
83
+ const attributes = Object.entries(node.props)
84
+ .filter(entry => entry[1].type === VALUE_TYPE_STATIC)
85
+ .map(descriptor_attribute_generate)
86
+ .filter(Boolean);
87
+ if (attributes.length === 0) return node.tag;
88
+ const invalid = attributes.find(attribute => attribute.includes(']['));
89
+ if (invalid) throw new SyntaxError(`Static attributes must not contain "][": ${invalid}`);
90
+ return `${node.tag}[${attributes.sort().join('][')}]`;
91
+ }
92
+
93
+ function descriptor_attribute_generate([key, value]) {
94
+ assert_identifier(key);
95
+ if (value.data === false) return '';
96
+ if (value.data === true) return key;
97
+ return `${key}=${string_escape(value.data)}`;
98
+ }
99
+
100
+ function attrs_generate(entries, identation) {
101
+ return props_generate(
102
+ entries.filter(entry => entry[1].type !== VALUE_TYPE_STATIC),
103
+ identation
104
+ );
105
+ }
106
+
107
+ function props_generate(entries, identation) {
108
+ if (entries.length === 0) return '';
109
+ for (const entry of entries) assert_identifier(entry[0]);
110
+ const assignments = entries.map(entry => {
111
+ const key = entry[0];
112
+ const value = value_generate(entry[1], identation);
113
+ return (
114
+ key === value
115
+ ? key
116
+ : `${entry[0]}: ${value}`
117
+ );
118
+ });
119
+ return `, {${list_generate(assignments, identation)}}`;
120
+ }
121
+
122
+ function value_generate(value, identation) {
123
+ switch (value.type) {
124
+ case VALUE_TYPE_STATIC: return JSON.stringify(value.data);
125
+ case VALUE_TYPE_FIELD:
126
+ // Allow js expressions for unless or ternary conditions
127
+ // TODO do this with transformations instead
128
+ // assert_identifier(value.data);
129
+ return value.data;
130
+ case VALUE_TYPE_STRING_CONCAT: return string_concat_generate(value.data, identation);
131
+ }
132
+ throw new TypeError(`Unknown value type: ${value.type}`);
133
+ }
134
+
135
+ function childs_generate(nodes, identation, lui_imports, component_imports) {
136
+ return list_generate(
137
+ nodes.map(node => node_generate(node, identation, lui_imports, component_imports)),
138
+ identation,
139
+ true
140
+ );
141
+ }
142
+
143
+ function node_generate(node, identation, lui_imports, component_imports) {
144
+ switch (node.type) {
145
+ case NODE_TYPE_COMPONENT:
146
+ assert_identifier(node.component);
147
+ lui_imports.add('node');
148
+ component_imports.add(node.component);
149
+ const props = props_generate(Object.entries(node.props), identation + 1);
150
+ return `node(${
151
+ node.component
152
+ }${
153
+ props === '' && node.children.length > 0
154
+ ? ', null'
155
+ : props
156
+ }${
157
+ node.children.length === 0
158
+ ? ''
159
+ : `, [${
160
+ childs_generate(node.children, identation + 1, lui_imports, component_imports)
161
+ }]`
162
+ })`;
163
+ case NODE_TYPE_ELEMENT:
164
+ lui_imports.add('node_dom');
165
+ const attrs = attrs_generate(Object.entries(node.props), identation + 1);
166
+ return `node_dom("${
167
+ descriptor_generate(node)
168
+ }"${
169
+ attrs === '' && node.children.length > 0
170
+ ? ', null'
171
+ : attrs
172
+ }${
173
+ node.children.length === 0
174
+ ? ''
175
+ : `, [${
176
+ childs_generate(node.children, identation + 1, lui_imports, component_imports)
177
+ }]`
178
+ })`;
179
+ case NODE_TYPE_IF:
180
+ return `${
181
+ value_generate(node.condition, identation)
182
+ } &&\n${
183
+ '\t'.repeat(identation + 1) +
184
+ node_generate(node.child, identation, lui_imports, component_imports)
185
+ }`;
186
+ case NODE_TYPE_MAP:
187
+ assert_identifier(node.component);
188
+ assert_identifier(node.from);
189
+ lui_imports.add('node_map');
190
+ component_imports.add(node.component);
191
+ return `node_map(${
192
+ node.component
193
+ }, ${
194
+ node.from
195
+ }${
196
+ props_generate(Object.entries(node.props), identation + 1)
197
+ })`;
198
+ }
199
+ throw new TypeError(`Unknown node type: ${node.type}`);
200
+ }
201
+
202
+ function string_concat_generate(data, identation) {
203
+ identation++;
204
+ if (data.length === 2) {
205
+ return data.map(item =>
206
+ item.type === VALUE_TYPE_STATIC
207
+ ? JSON.stringify(item.data)
208
+ : value_generate(item, identation)
209
+ ).join(' + ');
210
+ }
211
+ return `\`${
212
+ data.map(item => {
213
+ switch (item.type) {
214
+ case VALUE_TYPE_STATIC: return template_escape(item.data);
215
+ case VALUE_TYPE_FIELD:
216
+ // assert_identifier(item.data);
217
+ return `\${${item.data}}`;
218
+ }
219
+ return `\${\n${
220
+ '\t'.repeat(identation + 1) +
221
+ value_generate(item, identation)
222
+ }\n${'\t'.repeat(identation)}}`;
223
+ }).join('')
224
+ }\``;
225
+ }
226
+
227
+ function template_escape(string) {
228
+ return (
229
+ String(string)
230
+ .replaceAll('\\', '\\\\')
231
+ .replaceAll('`', '\\`')
232
+ .replaceAll('$', '\\$')
233
+ );
234
+ }
235
+
236
+ function string_escape(string) {
237
+ return (
238
+ String(string)
239
+ .replaceAll('\\', '\\\\')
240
+ .replaceAll('"', '\\"')
241
+ .replaceAll('\n', '\\n')
242
+ );
243
+ }
244
+
245
+ /**
246
+ Throws an error if the string is not a valid JavaScript identifier
247
+ @param {string} string
248
+ @throws {SyntaxError} if the string is not a valid JavaScript
249
+ */
250
+ function assert_identifier(string) {
251
+ if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(string)) {
252
+ throw new SyntaxError(`Invalid identifier: ${string}`);
253
+ }
254
+ }
255
+
256
+ /**
257
+ if a list entry contains one of these characters, it must be put on its own line
258
+ */
259
+ const regexp_noinline = /[\n:{[(]/;
260
+
261
+ /**
262
+ formats an object/array content, braces/brackets not included
263
+ @param {string[]} entries
264
+ @param {number} identation
265
+ @param {boolean} ordered
266
+ @return {string}
267
+ */
268
+ export function list_generate(entries, identation, ordered = false) {
269
+ switch (entries.length) {
270
+ case 0: return '';
271
+ case 1: if (!regexp_noinline.test(entries[0])) return ` ${entries[0]} `;
272
+ }
273
+ if (!ordered) entries.sort();
274
+ identation = '\t'.repeat(identation);
275
+ return `\n\t${identation + entries.join(',\n\t' + identation)},\n` + identation;
276
+ }