pub(crate) mod api;
pub(crate) mod ast;
mod inline;
mod list;
mod parse;
mod render_html;
mod table;
pub(crate) use parse::parse_markdown;
pub(crate) fn prefers_rich_render(text: &str) -> bool {
contains_table(text) || contains_task_list(text)
}
pub(crate) fn contains_table(text: &str) -> bool {
let lines: Vec<String> = text.lines().map(str::to_string).collect();
(0..lines.len()).any(|i| table::try_parse(&lines, i).is_some())
}
pub(crate) fn should_send_native_rich(text: &str) -> bool {
crate::config::Config::current()
.channels
.telegram
.rich_messages
&& has_rich_structure(text)
}
pub(crate) fn has_rich_structure(text: &str) -> bool {
contains_table(text)
|| text.lines().any(|line| {
let t = line.trim_start();
is_atx_heading(t)
|| list::is_item(t)
|| t.starts_with("```")
|| t == "$$"
|| t == "<details>"
|| t == "<details open>"
|| t.starts_with("<details ")
})
}
fn is_atx_heading(t: &str) -> bool {
let hashes = t.chars().take_while(|&c| c == '#').count();
(1..=6).contains(&hashes) && t[hashes..].starts_with(' ')
}
pub(crate) fn contains_task_list(text: &str) -> bool {
text.lines().any(|line| {
let t = line.trim_start();
let after = t
.strip_prefix("- ")
.or_else(|| t.strip_prefix("* "))
.or_else(|| t.strip_prefix("+ "));
matches!(after, Some(rest)
if rest.starts_with("[ ]") || rest.starts_with("[x]") || rest.starts_with("[X]"))
})
}
pub(crate) fn markdown_to_html(text: &str) -> String {
render_html::render_html(&parse_markdown(text))
}