erlydtl_filters.erl 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302
  1. %%%-------------------------------------------------------------------
  2. %%% File: erlydtl_filters.erl
  3. %%% @author Roberto Saccon <rsaccon@gmail.com> [http://rsaccon.com]
  4. %%% @author Evan Miller <emmiller@gmail.com>
  5. %%% @copyright 2008 Roberto Saccon, Evan Miller
  6. %%% @doc
  7. %%% Template filters
  8. %%% @end
  9. %%%
  10. %%% The MIT License
  11. %%%
  12. %%% Copyright (c) 2007 Roberto Saccon, Evan Miller
  13. %%%
  14. %%% Permission is hereby granted, free of charge, to any person obtaining a copy
  15. %%% of this software and associated documentation files (the "Software"), to deal
  16. %%% in the Software without restriction, including without limitation the rights
  17. %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  18. %%% copies of the Software, and to permit persons to whom the Software is
  19. %%% furnished to do so, subject to the following conditions:
  20. %%%
  21. %%% The above copyright notice and this permission notice shall be included in
  22. %%% all copies or substantial portions of the Software.
  23. %%%
  24. %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  25. %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  26. %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  27. %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  28. %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  29. %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  30. %%% THE SOFTWARE.
  31. %%%
  32. %%% @since 2007-11-11 by Roberto Saccon, Evan Miller
  33. %%%-------------------------------------------------------------------
  34. -module(erlydtl_filters).
  35. -author('rsaccon@gmail.com').
  36. -author('emmiller@gmail.com').
  37. -author('drew dot gulino at google dot com').
  38. -ifdef(TEST).
  39. -undef(TEST).
  40. -endif.
  41. -define(TEST,"").
  42. %-define(NOTEST,1).
  43. -define(NODEBUG,1).
  44. -include_lib("eunit/include/eunit.hrl").
  45. -ifdef(TEST).
  46. -export([cast_to_float/1,cast_to_integer/1,stringformat_io/7,round/2,unjoin/2,addDefaultURI/1]).
  47. -endif.
  48. -export([add/2,
  49. addslashes/1,
  50. capfirst/1,
  51. center/2,
  52. cut/2,
  53. date/1,
  54. date/2,
  55. default/2,
  56. default_if_none/2,
  57. dictsort/2,
  58. dictsortreversed/2,
  59. divisibleby/2,
  60. %escape/, - implemented in erlydtl_compiler
  61. escapejs/1,
  62. filesizeformat/1,
  63. first/1,
  64. fix_ampersands/1,
  65. floatformat/1,
  66. floatformat/2,
  67. force_escape/1,
  68. format_integer/1,
  69. format_number/1,
  70. get_digit/2,
  71. iriencode/1,
  72. join/2,
  73. last/1,
  74. length/1,
  75. length_is/2,
  76. linebreaks/1,
  77. linebreaksbr/1,
  78. linenumbers/1,
  79. ljust/2,
  80. lower/1,
  81. make_list/1,
  82. phone2numeric/1,
  83. pluralize/1,
  84. pluralize/2,
  85. pprint/1,
  86. random/1,
  87. random_num/1,
  88. random_range/1,
  89. removetags/2,
  90. rjust/2,
  91. %safe/, - implemented in erlydtl_compiler
  92. %safeseq/, - implemented in erlydtl_compiler
  93. slice/2,
  94. slugify/1,
  95. stringformat/2,
  96. striptags/1,
  97. time/1,
  98. time/2,
  99. timesince/1,
  100. timesince/2,
  101. timeuntil/1,
  102. timeuntil/2,
  103. title/1,
  104. truncatechars/2,
  105. truncatewords/2,
  106. truncatewords_html/2,
  107. unordered_list/1,
  108. upper/1,
  109. urlencode/1,
  110. urlencode/2,
  111. urlize/1,
  112. urlize/2,
  113. urlizetrunc/2,
  114. wordcount/1,
  115. wordwrap/2,
  116. yesno/2]).
  117. -define(NO_ENCODE(C), ((C >= $a andalso C =< $z) orelse
  118. (C >= $A andalso C =< $Z) orelse
  119. (C >= $0 andalso C =< $9) orelse
  120. (C =:= $\. orelse C =:= $-
  121. orelse C =:= $~ orelse C =:= $_))).
  122. -define(NO_IRI_ENCODE(C), (?NO_ENCODE(C) orelse (
  123. C =:= $/ orelse
  124. C =:= $# orelse
  125. C =:= $[ orelse
  126. C =:= $] orelse
  127. C =:= $= orelse
  128. C =:= $: orelse
  129. C =:= $; orelse
  130. C =:= $$ orelse
  131. C =:= $& orelse
  132. C =:= $( orelse
  133. C =:= $) orelse
  134. C =:= $+ orelse
  135. C =:= $, orelse
  136. C =:= $! orelse
  137. C =:= $? orelse
  138. C =:= $* orelse
  139. C =:= $@ orelse
  140. C =:= $' orelse
  141. C =:= $~))).
  142. -define(KILOBYTE, 1024).
  143. -define(MEGABYTE, (1024 * ?KILOBYTE)).
  144. -define(GIGABYTE, (1024 * ?MEGABYTE)).
  145. -define(SECONDS_PER_MINUTE, 60).
  146. -define(SECONDS_PER_HOUR, (60 * ?SECONDS_PER_MINUTE)).
  147. -define(SECONDS_PER_DAY, (24 * ?SECONDS_PER_HOUR)).
  148. -define(SECONDS_PER_WEEK, (7 * ?SECONDS_PER_DAY)).
  149. -define(SECONDS_PER_MONTH, (30 * ?SECONDS_PER_DAY)).
  150. -define(SECONDS_PER_YEAR, (365 * ?SECONDS_PER_DAY)).
  151. %% @doc Adds to values
  152. add(LHS, RHS) when is_number(LHS), is_number(RHS) ->
  153. LHS + RHS;
  154. add(LHS, RHS) when is_binary(LHS) ->
  155. add(unicode:characters_to_list(LHS), RHS);
  156. add(LHS, RHS) when is_binary(RHS) ->
  157. add(LHS, unicode:characters_to_list(RHS));
  158. add(LHS, RHS) when is_list(LHS), is_list(RHS) ->
  159. case {to_numeric(LHS), to_numeric(RHS)} of
  160. {{number, LHSNum}, {number, RHSNum}} ->
  161. LHSNum + RHSNum;
  162. _ ->
  163. LHS ++ RHS
  164. end;
  165. add(LHS, RHS) when is_list(LHS), is_number(RHS) ->
  166. case to_numeric(LHS) of
  167. {number, LHSNum} ->
  168. LHSNum + RHS;
  169. _ ->
  170. LHS ++ to_string(RHS)
  171. end;
  172. add(LHS, RHS) when is_number(LHS), is_list(RHS) ->
  173. case to_numeric(RHS) of
  174. {number, RHSNum} ->
  175. LHS + RHSNum;
  176. _ ->
  177. to_string(LHS) ++ RHS
  178. end.
  179. to_string(Num) when is_integer(Num) ->
  180. integer_to_list(Num);
  181. to_string(Num) when is_float(Num) ->
  182. float_to_list(Num).
  183. to_numeric(List) ->
  184. try
  185. {number, list_to_integer(List)}
  186. catch
  187. error:badarg ->
  188. try
  189. {number, list_to_float(List)}
  190. catch
  191. error:badarg ->
  192. undefined
  193. end
  194. end.
  195. %% @doc Adds slashes before quotes.
  196. addslashes(Input) when is_binary(Input) ->
  197. addslashes(unicode:characters_to_list(Input));
  198. addslashes(Input) when is_list(Input) ->
  199. addslashes(Input, []).
  200. %% @doc Capitalizes the first character of the value.
  201. capfirst([H|T]) when H >= $a andalso H =< $z ->
  202. [H + $A - $a | T];
  203. capfirst(Other) when is_list(Other) ->
  204. Other;
  205. capfirst(<<Byte:8/integer, Binary/binary>>) when Byte >= $a andalso Byte =< $z ->
  206. [(Byte + $A - $a)|unicode:characters_to_list(Binary)];
  207. capfirst(Other) when is_binary(Other) ->
  208. Other.
  209. %% @doc Centers the value in a field of a given width.
  210. center(Input, Number) when is_binary(Input) ->
  211. unicode:characters_to_binary(center(unicode:characters_to_list(Input), Number));
  212. center(Input, Number) when is_list(Input) ->
  213. string:centre(Input, Number).
  214. %% @doc Removes all values of arg from the given string.
  215. cut(Input, Arg) when is_binary(Arg) ->
  216. cut(Input, unicode:characters_to_list(Arg));
  217. cut(Input, Arg) when is_binary(Input) ->
  218. cut(unicode:characters_to_list(Input), Arg);
  219. cut(Input, [Char]) when is_list(Input) ->
  220. cut(Input, Char, []).
  221. %% @doc Formats a date according to the default format.
  222. date(Input) ->
  223. date(Input, "F j, Y").
  224. %% @doc Formats a date according to the given format.
  225. date(Input, FormatStr)
  226. when is_tuple(Input)
  227. andalso (size(Input) == 2 orelse size(Input) == 3) ->
  228. erlydtl_dateformat:format(Input, FormatStr);
  229. date(Input, _FormatStr) ->
  230. io:format("Unexpected date parameter: ~p~n", [Input]),
  231. "".
  232. %% @doc If value evaluates to `false', use given default. Otherwise, use the value.
  233. default(Input, Default) ->
  234. case erlydtl_runtime:is_false(Input) of
  235. true -> Default;
  236. false -> Input
  237. end.
  238. %% @doc If (and only if) value is `undefined', use given default. Otherwise, use the value.
  239. default_if_none(undefined, Default) ->
  240. Default;
  241. default_if_none(Input, _) ->
  242. Input.
  243. %% @doc Takes a list of dictionaries or proplists and returns that list sorted by the key given in the argument.
  244. dictsort(DictList, Key) when is_binary(Key) ->
  245. dictsort(DictList, [binary_to_atom(B,latin1) ||
  246. B <- binary:split(Key,<<".">>)]);
  247. dictsort(DictList, Key) ->
  248. case lists:all(
  249. fun(Dict) ->
  250. erlydtl_runtime:find_deep_value(Key, Dict) /= undefined
  251. end, DictList) of
  252. true ->
  253. lists:sort(
  254. fun(K1,K2) ->
  255. erlydtl_runtime:find_deep_value(Key,K1) =<
  256. erlydtl_runtime:find_deep_value(Key,K2)
  257. end, DictList);
  258. false -> error
  259. end.
  260. %% @doc Same as dictsort, but the list is reversed.
  261. dictsortreversed(DictList, Key) ->
  262. lists:reverse(dictsort(DictList, Key)).
  263. %% @doc Returns `true' if the value is divisible by the argument.
  264. divisibleby(Input, Divisor) when is_binary(Input) ->
  265. divisibleby(unicode:characters_to_list(Input), Divisor);
  266. divisibleby(Input, Divisor) when is_list(Input) ->
  267. divisibleby(list_to_integer(Input), Divisor);
  268. divisibleby(Input, Divisor) when is_binary(Divisor) ->
  269. divisibleby(Input, unicode:characters_to_list(Divisor));
  270. divisibleby(Input, Divisor) when is_list(Divisor) ->
  271. divisibleby(Input, list_to_integer(Divisor));
  272. divisibleby(Input, Divisor) when is_integer(Input), is_integer(Divisor) ->
  273. Input rem Divisor =:= 0.
  274. %% @doc Escapes characters for use in JavaScript strings.
  275. escapejs(Input) when is_binary(Input) ->
  276. unicode:characters_to_binary(escapejs(unicode:characters_to_list(Input)));
  277. escapejs(Input) when is_list(Input) ->
  278. escapejs(Input, []).
  279. %% @doc Format the value like a human-readable file size.
  280. filesizeformat(Input) when is_binary(Input) ->
  281. filesizeformat(unicode:characters_to_list(Input));
  282. filesizeformat(Input) when is_list(Input) ->
  283. filesizeformat(list_to_integer(Input));
  284. filesizeformat(Bytes) when is_integer(Bytes), Bytes >= ?GIGABYTE->
  285. filesizeformat(Bytes / ?GIGABYTE, "GB");
  286. filesizeformat(Bytes) when is_integer(Bytes), Bytes >= ?MEGABYTE ->
  287. filesizeformat(Bytes / ?MEGABYTE, "MB");
  288. filesizeformat(Bytes) when is_integer(Bytes), Bytes >= ?KILOBYTE ->
  289. filesizeformat(Bytes / ?KILOBYTE, "KB");
  290. filesizeformat(Bytes) when is_integer(Bytes) ->
  291. integer_to_list(Bytes) ++ " bytes".
  292. %% @doc Returns the first item in a list.
  293. first([First|_Rest]) ->
  294. [First];
  295. first(<<First, _/binary>>) ->
  296. <<First>>.
  297. %% @doc Replaces ampersands with &amp; entities.
  298. fix_ampersands(Input) when is_binary(Input) ->
  299. fix_ampersands(Input, 0);
  300. fix_ampersands(Input) when is_list(Input) ->
  301. fix_ampersands(Input, []).
  302. %% @doc When used without an argument, rounds a floating-point number to one decimal place
  303. %% -- but only if there's a decimal part to be displayed
  304. floatformat(Number) ->
  305. floatformat(Number, []).
  306. floatformat(Number, Place)
  307. when is_number(Number); is_binary(Number); is_list(Number) ->
  308. floatformat_io(cast_to_float(Number), cast_to_integer(Place));
  309. floatformat(_, _) -> "".
  310. floatformat_io(Number, []) ->
  311. floatformat_io(Number, -1);
  312. floatformat_io(Number, 0) ->
  313. hd(io_lib:format("~B", [erlang:round(Number)]));
  314. floatformat_io(Number, Precision) when Precision > 0 ->
  315. hd(io_lib:format("~.*f",[Precision, Number]));
  316. floatformat_io(Number, Precision) when Precision < 0 ->
  317. Round = erlang:round(Number),
  318. RoundPrecision = round(Number, -Precision),
  319. if RoundPrecision == Round ->
  320. floatformat_io(Round, 0);
  321. true ->
  322. floatformat_io(RoundPrecision, -Precision)
  323. end.
  324. round(Number, Precision) ->
  325. P = math:pow(10, Precision),
  326. erlang:round(Number * P) / P.
  327. %% @doc Applies HTML escaping to a string.
  328. force_escape(Input) when is_list(Input) ->
  329. escape(Input, []);
  330. force_escape(Input) when is_binary(Input) ->
  331. escape(Input, 0);
  332. force_escape(Input) ->
  333. Input.
  334. format_integer(Input) when is_integer(Input) ->
  335. integer_to_list(Input);
  336. format_integer(Input) ->
  337. Input.
  338. format_number(Input) when is_integer(Input) ->
  339. integer_to_list(Input);
  340. format_number(Input) when is_float(Input) ->
  341. io_lib:format("~p", [Input]);
  342. format_number(Input) when is_function(Input, 0) ->
  343. format_number(Input());
  344. format_number(Input) ->
  345. Input.
  346. %% @doc Given a whole number, returns the requested digit, where 1 is the right-most digit.
  347. get_digit(Input, Digit) when is_binary(Input) ->
  348. get_digit(unicode:characters_to_list(Input), Digit);
  349. get_digit(Input, Digit) when is_integer(Input) ->
  350. get_digit(integer_to_list(Input), Digit);
  351. get_digit(Input, Digit) when is_binary(Digit) ->
  352. get_digit(Input, unicode:characters_to_list(Digit));
  353. get_digit(Input, Digit) when is_list(Digit) ->
  354. get_digit(Input, list_to_integer(Digit));
  355. get_digit(Input, Digit) when Digit > erlang:length(Input) ->
  356. 0;
  357. get_digit(Input, Digit) when Digit > 0 ->
  358. lists:nth(Digit, lists:reverse(Input)) - $0;
  359. get_digit(Input, _) ->
  360. Input.
  361. iriencode(Input) ->
  362. iriencode(unicode:characters_to_list(Input), []).
  363. %% @doc Joins a list with a given separator.
  364. join(Input, Separator) when is_list(Input) ->
  365. join_io(Input, Separator).
  366. %% @doc Returns the last item in a list.
  367. last(Input) when is_binary(Input) ->
  368. case size(Input) of
  369. 0 -> Input;
  370. N ->
  371. Offset = N - 1,
  372. <<_:Offset/binary, Byte/binary>> = Input,
  373. Byte
  374. end;
  375. last(Input) when is_list(Input) ->
  376. [lists:last(Input)].
  377. %% @doc Returns the length of the value.
  378. length(Input) when is_list(Input) ->
  379. integer_to_list(erlang:length(Input));
  380. length(Input) when is_binary(Input) ->
  381. integer_to_list(size(Input)).
  382. %% @doc Returns True iff the value's length is the argument.
  383. length_is(Input, Number) when is_list(Input), is_integer(Number) ->
  384. length_is(Input, integer_to_list(Number));
  385. length_is(Input, Number) when is_list(Input), is_list(Number) ->
  386. ?MODULE:length(Input) =:= Number.
  387. %% @doc Replaces line breaks in plain text with appropriate HTML
  388. linebreaks(Input) when is_binary(Input) ->
  389. linebreaks(unicode:characters_to_list(Input),[]);
  390. linebreaks(Input) ->
  391. linebreaks(Input,[]).
  392. linebreaks([],Acc) ->
  393. "<p>" ++ lists:reverse(Acc) ++ "</p>";
  394. linebreaks([$\n|T], ">p<"++_ = Acc) ->
  395. linebreaks(T, Acc);
  396. linebreaks([$\r|T], ">p<"++_ = Acc) ->
  397. linebreaks(T, Acc);
  398. linebreaks([$\n, $\n|T],Acc) ->
  399. linebreaks(T, lists:reverse("</p><p>", Acc));
  400. linebreaks([$\r, $\n, $\r, $\n|T],Acc) ->
  401. linebreaks(T, lists:reverse("</p><p>", Acc));
  402. linebreaks([$\r, $\n|T], Acc) ->
  403. linebreaks(T, lists:reverse("<br />", Acc));
  404. linebreaks([$\n|T], Acc) ->
  405. linebreaks(T, lists:reverse("<br />", Acc));
  406. linebreaks([C|T], Acc) ->
  407. linebreaks(T, [C|Acc]).
  408. %% @doc Converts all newlines to HTML line breaks.
  409. linebreaksbr(Input) when is_binary(Input) ->
  410. linebreaksbr(Input, 0);
  411. linebreaksbr(Input) ->
  412. linebreaksbr(Input, []).
  413. %% @doc Displays text with line numbers.
  414. linenumbers(Input) when is_binary(Input) ->
  415. linenumbers(unicode:characters_to_list(Input));
  416. linenumbers(Input) when is_list(Input) ->
  417. linenumbers_io(Input, [], 1).
  418. linenumbers_io([], Acc, _) ->
  419. lists:reverse(Acc);
  420. linenumbers_io(Input, [], LineNumber) ->
  421. linenumbers_io(Input, lists:reverse(integer_to_list(LineNumber)++". "), LineNumber + 1);
  422. linenumbers_io("\n"++Rest, Acc, LineNumber) ->
  423. linenumbers_io(Rest, lists:reverse("\n" ++ integer_to_list(LineNumber) ++ ". ", Acc), LineNumber + 1);
  424. linenumbers_io([H|T], Acc, LineNumber) ->
  425. linenumbers_io(T, [H|Acc], LineNumber).
  426. %% @doc Left-aligns the value in a field of a given width.
  427. ljust(Input, Number) when is_binary(Input) ->
  428. unicode:characters_to_binary(ljust(unicode:characters_to_list(Input), Number));
  429. ljust(Input, Number) when is_list(Input) ->
  430. string:left(Input, Number).
  431. %% @doc Converts a string into all lowercase.
  432. lower(Input) when is_binary(Input) ->
  433. lower(Input, 0);
  434. lower(Input) ->
  435. string:to_lower(Input).
  436. %% @doc Returns the value turned into a list. For an integer, it's a list of digits.
  437. %% For a string, it's a list of characters.
  438. %% Added this for DTL compatibility, but since strings are lists in Erlang, no need for this.
  439. make_list(Input) when is_binary(Input) ->
  440. make_list(unicode:characters_to_list(Input));
  441. make_list(Input) ->
  442. unjoin(Input,"").
  443. %% @doc Converts a phone number (possibly containing letters) to its numerical equivalent.
  444. phone2numeric(Input) when is_binary(Input) ->
  445. phone2numeric(unicode:characters_to_list(Input));
  446. phone2numeric(Input) when is_list(Input) ->
  447. phone2numeric(Input, []).
  448. %% @doc Returns a plural suffix if the value is not 1. By default, this suffix is 's'.
  449. pluralize(Number, Suffix) when is_binary(Suffix) ->
  450. pluralize_io(Number, unicode:characters_to_list(Suffix) );
  451. pluralize(Number, Suffix) when is_list(Suffix) ->
  452. pluralize_io(Number, Suffix).
  453. pluralize(Number) ->
  454. pluralize(Number, "s").
  455. pluralize_io(Number, Suffix) ->
  456. [Singular, Plural] =
  457. case string:tokens(Suffix,",") of
  458. [P] -> ["", P];
  459. [S, P|_] -> [S, P]
  460. end,
  461. if Number == 1; Number == "1"; Number == <<"1">> -> Singular;
  462. true -> Plural
  463. end.
  464. %% @doc "pretty print" arbitrary data structures. Used for debugging.
  465. pprint(Input) ->
  466. io_lib:format("~p",[Input]).
  467. %% @doc Returns a random item from the given list.
  468. random(Input) when is_list(Input) ->
  469. lists:nth(random:uniform(erlang:length(Input)), Input);
  470. random(_) ->
  471. "".
  472. random_num(Value) ->
  473. random:seed(now()),
  474. random:uniform(Value).
  475. %% random tags to be used when using erlydtl in testing
  476. random_range(Range) ->
  477. [Start, End] = string:tokens(Range,","),
  478. %?debugFmt("Start, End: ~p,~p~n",[Start,End]),
  479. random_range(cast_to_integer(Start),cast_to_integer(End)).
  480. random_range(Start, End) when End >= Start ->
  481. %?debugFmt("Input, Start, End: ~p,~p,~p~n",[Input,Start,End]),
  482. Range = End - Start,
  483. Rand = random:uniform(Range),
  484. Num = Rand + Start,
  485. lists:flatten(io_lib:format("~B",[Num])).
  486. removetags(Input, Tags) when is_binary(Input) ->
  487. removetags(unicode:characters_to_list(Input), Tags);
  488. removetags(Input, Tags) when is_binary(Tags) ->
  489. removetags(Input, unicode:characters_to_list(Tags));
  490. removetags(Input, Tags) ->
  491. TagList = string:tokens(Tags," "),
  492. TagListString = string:join(TagList,"|"),
  493. Regex = lists:flatten(io_lib:format("</?(~s)( |\n)?>",[TagListString])),
  494. Result = re:replace(Input,Regex,"", [global,{return,list}]),
  495. Result.
  496. %% @doc Right-aligns the value in a field of a given width.
  497. rjust(Input, Number) when is_binary(Input) ->
  498. unicode:characters_to_binary(rjust(unicode:characters_to_list(Input), Number));
  499. rjust(Input, Number) ->
  500. string:right(Input, Number).
  501. %% @doc Returns a slice of the list.
  502. slice(Input, Index) when is_binary(Input) ->
  503. erlydtl_slice:slice(unicode:characters_to_list(Input), Index);
  504. slice(Input, Index) when is_list(Input) ->
  505. erlydtl_slice:slice(Input, Index).
  506. %% regex " ^([#0-\s+].)([0-9\*]+)(\.[0-9]+)([diouxXeEfFgGcrs]) " matches ALL of "-10.0f"
  507. %% ([#0-\s+]?)([0-9\*]+)?(\.?)([0-9]?)([diouxXeEfFgGcrs])
  508. %% @doc Returns a formatted string
  509. stringformat(Input, Conversion) when is_binary(Input) ->
  510. stringformat(unicode:characters_to_list(Input), Conversion);
  511. stringformat(Input, Conversion) when is_binary(Conversion) ->
  512. stringformat(Input, unicode:characters_to_list(Conversion));
  513. stringformat(Input, Conversion) ->
  514. ParsedConversion = re:replace(Conversion, "([\-#\+ ]?)([0-9\*]+)?(\.?)([0-9]?)([diouxXeEfFgGcrs])", "\\1 ,\\2 ,\\3 ,\\4 ,\\5 ", [{return,list}]),
  515. ?debugFmt("ParsedConversion: ~p~n", [ParsedConversion]),
  516. ParsedConversion1 = lists:map(fun(X) -> string:strip(X) end, string:tokens(ParsedConversion, ",")),
  517. [ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType] = ParsedConversion1,
  518. ?debugFmt("ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType: ~p, ~p, ~p, ~p, ~p ~n", [ConversionFlag, cast_to_integer(MinFieldWidth), Precision, cast_to_integer(PrecisionLength), ConversionType]),
  519. [String] = stringformat_io(Input, Conversion, ConversionFlag, cast_to_integer(MinFieldWidth), Precision, cast_to_integer(PrecisionLength), ConversionType),
  520. lists:flatten(String).
  521. %% @doc
  522. %% A conversion specifier contains two or more characters and has the following components, which must occur in this order:
  523. %%
  524. %% 1. The "%" character, which marks the start of the specifier.
  525. %% 2. Mapping key (optional), consisting of a parenthesised sequence of characters (for example, (somename)).
  526. %% 3. Conversion flags (optional), which affect the result of some conversion types.
  527. %% 4. Minimum field width (optional). If specified as an "*" (asterisk), the actual width is read from the next element of the tuple in values, and the object to convert comes after the minimum field width and optional precision.
  528. %% 5. Precision (optional), given as a "." (dot) followed by the precision. If specified as "*" (an asterisk), the actual width is read from the next element of the tuple in values, and the value to convert comes after the precision.
  529. %% 6. Length modifier (optional).
  530. %% 7. Conversion type.
  531. stringformat_io(Input, _Conversion, _ConversionFlag, [],
  532. _Precision, _PrecisionLength, "s") when is_list(Input) ->
  533. Format = lists:flatten(io_lib:format("~~s", [])),
  534. io_lib:format(Format, [Input]);
  535. stringformat_io(Input, _Conversion, ConversionFlag, MinFieldWidth,
  536. _Precision, _PrecisionLength, "s") when is_list(Input) ->
  537. %Conversion = [ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType],
  538. InputLength = erlang:length(Input),
  539. case erlang:abs(MinFieldWidth) < InputLength of
  540. true ->
  541. MFW = InputLength;
  542. false ->
  543. MFW = MinFieldWidth
  544. end,
  545. Format = lists:flatten(io_lib:format("~~~s~ps", [ConversionFlag,MFW])),
  546. io_lib:format(Format, [Input]);
  547. stringformat_io(Input, _Conversion, _ConversionFlag, MinFieldWidth,
  548. Precision, PrecisionLength, "f") when Precision == ".", MinFieldWidth == 0 ->
  549. Conversion1 = lists:concat(["","",Precision,PrecisionLength,"f"]),
  550. stringformat_io(Input, Conversion1, [], [], Precision, PrecisionLength, "f");
  551. stringformat_io(Input, Conversion, ConversionFlag, MinFieldWidth,
  552. Precision, "", "f") when Precision == "." ->
  553. Format = re:replace(Conversion, "f", "d", [{return, list}] ),
  554. stringformat_io(Input, Format, ConversionFlag, MinFieldWidth,
  555. Precision, "", "d");
  556. stringformat_io(Input, Conversion, _ConversionFlag, _MinFieldWidth,
  557. _Precision, _PrecisionLength, "f")->
  558. %Conversion = [ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType],
  559. Format = "~" ++ Conversion,
  560. io_lib:format(Format, [cast_to_float(Input)]);
  561. stringformat_io(Input, Conversion, _ConversionFlag, _MinFieldWidth,
  562. [], [], "d")->
  563. %?debugMsg("plain d"),
  564. %Conversion = [ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType],
  565. Format = "~" ++ re:replace(Conversion, "d", "B", [{return, list}] ),
  566. io_lib:format(Format, [cast_to_integer(Input)]);
  567. stringformat_io(Input, _Conversion, "-", MinFieldWidth,
  568. _Precision, PrecisionLength, "d") when MinFieldWidth > 0 ->
  569. %Format = "~" ++ re:replace(Conversion, "d", "B", [{return, list}] ),
  570. DecimalFormat = "~" ++ integer_to_list(PrecisionLength) ++ "..0B",
  571. Decimal = lists:flatten( io_lib:format(DecimalFormat, [cast_to_integer(Input)]) ),
  572. SpaceFormat = "~" ++ integer_to_list(MinFieldWidth - erlang:length(Decimal)) ++ "s",
  573. Spaces = io_lib:format(SpaceFormat,[""]),
  574. ?debugFmt("Spaces: |~s|", [Spaces]),
  575. ?debugFmt("Decimal: ~s", [Decimal]),
  576. [lists:flatten(Decimal ++ Spaces)];
  577. stringformat_io(Input, _Conversion, _ConversionFlag, MinFieldWidth,
  578. _Precision, PrecisionLength, "d") when MinFieldWidth > 0 ->
  579. %Format = "~" ++ re:replace(Conversion, "d", "B", [{return, list}] ),
  580. DecimalFormat = "~" ++ integer_to_list(PrecisionLength) ++ "..0B",
  581. Decimal = lists:flatten( io_lib:format(DecimalFormat, [cast_to_integer(Input)]) ),
  582. SpaceFormat = "~" ++ integer_to_list(MinFieldWidth - erlang:length(Decimal)) ++ "s",
  583. Spaces = io_lib:format(SpaceFormat,[""]),
  584. ?debugFmt("Spaces: |~s|", [Spaces]),
  585. ?debugFmt("Decimal: ~s", [Decimal]),
  586. [lists:flatten(Spaces ++ Decimal)];
  587. stringformat_io(Input, _Conversion, _ConversionFlag, _MinFieldWidth,
  588. _Precision, PrecisionLength, "d") ->
  589. %Conversion = [ConversionFlag, MinFieldWidth, Precision, PrecisionLength, ConversionType],
  590. %Format = "~" ++ PrecisionLength ++ "..0" ++ re:replace(Conversion, "d", "B", [{return, list}] ),
  591. %?debugFmt("precision d, Conversion: ~p~n", [Conversion]),
  592. Format = lists:flatten("~" ++ io_lib:format("~B..0B",[PrecisionLength])),
  593. ?debugFmt("Format: ~p~n",[Format]),
  594. io_lib:format(Format, [cast_to_integer(Input)]);
  595. stringformat_io(Input, Conversion, _ConversionFlag, _MinFieldWidth,
  596. _Precision, _PrecisionLength, "i")->
  597. Format = "~" ++ re:replace(Conversion, "i", "B", [{return, list}] ),
  598. io_lib:format(Format, [cast_to_integer(Input)]);
  599. stringformat_io(Input, Conversion, _ConversionFlag, _MinFieldWidth,
  600. _Precision, _PrecisionLength, "X")->
  601. Format = "~" ++ re:replace(Conversion, "X", ".16B", [{return, list}] ),
  602. io_lib:format(Format, [cast_to_integer(Input)]);
  603. stringformat_io(Input, Conversion, _ConversionFlag, _MinFieldWidth,
  604. _Precision, _PrecisionLength, "x")->
  605. Format = "~" ++ re:replace(Conversion, "x", ".16b", [{return, list}] ),
  606. io_lib:format(Format, [cast_to_integer(Input)]);
  607. stringformat_io(Input, Conversion, _ConversionFlag, _MinFieldWidth,
  608. _Precision, _PrecisionLength, "o")->
  609. Format = "~" ++ re:replace(Conversion, "o", ".8b", [{return, list}] ),
  610. io_lib:format(Format, [cast_to_integer(Input)]);
  611. stringformat_io(Input, _Conversion, _ConversionFlag, _MinFieldWidth,
  612. Precision, PrecisionLength, "e") when is_integer(PrecisionLength), PrecisionLength >= 2->
  613. ?debugFmt("PrecisionLength ~p~n", [PrecisionLength]),
  614. Conversion1 = lists:concat(["","",Precision,PrecisionLength + 1,"e"]),
  615. Format = "~" ++ Conversion1,
  616. io_lib:format(Format, [cast_to_float(Input)]);
  617. stringformat_io(Input, Conversion, ConversionFlag, MinFieldWidth,
  618. "", [], "e")->
  619. Format = "~" ++ re:replace(Conversion, "e", ".6e", [{return, list}] ),
  620. Raw = lists:flatten(stringformat_io(Input, Format, ConversionFlag, MinFieldWidth,
  621. ".", 6, "e")
  622. ),
  623. %io:format("Raw: ~p~n", [Raw]),
  624. Elocate = string:rstr(Raw,"e+"),
  625. %io:format("Elocate: ~p~n", [Elocate]),
  626. String = [string:substr(Raw,1,Elocate-1) ++ "e+"
  627. ++ io_lib:format("~2..0B",[list_to_integer(string:substr(Raw,Elocate+2))])], %works till +99, then outputs "**"
  628. %io:format("String: ~p~n", [String]),
  629. String;
  630. stringformat_io(Input, Conversion, ConversionFlag, MinFieldWidth,
  631. Precision, PrecisionLength, "E")->
  632. Format = re:replace(Conversion, "E", "e", [{return, list}] ),
  633. [Str] = stringformat_io(Input, Format, ConversionFlag, MinFieldWidth,
  634. Precision, PrecisionLength, "e"),
  635. [string:to_upper(Str)].
  636. %% @doc Strips all [X]HTML tags.
  637. striptags(Input) when is_binary(Input) ->
  638. striptags(unicode:characters_to_list(Input));
  639. striptags(Input) ->
  640. Regex = "(<[^>]+>)",
  641. Result = re:replace(Input,Regex,"", [global,{return,list}]),
  642. Result.
  643. cast_to_float([]) ->
  644. [];
  645. cast_to_float(Input) when is_float(Input) ->
  646. Input;
  647. cast_to_float(Input) when is_integer(Input) ->
  648. Input + 0.0;
  649. cast_to_float(Input) when is_binary(Input) ->
  650. %% be compatible with releases prior to R16B
  651. case erlang:function_exported(erlang, binary_to_float, 1) of
  652. true ->
  653. try erlang:binary_to_float(Input)
  654. catch
  655. error:_Reason ->
  656. erlang:binary_to_integer(Input) + 0.0
  657. end;
  658. false ->
  659. cast_to_float(binary_to_list(Input))
  660. end;
  661. cast_to_float(Input) when is_list(Input) ->
  662. try list_to_float(Input)
  663. catch
  664. error:_Reason ->
  665. list_to_integer(Input) + 0.0
  666. end.
  667. cast_to_integer([]) ->
  668. [];
  669. cast_to_integer(Input) when is_integer(Input) ->
  670. Input;
  671. cast_to_integer(Input) when is_float(Input) ->
  672. erlang:round(Input);
  673. cast_to_integer(Input) when is_binary(Input) ->
  674. cast_to_integer(unicode:characters_to_list(Input));
  675. cast_to_integer(Input) when is_list(Input)->
  676. case lists:member($., Input) of
  677. true ->
  678. erlang:round(erlang:list_to_float(Input));
  679. false ->
  680. erlang:list_to_integer(Input)
  681. end.
  682. cast_to_list(Input) when is_list(Input) -> Input;
  683. cast_to_list(Input) when is_binary(Input) -> binary_to_list(Input);
  684. cast_to_list(Input) when is_atom(Input) -> atom_to_list(Input);
  685. cast_to_list(Input) -> hd(io_lib:format("~p", [Input])).
  686. %% @doc Converts to lowercase, removes non-word characters (alphanumerics and underscores) and converts spaces to hyphens.
  687. slugify(Input) when is_binary(Input) ->
  688. slugify(unicode:characters_to_list(Input));
  689. slugify(Input) when is_list(Input) ->
  690. slugify(Input, []).
  691. %% @doc Formats a time according to the given format.
  692. time(Input) ->
  693. date(Input, "f a").
  694. time(Input, FormatStr) ->
  695. date(Input, FormatStr).
  696. timesince(Date) ->
  697. timesince(Date, calendar:local_time()).
  698. %%algorithm taken from django code
  699. timesince(Date,Comparison) ->
  700. Since = calendar:datetime_to_gregorian_seconds(Comparison) - calendar:datetime_to_gregorian_seconds(Date),
  701. timesince0(Since, [], 0).
  702. timesince0(_, Acc, 2) ->
  703. string:join(lists:reverse(Acc), ", ");
  704. timesince0(Seconds, Acc, Terms) when Seconds >= ?SECONDS_PER_YEAR ->
  705. Years = Seconds div ?SECONDS_PER_YEAR,
  706. timesince0(Seconds rem ?SECONDS_PER_YEAR, [io_lib:format("~B ~s~s", [Years, "year", pluralize(Years)])|Acc], Terms+1);
  707. timesince0(Seconds, Acc, Terms) when Seconds >= ?SECONDS_PER_MONTH ->
  708. Months = Seconds div ?SECONDS_PER_MONTH,
  709. timesince0(Seconds rem ?SECONDS_PER_MONTH, [io_lib:format("~B ~s~s", [Months, "month", pluralize(Months)])|Acc], Terms+1);
  710. timesince0(Seconds, Acc, Terms) when Seconds >= ?SECONDS_PER_WEEK ->
  711. Weeks = Seconds div ?SECONDS_PER_WEEK,
  712. timesince0(Seconds rem ?SECONDS_PER_WEEK, [io_lib:format("~B ~s~s", [Weeks, "week", pluralize(Weeks)])|Acc], Terms+1);
  713. timesince0(Seconds, Acc, Terms) when Seconds >= ?SECONDS_PER_DAY ->
  714. Days = Seconds div ?SECONDS_PER_DAY,
  715. timesince0(Seconds rem ?SECONDS_PER_DAY, [io_lib:format("~B ~s~s", [Days, "day", pluralize(Days)])|Acc], Terms+1);
  716. timesince0(Seconds, Acc, Terms) when Seconds >= ?SECONDS_PER_HOUR ->
  717. Hours = Seconds div ?SECONDS_PER_HOUR,
  718. timesince0(Seconds rem ?SECONDS_PER_HOUR, [io_lib:format("~B ~s~s", [Hours, "hour", pluralize(Hours)])|Acc], Terms+1);
  719. timesince0(Seconds, Acc, Terms) when Seconds >= ?SECONDS_PER_MINUTE ->
  720. Minutes = Seconds div ?SECONDS_PER_MINUTE,
  721. timesince0(Seconds rem ?SECONDS_PER_MINUTE,[io_lib:format("~B ~s~s", [Minutes, "minute", pluralize(Minutes)])|Acc], Terms+1);
  722. timesince0(Seconds, Acc, Terms) when Seconds >= 1 ->
  723. timesince0(0, [io_lib:format("~B ~s~s", [Seconds, "second", pluralize(Seconds)])|Acc], Terms+1);
  724. timesince0(Seconds, [], 0) when Seconds =< 0 ->
  725. timesince0(0, ["0 minutes"], 1);
  726. timesince0(0, Acc, Terms) ->
  727. timesince0(0, Acc, Terms+1).
  728. timeuntil(Date) ->
  729. timesince(calendar:local_time(),Date).
  730. timeuntil(Date,Comparison) ->
  731. timesince(Comparison,Date).
  732. %% @doc Converts a string into titlecase.
  733. title(Input) when is_binary(Input) ->
  734. title(unicode:characters_to_list(Input));
  735. title(Input) when is_list(Input) ->
  736. title(lower(Input), []).
  737. %% @doc Truncates a string after a certain number of characters.
  738. truncatechars(Input, Max) ->
  739. truncatechars_io(cast_to_list(Input), Max, []).
  740. %% @doc Truncates a string after a certain number of words.
  741. truncatewords(_Input, Max) when Max < 0 ->
  742. "";
  743. truncatewords(Input, Max) when is_binary(Input) ->
  744. unicode:characters_to_binary(truncatewords(unicode:characters_to_list(Input), Max));
  745. truncatewords(Input, Max) ->
  746. truncatewords_io(cast_to_list(Input), Max, []).
  747. %% @doc Similar to truncatewords, except that it is aware of HTML tags.
  748. truncatewords_html(_Input, Max) when Max < 0 ->
  749. "";
  750. truncatewords_html(Input, Max) when is_binary(Input) ->
  751. unicode:characters_to_binary(truncatewords_html(unicode:characters_to_list(Input), Max));
  752. truncatewords_html(Input, Max) ->
  753. truncatewords_html_io(cast_to_list(Input), Max, [], [], text).
  754. %% @doc Recursively takes a self-nested list and returns an HTML unordered list -- WITHOUT opening and closing `<ul>' tags.
  755. unordered_list(List) ->
  756. String = lists:flatten(unordered_list(List, [])),
  757. string:substr(String, 5, erlang:length(String) - 9).
  758. unordered_list([], Acc) ->
  759. ["<ul>", lists:reverse(Acc), "</ul>"];
  760. unordered_list([First|_] = List, []) when is_integer(First) ->
  761. "<li>"++List;
  762. unordered_list([First|Rest], Acc) when is_list(First), Rest == [] ->
  763. unordered_list(Rest, ["</li>"] ++ [unordered_list(First, []) | Acc ]) ;
  764. unordered_list([First|Rest], Acc) when is_list(First), is_integer(hd(hd(Rest))) ->
  765. unordered_list(Rest, [unordered_list(First, []) ++ "</li>" |Acc]);
  766. unordered_list([First|Rest], Acc) when is_list(First) ->
  767. unordered_list(Rest, [unordered_list(First, [])|Acc]).
  768. %% @doc Converts a string into all uppercase.
  769. upper(Input) when is_binary(Input) ->
  770. unicode:characters_to_binary(upper(unicode:characters_to_list(Input)));
  771. upper(Input) ->
  772. string:to_upper(Input).
  773. %% @doc Escapes a value for use in a URL.
  774. urlencode(Input) ->
  775. urlencode(Input, <<"/">>).
  776. urlencode(Input, Safe) when is_binary(Input) ->
  777. urlencode_io(Input, Safe, 0);
  778. urlencode(Input, Safe) when is_list(Input) ->
  779. urlencode_io(Input, Safe, []).
  780. %% @doc Returns the number of words.
  781. wordcount(Input) when is_binary(Input) ->
  782. wordcount(unicode:characters_to_list(Input));
  783. wordcount(Input) when is_list(Input) ->
  784. wordcount(Input, 0).
  785. %% @doc Wraps words at specified line length, uses `<BR/>' html tag to delimit lines
  786. wordwrap(Input, Number) when is_binary(Input) ->
  787. wordwrap(unicode:characters_to_list(Input), Number);
  788. wordwrap(Input, Number) when is_list(Input) ->
  789. wordwrap(Input, [], [], 0, Number).
  790. %% @doc Given a string mapping values for true, false and (optionally) undefined, returns one of those strings according to the value.
  791. yesno(Bool, Choices) when is_binary(Choices) ->
  792. yesno_io(Bool, Choices);
  793. yesno(Bool, Choices) when is_list(Choices) ->
  794. yesno_io(Bool, unicode:characters_to_binary(Choices)).
  795. % internal
  796. addslashes([], Acc) ->
  797. lists:reverse(Acc);
  798. addslashes([H|T], Acc) when H =:= $"; H =:= $' ->
  799. addslashes(T, [H, $\\|Acc]);
  800. addslashes([H|T], Acc) ->
  801. addslashes(T, [H|Acc]).
  802. cut([], _, Acc) ->
  803. lists:reverse(Acc);
  804. cut([H|T], H, Acc) ->
  805. cut(T, H, Acc);
  806. cut([H|T], Char, Acc) ->
  807. cut(T, Char, [H|Acc]).
  808. escape(Binary, Index) when is_binary(Binary) ->
  809. case Binary of
  810. <<Pre:Index/binary, $<, Post/binary>> ->
  811. process_binary_match(Pre, <<"&lt;">>, size(Post), escape(Post, 0));
  812. <<Pre:Index/binary, $>, Post/binary>> ->
  813. process_binary_match(Pre, <<"&gt;">>, size(Post), escape(Post, 0));
  814. <<Pre:Index/binary, $&, Post/binary>> ->
  815. process_binary_match(Pre, <<"&amp;">>, size(Post), escape(Post, 0));
  816. <<Pre:Index/binary, 34, Post/binary>> ->
  817. process_binary_match(Pre, <<"&quot;">>, size(Post), escape(Post, 0));
  818. <<Pre:Index/binary, 39, Post/binary>> ->
  819. process_binary_match(Pre, <<"&#039;">>, size(Post), escape(Post, 0));
  820. <<_:Index/binary, _, _/binary>> ->
  821. escape(Binary, Index + 1);
  822. Binary ->
  823. Binary
  824. end;
  825. escape([], Acc) ->
  826. lists:reverse(Acc);
  827. escape("<" ++ Rest, Acc) ->
  828. escape(Rest, lists:reverse("&lt;", Acc));
  829. escape(">" ++ Rest, Acc) ->
  830. escape(Rest, lists:reverse("&gt;", Acc));
  831. escape("&" ++ Rest, Acc) ->
  832. escape(Rest, lists:reverse("&amp;", Acc));
  833. escape("\"" ++ Rest, Acc) ->
  834. escape(Rest, lists:reverse("&quot;", Acc));
  835. escape("'" ++ Rest, Acc) ->
  836. escape(Rest, lists:reverse("&#039;", Acc));
  837. escape([S | Rest], Acc) when is_list(S); is_binary(S)->
  838. escape(Rest, [force_escape(S) | Acc]);
  839. escape([C | Rest], Acc) ->
  840. escape(Rest, [C | Acc]).
  841. escapejs([], Acc) ->
  842. lists:reverse(Acc);
  843. escapejs([C | Rest], Acc) when C < 32; C =:= $"; C =:= $'; C =:= $\\; C =:= $<;
  844. C =:= $>; C =:= $&; C =:= $=; C =:= $-; C =:= $;;
  845. C =:= 8232; C =:= 8233 -> % just following django here...
  846. escapejs(Rest, lists:reverse(lists:flatten(io_lib:format("\\u~4.16.0B", [C])), Acc));
  847. escapejs([C | Rest], Acc) ->
  848. escapejs(Rest, [C | Acc]).
  849. filesizeformat(Bytes, UnitStr) ->
  850. lists:flatten(io_lib:format("~.1f ~s", [Bytes, UnitStr])).
  851. fix_ampersands(Input, Index) when is_binary(Input) ->
  852. case Input of
  853. <<Pre:Index/binary, $&, Post/binary>> ->
  854. process_binary_match(Pre, <<"&amp;">>, size(Post), Post);
  855. <<_:Index/binary, _/binary>> ->
  856. fix_ampersands(Input, Index + 1);
  857. _ ->
  858. Input
  859. end;
  860. fix_ampersands([], Acc) ->
  861. lists:reverse(Acc);
  862. fix_ampersands("&" ++ Rest, Acc) ->
  863. fix_ampersands(Rest, lists:reverse("&amp;", Acc));
  864. fix_ampersands([C | Rest], Acc) ->
  865. fix_ampersands(Rest, [C | Acc]).
  866. iriencode([], Acc) ->
  867. lists:reverse(Acc);
  868. iriencode([C | Rest], Acc) when ?NO_IRI_ENCODE(C) ->
  869. iriencode(Rest, [C | Acc]);
  870. iriencode([$\s | Rest], Acc) ->
  871. iriencode(Rest, [$+ | Acc]);
  872. iriencode([C | Rest], Acc) ->
  873. <<Hi:4, Lo:4>> = <<C>>,
  874. iriencode(Rest, [hexdigit(Lo), hexdigit(Hi), $\% | Acc]).
  875. join_io([], _Sep) -> [];
  876. join_io([_] = X, _Sep) -> X;
  877. join_io([X|T], Sep) -> [X,Sep] ++ join_io(T, Sep).
  878. linebreaksbr(Input, Index) when is_binary(Input) ->
  879. Break = <<"<br />">>,
  880. case Input of
  881. <<Pre:Index/binary, $\r, $\n, Post/binary>> ->
  882. process_binary_match(Pre, Break, size(Post), linebreaksbr(Post, 0));
  883. <<Pre:Index/binary, $\n, Post/binary>> ->
  884. process_binary_match(Pre, Break, size(Post), linebreaksbr(Post, 0));
  885. <<_:Index/binary, _/binary>> ->
  886. linebreaksbr(Input, Index + 1);
  887. _ ->
  888. Input
  889. end;
  890. linebreaksbr([], Acc) ->
  891. lists:reverse(Acc);
  892. linebreaksbr("\r\n" ++ Rest, Acc) ->
  893. linebreaksbr(Rest, lists:reverse("<br />", Acc));
  894. linebreaksbr("\n" ++ Rest, Acc) ->
  895. linebreaksbr(Rest, lists:reverse("<br />", Acc));
  896. linebreaksbr([C | Rest], Acc) ->
  897. linebreaksbr(Rest, [C | Acc]).
  898. lower(Input, Index) ->
  899. case Input of
  900. <<Pre:Index/binary, Byte, Post/binary>> when Byte >= $A andalso Byte =< $Z ->
  901. process_binary_match(Pre, <<(Byte - $A + $a)>>, size(Post), lower(Post, 0));
  902. <<_:Index/binary, _/binary>> ->
  903. lower(Input, Index + 1);
  904. _ ->
  905. Input
  906. end.
  907. phone2numeric([], Acc) ->
  908. lists:reverse(Acc);
  909. phone2numeric([H|T], Acc) when H >= $a, H =< $c; H >= $A, H =< $C ->
  910. phone2numeric(T, [$2|Acc]);
  911. phone2numeric([H|T], Acc) when H >= $d, H =< $f; H >= $D, H =< $F ->
  912. phone2numeric(T, [$3|Acc]);
  913. phone2numeric([H|T], Acc) when H >= $g, H =< $i; H >= $G, H =< $I ->
  914. phone2numeric(T, [$4|Acc]);
  915. phone2numeric([H|T], Acc) when H >= $j, H =< $l; H >= $J, H =< $L ->
  916. phone2numeric(T, [$5|Acc]);
  917. phone2numeric([H|T], Acc) when H >= $m, H =< $o; H >= $M, H =< $O ->
  918. phone2numeric(T, [$6|Acc]);
  919. phone2numeric([H|T], Acc) when H >= $p, H =< $s; H >= $P, H =< $S ->
  920. phone2numeric(T, [$7|Acc]);
  921. phone2numeric([H|T], Acc) when H >= $t, H =< $v; H >= $T, H =< $V ->
  922. phone2numeric(T, [$8|Acc]);
  923. phone2numeric([H|T], Acc) when H >= $w, H =< $z; H >= $W, H =< $Z ->
  924. phone2numeric(T, [$9|Acc]);
  925. phone2numeric([H|T], Acc) ->
  926. phone2numeric(T, [H|Acc]).
  927. slugify([], Acc) ->
  928. lists:reverse(Acc);
  929. slugify([H|T], Acc) when H >= $A, H =< $Z ->
  930. slugify(T, [H-$A+$a|Acc]);
  931. slugify([$\ |T], Acc) ->
  932. slugify(T, [$-|Acc]);
  933. slugify([H|T], Acc) when H >= $a, H =< $z; H >= $0, H =< $9; H =:= $_ ->
  934. slugify(T, [H|Acc]);
  935. slugify([_|T], Acc) ->
  936. slugify(T, Acc).
  937. title([], Acc) ->
  938. lists:reverse(Acc);
  939. title([Char | Rest], [] = Acc) when Char >= $a, Char =< $z ->
  940. title(Rest, [Char + ($A - $a) | Acc]);
  941. title([Char | Rest], [Sep|[Sep2|_Other]] = Acc)
  942. when Char >= $a, Char =< $z,
  943. not (Sep >= $a andalso Sep =< $z),
  944. not (Sep >= $A andalso Sep =< $Z),
  945. not (Sep >= $0 andalso Sep =< $9),
  946. not (Sep =:= $' andalso (Sep2 >= $a andalso Sep2 =< $z)) ->
  947. title(Rest, [Char + ($A - $a) | Acc]);
  948. title([Char | Rest], Acc) ->
  949. title(Rest, [Char | Acc]).
  950. truncatechars_io([], _CharsLeft, Acc) ->
  951. lists:reverse(Acc);
  952. truncatechars_io(_Input, 0, Acc) ->
  953. lists:reverse("..." ++ drop_chars(Acc, 3));
  954. truncatechars_io([C|Rest], CharsLeft, Acc) when C >= 2#11111100 ->
  955. truncatechars_io(Rest, CharsLeft + 4, [C|Acc]);
  956. truncatechars_io([C|Rest], CharsLeft, Acc) when C >= 2#11111000 ->
  957. truncatechars_io(Rest, CharsLeft + 3, [C|Acc]);
  958. truncatechars_io([C|Rest], CharsLeft, Acc) when C >= 2#11110000 ->
  959. truncatechars_io(Rest, CharsLeft + 2, [C|Acc]);
  960. truncatechars_io([C|Rest], CharsLeft, Acc) when C >= 2#11100000 ->
  961. truncatechars_io(Rest, CharsLeft + 1, [C|Acc]);
  962. truncatechars_io([C|Rest], CharsLeft, Acc) when C >= 2#11000000 ->
  963. truncatechars_io(Rest, CharsLeft, [C|Acc]);
  964. truncatechars_io([C|Rest], CharsLeft, Acc) ->
  965. truncatechars_io(Rest, CharsLeft - 1, [C|Acc]).
  966. drop_chars([], _) -> [];
  967. drop_chars(Cs, 0) -> Cs;
  968. drop_chars([C|Cs], Count) when C >= 2#11111100 ->
  969. drop_chars(Cs, Count + 4);
  970. drop_chars([C|Cs], Count) when C >= 2#11111000 ->
  971. drop_chars(Cs, Count + 3);
  972. drop_chars([C|Cs], Count) when C >= 2#11110000 ->
  973. drop_chars(Cs, Count + 2);
  974. drop_chars([C|Cs], Count) when C >= 2#11100000 ->
  975. drop_chars(Cs, Count + 1);
  976. drop_chars([C|Cs], Count) when C >= 2#11000000 ->
  977. drop_chars(Cs, Count);
  978. drop_chars([_|Cs], Count) ->
  979. drop_chars(Cs, Count - 1).
  980. truncatewords_io([], _WordsLeft, Acc) ->
  981. lists:reverse(Acc);
  982. truncatewords_io(_Input, 0, Acc) ->
  983. lists:reverse("... " ++ Acc);
  984. truncatewords_io([C1, C2|Rest], WordsLeft, Acc) when C1 =/= $\s andalso C2 =:= $\s ->
  985. truncatewords_io([C2|Rest], WordsLeft - 1, [C1|Acc]);
  986. truncatewords_io([C1|Rest], WordsLeft, Acc) ->
  987. truncatewords_io(Rest, WordsLeft, [C1|Acc]).
  988. truncatewords_html_io([], _WordsLeft, Acc, [], _) ->
  989. lists:reverse(Acc);
  990. truncatewords_html_io(_Input, 0, Acc, [], _) ->
  991. lists:reverse(Acc);
  992. truncatewords_html_io(Input, 0, Acc, [Tag|RestOfTags], done) ->
  993. truncatewords_html_io(Input, 0, ">"++Tag++"/<" ++ Acc, RestOfTags, done);
  994. truncatewords_html_io(Input, 0, Acc, [Tag|RestOfTags], _) ->
  995. truncatewords_html_io(Input, 0, "...>"++Tag++"/<" ++ Acc, RestOfTags, done);
  996. truncatewords_html_io([], WordsLeft, Acc, [Tag|RestOfTags], _) ->
  997. truncatewords_html_io([], WordsLeft, ">"++Tag++"/<" ++ Acc, RestOfTags, text);
  998. truncatewords_html_io([C|Rest], WordsLeft, Acc, Tags, text) when C =:= $< ->
  999. truncatewords_html_io(Rest, WordsLeft, [C|Acc], [""|Tags], tag);
  1000. truncatewords_html_io([C1, C2|Rest], WordsLeft, Acc, Tags, text) when C1 =/= $\ , C2 =:= $\ ; C1 =/= $\ , C2 =:= $< ->
  1001. truncatewords_html_io([C2|Rest], WordsLeft - 1, [C1|Acc], Tags, text);
  1002. truncatewords_html_io([C|Rest], WordsLeft, Acc, Tags, text) ->
  1003. truncatewords_html_io(Rest, WordsLeft, [C|Acc], Tags, text);
  1004. truncatewords_html_io([C|Rest], WordsLeft, Acc, [""|Tags], tag) when C =:= $/ ->
  1005. truncatewords_html_io(Rest, WordsLeft, [C|Acc], Tags, close_tag);
  1006. truncatewords_html_io([C|Rest], WordsLeft, Acc, [Tag|RestOfTags], tag) when C >= $a, C =< $z; C >= $A, C =< $Z ->
  1007. truncatewords_html_io(Rest, WordsLeft, [C|Acc], [[C|Tag]|RestOfTags], tag);
  1008. truncatewords_html_io([C|Rest], WordsLeft, Acc, Tags, tag) when C =:= $> ->
  1009. truncatewords_html_io(Rest, WordsLeft, [C|Acc], Tags, text);
  1010. truncatewords_html_io([C|Rest], WordsLeft, Acc, Tags, tag) ->
  1011. truncatewords_html_io(Rest, WordsLeft, [C|Acc], Tags, attrs);
  1012. truncatewords_html_io([C|Rest], WordsLeft, Acc, Tags, attrs) when C =:= $> ->
  1013. truncatewords_html_io(Rest, WordsLeft, [C|Acc], Tags, text);
  1014. truncatewords_html_io([C|Rest], WordsLeft, Acc, [_Tag|RestOfTags], close_tag) when C =:= $> ->
  1015. truncatewords_html_io(Rest, WordsLeft, [C|Acc], RestOfTags, text).
  1016. wordcount([], Count) ->
  1017. Count;
  1018. wordcount([C1], Count) when C1 =/= $\ ->
  1019. Count+1;
  1020. wordcount([C1, C2|Rest], Count) when C1 =/= $\ andalso C2 =:= $\ ->
  1021. wordcount([C2|Rest], Count + 1);
  1022. wordcount([_|Rest], Count) ->
  1023. wordcount(Rest, Count).
  1024. % No more input, we're done
  1025. wordwrap([], Acc, WordAcc, _LineLength, _WrapAt) ->
  1026. lists:reverse(WordAcc ++ Acc);
  1027. % Premature newline
  1028. wordwrap([$\n | Rest], Acc, WordAcc, _LineLength, WrapAt) ->
  1029. wordwrap(Rest, [$\n | WordAcc ++ Acc], [], 0, WrapAt);
  1030. % Hit the wrap length at a space character. Add a newline
  1031. wordwrap([$\ | Rest], Acc, WordAcc, WrapAt, WrapAt) ->
  1032. wordwrap(Rest, [$\n | WordAcc ++ Acc], [], 0, WrapAt);
  1033. % Hit a space character before the wrap length. Keep going
  1034. wordwrap([$\ | Rest], Acc, WordAcc, LineLength, WrapAt) ->
  1035. wordwrap(Rest, [$\ | WordAcc ++ Acc], [], LineLength + 1 + erlang:length(WordAcc), WrapAt);
  1036. % Overflowed the current line while building a word
  1037. wordwrap([C | Rest], Acc, WordAcc, 0, WrapAt) when erlang:length(WordAcc) > WrapAt ->
  1038. wordwrap(Rest, Acc, [C | WordAcc], 0, WrapAt);
  1039. wordwrap([C | Rest], Acc, WordAcc, LineLength, WrapAt) when erlang:length(WordAcc) + LineLength > WrapAt ->
  1040. wordwrap(Rest, [$\n | Acc], [C | WordAcc], 0, WrapAt);
  1041. % Just building a word...
  1042. wordwrap([C | Rest], Acc, WordAcc, LineLength, WrapAt) ->
  1043. wordwrap(Rest, Acc, [C | WordAcc], LineLength, WrapAt).
  1044. urlencode_io(Input, Safe, Index) when is_binary(Input) ->
  1045. case Input of
  1046. <<_:Index/binary, Byte, _/binary>> when ?NO_ENCODE(Byte) ->
  1047. urlencode_io(Input, Safe, Index + 1);
  1048. <<Pre:Index/binary, C:1/binary, Post/binary>> ->
  1049. process_binary_match(
  1050. Pre, maybe_urlencode_char(C, Safe),
  1051. size(Post), urlencode_io(Post, Safe, 0));
  1052. Input ->
  1053. Input
  1054. end;
  1055. urlencode_io([], _Safe, Acc) ->
  1056. lists:reverse(Acc);
  1057. urlencode_io([C | Rest], Safe, Acc) when ?NO_ENCODE(C) ->
  1058. urlencode_io(Rest, Safe, [C | Acc]);
  1059. urlencode_io([C | Rest], Safe, Acc) ->
  1060. urlencode_io(Rest, Safe, [maybe_urlencode_char(<<C>>, Safe) | Acc]).
  1061. maybe_urlencode_char(C, Safe) ->
  1062. case binary:match(Safe, C) of
  1063. nomatch ->
  1064. <<Hi:4, Lo:4>> = C,
  1065. HiDigit = hexdigit(Hi),
  1066. LoDigit = hexdigit(Lo),
  1067. <<$%, HiDigit, LoDigit>>;
  1068. _ -> C
  1069. end.
  1070. %% @doc Converts URLs in text into clickable links.
  1071. %%TODO: Autoescape not yet implemented
  1072. urlize(Input) when is_binary(Input) ->
  1073. urlize(unicode:characters_to_list(Input),0);
  1074. urlize(Input) ->
  1075. urlize(Input,0).
  1076. urlize(Input, Trunc) when is_binary(Input) ->
  1077. urlize(unicode:characters_to_list(Input),Trunc);
  1078. urlize(Input, Trunc) ->
  1079. {ok,RE} = re:compile("(([[:alpha:]]+://|www\.)[^<>[:space:]]+[[:alnum:]/])"),
  1080. RegexResult = re:run(Input,RE,[global]),
  1081. case RegexResult of
  1082. {match, Matches} ->
  1083. Indexes = lists:map(fun(Match) -> lists:nth(2,Match) end, Matches),
  1084. Domains = lists:map(fun({Start, Length}) -> lists:sublist(Input, Start+1, Length) end, Indexes),
  1085. URIDomains = lists:map(fun(Domain) -> addDefaultURI(Domain) end, Domains),
  1086. case Trunc == 0 of
  1087. true ->
  1088. DomainsTrunc = Domains;
  1089. false ->
  1090. DomainsTrunc = lists:map(fun(Domain) -> string:concat( string:substr(Domain,1,Trunc-3), "...") end, Domains)
  1091. end,
  1092. ReplaceList = lists:zip(URIDomains,DomainsTrunc),
  1093. ReplaceStrings = lists:map(fun({URIDomain,Domain}) -> lists:flatten(io_lib:format("<a href=\"~s\" rel=\"nofollow\">~s</a>",[URIDomain,Domain])) end, ReplaceList),
  1094. Template = re:replace(Input,"(([[:alpha:]]+://|www\.)[^<>[:space:]]+[[:alnum:]/])", "~s", [global,{return,list}]),
  1095. Result = lists:flatten(io_lib:format(Template,ReplaceStrings)),
  1096. Result;
  1097. nomatch ->
  1098. Input
  1099. end.
  1100. %% @doc Converts URLs into clickable links just like urlize, but truncates URLs longer than the given character limit.
  1101. urlizetrunc(Input, Trunc) ->
  1102. urlize(Input, Trunc).
  1103. addDefaultURI(Domain) ->
  1104. case string:str(Domain,"://") of
  1105. 0 ->
  1106. Domain1 = string:concat("http://",Domain);
  1107. _ ->
  1108. Domain1 = Domain
  1109. end,
  1110. Domain1.
  1111. hexdigit(C) when C < 10 -> $0 + C;
  1112. hexdigit(C) when C < 16 -> $A + (C - 10).
  1113. process_binary_match(Pre, Insertion, SizePost, Post) ->
  1114. case {size(Pre), SizePost} of
  1115. {0, 0} -> Insertion;
  1116. {0, _} -> [Insertion, Post];
  1117. {_, 0} -> [Pre, Insertion];
  1118. _ -> [Pre, Insertion, Post]
  1119. end.
  1120. yesno_io(Val, Choices) ->
  1121. {True, False, Undefined} =
  1122. case binary:split(Choices, <<",">>, [global]) of
  1123. [T, F, U] -> {T, F, U};
  1124. [T, F] -> {T, F, F};
  1125. _ -> throw({yesno, choices})
  1126. end,
  1127. if Val =:= false -> False;
  1128. Val =:= undefined -> Undefined;
  1129. is_list(Val); is_binary(Val) ->
  1130. case iolist_size(Val) of
  1131. 0 -> False;
  1132. _ -> True
  1133. end;
  1134. true -> True
  1135. end.
  1136. %% unjoin == split in other languages; inverse of join
  1137. %%FROM: http://www.erlang.org/pipermail/erlang-questions/2008-October/038896.html
  1138. unjoin(String, []) ->
  1139. unjoin0(String);
  1140. unjoin(String, [Sep]) when is_integer(Sep) ->
  1141. unjoin1(String, Sep);
  1142. unjoin(String, [C1,C2|L]) when is_integer(C1), is_integer(C2) ->
  1143. unjoin2(String, C1, C2, L).
  1144. %% Split a string at "", which is deemed to occur _between_
  1145. %% adjacent characters, but queerly, not at the beginning
  1146. %% or the end.
  1147. unjoin0([C|Cs]) ->
  1148. [[C] | unjoin0(Cs)];
  1149. unjoin0([]) ->
  1150. [].
  1151. %% Split a string at a single character separator.
  1152. unjoin1(String, Sep) ->
  1153. unjoin1_loop(String, Sep, "").
  1154. unjoin1_loop([Sep|String], Sep, Rev) ->
  1155. [lists:reverse(Rev) | unjoin1(String, Sep)];
  1156. unjoin1_loop([Chr|String], Sep, Rev) ->
  1157. unjoin1_loop(String, Sep, [Chr|Rev]);
  1158. unjoin1_loop([], _, Rev) ->
  1159. [lists:reverse(Rev)].
  1160. %% Split a string at a multi-character separator
  1161. %% [C1,C2|L]. These components are split out for
  1162. %% a fast match.
  1163. unjoin2(String, C1, C2, L) ->
  1164. unjoin2_loop(String, C1, C2, L, "").
  1165. unjoin2_loop([C1|S = [C2|String]], C1, C2, L, Rev) ->
  1166. case unjoin_prefix(L, String)
  1167. of no -> unjoin2_loop(S, C1, C2, L, [C1|Rev])
  1168. ; Rest -> [lists:reverse(Rev) | unjoin2(Rest, C1, C2, L)]
  1169. end;
  1170. unjoin2_loop([Chr|String], C1, C2, L, Rev) ->
  1171. unjoin2_loop(String, C1, C2, L, [Chr|Rev]);
  1172. unjoin2_loop([], _, _, _, Rev) ->
  1173. [lists:reverse(Rev)].
  1174. unjoin_prefix([C|L], [C|S]) -> unjoin_prefix(L, S);
  1175. unjoin_prefix([], S) -> S;
  1176. unjoin_prefix(_, _) -> no.