use std::{borrow::Cow, fmt::Display, str::pattern::Pattern};
trait RaiseTo<Target> {
type Output<'a>
where
Target: 'a;
fn raise_to<'a>(&self, target: &'a mut Target) -> Self::Output<'a>;
}
#[derive(Clone, Debug)]
pub enum Media<'a> {
Gemini(Gemini<'a>),
Text(&'a str),
}
impl RaiseTo<String> for Media<'_> {
type Output<'a> = Media<'a>;
fn raise_to<'a>(&self, target: &'a mut String) -> Self::Output<'a> {
match self {
Media::Gemini(gem) => Media::Gemini(gem.raise_to(target)),
Media::Text(text) => Media::Text(text.raise_to(target)),
}
}
}
#[derive(Clone, Debug)]
pub struct Gemini<'a> {
pub lines: Vec<Line<'a>>,
}
impl RaiseTo<String> for Gemini<'_> {
type Output<'a> = Gemini<'a>;
fn raise_to<'a>(&self, target: &'a mut String) -> Self::Output<'a> {
Self {
lines: self.lines.iter().map(|ln| ln.raise_to(target)).collect(),
}
}
}
#[derive(Clone, Debug)]
enum RawLine<'a> {
Toggle { alt: TextLine<'a> },
Line(Line<'a>),
}
#[derive(Copy, Clone, Debug)]
pub enum HeadingLevel {
L1 = 1,
L2 = 2,
L3 = 3,
}
#[derive(Clone, Debug)]
pub enum Line<'a> {
Heading {
level: HeadingLevel,
title: TextLine<'a>,
},
Link {
url: TextLine<'a>,
description: Option<TextLine<'a>>,
},
Text(TextLine<'a>),
Preformatted(Preformat<'a>),
ListItem(TextLine<'a>),
Quote(TextLine<'a>),
}
impl RaiseTo<String> for Line<'_> {
type Output<'a> = Line<'a>;
fn raise_to<'a>(&self, target: &'a mut String) -> Self::Output<'a> {
match self {
Line::Heading { level, title } => Line::Heading {
level: *level,
title: title.raise_to(target),
},
Line::Link { url, description } => {
let url = url.raise_to(target);
let description = description.map(|d| d.raise_to(target));
Line::Link { url, description }
}
Line::Text(t) => Line::Text(t.raise_to(target)),
Line::Preformatted(p) => Line::Preformatted(p.raise_to(target)),
Line::ListItem(li) => Line::ListItem(li.raise_to(target)),
Line::Quote(q) => Line::Quote(q.raise_to(target)),
}
}
}
#[derive(Clone, Debug)]
pub struct Preformat<'a> {
pub alt: TextLine<'a>,
pub lines: Vec<TextLine<'a>>,
}
impl RaiseTo<String> for Preformat<'_> {
type Output<'a> = Preformat<'a>;
fn raise_to<'a>(&self, target: &'a mut String) -> Self::Output<'a> {
let alt = self.alt.raise_to(target);
let lines = self.lines.iter().map(|ln| ln.raise_to(target)).collect();
Self { alt, lines }
}
}
fn split_trim_maybe_once<'a, P: Pattern<'a> + Copy>(
s: &'a str,
p: P,
) -> (&'a str, Option<&'a str>) {
match s.split_once(p) {
Some((s, rem)) => (s, Some(rem.trim_start_matches(p))),
None => (s, None),
}
}
fn string_to_preformat(string: TextLine<'_>) -> Option<TextLine<'_>> {
let line = string.0;
if line.starts_with("```") {
return None;
} else {
return Some(string);
}
}
macro_rules! match_str {
(($name:ident) $($s:expr , $rem:ident => $body:expr ,)* _ => $else:expr) => {
$(if $name.starts_with($s) {
let $rem = &$name[$s.len()..];
$body
} else)* {$else}
};
}
fn string_to_line(string: TextLine<'_>) -> RawLine {
let line = string.0;
RawLine::Line({
match_str! {(line)
"```", rem => {
return RawLine::Toggle {
alt: TextLine(rem),
}
},
"=> ", rem => {
match rem.split_once(' ') {
Some((url, desc)) => {
let url = url.trim_start_matches(' ');
Line::Link {
url: TextLine(url),
description: Some(TextLine(desc)),
}
}
None => Line::Link {
url: TextLine(rem),
description: None,
},
}
},
"* ", rem => {
Line::ListItem(TextLine(rem))
},
"# ", rem => {
Line::Heading {
level: HeadingLevel::L1,
title: TextLine(rem),
}
},
"## ", rem => {
Line::Heading {
level: HeadingLevel::L2,
title: TextLine(rem),
}
},
"### ", rem => {
Line::Heading {
level: HeadingLevel::L3,
title: TextLine(rem),
}
},
">", rem => {
Line::Quote(TextLine(rem))
},
_ => {
Line::Text(TextLine(line))
}
}
})
}
fn string_to_lines(value: &str) -> Vec<Line> {
{
let mut outer = vec![];
let mut preformat_block: Option<Preformat> = None;
let lines = value.lines().map(TextLine);
for line in lines {
if preformat_block.is_some() {
match string_to_preformat(line) {
None => {
let Some(i) = preformat_block.take()
else {unreachable!("This is within the is_some arm of the if")};
outer.push(Line::Preformatted(i));
}
Some(p) => preformat_block.as_mut().unwrap().lines.push(p),
}
} else {
match string_to_line(line) {
RawLine::Toggle { alt } => {
preformat_block = Some(Preformat { alt, lines: vec![] });
}
RawLine::Line(l) => outer.push(l),
}
}
}
outer
}
}
#[derive(Copy, Clone, Debug)]
pub struct TextLine<'a>(&'a str);
impl<'a> From<TextLine<'a>> for Cow<'a, str> {
fn from(value: TextLine<'a>) -> Self {
value.0.into()
}
}
impl<'a> Display for TextLine<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl<'a> AsRef<str> for TextLine<'a> {
fn as_ref(&self) -> &str {
self.0
}
}
impl RaiseTo<String> for TextLine<'_> {
type Output<'a> = TextLine<'a>;
fn raise_to<'a>(&self, target: &'a mut String) -> Self::Output<'a> {
let old_end = target.len();
target.push_str(self.0);
TextLine(&target[old_end..])
}
}
impl RaiseTo<String> for &'_ str {
type Output<'a> = &'a str;
fn raise_to<'a>(&self, target: &'a mut String) -> Self::Output<'a> {
let old_end = target.len();
target.push_str(self);
&target[old_end..]
}
}
impl<'a> From<&'a str> for Gemini<'a> {
fn from(value: &'a str) -> Self {
Gemini {
lines: string_to_lines(value),
}
}
}