diff --git a/.babelrc b/.babelrc
index 92bb81ed..270349d2 100644
--- a/.babelrc
+++ b/.babelrc
@@ -5,7 +5,7 @@
"presets": ["react-hmre"]
},
"test": {
- "presets": ["react", "es2015"],
+ "presets": ["env" ,"react", "es2015"],
"plugins": [
[ "babel-plugin-webpack-alias", { "config": "${PWD}/webpack.config.js" } ]
]
diff --git a/.boostnoterc.sample b/.boostnoterc.sample
index 2caa2c1a..2d581a48 100644
--- a/.boostnoterc.sample
+++ b/.boostnoterc.sample
@@ -10,7 +10,6 @@
"theme": "monokai"
},
"hotkey": {
- "toggleFinder": "Cmd + Alt + S",
"toggleMain": "Cmd + Alt + L"
},
"isSideNavFolded": false,
@@ -23,7 +22,10 @@
"fontSize": "14",
"lineNumber": true
},
- "sortBy": "UPDATED_AT",
+ "sortBy": {
+ "default": "UPDATED_AT"
+ },
+ "sortTagsBy": "ALPHABETICAL",
"ui": {
"defaultNote": "ALWAYS_ASK",
"disableDirectWrite": false,
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 00000000..a4730cbf
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,16 @@
+# EditorConfig is awesome: http://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Space indentation
+[*]
+indent_style = space
+indent_size = 2
+trim_trailing_whitespace = true
+
+# The indent size used in the `package.json` file cannot be changed
+# https://github.com/npm/npm/pull/3180#issuecomment-16336516
+[{*.yml,*.yaml,package.json}]
+indent_style = space
+indent_size = 2
diff --git a/.eslintignore b/.eslintignore
index e9a81977..5f7deaa8 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,3 +1,4 @@
node_modules/
compiled/
dist/
+extra_scripts/
\ No newline at end of file
diff --git a/.eslintrc b/.eslintrc
index a1646659..1709c9d8 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -3,7 +3,9 @@
"plugins": ["react"],
"rules": {
"no-useless-escape": 0,
- "prefer-const": "warn",
+ "prefer-const": ["warn", {
+ "destructuring": "all"
+ }],
"no-unused-vars": "warn",
"no-undef": "warn",
"no-lone-blocks": "warn",
@@ -17,5 +19,8 @@
"FileReader": true,
"localStorage": true,
"fetch": true
+ },
+ "env": {
+ "jest": true
}
}
diff --git a/.travis.yml b/.travis.yml
index 013169e8..d9267f77 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,9 +1,9 @@
language: node_js
node_js:
- - stable
- - lts/*
+ - 7
script:
- npm run lint && npm run test
+ - yarn jest
- 'if [[ ${TRAVIS_PULL_REQUEST_BRANCH:-$TRAVIS_BRANCH} = "master" ]]; then npm install -g grunt npm@5.2 && grunt pre-build; fi'
after_success:
- openssl aes-256-cbc -K $encrypted_440d7f9a3c38_key -iv $encrypted_440d7f9a3c38_iv
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
index be4b7e4f..1f5ada57 100644
--- a/ISSUE_TEMPLATE.md
+++ b/ISSUE_TEMPLATE.md
@@ -1,10 +1,25 @@
+# Current behavior
+
+# Expected behavior
+
+# Steps to reproduce
+
+1.
+2.
+3.
+
+# Environment
+
+- Version :
+- OS Version and name :
+
diff --git a/LICENSE b/LICENSE
index 0b41fd66..7472c9eb 100644
--- a/LICENSE
+++ b/LICENSE
@@ -2,7 +2,7 @@ GPL-3.0
Boostnote - an open source note-taking app made for programmers just like you.
-Copyright (C) 2017 Maisin&Co., Inc.
+Copyright (C) 2017 - 2018 BoostIO
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
diff --git a/__mocks__/electron.js b/__mocks__/electron.js
new file mode 100644
index 00000000..2176fbac
--- /dev/null
+++ b/__mocks__/electron.js
@@ -0,0 +1,7 @@
+module.exports = {
+ require: jest.genMockFunction(),
+ match: jest.genMockFunction(),
+ app: jest.genMockFunction(),
+ remote: jest.genMockFunction(),
+ dialog: jest.genMockFunction()
+}
diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js
index 5d799935..d81ce39d 100644
--- a/browser/components/CodeEditor.js
+++ b/browser/components/CodeEditor.js
@@ -3,36 +3,37 @@ import React from 'react'
import _ from 'lodash'
import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir'
-import path from 'path'
-import copyImage from 'browser/main/lib/dataApi/copyImage'
-import { findStorage } from 'browser/lib/findStorage'
+import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
+import convertModeName from 'browser/lib/convertModeName'
+import { options, TableEditor } from '@susisu/mte-kernel'
+import TextEditorInterface from 'browser/lib/TextEditorInterface'
+import eventEmitter from 'browser/main/lib/eventEmitter'
+import iconv from 'iconv-lite'
+import crypto from 'crypto'
+import consts from 'browser/lib/consts'
import fs from 'fs'
+const { ipcRenderer } = require('electron')
+import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
-const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
-
-function pass (name) {
- switch (name) {
- case 'ejs':
- return 'Embedded Javascript'
- case 'html_ruby':
- return 'Embedded Ruby'
- case 'objectivec':
- return 'Objective C'
- case 'text':
- return 'Plain Text'
- default:
- return name
- }
-}
+const buildCMRulers = (rulers, enableRulers) =>
+ (enableRulers ? rulers.map(ruler => ({ column: ruler })) : [])
export default class CodeEditor extends React.Component {
constructor (props) {
super(props)
- this.changeHandler = (e) => this.handleChange(e)
+ this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
+ leading: false,
+ trailing: true
+ })
+ this.changeHandler = e => this.handleChange(e)
+ this.focusHandler = () => {
+ ipcRenderer.send('editor:focused', true)
+ }
this.blurHandler = (editor, e) => {
+ ipcRenderer.send('editor:focused', false)
if (e == null) return null
let el = e.relatedTarget
while (el != null) {
@@ -42,18 +43,87 @@ export default class CodeEditor extends React.Component {
el = el.parentNode
}
this.props.onBlur != null && this.props.onBlur(e)
+
+ const { storageKey, noteKey } = this.props
+ attachmentManagement.deleteAttachmentsNotPresentInNote(
+ this.editor.getValue(),
+ storageKey,
+ noteKey
+ )
}
this.pasteHandler = (editor, e) => this.handlePaste(editor, e)
- this.loadStyleHandler = (e) => {
+ this.loadStyleHandler = e => {
this.editor.refresh()
}
+ this.searchHandler = (e, msg) => this.handleSearch(msg)
+ this.searchState = null
+
+ this.formatTable = () => this.handleFormatTable()
+ }
+
+ handleSearch (msg) {
+ const cm = this.editor
+ const component = this
+
+ if (component.searchState) cm.removeOverlay(component.searchState)
+ if (msg.length < 3) return
+
+ cm.operation(function () {
+ component.searchState = makeOverlay(msg, 'searching')
+ cm.addOverlay(component.searchState)
+
+ function makeOverlay (query, style) {
+ query = new RegExp(
+ query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&'),
+ 'gi'
+ )
+ return {
+ token: function (stream) {
+ query.lastIndex = stream.pos
+ var match = query.exec(stream.string)
+ if (match && match.index === stream.pos) {
+ stream.pos += match[0].length || 1
+ return style
+ } else if (match) {
+ stream.pos = match.index
+ } else {
+ stream.skipToEnd()
+ }
+ }
+ }
+ }
+ })
+ }
+
+ handleFormatTable () {
+ this.tableEditor.formatAll(options({textWidthOptions: {}}))
}
componentDidMount () {
+ const { rulers, enableRulers } = this.props
+ const expandSnippet = this.expandSnippet.bind(this)
+
+ const defaultSnippet = [
+ {
+ id: crypto.randomBytes(16).toString('hex'),
+ name: 'Dummy text',
+ prefix: ['lorem', 'ipsum'],
+ content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
+ }
+ ]
+ if (!fs.existsSync(consts.SNIPPET_FILE)) {
+ fs.writeFileSync(
+ consts.SNIPPET_FILE,
+ JSON.stringify(defaultSnippet, null, 4),
+ 'utf8'
+ )
+ }
+
this.value = this.props.value
this.editor = CodeMirror(this.refs.root, {
+ rulers: buildCMRulers(rulers, enableRulers),
value: this.props.value,
- lineNumbers: true,
+ lineNumbers: this.props.displayLineNumbers,
lineWrapping: true,
theme: this.props.theme,
indentUnit: this.props.indentSize,
@@ -63,15 +133,24 @@ export default class CodeEditor extends React.Component {
scrollPastEnd: this.props.scrollPastEnd,
inputStyle: 'textarea',
dragDrop: false,
- autoCloseBrackets: true,
+ foldGutter: true,
+ gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
+ autoCloseBrackets: {
+ pairs: '()[]{}\'\'""$$**``',
+ triples: '```"""\'\'\'',
+ explode: '[]{}``$$',
+ override: true
+ },
extraKeys: {
Tab: function (cm) {
const cursor = cm.getCursor()
const line = cm.getLine(cursor.line)
+ const cursorPosition = cursor.ch
+ const charBeforeCursor = line.substr(cursorPosition - 1, 1)
if (cm.somethingSelected()) cm.indentSelection('add')
else {
const tabs = cm.getOption('indentWithTabs')
- if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)\] )?$/)) {
+ if (line.trimLeft().match(/^(-|\*|\+) (\[( |x)] )?$/)) {
cm.execCommand('goLineStart')
if (tabs) {
cm.execCommand('insertTab')
@@ -79,6 +158,21 @@ export default class CodeEditor extends React.Component {
cm.execCommand('insertSoftTab')
}
cm.execCommand('goLineEnd')
+ } else if (
+ !charBeforeCursor.match(/\t|\s|\r|\n/) &&
+ cursor.ch > 1
+ ) {
+ // text expansion on tab key if the char before is alphabet
+ const snippets = JSON.parse(
+ fs.readFileSync(consts.SNIPPET_FILE, 'utf8')
+ )
+ if (expandSnippet(line, cursor, cm, snippets) === false) {
+ if (tabs) {
+ cm.execCommand('insertTab')
+ } else {
+ cm.execCommand('insertSoftTab')
+ }
+ }
} else {
if (tabs) {
cm.execCommand('insertTab')
@@ -91,8 +185,8 @@ export default class CodeEditor extends React.Component {
'Cmd-T': function (cm) {
// Do nothing
},
- Enter: 'newlineAndIndentContinueMarkdownList',
- 'Ctrl-C': (cm) => {
+ Enter: 'boostNewLineAndIndentContinueMarkdownList',
+ 'Ctrl-C': cm => {
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
document.execCommand('copy')
}
@@ -103,9 +197,14 @@ export default class CodeEditor extends React.Component {
this.setMode(this.props.mode)
+ this.editor.on('focus', this.focusHandler)
this.editor.on('blur', this.blurHandler)
this.editor.on('change', this.changeHandler)
this.editor.on('paste', this.pasteHandler)
+ eventEmitter.on('top:search', this.searchHandler)
+
+ eventEmitter.emit('code:init')
+ this.editor.on('scroll', this.scrollHandler)
const editorTheme = document.getElementById('editorTheme')
editorTheme.addEventListener('load', this.loadStyleHandler)
@@ -115,6 +214,83 @@ export default class CodeEditor extends React.Component {
CodeMirror.Vim.defineEx('wq', 'wq', this.quitEditor)
CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor)
CodeMirror.Vim.map('ZZ', ':q', 'normal')
+
+ this.tableEditor = new TableEditor(new TextEditorInterface(this.editor))
+ eventEmitter.on('code:format-table', this.formatTable)
+ }
+
+ expandSnippet (line, cursor, cm, snippets) {
+ const wordBeforeCursor = this.getWordBeforeCursor(
+ line,
+ cursor.line,
+ cursor.ch
+ )
+ const templateCursorString = ':{}'
+ for (let i = 0; i < snippets.length; i++) {
+ if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) {
+ if (snippets[i].content.indexOf(templateCursorString) !== -1) {
+ const snippetLines = snippets[i].content.split('\n')
+ let cursorLineNumber = 0
+ let cursorLinePosition = 0
+ for (let j = 0; j < snippetLines.length; j++) {
+ const cursorIndex = snippetLines[j].indexOf(templateCursorString)
+ if (cursorIndex !== -1) {
+ cursorLineNumber = j
+ cursorLinePosition = cursorIndex
+ cm.replaceRange(
+ snippets[i].content.replace(templateCursorString, ''),
+ wordBeforeCursor.range.from,
+ wordBeforeCursor.range.to
+ )
+ cm.setCursor({
+ line: cursor.line + cursorLineNumber,
+ ch: cursorLinePosition
+ })
+ }
+ }
+ } else {
+ cm.replaceRange(
+ snippets[i].content,
+ wordBeforeCursor.range.from,
+ wordBeforeCursor.range.to
+ )
+ }
+ return true
+ }
+ }
+
+ return false
+ }
+
+ getWordBeforeCursor (line, lineNumber, cursorPosition) {
+ let wordBeforeCursor = ''
+ const originCursorPosition = cursorPosition
+ const emptyChars = /\t|\s|\r|\n/
+
+ // to prevent the word to expand is long that will crash the whole app
+ // the safeStop is there to stop user to expand words that longer than 20 chars
+ const safeStop = 20
+
+ while (cursorPosition > 0) {
+ const currentChar = line.substr(cursorPosition - 1, 1)
+ // if char is not an empty char
+ if (!emptyChars.test(currentChar)) {
+ wordBeforeCursor = currentChar + wordBeforeCursor
+ } else if (wordBeforeCursor.length >= safeStop) {
+ throw new Error('Your snippet trigger is too long !')
+ } else {
+ break
+ }
+ cursorPosition--
+ }
+
+ return {
+ text: wordBeforeCursor,
+ range: {
+ from: { line: lineNumber, ch: originCursorPosition },
+ to: { line: lineNumber, ch: cursorPosition }
+ }
+ }
}
quitEditor () {
@@ -122,15 +298,21 @@ export default class CodeEditor extends React.Component {
}
componentWillUnmount () {
+ this.editor.off('focus', this.focusHandler)
this.editor.off('blur', this.blurHandler)
this.editor.off('change', this.changeHandler)
this.editor.off('paste', this.pasteHandler)
+ eventEmitter.off('top:search', this.searchHandler)
+ this.editor.off('scroll', this.scrollHandler)
const editorTheme = document.getElementById('editorTheme')
editorTheme.removeEventListener('load', this.loadStyleHandler)
+
+ eventEmitter.off('code:format-table', this.formatTable)
}
componentDidUpdate (prevProps, prevState) {
let needRefresh = false
+ const { rulers, enableRulers } = this.props
if (prevProps.mode !== this.props.mode) {
this.setMode(this.props.mode)
}
@@ -148,6 +330,13 @@ export default class CodeEditor extends React.Component {
needRefresh = true
}
+ if (
+ prevProps.enableRulers !== enableRulers ||
+ prevProps.rulers !== rulers
+ ) {
+ this.editor.setOption('rulers', buildCMRulers(rulers, enableRulers))
+ }
+
if (prevProps.indentSize !== this.props.indentSize) {
this.editor.setOption('indentUnit', this.props.indentSize)
this.editor.setOption('tabSize', this.props.indentSize)
@@ -156,6 +345,10 @@ export default class CodeEditor extends React.Component {
this.editor.setOption('indentWithTabs', this.props.indentType !== 'space')
}
+ if (prevProps.displayLineNumbers !== this.props.displayLineNumbers) {
+ this.editor.setOption('lineNumbers', this.props.displayLineNumbers)
+ }
+
if (prevProps.scrollPastEnd !== this.props.scrollPastEnd) {
this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd)
}
@@ -166,7 +359,7 @@ export default class CodeEditor extends React.Component {
}
setMode (mode) {
- let syntax = CodeMirror.findModeByName(pass(mode))
+ let syntax = CodeMirror.findModeByName(convertModeName(mode))
if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
this.editor.setOption('mode', syntax.mime)
@@ -180,11 +373,9 @@ export default class CodeEditor extends React.Component {
}
}
- moveCursorTo (row, col) {
- }
+ moveCursorTo (row, col) {}
- scrollToLine (num) {
- }
+ scrollToLine (num) {}
focus () {
this.editor.focus()
@@ -210,64 +401,189 @@ export default class CodeEditor extends React.Component {
this.editor.setCursor(cursor)
}
- handleDropImage (e) {
- e.preventDefault()
- const imagePath = e.dataTransfer.files[0].path
- const filename = path.basename(imagePath)
-
- copyImage(imagePath, this.props.storageKey).then((imagePath) => {
- const imageMd = `})`
- this.insertImageMd(imageMd)
- })
+ handleDropImage (dropEvent) {
+ dropEvent.preventDefault()
+ const { storageKey, noteKey } = this.props
+ attachmentManagement.handleAttachmentDrop(
+ this,
+ storageKey,
+ noteKey,
+ dropEvent
+ )
}
- insertImageMd (imageMd) {
+ insertAttachmentMd (imageMd) {
this.editor.replaceSelection(imageMd)
}
handlePaste (editor, e) {
- const dataTransferItem = e.clipboardData.items[0]
- if (!dataTransferItem.type.match('image')) return
-
- const blob = dataTransferItem.getAsFile()
- const reader = new FileReader()
- let base64data
-
- reader.readAsDataURL(blob)
- reader.onloadend = () => {
- base64data = reader.result.replace(/^data:image\/png;base64,/, '')
- base64data += base64data.replace('+', ' ')
- const binaryData = new Buffer(base64data, 'base64').toString('binary')
- const imageName = Math.random().toString(36).slice(-16)
- const storagePath = findStorage(this.props.storageKey).path
- const imageDir = path.join(storagePath, 'images')
- if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
- const imagePath = path.join(imageDir, `${imageName}.png`)
- fs.writeFile(imagePath, binaryData, 'binary')
- const imageMd = `})`
- this.insertImageMd(imageMd)
+ const clipboardData = e.clipboardData
+ const { storageKey, noteKey } = this.props
+ const dataTransferItem = clipboardData.items[0]
+ const pastedTxt = clipboardData.getData('text')
+ const isURL = str => {
+ const matcher = /^(?:\w+:)?\/\/([^\s\.]+\.\S{2}|localhost[\:?\d]*)\S*$/
+ return matcher.test(str)
+ }
+ const isInLinkTag = editor => {
+ const startCursor = editor.getCursor('start')
+ const prevChar = editor.getRange(
+ { line: startCursor.line, ch: startCursor.ch - 2 },
+ { line: startCursor.line, ch: startCursor.ch }
+ )
+ const endCursor = editor.getCursor('end')
+ const nextChar = editor.getRange(
+ { line: endCursor.line, ch: endCursor.ch },
+ { line: endCursor.line, ch: endCursor.ch + 1 }
+ )
+ return prevChar === '](' && nextChar === ')'
+ }
+ if (dataTransferItem.type.match('image')) {
+ attachmentManagement.handlePastImageEvent(
+ this,
+ storageKey,
+ noteKey,
+ dataTransferItem
+ )
+ } else if (
+ this.props.fetchUrlTitle &&
+ isURL(pastedTxt) &&
+ !isInLinkTag(editor)
+ ) {
+ this.handlePasteUrl(e, editor, pastedTxt)
+ }
+ if (attachmentManagement.isAttachmentLink(pastedTxt)) {
+ attachmentManagement
+ .handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
+ .then(modifiedText => {
+ this.editor.replaceSelection(modifiedText)
+ })
+ e.preventDefault()
}
}
+ handleScroll (e) {
+ if (this.props.onScroll) {
+ this.props.onScroll(e)
+ }
+ }
+
+ handlePasteUrl (e, editor, pastedTxt) {
+ e.preventDefault()
+ const taggedUrl = `<${pastedTxt}>`
+ editor.replaceSelection(taggedUrl)
+
+ const isImageReponse = response => {
+ return (
+ response.headers.has('content-type') &&
+ response.headers.get('content-type').match(/^image\/.+$/)
+ )
+ }
+ const replaceTaggedUrl = replacement => {
+ const value = editor.getValue()
+ const cursor = editor.getCursor()
+ const newValue = value.replace(taggedUrl, replacement)
+ const newCursor = Object.assign({}, cursor, {
+ ch: cursor.ch + newValue.length - value.length
+ })
+ editor.setValue(newValue)
+ editor.setCursor(newCursor)
+ }
+
+ fetch(pastedTxt, {
+ method: 'get'
+ })
+ .then(response => {
+ if (isImageReponse(response)) {
+ return this.mapImageResponse(response, pastedTxt)
+ } else {
+ return this.mapNormalResponse(response, pastedTxt)
+ }
+ })
+ .then(replacement => {
+ replaceTaggedUrl(replacement)
+ })
+ .catch(e => {
+ replaceTaggedUrl(pastedTxt)
+ })
+ }
+
+ mapNormalResponse (response, pastedTxt) {
+ return this.decodeResponse(response).then(body => {
+ return new Promise((resolve, reject) => {
+ try {
+ const parsedBody = new window.DOMParser().parseFromString(
+ body,
+ 'text/html'
+ )
+ const linkWithTitle = `[${parsedBody.title}](${pastedTxt})`
+ resolve(linkWithTitle)
+ } catch (e) {
+ reject(e)
+ }
+ })
+ })
+ }
+
+ mapImageResponse (response, pastedTxt) {
+ return new Promise((resolve, reject) => {
+ try {
+ const url = response.url
+ const name = url.substring(url.lastIndexOf('/') + 1)
+ const imageLinkWithName = ``
+ resolve(imageLinkWithName)
+ } catch (e) {
+ reject(e)
+ }
+ })
+ }
+
+ decodeResponse (response) {
+ const headers = response.headers
+ const _charset = headers.has('content-type')
+ ? this.extractContentTypeCharset(headers.get('content-type'))
+ : undefined
+ return response.arrayBuffer().then(buff => {
+ return new Promise((resolve, reject) => {
+ try {
+ const charset = _charset !== undefined &&
+ iconv.encodingExists(_charset)
+ ? _charset
+ : 'utf-8'
+ resolve(iconv.decode(new Buffer(buff), charset).toString())
+ } catch (e) {
+ reject(e)
+ }
+ })
+ })
+ }
+
+ extractContentTypeCharset (contentType) {
+ return contentType
+ .split(';')
+ .filter(str => {
+ return str.trim().toLowerCase().startsWith('charset')
+ })
+ .map(str => {
+ return str.replace(/['"]/g, '').split('=')[1]
+ })[0]
+ }
+
render () {
- const { className, fontSize } = this.props
- let fontFamily = this.props.fontFamily
- fontFamily = _.isString(fontFamily) && fontFamily.length > 0
- ? [fontFamily].concat(defaultEditorFontFamily)
- : defaultEditorFontFamily
+ const {className, fontSize} = this.props
+ const fontFamily = normalizeEditorFontFamily(this.props.fontFamily)
+ const width = this.props.width
return (
this.handleDropImage(e)}
+ onDrop={e => this.handleDropImage(e)}
/>
)
}
@@ -275,6 +591,8 @@ export default class CodeEditor extends React.Component {
CodeEditor.propTypes = {
value: PropTypes.string,
+ enableRulers: PropTypes.bool,
+ rulers: PropTypes.arrayOf(Number),
mode: PropTypes.string,
className: PropTypes.string,
onBlur: PropTypes.func,
diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js
index e329d281..ee80c887 100644
--- a/browser/components/MarkdownEditor.js
+++ b/browser/components/MarkdownEditor.js
@@ -92,7 +92,9 @@ class MarkdownEditor extends React.Component {
if (this.state.isLocked) return
this.setState({ keyPressed: new Set() })
const { config } = this.props
- if (config.editor.switchPreview === 'BLUR') {
+ if (config.editor.switchPreview === 'BLUR' ||
+ (config.editor.switchPreview === 'DBL_CLICK' && this.state.status === 'CODE')
+ ) {
const cursorPosition = this.refs.code.editor.getCursor()
this.setState({
status: 'PREVIEW'
@@ -104,6 +106,20 @@ class MarkdownEditor extends React.Component {
}
}
+ handleDoubleClick (e) {
+ if (this.state.isLocked) return
+ this.setState({keyPressed: new Set()})
+ const { config } = this.props
+ if (config.editor.switchPreview === 'DBL_CLICK') {
+ this.setState({
+ status: 'CODE'
+ }, () => {
+ this.refs.code.focus()
+ eventEmitter.emit('topbar:togglelockbutton', this.state.status)
+ })
+ }
+ }
+
handlePreviewMouseDown (e) {
this.previewMouseDownedAt = new Date()
}
@@ -207,7 +223,7 @@ class MarkdownEditor extends React.Component {
}
render () {
- const { className, value, config, storageKey } = this.props
+ const {className, value, config, storageKey, noteKey} = this.props
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
@@ -242,8 +258,13 @@ class MarkdownEditor extends React.Component {
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
+ enableRulers={config.editor.enableRulers}
+ rulers={config.editor.rulers}
+ displayLineNumbers={config.editor.displayLineNumbers}
scrollPastEnd={config.editor.scrollPastEnd}
storageKey={storageKey}
+ noteKey={noteKey}
+ fetchUrlTitle={config.editor.fetchUrlTitle}
onChange={(e) => this.handleChange(e)}
onBlur={(e) => this.handleBlur(e)}
/>
@@ -260,9 +281,14 @@ class MarkdownEditor extends React.Component {
codeBlockFontFamily={config.editor.fontFamily}
lineNumber={config.preview.lineNumber}
indentSize={editorIndentSize}
- scrollPastEnd={config.editor.scrollPastEnd}
+ scrollPastEnd={config.preview.scrollPastEnd}
+ smartQuotes={config.preview.smartQuotes}
+ smartArrows={config.preview.smartArrows}
+ breaks={config.preview.breaks}
+ sanitize={config.preview.sanitize}
ref='preview'
onContextMenu={(e) => this.handleContextMenu(e)}
+ onDoubleClick={(e) => this.handleDoubleClick(e)}
tabIndex='0'
value={this.state.renderValue}
onMouseUp={(e) => this.handlePreviewMouseUp(e)}
@@ -270,6 +296,9 @@ class MarkdownEditor extends React.Component {
onCheckboxClick={(e) => this.handleCheckboxClick(e)}
showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path}
+ noteKey={noteKey}
+ customCSS={config.preview.customCSS}
+ allowCustomCSS={config.preview.allowCustomCSS}
/>
)
diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js
old mode 100644
new mode 100755
index 711cabcd..5376a773
--- a/browser/components/MarkdownPreview.js
+++ b/browser/components/MarkdownPreview.js
@@ -1,30 +1,51 @@
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'
import consts from 'browser/lib/consts'
import Raphael from 'raphael'
import flowchart from 'flowchart'
+import mermaidRender from './render/MermaidRender'
import SequenceDiagram from 'js-sequence-diagrams'
+import Chart from 'chart.js'
import eventEmitter from 'browser/main/lib/eventEmitter'
-import fs from 'fs'
import htmlTextHelper from 'browser/lib/htmlTextHelper'
+import convertModeName from 'browser/lib/convertModeName'
import copy from 'copy-to-clipboard'
import mdurl from 'mdurl'
+import exportNote from 'browser/main/lib/dataApi/exportNote'
+import { escapeHtmlCharacters } from 'browser/lib/utils'
const { remote } = require('electron')
+const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
+
const { app } = remote
const path = require('path')
+const fileUrl = require('file-url')
+
const dialog = remote.dialog
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
-const appPath = 'file://' + (process.env.NODE_ENV === 'production'
- ? app.getAppPath()
- : path.resolve())
+const appPath = fileUrl(
+ process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
+)
+const CSS_FILES = [
+ `${appPath}/node_modules/katex/dist/katex.min.css`,
+ `${appPath}/node_modules/codemirror/lib/codemirror.css`
+]
-function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) {
+function buildStyle (
+ fontFamily,
+ fontSize,
+ codeBlockFontFamily,
+ lineNumber,
+ scrollPastEnd,
+ theme,
+ allowCustomCSS,
+ customCSS
+) {
return `
@font-face {
font-family: 'Lato';
@@ -44,10 +65,23 @@ function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber) {
font-weight: 700;
text-rendering: optimizeLegibility;
}
+@font-face {
+ font-family: 'Material Icons';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Material Icons'),
+ local('MaterialIcons-Regular'),
+ url('${appPath}/resources/fonts/MaterialIcons-Regular.woff2') format('woff2'),
+ url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
+ url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
+}
+${allowCustomCSS ? customCSS : ''}
${markdownStyle}
+
body {
font-family: '${fontFamily.join("','")}';
font-size: ${fontSize}px;
+ ${scrollPastEnd && 'padding-bottom: 90vh;'}
}
code {
font-family: '${codeBlockFontFamily.join("','")}';
@@ -95,9 +129,38 @@ h2 {
body p {
white-space: normal;
}
+
+@media print {
+ body[data-theme="${theme}"] {
+ color: #000;
+ background-color: #fff;
+ }
+ .clipboardButton {
+ display: none
+ }
+}
`
}
+const scrollBarStyle = `
+::-webkit-scrollbar {
+ width: 12px;
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: rgba(0, 0, 0, 0.15);
+}
+`
+const scrollBarDarkStyle = `
+::-webkit-scrollbar {
+ width: 12px;
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: rgba(0, 0, 0, 0.3);
+}
+`
+
const { shell } = require('electron')
const OSX = global.process.platform === 'darwin'
@@ -106,52 +169,65 @@ if (!OSX) {
defaultFontFamily.unshift('Microsoft YaHei')
defaultFontFamily.unshift('meiryo')
}
-const defaultCodeBlockFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
-
+const defaultCodeBlockFontFamily = [
+ 'Monaco',
+ 'Menlo',
+ 'Ubuntu Mono',
+ 'Consolas',
+ 'source-code-pro',
+ 'monospace'
+]
export default class MarkdownPreview extends React.Component {
constructor (props) {
super(props)
- 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)
+ this.contextMenuHandler = e => this.handleContextMenu(e)
+ this.mouseDownHandler = e => this.handleMouseDown(e)
+ this.mouseUpHandler = e => this.handleMouseUp(e)
+ this.DoubleClickHandler = e => this.handleDoubleClick(e)
+ this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {
+ leading: false,
+ trailing: true
+ })
+ this.checkboxClickHandler = e => this.handleCheckboxClick(e)
this.saveAsTextHandler = () => this.handleSaveAsText()
this.saveAsMdHandler = () => this.handleSaveAsMd()
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
this.printHandler = () => this.handlePrint()
this.linkClickHandler = this.handlelinkClick.bind(this)
+ this.initMarkdown = this.initMarkdown.bind(this)
+ this.initMarkdown()
}
- handlePreviewAnchorClick (e) {
- e.preventDefault()
- e.stopPropagation()
-
- const anchor = e.target.closest('a')
- const href = anchor.getAttribute('href')
- if (_.isString(href) && href.match(/^#/)) {
- const targetElement = this.refs.root.contentWindow.document.getElementById(href.substring(1, href.length))
- if (targetElement != null) {
- this.getWindow().scrollTo(0, targetElement.offsetTop)
- }
- } else {
- shell.openExternal(href)
- }
+ initMarkdown () {
+ const { smartQuotes, sanitize, breaks } = this.props
+ this.markdown = new Markdown({
+ typographer: smartQuotes,
+ sanitize,
+ breaks
+ })
}
handleCheckboxClick (e) {
this.props.onCheckboxClick(e)
}
+ handleScroll (e) {
+ if (this.props.onScroll) {
+ this.props.onScroll(e)
+ }
+ }
+
handleContextMenu (e) {
- if (!this.props.onContextMenu) return
this.props.onContextMenu(e)
}
+ handleDoubleClick (e) {
+ if (this.props.onDoubleClick != null) this.props.onDoubleClick(e)
+ }
+
handleMouseDown (e) {
- if (!this.props.onMouseDown) return
if (e.target != null) {
switch (e.target.tagName) {
case 'A':
@@ -175,12 +251,97 @@ export default class MarkdownPreview extends React.Component {
}
handleSaveAsMd () {
- this.exportAsDocument('md')
+ this.exportAsDocument('md', (noteContent, exportTasks) => {
+ let result = noteContent
+ if (this.props && this.props.storagePath && this.props.noteKey) {
+ const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
+ noteContent,
+ this.props.storagePath
+ )
+ attachmentsAbsolutePaths.forEach(attachment => {
+ exportTasks.push({
+ src: attachment,
+ dst: attachmentManagement.DESTINATION_FOLDER
+ })
+ })
+ result = attachmentManagement.removeStorageAndNoteReferences(
+ noteContent,
+ this.props.noteKey
+ )
+ }
+ return result
+ })
}
handleSaveAsHtml () {
- this.exportAsDocument('html', (value) => {
- return this.refs.root.contentWindow.document.documentElement.outerHTML
+ this.exportAsDocument('html', (noteContent, exportTasks) => {
+ const {
+ fontFamily,
+ fontSize,
+ codeBlockFontFamily,
+ lineNumber,
+ codeBlockTheme,
+ scrollPastEnd,
+ theme,
+ allowCustomCSS,
+ customCSS
+ } = this.getStyleParams()
+
+ const inlineStyles = buildStyle(
+ fontFamily,
+ fontSize,
+ codeBlockFontFamily,
+ lineNumber,
+ scrollPastEnd,
+ theme,
+ allowCustomCSS,
+ customCSS
+ )
+ let body = this.markdown.render(
+ escapeHtmlCharacters(noteContent, { detectCodeBlock: true })
+ )
+ const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
+ const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
+ noteContent,
+ this.props.storagePath
+ )
+
+ files.forEach(file => {
+ if (global.process.platform === 'win32') {
+ file = file.replace('file:///', '')
+ } else {
+ file = file.replace('file://', '')
+ }
+ exportTasks.push({
+ src: file,
+ dst: 'css'
+ })
+ })
+ attachmentsAbsolutePaths.forEach(attachment => {
+ exportTasks.push({
+ src: attachment,
+ dst: attachmentManagement.DESTINATION_FOLDER
+ })
+ })
+ body = attachmentManagement.removeStorageAndNoteReferences(
+ body,
+ this.props.noteKey
+ )
+
+ let styles = ''
+ files.forEach(file => {
+ styles += ``
+ })
+
+ return `
+
+
+
+
+ ${styles}
+
+ ${body}
+ `
})
}
@@ -188,53 +349,108 @@ export default class MarkdownPreview extends React.Component {
this.refs.root.contentWindow.print()
}
- exportAsDocument (fileType, formatter) {
+ exportAsDocument (fileType, contentFormatter) {
const options = {
- filters: [
- { name: 'Documents', extensions: [fileType] }
- ],
+ filters: [{ name: 'Documents', extensions: [fileType] }],
properties: ['openFile', 'createDirectory']
}
- const value = formatter ? formatter.call(this, this.props.value) : this.props.value
- dialog.showSaveDialog(remote.getCurrentWindow(), options,
- (filename) => {
+ dialog.showSaveDialog(remote.getCurrentWindow(), options, filename => {
if (filename) {
- fs.writeFile(filename, value, (err) => {
- if (err) throw err
- })
+ const content = this.props.value
+ const storage = this.props.storagePath
+
+ exportNote(storage, content, filename, contentFormatter)
+ .then(res => {
+ dialog.showMessageBox(remote.getCurrentWindow(), {
+ type: 'info',
+ message: `Exported to ${filename}`
+ })
+ })
+ .catch(err => {
+ dialog.showErrorBox(
+ 'Export error',
+ err ? err.message || err : 'Unexpected error during export'
+ )
+ throw err
+ })
}
})
}
fixDecodedURI (node) {
- if (node && node.children.length === 1 && typeof node.children[0] === 'string') {
+ if (
+ node &&
+ node.children.length === 1 &&
+ typeof node.children[0] === 'string'
+ ) {
const { innerText, href } = node
- node.innerText = mdurl.decode(href) === innerText
- ? href
- : innerText
+ node.innerText = mdurl.decode(href) === innerText ? href : innerText
+ }
+ }
+
+ getScrollBarStyle () {
+ const { theme } = this.props
+
+ switch (theme) {
+ case 'dark':
+ case 'solarized-dark':
+ case 'monokai':
+ return scrollBarDarkStyle
+ default:
+ return scrollBarStyle
}
}
componentDidMount () {
this.refs.root.setAttribute('sandbox', 'allow-scripts')
- this.refs.root.contentWindow.document.body.addEventListener('contextmenu', this.contextMenuHandler)
+ this.refs.root.contentWindow.document.body.addEventListener(
+ 'contextmenu',
+ this.contextMenuHandler
+ )
- this.refs.root.contentWindow.document.head.innerHTML = `
+ let styles = `
-
-
+
`
+
+ CSS_FILES.forEach(file => {
+ styles += ``
+ })
+
+ this.refs.root.contentWindow.document.head.innerHTML = styles
this.rewriteIframe()
this.applyStyle()
- this.refs.root.contentWindow.document.addEventListener('mousedown', this.mouseDownHandler)
- this.refs.root.contentWindow.document.addEventListener('mouseup', this.mouseUpHandler)
- this.refs.root.contentWindow.document.addEventListener('drop', this.preventImageDroppedHandler)
- this.refs.root.contentWindow.document.addEventListener('dragover', this.preventImageDroppedHandler)
+ this.refs.root.contentWindow.document.addEventListener(
+ 'mousedown',
+ this.mouseDownHandler
+ )
+ this.refs.root.contentWindow.document.addEventListener(
+ 'mouseup',
+ this.mouseUpHandler
+ )
+ this.refs.root.contentWindow.document.addEventListener(
+ 'dblclick',
+ this.DoubleClickHandler
+ )
+ this.refs.root.contentWindow.document.addEventListener(
+ 'drop',
+ this.preventImageDroppedHandler
+ )
+ this.refs.root.contentWindow.document.addEventListener(
+ 'dragover',
+ this.preventImageDroppedHandler
+ )
+ this.refs.root.contentWindow.document.addEventListener(
+ 'scroll',
+ this.scrollHandler
+ )
eventEmitter.on('export:save-text', this.saveAsTextHandler)
eventEmitter.on('export:save-md', this.saveAsMdHandler)
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
@@ -242,11 +458,34 @@ export default class MarkdownPreview extends React.Component {
}
componentWillUnmount () {
- this.refs.root.contentWindow.document.body.removeEventListener('contextmenu', this.contextMenuHandler)
- this.refs.root.contentWindow.document.removeEventListener('mousedown', this.mouseDownHandler)
- this.refs.root.contentWindow.document.removeEventListener('mouseup', this.mouseUpHandler)
- this.refs.root.contentWindow.document.removeEventListener('drop', this.preventImageDroppedHandler)
- this.refs.root.contentWindow.document.removeEventListener('dragover', this.preventImageDroppedHandler)
+ this.refs.root.contentWindow.document.body.removeEventListener(
+ 'contextmenu',
+ this.contextMenuHandler
+ )
+ this.refs.root.contentWindow.document.removeEventListener(
+ 'mousedown',
+ this.mouseDownHandler
+ )
+ this.refs.root.contentWindow.document.removeEventListener(
+ 'mouseup',
+ this.mouseUpHandler
+ )
+ this.refs.root.contentWindow.document.removeEventListener(
+ 'dblclick',
+ this.DoubleClickHandler
+ )
+ this.refs.root.contentWindow.document.removeEventListener(
+ 'drop',
+ this.preventImageDroppedHandler
+ )
+ this.refs.root.contentWindow.document.removeEventListener(
+ 'dragover',
+ this.preventImageDroppedHandler
+ )
+ this.refs.root.contentWindow.document.removeEventListener(
+ 'scroll',
+ this.scrollHandler
+ )
eventEmitter.off('export:save-text', this.saveAsTextHandler)
eventEmitter.off('export:save-md', this.saveAsMdHandler)
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
@@ -255,122 +494,195 @@ export default class MarkdownPreview extends React.Component {
componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value) this.rewriteIframe()
- if (prevProps.fontFamily !== this.props.fontFamily ||
+ if (
+ prevProps.smartQuotes !== this.props.smartQuotes ||
+ prevProps.sanitize !== this.props.sanitize ||
+ prevProps.smartArrows !== this.props.smartArrows ||
+ prevProps.breaks !== this.props.breaks
+ ) {
+ this.initMarkdown()
+ this.rewriteIframe()
+ }
+ if (
+ prevProps.fontFamily !== this.props.fontFamily ||
prevProps.fontSize !== this.props.fontSize ||
prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily ||
prevProps.codeBlockTheme !== this.props.codeBlockTheme ||
prevProps.lineNumber !== this.props.lineNumber ||
prevProps.showCopyNotification !== this.props.showCopyNotification ||
- prevProps.theme !== this.props.theme) {
+ prevProps.theme !== this.props.theme ||
+ prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
+ prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
+ prevProps.customCSS !== this.props.customCSS
+ ) {
this.applyStyle()
this.rewriteIframe()
}
}
- applyStyle () {
- const { fontSize, lineNumber, codeBlockTheme } = this.props
+ getStyleParams () {
+ const {
+ fontSize,
+ lineNumber,
+ codeBlockTheme,
+ scrollPastEnd,
+ theme,
+ allowCustomCSS,
+ customCSS
+ } = this.props
let { fontFamily, codeBlockFontFamily } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
- ? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
+ ? fontFamily
+ .split(',')
+ .map(fontName => fontName.trim())
+ .concat(defaultFontFamily)
: defaultFontFamily
- codeBlockFontFamily = _.isString(codeBlockFontFamily) && codeBlockFontFamily.trim().length > 0
- ? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
+ codeBlockFontFamily = _.isString(codeBlockFontFamily) &&
+ codeBlockFontFamily.trim().length > 0
+ ? codeBlockFontFamily
+ .split(',')
+ .map(fontName => fontName.trim())
+ .concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
- this.setCodeTheme(codeBlockTheme)
- this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber)
+ return {
+ fontFamily,
+ fontSize,
+ codeBlockFontFamily,
+ lineNumber,
+ codeBlockTheme,
+ scrollPastEnd,
+ theme,
+ allowCustomCSS,
+ customCSS
+ }
}
- setCodeTheme (theme) {
- theme = consts.THEMES.some((_theme) => _theme === theme) && theme !== 'default'
+ applyStyle () {
+ const {
+ fontFamily,
+ fontSize,
+ codeBlockFontFamily,
+ lineNumber,
+ codeBlockTheme,
+ scrollPastEnd,
+ theme,
+ allowCustomCSS,
+ customCSS
+ } = this.getStyleParams()
+
+ this.getWindow().document.getElementById(
+ 'codeTheme'
+ ).href = this.GetCodeThemeLink(codeBlockTheme)
+ this.getWindow().document.getElementById('style').innerHTML = buildStyle(
+ fontFamily,
+ fontSize,
+ codeBlockFontFamily,
+ lineNumber,
+ scrollPastEnd,
+ theme,
+ allowCustomCSS,
+ customCSS
+ )
+ }
+
+ GetCodeThemeLink (theme) {
+ theme = consts.THEMES.some(_theme => _theme === theme) &&
+ theme !== 'default'
? theme
: 'elegant'
- this.getWindow().document.getElementById('codeTheme').href = theme.startsWith('solarized')
+ return theme.startsWith('solarized')
? `${appPath}/node_modules/codemirror/theme/solarized.css`
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
}
rewriteIframe () {
- _.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
- el.removeEventListener('click', this.anchorClickHandler)
- })
- _.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
- el.removeEventListener('click', this.checkboxClickHandler)
- })
+ _.forEach(
+ this.refs.root.contentWindow.document.querySelectorAll(
+ 'input[type="checkbox"]'
+ ),
+ el => {
+ el.removeEventListener('click', this.checkboxClickHandler)
+ }
+ )
- _.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
- el.removeEventListener('click', this.linkClickHandler)
- })
+ _.forEach(
+ this.refs.root.contentWindow.document.querySelectorAll('a'),
+ el => {
+ el.removeEventListener('click', this.linkClickHandler)
+ }
+ )
- const { theme, indentSize, showCopyNotification, storagePath } = this.props
+ const {
+ theme,
+ indentSize,
+ showCopyNotification,
+ storagePath,
+ noteKey
+ } = this.props
let { value, codeBlockTheme } = this.props
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
+ const renderedHTML = this.markdown.render(value)
+ attachmentManagement.migrateAttachments(value, storagePath, noteKey)
+ this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(
+ renderedHTML,
+ storagePath
+ )
+ _.forEach(
+ this.refs.root.contentWindow.document.querySelectorAll(
+ 'input[type="checkbox"]'
+ ),
+ el => {
+ el.addEventListener('click', this.checkboxClickHandler)
+ }
+ )
- const codeBlocks = value.match(/(```)(.|[\n])*?(```)/g)
- if (codeBlocks !== null) {
- codeBlocks.forEach((codeBlock) => {
- value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock))
- })
- }
- this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value)
+ _.forEach(
+ this.refs.root.contentWindow.document.querySelectorAll('a'),
+ el => {
+ this.fixDecodedURI(el)
+ el.addEventListener('click', this.linkClickHandler)
+ }
+ )
- _.forEach(this.refs.root.contentWindow.document.querySelectorAll('.taskListItem'), (el) => {
- el.parentNode.parentNode.style.listStyleType = 'none'
- })
-
- _.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
- this.fixDecodedURI(el)
- el.addEventListener('click', this.anchorClickHandler)
- })
-
- _.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
- el.addEventListener('click', this.checkboxClickHandler)
- })
-
- _.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
- el.addEventListener('click', this.linkClickHandler)
- })
-
- _.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => {
- el.src = markdown.normalizeLinkText(el.src)
- if (!/\/:storage/.test(el.src)) return
- el.src = `file:///${markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}`
- })
-
- codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme)
+ codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme)
? codeBlockTheme
: 'default'
- _.forEach(this.refs.root.contentWindow.document.querySelectorAll('.code code'), (el) => {
- let syntax = CodeMirror.findModeByName(el.className)
- if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
- CodeMirror.requireMode(syntax.mode, () => {
- const content = htmlTextHelper.decodeEntities(el.innerHTML)
- const copyIcon = document.createElement('i')
- copyIcon.innerHTML = ''
- copyIcon.onclick = (e) => {
- copy(content)
- if (showCopyNotification) {
- this.notify('Saved to Clipboard!', {
- body: 'Paste it wherever you want!',
- silent: true
- })
+ _.forEach(
+ this.refs.root.contentWindow.document.querySelectorAll('.code code'),
+ el => {
+ let syntax = CodeMirror.findModeByName(convertModeName(el.className))
+ if (syntax == null) syntax = CodeMirror.findModeByName('Plain Text')
+ CodeMirror.requireMode(syntax.mode, () => {
+ const content = htmlTextHelper.decodeEntities(el.innerHTML)
+ const copyIcon = document.createElement('i')
+ copyIcon.innerHTML =
+ ''
+ copyIcon.onclick = e => {
+ copy(content)
+ if (showCopyNotification) {
+ this.notify('Saved to Clipboard!', {
+ body: 'Paste it wherever you want!',
+ silent: true
+ })
+ }
}
- }
- el.parentNode.appendChild(copyIcon)
- el.innerHTML = ''
- if (codeBlockTheme.indexOf('solarized') === 0) {
- const [refThema, color] = codeBlockTheme.split(' ')
- el.parentNode.className += ` cm-s-${refThema} cm-s-${color} CodeMirror`
- } else {
- el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror`
- }
- CodeMirror.runMode(content, syntax.mime, el, {
- tabSize: indentSize
+ el.parentNode.appendChild(copyIcon)
+ el.innerHTML = ''
+ if (codeBlockTheme.indexOf('solarized') === 0) {
+ const [refThema, color] = codeBlockTheme.split(' ')
+ el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
+ } else {
+ el.parentNode.className += ` cm-s-${codeBlockTheme}`
+ }
+ CodeMirror.runMode(content, syntax.mime, el, {
+ tabSize: indentSize
+ })
})
- })
- })
+ }
+ )
const opts = {}
// if (this.props.theme === 'dark') {
// opts['font-color'] = '#DDD'
@@ -378,37 +690,71 @@ export default class MarkdownPreview extends React.Component {
// opts['element-color'] = '#DDD'
// opts['fill'] = '#3A404C'
// }
- _.forEach(this.refs.root.contentWindow.document.querySelectorAll('.flowchart'), (el) => {
- Raphael.setWindow(this.getWindow())
- try {
- const diagram = flowchart.parse(htmlTextHelper.decodeEntities(el.innerHTML))
- el.innerHTML = ''
- diagram.drawSVG(el, opts)
- _.forEach(el.querySelectorAll('a'), (el) => {
- el.addEventListener('click', this.anchorClickHandler)
- })
- } catch (e) {
- console.error(e)
- el.className = 'flowchart-error'
- el.innerHTML = 'Flowchart parse error: ' + e.message
+ _.forEach(
+ this.refs.root.contentWindow.document.querySelectorAll('.flowchart'),
+ el => {
+ Raphael.setWindow(this.getWindow())
+ try {
+ const diagram = flowchart.parse(
+ htmlTextHelper.decodeEntities(el.innerHTML)
+ )
+ el.innerHTML = ''
+ diagram.drawSVG(el, opts)
+ _.forEach(el.querySelectorAll('a'), el => {
+ el.addEventListener('click', this.linkClickHandler)
+ })
+ } catch (e) {
+ console.error(e)
+ el.className = 'flowchart-error'
+ el.innerHTML = 'Flowchart parse error: ' + e.message
+ }
}
- })
+ )
- _.forEach(this.refs.root.contentWindow.document.querySelectorAll('.sequence'), (el) => {
- Raphael.setWindow(this.getWindow())
- try {
- const diagram = SequenceDiagram.parse(htmlTextHelper.decodeEntities(el.innerHTML))
- el.innerHTML = ''
- diagram.drawSVG(el, {theme: 'simple'})
- _.forEach(el.querySelectorAll('a'), (el) => {
- el.addEventListener('click', this.anchorClickHandler)
- })
- } catch (e) {
- console.error(e)
- el.className = 'sequence-error'
- el.innerHTML = 'Sequence diagram parse error: ' + e.message
+ _.forEach(
+ this.refs.root.contentWindow.document.querySelectorAll('.sequence'),
+ el => {
+ Raphael.setWindow(this.getWindow())
+ try {
+ const diagram = SequenceDiagram.parse(
+ htmlTextHelper.decodeEntities(el.innerHTML)
+ )
+ el.innerHTML = ''
+ diagram.drawSVG(el, { theme: 'simple' })
+ _.forEach(el.querySelectorAll('a'), el => {
+ el.addEventListener('click', this.linkClickHandler)
+ })
+ } catch (e) {
+ console.error(e)
+ el.className = 'sequence-error'
+ el.innerHTML = 'Sequence diagram parse error: ' + e.message
+ }
}
- })
+ )
+
+ _.forEach(
+ this.refs.root.contentWindow.document.querySelectorAll('.chart'),
+ el => {
+ try {
+ const chartConfig = JSON.parse(el.innerHTML)
+ el.innerHTML = ''
+ var canvas = document.createElement('canvas')
+ el.appendChild(canvas)
+ /* eslint-disable no-new */
+ new Chart(canvas, chartConfig)
+ } catch (e) {
+ console.error(e)
+ el.className = 'chart-error'
+ el.innerHTML = 'chartjs diagram parse error: ' + e.message
+ }
+ }
+ )
+ _.forEach(
+ this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
+ el => {
+ mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme)
+ }
+ )
}
focus () {
@@ -420,7 +766,9 @@ export default class MarkdownPreview extends React.Component {
}
scrollTo (targetRow) {
- const blocks = this.getWindow().document.querySelectorAll('body>[data-line]')
+ const blocks = this.getWindow().document.querySelectorAll(
+ 'body>[data-line]'
+ )
for (let index = 0; index < blocks.length; index++) {
let block = blocks[index]
@@ -440,25 +788,64 @@ export default class MarkdownPreview extends React.Component {
notify (title, options) {
if (global.process.platform === 'win32') {
- options.icon = path.join('file://', global.__dirname, '../../resources/app.png')
+ options.icon = path.join(
+ 'file://',
+ global.__dirname,
+ '../../resources/app.png'
+ )
}
return new window.Notification(title, options)
}
handlelinkClick (e) {
- const noteHash = e.target.href.split('/').pop()
- const regexIsNoteLink = /^(.{20})-(.{20})$/
- if (regexIsNoteLink.test(noteHash)) {
- eventEmitter.emit('list:jump', noteHash)
+ e.preventDefault()
+ e.stopPropagation()
+
+ const href = e.target.href
+ const linkHash = href.split('/').pop()
+
+ const regexNoteInternalLink = /main.html#(.+)/
+ if (regexNoteInternalLink.test(linkHash)) {
+ const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
+ const targetElement = this.refs.root.contentWindow.document.getElementById(
+ targetId
+ )
+
+ if (targetElement != null) {
+ this.getWindow().scrollTo(0, targetElement.offsetTop)
+ }
+ return
}
+
+ // this will match the new uuid v4 hash and the old hash
+ // e.g.
+ // :note:1c211eb7dcb463de6490 and
+ // :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c
+ const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/
+ if (regexIsNoteLink.test(linkHash)) {
+ eventEmitter.emit('list:jump', linkHash.replace(':note:', ''))
+ return
+ }
+
+ // this will match the old link format storage.key-note.key
+ // e.g.
+ // 877f99c3268608328037-1c211eb7dcb463de6490
+ const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/
+ if (regexIsLegacyNoteLink.test(linkHash)) {
+ eventEmitter.emit('list:jump', linkHash.split('-')[1])
+ return
+ }
+
+ // other case
+ shell.openExternal(href)
}
render () {
const { className, style, tabIndex } = this.props
return (
-