erlydtl_filters.erl 51 KB

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