use std::fmt;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Tin {
Ob,
Cb,
Cl,
Ca,
Tx,
Gc,
Vl,
Cc,
Atr,
Atd,
Atk,
Ats,
Zz,
}
impl Tin {
pub fn name(self) -> &'static str {
match self {
Tin::Ob => "#OB",
Tin::Cb => "#CB",
Tin::Cl => "#CL",
Tin::Ca => "#CA",
Tin::Tx => "#TX",
Tin::Gc => "#GC",
Tin::Vl => "#VL",
Tin::Cc => "#CC",
Tin::Atr => "#ATR",
Tin::Atd => "#ATD",
Tin::Atk => "#ATK",
Tin::Ats => "#ATS",
Tin::Zz => "#ZZ",
}
}
pub fn describe(self) -> &'static str {
match self {
Tin::Ob => "{ — start of a block",
Tin::Cb => "} — end of a block",
Tin::Cl => ": — declaration separator",
Tin::Ca => "; — declaration terminator",
Tin::Tx => "a selector, keyframe value, or property name",
Tin::Gc => ", — selector-group separator",
Tin::Vl => "a declaration value (raw text)",
Tin::Cc => "a comment",
Tin::Atr => "an at-rule with a rules body (@media, @supports, …)",
Tin::Atd => "an at-rule with a declarations body (@font-face, @page)",
Tin::Atk => "@keyframes",
Tin::Ats => "a statement at-rule (@import, @charset, …)",
Tin::Zz => "end of input",
}
}
}
#[derive(Clone, Debug)]
pub struct Token {
pub tin: Tin,
pub val: String,
pub src: String,
pub r_i: usize,
pub c_i: usize,
pub use_text: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Error {
pub code: String,
pub message: String,
pub line: usize,
pub column: usize,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"[css/{}]: {} (line {}, column {})",
self.code, self.message, self.line, self.column
)
}
}
impl std::error::Error for Error {}
#[derive(Clone, Copy, Debug)]
pub struct Point {
pub s_i: usize,
pub r_i: usize,
pub c_i: usize,
}
const UNTERMINATED: usize = usize::MAX;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Kind {
Selector,
Decl,
}
const COMMENT_NODE_RULES: [&str; 6] = [
"items",
"decls",
"kfitems",
"declbody",
"rulesbody",
"kfbody",
];
fn is_comment_node_rule(name: &str) -> bool {
COMMENT_NODE_RULES.contains(&name)
}
const DECLS_KW: [&str; 7] = [
"font-face",
"page",
"viewport",
"-ms-viewport",
"counter-style",
"property",
"font-palette-values",
];
fn is_keyframes_kw(kw: &str) -> bool {
if kw == "keyframes" {
return true;
}
match vendor_prefix(kw) {
Some(v) => &kw[v.len()..] == "keyframes",
None => false,
}
}
pub fn vendor_prefix(kw: &str) -> Option<&str> {
let b = kw.as_bytes();
if b.first() != Some(&b'-') {
return None;
}
let mut i = 1;
while i < b.len() && b[i].is_ascii_lowercase() {
i += 1;
}
if 1 < i && b.get(i) == Some(&b'-') {
Some(&kw[..i + 1])
} else {
None
}
}
pub struct Lex {
src: String,
bytes: Vec<u8>,
pnt: Point,
buf: Vec<Token>,
lowercase_properties: bool,
}
impl Lex {
pub fn new(src: &str, lowercase_properties: bool) -> Lex {
Lex {
src: src.to_string(),
bytes: src.as_bytes().to_vec(),
pnt: Point {
s_i: 0,
r_i: 1,
c_i: 1,
},
buf: Vec::new(),
lowercase_properties,
}
}
pub fn peek(&mut self, n: usize, rule: &str) -> Result<&[Token], Error> {
while self.buf.len() < n {
let tok = self.lex_one(rule)?;
self.buf.push(tok);
}
Ok(&self.buf[..n])
}
pub fn consume(&mut self, n: usize) {
let n = n.min(self.buf.len());
self.buf.drain(..n);
}
pub fn point(&self) -> Point {
self.pnt
}
fn err(&self, code: &str, message: String) -> Error {
Error {
code: code.to_string(),
message,
line: self.pnt.r_i,
column: self.pnt.c_i,
}
}
fn lex_one(&mut self, rule: &str) -> Result<Token, Error> {
loop {
self.skip_space();
if self.pnt.s_i >= self.bytes.len() {
return Ok(Token {
tin: Tin::Zz,
val: String::new(),
src: String::new(),
r_i: self.pnt.r_i,
c_i: self.pnt.c_i,
use_text: String::new(),
});
}
if let Some(tok) = self.css_token(rule)? {
return Ok(tok);
}
let s_i = self.pnt.s_i;
let c = self.bytes[s_i];
if c == b'/' && self.bytes.get(s_i + 1) == Some(&b'*') {
let e = skip_comment(&self.bytes, s_i);
if UNTERMINATED == e {
return Err(self.err(
"unterminated_comment",
"unterminated comment: no closing '*/'".to_string(),
));
}
self.advance(s_i, e);
continue;
}
let tin = match c {
b'{' => Tin::Ob,
b'}' => Tin::Cb,
b':' => Tin::Cl,
b';' => Tin::Ca,
_ => {
let end = next_char_end(&self.bytes, s_i);
return Err(self.err(
"unexpected",
format!("unexpected character(s): {}", &self.src[s_i..end]),
));
}
};
let text = (c as char).to_string();
let tok = self.make(tin, text.clone(), text, String::new());
self.advance(s_i, s_i + 1);
return Ok(tok);
}
}
fn skip_space(&mut self) {
while self.pnt.s_i < self.bytes.len() {
match self.bytes[self.pnt.s_i] {
b' ' | b'\t' => {
self.pnt.s_i += 1;
self.pnt.c_i += 1;
}
b'\r' => {
self.pnt.s_i += 1;
self.pnt.c_i = 1;
}
b'\n' => {
self.pnt.s_i += 1;
self.pnt.r_i += 1;
self.pnt.c_i = 1;
}
_ => return,
}
}
}
fn make(&self, tin: Tin, val: String, src: String, use_text: String) -> Token {
Token {
tin,
val,
src,
r_i: self.pnt.r_i,
c_i: self.pnt.c_i,
use_text,
}
}
fn advance(&mut self, s_i: usize, end: usize) {
let stop = end.min(self.bytes.len());
let overshoot = end - stop;
let span = safe_slice(&self.src, s_i, stop);
match span.rfind('\n') {
Some(last_nl) => {
self.pnt.r_i += span.matches('\n').count();
self.pnt.c_i = 1 + col_width(&span[last_nl + 1..]) + overshoot;
}
None => self.pnt.c_i += col_width(span) + overshoot,
}
self.pnt.s_i = end;
}
fn css_token(&mut self, rule: &str) -> Result<Option<Token>, Error> {
let s_i = self.pnt.s_i;
let c = self.bytes[s_i];
if c == b'/' && self.bytes.get(s_i + 1) == Some(&b'*') {
if !is_comment_node_rule(rule) {
return Ok(None);
}
let e = match find_comment_end(&self.bytes, s_i) {
Some(e) => e,
None => {
return Err(self.err(
"unterminated_comment",
"unterminated comment: no closing '*/'".to_string(),
))
}
};
let tok = self.make(
Tin::Cc,
safe_slice(&self.src, s_i + 2, e).to_string(),
safe_slice(&self.src, s_i, e + 2).to_string(),
String::new(),
);
self.advance(s_i, e + 2);
return Ok(Some(tok));
}
if "declval" == rule {
if c == b'{' || c == b'}' || c == b';' || c == b':' {
return Ok(None);
}
let end = self.scan_or_bad(scan_value_end(&self.bytes, s_i))?;
let raw = safe_slice(&self.src, s_i, end);
let tok = self.make(
Tin::Vl,
es_trim(&strip_comments(raw)).to_string(),
raw.to_string(),
String::new(),
);
self.advance(s_i, end);
return Ok(Some(tok));
}
if c == b',' {
let tok = self.make(Tin::Gc, ",".to_string(), ",".to_string(), String::new());
self.advance(s_i, s_i + 1);
return Ok(Some(tok));
}
if c == b'@' {
return self.at_rule(s_i).map(Some);
}
if c == b'{' || c == b'}' || c == b';' {
return Ok(None);
}
let (kind, index) = scan_to_brace_or_end(&self.bytes, s_i);
self.scan_or_bad(index)?;
if Kind::Selector == kind {
let end = self.scan_or_bad(scan_selector_end(&self.bytes, s_i))?;
let raw = safe_slice(&self.src, s_i, end);
let tok = self.make(
Tin::Tx,
es_trim(&strip_comments(raw)).to_string(),
raw.to_string(),
String::new(),
);
self.advance(s_i, end);
return Ok(Some(tok));
}
let e_i = scan_prop_end(&self.bytes, s_i);
if e_i == s_i {
return Ok(None);
}
let raw = safe_slice(&self.src, s_i, e_i);
let mut prop = strip_comments(raw);
if prop.is_empty() {
return Ok(None);
}
if self.lowercase_properties {
prop = prop.to_lowercase();
}
let tok = self.make(Tin::Tx, prop, raw.to_string(), String::new());
self.advance(s_i, e_i);
Ok(Some(tok))
}
fn at_rule(&mut self, s_i: usize) -> Result<Token, Error> {
let mut k_end = s_i + 1;
while k_end < self.bytes.len() && is_at_char(self.bytes[k_end]) {
k_end += 1;
}
let kw = safe_slice(&self.src, s_i + 1, k_end).to_string();
let (kind, index) = scan_to_brace_or_end(&self.bytes, s_i);
let index = self.scan_or_bad(index)?;
if Kind::Selector == kind {
let prelude = es_trim(safe_slice(&self.src, k_end, index)).to_string();
let tin = if is_keyframes_kw(&kw) {
Tin::Atk
} else if DECLS_KW.contains(&kw.as_str()) {
Tin::Atd
} else {
Tin::Atr
};
let tok = self.make(
tin,
kw,
safe_slice(&self.src, s_i, index).to_string(),
prelude,
);
self.advance(s_i, index);
return Ok(tok);
}
let p_end = self.scan_or_bad(scan_value_end(&self.bytes, k_end))?;
let params = es_trim(safe_slice(&self.src, k_end, p_end)).to_string();
let end = if self.bytes.get(p_end) == Some(&b';') {
p_end + 1
} else {
p_end
};
let tok = self.make(
Tin::Ats,
kw,
safe_slice(&self.src, s_i, end).to_string(),
params,
);
self.advance(s_i, end);
Ok(tok)
}
fn scan_or_bad(&self, index: usize) -> Result<usize, Error> {
if UNTERMINATED == index {
return Err(self.err(
"unterminated_comment",
"unterminated comment: no closing '*/'".to_string(),
));
}
Ok(index)
}
}
pub fn es_is_whitespace(c: char) -> bool {
('\u{feff}' == c) || (c.is_whitespace() && '\u{85}' != c)
}
pub fn es_trim(s: &str) -> &str {
s.trim_matches(es_is_whitespace)
}
fn col_width(s: &str) -> usize {
s.chars().map(char::len_utf16).sum()
}
fn safe_slice(src: &str, start: usize, end: usize) -> &str {
let mut start = start.min(src.len());
let mut end = end.clamp(start, src.len());
while 0 < start && !src.is_char_boundary(start) {
start -= 1;
}
while start < end && !src.is_char_boundary(end) {
end -= 1;
}
&src[start..end]
}
fn next_char_end(src: &[u8], i: usize) -> usize {
let mut e = i + 1;
while e < src.len() && (src[e] & 0xC0) == 0x80 {
e += 1;
}
e.min(src.len())
}
fn find_comment_end(src: &[u8], i: usize) -> Option<usize> {
let mut j = i + 2;
while j + 1 < src.len() {
if src[j] == b'*' && src[j + 1] == b'/' {
return Some(j);
}
j += 1;
}
None
}
fn skip_comment(src: &[u8], i: usize) -> usize {
match find_comment_end(src, i) {
Some(e) => e + 2,
None => UNTERMINATED,
}
}
fn skip_string(src: &[u8], mut i: usize) -> usize {
let quote = src[i];
i += 1;
while i < src.len() {
if src[i] == b'\\' {
i += 2;
continue;
}
if src[i] == quote {
return i + 1;
}
i += 1;
}
i
}
fn scan_to_brace_or_end(src: &[u8], mut i: usize) -> (Kind, usize) {
let mut depth = 0usize;
while i < src.len() {
let c = src[i];
if c == b'"' || c == b'\'' {
i = skip_string(src, i);
continue;
}
if c == b'/' && src.get(i + 1) == Some(&b'*') {
let n = skip_comment(src, i);
if UNTERMINATED == n {
return (Kind::Decl, UNTERMINATED);
}
i = n;
continue;
}
if c == b'\\' {
i += 2;
continue;
}
if c == b'(' || c == b'[' {
depth += 1;
} else if c == b')' || c == b']' {
depth = depth.saturating_sub(1);
} else if 0 == depth {
if c == b'{' {
return (Kind::Selector, i);
}
if c == b';' || c == b'}' {
return (Kind::Decl, i);
}
}
i += 1;
}
(Kind::Decl, i)
}
fn scan_selector_end(src: &[u8], mut i: usize) -> usize {
let mut depth = 0usize;
while i < src.len() {
let c = src[i];
if c == b'"' || c == b'\'' {
i = skip_string(src, i);
continue;
}
if c == b'/' && src.get(i + 1) == Some(&b'*') {
let n = skip_comment(src, i);
if UNTERMINATED == n {
return UNTERMINATED;
}
i = n;
continue;
}
if c == b'\\' {
i += 2;
continue;
}
if c == b'(' || c == b'[' {
depth += 1;
} else if c == b')' || c == b']' {
depth = depth.saturating_sub(1);
} else if 0 == depth && (c == b',' || c == b'{') {
return i;
}
i += 1;
}
i
}
fn scan_value_end(src: &[u8], mut i: usize) -> usize {
let mut depth = 0usize;
while i < src.len() {
let c = src[i];
if c == b'"' || c == b'\'' {
i = skip_string(src, i);
continue;
}
if c == b'/' && src.get(i + 1) == Some(&b'*') {
let n = skip_comment(src, i);
if UNTERMINATED == n {
return UNTERMINATED;
}
i = n;
continue;
}
if c == b'\\' {
i += 2;
continue;
}
if c == b'(' || c == b'[' {
depth += 1;
} else if c == b')' || c == b']' {
depth = depth.saturating_sub(1);
} else if 0 == depth && (c == b';' || c == b'}') {
return i;
}
i += 1;
}
i
}
pub fn strip_comments(s: &str) -> String {
if !s.contains("/*") {
return s.to_string();
}
let src = s.as_bytes();
let mut out: Vec<u8> = Vec::with_capacity(src.len());
let mut i = 0;
while i < src.len() {
let c = src[i];
if c == b'"' || c == b'\'' {
let j = skip_string(src, i);
out.extend_from_slice(&src[i..j.min(src.len())]);
i = j;
continue;
}
if c == b'/' && src.get(i + 1) == Some(&b'*') {
let n = skip_comment(src, i);
if UNTERMINATED == n {
break;
}
i = n;
continue;
}
out.push(c);
i += 1;
}
String::from_utf8(out).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
}
fn is_prop_char(c: u8) -> bool {
c.is_ascii_alphanumeric()
|| c == b'-'
|| c == b'_'
|| c == b'#'
|| c == b'*'
|| c == b'/'
|| c == b'\\'
}
fn is_prop_suffix_char(c: u8) -> bool {
c.is_ascii_digit() || c.is_ascii_lowercase() || c == b'-' || c == b'_'
}
fn scan_prop_end(src: &[u8], i: usize) -> usize {
let mut e_i = i;
while e_i < src.len() && is_prop_char(src[e_i]) {
e_i += 1;
}
if e_i == i {
return i;
}
if src.get(e_i) == Some(&b'[') {
let mut b_i = e_i + 1;
while b_i < src.len() && is_prop_suffix_char(src[b_i]) {
b_i += 1;
}
if e_i + 1 < b_i && src.get(b_i) == Some(&b']') {
return b_i + 1;
}
}
e_i
}
fn is_at_char(c: u8) -> bool {
c.is_ascii_alphanumeric() || c == b'-'
}
pub fn split_selectors(prelude: &str) -> Vec<String> {
let src = prelude.as_bytes();
let mut out = Vec::new();
let mut i = 0;
while i <= src.len() {
let mut end = scan_selector_end(src, i);
if UNTERMINATED == end {
end = src.len();
}
let one = es_trim(&strip_comments(safe_slice(prelude, i, end))).to_string();
if !one.is_empty() {
out.push(one);
}
if end >= src.len() {
break;
}
i = end + 1;
}
out
}
pub fn start_pos(tok: &Token) -> (usize, usize) {
(tok.r_i, tok.c_i)
}
pub fn end_pos(tok: &Token) -> (usize, usize) {
match tok.src.rfind('\n') {
Some(last_nl) => (
tok.r_i + tok.src.matches('\n').count(),
1 + col_width(&tok.src[last_nl + 1..]),
),
None => (tok.r_i, tok.c_i + col_width(&tok.src)),
}
}