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

Presentation attributes support #20

Merged
merged 10 commits into from
Jul 1, 2022
25 changes: 14 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# @nrk/svg-to-js
> Module for concatenating SVG files into JavaScript.

##### Why load icons as JavaScript?
**Why load icons as JavaScript?**

SVG symbols are great for styling and accessibility, but can not load cross domain, or from external file and in IE (9,10,11). Javascript provides us a cacheable, cross-domain method to load the icons, without adding extra overhead to each html-file.

## Installation
Expand Down Expand Up @@ -56,12 +57,12 @@ You can add custom outputs by providing the correct array of `customOutputs`. Ea
{
camelCase: 'nrkClose',
titleCase: 'NrkClose',
symbol: '<symbol viewBox="0 0 15 15" id="nrk-close"><path stroke="currentColor" stroke-linecap="round" d="M2 2l11 11M2 13L13 2"/></symbol>',
svg: '<svg viewBox="0 0 15 15" class="nrk-close" width="15.000em" height="15.000em" aria-hidden="true" focusable="false"><path stroke="currentColor" stroke-linecap="round" d="M2 2l11 11M2 13L13 2"/></svg>',
jsx: `var attributes = {'aria-hidden': true, width: '15.000em', height: '15.000em', viewBox: '0 0 15 15', dangerouslySetInnerHTML: {__html: '<path stroke="currentColor" stroke-linecap="round" d="M2 2l11 11M2 13L13 2"/>'}}\n` +
' if (props) Object.keys(props).forEach(function (key) { attributes[key] = props[key] })\n' +
" return React.createElement('svg', attributes)"
}
symbol: '<symbol viewBox="0 0 24 24" id="nrk-close" ><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M12 10.5858l6.2929-6.29289 1.4142 1.41421L13.4142 12l6.2929 6.2929-1.4142 1.4142L12 13.4142l-6.29288 6.2929-1.41421-1.4142L10.5858 12 4.29291 5.70712l1.41421-1.41421L12 10.5858z"/></symbol>',
svg: '<svg viewBox="0 0 24 24" class="nrk-close" width="24.000em" height="24.000em" aria-hidden="true" focusable="false"><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M12 10.5858l6.2929-6.29289 1.4142 1.41421L13.4142 12l6.2929 6.2929-1.4142 1.4142L12 13.4142l-6.29288 6.2929-1.41421-1.4142L10.5858 12 4.29291 5.70712l1.41421-1.41421L12 10.5858z"/></svg>',
jsx: `var attributes = {'aria-hidden': true, width: '24.000em', height: '24.000em', viewBox: '0 0 24 24', dangerouslySetInnerHTML: {__html: '<path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M12 10.5858l6.2929-6.29289 1.4142 1.41421L13.4142 12l6.2929 6.2929-1.4142 1.4142L12 13.4142l-6.29288 6.2929-1.41421-1.4142L10.5858 12 4.29291 5.70712l1.41421-1.41421L12 10.5858z"/>'}}\n` +
' if (props) Object.keys(props).forEach(function (key) { attributes[key] = props[key] })\n' +
" return React.createElement('svg', attributes)"
},
```

### Generate svg strings with id-attributes
Expand All @@ -71,6 +72,7 @@ You can add custom outputs by providing the correct array of `customOutputs`. Ea
// nrk-bell.svg
// nrk-close-no-viewBox.svg
// nrk-close.svg
// nrk-download.svg

import svgtojs from '@nrk/svg-to-js'

Expand All @@ -83,7 +85,7 @@ const customOutputs = [{
}]

const options = {
banner: '/** Made with @nrk/svg-to-js **/',
banner: 'Made with @nrk/svg-to-js',
skjalgepalg marked this conversation as resolved.
Show resolved Hide resolved
input: __dirname,
customOutputs: customOutputs
}
Expand All @@ -105,8 +107,9 @@ Generates custom output file `id_output.js`:

```js
/** Made with @nrk/svg-to-js **/
export const nrkBell = '<svg id="NrkBell" viewBox="0 0 15 15" class="nrk-bell" width="15.000em" height="15.000em" aria-hidden="true" focusable="false"><path stroke="currentColor" fill="none" d="M7.5081246 2.5C4.0162492 2.5 4 5.38865948 4 6.2861215V9c0 1-1.5166599 1.7192343-1.5 2 .03450336.5814775.27977082.4920386.9090909.4920386h8.1818182C12.2186267 11.4920386 12.5 11.5 12.5 11c0-.3060964-1.5-1-1.5-2V6.2861215C11 5.35488333 11 2.5 7.5081246 2.5z"/><path d="M8.75 12.5h-2.5s0 1.25 1.25 1.25 1.25-1.25 1.25-1.25z"/><path stroke="currentColor" d="M7.5 1.5V2" stroke-linecap="round"/></svg>';
export const nrkCloseNoViewBox = '<svg id="NrkCloseNoViewBox" viewBox="0 0 15 15" class="nrk-close-no-viewBox" width="15.000em" height="15.000em" aria-hidden="true" focusable="false"><path stroke="currentColor" stroke-linecap="round" d="M2 2l11 11M2 13L13 2"/></svg>';
export const nrkClose = '<svg id="NrkClose" viewBox="0 0 15 15" class="nrk-close" width="15.000em" height="15.000em" aria-hidden="true" focusable="false"><path stroke="currentColor" stroke-linecap="round" d="M2 2l11 11M2 13L13 2"/></svg>';
export const nrkBell = '<svg id="NrkBell" viewBox="0 0 24 24" class="nrk-bell" width="24.000em" height="24.000em" aria-hidden="true" focusable="false"><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M13 1h-2v1.05475C7.86324 2.40036 5 4.38211 5 8v8.5L3 18v2h18v-2l-2-1.5V8c0-3.61788-2.8632-5.59963-6-5.94525V1zm1 20H9.99999c0 1.1046.89541 2 2.00001 2s2-.8954 2-2zM11.9974 4h.0052c1.3984.00051 2.6978.40515 3.5978 1.09086C16.4454 5.73464 17 6.65971 17 8v9.5l.6667.5H6.33333L7 17.5V8c0-1.34029.55463-2.26536 1.39959-2.90914.9-.68571 2.19941-1.09035 3.59781-1.09086z"/></svg>';
export const nrkCloseNoViewBox = '<svg id="NrkCloseNoViewBox" viewBox="0 0 15 15" class="nrk-close-no-viewBox" width="15.000em" height="15.000em" aria-hidden="true" focusable="false"><path stroke="currentColor" stroke-linecap="round" d="M2 2l11 11M2 13L13 2"/></svg>';
export const nrkClose = '<svg id="NrkClose" viewBox="0 0 24 24" class="nrk-close" width="24.000em" height="24.000em" aria-hidden="true" focusable="false"><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M12 10.5858l6.2929-6.29289 1.4142 1.41421L13.4142 12l6.2929 6.2929-1.4142 1.4142L12 13.4142l-6.29288 6.2929-1.41421-1.4142L10.5858 12 4.29291 5.70712l1.41421-1.41421L12 10.5858z"/></svg>';
export const nrkDownload = '<svg id="NrkDownload" viewBox="0 0 24 24" class="nrk-download" width="24.000em" height="24.000em" fill="currentColor" aria-hidden="true" focusable="false"><path d="M13 2h-2v15.1l-7-4.4V15l8 5 8-5v-2.3l-7 4.4V2Z"/><path d="M4 22h16v2H4z" opacity=".5"/></svg>';

```
11 changes: 8 additions & 3 deletions lib/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import path from 'path'
import fs from 'fs'

import { parseSVGTagPresentationAttributes } from './utils'

/**
* svgToJS Concatenates SVG-files into Javascript
* @param {Object} config
Expand Down Expand Up @@ -29,6 +31,9 @@ export default function svgToJS (config) {
const code = fs.readFileSync(path.join(config.input, file), 'utf-8')
const size = String(code.match(/viewBox="[^"]+/)).slice(9) || `0 0 ${String(code.match(/width="[^"]+/)).slice(7)} ${String(code.match(/width="[^"]+/)).slice(7)}`
const name = file.slice(0, -4)
const attrInfo = parseSVGTagPresentationAttributes(code)
const svgAttrStr = attrInfo.foundAttributes ? attrInfo.svgAttrStr : ''
const svgAttrStrJsx = attrInfo.foundAttributes ? attrInfo.svgAttrStrJsx : ''
skjalgepalg marked this conversation as resolved.
Show resolved Hide resolved
const body = code.replace(/^[^>]+>|<[^<]+$/g, '').replace(/\s*([<>])\s*/g, '$1') // Minified SVG body
const camelCase = name.replace(/-+./g, (m) => m.slice(-1).toUpperCase())
const titleCase = camelCase.replace(/./, (m) => m.toUpperCase())
Expand All @@ -41,10 +46,10 @@ export default function svgToJS (config) {
icons.push({
camelCase,
titleCase,
symbol: `<symbol viewBox="${size}" id="${name}">${body}</symbol>`,
svg: `<svg viewBox="${size}" class="${name}" width="${w}" height="${h}" aria-hidden="true" focusable="false">${body}</svg>`,
symbol: `<symbol viewBox="${size}" id="${name}" ${svgAttrStr}>${body}</symbol>`,
svg: `<svg viewBox="${size}" class="${name}" width="${w}" height="${h}" ${svgAttrStr} aria-hidden="true" focusable="false">${body}</svg>`,
jsx: `
var attributes = {'aria-hidden': true, width: '${w}', height: '${h}', viewBox: '${size}', dangerouslySetInnerHTML: {__html: '${body}'}}
var attributes = {'aria-hidden': true, width: '${w}', height: '${h}', viewBox: '${size}'${svgAttrStrJsx}, dangerouslySetInnerHTML: {__html: '${body}'}}
if (props) Object.keys(props).forEach(function (key) { attributes[key] = props[key] })
return React.createElement('svg', attributes)
`.trim()
Expand Down
123 changes: 123 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/**
* @typedef {Object} MissingSvgPresentationAttributes
* @property {false} foundAttributes
*/

/**
* @typedef {Object} SvgPresentationAttributes
* @property {true} foundAttributes
* @property {string} svgAttrStr
* @property {string} svgAttrStrJsx
*/

/**
* @typedef {Record<keyof typeof SVG_PRESENTATION_ATTRIBUTES, string | undefined>} PresentationAttributes
*/

/**
* Parses stringified svg-markup for presentation attributes and returns an object with relevant formats for use
* @param {string} markup Stringified svg markup
* @return {MissingSvgPresentationAttributes | SvgPresentationAttributes}
*/
export function parseSVGTagPresentationAttributes (markup) {
// Select tagName and all attributes as string from first matching tag
const tagParsingPattern = /<([a-z][a-z0-9]*)\s*([^>]*?)(\/?)>/
const { 1: tagName, 2: attributes } = tagParsingPattern.exec(markup)
if (tagName !== 'svg') {
console.warn(`getSVGTagPresentationAttributes: Unable to find leading svg-tag, in markup: ${markup}`)
return null
skjalgepalg marked this conversation as resolved.
Show resolved Hide resolved
}
// Select key and value from all attributes found in tagParsingPattern - ignores valueless attributes like `hidden`
const attributeParsingPattern = /(?:^|\s)([a-z-A-Z]*)\s*=\s*((?:'[^']*')|(?:"[^"]*")|\S+)/gi
/**
* @type PresentationAttributes
*/
const attrs = {}

for (let match; (match = attributeParsingPattern.exec(attributes));) {
const { 1: key, 2: val } = match
const isQuoted = val && (val[0] === '\'' || val[0] === '"')
skjalgepalg marked this conversation as resolved.
Show resolved Hide resolved
if (SVG_PRESENTATION_ATTRIBUTES.indexOf(key.toLowerCase()) > -1) {
attrs[key.toLowerCase()] = isQuoted ? val.slice(1, val.length - 1) : val
}
}
const foundAttributes = Object.keys(attrs).length > 0
if (!foundAttributes) {
return {
foundAttributes: false
}
}
return {
foundAttributes: true,
svgAttrStr: Object.keys(attrs).reduce((content, key) => content + `${key}="${attrs[key]}" `, '').trim(),
svgAttrStrJsx: Object.keys(attrs).reduce((content, key) => content + `, ${key}: '${attrs[key]}'`, '').trim()
}
}

// Attributes according to https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/Presentation
const SVG_PRESENTATION_ATTRIBUTES = [
'alignment-baseline',
'baseline-shift',
'clip-path',
'clip-rule',
'color',
'color-interpolation',
'color-interpolation-filters',
'color-rendering',
'cursor',
'd',
'direction',
'display',
'dominant-baseline',
'fill',
'fill-opacity',
'fill-rule',
'filter',
'flood-color',
'flood-opacity',
'font-family',
'font-size',
'font-size-adjust',
'font-stretch',
'font-style',
'font-variant',
'font-weight',
'image-rendering',
'letter-spacing',
'lighting-color',
'marker-end',
'marker-mid',
'marker-start',
'mask',
'opacity',
'overflow',
'pointer-events',
'shape-rendering',
'solid-color',
'solid-opacity',
'stop-color',
'stop-opacity',
'stroke',
'stroke-dasharray',
'stroke-dashoffset',
'stroke-linecap',
'stroke-linejoin',
'stroke-miterlimit',
'stroke-opacity',
'stroke-width',
'text-anchor',
'text-decoration',
'text-rendering',
'transform',
'unicode-bidi',
'vector-effect',
'visibility',
'word-spacing',
'writing-mode'
// 'clip', Deprecated
// 'color-profile', Deprecated
// 'enable-background', Deprecated
// 'glyph-orientation-horizontal', Deprecated
// 'glyph-orientation-vertical', Deprecated
// 'kerning', Deprecated
]
Loading