1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 09:46:22 +00:00

merge from upstream/master

This commit is contained in:
Callum Booth
2019-10-08 13:21:56 +01:00
102 changed files with 5118 additions and 3783 deletions

View File

@@ -18,7 +18,9 @@
"globals": { "globals": {
"FileReader": true, "FileReader": true,
"localStorage": true, "localStorage": true,
"fetch": true "fetch": true,
"Image": true,
"MutationObserver": true
}, },
"env": { "env": {
"jest": true "jest": true

View File

@@ -20,14 +20,15 @@ import styles from '../components/CodeEditor.styl'
const { ipcRenderer, remote, clipboard } = require('electron') const { ipcRenderer, remote, clipboard } = require('electron')
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
const spellcheck = require('browser/lib/spellcheck') const spellcheck = require('browser/lib/spellcheck')
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder') const buildEditorContextMenu = require('browser/lib/contextMenuBuilder').buildEditorContextMenu
import TurndownService from 'turndown' import { createTurndownService } from '../lib/turndown'
import {languageMaps} from '../lib/CMLanguageList' import {languageMaps} from '../lib/CMLanguageList'
import snippetManager from '../lib/SnippetManager' import snippetManager from '../lib/SnippetManager'
import {generateInEditor, tocExistsInEditor} from 'browser/lib/markdown-toc-generator' import {generateInEditor, tocExistsInEditor} from 'browser/lib/markdown-toc-generator'
import markdownlint from 'markdownlint' import markdownlint from 'markdownlint'
import Jsonlint from 'jsonlint-mod' import Jsonlint from 'jsonlint-mod'
import { DEFAULT_CONFIG } from '../main/lib/ConfigManager' import { DEFAULT_CONFIG } from '../main/lib/ConfigManager'
import prettier from 'prettier'
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
@@ -53,6 +54,7 @@ export default class CodeEditor extends React.Component {
this.focusHandler = () => { this.focusHandler = () => {
ipcRenderer.send('editor:focused', true) ipcRenderer.send('editor:focused', true)
} }
const debouncedDeletionOfAttachments = _.debounce(attachmentManagement.deleteAttachmentsNotPresentInNote, 30000)
this.blurHandler = (editor, e) => { this.blurHandler = (editor, e) => {
ipcRenderer.send('editor:focused', false) ipcRenderer.send('editor:focused', false)
if (e == null) return null if (e == null) return null
@@ -64,16 +66,13 @@ export default class CodeEditor extends React.Component {
el = el.parentNode el = el.parentNode
} }
this.props.onBlur != null && this.props.onBlur(e) this.props.onBlur != null && this.props.onBlur(e)
const { const {
storageKey, storageKey,
noteKey noteKey
} = this.props } = this.props
attachmentManagement.deleteAttachmentsNotPresentInNote( if (this.props.deleteUnusedAttachments === true) {
this.editor.getValue(), debouncedDeletionOfAttachments(this.editor.getValue(), storageKey, noteKey)
storageKey, }
noteKey
)
} }
this.pasteHandler = (editor, e) => { this.pasteHandler = (editor, e) => {
e.preventDefault() e.preventDefault()
@@ -102,7 +101,7 @@ export default class CodeEditor extends React.Component {
this.editorActivityHandler = () => this.handleEditorActivity() this.editorActivityHandler = () => this.handleEditorActivity()
this.turndownService = new TurndownService() this.turndownService = createTurndownService()
} }
handleSearch (msg) { handleSearch (msg) {
@@ -110,7 +109,7 @@ export default class CodeEditor extends React.Component {
const component = this const component = this
if (component.searchState) cm.removeOverlay(component.searchState) if (component.searchState) cm.removeOverlay(component.searchState)
if (msg.length < 3) return if (msg.length < 1) return
cm.operation(function () { cm.operation(function () {
component.searchState = makeOverlay(msg, 'searching') component.searchState = makeOverlay(msg, 'searching')
@@ -205,23 +204,11 @@ export default class CodeEditor extends React.Component {
'Cmd-T': function (cm) { 'Cmd-T': function (cm) {
// Do nothing // Do nothing
}, },
'Ctrl-/': function (cm) { [translateHotkey(hotkey.insertDate)]: function (cm) {
if (global.process.platform === 'darwin') { return }
const dateNow = new Date() const dateNow = new Date()
cm.replaceSelection(dateNow.toLocaleDateString()) cm.replaceSelection(dateNow.toLocaleDateString())
}, },
'Cmd-/': function (cm) { [translateHotkey(hotkey.insertDateTime)]: 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() const dateNow = new Date()
cm.replaceSelection(dateNow.toLocaleString()) cm.replaceSelection(dateNow.toLocaleString())
}, },
@@ -232,6 +219,28 @@ export default class CodeEditor extends React.Component {
} }
return CodeMirror.Pass return CodeMirror.Pass
}, },
[translateHotkey(hotkey.prettifyMarkdown)]: cm => {
// Default / User configured prettier options
const currentConfig = JSON.parse(self.props.prettierConfig)
// Parser type will always need to be markdown so we override the option before use
currentConfig.parser = 'markdown'
// Get current cursor position
const cursorPos = cm.getCursor()
currentConfig.cursorOffset = cm.doc.indexFromPos(cursorPos)
// Prettify contents of editor
const formattedTextDetails = prettier.formatWithCursor(cm.doc.getValue(), currentConfig)
const formattedText = formattedTextDetails.formatted
const formattedCursorPos = formattedTextDetails.cursorOffset
cm.doc.setValue(formattedText)
// Reset Cursor position to be at the same markdown as was before prettifying
const newCursorPos = cm.doc.posFromIndex(formattedCursorPos)
cm.doc.setCursor(newCursorPos)
},
[translateHotkey(hotkey.pasteSmartly)]: cm => { [translateHotkey(hotkey.pasteSmartly)]: cm => {
this.handlePaste(cm, true) this.handlePaste(cm, true)
} }
@@ -267,7 +276,7 @@ export default class CodeEditor extends React.Component {
value: this.props.value, value: this.props.value,
linesHighlighted: this.props.linesHighlighted, linesHighlighted: this.props.linesHighlighted,
lineNumbers: this.props.displayLineNumbers, lineNumbers: this.props.displayLineNumbers,
lineWrapping: true, lineWrapping: this.props.lineWrapping,
theme: this.props.theme, theme: this.props.theme,
indentUnit: this.props.indentSize, indentUnit: this.props.indentSize,
tabSize: this.props.indentSize, tabSize: this.props.indentSize,
@@ -285,7 +294,8 @@ export default class CodeEditor extends React.Component {
explode: this.props.explodingPairs, explode: this.props.explodingPairs,
override: true override: true
}, },
extraKeys: this.defaultKeyMap extraKeys: this.defaultKeyMap,
prettierConfig: this.props.prettierConfig
}) })
document.querySelector('.CodeMirror-lint-markers').style.display = enableMarkdownLint ? 'inline-block' : 'none' document.querySelector('.CodeMirror-lint-markers').style.display = enableMarkdownLint ? 'inline-block' : 'none'
@@ -566,6 +576,10 @@ export default class CodeEditor extends React.Component {
this.editor.setOption('lineNumbers', this.props.displayLineNumbers) this.editor.setOption('lineNumbers', this.props.displayLineNumbers)
} }
if (prevProps.lineWrapping !== this.props.lineWrapping) {
this.editor.setOption('lineWrapping', this.props.lineWrapping)
}
if (prevProps.scrollPastEnd !== this.props.scrollPastEnd) { if (prevProps.scrollPastEnd !== this.props.scrollPastEnd) {
this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd) this.editor.setOption('scrollPastEnd', this.props.scrollPastEnd)
} }
@@ -620,6 +634,9 @@ export default class CodeEditor extends React.Component {
this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'}) this.editor.addPanel(this.createSpellCheckPanel(), {position: 'bottom'})
} }
} }
if (prevProps.deleteUnusedAttachments !== this.props.deleteUnusedAttachments) {
this.editor.setOption('deleteUnusedAttachments', this.props.deleteUnusedAttachments)
}
if (needRefresh) { if (needRefresh) {
this.editor.refresh() this.editor.refresh()
@@ -848,6 +865,17 @@ export default class CodeEditor extends React.Component {
this.editor.setCursor(cursor) this.editor.setCursor(cursor)
} }
/**
* Update content of one line
* @param {Number} lineNumber
* @param {String} content
*/
setLineContent (lineNumber, content) {
const prevContent = this.editor.getLine(lineNumber)
const prevContentLength = prevContent ? prevContent.length : 0
this.editor.replaceRange(content, { line: lineNumber, ch: 0 }, { line: lineNumber, ch: prevContentLength })
}
handleDropImage (dropEvent) { handleDropImage (dropEvent) {
dropEvent.preventDefault() dropEvent.preventDefault()
const { const {
@@ -1186,7 +1214,8 @@ CodeEditor.propTypes = {
autoDetect: PropTypes.bool, autoDetect: PropTypes.bool,
spellCheck: PropTypes.bool, spellCheck: PropTypes.bool,
enableMarkdownLint: PropTypes.bool, enableMarkdownLint: PropTypes.bool,
customMarkdownLintConfig: PropTypes.string customMarkdownLintConfig: PropTypes.string,
deleteUnusedAttachments: PropTypes.bool
} }
CodeEditor.defaultProps = { CodeEditor.defaultProps = {
@@ -1200,5 +1229,7 @@ CodeEditor.defaultProps = {
autoDetect: false, autoDetect: false,
spellCheck: false, spellCheck: false,
enableMarkdownLint: DEFAULT_CONFIG.editor.enableMarkdownLint, enableMarkdownLint: DEFAULT_CONFIG.editor.enableMarkdownLint,
customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig customMarkdownLintConfig: DEFAULT_CONFIG.editor.customMarkdownLintConfig,
prettierConfig: DEFAULT_CONFIG.editor.prettierConfig,
deleteUnusedAttachments: DEFAULT_CONFIG.editor.deleteUnusedAttachments
} }

View File

@@ -169,14 +169,15 @@ class MarkdownEditor extends React.Component {
.split('\n') .split('\n')
const targetLine = lines[lineIndex] const targetLine = lines[lineIndex]
let newLine = targetLine
if (targetLine.match(checkedMatch)) { if (targetLine.match(checkedMatch)) {
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]') newLine = targetLine.replace(checkReplace, '[ ]')
} }
if (targetLine.match(uncheckedMatch)) { if (targetLine.match(uncheckedMatch)) {
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]') newLine = targetLine.replace(uncheckReplace, '[x]')
} }
this.refs.code.setValue(lines.join('\n')) this.refs.code.setLineContent(lineIndex, newLine)
} }
} }
@@ -304,6 +305,7 @@ class MarkdownEditor extends React.Component {
enableRulers={config.editor.enableRulers} enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers} rulers={config.editor.rulers}
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
lineWrapping
matchingPairs={config.editor.matchingPairs} matchingPairs={config.editor.matchingPairs}
matchingTriples={config.editor.matchingTriples} matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs} explodingPairs={config.editor.explodingPairs}
@@ -321,6 +323,8 @@ class MarkdownEditor extends React.Component {
switchPreview={config.editor.switchPreview} switchPreview={config.editor.switchPreview}
enableMarkdownLint={config.editor.enableMarkdownLint} enableMarkdownLint={config.editor.enableMarkdownLint}
customMarkdownLintConfig={config.editor.customMarkdownLintConfig} customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
prettierConfig={config.editor.prettierConfig}
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
/> />
<MarkdownPreview styleName={this.state.status === 'PREVIEW' <MarkdownPreview styleName={this.state.status === 'PREVIEW'
? 'preview' ? 'preview'
@@ -340,6 +344,7 @@ class MarkdownEditor extends React.Component {
smartArrows={config.preview.smartArrows} smartArrows={config.preview.smartArrows}
breaks={config.preview.breaks} breaks={config.preview.breaks}
sanitize={config.preview.sanitize} sanitize={config.preview.sanitize}
mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
ref='preview' ref='preview'
onContextMenu={(e) => this.handleContextMenu(e)} onContextMenu={(e) => this.handleContextMenu(e)}
onDoubleClick={(e) => this.handleDoubleClick(e)} onDoubleClick={(e) => this.handleDoubleClick(e)}

View File

@@ -18,15 +18,13 @@ import mdurl from 'mdurl'
import exportNote from 'browser/main/lib/dataApi/exportNote' import exportNote from 'browser/main/lib/dataApi/exportNote'
import { escapeHtmlCharacters } from 'browser/lib/utils' import { escapeHtmlCharacters } from 'browser/lib/utils'
import yaml from 'js-yaml' import yaml from 'js-yaml'
import context from 'browser/lib/context'
import i18n from 'browser/lib/i18n'
import fs from 'fs'
import { render } from 'react-dom' import { render } from 'react-dom'
import Carousel from 'react-image-carousel' import Carousel from 'react-image-carousel'
import ConfigManager from '../main/lib/ConfigManager' import ConfigManager from '../main/lib/ConfigManager'
const { remote, shell } = require('electron') const { remote, shell } = require('electron')
const attachmentManagement = require('../main/lib/dataApi/attachmentManagement') const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder').buildMarkdownPreviewContextMenu
const { app } = remote const { app } = remote
const path = require('path') const path = require('path')
@@ -34,8 +32,6 @@ const fileUrl = require('file-url')
const dialog = remote.dialog const dialog = remote.dialog
const uri2path = require('file-uri-to-path')
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1] const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const appPath = fileUrl( const appPath = fileUrl(
process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve() process.env.NODE_ENV === 'production' ? app.getAppPath() : path.resolve()
@@ -46,16 +42,31 @@ const CSS_FILES = [
`${appPath}/node_modules/react-image-carousel/lib/css/main.min.css` `${appPath}/node_modules/react-image-carousel/lib/css/main.min.css`
] ]
function buildStyle ( /**
* @param {Object} opts
* @param {String} opts.fontFamily
* @param {Numberl} opts.fontSize
* @param {String} opts.codeBlockFontFamily
* @param {String} opts.theme
* @param {Boolean} [opts.lineNumber] Should show line number
* @param {Boolean} [opts.scrollPastEnd]
* @param {Boolean} [opts.optimizeOverflowScroll] Should tweak body style to optimize overflow scrollbar display
* @param {Boolean} [opts.allowCustomCSS] Should add custom css
* @param {String} [opts.customCSS] Will be added to bottom, only if `opts.allowCustomCSS` is truthy
* @returns {String}
*/
function buildStyle (opts) {
const {
fontFamily, fontFamily,
fontSize, fontSize,
codeBlockFontFamily, codeBlockFontFamily,
lineNumber, lineNumber,
scrollPastEnd, scrollPastEnd,
optimizeOverflowScroll,
theme, theme,
allowCustomCSS, allowCustomCSS,
customCSS customCSS
) { } = opts
return ` return `
@font-face { @font-face {
font-family: 'Lato'; font-family: 'Lato';
@@ -85,12 +96,14 @@ function buildStyle (
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'), url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype'); url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
} }
${markdownStyle} ${markdownStyle}
body { body {
font-family: '${fontFamily.join("','")}'; font-family: '${fontFamily.join("','")}';
font-size: ${fontSize}px; font-size: ${fontSize}px;
${scrollPastEnd && 'padding-bottom: 90vh;'} ${scrollPastEnd ? 'padding-bottom: 90vh;' : ''}
${optimizeOverflowScroll ? 'height: 100%;' : ''}
} }
@media print { @media print {
body { body {
@@ -249,30 +262,12 @@ export default class MarkdownPreview extends React.Component {
} }
handleContextMenu (event) { handleContextMenu (event) {
// If a contextMenu handler was passed to us, use it instead of the self-defined one -> return const menu = buildMarkdownPreviewContextMenu(this, event)
if (_.isFunction(this.props.onContextMenu)) { const switchPreview = ConfigManager.get().editor.switchPreview
if (menu != null && switchPreview !== 'RIGHTCLICK') {
menu.popup(remote.getCurrentWindow())
} else if (_.isFunction(this.props.onContextMenu)) {
this.props.onContextMenu(event) this.props.onContextMenu(event)
return
}
// No contextMenu was passed to us -> execute our own link-opener
if (event.target.tagName.toLowerCase() === 'a' && event.target.getAttribute('href')) {
const href = event.target.href
const isLocalFile = href.startsWith('file:')
if (isLocalFile) {
const absPath = uri2path(href)
try {
if (fs.lstatSync(absPath).isFile()) {
context.popup([
{
label: i18n.__('Show in explorer'),
click: (e) => shell.showItemInFolder(absPath)
}
])
}
} catch (e) {
console.log('Error while evaluating if the file is locally available', e)
}
}
} }
} }
@@ -334,7 +329,7 @@ export default class MarkdownPreview extends React.Component {
customCSS customCSS
} = this.getStyleParams() } = this.getStyleParams()
const inlineStyles = buildStyle( const inlineStyles = buildStyle({
fontFamily, fontFamily,
fontSize, fontSize,
codeBlockFontFamily, codeBlockFontFamily,
@@ -343,9 +338,13 @@ export default class MarkdownPreview extends React.Component {
theme, theme,
allowCustomCSS, allowCustomCSS,
customCSS customCSS
})
let body = this.markdown.render(noteContent)
body = attachmentManagement.fixLocalURLS(
body,
this.props.storagePath
) )
const body = this.markdown.render(noteContent) const files = [this.getCodeThemeLink(codeBlockTheme), ...CSS_FILES]
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
files.forEach(file => { files.forEach(file => {
if (global.process.platform === 'win32') { if (global.process.platform === 'win32') {
file = file.replace('file:///', '') file = file.replace('file:///', '')
@@ -381,7 +380,7 @@ export default class MarkdownPreview extends React.Component {
handleSaveAsPdf () { handleSaveAsPdf () {
this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => { this.exportAsDocument('pdf', (noteContent, exportTasks, targetDir) => {
const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: false}}) const printout = new remote.BrowserWindow({show: false, webPreferences: {webSecurity: false, javascript: false}})
printout.loadURL('data:text/html;charset=UTF-8,' + this.htmlContentFormatter(noteContent, exportTasks, targetDir)) printout.loadURL('data:text/html;charset=UTF-8,' + this.htmlContentFormatter(noteContent, exportTasks, targetDir))
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
printout.webContents.on('did-finish-load', () => { printout.webContents.on('did-finish-load', () => {
@@ -576,16 +575,19 @@ export default class MarkdownPreview extends React.Component {
} }
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value) this.rewriteIframe() // actual rewriteIframe function should be called only once
let needsRewriteIframe = false
if (prevProps.value !== this.props.value) needsRewriteIframe = true
if ( if (
prevProps.smartQuotes !== this.props.smartQuotes || prevProps.smartQuotes !== this.props.smartQuotes ||
prevProps.sanitize !== this.props.sanitize || prevProps.sanitize !== this.props.sanitize ||
prevProps.mermaidHTMLLabel !== this.props.mermaidHTMLLabel ||
prevProps.smartArrows !== this.props.smartArrows || prevProps.smartArrows !== this.props.smartArrows ||
prevProps.breaks !== this.props.breaks || prevProps.breaks !== this.props.breaks ||
prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox
) { ) {
this.initMarkdown() this.initMarkdown()
this.rewriteIframe() needsRewriteIframe = true
} }
if ( if (
prevProps.fontFamily !== this.props.fontFamily || prevProps.fontFamily !== this.props.fontFamily ||
@@ -600,8 +602,17 @@ export default class MarkdownPreview extends React.Component {
prevProps.customCSS !== this.props.customCSS prevProps.customCSS !== this.props.customCSS
) { ) {
this.applyStyle() this.applyStyle()
needsRewriteIframe = true
}
if (needsRewriteIframe) {
this.rewriteIframe() this.rewriteIframe()
} }
// Should scroll to top after selecting another note
if (prevProps.noteKey !== this.props.noteKey) {
this.getWindow().scrollTo(0, 0)
}
} }
getStyleParams () { getStyleParams () {
@@ -657,27 +668,27 @@ export default class MarkdownPreview extends React.Component {
this.getWindow().document.getElementById( this.getWindow().document.getElementById(
'codeTheme' 'codeTheme'
).href = this.GetCodeThemeLink(codeBlockTheme) ).href = this.getCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle( this.getWindow().document.getElementById('style').innerHTML = buildStyle({
fontFamily, fontFamily,
fontSize, fontSize,
codeBlockFontFamily, codeBlockFontFamily,
lineNumber, lineNumber,
scrollPastEnd, scrollPastEnd,
optimizeOverflowScroll: true,
theme, theme,
allowCustomCSS, allowCustomCSS,
customCSS customCSS
) })
this.getWindow().document.documentElement.style.overflowY = 'hidden'
} }
GetCodeThemeLink (name) { getCodeThemeLink (name) {
const theme = consts.THEMES.find(theme => theme.name === name) const theme = consts.THEMES.find(theme => theme.name === name)
if (theme) { return theme != null
return `${appPath}/${theme.path}` ? theme.path
} else { : `${appPath}/node_modules/codemirror/theme/elegant.css`
return `${appPath}/node_modules/codemirror/theme/elegant.css`
}
} }
rewriteIframe () { rewriteIframe () {
@@ -703,7 +714,8 @@ export default class MarkdownPreview extends React.Component {
showCopyNotification, showCopyNotification,
storagePath, storagePath,
noteKey, noteKey,
sanitize sanitize,
mermaidHTMLLabel
} = this.props } = this.props
let { value, codeBlockTheme } = this.props let { value, codeBlockTheme } = this.props
@@ -835,6 +847,7 @@ export default class MarkdownPreview extends React.Component {
canvas.height = height.value + 'vh' canvas.height = height.value + 'vh'
} }
// eslint-disable-next-line no-unused-vars
const chart = new Chart(canvas, chartConfig) const chart = new Chart(canvas, chartConfig)
} catch (e) { } catch (e) {
el.className = 'chart-error' el.className = 'chart-error'
@@ -845,7 +858,7 @@ export default class MarkdownPreview extends React.Component {
_.forEach( _.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'), this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
el => { el => {
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme) mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme, mermaidHTMLLabel)
} }
) )
@@ -895,6 +908,12 @@ export default class MarkdownPreview extends React.Component {
this.setImgOnClickEventHelper(img, rect) this.setImgOnClickEventHelper(img, rect)
imgObserver.observe(parentEl, config) imgObserver.observe(parentEl, config)
} }
const aList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll('a')
for (const a of aList) {
a.removeEventListener('click', this.linkClickHandler)
a.addEventListener('click', this.linkClickHandler)
}
} }
setImgOnClickEventHelper (img, rect) { setImgOnClickEventHelper (img, rect) {
@@ -967,8 +986,6 @@ export default class MarkdownPreview extends React.Component {
overlay.appendChild(zoomImg) overlay.appendChild(zoomImg)
document.body.appendChild(overlay) document.body.appendChild(overlay)
} }
this.getWindow().scrollTo(0, 0)
} }
focus () { focus () {
@@ -1023,11 +1040,11 @@ export default class MarkdownPreview extends React.Component {
if (!rawHref) return // not checked href because parser will create file://... string for [empty link]() if (!rawHref) return // not checked href because parser will create file://... string for [empty link]()
const regexNoteInternalLink = /.*[main.\w]*.html#/ const extractId = /(main.html)?#/
const regexNoteInternalLink = new RegExp(`${extractId.source}(.+)`)
if (regexNoteInternalLink.test(href)) { if (regexNoteInternalLink.test(linkHash)) {
const targetId = mdurl.encode(linkHash) const targetId = mdurl.encode(linkHash.replace(extractId, ''))
const targetElement = this.refs.root.contentWindow.document.querySelector( const targetElement = this.refs.root.contentWindow.document.getElementById(
targetId targetId
) )

View File

@@ -89,14 +89,15 @@ class MarkdownSplitEditor extends React.Component {
.split('\n') .split('\n')
const targetLine = lines[lineIndex] const targetLine = lines[lineIndex]
let newLine = targetLine
if (targetLine.match(checkedMatch)) { if (targetLine.match(checkedMatch)) {
lines[lineIndex] = targetLine.replace(checkReplace, '[ ]') newLine = targetLine.replace(checkReplace, '[ ]')
} }
if (targetLine.match(uncheckedMatch)) { if (targetLine.match(uncheckedMatch)) {
lines[lineIndex] = targetLine.replace(uncheckReplace, '[x]') newLine = targetLine.replace(uncheckReplace, '[x]')
} }
this.refs.code.setValue(lines.join('\n')) this.refs.code.setLineContent(lineIndex, newLine)
} }
} }
@@ -202,7 +203,6 @@ class MarkdownSplitEditor extends React.Component {
onMouseMove={e => this.handleMouseMove(e)} onMouseMove={e => this.handleMouseMove(e)}
onMouseUp={e => this.handleMouseUp(e)}> onMouseUp={e => this.handleMouseUp(e)}>
<CodeEditor <CodeEditor
styleName='codeEditor'
ref='code' ref='code'
width={editorStyle.width} width={editorStyle.width}
height={editorStyle.height} height={editorStyle.height}
@@ -213,6 +213,7 @@ class MarkdownSplitEditor extends React.Component {
fontFamily={config.editor.fontFamily} fontFamily={config.editor.fontFamily}
fontSize={editorStyle.fontSize} fontSize={editorStyle.fontSize}
displayLineNumbers={config.editor.displayLineNumbers} displayLineNumbers={config.editor.displayLineNumbers}
lineWrapping
matchingPairs={config.editor.matchingPairs} matchingPairs={config.editor.matchingPairs}
matchingTriples={config.editor.matchingTriples} matchingTriples={config.editor.matchingTriples}
explodingPairs={config.editor.explodingPairs} explodingPairs={config.editor.explodingPairs}
@@ -234,13 +235,13 @@ class MarkdownSplitEditor extends React.Component {
switchPreview={config.editor.switchPreview} switchPreview={config.editor.switchPreview}
enableMarkdownLint={config.editor.enableMarkdownLint} enableMarkdownLint={config.editor.enableMarkdownLint}
customMarkdownLintConfig={config.editor.customMarkdownLintConfig} customMarkdownLintConfig={config.editor.customMarkdownLintConfig}
deleteUnusedAttachments={config.editor.deleteUnusedAttachments}
/> />
<div styleName={isStacking ? 'slider-hoz' : 'slider'} style={{left: sliderStyle.left, top: sliderStyle.top}} onMouseDown={e => this.handleMouseDown(e)} > <div styleName={isStacking ? 'slider-hoz' : 'slider'} style={{left: sliderStyle.left, top: sliderStyle.top}} onMouseDown={e => this.handleMouseDown(e)} >
<div styleName='slider-hitbox' /> <div styleName='slider-hitbox' />
</div> </div>
<MarkdownPreview <MarkdownPreview
style={previewStyle} style={previewStyle}
styleName='preview'
theme={config.ui.theme} theme={config.ui.theme}
keyMap={config.editor.keyMap} keyMap={config.editor.keyMap}
fontSize={config.preview.fontSize} fontSize={config.preview.fontSize}
@@ -253,6 +254,7 @@ class MarkdownSplitEditor extends React.Component {
smartArrows={config.preview.smartArrows} smartArrows={config.preview.smartArrows}
breaks={config.preview.breaks} breaks={config.preview.breaks}
sanitize={config.preview.sanitize} sanitize={config.preview.sanitize}
mermaidHTMLLabel={config.preview.mermaidHTMLLabel}
ref='preview' ref='preview'
tabInde='0' tabInde='0'
value={value} value={value}

View File

@@ -9,6 +9,7 @@
top -2px top -2px
width 0 width 0
z-index 0 z-index 0
border-left 1px solid $ui-borderColor
.slider-hitbox .slider-hitbox
absolute top bottom left right absolute top bottom left right
width 7px width 7px
@@ -23,3 +24,23 @@
height 7px height 7px
cursor row-resize cursor row-resize
body[data-theme="dark"]
.root
.slider
border-left 1px solid $ui-dark-borderColor
body[data-theme="solarized-dark"]
.root
.slider
border-left 1px solid $ui-solarized-dark-borderColor
body[data-theme="monokai"]
.root
.slider
border-left 1px solid $ui-monokai-borderColor
body[data-theme="dracula"]
.root
.slider
border-left 1px solid $ui-dracula-borderColor

View File

@@ -8,7 +8,7 @@ const ModalEscButton = ({
}) => ( }) => (
<button styleName='escButton' onClick={handleEscButtonClick}> <button styleName='escButton' onClick={handleEscButtonClick}>
<div styleName='esc-mark'>×</div> <div styleName='esc-mark'>×</div>
<div styleName='esc-text'>esc</div> <div>esc</div>
</button> </button>
) )

View File

@@ -3,7 +3,7 @@
*/ */
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import { isArray } from 'lodash' import { isArray, sortBy } from 'lodash'
import invertColor from 'invert-color' import invertColor from 'invert-color'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import { getTodoStatus } from 'browser/lib/getTodoStatus' import { getTodoStatus } from 'browser/lib/getTodoStatus'
@@ -43,7 +43,7 @@ const TagElementList = (tags, showTagsAlphabetically, coloredTags) => {
} }
if (showTagsAlphabetically) { if (showTagsAlphabetically) {
return _.sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] })) return sortBy(tags).map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
} else { } else {
return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] })) return tags.map(tag => TagElement({ tagName: tag, color: coloredTags[tag] }))
} }
@@ -148,15 +148,14 @@ NoteItem.propTypes = {
tags: PropTypes.array, tags: PropTypes.array,
isStarred: PropTypes.bool.isRequired, isStarred: PropTypes.bool.isRequired,
isTrashed: PropTypes.bool.isRequired, isTrashed: PropTypes.bool.isRequired,
blog: { blog: PropTypes.shape({
blogLink: PropTypes.string, blogLink: PropTypes.string,
blogId: PropTypes.number blogId: PropTypes.number
} })
}), }),
handleNoteClick: PropTypes.func.isRequired, handleNoteClick: PropTypes.func.isRequired,
handleNoteContextMenu: PropTypes.func.isRequired, handleNoteContextMenu: PropTypes.func.isRequired,
handleDragStart: PropTypes.func.isRequired, handleDragStart: PropTypes.func.isRequired
handleDragEnd: PropTypes.func.isRequired
} }
export default CSSModules(NoteItem, styles) export default CSSModules(NoteItem, styles)

View File

@@ -74,7 +74,7 @@ SideNavFilter.propTypes = {
isStarredActive: PropTypes.bool.isRequired, isStarredActive: PropTypes.bool.isRequired,
isTrashedActive: PropTypes.bool.isRequired, isTrashedActive: PropTypes.bool.isRequired,
handleStarredButtonClick: PropTypes.func.isRequired, handleStarredButtonClick: PropTypes.func.isRequired,
handleTrashdButtonClick: PropTypes.func.isRequired handleTrashedButtonClick: PropTypes.func.isRequired
} }
export default CSSModules(SideNavFilter, styles) export default CSSModules(SideNavFilter, styles)

View File

@@ -114,7 +114,7 @@ class SnippetTab extends React.Component {
> >
{snippet.name.trim().length > 0 {snippet.name.trim().length > 0
? snippet.name ? snippet.name
: <span styleName='button-unnamed'> : <span>
{i18n.__('Unnamed')} {i18n.__('Unnamed')}
</span> </span>
} }

View File

@@ -25,10 +25,10 @@ const TodoProcess = ({
) )
TodoProcess.propTypes = { TodoProcess.propTypes = {
todoStatus: { todoStatus: PropTypes.exact({
total: PropTypes.number.isRequired, total: PropTypes.number.isRequired,
completed: PropTypes.number.isRequired completed: PropTypes.number.isRequired
} })
} }
export default CSSModules(TodoProcess, styles) export default CSSModules(TodoProcess, styles)

View File

@@ -19,7 +19,7 @@ function getId () {
return id return id
} }
function render (element, content, theme) { function render (element, content, theme, enableHTMLLabel) {
try { try {
const height = element.attributes.getNamedItem('data-height') const height = element.attributes.getNamedItem('data-height')
if (height && height.value !== 'undefined') { if (height && height.value !== 'undefined') {
@@ -29,7 +29,8 @@ function render (element, content, theme) {
mermaidAPI.initialize({ mermaidAPI.initialize({
theme: isDarkTheme ? 'dark' : 'default', theme: isDarkTheme ? 'dark' : 'default',
themeCSS: isDarkTheme ? darkThemeStyling : '', themeCSS: isDarkTheme ? darkThemeStyling : '',
useMaxWidth: false useMaxWidth: false,
flowchart: { htmlLabels: enableHTMLLabel }
}) })
mermaidAPI.render(getId(), content, (svgGraph) => { mermaidAPI.render(getId(), content, (svgGraph) => {
element.innerHTML = svgGraph element.innerHTML = svgGraph

View File

@@ -7,6 +7,7 @@ const CODEMIRROR_THEME_PATH = 'node_modules/codemirror/theme'
const CODEMIRROR_EXTRA_THEME_PATH = 'extra_scripts/codemirror/theme' const CODEMIRROR_EXTRA_THEME_PATH = 'extra_scripts/codemirror/theme'
const isProduction = process.env.NODE_ENV === 'production' const isProduction = process.env.NODE_ENV === 'production'
const paths = [ const paths = [
isProduction ? path.join(app.getAppPath(), CODEMIRROR_THEME_PATH) : path.resolve(CODEMIRROR_THEME_PATH), isProduction ? path.join(app.getAppPath(), CODEMIRROR_THEME_PATH) : path.resolve(CODEMIRROR_THEME_PATH),
isProduction ? path.join(app.getAppPath(), CODEMIRROR_EXTRA_THEME_PATH) : path.resolve(CODEMIRROR_EXTRA_THEME_PATH) isProduction ? path.join(app.getAppPath(), CODEMIRROR_EXTRA_THEME_PATH) : path.resolve(CODEMIRROR_EXTRA_THEME_PATH)
@@ -18,7 +19,7 @@ const themes = paths
return { return {
name, name,
path: path.join(directory.split(/\//g).slice(-3).join('/'), file), path: path.join(directory, file),
className: `cm-s-${name}` className: `cm-s-${name}`
} }
})) }))
@@ -27,17 +28,16 @@ const themes = paths
themes.splice(themes.findIndex(({ name }) => name === 'solarized'), 1, { themes.splice(themes.findIndex(({ name }) => name === 'solarized'), 1, {
name: 'solarized dark', name: 'solarized dark',
path: `${CODEMIRROR_THEME_PATH}/solarized.css`, path: path.join(paths[0], 'solarized.css'),
className: `cm-s-solarized cm-s-dark` className: `cm-s-solarized cm-s-dark`
}, { }, {
name: 'solarized light', name: 'solarized light',
path: `${CODEMIRROR_THEME_PATH}/solarized.css`, path: path.join(paths[0], 'solarized.css'),
className: `cm-s-solarized cm-s-light` className: `cm-s-solarized cm-s-light`
}) })
themes.splice(0, 0, { themes.splice(0, 0, {
name: 'default', name: 'default',
path: `${CODEMIRROR_THEME_PATH}/elegant.css`, path: path.join(paths[0], 'elegant.css'),
className: `cm-s-default` className: `cm-s-default`
}) })

View File

@@ -1,6 +1,12 @@
import i18n from 'browser/lib/i18n'
import fs from 'fs'
const {remote} = require('electron') const {remote} = require('electron')
const {Menu} = remote.require('electron') const {Menu} = remote.require('electron')
const {clipboard} = remote.require('electron')
const {shell} = remote.require('electron')
const spellcheck = require('./spellcheck') const spellcheck = require('./spellcheck')
const uri2path = require('file-uri-to-path')
/** /**
* Creates the context menu that is shown when there is a right click in the editor of a (not-snippet) note. * Creates the context menu that is shown when there is a right click in the editor of a (not-snippet) note.
@@ -62,4 +68,57 @@ const buildEditorContextMenu = function (editor, event) {
return Menu.buildFromTemplate(template) return Menu.buildFromTemplate(template)
} }
module.exports = buildEditorContextMenu /**
* Creates the context menu that is shown when there is a right click Markdown preview of a (not-snippet) note.
* @param {MarkdownPreview} markdownPreview
* @param {MouseEvent} event that has triggered the creation of the context menu
* @returns {Electron.Menu} The created electron context menu
*/
const buildMarkdownPreviewContextMenu = function (markdownPreview, event) {
if (markdownPreview == null || event == null || event.pageX == null || event.pageY == null) {
return null
}
// Default context menu inclusions
const template = [{
role: 'copy'
}, {
role: 'selectall'
}]
if (event.target.tagName.toLowerCase() === 'a' && event.target.getAttribute('href')) {
// Link opener for files on the local system pointed to by href
const href = event.target.href
const isLocalFile = href.startsWith('file:')
if (isLocalFile) {
const absPath = uri2path(href)
try {
if (fs.lstatSync(absPath).isFile()) {
template.push(
{
label: i18n.__('Show in explorer'),
click: (e) => shell.showItemInFolder(absPath)
}
)
}
} catch (e) {
console.log('Error while evaluating if the file is locally available', e)
}
}
// Add option to context menu to copy url
template.push(
{
label: i18n.__('Copy Url'),
click: (e) => clipboard.writeText(href)
}
)
}
return Menu.buildFromTemplate(template)
}
module.exports =
{
buildEditorContextMenu: buildEditorContextMenu,
buildMarkdownPreviewContextMenu: buildMarkdownPreviewContextMenu
}

View File

@@ -1,5 +1,10 @@
import CodeMirror from 'codemirror' import CodeMirror from 'codemirror'
import 'codemirror-mode-elixir' import 'codemirror-mode-elixir'
const stylusCodeInfo = CodeMirror.modeInfo.find(info => info.name === 'Stylus')
if (stylusCodeInfo == null) {
CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']}) CodeMirror.modeInfo.push({name: 'Stylus', mime: 'text/x-styl', mode: 'stylus', ext: ['styl'], alias: ['styl']})
} else {
stylusCodeInfo.alias = ['styl']
}
CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']}) CodeMirror.modeInfo.push({name: 'Elixir', mime: 'text/x-elixir', mode: 'elixir', ext: ['ex']})

View File

@@ -1,5 +1,4 @@
const crypto = require('crypto') const crypto = require('crypto')
const _ = require('lodash')
const uuidv4 = require('uuid/v4') const uuidv4 = require('uuid/v4')
module.exports = function (uuid) { module.exports = function (uuid) {

View File

@@ -15,7 +15,7 @@ module.exports = function sanitizePlugin (md, options) {
options options
) )
} }
if (state.tokens[tokenIdx].type === '_fence') { if (state.tokens[tokenIdx].type.match(/.*_fence$/)) {
// escapeHtmlCharacters has better performance // escapeHtmlCharacters has better performance
state.tokens[tokenIdx].content = escapeHtmlCharacters( state.tokens[tokenIdx].content = escapeHtmlCharacters(
state.tokens[tokenIdx].content, state.tokens[tokenIdx].content,
@@ -96,6 +96,10 @@ function sanitizeInline (html, options) {
function naughtyHRef (href, options) { function naughtyHRef (href, options) {
// href = href.replace(/[\x00-\x20]+/g, '') // href = href.replace(/[\x00-\x20]+/g, '')
if (!href) {
// No href
return false
}
href = href.replace(/<\!\-\-.*?\-\-\>/g, '') href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
const matches = href.match(/^([a-zA-Z]+)\:/) const matches = href.match(/^([a-zA-Z]+)\:/)

View File

@@ -21,7 +21,7 @@ function uniqueSlug (slug, slugs, opts) {
} }
function linkify (token) { function linkify (token) {
token.content = mdlink(token.content, '#' + token.slug) token.content = mdlink(token.content, `#${decodeURI(token.slug)}`)
return token return token
} }

View File

@@ -2,6 +2,7 @@ import markdownit from 'markdown-it'
import sanitize from './markdown-it-sanitize-html' import sanitize from './markdown-it-sanitize-html'
import emoji from 'markdown-it-emoji' import emoji from 'markdown-it-emoji'
import math from '@rokt33r/markdown-it-math' import math from '@rokt33r/markdown-it-math'
import mdurl from 'mdurl'
import smartArrows from 'markdown-it-smartarrows' import smartArrows from 'markdown-it-smartarrows'
import _ from 'lodash' import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
@@ -150,9 +151,9 @@ class Markdown {
const content = token.content.split('\n').slice(0, -1).map(line => { const content = token.content.split('\n').slice(0, -1).map(line => {
const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line) const match = /!\[[^\]]*]\(([^\)]*)\)/.exec(line)
if (match) { if (match) {
return match[1] return mdurl.encode(match[1])
} else { } else {
return line return mdurl.encode(line)
} }
}).join('\n') }).join('\n')
@@ -288,8 +289,10 @@ class Markdown {
case 'list_item_open': case 'list_item_open':
case 'paragraph_open': case 'paragraph_open':
case 'table_open': case 'table_open':
if (token.map) {
token.attrPush(['data-line', token.map[0]]) token.attrPush(['data-line', token.map[0]])
} }
}
}) })
const result = originalRender.call(this.md.renderer, tokens, options, env) const result = originalRender.call(this.md.renderer, tokens, options, env)
return result return result

View File

@@ -1,17 +1,11 @@
import diacritics from 'diacritics-map'
function replaceDiacritics (str) {
return str.replace(/[À-ž]/g, function (ch) {
return diacritics[ch] || ch
})
}
module.exports = function slugify (title) { module.exports = function slugify (title) {
let slug = title.trim() const slug = encodeURI(
title.trim()
.replace(/^\s+/, '')
.replace(/\s+$/, '')
.replace(/\s+/g, '-')
.replace(/[\]\[\!\'\#\$\%\&\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\{\|\}\~\`]/g, '')
)
slug = replaceDiacritics(slug) return slug
slug = slug.replace(/[^\w\s-]/g, '').replace(/\s+/g, '-')
return encodeURI(slug).replace(/\-+$/, '')
} }

9
browser/lib/turndown.js Normal file
View File

@@ -0,0 +1,9 @@
const TurndownService = require('turndown')
const { gfm } = require('turndown-plugin-gfm')
export const createTurndownService = function () {
const turndown = new TurndownService()
turndown.use(gfm)
turndown.remove('script')
return turndown
}

View File

@@ -136,9 +136,24 @@ export function isMarkdownTitleURL (str) {
return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str) return /(^#{1,6}\s)(?:\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str)
} }
export function humanFileSize (bytes) {
const threshold = 1000
if (Math.abs(bytes) < threshold) {
return bytes + ' B'
}
var units = ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
var u = -1
do {
bytes /= threshold
++u
} while (Math.abs(bytes) >= threshold && u < units.length - 1)
return bytes.toFixed(1) + ' ' + units[u]
}
export default { export default {
lastFindInArray, lastFindInArray,
escapeHtmlCharacters, escapeHtmlCharacters,
isObjectEqual, isObjectEqual,
isMarkdownTitleURL isMarkdownTitleURL,
humanFileSize
} }

View File

@@ -0,0 +1,69 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './FromUrlButton.styl'
import _ from 'lodash'
import i18n from 'browser/lib/i18n'
class FromUrlButton extends React.Component {
constructor (props) {
super(props)
this.state = {
isActive: false
}
}
handleMouseDown (e) {
this.setState({
isActive: true
})
}
handleMouseUp (e) {
this.setState({
isActive: false
})
}
handleMouseLeave (e) {
this.setState({
isActive: false
})
}
render () {
const { className } = this.props
return (
<button className={_.isString(className)
? 'FromUrlButton ' + className
: 'FromUrlButton'
}
styleName={this.state.isActive || this.props.isActive
? 'root--active'
: 'root'
}
onMouseDown={(e) => this.handleMouseDown(e)}
onMouseUp={(e) => this.handleMouseUp(e)}
onMouseLeave={(e) => this.handleMouseLeave(e)}
onClick={this.props.onClick}>
<img styleName='icon'
src={this.state.isActive || this.props.isActive
? '../resources/icon/icon-external.svg'
: '../resources/icon/icon-external.svg'
}
/>
<span styleName='tooltip'>{i18n.__('Convert URL to Markdown')}</span>
</button>
)
}
}
FromUrlButton.propTypes = {
isActive: PropTypes.bool,
onClick: PropTypes.func,
className: PropTypes.string
}
export default CSSModules(FromUrlButton, styles)

View File

@@ -0,0 +1,41 @@
.root
top 45px
topBarButtonRight()
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)
&:hover .tooltip
opacity 1
.tooltip
tooltip()
position absolute
pointer-events none
top 50px
right 125px
width 90px
z-index 200
padding 5px
line-height normal
border-radius 2px
opacity 0
transition 0.1s
.root--active
@extend .root
transition 0.15s
color $ui-favorite-star-button-color
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)
.icon
transition transform 0.15s
height 13px
body[data-theme="dark"]
.root
topBarButtonDark()
&:hover
transition 0.2s
color alpha($ui-favorite-star-button-color, 0.6)

View File

@@ -11,7 +11,7 @@ const FullscreenButton = ({
const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B' const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
return ( return (
<button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}> <button styleName='control-fullScreenButton' title={i18n.__('Fullscreen')} onMouseDown={(e) => onClick(e)}>
<img styleName='iconInfo' src='../resources/icon/icon-full.svg' /> <img src='../resources/icon/icon-full.svg' />
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span> <span lang={i18n.locale} styleName='tooltip'>{i18n.__('Fullscreen')}({hotkey})</span>
</button> </button>
) )

View File

@@ -60,7 +60,7 @@ class InfoPanel extends React.Component {
</div> </div>
<div> <div>
<input styleName='infoPanel-noteLink' ref='noteLink' value={noteLink} onClick={(e) => { e.target.select() }} /> <input styleName='infoPanel-noteLink' ref='noteLink' defaultValue={noteLink} onClick={(e) => { e.target.select() }} />
<button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'> <button onClick={() => this.copyNoteLink()} styleName='infoPanel-copyButton'>
<i className='fa fa-clipboard' /> <i className='fa fa-clipboard' />
</button> </button>

View File

@@ -69,9 +69,6 @@ class MarkdownNoteDetail extends React.Component {
}) })
ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this)) ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
ee.on('code:generate-toc', this.generateToc) ee.on('code:generate-toc', this.generateToc)
// Focus content if using blur or double click
if (this.state.switchPreview === 'BLUR' || this.state.switchPreview === 'DBL_CLICK') this.focus()
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
@@ -86,6 +83,20 @@ class MarkdownNoteDetail extends React.Component {
if (this.refs.tags) this.refs.tags.reset() if (this.refs.tags) this.refs.tags.reset()
}) })
} }
// Focus content if using blur or double click
// --> Moved here from componentDidMount so a re-render during search won't set focus to the editor
const {switchPreview} = nextProps.config.editor
if (this.state.switchPreview !== switchPreview) {
this.setState({
switchPreview
})
if (switchPreview === 'BLUR' || switchPreview === 'DBL_CLICK') {
console.log('setting focus', switchPreview)
this.focus()
}
}
} }
componentWillUnmount () { componentWillUnmount () {
@@ -143,7 +154,6 @@ class MarkdownNoteDetail extends React.Component {
} }
handleFolderChange (e) { handleFolderChange (e) {
const { dispatch } = this.props
const { note } = this.state const { note } = this.state
const value = this.refs.folder.value const value = this.refs.folder.value
const splitted = value.split('-') const splitted = value.split('-')
@@ -302,7 +312,7 @@ class MarkdownNoteDetail extends React.Component {
} }
getToggleLockButton () { getToggleLockButton () {
return this.state.isLocked ? '../resources/icon/icon-previewoff-on.svg' : '../resources/icon/icon-previewoff-off.svg' return this.state.isLocked ? '../resources/icon/icon-lock.svg' : '../resources/icon/icon-unlock.svg'
} }
handleDeleteKeyDown (e) { handleDeleteKeyDown (e) {
@@ -411,7 +421,7 @@ class MarkdownNoteDetail extends React.Component {
} }
render () { render () {
const { data, location, config } = this.props const { data, dispatch, location, config } = this.props
const { note, editorType, isStacking } = this.state const { note, editorType, isStacking } = this.state
const storageKey = note.storage const storageKey = note.storage
const folderKey = note.folder const folderKey = note.folder
@@ -451,7 +461,7 @@ class MarkdownNoteDetail extends React.Component {
const detailTopBar = <div styleName='info'> const detailTopBar = <div styleName='info'>
<div styleName='info-left'> <div styleName='info-left'>
<div styleName='info-left-top'> <div>
<FolderSelect styleName='info-left-top-folderSelect' <FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder} value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder' ref='folder'
@@ -466,6 +476,7 @@ class MarkdownNoteDetail extends React.Component {
saveTagsAlphabetically={config.ui.saveTagsAlphabetically} saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically} showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data} data={data}
dispatch={dispatch}
onChange={this.handleUpdateTag.bind(this)} onChange={this.handleUpdateTag.bind(this)}
coloredTags={config.coloredTags} coloredTags={config.coloredTags}
/> />
@@ -478,6 +489,7 @@ class MarkdownNoteDetail extends React.Component {
} }
<ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} /> <ToggleModeButton onClick={(e) => this.handleSwitchMode(e)} editorType={editorType} />
<StarButton <StarButton
onClick={(e) => this.handleStarButtonClick(e)} onClick={(e) => this.handleStarButtonClick(e)}
isActive={note.isStarred} isActive={note.isStarred}
@@ -490,7 +502,7 @@ class MarkdownNoteDetail extends React.Component {
onFocus={(e) => this.handleFocus(e)} onFocus={(e) => this.handleFocus(e)}
onMouseDown={(e) => this.handleLockButtonMouseDown(e)} onMouseDown={(e) => this.handleLockButtonMouseDown(e)}
> >
<img styleName='iconInfo' src={imgSrc} /> <img src={imgSrc} />
{this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>} {this.state.isLocked ? <span styleName='tooltip'>Unlock</span> : <span styleName='tooltip'>Lock</span>}
</button> </button>
@@ -517,7 +529,7 @@ class MarkdownNoteDetail extends React.Component {
exportAsTxt={this.exportAsTxt} exportAsTxt={this.exportAsTxt}
exportAsHtml={this.exportAsHtml} exportAsHtml={this.exportAsHtml}
exportAsPdf={this.exportAsPdf} exportAsPdf={this.exportAsPdf}
wordCount={note.content.split(' ').length} wordCount={note.content.trim().split(/\s+/g).length}
letterCount={note.content.replace(/\r?\n/g, '').length} letterCount={note.content.replace(/\r?\n/g, '').length}
type={note.type} type={note.type}
print={this.print} print={this.print}

View File

@@ -82,10 +82,3 @@ body[data-theme="dracula"]
border-left 1px solid $ui-dracula-borderColor border-left 1px solid $ui-dracula-borderColor
background-color $ui-dracula-noteDetail-backgroundColor background-color $ui-dracula-noteDetail-backgroundColor
div
> button, div
-webkit-user-drag none
user-select none
> img, span
-webkit-user-drag none
user-select none

View File

@@ -108,3 +108,11 @@ body[data-theme="dracula"]
.info .info
border-color $ui-dracula-borderColor border-color $ui-dracula-borderColor
background-color $ui-dracula-noteDetail-backgroundColor background-color $ui-dracula-noteDetail-backgroundColor
.info > div
> button
-webkit-user-drag none
user-select none
> img, span
-webkit-user-drag none
user-select none

View File

@@ -10,7 +10,7 @@ const PermanentDeleteButton = ({
<button styleName='control-trashButton--in-trash' <button styleName='control-trashButton--in-trash'
onClick={(e) => onClick(e)} onClick={(e) => onClick(e)}
> >
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' /> <img src='../resources/icon/icon-trash.svg' />
<span styleName='tooltip'>{i18n.__('Permanent Delete')}</span> <span styleName='tooltip'>{i18n.__('Permanent Delete')}</span>
</button> </button>
) )

View File

@@ -518,6 +518,19 @@ class SnippetNoteDetail extends React.Component {
]) ])
} }
handleWrapLineButtonClick (e) {
context.popup([
{
label: 'on',
click: (e) => this.handleWrapLineItemClick(e, true)
},
{
label: 'off',
click: (e) => this.handleWrapLineItemClick(e, false)
}
])
}
handleIndentSizeItemClick (e, indentSize) { handleIndentSizeItemClick (e, indentSize) {
const { config, dispatch } = this.props const { config, dispatch } = this.props
const editor = Object.assign({}, config.editor, { const editor = Object.assign({}, config.editor, {
@@ -550,6 +563,22 @@ class SnippetNoteDetail extends React.Component {
}) })
} }
handleWrapLineItemClick (e, lineWrapping) {
const { config, dispatch } = this.props
const editor = Object.assign({}, config.editor, {
lineWrapping
})
ConfigManager.set({
editor
})
dispatch({
type: 'SET_CONFIG',
config: {
editor
}
})
}
focus () { focus () {
this.refs.description.focus() this.refs.description.focus()
} }
@@ -670,7 +699,7 @@ class SnippetNoteDetail extends React.Component {
} }
render () { render () {
const { data, config, location } = this.props const { data, dispatch, config, location } = this.props
const { note } = this.state const { note } = this.state
const storageKey = note.storage const storageKey = note.storage
@@ -720,6 +749,7 @@ class SnippetNoteDetail extends React.Component {
mode={snippet.mode || (autoDetect ? null : config.editor.snippetDefaultLanguage)} mode={snippet.mode || (autoDetect ? null : config.editor.snippetDefaultLanguage)}
value={snippet.content} value={snippet.content}
linesHighlighted={snippet.linesHighlighted} linesHighlighted={snippet.linesHighlighted}
lineWrapping={config.editor.lineWrapping}
theme={config.editor.theme} theme={config.editor.theme}
fontFamily={config.editor.fontFamily} fontFamily={config.editor.fontFamily}
fontSize={editorFontSize} fontSize={editorFontSize}
@@ -778,7 +808,7 @@ class SnippetNoteDetail extends React.Component {
const detailTopBar = <div styleName='info'> const detailTopBar = <div styleName='info'>
<div styleName='info-left'> <div styleName='info-left'>
<div styleName='info-left-top'> <div>
<FolderSelect styleName='info-left-top-folderSelect' <FolderSelect styleName='info-left-top-folderSelect'
value={this.state.note.storage + '-' + this.state.note.folder} value={this.state.note.storage + '-' + this.state.note.folder}
ref='folder' ref='folder'
@@ -793,6 +823,7 @@ class SnippetNoteDetail extends React.Component {
saveTagsAlphabetically={config.ui.saveTagsAlphabetically} saveTagsAlphabetically={config.ui.saveTagsAlphabetically}
showTagsAlphabetically={config.ui.showTagsAlphabetically} showTagsAlphabetically={config.ui.showTagsAlphabetically}
data={data} data={data}
dispatch={dispatch}
onChange={(e) => this.handleChange(e)} onChange={(e) => this.handleChange(e)}
coloredTags={config.coloredTags} coloredTags={config.coloredTags}
/> />
@@ -899,6 +930,12 @@ class SnippetNoteDetail extends React.Component {
size: {config.editor.indentSize}&nbsp; size: {config.editor.indentSize}&nbsp;
<i className='fa fa-caret-down' /> <i className='fa fa-caret-down' />
</button> </button>
<button
onClick={(e) => this.handleWrapLineButtonClick(e)}
>
Wrap Line: {config.editor.lineWrapping ? 'on' : 'off'}&nbsp;
<i className='fa fa-caret-down' />
</button>
</div> </div>
<StatusBar <StatusBar

View File

@@ -8,6 +8,7 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import Autosuggest from 'react-autosuggest' import Autosuggest from 'react-autosuggest'
import { push } from 'connected-react-router'
class TagSelect extends React.Component { class TagSelect extends React.Component {
constructor (props) { constructor (props) {
@@ -96,8 +97,11 @@ class TagSelect extends React.Component {
} }
handleTagLabelClick (tag) { handleTagLabelClick (tag) {
const { router } = this.context const { dispatch } = this.props
router.push(`/tags/${tag}`)
// Note: `tag` requires encoding later.
// E.g. % in tag is a problem (see issue #3170) - encodeURIComponent(tag) is not working.
dispatch(push(`/tags/${tag}`))
} }
handleTagRemoveButtonClick (tag) { handleTagRemoveButtonClick (tag) {
@@ -255,11 +259,8 @@ class TagSelect extends React.Component {
} }
} }
TagSelect.contextTypes = {
router: PropTypes.shape({})
}
TagSelect.propTypes = { TagSelect.propTypes = {
dispatch: PropTypes.func,
className: PropTypes.string, className: PropTypes.string,
value: PropTypes.arrayOf(PropTypes.string), value: PropTypes.arrayOf(PropTypes.string),
onChange: PropTypes.func, onChange: PropTypes.func,

View File

@@ -8,11 +8,11 @@ const ToggleModeButton = ({
onClick, editorType onClick, editorType
}) => ( }) => (
<div styleName='control-toggleModeButton'> <div styleName='control-toggleModeButton'>
<div styleName={editorType === 'SPLIT' ? 'active' : 'non-active'} onClick={() => onClick('SPLIT')}> <div styleName={editorType === 'SPLIT' ? 'active' : undefined} onClick={() => onClick('SPLIT')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} /> <img src={editorType === 'EDITOR_PREVIEW' ? '../resources/icon/icon-mode-markdown-off-active.svg' : ''} />
</div> </div>
<div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : 'non-active'} onClick={() => onClick('EDITOR_PREVIEW')}> <div styleName={editorType === 'EDITOR_PREVIEW' ? 'active' : undefined} onClick={() => onClick('EDITOR_PREVIEW')}>
<img styleName='item-star' src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} /> <img src={editorType === 'EDITOR_PREVIEW' ? '' : '../resources/icon/icon-mode-split-on-active.svg'} />
</div> </div>
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Mode')}</span> <span lang={i18n.locale} styleName='tooltip'>{i18n.__('Toggle Mode')}</span>
</div> </div>
@@ -20,7 +20,7 @@ const ToggleModeButton = ({
ToggleModeButton.propTypes = { ToggleModeButton.propTypes = {
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
editorType: PropTypes.string.Required editorType: PropTypes.string.isRequired
} }
export default CSSModules(ToggleModeButton, styles) export default CSSModules(ToggleModeButton, styles)

View File

@@ -75,3 +75,10 @@ body[data-theme="dracula"]
.active .active
background-color #bd93f9 background-color #bd93f9
box-shadow 2px 0px 7px #222222 box-shadow 2px 0px 7px #222222
.control-toggleModeButton
-webkit-user-drag none
user-select none
> div img
-webkit-user-drag none
user-select none

View File

@@ -10,7 +10,7 @@ const TrashButton = ({
<button styleName='control-trashButton' <button styleName='control-trashButton'
onClick={(e) => onClick(e)} onClick={(e) => onClick(e)}
> >
<img styleName='iconInfo' src='../resources/icon/icon-trash.svg' /> <img src='../resources/icon/icon-trash.svg' />
<span lang={i18n.locale} styleName='tooltip'>{i18n.__('Trash')}</span> <span lang={i18n.locale} styleName='tooltip'>{i18n.__('Trash')}</span>
</button> </button>
) )

View File

@@ -1,8 +1,8 @@
/* eslint-disable no-undef */ /* eslint-disable no-undef */
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line global-require
module.exports = require('./index.prod').default
} else {
// eslint-disable-next-line global-require // eslint-disable-next-line global-require
module.exports = require('./index.dev').default module.exports = require('./index.dev').default
} else {
// eslint-disable-next-line global-require
module.exports = require('./index.prod').default
} }

View File

@@ -102,7 +102,7 @@ class Main extends React.Component {
{ {
name: 'example.js', name: 'example.js',
mode: 'javascript', mode: 'javascript',
content: "var boostnote = document.getElementById('enjoy').innerHTML\n\nconsole.log(boostnote)", content: "var boostnote = document.getElementById('hello').innerHTML\n\nconsole.log(boostnote)",
linesHighlighted: [] linesHighlighted: []
} }
] ]
@@ -169,6 +169,7 @@ class Main extends React.Component {
} }
}) })
// eslint-disable-next-line no-undef
delete CodeMirror.keyMap.emacs['Ctrl-V'] delete CodeMirror.keyMap.emacs['Ctrl-V']
eventEmitter.on('editor:fullscreen', this.toggleFullScreen) eventEmitter.on('editor:fullscreen', this.toggleFullScreen)

View File

@@ -90,7 +90,7 @@ class NewNoteButton extends React.Component {
<div styleName='control'> <div styleName='control'>
<button styleName='control-newNoteButton' <button styleName='control-newNoteButton'
onClick={this.handleNewNoteButtonClick}> onClick={this.handleNewNoteButtonClick}>
<img styleName='iconTag' src='../resources/icon/icon-newnote.svg' /> <img src='../resources/icon/icon-newnote.svg' />
<span styleName='control-newNoteButton-tooltip'> <span styleName='control-newNoteButton-tooltip'>
{i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N {i18n.__('Make a note')} {OSX ? '⌘' : i18n.__('Ctrl')} + N
</span> </span>

View File

@@ -1138,7 +1138,7 @@ class NoteList extends React.Component {
} }
onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')} onClick={(e) => this.handleListStyleButtonClick(e, 'DEFAULT')}
> >
<img styleName='iconTag' src='../resources/icon/icon-column.svg' /> <img src='../resources/icon/icon-column.svg' />
</button> </button>
<button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL' <button title={i18n.__('Compressed View')} styleName={config.listStyle === 'SMALL'
? 'control-button--active' ? 'control-button--active'
@@ -1146,7 +1146,7 @@ class NoteList extends React.Component {
} }
onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')} onClick={(e) => this.handleListStyleButtonClick(e, 'SMALL')}
> >
<img styleName='iconTag' src='../resources/icon/icon-column-list.svg' /> <img src='../resources/icon/icon-column-list.svg' />
</button> </button>
</div> </div>
</div> </div>

View File

@@ -8,7 +8,7 @@ const PreferenceButton = ({
onClick onClick
}) => ( }) => (
<button styleName='top-menu-preference' onClick={(e) => onClick(e)}> <button styleName='top-menu-preference' onClick={(e) => onClick(e)}>
<img styleName='iconTag' src='../resources/icon/icon-setting.svg' /> <img src='../resources/icon/icon-setting.svg' />
<span styleName='tooltip'>{i18n.__('Preferences')}</span> <span styleName='tooltip'>{i18n.__('Preferences')}</span>
</button> </button>
) )

View File

@@ -362,14 +362,14 @@ class StorageItem extends React.Component {
<button styleName='header-addFolderButton' <button styleName='header-addFolderButton'
onClick={(e) => this.handleAddFolderButtonClick(e)} onClick={(e) => this.handleAddFolderButtonClick(e)}
> >
<img styleName='iconTag' src='../resources/icon/icon-plus.svg' /> <img src='../resources/icon/icon-plus.svg' />
</button> </button>
} }
<button styleName='header-info' <button styleName='header-info'
onClick={(e) => this.handleHeaderInfoClick(e)} onClick={(e) => this.handleHeaderInfoClick(e)}
> >
<span styleName='header-info-name'> <span>
{isFolded ? _.truncate(storage.name, {length: 1, omission: ''}) : storage.name} {isFolded ? _.truncate(storage.name, {length: 1, omission: ''}) : storage.name}
</span> </span>
{isFolded && {isFolded &&
@@ -380,7 +380,7 @@ class StorageItem extends React.Component {
</button> </button>
</div> </div>
{this.state.isOpen && {this.state.isOpen &&
<div styleName='folderList' > <div>
{folderList} {folderList}
</div> </div>
} }

View File

@@ -22,9 +22,10 @@ import context from 'browser/lib/context'
import { remote } from 'electron' import { remote } from 'electron'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import ColorPicker from 'browser/components/ColorPicker' import ColorPicker from 'browser/components/ColorPicker'
import { every, sortBy } from 'lodash'
function matchActiveTags (tags, activeTags) { function matchActiveTags (tags, activeTags) {
return _.every(activeTags, v => tags.indexOf(v) >= 0) return every(activeTags, v => tags.indexOf(v) >= 0)
} }
class SideNav extends React.Component { class SideNav extends React.Component {
@@ -271,6 +272,7 @@ class SideNav extends React.Component {
<div styleName='tagList'> <div styleName='tagList'>
{this.tagListComponent(data)} {this.tagListComponent(data)}
</div> </div>
<NavToggleButton isFolded={isFolded} handleToggleButtonClick={this.handleToggleButtonClick.bind(this)} />
</div> </div>
) )
} }
@@ -283,7 +285,7 @@ class SideNav extends React.Component {
const { colorPicker } = this.state const { colorPicker } = this.state
const activeTags = this.getActiveTags(location.pathname) const activeTags = this.getActiveTags(location.pathname)
const relatedTags = this.getRelatedTags(activeTags, data.noteMap) const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
let tagList = _.sortBy(data.tagNoteMap.map( let tagList = sortBy(data.tagNoteMap.map(
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) }) (tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
).filter( ).filter(
tag => tag.size > 0 tag => tag.size > 0
@@ -296,7 +298,7 @@ class SideNav extends React.Component {
}) })
} }
if (config.sortTagsBy === 'COUNTER') { if (config.sortTagsBy === 'COUNTER') {
tagList = _.sortBy(tagList, item => (0 - item.size)) tagList = sortBy(tagList, item => (0 - item.size))
} }
if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) { if (config.ui.showOnlyRelatedTags && (relatedTags.size > 0)) {
tagList = tagList.filter( tagList = tagList.filter(
@@ -440,7 +442,7 @@ class SideNav extends React.Component {
const style = {} const style = {}
if (!isFolded) style.width = this.props.width if (!isFolded) style.width = this.props.width
const isTagActive = location.pathname.match(/tag/) const isTagActive = /tag/.test(location.pathname)
return ( return (
<div className='SideNav' <div className='SideNav'
styleName={isFolded ? 'root--folded' : 'root'} styleName={isFolded ? 'root--folded' : 'root'}

View File

@@ -7,8 +7,8 @@ import ee from 'browser/main/lib/eventEmitter'
import NewNoteButton from 'browser/main/NewNoteButton' import NewNoteButton from 'browser/main/NewNoteButton'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import debounce from 'lodash/debounce' import debounce from 'lodash/debounce'
import CInput from 'react-composition-input'
import { push } from 'connected-react-router' import { push } from 'connected-react-router'
import queryString from 'query-string'
class TopBar extends React.Component { class TopBar extends React.Component {
constructor (props) { constructor (props) {
@@ -17,21 +17,29 @@ class TopBar extends React.Component {
this.state = { this.state = {
search: '', search: '',
searchOptions: [], searchOptions: [],
isSearching: false, isSearching: false
isAlphabet: false,
isIME: false,
isConfirmTranslation: false
} }
const { dispatch } = this.props
this.focusSearchHandler = () => { this.focusSearchHandler = () => {
this.handleOnSearchFocus() this.handleOnSearchFocus()
} }
this.codeInitHandler = this.handleCodeInit.bind(this) this.codeInitHandler = this.handleCodeInit.bind(this)
this.updateKeyword = this.updateKeyword.bind(this) this.handleKeyDown = this.handleKeyDown.bind(this)
this.handleSearchFocus = this.handleSearchFocus.bind(this)
this.handleSearchBlur = this.handleSearchBlur.bind(this)
this.handleSearchChange = this.handleSearchChange.bind(this)
this.handleSearchClearButton = this.handleSearchClearButton.bind(this) this.handleSearchClearButton = this.handleSearchClearButton.bind(this)
this.updateKeyword = debounce(this.updateKeyword, 1000 / 60, { this.debouncedUpdateKeyword = debounce((keyword) => {
dispatch(push(`/searched/${encodeURIComponent(keyword)}`))
this.setState({
search: keyword
})
ee.emit('top:search', keyword)
}, 1000 / 60, {
maxWait: 1000 / 8 maxWait: 1000 / 8
}) })
} }
@@ -63,14 +71,14 @@ class TopBar extends React.Component {
this.refs.search.childNodes[0].blur this.refs.search.childNodes[0].blur
dispatch(push('/searched')) dispatch(push('/searched'))
e.preventDefault() e.preventDefault()
this.debouncedUpdateKeyword('')
} }
handleKeyDown (e) { handleKeyDown (e) {
// reset states // Re-apply search field on ENTER key
this.setState({ if (e.keyCode === 13) {
isAlphabet: false, this.debouncedUpdateKeyword(e.target.value)
isIME: false }
})
// Clear search on ESC // Clear search on ESC
if (e.keyCode === 27) { if (e.keyCode === 27) {
@@ -88,52 +96,11 @@ class TopBar extends React.Component {
ee.emit('list:prior') ee.emit('list:prior')
e.preventDefault() e.preventDefault()
} }
// When the key is an alphabet, del, enter or ctr
if (e.keyCode <= 90 || e.keyCode >= 186 && e.keyCode <= 222) {
this.setState({
isAlphabet: true
})
// When the key is an IME input (Japanese, Chinese)
} else if (e.keyCode === 229) {
this.setState({
isIME: true
})
}
}
handleKeyUp (e) {
// reset states
this.setState({
isConfirmTranslation: false
})
// When the key is translation confirmation (Enter, Space)
if (this.state.isIME && (e.keyCode === 32 || e.keyCode === 13)) {
this.setState({
isConfirmTranslation: true
})
const keyword = this.refs.searchInput.value
this.updateKeyword(keyword)
}
} }
handleSearchChange (e) { handleSearchChange (e) {
if (this.state.isAlphabet || this.state.isConfirmTranslation) { const keyword = e.target.value
const keyword = this.refs.searchInput.value this.debouncedUpdateKeyword(keyword)
this.updateKeyword(keyword)
} else {
e.preventDefault()
}
}
updateKeyword (keyword) {
const { dispatch } = this.props
dispatch(push(`/searched/${encodeURIComponent(keyword)}`))
this.setState({
search: keyword
})
ee.emit('top:search', keyword)
} }
handleSearchFocus (e) { handleSearchFocus (e) {
@@ -141,6 +108,7 @@ class TopBar extends React.Component {
isSearching: true isSearching: true
}) })
} }
handleSearchBlur (e) { handleSearchBlur (e) {
e.stopPropagation() e.stopPropagation()
@@ -170,7 +138,7 @@ class TopBar extends React.Component {
} }
handleCodeInit () { handleCodeInit () {
ee.emit('top:search', this.refs.searchInput.value) ee.emit('top:search', this.refs.searchInput.value || '')
} }
render () { render () {
@@ -183,24 +151,23 @@ class TopBar extends React.Component {
<div styleName='control'> <div styleName='control'>
<div styleName='control-search'> <div styleName='control-search'>
<div styleName='control-search-input' <div styleName='control-search-input'
onFocus={(e) => this.handleSearchFocus(e)} onFocus={this.handleSearchFocus}
onBlur={(e) => this.handleSearchBlur(e)} onBlur={this.handleSearchBlur}
tabIndex='-1' tabIndex='-1'
ref='search' ref='search'
> >
<input <CInput
ref='searchInput' ref='searchInput'
value={this.state.search} value={this.state.search}
onChange={(e) => this.handleSearchChange(e)} onInputChange={this.handleSearchChange}
onKeyDown={(e) => this.handleKeyDown(e)} onKeyDown={this.handleKeyDown}
onKeyUp={(e) => this.handleKeyUp(e)}
placeholder={i18n.__('Search')} placeholder={i18n.__('Search')}
type='text' type='text'
className='searchInput' className='searchInput'
/> />
{this.state.search !== '' && {this.state.search !== '' &&
<button styleName='control-search-input-clear' <button styleName='control-search-input-clear'
onClick={(e) => this.handleSearchClearButton(e)} onClick={this.handleSearchClearButton}
> >
<i className='fa fa-fw fa-times' /> <i className='fa fa-fw fa-times' />
<span styleName='control-search-input-clear-tooltip'>{i18n.__('Clear Search')}</span> <span styleName='control-search-input-clear-tooltip'>{i18n.__('Clear Search')}</span>

View File

@@ -31,6 +31,9 @@ export const DEFAULT_CONFIG = {
toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M', toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace', deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace',
pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V', pasteSmartly: OSX ? 'Command + Shift + V' : 'Ctrl + Shift + V',
prettifyMarkdown: 'Shift + F',
insertDate: OSX ? 'Command + /' : 'Ctrl + /',
insertDateTime: OSX ? 'Command + Alt + /' : 'Ctrl + Shift + /',
toggleMenuBar: 'Alt' toggleMenuBar: 'Alt'
}, },
ui: { ui: {
@@ -48,6 +51,7 @@ export const DEFAULT_CONFIG = {
fontFamily: win ? 'Consolas' : 'Monaco', fontFamily: win ? 'Consolas' : 'Monaco',
indentType: 'space', indentType: 'space',
indentSize: '2', indentSize: '2',
lineWrapping: true,
enableRulers: false, enableRulers: false,
rulers: [80, 120], rulers: [80, 120],
displayLineNumbers: true, displayLineNumbers: true,
@@ -66,7 +70,14 @@ export const DEFAULT_CONFIG = {
spellcheck: false, spellcheck: false,
enableSmartPaste: false, enableSmartPaste: false,
enableMarkdownLint: false, enableMarkdownLint: false,
customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG customMarkdownLintConfig: DEFAULT_MARKDOWN_LINT_CONFIG,
prettierConfig: ` {
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true
}`,
deleteUnusedAttachments: true
}, },
preview: { preview: {
fontSize: '14', fontSize: '14',
@@ -84,8 +95,10 @@ export const DEFAULT_CONFIG = {
breaks: true, breaks: true,
smartArrows: false, smartArrows: false,
allowCustomCSS: false, allowCustomCSS: false,
customCSS: '',
customCSS: '/* Drop Your Custom CSS Code Here */',
sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE' sanitize: 'STRICT', // 'STRICT', 'ALLOW_STYLES', 'NONE'
mermaidHTMLLabel: false,
lineThroughCheckbox: true lineThroughCheckbox: true
}, },
blog: { blog: {
@@ -109,7 +122,6 @@ function validate (config) {
} }
function _save (config) { function _save (config) {
console.log(config)
window.localStorage.setItem('config', JSON.stringify(config)) window.localStorage.setItem('config', JSON.stringify(config))
} }
@@ -142,7 +154,7 @@ function get () {
const theme = consts.THEMES.find(theme => theme.name === config.editor.theme) const theme = consts.THEMES.find(theme => theme.name === config.editor.theme)
if (theme) { if (theme) {
editorTheme.setAttribute('href', `../${theme.path}`) editorTheme.setAttribute('href', theme.path)
} else { } else {
config.editor.theme = 'default' config.editor.theme = 'default'
} }
@@ -153,7 +165,13 @@ function get () {
function set (updates) { function set (updates) {
const currentConfig = get() const currentConfig = get()
const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, updates)
const arrangedUpdates = updates
if (updates.preview !== undefined && updates.preview.customCSS === '') {
arrangedUpdates.preview.customCSS = DEFAULT_CONFIG.preview.customCSS
}
const newConfig = Object.assign({}, DEFAULT_CONFIG, currentConfig, arrangedUpdates)
if (!validate(newConfig)) throw new Error('INVALID CONFIG') if (!validate(newConfig)) throw new Error('INVALID CONFIG')
_save(newConfig) _save(newConfig)
@@ -184,7 +202,7 @@ function set (updates) {
const newTheme = consts.THEMES.find(theme => theme.name === newConfig.editor.theme) const newTheme = consts.THEMES.find(theme => theme.name === newConfig.editor.theme)
if (newTheme) { if (newTheme) {
editorTheme.setAttribute('href', `../${newTheme.path}`) editorTheme.setAttribute('href', newTheme.path)
} }
ipcRenderer.send('config-renew', { ipcRenderer.send('config-renew', {

View File

@@ -8,6 +8,7 @@ const escapeStringRegexp = require('escape-string-regexp')
const sander = require('sander') const sander = require('sander')
const url = require('url') const url = require('url')
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import { isString } from 'lodash'
const STORAGE_FOLDER_PLACEHOLDER = ':storage' const STORAGE_FOLDER_PLACEHOLDER = ':storage'
const DESTINATION_FOLDER = 'attachments' const DESTINATION_FOLDER = 'attachments'
@@ -19,7 +20,7 @@ const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(
* @returns {Promise<Image>} Image element created * @returns {Promise<Image>} Image element created
*/ */
function getImage (file) { function getImage (file) {
if (_.isString(file)) { if (isString(file)) {
return new Promise(resolve => { return new Promise(resolve => {
const img = new Image() const img = new Image()
img.onload = () => resolve(img) img.onload = () => resolve(img)
@@ -241,6 +242,10 @@ function migrateAttachments (markdownContent, storagePath, noteKey) {
* @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths. * @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
*/ */
function fixLocalURLS (renderedHTML, storagePath) { function fixLocalURLS (renderedHTML, storagePath) {
const encodedWin32SeparatorRegex = /%5C/g
const storageRegex = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g')
const storageUrl = 'file:///' + path.join(storagePath, DESTINATION_FOLDER).replace(/\\/g, '/')
/* /*
A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`. A :storage reference is like `:storage/3b6f8bd6-4edd-4b15-96e0-eadc4475b564/f939b2c3.jpg`.
@@ -250,8 +255,7 @@ function fixLocalURLS (renderedHTML, storagePath) {
- `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows. - `(?:\\\/|%5C)` match the path seperator. `\\\/` for posix systems and `%5C` for windows.
*/ */
return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\\/|%5C)[-.\\w]+)+', 'g'), function (match) { return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(?:(?:\\\/|%5C)[-.\\w]+)+', 'g'), function (match) {
var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g') return match.replace(encodedWin32SeparatorRegex, '/').replace(storageRegex, storageUrl)
return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
}) })
} }
@@ -617,11 +621,79 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey
} }
}) })
}) })
} else {
console.info('Attachment folder ("' + attachmentFolder + '") did not exist..')
} }
} }
/**
* @description Get all existing attachments related to a specific note
including their status (in use or not) and their path. Return null if there're no attachment related to note or specified parametters are invalid
* @param markdownContent markdownContent of the current note
* @param storageKey StorageKey of the current note
* @param noteKey NoteKey of the currentNote
* @return {Promise<Array<{path: String, isInUse: bool}>>} Promise returning the
list of attachments with their properties */
function getAttachmentsPathAndStatus (markdownContent, storageKey, noteKey) {
if (storageKey == null || noteKey == null || markdownContent == null) {
return null
}
const targetStorage = findStorage.findStorage(storageKey)
const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent)
const attachmentsInNoteOnlyFileNames = []
if (attachmentsInNote) {
for (let i = 0; i < attachmentsInNote.length; i++) {
attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), ''))
}
}
if (fs.existsSync(attachmentFolder)) {
return new Promise((resolve, reject) => {
fs.readdir(attachmentFolder, (err, files) => {
if (err) {
console.error('Error reading directory "' + attachmentFolder + '". Error:')
console.error(err)
reject(err)
return
}
const attachments = []
for (const file of files) {
const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file)
if (!attachmentsInNoteOnlyFileNames.includes(file)) {
attachments.push({ path: absolutePathOfFile, isInUse: false })
} else {
attachments.push({ path: absolutePathOfFile, isInUse: true })
}
}
resolve(attachments)
})
})
} else {
return null
}
}
/**
* @description Remove all specified attachment paths
* @param attachments attachment paths
* @return {Promise} Promise after all attachments are removed */
function removeAttachmentsByPaths (attachments) {
const promises = []
for (const attachment of attachments) {
const promise = new Promise((resolve, reject) => {
fs.unlink(attachment, (err) => {
if (err) {
console.error('Could not delete "%s"', attachment)
console.error(err)
reject(err)
return
}
resolve()
})
})
promises.push(promise)
}
return Promise.all(promises)
}
/** /**
* Clones the attachments of a given note. * Clones the attachments of a given note.
* Copies the attachments to their new destination and updates the content of the new note so that the attachment-links again point to the correct destination. * Copies the attachments to their new destination and updates the content of the new note so that the attachment-links again point to the correct destination.
@@ -724,8 +796,10 @@ module.exports = {
getAbsolutePathsOfAttachmentsInContent, getAbsolutePathsOfAttachmentsInContent,
importAttachments, importAttachments,
removeStorageAndNoteReferences, removeStorageAndNoteReferences,
removeAttachmentsByPaths,
deleteAttachmentFolder, deleteAttachmentFolder,
deleteAttachmentsNotPresentInNote, deleteAttachmentsNotPresentInNote,
getAttachmentsPathAndStatus,
moveAttachments, moveAttachments,
cloneAttachments, cloneAttachments,
isAttachmentLink, isAttachmentLink,

View File

@@ -0,0 +1,79 @@
const http = require('http')
const https = require('https')
const { createTurndownService } = require('../../../lib/turndown')
const createNote = require('./createNote')
import { push } from 'connected-react-router'
import ee from 'browser/main/lib/eventEmitter'
function validateUrl (str) {
if (/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(str)) {
return true
} else {
return false
}
}
function createNoteFromUrl (url, storage, folder, dispatch = null, location = null) {
return new Promise((resolve, reject) => {
const td = createTurndownService()
if (!validateUrl(url)) {
reject({result: false, error: 'Please check your URL is in correct format. (Example, https://www.google.com)'})
}
const request = url.startsWith('https') ? https : http
const req = request.request(url, (res) => {
let data = ''
res.on('data', (chunk) => {
data += chunk
})
res.on('end', () => {
const markdownHTML = td.turndown(data)
if (dispatch !== null) {
createNote(storage, {
type: 'MARKDOWN_NOTE',
folder: folder,
title: '',
content: markdownHTML
})
.then((note) => {
const noteHash = note.key
dispatch({
type: 'UPDATE_NOTE',
note: note
})
dispatch(push({
pathname: location.pathname,
query: {key: noteHash}
}))
ee.emit('list:jump', noteHash)
ee.emit('detail:focus')
resolve({result: true, error: null})
})
} else {
createNote(storage, {
type: 'MARKDOWN_NOTE',
folder: folder,
title: '',
content: markdownHTML
}).then((note) => {
resolve({result: true, note, error: null})
})
}
})
})
req.on('error', (e) => {
console.error('error in parsing URL', e)
reject({result: false, error: e})
})
req.end()
})
}
module.exports = createNoteFromUrl

View File

@@ -3,7 +3,6 @@ const path = require('path')
const resolveStorageData = require('./resolveStorageData') const resolveStorageData = require('./resolveStorageData')
const resolveStorageNotes = require('./resolveStorageNotes') const resolveStorageNotes = require('./resolveStorageNotes')
const CSON = require('@rokt33r/season') const CSON = require('@rokt33r/season')
const sander = require('sander')
const { findStorage } = require('browser/lib/findStorage') const { findStorage } = require('browser/lib/findStorage')
const deleteSingleNote = require('./deleteNote') const deleteSingleNote = require('./deleteNote')

View File

@@ -43,7 +43,7 @@ function exportNote (nodeKey, storageKey, noteContent, targetPath, outputFormatt
) )
if (outputFormatter) { if (outputFormatter) {
exportedData = outputFormatter(exportedData, exportTasks, path.dirname(targetPath)) exportedData = outputFormatter(exportedData, exportTasks, targetPath)
} else { } else {
exportedData = Promise.resolve(exportedData) exportedData = Promise.resolve(exportedData)
} }

View File

@@ -11,6 +11,7 @@ const dataApi = {
exportFolder: require('./exportFolder'), exportFolder: require('./exportFolder'),
exportStorage: require('./exportStorage'), exportStorage: require('./exportStorage'),
createNote: require('./createNote'), createNote: require('./createNote'),
createNoteFromUrl: require('./createNoteFromUrl'),
updateNote: require('./updateNote'), updateNote: require('./updateNote'),
deleteNote: require('./deleteNote'), deleteNote: require('./deleteNote'),
moveNote: require('./moveNote'), moveNote: require('./moveNote'),

View File

@@ -1,7 +1,6 @@
const resolveStorageData = require('./resolveStorageData') const resolveStorageData = require('./resolveStorageData')
const _ = require('lodash') const _ = require('lodash')
const path = require('path') const path = require('path')
const fs = require('fs')
const CSON = require('@rokt33r/season') const CSON = require('@rokt33r/season')
const keygen = require('browser/lib/keygen') const keygen = require('browser/lib/keygen')
const sander = require('sander') const sander = require('sander')

View File

@@ -0,0 +1,118 @@
import PropTypes from 'prop-types'
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './CreateMarkdownFromURLModal.styl'
import dataApi from 'browser/main/lib/dataApi'
import ModalEscButton from 'browser/components/ModalEscButton'
import i18n from 'browser/lib/i18n'
class CreateMarkdownFromURLModal extends React.Component {
constructor (props) {
super(props)
this.state = {
name: '',
showerror: false,
errormessage: ''
}
}
componentDidMount () {
this.refs.name.focus()
this.refs.name.select()
}
handleCloseButtonClick (e) {
this.props.close()
}
handleChange (e) {
this.setState({
name: this.refs.name.value
})
}
handleKeyDown (e) {
if (e.keyCode === 27) {
this.props.close()
}
}
handleInputKeyDown (e) {
switch (e.keyCode) {
case 13:
this.confirm()
}
}
handleConfirmButtonClick (e) {
this.confirm()
}
showError (message) {
this.setState({
showerror: true,
errormessage: message
})
}
hideError () {
this.setState({
showerror: false,
errormessage: ''
})
}
confirm () {
this.hideError()
const { storage, folder, dispatch, location } = this.props
dataApi.createNoteFromUrl(this.state.name, storage, folder, dispatch, location).then((result) => {
this.props.close()
}).catch((result) => {
this.showError(result.error)
})
}
render () {
return (
<div styleName='root'
tabIndex='-1'
onKeyDown={(e) => this.handleKeyDown(e)}
>
<div styleName='header'>
<div styleName='title'>{i18n.__('Import Markdown From URL')}</div>
</div>
<ModalEscButton handleEscButtonClick={(e) => this.handleCloseButtonClick(e)} />
<div styleName='control'>
<div styleName='control-folder'>
<div styleName='control-folder-label'>{i18n.__('Insert URL Here')}</div>
<input styleName='control-folder-input'
ref='name'
value={this.state.name}
onChange={(e) => this.handleChange(e)}
onKeyDown={(e) => this.handleInputKeyDown(e)}
/>
</div>
<button styleName='control-confirmButton'
onClick={(e) => this.handleConfirmButtonClick(e)}
>
{i18n.__('Import')}
</button>
<div className='error' styleName='error'>{this.state.errormessage}</div>
</div>
</div>
)
}
}
CreateMarkdownFromURLModal.propTypes = {
storage: PropTypes.string,
folder: PropTypes.string,
dispatch: PropTypes.func,
location: PropTypes.shape({
pathname: PropTypes.string
})
}
export default CSSModules(CreateMarkdownFromURLModal, styles)

View File

@@ -0,0 +1,160 @@
.root
modal()
width 500px
height 270px
overflow hidden
position relative
.header
height 80px
margin-bottom 10px
margin-top 20px
font-size 18px
line-height 50px
background-color $ui-backgroundColor
color $ui-text-color
.title
font-size 36px
font-weight 600
.control-folder-label
text-align left
font-size 14px
color $ui-text-color
.control-folder-input
display block
height 40px
width 490px
padding 0 5px
margin 10px 0
border 1px solid $ui-input--create-folder-modal
border-radius 2px
background-color transparent
outline none
vertical-align middle
font-size 16px
&:disabled
background-color $ui-input--disabled-backgroundColor
&:focus, &:active
border-color $ui-active-color
.control-confirmButton
display block
height 35px
width 140px
border none
border-radius 2px
padding 0 25px
margin 20px auto
font-size 14px
colorPrimaryButton()
body[data-theme="dark"]
.root
modalDark()
width 500px
height 270px
overflow hidden
position relative
.header
background-color transparent
border-color $ui-dark-borderColor
color $ui-dark-text-color
.control-folder-label
color $ui-dark-text-color
.control-folder-input
border 1px solid $ui-input--create-folder-modal
color white
.description
color $ui-inactive-text-color
.control-confirmButton
colorDarkPrimaryButton()
body[data-theme="solarized-dark"]
.root
modalSolarizedDark()
width 500px
height 270px
overflow hidden
position relative
.header
background-color transparent
border-color $ui-dark-borderColor
color $ui-solarized-dark-text-color
.control-folder-label
color $ui-solarized-dark-text-color
.control-folder-input
border 1px solid $ui-input--create-folder-modal
color white
.description
color $ui-inactive-text-color
.control-confirmButton
colorSolarizedDarkPrimaryButton()
.error
text-align center
color #F44336
body[data-theme="monokai"]
.root
modalMonokai()
width 500px
height 270px
overflow hidden
position relative
.header
background-color transparent
border-color $ui-dark-borderColor
color $ui-monokai-text-color
.control-folder-label
color $ui-monokai-text-color
.control-folder-input
border 1px solid $ui-input--create-folder-modal
color white
.description
color $ui-inactive-text-color
.control-confirmButton
colorMonokaiPrimaryButton()
body[data-theme="dracula"]
.root
modalDracula()
width 500px
height 270px
overflow hidden
position relative
.header
background-color transparent
border-color $ui-dracula-borderColor
color $ui-dracula-text-color
.control-folder-label
color $ui-dracula-text-color
.control-folder-input
border 1px solid $ui-input--create-folder-modal
color white
.description
color $ui-inactive-text-color
.control-confirmButton
colorDraculaPrimaryButton()

View File

@@ -3,6 +3,8 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './NewNoteModal.styl' import styles from './NewNoteModal.styl'
import ModalEscButton from 'browser/components/ModalEscButton' import ModalEscButton from 'browser/components/ModalEscButton'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import { openModal } from 'browser/main/lib/modal'
import CreateMarkdownFromURLModal from '../modals/CreateMarkdownFromURLModal'
import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote' import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote'
import queryString from 'query-string' import queryString from 'query-string'
@@ -21,6 +23,18 @@ class NewNoteModal extends React.Component {
this.props.close() this.props.close()
} }
handleCreateMarkdownFromUrlClick (e) {
this.props.close()
const { storage, folder, dispatch, location } = this.props
openModal(CreateMarkdownFromURLModal, {
storage: storage,
folder: folder,
dispatch,
location
})
}
handleMarkdownNoteButtonClick (e) { handleMarkdownNoteButtonClick (e) {
const { storage, folder, dispatch, location, config } = this.props const { storage, folder, dispatch, location, config } = this.props
const params = location.search !== '' && queryString.parse(location.search) const params = location.search !== '' && queryString.parse(location.search)
@@ -115,10 +129,8 @@ class NewNoteModal extends React.Component {
</button> </button>
</div> </div>
<div styleName='description'> <div styleName='description'><i className='fa fa-arrows-h' />{i18n.__('Tab to switch format')}</div>
<i className='fa fa-arrows-h' />{i18n.__('Tab to switch format')} <div styleName='from-url' onClick={(e) => this.handleCreateMarkdownFromUrlClick(e)}>Or, create a new markdown note from a URL</div>
</div>
</div> </div>
) )
} }

View File

@@ -19,6 +19,7 @@
.control .control
padding 25px 0px padding 25px 0px
text-align center text-align center
display: flex
.control-button .control-button
width 240px width 240px
@@ -47,6 +48,12 @@
text-align center text-align center
margin-bottom 25px margin-bottom 25px
.from-url
color $ui-inactive-text-color
text-align center
margin-bottom 25px
cursor pointer
body[data-theme="dark"] body[data-theme="dark"]
.root .root
modalDark() modalDark()
@@ -61,7 +68,7 @@ body[data-theme="dark"]
&:focus &:focus
colorDarkPrimaryButton() colorDarkPrimaryButton()
.description .description, .from-url
color $ui-inactive-text-color color $ui-inactive-text-color
body[data-theme="solarized-dark"] body[data-theme="solarized-dark"]
@@ -78,7 +85,7 @@ body[data-theme="solarized-dark"]
&:focus &:focus
colorDarkPrimaryButton() colorDarkPrimaryButton()
.description .description, .from-url
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
body[data-theme="monokai"] body[data-theme="monokai"]
@@ -95,7 +102,7 @@ body[data-theme="monokai"]
&:focus &:focus
colorDarkPrimaryButton() colorDarkPrimaryButton()
.description .description, .from-url
color $ui-monokai-text-color color $ui-monokai-text-color
body[data-theme="dracula"] body[data-theme="dracula"]

View File

@@ -225,7 +225,7 @@ class FolderItem extends React.Component {
<div styleName='folderItem-left' <div styleName='folderItem-left'
style={{borderColor: folder.color}} style={{borderColor: folder.color}}
> >
<span styleName='folderItem-left-name'>{folder.name}</span> <span>{folder.name}</span>
<span styleName='folderItem-left-key'>({folder.key})</span> <span styleName='folderItem-left-key'>({folder.key})</span>
</div> </div>
<div styleName='folderItem-right'> <div styleName='folderItem-right'>
@@ -288,10 +288,10 @@ class Handle extends React.Component {
class SortableFolderItemComponent extends React.Component { class SortableFolderItemComponent extends React.Component {
render () { render () {
const StyledHandle = CSSModules(Handle, this.props.styles) const StyledHandle = CSSModules(Handle, styles)
const DragHandle = SortableHandle(StyledHandle) const DragHandle = SortableHandle(StyledHandle)
const StyledFolderItem = CSSModules(FolderItem, this.props.styles) const StyledFolderItem = CSSModules(FolderItem, styles)
return ( return (
<div> <div>

View File

@@ -22,7 +22,7 @@ class FolderList extends React.Component {
}) })
return ( return (
<div styleName='folderList'> <div>
{folderList.length > 0 {folderList.length > 0
? folderList ? folderList
: <div styleName='folderList-empty'>{i18n.__('No Folders')}</div> : <div styleName='folderList-empty'>{i18n.__('No Folders')}</div>

View File

@@ -76,13 +76,16 @@ class HotkeyTab extends React.Component {
handleHotkeyChange (e) { handleHotkeyChange (e) {
const { config } = this.state const { config } = this.state
config.hotkey = { config.hotkey = Object.assign({}, config.hotkey, {
toggleMain: this.refs.toggleMain.value, toggleMain: this.refs.toggleMain.value,
toggleMode: this.refs.toggleMode.value, toggleMode: this.refs.toggleMode.value,
deleteNote: this.refs.deleteNote.value, deleteNote: this.refs.deleteNote.value,
pasteSmartly: this.refs.pasteSmartly.value, pasteSmartly: this.refs.pasteSmartly.value,
toggleMenuBar: this.refs.toggleMenuBar.value prettifyMarkdown: this.refs.prettifyMarkdown.value,
} toggleMenuBar: this.refs.toggleMenuBar.value,
insertDate: this.refs.insertDate.value,
insertDateTime: this.refs.insertDateTime.value
})
this.setState({ this.setState({
config config
}) })
@@ -173,6 +176,38 @@ class HotkeyTab extends React.Component {
/> />
</div> </div>
</div> </div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Prettify Markdown')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}
ref='prettifyMarkdown'
value={config.hotkey.prettifyMarkdown}
type='text' />
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Insert Current Date')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
ref='insertDate'
value={config.hotkey.insertDate}
type='text'
disabled='true'
/>
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Insert Current Date and Time')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
ref='insertDateTime'
value={config.hotkey.insertDateTime}
type='text'
disabled='true'
/>
</div>
</div>
<div styleName='group-control'> <div styleName='group-control'>
<button styleName='group-control-leftButton' <button styleName='group-control-leftButton'
onClick={(e) => this.handleHintToggleButtonClick(e)} onClick={(e) => this.handleHintToggleButtonClick(e)}

View File

@@ -102,3 +102,11 @@ body[data-theme="solarized-dark"]
border-color $ui-solarized-dark-button-backgroundColor border-color $ui-solarized-dark-button-backgroundColor
background-color $ui-solarized-dark-button-backgroundColor background-color $ui-solarized-dark-button-backgroundColor
color $ui-solarized-dark-text-color color $ui-solarized-dark-text-color
body[data-theme="dracula"]
.header
border-color $ui-dracula-borderColor
.header-control-button
colorDraculaDefaultButton()
border-color $ui-dracula-borderColor

View File

@@ -3,8 +3,11 @@ import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import styles from './StoragesTab.styl' import styles from './StoragesTab.styl'
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
import StorageItem from './StorageItem' import StorageItem from './StorageItem'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import { humanFileSize } from 'browser/lib/utils'
import fs from 'fs'
const electron = require('electron') const electron = require('electron')
const { shell, remote } = electron const { shell, remote } = electron
@@ -35,8 +38,29 @@ class StoragesTab extends React.Component {
name: 'Unnamed', name: 'Unnamed',
type: 'FILESYSTEM', type: 'FILESYSTEM',
path: '' path: ''
},
attachments: []
} }
this.loadAttachmentStorage()
} }
loadAttachmentStorage () {
const promises = []
this.props.data.noteMap.map(note => {
const promise = attachmentManagement.getAttachmentsPathAndStatus(
note.content,
note.storage,
note.key
)
if (promise) promises.push(promise)
})
Promise.all(promises)
.then(data => {
const result = data.reduce((acc, curr) => acc.concat(curr), [])
this.setState({attachments: result})
})
.catch(console.error)
} }
handleAddStorageButton (e) { handleAddStorageButton (e) {
@@ -57,8 +81,39 @@ class StoragesTab extends React.Component {
e.preventDefault() e.preventDefault()
} }
handleRemoveUnusedAttachments (attachments) {
attachmentManagement.removeAttachmentsByPaths(attachments)
.then(() => this.loadAttachmentStorage())
.catch(console.error)
}
renderList () { renderList () {
const { data, boundingBox } = this.props const { data, boundingBox } = this.props
const { attachments } = this.state
const unusedAttachments = attachments.filter(attachment => !attachment.isInUse)
const inUseAttachments = attachments.filter(attachment => attachment.isInUse)
const totalUnusedAttachments = unusedAttachments.length
const totalInuseAttachments = inUseAttachments.length
const totalAttachments = totalUnusedAttachments + totalInuseAttachments
const totalUnusedAttachmentsSize = unusedAttachments
.reduce((acc, curr) => {
const stats = fs.statSync(curr.path)
const fileSizeInBytes = stats.size
return acc + fileSizeInBytes
}, 0)
const totalInuseAttachmentsSize = inUseAttachments
.reduce((acc, curr) => {
const stats = fs.statSync(curr.path)
const fileSizeInBytes = stats.size
return acc + fileSizeInBytes
}, 0)
const totalAttachmentsSize = totalUnusedAttachmentsSize + totalInuseAttachmentsSize
const unusedAttachmentPaths = unusedAttachments
.reduce((acc, curr) => acc.concat(curr.path), [])
if (!boundingBox) { return null } if (!boundingBox) { return null }
const storageList = data.storageMap.map((storage) => { const storageList = data.storageMap.map((storage) => {
@@ -82,6 +137,20 @@ class StoragesTab extends React.Component {
<i className='fa fa-plus' /> {i18n.__('Add Storage Location')} <i className='fa fa-plus' /> {i18n.__('Add Storage Location')}
</button> </button>
</div> </div>
<div styleName='header'>{i18n.__('Attachment storage')}</div>
<p styleName='list-attachment-label'>
Unused attachments size: {humanFileSize(totalUnusedAttachmentsSize)} ({totalUnusedAttachments} items)
</p>
<p styleName='list-attachment-label'>
In use attachments size: {humanFileSize(totalInuseAttachmentsSize)} ({totalInuseAttachments} items)
</p>
<p styleName='list-attachment-label'>
Total attachments size: {humanFileSize(totalAttachmentsSize)} ({totalAttachments} items)
</p>
<button styleName='list-attachement-clear-button'
onClick={() => this.handleRemoveUnusedAttachments(unusedAttachmentPaths)}>
{i18n.__('Clear unused attachments')}
</button>
</div> </div>
) )
} }

View File

@@ -33,6 +33,17 @@
colorDefaultButton() colorDefaultButton()
font-size $tab--button-font-size font-size $tab--button-font-size
border-radius 2px border-radius 2px
.list-attachment-label
margin-bottom 10px
color $ui-text-color
.list-attachement-clear-button
height 30px
border none
border-top-right-radius 2px
border-bottom-right-radius 2px
colorPrimaryButton()
vertical-align middle
padding 0 20px
.addStorage .addStorage
margin-bottom 15px margin-bottom 15px
@@ -154,8 +165,8 @@ body[data-theme="dark"]
.addStorage-body-control-cancelButton .addStorage-body-control-cancelButton
colorDarkDefaultButton() colorDarkDefaultButton()
border-color $ui-dark-borderColor border-color $ui-dark-borderColor
.list-attachement-clear-button
colorDarkPrimaryButton()
body[data-theme="solarized-dark"] body[data-theme="solarized-dark"]
.root .root
@@ -194,6 +205,8 @@ body[data-theme="solarized-dark"]
.addStorage-body-control-cancelButton .addStorage-body-control-cancelButton
colorDarkDefaultButton() colorDarkDefaultButton()
border-color $ui-solarized-dark-borderColor border-color $ui-solarized-dark-borderColor
.list-attachement-clear-button
colorSolarizedDarkPrimaryButton()
body[data-theme="monokai"] body[data-theme="monokai"]
.root .root
@@ -232,6 +245,8 @@ body[data-theme="monokai"]
.addStorage-body-control-cancelButton .addStorage-body-control-cancelButton
colorDarkDefaultButton() colorDarkDefaultButton()
border-color $ui-monokai-borderColor border-color $ui-monokai-borderColor
.list-attachement-clear-button
colorMonokaiPrimaryButton()
body[data-theme="dracula"] body[data-theme="dracula"]
.root .root
@@ -270,3 +285,5 @@ body[data-theme="dracula"]
.addStorage-body-control-cancelButton .addStorage-body-control-cancelButton
colorDarkDefaultButton() colorDarkDefaultButton()
border-color $ui-dracula-borderColor border-color $ui-dracula-borderColor
.list-attachement-clear-button
colorDraculaPrimaryButton()

View File

@@ -31,8 +31,12 @@ class UiTab extends React.Component {
CodeMirror.autoLoadMode(this.codeMirrorInstance.getCodeMirror(), 'javascript') CodeMirror.autoLoadMode(this.codeMirrorInstance.getCodeMirror(), 'javascript')
CodeMirror.autoLoadMode(this.customCSSCM.getCodeMirror(), 'css') CodeMirror.autoLoadMode(this.customCSSCM.getCodeMirror(), 'css')
CodeMirror.autoLoadMode(this.customMarkdownLintConfigCM.getCodeMirror(), 'javascript') CodeMirror.autoLoadMode(this.customMarkdownLintConfigCM.getCodeMirror(), 'javascript')
CodeMirror.autoLoadMode(this.prettierConfigCM.getCodeMirror(), 'javascript')
// Set CM editor Sizes
this.customCSSCM.getCodeMirror().setSize('400px', '400px') this.customCSSCM.getCodeMirror().setSize('400px', '400px')
this.prettierConfigCM.getCodeMirror().setSize('400px', '400px')
this.customMarkdownLintConfigCM.getCodeMirror().setSize('400px', '200px') this.customMarkdownLintConfigCM.getCodeMirror().setSize('400px', '200px')
this.handleSettingDone = () => { this.handleSettingDone = () => {
this.setState({UiAlert: { this.setState({UiAlert: {
type: 'success', type: 'success',
@@ -91,6 +95,7 @@ class UiTab extends React.Component {
enableRulers: this.refs.enableEditorRulers.value === 'true', enableRulers: this.refs.enableEditorRulers.value === 'true',
rulers: this.refs.editorRulers.value.replace(/[^0-9,]/g, '').split(','), rulers: this.refs.editorRulers.value.replace(/[^0-9,]/g, '').split(','),
displayLineNumbers: this.refs.editorDisplayLineNumbers.checked, displayLineNumbers: this.refs.editorDisplayLineNumbers.checked,
lineWrapping: this.refs.editorLineWrapping.checked,
switchPreview: this.refs.editorSwitchPreview.value, switchPreview: this.refs.editorSwitchPreview.value,
keyMap: this.refs.editorKeyMap.value, keyMap: this.refs.editorKeyMap.value,
snippetDefaultLanguage: this.refs.editorSnippetDefaultLanguage.value, snippetDefaultLanguage: this.refs.editorSnippetDefaultLanguage.value,
@@ -105,7 +110,9 @@ class UiTab extends React.Component {
spellcheck: this.refs.spellcheck.checked, spellcheck: this.refs.spellcheck.checked,
enableSmartPaste: this.refs.enableSmartPaste.checked, enableSmartPaste: this.refs.enableSmartPaste.checked,
enableMarkdownLint: this.refs.enableMarkdownLint.checked, enableMarkdownLint: this.refs.enableMarkdownLint.checked,
customMarkdownLintConfig: this.customMarkdownLintConfigCM.getCodeMirror().getValue() customMarkdownLintConfig: this.customMarkdownLintConfigCM.getCodeMirror().getValue(),
prettierConfig: this.prettierConfigCM.getCodeMirror().getValue(),
deleteUnusedAttachments: this.refs.deleteUnusedAttachments.checked
}, },
preview: { preview: {
fontSize: this.refs.previewFontSize.value, fontSize: this.refs.previewFontSize.value,
@@ -123,6 +130,7 @@ class UiTab extends React.Component {
breaks: this.refs.previewBreaks.checked, breaks: this.refs.previewBreaks.checked,
smartArrows: this.refs.previewSmartArrows.checked, smartArrows: this.refs.previewSmartArrows.checked,
sanitize: this.refs.previewSanitize.value, sanitize: this.refs.previewSanitize.value,
mermaidHTMLLabel: this.refs.previewMermaidHTMLLabel.checked,
allowCustomCSS: this.refs.previewAllowCustomCSS.checked, allowCustomCSS: this.refs.previewAllowCustomCSS.checked,
lineThroughCheckbox: this.refs.lineThroughCheckbox.checked, lineThroughCheckbox: this.refs.lineThroughCheckbox.checked,
customCSS: this.customCSSCM.getCodeMirror().getValue() customCSS: this.customCSSCM.getCodeMirror().getValue()
@@ -135,7 +143,7 @@ class UiTab extends React.Component {
const theme = consts.THEMES.find(theme => theme.name === newCodemirrorTheme) const theme = consts.THEMES.find(theme => theme.name === newCodemirrorTheme)
if (theme) { if (theme) {
checkHighLight.setAttribute('href', `../${theme.path}`) checkHighLight.setAttribute('href', theme.path)
} }
} }
@@ -545,6 +553,17 @@ class UiTab extends React.Component {
</label> </label>
</div> </div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.editor.lineWrapping}
ref='editorLineWrapping'
type='checkbox'
/>&nbsp;
{i18n.__('Wrap line in Snippet Note')}
</label>
</div>
<div styleName='group-checkBoxSection'> <div styleName='group-checkBoxSection'>
<label> <label>
<input onChange={(e) => this.handleUIChange(e)} <input onChange={(e) => this.handleUIChange(e)}
@@ -599,6 +618,16 @@ class UiTab extends React.Component {
{i18n.__('Enable spellcheck - Experimental feature!! :)')} {i18n.__('Enable spellcheck - Experimental feature!! :)')}
</label> </label>
</div> </div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.editor.deleteUnusedAttachments}
ref='deleteUnusedAttachments'
type='checkbox'
/>&nbsp;
{i18n.__('Delete attachments, that are not referenced in the text anymore')}
</label>
</div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
@@ -800,6 +829,16 @@ class UiTab extends React.Component {
</select> </select>
</div> </div>
</div> </div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.preview.mermaidHTMLLabel}
ref='previewMermaidHTMLLabel'
type='checkbox'
/>&nbsp;
{i18n.__('Enable HTML label in mermaid flowcharts')}
</label>
</div>
<div styleName='group-section'> <div styleName='group-section'>
<div styleName='group-section-label'> <div styleName='group-section-label'>
{i18n.__('LaTeX Inline Open Delimiter')} {i18n.__('LaTeX Inline Open Delimiter')}
@@ -883,7 +922,6 @@ class UiTab extends React.Component {
onChange={e => this.handleUIChange(e)} onChange={e => this.handleUIChange(e)}
ref={e => (this.customCSSCM = e)} ref={e => (this.customCSSCM = e)}
value={config.preview.customCSS} value={config.preview.customCSS}
defaultValue={'/* Drop Your Custom CSS Code Here */\n'}
options={{ options={{
lineNumbers: true, lineNumbers: true,
mode: 'css', mode: 'css',
@@ -892,7 +930,27 @@ class UiTab extends React.Component {
</div> </div>
</div> </div>
</div> </div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Prettier Config')}
</div>
<div styleName='group-section-control'>
<div style={{fontFamily}}>
<ReactCodeMirror
width='400px'
height='400px'
onChange={e => this.handleUIChange(e)}
ref={e => (this.prettierConfigCM = e)}
value={config.editor.prettierConfig}
options={{
lineNumbers: true,
mode: 'application/json',
lint: true,
theme: codemirrorTheme
}} />
</div>
</div>
</div>
<div styleName='group-control'> <div styleName='group-control'>
<button styleName='group-control-rightButton' <button styleName='group-control-rightButton'
onClick={(e) => this.handleSaveUIClick(e)}>{i18n.__('Save')} onClick={(e) => this.handleSaveUIClick(e)}>{i18n.__('Save')}

View File

@@ -147,7 +147,7 @@ class Preferences extends React.Component {
key={tab.target} key={tab.target}
onClick={(e) => this.handleNavButtonClick(tab.target)(e)} onClick={(e) => this.handleNavButtonClick(tab.target)(e)}
> >
<span styleName='nav-button-label'> <span>
{tab.label} {tab.label}
</span> </span>
{isUiHotkeyTab ? this.haveToSaveNotif(tab[tab.label].type, tab[tab.label].message) : null} {isUiHotkeyTab ? this.haveToSaveNotif(tab[tab.label].type, tab[tab.label].message) : null}

View File

@@ -476,7 +476,8 @@ const reducer = combineReducers({
router: connectRouter(history) router: connectRouter(history)
}) })
const store = createStore(reducer, undefined, compose( const store = createStore(reducer, undefined, process.env.NODE_ENV === 'development'
applyMiddleware(routerMiddleware(history)), DevTools.instrument())) ? compose(applyMiddleware(routerMiddleware(history)), DevTools.instrument())
: applyMiddleware(routerMiddleware(history)))
export { store, history } export { store, history }

View File

@@ -410,6 +410,15 @@ $ui-dracula-button--active-color = #f8f8f2
$ui-dracula-button--active-backgroundColor = #bd93f9 $ui-dracula-button--active-backgroundColor = #bd93f9
$ui-dracula-button--hover-backgroundColor = lighten($ui-dracula-backgroundColor, 10%) $ui-dracula-button--hover-backgroundColor = lighten($ui-dracula-backgroundColor, 10%)
$ui-dracula-button--focus-borderColor = lighten(#44475a, 25%) $ui-dracula-button--focus-borderColor = lighten(#44475a, 25%)
colorDraculaDefaultButton()
border-color $ui-dracula-borderColor
color $ui-dracula-text-color
background-color $ui-dracula-button-backgroundColor
&:hover
background-color $ui-dracula-button--hover-backgroundColor
&:active
&:active:hover
background-color $ui-dracula-button--active-backgroundColor
modalDracula() modalDracula()
position relative position relative

View File

@@ -3,6 +3,7 @@ const BrowserWindow = electron.BrowserWindow
const shell = electron.shell const shell = electron.shell
const ipc = electron.ipcMain const ipc = electron.ipcMain
const mainWindow = require('./main-window') const mainWindow = require('./main-window')
const os = require('os')
const macOS = process.platform === 'darwin' const macOS = process.platform === 'darwin'
// const WIN = process.platform === 'win32' // const WIN = process.platform === 'win32'
@@ -411,6 +412,28 @@ const help = {
click () { shell.openExternal('https://github.com/TobseF/boostnote-markdown-cheatsheet/blob/master/BOOSTNOTE_MARKDOWN_CHEAT_SHEET.md') } click () { shell.openExternal('https://github.com/TobseF/boostnote-markdown-cheatsheet/blob/master/BOOSTNOTE_MARKDOWN_CHEAT_SHEET.md') }
} }
] ]
},
{
type: 'separator'
},
{
label: 'About',
click () {
const version = electron.app.getVersion()
const electronVersion = process.versions.electron
const chromeVersion = process.versions.chrome
const nodeVersion = process.versions.node
const v8Version = process.versions.v8
const OSInfo = `${os.type()} ${os.arch()} ${os.release()}`
const detail = `Version: ${version}\nElectron: ${electronVersion}\nChrome: ${chromeVersion}\nNode.js: ${nodeVersion}\nV8: ${v8Version}\nOS: ${OSInfo}`
electron.dialog.showMessageBox(BrowserWindow.getFocusedWindow(),
{
title: 'BoostNote',
message: 'BoostNote',
type: 'info',
detail: `\n${detail}`
})
}
} }
] ]
} }

View File

@@ -54,7 +54,7 @@ const mainWindow = new BrowserWindow({
}, },
icon: path.resolve(__dirname, '../resources/app.png') icon: path.resolve(__dirname, '../resources/app.png')
}) })
const url = path.resolve(__dirname, process.env.NODE_ENV === 'production' ? './main.production.html' : './main.development.html') const url = path.resolve(__dirname, process.env.NODE_ENV === 'development' ? './main.development.html' : './main.production.html')
mainWindow.loadURL('file://' + url) mainWindow.loadURL('file://' + url)
mainWindow.setMenuBarVisibility(false) mainWindow.setMenuBarVisibility(false)

View File

@@ -73,6 +73,11 @@
mix-blend-mode: difference; mix-blend-mode: difference;
} }
.CodeMirror-scroll {
margin-bottom: 0;
padding-bottom: 0;
}
.CodeMirror-lint-tooltip { .CodeMirror-lint-tooltip {
z-index: 1003; z-index: 1003;
} }
@@ -110,7 +115,6 @@
<script src="../extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js"></script> <script src="../extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js"></script>
<script src="../extra_scripts/codemirror/mode/bfm/bfm.js"></script> <script src="../extra_scripts/codemirror/mode/bfm/bfm.js"></script>
<script src="../extra_scripts/codemirror/addon/hyperlink/hyperlink.js"></script> <script src="../extra_scripts/codemirror/addon/hyperlink/hyperlink.js"></script>
<script src="../extra_scripts/codemirror/mode/bfm/bfm.js"></script>
<script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script> <script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script>
<script src="../node_modules/codemirror/addon/edit/matchbrackets.js"></script> <script src="../node_modules/codemirror/addon/edit/matchbrackets.js"></script>

View File

@@ -71,6 +71,11 @@
border-left-color: rgba(142, 142, 142, 0.5); border-left-color: rgba(142, 142, 142, 0.5);
mix-blend-mode: difference; mix-blend-mode: difference;
} }
.CodeMirror-scroll {
margin-bottom: 0;
padding-bottom: 0;
}
</style> </style>
</head> </head>
@@ -105,7 +110,6 @@
<script src="../extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js"></script> <script src="../extra_scripts/boost/boostNewLineIndentContinueMarkdownList.js"></script>
<script src="../extra_scripts/codemirror/mode/bfm/bfm.js"></script> <script src="../extra_scripts/codemirror/mode/bfm/bfm.js"></script>
<script src="../extra_scripts/codemirror/addon/hyperlink/hyperlink.js"></script> <script src="../extra_scripts/codemirror/addon/hyperlink/hyperlink.js"></script>
<script src="../extra_scripts/codemirror/mode/bfm/bfm.js"></script>
<script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script> <script src="../node_modules/codemirror/addon/edit/closebrackets.js"></script>
<script src="../node_modules/codemirror/addon/edit/matchbrackets.js"></script> <script src="../node_modules/codemirror/addon/edit/matchbrackets.js"></script>

View File

@@ -156,5 +156,7 @@
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠", "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -212,5 +212,7 @@
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠", "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -187,5 +187,7 @@
"Snippet Default Language": "Snippet Default Language", "Snippet Default Language": "Snippet Default Language",
"New notes are tagged with the filtering tags": "New notes are tagged with the filtering tags", "New notes are tagged with the filtering tags": "New notes are tagged with the filtering tags",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -158,5 +158,7 @@
"Spellcheck disabled": "Deshabilitar corrector ortográfico", "Spellcheck disabled": "Deshabilitar corrector ortográfico",
"Show menu bar": "Mostrar barra del menú", "Show menu bar": "Mostrar barra del menú",
"Auto Detect": "Detección automática", "Auto Detect": "Detección automática",
"Snippet Default Language": "Lenguaje por defecto de los fragmentos de código" "Snippet Default Language": "Lenguaje por defecto de los fragmentos de código",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -160,5 +160,7 @@
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠", "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -172,5 +172,7 @@
"Snippet name": "Nom du snippet", "Snippet name": "Nom du snippet",
"Snippet prefix": "Préfixe du snippet", "Snippet prefix": "Préfixe du snippet",
"Delete Note": "Supprimer la note", "Delete Note": "Supprimer la note",
"New notes are tagged with the filtering tags": "Les nouvelles notes sont taggées avec les tags de filtrage" "New notes are tagged with the filtering tags": "Les nouvelles notes sont taggées avec les tags de filtrage",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -180,5 +180,7 @@
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠", "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -160,5 +160,7 @@
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠", "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -219,5 +219,7 @@
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ このノートのストレージに存在しない添付ファイルへのリンクを貼り付けました。添付ファイルへのリンクの貼り付けは同一ストレージ内でのみサポートされています。代わりに添付ファイルをドラッグアンドドロップしてください! ⚠", "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ このノートのストレージに存在しない添付ファイルへのリンクを貼り付けました。添付ファイルへのリンクの貼り付けは同一ストレージ内でのみサポートされています。代わりに添付ファイルをドラッグアンドドロップしてください! ⚠",
"Spellcheck disabled": "スペルチェック無効", "Spellcheck disabled": "スペルチェック無効",
"Show menu bar": "メニューバーを表示", "Show menu bar": "メニューバーを表示",
"Auto Detect": "自動検出" "Auto Detect": "自動検出",
"Enable HTML label in mermaid flowcharts": "mermaid flowchartでHTMLラベルを有効にする ⚠ このオプションには潜在的なXSSの危険性があります。",
"Wrap line in Snippet Note": "行を右端で折り返すSnippet Note"
} }

View File

@@ -163,5 +163,7 @@
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠", "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -156,5 +156,7 @@
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠", "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -165,5 +165,7 @@
"Add tag...": "Dodaj tag...", "Add tag...": "Dodaj tag...",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -156,5 +156,7 @@
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠", "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -155,5 +155,7 @@
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ Você colou um link referente a um anexo que não pôde ser encontrado no local de armazenamento desta nota. A vinculação de anexos de referência de links só é suportada se o local de origem e de destino for o mesmo de armazenamento. Por favor, arraste e solte o anexo na nota! ⚠", "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ Você colou um link referente a um anexo que não pôde ser encontrado no local de armazenamento desta nota. A vinculação de anexos de referência de links só é suportada se o local de origem e de destino for o mesmo de armazenamento. Por favor, arraste e solte o anexo na nota! ⚠",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -153,5 +153,7 @@
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠", "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -155,5 +155,7 @@
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠", "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -182,5 +182,7 @@
"Snippet Default Language": "ทำการ Snippet ภาษาที่เป็นค่าเริ่มต้น", "Snippet Default Language": "ทำการ Snippet ภาษาที่เป็นค่าเริ่มต้น",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -155,5 +155,7 @@
"Allow dangerous html tags": "Tehlikeli html etiketlerine izin ver", "Allow dangerous html tags": "Tehlikeli html etiketlerine izin ver",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

31
locales/zh-CN.json Executable file → Normal file
View File

@@ -4,7 +4,7 @@
"Preferences": "首选项", "Preferences": "首选项",
"Make a note": "新建笔记", "Make a note": "新建笔记",
"Ctrl": "Ctrl", "Ctrl": "Ctrl",
"Ctrl(^)": "Ctrl", "Ctrl(^)": "Ctrl(^)",
"to create a new note": "新建笔记", "to create a new note": "新建笔记",
"Toggle Mode": "切换模式", "Toggle Mode": "切换模式",
"Trash": "废纸篓", "Trash": "废纸篓",
@@ -183,7 +183,7 @@
"Help": "帮助", "Help": "帮助",
"Hungarian": "匈牙利语", "Hungarian": "匈牙利语",
"Hide Help": "隐藏帮助", "Hide Help": "隐藏帮助",
"wordpress": "Wordpress", "wordpress": "wordpress",
"Add Storage": "添加存储", "Add Storage": "添加存储",
"Name": "名称", "Name": "名称",
"Type": "类型", "Type": "类型",
@@ -219,6 +219,29 @@
"Allow custom CSS for preview": "允许预览自定义 CSS", "Allow custom CSS for preview": "允许预览自定义 CSS",
"Render newlines in Markdown paragraphs as <br>": "在 Markdown 段落中使用 <br> 换行", "Render newlines in Markdown paragraphs as <br>": "在 Markdown 段落中使用 <br> 换行",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "显示菜单栏",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "在 mermaid flowcharts 中启用 HTML 标签 ⚠ 这个选项可能会产生 XSS",
"Wrap line in Snippet Note": "在 Snippet Note 里换行",
"Toggle Editor Mode": "切换编辑模式",
"Insert Current Date": "插入当前日期",
"Insert Current Date and Time": "插入当前日期和时间",
"Paste HTML": "粘贴 HTML",
"Show/Hide Menu Bar": "显示/隐藏 菜单栏",
"Save tags of a note in alphabetical order": "按字母顺序存储标签",
"Show tags of a note in alphabetical order": "按字母顺序显示标签",
"Enable live count of notes": "实时统计标签下笔记个数",
"New notes are tagged with the filtering tags": "新建的笔记带有在标签列表过滤的标签",
"Front matter title field": "从 front-matter 中抽取标题的字段名",
"Extract title from front matter": "启用从 front-matter 抽取标题",
"Enable HTML paste": "启用 HTML 粘贴(自动转换 html 到 md",
"Enable spellcheck - Experimental feature!! :)": "启用拼写检查 - 实验性功能!! :)",
"Matching character pairs": "Matching character pairs",
"Matching character triples": "Matching character triples",
"Exploding character pairs": "Exploding character pairs",
"Custom MarkdownLint Rules": "自定义 MarkdownLint 规则",
"Enable MarkdownLint": "启用 MarkdownLint",
"When scrolling, synchronize preview with editor": "滚动编辑页时同步滚动预览页",
"This will delete all notes in the folder and can not be undone.": "即将删除文件夹中所有笔记,并且不能撤销。",
"Always Ask": "每次询问"
} }

4
locales/zh-TW.json Executable file → Normal file
View File

@@ -164,5 +164,7 @@
"⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠", "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠": "⚠ You have pasted a link referring an attachment that could not be found in the storage location of this note. Pasting links referring attachments is only supported if the source and destination location is the same storage. Please Drag&Drop the attachment instead! ⚠",
"Spellcheck disabled": "Spellcheck disabled", "Spellcheck disabled": "Spellcheck disabled",
"Show menu bar": "Show menu bar", "Show menu bar": "Show menu bar",
"Auto Detect": "Auto Detect" "Auto Detect": "Auto Detect",
"Enable HTML label in mermaid flowcharts": "Enable HTML label in mermaid flowcharts ⚠ This option potentially has a risk of XSS.",
"Wrap line in Snippet Note": "Wrap line in Snippet Note"
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "boost", "name": "boost",
"productName": "Boostnote", "productName": "Boostnote",
"version": "0.11.17", "version": "0.12.1",
"main": "index.js", "main": "index.js",
"description": "Boostnote", "description": "Boostnote",
"license": "GPL-3.0", "license": "GPL-3.0",
@@ -98,6 +98,7 @@
"mousetrap": "^1.6.2", "mousetrap": "^1.6.2",
"mousetrap-global-bind": "^1.1.0", "mousetrap-global-bind": "^1.1.0",
"node-ipc": "^8.1.0", "node-ipc": "^8.1.0",
"prettier": "^1.18.2",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"query-string": "^6.5.0", "query-string": "^6.5.0",
"raphael": "^2.2.7", "raphael": "^2.2.7",
@@ -105,6 +106,7 @@
"react-autosuggest": "^9.4.0", "react-autosuggest": "^9.4.0",
"react-codemirror": "^1.0.0", "react-codemirror": "^1.0.0",
"react-color": "^2.2.2", "react-color": "^2.2.2",
"react-composition-input": "^1.1.1",
"react-debounce-render": "^4.0.1", "react-debounce-render": "^4.0.1",
"react-dom": "^16.8.6", "react-dom": "^16.8.6",
"react-image-carousel": "^2.0.18", "react-image-carousel": "^2.0.18",

6
prettier.config Normal file
View File

@@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true
}

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<title>icon-external</title>
<defs>
<filter x="0.0%" y="0.0%" width="100.0%" height="100.0%" filterUnits="objectBoundingBox" id="filter-1">
<feOffset dx="0" dy="0" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0" type="matrix" in="shadowOffsetOuter1" result="shadowMatrixOuter1"></feColorMatrix>
<feMerge>
<feMergeNode in="shadowMatrixOuter1"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
</defs>
<g id="Artboard-4" stroke="none" stroke-width="1" fill="#c7c7c7" fill-rule="evenodd" transform="" stroke-linecap="round" stroke-linejoin="round">
<g>
<g>
<path d="M412.88,261.464c-11.423,0-20.682,9.259-20.682,20.682v156.879c0,17.43-14.181,31.611-31.612,31.611H72.975
c-17.43,0-31.611-14.181-31.611-31.611V151.414c0-17.43,14.181-31.611,31.611-31.611h156.879c11.422,0,20.682-9.26,20.682-20.682
c0-11.422-9.26-20.682-20.682-20.682H72.975C32.737,78.439,0,111.176,0,151.414v287.611C0,479.264,32.737,512,72.975,512h287.61
c40.239,0,72.976-32.736,72.977-72.975V282.146C433.562,270.723,424.303,261.464,412.88,261.464z"/>
</g>
</g>
<g>
<g>
<path d="M491.318,0H334.439c-11.423,0-20.682,9.26-20.682,20.682c0,11.422,9.259,20.682,20.682,20.682h136.197v136.197
c0,11.422,9.259,20.682,20.682,20.682c11.423,0,20.682-9.26,20.682-20.682V20.682C512,9.26,502.741,0,491.318,0z"/>
</g>
</g>
<g>
<g>
<path d="M505.942,6.058c-8.077-8.076-21.172-8.076-29.249,0L189.082,293.668c-8.077,8.077-8.077,21.172,0,29.249
c4.038,4.039,9.332,6.058,14.625,6.058c5.294,0,10.587-2.02,14.625-6.058L505.942,35.307
C514.019,27.23,514.019,14.135,505.942,6.058z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -287,7 +287,11 @@ it('should replace the all ":storage" path with the actual storage path', functi
' </p>\n' + ' </p>\n' +
' <pre class="fence" data-line="8">\n' + ' <pre class="fence" data-line="8">\n' +
' <span class="filename"></span>\n' + ' <span class="filename"></span>\n' +
' <div class="gallery" data-autoplay="undefined" data-height="undefined">:storage' + mdurl.encode(path.sep) + noteKey + mdurl.encode(path.sep) + 'f939b2c3.jpg</div>\n' + ' <div class="gallery" data-autoplay="undefined" data-height="undefined">:storage' + mdurl.encode(path.win32.sep) + noteKey + mdurl.encode(path.win32.sep) + 'f939b2c3.jpg</div>\n' +
' </pre>\n' +
' <pre class="fence" data-line="10">\n' +
' <span class="filename"></span>\n' +
' <div class="gallery" data-autoplay="undefined" data-height="undefined">:storage' + mdurl.encode(path.posix.sep) + noteKey + mdurl.encode(path.posix.sep) + 'f939b2c3.jpg</div>\n' +
' </pre>\n' + ' </pre>\n' +
' </body>\n' + ' </body>\n' +
'</html>' '</html>'
@@ -300,17 +304,21 @@ it('should replace the all ":storage" path with the actual storage path', functi
' <body data-theme="default">\n' + ' <body data-theme="default">\n' +
' <h2 data-line="0" id="Headline">Headline</h2>\n' + ' <h2 data-line="0" id="Headline">Headline</h2>\n' +
' <p data-line="2">\n' + ' <p data-line="2">\n' +
' <img src="file:///' + storagePath + path.sep + storageFolder + path.sep + noteKey + path.sep + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' + ' <img src="file:///' + storagePath + '/' + storageFolder + '/' + noteKey + '/' + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
' </p>\n' + ' </p>\n' +
' <p data-line="4">\n' + ' <p data-line="4">\n' +
' <a href="file:///' + storagePath + path.sep + storageFolder + path.sep + noteKey + path.sep + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' + ' <a href="file:///' + storagePath + '/' + storageFolder + '/' + noteKey + '/' + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
' </p>\n' + ' </p>\n' +
' <p data-line="6">\n' + ' <p data-line="6">\n' +
' <img src="file:///' + storagePath + path.sep + storageFolder + path.sep + noteKey + path.sep + 'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' + ' <img src="file:///' + storagePath + '/' + storageFolder + '/' + noteKey + '/' + 'd6c5ee92.jpg" alt="dummyImage2.jpg">\n' +
' </p>\n' + ' </p>\n' +
' <pre class="fence" data-line="8">\n' + ' <pre class="fence" data-line="8">\n' +
' <span class="filename"></span>\n' + ' <span class="filename"></span>\n' +
' <div class="gallery" data-autoplay="undefined" data-height="undefined">file:///' + storagePath + path.sep + storageFolder + path.sep + noteKey + path.sep + 'f939b2c3.jpg</div>\n' + ' <div class="gallery" data-autoplay="undefined" data-height="undefined">file:///' + storagePath + '/' + storageFolder + '/' + noteKey + '/' + 'f939b2c3.jpg</div>\n' +
' </pre>\n' +
' <pre class="fence" data-line="10">\n' +
' <span class="filename"></span>\n' +
' <div class="gallery" data-autoplay="undefined" data-height="undefined">file:///' + storagePath + '/' + storageFolder + '/' + noteKey + '/' + 'f939b2c3.jpg</div>\n' +
' </pre>\n' + ' </pre>\n' +
' </body>\n' + ' </body>\n' +
'</html>' '</html>'
@@ -345,10 +353,10 @@ it('should replace the ":storage" path with the actual storage path when they ha
' <body data-theme="default">\n' + ' <body data-theme="default">\n' +
' <h2 data-line="0" id="Headline">Headline</h2>\n' + ' <h2 data-line="0" id="Headline">Headline</h2>\n' +
' <p data-line="2">\n' + ' <p data-line="2">\n' +
' <img src="file:///' + storagePath + path.sep + storageFolder + path.sep + noteKey + path.sep + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' + ' <img src="file:///' + storagePath + '/' + storageFolder + '/' + noteKey + '/' + '0.6r4zdgc22xp.png" alt="dummyImage.png" >\n' +
' </p>\n' + ' </p>\n' +
' <p data-line="4">\n' + ' <p data-line="4">\n' +
' <a href="file:///' + storagePath + path.sep + storageFolder + path.sep + noteKey + path.sep + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' + ' <a href="file:///' + storagePath + '/' + storageFolder + '/' + noteKey + '/' + '0.q2i4iw0fyx.pdf">dummyPDF.pdf</a>\n' +
' </p>\n' + ' </p>\n' +
' </body>\n' + ' </body>\n' +
'</html>' '</html>'
@@ -570,6 +578,72 @@ it('should test that deleteAttachmentsNotPresentInNote does nothing if noteKey,
expect(fs.unlink).not.toHaveBeenCalled() expect(fs.unlink).not.toHaveBeenCalled()
}) })
it('should test that getAttachmentsPathAndStatus return null if noteKey, storageKey or noteContent was undefined', function () {
const noteKey = undefined
const storageKey = undefined
const markdownContent = ''
const result = systemUnderTest.getAttachmentsPathAndStatus(markdownContent, storageKey, noteKey)
expect(result).toBeNull()
})
it('should test that getAttachmentsPathAndStatus return null if noteKey, storageKey or noteContent was null', function () {
const noteKey = null
const storageKey = null
const markdownContent = ''
const result = systemUnderTest.getAttachmentsPathAndStatus(markdownContent, storageKey, noteKey)
expect(result).toBeNull()
})
it('should test that getAttachmentsPathAndStatus return the correct path and status for attachments', async function () {
const dummyStorage = {path: 'dummyStoragePath'}
const noteKey = 'noteKey'
const storageKey = 'storageKey'
const markdownContent =
'Test input' +
'![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.win32.sep + noteKey + path.win32.sep + 'file2.pdf](file2.pdf) \n'
const dummyFilesInFolder = ['file1.txt', 'file2.pdf', 'file3.jpg']
findStorage.findStorage = jest.fn(() => dummyStorage)
fs.existsSync = jest.fn(() => true)
fs.readdir = jest.fn((paht, callback) => callback(undefined, dummyFilesInFolder))
fs.unlink = jest.fn()
const targetStorage = findStorage.findStorage(storageKey)
const attachments = await systemUnderTest.getAttachmentsPathAndStatus(markdownContent, storageKey, noteKey)
expect(attachments.length).toBe(3)
expect(attachments[0].isInUse).toBe(false)
expect(attachments[1].isInUse).toBe(true)
expect(attachments[2].isInUse).toBe(false)
expect(attachments[0].path).toBe(
path.join(
targetStorage.path,
systemUnderTest.DESTINATION_FOLDER,
noteKey,
dummyFilesInFolder[0]
)
)
expect(attachments[1].path).toBe(
path.join(
targetStorage.path,
systemUnderTest.DESTINATION_FOLDER,
noteKey,
dummyFilesInFolder[1]
)
)
expect(attachments[2].path).toBe(
path.join(
targetStorage.path,
systemUnderTest.DESTINATION_FOLDER,
noteKey,
dummyFilesInFolder[2]
)
)
})
it('should test that moveAttachments moves attachments only if the source folder existed', function () { it('should test that moveAttachments moves attachments only if the source folder existed', function () {
fse.existsSync = jest.fn(() => false) fse.existsSync = jest.fn(() => false)
fse.moveSync = jest.fn() fse.moveSync = jest.fn()

View File

@@ -3,6 +3,9 @@ const copyFile = require('browser/main/lib/dataApi/copyFile')
const path = require('path') const path = require('path')
const fs = require('fs') const fs = require('fs')
const os = require('os')
const execSync = require('child_process').execSync
const removeDirCommand = os.platform() === 'win32' ? 'rmdir /s /q ' : 'rm -rf '
const testFile = 'test.txt' const testFile = 'test.txt'
const srcFolder = path.join(__dirname, '🤔') const srcFolder = path.join(__dirname, '🤔')
@@ -29,7 +32,7 @@ test('`copyFile` should handle encoded URI on src path', (t) => {
test.after((t) => { test.after((t) => {
fs.unlinkSync(srcPath) fs.unlinkSync(srcPath)
fs.unlinkSync(dstPath) fs.unlinkSync(dstPath)
fs.rmdirSync(srcFolder) execSync(removeDirCommand + '"' + srcFolder + '"')
fs.rmdirSync(dstFolder) execSync(removeDirCommand + '"' + dstFolder + '"')
}) })

View File

@@ -0,0 +1,43 @@
const test = require('ava')
const createNoteFromUrl = require('browser/main/lib/dataApi/createNoteFromUrl')
global.document = require('jsdom').jsdom('<body></body>')
global.window = document.defaultView
global.navigator = window.navigator
const Storage = require('dom-storage')
const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true })
const path = require('path')
const TestDummy = require('../fixtures/TestDummy')
const sander = require('sander')
const os = require('os')
const CSON = require('@rokt33r/season')
const storagePath = path.join(os.tmpdir(), 'test/create-note-from-url')
test.beforeEach((t) => {
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
})
test.serial('Create a note from URL', (t) => {
const storageKey = t.context.storage.cache.key
const folderKey = t.context.storage.json.folders[0].key
const url = 'https://shapeshed.com/writing-cross-platform-node/'
return createNoteFromUrl(url, storageKey, folderKey)
.then(function assert ({ note }) {
t.is(storageKey, note.storage)
const jsonData = CSON.readFileSync(path.join(storagePath, 'notes', note.key + '.cson'))
// Test if saved content is matching the created in memory note
t.is(note.content, jsonData.content)
t.is(note.tags.length, jsonData.tags.length)
})
})
test.after(function after () {
localStorage.clear()
sander.rimrafSync(storagePath)
})

View File

@@ -104,6 +104,11 @@ Term 2 with *inline markup*
` `
const shortcuts = '<kbd>Ctrl</kbd>\n\n[[Ctrl]]' const shortcuts = '<kbd>Ctrl</kbd>\n\n[[Ctrl]]'
const footnote = `
^[hello-world]
hello-world: https://github.com/BoostIO/Boostnote/
`
export default { export default {
basic, basic,
codeblock, codeblock,
@@ -115,5 +120,6 @@ export default {
subTexts, subTexts,
supTexts, supTexts,
deflists, deflists,
shortcuts shortcuts,
footnote
} }

View File

@@ -4,12 +4,14 @@ jest.mock('electron', () => {
}) })
const spellcheck = require('browser/lib/spellcheck') const spellcheck = require('browser/lib/spellcheck')
const buildEditorContextMenu = require('browser/lib/contextMenuBuilder') const buildEditorContextMenu = require('browser/lib/contextMenuBuilder').buildEditorContextMenu
const buildMarkdownPreviewContextMenu = require('browser/lib/contextMenuBuilder').buildMarkdownPreviewContextMenu
beforeEach(() => { beforeEach(() => {
menuBuilderParameter = null menuBuilderParameter = null
}) })
// Editor Context Menu
it('should make sure that no context menu is build if the passed editor instance was null', function () { it('should make sure that no context menu is build if the passed editor instance was null', function () {
const event = { const event = {
pageX: 12, pageX: 12,
@@ -124,3 +126,13 @@ it('should make sure that word suggestions creates a correct menu if there was a
expect(menuBuilderParameter[7].role).toEqual('selectall') expect(menuBuilderParameter[7].role).toEqual('selectall')
expect(spellcheck.getSpellingSuggestion).toHaveBeenCalledWith(wordToCorrect) expect(spellcheck.getSpellingSuggestion).toHaveBeenCalledWith(wordToCorrect)
}) })
// Markdown Preview Context Menu
it('should make sure that no context menu is built if the Markdown Preview instance was null', function () {
const event = {
pageX: 12,
pageY: 12
}
buildMarkdownPreviewContextMenu(null, event)
expect(menuBuilderParameter).toEqual(null)
})

View File

@@ -68,3 +68,8 @@ test('Markdown.render() should render shortcuts correctly', t => {
const rendered = md.render(markdownFixtures.shortcuts) const rendered = md.render(markdownFixtures.shortcuts)
t.snapshot(rendered) t.snapshot(rendered)
}) })
test('Markdown.render() should render footnote correctly', t => {
const rendered = md.render(markdownFixtures.footnote)
t.snapshot(rendered)
})

58
tests/lib/slugify-test.js Normal file
View File

@@ -0,0 +1,58 @@
import test from 'ava'
import slugify from 'browser/lib/slugify'
test('alphabet and digit', t => {
const upperAlphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
const lowerAlphabet = 'abcdefghijklmnopqrstuvwxyz'
const digit = '0123456789'
const testCase = upperAlphabet + lowerAlphabet + digit
const decodeSlug = decodeURI(slugify(testCase))
t.true(decodeSlug === testCase)
})
test('should delete unavailable symbols', t => {
const availableSymbols = '_-'
const testCase = availableSymbols + '][!\'#$%&()*+,./:;<=>?@\\^{|}~`'
const decodeSlug = decodeURI(slugify(testCase))
t.true(decodeSlug === availableSymbols)
})
test('should convert from white spaces between words to hyphens', t => {
const testCase = 'This is one'
const expectedString = 'This-is-one'
const decodeSlug = decodeURI(slugify(testCase))
t.true(decodeSlug === expectedString)
})
test('should remove leading white spaces', t => {
const testCase = ' This is one'
const expectedString = 'This-is-one'
const decodeSlug = decodeURI(slugify(testCase))
t.true(decodeSlug === expectedString)
})
test('should remove trailing white spaces', t => {
const testCase = 'This is one '
const expectedString = 'This-is-one'
const decodeSlug = decodeURI(slugify(testCase))
t.true(decodeSlug === expectedString)
})
test('2-byte charactor support', t => {
const testCase = '菠萝芒果テストÀžƁƵ'
const decodeSlug = decodeURI(slugify(testCase))
t.true(decodeSlug === testCase)
})
test('emoji', t => {
const testCase = '🌸'
const decodeSlug = decodeURI(slugify(testCase))
t.true(decodeSlug === testCase)
})

View File

@@ -4,6 +4,21 @@ The actual snapshot is saved in `markdown-test.js.snap`.
Generated by [AVA](https://ava.li). Generated by [AVA](https://ava.li).
## Markdown.render() should render footnote correctly
> Snapshot 1
`<p data-line="1"><sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup><br />␊
hello-world: <a href="https://github.com/BoostIO/Boostnote/">https://github.com/BoostIO/Boostnote/</a></p>␊
<hr class="footnotes-sep" />␊
<section class="footnotes">␊
<ol class="footnotes-list">␊
<li id="fn1" class="footnote-item"><p>hello-world <a href="#fnref1" class="footnote-backref">↩︎</a></p>␊
</li>␊
</ol>␊
</section>␊
`
## Markdown.render() should render line breaks correctly ## Markdown.render() should render line breaks correctly
> Snapshot 1 > Snapshot 1

Some files were not shown because too many files have changed in this diff Show More