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

Merge remote-tracking branch 'upstream/master' into allow-no-html-escape

This commit is contained in:
Nguyễn Việt Hưng
2018-07-04 13:50:10 +07:00
98 changed files with 3852 additions and 2050 deletions

View File

@@ -20,6 +20,6 @@ If your issue is regarding boostnote mobile, move to https://github.com/BoostIO/
- OS Version and name : - OS Version and name :
<!-- <!--
Love Boostnote? Please consider supporting us via OpenCollective: Love Boostnote? Please consider supporting us on IssueHunt:
👉 https://opencollective.com/boostnoteio 👉 https://issuehunt.io/repos/53266139
--> -->

View File

@@ -7,14 +7,16 @@ import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
import convertModeName from 'browser/lib/convertModeName' import convertModeName from 'browser/lib/convertModeName'
import eventEmitter from 'browser/main/lib/eventEmitter' import eventEmitter from 'browser/main/lib/eventEmitter'
import iconv from 'iconv-lite' import iconv from 'iconv-lite'
import crypto from 'crypto'
import consts from 'browser/lib/consts'
import fs from 'fs'
const { ipcRenderer } = require('electron') const { ipcRenderer } = require('electron')
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
const buildCMRulers = (rulers, enableRulers) => const buildCMRulers = (rulers, enableRulers) =>
enableRulers ? rulers.map(ruler => ({ column: ruler })) : [] enableRulers ? rulers.map(ruler => ({column: ruler})) : []
export default class CodeEditor extends React.Component { export default class CodeEditor extends React.Component {
constructor (props) { constructor (props) {
@@ -81,8 +83,21 @@ export default class CodeEditor extends React.Component {
componentDidMount () { componentDidMount () {
const { rulers, enableRulers } = this.props const { rulers, enableRulers } = this.props
this.value = this.props.value const expandSnippet = this.expandSnippet.bind(this)
const defaultSnippet = [
{
id: crypto.randomBytes(16).toString('hex'),
name: 'Dummy text',
prefix: ['lorem', 'ipsum'],
content: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
}
]
if (!fs.existsSync(consts.SNIPPET_FILE)) {
fs.writeFileSync(consts.SNIPPET_FILE, JSON.stringify(defaultSnippet, null, 4), 'utf8')
}
this.value = this.props.value
this.editor = CodeMirror(this.refs.root, { this.editor = CodeMirror(this.refs.root, {
rulers: buildCMRulers(rulers, enableRulers), rulers: buildCMRulers(rulers, enableRulers),
value: this.props.value, value: this.props.value,
@@ -103,6 +118,8 @@ export default class CodeEditor extends React.Component {
Tab: function (cm) { Tab: function (cm) {
const cursor = cm.getCursor() const cursor = cm.getCursor()
const line = cm.getLine(cursor.line) const line = cm.getLine(cursor.line)
const cursorPosition = cursor.ch
const charBeforeCursor = line.substr(cursorPosition - 1, 1)
if (cm.somethingSelected()) cm.indentSelection('add') if (cm.somethingSelected()) cm.indentSelection('add')
else { else {
const tabs = cm.getOption('indentWithTabs') const tabs = cm.getOption('indentWithTabs')
@@ -114,6 +131,16 @@ export default class CodeEditor extends React.Component {
cm.execCommand('insertSoftTab') cm.execCommand('insertSoftTab')
} }
cm.execCommand('goLineEnd') cm.execCommand('goLineEnd')
} else if (!charBeforeCursor.match(/\t|\s|\r|\n/) && cursor.ch > 1) {
// text expansion on tab key if the char before is alphabet
const snippets = JSON.parse(fs.readFileSync(consts.SNIPPET_FILE, 'utf8'))
if (expandSnippet(line, cursor, cm, snippets) === false) {
if (tabs) {
cm.execCommand('insertTab')
} else {
cm.execCommand('insertSoftTab')
}
}
} else { } else {
if (tabs) { if (tabs) {
cm.execCommand('insertTab') cm.execCommand('insertTab')
@@ -157,6 +184,73 @@ export default class CodeEditor extends React.Component {
CodeMirror.Vim.map('ZZ', ':q', 'normal') CodeMirror.Vim.map('ZZ', ':q', 'normal')
} }
expandSnippet (line, cursor, cm, snippets) {
const wordBeforeCursor = this.getWordBeforeCursor(line, cursor.line, cursor.ch)
const templateCursorString = ':{}'
for (let i = 0; i < snippets.length; i++) {
if (snippets[i].prefix.indexOf(wordBeforeCursor.text) !== -1) {
if (snippets[i].content.indexOf(templateCursorString) !== -1) {
const snippetLines = snippets[i].content.split('\n')
let cursorLineNumber = 0
let cursorLinePosition = 0
for (let j = 0; j < snippetLines.length; j++) {
const cursorIndex = snippetLines[j].indexOf(templateCursorString)
if (cursorIndex !== -1) {
cursorLineNumber = j
cursorLinePosition = cursorIndex
cm.replaceRange(
snippets[i].content.replace(templateCursorString, ''),
wordBeforeCursor.range.from,
wordBeforeCursor.range.to
)
cm.setCursor({ line: cursor.line + cursorLineNumber, ch: cursorLinePosition })
}
}
} else {
cm.replaceRange(
snippets[i].content,
wordBeforeCursor.range.from,
wordBeforeCursor.range.to
)
}
return true
}
}
return false
}
getWordBeforeCursor (line, lineNumber, cursorPosition) {
let wordBeforeCursor = ''
const originCursorPosition = cursorPosition
const emptyChars = /\t|\s|\r|\n/
// to prevent the word to expand is long that will crash the whole app
// the safeStop is there to stop user to expand words that longer than 20 chars
const safeStop = 20
while (cursorPosition > 0) {
const currentChar = line.substr(cursorPosition - 1, 1)
// if char is not an empty char
if (!emptyChars.test(currentChar)) {
wordBeforeCursor = currentChar + wordBeforeCursor
} else if (wordBeforeCursor.length >= safeStop) {
throw new Error('Your snippet trigger is too long !')
} else {
break
}
cursorPosition--
}
return {
text: wordBeforeCursor,
range: {
from: {line: lineNumber, ch: originCursorPosition},
to: {line: lineNumber, ch: cursorPosition}
}
}
}
quitEditor () { quitEditor () {
document.querySelector('textarea').blur() document.querySelector('textarea').blur()
} }
@@ -174,7 +268,7 @@ export default class CodeEditor extends React.Component {
componentDidUpdate (prevProps, prevState) { componentDidUpdate (prevProps, prevState) {
let needRefresh = false let needRefresh = false
const { rulers, enableRulers } = this.props const {rulers, enableRulers} = this.props
if (prevProps.mode !== this.props.mode) { if (prevProps.mode !== this.props.mode) {
this.setMode(this.props.mode) this.setMode(this.props.mode)
} }
@@ -274,6 +368,7 @@ export default class CodeEditor extends React.Component {
handlePaste (editor, e) { handlePaste (editor, e) {
const clipboardData = e.clipboardData const clipboardData = e.clipboardData
const {storageKey, noteKey} = this.props
const dataTransferItem = clipboardData.items[0] const dataTransferItem = clipboardData.items[0]
const pastedTxt = clipboardData.getData('text') const pastedTxt = clipboardData.getData('text')
const isURL = (str) => { const isURL = (str) => {
@@ -283,22 +378,28 @@ export default class CodeEditor extends React.Component {
const isInLinkTag = (editor) => { const isInLinkTag = (editor) => {
const startCursor = editor.getCursor('start') const startCursor = editor.getCursor('start')
const prevChar = editor.getRange( const prevChar = editor.getRange(
{ line: startCursor.line, ch: startCursor.ch - 2 }, {line: startCursor.line, ch: startCursor.ch - 2},
{ line: startCursor.line, ch: startCursor.ch } {line: startCursor.line, ch: startCursor.ch}
) )
const endCursor = editor.getCursor('end') const endCursor = editor.getCursor('end')
const nextChar = editor.getRange( const nextChar = editor.getRange(
{ line: endCursor.line, ch: endCursor.ch }, {line: endCursor.line, ch: endCursor.ch},
{ line: endCursor.line, ch: endCursor.ch + 1 } {line: endCursor.line, ch: endCursor.ch + 1}
) )
return prevChar === '](' && nextChar === ')' return prevChar === '](' && nextChar === ')'
} }
if (dataTransferItem.type.match('image')) { if (dataTransferItem.type.match('image')) {
const {storageKey, noteKey} = this.props
attachmentManagement.handlePastImageEvent(this, storageKey, noteKey, dataTransferItem) attachmentManagement.handlePastImageEvent(this, storageKey, noteKey, dataTransferItem)
} else if (this.props.fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) { } else if (this.props.fetchUrlTitle && isURL(pastedTxt) && !isInLinkTag(editor)) {
this.handlePasteUrl(e, editor, pastedTxt) this.handlePasteUrl(e, editor, pastedTxt)
} }
if (attachmentManagement.isAttachmentLink(pastedTxt)) {
attachmentManagement.handleAttachmentLinkPaste(storageKey, noteKey, pastedTxt)
.then((modifiedText) => {
this.editor.replaceSelection(modifiedText)
})
e.preventDefault()
}
} }
handleScroll (e) { handleScroll (e) {
@@ -312,24 +413,58 @@ export default class CodeEditor extends React.Component {
const taggedUrl = `<${pastedTxt}>` const taggedUrl = `<${pastedTxt}>`
editor.replaceSelection(taggedUrl) editor.replaceSelection(taggedUrl)
const isImageReponse = (response) => {
return response.headers.has('content-type') &&
response.headers.get('content-type').match(/^image\/.+$/)
}
const replaceTaggedUrl = (replacement) => {
const value = editor.getValue()
const cursor = editor.getCursor()
const newValue = value.replace(taggedUrl, replacement)
const newCursor = Object.assign({}, cursor, { ch: cursor.ch + newValue.length - value.length })
editor.setValue(newValue)
editor.setCursor(newCursor)
}
fetch(pastedTxt, { fetch(pastedTxt, {
method: 'get' method: 'get'
}).then((response) => { }).then((response) => {
return this.decodeResponse(response) if (isImageReponse(response)) {
}).then((response) => { return this.mapImageResponse(response, pastedTxt)
const parsedResponse = (new window.DOMParser()).parseFromString(response, 'text/html') } else {
const value = editor.getValue() return this.mapNormalResponse(response, pastedTxt)
const cursor = editor.getCursor() }
const LinkWithTitle = `[${parsedResponse.title}](${pastedTxt})` }).then((replacement) => {
const newValue = value.replace(taggedUrl, LinkWithTitle) replaceTaggedUrl(replacement)
editor.setValue(newValue)
editor.setCursor(cursor)
}).catch((e) => { }).catch((e) => {
const value = editor.getValue() replaceTaggedUrl(pastedTxt)
const newValue = value.replace(taggedUrl, pastedTxt) })
const cursor = editor.getCursor() }
editor.setValue(newValue)
editor.setCursor(cursor) mapNormalResponse (response, pastedTxt) {
return this.decodeResponse(response).then((body) => {
return new Promise((resolve, reject) => {
try {
const parsedBody = (new window.DOMParser()).parseFromString(body, 'text/html')
const linkWithTitle = `[${parsedBody.title}](${pastedTxt})`
resolve(linkWithTitle)
} catch (e) {
reject(e)
}
})
})
}
mapImageResponse (response, pastedTxt) {
return new Promise((resolve, reject) => {
try {
const url = response.url
const name = url.substring(url.lastIndexOf('/') + 1)
const imageLinkWithName = `![${name}](${pastedTxt})`
resolve(imageLinkWithName)
} catch (e) {
reject(e)
}
}) })
} }
@@ -359,11 +494,9 @@ export default class CodeEditor extends React.Component {
} }
render () { render () {
const { className, fontSize } = this.props const {className, fontSize} = this.props
let fontFamily = this.props.fontFamily const fontFamily = normalizeEditorFontFamily(this.props.fontFamily)
fontFamily = _.isString(fontFamily) && fontFamily.length > 0 const width = this.props.width
? [fontFamily].concat(defaultEditorFontFamily)
: defaultEditorFontFamily
return ( return (
<div <div
className={className == null className={className == null
@@ -373,8 +506,9 @@ export default class CodeEditor extends React.Component {
ref='root' ref='root'
tabIndex='-1' tabIndex='-1'
style={{ style={{
fontFamily: fontFamily.join(', '), fontFamily,
fontSize: fontSize fontSize: fontSize,
width: width
}} }}
onDrop={(e) => this.handleDropImage(e)} onDrop={(e) => this.handleDropImage(e)}
/> />

View File

@@ -283,6 +283,8 @@ class MarkdownEditor extends React.Component {
indentSize={editorIndentSize} indentSize={editorIndentSize}
scrollPastEnd={config.preview.scrollPastEnd} scrollPastEnd={config.preview.scrollPastEnd}
smartQuotes={config.preview.smartQuotes} smartQuotes={config.preview.smartQuotes}
smartArrows={config.preview.smartArrows}
breaks={config.preview.breaks}
sanitize={config.preview.sanitize} sanitize={config.preview.sanitize}
ref='preview' ref='preview'
onContextMenu={(e) => this.handleContextMenu(e)} onContextMenu={(e) => this.handleContextMenu(e)}
@@ -295,6 +297,8 @@ class MarkdownEditor extends React.Component {
showCopyNotification={config.ui.showCopyNotification} showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path} storagePath={storage.path}
noteKey={noteKey} noteKey={noteKey}
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
/> />
</div> </div>
) )

View File

@@ -22,10 +22,12 @@ const attachmentManagement = require('../main/lib/dataApi/attachmentManagement')
const { app } = remote const { app } = remote
const path = require('path') const path = require('path')
const fileUrl = require('file-url')
const dialog = remote.dialog const dialog = remote.dialog
const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1] const markdownStyle = require('!!css!stylus?sourceMap!./markdown.styl')[0][1]
const appPath = 'file://' + (process.env.NODE_ENV === 'production' const appPath = fileUrl(process.env.NODE_ENV === 'production'
? app.getAppPath() ? app.getAppPath()
: path.resolve()) : path.resolve())
const CSS_FILES = [ const CSS_FILES = [
@@ -33,7 +35,7 @@ const CSS_FILES = [
`${appPath}/node_modules/codemirror/lib/codemirror.css` `${appPath}/node_modules/codemirror/lib/codemirror.css`
] ]
function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme) { function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS) {
return ` return `
@font-face { @font-face {
font-family: 'Lato'; font-family: 'Lato';
@@ -53,7 +55,19 @@ function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scro
font-weight: 700; font-weight: 700;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff2') format('woff2'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
}
${allowCustomCSS ? customCSS : ''}
${markdownStyle} ${markdownStyle}
body { body {
font-family: '${fontFamily.join("','")}'; font-family: '${fontFamily.join("','")}';
font-size: ${fontSize}px; font-size: ${fontSize}px;
@@ -111,6 +125,9 @@ body p {
color: #000; color: #000;
background-color: #fff; background-color: #fff;
} }
.clipboardButton {
display: none
}
} }
` `
} }
@@ -133,7 +150,6 @@ export default class MarkdownPreview extends React.Component {
this.mouseUpHandler = (e) => this.handleMouseUp(e) this.mouseUpHandler = (e) => this.handleMouseUp(e)
this.DoubleClickHandler = (e) => this.handleDoubleClick(e) this.DoubleClickHandler = (e) => this.handleDoubleClick(e)
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true}) this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e) this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
this.saveAsTextHandler = () => this.handleSaveAsText() this.saveAsTextHandler = () => this.handleSaveAsText()
this.saveAsMdHandler = () => this.handleSaveAsMd() this.saveAsMdHandler = () => this.handleSaveAsMd()
@@ -146,29 +162,14 @@ export default class MarkdownPreview extends React.Component {
} }
initMarkdown () { initMarkdown () {
const { smartQuotes, sanitize } = this.props const { smartQuotes, sanitize, breaks } = this.props
this.markdown = new Markdown({ this.markdown = new Markdown({
typographer: smartQuotes, typographer: smartQuotes,
sanitize sanitize,
breaks
}) })
} }
handlePreviewAnchorClick (e) {
e.preventDefault()
e.stopPropagation()
const anchor = e.target.closest('a')
const href = anchor.getAttribute('href')
if (_.isString(href) && href.match(/^#/)) {
const targetElement = this.refs.root.contentWindow.document.getElementById(href.substring(1, href.length))
if (targetElement != null) {
this.getWindow().scrollTo(0, targetElement.offsetTop)
}
} else {
shell.openExternal(href)
}
}
handleCheckboxClick (e) { handleCheckboxClick (e) {
this.props.onCheckboxClick(e) this.props.onCheckboxClick(e)
} }
@@ -216,9 +217,9 @@ export default class MarkdownPreview extends React.Component {
handleSaveAsHtml () { handleSaveAsHtml () {
this.exportAsDocument('html', (noteContent, exportTasks) => { this.exportAsDocument('html', (noteContent, exportTasks) => {
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme} = this.getStyleParams() const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams()
const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme) const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS)
let body = this.markdown.render(escapeHtmlCharacters(noteContent)) let body = this.markdown.render(escapeHtmlCharacters(noteContent))
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
@@ -341,7 +342,10 @@ export default class MarkdownPreview extends React.Component {
componentDidUpdate (prevProps) { componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value) this.rewriteIframe() if (prevProps.value !== this.props.value) this.rewriteIframe()
if (prevProps.smartQuotes !== this.props.smartQuotes || prevProps.sanitize !== this.props.sanitize) { if (prevProps.smartQuotes !== this.props.smartQuotes ||
prevProps.sanitize !== this.props.sanitize ||
prevProps.smartArrows !== this.props.smartArrows ||
prevProps.breaks !== this.props.breaks) {
this.initMarkdown() this.initMarkdown()
this.rewriteIframe() this.rewriteIframe()
} }
@@ -352,14 +356,16 @@ export default class MarkdownPreview extends React.Component {
prevProps.lineNumber !== this.props.lineNumber || prevProps.lineNumber !== this.props.lineNumber ||
prevProps.showCopyNotification !== this.props.showCopyNotification || prevProps.showCopyNotification !== this.props.showCopyNotification ||
prevProps.theme !== this.props.theme || prevProps.theme !== this.props.theme ||
prevProps.scrollPastEnd !== this.props.scrollPastEnd) { prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
prevProps.customCSS !== this.props.customCSS) {
this.applyStyle() this.applyStyle()
this.rewriteIframe() this.rewriteIframe()
} }
} }
getStyleParams () { getStyleParams () {
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd, theme } = this.props const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS } = this.props
let { fontFamily, codeBlockFontFamily } = this.props let { fontFamily, codeBlockFontFamily } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0 fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily) ? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
@@ -368,14 +374,14 @@ export default class MarkdownPreview extends React.Component {
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily) ? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily : defaultCodeBlockFontFamily
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme} return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS}
} }
applyStyle () { applyStyle () {
const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme} = this.getStyleParams() const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams()
this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme) this.getWindow().document.getElementById('codeTheme').href = this.GetCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme) this.getWindow().document.getElementById('style').innerHTML = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS)
} }
GetCodeThemeLink (theme) { GetCodeThemeLink (theme) {
@@ -388,9 +394,6 @@ export default class MarkdownPreview extends React.Component {
} }
rewriteIframe () { rewriteIframe () {
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
el.removeEventListener('click', this.anchorClickHandler)
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => { _.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
el.removeEventListener('click', this.checkboxClickHandler) el.removeEventListener('click', this.checkboxClickHandler)
}) })
@@ -399,7 +402,7 @@ export default class MarkdownPreview extends React.Component {
el.removeEventListener('click', this.linkClickHandler) el.removeEventListener('click', this.linkClickHandler)
}) })
const { theme, indentSize, showCopyNotification, storagePath } = this.props const { theme, indentSize, showCopyNotification, storagePath, noteKey } = this.props
let { value, codeBlockTheme } = this.props let { value, codeBlockTheme } = this.props
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme) this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
@@ -411,18 +414,15 @@ export default class MarkdownPreview extends React.Component {
}) })
} }
let renderedHTML = this.markdown.render(value) let renderedHTML = this.markdown.render(value)
attachmentManagement.migrateAttachments(renderedHTML, storagePath, noteKey)
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath) this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath)
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
this.fixDecodedURI(el)
el.addEventListener('click', this.anchorClickHandler)
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => { _.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
el.addEventListener('click', this.checkboxClickHandler) el.addEventListener('click', this.checkboxClickHandler)
}) })
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { _.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
this.fixDecodedURI(el)
el.addEventListener('click', this.linkClickHandler) el.addEventListener('click', this.linkClickHandler)
}) })
@@ -473,7 +473,7 @@ export default class MarkdownPreview extends React.Component {
el.innerHTML = '' el.innerHTML = ''
diagram.drawSVG(el, opts) diagram.drawSVG(el, opts)
_.forEach(el.querySelectorAll('a'), (el) => { _.forEach(el.querySelectorAll('a'), (el) => {
el.addEventListener('click', this.anchorClickHandler) el.addEventListener('click', this.linkClickHandler)
}) })
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@@ -489,7 +489,7 @@ export default class MarkdownPreview extends React.Component {
el.innerHTML = '' el.innerHTML = ''
diagram.drawSVG(el, {theme: 'simple'}) diagram.drawSVG(el, {theme: 'simple'})
_.forEach(el.querySelectorAll('a'), (el) => { _.forEach(el.querySelectorAll('a'), (el) => {
el.addEventListener('click', this.anchorClickHandler) el.addEventListener('click', this.linkClickHandler)
}) })
} catch (e) { } catch (e) {
console.error(e) console.error(e)
@@ -538,11 +538,6 @@ export default class MarkdownPreview extends React.Component {
e.stopPropagation() e.stopPropagation()
const href = e.target.href const href = e.target.href
if (href.match(/^http/i)) {
shell.openExternal(href)
return
}
const linkHash = href.split('/').pop() const linkHash = href.split('/').pop()
const regexNoteInternalLink = /main.html#(.+)/ const regexNoteInternalLink = /main.html#(.+)/
@@ -574,6 +569,9 @@ export default class MarkdownPreview extends React.Component {
eventEmitter.emit('list:jump', linkHash.split('-')[1]) eventEmitter.emit('list:jump', linkHash.split('-')[1])
return return
} }
// other case
shell.openExternal(href)
} }
render () { render () {
@@ -596,9 +594,12 @@ MarkdownPreview.propTypes = {
onDoubleClick: PropTypes.func, onDoubleClick: PropTypes.func,
onMouseUp: PropTypes.func, onMouseUp: PropTypes.func,
onMouseDown: PropTypes.func, onMouseDown: PropTypes.func,
onContextMenu: PropTypes.func,
className: PropTypes.string, className: PropTypes.string,
value: PropTypes.string, value: PropTypes.string,
showCopyNotification: PropTypes.bool, showCopyNotification: PropTypes.bool,
storagePath: PropTypes.string, storagePath: PropTypes.string,
smartQuotes: PropTypes.bool smartQuotes: PropTypes.bool,
smartArrows: PropTypes.bool,
breaks: PropTypes.bool
} }

View File

@@ -14,6 +14,10 @@ class MarkdownSplitEditor extends React.Component {
this.focus = () => this.refs.code.focus() this.focus = () => this.refs.code.focus()
this.reload = () => this.refs.code.reload() this.reload = () => this.refs.code.reload()
this.userScroll = true this.userScroll = true
this.state = {
isSliderFocused: false,
codeEditorWidthInPercent: 50
}
} }
handleOnChange () { handleOnChange () {
@@ -87,6 +91,42 @@ class MarkdownSplitEditor extends React.Component {
} }
} }
handleMouseMove (e) {
if (this.state.isSliderFocused) {
const rootRect = this.refs.root.getBoundingClientRect()
const rootWidth = rootRect.width
const offset = rootRect.left
let newCodeEditorWidthInPercent = (e.pageX - offset) / rootWidth * 100
// limit minSize to 10%, maxSize to 90%
if (newCodeEditorWidthInPercent <= 10) {
newCodeEditorWidthInPercent = 10
}
if (newCodeEditorWidthInPercent >= 90) {
newCodeEditorWidthInPercent = 90
}
this.setState({
codeEditorWidthInPercent: newCodeEditorWidthInPercent
})
}
}
handleMouseUp (e) {
e.preventDefault()
this.setState({
isSliderFocused: false
})
}
handleMouseDown (e) {
e.preventDefault()
this.setState({
isSliderFocused: true
})
}
render () { render () {
const {config, value, storageKey, noteKey} = this.props const {config, value, storageKey, noteKey} = this.props
const storage = findStorage(storageKey) const storage = findStorage(storageKey)
@@ -95,12 +135,16 @@ class MarkdownSplitEditor extends React.Component {
let editorIndentSize = parseInt(config.editor.indentSize, 10) let editorIndentSize = parseInt(config.editor.indentSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4 if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
const previewStyle = {} const previewStyle = {}
if (this.props.ignorePreviewPointerEvents) previewStyle.pointerEvents = 'none' previewStyle.width = (100 - this.state.codeEditorWidthInPercent) + '%'
if (this.props.ignorePreviewPointerEvents || this.state.isSliderFocused) previewStyle.pointerEvents = 'none'
return ( return (
<div styleName='root'> <div styleName='root' ref='root'
onMouseMove={e => this.handleMouseMove(e)}
onMouseUp={e => this.handleMouseUp(e)}>
<CodeEditor <CodeEditor
styleName='codeEditor' styleName='codeEditor'
ref='code' ref='code'
width={this.state.codeEditorWidthInPercent + '%'}
mode='GitHub Flavored Markdown' mode='GitHub Flavored Markdown'
value={value} value={value}
theme={config.editor.theme} theme={config.editor.theme}
@@ -119,6 +163,9 @@ class MarkdownSplitEditor extends React.Component {
onChange={this.handleOnChange.bind(this)} onChange={this.handleOnChange.bind(this)}
onScroll={this.handleScroll.bind(this)} onScroll={this.handleScroll.bind(this)}
/> />
<div styleName='slider' style={{left: this.state.codeEditorWidthInPercent + '%'}} onMouseDown={e => this.handleMouseDown(e)} >
<div styleName='slider-hitbox' />
</div>
<MarkdownPreview <MarkdownPreview
style={previewStyle} style={previewStyle}
styleName='preview' styleName='preview'
@@ -131,6 +178,8 @@ class MarkdownSplitEditor extends React.Component {
lineNumber={config.preview.lineNumber} lineNumber={config.preview.lineNumber}
scrollPastEnd={config.preview.scrollPastEnd} scrollPastEnd={config.preview.scrollPastEnd}
smartQuotes={config.preview.smartQuotes} smartQuotes={config.preview.smartQuotes}
smartArrows={config.preview.smartArrows}
breaks={config.preview.breaks}
sanitize={config.preview.sanitize} sanitize={config.preview.sanitize}
ref='preview' ref='preview'
tabInde='0' tabInde='0'
@@ -140,6 +189,8 @@ class MarkdownSplitEditor extends React.Component {
showCopyNotification={config.ui.showCopyNotification} showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path} storagePath={storage.path}
noteKey={noteKey} noteKey={noteKey}
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
/> />
</div> </div>
) )

View File

@@ -3,7 +3,14 @@
height 100% height 100%
font-size 30px font-size 30px
display flex display flex
.codeEditor .slider
width 50% absolute top bottom
.preview top -2px
width 50% width 0
z-index 0
.slider-hitbox
absolute top bottom left right
width 7px
left -3px
z-index 10
cursor col-resize

View File

@@ -134,6 +134,7 @@ body[data-theme="dark"]
.item-simple-wrapper .item-simple-wrapper
border-color transparent border-color transparent
.item-simple-title .item-simple-title
.item-simple-title-empty
.item-simple-title-icon .item-simple-title-icon
.item-simple-bottom-time .item-simple-bottom-time
color $ui-dark-text-color color $ui-dark-text-color

View File

@@ -18,7 +18,7 @@
.iconWrap .iconWrap
width 20px width 20px
text-align center text-align center
.counters .counters
float right float right
color $ui-inactive-text-color color $ui-inactive-text-color
@@ -68,10 +68,9 @@
.menu-button-label .menu-button-label
position fixed position fixed
display inline-block display inline-block
height 32px height 36px
left 44px left 44px
padding 0 10px padding 0 10px
margin-top -8px
margin-left 0 margin-left 0
overflow ellipsis overflow ellipsis
z-index 10 z-index 10

View File

@@ -58,8 +58,8 @@
opacity 0 opacity 0
border-top-right-radius 2px border-top-right-radius 2px
border-bottom-right-radius 2px border-bottom-right-radius 2px
height 26px height 34px
line-height 26px line-height 32px
.folderList-item:hover, .folderList-item--active:hover .folderList-item:hover, .folderList-item--active:hover
.folderList-item-tooltip .folderList-item-tooltip

View File

@@ -293,6 +293,84 @@ kbd
line-height 1 line-height 1
padding 3px 5px padding 3px 5px
$admonition
box-shadow 0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2)
position relative
margin 1.5625em 0
padding 0 1.2rem
border-left .4rem solid #448aff
border-radius .2rem
overflow auto
html .admonition>:last-child
margin-bottom 1.2rem
.admonition .admonition
margin 1em 0
.admonition p
margin-top: 0.5em
$admonition-icon
position absolute
left 1.2rem
font-family: "Material Icons"
font-weight: normal;
font-style: normal;
font-size: 24px
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
/* Support for all WebKit browsers. */
-webkit-font-smoothing: antialiased;
/* Support for Safari and Chrome. */
text-rendering: optimizeLegibility;
/* Support for Firefox. */
-moz-osx-font-smoothing: grayscale;
/* Support for IE. */
font-feature-settings: 'liga';
$admonition-title
margin 0 -1.2rem
padding .8rem 1.2rem .8rem 4rem
border-bottom .1rem solid rgba(68,138,255,.1)
background-color rgba(68,138,255,.1)
font-weight 700
.admonition>.admonition-title:last-child
margin-bottom 0
admonition_types = {
note: {color: #0288D1, icon: "note"},
hint: {color: #009688, icon: "info_outline"},
danger: {color: #c2185b, icon: "block"},
caution: {color: #ffa726, icon: "warning"},
error: {color: #d32f2f, icon: "error_outline"},
attention: {color: #455a64, icon: "priority_high"}
}
for name, val in admonition_types
.admonition.{name}
@extend $admonition
border-left-color: val[color]
.admonition.{name}>.admonition-title
@extend $admonition-title
border-bottom-color: .1rem solid rgba(val[color], 0.2)
background-color: rgba(val[color], 0.2)
.admonition.{name}>.admonition-title:before
@extend $admonition-icon
color: val[color]
content: val[icon]
themeDarkBackground = darken(#21252B, 10%) themeDarkBackground = darken(#21252B, 10%)
themeDarkText = #f9f9f9 themeDarkText = #f9f9f9
themeDarkBorder = lighten(themeDarkBackground, 20%) themeDarkBorder = lighten(themeDarkBackground, 20%)
@@ -396,4 +474,6 @@ body[data-theme="monokai"]
td td
border-color themeMonokaiTableBorder border-color themeMonokaiTableBorder
&:last-child &:last-child
border-right solid 1px themeMonokaiTableBorder border-right solid 1px themeMonokaiTableBorder
kbd
background-color themeDarkBackground

View File

@@ -58,6 +58,9 @@ const languages = [
{ {
name: 'Spanish', name: 'Spanish',
locale: 'es-ES' locale: 'es-ES'
}, {
name: 'Turkish',
locale: 'tr'
} }
] ]

View File

@@ -12,6 +12,10 @@ const themes = fs.readdirSync(themePath)
}) })
themes.splice(themes.indexOf('solarized'), 1, 'solarized dark', 'solarized light') themes.splice(themes.indexOf('solarized'), 1, 'solarized dark', 'solarized light')
const snippetFile = process.env.NODE_ENV !== 'test'
? path.join(app.getPath('userData'), 'snippets.json')
: '' // return nothing as we specified different path to snippets.json in test
const consts = { const consts = {
FOLDER_COLORS: [ FOLDER_COLORS: [
'#E10051', '#E10051',
@@ -31,7 +35,16 @@ const consts = {
'Dodger Blue', 'Dodger Blue',
'Violet Eggplant' 'Violet Eggplant'
], ],
THEMES: ['default'].concat(themes) THEMES: ['default'].concat(themes),
SNIPPET_FILE: snippetFile,
DEFAULT_EDITOR_FONT_FAMILY: [
'Monaco',
'Menlo',
'Ubuntu Mono',
'Consolas',
'source-code-pro',
'monospace'
]
} }
module.exports = consts module.exports = consts

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 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'
import katex from 'katex' import katex from 'katex'
@@ -25,7 +26,7 @@ class Markdown {
linkify: true, linkify: true,
html: true, html: true,
xhtmlOut: true, xhtmlOut: true,
breaks: true, breaks: config.preview.breaks,
highlight: function (str, lang) { highlight: function (str, lang) {
const delimiter = ':' const delimiter = ':'
const langInfo = lang.split(delimiter) const langInfo = lang.split(delimiter)
@@ -141,6 +142,7 @@ class Markdown {
} }
}) })
this.md.use(require('markdown-it-kbd')) this.md.use(require('markdown-it-kbd'))
this.md.use(require('markdown-it-admonition'))
const deflate = require('markdown-it-plantuml/lib/deflate') const deflate = require('markdown-it-plantuml/lib/deflate')
this.md.use(require('markdown-it-plantuml'), '', { this.md.use(require('markdown-it-plantuml'), '', {
@@ -213,6 +215,10 @@ class Markdown {
return true return true
}) })
if (config.preview.smartArrows) {
this.md.use(smartArrows)
}
// Add line number attribute for scrolling // Add line number attribute for scrolling
const originalRender = this.md.renderer.render const originalRender = this.md.renderer.render
this.md.renderer.render = (tokens, options, env) => { this.md.renderer.render = (tokens, options, env) => {

0
browser/lib/markdown2.js Normal file
View File

View File

@@ -0,0 +1,9 @@
import consts from 'browser/lib/consts'
import isString from 'lodash/isString'
export default function normalizeEditorFontFamily (fontFamily) {
const defaultEditorFontFamily = consts.DEFAULT_EDITOR_FONT_FAMILY
return isString(fontFamily) && fontFamily.length > 0
? [fontFamily].concat(defaultEditorFontFamily).join(', ')
: defaultEditorFontFamily.join(', ')
}

View File

@@ -47,7 +47,25 @@ function escapeHtmlCharacters (html) {
return html return html
} }
export function isObjectEqual (a, b) {
const aProps = Object.getOwnPropertyNames(a)
const bProps = Object.getOwnPropertyNames(b)
if (aProps.length !== bProps.length) {
return false
}
for (var i = 0; i < aProps.length; i++) {
const propName = aProps[i]
if (a[propName] !== b[propName]) {
return false
}
}
return true
}
export default { export default {
lastFindInArray, lastFindInArray,
escapeHtmlCharacters escapeHtmlCharacters,
isObjectEqual
} }

View File

@@ -11,6 +11,7 @@
.control-infoButton-panel .control-infoButton-panel
z-index 200 z-index 200
margin-top 0px margin-top 0px
top: 50px
right 25px right 25px
position absolute position absolute
padding 20px 25px 0 25px padding 20px 25px 0 25px

View File

@@ -55,6 +55,10 @@ class MarkdownNoteDetail extends React.Component {
componentDidMount () { componentDidMount () {
ee.on('topbar:togglelockbutton', this.toggleLockButton) ee.on('topbar:togglelockbutton', this.toggleLockButton)
ee.on('topbar:togglemodebutton', () => {
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
this.handleSwitchMode(reversedType)
})
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
@@ -273,6 +277,7 @@ class MarkdownNoteDetail extends React.Component {
handleSwitchMode (type) { handleSwitchMode (type) {
this.setState({ editorType: type }, () => { this.setState({ editorType: type }, () => {
this.focus()
const newConfig = Object.assign({}, this.props.config) const newConfig = Object.assign({}, this.props.config)
newConfig.editor.type = type newConfig.editor.type = type
ConfigManager.set(newConfig) ConfigManager.set(newConfig)

View File

@@ -32,7 +32,7 @@ import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
const electron = require('electron') const electron = require('electron')
const { remote } = electron const { remote } = electron
const { Menu, MenuItem, dialog } = remote const { dialog } = remote
class SnippetNoteDetail extends React.Component { class SnippetNoteDetail extends React.Component {
constructor (props) { constructor (props) {
@@ -451,14 +451,14 @@ class SnippetNoteDetail extends React.Component {
} }
handleModeButtonClick (e, index) { handleModeButtonClick (e, index) {
const menu = new Menu() const templetes = []
CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => { CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => {
menu.append(new MenuItem({ templetes.push({
label: mode.name, label: mode.name,
click: (e) => this.handleModeOptionClick(index, mode.name)(e) click: (e) => this.handleModeOptionClick(index, mode.name)(e)
})) })
}) })
menu.popup(remote.getCurrentWindow()) context.popup(templetes)
} }
handleIndentTypeButtonClick (e) { handleIndentTypeButtonClick (e) {

View File

@@ -44,16 +44,9 @@ class TagSelect extends React.Component {
} }
removeLastTag () { removeLastTag () {
let { value } = this.props this.removeTagByCallback((value) => {
value.pop()
value = _.isArray(value) })
? value.slice()
: []
value.pop()
value = _.uniq(value)
this.value = value
this.props.onChange()
} }
reset () { reset () {
@@ -96,15 +89,22 @@ class TagSelect extends React.Component {
} }
handleTagRemoveButtonClick (tag) { handleTagRemoveButtonClick (tag) {
return (e) => { this.removeTagByCallback((value, tag) => {
let { value } = this.props
value.splice(value.indexOf(tag), 1) value.splice(value.indexOf(tag), 1)
value = _.uniq(value) }, tag)
}
this.value = value removeTagByCallback (callback, tag = null) {
this.props.onChange() let { value } = this.props
}
value = _.isArray(value)
? value.slice()
: []
callback(value, tag)
value = _.uniq(value)
this.value = value
this.props.onChange()
} }
render () { render () {
@@ -118,7 +118,7 @@ class TagSelect extends React.Component {
> >
<span styleName='tag-label'>#{tag}</span> <span styleName='tag-label'>#{tag}</span>
<button styleName='tag-removeButton' <button styleName='tag-removeButton'
onClick={(e) => this.handleTagRemoveButtonClick(tag)(e)} onClick={(e) => this.handleTagRemoveButtonClick(tag)}
> >
<img className='tag-removeButton-icon' src='../resources/icon/icon-x.svg' width='8px' /> <img className='tag-removeButton-icon' src='../resources/icon/icon-x.svg' width='8px' />
</button> </button>

View File

@@ -8,6 +8,7 @@ import SnippetNoteDetail from './SnippetNoteDetail'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import StatusBar from '../StatusBar' import StatusBar from '../StatusBar'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import debounceRender from 'react-debounce-render'
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
@@ -99,4 +100,4 @@ Detail.propTypes = {
ignorePreviewPointerEvents: PropTypes.bool ignorePreviewPointerEvents: PropTypes.bool
} }
export default CSSModules(Detail, styles) export default debounceRender(CSSModules(Detail, styles))

View File

@@ -16,6 +16,7 @@ import { hashHistory } from 'react-router'
import store from 'browser/main/store' import store from 'browser/main/store'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import { getLocales } from 'browser/lib/Languages' import { getLocales } from 'browser/lib/Languages'
import applyShortcuts from 'browser/main/lib/shortcutManager'
const path = require('path') const path = require('path')
const electron = require('electron') const electron = require('electron')
const { remote } = electron const { remote } = electron
@@ -159,7 +160,7 @@ class Main extends React.Component {
} else { } else {
i18n.setLocale('en') i18n.setLocale('en')
} }
applyShortcuts()
// Reload all data // Reload all data
dataApi.init() dataApi.init()
.then((data) => { .then((data) => {

View File

@@ -2,11 +2,13 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
import debounceRender from 'react-debounce-render'
import styles from './NoteList.styl' import styles from './NoteList.styl'
import moment from 'moment' import moment from 'moment'
import _ from 'lodash' import _ from 'lodash'
import ee from 'browser/main/lib/eventEmitter' import ee from 'browser/main/lib/eventEmitter'
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
import ConfigManager from 'browser/main/lib/ConfigManager' import ConfigManager from 'browser/main/lib/ConfigManager'
import NoteItem from 'browser/components/NoteItem' import NoteItem from 'browser/components/NoteItem'
import NoteItemSimple from 'browser/components/NoteItemSimple' import NoteItemSimple from 'browser/components/NoteItemSimple'
@@ -19,9 +21,10 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
import Markdown from '../../lib/markdown' import Markdown from '../../lib/markdown'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote' import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
import context from 'browser/lib/context'
const { remote } = require('electron') const { remote } = require('electron')
const { Menu, MenuItem, dialog } = remote const { dialog } = remote
const WP_POST_PATH = '/wp/v2/posts' const WP_POST_PATH = '/wp/v2/posts'
function sortByCreatedAt (a, b) { function sortByCreatedAt (a, b) {
@@ -489,55 +492,51 @@ class NoteList extends React.Component {
const updateLabel = i18n.__('Update Blog') const updateLabel = i18n.__('Update Blog')
const openBlogLabel = i18n.__('Open Blog') const openBlogLabel = i18n.__('Open Blog')
const menu = new Menu() const templates = []
if (location.pathname.match(/\/trash/)) { if (location.pathname.match(/\/trash/)) {
menu.append(new MenuItem({ templates.push({
label: restoreNote, label: restoreNote,
click: this.restoreNote click: this.restoreNote
})) }, {
menu.append(new MenuItem({
label: deleteLabel, label: deleteLabel,
click: this.deleteNote click: this.deleteNote
})) })
} else { } else {
if (!location.pathname.match(/\/starred/)) { if (!location.pathname.match(/\/starred/)) {
menu.append(new MenuItem({ templates.push({
label: pinLabel, label: pinLabel,
click: this.pinToTop click: this.pinToTop
})) })
} }
menu.append(new MenuItem({ templates.push({
label: deleteLabel, label: deleteLabel,
click: this.deleteNote click: this.deleteNote
})) }, {
menu.append(new MenuItem({
label: cloneNote, label: cloneNote,
click: this.cloneNote.bind(this) click: this.cloneNote.bind(this)
})) }, {
menu.append(new MenuItem({
label: copyNoteLink, label: copyNoteLink,
click: this.copyNoteLink(note) click: this.copyNoteLink(note)
})) })
if (note.type === 'MARKDOWN_NOTE') { if (note.type === 'MARKDOWN_NOTE') {
if (note.blog && note.blog.blogLink && note.blog.blogId) { if (note.blog && note.blog.blogLink && note.blog.blogId) {
menu.append(new MenuItem({ templates.push({
label: updateLabel, label: updateLabel,
click: this.publishMarkdown.bind(this) click: this.publishMarkdown.bind(this)
})) }, {
menu.append(new MenuItem({
label: openBlogLabel, label: openBlogLabel,
click: () => this.openBlog.bind(this)(note) click: () => this.openBlog.bind(this)(note)
})) })
} else { } else {
menu.append(new MenuItem({ templates.push({
label: publishLabel, label: publishLabel,
click: this.publishMarkdown.bind(this) click: this.publishMarkdown.bind(this)
})) })
} }
} }
} }
menu.popup() context.popup(templates)
} }
updateSelectedNotes (updateFunc, cleanSelection = true) { updateSelectedNotes (updateFunc, cleanSelection = true) {
@@ -662,6 +661,10 @@ class NoteList extends React.Component {
title: firstNote.title + ' ' + i18n.__('copy'), title: firstNote.title + ' ' + i18n.__('copy'),
content: firstNote.content content: firstNote.content
}) })
.then((note) => {
attachmentManagement.cloneAttachments(firstNote, note)
return note
})
.then((note) => { .then((note) => {
dispatch({ dispatch({
type: 'UPDATE_NOTE', type: 'UPDATE_NOTE',
@@ -943,15 +946,24 @@ class NoteList extends React.Component {
const viewType = this.getViewType() const viewType = this.getViewType()
const autoSelectFirst =
notes.length === 1 ||
selectedNoteKeys.length === 0 ||
notes.every(note => !selectedNoteKeys.includes(note.key))
const noteList = notes const noteList = notes
.map(note => { .map((note, index) => {
if (note == null) { if (note == null) {
return null return null
} }
const isDefault = config.listStyle === 'DEFAULT' const isDefault = config.listStyle === 'DEFAULT'
const uniqueKey = getNoteKey(note) const uniqueKey = getNoteKey(note)
const isActive = selectedNoteKeys.includes(uniqueKey)
const isActive =
selectedNoteKeys.includes(uniqueKey) ||
notes.length === 1 ||
(autoSelectFirst && index === 0)
const dateDisplay = moment( const dateDisplay = moment(
config.sortBy === 'CREATED_AT' config.sortBy === 'CREATED_AT'
? note.createdAt : note.updatedAt ? note.createdAt : note.updatedAt
@@ -1053,4 +1065,4 @@ NoteList.propTypes = {
}) })
} }
export default CSSModules(NoteList, styles) export default debounceRender(CSSModules(NoteList, styles))

View File

@@ -48,4 +48,5 @@ body[data-theme="dark"]
line-height normal line-height normal
border-radius 2px border-radius 2px
opacity 0 opacity 0
transition 0.1s transition 0.1s
white-space nowrap

View File

@@ -11,21 +11,26 @@ import StorageItemChild from 'browser/components/StorageItem'
import _ from 'lodash' import _ from 'lodash'
import { SortableElement } from 'react-sortable-hoc' import { SortableElement } from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
const { remote } = require('electron') const { remote } = require('electron')
const { Menu, dialog } = remote const { dialog } = remote
const escapeStringRegexp = require('escape-string-regexp')
const path = require('path')
class StorageItem extends React.Component { class StorageItem extends React.Component {
constructor (props) { constructor (props) {
super(props) super(props)
const { storage } = this.props
this.state = { this.state = {
isOpen: true isOpen: !!storage.isOpen
} }
} }
handleHeaderContextMenu (e) { handleHeaderContextMenu (e) {
const menu = Menu.buildFromTemplate([ context.popup([
{ {
label: i18n.__('Add Folder'), label: i18n.__('Add Folder'),
click: (e) => this.handleAddFolderButtonClick(e) click: (e) => this.handleAddFolderButtonClick(e)
@@ -38,8 +43,6 @@ class StorageItem extends React.Component {
click: (e) => this.handleUnlinkStorageClick(e) click: (e) => this.handleUnlinkStorageClick(e)
} }
]) ])
menu.popup()
} }
handleUnlinkStorageClick (e) { handleUnlinkStorageClick (e) {
@@ -66,8 +69,18 @@ class StorageItem extends React.Component {
} }
handleToggleButtonClick (e) { handleToggleButtonClick (e) {
const { storage, dispatch } = this.props
const isOpen = !this.state.isOpen
dataApi.toggleStorage(storage.key, isOpen)
.then((storage) => {
dispatch({
type: 'EXPAND_STORAGE',
storage,
isOpen
})
})
this.setState({ this.setState({
isOpen: !this.state.isOpen isOpen: isOpen
}) })
} }
@@ -92,7 +105,7 @@ class StorageItem extends React.Component {
} }
handleFolderButtonContextMenu (e, folder) { handleFolderButtonContextMenu (e, folder) {
const menu = Menu.buildFromTemplate([ context.popup([
{ {
label: i18n.__('Rename Folder'), label: i18n.__('Rename Folder'),
click: (e) => this.handleRenameFolderClick(e, folder) click: (e) => this.handleRenameFolderClick(e, folder)
@@ -121,8 +134,6 @@ class StorageItem extends React.Component {
click: (e) => this.handleFolderDeleteClick(e, folder) click: (e) => this.handleFolderDeleteClick(e, folder)
} }
]) ])
menu.popup()
} }
handleRenameFolderClick (e, folder) { handleRenameFolderClick (e, folder) {
@@ -201,7 +212,7 @@ class StorageItem extends React.Component {
createdNoteData.forEach((newNote) => { createdNoteData.forEach((newNote) => {
dispatch({ dispatch({
type: 'MOVE_NOTE', type: 'MOVE_NOTE',
originNote: noteData.find((note) => note.content === newNote.content), originNote: noteData.find((note) => note.content === newNote.oldContent),
note: newNote note: newNote
}) })
}) })
@@ -223,7 +234,8 @@ class StorageItem extends React.Component {
const { folderNoteMap, trashedSet } = data const { folderNoteMap, trashedSet } = data
const SortableStorageItemChild = SortableElement(StorageItemChild) const SortableStorageItemChild = SortableElement(StorageItemChild)
const folderList = storage.folders.map((folder, index) => { const folderList = storage.folders.map((folder, index) => {
const isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key))) let folderRegex = new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + escapeStringRegexp(path.sep) + 'folders' + escapeStringRegexp(path.sep) + folder.key)
const isActive = !!(location.pathname.match(folderRegex))
const noteSet = folderNoteMap.get(storage.key + '-' + folder.key) const noteSet = folderNoteMap.get(storage.key + '-' + folder.key)
let noteCount = 0 let noteCount = 0
@@ -253,7 +265,7 @@ class StorageItem extends React.Component {
) )
}) })
const isActive = location.pathname.match(new RegExp('\/storages\/' + storage.key + '$')) const isActive = location.pathname.match(new RegExp(escapeStringRegexp(path.sep) + 'storages' + escapeStringRegexp(path.sep) + storage.key + '$'))
return ( return (
<div styleName={isFolded ? 'root--folded' : 'root'} <div styleName={isFolded ? 'root--folded' : 'root'}

View File

@@ -44,7 +44,7 @@
height 36px height 36px
padding-left 25px padding-left 25px
padding-right 15px padding-right 15px
line-height 22px line-height 36px
cursor pointer cursor pointer
font-size 14px font-size 14px
border none border none
@@ -147,7 +147,7 @@ body[data-theme="dark"]
background-color $ui-dark-button--active-backgroundColor background-color $ui-dark-button--active-backgroundColor
&:active &:active
color $ui-dark-text-color color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor background-color $ui-dark-button--active-backgroundColor
.header--active .header--active
.header-addFolderButton .header-addFolderButton
@@ -180,7 +180,7 @@ body[data-theme="dark"]
&:active, &:active:hover &:active, &:active:hover
color $ui-dark-text-color color $ui-dark-text-color
background-color $ui-dark-button--active-backgroundColor background-color $ui-dark-button--active-backgroundColor

View File

@@ -29,6 +29,7 @@
border-radius 2px border-radius 2px
opacity 0 opacity 0
transition 0.1s transition 0.1s
white-space nowrap
body[data-theme="white"] body[data-theme="white"]
.non-active-button .non-active-button

View File

@@ -1,8 +1,6 @@
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import React from 'react' import React from 'react'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
const { remote } = require('electron')
const { Menu } = remote
import dataApi from 'browser/main/lib/dataApi' import dataApi from 'browser/main/lib/dataApi'
import styles from './SideNav.styl' import styles from './SideNav.styl'
import { openModal } from 'browser/main/lib/modal' import { openModal } from 'browser/main/lib/modal'
@@ -19,6 +17,7 @@ import ListButton from './ListButton'
import TagButton from './TagButton' import TagButton from './TagButton'
import {SortableContainer} from 'react-sortable-hoc' import {SortableContainer} from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
class SideNav extends React.Component { class SideNav extends React.Component {
// TODO: should not use electron stuff v0.7 // TODO: should not use electron stuff v0.7
@@ -185,7 +184,7 @@ class SideNav extends React.Component {
).filter( ).filter(
note => activeTags.every(tag => note.tags.includes(tag)) note => activeTags.every(tag => note.tags.includes(tag))
) )
let relatedTags = new Set() const relatedTags = new Set()
relatedNotes.forEach(note => note.tags.map(tag => relatedTags.add(tag))) relatedNotes.forEach(note => note.tags.map(tag => relatedTags.add(tag)))
return relatedTags return relatedTags
} }
@@ -224,7 +223,7 @@ class SideNav extends React.Component {
handleClickNarrowToTag (tag) { handleClickNarrowToTag (tag) {
const { router } = this.context const { router } = this.context
const { location } = this.props const { location } = this.props
let listOfTags = this.getActiveTags(location.pathname) const listOfTags = this.getActiveTags(location.pathname)
const indexOfTag = listOfTags.indexOf(tag) const indexOfTag = listOfTags.indexOf(tag)
if (indexOfTag > -1) { if (indexOfTag > -1) {
listOfTags.splice(indexOfTag, 1) listOfTags.splice(indexOfTag, 1)
@@ -254,10 +253,9 @@ class SideNav extends React.Component {
handleFilterButtonContextMenu (event) { handleFilterButtonContextMenu (event) {
const { data } = this.props const { data } = this.props
const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey)) const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
const menu = Menu.buildFromTemplate([ context.popup([
{ label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) } { label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) }
]) ])
menu.popup()
} }
render () { render () {

View File

@@ -4,10 +4,11 @@ import CSSModules from 'browser/lib/CSSModules'
import styles from './StatusBar.styl' import styles from './StatusBar.styl'
import ZoomManager from 'browser/main/lib/ZoomManager' import ZoomManager from 'browser/main/lib/ZoomManager'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
const electron = require('electron') const electron = require('electron')
const { remote, ipcRenderer } = electron const { remote, ipcRenderer } = electron
const { Menu, MenuItem, dialog } = remote const { dialog } = remote
const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0] const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
@@ -26,16 +27,16 @@ class StatusBar extends React.Component {
} }
handleZoomButtonClick (e) { handleZoomButtonClick (e) {
const menu = new Menu() const templates = []
zoomOptions.forEach((zoom) => { zoomOptions.forEach((zoom) => {
menu.append(new MenuItem({ templates.push({
label: Math.floor(zoom * 100) + '%', label: Math.floor(zoom * 100) + '%',
click: () => this.handleZoomMenuItemClick(zoom) click: () => this.handleZoomMenuItemClick(zoom)
})) })
}) })
menu.popup(remote.getCurrentWindow()) context.popup(templates)
} }
handleZoomMenuItemClick (zoomFactor) { handleZoomMenuItemClick (zoomFactor) {

View File

@@ -1,6 +1,7 @@
import _ from 'lodash' import _ from 'lodash'
import RcParser from 'browser/lib/RcParser' import RcParser from 'browser/lib/RcParser'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import ee from 'browser/main/lib/eventEmitter'
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
const win = global.process.platform === 'win32' const win = global.process.platform === 'win32'
@@ -20,7 +21,8 @@ export const DEFAULT_CONFIG = {
listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL' listStyle: 'DEFAULT', // 'DEFAULT', 'SMALL'
amaEnabled: true, amaEnabled: true,
hotkey: { hotkey: {
toggleMain: OSX ? 'Cmd + Alt + L' : 'Super + Alt + E' toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
toggleMode: OSX ? 'Command + M' : 'Ctrl + M'
}, },
ui: { ui: {
language: 'en', language: 'en',
@@ -56,6 +58,10 @@ export const DEFAULT_CONFIG = {
plantUMLServerAddress: 'http://www.plantuml.com/plantuml', plantUMLServerAddress: 'http://www.plantuml.com/plantuml',
scrollPastEnd: false, scrollPastEnd: false,
smartQuotes: true, smartQuotes: true,
breaks: true,
smartArrows: false,
allowCustomCSS: false,
customCSS: '',
sanitize: 'STRICT' // 'STRICT', 'ALLOW_STYLES', 'NONE' sanitize: 'STRICT' // 'STRICT', 'ALLOW_STYLES', 'NONE'
}, },
blog: { blog: {
@@ -166,6 +172,7 @@ function set (updates) {
ipcRenderer.send('config-renew', { ipcRenderer.send('config-renew', {
config: get() config: get()
}) })
ee.emit('config-renew')
} }
function assignConfigValues (originalConfig, rcConfig) { function assignConfigValues (originalConfig, rcConfig) {
@@ -175,6 +182,17 @@ function assignConfigValues (originalConfig, rcConfig) {
config.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui) config.ui = Object.assign({}, DEFAULT_CONFIG.ui, originalConfig.ui, rcConfig.ui)
config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor) config.editor = Object.assign({}, DEFAULT_CONFIG.editor, originalConfig.editor, rcConfig.editor)
config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview) config.preview = Object.assign({}, DEFAULT_CONFIG.preview, originalConfig.preview, rcConfig.preview)
rewriteHotkey(config)
return config
}
function rewriteHotkey (config) {
const keys = [...Object.keys(config.hotkey)]
keys.forEach(key => {
config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command')
})
return config return config
} }

View File

@@ -37,7 +37,8 @@ function addStorage (input) {
key, key,
name: input.name, name: input.name,
type: input.type, type: input.type,
path: input.path path: input.path,
isOpen: false
} }
return Promise.resolve(newStorage) return Promise.resolve(newStorage)
@@ -48,7 +49,8 @@ function addStorage (input) {
key: newStorage.key, key: newStorage.key,
type: newStorage.type, type: newStorage.type,
name: newStorage.name, name: newStorage.name,
path: newStorage.path path: newStorage.path,
isOpen: false
}) })
localStorage.setItem('storages', JSON.stringify(rawStorages)) localStorage.setItem('storages', JSON.stringify(rawStorages))

View File

@@ -3,7 +3,10 @@ const fs = require('fs')
const path = require('path') const path = require('path')
const findStorage = require('browser/lib/findStorage') const findStorage = require('browser/lib/findStorage')
const mdurl = require('mdurl') const mdurl = require('mdurl')
const fse = require('fs-extra')
const escapeStringRegexp = require('escape-string-regexp') const escapeStringRegexp = require('escape-string-regexp')
const sander = require('sander')
import i18n from 'browser/lib/i18n'
const STORAGE_FOLDER_PLACEHOLDER = ':storage' const STORAGE_FOLDER_PLACEHOLDER = ':storage'
const DESTINATION_FOLDER = 'attachments' const DESTINATION_FOLDER = 'attachments'
@@ -40,7 +43,7 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr
const targetStorage = findStorage.findStorage(storageKey) const targetStorage = findStorage.findStorage(storageKey)
const inputFile = fs.createReadStream(sourceFilePath) const inputFileStream = fs.createReadStream(sourceFilePath)
let destinationName let destinationName
if (useRandomName) { if (useRandomName) {
destinationName = `${uniqueSlug()}${path.extname(sourceFilePath)}` destinationName = `${uniqueSlug()}${path.extname(sourceFilePath)}`
@@ -50,8 +53,10 @@ function copyAttachment (sourceFilePath, storageKey, noteKey, useRandomName = tr
const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) const destinationDir = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
createAttachmentDestinationFolder(targetStorage.path, noteKey) createAttachmentDestinationFolder(targetStorage.path, noteKey)
const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName)) const outputFile = fs.createWriteStream(path.join(destinationDir, destinationName))
inputFile.pipe(outputFile) inputFileStream.pipe(outputFile)
resolve(destinationName) inputFileStream.on('end', () => {
resolve(destinationName)
})
} catch (e) { } catch (e) {
return reject(e) return reject(e)
} }
@@ -69,6 +74,31 @@ function createAttachmentDestinationFolder (destinationStoragePath, noteKey) {
} }
} }
/**
* @description Moves attachments from the old location ('/images') to the new one ('/attachments/noteKey)
* @param renderedHTML HTML of the current note
* @param storagePath Storage path of the current note
* @param noteKey Key of the current note
*/
function migrateAttachments (renderedHTML, storagePath, noteKey) {
if (sander.existsSync(path.join(storagePath, 'images'))) {
const attachments = getAttachmentsInContent(renderedHTML) || []
if (attachments !== []) {
createAttachmentDestinationFolder(storagePath, noteKey)
}
for (const attachment of attachments) {
const attachmentBaseName = path.basename(attachment)
const possibleLegacyPath = path.join(storagePath, 'images', attachmentBaseName)
if (sander.existsSync(possibleLegacyPath)) {
const destinationPath = path.join(storagePath, DESTINATION_FOLDER, attachmentBaseName)
if (!sander.existsSync(destinationPath)) {
sander.copyFileSync(possibleLegacyPath).to(destinationPath)
}
}
}
}
}
/** /**
* @description Fixes the URLs embedded in the generated HTML so that they again refer actual local files. * @description Fixes the URLs embedded in the generated HTML so that they again refer actual local files.
* @param {String} renderedHTML HTML in that the links should be fixed * @param {String} renderedHTML HTML in that the links should be fixed
@@ -76,7 +106,7 @@ function createAttachmentDestinationFolder (destinationStoragePath, 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) {
return renderedHTML.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER)) return renderedHTML.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
} }
/** /**
@@ -147,8 +177,9 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
base64data = reader.result.replace(/^data:image\/png;base64,/, '') base64data = reader.result.replace(/^data:image\/png;base64,/, '')
base64data += base64data.replace('+', ' ') base64data += base64data.replace('+', ' ')
const binaryData = new Buffer(base64data, 'base64').toString('binary') const binaryData = new Buffer(base64data, 'base64').toString('binary')
fs.writeFile(imagePath, binaryData, 'binary') fs.writeFileSync(imagePath, binaryData, 'binary')
const imageMd = generateAttachmentMarkdown(imageName, imagePath, true) const imageReferencePath = path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, imageName)
const imageMd = generateAttachmentMarkdown(imageName, imageReferencePath, true)
codeEditor.insertAttachmentMd(imageMd) codeEditor.insertAttachmentMd(imageMd)
} }
reader.readAsDataURL(blob) reader.readAsDataURL(blob)
@@ -161,7 +192,7 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
*/ */
function getAttachmentsInContent (markdownContent) { function getAttachmentsInContent (markdownContent) {
const preparedInput = markdownContent.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep) const preparedInput = markdownContent.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep)
const regexp = new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + '([a-zA-Z0-9]|-)+' + escapeStringRegexp(path.sep) + '[a-zA-Z0-9]+(\\.[a-zA-Z0-9]+)?', 'g') const regexp = new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + '|/)' + '?([a-zA-Z0-9]|-)*' + '(' + escapeStringRegexp(path.sep) + '|/)' + '([a-zA-Z0-9]|\\.)+(\\.[a-zA-Z0-9]+)?', 'g')
return preparedInput.match(regexp) return preparedInput.match(regexp)
} }
@@ -180,6 +211,39 @@ function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) {
return result return result
} }
/**
* @description Moves the attachments of the current note to the new location.
* Returns a modified version of the given content so that the links to the attachments point to the new note key.
* @param {String} oldPath Source of the note to be moved
* @param {String} newPath Destination of the note to be moved
* @param {String} noteKey Old note key
* @param {String} newNoteKey New note key
* @param {String} noteContent Content of the note to be moved
* @returns {String} Modified version of noteContent in which the paths of the attachments are fixed
*/
function moveAttachments (oldPath, newPath, noteKey, newNoteKey, noteContent) {
const src = path.join(oldPath, DESTINATION_FOLDER, noteKey)
const dest = path.join(newPath, DESTINATION_FOLDER, newNoteKey)
if (fse.existsSync(src)) {
fse.moveSync(src, dest)
}
return replaceNoteKeyWithNewNoteKey(noteContent, noteKey, newNoteKey)
}
/**
* Modifies the given content so that in all attachment references the oldNoteKey is replaced by the new one
* @param noteContent content that should be modified
* @param oldNoteKey note key to be replaced
* @param newNoteKey note key serving as a replacement
* @returns {String} modified note content
*/
function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) {
if (noteContent) {
return noteContent.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey, 'g'), path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey))
}
return noteContent
}
/** /**
* @description Deletes all :storage and noteKey references from the given input. * @description Deletes all :storage and noteKey references from the given input.
* @param input Input in which the references should be deleted * @param input Input in which the references should be deleted
@@ -187,7 +251,18 @@ function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) {
* @returns {String} Input without the references * @returns {String} Input without the references
*/ */
function removeStorageAndNoteReferences (input, noteKey) { function removeStorageAndNoteReferences (input, noteKey) {
return input.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey, 'g'), DESTINATION_FOLDER) return input.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + '(' + escapeStringRegexp(path.sep) + noteKey + ')?', 'g'), DESTINATION_FOLDER)
}
/**
* @description Deletes the attachment folder specified by the given storageKey and noteKey
* @param storageKey Key of the storage of the note to be deleted
* @param noteKey Key of the note to be deleted
*/
function deleteAttachmentFolder (storageKey, noteKey) {
const storagePath = findStorage.findStorage(storageKey)
const noteAttachmentPath = path.join(storagePath.path, DESTINATION_FOLDER, noteKey)
sander.rimrafSync(noteAttachmentPath)
} }
/** /**
@@ -197,6 +272,9 @@ function removeStorageAndNoteReferences (input, noteKey) {
* @param noteKey NoteKey of the current note. Is used to determine the belonging attachment folder. * @param noteKey NoteKey of the current note. Is used to determine the belonging attachment folder.
*/ */
function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey) { function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey) {
if (storageKey == null || noteKey == null || markdownContent == null) {
return
}
const targetStorage = findStorage.findStorage(storageKey) const targetStorage = findStorage.findStorage(storageKey)
const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey) const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
const attachmentsInNote = getAttachmentsInContent(markdownContent) const attachmentsInNote = getAttachmentsInContent(markdownContent)
@@ -206,11 +284,10 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey
attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), '')) attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), ''))
} }
} }
if (fs.existsSync(attachmentFolder)) { if (fs.existsSync(attachmentFolder)) {
fs.readdir(attachmentFolder, (err, files) => { fs.readdir(attachmentFolder, (err, files) => {
if (err) { if (err) {
console.error("Error reading directory '" + attachmentFolder + "'. Error:") console.error('Error reading directory "' + attachmentFolder + '". Error:')
console.error(err) console.error(err)
return return
} }
@@ -219,17 +296,109 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey
const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file) const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file)
fs.unlink(absolutePathOfFile, (err) => { fs.unlink(absolutePathOfFile, (err) => {
if (err) { if (err) {
console.error("Could not delete '%s'", absolutePathOfFile) console.error('Could not delete "%s"', absolutePathOfFile)
console.error(err) console.error(err)
return return
} }
console.info("File '" + absolutePathOfFile + "' deleted because it was not included in the content of the note") console.info('File "' + absolutePathOfFile + '" deleted because it was not included in the content of the note')
}) })
} }
}) })
}) })
} else { } else {
console.info("Attachment folder ('" + attachmentFolder + "') did not exist..") console.info('Attachment folder ("' + attachmentFolder + '") did not exist..')
}
}
/**
* 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.
* @param oldNote Note that is being cloned
* @param newNote Clone of the note
*/
function cloneAttachments (oldNote, newNote) {
if (newNote.type === 'MARKDOWN_NOTE') {
const oldStorage = findStorage.findStorage(oldNote.storage)
const newStorage = findStorage.findStorage(newNote.storage)
const attachmentsPaths = getAbsolutePathsOfAttachmentsInContent(oldNote.content, oldStorage.path) || []
const destinationFolder = path.join(newStorage.path, DESTINATION_FOLDER, newNote.key)
if (!sander.existsSync(destinationFolder)) {
sander.mkdirSync(destinationFolder)
}
for (const attachment of attachmentsPaths) {
const destination = path.join(newStorage.path, DESTINATION_FOLDER, newNote.key, path.basename(attachment))
sander.copyFileSync(attachment).to(destination)
}
newNote.content = replaceNoteKeyWithNewNoteKey(newNote.content, oldNote.key, newNote.key)
} else {
console.debug('Cloning of the attachment was skipped since it only works for MARKDOWN_NOTEs')
}
}
function generateFileNotFoundMarkdown () {
return '**' + i18n.__('⚠ 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! ⚠') + '**'
}
/**
* Determines whether a given text is a link to an boostnote attachment
* @param text Text that might contain a attachment link
* @return {Boolean} Result of the test
*/
function isAttachmentLink (text) {
if (text) {
return text.match(new RegExp('.*\\[.*\\]\\( *' + escapeStringRegexp(STORAGE_FOLDER_PLACEHOLDER) + escapeStringRegexp(path.sep) + '.*\\).*', 'gi')) != null
}
return false
}
/**
* @description Handles the paste of an attachment link. Copies the referenced attachment to the location belonging to the new note.
* Returns a modified version of the pasted text so that it matches the copied attachment (resp. the new location)
* @param storageKey StorageKey of the current note
* @param noteKey NoteKey of the currentNote
* @param linkText Text that was pasted
* @return {Promise<String>} Promise returning the modified text
*/
function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
if (storageKey != null && noteKey != null && linkText != null) {
const storagePath = findStorage.findStorage(storageKey).path
const attachments = getAttachmentsInContent(linkText) || []
const replaceInstructions = []
const copies = []
for (const attachment of attachments) {
const absPathOfAttachment = attachment.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER))
copies.push(
sander.exists(absPathOfAttachment)
.then((fileExists) => {
if (!fileExists) {
const fileNotFoundRegexp = new RegExp('!?' + escapeStringRegexp('[') + '[\\w|\\d|\\s|\\.]*\\]\\(\\s*' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + escapeStringRegexp(path.sep) + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + escapeStringRegexp(')'))
replaceInstructions.push({regexp: fileNotFoundRegexp, replacement: this.generateFileNotFoundMarkdown()})
return Promise.resolve()
}
return this.copyAttachment(absPathOfAttachment, storageKey, noteKey)
.then((fileName) => {
const replaceLinkRegExp = new RegExp(escapeStringRegexp('(') + ' *' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + escapeStringRegexp(path.sep) + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + ' *' + escapeStringRegexp(')'))
replaceInstructions.push({
regexp: replaceLinkRegExp,
replacement: '(' + path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName) + ')'
})
return Promise.resolve()
})
})
)
}
return Promise.all(copies).then(() => {
let modifiedLinkText = linkText
for (const replaceInstruction of replaceInstructions) {
modifiedLinkText = modifiedLinkText.replace(replaceInstruction.regexp, replaceInstruction.replacement)
}
return modifiedLinkText
})
} else {
console.log('One if the parameters was null -> Do nothing..')
return Promise.resolve(linkText)
} }
} }
@@ -242,7 +411,14 @@ module.exports = {
getAttachmentsInContent, getAttachmentsInContent,
getAbsolutePathsOfAttachmentsInContent, getAbsolutePathsOfAttachmentsInContent,
removeStorageAndNoteReferences, removeStorageAndNoteReferences,
deleteAttachmentFolder,
deleteAttachmentsNotPresentInNote, deleteAttachmentsNotPresentInNote,
moveAttachments,
cloneAttachments,
isAttachmentLink,
handleAttachmentLinkPaste,
generateFileNotFoundMarkdown,
migrateAttachments,
STORAGE_FOLDER_PLACEHOLDER, STORAGE_FOLDER_PLACEHOLDER,
DESTINATION_FOLDER DESTINATION_FOLDER
} }

View File

@@ -1,38 +0,0 @@
const fs = require('fs')
const path = require('path')
const { findStorage } = require('browser/lib/findStorage')
// TODO: ehhc: delete this
/**
* @description Copy an image and return the path.
* @param {String} filePath
* @param {String} storageKey
* @param {Boolean} rename create new filename or leave the old one
* @return {Promise<any>} an image path
*/
function copyImage (filePath, storageKey, rename = true) {
return new Promise((resolve, reject) => {
try {
const targetStorage = findStorage(storageKey)
const inputImage = fs.createReadStream(filePath)
const imageExt = path.extname(filePath)
const imageName = rename ? Math.random().toString(36).slice(-16) : path.basename(filePath, imageExt)
const basename = `${imageName}${imageExt}`
const imageDir = path.join(targetStorage.path, 'images')
if (!fs.existsSync(imageDir)) fs.mkdirSync(imageDir)
const outputImage = fs.createWriteStream(path.join(imageDir, basename))
outputImage.on('error', reject)
inputImage.on('error', reject)
inputImage.on('end', () => {
resolve(basename)
})
inputImage.pipe(outputImage)
} catch (e) {
return reject(e)
}
})
}
module.exports = copyImage

View File

@@ -0,0 +1,26 @@
import fs from 'fs'
import crypto from 'crypto'
import consts from 'browser/lib/consts'
import fetchSnippet from 'browser/main/lib/dataApi/fetchSnippet'
function createSnippet (snippetFile) {
return new Promise((resolve, reject) => {
const newSnippet = {
id: crypto.randomBytes(16).toString('hex'),
name: 'Unnamed snippet',
prefix: [],
content: ''
}
fetchSnippet(null, snippetFile).then((snippets) => {
snippets.push(newSnippet)
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
if (err) reject(err)
resolve(newSnippet)
})
}).catch((err) => {
reject(err)
})
})
}
module.exports = createSnippet

View File

@@ -5,6 +5,7 @@ const resolveStorageNotes = require('./resolveStorageNotes')
const CSON = require('@rokt33r/season') const CSON = require('@rokt33r/season')
const sander = require('sander') const sander = require('sander')
const { findStorage } = require('browser/lib/findStorage') const { findStorage } = require('browser/lib/findStorage')
const deleteSingleNote = require('./deleteNote')
/** /**
* @param {String} storageKey * @param {String} storageKey
@@ -49,11 +50,7 @@ function deleteFolder (storageKey, folderKey) {
const deleteAllNotes = targetNotes const deleteAllNotes = targetNotes
.map(function deleteNote (note) { .map(function deleteNote (note) {
const notePath = path.join(storage.path, 'notes', note.key + '.cson') return deleteSingleNote(storageKey, note.key)
return sander.unlink(notePath)
.catch(function (err) {
console.warn('Failed to delete', notePath, err)
})
}) })
return Promise.all(deleteAllNotes) return Promise.all(deleteAllNotes)
.then(() => storage) .then(() => storage)

View File

@@ -1,6 +1,7 @@
const resolveStorageData = require('./resolveStorageData') const resolveStorageData = require('./resolveStorageData')
const path = require('path') const path = require('path')
const sander = require('sander') const sander = require('sander')
const attachmentManagement = require('./attachmentManagement')
const { findStorage } = require('browser/lib/findStorage') const { findStorage } = require('browser/lib/findStorage')
function deleteNote (storageKey, noteKey) { function deleteNote (storageKey, noteKey) {
@@ -25,6 +26,10 @@ function deleteNote (storageKey, noteKey) {
storageKey storageKey
} }
}) })
.then(function deleteAttachments (storageInfo) {
attachmentManagement.deleteAttachmentFolder(storageInfo.storageKey, storageInfo.noteKey)
return storageInfo
})
} }
module.exports = deleteNote module.exports = deleteNote

View File

@@ -0,0 +1,17 @@
import fs from 'fs'
import consts from 'browser/lib/consts'
import fetchSnippet from 'browser/main/lib/dataApi/fetchSnippet'
function deleteSnippet (snippet, snippetFile) {
return new Promise((resolve, reject) => {
fetchSnippet(null, snippetFile).then((snippets) => {
snippets = snippets.filter(currentSnippet => currentSnippet.id !== snippet.id)
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
if (err) reject(err)
resolve(snippet)
})
})
})
}
module.exports = deleteSnippet

View File

@@ -0,0 +1,20 @@
import fs from 'fs'
import consts from 'browser/lib/consts'
function fetchSnippet (id, snippetFile) {
return new Promise((resolve, reject) => {
fs.readFile(snippetFile || consts.SNIPPET_FILE, 'utf8', (err, data) => {
if (err) {
reject(err)
}
const snippets = JSON.parse(data)
if (id) {
const snippet = snippets.find(snippet => { return snippet.id === id })
resolve(snippet)
}
resolve(snippets)
})
})
}
module.exports = fetchSnippet

View File

@@ -1,5 +1,6 @@
const dataApi = { const dataApi = {
init: require('./init'), init: require('./init'),
toggleStorage: require('./toggleStorage'),
addStorage: require('./addStorage'), addStorage: require('./addStorage'),
renameStorage: require('./renameStorage'), renameStorage: require('./renameStorage'),
removeStorage: require('./removeStorage'), removeStorage: require('./removeStorage'),
@@ -13,6 +14,10 @@ const dataApi = {
deleteNote: require('./deleteNote'), deleteNote: require('./deleteNote'),
moveNote: require('./moveNote'), moveNote: require('./moveNote'),
migrateFromV5Storage: require('./migrateFromV5Storage'), migrateFromV5Storage: require('./migrateFromV5Storage'),
createSnippet: require('./createSnippet'),
deleteSnippet: require('./deleteSnippet'),
updateSnippet: require('./updateSnippet'),
fetchSnippet: require('./fetchSnippet'),
_migrateFromV6Storage: require('./migrateFromV6Storage'), _migrateFromV6Storage: require('./migrateFromV6Storage'),
_resolveStorageData: require('./resolveStorageData'), _resolveStorageData: require('./resolveStorageData'),

View File

@@ -6,6 +6,7 @@ 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')
const { findStorage } = require('browser/lib/findStorage') const { findStorage } = require('browser/lib/findStorage')
const attachmentManagement = require('./attachmentManagement')
function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) { function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
let oldStorage, newStorage let oldStorage, newStorage
@@ -63,36 +64,20 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) {
noteData.key = newNoteKey noteData.key = newNoteKey
noteData.storage = newStorageKey noteData.storage = newStorageKey
noteData.updatedAt = new Date() noteData.updatedAt = new Date()
noteData.oldContent = noteData.content
return noteData return noteData
}) })
.then(function moveImages (noteData) { .then(function moveAttachments (noteData) {
if (oldStorage.path === newStorage.path) return noteData if (oldStorage.path === newStorage.path) {
return noteData
const searchImagesRegex = /!\[.*\]\(:storage\/(.+)\)/gi
let match = searchImagesRegex.exec(noteData.content)
const moveTasks = []
while (match != null) {
const [, filename] = match
const oldPath = path.join(oldStorage.path, 'attachments', filename)
const newPath = path.join(newStorage.path, 'attachments', filename)
// TODO: ehhc: attachmentManagement
moveTasks.push(
sander.copyFile(oldPath).to(newPath)
.then(() => {
fs.unlinkSync(oldPath)
})
)
// find next occurence
match = searchImagesRegex.exec(noteData.content)
} }
return Promise.all(moveTasks).then(() => noteData) noteData.content = attachmentManagement.moveAttachments(oldStorage.path, newStorage.path, noteKey, newNoteKey, noteData.content)
return noteData
}) })
.then(function writeAndReturn (noteData) { .then(function writeAndReturn (noteData) {
CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage'])) CSON.writeFileSync(path.join(newStorage.path, 'notes', noteData.key + '.cson'), _.omit(noteData, ['key', 'storage', 'oldContent']))
return noteData return noteData
}) })
.then(function deleteOldNote (data) { .then(function deleteOldNote (data) {

View File

@@ -8,7 +8,8 @@ function resolveStorageData (storageCache) {
key: storageCache.key, key: storageCache.key,
name: storageCache.name, name: storageCache.name,
type: storageCache.type, type: storageCache.type,
path: storageCache.path path: storageCache.path,
isOpen: storageCache.isOpen
} }
const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json') const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json')

View File

@@ -0,0 +1,28 @@
const _ = require('lodash')
const resolveStorageData = require('./resolveStorageData')
/**
* @param {String} key
* @param {Boolean} isOpen
* @return {Object} Storage meta data
*/
function toggleStorage (key, isOpen) {
let cachedStorageList
try {
cachedStorageList = JSON.parse(localStorage.getItem('storages'))
if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
} catch (err) {
console.log('error got')
console.error(err)
return Promise.reject(err)
}
const targetStorage = _.find(cachedStorageList, {key: key})
if (targetStorage == null) return Promise.reject('Storage')
targetStorage.isOpen = isOpen
localStorage.setItem('storages', JSON.stringify(cachedStorageList))
return resolveStorageData(targetStorage)
}
module.exports = toggleStorage

View File

@@ -0,0 +1,33 @@
import fs from 'fs'
import consts from 'browser/lib/consts'
function updateSnippet (snippet, snippetFile) {
return new Promise((resolve, reject) => {
const snippets = JSON.parse(fs.readFileSync(snippetFile || consts.SNIPPET_FILE, 'utf-8'))
for (let i = 0; i < snippets.length; i++) {
const currentSnippet = snippets[i]
if (currentSnippet.id === snippet.id) {
if (
currentSnippet.name === snippet.name &&
currentSnippet.prefix === snippet.prefix &&
currentSnippet.content === snippet.content
) {
// if everything is the same then don't write to disk
resolve(snippets)
} else {
currentSnippet.name = snippet.name
currentSnippet.prefix = snippet.prefix
currentSnippet.content = snippet.content
fs.writeFile(snippetFile || consts.SNIPPET_FILE, JSON.stringify(snippets, null, 4), (err) => {
if (err) reject(err)
resolve(snippets)
})
}
}
}
})
}
module.exports = updateSnippet

View File

@@ -0,0 +1,7 @@
import ee from 'browser/main/lib/eventEmitter'
module.exports = {
'toggleMode': () => {
ee.emit('topbar:togglemodebutton')
}
}

View File

@@ -0,0 +1,40 @@
import Mousetrap from 'mousetrap'
import CM from 'browser/main/lib/ConfigManager'
import ee from 'browser/main/lib/eventEmitter'
import { isObjectEqual } from 'browser/lib/utils'
require('mousetrap-global-bind')
import functions from './shortcut'
let shortcuts = CM.get().hotkey
ee.on('config-renew', function () {
// only update if hotkey changed !
const newHotkey = CM.get().hotkey
if (!isObjectEqual(newHotkey, shortcuts)) {
updateShortcut(newHotkey)
}
})
function updateShortcut (newHotkey) {
Mousetrap.reset()
shortcuts = newHotkey
applyShortcuts(newHotkey)
}
function formatShortcut (shortcut) {
return shortcut.toLowerCase().replace(/ /g, '')
}
function applyShortcuts (shortcuts) {
for (const shortcut in shortcuts) {
const toggler = formatShortcut(shortcuts[shortcut])
// only bind if the function for that shortcut exists
if (functions[shortcut]) {
Mousetrap.bindGlobal(toggler, functions[shortcut])
}
}
}
applyShortcuts(CM.get().hotkey)
module.exports = applyShortcuts

View File

@@ -11,11 +11,12 @@ p
font-size 16px font-size 16px
.cf-link .cf-link
width 250px
height 35px height 35px
border-radius 2px border-radius 2px
border none border none
background-color alpha(#1EC38B, 90%) background-color alpha(#1EC38B, 90%)
padding-left 20px
padding-right 20px
&:hover &:hover
background-color #1EC38B background-color #1EC38B
transition 0.2s transition 0.2s

View File

@@ -67,7 +67,8 @@ class HotkeyTab extends React.Component {
handleHotkeyChange (e) { handleHotkeyChange (e) {
const { config } = this.state const { config } = this.state
config.hotkey = { config.hotkey = {
toggleMain: this.refs.toggleMain.value toggleMain: this.refs.toggleMain.value,
toggleMode: this.refs.toggleMode.value
} }
this.setState({ this.setState({
config config
@@ -115,6 +116,17 @@ class HotkeyTab extends React.Component {
/> />
</div> </div>
</div> </div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Toggle editor mode')}</div>
<div styleName='group-section-control'>
<input styleName='group-section-control-input'
onChange={(e) => this.handleHotkeyChange(e)}
ref='toggleMode'
value={config.hotkey.toggleMode}
type='text'
/>
</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

@@ -0,0 +1,90 @@
import CodeMirror from 'codemirror'
import React from 'react'
import _ from 'lodash'
import styles from './SnippetTab.styl'
import CSSModules from 'browser/lib/CSSModules'
import dataApi from 'browser/main/lib/dataApi'
const defaultEditorFontFamily = ['Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', 'monospace']
const buildCMRulers = (rulers, enableRulers) =>
enableRulers ? rulers.map(ruler => ({ column: ruler })) : []
class SnippetEditor extends React.Component {
componentDidMount () {
this.props.onRef(this)
const { rulers, enableRulers } = this.props
this.cm = CodeMirror(this.refs.root, {
rulers: buildCMRulers(rulers, enableRulers),
lineNumbers: this.props.displayLineNumbers,
lineWrapping: true,
theme: this.props.theme,
indentUnit: this.props.indentSize,
tabSize: this.props.indentSize,
indentWithTabs: this.props.indentType !== 'space',
keyMap: this.props.keyMap,
scrollPastEnd: this.props.scrollPastEnd,
dragDrop: false,
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
autoCloseBrackets: true,
mode: 'null'
})
this.cm.setSize('100%', '100%')
let changeDelay = null
this.cm.on('change', () => {
this.snippet.content = this.cm.getValue()
clearTimeout(changeDelay)
changeDelay = setTimeout(() => {
this.saveSnippet()
}, 500)
})
}
componentWillUnmount () {
this.props.onRef(undefined)
}
onSnippetChanged (newSnippet) {
this.snippet = newSnippet
this.cm.setValue(this.snippet.content)
}
onSnippetNameOrPrefixChanged (newSnippet) {
this.snippet.name = newSnippet.name
this.snippet.prefix = newSnippet.prefix.toString().replace(/\s/g, '').split(',')
this.saveSnippet()
}
saveSnippet () {
dataApi.updateSnippet(this.snippet).catch((err) => { throw err })
}
render () {
const { fontSize } = this.props
let fontFamily = this.props.fontFamily
fontFamily = _.isString(fontFamily) && fontFamily.length > 0
? [fontFamily].concat(defaultEditorFontFamily)
: defaultEditorFontFamily
return (
<div styleName='SnippetEditor' ref='root' tabIndex='-1' style={{
fontFamily: fontFamily.join(', '),
fontSize: fontSize
}} />
)
}
}
SnippetEditor.defaultProps = {
readOnly: false,
theme: 'xcode',
keyMap: 'sublime',
fontSize: 14,
fontFamily: 'Monaco, Consolas',
indentSize: 4,
indentType: 'space'
}
export default CSSModules(SnippetEditor, styles)

View File

@@ -0,0 +1,95 @@
import React from 'react'
import styles from './SnippetTab.styl'
import CSSModules from 'browser/lib/CSSModules'
import dataApi from 'browser/main/lib/dataApi'
import i18n from 'browser/lib/i18n'
import eventEmitter from 'browser/main/lib/eventEmitter'
import context from 'browser/lib/context'
class SnippetList extends React.Component {
constructor (props) {
super(props)
this.state = {
snippets: []
}
}
componentDidMount () {
this.reloadSnippetList()
eventEmitter.on('snippetList:reload', this.reloadSnippetList.bind(this))
}
reloadSnippetList () {
dataApi.fetchSnippet().then(snippets => {
this.setState({snippets})
this.props.onSnippetSelect(snippets[0])
})
}
handleSnippetContextMenu (snippet) {
context.popup([{
label: i18n.__('Delete snippet'),
click: () => this.deleteSnippet(snippet)
}])
}
deleteSnippet (snippet) {
dataApi.deleteSnippet(snippet).then(() => {
this.reloadSnippetList()
this.props.onSnippetDeleted(snippet)
}).catch(err => { throw err })
}
handleSnippetClick (snippet) {
this.props.onSnippetSelect(snippet)
}
createSnippet () {
dataApi.createSnippet().then(() => {
this.reloadSnippetList()
// scroll to end of list when added new snippet
const snippetList = document.getElementById('snippets')
snippetList.scrollTop = snippetList.scrollHeight
}).catch(err => { throw err })
}
defineSnippetStyleName (snippet) {
const { currentSnippet } = this.props
if (currentSnippet == null) return
if (currentSnippet.id === snippet.id) {
return 'snippet-item-selected'
} else {
return 'snippet-item'
}
}
render () {
const { snippets } = this.state
return (
<div styleName='snippet-list'>
<div styleName='group-section'>
<div styleName='group-section-control'>
<button styleName='group-control-button' onClick={() => this.createSnippet()}>
<i className='fa fa-plus' /> {i18n.__('New Snippet')}
</button>
</div>
</div>
<ul id='snippets' styleName='snippets'>
{
snippets.map((snippet) => (
<li
styleName={this.defineSnippetStyleName(snippet)}
key={snippet.id}
onContextMenu={() => this.handleSnippetContextMenu(snippet)}
onClick={() => this.handleSnippetClick(snippet)}>
{snippet.name}
</li>
))
}
</ul>
</div>
)
}
}
export default CSSModules(SnippetList, styles)

View File

@@ -0,0 +1,117 @@
import React from 'react'
import CSSModules from 'browser/lib/CSSModules'
import styles from './SnippetTab.styl'
import SnippetEditor from './SnippetEditor'
import i18n from 'browser/lib/i18n'
import dataApi from 'browser/main/lib/dataApi'
import SnippetList from './SnippetList'
import eventEmitter from 'browser/main/lib/eventEmitter'
class SnippetTab extends React.Component {
constructor (props) {
super(props)
this.state = {
currentSnippet: null
}
this.changeDelay = null
}
handleSnippetNameOrPrefixChange () {
clearTimeout(this.changeDelay)
this.changeDelay = setTimeout(() => {
// notify the snippet editor that the name or prefix of snippet has been changed
this.snippetEditor.onSnippetNameOrPrefixChanged(this.state.currentSnippet)
eventEmitter.emit('snippetList:reload')
}, 500)
}
handleSnippetSelect (snippet) {
const { currentSnippet } = this.state
if (currentSnippet === null || currentSnippet.id !== snippet.id) {
dataApi.fetchSnippet(snippet.id).then(changedSnippet => {
// notify the snippet editor to load the content of the new snippet
this.snippetEditor.onSnippetChanged(changedSnippet)
this.setState({currentSnippet: changedSnippet})
})
}
}
onSnippetNameOrPrefixChanged (e, type) {
const newSnippet = Object.assign({}, this.state.currentSnippet)
if (type === 'name') {
newSnippet.name = e.target.value
} else {
newSnippet.prefix = e.target.value
}
this.setState({ currentSnippet: newSnippet })
this.handleSnippetNameOrPrefixChange()
}
handleDeleteSnippet (snippet) {
// prevent old snippet still display when deleted
if (snippet.id === this.state.currentSnippet.id) {
this.setState({currentSnippet: null})
}
}
render () {
const { config, storageKey } = this.props
const { currentSnippet } = this.state
let editorFontSize = parseInt(config.editor.fontSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 101)) editorFontSize = 14
let editorIndentSize = parseInt(config.editor.indentSize, 10)
if (!(editorFontSize > 0 && editorFontSize < 132)) editorIndentSize = 4
return (
<div styleName='root'>
<div styleName='header'>{i18n.__('Snippets')}</div>
<SnippetList
onSnippetSelect={this.handleSnippetSelect.bind(this)}
onSnippetDeleted={this.handleDeleteSnippet.bind(this)}
currentSnippet={currentSnippet} />
<div styleName='snippet-detail' style={{visibility: currentSnippet ? 'visible' : 'hidden'}}>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Snippet name')}</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
value={currentSnippet ? currentSnippet.name : ''}
onChange={e => { this.onSnippetNameOrPrefixChanged(e, 'name') }}
type='text' />
</div>
</div>
<div styleName='group-section'>
<div styleName='group-section-label'>{i18n.__('Snippet prefix')}</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
value={currentSnippet ? currentSnippet.prefix : ''}
onChange={e => { this.onSnippetNameOrPrefixChanged(e, 'prefix') }}
type='text' />
</div>
</div>
<div styleName='snippet-editor-section'>
<SnippetEditor
storageKey={storageKey}
theme={config.editor.theme}
keyMap={config.editor.keyMap}
fontFamily={config.editor.fontFamily}
fontSize={editorFontSize}
indentType={config.editor.indentType}
indentSize={editorIndentSize}
enableRulers={config.editor.enableRulers}
rulers={config.editor.rulers}
displayLineNumbers={config.editor.displayLineNumbers}
scrollPastEnd={config.editor.scrollPastEnd}
onRef={ref => { this.snippetEditor = ref }} />
</div>
</div>
</div>
)
}
}
SnippetTab.PropTypes = {
}
export default CSSModules(SnippetTab, styles)

View File

@@ -0,0 +1,198 @@
@import('./Tab')
@import('./ConfigTab')
.root
padding 15px
white-space pre
line-height 1.4
color alpha($ui-text-color, 90%)
width 100%
font-size 14px
.group
margin-bottom 45px
.group-header
@extend .header
color $ui-text-color
.group-header2
font-size 20px
color $ui-text-color
margin-bottom 15px
margin-top 30px
.group-section
margin-bottom 20px
display flex
line-height 30px
.group-section-label
width 150px
text-align left
margin-right 10px
font-size 14px
.group-section-control
flex 1
margin-left 5px
.group-section-control select
outline none
border 1px solid $ui-borderColor
font-size 16px
height 30px
width 250px
margin-bottom 5px
background-color transparent
.group-section-control-input
height 30px
vertical-align middle
width 400px
font-size $tab--button-font-size
border solid 1px $border-color
border-radius 2px
padding 0 5px
outline none
&:disabled
background-color $ui-input--disabled-backgroundColor
.group-control-button
height 30px
border none
border-top-right-radius 2px
border-bottom-right-radius 2px
colorPrimaryButton()
vertical-align middle
padding 0 20px
.group-checkBoxSection
margin-bottom 15px
display flex
line-height 30px
padding-left 15px
.group-control
padding-top 10px
box-sizing border-box
height 40px
text-align right
:global
.alert
display inline-block
position absolute
top 60px
right 15px
font-size 14px
.success
color #1EC38B
.error
color red
.warning
color #FFA500
.snippet-list
width 30%
height calc(100% - 200px)
position absolute
.snippets
height calc(100% - 8px)
overflow scroll
background: #f5f5f5
.snippet-item
height 50px
font-size 15px
line-height 50px
padding 0 5%
cursor pointer
position relative
&::after
width 90%
height 1px
background rgba(0, 0, 0, 0.1)
position absolute
top 100%
left 5%
content ''
&:hover
background darken(#f5f5f5, 5)
.snippet-item-selected
@extend .snippet-list .snippet-item
background darken(#f5f5f5, 5)
.snippet-detail
width 70%
height calc(100% - 200px)
position absolute
left 33%
.SnippetEditor
position absolute
width 100%
height 90%
body[data-theme="default"], body[data-theme="white"]
.snippets
background $ui-backgroundColor
.snippet-item
color black
&::after
background $ui-borderColor
&:hover
background darken($ui-backgroundColor, 5)
.snippet-item-selected
background darken($ui-backgroundColor, 5)
body[data-theme="dark"]
.snippets
background $ui-dark-backgroundColor
.snippet-item
color white
&::after
background $ui-dark-borderColor
&:hover
background darken($ui-dark-backgroundColor, 5)
.snippet-item-selected
background darken($ui-dark-backgroundColor, 5)
.snippet-detail
color white
.group-control-button
colorDarkPrimaryButton()
body[data-theme="solarized-dark"]
.snippets
background $ui-solarized-dark-backgroundColor
.snippet-item
color white
&::after
background $ui-solarized-dark-borderColor
&:hover
background darken($ui-solarized-dark-backgroundColor, 5)
.snippet-item-selected
background darken($ui-solarized-dark-backgroundColor, 5)
.snippet-detail
color white
.group-control-button
colorSolarizedDarkPrimaryButton()
body[data-theme="monokai"]
.snippets
background $ui-monokai-backgroundColor
.snippet-item
color White
&::after
background $ui-monokai-borderColor
&:hover
background darken($ui-monokai-backgroundColor, 5)
.snippet-item-selected
background darken($ui-monokai-backgroundColor, 5)
.snippet-detail
color white
.group-control-button
colorMonokaiPrimaryButton()

View File

@@ -182,7 +182,7 @@ class StoragesTab extends React.Component {
<div styleName='addStorage-body-section-path'> <div styleName='addStorage-body-section-path'>
<input styleName='addStorage-body-section-path-input' <input styleName='addStorage-body-section-path-input'
ref='addStoragePath' ref='addStoragePath'
placeholder='Select Folder' placeholder={i18n.__('Select Folder')}
value={this.state.newStorage.path} value={this.state.newStorage.path}
onChange={(e) => this.handleAddStorageChange(e)} onChange={(e) => this.handleAddStorageChange(e)}
/> />

View File

@@ -11,6 +11,7 @@ import 'codemirror-mode-elixir'
import _ from 'lodash' import _ from 'lodash'
import i18n from 'browser/lib/i18n' import i18n from 'browser/lib/i18n'
import { getLanguages } from 'browser/lib/Languages' import { getLanguages } from 'browser/lib/Languages'
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
const OSX = global.process.platform === 'darwin' const OSX = global.process.platform === 'darwin'
@@ -28,6 +29,8 @@ class UiTab extends React.Component {
componentDidMount () { componentDidMount () {
CodeMirror.autoLoadMode(this.codeMirrorInstance.getCodeMirror(), 'javascript') CodeMirror.autoLoadMode(this.codeMirrorInstance.getCodeMirror(), 'javascript')
CodeMirror.autoLoadMode(this.customCSSCM.getCodeMirror(), 'css')
this.customCSSCM.getCodeMirror().setSize('400px', '400px')
this.handleSettingDone = () => { this.handleSettingDone = () => {
this.setState({UiAlert: { this.setState({UiAlert: {
type: 'success', type: 'success',
@@ -97,7 +100,11 @@ class UiTab extends React.Component {
plantUMLServerAddress: this.refs.previewPlantUMLServerAddress.value, plantUMLServerAddress: this.refs.previewPlantUMLServerAddress.value,
scrollPastEnd: this.refs.previewScrollPastEnd.checked, scrollPastEnd: this.refs.previewScrollPastEnd.checked,
smartQuotes: this.refs.previewSmartQuotes.checked, smartQuotes: this.refs.previewSmartQuotes.checked,
sanitize: this.refs.previewSanitize.value breaks: this.refs.previewBreaks.checked,
smartArrows: this.refs.previewSmartArrows.checked,
sanitize: this.refs.previewSanitize.value,
allowCustomCSS: this.refs.previewAllowCustomCSS.checked,
customCSS: this.customCSSCM.getCodeMirror().getValue()
} }
} }
@@ -158,6 +165,7 @@ class UiTab extends React.Component {
const { config, codemirrorTheme } = this.state const { config, codemirrorTheme } = this.state
const codemirrorSampleCode = 'function iamHappy (happy) {\n\tif (happy) {\n\t console.log("I am Happy!")\n\t} else {\n\t console.log("I am not Happy!")\n\t}\n};' const codemirrorSampleCode = 'function iamHappy (happy) {\n\tif (happy) {\n\t console.log("I am Happy!")\n\t} else {\n\t console.log("I am not Happy!")\n\t}\n};'
const enableEditRulersStyle = config.editor.enableRulers ? 'block' : 'none' const enableEditRulersStyle = config.editor.enableRulers ? 'block' : 'none'
const fontFamily = normalizeEditorFontFamily(config.editor.fontFamily)
return ( return (
<div styleName='root'> <div styleName='root'>
<div styleName='group'> <div styleName='group'>
@@ -233,7 +241,7 @@ class UiTab extends React.Component {
disabled={OSX} disabled={OSX}
type='checkbox' type='checkbox'
/>&nbsp; />&nbsp;
Disable Direct Write(It will be applied after restarting) {i18n.__('Disable Direct Write (It will be applied after restarting)')}
</label> </label>
</div> </div>
: null : null
@@ -255,8 +263,16 @@ class UiTab extends React.Component {
}) })
} }
</select> </select>
<div styleName='code-mirror'> <div styleName='code-mirror' style={{fontFamily}}>
<ReactCodeMirror ref={e => (this.codeMirrorInstance = e)} value={codemirrorSampleCode} options={{ lineNumbers: true, readOnly: true, mode: 'javascript', theme: codemirrorTheme }} /> <ReactCodeMirror
ref={e => (this.codeMirrorInstance = e)}
value={codemirrorSampleCode}
options={{
lineNumbers: true,
readOnly: true,
mode: 'javascript',
theme: codemirrorTheme
}} />
</div> </div>
</div> </div>
</div> </div>
@@ -473,7 +489,27 @@ class UiTab extends React.Component {
ref='previewSmartQuotes' ref='previewSmartQuotes'
type='checkbox' type='checkbox'
/>&nbsp; />&nbsp;
Enable smart quotes {i18n.__('Enable smart quotes')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.preview.breaks}
ref='previewBreaks'
type='checkbox'
/>&nbsp;
{i18n.__('Render newlines in Markdown paragraphs as <br>')}
</label>
</div>
<div styleName='group-checkBoxSection'>
<label>
<input onChange={(e) => this.handleUIChange(e)}
checked={this.state.config.preview.smartArrows}
ref='previewSmartArrows'
type='checkbox'
/>&nbsp;
{i18n.__('Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.')}
</label> </label>
</div> </div>
@@ -558,6 +594,32 @@ class UiTab extends React.Component {
/> />
</div> </div>
</div> </div>
<div styleName='group-section'>
<div styleName='group-section-label'>
{i18n.__('Custom CSS')}
</div>
<div styleName='group-section-control'>
<input onChange={(e) => this.handleUIChange(e)}
checked={config.preview.allowCustomCSS}
ref='previewAllowCustomCSS'
type='checkbox'
/>&nbsp;
{i18n.__('Allow custom CSS for preview')}
<div style={{fontFamily}}>
<ReactCodeMirror
width='400px'
height='400px'
onChange={e => this.handleUIChange(e)}
ref={e => (this.customCSSCM = e)}
value={config.preview.customCSS}
options={{
lineNumbers: true,
mode: 'css',
theme: codemirrorTheme
}} />
</div>
</div>
</div>
<div styleName='group-control'> <div styleName='group-control'>
<button styleName='group-control-rightButton' <button styleName='group-control-rightButton'

View File

@@ -6,6 +6,7 @@ import UiTab from './UiTab'
import InfoTab from './InfoTab' import InfoTab from './InfoTab'
import Crowdfunding from './Crowdfunding' import Crowdfunding from './Crowdfunding'
import StoragesTab from './StoragesTab' import StoragesTab from './StoragesTab'
import SnippetTab from './SnippetTab'
import Blog from './Blog' import Blog from './Blog'
import ModalEscButton from 'browser/components/ModalEscButton' import ModalEscButton from 'browser/components/ModalEscButton'
import CSSModules from 'browser/lib/CSSModules' import CSSModules from 'browser/lib/CSSModules'
@@ -86,6 +87,14 @@ class Preferences extends React.Component {
haveToSave={alert => this.setState({BlogAlert: alert})} haveToSave={alert => this.setState({BlogAlert: alert})}
/> />
) )
case 'SNIPPET':
return (
<SnippetTab
dispatch={dispatch}
config={config}
data={data}
/>
)
case 'STORAGES': case 'STORAGES':
default: default:
return ( return (
@@ -123,7 +132,8 @@ class Preferences extends React.Component {
{target: 'UI', label: i18n.__('Interface'), UI: this.state.UIAlert}, {target: 'UI', label: i18n.__('Interface'), UI: this.state.UIAlert},
{target: 'INFO', label: i18n.__('About')}, {target: 'INFO', label: i18n.__('About')},
{target: 'CROWDFUNDING', label: i18n.__('Crowdfunding')}, {target: 'CROWDFUNDING', label: i18n.__('Crowdfunding')},
{target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert} {target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert},
{target: 'SNIPPET', label: i18n.__('Snippets')}
] ]
const navButtons = tabs.map((tab) => { const navButtons = tabs.map((tab) => {

View File

@@ -38,29 +38,13 @@ function data (state = defaultDataMap(), action) {
if (note.isTrashed) { if (note.isTrashed) {
state.trashedSet.add(uniqueKey) state.trashedSet.add(uniqueKey)
} }
const storageNoteList = getOrInitItem(state.storageNoteMap, note.storage)
let storageNoteList = state.storageNoteMap.get(note.storage)
if (storageNoteList == null) {
storageNoteList = new Set(storageNoteList)
state.storageNoteMap.set(note.storage, storageNoteList)
}
storageNoteList.add(uniqueKey) storageNoteList.add(uniqueKey)
let folderNoteSet = state.folderNoteMap.get(folderKey) const folderNoteSet = getOrInitItem(state.folderNoteMap, folderKey)
if (folderNoteSet == null) {
folderNoteSet = new Set(folderNoteSet)
state.folderNoteMap.set(folderKey, folderNoteSet)
}
folderNoteSet.add(uniqueKey) folderNoteSet.add(uniqueKey)
note.tags.forEach((tag) => { assignToTags(note.tags, state, uniqueKey)
let tagNoteList = state.tagNoteMap.get(tag)
if (tagNoteList == null) {
tagNoteList = new Set(tagNoteList)
state.tagNoteMap.set(tag, tagNoteList)
}
tagNoteList.add(uniqueKey)
})
}) })
return state return state
case 'UPDATE_NOTE': case 'UPDATE_NOTE':
@@ -74,40 +58,18 @@ function data (state = defaultDataMap(), action) {
state.noteMap = new Map(state.noteMap) state.noteMap = new Map(state.noteMap)
state.noteMap.set(uniqueKey, note) state.noteMap.set(uniqueKey, note)
if (oldNote == null || oldNote.isStarred !== note.isStarred) { updateStarredChange(oldNote, note, state, uniqueKey)
state.starredSet = new Set(state.starredSet)
if (note.isStarred) {
state.starredSet.add(uniqueKey)
} else {
state.starredSet.delete(uniqueKey)
}
}
if (oldNote == null || oldNote.isTrashed !== note.isTrashed) { if (oldNote == null || oldNote.isTrashed !== note.isTrashed) {
state.trashedSet = new Set(state.trashedSet) state.trashedSet = new Set(state.trashedSet)
if (note.isTrashed) { if (note.isTrashed) {
state.trashedSet.add(uniqueKey) state.trashedSet.add(uniqueKey)
state.starredSet.delete(uniqueKey) state.starredSet.delete(uniqueKey)
removeFromTags(note.tags, state, uniqueKey)
note.tags.forEach(tag => {
let tagNoteList = state.tagNoteMap.get(tag)
if (tagNoteList != null) {
tagNoteList = new Set(tagNoteList)
tagNoteList.delete(uniqueKey)
state.tagNoteMap.set(tag, tagNoteList)
}
})
} else { } else {
state.trashedSet.delete(uniqueKey) state.trashedSet.delete(uniqueKey)
note.tags.forEach(tag => { assignToTags(note.tags, state, uniqueKey)
let tagNoteList = state.tagNoteMap.get(tag)
if (tagNoteList != null) {
tagNoteList = new Set(tagNoteList)
tagNoteList.add(uniqueKey)
state.tagNoteMap.set(tag, tagNoteList)
}
})
if (note.isStarred) { if (note.isStarred) {
state.starredSet.add(uniqueKey) state.starredSet.add(uniqueKey)
@@ -125,54 +87,12 @@ function data (state = defaultDataMap(), action) {
} }
// Update foldermap if folder changed or post created // Update foldermap if folder changed or post created
if (oldNote == null || oldNote.folder !== note.folder) { updateFolderChange(oldNote, note, state, folderKey, uniqueKey)
state.folderNoteMap = new Map(state.folderNoteMap)
let folderNoteSet = state.folderNoteMap.get(folderKey)
folderNoteSet = new Set(folderNoteSet)
folderNoteSet.add(uniqueKey)
state.folderNoteMap.set(folderKey, folderNoteSet)
if (oldNote != null) {
const oldFolderKey = oldNote.storage + '-' + oldNote.folder
let oldFolderNoteList = state.folderNoteMap.get(oldFolderKey)
oldFolderNoteList = new Set(oldFolderNoteList)
oldFolderNoteList.delete(uniqueKey)
state.folderNoteMap.set(oldFolderKey, oldFolderNoteList)
}
}
if (oldNote != null) { if (oldNote != null) {
const discardedTags = _.difference(oldNote.tags, note.tags) updateTagChanges(oldNote, note, state, uniqueKey)
const addedTags = _.difference(note.tags, oldNote.tags)
if (discardedTags.length + addedTags.length > 0) {
state.tagNoteMap = new Map(state.tagNoteMap)
discardedTags.forEach((tag) => {
let tagNoteList = state.tagNoteMap.get(tag)
if (tagNoteList != null) {
tagNoteList = new Set(tagNoteList)
tagNoteList.delete(uniqueKey)
state.tagNoteMap.set(tag, tagNoteList)
}
})
addedTags.forEach((tag) => {
let tagNoteList = state.tagNoteMap.get(tag)
tagNoteList = new Set(tagNoteList)
tagNoteList.add(uniqueKey)
state.tagNoteMap.set(tag, tagNoteList)
})
}
} else { } else {
state.tagNoteMap = new Map(state.tagNoteMap) assignToTags(note.tags, state, uniqueKey)
note.tags.forEach((tag) => {
let tagNoteList = state.tagNoteMap.get(tag)
if (tagNoteList == null) {
tagNoteList = new Set(tagNoteList)
state.tagNoteMap.set(tag, tagNoteList)
}
tagNoteList.add(uniqueKey)
})
} }
return state return state
@@ -220,26 +140,10 @@ function data (state = defaultDataMap(), action) {
originFolderList.delete(originKey) originFolderList.delete(originKey)
state.folderNoteMap.set(originFolderKey, originFolderList) state.folderNoteMap.set(originFolderKey, originFolderList)
// From tagMap removeFromTags(originNote.tags, state, originKey)
if (originNote.tags.length > 0) {
state.tagNoteMap = new Map(state.tagNoteMap)
originNote.tags.forEach((tag) => {
let noteSet = state.tagNoteMap.get(tag)
noteSet = new Set(noteSet)
noteSet.delete(originKey)
state.tagNoteMap.set(tag, noteSet)
})
}
} }
if (oldNote == null || oldNote.isStarred !== note.isStarred) { updateStarredChange(oldNote, note, state, uniqueKey)
state.starredSet = new Set(state.starredSet)
if (note.isStarred) {
state.starredSet.add(uniqueKey)
} else {
state.starredSet.delete(uniqueKey)
}
}
if (oldNote == null || oldNote.isTrashed !== note.isTrashed) { if (oldNote == null || oldNote.isTrashed !== note.isTrashed) {
state.trashedSet = new Set(state.trashedSet) state.trashedSet = new Set(state.trashedSet)
@@ -260,55 +164,13 @@ function data (state = defaultDataMap(), action) {
} }
// Update foldermap if folder changed or post created // Update foldermap if folder changed or post created
if (oldNote == null || oldNote.folder !== note.folder) { updateFolderChange(oldNote, note, state, folderKey, uniqueKey)
state.folderNoteMap = new Map(state.folderNoteMap)
let folderNoteList = state.folderNoteMap.get(folderKey)
folderNoteList = new Set(folderNoteList)
folderNoteList.add(uniqueKey)
state.folderNoteMap.set(folderKey, folderNoteList)
if (oldNote != null) {
const oldFolderKey = oldNote.storage + '-' + oldNote.folder
let oldFolderNoteList = state.folderNoteMap.get(oldFolderKey)
oldFolderNoteList = new Set(oldFolderNoteList)
oldFolderNoteList.delete(uniqueKey)
state.folderNoteMap.set(oldFolderKey, oldFolderNoteList)
}
}
// Remove from old folder map // Remove from old folder map
if (oldNote != null) { if (oldNote != null) {
const discardedTags = _.difference(oldNote.tags, note.tags) updateTagChanges(oldNote, note, state, uniqueKey)
const addedTags = _.difference(note.tags, oldNote.tags)
if (discardedTags.length + addedTags.length > 0) {
state.tagNoteMap = new Map(state.tagNoteMap)
discardedTags.forEach((tag) => {
let tagNoteList = state.tagNoteMap.get(tag)
if (tagNoteList != null) {
tagNoteList = new Set(tagNoteList)
tagNoteList.delete(uniqueKey)
state.tagNoteMap.set(tag, tagNoteList)
}
})
addedTags.forEach((tag) => {
let tagNoteList = state.tagNoteMap.get(tag)
tagNoteList = new Set(tagNoteList)
tagNoteList.add(uniqueKey)
state.tagNoteMap.set(tag, tagNoteList)
})
}
} else { } else {
state.tagNoteMap = new Map(state.tagNoteMap) assignToTags(note.tags, state, uniqueKey)
note.tags.forEach((tag) => {
let tagNoteList = state.tagNoteMap.get(tag)
if (tagNoteList == null) {
tagNoteList = new Set(tagNoteList)
state.tagNoteMap.set(tag, tagNoteList)
}
tagNoteList.add(uniqueKey)
})
} }
return state return state
@@ -347,16 +209,7 @@ function data (state = defaultDataMap(), action) {
folderSet.delete(uniqueKey) folderSet.delete(uniqueKey)
state.folderNoteMap.set(folderKey, folderSet) state.folderNoteMap.set(folderKey, folderSet)
// From tagMap removeFromTags(targetNote.tags, state, uniqueKey)
if (targetNote.tags.length > 0) {
state.tagNoteMap = new Map(state.tagNoteMap)
targetNote.tags.forEach((tag) => {
let noteSet = state.tagNoteMap.get(tag)
noteSet = new Set(noteSet)
noteSet.delete(uniqueKey)
state.tagNoteMap.set(tag, noteSet)
})
}
} }
state.noteMap = new Map(state.noteMap) state.noteMap = new Map(state.noteMap)
state.noteMap.delete(uniqueKey) state.noteMap.delete(uniqueKey)
@@ -420,9 +273,7 @@ function data (state = defaultDataMap(), action) {
// Delete key from tag map // Delete key from tag map
state.tagNoteMap = new Map(state.tagNoteMap) state.tagNoteMap = new Map(state.tagNoteMap)
note.tags.forEach((tag) => { note.tags.forEach((tag) => {
let tagNoteSet = state.tagNoteMap.get(tag) const tagNoteSet = getOrInitItem(state.tagNoteMap, tag)
tagNoteSet = new Set(tagNoteSet)
state.tagNoteMap.set(tag, tagNoteSet)
tagNoteSet.delete(noteKey) tagNoteSet.delete(noteKey)
}) })
} }
@@ -449,11 +300,7 @@ function data (state = defaultDataMap(), action) {
state.starredSet.add(uniqueKey) state.starredSet.add(uniqueKey)
} }
let storageNoteList = state.storageNoteMap.get(note.storage) const storageNoteList = getOrInitItem(state.tagNoteMap, note.storage)
if (storageNoteList == null) {
storageNoteList = new Set(storageNoteList)
state.storageNoteMap.set(note.storage, storageNoteList)
}
storageNoteList.add(uniqueKey) storageNoteList.add(uniqueKey)
let folderNoteSet = state.folderNoteMap.get(folderKey) let folderNoteSet = state.folderNoteMap.get(folderKey)
@@ -464,11 +311,7 @@ function data (state = defaultDataMap(), action) {
folderNoteSet.add(uniqueKey) folderNoteSet.add(uniqueKey)
note.tags.forEach((tag) => { note.tags.forEach((tag) => {
let tagNoteSet = state.tagNoteMap.get(tag) const tagNoteSet = getOrInitItem(state.tagNoteMap, tag)
if (tagNoteSet == null) {
tagNoteSet = new Set(tagNoteSet)
state.tagNoteMap.set(tag, tagNoteSet)
}
tagNoteSet.add(uniqueKey) tagNoteSet.add(uniqueKey)
}) })
}) })
@@ -517,6 +360,12 @@ function data (state = defaultDataMap(), action) {
state.storageMap = new Map(state.storageMap) state.storageMap = new Map(state.storageMap)
state.storageMap.set(action.storage.key, action.storage) state.storageMap.set(action.storage.key, action.storage)
return state return state
case 'EXPAND_STORAGE':
state = Object.assign({}, state)
state.storageMap = new Map(state.storageMap)
action.storage.isOpen = action.isOpen
state.storageMap.set(action.storage.key, action.storage)
return state
} }
return state return state
} }
@@ -559,6 +408,73 @@ function status (state = defaultStatus, action) {
return state return state
} }
function updateStarredChange (oldNote, note, state, uniqueKey) {
if (oldNote == null || oldNote.isStarred !== note.isStarred) {
state.starredSet = new Set(state.starredSet)
if (note.isStarred) {
state.starredSet.add(uniqueKey)
} else {
state.starredSet.delete(uniqueKey)
}
}
}
function updateFolderChange (oldNote, note, state, folderKey, uniqueKey) {
if (oldNote == null || oldNote.folder !== note.folder) {
state.folderNoteMap = new Map(state.folderNoteMap)
let folderNoteList = state.folderNoteMap.get(folderKey)
folderNoteList = new Set(folderNoteList)
folderNoteList.add(uniqueKey)
state.folderNoteMap.set(folderKey, folderNoteList)
if (oldNote != null) {
const oldFolderKey = oldNote.storage + '-' + oldNote.folder
let oldFolderNoteList = state.folderNoteMap.get(oldFolderKey)
oldFolderNoteList = new Set(oldFolderNoteList)
oldFolderNoteList.delete(uniqueKey)
state.folderNoteMap.set(oldFolderKey, oldFolderNoteList)
}
}
}
function updateTagChanges (oldNote, note, state, uniqueKey) {
const discardedTags = _.difference(oldNote.tags, note.tags)
const addedTags = _.difference(note.tags, oldNote.tags)
if (discardedTags.length + addedTags.length > 0) {
removeFromTags(discardedTags, state, uniqueKey)
assignToTags(addedTags, state, uniqueKey)
}
}
function assignToTags (tags, state, uniqueKey) {
state.tagNoteMap = new Map(state.tagNoteMap)
tags.forEach((tag) => {
const tagNoteList = getOrInitItem(state.tagNoteMap, tag)
tagNoteList.add(uniqueKey)
})
}
function removeFromTags (tags, state, uniqueKey) {
state.tagNoteMap = new Map(state.tagNoteMap)
tags.forEach(tag => {
let tagNoteList = state.tagNoteMap.get(tag)
if (tagNoteList != null) {
tagNoteList = new Set(tagNoteList)
tagNoteList.delete(uniqueKey)
state.tagNoteMap.set(tag, tagNoteList)
}
})
}
function getOrInitItem (target, key) {
let results = target.get(key)
if (results == null) {
results = new Set()
target.set(key, results)
}
return results
}
const reducer = combineReducers({ const reducer = combineReducers({
data, data,
config, config,

View File

@@ -90,7 +90,7 @@ app.on('ready', function () {
mainWindow.setMenu(menu) mainWindow.setMenu(menu)
} }
// Check update every hour // Check update every day
setInterval(function () { setInterval(function () {
checkUpdate() checkUpdate()
}, 1000 * 60 * 60 * 24) }, 1000 * 60 * 60 * 24)
@@ -106,7 +106,7 @@ app.on('ready', function () {
checkUpdate() checkUpdate()
} }
}) })
}, 10000) }, 10 * 1000)
ipcServer = require('./ipcServer') ipcServer = require('./ipcServer')
ipcServer.server.start() ipcServer.server.start()
}) })

View File

@@ -266,6 +266,13 @@ const view = {
click () { click () {
mainWindow.setFullScreen(!mainWindow.isFullScreen()) mainWindow.setFullScreen(!mainWindow.isFullScreen())
} }
},
{
role: 'zoomin',
accelerator: macOS ? 'CommandOrControl+Plus' : 'Control+='
},
{
role: 'zoomout'
} }
] ]
} }

View File

@@ -17,7 +17,7 @@ const mainWindow = new BrowserWindow({
autoHideMenuBar: showMenu, autoHideMenuBar: showMenu,
webPreferences: { webPreferences: {
zoomFactor: 1.0, zoomFactor: 1.0,
blinkFeatures: 'OverlayScrollbars' enableBlinkFeatures: 'OverlayScrollbars'
}, },
icon: path.resolve(__dirname, '../resources/app.png') icon: path.resolve(__dirname, '../resources/app.png')
}) })

View File

@@ -115,7 +115,7 @@
<script src="../node_modules/react-redux/dist/react-redux.min.js"></script> <script src="../node_modules/react-redux/dist/react-redux.min.js"></script>
<script type='text/javascript'> <script type='text/javascript'>
const electron = require('electron') const electron = require('electron')
electron.webFrame.setZoomLevelLimits(1, 1) electron.webFrame.setVisualZoomLevelLimits(1, 1)
var scriptUrl = window._.find(electron.remote.process.argv, (a) => a === '--hot') var scriptUrl = window._.find(electron.remote.process.argv, (a) => a === '--hot')
? 'http://localhost:8080/assets/main.js' ? 'http://localhost:8080/assets/main.js'
: '../compiled/main.js' : '../compiled/main.js'

View File

@@ -150,5 +150,7 @@
"Sanitization": "Sanitization", "Sanitization": "Sanitization",
"Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)", "Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)",
"Allow styles": "Allow styles", "Allow styles": "Allow styles",
"Allow dangerous html tags": "Allow dangerous html tags" "Allow dangerous html tags": "Allow dangerous html tags",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.",
"⚠ 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! ⚠"
} }

View File

@@ -205,5 +205,7 @@
"Unnamed": "Unbenannt", "Unnamed": "Unbenannt",
"Rename": "Umbenennen", "Rename": "Umbenennen",
"Folder Name": "Ordnername", "Folder Name": "Ordnername",
"No tags": "Keine Tags" "No tags": "Keine Tags",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.",
"⚠ 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! ⚠"
} }

View File

@@ -4,9 +4,10 @@
"Preferences": "Preferences", "Preferences": "Preferences",
"Make a note": "Make a note", "Make a note": "Make a note",
"Ctrl": "Ctrl", "Ctrl": "Ctrl",
"Ctrl(^)": "Ctrl", "Ctrl(^)": "Ctrl(^)",
"to create a new note": "to create a new note", "to create a new note": "to create a new note",
"Toggle Mode": "Toggle Mode", "Toggle Mode": "Toggle Mode",
"Add tag...": "Add tag...",
"Trash": "Trash", "Trash": "Trash",
"MODIFICATION DATE": "MODIFICATION DATE", "MODIFICATION DATE": "MODIFICATION DATE",
"Words": "Words", "Words": "Words",
@@ -20,9 +21,12 @@
".html": ".html", ".html": ".html",
"Print": "Print", "Print": "Print",
"Your preferences for Boostnote": "Your preferences for Boostnote", "Your preferences for Boostnote": "Your preferences for Boostnote",
"Help": "Help",
"Hide Help": "Hide Help",
"Storages": "Storages", "Storages": "Storages",
"Add Storage Location": "Add Storage Location", "Add Storage Location": "Add Storage Location",
"Add Folder": "Add Folder", "Add Folder": "Add Folder",
"Select Folder": "Select Folder",
"Open Storage folder": "Open Storage folder", "Open Storage folder": "Open Storage folder",
"Unlink": "Unlink", "Unlink": "Unlink",
"Edit": "Edit", "Edit": "Edit",
@@ -34,6 +38,8 @@
"Solarized Dark": "Solarized Dark", "Solarized Dark": "Solarized Dark",
"Dark": "Dark", "Dark": "Dark",
"Show a confirmation dialog when deleting notes": "Show a confirmation dialog when deleting notes", "Show a confirmation dialog when deleting notes": "Show a confirmation dialog when deleting notes",
"Disable Direct Write (It will be applied after restarting)": "Disable Direct Write (It will be applied after restarting)",
"Show only related tags": "Show only related tags",
"Editor Theme": "Editor Theme", "Editor Theme": "Editor Theme",
"Editor Font Size": "Editor Font Size", "Editor Font Size": "Editor Font Size",
"Editor Font Family": "Editor Font Family", "Editor Font Family": "Editor Font Family",
@@ -51,6 +57,7 @@
"⚠️ Please restart boostnote after you change the keymap": "⚠️ Please restart boostnote after you change the keymap", "⚠️ Please restart boostnote after you change the keymap": "⚠️ Please restart boostnote after you change the keymap",
"Show line numbers in the editor": "Show line numbers in the editor", "Show line numbers in the editor": "Show line numbers in the editor",
"Allow editor to scroll past the last line": "Allow editor to scroll past the last line", "Allow editor to scroll past the last line": "Allow editor to scroll past the last line",
"Enable smart quotes": "Enable smart quotes",
"Bring in web page title when pasting URL on editor": "Bring in web page title when pasting URL on editor", "Bring in web page title when pasting URL on editor": "Bring in web page title when pasting URL on editor",
"Preview": "Preview", "Preview": "Preview",
"Preview Font Size": "Preview Font Size", "Preview Font Size": "Preview Font Size",
@@ -127,6 +134,7 @@
"Storage": "Storage", "Storage": "Storage",
"Hotkeys": "Hotkeys", "Hotkeys": "Hotkeys",
"Show/Hide Boostnote": "Show/Hide Boostnote", "Show/Hide Boostnote": "Show/Hide Boostnote",
"Toggle editor mode": "Toggle editor mode",
"Restore": "Restore", "Restore": "Restore",
"Permanent Delete": "Permanent Delete", "Permanent Delete": "Permanent Delete",
"Confirm note deletion": "Confirm note deletion", "Confirm note deletion": "Confirm note deletion",
@@ -146,12 +154,26 @@
"UserName": "UserName", "UserName": "UserName",
"Password": "Password", "Password": "Password",
"Russian": "Russian", "Russian": "Russian",
"Hungarian": "Hungarian",
"Command(⌘)": "Command(⌘)", "Command(⌘)": "Command(⌘)",
"Add Storage": "Add Storage",
"Name": "Name",
"Type": "Type",
"File System": "File System",
"Setting up 3rd-party cloud storage integration:": "Setting up 3rd-party cloud storage integration:",
"Cloud-Syncing-and-Backup": "Cloud-Syncing-and-Backup",
"Location": "Location",
"Add": "Add",
"Unlink Storage": "Unlink Storage",
"Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.": "Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.",
"Editor Rulers": "Editor Rulers", "Editor Rulers": "Editor Rulers",
"Enable": "Enable", "Enable": "Enable",
"Disable": "Disable", "Disable": "Disable",
"Sanitization": "Sanitization", "Sanitization": "Sanitization",
"Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)", "Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)",
"Render newlines in Markdown paragraphs as <br>": "Render newlines in Markdown paragraphs as <br>",
"Allow styles": "Allow styles", "Allow styles": "Allow styles",
"Allow dangerous html tags": "Allow dangerous html tags" "Allow dangerous html tags": "Allow dangerous html tags",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.",
"⚠ 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! ⚠"
} }

View File

@@ -150,5 +150,7 @@
"Sanitization": "Saneamiento", "Sanitization": "Saneamiento",
"Only allow secure html tags (recommended)": "Solo permitir etiquetas html seguras (recomendado)", "Only allow secure html tags (recommended)": "Solo permitir etiquetas html seguras (recomendado)",
"Allow styles": "Permitir estilos", "Allow styles": "Permitir estilos",
"Allow dangerous html tags": "Permitir etiquetas html peligrosas" "Allow dangerous html tags": "Permitir etiquetas html peligrosas",
"⚠ 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! ⚠",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown."
} }

View File

@@ -153,5 +153,7 @@
"Sanitization": "پاکسازی کردن", "Sanitization": "پاکسازی کردن",
"Only allow secure html tags (recommended)": "(فقط تگ های امن اچ تی ام ال مجاز اند.(پیشنهاد میشود", "Only allow secure html tags (recommended)": "(فقط تگ های امن اچ تی ام ال مجاز اند.(پیشنهاد میشود",
"Allow styles": "حالت های مجاز", "Allow styles": "حالت های مجاز",
"Allow dangerous html tags": "تگ های خطرناک اچ‌ تی ام ال مجاز اند" "Allow dangerous html tags": "تگ های خطرناک اچ‌ تی ام ال مجاز اند",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.",
"⚠ 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! ⚠"
} }

View File

@@ -141,14 +141,16 @@
"Portuguese": "Portugais", "Portuguese": "Portugais",
"Spanish": "Espagnol", "Spanish": "Espagnol",
"You have to save!": "Il faut sauvegarder !", "You have to save!": "Il faut sauvegarder !",
"Russian": "Russian", "Russian": "Russe",
"Command(⌘)": "Command(⌘)", "Command(⌘)": "Command(⌘)",
"Editor Rulers": "Editor Rulers", "Editor Rulers": "Règles dans l'éditeur",
"Enable": "Enable", "Enable": "Activer",
"Disable": "Disable", "Disable": "Désactiver",
"Allow preview to scroll past the last line": "Allow preview to scroll past the last line", "Allow preview to scroll past the last line": "Permettre de scroller après la dernière ligne dans l'aperçu",
"Sanitization": "Sanitization", "Sanitization": "Sanitization",
"Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)", "Only allow secure html tags (recommended)": "N'accepter que les tags html sécurisés (recommandé)",
"Allow styles": "Allow styles", "Allow styles": "Accepter les styles",
"Allow dangerous html tags": "Allow dangerous html tags" "Allow dangerous html tags": "Accepter les tags html dangereux",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convertir des flèches textuelles en jolis signes. ⚠ Cela va interferérer avec les éventuels commentaires HTML dans votre Markdown.",
"⚠ 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! ⚠": "⚠ Vous avez collé un lien qui référence une pièce-jointe qui n'a pas pu être récupéré dans le dossier de stockage de la note. Coller des liens qui font référence à des pièces-jointes ne fonctionne que si la source et la destination et la même. Veuillez plutôt utiliser du Drag & Drop ! ⚠"
} }

View File

@@ -7,6 +7,7 @@
"Ctrl(^)": "Ctrl", "Ctrl(^)": "Ctrl",
"to create a new note": "hogy létrehozz egy jegyzetet", "to create a new note": "hogy létrehozz egy jegyzetet",
"Toggle Mode": "Mód Váltás", "Toggle Mode": "Mód Váltás",
"Add tag...": "Tag hozzáadása...",
"Trash": "Lomtár", "Trash": "Lomtár",
"MODIFICATION DATE": "MÓDOSÍTÁS DÁTUMA", "MODIFICATION DATE": "MÓDOSÍTÁS DÁTUMA",
"Words": "Szó", "Words": "Szó",
@@ -20,9 +21,12 @@
".html": ".html", ".html": ".html",
"Print": "Nyomtatás", "Print": "Nyomtatás",
"Your preferences for Boostnote": "Boostnote beállításaid", "Your preferences for Boostnote": "Boostnote beállításaid",
"Help": "Súgó",
"Hide Help": "Súgó Elrejtése",
"Storages": "Tárolók", "Storages": "Tárolók",
"Add Storage Location": "Tároló Hozzáadása", "Add Storage Location": "Tároló Hozzáadása",
"Add Folder": "Könyvtár Hozzáadása", "Add Folder": "Könyvtár Hozzáadása",
"Select Folder": "Könyvtár Kiválasztása",
"Open Storage folder": "Tároló Megnyitása", "Open Storage folder": "Tároló Megnyitása",
"Unlink": "Tároló Leválasztása", "Unlink": "Tároló Leválasztása",
"Edit": "Szerkesztés", "Edit": "Szerkesztés",
@@ -34,6 +38,8 @@
"Solarized Dark": "Solarized Dark", "Solarized Dark": "Solarized Dark",
"Dark": "Sötét", "Dark": "Sötét",
"Show a confirmation dialog when deleting notes": "Kérjen megerősítést a jegyzetek törlése előtt", "Show a confirmation dialog when deleting notes": "Kérjen megerősítést a jegyzetek törlése előtt",
"Disable Direct Write (It will be applied after restarting)": "Jegyzet Azonnali Mentésének Tiltása (Újraindítás igényel)",
"Show only related tags": "Csak a kapcsolódó tag-ek megjelenítése",
"Editor Theme": "Szerkesztő Témája", "Editor Theme": "Szerkesztő Témája",
"Editor Font Size": "Szerkesztő Betűmérete", "Editor Font Size": "Szerkesztő Betűmérete",
"Editor Font Family": "Szerkesztő Betűtípusa", "Editor Font Family": "Szerkesztő Betűtípusa",
@@ -51,6 +57,7 @@
"⚠️ Please restart boostnote after you change the keymap": "⚠️ Kérlek, indítsd újra a programot a kiosztás megváltoztatása után", "⚠️ Please restart boostnote after you change the keymap": "⚠️ Kérlek, indítsd újra a programot a kiosztás megváltoztatása után",
"Show line numbers in the editor": "Mutatassa a sorszámokat a szerkesztőben", "Show line numbers in the editor": "Mutatassa a sorszámokat a szerkesztőben",
"Allow editor to scroll past the last line": "A szerkesztőben az utolsó sor alá is lehessen görgetni", "Allow editor to scroll past the last line": "A szerkesztőben az utolsó sor alá is lehessen görgetni",
"Enable smart quotes": "Idézőjelek párjának automatikus beírása",
"Bring in web page title when pasting URL on editor": "Weboldal főcímének lekérdezése URL cím beillesztésekor", "Bring in web page title when pasting URL on editor": "Weboldal főcímének lekérdezése URL cím beillesztésekor",
"Preview": "Megtekintés", "Preview": "Megtekintés",
"Preview Font Size": "Megtekintés Betűmérete", "Preview Font Size": "Megtekintés Betűmérete",
@@ -127,6 +134,7 @@
"Storage": "Tároló", "Storage": "Tároló",
"Hotkeys": "Gyorsbillentyűk", "Hotkeys": "Gyorsbillentyűk",
"Show/Hide Boostnote": "Boostnote Megjelenítése/Elrejtése", "Show/Hide Boostnote": "Boostnote Megjelenítése/Elrejtése",
"Toggle editor mode": "Szerkesztő mód váltása",
"Restore": "Visszaállítás", "Restore": "Visszaállítás",
"Permanent Delete": "Végleges Törlés", "Permanent Delete": "Végleges Törlés",
"Confirm note deletion": "Törlés megerősítése", "Confirm note deletion": "Törlés megerősítése",
@@ -146,8 +154,8 @@
"UserName": "FelhasznaloNev", "UserName": "FelhasznaloNev",
"Password": "Jelszo", "Password": "Jelszo",
"Russian": "Russian", "Russian": "Russian",
"Command(⌘)": "Command(⌘)",
"Hungarian": "Hungarian", "Hungarian": "Hungarian",
"Command(⌘)": "Command(⌘)",
"Add Storage": "Tároló hozzáadása", "Add Storage": "Tároló hozzáadása",
"Name": "Név", "Name": "Név",
"Type": "Típus", "Type": "Típus",
@@ -156,6 +164,17 @@
"Cloud-Syncing-and-Backup": "Cloud-Syncing-and-Backup", "Cloud-Syncing-and-Backup": "Cloud-Syncing-and-Backup",
"Location": "Hely", "Location": "Hely",
"Add": "Hozzáadás", "Add": "Hozzáadás",
"Select Folder": "Könyvtár Kiválasztása",
"Unlink Storage": "Tároló Leválasztása", "Unlink Storage": "Tároló Leválasztása",
"Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.": "A leválasztás eltávolítja ezt a tárolót a Boostnote-ból. Az adatok nem lesznek törölve, kérlek manuálisan töröld a könyvtárat a merevlemezről, ha szükséges." "Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.": "A leválasztás eltávolítja ezt a tárolót a Boostnote-ból. Az adatok nem lesznek törölve, kérlek manuálisan töröld a könyvtárat a merevlemezről, ha szükséges.",
"Editor Rulers": "Szerkesztő Margók",
"Enable": "Engedélyezés",
"Disable": "Tiltás",
"Sanitization": "Tisztítás",
"Only allow secure html tags (recommended)": "Csak a biztonságos html tag-ek engedélyezése (ajánlott)",
"Render newlines in Markdown paragraphs as <br>": "Az újsor karaktert <br> soremelésként jelenítse meg a Markdown jegyzetekben",
"Allow styles": "Stílusok engedélyezése",
"Allow dangerous html tags": "Veszélyes html tag-ek engedélyezése",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.",
"⚠ 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! ⚠"
} }

View File

@@ -153,5 +153,7 @@
"Sanitization": "Bonifica", "Sanitization": "Bonifica",
"Only allow secure html tags (recommended)": "Consenti solo tag HTML sicuri (raccomandato)", "Only allow secure html tags (recommended)": "Consenti solo tag HTML sicuri (raccomandato)",
"Allow styles": "Consenti stili", "Allow styles": "Consenti stili",
"Allow dangerous html tags": "Consenti tag HTML pericolosi" "Allow dangerous html tags": "Consenti tag HTML pericolosi",
}" "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.",
"⚠ 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! ⚠"
}

View File

@@ -7,6 +7,7 @@
"Ctrl(^)": "Ctrl", "Ctrl(^)": "Ctrl",
"to create a new note": "ノートを新規に作成", "to create a new note": "ノートを新規に作成",
"Toggle Mode": "モード切替", "Toggle Mode": "モード切替",
"Add tag...": "タグを追加...",
"Trash": "ゴミ箱", "Trash": "ゴミ箱",
"MODIFICATION DATE": "修正日", "MODIFICATION DATE": "修正日",
"Words": "ワード", "Words": "ワード",
@@ -20,9 +21,12 @@
".html": ".html", ".html": ".html",
"Print": "印刷", "Print": "印刷",
"Your preferences for Boostnote": "Boostnoteの個人設定", "Your preferences for Boostnote": "Boostnoteの個人設定",
"Help": "ヘルプ",
"Hide Help": "ヘルプを隠す",
"Storages": "ストレージ", "Storages": "ストレージ",
"Add Storage Location": "ストレージロケーションを追加", "Add Storage Location": "ストレージロケーションを追加",
"Add Folder": "フォルダを追加", "Add Folder": "フォルダを追加",
"Select Folder": "フォルダを選択",
"Open Storage folder": "ストレージフォルダを開く", "Open Storage folder": "ストレージフォルダを開く",
"Unlink": "リンク解除", "Unlink": "リンク解除",
"Edit": "編集", "Edit": "編集",
@@ -34,6 +38,8 @@
"Solarized Dark": "明灰", "Solarized Dark": "明灰",
"Dark": "暗灰", "Dark": "暗灰",
"Show a confirmation dialog when deleting notes": "ノートを削除する時に確認ダイアログを表示する", "Show a confirmation dialog when deleting notes": "ノートを削除する時に確認ダイアログを表示する",
"Disable Direct Write (It will be applied after restarting)": "Disable Direct Write (It will be applied after restarting)",
"Show only related tags": "関連するタグのみ表示する",
"Editor Theme": "エディタのテーマ", "Editor Theme": "エディタのテーマ",
"Editor Font Size": "エディタのフォントサイズ", "Editor Font Size": "エディタのフォントサイズ",
"Editor Font Family": "エディタのフォント", "Editor Font Family": "エディタのフォント",
@@ -48,17 +54,18 @@
"default": "デフォルト", "default": "デフォルト",
"vim": "vim", "vim": "vim",
"emacs": "emacs", "emacs": "emacs",
"⚠️ Please restart boostnote after you change the keymap": "⚠️ Plキーマップ変更後は Boostnote を再起動してください", "⚠️ Please restart boostnote after you change the keymap": "⚠️ キーマップ変更後は Boostnote を再起動してください",
"Show line numbers in the editor": "エディタ内に行番号を表示", "Show line numbers in the editor": "エディタ内に行番号を表示",
"Allow editor to scroll past the last line": "エディタが最終行以降にスクロールできるようにする", "Allow editor to scroll past the last line": "エディタが最終行以降にスクロールできるようにする",
"Bring in web page title when pasting URL on editor": "Bring in web page title when pasting URL on editor", "Enable smart quotes": "スマートクォートを有効にする",
"Bring in web page title when pasting URL on editor": "URLを貼り付けた時にWebページのタイトルを取得する",
"Preview": "プレビュー", "Preview": "プレビュー",
"Preview Font Size": "プレビュー時フォントサイズ", "Preview Font Size": "プレビュー時フォントサイズ",
"Preview Font Family": "プレビュー時フォント", "Preview Font Family": "プレビュー時フォント",
"Code block Theme": "コードブロックのテーマ", "Code block Theme": "コードブロックのテーマ",
"Allow preview to scroll past the last line": "プレビュー時に最終行以降にスクロールできるようにする", "Allow preview to scroll past the last line": "プレビュー時に最終行以降にスクロールできるようにする",
"Show line numbers for preview code blocks": "プレビュー時のコードブロック内に行番号を表示する", "Show line numbers for preview code blocks": "プレビュー時のコードブロック内に行番号を表示する",
"LaTeX Inline Open Delimiter": "LaTeX 開始デリミタ(インライン)Inline Open Delimiter", "LaTeX Inline Open Delimiter": "LaTeX 開始デリミタ(インライン)",
"LaTeX Inline Close Delimiter": "LaTeX 終了デリミタ(インライン)", "LaTeX Inline Close Delimiter": "LaTeX 終了デリミタ(インライン)",
"LaTeX Block Open Delimiter": "LaTeX 開始デリミタ(ブロック)", "LaTeX Block Open Delimiter": "LaTeX 開始デリミタ(ブロック)",
"LaTeX Block Close Delimiter": "LaTeX 終了デリミタ(ブロック)", "LaTeX Block Close Delimiter": "LaTeX 終了デリミタ(ブロック)",
@@ -83,7 +90,7 @@
"You can choose to enable or disable this option.": "このオプションは有効/無効を選択できます。", "You can choose to enable or disable this option.": "このオプションは有効/無効を選択できます。",
"Enable analytics to help improve Boostnote": "Boostnote の機能向上のための解析機能を有効にする", "Enable analytics to help improve Boostnote": "Boostnote の機能向上のための解析機能を有効にする",
"Crowdfunding": "クラウドファンディング", "Crowdfunding": "クラウドファンディング",
"Dear everyone,": "Dear everyone,", "Dear everyone,": "みなさまへ",
"Thank you for using Boostnote!": "Boostnote を利用いただき、ありがとうございます!", "Thank you for using Boostnote!": "Boostnote を利用いただき、ありがとうございます!",
"Boostnote is used in about 200 different countries and regions by an awesome community of developers.": "Boostnote はおよそ 200 の国と地域において、開発者コミュニティを中心に利用されています。", "Boostnote is used in about 200 different countries and regions by an awesome community of developers.": "Boostnote はおよそ 200 の国と地域において、開発者コミュニティを中心に利用されています。",
"To continue supporting this growth, and to satisfy community expectations,": "この成長を持続し、またコミュニティからの要望に答えるため、", "To continue supporting this growth, and to satisfy community expectations,": "この成長を持続し、またコミュニティからの要望に答えるため、",
@@ -112,6 +119,7 @@
"Updated": "更新日時", "Updated": "更新日時",
"Created": "作成日時", "Created": "作成日時",
"Alphabetically": "アルファベット順", "Alphabetically": "アルファベット順",
"Counter": "数順",
"Default View": "デフォルトビュー", "Default View": "デフォルトビュー",
"Compressed View": "圧縮ビュー", "Compressed View": "圧縮ビュー",
"Search": "検索", "Search": "検索",
@@ -126,6 +134,7 @@
"Storage": "ストレージ", "Storage": "ストレージ",
"Hotkeys": "ホットキー", "Hotkeys": "ホットキー",
"Show/Hide Boostnote": "Boostnote の表示/非表示", "Show/Hide Boostnote": "Boostnote の表示/非表示",
"Toggle editor mode": "エディタモードの切替",
"Restore": "リストア", "Restore": "リストア",
"Permanent Delete": "永久に削除", "Permanent Delete": "永久に削除",
"Confirm note deletion": "ノート削除確認", "Confirm note deletion": "ノート削除確認",
@@ -142,13 +151,29 @@
"Portuguese": "ポルトガル語", "Portuguese": "ポルトガル語",
"Spanish": "スペイン語", "Spanish": "スペイン語",
"You have to save!": "保存してください!", "You have to save!": "保存してください!",
"UserName": "ユーザー名",
"Password": "パスワード",
"Russian": "ロシア語", "Russian": "ロシア語",
"Hungarian": "ハンガリー語",
"Command(⌘)": "コマンド(⌘)", "Command(⌘)": "コマンド(⌘)",
"Add Storage": "ストレージを追加",
"Name": "名前",
"Type": "種類",
"File System": "ファイルシステム",
"Setting up 3rd-party cloud storage integration:": "サードパーティのクラウドストレージとの統合を設定する:",
"Cloud-Syncing-and-Backup": "Cloud-Syncing-and-Backup",
"Location": "ロケーション",
"Add": "追加",
"Unlink Storage": "ストレージのリンクを解除",
"Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.": "リンクの解除ではBoostnoteからリンクされたストレージを削除しますが、データは削除されません。データを削除する場合はご自身でハードドライブからフォルダを削除してください。",
"Editor Rulers": "罫線", "Editor Rulers": "罫線",
"Enable": "有効", "Enable": "有効",
"Disable": "無効", "Disable": "無効",
"Sanitization": "サニタイズ", "Sanitization": "サニタイズ",
"Only allow secure html tags (recommended)": "安全なHTMLタグのみ利用を許可する推奨", "Only allow secure html tags (recommended)": "安全なHTMLタグのみ利用を許可する推奨",
"Render newlines in Markdown paragraphs as <br>": "Markdown 中の改行でプレビューも改行する",
"Allow styles": "スタイルを許可する", "Allow styles": "スタイルを許可する",
"Allow dangerous html tags": "安全でないHTMLタグの利用を許可する" "Allow dangerous html tags": "安全でないHTMLタグの利用を許可する",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "テキストの矢印を綺麗な記号に変換する ⚠ この設定はMarkdown内でのHTMLコメントに干渉します。",
"⚠ 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! ⚠": "⚠ このノートのストレージに存在しない添付ファイルへのリンクを貼り付けました。添付ファイルへのリンクの貼り付けは同一ストレージ内でのみサポートされています。代わりに添付ファイルをドラッグアンドドロップしてください! ⚠"
} }

View File

@@ -144,7 +144,7 @@
"You have to save!": "저장해주세요!", "You have to save!": "저장해주세요!",
"Russian": "Russian", "Russian": "Russian",
"Command(⌘)": "Command(⌘)", "Command(⌘)": "Command(⌘)",
"Delete Folder": "폴더 삭", "Delete Folder": "폴더 삭",
"This will delete all notes in the folder and can not be undone.": "폴더의 모든 노트를 지우게 되고, 되돌릴 수 없습니다.", "This will delete all notes in the folder and can not be undone.": "폴더의 모든 노트를 지우게 되고, 되돌릴 수 없습니다.",
"UserName": "유저명", "UserName": "유저명",
"Password": "패스워드", "Password": "패스워드",
@@ -156,5 +156,7 @@
"Sanitization": "허용 태그 범위", "Sanitization": "허용 태그 범위",
"Only allow secure html tags (recommended)": "안전한 HTML 태그만 허용 (추천)", "Only allow secure html tags (recommended)": "안전한 HTML 태그만 허용 (추천)",
"Allow styles": "style 태그, 속성까지 허용", "Allow styles": "style 태그, 속성까지 허용",
"Allow dangerous html tags": "모든 위험한 태그 허용" "Allow dangerous html tags": "모든 위험한 태그 허용",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.",
"⚠ 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! ⚠"
} }

View File

@@ -149,5 +149,7 @@
"Sanitization": "Sanitization", "Sanitization": "Sanitization",
"Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)", "Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)",
"Allow styles": "Allow styles", "Allow styles": "Allow styles",
"Allow dangerous html tags": "Allow dangerous html tags" "Allow dangerous html tags": "Allow dangerous html tags",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.",
"⚠ 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! ⚠"
} }

View File

@@ -149,5 +149,7 @@
"Sanitization": "Sanitization", "Sanitization": "Sanitization",
"Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)", "Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)",
"Allow styles": "Allow styles", "Allow styles": "Allow styles",
"Allow dangerous html tags": "Allow dangerous html tags" "Allow dangerous html tags": "Allow dangerous html tags",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.",
"⚠ 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! ⚠"
} }

View File

@@ -149,5 +149,7 @@
"Sanitization": "Sanitização", "Sanitization": "Sanitização",
"Only allow secure html tags (recommended)": "Permitir apenas tags html seguras (recomendado)", "Only allow secure html tags (recommended)": "Permitir apenas tags html seguras (recomendado)",
"Allow styles": "Permitir estilos", "Allow styles": "Permitir estilos",
"Allow dangerous html tags": "Permitir tags html perigosas" "Allow dangerous html tags": "Permitir tags html perigosas",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.",
"⚠ 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! ⚠"
} }

View File

@@ -149,5 +149,7 @@
"Sanitization": "Sanitization", "Sanitization": "Sanitization",
"Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)", "Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)",
"Allow styles": "Allow styles", "Allow styles": "Allow styles",
"Allow dangerous html tags": "Allow dangerous html tags" "Allow dangerous html tags": "Allow dangerous html tags",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.",
"⚠ 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! ⚠"
} }

View File

@@ -146,5 +146,7 @@
"Russian": "Русский", "Russian": "Русский",
"Editor Rulers": "Editor Rulers", "Editor Rulers": "Editor Rulers",
"Enable": "Enable", "Enable": "Enable",
"Disable": "Disable" "Disable": "Disable",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.",
"⚠ 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! ⚠"
} }

View File

@@ -148,5 +148,7 @@
"Sanitization": "Sanitization", "Sanitization": "Sanitization",
"Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)", "Only allow secure html tags (recommended)": "Only allow secure html tags (recommended)",
"Allow styles": "Allow styles", "Allow styles": "Allow styles",
"Allow dangerous html tags": "Allow dangerous html tags" "Allow dangerous html tags": "Allow dangerous html tags",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.",
"⚠ 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! ⚠"
} }

155
locales/tr.json Normal file
View File

@@ -0,0 +1,155 @@
{
"Notes": "Notlar",
"Tags": "Etiketler",
"Preferences": "Tercihler",
"Make a note": "Not Oluştur",
"Ctrl": "Ctrl",
"Ctrl(^)": "Ctrl",
"to create a new note": "yeni not oluşturmak için",
"Toggle Mode": "Mod Değiştir",
"Trash": "Çöp",
"MODIFICATION DATE": "DEĞİŞİKLİK TARİHİ",
"Words": "Kelimeler",
"Letters": "Harfler",
"STORAGE": "SAKLAMA ALANI",
"FOLDER": "DOSYA",
"CREATION DATE": "OLUŞTURULMA TARİHİ",
"NOTE LINK": "NOT BAĞLANTISI",
".md": ".md",
".txt": ".txt",
".html": ".html",
"Print": "Yazdır",
"Your preferences for Boostnote": "Boostnote tercihleriniz",
"Storages": "Saklama Alanları",
"Add Storage Location": "Saklama Yeri Ekle",
"Add Folder": "Dosya Ekle",
"Open Storage folder": "Saklama Alanı Dosyasını Aç",
"Unlink": "Bağlantıyı kaldır",
"Edit": "Düzenle",
"Delete": "Sil",
"Interface": "Arayüz",
"Interface Theme": "Arayüz Teması",
"Default": "Varsayılan",
"White": "Beyaz",
"Solarized Dark": "Solarize Karanlık",
"Dark": "Karanlık",
"Show a confirmation dialog when deleting notes": "Notlar silinirken onay ekranını göster",
"Editor Theme": "Editör Teması",
"Editor Font Size": "Editör Yazı Büyüklüğü",
"Editor Font Family": "Editör Yazı Ailesi",
"Editor Indent Style": "Editör Girinti Stili",
"Spaces": "Boşluklar",
"Tabs": "Tablar",
"Switch to Preview": "Önizlemeye Geç",
"When Editor Blurred": "Editörden çıkıldığında",
"When Editor Blurred, Edit On Double Click": "Editörden Çıkıldığında, Çift Tıklayarak Düzenle",
"On Right Click": "Sağ tıklandığında",
"Editor Keymap": "Editör Tuş Haritası",
"default": "varsayılan",
"vim": "vim",
"emacs": "emacs",
"⚠️ Please restart boostnote after you change the keymap": "⚠️ Tuş haritası değişikliklerinden sonra lütfen Boostnote'u yeniden başlatın",
"Show line numbers in the editor": "Editörde satır numaralarını göster",
"Allow editor to scroll past the last line": "Editörün son satırı geçmesine izin ver",
"Bring in web page title when pasting URL on editor": "Editörde URL yapıştırırken web sayfasının başlığını getir",
"Preview": "Önizleme",
"Preview Font Size": "Yazı Büyüklüğünü Önizle",
"Preview Font Family": "Yazı Tipini Önizle",
"Code block Theme": "Kod bloğu Teması",
"Allow preview to scroll past the last line": "Önizlemenin son satırı geçmesine izin ver",
"Show line numbers for preview code blocks": "Kod bloklarının önizlemesinde satır numaralarını göster",
"LaTeX Inline Open Delimiter": "LaTeX Inline Open Delimiter",
"LaTeX Inline Close Delimiter": "LaTeX Inline Close Delimiter",
"LaTeX Block Open Delimiter": "LaTeX Block Open Delimiter",
"LaTeX Block Close Delimiter": "LaTeX Block Close Delimiter",
"Community": "Topluluk",
"Subscribe to Newsletter": "Bültene Kayıt Ol",
"GitHub": "GitHub",
"Blog": "Blog",
"Facebook Group": "Facebook Grubu",
"Twitter": "Twitter",
"About": "Hakkında",
"Boostnote": "Boostnote",
"An open source note-taking app made for programmers just like you.": "Tıpkı sizin gibi programcılar için yapılmış açık kaynak not alma uygulaması",
"Website": "Websitesi",
"Development": "Geliştirme",
" : Development configurations for Boostnote.": " : Boostnote için geliştirme ayarları.",
"Copyright (C) 2017 - 2018 BoostIO": "Her hakkı saklıdır. (C) 2017 - 2018 BoostIO",
"License: GPL v3": "Lisans: GPL v3",
"Analytics": "Analizler",
"Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote, uygulamanın geliştirilmesi amacıyla anonim veriler toplar. Notlarınızın içeriği gibi kişisel bilgiler kesinlikle toplanmaz.",
"You can see how it works on ": "Nasıl çalıştığını görebilirsiniz ",
"You can choose to enable or disable this option.": "Bu seçeneği etkinleştirmeyi veya devre dışı bırakmayı seçebilirsiniz.",
"Enable analytics to help improve Boostnote": "Boostnote'un geliştirilmesine katkıda bulunmak için analizleri etkinleştirin",
"Crowdfunding": "Kitle Fonlaması",
"Dear everyone,": "Sevgili herkes,",
"Thank you for using Boostnote!": "Boostnote'u kullandığınız için teşekkürler!",
"Boostnote is used in about 200 different countries and regions by an awesome community of developers.": "Boostnote, 200 farklı ülke ve bölgede, harika bir geliştirici topluluğu tarafından kullanılmaktadır.",
"To continue supporting this growth, and to satisfy community expectations,": "Bu büyümeyi desteklemeye devam etmek ve topluluk beklentilerini karşılamak için,",
"we would like to invest more time and resources in this project.": "bu projeye daha fazla zaman ve kaynak yatırmak istiyoruz.",
"If you like this project and see its potential, you can help by supporting us on OpenCollective!": "Bu projeyi beğeniyor ve potansiyel görüyorsanız, OpenCollective üzerinden bizi destekleyerek katkıda bulunabilirsiniz!",
"Thanks,": "Teşekkürler,",
"Boostnote maintainers": "Boostnote'un bakımını yapanlar",
"Support via OpenCollective": "OpenCollective aracılığıyla destekle",
"Language": "Dil",
"English": "İngilizce",
"German": "Almanca",
"French": "Fransızca",
"Show \"Saved to Clipboard\" notification when copying": "Kopyalandığında \"Clipboard'a kopyalandı\" uyarısını göster",
"All Notes": "Tüm Notlar",
"Starred": "Yıldızlı",
"Are you sure to ": "Bu klasörü",
" delete": " silmek istediğinize",
"this folder?": " emin misiniz?",
"Confirm": "Onayla",
"Cancel": "İptal",
"Markdown Note": "Markdown Notu",
"This format is for creating text documents. Checklists, code blocks and Latex blocks are available.": "Bu format metin dökümanları oluşturmak içindir. Listeler, kod blokları ve Latex blokları mevcuttur.",
"Snippet Note": "Parça Not",
"This format is for creating code snippets. Multiple snippets can be grouped into a single note.": "Bu format kod parçacıkları oluşturmak içindir. Çoklu kod parçaları tek bir not içinde gruplanabilir.",
"Tab to switch format": "Format değiştirmek için Tab tuşunu kullan",
"Updated": "Güncellendi",
"Created": "Oluşturuldu",
"Alphabetically": "Alfabetik Olarak",
"Default View": "Varsayılan Görünüm",
"Compressed View": "Sıkıştırılmış Görünüm",
"Search": "Ara",
"Blog Type": "Blog Tipi",
"Blog Address": "Blog Adresi",
"Save": "Kaydet",
"Auth": "Auth",
"Authentication Method": "Doğrulama Yöntemi",
"JWT": "JWT",
"USER": "KULLANICI",
"Token": "Token",
"Storage": "Saklama Alanı",
"Hotkeys": "Kısayol Tuşları",
"Show/Hide Boostnote": "Boostnote'u Göster/Gizle ",
"Restore": "Geri Yükle",
"Permanent Delete": "Kalıcı Olarak Sil",
"Confirm note deletion": "Not silmeyi onayla",
"This will permanently remove this note.": "Bu not kalıcı olarak silinecektir.",
"Successfully applied!": "Başarıyla Uygulandı!",
"Albanian": "Arnavutça",
"Chinese (zh-CN)": "Çince (zh-CN)",
"Chinese (zh-TW)": "Çince (zh-TW)",
"Danish": "Danca",
"Japanese": "Japonca",
"Korean": "Korean",
"Norwegian": "Norveççe",
"Polish": "Lehçe",
"Portuguese": "Portekizce",
"Spanish": "İspanyolca",
"You have to save!": "Kaydetmelisiniz!",
"UserName": "KullanıcıAdı",
"Password": "Şifre",
"Russian": "Rusça",
"Command(⌘)": "Command(⌘)",
"Editor Rulers": "Editör Cetvelleri",
"Enable": "Etkinleştir",
"Disable": "Etkisizleştir",
"Sanitization": "Temizleme",
"Only allow secure html tags (recommended)": "Sadece güvenli html etiketlerine izin ver (tavsiye edilen)",
"Allow styles": "Stillere izin ver",
"Allow dangerous html tags": "Tehlikeli html etiketlerine izin ver"
}

View File

@@ -23,7 +23,7 @@
"Storages": "本地存储", "Storages": "本地存储",
"Add Storage Location": "添加一个本地存储位置", "Add Storage Location": "添加一个本地存储位置",
"Add Folder": "新建文件夹", "Add Folder": "新建文件夹",
"Open Storage folder": "打开本地存储位置", "Open Storage folder": "打开本地存储文件夹",
"Unlink": "取消链接", "Unlink": "取消链接",
"Edit": "编辑", "Edit": "编辑",
"Delete": "删除", "Delete": "删除",
@@ -34,10 +34,11 @@
"Solarized Dark": "Solarized Dark", "Solarized Dark": "Solarized Dark",
"Dark": "Dark", "Dark": "Dark",
"Show a confirmation dialog when deleting notes": "删除笔记的时候,显示确认框", "Show a confirmation dialog when deleting notes": "删除笔记的时候,显示确认框",
"Editor": "编辑器",
"Editor Theme": "编辑器主题", "Editor Theme": "编辑器主题",
"Editor Font Size": "编辑器字号", "Editor Font Size": "编辑器字号",
"Editor Font Family": "编辑器字体", "Editor Font Family": "编辑器字体",
"Editor Indent Style": "缩进风格", "Editor Indent Style": "编辑器缩进风格",
"Spaces": "空格", "Spaces": "空格",
"Tabs": "Tabs", "Tabs": "Tabs",
"Switch to Preview": "快速切换到预览界面", "Switch to Preview": "快速切换到预览界面",
@@ -48,20 +49,21 @@
"default": "默认", "default": "默认",
"vim": "vim", "vim": "vim",
"emacs": "emacs", "emacs": "emacs",
"⚠️ Please restart boostnote after you change the keymap": "⚠️ 设置好快捷键后,记得重启boostnote", "⚠️ Please restart boostnote after you change the keymap": "⚠️ 修改后,请重启 boostnote",
"Show line numbers in the editor": "在编辑器中显示行号", "Show line numbers in the editor": "在编辑器中显示行号",
"Allow editor to scroll past the last line": "允许编辑器滚动到最后一行", "Allow editor to scroll past the last line": "允许编辑器滚动到最后一行",
"Bring in web page title when pasting URL on editor": "粘贴网页链接的时候,显示为网页标题", "Bring in web page title when pasting URL on editor": "粘贴网页链接的时候,显示为网页标题",
"Preview": "预览", "Preview": "预览",
"Preview Font Size": "预览字号", "Preview Font Size": "预览字号",
"Preview Font Family": "预览字体", "Preview Font Family": "预览字体",
"Code block Theme": "代码块主题", "Code block Theme": "代码块主题",
"Allow preview to scroll past the last line": "允许预览滚动到最后一行", "Allow preview to scroll past the last line": "允许预览滚动到最后一行",
"Show line numbers for preview code blocks": "在预览器中显示行号", "Show line numbers for preview code blocks": "在预览显示行号",
"LaTeX Inline Open Delimiter": "LaTeX 单行开头分隔符", "LaTeX Inline Open Delimiter": "LaTeX 单行开头分隔符",
"LaTeX Inline Close Delimiter": "LaTeX 单行结尾分隔符", "LaTeX Inline Close Delimiter": "LaTeX 单行结尾分隔符",
"LaTeX Block Open Delimiter": "LaTeX 多行开头分隔符", "LaTeX Block Open Delimiter": "LaTeX 多行开头分隔符",
"LaTeX Block Close Delimiter": "LaTeX 多行结尾分隔符", "LaTeX Block Close Delimiter": "LaTeX 多行结尾分隔符",
"PlantUML Server": "PlantUML 服务器",
"Community": "社区", "Community": "社区",
"Subscribe to Newsletter": "订阅邮件", "Subscribe to Newsletter": "订阅邮件",
"GitHub": "GitHub", "GitHub": "GitHub",
@@ -73,24 +75,24 @@
"An open source note-taking app made for programmers just like you.": "一款专门为程序员朋友量身打造的开源笔记", "An open source note-taking app made for programmers just like you.": "一款专门为程序员朋友量身打造的开源笔记",
"Website": "官网", "Website": "官网",
"Development": "开发", "Development": "开发",
" : Development configurations for Boostnote.": " : Boostnote的开发配置", " : Development configurations for Boostnote.": " : Boostnote 的开发配置",
"Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO", "Copyright (C) 2017 - 2018 BoostIO": "Copyright (C) 2017 - 2018 BoostIO",
"License: GPL v3": "License: GPL v3", "License: GPL v3": "License: GPL v3",
"Analytics": "分析", "Analytics": "分析",
"Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote 收集匿名数据只为了提升软件使用体验,绝对不收集任何个人信息(包括笔记内容)", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote 收集匿名数据只为了提升软件使用体验,绝对不收集任何个人信息(包括笔记内容)",
"You can see how it works on ": "你可以看看它的源码是如何运作的 ", "You can see how it works on ": "你可以看看它的源码是如何运作的 ",
"You can choose to enable or disable this option.": "你可以选择开启或不开启这个功能", "You can choose to enable or disable this option.": "你可以选择开启或不开启这个功能",
"Enable analytics to help improve Boostnote": "允许对数据进行分析帮助我们改进Boostnote", "Enable analytics to help improve Boostnote": "允许对数据进行分析,帮助我们改进 Boostnote",
"Crowdfunding": "众筹", "Crowdfunding": "众筹",
"Dear everyone,": "亲爱的用户:", "Dear everyone,": "亲爱的用户:",
"Thank you for using Boostnote!": "谢谢你使用Boostnote", "Thank you for using Boostnote!": "谢谢你使用 Boostnote",
"Boostnote is used in about 200 different countries and regions by an awesome community of developers.": "大约有200个不同的国家和地区的优秀开发者们都在使用Boostnote", "Boostnote is used in about 200 different countries and regions by an awesome community of developers.": "大约有200个不同的国家和地区的优秀开发者们都在使用 Boostnote",
"To continue supporting this growth, and to satisfy community expectations,": "为了继续支持这种发展,和满足社区的期待,", "To continue supporting this growth, and to satisfy community expectations,": "为了继续支持这种发展,和满足社区的期待,",
"we would like to invest more time and resources in this project.": "我们非常愿意投入更多的时间和资源到这个项目中。", "we would like to invest more time and resources in this project.": "我们非常愿意投入更多的时间和资源到这个项目中。",
"If you like this project and see its potential, you can help by supporting us on OpenCollective!": "如果你喜欢这款软件并且看好它的潜力, 请在OpenCollective上支持我们", "If you like this project and see its potential, you can help by supporting us on OpenCollective!": "如果你喜欢这款软件并且看好它的潜力, 请在 OpenCollective 上支持我们!",
"Thanks,": "十分感谢!", "Thanks,": "十分感谢!",
"Boostnote maintainers": "Boostnote的维护人员", "Boostnote maintainers": "Boostnote 的维护人员",
"Support via OpenCollective": "在OpenCollective上支持我们", "Support via OpenCollective": "在 OpenCollective 上支持我们",
"Language": "语言", "Language": "语言",
"English": "English", "English": "English",
"German": "German", "German": "German",
@@ -103,14 +105,15 @@
"this folder?": "这个文件夹?", "this folder?": "这个文件夹?",
"Confirm": "确认", "Confirm": "确认",
"Cancel": "取消", "Cancel": "取消",
"Markdown Note": "Markdown笔记", "Markdown Note": "Markdown 笔记",
"This format is for creating text documents. Checklists, code blocks and Latex blocks are available.": "创建文档清单代码块甚至是Latex格式文档", "This format is for creating text documents. Checklists, code blocks and Latex blocks are available.": "创建文档,清单,代码块甚至是 Latex 格式文档",
"Snippet Note": "代码笔记", "Snippet Note": "代码笔记",
"This format is for creating code snippets. Multiple snippets can be grouped into a single note.": "创建代码片段,支持多种语法代码片段", "This format is for creating code snippets. Multiple snippets can be grouped into a single note.": "创建代码片段,支持多种语法代码片段",
"Tab to switch format": "使用Tab键切换格式", "Tab to switch format": "使用 Tab 键切换格式",
"Updated": "更新时间", "Updated": "更新时间",
"Created": "创建时间", "Created": "创建时间",
"Alphabetically": "A~Z排序", "Alphabetically": "A~Z 排序",
"Counter":"标签下文章数量排序",
"Default View": "默认视图", "Default View": "默认视图",
"Compressed View": "列表视图", "Compressed View": "列表视图",
"Search": "搜索", "Search": "搜索",
@@ -146,7 +149,63 @@
"Enable": "开启", "Enable": "开启",
"Disable": "关闭", "Disable": "关闭",
"Sanitization": "代码处理", "Sanitization": "代码处理",
"Only allow secure html tags (recommended)": "只允许安全的html标签(推荐)", "Only allow secure html tags (recommended)": "只允许安全的 html 标签(推荐)",
"Allow styles": "允许样式", "Allow styles": "允许样式",
"Allow dangerous html tags": "允许危险的html标签" "Allow dangerous html tags": "允许危险的 html 标签",
"Select filter mode": "选择过滤模式",
"Add tag...": "添加标签...",
"Star":"星标",
"Fullscreen": "全屏",
"Info":"详情",
"Remove pin": "取消置顶",
"Pin to Top": "置顶",
"Delete Note": "删除笔记",
"Clone Note": "复制笔记",
"Restore Note": "恢复笔记",
"Copy Note Link": "复制笔记链接",
"Publish Blog": "发布博客",
"Update Blog": "更新博客",
"Open Blog": "打开博客",
"Empty Trash": "清空废纸篓",
"Rename Folder": "重命名文件夹",
"Export Folder": "导出文件夹",
"Export as txt": "导出为 txt",
"Export as md": "导出为 md",
"Delete Folder": "删除文件夹",
"Select directory": "选择目录",
"Select a folder to export the files to": "选择一个导出目录",
"Description...": "描述...",
"Publish Failed": "发布失败",
"Check and update your blog setting and try again.": "检查并修改你的博客设置后重试。",
"Delete a snippet": "删除一个代码片段",
"This work cannot be undone.": "此操作无法撤销。",
"Help": "帮助",
"Hungarian": "匈牙利语",
"Hide Help": "隐藏帮助",
"wordpress": "Wordpress",
"Add Storage": "添加存储",
"Name": "名称",
"Type": "类型",
"File System": "文件",
"Setting up 3rd-party cloud storage integration:": "设置整合第三方云存储:",
"Cloud-Syncing-and-Backup": "云端同步和备份",
"Location": "路径",
"Select Folder": "选择文件夹",
"Add": "添加",
"Available Keys": "可用按键",
"Select Directory": "选择目录",
"copy": "副本",
"Create new folder": "创建新文件夹",
"Folder name": "文件夹名称",
"Create": "创建",
"Unlink Storage": "取消存储链接",
"Unlinking removes this linked storage from Boostnote. No data is removed, please manually delete the folder from your hard drive if needed.": "取消链接会移除存储和 Boostnote 的链接。文件将不会被删除,如果你需要的话可以手动删除此目录。",
"Empty note": "空笔记",
"Unnamed":"未命名",
"Rename": "重命名",
"Folder Name": "文件夹名称",
"No tags":"无标签",
"Render newlines in Markdown paragraphs as <br>":"在 Markdown 段落中使用 <br> 换行",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.",
"⚠ 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! ⚠"
} }

View File

@@ -4,10 +4,10 @@
"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": "垃圾桶",
"MODIFICATION DATE": "修改時間", "MODIFICATION DATE": "修改時間",
"Words": "單字", "Words": "單字",
"Letters": "字數", "Letters": "字數",
@@ -20,8 +20,8 @@
".html": ".html", ".html": ".html",
"Print": "列印", "Print": "列印",
"Your preferences for Boostnote": "Boostnote 偏好設定", "Your preferences for Boostnote": "Boostnote 偏好設定",
"Storages": "本機儲存空間", "Storages": "儲存空間",
"Add Storage Location": "新增一個本機儲存位置", "Add Storage Location": "新增儲存位置",
"Add Folder": "新增資料夾", "Add Folder": "新增資料夾",
"Open Storage folder": "開啟儲存資料夾", "Open Storage folder": "開啟儲存資料夾",
"Unlink": "解除連結", "Unlink": "解除連結",
@@ -43,12 +43,12 @@
"Switch to Preview": "切回預覽頁面的時機", "Switch to Preview": "切回預覽頁面的時機",
"When Editor Blurred": "當編輯器失去焦點時", "When Editor Blurred": "當編輯器失去焦點時",
"When Editor Blurred, Edit On Double Click": "當編輯器失去焦點時,雙擊切換到編輯畫面", "When Editor Blurred, Edit On Double Click": "當編輯器失去焦點時,雙擊切換到編輯畫面",
"On Right Click": "點右鍵切換兩個頁面", "On Right Click": "點右鍵切換兩個頁面",
"Editor Keymap": "編輯器 Keymap", "Editor Keymap": "編輯器 Keymap",
"default": "預設", "default": "預設",
"vim": "vim", "vim": "vim",
"emacs": "emacs", "emacs": "emacs",
"⚠️ Please restart boostnote after you change the keymap": "⚠️ 請重新開啟 Boostnote 以完成設定。", "⚠️ Please restart boostnote after you change the keymap": "⚠️ 修改鍵盤配置請重新開啟 Boostnote ",
"Show line numbers in the editor": "在編輯器中顯示行號", "Show line numbers in the editor": "在編輯器中顯示行號",
"Allow editor to scroll past the last line": "允許編輯器捲軸捲動超過最後一行", "Allow editor to scroll past the last line": "允許編輯器捲軸捲動超過最後一行",
"Bring in web page title when pasting URL on editor": "在編輯器貼上網址的時候,自動加上網頁標題", "Bring in web page title when pasting URL on editor": "在編輯器貼上網址的時候,自動加上網頁標題",
@@ -79,7 +79,7 @@
"Analytics": "分析", "Analytics": "分析",
"Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote 收集匿名資料單純只為了提升軟體使用體驗,絕對不收集任何個人資料(包括筆記內容)", "Boostnote collects anonymous data for the sole purpose of improving the application, and strictly does not collect any personal information such the contents of your notes.": "Boostnote 收集匿名資料單純只為了提升軟體使用體驗,絕對不收集任何個人資料(包括筆記內容)",
"You can see how it works on ": "你可以看看它的程式碼是如何運作 ", "You can see how it works on ": "你可以看看它的程式碼是如何運作 ",
"You can choose to enable or disable this option.": "你可以選擇啟用或用這項功能", "You can choose to enable or disable this option.": "你可以選擇啟用或用這項功能",
"Enable analytics to help improve Boostnote": "允許數據分析以協助我們改進 Boostnote", "Enable analytics to help improve Boostnote": "允許數據分析以協助我們改進 Boostnote",
"Crowdfunding": "群眾募資", "Crowdfunding": "群眾募資",
"Dear everyone,": "親愛的用戶:", "Dear everyone,": "親愛的用戶:",
@@ -104,9 +104,9 @@
"Confirm": "確認", "Confirm": "確認",
"Cancel": "取消", "Cancel": "取消",
"Markdown Note": "Markdown 筆記", "Markdown Note": "Markdown 筆記",
"This format is for creating text documents. Checklists, code blocks and Latex blocks are available.": "建立文件、清單,也可以使用程式碼區塊甚至是 Latex 區塊。", "This format is for creating text documents. Checklists, code blocks and Latex blocks are available.": "建立文件、清單,也可以使用程式碼區塊 Latex 區塊。",
"Snippet Note": "程式碼片段筆記", "Snippet Note": "程式碼片段筆記",
"This format is for creating code snippets. Multiple snippets can be grouped into a single note.": "建立程式碼區塊片段。個程式碼區塊可以合在同一個筆記。", "This format is for creating code snippets. Multiple snippets can be grouped into a single note.": "建立程式碼區塊片段。個程式碼區塊可以分組爲同一個筆記。",
"Tab to switch format": "使用 Tab 鍵切換格式", "Tab to switch format": "使用 Tab 鍵切換格式",
"Updated": "依更新時間排序", "Updated": "依更新時間排序",
"Created": "依建立時間排序", "Created": "依建立時間排序",
@@ -117,12 +117,12 @@
"Blog Type": "部落格類型", "Blog Type": "部落格類型",
"Blog Address": "部落格網址", "Blog Address": "部落格網址",
"Save": "儲存", "Save": "儲存",
"Auth": "Auth", "Auth": "驗證",
"Authentication Method": "認證方法", "Authentication Method": "認證方法",
"JWT": "JWT", "JWT": "JWT",
"USER": "USER", "USER": "USER",
"Token": "Token", "Token": "Token",
"Storage": "本機儲存空間", "Storage": "儲存空間",
"Hotkeys": "快捷鍵", "Hotkeys": "快捷鍵",
"Show/Hide Boostnote": "顯示/隱藏 Boostnote", "Show/Hide Boostnote": "顯示/隱藏 Boostnote",
"Restore": "還原", "Restore": "還原",
@@ -144,9 +144,11 @@
"Russian": "Russian", "Russian": "Russian",
"Editor Rulers": "編輯器中顯示垂直尺規", "Editor Rulers": "編輯器中顯示垂直尺規",
"Enable": "啟用", "Enable": "啟用",
"Disable": "用", "Disable": "用",
"Sanitization": "過濾 HTML 程式碼", "Sanitization": "過濾 HTML 程式碼",
"Only allow secure html tags (recommended)": "只允許安全的 HTML 標籤 (建議)", "Only allow secure html tags (recommended)": "只允許安全的 HTML 標籤 (建議)",
"Allow styles": "允許樣式", "Allow styles": "允許樣式",
"Allow dangerous html tags": "允許危險的 HTML 標籤" "Allow dangerous html tags": "允許危險的 HTML 標籤",
"Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.": "Convert textual arrows to beautiful signs. ⚠ This will interfere with using HTML comments in your Markdown.",
"⚠ 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! ⚠"
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "boost", "name": "boost",
"productName": "Boostnote", "productName": "Boostnote",
"version": "0.11.4", "version": "0.11.7",
"main": "index.js", "main": "index.js",
"description": "Boostnote", "description": "Boostnote",
"license": "GPL-3.0", "license": "GPL-3.0",
@@ -12,12 +12,12 @@
"compile": "grunt compile", "compile": "grunt compile",
"test": "PWD=$(pwd) NODE_ENV=test ava --serial", "test": "PWD=$(pwd) NODE_ENV=test ava --serial",
"jest": "jest", "jest": "jest",
"fix": "npm run lint --fix", "fix": "eslint . --fix",
"lint": "eslint .", "lint": "eslint .",
"dev-start": "concurrently --kill-others \"npm run webpack\" \"npm run hot\"" "dev-start": "concurrently --kill-others \"npm run webpack\" \"npm run hot\""
}, },
"config": { "config": {
"electron-version": "1.7.11" "electron-version": "2.0.3"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -53,13 +53,16 @@
"@rokt33r/season": "^5.3.0", "@rokt33r/season": "^5.3.0",
"aws-sdk": "^2.48.0", "aws-sdk": "^2.48.0",
"aws-sdk-mobile-analytics": "^0.9.2", "aws-sdk-mobile-analytics": "^0.9.2",
"codemirror": "^5.37.0", "codemirror": "^5.39.0",
"codemirror-mode-elixir": "^1.1.1", "codemirror-mode-elixir": "^1.1.1",
"electron-config": "^0.2.1", "electron-config": "^0.2.1",
"electron-gh-releases": "^2.0.2", "electron-gh-releases": "^2.0.2",
"escape-string-regexp": "^1.0.5",
"file-url": "^2.0.2",
"filenamify": "^2.0.0", "filenamify": "^2.0.0",
"flowchart.js": "^1.6.5", "flowchart.js": "^1.6.5",
"font-awesome": "^4.3.0", "font-awesome": "^4.3.0",
"fs-extra": "^5.0.0",
"i18n-2": "^0.7.2", "i18n-2": "^0.7.2",
"iconv-lite": "^0.4.19", "iconv-lite": "^0.4.19",
"immutable": "^3.8.1", "immutable": "^3.8.1",
@@ -68,21 +71,24 @@
"lodash": "^4.11.1", "lodash": "^4.11.1",
"lodash-move": "^1.1.1", "lodash-move": "^1.1.1",
"markdown-it": "^6.0.1", "markdown-it": "^6.0.1",
"markdown-it-checkbox": "^1.1.0", "markdown-it-admonition": "https://github.com/johannbre/markdown-it-admonition.git",
"markdown-it-emoji": "^1.1.1", "markdown-it-emoji": "^1.1.1",
"markdown-it-footnote": "^3.0.0", "markdown-it-footnote": "^3.0.0",
"markdown-it-imsize": "^2.0.1", "markdown-it-imsize": "^2.0.1",
"markdown-it-kbd": "^1.1.1", "markdown-it-kbd": "^1.1.1",
"markdown-it-multimd-table": "^2.0.1", "markdown-it-multimd-table": "^2.0.1",
"markdown-it-named-headers": "^0.0.4", "markdown-it-named-headers": "^0.0.4",
"markdown-it-plantuml": "^0.3.0", "markdown-it-plantuml": "^1.1.0",
"md5": "^2.0.0", "markdown-it-smartarrows": "^1.0.1",
"mdurl": "^1.0.1", "mdurl": "^1.0.1",
"moment": "^2.10.3", "moment": "^2.10.3",
"mousetrap": "^1.6.1",
"mousetrap-global-bind": "^1.1.0",
"node-ipc": "^8.1.0", "node-ipc": "^8.1.0",
"raphael": "^2.2.7", "raphael": "^2.2.7",
"react": "^15.5.4", "react": "^15.5.4",
"react-codemirror": "^0.3.0", "react-codemirror": "^0.3.0",
"react-debounce-render": "^4.0.1",
"react-dom": "^15.0.2", "react-dom": "^15.0.2",
"react-redux": "^4.4.5", "react-redux": "^4.4.5",
"react-sortable-hoc": "^0.6.7", "react-sortable-hoc": "^0.6.7",
@@ -90,8 +96,6 @@
"sander": "^0.5.1", "sander": "^0.5.1",
"sanitize-html": "^1.18.2", "sanitize-html": "^1.18.2",
"striptags": "^2.2.1", "striptags": "^2.2.1",
"superagent": "^1.2.0",
"superagent-promise": "^1.0.3",
"unique-slug": "2.0.0", "unique-slug": "2.0.0",
"uuid": "^3.2.1" "uuid": "^3.2.1"
}, },
@@ -113,12 +117,12 @@
"css-loader": "^0.19.0", "css-loader": "^0.19.0",
"devtron": "^1.1.0", "devtron": "^1.1.0",
"dom-storage": "^2.0.2", "dom-storage": "^2.0.2",
"electron": "1.7.11", "electron": "2.0.3",
"electron-packager": "^6.0.0", "electron-packager": "^6.0.0",
"eslint": "^3.13.1", "eslint": "^3.13.1",
"eslint-config-standard": "^6.2.1", "eslint-config-standard": "^6.2.1",
"eslint-config-standard-jsx": "^3.2.0", "eslint-config-standard-jsx": "^3.2.0",
"eslint-plugin-react": "^7.2.0", "eslint-plugin-react": "^7.8.2",
"eslint-plugin-standard": "^3.0.1", "eslint-plugin-standard": "^3.0.1",
"faker": "^3.1.0", "faker": "^3.1.0",
"grunt": "^0.4.5", "grunt": "^0.4.5",

View File

@@ -25,7 +25,7 @@ Boostnote is an open source project. It's an independent project with its ongoin
## Community ## Community
- [Facebook Group](https://www.facebook.com/groups/boostnote/) - [Facebook Group](https://www.facebook.com/groups/boostnote/)
- [Twitter](https://twitter.com/boostnoteapp) - [Twitter](https://twitter.com/boostnoteapp)
- [Slack Group](https://join.slack.com/t/boostnote-group/shared_invite/enQtMzUxODgwMTc2MDg3LTgwZjA2Zjg3NjFlMzczNTVjNGMzZTk0MmIyNmE3ZjEwYTNhMTA0Y2Y4NDNlNWU4YjZlNmJiNGZhNDViOTA1ZjM) - [Slack Group](https://join.slack.com/t/boostnote-group/shared_invite/enQtMzkxOTk4ODkyNzc0LThkNmMzY2VlZjVhYTNiYjE5YjQyZGVjNTJlYTY1OGMyZTFjNGU5YTUyYjUzOWZhYTU4OTVlNDYyNDFjYWMzNDM)
- [Blog](https://boostlog.io/tags/boostnote) - [Blog](https://boostlog.io/tags/boostnote)
- [Reddit](https://www.reddit.com/r/Boostnote/) - [Reddit](https://www.reddit.com/r/Boostnote/)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -7,6 +7,9 @@ const findStorage = require('browser/lib/findStorage')
jest.mock('unique-slug') jest.mock('unique-slug')
const uniqueSlug = require('unique-slug') const uniqueSlug = require('unique-slug')
const mdurl = require('mdurl') const mdurl = require('mdurl')
const fse = require('fs-extra')
jest.mock('sander')
const sander = require('sander')
const systemUnderTest = require('browser/main/lib/dataApi/attachmentManagement') const systemUnderTest = require('browser/main/lib/dataApi/attachmentManagement')
@@ -48,11 +51,13 @@ it('should test that copyAttachment works correctly assuming correct working of
const noteKey = 'noteKey' const noteKey = 'noteKey'
const dummyUniquePath = 'dummyPath' const dummyUniquePath = 'dummyPath'
const dummyStorage = {path: 'dummyStoragePath'} const dummyStorage = {path: 'dummyStoragePath'}
const dummyReadStream = {}
dummyReadStream.pipe = jest.fn()
dummyReadStream.on = jest.fn((event, callback) => { callback() })
fs.existsSync = jest.fn() fs.existsSync = jest.fn()
fs.existsSync.mockReturnValue(true) fs.existsSync.mockReturnValue(true)
fs.createReadStream = jest.fn() fs.createReadStream = jest.fn(() => dummyReadStream)
fs.createReadStream.mockReturnValue({pipe: jest.fn()})
fs.createWriteStream = jest.fn() fs.createWriteStream = jest.fn()
findStorage.findStorage = jest.fn() findStorage.findStorage = jest.fn()
@@ -75,7 +80,11 @@ it('should test that copyAttachment creates a new folder if the attachment folde
const noteKey = 'noteKey' const noteKey = 'noteKey'
const attachmentFolderPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER) const attachmentFolderPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER)
const attachmentFolderNoteKyPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, noteKey) const attachmentFolderNoteKyPath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, noteKey)
const dummyReadStream = {}
dummyReadStream.pipe = jest.fn()
dummyReadStream.on = jest.fn()
fs.createReadStream = jest.fn(() => dummyReadStream)
fs.existsSync = jest.fn() fs.existsSync = jest.fn()
fs.existsSync.mockReturnValueOnce(true) fs.existsSync.mockReturnValueOnce(true)
fs.existsSync.mockReturnValueOnce(false) fs.existsSync.mockReturnValueOnce(false)
@@ -97,7 +106,11 @@ it('should test that copyAttachment creates a new folder if the attachment folde
it('should test that copyAttachment don\'t uses a random file name if not intended ', function () { it('should test that copyAttachment don\'t uses a random file name if not intended ', function () {
const dummyStorage = {path: 'dummyStoragePath'} const dummyStorage = {path: 'dummyStoragePath'}
const dummyReadStream = {}
dummyReadStream.pipe = jest.fn()
dummyReadStream.on = jest.fn()
fs.createReadStream = jest.fn(() => dummyReadStream)
fs.existsSync = jest.fn() fs.existsSync = jest.fn()
fs.existsSync.mockReturnValueOnce(true) fs.existsSync.mockReturnValueOnce(true)
fs.existsSync.mockReturnValueOnce(false) fs.existsSync.mockReturnValueOnce(false)
@@ -187,7 +200,7 @@ it('should test that getAttachmentsInContent finds all attachments', function ()
' </body>\n' + ' </body>\n' +
'</html>' '</html>'
const actual = systemUnderTest.getAttachmentsInContent(testInput) const actual = systemUnderTest.getAttachmentsInContent(testInput)
const expected = [':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg'] const expected = [':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp.png', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx.pdf', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg']
expect(actual).toEqual(expect.arrayContaining(expected)) expect(actual).toEqual(expect.arrayContaining(expected))
}) })
@@ -212,8 +225,8 @@ it('should test that getAbsolutePathsOfAttachmentsInContent returns all absolute
' </body>\n' + ' </body>\n' +
'</html>' '</html>'
const actual = systemUnderTest.getAbsolutePathsOfAttachmentsInContent(testInput, dummyStoragePath) const actual = systemUnderTest.getAbsolutePathsOfAttachmentsInContent(testInput, dummyStoragePath)
const expected = [dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp', const expected = [dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp.png',
dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx', dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx.pdf',
dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg'] dummyStoragePath + path.sep + systemUnderTest.DESTINATION_FOLDER + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg']
expect(actual).toEqual(expect.arrayContaining(expected)) expect(actual).toEqual(expect.arrayContaining(expected))
}) })
@@ -261,6 +274,19 @@ it('should remove the all ":storage" and noteKey references', function () {
expect(actual).toEqual(expectedOutput) expect(actual).toEqual(expectedOutput)
}) })
it('should delete the correct attachment folder if a note is deleted', function () {
const dummyStorage = {path: 'dummyStoragePath'}
const storageKey = 'storageKey'
const noteKey = 'noteKey'
findStorage.findStorage = jest.fn(() => dummyStorage)
sander.rimrafSync = jest.fn()
const expectedPathToBeDeleted = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, noteKey)
systemUnderTest.deleteAttachmentFolder(storageKey, noteKey)
expect(findStorage.findStorage).toHaveBeenCalledWith(storageKey)
expect(sander.rimrafSync).toHaveBeenCalledWith(expectedPathToBeDeleted)
})
it('should test that deleteAttachmentsNotPresentInNote deletes all unreferenced attachments ', function () { it('should test that deleteAttachmentsNotPresentInNote deletes all unreferenced attachments ', function () {
const dummyStorage = {path: 'dummyStoragePath'} const dummyStorage = {path: 'dummyStoragePath'}
const noteKey = 'noteKey' const noteKey = 'noteKey'
@@ -312,3 +338,402 @@ it('should test that deleteAttachmentsNotPresentInNote does not delete reference
} }
expect(fsUnlinkCallArguments.includes(path.join(attachmentFolderPath, dummyFilesInFolder[0]))).toBe(false) expect(fsUnlinkCallArguments.includes(path.join(attachmentFolderPath, dummyFilesInFolder[0]))).toBe(false)
}) })
it('should test that deleteAttachmentsNotPresentInNote does nothing if noteKey, storageKey or noteContent was null', function () {
const noteKey = null
const storageKey = null
const markdownContent = ''
findStorage.findStorage = jest.fn()
fs.existsSync = jest.fn()
fs.readdir = jest.fn()
fs.unlink = jest.fn()
systemUnderTest.deleteAttachmentsNotPresentInNote(markdownContent, storageKey, noteKey)
expect(fs.existsSync).not.toHaveBeenCalled()
expect(fs.readdir).not.toHaveBeenCalled()
expect(fs.unlink).not.toHaveBeenCalled()
})
it('should test that deleteAttachmentsNotPresentInNote does nothing if noteKey, storageKey or noteContent was undefined', function () {
const noteKey = undefined
const storageKey = undefined
const markdownContent = ''
findStorage.findStorage = jest.fn()
fs.existsSync = jest.fn()
fs.readdir = jest.fn()
fs.unlink = jest.fn()
systemUnderTest.deleteAttachmentsNotPresentInNote(markdownContent, storageKey, noteKey)
expect(fs.existsSync).not.toHaveBeenCalled()
expect(fs.readdir).not.toHaveBeenCalled()
expect(fs.unlink).not.toHaveBeenCalled()
})
it('should test that moveAttachments moves attachments only if the source folder existed', function () {
fse.existsSync = jest.fn(() => false)
fse.moveSync = jest.fn()
const oldPath = 'oldPath'
const newPath = 'newPath'
const oldNoteKey = 'oldNoteKey'
const newNoteKey = 'newNoteKey'
const content = ''
const expectedSource = path.join(oldPath, systemUnderTest.DESTINATION_FOLDER, oldNoteKey)
systemUnderTest.moveAttachments(oldPath, newPath, oldNoteKey, newNoteKey, content)
expect(fse.existsSync).toHaveBeenCalledWith(expectedSource)
expect(fse.moveSync).not.toHaveBeenCalled()
})
it('should test that moveAttachments moves attachments to the right destination', function () {
fse.existsSync = jest.fn(() => true)
fse.moveSync = jest.fn()
const oldPath = 'oldPath'
const newPath = 'newPath'
const oldNoteKey = 'oldNoteKey'
const newNoteKey = 'newNoteKey'
const content = ''
const expectedSource = path.join(oldPath, systemUnderTest.DESTINATION_FOLDER, oldNoteKey)
const expectedDestination = path.join(newPath, systemUnderTest.DESTINATION_FOLDER, newNoteKey)
systemUnderTest.moveAttachments(oldPath, newPath, oldNoteKey, newNoteKey, content)
expect(fse.existsSync).toHaveBeenCalledWith(expectedSource)
expect(fse.moveSync).toHaveBeenCalledWith(expectedSource, expectedDestination)
})
it('should test that moveAttachments returns a correct modified content version', function () {
fse.existsSync = jest.fn()
fse.moveSync = jest.fn()
const oldPath = 'oldPath'
const newPath = 'newPath'
const oldNoteKey = 'oldNoteKey'
const newNoteKey = 'newNoteKey'
const testInput =
'Test input' +
'![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNoteKey + path.sep + 'image.jpg](imageName}) \n' +
'[' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNoteKey + path.sep + 'pdf.pdf](pdf})'
const expectedOutput =
'Test input' +
'![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + newNoteKey + path.sep + 'image.jpg](imageName}) \n' +
'[' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + newNoteKey + path.sep + 'pdf.pdf](pdf})'
const actualContent = systemUnderTest.moveAttachments(oldPath, newPath, oldNoteKey, newNoteKey, testInput)
expect(actualContent).toBe(expectedOutput)
})
it('should test that cloneAttachments modifies the content of the new note correctly', function () {
const oldNote = {key: 'oldNoteKey', content: 'oldNoteContent', storage: 'storageKey', type: 'MARKDOWN_NOTE'}
const newNote = {key: 'newNoteKey', content: 'oldNoteContent', storage: 'storageKey', type: 'MARKDOWN_NOTE'}
const testInput =
'Test input' +
'![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNote.key + path.sep + 'image.jpg](imageName}) \n' +
'[' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNote.key + path.sep + 'pdf.pdf](pdf})'
newNote.content = testInput
findStorage.findStorage = jest.fn()
findStorage.findStorage.mockReturnValue({path: 'dummyStoragePath'})
const expectedOutput =
'Test input' +
'![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + newNote.key + path.sep + 'image.jpg](imageName}) \n' +
'[' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + newNote.key + path.sep + 'pdf.pdf](pdf})'
systemUnderTest.cloneAttachments(oldNote, newNote)
expect(newNote.content).toBe(expectedOutput)
})
it('should test that cloneAttachments finds all attachments and copies them to the new location', function () {
const storagePathOld = 'storagePathOld'
const storagePathNew = 'storagePathNew'
const dummyStorageOld = {path: storagePathOld}
const dummyStorageNew = {path: storagePathNew}
const oldNote = {key: 'oldNoteKey', content: 'oldNoteContent', storage: 'storageKeyOldNote', type: 'MARKDOWN_NOTE'}
const newNote = {key: 'newNoteKey', content: 'oldNoteContent', storage: 'storageKeyNewNote', type: 'MARKDOWN_NOTE'}
const testInput =
'Test input' +
'![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNote.key + path.sep + 'image.jpg](imageName}) \n' +
'[' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + oldNote.key + path.sep + 'pdf.pdf](pdf})'
oldNote.content = testInput
newNote.content = testInput
const copyFileSyncResp = {to: jest.fn()}
sander.copyFileSync = jest.fn()
sander.copyFileSync.mockReturnValue(copyFileSyncResp)
findStorage.findStorage = jest.fn()
findStorage.findStorage.mockReturnValueOnce(dummyStorageOld)
findStorage.findStorage.mockReturnValue(dummyStorageNew)
const pathAttachmentOneFrom = path.join(storagePathOld, systemUnderTest.DESTINATION_FOLDER, oldNote.key, 'image.jpg')
const pathAttachmentOneTo = path.join(storagePathNew, systemUnderTest.DESTINATION_FOLDER, newNote.key, 'image.jpg')
const pathAttachmentTwoFrom = path.join(storagePathOld, systemUnderTest.DESTINATION_FOLDER, oldNote.key, 'pdf.pdf')
const pathAttachmentTwoTo = path.join(storagePathNew, systemUnderTest.DESTINATION_FOLDER, newNote.key, 'pdf.pdf')
systemUnderTest.cloneAttachments(oldNote, newNote)
expect(findStorage.findStorage).toHaveBeenCalledWith(oldNote.storage)
expect(findStorage.findStorage).toHaveBeenCalledWith(newNote.storage)
expect(sander.copyFileSync).toHaveBeenCalledTimes(2)
expect(copyFileSyncResp.to).toHaveBeenCalledTimes(2)
expect(sander.copyFileSync.mock.calls[0][0]).toBe(pathAttachmentOneFrom)
expect(copyFileSyncResp.to.mock.calls[0][0]).toBe(pathAttachmentOneTo)
expect(sander.copyFileSync.mock.calls[1][0]).toBe(pathAttachmentTwoFrom)
expect(copyFileSyncResp.to.mock.calls[1][0]).toBe(pathAttachmentTwoTo)
})
it('should test that cloneAttachments finds all attachments and copies them to the new location', function () {
const oldNote = {key: 'oldNoteKey', content: 'oldNoteContent', storage: 'storageKeyOldNote', type: 'SOMETHING_ELSE'}
const newNote = {key: 'newNoteKey', content: 'oldNoteContent', storage: 'storageKeyNewNote', type: 'SOMETHING_ELSE'}
const testInput = 'Test input'
oldNote.content = testInput
newNote.content = testInput
sander.copyFileSync = jest.fn()
findStorage.findStorage = jest.fn()
systemUnderTest.cloneAttachments(oldNote, newNote)
expect(findStorage.findStorage).not.toHaveBeenCalled()
expect(sander.copyFileSync).not.toHaveBeenCalled()
})
it('should test that isAttachmentLink works correctly', function () {
expect(systemUnderTest.isAttachmentLink('text')).toBe(false)
expect(systemUnderTest.isAttachmentLink('text [linkText](link)')).toBe(false)
expect(systemUnderTest.isAttachmentLink('text ![linkText](link)')).toBe(false)
expect(systemUnderTest.isAttachmentLink('[linkText](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + 'noteKey' + path.sep + 'pdf.pdf)')).toBe(true)
expect(systemUnderTest.isAttachmentLink('![linkText](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + 'noteKey' + path.sep + 'pdf.pdf )')).toBe(true)
expect(systemUnderTest.isAttachmentLink('text [ linkText](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + 'noteKey' + path.sep + 'pdf.pdf)')).toBe(true)
expect(systemUnderTest.isAttachmentLink('text ![linkText ](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + 'noteKey' + path.sep + 'pdf.pdf)')).toBe(true)
expect(systemUnderTest.isAttachmentLink('[linkText](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + 'noteKey' + path.sep + 'pdf.pdf) test')).toBe(true)
expect(systemUnderTest.isAttachmentLink('![linkText](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + 'noteKey' + path.sep + 'pdf.pdf) test')).toBe(true)
expect(systemUnderTest.isAttachmentLink('text [linkText](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + 'noteKey' + path.sep + 'pdf.pdf) test')).toBe(true)
expect(systemUnderTest.isAttachmentLink('text ![linkText](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + 'noteKey' + path.sep + 'pdf.pdf) test')).toBe(true)
})
it('should test that handleAttachmentLinkPaste copies the attachments to the new location', function () {
const dummyStorage = {path: 'dummyStoragePath'}
findStorage.findStorage = jest.fn(() => dummyStorage)
const pastedNoteKey = 'b1e06f81-8266-49b9-b438-084003c2e723'
const newNoteKey = 'abc234-8266-49b9-b438-084003c2e723'
const pasteText = 'text ![alt](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'pdf.pdf)'
const storageKey = 'storageKey'
const expectedSourceFilePath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, pastedNoteKey, 'pdf.pdf')
sander.exists = jest.fn(() => Promise.resolve(true))
systemUnderTest.copyAttachment = jest.fn(() => Promise.resolve('dummyNewFileName'))
return systemUnderTest.handleAttachmentLinkPaste(storageKey, newNoteKey, pasteText)
.then(() => {
expect(findStorage.findStorage).toHaveBeenCalledWith(storageKey)
expect(sander.exists).toHaveBeenCalledWith(expectedSourceFilePath)
expect(systemUnderTest.copyAttachment).toHaveBeenCalledWith(expectedSourceFilePath, storageKey, newNoteKey)
})
})
it('should test that handleAttachmentLinkPaste don\'t try to copy the file if it does not exist', function () {
const dummyStorage = {path: 'dummyStoragePath'}
findStorage.findStorage = jest.fn(() => dummyStorage)
const pastedNoteKey = 'b1e06f81-8266-49b9-b438-084003c2e723'
const newNoteKey = 'abc234-8266-49b9-b438-084003c2e723'
const pasteText = 'text ![alt](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'pdf.pdf)'
const storageKey = 'storageKey'
const expectedSourceFilePath = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, pastedNoteKey, 'pdf.pdf')
sander.exists = jest.fn(() => Promise.resolve(false))
systemUnderTest.copyAttachment = jest.fn()
systemUnderTest.generateFileNotFoundMarkdown = jest.fn()
return systemUnderTest.handleAttachmentLinkPaste(storageKey, newNoteKey, pasteText)
.then(() => {
expect(findStorage.findStorage).toHaveBeenCalledWith(storageKey)
expect(sander.exists).toHaveBeenCalledWith(expectedSourceFilePath)
expect(systemUnderTest.copyAttachment).not.toHaveBeenCalled()
})
})
it('should test that handleAttachmentLinkPaste copies multiple attachments if multiple were pasted', function () {
const dummyStorage = {path: 'dummyStoragePath'}
findStorage.findStorage = jest.fn(() => dummyStorage)
const pastedNoteKey = 'b1e06f81-8266-49b9-b438-084003c2e723'
const newNoteKey = 'abc234-8266-49b9-b438-084003c2e723'
const pasteText = 'text ![alt](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'pdf.pdf) ..' +
'![secondAttachment](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'img.jpg)'
const storageKey = 'storageKey'
const expectedSourceFilePathOne = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, pastedNoteKey, 'pdf.pdf')
const expectedSourceFilePathTwo = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, pastedNoteKey, 'img.jpg')
sander.exists = jest.fn(() => Promise.resolve(true))
systemUnderTest.copyAttachment = jest.fn(() => Promise.resolve('dummyNewFileName'))
return systemUnderTest.handleAttachmentLinkPaste(storageKey, newNoteKey, pasteText)
.then(() => {
expect(findStorage.findStorage).toHaveBeenCalledWith(storageKey)
expect(sander.exists).toHaveBeenCalledWith(expectedSourceFilePathOne)
expect(sander.exists).toHaveBeenCalledWith(expectedSourceFilePathTwo)
expect(systemUnderTest.copyAttachment).toHaveBeenCalledWith(expectedSourceFilePathOne, storageKey, newNoteKey)
expect(systemUnderTest.copyAttachment).toHaveBeenCalledWith(expectedSourceFilePathTwo, storageKey, newNoteKey)
})
})
it('should test that handleAttachmentLinkPaste returns the correct modified paste text', function () {
const dummyStorage = {path: 'dummyStoragePath'}
findStorage.findStorage = jest.fn(() => dummyStorage)
const pastedNoteKey = 'b1e06f81-8266-49b9-b438-084003c2e723'
const newNoteKey = 'abc234-8266-49b9-b438-084003c2e723'
const dummyNewFileName = 'dummyNewFileName'
const pasteText = 'text ![alt](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'pdf.pdf)'
const expectedText = 'text ![alt](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + newNoteKey + path.sep + dummyNewFileName + ')'
const storageKey = 'storageKey'
sander.exists = jest.fn(() => Promise.resolve(true))
systemUnderTest.copyAttachment = jest.fn(() => Promise.resolve(dummyNewFileName))
return systemUnderTest.handleAttachmentLinkPaste(storageKey, newNoteKey, pasteText)
.then((returnedPastedText) => {
expect(returnedPastedText).toBe(expectedText)
})
})
it('should test that handleAttachmentLinkPaste returns the correct modified paste text if multiple links are posted', function () {
const dummyStorage = {path: 'dummyStoragePath'}
findStorage.findStorage = jest.fn(() => dummyStorage)
const pastedNoteKey = 'b1e06f81-8266-49b9-b438-084003c2e723'
const newNoteKey = 'abc234-8266-49b9-b438-084003c2e723'
const dummyNewFileNameOne = 'dummyNewFileName'
const dummyNewFileNameTwo = 'dummyNewFileNameTwo'
const pasteText = 'text ![alt](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'pdf.pdf) ' +
'![secondImage](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'img.jpg)'
const expectedText = 'text ![alt](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + newNoteKey + path.sep + dummyNewFileNameOne + ') ' +
'![secondImage](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + newNoteKey + path.sep + dummyNewFileNameTwo + ')'
const storageKey = 'storageKey'
sander.exists = jest.fn(() => Promise.resolve(true))
systemUnderTest.copyAttachment = jest.fn()
systemUnderTest.copyAttachment.mockReturnValueOnce(Promise.resolve(dummyNewFileNameOne))
systemUnderTest.copyAttachment.mockReturnValue(Promise.resolve(dummyNewFileNameTwo))
return systemUnderTest.handleAttachmentLinkPaste(storageKey, newNoteKey, pasteText)
.then((returnedPastedText) => {
expect(returnedPastedText).toBe(expectedText)
})
})
it('should test that handleAttachmentLinkPaste calls the copy method correct if multiple links are posted where one file was found and one was not', function () {
const dummyStorage = {path: 'dummyStoragePath'}
findStorage.findStorage = jest.fn(() => dummyStorage)
const pastedNoteKey = 'b1e06f81-8266-49b9-b438-084003c2e723'
const newNoteKey = 'abc234-8266-49b9-b438-084003c2e723'
const pasteText = 'text ![alt](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'pdf.pdf) ..' +
'![secondAttachment](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'img.jpg)'
const storageKey = 'storageKey'
const expectedSourceFilePathOne = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, pastedNoteKey, 'pdf.pdf')
const expectedSourceFilePathTwo = path.join(dummyStorage.path, systemUnderTest.DESTINATION_FOLDER, pastedNoteKey, 'img.jpg')
sander.exists = jest.fn()
sander.exists.mockReturnValueOnce(Promise.resolve(false))
sander.exists.mockReturnValue(Promise.resolve(true))
systemUnderTest.copyAttachment = jest.fn(() => Promise.resolve('dummyNewFileName'))
systemUnderTest.generateFileNotFoundMarkdown = jest.fn()
return systemUnderTest.handleAttachmentLinkPaste(storageKey, newNoteKey, pasteText)
.then(() => {
expect(findStorage.findStorage).toHaveBeenCalledWith(storageKey)
expect(sander.exists).toHaveBeenCalledWith(expectedSourceFilePathOne)
expect(sander.exists).toHaveBeenCalledWith(expectedSourceFilePathTwo)
expect(systemUnderTest.copyAttachment).toHaveBeenCalledTimes(1)
expect(systemUnderTest.copyAttachment).toHaveBeenCalledWith(expectedSourceFilePathTwo, storageKey, newNoteKey)
})
})
it('should test that handleAttachmentLinkPaste returns the correct modified paste text if the file was not found', function () {
const dummyStorage = {path: 'dummyStoragePath'}
findStorage.findStorage = jest.fn(() => dummyStorage)
const pastedNoteKey = 'b1e06f81-8266-49b9-b438-084003c2e723'
const newNoteKey = 'abc234-8266-49b9-b438-084003c2e723'
const pasteText = 'text ![alt.png](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'pdf.pdf)'
const storageKey = 'storageKey'
const fileNotFoundMD = 'file not found'
const expectedPastText = 'text ' + fileNotFoundMD
systemUnderTest.generateFileNotFoundMarkdown = jest.fn(() => fileNotFoundMD)
sander.exists = jest.fn(() => Promise.resolve(false))
return systemUnderTest.handleAttachmentLinkPaste(storageKey, newNoteKey, pasteText)
.then((returnedPastedText) => {
expect(returnedPastedText).toBe(expectedPastText)
})
})
it('should test that handleAttachmentLinkPaste returns the correct modified paste text if multiple files were not found', function () {
const dummyStorage = {path: 'dummyStoragePath'}
findStorage.findStorage = jest.fn(() => dummyStorage)
const pastedNoteKey = 'b1e06f81-8266-49b9-b438-084003c2e723'
const newNoteKey = 'abc234-8266-49b9-b438-084003c2e723'
const pasteText = 'text ![alt](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'pdf.pdf) ' +
'![secondImage](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'img.jpg)'
const storageKey = 'storageKey'
const fileNotFoundMD = 'file not found'
const expectedPastText = 'text ' + fileNotFoundMD + ' ' + fileNotFoundMD
systemUnderTest.generateFileNotFoundMarkdown = jest.fn(() => fileNotFoundMD)
sander.exists = jest.fn(() => Promise.resolve(false))
return systemUnderTest.handleAttachmentLinkPaste(storageKey, newNoteKey, pasteText)
.then((returnedPastedText) => {
expect(returnedPastedText).toBe(expectedPastText)
})
})
it('should test that handleAttachmentLinkPaste returns the correct modified paste text if one file was found and one was not found', function () {
const dummyStorage = {path: 'dummyStoragePath'}
findStorage.findStorage = jest.fn(() => dummyStorage)
const pastedNoteKey = 'b1e06f81-8266-49b9-b438-084003c2e723'
const newNoteKey = 'abc234-8266-49b9-b438-084003c2e723'
const dummyFoundFileName = 'dummyFileName'
const fileNotFoundMD = 'file not found'
const pasteText = 'text ![alt](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'pdf.pdf) .. ' +
'![secondAttachment](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'img.jpg)'
const storageKey = 'storageKey'
const expectedPastText = 'text ' + fileNotFoundMD + ' .. ![secondAttachment](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + newNoteKey + path.sep + dummyFoundFileName + ')'
sander.exists = jest.fn()
sander.exists.mockReturnValueOnce(Promise.resolve(false))
sander.exists.mockReturnValue(Promise.resolve(true))
systemUnderTest.copyAttachment = jest.fn(() => Promise.resolve(dummyFoundFileName))
systemUnderTest.generateFileNotFoundMarkdown = jest.fn(() => fileNotFoundMD)
return systemUnderTest.handleAttachmentLinkPaste(storageKey, newNoteKey, pasteText)
.then((returnedPastedText) => {
expect(returnedPastedText).toBe(expectedPastText)
})
})
it('should test that handleAttachmentLinkPaste returns the correct modified paste text if one file was found and one was not found', function () {
const dummyStorage = {path: 'dummyStoragePath'}
findStorage.findStorage = jest.fn(() => dummyStorage)
const pastedNoteKey = 'b1e06f81-8266-49b9-b438-084003c2e723'
const newNoteKey = 'abc234-8266-49b9-b438-084003c2e723'
const dummyFoundFileName = 'dummyFileName'
const fileNotFoundMD = 'file not found'
const pasteText = 'text ![alt](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'pdf.pdf) .. ' +
'![secondAttachment](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + pastedNoteKey + path.sep + 'img.jpg)'
const storageKey = 'storageKey'
const expectedPastText = 'text ![alt](' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + newNoteKey + path.sep + dummyFoundFileName + ') .. ' + fileNotFoundMD
sander.exists = jest.fn()
sander.exists.mockReturnValueOnce(Promise.resolve(true))
sander.exists.mockReturnValue(Promise.resolve(false))
systemUnderTest.copyAttachment = jest.fn(() => Promise.resolve(dummyFoundFileName))
systemUnderTest.generateFileNotFoundMarkdown = jest.fn(() => fileNotFoundMD)
return systemUnderTest.handleAttachmentLinkPaste(storageKey, newNoteKey, pasteText)
.then((returnedPastedText) => {
expect(returnedPastedText).toBe(expectedPastText)
})
})

View File

@@ -0,0 +1,34 @@
const test = require('ava')
const createSnippet = require('browser/main/lib/dataApi/createSnippet')
const sander = require('sander')
const os = require('os')
const path = require('path')
const snippetFilePath = path.join(os.tmpdir(), 'test', 'create-snippet')
const snippetFile = path.join(snippetFilePath, 'snippets.json')
test.beforeEach((t) => {
sander.writeFileSync(snippetFile, '[]')
})
test.serial('Create a snippet', (t) => {
return Promise.resolve()
.then(function doTest () {
return Promise.all([
createSnippet(snippetFile)
])
})
.then(function assert (data) {
data = data[0]
const snippets = JSON.parse(sander.readFileSync(snippetFile))
const snippet = snippets.find(currentSnippet => currentSnippet.id === data.id)
t.not(snippet, undefined)
t.is(snippet.name, data.name)
t.deepEqual(snippet.prefix, data.prefix)
t.is(snippet.content, data.content)
})
})
test.after.always(() => {
sander.rimrafSync(snippetFilePath)
})

View File

@@ -1,5 +1,9 @@
const test = require('ava') const test = require('ava')
const deleteFolder = require('browser/main/lib/dataApi/deleteFolder') const deleteFolder = require('browser/main/lib/dataApi/deleteFolder')
const attachmentManagement = require('browser/main/lib/dataApi/attachmentManagement')
const createNote = require('browser/main/lib/dataApi/createNote')
const fs = require('fs')
const faker = require('faker')
global.document = require('jsdom').jsdom('<body></body>') global.document = require('jsdom').jsdom('<body></body>')
global.window = document.defaultView global.window = document.defaultView
@@ -24,8 +28,32 @@ test.beforeEach((t) => {
test.serial('Delete a folder', (t) => { test.serial('Delete a folder', (t) => {
const storageKey = t.context.storage.cache.key const storageKey = t.context.storage.cache.key
const folderKey = t.context.storage.json.folders[0].key const folderKey = t.context.storage.json.folders[0].key
let noteKey
const input1 = {
type: 'SNIPPET_NOTE',
description: faker.lorem.lines(),
snippets: [{
name: faker.system.fileName(),
mode: 'text',
content: faker.lorem.lines()
}],
tags: faker.lorem.words().split(' '),
folder: folderKey
}
input1.title = input1.description.split('\n').shift()
return Promise.resolve() return Promise.resolve()
.then(function prepare () {
return createNote(storageKey, input1)
.then(function createAttachmentFolder (data) {
fs.mkdirSync(path.join(storagePath, attachmentManagement.DESTINATION_FOLDER))
fs.mkdirSync(path.join(storagePath, attachmentManagement.DESTINATION_FOLDER, data.key))
noteKey = data.key
return data
})
})
.then(function doTest () { .then(function doTest () {
return deleteFolder(storageKey, folderKey) return deleteFolder(storageKey, folderKey)
}) })
@@ -36,6 +64,9 @@ test.serial('Delete a folder', (t) => {
t.true(_.find(jsonData.folders, {key: folderKey}) == null) t.true(_.find(jsonData.folders, {key: folderKey}) == null)
const notePaths = sander.readdirSync(data.storage.path, 'notes') const notePaths = sander.readdirSync(data.storage.path, 'notes')
t.is(notePaths.length, t.context.storage.notes.filter((note) => note.folder !== folderKey).length) t.is(notePaths.length, t.context.storage.notes.filter((note) => note.folder !== folderKey).length)
const attachmentFolderPath = path.join(storagePath, attachmentManagement.DESTINATION_FOLDER, noteKey)
t.false(fs.existsSync(attachmentFolderPath))
}) })
}) })

View File

@@ -14,6 +14,8 @@ const sander = require('sander')
const os = require('os') const os = require('os')
const CSON = require('@rokt33r/season') const CSON = require('@rokt33r/season')
const faker = require('faker') const faker = require('faker')
const fs = require('fs')
const attachmentManagement = require('browser/main/lib/dataApi/attachmentManagement')
const storagePath = path.join(os.tmpdir(), 'test/delete-note') const storagePath = path.join(os.tmpdir(), 'test/delete-note')
@@ -42,6 +44,11 @@ test.serial('Delete a note', (t) => {
return Promise.resolve() return Promise.resolve()
.then(function doTest () { .then(function doTest () {
return createNote(storageKey, input1) return createNote(storageKey, input1)
.then(function createAttachmentFolder (data) {
fs.mkdirSync(path.join(storagePath, attachmentManagement.DESTINATION_FOLDER))
fs.mkdirSync(path.join(storagePath, attachmentManagement.DESTINATION_FOLDER, data.key))
return data
})
.then(function (data) { .then(function (data) {
return deleteNote(storageKey, data.key) return deleteNote(storageKey, data.key)
}) })
@@ -52,8 +59,13 @@ test.serial('Delete a note', (t) => {
t.fail('note cson must be deleted.') t.fail('note cson must be deleted.')
} catch (err) { } catch (err) {
t.is(err.code, 'ENOENT') t.is(err.code, 'ENOENT')
return data
} }
}) })
.then(function assertAttachmentFolderDeleted (data) {
const attachmentFolderPath = path.join(storagePath, attachmentManagement.DESTINATION_FOLDER, data.noteKey)
t.is(fs.existsSync(attachmentFolderPath), false)
})
}) })
test.after(function after () { test.after(function after () {

View File

@@ -0,0 +1,37 @@
const test = require('ava')
const deleteSnippet = require('browser/main/lib/dataApi/deleteSnippet')
const sander = require('sander')
const os = require('os')
const path = require('path')
const crypto = require('crypto')
const snippetFilePath = path.join(os.tmpdir(), 'test', 'delete-snippet')
const snippetFile = path.join(snippetFilePath, 'snippets.json')
const newSnippet = {
id: crypto.randomBytes(16).toString('hex'),
name: 'Unnamed snippet',
prefix: [],
content: ''
}
test.beforeEach((t) => {
sander.writeFileSync(snippetFile, JSON.stringify([newSnippet]))
})
test.serial('Delete a snippet', (t) => {
return Promise.resolve()
.then(function doTest () {
return Promise.all([
deleteSnippet(newSnippet, snippetFile)
])
})
.then(function assert (data) {
data = data[0]
const snippets = JSON.parse(sander.readFileSync(snippetFile))
t.is(snippets.length, 0)
})
})
test.after.always(() => {
sander.rimrafSync(snippetFilePath)
})

View File

@@ -13,6 +13,7 @@ const TestDummy = require('../fixtures/TestDummy')
const os = require('os') const os = require('os')
const faker = require('faker') const faker = require('faker')
const fs = require('fs') const fs = require('fs')
const sander = require('sander')
const storagePath = path.join(os.tmpdir(), 'test/export-note') const storagePath = path.join(os.tmpdir(), 'test/export-note')
@@ -60,3 +61,8 @@ test.serial('Export a folder', (t) => {
t.false(fs.existsSync(filePath)) t.false(fs.existsSync(filePath))
}) })
}) })
test.after.always(function after () {
localStorage.clear()
sander.rimrafSync(storagePath)
})

View File

@@ -0,0 +1,38 @@
const test = require('ava')
const toggleStorage = require('browser/main/lib/dataApi/toggleStorage')
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 _ = require('lodash')
const TestDummy = require('../fixtures/TestDummy')
const sander = require('sander')
const os = require('os')
const storagePath = path.join(os.tmpdir(), 'test/toggle-storage')
test.beforeEach((t) => {
t.context.storage = TestDummy.dummyStorage(storagePath)
localStorage.setItem('storages', JSON.stringify([t.context.storage.cache]))
})
test.serial('Toggle a storage location', (t) => {
const storageKey = t.context.storage.cache.key
return Promise.resolve()
.then(function doTest () {
return toggleStorage(storageKey, true)
})
.then(function assert (data) {
const cachedStorageList = JSON.parse(localStorage.getItem('storages'))
t.true(_.find(cachedStorageList, {key: storageKey}).isOpen === true)
})
})
test.after(function after () {
localStorage.clear()
sander.rimrafSync(storagePath)
})

View File

@@ -0,0 +1,47 @@
const test = require('ava')
const updateSnippet = require('browser/main/lib/dataApi/updateSnippet')
const sander = require('sander')
const os = require('os')
const path = require('path')
const crypto = require('crypto')
const snippetFilePath = path.join(os.tmpdir(), 'test', 'update-snippet')
const snippetFile = path.join(snippetFilePath, 'snippets.json')
const oldSnippet = {
id: crypto.randomBytes(16).toString('hex'),
name: 'Initial snippet',
prefix: [],
content: ''
}
const newSnippet = {
id: oldSnippet.id,
name: 'new name',
prefix: ['prefix'],
content: 'new content'
}
test.beforeEach((t) => {
sander.writeFileSync(snippetFile, JSON.stringify([oldSnippet]))
})
test.serial('Update a snippet', (t) => {
return Promise.resolve()
.then(function doTest () {
return Promise.all([
updateSnippet(newSnippet, snippetFile)
])
})
.then(function assert () {
const snippets = JSON.parse(sander.readFileSync(snippetFile))
const snippet = snippets.find(currentSnippet => currentSnippet.id === newSnippet.id)
t.not(snippet, undefined)
t.is(snippet.name, newSnippet.name)
t.deepEqual(snippet.prefix, newSnippet.prefix)
t.is(snippet.content, newSnippet.content)
})
})
test.after.always(() => {
sander.rimrafSync(snippetFilePath)
})

View File

@@ -48,10 +48,13 @@ const checkboxes = `
const smartQuotes = 'This is a "QUOTE".' const smartQuotes = 'This is a "QUOTE".'
const breaks = 'This is the first line.\nThis is the second line.'
export default { export default {
basic, basic,
codeblock, codeblock,
katex, katex,
checkboxes, checkboxes,
smartQuotes smartQuotes,
breaks
} }

View File

@@ -34,3 +34,12 @@ test('Markdown.render() should text with quotes correctly', t => {
const renderedNonSmartQuotes = newmd.render(markdownFixtures.smartQuotes) const renderedNonSmartQuotes = newmd.render(markdownFixtures.smartQuotes)
t.snapshot(renderedNonSmartQuotes) t.snapshot(renderedNonSmartQuotes)
}) })
test('Markdown.render() should render line breaks correctly', t => {
const renderedBreaks = md.render(markdownFixtures.breaks)
t.snapshot(renderedBreaks)
const newmd = new Markdown({ breaks: false })
const renderedNonBreaks = newmd.render(markdownFixtures.breaks)
t.snapshot(renderedNonBreaks)
})

View File

@@ -0,0 +1,16 @@
/**
* @fileoverview Unit test for browser/lib/normalizeEditorFontFamily
*/
import test from 'ava'
import normalizeEditorFontFamily from '../../browser/lib/normalizeEditorFontFamily'
import consts from '../../browser/lib/consts'
const defaultEditorFontFamily = consts.DEFAULT_EDITOR_FONT_FAMILY
test('normalizeEditorFontFamily() should return default font family (string[])', t => {
t.is(normalizeEditorFontFamily(), defaultEditorFontFamily.join(', '))
})
test('normalizeEditorFontFamily(["hoge", "huga"]) should return default font family connected with arg.', t => {
const arg = 'font1, font2'
t.is(normalizeEditorFontFamily(arg), `${arg}, ${defaultEditorFontFamily.join(', ')}`)
})

View File

@@ -4,6 +4,20 @@ 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 line breaks correctly
> Snapshot 1
`<p data-line="0">This is the first line.<br />␊
This is the second line.</p>␊
`
> Snapshot 2
`<p data-line="0">This is the first line.␊
This is the second line.</p>␊
`
## Markdown.render() should renders KaTeX correctly ## Markdown.render() should renders KaTeX correctly
> Snapshot 1 > Snapshot 1

View File

@@ -28,18 +28,15 @@ var config = {
externals: [ externals: [
'node-ipc', 'node-ipc',
'electron', 'electron',
'md5',
'superagent',
'superagent-promise',
'lodash', 'lodash',
'markdown-it', 'markdown-it',
'moment', 'moment',
'markdown-it-emoji', 'markdown-it-emoji',
'fs-jetpack', 'fs-jetpack',
'@rokt33r/markdown-it-math', '@rokt33r/markdown-it-math',
'markdown-it-checkbox',
'markdown-it-kbd', 'markdown-it-kbd',
'markdown-it-plantuml', 'markdown-it-plantuml',
'markdown-it-admonition',
'devtron', 'devtron',
'@rokt33r/season', '@rokt33r/season',
{ {

2674
yarn.lock

File diff suppressed because it is too large Load Diff