Skip to content

Commit

Permalink
issue #119: add method closest
Browse files Browse the repository at this point in the history
  • Loading branch information
taoqf committed May 12, 2021
1 parent 83fa267 commit 4d47389
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 1 deletion.
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Fast HTML Parser [![NPM version](https://badge.fury.io/js/node-html-parser.png)](http://badge.fury.io/js/node-html-parser) [![Build Status](https://travis-ci.org/taoqf/node-html-parser.svg?branch=master)](https://travis-ci.org/taoqf/node-html-parser)

Fast HTML Parser is a _very fast_ HTML parser. Which will generate a simplified
DOM tree, with basic element query support.
DOM tree, with element query support.

Per the design, it intends to parse massive HTML files in lowest price, thus the
performance is the top priority. For this reason, some malformatted HTML may not
Expand Down Expand Up @@ -112,6 +112,10 @@ Note: Full css3 selector supported since v3.0.0.

Query CSS Selector to find matching node.

### HTMLElement#closest(selector)

Query closest element by css selector.

### HTMLElement#appendChild(node)

Append a child node to childNodes
Expand Down
58 changes: 58 additions & 0 deletions src/nodes/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,64 @@ export default class HTMLElement extends Node {
// return null;
}

/**
* traverses the Element and its parents (heading toward the document root) until it finds a node that matches the provided selector string. Will return itself or the matching ancestor. If no such element exists, it returns null.
* @param selector a DOMString containing a selector list
*/
public closest(selector: string) {
type Predicate = (node: Node) => node is HTMLElement;

const mapChild = new Map<Node, Node>();
let el = this as Node;
let old = null as Node;
function findOne(test: Predicate, elems: Node[]) {
let elem = null as HTMLElement | null;

for (let i = 0, l = elems.length; i < l && !elem; i++) {
const el = elems[i];
if (test(el)) {
elem = el;
} else {
const child = mapChild.get(el);
if (child) {
elem = findOne(test, [child]);
}
}
}
return elem;
}
while (el) {
mapChild.set(el, old);
old = el;
el = el.parentNode;
}
el = this;
while (el) {
const e = selectOne(selector, el, {
xmlMode: true,
adapter: {
...Matcher,
getChildren(node: Node) {
const child = mapChild.get(node);
return child && [child];
},
getSiblings(node: Node) {
return [node];
},
findOne,
findAll(): Node[] {
return [];
}
}
});
if (e) {
return e;
}
el = el.parentNode;
}
return null;
}

/**
* Append a child node to childNodes
* @param {Node} node node to append
Expand Down
55 changes: 55 additions & 0 deletions test/119.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const { parse } = require('../dist');

// https://github.com/taoqf/node-html-parser/pull/112
describe('issue 119 closest', function () {
it('query with .a.b', function () {
const html = `<div class="a b">
<div id="el">
<div class="a b">foo</div>
</div>
</div>`;
const root = parse(html);
const el = root.querySelector('#el');
const d = el.closest('.a.b');
d.toString().should.eql(html);
});
it('query with ul[item]', function () {
const html = `<ul item="111" id="list"><li>Hello World<ul item="222"></ul></li></ul>`;
const root = parse(html);
const li = root.querySelector('li');
const ul = li.closest('ul[item]');
ul.getAttribute('item').should.eql('111');
});
it('queries', async function () {
const html = '<div class="f"><span><div id="foo"></div></span></div>';
const root = parse(html);
const d = root.querySelector('#foo');
d.closest('div').toString().should.eql('<div id="foo"></div>')
d.closest('span').toString().should.eql('<span><div id="foo"></div></span>')
d.closest('div.f').toString().should.eql('<div class="f"><span><div id="foo"></div></span></div>');
});
it('84', async function () {
const root = parse(`<a id="id" data-id="myid">
<div>
<span class="a b"></span>
<span data-bar="bar">
<div id="foo">
<a id="id" data-id="myid"></a>
</div>
</span>
<span data-bar="foo"></span>
</div>
</a>`);
const div = root.querySelector('#foo');
div.closest('#id').should.eql(root.firstChild);
should.equal(div.closest('span.a'), null);
should.equal(div.closest('span.b'), null);
should.equal(div.closest('span.a.b'), null);
div.closest('span').getAttribute('data-bar').should.eql('bar');
div.closest('[data-bar]').getAttribute('data-bar').should.eql('bar');
div.closest('[data-bar="bar"]').getAttribute('data-bar').should.eql('bar');
should.equal(div.closest('[data-bar="foo"]'), null);
div.closest('[data-id=myid]').should.eql(root.firstChild);
div.closest('[data-id="myid"]').should.eql(root.firstChild);
});
});

0 comments on commit 4d47389

Please sign in to comment.