Skip to main content

turbovault_parser/
lib.rs

1//! # TurboVault Parser
2//!
3//! Obsidian Flavored Markdown (OFM) parser built on `pulldown-cmark`.
4//!
5//! This crate provides:
6//! - Fast markdown parsing via `pulldown-cmark` (CommonMark foundation)
7//! - Frontmatter extraction (YAML via pulldown-cmark metadata blocks)
8//! - Obsidian-specific syntax: wikilinks, embeds, callouts, tags
9//! - **Code block awareness**: patterns inside code blocks/inline code are excluded
10//! - Link extraction and resolution
11//! - **Standalone parsing without vault context** (for tools like treemd)
12//!
13//! ## Architecture
14//!
15//! The parser uses a hybrid two-phase approach via unified `ParseEngine`:
16//!
17//! ### Phase 1: pulldown-cmark pass
18//! - Extracts CommonMark elements: headings, markdown links, tasks, frontmatter
19//! - Builds excluded ranges (code blocks, inline code, HTML) for Phase 2
20//!
21//! ### Phase 2: Regex pass (OFM extensions)
22//! - Parses Obsidian-specific syntax: wikilinks `[[]]`, embeds `![[]]`, tags `#tag`, callouts
23//! - **Skips excluded ranges** to avoid matching inside code blocks
24//!
25//! ### Performance optimizations
26//! - Builds a `LineIndex` once for O(log n) position lookups
27//! - Uses fast pre-filters to skip regex when patterns aren't present
28//!
29//! ## Quick Start
30//!
31//! ### With Vault Context
32//!
33//! ```
34//! use turbovault_parser::Parser;
35//! use std::path::PathBuf;
36//!
37//! let content = r#"---
38//! title: My Note
39//! tags: [important, review]
40//! ---
41//!
42//! # Heading
43//!
44//! [[WikiLink]] and [[Other Note#Heading]].
45//!
46//! - [x] Completed task
47//! - [ ] Pending task
48//! "#;
49//!
50//! let vault_path = PathBuf::from("/vault");
51//! let parser = Parser::new(vault_path);
52//!
53//! let path = PathBuf::from("my-note.md");
54//! if let Ok(result) = parser.parse_file(&path, content) {
55//!     // Access parsed components
56//!     if let Some(frontmatter) = &result.frontmatter {
57//!         println!("Frontmatter data: {:?}", frontmatter.data);
58//!     }
59//!     println!("Links: {}", result.links.len());
60//!     println!("Tasks: {}", result.tasks.len());
61//! }
62//! ```
63//!
64//! ### Standalone Parsing (No Vault Required)
65//!
66//! ```
67//! use turbovault_parser::{ParsedContent, ParseOptions};
68//!
69//! let content = "# Title\n\n[[WikiLink]] and [markdown](url) with #tag";
70//!
71//! // Parse everything
72//! let parsed = ParsedContent::parse(content);
73//! assert_eq!(parsed.wikilinks.len(), 1);
74//! assert_eq!(parsed.markdown_links.len(), 1);
75//! assert_eq!(parsed.tags.len(), 1);
76//!
77//! // Or parse selectively for better performance
78//! let parsed = ParsedContent::parse_with_options(content, ParseOptions::links_only());
79//! ```
80//!
81//! ### Individual Parsers (Granular Control)
82//!
83//! ```
84//! use turbovault_parser::{parse_wikilinks, parse_tags, parse_callouts};
85//!
86//! let content = "[[Link]] with #tag and > [!NOTE] callout";
87//!
88//! let wikilinks = parse_wikilinks(content);
89//! let tags = parse_tags(content);
90//! let callouts = parse_callouts(content);
91//! ```
92//!
93//! ## Supported OFM Features
94//!
95//! ### Links
96//! - Wikilinks: `[[Note]]`
97//! - Aliases: `[[Note|Alias]]`
98//! - Block references: `[[Note#^blockid]]`
99//! - Heading references: `[[Note#Heading]]`
100//! - Embeds: `![[Note]]`
101//! - Markdown links: `[text](url)`
102//!
103//! ### Frontmatter
104//! YAML frontmatter between `---` delimiters is extracted and parsed.
105//!
106//! ### Elements
107//! - **Headings**: H1-H6 with level tracking
108//! - **Tasks**: Markdown checkboxes with completion status
109//! - **Tags**: Inline tags like `#important`
110//! - **Callouts**: Obsidian callout syntax `> [!TYPE]` with multi-line content
111//!
112//! ## Performance
113//!
114//! The parser uses:
115//! - `pulldown-cmark` for CommonMark parsing + code block detection (O(n) linear time)
116//! - `std::sync::LazyLock` for compiled regex patterns (Rust 1.80+)
117//! - `LineIndex` for O(log n) position lookups via binary search
118//! - Fast pre-filters to skip regex when patterns aren't present
119//! - Excluded range tracking to avoid parsing inside code blocks
120
121#![warn(missing_docs, unreachable_pub)]
122
123// Core modules
124mod blocks;
125pub mod canvas;
126mod engine;
127mod frontmatter;
128pub mod models;
129pub mod parsers;
130mod standalone;
131
132// Main exports
133pub use canvas::{Canvas, CanvasEdge, CanvasNode, CanvasReference, parse_canvas};
134pub use engine::ReferenceDefinition;
135pub use frontmatter::{MAX_FRONTMATTER_BYTES, MAX_FRONTMATTER_DEPTH, parse_frontmatter_yaml};
136pub use models::TaskStatus;
137pub use parsers::Parser;
138pub use standalone::{ParseOptions, ParsedContent};
139
140// Block-level parsing (for treemd integration)
141pub use blocks::{MAX_LIST_DEPTH, parse_blocks, parse_blocks_from_line, slugify, to_plain_text};
142
143// OKF citation parsing (`# Citations` convention)
144pub use parsers::citations::parse_citations;
145
146// Re-export core types for consumers (no need to depend on turbovault-core separately)
147pub use turbovault_core::{
148    ContentBlock, InlineElement, LineIndex, LinkType, ListItem, SourcePosition, TableAlignment,
149};
150
151// ============================================================================
152// Simplified Public API - Individual Parser Functions
153// ============================================================================
154//
155// These functions provide granular parsing when you only need specific elements.
156// They all use the unified engine internally with LineIndex for efficient position tracking.
157
158/// Parse wikilinks from content.
159///
160/// Returns links with empty `source_file`. Use `Parser::parse_file()` for vault-aware parsing.
161///
162/// # Example
163/// ```
164/// use turbovault_parser::parse_wikilinks;
165///
166/// let links = parse_wikilinks("See [[Note]] and [[Other|alias]]");
167/// assert_eq!(links.len(), 2);
168/// assert_eq!(links[0].target, "Note");
169/// ```
170pub fn parse_wikilinks(content: &str) -> Vec<turbovault_core::Link> {
171    let engine = engine::ParseEngine::new(content);
172    let opts = ParseOptions {
173        parse_wikilinks: true,
174        ..ParseOptions::none()
175    };
176    engine.parse(&opts).wikilinks
177}
178
179/// Parse embeds from content.
180///
181/// # Example
182/// ```
183/// use turbovault_parser::parse_embeds;
184///
185/// let embeds = parse_embeds("![[image.png]] and ![[Note]]");
186/// assert_eq!(embeds.len(), 2);
187/// ```
188pub fn parse_embeds(content: &str) -> Vec<turbovault_core::Link> {
189    let engine = engine::ParseEngine::new(content);
190    let opts = ParseOptions {
191        parse_wikilinks: true, // Embeds are parsed with wikilinks
192        ..ParseOptions::none()
193    };
194    engine.parse(&opts).embeds
195}
196
197/// Parse markdown links from content.
198///
199/// # Example
200/// ```
201/// use turbovault_parser::parse_markdown_links;
202///
203/// let links = parse_markdown_links("[text](url) and [other](http://example.com)");
204/// assert_eq!(links.len(), 2);
205/// ```
206pub fn parse_markdown_links(content: &str) -> Vec<turbovault_core::Link> {
207    let engine = engine::ParseEngine::new(content);
208    let opts = ParseOptions {
209        parse_markdown_links: true,
210        ..ParseOptions::none()
211    };
212    engine.parse(&opts).markdown_links
213}
214
215/// Parse the reference definitions (`[label]: destination`) that reference
216/// links in `content` take their destinations from. The links themselves,
217/// `[text][label]`, `[label][]` and `[label]`, are among
218/// [`parse_markdown_links`], with the definition's destination as target.
219///
220/// # Example
221/// ```
222/// use turbovault_parser::parse_reference_definitions;
223///
224/// let content = "See [the paper][p].\n\n[p]: papers/My%20Paper.md \"Title\"\n";
225/// let definitions = parse_reference_definitions(content);
226/// assert_eq!(definitions[0].target, "papers/My Paper.md");
227/// assert_eq!(&content[definitions[0].destination.clone()], "papers/My%20Paper.md");
228/// ```
229pub fn parse_reference_definitions(content: &str) -> Vec<ReferenceDefinition> {
230    engine::reference_definitions(content)
231}
232
233/// Parse tags from content.
234///
235/// # Example
236/// ```
237/// use turbovault_parser::parse_tags;
238///
239/// let tags = parse_tags("Has #tag and #nested/tag");
240/// assert_eq!(tags.len(), 2);
241/// assert!(tags[1].is_nested);
242/// ```
243pub fn parse_tags(content: &str) -> Vec<turbovault_core::Tag> {
244    let engine = engine::ParseEngine::new(content);
245    let opts = ParseOptions {
246        parse_tags: true,
247        ..ParseOptions::none()
248    };
249    engine.parse(&opts).tags
250}
251
252/// Parse headings from content.
253///
254/// # Example
255/// ```
256/// use turbovault_parser::parse_headings;
257///
258/// let headings = parse_headings("# H1\n## H2\n### H3");
259/// assert_eq!(headings.len(), 3);
260/// assert_eq!(headings[0].level, 1);
261/// ```
262pub fn parse_headings(content: &str) -> Vec<turbovault_core::Heading> {
263    let engine = engine::ParseEngine::new(content);
264    let opts = ParseOptions {
265        parse_headings: true,
266        ..ParseOptions::none()
267    };
268    engine.parse(&opts).headings
269}
270
271/// Parse tasks from content.
272///
273/// # Example
274/// ```
275/// use turbovault_parser::parse_tasks;
276///
277/// let tasks = parse_tasks("- [ ] Todo\n- [x] Done");
278/// assert_eq!(tasks.len(), 2);
279/// assert!(!tasks[0].is_completed);
280/// assert!(tasks[1].is_completed);
281/// ```
282pub fn parse_tasks(content: &str) -> Vec<turbovault_core::TaskItem> {
283    let engine = engine::ParseEngine::new(content);
284    let opts = ParseOptions {
285        parse_tasks: true,
286        ..ParseOptions::none()
287    };
288    engine.parse(&opts).tasks
289}
290
291/// Parse callouts from content (header only, no multi-line content).
292///
293/// # Example
294/// ```
295/// use turbovault_parser::parse_callouts;
296///
297/// let callouts = parse_callouts("> [!NOTE] Title\n> Content");
298/// assert_eq!(callouts.len(), 1);
299/// ```
300pub fn parse_callouts(content: &str) -> Vec<turbovault_core::Callout> {
301    let engine = engine::ParseEngine::new(content);
302    let opts = ParseOptions {
303        parse_callouts: true,
304        full_callouts: false,
305        ..ParseOptions::none()
306    };
307    engine.parse(&opts).callouts
308}
309
310/// Parse callouts with full multi-line content extraction.
311///
312/// # Example
313/// ```
314/// use turbovault_parser::parse_callouts_full;
315///
316/// let callouts = parse_callouts_full("> [!NOTE] Title\n> Line 1\n> Line 2");
317/// assert_eq!(callouts[0].content, "Line 1\nLine 2");
318/// ```
319pub fn parse_callouts_full(content: &str) -> Vec<turbovault_core::Callout> {
320    let engine = engine::ParseEngine::new(content);
321    let opts = ParseOptions {
322        parse_callouts: true,
323        full_callouts: true,
324        ..ParseOptions::none()
325    };
326    engine.parse(&opts).callouts
327}
328
329/// Convenient prelude for common imports.
330///
331/// Includes core types, the main parser, standalone parsing API, and all parser functions.
332pub mod prelude {
333    // Core types from turbovault-core
334    pub use turbovault_core::{
335        Callout, CalloutType, ContentBlock, Frontmatter, Heading, InlineElement, LineIndex, Link,
336        LinkType, ListItem, SourcePosition, TableAlignment, Tag, TaskItem,
337    };
338
339    // Main parser
340    pub use crate::Parser;
341
342    // Standalone parsing API
343    pub use crate::{ParseOptions, ParsedContent};
344
345    // Individual parsers
346    pub use crate::{
347        parse_blocks, parse_blocks_from_line, parse_callouts, parse_callouts_full, parse_citations,
348        parse_embeds, parse_headings, parse_markdown_links, parse_tags, parse_tasks,
349        parse_wikilinks, slugify, to_plain_text,
350    };
351}