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

Merge branch 'master' into filter-tags-and-folders

This commit is contained in:
amedora
2019-05-09 09:23:17 +09:00
49 changed files with 546 additions and 290 deletions

View File

@@ -14,6 +14,8 @@ import {
import TextEditorInterface from 'browser/lib/TextEditorInterface'
import eventEmitter from 'browser/main/lib/eventEmitter'
import iconv from 'iconv-lite'
import { isMarkdownTitleURL } from 'browser/lib/utils'
import styles from '../components/CodeEditor.styl'
const { ipcRenderer, remote, clipboard } = require('electron')
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
@@ -22,6 +24,8 @@ const buildEditorContextMenu = require('browser/lib/contextMenuBuilder')
import TurndownService from 'turndown'
import {languageMaps} from '../lib/CMLanguageList'
import snippetManager from '../lib/SnippetManager'
import {generateInEditor, tocExistsInEditor} from 'browser/lib/markdown-toc-generator'
import markdownlint from 'markdownlint'
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
@@ -34,6 +38,38 @@ function translateHotkey (hotkey) {
return hotkey.replace(/\s*\+\s*/g, '-').replace(/Command/g, 'Cmd').replace(/Control/g, 'Ctrl')
}
const validatorOfMarkdown = (text, updateLinting) => {
const lintOptions = {
'strings': {
'content': text
}
}
return markdownlint(lintOptions, (err, result) => {
if (!err) {
const foundIssues = []
result.content.map(item => {
let ruleNames = ''
item.ruleNames.map((ruleName, index) => {
ruleNames += ruleName
if (index === item.ruleNames.length - 1) {
ruleNames += ': '
} else {
ruleNames += '/'
}
})
foundIssues.push({
from: CodeMirror.Pos(item.lineNumber, 0),
to: CodeMirror.Pos(item.lineNumber, 1),
message: ruleNames + item.ruleDescription,
severity: 'warning'
})
})
updateLinting(foundIssues)
}
})
}
export default class CodeEditor extends React.Component {
constructor (props) {
super(props)
@@ -197,6 +233,26 @@ export default class CodeEditor extends React.Component {
'Cmd-T': function (cm) {
// Do nothing
},
'Ctrl-/': function (cm) {
if (global.process.platform === 'darwin') { return }
const dateNow = new Date()
cm.replaceSelection(dateNow.toLocaleDateString())
},
'Cmd-/': function (cm) {
if (global.process.platform !== 'darwin') { return }
const dateNow = new Date()
cm.replaceSelection(dateNow.toLocaleDateString())
},
'Shift-Ctrl-/': function (cm) {
if (global.process.platform === 'darwin') { return }
const dateNow = new Date()
cm.replaceSelection(dateNow.toLocaleString())
},
'Shift-Cmd-/': function (cm) {
if (global.process.platform !== 'darwin') { return }
const dateNow = new Date()
cm.replaceSelection(dateNow.toLocaleString())
},
Enter: 'boostNewLineAndIndentContinueMarkdownList',
'Ctrl-C': cm => {
if (cm.getOption('keyMap').substr(0, 3) === 'vim') {
@@ -233,6 +289,7 @@ export default class CodeEditor extends React.Component {
snippetManager.init()
this.updateDefaultKeyMap()
const checkMarkdownNoteIsOpening = this.props.mode === 'Boost Flavored Markdown'
this.value = this.props.value
this.editor = CodeMirror(this.refs.root, {
rulers: buildCMRulers(rulers, enableRulers),
@@ -249,7 +306,11 @@ export default class CodeEditor extends React.Component {
inputStyle: 'textarea',
dragDrop: false,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
lint: checkMarkdownNoteIsOpening ? {
'getAnnotations': validatorOfMarkdown,
'async': true
} : false,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
autoCloseBrackets: {
pairs: this.props.matchingPairs,
triples: this.props.matchingTriples,
@@ -594,6 +655,34 @@ export default class CodeEditor extends React.Component {
handleChange (editor, changeObject) {
spellcheck.handleChange(editor, changeObject)
// The current note contains an toc. We'll check for changes on headlines.
// origin is undefined when markdownTocGenerator replace the old tod
if (tocExistsInEditor(editor) && changeObject.origin !== undefined) {
let requireTocUpdate
// Check if one of the changed lines contains a headline
for (let line = 0; line < changeObject.text.length; line++) {
if (this.linePossibleContainsHeadline(editor.getLine(changeObject.from.line + line))) {
requireTocUpdate = true
break
}
}
if (!requireTocUpdate) {
// Check if one of the removed lines contains a headline
for (let line = 0; line < changeObject.removed.length; line++) {
if (this.linePossibleContainsHeadline(changeObject.removed[line])) {
requireTocUpdate = true
break
}
}
}
if (requireTocUpdate) {
generateInEditor(editor)
}
}
this.updateHighlight(editor, changeObject)
this.value = editor.getValue()
@@ -602,6 +691,12 @@ export default class CodeEditor extends React.Component {
}
}
linePossibleContainsHeadline (currentLine) {
// We can't check if the line start with # because when some write text before
// the # we also need to update the toc
return currentLine.includes('# ')
}
incrementLines (start, linesAdded, linesRemoved, editor) {
const highlightedLines = editor.options.linesHighlighted
@@ -809,6 +904,8 @@ export default class CodeEditor extends React.Component {
if (isInFencedCodeBlock(editor)) {
this.handlePasteText(editor, pastedTxt)
} else if (fetchUrlTitle && isMarkdownTitleURL(pastedTxt) && !isInLinkTag(editor)) {
this.handlePasteUrl(editor, pastedTxt)
} else if (fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
this.handlePasteUrl(editor, pastedTxt)
} else if (attachmentManagement.isAttachmentLink(pastedTxt)) {
@@ -850,7 +947,17 @@ export default class CodeEditor extends React.Component {
}
handlePasteUrl (editor, pastedTxt) {
const taggedUrl = `<${pastedTxt}>`
let taggedUrl = `<${pastedTxt}>`
let urlToFetch = pastedTxt
let titleMark = ''
if (isMarkdownTitleURL(pastedTxt)) {
const pastedTxtSplitted = pastedTxt.split(' ')
titleMark = `${pastedTxtSplitted[0]} `
urlToFetch = pastedTxtSplitted[1]
taggedUrl = `<${urlToFetch}>`
}
editor.replaceSelection(taggedUrl)
const isImageReponse = response => {
@@ -862,22 +969,23 @@ export default class CodeEditor extends React.Component {
const replaceTaggedUrl = replacement => {
const value = editor.getValue()
const cursor = editor.getCursor()
const newValue = value.replace(taggedUrl, replacement)
const newValue = value.replace(taggedUrl, titleMark + replacement)
const newCursor = Object.assign({}, cursor, {
ch: cursor.ch + newValue.length - value.length
ch: cursor.ch + newValue.length - (value.length - titleMark.length)
})
editor.setValue(newValue)
editor.setCursor(newCursor)
}
fetch(pastedTxt, {
fetch(urlToFetch, {
method: 'get'
})
.then(response => {
if (isImageReponse(response)) {
return this.mapImageResponse(response, pastedTxt)
return this.mapImageResponse(response, urlToFetch)
} else {
return this.mapNormalResponse(response, pastedTxt)
return this.mapNormalResponse(response, urlToFetch)
}
})
.then(replacement => {

View File

@@ -3,4 +3,3 @@
.spellcheck-select
border: none
text-decoration underline wavy red

View File

@@ -8,7 +8,7 @@ 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 SequenceDiagram from '@rokt33r/js-sequence-diagrams'
import Chart from 'chart.js'
import eventEmitter from 'browser/main/lib/eventEmitter'
import htmlTextHelper from 'browser/lib/htmlTextHelper'
@@ -255,7 +255,7 @@ export default class MarkdownPreview extends React.Component {
return
}
// No contextMenu was passed to us -> execute our own link-opener
if (event.target.tagName.toLowerCase() === 'a') {
if (event.target.tagName.toLowerCase() === 'a' && event.target.getAttribute('href')) {
const href = event.target.href
const isLocalFile = href.startsWith('file:')
if (isLocalFile) {
@@ -282,33 +282,27 @@ export default class MarkdownPreview extends React.Component {
handleMouseDown (e) {
const config = ConfigManager.get()
const clickElement = e.target
const targetTag = clickElement.tagName // The direct parent HTML of where was clicked ie "BODY" or "DIV"
const lineNumber = getSourceLineNumberByElement(clickElement) // Line location of element clicked.
if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') {
eventEmitter.emit('topbar:togglemodebutton', 'CODE')
}
if (e.ctrlKey) {
if (config.editor.type === 'SPLIT') {
const clickElement = e.target
const lineNumber = getSourceLineNumberByElement(clickElement)
if (lineNumber !== -1) {
eventEmitter.emit('line:jump', lineNumber)
}
} else {
const clickElement = e.target
const lineNumber = getSourceLineNumberByElement(clickElement)
if (lineNumber !== -1) {
eventEmitter.emit('editor:focus')
eventEmitter.emit('line:jump', lineNumber)
}
}
}
if (e.target != null) {
switch (e.target.tagName) {
case 'A':
case 'INPUT':
return null
}
}
if (this.props.onMouseDown != null) this.props.onMouseDown(e)
if (this.props.onMouseDown != null && targetTag === 'BODY') this.props.onMouseDown(e)
}
handleMouseUp (e) {
@@ -676,14 +670,14 @@ export default class MarkdownPreview extends React.Component {
)
}
GetCodeThemeLink (theme) {
theme = consts.THEMES.some(_theme => _theme === theme) &&
theme !== 'default'
? theme
: 'elegant'
return theme.startsWith('solarized')
? `${appPath}/node_modules/codemirror/theme/solarized.css`
: `${appPath}/node_modules/codemirror/theme/${theme}.css`
GetCodeThemeLink (name) {
const theme = consts.THEMES.find(theme => theme.name === name)
if (theme) {
return `${appPath}/${theme.path}`
} else {
return `${appPath}/node_modules/codemirror/theme/elegant.css`
}
}
rewriteIframe () {
@@ -741,9 +735,9 @@ export default class MarkdownPreview extends React.Component {
}
)
codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme)
? codeBlockTheme
: 'default'
codeBlockTheme = consts.THEMES.find(theme => theme.name === codeBlockTheme)
const codeBlockThemeClassName = codeBlockTheme ? codeBlockTheme.className : 'cm-s-default'
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
@@ -766,14 +760,11 @@ export default class MarkdownPreview extends React.Component {
})
}
}
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}`
}
el.parentNode.className += ` ${codeBlockThemeClassName}`
CodeMirror.runMode(content, syntax.mime, el, {
tabSize: indentSize
})
@@ -888,78 +879,96 @@ export default class MarkdownPreview extends React.Component {
const markdownPreviewIframe = document.querySelector('.MarkdownPreview')
const rect = markdownPreviewIframe.getBoundingClientRect()
const config = { attributes: true, subtree: true }
const imgObserver = new MutationObserver((mutationList) => {
for (const mu of mutationList) {
if (mu.target.className === 'carouselContent-enter-done') {
this.setImgOnClickEventHelper(mu.target, rect)
break
}
}
})
const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('img')
for (const img of imgList) {
img.onclick = () => {
const widthMagnification = document.body.clientWidth / img.width
const heightMagnification = document.body.clientHeight / img.height
const baseOnWidth = widthMagnification < heightMagnification
const magnification = baseOnWidth ? widthMagnification : heightMagnification
const parentEl = img.parentElement
this.setImgOnClickEventHelper(img, rect)
imgObserver.observe(parentEl, config)
}
}
const zoomImgWidth = img.width * magnification
const zoomImgHeight = img.height * magnification
const zoomImgTop = (document.body.clientHeight - zoomImgHeight) / 2
const zoomImgLeft = (document.body.clientWidth - zoomImgWidth) / 2
const originalImgTop = img.y + rect.top
const originalImgLeft = img.x + rect.left
const originalImgRect = {
top: `${originalImgTop}px`,
left: `${originalImgLeft}px`,
width: `${img.width}px`,
height: `${img.height}px`
}
const zoomInImgRect = {
top: `${baseOnWidth ? zoomImgTop : 0}px`,
left: `${baseOnWidth ? 0 : zoomImgLeft}px`,
width: `${zoomImgWidth}px`,
height: `${zoomImgHeight}px`
}
const animationSpeed = 300
setImgOnClickEventHelper (img, rect) {
img.onclick = () => {
const widthMagnification = document.body.clientWidth / img.width
const heightMagnification = document.body.clientHeight / img.height
const baseOnWidth = widthMagnification < heightMagnification
const magnification = baseOnWidth ? widthMagnification : heightMagnification
const zoomImg = document.createElement('img')
zoomImg.src = img.src
const zoomImgWidth = img.width * magnification
const zoomImgHeight = img.height * magnification
const zoomImgTop = (document.body.clientHeight - zoomImgHeight) / 2
const zoomImgLeft = (document.body.clientWidth - zoomImgWidth) / 2
const originalImgTop = img.y + rect.top
const originalImgLeft = img.x + rect.left
const originalImgRect = {
top: `${originalImgTop}px`,
left: `${originalImgLeft}px`,
width: `${img.width}px`,
height: `${img.height}px`
}
const zoomInImgRect = {
top: `${baseOnWidth ? zoomImgTop : 0}px`,
left: `${baseOnWidth ? 0 : zoomImgLeft}px`,
width: `${zoomImgWidth}px`,
height: `${zoomImgHeight}px`
}
const animationSpeed = 300
const zoomImg = document.createElement('img')
zoomImg.src = img.src
zoomImg.style = `
position: absolute;
top: ${baseOnWidth ? zoomImgTop : 0}px;
left: ${baseOnWidth ? 0 : zoomImgLeft}px;
width: ${zoomImgWidth};
height: ${zoomImgHeight}px;
`
zoomImg.animate([
originalImgRect,
zoomInImgRect
], animationSpeed)
const overlay = document.createElement('div')
overlay.style = `
background-color: rgba(0,0,0,0.5);
cursor: zoom-out;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: ${document.body.clientHeight}px;
z-index: 100;
`
overlay.onclick = () => {
zoomImg.style = `
position: absolute;
top: ${baseOnWidth ? zoomImgTop : 0}px;
left: ${baseOnWidth ? 0 : zoomImgLeft}px;
width: ${zoomImgWidth};
height: ${zoomImgHeight}px;
top: ${originalImgTop}px;
left: ${originalImgLeft}px;
width: ${img.width}px;
height: ${img.height}px;
`
zoomImg.animate([
originalImgRect,
zoomInImgRect
const zoomOutImgAnimation = zoomImg.animate([
zoomInImgRect,
originalImgRect
], animationSpeed)
const overlay = document.createElement('div')
overlay.style = `
background-color: rgba(0,0,0,0.5);
cursor: zoom-out;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: ${document.body.clientHeight}px;
z-index: 100;
`
overlay.onclick = () => {
zoomImg.style = `
position: absolute;
top: ${originalImgTop}px;
left: ${originalImgLeft}px;
width: ${img.width}px;
height: ${img.height}px;
`
const zoomOutImgAnimation = zoomImg.animate([
zoomInImgRect,
originalImgRect
], animationSpeed)
zoomOutImgAnimation.onfinish = () => overlay.remove()
}
overlay.appendChild(zoomImg)
document.body.appendChild(overlay)
zoomOutImgAnimation.onfinish = () => overlay.remove()
}
overlay.appendChild(zoomImg)
document.body.appendChild(overlay)
}
this.getWindow().scrollTo(0, 0)
}
focus () {
@@ -1006,9 +1015,11 @@ export default class MarkdownPreview extends React.Component {
e.preventDefault()
e.stopPropagation()
const href = e.target.href
const href = e.target.getAttribute('href')
const linkHash = href.split('/').pop()
if (!href) return
const regexNoteInternalLink = /main.html#(.+)/
if (regexNoteInternalLink.test(linkHash)) {
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])

View File

@@ -16,8 +16,8 @@ const NavToggleButton = ({isFolded, handleToggleButtonClick}) => (
onClick={(e) => handleToggleButtonClick(e)}
>
{isFolded
? <i className='fa fa-angle-double-right' />
: <i className='fa fa-angle-double-left' />
? <i className='fa fa-angle-double-right fa-2x' />
: <i className='fa fa-angle-double-left fa-2x' />
}
</button>
)

View File

@@ -7,7 +7,7 @@
border-radius 16.5px
height 34px
width 34px
line-height 32px
line-height 100%
padding 0
&:hover
border: 1px solid #1EC38B;