From 1fe0b6accf718c68f46a12e3cb701e49a7f6b476 Mon Sep 17 00:00:00 2001 From: George Griffiths Date: Fri, 9 Oct 2020 13:37:27 +0100 Subject: [PATCH] pass at coverage for new 0.8.0 feats --- package.json | 2 +- src/normalize.js | 165 +++++++++++++++++++++++++++++++++++++++ src/querySelectorDeep.js | 158 +------------------------------------ test/basic.spec.js | 25 +++++- 4 files changed, 192 insertions(+), 158 deletions(-) create mode 100644 src/normalize.js diff --git a/package.json b/package.json index 0a7f8fb..558dd4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "query-selector-shadow-dom", - "version": "0.7.1", + "version": "0.8.0", "description": "use querySelector syntax to search for nodes inside of (nested) shadow roots", "main": "src/querySelectorDeep.js", "scripts": { diff --git a/src/normalize.js b/src/normalize.js new file mode 100644 index 0000000..c446e21 --- /dev/null +++ b/src/normalize.js @@ -0,0 +1,165 @@ +/* istanbul ignore file */ + + +// normalize-selector-rev-02.js +/* + author: kyle simpson (@getify) + original source: https://gist.github.com/getify/9679380 + + modified for tests by david kaye (@dfkaye) + 21 march 2014 + + rev-02 incorporate kyle's changes 3/2/42014 +*/ + +export function normalizeSelector(sel) { + // save unmatched text, if any + function saveUnmatched() { + if (unmatched) { + // whitespace needed after combinator? + if (tokens.length > 0 && /^[~+>]$/.test(tokens[tokens.length - 1])) { + tokens.push(" "); + } + + // save unmatched text + tokens.push(unmatched); + } + } + + var tokens = [], + match, + unmatched, + regex, + state = [0], + next_match_idx = 0, + prev_match_idx, + not_escaped_pattern = /(?:[^\\]|(?:^|[^\\])(?:\\\\)+)$/, + whitespace_pattern = /^\s+$/, + state_patterns = [ + /\s+|\/\*|["'>~+\[\(]/g, // general + /\s+|\/\*|["'\[\]\(\)]/g, // [..] set + /\s+|\/\*|["'\[\]\(\)]/g, // (..) set + null, // string literal (placeholder) + /\*\//g, // comment + ]; + sel = sel.trim(); + + while (true) { + unmatched = ""; + + regex = state_patterns[state[state.length - 1]]; + + regex.lastIndex = next_match_idx; + match = regex.exec(sel); + + // matched text to process? + if (match) { + prev_match_idx = next_match_idx; + next_match_idx = regex.lastIndex; + + // collect the previous string chunk not matched before this token + if (prev_match_idx < next_match_idx - match[0].length) { + unmatched = sel.substring( + prev_match_idx, + next_match_idx - match[0].length + ); + } + + // general, [ ] pair, ( ) pair? + if (state[state.length - 1] < 3) { + saveUnmatched(); + + // starting a [ ] pair? + if (match[0] === "[") { + state.push(1); + } + // starting a ( ) pair? + else if (match[0] === "(") { + state.push(2); + } + // starting a string literal? + else if (/^["']$/.test(match[0])) { + state.push(3); + state_patterns[3] = new RegExp(match[0], "g"); + } + // starting a comment? + else if (match[0] === "/*") { + state.push(4); + } + // ending a [ ] or ( ) pair? + else if (/^[\]\)]$/.test(match[0]) && state.length > 0) { + state.pop(); + } + // handling whitespace or a combinator? + else if (/^(?:\s+|[~+>])$/.test(match[0])) { + // need to insert whitespace before? + if ( + tokens.length > 0 && + !whitespace_pattern.test(tokens[tokens.length - 1]) && + state[state.length - 1] === 0 + ) { + // add normalized whitespace + tokens.push(" "); + } + + // case-insensitive attribute selector CSS L4 + if ( + state[state.length - 1] === 1 && + tokens.length === 5 && + tokens[2].charAt(tokens[2].length - 1) === "=" + ) { + tokens[4] = " " + tokens[4]; + } + + // whitespace token we can skip? + if (whitespace_pattern.test(match[0])) { + continue; + } + } + + // save matched text + tokens.push(match[0]); + } + // otherwise, string literal or comment + else { + // save unmatched text + tokens[tokens.length - 1] += unmatched; + + // unescaped terminator to string literal or comment? + if (not_escaped_pattern.test(tokens[tokens.length - 1])) { + // comment terminator? + if (state[state.length - 1] === 4) { + // ok to drop comment? + if ( + tokens.length < 2 || + whitespace_pattern.test(tokens[tokens.length - 2]) + ) { + tokens.pop(); + } + // otherwise, turn comment into whitespace + else { + tokens[tokens.length - 1] = " "; + } + + // handled already + match[0] = ""; + } + + state.pop(); + } + + // append matched text to existing token + tokens[tokens.length - 1] += match[0]; + } + } + // otherwise, end of processing (no more matches) + else { + unmatched = sel.substr(next_match_idx); + saveUnmatched(); + + break; + } + } + + return tokens.join("").trim(); +} diff --git a/src/querySelectorDeep.js b/src/querySelectorDeep.js index dac70b6..a45be02 100644 --- a/src/querySelectorDeep.js +++ b/src/querySelectorDeep.js @@ -3,6 +3,8 @@ * License Apache-2.0 */ +import { normalizeSelector } from './normalize'; + /** * Finds first matching elements on the page that may be in a shadow root using a complex selector of n-depth * @@ -177,159 +179,3 @@ export function collectAllElementsDeep(selector = null, root, cachedElements = n return allElements.filter(el => el.matches(selector)); } -// normalize-selector-rev-02.js -/* - author: kyle simpson (@getify) - original source: https://gist.github.com/getify/9679380 - - modified for tests by david kaye (@dfkaye) - 21 march 2014 - - rev-02 incorporate kyle's changes 3/2/42014 -*/ - /* istanbul ignore next */ - function normalizeSelector(sel) { - - // save unmatched text, if any - function saveUnmatched() { - if (unmatched) { - // whitespace needed after combinator? - if (tokens.length > 0 && - /^[~+>]$/.test(tokens[tokens.length-1]) - ) { - tokens.push(" "); - } - - // save unmatched text - tokens.push(unmatched); - } - } - - var tokens = [], match, unmatched, regex, state = [0], - next_match_idx = 0, prev_match_idx, - not_escaped_pattern = /(?:[^\\]|(?:^|[^\\])(?:\\\\)+)$/, - whitespace_pattern = /^\s+$/, - state_patterns = [ - /\s+|\/\*|["'>~+\[\(]/g, // general - /\s+|\/\*|["'\[\]\(\)]/g, // [..] set - /\s+|\/\*|["'\[\]\(\)]/g, // (..) set - null, // string literal (placeholder) - /\*\//g // comment - ] - ; - - sel = sel.trim(); - - while (true) { - unmatched = ""; - - regex = state_patterns[state[state.length-1]]; - - regex.lastIndex = next_match_idx; - match = regex.exec(sel); - - // matched text to process? - if (match) { - prev_match_idx = next_match_idx; - next_match_idx = regex.lastIndex; - - // collect the previous string chunk not matched before this token - if (prev_match_idx < next_match_idx - match[0].length) { - unmatched = sel.substring(prev_match_idx,next_match_idx - match[0].length); - } - - // general, [ ] pair, ( ) pair? - if (state[state.length-1] < 3) { - saveUnmatched(); - - // starting a [ ] pair? - if (match[0] === "[") { - state.push(1); - } - // starting a ( ) pair? - else if (match[0] === "(") { - state.push(2); - } - // starting a string literal? - else if (/^["']$/.test(match[0])) { - state.push(3); - state_patterns[3] = new RegExp(match[0],"g"); - } - // starting a comment? - else if (match[0] === "/*") { - state.push(4); - } - // ending a [ ] or ( ) pair? - else if (/^[\]\)]$/.test(match[0]) && state.length > 0) { - state.pop(); - } - // handling whitespace or a combinator? - else if (/^(?:\s+|[~+>])$/.test(match[0])) { - - // need to insert whitespace before? - if (tokens.length > 0 && - !whitespace_pattern.test(tokens[tokens.length-1]) && - state[state.length-1] === 0 - ) { - // add normalized whitespace - tokens.push(" "); - } - - // case-insensitive attribute selector CSS L4 - if (state[state.length-1] === 1 && - tokens.length === 5 && - tokens[2].charAt(tokens[2].length-1) === '=') { - tokens[4] = " " + tokens[4]; - } - - // whitespace token we can skip? - if (whitespace_pattern.test(match[0])) { - continue; - } - } - - // save matched text - tokens.push(match[0]); - } - // otherwise, string literal or comment - else { - // save unmatched text - tokens[tokens.length-1] += unmatched; - - // unescaped terminator to string literal or comment? - if (not_escaped_pattern.test(tokens[tokens.length-1])) { - // comment terminator? - if (state[state.length-1] === 4) { - // ok to drop comment? - if (tokens.length < 2 || - whitespace_pattern.test(tokens[tokens.length-2]) - ) { - tokens.pop(); - } - // otherwise, turn comment into whitespace - else { - tokens[tokens.length-1] = " "; - } - - // handled already - match[0] = ""; - } - - state.pop(); - } - - // append matched text to existing token - tokens[tokens.length-1] += match[0]; - } - } - // otherwise, end of processing (no more matches) - else { - unmatched = sel.substr(next_match_idx); - saveUnmatched(); - - break; - } - } - - return tokens.join("").trim(); - } \ No newline at end of file diff --git a/test/basic.spec.js b/test/basic.spec.js index 016b28d..e751457 100644 --- a/test/basic.spec.js +++ b/test/basic.spec.js @@ -154,6 +154,29 @@ describe("Basic Suite", function() { expect(testComponents[0].classList.contains('find-me')).toEqual(true); }); + it('handles descendant selector > that dooes not match child', function() { + const testComponent = createTestComponent(parent, { + childClassName: 'header-1', + internalHTML: '
' + }); + testComponent.shadowRoot.querySelector('.header-2').host = "test.com"; + testComponent.classList.add('container'); + const testComponents = querySelectorAllDeep(`.container > div > .header-2 > .doesnt-exist`); + expect(testComponents.length).toEqual(0); + }); + + it('handles descendant selector where child exists but parent does not', function() { + const testComponent = createTestComponent(parent, { + childClassName: 'header-1', + internalHTML: '
' + }); + testComponent.shadowRoot.querySelector('.header-2').host = "test.com"; + testComponent.classList.add('container'); + const testComponents = querySelectorAllDeep(`.container > div > .doesnt-exist > .find-me`); + expect(testComponents.length).toEqual(0); + }); + + it('can handle extra white space in selectors', function() { const testComponent = createTestComponent(parent, { childClassName: 'header-1', @@ -340,7 +363,7 @@ describe("Basic Suite", function() { createTestComponent(parent, { childClassName: 'inner-content' }); - const collectedElements = collectAllElementsDeep('', root) + const collectedElements = collectAllElementsDeep('*', root) expect(collectedElements.length).toEqual(4); const testComponents = querySelectorAllDeep('.inner-content', root, collectedElements);