use crate::MermaidConfig;
use super::{
NODE_TYPE_BANG, NODE_TYPE_CIRCLE, NODE_TYPE_CLOUD, NODE_TYPE_DEFAULT, NODE_TYPE_HEXAGON,
NODE_TYPE_RECT, NODE_TYPE_ROUNDED_RECT,
};
pub(super) fn starts_with_case_insensitive(haystack: &str, needle: &str) -> bool {
if haystack.len() < needle.len() {
return false;
}
haystack
.as_bytes()
.iter()
.take(needle.len())
.copied()
.map(|b| b.to_ascii_lowercase())
.eq(needle
.as_bytes()
.iter()
.copied()
.map(|b| b.to_ascii_lowercase()))
}
pub(super) fn split_indent(line: &str) -> (usize, &str) {
let mut indent_chars = 0usize;
let mut byte_idx = line.len();
for (idx, ch) in line.char_indices() {
if ch.is_whitespace() {
indent_chars += 1;
continue;
}
byte_idx = idx;
break;
}
if indent_chars == 0 {
byte_idx = 0;
} else if byte_idx == line.len() {
byte_idx = line.len();
}
(indent_chars, &line[byte_idx..])
}
pub(super) fn strip_inline_comment(line: &str) -> &str {
let mut in_quote = false;
let mut in_backtick_quote = false;
let mut it = line.char_indices().peekable();
while let Some((idx, ch)) = it.next() {
if in_backtick_quote {
if ch == '`' && it.peek().is_some_and(|(_, next)| *next == '"') {
in_backtick_quote = false;
it.next();
}
continue;
}
if in_quote {
if ch == '"' {
in_quote = false;
}
continue;
}
if ch == '"' {
if it.peek().is_some_and(|(_, next)| *next == '`') {
in_backtick_quote = true;
it.next();
continue;
}
in_quote = true;
continue;
}
if ch == '%' && it.peek().is_some_and(|(_, next)| *next == '%') {
return &line[..idx];
}
}
line
}
pub(super) fn parse_node_spec(
input: &str,
) -> std::result::Result<(String, String, i32, bool), String> {
let input = input.trim_end();
if input.is_empty() {
return Err("expected node".to_string());
}
if let Some((start, end)) = node_delimiter_pair_at_start(input) {
let (inner, tail) = extract_delimited(input, start, end)?;
if !tail.trim().is_empty() {
return Err("unexpected trailing input".to_string());
}
let (descr, descr_is_markdown) = unquote_node_descr(inner);
let ty = node_type_for(start, end);
return Ok((descr.clone(), descr, ty, descr_is_markdown));
}
let (id_raw, rest) = split_node_id(input);
let id_raw = id_raw.to_string();
let rest = rest.trim_end();
if rest.is_empty() {
return Ok((id_raw.clone(), id_raw, NODE_TYPE_DEFAULT, false));
}
let Some((start, end)) = node_delimiter_pair_at_start(rest) else {
return Err("expected node delimiter".to_string());
};
let (inner, tail) = extract_delimited(rest, start, end)?;
if !tail.trim().is_empty() {
return Err("unexpected trailing input".to_string());
}
let (descr, descr_is_markdown) = unquote_node_descr(inner);
let ty = node_type_for(start, end);
Ok((id_raw, descr, ty, descr_is_markdown))
}
fn split_node_id(input: &str) -> (&str, &str) {
let bytes = input.as_bytes();
for (idx, b) in bytes.iter().enumerate() {
match b {
b'(' | b')' | b'[' | b'{' | b'}' => return (&input[..idx], &input[idx..]),
_ => {}
}
}
(input, "")
}
fn node_delimiter_pair_at_start(input: &str) -> Option<(&'static str, &'static str)> {
let pairs: &[(&str, &str)] = &[
("(-", "-)"),
("-)", "(-"),
("((", "))"),
("))", "(("),
("{{", "}}"),
("[", "]"),
(")", "("),
("(", ")"),
];
for (start, end) in pairs {
if input.starts_with(start) {
return Some((*start, *end));
}
}
None
}
fn extract_delimited<'a>(
input: &'a str,
start: &str,
end: &str,
) -> std::result::Result<(&'a str, &'a str), String> {
if !input.starts_with(start) {
return Err("expected delimiter start".to_string());
}
let mut in_quote = false;
let mut in_backtick_quote = false;
let start_len = start.len();
let mut it = input[start_len..].char_indices().peekable();
while let Some((off, ch)) = it.next() {
let idx = start_len + off;
if in_backtick_quote {
if ch == '`' && it.peek().is_some_and(|(_, next)| *next == '"') {
in_backtick_quote = false;
it.next();
}
continue;
}
if in_quote {
if ch == '"' {
in_quote = false;
}
continue;
}
if ch == '"' {
if it.peek().is_some_and(|(_, next)| *next == '`') {
in_backtick_quote = true;
it.next();
continue;
}
in_quote = true;
continue;
}
if input[idx..].starts_with(end) {
let inner = &input[start_len..idx];
let tail = &input[idx + end.len()..];
return Ok((inner, tail));
}
}
Err("unterminated node delimiter".to_string())
}
fn unquote_node_descr(raw: &str) -> (String, bool) {
if let Some(inner) = raw.strip_prefix("\"`").and_then(|s| s.strip_suffix("`\"")) {
return (inner.to_string(), true);
}
if let Some(inner) = raw.strip_prefix('\"').and_then(|s| s.strip_suffix('\"')) {
return (inner.to_string(), false);
}
(raw.to_string(), false)
}
fn node_type_for(start: &str, end: &str) -> i32 {
match start {
"[" => NODE_TYPE_RECT,
"(" => {
if end == ")" {
NODE_TYPE_ROUNDED_RECT
} else {
NODE_TYPE_CLOUD
}
}
"((" => NODE_TYPE_CIRCLE,
")" => NODE_TYPE_CLOUD,
"))" => NODE_TYPE_BANG,
"{{" => NODE_TYPE_HEXAGON,
_ => NODE_TYPE_DEFAULT,
}
}
pub(super) fn get_i64(cfg: &MermaidConfig, dotted_path: &str) -> Option<i64> {
let mut cur = cfg.as_value();
for segment in dotted_path.split('.') {
cur = cur.as_object()?.get(segment)?;
}
cur.as_i64().or_else(|| cur.as_f64().map(|f| f as i64))
}