├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── 0.hls └── 1.hls └── src ├── hyperlisp.pest └── main.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | */*.swp 3 | *.swp 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | [[package]] 4 | name = "block-buffer" 5 | version = "0.7.3" 6 | source = "registry+https://github.com/rust-lang/crates.io-index" 7 | checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" 8 | dependencies = [ 9 | "block-padding", 10 | "byte-tools", 11 | "byteorder", 12 | "generic-array", 13 | ] 14 | 15 | [[package]] 16 | name = "block-padding" 17 | version = "0.1.5" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" 20 | dependencies = [ 21 | "byte-tools", 22 | ] 23 | 24 | [[package]] 25 | name = "byte-tools" 26 | version = "0.3.1" 27 | source = "registry+https://github.com/rust-lang/crates.io-index" 28 | checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" 29 | 30 | [[package]] 31 | name = "byteorder" 32 | version = "1.4.2" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" 35 | 36 | [[package]] 37 | name = "digest" 38 | version = "0.8.1" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" 41 | dependencies = [ 42 | "generic-array", 43 | ] 44 | 45 | [[package]] 46 | name = "ezcli" 47 | version = "0.3.4" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "548c58c0db03f84c193bff64f9fe250a069078a1f5663587684e90745d3b4904" 50 | 51 | [[package]] 52 | name = "fake-simd" 53 | version = "0.1.2" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" 56 | 57 | [[package]] 58 | name = "generic-array" 59 | version = "0.12.3" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "c68f0274ae0e023facc3c97b2e00f076be70e254bc851d972503b328db79b2ec" 62 | dependencies = [ 63 | "typenum", 64 | ] 65 | 66 | [[package]] 67 | name = "hyperlisp" 68 | version = "0.1.0" 69 | dependencies = [ 70 | "ezcli", 71 | "pest", 72 | "pest_derive", 73 | ] 74 | 75 | [[package]] 76 | name = "maplit" 77 | version = "1.0.2" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" 80 | 81 | [[package]] 82 | name = "opaque-debug" 83 | version = "0.2.3" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" 86 | 87 | [[package]] 88 | name = "pest" 89 | version = "2.1.3" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" 92 | dependencies = [ 93 | "ucd-trie", 94 | ] 95 | 96 | [[package]] 97 | name = "pest_derive" 98 | version = "2.1.0" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" 101 | dependencies = [ 102 | "pest", 103 | "pest_generator", 104 | ] 105 | 106 | [[package]] 107 | name = "pest_generator" 108 | version = "2.1.3" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" 111 | dependencies = [ 112 | "pest", 113 | "pest_meta", 114 | "proc-macro2", 115 | "quote", 116 | "syn", 117 | ] 118 | 119 | [[package]] 120 | name = "pest_meta" 121 | version = "2.1.3" 122 | source = "registry+https://github.com/rust-lang/crates.io-index" 123 | checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" 124 | dependencies = [ 125 | "maplit", 126 | "pest", 127 | "sha-1", 128 | ] 129 | 130 | [[package]] 131 | name = "proc-macro2" 132 | version = "1.0.24" 133 | source = "registry+https://github.com/rust-lang/crates.io-index" 134 | checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" 135 | dependencies = [ 136 | "unicode-xid", 137 | ] 138 | 139 | [[package]] 140 | name = "quote" 141 | version = "1.0.8" 142 | source = "registry+https://github.com/rust-lang/crates.io-index" 143 | checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df" 144 | dependencies = [ 145 | "proc-macro2", 146 | ] 147 | 148 | [[package]] 149 | name = "sha-1" 150 | version = "0.8.2" 151 | source = "registry+https://github.com/rust-lang/crates.io-index" 152 | checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" 153 | dependencies = [ 154 | "block-buffer", 155 | "digest", 156 | "fake-simd", 157 | "opaque-debug", 158 | ] 159 | 160 | [[package]] 161 | name = "syn" 162 | version = "1.0.60" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081" 165 | dependencies = [ 166 | "proc-macro2", 167 | "quote", 168 | "unicode-xid", 169 | ] 170 | 171 | [[package]] 172 | name = "typenum" 173 | version = "1.12.0" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" 176 | 177 | [[package]] 178 | name = "ucd-trie" 179 | version = "0.1.3" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" 182 | 183 | [[package]] 184 | name = "unicode-xid" 185 | version = "0.2.1" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" 188 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hyperlisp" 3 | version = "0.1.0" 4 | authors = ["curlpipe <11898833+curlpipe@users.noreply.github.com>"] 5 | edition = "2018" 6 | 7 | [profile.release] 8 | lto = true 9 | panic = 'abort' 10 | 11 | [dependencies] 12 | ezcli = "0.3.4" 13 | pest = "2.1.3" 14 | pest_derive = "2.1.0" 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 curlpipe 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hyperlisp 2 | 3 | ## What the heck is hyperlisp? 4 | 5 | - It's a markup language designed for the web 6 | - It transpiles directly into html 7 | - It's written in Rust therefore it can transpile quickly 8 | - It's inspired by Lisp and isn't too verbose 9 | - It's free to use for any purpose you wish 10 | - Feel free to modify and play around with how it's made 11 | - It's only 145 lines of code (excluding comments and blank lines) 12 | - The code is well commented 13 | 14 | 15 | ## Installation 16 | 17 | You can run the following commands on linux to get it up and running 18 | ```sh 19 | git clone https://github.com/curlpipe/hyperlisp.git 20 | cd hyperlisp 21 | cargo build --release 22 | sudo cp target/release/hyperlisp /usr/bin/ 23 | ``` 24 | 25 | Or you can use the prebuilt binaries provided in the releases section and move it to `/usr/bin/` 26 | 27 | ## Conversion from hyperlisp to html 28 | After installation, you'll be able to convert hyperlisp files into html 29 | 30 | To show the help menu: 31 | ``` 32 | hyperlisp -h 33 | ``` 34 | 35 | To convert `index.hls` file into html and print it out: 36 | ``` 37 | hyperlisp -i index.hls 38 | ``` 39 | 40 | To convert `index.hls` file into `index.html`: 41 | ``` 42 | hyperlisp -i index.hls -o index.html 43 | ``` 44 | 45 | ## Syntax 46 | You can check out the examples folder to see some examples of how to write it. 47 | 48 | Below are some smaller snippets to help you quickly grasp the language. 49 | 50 | ### Single tags 51 | 52 | ```lisp 53 | (h1 Hello World) 54 | ``` 55 | 56 | ```html 57 |

Hello World

58 | ``` 59 | 60 | ### Nested tags 61 | ```lisp 62 | (h1 This is an example of (b Nested tags!) Pretty cool!) 63 | ``` 64 | 65 | ```html 66 |

This is an example of Nested tags! Pretty cool!

67 | ``` 68 | 69 | ### Attributes 70 | ```lisp 71 | (div id="background" class="container gradient" 72 | Here's a tag: (h1 This is a div) 73 | ) 74 | ``` 75 | 76 | ```html 77 |
78 | Here's a tag:

This is a div

79 |
80 | ``` 81 | 82 | ### Multiple nested tags 83 | ```lisp 84 | (body (h1 Tag number 1) (p Tag number 2)) 85 | ``` 86 | 87 | ```html 88 |

Tag number 1

Tag number 2

89 | ``` 90 | 91 | ### Comments 92 | ```lisp 93 | (h1 This is a heading, it is displayed) !(This is a comment and it's not displayed) 94 | ``` 95 | 96 | ```html 97 |

This is a heading, it is displayed

98 | ``` 99 | 100 | -------------------------------------------------------------------------------- /examples/0.hls: -------------------------------------------------------------------------------- 1 | (html 2 | (head 3 | (title HTML test!) 4 | ) 5 | (body 6 | (h1 Hello World!) 7 | (p This is an example of hyperlisp) 8 | (p You can nest tags (b style="color: blue" easily)) 9 | ) 10 | ) 11 | -------------------------------------------------------------------------------- /examples/1.hls: -------------------------------------------------------------------------------- 1 | (html lang="en" 2 | (head 3 | (title Hyperlisp) 4 | ) 5 | (body style="font-family: verdana;" 6 | (h1 Hyperlisp!) 7 | (hr) 8 | (p This is an example of (b style="color: blue;" hyperlisp)) 9 | (h2 What the hell is hyperlisp?) 10 | (ul 11 | (li It's a markup language designed for the web) 12 | (li It's written in Rust and can transpile very quickly) 13 | (li It's inspired by Lisp, providing a nice way to write webpages) 14 | (li It's free, open source and you can learn it pretty quickly) 15 | (li It'll alert you to any errors in your markup, making it easier to debug) 16 | ) 17 | (h2 How do I use it?) 18 | (p 19 | You can head to my 20 | (a href="https://github.com/curlpipe/hyperlisp" Github repository) 21 | where you'll find everything you need to get started 22 | ) 23 | (p I hope that this tool makes you more productive :D) 24 | (br) 25 | (p - Curlpipe) 26 | ) 27 | ) 28 | -------------------------------------------------------------------------------- /src/hyperlisp.pest: -------------------------------------------------------------------------------- 1 | // Root is the roots of the tree, contains all the tags 2 | root = { (WHITESPACE | comment | tag)* } 3 | 4 | // A tag is made up of an id, attributes and body 5 | tag = ${ "(" ~ id ~ (" " ~ attribute)* ~ (" "? ~ body)? ~ ")" } 6 | 7 | // An ID is a html element name, e.g. "h1" or "script" 8 | id = ${ (!(")" | "(" | " " | "!" | "\n" | "\t") ~ ANY)+ } 9 | 10 | // An attribute is where extra info is provided like styling and classes 11 | attribute = ${ name ~ "=" ~ "\"" ~ value ~ "\"" } 12 | 13 | // Name captures anythign that isn't a bracket, whitespace or an equals sign 14 | name = { (!("=" | "(" | ")") ~ ANY)* } 15 | 16 | // Value captures everything that isn't a quote 17 | value = { (!("\"") ~ ANY)* } 18 | 19 | // A body is made up of one or more comments, tags or text 20 | body = ${ (comment | tag | text)+ } 21 | 22 | // Text is made up of anything that isn't a bracket 23 | text = ${ (!(")" | "(") ~ ANY)+ } 24 | 25 | // Comments are ignored by the parser and don't make it to Rust 26 | comment = _{ "!(" ~ (!(")") ~ ANY)* ~ ")" } 27 | 28 | // Whitespace is made up of spaces, newlines and tabs, these are ignored 29 | WHITESPACE = _{ " " | "\n" | "\t" } 30 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | // Hyperlisp parser is a markup language for the web inspired by Lisp 2 | #![warn(clippy::all, clippy::pedantic)] 3 | 4 | // Import pest, a parsing library 5 | extern crate pest; 6 | #[macro_use] 7 | extern crate pest_derive; 8 | 9 | // Import other libraries 10 | use ezcli::{name::Name, named_flag, option}; 11 | use pest::iterators::Pair; 12 | use pest::Parser; 13 | use std::collections::HashMap; 14 | use std::{fmt, fs}; 15 | 16 | // These are html tags that don't require an end tag 17 | const VOID: [&str; 17] = [ 18 | "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", 19 | "track", "wbr", "command", "keygen", "menuitem", 20 | ]; 21 | 22 | // Create a parser from the grammar 23 | #[derive(Parser)] 24 | #[grammar = "hyperlisp.pest"] 25 | pub struct HyperlispParser; 26 | 27 | // Create an enum to allow representation as a tree structure 28 | #[derive(Debug)] 29 | pub enum Hyperlisp<'a> { 30 | Tag(&'a str, HashMap<&'a str, &'a str>, Vec>), 31 | Text(&'a str), 32 | } 33 | 34 | fn main() { 35 | // Provide command line options 36 | named_flag!(h, Name::new("help", "h")); 37 | option!(input); 38 | option!(output); 39 | // Show help message if needed 40 | if h { help(); } 41 | // Check for input file 42 | if let Some(input) = input { 43 | // An input file was provided, read and parse it 44 | let input = fs::read_to_string(input).unwrap(); 45 | let p = HyperlispParser::parse(Rule::root, &input); 46 | // Convert into the tree structure and then reformat as html 47 | let tree: String = root(p.unwrap().next().unwrap()) 48 | .iter() 49 | .map(|x| format!("{}\n", x)) 50 | .collect(); 51 | // Remove leading and trailing newline characters 52 | let tree = tree.trim_matches('\n'); 53 | // Check for output file 54 | if let Some(file) = output { 55 | // Output file was provided, save to file 56 | if fs::write(&file, tree).is_ok() { 57 | println!("File saved successfully to {}", file); 58 | } else { 59 | eprintln!("File failed to save to {}", file); 60 | } 61 | } else { 62 | // Output file wasn't provided, print tree 63 | println!("{}", tree); 64 | } 65 | } else { 66 | // Input file wasn't provided, print help 67 | help(); 68 | } 69 | } 70 | 71 | fn help() -> bool { 72 | // Function to print help message and exit successfully 73 | println!( 74 | "Hyperlisp v{} 75 | Usage: 76 | hyperlisp [options] 77 | Options: 78 | --help | -h : Show this help message 79 | --input [file] | -i [file]: Specify an input file 80 | --output [file] | -o [file]: Specify an output file 81 | ", 82 | env!("CARGO_PKG_VERSION") 83 | ); 84 | std::process::exit(0); 85 | } 86 | 87 | fn root(p: Pair) -> Vec { 88 | // root = { (WHITESPACE | comment | tag)* } 89 | let mut tags = vec![]; 90 | for t in p.into_inner() { 91 | tags.push(match t.as_rule() { 92 | Rule::comment => continue, 93 | Rule::tag => tag(t), 94 | _ => unreachable!(), 95 | }) 96 | } 97 | tags 98 | } 99 | 100 | fn tag(t: Pair) -> Hyperlisp { 101 | // tag = ${ "(" ~ id ~ (" " ~ attribute)* ~ (" "? ~ body)? ~ ")" } 102 | let mut t = t.into_inner(); 103 | let id = t.next().unwrap(); 104 | let mut attributes = HashMap::new(); 105 | let mut ats = if let Some(n) = t.next() { 106 | n 107 | } else { 108 | return Hyperlisp::Tag(id.as_str(), attributes, vec![]); 109 | }; 110 | while let Rule::attribute = ats.as_rule() { 111 | let mut p = ats.clone().into_inner(); 112 | let name = p.next().unwrap().as_str(); 113 | let val = p.next().unwrap().as_str(); 114 | attributes.insert(name, val); 115 | if let Some(n) = t.next() { 116 | ats = n; 117 | } else { 118 | break; 119 | } 120 | } 121 | Hyperlisp::Tag(id.as_str(), attributes, body(ats)) 122 | } 123 | 124 | fn body(b: Pair) -> Vec { 125 | // body = ${ (comment | tag | text)+ } 126 | let mut result = vec![]; 127 | for i in b.into_inner() { 128 | result.push(match i.as_rule() { 129 | Rule::tag => tag(i), 130 | Rule::text => Hyperlisp::Text(i.as_str()), 131 | Rule::comment => continue, 132 | Rule::name => return result, 133 | _ => unreachable!(), 134 | }) 135 | } 136 | result 137 | } 138 | 139 | // Implement display trait for hyperlisp to allow html creation 140 | impl<'a> fmt::Display for Hyperlisp<'a> { 141 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 142 | // Determine the type of tag 143 | match self { 144 | // This is just text, display it 145 | Self::Text(s) => write!(f, "{}", s), 146 | // This is a tag, display it 147 | Self::Tag(i, a, b) => { 148 | // Read attributes and convert to html 149 | let mut attributes = vec![]; 150 | for (k, v) in a { 151 | attributes.push(format!(" {}=\"{}\"", k, v)) 152 | } 153 | // Read body and convert to html 154 | let mut body = vec![]; 155 | for p in b { 156 | body.push(format!("{}", p)) 157 | } 158 | // Format the tag into html 159 | if VOID.contains(i) && body.is_empty() { 160 | // End tag isn't needed, don't include it 161 | write!(f, "<{id}{attr}>", id = i, attr = attributes.join(""),) 162 | } else { 163 | // End tag is needed here, include it 164 | write!( 165 | f, 166 | "<{id}{attr}>{body}", 167 | id = i, 168 | attr = attributes.join(""), 169 | body = body.join(""), 170 | ) 171 | } 172 | } 173 | } 174 | } 175 | } 176 | --------------------------------------------------------------------------------