Skip to content

Commit

Permalink
Create renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
sakex committed Sep 17, 2023
1 parent 057547e commit d428f65
Show file tree
Hide file tree
Showing 11 changed files with 843 additions and 6 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
131 changes: 131 additions & 0 deletions src/custom_component_renderer/book_directory_renderer.rs
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);
}
}
10 changes: 10 additions & 0 deletions src/custom_component_renderer/component_trait.rs
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<()>;
}
148 changes: 148 additions & 0 deletions src/custom_component_renderer/dom_manipulator.rs
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() })
}
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>;
9 changes: 9 additions & 0 deletions src/custom_component_renderer/mod.rs
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::*;
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;
35 changes: 35 additions & 0 deletions src/custom_components/test_component.rs
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(())
}
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ fn is_nontranslatable_codeblock_group(events: &[(usize, Event)]) -> bool {
// Heuristic to check whether the codeblock nether has a
// literal string nor a line comment. We may actually
// want to use a lexer here to make this more robust.
!codeblock_text.contains("\"") && !codeblock_text.contains("//")
!codeblock_text.contains('"') && !codeblock_text.contains("//")
}
_ => false,
}
Expand Down
Loading

0 comments on commit d428f65

Please sign in to comment.