CSS 2.1 在解析的錯誤處理方面,有一個乍看之有點道理的所謂的核心語法[1]:
# 使用者代理會根據解析錯誤的處理規則,忽略樣式表中可以用下面文法但是不
# 能用附錄 G 的文法解析的部份。
# CSS 與未來的所有擴展不使用 "unused" 產生式,它的作用僅是幫助錯誤處
# 理。(參見 4.2 解析錯誤的處理規則。)
# 在有些情形中,使用者代理必須忽略不合法樣式表的一部分。本規範定義忽略
# 的意義如下:使用者代理解析不合法的部份(為了找到不合法部份的起點和終
# 點),但是當作這部份不存在來處理。
這些話可以用來解釋規範裡某些比較難懂的例子,例如:
# 以下全部相等:
# p { color:green }
# ...
# p { color:green; color{;color:maroon} }
因為按照
# block : '{' S* [ any | block | ATKEYWORD S* | ';' S* ]* '}' S*;
# ...
# declaration: property S* ':' S* value;
# ...
# value : [ any | block | ATKEYWORD S* ]+;
"{;color:maroon}" 可以被 tokenize 成 '{' ';' IDENT ':' IDENT '}',解析
成 '{' ';' any any any '}' → block → value,所以會被一起丟掉。
但是這個核心語法其實對規範*自己*的某些例子就不適用了,例如:
p @here {color: red}
因為 ATKEYWORD(@here)不能解析 any,所以 "p @here" 也不能解析成一個
selector。也就是,這個核心語法不能解釋這個例子,事實上,這個核心語法不能
解釋「敘述格式異常」裡的所有例子。
這個乍看有用其實不完全的核心語法一直受到批評,在 www-style 可以看到從
2009 直到 2011 年 CSS 2.1 變成 REC 都有看到對這個地方不滿的回饋。
我最近發現這個核心語法其實可以擴展成一個普遍文法[2],所謂的普遍語法(我
也沒修過編譯器的課),是指任何輸入都可以解析的語法。我把這個文法寫到 CSS
2.1 翻譯的討論頁面[3]了,也留了一些說明和其他參考資料。
附上實作這個語法的 bison+flex 檔案。在 Mac OS X 上用
flex scan3.l
bison -v -d css.y
clang css.tab.c lex.yy.c
cat EXAMPLE.css | ./a.out
就可以跑了。舉個例子,上面的例子,用這個解析器就會給出:
[[
declaration:
color : green
declaration:
color {
; color : maroon}
ruleset:
p {
color : green;
color {
; color : maroon}
}
]]
也就是 "color: maroon" 不是一個 declaration。其他例子 CSS 2.1 的測試資料
[4]裡還有很多奇奇怪怪的,對這方面想多了解的可以找來玩。
(我用來搞定 bison 花的時間比寫下這個文法還來的長,所以假如執行這個有問
題的歡迎私信問我。)
我在討論頁留了一些還沒有做得事的列表:
* 完整測試這個語法的瀏覽器兼容狀況(特別是 BAD_URI、CDO、CDC 的部份)
* 寫出 @media 的 block 內容語法(其實就是 at-rule、ruleset 前面都不能有
'}' 的 stylesheet。)也要測試一下有沒有在這個地方不省略 CDO、CDC 的瀏覽器。
* 用 EBNF 的 '-' 號讓這個文法更好讀。
* 了解這是 L-?
* 了解各 CSS 解析引擎用(例如:SASS)的合不合這個文法,還是拋錯。
有興趣的可以試試看再來討論。
[1] http://www.w3.org/html/ig/zh/wiki/CSS2#core-grammar
[2] http://en.wikipedia.org/wiki/Context-free_grammar#Universality
[3] http://www.w3.org/html/ig/zh/wiki/Talk:CSS2#core-grammar
[4]
http://test.csswg.org/suites/css2.1/nightly-unstable/xhtml1/chapter-4.xht#s4.1.6
%error-verbose
%{
#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
extern int yylex(void);
extern int yylineno;
static void yyerror(char *s) {fprintf(stderr, "%d: %s\n", yylineno, s);}
static char* cat(char *s,...)
{
va_list ap;
char *t = NULL;
t = (char*) malloc(1);
*t = '\0';
size_t len = 0;
assert(s);
va_start(ap, s);
while (s) {
len += strlen(s);
t = (char*) realloc(t, len + 1);
if (!t) {fprintf(stderr, "Out of memory."); exit(1);}
strcat(t, s);
s = va_arg(ap, char *);
}
va_end(ap);
return t;
}
%}
%union {
char *s;
}
/* All terminals return the matching string from the source */
%token <s> CDO CDC ATKEYWORD DELIM IDENT NUMBER PERCENTAGE DIMENSION
STRING URI HASH UNICODE_RANGE FUNCTION S DASHMATCH INCLUDES BAD_STRING
BAD_COMMENT BAD_URI EOF ';' ')' ']' '}' COMMENT
%type <s> statement ruleset at_rule block declaration_chain selector
selector_start atkeyword_middleopt selector_middleopt atkeyword_middle
selector_middle declaration any s_seqopt declaration_opt declaration_middle
selector_opt bracket_middleopt pare_middleopt brace_middleopt bracket_middle
pare_middle brace_middle no_close
%%
stylesheet
: s_seqopt statement_ignore_seq selector EOF { printf("remaining items: \n");
printf("%s\n", $3); YYACCEPT;}
| s_seqopt statement_ignore_seq EOF {YYACCEPT;}
;
statement_ignore_seq
: statement_ignore_seq statement
| statement_ignore_seq ignore s_seqopt
| /* empty */
;
ignore
: CDO
| CDC
;
statement
: ruleset { printf("ruleset: \n"); printf("%s\n", $1); }
| at_rule { printf("at_rule: \n"); printf("%s\n", $1); }
;
at_rule
: ATKEYWORD s_seqopt atkeyword_middleopt block {$$ = cat($1, $2, $3,
$4, NULL);}
| ATKEYWORD s_seqopt atkeyword_middleopt ';' s_seqopt {$$ = cat($1, $2, $3,
strdup(";\n"), NULL);}
| ATKEYWORD s_seqopt atkeyword_middleopt EOF {$$ = cat($1, $2, $3, $4, 0);}
;
ruleset
: selector_opt '{' s_seqopt declaration_opt declaration_chain '}' s_seqopt
{$$ = cat($1, strdup("{\n "), $4, $5, strdup("}\n"), NULL);}
| selector_opt '{' s_seqopt declaration_opt declaration_chain EOF
{$$ = cat($1, strdup("{\n "), $4, $5, strdup("}\n"), NULL);}
;
declaration_chain
: declaration_chain ';' s_seqopt declaration_opt
{$$ = cat($1, strdup(";\n "), $4, NULL);}
| /* empty */
{$$ = strdup("");}
;
selector_opt
: selector
| /* empty */ {$$ = strdup("");}
;
selector
: selector_start selector_middleopt {$$ = cat($1, strdup(" "), $2, NULL);}
;
selector_start
: any
| ';' s_seqopt
| ']' s_seqopt
| ')' s_seqopt
| '}' s_seqopt
;
atkeyword_middleopt
: atkeyword_middleopt atkeyword_middle {$$ = cat($1, strdup(" "), $2, 0);}
| /* empty */ {$$ = strdup("");}
;
selector_middleopt
: selector_middleopt selector_middle {$$ = cat($1, strdup(" "), $2, 0);}
| /* empty */ {$$ = strdup("");}
;
atkeyword_middle
: any {$$ = cat($1, 0);}
| ']' s_seqopt {$$ = cat($1, 0);}
| ')' s_seqopt {$$ = cat($1, 0);}
| '}' s_seqopt {$$ = cat($1, 0);}
| ATKEYWORD s_seqopt {$$ = cat($1, 0);}
| CDO s_seqopt {$$ = cat($1, 0);}
| CDC s_seqopt {$$ = cat($1, 0);}
;
selector_middle
: selector_start
| ATKEYWORD s_seqopt
| CDO s_seqopt
| CDC s_seqopt
;
declaration_opt
: declaration { printf("declaration: \n"); printf("%s\n\n", $$);}
| /* empty */ {$$ = strdup("");}
;
declaration
: declaration declaration_middle {$$ = cat($1, strdup(" "), $2, 0);}
| declaration_middle
;
declaration_middle
: no_close
| ']' s_seqopt
| ')' s_seqopt
;
any
: IDENT s_seqopt {$$ = cat($1, $2, 0);}
| NUMBER s_seqopt {$$ = cat($1, $2, 0);}
| PERCENTAGE s_seqopt {$$ = cat($1, $2, 0);}
| DIMENSION s_seqopt {$$ = cat($1, $2, 0);}
| STRING s_seqopt {$$ = cat($1, $2, 0);}
| BAD_STRING EOF {$$ = cat($1, $1[0] == '"' ? strdup("\"") : strdup("'"), 0);}
| DELIM s_seqopt {if (!strcmp($1, strdup("\\"))) $$ = strdup("\\\n"); else $$
= cat($1, $2, 0);}
| URI s_seqopt {$$ = cat($1, $2, 0);}
| HASH s_seqopt {$$ = cat($1, $2, 0);}
| UNICODE_RANGE s_seqopt {$$ = cat($1, $2, 0);}
| INCLUDES s_seqopt {$$ = cat($1, $2, 0);}
| DASHMATCH s_seqopt {$$ = cat($1, $2, 0);}
| ':' s_seqopt {$$ = cat(strdup(":"), $2, 0);}
| BAD_STRING S s_seqopt {$$ = cat($1, strdup("\n"), 0);}
| BAD_COMMENT EOF {$$ = strdup("");}
| BAD_URI s_seqopt pare_middleopt ')' s_seqopt {$$ = cat($1, $3, strdup(")"),
$5, 0);}
| BAD_URI s_seqopt pare_middleopt EOF {$$ = cat($1, $3, ")", 0);}
| FUNCTION s_seqopt pare_middleopt ')' s_seqopt {$$ = cat($1, $3, strdup(")"),
$5, 0);}
| FUNCTION s_seqopt pare_middleopt EOF {$$ = cat($1, $3, ")", 0);}
| '(' s_seqopt pare_middleopt ')' s_seqopt {$$ = cat(strdup("("),
$3, strdup(")"), $5, 0);}
| '(' s_seqopt pare_middleopt EOF {$$ = cat(strdup("("), $3, strdup(")"), 0);}
| '[' s_seqopt bracket_middleopt ']' s_seqopt {$$ = cat(strdup("["), $3,
strdup("]"), $5, 0);}
| '[' s_seqopt bracket_middleopt EOF {$$ = cat(strdup("["), $3, strdup("]"),
0);}
;
block
: '{' s_seqopt brace_middleopt '}' s_seqopt {$$ = cat(strdup("{\n"),
$3, strdup("}\n"), 0);}
| '{' s_seqopt brace_middleopt EOF {$$ = cat(strdup("{\n"), $3,
strdup("}\n"), 0);}
;
bracket_middleopt
: bracket_middleopt bracket_middle {$$ = cat($1, strdup(" "), $2, 0);}
| /* empty */ {$$ = strdup("");}
;
pare_middleopt
: pare_middleopt pare_middle {$$ = cat($1, strdup(" "), $2, 0);}
| /* empty */ {$$ = strdup("");}
;
brace_middleopt
: brace_middleopt brace_middle {$$ = cat($1, strdup(" "), $2, 0);}
| /* empty */ {$$ = strdup("");}
;
bracket_middle
: no_close
| ')' s_seqopt
| ';' s_seqopt
| '}' s_seqopt
;
pare_middle
: no_close
| ']' s_seqopt
| ';' s_seqopt
| '}' s_seqopt
;
brace_middle
: no_close
| ']' s_seqopt
| ';' s_seqopt
| ')' s_seqopt
;
no_close
: any
| block
| ATKEYWORD s_seqopt
| CDO s_seqopt
| CDC s_seqopt
;
s_seqopt
: s_seqopt S {$$ = strdup(" ");}
| s_seqopt COMMENT {}
| /* empty */ {$$ = strdup("");}
;
%%
int
main(int argc, char **argv)
{
yyparse();
}
%option 8bit caseless nodefault noyywrap noinput yylineno
%{
#undef EOF
#include "css.tab.h"
#include <string.h>
#define return_token(V) yylval.s = strdup(yytext); return V
%}
/* Currently only for ASCII-compatible encodings, such as ISO-8859-1 or UTF-8 */
ident [-]?{nmstart}{nmchar}*
name {nmchar}+
nmstart [_a-z]|{nonascii}|{escape}
nonascii [^\0-\240]
unicode \\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?
escape {unicode}|\\[^\n\r\f0-9a-f]
nmchar [_a-z0-9-]|{nonascii}|{escape}
num [0-9]+|[0-9]*\.[0-9]+
string {string1}|{string2}
string1 \"([^\n\r\f\\"]|\\{nl}|{escape})*\"
string2 \'([^\n\r\f\\']|\\{nl}|{escape})*\'
badstring {badstring1}|{badstring2}
badstring1 \"([^\n\r\f\\"]|\\{nl}|{escape})*\\?
badstring2 \'([^\n\r\f\\']|\\{nl}|{escape})*\\?
nl \n|\r\n|\r|\f
w [ \t\r\n\f]*
uri {uri1}|{uri2}
uri1 url\({w}{string}{w}\)
uri2 url\({w}([!#$%&*-~]|{nonascii}|{escape})*{w}\)
baduri {baduri1}|{baduri2}|{baduri3}
baduri1 url\({w}([!#$%&*-~]|{nonascii}|{escape})*{w}
baduri2 url\({w}{string}{w}
baduri3 url\({w}{badstring}
comment \/\*[^*]*\*+([^/*][^*]*\*+)*\/
badcomment {badcomment1}|{badcomment2}
badcomment1 \/\*[^*]*\*+([^/*][^*]*\*+)*
badcomment2 \/\*[^*]*(\*+[^/*][^*]*)*
/* At EOF, we return an infinite number of EOD tokens. */
%x eof
%%
<<EOF>> { return_token(EOF); yyterminate(); }
{uri} return_token(URI);
{baduri} return_token(BAD_URI);
u\+[0-9a-f?]{1,6}(-[0-9a-f]{1,6})? return_token(UNICODE_RANGE);
{ident} return_token(IDENT);
@{ident} return_token(ATKEYWORD);
#{name} return_token(HASH);
{num} return_token(NUMBER);
{num}% return_token(PERCENTAGE);
{num}{ident} return_token(DIMENSION);
"<!--" return_token(CDO);
"-->" return_token(CDC);
":" return_token(':');
";" return_token(';');
"{" return_token('{');
"}" return_token('}');
"(" return_token('(');
")" return_token(')');
"[" return_token('[');
"]" return_token(']');
[ \t\r\n\f]+ return_token(S);
{ident}\( return_token(FUNCTION);
"~=" return_token(INCLUDES);
"|=" return_token(DASHMATCH);
{comment} return_token(COMMENT);
{badcomment} return_token(BAD_COMMENT);
{string} return_token(STRING);
{badstring} return_token(BAD_STRING);
. return_token(DELIM);
{ident}/\\ return_token(IDENT);
#{name}/\\ return_token(HASH);
@{ident}/\\ return_token(ATKEYWORD);
#/\\ return_token(DELIM);
@/\\ return_token(DELIM);
@/- return_token(DELIM);
@/-\\ return_token(DELIM);
-/\\ return_token(DELIM);
-/- return_token(DELIM);
\</! return_token(DELIM);
\</!- return_token(DELIM);
{num}{ident}/\\ return_token(DIMENSION);
{num}/\\ return_token(NUMBER);
{num}/- return_token(NUMBER);
{num}/-\\ return_token(NUMBER);
[0-9]+/\. return_token(NUMBER);
u/\+ return_token(IDENT);
u\+[0-9a-f?]{1,6}/- return_token(UNICODE_RANGE);