use super::{parse_attrs, Attr};
use crate::model::{Color, FontRole, Highlight, Inline, TextStyle};
pub(crate) fn parse_inlines(s: &str) -> Vec<Inline> {
let mut out = Vec::new();
parse_into(s, TextStyle::default(), &mut out);
out
}
fn parse_into(s: &str, base: TextStyle, out: &mut Vec<Inline>) {
let mut buf = String::new();
let mut i = 0;
let mut prev: Option<char> = None;
while i < s.len() {
let rest = &s[i..];
if rest.starts_with('\n') {
flush(&mut buf, &base, out);
out.push(Inline::LineBreak);
i += 1;
prev = Some('\n');
continue;
}
if let Some(after) = rest.strip_prefix('\\') {
match after.chars().next() {
Some(ch) if ch.is_ascii_punctuation() => {
buf.push(ch);
i += 1 + ch.len_utf8();
prev = Some(ch);
}
_ => {
buf.push('\\');
i += 1;
prev = Some('\\');
}
}
continue;
}
if rest.starts_with("![") {
if let Some((alt, n)) = link_span(&rest[1..]) {
flush(&mut buf, &base, out);
parse_into(alt, base.clone(), out);
i += 1 + n;
prev = Some(')');
continue;
}
}
if rest.starts_with('[') {
if let Some((inner, attr_s, n)) = attr_span(rest) {
flush(&mut buf, &base, out);
parse_into(inner, apply_attrs(base.clone(), attr_s), out);
i += n;
prev = Some('}');
continue;
}
if let Some((inner, n)) = link_span(rest) {
flush(&mut buf, &base, out);
let mut st = base.clone();
st.link = true;
parse_into(inner, st, out);
i += n;
prev = Some(')');
continue;
}
}
if let Some(n) = emphasis(rest, prev, &base, &mut buf, out) {
prev = s[..i + n].chars().last();
i += n;
continue;
}
let ch = rest.chars().next().unwrap();
buf.push(ch);
i += ch.len_utf8();
prev = Some(ch);
}
flush(&mut buf, &base, out);
}
fn flush(buf: &mut String, style: &TextStyle, out: &mut Vec<Inline>) {
if !buf.is_empty() {
out.push(Inline::Text { text: std::mem::take(buf), style: style.clone() });
}
}
fn emphasis(
rest: &str,
prev: Option<char>,
base: &TextStyle,
buf: &mut String,
out: &mut Vec<Inline>,
) -> Option<usize> {
const DELIMS: &[&str] = &["***", "___", "**", "__", "~~", "==", "`", "*", "_"];
for &d in DELIMS {
let Some(after) = rest.strip_prefix(d) else { continue };
let underscore = d.starts_with('_');
if underscore && prev.is_some_and(is_word) {
continue; }
let Some(close) = find_close(after, d, underscore) else { continue };
let inner = &after[..close];
if inner.is_empty() {
continue;
}
flush(buf, base, out);
let consumed = d.len() * 2 + close;
if d == "`" {
out.push(Inline::Code(inner.to_string()));
} else {
let mut st = base.clone();
match d {
"***" | "___" => {
st.weight = Some(700);
st.italic = true;
}
"**" | "__" => st.weight = Some(700),
"*" | "_" => st.italic = true,
"~~" => st.strike = true,
"==" => st.highlight = Some(Highlight::Theme),
_ => {}
}
parse_into(inner, st, out);
}
return Some(consumed);
}
None
}
fn find_close(after: &str, d: &str, underscore: bool) -> Option<usize> {
let mut from = 0;
loop {
let pos = after[from..].find(d)? + from;
if underscore && after[pos + d.len()..].chars().next().is_some_and(is_word) {
from = pos + d.len();
continue;
}
return Some(pos);
}
}
fn is_word(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
fn attr_span(rest: &str) -> Option<(&str, &str, usize)> {
let close_br = rest.find(']')?;
let after = &rest[close_br + 1..];
if !after.starts_with('{') {
return None;
}
let close_brace = after.find('}')?;
let inner = &rest[1..close_br];
let attrs = &after[1..close_brace];
Some((inner, attrs, close_br + 1 + close_brace + 1))
}
fn link_span(rest: &str) -> Option<(&str, usize)> {
let close_br = rest.find(']')?;
let after = &rest[close_br + 1..];
if !after.starts_with('(') {
return None;
}
let mut depth = 0usize;
for (k, ch) in after.char_indices() {
match ch {
'(' => depth += 1,
')' => {
depth -= 1;
if depth == 0 {
return Some((&rest[1..close_br], close_br + 1 + k + 1));
}
}
'\n' => return None,
_ => {}
}
}
None
}
fn apply_attrs(mut st: TextStyle, attrs: &str) -> TextStyle {
for a in parse_attrs(attrs) {
match a {
Attr::Kv(k, v) => match k.as_str() {
"color" => {
if let Some(c) = Color::hex(&v) {
st.color = Some(c);
}
}
"bg" => {
if let Some(c) = Color::hex(&v) {
st.highlight = Some(Highlight::Custom(c));
}
}
"size" => {
if let Ok(m) = v.parse::<f32>() {
if m > 0.0 {
st.size = m;
}
}
}
"font" => {
st.font = match v.as_str() {
"sans" => FontRole::Sans,
"serif" => FontRole::Serif,
"mono" => FontRole::Mono,
"kai" => FontRole::Kai,
_ => FontRole::Named(v),
}
}
"weight" => {
if let Ok(w) = v.parse::<u16>() {
if (1..=1000).contains(&w) {
st.weight = Some(w);
}
}
}
"ring" => {
st.ring.get_or_insert_default().color = Color::hex(&v);
}
"ring-radius" => {
if let Some(r) = parse_len(&v) {
let m = st.ring.get_or_insert_default();
m.rx = Some(r);
m.ry = Some(r);
}
}
"ring-rx" => {
if let Some(r) = parse_len(&v) {
st.ring.get_or_insert_default().rx = Some(r);
}
}
"ring-ry" => {
if let Some(r) = parse_len(&v) {
st.ring.get_or_insert_default().ry = Some(r);
}
}
"ring-stroke" => {
if let Some(w) = parse_len(&v) {
st.ring.get_or_insert_default().width = Some(w);
}
}
"dot" => {
st.dot.get_or_insert_default().color = Color::hex(&v);
}
"dot-radius" => {
if let Some(r) = parse_len(&v) {
st.dot.get_or_insert_default().radius = Some(r);
}
}
"aside" => {
st.aside = match v.as_str() {
"left" => Some(crate::model::AsideSide::Left),
"right" => Some(crate::model::AsideSide::Right),
_ => st.aside,
}
}
_ => {}
},
Attr::Flag(f) => match f.as_str() {
"bold" => st.weight = Some(700),
"light" => st.weight = Some(300),
"italic" => st.italic = true,
"underline" => st.underline = true,
"strike" => st.strike = true,
"ring" => {
st.ring.get_or_insert_default();
}
"ring-each" => {
st.ring.get_or_insert_default().each = true;
}
"dot" => {
st.dot.get_or_insert_default();
}
"dot-each" => {
st.dot.get_or_insert_default().each = true;
}
"aside" => st.aside = Some(crate::model::AsideSide::Right),
_ => {}
},
}
}
st
}
fn parse_len(v: &str) -> Option<f32> {
v.parse::<f32>().ok().filter(|x| x.is_finite() && *x > 0.0)
}