├── .babelrc ├── .eslintrc ├── .gitignore ├── .tortilla └── manuals │ ├── templates │ ├── root.tmpl │ ├── step1.tmpl │ ├── step2.tmpl │ └── step3.tmpl │ └── views │ ├── root.md │ ├── step1.md │ ├── step2.md │ └── step3.md ├── README.md ├── package.json ├── src └── index.js └── webpack_config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: ["es2017"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { "es6": true, "browser": true }, 3 | "extends": ["eslint:recommended"], 4 | "parser": "babel-eslint", 5 | "settings": { 6 | "react": { 7 | "version": "15.0" 8 | }, 9 | }, 10 | "rules": { 11 | "no-unused-vars": 0 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.tortilla/manuals/templates/root.tmpl: -------------------------------------------------------------------------------- 1 | # Implementing a runtime version of JSX 2 | 3 | ## And learning how to think like a JSX parser 4 | 5 | JSX is one of the most commonly used syntax extensions out there. Originally JSX was parsed via a [Facebook fork of Esprima](https://github.com/facebookarchive/esprima) - a JavaScript syntax parser developed by jQuery. As it gained momentum, [Acorn](https://github.com/acornjs/acorn) took things to their hands and decided to make their own version of the parser which ended up being 1.5–2x faster than Esprima-fb, and is now being used by Babel. 6 | 7 | It definitely went through an evolution, but regardless of its phase, all parsers had a similar output - which is an AST. Once we have an AST representation of the JSX code, interpretation is extremely easy. 8 | 9 | Today we're gonna understand how a JSX parser thinks by implementing one of our own. Unlike Babel, rather than compiling, we're gonna evaluate the nodes in the AST according to their types, which means that we will be able to use JSX during runtime. 10 | Below is an example of the final product: 11 | 12 | ```js 13 | class Hello extends React.Component { 14 | render() { 15 | return jsx `
Hello ${this.props.name}
` 16 | } 17 | } 18 | 19 | ReactDOM.render( 20 | jsx `<${Hello} name="World" />`, 21 | document.getElementById('container') 22 | ) 23 | ``` 24 | 25 | Before we go ahead and rush to implementing the parser let's understand what we're aiming for. JSX simply takes an HTML-like syntax and transforms it into nested `React.createElement()` calls. What makes JSX unique is that we can use string interpolation within our HTML templates, so we can provide it with data which doesn't necessarily has to be serialized, things like functions, arrays, or objects. 26 | So given the following code: 27 | 28 | ```js 29 | const el = (props) => ( 30 |
31 | {props.text} 32 |
33 | ) 34 | ``` 35 | 36 | We should get the following output once compiling it with Babel: 37 | 38 | ```js 39 | const el = (props) => ( 40 | React.createElement( 41 | "div", 42 | { onClick: props.onClick }, 43 | React.createElement(Icon, { src: props.icon }), 44 | React.createElement( 45 | "span", 46 | null, 47 | props.text 48 | ) 49 | ) 50 | ) 51 | ``` 52 | 53 | Just aquick reminder - the compiled result should be used internally by ReactDOM to differentiate changes in the virtual DOM and then render them. This is something which is React specific and has nothing to do with JSX, so at this point we have achieved our goal. 54 | 55 | Essentially there are 3 things we should figure out when parsing a JSX code: 56 | 57 | - The name / component of the React element. 58 | - The props of the React element. 59 | - The children of the React element, for each this process should repeat itself recursively. 60 | 61 | As I mentioned earlier, it would be best if we could break down the code into nodes first and represent it as an AST. Looking at the input of the example above, we can roughly visualize how we would pluck the nodes from the code: 62 | 63 | ![Analyzing the JSX code.](https://cdn-images-1.medium.com/max/1600/1*AqTHDuxX5NNCI3iLycVfxA.png) 64 | 65 | And to put things simple, here's a schematic representation of the analysis above: 66 | 67 | ![A schematic representation of the analysis.](https://cdn-images-1.medium.com/max/1600/1*i8h2MocLHni8mTuPaakwBQ.png) 68 | 69 | Accordingly, we're gonna have 3 types of nodes: 70 | 71 | - Element node. 72 | - Props node. 73 | - Value node. 74 | 75 | Let's decide that each node has a base schema with the following properties: 76 | 77 | - node.type - which will represent the type name of the node, e.g. `element`, `props` and `value`. Based on the node type we can also determine that additional properties that the node's gonna carry. In our parser, each node type should have the following additional properties: 78 | 79 | ![node type schemas](https://cdn-images-1.medium.com/max/1600/1*dgAy6Zbj6ttfNqgppWIjug.png) 80 | 81 | - node.length -which represents the length of the sub-string in the code that the node occupies. This will help us trim the code string as we go with the parsing process so we can always focus on relevant parts of the string for the current node: 82 | 83 | ![Any time we parse a small part of the string, we slice the part we've just parsed.](https://cdn-images-1.medium.com/max/1600/1*PeiZnuBTKfLlDiaL24dgHw.png) 84 | 85 | In the function that we're gonna build we'll be taking advantage of ES6's tagged templates. Tagged templates are string literals which can be processed by a custom handler according to our needs (see [MDN docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates)). 86 | 87 | So essentially the signature of our function should look like this: 88 | 89 | ```js 90 | const jsx = (splits, ...values) => { 91 | // ... 92 | } 93 | ``` 94 | 95 | Since we're gonna heavily rely on regular expression, it will be much easier to deal with a consistent string, so we can unleash the regexp full potential. For now let's focus on the string part without the literal, and parse regular HTML string. Once we have that logic, we can implement string interpolation handling on top of it. 96 | -------------------------------------------------------------------------------- /.tortilla/manuals/templates/step1.tmpl: -------------------------------------------------------------------------------- 1 | As I already mentioned, our AST will be consisted of 3 node types, which means that we will have to create an ENUM that will contain the values `element`, `props` and `value`. This way the node types won't be hardcoded and patching the code can be very easy: 2 | 3 | {{{diffStep 1.1}}} 4 | 5 | Since we had 3 node types, it means that for each of them we should have a dedicated parsing function: 6 | 7 | {{{diffStep 1.2}}} 8 | 9 | Each function creates the basic node type and returns it. Note that at the begnning of the scope of each function I've defined a couple of variables: 10 | 11 | - `let match` - which will be used to store regular expression matches on the fly. 12 | - `let length` - which will be used to store the length of the match so we can trim the JSX code string right after and accumulate it in `node.length`. 13 | 14 | For now the `parseValue()` function is pretty straight forward and just returns a node which wraps the given string. 15 | 16 | We will begin with the implementation of the element node and we will branch out to other nodes as we go. First we will try to figure out the name of the element. If an element tag opener was not found, we will assume that the current part of the code is a value: 17 | 18 | {{{diffStep 1.3}}} 19 | 20 | Up next, we need to parse the props. To make things more efficient, we will need to first find the tag closer so we can provide the `parseProps()` method the relevant part of the string: 21 | 22 | {{{diffStep 1.4}}} 23 | 24 | Now that we've plucked the right substring, we can go ahead and implement the `parseProps()` function logic: 25 | 26 | {{{diffStep 1.5}}} 27 | 28 | The logic is pretty straight forward - we iterate through the string, and each time we try match the next key->value pair. Once a pair wasn't found, we return the node with the accumulated props. Note that providing only an attribute with no value is also a valid syntax which will set its value to `true` by default, thus the `/ *\w+/` regexp. Let's proceed where we left of with the element parsing implementation. 29 | 30 | We need to figure out whether the current element is self closing or not. If it is, we will return the node, and otherwise we will continue to parsing its children: 31 | 32 | {{{diffStep 1.6}}} 33 | 34 | Accordingly, we're gonna implement the children parsing logic: 35 | 36 | {{{diffStep 1.7}}} 37 | 38 | Children parsing is recursive. We keep calling the `parseElement()` method for the current substring until there's no more match. Once we've gone through all the children, we can finish the process by finding the closing tag: 39 | 40 | {{{diffStep 1.8}}} 41 | 42 | The HTML parsing part is finished! Now we can call the `parseElement()` for any given HTML string and we should get a JSON output which represents an AST, like the following: 43 | 44 | ```json 45 | { 46 | "type": "element", 47 | "props": { 48 | "type": "props", 49 | "length": 20, 50 | "props": { 51 | "onclick": "onclick()" 52 | } 53 | }, 54 | "children": [ 55 | { 56 | "type": "element", 57 | "props": { 58 | "type": "props", 59 | "length": 15, 60 | "props": { 61 | "src": "icon.svg" 62 | } 63 | }, 64 | "children": [], 65 | "length": 18, 66 | "name": "img" 67 | }, 68 | { 69 | "type": "element", 70 | "props": { 71 | "type": "props", 72 | "length": 0, 73 | "props": {} 74 | }, 75 | "children": [ 76 | { 77 | "type": "value", 78 | "length": 4, 79 | "value": "text" 80 | } 81 | ], 82 | "length": 12, 83 | "name": "span" 84 | } 85 | ], 86 | "length": 74, 87 | "name": "div" 88 | } 89 | ``` 90 | -------------------------------------------------------------------------------- /.tortilla/manuals/templates/step2.tmpl: -------------------------------------------------------------------------------- 1 | Now we're gonna add string interpolation on top of the HTML string parsing logic. Since we still wanna use the power of regexp at its full potential, we're gonna assume that the given string would be a template with placeholders, where each of them should be replaced with a value. That would be the easiest and most efficient way, rather than accepting an array of string splits. 2 | 3 | ```json 4 | [ 5 | "<__jsxPlaceholder>Hello __jsxPlaceholder", 6 | [MyComponent, "World", MyComponent] 7 | ] 8 | ``` 9 | 10 | Accordingly, we will update the parsing functions' signature and their calls, and we will define a placeholder constant: 11 | 12 | {{{diffStep 2.1}}} 13 | 14 | Note how I used the `Date.now()` function to define a postfix for the placeholder. This we can be sure that the same value won't be given by the user as a string (possible, very unlikely). Now we will go through each parsing function and we'll make sure that it knows how to deal with placeholders correctly. We will start with the `parseElement()` function. 15 | 16 | We will add an additional property to the node called: `node.tag`. The tag property is the component that will be used to create the React element. It can either be a string or a React.Component. If `node.name` is a placeholder, we will be taking the next value in the given values stack: 17 | 18 | {{{diffStep 2.2}}} 19 | 20 | We also made sure that the closing tag matches the opening tag. I've decided to "swallow" errors rather than throwing them for the sake of simplicity, but generally speaking it would make a lot of sense to implement error throws within the parsing functions. 21 | 22 | Up next would be the props node. This is fairly simple, we're only gonna add an additional regexp to the array of matchers, and that regexp will check for placeholders. If a placeholder was detected, we're gonna replace it with the next value in the values stack: 23 | 24 | {{{diffStep 2.3}}} 25 | 26 | Last but not least, would be the value node. This is the most complex to handle out of the 3 nodes, since it requires us to split the input string and create a dedicated value node out of each split. So now, instead of returning a single node value, we will return an array of them. Accordingly, we will also be changing the name of the function from `parseValue()` to `parseValues()`: 27 | 28 | {{{diffStep 2.4}}} 29 | 30 | The reason why I've decided to return an array of nodes and not a singe node which contains an array of values, just like the props node, is because it matches the signature of `React.createElement()` perfectly. The values will be passed as children with a spread operator (`...`), and you should see further this tutorial how this well it fits. 31 | 32 | Note that we've also changed the way we accumulate children in the `parseElement()` function. Since `parseValues()` returns an array now, and not a single node, we flatten it using an empty array concatenation (`[].concat()`), and we only push the children whose contents are not empty. 33 | -------------------------------------------------------------------------------- /.tortilla/manuals/templates/step3.tmpl: -------------------------------------------------------------------------------- 1 | At this point we should have a function which can transform a JSX code into an AST, including string interpolation. The only thing which is left to do now is build a function which will recursively create React elements out of the nodes in the tree. 2 | 3 | The main function of the module should be called with a template tag. If you went through the previous step, you should know that a consistent string has an advantage over an array of splits of strings, since we can unleash the full potential of a regexp with ease. Accordingly, we will take all the given splits and join them with the `placeholder` constant. 4 | 5 | ``` 6 | ['<', '> Hello ', ''] -> '<__jsxPlaceholder>Hello __jsxPlaceholder' 7 | ``` 8 | 9 | Once we join the string we can create React elements recursively: 10 | 11 | {{{diffStep 3.1}}} 12 | 13 | Note that if a node of value type is being iterated, we will just return the raw string, otherwise we will try to address its `node.children` property which doesn't exist. 14 | 15 | --- 16 | 17 | Our JSX runtime is now ready to use. You can view the source code at the official [Github repository](https://github.com/DAB0mB/jsx-runtime) or you can download using NPM and require via Node.JS: 18 | 19 | $ npm install jsx-runtime 20 | -------------------------------------------------------------------------------- /.tortilla/manuals/views/root.md: -------------------------------------------------------------------------------- 1 | ../../../README.md -------------------------------------------------------------------------------- /.tortilla/manuals/views/step1.md: -------------------------------------------------------------------------------- 1 | # Step 1: Starting with the core - an HTML parser 2 | 3 | [//]: # (head-end) 4 | 5 | 6 | As I already mentioned, our AST will be consisted of 3 node types, which means that we will have to create an ENUM that will contain the values `element`, `props` and `value`. This way the node types won't be hardcoded and patching the code can be very easy: 7 | 8 | [{]: (diffStep 1.1) 9 | 10 | #### [Step 1.1: Define node types ENUM](https://github.com/DAB0mB/jsx-runtime/commit/d341d00) 11 | 12 | ##### Changed package.json 13 | ```diff 14 | @@ -1,5 +1,6 @@ 15 | ┊1┊1┊{ 16 | ┊2┊2┊ "name": "jsx-runtime", 17 | +┊ ┊3┊ "version": "0.1.0", 18 | ┊3┊4┊ "description": "A runtime version of JSX", 19 | ┊4┊5┊ "main": "build/jsx-runtime.js", 20 | ┊5┊6┊ "repository": { 21 | ``` 22 | 23 | ##### Added src/index.js 24 | ```diff 25 | @@ -0,0 +1,5 @@ 26 | +┊ ┊1┊const types = { 27 | +┊ ┊2┊ element: 'element', 28 | +┊ ┊3┊ value: 'value', 29 | +┊ ┊4┊ props: 'props', 30 | +┊ ┊5┊}🚫↵ 31 | ``` 32 | 33 | [}]: # 34 | 35 | Since we had 3 node types, it means that for each of them we should have a dedicated parsing function: 36 | 37 | [{]: (diffStep 1.2) 38 | 39 | #### [Step 1.2: Define parse functions stubs](https://github.com/DAB0mB/jsx-runtime/commit/38b55a8) 40 | 41 | ##### Changed src/index.js 42 | ```diff 43 | @@ -2,4 +2,40 @@ 44 | ┊ 2┊ 2┊ element: 'element', 45 | ┊ 3┊ 3┊ value: 'value', 46 | ┊ 4┊ 4┊ props: 'props', 47 | -┊ 5┊ ┊}🚫↵ 48 | +┊ ┊ 5┊} 49 | +┊ ┊ 6┊ 50 | +┊ ┊ 7┊const parseElement = (str) => { 51 | +┊ ┊ 8┊ let match 52 | +┊ ┊ 9┊ let length 53 | +┊ ┊10┊ 54 | +┊ ┊11┊ const node = { 55 | +┊ ┊12┊ type: types.element, 56 | +┊ ┊13┊ props: parseProps(''), 57 | +┊ ┊14┊ children: [], 58 | +┊ ┊15┊ length: 0, 59 | +┊ ┊16┊ name: '', 60 | +┊ ┊17┊ } 61 | +┊ ┊18┊ 62 | +┊ ┊19┊ return node 63 | +┊ ┊20┊} 64 | +┊ ┊21┊ 65 | +┊ ┊22┊const parseProps = (str) => { 66 | +┊ ┊23┊ let match 67 | +┊ ┊24┊ let length 68 | +┊ ┊25┊ 69 | +┊ ┊26┊ const node = { 70 | +┊ ┊27┊ type: types.props, 71 | +┊ ┊28┊ length: 0, 72 | +┊ ┊29┊ props: {}, 73 | +┊ ┊30┊ } 74 | +┊ ┊31┊ 75 | +┊ ┊32┊ return node 76 | +┊ ┊33┊} 77 | +┊ ┊34┊ 78 | +┊ ┊35┊const parseValue = (str) => { 79 | +┊ ┊36┊ return { 80 | +┊ ┊37┊ type: types.value, 81 | +┊ ┊38┊ length: str.length, 82 | +┊ ┊39┊ value: str.trim(), 83 | +┊ ┊40┊ } 84 | +┊ ┊41┊} 85 | ``` 86 | 87 | [}]: # 88 | 89 | Each function creates the basic node type and returns it. Note that at the begnning of the scope of each function I've defined a couple of variables: 90 | 91 | - `let match` - which will be used to store regular expression matches on the fly. 92 | - `let length` - which will be used to store the length of the match so we can trim the JSX code string right after and accumulate it in `node.length`. 93 | 94 | For now the `parseValue()` function is pretty straight forward and just returns a node which wraps the given string. 95 | 96 | We will begin with the implementation of the element node and we will branch out to other nodes as we go. First we will try to figure out the name of the element. If an element tag opener was not found, we will assume that the current part of the code is a value: 97 | 98 | [{]: (diffStep 1.3) 99 | 100 | #### [Step 1.3: Check if value node](https://github.com/DAB0mB/jsx-runtime/commit/c126fc7) 101 | 102 | ##### Changed src/index.js 103 | ```diff 104 | @@ -16,6 +16,19 @@ 105 | ┊16┊16┊ name: '', 106 | ┊17┊17┊ } 107 | ┊18┊18┊ 108 | +┊ ┊19┊ match = str.match(/<(\w+)/) 109 | +┊ ┊20┊ 110 | +┊ ┊21┊ if (!match) { 111 | +┊ ┊22┊ str = str.split('<')[0] 112 | +┊ ┊23┊ 113 | +┊ ┊24┊ return parseValue(str) 114 | +┊ ┊25┊ } 115 | +┊ ┊26┊ 116 | +┊ ┊27┊ node.name = match[1] 117 | +┊ ┊28┊ length = match.index + match[0].length 118 | +┊ ┊29┊ str = str.slice(length) 119 | +┊ ┊30┊ node.length += length 120 | +┊ ┊31┊ 121 | ┊19┊32┊ return node 122 | ┊20┊33┊} 123 | ``` 124 | 125 | [}]: # 126 | 127 | Up next, we need to parse the props. To make things more efficient, we will need to first find the tag closer so we can provide the `parseProps()` method the relevant part of the string: 128 | 129 | [{]: (diffStep 1.4) 130 | 131 | #### [Step 1.4: Parse props](https://github.com/DAB0mB/jsx-runtime/commit/763712e) 132 | 133 | ##### Changed src/index.js 134 | ```diff 135 | @@ -29,6 +29,15 @@ 136 | ┊29┊29┊ str = str.slice(length) 137 | ┊30┊30┊ node.length += length 138 | ┊31┊31┊ 139 | +┊ ┊32┊ match = str.match(/>/) 140 | +┊ ┊33┊ 141 | +┊ ┊34┊ if (!match) return node 142 | +┊ ┊35┊ 143 | +┊ ┊36┊ node.props = parseProps(str.slice(0, match.index), values) 144 | +┊ ┊37┊ length = node.props.length 145 | +┊ ┊38┊ str = str.slice(length) 146 | +┊ ┊39┊ node.length += length 147 | +┊ ┊40┊ 148 | ┊32┊41┊ return node 149 | ┊33┊42┊} 150 | ``` 151 | 152 | [}]: # 153 | 154 | Now that we've plucked the right substring, we can go ahead and implement the `parseProps()` function logic: 155 | 156 | [{]: (diffStep 1.5) 157 | 158 | #### [Step 1.5: Implement props parsing logic](https://github.com/DAB0mB/jsx-runtime/commit/14ff2dd) 159 | 160 | ##### Changed src/index.js 161 | ```diff 162 | @@ -51,6 +51,27 @@ 163 | ┊51┊51┊ props: {}, 164 | ┊52┊52┊ } 165 | ┊53┊53┊ 166 | +┊ ┊54┊ const matchNextProp = () => { 167 | +┊ ┊55┊ match = 168 | +┊ ┊56┊ str.match(/ *\w+="(?:.*[^\\]")?/) || 169 | +┊ ┊57┊ str.match(/ *\w+/) 170 | +┊ ┊58┊ } 171 | +┊ ┊59┊ 172 | +┊ ┊60┊ matchNextProp() 173 | +┊ ┊61┊ 174 | +┊ ┊62┊ while (match) { 175 | +┊ ┊63┊ const propStr = match[0] 176 | +┊ ┊64┊ let [key, ...value] = propStr.split('=') 177 | +┊ ┊65┊ node.length += propStr.length 178 | +┊ ┊66┊ key = key.trim() 179 | +┊ ┊67┊ value = value.join('=') 180 | +┊ ┊68┊ value = value ? value.slice(1, -1) : true 181 | +┊ ┊69┊ node.props[key] = value 182 | +┊ ┊70┊ str = str.slice(0, match.index) + str.slice(match.index + propStr.length) 183 | +┊ ┊71┊ 184 | +┊ ┊72┊ matchNextProp() 185 | +┊ ┊73┊ } 186 | +┊ ┊74┊ 187 | ┊54┊75┊ return node 188 | ┊55┊76┊} 189 | ``` 190 | 191 | [}]: # 192 | 193 | The logic is pretty straight forward - we iterate through the string, and each time we try match the next key->value pair. Once a pair wasn't found, we return the node with the accumulated props. Note that providing only an attribute with no value is also a valid syntax which will set its value to `true` by default, thus the `/ *\w+/` regexp. Let's proceed where we left of with the element parsing implementation. 194 | 195 | We need to figure out whether the current element is self closing or not. If it is, we will return the node, and otherwise we will continue to parsing its children: 196 | 197 | [{]: (diffStep 1.6) 198 | 199 | #### [Step 1.6: Parse element closure](https://github.com/DAB0mB/jsx-runtime/commit/4b49cea) 200 | 201 | ##### Changed src/index.js 202 | ```diff 203 | @@ -38,6 +38,22 @@ 204 | ┊38┊38┊ str = str.slice(length) 205 | ┊39┊39┊ node.length += length 206 | ┊40┊40┊ 207 | +┊ ┊41┊ match = str.match(/^ *\/ *>/) 208 | +┊ ┊42┊ 209 | +┊ ┊43┊ if (match) { 210 | +┊ ┊44┊ node.length += match.index + match[0].length 211 | +┊ ┊45┊ 212 | +┊ ┊46┊ return node 213 | +┊ ┊47┊ } 214 | +┊ ┊48┊ 215 | +┊ ┊49┊ match = str.match(/>/) 216 | +┊ ┊50┊ 217 | +┊ ┊51┊ if (!match) return node 218 | +┊ ┊52┊ 219 | +┊ ┊53┊ length = match.index + 1 220 | +┊ ┊54┊ str = str.slice(length) 221 | +┊ ┊55┊ node.length += length 222 | +┊ ┊56┊ 223 | ┊41┊57┊ return node 224 | ┊42┊58┊} 225 | ``` 226 | 227 | [}]: # 228 | 229 | Accordingly, we're gonna implement the children parsing logic: 230 | 231 | [{]: (diffStep 1.7) 232 | 233 | #### [Step 1.7: Parse children](https://github.com/DAB0mB/jsx-runtime/commit/dad4502) 234 | 235 | ##### Changed src/index.js 236 | ```diff 237 | @@ -54,6 +54,16 @@ 238 | ┊54┊54┊ str = str.slice(length) 239 | ┊55┊55┊ node.length += length 240 | ┊56┊56┊ 241 | +┊ ┊57┊ let child = parseElement(str) 242 | +┊ ┊58┊ 243 | +┊ ┊59┊ while (child.type === types.element || child.value) { 244 | +┊ ┊60┊ length = child.length 245 | +┊ ┊61┊ str = str.slice(length) 246 | +┊ ┊62┊ node.length += length 247 | +┊ ┊63┊ node.children.push(child) 248 | +┊ ┊64┊ child = parseElement(str) 249 | +┊ ┊65┊ } 250 | +┊ ┊66┊ 251 | ┊57┊67┊ return node 252 | ┊58┊68┊} 253 | ``` 254 | 255 | [}]: # 256 | 257 | Children parsing is recursive. We keep calling the `parseElement()` method for the current substring until there's no more match. Once we've gone through all the children, we can finish the process by finding the closing tag: 258 | 259 | [{]: (diffStep 1.8) 260 | 261 | #### [Step 1.8: Parse closing tag](https://github.com/DAB0mB/jsx-runtime/commit/b9013d6) 262 | 263 | ##### Changed src/index.js 264 | ```diff 265 | @@ -64,6 +64,12 @@ 266 | ┊64┊64┊ child = parseElement(str) 267 | ┊65┊65┊ } 268 | ┊66┊66┊ 269 | +┊ ┊67┊ match = str.match(new RegExp(``)) 270 | +┊ ┊68┊ 271 | +┊ ┊69┊ if (!match) return node 272 | +┊ ┊70┊ 273 | +┊ ┊71┊ node.length += match.index + match[0].length 274 | +┊ ┊72┊ 275 | ┊67┊73┊ return node 276 | ┊68┊74┊} 277 | ``` 278 | 279 | [}]: # 280 | 281 | The HTML parsing part is finished! Now we can call the `parseElement()` for any given HTML string and we should get a JSON output which represents an AST, like the following: 282 | 283 | ```json 284 | { 285 | "type": "element", 286 | "props": { 287 | "type": "props", 288 | "length": 20, 289 | "props": { 290 | "onclick": "onclick()" 291 | } 292 | }, 293 | "children": [ 294 | { 295 | "type": "element", 296 | "props": { 297 | "type": "props", 298 | "length": 15, 299 | "props": { 300 | "src": "icon.svg" 301 | } 302 | }, 303 | "children": [], 304 | "length": 18, 305 | "name": "img" 306 | }, 307 | { 308 | "type": "element", 309 | "props": { 310 | "type": "props", 311 | "length": 0, 312 | "props": {} 313 | }, 314 | "children": [ 315 | { 316 | "type": "value", 317 | "length": 4, 318 | "value": "text" 319 | } 320 | ], 321 | "length": 12, 322 | "name": "span" 323 | } 324 | ], 325 | "length": 74, 326 | "name": "div" 327 | } 328 | ``` 329 | 330 | 331 | [//]: # (foot-start) 332 | 333 | [{]: (navStep) 334 | 335 | | [< Intro](https://github.com/DAB0mB/jsx-runtime/tree/master@0.1.0/README.md) | [Next Step >](https://github.com/DAB0mB/jsx-runtime/tree/master@0.1.0/.tortilla/manuals/views/step2.md) | 336 | |:--------------------------------|--------------------------------:| 337 | 338 | [}]: # 339 | -------------------------------------------------------------------------------- /.tortilla/manuals/views/step2.md: -------------------------------------------------------------------------------- 1 | # Step 2: Leveling up - string interpolation 2 | 3 | [//]: # (head-end) 4 | 5 | 6 | Now we're gonna add string interpolation on top of the HTML string parsing logic. Since we still wanna use the power of regexp at its full potential, we're gonna assume that the given string would be a template with placeholders, where each of them should be replaced with a value. That would be the easiest and most efficient way, rather than accepting an array of string splits. 7 | 8 | ```json 9 | [ 10 | "<__jsxPlaceholder>Hello __jsxPlaceholder", 11 | [MyComponent, "World", MyComponent] 12 | ] 13 | ``` 14 | 15 | Accordingly, we will update the parsing functions' signature and their calls, and we will define a placeholder constant: 16 | 17 | [{]: (diffStep 2.1) 18 | 19 | #### [Step 2.1: Initialize placeholder and update parsers signatures](https://github.com/DAB0mB/jsx-runtime/commit/733392d) 20 | 21 | ##### Changed src/index.js 22 | ```diff 23 | @@ -1,16 +1,18 @@ 24 | +┊ ┊ 1┊const placeholder = `__jsxPlaceholder${Date.now()}` 25 | +┊ ┊ 2┊ 26 | ┊ 1┊ 3┊const types = { 27 | ┊ 2┊ 4┊ element: 'element', 28 | ┊ 3┊ 5┊ value: 'value', 29 | ┊ 4┊ 6┊ props: 'props', 30 | ┊ 5┊ 7┊} 31 | ┊ 6┊ 8┊ 32 | -┊ 7┊ ┊const parseElement = (str) => { 33 | +┊ ┊ 9┊const parseElement = (str, values) => { 34 | ┊ 8┊10┊ let match 35 | ┊ 9┊11┊ let length 36 | ┊10┊12┊ 37 | ┊11┊13┊ const node = { 38 | ┊12┊14┊ type: types.element, 39 | -┊13┊ ┊ props: parseProps(''), 40 | +┊ ┊15┊ props: parseProps('', []), 41 | ┊14┊16┊ children: [], 42 | ┊15┊17┊ length: 0, 43 | ┊16┊18┊ name: '', 44 | ``` 45 | ```diff 46 | @@ -21,7 +23,7 @@ 47 | ┊21┊23┊ if (!match) { 48 | ┊22┊24┊ str = str.split('<')[0] 49 | ┊23┊25┊ 50 | -┊24┊ ┊ return parseValue(str) 51 | +┊ ┊26┊ return parseValue(str, values) 52 | ┊25┊27┊ } 53 | ┊26┊28┊ 54 | ┊27┊29┊ node.name = match[1] 55 | ``` 56 | ```diff 57 | @@ -54,14 +56,14 @@ 58 | ┊54┊56┊ str = str.slice(length) 59 | ┊55┊57┊ node.length += length 60 | ┊56┊58┊ 61 | -┊57┊ ┊ let child = parseElement(str) 62 | +┊ ┊59┊ let child = parseElement(str, values) 63 | ┊58┊60┊ 64 | ┊59┊61┊ while (child.type === types.element || child.value) { 65 | ┊60┊62┊ length = child.length 66 | ┊61┊63┊ str = str.slice(length) 67 | ┊62┊64┊ node.length += length 68 | ┊63┊65┊ node.children.push(child) 69 | -┊64┊ ┊ child = parseElement(str) 70 | +┊ ┊66┊ child = parseElement(str, values) 71 | ┊65┊67┊ } 72 | ┊66┊68┊ 73 | ┊67┊69┊ match = str.match(new RegExp(``)) 74 | ``` 75 | ```diff 76 | @@ -73,7 +75,7 @@ 77 | ┊73┊75┊ return node 78 | ┊74┊76┊} 79 | ┊75┊77┊ 80 | -┊76┊ ┊const parseProps = (str) => { 81 | +┊ ┊78┊const parseProps = (str, values) => { 82 | ┊77┊79┊ let match 83 | ┊78┊80┊ let length 84 | ┊79┊81┊ 85 | ``` 86 | ```diff 87 | @@ -107,7 +109,7 @@ 88 | ┊107┊109┊ return node 89 | ┊108┊110┊} 90 | ┊109┊111┊ 91 | -┊110┊ ┊const parseValue = (str) => { 92 | +┊ ┊112┊const parseValue = (str, values) => { 93 | ┊111┊113┊ return { 94 | ┊112┊114┊ type: types.value, 95 | ┊113┊115┊ length: str.length, 96 | ``` 97 | 98 | [}]: # 99 | 100 | Note how I used the `Date.now()` function to define a postfix for the placeholder. This we can be sure that the same value won't be given by the user as a string (possible, very unlikely). Now we will go through each parsing function and we'll make sure that it knows how to deal with placeholders correctly. We will start with the `parseElement()` function. 101 | 102 | We will add an additional property to the node called: `node.tag`. The tag property is the component that will be used to create the React element. It can either be a string or a React.Component. If `node.name` is a placeholder, we will be taking the next value in the given values stack: 103 | 104 | [{]: (diffStep 2.2) 105 | 106 | #### [Step 2.2: Handle placeholder when parsing element](https://github.com/DAB0mB/jsx-runtime/commit/800bac3) 107 | 108 | ##### Changed src/index.js 109 | ```diff 110 | @@ -27,6 +27,7 @@ 111 | ┊27┊27┊ } 112 | ┊28┊28┊ 113 | ┊29┊29┊ node.name = match[1] 114 | +┊ ┊30┊ node.tag = node.name === placeholder ? values.shift() : node.name 115 | ┊30┊31┊ length = match.index + match[0].length 116 | ┊31┊32┊ str = str.slice(length) 117 | ┊32┊33┊ node.length += length 118 | ``` 119 | ```diff 120 | @@ -72,6 +73,12 @@ 121 | ┊72┊73┊ 122 | ┊73┊74┊ node.length += match.index + match[0].length 123 | ┊74┊75┊ 124 | +┊ ┊76┊ if (node.name === placeholder) { 125 | +┊ ┊77┊ const value = values.shift() 126 | +┊ ┊78┊ 127 | +┊ ┊79┊ if (value !== node.tag) return node 128 | +┊ ┊80┊ } 129 | +┊ ┊81┊ 130 | ┊75┊82┊ return node 131 | ┊76┊83┊} 132 | ``` 133 | 134 | [}]: # 135 | 136 | We also made sure that the closing tag matches the opening tag. I've decided to "swallow" errors rather than throwing them for the sake of simplicity, but generally speaking it would make a lot of sense to implement error throws within the parsing functions. 137 | 138 | Up next would be the props node. This is fairly simple, we're only gonna add an additional regexp to the array of matchers, and that regexp will check for placeholders. If a placeholder was detected, we're gonna replace it with the next value in the values stack: 139 | 140 | [{]: (diffStep 2.3) 141 | 142 | #### [Step 2.3: Handle placeholder when parsing props](https://github.com/DAB0mB/jsx-runtime/commit/8a4887e) 143 | 144 | ##### Changed src/index.js 145 | ```diff 146 | @@ -95,6 +95,7 @@ 147 | ┊ 95┊ 95┊ const matchNextProp = () => { 148 | ┊ 96┊ 96┊ match = 149 | ┊ 97┊ 97┊ str.match(/ *\w+="(?:.*[^\\]")?/) || 150 | +┊ ┊ 98┊ str.match(new RegExp(` *\\w+=${placeholder}`)) || 151 | ┊ 98┊ 99┊ str.match(/ *\w+/) 152 | ┊ 99┊100┊ } 153 | ┊100┊101┊ 154 | ``` 155 | ```diff 156 | @@ -106,7 +107,9 @@ 157 | ┊106┊107┊ node.length += propStr.length 158 | ┊107┊108┊ key = key.trim() 159 | ┊108┊109┊ value = value.join('=') 160 | -┊109┊ ┊ value = value ? value.slice(1, -1) : true 161 | +┊ ┊110┊ value = 162 | +┊ ┊111┊ value === placeholder ? values.shift() : 163 | +┊ ┊112┊ value ? value.slice(1, -1) : true 164 | ┊110┊113┊ node.props[key] = value 165 | ┊111┊114┊ str = str.slice(0, match.index) + str.slice(match.index + propStr.length) 166 | ``` 167 | 168 | [}]: # 169 | 170 | Last but not least, would be the value node. This is the most complex to handle out of the 3 nodes, since it requires us to split the input string and create a dedicated value node out of each split. So now, instead of returning a single node value, we will return an array of them. Accordingly, we will also be changing the name of the function from `parseValue()` to `parseValues()`: 171 | 172 | [{]: (diffStep 2.4) 173 | 174 | #### [Step 2.4: Handle placeholder when parsing value](https://github.com/DAB0mB/jsx-runtime/commit/9afcbee) 175 | 176 | ##### Changed src/index.js 177 | ```diff 178 | @@ -23,7 +23,7 @@ 179 | ┊23┊23┊ if (!match) { 180 | ┊24┊24┊ str = str.split('<')[0] 181 | ┊25┊25┊ 182 | -┊26┊ ┊ return parseValue(str, values) 183 | +┊ ┊26┊ return parseValues(str, values) 184 | ┊27┊27┊ } 185 | ┊28┊28┊ 186 | ┊29┊29┊ node.name = match[1] 187 | ``` 188 | ```diff 189 | @@ -57,14 +57,26 @@ 190 | ┊57┊57┊ str = str.slice(length) 191 | ┊58┊58┊ node.length += length 192 | ┊59┊59┊ 193 | -┊60┊ ┊ let child = parseElement(str, values) 194 | +┊ ┊60┊ let children = [] 195 | ┊61┊61┊ 196 | -┊62┊ ┊ while (child.type === types.element || child.value) { 197 | -┊63┊ ┊ length = child.length 198 | -┊64┊ ┊ str = str.slice(length) 199 | -┊65┊ ┊ node.length += length 200 | -┊66┊ ┊ node.children.push(child) 201 | -┊67┊ ┊ child = parseElement(str, values) 202 | +┊ ┊62┊ const parseNextChildren = () => { 203 | +┊ ┊63┊ children = [].concat(parseElement(str, values)) 204 | +┊ ┊64┊ } 205 | +┊ ┊65┊ 206 | +┊ ┊66┊ parseNextChildren() 207 | +┊ ┊67┊ 208 | +┊ ┊68┊ while (children.length) { 209 | +┊ ┊69┊ children.forEach((child) => { 210 | +┊ ┊70┊ length = child.length 211 | +┊ ┊71┊ str = str.slice(length) 212 | +┊ ┊72┊ node.length += length 213 | +┊ ┊73┊ 214 | +┊ ┊74┊ if (child.type !== types.value || child.value) { 215 | +┊ ┊75┊ node.children.push(child) 216 | +┊ ┊76┊ } 217 | +┊ ┊77┊ }) 218 | +┊ ┊78┊ 219 | +┊ ┊79┊ parseNextChildren() 220 | ┊68┊80┊ } 221 | ┊69┊81┊ 222 | ┊70┊82┊ match = str.match(new RegExp(``)) 223 | ``` 224 | ```diff 225 | @@ -119,10 +131,40 @@ 226 | ┊119┊131┊ return node 227 | ┊120┊132┊} 228 | ┊121┊133┊ 229 | -┊122┊ ┊const parseValue = (str, values) => { 230 | -┊123┊ ┊ return { 231 | -┊124┊ ┊ type: types.value, 232 | -┊125┊ ┊ length: str.length, 233 | -┊126┊ ┊ value: str.trim(), 234 | -┊127┊ ┊ } 235 | +┊ ┊134┊const parseValues = (str, values) => { 236 | +┊ ┊135┊ const nodes = [] 237 | +┊ ┊136┊ 238 | +┊ ┊137┊ str.split(placeholder).forEach((split, index, splits) => { 239 | +┊ ┊138┊ let value 240 | +┊ ┊139┊ let length 241 | +┊ ┊140┊ 242 | +┊ ┊141┊ value = split 243 | +┊ ┊142┊ length = split.length 244 | +┊ ┊143┊ str = str.slice(length) 245 | +┊ ┊144┊ 246 | +┊ ┊145┊ if (length) { 247 | +┊ ┊146┊ nodes.push({ 248 | +┊ ┊147┊ type: types.value, 249 | +┊ ┊148┊ length, 250 | +┊ ┊149┊ value, 251 | +┊ ┊150┊ }) 252 | +┊ ┊151┊ } 253 | +┊ ┊152┊ 254 | +┊ ┊153┊ if (index === splits.length - 1) return 255 | +┊ ┊154┊ 256 | +┊ ┊155┊ value = values.pop() 257 | +┊ ┊156┊ length = placeholder.length 258 | +┊ ┊157┊ 259 | +┊ ┊158┊ if (typeof value === 'string') { 260 | +┊ ┊159┊ value = value.trim() 261 | +┊ ┊160┊ } 262 | +┊ ┊161┊ 263 | +┊ ┊162┊ nodes.push({ 264 | +┊ ┊163┊ type: types.value, 265 | +┊ ┊164┊ length, 266 | +┊ ┊165┊ value, 267 | +┊ ┊166┊ }) 268 | +┊ ┊167┊ }) 269 | +┊ ┊168┊ 270 | +┊ ┊169┊ return nodes 271 | ┊128┊170┊} 272 | ``` 273 | 274 | [}]: # 275 | 276 | The reason why I've decided to return an array of nodes and not a singe node which contains an array of values, just like the props node, is because it matches the signature of `React.createElement()` perfectly. The values will be passed as children with a spread operator (`...`), and you should see further this tutorial how this well it fits. 277 | 278 | Note that we've also changed the way we accumulate children in the `parseElement()` function. Since `parseValues()` returns an array now, and not a single node, we flatten it using an empty array concatenation (`[].concat()`), and we only push the children whose contents are not empty. 279 | 280 | 281 | [//]: # (foot-start) 282 | 283 | [{]: (navStep) 284 | 285 | | [< Previous Step](https://github.com/DAB0mB/jsx-runtime/tree/master@0.1.0/.tortilla/manuals/views/step1.md) | [Next Step >](https://github.com/DAB0mB/jsx-runtime/tree/master@0.1.0/.tortilla/manuals/views/step3.md) | 286 | |:--------------------------------|--------------------------------:| 287 | 288 | [}]: # 289 | -------------------------------------------------------------------------------- /.tortilla/manuals/views/step3.md: -------------------------------------------------------------------------------- 1 | # Step 3: The grand finale - execution 2 | 3 | [//]: # (head-end) 4 | 5 | 6 | At this point we should have a function which can transform a JSX code into an AST, including string interpolation. The only thing which is left to do now is build a function which will recursively create React elements out of the nodes in the tree. 7 | 8 | The main function of the module should be called with a template tag. If you went through the previous step, you should know that a consistent string has an advantage over an array of splits of strings, since we can unleash the full potential of a regexp with ease. Accordingly, we will take all the given splits and join them with the `placeholder` constant. 9 | 10 | ``` 11 | ['<', '> Hello ', ''] -> '<__jsxPlaceholder>Hello __jsxPlaceholder' 12 | ``` 13 | 14 | Once we join the string we can create React elements recursively: 15 | 16 | [{]: (diffStep 3.1) 17 | 18 | #### [Step 3.1: Create React elements out of nodes](https://github.com/DAB0mB/jsx-runtime/commit/56cb516) 19 | 20 | ##### Changed src/index.js 21 | ```diff 22 | @@ -1,3 +1,5 @@ 23 | +┊ ┊1┊import React from 'react' 24 | +┊ ┊2┊ 25 | ┊1┊3┊const placeholder = `__jsxPlaceholder${Date.now()}` 26 | ┊2┊4┊ 27 | ┊3┊5┊const types = { 28 | ``` 29 | ```diff 30 | @@ -6,6 +8,24 @@ 31 | ┊ 6┊ 8┊ props: 'props', 32 | ┊ 7┊ 9┊} 33 | ┊ 8┊10┊ 34 | +┊ ┊11┊export const jsx = (splits, ...values) => { 35 | +┊ ┊12┊ const root = parseElement(splits.join(placeholder), values) 36 | +┊ ┊13┊ 37 | +┊ ┊14┊ return createReactElement(root) 38 | +┊ ┊15┊} 39 | +┊ ┊16┊ 40 | +┊ ┊17┊const createReactElement = (node) => { 41 | +┊ ┊18┊ if (node.type === types.value) { 42 | +┊ ┊19┊ return node.value 43 | +┊ ┊20┊ } 44 | +┊ ┊21┊ 45 | +┊ ┊22┊ return React.createElement( 46 | +┊ ┊23┊ node.tag, 47 | +┊ ┊24┊ node.props.props, 48 | +┊ ┊25┊ ...node.children.map(createReactElement), 49 | +┊ ┊26┊ ) 50 | +┊ ┊27┊} 51 | +┊ ┊28┊ 52 | ┊ 9┊29┊const parseElement = (str, values) => { 53 | ┊10┊30┊ let match 54 | ┊11┊31┊ let length 55 | ``` 56 | ```diff 57 | @@ -168,3 +188,5 @@ 58 | ┊168┊188┊ 59 | ┊169┊189┊ return nodes 60 | ┊170┊190┊} 61 | +┊ ┊191┊ 62 | +┊ ┊192┊export default jsx 63 | ``` 64 | 65 | [}]: # 66 | 67 | Note that if a node of value type is being iterated, we will just return the raw string, otherwise we will try to address its `node.children` property which doesn't exist. 68 | 69 | --- 70 | 71 | Our JSX runtime is now ready to use. You can view the source code at the official [Github repository](https://github.com/DAB0mB/jsx-runtime) or you can download using NPM and require via Node.JS: 72 | 73 | $ npm install jsx-runtime 74 | 75 | 76 | [//]: # (foot-start) 77 | 78 | [{]: (navStep) 79 | 80 | | [< Previous Step](https://github.com/DAB0mB/jsx-runtime/tree/master@0.1.0/.tortilla/manuals/views/step2.md) | 81 | |:----------------------| 82 | 83 | [}]: # 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jsx Runtime 2 | 3 | [//]: # (head-end) 4 | 5 | 6 | # Implementing a runtime version of JSX 7 | 8 | ## And learning how to think like a JSX parser 9 | 10 | JSX is one of the most commonly used syntax extensions out there. Originally JSX was parsed via a [Facebook fork of Esprima](https://github.com/facebookarchive/esprima) - a JavaScript syntax parser developed by jQuery. As it gained momentum, [Acorn](https://github.com/acornjs/acorn) took things to their hands and decided to make their own version of the parser which ended up being 1.5–2x faster than Esprima-fb, and is now being used by Babel. 11 | 12 | It definitely went through an evolution, but regardless of its phase, all parsers had a similar output - which is an AST. Once we have an AST representation of the JSX code, interpretation is extremely easy. 13 | 14 | Today we're gonna understand how a JSX parser thinks by implementing one of our own. Unlike Babel, rather than compiling, we're gonna evaluate the nodes in the AST according to their types, which means that we will be able to use JSX during runtime. 15 | Below is an example of the final product: 16 | 17 | ```js 18 | class Hello extends React.Component { 19 | render() { 20 | return jsx `
Hello ${this.props.name}
` 21 | } 22 | } 23 | 24 | ReactDOM.render( 25 | jsx `<${Hello} name="World" />`, 26 | document.getElementById('container') 27 | ) 28 | ``` 29 | 30 | Before we go ahead and rush to implementing the parser let's understand what we're aiming for. JSX simply takes an HTML-like syntax and transforms it into nested `React.createElement()` calls. What makes JSX unique is that we can use string interpolation within our HTML templates, so we can provide it with data which doesn't necessarily has to be serialized, things like functions, arrays, or objects. 31 | So given the following code: 32 | 33 | ```js 34 | const el = (props) => ( 35 |
36 | {props.text} 37 |
38 | ) 39 | ``` 40 | 41 | We should get the following output once compiling it with Babel: 42 | 43 | ```js 44 | const el = (props) => ( 45 | React.createElement( 46 | "div", 47 | { onClick: props.onClick }, 48 | React.createElement(Icon, { src: props.icon }), 49 | React.createElement( 50 | "span", 51 | null, 52 | props.text 53 | ) 54 | ) 55 | ) 56 | ``` 57 | 58 | Just aquick reminder - the compiled result should be used internally by ReactDOM to differentiate changes in the virtual DOM and then render them. This is something which is React specific and has nothing to do with JSX, so at this point we have achieved our goal. 59 | 60 | Essentially there are 3 things we should figure out when parsing a JSX code: 61 | 62 | - The name / component of the React element. 63 | - The props of the React element. 64 | - The children of the React element, for each this process should repeat itself recursively. 65 | 66 | As I mentioned earlier, it would be best if we could break down the code into nodes first and represent it as an AST. Looking at the input of the example above, we can roughly visualize how we would pluck the nodes from the code: 67 | 68 | ![Analyzing the JSX code.](https://cdn-images-1.medium.com/max/1600/1*AqTHDuxX5NNCI3iLycVfxA.png) 69 | 70 | And to put things simple, here's a schematic representation of the analysis above: 71 | 72 | ![A schematic representation of the analysis.](https://cdn-images-1.medium.com/max/1600/1*i8h2MocLHni8mTuPaakwBQ.png) 73 | 74 | Accordingly, we're gonna have 3 types of nodes: 75 | 76 | - Element node. 77 | - Props node. 78 | - Value node. 79 | 80 | Let's decide that each node has a base schema with the following properties: 81 | 82 | - node.type - which will represent the type name of the node, e.g. `element`, `props` and `value`. Based on the node type we can also determine that additional properties that the node's gonna carry. In our parser, each node type should have the following additional properties: 83 | 84 | ![node type schemas](https://cdn-images-1.medium.com/max/1600/1*dgAy6Zbj6ttfNqgppWIjug.png) 85 | 86 | - node.length -which represents the length of the sub-string in the code that the node occupies. This will help us trim the code string as we go with the parsing process so we can always focus on relevant parts of the string for the current node: 87 | 88 | ![Any time we parse a small part of the string, we slice the part we've just parsed.](https://cdn-images-1.medium.com/max/1600/1*PeiZnuBTKfLlDiaL24dgHw.png) 89 | 90 | In the function that we're gonna build we'll be taking advantage of ES6's tagged templates. Tagged templates are string literals which can be processed by a custom handler according to our needs (see [MDN docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#Tagged_templates)). 91 | 92 | So essentially the signature of our function should look like this: 93 | 94 | ```js 95 | const jsx = (splits, ...values) => { 96 | // ... 97 | } 98 | ``` 99 | 100 | Since we're gonna heavily rely on regular expression, it will be much easier to deal with a consistent string, so we can unleash the regexp full potential. For now let's focus on the string part without the literal, and parse regular HTML string. Once we have that logic, we can implement string interpolation handling on top of it. 101 | 102 | 103 | [//]: # (foot-start) 104 | 105 | [{]: (navStep) 106 | 107 | | [Begin Tutorial >](.tortilla/manuals/views/step1.md) | 108 | |----------------------:| 109 | 110 | [}]: # 111 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsx-runtime", 3 | "version": "0.1.0", 4 | "description": "A runtime version of JSX", 5 | "main": "build/jsx-runtime.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://DAB0mB@github.com/DAB0mB/jsx-runtime.git" 9 | }, 10 | "scripts": { 11 | "build": "webpack --config webpack_config.js", 12 | "prepublish": "npm run build" 13 | }, 14 | "devDependencies": { 15 | "babel-core": "^6.26.3", 16 | "babel-eslint": "^9.0.0", 17 | "babel-loader": "^7.1.5", 18 | "babel-preset-es2017": "^6.24.1", 19 | "eslint": "^5.1.0", 20 | "eslint-loader": "^2.1.0", 21 | "eslint-plugin-react": "^7.11.1", 22 | "react": "^16.5.2", 23 | "webpack": "^4.16.1", 24 | "webpack-cli": "^3.1.0", 25 | "webpack-node-externals": "^1.7.2" 26 | }, 27 | "peerDependencies": { 28 | "react": "*" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const placeholder = `__jsxPlaceholder${Date.now()}` 4 | 5 | const types = { 6 | element: 'element', 7 | value: 'value', 8 | props: 'props', 9 | } 10 | 11 | export const jsx = (splits, ...values) => { 12 | const root = parseElement(splits.join(placeholder), values) 13 | 14 | return createReactElement(root) 15 | } 16 | 17 | const createReactElement = (node) => { 18 | if (node.type === types.value) { 19 | return node.value 20 | } 21 | 22 | return React.createElement( 23 | node.tag, 24 | node.props.props, 25 | ...node.children.map(createReactElement), 26 | ) 27 | } 28 | 29 | const parseElement = (str, values) => { 30 | let match 31 | let length 32 | 33 | const node = { 34 | type: types.element, 35 | props: parseProps('', []), 36 | children: [], 37 | length: 0, 38 | name: '', 39 | } 40 | 41 | match = str.match(/<(\w+)/) 42 | 43 | if (!match) { 44 | str = str.split('<')[0] 45 | 46 | return parseValues(str, values) 47 | } 48 | 49 | node.name = match[1] 50 | node.tag = node.name === placeholder ? values.shift() : node.name 51 | length = match.index + match[0].length 52 | str = str.slice(length) 53 | node.length += length 54 | 55 | match = str.match(/>/) 56 | 57 | if (!match) return node 58 | 59 | node.props = parseProps(str.slice(0, match.index), values) 60 | length = node.props.length 61 | str = str.slice(length) 62 | node.length += length 63 | 64 | match = str.match(/^ *\/ *>/) 65 | 66 | if (match) { 67 | node.length += match.index + match[0].length 68 | 69 | return node 70 | } 71 | 72 | match = str.match(/>/) 73 | 74 | if (!match) return node 75 | 76 | length = match.index + 1 77 | str = str.slice(length) 78 | node.length += length 79 | 80 | let children = [] 81 | 82 | const parseNextChildren = () => { 83 | children = [].concat(parseElement(str, values)) 84 | } 85 | 86 | parseNextChildren() 87 | 88 | while (children.length) { 89 | children.forEach((child) => { 90 | length = child.length 91 | str = str.slice(length) 92 | node.length += length 93 | 94 | if (child.type !== types.value || child.value) { 95 | node.children.push(child) 96 | } 97 | }) 98 | 99 | parseNextChildren() 100 | } 101 | 102 | match = str.match(new RegExp(``)) 103 | 104 | if (!match) return node 105 | 106 | node.length += match.index + match[0].length 107 | 108 | if (node.name === placeholder) { 109 | const value = values.shift() 110 | 111 | if (value !== node.tag) return node 112 | } 113 | 114 | return node 115 | } 116 | 117 | const parseProps = (str, values) => { 118 | let match 119 | let length 120 | 121 | const node = { 122 | type: types.props, 123 | length: 0, 124 | props: {}, 125 | } 126 | 127 | const matchNextProp = () => { 128 | match = 129 | str.match(/ *\w+="(?:.*[^\\]")?/) || 130 | str.match(new RegExp(` *\\w+=${placeholder}`)) || 131 | str.match(/ *\w+/) 132 | } 133 | 134 | matchNextProp() 135 | 136 | while (match) { 137 | const propStr = match[0] 138 | let [key, ...value] = propStr.split('=') 139 | node.length += propStr.length 140 | key = key.trim() 141 | value = value.join('=') 142 | value = 143 | value === placeholder ? values.shift() : 144 | value ? value.slice(1, -1) : true 145 | node.props[key] = value 146 | str = str.slice(0, match.index) + str.slice(match.index + propStr.length) 147 | 148 | matchNextProp() 149 | } 150 | 151 | return node 152 | } 153 | 154 | const parseValues = (str, values) => { 155 | const nodes = [] 156 | 157 | str.split(placeholder).forEach((split, index, splits) => { 158 | let value 159 | let length 160 | 161 | value = split 162 | length = split.length 163 | str = str.slice(length) 164 | 165 | if (length) { 166 | nodes.push({ 167 | type: types.value, 168 | length, 169 | value, 170 | }) 171 | } 172 | 173 | if (index === splits.length - 1) return 174 | 175 | value = values.pop() 176 | length = placeholder.length 177 | 178 | if (typeof value === 'string') { 179 | value = value.trim() 180 | } 181 | 182 | nodes.push({ 183 | type: types.value, 184 | length, 185 | value, 186 | }) 187 | }) 188 | 189 | return nodes 190 | } 191 | 192 | export default jsx 193 | -------------------------------------------------------------------------------- /webpack_config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const nodeExternals = require('webpack-node-externals') 3 | 4 | module.exports = { 5 | mode: 'none', 6 | devtool: 'sourcemap', 7 | entry: [ 8 | path.resolve(__dirname, 'src') 9 | ], 10 | output: { 11 | path: path.resolve(__dirname, 'build'), 12 | filename: 'jsx-runtime.js', 13 | library: '', 14 | libraryTarget: 'commonjs2' 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | exclude: [/node_modules/], 21 | use: [ 22 | 'babel-loader', 23 | 'eslint-loader' 24 | ] 25 | }, 26 | { 27 | test: /\.json$/, 28 | loader: 'json-loader' 29 | } 30 | ] 31 | }, 32 | externals: [nodeExternals()] 33 | } 34 | --------------------------------------------------------------------------------