├── .editorconfig ├── .gitattributes ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── example ├── create-tree.js ├── index.css ├── index.html ├── preact.html ├── react.html ├── readme.md ├── solid.html ├── svelte.html └── vue.html ├── index.d.ts ├── index.js ├── lib ├── index.js ├── types.d.ts └── types.js ├── license ├── package.json ├── readme.md ├── test └── index.js ├── tsconfig.json └── types-esmsh.d.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # https://github.com/github/linguist/blob/HEAD/docs/overrides.md 2 | example/*.html linguist-vendored 3 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | runs-on: ubuntu-latest 4 | steps: 5 | - uses: unifiedjs/beep-boop-beta@main 6 | with: 7 | repo-token: ${{secrets.GITHUB_TOKEN}} 8 | name: bb 9 | on: 10 | issues: 11 | types: [closed, edited, labeled, opened, reopened, unlabeled] 12 | pull_request_target: 13 | types: [closed, edited, labeled, opened, reopened, unlabeled] 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | main: 3 | name: ${{matrix.node}} 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v4 7 | - uses: actions/setup-node@v4 8 | with: 9 | node-version: ${{matrix.node}} 10 | - run: npm install 11 | - run: npm test 12 | - uses: codecov/codecov-action@v5 13 | strategy: 14 | matrix: 15 | node: 16 | - lts/hydrogen 17 | - node 18 | name: main 19 | on: 20 | - pull_request 21 | - push 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.d.ts.map 3 | *.d.ts 4 | *.log 5 | *.tsbuildinfo 6 | coverage/ 7 | node_modules/ 8 | yarn.lock 9 | /example/hast-util-to-jsx-runtime.min.js 10 | !/lib/types.d.ts 11 | !/index.d.ts 12 | !/types-esmsh.d.ts 13 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | example/hast-util-to-jsx-runtime.min.js 3 | *.html 4 | *.md 5 | -------------------------------------------------------------------------------- /example/create-tree.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate a hast tree for checking. 3 | * 4 | * > 👉 **Note**: this file is actual ESM that runs in browsers. 5 | */ 6 | 7 | /* eslint-env browser */ 8 | 9 | // To do: note: update this once in a while. 10 | import {h, s} from 'https://esm.sh/hastscript@9?dev' 11 | 12 | export function createTree() { 13 | return h('div', [ 14 | h('p', [ 15 | 'note: to show that state is kept, this is rerendered every second, it’s now ', 16 | h('b', new Date().toLocaleString()), 17 | '!' 18 | ]), 19 | h('hr'), 20 | h('h2', 'Event handlers'), 21 | h('p', [ 22 | 'You should be able to click on this and print something to the console.' 23 | ]), 24 | h('button', {onClick: 'console.log("it worked!")'}, 'Print to console'), 25 | h('hr'), 26 | h('h2', 'Inputs with control'), 27 | h('p', ['You should be able to change this text input:']), 28 | h('div', [ 29 | h('input#text-a', {type: 'text', value: 'Some text?'}), 30 | h('label', {htmlFor: 'text-a'}, 'Text input') 31 | ]), 32 | h('hr'), 33 | h('p', ['You should be able to change this range input:']), 34 | h('div', [ 35 | h('input#range-a', {type: 'range', value: 5, min: 0, max: 10}), 36 | h('label', {htmlFor: 'range-a'}, 'Range input') 37 | ]), 38 | h('hr'), 39 | h('p', ['You should be able to change this radio group:']), 40 | h('div', [ 41 | h('input#radio-a', { 42 | type: 'radio', 43 | name: 'radios', 44 | value: 'a', 45 | checked: true 46 | }), 47 | h('label', {htmlFor: 'radio-a'}, 'Alpha'), 48 | h('input#radio-b', {type: 'radio', name: 'radios', value: 'b'}), 49 | h('label', {htmlFor: 'radio-b'}, 'Bravo'), 50 | h('input#radio-c', {type: 'radio', name: 'radios', value: 'c'}), 51 | h('label', {htmlFor: 'radio-c'}, 'Charlie') 52 | ]), 53 | h('hr'), 54 | h('h2', 'Style attribute'), 55 | h( 56 | 'p', 57 | {style: {color: '#0366d6'}}, 58 | 'is this blue? Then style objects work' 59 | ), 60 | h('p', {style: 'color: #0366d6'}, 'is this blue? Then style strings work'), 61 | h( 62 | 'p', 63 | {style: '-webkit-transform: rotate(0.01turn)'}, 64 | 'is this tilted in webkit? Then vendor prefixes in style strings work' 65 | ), 66 | h( 67 | 'p', 68 | {style: {'-webkit-transform': 'rotate(0.01turn)'}}, 69 | 'is this tilted in webkit? Then prefixes in style objects work' 70 | ), 71 | h( 72 | 'p', 73 | {style: {WebkitTransform: 'rotate(0.01turn)'}}, 74 | 'is this tilted in webkit? Then camelcased in style objects work' 75 | ), 76 | h( 77 | 'p', 78 | { 79 | style: 80 | 'display: -webkit-box; overflow: hidden; -webkit-box-orient: vertical; -webkit-line-clamp: 2' 81 | }, 82 | 'This should be capped at 2 lines in webkit! Lorem ipsum dolor sit amet consectetur adipisicing elit. Temporibus natus similique eum. Dolorem est at aliquam, explicabo similique repudiandae veritatis? Eum aliquam hic eaque tenetur, enim ex odio voluptatum repellendus!' 83 | ), 84 | h( 85 | 'p', 86 | {style: '--fg: #0366d6; color: var(--fg)'}, 87 | 'Is this blue? Then CSS variables work.' 88 | ), 89 | h('h2', 'SVG: camel- and dash-cased attributes'), 90 | h('p', [ 91 | 'You should see two bright blue circles, the second skewed. ', 92 | 'This checks that the ', 93 | h('code', 'gradientUnits'), 94 | ' attribute (camel case), ', 95 | h('code', 'gradientTransform'), 96 | ' attribute (camel case), and ', 97 | h('code', 'stop-color'), 98 | ' attribute (dash case), all work. ', 99 | 'It also checks that the ', 100 | h('code', 'radialGradient'), 101 | ' element (camel case) works.' 102 | ]), 103 | // Example based on 104 | s('svg', {viewBox: '0 0 420 200', xmlns: 'http://www.w3.org/2000/svg'}, [ 105 | s( 106 | 'radialGradient#gradient-a', 107 | {gradientUnits: 'userSpaceOnUse', cx: 100, cy: 100, r: 100}, 108 | [ 109 | s('stop', {offset: '50%', stopColor: '#0366d6'}), 110 | s('stop', {offset: '50.1%', stopColor: 'transparent'}) 111 | ] 112 | ), 113 | s( 114 | 'radialGradient#gradient-b', 115 | { 116 | gradientUnits: 'userSpaceOnUse', 117 | cx: 100, 118 | cy: 100, 119 | r: 100, 120 | gradientTransform: 'skewX(25) translate(-50, 0)' 121 | }, 122 | [ 123 | s('stop', {offset: '50%', stopColor: '#0366d6'}), 124 | s('stop', {offset: '50.1%', stopColor: 'transparent'}) 125 | ] 126 | ), 127 | s('rect', { 128 | x: 0, 129 | y: 0, 130 | width: 200, 131 | height: 200, 132 | fill: 'url(#gradient-a)' 133 | }), 134 | s('rect', { 135 | x: 0, 136 | y: 0, 137 | width: 200, 138 | height: 200, 139 | fill: 'url(#gradient-b)', 140 | style: {transform: 'translateX(220px)'} 141 | }) 142 | ]), 143 | h('h2', 'xlink:href'), 144 | h('p', [ 145 | 'You should see one big circle broken down over four squares. ', 146 | 'The top right square is different, it instead contains one small circle. ', 147 | 'This checks that the ', 148 | h('code', 'clipPathUnits'), 149 | ' attribute (camel case), ', 150 | h('code', 'clip-path'), 151 | ' attribute (dash case), and', 152 | h('code', 'clipPath'), 153 | ' element (camel case), all work. ', 154 | 'Importantly, it also checks for ', 155 | h('code', 'xlink:href'), 156 | '!' 157 | ]), 158 | // Example from 159 | s('svg', {viewBox: '0 0 100 100'}, [ 160 | s('clipPath#clip-a', {clipPathUnits: 'userSpaceOnUse'}, [ 161 | s('circle', {cx: 50, cy: 50, r: 35}) 162 | ]), 163 | s('clipPath#clip-b', {clipPathUnits: 'objectBoundingBox'}, [ 164 | s('circle', {cx: 0.5, cy: 0.5, r: 0.35}) 165 | ]), 166 | s('rect#rect-a', {x: 0, y: 0, width: 45, height: 45}), 167 | s('rect#rect-b', {x: 0, y: 55, width: 45, height: 45}), 168 | s('rect#rect-c', {x: 55, y: 55, width: 45, height: 45}), 169 | s('rect#rect-d', {x: 55, y: 0, width: 45, height: 45}), 170 | s('use', { 171 | clipPath: 'url(#clip-a)', 172 | xLinkHref: '#rect-a', 173 | fill: '#0366d6' 174 | }), 175 | s('use', { 176 | clipPath: 'url(#clip-a)', 177 | xLinkHref: '#rect-b', 178 | fill: '#0366d6' 179 | }), 180 | s('use', { 181 | clipPath: 'url(#clip-a)', 182 | xLinkHref: '#rect-c', 183 | fill: '#0366d6' 184 | }), 185 | s('use', { 186 | clipPath: 'url(#clip-b)', 187 | xLinkHref: '#rect-d', 188 | fill: '#0366d6' 189 | }) 190 | ]), 191 | h('h2', 'mathml'), 192 | h('p', [ 193 | 'You should see a formula that has 100% width, where the content is ', 194 | 'centered, and with a big blue border. ', 195 | 'There should also be visible whitespace between ', 196 | h('code', '∀A'), 197 | ' and ', 198 | h('code', '∃P') 199 | ]), 200 | h( 201 | 'math', 202 | { 203 | xmlns: 'http://www.w3.org/1998/Math/MathML', 204 | display: 'block', 205 | className: ['mathml-class-works'] 206 | }, 207 | [ 208 | h( 209 | 'mrow', 210 | h('mo', {rspace: '0'}, '∀'), 211 | h('mi', 'A'), 212 | h('mo', {lspace: '0.22em', rspace: '0'}, '∃'), 213 | h('mi', 'P'), 214 | h('mo', {lspace: '0.22em', rspace: '0'}, '∀'), 215 | h('mi', 'B'), 216 | h('mspace', {width: '0.17em'}), 217 | h('mrow', [ 218 | h('mo', '['), 219 | h('mrow', [ 220 | h('mi', 'B'), 221 | h('mo', '∈'), 222 | h('mi', 'P'), 223 | h('mo', {lspace: '0.28em', rspace: '0.28em'}, '⟺'), 224 | h('mo', {rspace: '0'}, '∀'), 225 | h('mi', 'C'), 226 | h('mspace', {width: '0.17em'}), 227 | h('mrow', [ 228 | h('mo', '('), 229 | h('mrow', [ 230 | h('mi', 'C'), 231 | h('mo', '∈'), 232 | h('mi', 'B'), 233 | h('mo', '⇒'), 234 | h('mi', 'C'), 235 | h('mo', '∈'), 236 | h('mi', 'A') 237 | ]), 238 | h('mo', ')') 239 | ]) 240 | ]), 241 | h('mo', ']') 242 | ]) 243 | ) 244 | ] 245 | ), 246 | h('h2', 'xml:lang'), 247 | h('style', ':lang(fr) { color: #0366d6; }'), 248 | h('p', {xmlLang: 'fr'}, 'C’est bleu ? Ensuite ça marche'), 249 | h('h2', 'Custom elements'), 250 | h('style', '.custom-element-class-name { color: #0366d6; }'), 251 | h( 252 | 'some-element', 253 | {className: ['custom-element-class-name']}, 254 | 'Does it work?' 255 | ) 256 | ]) 257 | } 258 | -------------------------------------------------------------------------------- /example/index.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Style the examples. 3 | */ 4 | 5 | :root { 6 | color-scheme: light dark; 7 | background-color: hsl(0, 0%, 90%); 8 | } 9 | 10 | * { 11 | line-height: calc(1em + 1ex); 12 | box-sizing: border-box; 13 | } 14 | 15 | a { 16 | color: #0367d8; 17 | } 18 | 19 | body { 20 | font-family: system-ui; 21 | margin: 3em auto; 22 | max-width: 30em; 23 | } 24 | 25 | input { 26 | font: inherit; 27 | } 28 | 29 | code { 30 | font-family: 31 | 'San Francisco Mono', 'Monaco', 'Consolas', 'Lucida Console', 32 | 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; 33 | font-feature-settings: normal; 34 | font-size: smaller; 35 | background-color: rgba(0, 0, 0, 0.04); 36 | border-radius: 3px; 37 | padding: 0.2em 0.4em; 38 | } 39 | 40 | @media (prefers-color-scheme: dark) { 41 | :root { 42 | background-color: hsl(214, 13%, 10%); 43 | color: hsl(214, 13%, 90%); 44 | } 45 | 46 | code { 47 | background-color: rgba(0, 0, 0, 0.4); 48 | } 49 | } 50 | 51 | .mathml-class-works { 52 | border: 1ex solid #0366d6; 53 | padding: 1ex; 54 | } 55 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | hast-util-to-jsx-runtime 5 | 6 |

hast-util-to-jsx-runtime

7 |

See the following examples to check how different frameworks work: 8 |

    9 |
  1. React 10 |
  2. preact 11 |
  3. Solid 12 |
  4. Svelte 13 |
  5. Vue 14 | -------------------------------------------------------------------------------- /example/preact.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | preact + hast-util-to-jsx-runtime 5 | 6 |

    preact + hast-util-to-jsx-runtime

    7 |

    Note! This example is dynamic!

    8 |
    9 | 26 | -------------------------------------------------------------------------------- /example/react.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | React + hast-util-to-jsx-runtime 5 | 6 |

    React + hast-util-to-jsx-runtime

    7 |

    Note! This example is dynamic!

    8 |
    9 | 30 | -------------------------------------------------------------------------------- /example/readme.md: -------------------------------------------------------------------------------- 1 | # `hast-util-to-jsx-runtime`: web examples 2 | 3 | This folder contains examples that run in web browsers that check whether 4 | this utility works with different frameworks. 5 | 6 | To use them, first set up the Git repo with: 7 | 8 | ```sh 9 | npm install 10 | npm test 11 | ``` 12 | 13 | Then, start a simple server in this examples folder: 14 | 15 | ```sh 16 | python3 -m http.server 17 | ``` 18 | 19 | Open `http://localhost:8000` in a browser to see the results. 20 | -------------------------------------------------------------------------------- /example/solid.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Solid + hast-util-to-jsx-runtime 5 | 6 |

    Solid + hast-util-to-jsx-runtime

    7 |

    Note! This example is static! (to do: figure out a way to rerender Solid without changing everything?)

    8 |
    9 | 22 | -------------------------------------------------------------------------------- /example/svelte.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Svelte + hast-util-to-jsx-runtime 5 | 6 |

    Svelte + hast-util-to-jsx-runtime

    7 |

    Note! This example is static! (to do: figure out a way to rerender Svelte)

    8 |

    Note! Svelte seems completely broken: no support for style, SVG, etc.

    9 |
    10 | 20 | -------------------------------------------------------------------------------- /example/vue.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Vue + hast-util-to-jsx-runtime 5 | 6 |

    Vue + hast-util-to-jsx-runtime

    7 |

    Note! This example is static! (to do: figure out a way to rerender Vue)

    8 |
    9 | 27 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | Components, 3 | CreateEvaluater, 4 | ElementAttributeNameCase, 5 | EvaluateExpression, 6 | EvaluateProgram, 7 | Evaluater, 8 | ExtraProps, 9 | Fragment, 10 | Jsx, 11 | JsxDev, 12 | Options, 13 | Props, 14 | Source, 15 | Space, 16 | StylePropertyNameCase 17 | } from './lib/types.js' 18 | export {toJsxRuntime} from './lib/index.js' 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Note: types exposed from `index.d.ts`. 2 | export {toJsxRuntime} from './lib/index.js' 3 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {Identifier, Literal, MemberExpression} from 'estree' 3 | * @import {Jsx, JsxDev, Options, Props} from 'hast-util-to-jsx-runtime' 4 | * @import {Element, Nodes, Parents, Root, Text} from 'hast' 5 | * @import {MdxFlowExpressionHast, MdxTextExpressionHast} from 'mdast-util-mdx-expression' 6 | * @import {MdxJsxFlowElementHast, MdxJsxTextElementHast} from 'mdast-util-mdx-jsx' 7 | * @import {MdxjsEsmHast} from 'mdast-util-mdxjs-esm' 8 | * @import {Position} from 'unist' 9 | * @import {Child, Create, Field, JsxElement, State, Style} from './types.js' 10 | */ 11 | 12 | import {stringify as commas} from 'comma-separated-tokens' 13 | import {ok as assert} from 'devlop' 14 | import {name as isIdentifierName} from 'estree-util-is-identifier-name' 15 | import {whitespace} from 'hast-util-whitespace' 16 | import {find, hastToReact, html, svg} from 'property-information' 17 | import {stringify as spaces} from 'space-separated-tokens' 18 | import styleToJs from 'style-to-js' 19 | import {pointStart} from 'unist-util-position' 20 | import {VFileMessage} from 'vfile-message' 21 | 22 | // To do: next major: `Object.hasOwn`. 23 | const own = {}.hasOwnProperty 24 | 25 | /** @type {Map} */ 26 | const emptyMap = new Map() 27 | 28 | const cap = /[A-Z]/g 29 | 30 | // `react-dom` triggers a warning for *any* white space in tables. 31 | // To follow GFM, `mdast-util-to-hast` injects line endings between elements. 32 | // Other tools might do so too, but they don’t do here, so we remove all of 33 | // that. 34 | 35 | // See: . 36 | // See: . 37 | // See: . 38 | // See: . 39 | // See: . 40 | // See: . 41 | const tableElements = new Set(['table', 'tbody', 'thead', 'tfoot', 'tr']) 42 | 43 | const tableCellElement = new Set(['td', 'th']) 44 | 45 | const docs = 'https://github.com/syntax-tree/hast-util-to-jsx-runtime' 46 | 47 | /** 48 | * Transform a hast tree to preact, react, solid, svelte, vue, etc., 49 | * with an automatic JSX runtime. 50 | * 51 | * @param {Nodes} tree 52 | * Tree to transform. 53 | * @param {Options} options 54 | * Configuration (required). 55 | * @returns {JsxElement} 56 | * JSX element. 57 | */ 58 | 59 | export function toJsxRuntime(tree, options) { 60 | if (!options || options.Fragment === undefined) { 61 | throw new TypeError('Expected `Fragment` in options') 62 | } 63 | 64 | const filePath = options.filePath || undefined 65 | /** @type {Create} */ 66 | let create 67 | 68 | if (options.development) { 69 | if (typeof options.jsxDEV !== 'function') { 70 | throw new TypeError( 71 | 'Expected `jsxDEV` in options when `development: true`' 72 | ) 73 | } 74 | 75 | create = developmentCreate(filePath, options.jsxDEV) 76 | } else { 77 | if (typeof options.jsx !== 'function') { 78 | throw new TypeError('Expected `jsx` in production options') 79 | } 80 | 81 | if (typeof options.jsxs !== 'function') { 82 | throw new TypeError('Expected `jsxs` in production options') 83 | } 84 | 85 | create = productionCreate(filePath, options.jsx, options.jsxs) 86 | } 87 | 88 | /** @type {State} */ 89 | const state = { 90 | Fragment: options.Fragment, 91 | ancestors: [], 92 | components: options.components || {}, 93 | create, 94 | elementAttributeNameCase: options.elementAttributeNameCase || 'react', 95 | evaluater: options.createEvaluater ? options.createEvaluater() : undefined, 96 | filePath, 97 | ignoreInvalidStyle: options.ignoreInvalidStyle || false, 98 | passKeys: options.passKeys !== false, 99 | passNode: options.passNode || false, 100 | schema: options.space === 'svg' ? svg : html, 101 | stylePropertyNameCase: options.stylePropertyNameCase || 'dom', 102 | tableCellAlignToStyle: options.tableCellAlignToStyle !== false 103 | } 104 | 105 | const result = one(state, tree, undefined) 106 | 107 | // JSX element. 108 | if (result && typeof result !== 'string') { 109 | return result 110 | } 111 | 112 | // Text node or something that turned into nothing. 113 | return state.create( 114 | tree, 115 | state.Fragment, 116 | {children: result || undefined}, 117 | undefined 118 | ) 119 | } 120 | 121 | /** 122 | * Transform a node. 123 | * 124 | * @param {State} state 125 | * Info passed around. 126 | * @param {Nodes} node 127 | * Current node. 128 | * @param {string | undefined} key 129 | * Key. 130 | * @returns {Child | undefined} 131 | * Child, optional. 132 | */ 133 | function one(state, node, key) { 134 | if (node.type === 'element') { 135 | return element(state, node, key) 136 | } 137 | 138 | if (node.type === 'mdxFlowExpression' || node.type === 'mdxTextExpression') { 139 | return mdxExpression(state, node) 140 | } 141 | 142 | if (node.type === 'mdxJsxFlowElement' || node.type === 'mdxJsxTextElement') { 143 | return mdxJsxElement(state, node, key) 144 | } 145 | 146 | if (node.type === 'mdxjsEsm') { 147 | return mdxEsm(state, node) 148 | } 149 | 150 | if (node.type === 'root') { 151 | return root(state, node, key) 152 | } 153 | 154 | if (node.type === 'text') { 155 | return text(state, node) 156 | } 157 | } 158 | 159 | /** 160 | * Handle element. 161 | * 162 | * @param {State} state 163 | * Info passed around. 164 | * @param {Element} node 165 | * Current node. 166 | * @param {string | undefined} key 167 | * Key. 168 | * @returns {Child | undefined} 169 | * Child, optional. 170 | */ 171 | function element(state, node, key) { 172 | const parentSchema = state.schema 173 | let schema = parentSchema 174 | 175 | if (node.tagName.toLowerCase() === 'svg' && parentSchema.space === 'html') { 176 | schema = svg 177 | state.schema = schema 178 | } 179 | 180 | state.ancestors.push(node) 181 | 182 | const type = findComponentFromName(state, node.tagName, false) 183 | const props = createElementProps(state, node) 184 | let children = createChildren(state, node) 185 | 186 | if (tableElements.has(node.tagName)) { 187 | children = children.filter(function (child) { 188 | return typeof child === 'string' ? !whitespace(child) : true 189 | }) 190 | } 191 | 192 | addNode(state, props, type, node) 193 | addChildren(props, children) 194 | 195 | // Restore. 196 | state.ancestors.pop() 197 | state.schema = parentSchema 198 | 199 | return state.create(node, type, props, key) 200 | } 201 | 202 | /** 203 | * Handle MDX expression. 204 | * 205 | * @param {State} state 206 | * Info passed around. 207 | * @param {MdxFlowExpressionHast | MdxTextExpressionHast} node 208 | * Current node. 209 | * @returns {Child | undefined} 210 | * Child, optional. 211 | */ 212 | function mdxExpression(state, node) { 213 | if (node.data && node.data.estree && state.evaluater) { 214 | const program = node.data.estree 215 | const expression = program.body[0] 216 | assert(expression.type === 'ExpressionStatement') 217 | 218 | // Assume result is a child. 219 | return /** @type {Child | undefined} */ ( 220 | state.evaluater.evaluateExpression(expression.expression) 221 | ) 222 | } 223 | 224 | crashEstree(state, node.position) 225 | } 226 | 227 | /** 228 | * Handle MDX ESM. 229 | * 230 | * @param {State} state 231 | * Info passed around. 232 | * @param {MdxjsEsmHast} node 233 | * Current node. 234 | * @returns {Child | undefined} 235 | * Child, optional. 236 | */ 237 | function mdxEsm(state, node) { 238 | if (node.data && node.data.estree && state.evaluater) { 239 | // Assume result is a child. 240 | return /** @type {Child | undefined} */ ( 241 | state.evaluater.evaluateProgram(node.data.estree) 242 | ) 243 | } 244 | 245 | crashEstree(state, node.position) 246 | } 247 | 248 | /** 249 | * Handle MDX JSX. 250 | * 251 | * @param {State} state 252 | * Info passed around. 253 | * @param {MdxJsxFlowElementHast | MdxJsxTextElementHast} node 254 | * Current node. 255 | * @param {string | undefined} key 256 | * Key. 257 | * @returns {Child | undefined} 258 | * Child, optional. 259 | */ 260 | function mdxJsxElement(state, node, key) { 261 | const parentSchema = state.schema 262 | let schema = parentSchema 263 | 264 | if (node.name === 'svg' && parentSchema.space === 'html') { 265 | schema = svg 266 | state.schema = schema 267 | } 268 | 269 | state.ancestors.push(node) 270 | 271 | const type = 272 | node.name === null 273 | ? state.Fragment 274 | : findComponentFromName(state, node.name, true) 275 | const props = createJsxElementProps(state, node) 276 | const children = createChildren(state, node) 277 | 278 | addNode(state, props, type, node) 279 | addChildren(props, children) 280 | 281 | // Restore. 282 | state.ancestors.pop() 283 | state.schema = parentSchema 284 | 285 | return state.create(node, type, props, key) 286 | } 287 | 288 | /** 289 | * Handle root. 290 | * 291 | * @param {State} state 292 | * Info passed around. 293 | * @param {Root} node 294 | * Current node. 295 | * @param {string | undefined} key 296 | * Key. 297 | * @returns {Child | undefined} 298 | * Child, optional. 299 | */ 300 | function root(state, node, key) { 301 | /** @type {Props} */ 302 | const props = {} 303 | 304 | addChildren(props, createChildren(state, node)) 305 | 306 | return state.create(node, state.Fragment, props, key) 307 | } 308 | 309 | /** 310 | * Handle text. 311 | * 312 | * @param {State} _ 313 | * Info passed around. 314 | * @param {Text} node 315 | * Current node. 316 | * @returns {Child | undefined} 317 | * Child, optional. 318 | */ 319 | function text(_, node) { 320 | return node.value 321 | } 322 | 323 | /** 324 | * Add `node` to props. 325 | * 326 | * @param {State} state 327 | * Info passed around. 328 | * @param {Props} props 329 | * Props. 330 | * @param {unknown} type 331 | * Type. 332 | * @param {Element | MdxJsxFlowElementHast | MdxJsxTextElementHast} node 333 | * Node. 334 | * @returns {undefined} 335 | * Nothing. 336 | */ 337 | function addNode(state, props, type, node) { 338 | // If this is swapped out for a component: 339 | if (typeof type !== 'string' && type !== state.Fragment && state.passNode) { 340 | props.node = node 341 | } 342 | } 343 | 344 | /** 345 | * Add children to props. 346 | * 347 | * @param {Props} props 348 | * Props. 349 | * @param {Array} children 350 | * Children. 351 | * @returns {undefined} 352 | * Nothing. 353 | */ 354 | function addChildren(props, children) { 355 | if (children.length > 0) { 356 | const value = children.length > 1 ? children : children[0] 357 | 358 | if (value) { 359 | props.children = value 360 | } 361 | } 362 | } 363 | 364 | /** 365 | * @param {string | undefined} _ 366 | * Path to file. 367 | * @param {Jsx} jsx 368 | * Dynamic. 369 | * @param {Jsx} jsxs 370 | * Static. 371 | * @returns {Create} 372 | * Create a production element. 373 | */ 374 | function productionCreate(_, jsx, jsxs) { 375 | return create 376 | /** @type {Create} */ 377 | function create(_, type, props, key) { 378 | // Only an array when there are 2 or more children. 379 | const isStaticChildren = Array.isArray(props.children) 380 | const fn = isStaticChildren ? jsxs : jsx 381 | return key ? fn(type, props, key) : fn(type, props) 382 | } 383 | } 384 | 385 | /** 386 | * @param {string | undefined} filePath 387 | * Path to file. 388 | * @param {JsxDev} jsxDEV 389 | * Development. 390 | * @returns {Create} 391 | * Create a development element. 392 | */ 393 | function developmentCreate(filePath, jsxDEV) { 394 | return create 395 | /** @type {Create} */ 396 | function create(node, type, props, key) { 397 | // Only an array when there are 2 or more children. 398 | const isStaticChildren = Array.isArray(props.children) 399 | const point = pointStart(node) 400 | return jsxDEV( 401 | type, 402 | props, 403 | key, 404 | isStaticChildren, 405 | { 406 | columnNumber: point ? point.column - 1 : undefined, 407 | fileName: filePath, 408 | lineNumber: point ? point.line : undefined 409 | }, 410 | undefined 411 | ) 412 | } 413 | } 414 | 415 | /** 416 | * Create props from an element. 417 | * 418 | * @param {State} state 419 | * Info passed around. 420 | * @param {Element} node 421 | * Current element. 422 | * @returns {Props} 423 | * Props. 424 | */ 425 | function createElementProps(state, node) { 426 | /** @type {Props} */ 427 | const props = {} 428 | /** @type {string | undefined} */ 429 | let alignValue 430 | /** @type {string} */ 431 | let prop 432 | 433 | for (prop in node.properties) { 434 | if (prop !== 'children' && own.call(node.properties, prop)) { 435 | const result = createProperty(state, prop, node.properties[prop]) 436 | 437 | if (result) { 438 | const [key, value] = result 439 | 440 | if ( 441 | state.tableCellAlignToStyle && 442 | key === 'align' && 443 | typeof value === 'string' && 444 | tableCellElement.has(node.tagName) 445 | ) { 446 | alignValue = value 447 | } else { 448 | props[key] = value 449 | } 450 | } 451 | } 452 | } 453 | 454 | if (alignValue) { 455 | // Assume style is an object. 456 | const style = /** @type {Style} */ (props.style || (props.style = {})) 457 | style[state.stylePropertyNameCase === 'css' ? 'text-align' : 'textAlign'] = 458 | alignValue 459 | } 460 | 461 | return props 462 | } 463 | 464 | /** 465 | * Create props from a JSX element. 466 | * 467 | * @param {State} state 468 | * Info passed around. 469 | * @param {MdxJsxFlowElementHast | MdxJsxTextElementHast} node 470 | * Current JSX element. 471 | * @returns {Props} 472 | * Props. 473 | */ 474 | function createJsxElementProps(state, node) { 475 | /** @type {Props} */ 476 | const props = {} 477 | 478 | for (const attribute of node.attributes) { 479 | if (attribute.type === 'mdxJsxExpressionAttribute') { 480 | if (attribute.data && attribute.data.estree && state.evaluater) { 481 | const program = attribute.data.estree 482 | const expression = program.body[0] 483 | assert(expression.type === 'ExpressionStatement') 484 | const objectExpression = expression.expression 485 | assert(objectExpression.type === 'ObjectExpression') 486 | const property = objectExpression.properties[0] 487 | assert(property.type === 'SpreadElement') 488 | 489 | Object.assign( 490 | props, 491 | state.evaluater.evaluateExpression(property.argument) 492 | ) 493 | } else { 494 | crashEstree(state, node.position) 495 | } 496 | } else { 497 | // For JSX, the author is responsible of passing in the correct values. 498 | const name = attribute.name 499 | /** @type {unknown} */ 500 | let value 501 | 502 | if (attribute.value && typeof attribute.value === 'object') { 503 | if ( 504 | attribute.value.data && 505 | attribute.value.data.estree && 506 | state.evaluater 507 | ) { 508 | const program = attribute.value.data.estree 509 | const expression = program.body[0] 510 | assert(expression.type === 'ExpressionStatement') 511 | value = state.evaluater.evaluateExpression(expression.expression) 512 | } else { 513 | crashEstree(state, node.position) 514 | } 515 | } else { 516 | value = attribute.value === null ? true : attribute.value 517 | } 518 | 519 | // Assume a prop. 520 | props[name] = /** @type {Props[keyof Props]} */ (value) 521 | } 522 | } 523 | 524 | return props 525 | } 526 | 527 | /** 528 | * Create children. 529 | * 530 | * @param {State} state 531 | * Info passed around. 532 | * @param {Parents} node 533 | * Current element. 534 | * @returns {Array} 535 | * Children. 536 | */ 537 | function createChildren(state, node) { 538 | /** @type {Array} */ 539 | const children = [] 540 | let index = -1 541 | /** @type {Map} */ 542 | // Note: test this when Solid doesn’t want to merge my upcoming PR. 543 | /* c8 ignore next */ 544 | const countsByName = state.passKeys ? new Map() : emptyMap 545 | 546 | while (++index < node.children.length) { 547 | const child = node.children[index] 548 | /** @type {string | undefined} */ 549 | let key 550 | 551 | if (state.passKeys) { 552 | const name = 553 | child.type === 'element' 554 | ? child.tagName 555 | : child.type === 'mdxJsxFlowElement' || 556 | child.type === 'mdxJsxTextElement' 557 | ? child.name 558 | : undefined 559 | 560 | if (name) { 561 | const count = countsByName.get(name) || 0 562 | key = name + '-' + count 563 | countsByName.set(name, count + 1) 564 | } 565 | } 566 | 567 | const result = one(state, child, key) 568 | if (result !== undefined) children.push(result) 569 | } 570 | 571 | return children 572 | } 573 | 574 | /** 575 | * Handle a property. 576 | * 577 | * @param {State} state 578 | * Info passed around. 579 | * @param {string} prop 580 | * Key. 581 | * @param {Array | boolean | number | string | null | undefined} value 582 | * hast property value. 583 | * @returns {Field | undefined} 584 | * Field for runtime, optional. 585 | */ 586 | function createProperty(state, prop, value) { 587 | const info = find(state.schema, prop) 588 | 589 | // Ignore nullish and `NaN` values. 590 | if ( 591 | value === null || 592 | value === undefined || 593 | (typeof value === 'number' && Number.isNaN(value)) 594 | ) { 595 | return 596 | } 597 | 598 | if (Array.isArray(value)) { 599 | // Accept `array`. 600 | // Most props are space-separated. 601 | value = info.commaSeparated ? commas(value) : spaces(value) 602 | } 603 | 604 | // React only accepts `style` as object. 605 | if (info.property === 'style') { 606 | let styleObject = 607 | typeof value === 'object' ? value : parseStyle(state, String(value)) 608 | 609 | if (state.stylePropertyNameCase === 'css') { 610 | styleObject = transformStylesToCssCasing(styleObject) 611 | } 612 | 613 | return ['style', styleObject] 614 | } 615 | 616 | return [ 617 | state.elementAttributeNameCase === 'react' && info.space 618 | ? hastToReact[info.property] || info.property 619 | : info.attribute, 620 | value 621 | ] 622 | } 623 | 624 | /** 625 | * Parse a CSS declaration to an object. 626 | * 627 | * @param {State} state 628 | * Info passed around. 629 | * @param {string} value 630 | * CSS declarations. 631 | * @returns {Style} 632 | * Properties. 633 | * @throws 634 | * Throws `VFileMessage` when CSS cannot be parsed. 635 | */ 636 | function parseStyle(state, value) { 637 | try { 638 | return styleToJs(value, {reactCompat: true}) 639 | } catch (error) { 640 | if (state.ignoreInvalidStyle) { 641 | return {} 642 | } 643 | 644 | const cause = /** @type {Error} */ (error) 645 | const message = new VFileMessage('Cannot parse `style` attribute', { 646 | ancestors: state.ancestors, 647 | cause, 648 | ruleId: 'style', 649 | source: 'hast-util-to-jsx-runtime' 650 | }) 651 | message.file = state.filePath || undefined 652 | message.url = docs + '#cannot-parse-style-attribute' 653 | 654 | throw message 655 | } 656 | } 657 | 658 | /** 659 | * Create a JSX name from a string. 660 | * 661 | * @param {State} state 662 | * To do. 663 | * @param {string} name 664 | * Name. 665 | * @param {boolean} allowExpression 666 | * Allow member expressions and identifiers. 667 | * @returns {unknown} 668 | * To do. 669 | */ 670 | function findComponentFromName(state, name, allowExpression) { 671 | /** @type {Identifier | Literal | MemberExpression} */ 672 | let result 673 | 674 | if (!allowExpression) { 675 | result = {type: 'Literal', value: name} 676 | } else if (name.includes('.')) { 677 | const identifiers = name.split('.') 678 | let index = -1 679 | /** @type {Identifier | Literal | MemberExpression | undefined} */ 680 | let node 681 | 682 | while (++index < identifiers.length) { 683 | /** @type {Identifier | Literal} */ 684 | const prop = isIdentifierName(identifiers[index]) 685 | ? {type: 'Identifier', name: identifiers[index]} 686 | : {type: 'Literal', value: identifiers[index]} 687 | node = node 688 | ? { 689 | type: 'MemberExpression', 690 | object: node, 691 | property: prop, 692 | computed: Boolean(index && prop.type === 'Literal'), 693 | optional: false 694 | } 695 | : prop 696 | } 697 | 698 | assert(node, 'always a result') 699 | result = node 700 | } else { 701 | result = 702 | isIdentifierName(name) && !/^[a-z]/.test(name) 703 | ? {type: 'Identifier', name} 704 | : {type: 'Literal', value: name} 705 | } 706 | 707 | // Only literals can be passed in `components` currently. 708 | // No identifiers / member expressions. 709 | if (result.type === 'Literal') { 710 | const name = /** @type {string | number} */ (result.value) 711 | return own.call(state.components, name) ? state.components[name] : name 712 | } 713 | 714 | // Assume component. 715 | if (state.evaluater) { 716 | return state.evaluater.evaluateExpression(result) 717 | } 718 | 719 | crashEstree(state) 720 | } 721 | 722 | /** 723 | * @param {State} state 724 | * @param {Position | undefined} [place] 725 | * @returns {never} 726 | */ 727 | function crashEstree(state, place) { 728 | const message = new VFileMessage( 729 | 'Cannot handle MDX estrees without `createEvaluater`', 730 | { 731 | ancestors: state.ancestors, 732 | place, 733 | ruleId: 'mdx-estree', 734 | source: 'hast-util-to-jsx-runtime' 735 | } 736 | ) 737 | message.file = state.filePath || undefined 738 | message.url = docs + '#cannot-handle-mdx-estrees-without-createevaluater' 739 | 740 | throw message 741 | } 742 | 743 | /** 744 | * Transform a DOM casing style object to a CSS casing style object. 745 | * 746 | * @param {Style} domCasing 747 | * @returns {Style} 748 | */ 749 | function transformStylesToCssCasing(domCasing) { 750 | /** @type {Style} */ 751 | const cssCasing = {} 752 | /** @type {string} */ 753 | let from 754 | 755 | for (from in domCasing) { 756 | if (own.call(domCasing, from)) { 757 | cssCasing[transformStyleToCssCasing(from)] = domCasing[from] 758 | } 759 | } 760 | 761 | return cssCasing 762 | } 763 | 764 | /** 765 | * Transform a DOM casing style field to a CSS casing style field. 766 | * 767 | * @param {string} from 768 | * @returns {string} 769 | */ 770 | function transformStyleToCssCasing(from) { 771 | let to = from.replace(cap, toDash) 772 | // Handle `ms-xxx` -> `-ms-xxx`. 773 | if (to.slice(0, 3) === 'ms-') to = '-' + to 774 | return to 775 | } 776 | 777 | /** 778 | * Make `$0` dash cased. 779 | * 780 | * @param {string} $0 781 | * Capitalized ASCII leter. 782 | * @returns {string} 783 | * Dash and lower letter. 784 | */ 785 | function toDash($0) { 786 | return '-' + $0.toLowerCase() 787 | } 788 | -------------------------------------------------------------------------------- /lib/types.d.ts: -------------------------------------------------------------------------------- 1 | import type {Expression, Program} from 'estree' 2 | import type {Element, Nodes, Parents} from 'hast' 3 | import type { 4 | MdxJsxFlowElementHast, 5 | MdxJsxTextElementHast 6 | } from 'mdast-util-mdx-jsx' 7 | import type {Schema} from 'property-information' 8 | 9 | /** 10 | * Child. 11 | */ 12 | export type Child = JsxElement | string | null | undefined 13 | 14 | /** 15 | * Possible components to use. 16 | * 17 | * Each key is a tag name typed in `JSX.IntrinsicElements`. 18 | * Each value is either a different tag name, or a component accepting the 19 | * corresponding props (and an optional `node` prop if `passNode` is on). 20 | * 21 | * You can access props at `JSX.IntrinsicElements`. 22 | * For example, to find props for `a`, use `JSX.IntrinsicElements['a']`. 23 | */ 24 | // Note: this type has to be in `.ts` or `.d.ts`, otherwise TSC hardcodes 25 | // react into the `.d.ts` file. 26 | export type Components = { 27 | [TagName in keyof JsxIntrinsicElements]: 28 | | Component 29 | | keyof JsxIntrinsicElements 30 | } 31 | 32 | /** 33 | * Function or class component. 34 | * 35 | * You can access props at `JsxIntrinsicElements`. 36 | * For example, to find props for `a`, use `JsxIntrinsicElements['a']`. 37 | * 38 | * @typeParam ComponentProps 39 | * Props type. 40 | */ 41 | type Component = 42 | | ClassComponent 43 | | FunctionComponent 44 | 45 | /** 46 | * Create an evaluator that turns ESTree ASTs from embedded MDX into values. 47 | */ 48 | export type CreateEvaluater = () => Evaluater 49 | 50 | /** 51 | * Create something in development or production. 52 | */ 53 | export type Create = ( 54 | node: Nodes, 55 | type: unknown, 56 | props: Props, 57 | key: string | undefined 58 | ) => JsxElement 59 | 60 | /** 61 | * Class component: given props, returns an instance. 62 | * 63 | * @typeParam ComponentProps 64 | * Props type. 65 | * @param props 66 | * Props. 67 | * @returns 68 | * Instance. 69 | */ 70 | type ClassComponent = new ( 71 | props: ComponentProps 72 | ) => JsxElementClass 73 | 74 | /** 75 | * Casing to use for attribute names. 76 | * 77 | * HTML casing is for example `class`, `stroke-linecap`, `xml:lang`. 78 | * React casing is for example `className`, `strokeLinecap`, `xmlLang`. 79 | */ 80 | export type ElementAttributeNameCase = 'html' | 'react' 81 | 82 | /** 83 | * Turn an MDX expression into a value. 84 | */ 85 | export type EvaluateExpression = (expression: Expression) => unknown 86 | /** 87 | * Turn an MDX program (export/import statements) into a value. 88 | */ 89 | export type EvaluateProgram = (expression: Program) => unknown 90 | 91 | /** 92 | * Evaluator that turns ESTree ASTs from embedded MDX into values. 93 | */ 94 | export interface Evaluater { 95 | /** 96 | * Evaluate an expression. 97 | */ 98 | evaluateExpression: EvaluateExpression 99 | /** 100 | * Evaluate a program. 101 | */ 102 | evaluateProgram: EvaluateProgram 103 | } 104 | 105 | /** 106 | * Extra fields we pass. 107 | */ 108 | export interface ExtraProps { 109 | /** 110 | * Node (hast), 111 | * passed when `passNode` is on. 112 | */ 113 | node?: Element | undefined 114 | } 115 | 116 | /** 117 | * Property field. 118 | */ 119 | export type Field = [string, Value] 120 | 121 | /** 122 | * Represent the children, typically a symbol. 123 | */ 124 | export type Fragment = unknown 125 | 126 | /** 127 | * Basic functional component: given props, returns an element. 128 | * 129 | * @typeParam ComponentProps 130 | * Props type. 131 | * @param props 132 | * Props. 133 | * @returns 134 | * Result. 135 | */ 136 | type FunctionComponent = ( 137 | props: ComponentProps 138 | ) => JsxElement | string | null | undefined 139 | 140 | /** 141 | * Conditional type for a class. 142 | */ 143 | // @ts-ignore: conditionally defined; 144 | // it used to be possible to detect that with `any extends X ? X : Y` 145 | // but no longer. 146 | export type JsxElementClass = JSX.ElementClass 147 | 148 | /** 149 | * Conditional type for a node object. 150 | */ 151 | // @ts-ignore: conditionally defined; 152 | // it used to be possible to detect that with `any extends X ? X : Y` 153 | // but no longer. 154 | export type JsxElement = JSX.Element 155 | 156 | /** 157 | * Conditional type for a record of tag names to corresponding props. 158 | */ 159 | // @ts-ignore: conditionally defined; 160 | // it used to be possible to detect that with `any extends X ? X : Y` 161 | // but no longer. 162 | export type JsxIntrinsicElements = JSX.IntrinsicElements 163 | 164 | /** 165 | * Create a development element. 166 | */ 167 | export type JsxDev = ( 168 | // `any` because runtimes often have complex framework-specific types here. 169 | // type-coverage:ignore-next-line 170 | type: any, 171 | props: Props, 172 | key: string | undefined, 173 | isStaticChildren: boolean, 174 | source: Source, 175 | self: undefined 176 | ) => JsxElement 177 | 178 | /** 179 | * Create a production element. 180 | */ 181 | export type Jsx = ( 182 | // `any` because runtimes often have complex framework-specific types here. 183 | // type-coverage:ignore-next-line 184 | type: any, 185 | props: Props, 186 | key?: string | undefined 187 | ) => JsxElement 188 | 189 | /** 190 | * Configuration. 191 | */ 192 | export interface OptionsBase { 193 | /** 194 | * Components to use (optional). 195 | */ 196 | components?: Partial | null | undefined 197 | /** 198 | * Create an evaluator that turns ESTree ASTs into values (optional). 199 | */ 200 | createEvaluater?: CreateEvaluater | null | undefined 201 | /** 202 | * Specify casing to use for attribute names (default: `'react'`). 203 | */ 204 | elementAttributeNameCase?: ElementAttributeNameCase | null | undefined 205 | /** 206 | * File path to the original source file (optional). 207 | * 208 | * Passed in source info to `jsxDEV` when using the automatic runtime with 209 | * `development: true`. 210 | */ 211 | filePath?: string | null | undefined 212 | /** 213 | * Ignore invalid CSS in `style` props (default: `false`); 214 | * the default behavior is to throw an error. 215 | */ 216 | ignoreInvalidStyle?: boolean | null | undefined 217 | /** 218 | * Generate keys to optimize frameworks that support them (default: `true`). 219 | * 220 | * > 👉 **Note**: Solid currently fails if keys are passed. 221 | */ 222 | passKeys?: boolean | null | undefined 223 | /** 224 | * Pass the hast element node to components (default: `false`). 225 | */ 226 | passNode?: boolean | null | undefined 227 | /** 228 | * Whether `tree` is in the `'html'` or `'svg'` space (default: `'html'`). 229 | * 230 | * When an `` element is found in the HTML space, this package already 231 | * automatically switches to and from the SVG space when entering and exiting 232 | * it. 233 | */ 234 | space?: Space | null | undefined 235 | /** 236 | * Specify casing to use for property names in `style` objects (default: 237 | * `'dom'`). 238 | */ 239 | stylePropertyNameCase?: StylePropertyNameCase | null | undefined 240 | /** 241 | * Turn obsolete `align` props on `td` and `th` into CSS `style` props 242 | * (default: `true`). 243 | */ 244 | tableCellAlignToStyle?: boolean | null | undefined 245 | } 246 | 247 | /** 248 | * Configuration (development). 249 | */ 250 | export interface OptionsDevelopment extends OptionsBase { 251 | /** 252 | * Fragment. 253 | */ 254 | Fragment: Fragment 255 | /** 256 | * Whether to use `jsxDEV` (when on) or `jsx` and `jsxs` (when off). 257 | */ 258 | development: true 259 | /** 260 | * Development JSX. 261 | */ 262 | jsxDEV: JsxDev 263 | /** 264 | * Static JSX (optional). 265 | */ 266 | jsxs?: Jsx | null | undefined 267 | /** 268 | * Dynamic JSX (optional). 269 | */ 270 | jsx?: Jsx | null | undefined 271 | } 272 | 273 | /** 274 | * Configuration (production). 275 | */ 276 | export interface OptionsProduction extends OptionsBase { 277 | /** 278 | * Fragment. 279 | */ 280 | Fragment: Fragment 281 | /** 282 | * Whether to use `jsxDEV` (when on) or `jsx` and `jsxs` (when off) (optional). 283 | */ 284 | development?: false | null | undefined 285 | /** 286 | * Development JSX (optional). 287 | */ 288 | jsxDEV?: JsxDev | null | undefined 289 | /** 290 | * Static JSX. 291 | */ 292 | jsxs: Jsx 293 | /** 294 | * Dynamic JSX. 295 | */ 296 | jsx: Jsx 297 | } 298 | 299 | /** 300 | * Configuration (production or development). 301 | */ 302 | export interface OptionsUnknown extends OptionsBase { 303 | /** 304 | * Fragment. 305 | */ 306 | Fragment: Fragment 307 | /** 308 | * Whether to use `jsxDEV` (when on) or `jsx` and `jsxs` (when off). 309 | */ 310 | development: boolean 311 | /** 312 | * Dynamic JSX (optional). 313 | */ 314 | jsx?: Jsx | null | undefined 315 | /** 316 | * Development JSX (optional). 317 | */ 318 | jsxDEV?: JsxDev | null | undefined 319 | /** 320 | * Static JSX (optional). 321 | */ 322 | jsxs?: Jsx | null | undefined 323 | } 324 | 325 | export type Options = OptionsDevelopment | OptionsProduction | OptionsUnknown 326 | 327 | /** 328 | * Properties and children. 329 | */ 330 | export interface Props { 331 | [prop: string]: 332 | | Array 333 | | Child 334 | | Element 335 | | MdxJsxFlowElementHast 336 | | MdxJsxTextElementHast 337 | | Value 338 | | undefined 339 | children?: Array | Child 340 | node?: Element | MdxJsxFlowElementHast | MdxJsxTextElementHast | undefined 341 | } 342 | 343 | /** 344 | * Info about source. 345 | */ 346 | export interface Source { 347 | /** 348 | * Column where thing starts (0-indexed). 349 | */ 350 | columnNumber: number | undefined 351 | /** 352 | * Name of source file. 353 | */ 354 | fileName: string | undefined 355 | /** 356 | * Line where thing starts (1-indexed). 357 | */ 358 | lineNumber: number | undefined 359 | } 360 | 361 | /** 362 | * Namespace. 363 | * 364 | * > 👉 **Note**: hast is not XML. 365 | * > It supports SVG as embedded in HTML. 366 | * > It does not support the features available in XML. 367 | * > Passing SVG might break but fragments of modern SVG should be fine. 368 | * > Use `xast` if you need to support SVG as XML. 369 | */ 370 | export type Space = 'html' | 'svg' 371 | 372 | /** 373 | * Info passed around. 374 | */ 375 | export interface State { 376 | /** 377 | * Fragment symbol. 378 | */ 379 | Fragment: unknown 380 | /** 381 | * Stack of parents. 382 | */ 383 | ancestors: Array 384 | /** 385 | * Components to swap. 386 | */ 387 | components: Partial 388 | /** 389 | * Create something in development or production. 390 | */ 391 | create: Create 392 | /** 393 | * Casing to use for attribute names. 394 | */ 395 | elementAttributeNameCase: ElementAttributeNameCase 396 | /** 397 | * Evaluator that turns ESTree ASTs into values. 398 | */ 399 | evaluater: Evaluater | undefined 400 | /** 401 | * File path. 402 | */ 403 | filePath: string | undefined 404 | /** 405 | * Ignore invalid CSS in `style` props. 406 | */ 407 | ignoreInvalidStyle: boolean 408 | /** 409 | * Generate keys to optimize frameworks that support them. 410 | */ 411 | passKeys: boolean 412 | /** 413 | * Pass `node` to components. 414 | */ 415 | passNode: boolean 416 | /** 417 | * Current schema. 418 | */ 419 | schema: Schema 420 | /** 421 | * Casing to use for property names in `style` objects. 422 | */ 423 | stylePropertyNameCase: StylePropertyNameCase 424 | /** 425 | * Turn obsolete `align` props on `td` and `th` into CSS `style` props. 426 | */ 427 | tableCellAlignToStyle: boolean 428 | } 429 | 430 | /** 431 | * Casing to use for property names in `style` objects. 432 | * 433 | * CSS casing is for example `background-color` and `-webkit-line-clamp`. 434 | * DOM casing is for example `backgroundColor` and `WebkitLineClamp`. 435 | */ 436 | export type StylePropertyNameCase = 'css' | 'dom' 437 | 438 | /** 439 | * Style map. 440 | */ 441 | type Style = Record 442 | 443 | /** 444 | * Primitive property value and `Style` map. 445 | */ 446 | type Value = Style | boolean | number | string 447 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | // TypeScript only. 2 | export {} 3 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 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 | "author": "Titus Wormer (https://wooorm.com)", 3 | "bugs": "https://github.com/syntax-tree/hast-util-to-jsx-runtime/issues", 4 | "contributors": [ 5 | "Titus Wormer (https://wooorm.com)" 6 | ], 7 | "dependencies": { 8 | "@types/estree": "^1.0.0", 9 | "@types/hast": "^3.0.0", 10 | "@types/unist": "^3.0.0", 11 | "comma-separated-tokens": "^2.0.0", 12 | "devlop": "^1.0.0", 13 | "estree-util-is-identifier-name": "^3.0.0", 14 | "hast-util-whitespace": "^3.0.0", 15 | "mdast-util-mdx-expression": "^2.0.0", 16 | "mdast-util-mdx-jsx": "^3.0.0", 17 | "mdast-util-mdxjs-esm": "^2.0.0", 18 | "property-information": "^7.0.0", 19 | "space-separated-tokens": "^2.0.0", 20 | "style-to-js": "^1.0.0", 21 | "unist-util-position": "^5.0.0", 22 | "vfile-message": "^4.0.0" 23 | }, 24 | "description": "hast utility to transform to preact, react, solid, svelte, vue, etc", 25 | "devDependencies": { 26 | "@types/node": "^22.0.0", 27 | "@types/react": "^19.0.0", 28 | "@types/react-dom": "^19.0.0", 29 | "c8": "^10.0.0", 30 | "esbuild": "^0.25.0", 31 | "estree-util-visit": "^2.0.0", 32 | "hastscript": "^9.0.0", 33 | "prettier": "^3.0.0", 34 | "react": "^19.0.0", 35 | "react-dom": "^19.0.0", 36 | "remark-cli": "^12.0.0", 37 | "remark-preset-wooorm": "^11.0.0", 38 | "sval": "^0.6.0", 39 | "type-coverage": "^2.0.0", 40 | "typescript": "^5.0.0", 41 | "xo": "^0.60.0" 42 | }, 43 | "exports": "./index.js", 44 | "files": [ 45 | "lib/", 46 | "index.d.ts", 47 | "index.js" 48 | ], 49 | "funding": { 50 | "type": "opencollective", 51 | "url": "https://opencollective.com/unified" 52 | }, 53 | "keywords": [ 54 | "hast-util", 55 | "hast", 56 | "html", 57 | "preact", 58 | "react", 59 | "solid", 60 | "svelte", 61 | "unist", 62 | "utility", 63 | "util", 64 | "vue" 65 | ], 66 | "license": "MIT", 67 | "name": "hast-util-to-jsx-runtime", 68 | "prettier": { 69 | "bracketSpacing": false, 70 | "semi": false, 71 | "singleQuote": true, 72 | "tabWidth": 2, 73 | "trailingComma": "none", 74 | "useTabs": false 75 | }, 76 | "remarkConfig": { 77 | "plugins": [ 78 | "remark-preset-wooorm" 79 | ] 80 | }, 81 | "repository": "syntax-tree/hast-util-to-jsx-runtime", 82 | "scripts": { 83 | "build": "tsc --build --clean && tsc --build && type-coverage", 84 | "format": "remark --frail --output --quiet -- . && prettier --log-level warn --write -- . && xo --fix", 85 | "generate": "esbuild --bundle --format=esm --minify --outfile=example/hast-util-to-jsx-runtime.min.js --target=es2020 .", 86 | "prepack": "npm run build && npm run format", 87 | "test-api": "node --conditions development test/index.js", 88 | "test-coverage": "c8 --100 --reporter lcov -- npm run test-api", 89 | "test": "npm run generate && npm run build && npm run format && npm run test-coverage" 90 | }, 91 | "sideEffects": false, 92 | "typeCoverage": { 93 | "atLeast": 100, 94 | "detail": true, 95 | "ignoreCatch": true, 96 | "ignoreFiles": [ 97 | "example/**/*.js" 98 | ], 99 | "strict": true 100 | }, 101 | "type": "module", 102 | "version": "2.3.6", 103 | "xo": { 104 | "overrides": [ 105 | { 106 | "files": [ 107 | "**/*.d.ts" 108 | ], 109 | "rules": { 110 | "@typescript-eslint/array-type": [ 111 | "error", 112 | { 113 | "default": "generic" 114 | } 115 | ], 116 | "@typescript-eslint/ban-ts-comment": 0, 117 | "@typescript-eslint/ban-types": [ 118 | "error", 119 | { 120 | "extendDefaults": true 121 | } 122 | ], 123 | "@typescript-eslint/consistent-type-definitions": [ 124 | "error", 125 | "interface" 126 | ] 127 | } 128 | } 129 | ], 130 | "prettier": true, 131 | "rules": { 132 | "logical-assignment-operators": "off", 133 | "unicorn/prefer-at": "off", 134 | "unicorn/prefer-string-replace-all": "off", 135 | "unicorn/prevent-abbreviations": "off" 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # hast-util-to-jsx-runtime 2 | 3 | [![Build][badge-build-image]][badge-build-url] 4 | [![Coverage][badge-coverage-image]][badge-coverage-url] 5 | [![Downloads][badge-downloads-image]][badge-downloads-url] 6 | [![Size][badge-size-image]][badge-size-url] 7 | 8 | hast utility to transform a tree to 9 | preact, react, solid, svelte, vue, etcetera, 10 | with an automatic JSX runtime. 11 | 12 | ## Contents 13 | 14 | * [What is this?](#what-is-this) 15 | * [When should I use this?](#when-should-i-use-this) 16 | * [Install](#install) 17 | * [Use](#use) 18 | * [API](#api) 19 | * [`toJsxRuntime(tree, options)`](#tojsxruntimetree-options) 20 | * [`Components`](#components) 21 | * [`CreateEvaluater`](#createevaluater) 22 | * [`ElementAttributeNameCase`](#elementattributenamecase) 23 | * [`EvaluateExpression`](#evaluateexpression) 24 | * [`EvaluateProgram`](#evaluateprogram) 25 | * [`Evaluater`](#evaluater) 26 | * [`ExtraProps`](#extraprops) 27 | * [`Fragment`](#fragment) 28 | * [`Jsx`](#jsx) 29 | * [`JsxDev`](#jsxdev) 30 | * [`Options`](#options) 31 | * [`Props`](#props) 32 | * [`Source`](#source) 33 | * [`Space`](#space) 34 | * [`StylePropertyNameCase`](#stylepropertynamecase) 35 | * [Errors](#errors) 36 | * [Examples](#examples) 37 | * [Example: Preact](#example-preact) 38 | * [Example: Solid](#example-solid) 39 | * [Example: Svelte](#example-svelte) 40 | * [Example: Vue](#example-vue) 41 | * [Syntax](#syntax) 42 | * [Compatibility](#compatibility) 43 | * [Security](#security) 44 | * [Related](#related) 45 | * [Contribute](#contribute) 46 | * [License](#license) 47 | 48 | ## What is this? 49 | 50 | This package is a utility that takes a [hast][github-hast] tree and an 51 | [automatic JSX runtime][reactjs-jsx-runtime] and turns the tree into anything 52 | you wish. 53 | 54 | ## When should I use this? 55 | 56 | You can use this package when you have a hast syntax tree and want to use it 57 | with whatever framework. 58 | 59 | This package uses an automatic JSX runtime, 60 | which is a sort of lingua franca for frameworks to support JSX. 61 | 62 | Notably, 63 | automatic runtimes have support for passing extra information in development, 64 | and have guaranteed support for fragments. 65 | 66 | ## Install 67 | 68 | This package is [ESM only][github-gist-esm]. 69 | In Node.js (version 16+), 70 | install with [npm][npmjs-install]: 71 | 72 | ```sh 73 | npm install hast-util-to-jsx-runtime 74 | ``` 75 | 76 | In Deno with [`esm.sh`][esmsh]: 77 | 78 | ```js 79 | import {toJsxRuntime} from 'https://esm.sh/hast-util-to-jsx-runtime@2' 80 | ``` 81 | 82 | In browsers with [`esm.sh`][esmsh]: 83 | 84 | ```html 85 | 88 | ``` 89 | 90 | ## Use 91 | 92 | ```js 93 | import {h} from 'hastscript' 94 | import {toJsxRuntime} from 'hast-util-to-jsx-runtime' 95 | import {Fragment, jsxs, jsx} from 'react/jsx-runtime' 96 | import {renderToStaticMarkup} from 'react-dom/server' 97 | 98 | const tree = h('h1', 'Hello, world!') 99 | 100 | const doc = renderToStaticMarkup(toJsxRuntime(tree, {Fragment, jsxs, jsx})) 101 | 102 | console.log(doc) 103 | ``` 104 | 105 | Yields: 106 | 107 | ```html 108 |

    Hello, world!

    109 | ``` 110 | 111 | > **Note**: 112 | > to add better type support, 113 | > register a global JSX namespace: 114 | > 115 | > ```ts 116 | > import type {JSX as Jsx} from 'react/jsx-runtime' 117 | > 118 | > declare global { 119 | > namespace JSX { 120 | > type ElementClass = Jsx.ElementClass 121 | > type Element = Jsx.Element 122 | > type IntrinsicElements = Jsx.IntrinsicElements 123 | > } 124 | > } 125 | > ``` 126 | 127 | ## API 128 | 129 | This package exports the identifier [`toJsxRuntime`][api-to-jsx-runtime]. 130 | It exports the [TypeScript][] types 131 | [`Components`][api-components], 132 | [`CreateEvaluater`][api-create-evaluater], 133 | [`ElementAttributeNameCase`][api-element-attribute-name-case], 134 | [`EvaluateExpression`][api-evaluate-expression], 135 | [`EvaluateProgram`][api-evaluate-program], 136 | [`Evaluater`][api-evaluater], 137 | [`ExtraProps`][api-extra-props], 138 | [`Fragment`][api-fragment], 139 | [`Jsx`][api-jsx], 140 | [`JsxDev`][api-jsx-dev], 141 | [`Options`][api-options], 142 | [`Props`][api-props], 143 | [`Source`][api-source], 144 | [`Space`][api-Space], 145 | and 146 | [`StylePropertyNameCase`][api-style-property-name-case]. 147 | There is no default export. 148 | 149 | ### `toJsxRuntime(tree, options)` 150 | 151 | Transform a hast tree to 152 | preact, react, solid, svelte, vue, etcetera, 153 | with an automatic JSX runtime. 154 | 155 | ##### Parameters 156 | 157 | * `tree` 158 | ([`Node`][github-hast-nodes]) 159 | — tree to transform 160 | * `options` 161 | ([`Options`][api-options], required) 162 | — configuration 163 | 164 | ##### Returns 165 | 166 | Result from your configured JSX runtime 167 | (`JSX.Element` if defined, 168 | otherwise `unknown` which you can cast yourself). 169 | 170 | ### `Components` 171 | 172 | Possible components to use (TypeScript type). 173 | 174 | Each key is a tag name typed in `JSX.IntrinsicElements`, 175 | if defined. 176 | Each value is either a different tag name 177 | or a component accepting the corresponding props 178 | (and an optional `node` prop if `passNode` is on). 179 | 180 | You can access props at `JSX.IntrinsicElements`. 181 | For example, 182 | to find props for `a`, 183 | use `JSX.IntrinsicElements['a']`. 184 | 185 | ###### Type 186 | 187 | ```ts 188 | import type {Element} from 'hast' 189 | 190 | type ExtraProps = {node?: Element | undefined} 191 | 192 | type Components = { 193 | [TagName in keyof JSX.IntrinsicElements]: 194 | | Component 195 | | keyof JSX.IntrinsicElements 196 | } 197 | 198 | type Component = 199 | // Class component: 200 | | (new (props: ComponentProps) => JSX.ElementClass) 201 | // Function component: 202 | | ((props: ComponentProps) => JSX.Element | string | null | undefined) 203 | ``` 204 | 205 | ### `CreateEvaluater` 206 | 207 | Create an evaluator that turns ESTree ASTs from embedded MDX into values 208 | (TypeScript type). 209 | 210 | ###### Parameters 211 | 212 | There are no parameters. 213 | 214 | ###### Returns 215 | 216 | Evaluater ([`Evaluater`][api-evaluater]). 217 | 218 | ### `ElementAttributeNameCase` 219 | 220 | Casing to use for attribute names (TypeScript type). 221 | 222 | HTML casing is for example 223 | `class`, `stroke-linecap`, `xml:lang`. 224 | React casing is for example 225 | `className`, `strokeLinecap`, `xmlLang`. 226 | 227 | ###### Type 228 | 229 | ```ts 230 | type ElementAttributeNameCase = 'html' | 'react' 231 | ``` 232 | 233 | ### `EvaluateExpression` 234 | 235 | Turn an MDX expression into a value (TypeScript type). 236 | 237 | ###### Parameters 238 | 239 | * `expression` (`Expression` from `@types/estree`) 240 | — estree expression 241 | 242 | ###### Returns 243 | 244 | Result of expression (`unknown`). 245 | 246 | ### `EvaluateProgram` 247 | 248 | Turn an MDX program (export/import statements) into a value (TypeScript type). 249 | 250 | ###### Parameters 251 | 252 | * `program` (`Program` from `@types/estree`) 253 | — estree program 254 | 255 | ###### Returns 256 | 257 | Result of program (`unknown`); 258 | should likely be `undefined` as ESM changes the scope but doesn’t yield 259 | something. 260 | 261 | ### `Evaluater` 262 | 263 | Evaluator that turns ESTree ASTs from embedded MDX into values (TypeScript 264 | type). 265 | 266 | ###### Fields 267 | 268 | * `evaluateExpression` ([`EvaluateExpression`][api-evaluate-expression]) 269 | — evaluate an expression 270 | * `evaluateProgram` ([`EvaluateProgram`][api-evaluate-program]) 271 | — evaluate a program 272 | 273 | ### `ExtraProps` 274 | 275 | Extra fields we pass (TypeScript type). 276 | 277 | ###### Type 278 | 279 | ```ts 280 | type ExtraProps = {node?: Element | undefined} 281 | ``` 282 | 283 | ### `Fragment` 284 | 285 | Represent the children, 286 | typically a symbol (TypeScript type). 287 | 288 | ###### Type 289 | 290 | ```ts 291 | type Fragment = unknown 292 | ``` 293 | 294 | ### `Jsx` 295 | 296 | Create a production element (TypeScript type). 297 | 298 | ###### Parameters 299 | 300 | * `type` (`unknown`) 301 | — element type: 302 | `Fragment` symbol, 303 | tag name (`string`), 304 | component 305 | * `props` ([`Props`][api-props]) 306 | — element props, 307 | `children`, 308 | and maybe `node` 309 | * `key` (`string` or `undefined`) 310 | — dynamicly generated key to use 311 | 312 | ###### Returns 313 | 314 | Element from your framework 315 | (`JSX.Element` if defined, 316 | otherwise `unknown` which you can cast yourself). 317 | 318 | ### `JsxDev` 319 | 320 | Create a development element (TypeScript type). 321 | 322 | ###### Parameters 323 | 324 | * `type` (`unknown`) 325 | — element type: 326 | `Fragment` symbol, 327 | tag name (`string`), 328 | component 329 | * `props` ([`Props`][api-props]) 330 | — element props, 331 | `children`, 332 | and maybe `node` 333 | * `key` (`string` or `undefined`) 334 | — dynamicly generated key to use 335 | * `isStaticChildren` (`boolean`) 336 | — whether two or more children are passed (in an array), 337 | which is whether `jsxs` or `jsx` would be used 338 | * `source` ([`Source`][api-source]) 339 | — info about source 340 | * `self` (`undefined`) 341 | — nothing (this is used by frameworks that have components, 342 | we don’t) 343 | 344 | ###### Returns 345 | 346 | Element from your framework 347 | (`JSX.Element` if defined, 348 | otherwise `unknown` which you can cast yourself). 349 | 350 | ### `Options` 351 | 352 | Configuration (TypeScript type). 353 | 354 | ###### Fields 355 | 356 | * `Fragment` ([`Fragment`][api-fragment], required) 357 | — fragment 358 | * `jsxDEV` ([`JsxDev`][api-jsx-dev], required in development) 359 | — development JSX 360 | * `jsxs` ([`Jsx`][api-jsx], required in production) 361 | — static JSX 362 | * `jsx` ([`Jsx`][api-jsx], required in production) 363 | — dynamic JSX 364 | * `components` ([`Partial`][api-components], optional) 365 | — components to use 366 | * `createEvaluater` ([`CreateEvaluater`][api-create-evaluater], optional) 367 | — create an evaluator that turns ESTree ASTs into values 368 | * `development` (`boolean`, default: `false`) 369 | — whether to use `jsxDEV` when on or `jsx` and `jsxs` when off 370 | * `elementAttributeNameCase` 371 | ([`ElementAttributeNameCase`][api-element-attribute-name-case], 372 | default: `'react'`) 373 | — specify casing to use for attribute names 374 | * `filePath` (`string`, optional) 375 | — file path to the original source file, 376 | passed in source info to `jsxDEV` when using the automatic runtime with 377 | `development: true` 378 | * `passNode` (`boolean`, default: `false`) 379 | — pass the hast element node to components 380 | * `space` ([`Space`][api-space], default: `'html'`) 381 | — whether `tree` is in the `'html'` or `'svg'` space, when an `` 382 | element is found in the HTML space, 383 | this package already automatically switches to and from the SVG space when 384 | entering and exiting it 385 | * `stylePropertyNameCase` 386 | ([`StylePropertyNameCase`][api-style-property-name-case], 387 | default: `'dom'`) 388 | — specify casing to use for property names in `style` objects 389 | * `tableCellAlignToStyle` 390 | (`boolean`, default: `true`) 391 | — turn obsolete `align` props on `td` and `th` into CSS `style` props 392 | 393 | ### `Props` 394 | 395 | Properties and children (TypeScript type). 396 | 397 | ###### Type 398 | 399 | ```ts 400 | import type {Element} from 'hast' 401 | 402 | type Props = { 403 | [prop: string]: 404 | | Array // For `children`. 405 | | Record // For `style`. 406 | | Element // For `node`. 407 | | boolean 408 | | number 409 | | string 410 | | undefined 411 | children: Array | undefined 412 | node?: Element | undefined 413 | } 414 | ``` 415 | 416 | ### `Source` 417 | 418 | Info about source (TypeScript type). 419 | 420 | ###### Fields 421 | 422 | * `columnNumber` (`number` or `undefined`) 423 | — column where thing starts (0-indexed) 424 | * `fileName` (`string` or `undefined`) 425 | — name of source file 426 | * `lineNumber` (`number` or `undefined`) 427 | — line where thing starts (1-indexed) 428 | 429 | ### `Space` 430 | 431 | Namespace (TypeScript type). 432 | 433 | > 👉 **Note**: 434 | > hast is not XML; 435 | > it supports SVG as embedded in HTML; 436 | > it does not support the features available in XML; 437 | > passing SVG might break but fragments of modern SVG should be fine; 438 | > use `xast` if you need to support SVG as XML. 439 | 440 | ###### Type 441 | 442 | ```ts 443 | type Space = 'html' | 'svg' 444 | ``` 445 | 446 | ### `StylePropertyNameCase` 447 | 448 | Casing to use for property names in `style` objects (TypeScript type). 449 | 450 | CSS casing is for example `background-color` and `-webkit-line-clamp`. 451 | DOM casing is for example `backgroundColor` and `WebkitLineClamp`. 452 | 453 | ###### Type 454 | 455 | ```ts 456 | type StylePropertyNameCase = 'css' | 'dom' 457 | ``` 458 | 459 | ## Errors 460 | 461 | The following errors are thrown: 462 | 463 | ###### ``Expected `Fragment` in options`` 464 | 465 | This error is thrown when either `options` is not passed at all or 466 | when `options.Fragment` is `undefined`. 467 | 468 | The automatic JSX runtime needs a symbol for a fragment to work. 469 | 470 | To solve the error, 471 | make sure you are passing the correct fragment symbol from your framework. 472 | 473 | ###### `` Expected `jsxDEV` in options when `development: true` `` 474 | 475 | This error is thrown when `options.development` is turned on (`true`), 476 | but when `options.jsxDEV` is not a function. 477 | 478 | The automatic JSX runtime, 479 | in development, 480 | needs this function. 481 | 482 | To solve the error, 483 | make sure you are importing the correct runtime functions 484 | (for example, `'react/jsx-dev-runtime'`), 485 | and pass `jsxDEV`. 486 | 487 | ###### ``Expected `jsx` in production options`` 488 | 489 | ###### ``Expected `jsxs` in production options`` 490 | 491 | These errors are thrown when `options.development` is *not* turned on 492 | (`false` or not defined), 493 | and when `options.jsx` or `options.jsxs` are not functions. 494 | 495 | The automatic JSX runtime, 496 | in production, 497 | needs these functions. 498 | 499 | To solve the error, 500 | make sure you are importing the correct runtime functions 501 | (for example, `'react/jsx-runtime'`), 502 | and pass `jsx` and `jsxs`. 503 | 504 | ###### `` Cannot handle MDX estrees without `createEvaluater` `` 505 | 506 | This error is thrown when MDX nodes are passed that represent JavaScript 507 | programs or expressions. 508 | 509 | Supporting JavaScript can be unsafe and requires a different project. 510 | To support JavaScript, 511 | pass a `createEvaluater` function in `options`. 512 | 513 | ###### ``Cannot parse `style` attribute`` 514 | 515 | This error is thrown when a `style` attribute is found on an element, 516 | which cannot be parsed as CSS. 517 | 518 | Most frameworks don’t accept `style` as a string, 519 | so we need to parse it as CSS, 520 | and pass it as an object. 521 | But when broken CSS is used, 522 | such as `style="color:red; /*"`, 523 | we crash. 524 | 525 | To solve the error, 526 | make sure authors write valid CSS. 527 | Alternatively, 528 | pass `options.ignoreInvalidStyle: true` to swallow these errors. 529 | 530 | ## Examples 531 | 532 | ### Example: Preact 533 | 534 | > 👉 **Note**: 535 | > you must set `elementAttributeNameCase: 'html'` for preact. 536 | 537 | In Node.js, 538 | do: 539 | 540 | ```js 541 | import {h} from 'hastscript' 542 | import {toJsxRuntime} from 'hast-util-to-jsx-runtime' 543 | import {Fragment, jsx, jsxs} from 'preact/jsx-runtime' 544 | import {render} from 'preact-render-to-string' 545 | 546 | const result = render( 547 | toJsxRuntime(h('h1', 'hi!'), { 548 | Fragment, 549 | jsx, 550 | jsxs, 551 | elementAttributeNameCase: 'html' 552 | }) 553 | ) 554 | 555 | console.log(result) 556 | ``` 557 | 558 | Yields: 559 | 560 | ```html 561 |

    hi!

    562 | ``` 563 | 564 | In a browser, 565 | do: 566 | 567 | ```js 568 | import {h} from 'https://esm.sh/hastscript@9' 569 | import {toJsxRuntime} from 'https://esm.sh/hast-util-to-jsx-runtime@2' 570 | import {Fragment, jsx, jsxs} from 'https://esm.sh/preact@10/jsx-runtime' 571 | import {render} from 'https://esm.sh/preact@10' 572 | 573 | render( 574 | toJsxRuntime(h('h1', 'hi!'), { 575 | Fragment, 576 | jsx, 577 | jsxs, 578 | elementAttributeNameCase: 'html' 579 | }), 580 | document.getElementById('root') 581 | ) 582 | ``` 583 | 584 | To add better type support, 585 | register a global JSX namespace: 586 | 587 | ```ts 588 | import type {JSX as Jsx} from 'preact/jsx-runtime' 589 | 590 | declare global { 591 | namespace JSX { 592 | type ElementClass = Jsx.ElementClass 593 | type Element = Jsx.Element 594 | type IntrinsicElements = Jsx.IntrinsicElements 595 | } 596 | } 597 | ``` 598 | 599 | ### Example: Solid 600 | 601 | > 👉 **Note**: 602 | > you must set `elementAttributeNameCase: 'html'` and 603 | > `stylePropertyNameCase: 'css'` for Solid. 604 | 605 | In Node.js, 606 | do: 607 | 608 | ```js 609 | import {h} from 'hastscript' 610 | import {toJsxRuntime} from 'hast-util-to-jsx-runtime' 611 | import {Fragment, jsx, jsxs} from 'solid-jsx/jsx-runtime' 612 | 613 | console.log( 614 | toJsxRuntime(h('h1', 'hi!'), { 615 | Fragment, 616 | jsx, 617 | jsxs, 618 | elementAttributeNameCase: 'html', 619 | stylePropertyNameCase: 'css' 620 | }).t 621 | ) 622 | ``` 623 | 624 | Yields: 625 | 626 | ```html 627 |

    hi!

    628 | ``` 629 | 630 | In a browser, 631 | do: 632 | 633 | ```js 634 | import {h} from 'https://esm.sh/hastscript@9' 635 | import {toJsxRuntime} from 'https://esm.sh/hast-util-to-jsx-runtime@2' 636 | import {Fragment, jsx, jsxs} from 'https://esm.sh/solid-js@1/h/jsx-runtime' 637 | import {render} from 'https://esm.sh/solid-js@1/web' 638 | 639 | render(Component, document.getElementById('root')) 640 | 641 | function Component() { 642 | return toJsxRuntime(h('h1', 'hi!'), { 643 | Fragment, 644 | jsx, 645 | jsxs, 646 | elementAttributeNameCase: 'html', 647 | stylePropertyNameCase: 'css' 648 | }) 649 | } 650 | ``` 651 | 652 | To add better type support, 653 | register a global JSX namespace: 654 | 655 | ```ts 656 | import type {JSX as Jsx} from 'solid-js/jsx-runtime' 657 | 658 | declare global { 659 | namespace JSX { 660 | type ElementClass = Jsx.ElementClass 661 | type Element = Jsx.Element 662 | type IntrinsicElements = Jsx.IntrinsicElements 663 | } 664 | } 665 | ``` 666 | 667 | ### Example: Svelte 668 | 669 | 670 | 671 | I have no clue how to render a Svelte component in Node, 672 | but you can get that component with: 673 | 674 | ```js 675 | import {h} from 'hastscript' 676 | import {toJsxRuntime} from 'hast-util-to-jsx-runtime' 677 | import {Fragment, jsx, jsxs} from 'svelte-jsx' 678 | 679 | const svelteComponent = toJsxRuntime(h('h1', 'hi!'), {Fragment, jsx, jsxs}) 680 | 681 | console.log(svelteComponent) 682 | ``` 683 | 684 | Yields: 685 | 686 | ```text 687 | [class Component extends SvelteComponent] 688 | ``` 689 | 690 | Types for Svelte are broken. 691 | Raise it with Svelte. 692 | 693 | ### Example: Vue 694 | 695 | > 👉 **Note**: 696 | > you must set `elementAttributeNameCase: 'html'` for Vue. 697 | 698 | In Node.js, 699 | do: 700 | 701 | ```js 702 | import serverRenderer from '@vue/server-renderer' 703 | import {h} from 'hastscript' 704 | import {toJsxRuntime} from 'hast-util-to-jsx-runtime' 705 | import {Fragment, jsx, jsxs} from 'vue/jsx-runtime' // Available since `vue@3.3`. 706 | 707 | console.log( 708 | await serverRenderer.renderToString( 709 | toJsxRuntime(h('h1', 'hi!'), { 710 | Fragment, 711 | jsx, 712 | jsxs, 713 | elementAttributeNameCase: 'html' 714 | }) 715 | ) 716 | ) 717 | ``` 718 | 719 | Yields: 720 | 721 | ```html 722 |

    hi!

    723 | ``` 724 | 725 | In a browser, 726 | do: 727 | 728 | ```js 729 | import {h} from 'https://esm.sh/hastscript@9' 730 | import {toJsxRuntime} from 'https://esm.sh/hast-util-to-jsx-runtime@2' 731 | import {createApp} from 'https://esm.sh/vue@3' 732 | import {Fragment, jsx, jsxs} from 'https://esm.sh/vue@3/jsx-runtime' 733 | 734 | createApp(Component).mount('#root') 735 | 736 | function Component() { 737 | return toJsxRuntime(h('h1', 'hi!'), { 738 | Fragment, 739 | jsx, 740 | jsxs, 741 | elementAttributeNameCase: 'html' 742 | }) 743 | } 744 | ``` 745 | 746 | To add better type support, 747 | register a global JSX namespace: 748 | 749 | ```ts 750 | import type {JSX as Jsx} from 'vue/jsx-runtime' 751 | 752 | declare global { 753 | namespace JSX { 754 | type ElementClass = Jsx.ElementClass 755 | type Element = Jsx.Element 756 | type IntrinsicElements = Jsx.IntrinsicElements 757 | } 758 | } 759 | ``` 760 | 761 | ## Syntax 762 | 763 | HTML is parsed according to WHATWG HTML (the living standard), 764 | which is also followed by browsers such as Chrome, 765 | Firefox, 766 | and Safari. 767 | 768 | ## Compatibility 769 | 770 | Projects maintained by the unified collective are compatible with maintained 771 | versions of Node.js. 772 | 773 | When we cut a new major release, 774 | we drop support for unmaintained versions of Node. 775 | This means we try to keep the current release line, 776 | `hast-util-to-jsx-runtime@2`, 777 | compatible with Node.js 16. 778 | 779 | ## Security 780 | 781 | Be careful with user input in your hast tree. 782 | Use [`hast-util-santize`][github-hast-util-sanitize] to make hast trees safe. 783 | 784 | ## Related 785 | 786 | * [`hastscript`](https://github.com/syntax-tree/hastscript) 787 | — build hast trees 788 | * [`hast-util-to-html`](https://github.com/syntax-tree/hast-util-to-html) 789 | — serialize hast as HTML 790 | * [`hast-util-sanitize`][github-hast-util-sanitize] 791 | — sanitize hast 792 | 793 | ## Contribute 794 | 795 | See [`contributing.md`][health-contributing] 796 | in 797 | [`syntax-tree/.github`][health] 798 | for ways to get started. 799 | See [`support.md`][health-support] for ways to get help. 800 | 801 | This project has a [code of conduct][health-coc]. 802 | By interacting with this repository, 803 | organization, 804 | or community you agree to abide by its terms. 805 | 806 | ## License 807 | 808 | [MIT][file-license] © [Titus Wormer][wooorm] 809 | 810 | 811 | 812 | [api-components]: #components 813 | 814 | [api-create-evaluater]: #createevaluater 815 | 816 | [api-element-attribute-name-case]: #elementattributenamecase 817 | 818 | [api-evaluate-expression]: #evaluateexpression 819 | 820 | [api-evaluate-program]: #evaluateprogram 821 | 822 | [api-evaluater]: #evaluater 823 | 824 | [api-extra-props]: #extraprops 825 | 826 | [api-fragment]: #fragment 827 | 828 | [api-jsx]: #jsx 829 | 830 | [api-jsx-dev]: #jsxdev 831 | 832 | [api-options]: #options 833 | 834 | [api-props]: #props 835 | 836 | [api-source]: #source 837 | 838 | [api-space]: #space 839 | 840 | [api-style-property-name-case]: #stylepropertynamecase 841 | 842 | [api-to-jsx-runtime]: #tojsxruntimetree-options 843 | 844 | [badge-build-image]: https://github.com/syntax-tree/hast-util-to-jsx-runtime/workflows/main/badge.svg 845 | 846 | [badge-build-url]: https://github.com/syntax-tree/hast-util-to-jsx-runtime/actions 847 | 848 | [badge-coverage-image]: https://img.shields.io/codecov/c/github/syntax-tree/hast-util-to-jsx-runtime.svg 849 | 850 | [badge-coverage-url]: https://codecov.io/github/syntax-tree/hast-util-to-jsx-runtime 851 | 852 | [badge-downloads-image]: https://img.shields.io/npm/dm/hast-util-to-jsx-runtime.svg 853 | 854 | [badge-downloads-url]: https://www.npmjs.com/package/hast-util-to-jsx-runtime 855 | 856 | [badge-size-image]: https://img.shields.io/bundlejs/size/hast-util-to-jsx-runtime 857 | 858 | [badge-size-url]: https://bundlejs.com/?q=hast-util-to-jsx-runtime 859 | 860 | [esmsh]: https://esm.sh 861 | 862 | [file-license]: license 863 | 864 | [github-gist-esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 865 | 866 | [github-hast]: https://github.com/syntax-tree/hast 867 | 868 | [github-hast-nodes]: https://github.com/syntax-tree/hast#nodes 869 | 870 | [github-hast-util-sanitize]: https://github.com/syntax-tree/hast-util-sanitize 871 | 872 | [health]: https://github.com/syntax-tree/.github 873 | 874 | [health-coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md 875 | 876 | [health-contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md 877 | 878 | [health-support]: https://github.com/syntax-tree/.github/blob/main/support.md 879 | 880 | [npmjs-install]: https://docs.npmjs.com/cli/install 881 | 882 | [reactjs-jsx-runtime]: https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html 883 | 884 | [typescript]: https://www.typescriptlang.org 885 | 886 | [wooorm]: https://wooorm.com 887 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {Program} from 'estree' 3 | * @import {CreateEvaluater, ExtraProps, Source} from 'hast-util-to-jsx-runtime' 4 | */ 5 | 6 | import assert from 'node:assert/strict' 7 | import test from 'node:test' 8 | import {visit} from 'estree-util-visit' 9 | import {h, s} from 'hastscript' 10 | import {toJsxRuntime} from 'hast-util-to-jsx-runtime' 11 | import * as sval from 'sval' 12 | import {renderToStaticMarkup} from 'react-dom/server' 13 | import * as development from 'react/jsx-dev-runtime' 14 | import * as production from 'react/jsx-runtime' 15 | import React from 'react' 16 | 17 | /** @type {import('sval')['default']} */ 18 | // @ts-expect-error: ESM types are wrong. 19 | const Sval = sval.default 20 | 21 | test('core', async function (t) { 22 | await t.test('should expose the public api', async function () { 23 | assert.deepEqual( 24 | Object.keys(await import('hast-util-to-jsx-runtime')).sort(), 25 | ['toJsxRuntime'] 26 | ) 27 | }) 28 | 29 | await t.test( 30 | 'should support a production runtime (default)', 31 | async function () { 32 | assert.equal( 33 | renderToStaticMarkup(toJsxRuntime(h('a', {b: 'c'}, 'd'), production)), 34 | 'd' 35 | ) 36 | } 37 | ) 38 | 39 | await t.test('should support a development runtime', async function () { 40 | assert.equal( 41 | renderToStaticMarkup( 42 | toJsxRuntime(h('a', {b: 'c'}, 'd'), { 43 | ...development, 44 | development: true 45 | }) 46 | ), 47 | 'd' 48 | ) 49 | }) 50 | 51 | await t.test('should throw w/o `Fragment`', async function () { 52 | assert.throws(function () { 53 | // @ts-expect-error: check how the runtime handles no options 54 | toJsxRuntime(h()) 55 | }, /Expected `Fragment` in options/) 56 | }) 57 | 58 | await t.test('should throw w/ `Fragment`, w/o `jsx`', async function () { 59 | assert.throws(function () { 60 | // @ts-expect-error: check how the runtime handles `jsx`, `jsxs` missing. 61 | toJsxRuntime(h(), {Fragment: production.Fragment}) 62 | }, /Expected `jsx` in production options/) 63 | }) 64 | 65 | await t.test( 66 | 'should throw w/ `Fragment`, `jsx`, w/o `jsxs`', 67 | async function () { 68 | assert.throws(function () { 69 | // @ts-expect-error: check how the runtime handles `jsxs` missing. 70 | toJsxRuntime(h(), {Fragment: production.Fragment, jsx: production.jsx}) 71 | }, /Expected `jsxs` in production options/) 72 | } 73 | ) 74 | 75 | await t.test( 76 | 'should throw in development w/ `Fragment`, w/o `jsxDEV`', 77 | async function () { 78 | assert.throws(function () { 79 | toJsxRuntime(h(), {Fragment: production.Fragment, development: true}) 80 | }, /Expected `jsxDEV` in options when `development: true`/) 81 | } 82 | ) 83 | 84 | await t.test('should support a root (1)', async function () { 85 | const node = toJsxRuntime(h(null, 'd'), production) 86 | 87 | assert.equal(node.type, production.Fragment) 88 | }) 89 | 90 | await t.test('should support a root (2)', async function () { 91 | assert.equal( 92 | renderToStaticMarkup(toJsxRuntime(h(null, 'a'), production)), 93 | 'a' 94 | ) 95 | }) 96 | 97 | await t.test('should support a text (1)', async function () { 98 | const node = toJsxRuntime({type: 'text', value: 'a'}, production) 99 | 100 | assert.equal(node.type, production.Fragment) 101 | }) 102 | 103 | await t.test('should support a text (2)', async function () { 104 | assert.equal( 105 | renderToStaticMarkup( 106 | toJsxRuntime({type: 'text', value: 'a'}, production) 107 | ), 108 | 'a' 109 | ) 110 | }) 111 | 112 | await t.test('should support a doctype (1)', async function () { 113 | const node = toJsxRuntime({type: 'doctype'}, production) 114 | 115 | assert.equal(node.type, production.Fragment) 116 | }) 117 | 118 | await t.test('should support a doctype (2)', async function () { 119 | assert.equal( 120 | renderToStaticMarkup(toJsxRuntime({type: 'doctype'}, production)), 121 | '' 122 | ) 123 | }) 124 | 125 | await t.test('should support a comment (1)', async function () { 126 | const node = toJsxRuntime({type: 'comment', value: 'a'}, production) 127 | 128 | assert.equal(node.type, production.Fragment) 129 | }) 130 | 131 | await t.test('should support a comment (2)', async function () { 132 | assert.equal( 133 | renderToStaticMarkup( 134 | toJsxRuntime({type: 'comment', value: 'a'}, production) 135 | ), 136 | '' 137 | ) 138 | }) 139 | }) 140 | 141 | test('properties', async function (t) { 142 | await t.test('should support a bunch of properties', async function () { 143 | assert.equal( 144 | renderToStaticMarkup( 145 | toJsxRuntime( 146 | { 147 | type: 'element', 148 | tagName: 'div', 149 | properties: { 150 | id: 'a', 151 | title: 'b', 152 | className: ['c'], 153 | // Known comma-separated lists: 154 | accept: ['.jpg', '.jpeg'], 155 | ariaValueNow: 1, 156 | dataFoo: true, 157 | data123: true, 158 | dataFooBar: true, 159 | allowFullScreen: true, 160 | download: true, 161 | dataA: false, 162 | dataB: undefined, 163 | dataC: null 164 | }, 165 | children: [] 166 | }, 167 | production 168 | ) 169 | ), 170 | '
    ' 171 | ) 172 | }) 173 | 174 | await t.test('should support `style`', async function () { 175 | assert.equal( 176 | renderToStaticMarkup( 177 | toJsxRuntime( 178 | h('div', { 179 | style: 180 | 'color: /* ? */ red; background-color: blue; -webkit-border-radius: 3px; -moz-transition: initial; -ms-transition: unset' 181 | }), 182 | production 183 | ) 184 | ), 185 | '
    ' 186 | ) 187 | }) 188 | 189 | await t.test('should support `style` as an object', async function () { 190 | assert.equal( 191 | renderToStaticMarkup( 192 | toJsxRuntime( 193 | { 194 | type: 'element', 195 | tagName: 'div', 196 | // @ts-expect-error: check how the runtime handles `style` as an object. 197 | properties: {style: {color: 'red'}}, 198 | children: [] 199 | }, 200 | production 201 | ) 202 | ), 203 | '
    ' 204 | ) 205 | }) 206 | 207 | await t.test('should support vendor prefixes', async function () { 208 | assert.equal( 209 | renderToStaticMarkup( 210 | toJsxRuntime( 211 | h('div', {style: '-webkit-transform: rotate(0.01turn)'}), 212 | production 213 | ) 214 | ), 215 | '
    ' 216 | ) 217 | }) 218 | 219 | await t.test('should support CSS variables', async function () { 220 | assert.equal( 221 | renderToStaticMarkup( 222 | toJsxRuntime( 223 | h('div', {style: '--fg: #0366d6; color: var(--fg)'}), 224 | production 225 | ) 226 | ), 227 | '
    ' 228 | ) 229 | }) 230 | 231 | await t.test('should support CSS cased style objects', async function () { 232 | /** @type {unknown} */ 233 | let foundProps 234 | 235 | assert.equal( 236 | renderToStaticMarkup( 237 | toJsxRuntime( 238 | h('div', { 239 | style: 240 | '-webkit-transform:rotate(0.01turn); --fg: #0366d6; color: var(--fg); -ms-transition: unset' 241 | }), 242 | { 243 | ...production, 244 | /** 245 | * @param {React.ElementType} type 246 | */ 247 | jsx(type, props) { 248 | foundProps = props 249 | return production.jsx(type, {}) 250 | }, 251 | stylePropertyNameCase: 'css' 252 | } 253 | ) 254 | ), 255 | '
    ' 256 | ) 257 | 258 | assert.deepEqual(foundProps, { 259 | style: { 260 | '-webkit-transform': 'rotate(0.01turn)', 261 | '--fg': '#0366d6', 262 | color: 'var(--fg)', 263 | '-ms-transition': 'unset' 264 | } 265 | }) 266 | }) 267 | 268 | await t.test( 269 | 'should crash on invalid style strings (default)', 270 | async function () { 271 | assert.throws(function () { 272 | toJsxRuntime(h('div', {style: 'color:red; /*'}), production) 273 | }, /Cannot parse `style` attribute/) 274 | } 275 | ) 276 | 277 | await t.test( 278 | 'should crash on invalid style strings (w/o file path, w/ positional info)', 279 | async function () { 280 | assert.throws( 281 | function () { 282 | toJsxRuntime( 283 | { 284 | type: 'element', 285 | tagName: 'a', 286 | properties: {style: 'color:red; /*'}, 287 | children: [], 288 | position: { 289 | start: {line: 3, column: 2}, 290 | end: {line: 3, column: 123} 291 | } 292 | }, 293 | production 294 | ) 295 | }, 296 | /^3:2-3:123: Cannot parse `style` attribute/, 297 | 'Cannot parse style attribute: End of comment missing' 298 | ) 299 | } 300 | ) 301 | 302 | await t.test( 303 | 'should crash on invalid style strings (w/ file path, w/o positional info)', 304 | async function () { 305 | assert.throws(function () { 306 | toJsxRuntime( 307 | { 308 | type: 'element', 309 | tagName: 'a', 310 | properties: {style: 'color:red; /*'}, 311 | children: [] 312 | }, 313 | {...production, filePath: 'example.html'} 314 | ) 315 | }, /^1:1: Cannot parse `style` attribut/) 316 | } 317 | ) 318 | 319 | await t.test( 320 | 'should crash on invalid style strings (w/ file path, w/ positional info)', 321 | async function () { 322 | assert.throws(function () { 323 | toJsxRuntime( 324 | { 325 | type: 'element', 326 | tagName: 'a', 327 | properties: {style: 'color:red; /*'}, 328 | children: [], 329 | position: {start: {line: 3, column: 2}, end: {line: 3, column: 123}} 330 | }, 331 | {...production, filePath: 'example.html'} 332 | ) 333 | }, /^3:2-3:123: Cannot parse `style` attribute/) 334 | } 335 | ) 336 | 337 | await t.test( 338 | 'should not crash on invalid style strings w/ `ignoreInvalidStyle`', 339 | async function () { 340 | assert.equal( 341 | renderToStaticMarkup( 342 | toJsxRuntime(h('div', {style: 'color:red; /*'}), { 343 | ...production, 344 | ignoreInvalidStyle: true 345 | }) 346 | ), 347 | '
    ' 348 | ) 349 | } 350 | ) 351 | 352 | await t.test('should support properties in the SVG space', async function () { 353 | assert.equal( 354 | renderToStaticMarkup( 355 | toJsxRuntime( 356 | s('g', { 357 | colorInterpolationFilters: 'sRGB', 358 | xmlSpace: 'preserve', 359 | xmlnsXLink: 'http://www.w3.org/1999/xlink', 360 | xLinkArcRole: 'http://www.example.com' 361 | }), 362 | {...production, space: 'svg'} 363 | ) 364 | ), 365 | '' 366 | ) 367 | }) 368 | }) 369 | 370 | test('children', async function (t) { 371 | await t.test('should support no children', async function () { 372 | assert.equal( 373 | renderToStaticMarkup(toJsxRuntime(h('a'), production)), 374 | '' 375 | ) 376 | }) 377 | 378 | await t.test('should support a child', async function () { 379 | assert.equal( 380 | renderToStaticMarkup(toJsxRuntime(h('a', [h('b')]), production)), 381 | '' 382 | ) 383 | }) 384 | 385 | await t.test('should support two children', async function () { 386 | assert.equal( 387 | renderToStaticMarkup(toJsxRuntime(h('a', [h('b'), h('c')]), production)), 388 | '' 389 | ) 390 | }) 391 | 392 | await t.test('should support svg in html', async function () { 393 | assert.equal( 394 | renderToStaticMarkup( 395 | toJsxRuntime( 396 | h('.article', [ 397 | s( 398 | 'svg', 399 | { 400 | xmlns: 'http://www.w3.org/2000/svg', 401 | viewBox: [0, 0, 500, 500] 402 | }, 403 | [s('circle', {cx: 120, cy: 120, r: 100})] 404 | ) 405 | ]), 406 | production 407 | ) 408 | ), 409 | '
    ' 410 | ) 411 | }) 412 | 413 | await t.test('should support a void element', async function () { 414 | assert.equal( 415 | renderToStaticMarkup(toJsxRuntime(h('hr'), production)), 416 | '
    ' 417 | ) 418 | }) 419 | }) 420 | 421 | test('source', async function (t) { 422 | await t.test('should not add a source in production', async function () { 423 | assert.deepEqual( 424 | getSource( 425 | toJsxRuntime( 426 | { 427 | type: 'element', 428 | tagName: 'a', 429 | properties: {}, 430 | children: [], 431 | position: {start: {line: 3, column: 2}, end: {line: 3, column: 123}} 432 | }, 433 | {...production, filePath: 'a/b/c.html'} 434 | ) 435 | ), 436 | undefined 437 | ) 438 | }) 439 | 440 | await t.test('should add a source in development', async function () { 441 | assert.deepEqual( 442 | getSource( 443 | toJsxRuntime( 444 | { 445 | type: 'element', 446 | tagName: 'a', 447 | properties: {}, 448 | children: [], 449 | position: {start: {line: 3, column: 2}, end: {line: 3, column: 123}} 450 | }, 451 | {...development, development: true, filePath: 'a/b/c.html'} 452 | ) 453 | ), 454 | // Related: . 455 | // Note: something changed in React 19. 456 | // This source info is now hidden somewhere? 457 | undefined 458 | // {fileName: 'a/b/c.html', lineNumber: 3, columnNumber: 1} 459 | ) 460 | }) 461 | 462 | /** 463 | * @param {unknown} node 464 | * @returns {Source | undefined} 465 | */ 466 | function getSource(node) { 467 | // @ts-expect-error: `_source` is not typed by react but does exist. 468 | const source = /** @type {Source | undefined} */ (node._source) 469 | return source 470 | } 471 | }) 472 | 473 | test('components', async function (t) { 474 | await t.test('should support function components', async function () { 475 | assert.equal( 476 | renderToStaticMarkup( 477 | toJsxRuntime(h('b#x'), { 478 | ...production, 479 | components: { 480 | /** 481 | * @param {React.JSX.IntrinsicElements['b'] & ExtraProps} props 482 | */ 483 | b(props) { 484 | // Note: types for this are working. 485 | assert(props.id === 'x') 486 | return 'a' 487 | } 488 | } 489 | }) 490 | ), 491 | 'a' 492 | ) 493 | }) 494 | 495 | await t.test('should support class components', async function () { 496 | assert.equal( 497 | renderToStaticMarkup( 498 | toJsxRuntime(h('b#x'), { 499 | ...production, 500 | components: { 501 | b: class extends React.Component { 502 | /** 503 | * @param {React.JSX.IntrinsicElements['b']} props 504 | */ 505 | constructor(props) { 506 | super(props) 507 | // Note: types for this are working. 508 | assert(props.id === 'x') 509 | } 510 | 511 | render() { 512 | return 'a' 513 | } 514 | } 515 | } 516 | }) 517 | ), 518 | 'a' 519 | ) 520 | }) 521 | 522 | await t.test( 523 | 'should support components w/ `passNode: true`', 524 | async function () { 525 | assert.equal( 526 | renderToStaticMarkup( 527 | toJsxRuntime(h('b'), { 528 | ...production, 529 | passNode: true, 530 | components: { 531 | /** 532 | * @param {React.JSX.IntrinsicElements['b'] & ExtraProps} props 533 | */ 534 | b(props) { 535 | assert.ok(props.node) 536 | return 'a' 537 | } 538 | } 539 | }) 540 | ), 541 | 'a' 542 | ) 543 | } 544 | ) 545 | 546 | await t.test('should support components w/o `passNode`', async function () { 547 | assert.equal( 548 | renderToStaticMarkup( 549 | toJsxRuntime(h('b'), { 550 | ...production, 551 | components: { 552 | /** 553 | * @param {React.JSX.IntrinsicElements['b'] & ExtraProps} props 554 | */ 555 | b(props) { 556 | assert.equal(props.node, undefined) 557 | return 'a' 558 | } 559 | } 560 | }) 561 | ), 562 | 'a' 563 | ) 564 | }) 565 | 566 | await t.test('should not pas `node` to basic components', async function () { 567 | assert.equal( 568 | renderToStaticMarkup( 569 | toJsxRuntime(h('h1', 'a'), { 570 | ...production, 571 | passNode: true, 572 | components: {h1: 'h2'} 573 | }) 574 | ), 575 | '

    a

    ' 576 | ) 577 | }) 578 | }) 579 | 580 | test('react specific: filter whitespace in tables', async function (t) { 581 | await t.test( 582 | 'should ignore whitespace in `table`, `thead`, `tbody`, `tfoot`, and `tr`', 583 | async function () { 584 | assert.equal( 585 | renderToStaticMarkup( 586 | toJsxRuntime( 587 | h(null, [ 588 | h('table', [ 589 | ' ', 590 | h('thead', [ 591 | ' ', 592 | h('tr', [' ', h('th', [' ', h('b', 'a'), ' ']), ' ']), 593 | ' ' 594 | ]), 595 | ' ', 596 | h('tbody', [ 597 | ' ', 598 | h('tr', [' ', h('td', [' ', h('b', 'b'), ' ']), ' ']), 599 | ' ' 600 | ]), 601 | ' ', 602 | h('tfoot', [ 603 | ' ', 604 | h('tr', [' ', h('td', [' ', h('b', 'c'), ' ']), ' ']), 605 | ' ' 606 | ]), 607 | ' ' 608 | ]) 609 | ]), 610 | production 611 | ) 612 | ), 613 | '
    a
    b
    c
    ' 614 | ) 615 | } 616 | ) 617 | }) 618 | 619 | test('react specific: `align` to `style`', async function (t) { 620 | const tree = h(null, [ 621 | h('th', {style: 'color:red', align: 'center'}, 'alpha'), 622 | h('td', {style: 'background-color:blue;', align: 'left'}, 'bravo'), 623 | h('td', {align: 'right'}, 'charlie'), 624 | h('td', 'delta') 625 | ]) 626 | 627 | await t.test( 628 | 'should not transform `align` w/ `tableCellAlignToStyle: false`', 629 | async function () { 630 | assert.equal( 631 | renderToStaticMarkup( 632 | toJsxRuntime(tree, {...production, tableCellAlignToStyle: false}) 633 | ), 634 | 'alphabravocharliedelta' 635 | ) 636 | } 637 | ) 638 | 639 | await t.test( 640 | 'should transform `align` w/o `tableCellAlignToStyle`', 641 | async function () { 642 | assert.equal( 643 | renderToStaticMarkup(toJsxRuntime(tree, production)), 644 | 'alphabravocharliedelta' 645 | ) 646 | } 647 | ) 648 | 649 | await t.test( 650 | "should support `tableCellAlignToStyle` w/ `stylePropertyNameCase: 'css'`", 651 | async function () { 652 | /** @type {unknown} */ 653 | let foundProps 654 | 655 | assert.equal( 656 | renderToStaticMarkup( 657 | toJsxRuntime(h('td', {align: 'center'}), { 658 | ...production, 659 | /** 660 | * @param {React.ElementType} type 661 | */ 662 | jsx(type, props) { 663 | foundProps = props 664 | return production.jsx(type, {}) 665 | }, 666 | stylePropertyNameCase: 'css' 667 | }) 668 | ), 669 | '' 670 | ) 671 | 672 | assert.deepEqual(foundProps, {style: {'text-align': 'center'}}) 673 | } 674 | ) 675 | 676 | await t.test( 677 | "should support `tableCellAlignToStyle` w/ `stylePropertyNameCase: 'dom'`", 678 | async function () { 679 | /** @type {unknown} */ 680 | let foundProps 681 | 682 | assert.equal( 683 | renderToStaticMarkup( 684 | toJsxRuntime(h('td', {align: 'center'}), { 685 | ...production, 686 | /** 687 | * @param {React.ElementType} type 688 | */ 689 | jsx(type, props) { 690 | foundProps = props 691 | return production.jsx(type, {}) 692 | }, 693 | stylePropertyNameCase: 'dom' 694 | }) 695 | ), 696 | '' 697 | ) 698 | 699 | assert.deepEqual(foundProps, {style: {textAlign: 'center'}}) 700 | } 701 | ) 702 | }) 703 | 704 | test('mdx: jsx', async function (t) { 705 | await t.test('should transform MDX JSX (text)', async function () { 706 | assert.equal( 707 | renderToStaticMarkup( 708 | toJsxRuntime( 709 | { 710 | type: 'mdxJsxTextElement', 711 | name: 'a', 712 | attributes: [], 713 | children: [] 714 | }, 715 | production 716 | ) 717 | ), 718 | '' 719 | ) 720 | }) 721 | 722 | await t.test('should transform MDX JSX (flow)', async function () { 723 | assert.equal( 724 | renderToStaticMarkup( 725 | toJsxRuntime( 726 | { 727 | type: 'mdxJsxTextElement', 728 | name: 'h1', 729 | attributes: [], 730 | children: [] 731 | }, 732 | production 733 | ) 734 | ), 735 | '

    ' 736 | ) 737 | }) 738 | 739 | await t.test('should transform MDX JSX (fragment)', async function () { 740 | assert.equal( 741 | renderToStaticMarkup( 742 | toJsxRuntime( 743 | { 744 | type: 'mdxJsxTextElement', 745 | name: null, 746 | attributes: [], 747 | children: [{type: 'text', value: 'a'}] 748 | }, 749 | production 750 | ) 751 | ), 752 | 'a' 753 | ) 754 | }) 755 | 756 | await t.test('should transform MDX JSX (fragment)', async function () { 757 | assert.equal( 758 | renderToStaticMarkup( 759 | toJsxRuntime( 760 | { 761 | type: 'mdxJsxTextElement', 762 | name: null, 763 | attributes: [], 764 | children: [] 765 | }, 766 | production 767 | ) 768 | ), 769 | '' 770 | ) 771 | }) 772 | 773 | await t.test( 774 | 'should transform MDX JSX (attribute, w/o value)', 775 | async function () { 776 | assert.equal( 777 | renderToStaticMarkup( 778 | toJsxRuntime( 779 | { 780 | type: 'mdxJsxTextElement', 781 | name: 'a', 782 | attributes: [ 783 | {type: 'mdxJsxAttribute', name: 'hidden', value: null} 784 | ], 785 | children: [] 786 | }, 787 | production 788 | ) 789 | ), 790 | '' 791 | ) 792 | } 793 | ) 794 | 795 | await t.test( 796 | 'should transform MDX JSX (attribute, w/ value)', 797 | async function () { 798 | assert.equal( 799 | renderToStaticMarkup( 800 | toJsxRuntime( 801 | { 802 | type: 'mdxJsxTextElement', 803 | name: 'a', 804 | attributes: [{type: 'mdxJsxAttribute', name: 'x', value: 'y'}], 805 | children: [] 806 | }, 807 | production 808 | ) 809 | ), 810 | '' 811 | ) 812 | } 813 | ) 814 | 815 | await t.test('should transform MDX JSX (SVG)', async function () { 816 | assert.equal( 817 | renderToStaticMarkup( 818 | toJsxRuntime( 819 | { 820 | type: 'mdxJsxTextElement', 821 | name: 'svg', 822 | attributes: [], 823 | children: [ 824 | s('g', { 825 | colorInterpolationFilters: 'sRGB', 826 | xmlSpace: 'preserve', 827 | xmlnsXLink: 'http://www.w3.org/1999/xlink', 828 | xLinkArcRole: 'http://www.example.com' 829 | }) 830 | ] 831 | }, 832 | production 833 | ) 834 | ), 835 | '' 836 | ) 837 | }) 838 | 839 | await t.test('should transform MDX JSX (literal)', async function () { 840 | assert.equal( 841 | renderToStaticMarkup( 842 | toJsxRuntime( 843 | { 844 | type: 'mdxJsxTextElement', 845 | name: 'a', 846 | attributes: [], 847 | children: [] 848 | }, 849 | {...production, components: {a: 'b'}} 850 | ) 851 | ), 852 | '' 853 | ) 854 | }) 855 | 856 | await t.test('should transform MDX JSX (namespace)', async function () { 857 | assert.equal( 858 | renderToStaticMarkup( 859 | toJsxRuntime( 860 | { 861 | type: 'mdxJsxTextElement', 862 | name: 'a:b', 863 | attributes: [], 864 | children: [] 865 | }, 866 | {...production, components: {'a:b': 'b'}} 867 | ) 868 | ), 869 | '' 870 | ) 871 | }) 872 | 873 | await t.test('should throw on identifier by default', async function () { 874 | assert.throws(function () { 875 | toJsxRuntime( 876 | { 877 | type: 'mdxJsxFlowElement', 878 | name: 'A', 879 | attributes: [], 880 | children: [] 881 | }, 882 | production 883 | ) 884 | }, /Cannot handle MDX estrees without `createEvaluater`/) 885 | }) 886 | 887 | await t.test('should transform MDX JSX (component)', async function () { 888 | assert.equal( 889 | renderToStaticMarkup( 890 | toJsxRuntime( 891 | { 892 | type: 'root', 893 | children: [ 894 | { 895 | type: 'mdxjsEsm', 896 | value: "export const A = 'b'", 897 | data: { 898 | estree: { 899 | type: 'Program', 900 | body: [ 901 | { 902 | type: 'ExportNamedDeclaration', 903 | declaration: { 904 | type: 'VariableDeclaration', 905 | declarations: [ 906 | { 907 | type: 'VariableDeclarator', 908 | id: {type: 'Identifier', name: 'A'}, 909 | init: {type: 'Literal', value: 'b'} 910 | } 911 | ], 912 | kind: 'const' 913 | }, 914 | specifiers: [], 915 | source: null 916 | } 917 | ], 918 | sourceType: 'module', 919 | comments: [] 920 | } 921 | } 922 | }, 923 | { 924 | type: 'mdxJsxFlowElement', 925 | name: 'A', 926 | attributes: [], 927 | children: [] 928 | } 929 | ] 930 | }, 931 | {...production, createEvaluater} 932 | ) 933 | ), 934 | '' 935 | ) 936 | }) 937 | 938 | await t.test( 939 | 'should transform MDX JSX (member expression, non-identifier)', 940 | async function () { 941 | assert.equal( 942 | renderToStaticMarkup( 943 | toJsxRuntime( 944 | { 945 | type: 'root', 946 | children: [ 947 | { 948 | type: 'mdxjsEsm', 949 | value: "export const a = {'b-c': 'c'}", 950 | data: { 951 | estree: { 952 | type: 'Program', 953 | body: [ 954 | { 955 | type: 'ExportNamedDeclaration', 956 | declaration: { 957 | type: 'VariableDeclaration', 958 | declarations: [ 959 | { 960 | type: 'VariableDeclarator', 961 | id: {type: 'Identifier', name: 'a'}, 962 | init: { 963 | type: 'ObjectExpression', 964 | properties: [ 965 | { 966 | type: 'Property', 967 | method: false, 968 | shorthand: false, 969 | computed: false, 970 | key: {type: 'Literal', value: 'b-c'}, 971 | value: {type: 'Literal', value: 'c'}, 972 | kind: 'init' 973 | } 974 | ] 975 | } 976 | } 977 | ], 978 | kind: 'const' 979 | }, 980 | specifiers: [], 981 | source: null 982 | } 983 | ], 984 | sourceType: 'module', 985 | comments: [] 986 | } 987 | } 988 | }, 989 | { 990 | type: 'mdxJsxFlowElement', 991 | name: 'a.b-c', 992 | attributes: [], 993 | children: [] 994 | } 995 | ] 996 | }, 997 | {...production, createEvaluater} 998 | ) 999 | ), 1000 | '' 1001 | ) 1002 | } 1003 | ) 1004 | 1005 | await t.test( 1006 | 'should throw on expression attribute by default', 1007 | async function () { 1008 | assert.throws(function () { 1009 | toJsxRuntime( 1010 | { 1011 | type: 'mdxJsxFlowElement', 1012 | name: 'a', 1013 | attributes: [{type: 'mdxJsxExpressionAttribute', value: '...x'}], 1014 | children: [] 1015 | }, 1016 | production 1017 | ) 1018 | }, /Cannot handle MDX estrees without `createEvaluater`/) 1019 | } 1020 | ) 1021 | 1022 | await t.test( 1023 | 'should throw on attribute value expression by default', 1024 | async function () { 1025 | assert.throws(function () { 1026 | toJsxRuntime( 1027 | { 1028 | type: 'mdxJsxFlowElement', 1029 | name: 'a', 1030 | attributes: [ 1031 | { 1032 | type: 'mdxJsxAttribute', 1033 | name: 'x', 1034 | value: {type: 'mdxJsxAttributeValueExpression', value: '1'} 1035 | } 1036 | ], 1037 | children: [] 1038 | }, 1039 | production 1040 | ) 1041 | }, /Cannot handle MDX estrees without `createEvaluater`/) 1042 | } 1043 | ) 1044 | 1045 | await t.test( 1046 | 'should support expression attribute w/ `createEvaluater`', 1047 | async function () { 1048 | assert.equal( 1049 | renderToStaticMarkup( 1050 | toJsxRuntime( 1051 | { 1052 | type: 'mdxJsxTextElement', 1053 | name: 'a', 1054 | attributes: [ 1055 | { 1056 | type: 'mdxJsxExpressionAttribute', 1057 | value: "...{x: 'y'}", 1058 | data: { 1059 | estree: { 1060 | type: 'Program', 1061 | body: [ 1062 | { 1063 | type: 'ExpressionStatement', 1064 | expression: { 1065 | type: 'ObjectExpression', 1066 | properties: [ 1067 | { 1068 | type: 'SpreadElement', 1069 | argument: { 1070 | type: 'ObjectExpression', 1071 | properties: [ 1072 | { 1073 | type: 'Property', 1074 | method: false, 1075 | shorthand: false, 1076 | computed: false, 1077 | key: {type: 'Identifier', name: 'x'}, 1078 | value: {type: 'Literal', value: 'y'}, 1079 | kind: 'init' 1080 | } 1081 | ] 1082 | } 1083 | } 1084 | ] 1085 | } 1086 | } 1087 | ], 1088 | sourceType: 'module', 1089 | comments: [] 1090 | } 1091 | } 1092 | } 1093 | ], 1094 | children: [] 1095 | }, 1096 | {...production, createEvaluater} 1097 | ) 1098 | ), 1099 | '' 1100 | ) 1101 | } 1102 | ) 1103 | 1104 | await t.test( 1105 | 'should support attribute value expression w/ `createEvaluater`', 1106 | async function () { 1107 | assert.equal( 1108 | renderToStaticMarkup( 1109 | toJsxRuntime( 1110 | { 1111 | type: 'mdxJsxTextElement', 1112 | name: 'a', 1113 | attributes: [ 1114 | { 1115 | type: 'mdxJsxAttribute', 1116 | name: 'x', 1117 | value: { 1118 | type: 'mdxJsxAttributeValueExpression', 1119 | value: "'y'", 1120 | data: { 1121 | estree: { 1122 | type: 'Program', 1123 | body: [ 1124 | { 1125 | type: 'ExpressionStatement', 1126 | expression: {type: 'Literal', value: 'y'} 1127 | } 1128 | ], 1129 | sourceType: 'module', 1130 | comments: [] 1131 | } 1132 | } 1133 | } 1134 | } 1135 | ], 1136 | children: [] 1137 | }, 1138 | {...production, createEvaluater} 1139 | ) 1140 | ), 1141 | '' 1142 | ) 1143 | } 1144 | ) 1145 | }) 1146 | 1147 | test('mdx: expression', async function (t) { 1148 | await t.test('should throw on expression by default', async function () { 1149 | assert.throws(function () { 1150 | toJsxRuntime({type: 'mdxFlowExpression', value: "'a'"}, production) 1151 | }, /Cannot handle MDX estrees without `createEvaluater`/) 1152 | }) 1153 | 1154 | await t.test( 1155 | 'should support expression w/ `createEvaluater`', 1156 | async function () { 1157 | assert.equal( 1158 | renderToStaticMarkup( 1159 | toJsxRuntime( 1160 | { 1161 | type: 'mdxFlowExpression', 1162 | value: "'a'", 1163 | data: { 1164 | estree: { 1165 | type: 'Program', 1166 | body: [ 1167 | { 1168 | type: 'ExpressionStatement', 1169 | expression: {type: 'Literal', value: 'a'} 1170 | } 1171 | ], 1172 | sourceType: 'module', 1173 | comments: [] 1174 | } 1175 | } 1176 | }, 1177 | {...production, createEvaluater} 1178 | ) 1179 | ), 1180 | 'a' 1181 | ) 1182 | } 1183 | ) 1184 | }) 1185 | 1186 | test('mdx: ESM', async function (t) { 1187 | await t.test('should throw on ESM by default', async function () { 1188 | assert.throws(function () { 1189 | toJsxRuntime( 1190 | {type: 'mdxjsEsm', value: "export const a = 'a'"}, 1191 | production 1192 | ) 1193 | }, /Cannot handle MDX estrees without `createEvaluater`/) 1194 | }) 1195 | 1196 | await t.test('should support ESM w/ `createEvaluater`', async function () { 1197 | assert.equal( 1198 | renderToStaticMarkup( 1199 | toJsxRuntime( 1200 | { 1201 | type: 'root', 1202 | children: [ 1203 | { 1204 | type: 'mdxjsEsm', 1205 | value: "export const a = 'b'", 1206 | data: { 1207 | estree: { 1208 | type: 'Program', 1209 | body: [ 1210 | { 1211 | type: 'ExportNamedDeclaration', 1212 | declaration: { 1213 | type: 'VariableDeclaration', 1214 | declarations: [ 1215 | { 1216 | type: 'VariableDeclarator', 1217 | id: {type: 'Identifier', name: 'a'}, 1218 | init: {type: 'Literal', value: 'b'} 1219 | } 1220 | ], 1221 | kind: 'const' 1222 | }, 1223 | specifiers: [], 1224 | source: null 1225 | } 1226 | ], 1227 | sourceType: 'module', 1228 | comments: [] 1229 | } 1230 | } 1231 | }, 1232 | { 1233 | type: 'mdxFlowExpression', 1234 | value: 'a', 1235 | data: { 1236 | estree: { 1237 | type: 'Program', 1238 | body: [ 1239 | { 1240 | type: 'ExpressionStatement', 1241 | expression: {type: 'Identifier', name: 'a'} 1242 | } 1243 | ], 1244 | sourceType: 'module', 1245 | comments: [] 1246 | } 1247 | } 1248 | } 1249 | ] 1250 | }, 1251 | {...production, createEvaluater} 1252 | ) 1253 | ), 1254 | 'b' 1255 | ) 1256 | }) 1257 | }) 1258 | 1259 | /** 1260 | * @type {CreateEvaluater} 1261 | */ 1262 | function createEvaluater() { 1263 | const interpreter = new Sval({sandBox: true}) 1264 | 1265 | return { 1266 | evaluateExpression(expression) { 1267 | /** @type {Program} */ 1268 | const program = { 1269 | type: 'Program', 1270 | body: [ 1271 | { 1272 | type: 'ExpressionStatement', 1273 | expression: { 1274 | type: 'AssignmentExpression', 1275 | operator: '=', 1276 | left: { 1277 | type: 'MemberExpression', 1278 | object: {type: 'Identifier', name: 'exports'}, 1279 | property: { 1280 | type: 'Identifier', 1281 | name: '_evaluateExpressionValue' 1282 | }, 1283 | computed: false, 1284 | optional: false 1285 | }, 1286 | right: expression 1287 | } 1288 | } 1289 | ], 1290 | sourceType: 'module' 1291 | } 1292 | 1293 | // @ts-expect-error: note: `sval` types are wrong, programs are nodes. 1294 | interpreter.run(program) 1295 | const value = /** @type {unknown} */ ( 1296 | // type-coverage:ignore-next-line 1297 | interpreter.exports._evaluateExpressionValue 1298 | ) 1299 | // type-coverage:ignore-next-line 1300 | interpreter.exports._evaluateExpressionValue = undefined 1301 | return value 1302 | }, 1303 | /** 1304 | * @returns {undefined} 1305 | */ 1306 | evaluateProgram(program) { 1307 | visit(program, function (node, key, index, parents) { 1308 | // Sval doesn’t support exports yet. 1309 | if (node.type === 'ExportNamedDeclaration' && node.declaration) { 1310 | const parent = parents[parents.length - 1] 1311 | assert(parent) 1312 | assert(typeof key === 'string') 1313 | assert(typeof index === 'number') 1314 | // @ts-expect-error: fine. 1315 | parent[key][index] = node.declaration 1316 | } 1317 | }) 1318 | 1319 | // @ts-expect-error: note: `sval` types are wrong, programs are nodes. 1320 | interpreter.run(program) 1321 | } 1322 | } 1323 | } 1324 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declarationMap": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "lib": ["es2022"], 10 | "module": "node16", 11 | // Needed because `@types/react-dom` is currently broken. 12 | "skipLibCheck": true, 13 | "strict": true, 14 | "target": "es2022" 15 | }, 16 | "exclude": ["coverage/", "node_modules/"], 17 | "include": ["**/**.js", "lib/types.d.ts", "index.d.ts", "types-esmsh.d.ts"] 18 | } 19 | -------------------------------------------------------------------------------- /types-esmsh.d.ts: -------------------------------------------------------------------------------- 1 | // Support loading hastscript from 2 | declare module 'https://esm.sh/hastscript@9?dev' { 3 | export * from 'hastscript' 4 | } 5 | --------------------------------------------------------------------------------