use std::{iter::Peekable, vec::IntoIter};
use crate::{
textproc::process_text,
wrap::{Token, tokenize_markdown},
};
fn split_marks(s: &str) -> (&str, &str, &str) {
let first = s.find(|c| c != '*' && c != '_').unwrap_or(s.len());
let last = s.rfind(|c| c != '*' && c != '_').map_or(first, |i| i + 1);
(&s[..first], &s[first..last], &s[last..])
}
fn push_code(code: &str, out: &mut String) {
let mut max_run = 0;
let mut run = 0;
for c in code.chars() {
if c == '`' {
run += 1;
max_run = max_run.max(run);
} else {
run = 0;
}
}
let fence = "`".repeat(max_run + 1);
out.push_str(&fence);
out.push_str(code);
out.push_str(&fence);
}
fn inline_code_suffix<'a>(raw: &'a str, code: &'a str) -> &'a str {
let fence_len = raw.chars().take_while(|&ch| ch == '`').count();
if fence_len == 0 {
return "";
}
let body_start = fence_len;
let code_end = body_start + code.len();
if raw.get(body_start..code_end) != Some(code) {
return "";
}
let close_end = code_end + fence_len;
if raw.get(code_end..close_end) != Some(&raw[..fence_len]) {
return "";
}
raw.get(close_end..).unwrap_or("")
}
fn push_code_with_suffix(raw: &str, code: &str, out: &mut String) {
push_code(code, out);
out.push_str(inline_code_suffix(raw, code));
}
fn has_code_emphasis_adjacent(source: &str) -> bool {
source.contains("`*") || source.contains("`_") || source.contains("*`") || source.contains("_`")
}
fn handle_text_token<'a>(
raw: &'a str,
next: Option<&Token<'a>>,
out: &mut String,
pending: &mut &'a str,
) {
if !next.is_some_and(|token| matches!(token, Token::Code { .. })) {
out.push_str(raw);
return;
}
let (lead, body, trail) = split_marks(raw);
if body.is_empty() && trail.is_empty() {
*pending = lead;
return;
}
out.push_str(lead);
out.push_str(body);
*pending = trail;
}
fn try_fold_matching_emphasis<'a>(
tokens: &mut Peekable<IntoIter<Token<'a>>>,
pending: &mut &'a str,
raw: &'a str,
code: &str,
out: &mut String,
) -> bool {
let Some(Token::Text(next)) = tokens.peek() else {
return false;
};
let (lead, mid, trail) = split_marks(next);
if *pending == lead && mid.is_empty() && trail.is_empty() {
out.push_str(pending);
push_code_with_suffix(raw, code, out);
out.push_str(lead);
*pending = "";
tokens.next();
return true;
}
false
}
fn consume_code_affixes<'a>(
tokens: &mut Peekable<IntoIter<Token<'a>>>,
pending: &mut &'a str,
) -> (&'a str, &'a str, bool) {
let mut prefix = std::mem::take(pending);
let mut suffix = "";
let mut modified = !prefix.is_empty();
let Some(Token::Text(next)) = tokens.peek_mut() else {
return (prefix, suffix, modified);
};
let (lead, mid, _) = split_marks(next);
if lead.is_empty() {
return (prefix, suffix, modified);
}
modified = true;
if prefix.is_empty() {
prefix = lead;
} else if mid.is_empty() {
suffix = lead;
} else {
prefix = "";
}
*next = &next[lead.len()..];
(prefix, suffix, modified)
}
fn handle_code_token<'a>(
tokens: &mut Peekable<IntoIter<Token<'a>>>,
code_token: (&'a str, &'a str),
out: &mut String,
pending: &mut &'a str,
) {
let (raw, code) = code_token;
if !pending.is_empty() && try_fold_matching_emphasis(tokens, pending, raw, code, out) {
return;
}
let (prefix, suffix, modified) = consume_code_affixes(tokens, pending);
out.push_str(prefix);
if modified {
push_code_with_suffix(raw, code, out);
} else {
out.push_str(raw);
}
out.push_str(suffix);
}
#[must_use]
pub fn fix_code_emphasis(lines: &[String]) -> Vec<String> {
if lines.is_empty() {
return Vec::new();
}
let trailing_blanks = lines.iter().rev().take_while(|l| l.is_empty()).count();
if trailing_blanks == lines.len() {
return vec![String::new(); lines.len()];
}
let source = lines.join("\n");
if !has_code_emphasis_adjacent(&source) {
return lines.to_vec();
}
let mut tokens = tokenize_markdown(&source).into_iter().peekable();
let mut out = String::new();
let mut pending = "";
while let Some(token) = tokens.next() {
match token {
Token::Text(raw) => handle_text_token(raw, tokens.peek(), &mut out, &mut pending),
Token::Code { raw, code, .. } => {
handle_code_token(&mut tokens, (raw, code), &mut out, &mut pending);
}
Token::Fence(f) => out.push_str(f),
Token::Newline => out.push('\n'),
}
}
process_text(&out, trailing_blanks)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merges_emphasis_and_code() {
let input = vec![
"`StepContext`** Enhancement (in **`crates/rstest-bdd/src/context.rs`**)**".to_string(),
];
let expected = vec![
"**`StepContext` Enhancement (in `crates/rstest-bdd/src/context.rs`)**".to_string(),
];
assert_eq!(fix_code_emphasis(&input), expected);
}
#[test]
fn ignores_simple_text() {
let input = vec!["nothing here".to_string()];
assert_eq!(fix_code_emphasis(&input), input);
}
#[test]
fn preserves_emphasised_code_with_inflectional_suffix() {
let input = vec!["*`VarGuard`s*".to_string(), "**`fetch`ed**".to_string()];
assert_eq!(fix_code_emphasis(&input), input);
}
#[test]
fn preserves_emphasised_code_only() {
let input = vec!["**`code`**".to_string()];
assert_eq!(fix_code_emphasis(&input), input);
}
#[test]
fn preserves_inner_backticks_in_code() {
let input = vec!["``a`b``".to_string()];
assert_eq!(fix_code_emphasis(&input), input);
}
#[test]
fn preserves_standalone_code() {
let input = vec!["before `code` after".to_string()];
assert_eq!(fix_code_emphasis(&input), input);
}
#[test]
fn consume_code_affixes_clears_mixed_pending_prefix() {
let mut tokens = vec![Token::Text("*lead*tail")].into_iter().peekable();
let mut pending = "**";
let (prefix, suffix, modified) = consume_code_affixes(&mut tokens, &mut pending);
assert_eq!((prefix, suffix, modified), ("", "", true));
assert_eq!(tokens.next(), Some(Token::Text("lead*tail")));
}
}