Skip to content

Commit

Permalink
Create renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
sakex committed Sep 15, 2023
1 parent 057547e commit 1018859
Show file tree
Hide file tree
Showing 11 changed files with 871 additions and 5 deletions.
461 changes: 456 additions & 5 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,19 @@ description = "Plugins for a mdbook translation workflow based on Gettext."

[dependencies]
anyhow = "1.0.68"
ego-tree = "0.6.2"
markup5ever = "0.11.0"
markup5ever_rcdom = "0.2.0"
mdbook = { version = "0.4.25", default-features = false }
polib = "0.2.0"
pulldown-cmark = { version = "0.9.2", default-features = false }
pulldown-cmark-to-cmark = "10.0.4"
regex = "1.9.4"
scraper = "0.17.1"
semver = "1.0.16"
serde = "1.0.130"
serde_json = "1.0.91"
thiserror = "1.0.30"

[dev-dependencies]
pretty_assertions = "1.3.0"
Expand Down
140 changes: 140 additions & 0 deletions src/custom_component_renderer/book_directory_renderer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
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);
}
}
14 changes: 14 additions & 0 deletions src/custom_component_renderer/component_trait.rs
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<()>;
}
147 changes: 147 additions & 0 deletions src/custom_component_renderer/dom_manipulator.rs
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() })
}
17 changes: 17 additions & 0 deletions src/custom_component_renderer/error.rs
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>;
8 changes: 8 additions & 0 deletions src/custom_component_renderer/languages_configuration.rs
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>,
}
7 changes: 7 additions & 0 deletions src/custom_component_renderer/mod.rs
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;
2 changes: 2 additions & 0 deletions src/custom_components/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#[cfg(test)]
pub(crate) mod test_component;
Loading

0 comments on commit 1018859

Please sign in to comment.