Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement and integrate an mdBook plugin to strip markup from headings #4195

Merged
merged 3 commits into from
Jan 8, 2025
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
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
Loading