Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ mejiro-cli list
List post metadata stored in the `posts` directory. Use `-a` to include
unpublished drafts.

## 🧩 Code Block Insertion

Embed code from external files directly into your Markdown posts:

1. Create a directory for your post, e.g. `posts/20250618-test/`.
2. Add the code files you want to reference into that directory.
3. Use `@code[path/to/file]` in your Markdown to inline the file contents.

Example:

```markdown
Here's my code:

@code[20250618-test/main.py]
```


## About

Expand Down
68 changes: 68 additions & 0 deletions docs/DEV.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Mejiro CLI - Developer Guide

## Prerequisites

- Rust (edition 2024)
- `miniserve` for local testing (optional): `cargo install miniserve`

## Project Structure

This is a Rust workspace with 4 modules:

```
mejiro/
├── config/ # Blog configuration (YAML parsing)
├── html/ # HTML generation and markdown parsing
├── mejiro-cli/ # Main CLI application
├── search/ # WASM-based search functionality
```

## Setup

1. Clone the repository
2. Build the project:
```bash
cargo build
```

## Running Tests

Run all tests:
```bash
cargo test
```

Run tests for a specific package:
```bash
cargo test --package html
cargo test --package config
cargo test --package mejiro-cli
```

## Development Workflow

### Running the CLI

```bash
# Run from project root
cargo run -- <command>

# Examples:
cargo run -- new # Create a new blog post
cargo run -- compile # Compile markdown to HTML
cargo run -- list # List all posts
```

### Testing Locally

After compiling, serve the static site locally:

```bash
# Compile the blog
cargo run -- compile

# Serve the output directory
miniserve mejiro-cli/public -p 8080

# Open http://localhost:8080 in your browser
```
3 changes: 3 additions & 0 deletions html/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ chrono = "0.4.41"
pulldown-cmark = "0.13.0"
serde = { version = "1.0.219", features = ["derive"] }
serde_yaml = "0.9.34"

[dev-dependencies]
tempfile = "3"
165 changes: 165 additions & 0 deletions html/src/code_block.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
use std::fs;
use std::path::Path;

/// Infers the language identifier from a file path based on its extension.
fn infer_language_from_path(path: &str) -> &str {
if let Some(ext) = Path::new(path).extension() {
match ext.to_str().unwrap_or("") {
"rs" => "rust",
"py" => "python",
"js" => "javascript",
"ts" => "typescript",
"tsx" => "typescript",
"jsx" => "javascript",
"java" => "java",
"c" => "c",
"cpp" | "cc" | "cxx" | "hpp" | "h" => "cpp",
"go" => "go",
"rb" => "ruby",
"sh" | "bash" => "bash",
"yaml" | "yml" => "yaml",
"json" => "json",
"html" => "html",
"css" => "css",
"scss" | "sass" => "scss",
"md" => "markdown",
"sql" => "sql",
"xml" => "xml",
"toml" => "toml",
"r" => "r",
"php" => "php",
"swift" => "swift",
"kt" | "kts" => "kotlin",
"scala" => "scala",
_ => "",
}
} else {
""
}
}

/// Preprocesses markdown content to replace @code[filepath] directives with actual code blocks.
///
/// Searches for @code[...] patterns and replaces them with markdown code blocks containing
/// the content of the referenced files. The file paths are resolved relative to the
/// directory containing the markdown file.
///
/// If a file is not found, the @code[...] directive is left as-is in the output.
///
/// # Arguments
/// * `markdown` - The markdown content to preprocess
/// * `base_dir` - The directory containing the markdown file (used to resolve relative paths)
///
/// # Returns
/// The preprocessed markdown with @code[...] directives replaced by code blocks (or left as-is if files don't exist)
///
/// # Example
/// ```ignore
/// // In markdown: @code[20250608-8LVpG/main.py]
/// // Gets replaced with:
/// // ```python
/// // <content of main.py>
/// // ```
/// ```
pub fn preprocess_code_includes(markdown: &str, base_dir: &Path) -> String {
let mut result = String::new();
let mut last_pos = 0;

while let Some(start) = markdown[last_pos..].find("@code[") {
let start_pos = last_pos + start;

// Append everything before @code[
result.push_str(&markdown[last_pos..start_pos]);

// Find the closing ]
if let Some(end) = markdown[start_pos + 6..].find(']') {
let end_pos = start_pos + 6 + end;
let file_path = &markdown[start_pos + 6..end_pos];

// Resolve the file path relative to the markdown file's directory
let full_path = base_dir.join(file_path);

// Try to read the file content
match fs::read_to_string(&full_path) {
Ok(code_content) => {
// Infer language from extension
let lang = infer_language_from_path(file_path);

// Create markdown code block
let code_block = format!("```{}\n{}\n```", lang, code_content);
result.push_str(&code_block);
}
Err(_) => {
// File not found - leave the @code[...] directive as-is
result.push_str(&markdown[start_pos..end_pos + 1]);
}
}

last_pos = end_pos + 1;
} else {
// No closing bracket found, just append the @code[ and continue
result.push_str("@code[");
last_pos = start_pos + 6;
}
}

// Append the remaining content
result.push_str(&markdown[last_pos..]);

result
}

#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
use tempfile::TempDir;

#[test]
fn test_infer_language_from_path() {
assert_eq!(infer_language_from_path("main.py"), "python");
assert_eq!(infer_language_from_path("app.rs"), "rust");
assert_eq!(infer_language_from_path("index.js"), "javascript");
assert_eq!(infer_language_from_path("App.tsx"), "typescript");
assert_eq!(infer_language_from_path("test.go"), "go");
assert_eq!(infer_language_from_path("unknown.xyz"), "");
}

#[test]
fn test_preprocess_code_includes() {
let temp_dir = TempDir::new().unwrap();
let code_file = temp_dir.path().join("test.py");

let mut file = fs::File::create(&code_file).unwrap();
writeln!(file, "def hello():").unwrap();
writeln!(file, " print('Hello, World!')").unwrap();

let markdown = "# Test\n\n@code[test.py]\n\nSome text";
let result = preprocess_code_includes(markdown, temp_dir.path());

assert!(result.contains("```python"));
assert!(result.contains("def hello():"));
assert!(result.contains("print('Hello, World!')"));
assert!(result.contains("Some text"));
}

#[test]
fn test_preprocess_code_includes_missing_file() {
let temp_dir = TempDir::new().unwrap();
let markdown = "@code[nonexistent.py]";
let result = preprocess_code_includes(markdown, temp_dir.path());

// When file doesn't exist, the directive should be left as-is
assert_eq!(result, "@code[nonexistent.py]");
}

#[test]
fn test_preprocess_no_code_includes() {
let temp_dir = TempDir::new().unwrap();
let markdown = "# Test\n\nJust regular markdown content.";
let result = preprocess_code_includes(markdown, temp_dir.path());

assert_eq!(result, markdown);
}
}
1 change: 1 addition & 0 deletions html/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod aside;
pub mod code_block;
mod footer;
mod icon;
mod index;
Expand Down
Loading