1
0
mirror of https://github.com/BoostIo/Boostnote synced 2025-12-13 17:56:25 +00:00

Merge branch 'master' into export-yfm

This commit is contained in:
Baptiste Augrain
2020-06-12 15:17:02 +02:00
327 changed files with 21961 additions and 12973 deletions

View File

@@ -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'
@@ -8,53 +9,84 @@ import consts from 'browser/lib/consts'
import Raphael from 'raphael'
import flowchart from 'flowchart'
import mermaidRender from './render/MermaidRender'
import SequenceDiagram from 'js-sequence-diagrams'
import SequenceDiagram from '@rokt33r/js-sequence-diagrams'
import Chart from 'chart.js'
import eventEmitter from 'browser/main/lib/eventEmitter'
import config from 'browser/main/lib/ConfigManager'
import htmlTextHelper from 'browser/lib/htmlTextHelper'
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 formatMarkdown from 'browser/main/lib/dataApi/formatMarkdown'
import formatHTML, { CSS_FILES, buildStyle, getCodeThemeLink, getStyleParams } from 'browser/main/lib/dataApi/formatHTML'
import formatHTML, {
CSS_FILES,
buildStyle,
getCodeThemeLink,
getStyleParams
} from 'browser/main/lib/dataApi/formatHTML'
import formatPDF from 'browser/main/lib/dataApi/formatPDF'
import { escapeHtmlCharacters } from 'browser/lib/utils'
import yaml from 'js-yaml'
import context from 'browser/lib/context'
import i18n from 'browser/lib/i18n'
import fs from 'fs'
import path from 'path'
import uri2path from 'file-uri-to-path'
import { remote, shell } from 'electron'
import attachmentManagement from '../main/lib/dataApi/attachmentManagement'
import filenamify from 'filenamify'
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 { buildMarkdownPreviewContextMenu } from 'browser/lib/contextMenuBuilder'
const dialog = remote.dialog
const scrollBarStyle = `
::-webkit-scrollbar {
${config.get().ui.showScrollBar ? '' : 'display: none;'}
width: 12px;
}
::-webkit-scrollbar-thumb {
${config.get().ui.showScrollBar ? '' : 'display: none;'}
background-color: rgba(0, 0, 0, 0.15);
}
::-webkit-scrollbar-track-piece {
background-color: inherit;
}
`
const scrollBarDarkStyle = `
::-webkit-scrollbar {
${config.get().ui.showScrollBar ? '' : 'display: none;'}
width: 12px;
}
::-webkit-scrollbar-thumb {
${config.get().ui.showScrollBar ? '' : 'display: none;'}
background-color: rgba(0, 0, 0, 0.3);
}
::-webkit-scrollbar-track-piece {
background-color: inherit;
}
`
export default class MarkdownPreview extends React.Component {
constructor (props) {
// return the line number of the line that used to generate the specified element
// return -1 if the line is not found
function getSourceLineNumberByElement(element) {
let isHasLineNumber = element.dataset.line !== undefined
let parent = element
while (!isHasLineNumber && parent.parentElement !== null) {
parent = parent.parentElement
isHasLineNumber = parent.dataset.line !== undefined
}
return parent.dataset.line !== undefined ? parseInt(parent.dataset.line) : -1
}
class MarkdownPreview extends React.Component {
constructor(props) {
super(props)
this.contextMenuHandler = e => this.handleContextMenu(e)
@@ -69,14 +101,16 @@ export default class MarkdownPreview extends React.Component {
this.saveAsTextHandler = () => this.handleSaveAsText()
this.saveAsMdHandler = () => this.handleSaveAsMd()
this.saveAsHtmlHandler = () => this.handleSaveAsHtml()
this.saveAsPdfHandler = () => this.handleSaveAsPdf()
this.printHandler = () => this.handlePrint()
this.resizeHandler = _.throttle(this.handleResize.bind(this), 100)
this.linkClickHandler = this.handleLinkClick.bind(this)
this.initMarkdown = this.initMarkdown.bind(this)
this.initMarkdown()
}
initMarkdown () {
initMarkdown() {
const { smartQuotes, sanitize, breaks } = this.props
this.markdown = new Markdown({
typographer: smartQuotes,
@@ -85,64 +119,61 @@ export default class MarkdownPreview extends React.Component {
})
}
handleCheckboxClick (e) {
handleCheckboxClick(e) {
this.props.onCheckboxClick(e)
}
handleScroll (e) {
handleScroll(e) {
if (this.props.onScroll) {
this.props.onScroll(e)
}
}
handleContextMenu (event) {
// If a contextMenu handler was passed to us, use it instead of the self-defined one -> return
if (_.isFunction(this.props.onContextMenu)) {
handleContextMenu(event) {
const menu = buildMarkdownPreviewContextMenu(this, event)
const switchPreview = ConfigManager.get().editor.switchPreview
if (menu != null && switchPreview !== 'RIGHTCLICK') {
menu.popup(remote.getCurrentWindow())
} else if (_.isFunction(this.props.onContextMenu)) {
this.props.onContextMenu(event)
return
}
// No contextMenu was passed to us -> execute our own link-opener
if (event.target.tagName.toLowerCase() === 'a') {
const href = event.target.href
const isLocalFile = href.startsWith('file:')
if (isLocalFile) {
const absPath = uri2path(href)
try {
if (fs.lstatSync(absPath).isFile()) {
context.popup([
{
label: i18n.__('Show in explorer'),
click: (e) => shell.showItemInFolder(absPath)
}
])
}
} catch (e) {
console.log('Error while evaluating if the file is locally available', e)
}
}
}
}
handleDoubleClick (e) {
handleDoubleClick(e) {
if (this.props.onDoubleClick != null) this.props.onDoubleClick(e)
}
handleMouseDown (e) {
handleMouseDown(e) {
const config = ConfigManager.get()
if (config.editor.switchPreview === 'RIGHTCLICK' && e.buttons === 2 && config.editor.type === 'SPLIT') {
const clickElement = e.target
const targetTag = clickElement.tagName // The direct parent HTML of where was clicked ie "BODY" or "DIV"
const lineNumber = getSourceLineNumberByElement(clickElement) // Line location of element clicked.
if (
config.editor.switchPreview === 'RIGHTCLICK' &&
e.buttons === 2 &&
config.editor.type === 'SPLIT'
) {
eventEmitter.emit('topbar:togglemodebutton', 'CODE')
}
if (e.target != null) {
switch (e.target.tagName) {
case 'A':
case 'INPUT':
return null
if (e.ctrlKey) {
if (config.editor.type === 'SPLIT') {
if (lineNumber !== -1) {
eventEmitter.emit('line:jump', lineNumber)
}
} else {
if (lineNumber !== -1) {
eventEmitter.emit('editor:focus')
eventEmitter.emit('line:jump', lineNumber)
}
}
}
if (this.props.onMouseDown != null) this.props.onMouseDown(e)
if (this.props.onMouseDown != null && targetTag === 'BODY')
this.props.onMouseDown(e)
}
handleMouseUp (e) {
handleMouseUp(e) {
if (!this.props.onMouseUp) return
if (e.target != null && e.target.tagName === 'A') {
return null
@@ -150,23 +181,27 @@ export default class MarkdownPreview extends React.Component {
if (this.props.onMouseUp != null) this.props.onMouseUp(e)
}
handleSaveAsText () {
handleSaveAsText() {
this.exportAsDocument('txt')
}
handleSaveAsMd () {
handleSaveAsMd() {
this.exportAsDocument('md', formatMarkdown(this.props))
}
handleSaveAsHtml () {
handleSaveAsHtml() {
this.exportAsDocument('html', formatHTML(this.props))
}
handlePrint () {
handleSaveAsPdf() {
this.exportAsDocument('pdf', formatPDF(this.props))
}
handlePrint() {
this.refs.root.contentWindow.print()
}
exportAsDocument (fileType, contentFormatter) {
exportAsDocument(fileType, contentFormatter) {
const note = this.props.getNote()
const options = {
@@ -185,7 +220,8 @@ export default class MarkdownPreview extends React.Component {
.then(res => {
dialog.showMessageBox(remote.getCurrentWindow(), {
type: 'info',
message: `Exported to ${filename}`
message: `Exported to ${filename}`,
buttons: [i18n.__('Ok')]
})
})
.catch(err => {
@@ -199,7 +235,7 @@ export default class MarkdownPreview extends React.Component {
})
}
fixDecodedURI (node) {
fixDecodedURI(node) {
if (
node &&
node.children.length === 1 &&
@@ -211,21 +247,41 @@ export default class MarkdownPreview extends React.Component {
}
}
getScrollBarStyle () {
const { theme } = this.props
switch (theme) {
case 'dark':
case 'solarized-dark':
case 'monokai':
case 'dracula':
return scrollBarDarkStyle
default:
return scrollBarStyle
/**
* @description Convert special characters between three ```
* @param {string[]} splitWithCodeTag Array of HTML strings separated by three ```
* @returns {string} HTML in which special characters between three ``` have been converted
*/
escapeHtmlCharactersInCodeTag(splitWithCodeTag) {
for (let index = 0; index < splitWithCodeTag.length; index++) {
const codeTagRequired =
splitWithCodeTag[index] !== '```' && index < splitWithCodeTag.length - 1
if (codeTagRequired) {
splitWithCodeTag.splice(index + 1, 0, '```')
}
}
let inCodeTag = false
let result = ''
for (let content of splitWithCodeTag) {
if (content === '```') {
inCodeTag = !inCodeTag
} else if (inCodeTag) {
content = escapeHtmlCharacters(content)
}
result += content
}
return result
}
componentDidMount () {
getScrollBarStyle() {
const { theme } = this.props
return uiThemes.some(item => item.name === theme && item.isDark)
? scrollBarDarkStyle
: scrollBarStyle
}
componentDidMount() {
const { onDrop } = this.props
this.refs.root.setAttribute('sandbox', 'allow-scripts')
@@ -275,13 +331,15 @@ export default class MarkdownPreview extends React.Component {
'scroll',
this.scrollHandler
)
this.refs.root.contentWindow.addEventListener('resize', this.resizeHandler)
eventEmitter.on('export:save-text', this.saveAsTextHandler)
eventEmitter.on('export:save-md', this.saveAsMdHandler)
eventEmitter.on('export:save-html', this.saveAsHtmlHandler)
eventEmitter.on('export:save-pdf', this.saveAsPdfHandler)
eventEmitter.on('print', this.printHandler)
}
componentWillUnmount () {
componentWillUnmount() {
const { onDrop } = this.props
this.refs.root.contentWindow.document.body.removeEventListener(
@@ -312,23 +370,31 @@ export default class MarkdownPreview extends React.Component {
'scroll',
this.scrollHandler
)
this.refs.root.contentWindow.removeEventListener(
'resize',
this.resizeHandler
)
eventEmitter.off('export:save-text', this.saveAsTextHandler)
eventEmitter.off('export:save-md', this.saveAsMdHandler)
eventEmitter.off('export:save-html', this.saveAsHtmlHandler)
eventEmitter.off('export:save-pdf', this.saveAsPdfHandler)
eventEmitter.off('print', this.printHandler)
}
componentDidUpdate (prevProps) {
if (prevProps.value !== this.props.value) this.rewriteIframe()
componentDidUpdate(prevProps) {
// actual rewriteIframe function should be called only once
let needsRewriteIframe = false
if (prevProps.value !== this.props.value) needsRewriteIframe = true
if (
prevProps.smartQuotes !== this.props.smartQuotes ||
prevProps.sanitize !== this.props.sanitize ||
prevProps.mermaidHTMLLabel !== this.props.mermaidHTMLLabel ||
prevProps.smartArrows !== this.props.smartArrows ||
prevProps.breaks !== this.props.breaks ||
prevProps.lineThroughCheckbox !== this.props.lineThroughCheckbox
) {
this.initMarkdown()
this.rewriteIframe()
needsRewriteIframe = true
}
if (
prevProps.fontFamily !== this.props.fontFamily ||
@@ -340,14 +406,24 @@ export default class MarkdownPreview extends React.Component {
prevProps.theme !== this.props.theme ||
prevProps.scrollPastEnd !== this.props.scrollPastEnd ||
prevProps.allowCustomCSS !== this.props.allowCustomCSS ||
prevProps.customCSS !== this.props.customCSS
prevProps.customCSS !== this.props.customCSS ||
prevProps.RTL !== this.props.RTL
) {
this.applyStyle()
needsRewriteIframe = true
}
if (needsRewriteIframe) {
this.rewriteIframe()
}
// Should scroll to top after selecting another note
if (prevProps.noteKey !== this.props.noteKey) {
this.scrollTo(0, 0)
}
}
applyStyle () {
applyStyle() {
const {
fontFamily,
fontSize,
@@ -357,10 +433,13 @@ export default class MarkdownPreview extends React.Component {
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
customCSS,
RTL
} = getStyleParams(this.props)
this.getWindow().document.getElementById('codeTheme').href = getCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById(
'codeTheme'
).href = getCodeThemeLink(codeBlockTheme)
this.getWindow().document.getElementById('style').innerHTML = buildStyle(
fontFamily,
@@ -370,11 +449,12 @@ export default class MarkdownPreview extends React.Component {
scrollPastEnd,
theme,
allowCustomCSS,
customCSS
customCSS,
RTL
)
}
rewriteIframe () {
rewriteIframe() {
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll(
'input[type="checkbox"]'
@@ -396,11 +476,17 @@ export default class MarkdownPreview extends React.Component {
indentSize,
showCopyNotification,
storagePath,
noteKey
noteKey,
sanitize,
mermaidHTMLLabel
} = this.props
let { value, codeBlockTheme } = this.props
this.refs.root.contentWindow.document.body.setAttribute('data-theme', theme)
if (sanitize === 'NONE') {
const splitWithCodeTag = value.split('```')
value = this.escapeHtmlCharactersInCodeTag(splitWithCodeTag)
}
const renderedHTML = this.markdown.render(value)
attachmentManagement.migrateAttachments(value, storagePath, noteKey)
this.refs.root.contentWindow.document.body.innerHTML = attachmentManagement.fixLocalURLS(
@@ -424,9 +510,11 @@ export default class MarkdownPreview extends React.Component {
}
)
codeBlockTheme = consts.THEMES.some(_theme => _theme === codeBlockTheme)
? codeBlockTheme
: 'default'
codeBlockTheme = consts.THEMES.find(theme => theme.name === codeBlockTheme)
const codeBlockThemeClassName = codeBlockTheme
? codeBlockTheme.className
: 'cm-s-default'
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.code code'),
@@ -439,6 +527,8 @@ export default class MarkdownPreview extends React.Component {
copyIcon.innerHTML =
'<button class="clipboardButton"><svg width="13" height="13" viewBox="0 0 1792 1792" ><path d="M768 1664h896v-640h-416q-40 0-68-28t-28-68v-416h-384v1152zm256-1440v-64q0-13-9.5-22.5t-22.5-9.5h-704q-13 0-22.5 9.5t-9.5 22.5v64q0 13 9.5 22.5t22.5 9.5h704q13 0 22.5-9.5t9.5-22.5zm256 672h299l-299-299v299zm512 128v672q0 40-28 68t-68 28h-960q-40 0-68-28t-28-68v-160h-544q-40 0-68-28t-28-68v-1344q0-40 28-68t68-28h1088q40 0 68 28t28 68v328q21 13 36 28l408 408q28 28 48 76t20 88z"/></svg></button>'
copyIcon.onclick = e => {
e.preventDefault()
e.stopPropagation()
copy(content)
if (showCopyNotification) {
this.notify('Saved to Clipboard!', {
@@ -447,27 +537,20 @@ export default class MarkdownPreview extends React.Component {
})
}
}
el.parentNode.appendChild(copyIcon)
el.innerHTML = ''
if (codeBlockTheme.indexOf('solarized') === 0) {
const [refThema, color] = codeBlockTheme.split(' ')
el.parentNode.className += ` cm-s-${refThema} cm-s-${color}`
} else {
el.parentNode.className += ` cm-s-${codeBlockTheme}`
}
el.parentNode.className += ` ${codeBlockThemeClassName}`
CodeMirror.runMode(content, syntax.mime, el, {
tabSize: indentSize
})
})
}
)
const opts = {}
// if (this.props.theme === 'dark') {
// opts['font-color'] = '#DDD'
// opts['line-color'] = '#DDD'
// opts['element-color'] = '#DDD'
// opts['fill'] = '#3A404C'
// }
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.flowchart'),
el => {
@@ -513,7 +596,10 @@ export default class MarkdownPreview extends React.Component {
el => {
try {
const format = el.attributes.getNamedItem('data-format').value
const chartConfig = format === 'yaml' ? yaml.load(el.innerHTML) : JSON.parse(el.innerHTML)
const chartConfig =
format === 'yaml'
? yaml.load(el.innerHTML)
: JSON.parse(el.innerHTML)
el.innerHTML = ''
const canvas = document.createElement('canvas')
@@ -525,6 +611,7 @@ export default class MarkdownPreview extends React.Component {
canvas.height = height.value + 'vh'
}
// eslint-disable-next-line no-unused-vars
const chart = new Chart(canvas, chartConfig)
} catch (e) {
el.className = 'chart-error'
@@ -535,7 +622,12 @@ export default class MarkdownPreview extends React.Component {
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('.mermaid'),
el => {
mermaidRender(el, htmlTextHelper.decodeEntities(el.innerHTML), theme)
mermaidRender(
el,
htmlTextHelper.decodeEntities(el.innerHTML),
theme,
mermaidHTMLLabel
)
}
)
@@ -557,26 +649,133 @@ export default class MarkdownPreview extends React.Component {
autoplay = 0
}
render(
<Carousel
images={images}
autoplay={autoplay}
/>,
el
render(<Carousel images={images} autoplay={autoplay} />, el)
}
)
const markdownPreviewIframe = document.querySelector('.MarkdownPreview')
const rect = markdownPreviewIframe.getBoundingClientRect()
const config = { attributes: true, subtree: true }
const imgObserver = new MutationObserver(mutationList => {
for (const mu of mutationList) {
if (mu.target.className === 'carouselContent-enter-done') {
this.setImgOnClickEventHelper(mu.target, rect)
break
}
}
})
const imgList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll(
'img'
)
for (const img of imgList) {
const parentEl = img.parentElement
this.setImgOnClickEventHelper(img, rect)
imgObserver.observe(parentEl, config)
}
const aList = markdownPreviewIframe.contentWindow.document.body.querySelectorAll(
'a'
)
for (const a of aList) {
a.removeEventListener('click', this.linkClickHandler)
a.addEventListener('click', this.linkClickHandler)
}
}
setImgOnClickEventHelper(img, rect) {
img.onclick = () => {
const widthMagnification = document.body.clientWidth / img.width
const heightMagnification = document.body.clientHeight / img.height
const baseOnWidth = widthMagnification < heightMagnification
const magnification = baseOnWidth
? widthMagnification
: heightMagnification
const zoomImgWidth = img.width * magnification
const zoomImgHeight = img.height * magnification
const zoomImgTop = (document.body.clientHeight - zoomImgHeight) / 2
const zoomImgLeft = (document.body.clientWidth - zoomImgWidth) / 2
const originalImgTop = img.y + rect.top
const originalImgLeft = img.x + rect.left
const originalImgRect = {
top: `${originalImgTop}px`,
left: `${originalImgLeft}px`,
width: `${img.width}px`,
height: `${img.height}px`
}
const zoomInImgRect = {
top: `${baseOnWidth ? zoomImgTop : 0}px`,
left: `${baseOnWidth ? 0 : zoomImgLeft}px`,
width: `${zoomImgWidth}px`,
height: `${zoomImgHeight}px`
}
const animationSpeed = 300
const zoomImg = document.createElement('img')
zoomImg.src = img.src
zoomImg.style = `
position: absolute;
top: ${baseOnWidth ? zoomImgTop : 0}px;
left: ${baseOnWidth ? 0 : zoomImgLeft}px;
width: ${zoomImgWidth};
height: ${zoomImgHeight}px;
`
zoomImg.animate([originalImgRect, zoomInImgRect], animationSpeed)
const overlay = document.createElement('div')
overlay.style = `
background-color: rgba(0,0,0,0.5);
cursor: zoom-out;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: ${document.body.clientHeight}px;
z-index: 100;
`
overlay.onclick = () => {
zoomImg.style = `
position: absolute;
top: ${originalImgTop}px;
left: ${originalImgLeft}px;
width: ${img.width}px;
height: ${img.height}px;
`
const zoomOutImgAnimation = zoomImg.animate(
[zoomInImgRect, originalImgRect],
animationSpeed
)
zoomOutImgAnimation.onfinish = () => overlay.remove()
}
overlay.appendChild(zoomImg)
document.body.appendChild(overlay)
}
}
handleResize() {
_.forEach(
this.refs.root.contentWindow.document.querySelectorAll('svg[ratio]'),
el => {
el.setAttribute('height', el.clientWidth / el.getAttribute('ratio'))
}
)
}
focus () {
focus() {
this.refs.root.focus()
}
getWindow () {
getWindow() {
return this.refs.root.contentWindow
}
scrollTo (targetRow) {
/**
* @public
* @param {Number} targetRow
*/
scrollToRow(targetRow) {
const blocks = this.getWindow().document.querySelectorAll(
'body>[data-line]'
)
@@ -586,18 +785,27 @@ export default class MarkdownPreview extends React.Component {
const row = parseInt(block.getAttribute('data-line'))
if (row > targetRow || index === blocks.length - 1) {
block = blocks[index - 1]
block != null && this.getWindow().scrollTo(0, block.offsetTop)
block != null && this.scrollTo(0, block.offsetTop)
break
}
}
}
preventImageDroppedHandler (e) {
/**
* `document.body.scrollTo`
* @param {Number} x
* @param {Number} y
*/
scrollTo(x, y) {
this.getWindow().document.body.scrollTo(x, y)
}
preventImageDroppedHandler(e) {
e.preventDefault()
e.stopPropagation()
}
notify (title, options) {
notify(title, options) {
if (global.process.platform === 'win32') {
options.icon = path.join(
'file://',
@@ -608,24 +816,35 @@ export default class MarkdownPreview extends React.Component {
return new window.Notification(title, options)
}
handleLinkClick (e) {
handleLinkClick(e) {
e.preventDefault()
e.stopPropagation()
const href = e.target.href
const linkHash = href.split('/').pop()
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 regexNoteInternalLink = /main.html#(.+)/
if (regexNoteInternalLink.test(linkHash)) {
const targetId = mdurl.encode(linkHash.match(regexNoteInternalLink)[1])
const targetElement = this.refs.root.contentWindow.document.getElementById(
targetId
)
const parser = document.createElement('a')
parser.href = rawHref
const isStartWithHash = rawHref[0] === '#'
const { href, hash } = parser
if (targetElement != null) {
this.getWindow().scrollTo(0, targetElement.offsetTop)
const linkHash = hash === '' ? rawHref : hash // needed because we're having special link formats that are removed by parser e.g. :line:10
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 = this.getWindow().document.getElementById(targetId)
if (targetElement != null) {
this.scrollTo(0, targetElement.offsetTop)
}
return
}
return
}
// this will match the new uuid v4 hash and the old hash
@@ -656,11 +875,29 @@ 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
shell.openExternal(href)
this.openExternal(href)
}
render () {
openExternal(href) {
try {
const success =
shell.openExternal(href) || shell.openExternal(decodeURI(href))
if (!success) console.error('failed to open url ' + href)
} catch (e) {
// URI Error threw from decodeURI
console.error(e)
}
}
render() {
const { className, style, tabIndex } = this.props
return (
<iframe
@@ -689,3 +926,10 @@ MarkdownPreview.propTypes = {
smartArrows: PropTypes.bool,
breaks: PropTypes.bool
}
export default connect(
null,
null,
null,
{ forwardRef: true }
)(MarkdownPreview)