Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(parser): Add hooks for stack events #385

Merged
merged 4 commits into from Feb 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 31 additions & 0 deletions 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;

Expand Down Expand Up @@ -98,4 +100,33 @@ describe('parser', () => {
expect(doctype).toHaveProperty('publicId', '');
expect(doctype).toHaveProperty('systemId', '');
});

describe('Tree adapters', () => {
it('should support onItemPush and onItemPop', () => {
const onItemPush = jest.fn();
const onItemPop = jest.fn();
const document = parse5.parse('<p><p>', {
treeAdapter: {
...treeAdapters.default,
onItemPush,
onItemPop,
},
});

const htmlElement = document.childNodes[0];
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);
fb55 marked this conversation as resolved.
Show resolved Hide resolved
});
});
});
3 changes: 3 additions & 0 deletions packages/parse5/lib/parser/index.ts
Expand Up @@ -317,6 +317,7 @@ export class Parser<T extends TreeAdapterTypeMap> {

//Text parsing
private onItemPush(node: T['parentNode'], tid: number, isTop: boolean): void {
this.treeAdapter.onItemPush?.(node);
if (isTop && this.openElements.stackTop > 0) this._setContextModes(node, tid);
}

Expand All @@ -325,6 +326,8 @@ export class Parser<T extends TreeAdapterTypeMap> {
this._setEndLocation(node, this.currentToken!);
}

this.treeAdapter.onItemPop?.(node, this.openElements.current);

if (isTop) {
let current;
let currentTagId;
Expand Down
14 changes: 14 additions & 0 deletions packages/parse5/lib/tree-adapters/interface.ts
Expand Up @@ -279,4 +279,18 @@ export interface TreeAdapter<T extends TreeAdapterTypeMap = TreeAdapterTypeMap>
* @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'], newTop: T['parentNode']) => void;
}