From e89c0a3e61e1eb5ac3761a1ede5fd6c6bd5d389e Mon Sep 17 00:00:00 2001 From: Yu-Hung Ou Date: Fri, 2 Mar 2018 00:06:58 +1100 Subject: [PATCH] added support for toggling smart quotes in preview --- browser/components/MarkdownEditor.js | 1 + browser/components/MarkdownPreview.js | 24 +- browser/components/MarkdownSplitEditor.js | 1 + browser/lib/markdown.js | 310 +++++++++--------- browser/main/lib/ConfigManager.js | 3 +- browser/main/modals/PreferencesModal/UiTab.js | 13 +- 6 files changed, 191 insertions(+), 161 deletions(-) diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index f02a146a8..d0e2f505b 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -279,6 +279,7 @@ class MarkdownEditor extends React.Component { lineNumber={config.preview.lineNumber} indentSize={editorIndentSize} scrollPastEnd={config.preview.scrollPastEnd} + smartQuotes={config.preview.smartQuotes} ref='preview' onContextMenu={(e) => this.handleContextMenu(e)} onDoubleClick={(e) => this.handleDoubleClick(e)} diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index c5b0355de..ddda74bbd 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types' import React from 'react' -import markdown from 'browser/lib/markdown' +import Markdown from 'browser/lib/markdown' import _ from 'lodash' import CodeMirror from 'codemirror' import 'codemirror-mode-elixir' @@ -130,6 +130,13 @@ export default class MarkdownPreview extends React.Component { this.printHandler = () => this.handlePrint() this.linkClickHandler = this.handlelinkClick.bind(this) + this.initMarkdown = this.initMarkdown.bind(this) + this.initMarkdown() + } + + initMarkdown () { + const { smartQuotes } = this.props + this.markdown = new Markdown({ typographer: smartQuotes }) } handlePreviewAnchorClick (e) { @@ -198,7 +205,7 @@ export default class MarkdownPreview extends React.Component { const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams() const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber) - const body = markdown.render(noteContent) + const body = this.markdown.render(noteContent) const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] files.forEach((file) => { @@ -309,6 +316,10 @@ export default class MarkdownPreview extends React.Component { componentDidUpdate (prevProps) { if (prevProps.value !== this.props.value) this.rewriteIframe() + if (prevProps.smartQuotes !== this.props.smartQuotes) { + this.initMarkdown() + this.rewriteIframe() + } if (prevProps.fontFamily !== this.props.fontFamily || prevProps.fontSize !== this.props.fontSize || prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily || @@ -374,7 +385,7 @@ export default class MarkdownPreview extends React.Component { value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock)) }) } - this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value) + this.refs.root.contentWindow.document.body.innerHTML = this.markdown.render(value) _.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { this.fixDecodedURI(el) @@ -390,9 +401,9 @@ export default class MarkdownPreview extends React.Component { }) _.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => { - el.src = markdown.normalizeLinkText(el.src) + el.src = this.markdown.normalizeLinkText(el.src) if (!/\/:storage/.test(el.src)) return - el.src = `file:///${markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}` + el.src = `file:///${this.markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}` }) codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme) @@ -533,5 +544,6 @@ MarkdownPreview.propTypes = { className: PropTypes.string, value: PropTypes.string, showCopyNotification: PropTypes.bool, - storagePath: PropTypes.string + storagePath: PropTypes.string, + smartQuotes: PropTypes.bool } diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index 505fbaf42..0aa2d16c8 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -127,6 +127,7 @@ class MarkdownSplitEditor extends React.Component { codeBlockFontFamily={config.editor.fontFamily} lineNumber={config.preview.lineNumber} scrollPastEnd={config.preview.scrollPastEnd} + smartQuotes={config.preview.smartQuotes} ref='preview' tabInde='0' value={value} diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index d0801a1b9..e75e13ee4 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -19,171 +19,175 @@ function createGutter (str, firstLineNumber) { return '' + lines.join('') + '' } -var md = markdownit({ - typographer: true, - linkify: true, - html: true, - xhtmlOut: true, - breaks: true, - highlight: function (str, lang) { - const delimiter = ':' - const langInfo = lang.split(delimiter) - const langType = langInfo[0] - const fileName = langInfo[1] || '' - const firstLineNumber = parseInt(langInfo[2], 10) - - if (langType === 'flowchart') { - return `
${str}
` - } - if (langType === 'sequence') { - return `
${str}
` - } - return '
' +
-      '' + fileName + '' +
-      createGutter(str, firstLineNumber) +
-      '' +
-      str +
-      '
' - } -}) -md.use(emoji, { - shortcuts: {} -}) -md.use(math, { - inlineOpen: config.preview.latexInlineOpen, - inlineClose: config.preview.latexInlineClose, - blockOpen: config.preview.latexBlockOpen, - blockClose: config.preview.latexBlockClose, - inlineRenderer: function (str) { - let output = '' - try { - output = katex.renderToString(str.trim()) - } catch (err) { - output = `${err.message}` - } - return output - }, - blockRenderer: function (str) { - let output = '' - try { - output = katex.renderToString(str.trim(), { displayMode: true }) - } catch (err) { - output = `
${err.message}
` - } - return output - } -}) -md.use(require('markdown-it-imsize')) -md.use(require('markdown-it-footnote')) -md.use(require('markdown-it-multimd-table')) -md.use(require('markdown-it-named-headers'), { - slugify: (header) => { - return encodeURI(header.trim() - .replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '') - .replace(/\s+/g, '-')) - .replace(/\-+$/, '') - } -}) -md.use(require('markdown-it-kbd')) - -const deflate = require('markdown-it-plantuml/lib/deflate') -md.use(require('markdown-it-plantuml'), '', { - generateSource: function (umlCode) { - const s = unescape(encodeURIComponent(umlCode)) - const zippedCode = deflate.encode64( - deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9) - ) - return `http://www.plantuml.com/plantuml/svg/${zippedCode}` - } -}) - -// Override task item -md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) { - let content, terminate, i, l, token - let nextLine = startLine + 1 - const terminatorRules = state.md.block.ruler.getRules('paragraph') - const endLine = state.lineMax - - // jump line-by-line until empty one or EOF - for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { - // this would be a code block normally, but after paragraph - // it's considered a lazy continuation regardless of what's there - if (state.sCount[nextLine] - state.blkIndent > 3) { continue } - - // quirk for blockquotes, this line should already be checked by that rule - if (state.sCount[nextLine] < 0) { continue } - - // Some tags can terminate paragraph without empty line. - terminate = false - for (i = 0, l = terminatorRules.length; i < l; i++) { - if (terminatorRules[i](state, nextLine, endLine, true)) { - terminate = true - break +class Markdown { + constructor (options = {}) { + const defaultOptions = { + typographer: true, + linkify: true, + html: true, + xhtmlOut: true, + breaks: true, + highlight: function (str, lang) { + const delimiter = ':' + const langInfo = lang.split(delimiter) + const langType = langInfo[0] + const fileName = langInfo[1] || '' + const firstLineNumber = parseInt(langInfo[2], 10) + + if (langType === 'flowchart') { + return `
${str}
` + } + if (langType === 'sequence') { + return `
${str}
` + } + return '
' +
+          '' + fileName + '' +
+          createGutter(str, firstLineNumber) +
+          '' +
+          str +
+          '
' } } - if (terminate) { break } - } - content = state.getLines(startLine, nextLine, state.blkIndent, false).trim() + const updatedOptions = Object.assign(defaultOptions, options) + this.md = markdownit(updatedOptions) + this.md.use(emoji, { + shortcuts: {} + }) + this.md.use(math, { + inlineOpen: config.preview.latexInlineOpen, + inlineClose: config.preview.latexInlineClose, + blockOpen: config.preview.latexBlockOpen, + blockClose: config.preview.latexBlockClose, + inlineRenderer: function (str) { + let output = '' + try { + output = katex.renderToString(str.trim()) + } catch (err) { + output = `${err.message}` + } + return output + }, + blockRenderer: function (str) { + let output = '' + try { + output = katex.renderToString(str.trim(), { displayMode: true }) + } catch (err) { + output = `
${err.message}
` + } + return output + } + }) + this.md.use(require('markdown-it-imsize')) + this.md.use(require('markdown-it-footnote')) + this.md.use(require('markdown-it-multimd-table')) + this.md.use(require('markdown-it-named-headers'), { + slugify: (header) => { + return encodeURI(header.trim() + .replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '') + .replace(/\s+/g, '-')) + .replace(/\-+$/, '') + } + }) + this.md.use(require('markdown-it-kbd')) + + const deflate = require('markdown-it-plantuml/lib/deflate') + this.md.use(require('markdown-it-plantuml'), '', { + generateSource: function (umlCode) { + const s = unescape(encodeURIComponent(umlCode)) + const zippedCode = deflate.encode64( + deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9) + ) + return `http://www.plantuml.com/plantuml/svg/${zippedCode}` + } + }) + + // Override task item + this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) { + let content, terminate, i, l, token + let nextLine = startLine + 1 + const terminatorRules = state.md.block.ruler.getRules('paragraph') + const endLine = state.lineMax + + // jump line-by-line until empty one or EOF + for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { + // this would be a code block normally, but after paragraph + // it's considered a lazy continuation regardless of what's there + if (state.sCount[nextLine] - state.blkIndent > 3) { continue } + + // quirk for blockquotes, this line should already be checked by that rule + if (state.sCount[nextLine] < 0) { continue } + + // Some tags can terminate paragraph without empty line. + terminate = false + for (i = 0, l = terminatorRules.length; i < l; i++) { + if (terminatorRules[i](state, nextLine, endLine, true)) { + terminate = true + break + } + } + if (terminate) { break } + } + + content = state.getLines(startLine, nextLine, state.blkIndent, false).trim() - state.line = nextLine + state.line = nextLine - token = state.push('paragraph_open', 'p', 1) - token.map = [startLine, state.line] + token = state.push('paragraph_open', 'p', 1) + token.map = [startLine, state.line] - if (state.parentType === 'list') { - const match = content.match(/^\[( |x)\] ?(.+)/i) - if (match) { - const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open') - if (liToken) { - if (!liToken.attrs) { - liToken.attrs = [] + if (state.parentType === 'list') { + const match = content.match(/^\[( |x)\] ?(.+)/i) + if (match) { + const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open') + if (liToken) { + if (!liToken.attrs) { + liToken.attrs = [] + } + liToken.attrs.push(['class', 'taskListItem']) + } + content = `` } - liToken.attrs.push(['class', 'taskListItem']) } - content = `` + + token = state.push('inline', '', 0) + token.content = content + token.map = [startLine, state.line] + token.children = [] + + token = state.push('paragraph_close', 'p', -1) + + return true + }) + + // Add line number attribute for scrolling + const originalRender = this.md.renderer.render + this.md.renderer.render = (tokens, options, env) => { + tokens.forEach((token) => { + switch (token.type) { + case 'heading_open': + case 'paragraph_open': + case 'blockquote_open': + case 'table_open': + token.attrPush(['data-line', token.map[0]]) + } + }) + const result = originalRender.call(this.md.renderer, tokens, options, env) + return result } + // FIXME We should not depend on global variable. + window.md = this.md } - token = state.push('inline', '', 0) - token.content = content - token.map = [startLine, state.line] - token.children = [] - - token = state.push('paragraph_close', 'p', -1) - - return true -}) - -// Add line number attribute for scrolling -const originalRender = md.renderer.render -md.renderer.render = function render (tokens, options, env) { - tokens.forEach((token) => { - switch (token.type) { - case 'heading_open': - case 'paragraph_open': - case 'blockquote_open': - case 'table_open': - token.attrPush(['data-line', token.map[0]]) - } - }) - const result = originalRender.call(md.renderer, tokens, options, env) - return result -} -// FIXME We should not depend on global variable. -window.md = md + render (content) { + if (!_.isString(content)) content = '' + return this.md.render(content) + } -function normalizeLinkText (linkText) { - return md.normalizeLinkText(linkText) + normalizeLinkText (linkText) { + return this.md.normalizeLinkText(linkText) + } } -const markdown = { - render: function markdown (content) { - if (!_.isString(content)) content = '' - const renderedContent = md.render(content) - return renderedContent - }, - normalizeLinkText -} +export default Markdown -export default markdown diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index 0c8d6ee90..7080105c4 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -48,7 +48,8 @@ export const DEFAULT_CONFIG = { latexInlineClose: '$', latexBlockOpen: '$$', latexBlockClose: '$$', - scrollPastEnd: false + scrollPastEnd: false, + smartQuotes: true } } diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index 50e13f6c8..ddffe6d9c 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -88,7 +88,8 @@ class UiTab extends React.Component { latexInlineClose: this.refs.previewLatexInlineClose.value, latexBlockOpen: this.refs.previewLatexBlockOpen.value, latexBlockClose: this.refs.previewLatexBlockClose.value, - scrollPastEnd: this.refs.previewScrollPastEnd.checked + scrollPastEnd: this.refs.previewScrollPastEnd.checked, + smartQuotes: this.refs.previewSmartQuotes.checked } } @@ -402,6 +403,16 @@ class UiTab extends React.Component { Show line numbers for preview code blocks +
+ +
LaTeX Inline Open Delimiter