From 65e1a390277320badcb291deaf3551139364471c Mon Sep 17 00:00:00 2001 From: Dick Choi Date: Tue, 26 Jul 2016 01:13:08 +0900 Subject: [PATCH 1/5] debounce dispatch --- browser/main/Detail/MarkdownNoteDetail.js | 18 ++++++++++-------- browser/main/Detail/SnippetNoteDetail.js | 18 ++++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) 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) { From 80d16233e788be70872889194df7bce8cd7156b6 Mon Sep 17 00:00:00 2001 From: Dick Choi Date: Tue, 26 Jul 2016 01:44:26 +0900 Subject: [PATCH 2/5] foot note --- browser/components/MarkdownPreview.js | 32 +++++++++++++++++++++------ browser/lib/markdown.js | 1 + package.json | 1 + 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 6d5183ae..9a09679b 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,22 @@ 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.goExternalHandler = (e) => this.handlePreviewAnchorClick(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) + } } handleContextMenu (e) { @@ -34,10 +45,16 @@ export default class MarkdownPreview extends React.Component { } handleMouseDown (e) { + if (e.target != null && e.target.tagName === 'A') { + 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 +85,7 @@ 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.goExternalHandler) }) let { value, fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme } = this.props @@ -111,7 +128,7 @@ 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.goExternalHandler) }) } @@ -147,6 +164,7 @@ export default class MarkdownPreview extends React.Component { style={style} tabIndex={tabIndex} ref='root' + onClick={(e) => this.handleClick(e)} /> ) } diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 9667bf80..be2afc59 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -60,6 +60,7 @@ md.use(math, { } }) md.use(require('markdown-it-checkbox')) +md.use(require('markdown-it-footnote')) let originalRenderToken = md.renderer.renderToken md.renderer.renderToken = function renderToken (tokens, idx, options) { 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", From c6eff157dee6671a3c484f41e0a3aae428930db6 Mon Sep 17 00:00:00 2001 From: Dick Choi Date: Tue, 26 Jul 2016 13:27:59 +0900 Subject: [PATCH 3/5] improve line anchors placement --- browser/components/MarkdownPreview.js | 2 +- browser/components/markdown.styl | 3 ++- browser/lib/markdown.js | 22 +++++++++------------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 9a09679b..8a5b1353 100644 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -148,7 +148,7 @@ export default class MarkdownPreview extends React.Component { let row = parseInt(lineAnchor.getAttribute('data-key')) if (row > targetRow) { let targetAnchor = lineAnchors[index - 1] - this.getWindow().scrollTo(0, targetAnchor.offsetTop) + targetAnchor != null && this.getWindow().scrollTo(0, targetAnchor.offsetTop) break } } diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index 2cd0f3e2..5d7ea39c 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -105,9 +105,10 @@ a &.lineAnchor padding 0 margin 0 - display block + display inline-block font-size 0 height 0 + width 0 hr border-top none border-bottom solid 1px borderColor diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index be2afc59..31111e61 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -59,22 +59,18 @@ md.use(math, { return output } }) -md.use(require('markdown-it-checkbox')) md.use(require('markdown-it-footnote')) -let originalRenderToken = md.renderer.renderToken -md.renderer.renderToken = function renderToken (tokens, idx, options) { - let token = tokens[idx] - - let result = originalRenderToken.call(md.renderer, tokens, idx, options) - if (token.map != null) { - return result + '' - } - return result -} +window.md = md export default function markdown (content) { if (content == null) content = '' - - return md.render(content.toString()) + content = content.toString() + .split('\n') + .map((line, index) => { + if (line.trim().length === 0) return '' + return line + '' + }) + .join('\n') + return md.render(content) } From 49a4b5feb4f7a06a1f606450d9eda810d072297a Mon Sep 17 00:00:00 2001 From: Dick Choi Date: Tue, 26 Jul 2016 17:07:56 +0900 Subject: [PATCH 4/5] no more line anchors --- browser/components/MarkdownPreview.js | 4 ++-- browser/components/markdown.styl | 12 ------------ browser/lib/markdown.js | 25 +++++++++++++++++-------- 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 8a5b1353..9936f9fa 100644 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -141,11 +141,11 @@ export default class MarkdownPreview extends React.Component { } scrollTo (targetRow) { - let lineAnchors = this.getWindow().document.querySelectorAll('a.lineAnchor') + let lineAnchors = this.getWindow().document.querySelectorAll('[data-line]') for (let index = 0; index < lineAnchors.length; index++) { let lineAnchor = lineAnchors[index] - let row = parseInt(lineAnchor.getAttribute('data-key')) + let row = parseInt(lineAnchor.getAttribute('data-line')) if (row > targetRow) { let targetAnchor = lineAnchors[index - 1] targetAnchor != null && this.getWindow().scrollTo(0, targetAnchor.offsetTop) diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index 5d7ea39c..c563d9ef 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -102,13 +102,6 @@ a background-color alpha(#FFC95C, 0.3) &:visited color brandColor - &.lineAnchor - padding 0 - margin 0 - display inline-block - font-size 0 - height 0 - width 0 hr border-top none border-bottom solid 1px borderColor @@ -148,9 +141,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 @@ -196,8 +186,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 31111e61..bd3d9a6b 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 @@ -61,16 +62,24 @@ md.use(math, { }) md.use(require('markdown-it-footnote')) +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 = '' - content = content.toString() - .split('\n') - .map((line, index) => { - if (line.trim().length === 0) return '' - return line + '' - }) - .join('\n') + if (!_.isString(content)) content = '' + return md.render(content) } From 9cd6d6d4c14d30a4a346b0fd56f031b6337f13c7 Mon Sep 17 00:00:00 2001 From: Dick Choi Date: Tue, 26 Jul 2016 20:00:32 +0900 Subject: [PATCH 5/5] GFM checkbox --- browser/components/CodeEditor.js | 6 ++++ browser/components/MarkdownEditor.js | 24 +++++++++++++ browser/components/MarkdownPreview.js | 35 ++++++++++++------ browser/components/markdown.styl | 4 +++ browser/lib/markdown.js | 51 +++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 10 deletions(-) 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 9936f9fa..c36c67b6 100644 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -22,7 +22,8 @@ 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.goExternalHandler = (e) => this.handlePreviewAnchorClick(e) + this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e) + this.checkboxClickHandler = (e) => this.handleCheckboxClick(e) } handlePreviewAnchorClick (e) { @@ -40,13 +41,21 @@ export default class MarkdownPreview extends React.Component { } } + handleCheckboxClick (e) { + this.props.onCheckboxClick(e) + } + handleContextMenu (e) { this.props.onContextMenu(e) } handleMouseDown (e) { - if (e.target != null && e.target.tagName === 'A') { - return null + if (e.target != null) { + switch (e.target.tagName) { + case 'A': + case 'INPUT': + return null + } } if (this.props.onMouseDown != null) this.props.onMouseDown(e) } @@ -85,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', this.goExternalHandler) + 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 @@ -128,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('click', this.goExternalHandler) + el.addEventListener('click', this.anchorClickHandler) + }) + Array.prototype.forEach.call(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => { + el.addEventListener('click', this.checkboxClickHandler) }) } @@ -141,13 +156,13 @@ export default class MarkdownPreview extends React.Component { } scrollTo (targetRow) { - let lineAnchors = this.getWindow().document.querySelectorAll('[data-line]') + 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-line')) + 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] + let targetAnchor = blocks[index - 1] targetAnchor != null && this.getWindow().scrollTo(0, targetAnchor.offsetTop) break } diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index c563d9ef..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 diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index bd3d9a6b..56d4070e 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -61,7 +61,58 @@ md.use(math, { } }) 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 + // 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') { + 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) => {