diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index e9e1dfbf..f16cc53c 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -5,23 +5,18 @@ import CodeMirror from 'codemirror' import 'codemirror-mode-elixir' import attachmentManagement from 'browser/main/lib/dataApi/attachmentManagement' import convertModeName from 'browser/lib/convertModeName' +import { options, TableEditor } from '@susisu/mte-kernel' +import TextEditorInterface from 'browser/lib/TextEditorInterface' 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') +import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js' -const defaultEditorFontFamily = [ - 'Monaco', - 'Menlo', - 'Ubuntu Mono', - 'Consolas', - 'source-code-pro', - 'monospace' -] const buildCMRulers = (rulers, enableRulers) => (enableRulers ? rulers.map(ruler => ({ column: ruler })) : []) @@ -63,6 +58,8 @@ export default class CodeEditor extends React.Component { } this.searchHandler = (e, msg) => this.handleSearch(msg) this.searchState = null + + this.formatTable = () => this.handleFormatTable() } handleSearch (msg) { @@ -99,6 +96,10 @@ export default class CodeEditor extends React.Component { }) } + handleFormatTable () { + this.tableEditor.formatAll(options({textWidthOptions: {}})) + } + componentDidMount () { const { rulers, enableRulers } = this.props const expandSnippet = this.expandSnippet.bind(this) @@ -135,7 +136,12 @@ export default class CodeEditor extends React.Component { dragDrop: false, foldGutter: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], - autoCloseBrackets: true, + autoCloseBrackets: { + pairs: '()[]{}\'\'""$$**``', + triples: '```"""\'\'\'', + explode: '[]{}``$$', + override: true + }, extraKeys: { Tab: function (cm) { const cursor = cm.getCursor() @@ -210,6 +216,8 @@ export default class CodeEditor extends React.Component { CodeMirror.Vim.defineEx('qw', 'qw', this.quitEditor) CodeMirror.Vim.map('ZZ', ':q', 'normal') this.setState({ isReady: true }) + this.tableEditor = new TableEditor(new TextEditorInterface(this.editor)) + eventEmitter.on('code:format-table', this.formatTable) } expandSnippet (line, cursor, cm, snippets) { @@ -299,6 +307,8 @@ export default class CodeEditor extends React.Component { this.editor.off('scroll', this.scrollHandler) const editorTheme = document.getElementById('editorTheme') editorTheme.removeEventListener('load', this.loadStyleHandler) + + eventEmitter.off('code:format-table', this.formatTable) } componentDidUpdate (prevProps, prevState) { @@ -561,11 +571,8 @@ export default class CodeEditor extends React.Component { } render () { - const { className, fontSize } = this.props - let fontFamily = this.props.fontFamily - fontFamily = _.isString(fontFamily) && fontFamily.length > 0 - ? [fontFamily].concat(defaultEditorFontFamily) - : defaultEditorFontFamily + const {className, fontSize} = this.props + const fontFamily = normalizeEditorFontFamily(this.props.fontFamily) const width = this.props.width return (
{ + let result = noteContent + if (this.props && this.props.storagePath && this.props.noteKey) { + const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(noteContent, this.props.storagePath) + attachmentsAbsolutePaths.forEach((attachment) => { + exportTasks.push({ + src: attachment, + dst: attachmentManagement.DESTINATION_FOLDER + }) + }) + result = attachmentManagement.removeStorageAndNoteReferences(noteContent, this.props.noteKey) + } + return result + }) } handleSaveAsHtml () { this.exportAsDocument('html', (noteContent, exportTasks) => { - const { - fontFamily, - fontSize, - codeBlockFontFamily, - lineNumber, - codeBlockTheme, - scrollPastEnd, - theme, - allowCustomCSS, - customCSS - } = this.getStyleParams() - const inlineStyles = buildStyle( - fontFamily, - fontSize, - codeBlockFontFamily, - lineNumber, - scrollPastEnd, - theme, - allowCustomCSS, - customCSS - ) - let body = this.markdown.render(escapeHtmlCharacters(noteContent)) + const {fontFamily, fontSize, codeBlockFontFamily, lineNumber, codeBlockTheme, scrollPastEnd, theme, allowCustomCSS, customCSS} = this.getStyleParams() + const inlineStyles = buildStyle(fontFamily, fontSize, codeBlockFontFamily, lineNumber, scrollPastEnd, theme, allowCustomCSS, customCSS) + let body = this.markdown.render(escapeHtmlCharacters(noteContent, { detectCodeBlock: true })) const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES] const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent( noteContent, this.props.storagePath ) - files.forEach(file => { - file = file.replace('file://', '') + + files.forEach((file) => { + if (global.process.platform === 'win32') { + file = file.replace('file:///', '') + } else { + file = file.replace('file://', '') + } exportTasks.push({ src: file, dst: 'css' @@ -347,6 +367,21 @@ export default class MarkdownPreview extends React.Component { } } + getScrollBarStyle () { + const { + theme + } = this.props + + switch (theme) { + case 'dark': + case 'solarized-dark': + case 'monokai': + return scrollBarDarkStyle + default: + return scrollBarStyle + } + } + componentDidMount () { this.refs.root.setAttribute('sandbox', 'allow-scripts') this.refs.root.contentWindow.document.body.addEventListener( @@ -358,6 +393,9 @@ export default class MarkdownPreview extends React.Component { + ` CSS_FILES.forEach(file => { @@ -565,23 +603,9 @@ export default class MarkdownPreview extends React.Component { let { value, codeBlockTheme } = this.props this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme) - - const codeBlocks = value.match(/(```)(.|[\n])*?(```)/g) - if (codeBlocks !== null) { - codeBlocks.forEach(codeBlock => { - value = value.replace( - codeBlock, - htmlTextHelper.encodeEntities(codeBlock) - ) - }) - } - let renderedHTML = this.markdown.render(value) - attachmentManagement.migrateAttachments(renderedHTML, storagePath, noteKey) - this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS( - renderedHTML, - storagePath - ) - + const renderedHTML = this.markdown.render(value) + attachmentManagement.migrateAttachments(value, storagePath, noteKey) + this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(renderedHTML, storagePath) _.forEach( this.refs.root.contentWindow.document.querySelectorAll( 'input[type="checkbox"]' @@ -683,6 +707,30 @@ export default class MarkdownPreview extends React.Component { el.innerHTML = 'Sequence diagram parse error: ' + e.message } } + }) + + _.forEach( + this.refs.root.contentWindow.document.querySelectorAll('.chart'), + (el) => { + try { + const chartConfig = JSON.parse(el.innerHTML) + el.innerHTML = '' + var canvas = document.createElement('canvas') + el.appendChild(canvas) + /* eslint-disable no-new */ + new Chart(canvas, chartConfig) + } catch (e) { + console.error(e) + el.className = 'chart-error' + el.innerHTML = 'chartjs diagram parse error: ' + e.message + } + } + ) + _.forEach( + this.refs.root.contentWindow.document.querySelectorAll('.mermaid'), + (el) => { + mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme) + } ) } diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl index cf94bb8e..03503231 100644 --- a/browser/components/markdown.styl +++ b/browser/components/markdown.styl @@ -68,7 +68,7 @@ body padding 5px margin -5px border-radius 5px - .flowchart-error, .sequence-error + .flowchart-error, .sequence-error .chart-error background-color errorBackgroundColor color errorTextColor padding 5px @@ -213,7 +213,7 @@ pre margin 0 0 1em display flex line-height 1.4em - &.flowchart, &.sequence + &.flowchart, &.sequence, &.chart display flex justify-content center background-color white diff --git a/browser/components/render/MermaidRender.js b/browser/components/render/MermaidRender.js new file mode 100644 index 00000000..12dce327 --- /dev/null +++ b/browser/components/render/MermaidRender.js @@ -0,0 +1,39 @@ +import mermaidAPI from 'mermaid' + +// fixes bad styling in the mermaid dark theme +const darkThemeStyling = ` +.loopText tspan { + fill: white; +}` + +function getRandomInt (min, max) { + return Math.floor(Math.random() * (max - min)) + min +} + +function getId () { + var pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + var id = 'm-' + for (var i = 0; i < 7; i++) { + id += pool[getRandomInt(0, 16)] + } + return id +} + +function render (element, content, theme) { + try { + let isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' + mermaidAPI.initialize({ + theme: isDarkTheme ? 'dark' : 'default', + themeCSS: isDarkTheme ? darkThemeStyling : '' + }) + mermaidAPI.render(getId(), content, (svgGraph) => { + element.innerHTML = svgGraph + }) + } catch (e) { + console.error(e) + element.className = 'mermaid-error' + element.innerHTML = 'mermaid diagram parse error: ' + e.message + } +} + +export default render diff --git a/browser/lib/TextEditorInterface.js b/browser/lib/TextEditorInterface.js new file mode 100644 index 00000000..53ae2337 --- /dev/null +++ b/browser/lib/TextEditorInterface.js @@ -0,0 +1,53 @@ +import { Point } from '@susisu/mte-kernel' + +export default class TextEditorInterface { + constructor (editor) { + this.editor = editor + } + + getCursorPosition () { + const pos = this.editor.getCursor() + return new Point(pos.line, pos.ch) + } + + setCursorPosition (pos) { + this.editor.setCursor({line: pos.row, ch: pos.column}) + } + + setSelectionRange (range) { + this.editor.setSelection({ + anchor: {line: range.start.row, ch: range.start.column}, + head: {line: range.end.row, ch: range.end.column} + }) + } + + getLastRow () { + return this.editor.lastLine() + } + + acceptsTableEdit (row) { + return true + } + + getLine (row) { + return this.editor.getLine(row) + } + + insertLine (row, line) { + this.editor.replaceRange(line, {line: row, ch: 0}) + } + + deleteLine (row) { + this.editor.replaceRange('', {line: row, ch: 0}, {line: row, ch: this.editor.getLine(row).length}) + } + + replaceLines (startRow, endRow, lines) { + endRow-- // because endRow is a first line after a table. + const endRowCh = this.editor.getLine(endRow).length + this.editor.replaceRange(lines, {line: startRow, ch: 0}, {line: endRow, ch: endRowCh}) + } + + transact (func) { + func() + } +} diff --git a/browser/lib/consts.js b/browser/lib/consts.js index ec811007..84b962eb 100644 --- a/browser/lib/consts.js +++ b/browser/lib/consts.js @@ -36,7 +36,15 @@ const consts = { 'Violet Eggplant' ], THEMES: ['default'].concat(themes), - SNIPPET_FILE: snippetFile + SNIPPET_FILE: snippetFile, + DEFAULT_EDITOR_FONT_FAMILY: [ + 'Monaco', + 'Menlo', + 'Ubuntu Mono', + 'Consolas', + 'source-code-pro', + 'monospace' + ] } module.exports = consts diff --git a/browser/lib/markdown-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js index beec9566..05e5e7be 100644 --- a/browser/lib/markdown-it-sanitize-html.js +++ b/browser/lib/markdown-it-sanitize-html.js @@ -1,6 +1,7 @@ 'use strict' import sanitizeHtml from 'sanitize-html' +import { escapeHtmlCharacters } from './utils' module.exports = function sanitizePlugin (md, options) { options = options || {} @@ -8,13 +9,26 @@ module.exports = function sanitizePlugin (md, options) { md.core.ruler.after('linkify', 'sanitize_inline', state => { for (let tokenIdx = 0; tokenIdx < state.tokens.length; tokenIdx++) { if (state.tokens[tokenIdx].type === 'html_block') { - state.tokens[tokenIdx].content = sanitizeHtml(state.tokens[tokenIdx].content, options) + state.tokens[tokenIdx].content = sanitizeHtml( + state.tokens[tokenIdx].content, + options + ) + } + if (state.tokens[tokenIdx].type === 'fence') { + // escapeHtmlCharacters has better performance + state.tokens[tokenIdx].content = escapeHtmlCharacters( + state.tokens[tokenIdx].content, + { skipSingleQuote: true } + ) } if (state.tokens[tokenIdx].type === 'inline') { const inlineTokens = state.tokens[tokenIdx].children for (let childIdx = 0; childIdx < inlineTokens.length; childIdx++) { if (inlineTokens[childIdx].type === 'html_inline') { - inlineTokens[childIdx].content = sanitizeHtml(inlineTokens[childIdx].content, options) + inlineTokens[childIdx].content = sanitizeHtml( + inlineTokens[childIdx].content, + options + ) } } } diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js index 4dafa4a3..49fd2f86 100644 --- a/browser/lib/markdown.js +++ b/browser/lib/markdown.js @@ -40,6 +40,12 @@ class Markdown { if (langType === 'sequence') { return `
${str}
` } + if (langType === 'chart') { + return `
${str}
` + } + if (langType === 'mermaid') { + return `
${str}
` + } return '
' +
           '' + fileName + '' +
           createGutter(str, firstLineNumber) +
@@ -157,6 +163,22 @@ class Markdown {
       }
     })
 
+    // Ditaa support
+    this.md.use(require('markdown-it-plantuml'), {
+      openMarker: '@startditaa',
+      closeMarker: '@endditaa',
+      generateSource: function (umlCode) {
+        const stripTrailingSlash = (url) => url.endsWith('/') ? url.slice(0, -1) : url
+        // Currently PlantUML server doesn't support Ditaa in SVG, so we set the format as PNG at the moment.
+        const serverAddress = stripTrailingSlash(config.preview.plantUMLServerAddress) + '/png'
+        const s = unescape(encodeURIComponent(umlCode))
+        const zippedCode = deflate.encode64(
+          deflate.zip_deflate(`@startditaa\n${s}\n@endditaa`, 9)
+        )
+        return `${serverAddress}/${zippedCode}`
+      }
+    })
+
     // Override task item
     this.md.block.ruler.at('paragraph', function (state, startLine/*, endLine */) {
       let content, terminate, i, l, token
@@ -245,4 +267,3 @@ class Markdown {
 }
 
 export default Markdown
-
diff --git a/browser/lib/normalizeEditorFontFamily.js b/browser/lib/normalizeEditorFontFamily.js
new file mode 100644
index 00000000..a2a2ec31
--- /dev/null
+++ b/browser/lib/normalizeEditorFontFamily.js
@@ -0,0 +1,9 @@
+import consts from 'browser/lib/consts'
+import isString from 'lodash/isString'
+
+export default function normalizeEditorFontFamily (fontFamily) {
+  const defaultEditorFontFamily = consts.DEFAULT_EDITOR_FONT_FAMILY
+  return isString(fontFamily) && fontFamily.length > 0
+    ? [fontFamily].concat(defaultEditorFontFamily).join(', ')
+    : defaultEditorFontFamily.join(', ')
+}
diff --git a/browser/lib/search.js b/browser/lib/search.js
index b42fd389..cf5b3b1b 100644
--- a/browser/lib/search.js
+++ b/browser/lib/search.js
@@ -23,7 +23,9 @@ function findByWordOrTag (notes, block) {
       return true
     }
     if (note.type === 'SNIPPET_NOTE') {
-      return note.description.match(wordRegExp)
+      return note.description.match(wordRegExp) || note.snippets.some((snippet) => {
+        return snippet.name.match(wordRegExp) || snippet.content.match(wordRegExp)
+      })
     } else if (note.type === 'MARKDOWN_NOTE') {
       return note.content.match(wordRegExp)
     }
diff --git a/browser/lib/utils.js b/browser/lib/utils.js
index 441cfbc7..1d15b722 100644
--- a/browser/lib/utils.js
+++ b/browser/lib/utils.js
@@ -6,52 +6,113 @@ export function lastFindInArray (array, callback) {
   }
 }
 
-export function escapeHtmlCharacters (text) {
-  const matchHtmlRegExp = /["'&<>]/
-  const str = '' + text
-  const match = matchHtmlRegExp.exec(str)
+export function escapeHtmlCharacters (
+  html,
+  opt = { detectCodeBlock: false, skipSingleQuote: false }
+) {
+  const matchHtmlRegExp = /["'&<>]/g
+  const matchCodeBlockRegExp = /```/g
+  const escapes = ['"', '&', ''', '<', '>']
+  let match = null
+  const replaceAt = (str, index, replace) =>
+    str.substr(0, index) +
+    replace +
+    str.substr(index + replace.length - (replace.length - 1))
 
-  if (!match) {
-    return str
-  }
-
-  let escape
-  let html = ''
-  let index = 0
-  let lastIndex = 0
-
-  for (index = match.index; index < str.length; index++) {
-    switch (str.charCodeAt(index)) {
-      case 34: // "
-        escape = '"'
-        break
-      case 38: // &
-        escape = '&'
-        break
-      case 39: // '
-        escape = '''
-        break
-      case 60: // <
-        escape = '<'
-        break
-      case 62: // >
-        escape = '>'
-        break
-      default:
+  while ((match = matchHtmlRegExp.exec(html)) !== null) {
+    const current = { char: match[0], index: match.index }
+    const codeBlockIndexs = []
+    let openCodeBlock = null
+    // if the detectCodeBlock option is activated then this function should skip
+    // characters that needed to be escape but located in code block
+    if (opt.detectCodeBlock) {
+      // The first type of code block is lines that start with 4 spaces
+      // Here we check for the \n character located before the character that
+      // needed to be escape. It means we check for the begining of the line that
+      // contain that character, then we check if there are 4 spaces next to the
+      // \n character (the line start with 4 spaces)
+      let previousLineEnd = current.index - 1
+      while (html[previousLineEnd] !== '\n' && previousLineEnd !== -1) {
+        previousLineEnd--
+      }
+      // 4 spaces means this character is in a code block
+      if (
+        html[previousLineEnd + 1] === ' ' &&
+        html[previousLineEnd + 2] === ' ' &&
+        html[previousLineEnd + 3] === ' ' &&
+        html[previousLineEnd + 4] === ' '
+      ) {
+        // skip the current character
         continue
+      }
+      // The second type of code block is lines that wrapped in ```
+      // We will get the position of each ```
+      // then push it into an array
+      // then the array returned will be like this:
+      // [startCodeblock, endCodeBlock, startCodeBlock, endCodeBlock]
+      while ((openCodeBlock = matchCodeBlockRegExp.exec(html)) !== null) {
+        codeBlockIndexs.push(openCodeBlock.index)
+      }
+      let shouldSkipChar = false
+      // we loop through the array of positions
+      // we skip 2 element as the i index position is the position of ``` that
+      // open the codeblock and the i + 1 is the position of the ``` that close
+      // the code block
+      for (let i = 0; i < codeBlockIndexs.length; i += 2) {
+        // the i index position is the position of the ``` that open code block
+        // so we have to + 2 as that position is the position of the first ` in the ````
+        // but we need to make sure that the position current character is larger
+        // that the last ` in the ``` that open the code block so we have to take
+        // the position of the first ` and + 2
+        // the i + 1 index position is the closing ``` so the char must less than it
+        if (
+          current.index > codeBlockIndexs[i] + 2 &&
+          current.index < codeBlockIndexs[i + 1]
+        ) {
+          // skip it
+          shouldSkipChar = true
+          break
+        }
+      }
+      if (shouldSkipChar) {
+        // skip the current character
+        continue
+      }
     }
-
-    if (lastIndex !== index) {
-      html += str.substring(lastIndex, index)
+    // otherwise, escape it !!!
+    if (current.char === '&') {
+      // when escaping character & we have to be becareful as the & could be a part
+      // of an escaped character like " will be came &quot;
+      let nextStr = ''
+      let nextIndex = current.index
+      let escapedStr = false
+      // maximum length of an escaped string is 5. For example ('"')
+      // we take the next 5 character of the next string if it is one of the string:
+      // ['"', '&', ''', '<', '>'] then we will not escape the & character
+      // as it is a part of the escaped string and should not be escaped
+      while (nextStr.length <= 5) {
+        nextStr += html[nextIndex]
+        nextIndex++
+        if (escapes.indexOf(nextStr) !== -1) {
+          escapedStr = true
+          break
+        }
+      }
+      if (!escapedStr) {
+        // this & char is not a part of an escaped string
+        html = replaceAt(html, current.index, '&')
+      }
+    } else if (current.char === '"') {
+      html = replaceAt(html, current.index, '"')
+    } else if (current.char === "'" && !opt.skipSingleQuote) {
+      html = replaceAt(html, current.index, ''')
+    } else if (current.char === '<') {
+      html = replaceAt(html, current.index, '<')
+    } else if (current.char === '>') {
+      html = replaceAt(html, current.index, '>')
     }
-
-    lastIndex = index + 1
-    html += escape
   }
-
-  return lastIndex !== index
-      ? html + str.substring(lastIndex, index)
-      : html
+  return html
 }
 
 export function isObjectEqual (a, b) {
diff --git a/browser/main/Detail/FullscreenButton.js b/browser/main/Detail/FullscreenButton.js
index 3d29c264..ee212603 100644
--- a/browser/main/Detail/FullscreenButton.js
+++ b/browser/main/Detail/FullscreenButton.js
@@ -4,12 +4,14 @@ import CSSModules from 'browser/lib/CSSModules'
 import styles from './FullscreenButton.styl'
 import i18n from 'browser/lib/i18n'
 
+const OSX = global.process.platform === 'darwin'
+const hotkey = (OSX ? i18n.__('Command(⌘)') : i18n.__('Ctrl(^)')) + '+B'
 const FullscreenButton = ({
  onClick
 }) => (
   
 )
 
diff --git a/browser/main/Detail/InfoPanel.styl b/browser/main/Detail/InfoPanel.styl
index ac91bbec..2a73ca7e 100644
--- a/browser/main/Detail/InfoPanel.styl
+++ b/browser/main/Detail/InfoPanel.styl
@@ -33,6 +33,7 @@
 .control-infoButton-panel-trash
   z-index 200
   margin-top 0px
+  top 50px
   right 0px
   position absolute
   padding 20px 25px 0 25px
diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js
index c65f1425..652d1f53 100644
--- a/browser/main/Detail/SnippetNoteDetail.js
+++ b/browser/main/Detail/SnippetNoteDetail.js
@@ -32,7 +32,7 @@ import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
 
 const electron = require('electron')
 const { remote } = electron
-const { Menu, MenuItem, dialog } = remote
+const { dialog } = remote
 
 class SnippetNoteDetail extends React.Component {
   constructor (props) {
@@ -441,7 +441,7 @@ class SnippetNoteDetail extends React.Component {
           const isSuper = global.process.platform === 'darwin'
             ? e.metaKey
             : e.ctrlKey
-          if (isSuper) {
+          if (isSuper && !e.shiftKey) {
             e.preventDefault()
             this.addSnippet()
           }
@@ -451,14 +451,14 @@ class SnippetNoteDetail extends React.Component {
   }
 
   handleModeButtonClick (e, index) {
-    const menu = new Menu()
+    const templetes = []
     CodeMirror.modeInfo.sort(function (a, b) { return a.name.localeCompare(b.name) }).forEach((mode) => {
-      menu.append(new MenuItem({
+      templetes.push({
         label: mode.name,
         click: (e) => this.handleModeOptionClick(index, mode.name)(e)
-      }))
+      })
     })
-    menu.popup(remote.getCurrentWindow())
+    context.popup(templetes)
   }
 
   handleIndentTypeButtonClick (e) {
diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js
index d14c7a8c..e251dd42 100644
--- a/browser/main/Detail/TagSelect.js
+++ b/browser/main/Detail/TagSelect.js
@@ -5,6 +5,7 @@ import styles from './TagSelect.styl'
 import _ from 'lodash'
 import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
 import i18n from 'browser/lib/i18n'
+import ee from 'browser/main/lib/eventEmitter'
 
 class TagSelect extends React.Component {
   constructor (props) {
@@ -13,16 +14,26 @@ class TagSelect extends React.Component {
     this.state = {
       newTag: ''
     }
+    this.addtagHandler = this.handleAddTag.bind(this)
   }
 
   componentDidMount () {
     this.value = this.props.value
+    ee.on('editor:add-tag', this.addtagHandler)
   }
 
   componentDidUpdate () {
     this.value = this.props.value
   }
 
+  componentWillUnmount () {
+    ee.off('editor:add-tag', this.addtagHandler)
+  }
+
+  handleAddTag () {
+    this.refs.newTag.focus()
+  }
+
   handleNewTagInputKeyDown (e) {
     switch (e.keyCode) {
       case 9:
diff --git a/browser/main/Detail/index.js b/browser/main/Detail/index.js
index 2c451085..b6b6ef14 100644
--- a/browser/main/Detail/index.js
+++ b/browser/main/Detail/index.js
@@ -9,6 +9,7 @@ import ee from 'browser/main/lib/eventEmitter'
 import StatusBar from '../StatusBar'
 import i18n from 'browser/lib/i18n'
 import debounceRender from 'react-debounce-render'
+import searchFromNotes from 'browser/lib/search'
 
 const OSX = global.process.platform === 'darwin'
 
@@ -35,11 +36,38 @@ class Detail extends React.Component {
   }
 
   render () {
-    const { location, data, config } = this.props
+    const { location, data, params, config } = this.props
     let note = null
+
     if (location.query.key != null) {
       const noteKey = location.query.key
-      note = data.noteMap.get(noteKey)
+      const allNotes = data.noteMap.map(note => note)
+      const trashedNotes = data.trashedSet.toJS().map(uniqueKey => data.noteMap.get(uniqueKey))
+      let displayedNotes = allNotes
+
+      if (location.pathname.match(/\/searched/)) {
+        const searchStr = params.searchword
+        displayedNotes = searchStr === undefined || searchStr === '' ? allNotes
+          : searchFromNotes(allNotes, searchStr)
+      }
+
+      if (location.pathname.match(/\/tags/)) {
+        const listOfTags = params.tagname.split(' ')
+        displayedNotes = data.noteMap.map(note => note).filter(note =>
+          listOfTags.every(tag => note.tags.includes(tag))
+        )
+      }
+
+      if (location.pathname.match(/\/trashed/)) {
+        displayedNotes = trashedNotes
+      } else {
+        displayedNotes = _.differenceWith(displayedNotes, trashedNotes, (note, trashed) => note.key === trashed.key)
+      }
+
+      const noteKeys = displayedNotes.map(note => note.key)
+      if (noteKeys.includes(noteKey)) {
+        note = data.noteMap.get(noteKey)
+      }
     }
 
     if (note == null) {
diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js
index 3626130d..eeb16a5f 100644
--- a/browser/main/NoteList/index.js
+++ b/browser/main/NoteList/index.js
@@ -21,9 +21,10 @@ import AwsMobileAnalyticsConfig from 'browser/main/lib/AwsMobileAnalyticsConfig'
 import Markdown from '../../lib/markdown'
 import i18n from 'browser/lib/i18n'
 import { confirmDeleteNote } from 'browser/lib/confirmDeleteNote'
+import context from 'browser/lib/context'
 
 const { remote } = require('electron')
-const { Menu, MenuItem, dialog } = remote
+const { dialog } = remote
 const WP_POST_PATH = '/wp/v2/posts'
 
 function sortByCreatedAt (a, b) {
@@ -491,55 +492,51 @@ class NoteList extends React.Component {
     const updateLabel = i18n.__('Update Blog')
     const openBlogLabel = i18n.__('Open Blog')
 
-    const menu = new Menu()
+    const templates = []
 
     if (location.pathname.match(/\/trash/)) {
-      menu.append(new MenuItem({
+      templates.push({
         label: restoreNote,
         click: this.restoreNote
-      }))
-      menu.append(new MenuItem({
+      }, {
         label: deleteLabel,
         click: this.deleteNote
-      }))
+      })
     } else {
       if (!location.pathname.match(/\/starred/)) {
-        menu.append(new MenuItem({
+        templates.push({
           label: pinLabel,
           click: this.pinToTop
-        }))
+        })
       }
-      menu.append(new MenuItem({
+      templates.push({
         label: deleteLabel,
         click: this.deleteNote
-      }))
-      menu.append(new MenuItem({
+      }, {
         label: cloneNote,
         click: this.cloneNote.bind(this)
-      }))
-      menu.append(new MenuItem({
+      }, {
         label: copyNoteLink,
         click: this.copyNoteLink(note)
-      }))
+      })
       if (note.type === 'MARKDOWN_NOTE') {
         if (note.blog && note.blog.blogLink && note.blog.blogId) {
-          menu.append(new MenuItem({
+          templates.push({
             label: updateLabel,
             click: this.publishMarkdown.bind(this)
-          }))
-          menu.append(new MenuItem({
+          }, {
             label: openBlogLabel,
             click: () => this.openBlog.bind(this)(note)
-          }))
+          })
         } else {
-          menu.append(new MenuItem({
+          templates.push({
             label: publishLabel,
             click: this.publishMarkdown.bind(this)
-          }))
+          })
         }
       }
     }
-    menu.popup()
+    context.popup(templates)
   }
 
   updateSelectedNotes (updateFunc, cleanSelection = true) {
diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js
index 93e9157f..d72f0a8f 100644
--- a/browser/main/SideNav/StorageItem.js
+++ b/browser/main/SideNav/StorageItem.js
@@ -11,9 +11,10 @@ import StorageItemChild from 'browser/components/StorageItem'
 import _ from 'lodash'
 import { SortableElement } from 'react-sortable-hoc'
 import i18n from 'browser/lib/i18n'
+import context from 'browser/lib/context'
 
 const { remote } = require('electron')
-const { Menu, dialog } = remote
+const { dialog } = remote
 const escapeStringRegexp = require('escape-string-regexp')
 const path = require('path')
 
@@ -21,13 +22,15 @@ class StorageItem extends React.Component {
   constructor (props) {
     super(props)
 
+    const { storage } = this.props
+
     this.state = {
-      isOpen: true
+      isOpen: !!storage.isOpen
     }
   }
 
   handleHeaderContextMenu (e) {
-    const menu = Menu.buildFromTemplate([
+    context.popup([
       {
         label: i18n.__('Add Folder'),
         click: (e) => this.handleAddFolderButtonClick(e)
@@ -40,8 +43,6 @@ class StorageItem extends React.Component {
         click: (e) => this.handleUnlinkStorageClick(e)
       }
     ])
-
-    menu.popup()
   }
 
   handleUnlinkStorageClick (e) {
@@ -68,8 +69,18 @@ class StorageItem extends React.Component {
   }
 
   handleToggleButtonClick (e) {
+    const { storage, dispatch } = this.props
+    const isOpen = !this.state.isOpen
+    dataApi.toggleStorage(storage.key, isOpen)
+      .then((storage) => {
+        dispatch({
+          type: 'EXPAND_STORAGE',
+          storage,
+          isOpen
+        })
+      })
     this.setState({
-      isOpen: !this.state.isOpen
+      isOpen: isOpen
     })
   }
 
@@ -94,7 +105,7 @@ class StorageItem extends React.Component {
   }
 
   handleFolderButtonContextMenu (e, folder) {
-    const menu = Menu.buildFromTemplate([
+    context.popup([
       {
         label: i18n.__('Rename Folder'),
         click: (e) => this.handleRenameFolderClick(e, folder)
@@ -123,8 +134,6 @@ class StorageItem extends React.Component {
         click: (e) => this.handleFolderDeleteClick(e, folder)
       }
     ])
-
-    menu.popup()
   }
 
   handleRenameFolderClick (e, folder) {
diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js
index 67adf700..c4fa417b 100644
--- a/browser/main/SideNav/index.js
+++ b/browser/main/SideNav/index.js
@@ -1,8 +1,6 @@
 import PropTypes from 'prop-types'
 import React from 'react'
 import CSSModules from 'browser/lib/CSSModules'
-const { remote } = require('electron')
-const { Menu } = remote
 import dataApi from 'browser/main/lib/dataApi'
 import styles from './SideNav.styl'
 import { openModal } from 'browser/main/lib/modal'
@@ -19,6 +17,7 @@ import ListButton from './ListButton'
 import TagButton from './TagButton'
 import {SortableContainer} from 'react-sortable-hoc'
 import i18n from 'browser/lib/i18n'
+import context from 'browser/lib/context'
 
 class SideNav extends React.Component {
   // TODO: should not use electron stuff v0.7
@@ -254,10 +253,9 @@ class SideNav extends React.Component {
   handleFilterButtonContextMenu (event) {
     const { data } = this.props
     const trashedNotes = data.trashedSet.toJS().map((uniqueKey) => data.noteMap.get(uniqueKey))
-    const menu = Menu.buildFromTemplate([
+    context.popup([
       { label: i18n.__('Empty Trash'), click: () => this.emptyTrash(trashedNotes) }
     ])
-    menu.popup()
   }
 
   render () {
diff --git a/browser/main/StatusBar/index.js b/browser/main/StatusBar/index.js
index e5f5ae1a..8b48e3d3 100644
--- a/browser/main/StatusBar/index.js
+++ b/browser/main/StatusBar/index.js
@@ -4,10 +4,11 @@ import CSSModules from 'browser/lib/CSSModules'
 import styles from './StatusBar.styl'
 import ZoomManager from 'browser/main/lib/ZoomManager'
 import i18n from 'browser/lib/i18n'
+import context from 'browser/lib/context'
 
 const electron = require('electron')
 const { remote, ipcRenderer } = electron
-const { Menu, MenuItem, dialog } = remote
+const { dialog } = remote
 
 const zoomOptions = [0.8, 0.9, 1, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2.0]
 
@@ -26,16 +27,16 @@ class StatusBar extends React.Component {
   }
 
   handleZoomButtonClick (e) {
-    const menu = new Menu()
+    const templates = []
 
     zoomOptions.forEach((zoom) => {
-      menu.append(new MenuItem({
+      templates.push({
         label: Math.floor(zoom * 100) + '%',
         click: () => this.handleZoomMenuItemClick(zoom)
-      }))
+      })
     })
 
-    menu.popup(remote.getCurrentWindow())
+    context.popup(templates)
   }
 
   handleZoomMenuItemClick (zoomFactor) {
diff --git a/browser/main/TopBar/index.js b/browser/main/TopBar/index.js
index ae4d9664..a5687ecb 100644
--- a/browser/main/TopBar/index.js
+++ b/browser/main/TopBar/index.js
@@ -156,8 +156,7 @@ class TopBar extends React.Component {
     if (this.state.isSearching) {
       el.blur()
     } else {
-      el.focus()
-      el.setSelectionRange(0, el.value.length)
+      el.select()
     }
   }
 
diff --git a/browser/main/global.styl b/browser/main/global.styl
index 7025163f..8f3216ef 100644
--- a/browser/main/global.styl
+++ b/browser/main/global.styl
@@ -15,6 +15,12 @@ body
   font-weight 200
   -webkit-font-smoothing antialiased
 
+::-webkit-scrollbar
+  width 12px
+
+::-webkit-scrollbar-thumb
+  background-color rgba(0, 0, 0, 0.15)
+
 button, input, select, textarea
   font-family DEFAULT_FONTS
 
@@ -85,9 +91,11 @@ modalBackColor = white
     absolute top left bottom right
     background-color modalBackColor
     z-index modalZIndex + 1
-    
+
 
 body[data-theme="dark"]
+  ::-webkit-scrollbar-thumb
+    background-color rgba(0, 0, 0, 0.3)
   .ModalBase
     .modalBack
       background-color $ui-dark-backgroundColor
@@ -128,6 +136,8 @@ body[data-theme="dark"]
   z-index modalZIndex + 5
 
 body[data-theme="solarized-dark"]
+  ::-webkit-scrollbar-thumb
+    background-color rgba(0, 0, 0, 0.3)
   .ModalBase
     .modalBack
       background-color $ui-solarized-dark-backgroundColor
@@ -135,9 +145,10 @@ body[data-theme="solarized-dark"]
     color: $ui-solarized-dark-text-color
 
 body[data-theme="monokai"]
+  ::-webkit-scrollbar-thumb
+    background-color rgba(0, 0, 0, 0.3)
   .ModalBase
     .modalBack
       background-color $ui-monokai-backgroundColor
   .sortableItemHelper
     color: $ui-monokai-text-color
-
diff --git a/browser/main/lib/dataApi/addStorage.js b/browser/main/lib/dataApi/addStorage.js
index 630c0bd3..bfd6698a 100644
--- a/browser/main/lib/dataApi/addStorage.js
+++ b/browser/main/lib/dataApi/addStorage.js
@@ -37,7 +37,8 @@ function addStorage (input) {
     key,
     name: input.name,
     type: input.type,
-    path: input.path
+    path: input.path,
+    isOpen: false
   }
 
   return Promise.resolve(newStorage)
@@ -48,7 +49,8 @@ function addStorage (input) {
         key: newStorage.key,
         type: newStorage.type,
         name: newStorage.name,
-        path: newStorage.path
+        path: newStorage.path,
+        isOpen: false
       })
 
       localStorage.setItem('storages', JSON.stringify(rawStorages))
diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js
index a4c420bd..d1e0ab62 100644
--- a/browser/main/lib/dataApi/attachmentManagement.js
+++ b/browser/main/lib/dataApi/attachmentManagement.js
@@ -10,6 +10,7 @@ import i18n from 'browser/lib/i18n'
 
 const STORAGE_FOLDER_PLACEHOLDER = ':storage'
 const DESTINATION_FOLDER = 'attachments'
+const PATH_SEPARATORS = escapeStringRegexp(path.posix.sep) + escapeStringRegexp(path.win32.sep)
 
 /**
  * @description
@@ -76,14 +77,14 @@ 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 markdownContent 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 !== []) {
+function migrateAttachments (markdownContent, storagePath, noteKey) {
+  if (noteKey !== undefined && sander.existsSync(path.join(storagePath, 'images'))) {
+    const attachments = getAttachmentsInMarkdownContent(markdownContent) || []
+    if (attachments.length) {
       createAttachmentDestinationFolder(storagePath, noteKey)
     }
     for (const attachment of attachments) {
@@ -106,7 +107,10 @@ function migrateAttachments (renderedHTML, storagePath, noteKey) {
  * @returns {String} postprocessed HTML in which all :storage references are mapped to the actual paths.
  */
 function fixLocalURLS (renderedHTML, storagePath) {
-  return renderedHTML.replace(new RegExp(mdurl.encode(path.sep), 'g'), path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
+  return renderedHTML.replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER + '.*?"', 'g'), function (match) {
+    var encodedPathSeparators = new RegExp(mdurl.encode(path.win32.sep) + '|' + mdurl.encode(path.posix.sep), 'g')
+    return match.replace(encodedPathSeparators, path.sep).replace(new RegExp('/?' + STORAGE_FOLDER_PLACEHOLDER, 'g'), 'file:///' + path.join(storagePath, DESTINATION_FOLDER))
+  })
 }
 
 /**
@@ -186,13 +190,13 @@ function handlePastImageEvent (codeEditor, storageKey, noteKey, dataTransferItem
 }
 
 /**
- * @description Returns all attachment paths of the given markdown
- * @param {String} markdownContent content in which the attachment paths should be found
- * @returns {String[]} Array of the relative paths (starting with :storage) of the attachments of the given markdown
- */
-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')
+* @description Returns all attachment paths of the given markdown
+* @param {String} markdownContent content in which the attachment paths should be found
+* @returns {String[]} Array of the relative paths (starting with :storage) of the attachments of the given markdown
+*/
+function getAttachmentsInMarkdownContent (markdownContent) {
+  const preparedInput = markdownContent.replace(new RegExp('[' + PATH_SEPARATORS + ']', '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')
   return preparedInput.match(regexp)
 }
 
@@ -203,7 +207,7 @@ function getAttachmentsInContent (markdownContent) {
  * @returns {String[]} Absolute paths of the referenced attachments
  */
 function getAbsolutePathsOfAttachmentsInContent (markdownContent, storagePath) {
-  const temp = getAttachmentsInContent(markdownContent) || []
+  const temp = getAttachmentsInMarkdownContent(markdownContent) || []
   const result = []
   for (const relativePath of temp) {
     result.push(relativePath.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER, 'g'), path.join(storagePath, DESTINATION_FOLDER)))
@@ -239,7 +243,8 @@ function moveAttachments (oldPath, newPath, noteKey, newNoteKey, noteContent) {
  */
 function replaceNoteKeyWithNewNoteKey (noteContent, oldNoteKey, newNoteKey) {
   if (noteContent) {
-    return noteContent.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey, 'g'), path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey))
+    const preparedInput = noteContent.replace(new RegExp('[' + PATH_SEPARATORS + ']', 'g'), path.sep)
+    return preparedInput.replace(new RegExp(STORAGE_FOLDER_PLACEHOLDER + escapeStringRegexp(path.sep) + oldNoteKey, 'g'), path.join(STORAGE_FOLDER_PLACEHOLDER, newNoteKey))
   }
   return noteContent
 }
@@ -277,7 +282,7 @@ function deleteAttachmentsNotPresentInNote (markdownContent, storageKey, noteKey
   }
   const targetStorage = findStorage.findStorage(storageKey)
   const attachmentFolder = path.join(targetStorage.path, DESTINATION_FOLDER, noteKey)
-  const attachmentsInNote = getAttachmentsInContent(markdownContent)
+  const attachmentsInNote = getAttachmentsInMarkdownContent(markdownContent)
   const attachmentsInNoteOnlyFileNames = []
   if (attachmentsInNote) {
     for (let i = 0; i < attachmentsInNote.length; i++) {
@@ -348,7 +353,7 @@ function generateFileNotFoundMarkdown () {
  */
 function isAttachmentLink (text) {
   if (text) {
-    return text.match(new RegExp('.*\\[.*\\]\\( *' + escapeStringRegexp(STORAGE_FOLDER_PLACEHOLDER) + escapeStringRegexp(path.sep) + '.*\\).*', 'gi')) != null
+    return text.match(new RegExp('.*\\[.*\\]\\( *' + escapeStringRegexp(STORAGE_FOLDER_PLACEHOLDER) + '[' + PATH_SEPARATORS + ']' + '.*\\).*', 'gi')) != null
   }
   return false
 }
@@ -364,7 +369,7 @@ function isAttachmentLink (text) {
 function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
   if (storageKey != null && noteKey != null && linkText != null) {
     const storagePath = findStorage.findStorage(storageKey).path
-    const attachments = getAttachmentsInContent(linkText) || []
+    const attachments = getAttachmentsInMarkdownContent(linkText) || []
     const replaceInstructions = []
     const copies = []
     for (const attachment of attachments) {
@@ -373,13 +378,13 @@ function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
         sander.exists(absPathOfAttachment)
           .then((fileExists) => {
             if (!fileExists) {
-              const fileNotFoundRegexp = new RegExp('!?' + escapeStringRegexp('[') + '[\\w|\\d|\\s|\\.]*\\]\\(\\s*' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + escapeStringRegexp(path.sep) + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + escapeStringRegexp(')'))
+              const fileNotFoundRegexp = new RegExp('!?' + escapeStringRegexp('[') + '[\\w|\\d|\\s|\\.]*\\]\\(\\s*' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + PATH_SEPARATORS + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + escapeStringRegexp(')'))
               replaceInstructions.push({regexp: fileNotFoundRegexp, replacement: this.generateFileNotFoundMarkdown()})
               return Promise.resolve()
             }
             return this.copyAttachment(absPathOfAttachment, storageKey, noteKey)
               .then((fileName) => {
-                const replaceLinkRegExp = new RegExp(escapeStringRegexp('(') + ' *' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + escapeStringRegexp(path.sep) + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + ' *' + escapeStringRegexp(')'))
+                const replaceLinkRegExp = new RegExp(escapeStringRegexp('(') + ' *' + STORAGE_FOLDER_PLACEHOLDER + '[\\w|\\d|\\-|' + PATH_SEPARATORS + ']*' + escapeStringRegexp(path.basename(absPathOfAttachment)) + ' *' + escapeStringRegexp(')'))
                 replaceInstructions.push({
                   regexp: replaceLinkRegExp,
                   replacement: '(' + path.join(STORAGE_FOLDER_PLACEHOLDER, noteKey, fileName) + ')'
@@ -408,7 +413,7 @@ module.exports = {
   generateAttachmentMarkdown,
   handleAttachmentDrop,
   handlePastImageEvent,
-  getAttachmentsInContent,
+  getAttachmentsInMarkdownContent,
   getAbsolutePathsOfAttachmentsInContent,
   removeStorageAndNoteReferences,
   deleteAttachmentFolder,
diff --git a/browser/main/lib/dataApi/index.js b/browser/main/lib/dataApi/index.js
index 7c57e016..4e2f0061 100644
--- a/browser/main/lib/dataApi/index.js
+++ b/browser/main/lib/dataApi/index.js
@@ -1,5 +1,6 @@
 const dataApi = {
   init: require('./init'),
+  toggleStorage: require('./toggleStorage'),
   addStorage: require('./addStorage'),
   renameStorage: require('./renameStorage'),
   removeStorage: require('./removeStorage'),
diff --git a/browser/main/lib/dataApi/resolveStorageData.js b/browser/main/lib/dataApi/resolveStorageData.js
index af040c5d..681a102e 100644
--- a/browser/main/lib/dataApi/resolveStorageData.js
+++ b/browser/main/lib/dataApi/resolveStorageData.js
@@ -8,7 +8,8 @@ function resolveStorageData (storageCache) {
     key: storageCache.key,
     name: storageCache.name,
     type: storageCache.type,
-    path: storageCache.path
+    path: storageCache.path,
+    isOpen: storageCache.isOpen
   }
 
   const boostnoteJSONPath = path.join(storageCache.path, 'boostnote.json')
diff --git a/browser/main/lib/dataApi/toggleStorage.js b/browser/main/lib/dataApi/toggleStorage.js
new file mode 100644
index 00000000..dbb625c3
--- /dev/null
+++ b/browser/main/lib/dataApi/toggleStorage.js
@@ -0,0 +1,28 @@
+const _ = require('lodash')
+const resolveStorageData = require('./resolveStorageData')
+
+/**
+ * @param {String} key
+ * @param {Boolean} isOpen
+ * @return {Object} Storage meta data
+ */
+function toggleStorage (key, isOpen) {
+  let cachedStorageList
+  try {
+    cachedStorageList = JSON.parse(localStorage.getItem('storages'))
+    if (!_.isArray(cachedStorageList)) throw new Error('invalid storages')
+  } catch (err) {
+    console.log('error got')
+    console.error(err)
+    return Promise.reject(err)
+  }
+  const targetStorage = _.find(cachedStorageList, {key: key})
+  if (targetStorage == null) return Promise.reject('Storage')
+
+  targetStorage.isOpen = isOpen
+  localStorage.setItem('storages', JSON.stringify(cachedStorageList))
+
+  return resolveStorageData(targetStorage)
+}
+
+module.exports = toggleStorage
diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js
index 185004e7..b748587c 100644
--- a/browser/main/modals/NewNoteModal.js
+++ b/browser/main/modals/NewNoteModal.js
@@ -12,8 +12,7 @@ class NewNoteModal extends React.Component {
   constructor (props) {
     super(props)
 
-    this.state = {
-    }
+    this.state = {}
   }
 
   componentDidMount () {
@@ -35,19 +34,20 @@ class NewNoteModal extends React.Component {
         title: '',
         content: ''
       })
-      .then((note) => {
+      .then(note => {
         const noteHash = note.key
         dispatch({
           type: 'UPDATE_NOTE',
           note: note
         })
+
         hashHistory.push({
           pathname: location.pathname,
-          query: {key: noteHash}
+          query: { key: noteHash }
         })
         ee.emit('list:jump', noteHash)
         ee.emit('detail:focus')
-        this.props.close()
+        setTimeout(this.props.close, 200)
       })
   }
 
@@ -69,13 +69,15 @@ class NewNoteModal extends React.Component {
         folder: folder,
         title: '',
         description: '',
-        snippets: [{
-          name: '',
-          mode: 'text',
-          content: ''
-        }]
+        snippets: [
+          {
+            name: '',
+            mode: 'text',
+            content: ''
+          }
+        ]
       })
-      .then((note) => {
+      .then(note => {
         const noteHash = note.key
         dispatch({
           type: 'UPDATE_NOTE',
@@ -83,11 +85,11 @@ class NewNoteModal extends React.Component {
         })
         hashHistory.push({
           pathname: location.pathname,
-          query: {key: noteHash}
+          query: { key: noteHash }
         })
         ee.emit('list:jump', noteHash)
         ee.emit('detail:focus')
-        this.props.close()
+        setTimeout(this.props.close, 200)
       })
   }
 
@@ -106,49 +108,65 @@ class NewNoteModal extends React.Component {
 
   render () {
     return (
-      
this.handleKeyDown(e)} + onKeyDown={e => this.handleKeyDown(e)} >
{i18n.__('Make a note')}
- this.handleCloseButtonClick(e)} /> + this.handleCloseButtonClick(e)} + />
- -
-
{i18n.__('Tab to switch format')}
+
+ {i18n.__('Tab to switch format')} +
) } } -NewNoteModal.propTypes = { -} +NewNoteModal.propTypes = {} export default CSSModules(NewNoteModal, styles) diff --git a/browser/main/modals/PreferencesModal/SnippetEditor.js b/browser/main/modals/PreferencesModal/SnippetEditor.js index f0e93dec..4ce5dc34 100644 --- a/browser/main/modals/PreferencesModal/SnippetEditor.js +++ b/browser/main/modals/PreferencesModal/SnippetEditor.js @@ -27,7 +27,12 @@ class SnippetEditor extends React.Component { dragDrop: false, foldGutter: true, gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], - autoCloseBrackets: true, + autoCloseBrackets: { + pairs: '()[]{}\'\'""$$**``', + triples: '```"""\'\'\'', + explode: '[]{}``$$', + override: true + }, mode: 'null' }) this.cm.setSize('100%', '100%') diff --git a/browser/main/modals/PreferencesModal/SnippetList.js b/browser/main/modals/PreferencesModal/SnippetList.js index 3cf28cf6..3e892f97 100644 --- a/browser/main/modals/PreferencesModal/SnippetList.js +++ b/browser/main/modals/PreferencesModal/SnippetList.js @@ -4,8 +4,7 @@ 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 +import context from 'browser/lib/context' class SnippetList extends React.Component { constructor (props) { @@ -21,18 +20,17 @@ class SnippetList extends React.Component { } reloadSnippetList () { - dataApi.fetchSnippet().then(snippets => this.setState({snippets})) + dataApi.fetchSnippet().then(snippets => { + this.setState({snippets}) + this.props.onSnippetSelect(snippets[0]) + }) } handleSnippetContextMenu (snippet) { - const menu = new Menu() - menu.append(new MenuItem({ + context.popup([{ label: i18n.__('Delete snippet'), - click: () => { - this.deleteSnippet(snippet) - } - })) - menu.popup() + click: () => this.deleteSnippet(snippet) + }]) } deleteSnippet (snippet) { @@ -43,7 +41,7 @@ class SnippetList extends React.Component { } handleSnippetClick (snippet) { - this.props.onSnippetClick(snippet) + this.props.onSnippetSelect(snippet) } createSnippet () { @@ -55,6 +53,16 @@ class SnippetList extends React.Component { }).catch(err => { throw err }) } + defineSnippetStyleName (snippet) { + const { currentSnippet } = this.props + if (currentSnippet == null) return + if (currentSnippet.id === snippet.id) { + return 'snippet-item-selected' + } else { + return 'snippet-item' + } + } + render () { const { snippets } = this.state return ( @@ -70,7 +78,7 @@ class SnippetList extends React.Component { { snippets.map((snippet) => (
  • this.handleSnippetContextMenu(snippet)} onClick={() => this.handleSnippetClick(snippet)}> diff --git a/browser/main/modals/PreferencesModal/SnippetTab.js b/browser/main/modals/PreferencesModal/SnippetTab.js index 67e9ace6..e35ecd69 100644 --- a/browser/main/modals/PreferencesModal/SnippetTab.js +++ b/browser/main/modals/PreferencesModal/SnippetTab.js @@ -25,7 +25,7 @@ class SnippetTab extends React.Component { }, 500) } - handleSnippetClick (snippet) { + handleSnippetSelect (snippet) { const { currentSnippet } = this.state if (currentSnippet === null || currentSnippet.id !== snippet.id) { dataApi.fetchSnippet(snippet.id).then(changedSnippet => { @@ -66,8 +66,9 @@ class SnippetTab extends React.Component {
    {i18n.__('Snippets')}
    + onSnippetSelect={this.handleSnippetSelect.bind(this)} + onSnippetDeleted={this.handleDeleteSnippet.bind(this)} + currentSnippet={currentSnippet} />
    {i18n.__('Snippet name')}
    diff --git a/browser/main/modals/PreferencesModal/SnippetTab.styl b/browser/main/modals/PreferencesModal/SnippetTab.styl index 118c56ed..02307b64 100644 --- a/browser/main/modals/PreferencesModal/SnippetTab.styl +++ b/browser/main/modals/PreferencesModal/SnippetTab.styl @@ -122,6 +122,10 @@ &:hover background darken(#f5f5f5, 5) + .snippet-item-selected + @extend .snippet-list .snippet-item + background darken(#f5f5f5, 5) + .snippet-detail width 70% height calc(100% - 200px) @@ -142,6 +146,8 @@ body[data-theme="default"], body[data-theme="white"] background $ui-borderColor &:hover background darken($ui-backgroundColor, 5) + .snippet-item-selected + background darken($ui-backgroundColor, 5) body[data-theme="dark"] .snippets @@ -152,8 +158,12 @@ body[data-theme="dark"] background $ui-dark-borderColor &:hover background darken($ui-dark-backgroundColor, 5) + .snippet-item-selected + background darken($ui-dark-backgroundColor, 5) .snippet-detail color white + .group-control-button + colorDarkPrimaryButton() body[data-theme="solarized-dark"] .snippets @@ -164,8 +174,12 @@ body[data-theme="solarized-dark"] background $ui-solarized-dark-borderColor &:hover background darken($ui-solarized-dark-backgroundColor, 5) + .snippet-item-selected + background darken($ui-solarized-dark-backgroundColor, 5) .snippet-detail color white + .group-control-button + colorSolarizedDarkPrimaryButton() body[data-theme="monokai"] .snippets @@ -176,5 +190,9 @@ body[data-theme="monokai"] background $ui-monokai-borderColor &:hover background darken($ui-monokai-backgroundColor, 5) + .snippet-item-selected + background darken($ui-monokai-backgroundColor, 5) .snippet-detail color white + .group-control-button + colorMonokaiPrimaryButton() diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index ce149f65..aa3568e7 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -11,6 +11,7 @@ import 'codemirror-mode-elixir' import _ from 'lodash' import i18n from 'browser/lib/i18n' import { getLanguages } from 'browser/lib/Languages' +import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' const OSX = global.process.platform === 'darwin' @@ -164,7 +165,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 + const fontFamily = normalizeEditorFontFamily(config.editor.fontFamily) return (
    @@ -262,8 +263,16 @@ class UiTab extends React.Component { }) } -
    - (this.codeMirrorInstance = e)} value={codemirrorSampleCode} options={{ lineNumbers: true, readOnly: true, mode: 'javascript', theme: codemirrorTheme }} /> +
    + (this.codeMirrorInstance = e)} + value={codemirrorSampleCode} + options={{ + lineNumbers: true, + readOnly: true, + mode: 'javascript', + theme: codemirrorTheme + }} />
    @@ -596,7 +605,19 @@ class UiTab extends React.Component { type='checkbox' />  {i18n.__('Allow custom CSS for preview')} - this.handleUIChange(e)} ref={e => (this.customCSSCM = e)} value={config.preview.customCSS} options={{ lineNumbers: true, mode: 'css', theme: codemirrorTheme }} /> +
    + this.handleUIChange(e)} + ref={e => (this.customCSSCM = e)} + value={config.preview.customCSS} + options={{ + lineNumbers: true, + mode: 'css', + theme: codemirrorTheme + }} /> +
    diff --git a/browser/main/store.js b/browser/main/store.js index 7ea6decb..a1b6b791 100644 --- a/browser/main/store.js +++ b/browser/main/store.js @@ -360,6 +360,12 @@ function data (state = defaultDataMap(), action) { state.storageMap = new Map(state.storageMap) state.storageMap.set(action.storage.key, action.storage) return state + case 'EXPAND_STORAGE': + state = Object.assign({}, state) + state.storageMap = new Map(state.storageMap) + action.storage.isOpen = action.isOpen + state.storageMap.set(action.storage.key, action.storage) + return state } return state } diff --git a/docs/jp/build.md b/docs/jp/build.md index 4d0fab33..b8d2e414 100644 --- a/docs/jp/build.md +++ b/docs/jp/build.md @@ -1,9 +1,16 @@ # Build +## 環境 +* npm: 4.x +* node: 7.x + +`npm v5.x` だと `$ grunt pre-build` が失敗するので、 `npm v4.x` を使用してください。 + ## 開発 Webpack HRMを使います。 -次の命令から私達がしておいた設定を使うことができます。 +Boostnoteの最上位ディレクトリにて以下のコマンドを実行して、 +デフォルトの設定の開発環境を起動させます。 依存するパッケージをインストールします。 @@ -27,15 +34,15 @@ $ yarn run dev-start > ### 注意 > 時々、直接リフレッシュをする必要があります。 -> 1. コンポネントのコンストラクター関数を編集する場合 -> 2. 新しいCSSクラスを追加する場合(1.の理由と同じ: CSSクラス名はコンポネントごとに書きなおされまが、この作業はコンストラクターで行われます。) +> 1. コンポーネントのコンストラクタ関数を編集する場合 +> 2. 新しいCSSクラスを追加する場合(1.の理由と同じ: CSSクラス名はコンポーネントごとに書きなおされますが、この作業はコンストラクタで行われます。) ## 配布 Gruntを使います。 -実際の配布は`grunt`で実行できます。しかし、これにはCodesignとAuthenticodeの仮定が含まれるので、使っては行けないです。 +実際の配布は`grunt`で実行できます。しかし、これにはCodesignとAuthenticodeを実行するタスクが含まれるので、使用しないでください。 -それで、実行ファイルを作るスクリプトを用意しておきました。 +代わりに、実行ファイルを作るスクリプトを用意しておきました。 このビルドはnpm v5.3.0では動かないのでv5.2.0で動かす必要があります。 diff --git a/lib/main-menu.js b/lib/main-menu.js index 9345bd67..cda964c5 100644 --- a/lib/main-menu.js +++ b/lib/main-menu.js @@ -136,6 +136,15 @@ const file = { { type: 'separator' }, + { + label: 'Format Table', + click () { + mainWindow.webContents.send('code:format-table') + } + }, + { + type: 'separator' + }, { label: 'Print', accelerator: 'CommandOrControl+P', @@ -209,6 +218,16 @@ const edit = { label: 'Select All', accelerator: 'Command+A', selector: 'selectAll:' + }, + { + type: 'separator' + }, + { + label: 'Add Tag', + accelerator: 'CommandOrControl+Shift+T', + click () { + mainWindow.webContents.send('editor:add-tag') + } } ] } @@ -235,14 +254,14 @@ const view = { }, { label: 'Next Note', - accelerator: 'Control+J', + accelerator: 'CommandOrControl+]', click () { mainWindow.webContents.send('list:next') } }, { label: 'Previous Note', - accelerator: 'Control+K', + accelerator: 'CommandOrControl+[', click () { mainWindow.webContents.send('list:prior') } @@ -267,6 +286,19 @@ const view = { mainWindow.setFullScreen(!mainWindow.isFullScreen()) } }, + { + type: 'separator' + }, + { + label: 'Toggle Side Bar', + accelerator: 'CommandOrControl+B', + click () { + mainWindow.webContents.send('editor:fullscreen') + } + }, + { + type: 'separator' + }, { role: 'zoomin', accelerator: macOS ? 'CommandOrControl+Plus' : 'Control+=' diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 8150228b..00000000 --- a/package-lock.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "boost", - "version": "0.10.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "requires": { - "ms": "2.0.0" - } - }, - "i18n-2": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/i18n-2/-/i18n-2-0.7.2.tgz", - "integrity": "sha512-Rdh6vfpNhL7q61cNf27x7QGULTi1TcGLVdFb5OJ6dOiJo+EkOTqEg0+3xgyeEMgYhopUBsh2IiSkFkjM+EhEmA==", - "requires": { - "debug": "3.1.0", - "sprintf": "0.1.5" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "sprintf": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/sprintf/-/sprintf-0.1.5.tgz", - "integrity": "sha1-j4PjmpMXwaUCy324BQ5Rxnn27c8=" - } - } -} diff --git a/package.json b/package.json index 28f1239b..fbbb025f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "boost", "productName": "Boostnote", - "version": "0.11.6", + "version": "0.11.8", "main": "index.js", "description": "Boostnote", "license": "GPL-3.0", @@ -51,9 +51,11 @@ "dependencies": { "@rokt33r/markdown-it-math": "^4.0.1", "@rokt33r/season": "^5.3.0", + "@susisu/mte-kernel": "^2.0.0", "aws-sdk": "^2.48.0", "aws-sdk-mobile-analytics": "^0.9.2", - "codemirror": "^5.37.0", + "chart.js": "^2.7.2", + "codemirror": "^5.39.0", "codemirror-mode-elixir": "^1.1.1", "electron-config": "^0.2.1", "electron-gh-releases": "^2.0.2", @@ -71,7 +73,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-admonition": "^1.0.4", "markdown-it-emoji": "^1.1.1", "markdown-it-footnote": "^3.0.0", "markdown-it-imsize": "^2.0.1", @@ -81,6 +83,7 @@ "markdown-it-plantuml": "^1.1.0", "markdown-it-smartarrows": "^1.0.1", "mdurl": "^1.0.1", + "mermaid": "^8.0.0-rc.8", "moment": "^2.10.3", "mousetrap": "^1.6.1", "mousetrap-global-bind": "^1.1.0", diff --git a/readme.md b/readme.md index 40866e46..c6674902 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -:mega: We've launched [Boostnote Bounty Program](http://bit.ly/2I5Tpik). +:mega: The Boostnote team launches [IssueHunt](https://issuehunt.io/) for sustainable open-source ecosystem. ![Boostnote app screenshot](./resources/repository/top.png) @@ -19,13 +19,15 @@ Thank you to all the people who already contributed to Boostnote! ## Supporting Boostnote -Boostnote is an open source project. It's an independent project with its ongoing development made possible entirely thanks to the support by these awesome [backers](https://github.com/BoostIO/Boostnote/blob/master/Backers.md). If you'd like to join them, please consider: -- [Become a backer or sponsor on Open Collective.](https://opencollective.com/boostnoteio) +Boostnote is an open source project. It's an independent project with its ongoing development made possible entirely thanks to the support by these awesome backers. + +Any issues on Boostnote can be funded by anyone and that money will be distributed to contributors and maintainers. If you'd like to join them, please consider: +- [Become a backer on IssueHunt](https://issuehunt.io/repos/53266139). ## 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/enQtMzcwNDU3NDU3ODI0LTU1ZDgwZDNiZTNmN2RhOTY4OTM5ODY0ODUzMTRiNmQ0ZDMzZDRiYzg2YmQ5ZDYzZTQxYjMxYzBlNTM4NjcyYjM) +- [Slack Group](https://join.slack.com/t/boostnote-group/shared_invite/enQtMzkxOTk4ODkyNzc0LThkNmMzY2VlZjVhYTNiYjE5YjQyZGVjNTJlYTY1OGMyZTFjNGU5YTUyYjUzOWZhYTU4OTVlNDYyNDFjYWMzNDM) - [Blog](https://boostlog.io/tags/boostnote) - [Reddit](https://www.reddit.com/r/Boostnote/) diff --git a/tests/dataApi/attachmentManagement.test.js b/tests/dataApi/attachmentManagement.test.js index 4ce031a7..a4cc8082 100644 --- a/tests/dataApi/attachmentManagement.test.js +++ b/tests/dataApi/attachmentManagement.test.js @@ -169,6 +169,43 @@ it('should replace the all ":storage" path with the actual storage path', functi expect(actual).toEqual(expectedOutput) }) +it('should replace the ":storage" path with the actual storage path when they have different path separators', function () { + const storageFolder = systemUnderTest.DESTINATION_FOLDER + const testInput = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

    Headline

    \n' + + '

    \n' + + ' dummyImage.png\n' + + '

    \n' + + '

    \n' + + ' dummyPDF.pdf\n' + + '

    \n' + + ' \n' + + '' + const storagePath = '<>' + const expectedOutput = + '\n' + + ' \n' + + ' //header\n' + + ' \n' + + ' \n' + + '

    Headline

    \n' + + '

    \n' + + ' dummyImage.png\n' + + '

    \n' + + '

    \n' + + ' dummyPDF.pdf\n' + + '

    \n' + + ' \n' + + '' + const actual = systemUnderTest.fixLocalURLS(testInput, storagePath) + expect(actual).toEqual(expectedOutput) +}) + it('should test that generateAttachmentMarkdown works correct both with previews and without', function () { const fileName = 'fileName' const path = 'path' @@ -180,27 +217,35 @@ it('should test that generateAttachmentMarkdown works correct both with previews expect(actual).toEqual(expected) }) -it('should test that getAttachmentsInContent finds all attachments', function () { - const testInput = - '\n' + - ' \n' + - ' //header\n' + - ' \n' + - ' \n' + - '

    Headline

    \n' + - '

    \n' + - ' dummyImage.png\n' + - '

    \n' + - '

    \n' + - ' dummyPDF.pdf\n' + - '

    \n' + - '

    \n' + - ' dummyImage2.jpg\n' + - '

    \n' + - ' \n' + - '' - const actual = systemUnderTest.getAttachmentsInContent(testInput) - const expected = [':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.6r4zdgc22xp.png', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.q2i4iw0fyx.pdf', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'd6c5ee92.jpg'] +it('should test that migrateAttachments work when they have different path separators', function () { + sander.existsSync = jest.fn(() => true) + const dummyStoragePath = 'dummyStoragePath' + const imagesPath = path.join(dummyStoragePath, 'images') + const attachmentsPath = path.join(dummyStoragePath, 'attachments') + const noteKey = 'noteKey' + const testInput = '"# Test\n' + + '\n' + + '![Screenshot1](:storage' + path.win32.sep + '0.3b88d0dc.png)\n' + + '![Screenshot2](:storage' + path.posix.sep + '0.2cb8875c.pdf)"' + + systemUnderTest.migrateAttachments(testInput, dummyStoragePath, noteKey) + + expect(sander.existsSync.mock.calls[0][0]).toBe(imagesPath) + expect(sander.existsSync.mock.calls[1][0]).toBe(path.join(imagesPath, '0.3b88d0dc.png')) + expect(sander.existsSync.mock.calls[2][0]).toBe(path.join(attachmentsPath, '0.3b88d0dc.png')) + expect(sander.existsSync.mock.calls[3][0]).toBe(path.join(imagesPath, '0.2cb8875c.pdf')) + expect(sander.existsSync.mock.calls[4][0]).toBe(path.join(attachmentsPath, '0.2cb8875c.pdf')) +}) + +it('should test that getAttachmentsInMarkdownContent finds all attachments when they have different path separators', function () { + const testInput = '"# Test\n' + + '\n' + + '![Screenshot1](:storage' + path.win32.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.win32.sep + '0.3b88d0dc.png)\n' + + '![Screenshot2](:storage' + path.posix.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.posix.sep + '2cb8875c.pdf)\n' + + '![Screenshot3](:storage' + path.win32.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.posix.sep + 'bbf49b02.jpg)"' + + const actual = systemUnderTest.getAttachmentsInMarkdownContent(testInput) + const expected = [':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '0.3b88d0dc.png', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + '2cb8875c.pdf', ':storage' + path.sep + '9c9c4ba3-bc1e-441f-9866-c1e9a806e31c' + path.sep + 'bbf49b02.jpg'] expect(actual).toEqual(expect.arrayContaining(expected)) }) @@ -274,6 +319,21 @@ it('should remove the all ":storage" and noteKey references', function () { expect(actual).toEqual(expectedOutput) }) +it('should make sure that "removeStorageAndNoteReferences" works with markdown content as well', function () { + const noteKey = 'noteKey' + const testInput = + 'Test input' + + '![' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + noteKey + path.sep + 'image.jpg](imageName}) \n' + + '[' + systemUnderTest.STORAGE_FOLDER_PLACEHOLDER + path.sep + noteKey + path.sep + 'pdf.pdf](pdf})' + + const expectedOutput = + 'Test input' + + '![' + systemUnderTest.DESTINATION_FOLDER + path.sep + 'image.jpg](imageName}) \n' + + '[' + systemUnderTest.DESTINATION_FOLDER + path.sep + 'pdf.pdf](pdf})' + const actual = systemUnderTest.removeStorageAndNoteReferences(testInput, noteKey) + expect(actual).toEqual(expectedOutput) +}) + it('should delete the correct attachment folder if a note is deleted', function () { const dummyStorage = {path: 'dummyStoragePath'} const storageKey = 'storageKey' diff --git a/tests/dataApi/toggleStorage-test.js b/tests/dataApi/toggleStorage-test.js new file mode 100644 index 00000000..5169a4f4 --- /dev/null +++ b/tests/dataApi/toggleStorage-test.js @@ -0,0 +1,38 @@ +const test = require('ava') +const toggleStorage = require('browser/main/lib/dataApi/toggleStorage') + +global.document = require('jsdom').jsdom('') +global.window = document.defaultView +global.navigator = window.navigator + +const Storage = require('dom-storage') +const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true }) +const path = require('path') +const _ = require('lodash') +const TestDummy = require('../fixtures/TestDummy') +const sander = require('sander') +const os = require('os') + +const storagePath = path.join(os.tmpdir(), 'test/toggle-storage') + +test.beforeEach((t) => { + t.context.storage = TestDummy.dummyStorage(storagePath) + localStorage.setItem('storages', JSON.stringify([t.context.storage.cache])) +}) + +test.serial('Toggle a storage location', (t) => { + const storageKey = t.context.storage.cache.key + return Promise.resolve() + .then(function doTest () { + return toggleStorage(storageKey, true) + }) + .then(function assert (data) { + const cachedStorageList = JSON.parse(localStorage.getItem('storages')) + t.true(_.find(cachedStorageList, {key: storageKey}).isOpen === true) + }) +}) + +test.after(function after () { + localStorage.clear() + sander.rimrafSync(storagePath) +}) diff --git a/tests/lib/escapeHtmlCharacters-test.js b/tests/lib/escapeHtmlCharacters-test.js new file mode 100644 index 00000000..672ef917 --- /dev/null +++ b/tests/lib/escapeHtmlCharacters-test.js @@ -0,0 +1,73 @@ +const { escapeHtmlCharacters } = require('browser/lib/utils') +const test = require('ava') + +test('escapeHtmlCharacters should return the original string if nothing needed to escape', t => { + const input = 'Nothing to be escaped' + const expected = 'Nothing to be escaped' + const actual = escapeHtmlCharacters(input) + t.is(actual, expected) +}) + +test('escapeHtmlCharacters should skip code block if that option is enabled', t => { + const input = ` +` + const expected = ` +<escapeMe>` + const actual = escapeHtmlCharacters(input, { detectCodeBlock: true }) + t.is(actual, expected) +}) + +test('escapeHtmlCharacters should NOT skip character not in code block but start with 4 spaces', t => { + const input = '4 spaces &' + const expected = '4 spaces &' + const actual = escapeHtmlCharacters(input, { detectCodeBlock: true }) + t.is(actual, expected) +}) + +test('escapeHtmlCharacters should NOT skip code block if that option is NOT enabled', t => { + const input = ` +` + const expected = ` <no escape> +<escapeMe>` + const actual = escapeHtmlCharacters(input) + t.is(actual, expected) +}) + +test("escapeHtmlCharacters should NOT escape & character if it's a part of an escaped character", t => { + const input = 'Do not escape & or " but do escape &' + const expected = 'Do not escape & or " but do escape &' + const actual = escapeHtmlCharacters(input) + t.is(actual, expected) +}) + +test('escapeHtmlCharacters should skip char if in code block', t => { + const input = ` +\`\`\` + +\`\`\` +dasdasd +dasdasdasd +\`\`\` + +\`\`\` +` + const expected = ` +\`\`\` + +\`\`\` +das<das>dasd +dasdasdasd +\`\`\` + +\`\`\` +` + const actual = escapeHtmlCharacters(input, { detectCodeBlock: true }) + t.is(actual, expected) +}) + +test('escapeHtmlCharacters should return the correct result', t => { + const input = '& < > " \'' + const expected = '& < > " '' + const actual = escapeHtmlCharacters(input) + t.is(actual, expected) +}) diff --git a/tests/lib/normalize-editor-font-family-test.js b/tests/lib/normalize-editor-font-family-test.js new file mode 100644 index 00000000..aacd03ac --- /dev/null +++ b/tests/lib/normalize-editor-font-family-test.js @@ -0,0 +1,16 @@ +/** + * @fileoverview Unit test for browser/lib/normalizeEditorFontFamily + */ +import test from 'ava' +import normalizeEditorFontFamily from '../../browser/lib/normalizeEditorFontFamily' +import consts from '../../browser/lib/consts' +const defaultEditorFontFamily = consts.DEFAULT_EDITOR_FONT_FAMILY + +test('normalizeEditorFontFamily() should return default font family (string[])', t => { + t.is(normalizeEditorFontFamily(), defaultEditorFontFamily.join(', ')) +}) + +test('normalizeEditorFontFamily(["hoge", "huga"]) should return default font family connected with arg.', t => { + const arg = 'font1, font2' + t.is(normalizeEditorFontFamily(arg), `${arg}, ${defaultEditorFontFamily.join(', ')}`) +}) diff --git a/yarn.lock b/yarn.lock index 7bf19d2b..f77252ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -79,6 +79,12 @@ fs-plus "2.x" optimist "~0.4.0" +"@susisu/mte-kernel@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@susisu/mte-kernel/-/mte-kernel-2.0.0.tgz#da3354d6a07ea27f36ec91d9fccf6bfa9010d00e" + dependencies: + meaw "^2.0.0" + "@types/node@^8.0.24": version "8.10.17" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.17.tgz#d48cf10f0dc6dcf59f827f5a3fc7a4a6004318d3" @@ -1574,6 +1580,26 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" +chart.js@^2.7.2: + version "2.7.2" + resolved "http://registry.npm.taobao.org/chart.js/download/chart.js-2.7.2.tgz#3c9fde4dc5b95608211bdefeda7e5d33dffa5714" + dependencies: + chartjs-color "^2.1.0" + moment "^2.10.2" + +chartjs-color-string@^0.5.0: + version "0.5.0" + resolved "http://registry.npm.taobao.org/chartjs-color-string/download/chartjs-color-string-0.5.0.tgz#8d3752d8581d86687c35bfe2cb80ac5213ceb8c1" + dependencies: + color-name "^1.0.0" + +chartjs-color@^2.1.0: + version "2.2.0" + resolved "http://registry.npm.taobao.org/chartjs-color/download/chartjs-color-2.2.0.tgz#84a2fb755787ed85c39dd6dd8c7b1d88429baeae" + dependencies: + chartjs-color-string "^0.5.0" + color-convert "^0.5.3" + chokidar@^1.0.0, chokidar@^1.4.2: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" @@ -1727,10 +1753,14 @@ codemirror-mode-elixir@^1.1.1: dependencies: codemirror "^5.20.2" -codemirror@^5.18.2, codemirror@^5.20.2, codemirror@^5.37.0: +codemirror@^5.18.2, codemirror@^5.20.2: version "5.38.0" resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.38.0.tgz#26a9551446e51dbdde36aabe60f72469724fd332" +codemirror@^5.39.0: + version "5.39.0" + resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.39.0.tgz#4654f7d2f7e525e04a62e72d9482348ccb37dce5" + coffee-script@^1.10.0: version "1.12.7" resolved "https://registry.yarnpkg.com/coffee-script/-/coffee-script-1.12.7.tgz#c05dae0cb79591d05b3070a8433a98c9a89ccc53" @@ -1746,6 +1776,10 @@ collection-visit@^1.0.0: map-visit "^1.0.0" object-visit "^1.0.0" +color-convert@^0.5.3: + version "0.5.3" + resolved "http://registry.npm.taobao.org/color-convert/download/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd" + color-convert@^1.3.0, color-convert@^1.9.0: version "1.9.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.1.tgz#c1261107aeb2f294ebffec9ed9ecad529a6097ed" @@ -1802,6 +1836,10 @@ combined-stream@~0.0.4, combined-stream@~0.0.5: dependencies: delayed-stream "0.0.5" +commander@2: + version "2.16.0" + resolved "http://registry.npm.taobao.org/commander/download/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50" + commander@2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.3.0.tgz#fd430e889832ec353b9acd1de217c11cb3eef873" @@ -2142,12 +2180,250 @@ currently-unhandled@^0.4.1: dependencies: array-find-index "^1.0.1" +d3-array@1, d3-array@1.2.1, d3-array@^1.2.0: + version "1.2.1" + resolved "http://registry.npm.taobao.org/d3-array/download/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc" + +d3-axis@1.0.8: + version "1.0.8" + resolved "http://registry.npm.taobao.org/d3-axis/download/d3-axis-1.0.8.tgz#31a705a0b535e65759de14173a31933137f18efa" + +d3-brush@1.0.4: + version "1.0.4" + resolved "http://registry.npm.taobao.org/d3-brush/download/d3-brush-1.0.4.tgz#00c2f238019f24f6c0a194a26d41a1530ffe7bc4" + dependencies: + d3-dispatch "1" + d3-drag "1" + d3-interpolate "1" + d3-selection "1" + d3-transition "1" + +d3-chord@1.0.4: + version "1.0.4" + resolved "http://registry.npm.taobao.org/d3-chord/download/d3-chord-1.0.4.tgz#7dec4f0ba886f713fe111c45f763414f6f74ca2c" + dependencies: + d3-array "1" + d3-path "1" + +d3-collection@1, d3-collection@1.0.4: + version "1.0.4" + resolved "http://registry.npm.taobao.org/d3-collection/download/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2" + +d3-color@1: + version "1.2.0" + resolved "http://registry.npm.taobao.org/d3-color/download/d3-color-1.2.0.tgz#d1ea19db5859c86854586276ec892cf93148459a" + +d3-color@1.0.3: + version "1.0.3" + resolved "http://registry.npm.taobao.org/d3-color/download/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b" + +d3-dispatch@1, d3-dispatch@1.0.3: + version "1.0.3" + resolved "http://registry.npm.taobao.org/d3-dispatch/download/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8" + +d3-drag@1, d3-drag@1.2.1: + version "1.2.1" + resolved "http://registry.npm.taobao.org/d3-drag/download/d3-drag-1.2.1.tgz#df8dd4c502fb490fc7462046a8ad98a5c479282d" + dependencies: + d3-dispatch "1" + d3-selection "1" + +d3-dsv@1, d3-dsv@1.0.8: + version "1.0.8" + resolved "http://registry.npm.taobao.org/d3-dsv/download/d3-dsv-1.0.8.tgz#907e240d57b386618dc56468bacfe76bf19764ae" + dependencies: + commander "2" + iconv-lite "0.4" + rw "1" + +d3-ease@1, d3-ease@1.0.3: + version "1.0.3" + resolved "http://registry.npm.taobao.org/d3-ease/download/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e" + +d3-force@1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/d3-force/download/d3-force-1.1.0.tgz#cebf3c694f1078fcc3d4daf8e567b2fbd70d4ea3" + dependencies: + d3-collection "1" + d3-dispatch "1" + d3-quadtree "1" + d3-timer "1" + +d3-format@1: + version "1.3.0" + resolved "http://registry.npm.taobao.org/d3-format/download/d3-format-1.3.0.tgz#a3ac44269a2011cdb87c7b5693040c18cddfff11" + +d3-format@1.2.2: + version "1.2.2" + resolved "http://registry.npm.taobao.org/d3-format/download/d3-format-1.2.2.tgz#1a39c479c8a57fe5051b2e67a3bee27061a74e7a" + +d3-geo@1.9.1: + version "1.9.1" + resolved "http://registry.npm.taobao.org/d3-geo/download/d3-geo-1.9.1.tgz#157e3b0f917379d0f73bebfff3be537f49fa7356" + dependencies: + d3-array "1" + +d3-hierarchy@1.1.5: + version "1.1.5" + resolved "http://registry.npm.taobao.org/d3-hierarchy/download/d3-hierarchy-1.1.5.tgz#a1c845c42f84a206bcf1c01c01098ea4ddaa7a26" + +d3-interpolate@1: + version "1.2.0" + resolved "http://registry.npm.taobao.org/d3-interpolate/download/d3-interpolate-1.2.0.tgz#40d81bd8e959ff021c5ea7545bc79b8d22331c41" + dependencies: + d3-color "1" + +d3-interpolate@1.1.6: + version "1.1.6" + resolved "http://registry.npm.taobao.org/d3-interpolate/download/d3-interpolate-1.1.6.tgz#2cf395ae2381804df08aa1bf766b7f97b5f68fb6" + dependencies: + d3-color "1" + +d3-path@1, d3-path@1.0.5: + version "1.0.5" + resolved "http://registry.npm.taobao.org/d3-path/download/d3-path-1.0.5.tgz#241eb1849bd9e9e8021c0d0a799f8a0e8e441764" + +d3-polygon@1.0.3: + version "1.0.3" + resolved "http://registry.npm.taobao.org/d3-polygon/download/d3-polygon-1.0.3.tgz#16888e9026460933f2b179652ad378224d382c62" + +d3-quadtree@1, d3-quadtree@1.0.3: + version "1.0.3" + resolved "http://registry.npm.taobao.org/d3-quadtree/download/d3-quadtree-1.0.3.tgz#ac7987e3e23fe805a990f28e1b50d38fcb822438" + +d3-queue@3.0.7: + version "3.0.7" + resolved "http://registry.npm.taobao.org/d3-queue/download/d3-queue-3.0.7.tgz#c93a2e54b417c0959129d7d73f6cf7d4292e7618" + +d3-random@1.1.0: + version "1.1.0" + resolved "http://registry.npm.taobao.org/d3-random/download/d3-random-1.1.0.tgz#6642e506c6fa3a648595d2b2469788a8d12529d3" + +d3-request@1.0.6: + version "1.0.6" + resolved "http://registry.npm.taobao.org/d3-request/download/d3-request-1.0.6.tgz#a1044a9ef4ec28c824171c9379fae6d79474b19f" + dependencies: + d3-collection "1" + d3-dispatch "1" + d3-dsv "1" + xmlhttprequest "1" + +d3-scale@1.0.7: + version "1.0.7" + resolved "http://registry.npm.taobao.org/d3-scale/download/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d" + dependencies: + d3-array "^1.2.0" + d3-collection "1" + d3-color "1" + d3-format "1" + d3-interpolate "1" + d3-time "1" + d3-time-format "2" + +d3-selection@1, d3-selection@1.3.0, d3-selection@^1.1.0: + version "1.3.0" + resolved "http://registry.npm.taobao.org/d3-selection/download/d3-selection-1.3.0.tgz#d53772382d3dc4f7507bfb28bcd2d6aed2a0ad6d" + +d3-shape@1.2.0: + version "1.2.0" + resolved "http://registry.npm.taobao.org/d3-shape/download/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777" + dependencies: + d3-path "1" + +d3-time-format@2, d3-time-format@2.1.1: + version "2.1.1" + resolved "http://registry.npm.taobao.org/d3-time-format/download/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31" + dependencies: + d3-time "1" + +d3-time@1, d3-time@1.0.8: + version "1.0.8" + resolved "http://registry.npm.taobao.org/d3-time/download/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84" + +d3-timer@1, d3-timer@1.0.7: + version "1.0.7" + resolved "http://registry.npm.taobao.org/d3-timer/download/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531" + +d3-transition@1, d3-transition@1.1.1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/d3-transition/download/d3-transition-1.1.1.tgz#d8ef89c3b848735b060e54a39b32aaebaa421039" + dependencies: + d3-color "1" + d3-dispatch "1" + d3-ease "1" + d3-interpolate "1" + d3-selection "^1.1.0" + d3-timer "1" + +d3-voronoi@1.1.2: + version "1.1.2" + resolved "http://registry.npm.taobao.org/d3-voronoi/download/d3-voronoi-1.1.2.tgz#1687667e8f13a2d158c80c1480c5a29cb0d8973c" + +d3-zoom@1.7.1: + version "1.7.1" + resolved "http://registry.npm.taobao.org/d3-zoom/download/d3-zoom-1.7.1.tgz#02f43b3c3e2db54f364582d7e4a236ccc5506b63" + dependencies: + d3-dispatch "1" + d3-drag "1" + d3-interpolate "1" + d3-selection "1" + d3-transition "1" + +d3@^4.13.0: + version "4.13.0" + resolved "http://registry.npm.taobao.org/d3/download/d3-4.13.0.tgz#ab236ff8cf0cfc27a81e69bf2fb7518bc9b4f33d" + dependencies: + d3-array "1.2.1" + d3-axis "1.0.8" + d3-brush "1.0.4" + d3-chord "1.0.4" + d3-collection "1.0.4" + d3-color "1.0.3" + d3-dispatch "1.0.3" + d3-drag "1.2.1" + d3-dsv "1.0.8" + d3-ease "1.0.3" + d3-force "1.1.0" + d3-format "1.2.2" + d3-geo "1.9.1" + d3-hierarchy "1.1.5" + d3-interpolate "1.1.6" + d3-path "1.0.5" + d3-polygon "1.0.3" + d3-quadtree "1.0.3" + d3-queue "3.0.7" + d3-random "1.1.0" + d3-request "1.0.6" + d3-scale "1.0.7" + d3-selection "1.3.0" + d3-shape "1.2.0" + d3-time "1.0.8" + d3-time-format "2.1.1" + d3-timer "1.0.7" + d3-transition "1.1.1" + d3-voronoi "1.1.2" + d3-zoom "1.7.1" + d@1: version "1.0.0" resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f" dependencies: es5-ext "^0.10.9" +dagre-d3-renderer@^0.5.8: + version "0.5.8" + resolved "http://registry.npm.taobao.org/dagre-d3-renderer/download/dagre-d3-renderer-0.5.8.tgz#aa071bb71d3c4d67426925906f3f6ddead49c1a3" + dependencies: + dagre-layout "^0.8.8" + lodash "^4.17.5" + +dagre-layout@^0.8.8: + version "0.8.8" + resolved "http://registry.npm.taobao.org/dagre-layout/download/dagre-layout-0.8.8.tgz#9b6792f24229f402441c14162c1049e3f261f6d9" + dependencies: + graphlibrary "^2.2.0" + lodash "^4.17.5" + dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" @@ -3681,6 +3957,12 @@ graceful-fs@~1.2.0: version "1.2.3" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-1.2.3.tgz#15a4806a57547cb2d2dbf27f42e89a8c3451b364" +graphlibrary@^2.2.0: + version "2.2.0" + resolved "http://registry.npm.taobao.org/graphlibrary/download/graphlibrary-2.2.0.tgz#017a14899775228dec4497a39babfdd6bf56eac6" + dependencies: + lodash "^4.17.5" + growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" @@ -3870,6 +4152,10 @@ hawk@~2.3.0: hoek "2.x.x" sntp "1.x.x" +he@^1.1.1: + version "1.1.1" + resolved "http://registry.npm.taobao.org/he/download/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd" + highlight.js@^9.3.0: version "9.12.0" resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-9.12.0.tgz#e6d9dbe57cbefe60751f02af336195870c90c01e" @@ -4033,16 +4319,16 @@ i18n-2@^0.7.2: debug "^3.1.0" sprintf "^0.1.5" -iconv-lite@0.4.19: - version "0.4.19" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" - -iconv-lite@^0.4.19, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4, iconv-lite@^0.4.19, iconv-lite@^0.4.4, iconv-lite@~0.4.13: version "0.4.23" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + iconv-lite@~0.2.11: version "0.2.11" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.2.11.tgz#1ce60a3a57864a292d1321ff4609ca4bb965adc8" @@ -5294,7 +5580,7 @@ lodash@^3.5.0: version "3.10.1" resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" -lodash@^4.0.0, lodash@^4.0.1, lodash@^4.11.1, lodash@^4.12.0, lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1: +lodash@^4.0.0, lodash@^4.0.1, lodash@^4.11.1, lodash@^4.12.0, lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.5.1, lodash@^4.6.1: version "4.17.10" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" @@ -5364,9 +5650,9 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -"markdown-it-admonition@https://github.com/johannbre/markdown-it-admonition.git": - version "1.0.2" - resolved "https://github.com/johannbre/markdown-it-admonition.git#e0c0fcd59e9119d6d60ed209aa3d0f1177ec0166" +markdown-it-admonition@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/markdown-it-admonition/-/markdown-it-admonition-1.0.4.tgz#d7bbc7eb1fe6168fc8cc304de7a9d8c993acb2f5" markdown-it-emoji@^1.1.1: version "1.4.0" @@ -5466,6 +5752,10 @@ mdurl@^1.0.1, mdurl@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" +meaw@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/meaw/-/meaw-2.0.0.tgz#7c3467efee5618cb865661dfaa38d6948dc23f7a" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -5523,6 +5813,19 @@ merge@^1.1.3: version "1.2.0" resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" +mermaid@^8.0.0-rc.8: + version "8.0.0-rc.8" + resolved "http://registry.npm.taobao.org/mermaid/download/mermaid-8.0.0-rc.8.tgz#74ed54d0d46e9ee71c4db2730b2d83d516a21e72" + dependencies: + d3 "^4.13.0" + dagre-d3-renderer "^0.5.8" + dagre-layout "^0.8.8" + graphlibrary "^2.2.0" + he "^1.1.1" + lodash "^4.17.5" + moment "^2.21.0" + scope-css "^1.0.5" + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -5692,6 +5995,10 @@ mock-require@^3.0.1: get-caller-file "^1.0.2" normalize-path "^2.1.1" +moment@^2.10.2, moment@^2.21.0: + version "2.22.2" + resolved "http://registry.npm.taobao.org/moment/download/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" + moment@^2.10.3: version "2.22.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.1.tgz#529a2e9bf973f259c9643d237fda84de3a26e8ad" @@ -7341,6 +7648,10 @@ run-series@^1.1.1: version "1.1.8" resolved "https://registry.yarnpkg.com/run-series/-/run-series-1.1.8.tgz#2c4558f49221e01cd6371ff4e0a1e203e460fc36" +rw@1: + version "1.3.3" + resolved "http://registry.npm.taobao.org/rw/download/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" + rx-lite@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" @@ -7418,6 +7729,10 @@ sax@>=0.6.0, sax@^1.2.1, sax@^1.2.4, sax@~1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" +scope-css@^1.0.5: + version "1.1.0" + resolved "http://registry.npm.taobao.org/scope-css/download/scope-css-1.1.0.tgz#74eff45461bc9d3f3b29ed575b798cd722fa1256" + semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" @@ -8818,6 +9133,10 @@ xmldom@0.1.x: version "0.1.27" resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.27.tgz#d501f97b3bdb403af8ef9ecc20573187aadac0e9" +xmlhttprequest@1: + version "1.8.0" + resolved "http://registry.npm.taobao.org/xmlhttprequest/download/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" + xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"