Skip to content

Commit f7f81db

Browse files
author
George Griffiths
committed
fixes #31 normalize css selectors
1 parent b26c93e commit f7f81db

File tree

4 files changed

+201
-11
lines changed

4 files changed

+201
-11
lines changed

README.md

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,17 @@ Both of the methods above accept a 2nd parameter, see section `Provide alternati
2020

2121
### CodeceptJS
2222

23-
2423
More details here: https://github.com/Georgegriff/query-selector-shadow-dom/blob/master/plugins/codeceptjs
2524

25+
### Puppeteer
26+
27+
There are some puppeteer examples available in the examples folder of this repository.
28+
29+
[Puppeteer examples](https://github.com/Georgegriff/query-selector-shadow-dom/blob/master/examples/puppeteer)
30+
2631
### Playwright
2732

28-
Update: as of Playwright v0.14.0 their CSS and text selectors work with shadow Dom our of the box, you don't need this library
33+
Update: as of Playwright v0.14.0 their CSS and text selectors work with shadow Dom our of the box, you don't need this library!
2934

3035
Playwright works really nicely with this package.
3136

@@ -44,13 +49,6 @@ const playwright = require('playwright');
4449

4550
For a full example see: https://github.com/Georgegriff/query-selector-shadow-dom/blob/master/examples/playwright
4651

47-
### Puppeteer
48-
49-
There are some puppeteer examples available in the examples folder of this repository.
50-
51-
[Puppeteer examples](https://github.com/Georgegriff/query-selector-shadow-dom/blob/master/examples/puppeteer)
52-
53-
5452
### Provide alternative node
5553
```javascript
5654
// query from another node

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "query-selector-shadow-dom",
3-
"version": "0.4.6",
3+
"version": "0.5.0",
44
"description": "use querySelector syntax to search for nodes inside of (nested) shadow roots",
55
"main": "src/querySelectorDeep.js",
66
"scripts": {

src/querySelectorDeep.js

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export function querySelectorDeep(selector, root = document) {
2626
}
2727

2828
function _querySelectorDeep(selector, findMany, root) {
29+
selector = normalizeSelector(selector)
2930
let lightElement = root.querySelector(selector);
3031

3132
if (document.head.createShadowRoot || document.head.attachShadow) {
@@ -150,4 +151,167 @@ function collectAllElementsDeep(selector = null, root) {
150151
findAllElements(root.querySelectorAll('*'));
151152

152153
return selector ? allElements.filter(el => el.matches(selector)) : allElements;
153-
}
154+
}
155+
156+
157+
// normalize-selector-rev-02.js
158+
/*
159+
author: kyle simpson (@getify)
160+
original source: https://gist.github.com/getify/9679380
161+
162+
modified for tests by david kaye (@dfkaye)
163+
21 march 2014
164+
165+
rev-02 incorporate kyle's changes 3/2/42014
166+
*/
167+
if (typeof require == 'function' && typeof module != 'undefined') {
168+
// enable to re-use in a browser without require.js
169+
module.exports = normalizeSelector;
170+
}
171+
172+
173+
function normalizeSelector(sel) {
174+
175+
// save unmatched text, if any
176+
function saveUnmatched() {
177+
if (unmatched) {
178+
// whitespace needed after combinator?
179+
if (tokens.length > 0 &&
180+
/^[~+>]$/.test(tokens[tokens.length-1])
181+
) {
182+
tokens.push(" ");
183+
}
184+
185+
// save unmatched text
186+
tokens.push(unmatched);
187+
}
188+
}
189+
190+
var tokens = [], match, unmatched, regex, state = [0],
191+
next_match_idx = 0, prev_match_idx,
192+
not_escaped_pattern = /(?:[^\\]|(?:^|[^\\])(?:\\\\)+)$/,
193+
whitespace_pattern = /^\s+$/,
194+
state_patterns = [
195+
/\s+|\/\*|["'>~+\[\(]/g, // general
196+
/\s+|\/\*|["'\[\]\(\)]/g, // [..] set
197+
/\s+|\/\*|["'\[\]\(\)]/g, // (..) set
198+
null, // string literal (placeholder)
199+
/\*\//g // comment
200+
]
201+
;
202+
203+
sel = sel.trim();
204+
205+
while (true) {
206+
unmatched = "";
207+
208+
regex = state_patterns[state[state.length-1]];
209+
210+
regex.lastIndex = next_match_idx;
211+
match = regex.exec(sel);
212+
213+
// matched text to process?
214+
if (match) {
215+
prev_match_idx = next_match_idx;
216+
next_match_idx = regex.lastIndex;
217+
218+
// collect the previous string chunk not matched before this token
219+
if (prev_match_idx < next_match_idx - match[0].length) {
220+
unmatched = sel.substring(prev_match_idx,next_match_idx - match[0].length);
221+
}
222+
223+
// general, [ ] pair, ( ) pair?
224+
if (state[state.length-1] < 3) {
225+
saveUnmatched();
226+
227+
// starting a [ ] pair?
228+
if (match[0] === "[") {
229+
state.push(1);
230+
}
231+
// starting a ( ) pair?
232+
else if (match[0] === "(") {
233+
state.push(2);
234+
}
235+
// starting a string literal?
236+
else if (/^["']$/.test(match[0])) {
237+
state.push(3);
238+
state_patterns[3] = new RegExp(match[0],"g");
239+
}
240+
// starting a comment?
241+
else if (match[0] === "/*") {
242+
state.push(4);
243+
}
244+
// ending a [ ] or ( ) pair?
245+
else if (/^[\]\)]$/.test(match[0]) && state.length > 0) {
246+
state.pop();
247+
}
248+
// handling whitespace or a combinator?
249+
else if (/^(?:\s+|[~+>])$/.test(match[0])) {
250+
251+
// need to insert whitespace before?
252+
if (tokens.length > 0 &&
253+
!whitespace_pattern.test(tokens[tokens.length-1]) &&
254+
state[state.length-1] === 0
255+
) {
256+
// add normalized whitespace
257+
tokens.push(" ");
258+
}
259+
260+
// case-insensitive attribute selector CSS L4
261+
if (state[state.length-1] === 1 &&
262+
tokens.length === 5 &&
263+
tokens[2].charAt(tokens[2].length-1) === '=') {
264+
tokens[4] = " " + tokens[4];
265+
}
266+
267+
// whitespace token we can skip?
268+
if (whitespace_pattern.test(match[0])) {
269+
continue;
270+
}
271+
}
272+
273+
// save matched text
274+
tokens.push(match[0]);
275+
}
276+
// otherwise, string literal or comment
277+
else {
278+
// save unmatched text
279+
tokens[tokens.length-1] += unmatched;
280+
281+
// unescaped terminator to string literal or comment?
282+
if (not_escaped_pattern.test(tokens[tokens.length-1])) {
283+
// comment terminator?
284+
if (state[state.length-1] === 4) {
285+
// ok to drop comment?
286+
if (tokens.length < 2 ||
287+
whitespace_pattern.test(tokens[tokens.length-2])
288+
) {
289+
tokens.pop();
290+
}
291+
// otherwise, turn comment into whitespace
292+
else {
293+
tokens[tokens.length-1] = " ";
294+
}
295+
296+
// handled already
297+
match[0] = "";
298+
}
299+
300+
state.pop();
301+
}
302+
303+
// append matched text to existing token
304+
tokens[tokens.length-1] += match[0];
305+
}
306+
}
307+
// otherwise, end of processing (no more matches)
308+
else {
309+
unmatched = sel.substr(next_match_idx);
310+
saveUnmatched();
311+
312+
break;
313+
}
314+
}
315+
316+
return tokens.join("").trim();
317+
}

test/basic.spec.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,34 @@ describe("Basic Suite", function() {
198198
expect(testComponents.length).toEqual(5);
199199
});
200200

201+
it('can handle spacing around attribute values', function() {
202+
const testComponent = createTestComponent(parent, {
203+
childClassName: 'header-1',
204+
internalHTML: '<div class="header-2">Content</div>'
205+
});
206+
const test2 = createTestComponent(testComponent, {
207+
childClassName: 'header-2'
208+
});
209+
test2.setAttribute('data-test', 'Hello, World')
210+
testComponent.classList.add('header-1');
211+
const testComponents = querySelectorAllDeep(`.header-1 [ data-test = "Hello, World" ], .header-2, .header-1`);
212+
expect(testComponents.length).toEqual(5);
213+
});
214+
215+
it('can handle spacing around attribute values with [ in attribute', function() {
216+
const testComponent = createTestComponent(parent, {
217+
childClassName: 'header-1',
218+
internalHTML: '<div class="header-2">Content</div>'
219+
});
220+
const test2 = createTestComponent(testComponent, {
221+
childClassName: 'header-2'
222+
});
223+
test2.setAttribute('data-braces-test', ' [ Hello, World ] ')
224+
testComponent.classList.add('header-1');
225+
const testComponents = querySelectorAllDeep(`.header-1 [ data-braces-test = " [ Hello, World ] " ], .header-2, .header-1`);
226+
expect(testComponents.length).toEqual(5);
227+
});
228+
201229
it('can escaped comma in attribute values', function() {
202230
const testComponent = createTestComponent(parent, {
203231
childClassName: 'header-1',

0 commit comments

Comments
 (0)