Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 104 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!

Expand Down Expand Up @@ -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.
Expand Down
160 changes: 152 additions & 8 deletions nodes/DocumentGenerator/DocumentGenerator.node.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -108,6 +110,83 @@ export class DocumentGenerator implements INodeType {
description:
'The template URL to use for rendering. Please check the <a href="https://handlebarsjs.com/guide/expressions.html#basic-usage">official page</a> 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',
Expand Down Expand Up @@ -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
Expand All @@ -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<string, unknown>)[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';
Expand All @@ -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,
Expand Down