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 ") { let rest = &line[4..]; if let Some(colon_pos) = rest.find(':') { let before_colon = rest[..colon_pos].trim(); if is_ident(before_colon) { let after_colon = &rest[colon_pos + 1..]; if after_colon.contains('=') { return true; } } } if let Some(eq_pos) = rest.find('=') { let after_eq = rest.as_bytes().get(eq_pos + 1); if after_eq != Some(&b'=') { let name = rest[..eq_pos].trim(); if is_ident(name) { return true; } } } return false; } if line.starts_with("while ") || line.starts_with("while(") { return true; } if line.starts_with("fn ") { return true; } if line.starts_with("if ") || line.starts_with("if(") { return true; } if line.starts_with("else ") || line == "else" || line.starts_with("else{") { return true; } if line.starts_with("for ") { return true; } if line.starts_with("return ") || line == "return" { return true; } if line == "}" || line.starts_with("} ") { return true; } 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 comment_depth: usize = 0; let mut brace_depth: i32 = 0; for (i, line) in text.lines().enumerate() { let was_in_comment = comment_depth > 0; comment_depth = scan_comment_depth(line, comment_depth); if was_in_comment || line.trim().starts_with("/*") { result.push(ClassifiedLine { index: i, kind: LineKind::Comment, content: line.to_string() }); } else if brace_depth > 0 { let trimmed = line.trim(); let opens = trimmed.matches('{').count() as i32; let closes = trimmed.matches('}').count() as i32; brace_depth += opens - closes; if brace_depth < 0 { brace_depth = 0; } result.push(ClassifiedLine { index: i, kind: LineKind::Cordial, content: line.to_string() }); } else { let cl = classify_line(i, line); if cl.kind == LineKind::Cordial { let trimmed = line.trim(); let opens = trimmed.matches('{').count() as i32; let closes = trimmed.matches('}').count() as i32; brace_depth += opens - closes; if brace_depth < 0 { brace_depth = 0; } } result.push(cl); } } result } fn scan_comment_depth(line: &str, mut depth: usize) -> usize { let bytes = line.as_bytes(); let len = bytes.len(); let mut i = 0; while i < len.saturating_sub(1) { if bytes[i] == b'/' && bytes[i + 1] == b'*' { depth += 1; i += 2; } else if bytes[i] == b'*' && bytes[i + 1] == b'/' { depth = depth.saturating_sub(1); i += 2; } else { i += 1; } } depth } #[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 let_prose_not_cordial() { let c = classify_line(0, "let us consider something"); assert_eq!(c.kind, LineKind::Markdown); } #[test] fn let_without_equals_not_cordial() { let c = classify_line(0, "let me explain"); 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); } #[test] fn nested_block_comments() { let lines = classify_document("/* outer /* inner */ still comment */\nlet x = 5"); assert_eq!(lines[0].kind, LineKind::Comment); assert_eq!(lines[1].kind, LineKind::Cordial); } #[test] fn nested_multiline_block_comments() { let doc = "/* outer\n/* inner */\nstill in outer\n*/\nlet x = 5"; let lines = classify_document(doc); 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::Comment); assert_eq!(lines[4].kind, LineKind::Cordial); } #[test] fn while_line() { let c = classify_line(0, "while (i < 10) {"); assert_eq!(c.kind, LineKind::Cordial); } #[test] fn fn_line() { let c = classify_line(0, "fn add(a, b) {"); assert_eq!(c.kind, LineKind::Cordial); } #[test] fn closing_brace() { let c = classify_line(0, "}"); assert_eq!(c.kind, LineKind::Cordial); } #[test] fn while_block_body_classified() { let doc = "while (x > 0) {\n x = x - 1\n}"; let lines = classify_document(doc); assert_eq!(lines[0].kind, LineKind::Cordial); assert_eq!(lines[1].kind, LineKind::Cordial); assert_eq!(lines[2].kind, LineKind::Cordial); } #[test] fn fn_block_body_classified() { let doc = "fn add(a, b) {\n a + b\n}"; let lines = classify_document(doc); assert_eq!(lines[0].kind, LineKind::Cordial); assert_eq!(lines[1].kind, LineKind::Cordial); assert_eq!(lines[2].kind, LineKind::Cordial); } #[test] fn let_with_type_annotation() { let c = classify_line(0, "let x: int = 5"); assert_eq!(c.kind, LineKind::Cordial); } #[test] fn let_with_bool_type() { let c = classify_line(0, "let flag: bool = 1"); assert_eq!(c.kind, LineKind::Cordial); } #[test] fn if_line() { let c = classify_line(0, "if (x > 5) {"); assert_eq!(c.kind, LineKind::Cordial); } #[test] fn else_line() { let c = classify_line(0, "} else {"); assert_eq!(c.kind, LineKind::Cordial); } #[test] fn for_line() { let c = classify_line(0, "for i in arr {"); assert_eq!(c.kind, LineKind::Cordial); } #[test] fn return_line() { let c = classify_line(0, "return x"); assert_eq!(c.kind, LineKind::Cordial); } #[test] fn if_block_body_classified() { let doc = "if (x > 5) {\n x = 1\n} else {\n x = 0\n}"; let lines = classify_document(doc); assert!(lines.iter().all(|l| l.kind == LineKind::Cordial)); } }