use std::fmt;
use std::sync::Arc;
use tabnas::{
Context, Lexer, Rule, TabnasError, Tin as EngineTin, Token, Value, TIN_TX, TIN_VL, TIN_ZZ,
};
#[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, 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 {}
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 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)
}
pub(crate) 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 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], i: usize) -> (Kind, usize) {
let mut scan = Scan { i, depth: 0 };
loop {
if let Some(found) = scan.step(src) {
return found;
}
}
}
#[derive(Clone, Copy, Debug)]
struct Scan {
i: usize,
depth: usize,
}
impl Scan {
fn step(&mut self, src: &[u8]) -> Option<(Kind, usize)> {
let Some(&c) = src.get(self.i) else {
return Some((Kind::Decl, self.i));
};
if c == b'"' || c == b'\'' {
self.i = skip_string(src, self.i);
return None;
}
if c == b'/' && src.get(self.i + 1) == Some(&b'*') {
let n = skip_comment(src, self.i);
if UNTERMINATED == n {
return Some((Kind::Decl, UNTERMINATED));
}
self.i = n;
return None;
}
if c == b'\\' {
self.i += 2;
return None;
}
if c == b'(' || c == b'[' {
self.depth += 1;
} else if c == b')' || c == b']' {
self.depth = self.depth.saturating_sub(1);
} else if 0 == self.depth {
if c == b'{' {
return Some((Kind::Selector, self.i));
}
if c == b';' || c == b'}' {
return Some((Kind::Decl, self.i));
}
}
self.i += 1;
None
}
}
#[derive(Clone, Copy, Debug)]
struct BraceScans {
from: usize,
found: (Kind, usize),
cursor: Scan,
}
pub(crate) const BRACE_SCAN: &str = "tabnas-css/brace-scan";
impl BraceScans {
fn scan(ctx: &mut Context, src: &[u8], s_i: usize) -> (Kind, usize) {
let Some(Value::Array(slot)) = ctx.u.get_mut(BRACE_SCAN) else {
let found = scan_to_brace_or_end(src, s_i);
let kept = BraceScans::fresh(s_i, found);
ctx.u
.insert(BRACE_SCAN.to_string(), Value::array(kept.fields().to_vec()));
return found;
};
let fields = Arc::make_mut(slot);
if let Some(mut kept) = BraceScans::decode(fields) {
if kept.from <= s_i && s_i < kept.found.1 && kept.cursor.i <= s_i {
while kept.cursor.i < s_i && kept.cursor.step(src).is_none() {}
let reached = kept.cursor.i == s_i && 0 == kept.cursor.depth;
kept.encode(fields);
if reached {
return kept.found;
}
}
}
let found = scan_to_brace_or_end(src, s_i);
BraceScans::fresh(s_i, found).encode(fields);
found
}
fn fresh(from: usize, found: (Kind, usize)) -> BraceScans {
BraceScans {
from,
found,
cursor: Scan { i: from, depth: 0 },
}
}
fn decode(fields: &[Value]) -> Option<BraceScans> {
let at = |n: usize| match fields.get(n) {
Some(Value::Number(v)) => Some(*v as usize),
_ => None,
};
let kind = if 0 == at(1)? {
Kind::Selector
} else {
Kind::Decl
};
Some(BraceScans {
from: at(0)?,
found: (kind, at(2)?),
cursor: Scan {
i: at(3)?,
depth: at(4)?,
},
})
}
fn numbers(&self) -> [f64; 5] {
let kind = match self.found.0 {
Kind::Selector => 0,
Kind::Decl => 1,
};
[
self.from,
kind,
self.found.1,
self.cursor.i,
self.cursor.depth,
]
.map(|n| n as f64)
}
fn fields(&self) -> [Value; 5] {
self.numbers().map(Value::Number)
}
fn encode(&self, fields: &mut Vec<Value>) {
let numbers = self.numbers();
if numbers.len() == fields.len() {
for (field, n) in fields.iter_mut().zip(numbers) {
match field {
Value::Number(slot) => *slot = n,
other => *other = Value::Number(n),
}
}
} else {
*fields = self.fields().to_vec();
}
}
}
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
}
const BY_NAME: EngineTin = -1;
pub(crate) const OVERSHOOT: &str = "tabnas-css/overshoot";
const BAD_SPAN: usize = 32;
enum Plan {
Decline,
Bad,
Emit {
name: &'static str,
tin: EngineTin,
val: String,
raw: String,
use_kv: Option<(&'static str, String)>,
end: usize,
},
}
fn plan_token(
src: &str,
s_i: usize,
rule: &str,
lower: bool,
brace: &mut dyn FnMut(usize) -> (Kind, usize),
) -> Plan {
let bytes = src.as_bytes();
let Some(&c) = bytes.get(s_i) else {
return Plan::Decline;
};
if c == b' ' || c == b'\t' || c == b'\r' || c == b'\n' {
return Plan::Decline;
}
let emit = |name, tin, val: String, raw: &str, use_kv, end| Plan::Emit {
name,
tin,
val,
raw: raw.to_string(),
use_kv,
end,
};
if c == b'/' && bytes.get(s_i + 1) == Some(&b'*') {
if !is_comment_node_rule(rule) {
return Plan::Decline;
}
return match find_comment_end(bytes, s_i) {
None => Plan::Bad,
Some(e) => emit(
"#CC",
BY_NAME,
safe_slice(src, s_i + 2, e).to_string(),
safe_slice(src, s_i, e + 2),
None,
e + 2,
),
};
}
if "declval" == rule {
if c == b'{' || c == b'}' || c == b';' || c == b':' {
return Plan::Decline;
}
let end = scan_value_end(bytes, s_i);
if UNTERMINATED == end {
return Plan::Bad;
}
let raw = safe_slice(src, s_i, end);
let val = es_trim(&strip_comments(raw)).to_string();
return emit("#VL", TIN_VL, val, raw, None, end);
}
if c == b',' {
return emit("#GC", BY_NAME, ",".to_string(), ",", None, s_i + 1);
}
if c == b'@' {
let mut k_end = s_i + 1;
while k_end < bytes.len() && is_at_char(bytes[k_end]) {
k_end += 1;
}
let kw = safe_slice(src, s_i + 1, k_end).to_string();
let (kind, index) = brace(s_i);
if UNTERMINATED == index {
return Plan::Bad;
}
if Kind::Selector == kind {
let prelude = es_trim(safe_slice(src, k_end, index)).to_string();
let (name, tin) = if is_keyframes_kw(&kw) {
("#ATK", BY_NAME)
} else if DECLS_KW.contains(&kw.as_str()) {
("#ATD", BY_NAME)
} else {
("#ATR", BY_NAME)
};
let raw = safe_slice(src, s_i, index);
return emit(name, tin, kw, raw, Some(("prelude", prelude)), index);
}
let p_end = scan_value_end(bytes, k_end);
if UNTERMINATED == p_end {
return Plan::Bad;
}
let params = es_trim(safe_slice(src, k_end, p_end)).to_string();
let end = if bytes.get(p_end) == Some(&b';') {
p_end + 1
} else {
p_end
};
let raw = safe_slice(src, s_i, end);
return emit("#ATS", BY_NAME, kw, raw, Some(("params", params)), end);
}
if c == b'{' || c == b'}' || c == b';' {
return Plan::Decline;
}
let (kind, index) = brace(s_i);
if UNTERMINATED == index {
return Plan::Bad;
}
if Kind::Selector == kind {
let end = scan_selector_end(bytes, s_i);
if UNTERMINATED == end {
return Plan::Bad;
}
let raw = safe_slice(src, s_i, end);
let val = es_trim(&strip_comments(raw)).to_string();
return emit("#TX", TIN_TX, val, raw, None, end);
}
let e_i = scan_prop_end(bytes, s_i);
if e_i == s_i {
return Plan::Decline;
}
let raw = safe_slice(src, s_i, e_i);
let mut prop = strip_comments(raw);
if prop.is_empty() {
return Plan::Decline;
}
if lower {
prop = prop.to_lowercase();
}
emit("#TX", TIN_TX, prop, raw, None, e_i)
}
pub(crate) fn css_token(
lexer: &mut Lexer<'_>,
rule: &mut Rule,
ctx: &mut Context,
lower: bool,
) -> Option<Token> {
let point = lexer.point();
let s_i = point.site.si;
let (plan, chars, overshoot) = {
let src = lexer.source();
let bytes = src.as_bytes();
let mut brace = |at: usize| BraceScans::scan(ctx, bytes, at);
let plan = plan_token(src, s_i, rule.name.as_str(), lower, &mut brace);
let (chars, overshoot) = match &plan {
Plan::Decline => return None,
Plan::Bad => (
safe_slice(src, s_i, src.len())
.chars()
.take(BAD_SPAN)
.count(),
0,
),
Plan::Emit { end, .. } => (
safe_slice(src, s_i, (*end).min(src.len())).chars().count(),
end.saturating_sub(src.len()),
),
};
(plan, chars, overshoot)
};
match plan {
Plan::Decline => None,
Plan::Bad => Some(lexer.bad_span(
"unterminated_comment",
point.site.pos,
point.site.pos + chars,
)),
Plan::Emit {
name,
tin,
val,
raw,
use_kv,
..
} => {
lexer.advance_chars(chars);
if 0 < overshoot {
ctx.u
.insert(OVERSHOOT.to_string(), Value::Number(overshoot as f64));
}
let mut token = lexer.token(name, tin, Value::String(val), raw, point);
if let Some((key, text)) = use_kv {
token
.use_data_mut()
.insert(key.to_string(), Value::String(text));
}
Some(token)
}
}
}
pub(crate) fn lex_subscriber(token: &mut Token, _rule: &mut Rule, ctx: &mut Context) {
if TIN_ZZ == token.tin {
if let Some(Value::Number(n)) = ctx.u.get(OVERSHOOT) {
token.site.ci += *n as usize;
}
}
}
pub(crate) fn astral_between(src: &str, from: usize, to: usize) -> usize {
src.chars()
.skip(from)
.take(to.saturating_sub(from))
.filter(|c| 2 == c.len_utf16())
.count()
}
impl From<TabnasError> for Error {
fn from(error: TabnasError) -> Error {
let from = error.pos.saturating_sub(error.col.saturating_sub(1));
let wide = astral_between(&error.full_source, from, error.pos);
Error {
code: error.code,
message: error.detail,
line: error.row,
column: error.col + wide,
}
}
}