@samuelbines/nunjucks 0.0.3 → 0.0.5

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.
@@ -1,279 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- // test/environment.test.ts
4
- const vitest_1 = require("vitest");
5
- // IMPORTANT: update this import to the file that exports Environment/Template/Context/asap/callbackAsap
6
- const environment_1 = require("../src/environment");
7
- // ---- Helpers: minimal loader stubs ----
8
- class SyncLoader {
9
- async = false;
10
- cache = {};
11
- on;
12
- sources = {};
13
- constructor(sources) {
14
- if (sources)
15
- this.sources = sources;
16
- }
17
- getSource(name) {
18
- const hit = this.sources[name];
19
- if (!hit)
20
- return null;
21
- return {
22
- src: hit.src,
23
- path: hit.path ?? name,
24
- noCache: hit.noCache ?? false,
25
- };
26
- }
27
- }
28
- class RelativeLoader extends SyncLoader {
29
- isRelative(filename) {
30
- return filename.startsWith('./') || filename.startsWith('../');
31
- }
32
- resolve(parent, filename) {
33
- // very small join; just enough for tests
34
- const base = parent.split('/').slice(0, -1).join('/');
35
- return `${base}/${filename.replace(/^\.\//, '')}`;
36
- }
37
- }
38
- // ---- Helpers: tiny code templates (no compiler involved) ----
39
- function codeTemplateReturning(text) {
40
- return {
41
- type: 'code',
42
- obj: {
43
- root(_env, _ctx, _frame, _runtime, cb) {
44
- cb(null, text);
45
- },
46
- },
47
- };
48
- }
49
- function codeTemplateCallingLookup(name) {
50
- return {
51
- type: 'code',
52
- obj: {
53
- root(_env, ctx, _frame, _runtime, cb) {
54
- cb(null, String(ctx.lookup(name)));
55
- },
56
- },
57
- };
58
- }
59
- (0, vitest_1.describe)('Environment basics', () => {
60
- (0, vitest_1.it)('addFilter/getFilter', () => {
61
- const env = new environment_1.Environment();
62
- const f = (v) => String(v).toUpperCase();
63
- env.addFilter('up', f);
64
- (0, vitest_1.expect)(env.getFilter('up')).toBe(f);
65
- (0, vitest_1.expect)(() => env.getFilter('missing')).toThrow(/filter not found/i);
66
- });
67
- (0, vitest_1.it)('addFilter with async marks asyncFilters', () => {
68
- const env = new environment_1.Environment();
69
- env.addFilter('a', () => 'x', [
70
- /*anything*/
71
- ]);
72
- (0, vitest_1.expect)(env.asyncFilters).toContain('a');
73
- });
74
- (0, vitest_1.it)('addExtension / hasExtension / getExtension / removeExtension', () => {
75
- const env = new environment_1.Environment();
76
- const ext = { foo: 1 };
77
- env.addExtension('myExt', ext);
78
- (0, vitest_1.expect)(env.hasExtension('myExt')).toBe(true);
79
- (0, vitest_1.expect)(env.getExtension('myExt')).toBe(ext);
80
- env.removeExtension('myExt');
81
- (0, vitest_1.expect)(env.hasExtension('myExt')).toBe(false);
82
- (0, vitest_1.expect)(env.getExtension('myExt')).toBeUndefined();
83
- });
84
- });
85
- (0, vitest_1.describe)('Environment.resolveTemplate', () => {
86
- (0, vitest_1.it)('uses loader.resolve for relative templates when parentName is provided', () => {
87
- const loader = new RelativeLoader();
88
- const env = new environment_1.Environment({ loaders: [loader] });
89
- const resolved = env.resolveTemplate(loader, 'pages/base.njk', './child.njk');
90
- (0, vitest_1.expect)(resolved).toBe('pages/child.njk');
91
- });
92
- (0, vitest_1.it)('returns filename as-is when not relative', () => {
93
- const loader = new RelativeLoader();
94
- const env = new environment_1.Environment({ loaders: [loader] });
95
- const resolved = env.resolveTemplate(loader, 'pages/base.njk', 'child.njk');
96
- (0, vitest_1.expect)(resolved).toBe('child.njk');
97
- });
98
- });
99
- (0, vitest_1.describe)('Environment.getTemplate (sync loaders)', () => {
100
- (0, vitest_1.it)('returns cached template when present', () => {
101
- const loader = new SyncLoader();
102
- const env = new environment_1.Environment({ loaders: [loader] });
103
- const t1 = new environment_1.Template(codeTemplateReturning('hello'), env, 't.njk', true);
104
- loader.cache['t.njk'] = t1;
105
- const got = env.getTemplate('t.njk', (e, r) => { }, {});
106
- (0, vitest_1.expect)(got).toBe(t1);
107
- });
108
- (0, vitest_1.it)('loads from loader and caches when noCache is false', () => {
109
- const loader = new SyncLoader({
110
- 'a.njk': {
111
- src: codeTemplateReturning('A'),
112
- path: 'a.njk',
113
- noCache: false,
114
- },
115
- });
116
- const env = new environment_1.Environment({ loaders: [loader] });
117
- const tmpl = env.getTemplate('a.njk', (e, r) => { }, {});
118
- (0, vitest_1.expect)(tmpl).toBeInstanceOf(environment_1.Template);
119
- // should now be cached at name
120
- (0, vitest_1.expect)(loader.cache['a.njk']).toBe(tmpl);
121
- // eagerCompile=true should have compiled
122
- (0, vitest_1.expect)(tmpl.compiled).toBe(true);
123
- // and it should render
124
- (0, vitest_1.expect)(tmpl.render({})).toBe('A');
125
- });
126
- (0, vitest_1.it)('does not cache when noCache is true', () => {
127
- const loader = new SyncLoader({
128
- 'a.njk': {
129
- src: codeTemplateReturning('A'),
130
- path: 'a.njk',
131
- noCache: true,
132
- },
133
- });
134
- const env = new environment_1.Environment({ loaders: [loader] });
135
- const tmpl = env.getTemplate('a.njk', (e, r) => { });
136
- (0, vitest_1.expect)(loader.cache['a.njk']).toBeUndefined();
137
- (0, vitest_1.expect)(tmpl.render({})).toBe('A');
138
- });
139
- (0, vitest_1.it)('throws when template missing and ignoreMissing not set', () => {
140
- const loader = new SyncLoader({});
141
- const env = new environment_1.Environment({ loaders: [loader] });
142
- (0, vitest_1.expect)(() => env.getTemplate('missing.njk', (e, r) => { })).toThrow(/template not found/i);
143
- });
144
- (0, vitest_1.it)('returns a noop template when ignoreMissing=true', () => {
145
- const loader = new SyncLoader({});
146
- const env = new environment_1.Environment({ loaders: [loader] });
147
- const tmpl = env.getTemplate('missing.njk', (e, r) => { }, {
148
- eagerCompile: false,
149
- parentName: null,
150
- ignoreMissing: true,
151
- });
152
- (0, vitest_1.expect)(tmpl).toBeInstanceOf(environment_1.Template);
153
- // should render empty string
154
- (0, vitest_1.expect)(tmpl.render({})).toBe('');
155
- });
156
- (0, vitest_1.it)('supports callback API and returns undefined', async () => {
157
- const loader = new SyncLoader({
158
- 'a.njk': { src: codeTemplateReturning('A'), path: 'a.njk' },
159
- });
160
- const env = new environment_1.Environment({ loaders: [loader] });
161
- const cb = vitest_1.vi.fn();
162
- const ret = env.getTemplate('a.njk', cb, {});
163
- (0, vitest_1.expect)(ret).toBeUndefined();
164
- // callback is invoked synchronously for sync loaders in this implementation
165
- (0, vitest_1.expect)(cb).toHaveBeenCalledTimes(1);
166
- (0, vitest_1.expect)(cb.mock.calls[0][0]).toBeNull();
167
- (0, vitest_1.expect)(cb.mock.calls[0][1]).toBeInstanceOf(environment_1.Template);
168
- });
169
- (0, vitest_1.it)('accepts Template instance directly', () => {
170
- const env = new environment_1.Environment();
171
- const t = new environment_1.Template(codeTemplateReturning('X'), env, 'x.njk', true);
172
- const got = env.getTemplate(t, (e, r) => { });
173
- (0, vitest_1.expect)(got).toBe(t);
174
- (0, vitest_1.expect)(got.render({})).toBe('X');
175
- });
176
- (0, vitest_1.it)('throws if name is not a string or Template', () => {
177
- const env = new environment_1.Environment();
178
- // @ts-expect-error
179
- (0, vitest_1.expect)(() => env.getTemplate(123, false)).toThrow(/template names must be a string/i);
180
- });
181
- });
182
- (0, vitest_1.describe)('Environment.render / renderString', () => {
183
- (0, vitest_1.it)('render returns sync string when callback not provided', () => {
184
- const loader = new SyncLoader({
185
- 'a.njk': { src: codeTemplateReturning('Hello'), path: 'a.njk' },
186
- });
187
- const env = new environment_1.Environment({ loaders: [loader] });
188
- const out = env.render('a.njk', { any: 1 });
189
- (0, vitest_1.expect)(out).toBe('Hello');
190
- });
191
- (0, vitest_1.it)('render calls callback asynchronously (callbackAsap) when no parentFrame', async () => {
192
- const loader = new SyncLoader({
193
- 'a.njk': { src: codeTemplateReturning('Hello'), path: 'a.njk' },
194
- });
195
- const env = new environment_1.Environment({ loaders: [loader] });
196
- const cb = vitest_1.vi.fn();
197
- env.render('a.njk', {}, cb);
198
- // callback should NOT have fired yet because Template.render forces async when no parentFrame
199
- (0, vitest_1.expect)(cb).not.toHaveBeenCalled();
200
- await Promise.resolve();
201
- (0, vitest_1.expect)(cb).toHaveBeenCalledTimes(1);
202
- (0, vitest_1.expect)(cb.mock.calls[0][0]).toBeNull();
203
- (0, vitest_1.expect)(cb.mock.calls[0][1]).toBe('Hello');
204
- });
205
- (0, vitest_1.it)('renderString renders from a code template object', () => {
206
- const env = new environment_1.Environment();
207
- const out = env.renderString(codeTemplateReturning('S'), {}, {});
208
- (0, vitest_1.expect)(out).toBe('S');
209
- });
210
- (0, vitest_1.it)('Context.lookup prefers globals when ctx lacks key', () => {
211
- const env = new environment_1.Environment();
212
- // env.addGlobal('g', 'GLOB');
213
- const tmpl = new environment_1.Template(codeTemplateCallingLookup('g'), env, 't.njk', true);
214
- (0, vitest_1.expect)(tmpl.render({})).toBe('GLOB');
215
- });
216
- (0, vitest_1.it)('Context.lookup prefers ctx value over globals when both exist', () => {
217
- const env = new environment_1.Environment();
218
- // env.addGlobal('g', 'GLOB');
219
- const tmpl = new environment_1.Template(codeTemplateCallingLookup('g'), env, 't.njk', true);
220
- (0, vitest_1.expect)(tmpl.render({ g: 'LOCAL' })).toBe('LOCAL');
221
- });
222
- });
223
- (0, vitest_1.describe)('Context blocks + super', () => {
224
- (0, vitest_1.it)('addBlock/getBlock', () => {
225
- const ctx = new environment_1.Context({}, {}, new environment_1.Environment());
226
- ctx.addBlock('b', () => 'x');
227
- (0, vitest_1.expect)(typeof ctx.getBlock('b')).toBe('function');
228
- (0, vitest_1.expect)(() => ctx.getBlock('missing')).toThrow(/unknown block/i);
229
- });
230
- (0, vitest_1.it)('getSuper calls the next block in the stack', () => {
231
- const env = new environment_1.Environment();
232
- const ctx = new environment_1.Context({}, {}, env);
233
- const b1 = vitest_1.vi.fn((_env, _ctx, _frame, _runtime, cb) => cb(null, 'one'));
234
- const b2 = vitest_1.vi.fn((_env, _ctx, _frame, _runtime, cb) => cb(null, 'two'));
235
- // first pushed is "top" (index 0), super should call next (index 1)
236
- ctx.addBlock('x', b1);
237
- ctx.addBlock('x', b2);
238
- const cb = vitest_1.vi.fn();
239
- // ask for super of b1 -> should run b2
240
- ctx.getSuper(env, 'x', b1, {}, {}, cb);
241
- (0, vitest_1.expect)(b2).toHaveBeenCalledTimes(1);
242
- (0, vitest_1.expect)(cb).toHaveBeenCalledWith(null, 'two');
243
- });
244
- (0, vitest_1.it)('getSuper throws when no super block exists', () => {
245
- const env = new environment_1.Environment();
246
- const ctx = new environment_1.Context({}, {}, env);
247
- const b1 = vitest_1.vi.fn();
248
- ctx.addBlock('x', b1);
249
- (0, vitest_1.expect)(() => ctx.getSuper(env, 'x', b1, {}, {}, vitest_1.vi.fn())).toThrow(/no super block available/i);
250
- });
251
- });
252
- (0, vitest_1.describe)('Template compilation from compiler (mocked)', () => {
253
- (0, vitest_1.beforeEach)(() => {
254
- vitest_1.vi.resetModules();
255
- vitest_1.vi.restoreAllMocks();
256
- });
257
- (0, vitest_1.it)('compiles string templates via compiler.compile (mocked) and renders', async () => {
258
- // We need to import a fresh copy after mocking
259
- vitest_1.vi.mock('../src/compiler', () => {
260
- return {
261
- compile: vitest_1.vi.fn(() => {
262
- // This source is executed with `new Function(source)` and must return props when invoked.
263
- // It should return an object like: { root(...) {}, b_name(...) {} }
264
- return `
265
- return function() {
266
- return {
267
- root: function(env, ctx, frame, runtime, cb) { cb(null, "FROM_COMPILER"); }
268
- };
269
- };
270
- `;
271
- }),
272
- };
273
- });
274
- const mod = await import('../src/environment');
275
- const env = new mod.Environment();
276
- const tmpl = new mod.Template('hello {{x}}', env, 't.njk', true);
277
- (0, vitest_1.expect)(tmpl.render({})).toBe('FROM_COMPILER');
278
- });
279
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,86 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const path_1 = __importDefault(require("path"));
7
- const vitest_1 = require("vitest");
8
- const express_app_1 = require("../src/express-app");
9
- (0, vitest_1.describe)('express-app.ts (express integration)', () => {
10
- (0, vitest_1.it)('registers the view class and env on the app via app.set', () => {
11
- const env = { render: vitest_1.vi.fn() };
12
- const app = { set: vitest_1.vi.fn() };
13
- const out = (0, express_app_1.express)(env, app);
14
- (0, vitest_1.expect)(out).toBe(env);
15
- // called with view ctor + env
16
- (0, vitest_1.expect)(app.set).toHaveBeenCalledWith('view', vitest_1.expect.any(Function));
17
- (0, vitest_1.expect)(app.set).toHaveBeenCalledWith('nunjucksEnv', env);
18
- });
19
- (0, vitest_1.it)('View constructor uses file extension when provided and does not change name', () => {
20
- const env = { render: vitest_1.vi.fn() };
21
- const app = { set: vitest_1.vi.fn() };
22
- (0, express_app_1.express)(env, app);
23
- const ViewCtor = app.set.mock.calls.find((c) => c[0] === 'view')[1];
24
- const view = new ViewCtor('index.njk', {
25
- name: 'index.njk',
26
- path: '',
27
- defaultEngine: 'express',
28
- ext: '',
29
- });
30
- // because you assign to closure var "name", but render uses this.name
31
- // so for correctness, the constructor should set instance fields (it currently doesn't).
32
- // This test documents current behavior: instance.name is undefined.
33
- (0, vitest_1.expect)(view.name).toBeUndefined();
34
- });
35
- (0, vitest_1.it)('render calls env.render with (this.name, opts, cb)', () => {
36
- const env = { render: vitest_1.vi.fn() };
37
- const app = { set: vitest_1.vi.fn() };
38
- (0, express_app_1.express)(env, app);
39
- const ViewCtor = app.set.mock.calls.find((c) => c[0] === 'view')[1];
40
- const view = new ViewCtor('index.njk', {
41
- name: 'index.njk',
42
- path: '',
43
- defaultEngine: 'express',
44
- ext: '',
45
- });
46
- const cb = vitest_1.vi.fn();
47
- const opts = { a: 1 };
48
- // render uses this.name (instance field)
49
- view.name = 'index.njk';
50
- view.render(opts, cb);
51
- (0, vitest_1.expect)(env.render).toHaveBeenCalledTimes(1);
52
- (0, vitest_1.expect)(env.render).toHaveBeenCalledWith('index.njk', opts, cb);
53
- });
54
- (0, vitest_1.it)('throws when there is no extension and no defaultEngine', () => {
55
- const env = { render: vitest_1.vi.fn() };
56
- const app = { set: vitest_1.vi.fn() };
57
- (0, express_app_1.express)(env, app);
58
- const ViewCtor = app.set.mock.calls.find((c) => c[0] === 'view')[1];
59
- // Your NunjucksView signature is (name, opts), but it ignores opts.defaultEngine
60
- // and uses the closure `defaultEngine = "express"` (always truthy).
61
- // So there is currently NO way to trigger this throw branch without code changes.
62
- //
63
- // This test expresses the intended behavior by constructing a small patched copy
64
- // of the constructor logic to demonstrate expected throw; but we cannot reach it
65
- // through the current public API. If you fix the impl to use opts.defaultEngine,
66
- // remove this workaround and test the real behavior.
67
- const makeThrowingCtor = () => {
68
- function NunjucksView(_name, _opts) {
69
- let name = _name;
70
- let ext = path_1.default.extname(name);
71
- const defaultEngine = ''; // falsy
72
- if (!ext && !defaultEngine) {
73
- throw new Error('No default engine was specified and no extension was provided.');
74
- }
75
- if (!ext) {
76
- name += ext = (defaultEngine[0] !== '.' ? '.' : '') + defaultEngine;
77
- }
78
- }
79
- return NunjucksView;
80
- };
81
- const Throwing = makeThrowingCtor();
82
- (0, vitest_1.expect)(() => new Throwing('index', {})).toThrow(/No default engine was specified/i);
83
- // and ViewCtor itself should not throw for "index" due to current bug
84
- (0, vitest_1.expect)(() => new ViewCtor('index', {})).not.toThrow();
85
- });
86
- });
@@ -1,13 +0,0 @@
1
- export {};
2
- /**
3
- * NOTE:
4
- * - I did NOT test escape/forceescape/safe because your `escape` filter currently calls itself recursively:
5
- * export const escape = (str) => r.markSafe(escape(str.toString()))
6
- * That will stack overflow. Once you fix it to call a real escaping function (e.g. lib.escape),
7
- * I can add tests for it too.
8
- *
9
- * - I did NOT test `batch` here because the loop in your snippet looks syntactically wrong:
10
- * for (i < arr.length; i++; )
11
- * If that’s a paste typo and your real code is valid, tell me your actual batch() implementation
12
- * and I’ll add tests for it as well.
13
- */
@@ -1,286 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- // test/filters.test.ts
4
- const vitest_1 = require("vitest");
5
- /**
6
- * These tests mock ./runtime and ./lib so we can test filter logic in isolation
7
- * without depending on your runtime safety/markSafe implementations.
8
- *
9
- * IMPORTANT: Update the import paths below if your filters file lives elsewhere.
10
- */
11
- vitest_1.vi.mock('../src/runtime', () => {
12
- return {
13
- default: {
14
- // keep it simple: "safe" handling just wraps + copySafeness returns output
15
- markSafe: (v) => ({ __safe: true, val: String(v) }),
16
- copySafeness: (_inp, out) => out,
17
- makeMacro: (_args, _kwargs, fn) => fn,
18
- },
19
- };
20
- });
21
- vitest_1.vi.mock('../src/lib', () => {
22
- const TemplateError = (msg) => new Error(msg);
23
- return {
24
- repeat: (s, n) => s.repeat(Math.max(0, Math.floor(n))),
25
- isObject: (o) => o !== null && typeof o === 'object' && !Array.isArray(o),
26
- TemplateError,
27
- isString: (v) => typeof v === 'string' || v instanceof String,
28
- _entries: (o) => Object.entries(o ?? {}),
29
- getAttrGetter: (attr) => (obj) => attr ? obj?.[attr] : obj,
30
- toArray: (v) => (Array.isArray(v) ? v : v == null ? [] : [v]),
31
- groupBy: (arr, attr, _throwOnUndefined) => {
32
- const out = {};
33
- for (const item of arr ?? []) {
34
- const k = String(item?.[attr]);
35
- (out[k] ||= []).push(item);
36
- }
37
- // jinja-style groupby returns array of {grouper, list} often,
38
- // but your filter just forwards groupBy; we return a deterministic shape for tests:
39
- return Object.entries(out).map(([grouper, list]) => ({ grouper, list }));
40
- },
41
- };
42
- });
43
- async function loadFilters() {
44
- return await import('../src/filters');
45
- }
46
- (0, vitest_1.describe)('filters.ts', () => {
47
- (0, vitest_1.beforeEach)(() => {
48
- vitest_1.vi.restoreAllMocks();
49
- });
50
- (0, vitest_1.it)('center pads evenly and uses copySafeness', async () => {
51
- const f = await loadFilters();
52
- (0, vitest_1.expect)(f.center('hi', 6)).toBe(' hi ');
53
- (0, vitest_1.expect)(f.center('already long', 3)).toBe('already long');
54
- });
55
- (0, vitest_1.it)('default_ (and alias d) chooses val/def based on bool flag', async () => {
56
- const f = await loadFilters();
57
- (0, vitest_1.expect)(f.default_(null, 'x')).toBe('x');
58
- (0, vitest_1.expect)(f.default_('a', 'x')).toBe('a');
59
- // bool=true path in your code: return def || val
60
- (0, vitest_1.expect)(f.default_(null, 'x', true)).toBe('x');
61
- (0, vitest_1.expect)(f.default_('a', '', true)).toBe('a');
62
- (0, vitest_1.expect)(f.d).toBe(f.default_);
63
- });
64
- (0, vitest_1.it)('dictsort sorts by key/value and respects caseSensitive', async () => {
65
- const f = await loadFilters();
66
- const obj = Object.create({ z: 0 });
67
- obj.B = 2;
68
- obj.a = 1;
69
- // by value default
70
- (0, vitest_1.expect)(f.dictsort(obj, true)).toEqual([
71
- ['z', 0],
72
- ['a', 1],
73
- ['B', 2],
74
- ]);
75
- // by key, case-insensitive should treat 'B' and 'a' accordingly
76
- (0, vitest_1.expect)(f.dictsort({ B: 2, a: 1 }, false, 'key')).toEqual([
77
- ['a', 1],
78
- ['B', 2],
79
- ]);
80
- (0, vitest_1.expect)(() => f.dictsort('nope', true)).toThrow(/val must be an object/i);
81
- (0, vitest_1.expect)(() => f.dictsort({ a: 1 }, true, 'nope')).toThrow(/only sort by/i);
82
- });
83
- (0, vitest_1.it)('indent indents lines, optionally skipping the first', async () => {
84
- const f = await loadFilters();
85
- (0, vitest_1.expect)(f.indent('a\nb', 2, false)).toBe('a\n b');
86
- (0, vitest_1.expect)(f.indent('a\nb', 2, true)).toBe(' a\n b');
87
- (0, vitest_1.expect)(f.indent('', 4, true)).toBe('');
88
- });
89
- (0, vitest_1.it)('join joins array, supports attr picking', async () => {
90
- const f = await loadFilters();
91
- (0, vitest_1.expect)(f.join([1, 2, 3], '-')).toBe('1-2-3');
92
- (0, vitest_1.expect)(f.join([{ x: 'a' }, { x: 'b' }], ',', 'x')).toBe('a,b');
93
- });
94
- (0, vitest_1.it)('length counts array/string/map/set; object branch currently returns 0 due to missing return', async () => {
95
- const f = await loadFilters();
96
- (0, vitest_1.expect)(f.length([1, 2, 3])).toBe(3);
97
- (0, vitest_1.expect)(f.length('abc')).toBe(3);
98
- (0, vitest_1.expect)(f.length(new Map([['a', 1]]))).toBe(1);
99
- (0, vitest_1.expect)(f.length(new Set([1, 2]))).toBe(2);
100
- // NOTE: your implementation has:
101
- // if (isObject(str)) Object.keys(str).length; // missing return
102
- // so currently it falls through and returns 0. This test locks that in (and highlights the bug).
103
- (0, vitest_1.expect)(f.length({ a: 1, b: 2 })).toBe(0);
104
- });
105
- (0, vitest_1.it)('list: string -> chars, object -> {key,value} entries, array -> itself; throws otherwise', async () => {
106
- const f = await loadFilters();
107
- (0, vitest_1.expect)(f.list('ab')).toEqual(['a', 'b']);
108
- (0, vitest_1.expect)(f.list({ a: 1, b: 2 })).toEqual([
109
- { key: 'a', value: 1 },
110
- { key: 'b', value: 2 },
111
- ]);
112
- (0, vitest_1.expect)(f.list([1, 2])).toEqual([1, 2]);
113
- (0, vitest_1.expect)(() => f.list(123)).toThrow(/type not iterable/i);
114
- });
115
- (0, vitest_1.it)('random uses Math.random (stubbed)', async () => {
116
- const f = await loadFilters();
117
- const spy = vitest_1.vi.spyOn(Math, 'random').mockReturnValue(0.6); // floor(0.6*5)=3
118
- (0, vitest_1.expect)(f.random([0, 1, 2, 3, 4])).toBe(3);
119
- spy.mockRestore();
120
- });
121
- (0, vitest_1.it)('reject/select use env.getTest + toArray', async () => {
122
- const f = await loadFilters();
123
- const ctx = {
124
- env: {
125
- getTest: (name) => {
126
- if (name === 'truthy')
127
- return (x) => !!x;
128
- if (name === 'gt')
129
- return (x, n) => x > n;
130
- throw new Error('unknown test');
131
- },
132
- },
133
- };
134
- // select expects test === true
135
- (0, vitest_1.expect)(f.select.call(ctx, [0, 1, 2], 'truthy')).toEqual([1, 2]);
136
- // reject expects test === false
137
- (0, vitest_1.expect)(f.reject.call(ctx, [0, 1, 2], 'truthy')).toEqual([0]);
138
- (0, vitest_1.expect)(f.select.call(ctx, [1, 2, 3], 'gt', 2)).toEqual([3]);
139
- (0, vitest_1.expect)(f.reject.call(ctx, [1, 2, 3], 'gt', 2)).toEqual([1, 2]);
140
- // toArray: non-array becomes [value]
141
- (0, vitest_1.expect)(f.select.call(ctx, 5, 'truthy')).toEqual([5]);
142
- });
143
- (0, vitest_1.it)('rejectattr/selectattr filter by truthiness of attr', async () => {
144
- const f = await loadFilters();
145
- const arr = [{ ok: true }, { ok: false }, {}];
146
- (0, vitest_1.expect)(f.selectattr(arr, 'ok')).toEqual([{ ok: true }]);
147
- (0, vitest_1.expect)(f.rejectattr(arr, 'ok')).toEqual([{ ok: false }, {}]);
148
- });
149
- (0, vitest_1.it)('replace supports regex replacement and string replacement with maxCount and empty old', async () => {
150
- const f = await loadFilters();
151
- (0, vitest_1.expect)(f.replace('a-b-c', /-/g, '_', 0)).toBe('a_b_c');
152
- (0, vitest_1.expect)(f.replace('aaaa', 'a', 'b', 2)).toBe('bbaa');
153
- (0, vitest_1.expect)(f.replace('aaaa', 'a', 'b', 0)).toBe('aaaa');
154
- (0, vitest_1.expect)(f.replace('aaaa', 'x', 'b', 10)).toBe('aaaa');
155
- // old === '' inserts between chars and both ends
156
- (0, vitest_1.expect)(f.replace('ab', '', '-', 99)).toBe('-a-b-');
157
- });
158
- (0, vitest_1.it)('reverse reverses arrays in-place and strings as a new string', async () => {
159
- const f = await loadFilters();
160
- const arr = [1, 2, 3];
161
- (0, vitest_1.expect)(f.reverse(arr)).toEqual([3, 2, 1]);
162
- (0, vitest_1.expect)(arr).toEqual([3, 2, 1]); // in-place
163
- (0, vitest_1.expect)(f.reverse('abc')).toBe('cba');
164
- });
165
- (0, vitest_1.it)('round supports round/floor/ceil with precision', async () => {
166
- const f = await loadFilters();
167
- (0, vitest_1.expect)(f.round(1.234, 2)).toBe(1.23);
168
- (0, vitest_1.expect)(f.round(1.235, 2)).toBe(1.24);
169
- (0, vitest_1.expect)(f.round(1.231, 2, 'ceil')).toBe(1.24);
170
- (0, vitest_1.expect)(f.round(1.239, 2, 'floor')).toBe(1.23);
171
- });
172
- (0, vitest_1.it)('slice splits into N slices and can fill', async () => {
173
- const f = await loadFilters();
174
- (0, vitest_1.expect)(f.slice([1, 2, 3, 4], 2)).toEqual([
175
- [1, 2],
176
- [3, 4],
177
- ]);
178
- // fillWith=true pushes true into later slices when i >= extra
179
- (0, vitest_1.expect)(f.slice([1, 2, 3], 2, true)).toEqual([
180
- [1, 2],
181
- [3, true],
182
- ]);
183
- });
184
- (0, vitest_1.it)('sum sums, optionally by attr, and supports start', async () => {
185
- const f = await loadFilters();
186
- (0, vitest_1.expect)(f.sum([1, 2, 3], '', 0)).toBe(6);
187
- (0, vitest_1.expect)(f.sum([{ n: 1 }, { n: 2 }], 'n', 10)).toBe(13);
188
- });
189
- (0, vitest_1.it)('sort (macro) sorts with reverse/case_sensitive/attribute and throwOnUndefined', async () => {
190
- const f = await loadFilters();
191
- const ctx1 = { env: { opts: { throwOnUndefined: false } } };
192
- (0, vitest_1.expect)(f.sort.call(ctx1, ['b', 'A', 'c'], false, false, undefined)).toEqual(['A', 'b', 'c']); // case-insensitive -> 'a','b','c'
193
- const ctx2 = { env: { opts: { throwOnUndefined: true } } };
194
- (0, vitest_1.expect)(() => f.sort.call(ctx2, [{ x: 1 }, { y: 2 }], false, false, 'x')).toThrow(/resolved to undefined/i);
195
- (0, vitest_1.expect)(f.sort.call(ctx1, [{ x: 2 }, { x: 1 }], true, false, 'x')).toEqual([
196
- { x: 2 },
197
- { x: 1 },
198
- ]); // reversed
199
- });
200
- (0, vitest_1.it)('striptags removes tags and optionally preserves linebreaks', async () => {
201
- const f = await loadFilters();
202
- const input = 'a <b>bold</b>\r\n\r\n c';
203
- (0, vitest_1.expect)(f.striptags(input, true)).toBe('a bold\n\nc');
204
- (0, vitest_1.expect)(f.striptags(input, false)).toBe('a bold c');
205
- });
206
- (0, vitest_1.it)('title/capitalize/lower/upper/isUpper', async () => {
207
- const f = await loadFilters();
208
- (0, vitest_1.expect)(f.title('hello WORLD')).toBe('Hello World');
209
- (0, vitest_1.expect)(f.capitalize('hELLO')).toBe('Hello');
210
- (0, vitest_1.expect)(f.lower('HeLLo')).toBe('hello');
211
- (0, vitest_1.expect)(f.upper('hi')).toBe('HI');
212
- (0, vitest_1.expect)(f.isUpper('HI')).toBe(true);
213
- (0, vitest_1.expect)(f.isUpper('Hi')).toBe(false);
214
- });
215
- (0, vitest_1.it)('truncate respects killwords and default end', async () => {
216
- const f = await loadFilters();
217
- (0, vitest_1.expect)(f.truncate('short', 10, false)).toBe('short');
218
- (0, vitest_1.expect)(f.truncate('hello world there', 8, false)).toBe('hello...');
219
- (0, vitest_1.expect)(f.truncate('hello world there', 8, true)).toBe('hello wo...');
220
- (0, vitest_1.expect)(f.truncate('hello world there', 8, false, '>>>')).toBe('hello>>>');
221
- });
222
- (0, vitest_1.it)('urlencode encodes strings and objects/arrays of pairs', async () => {
223
- const f = await loadFilters();
224
- (0, vitest_1.expect)(f.urlencode('a b')).toBe('a%20b');
225
- (0, vitest_1.expect)(f.urlencode({ a: 'x y', b: 1 })).toBe('a=x%20y&b=1');
226
- (0, vitest_1.expect)(f.urlencode([
227
- ['a', 'x y'],
228
- ['b', 1],
229
- ])).toBe('a=x%20y&b=1');
230
- });
231
- (0, vitest_1.it)('urlize turns urls/emails into links and supports nofollow + length', async () => {
232
- const f = await loadFilters();
233
- (0, vitest_1.expect)(f.urlize('go https://example.com now', Infinity, true)).toBe('go <a href="https://example.com" rel="nofollow">https://example.com</a> now');
234
- // www.
235
- (0, vitest_1.expect)(f.urlize('www.example.com', 7, false)).toBe('<a href="http://www.example.com">www.exa</a>');
236
- // email
237
- (0, vitest_1.expect)(f.urlize('me@test.com', Infinity, false)).toBe('<a href="mailto:me@test.com">me@test.com</a>');
238
- // tld without scheme
239
- (0, vitest_1.expect)(f.urlize('example.org', Infinity, true)).toBe('<a href="http://example.org" rel="nofollow">example.org</a>');
240
- });
241
- (0, vitest_1.it)('wordcount counts words or returns null', async () => {
242
- const f = await loadFilters();
243
- (0, vitest_1.expect)(f.wordcount('one two three')).toBe(3);
244
- (0, vitest_1.expect)(f.wordcount(' ')).toBe(null);
245
- });
246
- (0, vitest_1.it)('float/int/isInt/isFloat', async () => {
247
- const f = await loadFilters();
248
- (0, vitest_1.expect)(f.float('1.5', 9)).toBe(1.5);
249
- (0, vitest_1.expect)(f.float('nope', 9)).toBe(9);
250
- (0, vitest_1.expect)(f.isInt(2)).toBe(true);
251
- (0, vitest_1.expect)(f.isInt(2.2)).toBe(false);
252
- (0, vitest_1.expect)(f.isFloat(2.2)).toBe(true);
253
- (0, vitest_1.expect)(f.isFloat(2)).toBe(false);
254
- // int macro (makeMacro mocked to return fn)
255
- (0, vitest_1.expect)(f.int('ff', 0, 16)).toBe(255);
256
- (0, vitest_1.expect)(f.int('nope', 7, 10)).toBe(7);
257
- });
258
- (0, vitest_1.it)('trim removes leading/trailing whitespace', async () => {
259
- const f = await loadFilters();
260
- (0, vitest_1.expect)(f.trim(' a \n')).toBe('a');
261
- });
262
- (0, vitest_1.it)('first/last', async () => {
263
- const f = await loadFilters();
264
- (0, vitest_1.expect)(f.first([1, 2, 3])).toBe(1);
265
- (0, vitest_1.expect)(f.last([1, 2, 3])).toBe(3);
266
- });
267
- (0, vitest_1.it)('nl2br replaces newlines with <br />', async () => {
268
- const f = await loadFilters();
269
- (0, vitest_1.expect)(f.nl2br('a\nb')).toBe('a<br />\n<b');
270
- // NOTE: your implementation is: replace(/\r\n|\n/g, '<br />\n')
271
- // so "a\nb" -> "a<br />\nb"
272
- (0, vitest_1.expect)(f.nl2br('a\nb')).toBe('a<br />\nb');
273
- });
274
- });
275
- /**
276
- * NOTE:
277
- * - I did NOT test escape/forceescape/safe because your `escape` filter currently calls itself recursively:
278
- * export const escape = (str) => r.markSafe(escape(str.toString()))
279
- * That will stack overflow. Once you fix it to call a real escaping function (e.g. lib.escape),
280
- * I can add tests for it too.
281
- *
282
- * - I did NOT test `batch` here because the loop in your snippet looks syntactically wrong:
283
- * for (i < arr.length; i++; )
284
- * If that’s a paste typo and your real code is valid, tell me your actual batch() implementation
285
- * and I’ll add tests for it as well.
286
- */
@@ -1 +0,0 @@
1
- export {};