Skip to content

Commit

Permalink
feat: add basic built-in filters
Browse files Browse the repository at this point in the history
  • Loading branch information
learosema committed Oct 3, 2024
1 parent ea60c88 commit 565d8ea
Show file tree
Hide file tree
Showing 4 changed files with 179 additions and 6 deletions.
102 changes: 102 additions & 0 deletions src/builtin-filters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Serialize JSON
* @param {any} arg any object to be serialized
* @param {boolean} pretty whether to indent json
* @returns
*/
export function json(arg, pretty = false) {
return pretty? JSON.stringify(arg, null, 2) : JSON.stringify(arg)
}

/**
* Format date
* @param {string|Date|number} date
* @param {Intl.DateTimeFormatOptions} options
* @param {Intl.LocalesArgument} locales the locale (default: 'en-US')
* @returns {string} formatted date
*/
export function date(date, options = null, locales = 'en-US') {
return new Intl.DateTimeFormat(locales, options).format(typeof date === 'string' ? Date.parse(date): date)
}

/**
* Format as currency.
* @param {number} amount
* @param {string?} currency
* @param {Intl.LocalesArgument} locales
* @returns
*/
export function currency(amount, currency = 'usd', locales = 'en-US') {
return new Intl.NumberFormat(locales, {style: 'currency', currency}).format(amount);
}

/**
* Format number
* @param {number} value
* @param {Intl.NumberFormatOptions} options
* @param {Intl.LocalesArgument} locales
* @returns {string} formatted number
*/
export function numberFormat(value, options, locales) {
return new Intl.NumberFormat(locales || getEnvLocale(), options).format(value);
}

/**
* Select a first N elements of an array
* @param {Iterable} array
* @param {number} limit
* @returns
*/
export function limit(array, limit) {
if (limit < 0) {
throw new Error(`Negative limits are not allowed: ${limit}.`);
}
return Array.from(array).slice(0, limit);
};

/**
* Copy an array and reverse
* @param {Iterable} array
* @returns new array, revered
*/
export function reverse(array) {
return Array.from(array).reverse();
}

/**
* Return a sorted copy of an array
* @param {Iterable} array
* @returns new array, sorted
*/
export function sort(array) {
const result = Array.from(array);
result.sort();
return result;
}

/**
* Return the last N elements, in reverse order
* @param {Iterable} array
* @param {number} amount
*/
export function last(amount = 1) {
return Array.from(array).reverse().slice(0, amount);
}

/**
* Escape HTML (replace angle brackets and ampersands with entities)
* @param {string} str
* @returns escaped html
*/
export function htmlentities(str) {
return str.replace(/\&/, '&amp;').replace(/</gm, '&lt;').replace(/>/gm, '&gt;');
}

/**
* URL-encode
* @param {string} str
* @returns encoded string
*/
export function urlencode(str) {
return encodeURIComponent(str);
}
3 changes: 2 additions & 1 deletion src/sissi-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import css from "./css.js";
import html from "./html.js";
import md from "./md.js";
import { defaultNaming } from "./naming.js";
import * as builtinFilters from './builtin-filters.js';

export class SissiConfig {

Expand All @@ -18,7 +19,7 @@ export class SissiConfig {

templateFormats = new Map();
extensions = new Map();
filters = new Map();
filters = new Map(Object.entries(builtinFilters));

constructor(options = null) {
this.addPlugin(html);
Expand Down
11 changes: 6 additions & 5 deletions src/transforms/template-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ function mergeMaps(map1, map2) {
}

function htmlEscape(input) {
return input?.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
return input?.toString().replace(/\&/g, '&amp;').replace(/\</g, '&lt;').replace(/\>/g, '&gt;');
}

/**
Expand Down Expand Up @@ -54,8 +54,9 @@ export function parseArguments(args, data) {
}
try {
return JSON.parse(arg)
} catch (_err) {
return null;
} catch (err) {
console.error('error parsing JSON:', err.message);
return [];
}
});
}
Expand Down Expand Up @@ -83,7 +84,7 @@ export function template(str) {
if (filter && filters instanceof Map &&
filters.has(filter) && typeof filters.get(filter) === 'function') {
const filterArgs = parseArguments(filterParams, data);
result = filters.get(filter)(result, ...filterArgs);
result = filters.get(filter)(result, ...(filterArgs||[]));
}
return isSafe ? result : htmlEscape(result);
});
Expand Down Expand Up @@ -141,7 +142,7 @@ export async function handleTemplateFile(config, data, inputFile) {

const processor = await plugin.compile(body, inputFile);

let fileContent = template(await processor(fileData))(fileData);
let fileContent = template(await processor(fileData))(fileData, config.filters);

if (fileData.layout) {
const layoutFilePath = path.normalize(path.join(config.dir.layouts, fileData.layout));
Expand Down
69 changes: 69 additions & 0 deletions tests/builtin-filters.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, it } from 'node:test'
import assert from 'node:assert/strict';
import path from 'node:path'
import { handleTemplateFile } from '../src/transforms/template-data.js';
import { SissiConfig } from '../src/sissi-config.js';
describe('builtin filters', () => {

const dummyResolver = (map) => (...paths) => map.get(path.normalize(path.join(...paths)));

it('should format numbers', async () => {
const config = new SissiConfig();

const vFS = new Map();
vFS.set('index.html', '{{ thousandPi | numberFormat: numberFormatOptions, "de-DE" }}');

config.resolve = dummyResolver(vFS);

const data = { thousandPi: Math.PI * 1e3, numberFormatOptions: {maximumFractionDigits: 2, minimumFractionDigits: 2} };

const result = await handleTemplateFile(config, data, 'index.html');

assert.equal(result.content, '3.141,59');
});

it('should format currencies', async () => {
const config = new SissiConfig();

const vFS = new Map();
vFS.set('index.html', '{{ million | currency: "eur", "de-DE" }}');

config.resolve = dummyResolver(vFS);

const data = { million: 1e6 };

const result = await handleTemplateFile(config, data, 'index.html');

assert.equal(result.content, '1.000.000,00 €');
});

it('should format dates', async () => {
const config = new SissiConfig();

const vFS = new Map();
vFS.set('index.html', '{{ newYear | date: dateFormatOptions, "de-DE" }}');

config.resolve = dummyResolver(vFS);

const data = { newYear: new Date('2025-04-01'), dateFormatOptions: {"day": "2-digit", "month": "2-digit", "year": "numeric"} };

const result = await handleTemplateFile(config, data, 'index.html');

assert.equal(result.content, '01.04.2025');
});

it('should serialize json', async () => {
const config = new SissiConfig();

const vFS = new Map();
vFS.set('index.html', '{{ answer | json }}');

config.resolve = dummyResolver(vFS);

const data = { answer: {result: 42} };

const result = await handleTemplateFile(config, data, 'index.html');

assert.equal(result.content, '{"result":42}');
});
});

0 comments on commit 565d8ea

Please sign in to comment.