diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index 2a4ae71b7..057d0cd8f 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -21,7 +21,7 @@ const { ipcRenderer, remote, clipboard } = require('electron') import normalizeEditorFontFamily from 'browser/lib/normalizeEditorFontFamily' const spellcheck = require('browser/lib/spellcheck') const buildEditorContextMenu = require('browser/lib/contextMenuBuilder').buildEditorContextMenu -import TurndownService from 'turndown' +import { createTurndownService } from '../lib/turndown' import {languageMaps} from '../lib/CMLanguageList' import snippetManager from '../lib/SnippetManager' import {generateInEditor, tocExistsInEditor} from 'browser/lib/markdown-toc-generator' @@ -98,7 +98,7 @@ export default class CodeEditor extends React.Component { this.editorActivityHandler = () => this.handleEditorActivity() - this.turndownService = new TurndownService() + this.turndownService = createTurndownService() } handleSearch (msg) { diff --git a/browser/lib/turndown.js b/browser/lib/turndown.js new file mode 100644 index 000000000..a1c3e1280 --- /dev/null +++ b/browser/lib/turndown.js @@ -0,0 +1,9 @@ +const TurndownService = require('turndown') +const { gfm } = require('turndown-plugin-gfm') + +export const createTurndownService = function () { + const turndown = new TurndownService() + turndown.use(gfm) + turndown.remove('script') + return turndown +} diff --git a/browser/main/Detail/FromUrlButton.js b/browser/main/Detail/FromUrlButton.js new file mode 100644 index 000000000..0d1c1b4c5 --- /dev/null +++ b/browser/main/Detail/FromUrlButton.js @@ -0,0 +1,69 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './FromUrlButton.styl' +import _ from 'lodash' +import i18n from 'browser/lib/i18n' + +class FromUrlButton extends React.Component { + constructor (props) { + super(props) + + this.state = { + isActive: false + } + } + + handleMouseDown (e) { + this.setState({ + isActive: true + }) + } + + handleMouseUp (e) { + this.setState({ + isActive: false + }) + } + + handleMouseLeave (e) { + this.setState({ + isActive: false + }) + } + + render () { + const { className } = this.props + + return ( + + ) + } +} + +FromUrlButton.propTypes = { + isActive: PropTypes.bool, + onClick: PropTypes.func, + className: PropTypes.string +} + +export default CSSModules(FromUrlButton, styles) diff --git a/browser/main/Detail/FromUrlButton.styl b/browser/main/Detail/FromUrlButton.styl new file mode 100644 index 000000000..66c2d7309 --- /dev/null +++ b/browser/main/Detail/FromUrlButton.styl @@ -0,0 +1,41 @@ +.root + top 45px + topBarButtonRight() + &:hover + transition 0.2s + color alpha($ui-favorite-star-button-color, 0.6) + &:hover .tooltip + opacity 1 + +.tooltip + tooltip() + position absolute + pointer-events none + top 50px + right 125px + width 90px + z-index 200 + padding 5px + line-height normal + border-radius 2px + opacity 0 + transition 0.1s + +.root--active + @extend .root + transition 0.15s + color $ui-favorite-star-button-color + &:hover + transition 0.2s + color alpha($ui-favorite-star-button-color, 0.6) + +.icon + transition transform 0.15s + height 13px + +body[data-theme="dark"] + .root + topBarButtonDark() + &:hover + transition 0.2s + color alpha($ui-favorite-star-button-color, 0.6) diff --git a/browser/main/Detail/MarkdownNoteDetail.js b/browser/main/Detail/MarkdownNoteDetail.js index 45024751e..a826f3e89 100755 --- a/browser/main/Detail/MarkdownNoteDetail.js +++ b/browser/main/Detail/MarkdownNoteDetail.js @@ -472,6 +472,7 @@ class MarkdownNoteDetail extends React.Component {
this.handleSwitchMode(e)} editorType={editorType} /> + this.handleStarButtonClick(e)} isActive={note.isStarred} diff --git a/browser/main/Detail/StarButton.styl b/browser/main/Detail/StarButton.styl index e9c523e97..e06d1ac91 100644 --- a/browser/main/Detail/StarButton.styl +++ b/browser/main/Detail/StarButton.styl @@ -42,4 +42,4 @@ body[data-theme="dark"] topBarButtonDark() &:hover transition 0.2s - color alpha($ui-favorite-star-button-color, 0.6) \ No newline at end of file + color alpha($ui-favorite-star-button-color, 0.6) diff --git a/browser/main/lib/dataApi/createNoteFromUrl.js b/browser/main/lib/dataApi/createNoteFromUrl.js new file mode 100644 index 000000000..f98782104 --- /dev/null +++ b/browser/main/lib/dataApi/createNoteFromUrl.js @@ -0,0 +1,79 @@ +const http = require('http') +const https = require('https') +const { createTurndownService } = require('../../../lib/turndown') +const createNote = require('./createNote') + +import { push } from 'connected-react-router' +import ee from 'browser/main/lib/eventEmitter' + +function validateUrl (str) { + if (/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(str)) { + return true + } else { + return false + } +} + +function createNoteFromUrl (url, storage, folder, dispatch = null, location = null) { + return new Promise((resolve, reject) => { + const td = createTurndownService() + + if (!validateUrl(url)) { + reject({result: false, error: 'Please check your URL is in correct format. (Example, https://www.google.com)'}) + } + + const request = url.startsWith('https') ? https : http + + const req = request.request(url, (res) => { + let data = '' + + res.on('data', (chunk) => { + data += chunk + }) + + res.on('end', () => { + const markdownHTML = td.turndown(data) + + if (dispatch !== null) { + createNote(storage, { + type: 'MARKDOWN_NOTE', + folder: folder, + title: '', + content: markdownHTML + }) + .then((note) => { + const noteHash = note.key + dispatch({ + type: 'UPDATE_NOTE', + note: note + }) + dispatch(push({ + pathname: location.pathname, + query: {key: noteHash} + })) + ee.emit('list:jump', noteHash) + ee.emit('detail:focus') + resolve({result: true, error: null}) + }) + } else { + createNote(storage, { + type: 'MARKDOWN_NOTE', + folder: folder, + title: '', + content: markdownHTML + }).then((note) => { + resolve({result: true, note, error: null}) + }) + } + }) + }) + + req.on('error', (e) => { + console.error('error in parsing URL', e) + reject({result: false, error: e}) + }) + req.end() + }) +} + +module.exports = createNoteFromUrl diff --git a/browser/main/lib/dataApi/index.js b/browser/main/lib/dataApi/index.js index 92be6b936..6e88bbf96 100644 --- a/browser/main/lib/dataApi/index.js +++ b/browser/main/lib/dataApi/index.js @@ -11,6 +11,7 @@ const dataApi = { exportFolder: require('./exportFolder'), exportStorage: require('./exportStorage'), createNote: require('./createNote'), + createNoteFromUrl: require('./createNoteFromUrl'), updateNote: require('./updateNote'), deleteNote: require('./deleteNote'), moveNote: require('./moveNote'), diff --git a/browser/main/modals/CreateMarkdownFromURLModal.js b/browser/main/modals/CreateMarkdownFromURLModal.js new file mode 100644 index 000000000..31988059d --- /dev/null +++ b/browser/main/modals/CreateMarkdownFromURLModal.js @@ -0,0 +1,118 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './CreateMarkdownFromURLModal.styl' +import dataApi from 'browser/main/lib/dataApi' +import ModalEscButton from 'browser/components/ModalEscButton' +import i18n from 'browser/lib/i18n' + +class CreateMarkdownFromURLModal extends React.Component { + constructor (props) { + super(props) + + this.state = { + name: '', + showerror: false, + errormessage: '' + } + } + + componentDidMount () { + this.refs.name.focus() + this.refs.name.select() + } + + handleCloseButtonClick (e) { + this.props.close() + } + + handleChange (e) { + this.setState({ + name: this.refs.name.value + }) + } + + handleKeyDown (e) { + if (e.keyCode === 27) { + this.props.close() + } + } + + handleInputKeyDown (e) { + switch (e.keyCode) { + case 13: + this.confirm() + } + } + + handleConfirmButtonClick (e) { + this.confirm() + } + + showError (message) { + this.setState({ + showerror: true, + errormessage: message + }) + } + + hideError () { + this.setState({ + showerror: false, + errormessage: '' + }) + } + + confirm () { + this.hideError() + const { storage, folder, dispatch, location } = this.props + + dataApi.createNoteFromUrl(this.state.name, storage, folder, dispatch, location).then((result) => { + this.props.close() + }).catch((result) => { + this.showError(result.error) + }) + } + + render () { + return ( +
this.handleKeyDown(e)} + > +
+
{i18n.__('Import Markdown From URL')}
+
+ this.handleCloseButtonClick(e)} /> +
+
+
{i18n.__('Insert URL Here')}
+ this.handleChange(e)} + onKeyDown={(e) => this.handleInputKeyDown(e)} + /> +
+ +
{this.state.errormessage}
+
+
+ ) + } +} + +CreateMarkdownFromURLModal.propTypes = { + storage: PropTypes.string, + folder: PropTypes.string, + dispatch: PropTypes.func, + location: PropTypes.shape({ + pathname: PropTypes.string + }) +} + +export default CSSModules(CreateMarkdownFromURLModal, styles) diff --git a/browser/main/modals/CreateMarkdownFromURLModal.styl b/browser/main/modals/CreateMarkdownFromURLModal.styl new file mode 100644 index 000000000..5e59e4654 --- /dev/null +++ b/browser/main/modals/CreateMarkdownFromURLModal.styl @@ -0,0 +1,160 @@ +.root + modal() + width 500px + height 270px + overflow hidden + position relative + +.header + height 80px + margin-bottom 10px + margin-top 20px + font-size 18px + line-height 50px + background-color $ui-backgroundColor + color $ui-text-color + +.title + font-size 36px + font-weight 600 + +.control-folder-label + text-align left + font-size 14px + color $ui-text-color + +.control-folder-input + display block + height 40px + width 490px + padding 0 5px + margin 10px 0 + border 1px solid $ui-input--create-folder-modal + border-radius 2px + background-color transparent + outline none + vertical-align middle + font-size 16px + &:disabled + background-color $ui-input--disabled-backgroundColor + &:focus, &:active + border-color $ui-active-color + +.control-confirmButton + display block + height 35px + width 140px + border none + border-radius 2px + padding 0 25px + margin 20px auto + font-size 14px + colorPrimaryButton() + +body[data-theme="dark"] + .root + modalDark() + width 500px + height 270px + overflow hidden + position relative + + .header + background-color transparent + border-color $ui-dark-borderColor + color $ui-dark-text-color + + .control-folder-label + color $ui-dark-text-color + + .control-folder-input + border 1px solid $ui-input--create-folder-modal + color white + + .description + color $ui-inactive-text-color + + .control-confirmButton + colorDarkPrimaryButton() + +body[data-theme="solarized-dark"] + .root + modalSolarizedDark() + width 500px + height 270px + overflow hidden + position relative + + .header + background-color transparent + border-color $ui-dark-borderColor + color $ui-solarized-dark-text-color + + .control-folder-label + color $ui-solarized-dark-text-color + + .control-folder-input + border 1px solid $ui-input--create-folder-modal + color white + + .description + color $ui-inactive-text-color + + .control-confirmButton + colorSolarizedDarkPrimaryButton() + +.error + text-align center + color #F44336 + +body[data-theme="monokai"] + .root + modalMonokai() + width 500px + height 270px + overflow hidden + position relative + + .header + background-color transparent + border-color $ui-dark-borderColor + color $ui-monokai-text-color + + .control-folder-label + color $ui-monokai-text-color + + .control-folder-input + border 1px solid $ui-input--create-folder-modal + color white + + .description + color $ui-inactive-text-color + + .control-confirmButton + colorMonokaiPrimaryButton() + +body[data-theme="dracula"] + .root + modalDracula() + width 500px + height 270px + overflow hidden + position relative + + .header + background-color transparent + border-color $ui-dracula-borderColor + color $ui-dracula-text-color + + .control-folder-label + color $ui-dracula-text-color + + .control-folder-input + border 1px solid $ui-input--create-folder-modal + color white + + .description + color $ui-inactive-text-color + + .control-confirmButton + colorDraculaPrimaryButton() diff --git a/browser/main/modals/NewNoteModal.js b/browser/main/modals/NewNoteModal.js index a17a36cdf..476fa2524 100644 --- a/browser/main/modals/NewNoteModal.js +++ b/browser/main/modals/NewNoteModal.js @@ -3,6 +3,8 @@ import CSSModules from 'browser/lib/CSSModules' import styles from './NewNoteModal.styl' import ModalEscButton from 'browser/components/ModalEscButton' import i18n from 'browser/lib/i18n' +import { openModal } from 'browser/main/lib/modal' +import CreateMarkdownFromURLModal from '../modals/CreateMarkdownFromURLModal' import { createMarkdownNote, createSnippetNote } from 'browser/lib/newNote' import queryString from 'query-string' @@ -21,6 +23,18 @@ class NewNoteModal extends React.Component { this.props.close() } + handleCreateMarkdownFromUrlClick (e) { + this.props.close() + + const { storage, folder, dispatch, location } = this.props + openModal(CreateMarkdownFromURLModal, { + storage: storage, + folder: folder, + dispatch, + location + }) + } + handleMarkdownNoteButtonClick (e) { const { storage, folder, dispatch, location, config } = this.props const params = location.search !== '' && queryString.parse(location.search) @@ -115,10 +129,8 @@ class NewNoteModal extends React.Component {
-
- {i18n.__('Tab to switch format')} -
- +
{i18n.__('Tab to switch format')}
+
this.handleCreateMarkdownFromUrlClick(e)}>Or, create a new markdown note from a URL
) } diff --git a/browser/main/modals/NewNoteModal.styl b/browser/main/modals/NewNoteModal.styl index 662f3f697..ff0052bdb 100644 --- a/browser/main/modals/NewNoteModal.styl +++ b/browser/main/modals/NewNoteModal.styl @@ -48,6 +48,12 @@ text-align center margin-bottom 25px +.from-url + color $ui-inactive-text-color + text-align center + margin-bottom 25px + cursor pointer + body[data-theme="dark"] .root modalDark() @@ -62,7 +68,7 @@ body[data-theme="dark"] &:focus colorDarkPrimaryButton() - .description + .description, .from-url color $ui-inactive-text-color body[data-theme="solarized-dark"] @@ -79,7 +85,7 @@ body[data-theme="solarized-dark"] &:focus colorDarkPrimaryButton() - .description + .description, .from-url color $ui-solarized-dark-text-color body[data-theme="monokai"] @@ -96,7 +102,7 @@ body[data-theme="monokai"] &:focus colorDarkPrimaryButton() - .description + .description, .from-url color $ui-monokai-text-color body[data-theme="dracula"] diff --git a/resources/icon/icon-external.svg b/resources/icon/icon-external.svg new file mode 100644 index 000000000..8d340b123 --- /dev/null +++ b/resources/icon/icon-external.svg @@ -0,0 +1,39 @@ + + + + icon-external + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/dataApi/createNoteFromUrl-test.js b/tests/dataApi/createNoteFromUrl-test.js new file mode 100644 index 000000000..a324a3e5e --- /dev/null +++ b/tests/dataApi/createNoteFromUrl-test.js @@ -0,0 +1,43 @@ +const test = require('ava') +const createNoteFromUrl = require('browser/main/lib/dataApi/createNoteFromUrl') + +global.document = require('jsdom').jsdom('') +global.window = document.defaultView +global.navigator = window.navigator + +const Storage = require('dom-storage') +const localStorage = window.localStorage = global.localStorage = new Storage(null, { strict: true }) +const path = require('path') +const TestDummy = require('../fixtures/TestDummy') +const sander = require('sander') +const os = require('os') +const CSON = require('@rokt33r/season') + +const storagePath = path.join(os.tmpdir(), 'test/create-note-from-url') + +test.beforeEach((t) => { + t.context.storage = TestDummy.dummyStorage(storagePath) + localStorage.setItem('storages', JSON.stringify([t.context.storage.cache])) +}) + +test.serial('Create a note from URL', (t) => { + const storageKey = t.context.storage.cache.key + const folderKey = t.context.storage.json.folders[0].key + + const url = 'https://shapeshed.com/writing-cross-platform-node/' + + return createNoteFromUrl(url, storageKey, folderKey) + .then(function assert ({ note }) { + t.is(storageKey, note.storage) + const jsonData = CSON.readFileSync(path.join(storagePath, 'notes', note.key + '.cson')) + + // Test if saved content is matching the created in memory note + t.is(note.content, jsonData.content) + t.is(note.tags.length, jsonData.tags.length) + }) +}) + +test.after(function after () { + localStorage.clear() + sander.rimrafSync(storagePath) +}) diff --git a/yarn.lock b/yarn.lock index a10a0fc5a..76c64da9f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6771,8 +6771,8 @@ number-is-nan@^1.0.0: resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.4.tgz#2285631f34a95f0d0395cd900c96ed39b58f346e" nwsapi@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.0.tgz#7c8faf4ad501e1d17a651ebc5547f966b547c5c7" + version "2.0.1" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.1.tgz#a50d59a2dcb14b6931401171713ced2d0eb3468f" nwsapi@^2.0.7: version "2.0.8"