From 9657110979ceeb60f18755ffe4eece5221a9044d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Sun, 16 Sep 2018 14:46:43 +0000 Subject: [PATCH] feat: new option "svgProps" (#172) Allow to pass dynamic svg props: In this diff I introduce the new option `svgProps` which is similar to svgAttributes but applies values in interpolation style `{true}`. This allows to specify expressions depending on template values for example default `currentColor` in `fill` prop. `svgAttributes` is now deprecated and will be removed in v3. --- README.md | 21 +-- packages/cli/README.md | 47 +++---- .../cli/src/__snapshots__/index.test.js.snap | 14 ++ packages/cli/src/index.js | 17 ++- packages/cli/src/index.test.js | 1 + packages/core/src/config.js | 1 + packages/core/src/h2x/svgAttributes.js | 12 +- packages/core/src/h2x/svgProps.js | 69 ++++++++++ packages/core/src/h2x/svgProps.test.js | 126 ++++++++++++++++++ packages/core/src/index.js | 1 + packages/core/src/plugins/h2x.js | 2 + 11 files changed, 272 insertions(+), 39 deletions(-) create mode 100644 packages/core/src/h2x/svgProps.js create mode 100644 packages/core/src/h2x/svgProps.test.js diff --git a/README.md b/README.md index 485d7a22..12cc729f 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ powerful and configurable HTML transpiler. It uses AST (like ## Command line usage ``` -Usage: svgr [options] +Usage: svgr [options] Options: @@ -94,7 +94,8 @@ Options: --ref add svgRef prop to svg --no-dimensions remove width and height from root SVG tag --no-expand-props disable props expanding - --svg-attributes add some attributes to the svg + --svg-attributes add attributes to the svg element (deprecated) + --svg-props add props to the svg element --replace-attr-values replace an attribute value --template specify a custom template to use --title-prop create a title element linked with props @@ -104,8 +105,8 @@ Options: --no-svgo disable SVGO -h, --help output usage information -Examples: - svgr --replace-attr-values "#fff=currentColor" icon.svg + Examples: + svgr --replace-attr-values "#fff=currentColor" icon.svg ``` ### Recipes @@ -344,13 +345,15 @@ change an icon color to "currentColor" in order to inherit from text color. | ------- | --------------------------------- | ------------------------------------ | | `[]` | `--replace-attr-values ` | `replaceAttrValues: { old: 'new' }>` | -### SVG attributes +### SVG props -Add attributes to the root SVG tag. +Add props to the root SVG tag. -| Default | CLI Override | API Override | -| ------- | ------------------------------- | ----------------------------------- | -| `[]` | `--svg-attributes ` | `svgAttributes: { name: 'value' }>` | +| Default | CLI Override | API Override | +| ------- | -------------------------- | ------------------------------ | +| `[]` | `--svg-props ` | `svgProps: { name: 'value' }>` | + +> You can specify dynamic property using curly braces: `{ focusable: "{true}" }` or `--svg-props focusable={true}`. It is particulary useful with a custom template. ### Template diff --git a/packages/cli/README.md b/packages/cli/README.md index 46541731..95d84614 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -13,29 +13,30 @@ npm install @svgr/cli ## Usage ``` - Usage: index [options] - - Options: - - -V, --version output the version number - --config specify the path of the svgr config - -d, --out-dir output files into a directory - --ext specify a custom file extension (default: "js") - --filename-case specify filename case (pascal, kebab, camel) (default: "pascal") - --icon use "1em" as width and height - --native add react-native support with react-native-svg - --ref add svgRef prop to svg - --no-dimensions remove width and height from root SVG tag - --no-expand-props disable props expanding - --svg-attributes add some attributes to the svg - --replace-attr-values replace an attribute value - --template specify a custom template to use - --title-prop create a title element linked with props - --prettier-config Prettier config - --no-prettier disable Prettier - --svgo-config SVGO config - --no-svgo disable SVGO - -h, --help output usage information +Usage: svgr [options] + +Options: + + -V, --version output the version number + --config specify the path of the svgr config + -d, --out-dir output files into a directory + --ext specify a custom file extension (default: "js") + --filename-case specify filename case (pascal, kebab, camel) (default: "pascal") + --icon use "1em" as width and height + --native add react-native support with react-native-svg + --ref add svgRef prop to svg + --no-dimensions remove width and height from root SVG tag + --no-expand-props disable props expanding + --svg-attributes add attributes to the svg element (deprecated) + --svg-props add props to the svg element + --replace-attr-values replace an attribute value + --template specify a custom template to use + --title-prop create a title element linked with props + --prettier-config Prettier config + --no-prettier disable Prettier + --svgo-config SVGO config + --no-svgo disable SVGO + -h, --help output usage information Examples: svgr --replace-attr-values "#fff=currentColor" icon.svg diff --git a/packages/cli/src/__snapshots__/index.test.js.snap b/packages/cli/src/__snapshots__/index.test.js.snap index ec3f570e..77f54221 100644 --- a/packages/cli/src/__snapshots__/index.test.js.snap +++ b/packages/cli/src/__snapshots__/index.test.js.snap @@ -292,6 +292,20 @@ export default File " `; +exports[`cli should support various args: --svg-props "hidden={true}" 1`] = ` +"import React from 'react' + +const File = props => ( + +) + +export default File + +" +`; + exports[`cli should support various args: --title-prop 1`] = ` "import React from 'react' diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index 66503db5..72bf4720 100644 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -3,6 +3,7 @@ import program from 'commander' import path from 'path' import glob from 'glob' import fs from 'fs' +import chalk from 'chalk' import pkg from '../package.json' import fileCommand from './fileCommand' import dirCommand from './dirCommand' @@ -51,7 +52,12 @@ program .option('--no-expand-props', 'disable props expanding') .option( '--svg-attributes ', - 'add some attributes to the svg', + 'add attributes to the svg element (deprecated)', + parseObject, + ) + .option( + '--svg-props ', + 'add props to the svg element', parseObject, ) .option( @@ -123,6 +129,15 @@ async function run() { } } + // TODO remove in v3 + if (program.outDir && program.svgAttributes) { + console.log( + chalk.yellow( + '--svg-attributes option is deprecated an will be removed in v3, please use --svg-props instead', + ), + ) + } + const command = program.outDir ? dirCommand : fileCommand await command(program, filenames, config) } diff --git a/packages/cli/src/index.test.js b/packages/cli/src/index.test.js index 77932e50..90afd345 100644 --- a/packages/cli/src/index.test.js +++ b/packages/cli/src/index.test.js @@ -127,6 +127,7 @@ describe('cli', () => { ['--ref'], ['--replace-attr-values "#063855=currentColor"'], ['--svg-attributes "focusable=false"'], + [`--svg-props "hidden={true}"`], ['--no-svgo'], ['--no-prettier'], ['--title-prop'], diff --git a/packages/core/src/config.js b/packages/core/src/config.js index 381ca32b..34da6a46 100644 --- a/packages/core/src/config.js +++ b/packages/core/src/config.js @@ -11,6 +11,7 @@ export const DEFAULT_CONFIG = { ref: false, replaceAttrValues: null, svgAttributes: null, + svgProps: null, svgo: true, svgoConfig: null, template: null, diff --git a/packages/core/src/h2x/svgAttributes.js b/packages/core/src/h2x/svgAttributes.js index 7ae948f8..11d6d24f 100644 --- a/packages/core/src/h2x/svgAttributes.js +++ b/packages/core/src/h2x/svgAttributes.js @@ -16,7 +16,7 @@ const areAttrsAlreadyInjected = (node, attributes = {}) => { }, true) } -const svgAttribute = (attributes = {}) => () => { +const svgAttributes = (attributes = {}) => () => { const keys = Object.keys(attributes) return { @@ -27,10 +27,10 @@ const svgAttribute = (attributes = {}) => () => { if (areAttrsAlreadyInjected(path.node, attributes)) return const parseAttributes = keys.reduce((accumulation, key) => { - const props = new JSXAttribute() - props.name = key - props.value = attributes[key] - return [...accumulation, props] + const prop = new JSXAttribute() + prop.name = key + prop.value = attributes[key] + return [...accumulation, prop] }, []) const mergeAttributes = path.node.attributes.reduce( @@ -50,4 +50,4 @@ const svgAttribute = (attributes = {}) => () => { } } -export default svgAttribute +export default svgAttributes diff --git a/packages/core/src/h2x/svgProps.js b/packages/core/src/h2x/svgProps.js new file mode 100644 index 00000000..e5446655 --- /dev/null +++ b/packages/core/src/h2x/svgProps.js @@ -0,0 +1,69 @@ +import { JSXAttribute } from 'h2x-plugin-jsx' + +const compareAttrsName = attr => otherAttr => attr.name === otherAttr.name +const compareAttrsValue = attr => otherAttr => attr.value === otherAttr.value + +const compareAttrs = attr => otherAttr => + compareAttrsName(attr)(otherAttr) && compareAttrsValue(attr)(otherAttr) + +const areAttrsAlreadyInjected = (node, attributes = {}) => { + const nodeAttrs = node.attributes + + return Object.keys(attributes).reduce((accumulation, key) => { + if (nodeAttrs.some(compareAttrs({ name: key, value: attributes[key] }))) + return accumulation + return false + }, true) +} + +const svgProps = (props = {}) => () => { + const interpolated = new Set() + const keys = Object.keys(props) + const attributes = keys.reduce((acc, prop) => { + const value = props[prop] + if ( + typeof value === 'string' && + value.startsWith('{') && + value.endsWith('}') + ) { + acc[prop] = value.slice(1, -1) + interpolated.add(prop) + } else { + acc[prop] = value + } + return acc + }, {}) + + return { + visitor: { + JSXElement: { + enter(path) { + if (path.node.name !== 'svg') return + if (areAttrsAlreadyInjected(path.node, attributes)) return + + const parseAttributes = keys.reduce((accumulation, key) => { + const prop = new JSXAttribute() + prop.name = key + prop.value = attributes[key] + prop.literal = interpolated.has(key) + return [...accumulation, prop] + }, []) + + const mergeAttributes = path.node.attributes.reduce( + (accumulation, value) => { + if (accumulation.some(compareAttrsName(value))) + return accumulation + return [...accumulation, value] + }, + parseAttributes, + ) + + path.node.attributes = mergeAttributes + path.replace(path.node) + }, + }, + }, + } +} + +export default svgProps diff --git a/packages/core/src/h2x/svgProps.test.js b/packages/core/src/h2x/svgProps.test.js new file mode 100644 index 00000000..64110d7a --- /dev/null +++ b/packages/core/src/h2x/svgProps.test.js @@ -0,0 +1,126 @@ +import jsx from 'h2x-plugin-jsx' +import { transform } from 'h2x-core' +import svgProps from './svgProps' + +function transformSvgProps(code, props) { + return transform(code, { plugins: [jsx, svgProps(props)] }) +} + +describe('svgProps', () => { + it('should add one prop to svg', () => { + expect( + transformSvgProps( + ` + + + + + `, + { focusable: false }, + ), + ).toMatchInlineSnapshot(` +" + + + + +" +`) + }) + + it('should add multiple props to svg', () => { + expect( + transformSvgProps( + ` + + + + + `, + { + focusable: false, + hidden: 'hidden', + }, + ), + ).toMatchInlineSnapshot(` +" +" +`) + }) + + it('should update old prop to the new prop', () => { + expect( + transformSvgProps( + ` + + + + + `, + { focusable: true }, + ), + ).toMatchInlineSnapshot(` +" + + + + +" +`) + }) + + it('should add a new prop', () => { + expect( + transformSvgProps( + ` + + + + + `, + { focusable: true, hidden: true }, + ), + ).toMatchInlineSnapshot(` +" +" +`) + }) + + it('should interpolate prop with curly brackets', () => { + expect( + transformSvgProps( + ` + + + + + `, + { + // literal + focusable: `{true}`, + // variables + hidden: `{hidden}`, + // expression + fill: `{fill == null ? 'currentColor' : fill}`, + // string + id: 'foo', + }, + ), + ).toMatchInlineSnapshot(` +" +" +`) + }) +}) diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 2aa2e77d..87248ab1 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -12,6 +12,7 @@ export { default as removeStyle } from './h2x/removeStyle' export { default as replaceAttrValues } from './h2x/replaceAttrValues' export { default as stripAttribute } from './h2x/stripAttribute' export { default as svgAttributes } from './h2x/svgAttributes' +export { default as svgProps } from './h2x/svgProps' export { default as svgRef } from './h2x/svgRef' export { default as titleProp } from './h2x/titleProp' export { default as toReactNative } from './h2x/toReactNative' diff --git a/packages/core/src/plugins/h2x.js b/packages/core/src/plugins/h2x.js index 1a9f618d..c2663f88 100644 --- a/packages/core/src/plugins/h2x.js +++ b/packages/core/src/plugins/h2x.js @@ -9,6 +9,7 @@ import { replaceAttrValues, stripAttribute, svgAttributes, + svgProps, svgRef, titleProp, toReactNative, @@ -27,6 +28,7 @@ function configToPlugins(config) { if (config.icon) plugins.push(emSize()) if (config.ref) plugins.push(svgRef()) if (config.svgAttributes) plugins.push(svgAttributes(config.svgAttributes)) + if (config.svgProps) plugins.push(svgProps(config.svgProps)) // TODO remove boolean value in the next major release if (config.expandProps) plugins.push(