use super::cursor::Cursor;
use super::expression::{parse_braced, parse_expression, parse_string};
use super::is_tag_name_byte;
use super::scope::Scope;
use crate::syntax::{Attribute, Expression, LocatedError, Position};
pub(super) struct Attributes<'s> {
pub(super) given: Vec<Attribute>,
pub(super) is_self_closing: bool,
pub(super) deferred: Option<Deferred<'s>>,
}
pub(super) struct Deferred<'s> {
name: String,
position: Position,
value: Cursor<'s>,
}
impl Deferred<'_> {
pub(super) fn parse(mut self, scope: &mut Scope) -> Result<Attribute, LocatedError> {
let value = parse_value(&mut self.value, scope, &self.name, self.position)?;
Ok(Attribute {
name: self.name,
value,
position: self.position,
})
}
}
pub(super) fn parse_attributes(
cursor: &mut Cursor<'_>,
scope: &mut Scope,
tag: &str,
position: Position,
) -> Result<(Vec<Attribute>, bool), LocatedError> {
let attributes = parse_attributes_deferring(cursor, scope, tag, position, None)?;
Ok((attributes.given, attributes.is_self_closing))
}
pub(super) fn parse_attributes_deferring<'s>(
cursor: &mut Cursor<'s>,
scope: &mut Scope,
tag: &str,
position: Position,
deferred_name: Option<&str>,
) -> Result<Attributes<'s>, LocatedError> {
let mut attributes = Attributes {
given: Vec::new(),
is_self_closing: false,
deferred: None,
};
loop {
cursor.skip_whitespace();
if cursor.eat("/>") {
attributes.is_self_closing = true;
return Ok(attributes);
}
if cursor.eat(">") {
return Ok(attributes);
}
if cursor.is_at_end() {
bail!(position, "`<{tag}` is never ended with `>`");
}
let attribute_position = cursor.position();
let name = parse_name(cursor, tag)?;
let is_given = attributes
.given
.iter()
.any(|existing| written_name(&existing.name) == name)
|| attributes
.deferred
.as_ref()
.is_some_and(|deferred| deferred.name == name);
if is_given {
bail!(attribute_position, "`{name}` is given twice on `<{tag}>`");
}
if deferred_name == Some(name) && cursor.eat("=") {
cursor.skip_whitespace();
attributes.deferred = Some(Deferred {
name: name.to_owned(),
position: attribute_position,
value: cursor.clone(),
});
skip_value(cursor, name)?;
continue;
}
let value = if cursor.eat("=") {
cursor.skip_whitespace();
if cursor.eat("{{") {
let fields = parse_fields(cursor, scope, name)?;
attributes.given.extend(fields);
continue;
}
parse_value(cursor, scope, name, attribute_position)?
} else {
Expression::Authored(String::new())
};
attributes.given.push(Attribute {
name: name.to_owned(),
value,
position: attribute_position,
});
}
}
fn parse_name<'s>(cursor: &mut Cursor<'s>, tag: &str) -> Result<&'s str, LocatedError> {
let position = cursor.position();
let name = cursor.take_while(|byte| is_tag_name_byte(byte) || byte == b'_');
if name.is_empty() {
return Err(cursor.unexpected(format_args!(
"expected an attribute name or the end of `<{tag}>`"
)));
}
if cursor.peek() == Some(b':') {
bail!(
position,
"`{name}:` attribute names are not supported; SVG reads a plain `href` and HTML a plain `lang`"
);
}
cursor.skip_whitespace();
Ok(name)
}
fn parse_fields(
cursor: &mut Cursor<'_>,
scope: &mut Scope,
name: &str,
) -> Result<Vec<Attribute>, LocatedError> {
let mut fields: Vec<Attribute> = Vec::new();
loop {
cursor.skip_whitespace();
let position = cursor.position();
let field = cursor.take_identifier();
if field.is_empty() {
return Err(cursor.unexpected(format_args!("expected a field name in `{name}`")));
}
let field_name = format!("{name}.{field}");
if fields.iter().any(|given| given.name == field_name) {
bail!(position, "`{name}` names the field `{field}` twice");
}
cursor.skip_whitespace();
cursor.expect(":", "after the field name")?;
let value = parse_expression(cursor, scope.names())?;
scope.record(&value, position);
fields.push(Attribute {
name: field_name,
value,
position,
});
cursor.skip_whitespace();
if !cursor.eat(",") {
cursor.expect("}}", "after the fields")?;
return Ok(fields);
}
}
}
fn written_name(name: &str) -> &str {
name.split_once('.').map_or(name, |(written, _)| written)
}
fn skip_value(cursor: &mut Cursor<'_>, name: &str) -> Result<(), LocatedError> {
let Some(quote @ (b'"' | b'\'')) = cursor.peek() else {
if cursor.peek() == Some(b'{') {
return skip_braced(cursor);
}
return Err(cursor.unexpected(format_args!(
"expected `\"`, `'` or `{{` to start the value of `{name}`"
)));
};
let start = cursor.position();
cursor.advance();
loop {
cursor.take_while(|byte| byte != quote && byte != b'{');
match cursor.peek() {
Some(byte) if byte == quote => break,
Some(_) => skip_braced(cursor)?,
None => bail!(
start,
"attribute value is never closed with `{}`",
char::from(quote)
),
}
}
cursor.advance();
Ok(())
}
fn skip_braced(cursor: &mut Cursor<'_>) -> Result<(), LocatedError> {
let start = cursor.position();
cursor.advance();
loop {
match cursor.peek() {
Some(b'"') => {
parse_string(cursor)?;
}
Some(b'}') => break,
Some(_) => cursor.advance(),
None => bail!(start, "`{{` is never closed with `}}`"),
}
}
cursor.advance();
Ok(())
}
fn parse_value(
cursor: &mut Cursor<'_>,
scope: &mut Scope,
name: &str,
position: Position,
) -> Result<Expression, LocatedError> {
match cursor.peek() {
Some(quote @ (b'"' | b'\'')) => parse_quoted(cursor, scope, quote),
Some(b'{') => {
cursor.advance();
let value = parse_braced(cursor, scope.names())?;
scope.record(&value, position);
Ok(value)
}
_ => Err(cursor.unexpected(format_args!(
"expected `\"`, `'` or `{{` to start the value of `{name}`"
))),
}
}
fn parse_quoted(
cursor: &mut Cursor<'_>,
scope: &mut Scope,
quote: u8,
) -> Result<Expression, LocatedError> {
let start = cursor.position();
cursor.advance();
let mut parts = Vec::new();
loop {
let text = cursor.take_while(|byte| byte != quote && byte != b'{');
if !text.is_empty() {
parts.push(Expression::Authored(text.replace('"', """)));
}
match cursor.peek() {
Some(byte) if byte == quote => break,
Some(_) => {
let position = cursor.position();
cursor.advance();
let part = parse_braced(cursor, scope.names())?;
scope.record(&part, position);
parts.push(part);
}
None => {
bail!(
start,
"attribute value is never closed with `{}`",
char::from(quote)
);
}
}
}
cursor.advance();
Ok(quoted_value(parts))
}
fn quoted_value(mut parts: Vec<Expression>) -> Expression {
match parts.as_slice() {
[] => Expression::Authored(String::new()),
[Expression::Authored(_)] => parts.remove(0),
_ => Expression::Interpolated(parts),
}
}