• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

svg-sprite / svg-sprite / 673260237

pending completion
673260237

Pull #436

github

GitHub
Merge 961184c96 into ebff837d7
Pull Request #436: Enforce Lint and assorted tweaks

313 of 433 branches covered (72.29%)

Branch coverage included in aggregate %.

1952 of 1952 new or added lines in 12 files covered. (100.0%)

2877 of 3251 relevant lines covered (88.5%)

75.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.82
/lib/svg-sprite/shape.js
1
'use strict';
1✔
2

1✔
3
/**
1✔
4
 * svg-sprite is a Node.js module for creating SVG sprites
1✔
5
 *
1✔
6
 * @see https://github.com/svg-sprite/svg-sprite
1✔
7
 *
1✔
8
 * @author Joschi Kuphal <joschi@kuphal.net> (https://github.com/jkphl)
1✔
9
 * @copyright © 2018 Joschi Kuphal
1✔
10
 * @license MIT https://github.com/svg-sprite/svg-sprite/blob/master/LICENSE
1✔
11
 */
1✔
12

1✔
13
const path = require('path');
1✔
14
const { format } = require('util');
1✔
15
const { execFile } = require('child_process');
1✔
16
const _ = require('lodash');
1✔
17
const { DOMParser, XMLSerializer } = require('xmldom');
1✔
18
const xpath = require('xpath');
1✔
19
const cssom = require('cssom');
1✔
20
const { CssSelectorParser } = require('css-selector-parser');
1✔
21
const phantomjs = require('phantomjs-prebuilt').path;
1✔
22
const async = require('async');
1✔
23
const csso = require('csso');
1✔
24

1✔
25
const dimensionsPhantomScript = path.resolve(__dirname, 'shape/dimensions.phantom.js');
1✔
26

1✔
27
/**
1✔
28
 * Default callback for shape ID generation
1✔
29
 *
1✔
30
 * @param {String} template         Template string
1✔
31
 * @return {String}                 Shape ID
1✔
32
 */
1✔
33
const createIdGenerator = template => {
1✔
34
    /**
1✔
35
     * ID generator
1✔
36
     *
1✔
37
     * @param {String} name         Relative file path
1✔
38
     * @returns {String}            Shape ID
1✔
39
     */
1✔
40
    const generator = function(name) {
1✔
41
        const pathname = this.separator ? name.split(path.sep).join(this.separator) : name;
52!
42
        return format(template || '%s', path.basename(pathname.replace(/\s+/g, this.whitespace), '.svg'));
52!
43
    };
1✔
44

1✔
45
    return generator;
1✔
46
};
1✔
47

1✔
48
/**
1✔
49
 * Default shape configuration
1✔
50
 *
1✔
51
 * @type {Object}
1✔
52
 */
1✔
53
const defaultConfig = {
1✔
54
    /**
1✔
55
     * Shape ID related options
1✔
56
     *
1✔
57
     * @type {Object}
1✔
58
     */
1✔
59
    id: {
1✔
60
        /**
1✔
61
         * ID part separator (used for directory-to-ID traversal)
1✔
62
         *
1✔
63
         * @type {String}
1✔
64
         */
1✔
65
        separator: '--',
1✔
66
        /**
1✔
67
         * Pseudo selector separator
1✔
68
         *
1✔
69
         * @type {String}
1✔
70
         */
1✔
71
        pseudo: '~',
1✔
72
        /**
1✔
73
         * Whitespace replacement string
1✔
74
         *
1✔
75
         * @type {String}
1✔
76
         */
1✔
77
        whitespace: '_',
1✔
78
        /**
1✔
79
         * ID traversal callback
1✔
80
         *
1✔
81
         * @param {Function}
1✔
82
         */
1✔
83
        generator: createIdGenerator('%s')
1✔
84
    },
1✔
85
    /**
1✔
86
     * Dimension related options
1✔
87
     *
1✔
88
     * @type {Object}
1✔
89
     */
1✔
90
    dimension: {
1✔
91
        /**
1✔
92
         * Max. shape width
1✔
93
         *
1✔
94
         * @type {Number}
1✔
95
         */
1✔
96
        maxWidth: 2000,
1✔
97
        /**
1✔
98
         * Max. shape height
1✔
99
         *
1✔
100
         * @type {Number}
1✔
101
         */
1✔
102
        maxHeight: 2000,
1✔
103
        /**
1✔
104
         * Coordinate decimal places
1✔
105
         *
1✔
106
         * @type {Number}
1✔
107
         */
1✔
108
        precision: 2,
1✔
109
        /**
1✔
110
         * Add dimension attributes
1✔
111
         *
1✔
112
         * @type {Boolean}
1✔
113
         */
1✔
114
        attributes: false
1✔
115
    },
1✔
116
    /**
1✔
117
     * Spacing related options
1✔
118
     *
1✔
119
     * @type {Number}
1✔
120
     */
1✔
121
    spacing: {
1✔
122
        /**
1✔
123
         * Padding around the shape
1✔
124
         *
1✔
125
         * @type {Number|Array}
1✔
126
         */
1✔
127
        padding: { top: 0, right: 0, bottom: 0, left: 0 },
1✔
128
        /**
1✔
129
         * Box sizing strategy
1✔
130
         *
1✔
131
         * Might be 'content' (padding is added outside of the shape), 'padding' (shape plus padding will make for the given maximum size)
1✔
132
         * or 'contain' (like 'padding', but size will be fixed instead of maximum)
1✔
133
         *
1✔
134
         * @type {String}
1✔
135
         */
1✔
136
        box: 'content'
1✔
137
    }
1✔
138
};
1✔
139
const svgReferenceProperties = ['style', 'fill', 'stroke', 'filter', 'clip-path', 'mask', 'marker-start', 'marker-end', 'marker-mid'];
1✔
140

1✔
141
/**
1✔
142
 * SVGShape
1✔
143
 *
1✔
144
 * @param {Vinyl} file                  Vinyl file
1✔
145
 * @param {SVGSpriter} spriter          Spriter instance
1✔
146
 */
1✔
147
function SVGShape(file, spriter) {
52✔
148
    this.source = file;
52✔
149
    this.spriter = spriter;
52✔
150
    this.svg = { current: this.source.contents.toString(), ready: null };
52✔
151
    this.name = path.basename(this.source.path);
52✔
152
    this.config = _.merge(_.cloneDeep(defaultConfig), this.spriter.config.shape || {});
52!
153

52✔
154
    if (!_.isFunction(this.config.id.generator)) {
52!
155
        this.config.id.generator = createIdGenerator(_.isString(this.config.id.generator) ? this.config.id.generator + (this.config.id.generator.includes('%s') ? '' : '%s') : '%s');
×
156
    }
×
157

52✔
158
    this.id = this.config.id.generator(this.name, this.source);
52✔
159
    this.state = this.id.split(this.config.id.pseudo);
52✔
160
    this.base = this.state.shift();
52✔
161
    this.state = this.state.shift() || null;
52✔
162
    this.master = null;
52✔
163
    this.copies = 0;
52✔
164
    this._precision = 10 ** Number(this.config.dimension.precision);
52✔
165
    this._scale = 1;
52✔
166
    this._namespaced = false;
52✔
167

52✔
168
    // Determine meta & alignment data
52✔
169
    const relative = path.basename(this.source.relative, '.svg');
52✔
170
    this.meta = this.id in this.config.meta ? this.config.meta[this.id] : (relative in this.config.meta ? this.config.meta[relative] : {});
52!
171
    this.align = Object.entries({ ...this.config.align['*'], ...(this.id in this.config.align ? this.config.align[this.id] : (relative in this.config.align ? this.config.align[relative] : {})) });
52!
172

52✔
173
    // Initially set the SVG of this shape
52✔
174
    this._initSVG();
52✔
175

52✔
176
    // XML declaration and doctype
52✔
177
    const xmldecl = this.svg.current.match(/<\?xml.*?>/gi);
52✔
178
    const doctype = this.svg.current.match(/<!doctype.*?>/gi);
52✔
179
    this.xmlDeclaration = xmldecl ? xmldecl[0] : '<?xml version="1.0" encoding="utf-8"?>';
52!
180
    this.doctypeDeclaration = doctype ? doctype[0] : '';
52✔
181

52✔
182
    this.spriter.verbose('Added shape "%s:%s"', this.base, this.state || 'regular');
52✔
183
}
52✔
184

1✔
185
/**
1✔
186
 * Prototype properties
1✔
187
 *
1✔
188
 * @type {Object}
1✔
189
 */
1✔
190
SVGShape.prototype = {};
1✔
191

1✔
192
/**
1✔
193
 * SVG stages
1✔
194
 *
1✔
195
 * @type {Object}
1✔
196
 */
1✔
197
SVGShape.prototype.svg = null;
1✔
198

1✔
199
/**
1✔
200
 * Default SVG namespace
1✔
201
 *
1✔
202
 * @type {String}
1✔
203
 */
1✔
204
SVGShape.prototype.DEFAULT_SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
1✔
205

1✔
206
/**
1✔
207
 * Xlink namespace
1✔
208
 *
1✔
209
 * @type {String}
1✔
210
 */
1✔
211
SVGShape.prototype.XLINK_NAMESPACE = 'http://www.w3.org/1999/xlink';
1✔
212

1✔
213
/**
1✔
214
 * Return a string representation of the shape
1✔
215
 *
1✔
216
 * @return {String}         String representation
1✔
217
 */
1✔
218
SVGShape.prototype.toString = function() {
1✔
219
    return '[object SVGShape]';
×
220
};
1✔
221

1✔
222
/**
1✔
223
 * Recursively strip unneeded namespace declarations
1✔
224
 *
1✔
225
 * @param {Element} element     Element
1✔
226
 * @param {Object} nsMap        Namespace map
1✔
227
 * @return {Element}            Element
1✔
228
 */
1✔
229
SVGShape.prototype._stripInlineNamespaceDeclarations = function(element, nsMap) {
1✔
230
    const parentNsMap = _.clone(element._nsMap);
8,930✔
231
    nsMap = nsMap || { '': this.DEFAULT_SVG_NAMESPACE };
8,930✔
232

8,930✔
233
    // Strip the default SVG namespace
8,930✔
234
    if (nsMap[''] === this.DEFAULT_SVG_NAMESPACE) {
8,930✔
235
        const defaultNamespace = element.attributes.getNamedItem('xmlns');
1,162✔
236
        if (defaultNamespace !== undefined && defaultNamespace.value === this.DEFAULT_SVG_NAMESPACE) {
1,162✔
237
            element.attributes.removeNamedItem('xmlns');
79✔
238
        }
79✔
239
    }
1,162✔
240

8,930✔
241
    if (!('xlink' in nsMap) || nsMap.xlink === this.XLINK_NAMESPACE) {
8,930✔
242
        const xlinkNamespace = element.attributes.getNamedItem('xmlns:xlink');
8,930✔
243
        if (xlinkNamespace !== undefined && xlinkNamespace.value === this.XLINK_NAMESPACE) {
8,930✔
244
            element.attributes.removeNamedItem('xmlns:xlink');
73✔
245
        }
73✔
246
    }
8,930✔
247

8,930✔
248
    for (let c = 0; c < element.childNodes.length; c++) {
8,930✔
249
        if (element.childNodes.item(c).nodeType === 1) {
8,851✔
250
            this._stripInlineNamespaceDeclarations(element.childNodes.item(c), parentNsMap);
8,845✔
251
        }
8,845✔
252
    }
8,851✔
253

8,930✔
254
    return element;
8,930✔
255
};
1✔
256

1✔
257
/**
1✔
258
 * Return the SVG of this shape
1✔
259
 *
1✔
260
 * @param {Boolean} inline          Prepare for inline usage (strip redundant XML namespaces)
1✔
261
 * @param {Function} transform      Final transformer before serialization (operating on a clone)
1✔
262
 * @return {String}                 SVG
1✔
263
 */
1✔
264
SVGShape.prototype.getSVG = function(inline, transform) {
1✔
265
    let svg;
146✔
266

146✔
267
    // If this is a distributed copy
146✔
268
    if (this.master) {
146✔
269
        svg = this.dom.createElementNS(this.DEFAULT_SVG_NAMESPACE, 'use');
6✔
270
        svg.setAttribute('xlink:href', '#' + this.master.id);
6✔
271
    } else {
146✔
272
        svg = this.dom.documentElement.cloneNode(true);
140✔
273
    }
140✔
274

146✔
275
    // Call the final transformer (if available)
146✔
276
    if (_.isFunction(transform)) {
146✔
277
        transform(svg);
83✔
278
    }
83✔
279

146✔
280
    // If the SVG is to be used inline or as part of a sprite or is a distributed copy: Strip redundand namespace declarations
146✔
281
    if (inline || this.master) {
146✔
282
        return new XMLSerializer().serializeToString(this._stripInlineNamespaceDeclarations(svg));
85✔
283
    }
85✔
284

61✔
285
    // Else: Add XML and DOCTYPE declarations if required
61✔
286
    svg = new XMLSerializer().serializeToString(svg);
61✔
287

61✔
288
    // Add DOCTYPE declaration
61✔
289
    if (this.spriter.config.svg.doctypeDeclaration) {
61✔
290
        svg = this.doctypeDeclaration + svg;
61✔
291
    }
61✔
292

61✔
293
    // Add XML declaration
61✔
294
    if (this.spriter.config.svg.xmlDeclaration) {
61✔
295
        svg = this.xmlDeclaration + svg;
61✔
296
    }
61✔
297

61✔
298
    return svg;
61✔
299
};
1✔
300

1✔
301
/**
1✔
302
 * Set the SVG of this shape
1✔
303
 *
1✔
304
 * @param {String} svg      SVG
1✔
305
 * @return {SVGShape}       Self reference
1✔
306
 */
1✔
307
SVGShape.prototype.setSVG = function(svg) {
1✔
308
    this.svg.current = svg;
46✔
309
    this.svg.ready = null;
46✔
310
    return this._initSVG();
46✔
311
};
1✔
312

1✔
313
/**
1✔
314
 * Initialize the SVG of this shape
1✔
315
 *
1✔
316
 * @return {SVGShape}       Self reference
1✔
317
 */
1✔
318
SVGShape.prototype._initSVG = function() {
1✔
319
    // Basic check for basic SVG file structure
98✔
320
    const svgStart = this.svg.current.match(/<svg(?:\s+[a-z\d-:]+=("|').*?\1)*\s*(?:(\/)|(>[^]*<\/svg))>/i);
98✔
321
    if (!svgStart) {
98!
322
        const e = new Error('Invalid SVG file');
×
323
        e.name = 'ArgumentError';
×
324
        e.errno = 1429395394;
×
325
        throw e;
×
326
    }
×
327

98✔
328
    // Resolve XML entities
98✔
329
    const entityRegExp = /<!ENTITY\s+(\S+)\s+(["'])(.+)\2>/;
98✔
330
    let entityStart = 0;
98✔
331
    let entities = 0;
98✔
332
    const entityMap = {};
98✔
333
    let entity;
98✔
334
    do {
98✔
335
        entity = entityRegExp.exec(this.svg.current.substr(entityStart));
98✔
336
        if (entity) {
98!
337
            ++entities;
×
338
            entityStart += entity.index + entity[0].length;
×
339
            entityMap[entity[1]] = entity[3];
×
340
        }
×
341
    } while (entity);
98✔
342

98✔
343
    if (entities) {
98!
344
        let svg = this.svg.current.substr(svgStart.index);
×
345
        for (entity in entityMap) {
×
346
            svg = svg.replace('&' + entity + ';', entityMap[entity]);
×
347
        }
×
348

×
349
        this.svg.current = this.svg.current.substr(0, svgStart.index) + svg;
×
350
    }
×
351

98✔
352
    // Parse the XML
98✔
353
    this.dom = new DOMParser({
98✔
354
        locator: {},
98✔
355
        errorHandler(level, message) {
98✔
356
            const e = new Error(format('Invalid SVG file (%s)', message.split('\n').join(' ')));
×
357
            e.name = 'ArgumentError';
×
358
            e.errno = 1429394706;
×
359
            throw e;
×
360
        }
×
361
    }).parseFromString(this.svg.current);
98✔
362

98✔
363
    // Determine the shape width
98✔
364
    const width = this.dom.documentElement.getAttribute('width');
98✔
365
    this.width = width.length ? Number.parseFloat(width) : false;
98!
366

98✔
367
    // Determine the shape height
98✔
368
    const height = this.dom.documentElement.getAttribute('height');
98✔
369
    this.height = height.length ? Number.parseFloat(height) : false;
98!
370

98✔
371
    // Determine the viewbox
98✔
372
    let viewBox = this.dom.documentElement.getAttribute('viewBox');
98✔
373
    if (viewBox.length) {
98✔
374
        viewBox = viewBox.split(/[^-\d.]+/);
6✔
375
        while (viewBox.length < 4) {
6!
376
            viewBox.push(0); // TODO?
×
377
        }
×
378

6✔
379
        viewBox.forEach((value, index) => {
6✔
380
            viewBox[index] = Number.parseFloat(value);
24✔
381
        });
6✔
382
        this.viewBox = viewBox;
6✔
383
    } else {
98✔
384
        this.viewBox = false;
92✔
385
    }
92✔
386

98✔
387
    this.title = null;
98✔
388
    this.description = null;
98✔
389

98✔
390
    const children = this.dom.documentElement.childNodes;
98✔
391
    const meta = { title: 'title', description: 'desc' };
98✔
392

98✔
393
    for (let c = 0; c < children.length; c++) {
98✔
394
        for (const m in meta) {
1,020✔
395
            if (meta[m] === children.item(c).localName) {
2,040✔
396
                this[m] = children.item(c);
6✔
397
            }
6✔
398
        }
2,040✔
399
    }
1,020✔
400

98✔
401
    return this;
98✔
402
};
1✔
403

1✔
404
/**
1✔
405
 * Return the dimensions of this shape
1✔
406
 *
1✔
407
 * @return {Object}             Dimensions
1✔
408
 */
1✔
409
SVGShape.prototype.getDimensions = function() {
1✔
410
    return {
255✔
411
        width: this.width,
255✔
412
        height: this.height
255✔
413
    };
255✔
414
};
1✔
415

1✔
416
/**
1✔
417
 * Set the dimensions of this shape
1✔
418
 *
1✔
419
 * @param {Number} width        Width
1✔
420
 * @param {Number} height       Height
1✔
421
 * @return {SVGShape}           Self reference
1✔
422
 */
1✔
423
SVGShape.prototype.setDimensions = function(width, height) {
1✔
424
    this.width = this._round(Math.max(0, Number.parseFloat(width)));
×
425
    this.dom.documentElement.setAttribute('width', this.width);
×
426
    this.height = this._round(Math.max(0, Number.parseFloat(height)));
×
427
    this.dom.documentElement.setAttribute('height', this.height);
×
428
    return this;
×
429
};
1✔
430

1✔
431
/**
1✔
432
 * Return the shape's viewBox (and set it if it doesn't exist yet)
1✔
433
 *
1✔
434
 * @param {Number} width        Width
1✔
435
 * @param {Height} height       Height
1✔
436
 * @return {Array}              Viewbox
1✔
437
 */
1✔
438
SVGShape.prototype.getViewbox = function(width, height) {
1✔
439
    if (!this.viewBox) {
46✔
440
        this.setViewbox(0, 0, width || this.width, height || this.height);
46!
441
    }
46✔
442

46✔
443
    return this.viewBox;
46✔
444
};
1✔
445

1✔
446
/**
1✔
447
 * Set the shape's viewBox
1✔
448
 *
1✔
449
 * @param {Number} x            X coordinate
1✔
450
 * @param {Number} y            Y coordinate
1✔
451
 * @param {Number} width        Width
1✔
452
 * @param {Number} height       Height
1✔
453
 * @return {Array}              Viewbox
1✔
454
 */
1✔
455
SVGShape.prototype.setViewbox = function(x, y, width, height) {
1✔
456
    if (Array.isArray(x)) {
46!
457
        this.viewBox = x.map(n => Number.parseFloat(n));
×
458
        while (this.viewBox.length < 4) {
×
459
            this.viewBox.push(0); // TODO?
×
460
        }
×
461
    } else {
46✔
462
        this.viewBox = [
46✔
463
            Number.parseFloat(x),
46✔
464
            Number.parseFloat(y),
46✔
465
            Number.parseFloat(width),
46✔
466
            Number.parseFloat(height)
46✔
467
        ];
46✔
468
    }
46✔
469

46✔
470
    this.dom.documentElement.setAttribute('viewBox', this.viewBox.join(' '));
46✔
471
    return this.viewBox;
46✔
472
};
1✔
473

1✔
474
/**
1✔
475
 * Complement the SVG shape by adding dimensions, padding and meta data
1✔
476
 *
1✔
477
 * @param {Function} cb         Callback
1✔
478
 */
1✔
479
SVGShape.prototype.complement = function(cb) {
1✔
480
    async.waterfall([
46✔
481
        // Prepare dimensions
46✔
482
        this._complementDimensions.bind(this),
46✔
483

46✔
484
        // Set padding
46✔
485
        this._addPadding.bind(this),
46✔
486

46✔
487
        // Set meta data
46✔
488
        this._addMetadata.bind(this)
46✔
489
    ], error => {
46✔
490
        // Save the transformed state
46✔
491
        this.svg.ready = new XMLSerializer().serializeToString(this.dom.documentElement);
46✔
492
        cb(error, this);
46✔
493
    });
46✔
494
};
1✔
495

1✔
496
/**
1✔
497
 * Complement the shape's dimensions
1✔
498
 *
1✔
499
 * @param {Function} cb         Callback
1✔
500
 */
1✔
501
SVGShape.prototype._complementDimensions = function(cb) {
1✔
502
    if (this.width && this.height) {
46✔
503
        this._setDimensions(cb);
46✔
504
    } else {
46!
505
        this._determineDimensions(this._setDimensions.bind(this, cb));
×
506
    }
×
507
};
1✔
508

1✔
509
/**
1✔
510
 * Determine the shape's dimension by rendering it
1✔
511
 *
1✔
512
 * @param {Function} cb         Callback
1✔
513
 */
1✔
514
SVGShape.prototype._determineDimensions = function(cb) {
1✔
515
    // Try to use a viewBox attribute for image determination
×
516
    if (this.viewBox !== false) {
×
517
        this.width = this._round(this.viewBox[2]);
×
518
        this.height = this._round(this.viewBox[3]);
×
519
    }
×
520

×
521
    // If the viewBox attribute didn't suffice: Render the SVG image
×
522
    if (!this.width || !this.height) {
×
523
        execFile(phantomjs, [dimensionsPhantomScript, this.getSVG(false), 'file://' + this.source.path], (err, stdout, stderr) => {
×
524
            if (err) {
×
525
                cb(err);
×
526
            } else if (stdout.length > 0) { // PhantomJS always outputs to stdout.
×
527
                const dimensions = JSON.parse(stdout.toString().trim());
×
528
                this.width = this._round(dimensions.width);
×
529
                this.height = this._round(dimensions.height);
×
530
                cb(null);
×
531
            } else if (stderr.length > 0) {
×
532
                cb(new Error(stderr.toString().trim()));
×
533
            } else {
×
534
                cb(new Error('PhantomJS didn\'t return dimensions for "' + this.name + '"'));
×
535
            }
×
536
        });
×
537
    } else {
×
538
        cb(null);
×
539
    }
×
540
};
1✔
541

1✔
542
/**
1✔
543
 * Round a number considering the given decimal place precision
1✔
544
 *
1✔
545
 * @param {Number} n            Number
1✔
546
 * @return {Number}             Rounded number
1✔
547
 */
1✔
548
SVGShape.prototype._round = function(n) {
1✔
549
    return Math.round(n * this._precision) / this._precision;
54✔
550
};
1✔
551

1✔
552
/**
1✔
553
 * Scale the shape if necessary
1✔
554
 *
1✔
555
 * @param {Function} cb         Callback
1✔
556
 */
1✔
557
SVGShape.prototype._setDimensions = function(cb) {
1✔
558
    // Ensure the original viewBox is set
46✔
559
    this.getViewbox(this.width, this.height);
46✔
560

46✔
561
    const includePadding = ['padding', 'icon'].includes(this.config.spacing.box);
46✔
562
    const forceScale = this.config.spacing.box === 'icon';
46✔
563
    const horizontalPadding = includePadding * Math.max(0, this.config.spacing.padding.right + this.config.spacing.padding.left);
46✔
564
    const width = this.width + horizontalPadding;
46✔
565
    const verticalPadding = includePadding * Math.max(0, this.config.spacing.padding.top + this.config.spacing.padding.bottom);
46✔
566
    const height = this.height + verticalPadding;
46✔
567

46✔
568
    // Does the shape need to be scaled?
46✔
569
    if (width > this.config.dimension.maxWidth || height > this.config.dimension.maxHeight || (forceScale && width < this.config.dimension.maxWidth && height < this.config.dimension.maxHeight)) {
46!
570
        const maxWidth = this.config.dimension.maxWidth - horizontalPadding;
6✔
571
        const maxHeight = this.config.dimension.maxHeight - verticalPadding;
6✔
572
        this._scale = Math.min(maxWidth / this.width, maxHeight / this.height);
6✔
573
        this.width = Math.min(maxWidth, this._round(this.width * this._scale));
6✔
574
        this.height = Math.min(maxHeight, this._round(this.height * this._scale));
6✔
575
    }
6✔
576

46✔
577
    // In "icon" box sizing mode: Resize bounding box and center shape by adding padding
46✔
578
    if (forceScale) {
46!
579
        const diffWidth = this.config.dimension.maxWidth - this.width - horizontalPadding;
×
580
        const diffHeight = this.config.dimension.maxHeight - this.height - verticalPadding;
×
581
        this.config.spacing.padding.left += diffWidth / 2;
×
582
        this.config.spacing.padding.right += diffWidth / 2;
×
583
        this.config.spacing.padding.top += diffHeight / 2;
×
584
        this.config.spacing.padding.bottom += diffHeight / 2;
×
585
    }
×
586

46✔
587
    const dimensions = this.getDimensions();
46✔
588
    for (const attr in dimensions) {
46✔
589
        this.dom.documentElement.setAttribute(attr, dimensions[attr]);
92✔
590
    }
92✔
591

46✔
592
    cb(null);
46✔
593
};
1✔
594

1✔
595
/**
1✔
596
 * Add padding to this shape
1✔
597
 *
1✔
598
 * @param {Function} cb         Callback
1✔
599
 */
1✔
600
SVGShape.prototype._addPadding = function(cb) {
1✔
601
    const { padding } = this.config.spacing;
46✔
602
    if (padding.top || padding.right || padding.bottom || padding.left) {
46!
603
        // Update viewBox
×
604
        const viewBox = this.getViewbox();
×
605
        viewBox[0] -= this.config.spacing.padding.left / this._scale;
×
606
        viewBox[1] -= this.config.spacing.padding.top / this._scale;
×
607
        viewBox[2] += (this.config.spacing.padding.right + this.config.spacing.padding.left) / this._scale;
×
608
        viewBox[3] += (this.config.spacing.padding.top + this.config.spacing.padding.bottom) / this._scale;
×
609
        this.setViewbox(viewBox.map(this._round.bind(this)));
×
610

×
611
        // Update dimensions
×
612
        this.setDimensions(this.width + this.config.spacing.padding.right + this.config.spacing.padding.left, this.height + this.config.spacing.padding.top + this.config.spacing.padding.bottom);
×
613
    }
×
614

46✔
615
    cb(null);
46✔
616
};
1✔
617

1✔
618
/**
1✔
619
 * Add metadata to this shape
1✔
620
 *
1✔
621
 * @param {Function} cb         Callback
1✔
622
 */
1✔
623
SVGShape.prototype._addMetadata = function(cb) {
1✔
624
    const ariaLabelledBy = [];
46✔
625

46✔
626
    // Check if description meta data is available
46✔
627
    if ('description' in this.meta && _.isString(this.meta.description) && this.meta.description.length) {
46!
628
        if (!this.description) {
×
629
            this.description = this.dom.documentElement.insertBefore(this.dom.createElementNS(this.DEFAULT_SVG_NAMESPACE, 'desc'), this.dom.documentElement.firstChild);
×
630
        }
×
631

×
632
        this.description.textContent = this.meta.description;
×
633
        this.description.setAttribute('id', this.id + '-desc');
×
634
        ariaLabelledBy.push(this.id + '-desc');
×
635
    }
×
636

46✔
637
    // Check if title meta data is available
46✔
638
    if ('title' in this.meta && _.isString(this.meta.title) && this.meta.title.length) {
46!
639
        if (!this.title) {
×
640
            this.title = this.dom.documentElement.insertBefore(this.dom.createElementNS(this.DEFAULT_SVG_NAMESPACE, 'title'), this.dom.documentElement.firstChild);
×
641
        }
×
642

×
643
        this.title.textContent = this.meta.title;
×
644
        this.title.setAttribute('id', this.id + '-title');
×
645
        ariaLabelledBy.push(this.id + '-title');
×
646
    }
×
647

46✔
648
    if (ariaLabelledBy.length) {
46!
649
        this.dom.documentElement.setAttribute('aria-labelledby', ariaLabelledBy.join(' '));
×
650
    } else if (this.dom.documentElement.hasAttribute('aria-labelledby')) {
46!
651
        this.dom.documentElement.removeAttribute('aria-labelledby');
×
652
    }
×
653

46✔
654
    cb(null);
46✔
655
};
1✔
656

1✔
657
/**
1✔
658
 * Apply a namespace prefix to all IDs within the SVG document
1✔
659
 *
1✔
660
 * @param {String} ns               ID namespace
1✔
661
 */
1✔
662
SVGShape.prototype.setNamespace = function(ns) {
1✔
663
    const namespaceIds = Boolean(this.spriter.config.svg.namespaceIDs);
90✔
664
    const namespaceClassnames = Boolean(this.spriter.config.svg.namespaceClassnames);
90✔
665
    const namespaceIDPrefix = this.spriter.config.svg.namespaceIDPrefix || '';
90✔
666
    if (!this._namespaced && (namespaceIds || namespaceClassnames)) {
90!
667
        // Ensure the shape has been complemented before
46✔
668
        if (!this.svg.ready) {
46!
669
            const error = new Error('Shape namespace cannot be set before complementing');
×
670
            error.name = 'NotPermittedError';
×
671
            error.errno = 1419162245;
×
672
            throw error;
×
673
        }
×
674

46✔
675
        const select = xpath.useNamespaces({ svg: this.DEFAULT_SVG_NAMESPACE, xlink: this.XLINK_NAMESPACE });
46✔
676
        let substIds = null;
46✔
677
        let substClassnames = null;
46✔
678

46✔
679
        // If IDs should be namespaced
46✔
680
        if (namespaceIds) {
46✔
681
            // Build an ID substitution table (and alter the elements' IDs accordingly)
46✔
682
            substIds = {};
46✔
683
            select('//*[@id]', this.dom).forEach(elem => {
46✔
684
                const id = elem.getAttribute('id');
1,450✔
685
                const substId = namespaceIDPrefix + ns + id;
1,450✔
686
                substIds['#' + id] = substId;
1,450✔
687
                elem.setAttribute('id', substId);
1,450✔
688
            });
46✔
689

46✔
690
            // Substitute ID references in xlink:href attributes
46✔
691
            select('//@xlink:href', this.dom).forEach(xlink => {
46✔
692
                const xlinkValue = xlink.nodeValue;
710✔
693
                if (!xlinkValue.startsWith('data:') && xlinkValue in substIds) {
710✔
694
                    xlink.ownerElement.setAttribute('xlink:href', '#' + substIds[xlinkValue]);
710✔
695
                }
710✔
696
            });
46✔
697

46✔
698
            // Substitute ID references in referencing attributes
46✔
699
            svgReferenceProperties.forEach(refProperty => {
46✔
700
                select('//@' + refProperty, this.dom).forEach(ref => {
414✔
701
                    ref.ownerElement.setAttribute(ref.localName, this._replaceIdAndClassnameReferences(ref.nodeValue, substIds, substClassnames, false));
1,638✔
702
                });
414✔
703
            });
46✔
704

46✔
705
            // Substitute ID references in aria-labelledby attribute
46✔
706
            if (this.dom.documentElement.hasAttribute('aria-labelledby')) {
46!
707
                this.dom.documentElement.setAttribute('aria-labelledby', this.dom.documentElement.getAttribute('aria-labelledby').split(' ').map(label => {
×
708
                    return '#' + label in substIds ? substIds['#' + label] : label;
×
709
                }).join(' '));
×
710
            }
×
711
        }
46✔
712

46✔
713
        // If CSS class names should be namespaced
46✔
714
        if (namespaceClassnames) {
46✔
715
            // Build a class name substitution table (and alter the elements' class names accordingly)
46✔
716
            substClassnames = {};
46✔
717
            select('//*[@class]', this.dom).forEach(elem => {
46✔
718
                const elemClassnames = [];
6✔
719
                elem.getAttribute('class').split(' ')
6✔
720
                    .filter(classname => classname.trim())
6✔
721
                    .forEach(classname => {
6✔
722
                        const substClassname = ns + classname;
6✔
723
                        substClassnames['.' + classname] = substClassname;
6✔
724
                        elemClassnames.push(substClassname);
6✔
725
                    });
6✔
726
                elem.setAttribute('class', elemClassnames.join(' '));
6✔
727
            });
46✔
728
        }
46✔
729

46✔
730
        // Substitute ID references in <style> elements
46✔
731
        const style = select('//svg:style', this.dom);
46✔
732
        if (style.length) {
46✔
733
            select('//svg:style', this.dom).forEach(style => {
6✔
734
                style.textContent = csso.minifyBlock(this._replaceIdAndClassnameReferences(style.textContent, substIds, substClassnames, true), { restructure: false }).css;
6✔
735
            });
6✔
736
        }
6✔
737

46✔
738
        this._namespaced = true;
46✔
739
    }
46✔
740
};
1✔
741

1✔
742
/**
1✔
743
 * Reset the shapes namespace
1✔
744
 */
1✔
745
SVGShape.prototype.resetNamespace = function() {
1✔
746
    if (this._namespaced && Boolean(this.spriter.config.svg.namespaceIDs)) {
×
747
        this._namespaced = false;
×
748
        this.dom = new DOMParser().parseFromString(this.svg.ready);
×
749
    }
×
750
};
1✔
751

1✔
752
/**
1✔
753
 * Replace ID references
1✔
754
 *
1✔
755
 * @param {String} str                  String
1✔
756
 * @param {Object} substIds             ID substitutions
1✔
757
 * @param {Object} substClassnames      Class name substitutions
1✔
758
 * @param {Boolean} selectors           Substitute CSS selectors
1✔
759
 * @return {String}                     String with replaced ID and class name references
1✔
760
 */
1✔
761
SVGShape.prototype._replaceIdAndClassnameReferences = function(str, substIds, substClassnames, selectors) {
1✔
762
    // If ID replacement is to be applied: Replace url()-style ID references
1,644✔
763
    if (substIds !== null) {
1,644✔
764
        str = str.replace(/url\s*\(\s*["']?([^\s"')]+)["']?\s*\)/g, (match, id) => {
1,644✔
765
            return 'url(' + (id in substIds ? '#' + substIds[id] : id) + ')';
710!
766
        });
1,644✔
767
    }
1,644✔
768

1,644✔
769
    return selectors ? this._replaceIdAndClassnameReferencesInCssSelectors(str, cssom.parse(str).cssRules, substIds, substClassnames) : str;
1,644✔
770
};
1✔
771

1✔
772
/**
1✔
773
 * Recursively replace ID references in CSS selectors
1✔
774
 *
1✔
775
 * @param {String} str                  Original CSS text
1✔
776
 * @param {Array} rules                 CSS rules
1✔
777
 * @param {Object} substIds             ID substitutions
1✔
778
 * @param {Object} substClassnames      Class name substitutions
1✔
779
 * @return {String}                     Substituted CSS text
1✔
780
 */
1✔
781
SVGShape.prototype._replaceIdAndClassnameReferencesInCssSelectors = function(str, rules, substIds, substClassnames) {
1✔
782
    let css = '';
18✔
783

18✔
784
    rules.forEach(rule => {
18✔
785
        let selText = rule.selectorText;
42✔
786

42✔
787
        // @-rule
42✔
788
        if (selText === undefined) {
42✔
789
            // If there's a key text: Copy the CSS rule
12✔
790
            if (rule.keyText) {
12!
791
                css += str.substr(rule.__starts, rule.__ends);
×
792

×
793
            // Else: Recursively process rule content
×
794
            } else if (Array.isArray(rule.cssRules)) {
12✔
795
                css += str.substring(rule.__starts, rule.cssRules[0].__starts) + this._replaceIdAndClassnameReferencesInCssSelectors(str, rule.cssRules, substIds, substClassnames) + str.substring(rule.cssRules[rule.cssRules.length - 1].__ends, rule.__ends);
12✔
796
            }
12✔
797

12✔
798
        // Regular selector
12✔
799
        } else {
42✔
800
            const origSelText = selText;
30✔
801
            const csssel = new CssSelectorParser();
30✔
802
            let sel = csssel.parse(selText);
30✔
803
            const ids = [];
30✔
804
            let classnames = [];
30✔
805
            const classnameFilter = classname => {
30✔
806
                if ('.' + classname in substClassnames) {
12✔
807
                    classnames.push(classname);
12✔
808
                }
12✔
809
            };
30✔
810

30✔
811
            const idOrClassSubstitution = sel => {
30✔
812
                // If ID substitution should be applied: Search for an ID
30✔
813
                if ('id' in sel.rule && substIds !== null && '#' + sel.rule.id in substIds) {
30✔
814
                    ids.push(sel.rule.id);
24✔
815
                }
24✔
816

30✔
817
                // If class name substitution should be applied: Search for class names
30✔
818
                if ('classNames' in sel.rule && substClassnames !== null && Array.isArray(sel.rule.classNames)) {
30✔
819
                    sel.rule.classNames.forEach(classname => {
12✔
820
                        classnameFilter(classname);
12✔
821
                    });
12✔
822
                }
12✔
823
            };
30✔
824

30✔
825
            // If there are multiple subselectors, substitute all of them
30✔
826
            if ('selectors' in sel) {
30!
827
                sel.selectors.forEach(selector => {
×
828
                    idOrClassSubstitution(selector);
×
829
                });
×
830
            }
×
831

30✔
832
            // While there are nested rules: Substitute and recurse
30✔
833
            while (typeof sel === 'object' && 'rule' in sel) {
30✔
834
                idOrClassSubstitution(sel);
30✔
835
                sel = sel.rule;
30✔
836
            }
30✔
837

30✔
838
            // Substitute IDs within the selector
30✔
839
            if (ids.length) {
30✔
840
                ids.sort((a, b) => b.length - a.length)
24✔
841
                    .forEach(id => {
24✔
842
                        selText = selText.split('#' + id).join('#' + substIds['#' + id]);
24✔
843
                    });
24✔
844
            }
24✔
845

30✔
846
            // Substitute class names within the selector
30✔
847
            if (classnames.length) {
30✔
848
                classnames = [...new Set(classnames)]
12✔
849
                    .sort((a, b) => b.length - a.length)
12✔
850
                    .forEach(classname => {
12✔
851
                        selText = selText.split('.' + classname).join('.' + substClassnames['.' + classname]);
12✔
852
                    });
12✔
853
            }
12✔
854

30✔
855
            // Rebuild the selector
30✔
856
            css += selText + str.substring(rule.__starts + origSelText.length, rule.__ends);
30✔
857
        }
30✔
858
    });
18✔
859

18✔
860
    return css;
18✔
861
};
1✔
862

1✔
863
/**
1✔
864
 * Create distribute to several copies (if configured)
1✔
865
 *
1✔
866
 * @return {Array}              Displaced copies
1✔
867
 */
1✔
868
SVGShape.prototype.distribute = function() {
1✔
869
    const copies = [];
46✔
870
    const alignments = this.align.slice(0);
46✔
871
    const align = alignments.shift();
46✔
872
    const { base } = this;
46✔
873
    this.base = format(align[0], this.base);
46✔
874
    this.id = this.base + (this.state ? this.config.id.pseudo + this.state : '');
46✔
875
    this.align = align[1];
46✔
876
    copies.push(this);
46✔
877

46✔
878
    // Run through all remaining alignments
46✔
879
    alignments.forEach(alignment => {
46✔
880
        const copy = _.merge(new SVGShape(this.source, this.spriter), this);
6✔
881
        copy.base = format(alignment[0], base);
6✔
882
        copy.id = copy.base + (this.state ? this.config.id.pseudo + this.state : '');
6!
883
        copy.align = alignment[1];
6✔
884
        copy.master = this;
6✔
885
        copies.push(copy);
6✔
886
    });
46✔
887

46✔
888
    this.copies = alignments.length;
46✔
889
    return copies;
46✔
890
};
1✔
891

1✔
892
/**
1✔
893
 * Module export (constructor wrapper)
1✔
894
 *
1✔
895
 * @param {String} svg          SVG content
1✔
896
 * @param {String} name         Name part or the file path
1✔
897
 * @param {String} file         Absolute file path
1✔
898
 * @param {Object} config       SVG shape configuration
1✔
899
 * @return {SVGShape}           SVGShape instance
1✔
900
 */
1✔
901
module.exports = function(svg, name) {
1✔
902
    return new SVGShape(svg, name);
46✔
903
};
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc