├── .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 | 
64 |
65 | And to put things simple, here's a schematic representation of the analysis above:
66 |
67 | 
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 | 
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 | 
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(`${node.name}>`))
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(`${node.name}>`))
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(`${node.name}>`))
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 | 
69 |
70 | And to put things simple, here's a schematic representation of the analysis above:
71 |
72 | 
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 | 
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 | 
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(`${node.name}>`))
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 |
--------------------------------------------------------------------------------