Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

docs(adr): ADR-0012: MDBook Renderer Trait Implementation

Aperta
#20 0 commenti 0 reazioni 1 assegnatario Vedi su GitHub

@aRustyDev ci sta già lavorando.

Dal 5/1/2026.

Valutazione

Questa issue non è ancora stata valutata.

Descrizione

documentation

ADR-0012: MDBook Renderer Trait Implementation

Status

Accepted

Context

mdbook-htmx is an alternative backend for MDBook. To integrate properly with MDBook's build pipeline, we need to understand and implement the Renderer trait from mdbook-renderer crate, and correctly consume the RenderContext JSON structure passed via stdin.

This ADR documents the integration approach and the data structures we receive from MDBook.

Decision Drivers

  1. Compatibility - Must work with standard mdbook build command
  2. Future-Proofing - Handle MDBook version changes gracefully
  3. Extension - Support custom frontmatter beyond MDBook's standard fields
  4. Error Handling - Fail gracefully with actionable error messages

Decision

Implement as a standalone binary that reads RenderContext JSON from stdin.

This is the standard pattern for MDBook backends, allowing:

  • Installation via cargo install mdbook-htmx
  • Invocation via [output.htmx] in book.toml
  • No changes required to MDBook itself

RenderContext Structure

MDBook passes a JSON representation of RenderContext via stdin:

{
  "version": "0.4.40",
  "root": "/path/to/book",
  "book": {
    "items": [
      {
        "Chapter": {
          "name": "Introduction",
          "content": "# Introduction\n\nWelcome to...",
          "number": [1],
          "sub_items": [],
          "path": "intro.md",
          "source_path": "intro.md",
          "parent_names": []
        }
      },
      "Separator",
      {
        "PartTitle": "Getting Started"
      },
      {
        "Chapter": {
          "name": "Installation",
          "content": "# Installation\n\n...",
          "number": [2],
          "sub_items": [
            {
              "Chapter": {
                "name": "Linux",
                "content": "...",
                "number": [2, 1],
                "sub_items": [],
                "path": "install/linux.md",
                "source_path": "install/linux.md",
                "parent_names": ["Installation"]
              }
            }
          ],
          "path": "install/index.md",
          "source_path": "install/README.md",
          "parent_names": []
        }
      }
    ]
  },
  "config": {
    "book": {
      "title": "Example Book",
      "authors": ["John Doe"],
      "description": "An example book",
      "src": "src",
      "language": "en"
    },
    "build": {
      "build-dir": "book",
      "create-missing": true,
      "use-default-preprocessors": true
    },
    "output": {
      "htmx": {
        "version": "2.0.4",
        "boost": true,
        "template-engine": "tera"
      }
    }
  },
  "destination": "/path/to/book/htmx"
}
Key Fields
Field Type Description
version String MDBook version (for compatibility checks)
root PathBuf Book's root directory
book.items Vec Tree of chapters, separators, part titles
config Config Full book.toml configuration
destination PathBuf Output directory for this renderer
BookItem Variants
enum BookItem {
    Chapter(Chapter),
    Separator,
    PartTitle(String),
}
Chapter Structure
Field Type Description
name String Chapter title
content String Raw markdown content
number Option<Vec> Section number (e.g., [1, 2] = "1.2.")
sub_items Vec Nested chapters
path Option Logical path (README → index.md)
source_path Option Actual file path
parent_names Vec Ancestor chapter names

Implementation

Entry Point
use std::io;
use mdbook_renderer::RenderContext;

fn main() -> anyhow::Result<()> {
    // Initialize logging
    env_logger::init();

    // Check for "supports" subcommand
    if let Some(arg) = std::env::args().nth(1) {
        if arg == "supports" {
            // We support all renderers
            return Ok(());
        }
    }

    // Read RenderContext from stdin
    let ctx = RenderContext::from_json(&mut io::stdin())?;

    // Validate MDBook version
    check_version(&ctx.version)?;

    // Load our configuration
    let config: HtmxConfig = ctx.config
        .get_deserialized("output.htmx")?
        .unwrap_or_default();

    // Render the book
    let renderer = HtmxRenderer::new(config);
    renderer.render(&ctx)?;

    Ok(())
}
Version Compatibility
fn check_version(version: &str) -> anyhow::Result<()> {
    let semver: semver::Version = version.parse()?;

    // Require MDBook 0.4.21+ for RenderContext stability
    let minimum = semver::Version::new(0, 4, 21);

    if semver < minimum {
        anyhow::bail!(
            "mdbook-htmx requires MDBook {} or later, found {}",
            minimum, version
        );
    }

    Ok(())
}
Accessing Custom Configuration
use serde::Deserialize;

#[derive(Debug, Deserialize, Default)]
#[serde(default, rename_all = "kebab-case")]
pub struct HtmxConfig {
    pub version: String,
    pub boost: bool,
    pub swap_strategy: String,
    pub target: String,
    pub push_url: bool,
    pub template_engine: String,
    pub theme: Option<String>,
    pub output_mode: OutputMode,
    pub navigation: NavigationConfig,
    pub authz: AuthzConfig,
    pub authn: AuthnConfig,
    pub search: SearchConfig,
    pub assets: AssetsConfig,
}

#[derive(Debug, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub enum OutputMode {
    Full,
    Fragments,
    #[default]
    Both,
}

Rendering Pipeline

  1. Parse RenderContext - Deserialize JSON from stdin
  2. Load Configuration - Extract [output.htmx] settings
  3. Initialize Tera - Load templates from theme or defaults
  4. Register Custom Filters - Add asset(), url(), etc.
  5. Iterate Chapters - Walk book.items depth-first
  6. Extract Frontmatter - Parse YAML from chapter content
  7. Render HTML - Apply Tera templates to each chapter
  8. Generate Manifest - Build manifest.json from collected metadata
  9. Build Search Index - Generate search-index.json
  10. Copy Assets - Handle CSS, JS, images
impl Renderer for HtmxRenderer {
    fn name(&self) -> &str {
        "htmx"
    }

    fn render(&self, ctx: &RenderContext) -> Result<()> {
        // Ensure destination exists
        fs::create_dir_all(&ctx.destination)?;

        // Initialize template engine
        let tera = self.init_tera(ctx)?;

        // Collect page metadata
        let mut pages = Vec::new();

        // Render each chapter
        for item in ctx.book.iter() {
            if let BookItem::Chapter(ch) = item {
                if ch.is_draft_chapter() {
                    continue;
                }

                let page = self.render_chapter(ctx, &tera, ch)?;
                pages.push(page);
            }
        }

        // Generate manifest
        self.write_manifest(ctx, &pages)?;

        // Generate search index
        if self.config.search.enabled {
            self.write_search_index(ctx, &pages)?;
        }

        // Copy assets
        self.copy_assets(ctx)?;

        Ok(())
    }
}

Custom Tera Filters

Register filters for template use:

fn init_tera(&self, ctx: &RenderContext) -> Result<Tera> {
    let mut tera = Tera::default();

    // Load templates from theme or defaults
    let templates_dir = self.config.theme
        .as_ref()
        .map(|t| ctx.root.join(t))
        .unwrap_or_else(|| self.default_templates());

    tera.add_template_files(/* ... */)?;

    // Register custom filters
    let destination = ctx.destination.clone();
    tera.register_function("asset", make_asset_fn(destination));

    // Content filters
    tera.register_filter("markdown", markdown_filter);
    tera.register_filter("toc", toc_filter);

    Ok(tera)
}

fn make_asset_fn(destination: PathBuf) -> impl Function {
    Box::new(move |args: &HashMap<String, Value>| -> tera::Result<Value> {
        match args.get("path") {
            Some(val) => {
                let path: String = from_value(val.clone())?;
                // In production, would add content hash
                Ok(to_value(format!("/assets/{}", path))?)
            }
            None => Err("asset() requires 'path' argument".into()),
        }
    })
}

Error Handling

Fail with clear, actionable errors:

// Bad: process::exit(1) with no message
// Good: anyhow with context

fn render_chapter(&self, ctx: &RenderContext, tera: &Tera, ch: &Chapter)
    -> Result<PageMeta>
{
    let path = ch.path.as_ref()
        .ok_or_else(|| anyhow::anyhow!(
            "Chapter '{}' has no path (draft chapter?)", ch.name
        ))?;

    let frontmatter = extract_frontmatter(&ch.content)
        .with_context(|| format!(
            "Failed to parse frontmatter in '{}'", path.display()
        ))?;

    let html = tera.render("page.html", &context)
        .with_context(|| format!(
            "Failed to render template for '{}'", path.display()
        ))?;

    Ok(PageMeta { /* ... */ })
}

Consequences

Positive
  • Standard MDBook integration pattern
  • Receives full book structure including preprocessor output
  • Access to complete configuration
  • Works with existing MDBook tooling (mdbook watch, etc.)
Negative
  • No incremental builds (MDBook limitation)
  • Must re-render entire book on any change
  • Large books may have slow build times
Mitigation
  • Cache rendered fragments by content hash
  • Parallelize chapter rendering with Rayon
  • Consider implementing incremental rendering in future MDBook versions

Alternatives Considered

Library Integration

Implement as library linked into MDBook.

Rejected because:

  • Requires MDBook code changes
  • Harder to distribute
  • No standard pattern
Preprocessor Instead of Backend

Implement as a preprocessor that modifies content.

Rejected because:

  • Preprocessors can't control output structure
  • Can't generate manifest.json
  • Wrong abstraction level

References

Lingua principale
Rust
Stelle
0
Fork
1
Metriche di merge delle PR
Nessuna PR unita negli ultimi 30g

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di aRustyDev/mdbook-htmx

Tutte le issue di aRustyDev/mdbook-htmx

Issue simili

Altre issue su Rust

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.