mod helpers;
pub use helpers::{
apply_filename_style, extract_first_line_h1, has_non_portable_chars, prettify_filename,
sanitize_filename, slugify, slugify_title, sync_h1_in_body,
};
use crate::date;
use crate::error::{DiaryxError, Result};
use crate::fs::{AsyncFileSystem, FileSystem};
use crate::link_parser;
use crate::yaml_value::YamlValue;
use indexmap::IndexMap;
use std::path::{Path, PathBuf};
pub struct DiaryxApp<FS: AsyncFileSystem> {
fs: FS,
}
pub struct DiaryxAppSync<FS: FileSystem> {
fs: FS,
}
impl<FS: AsyncFileSystem> DiaryxApp<FS> {
pub fn new(fs: FS) -> Self {
Self { fs }
}
pub fn fs(&self) -> &FS {
&self.fs
}
pub async fn create_entry(&self, path: &str) -> Result<()> {
let content = format!("---\ntitle: {}\n---\n\n# {}\n\n", path, path);
self.fs
.create_new(std::path::Path::new(path), &content)
.await
.map_err(|e| DiaryxError::FileWrite {
path: PathBuf::from(path),
source: e,
})?;
Ok(())
}
async fn parse_file(&self, path: &str) -> Result<(IndexMap<String, YamlValue>, String)> {
let path_buf = PathBuf::from(path);
let content = self
.fs
.read_to_string(std::path::Path::new(path))
.await
.map_err(|e| DiaryxError::FileRead {
path: path_buf.clone(),
source: e,
})?;
if !content.starts_with("---\n") && !content.starts_with("---\r\n") {
return Err(DiaryxError::NoFrontmatter(path_buf));
}
let rest = &content[4..]; let end_idx = rest
.find("\n---\n")
.or_else(|| rest.find("\n---\r\n"))
.ok_or_else(|| DiaryxError::NoFrontmatter(path_buf.clone()))?;
let frontmatter_str = &rest[..end_idx];
let body = &rest[end_idx + 5..];
let frontmatter: IndexMap<String, YamlValue> = serde_yaml::from_str(frontmatter_str)?;
Ok((frontmatter, body.to_string()))
}
async fn parse_file_or_create_frontmatter(
&self,
path: &str,
) -> Result<(IndexMap<String, YamlValue>, String)> {
let path_buf = PathBuf::from(path);
let content = match self.fs.read_to_string(std::path::Path::new(path)).await {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok((IndexMap::new(), String::new()));
}
Err(e) => {
return Err(DiaryxError::FileRead {
path: path_buf,
source: e,
});
}
};
if !content.starts_with("---\n") && !content.starts_with("---\r\n") {
return Ok((IndexMap::new(), content));
}
let rest = &content[4..]; let end_idx = rest.find("\n---\n").or_else(|| rest.find("\n---\r\n"));
match end_idx {
Some(idx) => {
let frontmatter_str = &rest[..idx];
let body = &rest[idx + 5..];
let frontmatter: IndexMap<String, YamlValue> =
serde_yaml::from_str(frontmatter_str)?;
Ok((frontmatter, body.to_string()))
}
None => {
Ok((IndexMap::new(), content))
}
}
}
async fn reconstruct_file(
&self,
path: &str,
frontmatter: &IndexMap<String, YamlValue>,
body: &str,
) -> Result<()> {
let yaml_str = serde_yaml::to_string(frontmatter)?;
let content = format!("---\n{}---\n{}", yaml_str, body);
self.fs
.write_file(std::path::Path::new(path), &content)
.await
.map_err(|e| DiaryxError::FileWrite {
path: PathBuf::from(path),
source: e,
})?;
Ok(())
}
pub async fn set_frontmatter_property(
&self,
path: &str,
key: &str,
value: YamlValue,
) -> Result<()> {
let (mut frontmatter, body) = self.parse_file_or_create_frontmatter(path).await?;
frontmatter.insert(key.to_string(), value);
self.reconstruct_file(path, &frontmatter, &body).await
}
pub async fn remove_frontmatter_property(&self, path: &str, key: &str) -> Result<()> {
match self.parse_file(path).await {
Ok((mut frontmatter, body)) => {
frontmatter.shift_remove(key);
self.reconstruct_file(path, &frontmatter, &body).await
}
Err(DiaryxError::NoFrontmatter(_)) => Ok(()), Err(e) => Err(e),
}
}
pub async fn rename_frontmatter_property(
&self,
path: &str,
old_key: &str,
new_key: &str,
) -> Result<bool> {
let (frontmatter, body) = match self.parse_file(path).await {
Ok(result) => result,
Err(DiaryxError::NoFrontmatter(_)) => return Ok(false), Err(e) => return Err(e),
};
if !frontmatter.contains_key(old_key) {
return Ok(false);
}
let mut result: IndexMap<String, YamlValue> = IndexMap::new();
for (k, v) in frontmatter {
if k == old_key {
result.insert(new_key.to_string(), v);
} else {
result.insert(k, v);
}
}
self.reconstruct_file(path, &result, &body).await?;
Ok(true)
}
pub async fn get_frontmatter_property(
&self,
path: &str,
key: &str,
) -> Result<Option<YamlValue>> {
match self.parse_file(path).await {
Ok((frontmatter, _)) => Ok(frontmatter.get(key).cloned()),
Err(DiaryxError::NoFrontmatter(_)) => Ok(None), Err(e) => Err(e),
}
}
pub async fn get_all_frontmatter(&self, path: &str) -> Result<IndexMap<String, YamlValue>> {
match self.parse_file(path).await {
Ok((frontmatter, _)) => Ok(frontmatter),
Err(DiaryxError::NoFrontmatter(_)) => Ok(IndexMap::new()), Err(e) => Err(e),
}
}
pub async fn get_content(&self, path: &str) -> Result<String> {
let (_, body) = self.parse_file_or_create_frontmatter(path).await?;
Ok(body)
}
pub async fn set_content(&self, path: &str, content: &str) -> Result<()> {
let (frontmatter, _) = self.parse_file_or_create_frontmatter(path).await?;
self.reconstruct_file(path, &frontmatter, content).await
}
pub async fn clear_content(&self, path: &str) -> Result<()> {
self.set_content(path, "").await
}
pub async fn touch_updated(&self, path: &str) -> Result<()> {
let timestamp = date::current_local_timestamp_rfc3339();
self.set_frontmatter_property(path, "updated", YamlValue::String(timestamp))
.await
}
pub async fn save_content(&self, path: &str, content: &str) -> Result<()> {
self.save_content_with_options(path, content, true).await
}
pub async fn save_content_with_options(
&self,
path: &str,
content: &str,
auto_update_timestamp: bool,
) -> Result<()> {
self.set_content(path, content).await?;
if auto_update_timestamp {
self.touch_updated(path).await?;
}
Ok(())
}
pub async fn append_content(&self, path: &str, content: &str) -> Result<()> {
let (frontmatter, body) = self.parse_file_or_create_frontmatter(path).await?;
let new_body = if body.is_empty() {
content.to_string()
} else if body.ends_with('\n') {
format!("{}{}", body, content)
} else {
format!("{}\n{}", body, content)
};
self.reconstruct_file(path, &frontmatter, &new_body).await
}
pub async fn prepend_content(&self, path: &str, content: &str) -> Result<()> {
let (frontmatter, body) = self.parse_file_or_create_frontmatter(path).await?;
let new_body = if body.is_empty() {
content.to_string()
} else if content.ends_with('\n') {
format!("{}{}", content, body)
} else {
format!("{}\n{}", content, body)
};
self.reconstruct_file(path, &frontmatter, &new_body).await
}
pub async fn add_attachment(&self, path: &str, attachment_path: &str) -> Result<()> {
let (mut frontmatter, body) = self.parse_file_or_create_frontmatter(path).await?;
let parsed_target = link_parser::parse_link(attachment_path);
let target_canonical = link_parser::to_canonical(&parsed_target, Path::new(path));
let attachments = frontmatter
.entry("attachments".to_string())
.or_insert(YamlValue::Sequence(vec![]));
if let YamlValue::Sequence(list) = attachments {
let exists = list.iter().any(|item| {
if let YamlValue::String(existing) = item {
let parsed_existing = link_parser::parse_link(existing);
return link_parser::to_canonical(&parsed_existing, Path::new(path))
== target_canonical;
}
false
});
if !exists {
list.push(YamlValue::String(attachment_path.to_string()));
}
}
self.reconstruct_file(path, &frontmatter, &body).await
}
pub async fn remove_attachment(&self, path: &str, attachment_path: &str) -> Result<()> {
let (mut frontmatter, body) = match self.parse_file(path).await {
Ok(result) => result,
Err(DiaryxError::NoFrontmatter(_)) => return Ok(()),
Err(e) => return Err(e),
};
let parsed_target = link_parser::parse_link(attachment_path);
let target_canonical = link_parser::to_canonical(&parsed_target, Path::new(path));
if let Some(YamlValue::Sequence(list)) = frontmatter.get_mut("attachments") {
list.retain(|item| {
if let YamlValue::String(s) = item {
let parsed_existing = link_parser::parse_link(s);
link_parser::to_canonical(&parsed_existing, Path::new(path)) != target_canonical
} else {
true
}
});
if list.is_empty() {
frontmatter.shift_remove("attachments");
}
}
self.reconstruct_file(path, &frontmatter, &body).await
}
pub async fn get_attachments(&self, path: &str) -> Result<Vec<String>> {
let (frontmatter, _) = match self.parse_file(path).await {
Ok(result) => result,
Err(DiaryxError::NoFrontmatter(_)) => return Ok(vec![]),
Err(e) => return Err(e),
};
match frontmatter.get("attachments") {
Some(YamlValue::Sequence(list)) => Ok(list
.iter()
.filter_map(|v| {
if let YamlValue::String(s) = v {
Some(s.clone())
} else {
None
}
})
.collect()),
_ => Ok(vec![]),
}
}
}
impl<FS: FileSystem> DiaryxAppSync<FS> {
pub fn new(fs: FS) -> Self {
Self { fs }
}
pub fn fs(&self) -> &FS {
&self.fs
}
pub fn create_entry(&self, path: &str) -> Result<()> {
let content = format!("---\ntitle: {}\n---\n\n# {}\n\n", path, path);
self.fs.create_new(std::path::Path::new(path), &content)?;
Ok(())
}
fn parse_file(&self, path: &str) -> Result<(IndexMap<String, YamlValue>, String)> {
let path_buf = PathBuf::from(path);
let content = self
.fs
.read_to_string(std::path::Path::new(path))
.map_err(|e| DiaryxError::FileRead {
path: path_buf.clone(),
source: e,
})?;
if !content.starts_with("---\n") && !content.starts_with("---\r\n") {
return Err(DiaryxError::NoFrontmatter(path_buf));
}
let rest = &content[4..]; let end_idx = rest
.find("\n---\n")
.or_else(|| rest.find("\n---\r\n"))
.ok_or_else(|| DiaryxError::NoFrontmatter(path_buf.clone()))?;
let frontmatter_str = &rest[..end_idx];
let body = &rest[end_idx + 5..];
let frontmatter: IndexMap<String, YamlValue> = serde_yaml::from_str(frontmatter_str)?;
Ok((frontmatter, body.to_string()))
}
fn parse_file_or_create_frontmatter(
&self,
path: &str,
) -> Result<(IndexMap<String, YamlValue>, String)> {
let path_buf = PathBuf::from(path);
let content = match self.fs.read_to_string(std::path::Path::new(path)) {
Ok(c) => c,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok((IndexMap::new(), String::new()));
}
Err(e) => {
return Err(DiaryxError::FileRead {
path: path_buf,
source: e,
});
}
};
if !content.starts_with("---\n") && !content.starts_with("---\r\n") {
return Ok((IndexMap::new(), content));
}
let rest = &content[4..]; let end_idx = rest.find("\n---\n").or_else(|| rest.find("\n---\r\n"));
match end_idx {
Some(idx) => {
let frontmatter_str = &rest[..idx];
let body = &rest[idx + 5..];
let frontmatter: IndexMap<String, YamlValue> =
serde_yaml::from_str(frontmatter_str)?;
Ok((frontmatter, body.to_string()))
}
None => {
Ok((IndexMap::new(), content))
}
}
}
fn reconstruct_file(
&self,
path: &str,
frontmatter: &IndexMap<String, YamlValue>,
body: &str,
) -> Result<()> {
let yaml_str = serde_yaml::to_string(frontmatter)?;
let content = format!("---\n{}---\n{}", yaml_str, body);
self.fs
.write_file(std::path::Path::new(path), &content)
.map_err(|e| DiaryxError::FileWrite {
path: PathBuf::from(path),
source: e,
})?;
Ok(())
}
pub fn set_frontmatter_property(&self, path: &str, key: &str, value: YamlValue) -> Result<()> {
let (mut frontmatter, body) = self.parse_file_or_create_frontmatter(path)?;
frontmatter.insert(key.to_string(), value);
self.reconstruct_file(path, &frontmatter, &body)
}
pub fn remove_frontmatter_property(&self, path: &str, key: &str) -> Result<()> {
match self.parse_file(path) {
Ok((mut frontmatter, body)) => {
frontmatter.shift_remove(key);
self.reconstruct_file(path, &frontmatter, &body)
}
Err(DiaryxError::NoFrontmatter(_)) => Ok(()), Err(e) => Err(e),
}
}
pub fn rename_frontmatter_property(
&self,
path: &str,
old_key: &str,
new_key: &str,
) -> Result<bool> {
let (frontmatter, body) = match self.parse_file(path) {
Ok(result) => result,
Err(DiaryxError::NoFrontmatter(_)) => return Ok(false), Err(e) => return Err(e),
};
if !frontmatter.contains_key(old_key) {
return Ok(false);
}
let mut result: IndexMap<String, YamlValue> = IndexMap::new();
for (k, v) in frontmatter {
if k == old_key {
result.insert(new_key.to_string(), v);
} else {
result.insert(k, v);
}
}
self.reconstruct_file(path, &result, &body)?;
Ok(true)
}
pub fn get_content(&self, path: &str) -> Result<String> {
match self.parse_file_or_create_frontmatter(path) {
Ok((_frontmatter, body)) => Ok(body),
Err(e) => Err(e),
}
}
pub fn set_content(&self, path: &str, content: &str) -> Result<()> {
let (frontmatter, _old_body) = self.parse_file_or_create_frontmatter(path)?;
self.reconstruct_file(path, &frontmatter, content)
}
pub fn clear_content(&self, path: &str) -> Result<()> {
self.set_content(path, "")
}
pub fn append_content(&self, path: &str, content: &str) -> Result<()> {
let (frontmatter, mut body) = self.parse_file_or_create_frontmatter(path)?;
if body.is_empty() {
body = content.to_string();
} else if body.ends_with('\n') {
body.push_str(content);
} else {
body.push('\n');
body.push_str(content);
}
self.reconstruct_file(path, &frontmatter, &body)
}
pub fn prepend_content(&self, path: &str, content: &str) -> Result<()> {
let (frontmatter, body) = self.parse_file_or_create_frontmatter(path)?;
let new_body = if body.is_empty() {
content.to_string()
} else if content.ends_with('\n') {
format!("{}{}", content, body)
} else {
format!("{}\n{}", content, body)
};
self.reconstruct_file(path, &frontmatter, &new_body)
}
pub fn get_frontmatter_property(&self, path: &str, key: &str) -> Result<Option<YamlValue>> {
match self.parse_file(path) {
Ok((frontmatter, _)) => Ok(frontmatter.get(key).cloned()),
Err(DiaryxError::NoFrontmatter(_)) => Ok(None), Err(e) => Err(e),
}
}
pub fn get_all_frontmatter(&self, path: &str) -> Result<IndexMap<String, YamlValue>> {
match self.parse_file(path) {
Ok((frontmatter, _)) => Ok(frontmatter),
Err(DiaryxError::NoFrontmatter(_)) => Ok(IndexMap::new()), Err(e) => Err(e),
}
}
pub fn add_attachment(&self, path: &str, attachment_path: &str) -> Result<()> {
let (mut frontmatter, body) = self.parse_file_or_create_frontmatter(path)?;
let parsed_target = link_parser::parse_link(attachment_path);
let target_canonical = link_parser::to_canonical(&parsed_target, Path::new(path));
let attachments = frontmatter
.entry("attachments".to_string())
.or_insert(YamlValue::Sequence(vec![]));
if let YamlValue::Sequence(list) = attachments {
let exists = list.iter().any(|item| {
if let YamlValue::String(existing) = item {
let parsed_existing = link_parser::parse_link(existing);
return link_parser::to_canonical(&parsed_existing, Path::new(path))
== target_canonical;
}
false
});
if !exists {
list.push(YamlValue::String(attachment_path.to_string()));
}
}
self.reconstruct_file(path, &frontmatter, &body)
}
pub fn remove_attachment(&self, path: &str, attachment_path: &str) -> Result<()> {
let (mut frontmatter, body) = match self.parse_file(path) {
Ok(result) => result,
Err(DiaryxError::NoFrontmatter(_)) => return Ok(()),
Err(e) => return Err(e),
};
let parsed_target = link_parser::parse_link(attachment_path);
let target_canonical = link_parser::to_canonical(&parsed_target, Path::new(path));
if let Some(YamlValue::Sequence(list)) = frontmatter.get_mut("attachments") {
list.retain(|item| {
if let YamlValue::String(s) = item {
let parsed_existing = link_parser::parse_link(s);
link_parser::to_canonical(&parsed_existing, Path::new(path)) != target_canonical
} else {
true
}
});
if list.is_empty() {
frontmatter.shift_remove("attachments");
}
}
self.reconstruct_file(path, &frontmatter, &body)
}
pub fn get_attachments(&self, path: &str) -> Result<Vec<String>> {
let (frontmatter, _) = match self.parse_file(path) {
Ok(result) => result,
Err(DiaryxError::NoFrontmatter(_)) => return Ok(vec![]),
Err(e) => return Err(e),
};
match frontmatter.get("attachments") {
Some(YamlValue::Sequence(list)) => Ok(list
.iter()
.filter_map(|v| {
if let YamlValue::String(s) = v {
Some(s.clone())
} else {
None
}
})
.collect()),
_ => Ok(vec![]),
}
}
pub fn resolve_attachment(
&self,
entry_path: &str,
attachment_name: &str,
) -> Result<Option<PathBuf>> {
use crate::workspace::IndexFrontmatter;
let entry_path = Path::new(entry_path);
let entry_dir = entry_path.parent().unwrap_or(Path::new("."));
let content = match self.fs.read_to_string(entry_path) {
Ok(c) => c,
Err(_) => return Ok(None),
};
if !content.starts_with("---\n") && !content.starts_with("---\r\n") {
return Ok(None);
}
let rest = &content[4..];
let end_idx = match rest.find("\n---\n").or_else(|| rest.find("\n---\r\n")) {
Some(idx) => idx,
None => return Ok(None),
};
let frontmatter_str = &rest[..end_idx];
let frontmatter: IndexFrontmatter = match serde_yaml::from_str(frontmatter_str) {
Ok(fm) => fm,
Err(_) => return Ok(None),
};
for att_path in frontmatter.attachments_list() {
let parsed = link_parser::parse_link(att_path);
let resolved = entry_dir.join(&parsed.path);
if resolved.file_name().map(|n| n.to_string_lossy()) == Some(attachment_name.into())
&& self.fs.exists(&resolved)
{
return Ok(Some(resolved));
}
if parsed.path == attachment_name
|| parsed.path.ends_with(&format!("/{}", attachment_name))
{
let resolved = entry_dir.join(&parsed.path);
if self.fs.exists(&resolved) {
return Ok(Some(resolved));
}
}
}
if let Some(ref parent_rel) = frontmatter.part_of {
let parsed_parent = link_parser::parse_link(parent_rel);
let canonical_parent = link_parser::to_canonical(&parsed_parent, entry_path);
let parent_path = PathBuf::from(canonical_parent);
if self.fs.exists(&parent_path) {
return self.resolve_attachment(&parent_path.to_string_lossy(), attachment_name);
}
}
Ok(None)
}
pub fn sort_frontmatter(&self, path: &str, pattern: Option<&str>) -> Result<()> {
let (frontmatter, body) = match self.parse_file(path) {
Ok(result) => result,
Err(DiaryxError::NoFrontmatter(_)) => return Ok(()), Err(e) => return Err(e),
};
let sorted = match pattern {
Some(p) => self.sort_by_pattern(frontmatter, p),
None => self.sort_alphabetically(frontmatter),
};
self.reconstruct_file(path, &sorted, &body)
}
fn sort_alphabetically(
&self,
frontmatter: IndexMap<String, YamlValue>,
) -> IndexMap<String, YamlValue> {
let mut pairs: Vec<_> = frontmatter.into_iter().collect();
pairs.sort_by(|a, b| a.0.cmp(&b.0));
pairs.into_iter().collect()
}
fn sort_by_pattern(
&self,
frontmatter: IndexMap<String, YamlValue>,
pattern: &str,
) -> IndexMap<String, YamlValue> {
let priority_keys: Vec<&str> = pattern.split(',').map(|s| s.trim()).collect();
let mut result = IndexMap::new();
let mut remaining: IndexMap<String, YamlValue> = frontmatter;
for key in &priority_keys {
if *key == "*" {
let mut rest: Vec<_> = remaining.drain(..).collect();
rest.sort_by(|a, b| a.0.cmp(&b.0));
for (k, v) in rest {
result.insert(k, v);
}
break;
} else if let Some(value) = remaining.shift_remove(*key) {
result.insert(key.to_string(), value);
}
}
if !remaining.is_empty() {
let mut rest: Vec<_> = remaining.drain(..).collect();
rest.sort_by(|a, b| a.0.cmp(&b.0));
for (k, v) in rest {
result.insert(k, v);
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fs::SyncToAsyncFs;
use crate::test_utils::MockFileSystem;
#[test]
fn test_get_content() {
let fs = MockFileSystem::new().with_file("test.md", "---\ntitle: Test\n---\nHello, world!");
let app = DiaryxApp::new(SyncToAsyncFs::new(fs));
let content = crate::fs::block_on_test(app.get_content("test.md")).unwrap();
assert_eq!(content, "Hello, world!");
}
#[test]
fn test_get_content_empty_body() {
let fs = MockFileSystem::new().with_file("test.md", "---\ntitle: Test\n---\n");
let app = DiaryxApp::new(SyncToAsyncFs::new(fs));
let content = crate::fs::block_on_test(app.get_content("test.md")).unwrap();
assert_eq!(content, "");
}
#[test]
fn test_get_content_no_frontmatter() {
let fs = MockFileSystem::new().with_file("test.md", "Just plain content");
let app = DiaryxApp::new(SyncToAsyncFs::new(fs));
let content = crate::fs::block_on_test(app.get_content("test.md")).unwrap();
assert_eq!(content, "Just plain content");
}
#[test]
fn test_set_content() {
let fs = MockFileSystem::new().with_file("test.md", "---\ntitle: Test\n---\nOld content");
let app = DiaryxApp::new(SyncToAsyncFs::new(fs.clone()));
crate::fs::block_on_test(app.set_content("test.md", "New content")).unwrap();
let result = fs.get_content("test.md").unwrap();
assert!(result.contains("title: Test"));
assert!(result.contains("New content"));
assert!(!result.contains("Old content"));
}
#[test]
fn test_set_content_preserves_frontmatter() {
let fs = MockFileSystem::new().with_file(
"test.md",
"---\ntitle: My Title\ndescription: A description\n---\nOld",
);
let app = DiaryxApp::new(SyncToAsyncFs::new(fs.clone()));
crate::fs::block_on_test(app.set_content("test.md", "New body")).unwrap();
let result = fs.get_content("test.md").unwrap();
assert!(result.contains("title: My Title"));
assert!(result.contains("description: A description"));
assert!(result.contains("New body"));
}
#[test]
fn test_clear_content() {
let fs =
MockFileSystem::new().with_file("test.md", "---\ntitle: Test\n---\nSome content here");
let app = DiaryxApp::new(SyncToAsyncFs::new(fs.clone()));
crate::fs::block_on_test(app.clear_content("test.md")).unwrap();
let result = fs.get_content("test.md").unwrap();
assert!(result.contains("title: Test"));
assert!(result.ends_with("---\n"));
}
#[test]
fn test_append_content() {
let fs = MockFileSystem::new().with_file("test.md", "---\ntitle: Test\n---\nFirst line");
let app = DiaryxApp::new(SyncToAsyncFs::new(fs.clone()));
crate::fs::block_on_test(app.append_content("test.md", "Second line")).unwrap();
let result = fs.get_content("test.md").unwrap();
assert!(result.contains("First line"));
assert!(result.contains("Second line"));
let first_pos = result.find("First line").unwrap();
let second_pos = result.find("Second line").unwrap();
assert!(second_pos > first_pos);
}
#[test]
fn test_append_content_adds_newline() {
let fs = MockFileSystem::new()
.with_file("test.md", "---\ntitle: Test\n---\nNo trailing newline");
let app = DiaryxApp::new(SyncToAsyncFs::new(fs.clone()));
crate::fs::block_on_test(app.append_content("test.md", "Appended")).unwrap();
let content = crate::fs::block_on_test(app.get_content("test.md")).unwrap();
assert!(content.contains("No trailing newline\nAppended"));
}
#[test]
fn test_append_content_to_empty_body() {
let fs = MockFileSystem::new().with_file("test.md", "---\ntitle: Test\n---\n");
let app = DiaryxApp::new(SyncToAsyncFs::new(fs.clone()));
crate::fs::block_on_test(app.append_content("test.md", "New content")).unwrap();
let content = crate::fs::block_on_test(app.get_content("test.md")).unwrap();
assert_eq!(content, "New content");
}
#[test]
fn test_prepend_content() {
let fs =
MockFileSystem::new().with_file("test.md", "---\ntitle: Test\n---\nExisting content");
let app = DiaryxApp::new(SyncToAsyncFs::new(fs.clone()));
crate::fs::block_on_test(app.prepend_content("test.md", "# Header")).unwrap();
let result = fs.get_content("test.md").unwrap();
assert!(result.contains("# Header"));
assert!(result.contains("Existing content"));
let header_pos = result.find("# Header").unwrap();
let existing_pos = result.find("Existing content").unwrap();
assert!(header_pos < existing_pos);
}
#[test]
fn test_prepend_content_adds_newline() {
let fs = MockFileSystem::new().with_file("test.md", "---\ntitle: Test\n---\nExisting");
let app = DiaryxApp::new(SyncToAsyncFs::new(fs.clone()));
crate::fs::block_on_test(app.prepend_content("test.md", "Prepended")).unwrap();
let content = crate::fs::block_on_test(app.get_content("test.md")).unwrap();
assert!(content.contains("Prepended\nExisting"));
}
#[test]
fn test_prepend_content_to_empty_body() {
let fs = MockFileSystem::new().with_file("test.md", "---\ntitle: Test\n---\n");
let app = DiaryxApp::new(SyncToAsyncFs::new(fs.clone()));
crate::fs::block_on_test(app.prepend_content("test.md", "New content")).unwrap();
let content = crate::fs::block_on_test(app.get_content("test.md")).unwrap();
assert_eq!(content, "New content");
}
#[test]
fn test_content_operations_on_nonexistent_file() {
let fs = MockFileSystem::new();
let app = DiaryxApp::new(SyncToAsyncFs::new(fs.clone()));
crate::fs::block_on_test(app.set_content("new.md", "Content")).unwrap();
let result = fs.get_content("new.md").unwrap();
assert!(result.contains("Content"));
}
#[test]
fn test_multiple_content_operations() {
let fs = MockFileSystem::new().with_file("test.md", "---\ntitle: Test\n---\n");
let app = DiaryxApp::new(SyncToAsyncFs::new(fs.clone()));
crate::fs::block_on_test(app.append_content("test.md", "Line 1")).unwrap();
crate::fs::block_on_test(app.append_content("test.md", "Line 2")).unwrap();
crate::fs::block_on_test(app.prepend_content("test.md", "# Title")).unwrap();
let content = crate::fs::block_on_test(app.get_content("test.md")).unwrap();
assert!(content.starts_with("# Title"));
assert!(content.contains("Line 1"));
assert!(content.contains("Line 2"));
}
}