diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 5554c4b8..8285bec5 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -5,19 +5,19 @@ Let us know what is currently happening. Please include some **screenshots** with the **developer tools** open (console tab) when you report a bug. -If your issue is regarding Boostnote mobile, please open an issue in the Boostnote Mobile repo ๐Ÿ‘‰ https://github.com/BoostIO/boostnote-mobile. +If your issue is regarding the new Boost Note.next, please open an issue in the new repo ๐Ÿ‘‰ https://github.com/BoostIO/BoostNote.next/issues. --> # Expected behavior # Steps to reproduce 1. @@ -26,8 +26,8 @@ Please be thorough, issues we can reproduce are easier to fix! # Environment -- Version : -- OS Version and name : +- Boostnote version: +- OS version and name: + ## Description + ## Issue fixed + @@ -20,6 +23,7 @@ your PR will be reviewed faster if we know exactly what it does. Change :white_circle: to :radio_button: in all the options that apply --> + ## Type of changes - :white_circle: Bug fix (Change that fixed an issue) @@ -34,3 +38,5 @@ Change :white_circle: to :radio_button: in all the options that apply - :white_circle: I have written test for my code and it has been tested - :white_circle: All existing tests have been passed - :white_circle: I have attached a screenshot/video to visualize my change if possible +- :white_circle: This PR will modify the UI or affects the UX +- :white_circle: This PR will add/update/delete a keybinding diff --git a/browser/components/MarkdownEditor.js b/browser/components/MarkdownEditor.js index a8b88891..d07ffb0e 100644 --- a/browser/components/MarkdownEditor.js +++ b/browser/components/MarkdownEditor.js @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ import PropTypes from 'prop-types' import React from 'react' import CSSModules from 'browser/lib/CSSModules' @@ -29,20 +30,23 @@ class MarkdownEditor extends React.Component { isLocked: props.isLocked } - this.lockEditorCode = () => this.handleLockEditor() + this.lockEditorCode = this.handleLockEditor.bind(this) + this.focusEditor = this.focusEditor.bind(this) + + this.previewRef = React.createRef() } componentDidMount() { this.value = this.refs.code.value eventEmitter.on('editor:lock', this.lockEditorCode) - eventEmitter.on('editor:focus', this.focusEditor.bind(this)) + eventEmitter.on('editor:focus', this.focusEditor) } componentDidUpdate() { this.value = this.refs.code.value } - componentWillReceiveProps(props) { + UNSAFE_componentWillReceiveProps(props) { if (props.value !== this.props.value) { this.queueRendering(props.value) } @@ -51,7 +55,7 @@ class MarkdownEditor extends React.Component { componentWillUnmount() { this.cancelQueue() eventEmitter.off('editor:lock', this.lockEditorCode) - eventEmitter.off('editor:focus', this.focusEditor.bind(this)) + eventEmitter.off('editor:focus', this.focusEditor) } focusEditor() { @@ -60,6 +64,9 @@ class MarkdownEditor extends React.Component { status: 'CODE' }, () => { + if (this.refs.code == null) { + return + } this.refs.code.focus() } ) @@ -104,7 +111,7 @@ class MarkdownEditor extends React.Component { if (newStatus === 'CODE') { this.refs.code.focus() } else { - this.refs.preview.focus() + this.previewRef.current.focus() } eventEmitter.emit('topbar:togglelockbutton', this.state.status) @@ -131,8 +138,8 @@ class MarkdownEditor extends React.Component { status: 'PREVIEW' }, () => { - this.refs.preview.focus() - this.refs.preview.scrollToRow(cursorPosition.line) + this.previewRef.current.focus() + this.previewRef.current.scrollToRow(cursorPosition.line) } ) eventEmitter.emit('topbar:togglelockbutton', this.state.status) @@ -379,6 +386,7 @@ class MarkdownEditor extends React.Component { RTL={RTL} /> this.handleContextMenu(e)} onDoubleClick={e => this.handleDoubleClick(e)} tabIndex='0' diff --git a/browser/components/MarkdownPreview.js b/browser/components/MarkdownPreview.js index 4306b0cf..d75d8f6f 100755 --- a/browser/components/MarkdownPreview.js +++ b/browser/components/MarkdownPreview.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types' import React from 'react' +import { connect } from 'react-redux' import Markdown from 'browser/lib/markdown' import _ from 'lodash' import CodeMirror from 'codemirror' @@ -21,6 +22,7 @@ import { escapeHtmlCharacters } from 'browser/lib/utils' import yaml from 'js-yaml' import { render } from 'react-dom' import Carousel from 'react-image-carousel' +import { push } from 'connected-react-router' import ConfigManager from '../main/lib/ConfigManager' import uiThemes from 'browser/lib/ui-themes' import i18n from 'browser/lib/i18n' @@ -252,7 +254,7 @@ function getSourceLineNumberByElement(element) { return parent.dataset.line !== undefined ? parseInt(parent.dataset.line) : -1 } -export default class MarkdownPreview extends React.Component { +class MarkdownPreview extends React.Component { constructor(props) { super(props) @@ -1116,6 +1118,7 @@ export default class MarkdownPreview extends React.Component { e.stopPropagation() const rawHref = e.target.getAttribute('href') + const { dispatch } = this.props if (!rawHref) return // not checked href because parser will create file://... string for [empty link]() const parser = document.createElement('a') @@ -1169,6 +1172,13 @@ export default class MarkdownPreview extends React.Component { return } + const regexIsTagLink = /^:tag:([\w]+)$/ + if (regexIsTagLink.test(rawHref)) { + const tag = rawHref.match(regexIsTagLink)[1] + dispatch(push(`/tags/${encodeURIComponent(tag)}`)) + return + } + // other case this.openExternal(href) } @@ -1213,3 +1223,10 @@ MarkdownPreview.propTypes = { smartArrows: PropTypes.bool, breaks: PropTypes.bool } + +export default connect( + null, + null, + null, + { forwardRef: true } +)(MarkdownPreview) diff --git a/browser/components/MarkdownSplitEditor.js b/browser/components/MarkdownSplitEditor.js index 080322b7..de06e131 100644 --- a/browser/components/MarkdownSplitEditor.js +++ b/browser/components/MarkdownSplitEditor.js @@ -215,6 +215,7 @@ class MarkdownSplitEditor extends React.Component {
this.handleCheckboxClick(e)} diff --git a/browser/components/StorageItem.js b/browser/components/StorageItem.js index 2b796c95..c03d78c5 100644 --- a/browser/components/StorageItem.js +++ b/browser/components/StorageItem.js @@ -21,7 +21,9 @@ const FolderIcon = ({ className, color, isActive }) => { /** * @param {boolean} isActive + * @param {object} tooltipRef, * @param {Function} handleButtonClick + * @param {Function} handleMouseEnter * @param {Function} handleContextMenu * @param {string} folderName * @param {string} folderColor @@ -35,7 +37,9 @@ const FolderIcon = ({ className, color, isActive }) => { const StorageItem = ({ styles, isActive, + tooltipRef, handleButtonClick, + handleMouseEnter, handleContextMenu, folderName, folderColor, @@ -49,6 +53,7 @@ const StorageItem = ({ ) @@ -83,7 +90,9 @@ const StorageItem = ({ StorageItem.propTypes = { isActive: PropTypes.bool.isRequired, + tooltipRef: PropTypes.object, handleButtonClick: PropTypes.func, + handleMouseEnter: PropTypes.func, handleContextMenu: PropTypes.func, folderName: PropTypes.string.isRequired, folderColor: PropTypes.string, diff --git a/browser/components/StorageItem.styl b/browser/components/StorageItem.styl index e0a3c6cd..72c4901e 100644 --- a/browser/components/StorageItem.styl +++ b/browser/components/StorageItem.styl @@ -60,6 +60,7 @@ border-bottom-right-radius 2px height 34px line-height 32px + transition-property opacity .folderList-item:hover, .folderList-item--active:hover .folderList-item-tooltip diff --git a/browser/components/StorageList.styl b/browser/components/StorageList.styl index 61fe195c..474f896b 100644 --- a/browser/components/StorageList.styl +++ b/browser/components/StorageList.styl @@ -1,5 +1,7 @@ .storageList - margin-bottom 37px + absolute left right + bottom 37px + top 180px overflow-y auto .storageList-folded diff --git a/browser/finder/NoteDetail.js b/browser/finder/NoteDetail.js deleted file mode 100644 index e69de29b..00000000 diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index e71e7841..d9f5f256 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -1,3 +1,4 @@ +/* eslint-disable camelcase */ import PropTypes from 'prop-types' import React from 'react' import CSSModules from 'browser/lib/CSSModules' @@ -57,7 +58,8 @@ class MarkdownNoteDetail extends React.Component { this.dispatchTimer = null this.toggleLockButton = this.handleToggleLockButton.bind(this) - this.generateToc = () => this.handleGenerateToc() + this.generateToc = this.handleGenerateToc.bind(this) + this.handleUpdateContent = this.handleUpdateContent.bind(this) } focus() { @@ -76,7 +78,7 @@ class MarkdownNoteDetail extends React.Component { ee.on('code:generate-toc', this.generateToc) } - componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps) { const isNewNote = nextProps.note.key !== this.props.note.key const hasDeletedTags = nextProps.note.tags.length < this.props.note.tags.length @@ -392,6 +394,9 @@ class MarkdownNoteDetail extends React.Component { } handleSwitchDirection() { + if (!this.props.config.editor.rtlEnabled) { + return + } // If in split mode, hide the lock button const direction = this.state.RTL this.setState({ RTL: !direction }) @@ -436,10 +441,10 @@ class MarkdownNoteDetail extends React.Component { storageKey={note.storage} noteKey={note.key} linesHighlighted={note.linesHighlighted} - onChange={this.handleUpdateContent.bind(this)} + onChange={this.handleUpdateContent} isLocked={this.state.isLocked} ignorePreviewPointerEvents={ignorePreviewPointerEvents} - RTL={this.state.RTL} + RTL={config.editor.rtlEnabled && this.state.RTL} /> ) } else { @@ -451,9 +456,9 @@ class MarkdownNoteDetail extends React.Component { storageKey={note.storage} noteKey={note.key} linesHighlighted={note.linesHighlighted} - onChange={this.handleUpdateContent.bind(this)} + onChange={this.handleUpdateContent} ignorePreviewPointerEvents={ignorePreviewPointerEvents} - RTL={this.state.RTL} + RTL={config.editor.rtlEnabled && this.state.RTL} /> ) } @@ -536,10 +541,12 @@ class MarkdownNoteDetail extends React.Component { onClick={e => this.handleSwitchMode(e)} editorType={editorType} /> - this.handleSwitchDirection(e)} - isRTL={this.state.RTL} - /> + {this.props.config.editor.rtlEnabled && ( + this.handleSwitchDirection(e)} + isRTL={this.state.RTL} + /> + )} this.handleStarButtonClick(e)} isActive={note.isStarred} diff --git a/browser/main/Detail/ToggleDirectionButton.js b/browser/main/Detail/ToggleDirectionButton.js index 4fe41e0c..b69cb1b7 100644 --- a/browser/main/Detail/ToggleDirectionButton.js +++ b/browser/main/Detail/ToggleDirectionButton.js @@ -20,7 +20,7 @@ const ToggleDirectionButton = ({ onClick, isRTL }) => ( ToggleDirectionButton.propTypes = { onClick: PropTypes.func.isRequired, - isRTL: PropTypes.string.isRequired + isRTL: PropTypes.bool.isRequired } export default CSSModules(ToggleDirectionButton, styles) diff --git a/browser/main/Main.js b/browser/main/Main.js index 04786fc6..1c265206 100644 --- a/browser/main/Main.js +++ b/browser/main/Main.js @@ -183,6 +183,7 @@ class Main extends React.Component { 'menubar:togglemenubar', this.toggleMenuBarVisible.bind(this) ) + eventEmitter.on('dispatch:push', this.changeRoutePush.bind(this)) } componentWillUnmount() { @@ -191,9 +192,15 @@ class Main extends React.Component { 'menubar:togglemenubar', this.toggleMenuBarVisible.bind(this) ) + eventEmitter.off('dispatch:push', this.changeRoutePush.bind(this)) clearInterval(this.refreshTheme) } + changeRoutePush(event, destination) { + const { dispatch } = this.props + dispatch(push(destination)) + } + toggleMenuBarVisible() { const { config } = this.props const { ui } = config diff --git a/browser/main/SideNav/StorageItem.js b/browser/main/SideNav/StorageItem.js index a20b0df1..a152fc00 100644 --- a/browser/main/SideNav/StorageItem.js +++ b/browser/main/SideNav/StorageItem.js @@ -144,6 +144,15 @@ class StorageItem extends React.Component { } } + handleFolderMouseEnter(e, tooltipRef, isFolded) { + if (isFolded) { + const buttonEl = e.currentTarget + const tooltipEl = tooltipRef.current + + tooltipEl.style.top = buttonEl.getBoundingClientRect().y + 'px' + } + } + handleFolderButtonContextMenu(e, folder) { context.popup([ { @@ -316,6 +325,7 @@ class StorageItem extends React.Component { folder.key ) const isActive = !!location.pathname.match(folderRegex) + const tooltipRef = React.createRef(null) const noteSet = folderNoteMap.get(storage.key + '-' + folder.key) let noteCount = 0 @@ -339,7 +349,11 @@ class StorageItem extends React.Component { key={folder.key} index={index} isActive={isActive || folder.key === this.state.draggedOver} + tooltipRef={tooltipRef} handleButtonClick={e => this.handleFolderButtonClick(folder.key)(e)} + handleMouseEnter={e => + this.handleFolderMouseEnter(e, tooltipRef, isFolded) + } handleContextMenu={e => this.handleFolderButtonContextMenu(e, folder)} folderName={folder.name} folderColor={folder.color} diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index 3888215b..8516a7fc 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -32,7 +32,7 @@ export const DEFAULT_CONFIG = { hotkey: { toggleMain: OSX ? 'Command + Alt + L' : 'Super + Alt + E', toggleMode: OSX ? 'Command + Alt + M' : 'Ctrl + M', - toggleDirection: OSX ? 'Command + Alt + Right' : 'Ctrl + Right', + toggleDirection: OSX ? 'Command + Alt + Right' : 'Ctrl + Alt + Right', deleteNote: OSX ? 'Command + Shift + Backspace' : 'Ctrl + Shift + Backspace', @@ -89,7 +89,8 @@ export const DEFAULT_CONFIG = { "semi": false, "singleQuote": true }`, - deleteUnusedAttachments: true + deleteUnusedAttachments: true, + rtlEnabled: false }, preview: { fontSize: '14', diff --git a/browser/main/modals/PreferencesModal/UiTab.js b/browser/main/modals/PreferencesModal/UiTab.js index 59135132..17eb5558 100644 --- a/browser/main/modals/PreferencesModal/UiTab.js +++ b/browser/main/modals/PreferencesModal/UiTab.js @@ -133,7 +133,8 @@ class UiTab extends React.Component { .getCodeMirror() .getValue(), prettierConfig: this.prettierConfigCM.getCodeMirror().getValue(), - deleteUnusedAttachments: this.refs.deleteUnusedAttachments.checked + deleteUnusedAttachments: this.refs.deleteUnusedAttachments.checked, + rtlEnabled: this.refs.rtlEnabled.checked }, preview: { fontSize: this.refs.previewFontSize.value, @@ -861,6 +862,18 @@ class UiTab extends React.Component { )} +
+ +
diff --git a/extra_scripts/codemirror/addon/hyperlink/hyperlink.js b/extra_scripts/codemirror/addon/hyperlink/hyperlink.js index 4740bd21..4ccbbe01 100755 --- a/extra_scripts/codemirror/addon/hyperlink/hyperlink.js +++ b/extra_scripts/codemirror/addon/hyperlink/hyperlink.js @@ -1,15 +1,24 @@ -(function (mod) { - if (typeof exports === 'object' && typeof module === 'object') { // Common JS +;(function(mod) { + if (typeof exports === 'object' && typeof module === 'object') { + // Common JS mod(require('../codemirror/lib/codemirror')) - } else if (typeof define === 'function' && define.amd) { // AMD + } else if (typeof define === 'function' && define.amd) { + // AMD define(['../codemirror/lib/codemirror'], mod) - } else { // Plain browser env + } else { + // Plain browser env mod(CodeMirror) } -})(function (CodeMirror) { +})(function(CodeMirror) { 'use strict' const shell = require('electron').shell + const remote = require('electron').remote + const eventEmitter = { + emit: function() { + remote.getCurrentWindow().webContents.send.apply(null, arguments) + } + } const yOffset = 2 const macOS = global.process.platform === 'darwin' @@ -28,11 +37,16 @@ this.tooltip = document.createElement('div') this.tooltipContent = document.createElement('div') this.tooltipIndicator = document.createElement('div') - this.tooltip.setAttribute('class', 'CodeMirror-hover CodeMirror-matchingbracket CodeMirror-selected') + this.tooltip.setAttribute( + 'class', + 'CodeMirror-hover CodeMirror-matchingbracket CodeMirror-selected' + ) this.tooltip.setAttribute('cm-ignore-events', 'true') this.tooltip.appendChild(this.tooltipContent) this.tooltip.appendChild(this.tooltipIndicator) - this.tooltipContent.textContent = `${macOS ? 'Cmd(โŒ˜)' : 'Ctrl(^)'} + click to follow link` + this.tooltipContent.textContent = `${ + macOS ? 'Cmd(โŒ˜)' : 'Ctrl(^)' + } + click to follow link` this.lineDiv.addEventListener('mousedown', this.onMouseDown) this.lineDiv.addEventListener('mouseenter', this.onMouseEnter, { @@ -51,7 +65,16 @@ const className = el.className.split(' ') if (className.indexOf('cm-url') !== -1) { - const match = /^\((.*)\)|\[(.*)\]|(.*)$/.exec(el.textContent) + // multiple cm-url because of search term + const cmUrlSpans = Array.from( + el.parentNode.getElementsByClassName('cm-url') + ) + const textContent = + cmUrlSpans.length > 1 + ? cmUrlSpans.map(span => span.textContent).join('') + : el.textContent + + const match = /^\((.*)\)|\[(.*)\]|(.*)$/.exec(textContent) const url = match[1] || match[2] || match[3] // `:storage` is the value of the variable `STORAGE_FOLDER_PLACEHOLDER` defined in `browser/main/lib/dataApi/attachmentManagement` @@ -60,13 +83,90 @@ return null } + specialLinkHandler(e, rawHref, linkHash) { + const isStartWithHash = rawHref[0] === '#' + + const extractIdRegex = /file:\/\/.*main.?\w*.html#/ // file://path/to/main(.development.)html + const regexNoteInternalLink = new RegExp(`${extractIdRegex.source}(.+)`) + if (isStartWithHash || regexNoteInternalLink.test(rawHref)) { + const posOfHash = linkHash.indexOf('#') + if (posOfHash > -1) { + const extractedId = linkHash.slice(posOfHash + 1) + const targetId = mdurl.encode(extractedId) + const targetElement = document.getElementById(targetId) // this.getWindow().document.getElementById(targetId) + + if (targetElement != null) { + this.scrollTo(0, targetElement.offsetTop) + } + return + } + } + + // this will match the new uuid v4 hash and the old hash + // e.g. + // :note:1c211eb7dcb463de6490 and + // :note:7dd23275-f2b4-49cb-9e93-3454daf1af9c + const regexIsNoteLink = /^:note:([a-zA-Z0-9-]{20,36})$/ + if (regexIsNoteLink.test(linkHash)) { + eventEmitter.emit('list:jump', linkHash.replace(':note:', '')) + 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 + const regexIsLegacyNoteLink = /^(.{20})-(.{20})$/ + if (regexIsLegacyNoteLink.test(linkHash)) { + eventEmitter.emit('list:jump', linkHash.split('-')[1]) + return + } + + const regexIsTagLink = /^:tag:([\w]+)$/ + if (regexIsTagLink.test(rawHref)) { + const tag = rawHref.match(regexIsTagLink)[1] + eventEmitter.emit('dispatch:push', `/tags/${encodeURIComponent(tag)}`) + return + } + } onMouseDown(e) { const { target } = e if (!e[modifier]) { return } + // Create URL spans array used for special case "search term is hitting a link". + const cmUrlSpans = Array.from( + e.target.parentNode.getElementsByClassName('cm-url') + ) + + const innerText = + cmUrlSpans.length > 1 + ? cmUrlSpans.map(span => span.textContent).join('') + : e.target.innerText + const rawHref = innerText.trim().slice(1, -1) // get link text from markdown text + + if (!rawHref) return // not checked href because parser will create file://... string for [empty link]() + + const parser = document.createElement('a') + parser.href = rawHref + const { href, hash } = parser + + const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10 + + this.specialLinkHandler(target, rawHref, linkHash) + const url = this.getUrl(target) + + // all special cases handled --> other case if (url) { e.preventDefault() @@ -79,9 +179,11 @@ const url = this.getUrl(target) if (url) { if (e[modifier]) { - target.classList.add('CodeMirror-activeline-background', 'CodeMirror-hyperlink') - } - else { + target.classList.add( + 'CodeMirror-activeline-background', + 'CodeMirror-hyperlink' + ) + } else { target.classList.add('CodeMirror-activeline-background') } @@ -90,7 +192,10 @@ } onMouseLeave(e) { if (this.tooltip.parentElement === this.lineDiv) { - e.target.classList.remove('CodeMirror-activeline-background', 'CodeMirror-hyperlink') + e.target.classList.remove( + 'CodeMirror-activeline-background', + 'CodeMirror-hyperlink' + ) this.lineDiv.removeChild(this.tooltip) } @@ -99,8 +204,7 @@ if (this.tooltip.parentElement === this.lineDiv) { if (e[modifier]) { e.target.classList.add('CodeMirror-hyperlink') - } - else { + } else { e.target.classList.remove('CodeMirror-hyperlink') } } @@ -110,21 +214,20 @@ const b2 = this.lineDiv.getBoundingClientRect() const tdiv = this.tooltip - tdiv.style.left = (b1.left - b2.left) + 'px' + tdiv.style.left = b1.left - b2.left + 'px' this.lineDiv.appendChild(tdiv) const b3 = tdiv.getBoundingClientRect() const top = b1.top - b2.top - b3.height - yOffset if (top < 0) { - tdiv.style.top = (b1.top - b2.top + b1.height + yOffset) + 'px' - } - else { + tdiv.style.top = b1.top - b2.top + b1.height + yOffset + 'px' + } else { tdiv.style.top = top + 'px' } } } - CodeMirror.defineOption('hyperlink', true, (cm) => { + CodeMirror.defineOption('hyperlink', true, cm => { const addon = new HyperLink(cm) }) -}) \ No newline at end of file +}) diff --git a/extra_scripts/codemirror/mode/bfm/bfm.js b/extra_scripts/codemirror/mode/bfm/bfm.js index 80f797b9..d08183cd 100644 --- a/extra_scripts/codemirror/mode/bfm/bfm.js +++ b/extra_scripts/codemirror/mode/bfm/bfm.js @@ -1,10 +1,20 @@ -(function(mod) { - if (typeof exports == "object" && typeof module == "object") // CommonJS - mod(require("../codemirror/lib/codemirror"), require("../codemirror/mode/gfm/gfm"), require("../codemirror/mode/yaml-frontmatter/yaml-frontmatter")) - else if (typeof define == "function" && define.amd) // AMD - define(["../codemirror/lib/codemirror", "../codemirror/mode/gfm/gfm", "../codemirror/mode/yaml-frontmatter/yaml-frontmatter"], mod) - else // Plain browser env - mod(CodeMirror) +;(function(mod) { + if (typeof exports == 'object' && typeof module == 'object') + // CommonJS + mod( + require('../codemirror/lib/codemirror'), + require('../codemirror/mode/gfm/gfm'), + require('../codemirror/mode/yaml-frontmatter/yaml-frontmatter') + ) + else if (typeof define == 'function' && define.amd) + // AMD + define([ + '../codemirror/lib/codemirror', + '../codemirror/mode/gfm/gfm', + '../codemirror/mode/yaml-frontmatter/yaml-frontmatter' + ], mod) + // Plain browser env + else mod(CodeMirror) })(function(CodeMirror) { 'use strict' @@ -45,189 +55,208 @@ } } - CodeMirror.defineMode('bfm', function (config, baseConfig) { - baseConfig.name = 'yaml-frontmatter' - const baseMode = CodeMirror.getMode(config, baseConfig) + CodeMirror.defineMode( + 'bfm', + function(config, baseConfig) { + baseConfig.name = 'yaml-frontmatter' + const baseMode = CodeMirror.getMode(config, baseConfig) - return { - startState: function() { - return { - baseState: CodeMirror.startState(baseMode), + return { + startState: function() { + return { + baseState: CodeMirror.startState(baseMode), - basePos: 0, - baseCur: null, - overlayPos: 0, - overlayCur: null, - streamSeen: null, + basePos: 0, + baseCur: null, + overlayPos: 0, + overlayCur: null, + streamSeen: null, - fencedEndRE: null, + fencedEndRE: null, - inTable: false, - rowIndex: 0 - } - }, - copyState: function(s) { - return { - baseState: CodeMirror.copyState(baseMode, s.baseState), + inTable: false, + rowIndex: 0 + } + }, + copyState: function(s) { + return { + baseState: CodeMirror.copyState(baseMode, s.baseState), - basePos: s.basePos, - baseCur: null, - overlayPos: s.overlayPos, - overlayCur: null, + basePos: s.basePos, + baseCur: null, + overlayPos: s.overlayPos, + overlayCur: null, - fencedMode: s.fencedMode, - fencedState: s.fencedMode ? CodeMirror.copyState(s.fencedMode, s.fencedState) : null, + fencedMode: s.fencedMode, + fencedState: s.fencedMode + ? CodeMirror.copyState(s.fencedMode, s.fencedState) + : null, - fencedEndRE: s.fencedEndRE, + fencedEndRE: s.fencedEndRE, - inTable: s.inTable, - rowIndex: s.rowIndex - } - }, - token: function(stream, state) { - const initialPos = stream.pos + inTable: s.inTable, + rowIndex: s.rowIndex + } + }, + token: function(stream, state) { + const initialPos = stream.pos - if (state.fencedEndRE && stream.match(state.fencedEndRE)) { - state.fencedEndRE = null - state.fencedMode = null - state.fencedState = null + if (state.fencedEndRE && stream.match(state.fencedEndRE)) { + state.fencedEndRE = null + state.fencedMode = null + state.fencedState = null - stream.pos = initialPos - } - else { - if (state.fencedMode) { - return state.fencedMode.token(stream, state.fencedState) + stream.pos = initialPos + } else { + if (state.fencedMode) { + return state.fencedMode.token(stream, state.fencedState) + } + + const match = stream.match(fencedCodeRE, true) + if (match) { + state.fencedEndRE = new RegExp(match[1] + '+ *$') + + state.fencedMode = getMode( + match[2], + match[3], + config, + stream.lineOracle.doc.cm + ) + if (state.fencedMode) { + state.fencedState = CodeMirror.startState(state.fencedMode) + } + + stream.pos = initialPos + } + } + + if ( + stream != state.streamSeen || + Math.min(state.basePos, state.overlayPos) < stream.start + ) { + state.streamSeen = stream + state.basePos = state.overlayPos = stream.start + } + + if (stream.start == state.basePos) { + state.baseCur = baseMode.token(stream, state.baseState) + state.basePos = stream.pos + } + if (stream.start == state.overlayPos) { + stream.pos = stream.start + state.overlayCur = this.overlayToken(stream, state) + state.overlayPos = stream.pos + } + stream.pos = Math.min(state.basePos, state.overlayPos) + + if (state.overlayCur == null) { + return state.baseCur + } else if (state.baseCur != null && state.combineTokens) { + return state.baseCur + ' ' + state.overlayCur + } else { + return state.overlayCur + } + }, + overlayToken: function(stream, state) { + state.combineTokens = false + + if (state.fencedEndRE && stream.match(state.fencedEndRE)) { + state.fencedEndRE = null + state.localMode = null + state.localState = null + + return null + } + + if (state.localMode) { + return state.localMode.token(stream, state.localState) || '' } const match = stream.match(fencedCodeRE, true) if (match) { state.fencedEndRE = new RegExp(match[1] + '+ *$') - state.fencedMode = getMode(match[2], match[3], config, stream.lineOracle.doc.cm) - if (state.fencedMode) { - state.fencedState = CodeMirror.startState(state.fencedMode) + state.localMode = getMode( + match[2], + match[3], + config, + stream.lineOracle.doc.cm + ) + if (state.localMode) { + state.localState = CodeMirror.startState(state.localMode) } - stream.pos = initialPos - } - } - - if (stream != state.streamSeen || Math.min(state.basePos, state.overlayPos) < stream.start) { - state.streamSeen = stream - state.basePos = state.overlayPos = stream.start - } - - if (stream.start == state.basePos) { - state.baseCur = baseMode.token(stream, state.baseState) - state.basePos = stream.pos - } - if (stream.start == state.overlayPos) { - stream.pos = stream.start - state.overlayCur = this.overlayToken(stream, state) - state.overlayPos = stream.pos - } - stream.pos = Math.min(state.basePos, state.overlayPos) - - if (state.overlayCur == null) { - return state.baseCur - } - else if (state.baseCur != null && state.combineTokens) { - return state.baseCur + ' ' + state.overlayCur - } - else { - return state.overlayCur - } - }, - overlayToken: function(stream, state) { - state.combineTokens = false - - if (state.fencedEndRE && stream.match(state.fencedEndRE)) { - state.fencedEndRE = null - state.localMode = null - state.localState = null - - return null - } - - if (state.localMode) { - return state.localMode.token(stream, state.localState) || '' - } - - const match = stream.match(fencedCodeRE, true) - if (match) { - state.fencedEndRE = new RegExp(match[1] + '+ *$') - - state.localMode = getMode(match[2], match[3], config, stream.lineOracle.doc.cm) - if (state.localMode) { - state.localState = CodeMirror.startState(state.localMode) - } - - return null - } - - state.combineTokens = true - - if (state.inTable) { - if (stream.match(/^\|/)) { - ++state.rowIndex - - stream.skipToEnd() - - if (state.rowIndex === 1) { - return 'table table-separator' - } else if (state.rowIndex % 2 === 0) { - return 'table table-row table-row-even' - } else { - return 'table table-row table-row-odd' - } - } else { - state.inTable = false - - stream.skipToEnd() return null } - } else if (stream.match(/^\|/)) { - state.inTable = true - state.rowIndex = 0 + + state.combineTokens = true + + if (state.inTable) { + if (stream.match(/^\|/)) { + ++state.rowIndex + + stream.skipToEnd() + + if (state.rowIndex === 1) { + return 'table table-separator' + } else if (state.rowIndex % 2 === 0) { + return 'table table-row table-row-even' + } else { + return 'table table-row table-row-odd' + } + } else { + state.inTable = false + + stream.skipToEnd() + return null + } + } else if (stream.match(/^\|/)) { + state.inTable = true + state.rowIndex = 0 + + stream.skipToEnd() + return 'table table-header' + } stream.skipToEnd() - return 'table table-header' - } - - stream.skipToEnd() - return null - }, - electricChars: baseMode.electricChars, - innerMode: function(state) { - if (state.fencedMode) { - return { - mode: state.fencedMode, - state: state.fencedState + return null + }, + electricChars: baseMode.electricChars, + innerMode: function(state) { + if (state.fencedMode) { + return { + mode: state.fencedMode, + state: state.fencedState + } + } else { + return { + mode: baseMode, + state: state.baseState + } } - } else { - return { - mode: baseMode, - state: state.baseState - } - } - }, - blankLine: function(state) { - state.inTable = false + }, + blankLine: function(state) { + state.inTable = false - if (state.fencedMode) { - return state.fencedMode.blankLine && state.fencedMode.blankLine(state.fencedState) - } else { - return baseMode.blankLine(state.baseState) + if (state.fencedMode) { + return ( + state.fencedMode.blankLine && + state.fencedMode.blankLine(state.fencedState) + ) + } else { + return baseMode.blankLine(state.baseState) + } } } - } - }, 'yaml-frontmatter') + }, + 'yaml-frontmatter' + ) CodeMirror.defineMIME('text/x-bfm', 'bfm') CodeMirror.modeInfo.push({ - name: "Boost Flavored Markdown", - mime: "text/x-bfm", - mode: "bfm" + name: 'Boost Flavored Markdown', + mime: 'text/x-bfm', + mode: 'bfm' }) -}) \ No newline at end of file +}) diff --git a/extra_scripts/codemirror/mode/gfm/gfm.js b/extra_scripts/codemirror/mode/gfm/gfm.js new file mode 100644 index 00000000..9fed7591 --- /dev/null +++ b/extra_scripts/codemirror/mode/gfm/gfm.js @@ -0,0 +1,157 @@ +// CodeMirror, copyright (c) by Marijn Haverbeke and others +// Distributed under an MIT license: https://codemirror.net/LICENSE + +;(function(mod) { + if (typeof exports == 'object' && typeof module == 'object') + // CommonJS + mod( + require('../codemirror/lib/codemirror'), + require('../codemirror/mode/markdown/markdown'), + require('../codemirror/addon/mode/overlay') + ) + else if (typeof define == 'function' && define.amd) + // AMD + define([ + '../codemirror/lib/codemirror', + '../codemirror/mode/markdown/markdown', + '../codemirror/addon/mode/overlay' + ], mod) + // Plain browser env + else mod(CodeMirror) +})(function(CodeMirror) { + 'use strict' + + var urlRE = /^((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?ยซยปโ€œโ€โ€˜โ€™]))/i + + CodeMirror.defineMode( + 'gfm', + function(config, modeConfig) { + var codeDepth = 0 + function blankLine(state) { + state.code = false + return null + } + var gfmOverlay = { + startState: function() { + return { + code: false, + codeBlock: false, + ateSpace: false + } + }, + copyState: function(s) { + return { + code: s.code, + codeBlock: s.codeBlock, + ateSpace: s.ateSpace + } + }, + token: function(stream, state) { + state.combineTokens = null + + // Hack to prevent formatting override inside code blocks (block and inline) + if (state.codeBlock) { + if (stream.match(/^```+/)) { + state.codeBlock = false + return null + } + stream.skipToEnd() + return null + } + if (stream.sol()) { + state.code = false + } + if (stream.sol() && stream.match(/^```+/)) { + stream.skipToEnd() + state.codeBlock = true + return null + } + // If this block is changed, it may need to be updated in Markdown mode + if (stream.peek() === '`') { + stream.next() + var before = stream.pos + stream.eatWhile('`') + var difference = 1 + stream.pos - before + if (!state.code) { + codeDepth = difference + state.code = true + } else { + if (difference === codeDepth) { + // Must be exact + state.code = false + } + } + return null + } else if (state.code) { + stream.next() + return null + } + // Check if space. If so, links can be formatted later on + if (stream.eatSpace()) { + state.ateSpace = true + return null + } + if (stream.sol() || state.ateSpace) { + state.ateSpace = false + if (modeConfig.gitHubSpice !== false) { + if ( + stream.match( + /^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?=.{0,6}\d)(?:[a-f0-9]{7,40}\b)/ + ) + ) { + // User/Project@SHA + // User@SHA + // SHA + state.combineTokens = true + return 'link' + } else if ( + stream.match( + /^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/ + ) + ) { + // User/Project#Num + // User#Num + // #Num + state.combineTokens = true + return 'link' + } + } + } + if ( + stream.match(urlRE) && + stream.string.slice(stream.start - 2, stream.start) != '](' && + (stream.start == 0 || + /\W/.test(stream.string.charAt(stream.start - 1))) + ) { + // URLs + // Taken from http://daringfireball.net/2010/07/improved_regex_for_matching_urls + // And then (issue #1160) simplified to make it not crash the Chrome Regexp engine + // And then limited url schemes to the CommonMark list, so foo:bar isn't matched as a URL + state.combineTokens = true + return 'link' + } + stream.next() + return null + }, + blankLine: blankLine + } + + var markdownConfig = { + taskLists: true, + strikethrough: true, + emoji: true + } + for (var attr in modeConfig) { + markdownConfig[attr] = modeConfig[attr] + } + markdownConfig.name = 'markdown' + return CodeMirror.overlayMode( + CodeMirror.getMode(config, markdownConfig), + gfmOverlay + ) + }, + 'markdown' + ) + + CodeMirror.defineMIME('text/x-gfm', 'gfm') +}) diff --git a/lib/main.development.html b/lib/main.development.html index 63e50af1..900c66c7 100644 --- a/lib/main.development.html +++ b/lib/main.development.html @@ -108,12 +108,12 @@ - + diff --git a/lib/main.production.html b/lib/main.production.html index aea19e3c..05d80345 100644 --- a/lib/main.production.html +++ b/lib/main.production.html @@ -104,12 +104,12 @@ - + diff --git a/package.json b/package.json index 75d12211..df26a586 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "boost", "productName": "Boostnote", - "version": "0.15.0", + "version": "0.15.2", "main": "index.js", "description": "Boostnote", "license": "GPL-3.0", diff --git a/readme.md b/readme.md index c8f6e6a9..b9870565 100644 --- a/readme.md +++ b/readme.md @@ -1,4 +1,4 @@ -> [We've launched desktop app of the new Boost Note now. We'll release its mobile app too in January 2020.](https://github.com/BoostIO/BoostNote.next) +> [We've launched desktop and mobile app of the new Boost Note now.](https://github.com/BoostIO/BoostNote.next) ![Boostnote app screenshot](./resources/repository/top.png) diff --git a/yarn.lock b/yarn.lock index f73d0d21..27221ff9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5987,8 +5987,9 @@ locate-path@^3.0.0: path-exists "^3.0.0" lodash-es@^4.2.1: - version "4.17.10" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05" + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" + integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== lodash-move@^1.1.1: version "1.1.1" @@ -6001,9 +6002,10 @@ lodash._getnative@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U= -lodash._reinterpolate@~3.0.0: +lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" + integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= lodash.clonedeep@^4.5.0: version "4.5.0" @@ -6075,8 +6077,9 @@ lodash.merge@^4.6.0: resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" lodash.mergewith@^4.6.0: - version "4.6.1" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== lodash.some@^4.5.1: version "4.6.0" @@ -6087,17 +6090,19 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" lodash.template@^4.2.2, lodash.template@^4.3.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== dependencies: - lodash._reinterpolate "~3.0.0" + lodash._reinterpolate "^3.0.0" lodash.templatesettings "^4.0.0" lodash.templatesettings@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316" + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== dependencies: - lodash._reinterpolate "~3.0.0" + lodash._reinterpolate "^3.0.0" lodash.uniq@^4.5.0: version "4.5.0" @@ -7777,9 +7782,10 @@ querystring@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" -querystringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.0.0.tgz#fa3ed6e68eb15159457c89b37bc6472833195755" +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== randomatic@^3.0.0: version "3.0.0" @@ -8421,6 +8427,7 @@ require-uncached@^1.0.2, require-uncached@^1.0.3: requires-port@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= resolve-cwd@^2.0.0: version "2.0.0" @@ -9815,10 +9822,11 @@ url-parse-lax@^1.0.0: prepend-http "^1.0.1" url-parse@^1.1.8, url-parse@~1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.0.tgz#6bfdaad60098c7fe06f623e42b22de62de0d3d75" + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== dependencies: - querystringify "^2.0.0" + querystringify "^2.1.1" requires-port "^1.0.0" url@0.10.3: