-
Notifications
You must be signed in to change notification settings - Fork 30
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
843 additions
and
6 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
131 changes: 131 additions & 0 deletions
131
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,131 @@ | ||
use super::dom_manipulator::NodeManipulator; | ||
use super::error::RendererError; | ||
use super::Component; | ||
use crate::custom_component_renderer::error::Result; | ||
use scraper::{Html, Selector}; | ||
use std::fs; | ||
use std::io::{Read, Write}; | ||
use std::path::Path; | ||
|
||
use std::collections::BTreeMap; | ||
|
||
use serde::Deserialize; | ||
|
||
#[derive(Deserialize, Debug)] | ||
pub struct LanguagesConfiguration { | ||
pub languages: BTreeMap<String, String>, | ||
} | ||
|
||
pub(crate) struct BookDirectoryRenderer { | ||
config: LanguagesConfiguration, | ||
components: Vec<Box<dyn Component>>, | ||
} | ||
|
||
impl BookDirectoryRenderer { | ||
pub(crate) fn new(config: LanguagesConfiguration) -> BookDirectoryRenderer { | ||
BookDirectoryRenderer { | ||
config, | ||
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, file_content: &str) -> Result<String> { | ||
let mut document = Html::parse_document(file_content); | ||
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)?; | ||
} | ||
} | ||
Ok(document.html()) | ||
} | ||
|
||
fn process_file(&mut self, path: &Path) -> Result<()> { | ||
if path.extension().unwrap_or_default() != "html" { | ||
return Ok(()); | ||
} | ||
let mut file_content = String::new(); | ||
{ | ||
let mut file = fs::File::open(path)?; | ||
file.read_to_string(&mut file_content)?; | ||
} | ||
let output_html = self.render_components(&file_content)?; | ||
let mut file = fs::File::create(path)?; | ||
file.write_all(output_html.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 { | ||
#[test] | ||
fn test_render_book() { | ||
use super::*; | ||
use crate::custom_components::test_component::TestComponent; | ||
use std::fs::File; | ||
use tempfile::tempdir; | ||
|
||
const INITIAL_HTML: &[u8] = b"<html><body><TestComponent name=\"test\"><div>TOREMOVE</div></TestComponent></body></html>"; | ||
|
||
let dir = tempdir().unwrap(); | ||
std::fs::write(dir.path().join("test.html"), INITIAL_HTML) | ||
.expect("Failed to write initial html"); | ||
|
||
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 = BookDirectoryRenderer::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><div name=\"test\"><ul><li>en: English</li><li>fr: French</li></ul></div></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,10 @@ | ||
use super::dom_manipulator::NodeManipulator; | ||
use crate::custom_component_renderer::error::Result; | ||
use crate::LanguagesConfiguration; | ||
|
||
pub trait Component { | ||
/// Returns the identifier of the component. ie `<i18n-helpers />` -> `i18n-helpers` | ||
fn identifier(&self) -> String; | ||
|
||
fn render(&mut self, node: NodeManipulator<'_>, 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,148 @@ | ||
use super::error::RendererError; | ||
use crate::custom_component_renderer::error::Result; | ||
use ego_tree::{NodeId, NodeMut, NodeRef, Tree}; | ||
use markup5ever::{namespace_url, 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>> { | ||
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>> { | ||
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(String::from( | ||
"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,9 @@ | ||
mod book_directory_renderer; | ||
mod component_trait; | ||
mod dom_manipulator; | ||
mod error; | ||
|
||
pub(crate) use book_directory_renderer::*; | ||
pub(crate) use component_trait::*; | ||
pub(crate) use dom_manipulator::*; | ||
pub(crate) use error::*; |
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; |
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,35 @@ | ||
use crate::{ | ||
create_node, create_text_node, Component, LanguagesConfiguration, NodeAttribute, | ||
NodeManipulator, RendererError, Result, | ||
}; | ||
|
||
pub struct TestComponent {} | ||
|
||
impl TestComponent { | ||
pub fn new() -> TestComponent { | ||
TestComponent {} | ||
} | ||
} | ||
|
||
impl Component for TestComponent { | ||
fn identifier(&self) -> String { | ||
String::from("TestComponent") | ||
} | ||
|
||
fn render(&mut self, node: NodeManipulator<'_>, config: &LanguagesConfiguration) -> Result<()> { | ||
let name = node | ||
.get_attribute("name")? | ||
.ok_or_else(|| RendererError::ComponentError(String::from("Missing attribute name")))?; | ||
let new_node = create_node("div", vec![NodeAttribute::new("name", name)]); | ||
let mut new_node = node.replace_with(new_node)?; | ||
let mut ul = new_node.append_child(create_node("ul", Vec::new()))?; | ||
|
||
let append_builder = ul.append_children(); | ||
for (identifier, language) in &config.languages { | ||
let li = append_builder.append_child(create_node("li", Vec::new())); | ||
li.append_child(create_text_node(&format!("{}: {}", identifier, language))); | ||
} | ||
ul.build_children()?; | ||
Ok(()) | ||
} | ||
} |
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
Oops, something went wrong.