diff --git a/rustbook-en/.github/workflows/main.yml b/rustbook-en/.github/workflows/main.yml
index 04b855e8c..da2dd5ba7 100644
--- a/rustbook-en/.github/workflows/main.yml
+++ b/rustbook-en/.github/workflows/main.yml
@@ -60,6 +60,8 @@ jobs:
echo "$(pwd)/bin" >> ${GITHUB_PATH}
- name: Install mdbook-trpl-note
run: cargo install --path packages/mdbook-trpl-note
+ - name: Install mdbook-trpl-listing
+ run: cargo install --path packages/mdbook-trpl-listing
- name: Install aspell
run: sudo apt-get install aspell
- name: Install shellcheck
diff --git a/rustbook-en/Cargo.lock b/rustbook-en/Cargo.lock
index 3fbf7d04a..7641aa965 100644
--- a/rustbook-en/Cargo.lock
+++ b/rustbook-en/Cargo.lock
@@ -972,11 +972,26 @@ dependencies = [
"shlex",
"tempfile",
"tokio",
- "toml",
+ "toml 0.5.11",
"topological-sort",
"warp",
]
+[[package]]
+name = "mdbook-trpl-listing"
+version = "0.1.0"
+dependencies = [
+ "assert_cmd",
+ "clap",
+ "mdbook",
+ "pulldown-cmark",
+ "pulldown-cmark-to-cmark",
+ "serde_json",
+ "thiserror",
+ "toml 0.8.12",
+ "xmlparser",
+]
+
[[package]]
name = "mdbook-trpl-note"
version = "1.0.0"
@@ -1515,15 +1530,24 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.116"
+version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
+checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [
"itoa",
"ryu",
"serde",
]
+[[package]]
+name = "serde_spanned"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -1707,18 +1731,18 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
[[package]]
name = "thiserror"
-version = "1.0.59"
+version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0126ad08bff79f29fc3ae6a55cc72352056dfff61e3ff8bb7129476d44b23aa"
+checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
-version = "1.0.59"
+version = "1.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1cd413b5d558b4c5bf3680e324a6fa5014e7b7c067a51e69dbdf47eb7148b66"
+checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
dependencies = [
"proc-macro2",
"quote",
@@ -1803,6 +1827,40 @@ dependencies = [
"serde",
]
+[[package]]
+name = "toml"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
+dependencies = [
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3328d4f68a705b2a4498da1d580585d39a6510f98318a2cec3018a7ec61ddef"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
+]
+
[[package]]
name = "topological-sort"
version = "0.2.2"
@@ -2232,6 +2290,15 @@ version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+[[package]]
+name = "winnow"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3c52e9c97a68071b23e836c9380edae937f17b9c4667bd021973efc689f618d"
+dependencies = [
+ "memchr",
+]
+
[[package]]
name = "xattr"
version = "1.3.1"
@@ -2242,3 +2309,9 @@ dependencies = [
"linux-raw-sys",
"rustix",
]
+
+[[package]]
+name = "xmlparser"
+version = "0.13.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
diff --git a/rustbook-en/Cargo.toml b/rustbook-en/Cargo.toml
index 0a24b0da0..80cdfdcd6 100644
--- a/rustbook-en/Cargo.toml
+++ b/rustbook-en/Cargo.toml
@@ -8,10 +8,15 @@ exclude = [
]
[workspace.dependencies]
+assert_cmd = "2.0.14"
walkdir = "2.3.1"
+clap = { version = "4.5.4", features = ["derive"] }
docopt = "1.1.0"
mdbook = "0.4.37"
+pulldown-cmark = { version = "0.10.3", features = ["simd"] }
+pulldown-cmark-to-cmark = "13.0.0"
serde = "1.0"
+serde_json = "1.0"
regex = "1.3.3"
lazy_static = "1.4.0"
flate2 = "1.0.13"
diff --git a/rustbook-en/book.toml b/rustbook-en/book.toml
index 416f2190f..b73ff03b7 100644
--- a/rustbook-en/book.toml
+++ b/rustbook-en/book.toml
@@ -6,9 +6,12 @@ title = "The Rust Programming Language"
authors = ["Steve Klabnik", "Carol Nichols", "Contributions from the Rust Community"]
[output.html]
-additional-css = ["ferris.css", "theme/2018-edition.css", "theme/semantic-notes.css"]
+additional-css = ["ferris.css", "theme/2018-edition.css", "theme/semantic-notes.css", "theme/listing.css"]
additional-js = ["ferris.js"]
git-repository-url = "https://github.com/rust-lang/book"
# Do not sync this preprocessor; it is for the HTML renderer only.
[preprocessor.trpl-note]
+
+[preprocessor.trpl-listing]
+output-mode = "default"
diff --git a/rustbook-en/nostarch/book.toml b/rustbook-en/nostarch/book.toml
index 4c3117933..55528625d 100644
--- a/rustbook-en/nostarch/book.toml
+++ b/rustbook-en/nostarch/book.toml
@@ -9,4 +9,7 @@ additional-js = ["../ferris.js"]
git-repository-url = "https://github.com/rust-lang/book"
[build]
-build-dir = "../tmp"
\ No newline at end of file
+build-dir = "../tmp"
+
+[preprocessor.trpl-listing]
+output-mode = "simple"
diff --git a/rustbook-en/packages/mdbook-trpl-listing/Cargo.toml b/rustbook-en/packages/mdbook-trpl-listing/Cargo.toml
new file mode 100644
index 000000000..fd829ef18
--- /dev/null
+++ b/rustbook-en/packages/mdbook-trpl-listing/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "mdbook-trpl-listing"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+clap = { workspace = true }
+mdbook = { workspace = true }
+pulldown-cmark = { workspace = true }
+pulldown-cmark-to-cmark = { workspace = true }
+serde_json = { workspace = true }
+thiserror = "1.0.60"
+toml = "0.8.12"
+xmlparser = "0.13.6"
+
+[dev-dependencies]
+assert_cmd = { workspace = true }
diff --git a/rustbook-en/packages/mdbook-trpl-listing/src/lib.rs b/rustbook-en/packages/mdbook-trpl-listing/src/lib.rs
new file mode 100644
index 000000000..99b62cc0a
--- /dev/null
+++ b/rustbook-en/packages/mdbook-trpl-listing/src/lib.rs
@@ -0,0 +1,746 @@
+use mdbook::{
+ book::Book,
+ errors::Result,
+ preprocess::{Preprocessor, PreprocessorContext},
+ BookItem,
+};
+use pulldown_cmark::{html, Event, Parser};
+use pulldown_cmark_to_cmark::cmark;
+use xmlparser::{Token, Tokenizer};
+
+/// A preprocessor for rendering listings more elegantly.
+///
+/// Given input like this:
+///
+/// ````markdown
+///
+///
+/// ```rust
+/// fn main() {}
+/// ```
+///
+///
+///
+/// ````
+///
+/// With no configuration, or with `output-mode = "default"`, it renders the
+/// following Markdown to be further preprocessed or rendered to HTML:
+///
+/// ````markdown
+///
+/// ````
+///
+/// When `output-mode = "simple"` in the configuration, it instead emits:
+///
+/// ````markdown
+/// Filename: src/main.rs
+///
+/// ```rust
+/// fn main() {}
+/// ```
+///
+/// Listing 1-2: Some *text*, yeah?
+/// ````
+pub struct TrplListing;
+
+impl Preprocessor for TrplListing {
+ fn name(&self) -> &str {
+ "trpl-listing"
+ }
+
+ fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> Result {
+ let config = ctx
+ .config
+ .get_preprocessor(self.name())
+ .ok_or(Error::NoConfig)?;
+
+ 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);
+
+ let mut errors: Vec = vec![];
+ book.for_each_mut(|item| {
+ if let BookItem::Chapter(ref mut chapter) = item {
+ match rewrite_listing(&chapter.content, mode) {
+ Ok(rewritten) => chapter.content = rewritten,
+ Err(reason) => errors.push(reason),
+ }
+ }
+ });
+
+ if errors.is_empty() {
+ Ok(book)
+ } else {
+ Err(CompositeError(errors.join("\n")).into())
+ }
+ }
+
+ fn supports_renderer(&self, renderer: &str) -> bool {
+ renderer == "html"
+ }
+}
+
+#[derive(Debug, thiserror::Error)]
+enum Error {
+ #[error("No config for trpl-listing")]
+ NoConfig,
+
+ #[error("Bad config value '{value}' for key '{key}'")]
+ BadValue { key: String, value: String },
+}
+
+#[derive(Debug, thiserror::Error)]
+#[error("Error(s) rewriting input: {0}")]
+struct CompositeError(String);
+
+#[derive(Debug, Clone, Copy)]
+enum Mode {
+ Default,
+ Simple,
+}
+
+/// Trivial marker struct to indicate an internal error.
+///
+/// The caller has enough info to do what it needs without passing data around.
+struct ParseErr;
+
+impl TryFrom<&str> for Mode {
+ type Error = ParseErr;
+
+ fn try_from(value: &str) -> std::prelude::v1::Result {
+ match value {
+ "default" => Ok(Mode::Default),
+ "simple" => Ok(Mode::Simple),
+ _ => Err(ParseErr),
+ }
+ }
+}
+
+fn rewrite_listing(src: &str, mode: Mode) -> Result {
+ let parser = Parser::new(src);
+
+ struct State<'e> {
+ current_listing: Option,
+ events: Vec, String>>,
+ }
+
+ let final_state = parser.fold(
+ State {
+ current_listing: None,
+ events: vec![],
+ },
+ |mut state, ev| {
+ match ev {
+ Event::Html(tag) => {
+ if tag.starts_with(" {
+ match local.as_str() {
+ "number" => builder
+ .with_number(value.as_str()),
+ "caption" => builder
+ .with_caption(value.as_str()),
+ "file-name" => builder
+ .with_file_name(value.as_str()),
+ _ => builder, // TODO: error on extra attrs?
+ }
+ }
+ _ => builder,
+ }
+ })
+ .build();
+
+ match listing_result {
+ Ok(listing) => {
+ let opening_event = match mode {
+ Mode::Default => {
+ let opening_html =
+ listing.opening_html();
+ Event::Html(opening_html.into())
+ }
+ Mode::Simple => {
+ let opening_text =
+ listing.opening_text();
+ Event::Text(opening_text.into())
+ }
+ };
+
+ state.current_listing = Some(listing);
+ state.events.push(Ok(opening_event));
+ }
+ Err(reason) => state.events.push(Err(reason)),
+ }
+ } else if tag.starts_with("") {
+ let trailing = if !tag.ends_with('>') {
+ tag.replace("", "")
+ } else {
+ String::from("")
+ };
+
+ match state.current_listing {
+ Some(listing) => {
+ let closing_event = match mode {
+ Mode::Default => {
+ let closing_html =
+ listing.closing_html(&trailing);
+ Event::Html(closing_html.into())
+ }
+ Mode::Simple => {
+ let closing_text =
+ listing.closing_text(&trailing);
+ Event::Text(closing_text.into())
+ }
+ };
+
+ state.current_listing = None;
+ state.events.push(Ok(closing_event));
+ }
+ None => state.events.push(Err(String::from(
+ "Closing `` without opening tag.",
+ ))),
+ }
+ } else {
+ state.events.push(Ok(Event::Html(tag)));
+ }
+ }
+ ev => state.events.push(Ok(ev)),
+ };
+ state
+ },
+ );
+
+ if final_state.current_listing.is_some() {
+ return Err("Unclosed listing".into());
+ }
+
+ let (events, errors): (Vec<_>, Vec<_>) =
+ final_state.events.into_iter().partition(|e| e.is_ok());
+
+ if !errors.is_empty() {
+ return Err(errors
+ .into_iter()
+ .map(|e| e.unwrap_err())
+ .collect::>()
+ .join("\n"));
+ }
+
+ let mut buf = String::with_capacity(src.len() * 2);
+ cmark(events.into_iter().map(|ok| ok.unwrap()), &mut buf)
+ .map_err(|e| format!("{e}"))?;
+ Ok(buf)
+}
+
+#[derive(Debug)]
+struct Listing {
+ number: String,
+ caption: String,
+ file_name: Option,
+}
+
+impl Listing {
+ fn opening_html(&self) -> String {
+ let figure = String::from("{trailing}"#,
+ number = self.number,
+ caption = self.caption
+ )
+ }
+
+ fn opening_text(&self) -> String {
+ self.file_name
+ .as_ref()
+ .map(|file_name| format!("\nFilename: {file_name}\n"))
+ .unwrap_or_default()
+ }
+
+ fn closing_text(&self, trailing: &str) -> String {
+ format!(
+ "Listing {number}: {caption}{trailing}",
+ number = self.number,
+ caption = self.caption,
+ )
+ }
+}
+
+struct ListingBuilder<'a> {
+ number: Option<&'a str>,
+ caption: Option<&'a str>,
+ file_name: Option<&'a str>,
+}
+
+impl<'a> ListingBuilder<'a> {
+ fn new() -> ListingBuilder<'a> {
+ ListingBuilder {
+ number: None,
+ caption: None,
+ file_name: None,
+ }
+ }
+
+ fn with_number(mut self, value: &'a str) -> Self {
+ self.number = Some(value);
+ self
+ }
+
+ fn with_caption(mut self, value: &'a str) -> Self {
+ self.caption = Some(value);
+ self
+ }
+
+ fn with_file_name(mut self, value: &'a str) -> Self {
+ self.file_name = Some(value);
+ self
+ }
+
+ fn build(self) -> Result {
+ let number = self
+ .number
+ .ok_or_else(|| String::from("Missing number"))?
+ .to_owned();
+
+ let caption = self
+ .caption
+ .map(|caption_source| {
+ let events = Parser::new(caption_source);
+ let mut buf = String::with_capacity(caption_source.len() * 2);
+ html::push_html(&mut buf, events);
+
+ // This is not particularly principled, but since the only
+ // place it is used is here, for caption source handling, it
+ // is “fine”.
+ buf.replace("", "").replace("
", "").replace('\n', "")
+ })
+ .ok_or_else(|| String::from("Missing caption"))?
+ .to_owned();
+
+ Ok(Listing {
+ number,
+ caption,
+ file_name: self.file_name.map(String::from),
+ })
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ /// Note: This inserts an additional backtick around the re-emitted code.
+ /// It is not clear *why*, but that seems to be an artifact of the rendering
+ /// done by the `pulldown_cmark_to_cmark` crate.
+ #[test]
+ fn default_mode_works() {
+ let result = rewrite_listing(
+ r#"
+
+```rust
+fn main() {}
+```
+
+"#,
+ Mode::Default,
+ );
+
+ assert_eq!(
+ &result.unwrap(),
+ r#""#
+ );
+ }
+
+ #[test]
+ fn simple_mode_works() {
+ let result = rewrite_listing(
+ r#"
+
+```rust
+fn main() {}
+```
+
+"#,
+ Mode::Simple,
+ );
+
+ assert_eq!(
+ &result.unwrap(),
+ r#"
+Filename: src/main.rs
+
+````rust
+fn main() {}
+````
+
+Listing 1-2: A write-up which might include inline Markdown like code
etc."#
+ );
+ }
+
+ #[test]
+ fn actual_listing() {
+ let result = rewrite_listing(
+ r#"Now open the *main.rs* file you just created and enter the code in Listing 1-1.
+
+
+
+```rust
+fn main() {
+ println!("Hello, world!");
+}
+```
+
+
+
+Save the file and go back to your terminal window"#,
+ Mode::Default,
+ );
+
+ assert!(result.is_ok());
+ assert_eq!(
+ result.unwrap(),
+ r#"Now open the *main.rs* file you just created and enter the code in Listing 1-1.
+
+
+
+Save the file and go back to your terminal window"#
+ );
+ }
+
+ #[test]
+ fn no_filename() {
+ let result = rewrite_listing(
+ r#"This is the opening.
+
+
+
+```rust
+fn main() {}
+```
+
+
+
+This is the closing."#,
+ Mode::Default,
+ );
+
+ assert!(result.is_ok());
+ assert_eq!(
+ result.unwrap(),
+ r#"This is the opening.
+
+
+
+This is the closing."#
+ );
+ }
+
+ /// Check that the config options are correctly handled.
+ ///
+ /// Note: none of these tests particularly exercise the *wiring*. They just
+ /// assume that the config itself is done correctly. This is a small enough
+ /// chunk of code that it easy to verify by hand at present. If it becomes
+ /// more complex in the future, it would be good to revisit and integrate
+ /// the same kinds of tests as the unit tests above here.
+ #[cfg(test)]
+ mod config {
+ use super::*;
+
+ // TODO: what *should* the behavior here be? I *think* it should error,
+ // in that there is a problem if it is invoked without that info.
+ #[test]
+ fn no_config() {
+ let input_json = r##"[
+ {
+ "root": "/path/to/book",
+ "config": {
+ "book": {
+ "authors": ["AUTHOR"],
+ "language": "en",
+ "multilingual": false,
+ "src": "src",
+ "title": "TITLE"
+ },
+ "preprocessor": {}
+ },
+ "renderer": "html",
+ "mdbook_version": "0.4.21"
+ },
+ {
+ "sections": [
+ {
+ "Chapter": {
+ "name": "Chapter 1",
+ "content": "# Chapter 1\n",
+ "number": [1],
+ "sub_items": [],
+ "path": "chapter_1.md",
+ "source_path": "chapter_1.md",
+ "parent_names": []
+ }
+ }
+ ],
+ "__non_exhaustive": null
+ }
+ ]"##;
+ let input_json = input_json.as_bytes();
+ let (ctx, book) =
+ mdbook::preprocess::CmdPreprocessor::parse_input(input_json)
+ .unwrap();
+ let result = TrplListing.run(&ctx, book);
+ assert!(result.is_err());
+ let err = result.unwrap_err();
+ assert_eq!(format!("{err}"), "No config for trpl-listing");
+ }
+
+ #[test]
+ fn empty_config() {
+ let input_json = r##"[
+ {
+ "root": "/path/to/book",
+ "config": {
+ "book": {
+ "authors": ["AUTHOR"],
+ "language": "en",
+ "multilingual": false,
+ "src": "src",
+ "title": "TITLE"
+ },
+ "preprocessor": {
+ "trpl-listing": {}
+ }
+ },
+ "renderer": "html",
+ "mdbook_version": "0.4.21"
+ },
+ {
+ "sections": [
+ {
+ "Chapter": {
+ "name": "Chapter 1",
+ "content": "# Chapter 1\n",
+ "number": [1],
+ "sub_items": [],
+ "path": "chapter_1.md",
+ "source_path": "chapter_1.md",
+ "parent_names": []
+ }
+ }
+ ],
+ "__non_exhaustive": null
+ }
+ ]"##;
+ let input_json = input_json.as_bytes();
+ let (ctx, book) =
+ mdbook::preprocess::CmdPreprocessor::parse_input(input_json)
+ .unwrap();
+ let result = TrplListing.run(&ctx, book);
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn specify_default() {
+ let input_json = r##"[
+ {
+ "root": "/path/to/book",
+ "config": {
+ "book": {
+ "authors": ["AUTHOR"],
+ "language": "en",
+ "multilingual": false,
+ "src": "src",
+ "title": "TITLE"
+ },
+ "preprocessor": {
+ "trpl-listing": {
+ "output-mode": "default"
+ }
+ }
+ },
+ "renderer": "html",
+ "mdbook_version": "0.4.21"
+ },
+ {
+ "sections": [
+ {
+ "Chapter": {
+ "name": "Chapter 1",
+ "content": "# Chapter 1\n",
+ "number": [1],
+ "sub_items": [],
+ "path": "chapter_1.md",
+ "source_path": "chapter_1.md",
+ "parent_names": []
+ }
+ }
+ ],
+ "__non_exhaustive": null
+ }
+ ]"##;
+ let input_json = input_json.as_bytes();
+ let (ctx, book) =
+ mdbook::preprocess::CmdPreprocessor::parse_input(input_json)
+ .unwrap();
+ let result = TrplListing.run(&ctx, book);
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn specify_simple() {
+ let input_json = r##"[
+ {
+ "root": "/path/to/book",
+ "config": {
+ "book": {
+ "authors": ["AUTHOR"],
+ "language": "en",
+ "multilingual": false,
+ "src": "src",
+ "title": "TITLE"
+ },
+ "preprocessor": {
+ "trpl-listing": {
+ "output-mode": "simple"
+ }
+ }
+ },
+ "renderer": "html",
+ "mdbook_version": "0.4.21"
+ },
+ {
+ "sections": [
+ {
+ "Chapter": {
+ "name": "Chapter 1",
+ "content": "# Chapter 1\n",
+ "number": [1],
+ "sub_items": [],
+ "path": "chapter_1.md",
+ "source_path": "chapter_1.md",
+ "parent_names": []
+ }
+ }
+ ],
+ "__non_exhaustive": null
+ }
+ ]"##;
+ let input_json = input_json.as_bytes();
+ let (ctx, book) =
+ mdbook::preprocess::CmdPreprocessor::parse_input(input_json)
+ .unwrap();
+ let result = TrplListing.run(&ctx, book);
+ assert!(result.is_ok());
+ }
+
+ #[test]
+ fn specify_invalid() {
+ let input_json = r##"[
+ {
+ "root": "/path/to/book",
+ "config": {
+ "book": {
+ "authors": ["AUTHOR"],
+ "language": "en",
+ "multilingual": false,
+ "src": "src",
+ "title": "TITLE"
+ },
+ "preprocessor": {
+ "trpl-listing": {
+ "output-mode": "nonsense"
+ }
+ }
+ },
+ "renderer": "html",
+ "mdbook_version": "0.4.21"
+ },
+ {
+ "sections": [
+ {
+ "Chapter": {
+ "name": "Chapter 1",
+ "content": "# Chapter 1\n",
+ "number": [1],
+ "sub_items": [],
+ "path": "chapter_1.md",
+ "source_path": "chapter_1.md",
+ "parent_names": []
+ }
+ }
+ ],
+ "__non_exhaustive": null
+ }
+ ]"##;
+ let input_json = input_json.as_bytes();
+ let (ctx, book) =
+ mdbook::preprocess::CmdPreprocessor::parse_input(input_json)
+ .unwrap();
+ let result = TrplListing.run(&ctx, book);
+ assert!(result.is_err());
+ let err = result.unwrap_err();
+ assert_eq!(
+ format!("{err}"),
+ "Bad config value '\"nonsense\"' for key 'output-mode'"
+ );
+ }
+ }
+}
diff --git a/rustbook-en/packages/mdbook-trpl-listing/src/main.rs b/rustbook-en/packages/mdbook-trpl-listing/src/main.rs
new file mode 100644
index 000000000..3792b46fa
--- /dev/null
+++ b/rustbook-en/packages/mdbook-trpl-listing/src/main.rs
@@ -0,0 +1,37 @@
+use std::io;
+
+use clap::{self, Parser, Subcommand};
+use mdbook::preprocess::{CmdPreprocessor, Preprocessor};
+
+use mdbook_trpl_listing::TrplListing;
+
+fn main() -> Result<(), String> {
+ let cli = Cli::parse();
+ if let Some(Command::Supports { renderer }) = cli.command {
+ return if TrplListing.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 = TrplListing.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,
+}
+
+#[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/Cargo.toml b/rustbook-en/packages/mdbook-trpl-note/Cargo.toml
index 5834b9667..d2f80f9b7 100644
--- a/rustbook-en/packages/mdbook-trpl-note/Cargo.toml
+++ b/rustbook-en/packages/mdbook-trpl-note/Cargo.toml
@@ -6,11 +6,11 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-clap = { version = "4.5.4", features = ["derive"] }
+clap = { workspace = true }
mdbook = { workspace = true }
-pulldown-cmark = { version = "0.10.3", features = ["simd"] }
-pulldown-cmark-to-cmark = "13.0.0"
-serde_json = "1.0.116"
+pulldown-cmark = { workspace = true }
+pulldown-cmark-to-cmark = { workspace = true }
+serde_json = { workspace = true }
[dev-dependencies]
-assert_cmd = "2.0.14"
+assert_cmd = { workspace = true }
diff --git a/rustbook-en/src/ch01-02-hello-world.md b/rustbook-en/src/ch01-02-hello-world.md
index f29146379..f0b97e494 100644
--- a/rustbook-en/src/ch01-02-hello-world.md
+++ b/rustbook-en/src/ch01-02-hello-world.md
@@ -49,7 +49,7 @@ convention is to use an underscore to separate them. For example, use
Now open the *main.rs* file you just created and enter the code in Listing 1-1.
-Filename: main.rs
+
```rust
fn main() {
@@ -57,7 +57,7 @@ fn main() {
}
```
-Listing 1-1: A program that prints `Hello, world!`
+
Save the file and go back to your terminal window in the
*~/projects/hello_world* directory. On Linux or macOS, enter the following
diff --git a/rustbook-en/tests/integration/main.rs b/rustbook-en/tests/integration/main.rs
new file mode 100644
index 000000000..6944fae69
--- /dev/null
+++ b/rustbook-en/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/theme/listing.css b/rustbook-en/theme/listing.css
new file mode 100644
index 000000000..9b5929c6e
--- /dev/null
+++ b/rustbook-en/theme/listing.css
@@ -0,0 +1,3 @@
+figure.listing {
+ margin: 0;
+}