use std::fmt;
use cssparser::{BasicParseErrorKind, ParseErrorKind as CssErrorKind, ToCss, Token};
use selectors::parser::SelectorParseErrorKind;
use unicode_width::UnicodeWidthChar;
const MAX_SELECTOR_ECHO: usize = 120;
const MAX_TOKEN_ECHO: usize = 40;
const MAX_GUTTER_WIDTH: usize = 72;
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum Error {
Parse {
kind: ParseErrorKind,
offset: usize,
},
Unsupported {
construct: String,
offset: Option<usize>,
},
}
impl Error {
#[must_use]
pub fn message(&self, selector: &str) -> String {
let quoted = quote(selector);
match self {
Error::Parse { kind, offset } => {
let (line, caret) = gutter(selector, *offset);
format!(
"Unable to parse the CSS selector {quoted}: {kind}\n |\n | {line}\n | {caret}"
)
}
Error::Unsupported {
construct,
offset: Some(offset),
} => {
let (line, caret) = gutter(selector, *offset);
format!(
"The CSS selector {quoted} uses {construct}, which this translator \
does not support\n |\n | {line}\n | {caret}"
)
}
Error::Unsupported {
construct,
offset: None,
} => format!(
"The CSS selector {quoted} uses {construct}, which this translator does not support"
),
}
}
#[deprecated(since = "0.3.0", note = "use `Error::message`, which takes `&self`")]
#[must_use]
pub fn into_message(self, selector: &str) -> String {
self.message(selector)
}
pub(crate) fn unsupported(construct: impl Into<String>) -> Self {
Error::Unsupported {
construct: construct.into(),
offset: None,
}
}
pub(crate) fn unsupported_at(construct: impl Into<String>, offset: usize) -> Self {
Error::Unsupported {
construct: construct.into(),
offset: Some(offset),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Parse { kind, offset } => {
write!(f, "invalid CSS selector at byte {offset}: {kind}")
}
Error::Unsupported {
construct,
offset: Some(offset),
} => {
write!(f, "unsupported CSS construct at byte {offset}: {construct}")
}
Error::Unsupported {
construct,
offset: None,
} => {
write!(f, "unsupported CSS construct: {construct}")
}
}
}
}
impl std::error::Error for Error {}
#[derive(Clone, Debug, Eq, PartialEq)]
#[non_exhaustive]
pub enum ParseErrorKind {
EmptySelector,
DanglingCombinator,
EndOfInput,
InvalidPosition,
UnexpectedToken(String),
ExpectedName(String),
UnsupportedPseudo(String),
InvalidAttributeSelector(String),
Other(String),
}
impl fmt::Display for ParseErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseErrorKind::EmptySelector => f.write_str("the selector is empty"),
ParseErrorKind::DanglingCombinator => f.write_str("a combinator with nothing after it"),
ParseErrorKind::EndOfInput => f.write_str("the selector ends unexpectedly"),
ParseErrorKind::InvalidPosition => {
f.write_str("a construct that is not allowed in this position")
}
ParseErrorKind::UnexpectedToken(token) => write!(f, "unexpected `{token}`"),
ParseErrorKind::ExpectedName(token) => write!(f, "expected a name, found `{token}`"),
ParseErrorKind::UnsupportedPseudo(name) => {
write!(
f,
"`{name}` is not a supported pseudo-class or pseudo-element"
)
}
ParseErrorKind::InvalidAttributeSelector(token) => {
write!(f, "`{token}` is not valid in an attribute selector")
}
ParseErrorKind::Other(detail) => f.write_str(detail),
}
}
}
impl ParseErrorKind {
pub(crate) fn from_kind(kind: &CssErrorKind<'_, SelectorParseErrorKind<'_>>) -> Self {
use SelectorParseErrorKind as S;
match kind {
CssErrorKind::Basic(BasicParseErrorKind::UnexpectedToken(t))
| CssErrorKind::Custom(S::ExplicitNamespaceUnexpectedToken(t)) => {
ParseErrorKind::UnexpectedToken(token_text(t))
}
CssErrorKind::Basic(BasicParseErrorKind::EndOfInput) => ParseErrorKind::EndOfInput,
CssErrorKind::Custom(S::EmptySelector) => ParseErrorKind::EmptySelector,
CssErrorKind::Custom(S::DanglingCombinator) => ParseErrorKind::DanglingCombinator,
CssErrorKind::Custom(S::InvalidState) => ParseErrorKind::InvalidPosition,
CssErrorKind::Custom(S::ClassNeedsIdent(t) | S::PseudoElementExpectedIdent(t)) => {
ParseErrorKind::ExpectedName(token_text(t))
}
CssErrorKind::Custom(S::UnsupportedPseudoClassOrElement(name)) => {
ParseErrorKind::UnsupportedPseudo(elide(sanitize(name)))
}
CssErrorKind::Custom(
S::NoQualifiedNameInAttributeSelector(t)
| S::InvalidQualNameInAttr(t)
| S::ExpectedBarInAttr(t)
| S::UnexpectedTokenInAttributeSelector(t)
| S::BadValueInAttr(t),
) => ParseErrorKind::InvalidAttributeSelector(token_text(t)),
CssErrorKind::Custom(S::ExpectedNamespace(prefix)) => ParseErrorKind::Other(format!(
"the namespace prefix `{}` is not declared",
elide(sanitize(prefix))
)),
_ => ParseErrorKind::Other("the selector is not valid CSS".to_owned()),
}
}
}
fn token_text(token: &Token<'_>) -> String {
let mut css = String::new();
let _ = token.to_css(&mut css);
elide(sanitize(&css))
}
fn sanitize(text: &str) -> String {
text.chars()
.map(|c| if c.is_control() { '\u{FFFD}' } else { c })
.collect()
}
fn elide(mut text: String) -> String {
if text.len() > MAX_TOKEN_ECHO {
text.truncate(char_boundary(&text, MAX_TOKEN_ECHO));
text.push('…');
}
text
}
fn quote(selector: &str) -> String {
if selector.len() <= MAX_SELECTOR_ECHO {
return format!("{selector:?}");
}
let head = &selector[..char_boundary(selector, MAX_SELECTOR_ECHO)];
let mut quoted = format!("{head:?}");
quoted.pop(); quoted.push('…');
quoted.push('"');
quoted
}
fn gutter(selector: &str, offset: usize) -> (String, String) {
let offset = char_boundary(selector, offset.min(selector.len()));
let (start, end) = line_bounds(selector, offset);
let cells: Vec<(char, usize, bool)> = selector[start..end]
.char_indices()
.map(|(i, c)| {
let (shown, width) = render(c);
(shown, width, start + i < offset)
})
.collect();
let caret_col: usize = cells.iter().filter(|c| c.2).map(|c| c.1).sum();
let total: usize = cells.iter().map(|c| c.1).sum();
let span = total.max(caret_col + 1);
let win_start = if span <= MAX_GUTTER_WIDTH {
0
} else {
(caret_col.saturating_sub(MAX_GUTTER_WIDTH / 2)).min(span - MAX_GUTTER_WIDTH)
};
let win_end = win_start + MAX_GUTTER_WIDTH;
let mut shown = String::new();
let mut shown_start = None;
let mut col = 0;
for &(c, width, _) in &cells {
if col >= win_start && col + width <= win_end {
shown_start.get_or_insert(col);
shown.push(c);
}
col += width;
}
let mut line = String::new();
if win_start > 0 {
line.push('…');
}
line.push_str(&shown);
if total > win_end {
line.push('…');
}
let pad =
caret_col.saturating_sub(shown_start.unwrap_or(win_start)) + usize::from(win_start > 0);
(line, format!("{}^", " ".repeat(pad)))
}
fn render(c: char) -> (char, usize) {
match c {
'\t' => (' ', 1),
c if c.is_control() => ('\u{FFFD}', 1),
c => (c, c.width().unwrap_or(1)),
}
}
fn line_bounds(s: &str, offset: usize) -> (usize, usize) {
let bytes = s.as_bytes();
let start = bytes[..offset]
.iter()
.rposition(|b| matches!(b, b'\n' | b'\r' | b'\x0C'))
.map_or(0, |i| i + 1);
let end = bytes[offset..]
.iter()
.position(|b| matches!(b, b'\n' | b'\r' | b'\x0C'))
.map_or(s.len(), |i| offset + i);
(start, end)
}
fn char_boundary(s: &str, mut offset: usize) -> usize {
while !s.is_char_boundary(offset) {
offset -= 1;
}
offset
}