diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js
index c36a50c1..41d71622 100644
--- a/browser/components/CodeEditor.js
+++ b/browser/components/CodeEditor.js
@@ -14,6 +14,8 @@ import consts from 'browser/lib/consts'
import fs from 'fs'
const { ipcRenderer } = require('electron')
import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily'
+import TurndownService from 'turndown'
+import { gfm } from 'turndown-plugin-gfm'
CodeMirror.modeURL = '../node_modules/codemirror/mode/%N/%N.js'
@@ -57,6 +59,7 @@ export default class CodeEditor extends React.Component {
}
this.searchHandler = (e, msg) => this.handleSearch(msg)
this.searchState = null
+ this.scrollToLineHandeler = this.scrollToLine.bind(this)
this.formatTable = () => this.handleFormatTable()
this.editorActivityHandler = () => this.handleEditorActivity()
@@ -125,6 +128,7 @@ export default class CodeEditor extends React.Component {
componentDidMount () {
const { rulers, enableRulers } = this.props
const expandSnippet = this.expandSnippet.bind(this)
+ eventEmitter.on('line:jump', this.scrollToLineHandeler)
const defaultSnippet = [
{
@@ -475,7 +479,13 @@ export default class CodeEditor extends React.Component {
moveCursorTo (row, col) {}
- scrollToLine (num) {}
+ scrollToLine (event, num) {
+ const cursor = {
+ line: num,
+ ch: 1
+ }
+ this.editor.setCursor(cursor)
+ }
focus () {
this.editor.focus()
@@ -538,7 +548,11 @@ export default class CodeEditor extends React.Component {
)
return prevChar === '](' && nextChar === ')'
}
- if (dataTransferItem.type.match('image')) {
+
+ const pastedHtml = clipboardData.getData('text/html')
+ if (pastedHtml !== '') {
+ this.handlePasteHtml(e, editor, pastedHtml)
+ } else if (dataTransferItem.type.match('image')) {
attachmentManagement.handlePastImageEvent(
this,
storageKey,
@@ -608,6 +622,12 @@ export default class CodeEditor extends React.Component {
})
}
+ handlePasteHtml (e, editor, pastedHtml) {
+ e.preventDefault()
+ const markdown = this.turndownService.turndown(pastedHtml)
+ editor.replaceSelection(markdown)
+ }
+
mapNormalResponse (response, pastedTxt) {
return this.decodeResponse(response).then(body => {
return new Promise((resolve, reject) => {
diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js
index 70df16a0..9c8a06d6 100644
--- a/browser/components/MarkdownEditor.js
+++ b/browser/components/MarkdownEditor.js
@@ -6,6 +6,7 @@ import CodeEditor from 'browser/components/CodeEditor'
import MarkdownPreview from 'browser/components/MarkdownPreview'
import eventEmitter from 'browser/main/lib/eventEmitter'
import { findStorage } from 'browser/lib/findStorage'
+import ConfigManager from 'browser/main/lib/ConfigManager'
class MarkdownEditor extends React.Component {
constructor (props) {
@@ -18,7 +19,7 @@ class MarkdownEditor extends React.Component {
this.supportMdSelectionBold = [16, 17, 186]
this.state = {
- status: 'PREVIEW',
+ status: props.config.editor.switchPreview === 'RIGHTCLICK' ? props.config.editor.delfaultStatus : 'PREVIEW',
renderValue: props.value,
keyPressed: new Set(),
isLocked: false
@@ -64,6 +65,10 @@ class MarkdownEditor extends React.Component {
})
}
+ setValue (value) {
+ this.refs.code.setValue(value)
+ }
+
handleChange (e) {
this.value = this.refs.code.value
this.props.onChange(e)
@@ -72,9 +77,7 @@ class MarkdownEditor extends React.Component {
handleContextMenu (e) {
const { config } = this.props
if (config.editor.switchPreview === 'RIGHTCLICK') {
- const newStatus = this.state.status === 'PREVIEW'
- ? 'CODE'
- : 'PREVIEW'
+ const newStatus = this.state.status === 'PREVIEW' ? 'CODE' : 'PREVIEW'
this.setState({
status: newStatus
}, () => {
@@ -84,6 +87,10 @@ class MarkdownEditor extends React.Component {
this.refs.preview.focus()
}
eventEmitter.emit('topbar:togglelockbutton', this.state.status)
+
+ const newConfig = Object.assign({}, config)
+ newConfig.editor.delfaultStatus = newStatus
+ ConfigManager.set(newConfig)
})
}
}
@@ -300,6 +307,7 @@ class MarkdownEditor extends React.Component {
noteKey={noteKey}
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
+ lineThroughCheckbox={config.preview.lineThroughCheckbox}
/>
)
diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js
index e69f312e..d9ff7074 100755
--- a/browser/components/MarkdownPreview.js
+++ b/browser/components/MarkdownPreview.js
@@ -17,6 +17,7 @@ import copy from 'copy-to-clipboard'
import mdurl from 'mdurl'
import exportNote from 'browser/main/lib/dataApi/exportNote'
import { escapeHtmlCharacters } from 'browser/lib/utils'
+import yaml from 'js-yaml'
import context from 'browser/lib/context'
import i18n from 'browser/lib/i18n'
import fs from 'fs'
@@ -80,7 +81,6 @@ function buildStyle (
url('${appPath}/resources/fonts/MaterialIcons-Regular.woff') format('woff'),
url('${appPath}/resources/fonts/MaterialIcons-Regular.ttf') format('truetype');
}
-${allowCustomCSS ? customCSS : ''}
${markdownStyle}
body {
@@ -88,6 +88,11 @@ body {
font-size: ${fontSize}px;
${scrollPastEnd && 'padding-bottom: 90vh;'}
}
+@media print {
+ body {
+ padding-bottom: initial;
+ }
+}
code {
font-family: '${codeBlockFontFamily.join("','")}';
background-color: rgba(0,0,0,0.04);
@@ -144,6 +149,8 @@ body p {
display: none
}
}
+
+${allowCustomCSS ? customCSS : ''}
`
}
@@ -325,9 +332,7 @@ export default class MarkdownPreview extends React.Component {
allowCustomCSS,
customCSS
)
- let body = this.markdown.render(
- escapeHtmlCharacters(noteContent, { detectCodeBlock: true })
- )
+ let body = this.markdown.render(noteContent)
const files = [this.GetCodeThemeLink(codeBlockTheme), ...CSS_FILES]
const attachmentsAbsolutePaths = attachmentManagement.getAbsolutePathsOfAttachmentsInContent(
noteContent,
@@ -484,10 +489,6 @@ export default class MarkdownPreview extends React.Component {
eventEmitter.on('export:save-md', this.saveAsMdHandler)
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
eventEmitter.on('print', this.printHandler)
- eventEmitter.on('config-renew', () => {
- this.markdown.updateConfig()
- this.rewriteIframe()
- })
}
componentWillUnmount () {
@@ -531,7 +532,8 @@ export default class MarkdownPreview extends React.Component {
prevProps.smartQuotes !== this.props.smartQuotes ||
prevProps.sanitize !== this.props.sanitize ||
prevProps.smartArrows !== this.props.smartArrows ||
- prevProps.breaks !== this.props.breaks
+ prevProps.breaks !== this.props.breaks ||
+ prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox
) {
this.initMarkdown()
this.rewriteIframe()
@@ -737,7 +739,6 @@ export default class MarkdownPreview extends React.Component {
el.addEventListener('click', this.linkClickHandler)
})
} catch (e) {
- console.error(e)
el.className = 'flowchart-error'
el.innerHTML = 'Flowchart parse error: ' + e.message
}
@@ -758,7 +759,6 @@ export default class MarkdownPreview extends React.Component {
el.addEventListener('click', this.linkClickHandler)
})
} catch (e) {
- console.error(e)
el.className = 'sequence-error'
el.innerHTML = 'Sequence diagram parse error: ' + e.message
}
@@ -769,14 +769,21 @@ export default class MarkdownPreview extends React.Component {
this.refs.root.contentWindow.document.querySelectorAll('.chart'),
el => {
try {
- const chartConfig = JSON.parse(el.innerHTML)
+ const format = el.attributes.getNamedItem('data-format').value
+ const chartConfig = format === 'yaml' ? yaml.load(el.innerHTML) : JSON.parse(el.innerHTML)
el.innerHTML = ''
- var canvas = document.createElement('canvas')
+
+ const canvas = document.createElement('canvas')
el.appendChild(canvas)
- /* eslint-disable no-new */
- new Chart(canvas, chartConfig)
+
+ const height = el.attributes.getNamedItem('data-height')
+ if (height && height.value !== 'undefined') {
+ el.style.height = height.value + 'vh'
+ canvas.height = height.value + 'vh'
+ }
+
+ const chart = new Chart(canvas, chartConfig)
} catch (e) {
- console.error(e)
el.className = 'chart-error'
el.innerHTML = 'chartjs diagram parse error: ' + e.message
}
@@ -860,6 +867,15 @@ export default class MarkdownPreview extends React.Component {
return
}
+ const regexIsLine = /^:line:[0-9]/
+ if (regexIsLine.test(linkHash)) {
+ const numberPattern = /\d+/g
+
+ const lineNumber = parseInt(linkHash.match(numberPattern)[0])
+ eventEmitter.emit('line:jump', lineNumber)
+ return
+ }
+
// this will match the old link format storage.key-note.key
// e.g.
// 877f99c3268608328037-1c211eb7dcb463de6490
diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js
index d714125a..ca2d3108 100644
--- a/browser/components/MarkdownSplitEditor.js
+++ b/browser/components/MarkdownSplitEditor.js
@@ -20,12 +20,18 @@ class MarkdownSplitEditor extends React.Component {
}
}
+ setValue (value) {
+ this.refs.code.setValue(value)
+ }
+
handleOnChange () {
this.value = this.refs.code.value
this.props.onChange()
}
handleScroll (e) {
+ if (!this.props.config.preview.scrollSync) return
+
const previewDoc = _.get(this, 'refs.preview.refs.root.contentWindow.document')
const codeDoc = _.get(this, 'refs.code.editor.doc')
let srcTop, srcHeight, targetTop, targetHeight
@@ -192,6 +198,7 @@ class MarkdownSplitEditor extends React.Component {
noteKey={noteKey}
customCSS={config.preview.customCSS}
allowCustomCSS={config.preview.allowCustomCSS}
+ lineThroughCheckbox={config.preview.lineThroughCheckbox}
/>
)
diff --git a/browser/components/NoteItem.js b/browser/components/NoteItem.js
index 600b7e2d..2fc70a39 100644
--- a/browser/components/NoteItem.js
+++ b/browser/components/NoteItem.js
@@ -24,16 +24,19 @@ const TagElement = ({ tagName }) => (
/**
* @description Tag element list component.
* @param {Array|null} tags
+ * @param {boolean} showTagsAlphabetically
* @return {React.Component}
*/
-const TagElementList = tags => {
+const TagElementList = (tags, showTagsAlphabetically) => {
if (!isArray(tags)) {
return []
}
- const tagElements = tags.map(tag => TagElement({ tagName: tag }))
-
- return tagElements
+ if (showTagsAlphabetically) {
+ return _.sortBy(tags).map(tag => TagElement({ tagName: tag }))
+ } else {
+ return tags.map(tag => TagElement({ tagName: tag }))
+ }
}
/**
@@ -55,7 +58,8 @@ const NoteItem = ({
pathname,
storageName,
folderName,
- viewType
+ viewType,
+ showTagsAlphabetically
}) => (
{note.tags.length > 0
- ? TagElementList(note.tags)
+ ? TagElementList(note.tags, showTagsAlphabetically)
:
(
{storageList.length > 0 ? storageList : (
-
No storage mount.
+
No storage mount.
)}
)
StorageList.propTypes = {
- storgaeList: PropTypes.arrayOf(PropTypes.element).isRequired
+ storageList: PropTypes.arrayOf(PropTypes.element).isRequired
}
export default CSSModules(StorageList, styles)
diff --git a/browser/components/TagListItem.js b/browser/components/TagListItem.js
index 6cd50c9c..eec8ab14 100644
--- a/browser/components/TagListItem.js
+++ b/browser/components/TagListItem.js
@@ -14,8 +14,8 @@ import CSSModules from 'browser/lib/CSSModules'
* @param {bool} isRelated
*/
-const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, isActive, isRelated, count}) => (
-
+const TagListItem = ({name, handleClickTagListItem, handleClickNarrowToTag, handleContextMenu, isActive, isRelated, count}) => (
+
handleContextMenu(e, name)}>
{isRelated
?
diff --git a/browser/components/TodoListPercentage.js b/browser/components/TodoListPercentage.js
index 3565f274..b917bbc1 100644
--- a/browser/components/TodoListPercentage.js
+++ b/browser/components/TodoListPercentage.js
@@ -12,7 +12,7 @@ import styles from './TodoListPercentage.styl'
*/
const TodoListPercentage = ({
- percentageOfTodo
+ percentageOfTodo, onClearCheckboxClick
}) => (
@@ -20,11 +20,15 @@ const TodoListPercentage = ({
{percentageOfTodo}%
+
+
onClearCheckboxClick(e)}>clear
+
)
TodoListPercentage.propTypes = {
- percentageOfTodo: PropTypes.number.isRequired
+ percentageOfTodo: PropTypes.number.isRequired,
+ onClearCheckboxClick: PropTypes.func.isRequired
}
export default CSSModules(TodoListPercentage, styles)
diff --git a/browser/components/TodoListPercentage.styl b/browser/components/TodoListPercentage.styl
index 7b6a7d61..5a0f3257 100644
--- a/browser/components/TodoListPercentage.styl
+++ b/browser/components/TodoListPercentage.styl
@@ -1,4 +1,5 @@
.percentageBar
+ display: flex
position absolute
top 72px
right 0px
@@ -30,6 +31,20 @@
color #f4f4f4
font-weight 600
+.todoClear
+ display flex
+ justify-content: flex-end
+ position absolute
+ z-index 120
+ width 100%
+ height 100%
+ padding 2px 10px
+
+.todoClearText
+ color #f4f4f4
+ cursor pointer
+ font-weight 500
+
body[data-theme="dark"]
.percentageBar
background-color #444444
@@ -39,6 +54,9 @@ body[data-theme="dark"]
.percentageText
color $ui-dark-text-color
+
+ .todoClearText
+ color $ui-dark-text-color
body[data-theme="solarized-dark"]
.percentageBar
@@ -50,6 +68,9 @@ body[data-theme="solarized-dark"]
.percentageText
color #fdf6e3
+ .todoClearText
+ color #fdf6e3
+
body[data-theme="monokai"]
.percentageBar
background-color: $ui-monokai-borderColor
@@ -69,3 +90,6 @@ body[data-theme="dracula"]
.percentageText
color $ui-dracula-text-color
+
+ .percentageText
+ color $ui-dracula-text-color
diff --git a/browser/components/markdown.styl b/browser/components/markdown.styl
index e091331b..b7f219b8 100644
--- a/browser/components/markdown.styl
+++ b/browser/components/markdown.styl
@@ -209,41 +209,39 @@ code
text-decoration none
margin-right 2px
pre
- padding 0.5em !important
+ padding 0.5rem !important
border solid 1px #D1D1D1
border-radius 5px
overflow-x auto
- margin 0 0 1em
+ margin 0 0 1rem
display flex
line-height 1.4em
- &.flowchart, &.sequence, &.chart
- display flex
- justify-content center
- background-color white
- &.CodeMirror
- height initial
- flex-wrap wrap
- &>code
- flex 1
- overflow-x auto
code
background-color inherit
margin 0
padding 0
border none
border-radius 0
+ &.CodeMirror
+ height initial
+ flex-wrap wrap
+ &>code
+ flex 1
+ overflow-x auto
+ &.mermaid svg
+ max-width 100% !important
&>span.filename
- width 100%
- border-radius: 5px 0px 0px 0px
- margin -8px 100% 8px -8px
- padding 0px 6px
+ margin -0.5rem 100% 0.5rem -0.5rem
+ padding 0.125rem 0.375rem
background-color #777;
color white
+ &:empty
+ display none
&>span.lineNumber
display none
font-size 1em
- padding 0.5em 0
- margin -0.5em 0.5em -0.5em -0.5em
+ padding 0.5rem 0
+ margin -0.5rem 0.5rem -0.5rem -0.5rem
border-right 1px solid
text-align right
border-top-left-radius 4px
@@ -375,6 +373,49 @@ for name, val in admonition_types
color: val[color]
content: val[icon]
+dl
+ margin 2rem 0
+ padding 0
+ display flex
+ width 100%
+ flex-wrap wrap
+ align-items flex-start
+ border-bottom 1px solid borderColor
+ background-color tableHeadBgColor
+
+dt
+ border-top 1px solid borderColor
+ font-weight bold
+ text-align right
+ overflow hidden
+ flex-basis 20%
+ padding 0.4rem 0.9rem
+ box-sizing border-box
+
+dd
+ border-top 1px solid borderColor
+ flex-basis 80%
+ padding 0.4rem 0.9rem
+ min-height 2.5rem
+ background-color $ui-noteDetail-backgroundColor
+ box-sizing border-box
+
+dd + dd
+ margin-left 20%
+
+pre.fence
+ flex-wrap wrap
+
+ .chart, .flowchart, .mermaid, .sequence
+ display flex
+ justify-content center
+ background-color white
+ max-width 100%
+ flex-grow 1
+
+ canvas, svg
+ max-width 100% !important
+
themeDarkBackground = darken(#21252B, 10%)
themeDarkText = #f9f9f9
themeDarkBorder = lighten(themeDarkBackground, 20%)
@@ -425,6 +466,14 @@ body[data-theme="dark"]
kbd
background-color themeDarkBorder
color themeDarkText
+ dl
+ border-color themeDarkBorder
+ background-color themeDarkTableHead
+ dt
+ border-color themeDarkBorder
+ dd
+ border-color themeDarkBorder
+ background-color themeDarkPreview
themeSolarizedDarkTableOdd = $ui-solarized-dark-noteDetail-backgroundColor
themeSolarizedDarkTableEven = darken($ui-solarized-dark-noteDetail-backgroundColor, 10%)
@@ -452,6 +501,14 @@ body[data-theme="solarized-dark"]
border-color themeSolarizedDarkTableBorder
&:last-child
border-right solid 1px themeSolarizedDarkTableBorder
+ dl
+ border-color themeDarkBorder
+ background-color themeSolarizedDarkTableHead
+ dt
+ border-color themeDarkBorder
+ dd
+ border-color themeDarkBorder
+ background-color $ui-solarized-dark-noteDetail-backgroundColor
themeMonokaiTableOdd = $ui-monokai-noteDetail-backgroundColor
themeMonokaiTableEven = darken($ui-monokai-noteDetail-backgroundColor, 10%)
@@ -481,6 +538,14 @@ body[data-theme="monokai"]
border-right solid 1px themeMonokaiTableBorder
kbd
background-color themeDarkBackground
+ dl
+ border-color themeDarkBorder
+ background-color themeMonokaiTableHead
+ dt
+ border-color themeDarkBorder
+ dd
+ border-color themeDarkBorder
+ background-color $ui-monokai-noteDetail-backgroundColor
themeDraculaTableOdd = $ui-dracula-noteDetail-backgroundColor
themeDraculaTableEven = darken($ui-dracula-noteDetail-backgroundColor, 10%)
@@ -509,4 +574,12 @@ body[data-theme="dracula"]
&:last-child
border-right solid 1px themeDraculaTableBorder
kbd
- background-color themeDarkBackground
\ No newline at end of file
+ background-color themeDarkBackground
+ dl
+ border-color themeDarkBorder
+ background-color themeDraculaTableHead
+ dt
+ border-color themeDarkBorder
+ dd
+ border-color themeDarkBorder
+ background-color $ui-dracula-noteDetail-backgroundColor
diff --git a/browser/components/render/MermaidRender.js b/browser/components/render/MermaidRender.js
index e8784d9d..7a3b3ea2 100644
--- a/browser/components/render/MermaidRender.js
+++ b/browser/components/render/MermaidRender.js
@@ -11,9 +11,9 @@ function getRandomInt (min, max) {
}
function getId () {
- var pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
- var id = 'm-'
- for (var i = 0; i < 7; i++) {
+ const pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
+ let id = 'm-'
+ for (let i = 0; i < 7; i++) {
id += pool[getRandomInt(0, 16)]
}
return id
@@ -21,16 +21,20 @@ function getId () {
function render (element, content, theme) {
try {
+ const height = element.attributes.getNamedItem('data-height')
+ if (height && height.value !== 'undefined') {
+ element.style.height = height.value + 'vh'
+ }
let isDarkTheme = theme === 'dark' || theme === 'solarized-dark' || theme === 'monokai' || theme === 'dracula'
mermaidAPI.initialize({
theme: isDarkTheme ? 'dark' : 'default',
- themeCSS: isDarkTheme ? darkThemeStyling : ''
+ themeCSS: isDarkTheme ? darkThemeStyling : '',
+ useMaxWidth: false
})
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
}
diff --git a/browser/lib/Languages.js b/browser/lib/Languages.js
index ddb7e0ed..1a798fdf 100644
--- a/browser/lib/Languages.js
+++ b/browser/lib/Languages.js
@@ -49,7 +49,7 @@ const languages = [
},
{
name: 'Portuguese',
- locale: 'pt'
+ locale: 'pt-BR'
},
{
name: 'Russian',
@@ -61,6 +61,9 @@ const languages = [
}, {
name: 'Turkish',
locale: 'tr'
+ }, {
+ name: 'Thai',
+ locale: 'th'
}
]
diff --git a/browser/lib/findNoteTitle.js b/browser/lib/findNoteTitle.js
index b954f172..912c3bdd 100644
--- a/browser/lib/findNoteTitle.js
+++ b/browser/lib/findNoteTitle.js
@@ -1,4 +1,4 @@
-export function findNoteTitle (value) {
+export function findNoteTitle (value, enableFrontMatterTitle, frontMatterTitleField = 'title') {
const splitted = value.split('\n')
let title = null
let isInsideCodeBlock = false
@@ -6,6 +6,11 @@ export function findNoteTitle (value) {
if (splitted[0] === '---') {
let line = 0
while (++line < splitted.length) {
+ if (enableFrontMatterTitle && splitted[line].startsWith(frontMatterTitleField + ':')) {
+ title = splitted[line].substring(frontMatterTitleField.length + 1).trim()
+
+ break
+ }
if (splitted[line] === '---') {
splitted.splice(0, line + 1)
@@ -14,17 +19,19 @@ export function findNoteTitle (value) {
}
}
- splitted.some((line, index) => {
- const trimmedLine = line.trim()
- const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
- if (trimmedLine.match('```')) {
- isInsideCodeBlock = !isInsideCodeBlock
- }
- if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
- title = trimmedLine
- return true
- }
- })
+ if (title === null) {
+ splitted.some((line, index) => {
+ const trimmedLine = line.trim()
+ const trimmedNextLine = splitted[index + 1] === undefined ? '' : splitted[index + 1].trim()
+ if (trimmedLine.match('```')) {
+ isInsideCodeBlock = !isInsideCodeBlock
+ }
+ if (isInsideCodeBlock === false && (trimmedLine.match(/^# +/) || trimmedNextLine.match(/^=+$/))) {
+ title = trimmedLine
+ return true
+ }
+ })
+ }
if (title === null) {
title = ''
diff --git a/browser/lib/markdown-it-deflist.js b/browser/lib/markdown-it-deflist.js
new file mode 100644
index 00000000..db14c636
--- /dev/null
+++ b/browser/lib/markdown-it-deflist.js
@@ -0,0 +1,232 @@
+'use strict'
+
+module.exports = function definitionListPlugin (md) {
+ var isSpace = md.utils.isSpace
+
+ // Search `[:~][\n ]`, returns next pos after marker on success
+ // or -1 on fail.
+ function skipMarker (state, line) {
+ let start = state.bMarks[line] + state.tShift[line]
+ const max = state.eMarks[line]
+
+ if (start >= max) { return -1 }
+
+ // Check bullet
+ const marker = state.src.charCodeAt(start++)
+ if (marker !== 0x7E/* ~ */ && marker !== 0x3A/* : */) { return -1 }
+
+ const pos = state.skipSpaces(start)
+
+ // require space after ":"
+ if (start === pos) { return -1 }
+
+ return start
+ }
+
+ function markTightParagraphs (state, idx) {
+ const level = state.level + 2
+
+ let i
+ let l
+ for (i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
+ if (state.tokens[i].level === level && state.tokens[i].type === 'paragraph_open') {
+ state.tokens[i + 2].hidden = true
+ state.tokens[i].hidden = true
+ i += 2
+ }
+ }
+ }
+
+ function deflist (state, startLine, endLine, silent) {
+ var ch,
+ contentStart,
+ ddLine,
+ dtLine,
+ itemLines,
+ listLines,
+ listTokIdx,
+ max,
+ newEndLine,
+ nextLine,
+ offset,
+ oldDDIndent,
+ oldIndent,
+ oldLineMax,
+ oldParentType,
+ oldSCount,
+ oldTShift,
+ oldTight,
+ pos,
+ prevEmptyEnd,
+ tight,
+ token
+
+ if (silent) {
+ // quirk: validation mode validates a dd block only, not a whole deflist
+ if (state.ddIndent < 0) { return false }
+ return skipMarker(state, startLine) >= 0
+ }
+
+ nextLine = startLine + 1
+ if (nextLine >= endLine) { return false }
+
+ if (state.isEmpty(nextLine)) {
+ nextLine++
+ if (nextLine >= endLine) { return false }
+ }
+
+ if (state.sCount[nextLine] < state.blkIndent) { return false }
+ contentStart = skipMarker(state, nextLine)
+ if (contentStart < 0) { return false }
+
+ // Start list
+ listTokIdx = state.tokens.length
+ tight = true
+
+ token = state.push('dl_open', 'dl', 1)
+ token.map = listLines = [ startLine, 0 ]
+
+ //
+ // Iterate list items
+ //
+
+ dtLine = startLine
+ ddLine = nextLine
+
+ // One definition list can contain multiple DTs,
+ // and one DT can be followed by multiple DDs.
+ //
+ // Thus, there is two loops here, and label is
+ // needed to break out of the second one
+ //
+ /* eslint no-labels:0,block-scoped-var:0 */
+ OUTER:
+ for (;;) {
+ prevEmptyEnd = false
+
+ token = state.push('dt_open', 'dt', 1)
+ token.map = [ dtLine, dtLine ]
+
+ token = state.push('inline', '', 0)
+ token.map = [ dtLine, dtLine ]
+ token.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim()
+ token.children = []
+
+ token = state.push('dt_close', 'dt', -1)
+
+ for (;;) {
+ token = state.push('dd_open', 'dd', 1)
+ token.map = itemLines = [ ddLine, 0 ]
+
+ pos = contentStart
+ max = state.eMarks[ddLine]
+ offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine])
+
+ while (pos < max) {
+ ch = state.src.charCodeAt(pos)
+
+ if (isSpace(ch)) {
+ if (ch === 0x09) {
+ offset += 4 - offset % 4
+ } else {
+ offset++
+ }
+ } else {
+ break
+ }
+
+ pos++
+ }
+
+ contentStart = pos
+
+ oldTight = state.tight
+ oldDDIndent = state.ddIndent
+ oldIndent = state.blkIndent
+ oldTShift = state.tShift[ddLine]
+ oldSCount = state.sCount[ddLine]
+ oldParentType = state.parentType
+ state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2
+ state.tShift[ddLine] = contentStart - state.bMarks[ddLine]
+ state.sCount[ddLine] = offset
+ state.tight = true
+ state.parentType = 'deflist'
+
+ newEndLine = ddLine
+ while (++newEndLine < endLine && (state.sCount[newEndLine] >= state.sCount[ddLine] || state.isEmpty(newEndLine))) {
+ }
+
+ oldLineMax = state.lineMax
+ state.lineMax = newEndLine
+
+ state.md.block.tokenize(state, ddLine, newEndLine, true)
+
+ state.lineMax = oldLineMax
+
+ // If any of list item is tight, mark list as tight
+ if (!state.tight || prevEmptyEnd) {
+ tight = false
+ }
+ // Item become loose if finish with empty line,
+ // but we should filter last element, because it means list finish
+ prevEmptyEnd = (state.line - ddLine) > 1 && state.isEmpty(state.line - 1)
+
+ state.tShift[ddLine] = oldTShift
+ state.sCount[ddLine] = oldSCount
+ state.tight = oldTight
+ state.parentType = oldParentType
+ state.blkIndent = oldIndent
+ state.ddIndent = oldDDIndent
+
+ token = state.push('dd_close', 'dd', -1)
+
+ itemLines[1] = nextLine = state.line
+
+ if (nextLine >= endLine) { break OUTER }
+
+ if (state.sCount[nextLine] < state.blkIndent) { break OUTER }
+ contentStart = skipMarker(state, nextLine)
+ if (contentStart < 0) { break }
+
+ ddLine = nextLine
+
+ // go to the next loop iteration:
+ // insert DD tag and repeat checking
+ }
+
+ if (nextLine >= endLine) { break }
+ dtLine = nextLine
+
+ if (state.isEmpty(dtLine)) { break }
+ if (state.sCount[dtLine] < state.blkIndent) { break }
+
+ ddLine = dtLine + 1
+ if (ddLine >= endLine) { break }
+ if (state.isEmpty(ddLine)) { ddLine++ }
+ if (ddLine >= endLine) { break }
+
+ if (state.sCount[ddLine] < state.blkIndent) { break }
+ contentStart = skipMarker(state, ddLine)
+ if (contentStart < 0) { break }
+
+ // go to the next loop iteration:
+ // insert DT and DD tags and repeat checking
+ }
+
+ // Finilize list
+ token = state.push('dl_close', 'dl', -1)
+
+ listLines[1] = nextLine
+
+ state.line = nextLine
+
+ // mark paragraphs tight if needed
+ if (tight) {
+ markTightParagraphs(state, listTokIdx)
+ }
+
+ return true
+ }
+
+ md.block.ruler.before('paragraph', 'deflist', deflist, { alt: [ 'paragraph', 'reference' ] })
+}
diff --git a/browser/lib/markdown-it-fence.js b/browser/lib/markdown-it-fence.js
new file mode 100644
index 00000000..fd1c759d
--- /dev/null
+++ b/browser/lib/markdown-it-fence.js
@@ -0,0 +1,132 @@
+'use strict'
+
+module.exports = function (md, renderers, defaultRenderer) {
+ const paramsRE = /^[ \t]*([\w+#-]+)?(?:\(((?:\s*\w[-\w]*(?:=(?:'(?:.*?[^\\])?'|"(?:.*?[^\\])?"|(?:[^'"][^\s]*)))?)*)\))?(?::([^:]*)(?::(\d+))?)?\s*$/
+
+ function fence (state, startLine, endLine) {
+ let pos = state.bMarks[startLine] + state.tShift[startLine]
+ let max = state.eMarks[startLine]
+
+ if (state.sCount[startLine] - state.blkIndent >= 4 || pos + 3 > max) {
+ return false
+ }
+
+ const marker = state.src.charCodeAt(pos)
+ if (!(marker === 96 || marker === 126)) {
+ return false
+ }
+
+ let mem = pos
+ pos = state.skipChars(pos, marker)
+
+ let len = pos - mem
+ if (len < 3) {
+ return false
+ }
+
+ const markup = state.src.slice(mem, pos)
+ const params = state.src.slice(pos, max)
+
+ let nextLine = startLine
+ let haveEndMarker = false
+
+ while (true) {
+ nextLine++
+ if (nextLine >= endLine) {
+ break
+ }
+
+ pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]
+ max = state.eMarks[nextLine]
+
+ if (pos < max && state.sCount[nextLine] < state.blkIndent) {
+ break
+ }
+ if (state.src.charCodeAt(pos) !== marker || state.sCount[nextLine] - state.blkIndent >= 4) {
+ continue
+ }
+
+ pos = state.skipChars(pos, marker)
+
+ if (pos - mem < len) {
+ continue
+ }
+
+ pos = state.skipSpaces(pos)
+
+ if (pos >= max) {
+ haveEndMarker = true
+ break
+ }
+ }
+
+ len = state.sCount[startLine]
+ state.line = nextLine + (haveEndMarker ? 1 : 0)
+
+ const parameters = {}
+ let langType = ''
+ let fileName = ''
+ let firstLineNumber = 1
+
+ let match = paramsRE.exec(params)
+ if (match) {
+ if (match[1]) {
+ langType = match[1]
+ }
+ if (match[3]) {
+ fileName = match[3]
+ }
+ if (match[4]) {
+ firstLineNumber = parseInt(match[4], 10)
+ }
+
+ if (match[2]) {
+ const params = match[2]
+ const regex = /(\w[-\w]*)(?:=(?:'(.*?[^\\])?'|"(.*?[^\\])?"|([^'"][^\s]*)))?/g
+
+ let name, value
+ while ((match = regex.exec(params))) {
+ name = match[1]
+ value = match[2] || match[3] || match[4] || null
+
+ const height = /^(\d+)h$/.exec(name)
+ if (height && !value) {
+ parameters.height = height[1]
+ } else {
+ parameters[name] = value
+ }
+ }
+ }
+ }
+
+ let token
+ if (renderers[langType]) {
+ token = state.push(`${langType}_fence`, 'div', 0)
+ } else {
+ token = state.push('_fence', 'code', 0)
+ }
+
+ token.langType = langType
+ token.fileName = fileName
+ token.firstLineNumber = firstLineNumber
+ token.parameters = parameters
+
+ token.content = state.getLines(startLine + 1, nextLine, len, true)
+ token.markup = markup
+ token.map = [startLine, state.line]
+
+ return true
+ }
+
+ md.block.ruler.before('fence', '_fence', fence, {
+ alt: ['paragraph', 'reference', 'blockquote', 'list']
+ })
+
+ for (const name in renderers) {
+ md.renderer.rules[`${name}_fence`] = (tokens, index) => renderers[name](tokens[index])
+ }
+
+ if (defaultRenderer) {
+ md.renderer.rules['_fence'] = (tokens, index) => defaultRenderer(tokens[index])
+ }
+}
diff --git a/browser/lib/markdown-it-sanitize-html.js b/browser/lib/markdown-it-sanitize-html.js
index 05e5e7be..8f6d86a8 100644
--- a/browser/lib/markdown-it-sanitize-html.js
+++ b/browser/lib/markdown-it-sanitize-html.js
@@ -2,6 +2,7 @@
import sanitizeHtml from 'sanitize-html'
import { escapeHtmlCharacters } from './utils'
+import url from 'url'
module.exports = function sanitizePlugin (md, options) {
options = options || {}
@@ -14,7 +15,7 @@ module.exports = function sanitizePlugin (md, options) {
options
)
}
- if (state.tokens[tokenIdx].type === 'fence') {
+ if (state.tokens[tokenIdx].type === '_fence') {
// escapeHtmlCharacters has better performance
state.tokens[tokenIdx].content = escapeHtmlCharacters(
state.tokens[tokenIdx].content,
@@ -25,7 +26,7 @@ module.exports = function sanitizePlugin (md, options) {
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 = sanitizeInline(
inlineTokens[childIdx].content,
options
)
@@ -35,3 +36,89 @@ module.exports = function sanitizePlugin (md, options) {
}
})
}
+
+const tagRegex = /<([A-Z][A-Z0-9]*)\s*((?:\s*[A-Z][A-Z0-9]*(?:=("|')(?:[^\3]+?)\3)?)*)\s*\/?>|<\/([A-Z][A-Z0-9]*)\s*>/i
+const attributesRegex = /([A-Z][A-Z0-9]*)(?:=("|')([^\2]+?)\2)?/ig
+
+function sanitizeInline (html, options) {
+ let match = tagRegex.exec(html)
+ if (!match) {
+ return ''
+ }
+
+ const { allowedTags, allowedAttributes, selfClosing, allowedSchemesAppliedToAttributes } = options
+
+ if (match[1] !== undefined) {
+ // opening tag
+ const tag = match[1].toLowerCase()
+ if (allowedTags.indexOf(tag) === -1) {
+ return ''
+ }
+
+ const attributes = match[2]
+
+ let attrs = ''
+ let name
+ let value
+
+ while ((match = attributesRegex.exec(attributes))) {
+ name = match[1].toLowerCase()
+ value = match[3]
+
+ if (allowedAttributes['*'].indexOf(name) !== -1 || (allowedAttributes[tag] && allowedAttributes[tag].indexOf(name) !== -1)) {
+ if (allowedSchemesAppliedToAttributes.indexOf(name) !== -1) {
+ if (naughtyHRef(value, options) || (tag === 'iframe' && name === 'src' && naughtyIFrame(value, options))) {
+ continue
+ }
+ }
+
+ attrs += ` ${name}`
+ if (match[2]) {
+ attrs += `="${value}"`
+ }
+ }
+ }
+
+ if (selfClosing.indexOf(tag) === -1) {
+ return '<' + tag + attrs + '>'
+ } else {
+ return '<' + tag + attrs + ' />'
+ }
+ } else {
+ // closing tag
+ if (allowedTags.indexOf(match[4].toLowerCase()) !== -1) {
+ return html
+ } else {
+ return ''
+ }
+ }
+}
+
+function naughtyHRef (href, options) {
+ // href = href.replace(/[\x00-\x20]+/g, '')
+ href = href.replace(/<\!\-\-.*?\-\-\>/g, '')
+
+ const matches = href.match(/^([a-zA-Z]+)\:/)
+ if (!matches) {
+ if (href.match(/^[\/\\]{2}/)) {
+ return !options.allowProtocolRelative
+ }
+
+ // No scheme
+ return false
+ }
+
+ const scheme = matches[1].toLowerCase()
+
+ return options.allowedSchemes.indexOf(scheme) === -1
+}
+
+function naughtyIFrame (src, options) {
+ try {
+ const parsed = url.parse(src, false, true)
+
+ return options.allowedIframeHostnames.index(parsed.hostname) === -1
+ } catch (e) {
+ return true
+ }
+}
diff --git a/browser/lib/markdown.js b/browser/lib/markdown.js
index 248dbb4b..13ef758a 100644
--- a/browser/lib/markdown.js
+++ b/browser/lib/markdown.js
@@ -21,39 +21,13 @@ function createGutter (str, firstLineNumber) {
class Markdown {
constructor (options = {}) {
- let config = ConfigManager.get()
+ const config = ConfigManager.get()
const defaultOptions = {
typographer: config.preview.smartQuotes,
linkify: true,
html: true,
xhtmlOut: true,
breaks: config.preview.breaks,
- 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}`
- }
- if (langType === 'chart') {
- return `${str}`
- }
- if (langType === 'mermaid') {
- return `${str}`
- }
- return '' +
- '' + fileName + '' +
- createGutter(str, firstLineNumber) +
- '' +
- str +
- '
'
- },
sanitize: 'STRICT'
}
@@ -106,7 +80,11 @@ class Markdown {
'iframe': ['src', 'width', 'height', 'frameborder', 'allowfullscreen'],
'input': ['type', 'id', 'checked']
},
- allowedIframeHostnames: ['www.youtube.com']
+ allowedIframeHostnames: ['www.youtube.com'],
+ selfClosing: [ 'img', 'br', 'hr', 'input' ],
+ allowedSchemes: [ 'http', 'https', 'ftp', 'mailto' ],
+ allowedSchemesAppliedToAttributes: [ 'href', 'src', 'cite' ],
+ allowProtocolRelative: true
})
}
@@ -149,10 +127,50 @@ class Markdown {
}
})
this.md.use(require('markdown-it-kbd'))
-
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']})
+ this.md.use(require('markdown-it-abbr'))
+ this.md.use(require('markdown-it-sub'))
+ this.md.use(require('markdown-it-sup'))
+ this.md.use(require('./markdown-it-deflist'))
this.md.use(require('./markdown-it-frontmatter'))
+ this.md.use(require('./markdown-it-fence'), {
+ chart: token => {
+ if (token.parameters.hasOwnProperty('yaml')) {
+ token.parameters.format = 'yaml'
+ }
+
+ return `
+ ${token.fileName}
+ ${token.content}
+ `
+ },
+ flowchart: token => {
+ return `
+ ${token.fileName}
+ ${token.content}
+ `
+ },
+ mermaid: token => {
+ return `
+ ${token.fileName}
+ ${token.content}
+ `
+ },
+ sequence: token => {
+ return `
+ ${token.fileName}
+ ${token.content}
+ `
+ }
+ }, token => {
+ return `
+ ${token.fileName}
+ ${createGutter(token.content, token.firstLineNumber)}
+ ${token.content}
+ `
+ })
+
const deflate = require('markdown-it-plantuml/lib/deflate')
this.md.use(require('markdown-it-plantuml'), '', {
generateSource: function (umlCode) {
@@ -253,9 +271,12 @@ class Markdown {
this.md.renderer.render = (tokens, options, env) => {
tokens.forEach((token) => {
switch (token.type) {
- case 'heading_open':
- case 'paragraph_open':
case 'blockquote_open':
+ case 'dd_open':
+ case 'dt_open':
+ case 'heading_open':
+ case 'list_item_open':
+ case 'paragraph_open':
case 'table_open':
token.attrPush(['data-line', token.map[0]])
}
@@ -265,9 +286,6 @@ class Markdown {
}
// FIXME We should not depend on global variable.
window.md = this.md
- this.updateConfig = () => {
- config = ConfigManager.get()
- }
}
render (content) {
diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js
index e4493a80..116fdec0 100755
--- a/browser/main/Detail/MarkdownNoteDetail.js
+++ b/browser/main/Detail/MarkdownNoteDetail.js
@@ -61,11 +61,14 @@ class MarkdownNoteDetail extends React.Component {
const reversedType = this.state.editorType === 'SPLIT' ? 'EDITOR_PREVIEW' : 'SPLIT'
this.handleSwitchMode(reversedType)
})
+ ee.on('hotkey:deletenote', this.handleDeleteNote.bind(this))
ee.on('code:generate-toc', this.generateToc)
}
componentWillReceiveProps (nextProps) {
- if (nextProps.note.key !== this.props.note.key && !this.state.isMovingNote) {
+ const isNewNote = nextProps.note.key !== this.props.note.key
+ const hasDeletedTags = nextProps.note.tags.length < this.props.note.tags.length
+ if (!this.state.isMovingNote && (isNewNote || hasDeletedTags)) {
if (this.saveQueue != null) this.saveNow()
this.setState({
note: Object.assign({}, nextProps.note)
@@ -91,7 +94,7 @@ class MarkdownNoteDetail extends React.Component {
handleUpdateContent () {
const { note } = this.state
note.content = this.refs.content.value
- note.title = markdown.strip(striptags(findNoteTitle(note.content)))
+ note.title = markdown.strip(striptags(findNoteTitle(note.content, this.props.config.editor.enableFrontMatterTitle, this.props.config.editor.frontMatterTitleField)))
this.updateNote(note)
}
@@ -293,9 +296,33 @@ class MarkdownNoteDetail extends React.Component {
})
}
+ handleDeleteNote () {
+ this.handleTrashButtonClick()
+ }
+
+ handleClearTodo () {
+ const { note } = this.state
+ const splitted = note.content.split('\n')
+
+ const clearTodoContent = splitted.map((line) => {
+ const trimmedLine = line.trim()
+ if (trimmedLine.match(/\[x\]/i)) {
+ return line.replace(/\[x\]/i, '[ ]')
+ } else {
+ return line
+ }
+ }).join('\n')
+
+ note.content = clearTodoContent
+ this.refs.content.setValue(note.content)
+
+ this.updateNote(note)
+ }
+
renderEditor () {
const { config, ignorePreviewPointerEvents } = this.props
const { note } = this.state
+
if (this.state.editorType === 'EDITOR_PREVIEW') {
return
-
+ this.handleClearTodo(e)} percentageOfTodo={getTodoPercentageOfCompleted(note.content)} />
this.handleSwitchMode(e)} editorType={editorType} />
diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js
index 9356a02c..afd81102 100644
--- a/browser/main/Detail/SnippetNoteDetail.js
+++ b/browser/main/Detail/SnippetNoteDetail.js
@@ -112,7 +112,7 @@ class SnippetNoteDetail extends React.Component {
if (this.refs.tags) note.tags = this.refs.tags.value
note.description = this.refs.description.value
note.updatedAt = new Date()
- note.title = findNoteTitle(note.description)
+ note.title = findNoteTitle(note.description, false)
this.setState({
note
@@ -354,12 +354,10 @@ class SnippetNoteDetail extends React.Component {
this.refs['code-' + this.state.snippetIndex].reload()
if (this.visibleTabs.offsetWidth > this.allTabs.scrollWidth) {
- console.log('no need for arrows')
this.moveTabBarBy(0)
} else {
const lastTab = this.allTabs.lastChild
if (lastTab.offsetLeft + lastTab.offsetWidth < this.visibleTabs.offsetWidth) {
- console.log('need to scroll')
const width = this.visibleTabs.offsetWidth
const newLeft = lastTab.offsetLeft + lastTab.offsetWidth - width
this.moveTabBarBy(newLeft > 0 ? -newLeft : 0)
@@ -627,7 +625,6 @@ class SnippetNoteDetail extends React.Component {
}
focusEditor () {
- console.log('code-' + this.state.snippetIndex)
this.refs['code-' + this.state.snippetIndex].focus()
}
@@ -759,6 +756,8 @@ class SnippetNoteDetail extends React.Component {
this.handleChange(e)}
/>
diff --git a/browser/main/Detail/TagSelect.js b/browser/main/Detail/TagSelect.js
index eb160e4c..6ced475b 100644
--- a/browser/main/Detail/TagSelect.js
+++ b/browser/main/Detail/TagSelect.js
@@ -179,10 +179,10 @@ class TagSelect extends React.Component {
}
render () {
- const { value, className } = this.props
+ const { value, className, showTagsAlphabetically } = this.props
const tagList = _.isArray(value)
- ? value.map((tag) => {
+ ? (showTagsAlphabetically ? _.sortBy(value) : value).map((tag) => {
return (
(
-
-
onClick('SPLIT')}>
-

-
-
onClick('EDITOR_PREVIEW')}>
-

-
-
{i18n.__('Toggle Mode')}
-
-)
-
-ToggleModeButton.propTypes = {
- onClick: PropTypes.func.isRequired,
- editorType: PropTypes.string.Required
-}
-
-export default CSSModules(ToggleModeButton, styles)
+import PropTypes from 'prop-types'
+import React from 'react'
+import CSSModules from 'browser/lib/CSSModules'
+import styles from './ToggleModeButton.styl'
+import i18n from 'browser/lib/i18n'
+
+const ToggleModeButton = ({
+ onClick, editorType
+}) => (
+
+
onClick('SPLIT')}>
+

+
+
onClick('EDITOR_PREVIEW')}>
+

+
+
{i18n.__('Toggle Mode')}
+
+)
+
+ToggleModeButton.propTypes = {
+ onClick: PropTypes.func.isRequired,
+ editorType: PropTypes.string.Required
+}
+
+export default CSSModules(ToggleModeButton, styles)
diff --git a/browser/main/Main.js b/browser/main/Main.js
index 5fe9d493..e9d2c94d 100644
--- a/browser/main/Main.js
+++ b/browser/main/Main.js
@@ -80,7 +80,6 @@ class Main extends React.Component {
}
})
.then(data => {
- console.log(data)
store.dispatch({
type: 'ADD_STORAGE',
storage: data.storage,
@@ -297,7 +296,7 @@ class Main extends React.Component {
onMouseUp={e => this.handleMouseUp(e)}
>
{!config.isSideNavFolded &&
diff --git a/browser/main/NoteList/index.js b/browser/main/NoteList/index.js
index 30ad93c3..13117af1 100644
--- a/browser/main/NoteList/index.js
+++ b/browser/main/NoteList/index.js
@@ -56,7 +56,6 @@ class NoteList extends React.Component {
super(props)
this.selectNextNoteHandler = () => {
- console.log('fired next')
this.selectNextNote()
}
this.selectPriorNoteHandler = () => {
@@ -616,7 +615,6 @@ class NoteList extends React.Component {
.catch((err) => {
console.error('Cannot Delete note: ' + err)
})
- console.log('Notes were all deleted')
} else {
if (!confirmDeleteNote(confirmDeletion, false)) return
@@ -636,7 +634,6 @@ class NoteList extends React.Component {
})
})
AwsMobileAnalyticsConfig.recordDynamicCustomEvent('EDIT_NOTE')
- console.log('Notes went to trash')
})
.catch((err) => {
console.error('Notes could not go to trash: ' + err)
@@ -996,6 +993,7 @@ class NoteList extends React.Component {
folderName={this.getNoteFolder(note).name}
storageName={this.getNoteStorage(note).name}
viewType={viewType}
+ showTagsAlphabetically={config.ui.showTagsAlphabetically}
/>
)
}
diff --git a/browser/main/SideNav/index.js b/browser/main/SideNav/index.js
index 977a8fb5..3e18095e 100644
--- a/browser/main/SideNav/index.js
+++ b/browser/main/SideNav/index.js
@@ -18,6 +18,11 @@ import TagButton from './TagButton'
import {SortableContainer} from 'react-sortable-hoc'
import i18n from 'browser/lib/i18n'
import context from 'browser/lib/context'
+import { remote } from 'electron'
+
+function matchActiveTags (tags, activeTags) {
+ return _.every(activeTags, v => tags.indexOf(v) >= 0)
+}
class SideNav extends React.Component {
// TODO: should not use electron stuff v0.7
@@ -30,6 +35,52 @@ class SideNav extends React.Component {
EventEmitter.off('side:preferences', this.handleMenuButtonClick)
}
+ deleteTag (tag) {
+ const selectedButton = remote.dialog.showMessageBox(remote.getCurrentWindow(), {
+ ype: 'warning',
+ message: i18n.__('Confirm tag deletion'),
+ detail: i18n.__('This will permanently remove this tag.'),
+ buttons: [i18n.__('Confirm'), i18n.__('Cancel')]
+ })
+
+ if (selectedButton === 0) {
+ const { data, dispatch, location, params } = this.props
+
+ const notes = data.noteMap
+ .map(note => note)
+ .filter(note => note.tags.indexOf(tag) !== -1)
+ .map(note => {
+ note = Object.assign({}, note)
+ note.tags = note.tags.slice()
+
+ note.tags.splice(note.tags.indexOf(tag), 1)
+
+ return note
+ })
+
+ Promise
+ .all(notes.map(note => dataApi.updateNote(note.storage, note.key, note)))
+ .then(updatedNotes => {
+ updatedNotes.forEach(note => {
+ dispatch({
+ type: 'UPDATE_NOTE',
+ note
+ })
+ })
+
+ if (location.pathname.match('/tags')) {
+ const tags = params.tagname.split(' ')
+ const index = tags.indexOf(tag)
+ if (index !== -1) {
+ tags.splice(index, 1)
+
+ this.context.router.push(`/tags/${tags.map(tag => encodeURIComponent(tag)).join(' ')}`)
+ }
+ }
+ })
+ }
+ }
+
handleMenuButtonClick (e) {
openModal(PreferencesModal)
}
@@ -44,6 +95,17 @@ class SideNav extends React.Component {
router.push('/starred')
}
+ handleTagContextMenu (e, tag) {
+ const menu = []
+
+ menu.push({
+ label: i18n.__('Delete Tag'),
+ click: this.deleteTag.bind(this, tag)
+ })
+
+ context.popup(menu)
+ }
+
handleToggleButtonClick (e) {
const { dispatch, config } = this.props
@@ -144,12 +206,20 @@ class SideNav extends React.Component {
tagListComponent () {
const { data, location, config } = this.props
- const relatedTags = this.getRelatedTags(this.getActiveTags(location.pathname), data.noteMap)
+ const activeTags = this.getActiveTags(location.pathname)
+ const relatedTags = this.getRelatedTags(activeTags, data.noteMap)
let tagList = _.sortBy(data.tagNoteMap.map(
(tag, name) => ({ name, size: tag.size, related: relatedTags.has(name) })
- ), ['name']).filter(
+ ).filter(
tag => tag.size > 0
- )
+ ), ['name'])
+ if (config.ui.enableLiveNoteCounts && activeTags.length !== 0) {
+ const notesTags = data.noteMap.map(note => note.tags)
+ tagList = tagList.map(tag => {
+ tag.size = notesTags.filter(tags => tags.includes(tag.name) && matchActiveTags(tags, activeTags)).length
+ return tag
+ })
+ }
if (config.sortTagsBy === 'COUNTER') {
tagList = _.sortBy(tagList, item => (0 - item.size))
}
@@ -165,6 +235,7 @@ class SideNav extends React.Component {
name={tag.name}
handleClickTagListItem={this.handleClickTagListItem.bind(this)}
handleClickNarrowToTag={this.handleClickNarrowToTag.bind(this)}
+ handleContextMenu={this.handleTagContextMenu.bind(this)}
isActive={this.getTagActive(location.pathname, tag.name)}
isRelated={tag.related}
key={tag.name}
@@ -247,7 +318,6 @@ class SideNav extends React.Component {
.catch((err) => {
console.error('Cannot Delete note: ' + err)
})
- console.log('Trash emptied')
}
handleFilterButtonContextMenu (event) {
diff --git a/browser/main/lib/AwsMobileAnalyticsConfig.js b/browser/main/lib/AwsMobileAnalyticsConfig.js
index 1ef4f8da..e4a21a92 100644
--- a/browser/main/lib/AwsMobileAnalyticsConfig.js
+++ b/browser/main/lib/AwsMobileAnalyticsConfig.js
@@ -45,7 +45,6 @@ function initAwsMobileAnalytics () {
if (getSendEventCond()) return
AWS.config.credentials.get((err) => {
if (!err) {
- console.log('Cognito Identity ID: ' + AWS.config.credentials.identityId)
recordDynamicCustomEvent('APP_STARTED')
recordStaticCustomEvent()
}
@@ -58,7 +57,7 @@ function recordDynamicCustomEvent (type, options = {}) {
mobileAnalyticsClient.recordEvent(type, options)
} catch (analyticsError) {
if (analyticsError instanceof ReferenceError) {
- console.log(analyticsError.name + ': ' + analyticsError.message)
+ console.error(analyticsError.name + ': ' + analyticsError.message)
}
}
}
@@ -71,7 +70,7 @@ function recordStaticCustomEvent () {
})
} catch (analyticsError) {
if (analyticsError instanceof ReferenceError) {
- console.log(analyticsError.name + ': ' + analyticsError.message)
+ console.error(analyticsError.name + ': ' + analyticsError.message)
}
}
}
diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js
index 1727deb8..4cbe80a7 100644
--- a/browser/main/lib/ConfigManager.js
+++ b/browser/main/lib/ConfigManager.js
@@ -24,7 +24,8 @@ export const DEFAULT_CONFIG = {
amaEnabled: true,
hotkey: {
toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E',
- toggleMode: OSX ? 'Command + Option + M' : 'Ctrl + M'
+ toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M',
+ deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace'
},
ui: {
language: 'en',
@@ -43,11 +44,14 @@ export const DEFAULT_CONFIG = {
enableRulers: false,
rulers: [80, 120],
displayLineNumbers: true,
- switchPreview: 'BLUR', // Available value: RIGHTCLICK, BLUR
+ switchPreview: 'BLUR', // 'BLUR', 'DBL_CLICK', 'RIGHTCLICK'
+ delfaultStatus: 'PREVIEW', // 'PREVIEW', 'CODE'
scrollPastEnd: false,
- type: 'SPLIT',
+ type: 'SPLIT', // 'SPLIT', 'EDITOR_PREVIEW'
fetchUrlTitle: true,
- enableTableEditor: false
+ enableTableEditor: false,
+ enableFrontMatterTitle: true,
+ frontMatterTitleField: 'title'
},
preview: {
fontSize: '14',
@@ -60,6 +64,7 @@ export const DEFAULT_CONFIG = {
latexBlockClose: '$$',
plantUMLServerAddress: 'http://www.plantuml.com/plantuml',
scrollPastEnd: false,
+ scrollSync: true,
smartQuotes: true,
breaks: true,
smartArrows: false,
@@ -198,7 +203,7 @@ function rewriteHotkey (config) {
const keys = [...Object.keys(config.hotkey)]
keys.forEach(key => {
config.hotkey[key] = config.hotkey[key].replace(/Cmd/g, 'Command')
- config.hotkey[key] = config.hotkey[key].replace(/Opt/g, 'Alt')
+ config.hotkey[key] = config.hotkey[key].replace(/Opt\s/g, 'Option ')
})
return config
}
diff --git a/browser/main/lib/dataApi/attachmentManagement.js b/browser/main/lib/dataApi/attachmentManagement.js
index 912450c1..c193eaf2 100644
--- a/browser/main/lib/dataApi/attachmentManagement.js
+++ b/browser/main/lib/dataApi/attachmentManagement.js
@@ -529,7 +529,6 @@ function handleAttachmentLinkPaste (storageKey, noteKey, linkText) {
return modifiedLinkText
})
} else {
- console.log('One if the parameters was null -> Do nothing..')
return Promise.resolve(linkText)
}
}
diff --git a/browser/main/lib/dataApi/renameStorage.js b/browser/main/lib/dataApi/renameStorage.js
index 78242bed..3b806d1c 100644
--- a/browser/main/lib/dataApi/renameStorage.js
+++ b/browser/main/lib/dataApi/renameStorage.js
@@ -14,7 +14,6 @@ function renameStorage (key, name) {
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)
}
diff --git a/browser/main/lib/dataApi/resolveStorageData.js b/browser/main/lib/dataApi/resolveStorageData.js
index 681a102e..da41f3d0 100644
--- a/browser/main/lib/dataApi/resolveStorageData.js
+++ b/browser/main/lib/dataApi/resolveStorageData.js
@@ -31,13 +31,9 @@ function resolveStorageData (storageCache) {
const version = parseInt(storage.version, 10)
if (version >= 1) {
- if (version > 1) {
- console.log('The repository version is newer than one of current app.')
- }
return Promise.resolve(storage)
}
- console.log('Transform Legacy storage', storage.path)
return migrateFromV6Storage(storage.path)
.then(() => storage)
}
diff --git a/browser/main/lib/dataApi/resolveStorageNotes.js b/browser/main/lib/dataApi/resolveStorageNotes.js
index fa3f19ae..9da27248 100644
--- a/browser/main/lib/dataApi/resolveStorageNotes.js
+++ b/browser/main/lib/dataApi/resolveStorageNotes.js
@@ -9,7 +9,7 @@ function resolveStorageNotes (storage) {
notePathList = sander.readdirSync(notesDirPath)
} catch (err) {
if (err.code === 'ENOENT') {
- console.log(notesDirPath, ' doesn\'t exist.')
+ console.error(notesDirPath, ' doesn\'t exist.')
sander.mkdirSync(notesDirPath)
} else {
console.warn('Failed to find note dir', notesDirPath, err)
diff --git a/browser/main/lib/dataApi/toggleStorage.js b/browser/main/lib/dataApi/toggleStorage.js
index dbb625c3..246d85ef 100644
--- a/browser/main/lib/dataApi/toggleStorage.js
+++ b/browser/main/lib/dataApi/toggleStorage.js
@@ -12,7 +12,6 @@ function toggleStorage (key, isOpen) {
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)
}
diff --git a/browser/main/lib/eventEmitter.js b/browser/main/lib/eventEmitter.js
index de08f078..1276545b 100644
--- a/browser/main/lib/eventEmitter.js
+++ b/browser/main/lib/eventEmitter.js
@@ -14,7 +14,6 @@ function once (name, listener) {
}
function emit (name, ...args) {
- console.log(name)
remote.getCurrentWindow().webContents.send(name, ...args)
}
diff --git a/browser/main/lib/ipcClient.js b/browser/main/lib/ipcClient.js
index 0c916617..c06296b5 100644
--- a/browser/main/lib/ipcClient.js
+++ b/browser/main/lib/ipcClient.js
@@ -14,14 +14,13 @@ nodeIpc.connectTo(
path.join(app.getPath('userData'), 'boostnote.service'),
function () {
nodeIpc.of.node.on('error', function (err) {
- console.log(err)
+ console.error(err)
})
nodeIpc.of.node.on('connect', function () {
- console.log('Connected successfully')
ipcRenderer.send('config-renew', {config: ConfigManager.get()})
})
nodeIpc.of.node.on('disconnect', function () {
- console.log('disconnected')
+ return
})
}
)
diff --git a/browser/main/lib/shortcut.js b/browser/main/lib/shortcut.js
index a6f33196..93e33c9b 100644
--- a/browser/main/lib/shortcut.js
+++ b/browser/main/lib/shortcut.js
@@ -3,5 +3,8 @@ import ee from 'browser/main/lib/eventEmitter'
module.exports = {
'toggleMode': () => {
ee.emit('topbar:togglemodebutton')
+ },
+ 'deleteNote': () => {
+ ee.emit('hotkey:deletenote')
}
}
diff --git a/browser/main/modals/PreferencesModal/Crowdfunding.js b/browser/main/modals/PreferencesModal/Crowdfunding.js
index f342fb76..f6389cd8 100644
--- a/browser/main/modals/PreferencesModal/Crowdfunding.js
+++ b/browser/main/modals/PreferencesModal/Crowdfunding.js
@@ -23,21 +23,29 @@ class Crowdfunding extends React.Component {
return (
{i18n.__('Crowdfunding')}
-
{i18n.__('Dear Boostnote users,')}
-
{i18n.__('Thank you for using Boostnote!')}
-
{i18n.__('Boostnote is used in about 200 different countries and regions by an awesome community of developers.')}
-
{i18n.__('To support our growing userbase, and satisfy community expectations,')}
-
{i18n.__('we would like to invest more time and resources in this project.')}
+
{i18n.__('We launched IssueHunt which is an issue-based crowdfunding / sourcing platform for open source projects.')}
+
{i18n.__('Anyone can put a bounty on not only a bug but also on OSS feature requests listed on IssueHunt. Collected funds will be distributed to project owners and contributors.')}
-
{i18n.__('If you use Boostnote and see its potential, help us out by supporting the project on OpenCollective!')}
+
{i18n.__('### Sustainable Open Source Ecosystem')}
+
{i18n.__('We discussed about open-source ecosystem and IssueHunt concept with the Boostnote team repeatedly. We actually also discussed with Matz who father of Ruby.')}
+
{i18n.__('The original reason why we made IssueHunt was to reward our contributors of Boostnote project. We’ve got tons of Github stars and hundred of contributors in two years.')}
+
{i18n.__('We thought that it will be nice if we can pay reward for our contributors.')}
-
{i18n.__('Thanks,')}
+
{i18n.__('### We believe Meritocracy')}
+
{i18n.__('We think developers who has skill and did great things must be rewarded properly.')}
+
{i18n.__('OSS projects are used in everywhere on the internet, but no matter how they great, most of owners of those projects need to have another job to sustain their living.')}
+
{i18n.__('It sometimes looks like exploitation.')}
+
{i18n.__('We’ve realized IssueHunt could enhance sustainability of open-source ecosystem.')}
+
+
{i18n.__('As same as issues of Boostnote are already funded on IssueHunt, your open-source projects can be also started funding from now.')}
+
+
{i18n.__('Thank you,')}
{i18n.__('The Boostnote Team')}
- this.handleLinkClick(e)}>{i18n.__('Support via OpenCollective')}
+ this.handleLinkClick(e)}>{i18n.__('See IssueHunt')}
)
diff --git a/browser/main/modals/PreferencesModal/HotkeyTab.js b/browser/main/modals/PreferencesModal/HotkeyTab.js
index 1c40a13a..7ad6f606 100644
--- a/browser/main/modals/PreferencesModal/HotkeyTab.js
+++ b/browser/main/modals/PreferencesModal/HotkeyTab.js
@@ -28,10 +28,20 @@ class HotkeyTab extends React.Component {
}})
}
this.handleSettingError = (err) => {
- this.setState({keymapAlert: {
- type: 'error',
- message: err.message != null ? err.message : i18n.__('An error occurred!')
- }})
+ if (
+ this.state.config.hotkey.toggleMain === '' ||
+ this.state.config.hotkey.toggleMode === ''
+ ) {
+ this.setState({keymapAlert: {
+ type: 'success',
+ message: i18n.__('Successfully applied!')
+ }})
+ } else {
+ this.setState({keymapAlert: {
+ type: 'error',
+ message: err.message != null ? err.message : i18n.__('An error occurred!')
+ }})
+ }
}
this.oldHotkey = this.state.config.hotkey
ipc.addListener('APP_SETTING_DONE', this.handleSettingDone)
@@ -68,7 +78,8 @@ class HotkeyTab extends React.Component {
const { config } = this.state
config.hotkey = {
toggleMain: this.refs.toggleMain.value,
- toggleMode: this.refs.toggleMode.value
+ toggleMode: this.refs.toggleMode.value,
+ deleteNote: this.refs.deleteNote.value
}
this.setState({
config
@@ -127,6 +138,17 @@ class HotkeyTab extends React.Component {
/>
+
+
{i18n.__('Delete Note')}
+
+ this.handleHotkeyChange(e)}
+ ref='deleteNote'
+ value={config.hotkey.deleteNote}
+ type='text'
+ />
+
+
this.handleHintToggleButtonClick(e)}
diff --git a/browser/main/modals/PreferencesModal/InfoTab.js b/browser/main/modals/PreferencesModal/InfoTab.js
index 1b2d55bb..a6acc963 100644
--- a/browser/main/modals/PreferencesModal/InfoTab.js
+++ b/browser/main/modals/PreferencesModal/InfoTab.js
@@ -84,7 +84,7 @@ class InfoTab extends React.Component {
>{i18n.__('GitHub')}
- this.handleLinkClick(e)}
>{i18n.__('Blog')}
diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js
index 8bc81b43..32a71d72 100644
--- a/browser/main/modals/PreferencesModal/UiTab.js
+++ b/browser/main/modals/PreferencesModal/UiTab.js
@@ -72,6 +72,9 @@ class UiTab extends React.Component {
showCopyNotification: this.refs.showCopyNotification.checked,
confirmDeletion: this.refs.confirmDeletion.checked,
showOnlyRelatedTags: this.refs.showOnlyRelatedTags.checked,
+ showTagsAlphabetically: this.refs.showTagsAlphabetically.checked,
+ saveTagsAlphabetically: this.refs.saveTagsAlphabetically.checked,
+ enableLiveNoteCounts: this.refs.enableLiveNoteCounts.checked,
disableDirectWrite: this.refs.uiD2w != null
? this.refs.uiD2w.checked
: false
@@ -90,7 +93,9 @@ class UiTab extends React.Component {
snippetDefaultLanguage: this.refs.editorSnippetDefaultLanguage.value,
scrollPastEnd: this.refs.scrollPastEnd.checked,
fetchUrlTitle: this.refs.editorFetchUrlTitle.checked,
- enableTableEditor: this.refs.enableTableEditor.checked
+ enableTableEditor: this.refs.enableTableEditor.checked,
+ enableFrontMatterTitle: this.refs.enableFrontMatterTitle.checked,
+ frontMatterTitleField: this.refs.frontMatterTitleField.value
},
preview: {
fontSize: this.refs.previewFontSize.value,
@@ -103,6 +108,7 @@ class UiTab extends React.Component {
latexBlockClose: this.refs.previewLatexBlockClose.value,
plantUMLServerAddress: this.refs.previewPlantUMLServerAddress.value,
scrollPastEnd: this.refs.previewScrollPastEnd.checked,
+ scrollSync: this.refs.previewScrollSync.checked,
smartQuotes: this.refs.previewSmartQuotes.checked,
breaks: this.refs.previewBreaks.checked,
smartArrows: this.refs.previewSmartArrows.checked,
@@ -227,16 +233,6 @@ class UiTab extends React.Component {
-
-
-
-
-
-
{
global.process.platform === 'win32'
?
@@ -282,6 +268,64 @@ class UiTab extends React.Component {
: null
}
+
+ Tags
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Editor
@@ -439,6 +483,31 @@ class UiTab extends React.Component {
+
+
+ {i18n.__('Front matter title field')}
+
+
+ this.handleUIChange(e)}
+ type='text'
+ />
+
+
+
+
+
+
+
+
+
+