use serde::{Serialize, Deserialize}; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub enum LineKind { Markdown, Cordial, Eval, Comment, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ClassifiedLine { pub index: usize, pub kind: LineKind, pub content: String, } pub fn classify_line(index: usize, raw: &str) -> ClassifiedLine { let trimmed = raw.trim(); let kind = if trimmed.starts_with("/=") { LineKind::Eval } else if trimmed.starts_with("//") { LineKind::Comment } else if is_cordial(trimmed) { LineKind::Cordial } else { LineKind::Markdown }; ClassifiedLine { index, kind, content: raw.to_string(), } } fn is_cordial(line: &str) -> bool { if line.starts_with("let ") { return true; } // variable assignment: identifier = expr (but not ==) if let Some(eq_pos) = line.find('=') { if eq_pos > 0 { let before = &line[..eq_pos]; let after_eq = line.as_bytes().get(eq_pos + 1); if after_eq != Some(&b'=') && !before.ends_with('!') && !before.ends_with('<') && !before.ends_with('>') { let candidate = before.trim(); if is_assignment_target(candidate) { return true; } } } } false } fn is_assignment_target(s: &str) -> bool { // simple variable: `x` if is_ident(s) { return true; } // function def: `f(x)` or `f(x, y)` if let Some(paren) = s.find('(') { let name = &s[..paren]; if is_ident(name) && s.ends_with(')') { return true; } } false } fn is_ident(s: &str) -> bool { if s.is_empty() { return false; } let mut chars = s.chars(); let first = chars.next().unwrap(); if !first.is_alphabetic() && first != '_' { return false; } chars.all(|c| c.is_alphanumeric() || c == '_') } pub fn classify_document(text: &str) -> Vec { let mut result = Vec::new(); let mut in_block_comment = false; for (i, line) in text.lines().enumerate() { if in_block_comment { let cl = ClassifiedLine { index: i, kind: LineKind::Comment, content: line.to_string() }; if line.contains("*/") { in_block_comment = false; } result.push(cl); continue; } let trimmed = line.trim(); if trimmed.starts_with("/*") { if trimmed.contains("*/") && trimmed.find("*/").unwrap() > trimmed.find("/*").unwrap() { // single-line block comment } else { in_block_comment = true; } result.push(ClassifiedLine { index: i, kind: LineKind::Comment, content: line.to_string() }); continue; } result.push(classify_line(i, line)); } result } #[cfg(test)] mod tests { use super::*; #[test] fn markdown_line() { let c = classify_line(0, "# Hello World"); assert_eq!(c.kind, LineKind::Markdown); } #[test] fn eval_line() { let c = classify_line(0, "/= 2 + 3"); assert_eq!(c.kind, LineKind::Eval); } #[test] fn comment_line() { let c = classify_line(0, "// this is a comment"); assert_eq!(c.kind, LineKind::Comment); } #[test] fn let_binding() { let c = classify_line(0, "let x = 5"); assert_eq!(c.kind, LineKind::Cordial); } #[test] fn variable_assignment() { let c = classify_line(0, "x = 5"); assert_eq!(c.kind, LineKind::Cordial); } #[test] fn function_def() { let c = classify_line(0, "f(x) = x^2"); assert_eq!(c.kind, LineKind::Cordial); } #[test] fn plain_text() { let c = classify_line(0, "Some notes about the project"); assert_eq!(c.kind, LineKind::Markdown); } #[test] fn single_line_block_comment() { let lines = classify_document("/* hello */"); assert_eq!(lines.len(), 1); assert_eq!(lines[0].kind, LineKind::Comment); } #[test] fn multiline_block_comment() { let lines = classify_document("/* start\nmiddle\nend */\nlet x = 5"); assert_eq!(lines.len(), 4); assert_eq!(lines[0].kind, LineKind::Comment); assert_eq!(lines[1].kind, LineKind::Comment); assert_eq!(lines[2].kind, LineKind::Comment); assert_eq!(lines[3].kind, LineKind::Cordial); } #[test] fn block_comment_then_code() { let lines = classify_document("/* comment */\n/= 2 + 3"); assert_eq!(lines[0].kind, LineKind::Comment); assert_eq!(lines[1].kind, LineKind::Eval); } }