pulldown-html-ext 0.5.0

Extended HTML rendering capabilities for pulldown-cmark
Documentation
# Custom Writers Examples

This guide provides practical examples of implementing custom HTML writers for specialized rendering needs.

## Basic Custom Writer

Create a simple custom writer that adds Bootstrap classes:

```rust
use pulldown_html_ext::{
    HtmlWriter, HtmlConfig, HtmlState, create_html_renderer,
    HeadingLevel, HtmlError
};
use pulldown_cmark_escape::{StrWrite, FmtWriter};
use pulldown_cmark::{Parser, CowStr, Event};

struct BootstrapWriter<W: StrWrite> {
    writer: W,
    config: HtmlConfig,
    state: HtmlState,
}

impl<W: StrWrite> BootstrapWriter<W> {
    fn new(writer: W, config: HtmlConfig) -> Self {
        Self {
            writer,
            config,
            state: HtmlState::new(),
        }
    }
}

impl<W: StrWrite> HtmlWriter<W> for BootstrapWriter<W> {
    fn get_writer(&mut self) -> &mut W {
        &mut self.writer
    }

    fn get_config(&self) -> &HtmlConfig {
        &self.config
    }

    fn get_state(&mut self) -> &mut HtmlState {
        &mut self.state
    }

    fn start_paragraph(&mut self) -> Result<(), HtmlError> {
        self.write_str(r#"<p class="lead">"#)
    }

    fn start_heading(
        &mut self,
        level: HeadingLevel,
        id: Option<&str>,
        classes: &[CowStr],
        attrs: &Vec<(CowStr, Option<CowStr>)>
    ) -> Result<(), HtmlError> {
        let level_num = level as u8;
        let display_class = match level_num {
            1 => "display-1",
            2 => "display-2",
            3 => "display-3",
            _ => "display-4",
        };
        
        self.write_str(&format!(r#"<h{} class="{} fw-bold""#, level_num, display_class))?;
        
        if let Some(id) = id {
            self.write_str(&format!(r#" id="{}""#, id))?;
        }
        
        if !classes.is_empty() {
            let class_str = classes.iter().map(|s| s.as_ref()).collect::<Vec<_>>().join(" ");
            self.write_str(&format!(r#" class="{}""#, class_str))?;
        }
        
        for (key, value) in attrs {
            self.write_str(" ")?;
            self.write_str(key)?;
            if let Some(val) = value {
                self.write_str(&format!(r#"="{}""#, val))?;
            }
        }
        
        self.write_str(">")
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let markdown = r#"
# Main Title

This is a paragraph.

## Section Title

Another paragraph here.
    "#;

    let mut output = String::new();
    let writer = BootstrapWriter::new(
        FmtWriter(&mut output),
        HtmlConfig::default()
    );
    let mut renderer = create_html_renderer(writer);
    
    let parser = Parser::new(markdown);
    renderer.run(parser)?;
    println!("{}", output);
    Ok(())
}
```

## Writer with Custom State

Implement a writer that tracks and numbers sections:

```rust
use std::collections::VecDeque;
use pulldown_html_ext::{
    HtmlWriter, HtmlConfig, HtmlState, create_html_renderer,
    HeadingLevel, HtmlError
};
use pulldown_cmark_escape::{StrWrite, FmtWriter};
use pulldown_cmark::{Parser, CowStr};

#[derive(Default)]
struct SectionNumbers {
    current: VecDeque<u32>,
}

impl SectionNumbers {
    fn push(&mut self) {
        self.current.push_back(1);
    }

    fn pop(&mut self) {
        self.current.pop_back();
    }

    fn increment_current(&mut self) {
        if let Some(last) = self.current.back_mut() {
            *last += 1;
        }
    }

    fn to_string(&self) -> String {
        self.current
            .iter()
            .map(|n| n.to_string())
            .collect::<Vec<_>>()
            .join(".")
    }
}

struct NumberedSectionsWriter<W: StrWrite> {
    writer: W,
    config: HtmlConfig,
    state: HtmlState,
    section_numbers: SectionNumbers,
}

impl<W: StrWrite> NumberedSectionsWriter<W> {
    fn new(writer: W, config: HtmlConfig) -> Self {
        Self {
            writer,
            config,
            state: HtmlState::new(),
            section_numbers: SectionNumbers::default(),
        }
    }
}

impl<W: StrWrite> HtmlWriter<W> for NumberedSectionsWriter<W> {
    fn get_writer(&mut self) -> &mut W {
        &mut self.writer
    }

    fn get_config(&self) -> &HtmlConfig {
        &self.config
    }

    fn get_state(&mut self) -> &mut HtmlState {
        &mut self.state
    }

    fn start_heading(
        &mut self,
        level: HeadingLevel,
        id: Option<&str>,
        classes: &[CowStr],
        attrs: &Vec<(CowStr, Option<CowStr>)>
    ) -> Result<(), HtmlError> {
        let level_num = level as u8;
        
        // Update section numbers
        while self.section_numbers.current.len() < level_num as usize {
            self.section_numbers.push();
        }
        while self.section_numbers.current.len() > level_num as usize {
            self.section_numbers.pop();
        }
        if !self.section_numbers.current.is_empty() {
            self.section_numbers.increment_current();
        }

        let section_number = self.section_numbers.to_string();
        
        // Write the heading tag with number
        self.write_str(&format!(
            r#"<h{} id="section-{}""#,
            level_num, section_number
        ))?;
        
        if !classes.is_empty() {
            let class_str = classes.iter().map(|s| s.as_ref()).collect::<Vec<_>>().join(" ");
            self.write_str(&format!(r#" class="{}""#, class_str))?;
        }
        
        for (key, value) in attrs {
            self.write_str(" ")?;
            self.write_str(key)?;
            if let Some(val) = value {
                self.write_str(&format!(r#"="{}""#, val))?;
            }
        }
        
        self.write_str(">")?;
        self.write_str(&format!("{} ", section_number))?;
        
        Ok(())
    }
}
```

## Writer with Enhanced Code Blocks

Create a writer that adds line numbers and copy buttons to code blocks:

```rust
use pulldown_html_ext::{
    HtmlWriter, HtmlConfig, HtmlState, create_html_renderer,
    CodeBlockKind, HtmlError
};
use pulldown_cmark_escape::{StrWrite, FmtWriter};
use pulldown_cmark::Parser;

struct EnhancedCodeWriter<W: StrWrite> {
    writer: W,
    config: HtmlConfig,
    state: HtmlState,
    line_count: usize,
}

impl<W: StrWrite> EnhancedCodeWriter<W> {
    fn new(writer: W, config: HtmlConfig) -> Self {
        Self {
            writer,
            config,
            state: HtmlState::new(),
            line_count: 0,
        }
    }
}

impl<W: StrWrite> HtmlWriter<W> for EnhancedCodeWriter<W> {
    // ... implementation of required trait methods ...

    fn start_code_block(&mut self, kind: CodeBlockKind) -> Result<(), HtmlError> {
        self.line_count = 0;
        self.get_state().currently_in_code_block = true;

        // Write container div
        self.write_str(r#"<div class="code-block-wrapper">"#)?;
        
        // Add copy button
        self.write_str(r#"<button class="copy-button" onclick="copyCode(this)">Copy</button>"#)?;
        
        // Start pre and code tags
        self.write_str("<pre><code")?;
        
        if let CodeBlockKind::Fenced(info) = kind {
            if !info.is_empty() {
                self.write_str(&format!(r#" class="language-{}">"#, info))?;
            } else {
                self.write_str(">")?;
            }
        } else {
            self.write_str(">")?;
        }
        
        Ok(())
    }

    fn text(&mut self, text: &str) -> Result<(), HtmlError> {
        if self.get_state().currently_in_code_block {
            for line in text.lines() {
                self.line_count += 1;
                self.write_str(&format!(
                    r#"<span class="line-number">{}</span>{}\n"#,
                    self.line_count, line
                ))?;
            }
            Ok(())
        } else {
            self.write_str(text)
        }
    }
}
```


## Next Steps

- Read the [Custom Writers Guide]../guide/custom-writers.md for detailed information
- Check the [Configuration Examples]custom-config.md for more customization options
- Explore the [HTML Rendering Guide]../guide/html-rendering.md for rendering details