From c2d56fde0b7ace0ca205e1abc6784e1c33fd715f Mon Sep 17 00:00:00 2001 From: evilebottnawi Date: Tue, 19 Mar 2019 14:38:13 +0300 Subject: [PATCH] refactor: remove unnecessary stuff --- src/__tests__/attributes.js | 69 +++++++++++++++--- src/__tests__/exceptions.js | 43 +++++++---- src/parser.js | 141 +++++++++++++++++------------------- 3 files changed, 156 insertions(+), 97 deletions(-) diff --git a/src/__tests__/attributes.js b/src/__tests__/attributes.js index 59c28e7..6d32e7d 100644 --- a/src/__tests__/attributes.js +++ b/src/__tests__/attributes.js @@ -51,6 +51,11 @@ test('select elements with or without a namespace', '[*|href]', (t, tree) => { t.deepEqual(tree.nodes[0].nodes[0].attribute, 'href'); }); +test('select elements with or without a namespace (3)', '[ |href ]', (t, tree) => { + t.deepEqual(tree.nodes[0].nodes[0].namespace, true); + t.deepEqual(tree.nodes[0].nodes[0].attribute, 'href'); +}); + test('namespace with escapes', '[\\31 \\#\\32 |href]', (t, tree) => { let attr = tree.nodes[0].nodes[0]; t.deepEqual(attr.namespace, '1#2'); @@ -99,6 +104,30 @@ test('attribute selector with escaped quote', '[title="Something \\"weird\\""]', t.deepEqual(tree.toString(), '[title="Something \\"weird\\""]'); }); +test('attribute selector with escaped quote with comments', '[/*t*/title/*t*/=/*t*/"Something \\"weird\\""/*t*/]', (t, tree) => { + let attr = tree.nodes[0].nodes[0]; + t.deepEqual(attr.value, 'Something "weird"'); + t.deepEqual(attr.getQuotedValue(), '\"Something \\"weird\\"\"'); + t.deepEqual(attr.getQuotedValue({smart: true}), '\'Something "weird"\''); + t.deepEqual(attr.getQuotedValue({quoteMark: null}), 'Something\\ \\"weird\\"'); + t.deepEqual(attr.quoteMark, '"'); + t.truthy(attr.quoted); + t.deepEqual(attr.raws.value, '"Something \\"weird\\""/*t*/'); + t.deepEqual(tree.toString(), '[/*t*/title/*t*/=/*t*/"Something \\"weird\\""/*t*/]'); +}); + +test('attribute selector with escaped quote with comments (2)', '[ /*t*/ title /*t*/ = /*t*/ "Something \\"weird\\"" /*t*/ ]', (t, tree) => { + let attr = tree.nodes[0].nodes[0]; + t.deepEqual(attr.value, 'Something "weird"'); + t.deepEqual(attr.getQuotedValue(), '\"Something \\"weird\\"\"'); + t.deepEqual(attr.getQuotedValue({smart: true}), '\'Something "weird"\''); + t.deepEqual(attr.getQuotedValue({quoteMark: null}), 'Something\\ \\"weird\\"'); + t.deepEqual(attr.quoteMark, '"'); + t.truthy(attr.quoted); + t.deepEqual(attr.raws.value, '"Something \\"weird\\""'); + t.deepEqual(tree.toString(), '[ /*t*/ title /*t*/ = /*t*/ "Something \\"weird\\"" /*t*/ ]'); +}); + test('attribute selector with escaped colon', '[ng\\:cloak]', (t, tree) => { t.deepEqual(tree.toString(), '[ng\\:cloak]'); let attr = tree.nodes[0].nodes[0]; @@ -328,36 +357,58 @@ test('spaces in attribute selectors', 'h1[ href *= "test" ]', (t, tree) => { t.truthy(tree.nodes[0].nodes[1].quoted); }); -test('insensitive attribute selector 1', '[href="test" i]', (t, tree) => { +test('insensitive attribute selector', '[href=test i]', (t, tree) => { t.deepEqual(tree.nodes[0].nodes[0].value, 'test'); t.deepEqual(tree.nodes[0].nodes[0].insensitive, true); - t.deepEqual(tree.nodes[0].nodes[0].insensitive, true); }); -test('insensitive attribute selector with a empty value', '[href="" i]', (t, tree) => { - t.deepEqual(tree.nodes[0].nodes[0].attribute, 'href'); - t.deepEqual(tree.nodes[0].nodes[0].operator, '='); - t.deepEqual(tree.nodes[0].nodes[0].value, ''); +test('insensitive attribute selector 2', '[href="test" i]', (t, tree) => { + t.deepEqual(tree.nodes[0].nodes[0].value, 'test'); t.deepEqual(tree.nodes[0].nodes[0].insensitive, true); - t.true(tree.nodes[0].nodes[0].quoted); }); -test('insensitive attribute selector 2', '[href=TEsT i ]', (t, tree) => { +test('insensitive attribute selector 3', '[href=TEsT i ]', (t, tree) => { t.deepEqual(tree.nodes[0].nodes[0].value, 'TEsT'); t.deepEqual(tree.nodes[0].nodes[0].insensitive, true); t.deepEqual(tree.nodes[0].nodes[0].spaces.value.after, ' '); t.deepEqual(tree.nodes[0].nodes[0].spaces.insensitive.after, ' '); }); -test('insensitive attribute selector 3', '[href=test i]', (t, tree) => { +test('insensitive attribute selector 4', '[href=test i]', (t, tree) => { t.deepEqual(tree.nodes[0].nodes[0].value, 'test'); t.deepEqual(tree.nodes[0].nodes[0].insensitive, true); }); + +test('insensitive attribute selector 5', '[href="" i]', (t, tree) => { + t.deepEqual(tree.nodes[0].nodes[0].value, ''); + t.deepEqual(tree.nodes[0].nodes[0].insensitive, true); +}); + +test('sensitive attribute selector', '[href=test s]', (t, tree) => { + t.deepEqual(tree.nodes[0].nodes[0].value, 'test'); + t.deepEqual(tree.nodes[0].nodes[0].insensitive, false); + t.deepEqual(tree.nodes[0].nodes[0].raws.insensitiveFlag, 's'); +}); + +test('multiple flags attribute selector', '[href=test qwer]', (t, tree) => { + t.deepEqual(tree.nodes[0].nodes[0].value, 'test'); + t.deepEqual(tree.nodes[0].nodes[0].insensitive, false); + t.deepEqual(tree.nodes[0].nodes[0].raws.insensitiveFlag, 'qwer'); +}); + test('capitalized insensitive attribute selector 3', '[href=test I]', (t, tree) => { t.deepEqual(tree.nodes[0].nodes[0].value, 'test'); t.deepEqual(tree.nodes[0].nodes[0].insensitive, true); }); +test('insensitive attribute selector with a empty value', '[href="" i]', (t, tree) => { + t.deepEqual(tree.nodes[0].nodes[0].attribute, 'href'); + t.deepEqual(tree.nodes[0].nodes[0].operator, '='); + t.deepEqual(tree.nodes[0].nodes[0].value, ''); + t.deepEqual(tree.nodes[0].nodes[0].insensitive, true); + t.true(tree.nodes[0].nodes[0].quoted); +}); + test('extraneous non-combinating whitespace', ' [href] , [class] ', (t, tree) => { t.deepEqual(tree.nodes[0].nodes[0].attribute, 'href'); t.deepEqual(tree.nodes[0].nodes[0].spaces.before, ' '); diff --git a/src/__tests__/exceptions.js b/src/__tests__/exceptions.js index a8dec86..fc9629f 100644 --- a/src/__tests__/exceptions.js +++ b/src/__tests__/exceptions.js @@ -1,27 +1,40 @@ import {throws} from './util/helpers'; // Unclosed elements -throws('unclosed string', 'a[href="wow]'); -throws('unclosed comment', '/* oops'); -throws('unclosed pseudo element', 'button::'); -throws('unclosed pseudo class', 'a:'); -throws('unclosed attribute selector', '[name="james"][href'); +throws('unclosed string', 'a[href="wow]', 'Unclosed quote'); +throws('unclosed comment', '/* oops', 'Unclosed comment'); +throws('unclosed pseudo element', 'button::', 'Expected a pseudo-class or pseudo-element.'); +throws('unclosed pseudo class', 'a:', 'Expected a pseudo-class or pseudo-element.'); +throws('unclosed attribute selector', '[name', 'Expected a closing square bracket.'); +throws('unclosed attribute selector (2)', '[name=', 'Expected a closing square bracket.'); +throws('unclosed attribute selector (3)', '[name="james"', 'Expected a closing square bracket.'); +throws('unclosed attribute selector (4)', '[name="james"][href', 'Expected a closing square bracket.'); +throws('unclosed attribute selector (5)', '[name="james"][href', 'Expected a closing square bracket.'); -throws('no opening parenthesis', ')'); -throws('no opening parenthesis (2)', ':global.foo)'); -throws('no opening parenthesis (3)', 'h1:not(h2:not(h3)))'); +throws('invalid attribute selector', '[]', 'Expected an attribute.'); +throws('invalid attribute selector (2)', '["hello"]', 'Expected an attribute.'); +throws('invalid attribute selector (3)', '[="hello"]', 'Expected an attribute, found "=" instead.'); -throws('no opening square bracket', ']'); -throws('no opening square bracket (2)', ':global.foo]'); -throws('no opening square bracket (3)', '[global]]'); +throws('no opening parenthesis', ')', 'Expected an opening parenthesis.'); +throws('no opening parenthesis (2)', ':global.foo)', 'Expected an opening parenthesis.'); +throws('no opening parenthesis (3)', 'h1:not(h2:not(h3)))', 'Expected an opening parenthesis.'); -throws('bad pseudo element', 'button::"after"'); -throws('missing closing parenthesis in pseudo', ':not([attr="test"]:not([attr="test"])'); +throws('no opening square bracket', ']', 'Expected an opening square bracket.'); +throws('no opening square bracket (2)', ':global.foo]', 'Expected an opening square bracket.'); +throws('no opening square bracket (3)', '[global]]', 'Expected an opening square bracket.'); -throws('bad syntax', '-moz-osx-font-smoothing: grayscale'); -throws('bad syntax (2)', '! .body'); +throws('bad pseudo element', 'button::"after"', 'Expected a pseudo-class or pseudo-element.'); +throws('missing closing parenthesis in pseudo', ':not([attr="test"]:not([attr="test"])', 'Expected a closing parenthesis.'); + +throws('bad syntax', '-moz-osx-font-smoothing: grayscale', 'Expected a pseudo-class or pseudo-element.'); +throws('bad syntax (2)', '! .body', 'Unexpected \'!\'. Escaping special characters with \\ may help.'); throws('missing backslash for semicolon', '.;'); throws('missing backslash for semicolon (2)', '.\;'); throws('unexpected / foo', '-Option\/root', "Unexpected '/'. Escaping special characters with \\ may help."); throws('bang in selector', '.foo !optional', "Unexpected '!'. Escaping special characters with \\ may help."); + +throws('misplaced parenthesis', ':not(', 'Expected a closing parenthesis.'); +throws('misplaced parenthesis (2)', ':not)', 'Expected an opening parenthesis.'); +throws('misplaced parenthesis (3)', ':not((', 'Expected a closing parenthesis.'); +throws('misplaced parenthesis (4)', ':not))', 'Expected an opening parenthesis.'); diff --git a/src/parser.js b/src/parser.js index 736d239..d6dbdc7 100644 --- a/src/parser.js +++ b/src/parser.js @@ -144,11 +144,18 @@ export default class Parser { attr.push(this.currToken); this.position ++; } - if (this.currToken[TOKEN.TYPE] !== tokens.closeSquare) { - return this.expected('closing square bracket', this.currToken[TOKEN.START_POS]); + + if (!this.currToken) { + this.position --; + return this.missingClosingSquareBracket(); } const len = attr.length; + + if (len === 0) { + return this.expected('attribute', this.currToken[TOKEN.START_POS]); + } + const node = { source: getSource( startingToken[1], @@ -298,32 +305,27 @@ export default class Parser { node.raws.value = (oldRawValue || oldValue) + content; } lastAdded = 'value'; - } else { + } else if ( + (node.value || node.value === '') && + (node.quoteMark || spaceAfterMeaningfulToken) + ) { let insensitive = (content === 'i' || content === "I"); - if ((node.value || node.value === '') && (node.quoteMark || spaceAfterMeaningfulToken)) { - node.insensitive = insensitive; - if (!insensitive || content === "I") { - ensureObject(node, 'raws'); - node.raws.insensitiveFlag = content; - } - lastAdded = 'insensitive'; - if (spaceBefore) { - ensureObject(node, 'spaces', 'insensitive'); - node.spaces.insensitive.before = spaceBefore; - - spaceBefore = ''; - } - if (commentBefore) { - ensureObject(node, 'raws', 'spaces', 'insensitive'); - node.raws.spaces.insensitive.before = commentBefore; - commentBefore = ''; - } - } else if (node.value || node.value === '') { - lastAdded = 'value'; - node.value += content; - if (node.raws.value) { - node.raws.value += content; - } + node.insensitive = insensitive; + if (!insensitive || content === "I") { + ensureObject(node, 'raws'); + node.raws.insensitiveFlag = content; + } + lastAdded = 'insensitive'; + if (spaceBefore) { + ensureObject(node, 'spaces', 'insensitive'); + node.spaces.insensitive.before = spaceBefore; + + spaceBefore = ''; + } + if (commentBefore) { + ensureObject(node, 'raws', 'spaces', 'insensitive'); + node.raws.spaces.insensitive.before = commentBefore; + commentBefore = ''; } } spaceAfterMeaningfulToken = false; @@ -465,8 +467,7 @@ export default class Parser { if (rawSpace === space) { rawSpace = undefined; } - let result = {space, rawSpace}; - return result; + return {space, rawSpace}; } isNamedCombinator (position = this.position) { @@ -475,30 +476,27 @@ export default class Parser { this.tokens[position + 2] && this.tokens[position + 2][TOKEN.TYPE] === tokens.slash; } + namedCombinator () { - if (this.isNamedCombinator()) { - let nameRaw = this.content(this.tokens[this.position + 1]); - let name = unesc(nameRaw).toLowerCase(); - let raws = {}; - if (name !== nameRaw) { - raws.value = `/${nameRaw}/`; - } - let node = new Combinator({ - value: `/${name}/`, - source: getSource( - this.currToken[TOKEN.START_LINE], - this.currToken[TOKEN.START_COL], - this.tokens[this.position + 2][TOKEN.END_LINE], - this.tokens[this.position + 2][TOKEN.END_COL], - ), - sourceIndex: this.currToken[TOKEN.START_POS], - raws, - }); - this.position = this.position + 3; - return node; - } else { - this.unexpected(); + let nameRaw = this.content(this.tokens[this.position + 1]); + let name = unesc(nameRaw).toLowerCase(); + let raws = {}; + if (name !== nameRaw) { + raws.value = `/${nameRaw}/`; } + let node = new Combinator({ + value: `/${name}/`, + source: getSource( + this.currToken[TOKEN.START_LINE], + this.currToken[TOKEN.START_COL], + this.tokens[this.position + 2][TOKEN.END_LINE], + this.tokens[this.position + 2][TOKEN.END_COL], + ), + sourceIndex: this.currToken[TOKEN.START_POS], + raws, + }); + this.position = this.position + 3; + return node; } combinator () { @@ -620,14 +618,22 @@ export default class Parser { }); } - missingParenthesis () { + missingOpeningParenthesis () { return this.expected('opening parenthesis', this.currToken[TOKEN.START_POS]); } - missingSquareBracket () { + missingClosingParenthesis () { + return this.expected('closing parenthesis', this.currToken[TOKEN.START_POS]); + } + + missingOpeningSquareBracket () { return this.expected('opening square bracket', this.currToken[TOKEN.START_POS]); } + missingClosingSquareBracket () { + return this.expected('closing square bracket', this.currToken[TOKEN.START_POS]); + } + unexpected () { return this.error(`Unexpected '${this.content()}'. Escaping special characters with \\ may help.`, this.currToken[TOKEN.START_POS]); } @@ -702,6 +708,10 @@ export default class Parser { parenValue += this.parseParenthesisToken(this.currToken); this.position ++; } + if (unbalanced) { + this.position --; + return this.missingClosingParenthesis(); + } if (last) { last.appendToPropertyAndEscape("value", parenValue, parenValue); } else { @@ -718,7 +728,8 @@ export default class Parser { } } if (unbalanced) { - return this.expected('closing parenthesis', this.currToken[TOKEN.START_POS]); + this.position --; + return this.missingClosingParenthesis(); } } @@ -733,22 +744,13 @@ export default class Parser { return this.expected(['pseudo-class', 'pseudo-element'], this.position - 1); } if (this.currToken[TOKEN.TYPE] === tokens.word) { - this.splitWord(false, (first, length) => { + this.splitWord(false, (first) => { pseudoStr += first; this.newNode(new Pseudo({ value: pseudoStr, source: getTokenSourceSpan(startingToken, this.currToken), sourceIndex: startingToken[TOKEN.START_POS], })); - if ( - length > 1 && - this.nextToken && - this.nextToken[TOKEN.TYPE] === tokens.openParenthesis - ) { - this.error('Misplaced parenthesis.', { - index: this.nextToken[TOKEN.START_POS], - }); - } }); } else { return this.expected(['pseudo-class', 'pseudo-element'], this.currToken[TOKEN.START_POS]); @@ -813,13 +815,6 @@ export default class Parser { this.position ++; let current = this.content(); word += current; - if (current.lastIndexOf('\\') === current.length - 1) { - let next = this.nextToken; - if (next && next[TOKEN.TYPE] === tokens.space) { - word += this.requiredSpace(this.content(next)); - this.position ++; - } - } nextToken = this.nextToken; } const hasClass = indexesOf(word, '.').filter(i => word[i - 1] !== '\\'); @@ -905,7 +900,7 @@ export default class Parser { break; case tokens.closeParenthesis: if (throwOnParenthesis) { - this.missingParenthesis(); + this.missingOpeningParenthesis(); } break; case tokens.openSquare: @@ -938,7 +933,7 @@ export default class Parser { break; // These cases throw; no break needed. case tokens.closeSquare: - this.missingSquareBracket(); + this.missingOpeningSquareBracket(); case tokens.semicolon: this.missingBackslash(); default: