From ab97238cb63029c2927791155b10378f134d8409 Mon Sep 17 00:00:00 2001 From: Lea Rosema Date: Mon, 30 Sep 2024 17:40:03 +0200 Subject: [PATCH 1/2] fix: add safe filter --- demo/_layouts/base.html | 2 +- src/transforms/template-data.js | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) 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}; } + From 3aa78632f1005f7b49b9e421f6d84b4caca9e548 Mon Sep 17 00:00:00 2001 From: Lea Rosema Date: Mon, 30 Sep 2024 17:50:54 +0200 Subject: [PATCH 2/2] fix: fix tests and add 2 more tests --- tests/fixtures/smallsite/_layouts/base.html | 2 +- tests/transforms/template-data.test.js | 22 ++++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) 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);