diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js
index 6ca06ab3..47780c4b 100644
--- a/browser/components/CodeEditor.js
+++ b/browser/components/CodeEditor.js
@@ -219,6 +219,12 @@ export default class CodeEditor extends React.Component {
session.on('change', this.changeHandler)
}
+ setValue (value) {
+ let session = this.editor.getSession()
+ session.setValue(value)
+ this.value = value
+ }
+
render () {
let { className, fontFamily } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js
index 2e07bc39..978e28f2 100644
--- a/browser/components/MarkdownEditor.js
+++ b/browser/components/MarkdownEditor.js
@@ -73,6 +73,29 @@ class MarkdownEditor extends React.Component {
}
}
+ handleCheckboxClick (e) {
+ e.preventDefault()
+ e.stopPropagation()
+ let idMatch = /checkbox-([0-9]+)/
+ let checkedMatch = /\[x\]/i
+ let uncheckedMatch = /\[ \]/
+ if (idMatch.test(e.target.getAttribute('id'))) {
+ let lineIndex = parseInt(e.target.getAttribute('id').match(idMatch)[1], 10) - 1
+ let lines = this.refs.code.value
+ .split('\n')
+
+ let targetLine = lines[lineIndex]
+
+ if (targetLine.match(checkedMatch)) {
+ lines[lineIndex] = targetLine.replace(checkedMatch, '[ ]')
+ }
+ if (targetLine.match(uncheckedMatch)) {
+ lines[lineIndex] = targetLine.replace(uncheckedMatch, '[x]')
+ }
+ this.refs.code.setValue(lines.join('\n'))
+ }
+ }
+
focus () {
if (this.state.status === 'PREVIEW') {
this.setState({
@@ -135,6 +158,7 @@ class MarkdownEditor extends React.Component {
value={value}
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
onMouseDown={(e) => this.handlePreviewMouseDown(e)}
+ onCheckboxClick={(e) => this.handleCheckboxClick(e)}
/>
)
diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js
index 6d5183ae..c36c67b6 100644
--- a/browser/components/MarkdownPreview.js
+++ b/browser/components/MarkdownPreview.js
@@ -5,11 +5,6 @@ import hljsTheme from 'browser/lib/hljsThemes'
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const { shell } = require('electron')
-const goExternal = function (e) {
- e.preventDefault()
- e.stopPropagation()
- shell.openExternal(e.target.href)
-}
const OSX = global.process.platform === 'darwin'
@@ -27,6 +22,27 @@ export default class MarkdownPreview extends React.Component {
this.contextMenuHandler = (e) => this.handleContextMenu(e)
this.mouseDownHandler = (e) => this.handleMouseDown(e)
this.mouseUpHandler = (e) => this.handleMouseUp(e)
+ this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
+ this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
+ }
+
+ handlePreviewAnchorClick (e) {
+ e.preventDefault()
+ e.stopPropagation()
+
+ let href = e.target.getAttribute('href')
+ if (_.isString(href) && href.match(/^#/)) {
+ let targetElement = this.refs.root.contentWindow.document.getElementById(href.substring(1, href.length))
+ if (targetElement != null) {
+ this.getWindow().scrollTo(0, targetElement.offsetTop)
+ }
+ } else {
+ shell.openExternal(e.target.href)
+ }
+ }
+
+ handleCheckboxClick (e) {
+ this.props.onCheckboxClick(e)
}
handleContextMenu (e) {
@@ -34,10 +50,20 @@ export default class MarkdownPreview extends React.Component {
}
handleMouseDown (e) {
+ if (e.target != null) {
+ switch (e.target.tagName) {
+ case 'A':
+ case 'INPUT':
+ return null
+ }
+ }
if (this.props.onMouseDown != null) this.props.onMouseDown(e)
}
handleMouseUp (e) {
+ if (e.target != null && e.target.tagName === 'A') {
+ return null
+ }
if (this.props.onMouseUp != null) this.props.onMouseUp(e)
}
@@ -68,7 +94,10 @@ export default class MarkdownPreview extends React.Component {
rewriteIframe () {
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
- el.removeEventListener('click', goExternal)
+ el.removeEventListener('click', this.anchorClickHandler)
+ })
+ Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
+ el.removeEventListener('click', this.checkboxClickHandler)
})
let { value, fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme } = this.props
@@ -111,7 +140,10 @@ export default class MarkdownPreview extends React.Component {
this.refs.root.contentWindow.document.body.innerHTML = markdown(value)
Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
- el.addEventListener('mousedown', goExternal)
+ el.addEventListener('click', this.anchorClickHandler)
+ })
+ Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
+ el.addEventListener('click', this.checkboxClickHandler)
})
}
@@ -124,14 +156,14 @@ export default class MarkdownPreview extends React.Component {
}
scrollTo (targetRow) {
- let lineAnchors = this.getWindow().document.querySelectorAll('a.lineAnchor')
+ let blocks = this.getWindow().document.querySelectorAll('body>[data-line]')
- for (let index = 0; index < lineAnchors.length; index++) {
- let lineAnchor = lineAnchors[index]
- let row = parseInt(lineAnchor.getAttribute('data-key'))
+ for (let index = 0; index < blocks.length; index++) {
+ let block = blocks[index]
+ let row = parseInt(block.getAttribute('data-line'))
if (row > targetRow) {
- let targetAnchor = lineAnchors[index - 1]
- this.getWindow().scrollTo(0, targetAnchor.offsetTop)
+ let targetAnchor = blocks[index - 1]
+ targetAnchor != null && this.getWindow().scrollTo(0, targetAnchor.offsetTop)
break
}
}
@@ -147,6 +179,7 @@ export default class MarkdownPreview extends React.Component {
style={style}
tabIndex={tabIndex}
ref='root'
+ onClick={(e) => this.handleClick(e)}
/>
)
}
diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl
index 2cd0f3e2..38b839df 100644
--- a/browser/components/markdown.styl
+++ b/browser/components/markdown.styl
@@ -68,6 +68,10 @@ body
padding 5px
margin -5px
border-radius 5px
+li
+ label.taskListItem
+ margin-left -2em
+ background-color white
div.math-rendered
text-align center
.math-failed
@@ -102,12 +106,6 @@ a
background-color alpha(#FFC95C, 0.3)
&:visited
color brandColor
- &.lineAnchor
- padding 0
- margin 0
- display block
- font-size 0
- height 0
hr
border-top none
border-bottom solid 1px borderColor
@@ -147,9 +145,6 @@ h6
line-height 1.4em
margin 1em 0 1em
color #777
-
-*:not(a.lineAnchor) + p, *:not(a.lineAnchor) + blockquote, *:not(a.lineAnchor) + ul, *:not(a.lineAnchor) + ol, *:not(a.lineAnchor) + pre
- margin-top 1em
p
line-height 1.6em
margin 0 0 1em
@@ -195,8 +190,6 @@ code
font-size 0.85em
text-decoration none
margin-right 2px
-*:not(a.lineAnchor) + code
- margin-left 2px
pre
padding 0.5em !important
border solid 1px alpha(borderColor, 0.5)
diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js
index 9667bf80..56d4070e 100644
--- a/browser/lib/markdown.js
+++ b/browser/lib/markdown.js
@@ -2,6 +2,7 @@ import markdownit from 'markdown-it'
import emoji from 'markdown-it-emoji'
import math from '@rokt33r/markdown-it-math'
import hljs from 'highlight.js'
+import _ from 'lodash'
const katex = window.katex
@@ -59,21 +60,77 @@ md.use(math, {
return output
}
})
-md.use(require('markdown-it-checkbox'))
+md.use(require('markdown-it-footnote'))
+// Override task item
+md.block.ruler.at('paragraph', function (state, startLine/*, endLine*/) {
+ let content, terminate, i, l, token
+ let nextLine = startLine + 1
+ let terminatorRules = state.md.block.ruler.getRules('paragraph')
+ let endLine = state.lineMax
-let originalRenderToken = md.renderer.renderToken
-md.renderer.renderToken = function renderToken (tokens, idx, options) {
- let token = tokens[idx]
+ // 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 }
- let result = originalRenderToken.call(md.renderer, tokens, idx, options)
- if (token.map != null) {
- return result + ''
+ // 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') {
+ let match = content.match(/\[( |x)\] ?(.+)/i)
+ if (match) {
+ 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
+let 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]])
+ }
+ })
+ let result = originalRender.call(md.renderer, tokens, options, env)
return result
}
+window.md = md
export default function markdown (content) {
- if (content == null) content = ''
+ if (!_.isString(content)) content = ''
- return md.render(content.toString())
+ return md.render(content)
}
diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js
index b31054fe..4ef6b806 100644
--- a/browser/main/Detail/MarkdownNoteDetail.js
+++ b/browser/main/Detail/MarkdownNoteDetail.js
@@ -89,15 +89,17 @@ class MarkdownNoteDetail extends React.Component {
}
save () {
- let { note, dispatch } = this.props
+ clearTimeout(this.saveQueue)
+ this.saveQueue = setTimeout(() => {
+ let { note, dispatch } = this.props
+ dispatch({
+ type: 'UPDATE_NOTE',
+ note: this.state.note
+ })
- dispatch({
- type: 'UPDATE_NOTE',
- note: this.state.note
- })
-
- dataApi
- .updateNote(note.storage, note.folder, note.key, this.state.note)
+ dataApi
+ .updateNote(note.storage, note.folder, note.key, this.state.note)
+ }, 1000)
}
handleFolderChange (e) {
diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js
index fba354ff..dc71a045 100644
--- a/browser/main/Detail/SnippetNoteDetail.js
+++ b/browser/main/Detail/SnippetNoteDetail.js
@@ -99,15 +99,17 @@ class SnippetNoteDetail extends React.Component {
}
save () {
- let { note, dispatch } = this.props
+ clearTimeout(this.saveQueue)
+ this.saveQueue = setTimeout(() => {
+ let { note, dispatch } = this.props
+ dispatch({
+ type: 'UPDATE_NOTE',
+ note: this.state.note
+ })
- dispatch({
- type: 'UPDATE_NOTE',
- note: this.state.note
- })
-
- dataApi
- .updateNote(note.storage, note.folder, note.key, this.state.note)
+ dataApi
+ .updateNote(note.storage, note.folder, note.key, this.state.note)
+ }, 1000)
}
handleFolderChange (e) {
diff --git a/package.json b/package.json
index a01b7d5c..ec1c6da1 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
"markdown-it": "^6.0.1",
"markdown-it-checkbox": "^1.1.0",
"markdown-it-emoji": "^1.1.1",
+ "markdown-it-footnote": "^3.0.0",
"md5": "^2.0.0",
"moment": "^2.10.3",
"sander": "^0.5.1",