1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 17:56:25 +00:00

Merge pull request #1621 from rayou/feature/add-smartquotes-toggle

Added support for toggling smart quotes in preview
This commit is contained in:
Junyoung Choi (Sai)
2018-03-09 23:37:42 +09:00
committed by GitHub
7 changed files with 295 additions and 177 deletions

View File

@@ -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)}

View File

@@ -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) => {
@@ -311,6 +318,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 ||
@@ -376,7 +387,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)
@@ -392,9 +403,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)
@@ -535,5 +546,6 @@ MarkdownPreview.propTypes = {
className: PropTypes.string,
value: PropTypes.string,
showCopyNotification: PropTypes.bool,
storagePath: PropTypes.string
storagePath: PropTypes.string,
smartQuotes: PropTypes.bool
}

View File

@@ -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}

View File

@@ -20,181 +20,187 @@ function createGutter (str, firstLineNumber) {
return '<span class="lineNumber CodeMirror-gutters">' + lines.join('') + '</span>'
}
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)
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 `<pre class="flowchart">${str}</pre>`
}
if (langType === 'sequence') {
return `<pre class="sequence">${str}</pre>`
}
return '<pre class="code CodeMirror">' +
'<span class="filename">' + fileName + '</span>' +
createGutter(str, firstLineNumber) +
'<code class="' + langType + '">' +
str +
'</code></pre>'
}
})
// Sanitize use rinput before other plugins
md.use(sanitize, {
allowedTags: ['img', 'iframe'],
allowedAttributes: {
'*': ['alt', 'style'],
'img': ['src', 'width', 'height'],
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen']
},
allowedIframeHostnames: ['www.youtube.com']
})
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 = `<span class="katex-error">${err.message}</span>`
}
return output
},
blockRenderer: function (str) {
let output = ''
try {
output = katex.renderToString(str.trim(), { displayMode: true })
} catch (err) {
output = `<div class="katex-error">${err.message}</div>`
}
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
}
}
if (terminate) { break }
}
content = state.getLines(startLine, nextLine, state.blkIndent, false).trim()
state.line = nextLine
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 (langType === 'flowchart') {
return `<pre class="flowchart">${str}</pre>`
}
liToken.attrs.push(['class', 'taskListItem'])
if (langType === 'sequence') {
return `<pre class="sequence">${str}</pre>`
}
return '<pre class="code">' +
'<span class="filename">' + fileName + '</span>' +
createGutter(str, firstLineNumber) +
'<code class="' + langType + '">' +
str +
'</code></pre>'
}
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
}
const updatedOptions = Object.assign(defaultOptions, options)
this.md = markdownit(updatedOptions)
// Sanitize use rinput before other plugins
this.md.use(sanitize, {
allowedTags: ['img', 'iframe'],
allowedAttributes: {
'*': ['alt', 'style'],
'img': ['src', 'width', 'height'],
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen']
},
allowedIframeHostnames: ['www.youtube.com']
})
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 = `<span class="katex-error">${err.message}</span>`
}
return output
},
blockRenderer: function (str) {
let output = ''
try {
output = katex.renderToString(str.trim(), { displayMode: true })
} catch (err) {
output = `<div class="katex-error">${err.message}</div>`
}
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
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 = []
}
liToken.attrs.push(['class', 'taskListItem'])
}
content = `<label class='taskListItem${match[1] !== ' ' ? ' checked' : ''}' for='checkbox-${startLine + 1}'><input type='checkbox'${match[1] !== ' ' ? ' checked' : ''} id='checkbox-${startLine + 1}'/> ${content.substring(4, content.length)}</label>`
}
}
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
function normalizeLinkText (linkText) {
return md.normalizeLinkText(linkText)
}
const markdown = {
render: function markdown (content) {
render (content) {
if (!_.isString(content)) content = ''
const renderedContent = md.render(content)
return renderedContent
},
normalizeLinkText
return this.md.render(content)
}
normalizeLinkText (linkText) {
return this.md.normalizeLinkText(linkText)
}
}
export default markdown
export default Markdown

View File

@@ -48,7 +48,8 @@ export const DEFAULT_CONFIG = {
latexInlineClose: '$',
latexBlockOpen: '$$',
latexBlockClose: '$$',
scrollPastEnd: false
scrollPastEnd: false,
smartQuotes: true
},
blog: {
type: 'wordpress', // Available value: wordpress, add more types in the future plz

View File

@@ -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
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.preview.smartQuotes}
ref='previewSmartQuotes'
type='checkbox'
/>&nbsp;
Enable smart quotes
</label>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>
LaTeX Inline Open Delimiter

86
docs/zh_TW/build.md Normal file
View File

@@ -0,0 +1,86 @@
# 編譯
此文件還提供下列的語言 [日文](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [韓文](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [俄文](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [簡體中文](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [法文](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) and [德文](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md).
## 環境
* npm: 4.x
* node: 7.x
`$ grunt pre-build``npm v5.x` 有問題,所以只能用 `npm v4.x`
## 開發
我們使用 Webpack HMR 來開發 Boostnote。

在專案根目錄底下執行下列指令,將會以原始設置啟動 Boostnote。
**用 yarn 來安裝必要 packages**
```bash
$ yarn
```
**開始開發**
```
$ yarn run dev-start
```
上述指令同時運行了 `yarn run webpack``yarn run hot`,相當於將這兩個指令在不同的 terminal 中運行。
`webpack` 會同時監控修改過的程式碼,並
The `webpack` will watch for code changes and then apply them automatically.
If the following error occurs: `Failed to load resource: net::ERR_CONNECTION_REFUSED`, please reload Boostnote.
![net::ERR_CONNECTION_REFUSED](https://cloud.githubusercontent.com/assets/11307908/24343004/081e66ae-1279-11e7-8d9e-7f478043d835.png)
> ### Notice
> There are some cases where you have to refresh the app manually.
> 1. When editing a constructor method of a component
> 2. When adding a new css class (similar to 1: the CSS class is re-written by each component. This process occurs at the Constructor method.)
## Deploy
We use Grunt to automate deployment.
You can build the program by using `grunt`. However, we don't recommend this because the default task includes codesign and authenticode.
So, we've prepared a separate script which just makes an executable file.
This build doesn't work on npm v5.3.0. So you need to use v5.2.0 when you build it.
```
grunt pre-build
```
You will find the executable in the `dist` directory. Note, the auto updater won't work because the app isn't signed.
If you find it necessary, you can use codesign or authenticode with this executable.
## Make own distribution packages (deb, rpm)
Distribution packages are created by exec `grunt build` on Linux platform (e.g. Ubuntu, Fedora).
> Note: You can create both `.deb` and `.rpm` in a single environment.
After installing the supported version of `node` and `npm`, install build dependency packages.
Ubuntu/Debian:
```
$ sudo apt-get install -y rpm fakeroot
```
Fedora:
```
$ sudo dnf install -y dpkg dpkg-dev rpm-build fakeroot
```
Then execute `grunt build`.
```
$ grunt build
```
You will find `.deb` and `.rpm` in the `dist` directory.