use std::fmt::Write as _;
use super::{Ctx, Formatter, MAX_FORMAT_DEPTH, Out, SEP};
use crate::ast::{Node, PathSegment, Value};
use crate::grammar::Attribute;
use crate::render::js;
pub(super) fn is_identifier(name: &str) -> bool {
!name.is_empty()
&& name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum AttributeKind {
Attribute,
Class,
}
impl Formatter<'_> {
pub(super) fn value(&mut self, value: &Value, ctx: Ctx, out: &mut Out) {
if self.stack >= MAX_FORMAT_DEPTH {
return;
}
self.stack += 1;
self.value_inner(value, ctx, out);
self.stack -= 1;
}
fn value_inner(&mut self, value: &Value, ctx: Ctx, out: &mut Out) {
match value {
Value::Null => {}
Value::Boolean(boolean) => out.text(boolean.to_string()),
Value::Number(number) => out.text(js::number(*number)),
Value::String(text) => out.text(text.clone()),
Value::Array(items) => {
for item in items {
self.value(item, ctx, out);
}
}
Value::Function(_) | Value::Variable(_) => out.text(self.scalar(value)),
hash @ Value::Hash(_) => out.text(self.scalar(hash)),
}
}
pub(super) fn scalar(&mut self, value: &Value) -> String {
if self.stack >= MAX_FORMAT_DEPTH {
return String::new();
}
self.stack += 1;
let out = self.scalar_inner(value);
self.stack -= 1;
out
}
fn scalar_inner(&mut self, value: &Value) -> String {
match value {
Value::Variable(variable) => variable_literal(variable),
Value::Function(function) => self.function_literal(function),
Value::Null => "null".to_owned(),
Value::Array(items) => {
let items: Vec<String> = items.iter().map(|item| self.scalar(item)).collect();
format!("[{}]", items.join(SEP))
}
Value::Hash(entries) => {
let entries: Vec<String> = entries
.iter()
.map(|(key, value)| {
let key = if is_identifier(key) {
key.clone()
} else {
format!("\"{key}\"")
};
format!("{key}: {}", self.scalar(value))
})
.collect();
format!("{{{}}}", entries.join(SEP))
}
Value::Boolean(boolean) => boolean.to_string(),
Value::Number(number) => json_number(*number),
Value::String(text) => json_string(text),
}
}
fn function_literal(&mut self, function: &crate::ast::Function) -> String {
let parameters: Vec<String> = function
.parameters
.values()
.map(|value| self.scalar(value))
.collect();
format!("{}({})", function.name, parameters.join(SEP))
}
pub(super) fn annotation_value(
&mut self,
kind: AttributeKind,
name: &str,
value: &Value,
) -> String {
let formatted = self.scalar(value);
if name == "primary" {
return formatted;
}
if name == "id"
&& let Value::String(text) = value
&& is_identifier(text)
{
return format!("#{text}");
}
if kind == AttributeKind::Class && is_identifier(name) {
return format!(".{name}");
}
format!("{name}={formatted}")
}
pub(super) fn attributes(&mut self, node: &Node<'_>) -> Vec<String> {
let mut out = Vec::new();
for (key, value) in &node.attributes {
match value {
Value::Hash(classes) if key == "class" => {
for name in classes.keys() {
let name = name.clone();
out.push(self.annotation_value(AttributeKind::Class, &name, value));
}
}
_ => out.push(self.annotation_value(AttributeKind::Attribute, key, value)),
}
}
out
}
pub(super) fn annotations(&mut self, node: &Node<'_>, out: &mut Out) {
if node.annotations.is_empty() {
return;
}
let values: Vec<String> = node
.annotations
.iter()
.map(|annotation| match annotation {
Attribute::Attribute { name, value } => {
self.annotation_value(AttributeKind::Attribute, name, value)
}
Attribute::Class { name } => {
self.annotation_value(AttributeKind::Class, name, &Value::Boolean(true))
}
})
.collect();
out.text(format!("{} ", super::OPEN));
out.text(values.join(super::SPACE));
out.text(format!(" {}", super::CLOSE));
}
}
fn variable_literal(variable: &crate::ast::Variable) -> String {
let mut out = "$".to_owned();
for (index, segment) in variable.path.iter().enumerate() {
match segment {
PathSegment::Key(key) if index == 0 => out.push_str(key),
PathSegment::Index(number) if index == 0 => out.push_str(&js::number(*number)),
PathSegment::Key(key) if is_identifier(key) => {
out.push('.');
out.push_str(key);
}
PathSegment::Key(key) => {
out.push('[');
out.push('"');
out.push_str(key);
out.push('"');
out.push(']');
}
PathSegment::Index(number) => {
out.push('[');
out.push_str(&js::number(*number));
out.push(']');
}
}
}
out
}
fn json_number(number: f64) -> String {
if number.is_finite() {
js::number(number)
} else {
"null".to_owned()
}
}
fn json_string(text: &str) -> String {
let mut out = String::with_capacity(text.len() + 2);
out.push('"');
for character in text.chars() {
match character {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\u{8}' => out.push_str("\\b"),
'\u{c}' => out.push_str("\\f"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
control if control < ' ' => {
let _ = write!(out, "\\u{:04x}", control as u32);
}
other => out.push(other),
}
}
out.push('"');
out
}