Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for using h, s as a JSX pragmas #15

Merged
merged 3 commits into from
Apr 26, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
.nyc_output/
coverage/
node_modules/
test/jsx-*.js
hastscript.js
hastscript.min.js
yarn.lock
18 changes: 14 additions & 4 deletions factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,23 @@ function factory(schema, defaultTagName, caseSensitive) {

// Hyperscript compatible DSL for creating virtual hast trees.
function h(selector, properties) {
var node = parseSelector(selector, defaultTagName)
var name = node.tagName.toLowerCase()
var node =
selector == null
? {type: 'root', children: []}
: parseSelector(selector, defaultTagName)
var name = selector == null ? null : node.tagName.toLowerCase()
var index = 1
var property

// Normalize the name.
node.tagName = adjust && own.call(adjust, name) ? adjust[name] : name
if (name != null) {
node.tagName = adjust && own.call(adjust, name) ? adjust[name] : name
}

// Handle props.
if (properties) {
if (
name == null ||
typeof properties === 'string' ||
'length' in properties ||
isNode(name, properties)
Expand Down Expand Up @@ -134,7 +140,11 @@ function addChild(nodes, value) {
addChild(nodes, value[index])
}
} else if (typeof value === 'object' && 'type' in value) {
nodes.push(value)
if (value.type === 'root') {
addChild(nodes, value.children)
} else {
nodes.push(value)
}
} else {
throw new Error('Expected node, nodes, or string, got `' + value + '`')
}
Expand Down
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,11 @@
"space-separated-tokens": "^1.0.0"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/plugin-syntax-jsx": "^7.0.0",
"@babel/plugin-transform-react-jsx": "^7.0.0",
"browserify": "^17.0.0",
"buble": "^0.20.0",
"dtslint": "^4.0.0",
"nyc": "^15.0.0",
"prettier": "^2.0.0",
Expand All @@ -54,16 +58,17 @@
"svg-tag-names": "^2.0.0",
"tape": "^5.0.0",
"tinyify": "^3.0.0",
"unist-builder": "^2.0.0",
"xo": "^0.35.0"
},
"scripts": {
"generate": "node build",
"generate": "node script/generate-jsx && node script/build",
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
"build-bundle": "browserify . -s hastscript > hastscript.js",
"build-mangle": "browserify . -s hastscript -p tinyify > hastscript.min.js",
"build": "npm run build-bundle && npm run build-mangle",
"test-api": "node test",
"test-coverage": "nyc --reporter lcov tape test.js",
"test-coverage": "nyc --reporter lcov tape test/index.js",
"test-types": "dtslint .",
"test": "npm run generate && npm run format && npm run build && npm run test-coverage && npm run test-types"
},
Expand Down
114 changes: 106 additions & 8 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,34 +138,124 @@ Yields:

## API

### `h(selector?[, properties][, ...children])`
### `h(selector?[, properties][, children])`

### `s(selector?[, properties][, ...children])`
### `s(selector?[, properties][, children])`

DSL to create virtual [**hast**][hast] [*trees*][tree] for HTML or SVG.
Create virtual [**hast**][hast] [*trees*][tree] for HTML or SVG.

##### Signatures

* `h(): root`
* `h(null[, …children]): root`
* `h(name[, properties][, …children]): element`

(and the same for `s`).

##### Parameters

###### `selector`

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

###### `properties`

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

###### `children`

(Lists of) child nodes (`string`, `Node`, `Array.<string|Node>`, optional).
When strings are encountered, they are mapped to [`text`][text] nodes.
(Lists of) children (`string`, `number`, `Node`, `Array.<children>`, optional).
When strings or numbers are encountered, they are mapped to [`Text`][text]
nodes.
If [`Root`][root] nodes are given, their children are used instead.

##### Returns

[`Element`][element].
[`Element`][element] or [`Root`][root].

## JSX

`hastscript` can be used as a pragma for JSX.
The example above can then be written like so, using inline Babel pragmas, so
that SVG can be used too:

`example-html.jsx`:

```jsx
/** @jsx h */
/** @jsxFrag null */
var h = require('hastscript')

console.log(
<div class="foo" id="some-id">
<span>some text</span>
<input type="text" value="foo" />
<a class="alpha bravo charlie" download>
deltaecho
</a>
</div>
)

console.log(
<form method="POST">
<input type="text" name="foo" />
<input type="text" name="bar" />
<input type="submit" name="send" />
</form>
)
```

`example-svg.jsx`:

```jsx
/** @jsx s */
/** @jsxFrag null */
var s = require('hastscript/svg')

console.log(
<svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 500 500">
<title>SVG `&lt;circle&gt;` element</title>
<circle cx={120} cy={120} r={100} />
</svg>
)
```

Because JSX does not allow dots (`.`) or number signs (`#`) in tag names, you
have to pass class names and IDs in as attributes.

Note that you must still import `hastscript` yourself and configure your
JavaScript compiler to use the identifier you assign it to as a pragma (and
pass `null` for fragments).

For [bublé][], this can be done by setting `jsx: 'h'` and `jsxFragment: 'null'`
(note that `jsxFragment` is currently only available on the API, not the CLI).
Bublé is less ideal because it allows a single pragma.

For [Babel][], use [`@babel/plugin-transform-react-jsx`][babel-jsx] (in classic
mode), and pass `pragma: 'h'` and `pragmaFrag: 'null'`.
This is less ideal because it allows a single pragma.

Babel also lets you configure this in a script:

```jsx
/** @jsx s */
/** @jsxFrag null */
var s = require('hastscript/svg')

console.log(<rect />)
```

This is useful because it allows using *both* `hastscript/html` and
`hastscript/svg`, although in different files.

## Security

Expand Down Expand Up @@ -317,10 +407,18 @@ abide by its terms.

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

[root]: https://github.com/syntax-tree/xast#root

[text]: https://github.com/syntax-tree/hast#text

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

[bublé]: https://github.com/Rich-Harris/buble

[babel]: https://github.com/babel/babel

[babel-jsx]: https://github.com/babel/babel/tree/main/packages/babel-plugin-transform-react-jsx

[parse-selector]: https://github.com/syntax-tree/hast-util-parse-selector

[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
Expand Down
File renamed without changes.
25 changes: 25 additions & 0 deletions script/generate-jsx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use strict'

var fs = require('fs')
var path = require('path')
var buble = require('buble')
var babel = require('@babel/core')

var doc = String(fs.readFileSync(path.join('test', 'jsx.jsx')))

fs.writeFileSync(
path.join('test', 'jsx-buble.js'),
buble.transform(doc.replace(/'name'/, "'jsx (buble)'"), {
jsx: 'h',
jsxFragment: 'null'
}).code
)

fs.writeFileSync(
path.join('test', 'jsx-babel.js'),
babel.transform(doc.replace(/'name'/, "'jsx (babel)'"), {
plugins: [
['@babel/plugin-transform-react-jsx', {pragma: 'h', pragmaFrag: 'null'}]
]
}).code
)
Loading