Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

copy riak/apps/mochiweb to external mochiweb repo

  • Loading branch information...
commit 6605a4b45aa0462181043ec260389a717db4d44f 1 parent 8256e55
@beerriot beerriot authored
Showing with 2,114 additions and 851 deletions.
  1. +10 −7 Makefile
  2. 0  ebin/.hg_empty_dir
  3. +4 −1 ebin/mochiweb.app
  4. +0 −81 examples/keepalive/keepalive.erl
  5. +12 −0 priv/skel/Makefile
  6. +24 −0 priv/skel/src/Makefile
  7. +1 −1  priv/skel/src/skel.app
  8. +1 −1  priv/skel/src/skel.erl
  9. +9 −1 priv/skel/src/skel_app.erl
  10. +11 −3 priv/skel/src/skel_deps.erl
  11. +9 −1 priv/skel/src/skel_sup.erl
  12. +8 −0 priv/skel/src/skel_web.erl
  13. +1 −0  priv/skel/start-dev.sh
  14. +3 −0  rebar.config
  15. +33 −0 src/Makefile
  16. +13 −14 src/mochifmt.erl
  17. +8 −0 src/mochifmt_records.erl
  18. +7 −0 src/mochifmt_std.erl
  19. +107 −0 src/mochiglobal.erl
  20. +27 −11 src/mochihex.erl
  21. +11 −8 src/mochijson.erl
  22. +161 −42 src/mochijson2.erl
  23. +69 −25 src/mochinum.erl
  24. +35 −0 src/mochiweb.app
  25. +14 −21 src/mochiweb.erl
  26. +7 −0 src/mochiweb_app.erl
  27. +24 −11 src/mochiweb_charref.erl
  28. +88 −36 src/mochiweb_cookies.erl
  29. +75 −0 src/mochiweb_cover.erl
  30. +8 −1 src/mochiweb_echo.erl
  31. +139 −26 src/mochiweb_headers.erl
  32. +247 −239 src/mochiweb_html.erl
  33. +133 −9 src/mochiweb_http.erl
  34. +94 −0 src/mochiweb_mime.erl
  35. +131 −25 src/mochiweb_multipart.erl
  36. +56 −134 src/mochiweb_request.erl
  37. +8 −0 src/mochiweb_response.erl
  38. +24 −13 src/mochiweb_skel.erl
  39. +10 −2 src/mochiweb_socket_server.erl
  40. +7 −0 src/mochiweb_sup.erl
  41. +382 −130 src/mochiweb_util.erl
  42. +7 −0 src/reloader.erl
  43. +2 −8 {priv/skel → }/support/include.mk
  44. +94 −0 support/run_tests.escript
View
17 Makefile
@@ -1,14 +1,17 @@
-
all:
- ./rebar compile
+ (cd src;$(MAKE) all)
edoc:
- erl -noshell -pa ebin \
- -eval "edoc:application(mochiweb, \".\", [{dir, \"doc\"}])" \
- -s init stop
+ (cd src;$(MAKE) edoc)
test:
- erl -noshell -pa ebin -s mochiweb test -s init stop
+ (cd src;$(MAKE) test)
clean:
- ./rebar clean
+ (cd src;$(MAKE) clean)
+
+clean_plt:
+ (cd src;$(MAKE) clean_plt)
+
+dialyzer:
+ (cd src;$(MAKE) dialyzer)
View
0  ebin/.hg_empty_dir
No changes.
View
5 ebin/mochiweb.app
@@ -1,7 +1,8 @@
{application, mochiweb,
[{description, "MochiMedia Web Server"},
- {vsn, "0.01"},
+ {vsn, "0.02"},
{modules, [
+ mochiglobal,
mochihex,
mochijson,
mochijson2,
@@ -10,10 +11,12 @@
mochiweb_app,
mochiweb_charref,
mochiweb_cookies,
+ mochiweb_cover,
mochiweb_echo,
mochiweb_headers,
mochiweb_html,
mochiweb_http,
+ mochiweb_mime,
mochiweb_multipart,
mochiweb_request,
mochiweb_response,
View
81 examples/keepalive/keepalive.erl
@@ -1,81 +0,0 @@
--module(keepalive).
-
-%% your web app can push data to clients using a technique called comet long
-%% polling. browsers make a request and your server waits to send a
-%% response until data is available. see wikipedia for a better explanation:
-%% http://en.wikipedia.org/wiki/Comet_(programming)#Ajax_with_long_polling
-%%
-%% since the majority of your http handlers will be idle at any given moment,
-%% you might consider making them hibernate while they wait for more data from
-%% another process. however, since the execution stack is discarded when a
-%% process hibernates, the handler would usually terminate after your response
-%% code runs. this means http keep alives wouldn't work; the handler process
-%% would terminate after each response and close its socket rather than
-%% returning to the big @mochiweb_http@ loop and processing another request.
-%%
-%% however, if mochiweb exposes a continuation that encapsulates the return to
-%% the top of the big loop in @mochiweb_http@, we can call that after the
-%% response. if you do that then control flow returns to the proper place,
-%% and keep alives work like they would if you hadn't hibernated.
-
--export([ start/1, loop/1
- ]).
-
-%% internal export (so hibernate can reach it)
--export([ resume/3
- ]).
-
--define(LOOP, {?MODULE, loop}).
-
-start(Options = [{port, _Port}]) ->
- mochiweb_http:start([{name, ?MODULE}, {loop, ?LOOP} | Options]).
-
-loop(Req) ->
- Path = Req:get(path),
- case string:tokens(Path, "/") of
- ["longpoll" | RestOfPath] ->
- %% the "reentry" is a continuation -- what @mochiweb_http@
- %% needs to do to start its loop back at the top
- Reentry = mochiweb_http:reentry(?LOOP),
-
- %% here we could send a message to some other process and hope
- %% to get an interesting message back after a while. for
- %% simplicity let's just send ourselves a message after a few
- %% seconds
- erlang:send_after(2000, self(), "honk honk"),
-
- %% since we expect to wait for a long time before getting a
- %% reply, let's hibernate. memory usage will be minimized, so
- %% we won't be wasting memory just sitting in a @receive@
- proc_lib:hibernate(?MODULE, resume, [Req, RestOfPath, Reentry]),
-
- %% we'll never reach this point, and this function @loop/1@
- %% won't ever return control to @mochiweb_http@. luckily
- %% @resume/3@ will take care of that.
- io:format("not gonna happen~n", []);
-
- _ ->
- ok(Req, io_lib:format("some other page: ~p", [Path]))
- end,
-
- io:format("restarting loop normally in ~p~n", [Path]),
- ok.
-
-%% this is the function that's called when a message arrives.
-resume(Req, RestOfPath, Reentry) ->
- receive
- Msg ->
- Text = io_lib:format("wake up message: ~p~nrest of path: ~p", [Msg, RestOfPath]),
- ok(Req, Text)
- end,
-
- %% if we didn't call @Reentry@ here then the function would finish and the
- %% process would exit. calling @Reentry@ takes care of returning control
- %% to @mochiweb_http@
- io:format("reentering loop via continuation in ~p~n", [Req:get(path)]),
- Reentry(Req).
-
-ok(Req, Response) ->
- Req:ok({_ContentType = "text/plain",
- _Headers = [],
- Response}).
View
12 priv/skel/Makefile
@@ -1,5 +1,17 @@
all:
(cd src;$(MAKE))
+edoc:
+ (cd src;$(MAKE) edoc)
+
+test:
+ (cd src;$(MAKE) test)
+
clean:
(cd src;$(MAKE) clean)
+
+clean_plt:
+ (cd src;$(MAKE) clean_plt)
+
+dialyzer:
+ (cd src;$(MAKE) dialyzer)
View
24 priv/skel/src/Makefile
@@ -1,5 +1,9 @@
include ../support/include.mk
+APPLICATION=skel
+DOC_OPTS={dir,\"../doc\"}
+TEST_PLT=$(TEST_DIR)/dialyzer_plt
+
all: $(EBIN_FILES)
debug:
@@ -7,3 +11,23 @@ debug:
clean:
rm -rf $(EBIN_FILES)
+
+edoc:
+ $(ERL) -noshell -pa ../ebin \
+ -eval "edoc:application($(APPLICATION), \".\", [$(DOC_OPTS)])" \
+ -s init stop
+
+test: $(EBIN_FILES)
+ mkdir -p $(TEST_DIR);
+ @../support/run_tests.escript $(EBIN_DIR) | tee $(TEST_DIR)/test.log
+
+$(TEST_PLT):
+ mkdir -p $(TEST_DIR)
+ cp $(DIALYZER_PLT) $(TEST_PLT)
+ dialyzer --plt $(TEST_PLT) --add_to_plt -r ../deps/*/ebin
+
+clean_plt:
+ rm $(TEST_PLT)
+
+dialyzer: $(TEST_PLT)
+ dialyzer --src --plt $(TEST_PLT) -DNOTEST -DDIALYZER -c ../src | tee $(TEST_DIR)/dialyzer.log
View
2  priv/skel/src/skel.app
@@ -1,6 +1,6 @@
{application, skel,
[{description, "skel"},
- {vsn, "0.01"},
+ {vsn, "0.9"},
{modules, [
skel,
skel_app,
View
2  priv/skel/src/skel.erl
@@ -14,7 +14,7 @@ ensure_started(App) ->
{error, {already_started, App}} ->
ok
end.
-
+
%% @spec start() -> ok
%% @doc Start the skel server.
start() ->
View
10 priv/skel/src/skel_app.erl
@@ -7,7 +7,7 @@
-author('author <author@example.com>').
-behaviour(application).
--export([start/2,stop/1]).
+-export([start/2, stop/1]).
%% @spec start(_Type, _StartArgs) -> ServerRet
@@ -20,3 +20,11 @@ start(_Type, _StartArgs) ->
%% @doc application stop callback for skel.
stop(_State) ->
ok.
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.
View
14 priv/skel/src/skel_deps.erl
@@ -27,7 +27,7 @@ deps_on_path() ->
end
end,
ordsets:from_list(lists:foldl(F, [], code:get_path())).
-
+
%% @spec new_siblings(Module) -> [Dir]
%% @doc Find new siblings paths relative to Module that aren't already on the
%% code path.
@@ -38,11 +38,11 @@ new_siblings(Module) ->
ordsets:is_element(
filename:basename(filename:dirname(X)),
Existing) =:= false],
- lists:filter(fun filelib:is_dir/1,
+ lists:filter(fun filelib:is_dir/1,
lists:append([[filename:join([X, "ebin"]),
filename:join([X, "include"])] ||
X <- Siblings])).
-
+
%% @spec ensure(Module) -> ok
%% @doc Ensure that all ebin and include paths for dependencies
@@ -82,3 +82,11 @@ local_path(Components, Module) ->
%% Equivalent to local_path(Components, ?MODULE).
local_path(Components) ->
local_path(Components, ?MODULE).
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.
View
10 priv/skel/src/skel_sup.erl
@@ -41,7 +41,7 @@ upgrade() ->
%% @spec init([]) -> SupervisorTree
%% @doc supervisor callback.
init([]) ->
- Ip = case os:getenv("MOCHIWEB_IP") of false -> "0.0.0.0"; Any -> Any end,
+ Ip = case os:getenv("MOCHIWEB_IP") of false -> "0.0.0.0"; Any -> Any end,
WebConfig = [
{ip, Ip},
{port, 8000},
@@ -52,3 +52,11 @@ init([]) ->
Processes = [Web],
{ok, {{one_for_one, 10, 10}, Processes}}.
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.
View
8 priv/skel/src/skel_web.erl
@@ -41,3 +41,11 @@ loop(Req, DocRoot) ->
get_option(Option, Options) ->
{proplists:get_value(Option, Options), proplists:delete(Option, Options)}.
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.
View
1  priv/skel/start-dev.sh
@@ -1,3 +1,4 @@
#!/bin/sh
cd `dirname $0`
+make
exec erl -pa $PWD/ebin $PWD/deps/*/ebin -boot start_sasl -s reloader -s skel
View
3  rebar.config
@@ -0,0 +1,3 @@
+{erl_opts, [debug_info, fail_on_warning]}.
+
+
View
33 src/Makefile
@@ -0,0 +1,33 @@
+include ../support/include.mk
+
+APPLICATION=mochiweb
+DOC_OPTS={dir,\"../doc\"}
+TEST_PLT=$(TEST_DIR)/dialyzer_plt
+
+all: $(EBIN_FILES)
+
+debug:
+ $(MAKE) DEBUG=-DDEBUG
+
+clean:
+ rm -rf $(EBIN_FILES)
+
+edoc:
+ $(ERL) -noshell -pa ../ebin \
+ -eval "edoc:application($(APPLICATION), \".\", [$(DOC_OPTS)])" \
+ -s init stop
+
+test: $(EBIN_FILES)
+ mkdir -p $(TEST_DIR);
+ @../support/run_tests.escript $(EBIN_DIR) | tee $(TEST_DIR)/test.log
+
+$(TEST_PLT):
+ mkdir -p $(TEST_DIR)
+ cp $(DIALYZER_PLT) $(TEST_PLT)
+ dialyzer --plt $(TEST_PLT) --add_to_plt
+
+clean_plt:
+ rm $(TEST_PLT)
+
+dialyzer: $(TEST_PLT)
+ dialyzer --src --plt $(TEST_PLT) -DNOTEST -DDIALYZER -c ../src | tee $(TEST_DIR)/dialyzer.log
View
27 src/mochifmt.erl
@@ -10,7 +10,6 @@
-export([tokenize/1, format/3, get_field/3, format_field/3]).
-export([bformat/2, bformat/3]).
-export([f/2, f/3]).
--export([test/0]).
-record(conversion, {length, precision, ctype, align, fill_char, sign}).
@@ -113,15 +112,6 @@ bformat(Format, Args) ->
bformat(Format, Args, Module) ->
iolist_to_binary(format(Format, Args, Module)).
-%% @spec test() -> ok
-%% @doc Run tests.
-test() ->
- ok = test_tokenize(),
- ok = test_format(),
- ok = test_std(),
- ok = test_records(),
- ok.
-
%% Internal API
add_raw("", Acc) ->
@@ -375,14 +365,21 @@ parse_std_conversion([$. | Spec], Acc) ->
parse_std_conversion([Type], Acc) ->
parse_std_conversion("", Acc#conversion{ctype=ctype(Type)}).
-test_tokenize() ->
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
+tokenize_test() ->
{?MODULE, [{raw, "ABC"}]} = tokenize("ABC"),
{?MODULE, [{format, {"0", "", ""}}]} = tokenize("{0}"),
{?MODULE, [{raw, "ABC"}, {format, {"1", "", ""}}, {raw, "DEF"}]} =
tokenize("ABC{1}DEF"),
ok.
-test_format() ->
+format_test() ->
<<" -4">> = bformat("{0:4}", [-4]),
<<" 4">> = bformat("{0:4}", [4]),
<<" 4">> = bformat("{0:{0}}", [4]),
@@ -410,12 +407,12 @@ test_format() ->
{{2008,5,4}, {4, 2, 2}}),
ok.
-test_std() ->
+std_test() ->
M = mochifmt_std:new(),
<<"01">> = bformat("{0}{1}", [0, 1], M),
ok.
-test_records() ->
+records_test() ->
M = mochifmt_records:new([{conversion, record_info(fields, conversion)}]),
R = #conversion{length=long, precision=hard, sign=peace},
long = M:get_value("length", R),
@@ -424,3 +421,5 @@ test_records() ->
<<"long hard">> = bformat("{length} {precision}", R, M),
<<"long hard">> = bformat("{0.length} {0.precision}", [R], M),
ok.
+
+-endif.
View
8 src/mochifmt_records.erl
@@ -28,3 +28,11 @@ get_rec_index(Atom, [Atom | _], Index) ->
Index;
get_rec_index(Atom, [_ | Rest], Index) ->
get_rec_index(Atom, Rest, 1 + Index).
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.
View
7 src/mochifmt_std.erl
@@ -21,3 +21,10 @@ get_value(Key, Args) ->
format_field(Arg, Format) ->
mochifmt:format_field(Arg, Format, THIS).
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.
View
107 src/mochiglobal.erl
@@ -0,0 +1,107 @@
+%% @author Bob Ippolito <bob@mochimedia.com>
+%% @copyright 2010 Mochi Media, Inc.
+%% @doc Abuse module constant pools as a "read-only shared heap" (since erts 5.6)
+%% <a href="http://www.erlang.org/pipermail/erlang-questions/2009-March/042503.html">[1]</a>.
+-module(mochiglobal).
+-author("Bob Ippolito <bob@mochimedia.com>").
+-export([get/1, get/2, put/2, delete/1]).
+
+-spec get(atom()) -> any() | undefined.
+%% @equiv get(K, undefined)
+get(K) ->
+ get(K, undefined).
+
+-spec get(atom(), T) -> any() | T.
+%% @doc Get the term for K or return Default.
+get(K, Default) ->
+ get(K, Default, key_to_module(K)).
+
+get(_K, Default, Mod) ->
+ try Mod:term()
+ catch error:undef ->
+ Default
+ end.
+
+-spec put(atom(), any()) -> ok.
+%% @doc Store term V at K, replaces an existing term if present.
+put(K, V) ->
+ put(K, V, key_to_module(K)).
+
+put(_K, V, Mod) ->
+ Bin = compile(Mod, V),
+ code:purge(Mod),
+ code:load_binary(Mod, atom_to_list(Mod) ++ ".erl", Bin),
+ ok.
+
+-spec delete(atom()) -> boolean().
+%% @doc Delete term stored at K, no-op if non-existent.
+delete(K) ->
+ delete(K, key_to_module(K)).
+
+delete(_K, Mod) ->
+ code:purge(Mod),
+ code:delete(Mod).
+
+-spec key_to_module(atom()) -> atom().
+key_to_module(K) ->
+ list_to_atom("mochiglobal:" ++ atom_to_list(K)).
+
+-spec compile(atom(), any()) -> binary().
+compile(Module, T) ->
+ {ok, Module, Bin} = compile:forms(forms(Module, T),
+ [verbose, report_errors]),
+ Bin.
+
+-spec forms(atom(), any()) -> [erl_syntax:syntaxTree()].
+forms(Module, T) ->
+ [erl_syntax:revert(X) || X <- term_to_abstract(Module, term, T)].
+
+-spec term_to_abstract(atom(), atom(), any()) -> [erl_syntax:syntaxTree()].
+term_to_abstract(Module, Getter, T) ->
+ [%% -module(Module).
+ erl_syntax:attribute(
+ erl_syntax:atom(module),
+ [erl_syntax:atom(Module)]),
+ %% -export([Getter/0]).
+ erl_syntax:attribute(
+ erl_syntax:atom(export),
+ [erl_syntax:list(
+ [erl_syntax:arity_qualifier(
+ erl_syntax:atom(Getter),
+ erl_syntax:integer(0))])]),
+ %% Getter() -> T.
+ erl_syntax:function(
+ erl_syntax:atom(Getter),
+ [erl_syntax:clause([], none, [erl_syntax:abstract(T)])])].
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+get_put_delete_test() ->
+ K = '$$test$$mochiglobal',
+ delete(K),
+ ?assertEqual(
+ bar,
+ get(K, bar)),
+ try
+ ?MODULE:put(K, baz),
+ ?assertEqual(
+ baz,
+ get(K, bar)),
+ ?MODULE:put(K, wibble),
+ ?assertEqual(
+ wibble,
+ ?MODULE:get(K))
+ after
+ delete(K)
+ end,
+ ?assertEqual(
+ bar,
+ get(K, bar)),
+ ?assertEqual(
+ undefined,
+ ?MODULE:get(K)),
+ ok.
+-endif.
View
38 src/mochihex.erl
@@ -6,7 +6,7 @@
-module(mochihex).
-author('bob@mochimedia.com').
--export([test/0, to_hex/1, to_bin/1, to_int/1, dehex/1, hexdigit/1]).
+-export([to_hex/1, to_bin/1, to_int/1, dehex/1, hexdigit/1]).
%% @type iolist() = [char() | binary() | iolist()]
%% @type iodata() = iolist() | binary()
@@ -46,16 +46,6 @@ hexdigit(C) when C >= 0, C =< 9 ->
hexdigit(C) when C =< 15 ->
C + $a - 10.
-%% @spec test() -> ok
-%% @doc Test this module.
-test() ->
- "ff000ff1" = to_hex([255, 0, 15, 241]),
- <<255, 0, 15, 241>> = to_bin("ff000ff1"),
- 16#ff000ff1 = to_int("ff000ff1"),
- "ff000ff1" = to_hex(16#ff000ff1),
- ok.
-
-
%% Internal API
to_hex(<<>>, Acc) ->
@@ -73,3 +63,29 @@ to_bin([], Acc) ->
to_bin([C1, C2 | Rest], Acc) ->
to_bin(Rest, [(dehex(C1) bsl 4) bor dehex(C2) | Acc]).
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
+to_hex_test() ->
+ "ff000ff1" = to_hex([255, 0, 15, 241]),
+ "ff000ff1" = to_hex(16#ff000ff1),
+ "0" = to_hex(16#0),
+ ok.
+
+to_bin_test() ->
+ <<255, 0, 15, 241>> = to_bin("ff000ff1"),
+ <<255, 0, 10, 161>> = to_bin("Ff000aA1"),
+ ok.
+
+to_int_test() ->
+ 16#ff000ff1 = to_int("ff000ff1"),
+ 16#ff000aa1 = to_int("FF000Aa1"),
+ 16#0 = to_int("0"),
+ ok.
+
+-endif.
View
19 src/mochijson.erl
@@ -8,7 +8,6 @@
-export([decoder/1, decode/1]).
-export([binary_encoder/1, binary_encode/1]).
-export([binary_decoder/1, binary_decode/1]).
--export([test/0]).
% This is a macro to placate syntax highlighters..
-define(Q, $\").
@@ -91,10 +90,6 @@ binary_encode(Any) ->
binary_decode(S) ->
mochijson2:decode(S).
-test() ->
- test_all(),
- mochijson2:test().
-
%% Internal API
parse_encoder_options([], State) ->
@@ -407,6 +402,13 @@ tokenize(L=[C | _], S) when C >= $0, C =< $9; C == $- ->
{{const, list_to_float(Float)}, Rest, S1}
end.
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
%% testing constructs borrowed from the Yaws JSON implementation.
%% Create an object from a list of Key/Value pairs.
@@ -462,11 +464,10 @@ equiv_list([], []) ->
equiv_list([V1 | L1], [V2 | L2]) ->
equiv(V1, V2) andalso equiv_list(L1, L2).
-test_all() ->
- test_issue33(),
+e2j_vec_test() ->
test_one(e2j_test_vec(utf8), 1).
-test_issue33() ->
+issue33_test() ->
%% http://code.google.com/p/mochiweb/issues/detail?id=33
Js = {struct, [{"key", [194, 163]}]},
Encoder = encoder([{input_encoding, utf8}]),
@@ -526,3 +527,5 @@ e2j_test_vec(utf8) ->
{{array, [-123, "foo", obj_from_list([{"bar", {array, []}}]), null]},
"[-123,\"foo\",{\"bar\":[]},null]"}
].
+
+-endif.
View
203 src/mochijson2.erl
@@ -9,7 +9,6 @@
-author('bob@mochimedia.com').
-export([encoder/1, encode/1]).
-export([decoder/1, decode/1]).
--export([test/0]).
% This is a macro to placate syntax highlighters..
-define(Q, $\").
@@ -39,8 +38,9 @@
%% @type json_number() = integer() | float()
%% @type json_array() = [json_term()]
%% @type json_object() = {struct, [{json_string(), json_term()}]}
+%% @type json_iolist() = {json, iolist()}
%% @type json_term() = json_string() | json_number() | json_array() |
-%% json_object()
+%% json_object() | json_iolist()
-record(encoder, {handler=null,
utf8=false}).
@@ -75,9 +75,6 @@ decoder(Options) ->
decode(S) ->
json_decode(S, #decoder{}).
-test() ->
- test_all().
-
%% Internal API
parse_encoder_options([], State) ->
@@ -111,6 +108,8 @@ json_encode(Array, State) when is_list(Array) ->
json_encode_array(Array, State);
json_encode({struct, Props}, State) when is_list(Props) ->
json_encode_proplist(Props, State);
+json_encode({json, IoList}, _State) ->
+ IoList;
json_encode(Bad, #encoder{handler=null}) ->
exit({json_encode, {bad_term, Bad}});
json_encode(Bad, State=#encoder{handler=Handler}) ->
@@ -205,12 +204,10 @@ json_bin_is_safe(<<C, Rest/binary>>) ->
false;
$\t ->
false;
- C when C >= 0, C < $\s; C >= 16#7f, C =< 16#10FFFF ->
+ C when C >= 0, C < $\s; C >= 16#7f ->
false;
C when C < 16#7f ->
- json_bin_is_safe(Rest);
- _ ->
- false
+ json_bin_is_safe(Rest)
end.
json_encode_string_unicode([], _State, Acc) ->
@@ -510,6 +507,12 @@ tokenize(B, S=#decoder{offset=O}) ->
trim = S#decoder.state,
{eof, S}
end.
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
%% testing constructs borrowed from the Yaws JSON implementation.
@@ -519,19 +522,13 @@ obj_new() ->
{struct, []}.
is_obj({struct, Props}) ->
- F = fun ({K, _}) when is_binary(K) ->
- true;
- (_) ->
- false
- end,
+ F = fun ({K, _}) when is_binary(K) -> true end,
lists:all(F, Props).
obj_from_list(Props) ->
Obj = {struct, Props},
- case is_obj(Obj) of
- true -> Obj;
- false -> exit({json_bad_object, Obj})
- end.
+ ?assert(is_obj(Obj)),
+ Obj.
%% Test for equivalence of Erlang terms.
%% Due to arbitrary order of construction, equivalent objects might
@@ -544,9 +541,7 @@ equiv(L1, L2) when is_list(L1), is_list(L2) ->
equiv_list(L1, L2);
equiv(N1, N2) when is_number(N1), is_number(N2) -> N1 == N2;
equiv(B1, B2) when is_binary(B1), is_binary(B2) -> B1 == B2;
-equiv(true, true) -> true;
-equiv(false, false) -> true;
-equiv(null, null) -> true.
+equiv(A, A) when A =:= true orelse A =:= false orelse A =:= null -> true.
%% Object representation and traversal order is unknown.
%% Use the sledgehammer and sort property lists.
@@ -566,11 +561,11 @@ equiv_list([], []) ->
equiv_list([V1 | L1], [V2 | L2]) ->
equiv(V1, V2) andalso equiv_list(L1, L2).
-test_all() ->
+decode_test() ->
[1199344435545.0, 1] = decode(<<"[1199344435545.0,1]">>),
- <<16#F0,16#9D,16#9C,16#95>> = decode([34,"\\ud835","\\udf15",34]),
- test_encoder_utf8(),
- test_input_validation(),
+ <<16#F0,16#9D,16#9C,16#95>> = decode([34,"\\ud835","\\udf15",34]).
+
+e2j_vec_test() ->
test_one(e2j_test_vec(utf8), 1).
test_one([], _N) ->
@@ -606,30 +601,30 @@ e2j_test_vec(utf8) ->
{[], "[]"},
{[[]], "[[]]"},
{[1, <<"foo">>], "[1,\"foo\"]"},
-
+
%% json array in a json object
{obj_from_list([{<<"foo">>, [123]}]),
"{\"foo\":[123]}"},
-
+
%% json object in a json object
{obj_from_list([{<<"foo">>, obj_from_list([{<<"bar">>, true}])}]),
"{\"foo\":{\"bar\":true}}"},
-
+
%% fold evaluation order
{obj_from_list([{<<"foo">>, []},
{<<"bar">>, obj_from_list([{<<"baz">>, true}])},
{<<"alice">>, <<"bob">>}]),
"{\"foo\":[],\"bar\":{\"baz\":true},\"alice\":\"bob\"}"},
-
+
%% json object in a json array
{[-123, <<"foo">>, obj_from_list([{<<"bar">>, []}]), null],
"[-123,\"foo\",{\"bar\":[]},null]"}
].
%% test utf8 encoding
-test_encoder_utf8() ->
+encoder_utf8_test() ->
%% safe conversion case (default)
- [34,"\\u0001","\\u0442","\\u0435","\\u0441","\\u0442",34] =
+ [34,"\\u0001","\\u0442","\\u0435","\\u0441","\\u0442",34] =
encode(<<1,"\321\202\320\265\321\201\321\202">>),
%% raw utf8 output (optional)
@@ -637,27 +632,151 @@ test_encoder_utf8() ->
[34,"\\u0001",[209,130],[208,181],[209,129],[209,130],34] =
Enc(<<1,"\321\202\320\265\321\201\321\202">>).
-test_input_validation() ->
+input_validation_test() ->
Good = [
- {16#00A3, <<?Q, 16#C2, 16#A3, ?Q>>}, % pound
- {16#20AC, <<?Q, 16#E2, 16#82, 16#AC, ?Q>>}, % euro
- {16#10196, <<?Q, 16#F0, 16#90, 16#86, 16#96, ?Q>>} % denarius
+ {16#00A3, <<?Q, 16#C2, 16#A3, ?Q>>}, %% pound
+ {16#20AC, <<?Q, 16#E2, 16#82, 16#AC, ?Q>>}, %% euro
+ {16#10196, <<?Q, 16#F0, 16#90, 16#86, 16#96, ?Q>>} %% denarius
],
lists:foreach(fun({CodePoint, UTF8}) ->
Expect = list_to_binary(xmerl_ucs:to_utf8(CodePoint)),
Expect = decode(UTF8)
end, Good),
-
+
Bad = [
- % 2nd, 3rd, or 4th byte of a multi-byte sequence w/o leading byte
+ %% 2nd, 3rd, or 4th byte of a multi-byte sequence w/o leading byte
<<?Q, 16#80, ?Q>>,
- % missing continuations, last byte in each should be 80-BF
+ %% missing continuations, last byte in each should be 80-BF
<<?Q, 16#C2, 16#7F, ?Q>>,
<<?Q, 16#E0, 16#80,16#7F, ?Q>>,
<<?Q, 16#F0, 16#80, 16#80, 16#7F, ?Q>>,
- % we don't support code points > 10FFFF per RFC 3629
+ %% we don't support code points > 10FFFF per RFC 3629
<<?Q, 16#F5, 16#80, 16#80, 16#80, ?Q>>
],
- lists:foreach(fun(X) ->
- ok = try decode(X) catch invalid_utf8 -> ok end
- end, Bad).
+ lists:foreach(
+ fun(X) ->
+ ok = try decode(X) catch invalid_utf8 -> ok end,
+ %% could be {ucs,{bad_utf8_character_code}} or
+ %% {json_encode,{bad_char,_}}
+ {'EXIT', _} = (catch encode(X))
+ end, Bad).
+
+inline_json_test() ->
+ ?assertEqual(<<"\"iodata iodata\"">>,
+ iolist_to_binary(
+ encode({json, [<<"\"iodata">>, " iodata\""]}))),
+ ?assertEqual({struct, [{<<"key">>, <<"iodata iodata">>}]},
+ decode(
+ encode({struct,
+ [{key, {json, [<<"\"iodata">>, " iodata\""]}}]}))),
+ ok.
+
+big_unicode_test() ->
+ UTF8Seq = list_to_binary(xmerl_ucs:to_utf8(16#0001d120)),
+ ?assertEqual(
+ <<"\"\\ud834\\udd20\"">>,
+ iolist_to_binary(encode(UTF8Seq))),
+ ?assertEqual(
+ UTF8Seq,
+ decode(iolist_to_binary(encode(UTF8Seq)))),
+ ok.
+
+custom_decoder_test() ->
+ ?assertEqual(
+ {struct, [{<<"key">>, <<"value">>}]},
+ (decoder([]))("{\"key\": \"value\"}")),
+ F = fun ({struct, [{<<"key">>, <<"value">>}]}) -> win end,
+ ?assertEqual(
+ win,
+ (decoder([{object_hook, F}]))("{\"key\": \"value\"}")),
+ ok.
+
+atom_test() ->
+ %% JSON native atoms
+ [begin
+ ?assertEqual(A, decode(atom_to_list(A))),
+ ?assertEqual(iolist_to_binary(atom_to_list(A)),
+ iolist_to_binary(encode(A)))
+ end || A <- [true, false, null]],
+ %% Atom to string
+ ?assertEqual(
+ <<"\"foo\"">>,
+ iolist_to_binary(encode(foo))),
+ ?assertEqual(
+ <<"\"\\ud834\\udd20\"">>,
+ iolist_to_binary(encode(list_to_atom(xmerl_ucs:to_utf8(16#0001d120))))),
+ ok.
+
+key_encode_test() ->
+ %% Some forms are accepted as keys that would not be strings in other
+ %% cases
+ ?assertEqual(
+ <<"{\"foo\":1}">>,
+ iolist_to_binary(encode({struct, [{foo, 1}]}))),
+ ?assertEqual(
+ <<"{\"foo\":1}">>,
+ iolist_to_binary(encode({struct, [{<<"foo">>, 1}]}))),
+ ?assertEqual(
+ <<"{\"foo\":1}">>,
+ iolist_to_binary(encode({struct, [{"foo", 1}]}))),
+ ?assertEqual(
+ <<"{\"\\ud834\\udd20\":1}">>,
+ iolist_to_binary(
+ encode({struct, [{[16#0001d120], 1}]}))),
+ ?assertEqual(
+ <<"{\"1\":1}">>,
+ iolist_to_binary(encode({struct, [{1, 1}]}))),
+ ok.
+
+unsafe_chars_test() ->
+ Chars = "\"\\\b\f\n\r\t",
+ [begin
+ ?assertEqual(false, json_string_is_safe([C])),
+ ?assertEqual(false, json_bin_is_safe(<<C>>)),
+ ?assertEqual(<<C>>, decode(encode(<<C>>)))
+ end || C <- Chars],
+ ?assertEqual(
+ false,
+ json_string_is_safe([16#0001d120])),
+ ?assertEqual(
+ false,
+ json_bin_is_safe(list_to_binary(xmerl_ucs:to_utf8(16#0001d120)))),
+ ?assertEqual(
+ [16#0001d120],
+ xmerl_ucs:from_utf8(
+ binary_to_list(
+ decode(encode(list_to_atom(xmerl_ucs:to_utf8(16#0001d120))))))),
+ ?assertEqual(
+ false,
+ json_string_is_safe([16#110000])),
+ ?assertEqual(
+ false,
+ json_bin_is_safe(list_to_binary(xmerl_ucs:to_utf8([16#110000])))),
+ %% solidus can be escaped but isn't unsafe by default
+ ?assertEqual(
+ <<"/">>,
+ decode(<<"\"\\/\"">>)),
+ ok.
+
+int_test() ->
+ ?assertEqual(0, decode("0")),
+ ?assertEqual(1, decode("1")),
+ ?assertEqual(11, decode("11")),
+ ok.
+
+float_fallback_test() ->
+ ?assertEqual(<<"-2147483649.0">>, iolist_to_binary(encode(-2147483649))),
+ ?assertEqual(<<"2147483648.0">>, iolist_to_binary(encode(2147483648))),
+ ok.
+
+handler_test() ->
+ ?assertEqual(
+ {'EXIT',{json_encode,{bad_term,{}}}},
+ catch encode({})),
+ F = fun ({}) -> [] end,
+ ?assertEqual(
+ <<"[]">>,
+ iolist_to_binary((encoder([{handler, F}]))({}))),
+ ok.
+
+-endif.
View
94 src/mochinum.erl
@@ -11,7 +11,7 @@
-module(mochinum).
-author("Bob Ippolito <bob@mochimedia.com>").
--export([digits/1, frexp/1, int_pow/2, int_ceil/1, test/0]).
+-export([digits/1, frexp/1, int_pow/2, int_ceil/1]).
%% IEEE 754 Float exponent bias
-define(FLOAT_BIAS, 1022).
@@ -40,7 +40,7 @@ digits(Float) ->
_ ->
R
end.
-
+
%% @spec frexp(F::float()) -> {Frac::float(), Exp::float()}
%% @doc Return the fractional and exponent part of an IEEE 754 double,
%% equivalent to the libc function of the same name.
@@ -120,7 +120,7 @@ digits1(Float, Exp, Frac) ->
case Exp >= 0 of
true ->
BExp = 1 bsl Exp,
- case (Frac /= ?BIG_POW) of
+ case (Frac =/= ?BIG_POW) of
true ->
scale((Frac * BExp * 2), 2, BExp, BExp,
Round, Round, Float);
@@ -129,7 +129,7 @@ digits1(Float, Exp, Frac) ->
Round, Round, Float)
end;
false ->
- case (Exp == ?MIN_EXP) orelse (Frac /= ?BIG_POW) of
+ case (Exp =:= ?MIN_EXP) orelse (Frac =/= ?BIG_POW) of
true ->
scale((Frac * 2), 1 bsl (1 - Exp), 1, 1,
Round, Round, Float);
@@ -205,7 +205,7 @@ generate(R0, S, MPlus, MMinus, LowOk, HighOk) ->
end
end.
-unpack(Float) ->
+unpack(Float) ->
<<Sign:1, Exp:11, Frac:52>> = <<Float:64/float>>,
{Sign, Exp, Frac}.
@@ -228,14 +228,13 @@ log2floor(Int, N) ->
log2floor(Int bsr 1, 1 + N).
-test() ->
- ok = test_frexp(),
- ok = test_int_ceil(),
- ok = test_int_pow(),
- ok = test_digits(),
- ok.
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
-test_int_ceil() ->
+int_ceil_test() ->
1 = int_ceil(0.0001),
0 = int_ceil(0.0),
1 = int_ceil(0.99),
@@ -243,8 +242,8 @@ test_int_ceil() ->
-1 = int_ceil(-1.5),
-2 = int_ceil(-2.0),
ok.
-
-test_int_pow() ->
+
+int_pow_test() ->
1 = int_pow(1, 1),
1 = int_pow(1, 0),
1 = int_pow(10, 0),
@@ -252,18 +251,61 @@ test_int_pow() ->
100 = int_pow(10, 2),
1000 = int_pow(10, 3),
ok.
-
-test_digits() ->
- "0" = digits(0),
- "0.0" = digits(0.0),
- "1.0" = digits(1.0),
- "-1.0" = digits(-1.0),
- "0.1" = digits(0.1),
- "0.01" = digits(0.01),
- "0.001" = digits(0.001),
- ok.
-test_frexp() ->
+%% XXX arg 02/12/10
+%% disabled pending resolution of http://code.google.com/p/mochiweb/issues/detail?id=63
+%digits_test_disabled() ->
+% ?assertEqual("0",
+% digits(0)),
+% ?assertEqual("0.0",
+% digits(0.0)),
+% ?assertEqual("1.0",
+% digits(1.0)),
+% ?assertEqual("-1.0",
+% digits(-1.0)),
+% ?assertEqual("0.1",
+% digits(0.1)),
+% ?assertEqual("0.01",
+% digits(0.01)),
+% ?assertEqual("0.001",
+% digits(0.001)),
+% ?assertEqual("1.0e+6",
+% digits(1000000.0)),
+% ?assertEqual("0.5",
+% digits(0.5)),
+% ?assertEqual("4503599627370496.0",
+% digits(4503599627370496.0)),
+% %% small denormalized number
+% %% 4.94065645841246544177e-324
+% <<SmallDenorm/float>> = <<0,0,0,0,0,0,0,1>>,
+% ?assertEqual("4.9406564584124654e-324",
+% digits(SmallDenorm)),
+% ?assertEqual(SmallDenorm,
+% list_to_float(digits(SmallDenorm))),
+% %% large denormalized number
+% %% 2.22507385850720088902e-308
+% <<BigDenorm/float>> = <<0,15,255,255,255,255,255,255>>,
+% ?assertEqual("2.225073858507201e-308",
+% digits(BigDenorm)),
+% ?assertEqual(BigDenorm,
+% list_to_float(digits(BigDenorm))),
+% %% small normalized number
+% %% 2.22507385850720138309e-308
+% <<SmallNorm/float>> = <<0,16,0,0,0,0,0,0>>,
+% ?assertEqual("2.2250738585072014e-308",
+% digits(SmallNorm)),
+% ?assertEqual(SmallNorm,
+% list_to_float(digits(SmallNorm))),
+% %% large normalized number
+% %% 1.79769313486231570815e+308
+% <<LargeNorm/float>> = <<127,239,255,255,255,255,255,255>>,
+% ?assertEqual("1.7976931348623157e+308",
+% digits(LargeNorm)),
+% ?assertEqual(LargeNorm,
+% list_to_float(digits(LargeNorm))),
+% ok.
+
+frexp_test() ->
%% zero
{0.0, 0} = frexp(0.0),
%% one
@@ -287,3 +329,5 @@ test_frexp() ->
<<LargeNorm/float>> = <<127,239,255,255,255,255,255,255>>,
{0.99999999999999989, 1024} = frexp(LargeNorm),
ok.
+
+-endif.
View
35 src/mochiweb.app
@@ -0,0 +1,35 @@
+{application, mochiweb,
+ [{description, "MochiMedia Web Server"},
+ {vsn, "0.02"},
+ {modules, [
+ mochiglobal,
+ mochihex,
+ mochijson,
+ mochijson2,
+ mochinum,
+ mochiweb,
+ mochiweb_app,
+ mochiweb_charref,
+ mochiweb_cookies,
+ mochiweb_cover,
+ mochiweb_echo,
+ mochiweb_headers,
+ mochiweb_html,
+ mochiweb_http,
+ mochiweb_mime,
+ mochiweb_multipart,
+ mochiweb_request,
+ mochiweb_response,
+ mochiweb_skel,
+ mochiweb_socket_server,
+ mochiweb_sup,
+ mochiweb_util,
+ reloader,
+ mochifmt,
+ mochifmt_std,
+ mochifmt_records
+ ]},
+ {registered, []},
+ {mod, {mochiweb_app, []}},
+ {env, []},
+ {applications, [kernel, stdlib]}]}.
View
35 src/mochiweb.erl
@@ -9,7 +9,6 @@
-export([start/0, stop/0]).
-export([new_request/1, new_response/1]).
-export([all_loaded/0, all_loaded/1, reload/0]).
--export([test/0]).
%% @spec start() -> ok
%% @doc Start the MochiWeb server.
@@ -24,21 +23,6 @@ stop() ->
application:stop(crypto),
Res.
-%% @spec test() -> ok
-%% @doc Run all of the tests for MochiWeb.
-test() ->
- mochiweb_util:test(),
- mochiweb_headers:test(),
- mochiweb_cookies:test(),
- mochihex:test(),
- mochinum:test(),
- mochijson:test(),
- mochiweb_charref:test(),
- mochiweb_html:test(),
- mochifmt:test(),
- test_request(),
- ok.
-
reload() ->
[c:l(Module) || Module <- all_loaded()].
@@ -96,11 +80,6 @@ new_response({Request, Code, Headers}) ->
%% Internal API
-test_request() ->
- R = mochiweb_request:new(z, z, "/foo/bar/baz%20wibble+quux?qs=2", z, []),
- "/foo/bar/baz wibble quux" = R:get(path),
- ok.
-
ensure_started(App) ->
case application:start(App) of
ok ->
@@ -108,3 +87,17 @@ ensure_started(App) ->
{error, {already_started, App}} ->
ok
end.
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
+request_test() ->
+ R = mochiweb_request:new(z, z, "/foo/bar/baz%20wibble+quux?qs=2", z, []),
+ "/foo/bar/baz wibble quux" = R:get(path),
+ ok.
+
+-endif.
View
7 src/mochiweb_app.erl
@@ -18,3 +18,10 @@ start(_Type, _StartArgs) ->
%% @doc application stop callback for mochiweb.
stop(_State) ->
ok.
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.
View
35 src/mochiweb_charref.erl
@@ -3,7 +3,7 @@
%% @doc Converts HTML 4 charrefs and entities to codepoints.
-module(mochiweb_charref).
--export([charref/1, test/0]).
+-export([charref/1]).
%% External API.
@@ -26,16 +26,6 @@ charref([$# | L]) ->
end;
charref(L) ->
entity(L).
-
-%% @spec test() -> ok
-%% @doc Run tests for mochiweb_charref.
-test() ->
- 1234 = charref("#1234"),
- 255 = charref("#xfF"),
- 255 = charref("#XFf"),
- 38 = charref("amp"),
- undefined = charref("not_an_entity"),
- ok.
%% Internal API.
@@ -293,3 +283,26 @@ entity("rsaquo") -> 8250;
entity("euro") -> 8364;
entity(_) -> undefined.
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
+exhaustive_entity_test() ->
+ T = mochiweb_cover:clause_lookup_table(?MODULE, entity),
+ [?assertEqual(V, entity(K)) || {K, V} <- T].
+
+charref_test() ->
+ 1234 = charref("#1234"),
+ 255 = charref("#xfF"),
+ 255 = charref(<<"#XFf">>),
+ 38 = charref("amp"),
+ 38 = charref(<<"amp">>),
+ undefined = charref("not_an_entity"),
+ undefined = charref("#not_an_entity"),
+ undefined = charref("#xnot_an_entity"),
+ ok.
+
+-endif.
View
124 src/mochiweb_cookies.erl
@@ -4,7 +4,7 @@
%% @doc HTTP Cookie parsing and generating (RFC 2109, RFC 2965).
-module(mochiweb_cookies).
--export([parse_cookie/1, cookie/3, cookie/2, test/0]).
+-export([parse_cookie/1, cookie/3, cookie/2]).
-define(QUOTE, $\").
@@ -29,8 +29,8 @@
cookie(Key, Value) ->
cookie(Key, Value, []).
-%% @spec cookie(Key::string(), Value::string(), Options::[Option]) -> header()
-%% where Option = {max_age, integer()} | {local_time, {date(), time()}}
+%% @spec cookie(Key::string(), Value::string(), Options::[Option]) -> header()
+%% where Option = {max_age, integer()} | {local_time, {date(), time()}}
%% | {domain, string()} | {path, string()}
%% | {secure, true | false} | {http_only, true | false}
%%
@@ -125,23 +125,16 @@ age_to_cookie_date(Age, LocalTime) ->
%% @spec parse_cookie(string()) -> [{K::string(), V::string()}]
%% @doc Parse the contents of a Cookie header field, ignoring cookie
%% attributes, and return a simple property list.
-parse_cookie("") ->
+parse_cookie("") ->
[];
-parse_cookie(Cookie) ->
+parse_cookie(Cookie) ->
parse_cookie(Cookie, []).
-%% @spec test() -> ok
-%% @doc Run tests for mochiweb_cookies.
-test() ->
- parse_cookie_test(),
- cookie_test(),
- ok.
-
%% Internal API
parse_cookie([], Acc) ->
- lists:reverse(Acc);
-parse_cookie(String, Acc) ->
+ lists:reverse(Acc);
+parse_cookie(String, Acc) ->
{{Token, Value}, Rest} = read_pair(String),
Acc1 = case Token of
"" ->
@@ -180,7 +173,7 @@ read_quoted([$\\, Any | Rest], Acc) ->
read_quoted(Rest, [Any | Acc]);
read_quoted([C | Rest], Acc) ->
read_quoted(Rest, [C | Acc]).
-
+
skip_whitespace(String) ->
F = fun (C) -> ?IS_WHITESPACE(C) end,
lists:dropwhile(F, String).
@@ -189,7 +182,7 @@ read_token(String) ->
F = fun (C) -> not ?IS_SEPARATOR(C) end,
lists:splitwith(F, String).
-skip_past_separator([]) ->
+skip_past_separator([]) ->
[];
skip_past_separator([$; | Rest]) ->
Rest;
@@ -198,24 +191,6 @@ skip_past_separator([$, | Rest]) ->
skip_past_separator([_ | Rest]) ->
skip_past_separator(Rest).
-parse_cookie_test() ->
- %% RFC example
- C1 = "$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\";
- Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\";
- Shipping=\"FedEx\"; $Path=\"/acme\"",
- [
- {"Customer","WILE_E_COYOTE"},
- {"Part_Number","Rocket_Launcher_0001"},
- {"Shipping","FedEx"}
- ] = parse_cookie(C1),
- %% Potential edge cases
- [{"foo", "x"}] = parse_cookie("foo=\"\\x\""),
- [] = parse_cookie("="),
- [{"foo", ""}, {"bar", ""}] = parse_cookie(" foo ; bar "),
- [{"foo", ""}, {"bar", ""}] = parse_cookie("foo=;bar="),
- [{"foo", "\";"}, {"bar", ""}] = parse_cookie("foo = \"\\\";\";bar "),
- [{"foo", "\";bar"}] = parse_cookie("foo=\"\\\";bar").
-
any_to_list(V) when is_list(V) ->
V;
any_to_list(V) when is_atom(V) ->
@@ -225,6 +200,81 @@ any_to_list(V) when is_binary(V) ->
any_to_list(V) when is_integer(V) ->
integer_to_list(V).
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
+quote_test() ->
+ %% ?assertError eunit macro is not compatible with coverage module
+ try quote(":wq")
+ catch error:{cookie_quoting_required, ":wq"} -> ok
+ end,
+ ?assertEqual(
+ "foo",
+ quote(foo)),
+ ok.
+
+parse_cookie_test() ->
+ %% RFC example
+ C1 = "$Version=\"1\"; Customer=\"WILE_E_COYOTE\"; $Path=\"/acme\";
+ Part_Number=\"Rocket_Launcher_0001\"; $Path=\"/acme\";
+ Shipping=\"FedEx\"; $Path=\"/acme\"",
+ ?assertEqual(
+ [{"Customer","WILE_E_COYOTE"},
+ {"Part_Number","Rocket_Launcher_0001"},
+ {"Shipping","FedEx"}],
+ parse_cookie(C1)),
+ %% Potential edge cases
+ ?assertEqual(
+ [{"foo", "x"}],
+ parse_cookie("foo=\"\\x\"")),
+ ?assertEqual(
+ [],
+ parse_cookie("=")),
+ ?assertEqual(
+ [{"foo", ""}, {"bar", ""}],
+ parse_cookie(" foo ; bar ")),
+ ?assertEqual(
+ [{"foo", ""}, {"bar", ""}],
+ parse_cookie("foo=;bar=")),
+ ?assertEqual(
+ [{"foo", "\";"}, {"bar", ""}],
+ parse_cookie("foo = \"\\\";\";bar ")),
+ ?assertEqual(
+ [{"foo", "\";bar"}],
+ parse_cookie("foo=\"\\\";bar")),
+ ?assertEqual(
+ [],
+ parse_cookie([])),
+ ?assertEqual(
+ [{"foo", "bar"}, {"baz", "wibble"}],
+ parse_cookie("foo=bar , baz=wibble ")),
+ ok.
+
+domain_test() ->
+ ?assertEqual(
+ {"Set-Cookie",
+ "Customer=WILE_E_COYOTE; "
+ "Version=1; "
+ "Domain=acme.com; "
+ "HttpOnly"},
+ cookie("Customer", "WILE_E_COYOTE",
+ [{http_only, true}, {domain, "acme.com"}])),
+ ok.
+
+local_time_test() ->
+ {"Set-Cookie", S} = cookie("Customer", "WILE_E_COYOTE",
+ [{max_age, 111}, {secure, true}]),
+ ?assertMatch(
+ ["Customer=WILE_E_COYOTE",
+ " Version=1",
+ " Expires=" ++ _,
+ " Max-Age=111",
+ " Secure"],
+ string:tokens(S, ";")),
+ ok.
cookie_test() ->
C1 = {"Set-Cookie",
@@ -238,8 +288,8 @@ cookie_test() ->
C1 = cookie(<<"Customer">>, <<"WILE_E_COYOTE">>, [{path, <<"/acme">>}]),
{"Set-Cookie","=NoKey; Version=1"} = cookie("", "NoKey", []),
-
- LocalTime = calendar:universal_time_to_local_time({{2007, 5, 15}, {13, 45, 33}}),
+ {"Set-Cookie","=NoKey; Version=1"} = cookie("", "NoKey"),
+ LocalTime = calendar:universal_time_to_local_time({{2007, 5, 15}, {13, 45, 33}}),
C2 = {"Set-Cookie",
"Customer=WILE_E_COYOTE; "
"Version=1; "
@@ -255,3 +305,5 @@ cookie_test() ->
C3 = cookie("Customer", "WILE_E_COYOTE",
[{max_age, 86417}, {local_time, LocalTime}]),
ok.
+
+-endif.
View
75 src/mochiweb_cover.erl
@@ -0,0 +1,75 @@
+%% @author Bob Ippolito <bob@mochimedia.com>
+%% @copyright 2010 Mochi Media, Inc.
+
+%% @doc Workarounds for various cover deficiencies.
+-module(mochiweb_cover).
+-export([get_beam/1, get_abstract_code/1,
+ get_clauses/2, clause_lookup_table/1]).
+-export([clause_lookup_table/2]).
+
+%% Internal
+
+get_beam(Module) ->
+ {Module, Beam, _Path} = code:get_object_code(Module),
+ Beam.
+
+get_abstract_code(Beam) ->
+ {ok, {_Module,
+ [{abstract_code,
+ {raw_abstract_v1, L}}]}} = beam_lib:chunks(Beam, [abstract_code]),
+ L.
+
+get_clauses(Function, Code) ->
+ [L] = [Clauses || {function, _, FName, _, Clauses}
+ <- Code, FName =:= Function],
+ L.
+
+clause_lookup_table(Module, Function) ->
+ clause_lookup_table(
+ get_clauses(Function,
+ get_abstract_code(get_beam(Module)))).
+
+clause_lookup_table(Clauses) ->
+ lists:foldr(fun clause_fold/2, [], Clauses).
+
+clause_fold({clause, _,
+ [InTerm],
+ _Guards=[],
+ [OutTerm]},
+ Acc) ->
+ try [{erl_parse:normalise(InTerm), erl_parse:normalise(OutTerm)} | Acc]
+ catch error:_ -> Acc
+ end;
+clause_fold(_, Acc) ->
+ Acc.
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+foo_table(a) -> b;
+foo_table("a") -> <<"b">>;
+foo_table(123) -> {4, 3, 2};
+foo_table([list]) -> [];
+foo_table([list1, list2]) -> [list1, list2, list3];
+foo_table(ignored) -> some, code, ignored;
+foo_table(Var) -> Var.
+
+foo_table_test() ->
+ T = clause_lookup_table(?MODULE, foo_table),
+ [?assertEqual(V, foo_table(K)) || {K, V} <- T].
+
+clause_lookup_table_test() ->
+ ?assertEqual(b, foo_table(a)),
+ ?assertEqual(ignored, foo_table(ignored)),
+ ?assertEqual('Var', foo_table('Var')),
+ ?assertEqual(
+ [{a, b},
+ {"a", <<"b">>},
+ {123, {4, 3, 2}},
+ {[list], []},
+ {[list1, list2], [list1, list2, list3]}],
+ clause_lookup_table(?MODULE, foo_table)).
+
+-endif.
View
9 src/mochiweb_echo.erl
@@ -9,7 +9,7 @@
stop() ->
mochiweb_socket_server:stop(?MODULE).
-
+
start() ->
mochiweb_socket_server:start([{name, ?MODULE},
{port, 6789},
@@ -29,3 +29,10 @@ loop(Socket) ->
_Other ->
exit(normal)
end.
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.
View
165 src/mochiweb_headers.erl
@@ -9,36 +9,12 @@
-export([delete_any/2, get_primary_value/2]).
-export([default/3, enter_from_list/2, default_from_list/2]).
-export([to_list/1, make/1]).
--export([test/0]).
+-export([from_binary/1]).
%% @type headers().
%% @type key() = atom() | binary() | string().
%% @type value() = atom() | binary() | string() | integer().
-%% @spec test() -> ok
-%% @doc Run tests for this module.
-test() ->
- H = ?MODULE:make([{hdr, foo}, {"Hdr", "bar"}, {'Hdr', 2}]),
- [{hdr, "foo, bar, 2"}] = ?MODULE:to_list(H),
- H1 = ?MODULE:insert(taco, grande, H),
- [{hdr, "foo, bar, 2"}, {taco, "grande"}] = ?MODULE:to_list(H1),
- H2 = ?MODULE:make([{"Set-Cookie", "foo"}]),
- [{"Set-Cookie", "foo"}] = ?MODULE:to_list(H2),
- H3 = ?MODULE:insert("Set-Cookie", "bar", H2),
- [{"Set-Cookie", "foo"}, {"Set-Cookie", "bar"}] = ?MODULE:to_list(H3),
- "foo, bar" = ?MODULE:get_value("set-cookie", H3),
- {value, {"Set-Cookie", "foo, bar"}} = ?MODULE:lookup("set-cookie", H3),
- undefined = ?MODULE:get_value("shibby", H3),
- none = ?MODULE:lookup("shibby", H3),
- H4 = ?MODULE:insert("content-type",
- "application/x-www-form-urlencoded; charset=utf8",
- H3),
- "application/x-www-form-urlencoded" = ?MODULE:get_primary_value(
- "content-type", H4),
- H4 = ?MODULE:delete_any("nonexistent-header", H4),
- H3 = ?MODULE:delete_any("content-type", H4),
- ok.
-
%% @spec empty() -> headers()
%% @doc Create an empty headers structure.
empty() ->
@@ -52,6 +28,39 @@ make(L) when is_list(L) ->
make(T) when is_tuple(T) ->
T.
+%% @spec from_binary(iolist()) -> headers()
+%% @doc Transforms a raw HTTP header into a mochiweb headers structure.
+%%
+%% The given raw HTTP header can be one of the following:
+%%
+%% 1) A string or a binary representing a full HTTP header ending with
+%% double CRLF.
+%% Examples:
+%% ```
+%% "Content-Length: 47\r\nContent-Type: text/plain\r\n\r\n"
+%% <<"Content-Length: 47\r\nContent-Type: text/plain\r\n\r\n">>'''
+%%
+%% 2) A list of binaries or strings where each element represents a raw
+%% HTTP header line ending with a single CRLF.
+%% Examples:
+%% ```
+%% [<<"Content-Length: 47\r\n">>, <<"Content-Type: text/plain\r\n">>]
+%% ["Content-Length: 47\r\n", "Content-Type: text/plain\r\n"]
+%% ["Content-Length: 47\r\n", <<"Content-Type: text/plain\r\n">>]'''
+%%
+from_binary(RawHttpHeader) when is_binary(RawHttpHeader) ->
+ from_binary(RawHttpHeader, []);
+from_binary(RawHttpHeaderList) ->
+ from_binary(list_to_binary([RawHttpHeaderList, "\r\n"])).
+
+from_binary(RawHttpHeader, Acc) ->
+ case erlang:decode_packet(httph, RawHttpHeader, []) of
+ {ok, {http_header, _, H, _, V}, Rest} ->
+ from_binary(Rest, [{H, V} | Acc]);
+ _ ->
+ make(Acc)
+ end.
+
%% @spec from_list([{key(), value()}]) -> headers()
%% @doc Construct a headers() from the given list.
from_list(List) ->
@@ -69,7 +78,7 @@ default_from_list(List, T) ->
%% @spec to_list(headers()) -> [{key(), string()}]
%% @doc Return the contents of the headers. The keys will be the exact key
-%% that was first inserted (e.g. may be an atom or binary, case is
+%% that was first inserted (e.g. may be an atom or binary, case is
%% preserved).
to_list(T) ->
F = fun ({K, {array, L}}, Acc) ->
@@ -183,4 +192,108 @@ any_to_list(V) when is_binary(V) ->
any_to_list(V) when is_integer(V) ->
integer_to_list(V).
+%%
+%% Tests.
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
+make_test() ->
+ Identity = make([{hdr, foo}]),
+ ?assertEqual(
+ Identity,
+ make(Identity)).
+
+enter_from_list_test() ->
+ H = make([{hdr, foo}]),
+ ?assertEqual(
+ [{baz, "wibble"}, {hdr, "foo"}],
+ to_list(enter_from_list([{baz, wibble}], H))),
+ ?assertEqual(
+ [{hdr, "bar"}],
+ to_list(enter_from_list([{hdr, bar}], H))),
+ ok.
+
+default_from_list_test() ->
+ H = make([{hdr, foo}]),
+ ?assertEqual(
+ [{baz, "wibble"}, {hdr, "foo"}],
+ to_list(default_from_list([{baz, wibble}], H))),
+ ?assertEqual(
+ [{hdr, "foo"}],
+ to_list(default_from_list([{hdr, bar}], H))),
+ ok.
+
+get_primary_value_test() ->
+ H = make([{hdr, foo}, {baz, <<"wibble;taco">>}]),
+ ?assertEqual(
+ "foo",
+ get_primary_value(hdr, H)),
+ ?assertEqual(
+ undefined,
+ get_primary_value(bar, H)),
+ ?assertEqual(
+ "wibble",
+ get_primary_value(<<"baz">>, H)),
+ ok.
+
+set_cookie_test() ->
+ H = make([{"set-cookie", foo}, {"set-cookie", bar}, {"set-cookie", baz}]),
+ ?assertEqual(
+ [{"set-cookie", "foo"}, {"set-cookie", "bar"}, {"set-cookie", "baz"}],
+ to_list(H)),
+ ok.
+
+headers_test() ->
+ H = ?MODULE:make([{hdr, foo}, {"Hdr", "bar"}, {'Hdr', 2}]),
+ [{hdr, "foo, bar, 2"}] = ?MODULE:to_list(H),
+ H1 = ?MODULE:insert(taco, grande, H),
+ [{hdr, "foo, bar, 2"}, {taco, "grande"}] = ?MODULE:to_list(H1),
+ H2 = ?MODULE:make([{"Set-Cookie", "foo"}]),
+ [{"Set-Cookie", "foo"}] = ?MODULE:to_list(H2),
+ H3 = ?MODULE:insert("Set-Cookie", "bar", H2),
+ [{"Set-Cookie", "foo"}, {"Set-Cookie", "bar"}] = ?MODULE:to_list(H3),
+ "foo, bar" = ?MODULE:get_value("set-cookie", H3),
+ {value, {"Set-Cookie", "foo, bar"}} = ?MODULE:lookup("set-cookie", H3),
+ undefined = ?MODULE:get_value("shibby", H3),
+ none = ?MODULE:lookup("shibby", H3),
+ H4 = ?MODULE:insert("content-type",
+ "application/x-www-form-urlencoded; charset=utf8",
+ H3),
+ "application/x-www-form-urlencoded" = ?MODULE:get_primary_value(
+ "content-type", H4),
+ H4 = ?MODULE:delete_any("nonexistent-header", H4),
+ H3 = ?MODULE:delete_any("content-type", H4),
+ HB = <<"Content-Length: 47\r\nContent-Type: text/plain\r\n\r\n">>,
+ H_HB = ?MODULE:from_binary(HB),
+ H_HB = ?MODULE:from_binary(binary_to_list(HB)),
+ "47" = ?MODULE:get_value("Content-Length", H_HB),
+ "text/plain" = ?MODULE:get_value("Content-Type", H_HB),
+ L_H_HB = ?MODULE:to_list(H_HB),
+ 2 = length(L_H_HB),
+ true = lists:member({'Content-Length', "47"}, L_H_HB),
+ true = lists:member({'Content-Type', "text/plain"}, L_H_HB),
+ HL = [ <<"Content-Length: 47\r\n">>, <<"Content-Type: text/plain\r\n">> ],
+ HL2 = [ "Content-Length: 47\r\n", <<"Content-Type: text/plain\r\n">> ],
+ HL3 = [ <<"Content-Length: 47\r\n">>, "Content-Type: text/plain\r\n" ],
+ H_HL = ?MODULE:from_binary(HL),
+ H_HL = ?MODULE:from_binary(HL2),
+ H_HL = ?MODULE:from_binary(HL3),
+ "47" = ?MODULE:get_value("Content-Length", H_HL),
+ "text/plain" = ?MODULE:get_value("Content-Type", H_HL),
+ L_H_HL = ?MODULE:to_list(H_HL),
+ 2 = length(L_H_HL),
+ true = lists:member({'Content-Length', "47"}, L_H_HL),
+ true = lists:member({'Content-Type', "text/plain"}, L_H_HL),
+ [] = ?MODULE:to_list(?MODULE:from_binary(<<>>)),
+ [] = ?MODULE:to_list(?MODULE:from_binary(<<"">>)),
+ [] = ?MODULE:to_list(?MODULE:from_binary(<<"\r\n">>)),
+ [] = ?MODULE:to_list(?MODULE:from_binary(<<"\r\n\r\n">>)),
+ [] = ?MODULE:to_list(?MODULE:from_binary("")),
+ [] = ?MODULE:to_list(?MODULE:from_binary([<<>>])),
+ [] = ?MODULE:to_list(?MODULE:from_binary([<<"">>])),
+ [] = ?MODULE:to_list(?MODULE:from_binary([<<"\r\n">>])),
+ [] = ?MODULE:to_list(?MODULE:from_binary([<<"\r\n\r\n">>])),
+ ok.
+-endif.
View
486 src/mochiweb_html.erl
@@ -4,9 +4,9 @@
%% @doc Loosely tokenizes and generates parse trees for HTML 4.
-module(mochiweb_html).
-export([tokens/1, parse/1, parse_tokens/1, to_tokens/1, escape/1,
- escape_attr/1, to_html/1, test/0]).
+ escape_attr/1, to_html/1]).
-% This is a macro to placate syntax highlighters..
+%% This is a macro to placate syntax highlighters..
-define(QUOTE, $\").
-define(SQUOTE, $\').
-define(ADV_COL(S, N),
@@ -124,40 +124,6 @@ escape_attr(I) when is_integer(I) ->
escape_attr(F) when is_float(F) ->
escape_attr(mochinum:digits(F), []).
-%% @spec test() -> ok
-%% @doc Run tests for mochiweb_html.
-test() ->
- test_destack(),
- test_tokens(),
- test_tokens2(),
- test_parse(),
- test_parse2(),
- test_parse_tokens(),
- test_escape(),
- test_escape_attr(),
- test_to_html(),
- ok.
-
-
-%% Internal API
-
-test_to_html() ->
- Expect = <<"<html><head><title>hey!</title></head><body><p class=\"foo\">what's up<br /></p><div>sucka</div><!-- comment! --></body></html>">>,
- Expect = iolist_to_binary(
- to_html({html, [],
- [{<<"head">>, [],
- [{title, <<"hey!">>}]},
- {body, [],
- [{p, [{class, foo}], [<<"what's">>, <<" up">>, {br}]},
- {'div', <<"sucka">>},
- {comment, <<" comment! ">>}]}]})),
- Expect1 = <<"<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">">>,
- Expect1 = iolist_to_binary(
- to_html({doctype,
- [<<"html">>, <<"PUBLIC">>,
- <<"-//W3C//DTD XHTML 1.0 Transitional//EN">>,
- <<"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">>]})),
- ok.
to_html([], Acc) ->
lists:reverse(Acc);
to_html([{'=', Content} | Rest], Acc) ->
@@ -205,16 +171,6 @@ attrs_to_html([{K, V} | Rest], Acc) ->
[[<<" ">>, escape(K), <<"=\"">>,
escape_attr(V), <<"\"">>] | Acc]).
-test_escape() ->
- <<"&amp;quot;\"word &lt;&lt;up!&amp;quot;">> =
- escape(<<"&quot;\"word <<up!&quot;">>),
- ok.
-
-test_escape_attr() ->
- <<"&amp;quot;&quot;word &lt;&lt;up!&amp;quot;">> =
- escape_attr(<<"&quot;\"word <<up!&quot;">>),
- ok.
-
escape([], Acc) ->
list_to_binary(lists:reverse(Acc));
escape("<" ++ Rest, Acc) ->
@@ -290,39 +246,6 @@ to_tokens([{Tag0, [B | R1]} | Rest], Acc) when is_binary(B) ->
Tag = to_tag(Tag0),
to_tokens([{Tag, R1} | Rest], [{data, B, false} | Acc]).
-test_tokens() ->
- [{start_tag, <<"foo">>, [{<<"bar">>, <<"baz">>},
- {<<"wibble">>, <<"wibble">>},
- {<<"alice">>, <<"bob">>}], true}] =
- tokens(<<"<foo bar=baz wibble='wibble' alice=\"bob\"/>">>),
- [{start_tag, <<"foo">>, [{<<"bar">>, <<"baz">>},
- {<<"wibble">>, <<"wibble">>},
- {<<"alice">>, <<"bob">>}], true}] =
- tokens(<<"<foo bar=baz wibble='wibble' alice=bob/>">>),
- [{comment, <<"[if lt IE 7]>\n<style type=\"text/css\">\n.no_ie { display: none; }\n</style>\n<![endif]">>}] =
- tokens(<<"<!--[if lt IE 7]>\n<style type=\"text/css\">\n.no_ie { display: none; }\n</style>\n<![endif]-->">>),
- [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false},
- {data, <<" A= B <= C ">>, false},
- {end_tag, <<"script">>}] =
- tokens(<<"<script type=\"text/javascript\"> A= B <= C </script>">>),
- [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false},
- {data, <<" A= B <= C ">>, false},
- {end_tag, <<"script">>}] =
- tokens(<<"<script type =\"text/javascript\"> A= B <= C </script>">>),
- [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false},
- {data, <<" A= B <= C ">>, false},
- {end_tag, <<"script">>}] =
- tokens(<<"<script type = \"text/javascript\"> A= B <= C </script>">>),
- [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false},
- {data, <<" A= B <= C ">>, false},
- {end_tag, <<"script">>}] =
- tokens(<<"<script type= \"text/javascript\"> A= B <= C </script>">>),
- [{start_tag, <<"textarea">>, [], false},
- {data, <<"<html></body>">>, false},
- {end_tag, <<"textarea">>}] =
- tokens(<<"<textarea><html></body></textarea>">>),
- ok.
-
tokens(B, S=#decoder{offset=O}, Acc) ->
case B of
<<_:O/binary>> ->
@@ -385,149 +308,6 @@ tokenize(B, S=#decoder{offset=O}) ->
tokenize_data(B, S)
end.
-test_parse() ->
- D0 = <<"<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">
-<html>
- <head>
- <meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">
- <title>Foo</title>
- <link rel=\"stylesheet\" type=\"text/css\" href=\"/static/rel/dojo/resources/dojo.css\" media=\"screen\">
- <link rel=\"stylesheet\" type=\"text/css\" href=\"/static/foo.css\" media=\"screen\">
- <!--[if lt IE 7]>
- <style type=\"text/css\">
- .no_ie { display: none; }
- </style>
- <![endif]-->
- <link rel=\"icon\" href=\"/static/images/favicon.ico\" type=\"image/x-icon\">
- <link rel=\"shortcut icon\" href=\"/static/images/favicon.ico\" type=\"image/x-icon\">
- </head>
- <body id=\"home\" class=\"tundra\"><![CDATA[&lt;<this<!-- is -->CDATA>&gt;]]></body>
-</html>">>,
- Expect = {<<"html">>, [],
- [{<<"head">>, [],
- [{<<"meta">>,
- [{<<"http-equiv">>,<<"Content-Type">>},
- {<<"content">>,<<"text/html; charset=UTF-8">>}],
- []},
- {<<"title">>,[],[<<"Foo">>]},
- {<<"link">>,
- [{<<"rel">>,<<"stylesheet">>},
- {<<"type">>,<<"text/css">>},
- {<<"href">>,<<"/static/rel/dojo/resources/dojo.css">>},
- {<<"media">>,<<"screen">>}],
- []},
- {<<"link">>,
- [{<<"rel">>,<<"stylesheet">>},
- {<<"type">>,<<"text/css">>},
- {<<"href">>,<<"/static/foo.css">>},
- {<<"media">>,<<"screen">>}],
- []},
- {comment,<<"[if lt IE 7]>\n <style type=\"text/css\">\n .no_ie { display: none; }\n </style>\n <![endif]">>},
- {<<"link">>,
- [{<<"rel">>,<<"icon">>},
- {<<"href">>,<<"/static/images/favicon.ico">>},
- {<<"type">>,<<"image/x-icon">>}],
- []},
- {<<"link">>,
- [{<<"rel">>,<<"shortcut icon">>},
- {<<"href">>,<<"/static/images/favicon.ico">>},
- {<<"type">>,<<"image/x-icon">>}],
- []}]},
- {<<"body">>,
- [{<<"id">>,<<"home">>},
- {<<"class">>,<<"tundra">>}],
- [<<"&lt;<this<!-- is -->CDATA>&gt;">>]}]},
- Expect = parse(D0),
- ok.
-
-test_tokens2() ->
- D0 = <<"<channel><title>from __future__ import *</title><link>http://bob.pythonmac.org</link><description>Bob's Rants</description></channel>">>,
- Expect = [{start_tag,<<"channel">>,[],false},
- {start_tag,<<"title">>,[],false},
- {data,<<"from __future__ import *">>,false},
- {end_tag,<<"title">>},
- {start_tag,<<"link">>,[],true},
- {data,<<"http://bob.pythonmac.org">>,false},
- {end_tag,<<"link">>},
- {start_tag,<<"description">>,[],false},
- {data,<<"Bob's Rants">>,false},
- {end_tag,<<"description">>},
- {end_tag,<<"channel">>}],
- Expect = tokens(D0),
- ok.
-
-test_parse2() ->
- D0 = <<"<channel><title>from __future__ import *</title><link>http://bob.pythonmac.org<br>foo</link><description>Bob's Rants</description></channel>">>,
- Expect = {<<"channel">>,[],
- [{<<"title">>,[],[<<"from __future__ import *">>]},
- {<<"link">>,[],[
- <<"http://bob.pythonmac.org">>,
- {<<"br">>,[],[]},
- <<"foo">>]},
- {<<"description">>,[],[<<"Bob's Rants">>]}]},
- Expect = parse(D0),
- ok.
-
-test_parse_tokens() ->