Skip to content

Commit 66acc85

Browse files
committed
Add an incomplete CSS serializer.
Serialization is considered correct if it maintains the invariant: parse(serialize(parse(text))) == parse(text) for any text (Unicode string.) This is not the case of this code yet (see servo#24), but it’s good enough for error reporting in Servo.
1 parent f494977 commit 66acc85

File tree

4 files changed

+227
-1
lines changed

4 files changed

+227
-1
lines changed

Makefile.in

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ cssparser-test: lib.rs $(RUST_SRC)
2222

2323
.PHONY: check
2424
check: cssparser-test
25-
./cssparser-test
25+
./cssparser-test $(TEST)
2626

2727
.PHONY: check-debug
2828
check-debug: cssparser-tests

lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ pub use tokenizer::*;
1313
pub use parser::*;
1414
pub use color::*;
1515
pub use nth::*;
16+
pub use serializer::{ToCss, serialize_identifier, serialize_string};
1617

1718
pub mod ast;
1819
pub mod tokenizer;
1920
pub mod parser;
2021
pub mod color;
2122
pub mod nth;
23+
pub mod serializer;
2224

2325
#[cfg(test)]
2426
mod tests;

serializer.rs

+212
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
use ast;
6+
use ast::*;
7+
8+
9+
impl ast::ComponentValue {
10+
pub fn to_css(&mut self) -> ~str {
11+
let mut css = ~"";
12+
self.to_css_push(&mut css);
13+
css
14+
}
15+
16+
pub fn to_css_push(&self, css: &mut ~str) {
17+
match *self {
18+
Ident(ref value) => serialize_identifier(value.as_slice(), css),
19+
AtKeyword(ref value) => {
20+
css.push_char('@');
21+
serialize_identifier(value.as_slice(), css);
22+
},
23+
Hash(ref value) | IDHash(ref value) => {
24+
css.push_char('#');
25+
serialize_identifier(value.as_slice(), css);
26+
}
27+
String(ref value) => serialize_string(value.as_slice(), css),
28+
URL(ref value) => {
29+
css.push_str("url(");
30+
serialize_string(value.as_slice(), css);
31+
css.push_char(')');
32+
},
33+
Delim(value) => css.push_char(value),
34+
35+
Number(ref value) => css.push_str(value.representation),
36+
Percentage(ref value) => {
37+
css.push_str(value.representation);
38+
css.push_char('%');
39+
},
40+
Dimension(ref value, ref unit) => {
41+
css.push_str(value.representation);
42+
serialize_identifier(unit.as_slice(), css);
43+
},
44+
45+
UnicodeRange(start, end) => {
46+
css.push_str(format!("U+{:X}", start));
47+
if end != start {
48+
css.push_str(format!("-{:X}", end));
49+
}
50+
}
51+
52+
WhiteSpace => css.push_char(' '),
53+
Colon => css.push_char(':'),
54+
Semicolon => css.push_char(';'),
55+
Comma => css.push_char(','),
56+
IncludeMatch => css.push_str("~="),
57+
DashMatch => css.push_str("|="),
58+
PrefixMatch => css.push_str("^="),
59+
SuffixMatch => css.push_str("$="),
60+
SubstringMatch => css.push_str("*="),
61+
Column => css.push_str("||"),
62+
CDO => css.push_str("<!--"),
63+
CDC => css.push_str("-->"),
64+
65+
Function(ref name, ref arguments) => {
66+
serialize_identifier(name.as_slice(), css);
67+
css.push_char('(');
68+
arguments.iter().to_css_push(css);
69+
css.push_char(')');
70+
},
71+
ParenthesisBlock(ref content) => {
72+
css.push_char('(');
73+
content.iter().to_css_push(css);
74+
css.push_char(')');
75+
},
76+
SquareBracketBlock(ref content) => {
77+
css.push_char('[');
78+
content.iter().to_css_push(css);
79+
css.push_char(']');
80+
},
81+
CurlyBracketBlock(ref content) => {
82+
css.push_char('{');
83+
content.iter().map(|t| match *t { (ref c, _) => c }).to_css_push(css);
84+
css.push_char('}');
85+
},
86+
87+
BadURL => css.push_str("url(<bad url>)"),
88+
BadString => css.push_str("\"<bad string>\n"),
89+
CloseParenthesis => css.push_char(')'),
90+
CloseSquareBracket => css.push_char(']'),
91+
CloseCurlyBracket => css.push_char('}'),
92+
}
93+
}
94+
}
95+
96+
97+
pub fn serialize_identifier(value: &str, css: &mut ~str) {
98+
// TODO: avoid decoding/re-encoding UTF-8?
99+
let mut iter = value.iter();
100+
let mut c = iter.next().unwrap();
101+
if c == '-' {
102+
c = match iter.next() {
103+
None => {
104+
css.push_str("\\-");
105+
return
106+
}
107+
Some(c) => {
108+
css.push_char('-');
109+
c
110+
},
111+
}
112+
};
113+
match c {
114+
'A'..'Z' | 'a'..'z' | '_' => css.push_char(c),
115+
_ if c > '\x7F' => css.push_char(c),
116+
'\n' => css.push_str("\\A "),
117+
'\r' => css.push_str("\\D "),
118+
'\x0C' => css.push_str("\\C "),
119+
'0'..'9' => css.push_str(format!("\\\\3{} ", c)),
120+
_ => { css.push_char('\\'); css.push_char(c) },
121+
}
122+
for c in iter {
123+
match c {
124+
'0'..'9' | 'A'..'Z' | 'a'..'z' | '_' | '-' => css.push_char(c),
125+
_ if c > '\x7F' => css.push_char(c),
126+
'\n' => css.push_str("\\A "),
127+
'\r' => css.push_str("\\D "),
128+
'\x0C' => css.push_str("\\C "),
129+
_ => { css.push_char('\\'); css.push_char(c) },
130+
}
131+
}
132+
}
133+
134+
pub fn serialize_string(value: &str, css: &mut ~str) {
135+
css.push_char('"');
136+
// TODO: avoid decoding/re-encoding UTF-8?
137+
for c in value.iter() {
138+
match c {
139+
'"' => css.push_str("\""),
140+
'\\' => css.push_str("\\\\"),
141+
'\n' => css.push_str("\\A "),
142+
'\r' => css.push_str("\\D "),
143+
'\x0C' => css.push_str("\\C "),
144+
_ => css.push_char(c),
145+
}
146+
}
147+
css.push_char('"');
148+
}
149+
150+
151+
pub trait ToCss {
152+
fn to_css(&mut self) -> ~str {
153+
let mut css = ~"";
154+
self.to_css_push(&mut css);
155+
css
156+
}
157+
158+
fn to_css_push(&mut self, css: &mut ~str);
159+
}
160+
161+
162+
impl<'self, I: Iterator<&'self ComponentValue>> ToCss for I {
163+
fn to_css_push(&mut self, css: &mut ~str) {
164+
let mut previous = match self.next() {
165+
None => return,
166+
Some(first) => { first.to_css_push(css); first }
167+
};
168+
macro_rules! matches(
169+
($value:expr, $($pattern:pat)|+) => (
170+
match $value { $($pattern)|+ => true, _ => false }
171+
);
172+
)
173+
loop { match self.next() { None => break, Some(component_value) => {
174+
let (a, b) = (previous, component_value);
175+
if (
176+
matches!(*a, Hash(*) | IDHash(*) | AtKeyword(*)) &&
177+
matches!(*b, Number(*) | Percentage(*) | Ident(*) | Dimension(*) |
178+
UnicodeRange(*) | URL(*) | Function(*))
179+
) || (
180+
matches!(*a, Number(*) | Ident(*) | Dimension(*)) &&
181+
matches!(*b, Number(*) | Ident(*) | Dimension(*))
182+
) || (
183+
matches!(*a, Number(*) | Ident(*) | Dimension(*)) &&
184+
matches!(*b, Percentage(*) | UnicodeRange(*) | URL(*) | Function(*))
185+
) || (
186+
matches!(*a, Ident(*)) &&
187+
matches!(*b, ParenthesisBlock(*))
188+
) || (
189+
matches!(*a, Delim('#') | Delim('@')) &&
190+
!matches!(*b, WhiteSpace)
191+
) || (
192+
matches!(*a, Delim('-') | Delim('+') | Delim('.') | Delim('<') |
193+
Delim('>') | Delim('!')) &&
194+
!matches!(*b, WhiteSpace)
195+
) || (
196+
!matches!(*a, WhiteSpace) &&
197+
matches!(*b, Delim('-') | Delim('+') | Delim('.') | Delim('<') |
198+
Delim('>') | Delim('!'))
199+
) || (
200+
matches!(*a, Delim('/')) &&
201+
matches!(*b, Delim('*'))
202+
) || (
203+
matches!(*a, Delim('*')) &&
204+
matches!(*b, Delim('/'))
205+
) {
206+
css.push_str("/**/")
207+
}
208+
component_value.to_css_push(css);
209+
previous = component_value;
210+
}}}
211+
}
212+
}

tests.rs

+12
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,18 @@ fn nth() {
173173
}
174174
175175
176+
// FIXME: serializer tests disabled for now
177+
// https://github.com/mozilla-servo/rust-cssparser/issues/24
178+
//#[test]
179+
fn serializer() {
180+
do run_json_tests(include_str!("css-parsing-tests/component_value_list.json")) |input| {
181+
let component_values = tokenize(input).map(|(c, _)| c).to_owned_vec();
182+
let serialized = component_values.iter().to_css();
183+
tokenize(serialized).map(|(c, _)| c).to_owned_vec()
184+
}
185+
}
186+
187+
176188
impl ToJson for Result<Rule, SyntaxError> {
177189
fn to_json(&self) -> json::Json {
178190
match *self {

0 commit comments

Comments
 (0)