diff --git a/demo/_layouts/base.html b/demo/_layouts/base.html
index 22aedbb..24c8db9 100644
--- a/demo/_layouts/base.html
+++ b/demo/_layouts/base.html
@@ -9,7 +9,7 @@
- {{ content }}
+ {{ content | safe }}
diff --git a/src/transforms/template-data.js b/src/transforms/template-data.js
index b42e775..6d91ae1 100644
--- a/src/transforms/template-data.js
+++ b/src/transforms/template-data.js
@@ -8,6 +8,14 @@ const TEMPLATE_REGEX = /\{\{\s*([\w\.\[\]]+)(?:\((.*)\))?(?:\s*\|\s([a-zA-Z*]\w*
const JSON_PATH_REGEX = /^[a-zA-Z_]\w*((?:\.\w+)|(?:\[\d+\]))*$/
const JSON_PATH_TOKEN = /(^[a-zA-Z_]\w*)|(\.[a-zA-Z_]\w*)|(\[\d+\])/g
+function mergeMaps(map1, map2) {
+ return new Map([...map1, ...map2]);
+}
+
+function htmlEscape(input) {
+ return input?.replace(/&/g, '&').replace(//g, '>');
+}
+
/**
* Poor girl's jsonpath
*
@@ -59,7 +67,11 @@ export function parseArguments(args, data) {
* @returns {(data: any, filters: Map) => string} a function that takes a data object and returns the processed template
*/
export function template(str) {
- return (data, filters) => {
+ const defaultFilters = new Map();
+ let isSafe = false;
+ defaultFilters.set('safe', (input) => { isSafe = true; return input; })
+ return (data, providedFilters) => {
+ const filters = mergeMaps(defaultFilters || new Map(), providedFilters || new Map())
return str.replace(TEMPLATE_REGEX, (_, expr, params, filter, filterParams) => {
let result = dataPath(expr)(data);
const args = parseArguments(params, data);
@@ -73,7 +85,7 @@ export function template(str) {
const filterArgs = parseArguments(filterParams, data);
result = filters.get(filter)(result, ...filterArgs);
}
- return result;
+ return isSafe ? result : htmlEscape(result);
});
}
}
@@ -123,7 +135,7 @@ export async function handleTemplateFile(config, data, inputFile) {
const l = await handleTemplateFile(config,
{...fileData, content: fileContent, layout: null}, layoutFilePath);
if (l) {
- fileContent = l.content;;
+ fileContent = l.content;
} else {
throw new Error('Layout not found:' + layoutFilePath);
}
@@ -131,3 +143,4 @@ export async function handleTemplateFile(config, data, inputFile) {
return {content: fileContent, filename: outputFile};
}
+
diff --git a/tests/fixtures/smallsite/_layouts/base.html b/tests/fixtures/smallsite/_layouts/base.html
index 22aedbb..24c8db9 100644
--- a/tests/fixtures/smallsite/_layouts/base.html
+++ b/tests/fixtures/smallsite/_layouts/base.html
@@ -9,7 +9,7 @@
- {{ content }}
+ {{ content | safe }}
diff --git a/tests/transforms/template-data.test.js b/tests/transforms/template-data.test.js
index 8efee55..5204df6 100644
--- a/tests/transforms/template-data.test.js
+++ b/tests/transforms/template-data.test.js
@@ -97,6 +97,18 @@ describe('template function', () => {
assert.equal(result, "HELLO");
});
+ it('should escape angle brackets and ampersands by default', () => {
+ const result = template('{{ content }}')({content: 'Hello
'});
+
+ assert.equal(result, '<h1>Hello</h1>')
+ });
+
+ it('should not escape angle brackets and ampersands when marked safe', () => {
+ const result = template('{{ content | safe }}')({content: 'Hello
'});
+
+ assert.equal(result, 'Hello
')
+ });
+
it('should be able to apply a filter with additional parameters', () => {
const data = { greeting: 'Hello Lea'}
const filters = new Map();
@@ -108,7 +120,7 @@ describe('template function', () => {
});
});
-describe('handleTemplateFile function', {only: true}, () => {
+describe('handleTemplateFile function', () => {
const withFrontmatter = (str, data) => `---json\n${JSON.stringify(data)}\n---\n${str}`
@@ -136,7 +148,7 @@ describe('handleTemplateFile function', {only: true}, () => {
const vFS = new Map();
vFS.set('index.html', withFrontmatter('{{ title }}
', {layout: 'base.html'}));
- vFS.set('_layouts/base.html', '{{ content }}')
+ vFS.set('_layouts/base.html', '{{ content | safe }}')
config.resolve = (...paths) => {
const resource = path.normalize(path.join(...paths));
@@ -155,7 +167,7 @@ describe('handleTemplateFile function', {only: true}, () => {
const vFS = new Map();
vFS.set('index.md', withFrontmatter(TEST_MD, {'layout': 'base.html', author: 'Lea Rosema'}));
- vFS.set('_layouts/base.html', '{{ content }}');
+ vFS.set('_layouts/base.html', '{{ content | safe }}');
config.resolve = (...paths) => {
const resource = path.normalize(path.join(...paths));
@@ -174,7 +186,7 @@ describe('handleTemplateFile function', {only: true}, () => {
const vFS = new Map();
vFS.set('index.md', withFrontmatter(TEST_MD, {'layout': 'base.html', author: 'Lea Rosema'}));
- vFS.set('_layouts/base.html', '{{ content }}');
+ vFS.set('_layouts/base.html', '{{ content | safe }}');
config.resolve = (...paths) => {
const resource = path.normalize(path.join(...paths));
@@ -206,7 +218,7 @@ describe('handleTemplateFile function', {only: true}, () => {
});
});
- it('should throw an error when a non-existant file is specified as layout', {only: true}, async () => {
+ it('should throw an error when a non-existant file is specified as layout', async () => {
const config = new SissiConfig();
config.addExtension(md);