diff --git a/packages/parse5-parser-stream/lib/index.ts b/packages/parse5-parser-stream/lib/index.ts index c54998ab6..74b737664 100644 --- a/packages/parse5-parser-stream/lib/index.ts +++ b/packages/parse5-parser-stream/lib/index.ts @@ -30,18 +30,31 @@ import type { DefaultTreeAdapterMap } from 'parse5/dist/tree-adapters/default.js * */ export class ParserStream extends Writable { + static getFragmentStream( + fragmentContext?: T['parentNode'] | null, + options?: ParserOptions + ): ParserStream { + const parser = Parser.getFragmentParser(fragmentContext, options); + const stream = new ParserStream(options, parser); + return stream; + } + private lastChunkWritten = false; private writeCallback: undefined | (() => void) = undefined; - public parser: Parser; private pendingHtmlInsertions: string[] = []; /** The resulting document node. */ - public document: T['document']; + public get document(): T['document'] { + return this.parser.document; + } + public getFragment(): T['documentFragment'] { + return this.parser.getFragment(); + } /** * @param options Parsing options. */ - constructor(options?: ParserOptions) { + constructor(options?: ParserOptions, public parser: Parser = new Parser(options)) { super({ decodeStrings: false }); const resume = (): void => { @@ -68,8 +81,7 @@ export class ParserStream } }; - this.parser = new Parser(options, undefined, undefined, scriptHandler); - this.document = this.parser.document; + this.parser.scriptHandler = scriptHandler; } //WritableStream implementation diff --git a/packages/parse5-parser-stream/test/location-info.test.ts b/packages/parse5-parser-stream/test/location-info.test.ts index 33a806ecb..aea24f736 100644 --- a/packages/parse5-parser-stream/test/location-info.test.ts +++ b/packages/parse5-parser-stream/test/location-info.test.ts @@ -5,7 +5,7 @@ import { parseChunked } from './utils/parse-chunked.js'; generateLocationInfoParserTests('location-info', (input, opts) => // NOTE: because of performance use bigger chunks here - parseChunked(input, opts, 100, 400) + parseChunked({ input }, opts, 100, 400) ); generateTestsForEachTreeAdapter('location-info', (treeAdapter) => { @@ -17,7 +17,7 @@ generateTestsForEachTreeAdapter('location-info', (treeAdapter) => { sourceCodeLocationInfo: true, }; - const document = parseChunked(html, opts).node; + const document = parseChunked({ input: html }, opts).node; const htmlEl = treeAdapter.getChildNodes(document)[0]; const headEl = treeAdapter.getChildNodes(htmlEl)[0]; const bodyEl = treeAdapter.getChildNodes(htmlEl)[1]; diff --git a/packages/parse5-parser-stream/test/parser-stream.test.ts b/packages/parse5-parser-stream/test/parser-stream.test.ts index 9559c8fa3..4653090d2 100644 --- a/packages/parse5-parser-stream/test/parser-stream.test.ts +++ b/packages/parse5-parser-stream/test/parser-stream.test.ts @@ -4,8 +4,22 @@ import { generateParsingTests } from 'parse5-test-utils/utils/generate-parsing-t import { parseChunked } from './utils/parse-chunked.js'; import { finished } from 'parse5-test-utils/utils/common.js'; -generateParsingTests('ParserStream', 'ParserStream', { skipFragments: true }, (test, opts) => - parseChunked(test.input, opts) +generateParsingTests( + 'ParserStream', + 'ParserStream', + { + expectErrors: [ + //TODO(GH-448): Foreign content behaviour was updated in the HTML spec. + //The old test suite still tests the old behaviour. + '269.foreign-fragment', + '270.foreign-fragment', + '307.foreign-fragment', + '309.foreign-fragment', + '316.foreign-fragment', + '317.foreign-fragment', + ], + }, + (test, opts) => parseChunked(test, opts) ); describe('ParserStream', () => { diff --git a/packages/parse5-parser-stream/test/scripting.test.ts b/packages/parse5-parser-stream/test/scripting.test.ts index ccd939d6e..a0f166c8a 100644 --- a/packages/parse5-parser-stream/test/scripting.test.ts +++ b/packages/parse5-parser-stream/test/scripting.test.ts @@ -14,14 +14,14 @@ generateParsingTests( 'ParserStream - Scripting', 'ParserStream - Scripting', { - skipFragments: true, withoutErrors: true, suitePath, }, async (test, opts) => { const chunks = makeChunks(test.input); - const parser = new ParserStream(opts); - const { document } = parser; + const parser = test.fragmentContext + ? ParserStream.getFragmentStream(test.fragmentContext, opts) + : new ParserStream(opts); parser.on('script', async (scriptElement, documentWrite, resume) => { const scriptTextNode = opts.treeAdapter.getChildNodes(scriptElement)[0]; @@ -48,7 +48,10 @@ generateParsingTests( await finished(parser); - return { node: document }; + return { + node: test.fragmentContext ? parser.getFragment() : parser.document, + chunks, + }; } ); diff --git a/packages/parse5-parser-stream/test/utils/parse-chunked.ts b/packages/parse5-parser-stream/test/utils/parse-chunked.ts index b907bba0f..90370a2bf 100644 --- a/packages/parse5-parser-stream/test/utils/parse-chunked.ts +++ b/packages/parse5-parser-stream/test/utils/parse-chunked.ts @@ -3,14 +3,16 @@ import type { TreeAdapterTypeMap } from 'parse5/dist/tree-adapters/interface.js' import { ParserStream } from '../../lib/index.js'; import { makeChunks } from 'parse5-test-utils/utils/common.js'; -export function parseChunked( - html: string, - opts: ParserOptions, +export function parseChunked( + test: { input: string; fragmentContext?: T['parentNode'] }, + opts: ParserOptions, minChunkSize?: number, maxChunkSize?: number ): { node: TreeAdapterTypeMap['document']; chunks: string[] } { - const parserStream = new ParserStream(opts); - const chunks = makeChunks(html, minChunkSize, maxChunkSize); + const parserStream = test.fragmentContext + ? ParserStream.getFragmentStream(test.fragmentContext, opts) + : new ParserStream(opts); + const chunks = makeChunks(test.input, minChunkSize, maxChunkSize); // NOTE: set small waterline for testing purposes parserStream.parser.tokenizer.preprocessor.bufferWaterline = 8; @@ -25,7 +27,7 @@ export function parseChunked( parserStream.end(chunks[chunks.length - 1]); return { - node: parserStream.document, + node: test.fragmentContext ? parserStream.getFragment() : parserStream.document, chunks, }; } diff --git a/packages/parse5/lib/index.ts b/packages/parse5/lib/index.ts index 333759412..c94f3aa5a 100644 --- a/packages/parse5/lib/index.ts +++ b/packages/parse5/lib/index.ts @@ -75,5 +75,9 @@ export function parseFragment { assert.strictEqual(document.childNodes[0].data, '!DOCTYPE html SYSTEM "about:legacy-compat"'); }); - describe('Regression - Incorrect arguments fallback for the parser.parseFragment (GH-82, GH-83)', () => { - beforeEach(() => { - Parser.parseFragment = function ( - html: string, - fragmentContext?: T['element'], - options?: ParserOptions - ): { - html: string; - fragmentContext: T['element'] | null | undefined; - options: ParserOptions | undefined; - } { - return { - html, - fragmentContext, - options, - }; - }; - }); - - afterEach(() => { - Parser.parseFragment = origParseFragment; - }); - - it('parses correctly', () => { - const fragmentContext = treeAdapters.default.createElement('div', NS.HTML, []); - const html = ''; - const opts = { sourceCodeLocationInfo: true }; - - let args: any = parse5.parseFragment(fragmentContext, html, opts); - - expect(args).toHaveProperty('fragmentContext', fragmentContext); - expect(args).toHaveProperty('html', html); - assert.ok(args.options.sourceCodeLocationInfo); - - args = parse5.parseFragment(html, opts); - - assert.ok(!args.fragmentContext); - expect(args).toHaveProperty('html', html); - assert.ok(args.options.sourceCodeLocationInfo); - - args = parse5.parseFragment(html); - - assert.ok(!args.fragmentContext); - expect(args).toHaveProperty('html', html); - assert.ok(!args.options); - }); - }); - describe("Regression - Don't inherit from Object when creating collections (GH-119)", () => { beforeEach(() => { /*eslint-disable no-extend-native*/ diff --git a/packages/parse5/lib/parser/index.ts b/packages/parse5/lib/parser/index.ts index 3fb04c115..bb38706d5 100644 --- a/packages/parse5/lib/parser/index.ts +++ b/packages/parse5/lib/parser/index.ts @@ -165,11 +165,10 @@ export class Parser implements TokenHandler, Stack return parser.document; } - public static parseFragment( - html: string, + public static getFragmentParser( fragmentContext?: T['parentNode'] | null, options?: ParserOptions - ): T['documentFragment'] { + ): Parser { const opts: Required> = { ...defaultParserOptions, ...options, @@ -194,12 +193,15 @@ export class Parser implements TokenHandler, Stack parser._insertFakeRootElement(); parser._resetInsertionMode(); parser._findFormInFragmentContext(); - parser.tokenizer.write(html, true); - const rootElement = opts.treeAdapter.getFirstChild(documentMock) as T['parentNode']; - const fragment = opts.treeAdapter.createDocumentFragment(); + return parser; + } + + public getFragment(): T['documentFragment'] { + const rootElement = this.treeAdapter.getFirstChild(this.document) as T['parentNode']; + const fragment = this.treeAdapter.createDocumentFragment(); - parser._adoptNodes(rootElement, fragment); + this._adoptNodes(rootElement, fragment); return fragment; } diff --git a/test/utils/generate-parsing-tests.ts b/test/utils/generate-parsing-tests.ts index e70dac8c1..4f913d1a6 100644 --- a/test/utils/generate-parsing-tests.ts +++ b/test/utils/generate-parsing-tests.ts @@ -137,19 +137,16 @@ export function generateParsingTests( name: string, prefix: string, { - skipFragments, withoutErrors, expectErrors: expectError = [], suitePath = treePath, - }: { skipFragments?: boolean; withoutErrors?: boolean; expectErrors?: string[]; suitePath?: URL }, + }: { withoutErrors?: boolean; expectErrors?: string[]; suitePath?: URL }, parse: ParseMethod ): void { generateTestsForEachTreeAdapter(name, (treeAdapter) => { const errorsToExpect = new Set(expectError); - for (const test of loadTreeConstructionTestData(suitePath, treeAdapter).filter( - (test) => !skipFragments || !test.fragmentContext - )) { + for (const test of loadTreeConstructionTestData(suitePath, treeAdapter)) { const expectError = errorsToExpect.delete(`${test.idx}.${test.setName}`); it(