Skip to content

Commit dd94377

Browse files
authored
Add support for using h, s as a JSX pragmas
Closes GH-15. Reviewed-by: Christian Murphy <[email protected]> Reviewed-by: Remco Haszing <[email protected]>
1 parent ecb5b46 commit dd94377

9 files changed

+339
-60
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
.nyc_output/
44
coverage/
55
node_modules/
6+
test/jsx-*.js
67
hastscript.js
78
hastscript.min.js
89
yarn.lock

factory.js

+14-4
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,23 @@ function factory(schema, defaultTagName, caseSensitive) {
1717

1818
// Hyperscript compatible DSL for creating virtual hast trees.
1919
function h(selector, properties) {
20-
var node = parseSelector(selector, defaultTagName)
21-
var name = node.tagName.toLowerCase()
20+
var node =
21+
selector == null
22+
? {type: 'root', children: []}
23+
: parseSelector(selector, defaultTagName)
24+
var name = selector == null ? null : node.tagName.toLowerCase()
2225
var index = 1
2326
var property
2427

2528
// Normalize the name.
26-
node.tagName = adjust && own.call(adjust, name) ? adjust[name] : name
29+
if (name != null) {
30+
node.tagName = adjust && own.call(adjust, name) ? adjust[name] : name
31+
}
2732

2833
// Handle props.
2934
if (properties) {
3035
if (
36+
name == null ||
3137
typeof properties === 'string' ||
3238
'length' in properties ||
3339
isNode(name, properties)
@@ -134,7 +140,11 @@ function addChild(nodes, value) {
134140
addChild(nodes, value[index])
135141
}
136142
} else if (typeof value === 'object' && 'type' in value) {
137-
nodes.push(value)
143+
if (value.type === 'root') {
144+
addChild(nodes, value.children)
145+
} else {
146+
nodes.push(value)
147+
}
138148
} else {
139149
throw new Error('Expected node, nodes, or string, got `' + value + '`')
140150
}

package.json

+7-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@
4545
"space-separated-tokens": "^1.0.0"
4646
},
4747
"devDependencies": {
48+
"@babel/core": "^7.0.0",
49+
"@babel/plugin-syntax-jsx": "^7.0.0",
50+
"@babel/plugin-transform-react-jsx": "^7.0.0",
4851
"browserify": "^17.0.0",
52+
"buble": "^0.20.0",
4953
"dtslint": "^4.0.0",
5054
"nyc": "^15.0.0",
5155
"prettier": "^2.0.0",
@@ -54,16 +58,17 @@
5458
"svg-tag-names": "^2.0.0",
5559
"tape": "^5.0.0",
5660
"tinyify": "^3.0.0",
61+
"unist-builder": "^2.0.0",
5762
"xo": "^0.35.0"
5863
},
5964
"scripts": {
60-
"generate": "node build",
65+
"generate": "node script/generate-jsx && node script/build",
6166
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
6267
"build-bundle": "browserify . -s hastscript > hastscript.js",
6368
"build-mangle": "browserify . -s hastscript -p tinyify > hastscript.min.js",
6469
"build": "npm run build-bundle && npm run build-mangle",
6570
"test-api": "node test",
66-
"test-coverage": "nyc --reporter lcov tape test.js",
71+
"test-coverage": "nyc --reporter lcov tape test/index.js",
6772
"test-types": "dtslint .",
6873
"test": "npm run generate && npm run format && npm run build && npm run test-coverage && npm run test-types"
6974
},

readme.md

+106-8
Original file line numberDiff line numberDiff line change
@@ -138,34 +138,124 @@ Yields:
138138

139139
## API
140140

141-
### `h(selector?[, properties][, ...children])`
141+
### `h(selector?[, properties][, children])`
142142

143-
### `s(selector?[, properties][, ...children])`
143+
### `s(selector?[, properties][, children])`
144144

145-
DSL to create virtual [**hast**][hast] [*trees*][tree] for HTML or SVG.
145+
Create virtual [**hast**][hast] [*trees*][tree] for HTML or SVG.
146+
147+
##### Signatures
148+
149+
* `h(): root`
150+
* `h(null[, …children]): root`
151+
* `h(name[, properties][, …children]): element`
152+
153+
(and the same for `s`).
146154

147155
##### Parameters
148156

149157
###### `selector`
150158

151159
Simple CSS selector (`string`, optional).
152160
Can contain a tag name (`foo`), IDs (`#bar`), and classes (`.baz`).
153-
If there is no tag name in the selector, `h` defaults to a `div` element,
154-
and `s` to a `g` element.
161+
If the selector is a string but there is no tag name in it, `h` defaults to
162+
build a `div` element, and `s` to a `g` element.
155163
`selector` is parsed by [`hast-util-parse-selector`][parse-selector].
164+
When string, builds an [`Element`][element].
165+
When nullish, builds a [`Root`][root] instead.
156166

157167
###### `properties`
158168

159169
Map of properties (`Object.<*>`, optional).
170+
Keys should match either the HTML attribute name, or the DOM property name, but
171+
are case-insensitive.
172+
Cannot be given when building a [`Root`][root].
160173

161174
###### `children`
162175

163-
(Lists of) child nodes (`string`, `Node`, `Array.<string|Node>`, optional).
164-
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 [`Root`][root] nodes are given, their children are used instead.
165180

166181
##### Returns
167182

168-
[`Element`][element].
183+
[`Element`][element] or [`Root`][root].
184+
185+
## JSX
186+
187+
`hastscript` can be used as a pragma for JSX.
188+
The example above can then be written like so, using inline Babel pragmas, so
189+
that SVG can be used too:
190+
191+
`example-html.jsx`:
192+
193+
```jsx
194+
/** @jsx h */
195+
/** @jsxFrag null */
196+
var h = require('hastscript')
197+
198+
console.log(
199+
<div class="foo" id="some-id">
200+
<span>some text</span>
201+
<input type="text" value="foo" />
202+
<a class="alpha bravo charlie" download>
203+
deltaecho
204+
</a>
205+
</div>
206+
)
207+
208+
console.log(
209+
<form method="POST">
210+
<input type="text" name="foo" />
211+
<input type="text" name="bar" />
212+
<input type="submit" name="send" />
213+
</form>
214+
)
215+
```
216+
217+
`example-svg.jsx`:
218+
219+
```jsx
220+
/** @jsx s */
221+
/** @jsxFrag null */
222+
var s = require('hastscript/svg')
223+
224+
console.log(
225+
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 500 500">
226+
<title>SVG `&lt;circle&gt;` element</title>
227+
<circle cx={120} cy={120} r={100} />
228+
</svg>
229+
)
230+
```
231+
232+
Because JSX does not allow dots (`.`) or number signs (`#`) in tag names, you
233+
have to pass class names and IDs in as attributes.
234+
235+
Note that you must still import `hastscript` yourself and configure your
236+
JavaScript compiler to use the identifier you assign it to as a pragma (and
237+
pass `null` for fragments).
238+
239+
For [bublé][], this can be done by setting `jsx: 'h'` and `jsxFragment: 'null'`
240+
(note that `jsxFragment` is currently only available on the API, not the CLI).
241+
Bublé is less ideal because it allows a single pragma.
242+
243+
For [Babel][], use [`@babel/plugin-transform-react-jsx`][babel-jsx] (in classic
244+
mode), and pass `pragma: 'h'` and `pragmaFrag: 'null'`.
245+
This is less ideal because it allows a single pragma.
246+
247+
Babel also lets you configure this in a script:
248+
249+
```jsx
250+
/** @jsx s */
251+
/** @jsxFrag null */
252+
var s = require('hastscript/svg')
253+
254+
console.log(<rect />)
255+
```
256+
257+
This is useful because it allows using *both* `hastscript/html` and
258+
`hastscript/svg`, although in different files.
169259

170260
## Security
171261

@@ -317,10 +407,18 @@ abide by its terms.
317407

318408
[element]: https://github.com/syntax-tree/hast#element
319409

410+
[root]: https://github.com/syntax-tree/xast#root
411+
320412
[text]: https://github.com/syntax-tree/hast#text
321413

322414
[u]: https://github.com/syntax-tree/unist-builder
323415

416+
[bublé]: https://github.com/Rich-Harris/buble
417+
418+
[babel]: https://github.com/babel/babel
419+
420+
[babel-jsx]: https://github.com/babel/babel/tree/main/packages/babel-plugin-transform-react-jsx
421+
324422
[parse-selector]: https://github.com/syntax-tree/hast-util-parse-selector
325423

326424
[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting

build.js script/build.js

File renamed without changes.

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: 'h',
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: 'h', pragmaFrag: 'null'}]
23+
]
24+
}).code
25+
)

0 commit comments

Comments
 (0)