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

feat(descProp): add descProp option #729

Merged
merged 23 commits into from
Jun 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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