├── .eslintrc.json ├── .github └── dependabot.yml ├── .gitignore ├── LICENSE ├── bin └── cli.js ├── browsers ├── DOMasSAX.js └── domsaxtest.html ├── lib ├── WritableStream.js ├── element.js ├── get-base-url.js ├── getURL.js ├── index.js └── process.js ├── package-lock.json ├── package.json ├── readabilitySAX.js ├── readme.md └── tests ├── benchmark.js ├── cleaneval.js ├── test_output.js ├── test_performance.js └── testpage.html /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "prettier"], 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "rules": { 8 | "eqeqeq": [2, "smart"], 9 | "no-caller": 2, 10 | "dot-notation": 2, 11 | "no-var": 2, 12 | "prefer-const": 2, 13 | "prefer-arrow-callback": [2, { "allowNamedFunctions": true }], 14 | "arrow-body-style": [2, "as-needed"], 15 | "object-shorthand": 2, 16 | "prefer-template": 2, 17 | "one-var": [2, "never"], 18 | "prefer-destructuring": [2, { "object": true }], 19 | "capitalized-comments": 2, 20 | "multiline-comment-style": [2, "starred-block"], 21 | "spaced-comment": 2, 22 | "yoda": [2, "never"], 23 | "curly": [2, "multi-line"], 24 | "no-else-return": 2 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | versioning-strategy: increase 9 | - package-ecosystem: "github-actions" 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Felix Böhm 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | THIS IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS, 11 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 12 | -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | if (process.argv.length < 3 || !/^https?:\/\//.test(process.argv[2])) { 4 | console.log("Usage: readability http://domain.tld/sub [format]"); 5 | return; 6 | } 7 | 8 | require("./getURL.js")( 9 | process.argv[2], 10 | process.argv[3] === "html" ? "html" : "text", 11 | (result) => { 12 | if (result.error) return console.log("ERROR:", result.text); 13 | 14 | // Else 15 | console.log("TITLE:", result.title); 16 | console.log("SCORE:", result.score); 17 | if (result.nextPage) console.log("NEXT PAGE:", result.nextPage); 18 | console.log("LENGTH:", result.textLength); 19 | console.log(""); 20 | 21 | let text; 22 | if ("text" in result) { 23 | text = require("entities").decodeHTML5(result.text); 24 | } else { 25 | text = result.html.replace(/\s+/g, " "); 26 | } 27 | process.stdout.write(`${text}\n`); 28 | 29 | process.exit(); 30 | } 31 | ); 32 | -------------------------------------------------------------------------------- /browsers/DOMasSAX.js: -------------------------------------------------------------------------------- 1 | /* 2 | * DOM port of E4XasSAX 3 | * Use the document root to initialise it 4 | */ 5 | 6 | function saxParser(elem, callbacks) { 7 | if (typeof callbacks !== "object") throw "please provide callbacks!"; 8 | 9 | // TODO: Support additional events, options for trim & space normalisation 10 | 11 | function parse(node) { 12 | const name = node.tagName.toLowerCase(); 13 | const attributeNodes = node.attributes; 14 | 15 | callbacks.onopentagname(name); 16 | 17 | for (let i = 0; i < attributeNodes.length; i++) { 18 | callbacks.onattribute( 19 | `${attributeNodes[i].name}`, 20 | attributeNodes[i].value 21 | ); 22 | } 23 | 24 | const childs = node.childNodes; 25 | 26 | for (let i = 0; i < childs.length; i++) { 27 | const { nodeType } = childs[i]; 28 | if (nodeType === 3 /* Text*/) { 29 | callbacks.ontext(childs[i].textContent); 30 | } else if (nodeType === 1 /* Element*/) parse(childs[i]); 31 | /* 32 | *Else if(nodeType === 8) //comment 33 | *if(callbacks.oncomment) callbacks.oncomment(childs[i].toString()); 34 | *[...] 35 | */ 36 | } 37 | callbacks.onclosetag(name); 38 | } 39 | 40 | parse(elem); 41 | } 42 | 43 | module.exports = saxParser; 44 | -------------------------------------------------------------------------------- /browsers/domsaxtest.html: -------------------------------------------------------------------------------- 1 | 2 | DOMasSAX test 3 | 4 |
5 |
6 |

Just a random test for the good of some nation!

7 |

asd, asdf asdf asdf, asdf asdf ,asdf asdf asdf aasdfasdfasdfasdfasdfadsfasdfsdafasdfsdf, asdf asdf, asdfasdfasdfasdfasdfasdf

8 | 9 |

asdfas, asdf asdf, asdf adsf

10 |
11 | 12 | 13 |

This is more text, so make it readable!

14 |

15 | Boom! 16 |

17 | next 18 |
19 | 20 | 21 | 22 | 32 | -------------------------------------------------------------------------------- /lib/WritableStream.js: -------------------------------------------------------------------------------- 1 | const Readability = require("../readabilitySAX"); 2 | const { Parser, CollectingHandler } = require("htmlparser2"); 3 | const { Writable } = require("readable-stream"); 4 | const parserOptions = { 5 | lowerCaseTags: true, 6 | }; 7 | 8 | module.exports = class WritableStream extends Writable { 9 | constructor(settings, callback) { 10 | super(); 11 | 12 | if (typeof settings === "function") { 13 | callback = settings; 14 | settings = null; 15 | } 16 | this._cb = callback; 17 | 18 | this._readability = new Readability(settings); 19 | this._handler = new CollectingHandler(this._readability); 20 | this._parser = new Parser(this._handler, parserOptions); 21 | } 22 | 23 | _write(chunk, encoding, cb) { 24 | this._parser.write(chunk); 25 | cb(); 26 | } 27 | 28 | end(chunk) { 29 | this._parser.end(chunk); 30 | super.end(); 31 | 32 | for ( 33 | let skipLevel = 1; 34 | this._readability._getCandidateNode().info.textLength < 250 && 35 | skipLevel < 4; 36 | skipLevel++ 37 | ) { 38 | this._readability.setSkipLevel(skipLevel); 39 | this._handler.restart(); 40 | } 41 | 42 | if (this._cb) { 43 | this._cb(this._readability.getArticle()); 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /lib/element.js: -------------------------------------------------------------------------------- 1 | const re_commas = /,[\s,]*/g; 2 | const re_whitespace = /\s+/g; 3 | 4 | const headerTags = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]); 5 | const newLinesAfter = new Set([...headerTags, "br", "li", "p"]); 6 | 7 | const tagScores = new Map([ 8 | ["address", -3], 9 | ["article", 30], 10 | ["blockquote", 3], 11 | ["body", -5], 12 | ["dd", -3], 13 | ["div", 5], 14 | ["dl", -3], 15 | ["dt", -3], 16 | ["form", -3], 17 | ["h2", -5], 18 | ["h3", -5], 19 | ["h4", -5], 20 | ["h5", -5], 21 | ["h6", -5], 22 | ["li", -3], 23 | ["ol", -3], 24 | ["pre", 3], 25 | ["section", 15], 26 | ["td", 3], 27 | ["th", -5], 28 | ["ul", -3], 29 | ]); 30 | 31 | /** 32 | * `Element` is a light-weight class that is used 33 | * instead of the DOM (and provides some DOM-like functionality) 34 | */ 35 | class Element { 36 | constructor(tagName, parent) { 37 | this.name = tagName; 38 | this.parent = parent; 39 | this.attributes = {}; 40 | this.children = []; 41 | this.tagScore = 0; 42 | this.attributeScore = 0; 43 | this.totalScore = 0; 44 | this.elementData = ""; 45 | this.info = { 46 | textLength: 0, 47 | linkLength: 0, 48 | commas: 0, 49 | density: 0, 50 | tagCount: new Map(), 51 | }; 52 | this.isCandidate = false; 53 | } 54 | 55 | addInfo() { 56 | const { info } = this; 57 | const childs = this.children; 58 | let elem; 59 | for (let i = 0; i < childs.length; i++) { 60 | elem = childs[i]; 61 | if (typeof elem === "string") { 62 | info.textLength += 63 | elem.trim()./* `replace(re_whitespace, " ").` */ length; 64 | if (re_commas.test(elem)) { 65 | info.commas += elem.split(re_commas).length - 1; 66 | } 67 | } else { 68 | if (elem.name === "a") { 69 | info.linkLength += 70 | elem.info.textLength + elem.info.linkLength; 71 | } else { 72 | info.textLength += elem.info.textLength; 73 | info.linkLength += elem.info.linkLength; 74 | } 75 | info.commas += elem.info.commas; 76 | 77 | for (const [tag, count] of elem.info.tagCount) { 78 | const infoCount = info.tagCount.get(tag) || 0; 79 | info.tagCount.set(tag, infoCount + count); 80 | } 81 | 82 | const infoCount = info.tagCount.get(elem.name) || 0; 83 | info.tagCount.set(elem.name, infoCount + 1); 84 | } 85 | } 86 | 87 | if (info.linkLength !== 0) { 88 | info.density = 89 | info.linkLength / (info.textLength + info.linkLength); 90 | } 91 | } 92 | 93 | getOuterHTML() { 94 | let ret = `<${this.name}`; 95 | 96 | for (const i in this.attributes) { 97 | ret += ` ${i}="${this.attributes[i]}"`; 98 | } 99 | 100 | if (this.children.length === 0) { 101 | if (formatTags.has(this.name)) return `${ret}/>`; 102 | return `${ret}>`; 103 | } 104 | 105 | return `${ret}>${this.getInnerHTML()}`; 106 | } 107 | 108 | getInnerHTML() { 109 | return this.children 110 | .map((child) => 111 | typeof child === "string" ? child : child.getOuterHTML() 112 | ) 113 | .join(""); 114 | } 115 | 116 | getFormattedText() { 117 | return this.children 118 | .map((child) => 119 | typeof child === "string" 120 | ? child.replace(re_whitespace, " ") 121 | : child.getFormattedText() + 122 | (newLinesAfter.has(child.name) ? "\n" : "") 123 | ) 124 | .join(""); 125 | } 126 | 127 | toString() { 128 | return this.children.join(""); 129 | } 130 | 131 | getTopCandidate() { 132 | let topScore = -Infinity; 133 | let score = 0; 134 | let topCandidate = null; 135 | 136 | for (let i = 0; i < this.children.length; i++) { 137 | const child = this.children[i]; 138 | 139 | if (typeof child === "string") continue; 140 | if (child.isCandidate) { 141 | // Add points for the tags name 142 | child.tagScore += tagScores.get(child.name) || 0; 143 | 144 | score = Math.floor( 145 | (child.tagScore + child.attributeScore) * 146 | (1 - child.info.density) 147 | ); 148 | if (topScore < score) { 149 | child.totalScore = topScore = score; 150 | topCandidate = child; 151 | } 152 | } 153 | 154 | const childCandidate = child.getTopCandidate(); 155 | if (childCandidate && topScore < childCandidate.totalScore) { 156 | topScore = childCandidate.totalScore; 157 | topCandidate = childCandidate; 158 | } 159 | } 160 | 161 | return topCandidate; 162 | } 163 | } 164 | 165 | const formatTags = new Map([ 166 | ["br", new Element("br")], 167 | ["hr", new Element("hr")], 168 | ]); 169 | 170 | module.exports = { 171 | Element, 172 | headerTags, 173 | formatTags, 174 | re_whitespace, 175 | }; 176 | -------------------------------------------------------------------------------- /lib/get-base-url.js: -------------------------------------------------------------------------------- 1 | const re_pageInURL = /[_-]?p[a-zA-Z]*[_-]?\d{1,2}$/; 2 | const re_badFirst = /^(?:[^a-z]{0,3}|index|\d+)$/i; 3 | const re_noLetters = /[^a-zA-Z]/; 4 | const re_params = /\?.*/; 5 | const re_extension = /00,|\.[a-zA-Z]+$/g; 6 | const re_justDigits = /^\d{1,2}$/; 7 | 8 | function getBaseURL(url) { 9 | if (url.path.length === 0) { 10 | // Return what we got 11 | return url.full.replace(re_params, ""); 12 | } 13 | 14 | let cleaned = ""; 15 | const elementNum = url.path.length - 1; 16 | 17 | for (let i = 0; i < elementNum; i++) { 18 | // Split off and save anything that looks like a file type and "00,"-trash. 19 | cleaned += `/${url.path[i].replace(re_extension, "")}`; 20 | } 21 | 22 | const first = url.full.replace(re_params, "").replace(/.*\//, ""); 23 | const second = url.path[elementNum]; 24 | 25 | if ( 26 | !(second.length < 3 && re_noLetters.test(first)) && 27 | !re_justDigits.test(second) 28 | ) { 29 | cleaned += `/${ 30 | re_pageInURL.test(second) 31 | ? second.replace(re_pageInURL, "") 32 | : second 33 | }`; 34 | } 35 | 36 | if (!re_badFirst.test(first)) { 37 | cleaned += `/${ 38 | re_pageInURL.test(first) ? first.replace(re_pageInURL, "") : first 39 | }`; 40 | } 41 | 42 | // This is our final, cleaned, base article URL. 43 | return `${url.protocol}//${url.domain}${cleaned}`; 44 | } 45 | 46 | module.exports = { 47 | getBaseURL, 48 | }; 49 | -------------------------------------------------------------------------------- /lib/getURL.js: -------------------------------------------------------------------------------- 1 | const WritableStream = require("./WritableStream.js"); 2 | const minreq = require("minreq"); 3 | 4 | module.exports = function (uri, settings, cb) { 5 | if (typeof settings === "function") { 6 | cb = settings; 7 | settings = {}; 8 | } else if (typeof settings === "string") { 9 | settings = { type: settings }; 10 | } 11 | 12 | let calledCB = false; 13 | function onErr(err) { 14 | if (calledCB) return; 15 | calledCB = true; 16 | 17 | err = err.toString(); 18 | cb({ 19 | title: "Error", 20 | text: err, 21 | html: `${err}`, 22 | error: true, 23 | }); 24 | } 25 | 26 | const req = minreq({ 27 | uri, 28 | only2xx: true, 29 | headers: { 30 | "user-agent": 31 | "Mozilla/5.0 (compatible; readabilitySAX/1.5; +https://github.com/fb55/readabilitySAX)", 32 | }, 33 | }) 34 | .on("error", onErr) 35 | .on("response", (resp) => { 36 | if ( 37 | "content-type" in resp.headers && 38 | resp.headers["content-type"].substr(0, 5) !== "text/" 39 | ) { 40 | // TODO we're actually only interested in text/html, but text/plain shouldn't result in an error (as it will be forwarded) 41 | onErr("document isn't text"); 42 | return; 43 | } 44 | settings.pageURL = req.response.location; 45 | 46 | const stream = new WritableStream(settings, (article) => { 47 | if (calledCB) return console.log("got article with called cb"); 48 | article.link = req.response.location; 49 | cb(article); 50 | }); 51 | 52 | req.pipe(stream); 53 | }); 54 | }; 55 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | Readability: require("../readabilitySAX"), 3 | get: require("./getURL"), 4 | process: require("./process"), 5 | WritableStream: require("./WritableStream"), 6 | createWritableStream(settings, cb) { 7 | return new module.exports.WritableStream(settings, cb); 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /lib/process.js: -------------------------------------------------------------------------------- 1 | const Readability = require("../readabilitySAX"); 2 | const { Parser } = require("htmlparser2"); 3 | 4 | module.exports = function (data, settings, skipLevel) { 5 | if (!skipLevel) skipLevel = 0; 6 | 7 | const readable = new Readability(settings); 8 | const parser = new Parser(readable); 9 | let article; 10 | 11 | do { 12 | if (skipLevel !== 0) readable.setSkipLevel(skipLevel); 13 | 14 | parser.parseComplete(data); 15 | 16 | article = readable.getArticle(); 17 | skipLevel += 1; 18 | } while (article.textLength < 250 && skipLevel < 4); 19 | 20 | return article; 21 | }; 22 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "readabilitySAX", 3 | "version": "1.6.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "readabilitySAX", 9 | "version": "1.6.1", 10 | "license": "BSD-like", 11 | "dependencies": { 12 | "entities": "^4.5.0", 13 | "htmlparser2": "^9.0.0", 14 | "minreq": "^0.2.3", 15 | "readable-stream": "^4.4.0" 16 | }, 17 | "bin": { 18 | "readability": "bin/cli.js" 19 | }, 20 | "devDependencies": { 21 | "coveralls": "^3.1.1", 22 | "eslint": "^8.46.0", 23 | "eslint-config-prettier": "^9.0.0", 24 | "prettier": "^2.8.3" 25 | } 26 | }, 27 | "node_modules/@aashutoshrathi/word-wrap": { 28 | "version": "1.2.6", 29 | "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", 30 | "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", 31 | "dev": true, 32 | "engines": { 33 | "node": ">=0.10.0" 34 | } 35 | }, 36 | "node_modules/@eslint-community/eslint-utils": { 37 | "version": "4.4.0", 38 | "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", 39 | "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", 40 | "dev": true, 41 | "dependencies": { 42 | "eslint-visitor-keys": "^3.3.0" 43 | }, 44 | "engines": { 45 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 46 | }, 47 | "peerDependencies": { 48 | "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 49 | } 50 | }, 51 | "node_modules/@eslint-community/regexpp": { 52 | "version": "4.6.2", 53 | "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", 54 | "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", 55 | "dev": true, 56 | "engines": { 57 | "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 58 | } 59 | }, 60 | "node_modules/@eslint/eslintrc": { 61 | "version": "2.1.1", 62 | "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.1.tgz", 63 | "integrity": "sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==", 64 | "dev": true, 65 | "dependencies": { 66 | "ajv": "^6.12.4", 67 | "debug": "^4.3.2", 68 | "espree": "^9.6.0", 69 | "globals": "^13.19.0", 70 | "ignore": "^5.2.0", 71 | "import-fresh": "^3.2.1", 72 | "js-yaml": "^4.1.0", 73 | "minimatch": "^3.1.2", 74 | "strip-json-comments": "^3.1.1" 75 | }, 76 | "engines": { 77 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 78 | }, 79 | "funding": { 80 | "url": "https://opencollective.com/eslint" 81 | } 82 | }, 83 | "node_modules/@eslint/eslintrc/node_modules/argparse": { 84 | "version": "2.0.1", 85 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 86 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 87 | "dev": true 88 | }, 89 | "node_modules/@eslint/eslintrc/node_modules/js-yaml": { 90 | "version": "4.1.0", 91 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 92 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 93 | "dev": true, 94 | "dependencies": { 95 | "argparse": "^2.0.1" 96 | }, 97 | "bin": { 98 | "js-yaml": "bin/js-yaml.js" 99 | } 100 | }, 101 | "node_modules/@eslint/js": { 102 | "version": "8.46.0", 103 | "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.46.0.tgz", 104 | "integrity": "sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==", 105 | "dev": true, 106 | "engines": { 107 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 108 | } 109 | }, 110 | "node_modules/@humanwhocodes/config-array": { 111 | "version": "0.11.10", 112 | "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", 113 | "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", 114 | "dev": true, 115 | "dependencies": { 116 | "@humanwhocodes/object-schema": "^1.2.1", 117 | "debug": "^4.1.1", 118 | "minimatch": "^3.0.5" 119 | }, 120 | "engines": { 121 | "node": ">=10.10.0" 122 | } 123 | }, 124 | "node_modules/@humanwhocodes/module-importer": { 125 | "version": "1.0.1", 126 | "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 127 | "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 128 | "dev": true, 129 | "engines": { 130 | "node": ">=12.22" 131 | }, 132 | "funding": { 133 | "type": "github", 134 | "url": "https://github.com/sponsors/nzakas" 135 | } 136 | }, 137 | "node_modules/@humanwhocodes/object-schema": { 138 | "version": "1.2.1", 139 | "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", 140 | "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", 141 | "dev": true 142 | }, 143 | "node_modules/@nodelib/fs.scandir": { 144 | "version": "2.1.5", 145 | "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", 146 | "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", 147 | "dev": true, 148 | "dependencies": { 149 | "@nodelib/fs.stat": "2.0.5", 150 | "run-parallel": "^1.1.9" 151 | }, 152 | "engines": { 153 | "node": ">= 8" 154 | } 155 | }, 156 | "node_modules/@nodelib/fs.stat": { 157 | "version": "2.0.5", 158 | "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", 159 | "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", 160 | "dev": true, 161 | "engines": { 162 | "node": ">= 8" 163 | } 164 | }, 165 | "node_modules/@nodelib/fs.walk": { 166 | "version": "1.2.8", 167 | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", 168 | "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", 169 | "dev": true, 170 | "dependencies": { 171 | "@nodelib/fs.scandir": "2.1.5", 172 | "fastq": "^1.6.0" 173 | }, 174 | "engines": { 175 | "node": ">= 8" 176 | } 177 | }, 178 | "node_modules/abort-controller": { 179 | "version": "3.0.0", 180 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", 181 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", 182 | "dependencies": { 183 | "event-target-shim": "^5.0.0" 184 | }, 185 | "engines": { 186 | "node": ">=6.5" 187 | } 188 | }, 189 | "node_modules/acorn": { 190 | "version": "8.10.0", 191 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", 192 | "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", 193 | "dev": true, 194 | "bin": { 195 | "acorn": "bin/acorn" 196 | }, 197 | "engines": { 198 | "node": ">=0.4.0" 199 | } 200 | }, 201 | "node_modules/acorn-jsx": { 202 | "version": "5.3.2", 203 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 204 | "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 205 | "dev": true, 206 | "peerDependencies": { 207 | "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 208 | } 209 | }, 210 | "node_modules/ajv": { 211 | "version": "6.12.6", 212 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 213 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 214 | "dev": true, 215 | "dependencies": { 216 | "fast-deep-equal": "^3.1.1", 217 | "fast-json-stable-stringify": "^2.0.0", 218 | "json-schema-traverse": "^0.4.1", 219 | "uri-js": "^4.2.2" 220 | }, 221 | "funding": { 222 | "type": "github", 223 | "url": "https://github.com/sponsors/epoberezkin" 224 | } 225 | }, 226 | "node_modules/ansi-regex": { 227 | "version": "5.0.1", 228 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 229 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 230 | "dev": true, 231 | "engines": { 232 | "node": ">=8" 233 | } 234 | }, 235 | "node_modules/ansi-styles": { 236 | "version": "4.3.0", 237 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 238 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 239 | "dev": true, 240 | "dependencies": { 241 | "color-convert": "^2.0.1" 242 | }, 243 | "engines": { 244 | "node": ">=8" 245 | }, 246 | "funding": { 247 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 248 | } 249 | }, 250 | "node_modules/argparse": { 251 | "version": "1.0.10", 252 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 253 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 254 | "dev": true, 255 | "dependencies": { 256 | "sprintf-js": "~1.0.2" 257 | } 258 | }, 259 | "node_modules/asn1": { 260 | "version": "0.2.4", 261 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 262 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 263 | "dev": true, 264 | "dependencies": { 265 | "safer-buffer": "~2.1.0" 266 | } 267 | }, 268 | "node_modules/assert-plus": { 269 | "version": "1.0.0", 270 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 271 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", 272 | "dev": true, 273 | "engines": { 274 | "node": ">=0.8" 275 | } 276 | }, 277 | "node_modules/asynckit": { 278 | "version": "0.4.0", 279 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 280 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", 281 | "dev": true 282 | }, 283 | "node_modules/aws-sign2": { 284 | "version": "0.7.0", 285 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 286 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", 287 | "dev": true, 288 | "engines": { 289 | "node": "*" 290 | } 291 | }, 292 | "node_modules/aws4": { 293 | "version": "1.11.0", 294 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", 295 | "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", 296 | "dev": true 297 | }, 298 | "node_modules/balanced-match": { 299 | "version": "1.0.2", 300 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 301 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 302 | "dev": true 303 | }, 304 | "node_modules/base64-js": { 305 | "version": "1.5.1", 306 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 307 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", 308 | "funding": [ 309 | { 310 | "type": "github", 311 | "url": "https://github.com/sponsors/feross" 312 | }, 313 | { 314 | "type": "patreon", 315 | "url": "https://www.patreon.com/feross" 316 | }, 317 | { 318 | "type": "consulting", 319 | "url": "https://feross.org/support" 320 | } 321 | ] 322 | }, 323 | "node_modules/bcrypt-pbkdf": { 324 | "version": "1.0.2", 325 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 326 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 327 | "dev": true, 328 | "dependencies": { 329 | "tweetnacl": "^0.14.3" 330 | } 331 | }, 332 | "node_modules/brace-expansion": { 333 | "version": "1.1.11", 334 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 335 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 336 | "dev": true, 337 | "dependencies": { 338 | "balanced-match": "^1.0.0", 339 | "concat-map": "0.0.1" 340 | } 341 | }, 342 | "node_modules/buffer": { 343 | "version": "6.0.3", 344 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", 345 | "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", 346 | "funding": [ 347 | { 348 | "type": "github", 349 | "url": "https://github.com/sponsors/feross" 350 | }, 351 | { 352 | "type": "patreon", 353 | "url": "https://www.patreon.com/feross" 354 | }, 355 | { 356 | "type": "consulting", 357 | "url": "https://feross.org/support" 358 | } 359 | ], 360 | "dependencies": { 361 | "base64-js": "^1.3.1", 362 | "ieee754": "^1.2.1" 363 | } 364 | }, 365 | "node_modules/callsites": { 366 | "version": "3.1.0", 367 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 368 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 369 | "dev": true, 370 | "engines": { 371 | "node": ">=6" 372 | } 373 | }, 374 | "node_modules/caseless": { 375 | "version": "0.12.0", 376 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 377 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", 378 | "dev": true 379 | }, 380 | "node_modules/chalk": { 381 | "version": "4.1.1", 382 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", 383 | "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", 384 | "dev": true, 385 | "dependencies": { 386 | "ansi-styles": "^4.1.0", 387 | "supports-color": "^7.1.0" 388 | }, 389 | "engines": { 390 | "node": ">=10" 391 | }, 392 | "funding": { 393 | "url": "https://github.com/chalk/chalk?sponsor=1" 394 | } 395 | }, 396 | "node_modules/color-convert": { 397 | "version": "2.0.1", 398 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 399 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 400 | "dev": true, 401 | "dependencies": { 402 | "color-name": "~1.1.4" 403 | }, 404 | "engines": { 405 | "node": ">=7.0.0" 406 | } 407 | }, 408 | "node_modules/color-name": { 409 | "version": "1.1.4", 410 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 411 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 412 | "dev": true 413 | }, 414 | "node_modules/combined-stream": { 415 | "version": "1.0.8", 416 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 417 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 418 | "dev": true, 419 | "dependencies": { 420 | "delayed-stream": "~1.0.0" 421 | }, 422 | "engines": { 423 | "node": ">= 0.8" 424 | } 425 | }, 426 | "node_modules/concat-map": { 427 | "version": "0.0.1", 428 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 429 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 430 | "dev": true 431 | }, 432 | "node_modules/core-util-is": { 433 | "version": "1.0.2", 434 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 435 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 436 | "dev": true 437 | }, 438 | "node_modules/coveralls": { 439 | "version": "3.1.1", 440 | "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.1.1.tgz", 441 | "integrity": "sha512-+dxnG2NHncSD1NrqbSM3dn/lE57O6Qf/koe9+I7c+wzkqRmEvcp0kgJdxKInzYzkICKkFMZsX3Vct3++tsF9ww==", 442 | "dev": true, 443 | "dependencies": { 444 | "js-yaml": "^3.13.1", 445 | "lcov-parse": "^1.0.0", 446 | "log-driver": "^1.2.7", 447 | "minimist": "^1.2.5", 448 | "request": "^2.88.2" 449 | }, 450 | "bin": { 451 | "coveralls": "bin/coveralls.js" 452 | }, 453 | "engines": { 454 | "node": ">=6" 455 | } 456 | }, 457 | "node_modules/cross-spawn": { 458 | "version": "7.0.3", 459 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 460 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 461 | "dev": true, 462 | "dependencies": { 463 | "path-key": "^3.1.0", 464 | "shebang-command": "^2.0.0", 465 | "which": "^2.0.1" 466 | }, 467 | "engines": { 468 | "node": ">= 8" 469 | } 470 | }, 471 | "node_modules/dashdash": { 472 | "version": "1.14.1", 473 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 474 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 475 | "dev": true, 476 | "dependencies": { 477 | "assert-plus": "^1.0.0" 478 | }, 479 | "engines": { 480 | "node": ">=0.10" 481 | } 482 | }, 483 | "node_modules/debug": { 484 | "version": "4.3.4", 485 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", 486 | "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", 487 | "dev": true, 488 | "dependencies": { 489 | "ms": "2.1.2" 490 | }, 491 | "engines": { 492 | "node": ">=6.0" 493 | }, 494 | "peerDependenciesMeta": { 495 | "supports-color": { 496 | "optional": true 497 | } 498 | } 499 | }, 500 | "node_modules/deep-is": { 501 | "version": "0.1.4", 502 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 503 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 504 | "dev": true 505 | }, 506 | "node_modules/delayed-stream": { 507 | "version": "1.0.0", 508 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 509 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", 510 | "dev": true, 511 | "engines": { 512 | "node": ">=0.4.0" 513 | } 514 | }, 515 | "node_modules/doctrine": { 516 | "version": "3.0.0", 517 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", 518 | "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", 519 | "dev": true, 520 | "dependencies": { 521 | "esutils": "^2.0.2" 522 | }, 523 | "engines": { 524 | "node": ">=6.0.0" 525 | } 526 | }, 527 | "node_modules/dom-serializer": { 528 | "version": "2.0.0", 529 | "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", 530 | "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", 531 | "dependencies": { 532 | "domelementtype": "^2.3.0", 533 | "domhandler": "^5.0.2", 534 | "entities": "^4.2.0" 535 | }, 536 | "funding": { 537 | "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" 538 | } 539 | }, 540 | "node_modules/domelementtype": { 541 | "version": "2.3.0", 542 | "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", 543 | "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", 544 | "funding": [ 545 | { 546 | "type": "github", 547 | "url": "https://github.com/sponsors/fb55" 548 | } 549 | ] 550 | }, 551 | "node_modules/domhandler": { 552 | "version": "5.0.3", 553 | "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", 554 | "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", 555 | "dependencies": { 556 | "domelementtype": "^2.3.0" 557 | }, 558 | "engines": { 559 | "node": ">= 4" 560 | }, 561 | "funding": { 562 | "url": "https://github.com/fb55/domhandler?sponsor=1" 563 | } 564 | }, 565 | "node_modules/domutils": { 566 | "version": "3.1.0", 567 | "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", 568 | "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", 569 | "dependencies": { 570 | "dom-serializer": "^2.0.0", 571 | "domelementtype": "^2.3.0", 572 | "domhandler": "^5.0.3" 573 | }, 574 | "funding": { 575 | "url": "https://github.com/fb55/domutils?sponsor=1" 576 | } 577 | }, 578 | "node_modules/ecc-jsbn": { 579 | "version": "0.1.2", 580 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 581 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 582 | "dev": true, 583 | "dependencies": { 584 | "jsbn": "~0.1.0", 585 | "safer-buffer": "^2.1.0" 586 | } 587 | }, 588 | "node_modules/entities": { 589 | "version": "4.5.0", 590 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 591 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 592 | "engines": { 593 | "node": ">=0.12" 594 | }, 595 | "funding": { 596 | "url": "https://github.com/fb55/entities?sponsor=1" 597 | } 598 | }, 599 | "node_modules/escape-string-regexp": { 600 | "version": "4.0.0", 601 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 602 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 603 | "dev": true, 604 | "engines": { 605 | "node": ">=10" 606 | }, 607 | "funding": { 608 | "url": "https://github.com/sponsors/sindresorhus" 609 | } 610 | }, 611 | "node_modules/eslint": { 612 | "version": "8.46.0", 613 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.46.0.tgz", 614 | "integrity": "sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==", 615 | "dev": true, 616 | "dependencies": { 617 | "@eslint-community/eslint-utils": "^4.2.0", 618 | "@eslint-community/regexpp": "^4.6.1", 619 | "@eslint/eslintrc": "^2.1.1", 620 | "@eslint/js": "^8.46.0", 621 | "@humanwhocodes/config-array": "^0.11.10", 622 | "@humanwhocodes/module-importer": "^1.0.1", 623 | "@nodelib/fs.walk": "^1.2.8", 624 | "ajv": "^6.12.4", 625 | "chalk": "^4.0.0", 626 | "cross-spawn": "^7.0.2", 627 | "debug": "^4.3.2", 628 | "doctrine": "^3.0.0", 629 | "escape-string-regexp": "^4.0.0", 630 | "eslint-scope": "^7.2.2", 631 | "eslint-visitor-keys": "^3.4.2", 632 | "espree": "^9.6.1", 633 | "esquery": "^1.4.2", 634 | "esutils": "^2.0.2", 635 | "fast-deep-equal": "^3.1.3", 636 | "file-entry-cache": "^6.0.1", 637 | "find-up": "^5.0.0", 638 | "glob-parent": "^6.0.2", 639 | "globals": "^13.19.0", 640 | "graphemer": "^1.4.0", 641 | "ignore": "^5.2.0", 642 | "imurmurhash": "^0.1.4", 643 | "is-glob": "^4.0.0", 644 | "is-path-inside": "^3.0.3", 645 | "js-yaml": "^4.1.0", 646 | "json-stable-stringify-without-jsonify": "^1.0.1", 647 | "levn": "^0.4.1", 648 | "lodash.merge": "^4.6.2", 649 | "minimatch": "^3.1.2", 650 | "natural-compare": "^1.4.0", 651 | "optionator": "^0.9.3", 652 | "strip-ansi": "^6.0.1", 653 | "text-table": "^0.2.0" 654 | }, 655 | "bin": { 656 | "eslint": "bin/eslint.js" 657 | }, 658 | "engines": { 659 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 660 | }, 661 | "funding": { 662 | "url": "https://opencollective.com/eslint" 663 | } 664 | }, 665 | "node_modules/eslint-config-prettier": { 666 | "version": "9.0.0", 667 | "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", 668 | "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", 669 | "dev": true, 670 | "bin": { 671 | "eslint-config-prettier": "bin/cli.js" 672 | }, 673 | "peerDependencies": { 674 | "eslint": ">=7.0.0" 675 | } 676 | }, 677 | "node_modules/eslint-scope": { 678 | "version": "7.2.2", 679 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", 680 | "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", 681 | "dev": true, 682 | "dependencies": { 683 | "esrecurse": "^4.3.0", 684 | "estraverse": "^5.2.0" 685 | }, 686 | "engines": { 687 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 688 | }, 689 | "funding": { 690 | "url": "https://opencollective.com/eslint" 691 | } 692 | }, 693 | "node_modules/eslint-visitor-keys": { 694 | "version": "3.4.2", 695 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz", 696 | "integrity": "sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==", 697 | "dev": true, 698 | "engines": { 699 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 700 | }, 701 | "funding": { 702 | "url": "https://opencollective.com/eslint" 703 | } 704 | }, 705 | "node_modules/eslint/node_modules/argparse": { 706 | "version": "2.0.1", 707 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 708 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 709 | "dev": true 710 | }, 711 | "node_modules/eslint/node_modules/js-yaml": { 712 | "version": "4.1.0", 713 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 714 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 715 | "dev": true, 716 | "dependencies": { 717 | "argparse": "^2.0.1" 718 | }, 719 | "bin": { 720 | "js-yaml": "bin/js-yaml.js" 721 | } 722 | }, 723 | "node_modules/espree": { 724 | "version": "9.6.1", 725 | "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", 726 | "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", 727 | "dev": true, 728 | "dependencies": { 729 | "acorn": "^8.9.0", 730 | "acorn-jsx": "^5.3.2", 731 | "eslint-visitor-keys": "^3.4.1" 732 | }, 733 | "engines": { 734 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 735 | }, 736 | "funding": { 737 | "url": "https://opencollective.com/eslint" 738 | } 739 | }, 740 | "node_modules/esprima": { 741 | "version": "4.0.1", 742 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 743 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 744 | "dev": true, 745 | "bin": { 746 | "esparse": "bin/esparse.js", 747 | "esvalidate": "bin/esvalidate.js" 748 | }, 749 | "engines": { 750 | "node": ">=4" 751 | } 752 | }, 753 | "node_modules/esquery": { 754 | "version": "1.5.0", 755 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", 756 | "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", 757 | "dev": true, 758 | "dependencies": { 759 | "estraverse": "^5.1.0" 760 | }, 761 | "engines": { 762 | "node": ">=0.10" 763 | } 764 | }, 765 | "node_modules/esrecurse": { 766 | "version": "4.3.0", 767 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 768 | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 769 | "dev": true, 770 | "dependencies": { 771 | "estraverse": "^5.2.0" 772 | }, 773 | "engines": { 774 | "node": ">=4.0" 775 | } 776 | }, 777 | "node_modules/estraverse": { 778 | "version": "5.3.0", 779 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 780 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 781 | "dev": true, 782 | "engines": { 783 | "node": ">=4.0" 784 | } 785 | }, 786 | "node_modules/esutils": { 787 | "version": "2.0.3", 788 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 789 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 790 | "dev": true, 791 | "engines": { 792 | "node": ">=0.10.0" 793 | } 794 | }, 795 | "node_modules/event-target-shim": { 796 | "version": "5.0.1", 797 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", 798 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", 799 | "engines": { 800 | "node": ">=6" 801 | } 802 | }, 803 | "node_modules/events": { 804 | "version": "3.3.0", 805 | "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", 806 | "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", 807 | "engines": { 808 | "node": ">=0.8.x" 809 | } 810 | }, 811 | "node_modules/extend": { 812 | "version": "3.0.2", 813 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 814 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", 815 | "dev": true 816 | }, 817 | "node_modules/extsprintf": { 818 | "version": "1.3.0", 819 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 820 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", 821 | "dev": true, 822 | "engines": [ 823 | "node >=0.6.0" 824 | ] 825 | }, 826 | "node_modules/fast-deep-equal": { 827 | "version": "3.1.3", 828 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 829 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 830 | "dev": true 831 | }, 832 | "node_modules/fast-json-stable-stringify": { 833 | "version": "2.1.0", 834 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 835 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 836 | "dev": true 837 | }, 838 | "node_modules/fast-levenshtein": { 839 | "version": "2.0.6", 840 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 841 | "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 842 | "dev": true 843 | }, 844 | "node_modules/fastq": { 845 | "version": "1.15.0", 846 | "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", 847 | "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", 848 | "dev": true, 849 | "dependencies": { 850 | "reusify": "^1.0.4" 851 | } 852 | }, 853 | "node_modules/file-entry-cache": { 854 | "version": "6.0.1", 855 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", 856 | "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", 857 | "dev": true, 858 | "dependencies": { 859 | "flat-cache": "^3.0.4" 860 | }, 861 | "engines": { 862 | "node": "^10.12.0 || >=12.0.0" 863 | } 864 | }, 865 | "node_modules/find-up": { 866 | "version": "5.0.0", 867 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 868 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 869 | "dev": true, 870 | "dependencies": { 871 | "locate-path": "^6.0.0", 872 | "path-exists": "^4.0.0" 873 | }, 874 | "engines": { 875 | "node": ">=10" 876 | }, 877 | "funding": { 878 | "url": "https://github.com/sponsors/sindresorhus" 879 | } 880 | }, 881 | "node_modules/flat-cache": { 882 | "version": "3.0.4", 883 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", 884 | "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", 885 | "dev": true, 886 | "dependencies": { 887 | "flatted": "^3.1.0", 888 | "rimraf": "^3.0.2" 889 | }, 890 | "engines": { 891 | "node": "^10.12.0 || >=12.0.0" 892 | } 893 | }, 894 | "node_modules/flatted": { 895 | "version": "3.2.0", 896 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.0.tgz", 897 | "integrity": "sha512-XprP7lDrVT+kE2c2YlfiV+IfS9zxukiIOvNamPNsImNhXadSsQEbosItdL9bUQlCZXR13SvPk20BjWSWLA7m4A==", 898 | "dev": true 899 | }, 900 | "node_modules/forever-agent": { 901 | "version": "0.6.1", 902 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 903 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", 904 | "dev": true, 905 | "engines": { 906 | "node": "*" 907 | } 908 | }, 909 | "node_modules/form-data": { 910 | "version": "2.3.3", 911 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 912 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 913 | "dev": true, 914 | "dependencies": { 915 | "asynckit": "^0.4.0", 916 | "combined-stream": "^1.0.6", 917 | "mime-types": "^2.1.12" 918 | }, 919 | "engines": { 920 | "node": ">= 0.12" 921 | } 922 | }, 923 | "node_modules/fs.realpath": { 924 | "version": "1.0.0", 925 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 926 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 927 | "dev": true 928 | }, 929 | "node_modules/getpass": { 930 | "version": "0.1.7", 931 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 932 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 933 | "dev": true, 934 | "dependencies": { 935 | "assert-plus": "^1.0.0" 936 | } 937 | }, 938 | "node_modules/glob": { 939 | "version": "7.1.7", 940 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", 941 | "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", 942 | "dev": true, 943 | "dependencies": { 944 | "fs.realpath": "^1.0.0", 945 | "inflight": "^1.0.4", 946 | "inherits": "2", 947 | "minimatch": "^3.0.4", 948 | "once": "^1.3.0", 949 | "path-is-absolute": "^1.0.0" 950 | }, 951 | "engines": { 952 | "node": "*" 953 | }, 954 | "funding": { 955 | "url": "https://github.com/sponsors/isaacs" 956 | } 957 | }, 958 | "node_modules/glob-parent": { 959 | "version": "6.0.2", 960 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 961 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 962 | "dev": true, 963 | "dependencies": { 964 | "is-glob": "^4.0.3" 965 | }, 966 | "engines": { 967 | "node": ">=10.13.0" 968 | } 969 | }, 970 | "node_modules/globals": { 971 | "version": "13.20.0", 972 | "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", 973 | "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", 974 | "dev": true, 975 | "dependencies": { 976 | "type-fest": "^0.20.2" 977 | }, 978 | "engines": { 979 | "node": ">=8" 980 | }, 981 | "funding": { 982 | "url": "https://github.com/sponsors/sindresorhus" 983 | } 984 | }, 985 | "node_modules/graphemer": { 986 | "version": "1.4.0", 987 | "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", 988 | "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", 989 | "dev": true 990 | }, 991 | "node_modules/har-schema": { 992 | "version": "2.0.0", 993 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 994 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", 995 | "dev": true, 996 | "engines": { 997 | "node": ">=4" 998 | } 999 | }, 1000 | "node_modules/har-validator": { 1001 | "version": "5.1.5", 1002 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", 1003 | "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", 1004 | "deprecated": "this library is no longer supported", 1005 | "dev": true, 1006 | "dependencies": { 1007 | "ajv": "^6.12.3", 1008 | "har-schema": "^2.0.0" 1009 | }, 1010 | "engines": { 1011 | "node": ">=6" 1012 | } 1013 | }, 1014 | "node_modules/has-flag": { 1015 | "version": "4.0.0", 1016 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 1017 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 1018 | "dev": true, 1019 | "engines": { 1020 | "node": ">=8" 1021 | } 1022 | }, 1023 | "node_modules/htmlparser2": { 1024 | "version": "9.0.0", 1025 | "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.0.0.tgz", 1026 | "integrity": "sha512-uxbSI98wmFT/G4P2zXx4OVx04qWUmyFPrD2/CNepa2Zo3GPNaCaaxElDgwUrwYWkK1nr9fft0Ya8dws8coDLLQ==", 1027 | "funding": [ 1028 | "https://github.com/fb55/htmlparser2?sponsor=1", 1029 | { 1030 | "type": "github", 1031 | "url": "https://github.com/sponsors/fb55" 1032 | } 1033 | ], 1034 | "dependencies": { 1035 | "domelementtype": "^2.3.0", 1036 | "domhandler": "^5.0.3", 1037 | "domutils": "^3.1.0", 1038 | "entities": "^4.5.0" 1039 | } 1040 | }, 1041 | "node_modules/http-signature": { 1042 | "version": "1.2.0", 1043 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 1044 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 1045 | "dev": true, 1046 | "dependencies": { 1047 | "assert-plus": "^1.0.0", 1048 | "jsprim": "^1.2.2", 1049 | "sshpk": "^1.7.0" 1050 | }, 1051 | "engines": { 1052 | "node": ">=0.8", 1053 | "npm": ">=1.3.7" 1054 | } 1055 | }, 1056 | "node_modules/ieee754": { 1057 | "version": "1.2.1", 1058 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 1059 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", 1060 | "funding": [ 1061 | { 1062 | "type": "github", 1063 | "url": "https://github.com/sponsors/feross" 1064 | }, 1065 | { 1066 | "type": "patreon", 1067 | "url": "https://www.patreon.com/feross" 1068 | }, 1069 | { 1070 | "type": "consulting", 1071 | "url": "https://feross.org/support" 1072 | } 1073 | ] 1074 | }, 1075 | "node_modules/ignore": { 1076 | "version": "5.2.4", 1077 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", 1078 | "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", 1079 | "dev": true, 1080 | "engines": { 1081 | "node": ">= 4" 1082 | } 1083 | }, 1084 | "node_modules/import-fresh": { 1085 | "version": "3.3.0", 1086 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", 1087 | "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", 1088 | "dev": true, 1089 | "dependencies": { 1090 | "parent-module": "^1.0.0", 1091 | "resolve-from": "^4.0.0" 1092 | }, 1093 | "engines": { 1094 | "node": ">=6" 1095 | }, 1096 | "funding": { 1097 | "url": "https://github.com/sponsors/sindresorhus" 1098 | } 1099 | }, 1100 | "node_modules/imurmurhash": { 1101 | "version": "0.1.4", 1102 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 1103 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 1104 | "dev": true, 1105 | "engines": { 1106 | "node": ">=0.8.19" 1107 | } 1108 | }, 1109 | "node_modules/inflight": { 1110 | "version": "1.0.6", 1111 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 1112 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 1113 | "dev": true, 1114 | "dependencies": { 1115 | "once": "^1.3.0", 1116 | "wrappy": "1" 1117 | } 1118 | }, 1119 | "node_modules/inherits": { 1120 | "version": "2.0.4", 1121 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 1122 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 1123 | "dev": true 1124 | }, 1125 | "node_modules/is-extglob": { 1126 | "version": "2.1.1", 1127 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1128 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 1129 | "dev": true, 1130 | "engines": { 1131 | "node": ">=0.10.0" 1132 | } 1133 | }, 1134 | "node_modules/is-glob": { 1135 | "version": "4.0.3", 1136 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 1137 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 1138 | "dev": true, 1139 | "dependencies": { 1140 | "is-extglob": "^2.1.1" 1141 | }, 1142 | "engines": { 1143 | "node": ">=0.10.0" 1144 | } 1145 | }, 1146 | "node_modules/is-path-inside": { 1147 | "version": "3.0.3", 1148 | "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", 1149 | "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", 1150 | "dev": true, 1151 | "engines": { 1152 | "node": ">=8" 1153 | } 1154 | }, 1155 | "node_modules/is-typedarray": { 1156 | "version": "1.0.0", 1157 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 1158 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", 1159 | "dev": true 1160 | }, 1161 | "node_modules/isexe": { 1162 | "version": "2.0.0", 1163 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1164 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 1165 | "dev": true 1166 | }, 1167 | "node_modules/isstream": { 1168 | "version": "0.1.2", 1169 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 1170 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", 1171 | "dev": true 1172 | }, 1173 | "node_modules/js-yaml": { 1174 | "version": "3.14.1", 1175 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", 1176 | "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", 1177 | "dev": true, 1178 | "dependencies": { 1179 | "argparse": "^1.0.7", 1180 | "esprima": "^4.0.0" 1181 | }, 1182 | "bin": { 1183 | "js-yaml": "bin/js-yaml.js" 1184 | } 1185 | }, 1186 | "node_modules/jsbn": { 1187 | "version": "0.1.1", 1188 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 1189 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", 1190 | "dev": true 1191 | }, 1192 | "node_modules/json-schema": { 1193 | "version": "0.4.0", 1194 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", 1195 | "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", 1196 | "dev": true 1197 | }, 1198 | "node_modules/json-schema-traverse": { 1199 | "version": "0.4.1", 1200 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 1201 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 1202 | "dev": true 1203 | }, 1204 | "node_modules/json-stable-stringify-without-jsonify": { 1205 | "version": "1.0.1", 1206 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 1207 | "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", 1208 | "dev": true 1209 | }, 1210 | "node_modules/json-stringify-safe": { 1211 | "version": "5.0.1", 1212 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 1213 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", 1214 | "dev": true 1215 | }, 1216 | "node_modules/jsprim": { 1217 | "version": "1.4.2", 1218 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", 1219 | "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", 1220 | "dev": true, 1221 | "dependencies": { 1222 | "assert-plus": "1.0.0", 1223 | "extsprintf": "1.3.0", 1224 | "json-schema": "0.4.0", 1225 | "verror": "1.10.0" 1226 | }, 1227 | "engines": { 1228 | "node": ">=0.6.0" 1229 | } 1230 | }, 1231 | "node_modules/lcov-parse": { 1232 | "version": "1.0.0", 1233 | "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-1.0.0.tgz", 1234 | "integrity": "sha1-6w1GtUER68VhrLTECO+TY73I9+A=", 1235 | "dev": true, 1236 | "bin": { 1237 | "lcov-parse": "bin/cli.js" 1238 | } 1239 | }, 1240 | "node_modules/levn": { 1241 | "version": "0.4.1", 1242 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 1243 | "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 1244 | "dev": true, 1245 | "dependencies": { 1246 | "prelude-ls": "^1.2.1", 1247 | "type-check": "~0.4.0" 1248 | }, 1249 | "engines": { 1250 | "node": ">= 0.8.0" 1251 | } 1252 | }, 1253 | "node_modules/locate-path": { 1254 | "version": "6.0.0", 1255 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 1256 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 1257 | "dev": true, 1258 | "dependencies": { 1259 | "p-locate": "^5.0.0" 1260 | }, 1261 | "engines": { 1262 | "node": ">=10" 1263 | }, 1264 | "funding": { 1265 | "url": "https://github.com/sponsors/sindresorhus" 1266 | } 1267 | }, 1268 | "node_modules/lodash.merge": { 1269 | "version": "4.6.2", 1270 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 1271 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 1272 | "dev": true 1273 | }, 1274 | "node_modules/log-driver": { 1275 | "version": "1.2.7", 1276 | "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", 1277 | "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", 1278 | "dev": true, 1279 | "engines": { 1280 | "node": ">=0.8.6" 1281 | } 1282 | }, 1283 | "node_modules/mime-db": { 1284 | "version": "1.48.0", 1285 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", 1286 | "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==", 1287 | "dev": true, 1288 | "engines": { 1289 | "node": ">= 0.6" 1290 | } 1291 | }, 1292 | "node_modules/mime-types": { 1293 | "version": "2.1.31", 1294 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", 1295 | "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", 1296 | "dev": true, 1297 | "dependencies": { 1298 | "mime-db": "1.48.0" 1299 | }, 1300 | "engines": { 1301 | "node": ">= 0.6" 1302 | } 1303 | }, 1304 | "node_modules/minimatch": { 1305 | "version": "3.1.2", 1306 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 1307 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 1308 | "dev": true, 1309 | "dependencies": { 1310 | "brace-expansion": "^1.1.7" 1311 | }, 1312 | "engines": { 1313 | "node": "*" 1314 | } 1315 | }, 1316 | "node_modules/minimist": { 1317 | "version": "1.2.7", 1318 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", 1319 | "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", 1320 | "dev": true, 1321 | "funding": { 1322 | "url": "https://github.com/sponsors/ljharb" 1323 | } 1324 | }, 1325 | "node_modules/minreq": { 1326 | "version": "0.2.3", 1327 | "resolved": "https://registry.npmjs.org/minreq/-/minreq-0.2.3.tgz", 1328 | "integrity": "sha1-nX/B038czMyrVeQCwS27eMfjqw0=", 1329 | "deprecated": "Deprecated, please refer to a more up-to-date request library!" 1330 | }, 1331 | "node_modules/ms": { 1332 | "version": "2.1.2", 1333 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1334 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 1335 | "dev": true 1336 | }, 1337 | "node_modules/natural-compare": { 1338 | "version": "1.4.0", 1339 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 1340 | "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", 1341 | "dev": true 1342 | }, 1343 | "node_modules/oauth-sign": { 1344 | "version": "0.9.0", 1345 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 1346 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", 1347 | "dev": true, 1348 | "engines": { 1349 | "node": "*" 1350 | } 1351 | }, 1352 | "node_modules/once": { 1353 | "version": "1.4.0", 1354 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1355 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 1356 | "dev": true, 1357 | "dependencies": { 1358 | "wrappy": "1" 1359 | } 1360 | }, 1361 | "node_modules/optionator": { 1362 | "version": "0.9.3", 1363 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", 1364 | "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", 1365 | "dev": true, 1366 | "dependencies": { 1367 | "@aashutoshrathi/word-wrap": "^1.2.3", 1368 | "deep-is": "^0.1.3", 1369 | "fast-levenshtein": "^2.0.6", 1370 | "levn": "^0.4.1", 1371 | "prelude-ls": "^1.2.1", 1372 | "type-check": "^0.4.0" 1373 | }, 1374 | "engines": { 1375 | "node": ">= 0.8.0" 1376 | } 1377 | }, 1378 | "node_modules/p-limit": { 1379 | "version": "3.1.0", 1380 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 1381 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 1382 | "dev": true, 1383 | "dependencies": { 1384 | "yocto-queue": "^0.1.0" 1385 | }, 1386 | "engines": { 1387 | "node": ">=10" 1388 | }, 1389 | "funding": { 1390 | "url": "https://github.com/sponsors/sindresorhus" 1391 | } 1392 | }, 1393 | "node_modules/p-locate": { 1394 | "version": "5.0.0", 1395 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 1396 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 1397 | "dev": true, 1398 | "dependencies": { 1399 | "p-limit": "^3.0.2" 1400 | }, 1401 | "engines": { 1402 | "node": ">=10" 1403 | }, 1404 | "funding": { 1405 | "url": "https://github.com/sponsors/sindresorhus" 1406 | } 1407 | }, 1408 | "node_modules/parent-module": { 1409 | "version": "1.0.1", 1410 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 1411 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 1412 | "dev": true, 1413 | "dependencies": { 1414 | "callsites": "^3.0.0" 1415 | }, 1416 | "engines": { 1417 | "node": ">=6" 1418 | } 1419 | }, 1420 | "node_modules/path-exists": { 1421 | "version": "4.0.0", 1422 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 1423 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 1424 | "dev": true, 1425 | "engines": { 1426 | "node": ">=8" 1427 | } 1428 | }, 1429 | "node_modules/path-is-absolute": { 1430 | "version": "1.0.1", 1431 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1432 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 1433 | "dev": true, 1434 | "engines": { 1435 | "node": ">=0.10.0" 1436 | } 1437 | }, 1438 | "node_modules/path-key": { 1439 | "version": "3.1.1", 1440 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 1441 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 1442 | "dev": true, 1443 | "engines": { 1444 | "node": ">=8" 1445 | } 1446 | }, 1447 | "node_modules/performance-now": { 1448 | "version": "2.1.0", 1449 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 1450 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", 1451 | "dev": true 1452 | }, 1453 | "node_modules/prelude-ls": { 1454 | "version": "1.2.1", 1455 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 1456 | "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 1457 | "dev": true, 1458 | "engines": { 1459 | "node": ">= 0.8.0" 1460 | } 1461 | }, 1462 | "node_modules/prettier": { 1463 | "version": "2.8.3", 1464 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.3.tgz", 1465 | "integrity": "sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==", 1466 | "dev": true, 1467 | "bin": { 1468 | "prettier": "bin-prettier.js" 1469 | }, 1470 | "engines": { 1471 | "node": ">=10.13.0" 1472 | }, 1473 | "funding": { 1474 | "url": "https://github.com/prettier/prettier?sponsor=1" 1475 | } 1476 | }, 1477 | "node_modules/process": { 1478 | "version": "0.11.10", 1479 | "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", 1480 | "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", 1481 | "engines": { 1482 | "node": ">= 0.6.0" 1483 | } 1484 | }, 1485 | "node_modules/psl": { 1486 | "version": "1.8.0", 1487 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", 1488 | "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", 1489 | "dev": true 1490 | }, 1491 | "node_modules/punycode": { 1492 | "version": "2.1.1", 1493 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 1494 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", 1495 | "dev": true, 1496 | "engines": { 1497 | "node": ">=6" 1498 | } 1499 | }, 1500 | "node_modules/qs": { 1501 | "version": "6.5.3", 1502 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", 1503 | "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", 1504 | "dev": true, 1505 | "engines": { 1506 | "node": ">=0.6" 1507 | } 1508 | }, 1509 | "node_modules/queue-microtask": { 1510 | "version": "1.2.3", 1511 | "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", 1512 | "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", 1513 | "dev": true, 1514 | "funding": [ 1515 | { 1516 | "type": "github", 1517 | "url": "https://github.com/sponsors/feross" 1518 | }, 1519 | { 1520 | "type": "patreon", 1521 | "url": "https://www.patreon.com/feross" 1522 | }, 1523 | { 1524 | "type": "consulting", 1525 | "url": "https://feross.org/support" 1526 | } 1527 | ] 1528 | }, 1529 | "node_modules/readable-stream": { 1530 | "version": "4.4.0", 1531 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.0.tgz", 1532 | "integrity": "sha512-kDMOq0qLtxV9f/SQv522h8cxZBqNZXuXNyjyezmfAAuribMyVXziljpQ/uQhfE1XLg2/TLTW2DsnoE4VAi/krg==", 1533 | "dependencies": { 1534 | "abort-controller": "^3.0.0", 1535 | "buffer": "^6.0.3", 1536 | "events": "^3.3.0", 1537 | "process": "^0.11.10" 1538 | }, 1539 | "engines": { 1540 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 1541 | } 1542 | }, 1543 | "node_modules/request": { 1544 | "version": "2.88.2", 1545 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", 1546 | "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", 1547 | "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", 1548 | "dev": true, 1549 | "dependencies": { 1550 | "aws-sign2": "~0.7.0", 1551 | "aws4": "^1.8.0", 1552 | "caseless": "~0.12.0", 1553 | "combined-stream": "~1.0.6", 1554 | "extend": "~3.0.2", 1555 | "forever-agent": "~0.6.1", 1556 | "form-data": "~2.3.2", 1557 | "har-validator": "~5.1.3", 1558 | "http-signature": "~1.2.0", 1559 | "is-typedarray": "~1.0.0", 1560 | "isstream": "~0.1.2", 1561 | "json-stringify-safe": "~5.0.1", 1562 | "mime-types": "~2.1.19", 1563 | "oauth-sign": "~0.9.0", 1564 | "performance-now": "^2.1.0", 1565 | "qs": "~6.5.2", 1566 | "safe-buffer": "^5.1.2", 1567 | "tough-cookie": "~2.5.0", 1568 | "tunnel-agent": "^0.6.0", 1569 | "uuid": "^3.3.2" 1570 | }, 1571 | "engines": { 1572 | "node": ">= 6" 1573 | } 1574 | }, 1575 | "node_modules/resolve-from": { 1576 | "version": "4.0.0", 1577 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 1578 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 1579 | "dev": true, 1580 | "engines": { 1581 | "node": ">=4" 1582 | } 1583 | }, 1584 | "node_modules/reusify": { 1585 | "version": "1.0.4", 1586 | "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", 1587 | "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", 1588 | "dev": true, 1589 | "engines": { 1590 | "iojs": ">=1.0.0", 1591 | "node": ">=0.10.0" 1592 | } 1593 | }, 1594 | "node_modules/rimraf": { 1595 | "version": "3.0.2", 1596 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", 1597 | "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", 1598 | "dev": true, 1599 | "dependencies": { 1600 | "glob": "^7.1.3" 1601 | }, 1602 | "bin": { 1603 | "rimraf": "bin.js" 1604 | }, 1605 | "funding": { 1606 | "url": "https://github.com/sponsors/isaacs" 1607 | } 1608 | }, 1609 | "node_modules/run-parallel": { 1610 | "version": "1.2.0", 1611 | "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", 1612 | "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", 1613 | "dev": true, 1614 | "funding": [ 1615 | { 1616 | "type": "github", 1617 | "url": "https://github.com/sponsors/feross" 1618 | }, 1619 | { 1620 | "type": "patreon", 1621 | "url": "https://www.patreon.com/feross" 1622 | }, 1623 | { 1624 | "type": "consulting", 1625 | "url": "https://feross.org/support" 1626 | } 1627 | ], 1628 | "dependencies": { 1629 | "queue-microtask": "^1.2.2" 1630 | } 1631 | }, 1632 | "node_modules/safe-buffer": { 1633 | "version": "5.2.1", 1634 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 1635 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 1636 | "dev": true, 1637 | "funding": [ 1638 | { 1639 | "type": "github", 1640 | "url": "https://github.com/sponsors/feross" 1641 | }, 1642 | { 1643 | "type": "patreon", 1644 | "url": "https://www.patreon.com/feross" 1645 | }, 1646 | { 1647 | "type": "consulting", 1648 | "url": "https://feross.org/support" 1649 | } 1650 | ] 1651 | }, 1652 | "node_modules/safer-buffer": { 1653 | "version": "2.1.2", 1654 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 1655 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 1656 | "dev": true 1657 | }, 1658 | "node_modules/shebang-command": { 1659 | "version": "2.0.0", 1660 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1661 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1662 | "dev": true, 1663 | "dependencies": { 1664 | "shebang-regex": "^3.0.0" 1665 | }, 1666 | "engines": { 1667 | "node": ">=8" 1668 | } 1669 | }, 1670 | "node_modules/shebang-regex": { 1671 | "version": "3.0.0", 1672 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1673 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1674 | "dev": true, 1675 | "engines": { 1676 | "node": ">=8" 1677 | } 1678 | }, 1679 | "node_modules/sprintf-js": { 1680 | "version": "1.0.3", 1681 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 1682 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 1683 | "dev": true 1684 | }, 1685 | "node_modules/sshpk": { 1686 | "version": "1.16.1", 1687 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 1688 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 1689 | "dev": true, 1690 | "dependencies": { 1691 | "asn1": "~0.2.3", 1692 | "assert-plus": "^1.0.0", 1693 | "bcrypt-pbkdf": "^1.0.0", 1694 | "dashdash": "^1.12.0", 1695 | "ecc-jsbn": "~0.1.1", 1696 | "getpass": "^0.1.1", 1697 | "jsbn": "~0.1.0", 1698 | "safer-buffer": "^2.0.2", 1699 | "tweetnacl": "~0.14.0" 1700 | }, 1701 | "bin": { 1702 | "sshpk-conv": "bin/sshpk-conv", 1703 | "sshpk-sign": "bin/sshpk-sign", 1704 | "sshpk-verify": "bin/sshpk-verify" 1705 | }, 1706 | "engines": { 1707 | "node": ">=0.10.0" 1708 | } 1709 | }, 1710 | "node_modules/strip-ansi": { 1711 | "version": "6.0.1", 1712 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1713 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1714 | "dev": true, 1715 | "dependencies": { 1716 | "ansi-regex": "^5.0.1" 1717 | }, 1718 | "engines": { 1719 | "node": ">=8" 1720 | } 1721 | }, 1722 | "node_modules/strip-json-comments": { 1723 | "version": "3.1.1", 1724 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1725 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1726 | "dev": true, 1727 | "engines": { 1728 | "node": ">=8" 1729 | }, 1730 | "funding": { 1731 | "url": "https://github.com/sponsors/sindresorhus" 1732 | } 1733 | }, 1734 | "node_modules/supports-color": { 1735 | "version": "7.2.0", 1736 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1737 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1738 | "dev": true, 1739 | "dependencies": { 1740 | "has-flag": "^4.0.0" 1741 | }, 1742 | "engines": { 1743 | "node": ">=8" 1744 | } 1745 | }, 1746 | "node_modules/text-table": { 1747 | "version": "0.2.0", 1748 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 1749 | "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", 1750 | "dev": true 1751 | }, 1752 | "node_modules/tough-cookie": { 1753 | "version": "2.5.0", 1754 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", 1755 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 1756 | "dev": true, 1757 | "dependencies": { 1758 | "psl": "^1.1.28", 1759 | "punycode": "^2.1.1" 1760 | }, 1761 | "engines": { 1762 | "node": ">=0.8" 1763 | } 1764 | }, 1765 | "node_modules/tunnel-agent": { 1766 | "version": "0.6.0", 1767 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1768 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 1769 | "dev": true, 1770 | "dependencies": { 1771 | "safe-buffer": "^5.0.1" 1772 | }, 1773 | "engines": { 1774 | "node": "*" 1775 | } 1776 | }, 1777 | "node_modules/tweetnacl": { 1778 | "version": "0.14.5", 1779 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 1780 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", 1781 | "dev": true 1782 | }, 1783 | "node_modules/type-check": { 1784 | "version": "0.4.0", 1785 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 1786 | "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 1787 | "dev": true, 1788 | "dependencies": { 1789 | "prelude-ls": "^1.2.1" 1790 | }, 1791 | "engines": { 1792 | "node": ">= 0.8.0" 1793 | } 1794 | }, 1795 | "node_modules/type-fest": { 1796 | "version": "0.20.2", 1797 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", 1798 | "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", 1799 | "dev": true, 1800 | "engines": { 1801 | "node": ">=10" 1802 | }, 1803 | "funding": { 1804 | "url": "https://github.com/sponsors/sindresorhus" 1805 | } 1806 | }, 1807 | "node_modules/uri-js": { 1808 | "version": "4.4.1", 1809 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 1810 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1811 | "dev": true, 1812 | "dependencies": { 1813 | "punycode": "^2.1.0" 1814 | } 1815 | }, 1816 | "node_modules/uuid": { 1817 | "version": "3.4.0", 1818 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 1819 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", 1820 | "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", 1821 | "dev": true, 1822 | "bin": { 1823 | "uuid": "bin/uuid" 1824 | } 1825 | }, 1826 | "node_modules/verror": { 1827 | "version": "1.10.0", 1828 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 1829 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 1830 | "dev": true, 1831 | "engines": [ 1832 | "node >=0.6.0" 1833 | ], 1834 | "dependencies": { 1835 | "assert-plus": "^1.0.0", 1836 | "core-util-is": "1.0.2", 1837 | "extsprintf": "^1.2.0" 1838 | } 1839 | }, 1840 | "node_modules/which": { 1841 | "version": "2.0.2", 1842 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1843 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1844 | "dev": true, 1845 | "dependencies": { 1846 | "isexe": "^2.0.0" 1847 | }, 1848 | "bin": { 1849 | "node-which": "bin/node-which" 1850 | }, 1851 | "engines": { 1852 | "node": ">= 8" 1853 | } 1854 | }, 1855 | "node_modules/wrappy": { 1856 | "version": "1.0.2", 1857 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1858 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1859 | "dev": true 1860 | }, 1861 | "node_modules/yocto-queue": { 1862 | "version": "0.1.0", 1863 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1864 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1865 | "dev": true, 1866 | "engines": { 1867 | "node": ">=10" 1868 | }, 1869 | "funding": { 1870 | "url": "https://github.com/sponsors/sindresorhus" 1871 | } 1872 | } 1873 | } 1874 | } 1875 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "readabilitySAX", 3 | "version": "1.6.1", 4 | "description": "the readability script ported to a sax parser", 5 | "author": "Felix Boehm ", 6 | "keywords": [ 7 | "html", 8 | "content extraction", 9 | "readability", 10 | "instapaper" 11 | ], 12 | "main": "lib/", 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/fb55/readabilitysax.git" 16 | }, 17 | "dependencies": { 18 | "entities": "^4.5.0", 19 | "htmlparser2": "^9.0.0", 20 | "minreq": "^0.2.3", 21 | "readable-stream": "^4.4.0" 22 | }, 23 | "devDependencies": { 24 | "coveralls": "^3.1.1", 25 | "eslint": "^8.46.0", 26 | "eslint-config-prettier": "^9.0.0", 27 | "prettier": "^2.8.3" 28 | }, 29 | "bin": { 30 | "readability": "bin/cli.js" 31 | }, 32 | "scripts": { 33 | "test": "node tests/test_output.js", 34 | "lint": "npm run lint:es && npm run lint:prettier", 35 | "lint:es": "eslint .", 36 | "lint:prettier": "npm run prettier -- --check", 37 | "format": "npm run format:es && npm run format:prettier", 38 | "format:es": "npm run lint:es -- --fix", 39 | "format:prettier": "npm run prettier -- --write", 40 | "prettier": "prettier '**/*.{ts,md,json,yml}'" 41 | }, 42 | "license": "BSD-like", 43 | "prettier": { 44 | "proseWrap": "always", 45 | "tabWidth": 4 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /readabilitySAX.js: -------------------------------------------------------------------------------- 1 | /* 2 | * The code is structured into three main parts: 3 | * 2. A list of properties that help readability to determine how a "good" element looks like 4 | * 3. The Readability class that provides the interface & logic (usable as a htmlparser2 handler) 5 | */ 6 | 7 | const { 8 | Element, 9 | formatTags, 10 | headerTags, 11 | re_whitespace, 12 | } = require("./lib/element.js"); 13 | const { getBaseURL } = require("./lib/get-base-url.js"); 14 | 15 | // 2. list of values 16 | const tagsToSkip = new Set([ 17 | "aside", 18 | "footer", 19 | "head", 20 | "label", 21 | "nav", 22 | "noscript", 23 | "script", 24 | "select", 25 | "style", 26 | "textarea", 27 | ]); 28 | 29 | const removeIfEmpty = new Set([ 30 | "blockquote", 31 | "li", 32 | "p", 33 | "pre", 34 | "tbody", 35 | "td", 36 | "th", 37 | "thead", 38 | "tr", 39 | ]); 40 | // Iframe added for html5 players 41 | const embeds = new Set(["embed", "object", "iframe"]); 42 | const goodAttributes = new Set(["alt", "href", "src", "title"]); 43 | const cleanConditionally = new Set(["div", "form", "ol", "table", "ul"]); 44 | const unpackDivs = new Set([...embeds, "div", "img"]); 45 | 46 | const noContent = new Set([ 47 | ...formatTags.keys(), 48 | "font", 49 | "input", 50 | "link", 51 | "meta", 52 | "span", 53 | ]); 54 | 55 | const divToPElements = [ 56 | "a", 57 | "blockquote", 58 | "dl", 59 | "img", 60 | "ol", 61 | "p", 62 | "pre", 63 | "table", 64 | "ul", 65 | ]; 66 | const okayIfEmpty = ["audio", "embed", "iframe", "img", "object", "video"]; 67 | 68 | const re_videos = /http:\/\/(?:www\.)?(?:youtube|vimeo)\.com/; 69 | const re_nextLink = /[>»]|continue|next|weiter(?:[^|]|$)/i; 70 | const re_prevLink = /[<«]|earl|new|old|prev/i; 71 | const re_extraneous = 72 | /all|archive|comment|discuss|e-?mail|login|print|reply|share|sign|single/i; 73 | const re_pages = /pag(?:e|ing|inat)/i; 74 | const re_pagenum = /p[ag]{0,2}(?:e|ing|ination)?[=/]\d{1,2}/i; 75 | 76 | const re_safe = /article-body|hentry|instapaper_body/; 77 | const re_final = /first|last/i; 78 | 79 | const re_positive = 80 | /article|blog|body|content|entry|main|news|pag(?:e|ination)|post|story|text/; 81 | const re_negative = 82 | /com(?:bx|ment|-)|contact|foot(?:er|note)?|masthead|media|meta|outbrain|promo|related|scroll|shoutbox|sidebar|sponsor|shopping|tags|tool|widget/; 83 | const re_unlikelyCandidates = 84 | /ad-break|agegate|auth?or|bookmark|cat|com(?:bx|ment|munity)|date|disqus|extra|foot|header|ignore|links|menu|nav|pag(?:er|ination)|popup|related|remark|rss|share|shoutbox|sidebar|similar|social|sponsor|teaserlist|time|tweet|twitter/; 85 | const re_okMaybeItsACandidate = /and|article|body|column|main|shadow/; 86 | 87 | const re_sentence = /\. |\.$/; 88 | 89 | const re_digits = /\d/; 90 | const re_slashes = /\/+/; 91 | const re_domain = /\/([^/]+)/; 92 | 93 | const re_protocol = /^\w+:/; 94 | const re_cleanPaths = /\/\.(?!\.)|\/[^/]*\/\.\./; 95 | 96 | const re_closing = /\/?(?:#.*)?$/; 97 | const re_imgUrl = /\.(gif|jpe?g|png|webp)$/i; 98 | 99 | function getCandidateSiblings(candidate) { 100 | // Check all siblings 101 | const ret = []; 102 | const childs = candidate.parent.children; 103 | const siblingScoreThreshold = Math.max(10, candidate.totalScore * 0.2); 104 | 105 | for (let i = 0; i < childs.length; i++) { 106 | if (typeof childs[i] === "string") continue; 107 | 108 | if (childs[i] === candidate); 109 | else if (candidate.elementData === childs[i].elementData) { 110 | // TODO: just the class name should be checked 111 | if ( 112 | childs[i].totalScore + candidate.totalScore * 0.2 >= 113 | siblingScoreThreshold 114 | ) { 115 | if (childs[i].name !== "p") childs[i].name = "div"; 116 | } else continue; 117 | } else if (childs[i].name === "p") { 118 | if ( 119 | childs[i].info.textLength >= 80 && 120 | childs[i].info.density < 0.25 121 | ); 122 | else if ( 123 | childs[i].info.textLength < 80 && 124 | childs[i].info.density === 0 && 125 | re_sentence.test(childs[i].toString()) 126 | ); 127 | else continue; 128 | } else continue; 129 | 130 | ret.push(childs[i]); 131 | } 132 | return ret; 133 | } 134 | 135 | const defaultSettings = { 136 | stripUnlikelyCandidates: true, 137 | weightClasses: true, 138 | cleanConditionally: true, 139 | cleanAttributes: true, 140 | replaceImgs: true, 141 | searchFurtherPages: true, 142 | linksToSkip: {}, // Pages that are already parsed 143 | /* 144 | * ` 145 | * pageURL: null, // URL of the page which is parsed 146 | * type: "html", //default type of output 147 | * ` 148 | */ 149 | resolvePaths: false, 150 | }; 151 | 152 | // 3. the readability class 153 | class Readability { 154 | constructor(settings) { 155 | this.onreset(); 156 | this._processSettings(settings); 157 | } 158 | 159 | onreset() { 160 | // The root node 161 | this._currentElement = new Element("document"); 162 | this._topCandidate = null; 163 | this._origTitle = this._headerTitle = ""; 164 | this._scannedLinks = new Map(); 165 | } 166 | 167 | _processSettings(settings) { 168 | this._settings = {}; 169 | 170 | for (const i in defaultSettings) { 171 | this._settings[i] = 172 | typeof settings[i] !== "undefined" 173 | ? settings[i] 174 | : defaultSettings[i]; 175 | } 176 | 177 | let path; 178 | if (settings.pageURL) { 179 | path = settings.pageURL.split(re_slashes); 180 | this._url = { 181 | protocol: path[0], 182 | domain: path[1], 183 | path: path.slice(2, -1), 184 | full: settings.pageURL.replace(re_closing, ""), 185 | }; 186 | this._baseURL = getBaseURL(this._url); 187 | } 188 | if (settings.type) this._settings.type = settings.type; 189 | } 190 | 191 | _convertLinks(path) { 192 | if (!this._url) return path; 193 | if (!path) return this._url.full; 194 | 195 | const pathSplit = path.split("/"); 196 | 197 | // Special cases 198 | if (pathSplit[1] === "") { 199 | // Paths starting with "//" 200 | if (pathSplit[0] === "") { 201 | return this._url.protocol + path; 202 | } 203 | // Full domain (if not caught before) 204 | if (pathSplit[0].substr(-1) === ":") { 205 | return path; 206 | } 207 | } 208 | 209 | // If path is starting with "/" 210 | if (pathSplit[0] === "") pathSplit.shift(); 211 | else Array.prototype.unshift.apply(pathSplit, this._url.path); 212 | 213 | path = pathSplit.join("/"); 214 | 215 | if (this._settings.resolvePaths) { 216 | while (path !== (path = path.replace(re_cleanPaths, ""))); 217 | } 218 | 219 | return `${this._url.protocol}//${this._url.domain}/${path}`; 220 | } 221 | 222 | _scanLink(elem) { 223 | let { href } = elem.attributes; 224 | 225 | if (!href) return; 226 | href = href.replace(re_closing, ""); 227 | 228 | if (href in this._settings.linksToSkip) return; 229 | if (href === this._baseURL || (this._url && href === this._url.full)) { 230 | return; 231 | } 232 | 233 | const match = href.match(re_domain); 234 | 235 | if (!match) return; 236 | if (this._url && match[1] !== this._url.domain) return; 237 | 238 | const text = elem.toString(); 239 | if (text.length > 25 || re_extraneous.test(text)) return; 240 | if (!re_digits.test(href.replace(this._baseURL, ""))) return; 241 | 242 | let score = 0; 243 | const linkData = text + elem.elementData; 244 | 245 | if (re_nextLink.test(linkData)) score += 50; 246 | if (re_pages.test(linkData)) score += 25; 247 | 248 | if (re_final.test(linkData)) { 249 | if (!re_nextLink.test(text)) { 250 | if ( 251 | !( 252 | this._scannedLinks.has(href) && 253 | re_nextLink.test(this._scannedLinks.get(href).text) 254 | ) 255 | ) { 256 | score -= 65; 257 | } 258 | } 259 | } 260 | 261 | if (re_negative.test(linkData) || re_extraneous.test(linkData)) { 262 | score -= 50; 263 | } 264 | if (re_prevLink.test(linkData)) score -= 200; 265 | 266 | if (re_pagenum.test(href) || re_pages.test(href)) score += 25; 267 | if (re_extraneous.test(href)) score -= 15; 268 | 269 | let current = elem; 270 | let posMatch = true; 271 | let negMatch = true; 272 | 273 | while ((current = current.parent)) { 274 | if (current.elementData === "") continue; 275 | if (posMatch && re_pages.test(current.elementData)) { 276 | score += 25; 277 | if (!negMatch) break; 278 | else posMatch = false; 279 | } 280 | if ( 281 | negMatch && 282 | re_negative.test(current.elementData) && 283 | !re_positive.test(current.elementData) 284 | ) { 285 | score -= 25; 286 | if (!posMatch) break; 287 | else negMatch = false; 288 | } 289 | } 290 | 291 | const parsedNum = parseInt(text, 10); 292 | if (parsedNum < 10) { 293 | if (parsedNum === 1) score -= 10; 294 | else score += 10 - parsedNum; 295 | } 296 | 297 | const link = this._scannedLinks.get(href); 298 | 299 | if (link) { 300 | link.score += score; 301 | link.text += ` ${text}`; 302 | } else { 303 | this._scannedLinks.set(href, { 304 | score, 305 | text, 306 | }); 307 | } 308 | } 309 | 310 | // Parser methods 311 | onopentagname(name) { 312 | if (noContent.has(name)) { 313 | if (formatTags.has(name)) { 314 | this._currentElement.children.push(formatTags.get(name)); 315 | } 316 | } else this._currentElement = new Element(name, this._currentElement); 317 | } 318 | 319 | onattribute(name, value) { 320 | if (!value) return; 321 | name = name.toLowerCase(); 322 | 323 | const elem = this._currentElement; 324 | 325 | if (name === "href" || name === "src") { 326 | // Fix links 327 | if (re_protocol.test(value)) elem.attributes[name] = value; 328 | else elem.attributes[name] = this._convertLinks(value); 329 | } else if (name === "id" || name === "class") { 330 | value = value.toLowerCase(); 331 | if (!this._settings.weightClasses); 332 | else if (re_safe.test(value)) { 333 | elem.attributeScore += 300; 334 | elem.isCandidate = true; 335 | } else if (re_negative.test(value)) elem.attributeScore -= 25; 336 | else if (re_positive.test(value)) elem.attributeScore += 25; 337 | 338 | elem.elementData += ` ${value}`; 339 | } else if ( 340 | elem.name === "img" && 341 | (name === "width" || name === "height") 342 | ) { 343 | value = parseInt(value, 10); 344 | if (value !== value); 345 | else if (value <= 32) { 346 | /* 347 | * NaN (skip) 348 | * skip the image 349 | * (use a tagname that's part of tagsToSkip) 350 | */ 351 | elem.name = "script"; 352 | } else if (name === "width" ? value >= 390 : value >= 290) { 353 | // Increase score of parent 354 | elem.parent.attributeScore += 20; 355 | } else if (name === "width" ? value >= 200 : value >= 150) { 356 | elem.parent.attributeScore += 5; 357 | } 358 | } else if (this._settings.cleanAttributes) { 359 | if (goodAttributes.has(name)) elem.attributes[name] = value; 360 | } else elem.attributes[name] = value; 361 | } 362 | 363 | ontext(text) { 364 | this._currentElement.children.push(text); 365 | } 366 | 367 | onclosetag(tagName) { 368 | if (noContent.has(tagName)) return; 369 | 370 | let elem = this._currentElement; 371 | 372 | this._currentElement = elem.parent; 373 | 374 | // Prepare title 375 | if (this._settings.searchFurtherPages && tagName === "a") { 376 | this._scanLink(elem); 377 | } else if (tagName === "title" && !this._origTitle) { 378 | this._origTitle = elem 379 | .toString() 380 | .trim() 381 | .replace(re_whitespace, " "); 382 | return; 383 | } else if (headerTags.has(tagName)) { 384 | const title = elem.toString().trim().replace(re_whitespace, " "); 385 | if (this._origTitle) { 386 | if (this._origTitle.includes(title)) { 387 | if (title.split(" ").length === 4) { 388 | // It's probably the title, so let's use it! 389 | this._headerTitle = title; 390 | } 391 | return; 392 | } 393 | if (tagName === "h1") return; 394 | } 395 | // If there was no title tag, use any h1 as the title 396 | else if (tagName === "h1") { 397 | this._headerTitle = title; 398 | return; 399 | } 400 | } 401 | 402 | if (tagsToSkip.has(tagName)) return; 403 | if ( 404 | this._settings.stripUnlikelyCandidates && 405 | re_unlikelyCandidates.test(elem.elementData) && 406 | !re_okMaybeItsACandidate.test(elem.elementData) 407 | ) { 408 | return; 409 | } 410 | if ( 411 | tagName === "div" && 412 | elem.children.length === 1 && 413 | typeof elem.children[0] === "object" && 414 | unpackDivs.has(elem.children[0].name) 415 | ) { 416 | // Unpack divs 417 | elem.parent.children.push(elem.children[0]); 418 | return; 419 | } 420 | 421 | elem.addInfo(); 422 | 423 | // Clean conditionally 424 | if (embeds.has(tagName)) { 425 | // Check if tag is wanted (youtube or vimeo) 426 | if ( 427 | !( 428 | "src" in elem.attributes && 429 | re_videos.test(elem.attributes.src) 430 | ) 431 | ) { 432 | return; 433 | } 434 | } else if (tagName === "h2" || tagName === "h3") { 435 | // Clean headers 436 | if (elem.attributeScore < 0 || elem.info.density > 0.33) return; 437 | } else if ( 438 | this._settings.cleanConditionally && 439 | cleanConditionally.has(tagName) 440 | ) { 441 | const p = elem.info.tagCount.get("p") || 0; 442 | const contentLength = elem.info.textLength + elem.info.linkLength; 443 | 444 | if (contentLength === 0) { 445 | if (elem.children.length === 0) return; 446 | if ( 447 | elem.children.length === 1 && 448 | typeof elem.children[0] === "string" 449 | ) { 450 | return; 451 | } 452 | } 453 | if ( 454 | elem.info.tagCount.get("li") - 100 > p && 455 | tagName !== "ul" && 456 | tagName !== "ol" 457 | ) { 458 | return; 459 | } 460 | if (contentLength < 25 && elem.info.tagCount.get("img") !== 1) { 461 | return; 462 | } 463 | if (elem.info.density > 0.5) return; 464 | if (elem.attributeScore < 25 && elem.info.density > 0.2) return; 465 | const embedCount = elem.info.tagCount.get("embed"); 466 | if ((embedCount === 1 && contentLength < 75) || embedCount > 1) { 467 | return; 468 | } 469 | } 470 | 471 | if ( 472 | (removeIfEmpty.has(tagName) || 473 | (!this._settings.cleanConditionally && 474 | cleanConditionally.has(tagName))) && 475 | elem.info.linkLength === 0 && 476 | elem.info.textLength === 0 && 477 | elem.children.length !== 0 && 478 | !okayIfEmpty.some((tag) => elem.info.tagCount.has(tag)) 479 | ) { 480 | return; 481 | } 482 | 483 | if ( 484 | this._settings.replaceImgs && 485 | tagName === "a" && 486 | elem.children.length === 1 && 487 | elem.children[0].name === "img" && 488 | re_imgUrl.test(elem.attributes.href) 489 | ) { 490 | elem = elem.children[0]; 491 | elem.attributes.src = elem.parent.attributes.href; 492 | } 493 | 494 | elem.parent.children.push(elem); 495 | 496 | // Should node be scored? 497 | if (tagName === "p" || tagName === "pre" || tagName === "td"); 498 | else if (tagName === "div") { 499 | // Check if div should be converted to a p 500 | if (divToPElements.some((name) => elem.info.tagCount.has(name))) { 501 | return; 502 | } 503 | elem.name = "p"; 504 | } else return; 505 | 506 | if ( 507 | elem.info.textLength + elem.info.linkLength > 24 && 508 | elem.parent && 509 | elem.parent.parent 510 | ) { 511 | elem.parent.isCandidate = elem.parent.parent.isCandidate = true; 512 | const addScore = 513 | 1 + 514 | elem.info.commas + 515 | Math.min( 516 | Math.floor( 517 | (elem.info.textLength + elem.info.linkLength) / 100 518 | ), 519 | 3 520 | ); 521 | elem.parent.tagScore += addScore; 522 | elem.parent.parent.tagScore += addScore / 2; 523 | } 524 | } 525 | 526 | _getCandidateNode() { 527 | let elem = this._topCandidate; 528 | let elems; 529 | if (!elem) { 530 | elem = this._topCandidate = this._currentElement.getTopCandidate(); 531 | } 532 | 533 | if (!elem) { 534 | // Select root node 535 | elem = this._currentElement; 536 | } else if (elem.parent.children.length > 1) { 537 | elems = getCandidateSiblings(elem); 538 | 539 | // Create a new object so that the prototype methods are callable 540 | elem = new Element("div"); 541 | elem.children = elems; 542 | elem.addInfo(); 543 | } 544 | 545 | while (elem.children.length === 1) { 546 | if (typeof elem.children[0] === "object") { 547 | elem = elem.children[0]; 548 | } else break; 549 | } 550 | 551 | return elem; 552 | } 553 | 554 | // SkipLevel is a shortcut to allow more elements of the page 555 | setSkipLevel(skipLevel) { 556 | if (skipLevel === 0) return; 557 | 558 | // If the prototype is still used for settings, change that 559 | if (this._settings === Readability.prototype._settings) { 560 | this._processSettings({}); 561 | } 562 | 563 | if (skipLevel > 0) this._settings.stripUnlikelyCandidates = false; 564 | if (skipLevel > 1) this._settings.weightClasses = false; 565 | if (skipLevel > 2) this._settings.cleanConditionally = false; 566 | } 567 | 568 | getTitle() { 569 | if (this._headerTitle) return this._headerTitle; 570 | if (!this._origTitle) return ""; 571 | 572 | let curTitle = this._origTitle; 573 | 574 | if (/ [|-] /.test(curTitle)) { 575 | curTitle = curTitle.replace(/(.*) [|-] .*/g, "$1"); 576 | 577 | if (curTitle.split(" ").length !== 3) { 578 | curTitle = this._origTitle.replace(/.*?[|-] /, ""); 579 | } 580 | } else if (curTitle.includes(": ")) { 581 | curTitle = curTitle.substr(curTitle.lastIndexOf(": ") + 2); 582 | 583 | if (curTitle.split(" ").length !== 3) { 584 | curTitle = this._origTitle.substr( 585 | this._origTitle.indexOf(": ") 586 | ); 587 | } 588 | } 589 | // TODO: support arrow ("\u00bb") 590 | 591 | curTitle = curTitle.trim(); 592 | 593 | if (curTitle.split(" ").length !== 5) return this._origTitle; 594 | return curTitle; 595 | } 596 | 597 | getNextPage() { 598 | let topScore = 49; 599 | let topLink = ""; 600 | for (const [href, link] of this._scannedLinks) { 601 | if (link.score > topScore) { 602 | topLink = href; 603 | topScore = link.score; 604 | } 605 | } 606 | 607 | return topLink; 608 | } 609 | 610 | getHTML(node) { 611 | if (!node) node = this._getCandidateNode(); 612 | return ( 613 | node 614 | .getInnerHTML() // => clean it 615 | // Remove
s in front of opening & closing

s 616 | .replace(/(?:(?:\s| ?)*)+(?=<\/?p)/g, "") 617 | // Remove spaces in front of
s 618 | .replace(/(?:\s| ?)+(?=)/g, "") 619 | // Turn all double+
s into

s 620 | .replace(/(?:){2,}/g, "

") 621 | // Trim the result 622 | .trim() 623 | ); 624 | } 625 | 626 | getText(node = this._getCandidateNode()) { 627 | return node 628 | .getFormattedText() 629 | .trim() 630 | .replace(/\n+(?=\n{2})/g, ""); 631 | } 632 | 633 | getEvents(cbs) { 634 | (function process(node) { 635 | cbs.onopentag(node.name, node.attributes); 636 | node.children.forEach((child) => 637 | typeof child === "string" ? cbs.ontext(child) : process(child) 638 | ); 639 | cbs.onclosetag(node.name); 640 | })(this._getCandidateNode()); 641 | } 642 | 643 | getArticle(type) { 644 | const elem = this._getCandidateNode(); 645 | 646 | const ret = { 647 | title: this._headerTitle || this.getTitle(), 648 | nextPage: this.getNextPage(), 649 | textLength: elem.info.textLength, 650 | score: this._topCandidate ? this._topCandidate.totalScore : 0, 651 | }; 652 | 653 | if (!type && this._settings.type) ({ type } = this._settings); 654 | 655 | if (type === "text") ret.text = this.getText(elem); 656 | else ret.html = this.getHTML(elem); 657 | 658 | return ret; 659 | } 660 | } 661 | 662 | module.exports = Readability; 663 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # readabilitySAX 2 | 3 | a fast and platform independent readability port 4 | 5 | ## About 6 | 7 | This is a port of the algorithm used by the 8 | [Readability](http://code.google.com/p/arc90labs-readability/) bookmarklet to 9 | extract relevant pieces of information from websites, using a SAX parser. 10 | 11 | The advantage over other ports, e.g. 12 | [arrix/node-readability](https://github.com/arrix/node-readability), is a 13 | smaller memory footprint and a much faster execution. In my tests, most pages, 14 | even large ones, were finished within 15ms (on node, see below for more 15 | information). It works with Rhino, so it runs on 16 | [YQL](http://developer.yahoo.com/yql "Yahoo! Query Language"), which may have 17 | interesting uses. And it works within a browser. 18 | 19 | The Readability extraction algorithm was completely ported, but some adjustments 20 | were made: 21 | 22 | - `

` and `
` tags are recognized and gain a higher value 23 | 24 | - If a heading is part of the pages ``, it is removed (Readability 25 | removed any single `<h2>`, and ignored other tags) 26 | 27 | - `henry` and `instapaper-body` are classes to show an algorithm like this 28 | where the content is. readabilitySAX recognizes them and adds additional 29 | points 30 | 31 | - Every bit of code that was taken from the original algorithm was optimized, 32 | eg. RegExps should now perform faster (they were optimized & use 33 | `RegExp#test` instead of `String#match`, which doesn't force the interpreter 34 | to build an array) 35 | 36 | - Some improvements made by 37 | [GGReadability](https://github.com/curthard89/COCOA-Stuff/tree/master/GGReadability) 38 | (an Obj-C port of Readability) were adopted 39 | - Images get additional scores when their `height` or `width` attributes 40 | are high - icon sized images (<= 32px) get skipped 41 | - Additional classes & ids are checked 42 | 43 | ## How To 44 | 45 | ### Install readabilitySAX 46 | 47 | npm install readabilitySAX 48 | 49 | ##### CLI 50 | 51 | A command line interface (CLI) may be installed via 52 | 53 | npm install -g readabilitySAX 54 | 55 | It's then available via 56 | 57 | readability <domain> [<format>] 58 | 59 | To get this readme, just run 60 | 61 | readability https://github.com/FB55/readabilitySAX 62 | 63 | The format is optional (it's either `text` or `html`, the default value is 64 | `text`). 65 | 66 | ### Usage 67 | 68 | ##### Node 69 | 70 | Just run `require("readabilitySAX")`. You'll get an object containing three 71 | methods: 72 | 73 | - `Readability(settings)`: The readability constructor. It works as a handler 74 | for `htmlparser2`. Read more about it 75 | [in the wiki](https://github.com/FB55/readabilitySAX/wiki/The-Readability-constructor)! 76 | 77 | - `WritableStream(settings, cb)`: A constructor that unites `htmlparser2` and 78 | the `Readability` constructor. It's a writable stream, so simply `.write` 79 | all your data to it. Your callback will be called once `.end` was called. 80 | Bonus: You can also `.pipe` data into it! 81 | 82 | - `createWritableStream(settings, cb)`: Returns a new instance of the 83 | `WritableStream`. (It's a simple factory method.) 84 | 85 | There are two methods available that are deprecated and **will be removed** in a 86 | future version: 87 | 88 | - `get(link, [settings], callback)`: Gets a webpage and process it. 89 | 90 | - `process(data)`: Takes a string, runs readabilitySAX and returns the page. 91 | 92 | **Please don't use those two methods anymore**. Streams are the way you should 93 | build interfaces in node, and that's what I want encourage people to use. 94 | 95 | ##### Browsers 96 | 97 | I started to implement simplified SAX-"parsers" for Rhino/YQL (using E4X) and 98 | the browser (using the DOM) to increase the overall performance on those 99 | platforms. The DOM version is inside the `/browsers` dir. 100 | 101 | A demo of how to use readabilitySAX inside a browser may be found at 102 | [jsFiddle](http://jsfiddle.net/pXqYR/embedded/). Some basic example files are 103 | inside the `/browsers` directory. 104 | 105 | ##### YQL 106 | 107 | A table using E4X-based events is available as the community table 108 | `redabilitySAX`, as well as 109 | [here](https://github.com/FB55/yql-tables/tree/master/readabilitySAX). 110 | 111 | ## Parsers (on node) 112 | 113 | Most SAX parsers (as sax.js) fail when a document is malformed XML, even if it's 114 | correct HTML. readabilitySAX should be used with 115 | [htmlparser2](http://npm.im/htmlparser2), my fork of the `htmlparser`-module 116 | (used by eg. `jsdom`), which corrects most faults. It's listed as a dependency, 117 | so npm should install it with readabilitySAX. 118 | 119 | ## Performance 120 | 121 | ##### Speed 122 | 123 | Using a package of 724 pages from [CleanEval](http://cleaneval.sigwac.org.uk) 124 | (their website seems to be down, try to google it), readabilitySAX processed all 125 | of them in 5768 ms, that's an average of 7.97 ms per page. 126 | 127 | The benchmark was done using `tests/benchmark.js` on a MacBook (late 2010) and 128 | is probably far from perfect. 129 | 130 | Performance is the main goal of this project. The current speed should be good 131 | enough to run readabilitySAX on a singe-threaded web server with an average 132 | number of requests. That's an accomplishment! 133 | 134 | ##### Accuracy 135 | 136 | The main goal of CleanEval is to evaluate the accuracy of an algorithm. 137 | 138 | **_// TODO_** 139 | 140 | ## Todo 141 | 142 | - Add documentation & examples 143 | - Add support for URLs containing hash-bangs (`#!`) 144 | - Allow fetching articles with more than one page 145 | - Don't remove all images inside `<a>` tags 146 | -------------------------------------------------------------------------------- /tests/benchmark.js: -------------------------------------------------------------------------------- 1 | const { createWritableStream } = require("../"); 2 | const fs = require("fs"); 3 | const dir = "/Users/felix/Downloads/CleanEval/finalrun-input/"; 4 | const files = fs.readdirSync(dir); 5 | let time = 0; 6 | const total = files.length; 7 | let skipped = 0; 8 | let min = 1 / 0; 9 | let max = -1 / 0; 10 | 11 | function run(name) { 12 | if (!name || name.charAt(0) === ".") return proc(); 13 | 14 | const file = fs.readFileSync(dir + name).toString(); 15 | const start = Date.now(); 16 | 17 | createWritableStream((ret) => { 18 | if (!ret.score) skipped++; 19 | else { 20 | const took = Date.now() - start; 21 | time += took; 22 | if (took < min) min = took; 23 | if (took > max) max = took; 24 | } 25 | }).end(file); 26 | } 27 | 28 | function proc() { 29 | if (!files.length) return; 30 | run(files.pop()); 31 | process.nextTick(proc); 32 | if (files.length % 10 === total % 10) { 33 | console.log("did", total - files.length); 34 | } 35 | } 36 | 37 | proc(); 38 | 39 | process.on("exit", () => { 40 | const did = total - skipped; 41 | console.log("took", time); 42 | console.log("runs", did); 43 | console.log("average", Math.round((time / did) * 1e4) / 1e4); 44 | console.log("min", min); 45 | console.log("max", max); 46 | console.log("skipped", skipped); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/cleaneval.js: -------------------------------------------------------------------------------- 1 | const getReadableContent = require("../").process; 2 | const fs = require("fs"); 3 | const dir = "/Users/felix/Downloads/CleanEval/"; 4 | const input = `${dir}finalrun-input/`; 5 | const output = `${dir}finalrun-output/`; 6 | const ents = require("entities"); 7 | 8 | fs.readdirSync(input).forEach((name) => { 9 | if (!name || name.charAt(0) === ".") return; 10 | 11 | const ret = getReadableContent(fs.readFileSync(input + name).toString(), { 12 | type: "text", 13 | }); 14 | 15 | // If(ret.score < 100) return; 16 | 17 | fs.writeFileSync( 18 | output + name.replace(".html", ".txt"), 19 | (ret.title ? `${ret.title}\n\n` : "") + ents.decodeHTML5(ret.text) 20 | ); 21 | }); 22 | 23 | console.log("Finished all files!"); 24 | 25 | const check = require("child_process").spawn("python", [ 26 | `${dir}cleaneval.py`, 27 | "-t", 28 | output, 29 | `${dir}GoldStandard/`, 30 | ]); 31 | 32 | check.stdout.pipe(process.stdout); 33 | check.stderr.pipe(process.stderr); 34 | -------------------------------------------------------------------------------- /tests/test_output.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const { Parser } = require("htmlparser2"); 3 | const Readability = require("../readabilitySAX"); 4 | const file = require("fs").readFileSync(`${__dirname}/testpage.html`, "utf8"); 5 | 6 | const expectedLinks = 2; 7 | 8 | const expectedUrl = { 9 | protocol: "http:", 10 | domain: "foo.bar", 11 | path: ["this.2", "is", "a", "long", "path"], 12 | full: "http://foo.bar/this.2/is/a/long/path/index?isnt=it", 13 | }; 14 | 15 | const expectedData = { 16 | title: "Realtime Performance Visualizations using Node.js", 17 | nextPage: "http://howtonode.org/heat-tracer/dummy/page/2", 18 | textLength: 11668, 19 | score: 83, 20 | html: "<a href=\"http://howtonode.org/243bfe84f43affd3244e1828d90a8dca7fcc34c4/heat-tracer\">Static Version</a><p>This article outlines how to create a realtime heatmap of your syscall latency using HTML5, some great node modules, and DTrace. It was inspired by talk that Bryan Cantrill and Brendan Greg gave on Joyent's cool cloud analytics tools. While specific, the code provided could easily be adapted to provide a heatmap of any type of aggregation Dtrace is capable of providing. </p>\n\n<h2>System Requirements</h2>\n\n<p>First thing's first, you're going to need a system with DTrace. This likely means Solaris (or one of its decedents), OS X, or a BSD variant. There doesn't appear to be Dtrace available for Linux. </p>\n\n<h2>Security</h2>\n\n<p>Secondly, please be aware that at the time of writing the demo code contains a fairly substantial secruity vulnerabilty. Namely the d script is sent from the client with no authentication what so ever. If you bind to localhost this shouldn't be a big deal for a demo. Time permitting I intend to clean up the code. </p>\n\n<h2>Dependencies</h2>\n\n<p>For this tutorial you'll also need:</p>\n\n<pre><code>node - http://nodejs.org/#download (duh)<br/>npm - https://github.com/isaacs/npm (makes installing modules a breeze)<br/>node-libdtrace - https://github.com/bcantrill/node-libdtrace (provides dtrace functionality)<br/>Socket.IO - 'npm install socket.io' (web sockets made easy)<br/></code></pre>\n\n<h2>Server</h2>\n\n<p>Now we're ready to start writing our web server: </p>\n\n<p><embed src=\"http://youtube.com/\"></embed> This is just a test embed! </p>\n\n<div><a href=\"http://howtonode.org/heat-tracer/heat-tracer/heat_tracer.js\">heat_tracer.js</a><pre><code>var http = require('http');<br/>var libdtrace = require('libdtrace');<br/>var io = require('socket.io');<br/>var express = require('express');</p><p>/* create our express server and prepare to serve javascript files in ./public<br/>*/<br/>var app = express.createServer();<br/>app.configure(function(){<br/>    app.use(express.staticProvider(__dirname + '/public'));<br/>    });</p><p>/* Before we go any further we must realize that each time a user connects we're going to want to<br/>   them send them dtrace aggregation every second. We can do so using 'setInterval', but we must<br/>   keep track of both the intervals we set and the dtrace consumers that are created as we'll need<br/>   them later when the client disconnects.<br/>*/<br/>var interval_id_by_session_id = {};<br/>var dtp_by_session_id = {};</p><p>/* In order to effecienctly send packets we're going to use the Socket.IO library which seemlessly<br/>   integrates with express.<br/>*/<br/>var websocket_server = io.listen(app);</p><p>/* Now that we have a web socket server, we need to create a handler for connection events. These<br/>   events represet a client connecting to our server */<br/>websocket_server.on('connection', function(socket) {</p><p>    /* Like the web server object, we must also define handlers for various socket events that<br/>       will happen during the lifetime of the connection. These will define how we interact with<br/>           the client. The first is a message event which occurs when the client sends something to<br/>       the server. */<br/>    socket.on( 'message', function(message) {<br/>        /* The only message the client ever sends will be sent right after connecting.<br/>                   So it will happen only once during the lifetime of a socket. This message also<br/>           contains a d script which defines an agregation to walk.<br/>           */<br/>        var dtp = new libdtrace.Consumer();<br/>        var dscript = message['dscript'];<br/>        console.log( dscript );<br/>        dtp.strcompile(dscript);<br/>        dtp.go();<br/>        dtp_by_session_id[socket.sessionId] = dtp;</p><p>         /* All that's left to do is send the aggration data from the dscript.  */<br/>         interval_id_by_session_id[socket.sessionId] = setInterval(function () {<br/>             var aggdata = {};<br/>             try {<br/>                 dtp.aggwalk(function (id, key, val) {<br/>                     for( index in val ) {<br/>                     /* console.log( 'key: ' + key + ', interval: ' +<br/>                        val[index][0][0] + '-' + val[index][0][1], ', count ' + val[index][1] ); */</p><p>                    aggdata[key] = val;<br/>                }<br/>                } );<br/>                socket.send( aggdata );<br/>            } catch( err ) {<br/>                console.log(err);<br/>            }</p><p>            },  1001 );<br/>        } );</p><p>    /* Not so fast. If a client disconnects we don't want their respective dtrace consumer to<br/>       keep collecting data any more. We also don't want to try to keep sending anything to them<br/>       period. So clean up. */<br/>    socket.on('disconnect', function(){<br/>        clearInterval(clearInterval(interval_id_by_session_id[socket.sessionId]));<br/>        var dtp = dtp_by_session_id[socket.sessionId];<br/>        delete dtp_by_session_id[socket.sessionId];<br/>        dtp.stop();<br/>        console.log('disconnected');<br/>        });</p><p>    } );</p><p>app.listen(80);</code></pre></div>\n\n<h2>Client</h2>\n\n<p>In order to display our heatmap, we're going to need some basic HTML with a canvas element:</p>\n\n<div><a href=\"http://howtonode.org/heat-tracer/heat-tracer/public/heat_tracer.html\">public/heat_tracer.html</a><pre><code><html><br/><head><br/><script src=\"http://localhost/socket.io/socket.io.js\"></script><br/><script src=\"http://localhost/heat_tracer_client.js\"></script><br/></head><br/><body onLoad='heat_tracer()'><br/><canvas id='canvas' width='1024' height='512'></canvas><br/></body><br/></html></code></pre></div>\n\n<p>Finally the JavaScript client which translates the raw streaming data into pretty picture:</p>\n\n<div><a href=\"http://howtonode.org/heat-tracer/heat-tracer/public/heat_tracer_client.js\">public/heat_tracer_client.js</a><pre><code>/* On load we create our web socket (or flash socket if your browser doesn't support it ) and<br/>   send the d script we wish to be tracing. This extremely powerful and *insecure*. */<br/>function heat_tracer() {</p><p>    //Global vars<br/>    setup();</p><p>    var socket = new io.Socket('localhost'); //connect to localhost presently<br/>    socket.connect();</p><p>    socket.on('connect', function(){<br/>        console.log('on connection');<br/>        var dscript = \"syscall:::entry\\n{\\nself->syscall_entry_ts[probefunc] = vtimestamp;\\n}\\nsyscall:::return\\n/self->syscall_entry_ts[probefunc]/\\n{\\n\\n@time[probefunc] = lquantize((vtimestamp - self->syscall_entry_ts[probefunc] ) / 1000, 0, 63, 2);\\nself->syscall_entry_ts[probefunc] = 0;\\n}\";<br/>        socket.send( { 'dscript' : dscript } );<br/>    });</p><p>    /* The only messages we recieve should contain contain the dtrace aggregation data we requested<br/>       on connection. */<br/>    socket.on('message', function(message){<br/>        //console.log( message );<br/>        draw(message);</p><p>        /* for ( key in message ) {<br/>           val = message[key];<br/>           console.log( 'key: ' + key + ', interval: ' + val[0][0] + '-' + val[0][1], ', count ' + val[1] );<br/>           }<br/>        */<br/>    });</p><p>    socket.on('disconnect', function(){<br/>    });</p><p>}</p><p>/* Take the aggregation data and update the heatmap */<br/>function draw(message) {</p><p>    /* Latest data goes in the right most column, initialize it */<br/>    var syscalls_by_latency = [];<br/>    for ( var index = 0; index < 32; index++ ) {<br/>    syscalls_by_latency[index] = 0;<br/>    }</p><p>    /* Presently we have the latency for each system call quantized in our message. Merge the data<br/>       such that we have all the system call latency quantized together. This gives us the number<br/>       of syscalls made with latencies in each particular band. */<br/>    for ( var syscall in message ) {<br/>    var val = message[syscall];<br/>    for ( result_index in val ) {<br/>        var latency_start = val[result_index][0][0];<br/>        var count =  val[result_index][1];<br/>        /* The d script we're using lquantizes from 0 to 63 in steps of two. So dividing by 2<br/>           tells us which row this result belongs in */<br/>        syscalls_by_latency[Math.floor(latency_start/2)] += count;<br/>    }<br/>    }</p><p>    /* We just created a new column, shift the console to the left and add it. */<br/>    console_columns.shift();<br/>    console_columns.push(syscalls_by_latency);<br/>    drawArray(console_columns);<br/>}</p><p>/* Draw the columns and rows that map up the heatmap on to the canvas element */<br/>function drawArray(console_columns) {<br/>    var canvas = document.getElementById('canvas');<br/>    if (canvas.getContext) {<br/>    var ctx = canvas.getContext('2d');<br/>    for ( var column_index in console_columns ) {<br/>        var column = console_columns[column_index];<br/>        for ( var entry_index in column ) {<br/>        entry = column[entry_index];</p><p>        /* We're using a logarithmic scale for the brightness. This was all arrived at by<br/>           trial and error and found to work well on my Mac.  In the future this<br/>           could all be adjustable with controls */<br/>        var red_value = 0;<br/>        if ( entry != 0 ) {<br/>            red_value = Math.floor(Math.log(entry)/Math.log(2));<br/>        }<br/>        //console.log(red_value);<br/>        ctx.fillStyle = 'rgb(' + (red_value * 25) + ',0,0)';<br/>        ctx.fillRect(column_index*16, 496-(entry_index*16), 16, 16);<br/>        }<br/>    }<br/>    }<br/>}</p><p>/* The heatmap is is really a 64x32 grid. Initialize the array which contains the grid data. */<br/>function setup() {<br/>    console_columns = [];</p><p>    for ( var column_index = 0; column_index < 64; column_index++ ) {<br/>    var column = [];<br/>    for ( var entry_index = 0; entry_index < 32; entry_index++ ) {<br/>        column[entry_index] = 0;<br/>    }<br/>    console_columns.push(column);<br/>    }</p><p>}</code></pre></div>\n\n<h2>Run It!</h2>\n\n<p>Run Heat Tacer with the following. Note, sudo is required by dtrace as it does kernal magic.</p>\n\n<pre><code>sudo node heat_tracer.js<br/></code></pre>\n\n<p>If all goes well you should see something a moving version of something like the image below.</p>\n\n<blockquote>\n <p><img src=\"http://howtonode.org/heat-tracer/heat_tracer.png\" alt=\"Alt value of image\"></img> </p>\n</blockquote>\n\n<h2>Contribute</h2>\n\n<p>You can find the latest version of Heat Tracer <a href=\"https://github.com/gflarity/Heat-Tracer\">here</a>. It is my hope that this article will provide the ground work for a much more abitious performance analytics project. If you're interested in contributing please let me know.</p>\n\n<h2>Further Research</h2>\n\n<p>More information about Bryan and Brendan's demo can be found <a href=\"http://dtrace.org/blogs/brendan/2011/01/24/cloud-analytics-first-video/\">here</a>.</p>\n\n<p>Socket.IO can be found <a href=\"http://socket.io/\">here</a>.</p><hr/>\n\n<a href=\"http://disqus.com/forums/howtonodeorg/?url=ref\">View the discussion thread.</a>", 21 | }; 22 | 23 | const readable = new Readability({ 24 | pageURL: "http://howtonode.org/heat-tracer/", 25 | resolvePaths: true, 26 | }); 27 | const parser = new Parser(readable); 28 | 29 | parser.parseComplete(file); 30 | 31 | const article = readable.getArticle(); 32 | 33 | assert.strictEqual( 34 | article.title, 35 | expectedData.title, 36 | "didn't get expected title!" 37 | ); 38 | assert.strictEqual( 39 | article.nextPage, 40 | expectedData.nextPage, 41 | "didn't get expected nextPage!" 42 | ); 43 | assert.strictEqual( 44 | article.textLength, 45 | expectedData.textLength, 46 | "didn't get expected textLength!" 47 | ); 48 | assert.strictEqual( 49 | article.score, 50 | expectedData.score, 51 | "didn't get expected score!" 52 | ); 53 | assert.strictEqual( 54 | article.html, 55 | expectedData.html, 56 | "didn't get expected html!" 57 | ); 58 | 59 | assert.strictEqual( 60 | require("util").inspect(readable._currentElement, false, 1 / 0).length, 61 | 245663, 62 | "tree had false size!" 63 | ); 64 | assert.strictEqual( 65 | Object.keys(readable._scannedLinks).length, 66 | expectedLinks, 67 | "wrong number of links!" 68 | ); 69 | 70 | testURL(); 71 | 72 | console.log("Passed!"); 73 | 74 | function testURL() { 75 | const readable = new Readability({ 76 | pageURL: "http://foo.bar/this.2/is/a/long/path/index?isnt=it", 77 | resolvePaths: true, 78 | }); 79 | 80 | assert.strictEqual( 81 | JSON.stringify(readable._url), 82 | JSON.stringify(expectedUrl), 83 | "wrong url" 84 | ); 85 | assert.strictEqual( 86 | readable._baseURL, 87 | "http://foo.bar/this.2/is/a/long/path", 88 | "wrong base" 89 | ); 90 | assert.strictEqual( 91 | readable._convertLinks("../asdf/foo/"), 92 | "http://foo.bar/this.2/is/a/long/asdf/foo/", 93 | "link1 wasn't resolved!" 94 | ); 95 | assert.strictEqual( 96 | readable._convertLinks("/asdf/foo/"), 97 | "http://foo.bar/asdf/foo/", 98 | "link2 wasn't resolved!" 99 | ); 100 | assert.strictEqual( 101 | readable._convertLinks("foo/"), 102 | "http://foo.bar/this.2/is/a/long/path/foo/", 103 | "link3 wasn't resolved!" 104 | ); 105 | } 106 | -------------------------------------------------------------------------------- /tests/test_performance.js: -------------------------------------------------------------------------------- 1 | const getReadableContent = require("../"); 2 | const { Parser } = require("htmlparser2"); 3 | const Readability = require("../readabilitySAX"); 4 | const request = require("request"); 5 | const url = require("url"); 6 | 7 | function ben(times, func) { 8 | const start = Date.now(); 9 | while (times-- > 0) func(); 10 | return Date.now() - start; 11 | } 12 | 13 | const processContent = function (data, settings) { 14 | const readable = new Readability(settings); 15 | const parser = new Parser(readable); 16 | 17 | console.log( 18 | "parsing took (ms):", 19 | ben(1e3, () => { 20 | parser.parseComplete(data); 21 | }) 22 | ); 23 | console.log( 24 | "getHTML took (ms):", 25 | ben(1e3, () => { 26 | readable.getHTML(); 27 | }) 28 | ); 29 | console.log( 30 | "getText took (ms):", 31 | ben(1e3, () => { 32 | readable.getText(); 33 | }) 34 | ); 35 | console.log( 36 | "getArticle took (ms):", 37 | ben(1e3, () => { 38 | readable.getArticle(); 39 | }) 40 | ); 41 | console.log( 42 | "Whole parsing took (ms):", 43 | ben(500, () => { 44 | getReadableContent.process(data, settings); 45 | }) 46 | ); 47 | }; 48 | 49 | if (process.argv.length > 2) { 50 | console.log("connecting to:", process.argv[2]); 51 | 52 | request(process.argv[2], (err, resp, body) => { 53 | processContent(body, { 54 | pageURL: url.format(resp.request.uri), 55 | log: false, 56 | }); 57 | }); 58 | } else { 59 | const file = require("fs").readFileSync( 60 | `${__dirname}/testpage.html`, 61 | "utf8" 62 | ); 63 | processContent(file, { 64 | pageURL: "http://howtonode.org/heat-tracer", 65 | log: false, 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /tests/testpage.html: -------------------------------------------------------------------------------- 1 | 2 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 3 | <html lang="en" xmlns="http://www.w3.org/1999/xhtml"><head><meta http-equiv="content-type" content="text/html; charset=UTF-8" /><title>Realtime Performance Visualizations using Node.js - How To Node - NodeJS
Static Version

Realtime Performance Visualizations using Node.js

This article outlines how to create a realtime heatmap of your syscall latency using HTML5, some great node modules, and DTrace. It was inspired by talk that Bryan Cantrill and Brendan Greg gave on Joyent's cool cloud analytics tools. While specific, the code provided could easily be adapted to provide a heatmap of any type of aggregation Dtrace is capable of providing.

4 | 5 |

System Requirements

6 | 7 |

First thing's first, you're going to need a system with DTrace. This likely means Solaris (or one of its decedents), OS X, or a BSD variant. There doesn't appear to be Dtrace available for Linux.

8 | 9 |

Security

10 | 11 |

Secondly, please be aware that at the time of writing the demo code contains a fairly substantial secruity vulnerabilty. Namely the d script is sent from the client with no authentication what so ever. If you bind to localhost this shouldn't be a big deal for a demo. Time permitting I intend to clean up the code.

12 | 13 |

Dependencies

14 | 15 |

For this tutorial you'll also need:

16 | 17 |
node - http://nodejs.org/#download (duh)
npm
- https://github.com/isaacs/npm (makes installing modules a breeze)
node
-libdtrace - https://github.com/bcantrill/node-libdtrace (provides dtrace functionality)
Socket.IO - 'npm install socket.io' (web sockets made easy)
18 | 19 |

Server

20 | 21 |

Now we're ready to start writing our web server:

22 | 23 |

This is just a test embed!

24 | 25 |
heat_tracer.js
var http = require('http');
var libdtrace = require('libdtrace');
var io = require('socket.io');
var express = require('express');

/* create our express server and prepare to serve javascript files in ./public
*/

var app = express.createServer();
app
.configure(function(){
    app
.use(express.staticProvider(__dirname + '/public'));
   
});


/* Before we go any further we must realize that each time a user connects we're going to want to
   them send them dtrace aggregation every second. We can do so using 'setInterval', but we must
   keep track of both the intervals we set and the dtrace consumers that are created as we'll need
   them later when the client disconnects.
*/

var interval_id_by_session_id = {};
var dtp_by_session_id = {};

/* In order to effecienctly send packets we're going to use the Socket.IO library which seemlessly
   integrates with express.  
*/

var websocket_server = io.listen(app);

/* Now that we have a web socket server, we need to create a handler for connection events. These
   events represet a client connecting to our server */

websocket_server
.on('connection', function(socket) {

   
/* Like the web server object, we must also define handlers for various socket events that
       will happen during the lifetime of the connection. These will define how we interact with
           the client. The first is a message event which occurs when the client sends something to
       the server. */

    socket
.on( 'message', function(message) {
       
/* The only message the client ever sends will be sent right after connecting.  
                   So it will happen only once during the lifetime of a socket. This message also
           contains a d script which defines an agregation to walk.
           */

       
var dtp = new libdtrace.Consumer();
       
var dscript = message['dscript'];
        console
.log( dscript );
        dtp
.strcompile(dscript);        
        dtp
.go();
        dtp_by_session_id
[socket.sessionId] = dtp;

         
/* All that's left to do is send the aggration data from the dscript.  */
         interval_id_by_session_id
[socket.sessionId] = setInterval(function () {
             
var aggdata = {};
             
try {
                 dtp
.aggwalk(function (id, key, val) {
                     
for( index in val ) {
                     
/* console.log( 'key: ' + key + ', interval: ' +
                        val[index][0][0] + '-' + val[index][0][1], ', count ' + val[index][1] ); */


                    aggdata
[key] = val;
               
}
               
} );
                socket
.send( aggdata );
           
} catch( err ) {
                console
.log(err);
           
}

           
},  1001 );
       
} );


   
/* Not so fast. If a client disconnects we don't want their respective dtrace consumer to
       keep collecting data any more. We also don't want to try to keep sending anything to them
       period. So clean up. */

    socket
.on('disconnect', function(){
        clearInterval
(clearInterval(interval_id_by_session_id[socket.sessionId]));
       
var dtp = dtp_by_session_id[socket.sessionId];
       
delete dtp_by_session_id[socket.sessionId];
        dtp
.stop();    
        console
.log('disconnected');
       
});


   
} );


app
.listen(80);
26 | 27 |

Client

28 | 29 |

In order to display our heatmap, we're going to need some basic HTML with a canvas element:

30 | 31 |
public/heat_tracer.html
<html>
<head>
<script src="http://localhost/socket.io/socket.io.js"></script>
<script src="http://localhost/heat_tracer_client.js"></script>
</head>
<body onLoad='heat_tracer()'>
<canvas id='canvas' width='1024' height='512'></canvas>
</body>
</html>
32 | 33 |

Finally the JavaScript client which translates the raw streaming data into pretty picture:

34 | 35 |
public/heat_tracer_client.js
/* On load we create our web socket (or flash socket if your browser doesn't support it ) and
   send the d script we wish to be tracing. This extremely powerful and *insecure*. */

function heat_tracer() {

   
//Global vars
    setup
();

   
var socket = new io.Socket('localhost'); //connect to localhost presently
    socket
.connect();

    socket
.on('connect', function(){
        console
.log('on connection');
       
var dscript = "syscall:::entry\n{\nself->syscall_entry_ts[probefunc] = vtimestamp;\n}\nsyscall:::return\n/self->syscall_entry_ts[probefunc]/\n{\n\n@time[probefunc] = lquantize((vtimestamp - self->syscall_entry_ts[probefunc] ) / 1000, 0, 63, 2);\nself->syscall_entry_ts[probefunc] = 0;\n}";
        socket
.send( { 'dscript' : dscript } );
   
});


   
/* The only messages we recieve should contain contain the dtrace aggregation data we requested
       on connection. */

    socket
.on('message', function(message){
       
//console.log( message );
        draw
(message);

       
/* for ( key in message ) {
           val = message[key];
           console.log( 'key: ' + key + ', interval: ' + val[0][0] + '-' + val[0][1], ', count ' + val[1] );
           }  
        */

   
});

    socket
.on('disconnect', function(){
   
});

}


/* Take the aggregation data and update the heatmap */
function draw(message) {  

   
/* Latest data goes in the right most column, initialize it */
   
var syscalls_by_latency = [];
   
for ( var index = 0; index < 32; index++ ) {
    syscalls_by_latency
[index] = 0;
   
}

   
/* Presently we have the latency for each system call quantized in our message. Merge the data
       such that we have all the system call latency quantized together. This gives us the number
       of syscalls made with latencies in each particular band. */

   
for ( var syscall in message ) {
   
var val = message[syscall];
   
for ( result_index in val ) {
       
var latency_start = val[result_index][0][0];
       
var count =  val[result_index][1];
       
/* The d script we're using lquantizes from 0 to 63 in steps of two. So dividing by 2
           tells us which row this result belongs in */

        syscalls_by_latency
[Math.floor(latency_start/2)] += count;                    
   
}
   
}


   
/* We just created a new column, shift the console to the left and add it. */
    console_columns
.shift();
    console_columns
.push(syscalls_by_latency);
    drawArray
(console_columns);
}



/* Draw the columns and rows that map up the heatmap on to the canvas element */
function drawArray(console_columns) {
   
var canvas = document.getElementById('canvas');
   
if (canvas.getContext) {
   
var ctx = canvas.getContext('2d');  
   
for ( var column_index in console_columns ) {
       
var column = console_columns[column_index];              
       
for ( var entry_index in column ) {
        entry
= column[entry_index];

       
/* We're using a logarithmic scale for the brightness. This was all arrived at by
           trial and error and found to work well on my Mac.  In the future this
           could all be adjustable with controls */

       
var red_value = 0;                
       
if ( entry != 0 ) {
            red_value
= Math.floor(Math.log(entry)/Math.log(2));                  
       
}
       
//console.log(red_value);                
        ctx
.fillStyle = 'rgb(' + (red_value * 25) + ',0,0)';
        ctx
.fillRect(column_index*16, 496-(entry_index*16), 16, 16);
       
}
   
}
   
}
}


/* The heatmap is is really a 64x32 grid. Initialize the array which contains the grid data. */
function setup() {
    console_columns
= [];

   
for ( var column_index = 0; column_index < 64; column_index++ ) {
   
var column = [];
   
for ( var entry_index = 0; entry_index < 32; entry_index++ ) {
        column
[entry_index] = 0;
   
}
    console_columns
.push(column);
   
}

}
36 | 37 |

Run It!

38 | 39 |

Run Heat Tacer with the following. Note, sudo is required by dtrace as it does kernal magic.

40 | 41 |
sudo node heat_tracer.js
42 | 43 |

If all goes well you should see something a moving version of something like the image below.

44 | 45 |
46 |

Alt value of image

47 |
48 | 49 |

Contribute

50 | 51 |

You can find the latest version of Heat Tracer here. It is my hope that this article will provide the ground work for a much more abitious performance analytics project. If you're interested in contributing please let me know.

52 | 53 |

Further Research

54 | 55 |

More information about Bryan and Brendan's demo can be found here.

56 | 57 |

Socket.IO can be found here.


58 | 63 | View the discussion thread.blog comments powered byDisqus 64 | 80 |
89 | 90 | next page 91 | 92 | 98 | 99 | 107 | --------------------------------------------------------------------------------