#![allow(dead_code)]
pub mod packet;
pub mod pie;
pub mod quadrant;
pub mod radar;
pub mod sankey;
pub mod treemap;
pub mod xychart;
#[cfg(test)]
pub mod tests;
use std::fmt;
use crate::preview::mermaid::flowchart::preprocess::preprocess;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
NotThisChart {
expected: &'static str,
header: String,
},
Empty {
kind: &'static str,
},
NoData {
kind: &'static str,
wanted: &'static str,
},
Unexpected {
kind: &'static str,
line: usize,
text: String,
},
BadNumber {
line: usize,
text: String,
why: &'static str,
},
Invalid {
line: usize,
message: String,
},
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::NotThisChart { expected, header } => {
if header.is_empty() {
write!(f, "not a {expected}: the source is empty")
} else {
write!(f, "not a {expected}: the source starts with `{header}`")
}
}
ParseError::Empty { kind } => write!(f, "{kind} has no content"),
ParseError::NoData { kind, wanted } => {
write!(f, "{kind} declares no {wanted}")
}
ParseError::Unexpected { kind, line, text } => {
write!(f, "{kind}: unexpected `{text}` at line {line}")
}
ParseError::BadNumber { line, text, why } => {
write!(f, "`{text}` at line {line}: {why}")
}
ParseError::Invalid { line: 0, message } => write!(f, "{message}"),
ParseError::Invalid { line, message } => write!(f, "{message} at line {line}"),
}
}
}
impl std::error::Error for ParseError {}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Preamble {
pub title: Option<String>,
pub acc_title: Option<String>,
pub acc_descr: Option<String>,
}
pub struct Source {
pub lines: Vec<String>,
pub header_index: usize,
pub header_rest: String,
pub front_matter_title: Option<String>,
}
pub fn find_header(src: &str, keywords: &[&str]) -> Option<Source> {
let pre = preprocess(src);
let lines: Vec<String> = pre.text.split('\n').map(str::to_string).collect();
for (i, line) in lines.iter().enumerate() {
if line.trim().is_empty() {
continue;
}
let t = line.trim_start();
for kw in keywords {
if starts_ci(t, kw) {
let rest = &t[kw.len()..];
if rest.chars().next().is_some_and(is_word_char) {
continue;
}
return Some(Source {
header_index: i,
header_rest: rest.trim().to_string(),
lines,
front_matter_title: pre.title,
});
}
}
return None;
}
None
}
pub fn first_word(src: &str) -> String {
let pre = preprocess(src);
pre.text
.split('\n')
.map(str::trim)
.find(|l| !l.is_empty())
.unwrap_or("")
.split_whitespace()
.next()
.unwrap_or("")
.to_string()
}
pub fn starts_ci(haystack: &str, needle: &str) -> bool {
let (h, n) = (haystack.as_bytes(), needle.as_bytes());
h.len() >= n.len() && h[..n.len()].eq_ignore_ascii_case(n)
}
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_' || c == '-'
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TitleSyntax {
Terminal,
RawRestOfLine,
Text,
}
pub struct Envelope {
pub preamble: Preamble,
in_acc_descr_block: bool,
body_title: bool,
}
impl Envelope {
pub fn new(front_matter_title: Option<String>) -> Envelope {
Envelope {
preamble: Preamble {
title: front_matter_title,
..Preamble::default()
},
in_acc_descr_block: false,
body_title: false,
}
}
pub fn read(&mut self, line: &str, title: TitleSyntax) -> bool {
if self.in_acc_descr_block {
if let Some((body, _)) = line.split_once('}') {
self.push_descr(body);
self.in_acc_descr_block = false;
} else {
self.push_descr(line);
}
return true;
}
let t = line.trim();
if let Some(rest) = strip_ci(t, "accDescr") {
let r = rest.trim_start();
if let Some(body) = r.strip_prefix('{') {
match body.split_once('}') {
Some((inner, _)) => self.push_descr(inner),
None => {
self.push_descr(body);
self.in_acc_descr_block = true;
}
}
return true;
}
if let Some(body) = r.strip_prefix(':') {
self.push_descr(body);
return true;
}
}
if let Some(rest) = strip_ci(t, "accTitle") {
if let Some(body) = rest.trim_start().strip_prefix(':') {
self.preamble.acc_title = Some(body.trim().to_string());
return true;
}
}
if let Some(rest) = strip_ci(t, "title") {
let is_title = rest.is_empty()
|| rest.starts_with([' ', '\t'])
|| (title != TitleSyntax::Terminal && rest.starts_with(':'));
if is_title {
let raw = rest.trim_start_matches([':', ' ', '\t']).trim_end();
let text = match title {
TitleSyntax::Text => {
let chars: Vec<char> = raw.chars().collect();
match read_quoted(&chars, 0) {
Some((inner, _)) => inner,
None => raw.to_string(),
}
}
_ => raw.to_string(),
};
self.preamble.title = if text.is_empty() { None } else { Some(text) };
self.body_title = true;
return true;
}
}
false
}
fn push_descr(&mut self, text: &str) {
let text = text.trim();
match &mut self.preamble.acc_descr {
Some(existing) if !text.is_empty() => {
existing.push('\n');
existing.push_str(text);
}
Some(_) => {}
None => self.preamble.acc_descr = Some(text.to_string()),
}
}
}
pub fn strip_ci<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
starts_ci(s, prefix).then(|| &s[prefix.len()..])
}
pub fn read_quoted(chars: &[char], i: usize) -> Option<(String, usize)> {
let quote = *chars.get(i)?;
if quote != '"' && quote != '\'' {
return None;
}
let mut out = String::new();
let mut j = i + 1;
while j < chars.len() {
match chars[j] {
'\\' if j + 1 < chars.len() => {
out.push(chars[j + 1]);
j += 2;
}
c if c == quote => return Some((out, j + 1)),
c => {
out.push(c);
j += 1;
}
}
}
None
}
pub fn parse_number(text: &str, allow_sign: bool) -> Option<f64> {
let mut body = text;
let mut negative = false;
if allow_sign {
if let Some(rest) = body.strip_prefix('-') {
negative = true;
body = rest;
} else if let Some(rest) = body.strip_prefix('+') {
body = rest;
}
}
if body.is_empty() || !body.bytes().all(|b| b.is_ascii_digit() || b == b'.') {
return None;
}
if body.bytes().filter(|b| *b == b'.').count() > 1 {
return None;
}
if body == "." {
return None;
}
let v: f64 = body.parse().ok()?;
if !v.is_finite() {
return None;
}
Some(if negative { -v } else { v })
}