diff options
author | Tom Milligan <tom.milligan@uipath.com> | 2023-07-21 17:41:58 +0100 |
---|---|---|
committer | Tom Milligan <tom.milligan@uipath.com> | 2023-07-22 10:44:11 +0100 |
commit | 4842daea1c0f1624344fe55c5d906ccd72345203 (patch) | |
tree | b1e3169c8f628a3d9b08de9f1bcd1964441404f9 | |
parent | 76212fccfb0b4978c1d35e2a0f688fbf8af2303a (diff) |
feat: add support for test renderer, running doctests
-rw-r--r-- | integration/book.toml | 3 | ||||
-rw-r--r-- | integration/expected/book.toml | 3 | ||||
-rw-r--r-- | integration/expected/chapter_1_main.html | 22 | ||||
-rwxr-xr-x | integration/scripts/check | 16 | ||||
-rw-r--r-- | integration/src/chapter_1.md | 12 | ||||
-rwxr-xr-x | scripts/install-mdbook | 2 | ||||
-rw-r--r-- | src/lib.rs | 400 | ||||
-rw-r--r-- | src/parse.rs | 2 |
8 files changed, 382 insertions, 78 deletions
diff --git a/integration/book.toml b/integration/book.toml index 5e0ae1f..89e18e7 100644 --- a/integration/book.toml +++ b/integration/book.toml @@ -12,6 +12,9 @@ command = "mdbook-admonish" assets_version = "2.0.1" # do not edit: managed by `mdbook-admonish install` after = ["links"] +[preprocessor.admonish.renderer.test] +render_mode = "strip" + [output] [output.html] diff --git a/integration/expected/book.toml b/integration/expected/book.toml index 5e0ae1f..89e18e7 100644 --- a/integration/expected/book.toml +++ b/integration/expected/book.toml @@ -12,6 +12,9 @@ command = "mdbook-admonish" assets_version = "2.0.1" # do not edit: managed by `mdbook-admonish install` after = ["links"] +[preprocessor.admonish.renderer.test] +render_mode = "strip" + [output] [output.html] diff --git a/integration/expected/chapter_1_main.html b/integration/expected/chapter_1_main.html index 57b88fb..1425700 100644 --- a/integration/expected/chapter_1_main.html +++ b/integration/expected/chapter_1_main.html @@ -30,7 +30,7 @@ </div> <div> <p>Failed with:</p> -<pre><code>TOML parsing error: TOML parse error at line 1, column 8 +<pre><code class="language-log">TOML parsing error: TOML parse error at line 1, column 8 | 1 | title=" | ^ @@ -38,7 +38,7 @@ invalid basic string </code></pre> <p>Original markdown input:</p> -<pre><code>```admonish title=" +<pre><code class="language-markdown">```admonish title=" No title, only body ``` </code></pre> @@ -72,4 +72,22 @@ No title, only body </code></pre> </div> </div> +<div id="admonition-note-3" class="admonition note"> +<div class="admonition-title"> +<p>Note</p> +<p><a class="admonition-anchor-link" href="#admonition-note-3"></a></p> +</div> +<div> +<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)] +</span><span class="boring">fn main() { +</span>let x = 10; +x = 20; +<span class="boring">}</span></code></pre></pre> +<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)] +</span><span class="boring">fn main() { +</span>let x = 10; +let x = 20; +<span class="boring">}</span></code></pre></pre> +</div> +</div> diff --git a/integration/scripts/check b/integration/scripts/check index ac831db..dfaf94a 100755 --- a/integration/scripts/check +++ b/integration/scripts/check @@ -51,7 +51,21 @@ if [ "$DIFF_RESULT" != 0 ]; then eprintln "error: generated html was different than expected" eprintln "" eprintln "error: If you expected the output to change, run:" - eprintln "./integration/update-snapshot" + eprintln "./integration/scripts/update-snapshot" eprintln "and commit the result" exit 1 fi + +eprintln "Verifying mdbook test runs doctests" +set +e +TEST_RESULT="$(mdbook test 2>&1 | grep "1 passed; 1 failed")" +set -e + +if [[ "$TEST_RESULT" != "test result: FAILED. 1 passed; 1 failed;"* ]]; then + eprintln "" + eprintln "error: mdbook test did not complete as expected" + eprintln "" + eprintln "Full output:" + mdbook test + exit 1 +fi diff --git a/integration/src/chapter_1.md b/integration/src/chapter_1.md index b1db164..aa0b3ce 100644 --- a/integration/src/chapter_1.md +++ b/integration/src/chapter_1.md @@ -29,3 +29,15 @@ Hidden on load Nested code block ``` ```` + +````admonish +```rust +let x = 10; +x = 20; +``` + +```rust +let x = 10; +let x = 20; +``` +```` diff --git a/scripts/install-mdbook b/scripts/install-mdbook index 0b550bf..8ff23e8 100755 --- a/scripts/install-mdbook +++ b/scripts/install-mdbook @@ -5,5 +5,5 @@ set -exuo pipefail cd "$(dirname "$0")"/.. if ! mdbook --version; then - cargo install mdbook --force + cargo install mdbook --version 0.4.32 --force fi @@ -19,6 +19,50 @@ use crate::{ }; #[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RenderTextMode { + Strip, + Html, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RenderMode { + Preserve, + Strip, + Html, +} + +impl FromStr for RenderMode { + type Err = (); + + fn from_str(string: &str) -> Result<Self, ()> { + match string { + "preserve" => Ok(Self::Preserve), + "strip" => Ok(Self::Strip), + _ => Err(()), + } + } +} + +fn test_render_mode(context: &PreprocessorContext) -> Result<RenderMode> { + const TOML_KEY: &str = "preprocessor.admonish.renderer.test.render_mode"; + let value = context.config.get(TOML_KEY); + + // If no key set, return default + let value = if let Some(value) = value { + value + } else { + return Ok(RenderMode::Preserve); + }; + + // Othersise, parse value + let value = value + .as_str() + .with_context(|| format!("Invalid value for {TOML_KEY}: {value:?}"))?; + + RenderMode::from_str(value).map_err(|_| anyhow!("Invalid value for {TOML_KEY}: {value}")) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] enum OnFailure { Bail, Continue, @@ -53,6 +97,24 @@ impl OnFailure { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum Renderer { + Html, + Test, +} + +impl FromStr for Renderer { + type Err = (); + + fn from_str(string: &str) -> Result<Self, ()> { + match string { + "html" => Ok(Self::Html), + "test" => Ok(Self::Test), + _ => Err(()), + } + } +} + pub struct Admonish; impl Preprocessor for Admonish { @@ -63,6 +125,22 @@ impl Preprocessor for Admonish { fn run(&self, ctx: &PreprocessorContext, mut book: Book) -> MdbookResult<Book> { ensure_compatible_assets_version(ctx)?; let on_failure = OnFailure::from_context(ctx); + let admonition_defaults = load_defaults(ctx)?; + let renderer = Renderer::from_str(&ctx.renderer).map_err(|_| { + anyhow!( + "mdbook-admonish called with unsupported renderer '{}", + &ctx.renderer + ) + })?; + let render_mode = match renderer { + Renderer::Html => RenderMode::Html, + Renderer::Test => test_render_mode(ctx)?, + }; + let render_text_mode = match render_mode { + RenderMode::Preserve => return Ok(book), + RenderMode::Html => RenderTextMode::Html, + RenderMode::Strip => RenderTextMode::Strip, + }; let mut res = None; book.for_each_mut(|item: &mut BookItem| { @@ -71,9 +149,17 @@ impl Preprocessor for Admonish { } if let BookItem::Chapter(ref mut chapter) = *item { - res = Some(preprocess(&chapter.content, ctx, on_failure).map(|md| { - chapter.content = md; - })); + res = Some( + preprocess( + &chapter.content, + on_failure, + &admonition_defaults, + render_text_mode, + ) + .map(|md| { + chapter.content = md; + }), + ); } }); @@ -81,7 +167,7 @@ impl Preprocessor for Admonish { } fn supports_renderer(&self, renderer: &str) -> bool { - renderer == "html" + Renderer::from_str(renderer).is_ok() } } @@ -215,6 +301,13 @@ impl<'a> Admonition<'a> { </{admonition_block}>"#, ) } + + /// Strips all admonish syntax, leaving the plain content of the block. + fn strip(&self) -> String { + // Add in newlines to preserve line numbering for test output + // These replace the code fences we stripped out + format!("\n{}\n", self.content) + } } const ANCHOR_ID_PREFIX: &str = "admonition"; @@ -259,13 +352,13 @@ fn parse_admonition<'a>( content: Cow::Owned(format!( r#"Failed with: -``` +```log {message} ``` Original markdown input: -{enclosing_fence} +{enclosing_fence}markdown {content} {enclosing_fence} "# @@ -280,9 +373,9 @@ Original markdown input: } fn load_defaults(ctx: &PreprocessorContext) -> Result<AdmonitionDefaults> { - let table_op = ctx.config.get("preprocessor.admonish.default"); + let table = ctx.config.get("preprocessor.admonish.default"); - Ok(if let Some(table) = table_op { + Ok(if let Some(table) = table { table .to_owned() .try_into() @@ -294,11 +387,10 @@ fn load_defaults(ctx: &PreprocessorContext) -> Result<AdmonitionDefaults> { fn preprocess( content: &str, - ctx: &PreprocessorContext, on_failure: OnFailure, + admonition_defaults: &AdmonitionDefaults, + render_text_mode: RenderTextMode, ) -> MdbookResult<String> { - let admonition_defaults = load_defaults(ctx)?; - let mut id_counter = Default::default(); let mut opts = Options::empty(); opts.insert(Options::ENABLE_TABLES); @@ -325,16 +417,25 @@ fn preprocess( }; let admonition = admonition?; - let anchor_id = unique_id_from_content( - if !admonition.title.is_empty() { - &admonition.title - } else { - ANCHOR_ID_DEFAULT - }, - &mut id_counter, - ); - admonish_blocks.push((span, admonition.html(&anchor_id))); + // Once we've identitified admonition blocks, handle them differently + // depending on our render mode + let new_content = match render_text_mode { + RenderTextMode::Html => { + let anchor_id = unique_id_from_content( + if !admonition.title.is_empty() { + &admonition.title + } else { + ANCHOR_ID_DEFAULT + }, + &mut id_counter, + ); + admonition.html(&anchor_id) + } + RenderTextMode::Strip => admonition.strip(), + }; + + admonish_blocks.push((span, new_content)); } } @@ -352,54 +453,58 @@ fn preprocess( mod test { use super::*; use pretty_assertions::assert_eq; + use serde_json::{json, Value}; + + fn mock_book(content: &str) -> Book { + serde_json::from_value(json!({ + "sections": [ + { + "Chapter": { + "name": "Chapter 1", + "content": content, + "number": [1], + "sub_items": [], + "path": "chapter_1.md", + "source_path": "chapter_1.md", + "parent_names": [] + } + } + ], + "__non_exhaustive": null + })) + .unwrap() + } - fn create_mock_context(admonish_ops: &str) -> PreprocessorContext { - let input_json = format!( - r##"[ - {{ - "root": "/path/to/book", - "config": {{ - "book": {{ - "authors": ["AUTHOR"], - "language": "en", - "multilingual": false, - "src": "src", - "title": "TITLE" - }}, - "preprocessor": {{ - "admonish": {admonish_ops} - }} - }}, - "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(); + fn mock_context(admonish: &Value, renderer: &str) -> PreprocessorContext { + let value = json!({ + "root": "/path/to/book", + "config": { + "book": { + "authors": ["AUTHOR"], + "language": "en", + "multilingual": false, + "src": "src", + "title": "TITLE" + }, + "preprocessor": { + "admonish": admonish, + } + }, + "renderer": renderer, + "mdbook_version": "0.4.21" + }); - let (ctx, _) = mdbook::preprocess::CmdPreprocessor::parse_input(input_json).unwrap(); - ctx + serde_json::from_value(value).unwrap() } fn prep(content: &str) -> String { - let ctx = create_mock_context("{}"); - preprocess(content, &ctx, OnFailure::Continue).unwrap() + preprocess( + content, + OnFailure::Continue, + &AdmonitionDefaults::default(), + RenderTextMode::Html, + ) + .unwrap() } #[test] @@ -853,7 +958,7 @@ Error rendering admonishment Failed with: -``` +```log TOML parsing error: TOML parse error at line 1, column 8 | 1 | title=" @@ -864,7 +969,7 @@ invalid basic string Original markdown input: -```` +````markdown ```admonish title=" Bonus content! ``` @@ -885,11 +990,15 @@ Bonus content! Bonus content! ``` "#; - let ctx = create_mock_context(r#"{}"#); assert_eq!( - preprocess(content, &ctx, OnFailure::Bail) - .unwrap_err() - .to_string(), + preprocess( + content, + OnFailure::Bail, + &AdmonitionDefaults::default(), + RenderTextMode::Html + ) + .unwrap_err() + .to_string(), r#"Error processing admonition, bailing: ```admonish title=" Bonus content! @@ -899,6 +1008,135 @@ Bonus content! } #[test] + fn run_html() { + let content = r#" +````admonish title="Title" +```rust +let x = 10; +x = 20; +``` +```` +"#; + let expected_content = r##" + +<div id="admonition-title" class="admonition note"> +<div class="admonition-title"> + +Title + +<a class="admonition-anchor-link" href="#admonition-title"></a> +</div> +<div> + +```rust +let x = 10; +x = 20; +``` + +</div> +</div> +"##; + + let ctx = mock_context( + &json!({ + "assets_version": "2.0.0" + }), + "html", + ); + let book = mock_book(content); + let expected_book = mock_book(expected_content); + + assert_eq!(Admonish.run(&ctx, book).unwrap(), expected_book) + } + + #[test] + fn run_test_preserves_by_default() { + let content = r#" +````admonish title="Title" +```rust +let x = 10; +x = 20; +``` +```` +"#; + let ctx = mock_context( + &json!({ + "assets_version": "2.0.0" + }), + "test", + ); + let book = mock_book(content); + let expected_book = book.clone(); + + assert_eq!(Admonish.run(&ctx, book).unwrap(), expected_book) + } + + #[test] + fn run_test_can_strip() { + let content = r#" +````admonish title="Title" +```rust +let x = 10; +x = 20; +``` +```` +"#; + let expected_content = r#" + +```rust +let x = 10; +x = 20; +``` + +"#; + let ctx = mock_context( + &json!({ + "assets_version": "2.0.0", + "renderer": { + "test": { + "render_mode": "strip", + }, + }, + }), + "test", + ); + let book = mock_book(content); + let expected_book = mock_book(expected_content); + + assert_eq!(Admonish.run(&ctx, book).unwrap(), expected_book) + } + + #[test] + fn test_renderer_strip_explicit() { + let content = r#" +````admonish title="Title" +```rust +let x = 10; +x = 20; +``` +```` +"#; + assert_eq!( + preprocess( + content, + OnFailure::Bail, + &AdmonitionDefaults::default(), + RenderTextMode::Strip + ) + .unwrap(), + r#" + +```rust +let x = 10; +x = 20; +``` + +"# + .to_owned() + ) + } + + #[test] fn block_collapsible() { let content = r#" ```admonish collapsible=true @@ -953,8 +1191,16 @@ A simple admonition. Text "##; - let ctx = create_mock_context(r#"{"default": {"title": "Admonish"}}"#); - let preprocess_result = preprocess(content, &ctx, OnFailure::Continue).unwrap(); + let preprocess_result = preprocess( + content, + OnFailure::Continue, + &AdmonitionDefaults { + title: Some("Admonish".to_owned()), + collapsible: None, + }, + RenderTextMode::Html, + ) + .unwrap(); assert_eq!(expected, preprocess_result); } @@ -979,8 +1225,16 @@ A simple admonition. Text "##; - let ctx = create_mock_context(r#"{"default": {"title": "Admonish"}}"#); - let preprocess_result = preprocess(content, &ctx, OnFailure::Continue).unwrap(); + let preprocess_result = preprocess( + content, + OnFailure::Continue, + &AdmonitionDefaults { + title: Some("Admonish".to_owned()), + collapsible: None, + }, + RenderTextMode::Html, + ) + .unwrap(); assert_eq!(expected, preprocess_result); } diff --git a/src/parse.rs b/src/parse.rs index 52b5f45..3d11eac 100644 --- a/src/parse.rs +++ b/src/parse.rs @@ -25,7 +25,7 @@ fn extract_admonish_body_start_index(content: &str) -> usize { } fn extract_admonish_body_end_index(content: &str) -> (usize, Fence) { - let fence_character = content.chars().rev().next().unwrap_or('`'); + let fence_character = content.chars().next_back().unwrap_or('`'); let number_fence_characters = content .chars() .rev() |