Skip to content

Commit fbfaee4

Browse files
authored
Merge pull request #124 from vectorlessflow/dev
feat: add parser registry and custom format support
2 parents 45f1a30 + 5bd1bad commit fbfaee4

21 files changed

Lines changed: 1005 additions & 62 deletions

File tree

crates/vectorless-compiler/src/config.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,16 @@ use vectorless_utils::fingerprint::{Fingerprint, Fingerprinter};
1818
use std::path::PathBuf;
1919

2020
/// Index mode for document processing.
21-
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21+
#[derive(Debug, Clone, PartialEq, Eq)]
2222
pub enum SourceFormat {
2323
/// Auto-detect format from file extension.
2424
Auto,
2525
/// Force Markdown format.
2626
Markdown,
2727
/// Force PDF format.
2828
Pdf,
29+
/// Custom format resolved via [`ParserRegistry`](crate::parse::ParserRegistry).
30+
Custom(String),
2931
}
3032

3133
impl Default for SourceFormat {

crates/vectorless-compiler/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ pub mod summary;
6767
// Re-export main types from pipeline
6868
pub use pipeline::{CompileMetrics, CompileResult, CompilerInput, PipelineExecutor};
6969

70+
// Re-export parser plugin types
71+
pub use parse::{Parser, ParserRegistry};
72+
7073
// Re-export config types
7174
pub use config::{PipelineOptions, SourceFormat, ThinningConfig};
7275
pub use vectorless_document::ReasoningIndexConfig;

crates/vectorless-compiler/src/parse/markdown/mod.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,46 @@ mod frontmatter;
2626
mod parser;
2727

2828
pub use parser::MarkdownParser;
29+
30+
use crate::parse::{ParseResult, Parser};
31+
use std::path::Path;
32+
use vectorless_error::Result;
33+
34+
/// [`Parser`] trait adapter for [`MarkdownParser`].
35+
pub struct MarkdownParserAdapter {
36+
inner: MarkdownParser,
37+
}
38+
39+
impl MarkdownParserAdapter {
40+
/// Create a new Markdown parser adapter.
41+
pub fn new() -> Self {
42+
Self {
43+
inner: MarkdownParser::new(),
44+
}
45+
}
46+
}
47+
48+
#[async_trait::async_trait]
49+
impl Parser for MarkdownParserAdapter {
50+
fn name(&self) -> &str {
51+
"markdown"
52+
}
53+
54+
fn extensions(&self) -> &[&str] {
55+
&["md", "markdown"]
56+
}
57+
58+
async fn parse_content(&self, content: &str) -> Result<ParseResult> {
59+
self.inner.parse(content).await
60+
}
61+
62+
async fn parse_file(&self, path: &Path) -> Result<ParseResult> {
63+
self.inner.parse_file(path).await
64+
}
65+
66+
async fn parse_bytes(&self, data: &[u8]) -> Result<ParseResult> {
67+
let content = std::str::from_utf8(data)
68+
.map_err(|e| vectorless_error::Error::Parse(format!("Invalid UTF-8: {}", e)))?;
69+
self.inner.parse(content).await
70+
}
71+
}

crates/vectorless-compiler/src/parse/mod.rs

Lines changed: 154 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,26 @@
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
927
pub mod markdown;
1028
pub mod pdf;
@@ -14,12 +32,134 @@ pub mod types;
1432
// Re-export core types at module level
1533
pub use types::{DocumentFormat, DocumentMeta, ParseResult, RawNode};
1634

35+
use std::collections::HashMap;
1736
use std::path::Path;
1837

1938
use crate::parse::markdown::MarkdownParser;
2039
use vectorless_error::Result;
2140
use 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.
24164
pub 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

crates/vectorless-compiler/src/parse/pdf/mod.rs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,49 @@ mod types;
2727

2828
pub use parser::PdfParser;
2929
pub use types::PdfPage;
30+
31+
use crate::parse::{ParseResult, Parser};
32+
use std::path::Path;
33+
use vectorless_error::Result;
34+
use vectorless_llm::LlmClient;
35+
36+
/// [`Parser`] trait adapter for [`PdfParser`].
37+
pub struct PdfParserAdapter {
38+
inner: PdfParser,
39+
}
40+
41+
impl PdfParserAdapter {
42+
/// Create a PDF parser adapter, optionally with LLM support.
43+
pub fn new(llm_client: Option<LlmClient>) -> Self {
44+
let inner = match llm_client {
45+
Some(client) => PdfParser::with_llm_client(client),
46+
None => PdfParser::new(),
47+
};
48+
Self { inner }
49+
}
50+
}
51+
52+
#[async_trait::async_trait]
53+
impl Parser for PdfParserAdapter {
54+
fn name(&self) -> &str {
55+
"pdf"
56+
}
57+
58+
fn extensions(&self) -> &[&str] {
59+
&["pdf"]
60+
}
61+
62+
async fn parse_content(&self, _content: &str) -> Result<ParseResult> {
63+
Err(vectorless_error::Error::Parse(
64+
"PDF requires bytes, not string content".into(),
65+
))
66+
}
67+
68+
async fn parse_file(&self, path: &Path) -> Result<ParseResult> {
69+
self.inner.parse_file(path).await
70+
}
71+
72+
async fn parse_bytes(&self, data: &[u8]) -> Result<ParseResult> {
73+
self.inner.parse_bytes_async(data, None).await
74+
}
75+
}

0 commit comments

Comments
 (0)