Skip to content

Commit c925e44

Browse files
authored
2025-11-25 updates - doctemplate port, grammar and error message fixes (#119)
2025-11-25 updates - doctemplate port, - grammar and error message fixes
1 parent 959d2f1 commit c925e44

File tree

153 files changed

+79706
-1409
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

153 files changed

+79706
-1409
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ path = "./crates/quarto-error-reporting"
6262
[workspace.dependencies.quarto-source-map]
6363
path = "./crates/quarto-source-map"
6464

65+
[workspace.dependencies.quarto-doctemplate]
66+
path = "./crates/quarto-doctemplate"
67+
68+
[workspace.dependencies.quarto-treesitter-ast]
69+
path = "./crates/quarto-treesitter-ast"
6570

6671
[workspace.lints.clippy]
6772
assigning_clones = "warn"

crates/pico-quarto-render/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,15 @@ repository.workspace = true
1212

1313
[dependencies]
1414
quarto-markdown-pandoc = { workspace = true }
15+
quarto-doctemplate = { workspace = true }
1516
anyhow.workspace = true
1617
clap = { version = "4.0", features = ["derive"] }
1718
walkdir = "2.5"
19+
include_dir = "0.7"
20+
rayon = "1.10"
21+
22+
[dev-dependencies]
23+
quarto-source-map = { workspace = true }
1824

1925
[lints]
2026
workspace = true
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# pico-quarto-render
2+
3+
Experimental batch renderer for QMD files to HTML.
4+
5+
This crate exists for prototyping and experimentation with Quarto's rendering pipeline. It is not intended for production use.
6+
7+
## Usage
8+
9+
```bash
10+
pico-quarto-render <INPUT_DIR> <OUTPUT_DIR> [-v]
11+
```
12+
13+
## Parallelism
14+
15+
Files are processed in parallel using [Rayon](https://docs.rs/rayon). To control the number of threads, set the `RAYON_NUM_THREADS` environment variable:
16+
17+
```bash
18+
# Use 4 threads
19+
RAYON_NUM_THREADS=4 pico-quarto-render input/ output/
20+
21+
# Use single thread (sequential processing, no rayon overhead)
22+
RAYON_NUM_THREADS=1 pico-quarto-render input/ output/
23+
```
24+
25+
If not set, Rayon defaults to the number of logical CPUs.
26+
27+
When `RAYON_NUM_THREADS=1`, the code bypasses Rayon entirely and uses a simple sequential loop. This produces cleaner stack traces for profiling.
57.6 KB
Binary file not shown.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* embedded_resolver.rs
3+
* Copyright (c) 2025 Posit, PBC
4+
*/
5+
6+
//! Embedded template resolver for pico-quarto-render.
7+
//!
8+
//! This module provides a `PartialResolver` implementation that loads templates
9+
//! from resources compiled into the binary via `include_dir`.
10+
11+
use include_dir::{Dir, include_dir};
12+
use quarto_doctemplate::resolver::{PartialResolver, resolve_partial_path};
13+
use std::path::Path;
14+
15+
/// Embedded HTML templates directory.
16+
static HTML_TEMPLATES: Dir = include_dir!("$CARGO_MANIFEST_DIR/src/resources/html-template");
17+
18+
/// Resolver that loads templates from embedded resources.
19+
///
20+
/// Templates are compiled into the binary at build time using `include_dir`.
21+
/// This resolver implements `PartialResolver` to support partial template loading.
22+
pub struct EmbeddedResolver;
23+
24+
impl PartialResolver for EmbeddedResolver {
25+
fn get_partial(&self, name: &str, base_path: &Path) -> Option<String> {
26+
// Resolve the partial path following Pandoc rules
27+
let partial_path = resolve_partial_path(name, base_path);
28+
29+
// Get the filename portion for embedded lookup
30+
// (templates are flat in our structure, so we just need the filename)
31+
let filename = partial_path.file_name()?.to_str()?;
32+
33+
HTML_TEMPLATES
34+
.get_file(filename)
35+
.and_then(|f| f.contents_utf8())
36+
.map(|s| s.to_string())
37+
}
38+
}
39+
40+
/// Get the main template source.
41+
///
42+
/// Returns the content of `template.html` from the embedded resources.
43+
pub fn get_main_template() -> Option<&'static str> {
44+
HTML_TEMPLATES
45+
.get_file("template.html")
46+
.and_then(|f| f.contents_utf8())
47+
}
48+
49+
#[cfg(test)]
50+
mod tests {
51+
use super::*;
52+
53+
#[test]
54+
fn test_get_main_template() {
55+
let template = get_main_template();
56+
assert!(template.is_some());
57+
let content = template.unwrap();
58+
assert!(content.contains("<!DOCTYPE html>"));
59+
assert!(content.contains("$body$"));
60+
}
61+
62+
#[test]
63+
fn test_embedded_resolver_finds_partials() {
64+
let resolver = EmbeddedResolver;
65+
let base_path = Path::new("template.html");
66+
67+
// Should find metadata.html partial
68+
let metadata = resolver.get_partial("metadata", base_path);
69+
assert!(metadata.is_some());
70+
71+
// Should find title-block.html partial
72+
let title_block = resolver.get_partial("title-block", base_path);
73+
assert!(title_block.is_some());
74+
75+
// Should find styles.html partial
76+
let styles = resolver.get_partial("styles", base_path);
77+
assert!(styles.is_some());
78+
}
79+
80+
#[test]
81+
fn test_embedded_resolver_missing_partial() {
82+
let resolver = EmbeddedResolver;
83+
let base_path = Path::new("template.html");
84+
85+
let missing = resolver.get_partial("nonexistent", base_path);
86+
assert!(missing.is_none());
87+
}
88+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* format_writers.rs
3+
* Copyright (c) 2025 Posit, PBC
4+
*/
5+
6+
//! Format-specific writers for template context building.
7+
//!
8+
//! This module provides a trait for format-specific AST-to-string conversion,
9+
//! and implementations for HTML output.
10+
11+
use anyhow::Result;
12+
use quarto_markdown_pandoc::pandoc::block::Block;
13+
use quarto_markdown_pandoc::pandoc::inline::Inlines;
14+
15+
/// Format-specific writers for converting Pandoc AST to strings.
16+
///
17+
/// Implementations of this trait provide the format-specific rendering
18+
/// needed when converting document metadata to template values.
19+
pub trait FormatWriters {
20+
/// Write blocks to a string.
21+
fn write_blocks(&self, blocks: &[Block]) -> Result<String>;
22+
23+
/// Write inlines to a string.
24+
fn write_inlines(&self, inlines: &Inlines) -> Result<String>;
25+
}
26+
27+
/// HTML format writers.
28+
///
29+
/// Uses the HTML writer from quarto-markdown-pandoc to convert
30+
/// Pandoc AST nodes to HTML strings.
31+
pub struct HtmlWriters;
32+
33+
impl FormatWriters for HtmlWriters {
34+
fn write_blocks(&self, blocks: &[Block]) -> Result<String> {
35+
let mut buf = Vec::new();
36+
quarto_markdown_pandoc::writers::html::write_blocks(blocks, &mut buf)?;
37+
Ok(String::from_utf8_lossy(&buf).into_owned())
38+
}
39+
40+
fn write_inlines(&self, inlines: &Inlines) -> Result<String> {
41+
let mut buf = Vec::new();
42+
quarto_markdown_pandoc::writers::html::write_inlines(inlines, &mut buf)?;
43+
Ok(String::from_utf8_lossy(&buf).into_owned())
44+
}
45+
}
46+
47+
#[cfg(test)]
48+
mod tests {
49+
use super::*;
50+
use quarto_markdown_pandoc::pandoc::Inline;
51+
use quarto_markdown_pandoc::pandoc::block::Paragraph;
52+
use quarto_markdown_pandoc::pandoc::inline::{Emph, Space, Str};
53+
54+
fn dummy_source_info() -> quarto_source_map::SourceInfo {
55+
quarto_source_map::SourceInfo::from_range(
56+
quarto_source_map::FileId(0),
57+
quarto_source_map::Range {
58+
start: quarto_source_map::Location {
59+
offset: 0,
60+
row: 0,
61+
column: 0,
62+
},
63+
end: quarto_source_map::Location {
64+
offset: 0,
65+
row: 0,
66+
column: 0,
67+
},
68+
},
69+
)
70+
}
71+
72+
#[test]
73+
fn test_html_writers_inlines() {
74+
let writers = HtmlWriters;
75+
let inlines = vec![
76+
Inline::Str(Str {
77+
text: "Hello".to_string(),
78+
source_info: dummy_source_info(),
79+
}),
80+
Inline::Space(Space {
81+
source_info: dummy_source_info(),
82+
}),
83+
Inline::Emph(Emph {
84+
content: vec![Inline::Str(Str {
85+
text: "world".to_string(),
86+
source_info: dummy_source_info(),
87+
})],
88+
source_info: dummy_source_info(),
89+
}),
90+
];
91+
92+
let result = writers.write_inlines(&inlines).unwrap();
93+
assert_eq!(result, "Hello <em>world</em>");
94+
}
95+
96+
#[test]
97+
fn test_html_writers_blocks() {
98+
let writers = HtmlWriters;
99+
let blocks = vec![Block::Paragraph(Paragraph {
100+
content: vec![Inline::Str(Str {
101+
text: "A paragraph.".to_string(),
102+
source_info: dummy_source_info(),
103+
})],
104+
source_info: dummy_source_info(),
105+
})];
106+
107+
let result = writers.write_blocks(&blocks).unwrap();
108+
assert_eq!(result, "<p>A paragraph.</p>\n");
109+
}
110+
}

0 commit comments

Comments
 (0)