33
44//! Document parsing for the compile pipeline.
55//!
6- //! Supports Markdown and PDF formats. Parsing is dispatched directly
7- //! via `match` — no trait objects or registry needed.
6+ //! Supports Markdown and PDF formats out of the box. Custom parsers can be
7+ //! added via the [`Parser`] trait and [`ParserRegistry`].
8+ //!
9+ //! # Adding a custom parser
10+ //!
11+ //! ```rust,ignore
12+ //! use vectorless_compiler::parse::{Parser, ParseResult, ParserRegistry};
13+ //!
14+ //! struct MyParser;
15+ //!
16+ //! #[async_trait]
17+ //! impl Parser for MyParser {
18+ //! fn name(&self) -> &str { "my-format" }
19+ //! fn extensions(&self) -> &[&str] { &["foo", "bar"] }
20+ //! async fn parse_content(&self, content: &str) -> Result<ParseResult> { ... }
21+ //! async fn parse_file(&self, path: &Path) -> Result<ParseResult> { ... }
22+ //! }
23+ //!
24+ //! let registry = ParserRegistry::default_parsers(None).with(MyParser);
25+ //! ```
826
927pub mod markdown;
1028pub mod pdf;
@@ -14,12 +32,134 @@ pub mod types;
1432// Re-export core types at module level
1533pub use types:: { DocumentFormat , DocumentMeta , ParseResult , RawNode } ;
1634
35+ use std:: collections:: HashMap ;
1736use std:: path:: Path ;
1837
1938use crate :: parse:: markdown:: MarkdownParser ;
2039use vectorless_error:: Result ;
2140use vectorless_llm:: LlmClient ;
2241
42+ // ---------------------------------------------------------------------------
43+ // Parser trait
44+ // ---------------------------------------------------------------------------
45+
46+ /// Trait for document format parsers.
47+ ///
48+ /// Implement this to add support for a new document format.
49+ /// Register via [`ParserRegistry::register`] or [`ParserRegistry::with`].
50+ #[ async_trait:: async_trait]
51+ pub trait Parser : Send + Sync {
52+ /// Parser name (e.g., "markdown", "pdf", "code").
53+ fn name ( & self ) -> & str ;
54+
55+ /// File extensions this parser handles, without dot (e.g., `["py", "rs"]`).
56+ fn extensions ( & self ) -> & [ & str ] {
57+ & [ ]
58+ }
59+
60+ /// Parse string content into raw nodes.
61+ async fn parse_content ( & self , content : & str ) -> Result < ParseResult > ;
62+
63+ /// Parse a file into raw nodes.
64+ async fn parse_file ( & self , path : & Path ) -> Result < ParseResult > ;
65+
66+ /// Parse binary data into raw nodes.
67+ async fn parse_bytes ( & self , data : & [ u8 ] ) -> Result < ParseResult > {
68+ let _ = data;
69+ Err ( vectorless_error:: Error :: Parse (
70+ "Binary parsing not supported by this parser" . into ( ) ,
71+ ) )
72+ }
73+ }
74+
75+ // ---------------------------------------------------------------------------
76+ // ParserRegistry
77+ // ---------------------------------------------------------------------------
78+
79+ /// Registry of document format parsers.
80+ ///
81+ /// Maps parser names and file extensions to [`Parser`] implementations.
82+ /// Built-in parsers for Markdown and PDF are provided by [`ParserRegistry::default_parsers`].
83+ pub struct ParserRegistry {
84+ parsers : HashMap < String , Box < dyn Parser > > ,
85+ extension_map : HashMap < String , String > ,
86+ }
87+
88+ impl ParserRegistry {
89+ /// Create an empty registry.
90+ pub fn new ( ) -> Self {
91+ Self {
92+ parsers : HashMap :: new ( ) ,
93+ extension_map : HashMap :: new ( ) ,
94+ }
95+ }
96+
97+ /// Register a parser. Extensions declared by the parser are auto-indexed.
98+ pub fn register ( & mut self , parser : impl Parser + ' static ) {
99+ let name = parser. name ( ) . to_string ( ) ;
100+ for ext in parser. extensions ( ) {
101+ self . extension_map . insert ( ext. to_lowercase ( ) , name. clone ( ) ) ;
102+ }
103+ self . parsers . insert ( name, Box :: new ( parser) ) ;
104+ }
105+
106+ /// Builder-style registration.
107+ pub fn with ( mut self , parser : impl Parser + ' static ) -> Self {
108+ self . register ( parser) ;
109+ self
110+ }
111+
112+ /// Get a parser by name.
113+ pub fn get ( & self , name : & str ) -> Option < & dyn Parser > {
114+ self . parsers . get ( name) . map ( |p| p. as_ref ( ) )
115+ }
116+
117+ /// Get a parser by file extension (lowercase).
118+ pub fn get_by_extension ( & self , ext : & str ) -> Option < & dyn Parser > {
119+ self . extension_map
120+ . get ( & ext. to_lowercase ( ) )
121+ . and_then ( |name| self . parsers . get ( name) )
122+ . map ( |p| p. as_ref ( ) )
123+ }
124+
125+ /// Default registry with built-in Markdown + PDF parsers.
126+ pub fn default_parsers ( llm_client : Option < LlmClient > ) -> Self {
127+ let mut registry = Self :: new ( ) ;
128+ registry. register ( markdown:: MarkdownParserAdapter :: new ( ) ) ;
129+ registry. register ( pdf:: PdfParserAdapter :: new ( llm_client) ) ;
130+ registry
131+ }
132+
133+ /// List all registered parser names.
134+ pub fn parser_names ( & self ) -> Vec < & str > {
135+ self . parsers . keys ( ) . map ( |s| s. as_str ( ) ) . collect ( )
136+ }
137+
138+ /// List all supported file extensions (lowercase, no dot).
139+ pub fn supported_extensions ( & self ) -> Vec < & str > {
140+ self . extension_map . keys ( ) . map ( |s| s. as_str ( ) ) . collect ( )
141+ }
142+ }
143+
144+ impl Default for ParserRegistry {
145+ fn default ( ) -> Self {
146+ Self :: default_parsers ( None )
147+ }
148+ }
149+
150+ impl std:: fmt:: Debug for ParserRegistry {
151+ fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
152+ f. debug_struct ( "ParserRegistry" )
153+ . field ( "parsers" , & self . parsers . keys ( ) . collect :: < Vec < _ > > ( ) )
154+ . field ( "extensions" , & self . extension_map )
155+ . finish ( )
156+ }
157+ }
158+
159+ // ---------------------------------------------------------------------------
160+ // Legacy free functions (backward compat — delegate to default registry)
161+ // ---------------------------------------------------------------------------
162+
23163/// Parse a string content document.
24164pub async fn parse_content (
25165 content : & str ,
@@ -34,6 +174,10 @@ pub async fn parse_content(
34174 DocumentFormat :: Pdf => Err ( vectorless_error:: Error :: Parse (
35175 "PDF requires bytes, not string content" . to_string ( ) ,
36176 ) ) ,
177+ _ => Err ( vectorless_error:: Error :: Parse ( format ! (
178+ "Unsupported format for content parsing: {:?}" ,
179+ format
180+ ) ) ) ,
37181 }
38182}
39183
@@ -55,6 +199,10 @@ pub async fn parse_file(
55199 } ;
56200 parser. parse_file ( path) . await
57201 }
202+ _ => Err ( vectorless_error:: Error :: Parse ( format ! (
203+ "Unsupported format for file parsing: {:?}" ,
204+ format
205+ ) ) ) ,
58206 }
59207}
60208
@@ -79,6 +227,10 @@ pub async fn parse_bytes(
79227 } ;
80228 parser. parse_bytes_async ( bytes, None ) . await
81229 }
230+ _ => Err ( vectorless_error:: Error :: Parse ( format ! (
231+ "Unsupported format for bytes parsing: {:?}" ,
232+ format
233+ ) ) ) ,
82234 }
83235}
84236
0 commit comments