#![allow(
clippy::panic,
reason = "a harness that cannot grade a case must say so, not grade it wrongly"
)]
use accent_proust::ast::Node;
use accent_proust::parse::{ParseOptions, PulldownTokenizer, parse_with};
use accent_proust::renderable::{RenderableTreeNode, RenderableTreeNodes, Scalar};
use accent_proust::validate::validate_tree;
use crate::config;
use crate::corpus::{Case, Renderer};
use crate::value::Value;
#[derive(Debug)]
pub enum Outcome {
Tree {
children: Vec<Value>,
validation: Vec<String>,
},
Html(String),
ValidationErrors(String),
}
#[derive(Debug)]
pub struct Unimplemented {
pub stage: &'static str,
pub phase: &'static str,
}
impl std::fmt::Display for Unimplemented {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} is not implemented (phase {})",
self.stage, self.phase
)
}
}
pub fn run(case: &Case) -> Result<Outcome, Unimplemented> {
let options = ParseOptions::new().allow_comments(true).slots(case.slots);
let document = parse_with(&case.code, &PulldownTokenizer::new(), &options);
if case.expected_error.is_some() {
let messages = parse_errors(&document);
if !messages.is_empty() {
return Ok(Outcome::ValidationErrors(messages.join("\n")));
}
let config = config::build(case).unwrap_or_default();
let found = validate_tree(&document, &config);
if found
.iter()
.any(|found| matches!(found.error.id, "node-undefined" | "tag-undefined"))
{
return Err(Unimplemented {
stage: "the built-in node and tag schemas",
phase: "D",
});
}
return Ok(Outcome::ValidationErrors(
found
.iter()
.map(|found| found.error.message.clone())
.collect::<Vec<_>>()
.join("\n"),
));
}
let config = match config::build(case) {
Ok(config) => config,
Err(e) => panic!(
"{}: its config block could not be translated: {e}",
case.name
),
};
let transformed = accent_proust::transform::transform(&document, &config);
match case.renderer {
Renderer::Html => Ok(Outcome::Html(accent_proust::render::render_all(
&transformed.into_vec(),
))),
Renderer::Tree => Ok(Outcome::Tree {
children: article_children(transformed),
validation: parse_errors(&document),
}),
}
}
fn article_children(nodes: RenderableTreeNodes) -> Vec<Value> {
match react(nodes).get("children") {
Some(Value::Seq(children)) => children.clone(),
_ => Vec::new(),
}
}
fn react(nodes: RenderableTreeNodes) -> Value {
match nodes {
RenderableTreeNodes::One(node) => react_node(node),
RenderableTreeNodes::Many(nodes) => {
let children: Vec<Value> = nodes.into_iter().map(react_node).collect();
let mut out = vec![("tag".to_string(), Value::Str("Fragment".to_string()))];
if !children.is_empty() {
out.push(("children".to_string(), Value::Seq(children)));
}
Value::Map(out)
}
other => panic!("the renderable tree grew a shape this harness cannot grade: {other:?}"),
}
}
fn react_attribute(nodes: RenderableTreeNodes) -> Value {
match nodes {
RenderableTreeNodes::One(node) => react_node(node),
RenderableTreeNodes::Many(nodes) => Value::Seq(nodes.into_iter().map(react_node).collect()),
other => panic!("the renderable tree grew a shape this harness cannot grade: {other:?}"),
}
}
fn react_node(node: RenderableTreeNode) -> Value {
let mut tag = match node {
RenderableTreeNode::Scalar(value) => return scalar(&value),
RenderableTreeNode::Tag(tag) => *tag,
other => {
panic!("the renderable tree grew a shape this harness cannot grade: {other:?}")
}
};
let mut attributes: Vec<(String, Value)> = Vec::new();
let mut class_name: Option<Value> = None;
for (key, value) in std::mem::take(&mut tag.attributes) {
if key == "class" {
let rendered = react_attribute(value);
if truthy(&rendered) {
class_name = Some(rendered);
}
continue;
}
attributes.push((key, react_attribute(value)));
}
if let Some(class_name) = class_name {
attributes.push(("className".to_string(), class_name));
}
let children: Vec<Value> = std::mem::take(&mut tag.children)
.into_iter()
.map(react_node)
.collect();
let mut out = vec![("tag".to_string(), Value::Str(std::mem::take(&mut tag.name)))];
if !attributes.is_empty() {
out.push(("attributes".to_string(), Value::Map(attributes)));
}
if !children.is_empty() {
out.push(("children".to_string(), Value::Seq(children)));
}
Value::Map(out)
}
fn truthy(value: &Value) -> bool {
match value {
Value::Null => false,
Value::Bool(boolean) => *boolean,
Value::Int(number) => *number != 0,
Value::Float(number) => *number != 0.0 && !number.is_nan(),
Value::Str(text) => !text.is_empty(),
Value::Seq(_) | Value::Map(_) => true,
}
}
fn scalar(value: &Scalar) -> Value {
match value {
Scalar::Null => Value::Null,
Scalar::Boolean(boolean) => Value::Bool(*boolean),
Scalar::Number(number) => Value::Float(*number),
Scalar::String(text) => Value::Str(text.clone()),
Scalar::Array(items) => Value::Seq(items.iter().map(scalar).collect()),
Scalar::Object(entries) => Value::Map(
entries
.iter()
.map(|(key, value)| (key.clone(), scalar(value)))
.collect(),
),
other => panic!("`Scalar` grew a variant this harness cannot grade: {other:?}"),
}
}
fn parse_errors(document: &Node<'_>) -> Vec<String> {
let mut out: Vec<String> = document
.errors
.iter()
.map(|error| error.message.clone())
.collect();
for node in document.walk() {
out.extend(node.errors.iter().map(|error| error.message.clone()));
}
out
}