Skip to content

Commit db94972

Browse files
authored
Add support using x as a JSX pragma
This PR tests that `xastscript` can be used as the pragma for JSX with bublé and babel. Code-wise, this adds support for using `x` to generate root nodes. This is done by omitting the tag name (like so: `x()`, `x(null, 'child')`). Previously, omitting a `name` resulted in an exception. Another aspect of supporting JSX is supporting fragments as children. As fragments yield root nodes, we unravel them and use only their children. While this could be seen a change, xast prohibits roots occurring in nodes, so the unraveling instead fixes what would otherwise be a broken tree. Related to: GH-3. Reviewed-by: Christian Murphy <[email protected]>
1 parent 46f5aa3 commit db94972

File tree

8 files changed

+297
-16
lines changed

8 files changed

+297
-16
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
.nyc_output/
44
coverage/
55
node_modules/
6+
test/jsx-*.js
67
yarn.lock

index.js

+11-3
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,21 @@ module.exports = x
44

55
// Creating xast elements.
66
function x(name, attributes) {
7-
var node = {type: 'element', name: name, attributes: {}, children: []}
7+
var node =
8+
name == null
9+
? {type: 'root', children: []}
10+
: {type: 'element', name: name, attributes: {}, children: []}
811
var index = 1
912
var key
1013

11-
if (typeof name !== 'string' || !name) {
14+
if (name != null && typeof name !== 'string') {
1215
throw new Error('Expected element name, got `' + name + '`')
1316
}
1417

1518
// Handle props.
1619
if (attributes) {
1720
if (
21+
name == null ||
1822
typeof attributes === 'string' ||
1923
typeof attributes === 'number' ||
2024
'length' in attributes
@@ -51,7 +55,11 @@ function addChild(nodes, value) {
5155
addChild(nodes, value[index])
5256
}
5357
} else if (typeof value === 'object' && value.type) {
54-
nodes.push(value)
58+
if (value.type === 'root') {
59+
addChild(nodes, value.children)
60+
} else {
61+
nodes.push(value)
62+
}
5563
} else {
5664
throw new TypeError('Expected node, nodes, string, got `' + value + '`')
5765
}

package.json

+8-2
Original file line numberDiff line numberDiff line change
@@ -34,20 +34,26 @@
3434
"@types/xast": "^1.0.0"
3535
},
3636
"devDependencies": {
37+
"@babel/core": "^7.0.0",
38+
"@babel/plugin-syntax-jsx": "^7.0.0",
39+
"@babel/plugin-transform-react-jsx": "^7.0.0",
40+
"buble": "^0.20.0",
3741
"dtslint": "^4.0.0",
3842
"nyc": "^15.0.0",
3943
"prettier": "^2.0.0",
4044
"remark-cli": "^9.0.0",
4145
"remark-preset-wooorm": "^8.0.0",
4246
"tape": "^5.0.0",
47+
"unist-builder": "^2.0.0",
4348
"xo": "^0.34.0"
4449
},
4550
"scripts": {
51+
"generate": "node script/generate-jsx",
4652
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
4753
"test-api": "node test",
48-
"test-coverage": "nyc --reporter lcov tape test.js",
54+
"test-coverage": "nyc --reporter lcov tape test/index.js",
4955
"test-types": "dtslint types",
50-
"test": "npm run format && npm run test-coverage && npm run test-types"
56+
"test": "npm run generate && npm run format && npm run test-coverage && npm run test-types"
5157
},
5258
"nyc": {
5359
"check-coverage": true,

readme.md

+78-7
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ console.log(
4848
// For other xast nodes, such as comments, instructions, doctypes, or cdata
4949
// can be created with unist-builder:
5050
console.log(
51-
u('root', [
51+
x(null, [
5252
u('instruction', {name: 'xml'}, 'version="1.0" encoding="UTF-8"'),
5353
x('album', [
5454
u('comment', 'Great album!'),
@@ -142,33 +142,94 @@ Yields:
142142

143143
## API
144144

145-
### `x(name[, attributes][, …children])`
145+
### `x(name?[, attributes][, …children])`
146146

147147
Create XML *[trees][tree]* in **[xast][]**.
148148

149+
##### Signatures
150+
151+
* `x(): root`
152+
* `x(null[, …children]): root`
153+
* `x(name[, attributes][, …children]): element`
154+
149155
##### Parameters
150156

151157
###### `name`
152158

153-
Qualified name (`string`).
159+
Qualified name (`string`, optional).
154160
Case sensitive and can contain a namespace prefix (such as `rdf:RDF`).
161+
When string, an [`Element`][element] is built.
162+
When nullish, a [`Root`][root] is built instead.
155163

156164
###### `attributes`
157165

158166
Map of attributes (`Object.<*>`, optional).
159167
Nullish (`null` or `undefined`) or `NaN` values are ignored, other values are
160168
turned to strings.
161169

162-
Cannot be omitted if `children` is a `Node`.
170+
Cannot be given if building a [`Root`][root].
171+
Cannot be omitted when building an [`Element`][element] if the first child is a
172+
[`Node`][node].
163173

164174
###### `children`
165175

166-
(Lists of) child nodes (`string`, `Node`, `Array.<children>`, optional).
167-
When strings are encountered, they are mapped to [`text`][text] nodes.
176+
(Lists of) children (`string`, `number`, `Node`, `Array.<children>`, optional).
177+
When strings or numbers are encountered, they are mapped to [`Text`][text]
178+
nodes.
179+
If a [`Root`][root] node is given, its children are used instead.
168180

169181
##### Returns
170182

171-
[`Element`][element].
183+
[`Element`][element] or [`Root`][root].
184+
185+
## JSX
186+
187+
`xastscript` can be used as a pragma for JSX.
188+
The example above (omitting the second) can then be written like so:
189+
190+
```jsx
191+
var u = require('unist-builder')
192+
var x = require('xastscript')
193+
194+
console.log(
195+
<album id={123}>
196+
<name>Born in the U.S.A.</name>
197+
<artist>Bruce Springsteen</artist>
198+
<releasedate>1984-04-06</releasedate>
199+
</album>
200+
)
201+
202+
console.log(
203+
<>
204+
{u('instruction', {name: 'xml'}, 'version="1.0" encoding="UTF-8"')}
205+
<album>
206+
{u('comment', 'Great album!')}
207+
<name>Born in the U.S.A.</name>
208+
<description>{u('cdata', '3 < 5 & 8 > 13')}</description>
209+
</album>
210+
</>
211+
)
212+
```
213+
214+
Note that you must still import `xastscript` yourself and configure your
215+
JavaScript compiler to use the identifier you assign it to as a pragma (and
216+
pass `null` for fragments).
217+
218+
For [bublé][], this can be done by setting `jsx: 'x'` and `jsxFragment: 'null'`
219+
(note that `jsxFragment` is currently only available on the API, not the CLI).
220+
221+
For [Babel][], use [`@babel/plugin-transform-react-jsx`][babel-jsx] (in classic
222+
mode), and pass `pragma: 'x'` and `pragmaFrag: 'null'`.
223+
224+
Babel also lets you configure this in a script:
225+
226+
```jsx
227+
/** @jsx x */
228+
/** @jsxFrag null */
229+
var x = require('xastscript')
230+
231+
console.log(<music />)
232+
```
172233

173234
## Security
174235

@@ -249,10 +310,20 @@ abide by its terms.
249310

250311
[tree]: https://github.com/syntax-tree/unist#tree
251312

313+
[node]: https://github.com/syntax-tree/unist#node
314+
315+
[root]: https://github.com/syntax-tree/xast#root
316+
252317
[element]: https://github.com/syntax-tree/xast#element
253318

254319
[text]: https://github.com/syntax-tree/xast#text
255320

256321
[u]: https://github.com/syntax-tree/unist-builder
257322

258323
[h]: https://github.com/syntax-tree/hastscript
324+
325+
[bublé]: https://github.com/Rich-Harris/buble
326+
327+
[babel]: https://github.com/babel/babel
328+
329+
[babel-jsx]: https://github.com/babel/babel/tree/main/packages/babel-plugin-transform-react-jsx

script/generate-jsx.js

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
'use strict'
2+
3+
var fs = require('fs')
4+
var path = require('path')
5+
var buble = require('buble')
6+
var babel = require('@babel/core')
7+
8+
var doc = String(fs.readFileSync(path.join('test', 'jsx.jsx')))
9+
10+
fs.writeFileSync(
11+
path.join('test', 'jsx-buble.js'),
12+
buble.transform(doc.replace(/'name'/, "'jsx (buble)'"), {
13+
jsx: 'x',
14+
jsxFragment: 'null'
15+
}).code
16+
)
17+
18+
fs.writeFileSync(
19+
path.join('test', 'jsx-babel.js'),
20+
babel.transform(doc.replace(/'name'/, "'jsx (babel)'"), {
21+
plugins: [
22+
['@babel/plugin-transform-react-jsx', {pragma: 'x', pragmaFrag: 'null'}]
23+
]
24+
}).code
25+
)

test.js renamed to test/core.js

+42-4
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
'use strict'
22

33
var test = require('tape')
4-
var x = require('.')
4+
var x = require('..')
55

66
test('xastscript', function (t) {
77
t.equal(typeof x, 'function', 'should expose a function')
88

9+
t.deepEqual(
10+
x(),
11+
{type: 'root', children: []},
12+
'should create a root when w/o `name`'
13+
)
14+
915
t.throws(
1016
function () {
11-
x()
17+
x(1)
1218
},
13-
/Expected element name, got `undefined`/,
14-
'should throw without `name`'
19+
/Expected element name, got `1`/,
20+
'should throw w/ incorrect `name`'
1521
)
1622

1723
t.deepEqual(
@@ -156,5 +162,37 @@ test('xastscript', function (t) {
156162
'should support omitting attributes when given an array for a child'
157163
)
158164

165+
t.deepEqual(
166+
x(null, '1'),
167+
{type: 'root', children: [{type: 'text', value: '1'}]},
168+
'should create a root with a textual child'
169+
)
170+
171+
t.deepEqual(
172+
x(null, 1),
173+
{type: 'root', children: [{type: 'text', value: '1'}]},
174+
'should create a root with a numerical child'
175+
)
176+
177+
t.deepEqual(
178+
x(null, x('a')),
179+
{
180+
type: 'root',
181+
children: [{type: 'element', name: 'a', attributes: {}, children: []}]
182+
},
183+
'should create a root with a node child'
184+
)
185+
186+
t.deepEqual(
187+
x('a', {}, [x(null, x('b'))]),
188+
{
189+
type: 'element',
190+
name: 'a',
191+
attributes: {},
192+
children: [{type: 'element', name: 'b', attributes: {}, children: []}]
193+
},
194+
'should create a node w/ by unraveling roots'
195+
)
196+
159197
t.end()
160198
})

test/index.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
'use strict'
2+
3+
/* eslint-disable import/no-unassigned-import */
4+
require('./core')
5+
require('./jsx-babel')
6+
require('./jsx-buble')
7+
/* eslint-enable import/no-unassigned-import */

0 commit comments

Comments
 (0)