Skip to content

Commit 7867d68

Browse files
committed
Add support for passing styles as CSS fields
Previously, all style props were passed using DOM (`backgroundColor`) casing. This does not work in some 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. Closes #6. Closes #7.
1 parent bb3e687 commit 7867d68

File tree

5 files changed

+160
-21
lines changed

5 files changed

+160
-21
lines changed

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* @typedef {import('./lib/state.js').Options} Options
55
* @typedef {import('./lib/state.js').Space} Space
66
* @typedef {import('./lib/state.js').State} State
7+
* @typedef {import('./lib/state.js').StylePropertyNameCase} StylePropertyNameCase
78
*/
89

910
export {handlers as defaultHandlers} from './lib/handlers/index.js'

lib/handlers/element.js

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
* @typedef {import('../state.js').State} State
88
*/
99

10+
/**
11+
* @typedef {Record<string, string>} Style
12+
*/
13+
1014
import {stringify as commas} from 'comma-separated-tokens'
1115
import {svg, find, hastToReact} from 'property-information'
1216
import {stringify as spaces} from 'space-separated-tokens'
@@ -17,6 +21,8 @@ import {
1721
import styleToObject from 'style-to-object'
1822

1923
const own = {}.hasOwnProperty
24+
const cap = /[A-Z]/g
25+
const dashSomething = /-([a-z])/g
2026

2127
/**
2228
* Turn a hast element into an estree node.
@@ -77,26 +83,30 @@ export function element(node, state) {
7783
}
7884

7985
if (prop === 'style') {
80-
/** @type {Record<string, string>} */
81-
// @ts-expect-error Assume `value` is an object otherwise.
82-
const styleValue =
83-
typeof value === 'string' ? parseStyle(value, node.tagName) : value
86+
let styleObject =
87+
typeof value === 'object'
88+
? value
89+
: parseStyle(String(value), node.tagName)
90+
91+
if (state.stylePropertyNameCase === 'css') {
92+
styleObject = transformStyleToCssCasing(styleObject)
93+
}
8494

8595
/** @type {Array<Property>} */
8696
const cssProperties = []
8797
/** @type {string} */
8898
let cssProp
8999

90-
for (cssProp in styleValue) {
100+
for (cssProp in styleObject) {
91101
// eslint-disable-next-line max-depth
92-
if (own.call(styleValue, cssProp)) {
102+
if (own.call(styleObject, cssProp)) {
93103
cssProperties.push({
94104
type: 'Property',
95105
method: false,
96106
shorthand: false,
97107
computed: false,
98108
key: {type: 'Identifier', name: cssProp},
99-
value: {type: 'Literal', value: String(styleValue[cssProp])},
109+
value: {type: 'Literal', value: String(styleObject[cssProp])},
100110
kind: 'init'
101111
})
102112
}
@@ -174,11 +184,11 @@ export function element(node, state) {
174184
* CSS text.
175185
* @param {string} tagName
176186
* Element name.
177-
* @returns {Record<string, string>}
187+
* @returns {Style}
178188
* Props.
179189
*/
180190
function parseStyle(value, tagName) {
181-
/** @type {Record<string, string>} */
191+
/** @type {Style} */
182192
const result = {}
183193

184194
try {
@@ -203,25 +213,68 @@ function parseStyle(value, tagName) {
203213
* Nothing.
204214
*/
205215
function iterator(name, value) {
206-
if (name.slice(0, 4) === '-ms-') name = 'ms-' + name.slice(4)
207-
result[name.replace(/-([a-z])/g, styleReplacer)] = value
216+
let key = name
217+
218+
if (key.slice(0, 2) !== '--') {
219+
// See: <https://alanhogan.com/code/vendor-prefixed-css-property-names-in-javascript>
220+
if (key.slice(0, 4) === '-ms-') key = 'ms-' + key.slice(4)
221+
key = key.replace(dashSomething, toCamel)
222+
}
223+
224+
result[key] = value
225+
}
226+
}
227+
228+
/**
229+
* Transform a DOM casing style object to a CSS casing style object.
230+
*
231+
* @param {Style} domCasing
232+
* @returns {Style}
233+
*/
234+
function transformStyleToCssCasing(domCasing) {
235+
/** @type {Style} */
236+
const cssCasing = {}
237+
/** @type {string} */
238+
let from
239+
240+
for (from in domCasing) {
241+
if (own.call(domCasing, from)) {
242+
let to = from.replace(cap, toDash)
243+
// Handle `ms-xxx` -> `-ms-xxx`.
244+
if (to.slice(0, 3) === 'ms-') to = '-' + to
245+
cssCasing[to] = domCasing[from]
246+
}
208247
}
248+
249+
return cssCasing
209250
}
210251

211252
/**
212-
* Uppercase `$1`.
253+
* Make `$1` capitalized.
213254
*
214255
* @param {string} _
215256
* Whatever.
216257
* @param {string} $1
217-
* an ASCII alphabetic.
258+
* Single ASCII alphabetical.
218259
* @returns {string}
219-
* Uppercased `$1`.
260+
* Capitalized `$1`.
220261
*/
221-
function styleReplacer(_, $1) {
262+
function toCamel(_, $1) {
222263
return $1.toUpperCase()
223264
}
224265

266+
/**
267+
* Make `$0` dash cased.
268+
*
269+
* @param {string} $0
270+
* Capitalized ASCII leter.
271+
* @returns {string}
272+
* Dash and lower letter.
273+
*/
274+
function toDash($0) {
275+
return '-' + $0.toLowerCase()
276+
}
277+
225278
/**
226279
* Checks if the given string is a valid identifier name.
227280
*

lib/state.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,22 @@
4747
* @returns {JsxChild | null | undefined | void}
4848
* estree node.
4949
*
50-
* @typedef {'react' | 'html'} ElementAttributeNameCase
50+
* @typedef {'html' | 'react'} ElementAttributeNameCase
5151
* Specify casing to use for attribute names.
5252
*
53-
* React casing is for example `className`, `strokeLinecap`, `xmlLang`.
5453
* HTML casing is for example `class`, `stroke-linecap`, `xml:lang`.
54+
* React casing is for example `className`, `strokeLinecap`, `xmlLang`.
55+
*
56+
* @typedef {'css' | 'dom'} StylePropertyNameCase
57+
* Casing to use for property names in `style` objects.
58+
*
59+
* CSS casing is for example `background-color` and `-webkit-line-clamp`.
60+
* DOM casing is for example `backgroundColor` and `WebkitLineClamp`.
5561
*
5662
* @typedef Options
5763
* Configuration.
5864
* @property {ElementAttributeNameCase | null | undefined} [elementAttributeNameCase='react']
59-
* Casing to use for attribute names.
65+
* Specify casing to use for attribute names.
6066
*
6167
* This casing is used for hast elements, not for embedded MDX JSX nodes
6268
* (components that someone authored manually).
@@ -68,13 +74,20 @@
6874
* When an `<svg>` element is found in the HTML space, this package already
6975
* automatically switches to and from the SVG space when entering and exiting
7076
* it.
77+
* @property {StylePropertyNameCase | null | undefined} [stylePropertyNameCase='dom']
78+
* Specify casing to use for property names in `style` objects.
79+
*
80+
* This casing is used for hast elements, not for embedded MDX JSX nodes
81+
* (components that someone authored manually).
7182
*
7283
* @typedef State
7384
* Info passed around about the current state.
7485
* @property {Schema} schema
7586
* Current schema.
7687
* @property {ElementAttributeNameCase} elementAttributeNameCase
7788
* Casing to use for attribute names.
89+
* @property {StylePropertyNameCase} stylePropertyNameCase
90+
* Casing to use for property names in `style` objects.
7891
* @property {Array<Comment>} comments
7992
* List of estree comments.
8093
* @property {Array<Directive | Statement | ModuleDeclaration>} esm
@@ -131,6 +144,7 @@ export function createState(options) {
131144
// Current space.
132145
schema: options.space === 'svg' ? svg : html,
133146
elementAttributeNameCase: options.elementAttributeNameCase || 'react',
147+
stylePropertyNameCase: options.stylePropertyNameCase || 'dom',
134148
// Results.
135149
comments: [],
136150
esm: [],

readme.md

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
* [`Options`](#options)
2525
* [`Space`](#space)
2626
* [`State`](#state)
27+
* [`StylePropertyNameCase`](#stylepropertynamecase)
2728
* [Types](#types)
2829
* [Compatibility](#compatibility)
2930
* [Security](#security)
@@ -198,13 +199,13 @@ Each key is a node type, each value is a [`Handle`][api-handle].
198199

199200
Specify casing to use for attribute names (TypeScript type).
200201

201-
React casing is for example `className`, `strokeLinecap`, `xmlLang`.
202202
HTML casing is for example `class`, `stroke-linecap`, `xml:lang`.
203+
React casing is for example `className`, `strokeLinecap`, `xmlLang`.
203204

204205
###### Type
205206

206207
```ts
207-
type ElementAttributeNameCase = 'react' | 'html'
208+
type ElementAttributeNameCase = 'html' | 'react'
208209
```
209210
210211
### `Handle`
@@ -242,6 +243,12 @@ Configuration (TypeScript type).
242243
which space the document is in; when an `<svg>` element is found in the
243244
HTML space, this package already automatically switches to and from the SVG
244245
space when entering and exiting it
246+
* `stylePropertyNameCase`
247+
([`StylePropertyNameCase`][api-style-property-name-case],
248+
default: `'dom'`)
249+
— specify casing to use for property names in `style` objects; this casing
250+
is used for hast elements, not for embedded MDX JSX nodes (components that
251+
someone authored manually)
245252
246253
### `Space`
247254
@@ -282,13 +289,27 @@ Info passed around about the current state (TypeScript type).
282289
* `createJsxElementName` (`(name: string) => EstreeJsxElementName`)
283290
— create a JSX attribute name
284291
292+
### `StylePropertyNameCase`
293+
294+
Casing to use for property names in `style` objects (TypeScript type).
295+
296+
CSS casing is for example `background-color` and `-webkit-line-clamp`.
297+
DOM casing is for example `backgroundColor` and `WebkitLineClamp`.
298+
299+
###### Type
300+
301+
```ts
302+
type StylePropertyNameCase = 'dom' | 'css'
303+
```
304+
285305
## Types
286306
287307
This package is fully typed with [TypeScript][].
288308
It exports the additional types
289309
[`ElementAttributeNameCase`][api-element-attribute-name-case],
290310
[`Handle`][api-handle], [`Options`][api-options],
291-
[`Space`][api-space], and [`State`][api-state].
311+
[`Space`][api-space], [`State`][api-state], and
312+
[`StylePropertyNameCase`][api-style-property-name-case].
292313
293314
## Compatibility
294315
@@ -406,3 +427,5 @@ abide by its terms.
406427
[api-space]: #space
407428
408429
[api-state]: #state
430+
431+
[api-style-property-name-case]: #stylepropertynamecase

test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,54 @@ test('toEstree', () => {
611611
'<div id="a" class="b c">{"d"}</div>;\n',
612612
"should support `elementAttributeNameCase: 'html'`"
613613
)
614+
615+
assert.equal(
616+
toJs(toEstree(h('h1', {style: 'background-color: red;'}, 'x')), {
617+
handlers: jsx
618+
}).value,
619+
'<h1 style={{\n backgroundColor: "red"\n}}>{"x"}</h1>;\n',
620+
'should use react casing for css properties by default'
621+
)
622+
623+
assert.equal(
624+
toJs(
625+
toEstree(h('h1', {style: 'background-color: red;'}, 'x'), {
626+
stylePropertyNameCase: 'css'
627+
}),
628+
{handlers: jsx}
629+
).value,
630+
'<h1 style={{\n background-color: "red"\n}}>{"x"}</h1>;\n',
631+
"should support `stylePropertyNameCase: 'css'`"
632+
)
633+
634+
assert.equal(
635+
toJs(
636+
toEstree(
637+
h('h1', {
638+
style:
639+
'-webkit-transform: rotate(0.01turn); -ms-transform: rotate(0.01turn); --fg: #0366d6; color: var(--fg)'
640+
})
641+
),
642+
{handlers: jsx}
643+
).value,
644+
'<h1 style={{\n WebkitTransform: "rotate(0.01turn)",\n msTransform: "rotate(0.01turn)",\n --fg: "#0366d6",\n color: "var(--fg)"\n}} />;\n',
645+
'should support vendor prefixes and css variables (dom)'
646+
)
647+
648+
assert.equal(
649+
toJs(
650+
toEstree(
651+
h('h1', {
652+
style:
653+
'-webkit-transform: rotate(0.01turn); -ms-transform: rotate(0.01turn); --fg: #0366d6; color: var(--fg)'
654+
}),
655+
{stylePropertyNameCase: 'css'}
656+
),
657+
{handlers: jsx}
658+
).value,
659+
'<h1 style={{\n -webkit-transform: "rotate(0.01turn)",\n -ms-transform: "rotate(0.01turn)",\n --fg: "#0366d6",\n color: "var(--fg)"\n}} />;\n',
660+
'should support vendor prefixes and css variables (css)'
661+
)
614662
})
615663

616664
test('integration (babel)', () => {

0 commit comments

Comments
 (0)