Template.hx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. /*
  2. * Copyright (C)2005-2019 Haxe Foundation
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a
  5. * copy of this software and associated documentation files (the "Software"),
  6. * to deal in the Software without restriction, including without limitation
  7. * the rights to use, copy, modify, merge, publish, distribute, sublicense,
  8. * and/or sell copies of the Software, and to permit persons to whom the
  9. * Software is furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  19. * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  20. * DEALINGS IN THE SOFTWARE.
  21. */
  22. package haxe;
  23. import haxe.ds.List;
  24. using StringTools;
  25. private enum TemplateExpr {
  26. OpVar(v:String);
  27. OpExpr(expr:Void->Dynamic);
  28. OpIf(expr:Void->Dynamic, eif:TemplateExpr, ?eelse:TemplateExpr);
  29. OpStr(str:String);
  30. OpBlock(l:List<TemplateExpr>);
  31. OpForeach(expr:Void->Dynamic, loop:TemplateExpr);
  32. OpMacro(name:String, params:List<TemplateExpr>);
  33. }
  34. private typedef Token = {
  35. var s:Bool;
  36. var p:String;
  37. var l:Array<String>;
  38. }
  39. private typedef ExprToken = {
  40. var s:Bool;
  41. var p:String;
  42. }
  43. /**
  44. `Template` provides a basic templating mechanism to replace values in a source
  45. String, and to have some basic logic.
  46. A complete documentation of the supported syntax is available at:
  47. <https://haxe.org/manual/std-template.html>
  48. **/
  49. class Template {
  50. static var splitter = ~/(::[A-Za-z0-9_ ()&|!+=\/><*."-]+::|\$\$([A-Za-z0-9_-]+)\()/;
  51. static var expr_splitter = ~/(\(|\)|[ \r\n\t]*"[^"]*"[ \r\n\t]*|[!+=\/><*.&|-]+)/;
  52. static var expr_trim = ~/^[ ]*([^ ]+)[ ]*$/;
  53. static var expr_int = ~/^[0-9]+$/;
  54. static var expr_float = ~/^([+-]?)(?=\d|,\d)\d*(,\d*)?([Ee]([+-]?\d+))?$/;
  55. /**
  56. Global replacements which are used across all `Template` instances. This
  57. has lower priority than the context argument of `execute()`.
  58. **/
  59. public static var globals:Dynamic = {};
  60. // To avoid issues with DCE, keep the array iterator.
  61. @:ifFeature("haxe.Template.run") static var hxKeepArrayIterator = [].iterator();
  62. var expr:TemplateExpr;
  63. var context:Dynamic;
  64. var macros:Dynamic;
  65. var stack:List<Dynamic>;
  66. var buf:StringBuf;
  67. /**
  68. Creates a new `Template` instance from `str`.
  69. `str` is parsed into tokens, which are stored for internal use. This
  70. means that multiple `execute()` operations on a single `Template` instance
  71. are more efficient than one `execute()` operations on multiple `Template`
  72. instances.
  73. If `str` is `null`, the result is unspecified.
  74. **/
  75. public function new(str:String) {
  76. var tokens = parseTokens(str);
  77. expr = parseBlock(tokens);
  78. if (!tokens.isEmpty())
  79. throw "Unexpected '" + tokens.first().s + "'";
  80. }
  81. /**
  82. Executes `this` `Template`, taking into account `context` for
  83. replacements and `macros` for callback functions.
  84. If `context` has a field `name`, its value replaces all occurrences of
  85. `::name::` in the `Template`. Otherwise `Template.globals` is checked instead,
  86. If `name` is not a field of that either, `::name::` is replaced with `null`.
  87. If `macros` has a field `name`, all occurrences of `$$name(args)` are
  88. replaced with the result of calling that field. The first argument is
  89. always the `resolve()` method, followed by the given arguments.
  90. If `macros` has no such field, the result is unspecified.
  91. If `context` is `null`, the result is unspecified. If `macros` is `null`,
  92. no macros are used.
  93. **/
  94. public function execute(context:Dynamic, ?macros:Dynamic):String {
  95. this.macros = if (macros == null) {} else macros;
  96. this.context = context;
  97. stack = new List();
  98. buf = new StringBuf();
  99. run(expr);
  100. return buf.toString();
  101. }
  102. function resolve(v:String):Dynamic {
  103. if (v == "__current__")
  104. return context;
  105. if (Reflect.isObject(context)) {
  106. var value = Reflect.getProperty(context, v);
  107. if (value != null || Reflect.hasField(context, v))
  108. return value;
  109. }
  110. for (ctx in stack) {
  111. var value = Reflect.getProperty(ctx, v);
  112. if (value != null || Reflect.hasField(ctx, v))
  113. return value;
  114. }
  115. return Reflect.field(globals, v);
  116. }
  117. function parseTokens(data:String) {
  118. var tokens = new List<Token>();
  119. while (splitter.match(data)) {
  120. var p = splitter.matchedPos();
  121. if (p.pos > 0)
  122. tokens.add({p: data.substr(0, p.pos), s: true, l: null});
  123. // : ?
  124. if (data.charCodeAt(p.pos) == 58) {
  125. tokens.add({p: data.substr(p.pos + 2, p.len - 4), s: false, l: null});
  126. data = splitter.matchedRight();
  127. continue;
  128. }
  129. // macro parse
  130. var parp = p.pos + p.len;
  131. var npar = 1;
  132. var params = [];
  133. var part = "";
  134. while (true) {
  135. var c = data.charCodeAt(parp);
  136. parp++;
  137. if (c == 40) {
  138. npar++;
  139. } else if (c == 41) {
  140. npar--;
  141. if (npar <= 0)
  142. break;
  143. } else if (c == null) {
  144. throw "Unclosed macro parenthesis";
  145. }
  146. if (c == 44 && npar == 1) {
  147. params.push(part);
  148. part = "";
  149. } else {
  150. part += String.fromCharCode(c);
  151. }
  152. }
  153. params.push(part);
  154. tokens.add({p: splitter.matched(2), s: false, l: params});
  155. data = data.substr(parp, data.length - parp);
  156. }
  157. if (data.length > 0)
  158. tokens.add({p: data, s: true, l: null});
  159. return tokens;
  160. }
  161. function parseBlock(tokens:List<Token>) {
  162. var l = new List();
  163. while (true) {
  164. var t = tokens.first();
  165. if (t == null)
  166. break;
  167. if (!t.s && (t.p == "end" || t.p == "else" || t.p.substr(0, 7) == "elseif "))
  168. break;
  169. l.add(parse(tokens));
  170. }
  171. if (l.length == 1)
  172. return l.first();
  173. return OpBlock(l);
  174. }
  175. function parse(tokens:List<Token>) {
  176. var t = tokens.pop();
  177. var p = t.p;
  178. if (t.s)
  179. return OpStr(p);
  180. // macro
  181. if (t.l != null) {
  182. var pe = new List();
  183. for (p in t.l)
  184. pe.add(parseBlock(parseTokens(p)));
  185. return OpMacro(p, pe);
  186. }
  187. function kwdEnd(kwd:String):Int {
  188. var pos = -1;
  189. var length = kwd.length;
  190. if (p.substr(0, length) == kwd) {
  191. pos = length;
  192. for (c in p.substr(length)) {
  193. switch c {
  194. case ' '.code: pos++;
  195. case _: break;
  196. }
  197. }
  198. }
  199. return pos;
  200. }
  201. // 'end' , 'else', 'elseif' can't be found here
  202. var pos = kwdEnd("if");
  203. if (pos > 0) {
  204. p = p.substr(pos, p.length - pos);
  205. var e = parseExpr(p);
  206. var eif = parseBlock(tokens);
  207. var t = tokens.first();
  208. var eelse;
  209. if (t == null)
  210. throw "Unclosed 'if'";
  211. if (t.p == "end") {
  212. tokens.pop();
  213. eelse = null;
  214. } else if (t.p == "else") {
  215. tokens.pop();
  216. eelse = parseBlock(tokens);
  217. t = tokens.pop();
  218. if (t == null || t.p != "end")
  219. throw "Unclosed 'else'";
  220. } else { // elseif
  221. t.p = t.p.substr(4, t.p.length - 4);
  222. eelse = parse(tokens);
  223. }
  224. return OpIf(e, eif, eelse);
  225. }
  226. var pos = kwdEnd("foreach");
  227. if (pos >= 0) {
  228. p = p.substr(pos, p.length - pos);
  229. var e = parseExpr(p);
  230. var efor = parseBlock(tokens);
  231. var t = tokens.pop();
  232. if (t == null || t.p != "end")
  233. throw "Unclosed 'foreach'";
  234. return OpForeach(e, efor);
  235. }
  236. if (expr_splitter.match(p))
  237. return OpExpr(parseExpr(p));
  238. return OpVar(p);
  239. }
  240. function parseExpr(data:String) {
  241. var l = new List<ExprToken>();
  242. var expr = data;
  243. while (expr_splitter.match(data)) {
  244. var p = expr_splitter.matchedPos();
  245. var k = p.pos + p.len;
  246. if (p.pos != 0)
  247. l.add({p: data.substr(0, p.pos), s: true});
  248. var p = expr_splitter.matched(0);
  249. l.add({p: p, s: p.indexOf('"') >= 0});
  250. data = expr_splitter.matchedRight();
  251. }
  252. if (data.length != 0) {
  253. for (i => c in data) {
  254. switch c {
  255. case ' '.code:
  256. case _:
  257. l.add({p: data.substr(i), s: true});
  258. break;
  259. }
  260. }
  261. }
  262. var e:Void->Dynamic;
  263. try {
  264. e = makeExpr(l);
  265. if (!l.isEmpty())
  266. throw l.first().p;
  267. } catch (s:String) {
  268. throw "Unexpected '" + s + "' in " + expr;
  269. }
  270. return function() {
  271. try {
  272. return e();
  273. } catch (exc:Dynamic) {
  274. throw "Error : " + Std.string(exc) + " in " + expr;
  275. }
  276. }
  277. }
  278. function makeConst(v:String):Void->Dynamic {
  279. expr_trim.match(v);
  280. v = expr_trim.matched(1);
  281. if (v.charCodeAt(0) == 34) {
  282. var str = v.substr(1, v.length - 2);
  283. return function() return str;
  284. }
  285. if (expr_int.match(v)) {
  286. var i = Std.parseInt(v);
  287. return function() {
  288. return i;
  289. };
  290. }
  291. if (expr_float.match(v)) {
  292. var f = Std.parseFloat(v);
  293. return function() {
  294. return f;
  295. };
  296. }
  297. var me = this;
  298. return function() {
  299. return me.resolve(v);
  300. };
  301. }
  302. function makePath(e:Void->Dynamic, l:List<ExprToken>) {
  303. var p = l.first();
  304. if (p == null || p.p != ".")
  305. return e;
  306. l.pop();
  307. var field = l.pop();
  308. if (field == null || !field.s)
  309. throw field.p;
  310. var f = field.p;
  311. expr_trim.match(f);
  312. f = expr_trim.matched(1);
  313. return makePath(function() {
  314. return Reflect.field(e(), f);
  315. }, l);
  316. }
  317. function makeExpr(l) {
  318. return makePath(makeExpr2(l), l);
  319. }
  320. function skipSpaces(l:List<ExprToken>) {
  321. var p = l.first();
  322. while (p != null) {
  323. for (c in p.p) {
  324. if (c != " ".code) {
  325. return;
  326. }
  327. }
  328. l.pop();
  329. p = l.first();
  330. }
  331. }
  332. function makeExpr2(l:List<ExprToken>):Void->Dynamic {
  333. skipSpaces(l);
  334. var p = l.pop();
  335. skipSpaces(l);
  336. if (p == null)
  337. throw "<eof>";
  338. if (p.s)
  339. return makeConst(p.p);
  340. switch (p.p) {
  341. case "(":
  342. skipSpaces(l);
  343. var e1:Dynamic = makeExpr(l);
  344. skipSpaces(l);
  345. var p = l.pop();
  346. if (p == null || p.s)
  347. throw p;
  348. if (p.p == ")")
  349. return e1;
  350. skipSpaces(l);
  351. var e2:Dynamic = makeExpr(l);
  352. skipSpaces(l);
  353. var p2 = l.pop();
  354. skipSpaces(l);
  355. if (p2 == null || p2.p != ")")
  356. throw p2;
  357. return switch (p.p) {
  358. case "+": function() {
  359. return cast e1() + e2();
  360. };
  361. case "-": function() {
  362. return cast e1() - e2();
  363. };
  364. case "*": function() {
  365. return cast e1() * e2();
  366. };
  367. case "/": function() {
  368. return cast e1() / e2();
  369. };
  370. case ">": function() {
  371. return cast e1() > e2();
  372. };
  373. case "<": function() {
  374. return cast e1() < e2();
  375. };
  376. case ">=": function() {
  377. return cast e1() >= e2();
  378. };
  379. case "<=": function() {
  380. return cast e1() <= e2();
  381. };
  382. case "==": function() {
  383. return cast e1() == e2();
  384. };
  385. case "!=": function() {
  386. return cast e1() != e2();
  387. };
  388. case "&&": function() {
  389. return cast e1() && e2();
  390. };
  391. case "||": function() {
  392. return cast e1() || e2();
  393. };
  394. default: throw "Unknown operation " + p.p;
  395. }
  396. case "!":
  397. var e:Void->Dynamic = makeExpr(l);
  398. return function() {
  399. var v:Dynamic = e();
  400. return (v == null || v == false);
  401. };
  402. case "-":
  403. var e = makeExpr(l);
  404. return function() {
  405. return -e();
  406. };
  407. }
  408. throw p.p;
  409. }
  410. function run(e:TemplateExpr) {
  411. switch (e) {
  412. case OpVar(v):
  413. buf.add(Std.string(resolve(v)));
  414. case OpExpr(e):
  415. buf.add(Std.string(e()));
  416. case OpIf(e, eif, eelse):
  417. var v:Dynamic = e();
  418. if (v == null || v == false) {
  419. if (eelse != null)
  420. run(eelse);
  421. } else
  422. run(eif);
  423. case OpStr(str):
  424. buf.add(str);
  425. case OpBlock(l):
  426. for (e in l)
  427. run(e);
  428. case OpForeach(e, loop):
  429. var v:Dynamic = e();
  430. try {
  431. var x:Dynamic = v.iterator();
  432. if (x.hasNext == null)
  433. throw null;
  434. v = x;
  435. } catch (e:Dynamic)
  436. try {
  437. if (v.hasNext == null)
  438. throw null;
  439. } catch (e:Dynamic) {
  440. throw "Cannot iter on " + v;
  441. }
  442. stack.push(context);
  443. var v:Iterator<Dynamic> = v;
  444. for (ctx in v) {
  445. context = ctx;
  446. run(loop);
  447. }
  448. context = stack.pop();
  449. case OpMacro(m, params):
  450. var v:Dynamic = Reflect.field(macros, m);
  451. var pl = new Array<Dynamic>();
  452. var old = buf;
  453. pl.push(resolve);
  454. for (p in params) {
  455. switch (p) {
  456. case OpVar(v): pl.push(resolve(v));
  457. default:
  458. buf = new StringBuf();
  459. run(p);
  460. pl.push(buf.toString());
  461. }
  462. }
  463. buf = old;
  464. try {
  465. buf.add(Std.string(Reflect.callMethod(macros, v, pl)));
  466. } catch (e:Dynamic) {
  467. var plstr = try pl.join(",") catch (e:Dynamic) "???";
  468. var msg = "Macro call " + m + "(" + plstr + ") failed (" + Std.string(e) + ")";
  469. #if neko
  470. neko.Lib.rethrow(msg);
  471. #else
  472. throw msg;
  473. #end
  474. }
  475. }
  476. }
  477. }