Wrote my own CSS parser + unit-tests

pull/98/head
Ian Prest 9 years ago
parent 7d51134458
commit f9fcd4e8c4

@ -13,13 +13,15 @@ all: js_files css_files bower_copy
.PHONY: js_files css_files bower_copy
# Rules to minify our .js files
js_files: js/jsonl.min.js
js_files: js/jsonl.min.js js/cssparser.js
js/%.min.js: js/%.js
$(call mkdir,$(dir $@))
uglifyjs "$^" > "$@"
js/%.js: %.grammar.js
$(call mkdir,$(dir $@))
node "$^" > "$@"
js/%.js: %.y
jison "$^" -o "$@"
.PRECIOUS: js/%.js
@ -74,7 +76,6 @@ $(call BOWER,bower_components/marked/marked.min.js)
$(call BOWER,bower_components/FileSaver/FileSaver.min.js)
$(call BOWER,bower_components/doT/doT.min.js)
$(call BOWER,bower_components/URLON/src/urlon.js)
$(call BOWER,bower_components/cssparser/lib/cssparser.js)
# Rules to generate a webfont from our source .svg files
@ -106,7 +107,13 @@ CUSTOM_FONT = $(eval $(call _CUSTOM_FONT,$(1).ttf,$(2)))$(eval $(call _CUSTOM_FO
$(call CUSTOM_FONT,kbd-custom,$(kbd-custom-glyphs))
test:
test: e2e-test unit-test
protractor tests/conf.js
unit-test:
jasmine
e2e-test:
protractor tests/conf.js
install:

@ -41,7 +41,6 @@
"ng-file-upload": "5.0.9",
"angular-ui-bootstrap": "0.12.0",
"fontawesome": "4.3.0",
"hint.css": "1.3.5",
"cssparser": "a4adfd2f5f62bb9c19bcfead974ac0f253a0fb82"
"hint.css": "1.3.5"
}
}

@ -0,0 +1,189 @@
/* Parser for CSS3, as defined by http://www.w3.org/TR/css-syntax-3 */
%lex
/* Lexer Macros */
comment \/\*([^\*](\*[^\/])?)*\*\/
newline \r\n|\n|\r|\f
whitespace [ \t\n\r\f]
hex [0-9a-fA-F]
escape \\([^0-9a-fA-F\n\r\f]|({hex}{1,6}{whitespace}?))
ws {whitespace}*
nonascii [\200-\377]
token_start ({escape}|[a-zA-Z_]|{nonascii})
token_char ({escape}|[a-zA-Z_0-9-]|{nonascii})
identifier \-?{token_start}{token_char}*
string \"({escape}|\\{newline}|[^"\\\r\n\f])*\"|\'({escape}|\\{newline}|[^'\\\r\n\f])*\'
urlu ({escape}|[^'"\(\)\\ \t\n\r\f\000-\010\016-\037\177])+
number [+-]?(\d+\.\d+|\.\d+|\d+)([eE][+-]?\d+)?
range [uU]'+'({hex}{1,6}('-'{hex}{1,6})?|'?'{6}|{hex}'?'{5}|{hex}{2}'?'{4}|{hex}{3}'?'{3}|{hex}{4}'?'{2}|{hex}{5}'?'{1})
%%
/* Lexical Tokens */
{comment} return 'WHITESPACE';
{whitespace}+ return 'WHITESPACE';
{identifier}'(' return 'FUNCTION';
{identifier} return 'IDENTIFIER';
'#'{token_char}+ return 'HASH';
'@'{identifier} return 'AT_KEYWORD';
'!'{ws}'important'{ws} return 'IMPORTANT';
{string} return 'STRING';
'url('{w}({string}|{urlu})?{w}')' return 'URL';
{number}{identifier} return 'DIMENSION';
{number}'%' return 'PERCENTAGE';
{number} return 'NUMBER';
{range} return 'UNICODE_RANGE';
[~|^$\*]?'=' return 'MATCH_TOKEN';
'||' return 'COLUMN_TOKEN';
'<!--' return 'CDO';
'-->' return 'CDC';
<<EOF>> return 'EOF';
'\'' return 'SQUOTE';
. return yytext;
/lex
/* language grammar */
%start stylesheet
%ebnf
%%
stylesheet
: WS EOF { return $$ = {}; }
| stylesheet_content WS EOF { return $$ = ($stylesheet_content && $stylesheet_content.length) ? { rules:$stylesheet_content } : {}; }
;
stylesheet_content
: stylesheet_item { $$ = []; if($stylesheet_item !== null) $$.push($stylesheet_item); }
| stylesheet_content stylesheet_item { $$ = $stylesheet_content; if($stylesheet_item !== null) $$.push($stylesheet_item); }
;
stylesheet_item
: WS CDO { $$ = null; }
| WS CDC { $$ = null; }
| qualified_rule { $$ = $qualified_rule; }
| at_rule { $$ = $at_rule; }
;
at_rule
: WS AT_KEYWORD ';' { $$ = { name: $AT_KEYWORD }}
| WS AT_KEYWORD at_rule_selector ';' { $$ = { name: $AT_KEYWORD, selector: $at_rule_selector.trim() }; if(!$$.selector) delete $$.selector; }
| WS AT_KEYWORD at_rule_selector '{' '}' { $$ = { name: $AT_KEYWORD, selector: $at_rule_selector.trim() }; if(!$$.selector) delete $$.selector; }
| WS AT_KEYWORD at_rule_selector '{' at_rule_content '}' { $$ = { name: $AT_KEYWORD, selector: $at_rule_selector.trim(), content: $at_rule_content.trim() }; if(!$$.selector) delete $$.selector; if(!$$.content) delete $$.content; }
;
at_rule_selector
: any_token { $$ = $any_token; }
| at_rule_selector any_token { $$ = $at_rule_selector + $any_token; }
;
at_rule_content
: any_token { $$ = $any_token; }
| at_rule_content ';' { $$ = $at_rule_content + ';'; }
| at_rule_content any_token { $$ = $at_rule_content + $any_token; }
;
qualified_rule
: qualified_rule_prelude '{' WS '}' { $$ = { selector: $qualified_rule_prelude } }
| qualified_rule_prelude '{' WS declaration_list '}' { $$ = { selector: $qualified_rule_prelude, decls: $declaration_list } }
| qualified_rule_prelude '{' WS declaration_list ';' WS '}' { $$ = { selector: $qualified_rule_prelude, decls: $declaration_list } }
;
declaration_list
: declaration { $$ = [$declaration]; }
| declaration_list ';' WS declaration { $$ = $declaration_list; $$.push($declaration); }
;
declaration
: IDENTIFIER WS ':' declaration_value { $$ = [$IDENTIFIER,$declaration_value.trim()]; }
;
// Anything other than a semi-colon or a closing brace
declaration_value
: any_token { $$ = $any_token; }
| declaration_value any_token { $$ = $declaration_value + $any_token; }
;
// Does not include semi-colon, or open/close braces
any_token
: (WHITESPACE | FUNCTION | IDENTIFIER | HASH | AT_KEYWORD |
IMPORTANT | STRING | URL | DIMENSION | PERCENTAGE | NUMBER |
UNICODE_RANGE | MATCH_TOKEN | COLUMN_TOKEN | CDO | CDC | EOF | SQUOTE |
'!'|'"'|'#'|'$'|'%'|'&'|'('|')'|'*'|'+'|','|'-'|'.'|'/'|
':'|'<'|'='|'>'|'?'|'@'|'['|'\'|']'|'^'|'_'|'`'|'|'|'~') { $$ = $1; }
;
qualified_rule_prelude
: WS selector WS { $$ = [$selector]; }
| qualified_rule_prelude ',' WS selector WS { $$ = $qualified_rule_prelude; $$.push($selector); }
;
selector
: single_selector { $$ = $single_selector; }
| selector WHITESPACE single_selector { $$ = $selector + ' ' + $single_selector; }
| selector WS '>' WS single_selector { $$ = $selector + '>' + $single_selector; }
| selector WS '+' WS single_selector { $$ = $selector + '+' + $single_selector; }
| selector WS '~' WS single_selector { $$ = $selector + '~' + $single_selector; }
| ':' IDENTIFIER { $$ = ':' + $IDENTIFIER; }
| ':' ':' IDENTIFIER { $$ = '::' + $IDENTIFIER; }
| selector ':' IDENTIFIER { $$ = $selector + ':' + $IDENTIFIER; }
| selector ':' ':' IDENTIFIER { $$ = $selector + '::' + $IDENTIFIER; }
| ':' FUNCTION WS selector_function_params { $$ = ':' + $FUNCTION + $selector_function_params + ')'; }
| selector ':' FUNCTION WS selector_function_params { $$ = $selector + ':' + $FUNCTION + $selector_function_params + ')'; }
;
selector_function_params
: IDENTIFIER WS ')' { $$ = $IDENTIFIER; }
| NUMBER WS ')' { $$ = $NUMBER; }
| DIMENSION WS ')' { $$ = $DIMENSION; } // 2n
| IDENTIFIER WS NUMBER WS ')' { $$ = $IDENTIFIER + $NUMBER; } // n+1
| IDENTIFIER WS '+' WS NUMBER WS ')' { $$ = $IDENTIFIER + '+' + $NUMBER; } // n+1
| IDENTIFIER WS '-' WS NUMBER WS ')' { $$ = $IDENTIFIER + '-' + $NUMBER; } // n+1
| DIMENSION WS NUMBER WS ')' { $$ = $DIMENSION + $NUMBER; } // 2n+1
| DIMENSION WS '+' WS NUMBER WS ')' { $$ = $DIMENSION + '+' + $NUMBER; } // 2n+1
| DIMENSION WS '-' WS NUMBER WS ')' { $$ = $DIMENSION + '-' + $NUMBER; } // 2n+1
;
single_selector
: '*' { $$ = $1; }
| '.' IDENTIFIER { $$ = '.' + $IDENTIFIER; }
| HASH { $$ = $HASH; }
| IDENTIFIER { $$ = $IDENTIFIER; }
| single_selector '.' IDENTIFIER { $$ = $single_selector + '.' + $IDENTIFIER; }
| single_selector HASH { $$ = $single_selector + $HASH; }
| single_selector '[' WS attribute_selector ']' { $$ = $single_selector + '[' + $attribute_selector + ']'; }
;
attribute_selector
: attribute_name WS { $$ = $attribute_name; }
| attribute_name WS MATCH_TOKEN WS attribute_value { $$ = $attribute_name + $MATCH_TOKEN + $attribute_value; }
;
attribute_name
: IDENTIFIER { $$ = $IDENTIFIER; }
;
attribute_value
: unquoted_attribute_value { $$ = $unquoted_attribute_value; }
| STRING { $$ = $STRING; }
;
unquoted_attribute_value
: unquoted_attribute_value_char { $$ = $unquoted_attribute_value_char; }
| IDENTIFIER { $$ = $IDENTIFIER; }
| unquoted_attribute_value unquoted_attribute_value_char { $$ = $unquoted_attribute_value + $unquoted_attribute_value_char; }
| unquoted_attribute_value IDENTIFIER { $$ = $unquoted_attribute_value + $IDENTIFIER; }
;
unquoted_attribute_value_char
: ('.'|':'|'-'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'|NUMBER) { $$ = $1; } // technically shouldn't allow full number syntax
;
WS
: /*empty*/ { $$ = ''; }
| WHITESPACE { $$ = ''; }
| ws WHITESPACE { $$ = ''; }
;

@ -13,7 +13,7 @@
"build_system": "Automatic",
//SublimeOnSaveBuild package (optional)
"filename_filter": "\\.(css|js|sass|less|scss)$",
"filename_filter": "\\.(css|js|sass|less|scss|y)$",
"build_on_save": 1
},
"build_systems":

@ -0,0 +1,118 @@
css = require('../js/cssparser.js');
describe('css parser', function() {
it('should parse an empty file', function() {
expect(css.parse('')).toEqual({});
});
it('should ignore HTML comments', function() {
expect(css.parse('<!--')).toEqual({});
expect(css.parse('-->')).toEqual({});
expect(css.parse('<!-- -->')).toEqual({});
expect(css.parse('--> <!--')).toEqual({});
});
it('should parse simple selectors', function() {
expect(css.parse('foo{}')).toEqual({ rules: [ {selector: ["foo"] } ] });
expect(css.parse('foo {}')).toEqual({ rules: [ {selector: ["foo"] } ] });
expect(css.parse('* {}')).toEqual({ rules: [ {selector: ["*"] } ] });
expect(css.parse('.foo {}')).toEqual({ rules: [ {selector: [".foo"] } ] });
expect(css.parse('foo.bar {}')).toEqual({ rules: [ {selector: ["foo.bar"] } ] });
expect(css.parse('#foo {}')).toEqual({ rules: [ {selector: ["#foo"] } ] });
expect(css.parse('foo#bar {}')).toEqual({ rules: [ {selector: ["foo#bar"] } ] });
expect(css.parse(' foo{}')).toEqual({ rules: [ {selector: ["foo"] } ] });
expect(css.parse(' foo {} ')).toEqual({ rules: [ {selector: ["foo"] } ] });
expect(css.parse('foo { }')).toEqual({ rules: [ {selector: ["foo"] } ] });
});
it('should parse combinators', function() {
expect(css.parse('foo bar {}')).toEqual({ rules: [ {selector: ["foo bar"] } ] });
expect(css.parse('foo\nbar {}')).toEqual({ rules: [ {selector: ["foo bar"] } ] });
expect(css.parse('foo > bar {}')).toEqual({ rules: [ {selector: ["foo>bar"] } ] });
expect(css.parse('foo + bar {}')).toEqual({ rules: [ {selector: ["foo+bar"] } ] });
expect(css.parse('foo ~ bar {}')).toEqual({ rules: [ {selector: ["foo~bar"] } ] });
});
it('should parse attribute selectors', function() {
expect(css.parse('foo[attr] {}')).toEqual({ rules: [ {selector: ["foo[attr]"] } ] });
expect(css.parse('foo[attr=val] {}')).toEqual({ rules: [ {selector: ["foo[attr=val]"] } ] });
expect(css.parse('foo[attr~=val] {}')).toEqual({ rules: [ {selector: ["foo[attr~=val]"] } ] });
expect(css.parse('foo[attr|=val] {}')).toEqual({ rules: [ {selector: ["foo[attr|=val]"] } ] });
expect(css.parse('foo[attr^=val] {}')).toEqual({ rules: [ {selector: ["foo[attr^=val]"] } ] });
expect(css.parse('foo[attr$=val] {}')).toEqual({ rules: [ {selector: ["foo[attr$=val]"] } ] });
expect(css.parse('foo[attr*=val] {}')).toEqual({ rules: [ {selector: ["foo[attr*=val]"] } ] });
expect(css.parse('foo[attr="val"] {}')).toEqual({ rules: [ {selector: ["foo[attr=\"val\"]"] } ] });
expect(css.parse('foo[attr~="val"] {}')).toEqual({ rules: [ {selector: ["foo[attr~=\"val\"]"] } ] });
expect(css.parse('foo[attr|="val"] {}')).toEqual({ rules: [ {selector: ["foo[attr|=\"val\"]"] } ] });
expect(css.parse('foo[attr^="val"] {}')).toEqual({ rules: [ {selector: ["foo[attr^=\"val\"]"] } ] });
expect(css.parse('foo[attr$="val"] {}')).toEqual({ rules: [ {selector: ["foo[attr$=\"val\"]"] } ] });
expect(css.parse('foo[attr*="val"] {}')).toEqual({ rules: [ {selector: ["foo[attr*=\"val\"]"] } ] });
expect(css.parse('foo[attr=\'val\'] {}')).toEqual({ rules: [ {selector: ["foo[attr='val']"] } ] });
expect(css.parse('foo[attr~=\'val\'] {}')).toEqual({ rules: [ {selector: ["foo[attr~='val']"] } ] });
expect(css.parse('foo[attr|=\'val\'] {}')).toEqual({ rules: [ {selector: ["foo[attr|='val']"] } ] });
expect(css.parse('foo[attr^=\'val\'] {}')).toEqual({ rules: [ {selector: ["foo[attr^='val']"] } ] });
expect(css.parse('foo[attr$=\'val\'] {}')).toEqual({ rules: [ {selector: ["foo[attr$='val']"] } ] });
expect(css.parse('foo[attr*=\'val\'] {}')).toEqual({ rules: [ {selector: ["foo[attr*='val']"] } ] });
expect(css.parse('foo[attr=5] {}')).toEqual({ rules: [ {selector: ["foo[attr=5]"] } ] });
expect(css.parse('foo[attr=--5] {}')).toEqual({ rules: [ {selector: ["foo[attr=--5]"] } ] });
expect(css.parse('foo[attr=4:5] {}')).toEqual({ rules: [ {selector: ["foo[attr=4:5]"] } ] });
expect(css.parse('foo[attr=x.y.z] {}')).toEqual({ rules: [ {selector: ["foo[attr=x.y.z]"] } ] });
});
it('should parse pseudo-classes', function() {
expect(css.parse(':after {}')).toEqual({ rules: [ {selector: [":after"] } ] });
expect(css.parse('foo:after {}')).toEqual({ rules: [ {selector: ["foo:after"] } ] });
expect(css.parse('foo:after:disabled {}')).toEqual({ rules: [ {selector: ["foo:after:disabled"] } ] });
expect(css.parse(':lang(fr-be) {}')).toEqual({ rules: [ {selector: [":lang(fr-be)"] } ] });
expect(css.parse('foo:lang(de) {}')).toEqual({ rules: [ {selector: ["foo:lang(de)"] } ] });
expect(css.parse('foo:nth-child(2n+1) {}')).toEqual({ rules: [ {selector: ["foo:nth-child(2n+1)"] } ] });
expect(css.parse('foo:nth-child(2n + 1) {}')).toEqual({ rules: [ {selector: ["foo:nth-child(2n+1)"] } ] });
expect(css.parse('foo:nth-child(2n) {}')).toEqual({ rules: [ {selector: ["foo:nth-child(2n)"] } ] });
expect(css.parse('foo:nth-child(n+1) {}')).toEqual({ rules: [ {selector: ["foo:nth-child(n+1)"] } ] });
expect(css.parse('foo:nth-child(n+-1) {}')).toEqual({ rules: [ {selector: ["foo:nth-child(n+-1)"] } ] });
expect(css.parse('foo:nth-child(+3n - 2) {}')).toEqual({ rules: [ {selector: ["foo:nth-child(+3n-2)"] } ] });
});
it('should parse pseudo-elements', function() {
expect(css.parse('::first-line {}')).toEqual({ rules: [ {selector: ["::first-line"] } ] });
expect(css.parse('div::first-line {}')).toEqual({ rules: [ {selector: ["div::first-line"] } ] });
});
it('should parse selector groups', function() {
expect(css.parse('foo, bar {}')).toEqual({ rules: [ {selector: ["foo","bar"] } ] });
expect(css.parse('foo ,bar {}')).toEqual({ rules: [ {selector: ["foo","bar"] } ] });
expect(css.parse('foo,bar {}')).toEqual({ rules: [ {selector: ["foo","bar"] } ] });
expect(css.parse('foo:after,\nbar {}')).toEqual({ rules: [ {selector: ["foo:after","bar"] } ] });
});
it('should parse multiple rules', function() {
expect(css.parse('foo{} bar{}')).toEqual({ rules: [ {selector: ["foo"] }, { selector:["bar"] } ] });
});
it('should parse simple declarations', function() {
expect(css.parse('foo{a:b}')).toEqual({ rules: [ {selector: ["foo"], decls: [['a','b']] } ] });
expect(css.parse('foo{a:b;}')).toEqual({ rules: [ {selector: ["foo"], decls: [['a','b']] } ] });
expect(css.parse('foo{a:b;c:d}')).toEqual({ rules: [ {selector: ["foo"], decls: [['a','b'],['c','d']] } ] });
expect(css.parse('foo{a:b;c:d;}')).toEqual({ rules: [ {selector: ["foo"], decls: [['a','b'],['c','d']] } ] });
expect(css.parse('foo{a: b; c:d;}')).toEqual({ rules: [ {selector: ["foo"], decls: [['a','b'],['c','d']] } ] });
expect(css.parse('foo{ a: b; c : d; }')).toEqual({ rules: [ {selector: ["foo"], decls: [['a','b'],['c','d']] } ] });
});
it('should parse complex declarations', function() {
// Basically, anything other than a semicolon is fair game!
expect(css.parse('foo{a:b(x+a);c:d}')).toEqual({ rules: [ {selector: ["foo"], decls: [['a','b(x+a)'],['c','d']] } ] });
expect(css.parse('foo{a:b(x+a\n) ;c:d;}')).toEqual({ rules: [ {selector: ["foo"], decls: [['a','b(x+a\n)'],['c','d']] } ] });
});
it('should parse at-rules', function() {
expect(css.parse('@foo;')).toEqual({ rules: [ {name: '@foo'} ] });
expect(css.parse('@foo ;')).toEqual({ rules: [ {name: '@foo'} ] });
expect(css.parse('@foo arbitrary-stuff here();')).toEqual({ rules: [ {name: '@foo', selector: "arbitrary-stuff here()"} ] });
expect(css.parse('@foo {}')).toEqual({ rules: [ {name: '@foo'} ] });
expect(css.parse('@foo { arbitrary-stuff; here(); }')).toEqual({ rules: [ {name: '@foo', content: "arbitrary-stuff; here();"} ] });
});
});

@ -20,7 +20,7 @@ var customMatchers = {
describe('keyboard serialization', function() {
beforeEach(function() {
this.addMatchers(customMatchers);
jasmine.addMatchers(customMatchers);
});
it('should handle empty keyboard', function() {

@ -0,0 +1,9 @@
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.js"
],
"helpers": [
"helpers/**/*.js"
]
}
Loading…
Cancel
Save