Skip to content

Commit

Permalink
Add support for passing styles as CSS fields
Browse files Browse the repository at this point in the history
Previously, all style props were passed using DOM (`backgroundColor`)
casing.
This does not work in some any other JSX runtimes.
Instead, some JSX runtimes want `background-color` instead.

This adds an option for that.

This commit also fixes support for CSS custom properties (`--fg: red`),
which always have to include those initial dashes, even when setting to
the DOM.
  • Loading branch information
wooorm committed Feb 2, 2023
1 parent 56d8336 commit 98117a3
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 19 deletions.
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* @typedef {import('./lib/index.js').Props} Props
* @typedef {import('./lib/index.js').Source} Source
* @typedef {import('./lib/index.js').Space} Space
* @typedef {import('./lib/index.js').StylePropertyNameCase} StylePropertyNameCase
*/

export {toJsxRuntime} from './lib/index.js'
83 changes: 73 additions & 10 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,17 @@
* @typedef {'html' | 'svg'} Space
* Namespace.
*
* @typedef {'react' | 'html'} ElementAttributeNameCase
* Specify casing to use for attribute names.
* @typedef {'html' | 'react'} ElementAttributeNameCase
* Casing to use for attribute names.
*
* React casing is for example `className`, `strokeLinecap`, `xmlLang`.
* HTML casing is for example `class`, `stroke-linecap`, `xml:lang`.
* React casing is for example `className`, `strokeLinecap`, `xmlLang`.
*
* @typedef {'css' | 'dom'} StylePropertyNameCase
* Casing to use for property names in `style` objects.
*
* CSS casing is for example `background-color` and `-webkit-line-clamp`.
* DOM casing is for example `backgroundColor` and `WebkitLineClamp`.
*
* @typedef Source
* Info about source.
Expand Down Expand Up @@ -99,6 +105,8 @@
* Pass `node` to components.
* @property {ElementAttributeNameCase} elementAttributeNameCase
* Casing to use for attribute names.
* @property {StylePropertyNameCase} stylePropertyNameCase
* Casing to use for property names in `style` objects.
* @property {Schema} schema
* Current schema.
* @property {unknown} Fragment
Expand All @@ -122,6 +130,8 @@
* Pass the hast element node to components.
* @property {ElementAttributeNameCase | null | undefined} [elementAttributeNameCase='react']
* Specify casing to use for attribute names.
* @property {StylePropertyNameCase | null | undefined} [stylePropertyNameCase='dom']
* Specify casing to use for property names in `style` objects.
* @property {Space | null | undefined} [space='html']
* Whether `tree` is in the `'html'` or `'svg'` space.
*
Expand Down Expand Up @@ -195,6 +205,9 @@ import {whitespace} from 'hast-util-whitespace'

const own = {}.hasOwnProperty

const cap = /[A-Z]/g
const dashSomething = /-([a-z])/g

// `react-dom` triggers a warning for *any* white space in tables.
// To follow GFM, `mdast-util-to-hast` injects line endings between elements.
// Other tools might do so too, but they don’t do here, so we remove all of
Expand Down Expand Up @@ -255,6 +268,7 @@ export function toJsxRuntime(tree, options) {
schema: options.space === 'svg' ? svg : html,
passNode: options.passNode || false,
elementAttributeNameCase: options.elementAttributeNameCase || 'react',
stylePropertyNameCase: options.stylePropertyNameCase || 'dom',
components: options.components || {},
filePath,
create
Expand Down Expand Up @@ -485,8 +499,15 @@ function createProperty(state, node, prop, value) {
}

// React only accepts `style` as object.
if (info.property === 'style' && typeof value === 'string') {
return ['style', parseStyle(state, node, value)]
if (info.property === 'style') {
let styleObject =
typeof value === 'object' ? value : parseStyle(state, node, String(value))

if (state.stylePropertyNameCase === 'css') {
styleObject = transformStyleToCssCasing(styleObject)
}

return ['style', styleObject]
}

return [
Expand Down Expand Up @@ -534,8 +555,8 @@ function parseStyle(state, node, value) {
return result

/**
* Add a CSS property (normal, so with dashes) to `result` as a camelcased
* CSS property.
* Add a CSS property (normal, so with dashes) to `result` as a DOM CSS
* property.
*
* @param {string} name
* Key.
Expand All @@ -545,9 +566,39 @@ function parseStyle(state, node, value) {
* Nothing.
*/
function replacer(name, value) {
if (name.slice(0, 4) === '-ms-') name = 'ms-' + name.slice(4)
result[name.replace(/-([a-z])/g, replace)] = value
let key = name

if (key.slice(0, 2) !== '--') {
if (key.slice(0, 4) === '-ms-') key = 'ms-' + key.slice(4)
key = key.replace(dashSomething, toCamel)
}

result[key] = value
}
}

/**
* Transform a DOM casing style object to a CSS casing style object.
*
* @param {Style} domCasing
* @returns {Style}
*/
function transformStyleToCssCasing(domCasing) {
/** @type {Style} */
const cssCasing = {}
/** @type {string} */
let from

for (from in domCasing) {
if (own.call(domCasing, from)) {
let to = from.replace(cap, toDash)
// Handle `ms-xxx` -> `-ms-xxx`.
if (to.slice(0, 3) === 'ms-') to = '-' + to
cssCasing[to] = domCasing[from]
}
}

return cssCasing
}

/**
Expand All @@ -560,6 +611,18 @@ function parseStyle(state, node, value) {
* @returns {string}
* Capitalized `$1`.
*/
function replace(_, $1) {
function toCamel(_, $1) {
return $1.toUpperCase()
}

/**
* Make `$0` dash cased.
*
* @param {string} $0
* Capitalized ASCII leter.
* @returns {string}
* Dash and lower letter.
*/
function toDash($0) {
return '-' + $0.toLowerCase()
}
41 changes: 32 additions & 9 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ with an automatic JSX runtime.
* [`Props`](#props)
* [`Source`](#source)
* [`Space`](#space-1)
* [`StylePropertyNameCase`](#stylepropertynamecase-1)
* [Examples](#examples)
* [Example: Preact](#example-preact)
* [Example: Vue](#example-vue)
Expand Down Expand Up @@ -157,19 +158,19 @@ Components to use ([`Partial<Components>`][api-components], optional).
Each key is the name of an HTML (or SVG) element to override.
The value is the component to render instead.

###### `elementAttributeNameCase`

Specify casing to use for attribute names
([`ElementAttributeNameCase`][api-element-attribute-name-case], default:
`'react'`).

###### `filePath`

File path to the original source file (`string`, optional).

Passed in source info to `jsxDEV` when using the automatic runtime with
`development: true`.

###### `elementAttributeNameCase`

Specify casing to use for attribute names
([`ElementAttributeNameCase`][api-element-attribute-name-case], default:
`'react'`).

###### `passNode`

Pass the hast element node to components (`boolean`, default: `false`).
Expand All @@ -189,6 +190,12 @@ it.
> Passing SVG might break but fragments of modern SVG should be fine.
> Use `xast` if you need to support SVG as XML.
###### `stylePropertyNameCase`

Specify casing to use for property names in `style` objects
([`StylePropertyNameCase`][api-style-property-name-case], default:
`'dom'`).

### `Components`

Possible components to use (TypeScript type).
Expand Down Expand Up @@ -222,10 +229,10 @@ type Component<ComponentProps> =
### `ElementAttributeNameCase`
Specify casing to use for attribute names (TypeScript type).
Casing to use for attribute names (TypeScript type).
React casing is for example `className`, `strokeLinecap`, `xmlLang`.
HTML casing is for example `class`, `stroke-linecap`, `xml:lang`.
React casing is for example `className`, `strokeLinecap`, `xmlLang`.
###### Type
Expand Down Expand Up @@ -329,6 +336,19 @@ Namespace (TypeScript type).
type Space = 'html' | 'svg'
```
### `StylePropertyNameCase`
Casing to use for property names in `style` objects (TypeScript type).
CSS casing is for example `background-color` and `-webkit-line-clamp`.
DOM casing is for example `backgroundColor` and `WebkitLineClamp`.
###### Type
```ts
type StylePropertyNameCase = 'dom' | 'css'
```
## Examples
### Example: Preact
Expand Down Expand Up @@ -420,7 +440,8 @@ It exports the additional types [`Components`][api-components],
[`ElementAttributeNameCase`][api-element-attribute-name-case],
[`Fragment`][api-fragment], [`Jsx`][api-jsx], [`JsxDev`][api-jsx-dev],
[`Options`][api-options], [`Props`][api-props], [`Source`][api-source],
and [`Space`][api-Space].
[`Space`][api-Space],
and [`StylePropertyNameCase`][api-style-property-name-case].

The function `toJsxRuntime` returns a `JSX.Element`, which means that the JSX
namespace has to by typed.
Expand Down Expand Up @@ -537,3 +558,5 @@ abide by its terms.
[api-source]: #source

[api-space]: #space-1

[api-style-property-name-case]: #stylepropertynamecase-1
77 changes: 77 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,83 @@ test('properties', () => {
'should support `style`'
)

assert.equal(
renderToStaticMarkup(
toJsxRuntime(
{
type: 'element',
tagName: 'div',
// @ts-expect-error: style as object, normally not supported, but passed through here.
properties: {style: {color: 'red'}},
children: []
},
production
)
),
'<div style="color:red"></div>',
'should support `style` as an object'
)

assert.equal(
renderToStaticMarkup(
toJsxRuntime(
h('div', {style: '-webkit-transform: rotate(0.01turn)'}),
production
)
),
'<div style="-webkit-transform:rotate(0.01turn)"></div>',
'should support vendor prefixes'
)

assert.equal(
renderToStaticMarkup(
toJsxRuntime(
h('div', {style: '--fg: #0366d6; color: var(--fg)'}),
production
)
),
'<div style="--fg:#0366d6;color:var(--fg)"></div>',
'should support CSS variables'
)

/** @type {unknown} */
let foundProps

assert.equal(
renderToStaticMarkup(
toJsxRuntime(
h('div', {
style:
'-webkit-transform:rotate(0.01turn); --fg: #0366d6; color: var(--fg); -ms-transition: unset'
}),
{
...production,
jsx(type, props) {
foundProps = props
return production.jsx('div', {children: []}, undefined)
},
stylePropertyNameCase: 'css'
}
)
),
'<div></div>',
'should support CSS cased style objects (1)'
)

assert.deepEqual(
foundProps,
{
children: undefined,
style: {
'-webkit-transform': 'rotate(0.01turn)',
'--fg': '#0366d6',
color: 'var(--fg)',
'-ms-transition': 'unset'
}
},
'should support CSS cased style objects (2)'
)

assert.throws(
() => {
toJsxRuntime(h('div', {style: 'color:red; /*'}), production)
Expand Down

0 comments on commit 98117a3

Please sign in to comment.