Skip to content

Commit

Permalink
feat: add descProp option (#729)
Browse files Browse the repository at this point in the history
  • Loading branch information
Aveline-art authored Jun 19, 2022
1 parent 846cd20 commit a0637d4
Show file tree
Hide file tree
Showing 20 changed files with 534 additions and 63 deletions.
4 changes: 4 additions & 0 deletions packages/babel-plugin-svg-dynamic-title/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ npm install --save-dev @svgr/babel-plugin-svg-dynamic-title
}
```

## Note

This plugin handles both the titleProp and descProp options. By default, it will handle titleProp only.

## License

MIT
77 changes: 69 additions & 8 deletions packages/babel-plugin-svg-dynamic-title/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { transform } from '@babel/core'
import plugin from '.'
import plugin, { Options } from '.'

const testPlugin = (code: string) => {
const testPlugin = (code: string, options: Options = {tag: 'title'}) => {
const result = transform(code, {
plugins: ['@babel/plugin-syntax-jsx', plugin],
plugins: ['@babel/plugin-syntax-jsx', [plugin, options]],
configFile: false,
})

return result?.code
}

describe('plugin', () => {
describe('title plugin', () => {
it('should add title attribute if not present', () => {
expect(testPlugin('<svg></svg>')).toMatchInlineSnapshot(
`"<svg>{title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
Expand All @@ -19,7 +19,9 @@ describe('plugin', () => {

it('should add title element and fallback to existing title', () => {
// testing when the existing title contains a simple string
expect(testPlugin(`<svg><title>Hello</title></svg>`)).toMatchInlineSnapshot(
expect(
testPlugin(`<svg><title>Hello</title></svg>`),
).toMatchInlineSnapshot(
`"<svg>{title === undefined ? <title id={titleId}>Hello</title> : title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
)
// testing when the existing title contains an JSXExpression
Expand All @@ -38,19 +40,78 @@ describe('plugin', () => {
)
})
it('should support empty title', () => {
expect(testPlugin('<svg><title></title></svg>')).toMatchInlineSnapshot(
expect(
testPlugin('<svg><title></title></svg>'),
).toMatchInlineSnapshot(
`"<svg>{title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
)
})
it('should support self closing title', () => {
expect(testPlugin('<svg><title /></svg>')).toMatchInlineSnapshot(
expect(
testPlugin('<svg><title /></svg>'),
).toMatchInlineSnapshot(
`"<svg>{title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
)
})

it('should work if an attribute is already present', () => {
expect(testPlugin('<svg><foo /></svg>')).toMatchInlineSnapshot(
expect(
testPlugin('<svg><foo /></svg>'),
).toMatchInlineSnapshot(
`"<svg>{title ? <title id={titleId}>{title}</title> : null}<foo /></svg>;"`,
)
})
})

describe('desc plugin', () => {
it('should add desc attribute if not present', () => {
expect(testPlugin('<svg></svg>', { tag: 'desc' })).toMatchInlineSnapshot(
`"<svg>{desc ? <desc id={descId}>{desc}</desc> : null}</svg>;"`,
)
})

it('should add desc element and fallback to existing desc', () => {
// testing when the existing desc contains a simple string
expect(
testPlugin(`<svg><desc>Hello</desc></svg>`, { tag: 'desc' }),
).toMatchInlineSnapshot(
`"<svg>{desc === undefined ? <desc id={descId}>Hello</desc> : desc ? <desc id={descId}>{desc}</desc> : null}</svg>;"`,
)
// testing when the existing desc contains an JSXExpression
expect(
testPlugin(`<svg><desc>{"Hello"}</desc></svg>`, { tag: 'desc' }),
).toMatchInlineSnapshot(
`"<svg>{desc === undefined ? <desc id={descId}>{\\"Hello\\"}</desc> : desc ? <desc id={descId}>{desc}</desc> : null}</svg>;"`,
)
})
it('should preserve any existing desc attributes', () => {
// testing when the existing desc contains a simple string
expect(
testPlugin(`<svg><desc id='a'>Hello</desc></svg>`, { tag: 'desc' }),
).toMatchInlineSnapshot(
`"<svg>{desc === undefined ? <desc id={descId || 'a'}>Hello</desc> : desc ? <desc id={descId || 'a'}>{desc}</desc> : null}</svg>;"`,
)
})
it('should support empty desc', () => {
expect(
testPlugin('<svg><desc></desc></svg>', { tag: 'desc' }),
).toMatchInlineSnapshot(
`"<svg>{desc ? <desc id={descId}>{desc}</desc> : null}</svg>;"`,
)
})
it('should support self closing desc', () => {
expect(
testPlugin('<svg><desc /></svg>', { tag: 'desc' }),
).toMatchInlineSnapshot(
`"<svg>{desc ? <desc id={descId}>{desc}</desc> : null}</svg>;"`,
)
})

it('should work if an attribute is already present', () => {
expect(
testPlugin('<svg><foo /></svg>', { tag: 'desc' }),
).toMatchInlineSnapshot(
`"<svg>{desc ? <desc id={descId}>{desc}</desc> : null}<foo /></svg>;"`,
)
})
})
67 changes: 41 additions & 26 deletions packages/babel-plugin-svg-dynamic-title/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,58 @@ import { NodePath, types as t } from '@babel/core'

const elements = ['svg', 'Svg']

const createTitleElement = (
type tag = 'title' | 'desc'

export interface Options {
tag: tag | null
}

interface State {
opts: Options
}

const createTagElement = (
tag: tag,
children: t.JSXExpressionContainer[] = [],
attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[] = [],
) => {
const title = t.jsxIdentifier('title')
const eleName = t.jsxIdentifier(tag)
return t.jsxElement(
t.jsxOpeningElement(title, attributes),
t.jsxClosingElement(title),
t.jsxOpeningElement(eleName, attributes),
t.jsxClosingElement(eleName),
children,
)
}

const createTitleIdAttribute = () =>
const createTagIdAttribute = (tag: tag) =>
t.jsxAttribute(
t.jsxIdentifier('id'),
t.jsxExpressionContainer(t.identifier('titleId')),
t.jsxExpressionContainer(t.identifier(`${tag}Id`)),
)

const addTitleIdAttribute = (
const addTagIdAttribute = (
tag: tag,
attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[],
) => {
const existingId = attributes.find(
(attribute) => t.isJSXAttribute(attribute) && attribute.name.name === 'id',
) as t.JSXAttribute | undefined

if (!existingId) {
return [...attributes, createTitleIdAttribute()]
return [...attributes, createTagIdAttribute(tag)]
}
existingId.value = t.jsxExpressionContainer(
t.isStringLiteral(existingId.value)
? t.logicalExpression('||', t.identifier('titleId'), existingId.value)
: t.identifier('titleId'),
? t.logicalExpression('||', t.identifier(`${tag}Id`), existingId.value)
: t.identifier(`${tag}Id`),
)
return attributes
}

const plugin = () => ({
visitor: {
JSXElement(path: NodePath<t.JSXElement>) {
JSXElement(path: NodePath<t.JSXElement>, state: State) {
const tag = state.opts.tag || 'title'
if (!elements.length) return

const openingElement = path.get('openingElement')
Expand All @@ -54,22 +67,24 @@ const plugin = () => ({
return
}

const getTitleElement = (
const getTagElement = (
existingTitle?: t.JSXElement,
): t.JSXExpressionContainer => {
const titleExpression = t.identifier('title')
const tagExpression = t.identifier(tag)
if (existingTitle) {
existingTitle.openingElement.attributes = addTitleIdAttribute(
existingTitle.openingElement.attributes = addTagIdAttribute(
tag,
existingTitle.openingElement.attributes,
)
}
const conditionalTitle = t.conditionalExpression(
titleExpression,
createTitleElement(
[t.jsxExpressionContainer(titleExpression)],
tagExpression,
createTagElement(
tag,
[t.jsxExpressionContainer(tagExpression)],
existingTitle
? existingTitle.openingElement.attributes
: [createTitleIdAttribute()],
: [createTagIdAttribute(tag)],
),
t.nullLiteral(),
)
Expand All @@ -80,7 +95,7 @@ const plugin = () => ({
t.conditionalExpression(
t.binaryExpression(
'===',
titleExpression,
tagExpression,
t.identifier('undefined'),
),
existingTitle,
Expand All @@ -92,25 +107,25 @@ const plugin = () => ({
}

// store the title element
let titleElement: t.JSXExpressionContainer | null = null
let tagElement: t.JSXExpressionContainer | null = null

const hasTitle = path.get('children').some((childPath) => {
if (childPath.node === titleElement) return false
if (childPath.node === tagElement) return false
if (!childPath.isJSXElement()) return false
const name = childPath.get('openingElement').get('name')
if (!name.isJSXIdentifier()) return false
if (name.node.name !== 'title') return false
titleElement = getTitleElement(childPath.node)
childPath.replaceWith(titleElement)
if (name.node.name !== tag) return false
tagElement = getTagElement(childPath.node)
childPath.replaceWith(tagElement)
return true
})

// create a title element if not already create
titleElement = titleElement || getTitleElement()
tagElement = tagElement || getTagElement()
if (!hasTitle) {
// path.unshiftContainer is not working well :(
// path.unshiftContainer('children', titleElement)
path.node.children.unshift(titleElement)
path.node.children.unshift(tagElement)
path.replaceWith(path.node)
}
},
Expand Down
Loading

1 comment on commit a0637d4

@vercel
Copy link

@vercel vercel bot commented on a0637d4 Jun 19, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

svgr – ./

svgr-gregberge.vercel.app
svgr-git-main-gregberge.vercel.app
api.react-svgr.com

Please sign in to comment.