diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 124bd8c30..c3d3a7305 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -219,7 +219,7 @@ export default class MarkdownPreview extends React.Component { const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams() const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS) - let body = this.markdown.render(escapeHtmlCharacters(noteContent)) + let body = this.markdown.render(escapeHtmlCharacters(noteContent, { detectCodeBlock: true })) const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(noteContent, this.props.storagePath) @@ -412,7 +412,7 @@ export default class MarkdownPreview extends React.Component { value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock)) }) } - let renderedHTML = this.markdown.render(value) + const renderedHTML = this.markdown.render(value) attachmentManagement.migrateAttachments(renderedHTML, storagePath, noteKey) this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath) diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 4dafa4a3a..931697a38 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -245,4 +245,3 @@ class Markdown { } export default Markdown - diff --git a/browser/lib/utils.js b/browser/lib/utils.js index 441cfbc7a..564ed3d25 100644 --- a/browser/lib/utils.js +++ b/browser/lib/utils.js @@ -6,52 +6,64 @@ export function lastFindInArray (array, callback) { } } -export function escapeHtmlCharacters (text) { - const matchHtmlRegExp = /["'&<>]/ - const str = '' + text - const match = matchHtmlRegExp.exec(str) +export function escapeHtmlCharacters (html, opt = { detectCodeBlock: false }) { + const matchHtmlRegExp = /["'&<>]/g + const escapes = ['"', '&', ''', '<', '>'] + let match = null + const replaceAt = (str, index, replace) => + str.substr(0, index) + + replace + + str.substr(index + replace.length - (replace.length - 1)) - if (!match) { - return str - } - - let escape - let html = '' - let index = 0 - let lastIndex = 0 - - for (index = match.index; index < str.length; index++) { - switch (str.charCodeAt(index)) { - case 34: // " - escape = '"' - break - case 38: // & - escape = '&' - break - case 39: // ' - escape = ''' - break - case 60: // < - escape = '<' - break - case 62: // > - escape = '>' - break - default: + // detecting code block + while ((match = matchHtmlRegExp.exec(html)) != null) { + const current = { char: match[0], index: match.index } + if (opt.detectCodeBlock) { + // position of the nearest line start + let previousLineEnd = current.index - 1 + while (html[previousLineEnd] !== '\n' && previousLineEnd !== -1) { + previousLineEnd-- + } + // 4 spaces means this character is in a code block + if ( + html[previousLineEnd + 1] === ' ' && + html[previousLineEnd + 2] === ' ' && + html[previousLineEnd + 3] === ' ' && + html[previousLineEnd + 4] === ' ' + ) { + // so skip it continue + } } - - if (lastIndex !== index) { - html += str.substring(lastIndex, index) + // otherwise, escape it !!! + if (current.char === '&') { + let nextStr = '' + let nextIndex = current.index + let escapedStr = false + // maximum length of an escape string is 5. For example ('"') + while (nextStr.length <= 5) { + nextStr += html[nextIndex] + nextIndex++ + if (escapes.indexOf(nextStr) !== -1) { + escapedStr = true + break + } + } + if (!escapedStr) { + // this & char is not a part of an escaped string + html = replaceAt(html, current.index, '&') + } + } else if (current.char === '"') { + html = replaceAt(html, current.index, '"') + } else if (current.char === "'") { + html = replaceAt(html, current.index, ''') + } else if (current.char === '<') { + html = replaceAt(html, current.index, '<') + } else if (current.char === '>') { + html = replaceAt(html, current.index, '>') } - - lastIndex = index + 1 - html += escape } - - return lastIndex !== index - ? html + str.substring(lastIndex, index) - : html + return html } export function isObjectEqual (a, b) { diff --git a/tests/lib/escapeHtmlCharacters-test.js b/tests/lib/escapeHtmlCharacters-test.js new file mode 100644 index 000000000..f13ab2975 --- /dev/null +++ b/tests/lib/escapeHtmlCharacters-test.js @@ -0,0 +1,48 @@ +const { escapeHtmlCharacters } = require('browser/lib/utils') +const test = require('ava') + +test('escapeHtmlCharacters should return the original string if nothing needed to escape', t => { + const input = 'Nothing to be escaped' + const expected = 'Nothing to be escaped' + const actual = escapeHtmlCharacters(input) + t.is(actual, expected) +}) + +test('escapeHtmlCharacters should skip code block if that option is enabled', t => { + const input = ` +` + const expected = ` +<escapeMe>` + const actual = escapeHtmlCharacters(input, { detectCodeBlock: true }) + t.is(actual, expected) +}) + +test('escapeHtmlCharacters should NOT skip character not in code block but start with 4 spaces', t => { + const input = '4 spaces &' + const expected = '4 spaces &' + const actual = escapeHtmlCharacters(input, { detectCodeBlock: true }) + t.is(actual, expected) +}) + +test('escapeHtmlCharacters should NOT skip code block if that option is NOT enabled', t => { + const input = ` +` + const expected = ` <no escape> +<escapeMe>` + const actual = escapeHtmlCharacters(input) + t.is(actual, expected) +}) + +test('escapeHtmlCharacters should NOT escape & character if it\'s a part of an escaped character', t => { + const input = 'Do not escape & or " but do escape &' + const expected = 'Do not escape & or " but do escape &' + const actual = escapeHtmlCharacters(input) + t.is(actual, expected) +}) + +test('escapeHtmlCharacters should return the correct result', t => { + const input = '& < > " \'' + const expected = '& < > " '' + const actual = escapeHtmlCharacters(input) + t.is(actual, expected) +})