diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 1f5ada57..5554c4b8 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,15 +1,25 @@ # Current behavior # Expected behavior + + # Steps to reproduce + + 1. 2. 3. 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 4c195797..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) }) } } @@ -250,7 +257,7 @@ class MarkdownEditor extends React.Component { : 'codeEditor--hide' } ref='code' - mode='GitHub Flavored Markdown' + mode='Boost Flavored Markdown' value={value} theme={config.editor.theme} keyMap={config.editor.keyMap} @@ -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/MarkdownEditor.styl b/browser/components/MarkdownEditor.styl index 13455e5d..c8fe2e49 100644 --- a/browser/components/MarkdownEditor.styl +++ b/browser/components/MarkdownEditor.styl @@ -16,7 +16,6 @@ .preview display block absolute top bottom left right - z-index 100 background-color white height 100% width 100% diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index b3d59b47..0af16e31 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -16,7 +16,6 @@ import convertModeName from 'browser/lib/convertModeName' 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 context from 'browser/lib/context' import i18n from 'browser/lib/i18n' import fs from 'fs' @@ -80,7 +79,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 +86,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 +147,8 @@ body p { display: none } } + +${allowCustomCSS ? customCSS : ''} ` } @@ -325,9 +330,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, @@ -425,6 +428,7 @@ export default class MarkdownPreview extends React.Component { case 'dark': case 'solarized-dark': case 'monokai': + case 'dracula': return scrollBarDarkStyle default: return scrollBarStyle @@ -526,7 +530,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() @@ -732,7 +737,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 } @@ -753,7 +757,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 } @@ -766,12 +769,18 @@ export default class MarkdownPreview extends React.Component { try { const chartConfig = 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 } @@ -855,6 +864,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 ddc9d7e0..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 @@ -145,7 +151,7 @@ class MarkdownSplitEditor extends React.Component { styleName='codeEditor' ref='code' width={this.state.codeEditorWidthInPercent + '%'} - mode='GitHub Flavored Markdown' + mode='Boost Flavored Markdown' value={value} theme={config.editor.theme} keyMap={config.editor.keyMap} @@ -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 }) => (
{percentageOfTodo}%
onClearCheckboxClick(e)}>clear
+${str}`
- }
- if (langType === 'sequence') {
- return `${str}`
- }
- if (langType === 'chart') {
- return `${str}`
- }
- if (langType === 'mermaid') {
- return `${str}`
- }
- return '' +
- '' + fileName + '' +
- createGutter(str, firstLineNumber) +
- '' +
- str +
- ''
- },
sanitize: 'STRICT'
}
@@ -105,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
})
}
@@ -152,6 +131,39 @@ class Markdown {
this.md.use(require('markdown-it-admonition'), {types: ['note', 'hint', 'attention', 'caution', 'danger', 'error']})
this.md.use(require('./markdown-it-frontmatter'))
+ this.md.use(require('./markdown-it-fence'), {
+ chart: token => {
+ return `+ ${token.fileName} +` + }, + flowchart: token => { + return `${token.content}+
+ ${token.fileName} +` + }, + mermaid: token => { + return `${token.content}+
+ ${token.fileName} +` + }, + sequence: token => { + return `${token.content}+
+ ${token.fileName} +` + } + }, token => { + return `${token.content}+
+ ${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) {
@@ -223,7 +235,11 @@ class Markdown {
if (!liToken.attrs) {
liToken.attrs = []
}
- liToken.attrs.push(['class', 'taskListItem'])
+ if (config.preview.lineThroughCheckbox) {
+ liToken.attrs.push(['class', `taskListItem${match[1] !== ' ' ? ' checked' : ''}`])
+ } else {
+ liToken.attrs.push(['class', 'taskListItem'])
+ }
}
content = ``
}
diff --git a/browser/main/Detail/Detail.styl b/browser/main/Detail/Detail.styl
index 49a634f3..1b7bd606 100644
--- a/browser/main/Detail/Detail.styl
+++ b/browser/main/Detail/Detail.styl
@@ -23,7 +23,7 @@ body[data-theme="dark"]
border-left 1px solid $ui-dark-borderColor
.empty-message
color $ui-dark-inactive-text-color
-
+
body[data-theme="solarized-dark"]
.root
background-color $ui-solarized-dark-noteDetail-backgroundColor
@@ -37,3 +37,10 @@ body[data-theme="monokai"]
border-left 1px solid $ui-monokai-borderColor
.empty-message
color $ui-monokai-text-color
+
+body[data-theme="dracula"]
+ .root
+ background-color $ui-dracula-noteDetail-backgroundColor
+ border-left 1px solid $ui-dracula-borderColor
+ .empty-message
+ color $ui-dracula-text-color
\ No newline at end of file
diff --git a/browser/main/Detail/FolderSelect.styl b/browser/main/Detail/FolderSelect.styl
index cfdc2734..fe045e3a 100644
--- a/browser/main/Detail/FolderSelect.styl
+++ b/browser/main/Detail/FolderSelect.styl
@@ -36,7 +36,7 @@
height 34px
width 20px
line-height 34px
-
+
.search-input
vertical-align middle
position relative
@@ -71,7 +71,7 @@
overflow ellipsis
cursor pointer
&:hover
- background-color $ui-button--hover-backgroundColor
+ background-color $ui-button--hover-backgroundColor
.search-optionList-item--active
@extend .search-optionList-item
@@ -159,3 +159,29 @@ body[data-theme="monokai"]
color $ui-monokai-button--active-color
.search-optionList-item-name-surfix
color $ui-monokai-inactive-text-color
+
+body[data-theme="dracula"]
+ .root
+ color $ui-dracula-text-color
+ &:hover
+ color #f8f8f2
+ background-color $ui-dark-button--hover-backgroundColor
+ border-color $ui-dracula-borderColor
+
+ .search-optionList
+ color #f8f8f2
+ border-color $ui-dracula-borderColor
+ background-color $ui-dracula-button-backgroundColor
+
+ .search-optionList-item
+ &:hover
+ background-color lighten($ui-dracula-button--hover-backgroundColor, 15%)
+
+ .search-optionList-item--active
+ background-color $ui-dracula-button--active-backgroundColor
+ color $ui-dracula-button--active-color
+ &:hover
+ background-color $ui-dark-button--hover-backgroundColor
+ color $ui-dracula-button--active-color
+ .search-optionList-item-name-surfix
+ color $ui-dracula-inactive-text-color
diff --git a/browser/main/Detail/InfoPanel.styl b/browser/main/Detail/InfoPanel.styl
index 2a73ca7e..1f774174 100644
--- a/browser/main/Detail/InfoPanel.styl
+++ b/browser/main/Detail/InfoPanel.styl
@@ -257,3 +257,43 @@ body[data-theme="monokai"]
color $ui-dark-inactive-text-color
&:hover
color $ui-monokai-text-color
+
+body[data-theme="dracula"]
+ .control-infoButton-panel
+ background-color $ui-dracula-noteList-backgroundColor
+
+ .control-infoButton-panel-trash
+ background-color $ui-dracula-noteList-backgroundColor
+
+ .modification-date
+ color $ui-dracula-text-color
+
+ .modification-date-desc
+ color $ui-inactive-text-color
+
+ .infoPanel-defaul-count
+ color $ui-dracula-text-color
+
+ .infoPanel-sub-count
+ color $ui-inactive-text-color
+
+ .infoPanel-default
+ color $ui-dracula-text-color
+
+ .infoPanel-sub
+ color $ui-inactive-text-color
+
+ .infoPanel-noteLink
+ background-color alpha($ui-dracula-borderColor, 20%)
+ color $ui-dracula-text-color
+
+ [id=export-wrap]
+ button
+ color $ui-dark-inactive-text-color
+ &:hover
+ background-color alpha($ui-dracula-borderColor, 20%)
+ color $ui-dracula-text-color
+ p
+ color $ui-dark-inactive-text-color
+ &:hover
+ color $ui-dracula-text-color
\ No newline at end of file
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 {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')}