summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTom Milligan <tom.milligan@uipath.com>2023-07-21 17:41:58 +0100
committerTom Milligan <tom.milligan@uipath.com>2023-07-22 10:44:11 +0100
commit4842daea1c0f1624344fe55c5d906ccd72345203 (patch)
treeb1e3169c8f628a3d9b08de9f1bcd1964441404f9
parent76212fccfb0b4978c1d35e2a0f688fbf8af2303a (diff)
feat: add support for test renderer, running doctests
-rw-r--r--integration/book.toml3
-rw-r--r--integration/expected/book.toml3
-rw-r--r--integration/expected/chapter_1_main.html22
-rwxr-xr-xintegration/scripts/check16
-rw-r--r--integration/src/chapter_1.md12
-rwxr-xr-xscripts/install-mdbook2
-rw-r--r--src/lib.rs400
-rw-r--r--src/parse.rs2
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=&quot;
| ^
@@ -38,7 +38,7 @@ invalid basic string
</code></pre>
<p>Original markdown input:</p>
-<pre><code>```admonish title=&quot;
+<pre><code class="language-markdown">```admonish title=&quot;
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
diff --git a/src/lib.rs b/src/lib.rs
index 306be87..063c9b1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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()