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 +///
+/// Filename: src/main.rs +/// +/// ```rust +/// fn main() {} +/// ``` +/// +///
Listing 1-2: Some text, yeah?
+/// +///
+/// ```` +/// +/// 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("
\n"); + + match self.file_name.as_ref() { + Some(file_name) => format!( + "{figure}Filename: {file_name}\n", + ), + None => figure, + } + } + + fn closing_html(&self, trailing: &str) -> String { + format!( + r#"
Listing {number}: {caption}
+
{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#"
+Filename: src/main.rs + +````rust +fn main() {} +```` + +
Listing 1-2: A write-up which might include inline Markdown like code etc.
+
"# + ); + } + + #[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. + +
+Filename: main.rs + +````rust +fn main() { + println!("Hello, world!"); +} +```` + +
Listing 1-1: A program that prints Hello, world!
+
+ +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. + +
+ +````rust +fn main() {} +```` + +
Listing 1-1: This is the caption
+
+ +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; +}