Telegram markdown support for structured json messages

This commit is contained in:
euzu
2025-10-22 11:09:43 +02:00
parent 86d8409391
commit 8245bc4c85
14 changed files with 160 additions and 48 deletions
+49
View File
@@ -3,6 +3,7 @@ use std::io::{self, Read};
use serde::de::DeserializeOwned;
use serde::{Deserialize};
use serde_json::{self, Deserializer, Value};
use crate::utils::humanize_snake_case;
fn read_skipping_ws(mut reader: impl Read) -> io::Result<u8> {
loop {
@@ -166,4 +167,52 @@ where
{
let value: Option<String> = Option::deserialize(deserializer)?;
Ok(value.unwrap_or_default())
}
const MARKDOWN_SPECIAL_CHARS: &str = r#"_*[]()~`>#+-=|{}.!\"#;
fn escape_markdown_v2(text: &str) -> String {
let mut escaped = String::new();
for c in text.chars() {
if MARKDOWN_SPECIAL_CHARS.contains(c) {
escaped.push('\\');
}
escaped.push(c);
}
escaped
}
fn json_to_markdown(value: &Value) -> String {
fn format_value(v: &Value, indent: usize) -> String {
let pad = " ".repeat(indent);
match v {
Value::Object(map) => map.iter()
.map(|(k, v)| {
let formatted = format_value(v, indent + 1);
let key = humanize_snake_case(k);
if v.is_object() || v.is_array() {
format!("{pad}*{key}:*\n{formatted}")
} else {
format!("{pad}*{key}:* {formatted}")
}
})
.collect::<Vec<_>>()
.join("\n"),
Value::Array(arr) => arr.iter()
.map(|v| format!("{pad}\\- {}", format_value(v, indent + 1).trim()))
.collect::<Vec<_>>()
.join("\n"),
Value::String(s) => escape_markdown_v2(s),
Value::Number(n) => escape_markdown_v2(&n.to_string()),
Value::Bool(b) => b.to_string(),
Value::Null => "null".to_string(),
}
}
format_value(value, 0)
}
pub fn json_str_to_markdown(json_str: &str) -> Result<String, serde_json::Error> {
let value: Value = serde_json::from_str(json_str)?;
Ok(json_to_markdown(&value))
}
+21
View File
@@ -106,6 +106,27 @@ pub fn mask_credentials(s: &str) -> String {
}
}
pub fn humanize_snake_case(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut capitalize_next = true;
for c in s.chars() {
if c == '_' {
result.push(' ');
capitalize_next = true;
} else if capitalize_next {
for up in c.to_uppercase() {
result.push(up);
}
capitalize_next = false;
} else {
result.push(c);
}
}
result
}
#[cfg(test)]
mod test {
use std::collections::HashSet;