Skip to content

Commit

Permalink
Create mdbook-tera-backend renderer (google#80)
Browse files Browse the repository at this point in the history
Create renderer

Co-authored-by: Martin Geisler <martin@geisler.net>
  • Loading branch information
2 people authored and dyoo committed Nov 9, 2023
1 parent cdb6d41 commit 8216b43
Show file tree
Hide file tree
Showing 11 changed files with 860 additions and 68 deletions.
367 changes: 367 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[workspace]
members = ["i18n-helpers"]
members = ["i18n-helpers", "mdbook-tera-backend"]
default-members = ["i18n-helpers"]
resolver = "2"
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
This repository contains the following crates that provide extensions and
infrastructure for [mdbook](https://github.com/rust-lang/mdBook/):

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

## Showcases

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

## Changelog

Please see the [i18n-helpers/CHANGELOG](CHANGELOG) for details on the changes in
each release.

### `mdbook-tera-backend`

Run

```shell
$ cargo install mdbook-tera-backend
```

## Contact

For questions or comments, please contact
Expand Down
104 changes: 104 additions & 0 deletions i18n-helpers/src/directives.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use regex::Regex;
use std::sync::OnceLock;

#[derive(Debug, PartialEq)]
pub enum Directive {
Skip,
TranslatorComment(String),
}

pub fn find(html: &str) -> Option<Directive> {
static RE: OnceLock<Regex> = OnceLock::new();
let re = RE.get_or_init(|| {
let pattern = r"(?x)
<!-{2,}\s* # the opening of the comment
(?:i18n|mdbook-xgettext) # the prefix
\s*[:-] # delimit between prefix and command
(?<command>.*[^-]) # the command part of the prefix
-{2,}> # the closing of the comment
";
Regex::new(pattern).expect("well-formed regex")
});

let captures = re.captures(html.trim())?;

let command = captures["command"].trim();
match command.split(is_delimiter).next() {
Some("skip") => Some(Directive::Skip),
Some("comment") => {
let start_of_comment_offset = std::cmp::min(
command.find("comment").unwrap() + "comment".len() + 1,
command.len(),
);
Some(Directive::TranslatorComment(
command[start_of_comment_offset..].trim().into(),
))
}
_ => None,
}
}

fn is_delimiter(c: char) -> bool {
c.is_whitespace() || c == ':' || c == '-'
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_is_comment_skip_directive_simple() {
assert!(matches!(find("<!-- i18n:skip -->"), Some(Directive::Skip)));
}

#[test]
fn test_is_comment_skip_directive_tolerates_spaces() {
assert!(matches!(find("<!-- i18n: skip -->"), Some(Directive::Skip)));
}

#[test]
fn test_is_comment_skip_directive_tolerates_dashes() {
assert!(matches!(
find("<!--- i18n:skip ---->"),
Some(Directive::Skip)
));
}

#[test]
fn test_is_comment_skip_directive_needs_skip() {
assert!(find("<!-- i18n: foo -->").is_none());
}

#[test]
fn test_is_comment_skip_directive_needs_to_be_a_comment() {
assert!(find("<div>i18: skip</div>").is_none());
}

#[test]
fn test_different_prefix() {
assert!(matches!(
find("<!-- mdbook-xgettext:skip -->"),
Some(Directive::Skip)
));
}

#[test]
fn test_translator_comment() {
assert!(match find("<!-- i18n-comment: hello world! -->") {
Some(Directive::TranslatorComment(s)) => {
s == "hello world!"
}
_ => false,
});
}

#[test]
fn test_translator_empty_comment_does_nothing() {
assert!(match find("<!-- i18n-comment -->") {
Some(Directive::TranslatorComment(s)) => {
s.is_empty()
}
_ => false,
});
}
}
91 changes: 28 additions & 63 deletions i18n-helpers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,11 @@
use polib::catalog::Catalog;
use pulldown_cmark::{CodeBlockKind, Event, LinkType, Tag};
use pulldown_cmark_to_cmark::{cmark_resume_with_options, Options, State};
use regex::Regex;
use std::sync::OnceLock;
use syntect::easy::ScopeRangeIterator;
use syntect::parsing::{ParseState, Scope, ScopeStack, SyntaxSet};

pub mod directives;
pub mod gettext;
pub mod normalize;

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

// An HTML comment directive to skip the next translation
// group.
Event::Html(s) if is_comment_skip_directive(s) => {
// If in the middle of translation, finish it.
if let State::Translate(_) = state {
let mut next_groups;
(next_groups, ctx) = state.into_groups(idx, events, ctx);
groups.append(&mut next_groups);

// Restart translation: subtle but should be
// needed to handle the skipping of the rest of
// the inlined content.
state = State::Translate(idx);
Event::Html(s) => {
match directives::find(s) {
Some(directives::Directive::Skip) => {
// If in the middle of translation, finish it.
if let State::Translate(_) = state {
let mut next_groups;
(next_groups, ctx) = state.into_groups(idx, events, ctx);
groups.append(&mut next_groups);

// Restart translation: subtle but should be
// needed to handle the skipping of the rest of
// the inlined content.
state = State::Translate(idx);
}

ctx.skip_next_group = true;
}
// Otherwise, treat as a skipping group.
_ => {
if let State::Translate(_) = state {
let mut next_groups;
(next_groups, ctx) = state.into_groups(idx, events, ctx);
groups.append(&mut next_groups);

state = State::Skip(idx);
}
}
}

ctx.skip_next_group = true;
}

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

/// Check whether the HTML is a directive to skip the next translation group.
fn is_comment_skip_directive(html: &str) -> bool {
static RE: OnceLock<Regex> = OnceLock::new();

let re =
RE.get_or_init(|| Regex::new(r"<!-{2,}\s*mdbook-xgettext\s*:\s*skip\s*-{2,}>").unwrap());
re.is_match(html.trim())
}

/// Returns true if the events appear to be a codeblock.
fn is_codeblock_group(events: &[(usize, Event)]) -> bool {
matches!(
Expand Down Expand Up @@ -1294,45 +1297,7 @@ $$
}

#[test]
fn test_is_comment_skip_directive_simple() {
assert_eq!(
is_comment_skip_directive("<!-- mdbook-xgettext:skip -->"),
true
);
}

#[test]
fn test_is_comment_skip_directive_tolerates_spaces() {
assert_eq!(
is_comment_skip_directive("<!-- mdbook-xgettext: skip -->"),
true
);
}

#[test]
fn test_is_comment_skip_directive_tolerates_dashes() {
assert_eq!(
is_comment_skip_directive("<!--- mdbook-xgettext:skip ---->"),
true
);
}

#[test]
fn test_is_comment_skip_directive_needs_skip() {
assert_eq!(
is_comment_skip_directive("<!-- mdbook-xgettext: foo -->"),
false
);
}
#[test]
fn test_is_comment_skip_directive_needs_to_be_a_comment() {
assert_eq!(
is_comment_skip_directive("<div>mdbook-xgettext: skip</div>"),
false
);
}

#[test]
fn extract_messages_skip_simple() {
assert_extract_messages(
r#"<!-- mdbook-xgettext:skip -->
Expand Down
20 changes: 20 additions & 0 deletions mdbook-tera-backend/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "mdbook-tera-backend"
version = "0.0.1"
authors = ["Martin Geisler <mgeisler@google.com>", "Alexandre Senges <asenges@google.com>"]
categories = ["template-engine"]
edition = "2021"
keywords = ["mdbook", "tera", "renderer", "template"]
license = "Apache-2.0"
repository = "https://github.com/google/mdbook-i18n-helpers"
description = "Plugin to extend mdbook with Tera templates and custom HTML components."

[dependencies]
anyhow = "1.0.75"
mdbook = { version = "0.4.25", default-features = false }
serde = "1.0"
serde_json = "1.0.91"
tera = "1.19.1"

[dev-dependencies]
tempdir = "0.3.7"
80 changes: 80 additions & 0 deletions mdbook-tera-backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Tera backend extension for `mdbook`

[![Visit crates.io](https://img.shields.io/crates/v/mdbook-i18n-helpers?style=flat-square)](https://crates.io/crates/mdbook-tera-backend)
[![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)
[![GitHub contributors](https://img.shields.io/github/contributors/google/mdbook-i18n-helpers?style=flat-square)](https://github.com/google/mdbook-i18n-helpers/graphs/contributors)
[![GitHub stars](https://img.shields.io/github/stars/google/mdbook-i18n-helpers?style=flat-square)](https://github.com/google/mdbook-i18n-helpers/stargazers)

This `mdbook` backend makes it possible to use
[tera](https://github.com/Keats/tera) templates and expand the capabilities of
your books. It works on top of the default HTML backend.

## Installation

Run

```shell
$ cargo install mdbook-tera-backend
```

## Usage

### Configuring the backend

To enable the backend, simply add `[output.tera-backend]` to your `book.toml`,
and configure the place where youre templates will live. For instance
`theme/templates`:

```toml
[output.html] # You must still enable the html backend.
[output.tera-backend]
template_dir = "theme/templates"
```

### Creating templates

Create your template files in the same directory as your book.

```html
<!-- ./theme/templates/hello_world.html -->
<div>
Hello world!
</div>
```

### Using templates in `index.hbs`

Since the HTML renderer will first render Handlebars templates, we need to tell
it to ignore Tera templates using `{{{{raw}}}}` blocks:

```html
{{{{raw}}}}
{% set current_language = ctx.config.book.language %}
<p>Current language: {{ current_language }}</p>
{% include "hello_world.html" %}
{{{{/raw}}}}
```

Includes names are based on the file name and not the whole file path.

### Tera documentation

Find out all you can do with Tera templates
[here](https://keats.github.io/tera/docs/).

## Changelog

Please see [CHANGELOG](../CHANGELOG.md) for details on the changes in each
release.

## Contact

For questions or comments, please contact
[Martin Geisler](mailto:mgeisler@google.com) or
[Alexandre Senges](mailto:asenges@google.come) or start a
[discussion](https://github.com/google/mdbook-i18n-helpers/discussions). We
would love to hear from you.

---

This is not an officially supported Google product.
Loading

0 comments on commit 8216b43

Please sign in to comment.