>),
+ InNote,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn no_note() {
+ let text = "Hello, world.\n\nThis is some text.";
+ let processed = rewrite(text);
+ assert_eq!(
+ render_markdown(&processed),
+ "Hello, world.
\nThis is some text.
\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),
+ "\nNote: This is some text.\nIt keeps going.
\n"
+ );
+ }
+
+ #[test]
+ fn regular_blockquote() {
+ let text = "> This is some text.\n> It keeps going.";
+ let processed = rewrite(text);
+ assert_eq!(
+ render_markdown(&processed),
+ "\nThis is some text.\nIt keeps going.
\n
\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),
+ "\nNote: This is some text.\nIt keeps going.
\n\nThis is regular text.
\n\nThis is a blockquote.
\n
\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),
+ "\nThis is quoted.
\n
\n"
+ );
+ }
+
+ #[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),
+ "\n\nThis is quoted.
\n
\n"
+ );
+ }
+
+ #[test]
+ fn with_h1_note() {
+ let text = "> # Header\n > And then some note content.";
+ let processed = rewrite(text);
+ assert_eq!(
+ render_markdown(&processed),
+ "\nHeader
\nAnd then some note content.
\n"
+ );
+ }
+
+ #[test]
+ fn with_h2_note() {
+ let text = "> ## Header\n > And then some note content.";
+ let processed = rewrite(text);
+ assert_eq!(
+ render_markdown(&processed),
+ "\nHeader
\nAnd then some note content.
\n"
+ );
+ }
+
+ #[test]
+ fn with_h3_note() {
+ let text = "> ### Header\n > And then some note content.";
+ let processed = rewrite(text);
+ assert_eq!(
+ render_markdown(&processed),
+ "\nHeader
\nAnd then some note content.
\n"
+ );
+ }
+
+ #[test]
+ fn with_h4_note() {
+ let text = "> #### Header\n > And then some note content.";
+ let processed = rewrite(text);
+ assert_eq!(
+ render_markdown(&processed),
+ "\nHeader
\nAnd then some note content.
\n"
+ );
+ }
+
+ #[test]
+ fn with_h5_note() {
+ let text = "> ##### Header\n > And then some note content.";
+ let processed = rewrite(text);
+ assert_eq!(
+ render_markdown(&processed),
+ "\nHeader
\nAnd then some note content.
\n"
+ );
+ }
+
+ #[test]
+ fn with_h6_note() {
+ let text = "> ###### Header\n > And then some note content.";
+ let processed = rewrite(text);
+ assert_eq!(
+ render_markdown(&processed),
+ "\nHeader
\nAnd then some note content.
\n"
+ );
+ }
+
+ #[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),
+ "\nHeader
\nAnd then some note content.
\n\n\nThis is quoted.
\n
\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),
+ "\nThis is quoted.
\n
\n\nHeader
\nAnd then some note content.
\n"
+ );
+ }
+
+ fn render_markdown(text: &str) -> String {
+ let parser = Parser::new(text);
+ let mut buf = String::new();
+ pulldown_cmark::html::push_html(&mut buf, parser);
+ buf
+ }
+}
diff --git a/rustbook-en/packages/mdbook-trpl-note/src/main.rs b/rustbook-en/packages/mdbook-trpl-note/src/main.rs
new file mode 100644
index 00000000..8649432f
--- /dev/null
+++ b/rustbook-en/packages/mdbook-trpl-note/src/main.rs
@@ -0,0 +1,38 @@
+use std::io;
+
+use clap::{self, Parser, Subcommand};
+use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
+
+use mdbook_trpl_note::TrplNote;
+
+fn main() -> Result<(), String> {
+ let cli = Cli::parse();
+ let simple_note = TrplNote;
+ if let Some(Command::Supports { renderer }) = cli.command {
+ return if simple_note.supports_renderer(&renderer) {
+ Ok(())
+ } else {
+ Err(format!("Renderer '{renderer}' is unsupported"))
+ };
+ }
+
+ let (ctx, book) =
+ CmdPreprocessor::parse_input(io::stdin()).map_err(|e| format!("blah: {e}"))?;
+ let processed = simple_note.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 notes in _The Rust Programming Language_.
+#[derive(Parser, Debug)]
+struct Cli {
+ #[command(subcommand)]
+ command: Option,
+}
+
+#[derive(Subcommand, Debug)]
+enum Command {
+ /// Is the renderer supported?
+ ///
+ /// All renderers are supported! This is the contract for mdBook.
+ Supports { renderer: String },
+}
diff --git a/rustbook-en/packages/mdbook-trpl-note/tests/integration/main.rs b/rustbook-en/packages/mdbook-trpl-note/tests/integration/main.rs
new file mode 100644
index 00000000..6944fae6
--- /dev/null
+++ b/rustbook-en/packages/mdbook-trpl-note/tests/integration/main.rs
@@ -0,0 +1,22 @@
+use assert_cmd::Command;
+
+#[test]
+fn supports_html_renderer() {
+ let cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))
+ .unwrap()
+ .args(["supports", "html"])
+ .ok();
+ assert!(cmd.is_ok());
+}
+
+#[test]
+fn errors_for_other_renderers() {
+ let cmd = Command::cargo_bin(env!("CARGO_PKG_NAME"))
+ .unwrap()
+ .args(["supports", "total-nonsense"])
+ .ok();
+ assert!(cmd.is_err());
+}
+
+// It would be nice to add an actual fixture for an mdbook, but doing *that* is
+// going to be a bit of a pain, and what I have should cover it for now.
diff --git a/rustbook-en/packages/tools/Cargo.toml b/rustbook-en/packages/tools/Cargo.toml
new file mode 100644
index 00000000..95dda7a1
--- /dev/null
+++ b/rustbook-en/packages/tools/Cargo.toml
@@ -0,0 +1,46 @@
+[package]
+name = "rust-book-tools"
+version = "0.0.1"
+description = "The Rust Book"
+edition = "2021"
+
+[[bin]]
+name = "concat_chapters"
+path = "src/bin/concat_chapters.rs"
+
+[[bin]]
+name = "convert_quotes"
+path = "src/bin/convert_quotes.rs"
+
+[[bin]]
+name = "lfp"
+path = "src/bin/lfp.rs"
+
+[[bin]]
+name = "link2print"
+path = "src/bin/link2print.rs"
+
+[[bin]]
+name = "release_listings"
+path = "src/bin/release_listings.rs"
+
+[[bin]]
+name = "remove_hidden_lines"
+path = "src/bin/remove_hidden_lines.rs"
+
+[[bin]]
+name = "remove_links"
+path = "src/bin/remove_links.rs"
+
+[[bin]]
+name = "remove_markup"
+path = "src/bin/remove_markup.rs"
+
+[dependencies]
+walkdir = { workspace = true }
+docopt = { workspace = true }
+serde = { workspace = true }
+regex = { workspace = true }
+lazy_static = { workspace = true }
+flate2 = { workspace = true }
+tar = { workspace = true }
diff --git a/rustbook-en/tools/src/bin/concat_chapters.rs b/rustbook-en/packages/tools/src/bin/concat_chapters.rs
similarity index 98%
rename from rustbook-en/tools/src/bin/concat_chapters.rs
rename to rustbook-en/packages/tools/src/bin/concat_chapters.rs
index 79ffec9b..046870ed 100644
--- a/rustbook-en/tools/src/bin/concat_chapters.rs
+++ b/rustbook-en/packages/tools/src/bin/concat_chapters.rs
@@ -1,6 +1,3 @@
-#[macro_use]
-extern crate lazy_static;
-
use std::collections::BTreeMap;
use std::env;
use std::fs::{create_dir, read_dir, File};
@@ -9,6 +6,7 @@ use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::exit;
+use lazy_static::lazy_static;
use regex::Regex;
static PATTERNS: &[(&str, &str)] = &[
diff --git a/rustbook-en/tools/src/bin/convert_quotes.rs b/rustbook-en/packages/tools/src/bin/convert_quotes.rs
similarity index 100%
rename from rustbook-en/tools/src/bin/convert_quotes.rs
rename to rustbook-en/packages/tools/src/bin/convert_quotes.rs
diff --git a/rustbook-en/tools/src/bin/lfp.rs b/rustbook-en/packages/tools/src/bin/lfp.rs
similarity index 99%
rename from rustbook-en/tools/src/bin/lfp.rs
rename to rustbook-en/packages/tools/src/bin/lfp.rs
index f7b6e17f..ee7f39c3 100644
--- a/rustbook-en/tools/src/bin/lfp.rs
+++ b/rustbook-en/packages/tools/src/bin/lfp.rs
@@ -1,11 +1,12 @@
// We have some long regex literals, so:
// ignore-tidy-linelength
-use docopt::Docopt;
-use serde::Deserialize;
use std::io::BufRead;
use std::{fs, io, path};
+use docopt::Docopt;
+use serde::Deserialize;
+
fn main() {
let args: Args = Docopt::new(USAGE)
.and_then(|d| d.deserialize())
diff --git a/rustbook-en/tools/src/bin/link2print.rs b/rustbook-en/packages/tools/src/bin/link2print.rs
similarity index 99%
rename from rustbook-en/tools/src/bin/link2print.rs
rename to rustbook-en/packages/tools/src/bin/link2print.rs
index 7d6def86..09edd84b 100644
--- a/rustbook-en/tools/src/bin/link2print.rs
+++ b/rustbook-en/packages/tools/src/bin/link2print.rs
@@ -1,11 +1,12 @@
// FIXME: we have some long lines that could be refactored, but it's not a big deal.
// ignore-tidy-linelength
-use regex::{Captures, Regex};
use std::collections::HashMap;
use std::io;
use std::io::Read;
+use regex::{Captures, Regex};
+
fn main() {
write_md(parse_links(parse_references(read_md())));
}
diff --git a/rustbook-en/tools/src/bin/release_listings.rs b/rustbook-en/packages/tools/src/bin/release_listings.rs
similarity index 99%
rename from rustbook-en/tools/src/bin/release_listings.rs
rename to rustbook-en/packages/tools/src/bin/release_listings.rs
index 4239a4da..c9a2f459 100644
--- a/rustbook-en/tools/src/bin/release_listings.rs
+++ b/rustbook-en/packages/tools/src/bin/release_listings.rs
@@ -1,7 +1,3 @@
-#[macro_use]
-extern crate lazy_static;
-
-use regex::Regex;
use std::error::Error;
use std::fs;
use std::fs::File;
@@ -9,6 +5,9 @@ use std::io::prelude::*;
use std::io::{BufReader, BufWriter};
use std::path::{Path, PathBuf};
+use lazy_static::lazy_static;
+use regex::Regex;
+
fn main() -> Result<(), Box> {
// Get all listings from the `listings` directory
let listings_dir = Path::new("listings");
diff --git a/rustbook-en/tools/src/bin/remove_hidden_lines.rs b/rustbook-en/packages/tools/src/bin/remove_hidden_lines.rs
similarity index 100%
rename from rustbook-en/tools/src/bin/remove_hidden_lines.rs
rename to rustbook-en/packages/tools/src/bin/remove_hidden_lines.rs
diff --git a/rustbook-en/tools/src/bin/remove_links.rs b/rustbook-en/packages/tools/src/bin/remove_links.rs
similarity index 98%
rename from rustbook-en/tools/src/bin/remove_links.rs
rename to rustbook-en/packages/tools/src/bin/remove_links.rs
index 042295d1..ebb46e2c 100644
--- a/rustbook-en/tools/src/bin/remove_links.rs
+++ b/rustbook-en/packages/tools/src/bin/remove_links.rs
@@ -1,5 +1,3 @@
-extern crate regex;
-
use regex::{Captures, Regex};
use std::collections::HashSet;
use std::io;
diff --git a/rustbook-en/tools/src/bin/remove_markup.rs b/rustbook-en/packages/tools/src/bin/remove_markup.rs
similarity index 98%
rename from rustbook-en/tools/src/bin/remove_markup.rs
rename to rustbook-en/packages/tools/src/bin/remove_markup.rs
index a9cc4b7f..cda6ebaa 100644
--- a/rustbook-en/tools/src/bin/remove_markup.rs
+++ b/rustbook-en/packages/tools/src/bin/remove_markup.rs
@@ -1,9 +1,8 @@
-extern crate regex;
-
-use regex::{Captures, Regex};
use std::io;
use std::io::Read;
+use regex::{Captures, Regex};
+
fn main() {
write_md(remove_markup(read_md()));
}
diff --git a/rustbook-en/src/ch10-03-lifetime-syntax.md b/rustbook-en/src/ch10-03-lifetime-syntax.md
index e77be953..197e2c6f 100644
--- a/rustbook-en/src/ch10-03-lifetime-syntax.md
+++ b/rustbook-en/src/ch10-03-lifetime-syntax.md
@@ -8,7 +8,7 @@ One detail we didn’t discuss in the [“References and
Borrowing”][references-and-borrowing] section in Chapter 4 is
that every reference in Rust has a *lifetime*, which is the scope for which
that reference is valid. Most of the time, lifetimes are implicit and inferred,
-just like most of the time, types are inferred. We must only annotate types
+just like most of the time, types are inferred. We only have to annotate types
when multiple types are possible. In a similar way, we must annotate lifetimes
when the lifetimes of references could be related in a few different ways. Rust
requires us to annotate the relationships using generic lifetime parameters to
diff --git a/rustbook-en/theme/semantic-notes.css b/rustbook-en/theme/semantic-notes.css
new file mode 100644
index 00000000..7e68eb37
--- /dev/null
+++ b/rustbook-en/theme/semantic-notes.css
@@ -0,0 +1,13 @@
+/*
+ This is copied directly from the styles for blockquotes, because notes were
+ historically rendered *as* blockquotes. This keeps the presentation of them
+ identical while updating the presentation.
+*/
+.note {
+ margin: 20px 0;
+ padding: 0 20px;
+ color: var(--fg);
+ background-color: var(--quote-bg);
+ border-block-start: 0.1em solid var(--quote-border);
+ border-block-end: 0.1em solid var(--quote-border);
+}
diff --git a/rustbook-en/tools/nostarch.sh b/rustbook-en/tools/nostarch.sh
index eec0ac5e..430b0809 100755
--- a/rustbook-en/tools/nostarch.sh
+++ b/rustbook-en/tools/nostarch.sh
@@ -9,7 +9,7 @@ rm -rf tmp/*.md
rm -rf tmp/markdown
# Render the book as Markdown to include all the code listings
-MDBOOK_OUTPUT__MARKDOWN=1 mdbook build -d tmp
+MDBOOK_OUTPUT__MARKDOWN=1 mdbook build nostarch
# Get all the Markdown files
find tmp/markdown -name "${1:-\"\"}*.md" -print0 | \