Skip to content

Commit 227209d

Browse files
fix: address bugs, improve architecture, and add features
Fix preprocess_math dead branch eating backticks before ~~, remove TOML frontmatter retry loop that masked parse errors, fix CLI version mismatch, remove global MARKDOWN_RENDERER that ignored configured syntax themes. Replace all eprintln warnings with proper error propagation. Make MarkdownRenderer::with_theme return Result, image processing and JS minification failures are now real errors. Parallelize content loading with rayon, skip raw_content in template serialization, gate static/asset copying and asset processing on full builds, make Sass compilation atomic (compile all then write all). Add nested collection support, custom permalink frontmatter field, theme partial sharing with shortcode templates, and builtin default partials for shortcode Tera.
1 parent 02f8b10 commit 227209d

File tree

10 files changed

+567
-318
lines changed

10 files changed

+567
-318
lines changed

apps/cli/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ name = "bamboo"
1616
path = "src/main.rs"
1717

1818
[dependencies]
19-
bamboo-ssg = { version = "0.1.1", path = "../../crates/bamboo" }
19+
bamboo-ssg = { version = "0.1.2", path = "../../crates/bamboo" }
2020
clap = { version = "4", features = ["derive"] }
2121
notify = "8"
2222
open = "5"

apps/cli/src/commands.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ pub fn build_site(
177177
if !shortcode_dirs.is_empty() {
178178
builder = builder.shortcode_dirs(&shortcode_dirs)?;
179179
}
180+
let theme_templates = theme_path.join("templates");
181+
if theme_templates.is_dir() {
182+
builder = builder.theme_templates_dir(&theme_templates);
183+
}
180184

181185
let site = builder.build()?;
182186

@@ -255,6 +259,10 @@ fn build_site_incremental(
255259
if !shortcode_dirs.is_empty() {
256260
builder = builder.shortcode_dirs(&shortcode_dirs)?;
257261
}
262+
let theme_templates = theme_path.join("templates");
263+
if theme_templates.is_dir() {
264+
builder = builder.theme_templates_dir(&theme_templates);
265+
}
258266

259267
let site = builder.build()?;
260268

crates/bamboo/src/assets.rs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -193,23 +193,14 @@ fn minify_js_files(output_dir: &Path) -> Result<()> {
193193
let session = minify_js::Session::new();
194194
let source = fs::read(file_path)?;
195195
let mut output = Vec::new();
196-
match minify_js::minify(
196+
minify_js::minify(
197197
&session,
198198
minify_js::TopLevelMode::Global,
199199
&source,
200200
&mut output,
201-
) {
202-
Ok(()) => {
203-
fs::write(file_path, output)?;
204-
}
205-
Err(error) => {
206-
eprintln!(
207-
"Warning: failed to minify {}: {}",
208-
file_path.display(),
209-
error
210-
);
211-
}
212-
}
201+
)
202+
.map_err(|error| std::io::Error::other(format!("{}: {}", file_path.display(), error)))?;
203+
fs::write(file_path, output)?;
213204
Ok(())
214205
})
215206
}
@@ -225,6 +216,8 @@ fn compile_sass_files(output_dir: &Path, load_paths: &[std::path::PathBuf]) -> R
225216
return Ok(());
226217
}
227218

219+
let mut compiled_results: Vec<(std::path::PathBuf, std::path::PathBuf, String)> = Vec::new();
220+
228221
for file_path in &all_sass_files {
229222
let filename = file_path
230223
.file_name()
@@ -258,11 +251,25 @@ fn compile_sass_files(output_dir: &Path, load_paths: &[std::path::PathBuf]) -> R
258251
})?;
259252

260253
let css_path = file_path.with_extension("css");
261-
fs::write(&css_path, compiled)?;
254+
compiled_results.push((file_path.clone(), css_path, compiled));
255+
}
256+
257+
for (_sass_path, css_path, compiled_css) in &compiled_results {
258+
fs::write(css_path, compiled_css)?;
259+
}
260+
261+
for (sass_path, _css_path, _compiled_css) in &compiled_results {
262+
if sass_path.exists() {
263+
fs::remove_file(sass_path)?;
264+
}
262265
}
263266

264267
for file_path in &all_sass_files {
265-
if file_path.exists() {
268+
let filename = file_path
269+
.file_name()
270+
.and_then(|name| name.to_str())
271+
.unwrap_or("");
272+
if filename.starts_with('_') && file_path.exists() {
266273
fs::remove_file(file_path)?;
267274
}
268275
}

crates/bamboo/src/images.rs

Lines changed: 24 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -92,30 +92,21 @@ pub fn process_images(output_dir: &Path, config: &ImageConfig) -> Result<ImageMa
9292
.map(|entry| entry.path().to_path_buf())
9393
.collect();
9494

95-
let results: Vec<Option<(String, Vec<ImageVariant>)>> = image_paths
95+
type ImageResult = Result<Option<(String, Vec<ImageVariant>)>>;
96+
let results: Vec<ImageResult> = image_paths
9697
.par_iter()
97-
.map(|path| -> Option<(String, Vec<ImageVariant>)> {
98-
let source_image = match ImageReader::open(path) {
99-
Ok(reader) => match reader.decode() {
100-
Ok(image) => image,
101-
Err(error) => {
102-
eprintln!(
103-
"Warning: failed to decode image {}: {}",
104-
path.display(),
105-
error
106-
);
107-
return None;
108-
}
109-
},
110-
Err(error) => {
111-
eprintln!(
112-
"Warning: failed to open image {}: {}",
113-
path.display(),
114-
error
115-
);
116-
return None;
98+
.map(|path| -> Result<Option<(String, Vec<ImageVariant>)>> {
99+
let reader = ImageReader::open(path).map_err(|error| {
100+
crate::error::BambooError::ImageProcessing {
101+
message: format!("failed to open {}: {}", path.display(), error),
117102
}
118-
};
103+
})?;
104+
let source_image =
105+
reader
106+
.decode()
107+
.map_err(|error| crate::error::BambooError::ImageProcessing {
108+
message: format!("failed to decode {}: {}", path.display(), error),
109+
})?;
119110

120111
let original_width = source_image.width();
121112
let original_height = source_image.height();
@@ -179,14 +170,13 @@ pub fn process_images(output_dir: &Path, config: &ImageConfig) -> Result<ImageMa
179170
}
180171
};
181172

182-
if let Err(error) = write_result {
183-
eprintln!(
184-
"Warning: failed to write image variant {}: {}",
173+
write_result.map_err(|error| crate::error::BambooError::ImageProcessing {
174+
message: format!(
175+
"failed to write variant {}: {}",
185176
variant_path.display(),
186177
error
187-
);
188-
continue;
189-
}
178+
),
179+
})?;
190180

191181
let relative_variant = variant_path
192182
.strip_prefix(output_dir)
@@ -203,16 +193,18 @@ pub fn process_images(output_dir: &Path, config: &ImageConfig) -> Result<ImageMa
203193
}
204194

205195
if !image_variants.is_empty() {
206-
Some((relative_original, image_variants))
196+
Ok(Some((relative_original, image_variants)))
207197
} else {
208-
None
198+
Ok(None)
209199
}
210200
})
211201
.collect();
212202

213203
let mut variants: HashMap<String, Vec<ImageVariant>> = HashMap::new();
214-
for result in results.into_iter().flatten() {
215-
variants.insert(result.0, result.1);
204+
for result in results {
205+
if let Some((key, value)) = result? {
206+
variants.insert(key, value);
207+
}
216208
}
217209

218210
Ok(ImageManifest { variants })

crates/bamboo/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ pub use cache::{
2020
pub use error::{BambooError, IoContext, Result};
2121
pub use parsing::{
2222
MarkdownRenderer, RenderedMarkdown, extract_excerpt, extract_frontmatter,
23-
parse_date_from_filename, parse_markdown, reading_time, slugify, word_count,
23+
parse_date_from_filename, reading_time, slugify, word_count,
2424
};
2525
pub use site::SiteBuilder;
2626
pub use theme::{ThemeEngine, clean_output_dir};

crates/bamboo/src/parsing.rs

Lines changed: 46 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,10 @@ use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, T
55
use serde_json::Value;
66
use std::collections::{HashMap, HashSet};
77
use std::path::Path;
8-
use std::sync::LazyLock;
98
use syntect::highlighting::ThemeSet;
109
use syntect::html::highlighted_html_for_string;
1110
use syntect::parsing::SyntaxSet;
1211

13-
static MARKDOWN_RENDERER: LazyLock<MarkdownRenderer> = LazyLock::new(MarkdownRenderer::new);
14-
1512
pub struct MarkdownRenderer {
1613
syntax_set: SyntaxSet,
1714
theme_set: ThemeSet,
@@ -38,22 +35,18 @@ impl MarkdownRenderer {
3835
}
3936
}
4037

41-
pub fn with_theme(theme_name: &str) -> Self {
38+
pub fn with_theme(theme_name: &str) -> Result<Self> {
4239
let theme_set = ThemeSet::load_defaults();
43-
let validated_theme_name = if theme_set.themes.contains_key(theme_name) {
44-
theme_name.to_string()
45-
} else {
46-
eprintln!(
47-
"Warning: syntax theme '{}' not found, falling back to 'base16-ocean.dark'",
48-
theme_name
49-
);
50-
"base16-ocean.dark".to_string()
51-
};
52-
Self {
40+
if !theme_set.themes.contains_key(theme_name) {
41+
return Err(BambooError::ThemeNotFound {
42+
name: format!("syntax theme '{}' not found", theme_name),
43+
});
44+
}
45+
Ok(Self {
5346
syntax_set: SyntaxSet::load_defaults_newlines(),
5447
theme_set,
55-
theme_name: validated_theme_name,
56-
}
48+
theme_name: theme_name.to_string(),
49+
})
5750
}
5851

5952
pub fn render(&self, content: &str) -> RenderedMarkdown {
@@ -238,10 +231,6 @@ fn escape_html(input: &str) -> String {
238231
crate::xml::escape(input)
239232
}
240233

241-
pub fn parse_markdown(content: &str) -> RenderedMarkdown {
242-
MARKDOWN_RENDERER.render(content)
243-
}
244-
245234
pub fn word_count(text: &str) -> usize {
246235
text.split_whitespace().count()
247236
}
@@ -301,11 +290,6 @@ pub fn preprocess_math(content: &str) -> String {
301290
position += 1;
302291
}
303292
continue;
304-
} else if position + 2 < length
305-
&& chars[position + 1] == '~'
306-
&& chars[position + 2] == '~'
307-
{
308-
continue;
309293
} else {
310294
in_inline_code = true;
311295
output.push(chars[position]);
@@ -561,32 +545,20 @@ pub fn extract_frontmatter(content: &str, path: &Path) -> Result<(Frontmatter, S
561545
fn parse_toml_frontmatter(content: &str, path: &Path) -> Result<(Frontmatter, String)> {
562546
let rest = &content[3..];
563547

564-
let mut search_offset = 0;
565-
loop {
566-
let end_index = find_closing_delimiter(&rest[search_offset..], "+++")
567-
.map(|position| search_offset + position)
568-
.ok_or_else(|| BambooError::InvalidFrontmatter {
569-
path: path.to_path_buf(),
570-
})?;
571-
572-
let frontmatter_str = &rest[..end_index];
573-
match toml::from_str::<HashMap<String, Value>>(frontmatter_str) {
574-
Ok(raw) => {
575-
let body = &rest[end_index + 3..];
576-
return Ok((Frontmatter { raw }, body.trim().to_string()));
577-
}
578-
Err(error) => {
579-
let next_start = end_index + 3;
580-
if next_start >= rest.len() {
581-
return Err(BambooError::TomlParse {
582-
path: path.to_path_buf(),
583-
message: error.to_string(),
584-
});
585-
}
586-
search_offset = next_start;
587-
}
588-
}
589-
}
548+
let end_index =
549+
find_closing_delimiter(rest, "+++").ok_or_else(|| BambooError::InvalidFrontmatter {
550+
path: path.to_path_buf(),
551+
})?;
552+
553+
let frontmatter_str = &rest[..end_index];
554+
let raw: HashMap<String, Value> =
555+
toml::from_str(frontmatter_str).map_err(|error| BambooError::TomlParse {
556+
path: path.to_path_buf(),
557+
message: error.to_string(),
558+
})?;
559+
560+
let body = &rest[end_index + 3..];
561+
Ok((Frontmatter { raw }, body.trim().to_string()))
590562
}
591563

592564
fn parse_yaml_frontmatter(content: &str, path: &Path) -> Result<(Frontmatter, String)> {
@@ -661,10 +633,14 @@ mod tests {
661633
use super::*;
662634
use std::path::PathBuf;
663635

636+
fn render(input: &str) -> RenderedMarkdown {
637+
MarkdownRenderer::new().render(input)
638+
}
639+
664640
#[test]
665641
fn test_parse_markdown() {
666642
let input = "# Hello\n\nThis is **bold**.";
667-
let output = parse_markdown(input);
643+
let output = render(input);
668644
assert!(output.html.contains("<h1"));
669645
assert!(output.html.contains("Hello"));
670646
assert!(output.html.contains("<strong>bold</strong>"));
@@ -673,23 +649,23 @@ mod tests {
673649
#[test]
674650
fn test_parse_markdown_with_code() {
675651
let input = "```rust\nfn main() {}\n```";
676-
let output = parse_markdown(input);
652+
let output = render(input);
677653
assert!(output.html.contains("fn"));
678654
assert!(output.html.contains("main"));
679655
}
680656

681657
#[test]
682658
fn test_heading_anchors() {
683659
let input = "## My Heading";
684-
let output = parse_markdown(input);
660+
let output = render(input);
685661
assert!(output.html.contains("id=\"my-heading\""));
686662
assert!(output.html.contains("href=\"#my-heading\""));
687663
}
688664

689665
#[test]
690666
fn test_toc_generation() {
691667
let input = "# Title\n## Section One\n### Subsection\n## Section Two";
692-
let output = parse_markdown(input);
668+
let output = render(input);
693669
assert_eq!(output.toc.len(), 4);
694670
assert_eq!(output.toc[0].level, 1);
695671
assert_eq!(output.toc[0].title, "Title");
@@ -826,4 +802,19 @@ mod tests {
826802
let output = preprocess_math(input);
827803
assert_eq!(output, input);
828804
}
805+
806+
#[test]
807+
fn test_preprocess_math_backtick_followed_by_tildes() {
808+
let input = "`~~something~~`";
809+
let output = preprocess_math(input);
810+
assert_eq!(output, input);
811+
}
812+
813+
#[test]
814+
fn test_toml_frontmatter_malformed_returns_error() {
815+
let content = "+++\ntitle = \n+++\n\nBody content";
816+
let path = PathBuf::from("test.md");
817+
let result = extract_frontmatter(content, &path);
818+
assert!(result.is_err());
819+
}
829820
}

0 commit comments

Comments
 (0)