├── .gitignore ├── .rspec ├── .rubocop.yml ├── .ruby-version ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console ├── emerald └── setup ├── circle.yml ├── config └── pre_commit.yml ├── cpp ├── .gitignore ├── .ruby-version ├── Gemfile ├── Gemfile.lock ├── Makefile ├── lib │ ├── catch.hpp │ ├── json.hpp │ ├── peglib.h │ └── utf8.h ├── requirements.txt ├── samples │ └── sample.emr ├── setup.py ├── src │ ├── grammar.cpp │ ├── grammar.hpp │ ├── grammar.peg │ ├── htmlencode.cpp │ ├── htmlencode.hpp │ ├── main.cpp │ ├── nodes │ │ ├── attribute.cpp │ │ ├── attribute.hpp │ │ ├── attributes.cpp │ │ ├── attributes.hpp │ │ ├── binary_expr.cpp │ │ ├── binary_expr.hpp │ │ ├── boolean.hpp │ │ ├── comment.cpp │ │ ├── comment.hpp │ │ ├── conditional.cpp │ │ ├── conditional.hpp │ │ ├── each.cpp │ │ ├── each.hpp │ │ ├── escaped.cpp │ │ ├── escaped.hpp │ │ ├── key_value_pair.cpp │ │ ├── key_value_pair.hpp │ │ ├── line.cpp │ │ ├── line.hpp │ │ ├── literal_new_line.cpp │ │ ├── literal_new_line.hpp │ │ ├── node.cpp │ │ ├── node.hpp │ │ ├── node_list.cpp │ │ ├── node_list.hpp │ │ ├── scope.cpp │ │ ├── scope.hpp │ │ ├── scope_fn.hpp │ │ ├── scoped_key_value_pairs.cpp │ │ ├── scoped_key_value_pairs.hpp │ │ ├── tag_statement.cpp │ │ ├── tag_statement.hpp │ │ ├── text_literal_content.cpp │ │ ├── text_literal_content.hpp │ │ ├── unary_expr.cpp │ │ ├── unary_expr.hpp │ │ ├── value_list.cpp │ │ ├── value_list.hpp │ │ ├── variable.cpp │ │ ├── variable.hpp │ │ ├── variable_name.cpp │ │ ├── variable_name.hpp │ │ ├── with.cpp │ │ └── with.hpp │ ├── preprocessor.cpp │ ├── preprocessor.hpp │ ├── scopes.peg │ ├── tokens.peg │ └── variables.peg ├── test │ ├── emerald_tests.cpp │ ├── general_tests.cpp │ ├── preprocessor_tests.cpp │ ├── scope_tests.cpp │ ├── templating_tests.cpp │ ├── test_helper.cpp │ └── test_helper.hpp └── util │ ├── .rubocop.yml │ ├── file_helper.rb │ ├── generate │ └── generator.rb ├── emerald-logo.png ├── emerald.gemspec ├── lib ├── emerald.rb └── emerald │ ├── grammar.rb │ ├── grammar │ ├── emerald.tt │ ├── scopes.tt │ ├── tokens.tt │ └── variables.tt │ ├── index.emr │ ├── nodes │ ├── attribute_list.rb │ ├── attributes.rb │ ├── base_scope_fn.rb │ ├── binary_expr.rb │ ├── boolean_expr.rb │ ├── comment.rb │ ├── each.rb │ ├── given.rb │ ├── line.rb │ ├── nested.rb │ ├── node.rb │ ├── pair_list.rb │ ├── root.rb │ ├── scope.rb │ ├── scope_fn.rb │ ├── tag_statement.rb │ ├── text_literal.rb │ ├── unary_expr.rb │ ├── unless.rb │ ├── value_list.rb │ ├── variable.rb │ ├── variable_name.rb │ └── with.rb │ ├── preprocessor.rb │ └── version.rb ├── sample.emr └── spec ├── emerald_spec.rb ├── functional ├── general_spec.rb ├── scope_spec.rb └── templating_spec.rb ├── preprocessor ├── emerald │ ├── events │ │ ├── events.emr │ │ └── form.emr │ └── general │ │ ├── attr.emr │ │ ├── html.emr │ │ ├── metas.emr │ │ ├── nested.emr │ │ ├── sample.emr │ │ └── scopes.emr ├── intermediate │ ├── events │ │ ├── events.txt │ │ └── form.txt │ └── general │ │ ├── attr.txt │ │ ├── html.txt │ │ ├── metas.txt │ │ ├── nested.txt │ │ ├── sample.txt │ │ └── scopes.txt └── preprocessor_spec.rb ├── spec_helper.rb └── treetop └── treetop_suite.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.bundle/ 3 | /.yardoc 4 | /Gemfile.lock 5 | /_yardoc/ 6 | /coverage/ 7 | /doc/ 8 | /pkg/ 9 | /spec/reports/ 10 | /tmp/ 11 | bin/pre-commit 12 | tags 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Metrics/LineLength: 2 | Max: 120 3 | 4 | Style/SpaceInsideHashLiteralBraces: 5 | EnforcedStyle: no_space 6 | 7 | Metrics/AbcSize: 8 | Enabled: false 9 | 10 | Metrics/MethodLength: 11 | Max: 30 12 | 13 | Style/DoubleNegation: 14 | Enabled: false 15 | 16 | Style/WordArray: 17 | Enabled: false 18 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.3.2 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in emerald.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Andrew McBurney, Dave Pagurek, Yu Chen Hu, Google Inc. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Drawing 3 |
4 | One templating language, for any stack. 5 |

6 | 7 | Build Status 8 | 9 | 10 | License 11 | 12 |

13 |

14 | 15 | ![Emerald Language](https://raw.githubusercontent.com/emerald-lang/emerald-emacs/master/emerald.png) 16 | 17 | # Usage 18 | ``` 19 | gem install emerald-lang 20 | emerald process some_html_file --beautify 21 | ``` 22 | 23 | # Contributing 24 | ## Setup 25 | ``` 26 | bundle install 27 | bundle exec rake setup 28 | ``` 29 | 30 | ## Running tests 31 | ``` 32 | bundle exec rake test 33 | ``` 34 | ## Pushing to Rubygems 35 | Update the `EMERALD_VERSION` constant, then: 36 | 37 | ``` 38 | rake release 39 | ``` 40 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | require 'pre-commit' 4 | 5 | RSpec::Core::RakeTask.new(:test) 6 | 7 | task :setup do 8 | sh 'pre-commit install' 9 | sh 'git config pre-commit.checks "[rubocop]"' 10 | end 11 | 12 | task :default => :test 13 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "emerald" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/emerald: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 5 | require 'emerald' 6 | 7 | Emerald::CLI.start(ARGV) 8 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | ruby: 3 | version: 4 | 2.3.2 5 | 6 | dependencies: 7 | pre: 8 | - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test && sudo apt-get update 9 | - sudo apt-get update; sudo apt-get install gcc-4.9; sudo apt-get install g++-4.9 10 | - sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.9 100 11 | - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-4.9 100 12 | - sudo apt-get install libboost-all-dev 13 | override: 14 | - bundle install: 15 | timeout: 240 16 | 17 | test: 18 | override: 19 | - bundle exec rake test 20 | - cd cpp && make test 21 | -------------------------------------------------------------------------------- /config/pre_commit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :checks_add: 3 | - :rubocop 4 | :checks_remove: 5 | - :rails 6 | -------------------------------------------------------------------------------- /cpp/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.slo 3 | *.lo 4 | *.o 5 | *.d 6 | *.obj 7 | 8 | # Precompiled Headers 9 | *.gch 10 | *.pch 11 | 12 | # Compiled Dynamic libraries 13 | *.so 14 | *.dylib 15 | *.dll 16 | *.dSYM 17 | 18 | # Fortran module files 19 | *.mod 20 | *.smod 21 | 22 | # Compiled Static libraries 23 | *.lai 24 | *.la 25 | *.a 26 | *.lib 27 | 28 | # Executables 29 | *.exe 30 | *.out 31 | *.app 32 | 33 | # personal notes 34 | todo.md 35 | 36 | # from g++ 37 | emerald 38 | emerald_test 39 | emerald.DSYM 40 | 41 | # From pip 42 | dist/ 43 | Emerald.egg-info/ 44 | -------------------------------------------------------------------------------- /cpp/.ruby-version: -------------------------------------------------------------------------------- 1 | 2.4.1 2 | -------------------------------------------------------------------------------- /cpp/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | ruby "2.4.1" 6 | 7 | gem "colorize" 8 | gem "rubocop" 9 | gem "thor" 10 | -------------------------------------------------------------------------------- /cpp/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | ast (2.3.0) 5 | colorize (0.8.1) 6 | parallel (1.11.2) 7 | parser (2.4.0.0) 8 | ast (~> 2.2) 9 | powerpack (0.1.1) 10 | rainbow (2.2.2) 11 | rake 12 | rake (12.0.0) 13 | rubocop (0.49.1) 14 | parallel (~> 1.10) 15 | parser (>= 2.3.3.1, < 3.0) 16 | powerpack (~> 0.1) 17 | rainbow (>= 1.99.1, < 3.0) 18 | ruby-progressbar (~> 1.7) 19 | unicode-display_width (~> 1.0, >= 1.0.1) 20 | ruby-progressbar (1.8.1) 21 | thor (0.19.4) 22 | unicode-display_width (1.3.0) 23 | 24 | PLATFORMS 25 | ruby 26 | 27 | DEPENDENCIES 28 | colorize 29 | rubocop 30 | thor 31 | 32 | RUBY VERSION 33 | ruby 2.4.1p111 34 | 35 | BUNDLED WITH 36 | 1.15.1 37 | -------------------------------------------------------------------------------- /cpp/Makefile: -------------------------------------------------------------------------------- 1 | CXX = g++ -std=c++11 2 | CXXFLAGS = -Wall -O -g -MMD 3 | 4 | # src/main.cpp 5 | CLI_OBJECT = src/main.o 6 | CLI_DEPEND = src/main.d 7 | 8 | # src/ 9 | SOURCE = $(filter-out src/main.cpp, $(wildcard src/*.cpp)) $(wildcard src/nodes/*.cpp) 10 | OBJECTS = ${SOURCE:.cpp=.o} 11 | DEPENDS = ${OBJECTS:.o=.d} 12 | 13 | # test/ 14 | TEST_SOURCE = $(wildcard test/*.cpp) 15 | TEST_OBJECTS = ${TEST_SOURCE:.cpp=.o} 16 | TEST_DEPENDS = ${TEST_OBJECTS:.o=.d} 17 | 18 | EXEC = emerald 19 | TEST_EXEC = emerald_test 20 | LIBS = -lboost_regex 21 | 22 | ${EXEC} : ${CLI_OBJECT} ${OBJECTS} 23 | ${CXX} ${CXXFLAGS} ${CLI_OBJECT} ${OBJECTS} -o ${EXEC} ${LIBS} 24 | 25 | ${TEST_EXEC}: ${OBJECTS} ${TEST_OBJECTS} 26 | ${CXX} ${CXXFLAGS} ${OBJECTS} ${TEST_OBJECTS} -o ${TEST_EXEC} ${LIBS} 27 | 28 | test: ${TEST_EXEC} 29 | ./${TEST_EXEC} 30 | 31 | clean: 32 | rm ${CLI_DEPEND} ${DEPENDS} ${TEST_DEPENDS} ${CLI_OBJECT} ${OBJECTS} ${TEST_OBJECTS} $(EXEC) ${TEST_EXEC} 33 | 34 | -include {DEPENDS} {TEST_DEPENDS} 35 | -------------------------------------------------------------------------------- /cpp/lib/utf8.h: -------------------------------------------------------------------------------- 1 | /* THIS FILE IS PUBLIC DOMAIN. NO WARRANTY OF ANY KIND. NO RIGHTS RESERVED. */ 2 | #ifndef UTF8_H_HEADERFILE 3 | #define UTF8_H_HEADERFILE 4 | 5 | #ifndef UTF32_INT_TYPE 6 | #define UTF32_INT_TYPE unsigned long 7 | #endif 8 | 9 | #ifndef true 10 | #define true 1 11 | #endif 12 | #ifndef false 13 | #define false 0 14 | #endif 15 | #ifndef bool 16 | #define bool int 17 | #endif 18 | 19 | static inline bool 20 | utf32_is_surrogate(UTF32_INT_TYPE cp) 21 | { 22 | return cp > 0xd7ff && cp < 0xe000; 23 | } 24 | 25 | static inline bool 26 | utf32_is_non_character(UTF32_INT_TYPE cp) 27 | { 28 | return ((0xfffeU == (0xffeffffeU & cp) || (cp >= 0xfdd0U && cp <= 0xfdefU))); 29 | } 30 | 31 | static inline bool 32 | utf32_is_valid(UTF32_INT_TYPE cp) 33 | { 34 | return cp < 0x10ffffU && !utf32_is_non_character(cp); 35 | } 36 | 37 | static inline unsigned 38 | utf8_encoded_len(UTF32_INT_TYPE cp) 39 | { 40 | if (cp < 0x80U) { 41 | return 1; 42 | } else if (cp < 0x800U) { 43 | return 2; 44 | } else if (!utf32_is_valid(cp) || utf32_is_surrogate(cp)) { 45 | return 0; 46 | } else if (cp < 0x10000U) { 47 | return 3; 48 | } else { 49 | return 4; 50 | } 51 | } 52 | 53 | static inline unsigned 54 | utf8_first_byte_length_hint(unsigned char ch) 55 | { 56 | switch (ch & ~0x0fU) { 57 | case 0x00: 58 | case 0x10: 59 | case 0x20: 60 | case 0x30: 61 | case 0x40: 62 | case 0x50: 63 | case 0x60: 64 | case 0x70: return 1; 65 | case 0xc0: return ch >= 0xc2 ? 2 : 0; 66 | case 0xd0: return 2; 67 | case 0xe0: return 3; 68 | case 0xf0: return ch <= 0xf4 ? 4 : 0; 69 | default: return 0; 70 | } 71 | } 72 | 73 | static inline bool 74 | utf8_first_byte_valid(unsigned char ch) 75 | { 76 | return 0 != utf8_first_byte_length_hint(ch); 77 | } 78 | 79 | static inline bool 80 | utf8_first_bytes_valid(unsigned char ch1, unsigned char ch2) 81 | { 82 | if (ch1 < 0x80) { 83 | return true; 84 | } else if (0x80 == (ch2 & 0xc0)) { 85 | /* 0x80..0xbf */ 86 | switch (ch1) { 87 | case 0xe0: return ch2 >= 0xa0; 88 | case 0xed: return ch2 <= 0x9f; 89 | case 0xf0: return ch2 >= 0x90; 90 | case 0xf4: return ch2 <= 0x8f; 91 | } 92 | return true; 93 | } 94 | return false; 95 | } 96 | 97 | /** 98 | * @return (UTF32_INT_TYPE-1 on failure. On success the decoded 99 | * Unicode codepoint is returned. 100 | */ 101 | static inline UTF32_INT_TYPE 102 | utf8_decode(const char *src, size_t size) 103 | { 104 | UTF32_INT_TYPE cp; 105 | unsigned n; 106 | 107 | if (0 == size) 108 | goto failure; 109 | 110 | cp = (unsigned char) *src; 111 | n = utf8_first_byte_length_hint(cp); 112 | if (1 != n) { 113 | unsigned char x; 114 | 115 | if (0 == n || n > size) 116 | goto failure; 117 | 118 | x = *++src; 119 | if (!utf8_first_bytes_valid(cp, x)) 120 | goto failure; 121 | 122 | n--; 123 | cp &= 0x3f >> n; 124 | 125 | for (;;) { 126 | cp = (cp << 6) | (x & 0x3f); 127 | if (--n == 0) 128 | break; 129 | x = *++src; 130 | if (0x80 != (x & 0xc0)) 131 | goto failure; 132 | } 133 | if (utf32_is_non_character(cp)) 134 | goto failure; 135 | } 136 | return cp; 137 | 138 | failure: 139 | return (UTF32_INT_TYPE) -1; 140 | } 141 | 142 | static inline unsigned 143 | utf8_encode(UTF32_INT_TYPE cp, char *buf) 144 | { 145 | unsigned n = utf8_encoded_len(cp); 146 | 147 | if (n > 0) { 148 | static const unsigned char first_byte[] = { 149 | 0xff, 0x00, 0xc0, 0xe0, 0xf0 150 | }; 151 | unsigned i = n; 152 | 153 | while (--i > 0) { 154 | buf[i] = (cp & 0x3f) | 0x80; 155 | cp >>= 6; 156 | } 157 | buf[0] = cp | first_byte[n]; 158 | } 159 | return n; 160 | } 161 | 162 | #endif /* UTF8_H_HEADERFILE */ 163 | /* vi: set ai et ts=2 sts=2 sw=2 cindent: */ 164 | -------------------------------------------------------------------------------- /cpp/requirements.txt: -------------------------------------------------------------------------------- 1 | cpplint 2 | -------------------------------------------------------------------------------- /cpp/samples/sample.emr: -------------------------------------------------------------------------------- 1 | * Emerald Language 2 | 3 | doctype html 4 | 5 | html 6 | head 7 | styles 8 | "css/main.css" 9 | "css/vendor/bootstrap.min.css" 10 | 11 | scripts 12 | "js/script.js" 13 | "js/other_script.js" 14 | 15 | style 16 | var black = #333 17 | var blue = #0066ff 18 | 19 | body 20 | header 21 | h1 Emerald 22 | h2 An html5 markup language designed with event driven 23 | applications in mind. 24 | 25 | main 26 | section 27 | h1 Why use Emerald? 28 | p Emerald allows you to scope events and styles to html 29 | elements in an elegant, clean way. 30 | 31 | figure 32 | figcaption Here's an example of elements scoped in a 33 | button here. 34 | 35 | button Click me. ( 36 | click -> console.log("I was clicked!") 37 | hover -> console.log("I was hovered!") 38 | ) 39 | 40 | footer ( 41 | hover -> 42 | this.border = 1px solid @blue 43 | this.text-shadow = 0px 0px 8px 2px rgba(0,0,0,0.3) 44 | ) 45 | p Like what you see? Check out the docs for more samples. 46 | -------------------------------------------------------------------------------- /cpp/setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Emerald, the language agnostic templating engine. 3 | Copyright 2016-2017, Emerald Language (MIT) 4 | """ 5 | 6 | from setuptools import setup 7 | 8 | setup( 9 | name='Emerald', 10 | 11 | version='1.0.0', 12 | 13 | description='Emerald, the language agnostic templating engine.', 14 | 15 | url='https://github.com/emerald-lang/emerald', 16 | 17 | author='Andrew Robert McBurney, Dave Pagurek van Mossel, Yu Chen Hou', 18 | 19 | author_email='andrewrobertmcburney@gmail.com, davepagurek@gmail.com, me@yuchenhou.com', 20 | 21 | license='MIT', 22 | 23 | classifiers=[ 24 | 'Development Status :: 3 - Alpha', 25 | 'Intended Audience :: Developers', 26 | 'Topic :: Software Development :: Template Engines', 27 | 'License :: OSI Approved :: MIT License', 28 | 'Programming Language :: Python :: 2' 29 | ], 30 | 31 | keywords='sample setuptools development', 32 | 33 | extras_require={ 34 | 'test': ['cpplint'] 35 | } 36 | ) 37 | -------------------------------------------------------------------------------- /cpp/src/grammar.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "grammar.hpp" 6 | 7 | // [START] Include nodes 8 | #include "nodes/node.hpp" 9 | #include "nodes/node_list.hpp" 10 | #include "nodes/boolean.hpp" 11 | #include "nodes/scope_fn.hpp" 12 | #include "nodes/scope.hpp" 13 | #include "nodes/line.hpp" 14 | #include "nodes/value_list.hpp" 15 | #include "nodes/literal_new_line.hpp" 16 | #include "nodes/attribute.hpp" 17 | #include "nodes/attributes.hpp" 18 | #include "nodes/tag_statement.hpp" 19 | #include "nodes/text_literal_content.hpp" 20 | #include "nodes/escaped.hpp" 21 | #include "nodes/comment.hpp" 22 | #include "nodes/scoped_key_value_pairs.hpp" 23 | #include "nodes/key_value_pair.hpp" 24 | #include "nodes/unary_expr.hpp" 25 | #include "nodes/binary_expr.hpp" 26 | #include "nodes/each.hpp" 27 | #include "nodes/with.hpp" 28 | #include "nodes/conditional.hpp" 29 | #include "nodes/variable_name.hpp" 30 | #include "nodes/variable.hpp" 31 | #include "nodes/scope.hpp" 32 | // [END] Include nodes 33 | 34 | namespace { 35 | // Helper function to turn a maybe rule (one element, made optional with a ?) into its value or a default 36 | template 37 | std::function optional(T default_value) { 38 | return [=](const peg::SemanticValues &sv) -> T { 39 | if (sv.size() > 0) { 40 | return sv[0].get(); 41 | } else { 42 | return default_value; 43 | } 44 | }; 45 | } 46 | 47 | // Helper to turn plural rules (one element, repeated with + or *) into a vector of a given type 48 | template 49 | std::function(const peg::SemanticValues&)> repeated() { 50 | return [](const peg::SemanticValues& sv) -> std::vector { 51 | std::vector contents; 52 | 53 | for (unsigned int n = 0; n < sv.size(); n++) { 54 | contents.push_back(sv[n].get()); 55 | } 56 | 57 | return contents; 58 | }; 59 | } 60 | } 61 | 62 | Grammar::Grammar() : emerald_parser(syntax) { 63 | emerald_parser["ROOT"] = 64 | [](const peg::SemanticValues& sv) -> NodePtr { 65 | NodePtrs nodes = sv[0].get(); 66 | return NodePtr(new NodeList(nodes, "\n")); 67 | }; 68 | 69 | emerald_parser["line"] = 70 | [](const peg::SemanticValues& sv) -> NodePtr { 71 | NodePtr line = sv[0].get(); 72 | 73 | return NodePtr(new Line(line)); 74 | }; 75 | 76 | emerald_parser["value_list"] = 77 | [](const peg::SemanticValues& sv) -> NodePtr { 78 | NodePtr keyword = sv[0].get(); 79 | NodePtrs literals = sv[1].get(); 80 | 81 | return NodePtr(new ValueList(keyword, literals)); 82 | }; 83 | 84 | emerald_parser["literal_new_line"] = 85 | [](const peg::SemanticValues& sv) -> NodePtr { 86 | NodePtr inline_lit_str = sv[0].get(); 87 | 88 | return NodePtr(new LiteralNewLine(inline_lit_str)); 89 | }; 90 | 91 | emerald_parser["pair_list"] = 92 | [](const peg::SemanticValues& sv) -> NodePtr { 93 | NodePtrs nodes; 94 | std::string base_keyword = sv[0].get(); 95 | std::vector semantic_values = sv[1].get>(); 96 | 97 | std::transform(semantic_values.begin(), semantic_values.end(), nodes.begin(), 98 | [=](const peg::SemanticValues& svs) { 99 | NodePtrs pairs = svs[0].get(); 100 | return NodePtr(new ScopedKeyValuePairs(base_keyword, pairs)); 101 | }); 102 | 103 | return NodePtr(new NodeList(nodes, "\n")); 104 | }; 105 | 106 | emerald_parser["scoped_key_value_pairs"] = repeated(); 107 | 108 | emerald_parser["scoped_key_value_pair"] = 109 | [](const peg::SemanticValues& sv) -> const peg::SemanticValues { 110 | return sv; 111 | }; 112 | 113 | emerald_parser["key_value_pair"] = 114 | [](const peg::SemanticValues& sv) -> NodePtr { 115 | std::string key = sv[0].get(); 116 | NodePtr value = sv[1].get(); 117 | 118 | return NodePtr(new KeyValuePair(key, value)); 119 | }; 120 | 121 | emerald_parser["comment"] = 122 | [](const peg::SemanticValues& sv) -> NodePtr { 123 | NodePtr text_content = sv[0].get(); 124 | 125 | return NodePtr(new Comment(text_content)); 126 | }; 127 | 128 | emerald_parser["maybe_id_name"] = optional(""); 129 | 130 | emerald_parser["class_names"] = repeated(); 131 | 132 | emerald_parser["tag_statement"] = 133 | [](const peg::SemanticValues& sv) -> NodePtr { 134 | std::string tag_name = sv[0].get(); 135 | std::string id_name = sv[1].get(); 136 | std::vector class_names = sv[2].get>(); 137 | NodePtr body = sv[3].get(); 138 | NodePtr attributes = sv[4].get(); 139 | NodePtr nested = sv[5].get(); 140 | 141 | return NodePtr( 142 | new TagStatement(tag_name, id_name, class_names, body, attributes, nested)); 143 | }; 144 | 145 | emerald_parser["attr_list"] = 146 | [](const peg::SemanticValues& sv) -> NodePtr { 147 | NodePtrs nodes = sv[0].get(); 148 | 149 | return NodePtr(new Attributes(nodes)); 150 | }; 151 | 152 | emerald_parser["attribute"] = 153 | [](const peg::SemanticValues& sv) -> NodePtr { 154 | std::string key = sv[0].get(); 155 | NodePtr value = sv[1].get(); 156 | return NodePtr(new Attribute(key, value)); 157 | }; 158 | 159 | emerald_parser["escaped"] = 160 | [](const peg::SemanticValues& sv) -> NodePtr { 161 | return NodePtr(new Escaped(sv.str())); 162 | }; 163 | 164 | emerald_parser["maybe_negation"] = optional(false); 165 | 166 | emerald_parser["negation"] = 167 | [](const peg::SemanticValues& sv) -> bool { 168 | return true; 169 | }; 170 | 171 | emerald_parser["unary_expr"] = 172 | [](const peg::SemanticValues& sv) -> BooleanPtr { 173 | bool negated = sv[0].get(); 174 | BooleanPtr expr = sv[1].get(); 175 | 176 | return BooleanPtr(new UnaryExpr(negated, expr)); 177 | }; 178 | 179 | emerald_parser["binary_expr"] = 180 | [](const peg::SemanticValues& sv) -> BooleanPtr { 181 | BooleanPtr lhs = sv[0].get(); 182 | std::string op_str = sv[1].get().str(); 183 | BooleanPtr rhs = sv[2].get(); 184 | BinaryExpr::Operator op; 185 | if (op_str == BinaryExpr::OR_STR) { 186 | op = BinaryExpr::Operator::OR; 187 | } else if (op_str == BinaryExpr::AND_STR) { 188 | op = BinaryExpr::Operator::AND; 189 | } else { 190 | throw "Invalid operator: " + op_str; 191 | } 192 | 193 | return BooleanPtr(new BinaryExpr(lhs, op, rhs)); 194 | }; 195 | 196 | emerald_parser["boolean_expr"] = 197 | [](const peg::SemanticValues& sv) -> BooleanPtr { 198 | return sv[0].get(); 199 | }; 200 | 201 | emerald_parser["maybe_key_name"] = optional(""); 202 | 203 | emerald_parser["each"] = 204 | [](const peg::SemanticValues& sv) -> ScopeFnPtr { 205 | std::string collection_name = sv[0].get(); 206 | std::string val_name = sv[1].get(); 207 | std::string key_name = sv[2].get(); 208 | 209 | return ScopeFnPtr(new Each(collection_name, val_name, key_name)); 210 | }; 211 | 212 | emerald_parser["with"] = 213 | [](const peg::SemanticValues& sv) -> ScopeFnPtr { 214 | std::string var_name = sv[0].get(); 215 | 216 | return ScopeFnPtr(new With(var_name)); 217 | }; 218 | 219 | emerald_parser["given"] = 220 | [](const peg::SemanticValues& sv) -> ScopeFnPtr { 221 | BooleanPtr condition = sv[0].get(); 222 | 223 | return ScopeFnPtr(new Conditional(true, condition)); 224 | }; 225 | 226 | emerald_parser["unless"] = 227 | [](const peg::SemanticValues& sv) -> ScopeFnPtr { 228 | BooleanPtr condition = sv[0].get(); 229 | 230 | return ScopeFnPtr(new Conditional(false, condition)); 231 | }; 232 | 233 | emerald_parser["scope_fn"] = 234 | [](const peg::SemanticValues& sv) -> ScopeFnPtr { 235 | return sv[0].get(); 236 | }; 237 | 238 | emerald_parser["variable_name"] = 239 | [](const peg::SemanticValues& sv) -> BooleanPtr { 240 | std::string name = sv.str(); 241 | 242 | return BooleanPtr(new VariableName(name)); 243 | }; 244 | 245 | emerald_parser["variable"] = 246 | [](const peg::SemanticValues& sv) -> NodePtr { 247 | std::string name = sv.token(0); 248 | 249 | return NodePtr(new Variable(name)); 250 | }; 251 | 252 | emerald_parser["scope"] = 253 | [](const peg::SemanticValues& sv) -> NodePtr { 254 | ScopeFnPtr scope_fn = sv[0].get(); 255 | NodePtr body = sv[1].get(); 256 | 257 | return NodePtr(new Scope(scope_fn, body)); 258 | }; 259 | 260 | // Repeated Nodes 261 | const std::vector repeated_nodes = { 262 | "statements", "literal_new_lines", "key_value_pairs", "ml_lit_str_quoteds", 263 | "ml_templess_lit_str_qs", "inline_literals", "il_lit_str_quoteds", "attributes" 264 | }; 265 | for (std::string repeated_node : repeated_nodes) { 266 | emerald_parser[repeated_node.c_str()] = repeated(); 267 | } 268 | 269 | // Wrapper Nodes 270 | const std::vector wrapper_nodes = { 271 | "statement", "text_content", "ml_lit_str_quoted", "ml_templess_lit_str_q", 272 | "inline_literal", "il_lit_str_quoted", "nested_tag" 273 | }; 274 | for (std::string wrapper_node : wrapper_nodes) { 275 | emerald_parser[wrapper_node.c_str()] = 276 | [](const peg::SemanticValues& sv) -> NodePtr { 277 | return sv[0].get(); 278 | }; 279 | } 280 | 281 | // Literals 282 | const std::vector literals = { 283 | "multiline_literal", "inline_lit_str", "inline_literals_node" 284 | }; 285 | for (std::string string_rule : literals) { 286 | emerald_parser[string_rule.c_str()] = 287 | [](const peg::SemanticValues& sv) -> NodePtr { 288 | NodePtrs body = sv[0].get(); 289 | 290 | return NodePtr(new NodeList(body, "")); 291 | }; 292 | } 293 | 294 | // Literal Contents 295 | const std::vector literal_contents = { 296 | "ml_lit_content", "il_lit_content", "il_lit_str_content" 297 | }; 298 | for (std::string string_rule_content : literal_contents) { 299 | emerald_parser[string_rule_content.c_str()] = 300 | [](const peg::SemanticValues& sv) -> NodePtr { 301 | return NodePtr(new TextLiteralContent(sv.str())); 302 | }; 303 | } 304 | 305 | const std::vector optional_nodes = { 306 | "maybe_text_content", "maybe_nested_tag", "maybe_attr_list" 307 | }; 308 | for (std::string rule_name : optional_nodes) { 309 | emerald_parser[rule_name.c_str()] = optional(NodePtr()); 310 | } 311 | 312 | // Terminals 313 | const std::vector terminals = { 314 | "attr", "tag", "class_name", "id_name", "key_name" 315 | }; 316 | for (std::string rule_name : terminals) { 317 | emerald_parser[rule_name.c_str()] = 318 | [](const peg::SemanticValues& sv) -> std::string { 319 | return sv.str(); 320 | }; 321 | } 322 | 323 | emerald_parser.enable_packrat_parsing(); 324 | } 325 | 326 | Grammar& Grammar::get_instance() { 327 | static Grammar instance; 328 | return instance; 329 | } 330 | 331 | peg::parser Grammar::get_parser() { 332 | return emerald_parser; 333 | } 334 | 335 | bool Grammar::valid(const std::string &input) { 336 | std::string output; 337 | emerald_parser.parse(input.c_str(), output); 338 | return output.length() == input.length(); 339 | } 340 | -------------------------------------------------------------------------------- /cpp/src/grammar.hpp: -------------------------------------------------------------------------------- 1 | #ifndef GRAMMAR_H 2 | #define GRAMMAR_H 3 | 4 | #include 5 | 6 | #include "../lib/peglib.h" 7 | 8 | /** 9 | * Singleton class for transforming Emerald code into intermediate 10 | * representation to be parsed by the PEG grammar 11 | */ 12 | class Grammar { 13 | 14 | public: 15 | Grammar(Grammar const&) = delete; // Copy constructor 16 | Grammar& operator=(Grammar const&) = delete; // Copy assignment 17 | Grammar(Grammar&&) = delete; // Move constructor 18 | Grammar& operator=(Grammar&&) = delete; // Move assignment 19 | 20 | static Grammar& get_instance(); 21 | peg::parser get_parser(); 22 | bool valid(const std::string &input); 23 | 24 | protected: 25 | Grammar(); 26 | 27 | private: 28 | std::string get_whitespace(int); 29 | 30 | // PEG parser 31 | peg::parser emerald_parser; 32 | 33 | // Grammar rules 34 | static constexpr const auto syntax = 35 | #include "grammar.peg" 36 | "\n" 37 | #include "tokens.peg" 38 | "\n" 39 | #include "scopes.peg" 40 | "\n" 41 | #include "variables.peg" 42 | "\n" 43 | R"(%whitespace <- [ \t]*)" 44 | ; 45 | 46 | }; 47 | 48 | #endif // GRAMMAR_H 49 | -------------------------------------------------------------------------------- /cpp/src/grammar.peg: -------------------------------------------------------------------------------- 1 | R"( 2 | ROOT <- statements 3 | 4 | statements <- (scope / pair_list / value_list / line / comment)+ 5 | 6 | scope <- scope_fn ~lbrace ~nl ROOT ~rbrace ~nl 7 | 8 | line <- (tag_statement / comment) ~nl 9 | 10 | value_list <- special_keyword ~nl ~lbrace ~nl literal_new_lines ~rbrace ~nl 11 | 12 | literal_new_lines <- literal_newline+ 13 | 14 | literal_new_line <- inline_lit_str ~nl 15 | 16 | pair_list <- base_keyword ~nl ~lbrace ~nl scoped_key_value_pairs ~rbrace ~nl 17 | 18 | scoped_key_value_pairs <- scoped_key_value_pair+ 19 | 20 | scoped_key_value_pair <- key_value_pairs ~nl 21 | 22 | key_value_pairs <- key_value_pair+ 23 | 24 | key_value_pair <- attr ~space+ inline_lit_str space* 25 | 26 | comment <- ~space* ~'*' ~space* text_content 27 | 28 | maybe_text_content <- text_content? 29 | 30 | text_content <- multiline_literal / ml_templess_lit / inline_literals_node 31 | 32 | multiline_literal <- ~'->' ~space* ~nl ml_lit_str_quoteds ~'$' 33 | 34 | ml_templess_lit <- ~('=>' / '~>') ~space* ~nl ml_templess_lit_str_qs ~'$' 35 | 36 | ml_lit_str_quoteds <- ml_lit_str_quoted* 37 | 38 | ml_lit_str_quoted <- variable / escaped / ml_lit_content 39 | 40 | # Jesus forgive me. 41 | ml_templess_lit_str_qs <- ml_templess_lit_str_q* 42 | 43 | ml_templess_lit_str_q <- escaped / ml_lit_content 44 | 45 | ################################ 46 | # Inline literals strings 47 | ################################ 48 | 49 | inline_literals_node <- inline_literals 50 | 51 | inline_literals <- inline_literal* 52 | 53 | inline_literal <- variable / escaped / il_lit_content 54 | 55 | inline_lit_str <- ~'"' il_lit_str_quoteds ~'"' 56 | 57 | il_lit_str_quoteds <- il_lit_str_quoted* 58 | 59 | il_lit_str_quoted <- variable / escaped / il_lit_content 60 | 61 | ml_lit_content <- !'$' . 62 | 63 | il_lit_content <- !lparen !nl . 64 | 65 | il_lit_str_content <- !'"' . 66 | 67 | escaped <- '\\' . 68 | 69 | tag_statement <- tag maybe_id_name class_names ~space* maybe_text_content maybe_attr_list maybe_nested_tag 70 | 71 | maybe_nested_tag <- nested_tag? 72 | 73 | nested_tag <- ~nl ~lbrace ~nl ROOT ~rbrace ~nl 74 | 75 | maybe_id_name <- id_name? 76 | 77 | id_name <- '#' $name< ([a-zA-Z_] / '-')+ > 78 | 79 | class_names <- class_name* 80 | 81 | class_name <- '.' $name< ([a-zA-Z_] / '-')+ > 82 | 83 | maybe_attr_list <- attr_list? 84 | 85 | attr_list <- ~lparen ~nl ~lbrace ~nl attributes ~rbrace ~nl ~rparen 86 | 87 | attributes <- attribute* 88 | 89 | attribute <- attr ~space* inline_lit_str ~nl 90 | )" 91 | -------------------------------------------------------------------------------- /cpp/src/htmlencode.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "htmlencode.hpp" 6 | 7 | std::map entities = { 8 | { "Æ", "AElig" }, 9 | { "Á", "Aacute" }, 10 | { "Â", "Acirc" }, 11 | { "À", "Agrave" }, 12 | { "Α", "Alpha" }, 13 | { "Å", "Aring" }, 14 | { "Ã", "Atilde" }, 15 | { "Ä", "Auml" }, 16 | { "Β", "Beta" }, 17 | { "Ç", "Ccedil" }, 18 | { "Χ", "Chi" }, 19 | { "‡", "Dagger" }, 20 | { "Δ", "Delta" }, 21 | { "Ð", "ETH" }, 22 | { "É", "Eacute" }, 23 | { "Ê", "Ecirc" }, 24 | { "È", "Egrave" }, 25 | { "Ε", "Epsilon" }, 26 | { "Η", "Eta" }, 27 | { "Ë", "Euml" }, 28 | { "Γ", "Gamma" }, 29 | { "Í", "Iacute" }, 30 | { "Î", "Icirc" }, 31 | { "Ì", "Igrave" }, 32 | { "Ι", "Iota" }, 33 | { "Ï", "Iuml" }, 34 | { "Κ", "Kappa" }, 35 | { "Λ", "Lambda" }, 36 | { "Μ", "Mu" }, 37 | { "Ñ", "Ntilde" }, 38 | { "Ν", "Nu" }, 39 | { "Œ", "OElig" }, 40 | { "Ó", "Oacute" }, 41 | { "Ô", "Ocirc" }, 42 | { "Ò", "Ograve" }, 43 | { "Ω", "Omega" }, 44 | { "Ο", "Omicron" }, 45 | { "Ø", "Oslash" }, 46 | { "Õ", "Otilde" }, 47 | { "Ö", "Ouml" }, 48 | { "Φ", "Phi" }, 49 | { "Π", "Pi" }, 50 | { "″", "Prime" }, 51 | { "Ψ", "Psi" }, 52 | { "Ρ", "Rho" }, 53 | { "Š", "Scaron" }, 54 | { "Σ", "Sigma" }, 55 | { "Þ", "THORN" }, 56 | { "Τ", "Tau" }, 57 | { "Θ", "Theta" }, 58 | { "Ú", "Uacute" }, 59 | { "Û", "Ucirc" }, 60 | { "Ù", "Ugrave" }, 61 | { "Υ", "Upsilon" }, 62 | { "Ü", "Uuml" }, 63 | { "Ξ", "Xi" }, 64 | { "Ý", "Yacute" }, 65 | { "Ÿ", "Yuml" }, 66 | { "Ζ", "Zeta" }, 67 | { "á", "aacute" }, 68 | { "â", "acirc" }, 69 | { "´", "acute" }, 70 | { "æ", "aelig" }, 71 | { "à", "agrave" }, 72 | { "ℵ", "alefsym" }, 73 | { "α", "alpha" }, 74 | { "&", "amp" }, 75 | { "∧", "and" }, 76 | { "∠", "ang" }, 77 | { "'", "apos" }, 78 | { "å", "aring" }, 79 | { "≈", "asymp" }, 80 | { "ã", "atilde" }, 81 | { "ä", "auml" }, 82 | { "„", "bdquo" }, 83 | { "β", "beta" }, 84 | { "¦", "brvbar" }, 85 | { "•", "bull" }, 86 | { "∩", "cap" }, 87 | { "ç", "ccedil" }, 88 | { "¸", "cedil" }, 89 | { "¢", "cent" }, 90 | { "χ", "chi" }, 91 | { "ˆ", "circ" }, 92 | { "♣", "clubs" }, 93 | { "≅", "cong" }, 94 | { "©", "copy" }, 95 | { "↵", "crarr" }, 96 | { "∪", "cup" }, 97 | { "¤", "curren" }, 98 | { "⇓", "dArr" }, 99 | { "†", "dagger" }, 100 | { "↓", "darr" }, 101 | { "°", "deg" }, 102 | { "δ", "delta" }, 103 | { "♦", "diams" }, 104 | { "÷", "divide" }, 105 | { "é", "eacute" }, 106 | { "ê", "ecirc" }, 107 | { "è", "egrave" }, 108 | { "∅", "empty" }, 109 | { "\xE2\x80\x83", "emsp" }, 110 | { "\xE2\x80\x82", "ensp" }, 111 | { "ε", "epsilon" }, 112 | { "≡", "equiv" }, 113 | { "η", "eta" }, 114 | { "ð", "eth" }, 115 | { "ë", "euml" }, 116 | { "€", "euro" }, 117 | { "∃", "exist" }, 118 | { "ƒ", "fnof" }, 119 | { "∀", "forall" }, 120 | { "½", "frac12" }, 121 | { "¼", "frac14" }, 122 | { "¾", "frac34" }, 123 | { "⁄", "frasl" }, 124 | { "γ", "gamma" }, 125 | { "≥", "ge" }, 126 | { ">", "gt" }, 127 | { "⇔", "hArr" }, 128 | { "↔", "harr" }, 129 | { "♥", "hearts" }, 130 | { "…", "hellip" }, 131 | { "í", "iacute" }, 132 | { "î", "icirc" }, 133 | { "¡", "iexcl" }, 134 | { "ì", "igrave" }, 135 | { "ℑ", "image" }, 136 | { "∞", "infin" }, 137 | { "∫", "int" }, 138 | { "ι", "iota" }, 139 | { "¿", "iquest" }, 140 | { "∈", "isin" }, 141 | { "ï", "iuml" }, 142 | { "κ", "kappa" }, 143 | { "⇐", "lArr" }, 144 | { "λ", "lambda" }, 145 | { "〈", "lang" }, 146 | { "«", "laquo" }, 147 | { "←", "larr" }, 148 | { "⌈", "lceil" }, 149 | { "“", "ldquo" }, 150 | { "≤", "le" }, 151 | { "⌊", "lfloor" }, 152 | { "∗", "lowast" }, 153 | { "◊", "loz" }, 154 | { "\xE2\x80\x8E", "lrm" }, 155 | { "‹", "lsaquo" }, 156 | { "‘", "lsquo" }, 157 | { "<", "lt" }, 158 | { "¯", "macr" }, 159 | { "—", "mdash" }, 160 | { "µ", "micro" }, 161 | { "·", "middot" }, 162 | { "−", "minus" }, 163 | { "μ", "mu" }, 164 | { "∇", "nabla" }, 165 | { "\xC2\xA0", "nbsp" }, 166 | { "–", "ndash" }, 167 | { "≠", "ne" }, 168 | { "∋", "ni" }, 169 | { "¬", "not" }, 170 | { "∉", "notin" }, 171 | { "⊄", "nsub" }, 172 | { "ñ", "ntilde" }, 173 | { "ν", "nu" }, 174 | { "ó", "oacute" }, 175 | { "ô", "ocirc" }, 176 | { "œ", "oelig" }, 177 | { "ò", "ograve" }, 178 | { "‾", "oline" }, 179 | { "ω", "omega" }, 180 | { "ο", "omicron" }, 181 | { "⊕", "oplus" }, 182 | { "∨", "or" }, 183 | { "ª", "ordf" }, 184 | { "º", "ordm" }, 185 | { "ø", "oslash" }, 186 | { "õ", "otilde" }, 187 | { "⊗", "otimes" }, 188 | { "ö", "ouml" }, 189 | { "¶", "para" }, 190 | { "∂", "part" }, 191 | { "‰", "permil" }, 192 | { "⊥", "perp" }, 193 | { "φ", "phi" }, 194 | { "π", "pi" }, 195 | { "ϖ", "piv" }, 196 | { "±", "plusmn" }, 197 | { "£", "pound" }, 198 | { "′", "prime" }, 199 | { "∏", "prod" }, 200 | { "∝", "prop" }, 201 | { "ψ", "psi" }, 202 | { "\"", "quot" }, 203 | { "⇒", "rArr" }, 204 | { "√", "radic" }, 205 | { "〉", "rang" }, 206 | { "»", "raquo" }, 207 | { "→", "rarr" }, 208 | { "⌉", "rceil" }, 209 | { "”", "rdquo" }, 210 | { "ℜ", "real" }, 211 | { "®", "reg" }, 212 | { "⌋", "rfloor" }, 213 | { "ρ", "rho" }, 214 | { "\xE2\x80\x8F", "rlm" }, 215 | { "›", "rsaquo" }, 216 | { "’", "rsquo" }, 217 | { "‚", "sbquo" }, 218 | { "š", "scaron" }, 219 | { "⋅", "sdot" }, 220 | { "§", "sect" }, 221 | { "\xC2\xAD", "shy" }, 222 | { "σ", "sigma" }, 223 | { "ς", "sigmaf" }, 224 | { "∼", "sim" }, 225 | { "♠", "spades" }, 226 | { "⊂", "sub" }, 227 | { "⊆", "sube" }, 228 | { "∑", "sum" }, 229 | { "¹", "sup1" }, 230 | { "²", "sup2" }, 231 | { "³", "sup3" }, 232 | { "⊃", "sup" }, 233 | { "⊇", "supe" }, 234 | { "ß", "szlig" }, 235 | { "τ", "tau" }, 236 | { "∴", "there4" }, 237 | { "θ", "theta" }, 238 | { "ϑ", "thetasym" }, 239 | { "\xE2\x80\x89", "thinsp" }, 240 | { "þ", "thorn" }, 241 | { "˜", "tilde" }, 242 | { "×", "times" }, 243 | { "™", "trade" }, 244 | { "⇑", "uArr" }, 245 | { "ú", "uacute" }, 246 | { "↑", "uarr" }, 247 | { "û", "ucirc" }, 248 | { "ù", "ugrave" }, 249 | { "¨", "uml" }, 250 | { "ϒ", "upsih" }, 251 | { "υ", "upsilon" }, 252 | { "ü", "uuml" }, 253 | { "℘", "weierp" }, 254 | { "ξ", "xi" }, 255 | { "ý", "yacute" }, 256 | { "¥", "yen" }, 257 | { "ÿ", "yuml" }, 258 | { "ζ", "zeta" }, 259 | { "\xE2\x80\x8D", "zwj" }, 260 | { "\xE2\x80\x8C", "zwnj" } 261 | }; 262 | 263 | int is_safe_char(UTF32_INT_TYPE c) { 264 | return c <= 127; 265 | } 266 | 267 | bool is_utf8_string(const char *p) { 268 | size_t size = strlen(p); 269 | 270 | while ('\0' != *p) { 271 | UTF32_INT_TYPE c; 272 | size_t clen; 273 | 274 | c = utf8_decode(p, size); 275 | if ((UTF32_INT_TYPE)-1 == c) 276 | return false; 277 | clen = utf8_first_byte_length_hint(*p); 278 | size -= clen; 279 | p += clen; 280 | } 281 | 282 | return true; 283 | } 284 | 285 | std::string c_substr(const char *p, size_t len) { 286 | std::string result; 287 | for (size_t i = 0; i < len; i++, p++) { 288 | result += *p; 289 | } 290 | return result; 291 | } 292 | 293 | std::string html_encode(const char *p) { 294 | size_t size = strlen(p); 295 | std::stringstream result; 296 | while ('\0' != *p) { 297 | UTF32_INT_TYPE c; 298 | size_t clen; 299 | 300 | c = utf8_decode(p, size); 301 | clen = utf8_first_byte_length_hint(*p); 302 | 303 | // See if the unicode character has an entity. It may 304 | // be multiple bytes long, so we have to copy all the 305 | // bytes of the character into a string rather than 306 | // using a single character 307 | auto entity = entities.find(c_substr(p, clen)); 308 | 309 | // Use named entity if it exists 310 | if (entity != entities.end()) { 311 | result << "&" << entity->second << ";"; 312 | 313 | // If it's not a regular ascii character, use its numeric value 314 | } else if (!is_safe_char(c)) { 315 | result << "&#" << (unsigned long) c << ";"; 316 | 317 | } else { 318 | result << (char)c; 319 | } 320 | 321 | size -= clen; 322 | p += clen; 323 | } 324 | 325 | return result.str(); 326 | } 327 | -------------------------------------------------------------------------------- /cpp/src/htmlencode.hpp: -------------------------------------------------------------------------------- 1 | #ifndef htmlencode_hpp 2 | #define htmlencode_hpp 3 | 4 | #include 5 | #include "../lib/utf8.h" 6 | 7 | int is_safe_char(UTF32_INT_TYPE c); 8 | bool is_utf8_string(const char *p); 9 | std::string html_encode(const char *p); 10 | 11 | #endif 12 | -------------------------------------------------------------------------------- /cpp/src/main.cpp: -------------------------------------------------------------------------------- 1 | /** 2 | * Emerald, the language agnostic templating engine. 3 | * 4 | * Copyright (c) 2016-2017 Andrew McBurney, Dave Pagurek, Yu Chen Hu, Google Inc. 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include "grammar.hpp" 32 | #include "preprocessor.hpp" 33 | 34 | void print_usage() { 35 | std::cout << "usage: emerald PATH [--ast]" << std::endl; 36 | exit(0); 37 | } 38 | 39 | void exit_and_error() { 40 | std::cerr << "Error, could not open input file." << std::endl; 41 | exit(0); 42 | } 43 | 44 | /** 45 | * Try to open file passed in from command line, and parse file input with 46 | * Emerald grammar 47 | */ 48 | int main(int argc, char** argv) { 49 | if (argc < 1) print_usage(); 50 | 51 | PreProcessor processor; 52 | std::string line, parsed; 53 | std::vector lines; 54 | 55 | // Try to open a file from name passed in from command line 56 | try { 57 | // Get text input from file 58 | std::ifstream input_file(argv[1]); 59 | 60 | // Exit if it fails 61 | if (input_file.fail()) exit_and_error(); 62 | 63 | // Get file input line by line 64 | while (std::getline(input_file, line)) lines.push_back(line); 65 | 66 | // Preprocess the emerald source code 67 | std::string output = processor.process(lines); 68 | 69 | // Get parser member from singleton 'Grammar' class 70 | peg::parser parser = Grammar::get_instance().get_parser(); 71 | 72 | // Parse preprocessed string 73 | parser.parse(output.c_str(), parsed); 74 | 75 | std::cout << parsed << std::endl; 76 | } catch (const std::ifstream::failure& e) { 77 | std::cout << "Exception opening/reading file" << std::endl; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /cpp/src/nodes/attribute.cpp: -------------------------------------------------------------------------------- 1 | #include "attribute.hpp" 2 | 3 | Attribute::Attribute(std::string key, NodePtr value): key(key), value(value) {} 4 | 5 | std::string Attribute::to_html(Json &context) { 6 | return key + "=" + value->to_html(context); 7 | } 8 | -------------------------------------------------------------------------------- /cpp/src/nodes/attribute.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ATTRIBUTE_H 2 | #define ATTRIBUTE_H 3 | 4 | #include "node.hpp" 5 | 6 | class Attribute : public Node { 7 | 8 | public: 9 | Attribute(std::string key, NodePtr value); 10 | 11 | std::string to_html(Json &context) override; 12 | 13 | private: 14 | std::string key; 15 | NodePtr value; 16 | 17 | }; 18 | 19 | #endif // ATTRIBUTE_H 20 | -------------------------------------------------------------------------------- /cpp/src/nodes/attributes.cpp: -------------------------------------------------------------------------------- 1 | #include "attributes.hpp" 2 | 3 | Attributes::Attributes(NodePtrs attributes): attributes(attributes) {} 4 | 5 | std::string Attributes::to_html(Json &context) { 6 | std::vector converted; 7 | std::transform(attributes.begin(), attributes.end(), converted.begin(), [&](NodePtr p) { 8 | return p->to_html(context); 9 | }); 10 | 11 | return boost::algorithm::join(converted, " "); 12 | } 13 | -------------------------------------------------------------------------------- /cpp/src/nodes/attributes.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ATTRIBUTES_H 2 | #define ATTRIBUTES_H 3 | 4 | #include "node.hpp" 5 | 6 | class Attributes : public Node { 7 | 8 | public: 9 | Attributes(NodePtrs attributes); 10 | 11 | std::string to_html(Json &context) override; 12 | 13 | private: 14 | NodePtrs attributes; 15 | 16 | }; 17 | 18 | #endif // ATTRIBUTES_H 19 | -------------------------------------------------------------------------------- /cpp/src/nodes/binary_expr.cpp: -------------------------------------------------------------------------------- 1 | #include "binary_expr.hpp" 2 | 3 | const std::string BinaryExpr::AND_STR = "and"; 4 | const std::string BinaryExpr::OR_STR = "or"; 5 | 6 | BinaryExpr::BinaryExpr(BooleanPtr lhs, Operator op, BooleanPtr rhs): 7 | lhs(lhs), 8 | rhs(rhs), 9 | op(op) 10 | {} 11 | 12 | bool BinaryExpr::truthy(Json &context) const { 13 | switch (op) { 14 | case AND: 15 | return lhs->truthy(context) && rhs->truthy(context); 16 | case OR: 17 | return lhs->truthy(context) || rhs->truthy(context); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /cpp/src/nodes/binary_expr.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BINARYEXPR_H 2 | #define BINARYEXPR_H 3 | 4 | #include "boolean.hpp" 5 | #include "node.hpp" 6 | #include 7 | 8 | class BinaryExpr : public Boolean { 9 | 10 | public: 11 | enum Operator { AND, OR }; 12 | static const std::string AND_STR; 13 | static const std::string OR_STR; 14 | 15 | BinaryExpr(BooleanPtr lhs, Operator op, BooleanPtr rhs); 16 | 17 | bool truthy(Json &context) const override; 18 | 19 | private: 20 | 21 | BooleanPtr lhs; 22 | BooleanPtr rhs; 23 | Operator op; 24 | }; 25 | 26 | #endif // BINARYEXPR_H 27 | -------------------------------------------------------------------------------- /cpp/src/nodes/boolean.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BOOLEAN_H 2 | #define BOOLEAN_H 3 | 4 | #include 5 | #include "node.hpp" 6 | 7 | class Boolean { 8 | public: 9 | virtual bool truthy(Json &context) const = 0; 10 | }; 11 | 12 | typedef std::shared_ptr BooleanPtr; 13 | #endif 14 | -------------------------------------------------------------------------------- /cpp/src/nodes/comment.cpp: -------------------------------------------------------------------------------- 1 | #include "comment.hpp" 2 | 3 | Comment::Comment(NodePtr text_content) : text_content(text_content) {} 4 | 5 | std::string Comment::to_html(Json &context) { 6 | return "\n"; 7 | } 8 | -------------------------------------------------------------------------------- /cpp/src/nodes/comment.hpp: -------------------------------------------------------------------------------- 1 | #ifndef COMMENT_H 2 | #define COMMENT_H 3 | 4 | #include "node.hpp" 5 | 6 | class Comment : public Node { 7 | 8 | public: 9 | Comment(NodePtr); 10 | 11 | std::string to_html(Json &context) override; 12 | 13 | private: 14 | NodePtr text_content; 15 | 16 | }; 17 | 18 | #endif // COMMENT_H 19 | -------------------------------------------------------------------------------- /cpp/src/nodes/conditional.cpp: -------------------------------------------------------------------------------- 1 | #include "conditional.hpp" 2 | 3 | Conditional::Conditional(bool expected, BooleanPtr condition): 4 | expected(expected), 5 | condition(condition) 6 | {} 7 | 8 | std::string Conditional::to_html(NodePtr body, Json &context) const { 9 | if (condition->truthy(context) == expected) { 10 | return body->to_html(context); 11 | } else { 12 | return ""; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /cpp/src/nodes/conditional.hpp: -------------------------------------------------------------------------------- 1 | #ifndef CONDITIONAL_H 2 | #define CONDITIONAL_H 3 | 4 | #include "node.hpp" 5 | #include "scope_fn.hpp" 6 | #include "boolean.hpp" 7 | 8 | class Conditional : public ScopeFn { 9 | 10 | public: 11 | Conditional(bool expected, BooleanPtr condition); 12 | 13 | std::string to_html(NodePtr body, Json &context) const override; 14 | 15 | private: 16 | bool expected; 17 | BooleanPtr condition; 18 | 19 | }; 20 | 21 | #endif // CONDITIONAL_H 22 | -------------------------------------------------------------------------------- /cpp/src/nodes/each.cpp: -------------------------------------------------------------------------------- 1 | #include "each.hpp" 2 | #include 3 | 4 | Each::Each(std::string collection_name, std::string val_name, std::string key_name): 5 | collection_name(collection_name), 6 | val_name(val_name), 7 | key_name(key_name) 8 | {} 9 | 10 | std::string Each::to_html(NodePtr body, Json &context) const { 11 | Json collection = context[collection_name]; 12 | if (collection.is_object()) { 13 | return each_in_collection(collection, body, context, false); 14 | } else if (collection.is_array()) { 15 | return each_in_collection(collection, body, context, true); 16 | } else { 17 | return ""; 18 | } 19 | } 20 | 21 | std::string Each::each_in_collection(const Json& collection, NodePtr body, const Json& context, bool key_is_index) const { 22 | std::vector iterations; 23 | 24 | // For each element in the collection 25 | for (Json::const_iterator it = collection.begin(); it < collection.end(); ++it) { 26 | 27 | // Make a new context with the element's key and value added at the root level 28 | Json new_context(context); 29 | 30 | // Add value 31 | new_context[val_name] = it.value(); 32 | 33 | // Add key, if specified 34 | if (!key_name.empty()) { 35 | if (key_is_index) { 36 | // Get the iteration of the loop 37 | new_context[key_name] = it - collection.begin(); 38 | } else { 39 | 40 | // Get the string name of the key 41 | new_context[key_name] = it.key(); 42 | } 43 | } 44 | iterations.push_back(body->to_html(new_context)); 45 | } 46 | 47 | return boost::algorithm::join(iterations, "\n"); 48 | } 49 | -------------------------------------------------------------------------------- /cpp/src/nodes/each.hpp: -------------------------------------------------------------------------------- 1 | #ifndef EACH_H 2 | #define EACH_H 3 | 4 | #include "scope_fn.hpp" 5 | #include "node.hpp" 6 | #include 7 | 8 | class Each : public ScopeFn { 9 | 10 | public: 11 | Each(std::string collection_name, std::string val_name, std::string key_name); 12 | 13 | std::string to_html(NodePtr body, Json &context) const override; 14 | 15 | private: 16 | std::string collection_name; 17 | std::string val_name; 18 | std::string key_name; 19 | 20 | std::string each_in_collection( 21 | const Json& collection, 22 | NodePtr body, 23 | const Json& context, 24 | bool key_is_index) const; 25 | }; 26 | 27 | #endif // EACH_H 28 | -------------------------------------------------------------------------------- /cpp/src/nodes/escaped.cpp: -------------------------------------------------------------------------------- 1 | #include "escaped.hpp" 2 | 3 | Escaped::Escaped(std::string content) : content(content) {} 4 | 5 | std::string Escaped::to_html(Json &context) { 6 | // Unescape the string 7 | return content.substr(1); 8 | } 9 | -------------------------------------------------------------------------------- /cpp/src/nodes/escaped.hpp: -------------------------------------------------------------------------------- 1 | #ifndef ESCAPED_H 2 | #define ESCAPED_H 3 | 4 | #include "node.hpp" 5 | 6 | class Escaped : public Node { 7 | 8 | public: 9 | Escaped(std::string content); 10 | 11 | std::string to_html(Json &context) override; 12 | 13 | private: 14 | std::string content; 15 | 16 | }; 17 | 18 | #endif // ESCAPED_H 19 | -------------------------------------------------------------------------------- /cpp/src/nodes/key_value_pair.cpp: -------------------------------------------------------------------------------- 1 | #include "key_value_pair.hpp" 2 | 3 | KeyValuePair::KeyValuePair(std::string key, NodePtr value) : key(key), value(value) {} 4 | 5 | std::string KeyValuePair::to_html(Json &context) { 6 | return key + "=\"" + value->to_html(context) + "\""; 7 | } 8 | -------------------------------------------------------------------------------- /cpp/src/nodes/key_value_pair.hpp: -------------------------------------------------------------------------------- 1 | #ifndef KEYVALUEPAIR_H 2 | #define KEYVALUEPAIR_H 3 | 4 | #include "node.hpp" 5 | 6 | class KeyValuePair : public Node { 7 | 8 | public: 9 | KeyValuePair(std::string, NodePtr); 10 | 11 | std::string to_html(Json &context) override; 12 | 13 | private: 14 | std::string key; 15 | NodePtr value; 16 | 17 | }; 18 | 19 | #endif // KEYVALUEPAIR_H 20 | -------------------------------------------------------------------------------- /cpp/src/nodes/line.cpp: -------------------------------------------------------------------------------- 1 | #include "line.hpp" 2 | 3 | Line::Line(NodePtr line) : line(line) {} 4 | 5 | std::string Line::to_html(Json &context) { 6 | return line->to_html(context); 7 | } 8 | -------------------------------------------------------------------------------- /cpp/src/nodes/line.hpp: -------------------------------------------------------------------------------- 1 | #ifndef LINE_H 2 | #define LINE_H 3 | 4 | #include "node.hpp" 5 | 6 | class Line : public Node { 7 | 8 | public: 9 | Line(NodePtr line); 10 | 11 | std::string to_html(Json &context) override; 12 | 13 | private: 14 | NodePtr line; 15 | 16 | }; 17 | 18 | #endif // LINE_H 19 | -------------------------------------------------------------------------------- /cpp/src/nodes/literal_new_line.cpp: -------------------------------------------------------------------------------- 1 | #include "literal_new_line.hpp" 2 | 3 | LiteralNewLine::LiteralNewLine(NodePtr inline_lit_str) : inline_lit_str(inline_lit_str) {} 4 | 5 | std::string LiteralNewLine::to_html(Json &context) { 6 | return inline_lit_str->to_html(context); 7 | } 8 | -------------------------------------------------------------------------------- /cpp/src/nodes/literal_new_line.hpp: -------------------------------------------------------------------------------- 1 | #ifndef LITERALNEWLINE_H 2 | #define LITERALNEWLINE_H 3 | 4 | #include "node.hpp" 5 | 6 | class LiteralNewLine : public Node { 7 | 8 | public: 9 | LiteralNewLine(NodePtr line); 10 | 11 | std::string to_html(Json &context) override; 12 | 13 | private: 14 | NodePtr inline_lit_str; 15 | 16 | }; 17 | 18 | #endif // LITERALNEWLINE_H 19 | -------------------------------------------------------------------------------- /cpp/src/nodes/node.cpp: -------------------------------------------------------------------------------- 1 | #include "node.hpp" 2 | -------------------------------------------------------------------------------- /cpp/src/nodes/node.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NODE_H 2 | #define NODE_H 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "../../lib/json.hpp" 9 | 10 | typedef nlohmann::json Json; 11 | 12 | class Node { 13 | 14 | public: 15 | virtual std::string to_html(Json &context) = 0; 16 | 17 | }; 18 | 19 | typedef std::shared_ptr NodePtr; 20 | typedef std::vector NodePtrs; 21 | 22 | #endif // NODE_H 23 | -------------------------------------------------------------------------------- /cpp/src/nodes/node_list.cpp: -------------------------------------------------------------------------------- 1 | #include "node_list.hpp" 2 | 3 | NodeList::NodeList(NodePtrs elements, std::string delimiter) : elements(elements), delimiter(delimiter) {} 4 | 5 | std::string NodeList::to_html(Json &context) { 6 | std::vector html_vector; 7 | std::transform(elements.begin(), elements.end(), html_vector.begin(), 8 | [&](NodePtr element) { return element->to_html(context); }); 9 | 10 | return boost::algorithm::join(html_vector, delimiter); 11 | } 12 | -------------------------------------------------------------------------------- /cpp/src/nodes/node_list.hpp: -------------------------------------------------------------------------------- 1 | #ifndef NODELIST_H 2 | #define NODELIST_H 3 | 4 | #include "node.hpp" 5 | 6 | class NodeList : public Node { 7 | 8 | public: 9 | NodeList(NodePtrs, std::string); 10 | 11 | std::string to_html(Json &context) override; 12 | 13 | private: 14 | NodePtrs elements; 15 | std::string delimiter; 16 | 17 | }; 18 | 19 | #endif // NODELIST_H 20 | -------------------------------------------------------------------------------- /cpp/src/nodes/scope.cpp: -------------------------------------------------------------------------------- 1 | #include "scope.hpp" 2 | 3 | Scope::Scope(ScopeFnPtr scope_fn, NodePtr body) : scope_fn(scope_fn), body(body) {} 4 | 5 | std::string Scope::to_html(Json &context) { 6 | return scope_fn->to_html(body, context); 7 | } 8 | -------------------------------------------------------------------------------- /cpp/src/nodes/scope.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SCOPE_H 2 | #define SCOPE_H 3 | 4 | #include "node.hpp" 5 | #include "scope_fn.hpp" 6 | 7 | class Scope : public Node { 8 | 9 | public: 10 | Scope(ScopeFnPtr scope_fn, NodePtr body); 11 | 12 | std::string to_html(Json &context) override; 13 | 14 | private: 15 | ScopeFnPtr scope_fn; 16 | NodePtr body; 17 | 18 | }; 19 | 20 | #endif // SCOPE_H 21 | -------------------------------------------------------------------------------- /cpp/src/nodes/scope_fn.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SCOPE_FN_H 2 | #define SCOPE_FN_H 3 | 4 | #include 5 | #include "node.hpp" 6 | 7 | class ScopeFn { 8 | public: 9 | virtual std::string to_html(NodePtr body, Json &context) const = 0; 10 | }; 11 | 12 | typedef std::shared_ptr ScopeFnPtr; 13 | #endif 14 | -------------------------------------------------------------------------------- /cpp/src/nodes/scoped_key_value_pairs.cpp: -------------------------------------------------------------------------------- 1 | #include "scoped_key_value_pairs.hpp" 2 | 3 | ScopedKeyValuePairs::ScopedKeyValuePairs(std::string keyword, NodePtrs pairs) : keyword(keyword), pairs(pairs) {} 4 | 5 | std::string ScopedKeyValuePairs::to_html(Json &context) { 6 | return ""; 7 | } 8 | -------------------------------------------------------------------------------- /cpp/src/nodes/scoped_key_value_pairs.hpp: -------------------------------------------------------------------------------- 1 | #ifndef SCOPEDKEYVALUEPAIRS_H 2 | #define SCOPEDKEYVALUEPAIRS_H 3 | 4 | #include "node.hpp" 5 | 6 | /** 7 | * Scoped key value pairs, scoped to a particular base_keyword in 'pair_list' rule 8 | * 9 | * Scoped to pair_list (has knowledge of base_keyword, NOTE: not a parser rule in emerald_parser) 10 | */ 11 | class ScopedKeyValuePairs : public Node { 12 | 13 | public: 14 | ScopedKeyValuePairs(std::string, NodePtrs); 15 | 16 | std::string to_html(Json &context) override; 17 | 18 | private: 19 | std::string keyword; 20 | NodePtrs pairs; 21 | 22 | }; 23 | 24 | #endif // SCOPEDKEYVALUEPAIRS_H 25 | -------------------------------------------------------------------------------- /cpp/src/nodes/tag_statement.cpp: -------------------------------------------------------------------------------- 1 | #include "tag_statement.hpp" 2 | #include 3 | 4 | TagStatement::TagStatement( 5 | std::string tag_name, 6 | std::string id, 7 | std::vector classes, 8 | NodePtr body, 9 | NodePtr attributes, 10 | NodePtr nested 11 | ): 12 | tag_name(tag_name), 13 | id(id), 14 | classes(classes), 15 | body(body), 16 | attributes(attributes), 17 | nested(nested), 18 | self_closing(void_tags.find(tag_name) != void_tags.end()) 19 | {} 20 | 21 | std::set TagStatement::void_tags = { 22 | "area", "base", "br", "col", "embed", "hr", "img", 23 | "input", "link", "menuitem", "meta", "param", "source", 24 | "track", "wbr" 25 | }; 26 | 27 | std::string TagStatement::to_html(Json &context) { 28 | if (self_closing) { 29 | return opening_tag(context); 30 | } else if (body) { 31 | return opening_tag(context) + body->to_html(context) + closing_tag(); 32 | } else { 33 | return opening_tag(context) + nested->to_html(context) + closing_tag(); 34 | } 35 | } 36 | 37 | std::string TagStatement::opening_tag(Json &context) const { 38 | return "<" + tag_name + id_attribute() + class_attribute() + 39 | (attributes ? " " + attributes->to_html(context) : "") + 40 | (self_closing ? " />" : ">"); 41 | } 42 | 43 | std::string TagStatement::id_attribute() const { 44 | if (id.empty()) { 45 | return ""; 46 | } else { 47 | return " id=\"" + id + "\""; 48 | } 49 | } 50 | 51 | std::string TagStatement::class_attribute() const { 52 | if (classes.size() == 0) { 53 | return ""; 54 | } else { 55 | return " class=\"" + boost::algorithm::join(classes, " ") + "\""; 56 | } 57 | } 58 | 59 | std::string TagStatement::closing_tag() const { 60 | return ""; 61 | } 62 | -------------------------------------------------------------------------------- /cpp/src/nodes/tag_statement.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TAG_STATEMENT_H 2 | #define TAG_STATEMENT_H 3 | 4 | #include "node.hpp" 5 | #include 6 | 7 | class TagStatement : public Node { 8 | public: 9 | TagStatement( 10 | std::string tag_name, 11 | std::string id, 12 | std::vector classes, 13 | NodePtr body, 14 | NodePtr attributes, 15 | NodePtr nested 16 | ); 17 | 18 | std::string to_html(Json &context) override; 19 | 20 | private: 21 | static std::set void_tags; 22 | 23 | std::string opening_tag(Json &context) const; 24 | std::string closing_tag() const; 25 | std::string id_attribute() const; 26 | std::string class_attribute() const; 27 | 28 | std::string tag_name; 29 | std::string id; 30 | std::vector classes; 31 | NodePtr body; 32 | NodePtr attributes; 33 | NodePtr nested; 34 | bool self_closing; 35 | }; 36 | 37 | #endif 38 | -------------------------------------------------------------------------------- /cpp/src/nodes/text_literal_content.cpp: -------------------------------------------------------------------------------- 1 | #include "text_literal_content.hpp" 2 | 3 | TextLiteralContent::TextLiteralContent(std::string content) : content(content) {} 4 | 5 | std::string TextLiteralContent::to_html(Json &context) { 6 | return content; 7 | } 8 | -------------------------------------------------------------------------------- /cpp/src/nodes/text_literal_content.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TEXTLITERALCONTENT_H 2 | #define TEXTLITERALCONTENT_H 3 | 4 | #include "node.hpp" 5 | 6 | class TextLiteralContent : public Node { 7 | 8 | public: 9 | TextLiteralContent(std::string content); 10 | 11 | std::string to_html(Json &context) override; 12 | 13 | private: 14 | std::string content; 15 | 16 | }; 17 | 18 | #endif // TEXTLITERALCONTENT_H 19 | -------------------------------------------------------------------------------- /cpp/src/nodes/unary_expr.cpp: -------------------------------------------------------------------------------- 1 | #include "unary_expr.hpp" 2 | 3 | UnaryExpr::UnaryExpr(bool negated, BooleanPtr expr) : negated(negated), expr(expr) {} 4 | 5 | bool UnaryExpr::truthy(Json &context) const { 6 | if (negated) { 7 | return !expr->truthy(context); 8 | } else { 9 | return expr->truthy(context); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cpp/src/nodes/unary_expr.hpp: -------------------------------------------------------------------------------- 1 | #ifndef UNARYEXPR_H 2 | #define UNARYEXPR_H 3 | 4 | #include "boolean.hpp" 5 | #include "node.hpp" 6 | 7 | class UnaryExpr : public Boolean { 8 | 9 | public: 10 | UnaryExpr(bool negated, BooleanPtr expr); 11 | 12 | bool truthy(Json &context) const override; 13 | 14 | private: 15 | bool negated; 16 | BooleanPtr expr; 17 | }; 18 | 19 | #endif // UNARYEXPR_H 20 | -------------------------------------------------------------------------------- /cpp/src/nodes/value_list.cpp: -------------------------------------------------------------------------------- 1 | #include "value_list.hpp" 2 | 3 | ValueList::ValueList(NodePtr keyword, NodePtrs literals) : keyword(keyword), literals(literals) {} 4 | 5 | std::string ValueList::check_keyword(NodePtr element, Json &context) { 6 | // TODO: figure out a 'text_value' alternative 7 | /** 8 | switch (keyword->text_value) { 9 | case "images": 10 | return "literal->to_html(context) + "\"/>"; 11 | break; 12 | case "styles": 13 | return "literal->to_html(context) + "\"/>"; 14 | break; 15 | case "scripts": 16 | return " " 163 | ""; 164 | 165 | REQUIRE(TestHelper::convert(input, {}) == output); 166 | } 167 | 168 | SECTION("supports \'scripts\' base rule") { 169 | std::vector input = { 170 | "scripts", 171 | " type 'text/javascript' src 'vendor/jquery.js'", 172 | " type 'text/javascript' src 'js/main.js'" 173 | }; 174 | const char *output = 175 | " " 176 | ""; 177 | 178 | REQUIRE(TestHelper::convert(input, {}) == output); 179 | } 180 | 181 | SECTION("supports \'styles\' special rule") { 182 | std::vector input = { 183 | "styles", 184 | " 'css/main.css'", 185 | " 'css/footer.css'" 186 | }; 187 | const char *output = 188 | " " 189 | ""; 190 | 191 | REQUIRE(TestHelper::convert(input, {}) == output); 192 | } 193 | 194 | SECTION("supports \'styles\' base rule") { 195 | std::vector input = { 196 | "styles", 197 | " href 'css/main.css' type 'text/css'", 198 | " href 'css/footer.css' type 'text/css'" 199 | }; 200 | const char *output = 201 | " " 202 | ""; 203 | 204 | REQUIRE(TestHelper::convert(input, {}) == output); 205 | } 206 | 207 | SECTION("supports \'metas\' base rule") { 208 | std::vector input = { 209 | "metas", 210 | " name 'test-name' content 'test-content'", 211 | " name 'test-name-2' content 'test-content-2'" 212 | }; 213 | const char *output = 214 | " " 215 | ""; 216 | 217 | REQUIRE(TestHelper::convert(input, {}) == output); 218 | } 219 | 220 | } // SECTION("list rules") 221 | 222 | } // TEST_CASE("attributes", "[general]") 223 | -------------------------------------------------------------------------------- /cpp/test/preprocessor_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "test_helper.hpp" 2 | #include "../src/preprocessor.hpp" 3 | 4 | TEST_CASE("source maps", "[preprocessor]") { 5 | 6 | SECTION("mapping lines from output to input") { 7 | std::vector input = { 8 | "div", 9 | " test" 10 | }; 11 | 12 | PreProcessor p; 13 | p.process(input); 14 | REQUIRE(p.get_source_map()[1] == 1); 15 | REQUIRE(p.get_source_map()[3] == 2); 16 | } 17 | 18 | } // TEST_CASE("source maps", "[preprocessor]") 19 | 20 | TEST_CASE("multiline literals", "[preprocessor]") { 21 | 22 | SECTION("preprocessing multiline literals") { 23 | std::vector input = { 24 | "h1 =>", 25 | " This is a multiline literal" 26 | }; 27 | std::vector output = { 28 | "h1 =>", 29 | "This is a multiline literal", 30 | "$" 31 | }; 32 | 33 | PreProcessor p; 34 | REQUIRE(p.process(input) == TestHelper::concat(output)); 35 | } 36 | 37 | SECTION("encoding HTML entities") { 38 | std::vector input = { 39 | "h1 =>", 40 | "
" 41 | }; 42 | std::vector output = { 43 | "h1 =>", 44 | "<div>", 45 | "$" 46 | }; 47 | 48 | PreProcessor p; 49 | REQUIRE(p.process(input) == TestHelper::concat(output)); 50 | } 51 | 52 | SECTION("encoding utf8 characters without semantic names") { 53 | std::vector input = { 54 | "h1 =>", 55 | " 👌" 56 | }; 57 | std::vector output = { 58 | "h1 =>", 59 | "👌", 60 | "$" 61 | }; 62 | 63 | PreProcessor p; 64 | REQUIRE(p.process(input) == TestHelper::concat(output)); 65 | } 66 | 67 | SECTION("skipping HTML entity encoding for ~> literals") { 68 | std::vector input = { 69 | "h1 ~>", 70 | "
" 71 | }; 72 | std::vector output = { 73 | "h1 ~>", 74 | "
", 75 | "$" 76 | }; 77 | 78 | PreProcessor p; 79 | REQUIRE(p.process(input) == TestHelper::concat(output)); 80 | } 81 | 82 | SECTION("escaping dollar signs") { 83 | std::vector input = { 84 | "h1 =>", 85 | " Thi$ i$ a multiline literal" 86 | }; 87 | std::vector output = { 88 | "h1 =>", 89 | "Thi\\$ i\\$ a multiline literal", 90 | "$" 91 | }; 92 | 93 | PreProcessor p; 94 | REQUIRE(p.process(input) == TestHelper::concat(output)); 95 | } 96 | 97 | SECTION("preserving empty lines") { 98 | std::vector input = { 99 | "pre =>", 100 | " line", 101 | "", 102 | " line" 103 | }; 104 | std::vector output = { 105 | "pre =>", 106 | "line", 107 | "", 108 | "line", 109 | "$" 110 | }; 111 | 112 | PreProcessor p; 113 | REQUIRE(p.process(input) == TestHelper::concat(output)); 114 | } 115 | 116 | } // TEST_CASE("multiline literals", "[preprocessor]") 117 | 118 | TEST_CASE("converting nesting") { 119 | 120 | SECTION("adding braces around nested tags") { 121 | std::vector input = { 122 | "div", 123 | " p test" 124 | }; 125 | std::vector output = { 126 | "div", 127 | "{", 128 | "p test", 129 | "}" 130 | }; 131 | 132 | PreProcessor p; 133 | REQUIRE(p.process(input) == TestHelper::concat(output)); 134 | } 135 | 136 | SECTION("adding braces around attributes") { 137 | std::vector input = { 138 | "h1 test (", 139 | " class \"title\"", 140 | ")" 141 | }; 142 | std::vector output = { 143 | "h1 test (", 144 | "{", 145 | "class \"title\"", 146 | "}", 147 | ")" 148 | }; 149 | 150 | PreProcessor p; 151 | REQUIRE(p.process(input) == TestHelper::concat(output)); 152 | } 153 | 154 | } // TEST_CASE("converting nesting") 155 | -------------------------------------------------------------------------------- /cpp/test/scope_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "test_helper.hpp" 2 | 3 | TEST_CASE("scope", "[grammar]") { 4 | 5 | SECTION("can determine truthiness") { 6 | std::vector input = { 7 | "given test", 8 | " h1 True", 9 | "unless test", 10 | " h1 False" 11 | }; 12 | const char *output = "

True

"; 13 | 14 | REQUIRE(TestHelper::convert(input, { "test", true }) == output); 15 | } 16 | 17 | SECTION("can determine falsiness") { 18 | std::vector input = { 19 | "given test", 20 | " h1 True", 21 | "unless test", 22 | " h1 False" 23 | }; 24 | const char *output = "

False

"; 25 | 26 | REQUIRE(TestHelper::convert(input, { "test", false }) == output); 27 | } 28 | 29 | SECTION("can check if a variable is present") { 30 | std::vector input = { 31 | "given test", 32 | " h1 True", 33 | "unless test", 34 | " h1 False" 35 | }; 36 | const char *output = "

False

"; 37 | 38 | REQUIRE(TestHelper::convert(input, {}) == output); 39 | } 40 | 41 | SECTION("boolean expressions") { 42 | 43 | SECTION("can do OR checks - true branch") { 44 | std::vector input = { 45 | "given a or b", 46 | " h1 True" 47 | }; 48 | const char *output = "

True

"; 49 | 50 | REQUIRE(TestHelper::convert(input, { "a", true, "b", false }) == output); 51 | } 52 | 53 | SECTION("can do OR checks - false branch") { 54 | std::vector input = { 55 | "given a or b", 56 | " h1 True" 57 | }; 58 | const char *output = ""; 59 | 60 | REQUIRE(TestHelper::convert(input, { "a", false, "b", false }) == output); 61 | } 62 | 63 | SECTION("can do AND checks - true branch") { 64 | std::vector input = { 65 | "given a and b", 66 | " h1 True" 67 | }; 68 | const char *output = "

True

"; 69 | 70 | REQUIRE(TestHelper::convert(input, { "a", true, "b", true }) == output); 71 | } 72 | 73 | SECTION("can do AND checks - false branch") { 74 | std::vector input = { 75 | "given a and b", 76 | " h1 True" 77 | }; 78 | const char *output = ""; 79 | 80 | REQUIRE(TestHelper::convert(input, { "a", true, "b", false }) == output); 81 | } 82 | 83 | SECTION("can do negation - true branch") { 84 | std::vector input = { 85 | "given not a", 86 | " h1 True" 87 | }; 88 | const char *output = "

True

"; 89 | 90 | REQUIRE(TestHelper::convert(input, { "a", false }) == output); 91 | } 92 | 93 | SECTION("can do negation - false branch") { 94 | std::vector input = { 95 | "given not a", 96 | " h1 True" 97 | }; 98 | const char *output = ""; 99 | 100 | REQUIRE(TestHelper::convert(input, { "a", true }) == output); 101 | } 102 | 103 | SECTION("can combine expressions - example one") { 104 | std::vector input = { 105 | "given a and b and c", 106 | " h1 True" 107 | }; 108 | const char *output = "

True

"; 109 | 110 | REQUIRE(TestHelper::convert(input, { "a", true, "b", true, "c", true }) == output); 111 | } 112 | 113 | SECTION("can combine expressions - example two") { 114 | std::vector input = { 115 | "given a or (b and c)", 116 | " h1 True" 117 | }; 118 | const char *output = "

True

"; 119 | 120 | REQUIRE(TestHelper::convert(input, { "a", false, "b", true, "c", true }) == output); 121 | } 122 | 123 | } // SECTION("boolean expressions") 124 | 125 | 126 | SECTION("loops") { 127 | 128 | SECTION("can loop over arrays") { 129 | std::vector input = { 130 | "each a as element", 131 | " h1 |element|" 132 | }; 133 | const char *output = "

1

2

3

"; 134 | 135 | REQUIRE(TestHelper::convert(input, { "a", {1, 2, 3} }) == output); 136 | } 137 | 138 | SECTION("can loop over arrays with indices") { 139 | std::vector input = { 140 | "each a as element, i", 141 | " h1 |i|" 142 | }; 143 | const char *output = "

0

1

2

"; 144 | 145 | REQUIRE(TestHelper::convert(input, { "a", {'a', 'b', 'c'} }) == output); 146 | } 147 | 148 | SECTION("can loop over hashes") { 149 | std::vector input = { 150 | "each a as v, k" 151 | " h1 |v| |k|" 152 | }; 153 | const char *output = "

b c

"; 154 | 155 | REQUIRE(TestHelper::convert(input, { "a", { "b", "c" } }) == output); 156 | } 157 | 158 | } // SECTION("loops") 159 | 160 | SECTION("with") { 161 | 162 | SECTION("can push scope") { 163 | std::vector input = { 164 | "with a" 165 | " h1 |b|" 166 | }; 167 | const char *output = "

c

"; 168 | 169 | REQUIRE(TestHelper::convert(input, { "a", { "b", "c" } }) == output); 170 | } 171 | 172 | } // SECTION("with") 173 | 174 | } // TEST_CASE("scope", "[grammar]") 175 | -------------------------------------------------------------------------------- /cpp/test/templating_tests.cpp: -------------------------------------------------------------------------------- 1 | #include "test_helper.hpp" 2 | #include "../src/preprocessor.hpp" 3 | 4 | TEST_CASE("templating", "[grammar]") { 5 | 6 | SECTION("works with no templating") { 7 | std::vector input = { 8 | "h1 Hello, world" 9 | }; 10 | const char *output = "

Hello, world

"; 11 | 12 | REQUIRE(TestHelper::convert(input, {}) == output); 13 | } 14 | 15 | SECTION("templating works for attributes") { 16 | std::vector input = { 17 | "section Attributes follow parentheses. (" 18 | "id \"header\"" 19 | "class \"|prefix|-text\"" 20 | "height \"50px\"" 21 | "width \"200px\"" 22 | }; 23 | const char *output = 24 | "
" 25 | "Attributes follow parentheses.
"; 26 | 27 | REQUIRE(TestHelper::convert(input, { "prefix", "class-prefix" }) == output); 28 | } 29 | 30 | SECTION("works with simple templating") { 31 | std::vector input = { 32 | "h1 Hello, |name|" 33 | }; 34 | const char *output = "

Hello, Dave

"; 35 | 36 | REQUIRE(TestHelper::convert(input, { "name", "Dave" }) == output); 37 | } 38 | 39 | SECTION("works with nested templating") { 40 | std::vector input = { 41 | "h1 Hello, |person.name|" 42 | }; 43 | const char *output = "

Hello, Andrew

"; 44 | 45 | REQUIRE(TestHelper::convert(input, { "person", { "name", "Andrew" } }) == output); 46 | } 47 | 48 | SECTION("works in multiline literals") { 49 | std::vector input = { 50 | "pre ->", 51 | " Hello, world,", 52 | " my name is |name|" 53 | }; 54 | const std::string output = "
Hello, world, my name is Dave
"; 55 | 56 | REQUIRE(TestHelper::convert(input, { "name", "Dave" }) == output); 57 | } 58 | 59 | SECTION("does not template in multiline templateless literals") { 60 | std::vector input = { 61 | "h1 =>", 62 | " Hello, world,", 63 | " my name is |name|" 64 | }; 65 | const std::string output = "
Hello, world, my name is |name|
"; 66 | 67 | REQUIRE(TestHelper::convert(input, { "name", "Dave" }) == output); 68 | } 69 | 70 | SECTION("escaping") { 71 | 72 | SECTION("handles escaped pipes") { 73 | std::vector input = { 74 | "h1 Hello, \\||name|" 75 | }; 76 | const char *output = "

Hello, |Andrew

"; 77 | 78 | REQUIRE(TestHelper::convert(input, { "name", "Andrew" }) == output); 79 | } 80 | 81 | SECTION("handles escaped escaped pipes") { 82 | std::vector input = { 83 | "h1 Hello, \\\\||name|" 84 | }; 85 | const char *output = "

Hello, \\Dave

"; 86 | 87 | REQUIRE(TestHelper::convert(input, { "name", "Dave" }) == output); 88 | } 89 | 90 | SECTION("handles braces in multiline literals") { 91 | std::vector input = { 92 | "h1 =>", 93 | " hey look a brace", 94 | " }" 95 | }; 96 | const char *output = "

hey look a brace }

"; 97 | 98 | REQUIRE(TestHelper::convert(input, {}) == output); 99 | } 100 | 101 | SECTION("handles braces in inline literals") { 102 | std::vector input = { 103 | "h1 hey look a brace }" 104 | }; 105 | const char *output = "

hey look a brace }

"; 106 | 107 | REQUIRE(TestHelper::convert(input, {}) == output); 108 | } 109 | 110 | SECTION("does not template templateless literals") { 111 | std::vector input = { 112 | "h1 =>", 113 | " if (a || b) {", 114 | " console.log(\"truthy\\n\");", 115 | " }" 116 | }; 117 | const char *output = 118 | "

if (a || b) {" 119 | " console.log("truthy\\n");" 120 | "}

"; 121 | 122 | REQUIRE(TestHelper::convert(input, {}) == TestHelper::whitespace_agnostic(output)); 123 | } 124 | 125 | SECTION("escaping parentheses works") { 126 | std::vector input = { 127 | "section here's some text \\(and some stuff in brackets\\)" 128 | }; 129 | const char *output = 130 | "
here\'s some text (and some stuff in brackets)
"; 131 | 132 | REQUIRE(TestHelper::convert(input, {}) == output); 133 | } 134 | 135 | } // SECTION("escaping") 136 | 137 | } // TEST_CASE("templating", "[grammar]") 138 | -------------------------------------------------------------------------------- /cpp/test/test_helper.cpp: -------------------------------------------------------------------------------- 1 | #include "test_helper.hpp" 2 | 3 | std::string TestHelper::concat(std::vector v) { 4 | return boost::algorithm::join(v, "\n") + "\n"; 5 | } 6 | 7 | std::string TestHelper::whitespace_agnostic(std::string input) { 8 | boost::regex regex_string("\\s+"); 9 | return boost::trim_copy(boost::regex_replace(input, regex_string, " ")); 10 | } 11 | 12 | std::string TestHelper::convert(const std::vector input, 13 | json context, bool preserve_whitespace) { 14 | std::string input_str = concat(input); 15 | std::string output = "test"; // TODO: change to process emerald 16 | return preserve_whitespace ? output : whitespace_agnostic(output); 17 | } 18 | -------------------------------------------------------------------------------- /cpp/test/test_helper.hpp: -------------------------------------------------------------------------------- 1 | #ifndef TEST_HELPER_H 2 | #define TEST_HELPER_H 3 | 4 | #include "../lib/catch.hpp" 5 | #include "../lib/json.hpp" 6 | #include "../src/grammar.hpp" 7 | #include "../src/preprocessor.hpp" 8 | 9 | #include 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | 16 | using json = nlohmann::json; 17 | 18 | namespace TestHelper { 19 | std::string concat(std::vector); 20 | std::string whitespace_agnostic(std::string); 21 | std::string convert(const std::vector, json, bool = false); 22 | } 23 | 24 | #endif 25 | -------------------------------------------------------------------------------- /cpp/util/.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | TargetRubyVersion: 2.4 3 | 4 | Metrics/LineLength: 5 | Max: 120 6 | 7 | Style/Documentation: 8 | Enabled: false 9 | 10 | Style/StringLiterals: 11 | EnforcedStyle: double_quotes 12 | ConsistentQuotesInMultiline: true 13 | Enabled: true 14 | -------------------------------------------------------------------------------- /cpp/util/file_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "colorize" 4 | require "fileutils" 5 | require "logger" 6 | 7 | module Emerald 8 | module FileHelper 9 | attr_accessor :logger 10 | def initialize 11 | @logger = ::Logger.new(STDOUT) 12 | end 13 | 14 | def create_file(file_name, file_contents) 15 | logger.info "[Info] Writing header file #{file_name}..." 16 | 17 | File.open(file_name, "w") { |file| file.write file_contents } 18 | 19 | logger.info "[Create] Wrote header file #{file_name}.".colorize(:green) 20 | end 21 | 22 | def update_file(file_path, text_to_insert, line_to_insert_before) 23 | logger.info "[Info] Updating grammar file #{file_path}..." 24 | 25 | modified_contents = File.readlines(file_path).map do |line| 26 | line.strip == line_to_insert_before ? text_to_insert + line : line 27 | end 28 | 29 | File.open(file_path, "w") { |file| file.write modified_contents.join } 30 | 31 | logger.info "[Update] Updated grammar file #{file_path}.".colorize(:green) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /cpp/util/generate: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require "thor" 5 | require_relative "generator.rb" 6 | 7 | module Emerald 8 | class CLI < Thor 9 | desc "node ", "Generates new default node for grammar." 10 | def node(name) 11 | Emerald::Generator.new.node(name) 12 | end 13 | end 14 | end 15 | 16 | Emerald::CLI.start(ARGV) -------------------------------------------------------------------------------- /cpp/util/generator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "file_helper.rb" 4 | 5 | module Emerald 6 | class Generator 7 | include FileHelper 8 | 9 | def node(name) 10 | write_header_file(name) 11 | write_source_file(name) 12 | update_grammar_source_file(name) 13 | end 14 | 15 | protected 16 | 17 | def write_header_file(name) 18 | file_contents = <<~EOF 19 | #ifndef #{upcase!(name)}_H 20 | #define #{upcase!(name)}_H 21 | 22 | #include "node.hpp" 23 | 24 | class #{class!(name)} : public Node { 25 | 26 | public: 27 | #{class!(name)}(NodePtr); 28 | 29 | std::string to_html(Json &context) override; 30 | 31 | private: 32 | NodePtr something; 33 | 34 | }; 35 | 36 | #endif // #{upcase!(name)}_H 37 | EOF 38 | 39 | create_file("src/nodes/#{name}.hpp", file_contents) 40 | end 41 | 42 | def write_source_file(name) 43 | file_contents = <<~EOF 44 | #include "#{name}.hpp" 45 | 46 | #{class!(name)}::#{class!(name)}(NodePtr something) : something(something) {} 47 | 48 | std::string #{class!(name)}::to_html(Json &context) { 49 | return line->to_html(context); 50 | } 51 | EOF 52 | 53 | create_file("src/nodes/#{name}.cpp", file_contents) 54 | end 55 | 56 | def update_grammar_source_file(name) 57 | grammar_rule = <<-EOF 58 | emerald_parser["#{name}"] = 59 | [](const peg::SemanticValues& sv) -> NodePtr { 60 | NodePtr something = sv[0].get(); 61 | 62 | return NodePtr(new #{class!(name)}(something)); 63 | }; 64 | 65 | EOF 66 | 67 | update_file("src/grammar.cpp", "#include \"nodes/#{name}.hpp\"\n", "// [END] Include nodes") 68 | update_file("src/grammar.cpp", grammar_rule, "// Terminals") 69 | end 70 | 71 | private 72 | 73 | def class!(str) 74 | str.split("_").collect(&:capitalize).join 75 | end 76 | 77 | def upcase!(str) 78 | str.delete("_").upcase 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /emerald-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emerald-lang/emerald/5cb7209f918e8ebdb36bb0455f58d3e941fc6e52/emerald-logo.png -------------------------------------------------------------------------------- /emerald.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'emerald/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'emerald-lang' 8 | spec.version = Emerald::VERSION 9 | spec.authors = ['Andrew McBurney', 'Dave Pagurek', 'Yu Chen Hu'] 10 | spec.email = ['andrewrobertmcburney@gmail.com', 'davepagurek@gmail.com', 'me@yuchenhou.com'] 11 | 12 | spec.summary = 'A language agnostic templating engine designed with event driven applications in mind.' 13 | spec.description = 'A language agnostic templating engine designed with event driven applications in mind.' # TODO: make better description 14 | spec.homepage = 'https://github.com/emerald-lang/emerald' # TODO: replace with website once created 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | spec.bindir = 'bin' 19 | spec.executables = ['emerald'] 20 | spec.require_paths = ['lib'] 21 | 22 | spec.add_development_dependency 'bundler', '~> 1.10' 23 | spec.add_development_dependency 'rake', '~> 10.0' 24 | spec.add_development_dependency 'rspec' 25 | spec.add_development_dependency 'test-unit' 26 | spec.add_development_dependency 'rubocop' 27 | spec.add_development_dependency 'pre-commit' 28 | 29 | spec.add_runtime_dependency 'htmlentities' 30 | spec.add_runtime_dependency 'treetop' 31 | spec.add_runtime_dependency 'thor' 32 | spec.add_runtime_dependency 'htmlbeautifier', '~> 1.1', '>= 1.1.1' 33 | end 34 | -------------------------------------------------------------------------------- /lib/emerald.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | # 5 | # Emerald, the language agnostic templating engine. 6 | # Copyright 2016, Emerald Language (MIT) 7 | # 8 | require 'emerald/version' 9 | 10 | require 'json' 11 | require 'thor' 12 | require 'htmlbeautifier' 13 | 14 | require_relative 'emerald/grammar' 15 | require_relative 'emerald/preprocessor' 16 | 17 | # Parses a context free grammar from the preprocessed emerald and generates 18 | # html associated with corresponding abstract syntax tree. 19 | module Emerald 20 | # The Emerald CLI 21 | class CLI < Thor 22 | class_option :beautify, :type => :boolean, :aliases => 'b' 23 | 24 | def self.exit_on_failure? 25 | true 26 | end 27 | 28 | # Main emerald option, processes the emerald file, generates an abstract 29 | # syntax tree based on the output from the preprocessing. 30 | desc 'process', 'Process a file or folder (recursively) and converts it to emerald.' 31 | option :output, aliases: 'o' 32 | def process(file_name, context_file_name = nil) 33 | begin 34 | output_name = options[:output] || file_name 35 | context = 36 | if context_file_name 37 | JSON.parse(IO.read(context_file_name)) 38 | else 39 | {} 40 | end 41 | 42 | input = IO.read(file_name) 43 | 44 | Emerald.write_html( 45 | Emerald.convert(input, context), 46 | output_name, 47 | options['beautify'] 48 | ) 49 | rescue Grammar::PreProcessorError, Grammar::ParserError => e 50 | raise Thor::Error, e.message 51 | end 52 | end 53 | end 54 | 55 | def self.convert(input, context = {}) 56 | preprocessed_emerald, source_map = PreProcessor.new.process_emerald(input) 57 | abstract_syntax_tree = Grammar.parse_grammar( 58 | preprocessed_emerald, 59 | input, 60 | source_map 61 | ) 62 | 63 | return abstract_syntax_tree.to_html(context) 64 | end 65 | 66 | # Write html to file and beautify it if the beautify global option is set to 67 | # true. 68 | def self.write_html(html_output, file_name, beautify) 69 | File.open(file_name + '.html', 'w') do |emerald_file| 70 | html_output = HtmlBeautifier.beautify(html_output) if beautify 71 | emerald_file.write(html_output) 72 | end 73 | puts "Wrote #{file_name + '.html'}" 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/emerald/grammar.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'polyglot' 5 | require 'treetop' 6 | 7 | # Require all treetop nodes for grammar 8 | Dir[File.dirname(__FILE__) + '/nodes/*.rb'].each { |f| require f } 9 | 10 | Treetop.load __dir__ + '/grammar/tokens' 11 | Treetop.load __dir__ + '/grammar/variables' 12 | Treetop.load __dir__ + '/grammar/scopes' 13 | Treetop.load __dir__ + '/grammar/emerald' 14 | 15 | # 16 | # Interface which interacts with emerald context free grammar. Parses the 17 | # preprocessed Emerald and prints out success if the parser was successful and 18 | # failure if there was an error when parsing. Returns an abstract syntax tree. 19 | # 20 | module Grammar 21 | # When the parser fails on a line from user input 22 | class ParserError < StandardError 23 | LINES_BEFORE = 3 24 | LINES_AFTER = 1 25 | 26 | def initialize(line_number, reason, source) 27 | @line_number = line_number 28 | @reason = reason 29 | @source = source 30 | end 31 | 32 | def message 33 | messages = [] 34 | if match = @reason.match(/(Expected .+?) at line/) 35 | messages << match[1] 36 | else 37 | messages << @reason 38 | end 39 | 40 | lines = @source.split(/\n/) 41 | LINES_BEFORE.downto(1).each do |i| 42 | messages << ' ' + lines[@line_number - i - 1] if lines[@line_number - i - 1] 43 | end 44 | messages << '>>> ' + lines[@line_number - 1] 45 | 1.upto(LINES_AFTER).each do |i| 46 | messages << ' ' + lines[@line_number + i - 1] if lines[@line_number + i - 1] 47 | end 48 | 49 | messages.join("\n") 50 | end 51 | end 52 | 53 | # When the parser fails on a line added only in preprocessing 54 | class PreProcessorError < ParserError 55 | def say 56 | messages = [] 57 | messages << "Error parsing in pre-processed Emerald. This is likely a bug." 58 | messages << @reason 59 | messages.concat(@source.lines.with_index { |line, i| puts "#{i + 1} #{line}" }) 60 | 61 | messages.join("\n") 62 | end 63 | end 64 | 65 | @parser = EmeraldParser.new 66 | 67 | # Parse the preprocessed emerald text and print failure if it fails the 68 | # parsing stage 69 | def self.parse_grammar(text, original, source_map) 70 | parsed = @parser.parse(text) 71 | 72 | if parsed.nil? 73 | source_line = source_map[@parser.failure_line][:source_line] 74 | if source_line.nil? 75 | raise PreProcessorError.new( 76 | @parser.failure_line, 77 | @parser.failure_reason, 78 | text 79 | ) 80 | end 81 | raise ParserError.new( 82 | source_line, 83 | @parser.failure_reason, 84 | original 85 | ) 86 | end 87 | 88 | parsed 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/emerald/grammar/emerald.tt: -------------------------------------------------------------------------------- 1 | # 2 | # Context free grammar for the Emerald language. 3 | # Emerald: a preprocessor for html5. 4 | # 5 | grammar Emerald 6 | include Tokens 7 | include Scopes 8 | include Variables 9 | 10 | rule main 11 | (scope / pair_list / value_list / nested / line / comment)+ 12 | end 13 | 14 | rule nested 15 | tag_statement newline lbrace newline main rbrace newline 16 | end 17 | 18 | rule scope 19 | fn:scope_fn lbrace newline body:main rbrace newline 20 | end 21 | 22 | rule line 23 | (tag_statement / comment) newline 24 | end 25 | 26 | rule value_list 27 | keyword:special_keyword newline lbrace newline 28 | list_items:(literal:inline_literal_string newline)+ 29 | rbrace newline 30 | end 31 | 32 | rule pair_list 33 | keyword:base_keyword newline lbrace newline 34 | list_items:(pairs:(attr:attr space+ literal:inline_literal_string space*)+ newline)+ 35 | rbrace newline 36 | end 37 | 38 | rule comment 39 | space* '*' space* text_content 40 | end 41 | 42 | rule text_content 43 | multiline_literal / multiline_templateless_literal / inline_literal 44 | end 45 | 46 | rule multiline_literal 47 | "->" space* newline 48 | body:(variable / escaped / !'$' .)* 49 | "$" 50 | end 51 | 52 | rule multiline_templateless_literal 53 | ("=>" / "~>") space* newline 54 | body:(escaped / !'$' .)* 55 | "$" 56 | end 57 | 58 | rule inline_literal 59 | body:( variable / escaped / !lparen !newline . )* 60 | end 61 | 62 | rule inline_literal_string 63 | '"' body:( variable / escaped / !'"' . )* '"' 64 | end 65 | 66 | rule escaped 67 | "\\" . 68 | end 69 | 70 | rule tag_statement 71 | tag identifier:id_name? classes:class_name* space* body:text_content? attributes:attr_list? 72 | end 73 | 74 | rule id_name 75 | '#' name:([a-zA-Z_\-]+) 76 | end 77 | 78 | rule class_name 79 | '.' name:([a-zA-Z_\-]+) 80 | end 81 | 82 | rule attr_list 83 | lparen newline lbrace newline attributes rbrace newline rparen 84 | end 85 | 86 | rule attributes 87 | attribute* 88 | end 89 | 90 | rule attribute 91 | attr space* inline_literal_string newline 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/emerald/grammar/scopes.tt: -------------------------------------------------------------------------------- 1 | # 2 | # Context free grammar of tokens (terminal 3 | # nodes) for the Emerald language. 4 | # 5 | grammar Scopes 6 | include Variables 7 | 8 | rule scope_fn 9 | (given / unless / each / with) "\n" 10 | 11 | end 12 | 13 | rule given 14 | "given" space* boolean_expr 15 | end 16 | 17 | rule unless 18 | "unless" space* boolean_expr 19 | end 20 | 21 | rule with 22 | "with" space* variable_name 23 | end 24 | 25 | rule each 26 | "each" space* collection:variable_name 27 | space* "as" space* val_name:variable_name 28 | indexed:(',' space* key_name:variable_name)? 29 | 30 | end 31 | 32 | rule boolean_expr 33 | binary_expr / unary_expr 34 | end 35 | 36 | rule binary_expr 37 | lhs:unary_expr 38 | space+ op:("and" / "or") space* 39 | rhs:boolean_expr 40 | 41 | end 42 | 43 | rule unary_expr 44 | negated:("not" space+)? 45 | (val:variable_name / '(' space* val:boolean_expr space* ')') 46 | 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/emerald/grammar/tokens.tt: -------------------------------------------------------------------------------- 1 | # 2 | # Context free grammar of tokens (terminal 3 | # nodes) for the Emerald language. 4 | # 5 | grammar Tokens 6 | rule base_keyword 7 | 'images' / 'metas' / 'scripts' / 'styles' 8 | end 9 | 10 | rule special_keyword 11 | 'images' / 'scripts' / 'styles' 12 | end 13 | 14 | rule tag 15 | [a-z] [a-z1-9]* 16 | end 17 | 18 | rule attr 19 | [a-z\-_]+ 20 | end 21 | 22 | rule event 23 | 'onabort' / 'onclick' / 'onhover' / 'onbeforeprint' / 'onbeforeunload' 24 | end 25 | 26 | rule equals 27 | '=' 28 | end 29 | 30 | rule comma 31 | ',' 32 | end 33 | 34 | rule lparen 35 | '(' 36 | end 37 | 38 | rule rparen 39 | ')' 40 | end 41 | 42 | rule lbrace 43 | "{" 44 | end 45 | 46 | rule rbrace 47 | "}" 48 | end 49 | 50 | rule space 51 | ' ' 52 | end 53 | 54 | rule newline 55 | [\n] 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/emerald/grammar/variables.tt: -------------------------------------------------------------------------------- 1 | # Variable support in Emerald 2 | grammar Variables 3 | rule variable 4 | '|' variable_name '|' 5 | end 6 | 7 | rule variable_name 8 | [a-zA-Z0-9_]+ ('.' variable_name)* 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/emerald/index.emr: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | styles 4 | "public/css/main.emr" 5 | 6 | scripts 7 | "public/js/main.js" 8 | 9 | body 10 | nav 11 | a Emerald 12 | a Docs 13 | a Github 14 | 15 | header 16 | h1 Emerald! 17 | h2 A templating engine for multiple languages. 18 | 19 | main 20 | section 21 | h3 About 22 | p Emerald is a templating engine designed for use in multiple languages. 23 | p Emerald may be used as a templating engine for node, python, ruby, or go web applications. 24 | h1 -> 25 | this is some literal text, not an actual tag 26 | with an indent 27 | and multiple lines 28 | and an e$cape character 29 | p and a tag 30 | p this is an actual tag now 31 | 32 | footer 33 | p Copyright Emerald Language 2016 34 | -------------------------------------------------------------------------------- /lib/emerald/nodes/attribute_list.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'node' 6 | 7 | # elements[2] is the attributes 8 | # call to_html() on them. 9 | class AttributeList < Node 10 | def to_html(context) 11 | elements[4].to_html(context) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/emerald/nodes/attributes.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'node' 6 | 7 | # The attributes hash for an element 8 | class Attributes < Node 9 | def to_html(context) 10 | output = '' 11 | elements.each do |e| 12 | output += " #{e.elements[0].text_value}=\"#{e.elements[2].to_html(context)}\"" 13 | end 14 | output 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/emerald/nodes/base_scope_fn.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | 6 | # Base class for scope functions 7 | class BaseScopeFn < Treetop::Runtime::SyntaxNode 8 | def to_html(_body, _context) 9 | raise 'not implemented :(' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/emerald/nodes/binary_expr.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'boolean_expr' 6 | 7 | # A boolean expression with two children and an operator 8 | class BinaryExpr < BooleanExpr 9 | def truthy?(context) 10 | case op.text_value 11 | when 'and' 12 | lhs.truthy?(context) && rhs.truthy?(context) 13 | when 'or' 14 | lhs.truthy?(context) || rhs.truthy?(context) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/emerald/nodes/boolean_expr.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | 6 | # The boolean condition for a logic statement 7 | class BooleanExpr < Treetop::Runtime::SyntaxNode 8 | def truthy?(context) 9 | elements.first.truthy?(context) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/emerald/nodes/comment.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'node' 6 | 7 | # Prints out the value of the comment's text 8 | class Comment < Node 9 | def to_html(context) 10 | "\n" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/emerald/nodes/each.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'base_scope_fn' 6 | 7 | # Function to map over elements in the context 8 | class Each < BaseScopeFn 9 | def to_html(body, context) 10 | vars = collection.content(context) 11 | key_name = indexed.text_value.length.positive? ? indexed.key_name : nil 12 | 13 | # TODO: clean up somehow 14 | if vars.is_a? Hash 15 | vars 16 | .map do |var, key| 17 | new_ctx = context.clone 18 | new_ctx[val_name.text_value] = var 19 | new_ctx[key_name.text_value] = key if key_name 20 | 21 | body.to_html(new_ctx) 22 | end 23 | .join("\n") 24 | elsif vars.is_a? Array 25 | vars 26 | .map.with_index do |var, idx| 27 | new_ctx = context.clone 28 | new_ctx[val_name.text_value] = var 29 | new_ctx[key_name.text_value] = idx if key_name 30 | 31 | body.to_html(new_ctx) 32 | end 33 | .join("\n") 34 | elsif vars.nil? 35 | '' 36 | else 37 | raise 'bad variable type :(' 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/emerald/nodes/given.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'base_scope_fn' 6 | 7 | # Renders if a condition is met 8 | class Given < BaseScopeFn 9 | def to_html(body, context) 10 | if boolean_expr.truthy?(context) 11 | body.to_html(context) 12 | else 13 | '' 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/emerald/nodes/line.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'node' 6 | 7 | # Either a tag or text 8 | class Line < Node 9 | def to_html(context) 10 | e = elements[0] 11 | e.to_html(context) 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/emerald/nodes/nested.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'node' 6 | 7 | # 8 | # Appends compiled html for nested rule to the output. 9 | # elements[0] is the tag statement, elements[4] is any nested html. 10 | # 11 | class Nested < Node 12 | def to_html(context) 13 | elements[0].opening_tag(context) + 14 | elements[4].to_html(context) + 15 | elements[0].closing_tag(context) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/emerald/nodes/node.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | 6 | # Base class for all Emerald syntax nodes 7 | class Node < Treetop::Runtime::SyntaxNode 8 | def to_html(_context) 9 | raise 'not implemented :(' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/emerald/nodes/pair_list.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'node' 6 | 7 | # Base rule for lists of images, metas, styles, and scripts 8 | class PairList < Node 9 | def to_html(context) 10 | list_items.elements.map do |e| 11 | attrs = e.pairs.elements.map do |j| 12 | "#{j.attr.text_value}=\"#{j.literal.to_html(context)}\"" 13 | end.join(' ') 14 | 15 | case keyword.text_value 16 | when 'images' then "" 17 | when 'metas' then "" 18 | when 'styles' then "" 19 | when 'scripts' then "" 20 | end 21 | end.join("\n") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/emerald/nodes/root.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'node' 6 | 7 | # A base piece of an Emerald file 8 | class Root < Node 9 | def to_html(context) 10 | elements 11 | .map { |e| e.to_html(context) } 12 | .join("\n") 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/emerald/nodes/scope.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'node' 6 | 7 | # Block that modifies the context 8 | class Scope < Node 9 | def to_html(context) 10 | fn.to_html(body, context) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/emerald/nodes/scope_fn.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'base_scope_fn' 6 | 7 | # Base class for scope functions 8 | class ScopeFn < BaseScopeFn 9 | def to_html(body, context) 10 | elements[0].to_html(body, context) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/emerald/nodes/tag_statement.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'node' 6 | 7 | # A tag 8 | class TagStatement < Node 9 | # http://w3c.github.io/html/syntax.html#void-elements 10 | VOID_TAGS = ( 11 | "area base br col embed hr img input link " + 12 | "menuitem meta param source track wbr" 13 | ).split(/\s+/) 14 | 15 | def to_html(context) 16 | if void_tag? 17 | opening_tag(context) 18 | else 19 | opening_tag(context) + 20 | ( 21 | if !body.empty? 22 | body.to_html(context) 23 | else 24 | '' 25 | end 26 | ) + 27 | closing_tag(context) 28 | end 29 | end 30 | 31 | def class_attribute 32 | unless classes.empty? 33 | class_names = classes 34 | .elements 35 | .map{ |c| c.name.text_value } 36 | .join(' ') 37 | 38 | " class=\"#{class_names}\"" 39 | else 40 | '' 41 | end 42 | end 43 | 44 | def id_attribute 45 | unless identifier.empty? 46 | " id=\"#{identifier.name.text_value}\"" 47 | else 48 | '' 49 | end 50 | end 51 | 52 | def opening_tag(context) 53 | "<#{tag.text_value}" + 54 | id_attribute + 55 | class_attribute + 56 | ( 57 | if !attributes.empty? 58 | ' ' + attributes.to_html(context) 59 | else 60 | '' 61 | end 62 | ) + ( 63 | if void_tag? 64 | ' />' 65 | else 66 | '>' 67 | end 68 | ) 69 | end 70 | 71 | def void_tag? 72 | VOID_TAGS.include? tag.text_value 73 | end 74 | 75 | def closing_tag(_context) 76 | "" 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /lib/emerald/nodes/text_literal.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'node' 6 | 7 | # A long block of text literal, with variable templating 8 | class TextLiteral < Node 9 | def to_html(context) 10 | body 11 | .elements 12 | .map do |element| 13 | if element.is_a?(Variable) 14 | element.to_html(context) 15 | else 16 | unescape element.text_value 17 | end 18 | end 19 | .join('') 20 | .rstrip 21 | end 22 | 23 | def unescape(text) 24 | text.gsub(/\\(.)/, '\1') 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/emerald/nodes/unary_expr.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'boolean_expr' 6 | 7 | # A boolean expression with one child 8 | class UnaryExpr < BooleanExpr 9 | def truthy?(context) 10 | if negated.text_value.length.positive? 11 | !elements[1].val.truthy?(context) 12 | else 13 | elements[1].val.truthy?(context) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/emerald/nodes/unless.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'base_scope_fn' 6 | 7 | # Renders unless a condition is met 8 | class Unless < BaseScopeFn 9 | def to_html(body, context) 10 | if boolean_expr.truthy?(context) 11 | '' 12 | else 13 | body.to_html(context) 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/emerald/nodes/value_list.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'node' 6 | 7 | # Special rule for lists of images, styles, and scripts 8 | class ValueList < Node 9 | def to_html(context) 10 | list_items.elements.map do |e| 11 | case keyword.text_value 12 | when 'images' then "" 13 | when 'styles' then "" 14 | when 'scripts' then "" 15 | end 16 | end.join("\n") 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/emerald/nodes/variable.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'node' 6 | 7 | # Variable interpolation in a template 8 | class Variable < Node 9 | def to_html(context) 10 | variable_name 11 | .content(context) 12 | .to_s 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/emerald/nodes/variable_name.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'boolean_expr' 6 | 7 | # Variable interpolation in a template 8 | class VariableName < BooleanExpr 9 | def content(context) 10 | text_value 11 | .split('.') 12 | .reduce(context) do |ctx, name| 13 | next nil if ctx.nil? 14 | ctx[name] || ctx[name.to_sym] || nil 15 | end 16 | end 17 | 18 | def truthy?(context) 19 | !!content(context) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/emerald/nodes/with.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'treetop' 5 | require_relative 'base_scope_fn' 6 | 7 | # Isolates the scope to a subset of the context 8 | class With < BaseScopeFn 9 | def to_html(body, context) 10 | var = elements[2].content(context) 11 | 12 | body.to_html(var) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/emerald/preprocessor.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'htmlentities' 5 | require_relative 'grammar' 6 | 7 | module Emerald 8 | # 9 | # Preprocess the emerald code and add notion of indentation so it may be parsed 10 | # by a context free grammar. Removes all whitespace and adds braces to denote 11 | # indentation. 12 | # 13 | class PreProcessor 14 | def initialize 15 | reset 16 | end 17 | 18 | # Reset class variables, used for testing 19 | def reset 20 | @in_literal = false 21 | @templateless_literal = false 22 | @current_indent = 0 23 | @b_count = 0 24 | @output = '' 25 | @encoder = HTMLEntities.new 26 | @source_map = {} 27 | end 28 | 29 | # Process the emerald to remove indentation and replace with brace convention 30 | # for an easier time parsing with context free grammar 31 | def process_emerald(input) 32 | input.each_line.with_index do |line, line_number| 33 | if @in_literal 34 | if line[0...-1].empty? 35 | new_indent = @current_indent 36 | line = " " * @current_indent + "\n" 37 | else 38 | new_indent = line.length - line.lstrip.length 39 | end 40 | else 41 | next if line.lstrip.empty? 42 | new_indent = line.length - line.lstrip.length 43 | end 44 | 45 | check_new_indent(new_indent) 46 | @output += remove_indent_whitespace(line) 47 | @source_map[@output.lines.length] = { 48 | source_line: line_number + 1 49 | } 50 | check_and_enter_literal(line) 51 | end 52 | 53 | close_tags(0) 54 | 55 | [@output, @source_map] 56 | end 57 | 58 | # Compares the value of the new_indent with the old indentation. 59 | # Invoked by: process_emerald 60 | def check_new_indent(new_indent) 61 | if new_indent > @current_indent 62 | open_tags(new_indent) 63 | elsif new_indent < @current_indent 64 | close_tags(new_indent) 65 | end 66 | end 67 | 68 | def open_tags(new_indent) 69 | return if @in_literal 70 | @output += "{\n" 71 | @b_count += 1 72 | @current_indent = new_indent 73 | end 74 | 75 | def close_tags(new_indent) 76 | if @in_literal 77 | close_literal(new_indent) 78 | else 79 | close_entered_tags(new_indent) 80 | end 81 | @current_indent = new_indent 82 | end 83 | 84 | # Append closing braces if in literal and new indent is less than old one 85 | def close_literal(new_indent) 86 | @output += "$\n" 87 | @in_literal = false 88 | 89 | (2..((@current_indent - new_indent) / 2)).each do 90 | @output += "}\n" 91 | @b_count -= 1 92 | end 93 | end 94 | 95 | # Append closing braces if not in literal and new indent is less than old one 96 | def close_entered_tags(new_indent) 97 | (1..((@current_indent - new_indent) / 2)).each do 98 | @output += "}\n" 99 | @b_count -= 1 100 | end 101 | end 102 | 103 | # Crop off only Emerald indent whitespace to preserve whitespace in the 104 | # literal. Since $ is the end character, we need to escape it in the literal. 105 | def remove_indent_whitespace(line) 106 | if @in_literal 107 | # Ignore indent whitespace, only count post-indent as the literal, 108 | # but keep any extra whitespace that might exist for literals 109 | cropped = line[@current_indent..-1] || '' 110 | if @templateless_literal 111 | # this is a fun one https://www.ruby-forum.com/topic/143645 112 | cropped = cropped.gsub("\\"){ "\\\\" } 113 | end 114 | 115 | unless @preserve_html_literal 116 | cropped = @encoder.encode cropped 117 | end 118 | 119 | # Escape $ since we use it as a terminator for literals, and encode HTML 120 | cropped.gsub('$', "\\$") 121 | else 122 | line.lstrip 123 | end 124 | end 125 | 126 | def check_and_enter_literal(line) 127 | if line.rstrip.end_with?('->') 128 | @in_literal = true 129 | @current_indent += 2 130 | @templateless_literal = false 131 | @preserve_html_literal = false 132 | elsif line.rstrip.end_with?('=>') 133 | @in_literal = true 134 | @current_indent += 2 135 | @templateless_literal = true 136 | @preserve_html_literal = false 137 | elsif line.rstrip.end_with?('~>') 138 | @in_literal = true 139 | @current_indent += 2 140 | @templateless_literal = true 141 | @preserve_html_literal = true 142 | end 143 | end 144 | end 145 | end 146 | -------------------------------------------------------------------------------- /lib/emerald/version.rb: -------------------------------------------------------------------------------- 1 | module Emerald 2 | VERSION = "1.0.0" 3 | end 4 | -------------------------------------------------------------------------------- /sample.emr: -------------------------------------------------------------------------------- 1 | * Emerald Language 2 | 3 | doctype html 4 | 5 | html 6 | head 7 | styles 8 | "css/main.css" 9 | "css/vendor/bootstrap.min.css" 10 | 11 | scripts 12 | "js/script.js" 13 | "js/other_script.js" 14 | 15 | style 16 | var black = #333 17 | var blue = #0066ff 18 | 19 | body 20 | header 21 | h1 Emerald 22 | h2 An html5 markup language designed with event driven 23 | applications in mind. 24 | 25 | main 26 | section 27 | h1 Why use Emerald? 28 | p Emerald allows you to scope events and styles to html 29 | elements in an elegant, clean way. 30 | 31 | figure 32 | figcaption Here's an example of elements scoped in a 33 | button here. 34 | 35 | button Click me. ( 36 | click -> console.log("I was clicked!") 37 | hover -> console.log("I was hovered!") 38 | ) 39 | 40 | footer ( 41 | hover -> 42 | this.border = 1px solid @blue 43 | this.text-shadow = 0px 0px 8px 2px rgba(0,0,0,0.3) 44 | ) 45 | p Like what you see? Check out the docs for more samples. 46 | -------------------------------------------------------------------------------- /spec/emerald_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'spec_helper' 3 | 4 | describe Emerald do 5 | it 'has a version number' do 6 | expect(Emerald::VERSION).not_to be nil 7 | end 8 | 9 | it 'shows meaningful errors' do 10 | input = <<~EMR 11 | html 12 | body 13 | h1 Hello, world ( 14 | class something 15 | ) 16 | p Test 17 | EMR 18 | expect{ Emerald.convert(input) }.to raise_error(Grammar::ParserError) do |e| 19 | expect(e.message).to include('>>> class something') 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/functional/general_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'spec_helper' 5 | 6 | describe Emerald do 7 | context 'attributes' do 8 | it 'supports nested tags with attributes' do 9 | expect( 10 | convert( 11 | context: {}, 12 | input: <<~EMR, 13 | section ( 14 | class "something" 15 | ) 16 | h1 Test 17 | EMR 18 | ) 19 | ).to eq('

Test

') 20 | end 21 | 22 | it 'text rule works with attributes' do 23 | expect( 24 | convert( 25 | context: {}, 26 | input: <<~EMR, 27 | h1 Attributes follow parentheses. ( 28 | id "header" 29 | class "main-text" 30 | height "50px" 31 | width "200px" 32 | ) 33 | EMR 34 | ) 35 | ).to eq('

Attributes follow parentheses.

') 36 | end 37 | 38 | it 'tag statement rule works with attributes' do 39 | expect( 40 | convert( 41 | context: {}, 42 | input: <<~EMR, 43 | section Attributes follow parentheses. ( 44 | id "header" 45 | class "main-text" 46 | height "50px" 47 | width "200px" 48 | ) 49 | EMR 50 | ) 51 | ).to eq('') 53 | end 54 | end 55 | 56 | context 'comments' do 57 | it 'supports single line comments' do 58 | expect( 59 | convert( 60 | context: {}, 61 | input: <<~EMR, 62 | * test 63 | h1 test 64 | EMR 65 | ) 66 | ).to eq('

test

') 67 | end 68 | 69 | it 'supports multiline comments' do 70 | expect( 71 | convert( 72 | context: {}, 73 | input: <<~EMR, 74 | * -> 75 | test 76 | h1 test 77 | EMR 78 | ) 79 | ).to eq('

test

') 80 | end 81 | end 82 | 83 | context 'inline identifiers' do 84 | it 'converts classes' do 85 | expect( 86 | convert( 87 | context: {}, 88 | input: <<~EMR, 89 | h1.something test 90 | EMR 91 | ) 92 | ).to eq('

test

') 93 | end 94 | 95 | it 'converts multiple classes' do 96 | expect( 97 | convert( 98 | context: {}, 99 | input: <<~EMR, 100 | h1.a.b test 101 | EMR 102 | ) 103 | ).to eq('

test

') 104 | end 105 | 106 | it 'supports ids' do 107 | expect( 108 | convert( 109 | context: {}, 110 | input: <<~EMR, 111 | h1#some-id_ test 112 | EMR 113 | ) 114 | ).to eq('

test

') 115 | end 116 | 117 | it 'supports classes and ids' do 118 | expect( 119 | convert( 120 | context: {}, 121 | input: <<~EMR, 122 | h1#a.b.c test 123 | EMR 124 | ) 125 | ).to eq('

test

') 126 | end 127 | 128 | it 'self-closes void tags' do 129 | expect( 130 | convert( 131 | context: {}, 132 | input: <<~EMR, 133 | img 134 | EMR 135 | ) 136 | ).to eq('') 137 | end 138 | end 139 | 140 | context 'list rules' do 141 | it 'supports \'images\' special rule' do 142 | expect( 143 | convert( 144 | context: {}, 145 | input: <<~EMR, 146 | images 147 | "images/nav/home.png" 148 | "images/nav/about.png" 149 | "images/nav/blog.png" 150 | "images/nav/contact.png" 151 | EMR 152 | ) 153 | ).to eq(" ") 154 | end 155 | 156 | it 'supports \'images\' base rule' do 157 | expect( 158 | convert( 159 | context: {}, 160 | input: <<~EMR, 161 | images 162 | src "images/nav/home.png" 163 | src "images/nav/about.png" 164 | src "images/nav/blog.png" 165 | src "images/nav/contact.png" 166 | EMR 167 | ) 168 | ).to eq(" ") 169 | end 170 | 171 | it 'supports \'scripts\' special rule' do 172 | expect( 173 | convert( 174 | context: {}, 175 | input: <<~EMR, 176 | scripts 177 | "vendor/jquery.js" 178 | "js/main.js" 179 | EMR 180 | ) 181 | ).to eq(" ") 182 | end 183 | 184 | it 'supports \'scripts\' base rule' do 185 | expect( 186 | convert( 187 | context: {}, 188 | input: <<~EMR, 189 | scripts 190 | type "text/javascript" src "vendor/jquery.js" 191 | type "text/javascript" src "js/main.js" 192 | EMR 193 | ) 194 | ).to eq(" ") 195 | end 196 | 197 | it 'supports \'styles\' special rule' do 198 | expect( 199 | convert( 200 | context: {}, 201 | input: <<~EMR, 202 | styles 203 | "css/main.css" 204 | "css/footer.css" 205 | EMR 206 | ) 207 | ).to eq(" ") 208 | end 209 | 210 | it 'supports \'styles\' base rule' do 211 | expect( 212 | convert( 213 | context: {}, 214 | input: <<~EMR, 215 | styles 216 | href "css/main.css" type "text/css" 217 | href "css/footer.css" type "text/css" 218 | EMR 219 | ) 220 | ).to eq(" ") 221 | end 222 | 223 | it 'supports \'metas\' base rule' do 224 | expect( 225 | convert( 226 | context: {}, 227 | input: <<~EMR, 228 | metas 229 | name "test-name" content "test-content" 230 | name "test-name-2" content "test-content-2" 231 | EMR 232 | ) 233 | ).to eq(" ") 234 | end 235 | end 236 | end 237 | -------------------------------------------------------------------------------- /spec/functional/scope_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'spec_helper' 5 | 6 | # 7 | # Unit testing for Emerald Language. Asserts that emerald passed in is 8 | # compiled to valid html, which is equivalent in semantic value. 9 | # 10 | describe Emerald do 11 | context 'given/unless' do 12 | it 'can determine truthiness' do 13 | expect(convert( 14 | context: {test: true}, 15 | input: <<~EMR, 16 | given test 17 | h1 True 18 | unless test 19 | h1 False 20 | EMR 21 | )).to eq('

True

') 22 | end 23 | 24 | it 'can determine falsiness' do 25 | expect(convert( 26 | context: {test: false}, 27 | input: <<~EMR, 28 | given test 29 | h1 True 30 | unless test 31 | h1 False 32 | EMR 33 | )).to eq('

False

') 34 | end 35 | 36 | it 'can check if a variable is present' do 37 | expect(convert( 38 | context: {}, 39 | input: <<~EMR, 40 | given test 41 | h1 True 42 | unless test 43 | h1 False 44 | EMR 45 | )).to eq('

False

') 46 | end 47 | end 48 | 49 | context 'boolean expressions' do 50 | it 'can do OR checks' do 51 | expect(convert( 52 | context: {a: true, b: false}, 53 | input: <<~EMR, 54 | given a or b 55 | h1 True 56 | EMR 57 | )).to eq('

True

') 58 | 59 | expect(convert( 60 | context: {a: false, b: false}, 61 | input: <<~EMR, 62 | given a or b 63 | h1 True 64 | EMR 65 | )).to eq('') 66 | end 67 | 68 | it 'can do AND checks' do 69 | expect(convert( 70 | context: {a: true, b: true}, 71 | input: <<~EMR, 72 | given a and b 73 | h1 True 74 | EMR 75 | )).to eq('

True

') 76 | 77 | expect(convert( 78 | context: {a: true, b: false}, 79 | input: <<~EMR, 80 | given a and b 81 | h1 True 82 | EMR 83 | )).to eq('') 84 | end 85 | 86 | it 'can do negation' do 87 | expect(convert( 88 | context: {a: false}, 89 | input: <<~EMR, 90 | given not a 91 | h1 True 92 | EMR 93 | )).to eq('

True

') 94 | 95 | expect(convert( 96 | context: {a: true}, 97 | input: <<~EMR, 98 | given not a 99 | h1 True 100 | EMR 101 | )).to eq('') 102 | end 103 | 104 | it 'can combine expressions' do 105 | expect(convert( 106 | context: {a: true, b: true, c: true}, 107 | input: <<~EMR, 108 | given a and b and c 109 | h1 True 110 | EMR 111 | )).to eq('

True

') 112 | 113 | expect(convert( 114 | context: {a: false, b: true, c: true}, 115 | input: <<~EMR, 116 | given a or (b and c) 117 | h1 True 118 | EMR 119 | )).to eq('

True

') 120 | 121 | expect(convert( 122 | context: {a: false, b: true, c: true}, 123 | input: <<~EMR, 124 | given a or (b and c) 125 | h1 True 126 | EMR 127 | )).to eq('

True

') 128 | end 129 | end 130 | 131 | context 'loops' do 132 | it 'can loop over arrays' do 133 | expect(convert( 134 | context: {a: [1, 2, 3]}, 135 | input: <<~EMR, 136 | each a as element 137 | h1 |element| 138 | EMR 139 | )).to eq('

1

2

3

') 140 | end 141 | 142 | it 'can loop over arrays with indices' do 143 | expect(convert( 144 | context: {a: ['a', 'b', 'c']}, 145 | input: <<~EMR, 146 | each a as element, i 147 | h1 |i| 148 | EMR 149 | )).to eq('

0

1

2

') 150 | end 151 | 152 | it 'can loop over hashes' do 153 | expect(convert( 154 | context: {a: {b: 'c'}}, 155 | input: <<~EMR, 156 | each a as v, k 157 | h1 |v| |k| 158 | EMR 159 | )).to eq('

b c

') 160 | end 161 | end 162 | 163 | context 'with' do 164 | it 'can push scope' do 165 | expect(convert( 166 | context: {a: {b: 'c'}}, 167 | input: <<~EMR, 168 | with a 169 | h1 |b| 170 | EMR 171 | )).to eq('

c

') 172 | end 173 | end 174 | end 175 | -------------------------------------------------------------------------------- /spec/functional/templating_spec.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'spec_helper' 5 | 6 | describe Emerald do 7 | context 'templating' do 8 | it 'works with no templating' do 9 | expect( 10 | convert( 11 | context: {}, 12 | input: <<~EMR, 13 | h1 Hello, world 14 | EMR 15 | ) 16 | ).to eq('

Hello, world

') 17 | end 18 | 19 | it 'templating works for attributes' do 20 | expect( 21 | convert( 22 | context: {prefix: 'class-prefix'}, 23 | input: <<~EMR, 24 | section Attributes follow parentheses. ( 25 | id "header" 26 | class "|prefix|-text" 27 | height "50px" 28 | width "200px" 29 | ) 30 | EMR 31 | ) 32 | ).to eq('') 34 | end 35 | 36 | it 'works with simple templating' do 37 | expect( 38 | convert( 39 | context: {name: 'Dave'}, 40 | input: <<~EMR, 41 | h1 Hello, |name| 42 | EMR 43 | ) 44 | ).to eq('

Hello, Dave

') 45 | end 46 | 47 | it 'works with nested templating' do 48 | expect( 49 | convert( 50 | context: {person: {name: 'Dave'}}, 51 | input: <<~EMR, 52 | h1 Hello, |person.name| 53 | EMR 54 | ) 55 | ).to eq('

Hello, Dave

') 56 | end 57 | 58 | it 'works in multiline literals' do 59 | expect( 60 | convert( 61 | context: {name: 'Dave'}, 62 | input: <<~EMR, 63 | pre -> 64 | Hello, world, 65 | my name is |name| 66 | EMR 67 | ) 68 | ).to eq(whitespace_agnostic(<<~HTML)) 69 |
Hello, world,
 70 |         my name is Dave
71 | HTML 72 | end 73 | 74 | it 'does not template in multiline templateless literals' do 75 | expect( 76 | convert( 77 | context: {name: 'Dave'}, 78 | input: <<~EMR, 79 | h1 => 80 | Hello, world, 81 | my name is |name| 82 | EMR 83 | ) 84 | ).to eq(whitespace_agnostic(<<~HTML)) 85 |

Hello, world, 86 | my name is |name|

87 | HTML 88 | end 89 | end 90 | 91 | context 'escaping' do 92 | it 'handles escaped pipes' do 93 | expect( 94 | convert( 95 | context: {name: 'Dave'}, 96 | input: <<~EMR, 97 | h1 Hello, \\||name| 98 | EMR 99 | ) 100 | ).to eq('

Hello, |Dave

') 101 | end 102 | 103 | it 'handles escaped escaped pipes' do 104 | expect( 105 | convert( 106 | context: {name: 'Dave'}, 107 | input: <<~EMR, 108 | h1 Hello, \\\\|name| 109 | EMR 110 | ) 111 | ).to eq('

Hello, \Dave

') 112 | end 113 | 114 | it 'handles braces in multiline literals' do 115 | expect( 116 | convert( 117 | context: {}, 118 | input: <<~EMR, 119 | h1 => 120 | hey look a brace 121 | } 122 | EMR 123 | ) 124 | ).to eq('

hey look a brace }

') 125 | end 126 | 127 | it 'handles braces in inline literals' do 128 | expect( 129 | convert( 130 | context: {}, 131 | input: <<~EMR, 132 | h1 hey look a brace } 133 | EMR 134 | ) 135 | ).to eq('

hey look a brace }

') 136 | end 137 | 138 | it 'does not template templateless literals' do 139 | expect( 140 | convert( 141 | context: {}, 142 | input: <<~EMR, 143 | h1 => 144 | if (a || b) { 145 | console.log("truthy\\n"); 146 | } 147 | EMR 148 | ) 149 | ).to eq(whitespace_agnostic(<<~HTML)) 150 |

if (a || b) { 151 | console.log("truthy\\n"); 152 | }

153 | HTML 154 | end 155 | 156 | it 'escaping parentheses works' do 157 | expect( 158 | convert( 159 | context: {}, 160 | input: <<~EMR, 161 | section here's some text \\(and some stuff in brackets\\) ( 162 | class "something" 163 | ) 164 | EMR 165 | ) 166 | ).to eq('
here\'s some text (and some stuff in brackets)
') 167 | end 168 | end 169 | end 170 | -------------------------------------------------------------------------------- /spec/preprocessor/emerald/events/events.emr: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title Event Scoping Example 4 | 5 | body 6 | button ( 7 | click ~> 8 | alert("I was clicked") 9 | hover ~> 10 | alert("I was hovered") 11 | ) 12 | 13 | footer 14 | p Copyright Emerald Language 2016 15 | -------------------------------------------------------------------------------- /spec/preprocessor/emerald/events/form.emr: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title Form example 4 | 5 | body 6 | form#form-id 7 | h3 Name 8 | input ( 9 | type text 10 | name name 11 | autocomplete off 12 | ) 13 | 14 | h3 Email 15 | input ( 16 | type text 17 | name email 18 | autocomplete off 19 | ) 20 | 21 | h3 Message 22 | textarea ( 23 | type text 24 | name message 25 | autocomplete off 26 | ) 27 | 28 | button Submit ( 29 | type submit 30 | click -> 31 | validateForm() 32 | ) 33 | 34 | footer 35 | p Copyright Emerald Language 2016 36 | -------------------------------------------------------------------------------- /spec/preprocessor/emerald/general/attr.emr: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title Attributes Example 4 | 5 | body 6 | section Attributes follow parentheses. ( 7 | id "header" 8 | class "main-text" 9 | ) 10 | 11 | footer 12 | p Copyright Emerald Language 2016 13 | -------------------------------------------------------------------------------- /spec/preprocessor/emerald/general/html.emr: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | title Sample 1 4 | scripts 5 | "js/script.js" 6 | 7 | body 8 | div 9 | h1 Test this out 10 | 11 | div 12 | h2 Another one 13 | -------------------------------------------------------------------------------- /spec/preprocessor/emerald/general/metas.emr: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | metas 4 | name "keywords", content "HTML, CSS, XML, XHTML" 5 | name "description", content "Meta tags example" 6 | 7 | body 8 | header 9 | h1 Emerald 10 | h2 An html5 markup language designed with event driven 11 | applications in mind. 12 | -------------------------------------------------------------------------------- /spec/preprocessor/emerald/general/nested.emr: -------------------------------------------------------------------------------- 1 | html 2 | head 3 | a a 4 | 5 | body 6 | a a 7 | -------------------------------------------------------------------------------- /spec/preprocessor/emerald/general/sample.emr: -------------------------------------------------------------------------------- 1 | ! doctype 5 2 | 3 | * emerald sample 4 | 5 | html 6 | head 7 | title Title of an article 8 | 9 | styles 10 | "css/main.css" 11 | "css/styles.css" 12 | 13 | body 14 | header 15 | section(id="test" class="main-text") 16 | h1 This is some sample text. 17 | 18 | footer 19 | section 20 | div - 21 | -------------------------------------------------------------------------------- /spec/preprocessor/emerald/general/scopes.emr: -------------------------------------------------------------------------------- 1 | given test 2 | h1 True 3 | unless test 4 | h1 False 5 | -------------------------------------------------------------------------------- /spec/preprocessor/intermediate/events/events.txt: -------------------------------------------------------------------------------- 1 | html 2 | { 3 | head 4 | { 5 | title Event Scoping Example 6 | } 7 | body 8 | { 9 | button ( 10 | { 11 | click ~> 12 | alert("I was clicked") 13 | $ 14 | hover ~> 15 | alert("I was hovered") 16 | $ 17 | } 18 | ) 19 | } 20 | footer 21 | { 22 | p Copyright Emerald Language 2016 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /spec/preprocessor/intermediate/events/form.txt: -------------------------------------------------------------------------------- 1 | html 2 | { 3 | head 4 | { 5 | title Form example 6 | } 7 | body 8 | { 9 | form#form-id 10 | { 11 | h3 Name 12 | input ( 13 | { 14 | type text 15 | name name 16 | autocomplete off 17 | } 18 | ) 19 | h3 Email 20 | input ( 21 | { 22 | type text 23 | name email 24 | autocomplete off 25 | } 26 | ) 27 | h3 Message 28 | textarea ( 29 | { 30 | type text 31 | name message 32 | autocomplete off 33 | } 34 | ) 35 | button Submit ( 36 | { 37 | type submit 38 | click -> 39 | validateForm() 40 | $ 41 | } 42 | ) 43 | } 44 | footer 45 | { 46 | p Copyright Emerald Language 2016 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /spec/preprocessor/intermediate/general/attr.txt: -------------------------------------------------------------------------------- 1 | html 2 | { 3 | head 4 | { 5 | title Attributes Example 6 | } 7 | body 8 | { 9 | section Attributes follow parentheses. ( 10 | { 11 | id "header" 12 | class "main-text" 13 | } 14 | ) 15 | } 16 | footer 17 | { 18 | p Copyright Emerald Language 2016 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /spec/preprocessor/intermediate/general/html.txt: -------------------------------------------------------------------------------- 1 | html 2 | { 3 | head 4 | { 5 | title Sample 1 6 | scripts 7 | { 8 | "js/script.js" 9 | } 10 | } 11 | body 12 | { 13 | div 14 | { 15 | h1 Test this out 16 | } 17 | div 18 | { 19 | h2 Another one 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /spec/preprocessor/intermediate/general/metas.txt: -------------------------------------------------------------------------------- 1 | html 2 | { 3 | head 4 | { 5 | metas 6 | { 7 | name "keywords", content "HTML, CSS, XML, XHTML" 8 | name "description", content "Meta tags example" 9 | } 10 | } 11 | body 12 | { 13 | header 14 | { 15 | h1 Emerald 16 | h2 An html5 markup language designed with event driven 17 | applications in mind. 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /spec/preprocessor/intermediate/general/nested.txt: -------------------------------------------------------------------------------- 1 | html 2 | { 3 | head 4 | { 5 | a a 6 | } 7 | body 8 | { 9 | a a 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /spec/preprocessor/intermediate/general/sample.txt: -------------------------------------------------------------------------------- 1 | ! doctype 5 2 | * emerald sample 3 | html 4 | { 5 | head 6 | { 7 | title Title of an article 8 | styles 9 | { 10 | "css/main.css" 11 | "css/styles.css" 12 | } 13 | } 14 | body 15 | { 16 | header 17 | { 18 | section(id="test" class="main-text") 19 | { 20 | h1 This is some sample text. 21 | } 22 | } 23 | footer 24 | { 25 | section 26 | { 27 | div - 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /spec/preprocessor/intermediate/general/scopes.txt: -------------------------------------------------------------------------------- 1 | given test 2 | { 3 | h1 True 4 | } 5 | unless test 6 | { 7 | h1 False 8 | } 9 | -------------------------------------------------------------------------------- /spec/preprocessor/preprocessor_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | # Recursively walk directory and get all test case files for the test suite. 6 | # Performs preprocessing operation on each file and stores the results in a 7 | # hash dictionary, with file name as key. 8 | def walk(path, process, hash = {}) 9 | Dir.foreach(path) do |file| 10 | new_path = File.join(path, file) 11 | 12 | next if file == '..' || file == '.' 13 | hash = if process 14 | get_new_hash(hash, file, new_path) 15 | else 16 | get_expected(hash, file, new_path) 17 | end 18 | end 19 | 20 | hash 21 | end 22 | 23 | # Preprocesses emerald and returns it in the new hash. Walks the new path if 24 | # new path is a directory 25 | def get_new_hash(hash, file, new_path) 26 | if File.directory?(new_path) 27 | walk(new_path, true, hash) 28 | else 29 | hash[file[/[^\.]+/]], _ = Emerald::PreProcessor.new.process_emerald(IO.read(new_path)) 30 | end 31 | 32 | hash 33 | end 34 | 35 | def get_expected(hash, file, new_path) 36 | if File.directory?(new_path) 37 | walk(new_path, false, hash) 38 | else 39 | hash[file[/[^\.]+/]] = File.open(new_path, 'r').read 40 | end 41 | 42 | hash 43 | end 44 | 45 | describe Emerald::PreProcessor do 46 | # We only test the valid samlpes, because the invalid emerald samples will 47 | # also be preprocessed regardless of their semantics. Error checking happens 48 | # in the parsing and code generation phase. 49 | output = walk(__dir__ + '/emerald/', true) 50 | expected = walk(__dir__ + '/intermediate/', false) 51 | 52 | context 'general tests' do 53 | output.each do |key, value| 54 | it "works for #{key}" do 55 | expect(value).to eq(expected[key]) 56 | end 57 | end 58 | end 59 | 60 | context 'source maps' do 61 | it 'maps lines from output to input' do 62 | input = <<~EMR 63 | div 64 | h1 Test 65 | EMR 66 | map = { 67 | 1 => {source_line: 1}, 68 | 3 => {source_line: 2} 69 | } 70 | expect(source_map(input)).to eq(map) 71 | end 72 | end 73 | 74 | context 'multiline literals' do 75 | it 'preprocesses multiline literals' do 76 | input = <<~EMR 77 | h1 => 78 | This is a multiline literal 79 | EMR 80 | output = whitespace_agnostic <<~PREPROCESSED 81 | h1 => 82 | This is a multiline literal 83 | $ 84 | PREPROCESSED 85 | expect(preprocess(input)).to eq(output) 86 | end 87 | 88 | it 'encodes html entities' do 89 | input = <<~EMR 90 | h1 => 91 |
92 | EMR 93 | output = whitespace_agnostic <<~PREPROCESSED 94 | h1 => 95 | <div> 96 | $ 97 | PREPROCESSED 98 | expect(preprocess(input)).to eq(output) 99 | end 100 | 101 | it 'does not encode html entities for ~> literals' do 102 | input = <<~EMR 103 | h1 ~> 104 |
105 | EMR 106 | output = whitespace_agnostic <<~PREPROCESSED 107 | h1 ~> 108 |
109 | $ 110 | PREPROCESSED 111 | expect(preprocess(input)).to eq(output) 112 | end 113 | 114 | it 'escapes dollar signs' do 115 | input = <<~EMR 116 | h1 => 117 | Thi$ i$ a multiline literal 118 | EMR 119 | output = whitespace_agnostic <<~PREPROCESSED 120 | h1 => 121 | Thi\\$ i\\$ a multiline literal 122 | $ 123 | PREPROCESSED 124 | expect(preprocess(input)).to eq(output) 125 | end 126 | 127 | it 'preserves empty lines' do 128 | input = <<~EMR 129 | pre -> 130 | line 131 | 132 | line 133 | EMR 134 | output = <<~PREPROCESSED 135 | pre -> 136 | line 137 | 138 | line 139 | $ 140 | PREPROCESSED 141 | expect(preprocess(input, preserve_whitespace: true)).to eq(output) 142 | end 143 | end 144 | end 145 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 3 | require 'emerald' 4 | 5 | def whitespace_agnostic(str) 6 | str.gsub(/\s+/, ' ').strip 7 | end 8 | 9 | def convert(context:, input:, preserve_whitespace: false) 10 | output = Emerald.convert(input, context) 11 | unless preserve_whitespace 12 | whitespace_agnostic output 13 | else 14 | output 15 | end 16 | end 17 | 18 | def preprocess(input, preserve_whitespace: false) 19 | output, _ = Emerald::PreProcessor.new.process_emerald(input) 20 | unless preserve_whitespace 21 | whitespace_agnostic output 22 | else 23 | output 24 | end 25 | end 26 | 27 | def source_map(input) 28 | _, map = Emerald::PreProcessor.new.process_emerald(input) 29 | map 30 | end 31 | -------------------------------------------------------------------------------- /spec/treetop/treetop_suite.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'test/unit' 5 | require 'polyglot' 6 | require 'treetop' 7 | 8 | # Require all treetop nodes for grammar 9 | Dir[__dir__ + '/../../ruby/nodes/*.rb'].each { |f| require_relative f } 10 | 11 | Treetop.load __dir__ + '/../../ruby/grammar/tokens' 12 | Treetop.load __dir__ + '/../../ruby/grammar/variables' 13 | Treetop.load __dir__ + '/../../ruby/grammar/scopes' 14 | Treetop.load __dir__ + '/../../ruby/grammar/emerald' 15 | 16 | # 17 | # Unit testing for Treetop parser. Asserts that valid preprocessed emerald is 18 | # accepted and invalid preprocessed emerald is rejected. 19 | # 20 | class TreetopSuite < Test::Unit::TestCase 21 | # Walks the directory passed in and parses the preprocessed emerald to 22 | # check if it passes the treetop CFG. 23 | def walk(path, list = []) 24 | parser = EmeraldParser.new 25 | 26 | Dir.foreach(path) do |file| 27 | new_path = File.join(path, file) 28 | 29 | next if file == '..' || file == '.' 30 | list = get_new_list(list, new_path, parser) 31 | end 32 | 33 | list 34 | end 35 | 36 | # If the new path is a file, returns a new list with result of parsing 37 | # preprocessed file. If the new path is a directory, recursively walk 38 | # directory. 39 | def get_new_list(list, new_path, parser) 40 | if File.directory?(new_path) 41 | walk(new_path, list) 42 | else 43 | f = File.open(new_path) 44 | list.push([parser.parse(f.read), new_path]) 45 | end 46 | 47 | list 48 | end 49 | 50 | # Tests valid preprocessed files to ensure the files are parsed correctly 51 | # by the CFG, and do not fail in the parsing stage. 52 | def test_valid_samples 53 | output = walk(__dir__ + '/../preprocessor/intermediate/general') 54 | 55 | output.each do |out| 56 | puts out[1] if out[0].nil? 57 | assert_not_equal(out[0], nil) 58 | end 59 | end 60 | end 61 | --------------------------------------------------------------------------------