-
Notifications
You must be signed in to change notification settings - Fork 87
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
30 changed files
with
1,549 additions
and
2,022 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
22 changes: 0 additions & 22 deletions
22
rustbook-en/packages/mdbook-trpl-note/tests/integration/main.rs
This file was deleted.
Oops, something went wrong.
484 changes: 233 additions & 251 deletions
484
...n/packages/mdbook-trpl-listing/Cargo.lock → rustbook-en/packages/mdbook-trpl/Cargo.lock
Large diffs are not rendered by default.
Oops, something went wrong.
19 changes: 15 additions & 4 deletions
19
...n/packages/mdbook-trpl-listing/Cargo.toml → rustbook-en/packages/mdbook-trpl/Cargo.toml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
# mdbook_trpl | ||
|
||
A shared package for [mdbook][mdbook] [preprocessors][pre] used in [_The Rust | ||
Programming Language_][trpl]. | ||
|
||
Supplies the following preprocessor binaries: | ||
|
||
- [mdbook-trpl-note](./src/bin/note) | ||
- [mdbook-trpl-listing](./src/bin/listing) | ||
|
||
[mdbook]: https://crates.io/crates/mdbook | ||
[pre]: https://rust-lang.github.io/mdBook/format/configuration/preprocessors.html | ||
[trpl]: https://doc.rust-lang.org/book/ |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
use std::io; | ||
|
||
use clap::{self, Parser, Subcommand}; | ||
|
||
use mdbook::preprocess::{CmdPreprocessor, Preprocessor}; | ||
use mdbook_trpl::Figure; | ||
|
||
fn main() -> Result<(), String> { | ||
match Cli::parse().command { | ||
Some(Command::Supports { renderer }) => { | ||
if Figure.supports_renderer(&renderer) { | ||
Ok(()) | ||
} else { | ||
Err(format!("Renderer '{renderer}' is unsupported")) | ||
} | ||
} | ||
None => { | ||
let (ctx, book) = CmdPreprocessor::parse_input(io::stdin()) | ||
.map_err(|e| format!("{e}"))?; | ||
let processed = | ||
Figure.run(&ctx, book).map_err(|e| format!("{e}"))?; | ||
serde_json::to_writer(io::stdout(), &processed) | ||
.map_err(|e| format!("{e}")) | ||
} | ||
} | ||
} | ||
|
||
/// A simple preprocessor for handling figures with images in _The Rust | ||
/// Programming Language_ book. | ||
#[derive(Parser, Debug)] | ||
struct Cli { | ||
#[command(subcommand)] | ||
command: Option<Command>, | ||
} | ||
|
||
#[derive(Subcommand, Debug)] | ||
enum Command { | ||
/// Is the renderer supported? | ||
/// | ||
/// Supported renderers are `'html'`, `'markdown'`, and `'test'`. | ||
Supports { renderer: String }, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
//! Get any `preprocessor.trpl-*` config. | ||
use mdbook::preprocess::PreprocessorContext; | ||
|
||
#[derive(Debug, Clone, Copy)] | ||
pub enum Mode { | ||
Default, | ||
Simple, | ||
} | ||
|
||
impl Mode { | ||
pub fn from_context( | ||
ctx: &PreprocessorContext, | ||
preprocessor_name: &str, | ||
) -> Result<Mode, Error> { | ||
let config = ctx | ||
.config | ||
.get_preprocessor(preprocessor_name) | ||
.ok_or_else(|| Error::NoConfig(preprocessor_name.into()))?; | ||
|
||
let key = String::from("output-mode"); | ||
let mode = config | ||
.get(&key) | ||
.map(|value| match value.as_str() { | ||
Some(s) => Mode::try_from(s).map_err(|_| Error::BadValue { | ||
key, | ||
value: value.to_string(), | ||
}), | ||
None => Err(Error::BadValue { | ||
key, | ||
value: value.to_string(), | ||
}), | ||
}) | ||
.transpose()? | ||
.unwrap_or(Mode::Default); | ||
|
||
Ok(mode) | ||
} | ||
} | ||
|
||
/// Trivial marker struct to indicate an internal error. | ||
/// | ||
/// The caller has enough info to do what it needs without passing data around. | ||
pub struct ParseErr; | ||
|
||
impl TryFrom<&str> for Mode { | ||
type Error = ParseErr; | ||
|
||
fn try_from(value: &str) -> Result<Self, Self::Error> { | ||
match value { | ||
"default" => Ok(Mode::Default), | ||
"simple" => Ok(Mode::Simple), | ||
_ => Err(ParseErr), | ||
} | ||
} | ||
} | ||
|
||
#[derive(Debug, thiserror::Error)] | ||
pub enum Error { | ||
#[error(transparent)] | ||
Mdbook(#[from] mdbook::errors::Error), | ||
|
||
#[error("No config for '{0}'")] | ||
NoConfig(String), | ||
|
||
#[error("Bad config value '{value}' for key '{key}'")] | ||
BadValue { key: String, value: String }, | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
use anyhow::{anyhow, Result}; | ||
use html_parser::{Dom, Node}; | ||
use mdbook::{book::Book, preprocess::Preprocessor, BookItem}; | ||
|
||
use pulldown_cmark::Event; | ||
use pulldown_cmark_to_cmark::cmark; | ||
|
||
use crate::config::Mode; | ||
|
||
/// A simple preprocessor to rewrite `<figure>`s with `<img>`s. | ||
/// | ||
/// This is a no-op by default; it only operates on the book chapters when the | ||
/// `[preprocessor.trpl-figure]` has `output-mode = "simple"`. | ||
/// | ||
/// Takes in Markdown containing like this: | ||
/// | ||
/// ```markdown | ||
/// <figure> | ||
/// | ||
/// <img src="http://www.example.com/some-cool-image.jpg"> | ||
/// | ||
/// <figcaption>Figure 1-2: A description of the image</figcaption> | ||
/// | ||
/// </figure> | ||
/// ``` | ||
/// | ||
/// Spits out Markdown like this: | ||
/// | ||
/// ```markdown | ||
/// | ||
/// <img src="http://www.example.com/some-cool-image.jpg"> | ||
/// | ||
/// Figure 1-2: A description of the image | ||
/// | ||
/// ``` | ||
pub struct TrplFigure; | ||
|
||
impl TrplFigure { | ||
pub fn supports_renderer(&self, renderer: &str) -> bool { | ||
renderer == "html" || renderer == "markdown" || renderer == "test" | ||
} | ||
} | ||
|
||
impl Preprocessor for TrplFigure { | ||
fn name(&self) -> &str { | ||
"trpl-figure" | ||
} | ||
|
||
fn run( | ||
&self, | ||
ctx: &mdbook::preprocess::PreprocessorContext, | ||
mut book: Book, | ||
) -> Result<Book> { | ||
// The `<figure>`-based output is only replaced in the `Simple` mode. | ||
let Mode::Simple = Mode::from_context(ctx, self.name())? else { | ||
return Ok(book); | ||
}; | ||
|
||
let mut errors = vec![]; | ||
book.for_each_mut(|item| { | ||
if let BookItem::Chapter(ref mut chapter) = item { | ||
match rewrite_figure(&chapter.content) { | ||
Ok(rewritten) => chapter.content = rewritten, | ||
Err(reason) => errors.push(reason), | ||
} | ||
} | ||
}); | ||
|
||
if errors.is_empty() { | ||
Ok(book) | ||
} else { | ||
Err(CompositeError(errors).into()) | ||
} | ||
} | ||
} | ||
|
||
#[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>"; | ||
|
||
const OPEN_CAPTION: &'static str = "<figcaption>"; | ||
const CLOSE_CAPTION: &'static str = "</figcaption>"; | ||
|
||
fn rewrite_figure(text: &str) -> Result<String> { | ||
let final_state = crate::parser(text).try_fold( | ||
State { | ||
current: None, | ||
events: Vec::new(), | ||
}, | ||
|mut state, event| { | ||
match (event, &mut state.current) { | ||
// -- Open figure | ||
(Event::Html(tag), None) if tag.starts_with(OPEN_FIGURE) => { | ||
let mut figure = Figure::new(); | ||
figure.events.push(Event::Text("\n".into())); | ||
state.current.replace(figure); | ||
} | ||
|
||
(Event::Html(tag), Some(_)) if tag.starts_with(OPEN_FIGURE) => { | ||
return Err(anyhow!( | ||
"Opening `<figure>` when already in a `<figure>`" | ||
)) | ||
} | ||
|
||
// -- Close figure | ||
(Event::Html(tag), Some(figure)) | ||
if tag.starts_with(CLOSE_FIGURE) => | ||
{ | ||
if figure.in_caption { | ||
return Err(anyhow!("Unclosed `<figcaption>`")); | ||
} | ||
|
||
state.events.append(&mut figure.events); | ||
state.events.push(Event::Text("\n".into())); | ||
let _ = state.current.take(); | ||
} | ||
|
||
(Event::Html(tag), None) if tag.trim() == CLOSE_FIGURE => { | ||
return Err(anyhow!(bad_close(CLOSE_FIGURE, OPEN_CAPTION))); | ||
} | ||
|
||
// -- Start captions | ||
// We do not allow nested captions, but if we have not yet | ||
// started a caption, it is legal to start one, and we | ||
// intentionally ignore that event entirely other than tracking | ||
// that we have started a caption. We will push the body of the | ||
// caption into the figure’s events when we hit them. | ||
// | ||
// Note: this does not support `<figcaption class="...">`. | ||
(Event::Html(tag), Some(fig)) | ||
if tag.starts_with(OPEN_CAPTION) => | ||
{ | ||
if fig.in_caption { | ||
return Err(anyhow!(bad_open(OPEN_CAPTION))); | ||
} else { | ||
if tag.trim().ends_with(CLOSE_CAPTION) { | ||
let text = Dom::parse(tag.as_ref())? | ||
.children | ||
.into_iter() | ||
.filter_map(text_of) | ||
.collect::<String>(); | ||
|
||
if text.is_empty() { | ||
return Err(anyhow!( | ||
"Missing caption in `<figcaption>`" | ||
)); | ||
} | ||
|
||
fig.events.push(Event::Text(text.into())); | ||
} else { | ||
fig.events.push(Event::Text("\n".into())); | ||
fig.in_caption = true; | ||
} | ||
} | ||
} | ||
|
||
(Event::Html(tag), None) if tag.starts_with(OPEN_CAPTION) => { | ||
return Err(anyhow!(bad_open(OPEN_CAPTION))) | ||
} | ||
|
||
// -- Close captions | ||
(Event::Html(tag), Some(fig)) | ||
if tag.trim() == CLOSE_CAPTION => | ||
{ | ||
if fig.in_caption { | ||
fig.events.push(Event::Text("\n".into())); | ||
fig.in_caption = false; | ||
} else { | ||
return Err(anyhow!(bad_close( | ||
CLOSE_CAPTION, | ||
OPEN_CAPTION | ||
))); | ||
} | ||
} | ||
|
||
(Event::Html(tag), None) if tag.trim() == CLOSE_CAPTION => { | ||
return Err(anyhow!(bad_close(CLOSE_CAPTION, OPEN_FIGURE))); | ||
} | ||
|
||
// Otherwise, if in the body of a figure, push whatever other | ||
// events without modification into the figure state. | ||
(ev, Some(ref mut figure)) => figure.events.push(ev), | ||
|
||
// And if not in a figure, no modifications whatsoever. | ||
(ev, None) => state.events.push(ev), | ||
} | ||
Ok(state) | ||
}, | ||
)?; | ||
|
||
if final_state.current.is_some() { | ||
return Err(anyhow!("Unclosed `<figure>`")); | ||
} | ||
|
||
let mut rewritten = String::new(); | ||
cmark(final_state.events.into_iter(), &mut rewritten)?; | ||
Ok(rewritten) | ||
} | ||
|
||
fn text_of(node: Node) -> Option<String> { | ||
match node { | ||
Node::Text(text) => Some(text), | ||
Node::Element(element) => { | ||
Some(element.children.into_iter().filter_map(text_of).collect()) | ||
} | ||
Node::Comment(_) => None, | ||
} | ||
} | ||
|
||
fn bad_open(tag: &str) -> String { | ||
format!("Opening `<{tag}>` while not in a `<figure>`.") | ||
} | ||
|
||
fn bad_close(close: &str, required_open: &str) -> String { | ||
format!("Closing `<{close}>` while not in a `<{required_open}>`.") | ||
} | ||
|
||
#[derive(Debug)] | ||
struct State<'e> { | ||
current: Option<Figure<'e>>, | ||
events: Vec<Event<'e>>, | ||
} | ||
|
||
#[derive(Debug)] | ||
struct Figure<'e> { | ||
events: Vec<Event<'e>>, | ||
in_caption: bool, | ||
} | ||
|
||
impl<'e> Figure<'e> { | ||
fn new() -> Figure<'e> { | ||
Figure { | ||
events: vec![], | ||
in_caption: false, | ||
} | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
use super::*; | ||
|
||
#[test] | ||
fn text_without_figures_is_ignored() { | ||
let actual = rewrite_figure("This is some basic text.").unwrap(); | ||
assert_eq!(actual, "This is some basic text."); | ||
} | ||
|
||
#[test] | ||
fn text_with_figure_replaces_it_with_simple_text() { | ||
let actual = rewrite_figure( | ||
r#"<figure> | ||
<img src="http://www.example.com/some-image.jpg"> | ||
<figcaption>Figure 12-34: Look at this cool picture!</figcaption> | ||
</figure>"#, | ||
) | ||
.unwrap(); | ||
|
||
let expected = r#" | ||
<img src="http://www.example.com/some-image.jpg"> | ||
Figure 12-34: Look at this cool picture! | ||
"#; | ||
|
||
assert_eq!(actual, expected); | ||
} | ||
|
||
#[test] | ||
fn unclosed_figure() { | ||
let result = rewrite_figure("<figure>"); | ||
let actual = format!("{:?}", result.unwrap_err()); | ||
assert_eq!(actual, "Unclosed `<figure>`"); | ||
} | ||
|
||
#[test] | ||
fn empty_caption() { | ||
let result = rewrite_figure( | ||
"<figure> | ||
<figcaption></figcaption> | ||
</figure>", | ||
); | ||
let actual = format!("{:?}", result.unwrap_err()); | ||
assert_eq!(actual, "Missing caption in `<figcaption>`"); | ||
} | ||
|
||
#[test] | ||
fn unclosed_caption() { | ||
let result = rewrite_figure( | ||
"<figure> | ||
<figcaption> | ||
</figure>", | ||
); | ||
let actual = format!("{:?}", result.unwrap_err()); | ||
assert_eq!(actual, "Unclosed `<figcaption>`"); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
mod config; | ||
mod figure; | ||
mod listing; | ||
mod note; | ||
|
||
pub use config::Mode; | ||
pub use figure::TrplFigure as Figure; | ||
pub use listing::TrplListing as Listing; | ||
pub use note::TrplNote as Note; | ||
use pulldown_cmark::{Options, Parser}; | ||
|
||
/// Convenience function to get a parser matching `mdbook::new_cmark_parser`. | ||
/// | ||
/// This is implemented separately so we are decoupled from mdbook's dependency | ||
/// versions and can update at will (albeit with care to stay aligned with what | ||
/// mdbook does!) to later versions of `pulldown-cmark` and related tools. | ||
/// | ||
/// Notes: | ||
/// | ||
/// - `mdbook::new_cmark_parser` has an additional parameter which allows smart | ||
/// punctuation to be enabled or disabled; we always enable it. | ||
/// - We do not use footnotes in the text at present, but this goes out of its | ||
/// way to match this up to the old footnotes behavior just to make sure the | ||
/// parsing etc. is all the same. | ||
pub fn parser(text: &str) -> Parser<'_> { | ||
let mut opts = Options::empty(); | ||
opts.insert(Options::ENABLE_TABLES); | ||
opts.insert(Options::ENABLE_FOOTNOTES); | ||
opts.insert(Options::ENABLE_OLD_FOOTNOTES); | ||
opts.insert(Options::ENABLE_STRIKETHROUGH); | ||
opts.insert(Options::ENABLE_TASKLISTS); | ||
opts.insert(Options::ENABLE_HEADING_ATTRIBUTES); | ||
opts.insert(Options::ENABLE_SMART_PUNCTUATION); | ||
Parser::new_ext(text, opts) | ||
} |
311 changes: 143 additions & 168 deletions
311
...n/packages/mdbook-trpl-listing/src/lib.rs → ...n/packages/mdbook-trpl/src/listing/mod.rs
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
use mdbook::{ | ||
book::Book, | ||
errors::Result, | ||
preprocess::{Preprocessor, PreprocessorContext}, | ||
BookItem, | ||
}; | ||
use pulldown_cmark::{ | ||
Event::{self, *}, | ||
Tag, TagEnd, | ||
}; | ||
use pulldown_cmark_to_cmark::cmark; | ||
|
||
/// A simple preprocessor for semantic notes in _The Rust Programming Language_. | ||
/// | ||
/// Takes in Markdown like this: | ||
/// | ||
/// ```markdown | ||
/// > Note: This is a note. | ||
/// ``` | ||
/// | ||
/// Spits out Markdown like this: | ||
/// | ||
/// ```markdown | ||
/// <section class="note" aria-role="note"> | ||
/// | ||
/// This is a note. | ||
/// | ||
/// </section> | ||
/// ``` | ||
pub struct TrplNote; | ||
|
||
impl Preprocessor for TrplNote { | ||
fn name(&self) -> &str { | ||
"simple-note-preprocessor" | ||
} | ||
|
||
fn run(&self, _ctx: &PreprocessorContext, mut book: Book) -> Result<Book> { | ||
book.for_each_mut(|item| { | ||
if let BookItem::Chapter(ref mut chapter) = item { | ||
chapter.content = rewrite(&chapter.content); | ||
} | ||
}); | ||
Ok(book) | ||
} | ||
|
||
fn supports_renderer(&self, renderer: &str) -> bool { | ||
renderer == "html" || renderer == "markdown" || renderer == "test" | ||
} | ||
} | ||
|
||
pub fn rewrite(text: &str) -> String { | ||
let parser = crate::parser(text); | ||
|
||
let mut events = Vec::new(); | ||
let mut state = Default; | ||
|
||
for event in parser { | ||
match (&mut state, event) { | ||
(Default, Start(Tag::BlockQuote(_))) => { | ||
state = StartingBlockquote(vec![Start(Tag::BlockQuote(None))]); | ||
} | ||
|
||
(StartingBlockquote(blockquote_events), Text(content)) => { | ||
if content.starts_with("Note: ") { | ||
// This needs the "extra" `SoftBreak`s so that when the final rendering pass | ||
// happens, it does not end up treating the internal content as inline *or* | ||
// treating the HTML tags as inline tags: | ||
// | ||
// - Content inside HTML blocks is only rendered as Markdown when it is | ||
// separated from the block HTML elements: otherwise it gets treated as inline | ||
// HTML and *not* rendered. | ||
// - Along the same lines, an HTML tag that happens to be directly adjacent to | ||
// the end of a previous Markdown block will end up being rendered as part of | ||
// that block. | ||
events.extend([ | ||
SoftBreak, | ||
SoftBreak, | ||
Html( | ||
r#"<section class="note" aria-role="note">"#.into(), | ||
), | ||
SoftBreak, | ||
SoftBreak, | ||
Start(Tag::Paragraph), | ||
Text(content), | ||
]); | ||
state = InNote; | ||
} else { | ||
events.append(blockquote_events); | ||
events.push(Text(content)); | ||
state = Default; | ||
} | ||
} | ||
|
||
( | ||
StartingBlockquote(_blockquote_events), | ||
heading @ Start(Tag::Heading { .. }), | ||
) => { | ||
events.extend([ | ||
SoftBreak, | ||
SoftBreak, | ||
Html(r#"<section class="note" aria-role="note">"#.into()), | ||
SoftBreak, | ||
SoftBreak, | ||
heading, | ||
]); | ||
state = InNote; | ||
} | ||
|
||
(StartingBlockquote(ref mut events), Start(tag)) => { | ||
events.push(Start(tag)); | ||
} | ||
|
||
(InNote, End(TagEnd::BlockQuote(_))) => { | ||
// As with the start of the block HTML, the closing HTML must be | ||
// separated from the Markdown text by two newlines. | ||
events.extend([ | ||
SoftBreak, | ||
SoftBreak, | ||
Html("</section>".into()), | ||
]); | ||
state = Default; | ||
} | ||
|
||
(_, event) => { | ||
events.push(event); | ||
} | ||
} | ||
} | ||
|
||
let mut buf = String::new(); | ||
cmark(events.into_iter(), &mut buf).unwrap(); | ||
buf | ||
} | ||
|
||
use State::*; | ||
|
||
#[derive(Debug)] | ||
enum State<'e> { | ||
Default, | ||
StartingBlockquote(Vec<Event<'e>>), | ||
InNote, | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
use super::*; | ||
|
||
#[test] | ||
fn no_note() { | ||
let text = "Hello, world.\n\nThis is some text."; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<p>Hello, world.</p>\n<p>This is some text.</p>\n" | ||
); | ||
} | ||
|
||
#[test] | ||
fn with_note() { | ||
let text = "> Note: This is some text.\n> It keeps going."; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<section class=\"note\" aria-role=\"note\">\n<p>Note: This is some text.\nIt keeps going.</p>\n</section>" | ||
); | ||
} | ||
|
||
#[test] | ||
fn regular_blockquote() { | ||
let text = "> This is some text.\n> It keeps going."; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<blockquote>\n<p>This is some text.\nIt keeps going.</p>\n</blockquote>\n" | ||
); | ||
} | ||
|
||
#[test] | ||
fn combined() { | ||
let text = "> Note: This is some text.\n> It keeps going.\n\nThis is regular text.\n\n> This is a blockquote.\n"; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<section class=\"note\" aria-role=\"note\">\n<p>Note: This is some text.\nIt keeps going.</p>\n</section>\n<p>This is regular text.</p>\n<blockquote>\n<p>This is a blockquote.</p>\n</blockquote>\n" | ||
); | ||
} | ||
|
||
#[test] | ||
fn blockquote_then_note() { | ||
let text = "> This is quoted.\n\n> Note: This is noted."; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<blockquote>\n<p>This is quoted.</p>\n</blockquote>\n<section class=\"note\" aria-role=\"note\">\n<p>Note: This is noted.</p>\n</section>" | ||
); | ||
} | ||
|
||
#[test] | ||
fn note_then_blockquote() { | ||
let text = "> Note: This is noted.\n\n> This is quoted."; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<section class=\"note\" aria-role=\"note\">\n<p>Note: This is noted.</p>\n</section>\n<blockquote>\n<p>This is quoted.</p>\n</blockquote>\n" | ||
); | ||
} | ||
|
||
#[test] | ||
fn with_h1_note() { | ||
let text = "> # Header\n > And then some note content."; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<section class=\"note\" aria-role=\"note\">\n<h1>Header</h1>\n<p>And then some note content.</p>\n</section>" | ||
); | ||
} | ||
|
||
#[test] | ||
fn with_h2_note() { | ||
let text = "> ## Header\n > And then some note content."; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<section class=\"note\" aria-role=\"note\">\n<h2>Header</h2>\n<p>And then some note content.</p>\n</section>" | ||
); | ||
} | ||
|
||
#[test] | ||
fn with_h3_note() { | ||
let text = "> ### Header\n > And then some note content."; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<section class=\"note\" aria-role=\"note\">\n<h3>Header</h3>\n<p>And then some note content.</p>\n</section>" | ||
); | ||
} | ||
|
||
#[test] | ||
fn with_h4_note() { | ||
let text = "> #### Header\n > And then some note content."; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<section class=\"note\" aria-role=\"note\">\n<h4>Header</h4>\n<p>And then some note content.</p>\n</section>" | ||
); | ||
} | ||
|
||
#[test] | ||
fn with_h5_note() { | ||
let text = "> ##### Header\n > And then some note content."; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<section class=\"note\" aria-role=\"note\">\n<h5>Header</h5>\n<p>And then some note content.</p>\n</section>" | ||
); | ||
} | ||
|
||
#[test] | ||
fn with_h6_note() { | ||
let text = "> ###### Header\n > And then some note content."; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<section class=\"note\" aria-role=\"note\">\n<h6>Header</h6>\n<p>And then some note content.</p>\n</section>" | ||
); | ||
} | ||
|
||
#[test] | ||
fn h1_then_blockquote() { | ||
let text = | ||
"> # Header\n > And then some note content.\n\n> This is quoted."; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<section class=\"note\" aria-role=\"note\">\n<h1>Header</h1>\n<p>And then some note content.</p>\n</section>\n<blockquote>\n<p>This is quoted.</p>\n</blockquote>\n" | ||
); | ||
} | ||
|
||
#[test] | ||
fn blockquote_then_h1_note() { | ||
let text = | ||
"> This is quoted.\n\n> # Header\n > And then some note content."; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<blockquote>\n<p>This is quoted.</p>\n</blockquote>\n<section class=\"note\" aria-role=\"note\">\n<h1>Header</h1>\n<p>And then some note content.</p>\n</section>" | ||
); | ||
} | ||
|
||
#[test] | ||
fn blockquote_with_strong() { | ||
let text = "> **Bold text in a paragraph.**"; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<blockquote>\n<p><strong>Bold text in a paragraph.</strong></p>\n</blockquote>\n" | ||
); | ||
} | ||
|
||
#[test] | ||
fn normal_table() { | ||
let text = "| Header 1 | Header 2 |\n| -------- | -------- |\n| Text 123 | More 456 |"; | ||
let processed = rewrite(text); | ||
|
||
assert_eq!( | ||
processed, | ||
"|Header 1|Header 2|\n|--------|--------|\n|Text 123|More 456|", | ||
"It strips some whitespace but otherwise leaves the table intact." | ||
); | ||
} | ||
|
||
#[test] | ||
fn table_in_note() { | ||
let text = "> Note: table stuff.\n\n| Header 1 | Header 2 |\n| -------- | -------- |\n| Text 123 | More 456 |"; | ||
let processed = rewrite(text); | ||
|
||
assert_eq!( | ||
processed, | ||
"\n\n<section class=\"note\" aria-role=\"note\">\n\nNote: table stuff.\n\n</section>\n\n|Header 1|Header 2|\n|--------|--------|\n|Text 123|More 456|", | ||
"It adds the note markup but leaves the table untouched, to be rendered as Markdown." | ||
); | ||
} | ||
|
||
#[test] | ||
fn table_in_quote() { | ||
let text = "> A table.\n\n| Header 1 | Header 2 |\n| -------- | -------- |\n| Text 123 | More 456 |"; | ||
let processed = rewrite(text); | ||
assert_eq!( | ||
render_markdown(&processed), | ||
"<blockquote>\n<p>A table.</p>\n</blockquote>\n<table><thead><tr><th>Header 1</th><th>Header 2</th></tr></thead><tbody>\n<tr><td>Text 123</td><td>More 456</td></tr>\n</tbody></table>\n", | ||
"It renders blockquotes with nested tables as expected." | ||
); | ||
} | ||
|
||
fn render_markdown(text: &str) -> String { | ||
let parser = crate::parser(text); | ||
let mut buf = String::new(); | ||
pulldown_cmark::html::push_html(&mut buf, parser); | ||
buf | ||
} |
20 changes: 20 additions & 0 deletions
20
rustbook-en/packages/mdbook-trpl/tests/integration/main.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
mod note { | ||
use assert_cmd::Command; | ||
#[test] | ||
fn supports_html_renderer() { | ||
let cmd = Command::cargo_bin("mdbook-trpl-note") | ||
.unwrap() | ||
.args(["supports", "html"]) | ||
.ok(); | ||
assert!(cmd.is_ok()); | ||
} | ||
|
||
#[test] | ||
fn errors_for_other_renderers() { | ||
let cmd = Command::cargo_bin("mdbook-trpl-note") | ||
.unwrap() | ||
.args(["supports", "total-nonsense"]) | ||
.ok(); | ||
assert!(cmd.is_err()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
147 changes: 147 additions & 0 deletions
147
rustbook-en/packages/tools/src/bin/cleanup_blockquotes.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
//! Fix incorrect round-tripping of block quotes in `pulldown-cmark-to-cmark`: | ||
//! | ||
//! - Eliminate extraneous leading `>` | ||
//! - Eliminate extraneous indent. | ||
//! | ||
//! Note: later versions of `pulldown-cmark-to-cmark` will likely fix this, so | ||
//! check when upgrading it if it is still necessary! | ||
use std::io::{self, Read}; | ||
|
||
use lazy_static::lazy_static; | ||
use regex::Regex; | ||
|
||
fn main() { | ||
let input = { | ||
let mut buffer = String::new(); | ||
io::stdin() | ||
.read_to_string(&mut buffer) | ||
.unwrap_or_else(|e| panic!("{e}")); | ||
buffer | ||
}; | ||
|
||
let fixed = cleanup_blockquotes(input); | ||
print!("{fixed}"); | ||
} | ||
|
||
fn cleanup_blockquotes(input: String) -> String { | ||
let normal_start = EXTRA_SPACE.replace_all(&input, ">"); | ||
let sans_empty_leading = EMPTY_LEADING.replace_all(&normal_start, "\n\n"); | ||
sans_empty_leading.to_string() | ||
} | ||
|
||
lazy_static! { | ||
static ref EXTRA_SPACE: Regex = Regex::new("(?m)^ >").unwrap(); | ||
static ref EMPTY_LEADING: Regex = Regex::new("\n\n> ?\n").unwrap(); | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn extra_space() { | ||
let input = " > Hello".to_string(); | ||
let actual = cleanup_blockquotes(input); | ||
assert_eq!(actual, "> Hello"); | ||
} | ||
|
||
#[test] | ||
fn empty_leading() { | ||
let input = "\n\n>\n> Hello".into(); | ||
let actual = cleanup_blockquotes(input); | ||
assert_eq!(actual, "\n\n> Hello"); | ||
} | ||
|
||
#[test] | ||
fn leading_after_extra_space_cleaned_up() { | ||
let input = r#"Start | ||
> | ||
> Note: Hey. | ||
Wrap."# | ||
.into(); | ||
|
||
let actual = cleanup_blockquotes(input); | ||
assert_eq!( | ||
actual, | ||
r#"Start | ||
> Note: Hey. | ||
Wrap."# | ||
); | ||
} | ||
|
||
/// This particular input was the result of running any of the mdbook | ||
/// preprocessors which use `pulldown-cmark-to-cmark@<=18.0.0`. | ||
#[test] | ||
fn regression_ch17_example() { | ||
// This is an example of the original motivating input which we are fixing. | ||
let input = r#" | ||
We have to explicitly await both of these futures, because futures in Rust are | ||
*lazy*: they don’t do anything until you ask them to with `await`. (In fact, | ||
Rust will show a compiler warning if you don’t use a future.) This should | ||
remind you of our discussion of iterators [back in Chapter 13][iterators-lazy]. | ||
Iterators do nothing unless you call their `next` method—whether directly, or | ||
using `for` loops or methods such as `map` which use `next` under the hood. With | ||
futures, the same basic idea applies: they do nothing unless you explicitly ask | ||
them to. This laziness allows Rust to avoid running async code until it’s | ||
actually needed. | ||
> | ||
> Note: This is different from the behavior we saw when using `thread::spawn` in | ||
> the previous chapter, where the closure we passed to another thread started | ||
> running immediately. It’s also different from how many other languages | ||
> approach async! But it’s important for Rust. We’ll see why that is later. | ||
Once we have `response_text`, we can then parse it into an instance of the | ||
`Html` type using `Html::parse`. Instead of a raw string, we now have a data | ||
type we can use to work with the HTML as a richer data structure. In particular, | ||
we can use the `select_first` method to find the first instance of a given CSS | ||
selector. By passing the string `"title"`, we’ll get the first `<title>` | ||
element in the document, if there is one. Because there may not be any matching | ||
element, `select_first` returns an `Option<ElementRef>`. Finally, we use the | ||
`Option::map` method, which lets us work with the item in the `Option` if it’s | ||
present, and do nothing if it isn’t. (We could also use a `match` expression | ||
here, but `map` is more idiomatic.) In the body of the function we supply to | ||
`map`, we call `inner_html` on the `title_element` to get its content, which is | ||
a `String`. When all is said and done, we have an `Option<String>`. | ||
"#.to_string(); | ||
|
||
let actual = cleanup_blockquotes(input); | ||
assert_eq!( | ||
actual, | ||
r#" | ||
We have to explicitly await both of these futures, because futures in Rust are | ||
*lazy*: they don’t do anything until you ask them to with `await`. (In fact, | ||
Rust will show a compiler warning if you don’t use a future.) This should | ||
remind you of our discussion of iterators [back in Chapter 13][iterators-lazy]. | ||
Iterators do nothing unless you call their `next` method—whether directly, or | ||
using `for` loops or methods such as `map` which use `next` under the hood. With | ||
futures, the same basic idea applies: they do nothing unless you explicitly ask | ||
them to. This laziness allows Rust to avoid running async code until it’s | ||
actually needed. | ||
> Note: This is different from the behavior we saw when using `thread::spawn` in | ||
> the previous chapter, where the closure we passed to another thread started | ||
> running immediately. It’s also different from how many other languages | ||
> approach async! But it’s important for Rust. We’ll see why that is later. | ||
Once we have `response_text`, we can then parse it into an instance of the | ||
`Html` type using `Html::parse`. Instead of a raw string, we now have a data | ||
type we can use to work with the HTML as a richer data structure. In particular, | ||
we can use the `select_first` method to find the first instance of a given CSS | ||
selector. By passing the string `"title"`, we’ll get the first `<title>` | ||
element in the document, if there is one. Because there may not be any matching | ||
element, `select_first` returns an `Option<ElementRef>`. Finally, we use the | ||
`Option::map` method, which lets us work with the item in the `Option` if it’s | ||
present, and do nothing if it isn’t. (We could also use a `match` expression | ||
here, but `map` is more idiomatic.) In the body of the function we supply to | ||
`map`, we call `inner_html` on the `title_element` to get its content, which is | ||
a `String`. When all is said and done, we have an `Option<String>`. | ||
"# | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters