Swiftly/core/src/doc.rs

185 lines
4.7 KiB
Rust

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<ClassifiedLine> {
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);
}
}