From 2409e2a4e3847fe11772cf1c46373c37c7092e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Tue, 18 Jan 2022 18:51:24 +0000 Subject: [PATCH 1/4] feat(parser): Add hooks for stack events Fixes #237 --- packages/parse5/lib/parser/index.test.ts | 24 ++++++++++++++++++++++++ packages/parse5/lib/parser/index.ts | 19 +++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/packages/parse5/lib/parser/index.test.ts b/packages/parse5/lib/parser/index.test.ts index 82fc0c06f..7ddd8a344 100644 --- a/packages/parse5/lib/parser/index.test.ts +++ b/packages/parse5/lib/parser/index.test.ts @@ -1,10 +1,12 @@ import * as assert from 'node:assert'; import * as parse5 from 'parse5'; +import { jest } from '@jest/globals'; import { Parser, ParserOptions } from './index.js'; import type { TreeAdapterTypeMap } from './../tree-adapters/interface.js'; import { generateParsingTests } from 'parse5-test-utils/utils/generate-parsing-tests.js'; import { treeAdapters } from 'parse5-test-utils/utils/common.js'; import { NAMESPACES as NS } from '../common/html.js'; +import { isElementNode } from '../tree-adapters/default.js'; const origParseFragment = Parser.prototype.parseFragment; @@ -98,4 +100,26 @@ describe('parser', () => { expect(doctype).toHaveProperty('publicId', ''); expect(doctype).toHaveProperty('systemId', ''); }); + + describe('Options', () => { + it('should support onItemPush and onItemPop', () => { + const onItemPush = jest.fn(); + const onItemPop = jest.fn(); + const document = parse5.parse('

', { + onItemPush, + onItemPop, + }); + + const htmlElement = document.childNodes[0]; + assert.ok(isElementNode(htmlElement)); + const bodyElement = htmlElement.childNodes[1]; + assert.ok(isElementNode(bodyElement)); + expect(onItemPush).toHaveBeenCalledTimes(5); + expect(onItemPush).toHaveBeenNthCalledWith(1, htmlElement); + expect(onItemPush).toHaveBeenNthCalledWith(3, bodyElement); + expect(onItemPush).toHaveBeenLastCalledWith(bodyElement.childNodes[1]); + expect(onItemPop).toHaveBeenCalledTimes(2); + expect(onItemPop).toHaveBeenLastCalledWith(bodyElement.childNodes[0]); + }); + }); }); diff --git a/packages/parse5/lib/parser/index.ts b/packages/parse5/lib/parser/index.ts index 3ddb4905b..57cc9c10d 100644 --- a/packages/parse5/lib/parser/index.ts +++ b/packages/parse5/lib/parser/index.ts @@ -109,6 +109,22 @@ export interface ParserOptions { * @default `null` */ onParseError?: ParserErrorHandler | null; + + /** + * Callback for elements being pushed to the stack of open elements. + * + * @default `null` + * @param element The element being pushed to the stack of open elements. + */ + onItemPush?: ((item: T['element']) => void) | null; + + /** + * Callback for elements being popped from the stack of open elements. + * + * @default `null` + * @param item The element being popped. + */ + onItemPop?: ((item: T['element']) => void) | null; } //Parser @@ -317,6 +333,7 @@ export class Parser { //Text parsing private onItemPush(node: T['parentNode'], tid: number, isTop: boolean): void { + this.options.onItemPush?.(node); if (isTop && this.openElements.stackTop > 0) this._setContextModes(node, tid); } @@ -325,6 +342,8 @@ export class Parser { this._setEndLocation(node, this.currentToken!); } + this.options.onItemPop?.(node); + if (isTop) { let current; let currentTagId; From 9c60b206039d3654c2f1760b4912291eb27ade5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Fri, 4 Feb 2022 20:57:09 -0600 Subject: [PATCH 2/4] Move stack events to tree adapter --- packages/parse5/lib/parser/index.test.ts | 9 ++++++--- packages/parse5/lib/parser/index.ts | 20 ++----------------- .../parse5/lib/tree-adapters/interface.ts | 14 +++++++++++++ 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/parse5/lib/parser/index.test.ts b/packages/parse5/lib/parser/index.test.ts index 7ddd8a344..e6a692f22 100644 --- a/packages/parse5/lib/parser/index.test.ts +++ b/packages/parse5/lib/parser/index.test.ts @@ -101,13 +101,16 @@ describe('parser', () => { expect(doctype).toHaveProperty('systemId', ''); }); - describe('Options', () => { + describe('Tree adapters', () => { it('should support onItemPush and onItemPop', () => { const onItemPush = jest.fn(); const onItemPop = jest.fn(); const document = parse5.parse('

', { - onItemPush, - onItemPop, + treeAdapter: { + ...treeAdapters.default, + onItemPush, + onItemPop, + }, }); const htmlElement = document.childNodes[0]; diff --git a/packages/parse5/lib/parser/index.ts b/packages/parse5/lib/parser/index.ts index 57cc9c10d..6a87e421f 100644 --- a/packages/parse5/lib/parser/index.ts +++ b/packages/parse5/lib/parser/index.ts @@ -109,22 +109,6 @@ export interface ParserOptions { * @default `null` */ onParseError?: ParserErrorHandler | null; - - /** - * Callback for elements being pushed to the stack of open elements. - * - * @default `null` - * @param element The element being pushed to the stack of open elements. - */ - onItemPush?: ((item: T['element']) => void) | null; - - /** - * Callback for elements being popped from the stack of open elements. - * - * @default `null` - * @param item The element being popped. - */ - onItemPop?: ((item: T['element']) => void) | null; } //Parser @@ -333,7 +317,7 @@ export class Parser { //Text parsing private onItemPush(node: T['parentNode'], tid: number, isTop: boolean): void { - this.options.onItemPush?.(node); + this.treeAdapter.onItemPush?.(node); if (isTop && this.openElements.stackTop > 0) this._setContextModes(node, tid); } @@ -342,7 +326,7 @@ export class Parser { this._setEndLocation(node, this.currentToken!); } - this.options.onItemPop?.(node); + this.treeAdapter.onItemPop?.(node); if (isTop) { let current; diff --git a/packages/parse5/lib/tree-adapters/interface.ts b/packages/parse5/lib/tree-adapters/interface.ts index d0cac0466..54137cfaf 100644 --- a/packages/parse5/lib/tree-adapters/interface.ts +++ b/packages/parse5/lib/tree-adapters/interface.ts @@ -279,4 +279,18 @@ export interface TreeAdapter * @param contentElement - Content element. */ setTemplateContent(templateElement: T['template'], contentElement: T['documentFragment']): void; + + /** + * Optional callback for elements being pushed to the stack of open elements. + * + * @param element The element being pushed to the stack of open elements. + */ + onItemPush?: (item: T['element']) => void; + + /** + * Optional callback for elements being popped from the stack of open elements. + * + * @param item The element being popped. + */ + onItemPop?: (item: T['element']) => void; } From cc0d661fa37dada169e25933d05b995f8286127c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Sat, 5 Feb 2022 08:43:17 -0600 Subject: [PATCH 3/4] Add new stack top as an additional parameter for `onItemPop` --- packages/parse5/lib/parser/index.test.ts | 2 +- packages/parse5/lib/parser/index.ts | 2 +- packages/parse5/lib/tree-adapters/interface.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/parse5/lib/parser/index.test.ts b/packages/parse5/lib/parser/index.test.ts index e6a692f22..407ad8d07 100644 --- a/packages/parse5/lib/parser/index.test.ts +++ b/packages/parse5/lib/parser/index.test.ts @@ -122,7 +122,7 @@ describe('parser', () => { expect(onItemPush).toHaveBeenNthCalledWith(3, bodyElement); expect(onItemPush).toHaveBeenLastCalledWith(bodyElement.childNodes[1]); expect(onItemPop).toHaveBeenCalledTimes(2); - expect(onItemPop).toHaveBeenLastCalledWith(bodyElement.childNodes[0]); + expect(onItemPop).toHaveBeenLastCalledWith(bodyElement.childNodes[0], bodyElement); }); }); }); diff --git a/packages/parse5/lib/parser/index.ts b/packages/parse5/lib/parser/index.ts index 6a87e421f..97b528237 100644 --- a/packages/parse5/lib/parser/index.ts +++ b/packages/parse5/lib/parser/index.ts @@ -326,7 +326,7 @@ export class Parser { this._setEndLocation(node, this.currentToken!); } - this.treeAdapter.onItemPop?.(node); + this.treeAdapter.onItemPop?.(node, this.openElements.current); if (isTop) { let current; diff --git a/packages/parse5/lib/tree-adapters/interface.ts b/packages/parse5/lib/tree-adapters/interface.ts index 54137cfaf..eaffab277 100644 --- a/packages/parse5/lib/tree-adapters/interface.ts +++ b/packages/parse5/lib/tree-adapters/interface.ts @@ -292,5 +292,5 @@ export interface TreeAdapter * * @param item The element being popped. */ - onItemPop?: (item: T['element']) => void; + onItemPop?: (item: T['element'], newTop: T['parentNode']) => void; } From bf5c969940d8050f24f37afd0e02a48f673e2d99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20B=C3=B6hm?= <188768+fb55@users.noreply.github.com> Date: Thu, 10 Feb 2022 16:40:30 +0100 Subject: [PATCH 4/4] Add some comments about which elements we are testing --- packages/parse5/lib/parser/index.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/parse5/lib/parser/index.test.ts b/packages/parse5/lib/parser/index.test.ts index 407ad8d07..53170bafb 100644 --- a/packages/parse5/lib/parser/index.test.ts +++ b/packages/parse5/lib/parser/index.test.ts @@ -117,11 +117,15 @@ describe('parser', () => { assert.ok(isElementNode(htmlElement)); const bodyElement = htmlElement.childNodes[1]; assert.ok(isElementNode(bodyElement)); + // Expect 5 opened elements; in order: html, head, body, and 2x p expect(onItemPush).toHaveBeenCalledTimes(5); expect(onItemPush).toHaveBeenNthCalledWith(1, htmlElement); expect(onItemPush).toHaveBeenNthCalledWith(3, bodyElement); + // The last opened element is the second p expect(onItemPush).toHaveBeenLastCalledWith(bodyElement.childNodes[1]); + // The second p isn't closed, plus we never pop body and html. Alas, only 2 pop events (head and p). expect(onItemPop).toHaveBeenCalledTimes(2); + // The last pop event should be the first p. expect(onItemPop).toHaveBeenLastCalledWith(bodyElement.childNodes[0], bodyElement); }); });