n8n-nodes-script-runner 1.16.0 → 1.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,29 @@
1
1
  # n8n Script Runner Node
2
2
 
3
- A custom n8n node that allows you to run custom JavaScript scripts with **jsdom** or **cheerio** for HTML parsing and manipulation.
3
+ A custom n8n node package that includes:
4
+ - **Script Runner**: Run custom JavaScript scripts with **jsdom** or **cheerio** for HTML parsing
5
+ - **HTTP Request Runner**: Execute HTTP requests with axios
6
+ - **LinkedIn Fetcher**: Fetch LinkedIn data via Unipile API
7
+ - **HTML to PDF Converter**: Convert HTML content or URLs to PDF files
4
8
 
5
9
  ## Features
6
10
 
11
+ ### Script Runner
7
12
  - ⚡ Fast HTML parsing with Cheerio (jQuery-like syntax)
8
13
  - 🌐 Full DOM implementation with jsdom
9
14
  - 🔧 Run custom JavaScript code within n8n workflows
10
15
  - 📝 Access to input items and HTML content
11
16
  - 🔄 Process multiple items in batch
12
17
 
18
+ ### HTML to PDF Converter
19
+ - 📄 Convert HTML strings to PDF files
20
+ - 🌐 Convert web pages (URLs) to PDF
21
+ - ⚙️ Configurable page format (A4, A3, Letter, Legal, Tabloid)
22
+ - 🎨 Support for custom orientations
23
+ - 💾 Output as binary data or base64 string
24
+ - 🚀 Lightweight - no browser required (uses PDFKit)
25
+ - 📝 Extracts headings and paragraphs from HTML
26
+
13
27
  ## Installation
14
28
 
15
29
  ### Community Node Installation (Recommended)
@@ -28,7 +42,48 @@ A custom n8n node that allows you to run custom JavaScript scripts with **jsdom*
28
42
  5. Build: `npm run build`
29
43
  6. Restart n8n
30
44
 
31
- ## Usage Examples
45
+ ## HTML to PDF Converter
46
+
47
+ ### Example 1: Convert HTML String to PDF
48
+
49
+ Configure the node with:
50
+ - **HTML Source**: HTML String
51
+ - **HTML Content**: Your HTML code
52
+ - **Output Format**: Binary Data
53
+ - **File Name**: output.pdf
54
+
55
+ ### Example 2: Convert Web Page to PDF
56
+
57
+ Configure the node with:
58
+ - **HTML Source**: URL
59
+ - **URL**: https://example.com
60
+ - **Output Format**: Binary Data
61
+ - **Page Format**: A4
62
+ - **Landscape**: false
63
+
64
+ ### Example 3: Custom Margins and Settings
65
+
66
+ ```json
67
+ {
68
+ "htmlSource": "string",
69
+ "htmlContent": "<html><body><h1>Invoice</h1><p>Total: $100</p></body></html>",
70
+ "outputFormat": "binary",
71
+ "fileName": "invoice.pdf",
72
+ "pageFormat": "Letter",
73
+ "margin": {
74
+ "top": "20mm",
75
+ "right": "15mm",
76
+ "bottom": "20mm",
77
+ "left": "15mm"
78
+ },
79
+ "printBackground": true,
80
+ "landscape": false
81
+ }
82
+ ```
83
+
84
+ The PDF will be available as binary data that can be saved to disk or sent via email in subsequent nodes.
85
+
86
+ ## Script Runner - Usage Examples
32
87
 
33
88
  ### Example 1: Extract Text with Cheerio
34
89
 
@@ -92,7 +147,50 @@ const $ = cheerio.load($item.json.html || html);
92
147
 
93
148
  // Extract data
94
149
  const title = $('h1').first().text();
95
- const description = $('meta[name="description"]').attr('content');
150
+ conHTML to PDF Converter
151
+
152
+ ### Example 1: Convert HTML String to PDF
153
+
154
+ Configure the node with:
155
+ - **HTML Source**: HTML String
156
+ - **HTML Content**: Your HTML code
157
+ - **Output Format**: Binary Data
158
+ - **File Name**: output.pdf
159
+
160
+ ### Example 2: Convert Web Page to PDF
161
+
162
+ Configure the node with:
163
+ - **HTML Source**: URL
164
+ - **URL**: https://example.com
165
+ - **Output Format**: Binary Data
166
+ - **Page Format**: A4
167
+ - **Landscape**: false
168
+
169
+ ### Example 3: Custom Margins and Settings
170
+
171
+ ```json
172
+ {
173
+ "htmlSource": "string",
174
+ "htmlContent": "<html><body><h1>Invoice</h1><p>Total: $100</p></body></html>",
175
+ "outputFormat": "binary",
176
+ "fileName": "invoice.pdf",
177
+ "pageFormat": "Letter",
178
+ "margin": {
179
+ "top": "20mm",
180
+ "right": "15mm",
181
+ "bottom": "20mm",
182
+ "left": "15mm"
183
+ },
184
+ "printBackground": true,
185
+ "landscape": false
186
+ }
187
+ ```
188
+
189
+ The PDF will be available as binary data that can be saved to disk or sent via email in subsequent nodes.
190
+
191
+ ## Script Runner Usage Examples
192
+
193
+ ### Example 1: Extract Text with Cheerio'meta[name="description"]').attr('content');
96
194
 
97
195
  return {
98
196
  url,
@@ -0,0 +1,5 @@
1
+ import { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
2
+ export declare class HtmlToPdfConverter implements INodeType {
3
+ description: INodeTypeDescription;
4
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
5
+ }
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.HtmlToPdfConverter = void 0;
7
+ const n8n_workflow_1 = require("n8n-workflow");
8
+ const pdfkit_1 = __importDefault(require("pdfkit"));
9
+ const html_to_text_1 = require("html-to-text");
10
+ const jsdom_1 = require("jsdom");
11
+ const axios_1 = __importDefault(require("axios"));
12
+ class HtmlToPdfConverter {
13
+ constructor() {
14
+ this.description = {
15
+ displayName: 'HTML to PDF Converter',
16
+ name: 'htmlToPdfConverter',
17
+ icon: 'file:pdf.svg',
18
+ group: ['transform'],
19
+ version: 1,
20
+ description: 'Convert HTML content to PDF files',
21
+ defaults: {
22
+ name: 'HTML to PDF',
23
+ },
24
+ inputs: ['main'],
25
+ outputs: ['main'],
26
+ properties: [
27
+ {
28
+ displayName: 'HTML Source',
29
+ name: 'htmlSource',
30
+ type: 'options',
31
+ options: [
32
+ {
33
+ name: 'HTML String',
34
+ value: 'string',
35
+ },
36
+ {
37
+ name: 'URL',
38
+ value: 'url',
39
+ },
40
+ ],
41
+ default: 'string',
42
+ description: 'Source of the HTML content',
43
+ },
44
+ {
45
+ displayName: 'HTML Content',
46
+ name: 'htmlContent',
47
+ type: 'string',
48
+ typeOptions: {
49
+ rows: 10,
50
+ },
51
+ displayOptions: {
52
+ show: {
53
+ htmlSource: ['string'],
54
+ },
55
+ },
56
+ default: '',
57
+ placeholder: '<html><body><h1>Hello World</h1></body></html>',
58
+ description: 'The HTML content to convert to PDF',
59
+ required: true,
60
+ },
61
+ {
62
+ displayName: 'URL',
63
+ name: 'url',
64
+ type: 'string',
65
+ displayOptions: {
66
+ show: {
67
+ htmlSource: ['url'],
68
+ },
69
+ },
70
+ default: '',
71
+ placeholder: 'https://example.com',
72
+ description: 'URL of the webpage to convert to PDF',
73
+ required: true,
74
+ },
75
+ {
76
+ displayName: 'Output Format',
77
+ name: 'outputFormat',
78
+ type: 'options',
79
+ options: [
80
+ {
81
+ name: 'Binary Data',
82
+ value: 'binary',
83
+ description: 'Return PDF as binary data (recommended)',
84
+ },
85
+ {
86
+ name: 'Base64 String',
87
+ value: 'base64',
88
+ description: 'Return PDF as base64 encoded string',
89
+ },
90
+ ],
91
+ default: 'binary',
92
+ description: 'How to return the PDF data',
93
+ },
94
+ {
95
+ displayName: 'File Name',
96
+ name: 'fileName',
97
+ type: 'string',
98
+ default: 'output.pdf',
99
+ description: 'Name for the output PDF file',
100
+ required: true,
101
+ },
102
+ {
103
+ displayName: 'Page Format',
104
+ name: 'pageFormat',
105
+ type: 'options',
106
+ options: [
107
+ { name: 'A4', value: 'A4' },
108
+ { name: 'A3', value: 'A3' },
109
+ { name: 'Letter', value: 'Letter' },
110
+ { name: 'Legal', value: 'Legal' },
111
+ { name: 'Tabloid', value: 'Tabloid' },
112
+ ],
113
+ default: 'A4',
114
+ description: 'Paper format for the PDF',
115
+ },
116
+ {
117
+ displayName: 'Margin',
118
+ name: 'margin',
119
+ type: 'json',
120
+ default: '{"top": "10mm", "right": "10mm", "bottom": "10mm", "left": "10mm"}',
121
+ description: 'Page margins (JSON format with top, right, bottom, left)',
122
+ },
123
+ {
124
+ displayName: 'Print Background',
125
+ name: 'printBackground',
126
+ type: 'boolean',
127
+ default: true,
128
+ description: 'Whether to print background graphics',
129
+ },
130
+ {
131
+ displayName: 'Landscape',
132
+ name: 'landscape',
133
+ type: 'boolean',
134
+ default: false,
135
+ description: 'Whether to use landscape orientation',
136
+ },
137
+ {
138
+ displayName: 'Wait for Network Idle',
139
+ name: 'waitForNetworkIdle',
140
+ type: 'boolean',
141
+ default: true,
142
+ description: 'Whether to wait for network requests to finish before generating PDF',
143
+ },
144
+ ],
145
+ };
146
+ }
147
+ async execute() {
148
+ const items = this.getInputData();
149
+ const returnData = [];
150
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
151
+ try {
152
+ const htmlSource = this.getNodeParameter('htmlSource', itemIndex);
153
+ const outputFormat = this.getNodeParameter('outputFormat', itemIndex);
154
+ const fileName = this.getNodeParameter('fileName', itemIndex);
155
+ const pageFormat = this.getNodeParameter('pageFormat', itemIndex);
156
+ const landscape = this.getNodeParameter('landscape', itemIndex);
157
+ let htmlContent = '';
158
+ if (htmlSource === 'url') {
159
+ const url = this.getNodeParameter('url', itemIndex);
160
+ if (!url) {
161
+ throw new Error('URL is required when HTML Source is set to URL');
162
+ }
163
+ const response = await axios_1.default.get(url);
164
+ htmlContent = response.data;
165
+ }
166
+ else {
167
+ htmlContent = this.getNodeParameter('htmlContent', itemIndex);
168
+ if (!htmlContent) {
169
+ throw new Error('HTML Content is required when HTML Source is set to HTML String');
170
+ }
171
+ }
172
+ // Parse HTML with JSDOM
173
+ const dom = new jsdom_1.JSDOM(htmlContent);
174
+ const document = dom.window.document;
175
+ // Extract text content
176
+ const textContent = (0, html_to_text_1.convert)(htmlContent, {
177
+ wordwrap: 130,
178
+ preserveNewlines: true,
179
+ });
180
+ // Create PDF
181
+ const pdfBuffer = await new Promise((resolve, reject) => {
182
+ const chunks = [];
183
+ // Define page sizes
184
+ const pageSizes = {
185
+ 'A4': [595.28, 841.89],
186
+ 'A3': [841.89, 1190.55],
187
+ 'Letter': [612, 792],
188
+ 'Legal': [612, 1008],
189
+ 'Tabloid': [792, 1224],
190
+ };
191
+ const size = pageSizes[pageFormat] || pageSizes['A4'];
192
+ const doc = new pdfkit_1.default({
193
+ size: landscape ? [size[1], size[0]] : size,
194
+ margin: 50,
195
+ });
196
+ doc.on('data', (chunk) => chunks.push(chunk));
197
+ doc.on('end', () => resolve(Buffer.concat(chunks)));
198
+ doc.on('error', reject);
199
+ // Add title from HTML
200
+ const title = document.querySelector('title')?.textContent || 'Document';
201
+ doc.fontSize(20).text(title, { align: 'center' });
202
+ doc.moveDown();
203
+ // Add headings and paragraphs
204
+ const headings = document.querySelectorAll('h1, h2, h3, h4, h5, h6');
205
+ const paragraphs = document.querySelectorAll('p');
206
+ if (headings.length > 0 || paragraphs.length > 0) {
207
+ headings.forEach((heading) => {
208
+ const level = parseInt(heading.tagName.substring(1));
209
+ const fontSize = 24 - (level * 2);
210
+ doc.font('Helvetica-Bold').fontSize(fontSize).text(heading.textContent || '');
211
+ doc.font('Helvetica');
212
+ doc.moveDown(0.5);
213
+ });
214
+ paragraphs.forEach((para) => {
215
+ doc.fontSize(12).text(para.textContent || '');
216
+ doc.moveDown();
217
+ });
218
+ }
219
+ else {
220
+ // Fallback to plain text conversion
221
+ doc.fontSize(12).text(textContent);
222
+ }
223
+ doc.end();
224
+ });
225
+ if (outputFormat === 'binary') {
226
+ const binaryData = await this.helpers.prepareBinaryData(pdfBuffer, fileName, 'application/pdf');
227
+ returnData.push({
228
+ json: {
229
+ success: true,
230
+ fileName,
231
+ size: pdfBuffer.length,
232
+ },
233
+ binary: {
234
+ data: binaryData,
235
+ },
236
+ pairedItem: { item: itemIndex },
237
+ });
238
+ }
239
+ else {
240
+ const base64String = pdfBuffer.toString('base64');
241
+ returnData.push({
242
+ json: {
243
+ success: true,
244
+ fileName,
245
+ size: pdfBuffer.length,
246
+ pdfData: base64String,
247
+ },
248
+ pairedItem: { item: itemIndex },
249
+ });
250
+ }
251
+ }
252
+ catch (err) {
253
+ const error = err;
254
+ if (this.continueOnFail()) {
255
+ returnData.push({
256
+ json: {
257
+ success: false,
258
+ error: error.message || 'PDF generation failed',
259
+ },
260
+ pairedItem: { item: itemIndex },
261
+ });
262
+ continue;
263
+ }
264
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { itemIndex });
265
+ }
266
+ }
267
+ return [returnData];
268
+ }
269
+ }
270
+ exports.HtmlToPdfConverter = HtmlToPdfConverter;
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2
+ <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/>
3
+ <polyline points="14 2 14 8 20 8"/>
4
+ <text x="7" y="17" font-size="6" font-weight="bold" fill="currentColor" font-family="Arial">PDF</text>
5
+ </svg>
@@ -9,7 +9,7 @@ const axios_1 = __importDefault(require("axios"));
9
9
  function randomDelayMs(minSeconds = 45, maxSeconds = 90) {
10
10
  return Math.floor(Math.random() * (maxSeconds - minSeconds + 1) + minSeconds) * 1000;
11
11
  }
12
- async function fetchLinkedIn(accountId, url, totalLimit, apiKey, minDelaySec, maxDelaySec, startCursor = null, collected = [], seenIds = new Set(), consecutiveEmptyPages = 0, pageCount = 0) {
12
+ async function fetchLinkedIn(DSN, accountId, url, totalLimit, apiKey, minDelaySec, maxDelaySec, startCursor = null, collected = [], seenIds = new Set(), consecutiveEmptyPages = 0, pageCount = 0) {
13
13
  const MAX_PAGES = 25;
14
14
  const MAX_CONSECUTIVE_EMPTY = 1;
15
15
  const PAGE_SIZE = 100;
@@ -34,7 +34,7 @@ async function fetchLinkedIn(accountId, url, totalLimit, apiKey, minDelaySec, ma
34
34
  });
35
35
  const config = {
36
36
  method: 'POST',
37
- url: `https://api15.unipile.com:14554/api/v1/linkedin/search?${queryParams.toString()}`,
37
+ url: `https://${DSN}/api/v1/linkedin/search?${queryParams.toString()}`,
38
38
  headers: {
39
39
  'X-API-KEY': apiKey,
40
40
  'Accept': 'application/json',
@@ -129,7 +129,7 @@ async function fetchLinkedIn(accountId, url, totalLimit, apiKey, minDelaySec, ma
129
129
  this.logger.info(`Added ${addedThisPage} new items → total ${collected.length}/${totalLimit}. ` +
130
130
  `Waiting ~${Math.round(delayMs / 1000)} seconds...`);
131
131
  await new Promise(resolve => setTimeout(resolve, delayMs));
132
- return fetchLinkedIn.call(this, accountId, url, totalLimit, apiKey, minDelaySec, maxDelaySec, nextCursor, collected, seenIds, consecutiveEmptyPages, pageCount + 1);
132
+ return fetchLinkedIn.call(this, DSN, accountId, url, totalLimit, apiKey, minDelaySec, maxDelaySec, nextCursor, collected, seenIds, consecutiveEmptyPages, pageCount + 1);
133
133
  }
134
134
  }
135
135
  catch (err) {
@@ -175,6 +175,14 @@ class LinkedInFetcher {
175
175
  inputs: ['main'],
176
176
  outputs: ['main'],
177
177
  properties: [
178
+ {
179
+ displayName: 'DSN',
180
+ name: 'DSN',
181
+ type: 'string',
182
+ default: 'api15.unipile.com:14554',
183
+ placeholder: 'e.g. api15.unipile.com:14554',
184
+ description: 'Custom DNS address for the Unipile API',
185
+ },
178
186
  {
179
187
  displayName: 'Account ID',
180
188
  name: 'accountId',
@@ -238,6 +246,7 @@ class LinkedInFetcher {
238
246
  const returnData = [];
239
247
  for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
240
248
  try {
249
+ const DSN = this.getNodeParameter('DSN', itemIndex, 'api15.unipile.com:14554');
241
250
  const accountId = this.getNodeParameter('accountId', itemIndex);
242
251
  const url = this.getNodeParameter('url', itemIndex)?.trim();
243
252
  const totalLimit = this.getNodeParameter('total', itemIndex);
@@ -252,7 +261,7 @@ class LinkedInFetcher {
252
261
  }
253
262
  if (startCursor === '' || startCursor === 'null')
254
263
  startCursor = null;
255
- const result = await fetchLinkedIn.call(this, accountId, url, total, apiKey, minDelay, maxDelay, startCursor);
264
+ const result = await fetchLinkedIn.call(this, DSN, accountId, url, total, apiKey, minDelay, maxDelay, startCursor);
256
265
  returnData.push({
257
266
  json: {
258
267
  result: result.items,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-script-runner",
3
- "version": "1.16.0",
3
+ "version": "1.16.2",
4
4
  "description": "Custom n8n nodes for script execution (jsdom, cheerio) and HTTP requests (axios, fetch)",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -29,21 +29,26 @@
29
29
  "nodes": [
30
30
  "dist/nodes/ScriptRunner/ScriptRunner.node.js",
31
31
  "dist/nodes/HttpRequestRunner/HttpRequestRunner.node.js",
32
- "dist/nodes/LinkedInFetcher/LinkedInFetcher.node.js"
32
+ "dist/nodes/LinkedInFetcher/LinkedInFetcher.node.js",
33
+ "dist/nodes/HtmlToPdfConverter/HtmlToPdfConverter.node.js"
33
34
  ]
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/jsdom": "^27.0.0",
37
38
  "@types/node": "^20.10.0",
39
+ "@types/pdfkit": "^0.13.0",
38
40
  "@types/user-agents": "^1.0.4",
39
41
  "gulp": "^4.0.2",
40
42
  "n8n-workflow": "^1.0.0",
41
43
  "typescript": "^5.3.0"
42
44
  },
43
45
  "dependencies": {
46
+ "@types/html-to-text": "^9.0.4",
44
47
  "axios": "^1.6.0",
45
48
  "cheerio": "^1.0.0-rc.12",
49
+ "html-to-text": "^9.0.5",
46
50
  "jsdom": "^23.0.0",
51
+ "pdfkit": "^0.15.0",
47
52
  "user-agents": "^1.1.669"
48
53
  },
49
54
  "author": "",