├── .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('') 52 | parseText('<'); 53 | parseText('/g>'); 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": "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", 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('') 17 | let svg = lastOpenElement; 18 | t.equal(lastOpenElement.tagName, 'svg'); 19 | t.equal(lastOpenElement.attributes.get('viewBox'), '0 0 5.1e5 762000'); 20 | parseText('') 22 | t.equal(lastOpenElement.attributes.get('id'), 'borders'); 23 | 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 + ''); 44 | } 45 | ); 46 | parseText(''); 47 | parseText('') 48 | parseText('<'); 49 | parseText('/g>'); 50 | 51 | // we got indents now: 52 | t.equal(buffer.join('\n'), 53 | ` 54 | 55 | 56 | `) 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('') 74 | parseText(``); 75 | 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('') 111 | parseText(`Hello world`); 112 | 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('') 144 | parseText(``); 145 | 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('') 246 | parseText(testCases.map(testCase => { 247 | if (testCase.fill) { 248 | return ``; 249 | } 250 | return ``; 251 | }).join('\n')); 252 | 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 | '', 267 | '', 268 | ''].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 | }); --------------------------------------------------------------------------------