//! Markdown as an HTML page or plain text (MARK-11).
mod files;
mod pictures;
mod plain;
#[cfg(test)]
mod tests;
use std::{io, path::Path};
use ely_gpui_component::{
documents::{diagram_svg, markdown_options},
typography::formula_svg,
};
use markdown::{CompileOptions, Options, mdast::Node};
pub use files::{Format, Target, write};
pub use pictures::Pictures;
pub use plain::plain;
/// A page and what it could not carry.
#[derive(Debug, Default)]
pub struct Page {
pub html: String,
/// Pictures to copy beside the page: from, then name.
pub copies: Vec<(std::path::PathBuf, String)>,
/// Pictures left out, as the document wrote them.
pub missing: Vec,
/// Lines of diagrams kept as code.
pub diagrams: Vec,
/// Lines of formulas kept as code, and why.
pub formulas: Vec<(usize, String)>,
}
const DISPLAY: &str = "
";
const IMAGE: &str = ", pictures: &Pictures) -> io::Result {
let options = Options {
parse: markdown_options(),
compile: CompileOptions::gfm(),
};
let html = markdown::to_html_with_options(text, &options)
.map_err(|error| io::Error::other(error.to_string()))?;
let tree = markdown::to_mdast(text, &options.parse)
.map_err(|error| io::Error::other(error.to_string()))?;
let mut page = Page::default();
let mut placed = pictures::Placed::new(beside, pictures);
let mut rest = html.as_str();
while let Some((at, mark)) = [DISPLAY, INLINE, MERMAID, IMAGE]
.iter()
.filter_map(|mark| rest.find(mark).map(|at| (at, *mark)))
.min()
{
page.html.push_str(&rest[..at]);
rest = &rest[at + mark.len()..];
let close = match mark {
DISPLAY | MERMAID => "
",
INLINE => "",
_ => "\"",
};
let end = rest
.find(close)
.ok_or_else(|| io::Error::other("markdown-rs left an element open"))?;
let inside = &rest[..end];
rest = &rest[end + close.len()..];
let source = unescape(inside);
if mark == IMAGE {
page.html.push_str(IMAGE);
page.html
.push_str(&placed.place(&source, rest, &mut page.missing));
page.html.push('"');
continue;
}
let drawn = match mark {
MERMAID => match diagram_svg(&source) {
Ok(svg) => Some(format!("
{}
", bare(&svg))),
Err(_) => {
page.diagrams.push(line(&tree, &source, true));
None
}
},
_ => match formula_svg(&source, mark == DISPLAY) {
Ok(svg) if mark == DISPLAY => {
Some(format!("
{}
", bare(&svg)))
}
Ok(svg) => Some(format!("{}", bare(&svg))),
Err(error) => {
page.formulas
.push((line(&tree, &source, false), error.to_string()));
None
}
},
};
// What did not draw stays as written.
page.html += &drawn.unwrap_or_else(|| format!("{mark}{inside}{close}"));
}
page.html.push_str(rest);
page.copies = placed.copies;
log::info!(
"mark: a page of {} bytes, {} copies, {} missing, {} diagrams and {} formulas as code",
page.html.len(),
page.copies.len(),
page.missing.len(),
page.diagrams.len(),
page.formulas.len()
);
Ok(page)
}
/// A whole HTML document around `body`.
pub fn document(title: &str, body: &str) -> String {
let title = title.replace('&', "&").replace('<', "<");
format!(
"\n{title}\n\n{body}\n"
)
}
/// markdown-rs's four escapes undone.
fn unescape(text: &str) -> String {
text.replace("<", "<")
.replace(">", ">")
.replace(""", "\"")
.replace("&", "&")
}
/// An SVG without its XML declaration.
fn bare(svg: &str) -> &str {
svg.find("