diff --git a/.eslintrc.json b/.eslintrc.json index 70fd13e7d..71ec49955 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,27 +1,62 @@ { - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "prettier", - "prettier/@typescript-eslint" - ], + "extends": ["eslint:recommended", "prettier"], "env": { "node": true, "es6": true }, "rules": { - "eqeqeq": 2, + "eqeqeq": [2, "smart"], "no-caller": 2, "dot-notation": 2, "no-var": 2, "prefer-const": 2, - "prefer-arrow-callback": 2, + "prefer-arrow-callback": [2, { "allowNamedFunctions": true }], "arrow-body-style": [2, "as-needed"], "object-shorthand": 2, - - "@typescript-eslint/explicit-function-return-type": 0, - "@typescript-eslint/explicit-member-accessibility": 0, - "@typescript-eslint/no-use-before-define": [2, { "functions": false }] - } + "prefer-template": 2, + "one-var": [2, "never"], + "prefer-destructuring": [2, { "object": true }], + "capitalized-comments": 2, + "multiline-comment-style": [2, "starred-block"], + "spaced-comment": 2, + "yoda": [2, "never"], + "curly": [2, "multi-line"], + "no-else-return": 2 + }, + "overrides": [ + { + "files": "*.ts", + "extends": [ + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "prettier/@typescript-eslint" + ], + "parserOptions": { + "sourceType": "module", + "project": "./tsconfig.eslint.json" + }, + "rules": { + "@typescript-eslint/prefer-for-of": 0, + "@typescript-eslint/member-ordering": 0, + "@typescript-eslint/explicit-function-return-type": 0, + "@typescript-eslint/no-unused-vars": 0, + "@typescript-eslint/no-use-before-define": [ + 2, + { "functions": false } + ], + "@typescript-eslint/consistent-type-definitions": [ + 2, + "interface" + ], + "@typescript-eslint/prefer-function-type": 2, + "@typescript-eslint/no-unnecessary-type-arguments": 2, + "@typescript-eslint/prefer-string-starts-ends-with": 2, + "@typescript-eslint/prefer-readonly": 2, + "@typescript-eslint/prefer-includes": 2, + "@typescript-eslint/no-unnecessary-condition": 2, + "@typescript-eslint/switch-exhaustiveness-check": 2, + "@typescript-eslint/prefer-nullish-coalescing": 2 + } + } + ] } diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index e68c90cab..b440c7477 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,12 +1,2 @@ -# These are supported funding model platforms - -github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username -open_collective: # Replace with a single Open Collective username -ko_fi: # Replace with a single Ko-fi username -tidelift: "npm/htmlparser2" -community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry -liberapay: # Replace with a single Liberapay username -issuehunt: # Replace with a single IssueHunt username -otechie: # Replace with a single Otechie username -custom: # Replace with a single custom sponsorship URL +github: [fb55] +tidelift: npm/htmlparser2 diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 000000000..646e1e1d3 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,52 @@ +name: "Code scanning - action" + +on: + push: + branches: [master] + pull_request: + # The branches below must be a subset of the branches above + branches: [master] + schedule: + - cron: "0 7 * * 0" + +jobs: + CodeQL-Build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + # Override language selection by uncommenting this and choosing your languages + # with: + # languages: go, javascript, csharp, python, cpp, java + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/nodejs-lint.yml b/.github/workflows/nodejs-lint.yml new file mode 100644 index 000000000..e4e25ceca --- /dev/null +++ b/.github/workflows/nodejs-lint.yml @@ -0,0 +1,16 @@ +name: Node.js Lint + +on: [push, pull_request] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v1 + with: + node-version: 14.x + - run: npm ci + - run: npm run lint + env: + CI: true diff --git a/.github/workflows/nodejs-test.yml b/.github/workflows/nodejs-test.yml new file mode 100644 index 000000000..4445e1130 --- /dev/null +++ b/.github/workflows/nodejs-test.yml @@ -0,0 +1,39 @@ +name: Node.js Test + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [10.x, 12.x, 14.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - run: npm run build --if-present + - run: npm test + env: + CI: true + - name: Coveralls Parallel + uses: coverallsapp/github-action@v1.1.1 + with: + github-token: ${{ secrets.github_token }} + flag-name: run-${{ matrix.node-version }} + parallel: true + + finish: + needs: test + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@v1.1.1 + with: + github-token: ${{ secrets.github_token }} + parallel-finished: true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..f41745234 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +node_modules/ +coverage/ +lib/ diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 02026fc8b..000000000 --- a/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -language: node_js -node_js: - - lts/* -after_success: npm run coverage diff --git a/README.md b/README.md index 39d327adb..e0b594007 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![NPM version](http://img.shields.io/npm/v/htmlparser2.svg?style=flat)](https://npmjs.org/package/htmlparser2) [![Downloads](https://img.shields.io/npm/dm/htmlparser2.svg?style=flat)](https://npmjs.org/package/htmlparser2) -[![Build Status](http://img.shields.io/travis/fb55/htmlparser2/master.svg?style=flat)](http://travis-ci.org/fb55/htmlparser2) +[![Build Status](https://img.shields.io/github/workflow/status/fb55/htmlparser2/Node.js%20Test?label=tests&style=flat)](https://github.com/fb55/htmlparser2/actions?query=workflow%3A%22Node.js+Test%22) [![Coverage](http://img.shields.io/coveralls/fb55/htmlparser2.svg?style=flat)](https://coveralls.io/r/fb55/htmlparser2) A forgiving HTML/XML/RSS parser. @@ -10,7 +10,7 @@ The parser can handle streams and provides a callback interface. ## Installation - npm install htmlparser2 + npm install --save htmlparser2 A live demo of htmlparser2 is available [here](https://astexplorer.net/#/2AmVrGuGVJ). @@ -18,24 +18,21 @@ A live demo of htmlparser2 is available [here](https://astexplorer.net/#/2AmVrGu ```javascript const htmlparser2 = require("htmlparser2"); -const parser = new htmlparser2.Parser( - { - onopentag(name, attribs) { - if (name === "script" && attribs.type === "text/javascript") { - console.log("JS! Hooray!"); - } - }, - ontext(text) { - console.log("-->", text); - }, - onclosetag(tagname) { - if (tagname === "script") { - console.log("That's it?!"); - } +const parser = new htmlparser2.Parser({ + onopentag(name, attribs) { + if (name === "script" && attribs.type === "text/javascript") { + console.log("JS! Hooray!"); } }, - { decodeEntities: true } -); + ontext(text) { + console.log("-->", text); + }, + onclosetag(tagname) { + if (tagname === "script") { + console.log("That's it?!"); + } + }, +}); parser.write( "Xyz
"; + const normalScriptOutput = [ + "onopentagname: 'script'", + "onopentagend", + "onclosetag: 'script'", + "onopentagname: 'div'", + "onopentagend", + "onclosetag: 'div'", + "onend", + ]; + + tokenizer.write(normalScriptInput); + tokenizer.end(); + expect(logger.log).toEqual(normalScriptOutput); + tokenizer.reset(); + logger.log = []; + + const normalStyleInput = "
"; + const normalStyleOutput = [ + "onopentagname: 'style'", + "onopentagend", + "onclosetag: 'style'", + "onopentagname: 'div'", + "onopentagend", + "onclosetag: 'div'", + "onend", + ]; + + tokenizer.write(normalStyleInput); + tokenizer.end(); + expect(logger.log).toEqual(normalStyleOutput); + tokenizer.reset(); + logger.log = []; + + const normalTitleInput = "
"; + const normalTitleOutput = [ + "onopentagname: 'title'", + "onopentagend", + "onclosetag: 'title'", + "onopentagname: 'div'", + "onopentagend", + "onclosetag: 'div'", + "onend", + ]; + + tokenizer.write(normalTitleInput); + tokenizer.end(); + expect(logger.log).toEqual(normalTitleOutput); + tokenizer.reset(); + logger.log = []; + }); +}); diff --git a/src/Tokenizer.ts b/src/Tokenizer.ts index 3a5761898..f4c4acb01 100644 --- a/src/Tokenizer.ts +++ b/src/Tokenizer.ts @@ -6,14 +6,14 @@ import xmlMap from "entities/lib/maps/xml.json"; /** All the states the tokenizer can be in. */ const enum State { Text = 1, - BeforeTagName, //after < + BeforeTagName, // After < InTagName, InSelfClosingTag, BeforeClosingTagName, InClosingTagName, AfterClosingTagName, - //attributes + // Attributes BeforeAttributeName, InAttributeName, AfterAttributeName, @@ -22,20 +22,21 @@ const enum State { InAttributeValueSq, // ' InAttributeValueNq, - //declarations + // Declarations BeforeDeclaration, // ! InDeclaration, - //processing instructions + // Processing instructions InProcessingInstruction, // ? - //comments + // Comments BeforeComment, InComment, + InSpecialComment, AfterComment1, AfterComment2, - //cdata + // Cdata BeforeCdata1, // [ BeforeCdata2, // C BeforeCdata3, // D @@ -46,50 +47,66 @@ const enum State { AfterCdata1, // ] AfterCdata2, // ] - //special tags - BeforeSpecial, //S - BeforeSpecialEnd, //S - - BeforeScript1, //C - BeforeScript2, //R - BeforeScript3, //I - BeforeScript4, //P - BeforeScript5, //T - AfterScript1, //C - AfterScript2, //R - AfterScript3, //I - AfterScript4, //P - AfterScript5, //T - - BeforeStyle1, //T - BeforeStyle2, //Y - BeforeStyle3, //L - BeforeStyle4, //E - AfterStyle1, //T - AfterStyle2, //Y - AfterStyle3, //L - AfterStyle4, //E - - BeforeEntity, //& - BeforeNumericEntity, //# + // Special tags + BeforeSpecialS, // S + BeforeSpecialSEnd, // S + + BeforeScript1, // C + BeforeScript2, // R + BeforeScript3, // I + BeforeScript4, // P + BeforeScript5, // T + AfterScript1, // C + AfterScript2, // R + AfterScript3, // I + AfterScript4, // P + AfterScript5, // T + + BeforeStyle1, // T + BeforeStyle2, // Y + BeforeStyle3, // L + BeforeStyle4, // E + AfterStyle1, // T + AfterStyle2, // Y + AfterStyle3, // L + AfterStyle4, // E + + BeforeSpecialT, // T + BeforeSpecialTEnd, // T + BeforeTitle1, // I + BeforeTitle2, // T + BeforeTitle3, // L + BeforeTitle4, // E + AfterTitle1, // I + AfterTitle2, // T + AfterTitle3, // L + AfterTitle4, // E + + BeforeEntity, // & + BeforeNumericEntity, // # InNamedEntity, InNumericEntity, - InHexEntity //X + InHexEntity, // X } const enum Special { None = 1, Script, - Style + Style, + Title, } function whitespace(c: string): boolean { return c === " " || c === "\n" || c === "\t" || c === "\f" || c === "\r"; } +function isASCIIAlpha(c: string): boolean { + return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z"); +} + interface Callbacks { - onattribdata(value: string): void; //TODO implement the new event - onattribend(): void; + onattribdata(value: string): void; + onattribend(quote: string | undefined | null): void; onattribname(name: string): void; oncdata(data: string): void; onclosetag(name: string): void; @@ -116,16 +133,15 @@ function ifElseState(upper: string, SUCCESS: State, FAILURE: State) { t._index--; } }; - } else { - return (t: Tokenizer, c: string) => { - if (c === lower || c === upper) { - t._state = SUCCESS; - } else { - t._state = FAILURE; - t._index--; - } - }; } + return (t: Tokenizer, c: string) => { + if (c === lower || c === upper) { + t._state = SUCCESS; + } else { + t._state = FAILURE; + t._index--; + } + }; } function consumeSpecialNameChar(upper: string, NEXT_STATE: State) { @@ -136,7 +152,7 @@ function consumeSpecialNameChar(upper: string, NEXT_STATE: State) { t._state = NEXT_STATE; } else { t._state = State.InTagName; - t._index--; //consume the token again + t._index--; // Consume the token again } }; } @@ -185,6 +201,16 @@ const stateAfterStyle1 = ifElseState("Y", State.AfterStyle2, State.Text); const stateAfterStyle2 = ifElseState("L", State.AfterStyle3, State.Text); const stateAfterStyle3 = ifElseState("E", State.AfterStyle4, State.Text); +const stateBeforeSpecialT = consumeSpecialNameChar("I", State.BeforeTitle1); +const stateBeforeTitle1 = consumeSpecialNameChar("T", State.BeforeTitle2); +const stateBeforeTitle2 = consumeSpecialNameChar("L", State.BeforeTitle3); +const stateBeforeTitle3 = consumeSpecialNameChar("E", State.BeforeTitle4); + +const stateAfterSpecialTEnd = ifElseState("I", State.AfterTitle1, State.Text); +const stateAfterTitle1 = ifElseState("T", State.AfterTitle2, State.Text); +const stateAfterTitle2 = ifElseState("L", State.AfterTitle3, State.Text); +const stateAfterTitle3 = ifElseState("E", State.AfterTitle4, State.Text); + const stateBeforeEntity = ifElseState( "#", State.BeforeNumericEntity, @@ -200,228 +226,266 @@ export default class Tokenizer { /** The current state the tokenizer is in. */ _state = State.Text; /** The read buffer. */ - _buffer = ""; + private buffer = ""; /** The beginning of the section that is currently being read. */ - _sectionStart = 0; + public sectionStart = 0; /** The index within the buffer that we are currently looking at. */ _index = 0; /** * Data that has already been processed will be removed from the buffer occasionally. * `_bufferOffset` keeps track of how many characters have been removed, to make sure position information is accurate. */ - _bufferOffset = 0; + private bufferOffset = 0; /** Some behavior, eg. when decoding entities, is done while we are in another state. This keeps track of the other state type. */ - _baseState = State.Text; + private baseState = State.Text; /** For special parsing behavior inside of script and style tags. */ - _special = Special.None; + private special = Special.None; /** Indicates whether the tokenizer has been paused. */ - _running = true; + private running = true; /** Indicates whether the tokenizer has finished running / `.end` has been called. */ - _ended = false; + private ended = false; - _cbs: Callbacks; - _xmlMode: boolean; - _decodeEntities: boolean; + private readonly cbs: Callbacks; + private readonly xmlMode: boolean; + private readonly decodeEntities: boolean; constructor( options: { xmlMode?: boolean; decodeEntities?: boolean } | null, cbs: Callbacks ) { - this._cbs = cbs; - this._xmlMode = !!(options && options.xmlMode); - this._decodeEntities = !!(options && options.decodeEntities); + this.cbs = cbs; + this.xmlMode = !!options?.xmlMode; + this.decodeEntities = options?.decodeEntities ?? true; } - reset() { + public reset(): void { this._state = State.Text; - this._buffer = ""; - this._sectionStart = 0; + this.buffer = ""; + this.sectionStart = 0; this._index = 0; - this._bufferOffset = 0; - this._baseState = State.Text; - this._special = Special.None; - this._running = true; - this._ended = false; + this.bufferOffset = 0; + this.baseState = State.Text; + this.special = Special.None; + this.running = true; + this.ended = false; } - _stateText(c: string) { + public write(chunk: string): void { + if (this.ended) this.cbs.onerror(Error(".write() after done!")); + this.buffer += chunk; + this.parse(); + } + + public end(chunk?: string): void { + if (this.ended) this.cbs.onerror(Error(".end() after done!")); + if (chunk) this.write(chunk); + this.ended = true; + if (this.running) this.finish(); + } + + public pause(): void { + this.running = false; + } + + public resume(): void { + this.running = true; + if (this._index < this.buffer.length) { + this.parse(); + } + if (this.ended) { + this.finish(); + } + } + + /** + * The current index within all of the written data. + */ + public getAbsoluteIndex(): number { + return this.bufferOffset + this._index; + } + + private stateText(c: string) { if (c === "<") { - if (this._index > this._sectionStart) { - this._cbs.ontext(this._getSection()); + if (this._index > this.sectionStart) { + this.cbs.ontext(this.getSection()); } this._state = State.BeforeTagName; - this._sectionStart = this._index; + this.sectionStart = this._index; } else if ( - this._decodeEntities && - this._special === Special.None && + this.decodeEntities && + this.special === Special.None && c === "&" ) { - if (this._index > this._sectionStart) { - this._cbs.ontext(this._getSection()); + if (this._index > this.sectionStart) { + this.cbs.ontext(this.getSection()); } - this._baseState = State.Text; + this.baseState = State.Text; this._state = State.BeforeEntity; - this._sectionStart = this._index; + this.sectionStart = this._index; } } - _stateBeforeTagName(c: string) { + private stateBeforeTagName(c: string) { if (c === "/") { this._state = State.BeforeClosingTagName; } else if (c === "<") { - this._cbs.ontext(this._getSection()); - this._sectionStart = this._index; + this.cbs.ontext(this.getSection()); + this.sectionStart = this._index; } else if ( c === ">" || - this._special !== Special.None || + this.special !== Special.None || whitespace(c) ) { this._state = State.Text; } else if (c === "!") { this._state = State.BeforeDeclaration; - this._sectionStart = this._index + 1; + this.sectionStart = this._index + 1; } else if (c === "?") { this._state = State.InProcessingInstruction; - this._sectionStart = this._index + 1; + this.sectionStart = this._index + 1; + } else if (!isASCIIAlpha(c)) { + this._state = State.Text; } else { this._state = - !this._xmlMode && (c === "s" || c === "S") - ? State.BeforeSpecial + !this.xmlMode && (c === "s" || c === "S") + ? State.BeforeSpecialS + : !this.xmlMode && (c === "t" || c === "T") + ? State.BeforeSpecialT : State.InTagName; - this._sectionStart = this._index; + this.sectionStart = this._index; } } - _stateInTagName(c: string) { + private stateInTagName(c: string) { if (c === "/" || c === ">" || whitespace(c)) { - this._emitToken("onopentagname"); + this.emitToken("onopentagname"); this._state = State.BeforeAttributeName; this._index--; } } - _stateBeforeClosingTagName(c: string) { + private stateBeforeClosingTagName(c: string) { if (whitespace(c)) { - // ignore + // Ignore } else if (c === ">") { this._state = State.Text; - } else if (this._special !== Special.None) { + } else if (this.special !== Special.None) { if (c === "s" || c === "S") { - this._state = State.BeforeSpecialEnd; + this._state = State.BeforeSpecialSEnd; + } else if (c === "t" || c === "T") { + this._state = State.BeforeSpecialTEnd; } else { this._state = State.Text; this._index--; } + } else if (!isASCIIAlpha(c)) { + this._state = State.InSpecialComment; + this.sectionStart = this._index; } else { this._state = State.InClosingTagName; - this._sectionStart = this._index; + this.sectionStart = this._index; } } - _stateInClosingTagName(c: string) { + private stateInClosingTagName(c: string) { if (c === ">" || whitespace(c)) { - this._emitToken("onclosetag"); + this.emitToken("onclosetag"); this._state = State.AfterClosingTagName; this._index--; } } - _stateAfterClosingTagName(c: string) { - //skip everything until ">" + private stateAfterClosingTagName(c: string) { + // Skip everything until ">" if (c === ">") { this._state = State.Text; - this._sectionStart = this._index + 1; + this.sectionStart = this._index + 1; } } - _stateBeforeAttributeName(c: string) { + private stateBeforeAttributeName(c: string) { if (c === ">") { - this._cbs.onopentagend(); + this.cbs.onopentagend(); this._state = State.Text; - this._sectionStart = this._index + 1; + this.sectionStart = this._index + 1; } else if (c === "/") { this._state = State.InSelfClosingTag; } else if (!whitespace(c)) { this._state = State.InAttributeName; - this._sectionStart = this._index; + this.sectionStart = this._index; } } - _stateInSelfClosingTag(c: string) { + private stateInSelfClosingTag(c: string) { if (c === ">") { - this._cbs.onselfclosingtag(); + this.cbs.onselfclosingtag(); this._state = State.Text; - this._sectionStart = this._index + 1; + this.sectionStart = this._index + 1; + this.special = Special.None; // Reset special state, in case of self-closing special tags } else if (!whitespace(c)) { this._state = State.BeforeAttributeName; this._index--; } } - _stateInAttributeName(c: string) { + private stateInAttributeName(c: string) { if (c === "=" || c === "/" || c === ">" || whitespace(c)) { - this._cbs.onattribname(this._getSection()); - this._sectionStart = -1; + this.cbs.onattribname(this.getSection()); + this.sectionStart = -1; this._state = State.AfterAttributeName; this._index--; } } - _stateAfterAttributeName(c: string) { + private stateAfterAttributeName(c: string) { if (c === "=") { this._state = State.BeforeAttributeValue; } else if (c === "/" || c === ">") { - this._cbs.onattribend(); + this.cbs.onattribend(undefined); this._state = State.BeforeAttributeName; this._index--; } else if (!whitespace(c)) { - this._cbs.onattribend(); + this.cbs.onattribend(undefined); this._state = State.InAttributeName; - this._sectionStart = this._index; + this.sectionStart = this._index; } } - _stateBeforeAttributeValue(c: string) { + private stateBeforeAttributeValue(c: string) { if (c === '"') { this._state = State.InAttributeValueDq; - this._sectionStart = this._index + 1; + this.sectionStart = this._index + 1; } else if (c === "'") { this._state = State.InAttributeValueSq; - this._sectionStart = this._index + 1; + this.sectionStart = this._index + 1; } else if (!whitespace(c)) { this._state = State.InAttributeValueNq; - this._sectionStart = this._index; - this._index--; //reconsume token + this.sectionStart = this._index; + this._index--; // Reconsume token } } - _stateInAttributeValueDoubleQuotes(c: string) { - if (c === '"') { - this._emitToken("onattribdata"); - this._cbs.onattribend(); + private handleInAttributeValue(c: string, quote: string) { + if (c === quote) { + this.emitToken("onattribdata"); + this.cbs.onattribend(quote); this._state = State.BeforeAttributeName; - } else if (this._decodeEntities && c === "&") { - this._emitToken("onattribdata"); - this._baseState = this._state; + } else if (this.decodeEntities && c === "&") { + this.emitToken("onattribdata"); + this.baseState = this._state; this._state = State.BeforeEntity; - this._sectionStart = this._index; + this.sectionStart = this._index; } } - _stateInAttributeValueSingleQuotes(c: string) { - if (c === "'") { - this._emitToken("onattribdata"); - this._cbs.onattribend(); - this._state = State.BeforeAttributeName; - } else if (this._decodeEntities && c === "&") { - this._emitToken("onattribdata"); - this._baseState = this._state; - this._state = State.BeforeEntity; - this._sectionStart = this._index; - } + private stateInAttributeValueDoubleQuotes(c: string) { + this.handleInAttributeValue(c, '"'); + } + private stateInAttributeValueSingleQuotes(c: string) { + this.handleInAttributeValue(c, "'"); } - _stateInAttributeValueNoQuotes(c: string) { + private stateInAttributeValueNoQuotes(c: string) { if (whitespace(c) || c === ">") { - this._emitToken("onattribdata"); - this._cbs.onattribend(); + this.emitToken("onattribdata"); + this.cbs.onattribend(null); this._state = State.BeforeAttributeName; this._index--; - } else if (this._decodeEntities && c === "&") { - this._emitToken("onattribdata"); - this._baseState = this._state; + } else if (this.decodeEntities && c === "&") { + this.emitToken("onattribdata"); + this.baseState = this._state; this._state = State.BeforeEntity; - this._sectionStart = this._index; + this.sectionStart = this._index; } } - _stateBeforeDeclaration(c: string) { + private stateBeforeDeclaration(c: string) { this._state = c === "[" ? State.BeforeCdata1 @@ -429,317 +493,304 @@ export default class Tokenizer { ? State.BeforeComment : State.InDeclaration; } - _stateInDeclaration(c: string) { + private stateInDeclaration(c: string) { if (c === ">") { - this._cbs.ondeclaration(this._getSection()); + this.cbs.ondeclaration(this.getSection()); this._state = State.Text; - this._sectionStart = this._index + 1; + this.sectionStart = this._index + 1; } } - _stateInProcessingInstruction(c: string) { + private stateInProcessingInstruction(c: string) { if (c === ">") { - this._cbs.onprocessinginstruction(this._getSection()); + this.cbs.onprocessinginstruction(this.getSection()); this._state = State.Text; - this._sectionStart = this._index + 1; + this.sectionStart = this._index + 1; } } - _stateBeforeComment(c: string) { + private stateBeforeComment(c: string) { if (c === "-") { this._state = State.InComment; - this._sectionStart = this._index + 1; + this.sectionStart = this._index + 1; } else { this._state = State.InDeclaration; } } - _stateInComment(c: string) { + private stateInComment(c: string) { if (c === "-") this._state = State.AfterComment1; } - _stateAfterComment1(c: string) { + private stateInSpecialComment(c: string) { + if (c === ">") { + this.cbs.oncomment( + this.buffer.substring(this.sectionStart, this._index) + ); + this._state = State.Text; + this.sectionStart = this._index + 1; + } + } + private stateAfterComment1(c: string) { if (c === "-") { this._state = State.AfterComment2; } else { this._state = State.InComment; } } - _stateAfterComment2(c: string) { + private stateAfterComment2(c: string) { if (c === ">") { - //remove 2 trailing chars - this._cbs.oncomment( - this._buffer.substring(this._sectionStart, this._index - 2) + // Remove 2 trailing chars + this.cbs.oncomment( + this.buffer.substring(this.sectionStart, this._index - 2) ); this._state = State.Text; - this._sectionStart = this._index + 1; + this.sectionStart = this._index + 1; } else if (c !== "-") { this._state = State.InComment; } - // else: stay in AFTER_COMMENT_2 (`--->`) + // Else: stay in AFTER_COMMENT_2 (`--->`) } - _stateBeforeCdata6(c: string) { + private stateBeforeCdata6(c: string) { if (c === "[") { this._state = State.InCdata; - this._sectionStart = this._index + 1; + this.sectionStart = this._index + 1; } else { this._state = State.InDeclaration; this._index--; } } - _stateInCdata(c: string) { + private stateInCdata(c: string) { if (c === "]") this._state = State.AfterCdata1; } - _stateAfterCdata1(c: string) { + private stateAfterCdata1(c: string) { if (c === "]") this._state = State.AfterCdata2; else this._state = State.InCdata; } - _stateAfterCdata2(c: string) { + private stateAfterCdata2(c: string) { if (c === ">") { - //remove 2 trailing chars - this._cbs.oncdata( - this._buffer.substring(this._sectionStart, this._index - 2) + // Remove 2 trailing chars + this.cbs.oncdata( + this.buffer.substring(this.sectionStart, this._index - 2) ); this._state = State.Text; - this._sectionStart = this._index + 1; + this.sectionStart = this._index + 1; } else if (c !== "]") { this._state = State.InCdata; } - //else: stay in AFTER_CDATA_2 (`]]]>`) + // Else: stay in AFTER_CDATA_2 (`]]]>`) } - _stateBeforeSpecial(c: string) { + private stateBeforeSpecialS(c: string) { if (c === "c" || c === "C") { this._state = State.BeforeScript1; } else if (c === "t" || c === "T") { this._state = State.BeforeStyle1; } else { this._state = State.InTagName; - this._index--; //consume the token again + this._index--; // Consume the token again } } - _stateBeforeSpecialEnd(c: string) { - if (this._special === Special.Script && (c === "c" || c === "C")) { + private stateBeforeSpecialSEnd(c: string) { + if (this.special === Special.Script && (c === "c" || c === "C")) { this._state = State.AfterScript1; - } else if ( - this._special === Special.Style && - (c === "t" || c === "T") - ) { + } else if (this.special === Special.Style && (c === "t" || c === "T")) { this._state = State.AfterStyle1; } else this._state = State.Text; } - _stateBeforeScript5(c: string) { + private stateBeforeSpecialLast(c: string, special: Special) { if (c === "/" || c === ">" || whitespace(c)) { - this._special = Special.Script; + this.special = special; } this._state = State.InTagName; - this._index--; //consume the token again + this._index--; // Consume the token again } - _stateAfterScript5(c: string) { + private stateAfterSpecialLast(c: string, sectionStartOffset: number) { if (c === ">" || whitespace(c)) { - this._special = Special.None; + this.special = Special.None; this._state = State.InClosingTagName; - this._sectionStart = this._index - 6; - this._index--; //reconsume the token + this.sectionStart = this._index - sectionStartOffset; + this._index--; // Reconsume the token } else this._state = State.Text; } - _stateBeforeStyle4(c: string) { - if (c === "/" || c === ">" || whitespace(c)) { - this._special = Special.Style; - } - this._state = State.InTagName; - this._index--; //consume the token again - } - _stateAfterStyle4(c: string) { - if (c === ">" || whitespace(c)) { - this._special = Special.None; - this._state = State.InClosingTagName; - this._sectionStart = this._index - 5; - this._index--; //reconsume the token - } else this._state = State.Text; - } - //for entities terminated with a semicolon - _parseNamedEntityStrict() { - //offset = 1 - if (this._sectionStart + 1 < this._index) { - const entity = this._buffer.substring( - this._sectionStart + 1, - this._index - ), - map = this._xmlMode ? xmlMap : entityMap; + // For entities terminated with a semicolon + private parseFixedEntity( + map: Record = this.xmlMode ? xmlMap : entityMap + ) { + // Offset = 1 + if (this.sectionStart + 1 < this._index) { + const entity = this.buffer.substring( + this.sectionStart + 1, + this._index + ); if (Object.prototype.hasOwnProperty.call(map, entity)) { - // @ts-ignore - this._emitPartial(map[entity]); - this._sectionStart = this._index + 1; + this.emitPartial(map[entity]); + this.sectionStart = this._index + 1; } } } - //parses legacy entities (without trailing semicolon) - _parseLegacyEntity() { - const start = this._sectionStart + 1; - let limit = this._index - start; - if (limit > 6) limit = 6; // The max length of legacy entities is 6 + // Parses legacy entities (without trailing semicolon) + private parseLegacyEntity() { + const start = this.sectionStart + 1; + // The max length of legacy entities is 6 + let limit = Math.min(this._index - start, 6); while (limit >= 2) { // The min length of legacy entities is 2 - const entity = this._buffer.substr(start, limit); + const entity = this.buffer.substr(start, limit); if (Object.prototype.hasOwnProperty.call(legacyMap, entity)) { - // @ts-ignore - this._emitPartial(legacyMap[entity]); - this._sectionStart += limit + 1; + this.emitPartial((legacyMap as Record)[entity]); + this.sectionStart += limit + 1; return; - } else { - limit--; } + limit--; } } - _stateInNamedEntity(c: string) { + private stateInNamedEntity(c: string) { if (c === ";") { - this._parseNamedEntityStrict(); - if (this._sectionStart + 1 < this._index && !this._xmlMode) { - this._parseLegacyEntity(); + this.parseFixedEntity(); + // Retry as legacy entity if entity wasn't parsed + if ( + this.baseState === State.Text && + this.sectionStart + 1 < this._index && + !this.xmlMode + ) { + this.parseLegacyEntity(); } - this._state = this._baseState; - } else if ( - (c < "a" || c > "z") && - (c < "A" || c > "Z") && - (c < "0" || c > "9") - ) { - if (this._xmlMode || this._sectionStart + 1 === this._index) { - // ignore - } else if (this._baseState !== State.Text) { + this._state = this.baseState; + } else if ((c < "0" || c > "9") && !isASCIIAlpha(c)) { + if (this.xmlMode || this.sectionStart + 1 === this._index) { + // Ignore + } else if (this.baseState !== State.Text) { if (c !== "=") { - this._parseNamedEntityStrict(); + // Parse as legacy entity, without allowing additional characters. + this.parseFixedEntity(legacyMap); } } else { - this._parseLegacyEntity(); + this.parseLegacyEntity(); } - this._state = this._baseState; + this._state = this.baseState; this._index--; } } - _decodeNumericEntity(offset: number, base: number) { - const sectionStart = this._sectionStart + offset; + private decodeNumericEntity(offset: number, base: number, strict: boolean) { + const sectionStart = this.sectionStart + offset; if (sectionStart !== this._index) { - //parse entity - const entity = this._buffer.substring(sectionStart, this._index); + // Parse entity + const entity = this.buffer.substring(sectionStart, this._index); const parsed = parseInt(entity, base); - this._emitPartial(decodeCodePoint(parsed)); - this._sectionStart = this._index; - } else { - this._sectionStart--; + this.emitPartial(decodeCodePoint(parsed)); + this.sectionStart = strict ? this._index + 1 : this._index; } - this._state = this._baseState; + this._state = this.baseState; } - _stateInNumericEntity(c: string) { + private stateInNumericEntity(c: string) { if (c === ";") { - this._decodeNumericEntity(2, 10); - this._sectionStart++; + this.decodeNumericEntity(2, 10, true); } else if (c < "0" || c > "9") { - if (!this._xmlMode) { - this._decodeNumericEntity(2, 10); + if (!this.xmlMode) { + this.decodeNumericEntity(2, 10, false); } else { - this._state = this._baseState; + this._state = this.baseState; } this._index--; } } - _stateInHexEntity(c: string) { + private stateInHexEntity(c: string) { if (c === ";") { - this._decodeNumericEntity(3, 16); - this._sectionStart++; + this.decodeNumericEntity(3, 16, true); } else if ( (c < "a" || c > "f") && (c < "A" || c > "F") && (c < "0" || c > "9") ) { - if (!this._xmlMode) { - this._decodeNumericEntity(3, 16); + if (!this.xmlMode) { + this.decodeNumericEntity(3, 16, false); } else { - this._state = this._baseState; + this._state = this.baseState; } this._index--; } } - _cleanup() { - if (this._sectionStart < 0) { - this._buffer = ""; - this._bufferOffset += this._index; + private cleanup() { + if (this.sectionStart < 0) { + this.buffer = ""; + this.bufferOffset += this._index; this._index = 0; - } else if (this._running) { + } else if (this.running) { if (this._state === State.Text) { - if (this._sectionStart !== this._index) { - this._cbs.ontext(this._buffer.substr(this._sectionStart)); + if (this.sectionStart !== this._index) { + this.cbs.ontext(this.buffer.substr(this.sectionStart)); } - this._buffer = ""; - this._bufferOffset += this._index; + this.buffer = ""; + this.bufferOffset += this._index; this._index = 0; - } else if (this._sectionStart === this._index) { - //the section just started - this._buffer = ""; - this._bufferOffset += this._index; + } else if (this.sectionStart === this._index) { + // The section just started + this.buffer = ""; + this.bufferOffset += this._index; this._index = 0; } else { - //remove everything unnecessary - this._buffer = this._buffer.substr(this._sectionStart); - this._index -= this._sectionStart; - this._bufferOffset += this._sectionStart; + // Remove everything unnecessary + this.buffer = this.buffer.substr(this.sectionStart); + this._index -= this.sectionStart; + this.bufferOffset += this.sectionStart; } - this._sectionStart = 0; + this.sectionStart = 0; } } - //TODO make events conditional - write(chunk: string) { - if (this._ended) this._cbs.onerror(Error(".write() after done!")); - this._buffer += chunk; - this._parse(); - } - - // Iterates through the buffer, calling the function corresponding to the current state. - // States that are more likely to be hit are higher up, as a performance improvement. - _parse() { - while (this._index < this._buffer.length && this._running) { - const c = this._buffer.charAt(this._index); + /** + * Iterates through the buffer, calling the function corresponding to the current state. + * + * States that are more likely to be hit are higher up, as a performance improvement. + */ + private parse() { + while (this._index < this.buffer.length && this.running) { + const c = this.buffer.charAt(this._index); if (this._state === State.Text) { - this._stateText(c); + this.stateText(c); } else if (this._state === State.InAttributeValueDq) { - this._stateInAttributeValueDoubleQuotes(c); + this.stateInAttributeValueDoubleQuotes(c); } else if (this._state === State.InAttributeName) { - this._stateInAttributeName(c); + this.stateInAttributeName(c); } else if (this._state === State.InComment) { - this._stateInComment(c); + this.stateInComment(c); + } else if (this._state === State.InSpecialComment) { + this.stateInSpecialComment(c); } else if (this._state === State.BeforeAttributeName) { - this._stateBeforeAttributeName(c); + this.stateBeforeAttributeName(c); } else if (this._state === State.InTagName) { - this._stateInTagName(c); + this.stateInTagName(c); } else if (this._state === State.InClosingTagName) { - this._stateInClosingTagName(c); + this.stateInClosingTagName(c); } else if (this._state === State.BeforeTagName) { - this._stateBeforeTagName(c); + this.stateBeforeTagName(c); } else if (this._state === State.AfterAttributeName) { - this._stateAfterAttributeName(c); + this.stateAfterAttributeName(c); } else if (this._state === State.InAttributeValueSq) { - this._stateInAttributeValueSingleQuotes(c); + this.stateInAttributeValueSingleQuotes(c); } else if (this._state === State.BeforeAttributeValue) { - this._stateBeforeAttributeValue(c); + this.stateBeforeAttributeValue(c); } else if (this._state === State.BeforeClosingTagName) { - this._stateBeforeClosingTagName(c); + this.stateBeforeClosingTagName(c); } else if (this._state === State.AfterClosingTagName) { - this._stateAfterClosingTagName(c); - } else if (this._state === State.BeforeSpecial) { - this._stateBeforeSpecial(c); + this.stateAfterClosingTagName(c); + } else if (this._state === State.BeforeSpecialS) { + this.stateBeforeSpecialS(c); } else if (this._state === State.AfterComment1) { - this._stateAfterComment1(c); + this.stateAfterComment1(c); } else if (this._state === State.InAttributeValueNq) { - this._stateInAttributeValueNoQuotes(c); + this.stateInAttributeValueNoQuotes(c); } else if (this._state === State.InSelfClosingTag) { - this._stateInSelfClosingTag(c); + this.stateInSelfClosingTag(c); } else if (this._state === State.InDeclaration) { - this._stateInDeclaration(c); + this.stateInDeclaration(c); } else if (this._state === State.BeforeDeclaration) { - this._stateBeforeDeclaration(c); + this.stateBeforeDeclaration(c); } else if (this._state === State.AfterComment2) { - this._stateAfterComment2(c); + this.stateAfterComment2(c); } else if (this._state === State.BeforeComment) { - this._stateBeforeComment(c); - } else if (this._state === State.BeforeSpecialEnd) { - this._stateBeforeSpecialEnd(c); + this.stateBeforeComment(c); + } else if (this._state === State.BeforeSpecialSEnd) { + this.stateBeforeSpecialSEnd(c); + } else if (this._state === State.BeforeSpecialTEnd) { + stateAfterSpecialTEnd(this, c); } else if (this._state === State.AfterScript1) { stateAfterScript1(this, c); } else if (this._state === State.AfterScript2) { @@ -755,21 +806,21 @@ export default class Tokenizer { } else if (this._state === State.BeforeScript4) { stateBeforeScript4(this, c); } else if (this._state === State.BeforeScript5) { - this._stateBeforeScript5(c); + this.stateBeforeSpecialLast(c, Special.Script); } else if (this._state === State.AfterScript4) { stateAfterScript4(this, c); } else if (this._state === State.AfterScript5) { - this._stateAfterScript5(c); + this.stateAfterSpecialLast(c, 6); } else if (this._state === State.BeforeStyle1) { stateBeforeStyle1(this, c); } else if (this._state === State.InCdata) { - this._stateInCdata(c); + this.stateInCdata(c); } else if (this._state === State.BeforeStyle2) { stateBeforeStyle2(this, c); } else if (this._state === State.BeforeStyle3) { stateBeforeStyle3(this, c); } else if (this._state === State.BeforeStyle4) { - this._stateBeforeStyle4(c); + this.stateBeforeSpecialLast(c, Special.Style); } else if (this._state === State.AfterStyle1) { stateAfterStyle1(this, c); } else if (this._state === State.AfterStyle2) { @@ -777,11 +828,29 @@ export default class Tokenizer { } else if (this._state === State.AfterStyle3) { stateAfterStyle3(this, c); } else if (this._state === State.AfterStyle4) { - this._stateAfterStyle4(c); + this.stateAfterSpecialLast(c, 5); + } else if (this._state === State.BeforeSpecialT) { + stateBeforeSpecialT(this, c); + } else if (this._state === State.BeforeTitle1) { + stateBeforeTitle1(this, c); + } else if (this._state === State.BeforeTitle2) { + stateBeforeTitle2(this, c); + } else if (this._state === State.BeforeTitle3) { + stateBeforeTitle3(this, c); + } else if (this._state === State.BeforeTitle4) { + this.stateBeforeSpecialLast(c, Special.Title); + } else if (this._state === State.AfterTitle1) { + stateAfterTitle1(this, c); + } else if (this._state === State.AfterTitle2) { + stateAfterTitle2(this, c); + } else if (this._state === State.AfterTitle3) { + stateAfterTitle3(this, c); + } else if (this._state === State.AfterTitle4) { + this.stateAfterSpecialLast(c, 5); } else if (this._state === State.InProcessingInstruction) { - this._stateInProcessingInstruction(c); + this.stateInProcessingInstruction(c); } else if (this._state === State.InNamedEntity) { - this._stateInNamedEntity(c); + this.stateInNamedEntity(c); } else if (this._state === State.BeforeCdata1) { stateBeforeCdata1(this, c); } else if (this._state === State.BeforeEntity) { @@ -791,84 +860,69 @@ export default class Tokenizer { } else if (this._state === State.BeforeCdata3) { stateBeforeCdata3(this, c); } else if (this._state === State.AfterCdata1) { - this._stateAfterCdata1(c); + this.stateAfterCdata1(c); } else if (this._state === State.AfterCdata2) { - this._stateAfterCdata2(c); + this.stateAfterCdata2(c); } else if (this._state === State.BeforeCdata4) { stateBeforeCdata4(this, c); } else if (this._state === State.BeforeCdata5) { stateBeforeCdata5(this, c); } else if (this._state === State.BeforeCdata6) { - this._stateBeforeCdata6(c); + this.stateBeforeCdata6(c); } else if (this._state === State.InHexEntity) { - this._stateInHexEntity(c); + this.stateInHexEntity(c); } else if (this._state === State.InNumericEntity) { - this._stateInNumericEntity(c); + this.stateInNumericEntity(c); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition } else if (this._state === State.BeforeNumericEntity) { stateBeforeNumericEntity(this, c); } else { - this._cbs.onerror(Error("unknown _state"), this._state); + this.cbs.onerror(Error("unknown _state"), this._state); } this._index++; } - this._cleanup(); - } - pause() { - this._running = false; + this.cleanup(); } - resume() { - this._running = true; - if (this._index < this._buffer.length) { - this._parse(); - } - if (this._ended) { - this._finish(); - } - } - end(chunk?: string) { - if (this._ended) this._cbs.onerror(Error(".end() after done!")); - if (chunk) this.write(chunk); - this._ended = true; - if (this._running) this._finish(); - } - _finish() { - //if there is remaining data, emit it in a reasonable way - if (this._sectionStart < this._index) { - this._handleTrailingData(); + + private finish() { + // If there is remaining data, emit it in a reasonable way + if (this.sectionStart < this._index) { + this.handleTrailingData(); } - this._cbs.onend(); + this.cbs.onend(); } - _handleTrailingData() { - const data = this._buffer.substr(this._sectionStart); + + private handleTrailingData() { + const data = this.buffer.substr(this.sectionStart); if ( this._state === State.InCdata || this._state === State.AfterCdata1 || this._state === State.AfterCdata2 ) { - this._cbs.oncdata(data); + this.cbs.oncdata(data); } else if ( this._state === State.InComment || this._state === State.AfterComment1 || this._state === State.AfterComment2 ) { - this._cbs.oncomment(data); - } else if (this._state === State.InNamedEntity && !this._xmlMode) { - this._parseLegacyEntity(); - if (this._sectionStart < this._index) { - this._state = this._baseState; - this._handleTrailingData(); + this.cbs.oncomment(data); + } else if (this._state === State.InNamedEntity && !this.xmlMode) { + this.parseLegacyEntity(); + if (this.sectionStart < this._index) { + this._state = this.baseState; + this.handleTrailingData(); } - } else if (this._state === State.InNumericEntity && !this._xmlMode) { - this._decodeNumericEntity(2, 10); - if (this._sectionStart < this._index) { - this._state = this._baseState; - this._handleTrailingData(); + } else if (this._state === State.InNumericEntity && !this.xmlMode) { + this.decodeNumericEntity(2, 10, false); + if (this.sectionStart < this._index) { + this._state = this.baseState; + this.handleTrailingData(); } - } else if (this._state === State.InHexEntity && !this._xmlMode) { - this._decodeNumericEntity(3, 16); - if (this._sectionStart < this._index) { - this._state = this._baseState; - this._handleTrailingData(); + } else if (this._state === State.InHexEntity && !this.xmlMode) { + this.decodeNumericEntity(3, 16, false); + if (this.sectionStart < this._index) { + this._state = this.baseState; + this.handleTrailingData(); } } else if ( this._state !== State.InTagName && @@ -881,26 +935,26 @@ export default class Tokenizer { this._state !== State.InAttributeValueNq && this._state !== State.InClosingTagName ) { - this._cbs.ontext(data); + this.cbs.ontext(data); } - //else, ignore remaining data - //TODO add a way to remove current tag + /* + * Else, ignore remaining data + * TODO add a way to remove current tag + */ } - getAbsoluteIndex(): number { - return this._bufferOffset + this._index; - } - _getSection(): string { - return this._buffer.substring(this._sectionStart, this._index); + + private getSection(): string { + return this.buffer.substring(this.sectionStart, this._index); } - _emitToken(name: "onopentagname" | "onclosetag" | "onattribdata") { - this._cbs[name](this._getSection()); - this._sectionStart = -1; + private emitToken(name: "onopentagname" | "onclosetag" | "onattribdata") { + this.cbs[name](this.getSection()); + this.sectionStart = -1; } - _emitPartial(value: string) { - if (this._baseState !== State.Text) { - this._cbs.onattribdata(value); //TODO implement the new event + private emitPartial(value: string) { + if (this.baseState !== State.Text) { + this.cbs.onattribdata(value); // TODO implement the new event } else { - this._cbs.ontext(value); + this.cbs.ontext(value); } } } diff --git a/src/WritableStream.spec.ts b/src/WritableStream.spec.ts index d0d31f643..e0f6fa7ec 100644 --- a/src/WritableStream.spec.ts +++ b/src/WritableStream.spec.ts @@ -7,6 +7,7 @@ describe("WritableStream", () => { stream.write(Buffer.from([0xe2, 0x82])); stream.write(Buffer.from([0xac])); + stream.write(""); stream.end(); expect(ontext).toBeCalledWith("€"); diff --git a/src/WritableStream.ts b/src/WritableStream.ts index 670020cd1..baa204e38 100644 --- a/src/WritableStream.ts +++ b/src/WritableStream.ts @@ -13,21 +13,22 @@ function isBuffer(_chunk: string | Buffer, encoding: string): _chunk is Buffer { * @see Parser */ export class WritableStream extends Writable { - _parser: Parser; - _decoder = new StringDecoder(); + private readonly _parser: Parser; + private readonly _decoder = new StringDecoder(); constructor(cbs: Partial, options?: ParserOptions) { super({ decodeStrings: false }); this._parser = new Parser(cbs, options); } - _write(chunk: string | Buffer, encoding: string, cb: () => void) { - if (isBuffer(chunk, encoding)) chunk = this._decoder.write(chunk); - this._parser.write(chunk); + _write(chunk: string | Buffer, encoding: string, cb: () => void): void { + this._parser.write( + isBuffer(chunk, encoding) ? this._decoder.write(chunk) : chunk + ); cb(); } - _final(cb: () => void) { + _final(cb: () => void): void { this._parser.end(this._decoder.end()); cb(); } diff --git a/src/__fixtures__/Documents/Atom_Example.xml b/src/__fixtures__/Documents/Atom_Example.xml index f83638030..c19b0d36d 100644 --- a/src/__fixtures__/Documents/Atom_Example.xml +++ b/src/__fixtures__/Documents/Atom_Example.xml @@ -22,4 +22,6 @@

Some content.

+ + diff --git a/src/__fixtures__/Documents/RSS_Example.xml b/src/__fixtures__/Documents/RSS_Example.xml index 0d1fde875..18563449e 100644 --- a/src/__fixtures__/Documents/RSS_Example.xml +++ b/src/__fixtures__/Documents/RSS_Example.xml @@ -43,6 +43,7 @@ Tue, 20 May 2003 08:56:02 GMT http://liftoff.msfc.nasa.gov/2003/05/20.html#item570 + \ No newline at end of file diff --git a/src/__fixtures__/Events/01-simple.json b/src/__fixtures__/Events/01-simple.json index 1efe6a4f4..7f5fd154a 100644 --- a/src/__fixtures__/Events/01-simple.json +++ b/src/__fixtures__/Events/01-simple.json @@ -1,9 +1,5 @@ { "name": "simple", - "options": { - "handler": {}, - "parser": {} - }, "html": "

adsf

", "expected": [ { @@ -12,7 +8,7 @@ }, { "event": "attribute", - "data": ["class", "test"] + "data": ["class", "test", null] }, { "event": "opentag", diff --git a/src/__fixtures__/Events/02-template.json b/src/__fixtures__/Events/02-template.json index 76447a4e7..1b77db1b6 100644 --- a/src/__fixtures__/Events/02-template.json +++ b/src/__fixtures__/Events/02-template.json @@ -1,9 +1,5 @@ { "name": "Template script tags", - "options": { - "handler": {}, - "parser": {} - }, "html": "

", "expected": [ { @@ -20,7 +16,7 @@ }, { "event": "attribute", - "data": ["type", "text/template"] + "data": ["type", "text/template", "\""] }, { "event": "opentag", diff --git a/src/__fixtures__/Events/03-lowercase_tags.json b/src/__fixtures__/Events/03-lowercase_tags.json index dafa4eedc..c65665a3b 100644 --- a/src/__fixtures__/Events/03-lowercase_tags.json +++ b/src/__fixtures__/Events/03-lowercase_tags.json @@ -1,7 +1,6 @@ { "name": "Lowercase tags", "options": { - "handler": {}, "parser": { "lowerCaseTags": true } @@ -14,7 +13,7 @@ }, { "event": "attribute", - "data": ["class", "test"] + "data": ["class", "test", null] }, { "event": "opentag", diff --git a/src/__fixtures__/Events/04-cdata.json b/src/__fixtures__/Events/04-cdata.json index c4c655535..2e3d1ac3c 100644 --- a/src/__fixtures__/Events/04-cdata.json +++ b/src/__fixtures__/Events/04-cdata.json @@ -1,7 +1,6 @@ { "name": "CDATA", "options": { - "handler": {}, "parser": { "xmlMode": true } }, "html": "<> fo]]>", diff --git a/src/__fixtures__/Events/05-cdata-special.json b/src/__fixtures__/Events/05-cdata-special.json index d23adf415..977b7bd92 100644 --- a/src/__fixtures__/Events/05-cdata-special.json +++ b/src/__fixtures__/Events/05-cdata-special.json @@ -1,9 +1,5 @@ { "name": "CDATA (inside special)", - "options": { - "handler": {}, - "parser": {} - }, "html": "", "expected": [ { diff --git a/src/__fixtures__/Events/06-leading-lt.json b/src/__fixtures__/Events/06-leading-lt.json index f99044f09..101800b59 100644 --- a/src/__fixtures__/Events/06-leading-lt.json +++ b/src/__fixtures__/Events/06-leading-lt.json @@ -1,9 +1,5 @@ { "name": "leading lt", - "options": { - "handler": {}, - "parser": {} - }, "html": ">a>", "expected": [ { diff --git a/src/__fixtures__/Events/07-self-closing.json b/src/__fixtures__/Events/07-self-closing.json index 6cbabbfbb..b5ba22591 100644 --- a/src/__fixtures__/Events/07-self-closing.json +++ b/src/__fixtures__/Events/07-self-closing.json @@ -1,9 +1,5 @@ { "name": "Self-closing tags", - "options": { - "handler": {}, - "parser": {} - }, "html": "Foo
", "expected": [ { @@ -12,7 +8,7 @@ }, { "event": "attribute", - "data": ["href", "http://test.com/"] + "data": ["href", "http://test.com/", null] }, { "event": "opentag", diff --git a/src/__fixtures__/Events/08-implicit-close-tags.json b/src/__fixtures__/Events/08-implicit-close-tags.json index f3b56ca21..01d8af217 100644 --- a/src/__fixtures__/Events/08-implicit-close-tags.json +++ b/src/__fixtures__/Events/08-implicit-close-tags.json @@ -1,17 +1,16 @@ { "name": "Implicit close tags", - "options": {}, "html": "
  1. TH

    Heading

    Div
    Div2
  2. Heading 2

Para

Heading 4

  • Hi
  • bye
", "expected": [ { "event": "opentagname", "data": ["ol"] }, { "event": "opentag", "data": ["ol", {}] }, { "event": "opentagname", "data": ["li"] }, - { "event": "attribute", "data": ["class", "test"] }, + { "event": "attribute", "data": ["class", "test", null] }, { "event": "opentag", "data": ["li", { "class": "test" }] }, { "event": "opentagname", "data": ["div"] }, { "event": "opentag", "data": ["div", {}] }, { "event": "opentagname", "data": ["table"] }, - { "event": "attribute", "data": ["style", "width:100%"] }, + { "event": "attribute", "data": ["style", "width:100%", null] }, { "event": "opentag", "data": ["table", { "style": "width:100%" }] }, { "event": "opentagname", "data": ["tr"] }, { "event": "opentag", "data": ["tr", {}] }, @@ -20,7 +19,7 @@ { "event": "text", "data": ["TH"] }, { "event": "closetag", "data": ["th"] }, { "event": "opentagname", "data": ["td"] }, - { "event": "attribute", "data": ["colspan", "2"] }, + { "event": "attribute", "data": ["colspan", "2", null] }, { "event": "opentag", "data": ["td", { "colspan": "2" }] }, { "event": "opentagname", "data": ["h3"] }, { "event": "opentag", "data": ["h3", {}] }, diff --git a/src/__fixtures__/Events/09-attributes.json b/src/__fixtures__/Events/09-attributes.json index b5aac14c5..c1f72716a 100644 --- a/src/__fixtures__/Events/09-attributes.json +++ b/src/__fixtures__/Events/09-attributes.json @@ -1,9 +1,5 @@ { "name": "attributes (no white space, no value, no quotes)", - "options": { - "handler": {}, - "parser": {} - }, "html": "", "expected": [ { @@ -12,11 +8,11 @@ }, { "event": "attribute", - "data": ["class", "test0"] + "data": ["class", "test0", "\""] }, { "event": "attribute", - "data": ["title", "test1"] + "data": ["title", "test1", "\""] }, { "event": "attribute", @@ -24,7 +20,7 @@ }, { "event": "attribute", - "data": ["value", "test2"] + "data": ["value", "test2", null] }, { "event": "opentag", diff --git a/src/__fixtures__/Events/10-crazy-attrib.json b/src/__fixtures__/Events/10-crazy-attrib.json index a76ec161c..23b607739 100644 --- a/src/__fixtures__/Events/10-crazy-attrib.json +++ b/src/__fixtures__/Events/10-crazy-attrib.json @@ -1,9 +1,5 @@ { "name": "crazy attribute", - "options": { - "handler": {}, - "parser": {} - }, "html": "

stuff

", "expected": [ { diff --git a/src/__fixtures__/Events/12-long-comment-end.json b/src/__fixtures__/Events/12-long-comment-end.json index 65963e248..a6344b270 100644 --- a/src/__fixtures__/Events/12-long-comment-end.json +++ b/src/__fixtures__/Events/12-long-comment-end.json @@ -1,19 +1,15 @@ { "name": "Long comment ending", - "options": { - "handler": {}, - "parser": {} - }, "html": "", "expected": [ { "event": "opentagname", "data": ["meta"] }, - { "event": "attribute", "data": ["id", "before"] }, + { "event": "attribute", "data": ["id", "before", "'"] }, { "event": "opentag", "data": ["meta", { "id": "before" }] }, { "event": "closetag", "data": ["meta"] }, { "event": "comment", "data": [" text -"] }, { "event": "commentend", "data": [] }, { "event": "opentagname", "data": ["meta"] }, - { "event": "attribute", "data": ["id", "after"] }, + { "event": "attribute", "data": ["id", "after", "'"] }, { "event": "opentag", "data": ["meta", { "id": "after" }] }, { "event": "closetag", "data": ["meta"] } ] diff --git a/src/__fixtures__/Events/13-long-cdata-end.json b/src/__fixtures__/Events/13-long-cdata-end.json index b000ad7b0..9e8d4f93c 100644 --- a/src/__fixtures__/Events/13-long-cdata-end.json +++ b/src/__fixtures__/Events/13-long-cdata-end.json @@ -1,7 +1,6 @@ { "name": "Long CDATA ending", "options": { - "handler": {}, "parser": { "xmlMode": true } }, "html": "", diff --git a/src/__fixtures__/Events/14-implicit-open-tags.json b/src/__fixtures__/Events/14-implicit-open-tags.json index fdcd647e9..274d10a76 100644 --- a/src/__fixtures__/Events/14-implicit-open-tags.json +++ b/src/__fixtures__/Events/14-implicit-open-tags.json @@ -1,9 +1,5 @@ { "name": "Implicit open p and br tags", - "options": { - "handler": {}, - "parser": {} - }, "html": "
Hallo

World


", "expected": [ { "event": "opentagname", "data": ["div"] }, diff --git a/src/__fixtures__/Events/15-lt-whitespace.json b/src/__fixtures__/Events/15-lt-whitespace.json index 6c6ef6455..47b26abe9 100644 --- a/src/__fixtures__/Events/15-lt-whitespace.json +++ b/src/__fixtures__/Events/15-lt-whitespace.json @@ -1,9 +1,5 @@ { "name": "lt followed by whitespace", - "options": { - "handler": {}, - "parser": {} - }, "html": "a < b", "expected": [ { diff --git a/src/__fixtures__/Events/16-double_attribs.json b/src/__fixtures__/Events/16-double_attribs.json index d21d57f50..1b811cbb3 100644 --- a/src/__fixtures__/Events/16-double_attribs.json +++ b/src/__fixtures__/Events/16-double_attribs.json @@ -1,9 +1,5 @@ { "name": "double attribute", - "options": { - "handler": {}, - "parser": {} - }, "html": "

", "expected": [ { @@ -12,11 +8,11 @@ }, { "event": "attribute", - "data": ["class", "test"] + "data": ["class", "test", null] }, { "event": "attribute", - "data": ["class", "boo"] + "data": ["class", "boo", null] }, { "event": "opentag", diff --git a/src/__fixtures__/Events/17-numeric_entities.json b/src/__fixtures__/Events/17-numeric_entities.json index 02bfb3fdd..a869954b5 100644 --- a/src/__fixtures__/Events/17-numeric_entities.json +++ b/src/__fixtures__/Events/17-numeric_entities.json @@ -1,9 +1,5 @@ { "name": "numeric entities", - "options": { - "handler": {}, - "parser": { "decodeEntities": true } - }, "html": "abcdfg&#x;h", "expected": [ { diff --git a/src/__fixtures__/Events/18-legacy_entities.json b/src/__fixtures__/Events/18-legacy_entities.json index 9ee83d7f9..3c3281e5e 100644 --- a/src/__fixtures__/Events/18-legacy_entities.json +++ b/src/__fixtures__/Events/18-legacy_entities.json @@ -1,9 +1,5 @@ { "name": "legacy entities", - "options": { - "handler": {}, - "parser": { "decodeEntities": true } - }, "html": "&elíe&eer;s<er", "expected": [ { diff --git a/src/__fixtures__/Events/19-named_entities.json b/src/__fixtures__/Events/19-named_entities.json index d71a4f80d..25a941273 100644 --- a/src/__fixtures__/Events/19-named_entities.json +++ b/src/__fixtures__/Events/19-named_entities.json @@ -1,9 +1,5 @@ { "name": "named entities", - "options": { - "handler": {}, - "parser": { "decodeEntities": true } - }, "html": "&el<er∳foo&bar", "expected": [ { diff --git a/src/__fixtures__/Events/20-xml_entities.json b/src/__fixtures__/Events/20-xml_entities.json index 0e636ba56..96d3ea393 100644 --- a/src/__fixtures__/Events/20-xml_entities.json +++ b/src/__fixtures__/Events/20-xml_entities.json @@ -1,8 +1,7 @@ { "name": "xml entities", "options": { - "handler": {}, - "parser": { "decodeEntities": true, "xmlMode": true } + "parser": { "xmlMode": true } }, "html": "&>&<üabcde", "expected": [ diff --git a/src/__fixtures__/Events/21-entity_in_attribute.json b/src/__fixtures__/Events/21-entity_in_attribute.json index 65b67c072..b41a90c1a 100644 --- a/src/__fixtures__/Events/21-entity_in_attribute.json +++ b/src/__fixtures__/Events/21-entity_in_attribute.json @@ -1,9 +1,5 @@ { "name": "entity in attribute", - "options": { - "handler": {}, - "parser": { "decodeEntities": true } - }, "html": "", "expected": [ { @@ -14,7 +10,8 @@ "event": "attribute", "data": [ "href", - "http://example.com/page?param=value¶m2¶m3=>testing", "expected": [ { diff --git a/src/__fixtures__/Events/23-legacy_entity_fail.json b/src/__fixtures__/Events/23-legacy_entity_fail.json index b7bf5afc1..be7d3cb26 100644 --- a/src/__fixtures__/Events/23-legacy_entity_fail.json +++ b/src/__fixtures__/Events/23-legacy_entity_fail.json @@ -1,9 +1,5 @@ { "name": "legacy entities", - "options": { - "handler": {}, - "parser": { "decodeEntities": true } - }, "html": "M&M", "expected": [ { diff --git a/src/__fixtures__/Events/24-special_special.json b/src/__fixtures__/Events/24-special_special.json index f81a62f58..b347a0ff1 100644 --- a/src/__fixtures__/Events/24-special_special.json +++ b/src/__fixtures__/Events/24-special_special.json @@ -1,8 +1,71 @@ { "name": "Special special tags", - "options": {}, - "html": "", + "html": "<b>foo</b><title>", "expected": [ + { + "event": "opentagname", + "data": ["title"] + }, + { + "event": "opentag", + "data": ["title", {}] + }, + { + "event": "text", + "data": ["foo"] + }, + { + "event": "closetag", + "data": ["title"] + }, + { + "event": "opentagname", + "data": ["sitle"] + }, + { + "event": "opentag", + "data": ["sitle", {}] + }, + { + "event": "opentagname", + "data": ["b"] + }, + { + "event": "opentag", + "data": ["b", {}] + }, + { + "event": "closetag", + "data": ["b"] + }, + { + "event": "closetag", + "data": ["sitle"] + }, + { + "event": "opentagname", + "data": ["ttyle"] + }, + { + "event": "opentag", + "data": ["ttyle", {}] + }, + { + "event": "opentagname", + "data": ["b"] + }, + { + "event": "opentag", + "data": ["b", {}] + }, + { + "event": "closetag", + "data": ["b"] + }, + { + "event": "closetag", + "data": ["ttyle"] + }, { "event": "opentagname", "data": ["script"] diff --git a/src/__fixtures__/Events/25-empty_tag_name.json b/src/__fixtures__/Events/25-empty_tag_name.json index 7f59ff50e..eb4703dfd 100644 --- a/src/__fixtures__/Events/25-empty_tag_name.json +++ b/src/__fixtures__/Events/25-empty_tag_name.json @@ -1,6 +1,5 @@ { "name": "Empty tag name", - "options": {}, "html": "< ></ >", "expected": [ { diff --git a/src/__fixtures__/Events/26-not-quite-closed.json b/src/__fixtures__/Events/26-not-quite-closed.json index 0b36e92be..e5a03588c 100644 --- a/src/__fixtures__/Events/26-not-quite-closed.json +++ b/src/__fixtures__/Events/26-not-quite-closed.json @@ -1,6 +1,5 @@ { "name": "Not quite closed", - "options": {}, "html": "<foo /bar></foo bar>", "expected": [ { diff --git a/src/__fixtures__/Events/27-entities_in_attributes.json b/src/__fixtures__/Events/27-entities_in_attributes.json index 71a3c0bb9..6e93bb8ad 100644 --- a/src/__fixtures__/Events/27-entities_in_attributes.json +++ b/src/__fixtures__/Events/27-entities_in_attributes.json @@ -1,9 +1,5 @@ { "name": "Entities in attributes", - "options": { - "handler": {}, - "parser": { "decodeEntities": true } - }, "html": "<foo bar=& baz=\"&\" boo='&' noo=>", "expected": [ { @@ -12,19 +8,19 @@ }, { "event": "attribute", - "data": ["bar", "&"] + "data": ["bar", "&", null] }, { "event": "attribute", - "data": ["baz", "&"] + "data": ["baz", "&", "\""] }, { "event": "attribute", - "data": ["boo", "&"] + "data": ["boo", "&", "'"] }, { "event": "attribute", - "data": ["noo", ""] + "data": ["noo", "", null] }, { "event": "opentag", diff --git a/src/__fixtures__/Events/28-cdata_in_html.json b/src/__fixtures__/Events/28-cdata_in_html.json index 4c2402def..c875c04e4 100644 --- a/src/__fixtures__/Events/28-cdata_in_html.json +++ b/src/__fixtures__/Events/28-cdata_in_html.json @@ -1,6 +1,5 @@ { "name": "CDATA in HTML", - "options": {}, "html": "<![CDATA[ foo ]]>", "expected": [ { "event": "comment", "data": ["[CDATA[ foo ]]"] }, diff --git a/src/__fixtures__/Events/29-comment_edge-cases.json b/src/__fixtures__/Events/29-comment_edge-cases.json index b1f1b6156..98777436e 100644 --- a/src/__fixtures__/Events/29-comment_edge-cases.json +++ b/src/__fixtures__/Events/29-comment_edge-cases.json @@ -1,6 +1,5 @@ { "name": "Comment edge-cases", - "options": {}, "html": "<!-foo><!-- --- --><!--foo", "expected": [ { diff --git a/src/__fixtures__/Events/31-comment_false-ending.json b/src/__fixtures__/Events/31-comment_false-ending.json index c75dc23db..fb925ebb4 100644 --- a/src/__fixtures__/Events/31-comment_false-ending.json +++ b/src/__fixtures__/Events/31-comment_false-ending.json @@ -1,6 +1,5 @@ { "name": "Comment false ending", - "options": {}, "html": "<!-- a-b-> -->", "expected": [ { "event": "comment", "data": [" a-b-> "] }, diff --git a/src/__fixtures__/Events/32-script-ending-with-lessthan.json b/src/__fixtures__/Events/32-script-ending-with-lessthan.json index 1144681ed..22106e0ed 100644 --- a/src/__fixtures__/Events/32-script-ending-with-lessthan.json +++ b/src/__fixtures__/Events/32-script-ending-with-lessthan.json @@ -1,9 +1,5 @@ { "name": "Scripts ending with <", - "options": { - "handler": {}, - "parser": {} - }, "html": "<script><</script>", "expected": [ { diff --git a/src/__fixtures__/Events/34-not-alpha-tags.json b/src/__fixtures__/Events/34-not-alpha-tags.json new file mode 100644 index 000000000..2f8dfb56f --- /dev/null +++ b/src/__fixtures__/Events/34-not-alpha-tags.json @@ -0,0 +1,9 @@ +{ + "name": "tag names are not ASCII alpha", + "html": "<12>text</12>", + "expected": [ + { "event": "text", "data": ["<12>text"] }, + { "event": "comment", "data": ["12"] }, + { "event": "commentend", "data": [] } + ] +} diff --git a/src/__fixtures__/Events/35-non-br-void-close-tag.json b/src/__fixtures__/Events/35-non-br-void-close-tag.json new file mode 100644 index 000000000..edc473b36 --- /dev/null +++ b/src/__fixtures__/Events/35-non-br-void-close-tag.json @@ -0,0 +1,33 @@ +{ + "name": "open-implies-close case of (non-br) void close tag in non-XML mode", + "options": { + "parser": { "lowerCaseAttributeNames": true } + }, + "html": "<select><input></select>", + "expected": [ + { + "event": "opentagname", + "data": ["select"] + }, + { + "event": "opentag", + "data": ["select", {}] + }, + { + "event": "closetag", + "data": ["select"] + }, + { + "event": "opentagname", + "data": ["input"] + }, + { + "event": "opentag", + "data": ["input", {}] + }, + { + "event": "closetag", + "data": ["input"] + } + ] +} diff --git a/src/__fixtures__/Events/36-entity-in-attrib.json b/src/__fixtures__/Events/36-entity-in-attrib.json new file mode 100644 index 000000000..fd87aeb0a --- /dev/null +++ b/src/__fixtures__/Events/36-entity-in-attrib.json @@ -0,0 +1,26 @@ +{ + "name": "entity in attribute (#276)", + "html": "<img src=\"?&image_uri=1&ℑ=2&image=3\"/>?&image_uri=1&ℑ=2&image=3", + "expected": [ + { + "event": "opentagname", + "data": ["img"] + }, + { + "event": "attribute", + "data": ["src", "?&image_uri=1&â„‘=2&image=3", "\""] + }, + { + "event": "opentag", + "data": ["img", { "src": "?&image_uri=1&â„‘=2&image=3" }] + }, + { + "event": "closetag", + "data": ["img"] + }, + { + "event": "text", + "data": ["?&image_uri=1&â„‘=2&image=3"] + } + ] +} diff --git a/src/__fixtures__/Stream/01-basic.json b/src/__fixtures__/Stream/01-basic.json index 56aedf0d4..fa1683955 100644 --- a/src/__fixtures__/Stream/01-basic.json +++ b/src/__fixtures__/Stream/01-basic.json @@ -1,6 +1,5 @@ { "name": "Basic html", - "options": {}, "file": "Basic.html", "expected": [ { diff --git a/src/__fixtures__/Stream/02-RSS.json b/src/__fixtures__/Stream/02-RSS.json index 78f6e26cc..cec635265 100644 --- a/src/__fixtures__/Stream/02-RSS.json +++ b/src/__fixtures__/Stream/02-RSS.json @@ -31,7 +31,7 @@ }, { "event": "attribute", - "data": ["version", "2.0"] + "data": ["version", "2.0", "\""] }, { "event": "opentag", @@ -321,7 +321,7 @@ { "event": "text", "data": [ - "How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href=\"http://howe.iki.rssi.ru/GCTC/gctc_e.htm\">Star City</a>." + "How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href=\"http://howe.iki.rssi.ru/GCTC/gctc_e.htm\">Star City</a>." ] }, { @@ -403,7 +403,7 @@ { "event": "text", "data": [ - "Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href=\"http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm\">partial eclipse of the Sun</a> on Saturday, May 31st." + "Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href=\"http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm\">partial eclipse of the Sun</a> on Saturday, May 31st." ] }, { @@ -696,7 +696,47 @@ }, { "event": "text", - "data": ["\n\n "] + "data": ["\n\n "] + }, + { + "event": "opentagname", + "data": ["media:content"] + }, + { + "event": "attribute", + "data": ["height", "200", "\""] + }, + { + "event": "attribute", + "data": ["medium", "image", "\""] + }, + { + "event": "attribute", + "data": ["url", "https://picsum.photos/200", "\""] + }, + { + "event": "attribute", + "data": ["width", "200", "\""] + }, + { + "event": "opentag", + "data": [ + "media:content", + { + "height": "200", + "medium": "image", + "url": "https://picsum.photos/200", + "width": "200" + } + ] + }, + { + "event": "closetag", + "data": ["media:content"] + }, + { + "event": "text", + "data": ["\n "] }, { "event": "closetag", diff --git a/src/__fixtures__/Stream/03-Atom.json b/src/__fixtures__/Stream/03-Atom.json index 6dcf484ad..238d30ccb 100644 --- a/src/__fixtures__/Stream/03-Atom.json +++ b/src/__fixtures__/Stream/03-Atom.json @@ -29,7 +29,7 @@ }, { "event": "attribute", - "data": ["xmlns", "http://www.w3.org/2005/Atom"] + "data": ["xmlns", "http://www.w3.org/2005/Atom", "\""] }, { "event": "opentag", @@ -90,11 +90,11 @@ }, { "event": "attribute", - "data": ["href", "http://example.org/feed/"] + "data": ["href", "http://example.org/feed/", "\""] }, { "event": "attribute", - "data": ["rel", "self"] + "data": ["rel", "self", "\""] }, { "event": "opentag", @@ -120,7 +120,7 @@ }, { "event": "attribute", - "data": ["href", "http://example.org/"] + "data": ["href", "http://example.org/", "\""] }, { "event": "opentag", @@ -277,7 +277,7 @@ }, { "event": "attribute", - "data": ["href", "http://example.org/2003/12/13/atom03"] + "data": ["href", "http://example.org/2003/12/13/atom03", "\""] }, { "event": "opentag", @@ -302,15 +302,15 @@ }, { "event": "attribute", - "data": ["rel", "alternate"] + "data": ["rel", "alternate", "\""] }, { "event": "attribute", - "data": ["type", "text/html"] + "data": ["type", "text/html", "\""] }, { "event": "attribute", - "data": ["href", "http://example.org/2003/12/13/atom03.html"] + "data": ["href", "http://example.org/2003/12/13/atom03.html", "\""] }, { "event": "opentag", @@ -337,11 +337,11 @@ }, { "event": "attribute", - "data": ["rel", "edit"] + "data": ["rel", "edit", "\""] }, { "event": "attribute", - "data": ["href", "http://example.org/2003/12/13/atom03/edit"] + "data": ["href", "http://example.org/2003/12/13/atom03/edit", "\""] }, { "event": "opentag", @@ -407,7 +407,7 @@ }, { "event": "attribute", - "data": ["type", "html"] + "data": ["type", "html", "\""] }, { "event": "opentag", @@ -446,6 +446,22 @@ "event": "closetag", "data": ["entry"] }, + { + "data": ["\n\n\t"], + "event": "text" + }, + { + "data": ["entry"], + "event": "opentagname" + }, + { + "data": ["entry", {}], + "event": "opentag" + }, + { + "data": ["entry"], + "event": "closetag" + }, { "event": "text", "data": ["\n\n"] diff --git a/src/__fixtures__/Stream/04-RDF.json b/src/__fixtures__/Stream/04-RDF.json index 0f1f62fa5..146aefe39 100644 --- a/src/__fixtures__/Stream/04-RDF.json +++ b/src/__fixtures__/Stream/04-RDF.json @@ -17,45 +17,55 @@ }, { "event": "attribute", - "data": ["xmlns:rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"] + "data": [ + "xmlns:rdf", + "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "\"" + ] }, { "event": "attribute", - "data": ["xmlns", "http://purl.org/rss/1.0/"] + "data": ["xmlns", "http://purl.org/rss/1.0/", "\""] }, { "event": "attribute", - "data": ["xmlns:ev", "http://purl.org/rss/1.0/modules/event/"] + "data": ["xmlns:ev", "http://purl.org/rss/1.0/modules/event/", "\""] }, { "event": "attribute", "data": [ "xmlns:content", - "http://purl.org/rss/1.0/modules/content/" + "http://purl.org/rss/1.0/modules/content/", + "\"" ] }, { "event": "attribute", - "data": ["xmlns:taxo", "http://purl.org/rss/1.0/modules/taxonomy/"] + "data": [ + "xmlns:taxo", + "http://purl.org/rss/1.0/modules/taxonomy/", + "\"" + ] }, { "event": "attribute", - "data": ["xmlns:dc", "http://purl.org/dc/elements/1.1/"] + "data": ["xmlns:dc", "http://purl.org/dc/elements/1.1/", "\""] }, { "event": "attribute", "data": [ "xmlns:syn", - "http://purl.org/rss/1.0/modules/syndication/" + "http://purl.org/rss/1.0/modules/syndication/", + "\"" ] }, { "event": "attribute", - "data": ["xmlns:dcterms", "http://purl.org/dc/terms/"] + "data": ["xmlns:dcterms", "http://purl.org/dc/terms/", "\""] }, { "event": "attribute", - "data": ["xmlns:admin", "http://webns.net/mvcb/"] + "data": ["xmlns:admin", "http://webns.net/mvcb/", "\""] }, { "event": "opentag", @@ -84,7 +94,7 @@ }, { "event": "attribute", - "data": ["rdf:about", "https://github.com/fb55/htmlparser2/"] + "data": ["rdf:about", "https://github.com/fb55/htmlparser2/", "\""] }, { "event": "opentag", @@ -387,7 +397,8 @@ "event": "attribute", "data": [ "rdf:resource", - "http://somefakesite/path/to/something.html" + "http://somefakesite/path/to/something.html", + "\"" ] }, { @@ -437,7 +448,11 @@ }, { "event": "attribute", - "data": ["rdf:about", "http://somefakesite/path/to/something.html"] + "data": [ + "rdf:about", + "http://somefakesite/path/to/something.html", + "\"" + ] }, { "event": "opentag", @@ -694,7 +709,8 @@ "event": "attribute", "data": [ "rdf:about", - "http://somefakesite/path/to/something-else.html" + "http://somefakesite/path/to/something-else.html", + "\"" ] }, { diff --git a/src/__fixtures__/Stream/05-Attributes.json b/src/__fixtures__/Stream/05-Attributes.json index c7b01a945..9cc009976 100644 --- a/src/__fixtures__/Stream/05-Attributes.json +++ b/src/__fixtures__/Stream/05-Attributes.json @@ -1,6 +1,5 @@ { "name": "Attributes", - "options": {}, "file": "Attributes.html", "expected": [ { @@ -93,15 +92,15 @@ }, { "event": "attribute", - "data": ["id", "test0"] + "data": ["id", "test0", "\""] }, { "event": "attribute", - "data": ["class", "value0"] + "data": ["class", "value0", "\""] }, { "event": "attribute", - "data": ["title", "value1"] + "data": ["title", "value1", "\""] }, { "event": "opentag", @@ -144,11 +143,11 @@ }, { "event": "attribute", - "data": ["id", "test1"] + "data": ["id", "test1", "\""] }, { "event": "attribute", - "data": ["class", "value2"] + "data": ["class", "value2", null] }, { "event": "attribute", @@ -197,15 +196,15 @@ }, { "event": "attribute", - "data": ["id", "test2"] + "data": ["id", "test2", "\""] }, { "event": "attribute", - "data": ["class", "value4"] + "data": ["class", "value4", "\""] }, { "event": "attribute", - "data": ["title", "value5"] + "data": ["title", "value5", "\""] }, { "event": "opentag", diff --git a/src/__fixtures__/Stream/06-Svg.json b/src/__fixtures__/Stream/06-Svg.json index 89084d954..c8cfd2a61 100644 --- a/src/__fixtures__/Stream/06-Svg.json +++ b/src/__fixtures__/Stream/06-Svg.json @@ -80,15 +80,15 @@ }, { "event": "attribute", - "data": ["version", "1.1"] + "data": ["version", "1.1", "\""] }, { "event": "attribute", - "data": ["xmlns", "http://www.w3.org/2000/svg"] + "data": ["xmlns", "http://www.w3.org/2000/svg", "\""] }, { "event": "attribute", - "data": ["xmlns:xlink", "http://www.w3.org/1999/xlink"] + "data": ["xmlns:xlink", "http://www.w3.org/1999/xlink", "\""] }, { "event": "opentag", diff --git a/src/__fixtures__/test-helper.ts b/src/__fixtures__/test-helper.ts index 5f474471d..f371b916d 100644 --- a/src/__fixtures__/test-helper.ts +++ b/src/__fixtures__/test-helper.ts @@ -1,6 +1,5 @@ import { Parser, Handler, ParserOptions } from "../Parser"; import { CollectingHandler } from "../CollectingHandler"; -import { DomHandlerOptions } from ".."; import fs from "fs"; import path from "path"; @@ -8,7 +7,7 @@ export function writeToParser( handler: Partial<Handler>, options: ParserOptions | undefined, data: string -) { +): void { const parser = new Parser(handler, options); // First, try to run the test via chunks for (let i = 0; i < data.length; i++) { @@ -27,36 +26,39 @@ interface Event { // Returns a tree structure export function getEventCollector( cb: (error: Error | null, events?: Event[]) => void -) { +): CollectingHandler { const handler = new CollectingHandler({ onerror: cb, onend() { cb(null, handler.events.reduce(eventReducer, [])); - } + }, }); return handler; } -function eventReducer(events: Event[], arr: [string, ...unknown[]]): Event[] { - if ( - arr[0] === "onerror" || - arr[0] === "onend" || - arr[0] === "onparserinit" - ) { - // ignore +function eventReducer( + events: Event[], + [event, ...data]: [string, ...unknown[]] +): Event[] { + if (event === "onerror" || event === "onend" || event === "onparserinit") { + // Ignore } else if ( - arr[0] === "ontext" && + event === "ontext" && events.length && events[events.length - 1].event === "text" ) { // Combine text nodes - // @ts-ignore - events[events.length - 1].data[0] += arr[1]; + (events[events.length - 1].data[0] as string) += data[0]; } else { + // Remove `undefined`s from attribute responses, as they cannot be represented in JSON. + if (event === "onattribute" && data[2] === undefined) { + data.pop(); + } + events.push({ - event: arr[0].substr(2), - data: arr.slice(1) + event: event.substr(2), + data, }); } @@ -66,12 +68,12 @@ function eventReducer(events: Event[], arr: [string, ...unknown[]]): Event[] { function getCallback(file: TestFile, done: (err?: Error | null) => void) { let repeated = false; - return (err: null | Error, actual?: {} | {}[]) => { + return (err: null | Error, actual?: unknown | unknown[]) => { expect(err).toBeNull(); if (file.useSnapshot) { expect(actual).toMatchSnapshot(); } else { - expect(actual).toEqual(file.expected); + expect(actual).toStrictEqual(file.expected); } if (repeated) done(); @@ -81,36 +83,35 @@ function getCallback(file: TestFile, done: (err?: Error | null) => void) { interface TestFile { name: string; - options: { + options?: { parser?: ParserOptions; - handler?: DomHandlerOptions; } & Partial<ParserOptions>; html: string; file: string; useSnapshot?: boolean; - expected?: {} | {}[]; + expected?: unknown | unknown[]; } export function createSuite( name: string, getResult: ( file: TestFile, - done: (error: Error | null, actual?: {} | {}[]) => void + done: (error: Error | null, actual?: unknown | unknown[]) => void ) => void -) { +): void { describe(name, readDir); function readDir() { const dir = path.join(__dirname, name); fs.readdirSync(dir) - .filter(file => !file.startsWith(".") && !file.startsWith("_")) - .map(name => path.join(dir, name)) + .filter((file) => !file.startsWith(".") && !file.startsWith("_")) + .map((name) => path.join(dir, name)) .map(require) .forEach(runTest); } function runTest(file: TestFile) { - test(file.name, done => getResult(file, getCallback(file, done))); + test(file.name, (done) => getResult(file, getCallback(file, done))); } } diff --git a/src/__snapshots__/FeedHandler.spec.ts.snap b/src/__snapshots__/FeedHandler.spec.ts.snap index 5d4d8969c..9917549a4 100644 --- a/src/__snapshots__/FeedHandler.spec.ts.snap +++ b/src/__snapshots__/FeedHandler.spec.ts.snap @@ -10,9 +10,13 @@ Object { "description": "Some content.", "id": "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a", "link": "http://example.org/2003/12/13/atom03", + "media": Array [], "pubDate": 2003-12-13T18:30:02.000Z, "title": "Atom-Powered Robots Run Amok", }, + Object { + "media": Array [], + }, ], "link": "http://example.org/feed/", "title": "Example Feed", @@ -31,9 +35,13 @@ Object { "description": "Some content.", "id": "urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a", "link": "http://example.org/2003/12/13/atom03", + "media": Array [], "pubDate": 2003-12-13T18:30:02.000Z, "title": "Atom-Powered Robots Run Amok", }, + Object { + "media": Array [], + }, ], "link": "http://example.org/feed/", "title": "Example Feed", @@ -49,11 +57,13 @@ Object { Object { "description": "Great test content<br>A link: <a href=\\"http://github.com\\">Github</a>", "link": "http://somefakesite/path/to/something.html", + "media": Array [], "title": "Fast HTML Parsing", }, Object { "description": "The early bird gets the worm", "link": "http://somefakesite/path/to/something-else.html", + "media": Array [], "title": "This space intentionally left blank", }, ], @@ -70,11 +80,13 @@ Object { Object { "description": "Great test content<br>A link: <a href=\\"http://github.com\\">Github</a>", "link": "http://somefakesite/path/to/something.html", + "media": Array [], "title": "Fast HTML Parsing", }, Object { "description": "The early bird gets the worm", "link": "http://somefakesite/path/to/something-else.html", + "media": Array [], "title": "This space intentionally left blank", }, ], @@ -91,21 +103,24 @@ Object { "id": "", "items": Array [ Object { - "description": "How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href=\\"http://howe.iki.rssi.ru/GCTC/gctc_e.htm\\">Star City</a>.", + "description": "How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href=\\"http://howe.iki.rssi.ru/GCTC/gctc_e.htm\\">Star City</a>.", "id": "http://liftoff.msfc.nasa.gov/2003/06/03.html#item573", "link": "http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp", + "media": Array [], "pubDate": 2003-06-03T09:39:21.000Z, "title": "Star City", }, Object { - "description": "Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href=\\"http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm\\">partial eclipse of the Sun</a> on Saturday, May 31st.", + "description": "Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href=\\"http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm\\">partial eclipse of the Sun</a> on Saturday, May 31st.", "id": "http://liftoff.msfc.nasa.gov/2003/05/30.html#item572", + "media": Array [], "pubDate": 2003-05-30T11:06:42.000Z, }, Object { "description": "Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly. The proposed VASIMR engine would do that.", "id": "http://liftoff.msfc.nasa.gov/2003/05/27.html#item571", "link": "http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp", + "media": Array [], "pubDate": 2003-05-27T08:37:32.000Z, "title": "The Engine That Does More", }, @@ -113,6 +128,15 @@ Object { "description": "Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them. Instead, astronauts have other options.", "id": "http://liftoff.msfc.nasa.gov/2003/05/20.html#item570", "link": "http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp", + "media": Array [ + Object { + "height": 200, + "isDefault": false, + "medium": "image", + "url": "https://picsum.photos/200", + "width": 200, + }, + ], "pubDate": 2003-05-20T08:56:02.000Z, "title": "Astronauts' Dirty Laundry", }, @@ -131,21 +155,24 @@ Object { "id": "", "items": Array [ Object { - "description": "How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href=\\"http://howe.iki.rssi.ru/GCTC/gctc_e.htm\\">Star City</a>.", + "description": "How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href=\\"http://howe.iki.rssi.ru/GCTC/gctc_e.htm\\">Star City</a>.", "id": "http://liftoff.msfc.nasa.gov/2003/06/03.html#item573", "link": "http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp", + "media": Array [], "pubDate": 2003-06-03T09:39:21.000Z, "title": "Star City", }, Object { - "description": "Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href=\\"http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm\\">partial eclipse of the Sun</a> on Saturday, May 31st.", + "description": "Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href=\\"http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm\\">partial eclipse of the Sun</a> on Saturday, May 31st.", "id": "http://liftoff.msfc.nasa.gov/2003/05/30.html#item572", + "media": Array [], "pubDate": 2003-05-30T11:06:42.000Z, }, Object { "description": "Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly. The proposed VASIMR engine would do that.", "id": "http://liftoff.msfc.nasa.gov/2003/05/27.html#item571", "link": "http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp", + "media": Array [], "pubDate": 2003-05-27T08:37:32.000Z, "title": "The Engine That Does More", }, @@ -153,6 +180,15 @@ Object { "description": "Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them. Instead, astronauts have other options.", "id": "http://liftoff.msfc.nasa.gov/2003/05/20.html#item570", "link": "http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp", + "media": Array [ + Object { + "height": 200, + "isDefault": false, + "medium": "image", + "url": "https://picsum.photos/200", + "width": 200, + }, + ], "pubDate": 2003-05-20T08:56:02.000Z, "title": "Astronauts' Dirty Laundry", }, @@ -171,21 +207,24 @@ Object { "id": "", "items": Array [ Object { - "description": "How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href=\\"http://howe.iki.rssi.ru/GCTC/gctc_e.htm\\">Star City</a>.", + "description": "How do Americans get ready to work with Russians aboard the International Space Station? They take a crash course in culture, language and protocol at Russia's <a href=\\"http://howe.iki.rssi.ru/GCTC/gctc_e.htm\\">Star City</a>.", "id": "http://liftoff.msfc.nasa.gov/2003/06/03.html#item573", "link": "http://liftoff.msfc.nasa.gov/news/2003/news-starcity.asp", + "media": Array [], "pubDate": 2003-06-03T09:39:21.000Z, "title": "Star City", }, Object { - "description": "Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href=\\"http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm\\">partial eclipse of the Sun</a> on Saturday, May 31st.", + "description": "Sky watchers in Europe, Asia, and parts of Alaska and Canada will experience a <a href=\\"http://science.nasa.gov/headlines/y2003/30may_solareclipse.htm\\">partial eclipse of the Sun</a> on Saturday, May 31st.", "id": "http://liftoff.msfc.nasa.gov/2003/05/30.html#item572", + "media": Array [], "pubDate": 2003-05-30T11:06:42.000Z, }, Object { "description": "Before man travels to Mars, NASA hopes to design new engines that will let us fly through the Solar System more quickly. The proposed VASIMR engine would do that.", "id": "http://liftoff.msfc.nasa.gov/2003/05/27.html#item571", "link": "http://liftoff.msfc.nasa.gov/news/2003/news-VASIMR.asp", + "media": Array [], "pubDate": 2003-05-27T08:37:32.000Z, "title": "The Engine That Does More", }, @@ -193,6 +232,15 @@ Object { "description": "Compared to earlier spacecraft, the International Space Station has many luxuries, but laundry facilities are not one of them. Instead, astronauts have other options.", "id": "http://liftoff.msfc.nasa.gov/2003/05/20.html#item570", "link": "http://liftoff.msfc.nasa.gov/news/2003/news-laundry.asp", + "media": Array [ + Object { + "height": 200, + "isDefault": false, + "medium": "image", + "url": "https://picsum.photos/200", + "width": 200, + }, + ], "pubDate": 2003-05-20T08:56:02.000Z, "title": "Astronauts' Dirty Laundry", }, diff --git a/src/__snapshots__/index.spec.ts.snap b/src/__snapshots__/index.spec.ts.snap index 2862b8e12..675a064c1 100644 --- a/src/__snapshots__/index.spec.ts.snap +++ b/src/__snapshots__/index.spec.ts.snap @@ -2,40 +2,8 @@ exports[`Index createDomStream 1`] = ` Array [ - DataNode { - "data": "&This is text", - "endIndex": null, - "next": DataNode { - "data": " and comments ", - "endIndex": null, - "next": <tags />, - "parent": null, - "prev": [Circular], - "startIndex": null, - "type": "comment", - }, - "parent": null, - "prev": null, - "startIndex": null, - "type": "text", - }, - DataNode { - "data": " and comments ", - "endIndex": null, - "next": <tags />, - "parent": null, - "prev": DataNode { - "data": "&This is text", - "endIndex": null, - "next": [Circular], - "parent": null, - "prev": null, - "startIndex": null, - "type": "text", - }, - "startIndex": null, - "type": "comment", - }, + &This is text, + <!-- and comments -->, <tags />, ] `; @@ -51,73 +19,16 @@ Array [ "data": "?foo", "endIndex": null, "name": "?foo", - "next": DataNode { - "data": "Yay!", - "endIndex": null, - "next": null, - "parent": <c> - [Circular] - [Circular] - </c>, - "prev": [Circular], - "startIndex": null, - "type": "text", - }, + "next": Yay!, "parent": <c> [Circular] - DataNode { - "data": "Yay!", - "endIndex": null, - "next": null, - "parent": <c> - [Circular] - [Circular] - </c>, - "prev": [Circular], - "startIndex": null, - "type": "text", - } + Yay! </c>, "prev": null, "startIndex": null, "type": "directive", } - DataNode { - "data": "Yay!", - "endIndex": null, - "next": null, - "parent": <c> - ProcessingInstruction { - "data": "?foo", - "endIndex": null, - "name": "?foo", - "next": [Circular], - "parent": <c> - [Circular] - [Circular] - </c>, - "prev": null, - "startIndex": null, - "type": "directive", - } - [Circular] - </c>, - "prev": ProcessingInstruction { - "data": "?foo", - "endIndex": null, - "name": "?foo", - "next": [Circular], - "parent": <c> - [Circular] - [Circular] - </c>, - "prev": null, - "startIndex": null, - "type": "directive", - }, - "startIndex": null, - "type": "text", - } + Yay! </c> </b> </a>, diff --git a/src/__tests__/events.ts b/src/__tests__/events.ts index c55cf5712..2abd5852b 100644 --- a/src/__tests__/events.ts +++ b/src/__tests__/events.ts @@ -3,7 +3,7 @@ import * as helper from "../__fixtures__/test-helper"; helper.createSuite("Events", (test, cb) => helper.writeToParser( helper.getEventCollector(cb), - test.options.parser, + test.options?.parser, test.html ) ); diff --git a/src/index.spec.ts b/src/index.spec.ts index fb3489cbe..681b4ffae 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -1,16 +1,23 @@ -import { parseDOM, createDomStream } from "."; +import { + parseDOM, + createDomStream, + DomHandler, + DefaultHandler, + RssHandler, +} from "."; +import { FeedHandler } from "./FeedHandler"; import { Element } from "domhandler"; // Add an `attributes` prop to the Element for now, to make it possible for Jest to render DOM nodes. Object.defineProperty(Element.prototype, "attributes", { get() { - return Object.keys(this.attribs).map(name => ({ + return Object.keys(this.attribs).map((name) => ({ name, - value: this.attribs[name] + value: this.attribs[name], })); }, configurable: true, - enumerable: false + enumerable: false, }); describe("Index", () => { @@ -19,7 +26,7 @@ describe("Index", () => { expect(dom).toMatchSnapshot(); }); - test("createDomStream", done => { + test("createDomStream", (done) => { const domStream = createDomStream((err, dom) => { expect(err).toBeNull(); expect(dom).toMatchSnapshot(); @@ -33,4 +40,11 @@ describe("Index", () => { domStream.end(); }); + + describe("API", () => { + it("should export the appropriate APIs", () => { + expect(RssHandler).toEqual(FeedHandler); + expect(DomHandler).toEqual(DefaultHandler); + }); + }); }); diff --git a/src/index.ts b/src/index.ts index fefee2ec4..3067ac1d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,7 +31,7 @@ export function createDomStream( cb: (error: Error | null, dom: Node[]) => void, options?: Options, elementCb?: (element: Element) => void -) { +): Parser { const handler = new DomHandler(cb, options, elementCb); return new Parser(handler, options); } @@ -40,37 +40,13 @@ export { default as Tokenizer } from "./Tokenizer"; import * as ElementType from "domelementtype"; export { ElementType }; -/** - * List of all events that the parser emits. - * - * Format: eventname: number of arguments. - */ -export const EVENTS = { - attribute: 2, - cdatastart: 0, - cdataend: 0, - text: 1, - processinginstruction: 2, - comment: 1, - commentend: 0, - closetag: 1, - opentag: 2, - opentagname: 1, - error: 1, - end: 0 -}; - /* - All of the following exports exist for backwards-compatibility. - They should probably be removed eventually. -*/ + * All of the following exports exist for backwards-compatibility. + * They should probably be removed eventually. + */ export * from "./FeedHandler"; -export * from "./WritableStream"; -export * from "./CollectingHandler"; - -import * as DomUtils from "domutils"; -export { DomUtils }; +export * as DomUtils from "domutils"; // Old names for Dom- & FeedHandler export { DomHandler as DefaultHandler }; diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json new file mode 100644 index 000000000..84f161316 --- /dev/null +++ b/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "exclude": [] +}