json-template.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839
  1. // Copyright (C) 2009 Andy Chu
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. // $Id$
  15. //
  16. // JavaScript implementation of json-template.
  17. //
  18. // This is predefined in tests, shouldn't be defined anywhere else. TODO: Do
  19. // something nicer.
  20. var log = log || function() {};
  21. var repr = repr || function() {};
  22. // The "module" exported by this script is called "jsontemplate":
  23. var jsontemplate = function() {
  24. // Regex escaping for metacharacters
  25. function EscapeMeta(meta) {
  26. return meta.replace(/([\{\}\(\)\[\]\|\^\$\-\+\?])/g, '\\$1');
  27. }
  28. var token_re_cache = {};
  29. function _MakeTokenRegex(meta_left, meta_right) {
  30. var key = meta_left + meta_right;
  31. var regex = token_re_cache[key];
  32. if (regex === undefined) {
  33. var str = '(' + EscapeMeta(meta_left) + '.*?' + EscapeMeta(meta_right) +
  34. '\n?)';
  35. regex = new RegExp(str, 'g');
  36. }
  37. return regex;
  38. }
  39. //
  40. // Formatters
  41. //
  42. function HtmlEscape(s) {
  43. return s.replace(/&/g,'&').
  44. replace(/>/g,'>').
  45. replace(/</g,'&lt;');
  46. }
  47. function HtmlTagEscape(s) {
  48. return s.replace(/&/g,'&amp;').
  49. replace(/>/g,'&gt;').
  50. replace(/</g,'&lt;').
  51. replace(/"/g,'&quot;');
  52. }
  53. // Default ToString can be changed
  54. function ToString(s) {
  55. if (s === null) {
  56. return 'null';
  57. }
  58. return s.toString();
  59. }
  60. // Formatter to pluralize words
  61. function _Pluralize(value, unused_context, args) {
  62. var s, p;
  63. switch (args.length) {
  64. case 0:
  65. s = ''; p = 's';
  66. break;
  67. case 1:
  68. s = ''; p = args[0];
  69. break;
  70. case 2:
  71. s = args[0]; p = args[1];
  72. break;
  73. default:
  74. // Should have been checked at compile time
  75. throw {
  76. name: 'EvaluationError', message: 'pluralize got too many args'
  77. };
  78. }
  79. return (value > 1) ? p : s;
  80. }
  81. function _Cycle(value, unused_context, args) {
  82. // Cycle between various values on consecutive integers.
  83. // @index starts from 1, so use 1-based indexing.
  84. return args[(value - 1) % args.length];
  85. }
  86. var DEFAULT_FORMATTERS = {
  87. 'html': HtmlEscape,
  88. 'htmltag': HtmlTagEscape,
  89. 'html-attr-value': HtmlTagEscape,
  90. 'str': ToString,
  91. 'raw': function(x) { return x; },
  92. 'AbsUrl': function(value, context) {
  93. // TODO: Normalize leading/trailing slashes
  94. return context.get('base-url') + '/' + value;
  95. }
  96. };
  97. var DEFAULT_PREDICATES = {
  98. 'singular?': function(x) { return x == 1; },
  99. 'plural?': function(x) { return x > 1; },
  100. 'Debug?': function(unused, context) {
  101. try {
  102. return context.get('debug');
  103. } catch(err) {
  104. if (err.name == 'UndefinedVariable') {
  105. return false;
  106. } else {
  107. throw err;
  108. }
  109. }
  110. }
  111. };
  112. var FunctionRegistry = function() {
  113. return {
  114. lookup: function(user_str) {
  115. return [null, null];
  116. }
  117. };
  118. };
  119. var SimpleRegistry = function(obj) {
  120. return {
  121. lookup: function(user_str) {
  122. var func = obj[user_str] || null;
  123. return [func, null];
  124. }
  125. };
  126. };
  127. var CallableRegistry = function(callable) {
  128. return {
  129. lookup: function(user_str) {
  130. var func = callable(user_str);
  131. return [func, null];
  132. }
  133. };
  134. };
  135. // Default formatters which can't be expressed in DEFAULT_FORMATTERS
  136. var PrefixRegistry = function(functions) {
  137. return {
  138. lookup: function(user_str) {
  139. for (var i = 0; i < functions.length; i++) {
  140. var name = functions[i].name, func = functions[i].func;
  141. if (user_str.slice(0, name.length) == name) {
  142. // Delimiter is usually a space, but could be something else
  143. var args;
  144. var splitchar = user_str.charAt(name.length);
  145. if (splitchar === '') {
  146. args = []; // No arguments
  147. } else {
  148. args = user_str.split(splitchar).slice(1);
  149. }
  150. return [func, args];
  151. }
  152. }
  153. return [null, null]; // No formatter
  154. }
  155. };
  156. };
  157. var ChainedRegistry = function(registries) {
  158. return {
  159. lookup: function(user_str) {
  160. for (var i=0; i<registries.length; i++) {
  161. var result = registries[i].lookup(user_str);
  162. if (result[0]) {
  163. return result;
  164. }
  165. }
  166. return [null, null]; // Nothing found
  167. }
  168. };
  169. };
  170. //
  171. // Template implementation
  172. //
  173. function _ScopedContext(context, undefined_str) {
  174. // The stack contains:
  175. // The current context (an object).
  176. // An iteration index. -1 means we're NOT iterating.
  177. var stack = [{context: context, index: -1}];
  178. return {
  179. PushSection: function(name) {
  180. if (name === undefined || name === null) {
  181. return null;
  182. }
  183. var new_context;
  184. if (name == '@') {
  185. new_context = stack[stack.length-1].context;
  186. } else {
  187. new_context = stack[stack.length-1].context[name] || null;
  188. }
  189. stack.push({context: new_context, index: -1});
  190. return new_context;
  191. },
  192. Pop: function() {
  193. stack.pop();
  194. },
  195. next: function() {
  196. var stacktop = stack[stack.length-1];
  197. // Now we're iterating -- push a new mutable object onto the stack
  198. if (stacktop.index == -1) {
  199. stacktop = {context: null, index: 0};
  200. stack.push(stacktop);
  201. }
  202. // The thing we're iterating over
  203. var context_array = stack[stack.length-2].context;
  204. // We're already done
  205. if (stacktop.index == context_array.length) {
  206. stack.pop();
  207. return undefined; // sentinel to say that we're done
  208. }
  209. stacktop.context = context_array[stacktop.index++];
  210. return true; // OK, we mutated the stack
  211. },
  212. _Undefined: function(name) {
  213. if (undefined_str === undefined) {
  214. throw {
  215. name: 'UndefinedVariable', message: name + ' is not defined'
  216. };
  217. } else {
  218. return undefined_str;
  219. }
  220. },
  221. _LookUpStack: function(name) {
  222. var i = stack.length - 1;
  223. while (true) {
  224. var frame = stack[i];
  225. if (name == '@index') {
  226. if (frame.index != -1) { // -1 is undefined
  227. return frame.index;
  228. }
  229. } else {
  230. var context = frame.context;
  231. if (typeof context === 'object') {
  232. var value = context[name];
  233. if (value !== undefined) {
  234. return value;
  235. }
  236. }
  237. }
  238. i--;
  239. if (i <= -1) {
  240. return this._Undefined(name);
  241. }
  242. }
  243. },
  244. get: function(name) {
  245. if (name == '@') {
  246. return stack[stack.length-1].context;
  247. }
  248. var parts = name.split('.');
  249. var value = this._LookUpStack(parts[0]);
  250. if (parts.length > 1) {
  251. for (var i=1; i<parts.length; i++) {
  252. value = value[parts[i]];
  253. if (value === undefined) {
  254. return this._Undefined(parts[i]);
  255. }
  256. }
  257. }
  258. return value;
  259. }
  260. };
  261. }
  262. // Crockford's "functional inheritance" pattern
  263. var _AbstractSection = function(spec) {
  264. var that = {};
  265. that.current_clause = [];
  266. that.Append = function(statement) {
  267. that.current_clause.push(statement);
  268. };
  269. that.AlternatesWith = function() {
  270. throw {
  271. name: 'TemplateSyntaxError',
  272. message:
  273. '{.alternates with} can only appear with in {.repeated section ...}'
  274. };
  275. };
  276. that.NewOrClause = function(pred) {
  277. throw { name: 'NotImplemented' }; // "Abstract"
  278. };
  279. return that;
  280. };
  281. var _Section = function(spec) {
  282. var that = _AbstractSection(spec);
  283. that.statements = {'default': that.current_clause};
  284. that.section_name = spec.section_name;
  285. that.Statements = function(clause) {
  286. clause = clause || 'default';
  287. return that.statements[clause] || [];
  288. };
  289. that.NewOrClause = function(pred) {
  290. if (pred) {
  291. throw {
  292. name: 'TemplateSyntaxError',
  293. message: '{.or} clause only takes a predicate inside predicate blocks'
  294. };
  295. }
  296. that.current_clause = [];
  297. that.statements['or'] = that.current_clause;
  298. };
  299. return that;
  300. };
  301. // Repeated section is like section, but it supports {.alternates with}
  302. var _RepeatedSection = function(spec) {
  303. var that = _Section(spec);
  304. that.AlternatesWith = function() {
  305. that.current_clause = [];
  306. that.statements['alternate'] = that.current_clause;
  307. };
  308. return that;
  309. };
  310. // Represents a sequence of predicate clauses.
  311. var _PredicateSection = function(spec) {
  312. var that = _AbstractSection(spec);
  313. // Array of func, statements
  314. that.clauses = [];
  315. that.NewOrClause = function(pred) {
  316. // {.or} always executes if reached, so use identity func with no args
  317. pred = pred || [function(x) { return true; }, null];
  318. that.current_clause = [];
  319. that.clauses.push([pred, that.current_clause]);
  320. };
  321. return that;
  322. };
  323. function _Execute(statements, context, callback) {
  324. for (var i=0; i<statements.length; i++) {
  325. var statement = statements[i];
  326. if (typeof(statement) == 'string') {
  327. callback(statement);
  328. } else {
  329. var func = statement[0];
  330. var args = statement[1];
  331. func(args, context, callback);
  332. }
  333. }
  334. }
  335. function _DoSubstitute(statement, context, callback) {
  336. var value;
  337. value = context.get(statement.name);
  338. // Format values
  339. for (var i=0; i<statement.formatters.length; i++) {
  340. var pair = statement.formatters[i];
  341. var formatter = pair[0];
  342. var args = pair[1];
  343. value = formatter(value, context, args);
  344. }
  345. callback(value);
  346. }
  347. // for [section foo]
  348. function _DoSection(args, context, callback) {
  349. var block = args;
  350. var value = context.PushSection(block.section_name);
  351. var do_section = false;
  352. // "truthy" values should have their sections executed.
  353. if (value) {
  354. do_section = true;
  355. }
  356. // Except: if the value is a zero-length array (which is "truthy")
  357. if (value && value.length === 0) {
  358. do_section = false;
  359. }
  360. if (do_section) {
  361. _Execute(block.Statements(), context, callback);
  362. context.Pop();
  363. } else { // Empty list, None, False, etc.
  364. context.Pop();
  365. _Execute(block.Statements('or'), context, callback);
  366. }
  367. }
  368. // {.pred1?} A {.or pred2?} B ... {.or} Z {.end}
  369. function _DoPredicates(args, context, callback) {
  370. // Here we execute the first clause that evaluates to true, and then stop.
  371. var block = args;
  372. var value = context.get('@');
  373. for (var i=0; i<block.clauses.length; i++) {
  374. var clause = block.clauses[i];
  375. var predicate = clause[0][0];
  376. var pred_args = clause[0][1];
  377. var statements = clause[1];
  378. var do_clause = predicate(value, context, pred_args);
  379. if (do_clause) {
  380. _Execute(statements, context, callback);
  381. break;
  382. }
  383. }
  384. }
  385. function _DoRepeatedSection(args, context, callback) {
  386. var block = args;
  387. items = context.PushSection(block.section_name);
  388. pushed = true;
  389. if (items && items.length > 0) {
  390. // TODO: check that items is an array; apparently this is hard in JavaScript
  391. //if type(items) is not list:
  392. // raise EvaluationError('Expected a list; got %s' % type(items))
  393. // Execute the statements in the block for every item in the list.
  394. // Execute the alternate block on every iteration except the last. Each
  395. // item could be an atom (string, integer, etc.) or a dictionary.
  396. var last_index = items.length - 1;
  397. var statements = block.Statements();
  398. var alt_statements = block.Statements('alternate');
  399. for (var i=0; context.next() !== undefined; i++) {
  400. _Execute(statements, context, callback);
  401. if (i != last_index) {
  402. _Execute(alt_statements, context, callback);
  403. }
  404. }
  405. } else {
  406. _Execute(block.Statements('or'), context, callback);
  407. }
  408. context.Pop();
  409. }
  410. var _SECTION_RE = /(repeated)?\s*(section)\s+(\S+)?/;
  411. var _OR_RE = /or(?:\s+(.+))?/;
  412. var _IF_RE = /if(?:\s+(.+))?/;
  413. // Turn a object literal, function, or Registry into a Registry
  414. function MakeRegistry(obj) {
  415. if (!obj) {
  416. // if null/undefined, use a totally empty FunctionRegistry
  417. return new FunctionRegistry();
  418. } else if (typeof obj === 'function') {
  419. return new CallableRegistry(obj);
  420. } else if (obj.lookup !== undefined) {
  421. // TODO: Is this a good pattern? There is a namespace conflict where get
  422. // could be either a formatter or a method on a FunctionRegistry.
  423. // instanceof might be more robust.
  424. return obj;
  425. } else if (typeof obj === 'object') {
  426. return new SimpleRegistry(obj);
  427. }
  428. }
  429. // TODO: The compile function could be in a different module, in case we want to
  430. // compile on the server side.
  431. function _Compile(template_str, options) {
  432. var more_formatters = MakeRegistry(options.more_formatters);
  433. // default formatters with arguments
  434. var default_formatters = PrefixRegistry([
  435. {name: 'pluralize', func: _Pluralize},
  436. {name: 'cycle', func: _Cycle}
  437. ]);
  438. var all_formatters = new ChainedRegistry([
  439. more_formatters,
  440. SimpleRegistry(DEFAULT_FORMATTERS),
  441. default_formatters
  442. ]);
  443. var more_predicates = MakeRegistry(options.more_predicates);
  444. // TODO: Add defaults
  445. var all_predicates = new ChainedRegistry([
  446. more_predicates, SimpleRegistry(DEFAULT_PREDICATES)
  447. ]);
  448. // We want to allow an explicit null value for default_formatter, which means
  449. // that an error is raised if no formatter is specified.
  450. var default_formatter;
  451. if (options.default_formatter === undefined) {
  452. default_formatter = 'str';
  453. } else {
  454. default_formatter = options.default_formatter;
  455. }
  456. function GetFormatter(format_str) {
  457. var pair = all_formatters.lookup(format_str);
  458. if (!pair[0]) {
  459. throw {
  460. name: 'BadFormatter',
  461. message: format_str + ' is not a valid formatter'
  462. };
  463. }
  464. return pair;
  465. }
  466. function GetPredicate(pred_str) {
  467. var pair = all_predicates.lookup(pred_str);
  468. if (!pair[0]) {
  469. throw {
  470. name: 'BadPredicate',
  471. message: pred_str + ' is not a valid predicate'
  472. };
  473. }
  474. return pair;
  475. }
  476. var format_char = options.format_char || '|';
  477. if (format_char != ':' && format_char != '|') {
  478. throw {
  479. name: 'ConfigurationError',
  480. message: 'Only format characters : and | are accepted'
  481. };
  482. }
  483. var meta = options.meta || '{}';
  484. var n = meta.length;
  485. if (n % 2 == 1) {
  486. throw {
  487. name: 'ConfigurationError',
  488. message: meta + ' has an odd number of metacharacters'
  489. };
  490. }
  491. var meta_left = meta.substring(0, n/2);
  492. var meta_right = meta.substring(n/2, n);
  493. var token_re = _MakeTokenRegex(meta_left, meta_right);
  494. var current_block = _Section({});
  495. var stack = [current_block];
  496. var strip_num = meta_left.length; // assume they're the same length
  497. var token_match;
  498. var last_index = 0;
  499. while (true) {
  500. token_match = token_re.exec(template_str);
  501. if (token_match === null) {
  502. break;
  503. } else {
  504. var token = token_match[0];
  505. }
  506. // Add the previous literal to the program
  507. if (token_match.index > last_index) {
  508. var tok = template_str.slice(last_index, token_match.index);
  509. current_block.Append(tok);
  510. }
  511. last_index = token_re.lastIndex;
  512. var had_newline = false;
  513. if (token.slice(-1) == '\n') {
  514. token = token.slice(null, -1);
  515. had_newline = true;
  516. }
  517. token = token.slice(strip_num, -strip_num);
  518. if (token.charAt(0) == '#') {
  519. continue; // comment
  520. }
  521. if (token.charAt(0) == '.') { // Keyword
  522. token = token.substring(1, token.length);
  523. var literal = {
  524. 'meta-left': meta_left,
  525. 'meta-right': meta_right,
  526. 'space': ' ',
  527. 'tab': '\t',
  528. 'newline': '\n'
  529. }[token];
  530. if (literal !== undefined) {
  531. current_block.Append(literal);
  532. continue;
  533. }
  534. var new_block, func;
  535. var section_match = token.match(_SECTION_RE);
  536. if (section_match) {
  537. var repeated = section_match[1];
  538. var section_name = section_match[3];
  539. if (repeated) {
  540. func = _DoRepeatedSection;
  541. new_block = _RepeatedSection({section_name: section_name});
  542. } else {
  543. func = _DoSection;
  544. new_block = _Section({section_name: section_name});
  545. }
  546. current_block.Append([func, new_block]);
  547. stack.push(new_block);
  548. current_block = new_block;
  549. continue;
  550. }
  551. var pred_str, pred;
  552. // Check {.or pred?} before {.pred?}
  553. var or_match = token.match(_OR_RE);
  554. if (or_match) {
  555. pred_str = or_match[1];
  556. pred = pred_str ? GetPredicate(pred_str) : null;
  557. current_block.NewOrClause(pred);
  558. continue;
  559. }
  560. // Match either {.pred?} or {.if pred?}
  561. var matched = false;
  562. var if_match = token.match(_IF_RE);
  563. if (if_match) {
  564. pred_str = if_match[1];
  565. matched = true;
  566. } else if (token.charAt(token.length-1) == '?') {
  567. pred_str = token;
  568. matched = true;
  569. }
  570. if (matched) {
  571. pred = pred_str ? GetPredicate(pred_str) : null;
  572. new_block = _PredicateSection();
  573. new_block.NewOrClause(pred);
  574. current_block.Append([_DoPredicates, new_block]);
  575. stack.push(new_block);
  576. current_block = new_block;
  577. continue;
  578. }
  579. if (token == 'alternates with') {
  580. current_block.AlternatesWith();
  581. continue;
  582. }
  583. if (token == 'end') {
  584. // End the block
  585. stack.pop();
  586. if (stack.length > 0) {
  587. current_block = stack[stack.length-1];
  588. } else {
  589. throw {
  590. name: 'TemplateSyntaxError',
  591. message: 'Got too many {end} statements'
  592. };
  593. }
  594. continue;
  595. }
  596. }
  597. // A variable substitution
  598. var parts = token.split(format_char);
  599. var formatters;
  600. var name;
  601. if (parts.length == 1) {
  602. if (default_formatter === null) {
  603. throw {
  604. name: 'MissingFormatter',
  605. message: 'This template requires explicit formatters.'
  606. };
  607. }
  608. // If no formatter is specified, use the default.
  609. formatters = [GetFormatter(default_formatter)];
  610. name = token;
  611. } else {
  612. formatters = [];
  613. for (var j=1; j<parts.length; j++) {
  614. formatters.push(GetFormatter(parts[j]));
  615. }
  616. name = parts[0];
  617. }
  618. current_block.Append([_DoSubstitute, {name: name, formatters: formatters}]);
  619. if (had_newline) {
  620. current_block.Append('\n');
  621. }
  622. }
  623. // Add the trailing literal
  624. current_block.Append(template_str.slice(last_index));
  625. if (stack.length !== 1) {
  626. throw {
  627. name: 'TemplateSyntaxError',
  628. message: 'Got too few {end} statements'
  629. };
  630. }
  631. return current_block;
  632. }
  633. // The Template class is defined in the traditional style so that users can add
  634. // methods by mutating the prototype attribute. TODO: Need a good idiom for
  635. // inheritance without mutating globals.
  636. function Template(template_str, options) {
  637. // Add 'new' if we were not called with 'new', so prototyping works.
  638. if(!(this instanceof Template)) {
  639. return new Template(template_str, options);
  640. }
  641. this._options = options || {};
  642. this._program = _Compile(template_str, this._options);
  643. }
  644. Template.prototype.render = function(data_dict, callback) {
  645. // options.undefined_str can either be a string or undefined
  646. var context = _ScopedContext(data_dict, this._options.undefined_str);
  647. _Execute(this._program.Statements(), context, callback);
  648. };
  649. Template.prototype.expand = function(data_dict) {
  650. var tokens = [];
  651. this.render(data_dict, function(x) { tokens.push(x); });
  652. return tokens.join('');
  653. };
  654. // fromString is a construction method that allows metadata to be written at the
  655. // beginning of the template string. See Python's FromFile for a detailed
  656. // description of the format.
  657. //
  658. // The argument 'options' takes precedence over the options in the template, and
  659. // can be used for non-serializable options like template formatters.
  660. var OPTION_RE = /^([a-zA-Z\-]+):\s*(.*)/;
  661. var OPTION_NAMES = [
  662. 'meta', 'format-char', 'default-formatter', 'undefined-str'];
  663. // Use this "linear search" instead of Array.indexOf, which is nonstandard
  664. var OPTION_NAMES_RE = new RegExp(OPTION_NAMES.join('|'));
  665. function fromString(s, options) {
  666. var parsed = {};
  667. var begin = 0, end = 0;
  668. while (true) {
  669. var parsedOption = false;
  670. end = s.indexOf('\n', begin);
  671. if (end == -1) {
  672. break;
  673. }
  674. var line = s.slice(begin, end);
  675. begin = end+1;
  676. var match = line.match(OPTION_RE);
  677. if (match !== null) {
  678. var name = match[1].toLowerCase(), value = match[2];
  679. if (name.match(OPTION_NAMES_RE)) {
  680. name = name.replace('-', '_');
  681. value = value.replace(/^\s+/, '').replace(/\s+$/, '');
  682. if (name == 'default_formatter' && value.toLowerCase() == 'none') {
  683. value = null;
  684. }
  685. parsed[name] = value;
  686. parsedOption = true;
  687. }
  688. }
  689. if (!parsedOption) {
  690. break;
  691. }
  692. }
  693. // TODO: This doesn't enforce the blank line between options and template, but
  694. // that might be more trouble than it's worth
  695. if (parsed !== {}) {
  696. body = s.slice(begin);
  697. } else {
  698. body = s;
  699. }
  700. for (var o in options) {
  701. parsed[o] = options[o];
  702. }
  703. return Template(body, parsed);
  704. }
  705. // We just export one name for now, the Template "class".
  706. // We need HtmlEscape in the browser tests, so might as well export it.
  707. return {
  708. Template: Template, HtmlEscape: HtmlEscape,
  709. FunctionRegistry: FunctionRegistry, SimpleRegistry: SimpleRegistry,
  710. CallableRegistry: CallableRegistry, ChainedRegistry: ChainedRegistry,
  711. fromString: fromString,
  712. // Private but exposed for testing
  713. _Section: _Section
  714. };
  715. }();