From 1aaf5eb6a2e2e91782f7fc8c2d2c06958419601e Mon Sep 17 00:00:00 2001 From: Patrick Szmucer Date: Mon, 3 May 2021 00:30:27 +0100 Subject: [PATCH 1/4] Add failing tests --- src/__tests__/nonstandard.js | 54 ++++++++++++++++++++++++++++++++---- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/src/__tests__/nonstandard.js b/src/__tests__/nonstandard.js index 422112b..8015ea8 100644 --- a/src/__tests__/nonstandard.js +++ b/src/__tests__/nonstandard.js @@ -4,14 +4,32 @@ test('non-standard selector', '.icon.is-$(network)', (t, tree) => { let class1 = tree.nodes[0].nodes[0]; t.deepEqual(class1.value, 'icon'); t.deepEqual(class1.type, 'class'); + t.deepEqual(class1.source.start.column, 1); + t.deepEqual(class1.source.end.column, 5); + t.deepEqual(class1.sourceIndex, 0); + let class2 = tree.nodes[0].nodes[1]; t.deepEqual(class2.value, 'is-$(network)'); t.deepEqual(class2.type, 'class'); + t.deepEqual(class2.source.start.column, 6); + // t.deepEqual(class2.source.end.column, 19); // Fail - 10 + t.deepEqual(class2.sourceIndex, 5); }); test('at word in selector', 'em@il.com', (t, tree) => { - t.deepEqual(tree.nodes[0].nodes[0].value, 'em@il'); - t.deepEqual(tree.nodes[0].nodes[1].value, 'com'); + const node1 = tree.nodes[0].nodes[0]; + t.deepEqual(node1.value, 'em@il'); + t.deepEqual(node1.type, 'tag'); + t.deepEqual(node1.source.start.column, 1); + t.deepEqual(node1.source.end.column, 5); + t.deepEqual(node1.sourceIndex, 0); + + const node2 = tree.nodes[0].nodes[1]; + t.deepEqual(node2.value, 'com'); + t.deepEqual(node2.type, 'class'); + t.deepEqual(node2.source.start.column, 6); + t.deepEqual(node2.source.end.column, 9); + t.deepEqual(node2.sourceIndex, 5); }); test('leading combinator', '> *', (t, tree) => { @@ -20,8 +38,12 @@ test('leading combinator', '> *', (t, tree) => { }); test('sass escapes', '.#{$classname}', (t, tree) => { - t.deepEqual(tree.nodes[0].nodes[0].type, "class"); - t.deepEqual(tree.nodes[0].nodes[0].value, "#{$classname}"); + const node = tree.nodes[0].nodes[0]; + t.deepEqual(node.type, "class"); + t.deepEqual(node.value, "#{$classname}"); + t.deepEqual(node.source.start.column, 1); + t.deepEqual(node.source.end.column, 14); + t.deepEqual(node.sourceIndex, 0); }); test('sass escapes (2)', '[lang=#{$locale}]', (t, tree) => { @@ -31,14 +53,34 @@ test('sass escapes (2)', '[lang=#{$locale}]', (t, tree) => { t.deepEqual(tree.nodes[0].nodes[0].value, "#{$locale}"); }); +test('sass escapes (3)', '.classname1.#{$classname2}', (t, tree) => { + const node1 = tree.nodes[0].nodes[0]; + t.deepEqual(node1.type, "class"); + t.deepEqual(node1.value, "classname1"); + t.deepEqual(node1.source.start.column, 1); + t.deepEqual(node1.source.end.column, 11); + t.deepEqual(node1.sourceIndex, 0); + + const node2 = tree.nodes[0].nodes[1]; + t.deepEqual(node2.type, "class"); + t.deepEqual(node2.value, "#{$classname2}"); + t.deepEqual(node2.source.start.column, 12); + t.deepEqual(node2.source.end.column, 26); + t.deepEqual(node2.sourceIndex, 11); +}); + test('placeholder', '%foo', (t, tree) => { t.deepEqual(tree.nodes[0].nodes[0].type, "tag"); t.deepEqual(tree.nodes[0].nodes[0].value, "%foo"); }); test('styled selector', '${Step}', (t, tree) => { - t.deepEqual(tree.nodes[0].nodes[0].type, "tag"); - t.deepEqual(tree.nodes[0].nodes[0].value, "${Step}"); + const node = tree.nodes[0].nodes[0]; + t.deepEqual(node.type, "tag"); + t.deepEqual(node.value, "${Step}"); + t.deepEqual(node.source.start.column, 1); + t.deepEqual(node.source.end.column, 7); + t.deepEqual(node.sourceIndex, 0); }); test('styled selector (2)', '${Step}:nth-child(odd)', (t, tree) => { From 84a5154a64dc171baa204b5051eb8cb9e3620576 Mon Sep 17 00:00:00 2001 From: Patrick Szmucer Date: Mon, 3 May 2021 00:30:43 +0100 Subject: [PATCH 2/4] Fix tests --- src/parser.js | 132 ++++++++++++++++++++++--------------------- src/sortAscending.js | 3 - 2 files changed, 67 insertions(+), 68 deletions(-) delete mode 100644 src/sortAscending.js diff --git a/src/parser.js b/src/parser.js index 64859c2..6236c59 100644 --- a/src/parser.js +++ b/src/parser.js @@ -11,7 +11,6 @@ import Universal from './selectors/universal'; import Combinator from './selectors/combinator'; import Nesting from './selectors/nesting'; -import sortAsc from './sortAscending'; import tokenize, {FIELDS as TOKEN} from './tokenize'; import * as tokens from './tokenTypes'; @@ -95,23 +94,6 @@ function unescapeProp (node, prop) { return node; } -function indexesOf (array, item) { - let i = -1; - const indexes = []; - - while ((i = array.indexOf(item, i + 1)) !== -1) { - indexes.push(i); - } - - return indexes; -} - -function uniqs () { - const list = Array.prototype.concat.apply([], arguments); - - return list.filter((item, i) => i === list.indexOf(item)); -} - export default class Parser { constructor (rule, options = {}) { this.rule = rule; @@ -818,74 +800,94 @@ export default class Parser { } splitWord (namespace, firstCallback) { + // Collect all the relevant tokens together + const tokensList = [this.currToken]; let nextToken = this.nextToken; - let word = this.content(); while ( nextToken && ~[tokens.dollar, tokens.caret, tokens.equals, tokens.word].indexOf(nextToken[TOKEN.TYPE]) ) { this.position ++; - let current = this.content(); - word += current; - if (current.lastIndexOf('\\') === current.length - 1) { + const token = this.currToken; + tokensList.push(token); + if (this.content(token).endsWith('\\')) { let next = this.nextToken; if (next && next[TOKEN.TYPE] === tokens.space) { - word += this.requiredSpace(this.content(next)); + tokensList.push(next); this.position ++; } } nextToken = this.nextToken; } - const hasClass = indexesOf(word, '.').filter(i => word[i - 1] !== '\\'); - let hasId = indexesOf(word, '#').filter(i => word[i - 1] !== '\\'); - // Eliminate Sass interpolations from the list of id indexes - const interpolations = indexesOf(word, '#{'); - if (interpolations.length) { - hasId = hasId.filter(hashIndex => !~interpolations.indexOf(hashIndex)); + + // Get the content of each token + const tokensContent = tokensList.map(token => { + if (token[TOKEN.TYPE] === tokens.space) { + return this.requiredSpace(token); + } + return this.content(token); + }); + + // Parse the list of tokens and create a list of new nodes + const nodesToCreate = []; + let inProgressNode; + tokensList.forEach((token, tokenIndex) => { + const content = tokensContent[tokenIndex]; + for (let i = 0; i < content.length; i++) { + const char = content[i]; + const prevChar = content[i - 1] || (tokenIndex !== 0 ? tokensContent[tokenIndex - 1].slice(-1) : undefined); + const nextChar = content[i + 1] || (tokenIndex !== tokensContent.length - 1 ? tokensContent[tokenIndex + 1][0] : undefined); + + if (char === "." && prevChar !== "\\") { + initNode(ClassName); + } else if (char === "#" && prevChar !== "\\" && nextChar !== "{") { + initNode(ID); + } else if (!inProgressNode) { + initNode(Tag, char); + } else { + inProgressNode.value += char; + inProgressNode.endToken = token; + inProgressNode.endIndex = i; + } + + function initNode (NodeConstructor, value = "") { + if (inProgressNode) { + nodesToCreate.push(inProgressNode); + } + inProgressNode = { + NodeConstructor, + value, + startToken: token, + endToken: token, + startIndex: i, + endIndex: i, + }; + } + } + }); + if (inProgressNode) { + nodesToCreate.push(inProgressNode); } - let indices = sortAsc(uniqs([0, ...hasClass, ...hasId])); - indices.forEach((ind, i) => { - const index = indices[i + 1] || word.length; - const value = word.slice(ind, index); + + nodesToCreate.forEach((node, i) => { if (i === 0 && firstCallback) { - return firstCallback.call(this, value, indices.length); + firstCallback.call(this, node.value, nodesToCreate.length); + return; } - let node; - const current = this.currToken; - const sourceIndex = current[TOKEN.START_POS] + indices[i]; + const {NodeConstructor, value, startToken, endToken, startIndex, endIndex} = node; + const sourceIndex = startToken[TOKEN.START_POS] + startIndex; const source = getSource( - current[1], - current[2] + ind, - current[3], - current[2] + (index - 1) + startToken[TOKEN.START_LINE], + startToken[TOKEN.START_COL] + startIndex, + endToken[TOKEN.END_LINE], + endToken[TOKEN.START_COL] + endIndex ); - if (~hasClass.indexOf(ind)) { - let classNameOpts = { - value: value.slice(1), - source, - sourceIndex, - }; - node = new ClassName(unescapeProp(classNameOpts, "value")); - } else if (~hasId.indexOf(ind)) { - let idOpts = { - value: value.slice(1), - source, - sourceIndex, - }; - node = new ID(unescapeProp(idOpts, "value")); - } else { - let tagOpts = { - value, - source, - sourceIndex, - }; - unescapeProp(tagOpts, "value"); - node = new Tag(tagOpts); - } - this.newNode(node, namespace); + const opts = unescapeProp({value, source, sourceIndex}, "value"); + this.newNode(new NodeConstructor(opts), namespace); // Ensure that the namespace is used only once namespace = null; }); + this.position ++; } diff --git a/src/sortAscending.js b/src/sortAscending.js deleted file mode 100644 index 824e3b4..0000000 --- a/src/sortAscending.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function sortAscending (list) { - return list.sort((a, b) => a - b); -}; From 6072323b5851a0568b7b7b3387a6ef164b020484 Mon Sep 17 00:00:00 2001 From: Patrick Szmucer Date: Mon, 3 May 2021 14:23:51 +0100 Subject: [PATCH 3/4] Add test for selector with spaces --- src/__tests__/nonstandard.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/__tests__/nonstandard.js b/src/__tests__/nonstandard.js index 8015ea8..640ae3c 100644 --- a/src/__tests__/nonstandard.js +++ b/src/__tests__/nonstandard.js @@ -69,6 +69,15 @@ test('sass escapes (3)', '.classname1.#{$classname2}', (t, tree) => { t.deepEqual(node2.sourceIndex, 11); }); +test('Sass escapes (4)', `.#{classname1}\\ classname2`, (t, tree) => { + const node = tree.nodes[0].nodes[0]; + t.deepEqual(node.type, "class"); + t.deepEqual(node.value, "#{classname1} classname2"); + t.deepEqual(node.source.start.column, 1); + t.deepEqual(node.source.end.column, 26); + t.deepEqual(node.sourceIndex, 0); +}); + test('placeholder', '%foo', (t, tree) => { t.deepEqual(tree.nodes[0].nodes[0].type, "tag"); t.deepEqual(tree.nodes[0].nodes[0].value, "%foo"); From b4c1b7193a3d7ece692ef0dcf6b81f3e5ce4a08a Mon Sep 17 00:00:00 2001 From: Patrick Szmucer Date: Mon, 3 May 2021 14:26:50 +0100 Subject: [PATCH 4/4] Test for an escaped dollar instead --- src/__tests__/nonstandard.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/__tests__/nonstandard.js b/src/__tests__/nonstandard.js index 640ae3c..ba47151 100644 --- a/src/__tests__/nonstandard.js +++ b/src/__tests__/nonstandard.js @@ -69,12 +69,12 @@ test('sass escapes (3)', '.classname1.#{$classname2}', (t, tree) => { t.deepEqual(node2.sourceIndex, 11); }); -test('Sass escapes (4)', `.#{classname1}\\ classname2`, (t, tree) => { +test('Sass escapes (4)', `.#{$classname1}\\$classname2`, (t, tree) => { const node = tree.nodes[0].nodes[0]; t.deepEqual(node.type, "class"); - t.deepEqual(node.value, "#{classname1} classname2"); + t.deepEqual(node.value, "#{$classname1}$classname2"); t.deepEqual(node.source.start.column, 1); - t.deepEqual(node.source.end.column, 26); + t.deepEqual(node.source.end.column, 27); t.deepEqual(node.sourceIndex, 0); });