@jackwener/opencli 0.1.2 → 0.2.0
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/dist/browser.d.ts +1 -0
- package/dist/browser.js +20 -1
- package/dist/output.d.ts +1 -1
- package/dist/output.js +12 -8
- package/dist/pipeline/template.js +72 -15
- package/dist/pipeline/template.test.js +18 -0
- package/package.json +1 -1
- package/src/browser.ts +16 -1
- package/src/output.ts +10 -6
- package/src/pipeline/template.test.ts +18 -0
- package/src/pipeline/template.ts +70 -14
package/dist/browser.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export declare class Page implements IPage {
|
|
|
13
13
|
call(method: string, params?: Record<string, any>): Promise<any>;
|
|
14
14
|
goto(url: string): Promise<void>;
|
|
15
15
|
evaluate(js: string): Promise<any>;
|
|
16
|
+
private normalizeEval;
|
|
16
17
|
snapshot(opts?: {
|
|
17
18
|
interactive?: boolean;
|
|
18
19
|
compact?: boolean;
|
package/dist/browser.js
CHANGED
|
@@ -72,7 +72,26 @@ export class Page {
|
|
|
72
72
|
await this.call('tools/call', { name: 'browser_navigate', arguments: { url } });
|
|
73
73
|
}
|
|
74
74
|
async evaluate(js) {
|
|
75
|
-
|
|
75
|
+
// Normalize IIFE format to function format expected by MCP browser_evaluate
|
|
76
|
+
const normalized = this.normalizeEval(js);
|
|
77
|
+
return this.call('tools/call', { name: 'browser_evaluate', arguments: { function: normalized } });
|
|
78
|
+
}
|
|
79
|
+
normalizeEval(source) {
|
|
80
|
+
const s = source.trim();
|
|
81
|
+
if (!s)
|
|
82
|
+
return '() => undefined';
|
|
83
|
+
// IIFE: (async () => {...})() → wrap as () => (...)
|
|
84
|
+
if (s.startsWith('(') && s.endsWith(')()'))
|
|
85
|
+
return `() => (${s})`;
|
|
86
|
+
// Already a function/arrow
|
|
87
|
+
if (/^(async\s+)?\([^)]*\)\s*=>/.test(s))
|
|
88
|
+
return s;
|
|
89
|
+
if (/^(async\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=>/.test(s))
|
|
90
|
+
return s;
|
|
91
|
+
if (s.startsWith('function ') || s.startsWith('async function '))
|
|
92
|
+
return s;
|
|
93
|
+
// Raw expression → wrap
|
|
94
|
+
return `() => (${s})`;
|
|
76
95
|
}
|
|
77
96
|
async snapshot(opts = {}) {
|
|
78
97
|
const raw = await this.call('tools/call', { name: 'browser_snapshot', arguments: {} });
|
package/dist/output.d.ts
CHANGED
package/dist/output.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Output formatting: table, JSON, Markdown, CSV.
|
|
2
|
+
* Output formatting: table, JSON, Markdown, CSV, YAML.
|
|
3
3
|
*/
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import Table from 'cli-table3';
|
|
6
|
+
import yaml from 'js-yaml';
|
|
6
7
|
export function render(data, opts = {}) {
|
|
7
8
|
const fmt = opts.fmt ?? 'table';
|
|
8
9
|
if (data === null || data === undefined) {
|
|
@@ -20,6 +21,10 @@ export function render(data, opts = {}) {
|
|
|
20
21
|
case 'csv':
|
|
21
22
|
renderCsv(data, opts);
|
|
22
23
|
break;
|
|
24
|
+
case 'yaml':
|
|
25
|
+
case 'yml':
|
|
26
|
+
renderYaml(data);
|
|
27
|
+
break;
|
|
23
28
|
default:
|
|
24
29
|
renderTable(data, opts);
|
|
25
30
|
break;
|
|
@@ -32,19 +37,15 @@ function renderTable(data, opts) {
|
|
|
32
37
|
return;
|
|
33
38
|
}
|
|
34
39
|
const columns = opts.columns ?? Object.keys(rows[0]);
|
|
35
|
-
const header = columns.map(
|
|
40
|
+
const header = columns.map(c => capitalize(c));
|
|
36
41
|
const table = new Table({
|
|
37
42
|
head: header.map(h => chalk.bold(h)),
|
|
38
43
|
style: { head: [], border: [] },
|
|
39
44
|
wordWrap: true,
|
|
40
45
|
wrapOnWordBoundary: true,
|
|
41
|
-
colWidths: columns.map((
|
|
46
|
+
colWidths: columns.map((_c, i) => {
|
|
42
47
|
if (i === 0)
|
|
43
|
-
return
|
|
44
|
-
if (c === 'url' || c === 'description')
|
|
45
|
-
return null;
|
|
46
|
-
if (c === 'title' || c === 'name' || c === 'repo')
|
|
47
|
-
return null;
|
|
48
|
+
return 6;
|
|
48
49
|
return null;
|
|
49
50
|
}).filter(() => true),
|
|
50
51
|
});
|
|
@@ -93,6 +94,9 @@ function renderCsv(data, opts) {
|
|
|
93
94
|
}).join(','));
|
|
94
95
|
}
|
|
95
96
|
}
|
|
97
|
+
function renderYaml(data) {
|
|
98
|
+
console.log(yaml.dump(data, { sortKeys: false, lineWidth: 120, noRefs: true }));
|
|
99
|
+
}
|
|
96
100
|
function capitalize(s) {
|
|
97
101
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
98
102
|
}
|
|
@@ -4,9 +4,19 @@
|
|
|
4
4
|
export function render(template, ctx) {
|
|
5
5
|
if (typeof template !== 'string')
|
|
6
6
|
return template;
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// Full expression: entire string is a single ${{ ... }}
|
|
8
|
+
// Use [^}] to prevent matching across }} boundaries (e.g. "${{ a }}-${{ b }}")
|
|
9
|
+
const fullMatch = template.match(/^\$\{\{\s*([^}]*(?:\}[^}][^}]*)*)\s*\}\}$/);
|
|
10
|
+
if (fullMatch && !template.includes('}}-') && !template.includes('}}${{'))
|
|
9
11
|
return evalExpr(fullMatch[1].trim(), ctx);
|
|
12
|
+
// Check if the entire string is a single expression (no other text around it)
|
|
13
|
+
const singleExpr = template.match(/^\$\{\{\s*([\s\S]*?)\s*\}\}$/);
|
|
14
|
+
if (singleExpr) {
|
|
15
|
+
// Verify it's truly a single expression (no other ${{ inside)
|
|
16
|
+
const inner = singleExpr[1];
|
|
17
|
+
if (!inner.includes('${{'))
|
|
18
|
+
return evalExpr(inner.trim(), ctx);
|
|
19
|
+
}
|
|
10
20
|
return template.replace(/\$\{\{\s*(.*?)\s*\}\}/g, (_m, expr) => String(evalExpr(expr.trim(), ctx)));
|
|
11
21
|
}
|
|
12
22
|
export function evalExpr(expr, ctx) {
|
|
@@ -14,19 +24,14 @@ export function evalExpr(expr, ctx) {
|
|
|
14
24
|
const item = ctx.item ?? {};
|
|
15
25
|
const data = ctx.data;
|
|
16
26
|
const index = ctx.index ?? 0;
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const intVal = parseInt(defaultVal, 10);
|
|
26
|
-
if (!isNaN(intVal) && String(intVal) === defaultVal.trim())
|
|
27
|
-
return intVal;
|
|
28
|
-
return defaultVal.replace(/^['"]|['"]$/g, '');
|
|
29
|
-
}
|
|
27
|
+
// ── Pipe filters: expr | filter1(arg) | filter2 ──
|
|
28
|
+
// Supports: default(val), join(sep), upper, lower, truncate(n), trim, replace(old,new)
|
|
29
|
+
if (expr.includes('|') && !expr.includes('||')) {
|
|
30
|
+
const segments = expr.split('|').map(s => s.trim());
|
|
31
|
+
const mainExpr = segments[0];
|
|
32
|
+
let result = resolvePath(mainExpr, { args, item, data, index });
|
|
33
|
+
for (let i = 1; i < segments.length; i++) {
|
|
34
|
+
result = applyFilter(segments[i], result);
|
|
30
35
|
}
|
|
31
36
|
return result;
|
|
32
37
|
}
|
|
@@ -59,6 +64,58 @@ export function evalExpr(expr, ctx) {
|
|
|
59
64
|
}
|
|
60
65
|
return resolvePath(expr, { args, item, data, index });
|
|
61
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Apply a named filter to a value.
|
|
69
|
+
* Supported filters:
|
|
70
|
+
* default(val), join(sep), upper, lower, truncate(n), trim,
|
|
71
|
+
* replace(old,new), keys, length, first, last
|
|
72
|
+
*/
|
|
73
|
+
function applyFilter(filterExpr, value) {
|
|
74
|
+
const match = filterExpr.match(/^(\w+)(?:\((.+)\))?$/);
|
|
75
|
+
if (!match)
|
|
76
|
+
return value;
|
|
77
|
+
const [, name, rawArgs] = match;
|
|
78
|
+
const filterArg = rawArgs?.replace(/^['"]|['"]$/g, '') ?? '';
|
|
79
|
+
switch (name) {
|
|
80
|
+
case 'default': {
|
|
81
|
+
if (value === null || value === undefined || value === '') {
|
|
82
|
+
const intVal = parseInt(filterArg, 10);
|
|
83
|
+
if (!isNaN(intVal) && String(intVal) === filterArg.trim())
|
|
84
|
+
return intVal;
|
|
85
|
+
return filterArg;
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
case 'join':
|
|
90
|
+
return Array.isArray(value) ? value.join(filterArg || ', ') : value;
|
|
91
|
+
case 'upper':
|
|
92
|
+
return typeof value === 'string' ? value.toUpperCase() : value;
|
|
93
|
+
case 'lower':
|
|
94
|
+
return typeof value === 'string' ? value.toLowerCase() : value;
|
|
95
|
+
case 'trim':
|
|
96
|
+
return typeof value === 'string' ? value.trim() : value;
|
|
97
|
+
case 'truncate': {
|
|
98
|
+
const n = parseInt(filterArg, 10) || 50;
|
|
99
|
+
return typeof value === 'string' && value.length > n ? value.slice(0, n) + '...' : value;
|
|
100
|
+
}
|
|
101
|
+
case 'replace': {
|
|
102
|
+
if (typeof value !== 'string')
|
|
103
|
+
return value;
|
|
104
|
+
const parts = rawArgs?.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')) ?? [];
|
|
105
|
+
return parts.length >= 2 ? value.replaceAll(parts[0], parts[1]) : value;
|
|
106
|
+
}
|
|
107
|
+
case 'keys':
|
|
108
|
+
return value && typeof value === 'object' ? Object.keys(value) : value;
|
|
109
|
+
case 'length':
|
|
110
|
+
return Array.isArray(value) ? value.length : typeof value === 'string' ? value.length : value;
|
|
111
|
+
case 'first':
|
|
112
|
+
return Array.isArray(value) ? value[0] : value;
|
|
113
|
+
case 'last':
|
|
114
|
+
return Array.isArray(value) ? value[value.length - 1] : value;
|
|
115
|
+
default:
|
|
116
|
+
return value;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
62
119
|
export function resolvePath(pathStr, ctx) {
|
|
63
120
|
const args = ctx.args ?? {};
|
|
64
121
|
const item = ctx.item ?? {};
|
|
@@ -54,6 +54,24 @@ describe('evalExpr', () => {
|
|
|
54
54
|
it('resolves simple path', () => {
|
|
55
55
|
expect(evalExpr('item.title', { item: { title: 'Test' } })).toBe('Test');
|
|
56
56
|
});
|
|
57
|
+
it('applies join filter', () => {
|
|
58
|
+
expect(evalExpr('item.tags | join(,)', { item: { tags: ['a', 'b', 'c'] } })).toBe('a,b,c');
|
|
59
|
+
});
|
|
60
|
+
it('applies upper filter', () => {
|
|
61
|
+
expect(evalExpr('item.name | upper', { item: { name: 'hello' } })).toBe('HELLO');
|
|
62
|
+
});
|
|
63
|
+
it('applies lower filter', () => {
|
|
64
|
+
expect(evalExpr('item.name | lower', { item: { name: 'HELLO' } })).toBe('hello');
|
|
65
|
+
});
|
|
66
|
+
it('applies truncate filter', () => {
|
|
67
|
+
expect(evalExpr('item.text | truncate(5)', { item: { text: 'Hello World!' } })).toBe('Hello...');
|
|
68
|
+
});
|
|
69
|
+
it('chains filters', () => {
|
|
70
|
+
expect(evalExpr('item.name | upper | truncate(3)', { item: { name: 'hello' } })).toBe('HEL...');
|
|
71
|
+
});
|
|
72
|
+
it('applies length filter', () => {
|
|
73
|
+
expect(evalExpr('item.items | length', { item: { items: [1, 2, 3] } })).toBe(3);
|
|
74
|
+
});
|
|
57
75
|
});
|
|
58
76
|
describe('render', () => {
|
|
59
77
|
it('renders full expression', () => {
|
package/package.json
CHANGED
package/src/browser.ts
CHANGED
|
@@ -67,7 +67,22 @@ export class Page implements IPage {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
async evaluate(js: string): Promise<any> {
|
|
70
|
-
|
|
70
|
+
// Normalize IIFE format to function format expected by MCP browser_evaluate
|
|
71
|
+
const normalized = this.normalizeEval(js);
|
|
72
|
+
return this.call('tools/call', { name: 'browser_evaluate', arguments: { function: normalized } });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private normalizeEval(source: string): string {
|
|
76
|
+
const s = source.trim();
|
|
77
|
+
if (!s) return '() => undefined';
|
|
78
|
+
// IIFE: (async () => {...})() → wrap as () => (...)
|
|
79
|
+
if (s.startsWith('(') && s.endsWith(')()')) return `() => (${s})`;
|
|
80
|
+
// Already a function/arrow
|
|
81
|
+
if (/^(async\s+)?\([^)]*\)\s*=>/.test(s)) return s;
|
|
82
|
+
if (/^(async\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=>/.test(s)) return s;
|
|
83
|
+
if (s.startsWith('function ') || s.startsWith('async function ')) return s;
|
|
84
|
+
// Raw expression → wrap
|
|
85
|
+
return `() => (${s})`;
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
async snapshot(opts: { interactive?: boolean; compact?: boolean; maxDepth?: number; raw?: boolean } = {}): Promise<any> {
|
package/src/output.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Output formatting: table, JSON, Markdown, CSV.
|
|
2
|
+
* Output formatting: table, JSON, Markdown, CSV, YAML.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import Table from 'cli-table3';
|
|
7
|
+
import yaml from 'js-yaml';
|
|
7
8
|
|
|
8
9
|
export interface RenderOptions {
|
|
9
10
|
fmt?: string;
|
|
@@ -23,6 +24,7 @@ export function render(data: any, opts: RenderOptions = {}): void {
|
|
|
23
24
|
case 'json': renderJson(data); break;
|
|
24
25
|
case 'md': case 'markdown': renderMarkdown(data, opts); break;
|
|
25
26
|
case 'csv': renderCsv(data, opts); break;
|
|
27
|
+
case 'yaml': case 'yml': renderYaml(data); break;
|
|
26
28
|
default: renderTable(data, opts); break;
|
|
27
29
|
}
|
|
28
30
|
}
|
|
@@ -32,16 +34,14 @@ function renderTable(data: any, opts: RenderOptions): void {
|
|
|
32
34
|
if (!rows.length) { console.log(chalk.dim('(no data)')); return; }
|
|
33
35
|
const columns = opts.columns ?? Object.keys(rows[0]);
|
|
34
36
|
|
|
35
|
-
const header = columns.map(
|
|
37
|
+
const header = columns.map(c => capitalize(c));
|
|
36
38
|
const table = new Table({
|
|
37
39
|
head: header.map(h => chalk.bold(h)),
|
|
38
40
|
style: { head: [], border: [] },
|
|
39
41
|
wordWrap: true,
|
|
40
42
|
wrapOnWordBoundary: true,
|
|
41
|
-
colWidths: columns.map((
|
|
42
|
-
if (i === 0) return
|
|
43
|
-
if (c === 'url' || c === 'description') return null as any;
|
|
44
|
-
if (c === 'title' || c === 'name' || c === 'repo') return null as any;
|
|
43
|
+
colWidths: columns.map((_c, i) => {
|
|
44
|
+
if (i === 0) return 6;
|
|
45
45
|
return null as any;
|
|
46
46
|
}).filter(() => true),
|
|
47
47
|
});
|
|
@@ -91,6 +91,10 @@ function renderCsv(data: any, opts: RenderOptions): void {
|
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
function renderYaml(data: any): void {
|
|
95
|
+
console.log(yaml.dump(data, { sortKeys: false, lineWidth: 120, noRefs: true }));
|
|
96
|
+
}
|
|
97
|
+
|
|
94
98
|
function capitalize(s: string): string {
|
|
95
99
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
96
100
|
}
|
|
@@ -57,6 +57,24 @@ describe('evalExpr', () => {
|
|
|
57
57
|
it('resolves simple path', () => {
|
|
58
58
|
expect(evalExpr('item.title', { item: { title: 'Test' } })).toBe('Test');
|
|
59
59
|
});
|
|
60
|
+
it('applies join filter', () => {
|
|
61
|
+
expect(evalExpr('item.tags | join(,)', { item: { tags: ['a', 'b', 'c'] } })).toBe('a,b,c');
|
|
62
|
+
});
|
|
63
|
+
it('applies upper filter', () => {
|
|
64
|
+
expect(evalExpr('item.name | upper', { item: { name: 'hello' } })).toBe('HELLO');
|
|
65
|
+
});
|
|
66
|
+
it('applies lower filter', () => {
|
|
67
|
+
expect(evalExpr('item.name | lower', { item: { name: 'HELLO' } })).toBe('hello');
|
|
68
|
+
});
|
|
69
|
+
it('applies truncate filter', () => {
|
|
70
|
+
expect(evalExpr('item.text | truncate(5)', { item: { text: 'Hello World!' } })).toBe('Hello...');
|
|
71
|
+
});
|
|
72
|
+
it('chains filters', () => {
|
|
73
|
+
expect(evalExpr('item.name | upper | truncate(3)', { item: { name: 'hello' } })).toBe('HEL...');
|
|
74
|
+
});
|
|
75
|
+
it('applies length filter', () => {
|
|
76
|
+
expect(evalExpr('item.items | length', { item: { items: [1, 2, 3] } })).toBe(3);
|
|
77
|
+
});
|
|
60
78
|
});
|
|
61
79
|
|
|
62
80
|
describe('render', () => {
|
package/src/pipeline/template.ts
CHANGED
|
@@ -11,8 +11,17 @@ export interface RenderContext {
|
|
|
11
11
|
|
|
12
12
|
export function render(template: any, ctx: RenderContext): any {
|
|
13
13
|
if (typeof template !== 'string') return template;
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
// Full expression: entire string is a single ${{ ... }}
|
|
15
|
+
// Use [^}] to prevent matching across }} boundaries (e.g. "${{ a }}-${{ b }}")
|
|
16
|
+
const fullMatch = template.match(/^\$\{\{\s*([^}]*(?:\}[^}][^}]*)*)\s*\}\}$/);
|
|
17
|
+
if (fullMatch && !template.includes('}}-') && !template.includes('}}${{')) return evalExpr(fullMatch[1].trim(), ctx);
|
|
18
|
+
// Check if the entire string is a single expression (no other text around it)
|
|
19
|
+
const singleExpr = template.match(/^\$\{\{\s*([\s\S]*?)\s*\}\}$/);
|
|
20
|
+
if (singleExpr) {
|
|
21
|
+
// Verify it's truly a single expression (no other ${{ inside)
|
|
22
|
+
const inner = singleExpr[1];
|
|
23
|
+
if (!inner.includes('${{')) return evalExpr(inner.trim(), ctx);
|
|
24
|
+
}
|
|
16
25
|
return template.replace(/\$\{\{\s*(.*?)\s*\}\}/g, (_m, expr) => String(evalExpr(expr.trim(), ctx)));
|
|
17
26
|
}
|
|
18
27
|
|
|
@@ -22,18 +31,14 @@ export function evalExpr(expr: string, ctx: RenderContext): any {
|
|
|
22
31
|
const data = ctx.data;
|
|
23
32
|
const index = ctx.index ?? 0;
|
|
24
33
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const intVal = parseInt(defaultVal!, 10);
|
|
34
|
-
if (!isNaN(intVal) && String(intVal) === defaultVal!.trim()) return intVal;
|
|
35
|
-
return defaultVal!.replace(/^['"]|['"]$/g, '');
|
|
36
|
-
}
|
|
34
|
+
// ── Pipe filters: expr | filter1(arg) | filter2 ──
|
|
35
|
+
// Supports: default(val), join(sep), upper, lower, truncate(n), trim, replace(old,new)
|
|
36
|
+
if (expr.includes('|') && !expr.includes('||')) {
|
|
37
|
+
const segments = expr.split('|').map(s => s.trim());
|
|
38
|
+
const mainExpr = segments[0];
|
|
39
|
+
let result = resolvePath(mainExpr, { args, item, data, index });
|
|
40
|
+
for (let i = 1; i < segments.length; i++) {
|
|
41
|
+
result = applyFilter(segments[i], result);
|
|
37
42
|
}
|
|
38
43
|
return result;
|
|
39
44
|
}
|
|
@@ -66,6 +71,57 @@ export function evalExpr(expr: string, ctx: RenderContext): any {
|
|
|
66
71
|
return resolvePath(expr, { args, item, data, index });
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Apply a named filter to a value.
|
|
76
|
+
* Supported filters:
|
|
77
|
+
* default(val), join(sep), upper, lower, truncate(n), trim,
|
|
78
|
+
* replace(old,new), keys, length, first, last
|
|
79
|
+
*/
|
|
80
|
+
function applyFilter(filterExpr: string, value: any): any {
|
|
81
|
+
const match = filterExpr.match(/^(\w+)(?:\((.+)\))?$/);
|
|
82
|
+
if (!match) return value;
|
|
83
|
+
const [, name, rawArgs] = match;
|
|
84
|
+
const filterArg = rawArgs?.replace(/^['"]|['"]$/g, '') ?? '';
|
|
85
|
+
|
|
86
|
+
switch (name) {
|
|
87
|
+
case 'default': {
|
|
88
|
+
if (value === null || value === undefined || value === '') {
|
|
89
|
+
const intVal = parseInt(filterArg, 10);
|
|
90
|
+
if (!isNaN(intVal) && String(intVal) === filterArg.trim()) return intVal;
|
|
91
|
+
return filterArg;
|
|
92
|
+
}
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
case 'join':
|
|
96
|
+
return Array.isArray(value) ? value.join(filterArg || ', ') : value;
|
|
97
|
+
case 'upper':
|
|
98
|
+
return typeof value === 'string' ? value.toUpperCase() : value;
|
|
99
|
+
case 'lower':
|
|
100
|
+
return typeof value === 'string' ? value.toLowerCase() : value;
|
|
101
|
+
case 'trim':
|
|
102
|
+
return typeof value === 'string' ? value.trim() : value;
|
|
103
|
+
case 'truncate': {
|
|
104
|
+
const n = parseInt(filterArg, 10) || 50;
|
|
105
|
+
return typeof value === 'string' && value.length > n ? value.slice(0, n) + '...' : value;
|
|
106
|
+
}
|
|
107
|
+
case 'replace': {
|
|
108
|
+
if (typeof value !== 'string') return value;
|
|
109
|
+
const parts = rawArgs?.split(',').map(s => s.trim().replace(/^['"]|['"]$/g, '')) ?? [];
|
|
110
|
+
return parts.length >= 2 ? value.replaceAll(parts[0], parts[1]) : value;
|
|
111
|
+
}
|
|
112
|
+
case 'keys':
|
|
113
|
+
return value && typeof value === 'object' ? Object.keys(value) : value;
|
|
114
|
+
case 'length':
|
|
115
|
+
return Array.isArray(value) ? value.length : typeof value === 'string' ? value.length : value;
|
|
116
|
+
case 'first':
|
|
117
|
+
return Array.isArray(value) ? value[0] : value;
|
|
118
|
+
case 'last':
|
|
119
|
+
return Array.isArray(value) ? value[value.length - 1] : value;
|
|
120
|
+
default:
|
|
121
|
+
return value;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
69
125
|
export function resolvePath(pathStr: string, ctx: RenderContext): any {
|
|
70
126
|
const args = ctx.args ?? {};
|
|
71
127
|
const item = ctx.item ?? {};
|