├── .gitignore ├── .travis.yml ├── src ├── formatter │ ├── xml_formatter.rs │ ├── mod.rs │ ├── html4_formatter.rs │ ├── html5_formatter.rs │ └── xhtml_formatter.rs ├── parser │ ├── doctype.rs │ ├── mod.rs │ └── element.rs ├── regex │ └── mod.rs ├── lib.rs ├── parse.rs ├── lex.rs └── arena │ └── mod.rs ├── Cargo.toml ├── LICENSE ├── RELEASE ├── tests ├── common │ └── mod.rs ├── tests.rs └── tests.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /src/macros/target 3 | /src/macros/*.rs.bk 4 | /src/traits/target 5 | /src/traits/*.rs.bk 6 | **/*.rs.bk 7 | Cargo.lock 8 | .vscode 9 | .idea 10 | .DS_Store -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | - nightly 5 | 6 | matrix: 7 | include: 8 | - env: TARGET=x86_64-unknown-linux-gnu 9 | - env: TARGET=x86_64-apple-darwin 10 | os: osx 11 | - env: TARGET=x86_64-pc-windows-gnu 12 | -------------------------------------------------------------------------------- /src/formatter/xml_formatter.rs: -------------------------------------------------------------------------------- 1 | use crate::arena::{Arena, ArenaItem}; 2 | use crate::formatter::HtmlFormatter; 3 | use crate::parser::Haml; 4 | 5 | #[derive(Debug)] 6 | pub struct XmlFormatter; 7 | 8 | impl XmlFormatter { 9 | pub fn new() -> XmlFormatter { 10 | XmlFormatter {} 11 | } 12 | } 13 | 14 | impl HtmlFormatter for XmlFormatter { 15 | fn generate(&self, arena: &Arena) -> String { 16 | String::new() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hamlrs" 3 | version = "0.4.2" 4 | authors = ["Jon Hartwell "] 5 | license = "MIT" 6 | description = "A Rust library for parsing [Haml](http://haml.info) templates" 7 | repository = "https://github.com/jhartwell/haml-rs" 8 | keywords = ["haml", "templating", "template"] 9 | categories = ["development-tools", "parser-implementations", "template-engine"] 10 | readme = "README.md" 11 | maintenance = { status = "actively-developed" } 12 | edition = "2018" 13 | 14 | [lib] 15 | name = "haml" 16 | path = "src/lib.rs" 17 | test = true 18 | doctest = true 19 | doc = true 20 | 21 | [dependencies] 22 | regex = "1" 23 | 24 | [dev-dependencies] 25 | serde_derive = "1.0.91" 26 | serde = "1.0.91" 27 | serde_json = "1.0.39" -------------------------------------------------------------------------------- /src/formatter/mod.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use crate::arena::Arena; 4 | use crate::Format; 5 | 6 | pub mod html4_formatter; 7 | pub mod html5_formatter; 8 | pub mod xhtml_formatter; 9 | pub mod xml_formatter; 10 | 11 | use html4_formatter::Html4Formatter; 12 | use html5_formatter::Html5Formatter; 13 | use xhtml_formatter::XHtmlFormatter; 14 | use xml_formatter::XmlFormatter; 15 | 16 | pub trait HtmlFormatter: Debug { 17 | fn generate(&self, arena: &Arena) -> String; 18 | } 19 | 20 | pub fn get_formatter(format: &Format) -> Box { 21 | match format { 22 | Format::Html4() => Box::new(Html4Formatter::new()), 23 | Format::Html5() => Box::new(Html5Formatter::new()), 24 | Format::XHtml() => Box::new(XHtmlFormatter::new()), 25 | Format::Xml() => Box::new(XmlFormatter::new()), 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Jonathan Hartwell 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /RELEASE: -------------------------------------------------------------------------------- 1 | Version 0.4.2 2 | ------- 3 | Fixed bug where periods in text would cause parse failure as it was trying to parse as a class. Text with period now properly generates 4 | 5 | Version 0.4.1 6 | ------- 7 | Fixed bug with comments not being outputted in the resulting HTML 8 | 9 | Version 0.4.0 10 | ------- 11 | Removed CLI from library project. The crate for the CLI is now https://crates.io/crates/hamlrs-cli 12 | 13 | Version 0.3.0 14 | ------- 15 | Features: 16 | * CLI 17 | - The CLI application now supports pretty-printing an AST of a Haml file. You can do this by running hamlrs ast input.haml 18 | * Library 19 | - Added Ruby style attributes with dictionary syntax 20 | - Added to_ast function in the haml module so it is possible to generate both AST or HTML from the library 21 | - Attributes are now ordered similar to how the Ruby version of Haml does 22 | 23 | Bug Fixes: 24 | * Fixed various issues with improper nesting of elements. There were cases where nested elements would be put under the root element. 25 | * Fixed issue where empty lines caused the parser to stop parsing. This was causing documents to only generate half of what they should 26 | 27 | 28 | Version 0.2.0 29 | ------- 30 | Breaking Changes: 31 | Public: 32 | * The Haml struct has been removed from the haml module. Instead of exporting the struct there is now just the to_html function 33 | that is exported 34 | Private: 35 | * The Generator struct from the generator module has been removed. Instead of exporting the struct there is now just the to_html function 36 | that is exported 37 | Bug Fixes: 38 | * Fixed Windows support and tested on Windows machine. 39 | Features: 40 | * Added platform dependent newline support. On Windows machines, the HTML will generate with CRLF and on non-Windows machines 41 | the newline will be a LF. 42 | 43 | Version 0.1.2 44 | ------- 45 | README update to ensure that crates.io was properly updated 46 | Version 0.1.1 47 | ------- 48 | Bug Fixes: 49 | * Fixed issue with hamlrs not properly handling carriage returns in Windows. 50 | -------------------------------------------------------------------------------- /src/parser/doctype.rs: -------------------------------------------------------------------------------- 1 | use crate::regex::prolog; 2 | use crate::Format; 3 | 4 | pub struct Doctype<'a> { 5 | value: Option<&'a str>, 6 | format: &'a Format, 7 | } 8 | 9 | impl<'a> Doctype<'a> { 10 | pub fn new(format: &'a Format, value: Option<&'a str>) -> Doctype<'a> { 11 | Doctype { format, value } 12 | } 13 | 14 | pub fn to_html(&self) -> String { 15 | match self.format { 16 | Format::XHtml() => self.xhtml_options().to_owned(), 17 | Format::Html4() => self.html4_options().to_owned(), 18 | Format::Html5() => String::new(), 19 | Format::Xml() => String::new(), 20 | } 21 | } 22 | 23 | fn html4_options(&self) -> &str { 24 | if let Some(value) = &self.value { 25 | match value.to_lowercase().as_str() { 26 | "frameset" => r#""#, 27 | "strict" => r#""#, 28 | _ => r#""#, 29 | } 30 | } else { 31 | "" 32 | } 33 | } 34 | 35 | fn xhtml_options(&self) -> &str { 36 | if let Some(value) = &self.value { 37 | match value.to_lowercase().as_str() { 38 | "strict" => r#""#, 39 | "frameset" => r#""#, 40 | "5" => "", 41 | "1.1" => r#""#, 42 | "basic" => r#""#, 43 | "mobile" => r#""#, 44 | "rdfa" => r#""#, 45 | _ => "" 46 | } 47 | } else { 48 | "" 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use haml; 2 | // use haml::HtmlFormat; 3 | use haml::Format; 4 | use serde_derive::{Deserialize, Serialize}; 5 | use std::collections::HashMap; 6 | 7 | pub type Tests = HashMap>; 8 | 9 | pub trait TestCollection { 10 | fn run(&self); 11 | fn run_test_by_name(&self, name: &str); 12 | fn run_test_section(&self, name: &str); 13 | } 14 | 15 | impl TestCollection for Tests { 16 | fn run(&self) { 17 | for (_, value) in self { 18 | for (name, test) in value { 19 | test.run(name); 20 | } 21 | } 22 | } 23 | 24 | fn run_test_by_name(&self, name: &str) { 25 | for (_, value) in self { 26 | for (test_name, test) in value { 27 | if name == test_name { 28 | test.run(name); 29 | } 30 | } 31 | } 32 | } 33 | 34 | fn run_test_section(&self, name: &str) { 35 | if let Some(val) = self.get(name) { 36 | for (test_name, test) in val { 37 | test.run(test_name); 38 | } 39 | } 40 | } 41 | } 42 | 43 | #[derive(Serialize, Deserialize, Debug)] 44 | pub struct Test { 45 | pub config: Option, 46 | pub haml: String, 47 | pub html: String, 48 | pub optional: Option, 49 | } 50 | 51 | impl Test { 52 | pub fn run(&self, name: &str) { 53 | println!("Running test: {}", name); 54 | println!("Input Haml:\n {}", self.haml); 55 | match self.optional { 56 | Some(true) => return, 57 | _ => { 58 | let mut format: Format = Format::Html5(); 59 | if let Some(config) = &self.config { 60 | if let Some(config_format) = &config.format { 61 | match config_format.as_str() { 62 | "xhtml" => format = Format::XHtml(), 63 | "html4" => format = Format::Html4(), 64 | "html5" => format = Format::Html5(), 65 | _ => format = Format::Html5(), 66 | } 67 | } 68 | } 69 | println!("Format: {}", format); 70 | let actual_html = haml::to_html(&self.haml, &format); 71 | assert_eq!(self.html, actual_html); 72 | } 73 | } 74 | } 75 | } 76 | 77 | #[derive(Serialize, Deserialize, Debug)] 78 | pub struct Config { 79 | pub format: Option, 80 | pub escape_html: Option, 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Update 2 | 3 | I have had some personal issues come up and I have ignored this project for a few months because of it. After getting back into it I realized how badly it was written. I am working on rewriting the codebase so that it is clearer and easier to work with. While doing that I will be using the tests from the Ruby implementation to ensure that nothing is broken and work on achieving greater compatibility. 4 | 5 | # Haml-rs 6 | 7 | [![](https://img.shields.io/crates/v/hamlrs.svg?maxAge=25920)](https://crates.io/crates/hamlrs) ![Travis CI Build Status](https://travis-ci.org/Haml-rs/haml-rs.svg?branch=master) 8 | 9 | This is a library for parsing [Haml](http://haml.info/) templates. You are able to get Haml-rs on [Crates.io](https://crates.io/crates/hamlrs). The aim for this is to produce identical HTML to what the Ruby [Haml gem](https://rubygems.org/gems/haml) produces. 10 | 11 | ## Usage 12 | 13 | To include haml-rs in your project add the following to your Cargo.toml: 14 | 15 | ``` 16 | [dependencies] 17 | hamlrs = "0.4.2" 18 | ``` 19 | Then add the following to your code: 20 | 21 | ```rust 22 | extern crate haml; 23 | ``` 24 | ## Example 25 | 26 | #### Library 27 | ```rust 28 | extern crate haml; 29 | 30 | fn main() { 31 | let test_haml = "%span"; 32 | let html = haml::to_html(&test_haml); 33 | } 34 | ``` 35 | 36 | ### Stability 37 | 38 | This software is in its early stages and as such there may be issues with stability. 39 | 40 | If you find any bugs please don't hesitate to open an [issue on github](https://github.com/Haml-rs/haml-rs/issues) or, if you want, you can reach out directly to me at jon@dontbreakthebuild.com 41 | 42 | 43 | ## Integration tests 44 | 45 | There are currently a few integration tests (and more to come). If you are going to contribute to an integration test please make sure that the HTML file that is generated by the Ruby [Haml gem](https://rubygems.org/gems/haml) so that we can ensure that we are producing the same output as the reference implementation. 46 | 47 | ## Current limitations 48 | 49 | There is no variable/expression support in Haml-rs. This is something that is being thought about and if you want to add your thoughts you are more than welcome to comment [here](https://github.com/jhartwell/haml-rs/issues/6). 50 | 51 | ## License 52 | 53 | This project is licensed under the [MIT license](https://github.com/jhartwell/haml-rs/blob/master/LICENSE). 54 | 55 | ## Contribution 56 | 57 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in Haml-rs by you, shall be licensed as MIT, without any additional terms or conditions. 58 | 59 | 60 | If you have any questions you can reach me via email at jon@dontbreakthebuild.com -------------------------------------------------------------------------------- /src/regex/mod.rs: -------------------------------------------------------------------------------- 1 | pub const WHITESPACE: &str = r"\s*"; 2 | pub const STRING: &str = r"\w+"; 3 | 4 | pub const TEXT_REGEX: &str = r"^(\s*)(?P.+)"; 5 | 6 | pub const COMMENT_REGEX: &str = r"\s*/(?P.*)"; 7 | 8 | fn element_name() -> String { 9 | r"[%]{1}[\w|:|\-|_]+[>|<]{0,1}".to_owned() 10 | } 11 | 12 | pub fn element_class_id() -> String { 13 | r"[#|.]{1}[\w|\-|_]+".to_owned() 14 | } 15 | 16 | fn element_text() -> String { 17 | r"\s+.+".to_owned() 18 | } 19 | 20 | fn self_close() -> String { 21 | r"[/]".to_owned() 22 | } 23 | 24 | pub fn silent_comment() -> String { 25 | r"^(?P\s*)(-#)".to_owned() 26 | } 27 | 28 | pub fn conditional_comment() -> String { 29 | r#"(?P(\s*))[\[](?P([^\]]*))[\]]"#.to_string() 30 | } 31 | 32 | pub fn element() -> String { 33 | format!( 34 | "^(?P{})*(?P{}){{1}}(?P({})*)(?P({}){{0,1}})(?P({}){{0,1}})(?P{}{{0,1}})(?P{})*", 35 | WHITESPACE, 36 | element_name(), 37 | element_class_id(), 38 | ruby_attributes(), 39 | html_attributes(), 40 | self_close(), 41 | element_text(), 42 | ) 43 | } 44 | 45 | pub fn prolog() -> String { 46 | r"^\s*!!!\s*(?P([\w|.|\d]*))".to_owned() 47 | } 48 | 49 | pub fn sanitize() -> String { 50 | r"^(\s*)(&=)\s*[']{1}(?P([^'|^\n]*))".to_owned() 51 | } 52 | 53 | pub fn div() -> String { 54 | format!( 55 | "(?P{})*(?P{}){{1}}(?P({})*)(?P({}){{0,1}})(?P({}){{0,1}})(?P{}{{0,1}})(?P{})*", 56 | WHITESPACE, 57 | element_class_id(), 58 | element_class_id(), 59 | ruby_attributes(), 60 | html_attributes(), 61 | self_close(), 62 | element_text(), 63 | ) 64 | } 65 | 66 | fn ruby_attributes() -> String { 67 | //"[{{]((\\s*[:]\\w+){1}\\s*[=]\\s*[']\\w*[']\\s*)+[}}]".to_owned() 68 | r#"[{]{1}[^}]*[}]{1}"#.to_owned() 69 | } 70 | 71 | fn html_attributes() -> String { 72 | r"[(]{1}[^)]+[)]{1}".to_owned() 73 | //r"\([\w:]*\s*[=]\s*[\w]*\)".to_owned() 74 | } 75 | 76 | pub fn ruby_attribute() -> String { 77 | r#"([:]{1}([^\s]+)\s*(=>){1}\s*["]{1}([^"]*)["]{1})*"#.to_owned() 78 | } 79 | 80 | pub fn html_attribute() -> String { 81 | r#"(([^\s|^(]+)\s*(=){1}\s*[']{0,1}([^'|^)]*)[']{0,1})"#.to_owned() 82 | } 83 | 84 | pub fn class() -> String { 85 | r"([.]{1}[^.|^#]+)*".to_owned() 86 | } 87 | 88 | pub fn id() -> String { 89 | r"([#]{1}[^.|^#]+)*".to_owned() 90 | } 91 | 92 | #[cfg(test)] 93 | mod test { 94 | use super::*; 95 | use ::regex::Regex; 96 | #[test] 97 | fn zzz() { 98 | let a = "(a='b'\nc='d')"; 99 | let r = Regex::new(&html_attributes()).unwrap(); 100 | assert!(r.is_match(a)); 101 | } 102 | } -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | // mod arena; 3 | // mod formatter; 4 | // mod parser; 5 | // mod regex; 6 | // use formatter::HtmlFormatter; 7 | // use parser::Parser; 8 | mod lex; 9 | mod parse; 10 | 11 | use std::collections::{BTreeSet, HashMap}; 12 | 13 | #[derive(Debug)] 14 | pub enum Format { 15 | Html4(), 16 | Html5(), 17 | Xml(), 18 | XHtml(), 19 | } 20 | 21 | pub fn to_html(haml: &str, format: &Format) -> String { 22 | // let mut parser = Parser::new(format); 23 | // let ast = parser.parse(haml); 24 | // let generator = formatter::get_formatter(format); 25 | // generator.generate(&ast) 26 | String::new() 27 | } 28 | 29 | use std::fmt; 30 | 31 | impl fmt::Display for Format { 32 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 33 | let mut format = "html5"; 34 | match self { 35 | Format::XHtml() => format = "xhtml", 36 | Format::Html4() => format = "html4", 37 | Format::Html5() => format = "html5", 38 | Format::Xml() => format = "xml", 39 | } 40 | write!(f, "{}", format) 41 | } 42 | } 43 | 44 | 45 | #[derive(Debug, PartialEq)] 46 | pub(crate) enum Token { 47 | Whitespace(), 48 | Text(String), 49 | OpenParen(), 50 | CloseParen(), 51 | OpenBrace(), 52 | CloseBrace(), 53 | PercentageSign(), 54 | Period(), 55 | Equal(), 56 | SingleQuote(), 57 | DoubleQuote(), 58 | ForwardSlash(), 59 | BackSlash(), 60 | Hashtag(), 61 | LessThan(), 62 | GreaterThan(), 63 | Exclamation(), 64 | Ampersand(), 65 | Tilde(), 66 | Newline(), 67 | } 68 | 69 | pub trait Haml {} 70 | 71 | pub(crate) struct Declaration { 72 | value: String, 73 | } 74 | 75 | impl Declaration { 76 | pub fn new(value: &str) -> Declaration { 77 | Declaration { 78 | value: value.to_string(), 79 | } 80 | } 81 | } 82 | 83 | impl Haml for Declaration {} 84 | 85 | pub(crate) struct Element { 86 | name: String, 87 | attributes: HashMap>, 88 | attribute_order: BTreeSet, 89 | } 90 | 91 | impl Element { 92 | pub fn new(name: &str) -> Element { 93 | Element { 94 | name: name.to_string(), 95 | attributes: HashMap::new(), 96 | attribute_order: BTreeSet::new(), 97 | } 98 | } 99 | } 100 | impl Haml for Element {} 101 | 102 | pub(crate) struct Class { 103 | pub name: String, 104 | } 105 | 106 | impl Class { 107 | pub fn new(name: &str) -> Class { 108 | Class { 109 | name: name.to_string(), 110 | } 111 | } 112 | } 113 | 114 | impl Haml for Class {} 115 | 116 | 117 | pub(crate) struct Id { 118 | pub name: String, 119 | } 120 | 121 | impl Id { 122 | pub fn new(name: &str) -> Id { 123 | Id { 124 | name: name.to_string(), 125 | } 126 | } 127 | } 128 | 129 | impl Haml for Id {} -------------------------------------------------------------------------------- /src/parse.rs: -------------------------------------------------------------------------------- 1 | use crate::{Class, Declaration, Element, Haml, Id, Token}; 2 | 3 | struct State<'a> { 4 | tokens: &'a Vec, 5 | length: usize, 6 | index: usize, 7 | } 8 | 9 | impl<'a> State<'a> { 10 | pub fn new(tokens: &'a Vec) -> State { 11 | let length = tokens.len(); 12 | State { 13 | tokens, 14 | length, 15 | index: 0, 16 | } 17 | } 18 | 19 | fn get_next(&mut self) -> Option<&Token> { 20 | if self.index + 1 < self.length { 21 | self.tokens.get(self.index + 1) 22 | } else { 23 | None 24 | } 25 | } 26 | 27 | fn declaration(&mut self) -> Option> { 28 | if self.index + 1 < self.length { 29 | let temp = self.index; 30 | match self.get_next() { 31 | Some(Token::Exclamation()) => { 32 | self.index += 1; 33 | match self.get_next() { 34 | Some(Token::Exclamation()) => { 35 | self.index += 1; 36 | match self.get_next() { 37 | Some(Token::Text(txt)) => Some(Box::new(Declaration::new(txt))), 38 | None => Some(Box::new(Declaration::new(""))), 39 | _ => None, 40 | } 41 | }, 42 | _ => { 43 | self.index = temp; 44 | None 45 | } 46 | } 47 | }, 48 | _ => { 49 | self.index = temp; 50 | None 51 | } 52 | } 53 | } else { 54 | None 55 | } 56 | } 57 | 58 | fn classes_and_ids(&mut self, items: &mut Vec>) { 59 | match self.get_next() { 60 | Some(Token::Period()) => { 61 | match self.get_next() { 62 | Some(Token::Text(txt)) => { 63 | items.push(Box::new(Class::new(txt))); 64 | self.classes_and_ids(items); 65 | }, 66 | _ => (), 67 | } 68 | }, 69 | Some(Token::Hashtag()) => { 70 | match self.get_next() { 71 | Some(Token::Text(txt)) => { 72 | items.push(Box::new(Id::new(txt))); 73 | self.classes_and_ids(items); 74 | }, 75 | _ => (), 76 | } 77 | }, 78 | Some(Token::OpenParen()) => (), 79 | Some(Token::OpenBrace()) => (), 80 | _ => panic!("Invalid token"), 81 | } 82 | } 83 | 84 | fn element(&mut self) -> Option> { 85 | let mut temp = self.index; 86 | if self.index + 1 < self.length { 87 | match self.get_next() { 88 | Some(Token::Text(txt)) => { 89 | self.index = self.index + 1; 90 | let inner_temp = self.index; 91 | match self.get_next() { 92 | Some(Token::Period()) => { 93 | let mut items = vec![]; 94 | self.classes_and_ids(&mut items); 95 | } 96 | } 97 | let element = Element::new(&txt.to_string()); 98 | let mut items = vec![]; 99 | self.classes_and_ids(&mut items); 100 | element 101 | }, 102 | _ => { 103 | self.index = temp; 104 | None 105 | } 106 | } 107 | } else { 108 | None 109 | } 110 | } 111 | pub fn next(&mut self) -> Option> { 112 | match self.tokens.get(self.index) { 113 | Some(Token::Exclamation()) => self.declaration(), 114 | Some(Token::PercentageSign()) => self.element(), 115 | _ => None, 116 | } 117 | } 118 | } 119 | 120 | pub(crate) fn parse(tokens: &Vec) -> String { 121 | // let mut arena = Arena::new(); 122 | let mut index = 0; 123 | let len = tokens.len(); 124 | loop { 125 | 126 | } 127 | for tok in tokens { 128 | match tok { 129 | Token::Text(txt) => (), 130 | Token::Whitespace() => (), 131 | Token::OpenParen() => (), 132 | Token::CloseParen() => (), 133 | Token::OpenBrace() => (), 134 | Token::CloseBrace() => (), 135 | Token::PercentageSign() => (), 136 | Token::Period() => (), 137 | Token::Equal() => (), 138 | Token::SingleQuote() => (), 139 | Token::DoubleQuote() => (), 140 | Token::ForwardSlash() => (), 141 | Token::BackSlash() => (), 142 | Token::Hashtag() => (), 143 | Token::LessThan() => (), 144 | Token::GreaterThan() => (), 145 | Token::Exclamation() => (), 146 | Token::Ampersand() => (), 147 | Token::Tilde() => (), 148 | Token::Newline() => (), 149 | } 150 | } 151 | String::new() 152 | } -------------------------------------------------------------------------------- /src/formatter/html4_formatter.rs: -------------------------------------------------------------------------------- 1 | use crate::arena::{Arena, ArenaItem}; 2 | use crate::formatter::HtmlFormatter; 3 | use crate::parser::Haml; 4 | use std::collections::HashMap; 5 | #[derive(Debug)] 6 | pub struct Html4Formatter { 7 | self_closing_tags: HashMap, 8 | } 9 | 10 | impl HtmlFormatter for Html4Formatter { 11 | fn generate(&self, arena: &Arena) -> String { 12 | let root = arena.root(); 13 | let mut html = String::new(); 14 | for child in &root.children { 15 | let item = arena.item(*child); 16 | match &item.value { 17 | Haml::SilentComment(_) => (), 18 | Haml::Element(_) => html.push_str(&self.element_to_html(item, arena)), 19 | Haml::Comment(_) => html.push_str(&self.comment_to_html(item, arena)), 20 | Haml::Text(text) => html.push_str(&format!("{}\n", text.to_owned())), 21 | Haml::InnerText(text) => html.push_str(&text), 22 | Haml::Prolog(prolog) => html.push_str(&self.prolog_to_html(prolog)), 23 | Haml::ConditionalComment(ws, val) => { 24 | html.push_str(&self.conditional_comment_to_html(item, arena)) 25 | } 26 | _ => (), 27 | } 28 | } 29 | html.trim().to_owned() 30 | } 31 | } 32 | 33 | impl Html4Formatter { 34 | pub fn new() -> Html4Formatter { 35 | let mut self_closing_tags: HashMap = HashMap::new(); 36 | self_closing_tags.insert("meta".to_string(), true); 37 | Html4Formatter { self_closing_tags } 38 | } 39 | 40 | fn prolog_to_html(&self, value: &Option) -> &str { 41 | match value { 42 | None => r#""#, 43 | Some(v) => { 44 | match v.to_lowercase().as_str() { 45 | "frameset" => r#""#, 46 | "strict" => r#""#, 47 | _ => "", 48 | } 49 | } 50 | } 51 | } 52 | 53 | fn item_to_html(&self, item: &ArenaItem, arena: &Arena) -> String { 54 | match &item.value { 55 | Haml::Text(text) => format!("{}\n", text.to_owned()), 56 | Haml::Comment(comment) => self.comment_to_html(item, arena), 57 | Haml::Element(_) => self.element_to_html(item, arena), 58 | Haml::InnerText(text) => text.to_owned(), 59 | Haml::Prolog(Some(prolog)) => prolog.to_owned(), 60 | _ => String::new(), 61 | } 62 | } 63 | fn comment_to_html(&self, item: &ArenaItem, arena: &Arena) -> String { 64 | let mut html = String::new(); 65 | if let Haml::Comment(line) = &item.value { 66 | html.push_str(&format!(""); 78 | html 79 | } 80 | fn conditional_comment_to_html(&self, item: &ArenaItem, arena: &Arena) -> String { 81 | let mut html = String::new(); 82 | if let Haml::ConditionalComment(ws, value) = &item.value { 83 | html.push_str(&format!("") 89 | } 90 | html 91 | } 92 | fn element_to_html(&self, item: &ArenaItem, arena: &Arena) -> String { 93 | let mut html = String::new(); 94 | if let Haml::Element(el) = &item.value { 95 | html.push_str(&format!("<{}", el.name().unwrap())); 96 | for key in el.attributes().iter() { 97 | if let Some(value) = el.get_attribute(key) { 98 | // this needs to be separated eventuallyas this is html5 specific 99 | if key.trim() == "checked" && value == "true" { 100 | html.push_str(" checked"); 101 | } else { 102 | html.push_str(&format!(" {}='{}'", key, value.to_owned())); 103 | } 104 | } 105 | } 106 | 107 | html.push('>'); 108 | if !el.self_close && !self.self_closing_tags.contains_key(&el.name().unwrap()) { 109 | if let Some(text) = &el.inline_text { 110 | html.push_str(&format!("{}", text.trim())); 111 | } 112 | if item.children.len() > 0 { 113 | let mut index = 0; 114 | if Some("pre".to_owned()) != el.name() 115 | && Some("textarea".to_owned()) != el.name() 116 | { 117 | html.push('\n'); 118 | } 119 | for c in item.children.iter() { 120 | let i = arena.item(*c); 121 | html.push_str(&self.item_to_html(i, arena)); 122 | } 123 | } 124 | if Some("pre".to_owned()) == el.name() || Some("textarea".to_owned()) == el.name() { 125 | html = html.trim_end().to_owned(); 126 | } 127 | if Some("input".to_owned()) != el.name() { 128 | html.push_str(&format!("\n", el.name().unwrap())); 129 | } 130 | } 131 | } 132 | html 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/lex.rs: -------------------------------------------------------------------------------- 1 | use crate::Token; 2 | 3 | pub(crate) fn lex(haml: &str) -> Vec { 4 | let mut tokens = vec![]; 5 | let mut buffer = String::new(); 6 | for ch in haml.chars() { 7 | match ch { 8 | ' ' => { 9 | if !buffer.is_empty() { 10 | tokens.push(Token::Text(buffer.to_string())); 11 | buffer.clear(); 12 | } 13 | tokens.push(Token::Whitespace()); 14 | }, 15 | '(' => { 16 | if !buffer.is_empty() { 17 | tokens.push(Token::Text(buffer.to_string())); 18 | buffer.clear(); 19 | } 20 | tokens.push(Token::OpenParen()); 21 | }, 22 | ')' => { 23 | if !buffer.is_empty() { 24 | tokens.push(Token::Text(buffer.to_string())); 25 | buffer.clear(); 26 | } 27 | tokens.push(Token::CloseParen()); 28 | }, 29 | '{' => { 30 | if !buffer.is_empty() { 31 | tokens.push(Token::Text(buffer.to_string())); 32 | buffer.clear(); 33 | } 34 | tokens.push(Token::OpenBrace()); 35 | }, 36 | '}' => { 37 | if !buffer.is_empty() { 38 | tokens.push(Token::Text(buffer.to_string())); 39 | buffer.clear(); 40 | } 41 | tokens.push(Token::CloseBrace()); 42 | }, 43 | '%' => { 44 | if !buffer.is_empty() { 45 | tokens.push(Token::Text(buffer.to_string())); 46 | buffer.clear(); 47 | } 48 | tokens.push(Token::PercentageSign()); 49 | }, 50 | '.' => { 51 | if !buffer.is_empty() { 52 | tokens.push(Token::Text(buffer.to_string())); 53 | buffer.clear(); 54 | } 55 | tokens.push(Token::Period()); 56 | }, 57 | '=' => { 58 | if !buffer.is_empty() { 59 | tokens.push(Token::Text(buffer.to_string())); 60 | buffer.clear(); 61 | } 62 | tokens.push(Token::Equal()); 63 | }, 64 | '\"' => { 65 | if !buffer.is_empty() { 66 | tokens.push(Token::Text(buffer.to_string())); 67 | buffer.clear(); 68 | } 69 | tokens.push(Token::SingleQuote()); 70 | }, 71 | '\'' => { 72 | if !buffer.is_empty() { 73 | tokens.push(Token::Text(buffer.to_string())); 74 | buffer.clear(); 75 | } 76 | tokens.push(Token::DoubleQuote()); 77 | }, 78 | '\\' => { 79 | if !buffer.is_empty() { 80 | tokens.push(Token::Text(buffer.to_string())); 81 | buffer.clear(); 82 | } 83 | tokens.push(Token::BackSlash()); 84 | }, 85 | '/' => { 86 | if !buffer.is_empty() { 87 | tokens.push(Token::Text(buffer.to_string())); 88 | buffer.clear(); 89 | } 90 | tokens.push(Token::ForwardSlash()); 91 | }, 92 | '#' => { 93 | if !buffer.is_empty() { 94 | tokens.push(Token::Text(buffer.to_string())); 95 | buffer.clear(); 96 | } 97 | tokens.push(Token::Hashtag()); 98 | }, 99 | '<' => { 100 | if !buffer.is_empty() { 101 | tokens.push(Token::Text(buffer.to_string())); 102 | buffer.clear(); 103 | } 104 | tokens.push(Token::LessThan()); 105 | }, 106 | '>' => { 107 | if !buffer.is_empty() { 108 | tokens.push(Token::Text(buffer.to_string())); 109 | buffer.clear(); 110 | } 111 | tokens.push(Token::GreaterThan()); 112 | }, 113 | '!' => { 114 | if !buffer.is_empty() { 115 | tokens.push(Token::Text(buffer.to_string())); 116 | buffer.clear(); 117 | } 118 | tokens.push(Token::Exclamation()); 119 | }, 120 | '&' => { 121 | if !buffer.is_empty() { 122 | tokens.push(Token::Text(buffer.to_string())); 123 | buffer.clear(); 124 | } 125 | tokens.push(Token::Ampersand()); 126 | }, 127 | '~' => { 128 | if !buffer.is_empty() { 129 | tokens.push(Token::Text(buffer.to_string())); 130 | buffer.clear(); 131 | } 132 | tokens.push(Token::Tilde()); 133 | }, 134 | '\n' => { 135 | if !buffer.is_empty() { 136 | tokens.push(Token::Text(buffer.to_string())); 137 | buffer.clear(); 138 | } 139 | tokens.push(Token::Newline()); 140 | }, 141 | c => buffer.push(c), 142 | } 143 | 144 | } 145 | if !buffer.is_empty() { 146 | tokens.push(Token::Text(buffer.to_string())); 147 | } 148 | tokens 149 | } 150 | 151 | #[cfg(test)] 152 | mod test { 153 | use super::*; 154 | 155 | #[test] 156 | fn t() { 157 | let haml = "%test"; 158 | let tokens = lex(haml); 159 | let mut it = tokens.iter(); 160 | assert_eq!(Some(&Token::PercentageSign()), it.next()); 161 | assert_eq!(Some(&Token::Text("test".to_string())), it.next()); 162 | assert_eq!(None, it.next()); 163 | } 164 | } -------------------------------------------------------------------------------- /src/formatter/html5_formatter.rs: -------------------------------------------------------------------------------- 1 | use crate::arena::{Arena, ArenaItem}; 2 | use crate::formatter::HtmlFormatter; 3 | use crate::parser::Haml; 4 | use std::collections::HashMap; 5 | #[derive(Debug)] 6 | pub struct Html5Formatter { 7 | self_closing_tags: HashMap, 8 | } 9 | 10 | impl HtmlFormatter for Html5Formatter { 11 | fn generate(&self, arena: &Arena) -> String { 12 | let root = arena.root(); 13 | let mut html = String::new(); 14 | for child in &root.children { 15 | let item = arena.item(*child); 16 | match &item.value { 17 | Haml::SilentComment(_) => (), 18 | Haml::Element(_) => html.push_str(&self.element_to_html(item, arena)), 19 | Haml::Comment(_) => html.push_str(&self.comment_to_html(item, arena)), 20 | Haml::Text(text) => html.push_str(&format!("{}\n", text.to_owned())), 21 | Haml::InnerText(text) => html.push_str(&text), 22 | Haml::Prolog(Some(_)) => (), 23 | Haml::Prolog(None) => html.push_str(""), 24 | Haml::ConditionalComment(_, _) => { 25 | html.push_str(&self.conditional_comment_to_html(item, arena)) 26 | } 27 | _ => (), 28 | } 29 | if item.children.len() == 0 && item.parent != 0 { 30 | html.push('\n'); 31 | } 32 | } 33 | html.trim().to_owned() 34 | } 35 | } 36 | 37 | impl Html5Formatter { 38 | pub fn new() -> Html5Formatter { 39 | let mut self_closing_tags: HashMap = HashMap::new(); 40 | self_closing_tags.insert("meta".to_string(), true); 41 | Html5Formatter { self_closing_tags } 42 | } 43 | 44 | fn item_to_html(&self, item: &ArenaItem, arena: &Arena) -> String { 45 | match &item.value { 46 | Haml::Text(text) => format!("{}\n", text.to_owned()), 47 | Haml::Comment(comment) => self.comment_to_html(item, arena), 48 | Haml::Element(_) => self.element_to_html(item, arena), 49 | Haml::InnerText(text) => format!("{}\n", text), 50 | Haml::Prolog(Some(prolog)) => prolog.to_owned(), 51 | Haml::ConditionalComment(_, _) => self.conditional_comment_to_html(item, arena), 52 | _ => String::new(), 53 | } 54 | } 55 | fn comment_to_html(&self, item: &ArenaItem, arena: &Arena) -> String { 56 | let mut html = String::new(); 57 | if let Haml::Comment(line) = &item.value { 58 | html.push_str(&format!(""); 70 | html 71 | } 72 | 73 | fn conditional_comment_to_html(&self, item: &ArenaItem, arena: &Arena) -> String { 74 | let mut html = String::new(); 75 | if let Haml::ConditionalComment(ws, value) = &item.value { 76 | html.push_str(&format!("") 82 | } 83 | html 84 | } 85 | 86 | fn element_to_html(&self, item: &ArenaItem, arena: &Arena) -> String { 87 | let mut html = String::new(); 88 | if let Haml::Element(el) = &item.value { 89 | html.push_str(&format!("<{}", el.name().unwrap())); 90 | 91 | for key in el.attributes().iter() { 92 | if let Some(value) = el.get_attribute(key) { 93 | if key.trim() == "checked" && value == "true" { 94 | html.push_str(" checked"); 95 | } else { 96 | match value.is_empty() { 97 | false => html.push_str(&format!(" {}='{}'", key.trim(), value)), 98 | true => html.push_str(&format!(" {}", key.trim())), 99 | } 100 | } 101 | } 102 | } 103 | 104 | html.push('>'); 105 | if !el.self_close && !self.self_closing_tags.contains_key(&el.name().unwrap()) { 106 | let mut has_inline_text = false; 107 | if let Some(text) = &el.inline_text { 108 | html.push_str(&format!("{}", text)); 109 | has_inline_text = true; 110 | } 111 | 112 | if item.children.len() > 0 { 113 | let mut index = 0; 114 | if Some("pre".to_owned()) != el.name() 115 | && Some("textarea".to_owned()) != el.name() && !el.whitespace_removal_inside 116 | { 117 | html.push('\n'); 118 | } 119 | for c in item.children.iter() { 120 | let i = arena.item(*c); 121 | html.push_str(&self.item_to_html(i, arena)); 122 | } 123 | } 124 | if el.whitespace_removal_inside { 125 | html = html.trim_end().to_string(); 126 | } 127 | if Some("pre".to_owned()) == el.name() || Some("textarea".to_owned()) == el.name() { 128 | html = html.trim_end().to_owned(); 129 | } 130 | if Some("input".to_owned()) != el.name() { 131 | html.push_str(&format!("", el.name().unwrap())); 132 | if item.children.len() > 0 { 133 | if !el.whitespace_removal_outside { 134 | html.push('\n'); 135 | } 136 | } else if has_inline_text && item.parent != 0 { 137 | html.push('\n'); 138 | } else if item.parent != 0 { 139 | html.push('\n'); 140 | } 141 | } 142 | } 143 | } 144 | html 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/formatter/xhtml_formatter.rs: -------------------------------------------------------------------------------- 1 | use crate::arena::{Arena, ArenaItem}; 2 | use crate::formatter::HtmlFormatter; 3 | use crate::parser::Haml; 4 | 5 | use std::collections::HashMap; 6 | 7 | #[derive(Debug)] 8 | pub struct XHtmlFormatter { 9 | self_closing_tags: HashMap, 10 | } 11 | 12 | impl HtmlFormatter for XHtmlFormatter { 13 | fn generate(&self, arena: &Arena) -> String { 14 | let root = arena.root(); 15 | let mut html = String::new(); 16 | for child in &root.children { 17 | let item = arena.item(*child); 18 | match &item.value { 19 | Haml::SilentComment(_) => (), 20 | Haml::Element(_) => html.push_str(&self.element_to_html(item, arena)), 21 | Haml::Comment(_) => html.push_str(&self.comment_to_html(item, arena)), 22 | Haml::Text(text) => html.push_str(&format!("{}\n", text.to_owned())), 23 | Haml::InnerText(text) => html.push_str(&text), 24 | Haml::Prolog(prolog) => html.push_str(&self.prolog_to_html(prolog)), 25 | _ => (), 26 | } 27 | } 28 | html.trim().to_owned() 29 | } 30 | } 31 | 32 | impl XHtmlFormatter { 33 | pub fn new() -> XHtmlFormatter { 34 | let mut self_closing_tags: HashMap = HashMap::new(); 35 | self_closing_tags.insert("input".to_string(), true); 36 | self_closing_tags.insert("meta".to_string(), true); 37 | XHtmlFormatter { self_closing_tags } 38 | } 39 | 40 | fn prolog_to_html(&self, value: &Option) -> &str { 41 | match value { 42 | None =>r#""#, 43 | Some(val) => { 44 | 45 | match val.to_lowercase().as_ref() { 46 | "strict" => r#""#, 47 | "frameset" => r#""#, 48 | "5" => "", 49 | "1.1" => r#""#, 50 | "basic" => r#""#, 51 | "mobile" => r#""#, 52 | "rdfa" => r#""#, 53 | "xml" => r#""#, 54 | _ => "" 55 | }}} 56 | } 57 | 58 | fn conditional_comment_to_html(&self, item: &ArenaItem, arena: &Arena) -> String { 59 | let mut html = String::new(); 60 | if let Haml::ConditionalComment(ws, value) = &item.value { 61 | html.push_str(&format!("") 67 | } 68 | html 69 | } 70 | 71 | fn item_to_html(&self, item: &ArenaItem, arena: &Arena) -> String { 72 | match &item.value { 73 | Haml::Text(text) => format!("{}\n", text.to_owned()), 74 | Haml::Comment(comment) => self.comment_to_html(item, arena), 75 | Haml::Element(_) => self.element_to_html(item, arena), 76 | Haml::InnerText(text) => text.to_owned(), 77 | Haml::Prolog(prolog) => self.prolog_to_html(prolog).to_string(), 78 | Haml::ConditionalComment(ws, val) => self.conditional_comment_to_html(item, arena), 79 | _ => String::new(), 80 | } 81 | } 82 | fn comment_to_html(&self, item: &ArenaItem, arena: &Arena) -> String { 83 | let mut html = String::new(); 84 | if let Haml::Comment(line) = &item.value { 85 | html.push_str(&format!(""); 97 | html 98 | } 99 | 100 | fn element_to_html(&self, item: &ArenaItem, arena: &Arena) -> String { 101 | let mut html = String::new(); 102 | if let Haml::Element(el) = &item.value { 103 | html.push_str(&format!("<{}", el.name().unwrap())); 104 | for key in el.attributes().iter() { 105 | if let Some(value) = el.get_attribute(key) { 106 | if key.trim() == "checked" && value == "true" { 107 | html.push_str(&format!(" checked='checked'")); 108 | } else { 109 | html.push_str(&format!(" {}='{}'", key, value.to_owned())); 110 | } 111 | } 112 | } 113 | 114 | if self.self_closing_tags.contains_key(&el.name().unwrap()) || el.self_close { 115 | html.push_str(" />"); 116 | return html; 117 | } else { 118 | html.push('>'); 119 | } 120 | if !el.self_close { 121 | if let Some(text) = &el.inline_text { 122 | html.push_str(&format!("{}", text.trim())); 123 | } 124 | if item.children.len() > 0 { 125 | let mut index = 0; 126 | if Some("pre".to_owned()) != el.name() 127 | && Some("textarea".to_owned()) != el.name() 128 | { 129 | html.push('\n'); 130 | } 131 | for c in item.children.iter() { 132 | let i = arena.item(*c); 133 | html.push_str(&self.item_to_html(i, arena)); 134 | } 135 | } 136 | if Some("pre".to_owned()) == el.name() || Some("textarea".to_owned()) == el.name() { 137 | html = html.trim_end().to_owned(); 138 | } 139 | if Some("input".to_owned()) != el.name() { 140 | html.push_str(&format!("\n", el.name().unwrap())); 141 | } 142 | } 143 | } 144 | html 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/arena/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::formatter::HtmlFormatter; 2 | use crate::parser::Haml; 3 | 4 | #[derive(Debug)] 5 | pub struct Arena { 6 | items: Vec, 7 | } 8 | 9 | #[derive(Debug)] 10 | pub struct ArenaItem { 11 | pub value: Haml, 12 | pub parent: usize, 13 | pub children: Vec, 14 | } 15 | 16 | impl ArenaItem { 17 | pub fn new(value: Haml, parent: usize) -> ArenaItem { 18 | ArenaItem { 19 | value, 20 | parent, 21 | children: vec![], 22 | } 23 | } 24 | } 25 | 26 | impl Arena { 27 | pub fn new() -> Arena { 28 | Arena { 29 | items: vec![ArenaItem::new(Haml::Root(), 0)], 30 | } 31 | } 32 | 33 | pub fn insert(&mut self, haml: Haml, parent: usize) -> usize { 34 | self.items.push(ArenaItem::new(haml, parent)); 35 | let idx: usize = self.items.len() - 1; 36 | if idx > 0 { 37 | self.items[parent].children.push(idx); 38 | } 39 | idx 40 | } 41 | 42 | pub fn parent(&self, i: usize) -> usize { 43 | self.items[i].parent 44 | } 45 | 46 | pub fn children_of(&self, i: usize) -> &Vec { 47 | &self.items[i].children 48 | } 49 | 50 | pub fn item(&self, i: usize) -> &ArenaItem { 51 | &self.items[i] 52 | } 53 | 54 | pub fn root(&self) -> &ArenaItem { 55 | &self.items[0] 56 | } 57 | 58 | pub fn from_whitespace(&self, start_index: usize, ws: usize) -> usize { 59 | let mut idx = start_index; 60 | let mut parent = start_index; 61 | 62 | loop { 63 | let i = &self.items[idx]; 64 | match &i.value { 65 | Haml::Element(ref el) => { 66 | if el.whitespace == 0 && ws == 0 { 67 | parent = 0; 68 | break; 69 | } 70 | if el.whitespace < ws { 71 | parent = idx; 72 | break; 73 | } 74 | } 75 | Haml::SilentComment(whitespace) => { 76 | if *whitespace == 0 as usize && ws == 0 { 77 | break; 78 | } 79 | if *whitespace < ws { 80 | parent = idx; 81 | break; 82 | } 83 | }, 84 | Haml::ConditionalComment(whitespace, _) => { 85 | if *whitespace == 0 as usize && ws == 0 { 86 | break; 87 | } 88 | if *whitespace < ws { 89 | parent = idx; 90 | break; 91 | } 92 | } 93 | _ => idx = i.parent, 94 | } 95 | } 96 | parent 97 | } 98 | 99 | // pub fn to_html(&self) -> String { 100 | // self.formatter.generate(self) 101 | // let mut html = String::new(); 102 | // let root = self.root(); 103 | // for child in root.children.iter() { 104 | // let item = self.item(*child); 105 | // match item.value { 106 | // Haml::SilentComment(_) => (), 107 | // _ => html.push_str(&self.formatter.generate(item)) 108 | // } 109 | // // html.push_str(&self.item_to_html(self.item(*child))); 110 | // } 111 | // html.trim().to_owned() 112 | // } 113 | 114 | // fn item_to_html(&self, item: &ArenaItem) -> String { 115 | // match &item.value { 116 | // Haml::Text(text) => format!("{}\n",text.to_owned()), 117 | // Haml::Comment(comment) => self.comment_to_html(&item), 118 | // Haml::Element(_) => self.element_to_html(&item), 119 | // Haml::InnerText(text) => text.to_owned(), 120 | // Haml::Prolog(Some(prolog)) => prolog.to_owned(), 121 | // _ => String::new(), 122 | // } 123 | // } 124 | 125 | // fn comment_to_html(&self, item: &ArenaItem) -> String { 126 | // let mut html = String::new(); 127 | // if let Haml::Comment(line) = &item.value { 128 | // html.push_str(&format!(""); 140 | // html 141 | // } 142 | // fn element_to_html(&self, item: &ArenaItem) -> String { 143 | // if let Haml::Element(el) = &item.value { 144 | 145 | // html.push_str(&format!("<{}", el.name().unwrap())); 146 | // for key in el.attributes().iter() { 147 | // if let Some(value) = el.get_attribute(key) { 148 | // // this needs to be separated eventuallyas this is html5 specific 149 | // if key.trim() == "checked" { 150 | // match &self.format { 151 | // Format::Html5() => if value == "true" { 152 | // html.push_str(" checked"); 153 | // }, 154 | // _ => match value.as_ref() { 155 | // "true" => html.push_str(" checked='checked'"), 156 | // _ => html.push_str(&format!(" checked='{}'", value)), 157 | // } 158 | // } 159 | // } else { 160 | // html.push_str(&format!(" {}='{}'", key, value)); 161 | // } 162 | // } 163 | // } 164 | 165 | // if Some("input".to_owned()) == el.name() || Some("meta".to_owned()) == el.name() { 166 | // match self.format { 167 | // Format::XHtml() => html.push_str(" />"), 168 | // _ => html.push('>'), 169 | // } 170 | // } else { 171 | // html.push('>'); 172 | // } 173 | // if !el.self_close { 174 | // if let Some(text) = &el.inline_text { 175 | // html.push_str(&format!("{}", text.trim())); 176 | // } 177 | // if item.children.len() > 0 { 178 | // let mut index = 0; 179 | // if Some("pre".to_owned()) != el.name() && Some("textarea".to_owned()) != el.name() { 180 | // html.push('\n'); 181 | // } 182 | // for c in item.children.iter() { 183 | // let i = self.item(*c); 184 | // html.push_str(&self.item_to_html(i)); 185 | // } 186 | // } 187 | // if Some("pre".to_owned()) == el.name() || Some("textarea".to_owned()) == el.name() { 188 | // html =html.trim_end().to_owned(); 189 | // } 190 | // if Some("input".to_owned()) != el.name() { 191 | // html.push_str(&format!("\n", el.name().unwrap())); 192 | // } 193 | // } 194 | // } 195 | // html 196 | // } 197 | } 198 | -------------------------------------------------------------------------------- /tests/tests.rs: -------------------------------------------------------------------------------- 1 | // extern crate haml; 2 | // extern crate serde; 3 | // extern crate serde_derive; 4 | // extern crate serde_json; 5 | 6 | // use serde_json::Error; 7 | 8 | // mod common; 9 | // use common::{TestCollection, Tests}; 10 | 11 | // fn load_json() -> Result { 12 | // let json = include_str!("tests.json"); 13 | // let tests: Tests = serde_json::from_str(&json)?; 14 | // Ok(tests) 15 | // } 16 | 17 | // /* 18 | // * Run all non-optional tests in the json file 19 | // */ 20 | // #[test] 21 | // fn all() -> Result<(), Error> { 22 | // let tests = load_json()?; 23 | // tests.run(); 24 | // Ok(()) 25 | // } 26 | 27 | // #[test] 28 | // fn section() -> Result<(), Error> { 29 | // let tests = load_json()?; 30 | // tests.run_test_section("tags with HTML-style attributes"); 31 | // tests.run_test_section("whitespace removal"); 32 | // tests.run_test_section("conditional comments"); 33 | // tests.run_test_section("whitespace preservation"); 34 | // tests.run_test_section("boolean attributes"); 35 | // tests.run_test_section("HTML escaping"); 36 | // tests.run_test_section("Ruby-style interpolation"); 37 | // tests.run_test_section("internal filters"); 38 | 39 | // tests.run_test_section("markup comments"); 40 | // tests.run_test_section("silent comments"); 41 | // tests.run_test_section("tags with Ruby-style attributes"); 42 | 43 | // tests.run_test_section("tags with nested content"); 44 | // tests.run_test_section("tags with inline content"); 45 | // tests.run_test_section("tags with unusual CSS identifiers"); 46 | // tests.run_test_section("tags with unusual HTML characters"); 47 | // tests.run_test_section("basic Haml tags and CSS"); 48 | // tests.run_test_section("headers"); 49 | // Ok(()) 50 | // } 51 | 52 | 53 | // #[test] 54 | // fn double() -> Result<(), Error> { 55 | // let tests = load_json()?; 56 | // tests.run_test_by_name("a tag with '>' appended and nested content"); 57 | // tests.run_test_by_name("Inline content multiple simple tags"); 58 | // Ok(()) 59 | // } 60 | 61 | // /* 62 | // * This is used for testing one specific test from the JSON file at a time. 63 | // * Pass in the key for the test data to run_test_by_name and it will execute 64 | // * that given test 65 | // */ 66 | // #[test] 67 | // fn single() -> Result<(), Error> { 68 | // let tests = load_json()?; 69 | // // tests.run_test_by_name("a self-closing tag (HTML4)"); 70 | // tests.run_test_by_name("HTML-style attributes separated with newlines"); 71 | // Ok(()) 72 | // } 73 | 74 | // #[test] 75 | // fn completed() -> Result<(), Error> { 76 | // let tests = load_json()?; 77 | // tests.run_test_by_name("HTML-style 'class' as an attribute"); 78 | // tests.run_test_by_name("an HTML 4 frameset doctype"); 79 | // tests.run_test_by_name("HTML-style tag with a CSS id and 'id' as an attribute"); 80 | // tests.run_test_by_name("an HTML 5 XML prolog (silent)"); 81 | // tests.run_test_by_name("an HTML 5 doctype"); 82 | // tests.run_test_by_name("an XHTML 1.1 doctype"); 83 | // tests.run_test_by_name("HTML-style multiple attributes"); 84 | // tests.run_test_by_name("an XHTML default (transitional) doctype"); 85 | // tests.run_test_by_name("HTML-style tag with an atomic attribute"); 86 | // tests.run_test_by_name("boolean attribute with XHTML"); 87 | // tests.run_test_by_name("a self-closing tag ('/' modifier + HTML5)"); 88 | // tests.run_test_by_name("a class with underscores"); 89 | // tests.run_test_by_name("inside a textarea tag"); 90 | // tests.run_test_by_name("boolean attribute with HTML"); 91 | // tests.run_test_by_name("a multiply nested silent comment"); 92 | // tests.run_test_by_name("a nested markup comment nested markup comment"); 93 | // tests.run_test_by_name("Inline content multiple simple tags"); 94 | // tests.run_test_by_name("Inline content tag with CSS"); 95 | // tests.run_test_by_name("Inline content simple tag"); 96 | // tests.run_test_by_name("a class with dashes"); 97 | // tests.run_test_by_name("a class with underscores"); 98 | // tests.run_test_by_name("an all-numeric class"); 99 | // tests.run_test_by_name("a tag with PascalCase"); 100 | // tests.run_test_by_name("Ruby-style attributes separated with newlines"); 101 | // tests.run_test_by_name("a tag with colons"); 102 | // tests.run_test_by_name("inside a pre tag"); 103 | // tests.run_test_by_name("a tag with underscores"); 104 | // tests.run_test_by_name("an inline markup comment"); 105 | // tests.run_test_by_name("a simple Haml tag"); 106 | // tests.run_test_by_name("a tag with a CSS class"); 107 | // tests.run_test_by_name("a tag with multiple CSS classes"); 108 | // tests.run_test_by_name("a tag with a CSS id"); 109 | // tests.run_test_by_name("a tag with multiple CSS id's"); 110 | // tests.run_test_by_name("a tag with a class followed by an id"); 111 | // tests.run_test_by_name("a tag with an id followed by a class"); 112 | // tests.run_test_by_name("an implicit div with a CSS id"); 113 | // tests.run_test_by_name("an implicit div with a CSS class"); 114 | // tests.run_test_by_name("multiple simple Haml tags"); 115 | // tests.run_test_by_name("a tag with dashes"); 116 | // tests.run_test_by_name("a tag with camelCase"); 117 | // tests.run_test_by_name("code following '&='"); 118 | // tests.run_test_by_name("an XHTML 1.1 basic doctype"); 119 | // Ok(()) 120 | // } 121 | // // #[test] 122 | // // fn completed_nested_content() -> Result<(), Error> { 123 | // // let tests = load_json()?; 124 | // // tests.run_test_by_name("Nested content tag with CSS"); 125 | // // Ok(()) 126 | // // } 127 | // // #[test] 128 | // // fn completed_comments() -> Result<(), Error> { 129 | // // let tests = load_json()?; 130 | 131 | // // tests.run_test_by_name("a nested markup comment nested markup comment"); 132 | // // tests.run_test_by_name("an inline markup comment"); 133 | // // tests.run_test_by_name("a multiply nested silent comment with inconsistent indents"); 134 | 135 | // // Ok(()) 136 | // // } 137 | 138 | // // #[test] 139 | // // fn completed_text() -> Result<(), Error> { 140 | // // let tests = load_json()?; 141 | 142 | // // tests.run_test_by_name("inside a textarea tag"); 143 | 144 | // // Ok(()) 145 | // // } 146 | 147 | // // #[test] 148 | // // fn completed_tags() -> Result<(), Error> { 149 | // // let tests = load_json()?; 150 | 151 | // // tests.run_test_by_name("a self-closing tag (XHTML)"); 152 | // // tests.run_test_by_name("a tag with multiple CSS classes"); 153 | 154 | // // Ok(()) 155 | // // } 156 | 157 | // // #[test] 158 | // // fn completed_boolean_attributes() -> Result<(), Error> { 159 | // // let tests = load_json()?; 160 | 161 | // // tests.run_test_by_name("boolean attribute with HTML"); 162 | // // tests.run_test_by_name("boolean attribute with XHTML"); 163 | // // Ok(()) 164 | // // } 165 | 166 | // // #[test] 167 | // // fn completed_html_style_attributes() -> Result<(), Error> { 168 | // // let tests = load_json()?; 169 | 170 | // // tests.run_test_by_name("HTML-style multiple attributes"); 171 | // // Ok(()) 172 | // // } 173 | 174 | // // #[test] 175 | // // fn completed_filters() -> Result<(), Error> { 176 | // // let tests = load_json()?; 177 | 178 | // // tests.run_test_by_name("content in a 'css' filter (HTML)"); 179 | 180 | // // Ok(()) 181 | // // } 182 | -------------------------------------------------------------------------------- /src/parser/mod.rs: -------------------------------------------------------------------------------- 1 | mod doctype; 2 | pub mod element; 3 | 4 | use crate::arena::Arena; 5 | use crate::Format; 6 | 7 | 8 | pub fn parse(haml: &str, format: Format) -> Arena { 9 | let mut arena = Arena::new(); 10 | for (idx, ch) in haml.char_indices() { 11 | 12 | } 13 | arena 14 | } 15 | // mod doctype; 16 | // pub mod element; 17 | 18 | // use crate::arena::Arena; 19 | // use crate::formatter::html4_formatter::Html4Formatter; 20 | // use crate::formatter::html5_formatter::Html5Formatter; 21 | // use crate::formatter::xhtml_formatter::XHtmlFormatter; 22 | // use crate::formatter::xml_formatter::XmlFormatter; 23 | // use crate::formatter::HtmlFormatter; 24 | // use crate::regex::{ 25 | // conditional_comment, prolog, sanitize, silent_comment, COMMENT_REGEX, TEXT_REGEX, 26 | // }; 27 | // use crate::Format; 28 | // use doctype::Doctype; 29 | // use element::{Element, ElementType}; 30 | // use regex::Regex; 31 | // use std::collections::HashMap; 32 | 33 | // fn sanitize_html(line: &str) -> Option { 34 | // let r = Regex::new(&sanitize()).unwrap(); 35 | // match r.is_match(line) { 36 | // true => { 37 | // let caps = r.captures(line).unwrap(); 38 | // match caps.name("text") { 39 | // Some(m) => Some( 40 | // m.as_str() 41 | // .replace("&", "&") 42 | // .replace("<", "<") 43 | // .replace(">", ">") 44 | // .replace("'", "'") 45 | // .replace("\"", """) 46 | // .to_owned(), 47 | // ), 48 | // None => None, 49 | // } 50 | // } 51 | // false => None, 52 | // } 53 | // } 54 | 55 | // fn text_from_string(line: &str) -> Option { 56 | // let r = Regex::new(TEXT_REGEX).unwrap(); 57 | // match r.captures(line) { 58 | // Some(m) => match m.name("text") { 59 | // Some(n) => Some(n.as_str().to_owned()), 60 | // None => None, 61 | // }, 62 | // None => None, 63 | // } 64 | // } 65 | 66 | // fn comment(line: &str) -> Option { 67 | // let r = Regex::new(COMMENT_REGEX).unwrap(); 68 | // match r.is_match(line) { 69 | // true => { 70 | // let caps = r.captures(line).unwrap(); 71 | // if let Some(c) = caps.name("comment") { 72 | // Some(c.as_str().to_owned()) 73 | // } else { 74 | // None 75 | // } 76 | // } 77 | // false => None, 78 | // } 79 | // } 80 | 81 | // fn silent(line: &str) -> Option { 82 | // let r = Regex::new(&silent_comment()).unwrap(); 83 | // match r.captures(line) { 84 | // Some(m) => match m.name("ws") { 85 | // Some(ws) => Some(Haml::SilentComment(ws.as_str().len())), 86 | // None => Some(Haml::SilentComment(0)), 87 | // }, 88 | // None => None, 89 | // } 90 | // } 91 | 92 | // fn conditional(line: &str) -> Option { 93 | // let r = Regex::new(&conditional_comment()).unwrap(); 94 | // let mut whitespace = 0; 95 | // let mut value = String::new(); 96 | // match r.captures(line) { 97 | // Some(m) => { 98 | // match m.name("ws") { 99 | // Some(ws) => whitespace = ws.as_str().len(), 100 | // None => whitespace = 0, 101 | // } 102 | // match m.name("val") { 103 | // Some(val) => value = val.as_str().to_string(), 104 | // None => (), 105 | // } 106 | // Some(Haml::ConditionalComment(whitespace, value)) 107 | // } 108 | // None => None, 109 | // } 110 | // } 111 | 112 | // #[derive(Clone, Debug, PartialEq)] 113 | // pub enum Haml { 114 | // Root(), 115 | // Element(Element), 116 | // Text(String), 117 | // InnerText(String), 118 | // Comment(String), 119 | // Prolog(Option), 120 | // SilentComment(usize), 121 | // ConditionalComment(usize, String), 122 | // } 123 | 124 | // pub struct Parser<'a> { 125 | // arena: Arena, 126 | // format: &'a Format, 127 | // } 128 | 129 | // impl<'a> Parser<'a> { 130 | // pub fn new(format: &'a Format) -> Parser { 131 | // Parser { 132 | // arena: Arena::new(), 133 | // format, 134 | // } 135 | // } 136 | 137 | // pub fn parse(&mut self, haml: &str) -> &Arena { 138 | // let mut previous_id = 0; 139 | // let mut first_line = true; 140 | // let prolog_regex = Regex::new(&prolog()).unwrap(); 141 | // for line in haml.lines() { 142 | // println!("Hi: {}", line); 143 | // // matches lines that start with &= 144 | // if let Some(sanitized_html) = sanitize_html(line) { 145 | // self.arena.insert(Haml::Text(sanitized_html), previous_id); 146 | // first_line = false; 147 | // } else if let Some(sc) = silent(line) { 148 | // previous_id = self.arena.insert(sc, previous_id); 149 | // first_line = false; 150 | // } else if let Some(cc) = conditional(line) { 151 | // previous_id = self.arena.insert(cc, previous_id); 152 | // first_line = false; 153 | // } else if prolog_regex.is_match(line) { 154 | // first_line = false; 155 | // let caps = prolog_regex.captures(line).unwrap(); 156 | // let value = match caps.name("type") { 157 | // Some(m) => match m.as_str() { 158 | // "" => None, 159 | // val => Some(val.to_string()), 160 | // }, 161 | // None => None, 162 | // }; 163 | // self.arena.insert(Haml::Prolog(value), previous_id); 164 | // } else if let Some(el) = Element::from_string(line) { 165 | // let ws = el.whitespace; 166 | // let element = Haml::Element(el); 167 | 168 | // if !first_line { 169 | // let p_id = self.arena.from_whitespace(previous_id, ws); 170 | // previous_id = self.arena.insert(element, p_id); 171 | // } else { 172 | // previous_id = self.arena.insert(element, 0); 173 | // first_line = false; 174 | // } 175 | // } else if let Some(comment) = comment(line) { 176 | // previous_id = self.arena.insert(Haml::Comment(comment), previous_id); 177 | // first_line = false; 178 | // } else if let Some(text_line) = text_from_string(line) { 179 | // first_line = false; 180 | // self.arena.insert(Haml::Text(text_line), previous_id); 181 | // } 182 | // } 183 | // &self.arena 184 | // } 185 | // } 186 | 187 | // // #[cfg(test)] 188 | // // mod test { 189 | // // use super::*; 190 | 191 | // // #[test] 192 | // // fn parse_text() { 193 | // // let haml = r"\= test"; 194 | // // let mut p = Parser::new(); 195 | // // let e = p.parse(haml); 196 | // // let id = e.root().children[0]; 197 | // // let item = e.item(id); 198 | // // match &item.value { 199 | // // Haml::Text(ref text) => assert_eq!("= test".to_owned(), *text), 200 | // // _ => panic!("failed"), 201 | // // } 202 | // // } 203 | 204 | // // #[test] 205 | // // fn parse_element_text() { 206 | // // let haml = "%hi\n\\value"; 207 | // // let mut p = Parser::new(); 208 | // // let e = p.parse(haml); 209 | // // let id = e.root().children[0]; 210 | // // let item = e.item(id); 211 | // // if let Haml::Element(el) = &item.value { 212 | // // let mut it = item.children.iter(); 213 | // // match it.next() { 214 | // // Some(child_id) => { 215 | // // let child = e.item(*child_id); 216 | // // match &child.value { 217 | // // Haml::Text(ref txt) => assert_eq!("value".to_owned(), *txt), 218 | // // _ => panic!("Failed"), 219 | // // } 220 | // // }, 221 | // // None => panic!("Failed"), 222 | // // } 223 | // // } 224 | // // } 225 | 226 | // // #[test] 227 | // // fn parse_element() { 228 | // // let haml = "%hi\n .box\n #b\n %span"; 229 | // // let mut p = Parser::new(); 230 | // // let e = p.parse(haml); 231 | // // let id = e.item(0).children[0]; 232 | // // let item = e.item(id); 233 | // // let el = match &item.value { 234 | // // Haml::Element(el) => el, 235 | // // _ => panic!("failed"), 236 | // // }; 237 | 238 | // // assert_eq!(Some("%hi".to_owned()), el.name); 239 | // // assert_eq!(ElementType::Other(), el.element_type); 240 | // // assert_eq!(0, el.whitespace); 241 | 242 | // // let mut it = item.children.iter(); 243 | // // let b = it.next().unwrap(); 244 | // // let bel = e.item(*b); 245 | // // let el2 = match &bel.value { 246 | // // Haml::Element(el) => el, 247 | // // _ => panic!("failed") 248 | // // }; 249 | // // assert_eq!(Some(".box".to_owned()), el2.name); 250 | // // assert_eq!(ElementType::Div(), el2.element_type); 251 | // // assert_eq!(2, el2.whitespace); 252 | 253 | // // let mut it2 = bel.children.iter(); 254 | // // let c = it2.next().unwrap(); 255 | // // let cel = e.item(*c); 256 | // // let el3 = match &cel.value { 257 | // // Haml::Element(el) => el, 258 | // // _ => panic!("failed") 259 | // // }; 260 | // // assert_eq!(Some("#b".to_owned()), el3.name); 261 | // // assert_eq!(ElementType::Div(), el3.element_type); 262 | // // assert_eq!(4, el3.whitespace); 263 | 264 | // // let mut d = it.next().unwrap(); 265 | // // let del = e.item(*d); 266 | // // let el4 = match &del.value { 267 | // // Haml::Element(el) => el, 268 | // // _ => panic!("failed") 269 | // // }; 270 | // // assert_eq!(Some("%span".to_owned()), el4.name); 271 | // // assert_eq!(ElementType::Other(), el4.element_type); 272 | // // assert_eq!(2, el4.whitespace); 273 | 274 | // // } 275 | // // } 276 | -------------------------------------------------------------------------------- /src/parser/element.rs: -------------------------------------------------------------------------------- 1 | // use https://regexr.com/ to test regex 2 | use crate::regex::{div, element, element_class_id, html_attribute, ruby_attribute}; 3 | use regex::{Captures, Regex}; 4 | use std::collections::{BTreeSet, HashMap}; 5 | 6 | #[derive(Debug, PartialEq, Clone)] 7 | pub enum ElementType { 8 | Div(), 9 | Other(), 10 | } 11 | 12 | #[derive(Clone, Debug, PartialEq)] 13 | pub struct Element { 14 | pub whitespace: usize, 15 | pub name: Option, 16 | pub element_type: ElementType, 17 | pub inline_text: Option, 18 | pub attributes: HashMap>, 19 | pub attribute_order: BTreeSet, 20 | pub self_close: bool, 21 | pub whitespace_removal_inside: bool, 22 | pub whitespace_removal_outside: bool, 23 | } 24 | 25 | impl Element { 26 | pub fn name(&self) -> Option { 27 | if let Some(name) = &self.name { 28 | Some(name.to_owned()) 29 | } else { 30 | None 31 | } 32 | } 33 | 34 | pub fn attributes(&self) -> &BTreeSet { 35 | &self.attribute_order 36 | } 37 | 38 | fn div_id_class(caps: &Captures) -> Option { 39 | let mut value = String::new(); 40 | let name = match caps.name("name") { 41 | Some(name) => name.as_str(), 42 | _ => "", 43 | }; 44 | let other = match caps.name("classid") { 45 | Some(classid) => classid.as_str(), 46 | _ => "", 47 | }; 48 | let output = format!("{}{}", name.trim(), other.trim()); 49 | match output.is_empty() { 50 | true => None, 51 | _ => Some(output), 52 | } 53 | } 54 | 55 | fn create_div<'a>(caps: &'a Captures) -> Element { 56 | let (mut attributes, order) = Element::handle_attributes(caps); 57 | Element { 58 | whitespace: Element::handle_whitespace(caps), 59 | name: Some("div".to_string()), 60 | element_type: ElementType::Div(), 61 | inline_text: Element::handle_inline_text(caps), 62 | attributes: attributes, 63 | attribute_order: order, 64 | self_close: Element::handle_self_close(caps), 65 | whitespace_removal_inside: false, 66 | whitespace_removal_outside: false, 67 | } 68 | } 69 | 70 | fn handle_self_close(caps: &Captures) -> bool { 71 | match caps.name("self_close") { 72 | Some(m) => match m.as_str() { 73 | "" => false, 74 | _ => true, 75 | }, 76 | None => false, 77 | } 78 | } 79 | fn handle_whitespace(caps: &Captures) -> usize { 80 | match caps.name("ws") { 81 | Some(ws) => ws.as_str().len(), 82 | None => 0, 83 | } 84 | } 85 | 86 | fn handle_name(caps: &Captures) -> (Option, bool, bool) { 87 | match caps.name("name") { 88 | Some(name) => { 89 | 90 | let mut val = name.as_str().to_owned(); 91 | let whitespace_removal_inside = val.ends_with("<"); 92 | let whitespace_removal_outside = val.ends_with(">"); 93 | let len = val.len(); 94 | if val.starts_with("%") { 95 | val = val[1..].to_owned(); 96 | if whitespace_removal_inside || whitespace_removal_outside { 97 | // len - 1 is the full string, len - 2 takes the last character off 98 | val = val[0..len - 2].to_string(); 99 | } 100 | } 101 | (Some(val.as_str().to_owned()), whitespace_removal_inside, whitespace_removal_outside) 102 | } 103 | None => (None, false, false) 104 | } 105 | } 106 | 107 | fn format_value(val: &str) -> String { 108 | match val.starts_with("\"") { 109 | true => val[1..val.len() - 1].to_owned(), 110 | false => val.to_owned(), 111 | } 112 | } 113 | 114 | fn handle_whitespace_removal(caps: &Captures) -> (bool, bool) { 115 | match caps.name("name") { 116 | None => (false, false), 117 | Some(m) => { 118 | let val = m.as_str(); 119 | let remove_surrounding_tag = val.ends_with(">"); 120 | let remove_inside_tag = val.ends_with("<"); 121 | (remove_inside_tag, remove_surrounding_tag) 122 | } 123 | } 124 | } 125 | 126 | fn add_to_map(map: &mut HashMap>, key: &str, value: &str) { 127 | let mut val = value.to_string(); 128 | if val.starts_with("'") { 129 | val = val[1..].to_string(); 130 | } 131 | if val.ends_with("'") { 132 | val = val[0..val.len() - 1].to_string(); 133 | } 134 | if let Some(values) = map.get_mut(key) { 135 | match key { 136 | "id" => { 137 | (*values).clear(); 138 | (*values).push(format!("id_{}", val)); 139 | } 140 | _ => (*values).push(val), 141 | } 142 | } else { 143 | map.insert(key.to_owned(), vec![val]); 144 | } 145 | } 146 | 147 | // parse the attributes by hand so that atomic values are covered 148 | fn parse_html_attributes( 149 | attributes: &str, 150 | map: &mut HashMap>, 151 | order: &mut BTreeSet, 152 | ) { 153 | if !attributes.is_empty() { 154 | let mut seen_key = false; 155 | let mut step = false; 156 | let mut attrs = String::new(); 157 | if attributes.starts_with("(") { 158 | attrs = attributes[1..].to_owned(); 159 | } 160 | 161 | if attrs.ends_with(")") { 162 | attrs = attrs[0..attrs.len() - 1].to_owned(); 163 | } 164 | 165 | let words: Vec<&str> = attrs.split(" ").collect(); 166 | let mut words_iter = words.iter(); 167 | let mut buffer = words_iter.next(); 168 | let mut temp = ""; 169 | loop { 170 | if let Some(wrd) = buffer { 171 | let split: Vec<&str> = wrd.split("=").collect(); 172 | if split.len() > 1 { 173 | let key = split.get(0).unwrap(); 174 | let value = split.get(1).unwrap(); 175 | Element::add_to_map(map, key, value); 176 | order.insert(key.to_string()); 177 | buffer = words_iter.next(); 178 | } else { 179 | if let Some(next) = words_iter.next() { 180 | match *next { 181 | "=" => match words_iter.next() { 182 | Some(w) => { 183 | Element::add_to_map(map, *wrd, w); 184 | order.insert((*wrd).to_owned()); 185 | buffer = words_iter.next(); 186 | } 187 | None => break, 188 | }, 189 | e => { 190 | map.insert((*wrd).to_owned(), vec![]); 191 | order.insert((*wrd).to_owned()); 192 | temp = e; 193 | buffer = Some(&temp); 194 | } 195 | } 196 | } else { 197 | map.insert((*wrd).to_owned(), vec![]); 198 | order.insert((*wrd).to_owned()); 199 | break; 200 | } 201 | } 202 | } else { 203 | break; 204 | } 205 | } 206 | } 207 | } 208 | fn handle_attrs( 209 | attributes: &str, 210 | attribute_regex: &str, 211 | separator: &str, 212 | map: &mut HashMap>, 213 | start_index: usize, 214 | order: &mut BTreeSet, 215 | ) { 216 | if !attributes.is_empty() { 217 | let r = Regex::new(attribute_regex).unwrap(); 218 | if r.is_match(attributes) { 219 | for attr in r.find_iter(attributes) { 220 | let mut attr_it = attr.as_str().split(separator); 221 | let id = attr_it.next(); 222 | let val = attr_it.next(); 223 | match (id, val) { 224 | (_, None) => continue, 225 | (None, _) => continue, 226 | (Some(i), Some(v)) => { 227 | if let Some(current_val) = 228 | map.get_mut(&i[start_index..].trim().to_owned()) 229 | { 230 | (*current_val).push(Element::format_value(v)); 231 | } else { 232 | order.insert(i[start_index..].to_owned()); 233 | map.insert( 234 | i[start_index..].to_owned(), 235 | vec![Element::format_value(v)], 236 | ); 237 | } 238 | } 239 | } 240 | } 241 | } 242 | } 243 | } 244 | 245 | fn handle_attributes(caps: &Captures) -> (HashMap>, BTreeSet) { 246 | let mut map: HashMap> = HashMap::new(); 247 | let mut order: BTreeSet = BTreeSet::new(); 248 | if let Some(c) = caps.name("name") { 249 | let name = c.as_str(); 250 | if name.starts_with(".") { 251 | map.insert("class".to_owned(), vec![name[1..].to_owned()]); 252 | order.insert("class".to_owned()); 253 | } else if name.starts_with("#") { 254 | map.insert("id".to_owned(), vec![name[1..].to_owned()]); 255 | order.insert("id".to_owned()); 256 | } 257 | } 258 | 259 | match caps.name("classid") { 260 | Some(c) => { 261 | let class_id_reg = Regex::new(&element_class_id()).unwrap(); 262 | let classid_value = c.as_str(); 263 | for ci in class_id_reg.find_iter(classid_value) { 264 | let value = classid_value[ci.start()..ci.end()].to_string(); 265 | match &value[0..1] { 266 | "#" => { 267 | map.insert("id".to_owned(), vec![value[1..].to_owned()]); 268 | order.insert("id".to_string()); 269 | } 270 | "." => { 271 | Element::add_to_map(&mut map, "class", &value[1..]); 272 | order.insert("class".to_string()); 273 | } 274 | _ => (), 275 | } 276 | } 277 | } 278 | None => (), 279 | } 280 | match caps.name("ruby_attributes") { 281 | Some(attributes) => Element::handle_attrs( 282 | attributes.as_str(), 283 | &ruby_attribute(), 284 | "=>", 285 | &mut map, 286 | 1, 287 | &mut order, 288 | ), 289 | None => (), 290 | } 291 | match caps.name("html_attributes") { 292 | Some(attributes) => { 293 | Element::parse_html_attributes(attributes.as_str(), &mut map, &mut order) 294 | } 295 | None => (), 296 | } 297 | 298 | (map, order) 299 | } 300 | 301 | fn handle_inline_text(caps: &Captures) -> Option { 302 | match caps.name("text") { 303 | Some(text) => Some(text.as_str().trim().to_owned()), 304 | None => None, 305 | } 306 | } 307 | 308 | fn create_element<'a>(caps: &'a Captures) -> Element { 309 | let (attributes, order) = Element::handle_attributes(caps); 310 | let (name, whitespace_removal_inside, whitespace_removal_outside) = Element::handle_name(caps); 311 | Element { 312 | whitespace: Element::handle_whitespace(caps), 313 | name, 314 | element_type: ElementType::Other(), 315 | inline_text: Element::handle_inline_text(caps), 316 | attributes: attributes, 317 | attribute_order: order, 318 | self_close: Element::handle_self_close(caps), 319 | whitespace_removal_inside, 320 | whitespace_removal_outside, 321 | } 322 | } 323 | 324 | pub fn from_string(haml: &str) -> Option { 325 | let element_regex = Regex::new(&element()).unwrap(); 326 | let div_regex = Regex::new(&div()); 327 | let element: Option = match Regex::new(&element()) { 328 | Ok(el) => match el.is_match(haml) { 329 | true => { 330 | let caps = el.captures(haml).unwrap(); 331 | Some(Element::create_element(&caps)) 332 | } 333 | false => None, 334 | }, 335 | _ => None, 336 | }; 337 | 338 | match element { 339 | Some(el) => Some(el), 340 | None => match div_regex { 341 | Ok(el) => match el.is_match(haml) { 342 | true => { 343 | let caps = el.captures(haml).unwrap(); 344 | Some(Element::create_div(&caps)) 345 | } 346 | false => None, 347 | }, 348 | _ => None, 349 | }, 350 | } 351 | } 352 | 353 | pub fn get_attribute(&self, name: &str) -> Option { 354 | if let Some(attributes) = self.attributes.get(name) { 355 | Some(attributes.join(" ").trim().to_owned()) 356 | } else { 357 | None 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /tests/tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers" : { 3 | 4 | "an XHTML XML prolog" : { 5 | "haml" : "!!! XML", 6 | "html" : "", 7 | "config" : { 8 | "format" : "xhtml" 9 | } 10 | }, 11 | 12 | "an XHTML default (transitional) doctype" : { 13 | "haml" : "!!!", 14 | "html" : "", 15 | "config" : { 16 | "format" : "xhtml" 17 | } 18 | }, 19 | 20 | "an XHTML 1.1 doctype" : { 21 | "haml" : "!!! 1.1", 22 | "html" : "", 23 | "config" : { 24 | "format" : "xhtml" 25 | } 26 | }, 27 | 28 | "an XHTML 1.2 mobile doctype" : { 29 | "haml" : "!!! mobile", 30 | "html" : "", 31 | "config" : { 32 | "format" : "xhtml" 33 | } 34 | }, 35 | 36 | "an XHTML 1.1 basic doctype" : { 37 | "haml" : "!!! basic", 38 | "html" : "", 39 | "config" : { 40 | "format" : "xhtml" 41 | } 42 | }, 43 | 44 | "an XHTML 1.0 frameset doctype" : { 45 | "haml" : "!!! frameset", 46 | "html" : "", 47 | "config" : { 48 | "format" : "xhtml" 49 | } 50 | }, 51 | 52 | "an HTML 5 doctype with XHTML syntax" : { 53 | "haml" : "!!! 5", 54 | "html" : "", 55 | "config" : { 56 | "format" : "xhtml" 57 | } 58 | }, 59 | 60 | "an HTML 5 XML prolog (silent)" : { 61 | "haml" : "!!! XML", 62 | "html" : "", 63 | "config" : { 64 | "format" : "html5" 65 | } 66 | }, 67 | 68 | "an HTML 5 doctype" : { 69 | "haml" : "!!!", 70 | "html" : "", 71 | "config" : { 72 | "format" : "html5" 73 | } 74 | }, 75 | 76 | "an HTML 4 XML prolog (silent)" : { 77 | "haml" : "!!! XML", 78 | "html" : "", 79 | "config" : { 80 | "format" : "html4" 81 | } 82 | }, 83 | 84 | "an HTML 4 default (transitional) doctype" : { 85 | "haml" : "!!!", 86 | "html" : "", 87 | "config" : { 88 | "format" : "html4" 89 | } 90 | }, 91 | 92 | "an HTML 4 frameset doctype" : { 93 | "haml" : "!!! frameset", 94 | "html" : "", 95 | "config" : { 96 | "format" : "html4" 97 | } 98 | }, 99 | 100 | "an HTML 4 strict doctype" : { 101 | "haml" : "!!! strict", 102 | "html" : "", 103 | "config" : { 104 | "format" : "html4" 105 | } 106 | } 107 | 108 | }, 109 | 110 | "basic Haml tags and CSS": { 111 | 112 | "a simple Haml tag" : { 113 | "haml" : "%p", 114 | "html" : "

" 115 | }, 116 | 117 | "a self-closing tag (XHTML)" : { 118 | "haml" : "%meta", 119 | "html" : "", 120 | "config" : { 121 | "format" : "xhtml" 122 | } 123 | }, 124 | 125 | "a self-closing tag (HTML4)" : { 126 | "haml" : "%meta", 127 | "html" : "", 128 | "config" : { 129 | "format" : "html4" 130 | } 131 | }, 132 | 133 | "a self-closing tag (HTML5)" : { 134 | "haml" : "%meta", 135 | "html" : "", 136 | "config" : { 137 | "format" : "html5" 138 | } 139 | }, 140 | 141 | "a self-closing tag ('/' modifier + XHTML)" : { 142 | "haml" : "%zzz/", 143 | "html" : "", 144 | "config" : { 145 | "format" : "xhtml" 146 | } 147 | }, 148 | 149 | "a self-closing tag ('/' modifier + HTML5)" : { 150 | "haml" : "%zzz/", 151 | "html" : "", 152 | "config" : { 153 | "format" : "html5" 154 | } 155 | }, 156 | 157 | "a tag with a CSS class" : { 158 | "haml" : "%p.class1", 159 | "html" : "

" 160 | }, 161 | 162 | "a tag with multiple CSS classes" : { 163 | "haml" : "%p.class1.class2", 164 | "html" : "

" 165 | }, 166 | 167 | "a tag with a CSS id" : { 168 | "haml" : "%p#id1", 169 | "html" : "

" 170 | }, 171 | 172 | "a tag with multiple CSS id's" : { 173 | "haml" : "%p#id1#id2", 174 | "html" : "

" 175 | }, 176 | 177 | "a tag with a class followed by an id" : { 178 | "haml" : "%p.class1#id1", 179 | "html" : "

" 180 | }, 181 | 182 | "a tag with an id followed by a class" : { 183 | "haml" : "%p#id1.class1", 184 | "html" : "

" 185 | }, 186 | 187 | "an implicit div with a CSS id" : { 188 | "haml" : "#id1", 189 | "html" : "
" 190 | }, 191 | 192 | "an implicit div with a CSS class" : { 193 | "haml" : ".class1", 194 | "html" : "
" 195 | }, 196 | 197 | "multiple simple Haml tags" : { 198 | "haml" : "%div\n %div\n %p", 199 | "html" : "
\n
\n

\n
\n
" 200 | } 201 | }, 202 | 203 | "tags with unusual HTML characters" : { 204 | 205 | "a tag with colons" : { 206 | "haml" : "%ns:tag", 207 | "html" : "" 208 | }, 209 | 210 | "a tag with underscores" : { 211 | "haml" : "%snake_case", 212 | "html" : "" 213 | }, 214 | 215 | "a tag with dashes" : { 216 | "haml" : "%dashed-tag", 217 | "html" : "" 218 | }, 219 | 220 | "a tag with camelCase" : { 221 | "haml" : "%camelCase", 222 | "html" : "" 223 | }, 224 | 225 | "a tag with PascalCase" : { 226 | "haml" : "%PascalCase", 227 | "html" : "" 228 | } 229 | }, 230 | 231 | "tags with unusual CSS identifiers" : { 232 | 233 | "an all-numeric class" : { 234 | "haml" : ".123", 235 | "html" : "
" 236 | }, 237 | 238 | "a class with underscores" : { 239 | "haml" : ".__", 240 | "html" : "
" 241 | }, 242 | 243 | "a class with dashes" : { 244 | "haml" : ".--", 245 | "html" : "
" 246 | } 247 | }, 248 | 249 | "tags with inline content" : { 250 | 251 | "Inline content simple tag" : { 252 | "haml" : "%p hello", 253 | "html" : "

hello

" 254 | }, 255 | 256 | "Inline content tag with CSS" : { 257 | "haml" : "%p.class1 hello", 258 | "html" : "

hello

" 259 | }, 260 | 261 | "Inline content multiple simple tags" : { 262 | "haml" : "%div\n %div\n %p text", 263 | "html" : "
\n
\n

text

\n
\n
" 264 | } 265 | }, 266 | 267 | "tags with nested content" : { 268 | 269 | "Nested content simple tag" : { 270 | "haml" : "%p\n hello", 271 | "html" : "

\nhello\n

" 272 | }, 273 | 274 | "Nested content tag with CSS" : { 275 | "haml" : "%p.class1\n hello", 276 | "html" : "

\nhello\n

" 277 | }, 278 | 279 | "Nested content multiple simple tags" : { 280 | "haml" : "%div\n %div\n %p\n text", 281 | "html" : "
\n
\n

\ntext\n

\n
\n
" 282 | } 283 | }, 284 | 285 | "tags with HTML-style attributes": { 286 | 287 | "HTML-style one attribute" : { 288 | "haml" : "%p(a='b')", 289 | "html" : "

" 290 | }, 291 | 292 | "HTML-style multiple attributes" : { 293 | "haml" : "%p(a='b' c='d')", 294 | "html" : "

" 295 | }, 296 | 297 | "HTML-style attributes separated with newlines" : { 298 | "haml" : "%p(a='b'\n c='d')", 299 | "html" : "

" 300 | }, 301 | 302 | "HTML-style interpolated attribute" : { 303 | "haml" : "%p(a=\"#{var}\")", 304 | "html" : "

", 305 | "locals" : { 306 | "var" : "value" 307 | }, 308 | "optional": true 309 | }, 310 | 311 | "HTML-style 'class' as an attribute" : { 312 | "haml" : "%p(class='class1')", 313 | "html" : "

" 314 | }, 315 | 316 | "HTML-style tag with a CSS class and 'class' as an attribute" : { 317 | "haml" : "%p.class2(class='class1')", 318 | "html" : "

" 319 | }, 320 | 321 | "HTML-style tag with 'id' as an attribute" : { 322 | "haml" : "%p(id='1')", 323 | "html" : "

" 324 | }, 325 | 326 | "HTML-style tag with a CSS id and 'id' as an attribute" : { 327 | "haml" : "%p#id(id='1')", 328 | "html" : "

" 329 | }, 330 | 331 | "HTML-style tag with a variable attribute" : { 332 | "haml" : "%p(class=var)", 333 | "html" : "

", 334 | "locals" : { 335 | "var" : "hello" 336 | }, 337 | "optional": true 338 | }, 339 | 340 | "HTML-style tag with a CSS class and 'class' as a variable attribute" : { 341 | "haml" : ".hello(class=var)", 342 | "html" : "
", 343 | "locals" : { 344 | "var" : "world" 345 | }, 346 | "optional": true 347 | }, 348 | 349 | "HTML-style tag multiple CSS classes (sorted correctly)" : { 350 | "haml" : ".z(class=var)", 351 | "html" : "
", 352 | "locals" : { 353 | "var" : "a" 354 | }, 355 | "optional": true 356 | }, 357 | 358 | "HTML-style tag with an atomic attribute" : { 359 | "haml" : "%a(flag)", 360 | "html" : "" 361 | } 362 | }, 363 | 364 | "tags with Ruby-style attributes": { 365 | 366 | "Ruby-style one attribute" : { 367 | "haml" : "%p{:a => 'b'}", 368 | "html" : "

", 369 | "optional" : true 370 | }, 371 | 372 | "Ruby-style attributes hash with whitespace" : { 373 | "haml" : "%p{ :a => 'b' }", 374 | "html" : "

", 375 | "optional" : true 376 | }, 377 | 378 | "Ruby-style interpolated attribute" : { 379 | "haml" : "%p{:a =>\"#{var}\"}", 380 | "html" : "

", 381 | "optional" : true, 382 | "locals" : { 383 | "var" : "value" 384 | } 385 | }, 386 | 387 | "Ruby-style multiple attributes" : { 388 | "haml" : "%p{ :a => 'b', 'c' => 'd' }", 389 | "html" : "

", 390 | "optional" : true 391 | }, 392 | 393 | "Ruby-style attributes separated with newlines" : { 394 | "haml" : "%p{ :a => 'b',\n 'c' => 'd' }", 395 | "html" : "

", 396 | "optional" : true 397 | }, 398 | 399 | "Ruby-style 'class' as an attribute" : { 400 | "haml" : "%p{:class => 'class1'}", 401 | "html" : "

", 402 | "optional" : true 403 | }, 404 | 405 | "Ruby-style tag with a CSS class and 'class' as an attribute" : { 406 | "haml" : "%p.class2{:class => 'class1'}", 407 | "html" : "

", 408 | "optional" : true 409 | }, 410 | 411 | "Ruby-style tag with 'id' as an attribute" : { 412 | "haml" : "%p{:id => '1'}", 413 | "html" : "

", 414 | "optional" : true 415 | }, 416 | 417 | "Ruby-style tag with a CSS id and 'id' as an attribute" : { 418 | "haml" : "%p#id{:id => '1'}", 419 | "html" : "

", 420 | "optional" : true 421 | }, 422 | 423 | "Ruby-style tag with a CSS id and a numeric 'id' as an attribute" : { 424 | "haml" : "%p#id{:id => 1}", 425 | "html" : "

", 426 | "optional" : true 427 | }, 428 | 429 | "Ruby-style tag with a variable attribute" : { 430 | "haml" : "%p{:class => var}", 431 | "html" : "

", 432 | "optional" : true, 433 | "locals" : { 434 | "var" : "hello" 435 | } 436 | }, 437 | 438 | "Ruby-style tag with a CSS class and 'class' as a variable attribute" : { 439 | "haml" : ".hello{:class => var}", 440 | "html" : "
", 441 | "optional" : true, 442 | "locals" : { 443 | "var" : "world" 444 | } 445 | }, 446 | 447 | "Ruby-style tag multiple CSS classes (sorted correctly)" : { 448 | "haml" : ".z{:class => var}", 449 | "html" : "
", 450 | "optional" : true, 451 | "locals" : { 452 | "var" : "a" 453 | } 454 | } 455 | }, 456 | 457 | "silent comments" : { 458 | 459 | "an inline silent comment" : { 460 | "haml" : "-# hello", 461 | "html" : "" 462 | }, 463 | 464 | "a nested silent comment" : { 465 | "haml" : "-#\n hello", 466 | "html" : "" 467 | }, 468 | 469 | "a multiply nested silent comment" : { 470 | "haml" : "-#\n %div\n foo", 471 | "html" : "" 472 | }, 473 | 474 | "a multiply nested silent comment with inconsistent indents" : { 475 | "haml" : "-#\n %div\n foo", 476 | "html" : "" 477 | } 478 | }, 479 | 480 | "markup comments" : { 481 | 482 | "an inline markup comment" : { 483 | "haml" : "/ comment", 484 | "html" : "" 485 | }, 486 | 487 | "a nested markup comment nested markup comment" : { 488 | "haml" : "/\n comment\n comment2", 489 | "html" : "" 490 | } 491 | }, 492 | 493 | "conditional comments": { 494 | "a conditional comment" : { 495 | "haml" : "/[if IE]\n %p a", 496 | "html" : "" 497 | } 498 | }, 499 | 500 | "internal filters": { 501 | 502 | "content in an 'escaped' filter" : { 503 | "haml" : ":escaped\n <&\">", 504 | "html" : "<&">", 505 | "optional" : true 506 | }, 507 | 508 | "content in a 'preserve' filter" : { 509 | "haml" : ":preserve\n hello\n\n%p", 510 | "html" : "hello \n

", 511 | "optional" : true 512 | }, 513 | 514 | "content in a 'plain' filter" : { 515 | "haml" : ":plain\n hello\n\n%p", 516 | "html" : "hello\n

", 517 | "optional" : true 518 | }, 519 | 520 | "content in a 'css' filter (XHTML)" : { 521 | "haml" : ":css\n hello\n\n%p", 522 | "html" : "\n

", 523 | "config" : { 524 | "format" : "xhtml" 525 | }, 526 | "optional" : true 527 | }, 528 | 529 | "content in a 'javascript' filter (XHTML)" : { 530 | "haml" : ":javascript\n a();\n%p", 531 | "html" : "\n

", 532 | "config" : { 533 | "format" : "xhtml" 534 | }, 535 | "optional" : true 536 | }, 537 | 538 | "content in a 'css' filter (HTML)" : { 539 | "haml" : ":css\n hello\n\n%p", 540 | "html" : "\n

", 541 | "config" : { 542 | "format" : "html5" 543 | }, 544 | "optional" : true 545 | }, 546 | 547 | "content in a 'javascript' filter (HTML)" : { 548 | "haml" : ":javascript\n a();\n%p", 549 | "html" : "\n

", 550 | "config" : { 551 | "format" : "html5" 552 | }, 553 | "optional" : true 554 | } 555 | }, 556 | 557 | "Ruby-style interpolation": { 558 | 559 | "interpolation inside inline content" : { 560 | "haml" : "%p #{var}", 561 | "html" : "

value

", 562 | "optional" : true, 563 | "locals" : { 564 | "var" : "value" 565 | } 566 | }, 567 | 568 | "no interpolation when escaped" : { 569 | "haml" : "%p \\#{var}", 570 | "html" : "

#{var}

", 571 | "optional" : true, 572 | "locals" : { 573 | "var" : "value" 574 | } 575 | }, 576 | 577 | "interpolation when the escape character is escaped" : { 578 | "haml" : "%p \\\\#{var}", 579 | "html" : "

\\value

", 580 | "optional" : true, 581 | "locals" : { 582 | "var" : "value" 583 | } 584 | }, 585 | 586 | "interpolation inside filtered content" : { 587 | "haml" : ":plain\n #{var} interpolated: #{var}", 588 | "html" : "value interpolated: value", 589 | "optional" : true, 590 | "locals" : { 591 | "var" : "value" 592 | } 593 | } 594 | }, 595 | 596 | "HTML escaping" : { 597 | 598 | "code following '&='" : { 599 | "haml" : "&= '<\"&>'", 600 | "html" : "<"&>", 601 | "optional": true 602 | }, 603 | 604 | "code following '=' when escape_haml is set to true" : { 605 | "haml" : "= '<\"&>'", 606 | "html" : "<"&>", 607 | "config" : { 608 | "escape_html" : "true" 609 | }, 610 | "optional": true 611 | }, 612 | 613 | "code following '!=' when escape_haml is set to true" : { 614 | "haml" : "!= '<\"&>'", 615 | "html" : "<\"&>", 616 | "config" : { 617 | "escape_html" : "true" 618 | }, 619 | "optional": true 620 | } 621 | 622 | }, 623 | 624 | "boolean attributes" : { 625 | 626 | "boolean attribute with XHTML" : { 627 | "haml" : "%input(checked=true)", 628 | "html" : "", 629 | "config" : { 630 | "format" : "xhtml" 631 | } 632 | }, 633 | 634 | "boolean attribute with HTML" : { 635 | "haml" : "%input(checked=true)", 636 | "html" : "", 637 | "config" : { 638 | "format" : "html5" 639 | } 640 | } 641 | }, 642 | 643 | "whitespace preservation" : { 644 | 645 | "following the '~' operator" : { 646 | "haml" : "~ \"Foo\\n
Bar\\nBaz
\"", 647 | "html" : "Foo\n
Bar
Baz
", 648 | "optional" : true 649 | }, 650 | 651 | "inside a textarea tag" : { 652 | "haml" : "%textarea\n hello\n hello", 653 | "html" : "" 654 | }, 655 | 656 | "inside a pre tag" : { 657 | "haml" : "%pre\n hello\n hello", 658 | "html" : "
hello\nhello
" 659 | } 660 | }, 661 | 662 | "whitespace removal" : { 663 | 664 | "a tag with '>' appended and inline content" : { 665 | "haml" : "%li hello\n%li> world\n%li again", 666 | "html" : "
  • hello
  • world
  • again
  • " 667 | }, 668 | 669 | "a tag with '>' appended and nested content" : { 670 | "haml" : "%li hello\n%li>\n world\n%li again", 671 | "html" : "
  • hello
  • \nworld\n
  • again
  • " 672 | }, 673 | 674 | "a tag with '<' appended" : { 675 | "haml" : "%p<\n hello\n world", 676 | "html" : "

    hello\nworld

    " 677 | } 678 | } 679 | } --------------------------------------------------------------------------------