miki-template 2.3.2 → 2.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tags/util.js CHANGED
@@ -2,22 +2,7 @@
2
2
  * Utility template tags: static, url, regroup, spaceless.
3
3
  */
4
4
 
5
- // Helper to resolve expression values (literals, numbers, booleans, or context lookups)
6
- function resolveValue(token, context) {
7
- if (token === undefined || token === null) return '';
8
- if (typeof token !== 'string') return token;
9
- if (token === '') return '';
10
- if ((token.startsWith('"') && token.endsWith('"')) || (token.startsWith('\'') && token.endsWith('\''))) {
11
- return token.slice(1, -1);
12
- }
13
- if (token === 'true' || token === 'True') return true;
14
- if (token === 'false' || token === 'False') return false;
15
- if (token === 'none' || token === 'None' || token === 'null') return null;
16
- // Numeric literal (integer or float)
17
- if (/^-?\d+(\.\d+)?$/.test(token)) return Number(token);
18
- // Otherwise treat as a context variable lookup
19
- return context.get(token);
20
- }
5
+ const { evaluateExpression, parseVariableExpression, VariableNode } = require('../parser');
21
6
 
22
7
  class StaticNode {
23
8
  constructor(pathExpr) {
@@ -25,7 +10,7 @@ class StaticNode {
25
10
  }
26
11
 
27
12
  render(context) {
28
- const resolvedPath = resolveValue(this.pathExpr, context);
13
+ const resolvedPath = evaluateExpression(this.pathExpr, context);
29
14
 
30
15
  // Resolve static prefix
31
16
  let prefix = '/static/';
@@ -55,11 +40,11 @@ class UrlNode {
55
40
  }
56
41
 
57
42
  render(context) {
58
- const routeName = resolveValue(this.routeNameExpr, context);
59
- const resolvedPositional = this.positionalArgs.map(arg => resolveValue(arg, context));
43
+ const routeName = evaluateExpression(this.routeNameExpr, context);
44
+ const resolvedPositional = this.positionalArgs.map(arg => evaluateExpression(arg, context));
60
45
  const resolvedKwargs = {};
61
46
  for (const kw of this.kwargs) {
62
- resolvedKwargs[kw.name] = resolveValue(kw.valueExpr, context);
47
+ resolvedKwargs[kw.name] = evaluateExpression(kw.valueExpr, context);
63
48
  }
64
49
  const hasKwargs = Object.keys(resolvedKwargs).length > 0;
65
50
 
@@ -330,6 +315,147 @@ class LoadNode {
330
315
  }
331
316
  }
332
317
 
318
+ class FilterNode {
319
+ constructor(filterName, filterArg, body) {
320
+ this.filterName = filterName;
321
+ this.filterArg = filterArg;
322
+ this.body = body || [];
323
+ }
324
+
325
+ render(context) {
326
+ const raw = this.body.map(n => n.render(context)).join('');
327
+ const { getFilter } = require('../filters');
328
+ const fn = getFilter(this.filterName);
329
+ if (!fn) throw new Error(`Unknown filter: '${this.filterName}'`);
330
+ return fn(raw, this.filterArg, context);
331
+ }
332
+ }
333
+
334
+ function parseFilter(tagContent, parser) {
335
+ const expr = tagContent.slice(6).trim();
336
+ const nameMatch = expr.match(/^([a-zA-Z_][a-zA-Z0-9_]*)/);
337
+ const filterName = nameMatch ? nameMatch[1] : expr;
338
+ const rest = expr.slice(filterName.length).trim();
339
+ let filterArg = undefined;
340
+ if (rest.startsWith(':')) {
341
+ const argStr = rest.slice(1).trim();
342
+ if ((argStr.startsWith('"') && argStr.endsWith('"')) || (argStr.startsWith('\'') && argStr.endsWith('\''))) {
343
+ filterArg = argStr.slice(1, -1);
344
+ } else if (argStr) {
345
+ const num = Number(argStr);
346
+ filterArg = Number.isNaN(num) ? argStr : num;
347
+ }
348
+ }
349
+ const body = parser.parse(['endfilter']);
350
+ const next = parser.peek();
351
+ if (next && next.type === 'block' && next.content.split(/\s+/)[0] === 'endfilter') {
352
+ parser.advance();
353
+ }
354
+ return new FilterNode(filterName, filterArg, body);
355
+ }
356
+
357
+ class VerbatimNode {
358
+ constructor(body) {
359
+ this.body = body || [];
360
+ }
361
+
362
+ render(_context) {
363
+ return this.body.map(n => {
364
+ if (n.constructor.name === 'TextNode') return n.content;
365
+ return '';
366
+ }).join('');
367
+ }
368
+ }
369
+
370
+ function parseVerbatim(tagContent, _parser) {
371
+ const body = _parser.parse(['endverbatim']);
372
+ const next = _parser.peek();
373
+ if (next && next.type === 'block' && next.content.split(/\s+/)[0] === 'endverbatim') {
374
+ _parser.advance();
375
+ }
376
+ return new VerbatimNode(body);
377
+ }
378
+
379
+ class QueryStringNode {
380
+ constructor(args) {
381
+ this.args = args || [];
382
+ }
383
+
384
+ render(context) {
385
+ const params = new URLSearchParams();
386
+
387
+ for (const arg of this.args) {
388
+ const parsed = parseVariableExpression(arg.valueExpr);
389
+ const node = new VariableNode(parsed.varPath, parsed.filters, parsed.isLiteral, parsed.literalValue);
390
+ const value = node.render(context);
391
+
392
+ if (value === null || value === undefined || value === '') {
393
+ continue;
394
+ }
395
+ params.append(arg.name, String(value));
396
+ }
397
+
398
+ const qs = params.toString();
399
+ return qs ? '?' + qs : '';
400
+ }
401
+ }
402
+
403
+ function parseQuerystring(tagContent, _parser) {
404
+ const trimmed = tagContent.replace(/^querystring\s+/, '').trim();
405
+ const args = [];
406
+
407
+ const isQuotedString = (str) => {
408
+ if ((str.startsWith('"') && str.endsWith('"')) || (str.startsWith('\'') && str.endsWith('\''))) {
409
+ const quote = str[0];
410
+ const inner = str.slice(1, -1);
411
+ return !inner.includes(quote);
412
+ }
413
+ return false;
414
+ };
415
+
416
+ if (isQuotedString(trimmed)) {
417
+ const raw = trimmed.slice(1, -1);
418
+ const pairs = raw.split('&');
419
+ for (const pair of pairs) {
420
+ const [name, value] = pair.split('=');
421
+ if (name) {
422
+ args.push({ name, valueExpr: value ? `'${value}'` : '\'\'' });
423
+ }
424
+ }
425
+ return new QueryStringNode(args);
426
+ }
427
+
428
+ const argRegex = /(".*?"|'.*?'|[^\s]+)/g;
429
+ const matches = trimmed.match(argRegex) || [];
430
+
431
+ for (let i = 0; i < matches.length; i++) {
432
+ const tok = matches[i];
433
+ const isQuoted = (tok.startsWith('"') && tok.endsWith('"')) ||
434
+ (tok.startsWith('\'') && tok.endsWith('\''));
435
+ if (isQuoted) {
436
+ const name = tok.slice(1, -1);
437
+ if (name.includes('=')) {
438
+ const [n, v] = name.split('=');
439
+ args.push({ name: n, valueExpr: v ? `"${v}"` : '""' });
440
+ } else if (i + 1 < matches.length) {
441
+ const valueExpr = matches[i + 1];
442
+ i++;
443
+ args.push({ name, valueExpr });
444
+ }
445
+ continue;
446
+ }
447
+
448
+ const eq = tok.indexOf('=');
449
+ if (eq > 0) {
450
+ const name = tok.slice(0, eq);
451
+ const valueExpr = tok.slice(eq + 1);
452
+ args.push({ name, valueExpr });
453
+ }
454
+ }
455
+
456
+ return new QueryStringNode(args);
457
+ }
458
+
333
459
  class TemplatetagNode {
334
460
  constructor(token) {
335
461
  this.token = token;
@@ -385,9 +511,14 @@ module.exports = {
385
511
  TemplatetagNode,
386
512
  WidthRatioNode,
387
513
  DebugNode,
514
+ FilterNode,
515
+ VerbatimNode,
516
+ QueryStringNode,
388
517
  parsers: {
389
518
  static: parseStatic,
390
519
  url: parseUrl,
520
+ urlpk: parseUrl,
521
+ urlslug: parseUrl,
391
522
  regroup: parseRegroup,
392
523
  spaceless: parseSpaceless,
393
524
  csrf_token: parseCsrfToken,
@@ -395,6 +526,9 @@ module.exports = {
395
526
  load: parseLoad,
396
527
  templatetag: parseTemplatetag,
397
528
  widthratio: parseWidthRatio,
398
- debug: parseDebug
529
+ debug: parseDebug,
530
+ filter: parseFilter,
531
+ verbatim: parseVerbatim,
532
+ querystring: parseQuerystring
399
533
  }
400
534
  };