use std::sync::Arc;
use indexmap::IndexMap;
use crate::ast::{NodeType, Value};
use crate::renderable::{RenderableTreeNode, RenderableTreeNodes, Scalar, Tag};
use crate::transform;
use crate::validate::{RenderPolicy, Schema, SchemaAttribute, ValidationType};
#[must_use]
pub fn builtin() -> IndexMap<NodeType, Schema> {
let mut nodes = IndexMap::new();
nodes.insert(NodeType::Document, document());
nodes.insert(NodeType::Heading, heading());
nodes.insert(NodeType::Paragraph, element("p", &[NodeType::Inline]));
nodes.insert(NodeType::Image, image());
nodes.insert(NodeType::Fence, fence());
nodes.insert(NodeType::Blockquote, element("blockquote", NESTED_BLOCKS));
nodes.insert(NodeType::Item, element("li", CELL_CHILDREN));
nodes.insert(NodeType::List, list());
nodes.insert(NodeType::Hr, Schema::new().render("hr"));
nodes.insert(NodeType::Table, Schema::new().render("table"));
nodes.insert(NodeType::Td, td());
nodes.insert(NodeType::Th, th());
nodes.insert(NodeType::Tr, element("tr", &[NodeType::Th, NodeType::Td]));
nodes.insert(
NodeType::Tbody,
element("tbody", &[NodeType::Tr, NodeType::Tag]),
);
nodes.insert(NodeType::Thead, element("thead", &[NodeType::Tr]));
nodes.insert(
NodeType::Strong,
marked(
"strong",
&[
NodeType::Em,
NodeType::S,
NodeType::Link,
NodeType::Code,
NodeType::Text,
NodeType::Tag,
],
),
);
nodes.insert(
NodeType::Em,
marked(
"em",
&[
NodeType::Strong,
NodeType::S,
NodeType::Link,
NodeType::Code,
NodeType::Text,
NodeType::Tag,
],
),
);
nodes.insert(
NodeType::S,
element(
"s",
&[
NodeType::Strong,
NodeType::Em,
NodeType::Link,
NodeType::Code,
NodeType::Text,
NodeType::Tag,
],
),
);
nodes.insert(NodeType::Inline, inline());
nodes.insert(NodeType::Link, link());
nodes.insert(NodeType::Code, code());
nodes.insert(NodeType::Text, text());
nodes.insert(NodeType::Hardbreak, Schema::new().render("br"));
nodes.insert(NodeType::Softbreak, softbreak());
nodes.insert(NodeType::Comment, comment());
nodes.insert(NodeType::Error, Schema::new());
nodes.insert(NodeType::Node, Schema::new());
nodes
}
const TOP_LEVEL_BLOCKS: &[NodeType] = &[
NodeType::Heading,
NodeType::Paragraph,
NodeType::Image,
NodeType::Table,
NodeType::Tag,
NodeType::Fence,
NodeType::Blockquote,
NodeType::Comment,
NodeType::List,
NodeType::Hr,
];
const NESTED_BLOCKS: &[NodeType] = &[
NodeType::Heading,
NodeType::Paragraph,
NodeType::Image,
NodeType::Table,
NodeType::Tag,
NodeType::Fence,
NodeType::Blockquote,
NodeType::List,
NodeType::Hr,
];
const CELL_CHILDREN: &[NodeType] = &[
NodeType::Inline,
NodeType::Heading,
NodeType::Paragraph,
NodeType::Image,
NodeType::Table,
NodeType::Tag,
NodeType::Fence,
NodeType::Blockquote,
NodeType::List,
NodeType::Hr,
];
fn element(render: &str, children: &[NodeType]) -> Schema {
let mut schema = Schema::new().render(render);
schema.children = Some(children.to_vec());
schema
}
fn marked(render: &str, children: &[NodeType]) -> Schema {
element(render, children).attribute("marker", hidden(ValidationType::String))
}
fn typed(kind: ValidationType) -> SchemaAttribute {
SchemaAttribute {
attribute_type: Some(kind),
..SchemaAttribute::default()
}
}
fn hidden(kind: ValidationType) -> SchemaAttribute {
SchemaAttribute {
attribute_type: Some(kind),
render: RenderPolicy::Hidden,
..SchemaAttribute::default()
}
}
fn renamed(kind: ValidationType, name: &str) -> SchemaAttribute {
SchemaAttribute {
attribute_type: Some(kind),
render: RenderPolicy::Renamed(name.to_string()),
..SchemaAttribute::default()
}
}
fn required(mut attribute: SchemaAttribute) -> SchemaAttribute {
attribute.required = true;
attribute
}
fn document() -> Schema {
let mut schema = element("article", TOP_LEVEL_BLOCKS);
schema.attributes.insert(
"frontmatter".to_string(),
SchemaAttribute {
render: RenderPolicy::Hidden,
..SchemaAttribute::default()
},
);
schema
}
fn heading() -> Schema {
let mut schema = Schema::new().attribute("level", required(hidden(ValidationType::Number)));
schema.children = Some(vec![NodeType::Inline]);
schema.transform = Some(Arc::new(|node, config| {
let level = match node.get("level") {
Some(Value::Number(level)) => js_integer(*level),
_ => "1".to_string(),
};
one(Tag::with(
format!("h{level}"),
transform::attributes(node, config),
transform::children(node, config),
))
}));
schema
}
fn js_integer(number: f64) -> String {
if number.fract() == 0.0 && number.is_finite() {
#[allow(
clippy::cast_possible_truncation,
reason = "integral and finite, checked immediately above"
)]
return (number as i64).to_string();
}
number.to_string()
}
fn image() -> Schema {
Schema::new()
.render("img")
.attribute("src", required(typed(ValidationType::String)))
.attribute("alt", typed(ValidationType::String))
.attribute("title", typed(ValidationType::String))
}
fn fence() -> Schema {
let mut schema = Schema::new()
.render("pre")
.attribute("content", required(hidden(ValidationType::String)))
.attribute("language", renamed(ValidationType::String, "data-language"))
.attribute("process", {
let mut process = hidden(ValidationType::Boolean);
process.default = Some(Value::Boolean(true));
process
});
schema.transform = Some(Arc::new(|node, config| {
let attributes = transform::attributes(node, config);
let children = if node.children.is_empty() {
content_child(node, config)
} else {
transform::children(node, config)
};
one(Tag::with("pre", attributes, children))
}));
schema
}
fn content_child<'a>(
node: &'a crate::ast::Node<'a>,
config: &crate::validate::Config<'a>,
) -> Vec<RenderableTreeNode> {
node.get("content")
.and_then(|content| transform::resolve(content, config))
.as_ref()
.and_then(Scalar::from_value)
.map(|content| vec![RenderableTreeNode::Scalar(content)])
.unwrap_or_default()
}
fn list() -> Schema {
let mut schema = Schema::new()
.attribute("ordered", required(hidden(ValidationType::Boolean)))
.attribute("start", typed(ValidationType::Number))
.attribute("marker", hidden(ValidationType::String));
schema.children = Some(vec![NodeType::Item]);
schema.transform = Some(Arc::new(|node, config| {
let ordered = matches!(node.get("ordered"), Some(Value::Boolean(true)));
one(Tag::with(
if ordered { "ol" } else { "ul" },
transform::attributes(node, config),
transform::children(node, config),
))
}));
schema
}
fn td() -> Schema {
element("td", CELL_CHILDREN)
.attribute("align", typed(ValidationType::String))
.attribute("colspan", renamed(ValidationType::Number, "colSpan"))
.attribute("rowspan", renamed(ValidationType::Number, "rowSpan"))
}
fn th() -> Schema {
Schema::new()
.render("th")
.attribute("width", typed(ValidationType::String))
.attribute("align", typed(ValidationType::String))
.attribute("colspan", renamed(ValidationType::Number, "colSpan"))
.attribute("rowspan", renamed(ValidationType::Number, "rowSpan"))
}
fn inline() -> Schema {
let mut schema = Schema::new();
schema.children = Some(vec![
NodeType::Strong,
NodeType::Em,
NodeType::S,
NodeType::Code,
NodeType::Text,
NodeType::Tag,
NodeType::Link,
NodeType::Image,
NodeType::Hardbreak,
NodeType::Softbreak,
NodeType::Comment,
]);
schema
}
fn link() -> Schema {
element(
"a",
&[
NodeType::Strong,
NodeType::Em,
NodeType::S,
NodeType::Code,
NodeType::Text,
NodeType::Tag,
],
)
.attribute("href", required(typed(ValidationType::String)))
.attribute("title", typed(ValidationType::String))
}
fn code() -> Schema {
let mut schema = Schema::new()
.render("code")
.attribute("content", required(hidden(ValidationType::String)));
schema.transform = Some(Arc::new(|node, config| {
let attributes = transform::attributes(node, config);
one(Tag::with("code", attributes, content_child(node, config)))
}));
schema
}
fn text() -> Schema {
let mut schema = Schema::new().attribute("content", required(typed(ValidationType::String)));
schema.transform = Some(Arc::new(|node, config| {
transform::scalar(
node.get("content")
.and_then(|content| transform::resolve(content, config))
.as_ref()
.and_then(Scalar::from_value),
)
}));
schema
}
fn softbreak() -> Schema {
let mut schema = Schema::new();
schema.transform = Some(Arc::new(|_node, _config| {
RenderableTreeNodes::One(RenderableTreeNode::text(" "))
}));
schema
}
fn comment() -> Schema {
Schema::new().attribute("content", required(typed(ValidationType::String)))
}
fn one(tag: Tag) -> RenderableTreeNodes {
RenderableTreeNodes::One(RenderableTreeNode::tag(tag))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_node_type_the_parser_produces_has_a_schema() {
let nodes = builtin();
for node_type in [
NodeType::Blockquote,
NodeType::Code,
NodeType::Comment,
NodeType::Document,
NodeType::Em,
NodeType::Error,
NodeType::Fence,
NodeType::Hardbreak,
NodeType::Heading,
NodeType::Hr,
NodeType::Image,
NodeType::Inline,
NodeType::Item,
NodeType::Link,
NodeType::List,
NodeType::Node,
NodeType::Paragraph,
NodeType::S,
NodeType::Softbreak,
NodeType::Strong,
NodeType::Table,
NodeType::Tbody,
NodeType::Td,
NodeType::Text,
NodeType::Th,
NodeType::Thead,
NodeType::Tr,
] {
assert!(nodes.contains_key(&node_type), "no schema for {node_type}");
}
assert!(!nodes.contains_key(&NodeType::Tag));
}
#[test]
fn a_heading_level_prints_as_an_integer() {
assert_eq!(js_integer(1.0), "1");
assert_eq!(js_integer(6.0), "6");
}
}