reusePaths.js 6.33 KB
/**
 * @license
 * The MIT License
 *
 * Copyright © 2012–2016 Kir Belevich
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 *
 * Лицензия MIT
 *
 * Copyright © 2012–2016 Кир Белевич
 *
 * Данная лицензия разрешает лицам, получившим копию
 * данного
 * программного обеспечения и сопутствующей
 * документации
 * (в дальнейшем именуемыми «Программное Обеспечение»),
 * безвозмездно
 * использовать Программное Обеспечение без
 * ограничений, включая
 * неограниченное право на использование, копирование,
 * изменение,
 * добавление, публикацию, распространение,
 * сублицензирование
 * и/или продажу копий Программного Обеспечения, также
 * как и лицам,
 * которым предоставляется данное Программное
 * Обеспечение,
 * при соблюдении следующих условий:
 *
 * Указанное выше уведомление об авторском праве и
 * данные условия
 * должны быть включены во все копии или значимые части
 * данного
 * Программного Обеспечения.
 *
 * ДАННОЕ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК
 * ЕСТЬ»,
 * БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНО ВЫРАЖЕННЫХ ИЛИ
 * ПОДРАЗУМЕВАЕМЫХ,
 * ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ГАРАНТИЯМИ ТОВАРНОЙ
 * ПРИГОДНОСТИ,
 * СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И
 * ОТСУТСТВИЯ НАРУШЕНИЙ
 * ПРАВ. НИ В КАКОМ СЛУЧАЕ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ
 * НЕСУТ
 * ОТВЕТСТВЕННОСТИ ПО ИСКАМ О ВОЗМЕЩЕНИИ УЩЕРБА, УБЫТКОВ
 * ИЛИ ДРУГИХ
 * ТРЕБОВАНИЙ ПО ДЕЙСТВУЮЩИМ КОНТРАКТАМ, ДЕЛИКТАМ ИЛИ
 * ИНОМУ,
 * ВОЗНИКШИМ ИЗ, ИМЕЮЩИМ ПРИЧИНОЙ ИЛИ СВЯЗАННЫМ С
 * ПРОГРАММНЫМ
 * ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ ПРОГРАММНОГО
 * ОБЕСПЕЧЕНИЯ
 * ИЛИ ИНЫМИ ДЕЙСТВИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.
 */

'use strict';

var JSAPI = require('../lib/svgo/jsAPI');

exports.type = 'full';

exports.active = false;

exports.description = 'Finds <path> elements with the same d, fill, and ' +
                      'stroke, and converts them to <use> elements ' +
                      'referencing a single <path> def.';

/**
 * Finds <path> elements with the same d, fill, and stroke, and converts them to
 * <use> elements referencing a single <path> def.
 *
 * @author Jacob Howcroft
 */
exports.fn = function(data) {
  const seen = new Map();
  let count = 0;
  const defs = [];
  traverse(data, item => {
    if (!item.isElem('path') || !item.hasAttr('d')) {
      return;
    }
    const d = item.attr('d').value;
    const fill = (item.hasAttr('fill') && item.attr('fill').value) || '';
    const stroke = (item.hasAttr('stroke') && item.attr('stroke').value) || '';
    const key = d + ';s:' + stroke + ';f:' + fill;
    const hasSeen = seen.get(key);
    if (!hasSeen) {
      seen.set(key, {elem: item, reused: false});
      return;
    }
    if (!hasSeen.reused) {
      hasSeen.reused = true;
      if (!hasSeen.elem.hasAttr('id')) {
        hasSeen.elem.addAttr({name: 'id', local: 'id',
                              prefix: '', value: 'reuse-' + (count++)});
      }
      defs.push(hasSeen.elem);
    }
    item = convertToUse(item, hasSeen.elem.attr('id').value);
  });
  const defsTag = new JSAPI({
    elem: 'defs', prefix: '', local: 'defs', content: [], attrs: []}, data);
  data.content[0].spliceContent(0, 0, defsTag);
  for (let def of defs) {
    // Remove class and style before copying to avoid circular refs in
    // JSON.stringify. This is fine because we don't actually want class or
    // style information to be copied.
    const style = def.style;
    const defClass = def.class;
    delete def.style;
    delete def.class;
    const defClone = def.clone();
    def.style = style;
    def.class = defClass;
    defClone.removeAttr('transform');
    defsTag.spliceContent(0, 0, defClone);
    // Convert the original def to a use so the first usage isn't duplicated.
    def = convertToUse(def, defClone.attr('id').value);
    def.removeAttr('id');
  }
  return data;
};

/** */
function convertToUse(item, href) {
  item.renameElem('use');
  item.removeAttr('d');
  item.removeAttr('stroke');
  item.removeAttr('fill');
  item.addAttr({name: 'xlink:href', local: 'xlink:href',
                prefix: 'none', value: '#' + href});
  delete item.pathJS;
  return item;
}

/** */
function traverse(parent, callback) {
  if (parent.isEmpty()) {
    return;
  }
  for (let child of parent.content) {
    callback(child);
    traverse(child, callback);
  }
}