diff --git a/Cargo.toml b/Cargo.toml index cd62258..522c4d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,3 +4,8 @@ version = "0.1.0" edition = "2024" [dependencies] + +[lib] +name = "transpiler" +path = "lib/mod.rs" + diff --git a/lib/filesystem.rs b/lib/filesystem.rs new file mode 100644 index 0000000..8a1e566 --- /dev/null +++ b/lib/filesystem.rs @@ -0,0 +1,20 @@ +use std::io::Write; +use std::{fs, io}; +use std::path::Path; + +pub struct Filesystem; + +impl Filesystem { + /// This uses a generic: `>` to allow for easy use with any type that can be + /// converted into a &Path. Allowing String types, &str types and anything that can be + /// converted into a Path type, and then borrowed. There is no run time cost of doing this + /// either, so it is very fast and very elegant. + pub fn read_file>(path: P) -> io::Result { + fs::read_to_string(path) + } + + pub fn write_file>(path: P, contents: &str) -> io::Result<()> { + let mut file = fs::File::create(path)?; + file.write_all(contents.as_bytes()) + } +} diff --git a/lib/mod.rs b/lib/mod.rs new file mode 100644 index 0000000..45e3545 --- /dev/null +++ b/lib/mod.rs @@ -0,0 +1,3 @@ +pub mod node; +pub mod parser; +pub mod filesystem; diff --git a/lib/node.rs b/lib/node.rs new file mode 100644 index 0000000..61242d1 --- /dev/null +++ b/lib/node.rs @@ -0,0 +1,170 @@ +#[derive(Debug)] +pub enum Node { + // Structure Nodes + Document { children: Vec }, + Heading { level: u8, children: Vec }, + Paragraph { children: Vec }, + List { ordered: bool, children: Vec }, + ListItem { children: Vec }, + CodeBlock { children: Vec }, + BlockQuote { children: Vec }, + ImageNode { src: String, alt: String }, + + // Inline Nodes + Text { content: String }, + Bold { content: String }, + Italic { content: String }, + BoldItalic { content: String }, + Code { content: String }, + Link { href: String, content: String }, +} + +impl Node { + /// Recursively convert a node into a HTML string. This is used to generate the DOM output + /// tree. This should only be called on the root node of the tree. This function will recursively + /// call itself to create the entire tree. + /// + /// Currently, this function does not create indentation, that would be a nice touch though. + pub fn to_html(&self) -> String { + match self { + // Structure nodes + Node::Document { children } => { + let inner = children.iter().map(|x| x.to_html()).collect::(); + format!( + "\n\n\n\n\nDocument\n\n\n{}\n", + inner + ) + } + Node::Heading { level, children } => { + let inner = children.iter().map(|x| x.to_html()).collect::(); + format!("{}\n", inner, level = level) + } + Node::Paragraph { children } => { + let inner = children.iter().map(|x| x.to_html()).collect::(); + format!("

{}

\n", inner) + } + Node::List { ordered, children } => { + let inner = children.iter().map(|x| x.to_html()).collect::(); + let tag = if *ordered { "ol" } else { "ul" }; + format!("<{tag}>{}\n", inner, tag = tag) + } + Node::ListItem { children } => { + let inner = children.iter().map(|x| x.to_html()).collect::(); + format!("
  • {}
  • \n", inner) + } + Node::CodeBlock { children } => { + let inner = children.iter().map(|x| x.to_html()).collect::(); + format!("{}\n", inner) + } + Node::BlockQuote { children } => { + let inner = children.iter().map(|x| x.to_html()).collect::(); + format!("
    {}
    \n", inner) + } + Node::ImageNode { src, alt } => format!("\"{}\"\n", src, alt), + + // Inline nodes + Node::Text { content } => format!("{}", content), + Node::Bold { content } => format!("{}", content), + Node::Italic { content } => format!("{}", content), + Node::BoldItalic { content } => format!("{}", content), + Node::Code { content } => format!("{}", content), + Node::Link { href, content } => format!("{}", href, content), + } + } + + /// Determines if a node is empty. For structure nodes (those with `children`) this will be + /// true when there are no elements in the list. For inline nodes (those without `children`) + /// this will be true when the string content of all fields are blank. + pub fn is_empty(&self) -> bool { + match self { + // Structure nodes + Node::Document { children } + | Node::Heading { level: _, children } + | Node::Paragraph { children } + | Node::List { + ordered: _, + children, + } + | Node::ListItem { children } + | Node::CodeBlock { children } + | Node::BlockQuote { children } => children.is_empty(), + + + // Inline nodes + Node::Text { content } + | Node::Bold { content } + | Node::Italic { content } + | Node::BoldItalic { content } + | Node::Code { content } => content.is_empty(), + + // Special rules + Node::Link { href, content } => content.is_empty() && href.is_empty(), + Node::ImageNode { src, alt } => src.is_empty() && alt.is_empty(), + } + } + + /// Returns Some children if they exist, otherwise None will be returned. For nodes that do not + /// have children, None will be returned. + pub fn children(&self) -> Option<&[Node]> { + match self { + // Structure Nodes + Node::Document { children } + | Node::Heading { level: _, children } + | Node::Paragraph { children } + | Node::List { ordered: _, children, } + | Node::ListItem { children } + | Node::CodeBlock { children } + | Node::BlockQuote { children } => Some(&children), + + // Inline Nodes + Node::Text { content: _ } + | Node::Bold { content: _ } + | Node::Italic { content: _ } + | Node::BoldItalic { content: _ } + | Node::Code { content: _ } => None, + + // Special Nodes + Node::Link { href: _, content: _ } => None, + Node::ImageNode { src: _, alt: _ } => None, + } + } + + /// Add a child to the back of the list of children. If the node is a type which does not allow + /// children to be added, this function will panic. + /// + /// Example INVALID usage: + /// + /// ```rust + /// // Attempting to add an inline node as a child of another inline node. + /// let mut inline = Node::Text { content: String::from("Hello world") }; + /// let inline2 = Node::Bold { content: String::from(" bolded text") }; + /// inline.add_child(inline2); // Will panic! 'Can't add child to this node type.' + /// ``` + pub fn add_child(&mut self, child: Node) { + match self { + // Structure Nodes + Node::Document { children } + | Node::Heading { level: _, children } + | Node::Paragraph { children } + | Node::List { + ordered: _, + children, + } + | Node::ListItem { children } + | Node::CodeBlock { children } + | Node::BlockQuote { children } => children.push(child), + + // Inline Nodes + Node::Text { content: _ } + | Node::Bold { content: _ } + | Node::Italic { content: _ } + | Node::BoldItalic { content: _ } + | Node::Code { content: _ } => panic!("Can't add child to this node type."), + + // Special Nodes + Node::Link { href: _, content: _ } => panic!("Can't add child to this node type."), + Node::ImageNode { src: _, alt: _ } => panic!("Can't add child to this node type."), + }; + } +} + diff --git a/lib/parser.rs b/lib/parser.rs new file mode 100644 index 0000000..4b5fad2 --- /dev/null +++ b/lib/parser.rs @@ -0,0 +1,26 @@ +use crate::node::Node; + +#[derive(Debug)] +pub struct Parser { + content: String +} + +impl Parser { + /// Create a new parser object with the content attached. This does not take ownership of the + /// string provided and therefore dies with the string. The input string is normalized to + /// support operation on all operating systems. + pub fn new(content: &str) -> Self { + let normalized = content.replace("\r\n", "\n").replace("\r", ""); + Self { content: normalized } + } + + pub fn parse_document(&self) -> Node { + let chars = self.content.chars().peekable(); + + Node::Document { children: vec![ + Node::Paragraph { children: vec![ + Node::Text { content: chars.collect() }, + ]} + ]} + } +} diff --git a/src/main.rs b/src/main.rs index 7b532fe..3289050 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,247 +1,21 @@ -use std::io::Write; -use std::{fs, io, usize}; -use std::path::Path; - -pub enum Node { - // Structure Nodes - Document { children: Vec }, - Heading { level: u8, children: Vec }, - Paragraph { children: Vec }, - List { ordered: bool, children: Vec }, - ListItem { children: Vec }, - CodeBlock { children: Vec }, - BlockQuote { children: Vec }, - ImageNode { src: String, alt: String }, - - // Inline Nodes - Text { content: String }, - Bold { content: String }, - Italic { content: String }, - BoldItalic { content: String }, - Code { content: String }, - Link { href: String, content: String }, -} - -impl Node { - pub fn to_html(&self) -> String { - match self { - // Structure nodes - Node::Document { children } => { - let inner = children.iter().map(|x| x.to_html()).collect::(); - format!( - "\n\n\n\n\nDocument\n\n\n{}\n", - inner - ) - } - Node::Heading { level, children } => { - let inner = children.iter().map(|x| x.to_html()).collect::(); - format!("{}\n", inner, level = level) - } - Node::Paragraph { children } => { - let inner = children.iter().map(|x| x.to_html()).collect::(); - format!("

    {}

    \n", inner) - } - Node::List { ordered, children } => { - let inner = children.iter().map(|x| x.to_html()).collect::(); - let tag = if *ordered { "ol" } else { "ul" }; - format!("<{tag}>{}\n", inner, tag = tag) - } - Node::ListItem { children } => { - let inner = children.iter().map(|x| x.to_html()).collect::(); - format!("
  • {}
  • \n", inner) - } - Node::CodeBlock { children } => { - let inner = children.iter().map(|x| x.to_html()).collect::(); - format!("{}\n", inner) - } - Node::BlockQuote { children } => { - let inner = children.iter().map(|x| x.to_html()).collect::(); - format!("
    {}
    \n", inner) - } - Node::ImageNode { src, alt } => format!("\"{}\"\n", src, alt), - - // Inline nodes - Node::Text { content } => format!("{}", content), - Node::Bold { content } => format!("{}", content), - Node::Italic { content } => format!("{}", content), - Node::BoldItalic { content } => format!("{}", content), - Node::Code { content } => format!("{}", content), - Node::Link { href, content } => format!("{}", href, content), - } - } - - /// Determines if a node is empty. For structure nodes (those with `children`) this will be - /// true when there are no elements in the list. For inline nodes (those without `children`) - /// this will be true when the string content of all fields are blank. - pub fn is_empty(&self) -> bool { - match self { - // Structure nodes - Node::Document { children } - | Node::Heading { level: _, children } - | Node::Paragraph { children } - | Node::List { - ordered: _, - children, - } - | Node::ListItem { children } - | Node::CodeBlock { children } - | Node::BlockQuote { children } => children.is_empty(), - - - // Inline nodes - Node::Text { content } - | Node::Bold { content } - | Node::Italic { content } - | Node::BoldItalic { content } - | Node::Code { content } => content.is_empty(), - - // Special rules - Node::Link { href, content } => content.is_empty() && href.is_empty(), - Node::ImageNode { src, alt } => src.is_empty() && alt.is_empty(), - } - } - - /// Returns Some children if they exist, otherwise None will be returned. For nodes that do not - /// have children, None will be returned. - pub fn children(&self) -> Option<&[Node]> { - match self { - // Structure Nodes - Node::Document { children } - | Node::Heading { level: _, children } - | Node::Paragraph { children } - | Node::List { ordered: _, children, } - | Node::ListItem { children } - | Node::CodeBlock { children } - | Node::BlockQuote { children } => Some(&children), - - // Inline Nodes - Node::Text { content: _ } - | Node::Bold { content: _ } - | Node::Italic { content: _ } - | Node::BoldItalic { content: _ } - | Node::Code { content: _ } => None, - - // Special Nodes - Node::Link { href: _, content: _ } => None, - Node::ImageNode { src: _, alt: _ } => None, - } - } - - /// Add a child to the back of the list of children. If the node is a type which does not allow - /// children to be added, this function will panic. - /// - /// Example INVALID usage: - /// - /// ```rust - /// // Attempting to add an inline node as a child of another inline node. - /// let mut inline = Node::Text { content: String::from("Hello world") }; - /// let inline2 = Node::Bold { content: String::from(" bolded text") }; - /// inline.add_child(inline2); // Will panic! 'Can't add child to this node type.' - /// ``` - pub fn add_child(&mut self, child: Node) { - match self { - // Structure Nodes - Node::Document { children } - | Node::Heading { level: _, children } - | Node::Paragraph { children } - | Node::List { - ordered: _, - children, - } - | Node::ListItem { children } - | Node::CodeBlock { children } - | Node::BlockQuote { children } => children.push(child), - - // Inline Nodes - Node::Text { content: _ } - | Node::Bold { content: _ } - | Node::Italic { content: _ } - | Node::BoldItalic { content: _ } - | Node::Code { content: _ } => panic!("Can't add child to this node type."), - - // Special Nodes - Node::Link { href: _, content: _ } => panic!("Can't add child to this node type."), - Node::ImageNode { src: _, alt: _ } => panic!("Can't add child to this node type."), - }; - } -} - -pub struct Parser { - content: String, - position: usize, -} - -impl Parser { - pub fn new(content: String) -> Self { - // Normalize the input - let normalized = content.replace("\r\n", "\n").replace("\r", ""); - Self { content: normalized, position: 0 } - } - - pub fn parse_document(&self) -> Node { - let text = Node::Text { content: self.content.clone() }; - let heading = Node::Heading { level: 1, children: vec![text] }; - Node::Document { children: vec![ heading ] } - } - - fn peek(&self, offset: usize) -> u8 { - - } -} - -pub struct Filesystem; - -impl Filesystem { - /// This uses a generic: `>` to allow for easy use with any type that can be - /// converted into a &Path. Allowing String types, &str types and anything that can be - /// converted into a Path type, and then borrowed. There is no run time cost of doing this - /// either, so it is very fast and very elegant. - pub fn read_file>(path: P) -> io::Result { - fs::read_to_string(path) - } - - pub fn write_file>(path: P, contents: &str) -> io::Result<()> { - let mut file = fs::File::create(path)?; - file.write_all(contents.as_bytes()) - } -} +use transpiler::parser::Parser; +use transpiler::filesystem::Filesystem; pub fn main() -> Result<(), Box> { - let mut root = Node::Document { - children: vec![Node::Heading { - level: 1, - children: vec![ - Node::Text { - content: String::from("Heading level 1"), - }, - Node::Bold { - content: String::from(" this part is bold"), - }, - Node::Italic { - content: String::from(" this part is italic"), - }, - ], - }], - }; - - let p = Node::Paragraph { - children: vec![Node::Text { - content: String::from("This is a paragraph tag."), - }], - }; - - root.add_child(p); - - let parser = Parser::new(String::from("Hello world\r\nhello\n")); - let node = parser.parse_document(); - - let content = Filesystem::read_file("./test/journal.md"); - match content { - Ok(s) => Filesystem::write_file("./copied.md", &s)?, + let file = Filesystem::read_file("./test/journal.md"); + let content; + match file { + Ok(s) => content = s, Err(err) => panic!("Failed to read file. {}", err) } - println!("{}", node.to_html()); + let parser = Parser::new(&content); + let node = parser.parse_document(); + + match Filesystem::write_file("./output.html", &node.to_html()) { + Ok(_) => println!("Input document parsed and output written."), + Err(err) => panic!("Failed to write to output. {}", err) + } // Return a value to meet the main function requirements Ok(())