Skip to content

Commit 7165caf

Browse files
authored
Added find-first and find-last formatters with corresponding tests (#36)
* Added find-first and find-last formatters with corresponding tests * created helper function for shared logic + added find-nth * removed find-nth formatter, optimized utils and converted all test to use fixtures
1 parent 0368aac commit 7165caf

9 files changed

Lines changed: 178 additions & 0 deletions

File tree

__tests__/plugins/formatters.core.test.ts

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

373+
loader.paths('f-find-first-%N.html').forEach((path) => {
374+
test(`find-first - ${path}`, () => loader.execute(path));
375+
});
376+
377+
loader.paths('f-find-last-%N.html').forEach((path) => {
378+
test(`find-last - ${path}`, () => loader.execute(path));
379+
});
380+
373381
test('key-by', () => {
374382
let vars = variables([{ id: 1 }]);
375383
Core['key-by'].apply([], vars, CTX);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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}{@first.id}
12+
{.var @first items|find-first enabled}{@first.id}
13+
{.var @first items|find-first disabled}{@first.id} {# Note: "disabled" is not a valid path, so it should return nothing #}
14+
15+
:OUTPUT
16+
a
17+
b
18+
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: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
:JSON
2+
{
3+
"strings": ["a", "b", "c"],
4+
"empty": [],
5+
"notArray": "not-an-array",
6+
"objects": [{ "x": 1 }, { "x": 2 }],
7+
"noMatch": [{ "id": "a", "enabled": false }, { "id": "b" }]
8+
}
9+
10+
:TEMPLATE
11+
{.var @r strings|find-first}{@r}
12+
{.var @r empty|find-first}-{@r}
13+
{.var @r notArray|find-first}-{@r}
14+
{.var @r objects|find-first}{@r.x}
15+
{.var @r noMatch|find-first enabled}-{@r}
16+
17+
:OUTPUT
18+
a
19+
-
20+
-
21+
1
22+
-
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
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
:JSON
2+
{
3+
"strings": ["a", "b", "c"],
4+
"empty": [],
5+
"notArray": "not-an-array",
6+
"objects": [{ "x": 1 }, { "x": 2 }],
7+
"noMatch": [{ "id": "a", "enabled": false }, { "id": "b" }]
8+
}
9+
10+
:TEMPLATE
11+
{.var @r strings|find-last}{@r}
12+
{.var @r empty|find-last}-{@r}
13+
{.var @r notArray|find-last}-{@r}
14+
{.var @r objects|find-last}{@r.x}
15+
{.var @r noMatch|find-last enabled}-{@r}
16+
17+
:OUTPUT
18+
c
19+
-
20+
-
21+
2
22+
-

src/plugins/formatters.core.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Variable } from '../variable';
88
import { Type } from '../types';
99
import { executeTemplate } from '../exec';
1010
import { splitVariable } from '../util';
11+
import { findNthValidEntry, getLookupAndPath } from './util.find';
1112
import { format } from './util.format';
1213
import { escapeHtmlAttributes, escapeScriptTags, slugify, truncate } from './util.string';
1314
import utf8 from 'utf8';
@@ -243,6 +244,22 @@ export class KeyByFormatter extends Formatter {
243244
}
244245
}
245246

247+
export class FindFirstFormatter extends Formatter {
248+
apply(args: string[], vars: Variable[], ctx: Context): void {
249+
const first = vars[0];
250+
const { lookup, path } = getLookupAndPath(ctx, args);
251+
first.set(findNthValidEntry(first.get(), path, lookup, 1));
252+
}
253+
}
254+
255+
export class FindLastFormatter extends Formatter {
256+
apply(args: string[], vars: Variable[], ctx: Context): void {
257+
const first = vars[0];
258+
const { lookup, path } = getLookupAndPath(ctx, args);
259+
first.set(findNthValidEntry(first.get(), path, lookup, -1));
260+
}
261+
}
262+
246263
const NEWLINE = /\n/g;
247264

248265
export class LineBreaksFormatter extends Formatter {
@@ -421,6 +438,8 @@ export const CORE_FORMATTERS: FormatterTable = {
421438
'encode-space': new EncodeSpaceFormatter(),
422439
'encode-uri': new EncodeUriFormatter(),
423440
'encode-uri-component': new EncodeUriComponentFormatter(),
441+
'find-first': new FindFirstFormatter(),
442+
'find-last': new FindLastFormatter(),
424443
format: new FormatFormatter(),
425444
get: new GetFormatter(),
426445
html: new HtmlFormatter(),

src/plugins/util.find.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Context } from '../context';
2+
import { MISSING_NODE, Node, isTruthy, toNode } from '../node';
3+
import { splitVariable } from '../util';
4+
5+
export const getLookupAndPath = (ctx: Context, args: string[]) => {
6+
const argsCount = args.length;
7+
const hasLookup = argsCount === 2;
8+
const hasPath = argsCount >= 1;
9+
const lookup = hasLookup ? ctx.resolve(splitVariable(args[0])) : null;
10+
const path = hasPath ? splitVariable(args[hasLookup ? 1 : 0]) : null;
11+
return { lookup, path };
12+
};
13+
14+
export const findNthValidEntry = (items: Node, path: (string | number)[] | null, lookup: Node | null, nth: number): Node => {
15+
if (!Array.isArray(items) || items.length === 0) {
16+
return MISSING_NODE;
17+
}
18+
const forward = nth > 0;
19+
const start = forward ? 0 : items.length - 1;
20+
const end = forward ? items.length : -1;
21+
const step = forward ? 1 : -1;
22+
23+
let count = 0;
24+
const hasLookup = lookup != null;
25+
const hasPath = path != null;
26+
27+
for (let i = start; forward ? i < end : i > end; i += step) {
28+
const node = toNode(items[i]);
29+
if (!hasPath) {
30+
count += step;
31+
if (count == nth) {
32+
return node;
33+
}
34+
} else {
35+
const candidate = hasLookup ? lookup.path([node.asString()]) : node;
36+
if (isTruthy(candidate.path(path))) {
37+
count += step;
38+
if (count == nth) {
39+
return node;
40+
}
41+
}
42+
}
43+
}
44+
return MISSING_NODE;
45+
};

0 commit comments

Comments
 (0)