All files / src sprite-store.js

66.67% Statements 20/30
58.33% Branches 7/12
55.56% Functions 5/9
69.23% Lines 18/26

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112    1x             12x   10x 8x   10x 8x     2x                           1x                     2x 2x       2x 2x 2x 2x                               2x                                 2x   2x                                               1x  
'use strict'
 
const parse5 = require('parse5')
 
/**
 * Recursively searches an AST for an SVG node
 * @param {Object} node AST node
 */
function findSVGNode(node) {
  if (node.tagName === 'svg') return node
 
  if (node.childNodes) {
    const searchedChildren = node.childNodes
      .map(findSVGNode)
      .filter((childNode) => childNode)
    return searchedChildren.length ? searchedChildren[0] : null
  }
 
  return null
}
 
/**
 * The SpriteStore handles transforming and adding SVGs to an internal store by id.
 * SVGs are transformed to `<symbol>` elements with the SVG filename as the element
 * id.
 *
 * When getting the icon set from the store the set is alphabetized (and inherently
 * unique) so that content hashes are not affected by import order.
 */
class SpriteStore {
  constructor() {
    // Why use a map??? JUST FOR FUN ¯\_(ツ)_/¯
    this.icons = new Map()
  }
 
  /**
   * Handle transforming SVG for use in a sprite and add to internal store
   * @param {string} resourcePath
   * @param {string} svg
   * @param {string} id
   * @return {Object} SVG meta data returned for loader including id
   */
  addSVG(resourcePath, svg, id) {
    const document = parse5.parse(svg)
    const svgNode = findSVGNode(document)
 
    // If a viewbox has not been defined on an SVG, use the width and height attrs
    // to create one
    let viewBox = ''
    const viewBoxAttr = svgNode.attrs.find((attr) => attr.name === 'viewBox')
    Eif (viewBoxAttr) {
      viewBox = viewBoxAttr.value
    } else {
      const width = svgNode.attrs.find((attr) => attr.name === 'width')
      const height = svgNode.attrs.find((attr) => attr.name === 'height')
      if (!width || !height) {
        // eslint-disable-next-line no-console
        console.warn(
          `Parsed svg from ${resourcePath} does not have a viewBox or width and height attributes!`,
        )
      } else {
        viewBox = `0 0 ${width.value} ${height.value}`
      }
    }
 
    // Create a <symbol /> with id and viewBox attrs, that's all we need for
    // the svg symbol sprite
    const symbol = parse5.serialize({
      nodeName: '#document',
      mode: 'quirks',
      childNodes: [
        {
          nodeName: 'symbol',
          tagName: 'symbol',
          namespaceURI: 'http://www.w3.org/1999/xhtml',
          attrs: [
            { name: 'id', value: id },
            { name: 'viewBox', value: viewBox },
          ],
          childNodes: svgNode.childNodes,
        },
      ],
    })
 
    this.icons.set(id, symbol)
 
    return { id }
  }
 
  /**
   * Handle returning the complete SVG sprite content string with deduped, sorted
   * icons inside.
   * @return {string} SVG symbol sprite
   */
  getSpriteContent() {
    // Sort keys alphabetically before reducing content to ensure consistent hashes
    // for the same set of ideas (guarantee deterministic id order)
    const sortedIcons = Array.from(this.icons.keys()).sort()
 
    // Return svg with styles set to hide the svg, it isn't meant to be shown, the
    // icons will reference the symbols inside the svg.
    return `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" style="position: absolute; width: 0; height: 0">${sortedIcons.reduce(
      (prev, curr) => prev + this.icons.get(curr),
      '',
    )}</svg>`
  }
}
 
// Export singleton for use by loader and plugin. (If multiple sprites are needed
// this class will need to be updated to store icons by a sprite namespace)
module.exports = new SpriteStore()