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 @@
+
+
+
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"