├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── index.js ├── jsx-dev-runtime.js ├── jsx-runtime.js ├── lib ├── index.js ├── jsx-automatic.d.ts ├── jsx-automatic.js ├── jsx-classic.d.ts ├── jsx-classic.js └── runtime.js ├── license ├── package.json ├── readme.md ├── script └── generate-jsx.js ├── test-d ├── automatic.tsx ├── classic.tsx └── index.tsx ├── test ├── core.js ├── index.js └── jsx.jsx └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | name: bb 2 | on: 3 | issues: 4 | types: [opened, reopened, edited, closed, labeled, unlabeled] 5 | pull_request_target: 6 | types: [opened, reopened, edited, closed, labeled, unlabeled] 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: unifiedjs/beep-boop-beta@main 12 | with: 13 | repo-token: ${{secrets.GITHUB_TOKEN}} 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | main: 7 | name: ${{matrix.node}} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: ${{matrix.node}} 14 | - run: npm install 15 | - run: npm test 16 | - uses: codecov/codecov-action@v3 17 | strategy: 18 | matrix: 19 | node: 20 | - lts/gallium 21 | - node 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | coverage/ 4 | node_modules/ 5 | test/jsx-*.js 6 | yarn.lock 7 | /*.d.ts 8 | test/*.d.ts 9 | script/*.d.ts 10 | lib/*.d.ts 11 | !lib/jsx-automatic.d.ts 12 | !lib/jsx-classic.d.ts 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./lib/index.js').Attributes} Attributes 3 | * @typedef {import('./lib/index.js').Child} Child 4 | * @typedef {import('./lib/index.js').Result} Result 5 | */ 6 | 7 | export {x} from './lib/index.js' 8 | -------------------------------------------------------------------------------- /jsx-dev-runtime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('xast').Element} Element 3 | * @typedef {import('xast').Root} Root 4 | * @typedef {import('./lib/index.js').Child} Child 5 | * @typedef {import('./lib/runtime.js').JSXProps} JSXProps 6 | */ 7 | 8 | import {jsx} from './lib/runtime.js' 9 | 10 | // Export `JSX` as a global for TypeScript. 11 | export * from './lib/jsx-automatic.js' 12 | 13 | export {Fragment} from './jsx-runtime.js' 14 | 15 | // eslint-disable-next-line unicorn/prefer-export-from 16 | export const jsxDEV = 17 | /** 18 | * @type {{ 19 | * (name: null | undefined, props: {children?: Child}, key?: string, ...unused: Array): Root 20 | * (name: string, props: JSXProps, key?: string, ...unused: Array): Element 21 | * }} 22 | */ 23 | (jsx) 24 | -------------------------------------------------------------------------------- /jsx-runtime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./lib/runtime.js').JSXProps} JSXProps 3 | */ 4 | 5 | // Export `JSX` as a global for TypeScript. 6 | export * from './lib/jsx-automatic.js' 7 | 8 | export {Fragment, jsx, jsxs} from './lib/runtime.js' 9 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('xast').Element} Element 3 | * @typedef {import('xast').Nodes} Nodes 4 | * @typedef {import('xast').Root} Root 5 | */ 6 | 7 | /** 8 | * @typedef {Element | Root} Result 9 | * Result from a `x` call. 10 | * 11 | * @typedef {boolean | number | string | null | undefined} Value 12 | * Attribute value 13 | * 14 | * @typedef {{[attribute: string]: Value}} Attributes 15 | * Acceptable value for element properties. 16 | * 17 | * @typedef {boolean | number | string | null | undefined} PrimitiveChild 18 | * Primitive children, either ignored (nullish), or turned into text nodes. 19 | * @typedef {Array} ArrayChild 20 | * List of children. 21 | * @typedef {Nodes | PrimitiveChild | ArrayChild} Child 22 | * Acceptable child value. 23 | */ 24 | 25 | // Define JSX. 26 | 27 | /** 28 | * @typedef {import('./jsx-classic.js').Element} x.JSX.Element 29 | * @typedef {import('./jsx-classic.js').IntrinsicAttributes} x.JSX.IntrinsicAttributes 30 | * @typedef {import('./jsx-classic.js').IntrinsicElements} x.JSX.IntrinsicElements 31 | * @typedef {import('./jsx-classic.js').ElementChildrenAttribute} x.JSX.ElementChildrenAttribute 32 | */ 33 | 34 | /** 35 | * Create XML trees in xast. 36 | * 37 | * @param name 38 | * Qualified name. 39 | * 40 | * Case sensitive and can contain a namespace prefix (such as `rdf:RDF`). 41 | * When string, an `Element` is built. 42 | * When nullish, a `Root` is built instead. 43 | * @param attributes 44 | * Attributes of the element or first child. 45 | * @param children 46 | * Children of the node. 47 | * @returns 48 | * `Element` or `Root`. 49 | */ 50 | export const x = 51 | // Note: not yet possible to use the spread `...children` in JSDoc overloads. 52 | /** 53 | * @type {{ 54 | * (): Root 55 | * (name: null | undefined, ...children: Array): Root 56 | * (name: string, attributes?: Attributes, ...children: Array): Element 57 | * (name: string, ...children: Array): Element 58 | * }} 59 | */ 60 | ( 61 | /** 62 | * @param {string | null | undefined} [name] 63 | * @param {Attributes | Child | null | undefined} [attributes] 64 | * @param {Array} children 65 | * @returns {Result} 66 | */ 67 | function (name, attributes, ...children) { 68 | let index = -1 69 | /** @type {Result} */ 70 | let node 71 | 72 | if (name === undefined || name === null) { 73 | node = {type: 'root', children: []} 74 | // @ts-expect-error: Root builder doesn’t accept attributes. 75 | children.unshift(attributes) 76 | } else if (typeof name === 'string') { 77 | node = {type: 'element', name, attributes: {}, children: []} 78 | 79 | if (isAttributes(attributes)) { 80 | /** @type {string} */ 81 | let key 82 | 83 | for (key in attributes) { 84 | // Ignore nullish and NaN values. 85 | if ( 86 | attributes[key] !== undefined && 87 | attributes[key] !== null && 88 | (typeof attributes[key] !== 'number' || 89 | !Number.isNaN(attributes[key])) 90 | ) { 91 | node.attributes[key] = String(attributes[key]) 92 | } 93 | } 94 | } else { 95 | children.unshift(attributes) 96 | } 97 | } else { 98 | throw new TypeError('Expected element name, got `' + name + '`') 99 | } 100 | 101 | // Handle children. 102 | while (++index < children.length) { 103 | addChild(node.children, children[index]) 104 | } 105 | 106 | return node 107 | } 108 | ) 109 | 110 | /** 111 | * Add children. 112 | * 113 | * @param {Array} nodes 114 | * List of nodes. 115 | * @param {Child} value 116 | * Child. 117 | * @returns {undefined} 118 | * Nothing. 119 | */ 120 | function addChild(nodes, value) { 121 | let index = -1 122 | 123 | if (value === undefined || value === null) { 124 | // Empty. 125 | } else if (typeof value === 'string' || typeof value === 'number') { 126 | nodes.push({type: 'text', value: String(value)}) 127 | } else if (Array.isArray(value)) { 128 | while (++index < value.length) { 129 | addChild(nodes, value[index]) 130 | } 131 | } else if (typeof value === 'object' && 'type' in value) { 132 | if (value.type === 'root') { 133 | addChild(nodes, value.children) 134 | } else { 135 | nodes.push(value) 136 | } 137 | } else { 138 | throw new TypeError('Expected node, nodes, string, got `' + value + '`') 139 | } 140 | } 141 | 142 | /** 143 | * Check if `value` is `Attributes`. 144 | * 145 | * @param {Attributes | Child} value 146 | * Value. 147 | * @returns {value is Attributes} 148 | * Whether `value` is `Attributes`. 149 | */ 150 | function isAttributes(value) { 151 | if ( 152 | value === null || 153 | value === undefined || 154 | typeof value !== 'object' || 155 | Array.isArray(value) 156 | ) { 157 | return false 158 | } 159 | 160 | return true 161 | } 162 | -------------------------------------------------------------------------------- /lib/jsx-automatic.d.ts: -------------------------------------------------------------------------------- 1 | import type {Attributes, Child, Result} from './index.js' 2 | 3 | export namespace JSX { 4 | /** 5 | * This defines the return value of JSX syntax. 6 | */ 7 | type Element = Result 8 | 9 | /** 10 | * This disallows the use of functional components. 11 | */ 12 | type IntrinsicAttributes = never 13 | 14 | /** 15 | * This defines the prop types for known elements. 16 | * 17 | * For `xastscript` this defines any string may be used in combination with `xast` `Attributes`. 18 | * 19 | * This **must** be an interface. 20 | */ 21 | interface IntrinsicElements { 22 | [name: string]: 23 | | Attributes 24 | | { 25 | /** 26 | * The prop that matches `ElementChildrenAttribute` key defines the type of JSX children, defines the children type. 27 | */ 28 | children?: Child 29 | } 30 | } 31 | 32 | /** 33 | * The key of this interface defines as what prop children are passed. 34 | */ 35 | interface ElementChildrenAttribute { 36 | /** 37 | * Only the key matters, not the value. 38 | */ 39 | children?: never 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/jsx-automatic.js: -------------------------------------------------------------------------------- 1 | // Empty (only used for TypeScript). 2 | export {} 3 | -------------------------------------------------------------------------------- /lib/jsx-classic.d.ts: -------------------------------------------------------------------------------- 1 | import type {Attributes, Child, Result} from './index.js' 2 | 3 | /** 4 | * This unique symbol is declared to specify the key on which JSX children are passed, without conflicting 5 | * with the Attributes type. 6 | */ 7 | declare const children: unique symbol 8 | 9 | /** 10 | * This defines the return value of JSX syntax. 11 | */ 12 | export type Element = Result 13 | 14 | /** 15 | * This disallows the use of functional components. 16 | */ 17 | export type IntrinsicAttributes = never 18 | 19 | /** 20 | * This defines the prop types for known elements. 21 | * 22 | * For `xastscript` this defines any string may be used in combination with `xast` `Attributes`. 23 | * 24 | * This **must** be an interface. 25 | */ 26 | export interface IntrinsicElements { 27 | [name: string]: 28 | | Attributes 29 | | { 30 | /** 31 | * The prop that matches `ElementChildrenAttribute` key defines the type of JSX children, defines the children type. 32 | */ 33 | [children]?: Child 34 | } 35 | } 36 | 37 | /** 38 | * The key of this interface defines as what prop children are passed. 39 | */ 40 | export interface ElementChildrenAttribute { 41 | /** 42 | * Only the key matters, not the value. 43 | */ 44 | [children]?: never 45 | } 46 | -------------------------------------------------------------------------------- /lib/jsx-classic.js: -------------------------------------------------------------------------------- 1 | // Empty (only used for TypeScript). 2 | export {} 3 | -------------------------------------------------------------------------------- /lib/runtime.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./index.js').Child} Child 3 | * @typedef {import('./index.js').Element} Element 4 | * @typedef {import('./index.js').Root} Root 5 | * @typedef {import('./index.js').Value} Value 6 | */ 7 | 8 | /** 9 | * @typedef {{[x: string]: Child | Value}} JSXProps 10 | */ 11 | 12 | import {x} from './index.js' 13 | 14 | /** 15 | * Create XML trees in xast through JSX. 16 | * 17 | * @overload 18 | * @param {null} name 19 | * @param {{children?: Child}} props 20 | * @param {string} [key] 21 | * @returns {Root} 22 | * 23 | * @overload 24 | * @param {string} name 25 | * @param {JSXProps} props 26 | * @param {string} [key] 27 | * @returns {Element} 28 | * 29 | * @param {string | null} name 30 | * Qualified name. 31 | * 32 | * Case sensitive and can contain a namespace prefix (such as `rdf:RDF`). 33 | * When string, an `Element` is built. 34 | * When nullish, a `Root` is built instead. 35 | * @param {JSXProps} props 36 | * Map of attributes. 37 | * 38 | * Nullish (`null` or `undefined`) or `NaN` values are ignored, other values 39 | * are turned to strings. 40 | * 41 | * Cannot be given if building a `Root`. 42 | * Cannot be omitted when building an `Element` if the first child is a 43 | * `Node`. 44 | * @param {string | null | undefined} [_key] 45 | * Key (not used). 46 | * @returns {Element | Root} 47 | * Result. 48 | */ 49 | export function jsx(name, props, _key) { 50 | const {children, ...properties} = props 51 | return name === null 52 | ? x(name, children) 53 | : // @ts-expect-error: overloads are complex. 54 | x(name, properties, children) 55 | } 56 | 57 | export const jsxs = jsx 58 | 59 | export const Fragment = null 60 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2020 Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xastscript", 3 | "version": "4.0.0", 4 | "description": "xast utility to create trees", 5 | "license": "MIT", 6 | "keywords": [ 7 | "unist", 8 | "xast", 9 | "xast-util", 10 | "util", 11 | "utility", 12 | "xml", 13 | "dsl", 14 | "extensible", 15 | "markup", 16 | "language" 17 | ], 18 | "repository": "syntax-tree/xastscript", 19 | "bugs": "https://github.com/syntax-tree/xastscript/issues", 20 | "funding": { 21 | "type": "opencollective", 22 | "url": "https://opencollective.com/unified" 23 | }, 24 | "author": "Titus Wormer (https://wooorm.com)", 25 | "contributors": [ 26 | "Titus Wormer (https://wooorm.com)" 27 | ], 28 | "sideEffects": false, 29 | "type": "module", 30 | "exports": { 31 | ".": "./index.js", 32 | "./index.js": "./index.js", 33 | "./jsx-dev-runtime": "./jsx-dev-runtime.js", 34 | "./jsx-runtime": "./jsx-runtime.js" 35 | }, 36 | "files": [ 37 | "lib/", 38 | "index.d.ts", 39 | "index.js", 40 | "jsx-dev-runtime.d.ts", 41 | "jsx-dev-runtime.js", 42 | "jsx-runtime.d.ts", 43 | "jsx-runtime.js" 44 | ], 45 | "dependencies": { 46 | "@types/xast": "^2.0.0" 47 | }, 48 | "devDependencies": { 49 | "@types/node": "^20.0.0", 50 | "c8": "^8.0.0", 51 | "esast-util-from-js": "^2.0.0", 52 | "estree-util-build-jsx": "^3.0.0", 53 | "estree-util-to-js": "^2.0.0", 54 | "prettier": "^3.0.0", 55 | "remark-cli": "^11.0.0", 56 | "remark-preset-wooorm": "^9.0.0", 57 | "tsd": "^0.28.0", 58 | "type-coverage": "^2.0.0", 59 | "typescript": "^5.0.0", 60 | "unist-builder": "^4.0.0", 61 | "xo": "^0.55.0" 62 | }, 63 | "scripts": { 64 | "prepack": "npm run generate && npm run build && npm run format", 65 | "generate": "node script/generate-jsx.js", 66 | "build": "tsc --build --clean && tsc --build && tsd && type-coverage", 67 | "format": "remark . -qfo && prettier . -w --log-level warn && xo --fix", 68 | "test-api": "node --conditions development test/index.js", 69 | "test-coverage": "c8 --100 --reporter lcov npm run test-api", 70 | "test": "npm run generate && npm run build && npm run format && npm run test-coverage" 71 | }, 72 | "prettier": { 73 | "bracketSpacing": false, 74 | "semi": false, 75 | "singleQuote": true, 76 | "tabWidth": 2, 77 | "trailingComma": "none", 78 | "useTabs": false 79 | }, 80 | "remarkConfig": { 81 | "plugins": [ 82 | "remark-preset-wooorm" 83 | ] 84 | }, 85 | "typeCoverage": { 86 | "atLeast": 100, 87 | "detail": true, 88 | "ignoreCatch": true, 89 | "strict": true 90 | }, 91 | "xo": { 92 | "overrides": [ 93 | { 94 | "files": [ 95 | "**/*.ts" 96 | ], 97 | "rules": { 98 | "@typescript-eslint/consistent-indexed-object-style": "off", 99 | "@typescript-eslint/consistent-type-definitions": "off" 100 | } 101 | } 102 | ], 103 | "prettier": true 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # xastscript 2 | 3 | [![Build][build-badge]][build] 4 | [![Coverage][coverage-badge]][coverage] 5 | [![Downloads][downloads-badge]][downloads] 6 | [![Size][size-badge]][size] 7 | [![Sponsors][sponsors-badge]][collective] 8 | [![Backers][backers-badge]][collective] 9 | [![Chat][chat-badge]][chat] 10 | 11 | [xast][] utility to create trees with ease. 12 | 13 | ## Contents 14 | 15 | * [What is this?](#what-is-this) 16 | * [When should I use this?](#when-should-i-use-this) 17 | * [Install](#install) 18 | * [Use](#use) 19 | * [API](#api) 20 | * [`x(name?[, attributes][, …children])`](#xname-attributes-children) 21 | * [`Attributes`](#attributes-1) 22 | * [`Child`](#child) 23 | * [`Result`](#result) 24 | * [Syntax tree](#syntax-tree) 25 | * [JSX](#jsx) 26 | * [Types](#types) 27 | * [Compatibility](#compatibility) 28 | * [Security](#security) 29 | * [Related](#related) 30 | * [Contribute](#contribute) 31 | * [License](#license) 32 | 33 | ## What is this? 34 | 35 | This package is a hyperscript interface (like `createElement` from React and 36 | such) to help with creating xast trees. 37 | 38 | ## When should I use this? 39 | 40 | You can use this utility in your project when you generate xast syntax trees 41 | with code. 42 | It helps because it replaces most of the repetition otherwise needed in a syntax 43 | tree with function calls. 44 | 45 | You can instead use [`unist-builder`][u] when creating any unist nodes and 46 | [`hastscript`][h] when creating hast (HTML) nodes. 47 | 48 | ## Install 49 | 50 | This package is [ESM only][esm]. 51 | In Node.js (version 16+), install with [npm][]: 52 | 53 | ```sh 54 | npm install xastscript 55 | ``` 56 | 57 | In Deno with [`esm.sh`][esmsh]: 58 | 59 | ```js 60 | import {x} from 'https://esm.sh/xastscript@4' 61 | ``` 62 | 63 | In browsers with [`esm.sh`][esmsh]: 64 | 65 | ```html 66 | 69 | ``` 70 | 71 | ## Use 72 | 73 | ```js 74 | import {u} from 'unist-builder' 75 | import {x} from 'xastscript' 76 | 77 | // Children as an array: 78 | console.log( 79 | x('album', {id: 123}, [ 80 | x('name', 'Born in the U.S.A.'), 81 | x('artist', 'Bruce Springsteen'), 82 | x('releasedate', '1984-04-06') 83 | ]) 84 | ) 85 | 86 | // Children as arguments: 87 | console.log( 88 | x( 89 | 'album', 90 | {id: 123}, 91 | x('name', 'Exile in Guyville'), 92 | x('artist', 'Liz Phair'), 93 | x('releasedate', '1993-06-22') 94 | ) 95 | ) 96 | 97 | // For other xast nodes, such as comments, instructions, doctypes, or cdata 98 | // can be created with unist-builder: 99 | console.log( 100 | x(null, [ 101 | u('instruction', {name: 'xml'}, 'version="1.0" encoding="UTF-8"'), 102 | x('album', [ 103 | u('comment', 'Great album!'), 104 | x('name', 'Born in the U.S.A.'), 105 | x('description', [u('cdata', '3 < 5 & 8 > 13')]) 106 | ]) 107 | ]) 108 | ) 109 | ``` 110 | 111 | Yields: 112 | 113 | ```js 114 | { 115 | type: 'element', 116 | name: 'album', 117 | attributes: {id: '123'}, 118 | children: [ 119 | { 120 | type: 'element', 121 | name: 'name', 122 | attributes: {}, 123 | children: [{type: 'text', value: 'Born in the U.S.A.'}] 124 | }, 125 | { 126 | type: 'element', 127 | name: 'artist', 128 | attributes: {}, 129 | children: [{type: 'text', value: 'Bruce Springsteen'}] 130 | }, 131 | { 132 | type: 'element', 133 | name: 'releasedate', 134 | attributes: {}, 135 | children: [{type: 'text', value: '1984-04-06'}] 136 | } 137 | ] 138 | } 139 | { 140 | type: 'element', 141 | name: 'album', 142 | attributes: {id: '123'}, 143 | children: [ 144 | { 145 | type: 'element', 146 | name: 'name', 147 | attributes: {}, 148 | children: [{type: 'text', value: 'Exile in Guyville'}] 149 | }, 150 | { 151 | type: 'element', 152 | name: 'artist', 153 | attributes: {}, 154 | children: [{type: 'text', value: 'Liz Phair'}] 155 | }, 156 | { 157 | type: 'element', 158 | name: 'releasedate', 159 | attributes: {}, 160 | children: [{type: 'text', value: '1993-06-22'}] 161 | } 162 | ] 163 | } 164 | { 165 | type: 'root', 166 | children: [ 167 | {type: 'instruction', name: 'xml', value: 'version="1.0" encoding="UTF-8"'}, 168 | { 169 | type: 'element', 170 | name: 'album', 171 | attributes: {}, 172 | children: [ 173 | {type: 'comment', value: 'Great album!'}, 174 | { 175 | type: 'element', 176 | name: 'name', 177 | attributes: {}, 178 | children: [{type: 'text', value: 'Born in the U.S.A.'}] 179 | }, 180 | { 181 | type: 'element', 182 | name: 'description', 183 | attributes: {}, 184 | children: [{type: 'cdata', value: '3 < 5 & 8 > 13'}] 185 | } 186 | ] 187 | } 188 | ] 189 | } 190 | ``` 191 | 192 | ## API 193 | 194 | This package exports the identifier [`x`][api-x]. 195 | There is no default export. 196 | 197 | The export map supports the automatic JSX runtime. 198 | You can pass `xastscript` to your build tool (TypeScript, Babel, SWC) with an 199 | `importSource` option or similar. 200 | 201 | ### `x(name?[, attributes][, …children])` 202 | 203 | Create [xast][] trees. 204 | 205 | ##### Signatures 206 | 207 | * `x(): root` 208 | * `x(null[, …children]): root` 209 | * `x(name[, attributes][, …children]): element` 210 | 211 | ##### Parameters 212 | 213 | ###### `name` 214 | 215 | Qualified name (`string`, optional). 216 | 217 | Case sensitive and can contain a namespace prefix (such as `rdf:RDF`). 218 | When string, an [`Element`][element] is built. 219 | When nullish, a [`Root`][root] is built instead. 220 | 221 | ###### `attributes` 222 | 223 | Attributes of the element ([`Attributes`][api-attributes], optional). 224 | 225 | ###### `children` 226 | 227 | Children of the node ([`Array`][api-child] or `Child`, optional). 228 | 229 | ##### Returns 230 | 231 | Created tree ([`Result`][api-result]). 232 | 233 | [`Element`][element] when a `name` is passed, otherwise [`Root`][root]. 234 | 235 | ### `Attributes` 236 | 237 | Map of attributes (TypeScript type). 238 | 239 | Nullish (`null` or `undefined`) or `NaN` values are ignored, other values are 240 | turned to strings. 241 | 242 | ###### Type 243 | 244 | ```ts 245 | type Attributes = Record 246 | ``` 247 | 248 | ### `Child` 249 | 250 | (Lists of) children (TypeScript type). 251 | 252 | When strings or numbers are encountered, they are turned into [`Text`][text] 253 | nodes. 254 | [`Root`][root] nodes are treated as “fragments”, meaning that their children 255 | are used instead. 256 | 257 | ###### Type 258 | 259 | ```ts 260 | type Child = 261 | | Array 262 | | Node 263 | | boolean 264 | | number 265 | | string 266 | | null 267 | | undefined 268 | ``` 269 | 270 | ### `Result` 271 | 272 | Result from a `x` call (TypeScript type). 273 | 274 | ###### Type 275 | 276 | ```ts 277 | type Result = Element | Root 278 | ``` 279 | 280 | ## Syntax tree 281 | 282 | The syntax tree is [xast][]. 283 | 284 | ## JSX 285 | 286 | This package can be used with JSX. 287 | You should use the automatic JSX runtime set to `xastscript`. 288 | 289 | > 🪦 **Legacy**: you can also use the classic JSX runtime, but this is not 290 | > recommended. 291 | > To do so, import `x` yourself and define it as the pragma (plus set the 292 | > fragment to `null`). 293 | 294 | The Use example above (omitting the second) can then be written like so: 295 | 296 | ```jsx 297 | /** @jsxImportSource xastscript */ 298 | 299 | import {u} from 'unist-builder' 300 | 301 | console.log( 302 | 303 | Born in the U.S.A. 304 | Bruce Springsteen 305 | 1984-04-06 306 | 307 | ) 308 | 309 | console.log( 310 | <> 311 | {u('instruction', {name: 'xml'}, 'version="1.0" encoding="UTF-8"')} 312 | 313 | {u('comment', 'Great album!')} 314 | Born in the U.S.A. 315 | {u('cdata', '3 < 5 & 8 > 13')} 316 | 317 | 318 | ) 319 | ``` 320 | 321 | ## Types 322 | 323 | This package is fully typed with [TypeScript][]. 324 | It exports the additional types [`Attributes`][api-attributes], 325 | [`Child`][api-child], and [`Result`][api-result]. 326 | 327 | ## Compatibility 328 | 329 | Projects maintained by the unified collective are compatible with maintained 330 | versions of Node.js. 331 | 332 | When we cut a new major release, we drop support for unmaintained versions of 333 | Node. 334 | This means we try to keep the current release line, `xastscript@^4`, compatible 335 | with Node.js 16. 336 | 337 | ## Security 338 | 339 | XML can be a dangerous language: don’t trust user-provided data. 340 | 341 | ## Related 342 | 343 | * [`unist-builder`][u] 344 | — create any unist tree 345 | * [`hastscript`][h] 346 | — create a hast tree 347 | * [`xast-util-to-xml`](https://github.com/syntax-tree/xast-util-to-xml) 348 | — serialize xast as XML 349 | * [`xast-util-from-xml`](https://github.com/syntax-tree/xast-util-from-xml) 350 | — parse xast from XML 351 | * [`hast-util-to-xast`](https://github.com/syntax-tree/hast-util-to-xast) 352 | — transform hast to xast 353 | 354 | ## Contribute 355 | 356 | See [`contributing.md`][contributing] in [`syntax-tree/.github`][health] for 357 | ways to get started. 358 | See [`support.md`][support] for ways to get help. 359 | 360 | This project has a [code of conduct][coc]. 361 | By interacting with this repository, organization, or community you agree to 362 | abide by its terms. 363 | 364 | ## License 365 | 366 | [MIT][license] © [Titus Wormer][author] 367 | 368 | 369 | 370 | [build-badge]: https://github.com/syntax-tree/xastscript/workflows/main/badge.svg 371 | 372 | [build]: https://github.com/syntax-tree/xastscript/actions 373 | 374 | [coverage-badge]: https://img.shields.io/codecov/c/github/syntax-tree/xastscript.svg 375 | 376 | [coverage]: https://codecov.io/github/syntax-tree/xastscript 377 | 378 | [downloads-badge]: https://img.shields.io/npm/dm/xastscript.svg 379 | 380 | [downloads]: https://www.npmjs.com/package/xastscript 381 | 382 | [size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=xastscript 383 | 384 | [size]: https://bundlejs.com/?q=xastscript 385 | 386 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg 387 | 388 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg 389 | 390 | [collective]: https://opencollective.com/unified 391 | 392 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg 393 | 394 | [chat]: https://github.com/syntax-tree/unist/discussions 395 | 396 | [npm]: https://docs.npmjs.com/cli/install 397 | 398 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 399 | 400 | [esmsh]: https://esm.sh 401 | 402 | [typescript]: https://www.typescriptlang.org 403 | 404 | [license]: license 405 | 406 | [author]: https://wooorm.com 407 | 408 | [health]: https://github.com/syntax-tree/.github 409 | 410 | [contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md 411 | 412 | [support]: https://github.com/syntax-tree/.github/blob/main/support.md 413 | 414 | [coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md 415 | 416 | [xast]: https://github.com/syntax-tree/xast 417 | 418 | [root]: https://github.com/syntax-tree/xast#root 419 | 420 | [element]: https://github.com/syntax-tree/xast#element 421 | 422 | [text]: https://github.com/syntax-tree/xast#text 423 | 424 | [u]: https://github.com/syntax-tree/unist-builder 425 | 426 | [h]: https://github.com/syntax-tree/hastscript 427 | 428 | [api-x]: #xname-attributes-children 429 | 430 | [api-attributes]: #attributes-1 431 | 432 | [api-child]: #child 433 | 434 | [api-result]: #result 435 | -------------------------------------------------------------------------------- /script/generate-jsx.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import acornJsx from 'acorn-jsx' 3 | import {buildJsx} from 'estree-util-build-jsx' 4 | import {fromJs} from 'esast-util-from-js' 5 | import {toJs} from 'estree-util-to-js' 6 | 7 | const doc = String( 8 | await fs.readFile(new URL('../test/jsx.jsx', import.meta.url)) 9 | ) 10 | 11 | const treeAutomatic = fromJs( 12 | doc.replace(/'name'/, "'jsx (estree-util-build-jsx, automatic)'"), 13 | {plugins: [acornJsx()], module: true} 14 | ) 15 | 16 | const treeAutomaticDevelopment = fromJs( 17 | doc.replace( 18 | /'name'/, 19 | "'jsx (estree-util-build-jsx, automatic, development)'" 20 | ), 21 | {plugins: [acornJsx()], module: true} 22 | ) 23 | 24 | const treeClassic = fromJs( 25 | doc.replace(/'name'/, "'jsx (estree-util-build-jsx, classic)'"), 26 | { 27 | plugins: [acornJsx()], 28 | module: true 29 | } 30 | ) 31 | 32 | buildJsx(treeAutomatic, { 33 | runtime: 'automatic', 34 | importSource: 'xastscript' 35 | }) 36 | buildJsx(treeAutomaticDevelopment, { 37 | runtime: 'automatic', 38 | importSource: 'xastscript', 39 | development: true 40 | }) 41 | buildJsx(treeClassic, {pragma: 'x', pragmaFrag: 'null'}) 42 | 43 | await fs.writeFile( 44 | new URL('../test/jsx-build-jsx-automatic.js', import.meta.url), 45 | toJs(treeAutomatic).value 46 | ) 47 | 48 | await fs.writeFile( 49 | new URL('../test/jsx-build-jsx-automatic-development.js', import.meta.url), 50 | // There’s a problem with `this` that TS doesn’t like. 51 | '// @ts-nocheck\n\n' + toJs(treeAutomaticDevelopment).value 52 | ) 53 | 54 | await fs.writeFile( 55 | new URL('../test/jsx-build-jsx-classic.js', import.meta.url), 56 | toJs(treeClassic).value 57 | ) 58 | -------------------------------------------------------------------------------- /test-d/automatic.tsx: -------------------------------------------------------------------------------- 1 | /* @jsxRuntime automatic */ 2 | /* @jsxImportSource xastscript */ 3 | 4 | import {expectType} from 'tsd' 5 | import type {Element, Root} from 'xast' 6 | import {x} from '../index.js' 7 | import {Fragment, jsx, jsxs} from '../jsx-runtime.js' 8 | 9 | type Result = Element | Root 10 | 11 | // JSX automatic runtime. 12 | expectType(jsx(Fragment, {})) 13 | expectType(jsx(Fragment, {children: x('x')})) 14 | expectType(jsx('a', {})) 15 | expectType(jsx('a', {children: 'a'})) 16 | expectType(jsx('a', {children: x('x')})) 17 | expectType(jsxs('a', {children: ['a', 'b']})) 18 | expectType(jsxs('a', {children: [x('x'), x('y')]})) 19 | 20 | expectType(<>) 21 | expectType() 22 | expectType() 23 | expectType() 24 | expectType(string) 25 | expectType({['string', 'string']}) 26 | expectType( 27 | 28 | <> 29 | 30 | ) 31 | expectType({x()}) 32 | expectType({x('b')}) 33 | expectType( 34 | 35 | c 36 | 37 | ) 38 | expectType( 39 | 40 | 41 | 42 | 43 | ) 44 | 45 | expectType({[, ]}) 46 | expectType({[, ]}) 47 | expectType({[]}) 48 | 49 | // @ts-expect-error: not a valid child. 50 | expectType() 51 | 52 | // @ts-expect-error: not a valid child. 53 | expectType() 54 | 55 | // @ts-expect-error: not a valid child. 56 | expectType({{invalid: 'child'}}) 57 | 58 | // This is where the automatic runtime differs from the classic runtime. 59 | // The automatic runtime the children prop to define JSX children, whereas it’s used as an attribute in the classic runtime. 60 | expectType(} />) 61 | 62 | declare function Bar(props?: Record): Element 63 | 64 | // @ts-expect-error: components not supported. 65 | expectType() 66 | -------------------------------------------------------------------------------- /test-d/classic.tsx: -------------------------------------------------------------------------------- 1 | /* @jsx x */ 2 | /* @jsxFrag null */ 3 | 4 | import {expectType} from 'tsd' 5 | import type {Element, Root} from 'xast' 6 | import {x} from '../index.js' 7 | 8 | type Result = Element | Root 9 | 10 | expectType(<>) 11 | expectType() 12 | expectType() 13 | expectType() 14 | expectType(string) 15 | expectType({['string', 'string']}) 16 | expectType( 17 | 18 | <> 19 | 20 | ) 21 | expectType({x()}) 22 | expectType({x('b')}) 23 | expectType( 24 | 25 | c 26 | 27 | ) 28 | expectType( 29 | 30 | 31 | 32 | 33 | ) 34 | expectType({[, ]}) 35 | expectType({[, ]}) 36 | expectType({[]}) 37 | 38 | // @ts-expect-error: not a valid attribute value. 39 | expectType() 40 | 41 | // @ts-expect-error: not a valid attribute value. 42 | expectType() 43 | 44 | // @ts-expect-error: not a valid child. 45 | expectType({{invalid: 'child'}}) 46 | 47 | // @ts-expect-error: classic runtime does not accept children as an attribute. 48 | // This is where the classic runtime differs from the automatic runtime. 49 | // The automatic runtime the children prop to define JSX children, whereas it’s 50 | // used as an attribute in the classic runtime. 51 | expectType(} />) 52 | 53 | declare function Bar(props?: Record): Element 54 | 55 | // @ts-expect-error: components not supported. 56 | expectType() 57 | -------------------------------------------------------------------------------- /test-d/index.tsx: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd' 2 | import type {Element, Root} from 'xast' 3 | import {x} from '../index.js' 4 | 5 | expectType(x()) 6 | 7 | // @ts-expect-error: not a valid name. 8 | x(true) 9 | 10 | expectType(x(null)) 11 | expectType(x(undefined)) 12 | expectType(x('')) 13 | expectType(x('', null)) 14 | expectType(x('', undefined)) 15 | expectType(x('', 1)) 16 | expectType(x('', 'a')) 17 | expectType(x('', true)) 18 | expectType(x('', [1, 'a', null])) 19 | expectType(x('', [true])) 20 | 21 | expectType(x('', {})) 22 | expectType(x('', {}, [1, 'a', null])) 23 | expectType(x('', {p: 1})) 24 | expectType(x('', {p: null})) 25 | expectType(x('', {p: undefined})) 26 | expectType(x('', {p: true})) 27 | expectType(x('', {p: false})) 28 | expectType(x('', {p: 'a'})) 29 | 30 | // @ts-expect-error: not a valid child. 31 | x('', {p: [1]}) 32 | 33 | // @ts-expect-error: not a valid child. 34 | x('', {p: [true]}) 35 | 36 | // @ts-expect-error: not a valid child. 37 | x('', {p: ['a']}) 38 | 39 | // @ts-expect-error: not a valid child. 40 | x('', {p: {x: true}}) 41 | -------------------------------------------------------------------------------- /test/core.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import test from 'node:test' 3 | import {x} from 'xastscript' 4 | 5 | test('xastscript', async function (t) { 6 | await t.test('should expose the public api (`/`)', async function () { 7 | assert.deepEqual(Object.keys(await import('xastscript')).sort(), ['x']) 8 | }) 9 | 10 | await t.test( 11 | 'should expose the public api (`/jsx-runtime`)', 12 | async function () { 13 | assert.deepEqual( 14 | // eslint-disable-next-line n/file-extension-in-import -- ESLint is wrong. 15 | Object.keys(await import('xastscript/jsx-runtime')).sort(), 16 | ['Fragment', 'jsx', 'jsxs'] 17 | ) 18 | } 19 | ) 20 | 21 | await t.test( 22 | 'should expose the public api (`/jsx-dev-runtime`)', 23 | async function () { 24 | assert.deepEqual( 25 | // eslint-disable-next-line n/file-extension-in-import -- ESLint is wrong. 26 | Object.keys(await import('xastscript/jsx-dev-runtime')).sort(), 27 | ['Fragment', 'jsxDEV'] 28 | ) 29 | } 30 | ) 31 | 32 | await t.test('should create a root when w/o `name`', async function () { 33 | assert.deepEqual(x(), {type: 'root', children: []}) 34 | }) 35 | 36 | await t.test('should throw w/ incorrect `name`', async function () { 37 | assert.throws(function () { 38 | // @ts-expect-error: check how the runtime handles an incorrect name. 39 | x(1) 40 | }, /Expected element name, got `1`/) 41 | }) 42 | 43 | await t.test('should create an element when given `name`', async function () { 44 | assert.deepEqual(x('y'), { 45 | type: 'element', 46 | name: 'y', 47 | attributes: {}, 48 | children: [] 49 | }) 50 | }) 51 | 52 | await t.test('should treat `name` case-sensitive', async function () { 53 | assert.deepEqual(x('Y'), { 54 | type: 'element', 55 | name: 'Y', 56 | attributes: {}, 57 | children: [] 58 | }) 59 | }) 60 | 61 | await t.test( 62 | 'should create an element with given attributes', 63 | async function () { 64 | assert.deepEqual(x('y', {a: 'b'}), { 65 | type: 'element', 66 | name: 'y', 67 | attributes: {a: 'b'}, 68 | children: [] 69 | }) 70 | } 71 | ) 72 | 73 | await t.test( 74 | 'should ignore null, undefined, and NaN attribute values', 75 | async function () { 76 | assert.deepEqual(x('y', {a: null, b: undefined, c: Number.NaN}), { 77 | type: 'element', 78 | name: 'y', 79 | attributes: {}, 80 | children: [] 81 | }) 82 | } 83 | ) 84 | 85 | await t.test('should add a child', async function () { 86 | assert.deepEqual(x('y', {}, x('z')), { 87 | type: 'element', 88 | name: 'y', 89 | attributes: {}, 90 | children: [{type: 'element', name: 'z', attributes: {}, children: []}] 91 | }) 92 | }) 93 | 94 | await t.test('should add children as an array', async function () { 95 | assert.deepEqual(x('y', {}, [x('a'), x('b')]), { 96 | type: 'element', 97 | name: 'y', 98 | attributes: {}, 99 | children: [ 100 | {type: 'element', name: 'a', attributes: {}, children: []}, 101 | {type: 'element', name: 'b', attributes: {}, children: []} 102 | ] 103 | }) 104 | }) 105 | 106 | await t.test( 107 | 'should add children in a deeply nested array', 108 | async function () { 109 | assert.deepEqual( 110 | // @ts-expect-error: check how the runtime handles deep children lists (not support in TS). 111 | x('y', {}, [[[x('a')]], [[[[x('b')]], x('c')]]]), 112 | { 113 | type: 'element', 114 | name: 'y', 115 | attributes: {}, 116 | children: [ 117 | {type: 'element', name: 'a', attributes: {}, children: []}, 118 | {type: 'element', name: 'b', attributes: {}, children: []}, 119 | {type: 'element', name: 'c', attributes: {}, children: []} 120 | ] 121 | } 122 | ) 123 | } 124 | ) 125 | 126 | await t.test('should add children as arguments', async function () { 127 | assert.deepEqual(x('y', {}, x('a'), x('b')), { 128 | type: 'element', 129 | name: 'y', 130 | attributes: {}, 131 | children: [ 132 | {type: 'element', name: 'a', attributes: {}, children: []}, 133 | {type: 'element', name: 'b', attributes: {}, children: []} 134 | ] 135 | }) 136 | }) 137 | 138 | await t.test('should add strings and numbers as literals', async function () { 139 | assert.deepEqual(x('y', {}, 'a', 1), { 140 | type: 'element', 141 | name: 'y', 142 | attributes: {}, 143 | children: [ 144 | {type: 'text', value: 'a'}, 145 | {type: 'text', value: '1'} 146 | ] 147 | }) 148 | }) 149 | 150 | await t.test('should ignore null and undefined children', async function () { 151 | assert.deepEqual(x('y', {}, null, undefined), { 152 | type: 'element', 153 | name: 'y', 154 | attributes: {}, 155 | children: [] 156 | }) 157 | }) 158 | 159 | await t.test('should throw on invalid children', async function () { 160 | assert.throws(function () { 161 | // @ts-expect-error: check how the runtime handles non-nodes. 162 | x('y', {}, {}) 163 | }, /Expected node, nodes, string, got `\[object Object]`/) 164 | }) 165 | 166 | await t.test( 167 | 'should support omitting attributes when given a string for a child', 168 | async function () { 169 | assert.deepEqual(x('y', 'z'), { 170 | type: 'element', 171 | name: 'y', 172 | attributes: {}, 173 | children: [{type: 'text', value: 'z'}] 174 | }) 175 | } 176 | ) 177 | 178 | await t.test( 179 | 'should support omitting attributes when given a number for a child', 180 | async function () { 181 | assert.deepEqual(x('y', 1), { 182 | type: 'element', 183 | name: 'y', 184 | attributes: {}, 185 | children: [{type: 'text', value: '1'}] 186 | }) 187 | } 188 | ) 189 | 190 | await t.test( 191 | 'should support omitting attributes when given an array for a child', 192 | async function () { 193 | assert.deepEqual(x('y', ['a', 1]), { 194 | type: 'element', 195 | name: 'y', 196 | attributes: {}, 197 | children: [ 198 | {type: 'text', value: 'a'}, 199 | {type: 'text', value: '1'} 200 | ] 201 | }) 202 | } 203 | ) 204 | 205 | await t.test('should create a root with a textual child', async function () { 206 | assert.deepEqual(x(null, '1'), { 207 | type: 'root', 208 | children: [{type: 'text', value: '1'}] 209 | }) 210 | }) 211 | 212 | await t.test( 213 | 'should create a root with a numerical child', 214 | async function () { 215 | assert.deepEqual(x(null, 1), { 216 | type: 'root', 217 | children: [{type: 'text', value: '1'}] 218 | }) 219 | } 220 | ) 221 | 222 | await t.test('should create a root with a node child', async function () { 223 | assert.deepEqual(x(null, x('a')), { 224 | type: 'root', 225 | children: [{type: 'element', name: 'a', attributes: {}, children: []}] 226 | }) 227 | }) 228 | 229 | await t.test( 230 | 'should create a node w/ by unraveling roots', 231 | async function () { 232 | assert.deepEqual(x('a', {}, [x(null, x('b'))]), { 233 | type: 'element', 234 | name: 'a', 235 | attributes: {}, 236 | children: [{type: 'element', name: 'b', attributes: {}, children: []}] 237 | }) 238 | } 239 | ) 240 | }) 241 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unassigned-import */ 2 | import './core.js' 3 | import './jsx-build-jsx-automatic.js' 4 | import './jsx-build-jsx-automatic-development.js' 5 | import './jsx-build-jsx-classic.js' 6 | /* eslint-enable import/no-unassigned-import */ 7 | -------------------------------------------------------------------------------- /test/jsx.jsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource xastscript */ 2 | 3 | import assert from 'node:assert/strict' 4 | import test from 'node:test' 5 | import {u} from 'unist-builder' 6 | import {x} from 'xastscript' 7 | 8 | test('name', async function (t) { 9 | await t.test('should support a self-closing element', async function () { 10 | assert.deepEqual(, x('a')) 11 | }) 12 | 13 | await t.test('should support a value as a child', async function () { 14 | assert.deepEqual(b, x('a', 'b')) 15 | }) 16 | 17 | await t.test('should support an uppercase tag name', async function () { 18 | const A = 'a' 19 | 20 | // Note: this file is a template, generated with different runtimes. 21 | // @ts-ignore: TS (depending on this build) sometimes doesn’t understand. 22 | assert.deepEqual(, x(A)) 23 | }) 24 | 25 | await t.test('should support expressions as children', async function () { 26 | assert.deepEqual({1 + 1}, x('a', '2')) 27 | }) 28 | 29 | await t.test('should support a fragment', async function () { 30 | assert.deepEqual(<>, u('root', [])) 31 | }) 32 | 33 | await t.test('should support a fragment with text', async function () { 34 | assert.deepEqual(<>a, u('root', [u('text', 'a')])) 35 | }) 36 | 37 | await t.test('should support a fragment with an element', async function () { 38 | assert.deepEqual( 39 | <> 40 | 41 | , 42 | u('root', [x('a')]) 43 | ) 44 | }) 45 | 46 | await t.test( 47 | 'should support a fragment with an expression', 48 | async function () { 49 | assert.deepEqual(<>{-1}, u('root', [u('text', '-1')])) 50 | } 51 | ) 52 | 53 | await t.test('should support members as names (`a.b`)', async function () { 54 | const com = {acme: {a: 'A', b: 'B'}} 55 | 56 | assert.deepEqual( 57 | // Note: this file is a template, generated with different runtimes. 58 | // @ts-ignore: TS (depending on this build) sometimes doesn’t understand. 59 | , 60 | x(com.acme.a) 61 | ) 62 | }) 63 | 64 | await t.test('should support a boolean attribute', async function () { 65 | assert.deepEqual(, x('a', {b: 'true'})) 66 | }) 67 | 68 | await t.test('should support a double quoted attribute', async function () { 69 | assert.deepEqual(, x('a', {b: ''})) 70 | }) 71 | 72 | await t.test('should support a single quoted attribute', async function () { 73 | assert.deepEqual(, x('a', {b: '"'})) 74 | }) 75 | 76 | await t.test('should support expression value attributes', async function () { 77 | assert.deepEqual(, x('a', {b: '2'})) 78 | }) 79 | 80 | await t.test( 81 | 'should support expression spread attributes', 82 | async function () { 83 | const props = {a: 1, b: 2} 84 | 85 | assert.deepEqual(, x('a', props)) 86 | } 87 | ) 88 | 89 | await t.test( 90 | 'should support text, elements, and expressions in JSX', 91 | async function () { 92 | assert.deepEqual( 93 | 94 | ce 95 | {1 + 1} 96 | , 97 | x('a', [x('b'), 'c', x('d', 'e'), '2']) 98 | ) 99 | } 100 | ) 101 | 102 | await t.test( 103 | 'should support a fragment in an element (#1)', 104 | async function () { 105 | assert.deepEqual( 106 | 107 | <>{1} 108 | , 109 | x('a', '1') 110 | ) 111 | } 112 | ) 113 | 114 | await t.test( 115 | 'should support a fragment in an element (#2)', 116 | async function () { 117 | const dl = [ 118 | ['Firefox', 'A red panda.'], 119 | ['Chrome', 'A chemical element.'] 120 | ] 121 | 122 | assert.deepEqual( 123 |
124 | {dl.map(function ([title, definition]) { 125 | return ( 126 | <> 127 |
{title}
128 |
{definition}
129 | 130 | ) 131 | })} 132 |
, 133 | x('dl', [ 134 | x('dt', dl[0][0]), 135 | x('dd', dl[0][1]), 136 | x('dt', dl[1][0]), 137 | x('dd', dl[1][1]) 138 | ]) 139 | ) 140 | } 141 | ) 142 | }) 143 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "exactOptionalPropertyTypes": true, 8 | "jsx": "preserve", 9 | "lib": ["es2022"], 10 | "module": "node16", 11 | "strict": true, 12 | "target": "es2022" 13 | }, 14 | "exclude": ["coverage/", "node_modules/"], 15 | "include": [ 16 | "**/*.js", 17 | "**/*.jsx", 18 | "lib/jsx-automatic.d.ts", 19 | "lib/jsx-classic.d.ts" 20 | ] 21 | } 22 | --------------------------------------------------------------------------------