Skip to content

Commit

Permalink
feat(filter): add filter unique (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
wenfw authored Feb 5, 2025
1 parent a778034 commit 5f87eb2
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 7 deletions.
83 changes: 82 additions & 1 deletion packages/environment/src/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1562,6 +1562,87 @@ export const tojson = nunjucksFunction(["o", "indent"])(function tojson(
);
});

function syncUnique(
evalCtx: EvalContext,
value: unknown,
caseSensitive: boolean = false,
attribute: string | number | null = null,
): unknown[] {
let arr = syncList(value);

if (attribute !== null) {
arr = arr.map(syncMakeAttrGetter(evalCtx.environment, attribute));
}

const res: unknown[] = [];

for (const item of arr) {
if (
!res.some((x) =>
caseSensitive ? x === item : ignoreCase(x) === ignoreCase(item),
)
)
res.push(item);
}

return res;
}

async function asyncUnique(
evalCtx: EvalContext,
value: unknown,
caseSensitive: boolean = false,
attribute: string | number | null = null,
): Promise<unknown[]> {
const arr = await asyncList(value);

if (attribute !== null) {
const attrGetter = asyncMakeAttrGetter(evalCtx.environment, attribute);
for (let i = 0; i < arr.length; i++) {
arr[i] = await attrGetter(arr[i]);
}
}
return syncUnique(evalCtx, arr, caseSensitive);
}

const doUnique: {
(
evalCtx: EvalContext<true>,
value: unknown,
caseSensitive: boolean,
attribute: string | number | null,
): Promise<unknown>;
(
evalCtx: EvalContext<false>,
value: unknown,
caseSensitive: boolean,
attribute: string | number | null,
): unknown;
} = (
evalCtx: EvalContext,
value: unknown,
caseSensitive: boolean = false,
attribute: string | number | null = null,
): any => {
return evalCtx.isAsync()
? asyncUnique(evalCtx, value, caseSensitive, attribute)
: syncUnique(evalCtx, value, caseSensitive, attribute);
};

/**
* Returns a list of unique items from the given iterable.
*
* @param value The iterable to get unique items from.
* @param case_sensitive Treat upper and lower case strings as distinct.
* @param attribute Filter objects with unique values for this attribute.
*/
export const unique = nunjucksFunction(
["value", "case_sensitive", "attribute"],
{
passArg: "evalContext",
},
)(doUnique);

export default {
abs,
// attr,
Expand Down Expand Up @@ -1609,7 +1690,7 @@ export default {
title,
trim,
truncate,
// unique,
unique,
upper,
urlencode,
urlize,
Expand Down
35 changes: 34 additions & 1 deletion test/asyncFilters.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { Environment } from "@nunjucks/environment";
import { markSafe } from "@nunjucks/runtime";
import { markSafe, str } from "@nunjucks/runtime";
import { describe, expect, it } from "@jest/globals";

class Magic {
value: any;
constructor(value: any) {
this.value = value;
}
toString() {
return str(this.value);
}
}

async function* asyncGen<T>(
iter: Iterable<T>,
): AsyncGenerator<T, void, unknown> {
Expand Down Expand Up @@ -171,4 +181,27 @@ describe("filters in async environment", () => {
);
expect(await tmpl.render({ users })).toBe("john|jane");
});

describe("unique", () => {
it("basic", async () => {
const items = () => asyncGen(["b", "A", "a", "b"]);
const tmpl = env.fromString("{{ items()|unique|join }}");
expect(await tmpl.render({ items })).toBe("bA");
});

it("case sensitive", async () => {
const items = () => asyncGen("bAab");
const tmpl = env.fromString("{{ items()|unique(true)|join }}");
expect(await tmpl.render({ items })).toBe("bAa");
});

it("attribute", async () => {
const items = () =>
asyncGen([3, 2, 4, 1, 2].map((val) => new Magic(val)));
const tmpl = env.fromString(
"{{ items()|unique(attribute='value')|join }}",
);
expect(await tmpl.render({ items })).toBe("3241");
});
});
});
10 changes: 5 additions & 5 deletions test/filters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,17 +562,17 @@ describe("filters", () => {
});
});

describe.skip("unique", () => {
describe("unique", () => {
it("basic", () => {
const tmpl = env.fromString('{{ "".join(["b", "A", "a", "b"]|unique) }}');
const tmpl = env.fromString('{{ ["b", "A", "a", "b"]|unique|join }}');
expect(tmpl.render()).toBe("bA");
});

it("case sensitive", () => {
const tmpl = env.fromString(
'{{ "".join(["b", "A", "a", "b"]|unique(true)) }}',
);
const tmpl = env.fromString('{{ "bAab"|unique(true)|join }}');
expect(tmpl.render()).toBe("bAa");
});

it("attribute", () => {
const items = [3, 2, 4, 1, 2].map((val) => new Magic(val));
const tmpl = env.fromString("{{ items|unique(attribute='value')|join }}");
Expand Down

1 comment on commit 5f87eb2

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines Statements Branches Functions
Coverage: 73%
72.93% (4531/6212) 60.49% (1749/2891) 69.84% (836/1197)

Please sign in to comment.