Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add transformAttributes option to @svgr/core and hast-util-to-babel-ast #927

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface Config {
// JSX
jsx?: {
babelConfig?: BabelTransformOptions
transformAttributes?: boolean | ((key: string) => string)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
exports[`hast-util-to-babel-ast should handle spaces and tab 1`] = `"<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M25,5h-3V3c0-1.7-1.3-3-3-3H5C3.3,0,2,1.3,2,3v20c0,1.7,1.3,3,3,3h4v1c0,2.2,1.8,4,4,4h12c2.2,0,4-1.8,4-4V9 C29,6.8,27.2,5,25,5z M5,24c-0.6,0-1-0.5-1-1V3c0-0.6,0.4-1,1-1h14c0.5,0,1,0.4,1,1v2h-6.3H13H6.3c-0.6,0-1,0.4-1,1s0.4,1,1,1h3.2 C9.4,7.3,9.2,7.7,9.1,8C9.1,8,9,8,9,8H6.5c-0.6,0-1,0.4-1,1s0.4,1,1,1H9v3c-0.1,0-0.1,0-0.2,0H6.1c-0.6,0-1,0.4-1,1s0.4,1,1,1h2.7 c0.1,0,0.1,0,0.2,0V16c-0.1,0-0.1,0-0.2,0H6.1c-0.6,0-1,0.4-1,1c0,0.6,0.4,1,1,1h2.7c0.1,0,0.1,0,0.2,0V19c-0.1,0-0.1,0-0.2,0H6.1 c-0.6,0-1,0.4-1,1s0.4,1,1,1h2.7c0.1,0,0.1,0,0.2,0v3H5z M27,27c0,1.1-0.9,2-2,2H13c-1.1,0-2-0.9-2-2V9c0-1.1,0.9-2,2-2h0.7H25 c1.1,0,2,0.9,2,2V27z M25.2,19c0,0.6-0.4,1-1,1H13.4c-0.6,0-1-0.4-1-1s0.4-1,1-1h10.7C24.7,18,25.2,18.4,25.2,19z M25.2,22 c0,0.6-0.4,1-1,1H13.4c-0.6,0-1-0.4-1-1s0.4-1,1-1h10.7C24.7,21,25.2,21.4,25.2,22z M25.2,25c0,0.6-0.4,1-1,1H13.4c-0.6,0-1-0.4-1-1 s0.4-1,1-1h10.7C24.7,24,25.2,24.4,25.2,25z M12.3,11c0-0.6,0.4-1,1-1h7.3c0.6,0,1,0.4,1,1s-0.4,1-1,1h-7.3 C12.8,12,12.3,11.6,12.3,11z M16,13c0.6,0,1,0.4,1,1s-0.4,1-1,1h-2.5c-0.6,0-1-0.4-1-1s0.4-1,1-1H16z" /></svg>;"`;

exports[`hast-util-to-babel-ast transforms SVG 1`] = `"<svg width="88px" height="88px" viewBox="0 0 88 88" version={1.1} xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink"><title>{"Dismiss"}</title><desc>{"Created with Sketch."}</desc><defs /><g id="Blocks" stroke="none" strokeWidth={1} fill="none" fillRule="evenodd" strokeLinecap="square"><g id="Dismiss" stroke="#063855" strokeWidth={2}><path d="M51,37 L37,51" id="Shape" /><path d="M51,51 L37,37" id="Shape" /></g></g></svg>;"`;

exports[`hast-util-to-babel-ast transforms SVG with custom attribute transformer 1`] = `"<svg width="88px" height="88px" viewBox="0 0 88 88" version={1.1} xmlns="http://www.w3.org/2000/svg" xmlnsXlink="http://www.w3.org/1999/xlink"><title>{"Dismiss"}</title><desc>{"Created with Sketch."}</desc><defs /><g id="Blocks" stroke="none" stroke-width={1} fill="none" fill-rule="evenodd" stroke-linecap="square"><g id="Dismiss" stroke="#063855" stroke-width={2}><path d="M51,37 L37,51" id="Shape" /><path d="M51,51 L37,37" id="Shape" /></g></g></svg>;"`;

exports[`hast-util-to-babel-ast transforms SVG without transforming attributes 1`] = `"<svg width="88px" height="88px" viewBox="0 0 88 88" version={1.1} xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>{"Dismiss"}</title><desc>{"Created with Sketch."}</desc><defs /><g id="Blocks" stroke="none" stroke-width={1} fill="none" fill-rule="evenodd" stroke-linecap="square"><g id="Dismiss" stroke="#063855" stroke-width={2}><path d="M51,37 L37,51" id="Shape" /><path d="M51,51 L37,37" id="Shape" /></g></g></svg>;"`;
17 changes: 17 additions & 0 deletions packages/hast-util-to-babel-ast/src/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export type TransformAttributes = boolean | ((key: string) => string)

export interface Configuration {
transformAttributes: TransformAttributes
}

export const getConfig = <K extends keyof Configuration>(
config: Partial<Configuration> = {},
key: K,
): Configuration[K] => {
switch (key) {
case 'transformAttributes':
return config.transformAttributes ?? true
default:
throw new Error(`Unknown option ${key}`)
}
}
23 changes: 20 additions & 3 deletions packages/hast-util-to-babel-ast/src/getAttributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,24 @@ import type { ElementNode } from 'svg-parser'
import { isNumeric, kebabCase, replaceSpaces } from './util'
import { stringToObjectStyle } from './stringToObjectStyle'
import { ATTRIBUTE_MAPPING, ELEMENT_ATTRIBUTE_MAPPING } from './mappings'
import type { TransformAttributes } from './configuration'

const convertAriaAttribute = (kebabKey: string) => {
const [aria, ...parts] = kebabKey.split('-')
return `${aria}-${parts.join('').toLowerCase()}`
}

const getKey = (key: string, node: ElementNode) => {
const getKey = (
key: string,
node: ElementNode,
transformAttributes: TransformAttributes,
) => {
if (!transformAttributes) return t.jsxIdentifier(key)

if (typeof transformAttributes === 'function') {
return t.jsxIdentifier(transformAttributes(key))
}

const lowerCaseKey = key.toLowerCase()
const mappedElementAttribute =
// @ts-ignore
Expand Down Expand Up @@ -53,7 +64,10 @@ const getValue = (key: string, value: string[] | string | number) => {
return t.stringLiteral(replaceSpaces(value))
}

export const getAttributes = (node: ElementNode): t.JSXAttribute[] => {
export const getAttributes = (
node: ElementNode,
transformAttributes: TransformAttributes,
): t.JSXAttribute[] => {
if (!node.properties) return []
const keys = Object.keys(node.properties)
const attributes = []
Expand All @@ -62,7 +76,10 @@ export const getAttributes = (node: ElementNode): t.JSXAttribute[] => {
while (++index < keys.length) {
const key = keys[index]
const value = node.properties[key]
const attribute = t.jsxAttribute(getKey(key, node), getValue(key, value))
const attribute = t.jsxAttribute(
getKey(key, node, transformAttributes),
getValue(key, value),
)
attributes.push(attribute)
}

Expand Down
4 changes: 2 additions & 2 deletions packages/hast-util-to-babel-ast/src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getAttributes } from './getAttributes'
import { ELEMENT_TAG_NAME_MAPPING } from './mappings'
import type { RootNode, ElementNode, TextNode } from 'svg-parser'
import type { Helpers } from './helpers'
import { getConfig } from './configuration'

export const root = (h: Helpers, node: RootNode): t.Program =>
// @ts-ignore
Expand Down Expand Up @@ -44,15 +45,14 @@ export const element = (
parent: RootNode | ElementNode,
): t.JSXElement | t.ExpressionStatement | null => {
if (!node.tagName) return null

const children = all(h, node)
const selfClosing = children.length === 0

const name = ELEMENT_TAG_NAME_MAPPING[node.tagName] || node.tagName

const openingElement = t.jsxOpeningElement(
t.jsxIdentifier(name),
getAttributes(node),
getAttributes(node, getConfig(h.config, 'transformAttributes')),
selfClosing,
)

Expand Down
3 changes: 2 additions & 1 deletion packages/hast-util-to-babel-ast/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Configuration } from './configuration'
import * as handlers from './handlers'

export const helpers = { handlers }
export const helpers = { handlers, config: {} as Partial<Configuration> }

export type Helpers = typeof helpers
50 changes: 48 additions & 2 deletions packages/hast-util-to-babel-ast/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { parse } from 'svg-parser'
import generate from '@babel/generator'
import hastToBabelAst from './index'
import type { Configuration } from './configuration'

function transform(code: string) {
function transform(code: string, config: Partial<Configuration> = {}) {
const hastTree = parse(code)

const babelTree = hastToBabelAst(hastTree)
const babelTree = hastToBabelAst(hastTree, config)

const { code: generatedCode } = generate(babelTree)

Expand All @@ -32,6 +33,51 @@ describe('hast-util-to-babel-ast', () => {
expect(transform(code)).toMatchSnapshot()
})

it('transforms SVG without transforming attributes', () => {
const code = `
<?xml version="1.0" encoding="UTF-8"?>
<svg width="88px" height="88px" viewBox="0 0 88 88" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
<title>Dismiss</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Blocks" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="square">
<g id="Dismiss" stroke="#063855" stroke-width="2">
<path d="M51,37 L37,51" id="Shape"></path>
<path d="M51,51 L37,37" id="Shape"></path>
</g>
</g>
</svg>
`
expect(transform(code, { transformAttributes: false })).toMatchSnapshot()
})

it('transforms SVG with custom attribute transformer', () => {
const code = `
<?xml version="1.0" encoding="UTF-8"?>
<svg width="88px" height="88px" viewBox="0 0 88 88" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 46.2 (44496) - http://www.bohemiancoding.com/sketch -->
<title>Dismiss</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Blocks" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="square">
<g id="Dismiss" stroke="#063855" stroke-width="2">
<path d="M51,37 L37,51" id="Shape"></path>
<path d="M51,51 L37,37" id="Shape"></path>
</g>
</g>
</svg>
`
expect(
transform(code, {
transformAttributes: (key: string) =>
key.includes(':')
? key.replace(/[^a-z0-9]([a-z])/, (_, v) => v.toUpperCase())
: key,
}),
).toMatchSnapshot()
})

it('transforms "aria-x"', () => {
const code = `<svg aria-hidden="true"></svg>`
expect(transform(code)).toMatchInlineSnapshot(
Expand Down
6 changes: 5 additions & 1 deletion packages/hast-util-to-babel-ast/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import type { RootNode } from 'svg-parser'
import type * as t from '@babel/types'
import { root } from './handlers'
import { helpers } from './helpers'
import type { Configuration } from './configuration'

const toBabelAST = (tree: RootNode): t.Program => root(helpers, tree)
const toBabelAST = (
tree: RootNode,
config: Partial<Configuration> = {},
): t.Program => root({ ...helpers, config }, tree)

export default toBabelAST
34 changes: 34 additions & 0 deletions packages/plugin-jsx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,40 @@ npm install --save-dev @svgr/plugin-jsx
- Converting the [HAST](https://github.com/syntax-tree/hast) into a [Babel AST](https://github.com/babel/babel/blob/master/packages/babel-parser/ast/spec.md)
- Applying [`@svgr/babel-preset`](../babel-preset/README.md) transformations

## Skipping attribute transformations

For non-React implementations such as Preact, you can pass `false` to `jsx.transformAttributes`
and attributes will remain in their original form. If `jsx.transformAttributes` is `true` (the
default value), attributes will be transformed to their React equivalents—usually camelCase.
```js
// .svgrrc.js

module.exports = {
jsx: {
transformAttributes: false,
},
}
```

A function can also be passed to `transformAttributes`, which will be used _instead of_ the
default logic. It will be called with the attribute to transform and expect the transformed
attribute as a return value:

```js
// .svgrrc.js

module.exports = {
jsx: {
transformAttributes: (attribute) => {
if (attribute === 'fill') {
return 'currentColor'
}
return attribute
},
},
}
```

## Applying custom transformations

You can extend the Babel config applied in this plugin using `jsx.babelConfig` config path:
Expand Down
4 changes: 3 additions & 1 deletion packages/plugin-jsx/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ const jsxPlugin: Plugin = (code, config, state) => {
const filePath = state.filePath || 'unknown'
const hastTree = parse(code)

const babelTree = hastToBabelAst(hastTree)
const babelTree = hastToBabelAst(hastTree, {
transformAttributes: config.jsx?.transformAttributes ?? true,
})

const svgPresetOptions: SvgrPresetOptions = {
ref: config.ref,
Expand Down