-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
872 additions
and
5 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
141 changes: 141 additions & 0 deletions
141
src/custom_component_renderer/book_directory_renderer.rs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
use super::dom_manipulator::NodeManipulator; | ||
use super::{error::RendererError, languages_configuration::LanguagesConfiguration, Component}; | ||
use crate::custom_component_renderer::error::Result; | ||
use scraper::{Html, Selector}; | ||
use std::io::Write; | ||
use std::{fs, io::Read, path::Path}; | ||
|
||
pub(crate) struct BoookDirectoryRenderer { | ||
config: LanguagesConfiguration, | ||
// Saves on reallocations | ||
file_buffer_cache: String, | ||
components: Vec<Box<dyn Component>>, | ||
} | ||
|
||
impl BoookDirectoryRenderer { | ||
pub(crate) fn new(config: LanguagesConfiguration) -> BoookDirectoryRenderer { | ||
BoookDirectoryRenderer { | ||
config, | ||
file_buffer_cache: String::new(), | ||
components: Vec::new(), | ||
} | ||
} | ||
|
||
pub(crate) fn render_book(&mut self, path: &Path) -> Result<()> { | ||
if !path.is_dir() { | ||
return Err(RendererError::InvalidPath(format!( | ||
"{:?} is not a directory", | ||
path | ||
))); | ||
} | ||
self.render_book_directory(path) | ||
} | ||
|
||
pub(crate) fn add_component(&mut self, component: Box<dyn Component>) { | ||
self.components.push(component); | ||
} | ||
|
||
fn render_components(&mut self) -> Result<()> { | ||
let mut document = Html::parse_document(&self.file_buffer_cache); | ||
for custom_component in &mut self.components { | ||
let mut node_ids = Vec::new(); | ||
|
||
let selector = Selector::parse(&custom_component.identifier()) | ||
.map_err(|err| RendererError::InvalidIdentifier(err.to_string()))?; | ||
for node in document.select(&selector) { | ||
node_ids.push(node.id()); | ||
} | ||
let tree = &mut document.tree; | ||
for id in node_ids { | ||
let dom_manipulator = NodeManipulator::new(tree, id); | ||
custom_component.render(dom_manipulator, &self.config)?; | ||
} | ||
} | ||
self.file_buffer_cache.clear(); | ||
self.file_buffer_cache = document.html(); | ||
Ok(()) | ||
} | ||
|
||
fn process_file(&mut self, path: &Path) -> Result<()> { | ||
if path.extension().unwrap_or_default() != "html" { | ||
return Ok(()); | ||
} | ||
self.file_buffer_cache.clear(); | ||
{ | ||
let mut file = fs::File::open(path)?; | ||
file.read_to_string(&mut self.file_buffer_cache)?; | ||
} | ||
self.render_components()?; | ||
let mut file = fs::OpenOptions::new() | ||
.write(true) | ||
.truncate(true) | ||
.open(path)?; | ||
file.write_all(self.file_buffer_cache.as_bytes())?; | ||
Ok(()) | ||
} | ||
|
||
fn render_book_directory(&mut self, path: &Path) -> Result<()> { | ||
for entry in path.read_dir()? { | ||
let entry = entry?; | ||
let path = entry.path(); | ||
if path.is_dir() { | ||
self.render_book_directory(&path)?; | ||
} else { | ||
self.process_file(&path)?; | ||
} | ||
} | ||
Ok(()) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use std::collections::BTreeMap; | ||
|
||
#[test] | ||
fn test_render_book() { | ||
use super::*; | ||
use crate::custom_components::test_component::TestComponent; | ||
use std::fs::File; | ||
use std::io::Write; | ||
use tempfile::tempdir; | ||
|
||
let dir = tempdir().unwrap(); | ||
{ | ||
let mut file = File::create(dir.path().join("test.html")).unwrap(); | ||
file.write_all( | ||
b" | ||
<html> | ||
<body> | ||
<TestComponent name=\"test\"> | ||
<div>TOREMOVE</div> | ||
</TestComponent> | ||
</body> | ||
</html>", | ||
) | ||
.unwrap(); | ||
} | ||
|
||
let mut languages = BTreeMap::new(); | ||
languages.insert(String::from("en"), String::from("English")); | ||
languages.insert(String::from("fr"), String::from("French")); | ||
let mock_config = LanguagesConfiguration { languages }; | ||
|
||
let mut renderer = BoookDirectoryRenderer::new(mock_config); | ||
let test_component = Box::new(TestComponent::new()); | ||
renderer.add_component(test_component); | ||
renderer | ||
.render_book(dir.path()) | ||
.expect("Failed to render book"); | ||
|
||
let mut output = String::new(); | ||
let mut file = File::open(dir.path().join("test.html")).unwrap(); | ||
file.read_to_string(&mut output).unwrap(); | ||
|
||
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>"; | ||
|
||
let output_document = Html::parse_document(&output); | ||
let expected_document = Html::parse_document(EXPECTED); | ||
assert_eq!(output_document, expected_document); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
use super::dom_manipulator::NodeManipulator; | ||
use crate::custom_component_renderer::error::Result; | ||
use crate::custom_component_renderer::languages_configuration::LanguagesConfiguration; | ||
|
||
pub trait Component { | ||
/// Returns the identifier of the component. ie `<i18n-helpers />` -> `i18n-helpers` | ||
fn identifier(&self) -> String; | ||
|
||
fn render<'a>( | ||
&mut self, | ||
node: NodeManipulator<'a>, | ||
config: &LanguagesConfiguration, | ||
) -> Result<()>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
use super::error::RendererError; | ||
use crate::custom_component_renderer::error::Result; | ||
use ego_tree::{NodeId, NodeMut, NodeRef, Tree}; | ||
use markup5ever::namespace_url; | ||
use markup5ever::{ns, Attribute, LocalName, QualName}; | ||
use scraper::node::{Element, Text}; | ||
use scraper::Node; | ||
|
||
pub struct NodeManipulator<'a> { | ||
tree: &'a mut Tree<Node>, | ||
node_id: NodeId, | ||
append_children_builder: Option<AppendChildrenBuilder>, | ||
} | ||
|
||
impl<'a> NodeManipulator<'a> { | ||
pub fn new(tree: &'a mut Tree<Node>, node_id: NodeId) -> NodeManipulator<'a> { | ||
NodeManipulator { | ||
tree, | ||
node_id, | ||
append_children_builder: None, | ||
} | ||
} | ||
|
||
fn get_node(&'a self) -> Result<NodeRef<'a, Node>> { | ||
Ok(self.tree.get(self.node_id).ok_or_else(|| { | ||
RendererError::InternalError(format!("Node with id {:?} does not exist", self.node_id)) | ||
})?) | ||
} | ||
|
||
fn get_node_mut(&mut self) -> Result<NodeMut<'_, Node>> { | ||
Ok(self.tree.get_mut(self.node_id).ok_or_else(|| { | ||
RendererError::InternalError(format!("Node with id {:?} does not exist", self.node_id)) | ||
})?) | ||
} | ||
|
||
pub fn get_attribute(&self, attr: &str) -> Result<Option<&str>> { | ||
let node = self.get_node()?; | ||
match node.value() { | ||
Node::Element(element) => { | ||
let attr = element.attr(attr); | ||
Ok(attr) | ||
} | ||
_ => Err(RendererError::InternalError(format!( | ||
"Node with id {:?} is not an element", | ||
self.node_id | ||
))), | ||
} | ||
} | ||
|
||
/// Appends a child node and returns the id of the inserted node id. | ||
pub fn append_child(&'a mut self, new_node: Node) -> Result<Self> { | ||
let mut node = self.get_node_mut()?; | ||
let inserted_id = node.append(new_node).id(); | ||
Ok(Self::new(self.tree, inserted_id)) | ||
} | ||
|
||
pub fn append_children(&mut self) -> &mut AppendChildrenBuilder { | ||
let builder = AppendChildrenBuilder::new(None); | ||
self.append_children_builder = Some(builder); | ||
self.append_children_builder.as_mut().unwrap() | ||
} | ||
|
||
fn build_children_impl(&mut self, builder: AppendChildrenBuilder) -> Result<()> { | ||
let mut node = self.get_node_mut()?; | ||
let mut builder_to_nodeid = Vec::new(); | ||
for mut child in builder.children { | ||
let inserted_id = node.append(child.value.take().unwrap()).id(); | ||
builder_to_nodeid.push((child, inserted_id)); | ||
} | ||
let original_node_id = self.node_id; | ||
for (child, inserted_id) in builder_to_nodeid { | ||
self.node_id = inserted_id; | ||
self.build_children_impl(child)?; | ||
} | ||
self.node_id = original_node_id; | ||
Ok(()) | ||
} | ||
|
||
pub fn build_children(&'a mut self) -> Result<()> { | ||
let builder = self.append_children_builder.take().ok_or_else(|| { | ||
RendererError::InternalError(format!("Missing children builder in build_children call")) | ||
})?; | ||
self.build_children_impl(builder) | ||
} | ||
|
||
pub fn replace_with(mut self, new_node: Node) -> Result<Self> { | ||
let mut node = self.get_node_mut()?; | ||
let inserted_id = node.insert_after(new_node).id(); | ||
node.detach(); | ||
let Self { tree, .. } = self; | ||
Ok(Self::new(tree, inserted_id)) | ||
} | ||
} | ||
|
||
pub struct AppendChildrenBuilder { | ||
children: Vec<AppendChildrenBuilder>, | ||
value: Option<Node>, | ||
} | ||
|
||
impl AppendChildrenBuilder { | ||
fn new(value: Option<Node>) -> Self { | ||
Self { | ||
value, | ||
children: Vec::new(), | ||
} | ||
} | ||
|
||
pub fn append_child(&mut self, new_node: Node) -> &mut AppendChildrenBuilder { | ||
let new_builder = Self::new(Some(new_node)); | ||
self.children.push(new_builder); | ||
self.children.last_mut().unwrap() | ||
} | ||
} | ||
|
||
pub struct NodeAttribute { | ||
pub name: String, | ||
pub value: String, | ||
} | ||
|
||
impl NodeAttribute { | ||
pub fn new(name: &str, value: &str) -> Self { | ||
Self { | ||
name: String::from(name), | ||
value: String::from(value), | ||
} | ||
} | ||
} | ||
|
||
impl From<NodeAttribute> for Attribute { | ||
fn from(value: NodeAttribute) -> Self { | ||
Attribute { | ||
name: QualName::new(None, ns!(), LocalName::from(value.name)), | ||
value: value.value.into(), | ||
} | ||
} | ||
} | ||
|
||
pub fn create_node(name: &str, attributes: Vec<NodeAttribute>) -> Node { | ||
Node::Element(Element::new( | ||
QualName::new(None, ns!(), LocalName::from(name)), | ||
attributes.into_iter().map(Into::into).collect(), | ||
)) | ||
} | ||
|
||
pub fn create_text_node(text: &str) -> Node { | ||
Node::Text(Text { text: text.into() }) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
use thiserror::Error; | ||
|
||
#[derive(Error, Debug)] | ||
pub enum RendererError { | ||
#[error("IO Error: {0}")] | ||
IoError(#[from] std::io::Error), | ||
#[error("Invalid Identifier: {0}")] | ||
InvalidIdentifier(String), | ||
#[error("Invalid path: {0}")] | ||
InvalidPath(String), | ||
#[error("Internal Error: {0}")] | ||
InternalError(String), | ||
#[error("Component Rendering Error: {0}")] | ||
ComponentError(String), | ||
} | ||
|
||
pub type Result<T> = std::result::Result<T, RendererError>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
use std::collections::BTreeMap; | ||
|
||
use serde::Deserialize; | ||
|
||
#[derive(Deserialize, Debug)] | ||
pub struct LanguagesConfiguration { | ||
pub languages: BTreeMap<String, String>, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
pub(crate) mod book_directory_renderer; | ||
mod component_trait; | ||
pub(crate) mod dom_manipulator; | ||
pub(crate) mod error; | ||
pub(crate) mod languages_configuration; | ||
|
||
pub(crate) use component_trait::Component; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
#[cfg(test)] | ||
pub(crate) mod test_component; |
Oops, something went wrong.