typstify_parser/
lib.rs

1//! Typstify Parser Library
2//!
3//! Content parsers for Markdown and Typst formats.
4
5pub mod markdown;
6pub mod syntax;
7pub mod typst_parser;
8
9use std::path::Path;
10
11pub use markdown::MarkdownParser;
12pub use syntax::SyntaxHighlighter;
13use thiserror::Error;
14pub use typst_parser::TypstParser;
15use typstify_core::content::{ContentType, ParsedContent};
16
17/// Parser errors.
18#[derive(Debug, Error)]
19pub enum ParserError {
20    /// Markdown parsing error.
21    #[error("markdown error: {0}")]
22    Markdown(#[from] markdown::MarkdownError),
23
24    /// Typst parsing error.
25    #[error("typst error: {0}")]
26    Typst(#[from] typst_parser::TypstError),
27
28    /// Unsupported content type.
29    #[error("unsupported content type: {0:?}")]
30    UnsupportedType(ContentType),
31
32    /// Unknown file extension.
33    #[error("unknown file extension: {0}")]
34    UnknownExtension(String),
35}
36
37/// Result type for parser operations.
38pub type Result<T> = std::result::Result<T, ParserError>;
39
40/// Trait for content parsers.
41pub trait ContentParser {
42    /// Parse content from a string and file path.
43    fn parse(&self, content: &str, path: &Path) -> Result<ParsedContent>;
44}
45
46impl ContentParser for MarkdownParser {
47    fn parse(&self, content: &str, path: &Path) -> Result<ParsedContent> {
48        Ok(self.parse(content, path)?)
49    }
50}
51
52impl ContentParser for TypstParser {
53    fn parse(&self, content: &str, path: &Path) -> Result<ParsedContent> {
54        Ok(self.parse(content, path)?)
55    }
56}
57
58/// Registry for content parsers with auto-detection.
59#[derive(Debug)]
60pub struct ParserRegistry {
61    markdown: MarkdownParser,
62    typst: TypstParser,
63}
64
65impl Default for ParserRegistry {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl ParserRegistry {
72    /// Create a new parser registry with default parsers.
73    pub fn new() -> Self {
74        Self {
75            markdown: MarkdownParser::new(),
76            typst: TypstParser::new(),
77        }
78    }
79
80    /// Create a parser registry with a custom syntax theme.
81    pub fn with_theme(theme: &str) -> Self {
82        Self {
83            markdown: MarkdownParser::with_theme(theme),
84            typst: TypstParser::new(),
85        }
86    }
87
88    /// Parse content, auto-detecting the parser from file extension.
89    pub fn parse(&self, content: &str, path: &Path) -> Result<ParsedContent> {
90        let ext = path
91            .extension()
92            .and_then(|e| e.to_str())
93            .ok_or_else(|| ParserError::UnknownExtension("(none)".to_string()))?;
94
95        match ContentType::from_extension(ext) {
96            Some(ContentType::Markdown) => Ok(self.markdown.parse(content, path)?),
97            Some(ContentType::Typst) => Ok(self.typst.parse(content, path)?),
98            None => Err(ParserError::UnknownExtension(ext.to_string())),
99        }
100    }
101
102    /// Get the markdown parser.
103    pub fn markdown(&self) -> &MarkdownParser {
104        &self.markdown
105    }
106
107    /// Get the typst parser.
108    pub fn typst(&self) -> &TypstParser {
109        &self.typst
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn test_registry_markdown() {
119        let registry = ParserRegistry::new();
120        let content = r#"---
121title: "Test"
122---
123
124# Hello"#;
125
126        let result = registry.parse(content, Path::new("test.md")).unwrap();
127        assert_eq!(result.frontmatter.title, "Test");
128    }
129
130    #[test]
131    fn test_registry_unknown_extension() {
132        let registry = ParserRegistry::new();
133        let result = registry.parse("content", Path::new("test.xyz"));
134
135        assert!(matches!(result, Err(ParserError::UnknownExtension(_))));
136    }
137
138    #[test]
139    fn test_content_parser_trait() {
140        let parser = MarkdownParser::new();
141        let content = r#"---
142title: "Trait Test"
143---
144
145Content"#;
146
147        let result: Result<ParsedContent> =
148            ContentParser::parse(&parser, content, Path::new("test.md"));
149        assert!(result.is_ok());
150    }
151}