diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index f02a146a..d0e2f505 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -279,6 +279,7 @@ class MarkdownEditor extends React.Component { lineNumber={config.preview.lineNumber} indentSize={editorIndentSize} scrollPastEnd={config.preview.scrollPastEnd} + smartQuotes={config.preview.smartQuotes} ref='preview' onContextMenu={(e) => this.handleContextMenu(e)} onDoubleClick={(e) => this.handleDoubleClick(e)} diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 2bb42291..e4298a71 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types' import React from 'react' -import markdown from 'browser/lib/markdown' +import Markdown from 'browser/lib/markdown' import _ from 'lodash' import CodeMirror from 'codemirror' import 'codemirror-mode-elixir' @@ -130,6 +130,13 @@ export default class MarkdownPreview extends React.Component { this.printHandler = () => this.handlePrint() this.linkClickHandler = this.handlelinkClick.bind(this) + this.initMarkdown = this.initMarkdown.bind(this) + this.initMarkdown() + } + + initMarkdown () { + const { smartQuotes } = this.props + this.markdown = new Markdown({ typographer: smartQuotes }) } handlePreviewAnchorClick (e) { @@ -198,7 +205,7 @@ export default class MarkdownPreview extends React.Component { const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme} = this.getStyleParams() const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, lineNumber) - const body = markdown.render(noteContent) + const body = this.markdown.render(noteContent) const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] files.forEach((file) => { @@ -217,6 +224,7 @@ export default class MarkdownPreview extends React.Component { return ` + ${styles} @@ -310,6 +318,10 @@ export default class MarkdownPreview extends React.Component { componentDidUpdate (prevProps) { if (prevProps.value !== this.props.value) this.rewriteIframe() + if (prevProps.smartQuotes !== this.props.smartQuotes) { + this.initMarkdown() + this.rewriteIframe() + } if (prevProps.fontFamily !== this.props.fontFamily || prevProps.fontSize !== this.props.fontSize || prevProps.codeBlockFontFamily !== this.props.codeBlockFontFamily || @@ -375,7 +387,7 @@ export default class MarkdownPreview extends React.Component { value = value.replace(codeBlock, htmlTextHelper.encodeEntities(codeBlock)) }) } - this.refs.root.contentWindow.document.body.innerHTML = markdown.render(value) + this.refs.root.contentWindow.document.body.innerHTML = this.markdown.render(value) _.forEach(this.refs.root.contentWindow.document.querySelectorAll('a'), (el) => { this.fixDecodedURI(el) @@ -391,9 +403,9 @@ export default class MarkdownPreview extends React.Component { }) _.forEach(this.refs.root.contentWindow.document.querySelectorAll('img'), (el) => { - el.src = markdown.normalizeLinkText(el.src) + el.src = this.markdown.normalizeLinkText(el.src) if (!/\/:storage/.test(el.src)) return - el.src = `file:///${markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}` + el.src = `file:///${this.markdown.normalizeLinkText(path.join(storagePath, 'images', path.basename(el.src)))}` }) codeBlockTheme = consts.THEMES.some((_theme) => _theme === codeBlockTheme) @@ -420,9 +432,9 @@ export default class MarkdownPreview extends React.Component { el.innerHTML = '' if (codeBlockTheme.indexOf('solarized') === 0) { const [refThema, color] = codeBlockTheme.split(' ') - el.parentNode.className += ` cm-s-${refThema} cm-s-${color} CodeMirror` + el.parentNode.className += ` cm-s-${refThema} cm-s-${color}` } else { - el.parentNode.className += ` cm-s-${codeBlockTheme} CodeMirror` + el.parentNode.className += ` cm-s-${codeBlockTheme}` } CodeMirror.runMode(content, syntax.mime, el, { tabSize: indentSize @@ -505,9 +517,20 @@ export default class MarkdownPreview extends React.Component { handlelinkClick (e) { const noteHash = e.target.href.split('/').pop() - const regexIsNoteLink = /^(.{20})-(.{20})$/ + // this will match the new uuid v4 hash and the old hash + // e.g. + // :note:1c211eb7dcb463de6490 and + // :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c + const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/ if (regexIsNoteLink.test(noteHash)) { - eventEmitter.emit('list:jump', noteHash) + eventEmitter.emit('list:jump', noteHash.replace(':note:', '')) + } + // this will match the old link format storage.key-note.key + // e.g. + // 877f99c3268608328037-1c211eb7dcb463de6490 + const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/ + if (regexIsLegacyNoteLink.test(noteHash)) { + eventEmitter.emit('list:jump', noteHash.split('-')[1]) } } @@ -534,5 +557,6 @@ MarkdownPreview.propTypes = { className: PropTypes.string, value: PropTypes.string, showCopyNotification: PropTypes.bool, - storagePath: PropTypes.string + storagePath: PropTypes.string, + smartQuotes: PropTypes.bool } diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index 505fbaf4..0aa2d16c 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -127,6 +127,7 @@ class MarkdownSplitEditor extends React.Component { codeBlockFontFamily={config.editor.fontFamily} lineNumber={config.preview.lineNumber} scrollPastEnd={config.preview.scrollPastEnd} + smartQuotes={config.preview.smartQuotes} ref='preview' tabInde='0' value={value} diff --git a/browser/components/NoteItem.js b/browser/components/NoteItem.js index 253faa81..2013cfa0 100644 --- a/browser/components/NoteItem.js +++ b/browser/components/NoteItem.js @@ -62,9 +62,9 @@ const NoteItem = ({ ? 'item--active' : 'item' } - key={`${note.storage}-${note.key}`} - onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)} - onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)} + key={note.key} + onClick={e => handleNoteClick(e, note.key)} + onContextMenu={e => handleNoteContextMenu(e, note.key)} onDragStart={e => handleDragStart(e, note)} draggable='true' > diff --git a/browser/components/NoteItem.styl b/browser/components/NoteItem.styl index f213348a..5203ccea 100644 --- a/browser/components/NoteItem.styl +++ b/browser/components/NoteItem.styl @@ -117,7 +117,7 @@ $control-height = 30px font-size 12px line-height 20px overflow ellipsis - display flex + display block .item-bottom-tagList flex 1 diff --git a/browser/components/NoteItemSimple.js b/browser/components/NoteItemSimple.js index 0d2465e9..8262ea1d 100644 --- a/browser/components/NoteItemSimple.js +++ b/browser/components/NoteItemSimple.js @@ -28,9 +28,9 @@ const NoteItemSimple = ({ ? 'item-simple--active' : 'item-simple' } - key={`${note.storage}-${note.key}`} - onClick={e => handleNoteClick(e, `${note.storage}-${note.key}`)} - onContextMenu={e => handleNoteContextMenu(e, `${note.storage}-${note.key}`)} + key={note.key} + onClick={e => handleNoteClick(e, note.key)} + onContextMenu={e => handleNoteContextMenu(e, note.key)} onDragStart={e => handleDragStart(e, note)} draggable='true' > diff --git a/browser/components/StorageItem.js b/browser/components/StorageItem.js index 25be3c57..c92579da 100644 --- a/browser/components/StorageItem.js +++ b/browser/components/StorageItem.js @@ -6,6 +6,7 @@ import React from 'react' import styles from './StorageItem.styl' import CSSModules from 'browser/lib/CSSModules' import _ from 'lodash' +import { SortableHandle } from 'react-sortable-hoc' /** * @param {boolean} isActive @@ -23,32 +24,35 @@ import _ from 'lodash' const StorageItem = ({ isActive, handleButtonClick, handleContextMenu, folderName, folderColor, isFolded, noteCount, handleDrop, handleDragEnter, handleDragLeave -}) => ( - -) + {(!isFolded && _.isNumber(noteCount)) && + {noteCount} + } + {isFolded && + + {folderName} + + } + + ) +} StorageItem.propTypes = { isActive: PropTypes.bool.isRequired, diff --git a/browser/lib/keygen.js b/browser/lib/keygen.js index f4937a83..814efedd 100644 --- a/browser/lib/keygen.js +++ b/browser/lib/keygen.js @@ -1,7 +1,11 @@ const crypto = require('crypto') const _ = require('lodash') +const uuidv4 = require('uuid/v4') -module.exports = function (length) { - if (!_.isFinite(length)) length = 10 +module.exports = function (uuid) { + if (typeof uuid === typeof true && uuid) { + return uuidv4() + } + const length = 10 return crypto.randomBytes(length).toString('hex') } diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index b97f9d56..a2b9da51 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -20,181 +20,188 @@ function createGutter (str, firstLineNumber) { return '' + lines.join('') + '' } -var md = markdownit({ - typographer: true, - linkify: true, - html: true, - xhtmlOut: true, - breaks: true, - highlight: function (str, lang) { - const delimiter = ':' - const langInfo = lang.split(delimiter) - const langType = langInfo[0] - const fileName = langInfo[1] || '' - const firstLineNumber = parseInt(langInfo[2], 10) +class Markdown { + constructor (options = {}) { + const defaultOptions = { + typographer: true, + linkify: true, + html: true, + xhtmlOut: true, + breaks: true, + highlight: function (str, lang) { + const delimiter = ':' + const langInfo = lang.split(delimiter) + const langType = langInfo[0] + const fileName = langInfo[1] || '' + const firstLineNumber = parseInt(langInfo[2], 10) - if (langType === 'flowchart') { - return `
${str}
` - } - if (langType === 'sequence') { - return `
${str}
` - } - return '
' +
-      '' + fileName + '' +
-      createGutter(str, firstLineNumber) +
-      '' +
-      str +
-      '
' - } -}) -// Sanitize use rinput before other plugins -md.use(sanitize, { - allowedTags: ['img', 'iframe'], - allowedAttributes: { - '*': ['alt', 'style'], - 'img': ['src', 'width', 'height'], - 'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'] - }, - allowedIframeHostnames: ['www.youtube.com'] -}) -md.use(emoji, { - shortcuts: {} -}) -md.use(math, { - inlineOpen: config.preview.latexInlineOpen, - inlineClose: config.preview.latexInlineClose, - blockOpen: config.preview.latexBlockOpen, - blockClose: config.preview.latexBlockClose, - inlineRenderer: function (str) { - let output = '' - try { - output = katex.renderToString(str.trim()) - } catch (err) { - output = `${err.message}` - } - return output - }, - blockRenderer: function (str) { - let output = '' - try { - output = katex.renderToString(str.trim(), { displayMode: true }) - } catch (err) { - output = `
${err.message}
` - } - return output - } -}) -md.use(require('markdown-it-imsize')) -md.use(require('markdown-it-footnote')) -md.use(require('markdown-it-multimd-table')) -md.use(require('markdown-it-named-headers'), { - slugify: (header) => { - return encodeURI(header.trim() - .replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '') - .replace(/\s+/g, '-')) - .replace(/\-+$/, '') - } -}) -md.use(require('markdown-it-kbd')) - -const deflate = require('markdown-it-plantuml/lib/deflate') -md.use(require('markdown-it-plantuml'), '', { - generateSource: function (umlCode) { - const s = unescape(encodeURIComponent(umlCode)) - const zippedCode = deflate.encode64( - deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9) - ) - return `http://www.plantuml.com/plantuml/svg/${zippedCode}` - } -}) - -// Override task item -md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) { - let content, terminate, i, l, token - let nextLine = startLine + 1 - const terminatorRules = state.md.block.ruler.getRules('paragraph') - const endLine = state.lineMax - - // jump line-by-line until empty one or EOF - for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { - // this would be a code block normally, but after paragraph - // it's considered a lazy continuation regardless of what's there - if (state.sCount[nextLine] - state.blkIndent > 3) { continue } - - // quirk for blockquotes, this line should already be checked by that rule - if (state.sCount[nextLine] < 0) { continue } - - // Some tags can terminate paragraph without empty line. - terminate = false - for (i = 0, l = terminatorRules.length; i < l; i++) { - if (terminatorRules[i](state, nextLine, endLine, true)) { - terminate = true - break - } - } - if (terminate) { break } - } - - content = state.getLines(startLine, nextLine, state.blkIndent, false).trim() - - state.line = nextLine - - token = state.push('paragraph_open', 'p', 1) - token.map = [startLine, state.line] - - if (state.parentType === 'list') { - const match = content.match(/^\[( |x)\] ?(.+)/i) - if (match) { - const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open') - if (liToken) { - if (!liToken.attrs) { - liToken.attrs = [] + if (langType === 'flowchart') { + return `
${str}
` } - liToken.attrs.push(['class', 'taskListItem']) + if (langType === 'sequence') { + return `
${str}
` + } + return '
' +
+          '' + fileName + '' +
+          createGutter(str, firstLineNumber) +
+          '' +
+          str +
+          '
' } - content = `` } + + const updatedOptions = Object.assign(defaultOptions, options) + this.md = markdownit(updatedOptions) + + // Sanitize use rinput before other plugins + this.md.use(sanitize, { + allowedTags: ['img', 'iframe', 'input'], + allowedAttributes: { + '*': ['alt', 'style'], + 'img': ['src', 'width', 'height'], + 'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'], + 'input': ['type', 'id', 'checked'] + }, + allowedIframeHostnames: ['www.youtube.com'] + }) + + this.md.use(emoji, { + shortcuts: {} + }) + this.md.use(math, { + inlineOpen: config.preview.latexInlineOpen, + inlineClose: config.preview.latexInlineClose, + blockOpen: config.preview.latexBlockOpen, + blockClose: config.preview.latexBlockClose, + inlineRenderer: function (str) { + let output = '' + try { + output = katex.renderToString(str.trim()) + } catch (err) { + output = `${err.message}` + } + return output + }, + blockRenderer: function (str) { + let output = '' + try { + output = katex.renderToString(str.trim(), { displayMode: true }) + } catch (err) { + output = `
${err.message}
` + } + return output + } + }) + this.md.use(require('markdown-it-imsize')) + this.md.use(require('markdown-it-footnote')) + this.md.use(require('markdown-it-multimd-table')) + this.md.use(require('markdown-it-named-headers'), { + slugify: (header) => { + return encodeURI(header.trim() + .replace(/[\]\[\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~]/g, '') + .replace(/\s+/g, '-')) + .replace(/\-+$/, '') + } + }) + this.md.use(require('markdown-it-kbd')) + + const deflate = require('markdown-it-plantuml/lib/deflate') + this.md.use(require('markdown-it-plantuml'), '', { + generateSource: function (umlCode) { + const s = unescape(encodeURIComponent(umlCode)) + const zippedCode = deflate.encode64( + deflate.zip_deflate(`@startuml\n${s}\n@enduml`, 9) + ) + return `http://www.plantuml.com/plantuml/svg/${zippedCode}` + } + }) + + // Override task item + this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) { + let content, terminate, i, l, token + let nextLine = startLine + 1 + const terminatorRules = state.md.block.ruler.getRules('paragraph') + const endLine = state.lineMax + + // jump line-by-line until empty one or EOF + for (; nextLine < endLine && !state.isEmpty(nextLine); nextLine++) { + // this would be a code block normally, but after paragraph + // it's considered a lazy continuation regardless of what's there + if (state.sCount[nextLine] - state.blkIndent > 3) { continue } + + // quirk for blockquotes, this line should already be checked by that rule + if (state.sCount[nextLine] < 0) { continue } + + // Some tags can terminate paragraph without empty line. + terminate = false + for (i = 0, l = terminatorRules.length; i < l; i++) { + if (terminatorRules[i](state, nextLine, endLine, true)) { + terminate = true + break + } + } + if (terminate) { break } + } + + content = state.getLines(startLine, nextLine, state.blkIndent, false).trim() + + state.line = nextLine + + token = state.push('paragraph_open', 'p', 1) + token.map = [startLine, state.line] + + if (state.parentType === 'list') { + const match = content.match(/^\[( |x)\] ?(.+)/i) + if (match) { + const liToken = lastFindInArray(state.tokens, token => token.type === 'list_item_open') + if (liToken) { + if (!liToken.attrs) { + liToken.attrs = [] + } + liToken.attrs.push(['class', 'taskListItem']) + } + content = `` + } + } + + token = state.push('inline', '', 0) + token.content = content + token.map = [startLine, state.line] + token.children = [] + + token = state.push('paragraph_close', 'p', -1) + + return true + }) + + // Add line number attribute for scrolling + const originalRender = this.md.renderer.render + this.md.renderer.render = (tokens, options, env) => { + tokens.forEach((token) => { + switch (token.type) { + case 'heading_open': + case 'paragraph_open': + case 'blockquote_open': + case 'table_open': + token.attrPush(['data-line', token.map[0]]) + } + }) + const result = originalRender.call(this.md.renderer, tokens, options, env) + return result + } + // FIXME We should not depend on global variable. + window.md = this.md } - token = state.push('inline', '', 0) - token.content = content - token.map = [startLine, state.line] - token.children = [] - - token = state.push('paragraph_close', 'p', -1) - - return true -}) - -// Add line number attribute for scrolling -const originalRender = md.renderer.render -md.renderer.render = function render (tokens, options, env) { - tokens.forEach((token) => { - switch (token.type) { - case 'heading_open': - case 'paragraph_open': - case 'blockquote_open': - case 'table_open': - token.attrPush(['data-line', token.map[0]]) - } - }) - const result = originalRender.call(md.renderer, tokens, options, env) - return result -} -// FIXME We should not depend on global variable. -window.md = md - -function normalizeLinkText (linkText) { - return md.normalizeLinkText(linkText) -} - -const markdown = { - render: function markdown (content) { + render (content) { if (!_.isString(content)) content = '' - const renderedContent = md.render(content) - return renderedContent - }, - normalizeLinkText + return this.md.render(content) + } + + normalizeLinkText (linkText) { + return this.md.normalizeLinkText(linkText) + } } -export default markdown +export default Markdown + diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index a543a5aa..6821bf2f 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -139,7 +139,7 @@ class MarkdownNoteDetail extends React.Component { hashHistory.replace({ pathname: location.pathname, query: { - key: newNote.storage + '-' + newNote.key + key: newNote.key } }) this.setState({ @@ -393,7 +393,7 @@ class MarkdownNoteDetail extends React.Component { `${note.storage}-${note.key}` === noteKey) + return notes.find((note) => note.key === noteKey) } function findNotesByKeys (notes, noteKeys) { @@ -43,7 +43,7 @@ function findNotesByKeys (notes, noteKeys) { } function getNoteKey (note) { - return `${note.storage}-${note.key}` + return note.key } class NoteList extends React.Component { @@ -119,10 +119,10 @@ class NoteList extends React.Component { componentDidUpdate (prevProps) { const { location } = this.props const { selectedNoteKeys } = this.state - const visibleNoteKeys = this.notes.map(note => `${note.storage}-${note.key}`) + const visibleNoteKeys = this.notes.map(note => note.key) const note = this.notes[0] const prevKey = prevProps.location.query.key - const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && `${note.storage}-${note.key}` + const noteKey = visibleNoteKeys.includes(prevKey) ? prevKey : note && note.key if (note && location.query.key == null) { const { router } = this.context @@ -590,11 +590,9 @@ class NoteList extends React.Component { }) if (dialogueButtonIndex === 1) return Promise.all( - selectedNoteKeys.map((uniqueKey) => { - const storageKey = uniqueKey.split('-')[0] - const noteKey = uniqueKey.split('-')[1] + selectedNotes.map((note) => { return dataApi - .deleteNote(storageKey, noteKey) + .deleteNote(note.storage, note.key) }) ) .then((data) => { @@ -655,19 +653,18 @@ class NoteList extends React.Component { content: firstNote.content }) .then((note) => { - const uniqueKey = note.storage + '-' + note.key dispatch({ type: 'UPDATE_NOTE', note: note }) this.setState({ - selectedNoteKeys: [uniqueKey] + selectedNoteKeys: [note.key] }) hashHistory.push({ pathname: location.pathname, - query: {key: uniqueKey} + query: {key: note.key} }) }) } diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js index 8feea6fe..92222d27 100644 --- a/browser/main/SideNav/StorageItem.js +++ b/browser/main/SideNav/StorageItem.js @@ -9,6 +9,7 @@ import RenameFolderModal from 'browser/main/modals/RenameFolderModal' import dataApi from 'browser/main/lib/dataApi' import StorageItemChild from 'browser/components/StorageItem' import _ from 'lodash' +import { SortableElement } from 'react-sortable-hoc' const { remote } = require('electron') const { Menu, dialog } = remote @@ -219,7 +220,8 @@ class StorageItem extends React.Component { render () { const { storage, location, isFolded, data, dispatch } = this.props const { folderNoteMap, trashedSet } = data - const folderList = storage.folders.map((folder) => { + const SortableStorageItemChild = SortableElement(StorageItemChild) + const folderList = storage.folders.map((folder, index) => { const isActive = !!(location.pathname.match(new RegExp('\/storages\/' + storage.key + '\/folders\/' + folder.key))) const noteSet = folderNoteMap.get(storage.key + '-' + folder.key) @@ -233,8 +235,9 @@ class StorageItem extends React.Component { noteCount = noteSet.size - trashedNoteCount } return ( - this.handleFolderButtonClick(folder.key)(e)} handleContextMenu={(e) => this.handleFolderButtonContextMenu(e, folder)} @@ -256,9 +259,9 @@ class StorageItem extends React.Component { key={storage.key} >
this.handleHeaderContextMenu(e)} > diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js index 6d05e37b..5e559c64 100644 --- a/browser/main/SideNav/index.js +++ b/browser/main/SideNav/index.js @@ -17,6 +17,7 @@ import EventEmitter from 'browser/main/lib/eventEmitter' import PreferenceButton from './PreferenceButton' import ListButton from './ListButton' import TagButton from './TagButton' +import {SortableContainer} from 'react-sortable-hoc' class SideNav extends React.Component { // TODO: should not use electron stuff v0.7 @@ -68,6 +69,17 @@ class SideNav extends React.Component { router.push('/alltags') } + onSortEnd (storage) { + return ({oldIndex, newIndex}) => { + const { dispatch } = this.props + dataApi + .reorderFolder(storage.key, oldIndex, newIndex) + .then((data) => { + dispatch({ type: 'REORDER_FOLDER', storage: data.storage }) + }) + } + } + SideNavComponent (isFolded, storageList) { const { location, data } = this.props @@ -117,9 +129,9 @@ class SideNav extends React.Component { tagListComponent () { const { data, location } = this.props - const tagList = data.tagNoteMap.map((tag, name) => { + const tagList = _.sortBy(data.tagNoteMap.map((tag, name) => { return { name, size: tag.size } - }) + }), ['name']) return ( tagList.map(tag => { return ( @@ -148,10 +160,8 @@ class SideNav extends React.Component { emptyTrash (entries) { const { dispatch } = this.props - const deletionPromises = entries.map((storageAndNoteKey) => { - const storageKey = storageAndNoteKey.split('-')[0] - const noteKey = storageAndNoteKey.split('-')[1] - return dataApi.deleteNote(storageKey, noteKey) + const deletionPromises = entries.map((note) => { + return dataApi.deleteNote(note.storage, note.key) }) Promise.all(deletionPromises) .then((arrayOfStorageAndNoteKeys) => { @@ -167,9 +177,9 @@ class SideNav extends React.Component { handleFilterButtonContextMenu (event) { const { data } = this.props - const entries = data.trashedSet.toJS() + const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey)) const menu = Menu.buildFromTemplate([ - { label: 'Empty Trash', click: () => this.emptyTrash(entries) } + { label: 'Empty Trash', click: () => this.emptyTrash(trashedNotes) } ]) menu.popup() } @@ -180,13 +190,16 @@ class SideNav extends React.Component { const isFolded = config.isSideNavFolded const storageList = data.storageMap.map((storage, key) => { - return }) const style = {} diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index c7c5fc26..2da2f1c2 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -50,7 +50,8 @@ export const DEFAULT_CONFIG = { latexInlineClose: '$', latexBlockOpen: '$$', latexBlockClose: '$$', - scrollPastEnd: false + scrollPastEnd: false, + smartQuotes: true }, blog: { type: 'wordpress', // Available value: wordpress, add more types in the future plz diff --git a/browser/main/lib/dataApi/createNote.js b/browser/main/lib/dataApi/createNote.js index 4b667385..e5d44489 100644 --- a/browser/main/lib/dataApi/createNote.js +++ b/browser/main/lib/dataApi/createNote.js @@ -52,12 +52,12 @@ function createNote (storageKey, input) { return storage }) .then(function saveNote (storage) { - let key = keygen() + let key = keygen(true) let isUnique = false while (!isUnique) { try { sander.statSync(path.join(storage.path, 'notes', key + '.cson')) - key = keygen() + key = keygen(true) } catch (err) { if (err.code === 'ENOENT') { isUnique = true diff --git a/browser/main/lib/dataApi/moveNote.js b/browser/main/lib/dataApi/moveNote.js index b37b6ac5..928d331b 100644 --- a/browser/main/lib/dataApi/moveNote.js +++ b/browser/main/lib/dataApi/moveNote.js @@ -39,12 +39,12 @@ function moveNote (storageKey, noteKey, newStorageKey, newFolderKey) { return resolveStorageData(newStorage) .then(function findNewNoteKey (_newStorage) { newStorage = _newStorage - newNoteKey = keygen() + newNoteKey = keygen(true) let isUnique = false while (!isUnique) { try { sander.statSync(path.join(newStorage.path, 'notes', newNoteKey + '.cson')) - newNoteKey = keygen() + newNoteKey = keygen(true) } catch (err) { if (err.code === 'ENOENT') { isUnique = true diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js index 641c348d..c959000c 100644 --- a/browser/main/modals/NewNoteModal.js +++ b/browser/main/modals/NewNoteModal.js @@ -36,7 +36,7 @@ class NewNoteModal extends React.Component { content: '' }) .then((note) => { - const noteHash = `${note.storage}-${note.key}` + const noteHash = note.key dispatch({ type: 'UPDATE_NOTE', note: note @@ -76,7 +76,7 @@ class NewNoteModal extends React.Component { }] }) .then((note) => { - const noteHash = `${note.storage}-${note.key}` + const noteHash = note.key dispatch({ type: 'UPDATE_NOTE', note: note diff --git a/browser/main/modals/PreferencesModal/InfoTab.js b/browser/main/modals/PreferencesModal/InfoTab.js index 1173389e..1b2d55bb 100644 --- a/browser/main/modals/PreferencesModal/InfoTab.js +++ b/browser/main/modals/PreferencesModal/InfoTab.js @@ -43,7 +43,7 @@ class InfoTab extends React.Component { }) } else { this.setState({ - amaMessage: i18n.__('Thank\'s for trust us') + amaMessage: i18n.__('Thank\'s for trusting us') }) } diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index b6144d51..23c302d0 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -90,7 +90,8 @@ class UiTab extends React.Component { latexInlineClose: this.refs.previewLatexInlineClose.value, latexBlockOpen: this.refs.previewLatexBlockOpen.value, latexBlockClose: this.refs.previewLatexBlockClose.value, - scrollPastEnd: this.refs.previewScrollPastEnd.checked + scrollPastEnd: this.refs.previewScrollPastEnd.checked, + smartQuotes: this.refs.previewSmartQuotes.checked } } @@ -429,6 +430,16 @@ class UiTab extends React.Component { {i18n.__('Show line numbers for preview code blocks')}
+
+ +
{i18n.__('LaTeX Inline Open Delimiter')} diff --git a/browser/main/store.js b/browser/main/store.js index abd34889..f078ad20 100644 --- a/browser/main/store.js +++ b/browser/main/store.js @@ -27,7 +27,7 @@ function data (state = defaultDataMap(), action) { action.notes.some((note) => { if (note === undefined) return true - const uniqueKey = note.storage + '-' + note.key + const uniqueKey = note.key const folderKey = note.storage + '-' + note.folder state.noteMap.set(uniqueKey, note) @@ -66,7 +66,7 @@ function data (state = defaultDataMap(), action) { case 'UPDATE_NOTE': { const note = action.note - const uniqueKey = note.storage + '-' + note.key + const uniqueKey = note.key const folderKey = note.storage + '-' + note.folder const oldNote = state.noteMap.get(uniqueKey) @@ -162,9 +162,9 @@ function data (state = defaultDataMap(), action) { case 'MOVE_NOTE': { const originNote = action.originNote - const originKey = originNote.storage + '-' + originNote.key + const originKey = originNote.key const note = action.note - const uniqueKey = note.storage + '-' + note.key + const uniqueKey = note.key const folderKey = note.storage + '-' + note.folder const oldNote = state.noteMap.get(uniqueKey) @@ -297,7 +297,7 @@ function data (state = defaultDataMap(), action) { } case 'DELETE_NOTE': { - const uniqueKey = action.storageKey + '-' + action.noteKey + const uniqueKey = action.noteKey const targetNote = state.noteMap.get(uniqueKey) state = Object.assign({}, state) @@ -423,7 +423,7 @@ function data (state = defaultDataMap(), action) { state.folderNoteMap = new Map(state.folderNoteMap) state.tagNoteMap = new Map(state.tagNoteMap) action.notes.forEach((note) => { - const uniqueKey = note.storage + '-' + note.key + const uniqueKey = note.key const folderKey = note.storage + '-' + note.folder state.noteMap.set(uniqueKey, note) @@ -483,7 +483,7 @@ function data (state = defaultDataMap(), action) { state.tagNoteMap = new Map(state.tagNoteMap) state.starredSet = new Set(state.starredSet) notes.forEach((note) => { - const noteKey = storage.key + '-' + note.key + const noteKey = note.key state.noteMap.delete(noteKey) state.starredSet.delete(noteKey) note.tags.forEach((tag) => { diff --git a/docs/zh_TW/build.md b/docs/zh_TW/build.md new file mode 100644 index 00000000..ff17d9c5 --- /dev/null +++ b/docs/zh_TW/build.md @@ -0,0 +1,86 @@ +# 編譯 +此文件還提供下列的語言 [日文](https://github.com/BoostIO/Boostnote/blob/master/docs/jp/build.md), [韓文](https://github.com/BoostIO/Boostnote/blob/master/docs/ko/build.md), [俄文](https://github.com/BoostIO/Boostnote/blob/master/docs/ru/build.md), [簡體中文](https://github.com/BoostIO/Boostnote/blob/master/docs/zh_CN/build.md), [法文](https://github.com/BoostIO/Boostnote/blob/master/docs/fr/build.md) and [德文](https://github.com/BoostIO/Boostnote/blob/master/docs/de/build.md). + +## 環境 +* npm: 4.x +* node: 7.x + +`$ grunt pre-build` 在 `npm v5.x` 有問題,所以只能用 `npm v4.x` 。 + +## 開發 + +我們使用 Webpack HMR 來開發 Boostnote。 + +在專案根目錄底下執行下列指令,將會以原始設置啟動 Boostnote。 + +**用 yarn 來安裝必要 packages** + +```bash +$ yarn +``` + +**開始開發** + +``` +$ yarn run dev-start +``` + +上述指令同時運行了 `yarn run webpack` 及 `yarn run hot`,相當於將這兩個指令在不同的 terminal 中運行。 + +`webpack` 會同時監控修改過的程式碼,並 +The `webpack` will watch for code changes and then apply them automatically. + +If the following error occurs: `Failed to load resource: net::ERR_CONNECTION_REFUSED`, please reload Boostnote. + +![net::ERR_CONNECTION_REFUSED](https://cloud.githubusercontent.com/assets/11307908/24343004/081e66ae-1279-11e7-8d9e-7f478043d835.png) + +> ### Notice +> There are some cases where you have to refresh the app manually. +> 1. When editing a constructor method of a component +> 2. When adding a new css class (similar to 1: the CSS class is re-written by each component. This process occurs at the Constructor method.) + +## Deploy + +We use Grunt to automate deployment. +You can build the program by using `grunt`. However, we don't recommend this because the default task includes codesign and authenticode. + +So, we've prepared a separate script which just makes an executable file. + +This build doesn't work on npm v5.3.0. So you need to use v5.2.0 when you build it. + +``` +grunt pre-build +``` + +You will find the executable in the `dist` directory. Note, the auto updater won't work because the app isn't signed. + +If you find it necessary, you can use codesign or authenticode with this executable. + +## Make own distribution packages (deb, rpm) + +Distribution packages are created by exec `grunt build` on Linux platform (e.g. Ubuntu, Fedora). + +> Note: You can create both `.deb` and `.rpm` in a single environment. + +After installing the supported version of `node` and `npm`, install build dependency packages. + + +Ubuntu/Debian: + +``` +$ sudo apt-get install -y rpm fakeroot +``` + +Fedora: + +``` +$ sudo dnf install -y dpkg dpkg-dev rpm-build fakeroot +``` + +Then execute `grunt build`. + +``` +$ grunt build +``` + +You will find `.deb` and `.rpm` in the `dist` directory. diff --git a/package.json b/package.json index e079bcc0..9afbae5f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "boost", "productName": "Boostnote", - "version": "0.10.0", + "version": "0.11.1", "main": "index.js", "description": "Boostnote", "license": "GPL-3.0", @@ -62,7 +62,7 @@ "iconv-lite": "^0.4.19", "immutable": "^3.8.1", "js-sequence-diagrams": "^1000000.0.6", - "katex": "^0.8.3", + "katex": "^0.9.0", "lodash": "^4.11.1", "lodash-move": "^1.1.1", "markdown-it": "^6.0.1", @@ -89,7 +89,8 @@ "sanitize-html": "^1.18.2", "striptags": "^2.2.1", "superagent": "^1.2.0", - "superagent-promise": "^1.0.3" + "superagent-promise": "^1.0.3", + "uuid": "^3.2.1" }, "devDependencies": { "ava": "^0.16.0", diff --git a/readme.md b/readme.md index 67a5689d..b5d02729 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -:mega: Open sourcing our [Android and iOS apps](https://github.com/BoostIO/Boostnote-mobile)! +:mega: We've launched a blogging platform with markdown called **[Boostlog](https://boostlog.io/)**. ![Boostnote app screenshot](./resources/repository/top.png) @@ -25,8 +25,8 @@ 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/enQtMzAzMjI1MTIyNTQ3LTc2MjNiYWU3NTc1YjZlMTk3NzFmOWE1ZWU1MGRhMzBkMGIwMWFjOWMxMDRiM2I2NzkzYzc4OGZhNmVhZjYzZTM) -- [Blog](https://boostlog.io/@junp1234) +- [Slack Group](https://join.slack.com/t/boostnote-group/shared_invite/enQtMzI3NTIxMTQzMTQzLTUyYWZmZWM1YzcwYzQ5OWQ5YzA3Y2M2NzUzNmIwNzYzMjg5NmQyOGJlNzcyZDJhMGY0ZDc0ZjdlZDFhMDdiMWE) +- [Blog](https://boostlog.io/tags/boostnote) - [Reddit](https://www.reddit.com/r/Boostnote/) diff --git a/tests/fixtures/TestDummy.js b/tests/fixtures/TestDummy.js index 4edcc9cf..62cae846 100644 --- a/tests/fixtures/TestDummy.js +++ b/tests/fixtures/TestDummy.js @@ -22,7 +22,7 @@ function dummyBoostnoteJSONData (override = {}, isLegacy = false) { if (override.folders == null) { data.folders = [] - var folderCount = Math.floor((Math.random() * 5)) + 1 + var folderCount = Math.floor((Math.random() * 5)) + 2 for (var i = 0; i < folderCount; i++) { var key = keygen() while (data.folders.some((folder) => folder.key === key)) { @@ -105,11 +105,11 @@ function dummyStorage (storagePath, override = {}) { sander.writeFileSync(path.join(storagePath, 'boostnote.json'), JSON.stringify(jsonData)) var notesData = [] - var noteCount = Math.floor((Math.random() * 15)) + 1 + var noteCount = Math.floor((Math.random() * 15)) + 2 for (var i = 0; i < noteCount; i++) { - var key = keygen() + var key = keygen(true) while (notesData.some((note) => note.key === key)) { - key = keygen() + key = keygen(true) } var noteData = dummyNote({ @@ -149,9 +149,9 @@ function dummyLegacyStorage (storagePath, override = {}) { var folderNotes = [] var noteCount = Math.floor((Math.random() * 5)) + 1 for (var i = 0; i < noteCount; i++) { - var key = keygen(6) + var key = keygen(true) while (folderNotes.some((note) => note.key === key)) { - key = keygen(6) + key = keygen(true) } var noteData = dummyNote({ diff --git a/yarn.lock b/yarn.lock index 6dad2806..f370b9ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3986,11 +3986,11 @@ jsx-ast-utils@^2.0.0: dependencies: array-includes "^3.0.3" -katex@^0.8.3: - version "0.8.3" - resolved "https://registry.yarnpkg.com/katex/-/katex-0.8.3.tgz#909d99864baf964c3ccae39c4a99a8e0fb9a1bd0" +katex@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/katex/-/katex-0.9.0.tgz#26a7d082c21d53725422d2d71da9b2d8455fbd4a" dependencies: - match-at "^0.1.0" + match-at "^0.1.1" kind-of@^3.0.2: version "3.2.2" @@ -4254,9 +4254,9 @@ markdown-it@^6.0.1: mdurl "~1.0.1" uc.micro "^1.0.1" -match-at@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/match-at/-/match-at-0.1.0.tgz#f561e7709ff9a105b85cc62c6b8ee7c15bf24f31" +match-at@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/match-at/-/match-at-0.1.1.tgz#25d040d291777704d5e6556bbb79230ec2de0540" matcher@^0.1.1: version "0.1.2" @@ -6168,11 +6168,7 @@ speedometer@~0.1.2: sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - -sprintf@^0.1.5: - version "0.1.5" - resolved "https://registry.yarnpkg.com/sprintf/-/sprintf-0.1.5.tgz#8f83e39a9317c1a502cb7db8050e51c679f6edcf" - + srcset@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/srcset/-/srcset-1.0.0.tgz#a5669de12b42f3b1d5e83ed03c71046fc48f41ef" @@ -6812,6 +6808,10 @@ uuid@^2.0.1, uuid@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a" +uuid@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" + validate-npm-package-license@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc"