Skip to content

Commit dd6731c

Browse files
committed
#7131 main.addon.HtmlStringToVdom: WIP
1 parent d0bd744 commit dd6731c

3 files changed

Lines changed: 292 additions & 5 deletions

File tree

src/main/addon/HtmlStringToVdom.mjs

Lines changed: 124 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,30 +46,149 @@ class HtmlStringToVdom extends Base {
4646
* @param {Object|Object[]} opts
4747
* @param {String} [opts.type=text/html]
4848
* @param {String} opts.value
49+
* @param {Array} [opts.values] Dynamic values to replace placeholders
4950
* @returns {Object|Object[]} The vdom object or array of vdom objects
5051
*/
5152
createVdom(opts) {
5253
let arrayParam = true;
5354

5455
if (!Array.isArray(opts)) {
5556
arrayParam = false;
56-
opts = [opts]
57+
opts = [opts];
5758
}
5859

5960
const
6061
me = this,
6162
{mimeTypes} = HtmlStringToVdom,
6263
returnValue = [];
6364

64-
for (const {type='text/html', value} of opts) {
65+
for (const {type = 'text/html', value, values = []} of opts) {
6566
if (!mimeTypes.includes(type)) {
66-
throw new Error(`Invalid mimeType: ${type}. Supported values are: ${mimeTypes.join(', ')}`)
67+
throw new Error(`Invalid mimeType: ${type}. Supported values are: ${mimeTypes.join(', ')}`);
6768
}
6869

69-
const tree = me.domParser.parseFromString(value, type);
70+
const doc = me.domParser.parseFromString(value, type);
71+
72+
// If the parser returns an error document, handle it
73+
if (doc.querySelector('parsererror')) {
74+
console.error('Error parsing HTML string:', doc.querySelector('parsererror').textContent);
75+
returnValue.push({
76+
tag : 'div',
77+
html: 'Error parsing HTML'
78+
});
79+
continue;
80+
}
81+
82+
let nodes = Array.from(doc.body.childNodes);
83+
84+
// If there are no nodes in the body, check the head (e.g., for SVG)
85+
if (nodes.length === 0 && doc.head.childNodes.length > 0) {
86+
nodes = Array.from(doc.head.childNodes);
87+
}
88+
89+
if (nodes.length === 1) {
90+
returnValue.push(me.domNodeToVdom(nodes[0], values));
91+
} else {
92+
const fragment = [];
93+
for (const node of nodes) {
94+
// Ignore whitespace-only text nodes between elements
95+
if (node.nodeType === 3 && node.textContent.trim() === '') {
96+
continue;
97+
}
98+
fragment.push(me.domNodeToVdom(node, values));
99+
}
100+
returnValue.push(fragment);
101+
}
102+
}
103+
104+
return arrayParam ? returnValue : returnValue[0];
105+
}
106+
107+
/**
108+
* Recursively converts a DOM node to a VDOM object.
109+
* @param {Node} node The DOM node to convert.
110+
* @param {Array} values The array of dynamic values.
111+
* @returns {Object|String} The VDOM object or a string for text nodes.
112+
* @private
113+
*/
114+
domNodeToVdom(node, values) {
115+
// Text Node
116+
if (node.nodeType === 3) { // TEXT_NODE
117+
const text = node.textContent.trim();
118+
const match = text.match(/^__DYNAMIC_VALUE_(\d+)__$/);
119+
120+
// If the text node is exclusively a placeholder, return the raw value
121+
if (match) {
122+
return values[parseInt(match[1], 10)];
123+
}
124+
125+
// For regular text nodes (which might still contain placeholders for string values)
126+
return node.textContent.replace(/__DYNAMIC_VALUE_(\d+)__/g, (m, index) => {
127+
return values[parseInt(index, 10)];
128+
});
70129
}
71130

72-
return arrayParam ? returnValue : returnValue[0]
131+
// Element Node
132+
if (node.nodeType === 1) { // ELEMENT_NODE
133+
const vdom = {
134+
tag: node.tagName.toLowerCase()
135+
};
136+
137+
// Attributes
138+
if (node.hasAttributes()) {
139+
for (const attr of node.attributes) {
140+
let attrName = attr.name;
141+
let attrValue = attr.value;
142+
143+
// Replace placeholders in attribute values
144+
attrValue = attrValue.replace(/__DYNAMIC_VALUE_(\d+)__/g, (match, index) => {
145+
return values[parseInt(index, 10)];
146+
});
147+
148+
if (attrName === 'class') {
149+
attrName = 'cls';
150+
} else if (attrName === 'style') {
151+
vdom.style = this.parseStyle(attrValue);
152+
continue;
153+
}
154+
vdom[attrName] = attrValue;
155+
}
156+
}
157+
158+
// Children
159+
if (node.hasChildNodes()) {
160+
vdom.cn = [];
161+
for (const child of node.childNodes) {
162+
// Ignore whitespace-only text nodes that are not placeholders
163+
if (child.nodeType === 3 && child.textContent.trim() === '') {
164+
continue;
165+
}
166+
const childVdom = this.domNodeToVdom(child, values);
167+
vdom.cn.push(childVdom);
168+
}
169+
}
170+
171+
return vdom;
172+
}
173+
174+
return null; // Should not happen for valid HTML
175+
}
176+
177+
/**
178+
* Parses a style attribute string into an object.
179+
* @param {String} styleString The style string (e.g., "color: red; font-size: 16px").
180+
* @returns {Object} The style object.
181+
* @private
182+
*/
183+
parseStyle(styleString) {
184+
const style = {};
185+
styleString.split(';').forEach(declaration => {
186+
if (declaration.trim() !== '') {
187+
const [property, value] = declaration.split(':');
188+
style[property.trim()] = value.trim();
189+
}
190+
});
191+
return style;
73192
}
74193
}
75194

test/siesta/siesta.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ project.plan(
8484
{
8585
group: 'functional',
8686
items: [
87+
'tests/functional/HtmlStringToVdom.mjs',
8788
'tests/functional/Button.mjs'
8889
]
8990
},
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import Neo from '../../../../src/Neo.mjs';
2+
import * as core from '../../../../src/core/_export.mjs';
3+
import HtmlStringToVdom from '../../../../src/main/addon/HtmlStringToVdom.mjs';
4+
5+
const addon = Neo.create(HtmlStringToVdom);
6+
7+
StartTest(t => {
8+
t.beforeEach(t => {
9+
// The addon is a singleton, so we get the instance instead of creating a new one.
10+
// In a real app, this would be managed by the framework.
11+
// For testing, we can create it if it doesn't exist.
12+
if (!Neo.main.addon.HtmlStringToVdom) {
13+
Neo.main.addon.HtmlStringToVdom = Neo.create(HtmlStringToVdom);
14+
}
15+
});
16+
17+
t.afterEach(t => {
18+
19+
});
20+
21+
t.it('should parse a simple HTML string with a single root node', t => {
22+
const html = '<div><p>Hello</p></div>';
23+
const vdom = addon.createVdom({value: html});
24+
25+
t.expect(vdom).toEqual({
26+
tag: 'div',
27+
cn: [
28+
{
29+
tag: 'p',
30+
cn: ['Hello']
31+
}
32+
]
33+
});
34+
});
35+
36+
t.it('should parse attributes correctly', t => {
37+
const html = '<div id="my-div" class="foo bar" style="color: red; font-size: 16px;"></div>';
38+
const vdom = addon.createVdom({value: html});
39+
40+
t.expect(vdom).toEqual({
41+
tag: 'div',
42+
id: 'my-div',
43+
cls: 'foo bar',
44+
style: {
45+
color: 'red',
46+
'font-size': '16px'
47+
}
48+
});
49+
});
50+
51+
t.it('should handle multiple root elements by returning an array', t => {
52+
const html = '<p>One</p><span>Two</span>';
53+
const vdom = addon.createVdom({value: html});
54+
55+
t.expect(vdom).toEqual([
56+
{
57+
tag: 'p',
58+
cn: ['One']
59+
},
60+
{
61+
tag: 'span',
62+
cn: ['Two']
63+
}
64+
]);
65+
});
66+
67+
t.it('should handle dynamic values in text nodes', t => {
68+
const html = '<p>__DYNAMIC_VALUE_0__</p>';
69+
const values = ['Hello World'];
70+
const vdom = addon.createVdom({value: html, values: values});
71+
72+
t.expect(vdom).toEqual({
73+
tag: 'p',
74+
cn: ['Hello World']
75+
});
76+
});
77+
78+
t.it('should handle dynamic values in attributes', t => {
79+
const html = '<div id="__DYNAMIC_VALUE_0__"></div>';
80+
const values = ['my-dynamic-id'];
81+
const vdom = addon.createVdom({value: html, values: values});
82+
83+
t.expect(vdom).toEqual({
84+
tag: 'div',
85+
id: 'my-dynamic-id'
86+
});
87+
});
88+
89+
t.it('should handle multiple dynamic values', t => {
90+
const html = '<div class="__DYNAMIC_VALUE_0__"><p>__DYNAMIC_VALUE_1__</p></div>';
91+
const values = ['my-class', 'my-text'];
92+
const vdom = addon.createVdom({value: html, values: values});
93+
94+
t.expect(vdom).toEqual({
95+
tag: 'div',
96+
cls: 'my-class',
97+
cn: [
98+
{
99+
tag: 'p',
100+
cn: ['my-text']
101+
}
102+
]
103+
});
104+
});
105+
106+
t.it('should handle non-string dynamic values (e.g., component configs)', t => {
107+
const html = '<div>__DYNAMIC_VALUE_0__</div>';
108+
const component = {ntype: 'component', id: 'my-comp'};
109+
const values = [component];
110+
const vdom = addon.createVdom({value: html, values: values});
111+
112+
t.expect(vdom).toEqual({
113+
tag: 'div',
114+
cn: [
115+
{
116+
ntype: 'component',
117+
id: 'my-comp'
118+
}
119+
]
120+
});
121+
});
122+
123+
t.it('should handle void elements correctly (e.g., <input>)', t => {
124+
const html = '<input type="text" value="test">';
125+
const vdom = addon.createVdom({value: html});
126+
127+
t.expect(vdom).toEqual({
128+
tag: 'input',
129+
type: 'text',
130+
value: 'test'
131+
});
132+
});
133+
134+
t.it('should ignore whitespace-only text nodes', t => {
135+
const html = '<div> <p>Hi</p> </div>';
136+
const vdom = addon.createVdom({value: html});
137+
138+
t.expect(vdom).toEqual({
139+
tag: 'div',
140+
cn: [
141+
{
142+
tag: 'p',
143+
cn: ['Hi']
144+
}
145+
]
146+
});
147+
});
148+
149+
t.it('should handle an array of parsing requests', t => {
150+
const requests = [
151+
{ value: '<h1>Title</h1>' },
152+
{ value: '<p>__DYNAMIC_VALUE_0__</p>', values: ['Paragraph'] }
153+
];
154+
const vdoms = addon.createVdom(requests);
155+
156+
t.expect(vdoms).toEqual([
157+
{
158+
tag: 'h1',
159+
cn: ['Title']
160+
},
161+
{
162+
tag: 'p',
163+
cn: ['Paragraph']
164+
}
165+
]);
166+
});
167+
});

0 commit comments

Comments
 (0)