Skip to content

Commit 8216b43

Browse files
sakexmgeisler
authored andcommitted
Create mdbook-tera-backend renderer (google#80)
Create renderer Co-authored-by: Martin Geisler <[email protected]>
1 parent cdb6d41 commit 8216b43

File tree

11 files changed

+860
-68
lines changed

11 files changed

+860
-68
lines changed

Cargo.lock

Lines changed: 367 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[workspace]
2-
members = ["i18n-helpers"]
2+
members = ["i18n-helpers", "mdbook-tera-backend"]
33
default-members = ["i18n-helpers"]
44
resolver = "2"

README.md

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
This repository contains the following crates that provide extensions and
99
infrastructure for [mdbook](https://github.com/rust-lang/mdBook/):
1010

11-
- [mdbook-i18n-helpers](i18n-helpers/README.md): Gettext translation support for
12-
[mdbook](https://github.com/rust-lang/mdBook/)
11+
- [mdbook-i18n-helpers](./i18n-helpers/README.md): Gettext translation support
12+
for [mdbook](https://github.com/rust-lang/mdBook/)
13+
- [mdbook-tera-backend](./mdbook-tera-backend/README.md): Tera templates
14+
extension for [mdbook](https://github.com/rust-lang/mdBook/)'s HTML renderer.
1315

1416
## Showcases
1517

@@ -39,11 +41,17 @@ cargo install mdbook-i18n-helpers
3941
Please see [USAGE](i18n-helpers/USAGE.md) for how to translate your
4042
[mdbook](https://github.com/rust-lang/mdBook/) project.
4143

42-
## Changelog
43-
4444
Please see the [i18n-helpers/CHANGELOG](CHANGELOG) for details on the changes in
4545
each release.
4646

47+
### `mdbook-tera-backend`
48+
49+
Run
50+
51+
```shell
52+
$ cargo install mdbook-tera-backend
53+
```
54+
4755
## Contact
4856

4957
For questions or comments, please contact

i18n-helpers/src/directives.rs

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
use regex::Regex;
2+
use std::sync::OnceLock;
3+
4+
#[derive(Debug, PartialEq)]
5+
pub enum Directive {
6+
Skip,
7+
TranslatorComment(String),
8+
}
9+
10+
pub fn find(html: &str) -> Option<Directive> {
11+
static RE: OnceLock<Regex> = OnceLock::new();
12+
let re = RE.get_or_init(|| {
13+
let pattern = r"(?x)
14+
<!-{2,}\s* # the opening of the comment
15+
(?:i18n|mdbook-xgettext) # the prefix
16+
\s*[:-] # delimit between prefix and command
17+
(?<command>.*[^-]) # the command part of the prefix
18+
-{2,}> # the closing of the comment
19+
";
20+
Regex::new(pattern).expect("well-formed regex")
21+
});
22+
23+
let captures = re.captures(html.trim())?;
24+
25+
let command = captures["command"].trim();
26+
match command.split(is_delimiter).next() {
27+
Some("skip") => Some(Directive::Skip),
28+
Some("comment") => {
29+
let start_of_comment_offset = std::cmp::min(
30+
command.find("comment").unwrap() + "comment".len() + 1,
31+
command.len(),
32+
);
33+
Some(Directive::TranslatorComment(
34+
command[start_of_comment_offset..].trim().into(),
35+
))
36+
}
37+
_ => None,
38+
}
39+
}
40+
41+
fn is_delimiter(c: char) -> bool {
42+
c.is_whitespace() || c == ':' || c == '-'
43+
}
44+
45+
#[cfg(test)]
46+
mod tests {
47+
use super::*;
48+
49+
#[test]
50+
fn test_is_comment_skip_directive_simple() {
51+
assert!(matches!(find("<!-- i18n:skip -->"), Some(Directive::Skip)));
52+
}
53+
54+
#[test]
55+
fn test_is_comment_skip_directive_tolerates_spaces() {
56+
assert!(matches!(find("<!-- i18n: skip -->"), Some(Directive::Skip)));
57+
}
58+
59+
#[test]
60+
fn test_is_comment_skip_directive_tolerates_dashes() {
61+
assert!(matches!(
62+
find("<!--- i18n:skip ---->"),
63+
Some(Directive::Skip)
64+
));
65+
}
66+
67+
#[test]
68+
fn test_is_comment_skip_directive_needs_skip() {
69+
assert!(find("<!-- i18n: foo -->").is_none());
70+
}
71+
72+
#[test]
73+
fn test_is_comment_skip_directive_needs_to_be_a_comment() {
74+
assert!(find("<div>i18: skip</div>").is_none());
75+
}
76+
77+
#[test]
78+
fn test_different_prefix() {
79+
assert!(matches!(
80+
find("<!-- mdbook-xgettext:skip -->"),
81+
Some(Directive::Skip)
82+
));
83+
}
84+
85+
#[test]
86+
fn test_translator_comment() {
87+
assert!(match find("<!-- i18n-comment: hello world! -->") {
88+
Some(Directive::TranslatorComment(s)) => {
89+
s == "hello world!"
90+
}
91+
_ => false,
92+
});
93+
}
94+
95+
#[test]
96+
fn test_translator_empty_comment_does_nothing() {
97+
assert!(match find("<!-- i18n-comment -->") {
98+
Some(Directive::TranslatorComment(s)) => {
99+
s.is_empty()
100+
}
101+
_ => false,
102+
});
103+
}
104+
}

i18n-helpers/src/lib.rs

Lines changed: 28 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@
2626
use polib::catalog::Catalog;
2727
use pulldown_cmark::{CodeBlockKind, Event, LinkType, Tag};
2828
use pulldown_cmark_to_cmark::{cmark_resume_with_options, Options, State};
29-
use regex::Regex;
3029
use std::sync::OnceLock;
3130
use syntect::easy::ScopeRangeIterator;
3231
use syntect::parsing::{ParseState, Scope, ScopeStack, SyntaxSet};
3332

33+
pub mod directives;
3434
pub mod gettext;
3535
pub mod normalize;
3636

@@ -287,22 +287,34 @@ pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Vec<Group<'a>> {
287287
}
288288
}
289289

290-
// An HTML comment directive to skip the next translation
291-
// group.
292-
Event::Html(s) if is_comment_skip_directive(s) => {
293-
// If in the middle of translation, finish it.
294-
if let State::Translate(_) = state {
295-
let mut next_groups;
296-
(next_groups, ctx) = state.into_groups(idx, events, ctx);
297-
groups.append(&mut next_groups);
298-
299-
// Restart translation: subtle but should be
300-
// needed to handle the skipping of the rest of
301-
// the inlined content.
302-
state = State::Translate(idx);
290+
Event::Html(s) => {
291+
match directives::find(s) {
292+
Some(directives::Directive::Skip) => {
293+
// If in the middle of translation, finish it.
294+
if let State::Translate(_) = state {
295+
let mut next_groups;
296+
(next_groups, ctx) = state.into_groups(idx, events, ctx);
297+
groups.append(&mut next_groups);
298+
299+
// Restart translation: subtle but should be
300+
// needed to handle the skipping of the rest of
301+
// the inlined content.
302+
state = State::Translate(idx);
303+
}
304+
305+
ctx.skip_next_group = true;
306+
}
307+
// Otherwise, treat as a skipping group.
308+
_ => {
309+
if let State::Translate(_) = state {
310+
let mut next_groups;
311+
(next_groups, ctx) = state.into_groups(idx, events, ctx);
312+
groups.append(&mut next_groups);
313+
314+
state = State::Skip(idx);
315+
}
316+
}
303317
}
304-
305-
ctx.skip_next_group = true;
306318
}
307319

308320
// All other block-level events start or continue a
@@ -327,15 +339,6 @@ pub fn group_events<'a>(events: &'a [(usize, Event<'a>)]) -> Vec<Group<'a>> {
327339
groups
328340
}
329341

330-
/// Check whether the HTML is a directive to skip the next translation group.
331-
fn is_comment_skip_directive(html: &str) -> bool {
332-
static RE: OnceLock<Regex> = OnceLock::new();
333-
334-
let re =
335-
RE.get_or_init(|| Regex::new(r"<!-{2,}\s*mdbook-xgettext\s*:\s*skip\s*-{2,}>").unwrap());
336-
re.is_match(html.trim())
337-
}
338-
339342
/// Returns true if the events appear to be a codeblock.
340343
fn is_codeblock_group(events: &[(usize, Event)]) -> bool {
341344
matches!(
@@ -1294,45 +1297,7 @@ $$
12941297
}
12951298

12961299
#[test]
1297-
fn test_is_comment_skip_directive_simple() {
1298-
assert_eq!(
1299-
is_comment_skip_directive("<!-- mdbook-xgettext:skip -->"),
1300-
true
1301-
);
1302-
}
13031300

1304-
#[test]
1305-
fn test_is_comment_skip_directive_tolerates_spaces() {
1306-
assert_eq!(
1307-
is_comment_skip_directive("<!-- mdbook-xgettext: skip -->"),
1308-
true
1309-
);
1310-
}
1311-
1312-
#[test]
1313-
fn test_is_comment_skip_directive_tolerates_dashes() {
1314-
assert_eq!(
1315-
is_comment_skip_directive("<!--- mdbook-xgettext:skip ---->"),
1316-
true
1317-
);
1318-
}
1319-
1320-
#[test]
1321-
fn test_is_comment_skip_directive_needs_skip() {
1322-
assert_eq!(
1323-
is_comment_skip_directive("<!-- mdbook-xgettext: foo -->"),
1324-
false
1325-
);
1326-
}
1327-
#[test]
1328-
fn test_is_comment_skip_directive_needs_to_be_a_comment() {
1329-
assert_eq!(
1330-
is_comment_skip_directive("<div>mdbook-xgettext: skip</div>"),
1331-
false
1332-
);
1333-
}
1334-
1335-
#[test]
13361301
fn extract_messages_skip_simple() {
13371302
assert_extract_messages(
13381303
r#"<!-- mdbook-xgettext:skip -->

mdbook-tera-backend/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "mdbook-tera-backend"
3+
version = "0.0.1"
4+
authors = ["Martin Geisler <[email protected]>", "Alexandre Senges <[email protected]>"]
5+
categories = ["template-engine"]
6+
edition = "2021"
7+
keywords = ["mdbook", "tera", "renderer", "template"]
8+
license = "Apache-2.0"
9+
repository = "https://github.com/google/mdbook-i18n-helpers"
10+
description = "Plugin to extend mdbook with Tera templates and custom HTML components."
11+
12+
[dependencies]
13+
anyhow = "1.0.75"
14+
mdbook = { version = "0.4.25", default-features = false }
15+
serde = "1.0"
16+
serde_json = "1.0.91"
17+
tera = "1.19.1"
18+
19+
[dev-dependencies]
20+
tempdir = "0.3.7"

mdbook-tera-backend/README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Tera backend extension for `mdbook`
2+
3+
[![Visit crates.io](https://img.shields.io/crates/v/mdbook-i18n-helpers?style=flat-square)](https://crates.io/crates/mdbook-tera-backend)
4+
[![Build workflow](https://img.shields.io/github/actions/workflow/status/google/mdbook-i18n-helpers/test.yml?style=flat-square)](https://github.com/google/mdbook-i18n-helpers/actions/workflows/test.yml?query=branch%3Amain)
5+
[![GitHub contributors](https://img.shields.io/github/contributors/google/mdbook-i18n-helpers?style=flat-square)](https://github.com/google/mdbook-i18n-helpers/graphs/contributors)
6+
[![GitHub stars](https://img.shields.io/github/stars/google/mdbook-i18n-helpers?style=flat-square)](https://github.com/google/mdbook-i18n-helpers/stargazers)
7+
8+
This `mdbook` backend makes it possible to use
9+
[tera](https://github.com/Keats/tera) templates and expand the capabilities of
10+
your books. It works on top of the default HTML backend.
11+
12+
## Installation
13+
14+
Run
15+
16+
```shell
17+
$ cargo install mdbook-tera-backend
18+
```
19+
20+
## Usage
21+
22+
### Configuring the backend
23+
24+
To enable the backend, simply add `[output.tera-backend]` to your `book.toml`,
25+
and configure the place where youre templates will live. For instance
26+
`theme/templates`:
27+
28+
```toml
29+
[output.html] # You must still enable the html backend.
30+
[output.tera-backend]
31+
template_dir = "theme/templates"
32+
```
33+
34+
### Creating templates
35+
36+
Create your template files in the same directory as your book.
37+
38+
```html
39+
<!-- ./theme/templates/hello_world.html -->
40+
<div>
41+
Hello world!
42+
</div>
43+
```
44+
45+
### Using templates in `index.hbs`
46+
47+
Since the HTML renderer will first render Handlebars templates, we need to tell
48+
it to ignore Tera templates using `{{{{raw}}}}` blocks:
49+
50+
```html
51+
{{{{raw}}}}
52+
{% set current_language = ctx.config.book.language %}
53+
<p>Current language: {{ current_language }}</p>
54+
{% include "hello_world.html" %}
55+
{{{{/raw}}}}
56+
```
57+
58+
Includes names are based on the file name and not the whole file path.
59+
60+
### Tera documentation
61+
62+
Find out all you can do with Tera templates
63+
[here](https://keats.github.io/tera/docs/).
64+
65+
## Changelog
66+
67+
Please see [CHANGELOG](../CHANGELOG.md) for details on the changes in each
68+
release.
69+
70+
## Contact
71+
72+
For questions or comments, please contact
73+
[Martin Geisler](mailto:[email protected]) or
74+
[Alexandre Senges](mailto:[email protected]) or start a
75+
[discussion](https://github.com/google/mdbook-i18n-helpers/discussions). We
76+
would love to hear from you.
77+
78+
---
79+
80+
This is not an officially supported Google product.

0 commit comments

Comments
 (0)