lui-templates 0.0.6 → 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 +42 -2
- 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/src/parsers/liquid.js +294 -3
- 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/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
|
```
|
|
@@ -52,8 +57,8 @@ init(() => {
|
|
|
52
57
|
```js
|
|
53
58
|
import lui_templates from 'lui-templates';
|
|
54
59
|
|
|
55
|
-
const code = await lui_templates('src/templates/
|
|
56
|
-
await fs.writeFile('src/
|
|
60
|
+
const code = await lui_templates('src/templates/greeting.liquid');
|
|
61
|
+
await fs.writeFile('src/components/greeting.js', code, 'utf8');
|
|
57
62
|
|
|
58
63
|
await bundleApp('src/main.js'); // or whatever
|
|
59
64
|
```
|
|
@@ -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
|
+
}
|