use crate::markdown::Block;
pub const CODE_PAD_COLS: usize = 1;
pub fn code_indent_strip_chars(raw_line: &str, fenced: bool) -> usize {
if fenced {
return 0;
}
if raw_line.starts_with('\t') {
return 1;
}
raw_line.chars().take(4).take_while(|c| *c == ' ').count()
}
fn is_closing_fence_line(raw_line: &str) -> bool {
let trimmed = raw_line.trim();
let Some(first) = trimmed.chars().next() else {
return false;
};
(first == '`' || first == '~')
&& trimmed.chars().count() >= 3
&& trimmed.chars().all(|c| c == first)
}
pub fn is_code_fence_row(fenced: bool, raw_line_idx: usize, raw_lines: &[&str]) -> bool {
if !fenced {
return false;
}
if raw_line_idx == 0 {
return true;
}
raw_line_idx + 1 == raw_lines.len()
&& raw_lines
.get(raw_line_idx)
.is_some_and(|line| is_closing_fence_line(line))
}
pub fn code_raw_col_to_rendered_col(raw_line: &str, fenced: bool, raw_col: usize) -> usize {
raw_col.saturating_sub(code_indent_strip_chars(raw_line, fenced)) + CODE_PAD_COLS
}
pub fn code_rendered_col_to_raw_col(raw_line: &str, fenced: bool, rendered_col: usize) -> usize {
let strip = code_indent_strip_chars(raw_line, fenced);
(rendered_col.saturating_sub(CODE_PAD_COLS) + strip).min(raw_line.chars().count())
}
pub fn line_allows_raw_reveal(
block: Option<&Block>,
raw_line_idx: usize,
raw_lines: &[&str],
) -> bool {
match block {
Some(Block::CodeBlock { fenced, .. }) => {
is_code_fence_row(*fenced, raw_line_idx, raw_lines)
}
_ => true,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fenced_body_col_shifts_by_the_pad_only() {
assert_eq!(code_raw_col_to_rendered_col("let x = 1;", true, 0), 1);
assert_eq!(code_raw_col_to_rendered_col("let x = 1;", true, 6), 7);
}
#[test]
fn indented_body_col_drops_the_stripped_indent() {
assert_eq!(code_raw_col_to_rendered_col(" let x = 1;", false, 4), 1);
assert_eq!(code_raw_col_to_rendered_col(" let x = 1;", false, 10), 7);
}
#[test]
fn cols_inside_the_stripped_indent_collapse_onto_the_first_content_cell() {
for raw_col in 0..=4 {
assert_eq!(
code_raw_col_to_rendered_col(" code", false, raw_col),
CODE_PAD_COLS,
);
}
}
#[test]
fn code_indent_strip_stops_at_four_spaces() {
assert_eq!(code_indent_strip_chars(" deep", false), 4);
assert_eq!(code_indent_strip_chars("\tcode", false), 1);
assert_eq!(code_indent_strip_chars(" two", false), 2);
assert_eq!(code_indent_strip_chars("none", false), 0);
assert_eq!(code_indent_strip_chars(" indented", true), 0);
}
#[test]
fn col_round_trips_fenced() {
let raw = "fn main() {}";
for raw_col in 0..=raw.chars().count() {
let rendered = code_raw_col_to_rendered_col(raw, true, raw_col);
assert_eq!(code_rendered_col_to_raw_col(raw, true, rendered), raw_col);
}
}
#[test]
fn col_round_trips_indented() {
let raw = " fn main() {}";
for raw_col in 4..=raw.chars().count() {
let rendered = code_raw_col_to_rendered_col(raw, false, raw_col);
assert_eq!(code_rendered_col_to_raw_col(raw, false, rendered), raw_col);
}
}
#[test]
fn click_on_the_pad_cell_lands_on_the_first_content_char() {
assert_eq!(code_rendered_col_to_raw_col("code", true, 0), 0);
assert_eq!(code_rendered_col_to_raw_col(" code", false, 0), 4);
}
#[test]
fn click_in_the_trailing_fill_clamps_to_end_of_line() {
assert_eq!(code_rendered_col_to_raw_col("code", true, 60), 4);
}
#[test]
fn fence_rows_are_the_first_and_last_line_of_a_fenced_block() {
let lines = ["```rust", "let x = 1;", "```"];
assert!(is_code_fence_row(true, 0, &lines));
assert!(is_code_fence_row(true, 2, &lines));
assert!(!is_code_fence_row(true, 1, &lines));
}
#[test]
fn an_indented_block_has_no_fence_rows() {
let lines = [" a", " b", " c"];
assert!(!is_code_fence_row(false, 0, &lines));
assert!(!is_code_fence_row(false, 2, &lines));
}
#[test]
fn an_unclosed_fence_has_no_closing_fence_row() {
let lines = ["```rust", "let x = 1;"];
assert!(is_code_fence_row(true, 0, &lines));
assert!(!is_code_fence_row(true, 1, &lines));
assert!(!line_allows_raw_reveal(
Some(&Block::CodeBlock {
language: Some("rust".into()),
content: "let x = 1;\n".into(),
fenced: true,
}),
1,
&lines,
));
}
#[test]
fn closing_fence_accepts_tildes_and_longer_runs_but_not_prose() {
assert!(is_closing_fence_line("```"));
assert!(is_closing_fence_line("~~~"));
assert!(is_closing_fence_line("`````"));
assert!(is_closing_fence_line(" ``` "));
assert!(!is_closing_fence_line("```rust"));
assert!(!is_closing_fence_line("let x = 1;"));
assert!(!is_closing_fence_line("``"));
assert!(!is_closing_fence_line(""));
}
#[test]
fn reveal_rule_matches_the_rendered_view_gate() {
let fenced = Block::CodeBlock {
language: Some("rust".into()),
content: "x\n".into(),
fenced: true,
};
let indented = Block::CodeBlock {
language: None,
content: "x\n".into(),
fenced: false,
};
let para = Block::Paragraph { inlines: vec![] };
let fenced_lines = ["```rust", "x", "```"];
let indented_lines = [" x", " y"];
let prose_lines = ["hello"];
assert!(line_allows_raw_reveal(Some(&fenced), 0, &fenced_lines));
assert!(line_allows_raw_reveal(Some(&fenced), 2, &fenced_lines));
assert!(!line_allows_raw_reveal(Some(&fenced), 1, &fenced_lines));
assert!(!line_allows_raw_reveal(Some(&indented), 0, &indented_lines));
assert!(line_allows_raw_reveal(Some(¶), 0, &prose_lines));
assert!(line_allows_raw_reveal(None, 0, &prose_lines));
}
}