@mrhenry/twig-html-parser 0.1.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/LICENSE +24 -0
- package/package.json +16 -0
- package/src/atoms.js +335 -0
- package/src/html-tokenizer.js +1032 -0
- package/src/index.js +11 -0
- package/src/parse.js +36 -0
- package/src/parser.js +330 -0
- package/test/atoms.test.js +161 -0
- package/test/html-tokenizer.test.js +161 -0
- package/test/twig-html-parser.test.js +176 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Structure and position tests for the `@mrhenry/twig-html-parser` hybrid AST
|
|
4
|
+
* builder.
|
|
5
|
+
*
|
|
6
|
+
* The hybrid tree must reflect the source structure exactly: elements nest as
|
|
7
|
+
* written, twig leaves appear in data/attribute positions, and twig block tags
|
|
8
|
+
* pair into `twigBlock` nodes with their bodies. Node offsets must match the
|
|
9
|
+
* original source (the basis for report-only / range / cursor modes).
|
|
10
|
+
*
|
|
11
|
+
* @module test
|
|
12
|
+
*/
|
|
13
|
+
import { test } from 'node:test';
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
import { parse } from '@mrhenry/twig-html-parser';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {string} source
|
|
19
|
+
* @returns {any}
|
|
20
|
+
*/
|
|
21
|
+
function ast(source) {
|
|
22
|
+
return parse(source, 'index.twig');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {any} root
|
|
27
|
+
* @param {string} type
|
|
28
|
+
* @returns {Array<any>}
|
|
29
|
+
*/
|
|
30
|
+
function findAll(root, type) {
|
|
31
|
+
/** @type {Array<any>} */
|
|
32
|
+
const found = [];
|
|
33
|
+
const walk = (/** @type {any} */ n) => {
|
|
34
|
+
if (n.type === type) {
|
|
35
|
+
found.push(n);
|
|
36
|
+
}
|
|
37
|
+
if (n.children) {
|
|
38
|
+
for (const c of n.children) {
|
|
39
|
+
walk(c);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
walk(root);
|
|
44
|
+
return found;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Asserts node offsets line up with the source.
|
|
49
|
+
*
|
|
50
|
+
* @param {any} root
|
|
51
|
+
* @param {string} source
|
|
52
|
+
*/
|
|
53
|
+
function assertOffsetsInSource(root, source) {
|
|
54
|
+
const walk = (/** @type {any} */ n) => {
|
|
55
|
+
assert.ok(n.rawStart >= 0 && n.rawEnd <= source.length, `${n.type} out of range`);
|
|
56
|
+
assert.ok(n.rawStart <= n.rawEnd, `${n.type} reversed`);
|
|
57
|
+
if (n.raw) {
|
|
58
|
+
assert.equal(source.slice(n.rawStart, n.rawEnd), n.raw, `${n.type} raw mismatch`);
|
|
59
|
+
}
|
|
60
|
+
if (n.children) {
|
|
61
|
+
for (const c of n.children) {
|
|
62
|
+
walk(c);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
walk(root);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test('parses elements into a nested tree', () => {
|
|
70
|
+
const root = ast('<div><span>hi</span></div>');
|
|
71
|
+
assert.equal(root.type, 'root');
|
|
72
|
+
assert.equal(root.children.length, 1);
|
|
73
|
+
const div = root.children[0];
|
|
74
|
+
assert.equal(div.type, 'element');
|
|
75
|
+
assert.equal(div.name, 'div');
|
|
76
|
+
assert.equal(div.children[0].type, 'element');
|
|
77
|
+
assert.equal(div.children[0].name, 'span');
|
|
78
|
+
assert.equal(div.children[0].children[0].raw, 'hi');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('parses a twig print in an attribute value', () => {
|
|
82
|
+
const root = ast('<a href="{{ url }}">x</a>');
|
|
83
|
+
const a = root.children[0];
|
|
84
|
+
assert.equal(a.attrs.length, 1);
|
|
85
|
+
const attr = /** @type {any} */ (a.attrs[0]);
|
|
86
|
+
assert.equal(attr.nameRaw, 'href');
|
|
87
|
+
assert.equal(attr.valueChunks[0].type, 'twig');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('pairs conditional attributes into a twigBlock inside a start tag', () => {
|
|
91
|
+
const root = ast('<div class="row" {% if x %} id="y" {% endif %} data-x>');
|
|
92
|
+
const div = root.children[0];
|
|
93
|
+
const kinds = div.attrs.map((/** @type {any} */ a) => a.type ?? 'attr');
|
|
94
|
+
assert.deepEqual(kinds, ['attr', 'twigBlock', 'attr']);
|
|
95
|
+
const block = /** @type {any} */ (div.attrs[1]);
|
|
96
|
+
assert.equal(block.tag, 'if');
|
|
97
|
+
assert.equal(block.open.raw, '{% if x %}');
|
|
98
|
+
assert.equal(block.close.raw, '{% endif %}');
|
|
99
|
+
assert.equal(block.children.length, 1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('pairs a twig block in text position', () => {
|
|
103
|
+
const root = ast('{% if x %}<p>a</p>{% endif %}');
|
|
104
|
+
assert.equal(root.children.length, 1);
|
|
105
|
+
const block = root.children[0];
|
|
106
|
+
assert.equal(block.type, 'twigBlock');
|
|
107
|
+
assert.equal(block.tag, 'if');
|
|
108
|
+
assert.equal(block.children.length, 1);
|
|
109
|
+
assert.equal(block.children[0].type, 'element');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('splits else sections', () => {
|
|
113
|
+
const root = ast('{% if x %}a{% else %}b{% endif %}');
|
|
114
|
+
const block = root.children[0];
|
|
115
|
+
assert.equal(block.sections.length, 2);
|
|
116
|
+
assert.equal(block.sections[0].head, null);
|
|
117
|
+
assert.equal(block.sections[0].body[0].raw, 'a');
|
|
118
|
+
assert.equal(block.sections[1].head.raw, '{% else %}');
|
|
119
|
+
assert.equal(block.sections[1].body[0].raw, 'b');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('keeps a leaf for a set without a body', () => {
|
|
123
|
+
const root = ast('{% set x = 1 %}{{ x }}');
|
|
124
|
+
assert.equal(root.children[0].type, 'twig');
|
|
125
|
+
assert.equal(root.children[1].type, 'twig');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('pairs a capture set', () => {
|
|
129
|
+
const root = ast('{% set x %}hi{% endset %}');
|
|
130
|
+
assert.equal(root.children[0].type, 'twigBlock');
|
|
131
|
+
assert.equal(root.children[0].tag, 'set');
|
|
132
|
+
assert.equal(root.children[0].children[0].raw, 'hi');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('keeps raw text element content intact', () => {
|
|
136
|
+
const root = ast('<script>var x = "{{ v }}";</script>');
|
|
137
|
+
const script = root.children[0];
|
|
138
|
+
assert.equal(script.name, 'script');
|
|
139
|
+
assert.deepEqual(script.children.map((/** @type {any} */ c) => c.type), ['text', 'twig', 'text']);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('keeps html comments and doctype', () => {
|
|
143
|
+
const root = ast('<!doctype html><!-- c --><p>x</p>');
|
|
144
|
+
assert.deepEqual(root.children.map((/** @type {any} */ c) => c.type), ['doctype', 'comment', 'element']);
|
|
145
|
+
assert.equal(root.children[1].raw, '<!-- c -->');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('keeps twig comments', () => {
|
|
149
|
+
const root = ast('{# note #}<p>x</p>');
|
|
150
|
+
assert.deepEqual(root.children.map((/** @type {any} */ c) => c.type), ['twigComment', 'element']);
|
|
151
|
+
assert.equal(root.children[0].raw, '{# note #}');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('node raw offsets match the source', () => {
|
|
155
|
+
const source = '<div class="row" {% if x %} id="{{ id }}" {% endif %}>\n\t<p>hi</p>\n</div>\n{% if y %}x{% endif %}';
|
|
156
|
+
const root = ast(source);
|
|
157
|
+
assertOffsetsInSource(root, source);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('node raw offsets match the source across nesting', () => {
|
|
161
|
+
const source = '{% for i in items %}\n\t<a href="{{ i.url }}">{{ i.title }}</a>\n{% endfor %}';
|
|
162
|
+
const root = ast(source);
|
|
163
|
+
assertOffsetsInSource(root, source);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test('twig block indentation spans are contiguous with source', () => {
|
|
167
|
+
const source = 'a{% if x %}b{% else %}c{% endif %}d';
|
|
168
|
+
const root = ast(source);
|
|
169
|
+
const texts = findAll(root, 'text');
|
|
170
|
+
assert.deepEqual(texts.map((/** @type {any} */ t) => t.raw), ['a', 'b', 'c', 'd']);
|
|
171
|
+
assertOffsetsInSource(root, source);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('parser summary', () => {
|
|
175
|
+
assert.ok(true);
|
|
176
|
+
});
|