diff --git a/.gitignore b/.gitignore index fdefc8c..73a7333 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ .nyc_output/ coverage/ node_modules/ +test/jsx-*.js yarn.lock diff --git a/index.js b/index.js index 459e1d0..131304f 100644 --- a/index.js +++ b/index.js @@ -4,17 +4,21 @@ module.exports = x // Creating xast elements. function x(name, attributes) { - var node = {type: 'element', name: name, attributes: {}, children: []} + var node = + name == null + ? {type: 'root', children: []} + : {type: 'element', name: name, attributes: {}, children: []} var index = 1 var key - if (typeof name !== 'string' || !name) { + if (name != null && typeof name !== 'string') { throw new Error('Expected element name, got `' + name + '`') } // Handle props. if (attributes) { if ( + name == null || typeof attributes === 'string' || typeof attributes === 'number' || 'length' in attributes @@ -51,7 +55,11 @@ function addChild(nodes, value) { addChild(nodes, value[index]) } } else if (typeof value === 'object' && value.type) { - nodes.push(value) + if (value.type === 'root') { + addChild(nodes, value.children) + } else { + nodes.push(value) + } } else { throw new TypeError('Expected node, nodes, string, got `' + value + '`') } diff --git a/package.json b/package.json index 4a28790..adc4c79 100644 --- a/package.json +++ b/package.json @@ -34,20 +34,26 @@ "@types/xast": "^1.0.0" }, "devDependencies": { + "@babel/core": "^7.0.0", + "@babel/plugin-syntax-jsx": "^7.0.0", + "@babel/plugin-transform-react-jsx": "^7.0.0", + "buble": "^0.20.0", "dtslint": "^4.0.0", "nyc": "^15.0.0", "prettier": "^2.0.0", "remark-cli": "^9.0.0", "remark-preset-wooorm": "^8.0.0", "tape": "^5.0.0", + "unist-builder": "^2.0.0", "xo": "^0.34.0" }, "scripts": { + "generate": "node script/generate-jsx", "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", "test-api": "node test", - "test-coverage": "nyc --reporter lcov tape test.js", + "test-coverage": "nyc --reporter lcov tape test/index.js", "test-types": "dtslint types", - "test": "npm run format && npm run test-coverage && npm run test-types" + "test": "npm run generate && npm run format && npm run test-coverage && npm run test-types" }, "nyc": { "check-coverage": true, diff --git a/readme.md b/readme.md index dcdf1ec..96098b5 100644 --- a/readme.md +++ b/readme.md @@ -48,7 +48,7 @@ console.log( // For other xast nodes, such as comments, instructions, doctypes, or cdata // can be created with unist-builder: console.log( - u('root', [ + x(null, [ u('instruction', {name: 'xml'}, 'version="1.0" encoding="UTF-8"'), x('album', [ u('comment', 'Great album!'), @@ -142,16 +142,24 @@ Yields: ## API -### `x(name[, attributes][, …children])` +### `x(name?[, attributes][, …children])` Create XML *[trees][tree]* in **[xast][]**. +##### Signatures + +* `x(): root` +* `x(null[, …children]): root` +* `x(name[, attributes][, …children]): element` + ##### Parameters ###### `name` -Qualified name (`string`). +Qualified name (`string`, optional). Case sensitive and can contain a namespace prefix (such as `rdf:RDF`). +When string, an [`Element`][element] is built. +When nullish, a [`Root`][root] is built instead. ###### `attributes` @@ -159,16 +167,69 @@ Map of attributes (`Object.<*>`, optional). Nullish (`null` or `undefined`) or `NaN` values are ignored, other values are turned to strings. -Cannot be omitted if `children` is a `Node`. +Cannot be given if building a [`Root`][root]. +Cannot be omitted when building an [`Element`][element] if the first child is a +[`Node`][node]. ###### `children` -(Lists of) child nodes (`string`, `Node`, `Array.`, optional). -When strings are encountered, they are mapped to [`text`][text] nodes. +(Lists of) children (`string`, `number`, `Node`, `Array.`, optional). +When strings or numbers are encountered, they are mapped to [`Text`][text] +nodes. +If a [`Root`][root] node is given, its children are used instead. ##### Returns -[`Element`][element]. +[`Element`][element] or [`Root`][root]. + +## JSX + +`xastscript` can be used as a pragma for JSX. +The example above (omitting the second) can then be written like so: + +```jsx +var u = require('unist-builder') +var x = require('xastscript') + +console.log( + + Born in the U.S.A. + Bruce Springsteen + 1984-04-06 + +) + +console.log( + <> + {u('instruction', {name: 'xml'}, 'version="1.0" encoding="UTF-8"')} + + {u('comment', 'Great album!')} + Born in the U.S.A. + {u('cdata', '3 < 5 & 8 > 13')} + + +) +``` + +Note that you must still import `xastscript` 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: 'x'` and `jsxFragment: 'null'` +(note that `jsxFragment` is currently only available on the API, not the CLI). + +For [Babel][], use [`@babel/plugin-transform-react-jsx`][babel-jsx] (in classic +mode), and pass `pragma: 'x'` and `pragmaFrag: 'null'`. + +Babel also lets you configure this in a script: + +```jsx +/** @jsx x */ +/** @jsxFrag null */ +var x = require('xastscript') + +console.log() +``` ## Security @@ -249,6 +310,10 @@ abide by its terms. [tree]: https://github.com/syntax-tree/unist#tree +[node]: https://github.com/syntax-tree/unist#node + +[root]: https://github.com/syntax-tree/xast#root + [element]: https://github.com/syntax-tree/xast#element [text]: https://github.com/syntax-tree/xast#text @@ -256,3 +321,9 @@ abide by its terms. [u]: https://github.com/syntax-tree/unist-builder [h]: https://github.com/syntax-tree/hastscript + +[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 diff --git a/script/generate-jsx.js b/script/generate-jsx.js new file mode 100644 index 0000000..fcfe2b0 --- /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: 'x', + 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: 'x', pragmaFrag: 'null'}] + ] + }).code +) diff --git a/test.js b/test/core.js similarity index 78% rename from test.js rename to test/core.js index 2ad9a22..74e6626 100644 --- a/test.js +++ b/test/core.js @@ -1,17 +1,23 @@ 'use strict' var test = require('tape') -var x = require('.') +var x = require('..') test('xastscript', function (t) { t.equal(typeof x, 'function', 'should expose a function') + t.deepEqual( + x(), + {type: 'root', children: []}, + 'should create a root when w/o `name`' + ) + t.throws( function () { - x() + x(1) }, - /Expected element name, got `undefined`/, - 'should throw without `name`' + /Expected element name, got `1`/, + 'should throw w/ incorrect `name`' ) t.deepEqual( @@ -156,5 +162,37 @@ test('xastscript', function (t) { 'should support omitting attributes when given an array for a child' ) + t.deepEqual( + x(null, '1'), + {type: 'root', children: [{type: 'text', value: '1'}]}, + 'should create a root with a textual child' + ) + + t.deepEqual( + x(null, 1), + {type: 'root', children: [{type: 'text', value: '1'}]}, + 'should create a root with a numerical child' + ) + + t.deepEqual( + x(null, x('a')), + { + type: 'root', + children: [{type: 'element', name: 'a', attributes: {}, children: []}] + }, + 'should create a root with a node child' + ) + + t.deepEqual( + x('a', {}, [x(null, x('b'))]), + { + type: 'element', + name: 'a', + attributes: {}, + children: [{type: 'element', name: 'b', attributes: {}, children: []}] + }, + 'should create a node w/ by unraveling roots' + ) + t.end() }) 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..df65a7d --- /dev/null +++ b/test/jsx.jsx @@ -0,0 +1,125 @@ +'use strict' + +var test = require('tape') +var u = require('unist-builder') +var x = require('..') + +test('name', function (t) { + t.deepEqual(, x('a'), 'should support a self-closing element') + + t.deepEqual(b, x('a', 'b'), 'should support a value as a child') + + var A = 'a' + + t.deepEqual(, x(A), 'should support an uppercase tag name') + + t.deepEqual( + {1 + 1}, + x('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( + <> + + , + u('root', [x('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( + , + x(com.acme.a), + 'should support members as names (`a.b`)' + ) + + t.deepEqual( + , + x('a', {b: 'true'}), + 'should support a boolean attribute' + ) + + t.deepEqual( + , + x('a', {b: ''}), + 'should support a double quoted attribute' + ) + + t.deepEqual( + , + x('a', {b: '"'}), + 'should support a single quoted attribute' + ) + + t.deepEqual( + , + x('a', {b: '2'}), + 'should support expression value attributes' + ) + + var props = {a: 1, b: 2} + + t.deepEqual( + , + x('a', props), + 'should support expression spread attributes' + ) + + t.deepEqual( + + ce + {1 + 1} + , + x('a', [x('b'), 'c', x('d', 'e'), '2']), + 'should support text, elements, and expressions in jsx' + ) + + t.deepEqual( + + <>{1} + , + x('a', '1'), + 'should support a fragment in an element (#1)' + ) + + var dl = [ + ['Firefox', 'A red panda.'], + ['Chrome', 'A chemical element.'] + ] + + t.deepEqual( +
+ {dl.map(([title, definition]) => ( + <> +
{title}
+
{definition}
+ + ))} +
, + x('dl', [ + x('dt', dl[0][0]), + x('dd', dl[0][1]), + x('dt', dl[1][0]), + x('dd', dl[1][1]) + ]), + 'should support a fragment in an element (#2)' + ) + + t.end() +})