Add more comprehensive tests for HTML rendering

This adds a bunch of tests to better exercise the HTML rendering and to
be able to track any changes in its behavior.

This includes a new `check_all_main_files` to more conveniently check
the HTML content of every chapter in a book.
This commit is contained in:
Eric Huss
2025-09-16 14:07:54 -07:00
parent de155f859b
commit 03443f723c
69 changed files with 1054 additions and 23 deletions

View File

@@ -23,6 +23,8 @@ enum StatusCode {
pub struct BookTest {
/// The temp directory where the test should perform its work.
pub dir: PathBuf,
/// The original source directory if created from [`BookTest::from_dir`].
original_source: Option<PathBuf>,
/// Snapshot assertion support.
pub assert: snapbox::Assert,
/// This indicates whether or not the book has been built.
@@ -45,12 +47,12 @@ impl BookTest {
&[],
)
.unwrap_or_else(|e| panic!("failed to copy test book {dir:?} to {tmp:?}: {e:?}"));
Self::new(tmp)
Self::new(tmp, Some(dir))
}
/// Creates a new test with an empty temp directory.
pub fn empty() -> BookTest {
Self::new(Self::new_tmp())
Self::new(Self::new_tmp(), None)
}
/// Creates a new test with the given function to initialize a new book.
@@ -62,7 +64,7 @@ impl BookTest {
f(&mut bb);
bb.build()
.unwrap_or_else(|e| panic!("failed to initialize book at {tmp:?}: {e:?}"));
Self::new(tmp)
Self::new(tmp, None)
}
fn new_tmp() -> PathBuf {
@@ -78,10 +80,11 @@ impl BookTest {
tmp
}
fn new(dir: PathBuf) -> BookTest {
fn new(dir: PathBuf, original_source: Option<PathBuf>) -> BookTest {
let assert = assert(&dir);
BookTest {
dir,
original_source,
assert,
built: false,
}
@@ -101,13 +104,64 @@ impl BookTest {
let actual = read_to_string(&full_path);
let start = actual
.find("<main>")
.unwrap_or_else(|| panic!("didn't find <main> in:\n{actual}"));
.unwrap_or_else(|| panic!("didn't find <main> for `{full_path:?}` in:\n{actual}"));
let end = actual.find("</main>").unwrap();
let contents = actual[start + 6..end - 7].trim();
self.assert.eq(contents, expected);
self
}
/// Verifies the HTML output of all chapters in a book.
///
/// This calls [`BookTest::check_main_file`] for every `.html` file
/// generated by building the book. All of expected files are stored in a
/// director called "expected" in the original book source directory.
///
/// This only works when created with [`BookTest::from_dir`].
///
/// `404.html`, `print.html`, and `toc.html` are not validated. The root
/// `index.html` is also not validated (since it is often duplicated with
/// the first chapter). If you need to validate it, call
/// [`BookTest::check_main_file`] directly.
#[track_caller]
pub fn check_all_main_files(&mut self) -> &mut Self {
if !self.built {
self.build();
}
let book_root = self.dir.join("book");
let mut files = list_all_files(&book_root);
files.retain(|file| {
file.extension().is_some_and(|ext| ext == "html")
&& !matches!(
file.to_str().unwrap(),
"index.html" | "404.html" | "print.html" | "toc.html"
)
});
let expected_path = self
.original_source
.as_ref()
.expect("created with BookTest::from_dir")
.join("expected");
let mut expected_list = list_all_files(&expected_path);
for file in &files {
let expected = expected_path.join(file);
let data = snapbox::Data::read_from(&expected, None);
self.check_main_file(book_root.join(file).to_str().unwrap(), data);
if let Some(i) = expected_list.iter().position(|p| p == file) {
expected_list.remove(i);
}
}
// Verify there aren't any unused expected files.
if !expected_list.is_empty() {
panic!(
"extra expected files found in `{expected_path:?}:\n\
{expected_list:#?}\n\
Verify that these files are no longer needed and delete them."
);
}
self
}
/// Checks the summary contents of `toc.js` against the expected value.
#[track_caller]
pub fn check_toc_js(&mut self, expected: impl IntoData) -> &mut Self {
@@ -527,6 +581,8 @@ pub fn list_all_files(dir: &Path) -> Vec<PathBuf> {
walkdir::WalkDir::new(dir)
.sort_by_file_name()
.into_iter()
// Skip the outer directory.
.skip(1)
.map(|entry| {
let entry = entry.unwrap();
let path = entry.path();

View File

@@ -11,6 +11,7 @@ fn custom_header_attributes() {
<h1 id="attrs"><a class="header" href="#attrs">Heading Attributes</a></h1>
<h2 id="heading-with-classes" class="class1 class2"><a class="header" href="#heading-with-classes">Heading with classes</a></h2>
<h2 id="both" class="class1 class2"><a class="header" href="#both">Heading with id and classes</a></h2>
<h2 id="myh3" class="myclass1 myclass2" myattr="" otherattr="value">Heading with attribute</h2>
"##]]);
}
@@ -50,6 +51,9 @@ fn tables() {
<tr><td>Pipe in code</td><td><code>|</code></td></tr>
<tr><td>Pipe in code2</td><td><code>test | inside</code></td></tr>
</tbody></table>
</div><div class="table-wrapper"><table><thead><tr><th>Neither</th><th style="text-align: left">Left</th><th style="text-align: center">Center</th><th style="text-align: right">Right</th></tr></thead><tbody>
<tr><td>one</td><td style="text-align: left">two</td><td style="text-align: center">three</td><td style="text-align: right">four</td></tr>
</tbody></table>
</div>
"##]],
);
@@ -101,7 +105,10 @@ fn smart_punctuation() {
<li>Ellipsis: …</li>
<li>Double quote: “quote”</li>
<li>Single quote: quote</li>
<li>Quote in <code>"code"</code></li>
</ul>
<pre><code>"quoted"
</code></pre>
"##]],
)
.run("build", |cmd| {
@@ -117,7 +124,17 @@ fn smart_punctuation() {
<li>Ellipsis: ...</li>
<li>Double quote: "quote"</li>
<li>Single quote: 'quote'</li>
<li>Quote in <code>"code"</code></li>
</ul>
<pre><code>"quoted"
</code></pre>
"##]],
);
}
// Basic markdown syntax.
// This doesn't try to cover the commonmark test suite, but maybe it could some day?
#[test]
fn basic_markdown() {
BookTest::from_dir("markdown/basic_markdown").check_all_main_files();
}

View File

@@ -0,0 +1,2 @@
[book]
title = "basic_markdown"

View File

@@ -0,0 +1,44 @@
<h1 id="blockquotes"><a class="header" href="#blockquotes">Blockquotes</a></h1>
<p>Empty:</p>
<blockquote>
</blockquote>
<p>Normal:</p>
<blockquote>
<p>foo
bar</p>
</blockquote>
<p>Contains code block:</p>
<blockquote>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>let x = 1;
<span class="boring">}</span></code></pre></pre>
</blockquote>
<p>Random stuff:</p>
<blockquote>
<h3 id="and-now"><a class="header" href="#and-now">And now,</a></h3>
<p><strong>Let us <em>introduce</em></strong>
All kinds of</p>
<ul>
<li>tags</li>
<li>etc</li>
<li>stuff</li>
</ul>
<ol>
<li>
<p>In</p>
</li>
<li>
<p>The</p>
</li>
<li>
<p>blockquote</p>
<blockquote>
<p>cause we can</p>
<blockquote>
<p>Cause we can</p>
</blockquote>
</blockquote>
</li>
</ol>
</blockquote>

View File

@@ -0,0 +1,22 @@
<h1 id="code-blocks"><a class="header" href="#code-blocks">Code blocks</a></h1>
<pre><code>This is a codeblock
</code></pre>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// This links to a playpen
<span class="boring">}</span></code></pre></pre>
<pre><code class="language-bash editable"># This is an editable codeblock
</code></pre>
<pre><code class="language-text cls1 cls2 cls3">Text with different classes.
</code></pre>
<pre><code>Indented
code
block.
</code></pre>
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>let x = 1;
<span class="boring">}</span></code></pre></pre>
<pre><pre class="playground"><code class="language-rust">fn main() {
println!("hello");
}</code></pre></pre>

View File

@@ -0,0 +1,67 @@
<h1 id="inline-html"><a class="header" href="#inline-html">Inline HTML</a></h1>
<h2 id="comments"><a class="header" href="#comments">Comments</a></h2>
<!--comment-->
<!--
**bigger**
comment
-->
<h2 id="void-elements"><a class="header" href="#void-elements">Void elements</a></h2>
<map name="my-map">
<area shape="rect" coords="0,0,10,20" href="https://example.com/" alt="alt text">
</map>
<p>Line<br>break</p>
<p>Word<wbr>break</p>
<table>
<colgroup>
<col>
<col span="2" class="a">
</colgroup>
</table>
<p><embed
type="image/jpeg"
src="/image.jpg"
width="100"
height="200"></p>
<p>Rule:</p>
<hr>
<img src="example.jpg">
<input type="text">
<link href="example.css" rel="stylesheet">
<p><meta name="example"
content="Example content"></p>
<video>
<source src="video.webm" type="video/webm">
<track kind="captions" src="captions.vtt" srclang="en">
</video>
<h2 id="blocks"><a class="header" href="#blocks">Blocks</a></h2>
<div>
A block HTML element trying to do *markup*.
</div>
<div>
<p>A block HTML with spaces that <strong>cause</strong> it to be interleaved with markdown.</p>
</div>
<h2 id="scripts"><a class="header" href="#scripts">Scripts</a></h2>
<script></script>
<script async src="foo.js"></script>
<script>
const x = 'some *text* inside';
*don't* < try to "parse me
</script>
<h2 id="style"><a class="header" href="#style">Style</a></h2>
<style>
.foo {
background-color: red;
}
/* **don't** < try to "parse me
*/
</style>
<style media="(width < 500px)">
.bar { background-color: green }
</style>

View File

@@ -0,0 +1,3 @@
<h1 id="images"><a class="header" href="#images">Images</a></h1>
<p><img src="https://rust-lang.org/logos/rust-logo-256x256.png" alt="Image “alt” &amp; &quot; &quot;text&quot; &amp; &lt;stuff&gt; url &lt;em&gt;html&lt;/em&gt; — hard break " /></p>
<p><img src="https://rust-lang.org/logos/rust-logo-256x256.png" alt="Image with title" title="Some title" /></p>

View File

@@ -0,0 +1,8 @@
<h1 id="inlines"><a class="header" href="#inlines">Inlines</a></h1>
<p><em>emphasis</em> <strong>bold</strong> <strong><em>bold emphasis</em></strong></p>
<p>Some <code>inline code</code>.</p>
<p>Hard<br />
break</p>
<p>Invisible hard<br />
break</p>
<p>[escaped] &lt;html&gt; *here*</p>

View File

@@ -0,0 +1,16 @@
<h1 id="links"><a class="header" href="#links">Links</a></h1>
<ul>
<li><a href="https://example.com/inline">Inline</a></li>
<li><a href="https://example.com/collapsed">Collapsed</a></li>
<li>[Emtpy reference][]</li>
<li><a href="https://example.com/specific">Specific reference</a></li>
<li><a href="https://example.com/">https://example.com/</a></li>
<li><a href="mailto:john@example.com">john@example.com</a></li>
<li><a href="https://example.com/title" title="with title">Titled</a></li>
<li>[Broken collapsed]</li>
<li>[Broken reference][missing]</li>
<li><a href="path/foo.html">Markdown link</a></li>
<li><a href="path/foo.html#anchor">Markdown link anchor</a></li>
<li><a href="#anchor">Link anchor</a></li>
<li><a href="path.html#phantomdata">With md in anchor</a></li>
</ul>

View File

@@ -0,0 +1,50 @@
<h1 id="lists"><a class="header" href="#lists">Lists</a></h1>
<ol>
<li>A</li>
<li>Normal</li>
<li>Ordered</li>
<li>List</li>
</ol>
<hr />
<ol>
<li>A
<ol>
<li>Nested</li>
<li>List</li>
</ol>
</li>
<li>But</li>
<li>Still</li>
<li>Normal</li>
</ol>
<hr />
<ol start="7">
<li>Start list</li>
<li>with a different number.</li>
</ol>
<hr />
<ul>
<li>An</li>
<li>Unordered</li>
<li>Normal</li>
<li>List</li>
</ul>
<hr />
<ul>
<li>Nested
<ul>
<li>Unordered</li>
</ul>
</li>
<li>List</li>
</ul>
<hr />
<ul>
<li>This
<ol>
<li>Is</li>
<li>Normal</li>
</ol>
</li>
<li>?!</li>
</ul>

View File

@@ -0,0 +1,12 @@
<h1 id="svg"><a class="header" href="#svg">SVG</a></h1>
<svg version="1.1" width="300" height="200" xmlns="http://www.w3.org/2000/svg">
<style>
rect {
stroke: green;
stroke-width: 10px;
}
</style>
<rect width="100%" height="100%" fill="red" />
<circle cx="150" cy="100" r="80" fill="green" />
<text x="150" y="125" font-size="60" text-anchor="middle" fill="white">SVG</text>
</svg>

View File

@@ -0,0 +1,11 @@
# Summary
- [Blank](./blank.md)
- [Blockquotes](./blockquotes.md)
- [Code blocks](./code-blocks.md)
- [Lists](./lists.md)
- [Inlines](./inlines.md)
- [Links](./links.md)
- [Images](./images.md)
- [HTML](./html.md)
- [SVG](./svg.md)

View File

@@ -0,0 +1,31 @@
# Blockquotes
Empty:
>
Normal:
> foo
> bar
Contains code block:
> ```rust
> let x = 1;
> ```
Random stuff:
> ### And now,
>
> **Let us _introduce_**
> All kinds of
>
> - tags
> - etc
> - stuff
>
> 1. In
> 2. The
> 3. blockquote
>
> > cause we can
> >
> > > Cause we can

View File

@@ -0,0 +1,31 @@
# Code blocks
```
This is a codeblock
```
```rust
// This links to a playpen
```
```bash,editable
# This is an editable codeblock
```
```text cls1,,cls2 cls3
Text with different classes.
```
Indented
code
block.
```rust,edition2021
let x = 1;
```
```rust
fn main() {
println!("hello");
}
```

View File

@@ -0,0 +1,94 @@
# Inline HTML
## Comments
<!--comment-->
<!--
**bigger**
comment
-->
## Void elements
<map name="my-map">
<area shape="rect" coords="0,0,10,20" href="https://example.com/" alt="alt text">
</map>
Line<br>break
Word<wbr>break
<table>
<colgroup>
<col>
<col span="2" class="a">
</colgroup>
</table>
<embed
type="image/jpeg"
src="/image.jpg"
width="100"
height="200">
Rule:
<hr>
<img src="example.jpg">
<input type="text">
<link href="example.css" rel="stylesheet">
<meta name="example"
content="Example content">
<video>
<source src="video.webm" type="video/webm">
<track kind="captions" src="captions.vtt" srclang="en">
</video>
## Blocks
<div>
A block HTML element trying to do *markup*.
</div>
<div>
A block HTML with spaces that **cause** it to be interleaved with markdown.
</div>
## Scripts
<script></script>
<script async src="foo.js"></script>
<script>
const x = 'some *text* inside';
*don't* < try to "parse me
</script>
## Style
<style>
.foo {
background-color: red;
}
/* **don't** < try to "parse me
*/
</style>
<style media="(width < 500px)">
.bar { background-color: green }
</style>

View File

@@ -0,0 +1,12 @@
# Images
![Image *"alt"* & \"
`"text" & <stuff>`
[url](/foo)
<em>html</em>
---
hard\
break
](https://rust-lang.org/logos/rust-logo-256x256.png)
![Image with title](https://rust-lang.org/logos/rust-logo-256x256.png "Some title")

View File

@@ -0,0 +1,13 @@
# Inlines
*emphasis* **bold** **_bold emphasis_**
Some `inline code`.
Hard\
break
Invisible hard
break
\[escaped] \<html> \*here*

View File

@@ -0,0 +1,18 @@
# Links
- [Inline](https://example.com/inline)
- [Collapsed]
- [Emtpy reference][]
- [Specific reference][specific]
- <https://example.com/>
- <john@example.com>
- [Titled](https://example.com/title "with title")
- [Broken collapsed]
- [Broken reference][missing]
- [Markdown link](path/foo.md)
- [Markdown link anchor](path/foo.md#anchor)
- [Link anchor](#anchor)
- [With md in anchor](path.html#phantomdata)
[collapsed]: https://example.com/collapsed
[specific]: https://example.com/specific

View File

@@ -0,0 +1,40 @@
# Lists
1. A
2. Normal
3. Ordered
4. List
---
1. A
1. Nested
2. List
2. But
3. Still
4. Normal
---
7. Start list
7. with a different number.
---
- An
- Unordered
- Normal
- List
---
- Nested
- Unordered
- List
---
- This
1. Is
2. Normal
- ?!

View File

@@ -0,0 +1,14 @@
# SVG
<svg version="1.1" width="300" height="200" xmlns="http://www.w3.org/2000/svg">
<style>
rect {
stroke: green;
stroke-width: 10px;
}
</style>
<rect width="100%" height="100%" fill="red" />
<circle cx="150" cy="100" r="80" fill="green" />
<text x="150" y="125" font-size="60" text-anchor="middle" fill="white">SVG</text>
</svg>

View File

@@ -3,3 +3,5 @@
## Heading with classes {.class1 .class2}
## Heading with id and classes {#both .class1 .class2}
## Heading with attribute {.myclass1 myattr #myh3 otherattr=value .myclass2}

View File

@@ -5,3 +5,8 @@
- Ellipsis: ...
- Double quote: "quote"
- Single quote: 'quote'
- Quote in `"code"`
```
"quoted"
```

View File

@@ -7,3 +7,7 @@
| Double back in code | `\\` |
| Pipe in code | `\|` |
| Pipe in code2 | `test \| inside` |
| Neither | Left | Center | Right |
|---------|:-----|:------:|------:|
| one | two | three | four |

View File

@@ -1,3 +1,3 @@
# Summary
- [Rust Playground](./index.md)
- [Rust Playground](./disabled-rust-playground.md)

View File

@@ -1,3 +1,3 @@
# Summary
- [Rust Playground](./index.md)
- [Rust Playground](./rust-playground.md)

View File

@@ -1,25 +1,33 @@
//! Tests for print page.
use crate::prelude::*;
use snapbox::file;
// Tests relative links from the print page.
#[test]
fn relative_links() {
BookTest::from_dir("print/relative_links")
.check_main_file("book/print.html",
str![[r##"
<h1 id="first-chapter"><a class="header" href="#first-chapter">First Chapter</a></h1>
<div style="break-before: page; page-break-before: always;"></div><h1 id="first-nested"><a class="header" href="#first-nested">First Nested</a></h1>
<div style="break-before: page; page-break-before: always;"></div><h1 id="testing-relative-links-for-the-print-page"><a class="header" href="#testing-relative-links-for-the-print-page">Testing relative links for the print page</a></h1>
<p>When we link to <a href="second/../first/nested.html">the first section</a>, it should work on
both the print page and the non-print page.</p>
<p>A <a href="second/nested.html#some-section">fragment link</a> should work.</p>
<p>Link <a href="second/../../std/foo/bar.html">outside</a>.</p>
<p><img src="second/../images/picture.png" alt="Some image" /></p>
<p><a href="second/../first/markdown.html">HTML Link</a></p>
<img src="second/../images/picture.png" alt="raw html">
<h2 id="some-section"><a class="header" href="#some-section">Some section</a></h2>
"##]]);
BookTest::from_dir("print/relative_links").check_main_file(
"book/print.html",
file!("print/relative_links/expected/print.html"),
);
}
// Test for duplicate IDs, and links to those duplicates.
#[test]
fn duplicate_ids() {
BookTest::from_dir("print/duplicate_ids").check_main_file(
"book/print.html",
file!("print/duplicate_ids/expected/print.html"),
);
}
// Test for synthesized link to a chapter that does not have an h1.
#[test]
fn chapter_no_h1() {
BookTest::from_dir("print/chapter_no_h1").check_main_file(
"book/print.html",
file!("print/chapter_no_h1/expected/print.html"),
);
}
// Checks that print.html is noindex.

View File

@@ -0,0 +1,2 @@
[book]
title = "chapter_no_h1"

View File

@@ -0,0 +1,5 @@
<h1 id="chapter-1"><a class="header" href="#chapter-1">Chapter 1</a></h1>
<p>See <a href="chapter_2.html">chapter 2</a>.</p>
<div style="break-before: page; page-break-before: always;"></div><p>See <a href="./chapter_2.html">this</a>.</p>
<div style="break-before: page; page-break-before: always;"></div><h2 id="h2-instead"><a class="header" href="#h2-instead">H2 instead</a></h2>
<p>This has H2 instead of H1.</p>

View File

@@ -0,0 +1,5 @@
# Summary
- [Chapter 1](./chapter_1.md)
- [Chapter 2](./chapter_2.md)
- [H2 instead](./h2-instead.md)

View File

@@ -0,0 +1,3 @@
# Chapter 1
See [chapter 2](chapter_2.md).

View File

@@ -0,0 +1 @@
See [this](./chapter_2.md).

View File

@@ -0,0 +1,4 @@
## H2 instead
This has H2 instead of H1.

View File

@@ -0,0 +1,2 @@
[book]
title = "duplicate_ids"

View File

@@ -0,0 +1,11 @@
<h1 id="chapter-1"><a class="header" href="#chapter-1">Chapter 1</a></h1>
<h2 id="some-title"><a class="header" href="#some-title">Some title</a></h2>
<p>See <a href="chapter_2.html#some-title">other</a></p>
<p>See <a href="chapter_1.html#some-title">this</a></p>
<p>See <a href="chapter_1.html#some-title">this anchor only</a></p>
<div style="break-before: page; page-break-before: always;"></div><h1 id="chapter-2"><a class="header" href="#chapter-2">Chapter 2</a></h1>
<h2 id="some-title-1"><a class="header" href="#some-title-1">Some title</a></h2>
<p>See <a href="chapter_1.html#some-title">other</a></p>
<p>See <a href="chapter_2.html#some-title">this</a></p>
<p>See <a href="chapter_2.html#some-title">this anchor only</a></p>
<p><a href="chapter_1.html#some-title">Works with HTML extension too</a></p>

View File

@@ -0,0 +1,4 @@
# Summary
- [Chapter 1](./chapter_1.md)
- [Chapter 2](./chapter_2.md)

View File

@@ -0,0 +1,9 @@
# Chapter 1
## Some title
See [other](chapter_2.md#some-title)
See [this](chapter_1.md#some-title)
See [this anchor only](#some-title)

View File

@@ -0,0 +1,11 @@
# Chapter 2
## Some title
See [other](chapter_1.md#some-title)
See [this](chapter_2.md#some-title)
See [this anchor only](#some-title)
[Works with HTML extension too](chapter_1.html#some-title)

View File

@@ -0,0 +1 @@
# Intro

View File

@@ -0,0 +1,15 @@
<h1 id="first-chapter"><a class="header" href="#first-chapter">First Chapter</a></h1>
<div style="break-before: page; page-break-before: always;"></div><h1 id="first-nested"><a class="header" href="#first-nested">First Nested</a></h1>
<div style="break-before: page; page-break-before: always;"></div><h1 id="testing-relative-links-for-the-print-page"><a class="header" href="#testing-relative-links-for-the-print-page">Testing relative links for the print page</a></h1>
<p>When we link to <a href="second/../first/nested.html">the first section</a>, it should work on
both the print page and the non-print page.</p>
<p>The same link should work <a href="second/../first/nested.html">with an html extension</a>.</p>
<p>A <a href="second/nested.html#some-section">fragment link</a> should work.</p>
<p>Link <a href="second/../../std/foo/bar.html">outside</a>.</p>
<p>Link <a href="second/../../std/foo/bar.html#panic">outside with anchor</a>.</p>
<p><img src="second/../images/picture.png" alt="Some image" /></p>
<p><a href="second/../first/nested.html">HTML Link</a></p>
<img src="second/../images/picture.png" alt="raw html">
<h2 id="some-section"><a class="header" href="#some-section">Some section</a></h2>
<p><a href="https://example.com/foo.html#bar">Links with scheme shouldnt be touched.</a></p>
<p><a href="second/../images/not-html?arg1&arg2#with-anchor">Non-html link</a></p>

View File

@@ -3,14 +3,22 @@
When we link to [the first section](../first/nested.md), it should work on
both the print page and the non-print page.
The same link should work [with an html extension](../first/nested.html).
A [fragment link](#some-section) should work.
Link [outside](../../std/foo/bar.html).
Link [outside with anchor](../../std/foo/bar.html#panic).
![Some image](../images/picture.png)
<a href="../first/markdown.md">HTML Link</a>
<a href="../first/nested.md">HTML Link</a>
<img src="../images/picture.png" alt="raw html">
## Some section
[Links with scheme shouldn't be touched.](https://example.com/foo.html#bar)
<a href="../images/not-html?arg1&arg2#with-anchor">Non-html link</a>

View File

@@ -1,4 +1,6 @@
//! Tests for HTML rendering.
//!
//! Note that markdown-specific rendering tests are in the `markdown` module.
use crate::prelude::*;
@@ -41,3 +43,185 @@ fn first_chapter_is_copied_as_index_even_if_not_first_elem() {
]],
);
}
// Fontawesome `<i>` tag support.
#[test]
fn fontawesome() {
BookTest::from_dir("rendering/fontawesome").check_all_main_files();
}
// Tests the rendering when setting the default rust edition.
#[test]
fn default_rust_edition() {
BookTest::from_dir("rendering/default_rust_edition").check_all_main_files();
}
// Tests the rendering for editable code blocks.
#[test]
fn editable_rust_block() {
BookTest::from_dir("rendering/editable_rust_block").check_all_main_files();
}
// Tests for custom hide lines.
#[test]
fn hidelines() {
BookTest::from_dir("rendering/hidelines").check_all_main_files();
}
// Tests for code blocks of basic rust code.
#[test]
fn language_rust_playground() {
fn expect(input: &str, info: &str, expected: impl snapbox::IntoData) {
BookTest::init(|_| {})
.change_file("book.toml", "output.html.playground.editable = true")
.change_file("src/chapter_1.md", &format!("```rust {info}\n{input}\n```"))
.check_main_file("book/chapter_1.html", expected);
}
// No-main should be wrapped in `fn main` boring lines.
expect(
"x()",
"",
str![[r#"
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>x()
<span class="boring">}</span></code></pre></pre>
"#]],
);
// `fn main` should not be wrapped, not boring.
expect(
"fn main() {}",
"",
str![[
r#"<pre><pre class="playground"><code class="language-rust">fn main() {}</code></pre></pre>"#
]],
);
// Lines starting with `#` are boring.
expect(
"let s = \"foo\n # bar\n\";",
"editable",
str![[r#"
<pre><pre class="playground"><code class="language-rust editable">let s = "foo
<span class="boring"> bar
</span>";</code></pre></pre>
"#]],
);
// `##` is not boring and is used as an escape.
expect(
"let s = \"foo\n ## bar\n\";",
"editable",
str![[r#"
<pre><pre class="playground"><code class="language-rust editable">let s = "foo
# bar
";</code></pre></pre>
"#]],
);
// `#` on a line by itself is boring.
expect(
"let s = \"foo\n # bar\n#\n\";",
"editable",
str![[r#"
<pre><pre class="playground"><code class="language-rust editable">let s = "foo
<span class="boring"> bar
</span><span class="boring">
</span>";</code></pre></pre>
"#]],
);
// `#` must be followed by a space to be boring.
expect(
"#x;",
"",
str![[r#"
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>#x;
<span class="boring">}</span></code></pre></pre>
"#]],
);
// Other classes like "ignore" should not change things, and the class is
// included in the code tag.
expect(
"let s = \"foo\n # bar\n\";",
"ignore",
str![[r#"
<pre><code class="language-rust ignore">let s = "foo
<span class="boring"> bar
</span>";</code></pre>
"#]],
);
// Inner attributes and normal attributes are not boring.
expect(
"#![no_std]\nlet s = \"foo\";\n #[some_attr]",
"editable",
str![[r#"
<pre><pre class="playground"><code class="language-rust editable">#![no_std]
let s = "foo";
#[some_attr]</code></pre></pre>
"#]],
);
}
// Rust code block in a list.
#[test]
fn code_block_in_list() {
BookTest::init(|_| {})
.change_file(
"src/chapter_1.md",
r#"- inside list
```rust
fn foo() {
let x = 1;
}
```
"#,
)
.check_main_file(
"book/chapter_1.html",
str![[r#"
<ul>
<li>
<p>inside list</p>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>fn foo() {
let x = 1;
}
<span class="boring">}</span></code></pre></pre>
</li>
</ul>
"#]],
);
}
// Checks the rendering of links added to headers.
#[test]
fn header_links() {
BookTest::from_dir("rendering/header_links").check_all_main_files();
}
// A corrupted HTML end tag.
#[test]
fn busted_end_tag() {
BookTest::init(|_| {})
.change_file("src/chapter_1.md", "<div>x<span>foo</span/>y</div>")
.run("build", |cmd| {
cmd.expect_stderr(str![[r#"
INFO Book building has started
INFO Running the html backend
INFO HTML book written to `[ROOT]/book`
"#]]);
})
.check_main_file(
"book/chapter_1.html",
str!["<div>x<span>foo</span/>y</div>"],
);
}
// Various html blocks.
#[test]
fn html_blocks() {
BookTest::from_dir("rendering/html_blocks").check_all_main_files();
}

View File

@@ -0,0 +1,5 @@
[book]
title = "default_rust_edition"
[rust]
edition = "2021"

View File

@@ -0,0 +1,13 @@
<h1 id="chapter-1"><a class="header" href="#chapter-1">Chapter 1</a></h1>
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>let x = 2021;
<span class="boring">}</span></code></pre></pre>
<pre><pre class="playground"><code class="language-rust edition2021"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>let x = 2021;
<span class="boring">}</span></code></pre></pre>
<pre><pre class="playground"><code class="language-rust edition2024 edition2021"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>let x = 2024;
<span class="boring">}</span></code></pre></pre>

View File

@@ -0,0 +1,3 @@
# Summary
- [Default rust edition](./default-rust-edition.md)

View File

@@ -0,0 +1,13 @@
# Chapter 1
```rust
let x = 2021;
```
```rust,edition2021
let x = 2021;
```
```rust,edition2024
let x = 2024;
```

View File

@@ -0,0 +1,5 @@
[book]
title = "editable_rust_block"
[output.html.playground]
editable = true

View File

@@ -0,0 +1,8 @@
<h1 id="chapter-1"><a class="header" href="#chapter-1">Chapter 1</a></h1>
<pre><pre class="playground"><code class="language-rust editable">fn f() {
println!("hello");
}</code></pre></pre>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span><span class="boring">fn main() {
</span>// Not editable.
<span class="boring">}</span></code></pre></pre>

View File

@@ -0,0 +1,3 @@
# Summary
- [Editable rust blocks](./editable-rust.md)

View File

@@ -0,0 +1,11 @@
# Chapter 1
```rust,editable
fn f() {
println!("hello");
}
```
```rust
// Not editable.
```

View File

@@ -0,0 +1,2 @@
[book]
title = "fontawesome"

View File

@@ -0,0 +1,5 @@
<h1 id="chapter-1"><a class="header" href="#chapter-1">Chapter 1</a></h1>
<p><span id="example1" class="fa-svg extra-class"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M47.6 300.4L228.3 469.1c7.5 7 17.4 10.9 27.7 10.9s20.2-3.9 27.7-10.9L464.4 300.4c30.4-28.3 47.6-68 47.6-109.5v-5.8c0-69.9-50.5-129.5-119.4-141C347 36.5 300.6 51.4 268 84L256 96 244 84c-32.6-32.6-79-47.5-124.6-39.9C50.5 55.6 0 115.2 0 185.1v5.8c0 41.5 17.2 81.2 47.6 109.5z"/></svg></span></p>
<p><span class="fa-svg"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M272 304h-96C78.8 304 0 382.8 0 480c0 17.67 14.33 32 32 32h384c17.67 0 32-14.33 32-32C448 382.8 369.2 304 272 304zM48.99 464C56.89 400.9 110.8 352 176 352h96c65.16 0 119.1 48.95 127 112H48.99zM224 256c70.69 0 128-57.31 128-128c0-70.69-57.31-128-128-128S96 57.31 96 128C96 198.7 153.3 256 224 256zM224 48c44.11 0 80 35.89 80 80c0 44.11-35.89 80-80 80S144 172.1 144 128C144 83.89 179.9 48 224 48z"/></svg></span></p>
<p><span class="fa-svg"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 6.2.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --><path d="M448 48V384C385 407 366 416 329 416C266 416 242 384 179 384C159 384 143 388 128 392V328C143 324 159 320 179 320C242 320 266 352 329 352C349 352 364 349 384 343V135C364 141 349 144 329 144C266 144 242 112 179 112C128 112 104 133 64 141V448C64 466 50 480 32 480S0 466 0 448V64C0 46 14 32 32 32S64 46 64 64V77C104 69 128 48 179 48C242 48 266 80 329 80C366 80 385 71 448 48Z"/></svg></span></p>
<p><i class="fas fa-heart">Some text here.</i></p>

View File

@@ -0,0 +1,3 @@
# Summary
- [Font Awesome](./fa.md)

View File

@@ -0,0 +1,9 @@
# Chapter 1
<i id="example1" class="fas fa-heart extra-class"></i>
<i class="fa fa-user"></i>
<i class="fab fa-font-awesome"></i>
<i class="fas fa-heart">Some text here.</i>

View File

@@ -0,0 +1,2 @@
[book]
title = "header_links"

View File

@@ -0,0 +1,10 @@
<h1 id="header-links"><a class="header" href="#header-links">Header Links</a></h1>
<h2 id="foobar"><a class="header" href="#foobar">Foo^bar</a></h2>
<h3 id=""><a class="header" href="#"></a></h3>
<h4 id="-1"><a class="header" href="#-1"></a></h4>
<h2 id="hï"><a class="header" href="#hï"></a></h2>
<h2 id="repeat"><a class="header" href="#repeat">Repeat</a></h2>
<h2 id="repeat-1"><a class="header" href="#repeat-1">Repeat</a></h2>
<h2 id="repeat-2"><a class="header" href="#repeat-2">Repeat</a></h2>
<h2 id="repeat-1"><a class="header" href="#repeat-1">Repeat 1</a></h2>
<h2 id="with-emphasis-bold-bold_emphasis-code-escaped-html-link-httpsexamplecom"><a class="header" href="#with-emphasis-bold-bold_emphasis-code-escaped-html-link-httpsexamplecom"><!--comment--> With <em>emphasis</em> <strong>bold</strong> <strong><em>bold_emphasis</em></strong> <code>code</code> &lt;escaped&gt; <span>html</span> <a href="https://example.com/link">link</a> <a href="https://example.com/">https://example.com/</a></a></h2>

View File

@@ -0,0 +1,3 @@
# Summary
- [Header Links](./header_links.md)

View File

@@ -0,0 +1,19 @@
# Header Links
## Foo^bar
###
####
## Hï
## Repeat
## Repeat
## Repeat
## Repeat 1
## <!--comment--> With *emphasis* **bold** **_bold_emphasis_** `code` \<escaped> <span>html</span> [link] <https://example.com/>
[link]: https://example.com/link

View File

@@ -0,0 +1,5 @@
[book]
title = "hidelines"
[output.html.code.hidelines]
python = "~"

View File

@@ -0,0 +1,23 @@
<h1 id="hide-lines"><a class="header" href="#hide-lines">Hide Lines</a></h1>
<pre><code class="language-python"><span class="boring">hidden()
</span>nothidden():
<span class="boring"> hidden()
</span><span class="boring"> hidden()
</span> nothidden()
</code></pre>
<pre><code class="language-python hidelines=!!!"><span class="boring">hidden()
</span>nothidden():
<span class="boring"> hidden()
</span><span class="boring"> hidden()
</span> nothidden()
</code></pre>
<pre><pre class="playground"><code class="language-rust"><span class="boring">#![allow(unused)]
</span>#![allow(something)]
<span class="boring">fn main() {
</span><span class="boring">
</span>#hidden();
<span class="boring">hidden();
</span># not_hidden();
#[not_hidden]
not_hidden();
<span class="boring">}</span></code></pre></pre>

View File

@@ -0,0 +1,3 @@
# Summary
- [Hide Lines](./hide-lines.md)

View File

@@ -0,0 +1,27 @@
# Hide Lines
```python
~hidden()
nothidden():
~ hidden()
~hidden()
nothidden()
```
```python,hidelines=!!!
!!!hidden()
nothidden():
!!! hidden()
!!!hidden()
nothidden()
```
```rust
#![allow(something)]
#
#hidden();
# hidden();
## not_hidden();
#[not_hidden]
not_hidden();
```

View File

@@ -0,0 +1,2 @@
[book]
title = "html_blocks"

View File

@@ -0,0 +1,6 @@
<ul>
<li>
<p>List</p>
<!-- Comment in list -->
</li>
</ul>

View File

@@ -0,0 +1,3 @@
# Summary
- [Comment in list](./comment-in-list.md)

View File

@@ -0,0 +1,3 @@
* List
<!-- Comment in list -->