Skip to content

Commit 2b4bd4c

Browse files
committed
Added find-first and find-last formatters with corresponding tests
1 parent 0368aac commit 2b4bd4c

6 files changed

Lines changed: 185 additions & 0 deletions

File tree

__tests__/plugins/formatters.core.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,78 @@ loader.paths('f-json-pretty-%N.html').forEach((path) => {
370370
test(`json pretty - ${path}`, () => loader.execute(path));
371371
});
372372

373+
test('find-first', () => {
374+
// no args: return first element unconditionally
375+
let vars = variables(['a', 'b', 'c']);
376+
Core['find-first'].apply([], vars, CTX);
377+
expect(vars[0].get()).toEqual('a');
378+
379+
// empty array → missing
380+
vars = variables([]);
381+
Core['find-first'].apply([], vars, CTX);
382+
expect(vars[0].node.isMissing()).toBe(true);
383+
384+
// non-array → missing
385+
vars = variables('not-an-array');
386+
Core['find-first'].apply([], vars, CTX);
387+
expect(vars[0].node.isMissing()).toBe(true);
388+
389+
// no args: first object element
390+
vars = variables([{ x: 1 }, { x: 2 }]);
391+
Core['find-first'].apply([], vars, CTX);
392+
expect(vars[0].get()).toEqual({ x: 1 });
393+
394+
// 1 arg: find first element where path is truthy
395+
vars = variables([{ id: 'a', enabled: false }, { id: 'b', enabled: true }, { id: 'c', enabled: true }]);
396+
Core['find-first'].apply(['enabled'], vars, CTX);
397+
expect(vars[0].get()).toEqual({ id: 'b', enabled: true });
398+
399+
// 1 arg: none match → missing
400+
vars = variables([{ id: 'a', enabled: false }, { id: 'b' }]);
401+
Core['find-first'].apply(['enabled'], vars, CTX);
402+
expect(vars[0].node.isMissing()).toBe(true);
403+
});
404+
405+
loader.paths('f-find-first-%N.html').forEach((path) => {
406+
test(`find-first - ${path}`, () => loader.execute(path));
407+
});
408+
409+
test('find-last', () => {
410+
// no args: return last element unconditionally
411+
let vars = variables(['a', 'b', 'c']);
412+
Core['find-last'].apply([], vars, CTX);
413+
expect(vars[0].get()).toEqual('c');
414+
415+
// empty array → missing
416+
vars = variables([]);
417+
Core['find-last'].apply([], vars, CTX);
418+
expect(vars[0].node.isMissing()).toBe(true);
419+
420+
// non-array → missing
421+
vars = variables('not-an-array');
422+
Core['find-last'].apply([], vars, CTX);
423+
expect(vars[0].node.isMissing()).toBe(true);
424+
425+
// no args: last object element
426+
vars = variables([{ x: 1 }, { x: 2 }]);
427+
Core['find-last'].apply([], vars, CTX);
428+
expect(vars[0].get()).toEqual({ x: 2 });
429+
430+
// 1 arg: find last element where path is truthy
431+
vars = variables([{ id: 'a', enabled: true }, { id: 'b', enabled: true }, { id: 'c', enabled: false }]);
432+
Core['find-last'].apply(['enabled'], vars, CTX);
433+
expect(vars[0].get()).toEqual({ id: 'b', enabled: true });
434+
435+
// 1 arg: none match → missing
436+
vars = variables([{ id: 'a', enabled: false }, { id: 'b' }]);
437+
Core['find-last'].apply(['enabled'], vars, CTX);
438+
expect(vars[0].node.isMissing()).toBe(true);
439+
});
440+
441+
loader.paths('f-find-last-%N.html').forEach((path) => {
442+
test(`find-last - ${path}`, () => loader.execute(path));
443+
});
444+
373445
test('key-by', () => {
374446
let vars = variables([{ id: 1 }]);
375447
Core['key-by'].apply([], vars, CTX);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
:JSON
2+
{
3+
"items": [
4+
{ "id": "a", "enabled": false },
5+
{ "id": "b", "enabled": true },
6+
{ "id": "c", "enabled": true }
7+
]
8+
}
9+
10+
:TEMPLATE
11+
{.var @first items|find-first enabled}{@first.id}
12+
13+
:OUTPUT
14+
b
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
:JSON
2+
{
3+
"keys": ["a", "b", "c"],
4+
"map": {
5+
"a": { "enabled": false },
6+
"b": { "enabled": true },
7+
"c": { "enabled": true }
8+
}
9+
}
10+
11+
:TEMPLATE
12+
{keys|find-first map enabled}
13+
14+
:OUTPUT
15+
b
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
:JSON
2+
{
3+
"items": [
4+
{ "id": "a", "enabled": true },
5+
{ "id": "b", "enabled": true },
6+
{ "id": "c", "enabled": false }
7+
]
8+
}
9+
10+
:TEMPLATE
11+
{.var @last items|find-last enabled}{@last.id}
12+
13+
:OUTPUT
14+
b
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
:JSON
2+
{
3+
"keys": ["a", "b", "c"],
4+
"map": {
5+
"a": { "enabled": true },
6+
"b": { "enabled": true },
7+
"c": { "enabled": false }
8+
}
9+
}
10+
11+
:TEMPLATE
12+
{keys|find-last map enabled}
13+
14+
:OUTPUT
15+
b

src/plugins/formatters.core.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,59 @@ export class KeyByFormatter extends Formatter {
243243
}
244244
}
245245

246+
export class FindFirstFormatter extends Formatter {
247+
apply(args: string[], vars: Variable[], ctx: Context): void {
248+
const first = vars[0];
249+
if (first.node.type !== Type.ARRAY || first.node.value.length === 0) {
250+
first.set(MISSING_NODE);
251+
return;
252+
}
253+
if (args.length === 0) {
254+
first.set(new Node(first.get()[0]));
255+
return;
256+
}
257+
const hasLookup = args.length >= 2;
258+
const lookup = hasLookup ? ctx.resolve(splitVariable(args[0])) : null;
259+
const path = splitVariable(args[hasLookup ? 1 : 0]);
260+
for (const val of first.get()) {
261+
const element = new Node(val);
262+
const candidate = hasLookup ? lookup!.path([element.asString()]) : element;
263+
if (isTruthy(candidate.path(path))) {
264+
first.set(element);
265+
return;
266+
}
267+
}
268+
first.set(MISSING_NODE);
269+
}
270+
}
271+
272+
export class FindLastFormatter extends Formatter {
273+
apply(args: string[], vars: Variable[], ctx: Context): void {
274+
const first = vars[0];
275+
const arr: any[] = first.node.type === Type.ARRAY ? first.get() : null;
276+
if (!arr || arr.length === 0) {
277+
first.set(MISSING_NODE);
278+
return;
279+
}
280+
if (args.length === 0) {
281+
first.set(new Node(arr[arr.length - 1]));
282+
return;
283+
}
284+
const hasLookup = args.length >= 2;
285+
const lookup = hasLookup ? ctx.resolve(splitVariable(args[0])) : null;
286+
const path = splitVariable(args[hasLookup ? 1 : 0]);
287+
for (let i = arr.length - 1; i >= 0; i--) {
288+
const element = new Node(arr[i]);
289+
const candidate = hasLookup ? lookup!.path([element.asString()]) : element;
290+
if (isTruthy(candidate.path(path))) {
291+
first.set(element);
292+
return;
293+
}
294+
}
295+
first.set(MISSING_NODE);
296+
}
297+
}
298+
246299
const NEWLINE = /\n/g;
247300

248301
export class LineBreaksFormatter extends Formatter {
@@ -421,6 +474,8 @@ export const CORE_FORMATTERS: FormatterTable = {
421474
'encode-space': new EncodeSpaceFormatter(),
422475
'encode-uri': new EncodeUriFormatter(),
423476
'encode-uri-component': new EncodeUriComponentFormatter(),
477+
'find-first': new FindFirstFormatter(),
478+
'find-last': new FindLastFormatter(),
424479
format: new FormatFormatter(),
425480
get: new GetFormatter(),
426481
html: new HtmlFormatter(),

0 commit comments

Comments
 (0)