pub(crate) fn lines_of(content: &str) -> Vec<String> {
let mut lines: Vec<String> = content.lines().map(String::from).collect();
if lines.is_empty() {
lines.push(String::new());
}
lines
}
pub(crate) fn join_lines(lines: &[String]) -> String {
let mut out = lines.join("\n");
out.push('\n');
out
}
pub(crate) fn split_line(lines: &mut Vec<String>, i: usize, caret: usize) -> usize {
let i = i.min(lines.len().saturating_sub(1));
let line = std::mem::take(&mut lines[i]);
let byte = line
.char_indices()
.nth(caret)
.map(|(b, _)| b)
.unwrap_or(line.len());
let (head, tail) = line.split_at(byte);
lines[i] = head.to_string();
lines.insert(i + 1, tail.to_string());
i + 1
}
pub(crate) fn join_with_previous(lines: &mut Vec<String>, i: usize) -> Option<(usize, usize)> {
if i == 0 || i >= lines.len() {
return None;
}
let tail = lines.remove(i);
let caret = lines[i - 1].chars().count();
lines[i - 1].push_str(&tail);
Some((i - 1, caret))
}
pub(crate) const STARTER_STORY: &str = "---
title: Untitled Story
characters:
narrator: Narrator
---
# start
**narrator:** Once upon a time...
The story begins here.
";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lines_round_trip_through_join() {
let content = "a\nb\n\nc\n";
let lines = lines_of(content);
assert_eq!(lines, ["a", "b", "", "c"]);
assert_eq!(join_lines(&lines), content);
}
#[test]
fn empty_content_still_yields_an_editable_line() {
assert_eq!(lines_of(""), [""]);
assert_eq!(join_lines(&lines_of("")), "\n");
}
#[test]
fn split_line_moves_the_tail_below_the_caret() {
let mut lines = vec!["hello world".to_string()];
let cur = split_line(&mut lines, 0, 5);
assert_eq!(cur, 1);
assert_eq!(lines, ["hello", " world"]);
let cur = split_line(&mut lines, 1, 99);
assert_eq!(cur, 2);
assert_eq!(lines, ["hello", " world", ""]);
}
#[test]
fn split_line_is_utf8_safe() {
let mut lines = vec!["héllo".to_string()];
split_line(&mut lines, 0, 2);
assert_eq!(lines, ["hé", "llo"]);
}
#[test]
fn join_with_previous_lands_the_caret_at_the_seam() {
let mut lines = vec!["hé".to_string(), "llo".to_string()];
let (cur, caret) = join_with_previous(&mut lines, 1).unwrap();
assert_eq!((cur, caret), (0, 2), "caret in characters, not bytes");
assert_eq!(lines, ["héllo"]);
assert_eq!(join_with_previous(&mut lines, 0), None, "no previous line");
}
#[test]
fn starter_story_parses() {
concinnity_cook::build_only::validate_story_source(STARTER_STORY)
.expect("the starter story template parses");
}
}