Simplified JSX parser - initial commit
commit
578ecb9526
|
@ -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 };
|
|
@ -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"
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
]
|
|
@ -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">
|
||||
<text>
|
||||
<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'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);
|
||||
}
|
Loading…
Reference in New Issue