From 600258e85d2dd8e1aa70da07a3831040b25dad62 Mon Sep 17 00:00:00 2001
From: Titus Wormer <tituswormer@gmail.com>
Date: Fri, 6 Nov 2020 09:38:26 +0100
Subject: [PATCH 1/2] Add support for using `h`, `s` as a JSX pragmas
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This change tests that `hastscript` can be used as the pragma for JSX with bublé
and babel.

Code-wise, this adds support for using `h` to generate root nodes.
This is done by omitting the tag name (like so: `h()`, `h(null, 'child')`).
Previously, omitting a `name` resulted in the default element to be created
(`div` for `h`, `g` for `s`).

This change thus is a breaking change.
The old behavior is still available when passing an empty string: `h('')`.

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, hast prohibits roots occurring in nodes,
so the unraveling instead fixes what would otherwise be a broken tree.

Related to: syntax-tree/xastscript#3.
Related to: syntax-tree/xastscript#4.
---
 .gitignore                  |   1 +
 factory.js                  |  18 ++++--
 package.json                |   9 ++-
 readme.md                   | 114 ++++++++++++++++++++++++++++++---
 build.js => script/build.js |   0
 script/generate-jsx.js      |  25 ++++++++
 test.js => test/core.js     | 104 +++++++++++++++++--------------
 test/index.js               |   7 +++
 test/jsx.jsx                | 121 ++++++++++++++++++++++++++++++++++++
 9 files changed, 339 insertions(+), 60 deletions(-)
 rename build.js => script/build.js (100%)
 create mode 100644 script/generate-jsx.js
 rename test.js => test/core.js (92%)
 create mode 100644 test/index.js
 create mode 100644 test/jsx.jsx

diff --git a/.gitignore b/.gitignore
index a6a3ec3..f394799 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,6 +3,7 @@
 .nyc_output/
 coverage/
 node_modules/
+test/jsx-*.js
 hastscript.js
 hastscript.min.js
 yarn.lock
diff --git a/factory.js b/factory.js
index 62e4041..cc39c38 100644
--- a/factory.js
+++ b/factory.js
@@ -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)
@@ -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 + '`')
   }
diff --git a/package.json b/package.json
index bbcab4a..77ab26e 100644
--- a/package.json
+++ b/package.json
@@ -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",
@@ -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.34.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"
   },
diff --git a/readme.md b/readme.md
index 8a7fbd0..d6cf97c 100644
--- a/readme.md
+++ b/readme.md
@@ -138,11 +138,19 @@ 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
 
@@ -150,22 +158,104 @@ DSL to create virtual [**hast**][hast] [*trees*][tree] for HTML or SVG.
 
 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 (omitting the second) 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
 
@@ -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
diff --git a/build.js b/script/build.js
similarity index 100%
rename from build.js
rename to script/build.js
diff --git a/script/generate-jsx.js b/script/generate-jsx.js
new file mode 100644
index 0000000..a21d6f4
--- /dev/null
+++ b/script/generate-jsx.js
@@ -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
+)
diff --git a/test.js b/test/core.js
similarity index 92%
rename from test.js
rename to test/core.js
index 6d1b617..045c21f 100644
--- a/test.js
+++ b/test/core.js
@@ -1,8 +1,8 @@
 'use strict'
 
 var test = require('tape')
-var s = require('./svg')
-var h = require('./html')
+var s = require('../svg')
+var h = require('../html')
 
 test('hastscript', function (t) {
   t.equal(typeof h, 'function', 'should expose a function')
@@ -10,13 +10,19 @@ test('hastscript', function (t) {
   t.test('selector', function (t) {
     t.deepEqual(
       h(),
+      {type: 'root', children: []},
+      'should create a `root` node without arguments'
+    )
+
+    t.deepEqual(
+      h(''),
       {
         type: 'element',
         tagName: 'div',
         properties: {},
         children: []
       },
-      'should create a `div` element without arguments'
+      'should create a `div` element w/ an empty string name'
     )
 
     t.deepEqual(
@@ -113,7 +119,7 @@ test('hastscript', function (t) {
   t.test('properties', function (t) {
     t.test('known property names', function (t) {
       t.deepEqual(
-        h(null, {className: 'foo'}),
+        h('', {className: 'foo'}),
         {
           type: 'element',
           tagName: 'div',
@@ -124,7 +130,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {class: 'foo'}),
+        h('', {class: 'foo'}),
         {
           type: 'element',
           tagName: 'div',
@@ -135,7 +141,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {CLASS: 'foo'}),
+        h('', {CLASS: 'foo'}),
         {
           type: 'element',
           tagName: 'div',
@@ -146,7 +152,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {'class-name': 'foo'}),
+        h('', {'class-name': 'foo'}),
         {
           type: 'element',
           tagName: 'div',
@@ -161,7 +167,7 @@ test('hastscript', function (t) {
 
     t.test('unknown property names', function (t) {
       t.deepEqual(
-        h(null, {allowbigscreen: true}),
+        h('', {allowbigscreen: true}),
         {
           type: 'element',
           tagName: 'div',
@@ -172,7 +178,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {allowBigScreen: true}),
+        h('', {allowBigScreen: true}),
         {
           type: 'element',
           tagName: 'div',
@@ -183,7 +189,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {'allow_big-screen': true}),
+        h('', {'allow_big-screen': true}),
         {
           type: 'element',
           tagName: 'div',
@@ -198,7 +204,7 @@ test('hastscript', function (t) {
 
     t.test('other namespaces', function (t) {
       t.deepEqual(
-        h(null, {'aria-valuenow': 1}),
+        h('', {'aria-valuenow': 1}),
         {
           type: 'element',
           tagName: 'div',
@@ -209,7 +215,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {ariaValueNow: 1}),
+        h('', {ariaValueNow: 1}),
         {
           type: 'element',
           tagName: 'div',
@@ -220,7 +226,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        s(null, {'color-interpolation-filters': 'sRGB'}),
+        s('', {'color-interpolation-filters': 'sRGB'}),
         {
           type: 'element',
           tagName: 'g',
@@ -231,7 +237,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        s(null, {colorInterpolationFilters: 'sRGB'}),
+        s('', {colorInterpolationFilters: 'sRGB'}),
         {
           type: 'element',
           tagName: 'g',
@@ -242,7 +248,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        s(null, {'xml:space': 'preserve'}),
+        s('', {'xml:space': 'preserve'}),
         {
           type: 'element',
           tagName: 'g',
@@ -253,7 +259,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        s(null, {xmlSpace: 'preserve'}),
+        s('', {xmlSpace: 'preserve'}),
         {
           type: 'element',
           tagName: 'g',
@@ -264,7 +270,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        s(null, {'xmlns:xlink': 'http://www.w3.org/1999/xlink'}),
+        s('', {'xmlns:xlink': 'http://www.w3.org/1999/xlink'}),
         {
           type: 'element',
           tagName: 'g',
@@ -275,7 +281,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        s(null, {xmlnsXLink: 'http://www.w3.org/1999/xlink'}),
+        s('', {xmlnsXLink: 'http://www.w3.org/1999/xlink'}),
         {
           type: 'element',
           tagName: 'g',
@@ -286,7 +292,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        s(null, {'xlink:arcrole': 'http://www.example.com'}),
+        s('', {'xlink:arcrole': 'http://www.example.com'}),
         {
           type: 'element',
           tagName: 'g',
@@ -297,7 +303,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        s(null, {xLinkArcRole: 'http://www.example.com'}),
+        s('', {xLinkArcRole: 'http://www.example.com'}),
         {
           type: 'element',
           tagName: 'g',
@@ -312,7 +318,7 @@ test('hastscript', function (t) {
 
     t.test('data property names', function (t) {
       t.deepEqual(
-        h(null, {'data-foo': true}),
+        h('', {'data-foo': true}),
         {
           type: 'element',
           tagName: 'div',
@@ -323,7 +329,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {'data-123': true}),
+        h('', {'data-123': true}),
         {
           type: 'element',
           tagName: 'div',
@@ -334,7 +340,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {dataFooBar: true}),
+        h('', {dataFooBar: true}),
         {
           type: 'element',
           tagName: 'div',
@@ -345,7 +351,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {data123: true}),
+        h('', {data123: true}),
         {
           type: 'element',
           tagName: 'div',
@@ -356,7 +362,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {'data-foo.bar': true}),
+        h('', {'data-foo.bar': true}),
         {
           type: 'element',
           tagName: 'div',
@@ -367,7 +373,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {'dataFoo.bar': true}),
+        h('', {'dataFoo.bar': true}),
         {
           type: 'element',
           tagName: 'div',
@@ -378,7 +384,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {'data-foo!bar': true}),
+        h('', {'data-foo!bar': true}),
         {
           type: 'element',
           tagName: 'div',
@@ -389,7 +395,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {'dataFoo!bar': true}),
+        h('', {'dataFoo!bar': true}),
         {
           type: 'element',
           tagName: 'div',
@@ -404,7 +410,7 @@ test('hastscript', function (t) {
 
     t.test('unknown property values', function (t) {
       t.deepEqual(
-        h(null, {foo: 'bar'}),
+        h('', {foo: 'bar'}),
         {
           type: 'element',
           tagName: 'div',
@@ -415,7 +421,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {foo: 3}),
+        h('', {foo: 3}),
         {
           type: 'element',
           tagName: 'div',
@@ -426,7 +432,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {foo: true}),
+        h('', {foo: true}),
         {
           type: 'element',
           tagName: 'div',
@@ -437,7 +443,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {list: ['bar', 'baz']}),
+        h('', {list: ['bar', 'baz']}),
         {
           type: 'element',
           tagName: 'div',
@@ -448,7 +454,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {foo: null}),
+        h('', {foo: null}),
         {
           type: 'element',
           tagName: 'div',
@@ -459,7 +465,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {foo: undefined}),
+        h('', {foo: undefined}),
         {
           type: 'element',
           tagName: 'div',
@@ -470,7 +476,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {foo: NaN}),
+        h('', {foo: NaN}),
         {
           type: 'element',
           tagName: 'div',
@@ -485,7 +491,7 @@ test('hastscript', function (t) {
 
     t.test('known booleans', function (t) {
       t.deepEqual(
-        h(null, {allowFullScreen: ''}),
+        h('', {allowFullScreen: ''}),
         {
           type: 'element',
           tagName: 'div',
@@ -496,7 +502,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {allowFullScreen: 'yup'}),
+        h('', {allowFullScreen: 'yup'}),
         {
           type: 'element',
           tagName: 'div',
@@ -522,7 +528,7 @@ test('hastscript', function (t) {
 
     t.test('known overloaded booleans', function (t) {
       t.deepEqual(
-        h(null, {download: ''}),
+        h('', {download: ''}),
         {
           type: 'element',
           tagName: 'div',
@@ -533,7 +539,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {download: 'downLOAD'}),
+        h('', {download: 'downLOAD'}),
         {
           type: 'element',
           tagName: 'div',
@@ -544,7 +550,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {download: 'example.ogg'}),
+        h('', {download: 'example.ogg'}),
         {
           type: 'element',
           tagName: 'div',
@@ -596,7 +602,7 @@ test('hastscript', function (t) {
 
     t.test('known lists', function (t) {
       t.deepEqual(
-        h(null, {class: 'foo bar baz'}),
+        h('', {class: 'foo bar baz'}),
         {
           type: 'element',
           tagName: 'div',
@@ -633,7 +639,7 @@ test('hastscript', function (t) {
 
     t.test('style', function (t) {
       t.deepEqual(
-        h(null, {style: {color: 'red', '-webkit-border-radius': '3px'}}),
+        h('', {style: {color: 'red', '-webkit-border-radius': '3px'}}),
         {
           type: 'element',
           tagName: 'div',
@@ -646,7 +652,7 @@ test('hastscript', function (t) {
       )
 
       t.deepEqual(
-        h(null, {style: 'color:/*red*/purple; -webkit-border-radius: 3px'}),
+        h('', {style: 'color:/*red*/purple; -webkit-border-radius: 3px'}),
         {
           type: 'element',
           tagName: 'div',
@@ -1011,13 +1017,19 @@ test('hastscript', function (t) {
   t.test('svg', function (t) {
     t.deepEqual(
       s(),
+      {type: 'root', children: []},
+      'should create a `root` node without arguments'
+    )
+
+    t.deepEqual(
+      s(''),
       {
         type: 'element',
         tagName: 'g',
         properties: {},
         children: []
       },
-      'should create a `g` element without arguments'
+      'should create a `g` element w/ an empty string name'
     )
 
     t.deepEqual(
@@ -1132,7 +1144,7 @@ test('hastscript', function (t) {
 
   t.test('tag names', function (t) {
     t.deepEqual(
-      h(null, [h('DIV'), h('dIv'), h('div')]),
+      h('', [h('DIV'), h('dIv'), h('div')]),
       {
         type: 'element',
         tagName: 'div',
@@ -1147,7 +1159,7 @@ test('hastscript', function (t) {
     )
 
     t.deepEqual(
-      s(null, [
+      s('', [
         s('RECT'),
         s('rEcT'),
         s('rect'),
diff --git a/test/index.js b/test/index.js
new file mode 100644
index 0000000..d39cad7
--- /dev/null
+++ b/test/index.js
@@ -0,0 +1,7 @@
+'use strict'
+
+/* eslint-disable import/no-unassigned-import */
+require('./core')
+require('./jsx-babel')
+require('./jsx-buble')
+/* eslint-enable import/no-unassigned-import */
diff --git a/test/jsx.jsx b/test/jsx.jsx
new file mode 100644
index 0000000..2dc7856
--- /dev/null
+++ b/test/jsx.jsx
@@ -0,0 +1,121 @@
+'use strict'
+
+var test = require('tape')
+var u = require('unist-builder')
+var h = require('..')
+
+test('name', function (t) {
+  t.deepEqual(<a />, h('a'), 'should support a self-closing element')
+
+  t.deepEqual(<a>b</a>, h('a', 'b'), 'should support a value as a child')
+
+  var A = 'a'
+
+  t.deepEqual(<A />, h(A), 'should support an uppercase tag name')
+
+  t.deepEqual(
+    <a>{1 + 1}</a>,
+    h('a', '2'),
+    'should support expressions as children'
+  )
+
+  t.deepEqual(<></>, u('root', []), 'should support a fragment')
+
+  t.deepEqual(
+    <>a</>,
+    u('root', [u('text', 'a')]),
+    'should support a fragment with text'
+  )
+
+  t.deepEqual(
+    <>
+      <a />
+    </>,
+    u('root', [h('a')]),
+    'should support a fragment with an element'
+  )
+
+  t.deepEqual(
+    <>{-1}</>,
+    u('root', [u('text', '-1')]),
+    'should support a fragment with an expression'
+  )
+
+  var com = {acme: {a: 'A', b: 'B'}}
+
+  t.deepEqual(
+    <com.acme.a />,
+    h(com.acme.a),
+    'should support members as names (`a.b`)'
+  )
+
+  t.deepEqual(<a b />, h('a', {b: true}), 'should support a boolean attribute')
+
+  t.deepEqual(
+    <a b="" />,
+    h('a', {b: ''}),
+    'should support a double quoted attribute'
+  )
+
+  t.deepEqual(
+    <a b='"' />,
+    h('a', {b: '"'}),
+    'should support a single quoted attribute'
+  )
+
+  t.deepEqual(
+    <a b={1 + 1} />,
+    h('a', {b: 2}),
+    'should support expression value attributes'
+  )
+
+  var props = {a: 1, b: 2}
+
+  t.deepEqual(
+    <a {...props} />,
+    h('a', props),
+    'should support expression spread attributes'
+  )
+
+  t.deepEqual(
+    <a>
+      <b />c<d>e</d>
+      {1 + 1}
+    </a>,
+    h('a', [h('b'), 'c', h('d', 'e'), '2']),
+    'should support text, elements, and expressions in jsx'
+  )
+
+  t.deepEqual(
+    <a>
+      <>{1}</>
+    </a>,
+    h('a', '1'),
+    'should support a fragment in an element (#1)'
+  )
+
+  var dl = [
+    ['Firefox', 'A red panda.'],
+    ['Chrome', 'A chemical element.']
+  ]
+
+  t.deepEqual(
+    <dl>
+      {dl.map(([title, definition]) => (
+        <>
+          <dt>{title}</dt>
+          <dd>{definition}</dd>
+        </>
+      ))}
+    </dl>,
+    h('dl', [
+      h('dt', dl[0][0]),
+      h('dd', dl[0][1]),
+      h('dt', dl[1][0]),
+      h('dd', dl[1][1])
+    ]),
+    'should support a fragment in an element (#2)'
+  )
+
+  t.end()
+})

From 0d06593812639136f6cfc1f39909e9e5c9b20bf6 Mon Sep 17 00:00:00 2001
From: Titus Wormer <tituswormer@gmail.com>
Date: Fri, 6 Nov 2020 09:41:02 +0100
Subject: [PATCH 2/2] Fix typo

---
 readme.md | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/readme.md b/readme.md
index d6cf97c..2de9376 100644
--- a/readme.md
+++ b/readme.md
@@ -185,8 +185,8 @@ If [`Root`][root] nodes are given, their children are used instead.
 ## JSX
 
 `hastscript` can be used as a pragma for JSX.
-The example above (omitting the second) can then be written like so, using
-inline Babel pragmas, so that SVG can be used too:
+The example above can then be written like so, using inline Babel pragmas, so
+that SVG can be used too:
 
 `example-html.jsx`: