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

Merge branch 'master' into create-image-tag-when-pasting-image-url

# Conflicts:
#	browser/components/CodeEditor.js
This commit is contained in:
kawmra
2018-06-07 03:01:08 +09:00
54 changed files with 2326 additions and 1702 deletions

View File

@@ -7,7 +7,9 @@ import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement'
import convertModeName from 'browser/lib/convertModeName'
import eventEmitter from 'browser/main/lib/eventEmitter'
import iconv from 'iconv-lite'
import crypto from 'crypto'
import consts from 'browser/lib/consts'
import fs from 'fs'
const { ipcRenderer } = require('electron')
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
@@ -81,8 +83,21 @@ export default class CodeEditor extends React.Component {
componentDidMount () {
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, {
rulers: buildCMRulers(rulers, enableRulers),
value: this.props.value,
@@ -103,6 +118,8 @@ export default class CodeEditor extends React.Component {
Tab: function (cm) {
const cursor = cm.getCursor()
const line = cm.getLine(cursor.line)
const cursorPosition = cursor.ch
const charBeforeCursor = line.substr(cursorPosition - 1, 1)
if (cm.somethingSelected()) cm.indentSelection('add')
else {
const tabs = cm.getOption('indentWithTabs')
@@ -114,6 +131,16 @@ export default class CodeEditor extends React.Component {
cm.execCommand('insertSoftTab')
}
cm.execCommand('goLineEnd')
} else if (!charBeforeCursor.match(/\t|\s|\r|\n/) && cursor.ch > 1) {
// text expansion on tab key if the char before is alphabet
const snippets = JSON.parse(fs.readFileSync(consts.SNIPPET_FILE, 'utf8'))
if (expandSnippet(line, cursor, cm, snippets) === false) {
if (tabs) {
cm.execCommand('insertTab')
} else {
cm.execCommand('insertSoftTab')
}
}
} else {
if (tabs) {
cm.execCommand('insertTab')
@@ -157,6 +184,73 @@ export default class CodeEditor extends React.Component {
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 () {
document.querySelector('textarea').blur()
}
@@ -320,8 +414,9 @@ export default class CodeEditor extends React.Component {
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(cursor)
editor.setCursor(newCursor)
}
fetch(pastedTxt, {

View File

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

View File

@@ -32,7 +32,7 @@ const CSS_FILES = [
`${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 `
@font-face {
font-family: 'Lato';
@@ -52,7 +52,19 @@ function buildStyle (fontFamily, fontSize, codeBlockFontFamily, lineNumber, scro
font-weight: 700;
text-rendering: optimizeLegibility;
}
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: local('Material Icons'),
local('MaterialIcons-Regular'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff2') format('woff2'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
}
${allowCustomCSS ? customCSS : ''}
${markdownStyle}
body {
font-family: '${fontFamily.join("','")}';
font-size: ${fontSize}px;
@@ -132,7 +144,6 @@ export default class MarkdownPreview extends React.Component {
this.mouseUpHandler = (e) => this.handleMouseUp(e)
this.DoubleClickHandler = (e) => this.handleDoubleClick(e)
this.scrollHandler = _.debounce(this.handleScroll.bind(this), 100, {leading: false, trailing: true})
this.anchorClickHandler = (e) => this.handlePreviewAnchorClick(e)
this.checkboxClickHandler = (e) => this.handleCheckboxClick(e)
this.saveAsTextHandler = () => this.handleSaveAsText()
this.saveAsMdHandler = () => this.handleSaveAsMd()
@@ -153,22 +164,6 @@ export default class MarkdownPreview extends React.Component {
})
}
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) {
this.props.onCheckboxClick(e)
}
@@ -216,9 +211,9 @@ export default class MarkdownPreview extends React.Component {
handleSaveAsHtml () {
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))
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
@@ -343,6 +338,7 @@ export default class MarkdownPreview extends React.Component {
if (prevProps.value !== this.props.value) this.rewriteIframe()
if (prevProps.smartQuotes !== this.props.smartQuotes ||
prevProps.sanitize !== this.props.sanitize ||
prevProps.smartArrows !== this.props.smartArrows ||
prevProps.breaks !== this.props.breaks) {
this.initMarkdown()
this.rewriteIframe()
@@ -354,14 +350,16 @@ export default class MarkdownPreview extends React.Component {
prevProps.lineNumber !== this.props.lineNumber ||
prevProps.showCopyNotification !== this.props.showCopyNotification ||
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.rewriteIframe()
}
}
getStyleParams () {
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd, theme } = this.props
const { fontSize, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS } = this.props
let { fontFamily, codeBlockFontFamily } = this.props
fontFamily = _.isString(fontFamily) && fontFamily.trim().length > 0
? fontFamily.split(',').map(fontName => fontName.trim()).concat(defaultFontFamily)
@@ -370,14 +368,14 @@ export default class MarkdownPreview extends React.Component {
? codeBlockFontFamily.split(',').map(fontName => fontName.trim()).concat(defaultCodeBlockFontFamily)
: defaultCodeBlockFontFamily
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme}
return {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS}
}
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('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) {
@@ -390,9 +388,6 @@ export default class MarkdownPreview extends React.Component {
}
rewriteIframe () {
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
el.removeEventListener('click', this.anchorClickHandler)
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('input[type="checkbox"]'), (el) => {
el.removeEventListener('click', this.checkboxClickHandler)
})
@@ -401,7 +396,7 @@ export default class MarkdownPreview extends React.Component {
el.removeEventListener('click', this.linkClickHandler)
})
const { theme, indentSize, showCopyNotification, storagePath } = this.props
const { theme, indentSize, showCopyNotification, storagePath, noteKey } = this.props
let { value, codeBlockTheme } = this.props
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
@@ -413,18 +408,15 @@ export default class MarkdownPreview extends React.Component {
})
}
let renderedHTML = this.markdown.render(value)
attachmentManagement.migrateAttachments(renderedHTML, storagePath, noteKey)
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) => {
el.addEventListener('click', this.checkboxClickHandler)
})
_.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => {
this.fixDecodedURI(el)
el.addEventListener('click', this.linkClickHandler)
})
@@ -475,7 +467,7 @@ export default class MarkdownPreview extends React.Component {
el.innerHTML = ''
diagram.drawSVG(el, opts)
_.forEach(el.querySelectorAll('a'), (el) => {
el.addEventListener('click', this.anchorClickHandler)
el.addEventListener('click', this.linkClickHandler)
})
} catch (e) {
console.error(e)
@@ -491,7 +483,7 @@ export default class MarkdownPreview extends React.Component {
el.innerHTML = ''
diagram.drawSVG(el, {theme: 'simple'})
_.forEach(el.querySelectorAll('a'), (el) => {
el.addEventListener('click', this.anchorClickHandler)
el.addEventListener('click', this.linkClickHandler)
})
} catch (e) {
console.error(e)
@@ -598,10 +590,12 @@ MarkdownPreview.propTypes = {
onDoubleClick: PropTypes.func,
onMouseUp: PropTypes.func,
onMouseDown: PropTypes.func,
onContextMenu: PropTypes.func,
className: PropTypes.string,
value: PropTypes.string,
showCopyNotification: PropTypes.bool,
storagePath: PropTypes.string,
smartQuotes: PropTypes.bool,
smartArrows: PropTypes.bool,
breaks: PropTypes.bool
}

View File

@@ -131,6 +131,7 @@ class MarkdownSplitEditor extends React.Component {
lineNumber={config.preview.lineNumber}
scrollPastEnd={config.preview.scrollPastEnd}
smartQuotes={config.preview.smartQuotes}
smartArrows={config.preview.smartArrows}
breaks={config.preview.breaks}
sanitize={config.preview.sanitize}
ref='preview'
@@ -141,6 +142,8 @@ class MarkdownSplitEditor extends React.Component {
showCopyNotification={config.ui.showCopyNotification}
storagePath={storage.path}
noteKey={noteKey}
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
/>
</div>
)

View File

@@ -293,6 +293,82 @@ kbd
line-height 1
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-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: {border-color: #448aff, title-color: rgba(68,138,255,.1), icon: "note"},
hint: {border-color: #00bfa5, title-color: rgba(0,191,165,.1), icon: "info"},
danger: {border-color: #ff1744, title-color: rgba(255,23,68,.1), icon: "block"},
caution: {border-color: #ff9100, title-color: rgba(255,145,0,.1), icon: "warning"},
error: {border-color: #ff1744, title-color: rgba(255,23,68,.1), icon: "error"},
attention: {border-color: #64dd17, title-color: rgba(100,221,23,.1), icon: "priority_high"}
}
for name, val in admonition_types
.admonition.{name}
@extend $admonition
border-left-color: val[border-color]
.admonition.{name}>.admonition-title
@extend $admonition-title
border-bottom-color: .1rem solid val[title-color]
background-color: val[title-color]
.admonition.{name}>.admonition-title:before
@extend $admonition-icon
color: val[border-color]
content: val[icon]
themeDarkBackground = darken(#21252B, 10%)
themeDarkText = #f9f9f9
themeDarkBorder = lighten(themeDarkBackground, 20%)

View File

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

View File

@@ -2,6 +2,7 @@ import markdownit from 'markdown-it'
import sanitize from './markdown-it-sanitize-html'
import emoji from 'markdown-it-emoji'
import math from '@rokt33r/markdown-it-math'
import smartArrows from 'markdown-it-smartarrows'
import _ from 'lodash'
import ConfigManager from 'browser/main/lib/ConfigManager'
import katex from 'katex'
@@ -141,6 +142,7 @@ class Markdown {
}
})
this.md.use(require('markdown-it-kbd'))
this.md.use(require('markdown-it-admonition'))
const deflate = require('markdown-it-plantuml/lib/deflate')
this.md.use(require('markdown-it-plantuml'), '', {
@@ -213,6 +215,10 @@ class Markdown {
return true
})
if (config.preview.smartArrows) {
this.md.use(smartArrows)
}
// Add line number attribute for scrolling
const originalRender = this.md.renderer.render
this.md.renderer.render = (tokens, options, env) => {

View File

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

View File

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

View File

@@ -185,7 +185,7 @@ class SideNav extends React.Component {
).filter(
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)))
return relatedTags
}
@@ -224,7 +224,7 @@ class SideNav extends React.Component {
handleClickNarrowToTag (tag) {
const { router } = this.context
const { location } = this.props
let listOfTags = this.getActiveTags(location.pathname)
const listOfTags = this.getActiveTags(location.pathname)
const indexOfTag = listOfTags.indexOf(tag)
if (indexOfTag > -1) {
listOfTags.splice(indexOfTag, 1)

View File

@@ -59,6 +59,9 @@ export const DEFAULT_CONFIG = {
scrollPastEnd: false,
smartQuotes: true,
breaks: true,
smartArrows: false,
allowCustomCSS: false,
customCSS: '',
sanitize: 'STRICT' // 'STRICT', 'ALLOW_STYLES', 'NONE'
},
blog: {

View File

@@ -73,6 +73,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.
* @param {String} renderedHTML HTML in that the links should be fixed
@@ -152,7 +177,8 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
base64data += base64data.replace('+', ' ')
const binaryData = new Buffer(base64data, 'base64').toString('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)
}
reader.readAsDataURL(blob)
@@ -165,7 +191,7 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
*/
function getAttachmentsInContent (markdownContent) {
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)
}
@@ -224,7 +250,7 @@ function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) {
* @returns {String} Input without the references
*/
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)
}
/**
@@ -245,6 +271,9 @@ function deleteAttachmentFolder (storageKey, noteKey) {
* @param noteKey NoteKey of the current note. Is used to determine the belonging attachment folder.
*/
function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey) {
if (storageKey == null || noteKey == null || markdownContent == null) {
return
}
const targetStorage = findStorage.findStorage(storageKey)
const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
const attachmentsInNote = getAttachmentsInContent(markdownContent)
@@ -254,11 +283,10 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey
attachmentsInNoteOnlyFileNames.push(attachmentsInNote[i].replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + noteKey + escapeStringRegexp(path.sep), 'g'), ''))
}
}
if (fs.existsSync(attachmentFolder)) {
fs.readdir(attachmentFolder, (err, files) => {
if (err) {
console.error("Error reading directory '" + attachmentFolder + "'. Error:")
console.error('Error reading directory \'' + attachmentFolder + '\'. Error:')
console.error(err)
return
}
@@ -267,17 +295,17 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey
const absolutePathOfFile = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey, file)
fs.unlink(absolutePathOfFile, (err) => {
if (err) {
console.error("Could not delete '%s'", absolutePathOfFile)
console.error('Could not delete \'%s\'', absolutePathOfFile)
console.error(err)
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 {
console.info("Attachment folder ('" + attachmentFolder + "') did not exist..")
console.debug('Attachment folder (\'' + attachmentFolder + '\') did not exist..')
}
}
@@ -321,6 +349,7 @@ module.exports = {
deleteAttachmentsNotPresentInNote,
moveAttachments,
cloneAttachments,
migrateAttachments,
STORAGE_FOLDER_PLACEHOLDER,
DESTINATION_FOLDER
}

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

@@ -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

@@ -13,6 +13,10 @@ const dataApi = {
deleteNote: require('./deleteNote'),
moveNote: require('./moveNote'),
migrateFromV5Storage: require('./migrateFromV5Storage'),
createSnippet: require('./createSnippet'),
deleteSnippet: require('./deleteSnippet'),
updateSnippet: require('./updateSnippet'),
fetchSnippet: require('./fetchSnippet'),
_migrateFromV6Storage: require('./migrateFromV6Storage'),
_resolveStorageData: require('./resolveStorageData'),

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,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,87 @@
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'
const { remote } = require('electron')
const { Menu, MenuItem } = remote
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}))
}
handleSnippetContextMenu (snippet) {
const menu = new Menu()
menu.append(new MenuItem({
label: i18n.__('Delete snippet'),
click: () => {
this.deleteSnippet(snippet)
}
}))
menu.popup()
}
deleteSnippet (snippet) {
dataApi.deleteSnippet(snippet).then(() => {
this.reloadSnippetList()
this.props.onSnippetDeleted(snippet)
}).catch(err => { throw err })
}
handleSnippetClick (snippet) {
this.props.onSnippetClick(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 })
}
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='snippet-item'
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,116 @@
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)
}
handleSnippetClick (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
onSnippetClick={this.handleSnippetClick.bind(this)}
onSnippetDeleted={this.handleDeleteSnippet.bind(this)} />
<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,180 @@
@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-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)
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-detail
color white
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-detail
color white
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-detail
color white

View File

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

View File

@@ -28,6 +28,8 @@ class UiTab extends React.Component {
componentDidMount () {
CodeMirror.autoLoadMode(this.codeMirrorInstance.getCodeMirror(), 'javascript')
CodeMirror.autoLoadMode(this.customCSSCM.getCodeMirror(), 'css')
this.customCSSCM.getCodeMirror().setSize('400px', '400px')
this.handleSettingDone = () => {
this.setState({UiAlert: {
type: 'success',
@@ -98,7 +100,10 @@ class UiTab extends React.Component {
scrollPastEnd: this.refs.previewScrollPastEnd.checked,
smartQuotes: this.refs.previewSmartQuotes.checked,
breaks: this.refs.previewBreaks.checked,
sanitize: this.refs.previewSanitize.value
smartArrows: this.refs.previewSmartArrows.checked,
sanitize: this.refs.previewSanitize.value,
allowCustomCSS: this.refs.previewAllowCustomCSS.checked,
customCSS: this.customCSSCM.getCodeMirror().getValue()
}
}
@@ -159,6 +164,7 @@ class UiTab extends React.Component {
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 enableEditRulersStyle = config.editor.enableRulers ? 'block' : 'none'
const customCSS = config.preview.customCSS
return (
<div styleName='root'>
<div styleName='group'>
@@ -234,7 +240,7 @@ class UiTab extends React.Component {
disabled={OSX}
type='checkbox'
/>&nbsp;
Disable Direct Write(It will be applied after restarting)
{i18n.__('Disable Direct Write (It will be applied after restarting)')}
</label>
</div>
: null
@@ -474,7 +480,7 @@ class UiTab extends React.Component {
ref='previewSmartQuotes'
type='checkbox'
/>&nbsp;
Enable smart quotes
{i18n.__('Enable smart quotes')}
</label>
</div>
<div styleName='group-checkBoxSection'>
@@ -484,7 +490,17 @@ class UiTab extends React.Component {
ref='previewBreaks'
type='checkbox'
/>&nbsp;
Render newlines in Markdown paragraphs as &lt;br&gt;
{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>
</div>
@@ -569,6 +585,20 @@ class UiTab extends React.Component {
/>
</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')}
<ReactCodeMirror onChange={e => this.handleUIChange(e)} ref={e => (this.customCSSCM = e)} value={config.preview.customCSS} options={{ lineNumbers: true, mode: 'css', theme: codemirrorTheme }} />
</div>
</div>
<div styleName='group-control'>
<button styleName='group-control-rightButton'

View File

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

View File

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

View File

@@ -150,5 +150,6 @@
"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"
"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."
}

View File

@@ -205,5 +205,6 @@
"Unnamed": "Unbenannt",
"Rename": "Umbenennen",
"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."
}

View File

@@ -4,9 +4,10 @@
"Preferences": "Preferences",
"Make a note": "Make a note",
"Ctrl": "Ctrl",
"Ctrl(^)": "Ctrl",
"Ctrl(^)": "Ctrl(^)",
"to create a new note": "to create a new note",
"Toggle Mode": "Toggle Mode",
"Add tag...": "Add tag...",
"Trash": "Trash",
"MODIFICATION DATE": "MODIFICATION DATE",
"Words": "Words",
@@ -20,9 +21,12 @@
".html": ".html",
"Print": "Print",
"Your preferences for Boostnote": "Your preferences for Boostnote",
"Help": "Help",
"Hide Help": "Hide Help",
"Storages": "Storages",
"Add Storage Location": "Add Storage Location",
"Add Folder": "Add Folder",
"Select 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": "Show only related tags",
"Editor Theme": "Editor Theme",
"Editor Font Size": "Editor Font Size",
"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",
"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",
"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",
"Preview": "Preview",
"Preview Font Size": "Preview Font Size",
@@ -127,6 +134,7 @@
"Storage": "Storage",
"Hotkeys": "Hotkeys",
"Show/Hide Boostnote": "Show/Hide Boostnote",
"Toggle editor mode": "Toggle editor mode",
"Restore": "Restore",
"Permanent Delete": "Permanent Delete",
"Confirm note deletion": "Confirm note deletion",
@@ -146,12 +154,26 @@
"UserName": "UserName",
"Password": "Password",
"Russian": "Russian",
"Hungarian": "Hungarian",
"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",
"Select Folder": "Select Folder",
"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",
"Enable": "Enable",
"Disable": "Disable",
"Sanitization": "Sanitization",
"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 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."
}

View File

@@ -150,5 +150,6 @@
"Sanitization": "Saneamiento",
"Only allow secure html tags (recommended)": "Solo permitir etiquetas html seguras (recomendado)",
"Allow styles": "Permitir estilos",
"Allow dangerous html tags": "Permitir etiquetas html peligrosas"
"Allow dangerous html tags": "Permitir etiquetas html peligrosas",
"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,6 @@
"Sanitization": "پاکسازی کردن",
"Only allow secure html tags (recommended)": "(فقط تگ های امن اچ تی ام ال مجاز اند.(پیشنهاد میشود",
"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."
}

View File

@@ -150,5 +150,6 @@
"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"
"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."
}

View File

@@ -7,6 +7,7 @@
"Ctrl(^)": "Ctrl",
"to create a new note": "hogy létrehozz egy jegyzetet",
"Toggle Mode": "Mód Váltás",
"Add tag...": "Tag hozzáadása...",
"Trash": "Lomtár",
"MODIFICATION DATE": "MÓDOSÍTÁS DÁTUMA",
"Words": "Szó",
@@ -20,9 +21,12 @@
".html": ".html",
"Print": "Nyomtatás",
"Your preferences for Boostnote": "Boostnote beállításaid",
"Help": "Súgó",
"Hide Help": "Súgó Elrejtése",
"Storages": "Tárolók",
"Add Storage Location": "Tároló 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",
"Unlink": "Tároló Leválasztása",
"Edit": "Szerkesztés",
@@ -34,6 +38,8 @@
"Solarized Dark": "Solarized Dark",
"Dark": "Sötét",
"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 Font Size": "Szerkesztő Betűmérete",
"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",
"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",
"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",
"Preview": "Megtekintés",
"Preview Font Size": "Megtekintés Betűmérete",
@@ -127,6 +134,7 @@
"Storage": "Tároló",
"Hotkeys": "Gyorsbillentyűk",
"Show/Hide Boostnote": "Boostnote Megjelenítése/Elrejtése",
"Toggle editor mode": "Szerkesztő mód váltása",
"Restore": "Visszaállítás",
"Permanent Delete": "Végleges Törlés",
"Confirm note deletion": "Törlés megerősítése",
@@ -146,8 +154,8 @@
"UserName": "FelhasznaloNev",
"Password": "Jelszo",
"Russian": "Russian",
"Command(⌘)": "Command(⌘)",
"Hungarian": "Hungarian",
"Command(⌘)": "Command(⌘)",
"Add Storage": "Tároló hozzáadása",
"Name": "Név",
"Type": "Típus",
@@ -156,6 +164,16 @@
"Cloud-Syncing-and-Backup": "Cloud-Syncing-and-Backup",
"Location": "Hely",
"Add": "Hozzáadás",
"Select Folder": "Könyvtár Kivá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."
}

View File

@@ -153,5 +153,6 @@
"Sanitization": "Bonifica",
"Only allow secure html tags (recommended)": "Consenti solo tag HTML sicuri (raccomandato)",
"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."
}"

View File

@@ -150,5 +150,6 @@
"Sanitization": "サニタイズ",
"Only allow secure html tags (recommended)": "安全なHTMLタグのみ利用を許可する推奨",
"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."
}

View File

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

View File

@@ -149,5 +149,6 @@
"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"
"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."
}

View File

@@ -149,5 +149,6 @@
"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"
"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."
}

View File

@@ -149,5 +149,6 @@
"Sanitization": "Sanitização",
"Only allow secure html tags (recommended)": "Permitir apenas tags html seguras (recomendado)",
"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."
}

View File

@@ -149,5 +149,6 @@
"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"
"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."
}

View File

@@ -146,5 +146,6 @@
"Russian": "Русский",
"Editor Rulers": "Editor Rulers",
"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."
}

View File

@@ -148,5 +148,6 @@
"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"
"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."
}

View File

@@ -148,5 +148,6 @@
"Sanitization": "代码处理",
"Only allow secure html tags (recommended)": "只允许安全的html标签(推荐)",
"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."
}

View File

@@ -148,5 +148,6 @@
"Sanitization": "過濾 HTML 程式碼",
"Only allow secure html tags (recommended)": "只允許安全的 HTML 標籤 (建議)",
"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."
}

View File

@@ -12,12 +12,12 @@
"compile": "grunt compile",
"test": "PWD=$(pwd) NODE_ENV=test ava --serial",
"jest": "jest",
"fix": "npm run lint --fix",
"fix": "eslint . --fix",
"lint": "eslint .",
"dev-start": "concurrently --kill-others \"npm run webpack\" \"npm run hot\""
},
"config": {
"electron-version": "1.7.11"
"electron-version": "1.8.7"
},
"repository": {
"type": "git",
@@ -70,6 +70,7 @@
"lodash": "^4.11.1",
"lodash-move": "^1.1.1",
"markdown-it": "^6.0.1",
"markdown-it-admonition": "https://github.com/johannbre/markdown-it-admonition.git",
"markdown-it-checkbox": "^1.1.0",
"markdown-it-emoji": "^1.1.1",
"markdown-it-footnote": "^3.0.0",
@@ -78,6 +79,7 @@
"markdown-it-multimd-table": "^2.0.1",
"markdown-it-named-headers": "^0.0.4",
"markdown-it-plantuml": "^0.3.0",
"markdown-it-smartarrows": "^1.0.1",
"md5": "^2.0.0",
"mdurl": "^1.0.1",
"moment": "^2.10.3",
@@ -117,12 +119,12 @@
"css-loader": "^0.19.0",
"devtron": "^1.1.0",
"dom-storage": "^2.0.2",
"electron": "1.7.11",
"electron": "1.8.7",
"electron-packager": "^6.0.0",
"eslint": "^3.13.1",
"eslint-config-standard": "^6.2.1",
"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",
"faker": "^3.1.0",
"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
- [Facebook Group](https://www.facebook.com/groups/boostnote/)
- [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/enQtMzcwNDU3NDU3ODI0LTU1ZDgwZDNiZTNmN2RhOTY4OTM5ODY0ODUzMTRiNmQ0ZDMzZDRiYzg2YmQ5ZDYzZTQxYjMxYzBlNTM4NjcyYjM)
- [Blog](https://boostlog.io/tags/boostnote)
- [Reddit](https://www.reddit.com/r/Boostnote/)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -339,6 +339,38 @@ it('should test that deleteAttachmentsNotPresentInNote does not delete reference
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()
@@ -403,6 +435,8 @@ it('should test that cloneAttachments modifies the content of the new note corre
'![' + 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' +

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

@@ -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

@@ -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

@@ -40,6 +40,7 @@ var config = {
'markdown-it-checkbox',
'markdown-it-kbd',
'markdown-it-plantuml',
'markdown-it-admonition',
'devtron',
'@rokt33r/season',
{

2562
yarn.lock

File diff suppressed because it is too large Load Diff