diff --git a/README.md b/README.md
index aacdd9c..9ca5177 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,8 @@ The node can solve multiple use cases when creating content like:
* WordPress posts
* Telegram/Slack messages
* Use helpers to filter templates
+* Create custom helpers for business-specific logic
+* Dynamic content generation with complex data transformations
The sky is your limit!
@@ -140,9 +142,109 @@ Total invoice: 133.10€
I recommend using this method if you want to send multiple invoices.
## Helpers
-Now the node supports helpers thanks to the [@jaredwray/fumanchu](https://www.npmjs.com/package/@jaredwray/fumanchu#helpers) package.
-We recommend checking
+The node supports both built-in helpers and custom helpers thanks to the [@jaredwray/fumanchu](https://www.npmjs.com/package/@jaredwray/fumanchu#helpers) package.
+
+### Built-in Helpers
+The node includes a comprehensive set of built-in helpers for common operations like date formatting, string manipulation, comparisons, and more. Check the [fumanchu helpers documentation](https://www.npmjs.com/package/@jaredwray/fumanchu#helpers) for a complete list.
+
+### Custom Helpers
+You can now define your own custom Handlebars helpers to extend the templating functionality. This feature allows you to:
+- Create reusable functions for complex data transformations
+- Implement business-specific logic directly in templates
+- Load helpers from external sources for better code organization
+
+#### Enabling Custom Helpers
+1. Check the **"Use Custom Helpers"** option in the node configuration
+2. Choose your helpers source:
+ - **From Field**: Write JavaScript code directly in the node
+ - **From URL**: Load helpers from an external JavaScript file
+
+#### Custom Helpers Format
+Your custom helpers should be defined as a JavaScript module that exports an object with helper functions:
+
+```javascript
+module.exports = {
+ // String manipulation helpers
+ uppercase: function(str) {
+ return str.toUpperCase();
+ },
+
+ capitalize: function(str) {
+ return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
+ },
+
+ // Date formatting helpers
+ formatDate: function(date, format) {
+ const d = new Date(date);
+ if (format === 'short') {
+ return d.toLocaleDateString();
+ }
+ return d.toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ });
+ },
+
+ // Math helpers
+ multiply: function(a, b) {
+ return a * b;
+ },
+
+ percentage: function(value, total) {
+ return ((value / total) * 100).toFixed(2) + '%';
+ },
+
+ // Conditional helpers
+ isEven: function(number) {
+ return number % 2 === 0;
+ },
+
+ // Array helpers
+ joinWithComma: function(array) {
+ return array.join(', ');
+ }
+};
+```
+#### Loading Helpers from URL
+For better code organization, you can host your helpers in an external JavaScript file:
+
+```javascript
+// https://mydomain.com/helpers/custom-helpers.js
+module.exports = {
+ formatCurrency: function(amount, currency = 'EUR') {
+ return new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: currency
+ }).format(amount);
+ },
+
+ slugify: function(text) {
+ return text
+ .toLowerCase()
+ .replace(/[^\w\s-]/g, '')
+ .replace(/[\s_-]+/g, '-')
+ .replace(/^-+|-+$/g, '');
+ }
+};
+```
+
+Then use the URL in the node configuration: `https://mydomain.com/helpers/custom-helpers.js`
+
+#### Security and Limitations
+- Custom helpers run in a sandboxed environment for security
+- Execution timeout is set to 5 seconds to prevent infinite loops
+- Basic Node.js modules are available (console, Buffer, setTimeout, etc.)
+- Helper functions should be pure functions without side effects for best results
+
+#### Error Handling
+If there are issues with your custom helpers:
+- Syntax errors in the JavaScript code will be reported
+- Invalid helper exports will show descriptive error messages
+- Network issues when loading from URL will be handled gracefully
+
+We recommend testing your helpers thoroughly before using them in production workflows
## Doubts about templates syntax
Please, check the [official page](https://handlebarsjs.com/guide/expressions.html#basic-usage) to review all the existing expressions in Handlebars.
diff --git a/nodes/DocumentGenerator/DocumentGenerator.node.ts b/nodes/DocumentGenerator/DocumentGenerator.node.ts
index 3fea71d..00908f4 100644
--- a/nodes/DocumentGenerator/DocumentGenerator.node.ts
+++ b/nodes/DocumentGenerator/DocumentGenerator.node.ts
@@ -1,12 +1,14 @@
import { handlebars, helpers } from '@jaredwray/fumanchu';
-import { IExecuteFunctions } from 'n8n-core';
-import {
+import type { IExecuteFunctions } from 'n8n-core';
+import type {
IBinaryKeyData,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
+import { NodeOperationError } from 'n8n-workflow';
+import * as vm from 'vm';
/**
* A node which allows you to generate documents by templates.
@@ -108,6 +110,83 @@ export class DocumentGenerator implements INodeType {
description:
'The template URL to use for rendering. Please check the official page for Handlebars syntax.',
},
+ {
+ displayName: 'Use Custom Helpers',
+ name: 'useCustomHelpers',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to use custom Handlebars helpers',
+ displayOptions: {
+ show: {
+ operation: ['render'],
+ },
+ },
+ },
+ {
+ displayName: 'Helpers Source',
+ name: 'helpersSource',
+ type: 'options',
+ options: [
+ {
+ name: 'From Field',
+ value: 'field',
+ description: 'Provide helpers code directly in a field',
+ },
+ {
+ name: 'From URL',
+ value: 'url',
+ description: 'Load helpers from a URL',
+ },
+ ],
+ default: 'field',
+ displayOptions: {
+ show: {
+ useCustomHelpers: [true],
+ },
+ },
+ },
+ {
+ displayName: 'Custom Helpers Code',
+ name: 'helpersCode',
+ type: 'string',
+ required: true,
+ typeOptions: {
+ rows: 10,
+ alwaysOpenEditWindow: true,
+ },
+ displayOptions: {
+ show: {
+ useCustomHelpers: [true],
+ helpersSource: ['field'],
+ },
+ },
+ default: '',
+ placeholder: `// Example helpers
+module.exports = {
+ uppercase: function(str) {
+ return str.toUpperCase();
+ },
+ formatDate: function(date) {
+ return new Date(date).toLocaleDateString();
+ }
+};`,
+ description: 'JavaScript code that exports an object with helper functions. Each property should be a function that can be used in templates.',
+ },
+ {
+ displayName: 'Helpers URL',
+ name: 'helpersURL',
+ type: 'string',
+ required: true,
+ displayOptions: {
+ show: {
+ useCustomHelpers: [true],
+ helpersSource: ['url'],
+ },
+ },
+ default: '',
+ placeholder: 'https://mydomain.com/helpers/custom-helpers.js',
+ description: 'URL to a JavaScript file that exports an object with helper functions',
+ },
{
displayName: 'Define a Custom Output Key',
name: 'customOutputKey',
@@ -135,7 +214,7 @@ export class DocumentGenerator implements INodeType {
default: '',
placeholder: 'text',
description: 'The output property name where we save rendered text',
- }
+ },
],
};
// The execute method will go here
@@ -159,8 +238,73 @@ export class DocumentGenerator implements INodeType {
const templateURL = this.getNodeParameter('templateURL', 0) as string;
template = await this.helpers.request(templateURL);
}
+
+ // Initialize default helpers
helpers({ handlebars }, {});
+ // Load custom helpers if enabled
+ const useCustomHelpers = this.getNodeParameter('useCustomHelpers', 0) as boolean;
+ if (useCustomHelpers) {
+ try {
+ const helpersSource = this.getNodeParameter('helpersSource', 0) as string;
+ let helpersCode = '';
+
+ if (helpersSource === 'field') {
+ helpersCode = this.getNodeParameter('helpersCode', 0) as string;
+ } else if (helpersSource === 'url') {
+ const helpersURL = this.getNodeParameter('helpersURL', 0) as string;
+ helpersCode = await this.helpers.request(helpersURL);
+ }
+
+ if (helpersCode.trim()) {
+ // Create a safe execution context
+ const sandbox = {
+ module: { exports: {} },
+ exports: {},
+ require: require, // Allow require for basic Node.js modules
+ console: console,
+ Buffer: Buffer,
+ setTimeout: setTimeout,
+ clearTimeout: clearTimeout,
+ setInterval: setInterval,
+ clearInterval: clearInterval,
+ };
+
+ // Execute the helpers code in the sandbox
+ vm.createContext(sandbox);
+ vm.runInContext(helpersCode, sandbox, {
+ timeout: 5000, // 5 second timeout
+ displayErrors: true,
+ });
+
+ // Extract the exported helpers
+ const customHelpers = sandbox.module.exports || sandbox.exports;
+
+ if (typeof customHelpers === 'object' && customHelpers !== null) {
+ // Register each helper with handlebars
+ Object.keys(customHelpers).forEach((helperName) => {
+ const helper = (customHelpers as Record)[helperName];
+ if (typeof helper === 'function') {
+ handlebars.registerHelper(helperName, helper as (context?: unknown, ...args: unknown[]) => unknown);
+ }
+ });
+ } else {
+ throw new NodeOperationError(
+ this.getNode(),
+ 'Custom helpers must export an object with helper functions',
+ { itemIndex: 0 }
+ );
+ }
+ }
+ } catch (error) {
+ throw new NodeOperationError(
+ this.getNode(),
+ `Failed to load custom helpers: ${error.message}`,
+ { itemIndex: 0 }
+ );
+ }
+ }
+
const templateHelper = handlebars.compile(template);
let key = 'text';
@@ -169,20 +313,20 @@ export class DocumentGenerator implements INodeType {
}
if (oneTemplate) {
- var cleanedItems = items.map(function (item) {
+ const cleanedItems = items.map((item) => {
return item.json;
});
- let newItemJson: IDataObject = {};
+ const newItemJson: IDataObject = {};
newItemJson[key] = templateHelper({ items: cleanedItems });
returnData.push({ json: newItemJson });
} else {
for (let i = 0; i < items.length; i++) {
- let item = items[i];
+ const item = items[i];
if (operation === 'render') {
- let newItemJson: IDataObject = {};
+ const newItemJson: IDataObject = {};
// Get email input
// Get additional fields input
- var rendered = templateHelper(item.json);
+ const rendered = templateHelper(item.json);
newItemJson[key] = rendered;
returnData.push({
json: newItemJson,