Skip to content

Commit

Permalink
Merge pull request #20 from nrkno/presentation-attributes-support
Browse files Browse the repository at this point in the history
Presentation attributes support
  • Loading branch information
skjalgepalg authored Jul 1, 2022
2 parents 94bc47c + d5121c8 commit 6e577e1
Show file tree
Hide file tree
Showing 9 changed files with 391 additions and 102 deletions.
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',
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 : ''
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
121 changes: 121 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/**
* @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 { foundAttributes: false }
}
// 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] === '"')
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

0 comments on commit 6e577e1

Please sign in to comment.