first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.html",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Core Nodes"],
|
||||
"resources": {
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.html/"
|
||||
}
|
||||
]
|
||||
},
|
||||
"subcategories": {
|
||||
"Core Nodes": ["Data Transformation"]
|
||||
},
|
||||
"alias": ["extract", "template", "table"]
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
import cheerio from 'cheerio';
|
||||
import get from 'lodash/get';
|
||||
import type {
|
||||
INodeExecutionData,
|
||||
IExecuteFunctions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
IDataObject,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { getResolvables, sanitizeDataPathKey } from '@utils/utilities';
|
||||
|
||||
import { placeholder } from './placeholder';
|
||||
import type { IValueData } from './types';
|
||||
import { getValue } from './utils';
|
||||
|
||||
export const capitalizeHeader = (header: string, capitalize?: boolean) => {
|
||||
if (!capitalize) return header;
|
||||
return header
|
||||
.split('_')
|
||||
.filter((word) => word)
|
||||
.map((word) => word[0].toUpperCase() + word.slice(1))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
const extractionValuesCollection: INodeProperties = {
|
||||
displayName: 'Extraction Values',
|
||||
name: 'extractionValues',
|
||||
placeholder: 'Add Value',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
},
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'values',
|
||||
displayName: 'Values',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Key',
|
||||
name: 'key',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The key under which the extracted value should be saved',
|
||||
},
|
||||
{
|
||||
displayName: 'CSS Selector',
|
||||
name: 'cssSelector',
|
||||
type: 'string',
|
||||
default: '',
|
||||
placeholder: '.price',
|
||||
description: 'The CSS selector to use',
|
||||
},
|
||||
{
|
||||
displayName: 'Return Value',
|
||||
name: 'returnValue',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Attribute',
|
||||
value: 'attribute',
|
||||
description: 'Get an attribute value like "class" from an element',
|
||||
},
|
||||
{
|
||||
name: 'HTML',
|
||||
value: 'html',
|
||||
description: 'Get the HTML the element contains',
|
||||
},
|
||||
{
|
||||
name: 'Text',
|
||||
value: 'text',
|
||||
description: 'Get only the text content of the element',
|
||||
},
|
||||
{
|
||||
name: 'Value',
|
||||
value: 'value',
|
||||
description: 'Get value of an input, select or textarea',
|
||||
},
|
||||
],
|
||||
default: 'text',
|
||||
description: 'What kind of data should be returned',
|
||||
},
|
||||
{
|
||||
displayName: 'Attribute',
|
||||
name: 'attribute',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
returnValue: ['attribute'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'class',
|
||||
description: 'The name of the attribute to return the value off',
|
||||
},
|
||||
{
|
||||
displayName: 'Skip Selectors',
|
||||
name: 'skipSelectors',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
show: {
|
||||
returnValue: ['text'],
|
||||
'@version': [{ _cnd: { gt: 1.1 } }],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
placeholder: 'e.g. img, .className, #ItemId',
|
||||
description: 'Comma-separated list of selectors to skip in the text extraction',
|
||||
},
|
||||
{
|
||||
displayName: 'Return Array',
|
||||
name: 'returnArray',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
'Whether to return the values as an array so if multiple ones get found they also get returned separately. If not set all will be returned as a single string.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export class Html implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'HTML',
|
||||
name: 'html',
|
||||
icon: { light: 'file:html.svg', dark: 'file:html.dark.svg' },
|
||||
group: ['transform'],
|
||||
version: [1, 1.1, 1.2],
|
||||
subtitle: '={{ $parameter["operation"] }}',
|
||||
description: 'Work with HTML',
|
||||
defaults: {
|
||||
name: 'HTML',
|
||||
},
|
||||
inputs: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
parameterPane: 'wide',
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{
|
||||
name: 'Generate HTML Template',
|
||||
value: 'generateHtmlTemplate',
|
||||
action: 'Generate HTML template',
|
||||
},
|
||||
{
|
||||
name: 'Extract HTML Content',
|
||||
value: 'extractHtmlContent',
|
||||
action: 'Extract HTML Content',
|
||||
},
|
||||
{
|
||||
name: 'Convert to HTML Table',
|
||||
value: 'convertToHtmlTable',
|
||||
action: 'Convert to HTML Table',
|
||||
},
|
||||
],
|
||||
default: 'generateHtmlTemplate',
|
||||
},
|
||||
{
|
||||
displayName: 'HTML Template',
|
||||
name: 'html',
|
||||
typeOptions: {
|
||||
editor: 'htmlEditor',
|
||||
},
|
||||
type: 'string',
|
||||
default: placeholder,
|
||||
noDataExpression: true,
|
||||
description: 'HTML template to render',
|
||||
builderHint: {
|
||||
message:
|
||||
'Use expressions to generate loops, reference data, etc. Does not support handlebars.',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['generateHtmlTemplate'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName:
|
||||
'<b>Tips</b>: Type ctrl+space for completions. Use <code>{{ }}</code> for expressions and <code><style></code> tags for CSS. JS in <code><script></code> tags is included but not executed in n8n.',
|
||||
name: 'notice',
|
||||
type: 'notice',
|
||||
default: '',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['generateHtmlTemplate'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Source Data',
|
||||
name: 'sourceData',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'Binary',
|
||||
value: 'binary',
|
||||
},
|
||||
{
|
||||
name: 'JSON',
|
||||
value: 'json',
|
||||
},
|
||||
],
|
||||
default: 'json',
|
||||
description: 'If HTML should be read from binary or JSON data',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['extractHtmlContent'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Input Binary Field',
|
||||
name: 'dataPropertyName',
|
||||
type: 'string',
|
||||
requiresDataPath: 'single',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['extractHtmlContent'],
|
||||
sourceData: ['binary'],
|
||||
},
|
||||
},
|
||||
default: 'data',
|
||||
required: true,
|
||||
hint: 'The name of the input binary field containing the file to be extracted',
|
||||
},
|
||||
{
|
||||
displayName: 'JSON Property',
|
||||
name: 'dataPropertyName',
|
||||
type: 'string',
|
||||
requiresDataPath: 'single',
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['extractHtmlContent'],
|
||||
sourceData: ['json'],
|
||||
},
|
||||
},
|
||||
default: 'data',
|
||||
required: true,
|
||||
description:
|
||||
'Name of the JSON property in which the HTML to extract the data from can be found. The property can either contain a string or an array of strings.',
|
||||
},
|
||||
{
|
||||
...extractionValuesCollection,
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['extractHtmlContent'],
|
||||
'@version': [1],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
...extractionValuesCollection,
|
||||
default: {
|
||||
values: [
|
||||
{
|
||||
key: '',
|
||||
cssSelector: '',
|
||||
returnValue: 'text',
|
||||
returnArray: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['extractHtmlContent'],
|
||||
'@version': [{ _cnd: { gt: 1 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['extractHtmlContent'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Trim Values',
|
||||
name: 'trimValues',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to remove automatically all spaces and newlines from the beginning and end of the values',
|
||||
},
|
||||
{
|
||||
displayName: 'Clean Up Text',
|
||||
name: 'cleanUpText',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description:
|
||||
'Whether to remove leading and trailing whitespaces, line breaks (newlines) and condense multiple consecutive whitespaces into a single space',
|
||||
},
|
||||
],
|
||||
},
|
||||
// ----------------------------------
|
||||
// convertToHtmlTable
|
||||
// ----------------------------------
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: ['convertToHtmlTable'],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
displayName: 'Capitalize Headers',
|
||||
name: 'capitalize',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to capitalize the headers',
|
||||
},
|
||||
{
|
||||
displayName: 'Custom Styling',
|
||||
name: 'customStyling',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to use custom styling',
|
||||
},
|
||||
{
|
||||
displayName: 'Caption',
|
||||
name: 'caption',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Caption to add to the table',
|
||||
},
|
||||
{
|
||||
displayName: 'Table Attributes',
|
||||
name: 'tableAttributes',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Attributes to attach to the table',
|
||||
placeholder: 'e.g. style="padding:10px"',
|
||||
},
|
||||
{
|
||||
displayName: 'Header Attributes',
|
||||
name: 'headerAttributes',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Attributes to attach to the table header',
|
||||
placeholder: 'e.g. style="padding:10px"',
|
||||
},
|
||||
{
|
||||
displayName: 'Row Attributes',
|
||||
name: 'rowAttributes',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Attributes to attach to the table row',
|
||||
placeholder: 'e.g. style="padding:10px"',
|
||||
},
|
||||
{
|
||||
displayName: 'Cell Attributes',
|
||||
name: 'cellAttributes',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Attributes to attach to the table cell',
|
||||
placeholder: 'e.g. style="padding:10px"',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const items = this.getInputData();
|
||||
const operation = this.getNodeParameter('operation', 0);
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
|
||||
if (operation === 'convertToHtmlTable' && items.length) {
|
||||
let table = '';
|
||||
|
||||
const options = this.getNodeParameter('options', 0);
|
||||
|
||||
let tableStyle = '';
|
||||
let headerStyle = '';
|
||||
let cellStyle = '';
|
||||
|
||||
if (!options.customStyling) {
|
||||
tableStyle = "style='border-spacing:0; font-family:helvetica,arial,sans-serif'";
|
||||
headerStyle =
|
||||
"style='margin:0; padding:7px 20px 7px 0px; border-bottom:1px solid #eee; text-align:left; color:#888; font-weight:normal'";
|
||||
cellStyle = "style='margin:0; padding:7px 20px 7px 0px; border-bottom:1px solid #eee'";
|
||||
}
|
||||
|
||||
const tableAttributes = (options.tableAttributes as string) || '';
|
||||
const headerAttributes = (options.headerAttributes as string) || '';
|
||||
|
||||
const itemsData: IDataObject[] = [];
|
||||
const itemsKeys = new Set<string>();
|
||||
|
||||
for (const entry of items) {
|
||||
itemsData.push(entry.json);
|
||||
|
||||
for (const key of Object.keys(entry.json)) {
|
||||
itemsKeys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
const headers = Array.from(itemsKeys);
|
||||
|
||||
table += `<table ${tableStyle} ${tableAttributes}>`;
|
||||
|
||||
if (options.caption) {
|
||||
table += `<caption>${options.caption}</caption>`;
|
||||
}
|
||||
|
||||
table += `<thead ${headerStyle} ${headerAttributes}>`;
|
||||
table += '<tr>';
|
||||
table += headers
|
||||
.map((header) => '<th>' + capitalizeHeader(header, options.capitalize as boolean) + '</th>')
|
||||
.join('');
|
||||
table += '</tr>';
|
||||
table += '</thead>';
|
||||
|
||||
table += '<tbody>';
|
||||
itemsData.forEach((entry, entryIndex) => {
|
||||
const rowsAttributes = this.getNodeParameter(
|
||||
'options.rowAttributes',
|
||||
entryIndex,
|
||||
'',
|
||||
) as string;
|
||||
|
||||
table += `<tr ${rowsAttributes}>`;
|
||||
|
||||
const cellsAttributes = this.getNodeParameter(
|
||||
'options.cellAttributes',
|
||||
entryIndex,
|
||||
'',
|
||||
) as string;
|
||||
|
||||
table += headers
|
||||
.map((header) => {
|
||||
let td = `<td ${cellStyle} ${cellsAttributes}>`;
|
||||
|
||||
if (typeof entry[header] === 'boolean') {
|
||||
const isChecked = entry[header] ? 'checked="checked"' : '';
|
||||
td += `<input type="checkbox" ${isChecked}/>`;
|
||||
} else {
|
||||
td += entry[header];
|
||||
}
|
||||
td += '</td>';
|
||||
return td;
|
||||
})
|
||||
.join('');
|
||||
table += '</tr>';
|
||||
});
|
||||
|
||||
table += '</tbody>';
|
||||
table += '</table>';
|
||||
|
||||
return [
|
||||
[
|
||||
{
|
||||
json: { table },
|
||||
pairedItem: items.map((_item, index) => ({
|
||||
item: index,
|
||||
})),
|
||||
},
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
let item: INodeExecutionData;
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
try {
|
||||
if (operation === 'generateHtmlTemplate') {
|
||||
// ----------------------------------
|
||||
// generateHtmlTemplate
|
||||
// ----------------------------------
|
||||
|
||||
let html = this.getNodeParameter('html', itemIndex) as string;
|
||||
|
||||
for (const resolvable of getResolvables(html)) {
|
||||
html = html.replace(
|
||||
resolvable,
|
||||
this.evaluateExpression(resolvable, itemIndex) as string,
|
||||
);
|
||||
}
|
||||
|
||||
const result = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray({ html }),
|
||||
{
|
||||
itemData: { item: itemIndex },
|
||||
},
|
||||
);
|
||||
|
||||
returnData.push(...result);
|
||||
} else if (operation === 'extractHtmlContent') {
|
||||
// ----------------------------------
|
||||
// extractHtmlContent
|
||||
// ----------------------------------
|
||||
|
||||
const dataPropertyName = this.getNodeParameter('dataPropertyName', itemIndex);
|
||||
const extractionValues = this.getNodeParameter(
|
||||
'extractionValues',
|
||||
itemIndex,
|
||||
) as IDataObject;
|
||||
const options = this.getNodeParameter('options', itemIndex, {});
|
||||
const sourceData = this.getNodeParameter('sourceData', itemIndex) as string;
|
||||
|
||||
item = items[itemIndex];
|
||||
|
||||
let htmlArray: string[] | string = [];
|
||||
if (sourceData === 'json') {
|
||||
if (nodeVersion === 1) {
|
||||
const key = sanitizeDataPathKey(item.json, dataPropertyName);
|
||||
if (item.json[key] === undefined) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`No property named "${dataPropertyName}" exists!`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
htmlArray = item.json[key] as string;
|
||||
} else {
|
||||
const value = get(item.json, dataPropertyName);
|
||||
if (value === undefined) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`No property named "${dataPropertyName}" exists!`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
htmlArray = value as string;
|
||||
}
|
||||
} else {
|
||||
this.helpers.assertBinaryData(itemIndex, dataPropertyName);
|
||||
const binaryDataBuffer = await this.helpers.getBinaryDataBuffer(
|
||||
itemIndex,
|
||||
dataPropertyName,
|
||||
);
|
||||
htmlArray = binaryDataBuffer.toString('utf-8');
|
||||
}
|
||||
|
||||
// Convert it always to array that it works with a string or an array of strings
|
||||
if (!Array.isArray(htmlArray)) {
|
||||
htmlArray = [htmlArray];
|
||||
}
|
||||
|
||||
for (const html of htmlArray) {
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
const newItem: INodeExecutionData = {
|
||||
json: {},
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
};
|
||||
|
||||
// Iterate over all the defined values which should be extracted
|
||||
let htmlElement;
|
||||
for (const valueData of extractionValues.values as IValueData[]) {
|
||||
htmlElement = $(valueData.cssSelector);
|
||||
|
||||
if (valueData.returnArray) {
|
||||
// An array should be returned so iterate over one
|
||||
// value at a time
|
||||
newItem.json[valueData.key] = [];
|
||||
htmlElement.each((_, el) => {
|
||||
(newItem.json[valueData.key] as Array<string | undefined>).push(
|
||||
getValue($(el), valueData, options, nodeVersion),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
// One single value should be returned
|
||||
newItem.json[valueData.key] = getValue(
|
||||
htmlElement,
|
||||
valueData,
|
||||
options,
|
||||
nodeVersion,
|
||||
);
|
||||
}
|
||||
}
|
||||
returnData.push(newItem);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({
|
||||
json: {
|
||||
error: error.message,
|
||||
},
|
||||
pairedItem: {
|
||||
item: itemIndex,
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.64062 0H10.4375V1.78125H12.0937V0H13.8906V5.39062H12.0937V3.59375H10.4531V5.39062H8.64062M16.2656 1.79687H14.6797V0H19.6562V1.79687H18.0625V5.39062H16.2656M20.4453 0H22.3281L23.4844 1.89844L24.6406 0H26.5234V5.39062H24.7266V2.71875L23.4687 4.65625L22.2109 2.71875V5.39062H20.4453M27.4141 0H29.2109V3.60937H31.7578V5.39062H27.4141" fill="white"/>
|
||||
<path d="M8.57812 36.7969L6 7.85938H34.3437L31.7656 36.7812L20.1484 40" fill="#E44D26"/>
|
||||
<path d="M20.1719 37.5391V10.2344H31.7578L29.5469 34.9219" fill="#F16529"/>
|
||||
<path d="M11.2656 13.7734H20.1719V17.3203H15.1562L15.4844 20.9531H20.1719V24.4922H12.2344M12.3906 26.2734H15.9531L16.2031 29.1094L20.1719 30.1719V33.875L12.8906 31.8437" fill="#EBEBEB"/>
|
||||
<path d="M29.0469 13.7734H20.1562V17.3203H28.7187M28.3984 20.9531H20.1562V24.5H24.5312L24.1172 29.1094L20.1562 30.1719V33.8594L27.4219 31.8437" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 972 B |
@@ -0,0 +1,7 @@
|
||||
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.64062 0H10.4375V1.78125H12.0937V0H13.8906V5.39062H12.0937V3.59375H10.4531V5.39062H8.64062M16.2656 1.79687H14.6797V0H19.6562V1.79687H18.0625V5.39062H16.2656M20.4453 0H22.3281L23.4844 1.89844L24.6406 0H26.5234V5.39062H24.7266V2.71875L23.4687 4.65625L22.2109 2.71875V5.39062H20.4453M27.4141 0H29.2109V3.60937H31.7578V5.39062H27.4141" fill="black"/>
|
||||
<path d="M8.57812 36.7969L6 7.85938H34.3437L31.7656 36.7812L20.1484 40" fill="#E44D26"/>
|
||||
<path d="M20.1719 37.5391V10.2344H31.7578L29.5469 34.9219" fill="#F16529"/>
|
||||
<path d="M11.2656 13.7734H20.1719V17.3203H15.1562L15.4844 20.9531H20.1719V24.4922H12.2344M12.3906 26.2734H15.9531L16.2031 29.1094L20.1719 30.1719V33.875L12.8906 31.8437" fill="#EBEBEB"/>
|
||||
<path d="M29.0469 13.7734H20.1562V17.3203H28.7187M28.3984 20.9531H20.1562V24.5H24.5312L24.1172 29.1094L20.1562 30.1719V33.8594L27.4219 31.8437" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 972 B |
@@ -0,0 +1,44 @@
|
||||
export const placeholder = `
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>My HTML document</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>This is an H1 heading</h1>
|
||||
<h2>This is an H2 heading</h2>
|
||||
<p>This is a paragraph</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
background-color: #ffffff;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #ff6d5a;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #909399;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
console.log("Hello World!");
|
||||
</script>
|
||||
`.trim();
|
||||
@@ -0,0 +1,5 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
|
||||
describe('Test Html Node > extractHtmlContent', () => {
|
||||
new NodeTestHarness().setupTests();
|
||||
});
|
||||
@@ -0,0 +1,418 @@
|
||||
{
|
||||
"name": "html extract fix",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "b421815f-bbeb-480d-a759-6a0360a050b6",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [480, 780]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "extractHtmlContent",
|
||||
"extractionValues": {
|
||||
"values": [
|
||||
{
|
||||
"key": "data",
|
||||
"cssSelector": "html"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"cleanUpText": true
|
||||
}
|
||||
},
|
||||
"id": "73ed18ec-3a26-4300-b917-240faa810a33",
|
||||
"name": "HTML",
|
||||
"type": "n8n-nodes-base.html",
|
||||
"typeVersion": 1.2,
|
||||
"position": [1280, 260]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "extractHtmlContent",
|
||||
"extractionValues": {
|
||||
"values": [
|
||||
{
|
||||
"key": "data",
|
||||
"cssSelector": "html",
|
||||
"skipSelectors": "img, a"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "d8eaf4c4-be91-43ba-b5ff-efcc7ece60b0",
|
||||
"name": "HTML2",
|
||||
"type": "n8n-nodes-base.html",
|
||||
"typeVersion": 1.2,
|
||||
"position": [1280, 600]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "extractHtmlContent",
|
||||
"extractionValues": {
|
||||
"values": [
|
||||
{
|
||||
"key": "data",
|
||||
"cssSelector": "p",
|
||||
"returnArray": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "145a5168-69fd-49bd-a2a0-07841854937c",
|
||||
"name": "HTML3",
|
||||
"type": "n8n-nodes-base.html",
|
||||
"typeVersion": 1.2,
|
||||
"position": [1280, 760]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "extractHtmlContent",
|
||||
"extractionValues": {
|
||||
"values": [
|
||||
{
|
||||
"key": "data",
|
||||
"cssSelector": "=html"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"trimValues": true
|
||||
}
|
||||
},
|
||||
"id": "7a370ce9-e4c4-46e0-89a4-881a6c8a7019",
|
||||
"name": "HTML1",
|
||||
"type": "n8n-nodes-base.html",
|
||||
"typeVersion": 1.2,
|
||||
"position": [1280, 420]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "extractHtmlContent",
|
||||
"extractionValues": {
|
||||
"values": [
|
||||
{
|
||||
"key": "data",
|
||||
"cssSelector": "div",
|
||||
"returnValue": "attribute"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "64c9005d-f9fe-457e-8e24-f1c59e76aeae",
|
||||
"name": "HTML4",
|
||||
"type": "n8n-nodes-base.html",
|
||||
"typeVersion": 1.2,
|
||||
"position": [1280, 940]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "extractHtmlContent",
|
||||
"extractionValues": {
|
||||
"values": [
|
||||
{
|
||||
"key": "data",
|
||||
"cssSelector": "body",
|
||||
"returnValue": "html"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "eef6b477-2c28-4c20-884f-93bbd48f9d0d",
|
||||
"name": "HTML5",
|
||||
"type": "n8n-nodes-base.html",
|
||||
"typeVersion": 1.2,
|
||||
"position": [1280, 1120]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "extractHtmlContent",
|
||||
"extractionValues": {
|
||||
"values": [
|
||||
{
|
||||
"key": "data",
|
||||
"cssSelector": "#text-id",
|
||||
"returnValue": "value"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "a548e5e3-0dcd-4f52-a581-bd046ef325b3",
|
||||
"name": "HTML6",
|
||||
"type": "n8n-nodes-base.html",
|
||||
"typeVersion": 1.2,
|
||||
"position": [1280, 1280]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"data": {
|
||||
"data": "<html>\n<head>\n\t<title>My Page</title>\t\n</head>\n<body>\n\t<h1>My Page</h1>\n\t<p>Hello World</p>\n\t<div class=\"content\">\n\t\t<p>Another paragraph\n</p>\n\t\t<p>Yet \r\n\r\n\t\t\t\t\t\tanother paragraph\n</p>\n\t\t<p>Andone more\n</p>\n\t</div>\n\t<img src=\"https://n8n.io/n8n-logo.png\" alt=\"n8n.io logo\" />\n\t<a href=\"https://n8n.io\">n8n.io</a>\n <input id=\"text-id\" type=\"text\" value=\"n8n\" />\n</body>\n</html>"
|
||||
}
|
||||
},
|
||||
"id": "ed46f03d-6cde-4225-beab-fdbe82bf095f",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-testing.testData",
|
||||
"typeVersion": 1,
|
||||
"position": [780, 740]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "9d4c07df-3348-4b0e-b144-dfb038bddb99",
|
||||
"name": "No Operation, do nothing",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [1500, 260]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "0dd56421-7e3a-4908-a933-c4e09de6b7d5",
|
||||
"name": "No Operation, do nothing1",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [1580, 620]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "15c21267-b4b7-4805-ad40-32060400fcef",
|
||||
"name": "No Operation, do nothing2",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [1600, 780]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "d6fd1d78-e24b-4237-bd66-620130f0e5fc",
|
||||
"name": "No Operation, do nothing3",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [1640, 1140]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "f8b7457b-7b46-4af6-958c-ad770f29e587",
|
||||
"name": "No Operation, do nothing4",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [1560, 960]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "f509b012-78ea-4e14-9ee4-ebdd562efe3e",
|
||||
"name": "No Operation, do nothing5",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [1560, 420]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "173f20fb-80aa-42d1-97b1-fc7a751fbedd",
|
||||
"name": "No Operation, do nothing6",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [1580, 1300]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"No Operation, do nothing4": [
|
||||
{
|
||||
"json": {
|
||||
"data": {
|
||||
"class": "content"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"No Operation, do nothing": [
|
||||
{
|
||||
"json": {
|
||||
"data": "MY PAGEHello WorldAnother paragraphYet another paragraphAndone moren8n.io logo [https://n8n.io/n8n-logo.png] n8n.io [https://n8n.io]"
|
||||
}
|
||||
}
|
||||
],
|
||||
"No Operation, do nothing5": [
|
||||
{
|
||||
"json": {
|
||||
"data": "MY PAGE\n\nHello World\n\nAnother paragraph\n\nYet another paragraph\n\nAndone more\n\nn8n.io logo [https://n8n.io/n8n-logo.png] n8n.io [https://n8n.io]"
|
||||
}
|
||||
}
|
||||
],
|
||||
"No Operation, do nothing1": [
|
||||
{
|
||||
"json": {
|
||||
"data": "MY PAGE\n\nHello World\n\nAnother paragraph\n\nYet another paragraph\n\nAndone more"
|
||||
}
|
||||
}
|
||||
],
|
||||
"No Operation, do nothing2": [
|
||||
{
|
||||
"json": {
|
||||
"data": ["Hello World", "Another paragraph", "Yet another paragraph", "Andone more"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"No Operation, do nothing3": [
|
||||
{
|
||||
"json": {
|
||||
"data": "\n\t<h1>My Page</h1>\n\t<p>Hello World</p>\n\t<div class=\"content\">\n\t\t<p>Another paragraph\n</p>\n\t\t<p>Yet \n\n\t\t\t\t\t\tanother paragraph\n</p>\n\t\t<p>Andone more\n</p>\n\t</div>\n\t<img src=\"https://n8n.io/n8n-logo.png\" alt=\"n8n.io logo\">\n\t<a href=\"https://n8n.io\">n8n.io</a>\n <input id=\"text-id\" type=\"text\" value=\"n8n\">\n\n"
|
||||
}
|
||||
}
|
||||
],
|
||||
"No Operation, do nothing6": [
|
||||
{
|
||||
"json": {
|
||||
"data": "n8n"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HTML",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTML1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTML2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTML3",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTML4",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTML5",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "HTML6",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"HTML": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "No Operation, do nothing",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"HTML6": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "No Operation, do nothing6",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"HTML5": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "No Operation, do nothing3",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"HTML4": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "No Operation, do nothing4",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"HTML3": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "No Operation, do nothing2",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"HTML2": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "No Operation, do nothing1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"HTML1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "No Operation, do nothing5",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "170b087f-19bf-4cbd-90cf-d684fb112034",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd"
|
||||
},
|
||||
"id": "vqwcz5PIBQmAw4SZ",
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type cheerio from 'cheerio';
|
||||
|
||||
export type Cheerio = ReturnType<typeof cheerio>;
|
||||
|
||||
export interface IValueData {
|
||||
attribute?: string;
|
||||
skipSelectors?: string;
|
||||
cssSelector: string;
|
||||
returnValue: string;
|
||||
key: string;
|
||||
returnArray: boolean;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { convert } from 'html-to-text';
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
|
||||
import type { IValueData, Cheerio } from './types';
|
||||
|
||||
// The extraction functions
|
||||
const extractFunctions: {
|
||||
[key: string]: ($: Cheerio, valueData: IValueData, nodeVersion: number) => string | undefined;
|
||||
} = {
|
||||
attribute: ($: Cheerio, valueData: IValueData): string | undefined =>
|
||||
$.attr(valueData.attribute!),
|
||||
html: ($: Cheerio, _valueData: IValueData): string | undefined => $.html() || undefined,
|
||||
text: ($: Cheerio, _valueData: IValueData, nodeVersion: number): string | undefined => {
|
||||
if (nodeVersion <= 1.1) return $.text() || undefined;
|
||||
|
||||
const html = $.html() || '';
|
||||
|
||||
let options;
|
||||
if (_valueData.skipSelectors) {
|
||||
options = {
|
||||
selectors: _valueData.skipSelectors.split(',').map((s) => ({
|
||||
selector: s.trim(),
|
||||
format: 'skip',
|
||||
})),
|
||||
};
|
||||
}
|
||||
return convert(html, options);
|
||||
},
|
||||
value: ($: Cheerio, _valueData: IValueData): string | undefined => $.val(),
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple helper function which applies options
|
||||
*/
|
||||
export function getValue(
|
||||
$: Cheerio,
|
||||
valueData: IValueData,
|
||||
options: IDataObject,
|
||||
nodeVersion: number,
|
||||
) {
|
||||
let value = extractFunctions[valueData.returnValue]($, valueData, nodeVersion);
|
||||
|
||||
if (value === undefined) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (options.trimValues) {
|
||||
value = value.trim();
|
||||
}
|
||||
|
||||
if (options.cleanUpText) {
|
||||
value = value
|
||||
.replace(/^\s+|\s+$/g, '')
|
||||
.replace(/(\r\n|\n|\r)/gm, '')
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
Reference in New Issue
Block a user