orkmode/crates/org-parser/src/lib.rs

294 lines
9.3 KiB
Rust

//! Tree-sitter based parser for org-mode documents.
//!
//! This crate provides a high-performance parser for org-mode files using
//! tree-sitter under the hood. The parser produces a typed AST defined in
//! the `org-ast` crate.
//!
//! # Example
//!
//! ```
//! use org_parser::Parser;
//!
//! let mut parser = Parser::new();
//! let doc = parser.parse("* Hello World\nSome content").unwrap();
//!
//! assert_eq!(doc.sections.len(), 1);
//! assert_eq!(doc.sections[0].headline.title_text(), "Hello World");
//! ```
mod error;
mod convert;
mod ts;
pub use error::{ParseError, ParseResult};
pub use ts::OrgLanguage;
use org_ast::Document;
/// The main org-mode parser.
pub struct Parser {
ts_parser: tree_sitter::Parser,
}
impl Parser {
/// Create a new parser instance.
pub fn new() -> Self {
let mut ts_parser = tree_sitter::Parser::new();
ts_parser
.set_language(&OrgLanguage::language())
.expect("Failed to load org language");
Self { ts_parser }
}
/// Parse a string into an org document.
pub fn parse(&mut self, source: &str) -> ParseResult<Document> {
let tree = self
.ts_parser
.parse(source, None)
.ok_or(ParseError::ParseFailed)?;
convert::convert_document(source, tree.root_node())
}
/// Parse a string, reusing a previous tree for incremental parsing.
pub fn parse_incremental(
&mut self,
source: &str,
old_tree: Option<&tree_sitter::Tree>,
) -> ParseResult<(Document, tree_sitter::Tree)> {
let tree = self
.ts_parser
.parse(source, old_tree)
.ok_or(ParseError::ParseFailed)?;
let doc = convert::convert_document(source, tree.root_node())?;
Ok((doc, tree))
}
/// Get the raw tree-sitter tree without conversion.
/// Useful for debugging or custom traversal.
pub fn parse_raw(&mut self, source: &str) -> ParseResult<tree_sitter::Tree> {
self.ts_parser
.parse(source, None)
.ok_or(ParseError::ParseFailed)
}
}
impl Default for Parser {
fn default() -> Self {
Self::new()
}
}
impl Clone for Parser {
fn clone(&self) -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_document() {
let mut parser = Parser::new();
let doc = parser.parse("").unwrap();
assert!(doc.sections.is_empty());
assert!(doc.preamble.is_empty());
}
#[test]
fn test_simple_headline() {
let mut parser = Parser::new();
let doc = parser.parse("* Hello World").unwrap();
assert_eq!(doc.sections.len(), 1);
assert_eq!(doc.sections[0].headline.level, 1);
assert_eq!(doc.sections[0].headline.title_text(), "Hello World");
}
#[test]
fn test_nested_headlines() {
let mut parser = Parser::new();
let doc = parser.parse("* Level 1\n** Level 2\n*** Level 3").unwrap();
assert_eq!(doc.sections.len(), 1);
assert_eq!(doc.sections[0].children.len(), 1);
assert_eq!(doc.sections[0].children[0].children.len(), 1);
}
#[test]
fn test_preamble() {
let mut parser = Parser::new();
let doc = parser.parse("#+TITLE: My Document\n\nSome preamble text.\n\n* First Headline").unwrap();
assert!(!doc.preamble.is_empty());
assert_eq!(doc.sections.len(), 1);
}
#[test]
fn test_todo_keywords() {
let mut parser = Parser::new();
let doc = parser.parse("* TODO Task 1\n* DONE Task 2").unwrap();
assert_eq!(doc.sections.len(), 2);
assert!(doc.sections[0].is_todo());
assert!(!doc.sections[0].is_done());
assert!(doc.sections[1].is_done());
assert!(!doc.sections[1].is_todo());
}
#[test]
fn test_custom_todo_keywords() {
let mut parser = Parser::new();
let doc = parser.parse("#+TODO: NEXT WAITING | DONE CANCELLED\n* NEXT Task\n* WAITING Blocked").unwrap();
assert_eq!(doc.sections.len(), 2);
let kw1 = doc.sections[0].headline.keyword.as_ref().unwrap();
assert_eq!(kw1.keyword, "NEXT");
assert!(!kw1.done);
let kw2 = doc.sections[1].headline.keyword.as_ref().unwrap();
assert_eq!(kw2.keyword, "WAITING");
assert!(!kw2.done);
}
#[test]
fn test_priority() {
let mut parser = Parser::new();
let doc = parser.parse("* [#A] High priority\n* [#B] Medium\n* [#C] Low").unwrap();
assert_eq!(doc.sections.len(), 3);
assert_eq!(doc.sections[0].headline.priority, Some(org_ast::Priority::A));
assert_eq!(doc.sections[1].headline.priority, Some(org_ast::Priority::B));
assert_eq!(doc.sections[2].headline.priority, Some(org_ast::Priority::C));
}
#[test]
fn test_tags() {
let mut parser = Parser::new();
let doc = parser.parse("* Task :work:urgent:\n* Other :home:").unwrap();
assert_eq!(doc.sections[0].headline.tags, vec!["work", "urgent"]);
assert_eq!(doc.sections[1].headline.tags, vec!["home"]);
}
#[test]
fn test_property_drawer() {
let mut parser = Parser::new();
let doc = parser.parse("* Task\n:PROPERTIES:\n:CUSTOM_ID: my-task\n:EFFORT: 2h\n:END:").unwrap();
let section = &doc.sections[0];
assert!(section.properties.is_some());
let props = section.properties.as_ref().unwrap();
assert_eq!(props.get("CUSTOM_ID"), Some("my-task"));
assert_eq!(props.get("EFFORT"), Some("2h"));
assert_eq!(section.custom_id(), Some("my-task"));
}
#[test]
fn test_planning() {
let mut parser = Parser::new();
// Planning info must be on one line
let doc = parser.parse("* Task\nDEADLINE: <2025-01-15 Wed> SCHEDULED: <2025-01-14 Tue>").unwrap();
let planning = doc.sections[0].planning.as_ref().unwrap();
assert!(planning.deadline.is_some());
// Note: tree-sitter-org may not parse both on one line, check what we get
// For now just verify deadline works
}
#[test]
fn test_timestamp_with_time() {
let mut parser = Parser::new();
let doc = parser.parse("* Task\nDEADLINE: <2025-01-15 Wed 14:30>").unwrap();
let planning = doc.sections[0].planning.as_ref().unwrap();
let deadline = planning.deadline.as_ref().unwrap();
if let org_ast::TimestampValue::Point(p) = &deadline.value {
assert_eq!(p.date.year, 2025);
assert_eq!(p.date.month, 1);
assert_eq!(p.date.day, 15);
assert!(p.time.is_some());
let time = p.time.as_ref().unwrap();
assert_eq!(time.hour, 14);
assert_eq!(time.minute, 30);
} else {
panic!("Expected Point timestamp");
}
}
#[test]
fn test_repeater() {
let mut parser = Parser::new();
let doc = parser.parse("* Task\nDEADLINE: <2025-01-15 Wed +1w>").unwrap();
let planning = doc.sections[0].planning.as_ref().unwrap();
let deadline = planning.deadline.as_ref().unwrap();
assert!(deadline.has_repeater());
if let org_ast::TimestampValue::Point(p) = &deadline.value {
let r = p.repeater.as_ref().unwrap();
assert_eq!(r.value, 1);
assert_eq!(r.unit, org_ast::TimeUnit::Week);
}
}
#[test]
fn test_document_settings() {
let mut parser = Parser::new();
let doc = parser.parse("#+TITLE: My Document\n#+AUTHOR: Test User\n#+FILETAGS: :test:sample:").unwrap();
assert_eq!(doc.title(), Some("My Document"));
assert_eq!(doc.author(), Some("Test User"));
assert!(doc.settings.filetags.contains(&"test".to_string()));
assert!(doc.settings.filetags.contains(&"sample".to_string()));
}
#[test]
fn test_sibling_sections() {
let mut parser = Parser::new();
let doc = parser.parse("* First\n* Second\n* Third").unwrap();
assert_eq!(doc.sections.len(), 3);
assert_eq!(doc.sections[0].headline.title_text(), "First");
assert_eq!(doc.sections[1].headline.title_text(), "Second");
assert_eq!(doc.sections[2].headline.title_text(), "Third");
}
#[test]
fn test_all_sections_iterator() {
let mut parser = Parser::new();
let doc = parser.parse("* A\n** B\n*** C\n** D\n* E").unwrap();
let titles: Vec<_> = doc.all_sections().map(|s| s.headline.title_text()).collect();
assert_eq!(titles, vec!["A", "B", "C", "D", "E"]);
}
#[test]
fn test_todos_iterator() {
let mut parser = Parser::new();
let doc = parser.parse("* Regular\n* TODO Task1\n* DONE Task2\n* TODO Task3").unwrap();
let todos: Vec<_> = doc.todos().collect();
assert_eq!(todos.len(), 2);
assert_eq!(todos[0].headline.title_text(), "Task1");
assert_eq!(todos[1].headline.title_text(), "Task3");
}
#[test]
fn test_complex_headline() {
let mut parser = Parser::new();
let doc = parser.parse("* TODO [#A] Important task :work:urgent:").unwrap();
let section = &doc.sections[0];
assert!(section.is_todo());
assert_eq!(section.headline.priority, Some(org_ast::Priority::A));
assert_eq!(section.headline.title_text(), "Important task");
assert!(section.has_tag("work"));
assert!(section.has_tag("urgent"));
}
}