aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAmaury Levé2018-06-12 16:26:16 +0200
committerGitHub2018-06-12 16:26:16 +0200
commitdf153ba45fffa47f1bff7a4201d5fd16fc7b3445 (patch)
tree9e030ffbad7c5dfb71677634edb26c871fc76f67
parentfb56fdc0dc18d277ccfae2cdb948e9da367377ea (diff)
downloadsonar-css-df153ba45fffa47f1bff7a4201d5fd16fc7b3445.tar.bz2
Tokenize CSS (#40)
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/Token.java48
-rw-r--r--sonar-css-plugin/src/main/java/org/sonar/css/plugin/Tokenizer.java134
-rw-r--r--sonar-css-plugin/src/main/resources/tokenize.js374
-rw-r--r--sonar-css-plugin/src/test/java/org/sonar/css/plugin/TokenizerTest.java226
4 files changed, 782 insertions, 0 deletions
diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/Token.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/Token.java
new file mode 100644
index 0000000..dc9af61
--- /dev/null
+++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/Token.java
@@ -0,0 +1,48 @@
+/*
+ * SonarCSS
+ * Copyright (C) 2018-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.css.plugin;
+
+public class Token {
+
+ public enum Type {
+ COMMENT,
+ STRING,
+ WORD,
+ AT_WORD,
+ BRACKETS,
+ PUNCTUATOR
+ }
+
+ Type type;
+ String text;
+ Integer startLine;
+ Integer startColumn;
+ Integer endLine;
+ Integer endColumn;
+
+ public Token(Type type, String text, Integer startLine, Integer startColumn, Integer endLine, Integer endColumn) {
+ this.text = text;
+ this.type = type;
+ this.startLine = startLine;
+ this.startColumn = startColumn;
+ this.endLine = endLine;
+ this.endColumn = endColumn;
+ }
+}
diff --git a/sonar-css-plugin/src/main/java/org/sonar/css/plugin/Tokenizer.java b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/Tokenizer.java
new file mode 100644
index 0000000..8f03492
--- /dev/null
+++ b/sonar-css-plugin/src/main/java/org/sonar/css/plugin/Tokenizer.java
@@ -0,0 +1,134 @@
+/*
+ * SonarCSS
+ * Copyright (C) 2018-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.css.plugin;
+
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import javax.script.ScriptEngine;
+import javax.script.ScriptEngineManager;
+import javax.script.ScriptException;
+import org.sonar.api.internal.apachecommons.lang.StringEscapeUtils;
+import org.sonar.css.plugin.Token.Type;
+
+public class Tokenizer {
+
+ public List<Token> tokenize(String css) throws ScriptException {
+ ScriptEngineManager factory = new ScriptEngineManager();
+ ScriptEngine engine = factory.getEngineByName("JavaScript");
+ InputStream tokenizeScript = Tokenizer.class.getClassLoader().getResourceAsStream("tokenize.js");
+ engine.eval(new InputStreamReader(tokenizeScript, StandardCharsets.UTF_8));
+ String cssInput = "tokenize('" + StringEscapeUtils.escapeJavaScript(css) + "')";
+ Object tokens = engine.eval(cssInput);
+ return extractTokens(tokens);
+ }
+
+ private static List<Token> extractTokens(Object tokens) {
+ // tokens is result of call to javascript function tokenize(). It returns an array of arrays, where nested arrays
+ // correspond to tokens. These array javascript objects mapped in Java to Map objects where array index is key.
+
+ List<Token> resultList = new ArrayList<>();
+ for (Object tokenObject : ((Map<String, Object>) tokens).values()) {
+
+ // Access the inner arrays (disregard the keys) and use their length to decide which type of token we are
+ // dealing with.
+ Map<String, Object> tokenProperties = (Map<String, Object>) tokenObject;
+
+ // skip whitespace token (size < 4)
+ if (tokenProperties.size() >= 4) {
+ String text = tokenProperties.get("1").toString();
+ Type type = computeType(tokenProperties.get("0").toString(), text);
+ Integer startLine = convertToInt(tokenProperties.get("2"));
+ Integer startColumn = ((Double) tokenProperties.get("3")).intValue();
+
+ // all cases except for punctuator type
+ if (tokenProperties.size() == 6) {
+ Integer endLine = convertToInt(tokenProperties.get("4"));
+ Integer endColumn = ((Double) tokenProperties.get("5")).intValue();
+
+
+ if (isTokenWithPunctuator(text, ",", startLine, endLine)) {
+ resultList.addAll(splitTokenWithPunctuator(text, type, startLine, startColumn, endLine, endColumn));
+ } else if (isTokenWithPunctuator(text, ":", startLine, endLine)) {
+ resultList.addAll(splitTokenWithPunctuator(text, type, startLine, startColumn, endLine, endColumn));
+ } else {
+ resultList.add(new Token(type, text, startLine, startColumn, endLine, endColumn));
+ }
+ } else {
+ // is punctuator
+ resultList.add(new Token(type, text, startLine, startColumn, startLine, startColumn));
+ }
+ }
+ }
+
+ return resultList;
+ }
+
+ // Javascript tokenizer is not returning 2 tokens for words ending with a comma (e.g. foo,) and for words starting
+ // with at symbol and endings with colon (e.g. @base:) so we need to split the word into 2 tokens (1 word without
+ // the punctuator and 1 punctuator).
+ // For the sake of simplicity we don't handle words ending with the punctuator on a new line.
+ private static Boolean isTokenWithPunctuator(String text, String punctuator, Integer startLine, Integer endLine) {
+ return text.length() > 1 && text.endsWith(punctuator) && startLine.equals(endLine);
+ }
+
+ private static List<Token> splitTokenWithPunctuator(String text, Type type, Integer startLine, Integer startColumn, Integer endLine, Integer endColumn) {
+ List<Token> tokenList = new ArrayList<>();
+
+ tokenList.add(new Token(type, text.substring(0, text.length() - 1), startLine, startColumn, endLine, endColumn - 1));
+ tokenList.add(new Token(Type.PUNCTUATOR, text.substring(text.length() - 1), startLine, endColumn, endLine, endColumn));
+
+ return tokenList;
+ }
+
+ private static Integer convertToInt(Object value) {
+ if (value instanceof Double) {
+ return ((Double) value).intValue();
+ } else if (value instanceof Integer) {
+ return (Integer) value;
+ } else {
+ throw new IllegalStateException("Failed to convert to number: " + value);
+ }
+ }
+
+ private static Type computeType(String type, String text) {
+ switch (type) {
+ case "at-word":
+ return Type.AT_WORD;
+ case "word":
+ if (",".equals(text)) {
+ return Type.PUNCTUATOR;
+ } else {
+ return Type.WORD;
+ }
+ case "comment":
+ return Type.COMMENT;
+ case "string":
+ return Type.STRING;
+ case "brackets":
+ return Type.BRACKETS;
+ default:
+ return Type.PUNCTUATOR;
+ }
+ }
+}
diff --git a/sonar-css-plugin/src/main/resources/tokenize.js b/sonar-css-plugin/src/main/resources/tokenize.js
new file mode 100644
index 0000000..bbf9b1b
--- /dev/null
+++ b/sonar-css-plugin/src/main/resources/tokenize.js
@@ -0,0 +1,374 @@
+/*
+ * SonarCSS
+ * Copyright (C) 2018-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright 2013 Andrey Sitnik <andrey@sitnik.ru>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+var SINGLE_QUOTE = '\''.charCodeAt(0);
+var DOUBLE_QUOTE = '"'.charCodeAt(0);
+var BACKSLASH = '\\'.charCodeAt(0);
+var SLASH = '/'.charCodeAt(0);
+var NEWLINE = '\n'.charCodeAt(0);
+var SPACE = ' '.charCodeAt(0);
+var FEED = '\f'.charCodeAt(0);
+var TAB = '\t'.charCodeAt(0);
+var CR = '\r'.charCodeAt(0);
+var OPEN_SQUARE = '['.charCodeAt(0);
+var CLOSE_SQUARE = ']'.charCodeAt(0);
+var OPEN_PARENTHESES = '('.charCodeAt(0);
+var CLOSE_PARENTHESES = ')'.charCodeAt(0);
+var OPEN_CURLY = '{'.charCodeAt(0);
+var CLOSE_CURLY = '}'.charCodeAt(0);
+var SEMICOLON = ';'.charCodeAt(0);
+var ASTERISK = '*'.charCodeAt(0);
+var COLON = ':'.charCodeAt(0);
+var AT = '@'.charCodeAt(0);
+
+var RE_AT_END = /[ \n\t\r\f\{\(\)'"\\;/\[\]#]/g;
+var RE_WORD_END = /[ \n\t\r\f\(\)\{\}:;@!'"\\\]\[#]|\/(?=\*)/g;
+var RE_BAD_BRACKET = /.[\\\/\("'\n]/;
+var RE_HEX_ESCAPE = /[a-f0-9]/i;
+
+function tokenize(css) {
+ var input = {css: css, error: function() {}};
+ var processor = tokenizer(input);
+ var tokens = [];
+ while (!processor.endOfFile()) {
+ tokens.push(processor.nextToken());
+ }
+ return tokens;
+}
+
+function tokenizer(input ) {
+ var options = {};
+ var css = input.css.valueOf();
+ var ignore = options.ignoreErrors;
+
+ var code, next, quote, lines, last, content, escape,
+ nextLine, nextOffset, escaped, escapePos, prev, n, currentToken;
+
+ var length = css.length;
+ var offset = -1;
+ var line = 1;
+ var pos = 0;
+ var buffer = [];
+ var returned = [];
+
+ function unclosed(what) {
+ throw input.error('Unclosed ' + what, line, pos - offset);
+ }
+
+ function endOfFile() {
+ return returned.length === 0 && pos >= length;
+ }
+
+ function nextToken() {
+ if ( returned.length ) return returned.pop();
+ if ( pos >= length ) return;
+
+ code = css.charCodeAt(pos);
+ if ( code === NEWLINE || code === FEED ||
+ code === CR && css.charCodeAt(pos + 1) !== NEWLINE ) {
+ offset = pos;
+ line += 1;
+ }
+
+ switch ( code ) {
+ case NEWLINE:
+ case SPACE:
+ case TAB:
+ case CR:
+ case FEED:
+ next = pos;
+ do {
+ next += 1;
+ code = css.charCodeAt(next);
+ if ( code === NEWLINE ) {
+ offset = next;
+ line += 1;
+ }
+ } while ( code === SPACE ||
+ code === NEWLINE ||
+ code === TAB ||
+ code === CR ||
+ code === FEED );
+
+ currentToken = ['space', css.slice(pos, next)];
+ pos = next - 1;
+ break;
+
+ case OPEN_SQUARE:
+ currentToken = ['[', '[', line, pos - offset];
+ break;
+
+ case CLOSE_SQUARE:
+ currentToken = [']', ']', line, pos - offset];
+ break;
+
+ case OPEN_CURLY:
+ currentToken = ['{', '{', line, pos - offset];
+ break;
+
+ case CLOSE_CURLY:
+ currentToken = ['}', '}', line, pos - offset];
+ break;
+
+ case COLON:
+ currentToken = [':', ':', line, pos - offset];
+ break;
+
+ case SEMICOLON:
+ currentToken = [';', ';', line, pos - offset];
+ break;
+
+ case OPEN_PARENTHESES:
+ prev = buffer.length ? buffer.pop()[1] : '';
+ n = css.charCodeAt(pos + 1);
+ if ( prev === 'url' &&
+ n !== SINGLE_QUOTE && n !== DOUBLE_QUOTE &&
+ n !== SPACE && n !== NEWLINE && n !== TAB &&
+ n !== FEED && n !== CR ) {
+ next = pos;
+ do {
+ escaped = false;
+ next = css.indexOf(')', next + 1);
+ if ( next === -1 ) {
+ if ( ignore ) {
+ next = pos;
+ break;
+ } else {
+ unclosed('bracket');
+ }
+ }
+ escapePos = next;
+ while ( css.charCodeAt(escapePos - 1) === BACKSLASH ) {
+ escapePos -= 1;
+ escaped = !escaped;
+ }
+ } while ( escaped );
+
+ currentToken = ['brackets', css.slice(pos, next + 1),
+ line, pos - offset,
+ line, next - offset
+ ];
+
+ pos = next;
+
+ } else {
+ next = css.indexOf(')', pos + 1);
+ content = css.slice(pos, next + 1);
+
+ if ( next === -1 || RE_BAD_BRACKET.test(content) ) {
+ currentToken = ['(', '(', line, pos - offset];
+ } else {
+ currentToken = ['brackets', content,
+ line, pos - offset,
+ line, next - offset
+ ];
+ pos = next;
+ }
+ }
+
+ break;
+
+ case CLOSE_PARENTHESES:
+ currentToken = [')', ')', line, pos - offset];
+ break;
+
+ case SINGLE_QUOTE:
+ case DOUBLE_QUOTE:
+ quote = code === SINGLE_QUOTE ? '\'' : '"';
+ next = pos;
+ do {
+ escaped = false;
+ next = css.indexOf(quote, next + 1);
+ if ( next === -1 ) {
+ if ( ignore ) {
+ next = pos + 1;
+ break;
+ } else {
+ unclosed('string');
+ }
+ }
+ escapePos = next;
+ while ( css.charCodeAt(escapePos - 1) === BACKSLASH ) {
+ escapePos -= 1;
+ escaped = !escaped;
+ }
+ } while ( escaped );
+
+ content = css.slice(pos, next + 1);
+ lines = content.split('\n');
+ last = lines.length - 1;
+
+ if ( last > 0 ) {
+ nextLine = line + last;
+ nextOffset = next - lines[last].length;
+ } else {
+ nextLine = line;
+ nextOffset = offset;
+ }
+
+ currentToken = ['string', css.slice(pos, next + 1),
+ line, pos - offset,
+ nextLine, next - nextOffset
+ ];
+
+ offset = nextOffset;
+ line = nextLine;
+ pos = next;
+ break;
+
+ case AT:
+ RE_AT_END.lastIndex = pos + 1;
+ RE_AT_END.test(css);
+ if ( RE_AT_END.lastIndex === 0 ) {
+ next = css.length - 1;
+ } else {
+ next = RE_AT_END.lastIndex - 2;
+ }
+
+ currentToken = ['at-word', css.slice(pos, next + 1),
+ line, pos - offset,
+ line, next - offset
+ ];
+
+ pos = next;
+ break;
+
+ case BACKSLASH:
+ next = pos;
+ escape = true;
+ while ( css.charCodeAt(next + 1) === BACKSLASH ) {
+ next += 1;
+ escape = !escape;
+ }
+ code = css.charCodeAt(next + 1);
+ if ( escape && (code !== SLASH &&
+ code !== SPACE &&
+ code !== NEWLINE &&
+ code !== TAB &&
+ code !== CR &&
+ code !== FEED ) ) {
+ next += 1;
+ if ( RE_HEX_ESCAPE.test(css.charAt(next)) ) {
+ while ( RE_HEX_ESCAPE.test(css.charAt(next + 1)) ) {
+ next += 1;
+ }
+ if ( css.charCodeAt(next + 1) === SPACE ) {
+ next += 1;
+ }
+ }
+ }
+
+ currentToken = ['word', css.slice(pos, next + 1),
+ line, pos - offset,
+ line, next - offset
+ ];
+
+ pos = next;
+ break;
+
+ default:
+ if ( code === SLASH && css.charCodeAt(pos + 1) === ASTERISK ) {
+ next = css.indexOf('*/', pos + 2) + 1;
+ if ( next === 0 ) {
+ if ( ignore ) {
+ next = css.length;
+ } else {
+ unclosed('comment');
+ }
+ }
+
+ content = css.slice(pos, next + 1);
+ lines = content.split('\n');
+ last = lines.length - 1;
+
+ if ( last > 0 ) {
+ nextLine = line + last;
+ nextOffset = next - lines[last].length;
+ } else {
+ nextLine = line;
+ nextOffset = offset;
+ }
+
+ currentToken = ['comment', content,
+ line, pos - offset,
+ nextLine, next - nextOffset
+ ];
+
+ offset = nextOffset;
+ line = nextLine;
+ pos = next;
+
+ } else {
+ RE_WORD_END.lastIndex = pos + 1;
+ RE_WORD_END.test(css);
+ if ( RE_WORD_END.lastIndex === 0 ) {
+ next = css.length - 1;
+ } else {
+ next = RE_WORD_END.lastIndex - 2;
+ }
+
+ currentToken = ['word', css.slice(pos, next + 1),
+ line, pos - offset,
+ line, next - offset
+ ];
+
+ buffer.push(currentToken);
+
+ pos = next;
+ }
+
+ break;
+ }
+
+ pos++;
+ return currentToken;
+ }
+
+ function back(token) {
+ returned.push(token);
+ }
+
+ return {
+ back:back, nextToken:nextToken, endOfFile:endOfFile
+ };
+}
diff --git a/sonar-css-plugin/src/test/java/org/sonar/css/plugin/TokenizerTest.java b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/TokenizerTest.java
new file mode 100644
index 0000000..b1589c0
--- /dev/null
+++ b/sonar-css-plugin/src/test/java/org/sonar/css/plugin/TokenizerTest.java
@@ -0,0 +1,226 @@
+/*
+ * SonarCSS
+ * Copyright (C) 2018-2018 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.css.plugin;
+
+import java.util.List;
+import java.util.Optional;
+
+import javax.script.ScriptException;
+import org.junit.Test;
+import org.sonar.css.plugin.Token.Type;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class TokenizerTest {
+
+ private final static Tokenizer tokenizer = new Tokenizer();
+
+ @Test
+ public void word() throws ScriptException {
+ assertToken("bar { }", 0, "bar", Type.WORD);
+ assertToken("bar: foo { }", 0, "bar", Type.WORD);
+ assertToken("bar: foo-baz { }", 2, "foo-baz", Type.WORD);
+ assertToken("foo bar { }", 1, "bar", Type.WORD);
+ assertToken("#bar { }", 0, "#bar", Type.WORD);
+ assertToken("foo.bar { }", 0, "foo.bar", Type.WORD);
+ assertToken(".bar { }", 0, ".bar", Type.WORD);
+ assertToken("bar { foo: 42; }", 2, "foo", Type.WORD);
+ assertToken("bar { foo: baz; }", 4, "baz", Type.WORD);
+ assertToken("foo , bar { }", 2, "bar", Type.WORD);
+ }
+
+ @Test
+ public void semi_colon() throws ScriptException {
+ assertToken("bar: foo { }", 1, ":", Type.PUNCTUATOR);
+ assertToken("bar { foo; }", 3, ";", Type.PUNCTUATOR);
+ }
+
+ @Test
+ public void comma() throws ScriptException {
+ assertToken("foo , bar { }", 1, ",", Type.PUNCTUATOR);
+ assertToken("foo, bar { }", 1, ",", Type.PUNCTUATOR);
+ }
+
+ @Test
+ public void number_as_word() throws ScriptException {
+ assertToken("bar { foo: 1.15; }", 4, "1.15", Type.WORD);
+ assertToken("bar { foo: 1; }", 4, "1", Type.WORD);
+ assertToken("bar { foo: 1.15px; }", 4, "1.15px", Type.WORD);
+ assertToken("bar { foo: 1.15%; }", 4, "1.15%", Type.WORD);
+ assertToken("bar { foo: 1px; }", 4, "1px", Type.WORD);
+ assertToken("bar { foo: 1em/150%; }", 4, "1em/150%", Type.WORD);
+ }
+
+ @Test
+ public void brackets() throws ScriptException {
+ assertToken("bar { foo: (1.15); }", 4, "(1.15)", Type.BRACKETS);
+ assertToken("bar { foo: ( 1.15 ); }", 4, "( 1.15 )", Type.BRACKETS);
+ assertToken("bar { foo: (1.15 1 0px); }", 4, "(1.15 1 0px)", Type.BRACKETS);
+ assertToken("bar { foo: (1.15, 1, 0px); }", 4, "(1.15, 1, 0px)", Type.BRACKETS);
+ assertToken("bar { content: string(doctitle); }", 5, "(doctitle)", Type.BRACKETS);
+ assertToken("bar { string-set: booktitle content(); }", 6, "()", Type.BRACKETS);
+ assertToken("bar { a: b(attr(href, url), c) \")\"; }", 7, "(href, url)", Type.BRACKETS);
+ }
+
+ @Test
+ public void strings() throws ScriptException {
+ assertToken("bar { foo: \"\"; }", 4, "\"\"", Type.STRING);
+ assertToken("bar { foo: \"hello, world\"; }", 4, "\"hello, world\"", Type.STRING);
+ }
+
+ @Test
+ public void at_word() throws ScriptException {
+ assertToken("@bar { }", 0, "@bar", Type.AT_WORD);
+ }
+
+ @Test
+ public void comment() throws ScriptException {
+ assertToken("/* foo */", 0, "/* foo */", Type.COMMENT);
+ assertToken("foo { a: /* foo */ 42; }", 4, "/* foo */", Type.COMMENT);
+ assertToken("/* \n"
+ + " this is a comment\n"
+ + " and it is awesome because\n"
+ + " it is multiline!\n"
+ + "*/", 0, "/* \n"
+ + " this is a comment\n"
+ + " and it is awesome because\n"
+ + " it is multiline!\n"
+ + "*/", Type.COMMENT, 1, 1, 5, 2);
+ assertToken("foo { a: /* foo\nbar*/ 42; }", 4, "/* foo\nbar*/", Type.COMMENT, 1, 10, 2, 5);
+ }
+
+ @Test
+ public void hashtag() throws ScriptException {
+ assertToken("bar { color: #333; }", 4, "#333", Type.WORD);
+ assertToken("bar { color: #e535ab; }", 4, "#e535ab", Type.WORD);
+ assertToken("#table-of-contents + ul li { list-style: none; }", 0, "#table-of-contents", Type.WORD);
+ }
+
+ @Test
+ public void scss_variable() throws ScriptException {
+ assertToken("$font-stack: Helvetica;", 0, "$font-stack", Type.WORD);
+ assertToken("$message-color: blue !default;", 3, "!default", Type.WORD);
+
+ List<Token> tokenList = tokenizer.tokenize("p.message-#{$alertClass} { color: red; }");
+ assertThat(tokenList.size()).isEqualTo(11);
+ assertToken(tokenList, 0, "p.message-", Type.WORD);
+ assertToken(tokenList, 1, "#", Type.WORD);
+ assertToken(tokenList, 2, "{", Type.PUNCTUATOR);
+ assertToken(tokenList, 3, "$alertClass", Type.WORD);
+ assertToken(tokenList, 4, "}", Type.PUNCTUATOR);
+ assertToken(tokenList, 5, "{", Type.PUNCTUATOR);
+ assertToken(tokenList, 6, "color", Type.WORD);
+ assertToken(tokenList, 7, ":", Type.PUNCTUATOR);
+ assertToken(tokenList, 8, "red", Type.WORD);
+ assertToken(tokenList, 9, ";", Type.PUNCTUATOR);
+ assertToken(tokenList, 10, "}", Type.PUNCTUATOR);
+
+ }
+
+ @Test
+ public void scss_import() throws ScriptException {
+ List<Token> tokenList = tokenizer.tokenize("@import 'base';");
+
+ assertThat(tokenList.size()).isEqualTo(3);
+ assertToken(tokenList, 0, "@import", Type.AT_WORD);
+ assertToken(tokenList, 1, "'base'", Type.STRING);
+ assertToken(tokenList, 2, ";", Type.PUNCTUATOR);
+ }
+
+ @Test
+ public void scss_role() throws ScriptException {
+ List<Token> tokenList = tokenizer.tokenize("article[role=\"main\"] { width: 1px; }");
+
+ assertThat(tokenList.size()).isEqualTo(11);
+ assertToken(tokenList, 0, "article", Type.WORD);
+ assertToken(tokenList, 1, "[", Type.PUNCTUATOR);
+ assertToken(tokenList, 2, "role=", Type.WORD);
+ assertToken(tokenList, 3, "\"main\"", Type.STRING);
+ assertToken(tokenList, 4, "]", Type.PUNCTUATOR);
+ }
+
+ @Test
+ public void scss_operators() throws ScriptException {
+ assertToken("foo { width: 300px + 960px; }", 5, "+", Type.WORD);
+ assertToken("foo { width: 300px - 960px; }", 5, "-", Type.WORD);
+ assertToken("foo { width: 300px * 960px; }", 5, "*", Type.WORD);
+ assertToken("foo { width: 300px / 960px; }", 5, "/", Type.WORD);
+ }
+
+ @Test
+ public void scss_parent_selector() throws ScriptException {
+ assertToken("a { &:hover { color: red; } }", 2, "&", Type.WORD);
+ assertToken("p { body.no-touch & { display: none; } }", 3, "&", Type.WORD);
+ }
+
+ @Test
+ public void scss_control_directives() throws ScriptException {
+ assertToken("@if ($debug) { }", 0, "@if", Type.AT_WORD);
+ assertToken("@each $name in 'save' 'cancel' { }", 0, "@each", Type.AT_WORD);
+ }
+
+ @Test
+ public void less_variable() throws ScriptException {
+ assertToken("@nice-blue: #5B83AD;", 0, "@nice-blue", Type.AT_WORD);
+ assertToken("foo { color: @@color; }", 4, "@@color", Type.AT_WORD);
+ }
+
+ @Test
+ public void less_operators() throws ScriptException {
+ assertToken("@base: 2cm * 3mm;", 3, "*", Type.WORD);
+ }
+
+ @Test
+ public void less_escaping() throws ScriptException {
+ assertToken("@min768: ~\"(min-width: 768px)\";", 2, "~", Type.WORD);
+ }
+
+ @Test
+ public void less_comment() throws ScriptException {
+ // FIXME: Less allows // comment which are not supported by our current tokenizer
+ //assertToken("// Get in line!", 0, "Get in line!", Type.COMMENT);
+
+ assertToken("/* One heck of a block\n * style comment! */", 0, "/* One heck of a block\n * style comment! */", Type.COMMENT);
+ }
+
+ private static void assertToken(String input, int index, String value, Token.Type type) throws ScriptException {
+ List<Token> tokenList = tokenizer.tokenize(input);
+ assertToken(tokenList, index, value, type);
+ }
+
+ private static void assertToken(String input, int index, String value, Token.Type type, int line, int column, int
+ endLine, int endColumn) throws ScriptException {
+ List<Token> tokenList = tokenizer.tokenize(input);
+ assertToken(tokenList, index, value, type, line, column, endLine, endColumn);
+ }
+
+ private static void assertToken(List<Token> tokenList, int index, String value, Token.Type type) {
+ assertThat(tokenList.get(index).type).isEqualTo(type);
+ assertThat(tokenList.get(index).text).isEqualTo(value);
+ }
+
+ private static void assertToken(List<Token> tokenList, int index, String value, Token.Type type, int line, int column, int endLine, int endColumn) {
+ assertToken(tokenList, index, value, type);
+ assertThat(tokenList.get(index).startLine).isEqualTo(line);
+ assertThat(tokenList.get(index).startColumn).isEqualTo(column);
+ assertThat(tokenList.get(index).endLine).isEqualTo(endLine);
+ assertThat(tokenList.get(index).endColumn).isEqualTo(endColumn);
+ }
+}