├── .dialyzer-ignore-warnings ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── UNLICENSE ├── ebin └── .gitignore ├── rebar ├── src ├── walrus.app.src ├── walrus.erl ├── walrus_lexer.xrl ├── walrus_mochinum.erl └── walrus_parser.yrl └── test ├── compile_test.erl ├── lexer_test.erl ├── parser_test.erl └── render_test.erl /.dialyzer-ignore-warnings: -------------------------------------------------------------------------------- 1 | leexinc.hrl:52: The pattern can never match the type <_,_,'error' | 'skip_token',[{'{{' | '{{#' | '{{/' | '{{^' | '{{{' | '}}' | '}}}',_} | {'key',_,atom()} | {'text',_,binary()}]> 2 | leexinc.hrl:54: The pattern can never match the type <_,_,'error' | 'skip_token',[{'{{' | '{{#' | '{{/' | '{{^' | '{{{' | '}}' | '}}}',_} | {'key',_,atom()} | {'text',_,binary()}]> 3 | leexinc.hrl:59: The pattern can never match the type <_,_,'error',[{'{{' | '{{#' | '{{/' | '{{^' | '{{{' | '}}' | '}}}',_} | {'key',_,atom()} | {'text',_,binary()}]> 4 | leexinc.hrl:62: The pattern <_Rest, Line, {'error', S}, _Ts> can never match the type <_,_,'error',[{'{{' | '{{#' | '{{/' | '{{^' | '{{{' | '}}' | '}}}',_} | {'key',_,atom()} | {'text',_,binary()}]> 5 | leexinc.hrl:121: The pattern can never match the type <_,_,'error' | 'skip_token'> 6 | leexinc.hrl:123: The pattern can never match the type <_,_,'error' | 'skip_token'> 7 | leexinc.hrl:128: The pattern can never match the type <_,_,'error'> 8 | leexinc.hrl:131: The pattern can never match the type <_,_,'error'> 9 | leexinc.hrl:195: The pattern can never match the type <_,_,'error' | 'skip_token',_> 10 | leexinc.hrl:197: The pattern can never match the type <_,_,'error' | 'skip_token',_> 11 | leexinc.hrl:202: The pattern can never match the type <_,_,'error',_> 12 | leexinc.hrl:205: The pattern can never match the type <_,_,'error',_> 13 | leexinc.hrl:246: The pattern can never match the type <_,_,'error' | 'skip_token',_> 14 | leexinc.hrl:248: The pattern can never match the type <_,_,'error' | 'skip_token',_> 15 | leexinc.hrl:253: The pattern can never match the type <_,_,'error',_> 16 | leexinc.hrl:256: The pattern can never match the type <_,_,'error',_> 17 | leexinc.hrl:260: Function yyrev/2 will never be called -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | .dialyzer_plt 3 | src/walrus_lexer.erl 4 | src/walrus_parser.erl 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is the MIT license. 2 | 3 | Copyright (c) 2007 Mochi Media, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REBAR = ./rebar 2 | DIALYZER = dialyzer 3 | 4 | DIALYZER_WARNINGS = -Wunmatched_returns -Werror_handling \ 5 | -Wrace_conditions -Wbehaviours -Wunderspecs 6 | 7 | .PHONY: all compile test clean 8 | 9 | all: compile 10 | 11 | compile: 12 | @$(REBAR) compile 13 | 14 | test: compile 15 | @$(REBAR) eunit 16 | 17 | clean: 18 | @$(REBAR) clean 19 | 20 | build-plt: 21 | @$(DIALYZER) --build_plt --output_plt .dialyzer_plt \ 22 | --apps kernel stdlib 23 | 24 | dialyze: compile 25 | @$(DIALYZER) --src src --plt .dialyzer_plt $(DIALYZER_WARNINGS) | \ 26 | fgrep -vf .dialyzer-ignore-warnings 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Walrus - Mustache-like Templating 2 | ================================= 3 | 4 | Walrus is 50% Mustache and 50% large flippered marine mammal. 5 | 6 | Lexing is done with Leex and parsing is done with Yecc, both 7 | of which are included in newer versions of Erlang. 8 | 9 | Most Mustache constructs work, such as variables, unescaped 10 | variables, blocks, and inverse blocks. However, The Walrus is 11 | an opinionated animal, and partials will never be supported. 12 | Also, functions passed into the context are simply evaluated 13 | and are not passed the raw template. 14 | 15 | You can skip lexing and parsing templates every time you want 16 | to render them by using `compile/1`, which returns a renderer 17 | you can then pass a context that renders the template. 18 | 19 | Examples 20 | -------- 21 | 22 | ### Simple Render 23 | 24 | 1> Tmpl = "Hello {{{name}}}. 25 | 1> 26 | 1> Drinks: 27 | 1> 28 | 1> {{#drinks}} 29 | 1> - {{name}}, {{tastiness}} 30 | 1> {{/drinks}}". 31 | "Hello {{{name}}}.\n\nDrinks:\n\n{{#drinks}}\n - {{name}}, {{tastiness}}\n{{/drinks}}" 32 | 2> 33 | 2> Ctx = [{name, "Devin & Jane"}, 34 | 2> {drinks, [[{name, "Beer"}, 35 | 2> {tastiness, 5}], 36 | 2> [{name, "Juice"}, 37 | 2> {tastiness, 8}]]}]. 38 | [{name,"Devin & Jane"}, 39 | {drinks,[[{name,"Beer"},{tastiness,5}], 40 | [{name,"Juice"},{tastiness,8}]]}] 41 | 3> 42 | 3> walrus:render(Tmpl, Ctx). 43 | <<"Hello Devin & Jane.\n\nDrinks:\n\n\n - Beer, 5\n\n - Juice, 8\n">> 44 | 45 | ### Compiled Renderer 46 | 47 | 1> Renderer = walrus:compile("Hello {{{name}}}.\n\nDrinks:\n\n{{#drinks}}\n - {{name}}, {{tastiness}}\n{{/drinks}}"). 48 | #Fun 49 | 2> Ctx = [{name, "Devin & Jane"}, 50 | 2> {drinks, [[{name, "Beer"}, 51 | 2> {tastiness, 5}], 52 | 2> [{name, "Juice"}, 53 | 2> {tastiness, 8}]]}]. 54 | [{name,"Devin & Jane"}, 55 | {drinks,[[{name,"Beer"},{tastiness,5}], 56 | [{name,"Juice"},{tastiness,8}]]}] 57 | 3> 58 | 3> Renderer(Ctx). 59 | <<"Hello Devin & Jane.\n\nDrinks:\n\n\n - Beer, 5\n\n - Juice, 8\n">> 60 | 61 | Acknowledgments 62 | --------------- 63 | Robert Virding helped me tremendously when I struggled to 64 | understand Leex. He's also responsible for 65 | `([^{}]|({[^{])|(}[^}]))+`. 66 | 67 | License 68 | ------- 69 | 70 | All code released into the public domain (see `UNLICENSE`) 71 | except for the file `walrus_mochinum.erl`, which has it's own 72 | license (see `LICENSE`). 73 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /ebin/.gitignore: -------------------------------------------------------------------------------- 1 | *.app 2 | *.beam 3 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devinus/walrus/27007aca5b1fb04496af6ac631e8e9804fadbd4b/rebar -------------------------------------------------------------------------------- /src/walrus.app.src: -------------------------------------------------------------------------------- 1 | {application, walrus, [ 2 | {description, "Erlang Mustache compiler"}, 3 | {vsn, "0.1.0"}, 4 | {applications, [kernel, stdlib]}, 5 | {registered, []}, 6 | {env, []} 7 | ]}. 8 | -------------------------------------------------------------------------------- /src/walrus.erl: -------------------------------------------------------------------------------- 1 | -module(walrus). 2 | -author("Devin Torres "). 3 | 4 | -export([compile/1, render/2]). 5 | 6 | -define(is_falsy(V), 7 | (V =:= false orelse V =:= [] orelse V =:= undefined orelse V =:= null)). 8 | 9 | -type value() :: list() | binary() | integer() | float() | atom(). 10 | -type context() :: [{Key :: string(), Value :: value()}, ...]. 11 | -type stringifiable() :: value() | fun((Context :: context()) 12 | -> value()). 13 | 14 | -spec compile(Template :: list() | binary()) -> fun((Context :: context()) 15 | -> binary()). 16 | compile(Template) when is_binary(Template) -> 17 | compile(binary_to_list(Template)); 18 | compile(Template) when is_list(Template) -> 19 | {ok, Tokens, _} = walrus_lexer:string(Template), 20 | {ok, ParseTree} = walrus_parser:parse(Tokens), 21 | fun (Context) -> 22 | render(ParseTree, Context, []) 23 | end. 24 | 25 | -spec render(Template :: list() | binary(), Context :: context()) 26 | -> binary(). 27 | render(Template, Context) when is_binary(Template) -> 28 | render(binary_to_list(Template), Context); 29 | render(Template, Context) when is_list(Template) -> 30 | {ok, Tokens, _} = walrus_lexer:string(Template), 31 | {ok, ParseTree} = walrus_parser:parse(Tokens), 32 | render(ParseTree, Context, []). 33 | 34 | -spec render(ParseTree :: list(), Context :: context(), Acc :: list()) 35 | -> binary(). 36 | render([{text, Text} | ParseTree], Context, Acc) -> 37 | render(ParseTree, Context, [Text | Acc]); 38 | render([{var, Key} | ParseTree], Context, Acc) -> 39 | Value = get(Key, Context), 40 | render(ParseTree, Context, [stringify(Value, Context, true) | Acc]); 41 | render([{var_unescaped, Key} | ParseTree], Context, Acc) -> 42 | Value = get(Key, Context), 43 | render(ParseTree, Context, [stringify(Value, Context, false) | Acc]); 44 | render([{block, Key, SubParseTree} | ParseTree], Context, Acc) -> 45 | Value = get(Key, Context), 46 | case Value of 47 | Val when ?is_falsy(Val) -> 48 | render(ParseTree, Context, Acc); 49 | Val when is_list(Val) -> 50 | Tmpl = [render(SubParseTree, Ctx, []) || Ctx <- Val], 51 | render(ParseTree, Context, [Tmpl | Acc]); 52 | _ -> 53 | Tmpl = render(SubParseTree, Context, []), 54 | render(ParseTree, Context, [Tmpl | Acc]) 55 | end; 56 | render([{inverse, Key, SubParseTree} | ParseTree], Context, Acc) -> 57 | Value = get(Key, Context), 58 | case Value of 59 | Val when ?is_falsy(Val) -> 60 | Tmpl = render(SubParseTree, Context, []), 61 | render(ParseTree, Context, [Tmpl | Acc]); 62 | _ -> 63 | render(ParseTree, Context, Acc) 64 | end; 65 | render([], _Context, Acc) -> 66 | iolist_to_binary(lists:reverse(Acc)). 67 | 68 | -spec get(Key :: atom(), Context :: context()) -> stringifiable(). 69 | get(Key, Context) -> 70 | Value = proplists:get_value(Key, Context), 71 | if 72 | is_function(Value) -> Value(Context); 73 | true -> Value 74 | end. 75 | 76 | -spec stringify(Value :: stringifiable(), Context :: context(), Escape :: boolean()) 77 | -> iolist(). 78 | stringify(Value, _Context, false) when is_list(Value) -> 79 | Value; 80 | stringify(Value, _Context, true) when is_list(Value) -> 81 | escape(Value); 82 | stringify(Value, _Context, true) when is_binary(Value) -> 83 | escape(binary_to_list(Value)); 84 | stringify(Value, _Context, _Escape) when is_integer(Value) -> 85 | integer_to_list(Value); 86 | stringify(Value, _Context, _Escape) when is_float(Value) -> 87 | walrus_mochinum:digits(Value); 88 | stringify(Value, _Context, false) when is_atom(Value) -> 89 | atom_to_list(Value); 90 | stringify(Value, _Context, true) when is_atom(Value) -> 91 | escape(atom_to_list(Value)); 92 | stringify(Value, Context, Escape) when is_function(Value) -> 93 | stringify(Value(Context), Context, Escape). 94 | 95 | escape(Value) -> 96 | escape(Value, []). 97 | 98 | -spec escape(Value :: list(), Acc :: list()) -> iolist(). 99 | escape([$< | Tail], Acc) -> 100 | escape(Tail, [<<"<">> | Acc]); 101 | escape([$> | Tail], Acc) -> 102 | escape(Tail, [<<">">> | Acc]); 103 | escape([$& | Tail], Acc) -> 104 | escape(Tail, [<<"&">> | Acc]); 105 | escape([C | Tail], Acc) -> 106 | escape(Tail, [C | Acc]); 107 | escape([], Acc) -> 108 | lists:reverse(Acc). 109 | -------------------------------------------------------------------------------- /src/walrus_lexer.xrl: -------------------------------------------------------------------------------- 1 | Definitions. 2 | 3 | Key = [a-zA-Z0-9_]+ 4 | 5 | Rules. 6 | 7 | {{!.*}} : skip_token. 8 | ([^{}]|({[^{])|(}[^}]))+ : {token,{text,TokenLine,?ltb(TokenChars)}}. 9 | {{ : {token,{'{{',TokenLine}}. 10 | {{# : {token,{'{{#',TokenLine}}. 11 | {{/ : {token,{'{{/',TokenLine}}. 12 | {{\^ : {token,{'{{^',TokenLine}}. 13 | {{{ : {token,{'{{{',TokenLine}}. 14 | {{& : {token,{'{{&',TokenLine}}. 15 | \s*{Key}\s*}} : {token,{key,TokenLine,?key(TokenChars,TokenLen)},"}}"}. 16 | }} : {token,{'}}',TokenLine}}. 17 | }}} : {token,{'}}}',TokenLine}}. 18 | 19 | Erlang code. 20 | 21 | -define(ltb(List), list_to_binary(List)). 22 | 23 | -define(key(Chars, Len), 24 | list_to_atom(string:strip(string:left(Chars, Len-2)))). 25 | -------------------------------------------------------------------------------- /src/walrus_mochinum.erl: -------------------------------------------------------------------------------- 1 | %% @copyright 2007 Mochi Media, Inc. 2 | %% @author Bob Ippolito 3 | 4 | %% @doc Useful numeric algorithms for floats that cover some deficiencies 5 | %% in the math module. More interesting is digits/1, which implements 6 | %% the algorithm from: 7 | %% http://www.cs.indiana.edu/~burger/fp/index.html 8 | %% See also "Printing Floating-Point Numbers Quickly and Accurately" 9 | %% in Proceedings of the SIGPLAN '96 Conference on Programming Language 10 | %% Design and Implementation. 11 | 12 | -module(walrus_mochinum). 13 | -author("Bob Ippolito "). 14 | -export([digits/1, frexp/1, int_pow/2, int_ceil/1]). 15 | 16 | %% IEEE 754 Float exponent bias 17 | -define(FLOAT_BIAS, 1022). 18 | -define(MIN_EXP, -1074). 19 | -define(BIG_POW, 4503599627370496). 20 | 21 | %% External API 22 | 23 | %% @spec digits(number()) -> string() 24 | %% @doc Returns a string that accurately represents the given integer or float 25 | %% using a conservative amount of digits. Great for generating 26 | %% human-readable output, or compact ASCII serializations for floats. 27 | digits(N) when is_integer(N) -> 28 | integer_to_list(N); 29 | digits(0.0) -> 30 | "0.0"; 31 | digits(Float) -> 32 | {Frac1, Exp1} = frexp_int(Float), 33 | [Place0 | Digits0] = digits1(Float, Exp1, Frac1), 34 | {Place, Digits} = transform_digits(Place0, Digits0), 35 | R = insert_decimal(Place, Digits), 36 | case Float < 0 of 37 | true -> 38 | [$- | R]; 39 | _ -> 40 | R 41 | end. 42 | 43 | %% @spec frexp(F::float()) -> {Frac::float(), Exp::float()} 44 | %% @doc Return the fractional and exponent part of an IEEE 754 double, 45 | %% equivalent to the libc function of the same name. 46 | %% F = Frac * pow(2, Exp). 47 | frexp(F) -> 48 | frexp1(unpack(F)). 49 | 50 | %% @spec int_pow(X::integer(), N::integer()) -> Y::integer() 51 | %% @doc Moderately efficient way to exponentiate integers. 52 | %% int_pow(10, 2) = 100. 53 | int_pow(_X, 0) -> 54 | 1; 55 | int_pow(X, N) when N > 0 -> 56 | int_pow(X, N, 1). 57 | 58 | %% @spec int_ceil(F::float()) -> integer() 59 | %% @doc Return the ceiling of F as an integer. The ceiling is defined as 60 | %% F when F == trunc(F); 61 | %% trunc(F) when F < 0; 62 | %% trunc(F) + 1 when F > 0. 63 | int_ceil(X) -> 64 | T = trunc(X), 65 | case (X - T) of 66 | Pos when Pos > 0 -> T + 1; 67 | _ -> T 68 | end. 69 | 70 | 71 | %% Internal API 72 | 73 | int_pow(X, N, R) when N < 2 -> 74 | R * X; 75 | int_pow(X, N, R) -> 76 | int_pow(X * X, N bsr 1, case N band 1 of 1 -> R * X; 0 -> R end). 77 | 78 | insert_decimal(0, S) -> 79 | "0." ++ S; 80 | insert_decimal(Place, S) when Place > 0 -> 81 | L = length(S), 82 | case Place - L of 83 | 0 -> 84 | S ++ ".0"; 85 | N when N < 0 -> 86 | {S0, S1} = lists:split(L + N, S), 87 | S0 ++ "." ++ S1; 88 | N when N < 6 -> 89 | %% More places than digits 90 | S ++ lists:duplicate(N, $0) ++ ".0"; 91 | _ -> 92 | insert_decimal_exp(Place, S) 93 | end; 94 | insert_decimal(Place, S) when Place > -6 -> 95 | "0." ++ lists:duplicate(abs(Place), $0) ++ S; 96 | insert_decimal(Place, S) -> 97 | insert_decimal_exp(Place, S). 98 | 99 | insert_decimal_exp(Place, S) -> 100 | [C | S0] = S, 101 | S1 = case S0 of 102 | [] -> 103 | "0"; 104 | _ -> 105 | S0 106 | end, 107 | Exp = case Place < 0 of 108 | true -> 109 | "e-"; 110 | false -> 111 | "e+" 112 | end, 113 | [C] ++ "." ++ S1 ++ Exp ++ integer_to_list(abs(Place - 1)). 114 | 115 | 116 | digits1(Float, Exp, Frac) -> 117 | Round = ((Frac band 1) =:= 0), 118 | case Exp >= 0 of 119 | true -> 120 | BExp = 1 bsl Exp, 121 | case (Frac =/= ?BIG_POW) of 122 | true -> 123 | scale((Frac * BExp * 2), 2, BExp, BExp, 124 | Round, Round, Float); 125 | false -> 126 | scale((Frac * BExp * 4), 4, (BExp * 2), BExp, 127 | Round, Round, Float) 128 | end; 129 | false -> 130 | case (Exp =:= ?MIN_EXP) orelse (Frac =/= ?BIG_POW) of 131 | true -> 132 | scale((Frac * 2), 1 bsl (1 - Exp), 1, 1, 133 | Round, Round, Float); 134 | false -> 135 | scale((Frac * 4), 1 bsl (2 - Exp), 2, 1, 136 | Round, Round, Float) 137 | end 138 | end. 139 | 140 | scale(R, S, MPlus, MMinus, LowOk, HighOk, Float) -> 141 | Est = int_ceil(math:log10(abs(Float)) - 1.0e-10), 142 | %% Note that the scheme implementation uses a 326 element look-up table 143 | %% for int_pow(10, N) where we do not. 144 | case Est >= 0 of 145 | true -> 146 | fixup(R, S * int_pow(10, Est), MPlus, MMinus, Est, 147 | LowOk, HighOk); 148 | false -> 149 | Scale = int_pow(10, -Est), 150 | fixup(R * Scale, S, MPlus * Scale, MMinus * Scale, Est, 151 | LowOk, HighOk) 152 | end. 153 | 154 | fixup(R, S, MPlus, MMinus, K, LowOk, HighOk) -> 155 | TooLow = case HighOk of 156 | true -> 157 | (R + MPlus) >= S; 158 | false -> 159 | (R + MPlus) > S 160 | end, 161 | case TooLow of 162 | true -> 163 | [(K + 1) | generate(R, S, MPlus, MMinus, LowOk, HighOk)]; 164 | false -> 165 | [K | generate(R * 10, S, MPlus * 10, MMinus * 10, LowOk, HighOk)] 166 | end. 167 | 168 | generate(R0, S, MPlus, MMinus, LowOk, HighOk) -> 169 | D = R0 div S, 170 | R = R0 rem S, 171 | TC1 = case LowOk of 172 | true -> 173 | R =< MMinus; 174 | false -> 175 | R < MMinus 176 | end, 177 | TC2 = case HighOk of 178 | true -> 179 | (R + MPlus) >= S; 180 | false -> 181 | (R + MPlus) > S 182 | end, 183 | case TC1 of 184 | false -> 185 | case TC2 of 186 | false -> 187 | [D | generate(R * 10, S, MPlus * 10, MMinus * 10, 188 | LowOk, HighOk)]; 189 | true -> 190 | [D + 1] 191 | end; 192 | true -> 193 | case TC2 of 194 | false -> 195 | [D]; 196 | true -> 197 | case R * 2 < S of 198 | true -> 199 | [D]; 200 | false -> 201 | [D + 1] 202 | end 203 | end 204 | end. 205 | 206 | unpack(Float) -> 207 | <> = <>, 208 | {Sign, Exp, Frac}. 209 | 210 | frexp1({_Sign, 0, 0}) -> 211 | {0.0, 0}; 212 | frexp1({Sign, 0, Frac}) -> 213 | Exp = log2floor(Frac), 214 | <> = <>, 215 | {Frac1, -(?FLOAT_BIAS) - 52 + Exp}; 216 | frexp1({Sign, Exp, Frac}) -> 217 | <> = <>, 218 | {Frac1, Exp - ?FLOAT_BIAS}. 219 | 220 | log2floor(Int) -> 221 | log2floor(Int, 0). 222 | 223 | log2floor(0, N) -> 224 | N; 225 | log2floor(Int, N) -> 226 | log2floor(Int bsr 1, 1 + N). 227 | 228 | 229 | transform_digits(Place, [0 | Rest]) -> 230 | transform_digits(Place, Rest); 231 | transform_digits(Place, Digits) -> 232 | {Place, [$0 + D || D <- Digits]}. 233 | 234 | 235 | frexp_int(F) -> 236 | case unpack(F) of 237 | {_Sign, 0, Frac} -> 238 | {Frac, ?MIN_EXP}; 239 | {_Sign, Exp, Frac} -> 240 | {Frac + (1 bsl 52), Exp - 53 - ?FLOAT_BIAS} 241 | end. 242 | 243 | %% 244 | %% Tests 245 | %% 246 | -ifdef(TEST). 247 | -include_lib("eunit/include/eunit.hrl"). 248 | 249 | int_ceil_test() -> 250 | ?assertEqual(1, int_ceil(0.0001)), 251 | ?assertEqual(0, int_ceil(0.0)), 252 | ?assertEqual(1, int_ceil(0.99)), 253 | ?assertEqual(1, int_ceil(1.0)), 254 | ?assertEqual(-1, int_ceil(-1.5)), 255 | ?assertEqual(-2, int_ceil(-2.0)), 256 | ok. 257 | 258 | int_pow_test() -> 259 | ?assertEqual(1, int_pow(1, 1)), 260 | ?assertEqual(1, int_pow(1, 0)), 261 | ?assertEqual(1, int_pow(10, 0)), 262 | ?assertEqual(10, int_pow(10, 1)), 263 | ?assertEqual(100, int_pow(10, 2)), 264 | ?assertEqual(1000, int_pow(10, 3)), 265 | ok. 266 | 267 | digits_test() -> 268 | ?assertEqual("0", 269 | digits(0)), 270 | ?assertEqual("0.0", 271 | digits(0.0)), 272 | ?assertEqual("1.0", 273 | digits(1.0)), 274 | ?assertEqual("-1.0", 275 | digits(-1.0)), 276 | ?assertEqual("0.1", 277 | digits(0.1)), 278 | ?assertEqual("0.01", 279 | digits(0.01)), 280 | ?assertEqual("0.001", 281 | digits(0.001)), 282 | ?assertEqual("1.0e+6", 283 | digits(1000000.0)), 284 | ?assertEqual("0.5", 285 | digits(0.5)), 286 | ?assertEqual("4503599627370496.0", 287 | digits(4503599627370496.0)), 288 | %% small denormalized number 289 | %% 4.94065645841246544177e-324 =:= 5.0e-324 290 | <> = <<0,0,0,0,0,0,0,1>>, 291 | ?assertEqual("5.0e-324", 292 | digits(SmallDenorm)), 293 | ?assertEqual(SmallDenorm, 294 | list_to_float(digits(SmallDenorm))), 295 | %% large denormalized number 296 | %% 2.22507385850720088902e-308 297 | <> = <<0,15,255,255,255,255,255,255>>, 298 | ?assertEqual("2.225073858507201e-308", 299 | digits(BigDenorm)), 300 | ?assertEqual(BigDenorm, 301 | list_to_float(digits(BigDenorm))), 302 | %% small normalized number 303 | %% 2.22507385850720138309e-308 304 | <> = <<0,16,0,0,0,0,0,0>>, 305 | ?assertEqual("2.2250738585072014e-308", 306 | digits(SmallNorm)), 307 | ?assertEqual(SmallNorm, 308 | list_to_float(digits(SmallNorm))), 309 | %% large normalized number 310 | %% 1.79769313486231570815e+308 311 | <> = <<127,239,255,255,255,255,255,255>>, 312 | ?assertEqual("1.7976931348623157e+308", 313 | digits(LargeNorm)), 314 | ?assertEqual(LargeNorm, 315 | list_to_float(digits(LargeNorm))), 316 | %% issue #10 - mochinum:frexp(math:pow(2, -1074)). 317 | ?assertEqual("5.0e-324", 318 | digits(math:pow(2, -1074))), 319 | ok. 320 | 321 | frexp_test() -> 322 | %% zero 323 | ?assertEqual({0.0, 0}, frexp(0.0)), 324 | %% one 325 | ?assertEqual({0.5, 1}, frexp(1.0)), 326 | %% negative one 327 | ?assertEqual({-0.5, 1}, frexp(-1.0)), 328 | %% small denormalized number 329 | %% 4.94065645841246544177e-324 330 | <> = <<0,0,0,0,0,0,0,1>>, 331 | ?assertEqual({0.5, -1073}, frexp(SmallDenorm)), 332 | %% large denormalized number 333 | %% 2.22507385850720088902e-308 334 | <> = <<0,15,255,255,255,255,255,255>>, 335 | ?assertEqual( 336 | {0.99999999999999978, -1022}, 337 | frexp(BigDenorm)), 338 | %% small normalized number 339 | %% 2.22507385850720138309e-308 340 | <> = <<0,16,0,0,0,0,0,0>>, 341 | ?assertEqual({0.5, -1021}, frexp(SmallNorm)), 342 | %% large normalized number 343 | %% 1.79769313486231570815e+308 344 | <> = <<127,239,255,255,255,255,255,255>>, 345 | ?assertEqual( 346 | {0.99999999999999989, 1024}, 347 | frexp(LargeNorm)), 348 | %% issue #10 - mochinum:frexp(math:pow(2, -1074)). 349 | ?assertEqual( 350 | {0.5, -1073}, 351 | frexp(math:pow(2, -1074))), 352 | ok. 353 | 354 | -endif. -------------------------------------------------------------------------------- /src/walrus_parser.yrl: -------------------------------------------------------------------------------- 1 | Nonterminals template token var block inverse. 2 | 3 | Terminals text key '{{' '{{{' '{{&' '{{#' '{{/' '{{^' '}}' '}}}'. 4 | 5 | Rootsymbol template. 6 | 7 | template -> token : ['$1']. 8 | template -> token template : ['$1' | '$2']. 9 | 10 | token -> text : {text, ?value('$1')}. 11 | token -> var : '$1'. 12 | token -> block : '$1'. 13 | token -> inverse : '$1'. 14 | 15 | var -> '{{' key '}}' : {var, ?value('$2')}. 16 | var -> '{{{' key '}}}' : {var_unescaped, ?value('$2')}. 17 | var -> '{{&' key '}}' : {var_unescaped, ?value('$2')}. 18 | 19 | block -> '{{#' key '}}' template '{{/' key '}}' 20 | : section(block, '$2', '$6', '$4'). 21 | 22 | inverse -> '{{^' key '}}' template '{{/' key '}}' 23 | : section(inverse, '$2', '$6', '$4'). 24 | 25 | Erlang code. 26 | 27 | -define(value(Token), element(3, Token)). 28 | 29 | -type token() :: {atom(), integer(), atom() | list()}. 30 | 31 | -spec section(Type :: atom(), token(), token(), list()) 32 | -> {Type :: atom(), Key1 :: atom(), Tmpl :: list()}. 33 | section(Type, {_, _, Key1}, {_, _, Key2}, Tmpl) when Key1 =:= Key2 -> 34 | {Type, Key1, Tmpl}; 35 | section(_, {_, Line, Key}, _, _) -> 36 | return_error(Line, {"Unmatched section tag", Key}). 37 | -------------------------------------------------------------------------------- /test/compile_test.erl: -------------------------------------------------------------------------------- 1 | -module(compile_test). 2 | 3 | -compile([export_all]). 4 | 5 | basic_test() -> 6 | Template = "Hello {{{name}}}.\n" 7 | "Drinks:\n" 8 | "{{#drinks}}\n" 9 | " - {{name}}, {{tastiness}}\n" 10 | "{{/drinks}}", 11 | 12 | Renderer = walrus:compile(Template), 13 | 14 | Context = [{name, "Devin & Jane"}, 15 | {drinks, [[{name, "Beer"}, 16 | {tastiness, 5}], 17 | [{name, "Juice"}, 18 | {tastiness, 8}]]}], 19 | 20 | Expected = <<"Hello Devin & Jane.\n" 21 | "Drinks:\n\n" 22 | " - Beer, 5\n\n" 23 | " - Juice, 8\n">>, 24 | 25 | Expected = Renderer(Context). 26 | -------------------------------------------------------------------------------- /test/lexer_test.erl: -------------------------------------------------------------------------------- 1 | -module(lexer_test). 2 | 3 | -compile([export_all]). 4 | 5 | basic_test() -> 6 | Tmpl = "Hello {{name}}. Would you like a\n" 7 | "{{#over18}}beer{{/over18}}\n" 8 | "{{^over18}}juice box{{/over18}}?", 9 | Expected = {ok,[{text,1,<<"Hello ">>}, 10 | {'{{',1}, 11 | {key,1,name}, 12 | {'}}',1}, 13 | {text,1,<<". Would you like a\n">>}, 14 | {'{{#',2}, 15 | {key,2,over18}, 16 | {'}}',2}, 17 | {text,2,<<"beer">>}, 18 | {'{{/',2}, 19 | {key,2,over18}, 20 | {'}}',2}, 21 | {text,2,<<"\n">>}, 22 | {'{{^',3}, 23 | {key,3,over18}, 24 | {'}}',3}, 25 | {text,3,<<"juice box">>}, 26 | {'{{/',3}, 27 | {key,3,over18}, 28 | {'}}',3}, 29 | {text,3,<<"?">>}],3}, 30 | Expected = walrus_lexer:string(Tmpl). 31 | -------------------------------------------------------------------------------- /test/parser_test.erl: -------------------------------------------------------------------------------- 1 | -module(parser_test). 2 | 3 | -compile([export_all]). 4 | 5 | basic_test() -> 6 | Tokens = [{text,1,"Hello "}, 7 | {'{{',1}, 8 | {key,1,name}, 9 | {'}}',1}, 10 | {text,1,". Would you like a\n"}, 11 | {'{{#',2}, 12 | {key,2,over18}, 13 | {'}}',2}, 14 | {text,2,"beer"}, 15 | {'{{/',2}, 16 | {key,2,over18}, 17 | {'}}',2}, 18 | {text,2,"\n"}, 19 | {'{{^',3}, 20 | {key,3,over18}, 21 | {'}}',3}, 22 | {text,3,"juice box?"}, 23 | {'{{/',3}, 24 | {key,3,over18}, 25 | {'}}',3}, 26 | {text,3,"?"}], 27 | Expected = {ok,[{text,"Hello "}, 28 | {var,name}, 29 | {text,". Would you like a\n"}, 30 | {block,over18,[{text,"beer"}]}, 31 | {text,"\n"}, 32 | {inverse,over18,[{text,"juice box?"}]}, 33 | {text,"?"}]}, 34 | Expected = walrus_parser:parse(Tokens). 35 | -------------------------------------------------------------------------------- /test/render_test.erl: -------------------------------------------------------------------------------- 1 | -module(render_test). 2 | 3 | -compile([export_all]). 4 | 5 | basic_test() -> 6 | Template = "Hello {{{name}}}.\n" 7 | "Drinks:\n" 8 | "{{#drinks}}\n" 9 | " - {{name}}, {{tastiness}}\n" 10 | "{{/drinks}}", 11 | Context = [{name, "Devin & Jane"}, 12 | {drinks, [[{name, "Beer"}, 13 | {tastiness, 5}], 14 | [{name, "Juice"}, 15 | {tastiness, 8}]]}], 16 | Expected = <<"Hello Devin & Jane.\n" 17 | "Drinks:\n\n" 18 | " - Beer, 5\n\n" 19 | " - Juice, 8\n">>, 20 | Expected = walrus:render(Template, Context). 21 | --------------------------------------------------------------------------------