diff --git a/packages/nodes-base/nodes/Markdown/Markdown.node.ts b/packages/nodes-base/nodes/Markdown/Markdown.node.ts
new file mode 100644
index 0000000000..8a5bec5415
--- /dev/null
+++ b/packages/nodes-base/nodes/Markdown/Markdown.node.ts
@@ -0,0 +1,608 @@
+import { IExecuteFunctions } from 'n8n-core';
+
+import {
+ IDataObject,
+ INodeExecutionData,
+ INodeType,
+ INodeTypeDescription,
+ JsonObject,
+} from 'n8n-workflow';
+
+import { Converter } from 'showdown';
+
+import { NodeHtmlMarkdown } from 'node-html-markdown';
+
+import { isEmpty, set } from 'lodash';
+
+export class Markdown implements INodeType {
+ description: INodeTypeDescription = {
+ displayName: 'Markdown',
+ name: 'markdown',
+ icon: 'file:markdown.svg',
+ group: ['output'],
+ version: 1,
+ subtitle: '={{$parameter["mode"]==="markdownToHtml" ? "Markdown to HTML" : "HTML to Markdown"}}',
+ description: 'Convert data between Markdown and HTML',
+ defaults: {
+ name: 'Markdown',
+ color: '#000000',
+ },
+ inputs: ['main'],
+ outputs: ['main'],
+ credentials: [],
+ properties: [
+ {
+ displayName: 'Mode',
+ name: 'mode',
+ type: 'options',
+ options: [
+ {
+ name: 'Markdown to HTML',
+ value: 'markdownToHtml',
+ description: 'Convert data from Markdown to HTML',
+ },
+ {
+ name: 'HTML to Markdown',
+ value: 'htmlToMarkdown',
+ description: 'Convert data from HTML to Markdown',
+ },
+ ],
+ default: 'htmlToMarkdown',
+ },
+ {
+ displayName: 'HTML',
+ name: 'html',
+ type: 'string',
+ displayOptions: {
+ show: {
+ mode: ['htmlToMarkdown'],
+ },
+ },
+ default: '',
+ required: true,
+ description: 'The HTML to be converted to markdown',
+ },
+ {
+ displayName: 'Markdown',
+ name: 'markdown',
+ type: 'string',
+ typeOptions: {
+ alwaysOpenEditWindow: true,
+ },
+ displayOptions: {
+ show: {
+ mode: ['markdownToHtml'],
+ },
+ },
+ default: '',
+ required: true,
+ description: 'The Markdown to be converted to html',
+ },
+ {
+ displayName: 'Destination Key',
+ name: 'destinationKey',
+ type: 'string',
+ displayOptions: {
+ show: {
+ mode: ['markdownToHtml', 'htmlToMarkdown'],
+ },
+ },
+ default: 'data',
+ required: true,
+ placeholder: '',
+ description:
+ 'The field to put the output in. Specify nested fields using dots, e.g."level1.level2.newKey".',
+ },
+
+ //============= HTML to Markdown Options ===============
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ displayOptions: {
+ show: {
+ mode: ['htmlToMarkdown'],
+ },
+ },
+ options: [
+ {
+ displayName: 'Bullet Marker',
+ name: 'bulletMarker',
+ type: 'string',
+ default: '*',
+ description: 'Specify bullet marker, default *',
+ },
+ {
+ displayName: 'Code Block Fence',
+ name: 'codeFence',
+ type: 'string',
+ default: '```',
+ description: 'Specify code block fence, default ```',
+ },
+ {
+ displayName: 'Emphasis Delimiter',
+ name: 'emDelimiter',
+ type: 'string',
+ default: '_',
+ description: 'Specify emphasis delimiter, default _',
+ },
+ {
+ displayName: 'Global Escape Pattern',
+ name: 'globalEscape',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: false,
+ },
+ default: {},
+ description:
+ 'Setting this will override the default escape settings, you might want to use textReplace option instead',
+ options: [
+ {
+ name: 'value',
+ displayName: 'Value',
+ values: [
+ {
+ displayName: 'Pattern',
+ name: 'pattern',
+ type: 'string',
+ default: '',
+ description: 'RegEx for pattern',
+ },
+ {
+ displayName: 'Replacement',
+ name: 'replacement',
+ type: 'string',
+ default: '',
+ description: 'String replacement',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ displayName: 'Ignored Elements',
+ name: 'ignore',
+ type: 'string',
+ default: '',
+ description:
+ 'Supplied elements will be ignored (ignores inner text does not parse children)',
+ placeholder: 'e.g. h1, p ...',
+ hint: 'Comma separated elements',
+ },
+ {
+ displayName: 'Keep Images With Data',
+ name: 'keepDataImages',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether to keep images with data: URI (Note: These can be up to 1MB each), e.g. .',
+ },
+ {
+ displayName: 'Line Start Escape Pattern',
+ name: 'lineStartEscape',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: false,
+ },
+ default: {},
+ description:
+ 'Setting this will override the default escape settings, you might want to use textReplace option instead',
+ options: [
+ {
+ name: 'value',
+ displayName: 'Value',
+ values: [
+ {
+ displayName: 'Pattern',
+ name: 'pattern',
+ type: 'string',
+ default: '',
+ description: 'RegEx for pattern',
+ },
+ {
+ displayName: 'Replacement',
+ name: 'replacement',
+ type: 'string',
+ default: '',
+ description: 'String replacement',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ displayName: 'Max Consecutive New Lines',
+ name: 'maxConsecutiveNewlines',
+ type: 'number',
+ default: 3,
+ description: 'Specify max consecutive new lines allowed',
+ },
+ {
+ displayName: 'Place URLs At The Bottom',
+ name: 'useLinkReferenceDefinitions',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether to Place URLS at the bottom and format links using link reference definitions',
+ },
+ {
+ displayName: 'Strong Delimiter',
+ name: 'strongDelimiter',
+ type: 'string',
+ default: '**',
+ description: 'Specify strong delimiter, default **',
+ },
+ {
+ displayName: 'Style For Code Block',
+ name: 'codeBlockStyle',
+ type: 'options',
+ default: 'fence',
+ description: 'Specify style for code block, default "fence"',
+ options: [
+ {
+ name: 'Fence',
+ value: 'fence',
+ },
+ {
+ name: 'Indented',
+ value: 'indented',
+ },
+ ],
+ },
+ {
+ displayName: 'Text Replacement Pattern',
+ name: 'textReplace',
+ type: 'fixedCollection',
+ typeOptions: {
+ multipleValues: true,
+ },
+ default: [],
+ description:
+ 'User-defined text replacement pattern (Replaces matching text retrieved from nodes)',
+ options: [
+ {
+ name: 'values',
+ displayName: 'Values',
+ values: [
+ {
+ displayName: 'Pattern',
+ name: 'pattern',
+ type: 'string',
+ default: '',
+ description: 'RegEx for pattern',
+ },
+ {
+ displayName: 'Replacement',
+ name: 'replacement',
+ type: 'string',
+ default: '',
+ description: 'String replacement',
+ },
+ ],
+ },
+ ],
+ },
+ {
+ displayName: 'Treat As Blocks',
+ name: 'blockElements',
+ type: 'string',
+ default: '',
+ description:
+ 'Supplied elements will be treated as blocks (surrounded with blank lines)',
+ placeholder: 'e.g. p, div, ...',
+ hint: 'Comma separated elements',
+ },
+ ],
+ },
+ //============= Markdown to HTML Options ===============
+ {
+ displayName: 'Options',
+ name: 'options',
+ type: 'collection',
+ placeholder: 'Add Option',
+ default: {},
+ displayOptions: {
+ show: {
+ mode: ['markdownToHtml'],
+ },
+ },
+ options: [
+ {
+ displayName: 'Add Blank To Links',
+ name: 'openLinksInNewWindow',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether to open all links in new windows (by adding the attribute target="_blank" to tags)',
+ },
+ {
+ displayName: 'Automatic Linking To URLs',
+ name: 'simplifiedAutoLink',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to enable automatic linking to urls',
+ },
+ {
+ displayName: 'Backslash Escapes HTML Tags',
+ name: 'backslashEscapesHTMLTags',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to support for HTML Tag escaping ex: foo
, and tags instead of an HTML fragment',
+ },
+ {
+ displayName: 'Customized Header ID',
+ name: 'customizedHeaderId',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to use text in curly braces as header id',
+ },
+ {
+ displayName: 'Emoji Support',
+ name: 'emoji',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether to enable emoji support. Ex: this is a :smile: emoji For more info on available emojis, see https://github.com/showdownjs/showdown/wiki/Emojis.',
+ },
+ {
+ displayName: 'Encode Emails',
+ name: 'encodeEmails',
+ type: 'boolean',
+ default: true,
+ description:
+ 'Whether to enable e-mail addresses encoding through the use of Character Entities, transforming ASCII e-mail addresses into its equivalent decimal entities',
+ },
+ {
+ displayName: 'Exclude Trailing Punctuation From URLs',
+ name: 'excludeTrailingPunctuationFromURLs',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether to exclude trailing punctuation from autolinking urls. Punctuation excluded: . ! ? ( ). Only applies if simplifiedAutoLink option is set to true.',
+ },
+ {
+ displayName: 'GitHub Code Blocks',
+ name: 'ghCodeBlocks',
+ type: 'boolean',
+ default: true,
+ description: 'Whether to enable support for GFM code block style',
+ },
+ {
+ displayName: 'GitHub Compatible Header IDs',
+ name: 'ghCompatibleHeaderId',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether to generate header ids compatible with github style (spaces are replaced with dashes and a bunch of non alphanumeric chars are removed)',
+ },
+ {
+ displayName: 'GitHub Mention Link',
+ name: 'ghMentionsLink',
+ type: 'string',
+ default: 'https://github.com/{u}',
+ description:
+ 'Whether to change the link generated by @mentions. Showdown will replace {u} with the username. Only applies if ghMentions option is enabled.',
+ },
+ {
+ displayName: 'GitHub Mentions',
+ name: 'ghMentions',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to enable github @mentions, which link to the username mentioned',
+ },
+ {
+ displayName: 'GitHub Task Lists',
+ name: 'tasklists',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to enable support for GFM tasklists',
+ },
+ {
+ displayName: 'Header Level Start',
+ name: 'headerLevelStart',
+ type: 'number',
+ default: 1,
+ description: 'Whether to set the header starting level',
+ },
+ {
+ displayName: 'Mandatory Space Before Header',
+ name: 'requireSpaceBeforeHeadingText',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to make adding a space between # and the header text mandatory',
+ },
+ {
+ displayName: 'Middle Word Asterisks',
+ name: 'literalMidWordAsterisks',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether to stop showdown from interpreting asterisks in the middle of words as and and instead treat them as literal asterisks',
+ },
+ {
+ displayName: 'Middle Word Underscores',
+ name: 'literalMidWordUnderscores',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether to stop showdown from interpreting underscores in the middle of words as and and instead treat them as literal underscores',
+ },
+ {
+ displayName: 'No Header ID',
+ name: 'noHeaderId',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to disable the automatic generation of header ids',
+ },
+ {
+ displayName: 'Parse Image Dimensions',
+ name: 'parseImgDimensions',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether to enable support for setting image dimensions from within markdown syntax',
+ },
+ {
+ displayName: 'Prefix Header ID',
+ name: 'prefixHeaderId',
+ type: 'string',
+ default: 'section',
+ description: 'Add a prefix to the generated header ids',
+ },
+ {
+ displayName: 'Raw Header ID',
+ name: 'rawHeaderId',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether to remove only spaces, \' and " from generated header ids (including prefixes), replacing them with dashes (-)',
+ },
+ {
+ displayName: 'Raw Prefix Header ID',
+ name: 'rawPrefixHeaderId',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to prevent showdown from modifying the prefix',
+ },
+ {
+ displayName: 'Simple Line Breaks',
+ name: 'simpleLineBreaks',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether to parse line breaks as
, like GitHub does, without needing 2 spaces at the end of the line',
+ },
+ {
+ displayName: 'Smart Indentation Fix',
+ name: 'smartIndentationFix',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether to try to smartly fix indentation problems related to es6 template strings in the midst of indented code',
+ },
+ {
+ displayName: 'Spaces Indented Sublists',
+ name: 'disableForced4SpacesIndentedSublists',
+ type: 'boolean',
+ default: false,
+ description:
+ 'Whether to disable the requirement of indenting sublists by 4 spaces for them to be nested, effectively reverting to the old behavior where 2 or 3 spaces were enough',
+ },
+ {
+ displayName: 'Split Adjacent Blockquotes',
+ name: 'splitAdjacentBlockquotes',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to split adjacent blockquote blocks',
+ },
+ {
+ displayName: 'Strikethrough',
+ name: 'strikethrough',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to enable support for strikethrough syntax',
+ },
+ {
+ displayName: 'Tables Header ID',
+ name: 'tablesHeaderId',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to add an ID property to table headers tags',
+ },
+ {
+ displayName: 'Tables Support',
+ name: 'tables',
+ type: 'boolean',
+ default: false,
+ description: 'Whether to enable support for tables syntax',
+ },
+ ],
+ },
+ ],
+ };
+
+ async execute(this: IExecuteFunctions): Promise {
+ const items = this.getInputData();
+ const returnData: IDataObject[] = [];
+ const mode = this.getNodeParameter('mode', 0) as string;
+
+ const { length } = items;
+ for (let i = 0; i < length; i++) {
+ try {
+ if (mode === 'htmlToMarkdown') {
+ const options = this.getNodeParameter('options', i) as IDataObject;
+ const destinationKey = this.getNodeParameter('destinationKey', i) as string;
+
+ const textReplaceOption = this.getNodeParameter('options.textReplace.values', i, []) as IDataObject[];
+ options.textReplace = !isEmpty(textReplaceOption)
+ ? textReplaceOption.map((entry) => [entry.pattern, entry.replacement])
+ : undefined;
+
+ const lineStartEscapeOption = this.getNodeParameter('options.lineStartEscape.value', i, {}) as IDataObject;
+ options.lineStartEscape = !isEmpty(lineStartEscapeOption)
+ ? [lineStartEscapeOption.pattern, lineStartEscapeOption.replacement]
+ : undefined;
+
+ const globalEscapeOption = this.getNodeParameter('options.globalEscape.value', i, {}) as IDataObject;
+ options.globalEscape = !isEmpty(globalEscapeOption)
+ ? [globalEscapeOption.pattern, globalEscapeOption.replacement]
+ : undefined;
+
+ options.ignore = options.ignore
+ ? (options.ignore as string).split(',').map(element => element.trim()) : undefined;
+ options.blockElements = options.blockElements
+ ? (options.blockElements as string).split(',').map(element => element.trim()) : undefined;
+
+ const markdownOptions = {} as IDataObject;
+
+ Object.keys(options).forEach((option) => {
+ if (options[option]) {
+ markdownOptions[option] = options[option];
+ }
+ });
+
+ const html = this.getNodeParameter('html', i) as string;
+
+ const markdownFromHTML = NodeHtmlMarkdown.translate(html, markdownOptions);
+
+ const newItem = JSON.parse(JSON.stringify(items[i].json));
+ set(newItem, destinationKey, markdownFromHTML);
+ returnData.push(newItem);
+ }
+
+ if (mode === 'markdownToHtml') {
+ const markdown = this.getNodeParameter('markdown', i) as string;
+ const destinationKey = this.getNodeParameter('destinationKey', i) as string;
+ const options = this.getNodeParameter('options', i) as IDataObject;
+
+ const converter = new Converter();
+
+ Object.keys(options).forEach((key) => converter.setOption(key, options[key]));
+ const htmlFromMarkdown = converter.makeHtml(markdown);
+
+ const newItem = JSON.parse(JSON.stringify(items[i].json));
+ set(newItem, destinationKey, htmlFromMarkdown);
+
+ returnData.push(newItem);
+ }
+ } catch (error) {
+ if (this.continueOnFail()) {
+ returnData.push({ error: (error as JsonObject).message });
+ continue;
+ }
+ throw error;
+ }
+ }
+ return [this.helpers.returnJsonArray(returnData)];
+ }
+}
diff --git a/packages/nodes-base/nodes/Markdown/markdown.svg b/packages/nodes-base/nodes/Markdown/markdown.svg
new file mode 100644
index 0000000000..24f62fb329
--- /dev/null
+++ b/packages/nodes-base/nodes/Markdown/markdown.svg
@@ -0,0 +1,6 @@
+
+
diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json
index a3d4d5bf69..133ecd8823 100644
--- a/packages/nodes-base/package.json
+++ b/packages/nodes-base/package.json
@@ -515,6 +515,7 @@
"dist/nodes/Mailjet/Mailjet.node.js",
"dist/nodes/Mailjet/MailjetTrigger.node.js",
"dist/nodes/Mandrill/Mandrill.node.js",
+ "dist/nodes/Markdown/Markdown.node.js",
"dist/nodes/Marketstack/Marketstack.node.js",
"dist/nodes/Matrix/Matrix.node.js",
"dist/nodes/Mattermost/Mattermost.node.js",
@@ -707,6 +708,7 @@
"@types/nodemailer": "^6.4.0",
"@types/redis": "^2.8.11",
"@types/request-promise-native": "~1.0.15",
+ "@types/showdown": "^1.9.4",
"@types/ssh2-sftp-client": "^5.1.0",
"@types/tmp": "^0.2.0",
"@types/uuid": "^8.3.2",
@@ -756,6 +758,7 @@
"mssql": "^6.2.0",
"mysql2": "~2.3.0",
"n8n-core": "~0.113.0",
+ "node-html-markdown": "^1.1.3",
"node-ssh": "^12.0.0",
"nodemailer": "^6.5.0",
"pdf-parse": "^1.1.1",
@@ -766,6 +769,7 @@
"request": "^2.88.2",
"rhea": "^1.0.11",
"rss-parser": "^3.7.0",
+ "showdown": "^2.0.3",
"simple-git": "^3.5.0",
"snowflake-sdk": "^1.5.3",
"ssh2-sftp-client": "^7.0.0",