Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions __tests__/plugins/formatters.core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,14 @@ loader.paths('f-json-pretty-%N.html').forEach((path) => {
test(`json pretty - ${path}`, () => loader.execute(path));
});

loader.paths('f-find-first-%N.html').forEach((path) => {
test(`find-first - ${path}`, () => loader.execute(path));
});

loader.paths('f-find-last-%N.html').forEach((path) => {
test(`find-last - ${path}`, () => loader.execute(path));
});

test('key-by', () => {
let vars = variables([{ id: 1 }]);
Core['key-by'].apply([], vars, CTX);
Expand Down
18 changes: 18 additions & 0 deletions __tests__/plugins/resources/f-find-first-1.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
:JSON
{
"items": [
{ "id": "a", "enabled": false },
{ "id": "b", "enabled": true },
{ "id": "c", "enabled": true }
]
}

:TEMPLATE
{.var @first items|find-first}{@first.id}
{.var @first items|find-first enabled}{@first.id}
{.var @first items|find-first disabled}{@first.id} {# Note: "disabled" is not a valid path, so it should return nothing #}

:OUTPUT
a
b

15 changes: 15 additions & 0 deletions __tests__/plugins/resources/f-find-first-2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
:JSON
{
"keys": ["a", "b", "c"],
"map": {
"a": { "enabled": false },
"b": { "enabled": true },
"c": { "enabled": true }
}
}

:TEMPLATE
{keys|find-first map enabled}

:OUTPUT
b
22 changes: 22 additions & 0 deletions __tests__/plugins/resources/f-find-first-3.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
:JSON
{
"strings": ["a", "b", "c"],
"empty": [],
"notArray": "not-an-array",
"objects": [{ "x": 1 }, { "x": 2 }],
"noMatch": [{ "id": "a", "enabled": false }, { "id": "b" }]
}

:TEMPLATE
{.var @r strings|find-first}{@r}
{.var @r empty|find-first}-{@r}
{.var @r notArray|find-first}-{@r}
{.var @r objects|find-first}{@r.x}
{.var @r noMatch|find-first enabled}-{@r}

:OUTPUT
a
-
-
1
-
14 changes: 14 additions & 0 deletions __tests__/plugins/resources/f-find-last-1.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
:JSON
{
"items": [
{ "id": "a", "enabled": true },
{ "id": "b", "enabled": true },
{ "id": "c", "enabled": false }
]
}

:TEMPLATE
{.var @last items|find-last enabled}{@last.id}

:OUTPUT
b
15 changes: 15 additions & 0 deletions __tests__/plugins/resources/f-find-last-2.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
:JSON
{
"keys": ["a", "b", "c"],
"map": {
"a": { "enabled": true },
"b": { "enabled": true },
"c": { "enabled": false }
}
}

:TEMPLATE
{keys|find-last map enabled}

:OUTPUT
b
22 changes: 22 additions & 0 deletions __tests__/plugins/resources/f-find-last-3.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
:JSON
{
"strings": ["a", "b", "c"],
"empty": [],
"notArray": "not-an-array",
"objects": [{ "x": 1 }, { "x": 2 }],
"noMatch": [{ "id": "a", "enabled": false }, { "id": "b" }]
}

:TEMPLATE
{.var @r strings|find-last}{@r}
{.var @r empty|find-last}-{@r}
{.var @r notArray|find-last}-{@r}
{.var @r objects|find-last}{@r.x}
{.var @r noMatch|find-last enabled}-{@r}

:OUTPUT
c
-
-
2
-
19 changes: 19 additions & 0 deletions src/plugins/formatters.core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Variable } from '../variable';
import { Type } from '../types';
import { executeTemplate } from '../exec';
import { splitVariable } from '../util';
import { findNthValidEntry, getLookupAndPath } from './util.find';
import { format } from './util.format';
import { escapeHtmlAttributes, escapeScriptTags, slugify, truncate } from './util.string';
import utf8 from 'utf8';
Expand Down Expand Up @@ -243,6 +244,22 @@ export class KeyByFormatter extends Formatter {
}
}

export class FindFirstFormatter extends Formatter {
apply(args: string[], vars: Variable[], ctx: Context): void {
const first = vars[0];
const { lookup, path } = getLookupAndPath(ctx, args);
first.set(findNthValidEntry(first.get(), path, lookup, 1));
}
}

export class FindLastFormatter extends Formatter {
apply(args: string[], vars: Variable[], ctx: Context): void {
const first = vars[0];
const { lookup, path } = getLookupAndPath(ctx, args);
first.set(findNthValidEntry(first.get(), path, lookup, -1));
}
}

const NEWLINE = /\n/g;

export class LineBreaksFormatter extends Formatter {
Expand Down Expand Up @@ -421,6 +438,8 @@ export const CORE_FORMATTERS: FormatterTable = {
'encode-space': new EncodeSpaceFormatter(),
'encode-uri': new EncodeUriFormatter(),
'encode-uri-component': new EncodeUriComponentFormatter(),
'find-first': new FindFirstFormatter(),
'find-last': new FindLastFormatter(),
format: new FormatFormatter(),
get: new GetFormatter(),
html: new HtmlFormatter(),
Expand Down
45 changes: 45 additions & 0 deletions src/plugins/util.find.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Context } from '../context';
import { MISSING_NODE, Node, isTruthy, toNode } from '../node';
import { splitVariable } from '../util';

export const getLookupAndPath = (ctx: Context, args: string[]) => {
const argsCount = args.length;
const hasLookup = argsCount === 2;
const hasPath = argsCount >= 1;
const lookup = hasLookup ? ctx.resolve(splitVariable(args[0])) : null;
const path = hasPath ? splitVariable(args[hasLookup ? 1 : 0]) : null;
return { lookup, path };
};

export const findNthValidEntry = (items: Node, path: (string | number)[] | null, lookup: Node | null, nth: number): Node => {
if (!Array.isArray(items) || items.length === 0) {
return MISSING_NODE;
}
const forward = nth > 0;
const start = forward ? 0 : items.length - 1;
const end = forward ? items.length : -1;
const step = forward ? 1 : -1;

let count = 0;
const hasLookup = lookup != null;
const hasPath = path != null;

for (let i = start; forward ? i < end : i > end; i += step) {
const node = toNode(items[i]);
if (!hasPath) {
count += step;
if (count == nth) {
return node;
}
} else {
const candidate = hasLookup ? lookup.path([node.asString()]) : node;
if (isTruthy(candidate.path(path))) {
count += step;
if (count == nth) {
return node;
}
}
}
}
return MISSING_NODE;
};
Loading