use super::super::{Block, EngineDocument, InlineSpan, inline_text};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct GophermapContext {
pub host: String,
pub port: u16,
}
impl EngineDocument {
pub fn to_gophermap(&self, ctx: &GophermapContext) -> String {
let mut out = String::new();
for block in &self.blocks {
write_gophermap_block(block, ctx, &mut out, "");
}
out.push_str(".\r\n");
out
}
pub fn to_text(&self) -> String {
let mut out = String::new();
for block in &self.blocks {
write_text_block(block, &mut out, "");
}
out
}
}
fn push_info(out: &mut String, text: &str) {
for line in if text.is_empty() { "\u{0}" } else { text }.split('\n') {
let line = if line == "\u{0}" { "" } else { line };
out.push('i');
out.push_str(line);
out.push_str("\tfake\t(NULL)\t0\r\n");
}
}
fn push_link(out: &mut String, ctx: &GophermapContext, url: &str, label: &str) {
let display = if label.is_empty() { url } else { label };
if let Some(rest) = url.strip_prefix("gopher://") {
let (hostport, path) = rest.split_once('/').unwrap_or((rest, ""));
let (host, port) = match hostport.split_once(':') {
Some((h, p)) => (h, p.parse::<u16>().unwrap_or(70)),
None => (hostport, 70),
};
let mut chars = path.chars();
let item_type = chars.next().unwrap_or('1');
let selector: String = chars.collect();
out.push(item_type);
out.push_str(display);
out.push('\t');
out.push_str(if selector.is_empty() { "/" } else { &selector });
out.push('\t');
out.push_str(host);
out.push_str(&format!("\t{port}\r\n"));
} else {
out.push('h');
out.push_str(display);
out.push_str(&format!("\tURL:{url}\t{}\t{}\r\n", ctx.host, ctx.port));
}
}
fn collect_links(span: &InlineSpan, out: &mut Vec<(String, String)>) {
match span {
InlineSpan::Link { url, spans, .. } => {
out.push((url.clone(), inline_text(spans)));
for inner in spans {
collect_links(inner, out);
}
}
InlineSpan::Emphasis(spans) | InlineSpan::Strong(spans) => {
for inner in spans {
collect_links(inner, out);
}
}
_ => {}
}
}
fn write_gophermap_block(block: &Block, ctx: &GophermapContext, out: &mut String, prefix: &str) {
match block {
Block::Heading { spans, .. } => {
push_info(out, &format!("{prefix}{}", inline_text(spans)));
push_info(out, "");
}
Block::Paragraph { spans } => {
let text = inline_text(spans);
if !text.is_empty() && !super::is_link_only(spans) {
push_info(out, &format!("{prefix}{text}"));
}
let mut links = Vec::new();
for span in spans {
collect_links(span, &mut links);
}
for (url, label) in links {
push_link(out, ctx, &url, &label);
}
}
Block::CodeBlock { text, .. } | Block::Preformatted { text } => {
for line in text.lines() {
push_info(out, &format!("{prefix}{line}"));
}
}
Block::Quote { blocks } => {
for inner in blocks {
write_gophermap_block(inner, ctx, out, &format!("{prefix}> "));
}
}
Block::List { items, .. } => {
for item in items {
for inner in item {
write_gophermap_block(inner, ctx, out, &format!("{prefix}* "));
}
}
}
Block::Image { url, alt } => push_link(out, ctx, url, alt),
Block::Rule => push_info(out, &format!("{prefix}---")),
Block::FeedHeader {
title, subtitle, ..
} => {
push_info(out, &format!("{prefix}{title}"));
if let Some(subtitle) = subtitle {
push_info(out, &format!("{prefix}{subtitle}"));
}
}
Block::FeedEntry {
title,
date,
article_url,
..
} => {
let dated = match date {
Some(date) => format!("{prefix}{title} — {date}"),
None => format!("{prefix}{title}"),
};
match article_url {
Some(url) => push_link(out, ctx, url, &dated),
None => push_info(out, &dated),
}
}
Block::MetadataRow { label, value } => {
push_info(out, &format!("{prefix}{label}: {value}"));
}
Block::Badge { text } => push_info(out, &format!("{prefix}[{text}]")),
Block::Table { header, rows, .. } => {
for line in super::table_lines(header, rows) {
push_info(out, &format!("{prefix}{line}"));
}
}
}
}
fn write_text_block(block: &Block, out: &mut String, prefix: &str) {
match block {
Block::Table { header, rows, .. } => {
for line in super::table_lines(header, rows) {
out.push_str(prefix);
out.push_str(&line);
out.push('\n');
}
out.push('\n');
}
Block::Heading { spans, .. } => {
out.push_str(prefix);
out.push_str(&inline_text(spans));
out.push_str("\n\n");
}
Block::Paragraph { spans } => {
let text = text_with_links(spans);
if !text.is_empty() {
out.push_str(prefix);
out.push_str(&text);
out.push_str("\n\n");
}
}
Block::CodeBlock { text, .. } | Block::Preformatted { text } => {
for line in text.lines() {
out.push_str(prefix);
out.push_str(" ");
out.push_str(line);
out.push('\n');
}
out.push('\n');
}
Block::Quote { blocks } => {
for inner in blocks {
write_text_block(inner, out, &format!("{prefix}> "));
}
}
Block::List { ordered, items } => {
for (index, item) in items.iter().enumerate() {
let marker = if *ordered {
format!("{}. ", index + 1)
} else {
"- ".to_string()
};
let mut inner = String::new();
for block in item {
write_text_block(block, &mut inner, "");
}
out.push_str(prefix);
out.push_str(&marker);
out.push_str(inner.trim_end());
out.push('\n');
}
out.push('\n');
}
Block::Image { url, alt } => {
out.push_str(prefix);
if alt.is_empty() {
out.push_str(url);
} else {
out.push_str(&format!("{alt} <{url}>"));
}
out.push_str("\n\n");
}
Block::Rule => {
out.push_str(prefix);
out.push_str("---\n\n");
}
Block::FeedHeader {
title, subtitle, ..
} => {
out.push_str(prefix);
out.push_str(title);
out.push('\n');
if let Some(subtitle) = subtitle {
out.push_str(prefix);
out.push_str(subtitle);
out.push('\n');
}
out.push('\n');
}
Block::FeedEntry {
title,
date,
summary,
article_url,
..
} => {
out.push_str(prefix);
match date {
Some(date) => out.push_str(&format!("{title} — {date}\n")),
None => {
out.push_str(title);
out.push('\n');
}
}
if let Some(summary) = summary {
out.push_str(prefix);
out.push_str(summary);
out.push('\n');
}
if let Some(url) = article_url {
out.push_str(prefix);
out.push('<');
out.push_str(url);
out.push_str(">\n");
}
out.push('\n');
}
Block::MetadataRow { label, value } => {
out.push_str(prefix);
out.push_str(&format!("{label}: {value}\n"));
}
Block::Badge { text } => {
out.push_str(prefix);
out.push_str(&format!("[{text}]\n"));
}
}
}
fn text_with_links(spans: &[InlineSpan]) -> String {
let mut out = String::new();
for span in spans {
match span {
InlineSpan::Link { url, spans, .. } => {
let label = inline_text(spans);
if label.is_empty() || label == *url {
out.push('<');
out.push_str(url);
out.push('>');
} else {
out.push_str(&format!("{label} <{url}>"));
}
}
InlineSpan::Emphasis(spans) | InlineSpan::Strong(spans) => {
out.push_str(&text_with_links(spans));
}
InlineSpan::Text(text) | InlineSpan::Code(text) => out.push_str(text),
InlineSpan::SoftBreak => out.push(' '),
InlineSpan::LineBreak => out.push('\n'),
}
}
out
}