From 920fe458429910ee6b72a07789805a5ee7fa6a04 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 2 Nov 2020 15:01:47 +0100 Subject: [PATCH 1/4] Add support using `x` as a JSX pragma MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .gitignore | 1 + index.js | 11 +++-- package.json | 10 +++- script/generate-jsx.js | 25 ++++++++++ test.js => test/core.js | 46 ++++++++++++++++-- test/index.js | 7 +++ test/jsx.jsx | 102 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 193 insertions(+), 9 deletions(-) create mode 100644 script/generate-jsx.js rename test.js => test/core.js (78%) create mode 100644 test/index.js create mode 100644 test/jsx.jsx 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..276af57 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,8 @@ 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..887f46c 100644 --- a/package.json +++ b/package.json @@ -34,20 +34,26 @@ "@types/xast": "^1.0.0" }, "devDependencies": { + "@babel/core": "^7.12.3", + "@babel/plugin-syntax-jsx": "^7.12.1", + "@babel/plugin-transform-react-jsx": "^7.12.1", + "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.3", "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/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..c49718b --- /dev/null +++ b/test/jsx.jsx @@ -0,0 +1,102 @@ +'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' + ) + + t.end() +}) From 70d87a28ce40d622ce4865a84f856ea8f0607e53 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 2 Nov 2020 15:10:13 +0100 Subject: [PATCH 2/4] Refactor --- index.js | 7 +++++-- package.json | 8 ++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index 276af57..131304f 100644 --- a/index.js +++ b/index.js @@ -55,8 +55,11 @@ function addChild(nodes, value) { addChild(nodes, value[index]) } } else if (typeof value === 'object' && value.type) { - if (value.type === 'root') addChild(nodes, value.children) - else 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 887f46c..adc4c79 100644 --- a/package.json +++ b/package.json @@ -34,9 +34,9 @@ "@types/xast": "^1.0.0" }, "devDependencies": { - "@babel/core": "^7.12.3", - "@babel/plugin-syntax-jsx": "^7.12.1", - "@babel/plugin-transform-react-jsx": "^7.12.1", + "@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", @@ -44,7 +44,7 @@ "remark-cli": "^9.0.0", "remark-preset-wooorm": "^8.0.0", "tape": "^5.0.0", - "unist-builder": "^2.0.3", + "unist-builder": "^2.0.0", "xo": "^0.34.0" }, "scripts": { From d81d8b755e58d34bd2d7484d4deda7bce1881d04 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 2 Nov 2020 15:29:27 +0100 Subject: [PATCH 3/4] Add a more complex fragment example --- test/jsx.jsx | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/jsx.jsx b/test/jsx.jsx index c49718b..df65a7d 100644 --- a/test/jsx.jsx +++ b/test/jsx.jsx @@ -95,7 +95,30 @@ test('name', function (t) { <>{1} , x('a', '1'), - 'should support a fragment in an element' + '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() From 71320d1c1fb9f9d5ee91c4e2e144f28788164045 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 2 Nov 2020 17:39:05 +0100 Subject: [PATCH 4/4] Add docs --- readme.md | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 78 insertions(+), 7 deletions(-) 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