Simplified JSX parser - initial commit

master
Vitaliy Filippov 2021-08-28 17:55:56 +03:00
commit 578ecb9526
4 changed files with 311 additions and 0 deletions

149
jsxParser.js Normal file
View File

@ -0,0 +1,149 @@
const htmlspecialchars_table = {
nbsp: "\xA0",
quot: '"',
apos: "'",
lt: "<",
gt: ">",
amp: "&"
};
function htmlspecialchars_decode(s)
{
// Only a limited set of named entries is supported by design,
// because supporting all of them would require a large translation table
if (s == null)
{
// or undefined, because null == undefined
return "";
}
return s.replace(
/&(nbsp|quot|apos|lt|gt|amp|#x[0-9a-f]+|#[0-9]+);/gi,
(m, m1) =>
{
if (m1[0] == "#")
{
return String.fromCharCode(
m1[1] == "x"
? parseInt(m1.substr(2), 16)
: parseInt(m1.substr(1), 10)
);
}
return htmlspecialchars_table[m1];
}
);
}
function parse(content, components, createElement)
{
content = content.replace(/>\s+</g, "><");
const tag_re = /<(\/?)([a-z0-9\-]+)(([^>'"]+|"[^"]*"|'[^']*')*)>/i;
const attr_re = /^\s*([^\s="']+)(?:=([^"'\s]+|"[^"]*"|'[^']*'))?/i;
let m;
let r = [];
let stack = [r];
while ((m = tag_re.exec(content)))
{
let text = content.substr(0, m.index);
let close = m[1];
let tag = m[2];
let attrs = m[3];
content = content.substr(m.index + m[0].length);
text = text.replace(/^\s+/, "").replace(/\s+$/, "");
if (text !== "")
{
r.push(htmlspecialchars_decode(text));
}
if (close && stack.length > 1)
{
stack.pop();
r = stack[stack.length - 1];
}
else
{
attrs = attrs.replace(/\s+$/, "");
if (attrs[attrs.length - 1] == "/")
{
close = true;
attrs = attrs.substr(0, attrs.length - 1).replace(/\s+$/, "");
}
let attrhash = {};
while ((m = attr_re.exec(attrs)))
{
let key = m[1].toLowerCase();
let value = m[2];
if (value != null)
{
// remember that null == undefined, so this checks for undefined too
if (value[0] == '"' || value[0] == "'")
{
value = value.substr(1, value.length - 2);
}
value = htmlspecialchars_decode(value);
}
else
{
value = "true";
}
if (key === "href")
{
// 0x0A-0B-0C are ignored in schema value in IE
value = value.replace(/[\x0a\x0b\x0c]+/g, "");
}
if (key.substr(0, 2) !== "on" &&
(key !== "href" || !/^javascript:/.exec(value)))
{
if (key === "class")
{
// Convert to className for React
key = "className";
}
else if (key === "style")
{
// Convert to an object for React
let obj = {};
value = value.replace(/^\s+/, "").replace(/\s+$/, "");
for (let part of value.split(/\s*;\s*/))
{
let pos = part.indexOf(":");
if (pos >= 0)
{
let part_key = part
.substr(0, pos)
.replace(/\s+$/, "")
.replace(/-([a-z])/g, (m, m1) =>
m1.toUpperCase()
);
let part_value = part
.substr(pos + 1)
.replace(/^\s+/, "");
obj[part_key] = part_value;
}
}
value = obj;
}
attrhash[key] = value;
}
attrs = attrs.substr(m[0].length);
}
if (!close)
{
attrhash["children"] = [];
}
attrhash["key"] = r.length;
r.push(createElement(components[tag] || tag, attrhash));
if (!close)
{
stack.push(attrhash["children"]);
r = attrhash["children"];
}
}
}
content = content.replace(/^\s+/, "").replace(/\s+$/, "");
if (content !== "")
{
r.push(htmlspecialchars_decode(content));
}
return stack[0];
}
module.exports = { htmlspecialchars_decode, parse };

15
package.json Normal file
View File

@ -0,0 +1,15 @@
{
"name": "jsx-parser",
"version": "1.0.0",
"description": "Simplified JSX parser",
"main": "jsxParser.js",
"scripts": {
"test": "jest"
},
"keywords": [
"jsx",
"parser"
],
"author": "Vitaliy Filippov",
"license": "LGPL-3.0"
}

105
result1.json Normal file
View File

@ -0,0 +1,105 @@
[
{
"tag": "div",
"attrs": {
"className": "homeGrid",
"children": [
"<text>",
{
"tag": "div",
"attrs": {
"className": "homeTop",
"children": [],
"key": 1
}
},
{
"tag": "div",
"attrs": {
"style": {
"background": "black",
"textAlign": "right",
"color": "white",
"height": "20px",
"fontSize": "15px",
"width": "100px"
},
"children": [
{
"tag": {
"x": "examplecomponent"
},
"attrs": {
"title": "Hello world",
"skus": " 55-12940,102-109012",
"loc": "home",
"linktext": "I AM HERE",
"linkurl": "/hello",
"children": [],
"key": 0
}
}
],
"key": 2
}
},
{
"tag": "div",
"attrs": {
"className": "homeMiddle",
"children": [
{
"tag": "home",
"attrs": {
"children": [],
"key": 0
}
}
],
"key": 3
}
},
{
"tag": "div",
"attrs": {
"className": "homeBottom",
"children": [],
"key": 4
}
},
{
"tag": "div",
"attrs": {
"className": "homeBottom",
"children": [
{
"tag": "examplewithchildren",
"attrs": {
"logo": "/logo.svg",
"title": "Please wait",
"linktext": "READ MORE",
"linkurl": "https://google.com/",
"moretext": "More",
"content1": "Let's go",
"children": [
{
"tag": "enclosed",
"attrs": {
"a": "b",
"tagged": "true",
"key": 0
}
}
],
"key": 0
}
}
],
"key": 5
}
}
],
"key": 0
}
}
]

42
test.js Normal file
View File

@ -0,0 +1,42 @@
const { parse } = require('./jsxParser.js');
const result1 = require('./result1.json');
const cmps = {
ExampleComponent: { x: 'examplecomponent' },
home: 'home',
ExampleWithChildren: 'examplewithchildren',
enclosed: 'enclosed',
};
// Example includes:
// - nested components
// - HTML entities
// - inline styles
// - classNames
// - attribute without a value
const str = `
<div class="homeGrid">
&lt;text&gt;
<div class="homeTop"></div>
<div style="background: black; text-align: right; color: white; height: 20px; font-size: 15px; width: 100px">
<ExampleComponent title="Hello world" skus=" 55-12940,102-109012"
loc="home" linktext="I AM HERE" linkurl="/hello"></ExampleComponent>
</div>
<div class="homeMiddle"><home ></home></div>
<div class="homeBottom"> </div>
<div class="homeBottom">
<ExampleWithChildren logo="/logo.svg" title="Please wait"
linktext="READ MORE" linkurl="https://google.com/" moretext="More"
content1="Let&#039;s go">
<enclosed a="b" tagged />
</ExampleWithChildren>
</div>
</div>
`;
const result = parse(str, cmps, (tag, attrs) => ({ tag, attrs }));
if (JSON.stringify(result) != JSON.stringify(result1))
{
process.stderr.write('Test failed, got:\n');
process.stderr.write(JSON.stringify(result, null, 2));
process.exit(1);
}