use crate::types::Metadata;
use serde_yaml_ng::Value as YamlValue;
pub fn extract_frontmatter(content: &str) -> (Option<YamlValue>, String) {
if !content.starts_with("---") {
return (None, content.to_string());
}
let rest = &content[3..];
let mut end_pos = None;
let mut search_start = 0;
while let Some(pos) = rest[search_start..].find('\n') {
let absolute_pos = search_start + pos;
let after_newline = absolute_pos + 1;
if after_newline >= rest.len() {
break;
}
let remaining = &rest[after_newline..];
if remaining.starts_with("---") || remaining.starts_with("...") {
let delimiter_end = after_newline + 3;
if delimiter_end >= rest.len() || rest.as_bytes()[delimiter_end] == b'\n' {
end_pos = Some(absolute_pos);
break;
}
}
search_start = after_newline;
}
if let Some(end) = end_pos {
let frontmatter_str = &rest[..end];
let after_delimiter = end + 1; let remaining_start = if after_delimiter + 3 < rest.len() {
let after_delim = after_delimiter + 3;
if after_delim < rest.len() && rest.as_bytes()[after_delim] == b'\n' {
after_delim + 1
} else {
after_delim
}
} else {
rest.len()
};
let remaining = if remaining_start < rest.len() {
&rest[remaining_start..]
} else {
""
};
match serde_yaml_ng::from_str::<YamlValue>(frontmatter_str) {
Ok(value) => (Some(value), remaining.to_string()),
Err(_) => (None, content.to_string()),
}
} else {
(None, content.to_string())
}
}
pub fn extract_metadata_from_yaml(yaml: &YamlValue) -> Metadata {
let mut metadata = Metadata::default();
if let Some(title) = yaml.get("title").and_then(|v| v.as_str())
&& metadata.title.is_none()
{
metadata.title = Some(title.to_string());
}
if let Some(author) = yaml.get("author").and_then(|v| v.as_str())
&& metadata.created_by.is_none()
{
metadata.created_by = Some(author.to_string());
}
if let Some(date) = yaml.get("date").and_then(|v| v.as_str()) {
metadata.created_at = Some(date.to_string());
}
if let Some(keywords) = yaml.get("keywords") {
match keywords {
YamlValue::String(s) => {
if metadata.keywords.is_none() {
metadata.keywords = Some(s.split(',').map(|k| k.trim().to_string()).collect());
}
}
YamlValue::Sequence(seq) => {
let kw_vec: Vec<String> = seq.iter().filter_map(|v| v.as_str()).map(|s| s.to_string()).collect();
if metadata.keywords.is_none() {
metadata.keywords = Some(kw_vec);
}
}
_ => {}
}
}
if let Some(description) = yaml.get("description").and_then(|v| v.as_str()) {
metadata.subject = Some(description.to_string());
}
if let Some(abstract_val) = yaml.get("abstract").and_then(|v| v.as_str()) {
metadata.abstract_text = Some(abstract_val.to_string());
}
if let Some(subject) = yaml.get("subject").and_then(|v| v.as_str()) {
metadata.subject = Some(subject.to_string());
}
if let Some(category) = yaml.get("category").and_then(|v| v.as_str()) {
metadata.category = Some(category.to_string());
}
if let Some(tags) = yaml.get("tags") {
match tags {
YamlValue::String(s) => {
metadata.tags = Some(s.split(',').map(|t| t.trim().to_string()).collect());
}
YamlValue::Sequence(seq) => {
let tags_vec: Vec<String> = seq.iter().filter_map(|v| v.as_str()).map(|s| s.to_string()).collect();
metadata.tags = Some(tags_vec);
}
_ => {}
}
}
if let Some(language) = yaml.get("language").and_then(|v| v.as_str())
&& metadata.language.is_none()
{
metadata.language = Some(language.to_string());
}
if let Some(version) = yaml.get("version").and_then(|v| v.as_str()) {
metadata.document_version = Some(version.to_string());
}
metadata
}
pub fn extract_title_from_content(content: &str) -> Option<String> {
for line in content.lines() {
if let Some(heading) = line.strip_prefix("# ") {
return Some(heading.trim().to_string());
}
}
None
}
pub fn cells_to_markdown(cells: &[Vec<String>]) -> String {
if cells.is_empty() {
return String::new();
}
let mut md = String::new();
md.push('|');
for cell in &cells[0] {
md.push(' ');
md.push_str(cell);
md.push_str(" |");
}
md.push('\n');
md.push('|');
for _ in &cells[0] {
md.push_str(" --- |");
}
md.push('\n');
for row in &cells[1..] {
md.push('|');
for cell in row {
md.push(' ');
md.push_str(cell);
md.push_str(" |");
}
md.push('\n');
}
md
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frontmatter_basic() {
let content = "---\ntitle: Test\n---\n\n# Content";
let (yaml, remaining) = extract_frontmatter(content);
assert!(yaml.is_some());
assert!(remaining.contains("# Content"));
let metadata = extract_metadata_from_yaml(&yaml.unwrap());
assert_eq!(metadata.title.as_deref(), Some("Test"));
}
#[test]
fn test_frontmatter_with_dashes_in_content() {
let content = "---\ntitle: Test\ndescription: |\n This has ---\n in the middle\n---\n\n# Body";
let (yaml, remaining) = extract_frontmatter(content);
assert!(yaml.is_some());
assert!(remaining.contains("# Body"));
}
#[test]
fn test_frontmatter_with_dots_terminator() {
let content = "---\ntitle: Test\nauthor: John\n...\n\n# Content";
let (yaml, remaining) = extract_frontmatter(content);
assert!(yaml.is_some());
assert!(remaining.contains("# Content"));
let metadata = extract_metadata_from_yaml(&yaml.unwrap());
assert_eq!(metadata.title.as_deref(), Some("Test"));
}
#[test]
fn test_frontmatter_with_triple_dash_in_string() {
let content = "---\ntitle: \"Before --- After\"\nauthor: John\n---\n\n# Content";
let (yaml, remaining) = extract_frontmatter(content);
assert!(yaml.is_some());
assert!(remaining.contains("# Content"));
let metadata = extract_metadata_from_yaml(&yaml.unwrap());
assert_eq!(metadata.title.as_deref(), Some("Before --- After"));
}
#[test]
fn test_frontmatter_multiline_string_with_dashes() {
let content = "---\ntitle: Test\ndescription: |\n Line 1\n ---\n Line 2\n---\n\n# Body";
let (yaml, remaining) = extract_frontmatter(content);
assert!(yaml.is_some());
assert!(remaining.contains("# Body"));
let metadata = extract_metadata_from_yaml(&yaml.unwrap());
assert_eq!(metadata.title.as_deref(), Some("Test"));
}
#[test]
fn test_no_frontmatter() {
let content = "# Title\n\nContent without frontmatter";
let (yaml, remaining) = extract_frontmatter(content);
assert!(yaml.is_none());
assert_eq!(remaining, content);
}
#[test]
fn test_incomplete_frontmatter() {
let content = "---\ntitle: Test\nauthor: John\n\n# Content";
let (yaml, remaining) = extract_frontmatter(content);
assert!(yaml.is_none());
assert_eq!(remaining, content);
}
#[test]
fn test_extract_title_from_content() {
let content = "# My Document\n\nContent here";
assert_eq!(extract_title_from_content(content), Some("My Document".to_string()));
}
#[test]
fn test_extract_title_from_content_no_heading() {
let content = "Content without heading";
assert_eq!(extract_title_from_content(content), None);
}
#[test]
fn test_extract_title_from_content_level_2() {
let content = "## Subheading\n\nContent";
assert_eq!(extract_title_from_content(content), None);
}
#[test]
fn test_cells_to_markdown() {
let cells = vec![
vec!["Name".to_string(), "Age".to_string()],
vec!["Alice".to_string(), "30".to_string()],
vec!["Bob".to_string(), "25".to_string()],
];
let markdown = cells_to_markdown(&cells);
assert!(markdown.contains("| Name | Age |"));
assert!(markdown.contains("| Alice | 30 |"));
assert!(markdown.contains("| Bob | 25 |"));
assert!(markdown.contains("| --- | --- |"));
}
#[test]
fn test_cells_to_markdown_empty() {
let cells: Vec<Vec<String>> = vec![];
let markdown = cells_to_markdown(&cells);
assert_eq!(markdown, "");
}
#[test]
fn test_metadata_from_yaml_all_fields() {
let yaml_str = r#"
title: Test Document
author: John Doe
date: 2024-01-15
keywords:
- rust
- testing
description: A test document
abstract: This is an abstract
subject: Test Subject
category: Documentation
tags:
- tag1
- tag2
language: en
version: 1.0
"#;
let yaml: YamlValue = serde_yaml_ng::from_str(yaml_str).unwrap();
let metadata = extract_metadata_from_yaml(&yaml);
assert_eq!(metadata.title.as_deref(), Some("Test Document"));
assert_eq!(metadata.created_by.as_deref(), Some("John Doe"));
assert_eq!(metadata.created_at, Some("2024-01-15".to_string()));
assert!(metadata.keywords.is_some());
assert_eq!(metadata.subject, Some("Test Subject".to_string()));
assert!(metadata.tags.is_some());
}
#[test]
fn test_metadata_from_yaml_string_arrays() {
let yaml_str = r#"
keywords: "single, keyword, string"
tags: "tag1, tag2"
"#;
let yaml: YamlValue = serde_yaml_ng::from_str(yaml_str).unwrap();
let metadata = extract_metadata_from_yaml(&yaml);
assert_eq!(
metadata.keywords.as_deref(),
Some(["single", "keyword", "string"].map(String::from).as_slice())
);
}
}