diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 460920d8..d744a2e7 100644 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -8,21 +8,7 @@ import flowchart from 'flowchart' import SequenceDiagram from 'js-sequence-diagrams' import eventEmitter from 'browser/main/lib/eventEmitter' import fs from 'fs' - -function decodeHTMLEntities (text) { - var entities = [ - ['apos', '\''], - ['amp', '&'], - ['lt', '<'], - ['gt', '>'] - ] - - for (var i = 0, max = entities.length; i < max; ++i) { - text = text.replace(new RegExp('&' + entities[i][0] + ';', 'g'), entities[i][1]) - } - - return text -} +import htmlTextHelper from 'browser/lib/htmlTextHelper' const { remote } = require('electron') const { app } = remote @@ -241,6 +227,13 @@ export default class MarkdownPreview extends React.Component { let { value, theme, indentSize, codeBlockTheme } = this.props this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme) + + const codeBlocks = value.match(/(```)(.|[\n])*?(```)/g) + if (codeBlocks !== null) { + codeBlocks.forEach((codeBlock) => { + value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock)) + }) + } this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value) _.forEach(this.refs.root.contentWindow.document.querySelectorAll('.taskListItem'), (el) => { @@ -263,7 +256,7 @@ export default class MarkdownPreview extends React.Component { let syntax = CodeMirror.findModeByName(el.className) if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text') CodeMirror.requireMode(syntax.mode, () => { - let content = decodeHTMLEntities(el.innerHTML) + let content = htmlTextHelper.decodeEntities(el.innerHTML) el.innerHTML = '' el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror` CodeMirror.runMode(content, syntax.mime, el, { @@ -281,7 +274,7 @@ export default class MarkdownPreview extends React.Component { _.forEach(this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), (el) => { Raphael.setWindow(this.getWindow()) try { - let diagram = flowchart.parse(decodeHTMLEntities(el.innerHTML)) + let diagram = flowchart.parse(htmlTextHelper.decodeEntities(el.innerHTML)) el.innerHTML = '' diagram.drawSVG(el, opts) _.forEach(el.querySelectorAll('a'), (el) => { @@ -297,7 +290,7 @@ export default class MarkdownPreview extends React.Component { _.forEach(this.refs.root.contentWindow.document.querySelectorAll('.sequence'), (el) => { Raphael.setWindow(this.getWindow()) try { - let diagram = SequenceDiagram.parse(decodeHTMLEntities(el.innerHTML)) + let diagram = SequenceDiagram.parse(htmlTextHelper.decodeEntities(el.innerHTML)) el.innerHTML = '' diagram.drawSVG(el, {theme: 'simple'}) _.forEach(el.querySelectorAll('a'), (el) => { diff --git a/browser/lib/htmlTextHelper.js b/browser/lib/htmlTextHelper.js new file mode 100644 index 00000000..49952fbd --- /dev/null +++ b/browser/lib/htmlTextHelper.js @@ -0,0 +1,43 @@ +/** + * @fileoverview Text trimmer for html. + */ + +/** + * @param {string} text + * @return {string} + */ + +export function decodeEntities (text) { + var entities = [ + ['apos', '\''], + ['amp', '&'], + ['lt', '<'], + ['gt', '>'], + ['#63', '\\?'] + ] + + for (var i = 0, max = entities.length; i < max; ++i) { + text = text.replace(new RegExp(`&${entities[i][0]};`, 'g'), entities[i][1]) + } + + return text +} + +export function encodeEntities (text) { + const entities = [ + ['\'', 'apos'], + ['<', 'lt'], + ['>', 'gt'], + ['\\?', '#63'] + ] + + entities.forEach((entity) => { + text = text.replace(new RegExp(entity[0], 'g'), `&${entity[1]};`) + }) + return text +} + +export default { + decodeEntities, + encodeEntities +} diff --git a/tests/lib/html-text-helper-test.js b/tests/lib/html-text-helper-test.js new file mode 100644 index 00000000..a476c0dd --- /dev/null +++ b/tests/lib/html-text-helper-test.js @@ -0,0 +1,52 @@ +/** + * @fileoverview Unit test for browser/lib/htmlTextHelper + */ +const test = require('ava') +const htmlTextHelper = require('browser/lib/htmlTextHelper') + +// Unit test +test('htmlTextHelper#decodeEntities should return encoded text (string)', t => { + // [input, expected] + const testCases = [ + ['<a href=', 'Boostnote'], + ['<\\\\?php\n var = 'hoge';', '<\\\\?php\n var = \'hoge\';'], + ['&', '&'] + ] + + testCases.forEach(testCase => { + const [input, expected] = testCase + t.is(htmlTextHelper.decodeEntities(input), expected, `Test for decodeEntities() input: ${input} expected: ${expected}`) + }) +}) + +test('htmlTextHelper#decodeEntities() should return decoded text (string)', t => { + // [input, expected] + const testCases = [ + ['Boostnote', '<a href='https://boostnote.io'>Boostnote'], + [' { + const [input, expected] = testCase + t.is(htmlTextHelper.encodeEntities(input), expected, `Test for encodeEntities() input: ${input} expected: ${expected}`) + }) +}) + +// Integration test +test(t => { + const testCases = [ + 'var test = \'test\'', + 'Boostnote', + '' + ] + + testCases.forEach(testCase => { + const encodedText = htmlTextHelper.encodeEntities(testCase) + const decodedText = htmlTextHelper.decodeEntities(encodedText) + t.is(decodedText, testCase, 'Integration test through encodedText() and decodedText()') + }) +})