Skip to content

Commit a0637d4

Browse files
authored
feat: add descProp option (#729)
1 parent 846cd20 commit a0637d4

File tree

20 files changed

+534
-63
lines changed

20 files changed

+534
-63
lines changed

packages/babel-plugin-svg-dynamic-title/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ npm install --save-dev @svgr/babel-plugin-svg-dynamic-title
1616
}
1717
```
1818

19+
## Note
20+
21+
This plugin handles both the titleProp and descProp options. By default, it will handle titleProp only.
22+
1923
## License
2024

2125
MIT

packages/babel-plugin-svg-dynamic-title/src/index.test.ts

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { transform } from '@babel/core'
2-
import plugin from '.'
2+
import plugin, { Options } from '.'
33

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

1010
return result?.code
1111
}
1212

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

2020
it('should add title element and fallback to existing title', () => {
2121
// testing when the existing title contains a simple string
22-
expect(testPlugin(`<svg><title>Hello</title></svg>`)).toMatchInlineSnapshot(
22+
expect(
23+
testPlugin(`<svg><title>Hello</title></svg>`),
24+
).toMatchInlineSnapshot(
2325
`"<svg>{title === undefined ? <title id={titleId}>Hello</title> : title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
2426
)
2527
// testing when the existing title contains an JSXExpression
@@ -38,19 +40,78 @@ describe('plugin', () => {
3840
)
3941
})
4042
it('should support empty title', () => {
41-
expect(testPlugin('<svg><title></title></svg>')).toMatchInlineSnapshot(
43+
expect(
44+
testPlugin('<svg><title></title></svg>'),
45+
).toMatchInlineSnapshot(
4246
`"<svg>{title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
4347
)
4448
})
4549
it('should support self closing title', () => {
46-
expect(testPlugin('<svg><title /></svg>')).toMatchInlineSnapshot(
50+
expect(
51+
testPlugin('<svg><title /></svg>'),
52+
).toMatchInlineSnapshot(
4753
`"<svg>{title ? <title id={titleId}>{title}</title> : null}</svg>;"`,
4854
)
4955
})
5056

5157
it('should work if an attribute is already present', () => {
52-
expect(testPlugin('<svg><foo /></svg>')).toMatchInlineSnapshot(
58+
expect(
59+
testPlugin('<svg><foo /></svg>'),
60+
).toMatchInlineSnapshot(
5361
`"<svg>{title ? <title id={titleId}>{title}</title> : null}<foo /></svg>;"`,
5462
)
5563
})
5664
})
65+
66+
describe('desc plugin', () => {
67+
it('should add desc attribute if not present', () => {
68+
expect(testPlugin('<svg></svg>', { tag: 'desc' })).toMatchInlineSnapshot(
69+
`"<svg>{desc ? <desc id={descId}>{desc}</desc> : null}</svg>;"`,
70+
)
71+
})
72+
73+
it('should add desc element and fallback to existing desc', () => {
74+
// testing when the existing desc contains a simple string
75+
expect(
76+
testPlugin(`<svg><desc>Hello</desc></svg>`, { tag: 'desc' }),
77+
).toMatchInlineSnapshot(
78+
`"<svg>{desc === undefined ? <desc id={descId}>Hello</desc> : desc ? <desc id={descId}>{desc}</desc> : null}</svg>;"`,
79+
)
80+
// testing when the existing desc contains an JSXExpression
81+
expect(
82+
testPlugin(`<svg><desc>{"Hello"}</desc></svg>`, { tag: 'desc' }),
83+
).toMatchInlineSnapshot(
84+
`"<svg>{desc === undefined ? <desc id={descId}>{\\"Hello\\"}</desc> : desc ? <desc id={descId}>{desc}</desc> : null}</svg>;"`,
85+
)
86+
})
87+
it('should preserve any existing desc attributes', () => {
88+
// testing when the existing desc contains a simple string
89+
expect(
90+
testPlugin(`<svg><desc id='a'>Hello</desc></svg>`, { tag: 'desc' }),
91+
).toMatchInlineSnapshot(
92+
`"<svg>{desc === undefined ? <desc id={descId || 'a'}>Hello</desc> : desc ? <desc id={descId || 'a'}>{desc}</desc> : null}</svg>;"`,
93+
)
94+
})
95+
it('should support empty desc', () => {
96+
expect(
97+
testPlugin('<svg><desc></desc></svg>', { tag: 'desc' }),
98+
).toMatchInlineSnapshot(
99+
`"<svg>{desc ? <desc id={descId}>{desc}</desc> : null}</svg>;"`,
100+
)
101+
})
102+
it('should support self closing desc', () => {
103+
expect(
104+
testPlugin('<svg><desc /></svg>', { tag: 'desc' }),
105+
).toMatchInlineSnapshot(
106+
`"<svg>{desc ? <desc id={descId}>{desc}</desc> : null}</svg>;"`,
107+
)
108+
})
109+
110+
it('should work if an attribute is already present', () => {
111+
expect(
112+
testPlugin('<svg><foo /></svg>', { tag: 'desc' }),
113+
).toMatchInlineSnapshot(
114+
`"<svg>{desc ? <desc id={descId}>{desc}</desc> : null}<foo /></svg>;"`,
115+
)
116+
})
117+
})

packages/babel-plugin-svg-dynamic-title/src/index.ts

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,45 +3,58 @@ import { NodePath, types as t } from '@babel/core'
33

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

6-
const createTitleElement = (
6+
type tag = 'title' | 'desc'
7+
8+
export interface Options {
9+
tag: tag | null
10+
}
11+
12+
interface State {
13+
opts: Options
14+
}
15+
16+
const createTagElement = (
17+
tag: tag,
718
children: t.JSXExpressionContainer[] = [],
819
attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[] = [],
920
) => {
10-
const title = t.jsxIdentifier('title')
21+
const eleName = t.jsxIdentifier(tag)
1122
return t.jsxElement(
12-
t.jsxOpeningElement(title, attributes),
13-
t.jsxClosingElement(title),
23+
t.jsxOpeningElement(eleName, attributes),
24+
t.jsxClosingElement(eleName),
1425
children,
1526
)
1627
}
1728

18-
const createTitleIdAttribute = () =>
29+
const createTagIdAttribute = (tag: tag) =>
1930
t.jsxAttribute(
2031
t.jsxIdentifier('id'),
21-
t.jsxExpressionContainer(t.identifier('titleId')),
32+
t.jsxExpressionContainer(t.identifier(`${tag}Id`)),
2233
)
2334

24-
const addTitleIdAttribute = (
35+
const addTagIdAttribute = (
36+
tag: tag,
2537
attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[],
2638
) => {
2739
const existingId = attributes.find(
2840
(attribute) => t.isJSXAttribute(attribute) && attribute.name.name === 'id',
2941
) as t.JSXAttribute | undefined
3042

3143
if (!existingId) {
32-
return [...attributes, createTitleIdAttribute()]
44+
return [...attributes, createTagIdAttribute(tag)]
3345
}
3446
existingId.value = t.jsxExpressionContainer(
3547
t.isStringLiteral(existingId.value)
36-
? t.logicalExpression('||', t.identifier('titleId'), existingId.value)
37-
: t.identifier('titleId'),
48+
? t.logicalExpression('||', t.identifier(`${tag}Id`), existingId.value)
49+
: t.identifier(`${tag}Id`),
3850
)
3951
return attributes
4052
}
4153

4254
const plugin = () => ({
4355
visitor: {
44-
JSXElement(path: NodePath<t.JSXElement>) {
56+
JSXElement(path: NodePath<t.JSXElement>, state: State) {
57+
const tag = state.opts.tag || 'title'
4558
if (!elements.length) return
4659

4760
const openingElement = path.get('openingElement')
@@ -54,22 +67,24 @@ const plugin = () => ({
5467
return
5568
}
5669

57-
const getTitleElement = (
70+
const getTagElement = (
5871
existingTitle?: t.JSXElement,
5972
): t.JSXExpressionContainer => {
60-
const titleExpression = t.identifier('title')
73+
const tagExpression = t.identifier(tag)
6174
if (existingTitle) {
62-
existingTitle.openingElement.attributes = addTitleIdAttribute(
75+
existingTitle.openingElement.attributes = addTagIdAttribute(
76+
tag,
6377
existingTitle.openingElement.attributes,
6478
)
6579
}
6680
const conditionalTitle = t.conditionalExpression(
67-
titleExpression,
68-
createTitleElement(
69-
[t.jsxExpressionContainer(titleExpression)],
81+
tagExpression,
82+
createTagElement(
83+
tag,
84+
[t.jsxExpressionContainer(tagExpression)],
7085
existingTitle
7186
? existingTitle.openingElement.attributes
72-
: [createTitleIdAttribute()],
87+
: [createTagIdAttribute(tag)],
7388
),
7489
t.nullLiteral(),
7590
)
@@ -80,7 +95,7 @@ const plugin = () => ({
8095
t.conditionalExpression(
8196
t.binaryExpression(
8297
'===',
83-
titleExpression,
98+
tagExpression,
8499
t.identifier('undefined'),
85100
),
86101
existingTitle,
@@ -92,25 +107,25 @@ const plugin = () => ({
92107
}
93108

94109
// store the title element
95-
let titleElement: t.JSXExpressionContainer | null = null
110+
let tagElement: t.JSXExpressionContainer | null = null
96111

97112
const hasTitle = path.get('children').some((childPath) => {
98-
if (childPath.node === titleElement) return false
113+
if (childPath.node === tagElement) return false
99114
if (!childPath.isJSXElement()) return false
100115
const name = childPath.get('openingElement').get('name')
101116
if (!name.isJSXIdentifier()) return false
102-
if (name.node.name !== 'title') return false
103-
titleElement = getTitleElement(childPath.node)
104-
childPath.replaceWith(titleElement)
117+
if (name.node.name !== tag) return false
118+
tagElement = getTagElement(childPath.node)
119+
childPath.replaceWith(tagElement)
105120
return true
106121
})
107122

108123
// create a title element if not already create
109-
titleElement = titleElement || getTitleElement()
124+
tagElement = tagElement || getTagElement()
110125
if (!hasTitle) {
111126
// path.unshiftContainer is not working well :(
112127
// path.unshiftContainer('children', titleElement)
113-
path.node.children.unshift(titleElement)
128+
path.node.children.unshift(tagElement)
114129
path.replaceWith(path.node)
115130
}
116131
},

0 commit comments

Comments
 (0)