Skip to content

Commit d428f65

Browse files
committed
Create renderer
1 parent 057547e commit d428f65

File tree

11 files changed

+843
-6
lines changed

11 files changed

+843
-6
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,19 @@ description = "Plugins for a mdbook translation workflow based on Gettext."
1111

1212
[dependencies]
1313
anyhow = "1.0.68"
14+
ego-tree = "0.6.2"
15+
markup5ever = "0.11.0"
16+
markup5ever_rcdom = "0.2.0"
1417
mdbook = { version = "0.4.25", default-features = false }
1518
polib = "0.2.0"
1619
pulldown-cmark = { version = "0.9.2", default-features = false }
1720
pulldown-cmark-to-cmark = "10.0.4"
1821
regex = "1.9.4"
22+
scraper = "0.17.1"
1923
semver = "1.0.16"
24+
serde = "1.0.130"
2025
serde_json = "1.0.91"
26+
thiserror = "1.0.30"
2127

2228
[dev-dependencies]
2329
pretty_assertions = "1.3.0"
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
use super::dom_manipulator::NodeManipulator;
2+
use super::error::RendererError;
3+
use super::Component;
4+
use crate::custom_component_renderer::error::Result;
5+
use scraper::{Html, Selector};
6+
use std::fs;
7+
use std::io::{Read, Write};
8+
use std::path::Path;
9+
10+
use std::collections::BTreeMap;
11+
12+
use serde::Deserialize;
13+
14+
#[derive(Deserialize, Debug)]
15+
pub struct LanguagesConfiguration {
16+
pub languages: BTreeMap<String, String>,
17+
}
18+
19+
pub(crate) struct BookDirectoryRenderer {
20+
config: LanguagesConfiguration,
21+
components: Vec<Box<dyn Component>>,
22+
}
23+
24+
impl BookDirectoryRenderer {
25+
pub(crate) fn new(config: LanguagesConfiguration) -> BookDirectoryRenderer {
26+
BookDirectoryRenderer {
27+
config,
28+
components: Vec::new(),
29+
}
30+
}
31+
32+
pub(crate) fn render_book(&mut self, path: &Path) -> Result<()> {
33+
if !path.is_dir() {
34+
return Err(RendererError::InvalidPath(format!(
35+
"{:?} is not a directory",
36+
path
37+
)));
38+
}
39+
self.render_book_directory(path)
40+
}
41+
42+
pub(crate) fn add_component(&mut self, component: Box<dyn Component>) {
43+
self.components.push(component);
44+
}
45+
46+
fn render_components(&mut self, file_content: &str) -> Result<String> {
47+
let mut document = Html::parse_document(file_content);
48+
for custom_component in &mut self.components {
49+
let mut node_ids = Vec::new();
50+
51+
let selector = Selector::parse(&custom_component.identifier())
52+
.map_err(|err| RendererError::InvalidIdentifier(err.to_string()))?;
53+
for node in document.select(&selector) {
54+
node_ids.push(node.id());
55+
}
56+
let tree = &mut document.tree;
57+
for id in node_ids {
58+
let dom_manipulator = NodeManipulator::new(tree, id);
59+
custom_component.render(dom_manipulator, &self.config)?;
60+
}
61+
}
62+
Ok(document.html())
63+
}
64+
65+
fn process_file(&mut self, path: &Path) -> Result<()> {
66+
if path.extension().unwrap_or_default() != "html" {
67+
return Ok(());
68+
}
69+
let mut file_content = String::new();
70+
{
71+
let mut file = fs::File::open(path)?;
72+
file.read_to_string(&mut file_content)?;
73+
}
74+
let output_html = self.render_components(&file_content)?;
75+
let mut file = fs::File::create(path)?;
76+
file.write_all(output_html.as_bytes())?;
77+
Ok(())
78+
}
79+
80+
fn render_book_directory(&mut self, path: &Path) -> Result<()> {
81+
for entry in path.read_dir()? {
82+
let entry = entry?;
83+
let path = entry.path();
84+
if path.is_dir() {
85+
self.render_book_directory(&path)?;
86+
} else {
87+
self.process_file(&path)?;
88+
}
89+
}
90+
Ok(())
91+
}
92+
}
93+
94+
#[cfg(test)]
95+
mod tests {
96+
#[test]
97+
fn test_render_book() {
98+
use super::*;
99+
use crate::custom_components::test_component::TestComponent;
100+
use std::fs::File;
101+
use tempfile::tempdir;
102+
103+
const INITIAL_HTML: &[u8] = b"<html><body><TestComponent name=\"test\"><div>TOREMOVE</div></TestComponent></body></html>";
104+
105+
let dir = tempdir().unwrap();
106+
std::fs::write(dir.path().join("test.html"), INITIAL_HTML)
107+
.expect("Failed to write initial html");
108+
109+
let mut languages = BTreeMap::new();
110+
languages.insert(String::from("en"), String::from("English"));
111+
languages.insert(String::from("fr"), String::from("French"));
112+
let mock_config = LanguagesConfiguration { languages };
113+
114+
let mut renderer = BookDirectoryRenderer::new(mock_config);
115+
let test_component = Box::new(TestComponent::new());
116+
renderer.add_component(test_component);
117+
renderer
118+
.render_book(dir.path())
119+
.expect("Failed to render book");
120+
121+
let mut output = String::new();
122+
let mut file = File::open(dir.path().join("test.html")).unwrap();
123+
file.read_to_string(&mut output).unwrap();
124+
125+
const EXPECTED: &str = "<html><head></head><body><div name=\"test\"><ul><li>en: English</li><li>fr: French</li></ul></div></body></html>";
126+
127+
let output_document = Html::parse_document(&output);
128+
let expected_document = Html::parse_document(EXPECTED);
129+
assert_eq!(output_document, expected_document);
130+
}
131+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use super::dom_manipulator::NodeManipulator;
2+
use crate::custom_component_renderer::error::Result;
3+
use crate::LanguagesConfiguration;
4+
5+
pub trait Component {
6+
/// Returns the identifier of the component. ie `<i18n-helpers />` -> `i18n-helpers`
7+
fn identifier(&self) -> String;
8+
9+
fn render(&mut self, node: NodeManipulator<'_>, config: &LanguagesConfiguration) -> Result<()>;
10+
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
use super::error::RendererError;
2+
use crate::custom_component_renderer::error::Result;
3+
use ego_tree::{NodeId, NodeMut, NodeRef, Tree};
4+
use markup5ever::{namespace_url, ns, Attribute, LocalName, QualName};
5+
use scraper::node::{Element, Text};
6+
use scraper::Node;
7+
8+
pub struct NodeManipulator<'a> {
9+
tree: &'a mut Tree<Node>,
10+
node_id: NodeId,
11+
append_children_builder: Option<AppendChildrenBuilder>,
12+
}
13+
14+
impl<'a> NodeManipulator<'a> {
15+
pub fn new(tree: &'a mut Tree<Node>, node_id: NodeId) -> NodeManipulator<'a> {
16+
NodeManipulator {
17+
tree,
18+
node_id,
19+
append_children_builder: None,
20+
}
21+
}
22+
23+
fn get_node(&'a self) -> Result<NodeRef<'a, Node>> {
24+
self.tree.get(self.node_id).ok_or_else(|| {
25+
RendererError::InternalError(format!("Node with id {:?} does not exist", self.node_id))
26+
})
27+
}
28+
29+
fn get_node_mut(&mut self) -> Result<NodeMut<'_, Node>> {
30+
self.tree.get_mut(self.node_id).ok_or_else(|| {
31+
RendererError::InternalError(format!("Node with id {:?} does not exist", self.node_id))
32+
})
33+
}
34+
35+
pub fn get_attribute(&self, attr: &str) -> Result<Option<&str>> {
36+
let node = self.get_node()?;
37+
match node.value() {
38+
Node::Element(element) => {
39+
let attr = element.attr(attr);
40+
Ok(attr)
41+
}
42+
_ => Err(RendererError::InternalError(format!(
43+
"Node with id {:?} is not an element",
44+
self.node_id
45+
))),
46+
}
47+
}
48+
49+
/// Appends a child node and returns the id of the inserted node id.
50+
pub fn append_child(&'a mut self, new_node: Node) -> Result<Self> {
51+
let mut node = self.get_node_mut()?;
52+
let inserted_id = node.append(new_node).id();
53+
Ok(Self::new(self.tree, inserted_id))
54+
}
55+
56+
pub fn append_children(&mut self) -> &mut AppendChildrenBuilder {
57+
let builder = AppendChildrenBuilder::new(None);
58+
self.append_children_builder = Some(builder);
59+
self.append_children_builder.as_mut().unwrap()
60+
}
61+
62+
fn build_children_impl(&mut self, builder: AppendChildrenBuilder) -> Result<()> {
63+
let mut node = self.get_node_mut()?;
64+
let mut builder_to_nodeid = Vec::new();
65+
for mut child in builder.children {
66+
let inserted_id = node.append(child.value.take().unwrap()).id();
67+
builder_to_nodeid.push((child, inserted_id));
68+
}
69+
let original_node_id = self.node_id;
70+
for (child, inserted_id) in builder_to_nodeid {
71+
self.node_id = inserted_id;
72+
self.build_children_impl(child)?;
73+
}
74+
self.node_id = original_node_id;
75+
Ok(())
76+
}
77+
78+
pub fn build_children(&'a mut self) -> Result<()> {
79+
let builder = self.append_children_builder.take().ok_or_else(|| {
80+
RendererError::InternalError(String::from(
81+
"Missing children builder in build_children call",
82+
))
83+
})?;
84+
self.build_children_impl(builder)
85+
}
86+
87+
pub fn replace_with(mut self, new_node: Node) -> Result<Self> {
88+
let mut node = self.get_node_mut()?;
89+
let inserted_id = node.insert_after(new_node).id();
90+
node.detach();
91+
let Self { tree, .. } = self;
92+
Ok(Self::new(tree, inserted_id))
93+
}
94+
}
95+
96+
pub struct AppendChildrenBuilder {
97+
children: Vec<AppendChildrenBuilder>,
98+
value: Option<Node>,
99+
}
100+
101+
impl AppendChildrenBuilder {
102+
fn new(value: Option<Node>) -> Self {
103+
Self {
104+
value,
105+
children: Vec::new(),
106+
}
107+
}
108+
109+
pub fn append_child(&mut self, new_node: Node) -> &mut AppendChildrenBuilder {
110+
let new_builder = Self::new(Some(new_node));
111+
self.children.push(new_builder);
112+
self.children.last_mut().unwrap()
113+
}
114+
}
115+
116+
pub struct NodeAttribute {
117+
pub name: String,
118+
pub value: String,
119+
}
120+
121+
impl NodeAttribute {
122+
pub fn new(name: &str, value: &str) -> Self {
123+
Self {
124+
name: String::from(name),
125+
value: String::from(value),
126+
}
127+
}
128+
}
129+
130+
impl From<NodeAttribute> for Attribute {
131+
fn from(value: NodeAttribute) -> Self {
132+
Attribute {
133+
name: QualName::new(None, ns!(), LocalName::from(value.name)),
134+
value: value.value.into(),
135+
}
136+
}
137+
}
138+
139+
pub fn create_node(name: &str, attributes: Vec<NodeAttribute>) -> Node {
140+
Node::Element(Element::new(
141+
QualName::new(None, ns!(), LocalName::from(name)),
142+
attributes.into_iter().map(Into::into).collect(),
143+
))
144+
}
145+
146+
pub fn create_text_node(text: &str) -> Node {
147+
Node::Text(Text { text: text.into() })
148+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use thiserror::Error;
2+
3+
#[derive(Error, Debug)]
4+
pub enum RendererError {
5+
#[error("IO Error: {0}")]
6+
IoError(#[from] std::io::Error),
7+
#[error("Invalid Identifier: {0}")]
8+
InvalidIdentifier(String),
9+
#[error("Invalid path: {0}")]
10+
InvalidPath(String),
11+
#[error("Internal Error: {0}")]
12+
InternalError(String),
13+
#[error("Component Rendering Error: {0}")]
14+
ComponentError(String),
15+
}
16+
17+
pub type Result<T> = std::result::Result<T, RendererError>;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
mod book_directory_renderer;
2+
mod component_trait;
3+
mod dom_manipulator;
4+
mod error;
5+
6+
pub(crate) use book_directory_renderer::*;
7+
pub(crate) use component_trait::*;
8+
pub(crate) use dom_manipulator::*;
9+
pub(crate) use error::*;

src/custom_components/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#[cfg(test)]
2+
pub(crate) mod test_component;
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
use crate::{
2+
create_node, create_text_node, Component, LanguagesConfiguration, NodeAttribute,
3+
NodeManipulator, RendererError, Result,
4+
};
5+
6+
pub struct TestComponent {}
7+
8+
impl TestComponent {
9+
pub fn new() -> TestComponent {
10+
TestComponent {}
11+
}
12+
}
13+
14+
impl Component for TestComponent {
15+
fn identifier(&self) -> String {
16+
String::from("TestComponent")
17+
}
18+
19+
fn render(&mut self, node: NodeManipulator<'_>, config: &LanguagesConfiguration) -> Result<()> {
20+
let name = node
21+
.get_attribute("name")?
22+
.ok_or_else(|| RendererError::ComponentError(String::from("Missing attribute name")))?;
23+
let new_node = create_node("div", vec![NodeAttribute::new("name", name)]);
24+
let mut new_node = node.replace_with(new_node)?;
25+
let mut ul = new_node.append_child(create_node("ul", Vec::new()))?;
26+
27+
let append_builder = ul.append_children();
28+
for (identifier, language) in &config.languages {
29+
let li = append_builder.append_child(create_node("li", Vec::new()));
30+
li.append_child(create_text_node(&format!("{}: {}", identifier, language)));
31+
}
32+
ul.build_children()?;
33+
Ok(())
34+
}
35+
}

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ fn is_nontranslatable_codeblock_group(events: &[(usize, Event)]) -> bool {
339339
// Heuristic to check whether the codeblock nether has a
340340
// literal string nor a line comment. We may actually
341341
// want to use a lexer here to make this more robust.
342-
!codeblock_text.contains("\"") && !codeblock_text.contains("//")
342+
!codeblock_text.contains('"') && !codeblock_text.contains("//")
343343
}
344344
_ => false,
345345
}

0 commit comments

Comments
 (0)