├── .gitignore ├── .gitmodules ├── Changelog.md ├── Makefile ├── README.md ├── VERSION ├── benchmarks ├── bench.lua └── bench.rb ├── bin └── luahaml ├── haml.lua ├── haml ├── code.lua ├── comment.lua ├── end_stack.lua ├── ext.lua ├── filter.lua ├── header.lua ├── lua_adapter.lua ├── parser.lua ├── precompiler.lua ├── renderer.lua ├── string_buffer.lua └── tag.lua ├── rockspecs ├── luahaml-0.1.0-0.rockspec ├── luahaml-0.1.1-0.rockspec ├── luahaml-0.2.0-0.rockspec └── luahaml-scm-1.rockspec ├── sample.haml ├── sample_apps └── hello.lua └── spec ├── parser_spec.lua ├── precompiler_spec.lua └── renderer_spec.lua /.gitignore: -------------------------------------------------------------------------------- 1 | doc 2 | .DS_Store 3 | docs 4 | *.swp 5 | coverage.html 6 | pkg 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "spec/haml-spec"] 2 | path = spec/haml-spec 3 | url = git://github.com/haml/haml-spec.git 4 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | ## 0.3.0 (NOT RELEASED YET) 2 | 3 | * String interpolation now works with local variables. 4 | 5 | * Fixed markup comment handling - previously LuaHaml didn't parse Haml inside 6 | comments, which was incorrect behavior. 7 | 8 | * Fixed bug where when the last line of Haml was immediately preceded by a 9 | markup comment, then the line was not added to the buffer. 10 | 11 | ## 0.2.0 (2012-06-18) 12 | 13 | * Added a __newindex function on to the metatable for Haml environments. 14 | (Thanks [Ross Andrews](https://github.com/randrews)) 15 | 16 | * Fix bug with mixed tabs and spaces 17 | (Thanks [Ross Andrews](https://github.com/randrews)) 18 | 19 | * Fix bug which prevented if/elseif/else from working properly 20 | 21 | * Fall back to renderer's locals if no locals are passed to the `partial` function. 22 | 23 | * Various small documentation fixes. 24 | 25 | 26 | ## 0.1.0 (2010-11-23) 27 | 28 | First release. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: spec test clean 2 | DIST_NAME = $(shell lua -e 'v=io.open("VERSION"):read();print("lua-haml-" .. v .. "-0")') 3 | 4 | test: 5 | @tsc spec/haml-spec/lua_haml_spec.lua spec/*_spec.lua 6 | 7 | spec: 8 | @tsc -f spec/haml-spec/lua_haml_spec.lua spec/*_spec.lua 9 | 10 | package: clean 11 | mkdir -p pkg 12 | git clone . pkg/$(DIST_NAME) 13 | rm -rf pkg/$(DIST_NAME)/.git 14 | cd pkg && tar czfp "$(DIST_NAME).tar.gz" $(DIST_NAME) 15 | rm -rf pkg/$(DIST_NAME) 16 | md5 pkg/$(DIST_NAME).tar.gz 17 | 18 | clean: 19 | rm -rf pkg 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lua Haml 2 | 3 | ## About 4 | 5 | Lua Haml is an implementation of the [Haml](http://haml.info) markup 6 | language for Lua. 7 | 8 | A Haml language reference can be found 9 | [here](http://haml.info/docs/yardoc/file.HAML_REFERENCE.html). 10 | 11 | Lua Haml implements almost 100% of Ruby Haml, and attempts to be as compatible 12 | as possible with it, with the following exceptions: 13 | 14 | * Your script blocks are in Lua rather than Ruby, obviously. 15 | * A few Ruby-specific filters are not implemented, namely `:maruku`, `:ruby` and `:sass`. 16 | * No attribute methods. This feature would have to be added to Ruby-style 17 | attributes which are discouraged in Lua-Haml, or the creation of a 18 | Lua-specific attribute format, which I don't want to add. 19 | * No object reference. This feature is idiomatic to the Rails framework and 20 | doesn't really apply to Lua. 21 | * No ugly mode. Because of how Lua Haml is designed, there's no performance 22 | penalty for outputting indented code, so there's no reason to implement this 23 | option. 24 | 25 | Here's a [Haml 26 | template](http://github.com/norman/lua-haml/tree/master/sample.haml) that uses 27 | most of Lua Haml's features. 28 | 29 | ## TODO 30 | 31 | Lua Haml is now feature complete, but is still considered beta quality. That 32 | said, I am using it for a production website, and will work quickly to fix any 33 | bugs that are reported. So please feel free to use it for serious work - just 34 | not the Space Shuttle, ok? 35 | 36 | 37 | ## Getting it 38 | 39 | The easiest way to install is via LuaRocks: 40 | 41 | luarocks install luahaml 42 | 43 | You can also always install the latest master branch from Git via Luarocks: 44 | 45 | luarocks install luahaml --from=http://luarocks.org/repositories/rocks-cvs 46 | 47 | ## Installing without Luarocks 48 | 49 | If you do not wish to use Luarocks, just put `haml.lua` and the `haml` directories 50 | somewhere on your package path, and place `luahaml` somewhere in your execution 51 | path. 52 | 53 | Here's one of many ways you could do this: 54 | 55 | git clone git://github.com/norman/lua-haml.git 56 | cd lua-haml 57 | cp bin/luahaml ~/bin 58 | cp -rp haml haml.lua /usr/local/my_lua_libs_dir 59 | export LUA_PATH=";;/usr/local/my_lua_libs_dir/?.lua" 60 | 61 | Note that you can also download a .zip or .tar.gz from Github if you do not use 62 | Git. 63 | 64 | 65 | ## Using it in your application 66 | 67 | Here's a simple usage example: 68 | 69 | -- in file.haml 70 | %p= "Hello, " .. name .. "!" 71 | 72 | -- in your application 73 | local haml = require "haml" 74 | local haml_options = {format = "html5"} 75 | local engine = haml.new(options) 76 | local locals = {name = "Joe"} 77 | local rendered = engine:render_file("file.haml", locals) 78 | 79 | -- output 80 |
Hello, Joe!
81 | 82 | ## Hacking it 83 | 84 | The [Github repository](http://github.com/norman/lua-haml) is located at: 85 | 86 | git://github.com/norman/lua-haml.git 87 | 88 | To run the specs, you should also install Telescope: 89 | 90 | luarocks install telescope 91 | 92 | You can then run them using [Tlua](http://github.com/norman/tlua), or do 93 | 94 | tsc `find . -name '*_spec.lua'` 95 | 96 | ## Bug reports 97 | 98 | Please report them on the [Github issue tracker](http://github.com/norman/lua-haml/issues). 99 | 100 | ## Author 101 | 102 | [Norman Clarke](mailto://norman@njclarke.com) 103 | 104 | ## Thanks 105 | 106 | To Hampton Caitlin, Nathan Weizenbaum and Chris Eppstein for their work on the 107 | original Haml. Thanks also to Daniele Alessandri for being LuaHaml's earliest 108 | "real" user, and a source of constant encouragement. 109 | 110 | ## License 111 | 112 | The MIT License 113 | 114 | Copyright (c) 2009-2010 Norman Clarke 115 | 116 | Permission is hereby granted, free of charge, to any person obtaining a copy of 117 | this software and associated documentation files (the "Software"), to deal in 118 | the Software without restriction, including without limitation the rights to 119 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 120 | the Software, and to permit persons to whom the Software is furnished to do so, 121 | subject to the following conditions: 122 | 123 | The above copyright notice and this permission notice shall be included in all 124 | copies or substantial portions of the Software. 125 | 126 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 127 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 128 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 129 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 130 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 131 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 132 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.2.0 2 | -------------------------------------------------------------------------------- /benchmarks/bench.lua: -------------------------------------------------------------------------------- 1 | require "socket" 2 | require "haml" 3 | 4 | local n = 5000 5 | 6 | local template = [=[ 7 | !!! html 8 | %html 9 | %head 10 | %title Test 11 | %body 12 | %h1 simple markup 13 | %div#content 14 | %ul 15 | - for _, letter in ipairs({"a", "b", "c", "d", "e", "f", "g"}) do 16 | %li= letter 17 | ]=] 18 | 19 | local start = socket.gettime() 20 | for i = 1,n do 21 | local engine = haml.new() 22 | local html = engine:render(template) 23 | end 24 | local done = socket.gettime() 25 | 26 | print "Compile and render:" 27 | print(("%s seconds"):format(done - start)) 28 | 29 | local engine = haml.new() 30 | local phrases = engine:parse(template) 31 | local compiled = engine:compile(phrases) 32 | local haml_renderer = require "haml.renderer" 33 | local renderer = haml_renderer.new(compiled) 34 | 35 | local start = socket.gettime() 36 | for i = 1,n do 37 | renderer:render(compiled) 38 | end 39 | local done = socket.gettime() 40 | 41 | print "Render:" 42 | print(("%s seconds"):format(done - start)) -------------------------------------------------------------------------------- /benchmarks/bench.rb: -------------------------------------------------------------------------------- 1 | require "benchmark" 2 | require "haml" 3 | 4 | n = 5000 5 | 6 | haml = '!!! html 7 | %html 8 | %head 9 | %title Test 10 | %body 11 | %h1 simple markup 12 | %div#content 13 | %ul 14 | - ["a", "b", "c", "d", "e", "f", "g"].each do |letter| 15 | %li= letter' 16 | 17 | compiled = Haml::Engine.new(haml, :format => :html5, :ugly => true) 18 | 19 | 20 | Benchmark.bmbm do |bench| 21 | bench.report("haml #{Haml::VERSION} - compile & render") do 22 | for i in 0..n do 23 | Haml::Engine.new(haml, :format => :html5, :ugly => true).render 24 | end 25 | end 26 | bench.report("haml #{Haml::VERSION} - render") do 27 | for i in 0..n do 28 | compiled.render 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /bin/luahaml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | pcall(require, "luarocks.require") 3 | 4 | local haml = require "haml" 5 | local ext = require "haml.ext" 6 | 7 | local VERSION = "0.2.0" 8 | 9 | local banner = "LuaHaml %s, copyright Norman Clarke12 | -- For more information on Haml, please see The Haml website 13 | -- and the Haml language reference. 14 | --
15 | module "haml" 16 | 17 | --- Default Haml options. 18 | -- @field format The output format. Can be xhtml, html4 or html5. Defaults to xhtml. 19 | -- @field encoding The output encoding. Defaults to utf-8. Note that this is merely informative; no recoding is done. 20 | -- @field newline The string value to use for newlines. Defaults to "\n". 21 | -- @field space The string value to use for spaces. Defaults to " ". 22 | default_haml_options = { 23 | adapter = "lua", 24 | attribute_wrapper = "'", 25 | auto_close = true, 26 | escape_html = false, 27 | encoding = "utf-8", 28 | format = "xhtml", 29 | indent = " ", 30 | newline = "\n", 31 | preserve = {pre = true, textarea = true}, 32 | space = " ", 33 | suppress_eval = false, 34 | -- provided for compatiblity; does nothing 35 | ugly = false, 36 | html_escapes = { 37 | ["'"] = ''', 38 | ['"'] = '"', 39 | ['&'] = '&', 40 | ['<'] = '<', 41 | ['>'] = '>' 42 | }, 43 | --- These tags will be auto-closed if the output format is XHTML (the default). 44 | auto_closing_tags = { 45 | area = true, 46 | base = true, 47 | br = true, 48 | col = true, 49 | hr = true, 50 | img = true, 51 | input = true, 52 | link = true, 53 | meta = true, 54 | param = true 55 | } 56 | } 57 | 58 | local methods = {} 59 | 60 | --- Render a Haml string. 61 | -- @param haml_string The Haml string 62 | -- @param options Options for the precompiler 63 | -- @param locals Local variable values to set for the rendered template 64 | function methods:render(haml_string, locals) 65 | local parsed = self:parse(haml_string) 66 | local compiled = self:compile(parsed) 67 | local rendered = renderer.new(compiled, self.options):render(locals) 68 | return rendered 69 | end 70 | 71 | --- Render a Haml file. 72 | -- @param haml_string The Haml file 73 | -- @param options Options for the precompiler 74 | -- @param locals Local variable values to set for the rendered template 75 | function methods:render_file(file, locals) 76 | local fh = assert(open(file)) 77 | local haml_string = fh:read '*a' 78 | fh:close() 79 | self.options.file = file 80 | return self:render(haml_string, locals) 81 | end 82 | 83 | function methods:parse(haml_string) 84 | return parser.tokenize(haml_string) 85 | end 86 | 87 | function methods:compile(parsed) 88 | return precompiler.new(self.options):precompile(parsed) 89 | end 90 | 91 | function new(options) 92 | local engine = {} 93 | engine.options = merge_tables(default_haml_options, options or {}) 94 | return setmetatable(engine, {__index = methods}) 95 | end 96 | -------------------------------------------------------------------------------- /haml/code.lua: -------------------------------------------------------------------------------- 1 | module "haml.code" 2 | 3 | local function ending_for(state) 4 | return state.adapter.ending_for(state.curr_phrase.code) 5 | end 6 | 7 | local function should_preserve(state) 8 | return state.curr_phrase.operator == "preserved_script" 9 | end 10 | 11 | local function should_escape(state) 12 | if state.curr_phrase.operator ~= "unescaped_script" then 13 | return state.curr_phrase.operator == "escaped_script" or state.options.escape_html 14 | end 15 | end 16 | 17 | function code_for(state) 18 | 19 | state.adapter.close_tags(state) 20 | 21 | if state.options.suppress_eval then 22 | return state.buffer:newline() 23 | end 24 | 25 | if state.curr_phrase.operator == "silent_script" then 26 | state.buffer:code(state.curr_phrase.code) 27 | local ending = ending_for(state) 28 | if ending then 29 | state.endings:push(ending) 30 | end 31 | else 32 | state.buffer:string(state.options.indent:rep(state.endings:indent_level())) 33 | if should_escape(state) then 34 | state.buffer:code(('r:b(r:escape_html(%s))'):format(state.curr_phrase.code)) 35 | elseif should_preserve(state) then 36 | state.buffer:code(('r:b(r:preserve_html(%s))'):format(state.curr_phrase.code)) 37 | else 38 | state.buffer:code(('r:b(%s)'):format(state.curr_phrase.code)) 39 | end 40 | state.buffer:newline() 41 | end 42 | end -------------------------------------------------------------------------------- /haml/comment.lua: -------------------------------------------------------------------------------- 1 | local ext = require "haml.ext" 2 | local strip = ext.strip 3 | 4 | module "haml.comment" 5 | 6 | function comment_for(state) 7 | state:close_tags() 8 | 9 | if state.curr_phrase.operator == "markup_comment" then 10 | if state.curr_phrase.unparsed then 11 | state.buffer:string(state:indents() .. "") 15 | state.buffer:newline() 16 | else 17 | -- state.buffer:string(state:indents() .. "", {newline = true}) 19 | state.buffer:string(state:indents() .. '") 21 | end 22 | 23 | elseif state.curr_phrase.operator == "conditional_comment" then 24 | state.buffer:string(state:indents() .. ("") 26 | end 27 | end -------------------------------------------------------------------------------- /haml/end_stack.lua: -------------------------------------------------------------------------------- 1 | local insert = table.insert 2 | local remove = table.remove 3 | local setmetatable = setmetatable 4 | local type = type 5 | 6 | module "haml.end_stack" 7 | 8 | local methods = {} 9 | 10 | function methods:push(ending, callback) 11 | if callback then 12 | insert(self.endings, {ending, callback}) 13 | else 14 | insert(self.endings, ending) 15 | end 16 | if ending:match('>$') then 17 | self.indents = self.indents + 1 18 | end 19 | end 20 | 21 | function methods:pop() 22 | local ending = remove(self.endings) 23 | if not ending then return end 24 | local callback 25 | if type(ending) == "table" then 26 | callback = ending[2] 27 | ending = ending[1] 28 | end 29 | if ending:match('>$') then 30 | self.indents = self.indents - 1 31 | end 32 | return ending, callback 33 | end 34 | 35 | function methods:last() 36 | return self.endings[#self.endings] 37 | end 38 | 39 | function methods:indent_level() 40 | return self.indents 41 | end 42 | 43 | function methods:size() 44 | return #self.endings 45 | end 46 | 47 | function new() 48 | local endstack = {endings = {}, callbacks = {}, indents = 0} 49 | return setmetatable(endstack, {__index = methods}) 50 | end -------------------------------------------------------------------------------- /haml/ext.lua: -------------------------------------------------------------------------------- 1 | local lpeg = require "lpeg" 2 | 3 | local assert = assert 4 | local concat = table.concat 5 | local default_haml_options = _G["default_haml_options"] 6 | local error = error 7 | local insert = table.insert 8 | local ipairs = ipairs 9 | local loadstring = loadstring 10 | local pairs = pairs 11 | local select = select 12 | local sort = table.sort 13 | local tostring = tostring 14 | local type = type 15 | 16 | module "haml.ext" 17 | 18 | function interpolate_code(str) 19 | return str:gsub('([\\]*)#{(.-)}', function(slashes, match) 20 | if #slashes == 1 then 21 | return '#{' .. match .. '}' 22 | else 23 | return slashes .. '" ..' .. match .. ' .. "' 24 | end 25 | end) 26 | end 27 | 28 | -- Remove this before releasing 29 | function log(level, v) 30 | -- io.stderr:write(string.format("%s: %s\n", level, v)) 31 | end 32 | 33 | function escape_html(str, escapes) 34 | local chars = {} 35 | for k, _ in pairs(escapes) do 36 | insert(chars, k) 37 | end 38 | pattern = ("([%s])"):format(concat(chars, "")) 39 | return (str:gsub(pattern, escapes)) 40 | end 41 | 42 | function change_indents(str, len, options) 43 | local output = str:gsub("^" .. options.space, options.space:rep(len)) 44 | output = output:gsub(options.newline .. options.space, options.newline .. options.space:rep(len)) 45 | return output 46 | end 47 | 48 | function psplit(s, sep) 49 | sep = lpeg.P(sep) 50 | local elem = lpeg.C((1 - sep)^0) 51 | local p = lpeg.Ct(elem * (sep * elem)^0) 52 | return lpeg.match(p, s) 53 | end 54 | 55 | function do_error(position, message, ...) 56 | error(("Haml error: " .. message):format(...) .. " (at position " .. position .. ")") 57 | end 58 | 59 | function render_table(t) 60 | local buffer = {} 61 | for k, v in pairs(t) do 62 | if type(v) == "table" then v = render_table(v) end 63 | insert(buffer, ("%s=%s"):format(tostring(k), tostring(v))) 64 | end 65 | return "{" .. concat(buffer, ' ') .. "}" 66 | end 67 | 68 | --- Like pairs() but iterates over sorted table keys. 69 | -- @param t The table to iterate over 70 | -- @param func An option sorting function 71 | -- @return The iterator function 72 | -- @return The table 73 | -- @return The index 74 | function sorted_pairs(t, func) 75 | local keys = {} 76 | for key in pairs(t) do 77 | insert(keys, key) 78 | end 79 | sort(keys, func) 80 | local iterator, _, index = ipairs(keys) 81 | return function() 82 | local _, key = iterator(keys, index) 83 | index = index + 1 84 | return key, t[key] 85 | end, tab, index 86 | end 87 | 88 | --- Merge two or more tables together. 89 | -- Duplicate keys are overridden left to right, so for example merge(t1, t2) 90 | -- will use key values from t2. 91 | -- @return A table containing all the values of all the tables. 92 | function merge_tables(...) 93 | local numargs = select('#', ...) 94 | local out = {} 95 | for i = 1, numargs do 96 | local t = select(i, ...) 97 | if type(t) == "table" then 98 | for k, v in pairs(t) do 99 | out[k] = v 100 | end 101 | end 102 | end 103 | return out 104 | end 105 | 106 | --- Merge two or more tables together. 107 | -- Duplicate keys cause the value to be added as a table containing all the 108 | -- values for the key in every table. 109 | -- @return A table containing all the values of all the tables. 110 | function join_tables(...) 111 | local numargs = select('#', ...) 112 | local out = {} 113 | for i = 1, numargs do 114 | local t = select(i, ...) 115 | if type(t) == "table" then 116 | for k, v in pairs(t) do 117 | if out[k] then 118 | if type(out[k]) == "table" then 119 | insert(out[k], v) 120 | else 121 | out[k] = {out[k], v} 122 | end 123 | else 124 | out[k] = v 125 | end 126 | end 127 | end 128 | end 129 | return out 130 | end 131 | 132 | --- Flattens a table of tables. 133 | function flatten(...) 134 | local out = {} 135 | for _, attr in ipairs(arg) do 136 | for k, v in pairs(attr) do 137 | out[k] = v 138 | end 139 | end 140 | return out 141 | end 142 | 143 | --- Strip leading and trailing space from a string. 144 | function strip(str) 145 | return (str:gsub("^[%s]*", ""):gsub("[%s]*$", "")) 146 | end 147 | 148 | function render_attributes(attributes, options) 149 | local options = options or {} 150 | local q = options.attribute_wrapper or "'" 151 | local buffer = {""} 152 | for k, v in sorted_pairs(attributes) do 153 | if type(v) == "table" then 154 | if k == "class" then 155 | sort(v) 156 | insert(buffer, ("%s=" .. q .. "%s" .. q):format(k, concat(v, ' '))) 157 | elseif k == "id" then 158 | insert(buffer, ("%s=" .. q .. "%s" .. q):format(k, concat(v, '_'))) 159 | end 160 | elseif type(v) == "function" then 161 | if not options.suppress_eval then 162 | insert(buffer, ("%s=" .. q .. "%s" .. q):format(k, tostring(v()))) 163 | end 164 | elseif type(v) == "boolean" then 165 | if options.format == "xhtml" then 166 | insert(buffer, ("%s=" .. q .. "%s" .. q):format(k, k)) 167 | else 168 | insert(buffer, k) 169 | end 170 | else 171 | insert(buffer, ("%s=" .. q .. "%s" .. q):format(k, tostring(v))) 172 | end 173 | end 174 | return concat(buffer, " ") 175 | end 176 | -------------------------------------------------------------------------------- /haml/filter.lua: -------------------------------------------------------------------------------- 1 | local ext = require "haml.ext" 2 | 3 | local change_indents = ext.change_indents 4 | local concat = table.concat 5 | local escape_html = ext.escape_html 6 | local insert = table.insert 7 | local require = require 8 | local strip = ext.strip 9 | local do_error = ext.do_error 10 | 11 | module "haml.filter" 12 | 13 | local function code_filter(state) 14 | state.buffer:code(state.curr_phrase.content) 15 | end 16 | 17 | local function preserve_filter(state) 18 | local output = change_indents( 19 | state.curr_phrase.content, 20 | state:indent_level() - 1, 21 | state.options):gsub( 22 | "\n", ' '):gsub( 23 | "\r", ' ' 24 | ) 25 | state.buffer:string(output, {interpolate = true}) 26 | state.buffer:newline() 27 | end 28 | 29 | local function escaped_filter(state) 30 | local output = strip(change_indents( 31 | escape_html( 32 | state.curr_phrase.content, 33 | state.options.html_escapes 34 | ), 35 | state:indent_level() - 1, 36 | state.options 37 | )) 38 | state.buffer:string(output, {long = true, interpolate = true}) 39 | state.buffer:newline() 40 | end 41 | 42 | local function javascript_filter(state) 43 | local content = state.curr_phrase.content 44 | local options = state.options 45 | local indent_level = state:indent_level() 46 | local buffer = {} 47 | insert(buffer, state:indents() .. "") 50 | if options.format == "xhtml" then 51 | insert(buffer, 2, state:indents(1) .. "//") 53 | end 54 | local output = concat(buffer, options.newline) 55 | state.buffer:string(output, {long = true, interpolate = true}) 56 | state.buffer:newline() 57 | end 58 | 59 | local function cdata_filter(state) 60 | local content = state.curr_phrase.content 61 | local options = state.options 62 | local buffer = {} 63 | insert(buffer, state:indents() .. "") 66 | local output = concat(buffer, options.newline) 67 | state.buffer:string(output, {long = true, interpolate = true}) 68 | state.buffer:newline() 69 | end 70 | 71 | local function markdown_filter(state) 72 | local markdown = require "markdown" 73 | local output = state.curr_phrase.content:gsub("^"..state:indents(1), "") 74 | output = markdown(output:gsub(state.options.newline .. state:indents(1), state.options.newline)) 75 | state.buffer:string(change_indents(strip(output), state:indent_level(), state.options), {long = true, interpolate = true}) 76 | state.buffer:newline() 77 | end 78 | 79 | local function plain_filter(state) 80 | local output = change_indents( 81 | state.curr_phrase.content:gsub("[%s]*$", ""), 82 | state:indent_level() - 1, 83 | state.options 84 | ) 85 | state.buffer:string(output, {long = true, interpolate = true}) 86 | state.buffer:newline() 87 | end 88 | 89 | local function css_filter(state) 90 | local content = state.curr_phrase.content 91 | local options = state.options 92 | local indent_level = state:indent_level() 93 | local buffer = {} 94 | insert(buffer, state:indents() .. "") 97 | if options.format == "xhtml" then 98 | insert(buffer, 2, state:indents(1) .. "/**/") 100 | end 101 | local output = concat(buffer, options.newline) 102 | state.buffer:string(output, {long = true, interpolate = true}) 103 | state.buffer:newline() 104 | end 105 | 106 | filters = { 107 | cdata = cdata_filter, 108 | css = css_filter, 109 | escaped = escaped_filter, 110 | javascript = javascript_filter, 111 | lua = code_filter, 112 | markdown = markdown_filter, 113 | plain = plain_filter, 114 | preserve = preserve_filter 115 | } 116 | 117 | function filter_for(state) 118 | state:close_tags() 119 | local func 120 | if filters[state.curr_phrase.filter] then 121 | func = filters[state.curr_phrase.filter] 122 | else 123 | do_error(state.curr_phrase.pos, "No such filter \"%s\"", state.curr_phrase.filter) 124 | end 125 | return func(state) 126 | end 127 | -------------------------------------------------------------------------------- /haml/header.lua: -------------------------------------------------------------------------------- 1 | local ext = require "haml.ext" 2 | local do_error = ext.do_error 3 | 4 | module "haml.header" 5 | 6 | --- The HTML4 doctypes; default is 4.01 Transitional. 7 | html_doctypes = { 8 | ["5"] = '', 9 | STRICT = '', 10 | FRAMESET = '', 11 | DEFAULT = '' 12 | } 13 | 14 | --- The XHTML doctypes; default is 1.0 Transitional. 15 | xhtml_doctypes = { 16 | ["5"] = html_doctypes["5"], 17 | STRICT = '', 18 | FRAMESET = '', 19 | MOBILE = '', 20 | BASIC = '', 21 | DEFAULT = '' 22 | } 23 | 24 | 25 | --- Returns an XML prolog for the precompiler state. 26 | local function prolog_for(state) 27 | if state.options.format:match("^html") then return nil end 28 | local charset = state.curr_phrase.charset or state.options.encoding 29 | state.buffer:string((""):format(charset), {newline = true}) 30 | end 31 | 32 | --- Returns an (X)HTML doctype for the precompiler state. 33 | local function doctype_for(state) 34 | 35 | if state.options.format == 'html5' then 36 | return state.buffer:string(html_doctypes["5"], {newline = true}) 37 | 38 | elseif state.curr_phrase.version == "1.1" then 39 | return state.buffer:string('', {newline = true}) 40 | 41 | elseif state.options.format == 'xhtml' then 42 | local doctype = xhtml_doctypes[state.curr_phrase.doctype] or xhtml_doctypes.DEFAULT 43 | return state.buffer:string(doctype, {newline = true}) 44 | 45 | elseif state.options.format == 'html4' then 46 | local doctype = html_doctypes[state.curr_phrase.doctype] or html_doctypes.DEFAULT 47 | return state.buffer:string(doctype, {newline = true}) 48 | 49 | else 50 | do_error(state.curr_phrase.pos, 'don\'t understand doctype "%s"', state.curr_phrase.doctype) 51 | end 52 | 53 | end 54 | 55 | --- Returns an XML prolog or an X(HTML) doctype for the precompiler state. 56 | function header_for(state) 57 | if state.next_phrase and (#(state.next_phrase.space) or 0) > 0 then 58 | do_error(state.curr_phrase.pos, "you can not nest within a doctype declaration or XML prolog") 59 | end 60 | 61 | if state.curr_phrase.prolog then 62 | return prolog_for(state) 63 | else 64 | return doctype_for(state) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /haml/lua_adapter.lua: -------------------------------------------------------------------------------- 1 | local ext = require "haml.ext" 2 | 3 | local concat = table.concat 4 | local insert = table.insert 5 | local join_tables = ext.join_tables 6 | local pairs = pairs 7 | local setmetatable = setmetatable 8 | local type = type 9 | 10 | module "haml.lua_adapter" 11 | 12 | local function key_val(k, v, interpolate) 13 | if type(k) == "number" then 14 | return ('%s, '):format(v) 15 | elseif type(k) == "string" and interpolate then 16 | return ('["%s"] = %s, '):format(k, ext.interpolate_code(v)) 17 | else 18 | return ('["%s"] = %s, '):format(k, v) 19 | end 20 | end 21 | 22 | local function serialize_table(t, opts) 23 | local buffer = {} 24 | local opts = opts or {} 25 | insert(buffer, "{") 26 | for k, v in pairs(t) do 27 | if type(v) == "table" then 28 | insert(buffer, key_val(k, serialize_table(v, opts), opts.interpolate)) 29 | else 30 | insert(buffer, key_val(k, v, opts.interpolate)) 31 | end 32 | end 33 | insert(buffer, "}") 34 | return concat(buffer, "") 35 | end 36 | 37 | local functions = {} 38 | 39 | function functions.close_tags(state) 40 | local code = state.curr_phrase.code 41 | state:close_tags(function(ending) 42 | if code:match("^%s*else") then 43 | return ending ~= "end -- if" 44 | else 45 | return true 46 | end 47 | end) 48 | end 49 | 50 | function functions.newline() 51 | return 'r:b("\\n")' 52 | end 53 | 54 | function functions.code(value) 55 | return value 56 | end 57 | 58 | function functions.string(value, opts) 59 | if opts.interpolate and value:match('#{.-}') then 60 | return ("r:b(\"%s\")"):format(ext.interpolate_code(value)) 61 | else 62 | return ("r:b(%s)"):format(("%q"):format(value)) 63 | end 64 | 65 | end 66 | 67 | --- Format tables into tag attributes. 68 | function functions.format_attributes(...) 69 | return 'r:b(r:attr(' .. serialize_table(join_tables(...), {interpolate = true}) .. '))' 70 | end 71 | 72 | function functions.ending_for(code) 73 | if code:match "^%s*if.*" then 74 | return "end -- if" 75 | elseif code:match "^%s*else[^%w]*" then 76 | -- return "end -- else" 77 | elseif code:match "do%s*$" then 78 | return "end -- do" 79 | end 80 | return nil 81 | end 82 | 83 | function get_adapter(options) 84 | local adapter = {} 85 | return setmetatable(adapter, {__index = functions}) 86 | end -------------------------------------------------------------------------------- /haml/parser.lua: -------------------------------------------------------------------------------- 1 | local ext = require "haml.ext" 2 | local lpeg = require "lpeg" 3 | 4 | local concat = table.concat 5 | local error = error 6 | local insert = table.insert 7 | local ipairs = ipairs 8 | local match = lpeg.match 9 | local next = next 10 | local pairs = pairs 11 | local rawset = rawset 12 | local remove = table.remove 13 | local tostring = tostring 14 | local upper = string.upper 15 | 16 | --- Haml parser 17 | module "haml.parser" 18 | 19 | -- import lpeg feature functions into current module 20 | for k, v in pairs(lpeg) do 21 | if #k <= 3 then 22 | _M[k] = v 23 | end 24 | end 25 | 26 | local alnum = R("az", "AZ", "09") 27 | local leading_whitespace = Cg(S" \t"^0, "space") 28 | local inline_whitespace = S" \t" 29 | local eol = P"\n" + "\r\n" + "\r" 30 | local empty_line = Cg(P"", "empty_line") 31 | local multiline_modifier = Cg(P"|", "multiline_modifier") 32 | local unparsed = Cg((1 - eol - multiline_modifier)^1, "unparsed") 33 | local default_tag = "div" 34 | local singlequoted_string = P("'" * ((1 - S "'\r\n\f\\") + (P'\\' * 1))^0 * "'") 35 | local doublequoted_string = P('"' * ((1 - S '"\r\n\f\\') + (P'\\' * 1))^0 * '"') 36 | local quoted_string = singlequoted_string + doublequoted_string 37 | 38 | local operator_symbols = { 39 | conditional_comment = P"/[", 40 | escape = P"\\", 41 | filter = P":", 42 | header = P"!!!", 43 | markup_comment = P"/", 44 | script = P"=", 45 | silent_comment = P"-#" + "--", 46 | silent_script = P"-", 47 | tag = P"%", 48 | escaped_script = P"&=", 49 | unescaped_script = P"!=", 50 | preserved_script = P"~", 51 | } 52 | 53 | -- This builds a table of capture patterns that return the operator name rather 54 | -- than the literal operator string. 55 | local operators = {} 56 | for k, v in pairs(operator_symbols) do 57 | operators[k] = Cg(v / function() return k end, "operator") 58 | end 59 | 60 | local script_operator = P( 61 | operators.silent_script + 62 | operators.script + 63 | operators.escaped_script + 64 | operators.unescaped_script + 65 | operators.preserved_script 66 | ) 67 | 68 | -- (X)HTML Doctype or XML prolog 69 | local prolog = Cg(P"XML" + P"xml" / upper, "prolog") 70 | local charset = Cg((R("az", "AZ", "09") + S"-")^1, "charset") 71 | local version = Cg(P"1.1" + "1.0", "version") 72 | local doctype = Cg((R("az", "AZ")^1 + "5") / upper, "doctype") 73 | local prolog_and_charset = (prolog * (inline_whitespace^1 * charset^1)^0) 74 | local doctype_or_version = doctype + version 75 | local header = operators.header * (inline_whitespace * (prolog_and_charset + doctype_or_version))^0 76 | 77 | -- Modifiers that follow Haml markup tags 78 | local modifiers = { 79 | self_closing = Cg(P"/", "self_closing_modifier"), 80 | inner_whitespace = Cg(P"<", "inner_whitespace_modifier"), 81 | outer_whitespace = Cg(P">", "outer_whitespace_modifier") 82 | } 83 | 84 | -- Markup attributes 85 | function parse_html_style_attributes(a) 86 | local name = C((alnum + S".-:_")^1 ) 87 | local value = C(quoted_string + name) 88 | local sep = (P" " + eol)^1 89 | local assign = P'=' 90 | local pair = Cg(name * assign * value) * sep^-1 91 | local list = S("(") * Cf(Ct("") * pair^0, rawset) * S(")") 92 | return match(list, a) or error(("Could not parse attributes '%s'"):format(a)) 93 | end 94 | 95 | function parse_ruby_style_attributes(a) 96 | local name = (alnum + P"_")^1 97 | local key = (P":" * C(name)) + (P":"^0 * C(quoted_string)) / function(a) local a = a:gsub('[\'"]', ""); return a end 98 | local value = C(quoted_string + name) 99 | local sep = inline_whitespace^0 * P"," * (P" " + eol)^0 100 | local assign = P'=>' 101 | local pair = Cg(key * inline_whitespace^0 * assign * inline_whitespace^0 * value) * sep^-1 102 | local list = S("{") * inline_whitespace^0 * Cf(Ct("") * pair^0, rawset) * inline_whitespace^0 * S("}") 103 | return match(list, a) or error(("Could not parse attributes '%s'"):format(a)) 104 | end 105 | 106 | local html_style_attributes = P{"(" * ((quoted_string + (P(1) - S"()")) + V(1))^0 * ")"} / parse_html_style_attributes 107 | local ruby_style_attributes = P{"{" * ((quoted_string + (P(1) - S"{}")) + V(1))^0 * "}"} / parse_ruby_style_attributes 108 | local any_attributes = html_style_attributes + ruby_style_attributes 109 | local attributes = Cg(Ct((any_attributes * any_attributes^0)) / ext.flatten, "attributes") 110 | 111 | -- Haml HTML elements 112 | -- Character sequences for CSS and XML/HTML elements. Note that many invalid 113 | -- names are allowed because of Haml's flexibility. 114 | local function flatten_ids_and_classes(t) 115 | classes = {} 116 | ids = {} 117 | for _, t in pairs(t) do 118 | if t.id then 119 | insert(ids, t.id) 120 | else 121 | insert(classes, t.class) 122 | end 123 | end 124 | local out = {} 125 | if next(ids) then out.id = ("'%s'"):format(remove(ids)) end 126 | if next(classes) then out.class = ("'%s'"):format(concat(classes, " ")) end 127 | return out 128 | end 129 | 130 | local nested_content = Cg((Cmt(Cb("space"), function(subject, index, spaces) 131 | local buffer = {} 132 | local num_spaces = tostring(spaces or ""):len() 133 | local start = subject:sub(index) 134 | for _, line in ipairs(ext.psplit(start, "\n")) do 135 | if match(P" "^(num_spaces + 1), line) then 136 | insert(buffer, line) 137 | elseif line == "" then 138 | insert(buffer, line) 139 | else 140 | break 141 | end 142 | end 143 | local match = concat(buffer, "\n") 144 | return index + match:len(), match 145 | end)), "content") 146 | 147 | local css_name = S"-_" + alnum^1 148 | local class = P"." * Ct(Cg(css_name^1, "class")) 149 | local id = P"#" * Ct(Cg(css_name^1, "id")) 150 | local css = P{(class + id) * V(1)^0} 151 | local html_name = R("az", "AZ", "09") + S":-_" 152 | local explicit_tag = "%" * Cg(html_name^1, "tag") 153 | local implict_tag = Cg(-S(1) * #css / function() return default_tag end, "tag") 154 | local haml_tag = (explicit_tag + implict_tag) * Cg(Ct(css) / flatten_ids_and_classes, "css")^0 155 | local inline_code = operators.script * inline_whitespace^0 * Cg(unparsed^0 * -multiline_modifier / function(a) return a:gsub("\\", "\\\\") end, "inline_code") 156 | local multiline_code = operators.script * inline_whitespace^0 * Cg(((1 - multiline_modifier)^1 * multiline_modifier)^0 / function(a) return a:gsub("%s*|%s*", " ") end, "inline_code") 157 | local inline_content = inline_whitespace^0 * Cg(unparsed, "inline_content") 158 | local tag_modifiers = (modifiers.self_closing + (modifiers.inner_whitespace + modifiers.outer_whitespace)) 159 | 160 | -- Core Haml grammar 161 | local haml_element = Cg(Cp(), "pos") * leading_whitespace * ( 162 | -- Haml markup 163 | (haml_tag * attributes^0 * tag_modifiers^0 * (inline_code + multiline_code + inline_content)^0) + 164 | -- Doctype or prolog 165 | (header) + 166 | -- Silent comment 167 | (operators.silent_comment) * inline_whitespace^0 * Cg(unparsed^0, "comment") * nested_content + 168 | -- Script 169 | (script_operator) * inline_whitespace^1 * Cg(unparsed^0, "code") + 170 | -- IE conditional comments 171 | (operators.conditional_comment * Cg((P(1) - "]")^1, "condition")) * "]" + 172 | -- Markup comment 173 | (operators.markup_comment * inline_whitespace^0 * unparsed^0) + 174 | -- Filtered block 175 | (operators.filter * Cg((P(1) - eol)^0, "filter") * eol * nested_content) + 176 | -- Escaped 177 | (operators.escape * unparsed^0) + 178 | -- Unparsed content 179 | unparsed + 180 | -- Last resort 181 | empty_line 182 | ) 183 | local grammar = Ct(Ct(haml_element) * (eol^1 * Ct(haml_element))^0) 184 | 185 | function tokenize(input) 186 | return match(grammar, input) 187 | end -------------------------------------------------------------------------------- /haml/precompiler.lua: -------------------------------------------------------------------------------- 1 | local code = require "haml.code" 2 | local comment = require "haml.comment" 3 | local end_stack = require "haml.end_stack" 4 | local ext = require "haml.ext" 5 | local filter = require "haml.filter" 6 | local header = require "haml.header" 7 | local string_buffer = require "haml.string_buffer" 8 | local tag = require "haml.tag" 9 | 10 | local ipairs = ipairs 11 | local require = require 12 | local setmetatable = setmetatable 13 | local type = type 14 | 15 | --- Haml precompiler 16 | module "haml.precompiler" 17 | 18 | local function handle_current_phrase(compiler) 19 | local cp = compiler.curr_phrase 20 | local operator = cp.operator 21 | if operator == "header" then 22 | header.header_for(compiler) 23 | elseif operator == "filter" then 24 | filter.filter_for(compiler) 25 | elseif operator == "silent_comment" then 26 | compiler:close_tags() 27 | elseif operator == "markup_comment" then 28 | comment.comment_for(compiler) 29 | elseif operator == "conditional_comment" then 30 | comment.comment_for(compiler) 31 | elseif cp.tag then 32 | tag.tag_for(compiler) 33 | elseif cp.code then 34 | code.code_for(compiler) 35 | elseif cp.unparsed then 36 | compiler:close_tags() 37 | compiler.buffer:string(compiler:indents() .. cp.unparsed) 38 | compiler.buffer:newline(true) 39 | end 40 | end 41 | 42 | local methods = {} 43 | 44 | --- Precompile Haml into Lua code. 45 | -- @param phrases A table of parsed phrases produced by the parser. 46 | function methods:precompile(phrases) 47 | 48 | self.buffer = string_buffer.new(self.adapter) 49 | self.endings = end_stack.new() 50 | self.curr_phrase = {} 51 | self.next_phrase = {} 52 | self.prev_phrase = {} 53 | self.space_sequence = nil 54 | 55 | if self.options.file then 56 | self.buffer:code(("r:f(%q)"):format(self.options.file)) 57 | end 58 | 59 | for index, phrase in ipairs(phrases) do 60 | self.next_phrase = phrases[index + 1] 61 | self.prev_phrase = phrases[index - 1] 62 | self.curr_phrase = phrase 63 | self:__detect_whitespace_format() 64 | self:__validate_whitespace() 65 | self.buffer:code(("r:at(%d)"):format(phrase.pos)) 66 | handle_current_phrase(self) 67 | end 68 | 69 | self:__close_open_tags() 70 | 71 | return self.buffer:cat() 72 | end 73 | 74 | function methods:indent_level() 75 | if not self.space_sequence then 76 | return 0 77 | else 78 | return self.curr_phrase.space:len() / self.space_sequence:len() 79 | end 80 | end 81 | 82 | function methods:indent_diff() 83 | if not self.space_sequence then return 0 end 84 | return self:indent_level() - self.prev_phrase.space:len() / self.space_sequence:len() 85 | end 86 | 87 | function methods:indents(n) 88 | local l = self.endings:indent_level() 89 | return self.options.indent:rep(n and n + l or l) 90 | end 91 | 92 | -- You can pass in a function to check the last end tag and return 93 | -- after a certain level. For example, you can use it to close all 94 | -- open HTML tags and then bail when we reach an "end". This is useful 95 | -- for closing tags around "else" and "elseif". 96 | function methods:close_tags(func) 97 | if self:indent_diff() < 0 then 98 | local i = self:indent_diff() 99 | repeat 100 | if func and not func(self.endings:last()) then return end 101 | self:close_current() 102 | i = i + 1 103 | until i >= 0 104 | end 105 | end 106 | 107 | function methods:close_current() 108 | -- reset some per-tag state settings for each chunk. 109 | if self.buffer.suppress_whitespace then 110 | self.buffer:chomp() 111 | self.buffer.suppress_whitespace = false 112 | end 113 | 114 | local ending, callback = self.endings:pop() 115 | if not ending then return end 116 | 117 | if ending:match('>$') then 118 | self.buffer:string(self:indents() .. ending, {newline = true}) 119 | else 120 | 121 | if (not self.next_phrase) or self.next_phrase.code then 122 | ending = "end" 123 | end 124 | 125 | self.buffer:code(ending) 126 | end 127 | if callback then callback(self) end 128 | end 129 | 130 | function methods:__close_open_tags() 131 | while self.endings:size() > 0 do 132 | self:close_current() 133 | end 134 | end 135 | 136 | function methods:__detect_whitespace_format() 137 | if self.space_sequence then return end 138 | if #(self.curr_phrase.space or '') > 0 and not self.space_sequence then 139 | self.space_sequence = self.curr_phrase.space 140 | end 141 | end 142 | 143 | function methods:__validate_whitespace() 144 | if not self.space_sequence then return end 145 | if self.curr_phrase.space == "" then return end 146 | local prev_space = '' 147 | if self.prev_phrase then prev_space = self.prev_phrase.space end 148 | if self.curr_phrase.space:len() <= prev_space:len() then return end 149 | if self.curr_phrase.space == (prev_space .. self.space_sequence) then return end 150 | ext.do_error(self.curr_phrase.pos, "bad indentation") 151 | end 152 | 153 | --- Create a new Haml precompiler 154 | -- @param options Precompiler options. 155 | function new(options) 156 | local precompiler = { 157 | options = options, 158 | adapter = require(("haml.%s_adapter"):format(options.adapter)).get_adapter(options) 159 | } 160 | return setmetatable(precompiler, {__index = methods}) 161 | end 162 | -------------------------------------------------------------------------------- /haml/renderer.lua: -------------------------------------------------------------------------------- 1 | local ext = require "haml.ext" 2 | 3 | local _G = _G 4 | local assert = assert 5 | local concat = table.concat 6 | local error = error 7 | local getfenv = getfenv 8 | local insert = table.insert 9 | local loadstring = loadstring 10 | local open = io.open 11 | local pairs = pairs 12 | local pcall = pcall 13 | local require = require 14 | local setfenv = setfenv 15 | local setmetatable = setmetatable 16 | local sorted_pairs = ext.sorted_pairs 17 | local tostring = tostring 18 | local type = type 19 | local rawset = rawset 20 | 21 | module "haml.renderer" 22 | 23 | local methods = {} 24 | 25 | function methods:escape_html(...) 26 | return ext.escape_html(..., self.options.html_escapes) 27 | end 28 | 29 | local function escape_newlines(a, b, c) 30 | return a .. b:gsub("\n", " ") .. c 31 | end 32 | 33 | function methods:preserve_html(string) 34 | local string = string 35 | for tag, _ in pairs(self.options.preserve) do 36 | string = string:gsub(("(<%s>)(.*)(%s>)"):format(tag, tag), escape_newlines) 37 | end 38 | return string 39 | end 40 | 41 | function methods:attr(attr) 42 | return ext.render_attributes(attr, self.options) 43 | end 44 | 45 | function methods:at(pos) 46 | self.current_pos = pos 47 | end 48 | 49 | function methods:f(file) 50 | self.current_file = file 51 | end 52 | 53 | function methods:b(string) 54 | insert(self.buffer, string) 55 | end 56 | 57 | function methods:make_partial_func() 58 | local renderer = self 59 | local haml = require "haml" 60 | return function(file, locals) 61 | local engine = haml.new(self.options) 62 | local rendered = engine:render_file(("%s.haml"):format(file), locals or renderer.env.locals) 63 | -- if we're in a partial, by definition the last entry added to the buffer 64 | -- will be the current spaces 65 | return rendered:gsub("\n", "\n" .. self.buffer[#self.buffer]) 66 | end 67 | end 68 | 69 | function methods:make_yield_func() 70 | return function(content) 71 | return ext.strip(content:gsub("\n", "\n" .. self.buffer[#self.buffer])) 72 | end 73 | end 74 | 75 | function methods:render(locals) 76 | local locals = locals or {} 77 | self.buffer = {} 78 | self.current_pos = 0 79 | self.current_file = nil 80 | self.env.locals = locals or {} 81 | 82 | setmetatable(self.env, {__index = function(table, key) 83 | return locals[key] or _G[key] 84 | end, 85 | __newindex = function(table, key, val) rawset(locals, key, val) end 86 | }) 87 | 88 | local succeeded, err = pcall(self.func) 89 | if not succeeded then 90 | 91 | local line_number 92 | 93 | if self.current_file then 94 | local file = assert(open(self.current_file, "r")) 95 | local str = file:read(self.current_pos) 96 | line_number = #str - #str:gsub("\n", "") + 1 97 | end 98 | 99 | error( 100 | ("\nError in %s at line %s (offset %d):"):format( 101 | self.current_file or "a
", engine:render("- if true then\n %p a")) 11 | end) 12 | 13 | it("should handle if/else", function() 14 | assert_equal("a
", engine:render("- if true then\n %p a\n- else\n %p b")) 15 | end) 16 | 17 | it("should handle if/elseif", function() 18 | assert_equal("a
", engine:render("- if true then\n %p a\n- elseif false then\n %p b")) 19 | end) 20 | 21 | it("should handle if/elseif/else", function() 22 | assert_equal("a
", engine:render("- if true then\n %p a\n- elseif false then\n %p b\n- else\n %p c")) 23 | end) 24 | end) 25 | 26 | describe("the endstack", function() 27 | 28 | local es 29 | before(function() 30 | es = haml.end_stack.new() 31 | end) 32 | 33 | it("should have an initial indent level of 0", function() 34 | assert_equal(es:indent_level(), 0) 35 | end) 36 | 37 | it("should add an HTML tag and increase the indent level", function() 38 | es:push("") 39 | assert_equal(es:indent_level(), 1) 40 | end) 41 | 42 | it("should not increase the indent level for code endings", function() 43 | es:push("end") 44 | assert_equal(es:indent_level(), 0) 45 | end) 46 | 47 | it("should pop an HTML tag an decrease the indent level", function() 48 | es:push("