use {super::Node, std::fmt::Write};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ast {
inner: Vec<Node>,
}
impl Ast {
#[must_use]
pub fn from_owned(value: &(impl AsRef<str> + ?Sized)) -> Self {
Self::from_value(value.as_ref())
}
#[must_use]
#[allow(clippy::needless_pass_by_value)]
pub fn from_string(value: impl Into<String>) -> Self {
Self::from_value(&value.into())
}
#[must_use]
pub fn from_value(value: &(impl ToString + ?Sized)) -> Self {
let mut ast = vec![];
let mut in_preformatted = false;
let mut in_list = false;
let source = value.to_string();
let mut lines = source.lines();
while let Some(line) = lines.next() {
ast.append(&mut Self::evaluate(
line,
&mut lines,
&mut in_preformatted,
&mut in_list,
));
}
if source.ends_with('\n') {
if let Some(last) = ast.last() {
if !matches!(last, Node::Whitespace) {
ast.push(Node::Whitespace);
}
}
}
Self { inner: ast }
}
#[must_use]
pub const fn from_nodes(nodes: Vec<Node>) -> Self { Self { inner: nodes } }
#[must_use]
pub fn to_gemtext(&self) -> String {
let mut gemtext = String::new();
for node in &self.inner {
match node {
Node::Text(text) => {
let _ = writeln!(&mut gemtext, "{text}");
}
Node::Link { to, text } => {
let _ = writeln!(
&mut gemtext,
"=> {}{}",
to,
text.clone().map_or_else(String::new, |text| format!(" {text}")),
);
}
Node::Heading { level, text } => {
let _ = writeln!(&mut gemtext, "{} {}", "#".repeat(*level), text);
}
Node::List(items) => {
let _ = writeln!(
&mut gemtext,
"{}",
items
.iter()
.map(|i| format!("* {i}"))
.collect::<Vec<String>>()
.join("\n"),
);
}
Node::Blockquote(text) => {
let _ = writeln!(&mut gemtext, "> {text}");
}
Node::PreformattedText { alt_text, text } => {
let _ = writeln!(
&mut gemtext,
"```{}\n{}```",
alt_text.clone().unwrap_or_default(),
text
);
}
Node::Whitespace => gemtext.push('\n'),
}
}
if gemtext.ends_with('\n') {
gemtext.pop();
}
gemtext
}
#[must_use]
pub const fn inner(&self) -> &Vec<Node> { &self.inner }
#[allow(clippy::too_many_lines)]
fn evaluate(
line: &str,
lines: &mut std::str::Lines<'_>,
in_preformatted: &mut bool,
in_list: &mut bool,
) -> Vec<Node> {
let mut preformatted = String::new();
let mut alt_text = String::new();
let mut nodes = vec![];
let mut line = line;
let mut list_items = vec![];
loop {
match line.get(0..1).unwrap_or("") {
"=" if !*in_preformatted => {
let line = line.get(2..).unwrap_or("");
let mut split = line.split_whitespace();
nodes.push(Node::Link {
to: split.next().unwrap_or_default().to_string(),
text: {
let rest: Vec<&str> = split.collect();
if rest.is_empty() { None } else { Some(rest.join(" ")) }
},
});
break;
}
"#" if !*in_preformatted => {
let level =
line.trim_start().chars().take_while(|&c| c == '#').count();
nodes.push(Node::Heading {
level,
text: line
.chars()
.skip(level)
.collect::<String>()
.trim_start()
.to_string(),
});
break;
}
"*" if !*in_preformatted => {
if !*in_list {
*in_list = true;
}
list_items.push(line.get(1..).unwrap_or("").trim_start().to_string());
if let Some(next_line) = lines.next() {
line = next_line;
} else {
break;
}
}
">" if !*in_preformatted => {
nodes.push(Node::Blockquote(
line.get(1..).unwrap_or("").trim_start().to_string(),
));
break;
}
"`" => {
*in_preformatted = !*in_preformatted;
if *in_preformatted {
alt_text = line.get(3..).unwrap_or("").to_string();
if let Some(next_line) = lines.next() {
line = next_line;
} else {
break;
}
} else {
nodes.push(Node::PreformattedText {
alt_text: if alt_text.is_empty() { None } else { Some(alt_text) },
text: preformatted,
});
break;
}
}
"" if !*in_preformatted => {
if line.is_empty() {
nodes.push(Node::Whitespace);
} else {
nodes.push(Node::Text(line.to_string()));
}
break;
}
_ => {
if *in_preformatted {
let _ = writeln!(&mut preformatted, "{line}");
if let Some(next_line) = lines.next() {
line = next_line;
} else {
break;
}
} else {
if *in_list {
*in_list = false;
nodes.push(Node::Text(line.to_string()));
break;
}
nodes.push(Node::Text(line.to_string()));
break;
}
}
}
}
if !list_items.is_empty() {
nodes.reverse();
nodes.push(Node::List(list_items));
nodes.reverse();
}
nodes
}
}