commit 578ecb95266232ad79cf3490c056d26cdf69bcd4 Author: Vitaliy Filippov Date: Sat Aug 28 17:55:56 2021 +0300 Simplified JSX parser - initial commit diff --git a/jsxParser.js b/jsxParser.js new file mode 100644 index 0000000..3d4f656 --- /dev/null +++ b/jsxParser.js @@ -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+<"); + 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 }; diff --git a/package.json b/package.json new file mode 100644 index 0000000..2092215 --- /dev/null +++ b/package.json @@ -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" +} diff --git a/result1.json b/result1.json new file mode 100644 index 0000000..9bc9d84 --- /dev/null +++ b/result1.json @@ -0,0 +1,105 @@ +[ + { + "tag": "div", + "attrs": { + "className": "homeGrid", + "children": [ + "", + { + "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 + } + } +] \ No newline at end of file diff --git a/test.js b/test.js new file mode 100644 index 0000000..c69c2da --- /dev/null +++ b/test.js @@ -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 = ` +
+ <text> +
+
+ +
+
+
+
+ + + +
+
+`; +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); +}