Skip to content

Commit

Permalink
Merge pull request #4195 from rust-lang/mdbook-strip-heading-markup
Browse files Browse the repository at this point in the history
Implement and integrate an mdBook plugin to strip markup from headings
  • Loading branch information
chriskrycho authored Jan 8, 2025
2 parents fd0c2e9 + d82e6ea commit 2e4cd43
Show file tree
Hide file tree
Showing 10 changed files with 1,138 additions and 651 deletions.
3 changes: 3 additions & 0 deletions nostarch/book.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,8 @@ output-mode = "simple"
[preprocessor.trpl-figure]
output-mode = "simple"

[preprocessor.trpl-heading]
output-mode = "simple"

[rust]
edition = "2021"
1,378 changes: 751 additions & 627 deletions nostarch/chapter21.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions packages/mdbook-trpl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ path = "src/bin/note.rs"
name = "mdbook-trpl-listing"
path = "src/bin/listing.rs"

[[bin]]
name = "mdbook-trpl-heading"
path = "src/bin/heading.rs"

[[bin]]
name = "mdbook-trpl-figure"
path = "src/bin/figure.rs"
Expand Down
38 changes: 38 additions & 0 deletions packages/mdbook-trpl/src/bin/heading.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use std::io;

use clap::{self, Parser, Subcommand};
use mdbook::preprocess::{CmdPreprocessor, Preprocessor};

use mdbook_trpl::Heading;

fn main() -> Result<(), String> {
let cli = Cli::parse();
if let Some(Command::Supports { renderer }) = cli.command {
return if Heading.supports_renderer(&renderer) {
Ok(())
} else {
Err(format!("Renderer '{renderer}' is unsupported"))
};
}

let (ctx, book) = CmdPreprocessor::parse_input(io::stdin())
.map_err(|e| format!("{e}"))?;
let processed = Heading.run(&ctx, book).map_err(|e| format!("{e}"))?;
serde_json::to_writer(io::stdout(), &processed).map_err(|e| format!("{e}"))
}

/// A simple preprocessor for semantic markup for code listings in _The Rust
/// Programming Language_.
#[derive(Parser, Debug)]
struct Cli {
#[command(subcommand)]
command: Option<Command>,
}

#[derive(Subcommand, Debug)]
enum Command {
/// Is the renderer supported?
///
/// This supports the HTML
Supports { renderer: String },
}
2 changes: 1 addition & 1 deletion packages/mdbook-trpl/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
use mdbook::preprocess::PreprocessorContext;

#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Mode {
Default,
Simple,
Expand Down
15 changes: 1 addition & 14 deletions packages/mdbook-trpl/src/figure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use mdbook::{book::Book, preprocess::Preprocessor, BookItem};
use pulldown_cmark::Event;
use pulldown_cmark_to_cmark::cmark;

use crate::config::Mode;
use crate::{config::Mode, CompositeError};

/// A simple preprocessor to rewrite `<figure>`s with `<img>`s.
///
Expand Down Expand Up @@ -74,19 +74,6 @@ impl Preprocessor for TrplFigure {
}
}

#[derive(Debug, thiserror::Error)]
struct CompositeError(Vec<anyhow::Error>);

impl std::fmt::Display for CompositeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Error(s) rewriting input: {}",
self.0.iter().map(|e| format!("{e:?}")).collect::<String>()
)
}
}

const OPEN_FIGURE: &'static str = "<figure>";
const CLOSE_FIGURE: &'static str = "</figure>";

Expand Down
114 changes: 114 additions & 0 deletions packages/mdbook-trpl/src/heading/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use anyhow::anyhow;
use mdbook::{
book::Book,
preprocess::{Preprocessor, PreprocessorContext},
BookItem,
};
use pulldown_cmark::{Event, Tag, TagEnd};
use pulldown_cmark_to_cmark::cmark;

use crate::{CompositeError, Mode};

pub struct TrplHeading;

impl Preprocessor for TrplHeading {
fn name(&self) -> &str {
"trpl-heading"
}

fn run(
&self,
ctx: &PreprocessorContext,
mut book: Book,
) -> anyhow::Result<Book> {
let mode = Mode::from_context(ctx, self.name())?;

let mut errors = vec![];
book.for_each_mut(|item| {
if let BookItem::Chapter(ref mut chapter) = item {
match rewrite_headings(&chapter.content, mode) {
Ok(rewritten) => chapter.content = rewritten,
Err(reason) => errors.push(reason),
}
}
});

if errors.is_empty() {
Ok(book)
} else {
Err(CompositeError(errors).into())
}
}

fn supports_renderer(&self, renderer: &str) -> bool {
renderer == "html" || renderer == "markdown" || renderer == "test"
}
}

fn rewrite_headings(src: &str, mode: Mode) -> anyhow::Result<String> {
// Don't rewrite anything for the default mode.
if mode == Mode::Default {
return Ok(src.into());
}

#[derive(Default)]
struct State<'e> {
in_heading: bool,
events: Vec<Event<'e>>,
}

let final_state: State = crate::parser(src).try_fold(
State::default(),
|mut state, event| -> anyhow::Result<State> {
if state.in_heading {
match event {
// When we see the start or end of any of the inline tags
// (emphasis, strong emphasis, or strikethrough), or any
// inline HTML tags, we just skip emitting them. As dumb as
// that may seem, it does the job!
Event::Start(
Tag::Emphasis | Tag::Strong | Tag::Strikethrough,
)
| Event::End(
TagEnd::Emphasis
| TagEnd::Strong
| TagEnd::Strikethrough,
)
| Event::InlineHtml(_) => { /* skip */ }

// For code, we just emit the body of the inline code block,
// unchanged (the wrapping backticks are not present here).
Event::Code(code) => {
state.events.push(Event::Text(code));
}

// Assume headings are well-formed; you cannot have a nested
// headings, so we don't have to check heading level.
Event::End(TagEnd::Heading(_)) => {
state.in_heading = false;
state.events.push(event);
}
_ => state.events.push(event),
}
} else if matches!(event, Event::Start(Tag::Heading { .. })) {
state.events.push(event);
state.in_heading = true;
} else {
state.events.push(event);
}

Ok(state)
},
)?;

if final_state.in_heading {
return Err(anyhow!("Unclosed heading"));
}

let mut rewritten = String::new();
cmark(final_state.events.into_iter(), &mut rewritten)?;
Ok(rewritten)
}

#[cfg(test)]
mod tests;
Loading

0 comments on commit 2e4cd43

Please sign in to comment.