======
File path: ./Cargo.toml
[package]
name = "metatron"
version = "0.0.1"
edition = "2021"
authors = ["Evgeny Igumnov <igumnovnsk@gmail.com>"]
repository = "https://github.com/igumnoff/metatron"
documentation = "https://docs.rs/metatron"
license-file = "LICENSE"
keywords = []
categories = []
readme = "README.md"
description = "Metatron library"
[dependencies]
anyhow = "1.0.75"
thiserror = "1.0.44"
shiva = {path = "../shiva/lib"}
serde_yaml = "0.9.34"
serde_json = "1.0.115"
bytes = "1.5.0"
[dev-dependencies]
env_logger = "0.10.0"
======
File path: ./data/report-data.json
{
"rows": [
{
"name": "John",
"age": 25,
"salary": 50000
},
{
"name": "Jane",
"age": 30,
"salary": 60000
},
{
"name": "Jim",
"age": 35,
"salary": 70000
}
],
"params": {
"company_name": "ABCDFG Ltd",
"company_address": "1234 Elm St, Springfield, IL 62701",
"average_age": 30,
"average_salary": 60000
}
}
======
File path: ./data/report-template.yaml
title:
- header: $P{company_name} Employee Report
level: 1
page_header:
- text: Confidential information
size: 7
column_header:
- name: Name
width: 20
- name: Age
width: 5
- name: Salary
width: 5
row:
- value: $F(name)
- value: $F(age)
- value: $F(salary)
column_footer:
- value:
- value: $P(average_age)
- value: $P(average_salary)
page_footer:
- text: "Tel: +1 123 456 789"
size: 7
summary:
- paragraph:
- text: "Company address: $P{company_address}"
size: 10
======
File path: ./src/core.rs
use bytes::Bytes;
use std::any::Any;
use std::collections::HashMap;
use std::fmt::Debug;
use thiserror::Error;
pub struct Document {
pub elements: Vec<Box<dyn Element>>,
pub page_width: f32,
pub page_height: f32,
pub left_page_indent: f32,
pub right_page_indent: f32,
pub top_page_indent: f32,
pub bottom_page_indent: f32,
}
impl Document {
pub fn new(elements: &Vec<Box<dyn Element>>) -> anyhow::Result<Document> {
Ok(Document {
elements: (&elements).to_vec(),
page_width: 210.0,
page_height: 297.0,
left_page_indent: 10.0,
right_page_indent: 10.0,
top_page_indent: 10.0,
bottom_page_indent: 10.0,
})
}
}
pub trait TransformerTrait {
fn parse(document: &Bytes, images: &HashMap<String, Bytes>) -> anyhow::Result<Document>;
fn generate(document: &Document) -> anyhow::Result<(Bytes, HashMap<String, Bytes>)>;
}
pub enum ParserError {
Common,
}
pub enum GeneratorError {
Common,
}
pub enum CastingError {
Common,
}
pub trait Element: CloneableElement + Debug {
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
fn element_type(&self) -> ElementType;
fn paragraph_as_ref(&self) -> anyhow::Result<&ParagraphElement> {
Ok(self
.as_any()
.downcast_ref::<ParagraphElement>()
.ok_or(CastingError::Common)?)
}
fn paragraph_as_mut(&mut self) -> anyhow::Result<&mut ParagraphElement> {
Ok(self
.as_any_mut()
.downcast_mut::<ParagraphElement>()
.ok_or(CastingError::Common)?)
}
fn header_as_ref(&self) -> anyhow::Result<&HeaderElement> {
Ok(self
.as_any()
.downcast_ref::<HeaderElement>()
.ok_or(CastingError::Common)?)
}
fn header_as_mut(&mut self) -> anyhow::Result<&mut HeaderElement> {
Ok(self
.as_any_mut()
.downcast_mut::<HeaderElement>()
.ok_or(CastingError::Common)?)
}
fn text_as_ref(&self) -> anyhow::Result<&TextElement> {
Ok(self
.as_any()
.downcast_ref::<TextElement>()
.ok_or(CastingError::Common)?)
}
fn text_as_mut(&mut self) -> anyhow::Result<&mut TextElement> {
Ok(self
.as_any_mut()
.downcast_mut::<TextElement>()
.ok_or(CastingError::Common)?)
}
fn table_as_ref(&self) -> anyhow::Result<&TableElement> {
Ok(self
.as_any()
.downcast_ref::<TableElement>()
.ok_or(CastingError::Common)?)
}
fn table_as_mut(&mut self) -> anyhow::Result<&mut TableElement> {
Ok(self
.as_any_mut()
.downcast_mut::<TableElement>()
.ok_or(CastingError::Common)?)
}
fn table_header_as_ref(&self) -> anyhow::Result<&TableHeaderElement> {
Ok(self
.as_any()
.downcast_ref::<TableHeaderElement>()
.ok_or(CastingError::Common)?)
}
fn table_header_as_mut(&mut self) -> anyhow::Result<&mut TableHeaderElement> {
Ok(self
.as_any_mut()
.downcast_mut::<TableHeaderElement>()
.ok_or(CastingError::Common)?)
}
fn table_row_as_ref(&self) -> anyhow::Result<&TableRowElement> {
Ok(self
.as_any()
.downcast_ref::<TableRowElement>()
.ok_or(CastingError::Common)?)
}
fn table_row_as_mut(&mut self) -> anyhow::Result<&mut TableRowElement> {
Ok(self
.as_any_mut()
.downcast_mut::<TableRowElement>()
.ok_or(CastingError::Common)?)
}
fn table_cell_as_ref(&self) -> anyhow::Result<&TableCellElement> {
Ok(self
.as_any()
.downcast_ref::<TableCellElement>()
.ok_or(CastingError::Common)?)
}
fn table_cell_as_mut(&mut self) -> anyhow::Result<&mut TableCellElement> {
Ok(self
.as_any_mut()
.downcast_mut::<TableCellElement>()
.ok_or(CastingError::Common)?)
}
fn list_as_ref(&self) -> anyhow::Result<&ListElement> {
Ok(self
.as_any()
.downcast_ref::<ListElement>()
.ok_or(CastingError::Common)?)
}
fn list_as_mut(&mut self) -> anyhow::Result<&mut ListElement> {
Ok(self
.as_any_mut()
.downcast_mut::<ListElement>()
.ok_or(CastingError::Common)?)
}
fn list_item_as_ref(&self) -> anyhow::Result<&ListItemElement> {
Ok(self
.as_any()
.downcast_ref::<ListItemElement>()
.ok_or(CastingError::Common)?)
}
fn list_item_as_mut(&mut self) -> anyhow::Result<&mut ListItemElement> {
Ok(self
.as_any_mut()
.downcast_mut::<ListItemElement>()
.ok_or(CastingError::Common)?)
}
fn image_as_ref(&self) -> anyhow::Result<&ImageElement> {
Ok(self
.as_any()
.downcast_ref::<ImageElement>()
.ok_or(CastingError::Common)?)
}
fn image_as_mut(&mut self) -> anyhow::Result<&mut ImageElement> {
Ok(self
.as_any_mut()
.downcast_mut::<ImageElement>()
.ok_or(CastingError::Common)?)
}
fn hyperlink_as_ref(&self) -> anyhow::Result<&HyperlinkElement> {
Ok(self
.as_any()
.downcast_ref::<HyperlinkElement>()
.ok_or(CastingError::Common)?)
}
fn hyperlink_as_mut(&mut self) -> anyhow::Result<&mut HyperlinkElement> {
Ok(self
.as_any_mut()
.downcast_mut::<HyperlinkElement>()
.ok_or(CastingError::Common)?)
}
}
pub trait CloneableElement {
fn clone_box(&self) -> Box<dyn Element>;
}
impl<T> CloneableElement for T
where
T: 'static + Element + Clone,
{
fn clone_box(&self) -> Box<dyn Element> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn Element> {
fn clone(&self) -> Box<dyn Element> {
self.clone_box()
}
}
pub enum ElementType {
Text,
Paragraph,
Image,
Hyperlink,
Header,
Table,
TableHeader,
TableRow,
TableCell,
List,
ListItem,
PageBreak,
TableOfContents,
}
pub struct TextElement {
pub text: String,
pub size: u8,
}
impl TextElement {
pub fn new(text: &str, size: u8) -> anyhow::Result<Box<dyn Element>> {
Ok(Box::new(TextElement {
text: text.to_string(),
size,
}))
}
}
impl Element for TextElement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn element_type(&self) -> ElementType {
ElementType::Text
}
}
pub struct HeaderElement {
pub level: u8,
pub text: String,
pub children: Vec<Box<dyn Element>>,
}
impl HeaderElement {
pub fn new(text: &str, level: u8) -> anyhow::Result<Box<dyn Element>> {
Ok(Box::new(HeaderElement {
level,
text: text.to_string(),
children: vec![],
}))
}
}
impl Element for HeaderElement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn element_type(&self) -> ElementType {
ElementType::Header
}
}
pub struct ParagraphElement {
pub elements: Vec<Box<dyn Element>>,
}
impl ParagraphElement {
pub fn new(elements: &Vec<Box<dyn Element>>) -> anyhow::Result<Box<dyn Element>> {
Ok(Box::new(ParagraphElement {
elements: (&elements).to_vec(),
}))
}
}
impl Element for ParagraphElement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn element_type(&self) -> ElementType {
ElementType::Paragraph
}
}
pub struct TableElement {
pub rows: Vec<TableRowElement>,
pub headers: Vec<TableHeaderElement>,
}
impl TableElement {
pub fn new(
headers: &Vec<TableHeaderElement>,
rows: &Vec<TableRowElement>,
) -> anyhow::Result<Box<dyn Element>> {
Ok(Box::new(TableElement {
rows: rows.to_vec(),
headers: headers.to_vec(),
}))
}
}
impl Element for TableElement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn element_type(&self) -> ElementType {
ElementType::Table
}
}
pub struct TableHeaderElement {
pub element: Box<dyn Element>,
pub width: f32,
}
impl TableHeaderElement {
pub fn new(element: &Box<dyn Element>) -> anyhow::Result<TableHeaderElement> {
Ok(TableHeaderElement {
element: element.clone(),
width: 20.0,
})
}
}
impl Element for TableHeaderElement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn element_type(&self) -> ElementType {
ElementType::TableHeader
}
}
pub struct TableRowElement {
pub cells: Vec<TableCellElement>,
}
impl TableRowElement {
pub fn new(cells: &Vec<TableCellElement>) -> anyhow::Result<TableRowElement> {
Ok(TableRowElement {
cells: cells.to_vec(),
})
}
}
impl Element for TableRowElement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn element_type(&self) -> ElementType {
ElementType::TableRow
}
}
pub struct TableCellElement {
pub element: Box<dyn Element>,
}
impl TableCellElement {
pub fn new(element: &Box<dyn Element>) -> anyhow::Result<TableCellElement> {
Ok(TableCellElement {
element: element.clone(),
})
}
}
impl Element for TableCellElement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn element_type(&self) -> ElementType {
ElementType::TableCell
}
}
pub struct ListElement {
pub elements: Vec<ListItemElement>,
pub numbered: bool,
}
impl ListElement {
pub fn new(
elements: &Vec<ListItemElement>,
numbered: bool,
) -> anyhow::Result<Box<dyn Element>> {
Ok(Box::new(ListElement {
elements: elements.to_vec(),
numbered,
}))
}
}
impl Element for ListElement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn element_type(&self) -> ElementType {
ElementType::List
}
}
pub struct ListItemElement {
pub element: Box<dyn Element>,
}
impl ListItemElement {
pub fn new(element: &Box<dyn Element>) -> anyhow::Result<ListItemElement> {
Ok(ListItemElement {
element: element.clone(),
})
}
}
impl Element for ListItemElement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn element_type(&self) -> ElementType {
ElementType::ListItem
}
}
pub enum ImageType {
Png,
Jpeg,
}
pub struct ImageElement {
pub bytes: Bytes,
pub title: String,
pub alt: String,
pub image_type: ImageType,
}
impl ImageElement {
pub fn new(
bytes: &Bytes,
title: &str,
alt: &str,
image_type: ImageType,
) -> anyhow::Result<Box<dyn Element>> {
Ok(Box::new(ImageElement {
bytes: bytes.clone(),
title: title.to_string(),
alt: alt.to_string(),
image_type,
}))
}
}
impl Element for ImageElement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn element_type(&self) -> ElementType {
ElementType::Image
}
}
pub struct HyperlinkElement {
pub title: String,
pub url: String,
pub alt: String,
}
impl HyperlinkElement {
pub fn new(title: &str, url: &str, alt: &str) -> anyhow::Result<Box<dyn Element>> {
Ok(Box::new(HyperlinkElement {
title: title.to_string(),
url: url.to_string(),
alt: alt.to_string(),
}))
}
}
impl Element for HyperlinkElement {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn element_type(&self) -> ElementType {
ElementType::Hyperlink
}
}
======
File path: ./src/lib.rs
use std::collections::HashMap;
use bytes::Bytes;
use serde_yaml::Value as YValue;
use serde_json::Value as JValue;
use shiva::core::{Document, Element, HeaderElement, TableCellElement, TableElement, TableHeaderElement, TableRowElement, TextElement};
use thiserror::Error;
use crate::ReportError::Common;
pub struct Report;
impl Report {
pub fn generate(template: &str, data: &str,  _images: &HashMap<String, Bytes>) -> anyhow::Result<Document> {
let template: YValue = serde_yaml::from_str(template)?;
let data: JValue = serde_json::from_str(data)?;
let params = data["params"].as_object().ok_or(Common("Missing 'params' in data".to_string()))?;
let mut elements: Vec<Box<dyn Element>> = vec![];
if let Some(title) = template["title"].as_sequence() {
for header in title {
if let Some(header_text) = header["header"].as_str() {
let mut resolved_text = header_text.to_string();
for (key, value) in params {
resolved_text = resolved_text.replace(&format!("$P{{{}}}", key), value.as_str().unwrap_or(""));
}
let header_element = HeaderElement::new(&resolved_text, header["level"].as_u64().unwrap_or(1) as u8)?;
elements.push(header_element);
}
}
}
if let Some(columns) = template["column_header"].as_sequence() {
let mut headers = Vec::new();
for column in columns {
if let Some(name) = column["name"].as_str() {
let text_element = TextElement::new(name, 8)?; // Assuming default font size for headers
let width = column["width"].as_f64().unwrap_or(20.0) as f32; // Default width if not specified
let mut header_element = TableHeaderElement::new(&text_element)?;
header_element.width = width;
headers.push(header_element);
}
}
let mut rows = Vec::new();
if let Some(data_rows) = data["rows"].as_array() {
for data_row in data_rows {
let mut cells = Vec::new();
if let Some(row_configs) = template["row"].as_sequence() {
for row_config in row_configs {
if let Some(value_key) = row_config["value"].as_str() {
let field_name = value_key.trim_start_matches("$F(").trim_end_matches(")");
if let Some(value) = data_row[field_name].as_str() {
let text_element = TextElement::new(value, 8)?; // Default font size for cells
cells.push(TableCellElement::new(&text_element)?);
}
if let Some(value) = data_row[field_name].as_number() {
let value = value.to_string();
let text_element = TextElement::new(value.as_str(), 8)?; // Default font size for cells
cells.push(TableCellElement::new(&text_element)?);
}
}
}
}
rows.push(TableRowElement::new(&cells)?);
}
}
let table_element = TableElement::new(&headers, &rows)?;
elements.push(table_element);
}
let document = Document::new(&elements)?;
Ok(document)
}
}
pub enum ReportError {
Common(String),
}
mod tests {
use super::*;
use std::collections::HashMap;
use shiva::core::TransformerTrait;
fn test_generate() -> anyhow::Result<()> {
let template_vec = std::fs::read("data/report-template.yaml")?;
let template = std::str::from_utf8(&template_vec).unwrap();
let data_vec = std::fs::read("data/report-data.json")?;
let data = std::str::from_utf8(&data_vec).unwrap();
let images = HashMap::new();
let result = Report::generate(template, data, &images);
assert!(result.is_ok());
let doc = result?;
println!("{:?}", doc);
println!("=========================");
let result = shiva::markdown::Transformer::generate(&doc)?;
let result_str = std::str::from_utf8(&result.0)?;
println!("{}", result_str);
Ok(())
}
}
======
File path: ./src/markdown.rs
use crate::core::*;
use bytes::Bytes;
use regex::Regex;
use std::collections::HashMap;
pub struct Transformer;
impl TransformerTrait for Transformer {
fn parse(document: &Bytes, images: &HashMap<String, Bytes>) -> anyhow::Result<Document>
where
Self: Sized,
{
let document_str = std::str::from_utf8(document)?;
let mut elements: Vec<Box<dyn Element>> = Vec::new();
let lines = document_str.lines();
let mut current_paragraph_elements: Vec<Box<dyn Element>> = Vec::new();
let mut in_table = false;
let mut table_rows: Vec<TableRowElement> = Vec::new();
let mut table_headers: Vec<TableHeaderElement> = Vec::new();
let mut current_list_stack: Vec<Vec<ListItemElement>> = Vec::new();
let mut current_list_type_stack: Vec<bool> = Vec::new();
let img_regex = Regex::new("!\\[.*?\\]\\(.*? \".*?\"\\)").unwrap();
for line in lines {
let indent_level = line.chars().take_while(|c| c.is_whitespace()).count() / 2;
let trimmed_line = line.trim_start();
if trimmed_line.starts_with('-')
|| trimmed_line.starts_with('+')
|| trimmed_line.starts_with('*')
{
let list_item_text = trimmed_line[1..].trim();
let list_item =
ListItemElement::new(&TextElement::new(list_item_text, 8)?).unwrap();
if current_list_stack.len() <= indent_level {
while current_list_stack.len() <= indent_level {
current_list_stack.push(vec![]);
current_list_type_stack.push(false);
}
} else {
while current_list_stack.len() > indent_level + 1 {
let nested_items = current_list_stack.pop().unwrap();
let nested_list = ListElement::new(
&nested_items,
current_list_type_stack.pop().unwrap(),
)?;
if let Some(parent_list) = current_list_stack.last_mut() {
parent_list.push(ListItemElement::new(&nested_list)?);
}
}
}
current_list_stack[indent_level].push(list_item);
while current_list_stack.len() > indent_level + 1 {
let nested_items = current_list_stack.pop().unwrap();
let nested_list =
ListElement::new(&nested_items, current_list_type_stack.pop().unwrap())?;
if let Some(parent_list) = current_list_stack.last_mut() {
parent_list.push(ListItemElement::new(&nested_list)?);
}
}
} else if trimmed_line.chars().next().unwrap_or(' ').is_digit(10)
&& trimmed_line.chars().nth(1).unwrap_or(' ') == '.'
{
let list_item_text = trimmed_line.splitn(2, '.').nth(1).unwrap_or("").trim();
let list_item =
ListItemElement::new(&TextElement::new(list_item_text, 8)?).unwrap();
if current_list_stack.len() <= indent_level {
while current_list_stack.len() <= indent_level {
current_list_stack.push(vec![]);
current_list_type_stack
.push(trimmed_line.chars().next().unwrap().is_digit(10));
}
} else {
while current_list_stack.len() > indent_level + 1 {
let nested_items = current_list_stack.pop().unwrap();
let nested_list = ListElement::new(
&nested_items,
current_list_type_stack.pop().unwrap(),
)?;
if let Some(parent_list) = current_list_stack.last_mut() {
parent_list.push(ListItemElement::new(&nested_list)?);
}
}
}
current_list_stack[indent_level].push(list_item);
while current_list_stack.len() > indent_level + 1 {
let nested_items = current_list_stack.pop().unwrap();
let nested_list =
ListElement::new(&nested_items, current_list_type_stack.pop().unwrap())?;
if let Some(parent_list) = current_list_stack.last_mut() {
parent_list.push(ListItemElement::new(&nested_list)?);
}
}
} else {
if line.starts_with('|') && line.ends_with('|') {
if !in_table {
in_table = true;
table_headers = line
.split('|')
.filter(|x| !x.trim().is_empty() && !x.contains('-'))
.map(|header| {
TableHeaderElement::new(
&TextElement::new(header.trim(), 8).unwrap(),
)
.unwrap()
})
.collect();
} else if line.contains("---") {
continue;
} else if line.starts_with('|') && in_table {
let cells = line
.split('|')
.filter(|x| !x.trim().is_empty())
.map(|cell| {
TableCellElement::new(&TextElement::new(cell.trim(), 8).unwrap())
.unwrap()
})
.collect();
table_rows.push(TableRowElement::new(&cells).unwrap());
}
} else {
if in_table {
elements.push(TableElement::new(&table_headers, &table_rows)?);
table_rows.clear();
table_headers.clear();
in_table = false;
}
if line.starts_with('#') {
let level = line.chars().take_while(|&c| c == '#').count() as u8;
let text = line.trim_start_matches('#').trim();
let header = HeaderElement::new(text, level)?;
if !current_paragraph_elements.is_empty() {
elements.push(ParagraphElement::new(&current_paragraph_elements)?);
current_paragraph_elements.clear();
}
elements.push(header);
} else if !line.trim().is_empty() {
fn parser_text(
text: &str,
current_paragraph_elements: &mut Vec<Box<dyn Element>>,
) -> anyhow::Result<()> {
let mut start = 0;
let mut captures = 0;
let hyperlink_regex = Regex::new("(?:\\[([^\\]]+)\\])?(?:\\(|)(http[s]?:\\/\\/[^\\s\\)]+)(?:\\s\"([^\"]+)\")?\\)?").unwrap();
for cap in hyperlink_regex.captures_iter(text) {
captures += 1;
let (hyperlink_start, end) =
(cap.get(0).unwrap().start(), cap.get(0).unwrap().end());
let markdown = &text[hyperlink_start..end];
if hyperlink_start > start {
parser_text(
&text[start..hyperlink_start],
current_paragraph_elements,
)?;
}
if markdown.starts_with("h") {
current_paragraph_elements
.push(HyperlinkElement::new(markdown, markdown, markdown)?);
} else if markdown.starts_with("[") && markdown.ends_with("\")") {
let start_alt_text = markdown.find("[").unwrap() + 1;
let end_alt_text = markdown.find("]").unwrap();
let start_url_path = markdown.find("(").unwrap() + 1;
let end_url_path = markdown.find(" ").unwrap();
let start_title = markdown.find("\"").unwrap() + 1;
let end_title = markdown.rfind("\"").unwrap();
let alt_text = &markdown[start_alt_text..end_alt_text];
let url = &markdown[start_url_path..end_url_path];
let title = &markdown[start_title..end_title];
current_paragraph_elements
.push(HyperlinkElement::new(alt_text, url, title)?);
} else {
let start_alt_text = markdown.find("[").unwrap() + 1;
let end_alt_text = markdown.find("]").unwrap();
let start_url_path = markdown.find("(").unwrap() + 1;
let alt_text = &markdown[start_alt_text..end_alt_text];
let url = &markdown[start_url_path..markdown.len() - 1];
current_paragraph_elements
.push(HyperlinkElement::new(alt_text, url, alt_text)?);
}
start = end;
}
if captures == 0 {
current_paragraph_elements.push(TextElement::new(text, 8)?);
} else {
if start < text.len() {
parser_text(&text[start..], current_paragraph_elements)?;
}
}
Ok(())
}
let mut captures = 0;
let mut start = 0;
for cap in img_regex.captures_iter(line) {
captures += 1;
let (img_start, img_end) =
(cap.get(0).unwrap().start(), cap.get(0).unwrap().end());
let markdown = &line[img_start..img_end];
let start_alt_text = markdown.find("[").unwrap() + 1;
let end_alt_text = markdown.find("]").unwrap();
let start_file_path = markdown.find("(").unwrap() + 1;
let start_title = markdown.find("\"").unwrap() + 1;
let end_title = markdown.rfind("\"").unwrap();
let alt_text = &markdown[start_alt_text..end_alt_text];
let file_path = &markdown[start_file_path..end_title - 1];
let title = &markdown[start_title..end_title];
if img_start > start {
parser_text(
&line[start..img_start],
&mut current_paragraph_elements,
)?;
}
let image_empty = Bytes::new();
let image_bytes = images.get(file_path).map_or(&image_empty, |x| x);
current_paragraph_elements.push(ImageElement::new(
image_bytes,
&title,
&alt_text,
ImageType::Png,
)?);
start = img_end;
}
if captures == 0 {
parser_text(&line, &mut current_paragraph_elements)?;
} else {
if start < line.len() {
parser_text(&line[start..], &mut current_paragraph_elements)?;
}
}
} else if !current_paragraph_elements.is_empty() {
elements.push(ParagraphElement::new(&current_paragraph_elements)?);
current_paragraph_elements.clear();
}
}
while !current_list_stack.is_empty() {
let items = current_list_stack.pop().unwrap();
let list = ListElement::new(&items, current_list_type_stack.pop().unwrap())?;
if let Some(parent_list) = current_list_stack.last_mut() {
parent_list.push(ListItemElement::new(&list)?);
} else {
elements.push(list);
}
}
}
}
while !current_list_stack.is_empty() {
let items = current_list_stack.pop().unwrap();
let list = ListElement::new(&items, current_list_type_stack.pop().unwrap())?;
if let Some(parent_list) = current_list_stack.last_mut() {
parent_list.push(ListItemElement::new(&list)?);
} else {
elements.push(list);
}
}
if !current_paragraph_elements.is_empty() {
elements.push(ParagraphElement::new(&current_paragraph_elements)?);
}
if in_table {
elements.push(TableElement::new(&table_headers, &table_rows)?);
}
Ok(Document::new(&elements)?)
}
fn generate(document: &Document) -> anyhow::Result<(Bytes, HashMap<String, Bytes>)>
where
Self: Sized,
{
let mut images: HashMap<String, Bytes> = HashMap::new();
let mut image_num: i32 = 0;
let mut markdown = String::new();
fn generate_element(
element: &Box<dyn Element>,
markdown: &mut String,
list_depth: usize,
list_counters: &mut Vec<usize>,
list_types: &mut Vec<bool>,
images: &mut HashMap<String, Bytes>,
image_num: &mut i32,
) -> anyhow::Result<()> {
fn generate_list_item(
element: &ListItemElement,
markdown: &mut String,
list_depth: usize,
list_counters: &mut Vec<usize>,
list_types: &mut Vec<bool>,
images: &mut HashMap<String, Bytes>,
image_num: &mut i32,
) -> anyhow::Result<()> {
let prefix = if *list_types.last().unwrap() {
let counter = list_counters.last_mut().unwrap();
if &element.element.element_type() == &ElementType::Text {
*counter += 1;
}
format!("{}. ", counter)
} else {
"- ".to_string()
};
markdown.push_str(&"  ".repeat(list_depth - 1));
if &element.element.element_type() == &ElementType::Text {
markdown.push_str(&prefix);
}
generate_element(
&element.element,
markdown,
list_depth,
list_counters,
list_types,
images,
image_num,
)?;
if &element.element.element_type() == &ElementType::Text {
markdown.push('\n');
}
Ok(())
}
match element.element_type() {
ElementType::Header => {
let header = element.header_as_ref()?;
markdown.push_str(&"#".repeat(header.level as usize));
markdown.push(' ');
markdown.push_str(&header.text);
markdown.push('\n');
markdown.push('\n');
}
ElementType::Paragraph => {
let paragraph = element.paragraph_as_ref()?;
for child in &paragraph.elements {
generate_element(
child,
markdown,
list_depth,
list_counters,
list_types,
images,
image_num,
)?;
}
markdown.push('\n');
markdown.push('\n');
}
ElementType::List => {
let list = element.list_as_ref()?;
list_counters.push(0);
list_types.push(list.numbered);
for item in &list.elements {
generate_list_item(
&item,
markdown,
list_depth + 1,
list_counters,
list_types,
images,
image_num,
)?;
}
list_counters.pop();
list_types.pop();
if list_counters.len() == 0 {
markdown.push('\n');
}
}
ElementType::Text => {
let text = element.text_as_ref()?;
markdown.push_str(&text.text);
if !text.text.ends_with(" ") {
markdown.push_str(" ");
}
}
ElementType::Hyperlink => {
let hyperlink = element.hyperlink_as_ref()?;
if hyperlink.url == hyperlink.alt && hyperlink.alt == hyperlink.url {
markdown.push_str(&format!("{}", hyperlink.url));
} else {
markdown.push_str(&format!(
"[{}]({} \"{}\")",
hyperlink.title, hyperlink.url, hyperlink.alt
));
}
}
ElementType::Image => {
let image = element.image_as_ref()?;
let image_path = format!("image{}.png", image_num);
markdown.push_str(&format!(
"![{}]({} \"{}\")",
image.alt, image_path, image.title
));
images.insert(image_path.to_string(), image.bytes.clone());
*image_num += 1;
}
ElementType::Table => {
let table = element.table_as_ref()?;
let mut max_lengths: Vec<usize> = Vec::new();
for header in &table.headers {
let header_text = header.element.text_as_ref()?;
max_lengths.push(header_text.text.len());
}
for row in &table.rows {
for (cell_index, cell) in row.cells.iter().enumerate() {
let cell_text = cell.element.text_as_ref()?;
if cell_index < max_lengths.len() {
max_lengths[cell_index] =
max_lengths[cell_index].max(cell_text.text.len());
}
}
}
for (index, header) in table.headers.iter().enumerate() {
let header_text = header.element.text_as_ref()?;
let padding = max_lengths[index] - header_text.text.len();
markdown.push_str("| ");
markdown.push_str(&header_text.text);
markdown.push_str(&" ".repeat(padding));
markdown.push(' ');
}
markdown.push_str("|\n");
for max_length in &max_lengths {
markdown.push_str("|");
markdown.push_str(&"-".repeat(*max_length + 2));
}
markdown.push_str("|\n");
for row in &table.rows {
for (cell_index, cell) in row.cells.iter().enumerate() {
let cell_text = cell.element.text_as_ref()?;
let padding = max_lengths[cell_index] - cell_text.text.len();
markdown.push_str("| ");
markdown.push_str(&cell_text.text);
markdown.push_str(&" ".repeat(padding));
markdown.push(' ');
}
markdown.push_str("|\n");
}
markdown.push('\n');
}
_ => {}
}
Ok(())
}
let mut list_counters: Vec<usize> = Vec::new();
let mut list_types: Vec<bool> = Vec::new();
for element in &document.elements {
generate_element(
element,
&mut markdown,
0,
&mut list_counters,
&mut list_types,
&mut images,
&mut image_num,
)?;
}
Ok((Bytes::from(markdown), HashMap::new()))
}
}
mod tests {
use crate::core::*;
use crate::markdown::*;
use crate::text;
fn test() -> anyhow::Result<()> {
let document = r#"# First header
Paragraph  bla bla bla blabla bla bla blabla bla bla blabla bla bla blabla bla bla blabla bla bla blabla bla bla blabla bla bla blabla bla bla
blabla bla bla blabla bla bla blabla bla bla blabla bla bla bla
1. List item 1
2. List item 2
3. List item 3
1. List item secode level 1
2. List item secode level 2
4. List item 4
1. List item secode level 3
2. List item secode level 4
5. List item 5
1. List item secode level 5
- List item one
- List item two
- List item three
- List item four
- List item five
- List item zzz
- List item six
- List item seven
![Picture alt1](test/data/picture.png "Picture title1")
Bla bla bla ![Picture alt2](test/data/picture.png "Picture title2") bla. http://example.com  [Example](http://example.com) [Example](http://example.com "Example tooltip")
| Syntax      | Description |
| ----------- | ----------- |
| Header      | Title       |
| Paragraph   | Text        |
Paragraph2  bla bla bla blabla bla bla blabla bla bla blabla bla bla blabla bla bla blabla bla bla blabla bla bla blabla bla bla blabla bla bla
blabla2 bla bla blabla bla bla blabla bla bla blabla bla bla bla"#;
let mut images = HashMap::new();
let image_bytes = std::fs::read("test/data/picture.png")?;
images.insert("test/data/picture.png".to_string(), image_bytes);
let parsed = Transformer::parse(&document.as_bytes().into(), &HashMap::new());
let document_string = std::str::from_utf8(document.as_bytes())?;
println!("{}", document_string);
assert!(parsed.is_ok());
let parsed_document = parsed.unwrap();
println!("==========================");
println!("{:?}", parsed_document);
println!("==========================");
let generated_result = Transformer::generate(&parsed_document);
assert!(generated_result.is_ok());
let generated_bytes = generated_result?;
let generated_text = std::str::from_utf8(&generated_bytes.0)?;
println!("{}", generated_text);
println!("==========================");
let generated_result = text::Transformer::generate(&parsed_document);
assert!(generated_result.is_ok());
let generated_bytes = generated_result?;
let generated_text = std::str::from_utf8(&generated_bytes.0)?;
println!("{}", generated_text);
Ok(())
}
}
