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