Skip to content

Commit 49625cf

Browse files
committed
Create renderer
1 parent 057547e commit 49625cf

File tree

11 files changed

+872
-5
lines changed

11 files changed

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

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;

0 commit comments

Comments
 (0)