use super::*;
pub(super) fn join_paras(paras: &[Vec<Inline>]) -> Vec<Inline> {
let mut out: Vec<Inline> = Vec::new();
for (i, p) in paras.iter().enumerate() {
if i > 0 {
out.push(Inline::Text("\n".to_string()));
}
out.extend(p.iter().cloned());
}
out
}
pub(super) fn grp_arg(atoms: &[String]) -> String {
match atoms {
[] => String::new(),
[one] => one.clone(),
many => format!("(GRP {})", many.join(" ")),
}
}
pub(super) fn prefix_space(s: &str) -> String {
if s.is_empty() {
String::new()
} else {
format!(" {s}")
}
}
pub(super) fn text_atom(body: &str) -> Option<String> {
let t = norm_ws(body);
(!t.is_empty()).then(|| format!("(TEXT {})", encode_text(&t)))
}
pub(super) fn double_escape_md(s: &str) -> String {
s.replace('\\', "\\\\")
.replace("\\\\[", "\\[")
.replace("\\\\]", "\\]")
}
pub(super) fn append_rendered_text(atoms: &mut Vec<String>, extra: &str) {
if let Some(last) = atoms.last_mut()
&& let Some(text) = decode_text_atom(last)
&& let Some(merged) = text_atom(&format!("{text} {extra}"))
{
*last = merged;
return;
}
if let Some(atom) = text_atom(extra) {
atoms.push(atom);
}
}
pub(super) fn decode_text_atom(atom: &str) -> Option<String> {
let inner = atom.strip_prefix("(TEXT \"")?.strip_suffix("\")")?;
let mut out = String::with_capacity(inner.len());
let mut chars = inner.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('n') => out.push('\n'),
Some(other) => out.push(other), None => out.push('\\'),
}
} else {
out.push(c);
}
}
Some(out)
}
pub(super) fn strip_rd_comments(s: &str) -> String {
physical_lines(s)
.map(strip_rd_line_comment)
.collect::<Vec<_>>()
.join("\n")
}
pub(super) fn strip_rd_line_comment(line: &str) -> &str {
let mut escaped = false;
for (i, c) in line.char_indices() {
if escaped {
escaped = false;
} else if c == '\\' {
escaped = true;
} else if c == '%' {
return &line[..i];
}
}
line
}
pub(super) fn rcode_atoms(body: &str) -> Vec<String> {
let mut atoms = Vec::new();
let mut rest = body;
while let Some(idx) = rest.find('\n') {
let (seg, tail) = rest.split_at(idx + 1);
atoms.push(format!("(RCODE {})", encode_text(seg)));
rest = tail;
}
if !rest.is_empty() {
atoms.push(format!("(RCODE {})", encode_text(rest)));
}
atoms
}
pub(super) fn norm_ws(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut pending_space = false;
for c in s.chars() {
if is_posix_space(c) {
pending_space = true;
} else {
if pending_space && !out.is_empty() {
out.push(' ');
}
pending_space = false;
out.push(c);
}
}
out
}
pub(super) fn is_posix_space(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '\x0b' | '\x0c' | '\r')
}
pub(super) fn trim_field_atoms(atoms: &mut Vec<String>) {
trim_field_atoms_start(atoms);
trim_field_atoms_end(atoms);
}
pub(super) fn trim_field_atoms_start(atoms: &mut Vec<String>) {
while let Some(text) = atoms.first().and_then(|a| decode_text_atom(a)) {
let trimmed = text.trim_start();
if trimmed.is_empty() {
atoms.remove(0);
} else {
if trimmed.len() != text.len() {
atoms[0] = format!("(TEXT {})", encode_text(trimmed));
}
break;
}
}
}
pub(super) fn trim_field_atoms_end(atoms: &mut Vec<String>) {
while let Some(text) = atoms.last().and_then(|a| decode_text_atom(a)) {
let trimmed = text.trim_end();
if trimmed.is_empty() {
atoms.pop();
} else {
if trimmed.len() != text.len() {
let last = atoms.len() - 1;
atoms[last] = format!("(TEXT {})", encode_text(trimmed));
}
break;
}
}
}
pub(super) const SOFT_BREAK: char = '\u{c}';
pub(super) fn physical_lines(run: &str) -> impl Iterator<Item = &str> {
run.split(['\n', SOFT_BREAK])
}
pub(super) fn encode_text(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
_ => out.push(c),
}
}
out.push('"');
out
}
pub(super) fn strip_code_span(text: &str) -> String {
let ticks = text.bytes().take_while(|&b| b == b'`').count();
let inner = text
.get(ticks..text.len() - ticks)
.unwrap_or("")
.replace('\n', " ");
if inner.len() >= 2
&& inner.starts_with(' ')
&& inner.ends_with(' ')
&& !inner.trim().is_empty()
{
inner[1..inner.len() - 1].to_string()
} else {
inner
}
}
pub(super) fn code_wrap(inner: String, is_code: bool) -> String {
if is_code {
format!("(\\code {inner})")
} else {
inner
}
}
pub(super) fn unwrap_code_span(s: &str) -> (&str, bool) {
let b = s.as_bytes();
if b.len() >= 2 && b[0] == b'`' && b[b.len() - 1] == b'`' {
(&s[1..s.len() - 1], true)
} else {
(s, false)
}
}