├── .gitignore
├── LICENSE
├── README.md
├── dist
├── streaming-svg-parser.js
├── streaming-svg-parser.js.map
├── streaming-svg-parser.min.js
└── streaming-svg-parser.min.js.map
├── index.js
├── lib
├── NumberParser.js
├── createStreamingSVGParser.js
├── getElementFillColor.js
└── getPointsFromPathData.js
├── package-lock.json
├── package.json
├── perf
├── data
│ └── .gitignore
├── downloadFiles.sh
└── parse_italy.js
└── test
└── createStreamingSVGParser.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .nyc_output
3 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2021-2025 Andrei Kashcha
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # streaming-svg-parser
2 |
3 | Very fast parser of SVG (and likely XML) files, that doesn't need the entire file to start
4 | parsing it.
5 |
6 | ## install
7 |
8 | You can get this package via npm:
9 |
10 | ``` sh
11 | npm install streaming-svg-parser
12 | ```
13 |
14 | Or via CDN:
15 |
16 | ```
17 |
18 | ```
19 |
20 | ## usage
21 |
22 | You can find example of this parser in https://anvaka.github.io/map-of-reddit/ and
23 | more specifically - [here](https://github.com/anvaka/map-of-reddit/blob/756ece61fdf246be10076994f7f5876a7af002e8/src/lib/createSVGLoader.js#L10).
24 | I need to document this better, but here is a quick demo of how the parser could
25 | work to print indented elements along with their attributes:
26 |
27 | ``` js
28 | // If you are using node.js, you can use require() to load the parser.
29 | //
30 | // Otherwise, if you used CDN with tag,
31 | // `streamingSVGParser` will be available as global variable:
32 | const streamingSVGParser = require('streaming-svg-parser');
33 |
34 | let indent = '';
35 | let parseText = streamingSVGParser.createStreamingSVGParser(
36 | openElement => {
37 | // attributes are in a map, let's print it:
38 | let attributes = Array.from(openElement.attributes)
39 | .map(pair => pair.join('='))
40 | .join(' ');
41 |
42 | console.log(indent + 'Open ' + openElement.tagName + ' ' + attributes);
43 | indent += ' ';
44 | },
45 | closeElement => {
46 | indent = indent.substring(2);
47 | console.log(indent + 'Close ' + closeElement.tagName);
48 | }
49 | );
50 | parseText('');
51 | parseText('');
54 | ```
55 |
56 | This will print:
57 |
58 | ```
59 | Open svg clip-rule=evenodd viewBox=0 0 42 42
60 | Open g id=my-id
61 | Close g
62 | Close svg
63 | ```
64 |
65 | [Open in jsbin](https://jsbin.com/pikilibadu/2/edit?html,js,output)
66 |
67 | Note that `parseText()` was fed incomplete chunks of svg, which makes this parser
68 | ideal when you load large SVG files over the network but want to process them without
69 | waiting for the entire file to be loaded.
70 |
71 | ## XML Support
72 |
73 | While originally this library is written for SVG, it should work for simple XML files
74 | as well. Please let me know if you find anything missing.
75 |
76 | ## License
77 |
78 | MIT
--------------------------------------------------------------------------------
/dist/streaming-svg-parser.js:
--------------------------------------------------------------------------------
1 | var streamingSVGParser = (() => {
2 | var __getOwnPropNames = Object.getOwnPropertyNames;
3 | var __commonJS = (cb, mod) => function __require() {
4 | return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
5 | };
6 |
7 | // lib/createStreamingSVGParser.js
8 | var require_createStreamingSVGParser = __commonJS({
9 | "lib/createStreamingSVGParser.js"(exports, module) {
10 | var WAIT_TAG_OPEN = 1;
11 | var READ_TAG_OR_COMMENT = 2;
12 | var READ_TAG = 3;
13 | var READ_TAG_CLOSE = 4;
14 | var READ_COMMENT = 5;
15 | var WAIT_TAG_CLOSE = 6;
16 | var WAIT_ATTRIBUTE_OR_CLOSE_TAG = 7;
17 | var READ_ATTRIBUTE_NAME = 8;
18 | var READ_ATTRIBUTE_VALUE = 9;
19 | var WAIT_ATTRIBUTE_VALUE = 10;
20 | var WAIT_ATTRIBUTE_ASSIGNMENT_OR_NEXT_ATTRIBUTE = 11;
21 | var A = "A".charCodeAt(0);
22 | var Z = "z".charCodeAt(0);
23 | var whitespace = /\s/;
24 | module.exports = function createStreamingSVGParser(notifyTagOpen, notifyTagClose, generateAsync) {
25 | let currentState = WAIT_TAG_OPEN;
26 | let closeAttributeSymbol;
27 | let currentTagName;
28 | let currentAttributeName;
29 | let lastElement;
30 | let buffer;
31 | let innerText;
32 | if (notifyTagClose === void 0) {
33 | notifyTagClose = Function.prototype;
34 | }
35 | return generateAsync ? processChunkAsync : processChunkSync;
36 | function processChunkAsync(chunk) {
37 | return new Promise((resolve) => iterateSymbolsAsync(chunk, 0, resolve));
38 | }
39 | function iterateSymbolsAsync(chunk, idx, resolve) {
40 | let start = performance.now();
41 | let processed = 0;
42 | while (idx < chunk.length) {
43 | processSymbol(chunk[idx]);
44 | idx += 1;
45 | processed += 1;
46 | if (processed > 3e4) {
47 | let elapsed = performance.now() - start;
48 | if (elapsed > 32) {
49 | setTimeout(() => iterateSymbolsAsync(chunk, idx, resolve), 0);
50 | return;
51 | }
52 | }
53 | }
54 | resolve();
55 | }
56 | function processChunkSync(chunk) {
57 | return iterateSymbols(chunk, 0);
58 | }
59 | function iterateSymbols(chunk, idx) {
60 | let processed = 0;
61 | while (idx < chunk.length) {
62 | processSymbol(chunk[idx]);
63 | idx += 1;
64 | }
65 | }
66 | function processSymbol(ch) {
67 | switch (currentState) {
68 | case WAIT_TAG_OPEN:
69 | if (ch === "<")
70 | currentState = READ_TAG_OR_COMMENT;
71 | else if (innerText) {
72 | innerText.push(ch);
73 | }
74 | break;
75 | case WAIT_TAG_CLOSE:
76 | if (ch === ">")
77 | currentState = WAIT_TAG_OPEN;
78 | break;
79 | case READ_TAG_OR_COMMENT:
80 | if (ch === "!" || ch === "?") {
81 | buffer = [ch];
82 | currentState = READ_COMMENT;
83 | } else if (ch === "/") {
84 | if (innerText) {
85 | lastElement.innerText = innerText.join("");
86 | innerText = null;
87 | }
88 | notifyTagClose(lastElement);
89 | if (lastElement)
90 | lastElement = lastElement.parent;
91 | currentState = WAIT_TAG_CLOSE;
92 | innerText = null;
93 | } else {
94 | currentState = READ_TAG;
95 | buffer = [ch];
96 | }
97 | break;
98 | case READ_COMMENT: {
99 | buffer.push(ch);
100 | let l = buffer.length;
101 | if (buffer.length > 3 && buffer[l - 1] === ">" && buffer[l - 2] === "-" && buffer[l - 3] === "-") {
102 | currentState = WAIT_TAG_OPEN;
103 | innerText = null;
104 | } else if (buffer[0] === "!" && (buffer.length > 1 && buffer[1] !== "-") || buffer[0] === "?")
105 | currentState = WAIT_TAG_CLOSE;
106 | break;
107 | }
108 | case READ_TAG: {
109 | if (isTagNameCharacter(ch)) {
110 | buffer.push(ch);
111 | } else if (ch === "/") {
112 | } else {
113 | currentTagName = buffer.join("");
114 | currentState = WAIT_ATTRIBUTE_OR_CLOSE_TAG;
115 | let parent = lastElement;
116 | lastElement = {
117 | tagName: currentTagName,
118 | attributes: /* @__PURE__ */ new Map(),
119 | parent,
120 | children: []
121 | };
122 | if (parent)
123 | parent.children.push(lastElement);
124 | if (ch === ">")
125 | finishTag();
126 | }
127 | break;
128 | }
129 | case READ_TAG_CLOSE: {
130 | if (isTagNameCharacter(ch)) {
131 | buffer.push(ch);
132 | } else if (ch === ">") {
133 | let closedTag = buffer.join("");
134 | if (closedTag !== currentTagName) {
135 | throw new Error("Expected " + currentTagName + " to be closed, but got " + closedTag);
136 | }
137 | }
138 | break;
139 | }
140 | case WAIT_ATTRIBUTE_OR_CLOSE_TAG: {
141 | if (ch === ">") {
142 | finishTag();
143 | } else if (isTagNameCharacter(ch)) {
144 | buffer = [ch];
145 | currentState = READ_ATTRIBUTE_NAME;
146 | } else {
147 | buffer.push(ch);
148 | }
149 | break;
150 | }
151 | case READ_ATTRIBUTE_NAME: {
152 | if (!isTagNameCharacter(ch)) {
153 | currentAttributeName = buffer.join("");
154 | if (ch === "=")
155 | currentState = WAIT_ATTRIBUTE_VALUE;
156 | else if (ch === ">") {
157 | lastElement.attributes.set(currentAttributeName, true);
158 | finishTag();
159 | } else
160 | currentState = WAIT_ATTRIBUTE_ASSIGNMENT_OR_NEXT_ATTRIBUTE;
161 | } else {
162 | buffer.push(ch);
163 | }
164 | break;
165 | }
166 | case WAIT_ATTRIBUTE_ASSIGNMENT_OR_NEXT_ATTRIBUTE: {
167 | if (ch === "=") {
168 | currentState = WAIT_ATTRIBUTE_VALUE;
169 | } else if (isTagNameCharacter(ch)) {
170 | currentState = READ_ATTRIBUTE_NAME;
171 | lastElement.attributes.set(buffer.join(""), true);
172 | buffer = [ch];
173 | } else if (ch === ">") {
174 | lastElement.attributes.set(buffer.join(""), true);
175 | finishTag();
176 | }
177 | break;
178 | }
179 | case WAIT_ATTRIBUTE_VALUE: {
180 | if (ch === '"' || ch === "'" || !isWhiteSpace(ch)) {
181 | buffer = [];
182 | currentState = READ_ATTRIBUTE_VALUE;
183 | closeAttributeSymbol = ch;
184 | }
185 | break;
186 | }
187 | case READ_ATTRIBUTE_VALUE: {
188 | if (ch === closeAttributeSymbol) {
189 | currentState = WAIT_ATTRIBUTE_OR_CLOSE_TAG;
190 | lastElement.attributes.set(currentAttributeName, buffer.join(""));
191 | currentAttributeName = null;
192 | buffer = [];
193 | } else {
194 | buffer.push(ch);
195 | }
196 | break;
197 | }
198 | }
199 | }
200 | function finishTag() {
201 | let l = buffer.length;
202 | notifyTagOpen(lastElement);
203 | if (l > 0 && buffer[l - 1] === "/") {
204 | notifyTagClose(lastElement);
205 | if (lastElement)
206 | lastElement = lastElement.parent;
207 | }
208 | currentState = WAIT_TAG_OPEN;
209 | innerText = [];
210 | currentAttributeName = null;
211 | }
212 | };
213 | function isTagNameCharacter(ch) {
214 | let code = ch.charCodeAt(0);
215 | return A <= code && code <= Z || ch === "_" || ch === "-" || ch === ":";
216 | }
217 | function isWhiteSpace(ch) {
218 | return whitespace.test(ch);
219 | }
220 | }
221 | });
222 |
223 | // lib/NumberParser.js
224 | var require_NumberParser = __commonJS({
225 | "lib/NumberParser.js"(exports, module) {
226 | var CharacterLookup = {
227 | "0": 0,
228 | "1": 1,
229 | "2": 2,
230 | "3": 3,
231 | "4": 4,
232 | "5": 5,
233 | "6": 6,
234 | "7": 7,
235 | "8": 8,
236 | "9": 9
237 | };
238 | var NumberParser = class {
239 | constructor() {
240 | this.value = 0;
241 | this.fractionValue = 0;
242 | this.divider = 1;
243 | this.exponent = 0;
244 | this.isNegative = false;
245 | this.hasValue = false;
246 | this.hasFraction = false;
247 | this.hasExponent = false;
248 | }
249 | getValue() {
250 | let value = this.value;
251 | if (this.hasFraction) {
252 | value += this.fractionValue / this.divider;
253 | }
254 | if (this.hasExponent) {
255 | value *= Math.pow(10, this.exponent);
256 | }
257 | if (this.isNegative) {
258 | return -value;
259 | }
260 | return value;
261 | }
262 | reset() {
263 | this.value = 0;
264 | this.fractionValue = 0;
265 | this.divider = 1;
266 | this.exponent = 0;
267 | this.isNegative = false;
268 | this.hasValue = false;
269 | this.hasFraction = false;
270 | this.hasExponent = false;
271 | }
272 | addCharacter(ch) {
273 | this.hasValue = true;
274 | if (ch === "-") {
275 | this.isNegative = true;
276 | return;
277 | }
278 | if (ch === ".") {
279 | if (this.hasFraction)
280 | throw new Error("Already has fractional part!");
281 | this.hasFraction = true;
282 | return;
283 | }
284 | if (ch === "e") {
285 | if (this.hasExponent)
286 | throw new Error("Already has exponent");
287 | this.hasExponent = true;
288 | this.exponent = 0;
289 | return;
290 | }
291 | let numValue = CharacterLookup[ch];
292 | if (numValue === void 0)
293 | throw new Error("Not a digit: " + ch);
294 | if (this.hasExponent) {
295 | this.exponent = this.exponent * 10 + numValue;
296 | } else if (this.hasFraction) {
297 | this.fractionValue = this.fractionValue * 10 + numValue;
298 | this.divider *= 10;
299 | } else {
300 | this.value = this.value * 10 + numValue;
301 | }
302 | }
303 | };
304 | module.exports = NumberParser;
305 | }
306 | });
307 |
308 | // lib/getPointsFromPathData.js
309 | var require_getPointsFromPathData = __commonJS({
310 | "lib/getPointsFromPathData.js"(exports, module) {
311 | var NumberParser = require_NumberParser();
312 | var processCommand = {
313 | M(points, lastNumbers) {
314 | if (lastNumbers.length % 2 !== 0) {
315 | throw new Error("Expected an even number of numbers for M command");
316 | }
317 | if (points.length === 0) {
318 | for (let i = 0; i < lastNumbers.length; i += 2) {
319 | points.push([lastNumbers[i], lastNumbers[i + 1]]);
320 | }
321 | } else {
322 | throw new Error('Only one "Move" command per path is expected');
323 | }
324 | },
325 | m(points, lastNumbers) {
326 | let lx = 0, ly = 0;
327 | if (points.length > 0 && lastNumbers.length > 1) {
328 | let last = points[points.length - 1];
329 | lx = last[0] + lastNumbers[0];
330 | ly = last[1] + lastNumbers[1];
331 | ;
332 | }
333 | for (let i = 2; i < lastNumbers.length; i += 2) {
334 | let x = lx + lastNumbers[i];
335 | let y = ly + lastNumbers[i + 1];
336 | points.push([x, y]);
337 | lx = x;
338 | ly = y;
339 | }
340 | },
341 | L(points, lastNumbers) {
342 | for (let i = 0; i < lastNumbers.length; i += 2) {
343 | points.push([lastNumbers[i], lastNumbers[i + 1]]);
344 | }
345 | },
346 | l(points, lastNumbers) {
347 | let lx = 0, ly = 0;
348 | if (points.length > 0) {
349 | let last = points[points.length - 1];
350 | lx = last[0];
351 | ly = last[1];
352 | }
353 | for (let i = 0; i < lastNumbers.length; i += 2) {
354 | let x = lx + lastNumbers[i];
355 | let y = ly + lastNumbers[i + 1];
356 | points.push([x, y]);
357 | lx = x;
358 | ly = y;
359 | }
360 | },
361 | H(points, lastNumbers) {
362 | let y = 0;
363 | if (points.length > 0) {
364 | y = points[points.length - 1][1];
365 | }
366 | for (let i = 0; i < lastNumbers.length; i += 1) {
367 | let x = lastNumbers[i];
368 | points.push([x, y]);
369 | }
370 | },
371 | h(points, lastNumbers) {
372 | let y = 0, lx = 0;
373 | if (points.length > 0) {
374 | lx = points[points.length - 1][0];
375 | y = points[points.length - 1][1];
376 | }
377 | for (let i = 0; i < lastNumbers.length; i += 1) {
378 | let x = lx + lastNumbers[i];
379 | points.push([x, y]);
380 | lx = x;
381 | }
382 | },
383 | V(points, lastNumbers) {
384 | let x = 0;
385 | if (points.length > 0) {
386 | x = points[points.length - 1][0];
387 | }
388 | for (let i = 0; i < lastNumbers.length; i += 1) {
389 | points.push([x, lastNumbers[i]]);
390 | }
391 | },
392 | v(points, lastNumbers) {
393 | let ly = 0, x = 0;
394 | if (points.length > 0) {
395 | x = points[points.length - 1][0];
396 | ly = points[points.length - 1][1];
397 | }
398 | for (let i = 0; i < lastNumbers.length; i += 1) {
399 | let y = ly + lastNumbers[i];
400 | points.push([x, y]);
401 | ly = y;
402 | }
403 | }
404 | };
405 | function getPointsFromPathData(d) {
406 | let numParser = new NumberParser();
407 | let idx = 0;
408 | let l = d.length;
409 | let ch;
410 | let lastNumbers, lastCommand;
411 | let points = [];
412 | while (idx < l) {
413 | ch = d[idx];
414 | if (ch in processCommand) {
415 | if (numParser.hasValue) {
416 | lastNumbers.push(numParser.getValue());
417 | }
418 | numParser.reset();
419 | if (lastNumbers) {
420 | lastCommand(points, lastNumbers);
421 | }
422 | lastCommand = processCommand[ch];
423 | lastNumbers = [];
424 | } else if (ch === " " || ch === ",") {
425 | if (numParser.hasValue) {
426 | lastNumbers.push(numParser.getValue());
427 | numParser.reset();
428 | }
429 | } else if (ch === "Z" || ch === "z") {
430 | } else if (numParser.hasValue && ch === "-") {
431 | lastNumbers.push(numParser.getValue());
432 | numParser.reset();
433 | numParser.addCharacter(ch);
434 | } else {
435 | numParser.addCharacter(ch);
436 | }
437 | idx += 1;
438 | }
439 | if (numParser.hasValue) {
440 | lastNumbers.push(numParser.getValue());
441 | }
442 | if (lastNumbers) {
443 | lastCommand(points, lastNumbers);
444 | }
445 | return points;
446 | }
447 | module.exports = getPointsFromPathData;
448 | }
449 | });
450 |
451 | // lib/getElementFillColor.js
452 | var require_getElementFillColor = __commonJS({
453 | "lib/getElementFillColor.js"(exports, module) {
454 | module.exports = function getElementFillColor(el) {
455 | return getColor(el.attributes.get("fill") || el.attributes.get("style"));
456 | };
457 | function getColor(styleValue) {
458 | if (styleValue[0] === "#") {
459 | if (styleValue.length === 1 + 6) {
460 | let r = Number.parseInt(styleValue.substr(1, 2), 16);
461 | let g = Number.parseInt(styleValue.substr(3, 2), 16);
462 | let b = Number.parseInt(styleValue.substr(5, 2), 16);
463 | return hexColor([r, g, b]);
464 | }
465 | if (styleValue.length === 1 + 3 || styleValue.length === 1 + 4) {
466 | let rs = styleValue.substr(1, 1);
467 | let gs = styleValue.substr(2, 1);
468 | let bs = styleValue.substr(3, 1);
469 | let r = Number.parseInt(rs + rs, 16);
470 | let g = Number.parseInt(gs + gs, 16);
471 | let b = Number.parseInt(bs + bs, 16);
472 | return hexColor([r, g, b]);
473 | }
474 | throw new Error("Cannot parse this color yet " + styleValue);
475 | } else if (styleValue.startsWith("rgba")) {
476 | let colors = styleValue.substr(5).split(/,/).map((x) => Number.parseFloat(x));
477 | colors[3] = Math.round(colors[3] * 255);
478 | return alphaHexColor(colors);
479 | }
480 | let rgb = styleValue.match(/fill:rgb\((.+?)\)/);
481 | let rgbArray;
482 | if (rgb) {
483 | rgbArray = rgb[1].split(",").map((x) => Number.parseInt(x, 10)).filter(finiteNumber);
484 | }
485 | if (!rgbArray) {
486 | rgb = styleValue.match(/fill:#([0-9a-fA-F]{6})/);
487 | if (rgb) {
488 | rgbArray = [
489 | Number.parseInt(rgb[1].substr(0, 2), 16),
490 | Number.parseInt(rgb[1].substr(2, 2), 16),
491 | Number.parseInt(rgb[1].substr(4, 2), 16)
492 | ];
493 | }
494 | }
495 | if (!rgbArray) {
496 | rgb = styleValue.match(/fill:#([0-9a-fA-F]{3})/);
497 | if (rgb) {
498 | let rs = rgb[1].substr(0, 1);
499 | let gs = rgb[1].substr(1, 1);
500 | let bs = rgb[1].substr(2, 1);
501 | rgbArray = [
502 | Number.parseInt(rs + rs, 16),
503 | Number.parseInt(gs + gs, 16),
504 | Number.parseInt(bs + bs, 16)
505 | ];
506 | }
507 | }
508 | if (rgbArray) {
509 | if (rgbArray.length !== 3) {
510 | throw new Error("Cannot parse this color yet " + styleValue);
511 | }
512 | return hexColor(rgbArray);
513 | }
514 | console.error("Cannot parse this color yet " + styleValue);
515 | throw new Error("Cannot parse this color yet " + styleValue);
516 | }
517 | function hexColor(arr) {
518 | return arr;
519 | }
520 | function alphaHexColor(arr) {
521 | return arr;
522 | }
523 | function finiteNumber(x) {
524 | return Number.isFinite(x);
525 | }
526 | }
527 | });
528 |
529 | // index.js
530 | var require_streaming_svg_parser = __commonJS({
531 | "index.js"(exports, module) {
532 | var createStreamingSVGParser = require_createStreamingSVGParser();
533 | var getPointsFromPathData = require_getPointsFromPathData();
534 | var NumberParser = require_NumberParser();
535 | var getElementFillColor = require_getElementFillColor();
536 | module.exports = {
537 | createStreamingSVGParser,
538 | getPointsFromPathData,
539 | NumberParser,
540 | getElementFillColor
541 | };
542 | }
543 | });
544 | return require_streaming_svg_parser();
545 | })();
546 | //# sourceMappingURL=streaming-svg-parser.js.map
547 |
--------------------------------------------------------------------------------
/dist/streaming-svg-parser.js.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "sources": ["../lib/createStreamingSVGParser.js", "../lib/NumberParser.js", "../lib/getPointsFromPathData.js", "../lib/getElementFillColor.js", "../index.js"],
4 | "sourcesContent": ["\n// Possible states of SVG parsing\nconst WAIT_TAG_OPEN = 1;\nconst READ_TAG_OR_COMMENT = 2;\nconst READ_TAG = 3;\nconst READ_TAG_CLOSE = 4;\nconst READ_COMMENT = 5;\nconst WAIT_TAG_CLOSE = 6;\nconst WAIT_ATTRIBUTE_OR_CLOSE_TAG = 7;\nconst READ_ATTRIBUTE_NAME = 8;\nconst READ_ATTRIBUTE_VALUE = 9;\nconst WAIT_ATTRIBUTE_VALUE = 10;\nconst WAIT_ATTRIBUTE_ASSIGNMENT_OR_NEXT_ATTRIBUTE = 11;\n\nconst A = 'A'.charCodeAt(0);\nconst Z = 'z'.charCodeAt(0);\nconst whitespace = /\\s/;\n\n/**\n * Creates a new instance of the parser. Parser will consume chunk of text and will\n * notify the caller when new tag is opened or closed.\n * \n * If `generateAsync` is true - the parser will break its own execution,\n * allowing UI thread to catch up. (Only for browser environment now)\n * \n * @returns Function(chunk: String) - function that processes chunk of text\n * \n * WARNING: This may not work correctly with multi-byte unicode characters\n */\nmodule.exports = function createStreamingSVGParser(notifyTagOpen, notifyTagClose, generateAsync) {\n let currentState = WAIT_TAG_OPEN;\n let closeAttributeSymbol;\n let currentTagName;\n let currentAttributeName;\n let lastElement;\n let buffer;\n let innerText;\n if (notifyTagClose === undefined) {\n notifyTagClose = Function.prototype; // noop\n }\n\n return generateAsync ? processChunkAsync : processChunkSync;\n\n function processChunkAsync(chunk) {\n return new Promise(resolve => iterateSymbolsAsync(chunk, 0, resolve));\n }\n\n function iterateSymbolsAsync(chunk, idx, resolve) {\n let start = performance.now(); \n let processed = 0;\n\n while (idx < chunk.length) {\n // Assuming each element is a symbol (i.e. this wouldn't work for unicode well).\n processSymbol(chunk[idx]);\n\n idx += 1;\n processed += 1;\n if (processed > 30000) {\n let elapsed = performance.now() - start;\n if (elapsed > 32) {\n setTimeout(() => iterateSymbolsAsync(chunk, idx, resolve), 0);\n return;\n } \n }\n }\n resolve();\n }\n\n function processChunkSync(chunk) {\n return iterateSymbols(chunk, 0);\n }\n\n function iterateSymbols(chunk, idx) {\n let processed = 0;\n\n while (idx < chunk.length) {\n // Assuming each element is a symbol (i.e. this wouldn't work for unicode well).\n processSymbol(chunk[idx]);\n idx += 1;\n }\n }\n\n function processSymbol(ch) {\n switch (currentState) {\n case WAIT_TAG_OPEN: \n if (ch === '<') currentState = READ_TAG_OR_COMMENT;\n else if (innerText) {\n innerText.push(ch);\n }\n break;\n case WAIT_TAG_CLOSE: \n if (ch === '>') currentState = WAIT_TAG_OPEN;\n break;\n case READ_TAG_OR_COMMENT: \n if (ch === '!' || ch === '?') {\n buffer = [ch];\n currentState = READ_COMMENT;\n } else if (ch === '/') {\n if (innerText) {\n lastElement.innerText = innerText.join('');\n innerText = null;\n }\n notifyTagClose(lastElement);\n if (lastElement) lastElement = lastElement.parent;\n currentState = WAIT_TAG_CLOSE;\n innerText = null;\n } else {\n currentState = READ_TAG;\n buffer = [ch];\n }\n break;\n case READ_COMMENT: {\n buffer.push(ch);\n let l = buffer.length;\n if (buffer.length > 3 && \n buffer[l - 1] === '>' &&\n buffer[l - 2] === '-' &&\n buffer[l - 3] === '-') {\n currentState = WAIT_TAG_OPEN;\n innerText = null;\n } else if ((buffer[0] === '!' && (buffer.length > 1 && buffer[1] !== '-')) || // \n // Skip this one, as next `READ_TAG` will close it.\n } else {\n currentTagName = buffer.join('');\n currentState = WAIT_ATTRIBUTE_OR_CLOSE_TAG;\n let parent = lastElement;\n lastElement = {\n tagName: currentTagName,\n attributes: new Map(),\n parent,\n children: []\n }\n if (parent) parent.children.push(lastElement);\n if (ch === '>') finishTag();\n }\n break;\n }\n case READ_TAG_CLOSE: {\n if (isTagNameCharacter(ch)) {\n buffer.push(ch);\n } else if (ch === '>') {\n let closedTag = buffer.join('')\n if (closedTag !== currentTagName) {\n throw new Error('Expected ' + currentTagName + ' to be closed, but got ' + closedTag)\n }\n }\n\n break;\n }\n case WAIT_ATTRIBUTE_OR_CLOSE_TAG: {\n if (ch === '>') {\n finishTag();\n } else if (isTagNameCharacter(ch)) {\n buffer = [ch];\n currentState = READ_ATTRIBUTE_NAME;\n } else {\n buffer.push(ch);\n }\n break;\n }\n case READ_ATTRIBUTE_NAME: {\n if (!isTagNameCharacter(ch)) {\n currentAttributeName = buffer.join('');\n if (ch === '=') currentState = WAIT_ATTRIBUTE_VALUE;\n else if (ch === '>') {\n lastElement.attributes.set(currentAttributeName, true);\n finishTag();\n } else currentState = WAIT_ATTRIBUTE_ASSIGNMENT_OR_NEXT_ATTRIBUTE;\n } else {\n buffer.push(ch);\n }\n break;\n }\n case WAIT_ATTRIBUTE_ASSIGNMENT_OR_NEXT_ATTRIBUTE: {\n if (ch === '=') {\n currentState = WAIT_ATTRIBUTE_VALUE;\n } else if (isTagNameCharacter(ch)) {\n currentState = READ_ATTRIBUTE_NAME;\n // Case of a boolean attribute \n lastElement.attributes.set(buffer.join(''), true);\n buffer = [ch];\n } else if (ch === '>') {\n lastElement.attributes.set(buffer.join(''), true);\n finishTag();\n }\n break;\n }\n case WAIT_ATTRIBUTE_VALUE: {\n if (ch === \"\\\"\" || ch === \"'\" || !isWhiteSpace(ch)) {\n buffer = [];\n currentState = READ_ATTRIBUTE_VALUE;\n // not 100% accurate!\n closeAttributeSymbol = ch;\n }\n // TODO: Might want to tighten validation here;\n break;\n }\n case READ_ATTRIBUTE_VALUE: {\n if (ch === closeAttributeSymbol) {\n currentState = WAIT_ATTRIBUTE_OR_CLOSE_TAG;\n lastElement.attributes.set(currentAttributeName, buffer.join(''));\n currentAttributeName = null;\n buffer = [];\n } else {\n buffer.push(ch);\n }\n break;\n }\n }\n }\n\n\n function finishTag() {\n let l = buffer.length;\n notifyTagOpen(lastElement); // we finished reading the attribute definition\n\n if (l > 0 && buffer[l - 1] === '/') {\n // a case of quick close \n notifyTagClose(lastElement);\n // since we closed this tag, let's pop it, and wait for the sibling.\n if (lastElement) lastElement = lastElement.parent;\n }\n currentState = WAIT_TAG_OPEN;\n innerText = [];\n currentAttributeName = null;\n }\n}\n\nfunction isTagNameCharacter(ch) {\n let code = ch.charCodeAt(0);\n return (A <= code && code <= Z) || (ch === '_') || (ch === '-') || (ch === ':');\n}\n\nfunction isWhiteSpace(ch) {\n return whitespace.test(ch);\n}\n", "/**\n * Streaming parser of numbers.\n */\nconst CharacterLookup = {\n '0': 0,\n '1': 1,\n '2': 2,\n '3': 3,\n '4': 4,\n '5': 5,\n '6': 6,\n '7': 7,\n '8': 8,\n '9': 9\n}\n\n/**\n * Naive parser of integer numbers. Optimized for memory consumption and\n * CPU performance. Not very strong on validation side.\n */\nclass NumberParser {\n constructor() {\n this.value = 0;\n this.fractionValue = 0;\n this.divider = 1;\n this.exponent = 0;\n this.isNegative = false;\n this.hasValue = false;\n this.hasFraction = false;\n this.hasExponent = false\n }\n\n getValue() {\n let value = this.value;\n if (this.hasFraction) {\n value += this.fractionValue / this.divider;\n }\n if (this.hasExponent) {\n value *= Math.pow(10, this.exponent);\n }\n if (this.isNegative) {\n return -value;\n }\n return value;\n }\n\n reset() {\n this.value = 0;\n this.fractionValue = 0;\n this.divider = 1;\n this.exponent = 0;\n this.isNegative = false;\n this.hasValue = false;\n this.hasFraction = false;\n this.hasExponent = false\n }\n\n addCharacter(ch) {\n this.hasValue = true;\n if (ch === '-') {\n this.isNegative = true;\n return;\n }\n if (ch === '.') {\n if (this.hasFraction) throw new Error('Already has fractional part!');\n this.hasFraction = true;\n return;\n }\n if (ch === 'e') {\n if (this.hasExponent) throw new Error('Already has exponent');\n this.hasExponent = true;\n this.exponent = 0;\n return;\n }\n\n let numValue = CharacterLookup[ch];\n if (numValue === undefined) throw new Error('Not a digit: ' + ch)\n\n if (this.hasExponent) {\n this.exponent = this.exponent * 10 + numValue;\n } else if (this.hasFraction) {\n this.fractionValue = this.fractionValue * 10 + numValue;\n this.divider *= 10;\n } else {\n this.value = this.value * 10 + numValue;\n }\n }\n}\n\nmodule.exports = NumberParser;", "\n/**\n * Extremely fast SVG path data attribute parser. Currently\n * it doesn't support curves or arcs. Only M, L, H, V (and m, l, h, v) are\n * supported\n */\nconst NumberParser = require('./NumberParser');\n\nconst processCommand = {\n M(points, lastNumbers) {\n if (lastNumbers.length % 2 !== 0) {\n throw new Error('Expected an even number of numbers for M command');\n }\n if (points.length === 0) {\n // consider this to be absolute points\n for (let i = 0; i < lastNumbers.length; i += 2) {\n points.push([lastNumbers[i], lastNumbers[i + 1]]);\n }\n } else {\n // Note: this is not true for generic case, and could/should be extended to start a new path.\n // We are just optimizing for own sake of a single path\n throw new Error('Only one \"Move\" command per path is expected');\n }\n },\n m(points, lastNumbers) {\n // https://www.w3.org/TR/SVG11/paths.html#PathDataMovetoCommands\n let lx = 0, ly = 0;\n if (points.length > 0 && lastNumbers.length > 1) {\n let last = points[points.length - 1];\n lx = last[0] + lastNumbers[0];\n ly = last[1] + lastNumbers[1]; ;\n }\n // TODO: Likely need to break points here into two arrays.\n for (let i = 2; i < lastNumbers.length; i += 2) {\n let x = lx + lastNumbers[i];\n let y = ly + lastNumbers[i + 1];\n points.push([x, y]);\n lx = x; ly = y;\n }\n },\n // line to:\n L(points, lastNumbers) {\n // TODO: validate lastNumbers.length % 2 === 0\n for (let i = 0; i < lastNumbers.length; i += 2) {\n points.push([lastNumbers[i], lastNumbers[i + 1]]);\n }\n },\n // relative line to:\n l(points, lastNumbers) {\n let lx = 0, ly = 0;\n if (points.length > 0) {\n let last = points[points.length - 1];\n lx = last[0];\n ly = last[1];\n }\n for (let i = 0; i < lastNumbers.length; i += 2) {\n let x = lx + lastNumbers[i];\n let y = ly + lastNumbers[i + 1];\n points.push([x, y]);\n lx = x; ly = y;\n }\n },\n H(points, lastNumbers) {\n let y = 0;\n if (points.length > 0) {\n y = points[points.length - 1][1];\n }\n for (let i = 0; i < lastNumbers.length; i += 1) {\n let x = lastNumbers[i];\n points.push([x, y]);\n }\n },\n h(points, lastNumbers) {\n let y = 0, lx = 0;\n if (points.length > 0) {\n lx = points[points.length - 1][0];\n y = points[points.length - 1][1];\n }\n for (let i = 0; i < lastNumbers.length; i += 1) {\n let x = lx + lastNumbers[i];\n points.push([x, y]);\n lx = x;\n }\n },\n V(points, lastNumbers) {\n let x = 0;\n if (points.length > 0) {\n x = points[points.length - 1][0];\n }\n for (let i = 0; i < lastNumbers.length; i += 1) {\n points.push([x, lastNumbers[i]]);\n }\n },\n v(points, lastNumbers) {\n let ly = 0, x = 0;\n if (points.length > 0) {\n x = points[points.length - 1][0];\n ly = points[points.length - 1][1];\n }\n for (let i = 0; i < lastNumbers.length; i += 1) {\n let y = ly + lastNumbers[i];\n points.push([x, y]);\n ly = y;\n }\n }\n}\n\nfunction getPointsFromPathData(d) {\n let numParser = new NumberParser();\n let idx = 0;\n let l = d.length;\n let ch;\n let lastNumbers, lastCommand;\n let points = [];\n while (idx < l) {\n ch = d[idx];\n if (ch in processCommand) {\n if (numParser.hasValue) {\n lastNumbers.push(numParser.getValue())\n }\n numParser.reset();\n if (lastNumbers) {\n lastCommand(points, lastNumbers);\n }\n lastCommand = processCommand[ch];\n lastNumbers = [];\n } else if (ch === ' ' || ch === ',') {\n if (numParser.hasValue) {\n lastNumbers.push(numParser.getValue())\n numParser.reset();\n }\n // ignore.\n } else if (ch === 'Z' || ch === 'z') {\n // TODO: Likely need to close the path..\n // ignore\n } else if (numParser.hasValue && ch === '-') {\n // this considered to be a start of the next number.\n lastNumbers.push(numParser.getValue())\n numParser.reset();\n numParser.addCharacter(ch);\n } else {\n numParser.addCharacter(ch);\n }\n idx += 1;\n }\n if (numParser.hasValue) {\n lastNumbers.push(numParser.getValue());\n }\n if (lastNumbers) {\n lastCommand(points, lastNumbers);\n }\n return points;\n}\n\nmodule.exports = getPointsFromPathData;", "module.exports = function getElementFillColor(el) {\n return getColor(el.attributes.get('fill') || el.attributes.get('style'));\n}\n\nfunction getColor(styleValue) {\n // TODO: could probably be done faster.\n if (styleValue[0] === '#') {\n if (styleValue.length === 1 + 6) {\n // #rrggbb\n let r = Number.parseInt(styleValue.substr(1, 2), 16);\n let g = Number.parseInt(styleValue.substr(3, 2), 16);\n let b = Number.parseInt(styleValue.substr(5, 2), 16);\n return hexColor([r, g, b]);\n }\n if (styleValue.length === 1 + 3 || styleValue.length === 1 + 4) {\n // #rgba\n let rs = styleValue.substr(1, 1);\n let gs = styleValue.substr(2, 1);\n let bs = styleValue.substr(3, 1);\n // ignore a\n let r = Number.parseInt(rs + rs, 16);\n let g = Number.parseInt(gs + gs, 16);\n let b = Number.parseInt(bs + bs, 16);\n return hexColor([r, g, b]);\n }\n throw new Error('Cannot parse this color yet ' + styleValue);\n } else if (styleValue.startsWith('rgba')) {\n // rgba(rr,gg,bb,a)\n let colors = styleValue.substr(5).split(/,/).map(x => Number.parseFloat(x))\n colors[3] = Math.round(colors[3] * 255);\n return alphaHexColor(colors);\n }\n let rgb = styleValue.match(/fill:rgb\\((.+?)\\)/);\n let rgbArray;\n if (rgb) {\n rgbArray = rgb[1]\n .split(',')\n .map((x) => Number.parseInt(x, 10))\n .filter(finiteNumber);\n }\n if (!rgbArray) {\n rgb = styleValue.match(/fill:#([0-9a-fA-F]{6})/)\n if (rgb) {\n rgbArray = [\n Number.parseInt(rgb[1].substr(0, 2), 16),\n Number.parseInt(rgb[1].substr(2, 2), 16),\n Number.parseInt(rgb[1].substr(4, 2), 16)\n ]\n }\n }\n if (!rgbArray) {\n rgb = styleValue.match(/fill:#([0-9a-fA-F]{3})/)\n if (rgb) {\n let rs = rgb[1].substr(0, 1);\n let gs = rgb[1].substr(1, 1);\n let bs = rgb[1].substr(2, 1);\n rgbArray = [\n Number.parseInt(rs + rs, 16),\n Number.parseInt(gs + gs, 16),\n Number.parseInt(bs + bs, 16)\n ]\n }\n\n }\n if (rgbArray) {\n if (rgbArray.length !== 3){\n throw new Error('Cannot parse this color yet ' + styleValue);\n }\n return hexColor(rgbArray);\n }\n console.error('Cannot parse this color yet ' + styleValue)\n throw new Error('Cannot parse this color yet ' + styleValue);\n}\n\nfunction hexColor(arr) {\n return arr;\n}\nfunction alphaHexColor(arr) {\n return arr;\n}\nfunction finiteNumber(x) {\n return Number.isFinite(x);\n}", "const createStreamingSVGParser = require('./lib/createStreamingSVGParser');\nconst getPointsFromPathData = require('./lib/getPointsFromPathData');\nconst NumberParser = require('./lib/NumberParser');\nconst getElementFillColor = require('./lib/getElementFillColor');\n\nmodule.exports = {\n createStreamingSVGParser,\n getPointsFromPathData,\n NumberParser,\n\n // Somewhat specific methods. Defining it temporarily here. May go away\n getElementFillColor\n}"],
5 | "mappings": "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",
6 | "names": []
7 | }
8 |
--------------------------------------------------------------------------------
/dist/streaming-svg-parser.min.js:
--------------------------------------------------------------------------------
1 | var streamingSVGParser=(()=>{var A=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var d=A((Q,m)=>{var D="A".charCodeAt(0),L="z".charCodeAt(0),v=/\s/;m.exports=function(e,n,i){let r=1,l,f,h,T,a,u;return n===void 0&&(n=Function.prototype),i?S:M;function S(s){return new Promise(o=>I(s,0,o))}function I(s,o,c){let B=performance.now(),R=0;for(;o3e4&&performance.now()-B>32){setTimeout(()=>I(s,o,c),0);return}c()}function M(s){return W(s,0)}function W(s,o){let c=0;for(;o"&&(r=1);break;case 2:s==="!"||s==="?"?(a=[s],r=5):s==="/"?(u&&(T.innerText=u.join(""),u=null),n(T),T&&(T=T.parent),r=6,u=null):(r=3,a=[s]);break;case 5:{a.push(s);let o=a.length;a.length>3&&a[o-1]===">"&&a[o-2]==="-"&&a[o-3]==="-"?(r=1,u=null):(a[0]==="!"&&a.length>1&&a[1]!=="-"||a[0]==="?")&&(r=6);break}case 3:{if(E(s))a.push(s);else if(s!=="/"){f=a.join(""),r=7;let o=T;T={tagName:f,attributes:new Map,parent:o,children:[]},o&&o.children.push(T),s===">"&&_()}break}case 4:{if(E(s))a.push(s);else if(s===">"){let o=a.join("");if(o!==f)throw new Error("Expected "+f+" to be closed, but got "+o)}break}case 7:{s===">"?_():E(s)?(a=[s],r=8):a.push(s);break}case 8:{E(s)?a.push(s):(h=a.join(""),s==="="?r=10:s===">"?(T.attributes.set(h,!0),_()):r=11);break}case 11:{s==="="?r=10:E(s)?(r=8,T.attributes.set(a.join(""),!0),a=[s]):s===">"&&(T.attributes.set(a.join(""),!0),_());break}case 10:{(s==='"'||s==="'"||!F(s))&&(a=[],r=9,l=s);break}case 9:{s===l?(r=7,T.attributes.set(h,a.join("")),h=null,a=[]):a.push(s);break}}}function _(){let s=a.length;e(T),s>0&&a[s-1]==="/"&&(n(T),T&&(T=T.parent)),r=1,u=[],h=null}};function E(t){let e=t.charCodeAt(0);return D<=e&&e<=L||t==="_"||t==="-"||t===":"}function F(t){return v.test(t)}});var p=A((Y,N)=>{var P={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9},g=class{constructor(){this.value=0,this.fractionValue=0,this.divider=1,this.exponent=0,this.isNegative=!1,this.hasValue=!1,this.hasFraction=!1,this.hasExponent=!1}getValue(){let e=this.value;return this.hasFraction&&(e+=this.fractionValue/this.divider),this.hasExponent&&(e*=Math.pow(10,this.exponent)),this.isNegative?-e:e}reset(){this.value=0,this.fractionValue=0,this.divider=1,this.exponent=0,this.isNegative=!1,this.hasValue=!1,this.hasFraction=!1,this.hasExponent=!1}addCharacter(e){if(this.hasValue=!0,e==="-"){this.isNegative=!0;return}if(e==="."){if(this.hasFraction)throw new Error("Already has fractional part!");this.hasFraction=!0;return}if(e==="e"){if(this.hasExponent)throw new Error("Already has exponent");this.hasExponent=!0,this.exponent=0;return}let n=P[e];if(n===void 0)throw new Error("Not a digit: "+e);this.hasExponent?this.exponent=this.exponent*10+n:this.hasFraction?(this.fractionValue=this.fractionValue*10+n,this.divider*=10):this.value=this.value*10+n}};N.exports=g});var x=A(($,O)=>{var k=p(),C={M(t,e){if(e.length%2!==0)throw new Error("Expected an even number of numbers for M command");if(t.length===0)for(let n=0;n0&&e.length>1){let r=t[t.length-1];n=r[0]+e[0],i=r[1]+e[1]}for(let r=2;r0){let r=t[t.length-1];n=r[0],i=r[1]}for(let r=0;r0&&(n=t[t.length-1][1]);for(let i=0;i0&&(i=t[t.length-1][0],n=t[t.length-1][1]);for(let r=0;r0&&(n=t[t.length-1][0]);for(let i=0;i0&&(i=t[t.length-1][0],n=t[t.length-1][1]);for(let r=0;r{w.exports=function(e){return j(e.attributes.get("fill")||e.attributes.get("style"))};function j(t){if(t[0]==="#"){if(t.length===1+6){let i=Number.parseInt(t.substr(1,2),16),r=Number.parseInt(t.substr(3,2),16),l=Number.parseInt(t.substr(5,2),16);return[i,r,l]}if(t.length===1+3||t.length===1+4){let i=t.substr(1,1),r=t.substr(2,1),l=t.substr(3,1),f=Number.parseInt(i+i,16),h=Number.parseInt(r+r,16),T=Number.parseInt(l+l,16);return[f,h,T]}throw new Error("Cannot parse this color yet "+t)}else if(t.startsWith("rgba")){let i=t.substr(5).split(/,/).map(r=>Number.parseFloat(r));return i[3]=Math.round(i[3]*255),i}let e=t.match(/fill:rgb\((.+?)\)/),n;if(e&&(n=e[1].split(",").map(i=>Number.parseInt(i,10)).filter(q)),n||(e=t.match(/fill:#([0-9a-fA-F]{6})/),e&&(n=[Number.parseInt(e[1].substr(0,2),16),Number.parseInt(e[1].substr(2,2),16),Number.parseInt(e[1].substr(4,2),16)])),!n&&(e=t.match(/fill:#([0-9a-fA-F]{3})/),e)){let i=e[1].substr(0,1),r=e[1].substr(1,1),l=e[1].substr(2,1);n=[Number.parseInt(i+i,16),Number.parseInt(r+r,16),Number.parseInt(l+l,16)]}if(n){if(n.length!==3)throw new Error("Cannot parse this color yet "+t);return n}throw console.error("Cannot parse this color yet "+t),new Error("Cannot parse this color yet "+t)}function q(t){return Number.isFinite(t)}});var Z=A((te,G)=>{var y=d(),X=x(),z=p(),H=U();G.exports={createStreamingSVGParser:y,getPointsFromPathData:X,NumberParser:z,getElementFillColor:H}});return Z();})();
2 | //# sourceMappingURL=streaming-svg-parser.min.js.map
3 |
--------------------------------------------------------------------------------
/dist/streaming-svg-parser.min.js.map:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "sources": ["../lib/createStreamingSVGParser.js", "../lib/NumberParser.js", "../lib/getPointsFromPathData.js", "../lib/getElementFillColor.js", "../index.js"],
4 | "sourcesContent": ["\n// Possible states of SVG parsing\nconst WAIT_TAG_OPEN = 1;\nconst READ_TAG_OR_COMMENT = 2;\nconst READ_TAG = 3;\nconst READ_TAG_CLOSE = 4;\nconst READ_COMMENT = 5;\nconst WAIT_TAG_CLOSE = 6;\nconst WAIT_ATTRIBUTE_OR_CLOSE_TAG = 7;\nconst READ_ATTRIBUTE_NAME = 8;\nconst READ_ATTRIBUTE_VALUE = 9;\nconst WAIT_ATTRIBUTE_VALUE = 10;\nconst WAIT_ATTRIBUTE_ASSIGNMENT_OR_NEXT_ATTRIBUTE = 11;\n\nconst A = 'A'.charCodeAt(0);\nconst Z = 'z'.charCodeAt(0);\nconst whitespace = /\\s/;\n\n/**\n * Creates a new instance of the parser. Parser will consume chunk of text and will\n * notify the caller when new tag is opened or closed.\n * \n * If `generateAsync` is true - the parser will break its own execution,\n * allowing UI thread to catch up. (Only for browser environment now)\n * \n * @returns Function(chunk: String) - function that processes chunk of text\n * \n * WARNING: This may not work correctly with multi-byte unicode characters\n */\nmodule.exports = function createStreamingSVGParser(notifyTagOpen, notifyTagClose, generateAsync) {\n let currentState = WAIT_TAG_OPEN;\n let closeAttributeSymbol;\n let currentTagName;\n let currentAttributeName;\n let lastElement;\n let buffer;\n let innerText;\n if (notifyTagClose === undefined) {\n notifyTagClose = Function.prototype; // noop\n }\n\n return generateAsync ? processChunkAsync : processChunkSync;\n\n function processChunkAsync(chunk) {\n return new Promise(resolve => iterateSymbolsAsync(chunk, 0, resolve));\n }\n\n function iterateSymbolsAsync(chunk, idx, resolve) {\n let start = performance.now(); \n let processed = 0;\n\n while (idx < chunk.length) {\n // Assuming each element is a symbol (i.e. this wouldn't work for unicode well).\n processSymbol(chunk[idx]);\n\n idx += 1;\n processed += 1;\n if (processed > 30000) {\n let elapsed = performance.now() - start;\n if (elapsed > 32) {\n setTimeout(() => iterateSymbolsAsync(chunk, idx, resolve), 0);\n return;\n } \n }\n }\n resolve();\n }\n\n function processChunkSync(chunk) {\n return iterateSymbols(chunk, 0);\n }\n\n function iterateSymbols(chunk, idx) {\n let processed = 0;\n\n while (idx < chunk.length) {\n // Assuming each element is a symbol (i.e. this wouldn't work for unicode well).\n processSymbol(chunk[idx]);\n idx += 1;\n }\n }\n\n function processSymbol(ch) {\n switch (currentState) {\n case WAIT_TAG_OPEN: \n if (ch === '<') currentState = READ_TAG_OR_COMMENT;\n else if (innerText) {\n innerText.push(ch);\n }\n break;\n case WAIT_TAG_CLOSE: \n if (ch === '>') currentState = WAIT_TAG_OPEN;\n break;\n case READ_TAG_OR_COMMENT: \n if (ch === '!' || ch === '?') {\n buffer = [ch];\n currentState = READ_COMMENT;\n } else if (ch === '/') {\n if (innerText) {\n lastElement.innerText = innerText.join('');\n innerText = null;\n }\n notifyTagClose(lastElement);\n if (lastElement) lastElement = lastElement.parent;\n currentState = WAIT_TAG_CLOSE;\n innerText = null;\n } else {\n currentState = READ_TAG;\n buffer = [ch];\n }\n break;\n case READ_COMMENT: {\n buffer.push(ch);\n let l = buffer.length;\n if (buffer.length > 3 && \n buffer[l - 1] === '>' &&\n buffer[l - 2] === '-' &&\n buffer[l - 3] === '-') {\n currentState = WAIT_TAG_OPEN;\n innerText = null;\n } else if ((buffer[0] === '!' && (buffer.length > 1 && buffer[1] !== '-')) || // \n // Skip this one, as next `READ_TAG` will close it.\n } else {\n currentTagName = buffer.join('');\n currentState = WAIT_ATTRIBUTE_OR_CLOSE_TAG;\n let parent = lastElement;\n lastElement = {\n tagName: currentTagName,\n attributes: new Map(),\n parent,\n children: []\n }\n if (parent) parent.children.push(lastElement);\n if (ch === '>') finishTag();\n }\n break;\n }\n case READ_TAG_CLOSE: {\n if (isTagNameCharacter(ch)) {\n buffer.push(ch);\n } else if (ch === '>') {\n let closedTag = buffer.join('')\n if (closedTag !== currentTagName) {\n throw new Error('Expected ' + currentTagName + ' to be closed, but got ' + closedTag)\n }\n }\n\n break;\n }\n case WAIT_ATTRIBUTE_OR_CLOSE_TAG: {\n if (ch === '>') {\n finishTag();\n } else if (isTagNameCharacter(ch)) {\n buffer = [ch];\n currentState = READ_ATTRIBUTE_NAME;\n } else {\n buffer.push(ch);\n }\n break;\n }\n case READ_ATTRIBUTE_NAME: {\n if (!isTagNameCharacter(ch)) {\n currentAttributeName = buffer.join('');\n if (ch === '=') currentState = WAIT_ATTRIBUTE_VALUE;\n else if (ch === '>') {\n lastElement.attributes.set(currentAttributeName, true);\n finishTag();\n } else currentState = WAIT_ATTRIBUTE_ASSIGNMENT_OR_NEXT_ATTRIBUTE;\n } else {\n buffer.push(ch);\n }\n break;\n }\n case WAIT_ATTRIBUTE_ASSIGNMENT_OR_NEXT_ATTRIBUTE: {\n if (ch === '=') {\n currentState = WAIT_ATTRIBUTE_VALUE;\n } else if (isTagNameCharacter(ch)) {\n currentState = READ_ATTRIBUTE_NAME;\n // Case of a boolean attribute \n lastElement.attributes.set(buffer.join(''), true);\n buffer = [ch];\n } else if (ch === '>') {\n lastElement.attributes.set(buffer.join(''), true);\n finishTag();\n }\n break;\n }\n case WAIT_ATTRIBUTE_VALUE: {\n if (ch === \"\\\"\" || ch === \"'\" || !isWhiteSpace(ch)) {\n buffer = [];\n currentState = READ_ATTRIBUTE_VALUE;\n // not 100% accurate!\n closeAttributeSymbol = ch;\n }\n // TODO: Might want to tighten validation here;\n break;\n }\n case READ_ATTRIBUTE_VALUE: {\n if (ch === closeAttributeSymbol) {\n currentState = WAIT_ATTRIBUTE_OR_CLOSE_TAG;\n lastElement.attributes.set(currentAttributeName, buffer.join(''));\n currentAttributeName = null;\n buffer = [];\n } else {\n buffer.push(ch);\n }\n break;\n }\n }\n }\n\n\n function finishTag() {\n let l = buffer.length;\n notifyTagOpen(lastElement); // we finished reading the attribute definition\n\n if (l > 0 && buffer[l - 1] === '/') {\n // a case of quick close \n notifyTagClose(lastElement);\n // since we closed this tag, let's pop it, and wait for the sibling.\n if (lastElement) lastElement = lastElement.parent;\n }\n currentState = WAIT_TAG_OPEN;\n innerText = [];\n currentAttributeName = null;\n }\n}\n\nfunction isTagNameCharacter(ch) {\n let code = ch.charCodeAt(0);\n return (A <= code && code <= Z) || (ch === '_') || (ch === '-') || (ch === ':');\n}\n\nfunction isWhiteSpace(ch) {\n return whitespace.test(ch);\n}\n", "/**\n * Streaming parser of numbers.\n */\nconst CharacterLookup = {\n '0': 0,\n '1': 1,\n '2': 2,\n '3': 3,\n '4': 4,\n '5': 5,\n '6': 6,\n '7': 7,\n '8': 8,\n '9': 9\n}\n\n/**\n * Naive parser of integer numbers. Optimized for memory consumption and\n * CPU performance. Not very strong on validation side.\n */\nclass NumberParser {\n constructor() {\n this.value = 0;\n this.fractionValue = 0;\n this.divider = 1;\n this.exponent = 0;\n this.isNegative = false;\n this.hasValue = false;\n this.hasFraction = false;\n this.hasExponent = false\n }\n\n getValue() {\n let value = this.value;\n if (this.hasFraction) {\n value += this.fractionValue / this.divider;\n }\n if (this.hasExponent) {\n value *= Math.pow(10, this.exponent);\n }\n if (this.isNegative) {\n return -value;\n }\n return value;\n }\n\n reset() {\n this.value = 0;\n this.fractionValue = 0;\n this.divider = 1;\n this.exponent = 0;\n this.isNegative = false;\n this.hasValue = false;\n this.hasFraction = false;\n this.hasExponent = false\n }\n\n addCharacter(ch) {\n this.hasValue = true;\n if (ch === '-') {\n this.isNegative = true;\n return;\n }\n if (ch === '.') {\n if (this.hasFraction) throw new Error('Already has fractional part!');\n this.hasFraction = true;\n return;\n }\n if (ch === 'e') {\n if (this.hasExponent) throw new Error('Already has exponent');\n this.hasExponent = true;\n this.exponent = 0;\n return;\n }\n\n let numValue = CharacterLookup[ch];\n if (numValue === undefined) throw new Error('Not a digit: ' + ch)\n\n if (this.hasExponent) {\n this.exponent = this.exponent * 10 + numValue;\n } else if (this.hasFraction) {\n this.fractionValue = this.fractionValue * 10 + numValue;\n this.divider *= 10;\n } else {\n this.value = this.value * 10 + numValue;\n }\n }\n}\n\nmodule.exports = NumberParser;", "\n/**\n * Extremely fast SVG path data attribute parser. Currently\n * it doesn't support curves or arcs. Only M, L, H, V (and m, l, h, v) are\n * supported\n */\nconst NumberParser = require('./NumberParser');\n\nconst processCommand = {\n M(points, lastNumbers) {\n if (lastNumbers.length % 2 !== 0) {\n throw new Error('Expected an even number of numbers for M command');\n }\n if (points.length === 0) {\n // consider this to be absolute points\n for (let i = 0; i < lastNumbers.length; i += 2) {\n points.push([lastNumbers[i], lastNumbers[i + 1]]);\n }\n } else {\n // Note: this is not true for generic case, and could/should be extended to start a new path.\n // We are just optimizing for own sake of a single path\n throw new Error('Only one \"Move\" command per path is expected');\n }\n },\n m(points, lastNumbers) {\n // https://www.w3.org/TR/SVG11/paths.html#PathDataMovetoCommands\n let lx = 0, ly = 0;\n if (points.length > 0 && lastNumbers.length > 1) {\n let last = points[points.length - 1];\n lx = last[0] + lastNumbers[0];\n ly = last[1] + lastNumbers[1]; ;\n }\n // TODO: Likely need to break points here into two arrays.\n for (let i = 2; i < lastNumbers.length; i += 2) {\n let x = lx + lastNumbers[i];\n let y = ly + lastNumbers[i + 1];\n points.push([x, y]);\n lx = x; ly = y;\n }\n },\n // line to:\n L(points, lastNumbers) {\n // TODO: validate lastNumbers.length % 2 === 0\n for (let i = 0; i < lastNumbers.length; i += 2) {\n points.push([lastNumbers[i], lastNumbers[i + 1]]);\n }\n },\n // relative line to:\n l(points, lastNumbers) {\n let lx = 0, ly = 0;\n if (points.length > 0) {\n let last = points[points.length - 1];\n lx = last[0];\n ly = last[1];\n }\n for (let i = 0; i < lastNumbers.length; i += 2) {\n let x = lx + lastNumbers[i];\n let y = ly + lastNumbers[i + 1];\n points.push([x, y]);\n lx = x; ly = y;\n }\n },\n H(points, lastNumbers) {\n let y = 0;\n if (points.length > 0) {\n y = points[points.length - 1][1];\n }\n for (let i = 0; i < lastNumbers.length; i += 1) {\n let x = lastNumbers[i];\n points.push([x, y]);\n }\n },\n h(points, lastNumbers) {\n let y = 0, lx = 0;\n if (points.length > 0) {\n lx = points[points.length - 1][0];\n y = points[points.length - 1][1];\n }\n for (let i = 0; i < lastNumbers.length; i += 1) {\n let x = lx + lastNumbers[i];\n points.push([x, y]);\n lx = x;\n }\n },\n V(points, lastNumbers) {\n let x = 0;\n if (points.length > 0) {\n x = points[points.length - 1][0];\n }\n for (let i = 0; i < lastNumbers.length; i += 1) {\n points.push([x, lastNumbers[i]]);\n }\n },\n v(points, lastNumbers) {\n let ly = 0, x = 0;\n if (points.length > 0) {\n x = points[points.length - 1][0];\n ly = points[points.length - 1][1];\n }\n for (let i = 0; i < lastNumbers.length; i += 1) {\n let y = ly + lastNumbers[i];\n points.push([x, y]);\n ly = y;\n }\n }\n}\n\nfunction getPointsFromPathData(d) {\n let numParser = new NumberParser();\n let idx = 0;\n let l = d.length;\n let ch;\n let lastNumbers, lastCommand;\n let points = [];\n while (idx < l) {\n ch = d[idx];\n if (ch in processCommand) {\n if (numParser.hasValue) {\n lastNumbers.push(numParser.getValue())\n }\n numParser.reset();\n if (lastNumbers) {\n lastCommand(points, lastNumbers);\n }\n lastCommand = processCommand[ch];\n lastNumbers = [];\n } else if (ch === ' ' || ch === ',') {\n if (numParser.hasValue) {\n lastNumbers.push(numParser.getValue())\n numParser.reset();\n }\n // ignore.\n } else if (ch === 'Z' || ch === 'z') {\n // TODO: Likely need to close the path..\n // ignore\n } else if (numParser.hasValue && ch === '-') {\n // this considered to be a start of the next number.\n lastNumbers.push(numParser.getValue())\n numParser.reset();\n numParser.addCharacter(ch);\n } else {\n numParser.addCharacter(ch);\n }\n idx += 1;\n }\n if (numParser.hasValue) {\n lastNumbers.push(numParser.getValue());\n }\n if (lastNumbers) {\n lastCommand(points, lastNumbers);\n }\n return points;\n}\n\nmodule.exports = getPointsFromPathData;", "module.exports = function getElementFillColor(el) {\n return getColor(el.attributes.get('fill') || el.attributes.get('style'));\n}\n\nfunction getColor(styleValue) {\n // TODO: could probably be done faster.\n if (styleValue[0] === '#') {\n if (styleValue.length === 1 + 6) {\n // #rrggbb\n let r = Number.parseInt(styleValue.substr(1, 2), 16);\n let g = Number.parseInt(styleValue.substr(3, 2), 16);\n let b = Number.parseInt(styleValue.substr(5, 2), 16);\n return hexColor([r, g, b]);\n }\n if (styleValue.length === 1 + 3 || styleValue.length === 1 + 4) {\n // #rgba\n let rs = styleValue.substr(1, 1);\n let gs = styleValue.substr(2, 1);\n let bs = styleValue.substr(3, 1);\n // ignore a\n let r = Number.parseInt(rs + rs, 16);\n let g = Number.parseInt(gs + gs, 16);\n let b = Number.parseInt(bs + bs, 16);\n return hexColor([r, g, b]);\n }\n throw new Error('Cannot parse this color yet ' + styleValue);\n } else if (styleValue.startsWith('rgba')) {\n // rgba(rr,gg,bb,a)\n let colors = styleValue.substr(5).split(/,/).map(x => Number.parseFloat(x))\n colors[3] = Math.round(colors[3] * 255);\n return alphaHexColor(colors);\n }\n let rgb = styleValue.match(/fill:rgb\\((.+?)\\)/);\n let rgbArray;\n if (rgb) {\n rgbArray = rgb[1]\n .split(',')\n .map((x) => Number.parseInt(x, 10))\n .filter(finiteNumber);\n }\n if (!rgbArray) {\n rgb = styleValue.match(/fill:#([0-9a-fA-F]{6})/)\n if (rgb) {\n rgbArray = [\n Number.parseInt(rgb[1].substr(0, 2), 16),\n Number.parseInt(rgb[1].substr(2, 2), 16),\n Number.parseInt(rgb[1].substr(4, 2), 16)\n ]\n }\n }\n if (!rgbArray) {\n rgb = styleValue.match(/fill:#([0-9a-fA-F]{3})/)\n if (rgb) {\n let rs = rgb[1].substr(0, 1);\n let gs = rgb[1].substr(1, 1);\n let bs = rgb[1].substr(2, 1);\n rgbArray = [\n Number.parseInt(rs + rs, 16),\n Number.parseInt(gs + gs, 16),\n Number.parseInt(bs + bs, 16)\n ]\n }\n\n }\n if (rgbArray) {\n if (rgbArray.length !== 3){\n throw new Error('Cannot parse this color yet ' + styleValue);\n }\n return hexColor(rgbArray);\n }\n console.error('Cannot parse this color yet ' + styleValue)\n throw new Error('Cannot parse this color yet ' + styleValue);\n}\n\nfunction hexColor(arr) {\n return arr;\n}\nfunction alphaHexColor(arr) {\n return arr;\n}\nfunction finiteNumber(x) {\n return Number.isFinite(x);\n}", "const createStreamingSVGParser = require('./lib/createStreamingSVGParser');\nconst getPointsFromPathData = require('./lib/getPointsFromPathData');\nconst NumberParser = require('./lib/NumberParser');\nconst getElementFillColor = require('./lib/getElementFillColor');\n\nmodule.exports = {\n createStreamingSVGParser,\n getPointsFromPathData,\n NumberParser,\n\n // Somewhat specific methods. Defining it temporarily here. May go away\n getElementFillColor\n}"],
5 | "mappings": "2FAAA,gBAcA,GAAM,GAAI,IAAI,WAAW,CAAC,EACpB,EAAI,IAAI,WAAW,CAAC,EACpB,EAAa,KAanB,EAAO,QAAU,SAAkC,EAAe,EAAgB,EAAe,CAC/F,GAAI,GAAe,EACf,EACA,EACA,EACA,EACA,EACA,EACJ,MAAI,KAAmB,QACrB,GAAiB,SAAS,WAGrB,EAAgB,EAAoB,EAE3C,WAA2B,EAAO,CAChC,MAAO,IAAI,SAAQ,GAAW,EAAoB,EAAO,EAAG,CAAO,CAAC,CACtE,CAEA,WAA6B,EAAO,EAAK,EAAS,CAChD,GAAI,GAAQ,YAAY,IAAI,EACxB,EAAY,EAEhB,KAAO,EAAM,EAAM,QAMjB,GAJA,EAAc,EAAM,EAAI,EAExB,GAAO,EACP,GAAa,EACT,EAAY,KAEV,AADU,YAAY,IAAI,EAAI,EACpB,GAAI,CAChB,WAAW,IAAM,EAAoB,EAAO,EAAK,CAAO,EAAG,CAAC,EAC5D,MACF,CAGJ,EAAQ,CACV,CAEA,WAA0B,EAAO,CAC/B,MAAO,GAAe,EAAO,CAAC,CAChC,CAEA,WAAwB,EAAO,EAAK,CAClC,GAAI,GAAY,EAEhB,KAAO,EAAM,EAAM,QAEjB,EAAc,EAAM,EAAI,EACxB,GAAO,CAEX,CAEA,WAAuB,EAAI,CACzB,OAAQ,OACD,GACH,AAAI,IAAO,IAAK,EAAe,EACtB,GACP,EAAU,KAAK,CAAE,EAEnB,UACG,GACH,AAAI,IAAO,KAAK,GAAe,GAC/B,UACG,GACH,AAAI,IAAO,KAAO,IAAO,IACvB,GAAS,CAAC,CAAE,EACZ,EAAe,GACV,AAAI,IAAO,IACZ,IACF,GAAY,UAAY,EAAU,KAAK,EAAE,EACzC,EAAY,MAEd,EAAe,CAAW,EACtB,GAAa,GAAc,EAAY,QAC3C,EAAe,EACf,EAAY,MAEZ,GAAe,EACf,EAAS,CAAC,CAAE,GAEd,UACG,GAAc,CACjB,EAAO,KAAK,CAAE,EACd,GAAI,GAAI,EAAO,OACf,AAAI,EAAO,OAAS,GAClB,EAAO,EAAI,KAAO,KAClB,EAAO,EAAI,KAAO,KAClB,EAAO,EAAI,KAAO,IAChB,GAAe,EACf,EAAY,MACJ,GAAO,KAAO,KAAQ,EAAO,OAAS,GAAK,EAAO,KAAO,KAClE,EAAO,KAAO,MAAK,GAAe,GACrC,KACF,KACK,GAAU,CACb,GAAI,EAAmB,CAAE,EACvB,EAAO,KAAK,CAAE,UACL,IAAO,IAGX,CACL,EAAiB,EAAO,KAAK,EAAE,EAC/B,EAAe,EACf,GAAI,GAAS,EACb,EAAc,CACZ,QAAS,EACT,WAAY,GAAI,KAChB,SACA,SAAU,CAAC,CACb,EACI,GAAQ,EAAO,SAAS,KAAK,CAAW,EACxC,IAAO,KAAK,EAAU,CAC5B,CACA,KACF,KACK,GAAgB,CACnB,GAAI,EAAmB,CAAE,EACvB,EAAO,KAAK,CAAE,UACL,IAAO,IAAK,CACrB,GAAI,GAAY,EAAO,KAAK,EAAE,EAC9B,GAAI,IAAc,EAChB,KAAM,IAAI,OAAM,YAAc,EAAiB,0BAA4B,CAAS,CAExF,CAEA,KACF,KACK,GAA6B,CAChC,AAAI,IAAO,IACT,EAAU,EACL,AAAI,EAAmB,CAAE,EAC9B,GAAS,CAAC,CAAE,EACZ,EAAe,GAEf,EAAO,KAAK,CAAE,EAEhB,KACF,KACK,GAAqB,CACxB,AAAK,EAAmB,CAAE,EAQxB,EAAO,KAAK,CAAE,EAPd,GAAuB,EAAO,KAAK,EAAE,EACrC,AAAI,IAAO,IAAK,EAAe,GAC1B,AAAI,IAAO,IACd,GAAY,WAAW,IAAI,EAAsB,EAAI,EACrD,EAAU,GACL,EAAe,IAIxB,KACF,KACK,IAA6C,CAChD,AAAI,IAAO,IACT,EAAe,GACV,AAAI,EAAmB,CAAE,EAC9B,GAAe,EAEf,EAAY,WAAW,IAAI,EAAO,KAAK,EAAE,EAAG,EAAI,EAChD,EAAS,CAAC,CAAE,GACH,IAAO,KAChB,GAAY,WAAW,IAAI,EAAO,KAAK,EAAE,EAAG,EAAI,EAChD,EAAU,GAEZ,KACF,KACK,IAAsB,CACzB,AAAI,KAAO,KAAQ,IAAO,KAAO,CAAC,EAAa,CAAE,IAC/C,GAAS,CAAC,EACV,EAAe,EAEf,EAAuB,GAGzB,KACF,KACK,GAAsB,CACzB,AAAI,IAAO,EACT,GAAe,EACf,EAAY,WAAW,IAAI,EAAsB,EAAO,KAAK,EAAE,CAAC,EAChE,EAAuB,KACvB,EAAS,CAAC,GAEV,EAAO,KAAK,CAAE,EAEhB,KACF,EAEJ,CAGA,YAAqB,CACnB,GAAI,GAAI,EAAO,OACf,EAAc,CAAW,EAErB,EAAI,GAAK,EAAO,EAAI,KAAO,KAE7B,GAAe,CAAW,EAEtB,GAAa,GAAc,EAAY,SAE7C,EAAe,EACf,EAAY,CAAC,EACb,EAAuB,IACzB,CACF,EAEA,WAA4B,EAAI,CAC9B,GAAI,GAAO,EAAG,WAAW,CAAC,EAC1B,MAAQ,IAAK,GAAQ,GAAQ,GAAO,IAAO,KAAS,IAAO,KAAS,IAAO,GAC7E,CAEA,WAAsB,EAAI,CACxB,MAAO,GAAW,KAAK,CAAE,CAC3B,ICnPA,gBAGA,GAAM,GAAkB,CACtB,EAAK,EACL,EAAK,EACL,EAAK,EACL,EAAK,EACL,EAAK,EACL,EAAK,EACL,EAAK,EACL,EAAK,EACL,EAAK,EACL,EAAK,CACP,EAMM,EAAN,KAAmB,CACjB,aAAc,CACZ,KAAK,MAAQ,EACb,KAAK,cAAgB,EACrB,KAAK,QAAU,EACf,KAAK,SAAW,EAChB,KAAK,WAAa,GAClB,KAAK,SAAW,GAChB,KAAK,YAAc,GACnB,KAAK,YAAc,EACrB,CAEA,UAAW,CACT,GAAI,GAAQ,KAAK,MAOjB,MANI,MAAK,aACP,IAAS,KAAK,cAAgB,KAAK,SAEjC,KAAK,aACP,IAAS,KAAK,IAAI,GAAI,KAAK,QAAQ,GAEjC,KAAK,WACA,CAAC,EAEH,CACT,CAEA,OAAQ,CACN,KAAK,MAAQ,EACb,KAAK,cAAgB,EACrB,KAAK,QAAU,EACf,KAAK,SAAW,EAChB,KAAK,WAAa,GAClB,KAAK,SAAW,GAChB,KAAK,YAAc,GACnB,KAAK,YAAc,EACrB,CAEA,aAAa,EAAI,CAEf,GADA,KAAK,SAAW,GACZ,IAAO,IAAK,CACd,KAAK,WAAa,GAClB,MACF,CACA,GAAI,IAAO,IAAK,CACd,GAAI,KAAK,YAAa,KAAM,IAAI,OAAM,8BAA8B,EACpE,KAAK,YAAc,GACnB,MACF,CACA,GAAI,IAAO,IAAK,CACd,GAAI,KAAK,YAAa,KAAM,IAAI,OAAM,sBAAsB,EAC5D,KAAK,YAAc,GACnB,KAAK,SAAW,EAChB,MACF,CAEA,GAAI,GAAW,EAAgB,GAC/B,GAAI,IAAa,OAAW,KAAM,IAAI,OAAM,gBAAkB,CAAE,EAEhE,AAAI,KAAK,YACP,KAAK,SAAW,KAAK,SAAW,GAAK,EAChC,AAAI,KAAK,YACd,MAAK,cAAgB,KAAK,cAAgB,GAAK,EAC/C,KAAK,SAAW,IAEhB,KAAK,MAAQ,KAAK,MAAQ,GAAK,CAEnC,CACF,EAEA,EAAO,QAAU,ICzFjB,gBAMA,GAAM,GAAe,IAEf,EAAiB,CACrB,EAAE,EAAQ,EAAa,CACrB,GAAI,EAAY,OAAS,IAAM,EAC7B,KAAM,IAAI,OAAM,kDAAkD,EAEpE,GAAI,EAAO,SAAW,EAEpB,OAAS,GAAI,EAAG,EAAI,EAAY,OAAQ,GAAK,EAC3C,EAAO,KAAK,CAAC,EAAY,GAAI,EAAY,EAAI,EAAE,CAAC,MAKlD,MAAM,IAAI,OAAM,8CAA8C,CAElE,EACA,EAAE,EAAQ,EAAa,CAErB,GAAI,GAAK,EAAG,EAAK,EACjB,GAAI,EAAO,OAAS,GAAK,EAAY,OAAS,EAAG,CAC/C,GAAI,GAAO,EAAO,EAAO,OAAS,GAClC,EAAK,EAAK,GAAK,EAAY,GAC3B,EAAK,EAAK,GAAK,EAAY,EAC7B,CAEA,OAAS,GAAI,EAAG,EAAI,EAAY,OAAQ,GAAK,EAAG,CAC9C,GAAI,GAAI,EAAK,EAAY,GACrB,EAAI,EAAK,EAAY,EAAI,GAC7B,EAAO,KAAK,CAAC,EAAG,CAAC,CAAC,EAClB,EAAK,EAAG,EAAK,CACf,CACF,EAEA,EAAE,EAAQ,EAAa,CAErB,OAAS,GAAI,EAAG,EAAI,EAAY,OAAQ,GAAK,EAC3C,EAAO,KAAK,CAAC,EAAY,GAAI,EAAY,EAAI,EAAE,CAAC,CAEpD,EAEA,EAAE,EAAQ,EAAa,CACrB,GAAI,GAAK,EAAG,EAAK,EACjB,GAAI,EAAO,OAAS,EAAG,CACrB,GAAI,GAAO,EAAO,EAAO,OAAS,GAClC,EAAK,EAAK,GACV,EAAK,EAAK,EACZ,CACA,OAAS,GAAI,EAAG,EAAI,EAAY,OAAQ,GAAK,EAAG,CAC9C,GAAI,GAAI,EAAK,EAAY,GACrB,EAAI,EAAK,EAAY,EAAI,GAC7B,EAAO,KAAK,CAAC,EAAG,CAAC,CAAC,EAClB,EAAK,EAAG,EAAK,CACf,CACF,EACA,EAAE,EAAQ,EAAa,CACrB,GAAI,GAAI,EACR,AAAI,EAAO,OAAS,GAClB,GAAI,EAAO,EAAO,OAAS,GAAG,IAEhC,OAAS,GAAI,EAAG,EAAI,EAAY,OAAQ,GAAK,EAAG,CAC9C,GAAI,GAAI,EAAY,GACpB,EAAO,KAAK,CAAC,EAAG,CAAC,CAAC,CACpB,CACF,EACA,EAAE,EAAQ,EAAa,CACrB,GAAI,GAAI,EAAG,EAAK,EAChB,AAAI,EAAO,OAAS,GAClB,GAAK,EAAO,EAAO,OAAS,GAAG,GAC/B,EAAI,EAAO,EAAO,OAAS,GAAG,IAEhC,OAAS,GAAI,EAAG,EAAI,EAAY,OAAQ,GAAK,EAAG,CAC9C,GAAI,GAAI,EAAK,EAAY,GACzB,EAAO,KAAK,CAAC,EAAG,CAAC,CAAC,EAClB,EAAK,CACP,CACF,EACA,EAAE,EAAQ,EAAa,CACrB,GAAI,GAAI,EACR,AAAI,EAAO,OAAS,GAClB,GAAI,EAAO,EAAO,OAAS,GAAG,IAEhC,OAAS,GAAI,EAAG,EAAI,EAAY,OAAQ,GAAK,EAC3C,EAAO,KAAK,CAAC,EAAG,EAAY,EAAE,CAAC,CAEnC,EACA,EAAE,EAAQ,EAAa,CACrB,GAAI,GAAK,EAAG,EAAI,EAChB,AAAI,EAAO,OAAS,GAClB,GAAI,EAAO,EAAO,OAAS,GAAG,GAC9B,EAAK,EAAO,EAAO,OAAS,GAAG,IAEjC,OAAS,GAAI,EAAG,EAAI,EAAY,OAAQ,GAAK,EAAG,CAC9C,GAAI,GAAI,EAAK,EAAY,GACzB,EAAO,KAAK,CAAC,EAAG,CAAC,CAAC,EAClB,EAAK,CACP,CACF,CACF,EAEA,WAA+B,EAAG,CAChC,GAAI,GAAY,GAAI,GAChB,EAAM,EACN,EAAI,EAAE,OACN,EACA,EAAa,EACb,EAAS,CAAC,EACd,KAAO,EAAM,GACX,EAAK,EAAE,GACP,AAAI,IAAM,GACJ,GAAU,UACZ,EAAY,KAAK,EAAU,SAAS,CAAC,EAEvC,EAAU,MAAM,EACZ,GACF,EAAY,EAAQ,CAAW,EAEjC,EAAc,EAAe,GAC7B,EAAc,CAAC,GACV,AAAI,IAAO,KAAO,IAAO,IAC1B,EAAU,UACZ,GAAY,KAAK,EAAU,SAAS,CAAC,EACrC,EAAU,MAAM,GAGT,IAAO,KAAO,IAAO,KAGrB,GAAU,UAAY,IAAO,KAEtC,GAAY,KAAK,EAAU,SAAS,CAAC,EACrC,EAAU,MAAM,GAChB,EAAU,aAAa,CAAE,GAI3B,GAAO,EAET,MAAI,GAAU,UACZ,EAAY,KAAK,EAAU,SAAS,CAAC,EAEnC,GACF,EAAY,EAAQ,CAAW,EAE1B,CACT,CAEA,EAAO,QAAU,IC1JjB,mBAAO,QAAU,SAA6B,EAAI,CAChD,MAAO,GAAS,EAAG,WAAW,IAAI,MAAM,GAAK,EAAG,WAAW,IAAI,OAAO,CAAC,CACzE,EAEA,WAAkB,EAAY,CAE5B,GAAI,EAAW,KAAO,IAAK,CACzB,GAAI,EAAW,SAAW,EAAI,EAAG,CAE/B,GAAI,GAAI,OAAO,SAAS,EAAW,OAAO,EAAG,CAAC,EAAG,EAAE,EAC/C,EAAI,OAAO,SAAS,EAAW,OAAO,EAAG,CAAC,EAAG,EAAE,EAC/C,EAAI,OAAO,SAAS,EAAW,OAAO,EAAG,CAAC,EAAG,EAAE,EACnD,MAAO,AAAS,CAAC,EAAG,EAAG,CAAC,CAC1B,CACA,GAAI,EAAW,SAAW,EAAI,GAAK,EAAW,SAAW,EAAI,EAAG,CAE9D,GAAI,GAAK,EAAW,OAAO,EAAG,CAAC,EAC3B,EAAK,EAAW,OAAO,EAAG,CAAC,EAC3B,EAAK,EAAW,OAAO,EAAG,CAAC,EAE3B,EAAI,OAAO,SAAS,EAAK,EAAI,EAAE,EAC/B,EAAI,OAAO,SAAS,EAAK,EAAI,EAAE,EAC/B,EAAI,OAAO,SAAS,EAAK,EAAI,EAAE,EACnC,MAAO,AAAS,CAAC,EAAG,EAAG,CAAC,CAC1B,CACA,KAAM,IAAI,OAAM,+BAAiC,CAAU,CAC7D,SAAW,EAAW,WAAW,MAAM,EAAG,CAExC,GAAI,GAAS,EAAW,OAAO,CAAC,EAAE,MAAM,GAAG,EAAE,IAAI,GAAK,OAAO,WAAW,CAAC,CAAC,EAC1E,SAAO,GAAK,KAAK,MAAM,EAAO,GAAK,GAAG,EAC/B,AAAc,CACvB,CACA,GAAI,GAAM,EAAW,MAAM,mBAAmB,EAC1C,EAiBJ,GAhBI,GACF,GAAW,EAAI,GACZ,MAAM,GAAG,EACT,IAAI,AAAC,GAAM,OAAO,SAAS,EAAG,EAAE,CAAC,EACjC,OAAO,CAAY,GAEnB,GACH,GAAM,EAAW,MAAM,wBAAwB,EAC3C,GACF,GAAW,CACP,OAAO,SAAS,EAAI,GAAG,OAAO,EAAG,CAAC,EAAG,EAAE,EACvC,OAAO,SAAS,EAAI,GAAG,OAAO,EAAG,CAAC,EAAG,EAAE,EACvC,OAAO,SAAS,EAAI,GAAG,OAAO,EAAG,CAAC,EAAG,EAAE,CACzC,IAGF,CAAC,GACH,GAAM,EAAW,MAAM,wBAAwB,EAC3C,GAAK,CACP,GAAI,GAAK,EAAI,GAAG,OAAO,EAAG,CAAC,EACvB,EAAK,EAAI,GAAG,OAAO,EAAG,CAAC,EACvB,EAAK,EAAI,GAAG,OAAO,EAAG,CAAC,EAC3B,EAAW,CACT,OAAO,SAAS,EAAK,EAAI,EAAE,EAC3B,OAAO,SAAS,EAAK,EAAI,EAAE,EAC3B,OAAO,SAAS,EAAK,EAAI,EAAE,CAC7B,CACF,CAGF,GAAI,EAAU,CACZ,GAAI,EAAS,SAAW,EACtB,KAAM,IAAI,OAAM,+BAAiC,CAAU,EAE7D,MAAO,AAAS,EAClB,CACA,cAAQ,MAAM,+BAAiC,CAAU,EACnD,GAAI,OAAM,+BAAiC,CAAU,CAC7D,CAQA,WAAsB,EAAG,CACvB,MAAO,QAAO,SAAS,CAAC,CAC1B,IClFA,oBAAM,GAA2B,IAC3B,EAAwB,IACxB,EAAe,IACf,EAAsB,IAE5B,EAAO,QAAU,CACf,2BACA,wBACA,eAGA,qBACF",
6 | "names": []
7 | }
8 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const createStreamingSVGParser = require('./lib/createStreamingSVGParser');
2 | const getPointsFromPathData = require('./lib/getPointsFromPathData');
3 | const NumberParser = require('./lib/NumberParser');
4 | const getElementFillColor = require('./lib/getElementFillColor');
5 |
6 | module.exports = {
7 | createStreamingSVGParser,
8 | getPointsFromPathData,
9 | NumberParser,
10 |
11 | // Somewhat specific methods. Defining it temporarily here. May go away
12 | getElementFillColor
13 | }
--------------------------------------------------------------------------------
/lib/NumberParser.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Streaming parser of numbers.
3 | */
4 | const CharacterLookup = {
5 | '0': 0,
6 | '1': 1,
7 | '2': 2,
8 | '3': 3,
9 | '4': 4,
10 | '5': 5,
11 | '6': 6,
12 | '7': 7,
13 | '8': 8,
14 | '9': 9
15 | }
16 |
17 | /**
18 | * Naive parser of integer numbers. Optimized for memory consumption and
19 | * CPU performance. Not very strong on validation side.
20 | */
21 | class NumberParser {
22 | constructor() {
23 | this.value = 0;
24 | this.fractionValue = 0;
25 | this.divider = 1;
26 | this.exponent = 0;
27 | this.isNegative = false;
28 | this.hasValue = false;
29 | this.hasFraction = false;
30 | this.hasExponent = false
31 | }
32 |
33 | getValue() {
34 | let value = this.value;
35 | if (this.hasFraction) {
36 | value += this.fractionValue / this.divider;
37 | }
38 | if (this.hasExponent) {
39 | value *= Math.pow(10, this.exponent);
40 | }
41 | if (this.isNegative) {
42 | return -value;
43 | }
44 | return value;
45 | }
46 |
47 | reset() {
48 | this.value = 0;
49 | this.fractionValue = 0;
50 | this.divider = 1;
51 | this.exponent = 0;
52 | this.isNegative = false;
53 | this.hasValue = false;
54 | this.hasFraction = false;
55 | this.hasExponent = false
56 | }
57 |
58 | addCharacter(ch) {
59 | this.hasValue = true;
60 | if (ch === '-') {
61 | this.isNegative = true;
62 | return;
63 | }
64 | if (ch === '.') {
65 | if (this.hasFraction) throw new Error('Already has fractional part!');
66 | this.hasFraction = true;
67 | return;
68 | }
69 | if (ch === 'e') {
70 | if (this.hasExponent) throw new Error('Already has exponent');
71 | this.hasExponent = true;
72 | this.exponent = 0;
73 | return;
74 | }
75 |
76 | let numValue = CharacterLookup[ch];
77 | if (numValue === undefined) throw new Error('Not a digit: ' + ch)
78 |
79 | if (this.hasExponent) {
80 | this.exponent = this.exponent * 10 + numValue;
81 | } else if (this.hasFraction) {
82 | this.fractionValue = this.fractionValue * 10 + numValue;
83 | this.divider *= 10;
84 | } else {
85 | this.value = this.value * 10 + numValue;
86 | }
87 | }
88 | }
89 |
90 | module.exports = NumberParser;
--------------------------------------------------------------------------------
/lib/createStreamingSVGParser.js:
--------------------------------------------------------------------------------
1 |
2 | // Possible states of SVG parsing
3 | const WAIT_TAG_OPEN = 1;
4 | const READ_TAG_OR_COMMENT = 2;
5 | const READ_TAG = 3;
6 | const READ_TAG_CLOSE = 4;
7 | const READ_COMMENT = 5;
8 | const WAIT_TAG_CLOSE = 6;
9 | const WAIT_ATTRIBUTE_OR_CLOSE_TAG = 7;
10 | const READ_ATTRIBUTE_NAME = 8;
11 | const READ_ATTRIBUTE_VALUE = 9;
12 | const WAIT_ATTRIBUTE_VALUE = 10;
13 | const WAIT_ATTRIBUTE_ASSIGNMENT_OR_NEXT_ATTRIBUTE = 11;
14 |
15 | const A = 'A'.charCodeAt(0);
16 | const Z = 'z'.charCodeAt(0);
17 | const whitespace = /\s/;
18 |
19 | class XMLNode {
20 | constructor(name, parent) {
21 | this.children = [];
22 | this.attributes = new Map(),
23 | this.tagName = name;
24 | this.parent = parent;
25 | }
26 | }
27 |
28 | /**
29 | * Creates a new instance of the parser. Parser will consume chunk of text and will
30 | * notify the caller when new tag is opened or closed.
31 | *
32 | * If `generateAsync` is true - the parser will break its own execution,
33 | * allowing UI thread to catch up. (Only for browser environment now)
34 | *
35 | * @returns Function(chunk: String) - function that processes chunk of text
36 | *
37 | * WARNING: This may not work correctly with multi-byte unicode characters
38 | */
39 | module.exports = function createStreamingSVGParser(notifyTagOpen, notifyTagClose, generateAsync) {
40 | let currentState = WAIT_TAG_OPEN;
41 | let closeAttributeSymbol;
42 | let currentTagName;
43 | let currentAttributeName;
44 | let lastElement;
45 | let buffer;
46 | let innerText;
47 | if (notifyTagClose === undefined) {
48 | notifyTagClose = Function.prototype; // noop
49 | }
50 |
51 | return generateAsync ? processChunkAsync : processChunkSync;
52 |
53 | function processChunkAsync(chunk) {
54 | return new Promise(resolve => iterateSymbolsAsync(chunk, 0, resolve));
55 | }
56 |
57 | function iterateSymbolsAsync(chunk, idx, resolve) {
58 | let start = performance.now();
59 | let processed = 0;
60 |
61 | while (idx < chunk.length) {
62 | // Assuming each element is a symbol (i.e. this wouldn't work for unicode well).
63 | processSymbol(chunk[idx]);
64 |
65 | idx += 1;
66 | processed += 1;
67 | if (processed > 30000) {
68 | let elapsed = performance.now() - start;
69 | if (elapsed > 32) {
70 | setTimeout(() => iterateSymbolsAsync(chunk, idx, resolve), 0);
71 | return;
72 | }
73 | }
74 | }
75 | resolve();
76 | }
77 |
78 | function processChunkSync(chunk) {
79 | return iterateSymbols(chunk, 0);
80 | }
81 |
82 | function iterateSymbols(chunk, idx) {
83 | let processed = 0;
84 |
85 | while (idx < chunk.length) {
86 | // Assuming each element is a symbol (i.e. this wouldn't work for unicode well).
87 | processSymbol(chunk[idx]);
88 | idx += 1;
89 | }
90 | }
91 |
92 | function processSymbol(ch) {
93 | switch (currentState) {
94 | case WAIT_TAG_OPEN:
95 | if (ch === '<') {
96 | if (innerText && lastElement) {
97 | lastElement.innerText = innerText.join('');
98 | innerText = null;
99 | }
100 | currentState = READ_TAG_OR_COMMENT;
101 | } else if (innerText) {
102 | innerText.push(ch);
103 | }
104 | break;
105 | case WAIT_TAG_CLOSE:
106 | if (ch === '>') currentState = WAIT_TAG_OPEN;
107 | break;
108 | case READ_TAG_OR_COMMENT:
109 | if (ch === '!' || ch === '?') {
110 | buffer = [ch];
111 | currentState = READ_COMMENT;
112 | } else if (ch === '/') {
113 | if (innerText) {
114 | lastElement.innerText = innerText.join('');
115 | innerText = null;
116 | }
117 | notifyTagClose(lastElement);
118 | if (lastElement) lastElement = lastElement.parent;
119 | currentState = WAIT_TAG_CLOSE;
120 | innerText = null;
121 | } else {
122 | currentState = READ_TAG;
123 | buffer = [ch];
124 | }
125 | break;
126 | case READ_COMMENT: {
127 | buffer.push(ch);
128 | let l = buffer.length;
129 | if (buffer.length > 3 &&
130 | buffer[l - 1] === '>' &&
131 | buffer[l - 2] === '-' &&
132 | buffer[l - 3] === '-') {
133 | currentState = WAIT_TAG_OPEN;
134 | innerText = null;
135 | } else if ((buffer[0] === '!' && (buffer.length > 1 && buffer[1] !== '-')) || //
144 | // Skip this one, as next `READ_TAG` will close it.
145 | } else {
146 | currentTagName = buffer.join('');
147 | currentState = WAIT_ATTRIBUTE_OR_CLOSE_TAG;
148 | let parent = lastElement;
149 | lastElement = new XMLNode(currentTagName, parent);
150 |
151 | if (parent) parent.children.push(lastElement);
152 | if (ch === '>') finishTag();
153 | }
154 | break;
155 | }
156 | case READ_TAG_CLOSE: {
157 | if (isTagNameCharacter(ch)) {
158 | buffer.push(ch);
159 | } else if (ch === '>') {
160 | let closedTag = buffer.join('')
161 | if (closedTag !== currentTagName) {
162 | throw new Error('Expected ' + currentTagName + ' to be closed, but got ' + closedTag)
163 | }
164 | }
165 |
166 | break;
167 | }
168 | case WAIT_ATTRIBUTE_OR_CLOSE_TAG: {
169 | if (ch === '>') {
170 | finishTag();
171 | } else if (isTagNameCharacter(ch)) {
172 | buffer = [ch];
173 | currentState = READ_ATTRIBUTE_NAME;
174 | } else {
175 | buffer.push(ch);
176 | }
177 | break;
178 | }
179 | case READ_ATTRIBUTE_NAME: {
180 | if (!isTagNameCharacter(ch)) {
181 | currentAttributeName = buffer.join('');
182 | if (ch === '=') currentState = WAIT_ATTRIBUTE_VALUE;
183 | else if (ch === '>') {
184 | lastElement.attributes.set(currentAttributeName, true);
185 | finishTag();
186 | } else currentState = WAIT_ATTRIBUTE_ASSIGNMENT_OR_NEXT_ATTRIBUTE;
187 | } else {
188 | buffer.push(ch);
189 | }
190 | break;
191 | }
192 | case WAIT_ATTRIBUTE_ASSIGNMENT_OR_NEXT_ATTRIBUTE: {
193 | if (ch === '=') {
194 | currentState = WAIT_ATTRIBUTE_VALUE;
195 | } else if (isTagNameCharacter(ch)) {
196 | currentState = READ_ATTRIBUTE_NAME;
197 | // Case of a boolean attribute
198 | lastElement.attributes.set(buffer.join(''), true);
199 | buffer = [ch];
200 | } else if (ch === '>') {
201 | lastElement.attributes.set(buffer.join(''), true);
202 | finishTag();
203 | }
204 | break;
205 | }
206 | case WAIT_ATTRIBUTE_VALUE: {
207 | if (ch === "\"" || ch === "'" || !isWhiteSpace(ch)) {
208 | buffer = [];
209 | currentState = READ_ATTRIBUTE_VALUE;
210 | // not 100% accurate!
211 | closeAttributeSymbol = ch;
212 | }
213 | // TODO: Might want to tighten validation here;
214 | break;
215 | }
216 | case READ_ATTRIBUTE_VALUE: {
217 | if (ch === closeAttributeSymbol) {
218 | currentState = WAIT_ATTRIBUTE_OR_CLOSE_TAG;
219 | lastElement.attributes.set(currentAttributeName, buffer.join(''));
220 | currentAttributeName = null;
221 | buffer = [];
222 | } else {
223 | buffer.push(ch);
224 | }
225 | break;
226 | }
227 | }
228 | }
229 |
230 |
231 | function finishTag() {
232 | let l = buffer.length;
233 | notifyTagOpen(lastElement); // we finished reading the attribute definition
234 |
235 | if (l > 0 && buffer[l - 1] === '/') {
236 | // a case of quick close
237 | notifyTagClose(lastElement);
238 | // since we closed this tag, let's pop it, and wait for the sibling.
239 | if (lastElement) lastElement = lastElement.parent;
240 | }
241 | currentState = WAIT_TAG_OPEN;
242 | innerText = [];
243 | currentAttributeName = null;
244 | }
245 | }
246 |
247 | function isTagNameCharacter(ch) {
248 | let code = ch.charCodeAt(0);
249 | return (A <= code && code <= Z) || (ch === '_') || (ch === '-') || (ch === ':');
250 | }
251 |
252 | function isWhiteSpace(ch) {
253 | return whitespace.test(ch);
254 | }
255 |
--------------------------------------------------------------------------------
/lib/getElementFillColor.js:
--------------------------------------------------------------------------------
1 | module.exports = function getElementFillColor(el) {
2 | return getColor(el.attributes.get('fill') || el.attributes.get('style'));
3 | }
4 |
5 | function getColor(styleValue) {
6 | // TODO: could probably be done faster.
7 | if (styleValue[0] === '#') {
8 | if (styleValue.length === 1 + 6) {
9 | // #rrggbb
10 | let r = Number.parseInt(styleValue.substr(1, 2), 16);
11 | let g = Number.parseInt(styleValue.substr(3, 2), 16);
12 | let b = Number.parseInt(styleValue.substr(5, 2), 16);
13 | return hexColor([r, g, b]);
14 | }
15 | if (styleValue.length === 1 + 3 || styleValue.length === 1 + 4) {
16 | // #rgba
17 | let rs = styleValue.substr(1, 1);
18 | let gs = styleValue.substr(2, 1);
19 | let bs = styleValue.substr(3, 1);
20 | // ignore a
21 | let r = Number.parseInt(rs + rs, 16);
22 | let g = Number.parseInt(gs + gs, 16);
23 | let b = Number.parseInt(bs + bs, 16);
24 | return hexColor([r, g, b]);
25 | }
26 | throw new Error('Cannot parse this color yet ' + styleValue);
27 | } else if (styleValue.startsWith('rgba')) {
28 | // rgba(rr,gg,bb,a)
29 | let colors = styleValue.substr(5).split(/,/).map(x => Number.parseFloat(x))
30 | colors[3] = Math.round(colors[3] * 255);
31 | return alphaHexColor(colors);
32 | }
33 | let rgb = styleValue.match(/fill:rgb\((.+?)\)/);
34 | let rgbArray;
35 | if (rgb) {
36 | rgbArray = rgb[1]
37 | .split(',')
38 | .map((x) => Number.parseInt(x, 10))
39 | .filter(finiteNumber);
40 | }
41 | if (!rgbArray) {
42 | rgb = styleValue.match(/fill:#([0-9a-fA-F]{6})/)
43 | if (rgb) {
44 | rgbArray = [
45 | Number.parseInt(rgb[1].substr(0, 2), 16),
46 | Number.parseInt(rgb[1].substr(2, 2), 16),
47 | Number.parseInt(rgb[1].substr(4, 2), 16)
48 | ]
49 | }
50 | }
51 | if (!rgbArray) {
52 | rgb = styleValue.match(/fill:#([0-9a-fA-F]{3})/)
53 | if (rgb) {
54 | let rs = rgb[1].substr(0, 1);
55 | let gs = rgb[1].substr(1, 1);
56 | let bs = rgb[1].substr(2, 1);
57 | rgbArray = [
58 | Number.parseInt(rs + rs, 16),
59 | Number.parseInt(gs + gs, 16),
60 | Number.parseInt(bs + bs, 16)
61 | ]
62 | }
63 |
64 | }
65 | if (rgbArray) {
66 | if (rgbArray.length !== 3){
67 | throw new Error('Cannot parse this color yet ' + styleValue);
68 | }
69 | return hexColor(rgbArray);
70 | }
71 | console.error('Cannot parse this color yet ' + styleValue)
72 | throw new Error('Cannot parse this color yet ' + styleValue);
73 | }
74 |
75 | function hexColor(arr) {
76 | return arr;
77 | }
78 | function alphaHexColor(arr) {
79 | return arr;
80 | }
81 | function finiteNumber(x) {
82 | return Number.isFinite(x);
83 | }
--------------------------------------------------------------------------------
/lib/getPointsFromPathData.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | * Extremely fast SVG path data attribute parser. Currently
4 | * it doesn't support curves or arcs. Only M, L, H, V (and m, l, h, v) are
5 | * supported
6 | */
7 | const NumberParser = require('./NumberParser');
8 |
9 | const processCommand = {
10 | M(points, lastNumbers) {
11 | if (lastNumbers.length % 2 !== 0) {
12 | throw new Error('Expected an even number of numbers for M command');
13 | }
14 | if (points.length === 0) {
15 | // consider this to be absolute points
16 | for (let i = 0; i < lastNumbers.length; i += 2) {
17 | points.push([lastNumbers[i], lastNumbers[i + 1]]);
18 | }
19 | } else {
20 | // Note: this is not true for generic case, and could/should be extended to start a new path.
21 | // We are just optimizing for own sake of a single path
22 | throw new Error('Only one "Move" command per path is expected');
23 | }
24 | },
25 | m(points, lastNumbers) {
26 | // https://www.w3.org/TR/SVG11/paths.html#PathDataMovetoCommands
27 | let lx = 0, ly = 0;
28 | if (points.length > 0 && lastNumbers.length > 1) {
29 | let last = points[points.length - 1];
30 | lx = last[0] + lastNumbers[0];
31 | ly = last[1] + lastNumbers[1]; ;
32 | }
33 | // TODO: Likely need to break points here into two arrays.
34 | for (let i = 2; i < lastNumbers.length; i += 2) {
35 | let x = lx + lastNumbers[i];
36 | let y = ly + lastNumbers[i + 1];
37 | points.push([x, y]);
38 | lx = x; ly = y;
39 | }
40 | },
41 | // line to:
42 | L(points, lastNumbers) {
43 | // TODO: validate lastNumbers.length % 2 === 0
44 | for (let i = 0; i < lastNumbers.length; i += 2) {
45 | points.push([lastNumbers[i], lastNumbers[i + 1]]);
46 | }
47 | },
48 | // relative line to:
49 | l(points, lastNumbers) {
50 | let lx = 0, ly = 0;
51 | if (points.length > 0) {
52 | let last = points[points.length - 1];
53 | lx = last[0];
54 | ly = last[1];
55 | }
56 | for (let i = 0; i < lastNumbers.length; i += 2) {
57 | let x = lx + lastNumbers[i];
58 | let y = ly + lastNumbers[i + 1];
59 | points.push([x, y]);
60 | lx = x; ly = y;
61 | }
62 | },
63 | H(points, lastNumbers) {
64 | let y = 0;
65 | if (points.length > 0) {
66 | y = points[points.length - 1][1];
67 | }
68 | for (let i = 0; i < lastNumbers.length; i += 1) {
69 | let x = lastNumbers[i];
70 | points.push([x, y]);
71 | }
72 | },
73 | h(points, lastNumbers) {
74 | let y = 0, lx = 0;
75 | if (points.length > 0) {
76 | lx = points[points.length - 1][0];
77 | y = points[points.length - 1][1];
78 | }
79 | for (let i = 0; i < lastNumbers.length; i += 1) {
80 | let x = lx + lastNumbers[i];
81 | points.push([x, y]);
82 | lx = x;
83 | }
84 | },
85 | V(points, lastNumbers) {
86 | let x = 0;
87 | if (points.length > 0) {
88 | x = points[points.length - 1][0];
89 | }
90 | for (let i = 0; i < lastNumbers.length; i += 1) {
91 | points.push([x, lastNumbers[i]]);
92 | }
93 | },
94 | v(points, lastNumbers) {
95 | let ly = 0, x = 0;
96 | if (points.length > 0) {
97 | x = points[points.length - 1][0];
98 | ly = points[points.length - 1][1];
99 | }
100 | for (let i = 0; i < lastNumbers.length; i += 1) {
101 | let y = ly + lastNumbers[i];
102 | points.push([x, y]);
103 | ly = y;
104 | }
105 | }
106 | }
107 |
108 | function getPointsFromPathData(d) {
109 | let numParser = new NumberParser();
110 | let idx = 0;
111 | let l = d.length;
112 | let ch;
113 | let lastNumbers, lastCommand;
114 | let points = [];
115 | while (idx < l) {
116 | ch = d[idx];
117 | if (ch in processCommand) {
118 | if (numParser.hasValue) {
119 | lastNumbers.push(numParser.getValue())
120 | }
121 | numParser.reset();
122 | if (lastNumbers) {
123 | lastCommand(points, lastNumbers);
124 | }
125 | lastCommand = processCommand[ch];
126 | lastNumbers = [];
127 | } else if (ch === ' ' || ch === ',') {
128 | if (numParser.hasValue) {
129 | lastNumbers.push(numParser.getValue())
130 | numParser.reset();
131 | }
132 | // ignore.
133 | } else if (ch === 'Z' || ch === 'z') {
134 | // TODO: Likely need to close the path..
135 | // ignore
136 | } else if (numParser.hasValue && ch === '-') {
137 | // this considered to be a start of the next number.
138 | lastNumbers.push(numParser.getValue())
139 | numParser.reset();
140 | numParser.addCharacter(ch);
141 | } else {
142 | numParser.addCharacter(ch);
143 | }
144 | idx += 1;
145 | }
146 | if (numParser.hasValue) {
147 | lastNumbers.push(numParser.getValue());
148 | }
149 | if (lastNumbers) {
150 | lastCommand(points, lastNumbers);
151 | }
152 | return points;
153 | }
154 |
155 | module.exports = getPointsFromPathData;
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "streaming-svg-parser",
3 | "version": "1.1.0",
4 | "description": "An SVG parser that processes SVG documents in chunk",
5 | "main": "index.js",
6 | "jsdelivr": "dist/streaming-svg-parser.min.js",
7 | "unpkg": "dist/streaming-svg-parser.min.js",
8 | "scripts": {
9 | "test": "tap --branches=50 --lines=50 --statements=50 --functions=50 test/*.js",
10 | "build-min": "esbuild --bundle index.js --minify --sourcemap --outfile=dist/streaming-svg-parser.min.js --global-name=streamingSVGParser",
11 | "build-max": "esbuild --bundle index.js --sourcemap --outfile=dist/streaming-svg-parser.js --global-name=streamingSVGParser",
12 | "build": "npm run build-min && npm run build-max"
13 | },
14 | "repository": {
15 | "type": "git",
16 | "url": "git+https://github.com/anvaka/streaming-svg-parser.git"
17 | },
18 | "keywords": [
19 | "svg",
20 | "streaming",
21 | "parser"
22 | ],
23 | "author": "Andrei Kashcha",
24 | "license": "MIT",
25 | "bugs": {
26 | "url": "https://github.com/anvaka/streaming-svg-parser/issues"
27 | },
28 | "homepage": "https://github.com/anvaka/streaming-svg-parser#readme",
29 | "devDependencies": {
30 | "esbuild": "^0.14.49",
31 | "tap": "^16.2.0"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/perf/data/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/perf/downloadFiles.sh:
--------------------------------------------------------------------------------
1 | curl https://upload.wikimedia.org/wikipedia/commons/7/7f/Italy_-_Regions_and_provinces.svg > data/italy.svg
--------------------------------------------------------------------------------
/perf/parse_italy.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 | const lib = require('../index');
4 | const inputFile = process.argv[2] || path.join(__dirname, 'data', 'italy.svg');
5 |
6 | if(!fs.existsSync(inputFile)) {
7 | console.error(`File ${inputFile} not found. Try downloading file first using ./download.sh script`);
8 | process.exit(1);
9 | }
10 |
11 | let tagCount = 0;
12 | let totalLength = 0;
13 | const parseChunk = lib.createStreamingSVGParser(el => {
14 | tagCount += 1;
15 | });
16 |
17 | let readStream = fs.createReadStream(inputFile, 'utf8');
18 | let start = performance.now();
19 | readStream.on('data', chunk => {
20 | totalLength += chunk.length;
21 | parseChunk(chunk);
22 | });
23 | readStream.on('end', () => {
24 | let elapsed = performance.now() - start;
25 | console.log('File length is ' + prettyNumber(totalLength) + ' symbols');
26 | console.log('Processed ' + tagCount + ' tags in ' + elapsed + 'ms');
27 | });
28 |
29 | function prettyNumber(x) {
30 | return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
31 | }
--------------------------------------------------------------------------------
/test/createStreamingSVGParser.js:
--------------------------------------------------------------------------------
1 | const test = require('tap').test;
2 | const {createStreamingSVGParser, getPointsFromPathData} = require('../index');
3 | const getElementFillColor = require('../lib/getElementFillColor');
4 |
5 | test('it can parse', (t) => {
6 | let lastOpenElement, lastCloseElement;
7 | let parseText = createStreamingSVGParser(
8 | open => {
9 | lastOpenElement = open;
10 | },
11 | close => {
12 | lastCloseElement = close;
13 | }
14 | );
15 | parseText('');
16 | parseText('')
24 | t.equal(lastCloseElement, svg);
25 | t.end();
26 | });
27 |
28 | test('it can print', t => {
29 | let indent = '';
30 | let buffer = [];
31 | let parseText = createStreamingSVGParser(
32 | openElement => {
33 | // attributes is a map, let's print it
34 | let attributes = Array.from(openElement.attributes)
35 | .map(pair => pair.join('='))
36 | .join(' ');
37 |
38 | buffer.push(indent + '<' + openElement.tagName + ' ' + attributes + '>')
39 | indent += ' ';
40 | },
41 | closeElement => {
42 | indent = indent.substring(2);
43 | buffer.push(indent + '' + closeElement.tagName + '>');
44 | }
45 | );
46 | parseText('');
47 | parseText('');
50 |
51 | // we got indents now:
52 | t.equal(buffer.join('\n'),
53 | ``)
57 | t.end();
58 | })
59 |
60 | test('it can parse comments', t => {
61 | let passed = true;
62 | let totalTags = 0;
63 | let parseText = createStreamingSVGParser(
64 | Function.prototype,
65 | el => {
66 | if (el.tagName === 'text') {
67 | passed = false;
68 | }
69 | totalTags += 1;
70 | }
71 | );
72 | parseText('');
73 | parseText('');
76 | t.equal(passed, true);
77 | t.equal(totalTags, 1, 'One tag found')
78 | t.end()
79 | });
80 |
81 | test('it parses tags on new lines', t=> {
82 | let passed = false;
83 | let parseText = createStreamingSVGParser(
84 | el => {
85 | if (el.tagName === 'g') {
86 | passed = true;
87 | }
88 | },
89 | Function.prototype
90 | );
91 | parseText('')
92 | parseText('');
93 | t.equal(passed, true);
94 | t.end()
95 | });
96 |
97 | test('it can parse text', t => {
98 | let passed = false;
99 | let parseText = createStreamingSVGParser(
100 | Function.prototype,
101 | el => {
102 | if (el.tagName === 'text') {
103 | t.equal(el.innerText, 'Hello world')
104 | t.equal(el.attributes.get('style'), "font-family:'Arial'");
105 | passed = true;
106 | }
107 | }
108 | );
109 | parseText('');
110 | parseText('');
113 | t.equal(passed, true);
114 | t.end()
115 | });
116 |
117 | test('it can parse path data', t => {
118 | let passed = false;
119 | const pathData ='M10 10 L200 -10 l-10 -10 H100 h10V10 v10-10 1e1 0.5 m0 0 10 10';
120 | let parseText = createStreamingSVGParser(
121 | Function.prototype,
122 | el => {
123 | if (el.tagName === 'path') {
124 | let points = getPointsFromPathData(el.attributes.get('d'));
125 | t.same(points, [
126 | [10, 10], // M 10 10
127 | [200, -10], // L 200 -10
128 | [190, -20], // l -10 -10
129 | [100, -20], // H 100
130 | [110, -20], // h 10
131 | [110, 10], // V 10
132 | [110, 20], // v 10
133 | [110, 10], // -10
134 | [110, 20], // 1e1
135 | [110, 20.5], // 0.5
136 | [120, 30.5], // m0 0 10 10
137 | ]);
138 | passed = true;
139 | }
140 | }
141 | );
142 | parseText('');
143 | parseText('');
146 | t.equal(passed, true);
147 | t.end()
148 | });
149 |
150 | test('it can read single boolean attribute', t => {
151 | let passed = false;
152 | let parseText = createStreamingSVGParser(
153 | el => {
154 | t.ok(el.attributes.has('enabled'), 'enabled is present');
155 | passed = true;
156 | }, Function.prototype);
157 | parseText('')
158 | t.ok(passed);
159 | t.end();
160 | });
161 |
162 | test('it can get inner text', t => {
163 | let passed = false;
164 | let parseText = createStreamingSVGParser(
165 | Function.prototype,
166 | el => {
167 | t.equal(el.innerText, 'Hello world', 'inner text is correct');
168 | passed = true;
169 | });
170 | parseText('Hello world')
171 | t.ok(passed);
172 | t.end();
173 | });
174 |
175 | test('it ignores whitespace after single attribute', t => {
176 | let passed = false;
177 | let parseText = createStreamingSVGParser(
178 | el => {
179 | t.ok(el.attributes.has('enabled'), 'enabled is present');
180 | passed = true;
181 | }, Function.prototype);
182 |
183 | parseText('')
184 | t.ok(passed);
185 | t.end();
186 | });
187 |
188 | test('it can read single boolean attribute followed by another attribute', t => {
189 | let passed = false;
190 | let parseText = createStreamingSVGParser(
191 | el => {
192 | t.ok(el.attributes.has('enabled'), 'enabled is present');
193 | t.equal(el.attributes.get('d'), 'M0,0 1,1', 'data is correct');
194 | passed = true;
195 | }, Function.prototype);
196 | parseText('')
197 | t.ok(passed);
198 | t.end();
199 | });
200 |
201 | test('it can read tag without attributes', t => {
202 | let passed = false;
203 | let parseText = createStreamingSVGParser(
204 | el => {
205 | t.equal(el.tagName, 'g', 'g is read');
206 | passed = true;
207 | }, Function.prototype);
208 | parseText('')
209 | parseText('')
210 | t.ok(passed);
211 | t.end();
212 | });
213 |
214 |
215 | test('it can get element fill color', t => {
216 | let testCases = [
217 | {fill: '#ff0000', parsed: [255, 0, 0]},
218 | {fill: '#f00', parsed: [255, 0, 0]},
219 | {style: "fill:#f01", parsed: [255, 0, 17]},
220 | {style: "fill:rgb(255, 40, 0)", parsed: [255, 40, 0]},
221 | {fill: "rgba(255, 127, 0, 1)", parsed: [255, 127, 0, 255]},
222 | ];
223 | let processedCount = 0;
224 | let idToTestCase = new Map();
225 | testCases.forEach((testCase, idx) => {
226 | testCase.id = idx;
227 | idToTestCase.set('' + idx, testCase);
228 | });
229 |
230 | let parseText = createStreamingSVGParser(
231 | Function.prototype,
232 | el => {
233 | if (el.tagName !== 'circle') return;
234 | let id = el.attributes.get('id');
235 | let testCase = idToTestCase.get(id);
236 | if (!testCase) {
237 | throw new Error('Unknown test case id: ' + id);
238 | }
239 | let parsedColor = getElementFillColor(el);
240 | t.same(parsedColor, testCase.parsed);
241 | processedCount += 1;
242 | }
243 | );
244 | parseText('');
245 | parseText('');
253 | t.equal(processedCount, testCases.length);
254 | t.end();
255 | });
256 |
257 | test('it can process async', t => {
258 | let openCount = 0, closeCount = 0;
259 | let parseTextAsync = createStreamingSVGParser(
260 | open => { openCount += 1; },
261 | close => {closeCount += 1; },
262 | true
263 | );
264 | return parseTextAsync([
265 | '',
266 | ''].join('\n')).then(() => {
269 | t.equal(openCount, 2);
270 | t.equal(closeCount, 2);
271 | t.end();
272 | });
273 | })
274 |
275 | test('it throws when two M commands are in a row', t => {
276 | let passed = false;
277 | const pathData ='M10 10 M0 0';
278 | let parseText = createStreamingSVGParser(
279 | Function.prototype,
280 | el => {
281 | if (el.tagName === 'path') {
282 | t.throws(() => {
283 | getPointsFromPathData(el.attributes.get('d'));
284 | });
285 | passed = true;
286 | }
287 | }
288 | );
289 | parseText(``);
290 | t.equal(passed, true);
291 | t.end()
292 | });
293 |
294 | test('NumberParser throws on second exponent', t => {
295 | t.throws(
296 | () => getPointsFromPathData('M1e1e1 10'), /Already has exponent/
297 | );
298 | t.end();
299 | });
300 |
301 | test('NumberParser throws on second fractional part', t => {
302 | t.throws(
303 | () => getPointsFromPathData('M1.1.1 10'), /fractional part/
304 | );
305 | t.end();
306 | });
307 |
308 | test('NumberParser throws on not a digit', t => {
309 | t.throws(
310 | () => getPointsFromPathData('M1p 10'), /Not a digit/
311 | );
312 | t.end();
313 | })
314 |
315 | test('it throws when two M commands has non even points', t => {
316 | t.throws(() => {
317 | getPointsFromPathData('M10 L0');
318 | });
319 | t.end()
320 | });
321 |
322 | test('It ignores whitespace in path data parsing', t => {
323 | let points = getPointsFromPathData('M10 10');
324 | t.same(points, [[10, 10]]);
325 | t.end();
326 | });
327 |
328 | test('It ignore z in path data parsing', t => {
329 | let points = getPointsFromPathData('M10 10z');
330 | t.same(points, [[10, 10]]);
331 | t.end();
332 | });
--------------------------------------------------------------------------------