From 9d0ae18ccf91e4a4a8a1a671612b4eb8010ae666 Mon Sep 17 00:00:00 2001 From: Sergey Shambir Date: Sat, 4 Mar 2017 14:25:39 +0300 Subject: [PATCH 1/4] Added linebreak before ordered/unordered list when exporting markdown --- src/pen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pen.js b/src/pen.js index 4d9ab62..7ca7c93 100755 --- a/src/pen.js +++ b/src/pen.js @@ -838,7 +838,7 @@ Pen.prototype.toMd = function() { var html = this.getContent() .replace(/\n+/g, '') // remove line break - .replace(/<([uo])l\b[^>]*>(.*?)<\/\1l>/ig, '$2'); // remove ul/ol + .replace(/<([uo])l\b[^>]*>(.*?)<\/\1l>/ig, '\n$2'); // remove ul/ol for(var p in regs) { if (regs.hasOwnProperty(p)) From abd47c269a01f1833b8fa7795599694d438fa094 Mon Sep 17 00:00:00 2001 From: Sergey Shambir Date: Tue, 9 May 2017 09:21:53 +0300 Subject: [PATCH 2/4] Improved toMd output quality Regex-based converter doesn't work well on recursive nodes like nested lists, code, etc. Now it replaced with DOM visiting. --- src/pen.js | 285 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 259 insertions(+), 26 deletions(-) diff --git a/src/pen.js b/src/pen.js index 7ca7c93..9a1b7d8 100755 --- a/src/pen.js +++ b/src/pen.js @@ -541,7 +541,7 @@ var form = inputElement.form; var me = this; form.addEventListener("submit", function() { - inputElement.value = me.config.saveAsMarkdown ? me.toMd(me.config.editor.innerHTML) : me.config.editor.innerHTML; + inputElement.value = me.config.saveAsMarkdown ? me.toMd() : me.config.editor.innerHTML; }); }; @@ -818,34 +818,267 @@ return defaults.editor; }; - // export content as markdown - var regs = { - a: [/]*href=["']([^"]+|[^']+)\b[^>]*>(.*?)<\/a>/ig, '[$2]($1)'], - img: [/]*src=["']([^\"+|[^']+)[^>]*>/ig, '![]($1)'], - b: [/]*>(.*?)<\/b>/ig, '**$1**'], - i: [/]*>(.*?)<\/i>/ig, '***$1***'], - h: [/]*>(.*?)<\/h\1>/ig, function(a, b, c) { - return '\n' + ('######'.slice(0, b)) + ' ' + c + '\n'; - }], - li: [/<(li)\b[^>]*>(.*?)<\/\1>/ig, '* $2\n'], - blockquote: [/<(blockquote)\b[^>]*>(.*?)<\/\1>/ig, '\n> $2\n'], - pre: [/]*>(.*?)<\/pre>/ig, '\n```\n$1\n```\n'], - code: [/]*>(.*?)<\/code>/ig, '\n`\n$1\n`\n'], - p: [/]*>(.*?)<\/p>/ig, '\n$1\n'], - hr: [/]*>/ig, '\n---\n'] - }; + (function(root) { + let PenHtmlToMarkdown = function(editorNode) { + if (!editorNode) throw new Error('Invalid argument - undefined editor DOM node'); + + this._states = []; + this._state = { + isInsideCode: false, + marker: ' ', + markerNo: 0, + listLevel: 0, + listIndent: '', + href: '', + }; + this._markdown = ''; + this.toMd = function() { + this._visit(editorNode); + return this._markdown.trim(); + }; + }; + + PenHtmlToMarkdown.prototype._pushState = function() { + let newState = Object.assign({}, this._state); + this._states.push(this._state); + this._state = newState; + }; + + PenHtmlToMarkdown.prototype._popState = function() { + this._state = this._states.pop(); + }; + + PenHtmlToMarkdown.prototype.actionCode = { + beforeEnter: function () { + if (!this._state.isInsideCode) + this._markdown += '`'; + }, + afterEnter: function () { + this._state.isInsideCode = true; + }, + afterExit: function () { + if (!this._state.isInsideCode) + this._markdown += '`'; + }, + }; + + PenHtmlToMarkdown.prototype.actionPre = { + beforeEnter: function () { + if (!this._state.isInsideCode) + this._markdown += '\n```\n'; + }, + afterEnter: function () { + this._state.isInsideCode = true; + }, + afterExit: function () { + if (!this._state.isInsideCode) + this._markdown += '```\n\n'; + }, + }; + + PenHtmlToMarkdown.prototype.actionParagraph = { + afterExit: function () { + if (!this._state.isInsideCode) + this._markdown += '\n'; + } + }; + + PenHtmlToMarkdown.prototype.actionBlockquote = { + afterEnter: function () { + this._markdown += '>'; + }, + afterExit: function () { + this._markdown += '\n'; + } + }; + + PenHtmlToMarkdown.prototype.actionText = { + afterEnter: function (node) { + let text = node.wholeText; + if (!this._state.isInsideCode) { + text = text.replace(/\n+/g, ' '); + } + this._markdown += text; + } + }; + + PenHtmlToMarkdown.prototype.actionUnorderedList = { + afterEnter: function () { + this._markdown += '\n'; + this._state.marker = '*'; + this._state.listIndent = ' '.repeat(4 * this._state.listLevel); + this._state.listLevel += 1; + }, + afterExit: function () { + this._markdown += '\n'; + }, + }; + + PenHtmlToMarkdown.prototype.actionOrderedList = { + afterEnter: function () { + this._markdown += '\n'; + this._state.marker = '1.'; + this._state.markerNo = 1; + this._state.listIndent = ' '.repeat(4 * this._state.listLevel); + this._state.listLevel += 1; + }, + afterChild: function () { + this._state.markerNo += 1; + this._state.marker = this._state.markerNo.toString() + '.'; + }, + afterExit: function () { + this._markdown += '\n'; + }, + }; + + PenHtmlToMarkdown.prototype.actionListItem = { + afterEnter: function () { + this._markdown += this._state.listIndent + this._state.marker + " "; + }, + afterExit: function () { + this._markdown += '\n'; + } + }; + + PenHtmlToMarkdown.prototype.actionLink = { + afterEnter: function (node) { + this._state.href = node.attributes.getNamedItem("href").value; + this._markdown += '['; + }, + beforeExit: function (node) { + this._markdown += '](' + this._state.href + ')'; + }, + }; + + PenHtmlToMarkdown.prototype.actionBold = { + afterEnter: function () { + this._markdown += '**'; + }, + beforeExit: function () { + this._markdown += '**'; + } + }; - Pen.prototype.toMd = function() { - var html = this.getContent() - .replace(/\n+/g, '') // remove line break - .replace(/<([uo])l\b[^>]*>(.*?)<\/\1l>/ig, '\n$2'); // remove ul/ol + PenHtmlToMarkdown.prototype.actionItalic = { + afterEnter: function () { + this._markdown += '***'; + }, + beforeExit: function () { + this._markdown += '***'; + } + }; - for(var p in regs) { - if (regs.hasOwnProperty(p)) - html = html.replace.apply(html, regs[p]); + PenHtmlToMarkdown.prototype.actionImg = { + afterEnter: function (node) { + let src = node.attributes.getNamedItem("src").value; + let alt = node.attributes.getNamedItem("alt").value; + let parser = document.createElement('a'); + parser.href = src; + src = parser.pathname + parser.search + parser.hash; + + this._markdown += '![' + alt + '](' + src + ')'; + }, + }; + + PenHtmlToMarkdown.prototype.actionHorizontalLine = { + afterEnter: function () { + this._markdown += '\n---\n'; + }, + }; + + PenHtmlToMarkdown.prototype.actionLinebreak = { + afterEnter: function () { + this._markdown += '\n'; + }, + }; + + function makeHeaderAction(level) { + return { + beforeEnter: function () { + this._markdown += '\n' + '#'.repeat(level) + ' '; + }, + afterExit: function () { + this._markdown += '\n\n'; + } + } } - return html.replace(/\*{5}/g, '**'); - }; + PenHtmlToMarkdown.prototype.actionH1 = makeHeaderAction(1) + PenHtmlToMarkdown.prototype.actionH2 = makeHeaderAction(2) + PenHtmlToMarkdown.prototype.actionH3 = makeHeaderAction(3) + PenHtmlToMarkdown.prototype.actionH4 = makeHeaderAction(4) + PenHtmlToMarkdown.prototype.actionH5 = makeHeaderAction(5) + PenHtmlToMarkdown.prototype.actionH6 = makeHeaderAction(6) + + PenHtmlToMarkdown.prototype._selectAction = function(node) { + let dispatchTable = { + 'CODE': this.actionCode, + 'PRE': this.actionPre, + 'P': this.actionParagraph, + 'BLOCKQUOTE': this.actionBlockquote, + 'UL': this.actionUnorderedList, + 'OL': this.actionOrderedList, + 'LI': this.actionListItem, + 'A': this.actionLink, + 'IMG': this.actionImg, + 'B': this.actionBold, + 'I': this.actionItalic, + 'HR': this.actionHorizontalLine, + 'BR': this.actionLinebreak, + 'H1': this.actionH1, + 'H2': this.actionH2, + 'H3': this.actionH3, + 'H4': this.actionH4, + 'H5': this.actionH5, + 'H6': this.actionH6, + 'DIV': null, + 'SPAN': null, + }; + + switch (node.nodeType) + { + case Node.TEXT_NODE: + return this.actionText; + case Node.ELEMENT_NODE: + let action = dispatchTable[node.tagName]; + if (action !== undefined) { + return action; + } + console.log("unhandled tag:", node.tagName); + break; + } + return null; + } + + PenHtmlToMarkdown.prototype._visit = function(node) { + let action = this._selectAction(node) + if (action) { + if (action.beforeEnter) + action.beforeEnter.call(this, node); + this._pushState(); + if (action.afterEnter) + action.afterEnter.call(this, node); + } + + for (let child of node.childNodes) { + this._visit(child); + if (action && action.afterChild) + action.afterChil.call(this, node); + } + + if (action) { + if (action.beforeExit) + action.beforeExit.call(this, node); + this._popState(); + if (action.afterExit) + action.afterExit.call(this, node); + } + } + + Pen.prototype.toMd = function() { + let converter = new PenHtmlToMarkdown(this.config.editor) + return converter.toMd() + }; + }(root)); // make it accessible if (doc.getSelection) { From 6f768d7df4bb0a6d67879349cf0019c28e511b74 Mon Sep 17 00:00:00 2001 From: Sergey Shambir Date: Tue, 9 May 2017 16:50:06 +0300 Subject: [PATCH 3/4] Fixed warnings from eslint --- src/pen.js | 48 +++++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/src/pen.js b/src/pen.js index 9a1b7d8..a6f91f9 100755 --- a/src/pen.js +++ b/src/pen.js @@ -819,7 +819,7 @@ }; (function(root) { - let PenHtmlToMarkdown = function(editorNode) { + var PenHtmlToMarkdown = function(editorNode) { if (!editorNode) throw new Error('Invalid argument - undefined editor DOM node'); this._states = []; @@ -839,7 +839,7 @@ }; PenHtmlToMarkdown.prototype._pushState = function() { - let newState = Object.assign({}, this._state); + var newState = Object.assign({}, this._state); this._states.push(this._state); this._state = newState; }; @@ -894,7 +894,7 @@ PenHtmlToMarkdown.prototype.actionText = { afterEnter: function (node) { - let text = node.wholeText; + var text = node.wholeText; if (!this._state.isInsideCode) { text = text.replace(/\n+/g, ' '); } @@ -970,9 +970,9 @@ PenHtmlToMarkdown.prototype.actionImg = { afterEnter: function (node) { - let src = node.attributes.getNamedItem("src").value; - let alt = node.attributes.getNamedItem("alt").value; - let parser = document.createElement('a'); + var src = node.attributes.getNamedItem("src").value; + var alt = node.attributes.getNamedItem("alt").value; + var parser = document.createElement('a'); parser.href = src; src = parser.pathname + parser.search + parser.hash; @@ -1000,17 +1000,18 @@ afterExit: function () { this._markdown += '\n\n'; } - } + }; } - PenHtmlToMarkdown.prototype.actionH1 = makeHeaderAction(1) - PenHtmlToMarkdown.prototype.actionH2 = makeHeaderAction(2) - PenHtmlToMarkdown.prototype.actionH3 = makeHeaderAction(3) - PenHtmlToMarkdown.prototype.actionH4 = makeHeaderAction(4) - PenHtmlToMarkdown.prototype.actionH5 = makeHeaderAction(5) - PenHtmlToMarkdown.prototype.actionH6 = makeHeaderAction(6) + + PenHtmlToMarkdown.prototype.actionH1 = makeHeaderAction(1); + PenHtmlToMarkdown.prototype.actionH2 = makeHeaderAction(2); + PenHtmlToMarkdown.prototype.actionH3 = makeHeaderAction(3); + PenHtmlToMarkdown.prototype.actionH4 = makeHeaderAction(4); + PenHtmlToMarkdown.prototype.actionH5 = makeHeaderAction(5); + PenHtmlToMarkdown.prototype.actionH6 = makeHeaderAction(6); PenHtmlToMarkdown.prototype._selectAction = function(node) { - let dispatchTable = { + var dispatchTable = { 'CODE': this.actionCode, 'PRE': this.actionPre, 'P': this.actionParagraph, @@ -1039,7 +1040,7 @@ case Node.TEXT_NODE: return this.actionText; case Node.ELEMENT_NODE: - let action = dispatchTable[node.tagName]; + var action = dispatchTable[node.tagName]; if (action !== undefined) { return action; } @@ -1047,10 +1048,10 @@ break; } return null; - } + }; PenHtmlToMarkdown.prototype._visit = function(node) { - let action = this._selectAction(node) + var action = this._selectAction(node) if (action) { if (action.beforeEnter) action.beforeEnter.call(this, node); @@ -1059,11 +1060,12 @@ action.afterEnter.call(this, node); } - for (let child of node.childNodes) { - this._visit(child); + var self = this; + node.childNodes.forEach(function (child) { + self._visit(child); if (action && action.afterChild) - action.afterChil.call(this, node); - } + action.afterChil.call(self, node); + }); if (action) { if (action.beforeExit) @@ -1075,8 +1077,8 @@ } Pen.prototype.toMd = function() { - let converter = new PenHtmlToMarkdown(this.config.editor) - return converter.toMd() + var converter = new PenHtmlToMarkdown(this.config.editor); + return converter.toMd(); }; }(root)); From b8a3f3b71f81d1714713b0c617a6df2dd89663ef Mon Sep 17 00:00:00 2001 From: Sergey Shambir Date: Tue, 9 May 2017 17:45:12 +0300 Subject: [PATCH 4/4] Fixed warnings from eslint --- src/pen.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pen.js b/src/pen.js index a6f91f9..7bf6c4a 100755 --- a/src/pen.js +++ b/src/pen.js @@ -818,7 +818,7 @@ return defaults.editor; }; - (function(root) { + (function() { var PenHtmlToMarkdown = function(editorNode) { if (!editorNode) throw new Error('Invalid argument - undefined editor DOM node'); @@ -945,7 +945,7 @@ this._state.href = node.attributes.getNamedItem("href").value; this._markdown += '['; }, - beforeExit: function (node) { + beforeExit: function () { this._markdown += '](' + this._state.href + ')'; }, }; @@ -1051,7 +1051,7 @@ }; PenHtmlToMarkdown.prototype._visit = function(node) { - var action = this._selectAction(node) + var action = this._selectAction(node); if (action) { if (action.beforeEnter) action.beforeEnter.call(this, node); @@ -1074,13 +1074,13 @@ if (action.afterExit) action.afterExit.call(this, node); } - } + }; Pen.prototype.toMd = function() { var converter = new PenHtmlToMarkdown(this.config.editor); return converter.toMd(); }; - }(root)); + }()); // make it accessible if (doc.getSelection) {