markedit 0.3.0

An ergonomic library for manipulating unstructured Markdown documents.
Documentation
use criterion::{
    criterion_group, criterion_main, BenchmarkId, Criterion, Throughput,
};
use markedit::{Heading, Matcher, Rewriter, Writer};
use pulldown_cmark::Event;
use std::path::{Path, PathBuf};

fn known_markdown_files() -> impl Iterator<Item = PathBuf> {
    let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
    let markedit_files = vec![
        manifest_dir.join("README.md"),
        manifest_dir.join("LICENSE_APACHE.md"),
    ];

    let blog_posts_pattern = concat!(
        env!("CARGO_MANIFEST_DIR"),
        "/benches/adventures.michaelfbryan.com/content/posts/**/*.md"
    );
    let blog_posts = glob::glob(blog_posts_pattern)
        .unwrap()
        .map(|entry| entry.unwrap());

    blog_posts.chain(markedit_files)
}

fn canonical_name(path: &Path) -> &str {
    let stem = path.file_stem().unwrap().to_str().unwrap();

    if stem == "index" {
        canonical_name(path.parent().unwrap())
    } else {
        stem
    }
}

pub fn rewriting(c: &mut Criterion) {
    let mut group = c.benchmark_group("Rewriting");

    for filename in known_markdown_files() {
        let src = std::fs::read_to_string(&filename).unwrap();
        let name = canonical_name(&filename);

        group
            .throughput(Throughput::Bytes(src.len() as u64))
            .bench_with_input(
                BenchmarkId::new("baseline parse", name),
                &src,
                |b, src| b.iter(|| markedit::parse(src).count()),
            )
            .bench_with_input(
                BenchmarkId::new("add text after each heading", name),
                &src,
                |b, src| {
                    b.iter(|| {
                        markedit::insert_markdown_before(
                            "## Sub-Heading",
                            Heading::with_level(2).falling_edge(),
                        )
                        .rewrite(markedit::parse(src))
                        .count()
                    })
                },
            )
            .bench_with_input(
                BenchmarkId::new("lowercase all text", name),
                &src,
                |b, src| {
                    b.iter(|| {
                        markedit::change_text(
                            |_| true,
                            |text| text.to_uppercase(),
                        )
                        .rewrite(markedit::parse(src))
                        .count()
                    })
                },
            )
            .bench_with_input(
                BenchmarkId::new("uppercase level 2 headings", name),
                &src,
                |b, src| {
                    b.iter(|| {
                        upper_case_header_text(2)
                            .rewrite(markedit::parse(src))
                            .count()
                    })
                },
            );
    }
}

fn upper_case_header_text<'src>(level: u32) -> impl Rewriter<'src> {
    let mut matcher = Heading::with_level(level);

    move |ev: Event<'src>, writer: &mut Writer<'src>| {
        if matcher.matches_event(&ev) {
            if let Event::Text(text) = ev {
                writer.push(Event::Text(
                    text.into_string().to_uppercase().into(),
                ));
                return;
            }
        }

        writer.push(ev);
    }
}

criterion_group!(benches, rewriting);
criterion_main!(benches);