Basic markdown renderer (#197)

* Add basic markdown it rendering
* Add markdown preview
* Add embedings for vimeo, youtube, gist
* Add support for legacy shortcodes and link detection
* Set "both" as editor default
* Add markdown-it-task-lists
* Add twemoji
* Changed SlideShare short-code behaviour from embedding to generating a link
* Extract markdown it parser debugger into separate component
* Deactivate markdown it linkify for now
* Add link safety attributes
* Add one-click-embedding component and use it
* Added embedding changes and deprecations to CHANGELOG.md

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
Co-authored-by: Philip Molares <philip@mauricedoepke.de>
Co-authored-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
mrdrogdrog 2020-06-20 00:44:18 +02:00 committed by GitHub
parent 8ba2be7c70
commit 7189a63618
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 637 additions and 16 deletions

View file

@ -2,6 +2,21 @@
## [Unreleased]
### Deprecations
- This version of CodiMD is the last version that supports the following short-code syntaxes for embedding content. Embedding works now instead by putting the plain webpage link to the content into a single line.
- `{%youtube someid %}` -> https://youtube.com/watch?v=someid
- `{%vimeo 123456789 %}` -> https://vimeo.com/123456789
- `{%gist user/12345 %}` -> https://gist.github.com/user/12345
- `{%slideshare user/my-awesome-presentation %}` -> Embedding removed
- `{%speakerdeck foobar %}` -> Embedding removed
### Removed
- SlideShare embedding
- If a legacy embedding code is detected it will show the link to the presentation instead of the embedded presentation
- Speakerdeck embedding
- If a legacy embedding code is detected it will show the link to the presentation instead of the embedded presentation
### Added
- A new table view for the history (besides the card view)
@ -12,6 +27,7 @@
- The sign-in/sign-up functions are now on a separate page
- The history shows both the entries saved in LocalStorage and the entries saved on the server together
- The gist and pdf embeddings now use a one-click aproach similar to vimeo and youtube
---

View file

@ -8,12 +8,14 @@
"@testing-library/user-event": "11.4.2",
"@types/codemirror": "0.0.96",
"@types/jest": "26.0.0",
"@types/markdown-it": "^10.0.1",
"@types/node": "12.12.47",
"@types/node-sass": "4.11.1",
"@types/react": "16.9.36",
"@types/react-bootstrap": "1.0.1",
"@types/react-bootstrap-typeahead": "3.4.6",
"@types/react-dom": "16.9.8",
"@types/react-html-parser": "^2.0.1",
"@types/react-redux": "7.1.9",
"@types/react-router": "5.1.7",
"@types/react-router-bootstrap": "0.24.5",
@ -31,9 +33,14 @@
"eslint-plugin-promise": "4.2.1",
"eslint-plugin-standard": "4.0.1",
"fork-awesome": "1.1.7",
"github-markdown-css": "^4.0.0",
"i18next": "19.4.5",
"i18next-browser-languagedetector": "4.2.0",
"i18next-http-backend": "1.0.15",
"markdown-it": "^11.0.0",
"markdown-it-emoji": "^1.4.0",
"markdown-it-regex": "^0.2.0",
"markdown-it-task-lists": "^2.1.1",
"moment": "2.26.0",
"node-sass": "4.14.1",
"react": "16.13.1",
@ -41,6 +48,7 @@
"react-bootstrap-typeahead": "5.0.0-rc.3",
"react-codemirror2": "7.2.1",
"react-dom": "16.13.1",
"react-html-parser": "^2.0.2",
"react-i18next": "11.5.1",
"react-redux": "7.2.0",
"react-router": "5.2.0",

View file

@ -11,18 +11,22 @@ import 'codemirror/addon/search/match-highlighter'
import 'codemirror/addon/selection/active-line'
import 'codemirror/keymap/sublime.js'
import 'codemirror/mode/gfm/gfm.js'
import React, { useState } from 'react'
import React from 'react'
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
import { useTranslation } from 'react-i18next'
import './editor-window.scss'
const EditorWindow: React.FC = () => {
export interface EditorWindowProps {
onContentChange: (content: string) => void
content: string
}
const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, content }) => {
const { t } = useTranslation()
const [content, setContent] = useState<string>('')
return (
<ControlledCodeMirror
className="h-100 w-100"
className="h-100 w-100 flex-fill"
value={content}
options={{
mode: 'gfm',
@ -58,10 +62,7 @@ const EditorWindow: React.FC = () => {
}
}
onBeforeChange={(editor, data, value) => {
setContent(value)
}}
onChange={(editor, data, value) => {
console.log('change!')
onContentChange(value)
}}
/>
)

View file

@ -12,6 +12,7 @@ import { TaskBar } from './task-bar/task-bar'
const Editor: React.FC = () => {
const editorMode: EditorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
const [markdownContent, setMarkdownContent] = useState('# Embedding demo\n\n## Slideshare\n{%slideshare mazlan1/internet-of-things-the-tip-of-an-iceberg %}\n\n## Gist\nhttps://gist.github.com/schacon/1\n\n## YouTube\nhttps://www.youtube.com/watch?v=KgMpKsp23yY\n\n## Vimeo\nhttps://vimeo.com/23237102')
const isWide = useMedia({ minWidth: 576 })
const [firstDraw, setFirstDraw] = useState(true)
@ -32,9 +33,9 @@ const Editor: React.FC = () => {
<TaskBar/>
<Splitter
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
left={<EditorWindow/>}
left={<EditorWindow onContentChange={content => setMarkdownContent(content)} content={markdownContent}/>}
showRight={editorMode === EditorMode.PREVIEW || (editorMode === EditorMode.BOTH)}
right={<MarkdownPreview/>}
right={<MarkdownPreview content={markdownContent}/>}
containerClassName={'overflow-hidden'}/>
</div>
</Fragment>

View file

@ -0,0 +1,8 @@
import MarkdownIt from 'markdown-it/lib'
export const MarkdownItParserDebugger: MarkdownIt.PluginSimple = (md: MarkdownIt) => {
md.core.ruler.push('test', (state) => {
console.log(state)
return true
})
}

View file

@ -0,0 +1,6 @@
@import '../../../../node_modules/github-markdown-css/github-markdown.css';
.markdown-body {
max-width: 758px;
font-family: 'Source Sans Pro', "twemoji", sans-serif;
}

View file

@ -1,9 +1,81 @@
import React from 'react'
import MarkdownIt from 'markdown-it'
import emoji from 'markdown-it-emoji'
import markdownItRegex from 'markdown-it-regex'
import taskList from 'markdown-it-task-lists'
import React, { ReactElement, useMemo } from 'react'
import ReactHtmlParser, { convertNodeToElement, Transform } from 'react-html-parser'
import { MarkdownItParserDebugger } from './markdown-it-plugins/parser-debugger'
import './markdown-preview.scss'
import { replaceGistLink } from './regex-plugins/replace-gist-link'
import { replaceLegacyGistShortCode } from './regex-plugins/replace-legacy-gist-short-code'
import { replaceLegacySlideshareShortCode } from './regex-plugins/replace-legacy-slideshare-short-code'
import { replaceLegacySpeakerdeckShortCode } from './regex-plugins/replace-legacy-speakerdeck-short-code'
import { replaceLegacyVimeoShortCode } from './regex-plugins/replace-legacy-vimeo-short-code'
import { replaceLegacyYoutubeShortCode } from './regex-plugins/replace-legacy-youtube-short-code'
import { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
import { getGistReplacement } from './replace-components/gist/gist-frame'
import { getVimeoReplacement } from './replace-components/vimeo/vimeo-frame'
import { getYouTubeReplacement } from './replace-components/youtube/youtube-frame'
export interface MarkdownPreviewProps {
content: string
}
const MarkdownPreview: React.FC<MarkdownPreviewProps> = ({ content }) => {
const markdownIt = useMemo(() => {
const md = new MarkdownIt('default', {
html: true,
breaks: true,
langPrefix: '',
linkify: false,
typographer: true
})
md.use(taskList)
md.use(emoji)
md.use(markdownItRegex, replaceLegacyYoutubeShortCode)
md.use(markdownItRegex, replaceLegacyVimeoShortCode)
md.use(markdownItRegex, replaceLegacyGistShortCode)
md.use(markdownItRegex, replaceLegacySlideshareShortCode)
md.use(markdownItRegex, replaceLegacySpeakerdeckShortCode)
md.use(markdownItRegex, replaceYouTubeLink)
md.use(markdownItRegex, replaceVimeoLink)
md.use(markdownItRegex, replaceGistLink)
md.use(MarkdownItParserDebugger)
return md
}, [])
const result: ReactElement[] = useMemo(() => {
const youtubeIdCounterMap = new Map<string, number>()
const vimeoIdCounterMap = new Map<string, number>()
const gistIdCounterMap = new Map<string, number>()
const html: string = markdownIt.render(content)
const transform: Transform = (node, index) => {
const resultYT = getYouTubeReplacement(node, youtubeIdCounterMap)
if (resultYT) {
return resultYT
}
const resultVimeo = getVimeoReplacement(node, vimeoIdCounterMap)
if (resultVimeo) {
return resultVimeo
}
const resultGist = getGistReplacement(node, gistIdCounterMap)
if (resultGist) {
return resultGist
}
return convertNodeToElement(node, index, transform)
}
const ret: ReactElement[] = ReactHtmlParser(html, { transform: transform })
return ret
}, [content, markdownIt])
const MarkdownPreview: React.FC = () => {
return (
<div className='h-100 px-2 py-1 bg-white'>
Hello, MarkdownPreview!
<div className={'bg-light container-fluid flex-fill h-100 overflow-y-scroll pb-5'}>
<div className={'markdown-body container-fluid'}>{result}</div>
</div>
)
}

View file

@ -0,0 +1,18 @@
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
const protocolRegex = /(?:http(?:s)?:\/\/)?/
const domainRegex = /(?:gist\.github\.com\/)/
const idRegex = /(\w+\/\w+)/
const tailRegex = /(?:[./?#].*)?/
const gistUrlRegex = new RegExp(`(?:${protocolRegex.source}${domainRegex.source}${idRegex.source}${tailRegex.source})`)
const linkRegex = new RegExp(`^${gistUrlRegex.source}$`, 'i')
export const replaceGistLink: RegexOptions = {
name: 'gist-link',
regex: linkRegex,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<codimd-gist id="${match}"></codimd-gist>`
}
}

View file

@ -0,0 +1,13 @@
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
const finalRegex = /^{%gist (\w+\/\w+) ?%}$/
export const replaceLegacyGistShortCode: RegexOptions = {
name: 'legacy-gist-short-code',
regex: finalRegex,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<codimd-gist id="${match}"></codimd-gist>`
}
}

View file

@ -0,0 +1,11 @@
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
const finalRegex = /^{%slideshare (\w+\/[\w-]+) ?%}$/
export const replaceLegacySlideshareShortCode: RegexOptions = {
name: 'legacy-slideshare-short-code',
regex: finalRegex,
replace: (match) => {
return `<a target="_blank" rel="noopener noreferrer" href="https://www.slideshare.net/${match}">https://www.slideshare.net/${match}</a>`
}
}

View file

@ -0,0 +1,11 @@
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
const finalRegex = /^{%speakerdeck (\w+\/[\w-]+) ?%}$/
export const replaceLegacySpeakerdeckShortCode: RegexOptions = {
name: 'legacy-speakerdeck-short-code',
regex: finalRegex,
replace: (match) => {
return `<a target="_blank" rel="noopener noreferrer" href="https://speakerdeck.com//${match}">https://speakerdeck.com/${match}</a>`
}
}

View file

@ -0,0 +1,11 @@
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
export const replaceLegacyVimeoShortCode: RegexOptions = {
name: 'legacy-vimeo-short-code',
regex: /^{%vimeo ([\d]{6,11}) ?%}$/,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<codimd-vimeo id="${match}"></codimd-vimeo>`
}
}

View file

@ -0,0 +1,11 @@
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
export const replaceLegacyYoutubeShortCode: RegexOptions = {
name: 'legacy-youtube-short-code',
regex: /^{%youtube ([^"&?\\/\s]{11}) ?%}$/,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<codimd-youtube id="${match}"></codimd-youtube>`
}
}

View file

@ -0,0 +1,18 @@
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
const protocolRegex = /(?:http(?:s)?:\/\/)?/
const domainRegex = /(?:player\.)?(?:vimeo\.com\/)(?:(?:channels|album|ondemand|groups)\/\w+\/)?(?:video\/)?/
const idRegex = /([\d]{6,11})/
const tailRegex = /(?:[?#].*)?/
const vimeoVideoUrlRegex = new RegExp(`(?:${protocolRegex.source}${domainRegex.source}${idRegex.source}${tailRegex.source})`)
const linkRegex = new RegExp(`^${vimeoVideoUrlRegex.source}$`, 'i')
export const replaceVimeoLink: RegexOptions = {
name: 'vimeo-link',
regex: linkRegex,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<codimd-vimeo id="${match}"></codimd-vimeo>`
}
}

View file

@ -0,0 +1,19 @@
import { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
const protocolRegex = /(?:http(?:s)?:\/\/)?/
const subdomainRegex = /(?:www.)?/
const pathRegex = /(?:youtube(?:-nocookie)?\.com\/(?:[^\\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)/
const idRegex = /([^"&?\\/\s]{11})/
const tailRegex = /(?:[?&#].*)?/
const youtubeVideoUrlRegex = new RegExp(`(?:${protocolRegex.source}${subdomainRegex.source}${pathRegex.source}${idRegex.source}${tailRegex.source})`)
const linkRegex = new RegExp(`^${youtubeVideoUrlRegex.source}$`, 'i')
export const replaceYouTubeLink: RegexOptions = {
name: 'youtube-link',
regex: linkRegex,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<codimd-youtube id="${match}"></codimd-youtube>`
}
}

View file

@ -0,0 +1,5 @@
.gist-frame {
.one-click-embedding-preview {
filter: blur(3px);
}
}

View file

@ -0,0 +1,88 @@
import { DomElement } from 'domhandler'
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react'
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
import { getIdFromCodiMdTag } from '../video-util'
import './gist-frame.scss'
import preview from './gist-preview.png'
export interface GistFrameProps {
id: string
}
interface resizeEvent {
size: number
id: string
}
const getElementReplacement = (node: DomElement, counterMap: Map<string, number>): (ReactElement | undefined) => {
const gistId = getIdFromCodiMdTag(node, 'gist')
if (gistId) {
const count = (counterMap.get(gistId) || 0) + 1
counterMap.set(gistId, count)
return (
<OneClickEmbedding previewContainerClassName={'gist-frame'} key={`gist_${gistId}_${count}`} loadingImageUrl={preview} hoverIcon={'github'} tooltip={'click to load gist'}>
<GistFrame id={gistId}/>
</OneClickEmbedding>
)
}
}
export const GistFrame: React.FC<GistFrameProps> = ({ id }) => {
const iframeHtml = useMemo(() => {
return (`
<html lang="en">
<head>
<base target="_parent">
<title>gist</title>
<style>
* { font-size:12px; }
body{ overflow:hidden; margin: 0;}
</style>
<script type="text/javascript">
function doLoad() {
window.parent.postMessage({eventType: 'gistResize', size: document.body.scrollHeight, id: '${id}'}, '*')
tweakLinks();
}
function tweakLinks() {
document.querySelectorAll(".gist-meta > a").forEach((link) => {
link.rel="noopener noreferer"
link.target="_blank"
})
}
</script>
</head>
<body onload="doLoad()">
<script type="text/javascript" src="https://gist.github.com/${id}.js"></script>
</body>
</html>`)
}, [id])
const [frameHeight, setFrameHeight] = useState(0)
const sizeMessage = useCallback((message: MessageEvent) => {
const data = message.data as resizeEvent
if (data.id !== id) {
return
}
setFrameHeight(data.size)
}, [id])
useEffect(() => {
window.addEventListener('message', sizeMessage)
return () => {
window.removeEventListener('message', sizeMessage)
}
}, [sizeMessage])
return (
<iframe
sandbox="allow-scripts allow-top-navigation-by-user-activation allow-popups"
width='100%'
height={`${frameHeight}px`}
frameBorder='0'
title={`gist ${id}`}
src={`data:text/html;base64,${btoa(iframeHtml)}`}/>
)
}
export { getElementReplacement as getGistReplacement }

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -0,0 +1,30 @@
.one-click-embedding {
position: relative;
cursor: pointer;
width: 100%;
overflow: hidden;
.one-click-embedding-icon {
display: inline;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: white;
opacity: 0.4;
transition: opacity 0.2s;
text-shadow: #000000 0 0 5px;
}
&:hover > i {
opacity: 0.8;
text-shadow: #000000 0 0 10px;
}
.one-click-embedding-preview {
background: none;
height: 100%;
object-fit: contain;
}
}

View file

@ -0,0 +1,49 @@
import React, { useEffect, useState } from 'react'
import { IconName } from '../../../../common/fork-awesome/fork-awesome-icon'
import { ShowIf } from '../../../../common/show-if/show-if'
import './one-click-embedding.scss'
interface OneClickFrameProps {
onImageFetch?: () => Promise<string>
loadingImageUrl: string
hoverIcon?: IconName
tooltip?: string
containerClassName?: string
previewContainerClassName?: string
}
export const OneClickEmbedding: React.FC<OneClickFrameProps> = ({ previewContainerClassName, containerClassName, onImageFetch, loadingImageUrl, children, tooltip, hoverIcon }) => {
const [showFrame, setShowFrame] = useState(false)
const [previewImageLink, setPreviewImageLink] = useState<string>(loadingImageUrl)
const showChildren = () => {
setShowFrame(true)
}
useEffect(() => {
if (!onImageFetch) {
return
}
onImageFetch().then((imageLink) => {
setPreviewImageLink(imageLink)
}).catch((message) => {
console.error(message)
})
}, [onImageFetch])
return (
<span className={ containerClassName }>
<ShowIf condition={showFrame}>
{children}
</ShowIf>
<ShowIf condition={!showFrame}>
<span className={`one-click-embedding ${previewContainerClassName || ''}`} onClick={showChildren}>
<img className={'one-click-embedding-preview'} src={previewImageLink} alt={tooltip || ''} title={tooltip || ''}/>
<ShowIf condition={!!hoverIcon}>
<i className={`one-click-embedding-icon fa fa-${hoverIcon as string} fa-5x`}/>
</ShowIf>
</span>
</ShowIf>
</span>
)
}

View file

@ -0,0 +1,12 @@
import { DomElement } from 'domhandler'
export const getIdFromCodiMdTag = (node: DomElement, tagName: string): (string | undefined) => {
if (node.name !== `codimd-${tagName}` || !node.attribs || !node.attribs.id) {
return
}
return node.attribs.id
}
export interface VideoFrameProps {
id: string
}

View file

@ -0,0 +1,49 @@
import { DomElement } from 'domhandler'
import React, { ReactElement, useCallback } from 'react'
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
import { getIdFromCodiMdTag, VideoFrameProps } from '../video-util'
const getElementReplacement = (node: DomElement, counterMap: Map<string, number>): (ReactElement | undefined) => {
const videoId = getIdFromCodiMdTag(node, 'vimeo')
if (videoId) {
const count = (counterMap.get(videoId) || 0) + 1
counterMap.set(videoId, count)
return <VimeoFrame key={`vimeo_${videoId}_${count}`} id={videoId}/>
}
}
interface VimeoApiResponse {
// Vimeo uses strange names for their fields. ESLint doesn't like that.
// eslint-disable-next-line camelcase
thumbnail_large?: string
}
export const VimeoFrame: React.FC<VideoFrameProps> = ({ id }) => {
const getPreviewImageLink = useCallback(async () => {
const response = await fetch(`https://vimeo.com/api/v2/video/${id}.json`, {
credentials: 'omit',
referrerPolicy: 'no-referrer'
})
if (response.status !== 200) {
throw new Error('Error while loading data from vimeo api')
}
const vimeoResponse: VimeoApiResponse[] = await response.json() as VimeoApiResponse[]
if (vimeoResponse[0] && vimeoResponse[0].thumbnail_large) {
return vimeoResponse[0].thumbnail_large
} else {
throw new Error('Invalid vimeo response')
}
}, [id])
return (
<OneClickEmbedding containerClassName={'embed-responsive embed-responsive-16by9'} previewContainerClassName={'embed-responsive-item'} loadingImageUrl={'https://i.vimeocdn.com/video/'} hoverIcon={'vimeo-square'}
onImageFetch={getPreviewImageLink}>
<iframe className='embed-responsive-item' title={`vimeo video of ${id}`}
src={`https://player.vimeo.com/video/${id}?autoplay=1`}
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"/>
</OneClickEmbedding>
)
}
export { getElementReplacement as getVimeoReplacement }

View file

@ -0,0 +1,25 @@
import { DomElement } from 'domhandler'
import React, { ReactElement } from 'react'
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
import { getIdFromCodiMdTag, VideoFrameProps } from '../video-util'
const getElementReplacement = (node: DomElement, counterMap: Map<string, number>): (ReactElement | undefined) => {
const videoId = getIdFromCodiMdTag(node, 'youtube')
if (videoId) {
const count = (counterMap.get(videoId) || 0) + 1
counterMap.set(videoId, count)
return <YouTubeFrame key={`youtube_${videoId}_${count}`} id={videoId}/>
}
}
export const YouTubeFrame: React.FC<VideoFrameProps> = ({ id }) => {
return (
<OneClickEmbedding containerClassName={'embed-responsive embed-responsive-16by9'} previewContainerClassName={'embed-responsive-item'} hoverIcon={'youtube-play'} loadingImageUrl={`//i.ytimg.com/vi/${id}/maxresdefault.jpg`}>
<iframe className='embed-responsive-item' title={`youtube video of ${id}`}
src={`//www.youtube-nocookie.com/embed/${id}?autoplay=1`}
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"/>
</OneClickEmbedding>
)
}
export { getElementReplacement as getYouTubeReplacement }

View file

@ -0,0 +1,6 @@
declare module 'markdown-it-emoji' {
import MarkdownIt from 'markdown-it/lib'
const markdownItEmoji: MarkdownIt.PluginSimple
export = markdownItEmoji
}

View file

@ -0,0 +1,6 @@
declare module 'markdown-it-regex' {
import MarkdownIt from 'markdown-it/lib'
import { RegexOptions } from './interface'
const markdownItRegex: MarkdownIt.PluginWithOptions<RegexOptions>
export = markdownItRegex
}

View file

@ -0,0 +1,5 @@
export interface RegexOptions {
name: string,
regex: RegExp,
replace: (match: string) => string
}

View file

@ -0,0 +1,6 @@
declare module 'markdown-it-task-lists' {
import MarkdownIt from 'markdown-it/lib'
const markdownItTaskLists: MarkdownIt.PluginSimple
export = markdownItTaskLists
}

Binary file not shown.

View file

@ -31,3 +31,12 @@ body {
.mvh-100 {
min-height: 100vh;
}
.overflow-y-scroll {
overflow-y: scroll;
}
@font-face {
font-family: "twemoji";
src: url("TwemojiMozilla.ttf") format("truetype");
}

View file

@ -3,7 +3,7 @@ import { EditorMode } from '../../components/editor/task-bar/editor-view-mode'
import { EditorConfig, EditorConfigActions, EditorConfigActionType, SetEditorConfigAction } from './types'
export const initialState: EditorConfig = {
editorMode: EditorMode.EDITOR
editorMode: EditorMode.BOTH
}
export const EditorConfigReducer: Reducer<EditorConfig, EditorConfigActions> = (state: EditorConfig = initialState, action: EditorConfigActions) => {

109
yarn.lock
View file

@ -1470,6 +1470,18 @@
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
"@types/domhandler@*":
version "2.4.1"
resolved "https://registry.yarnpkg.com/@types/domhandler/-/domhandler-2.4.1.tgz#7b3b347f7762180fbcb1ece1ce3dd0ebbb8c64cf"
integrity sha512-cfBw6q6tT5sa1gSPFSRKzF/xxYrrmeiut7E0TxNBObiLSBTuFEHibcfEe3waQPEDbqBsq+ql/TOniw65EyDFMA==
"@types/domutils@*":
version "1.7.2"
resolved "https://registry.yarnpkg.com/@types/domutils/-/domutils-1.7.2.tgz#89422e579c165994ad5c09ce90325da596cc105d"
integrity sha512-Nnwy1Ztwq42SSNSZSh9EXBJGrOZPR+PQ2sRT4VZy8hnsFXfCil7YlKO2hd2360HyrtFz2qwnKQ13ENrgXNxJbw==
dependencies:
"@types/domhandler" "*"
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
@ -1507,6 +1519,15 @@
"@types/react" "*"
hoist-non-react-statics "^3.3.0"
"@types/htmlparser2@*":
version "3.10.1"
resolved "https://registry.yarnpkg.com/@types/htmlparser2/-/htmlparser2-3.10.1.tgz#1e65ba81401d53f425c1e2ba5a3d05c90ab742c7"
integrity sha512-fCxmHS4ryCUCfV9+CJZY1UjkbR+6Al/EQdX5Jh03qBj9gdlPG5q+7uNoDgE/ZNXb3XNWSAQgqKIWnbRCbOyyWA==
dependencies:
"@types/domhandler" "*"
"@types/domutils" "*"
"@types/node" "*"
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
@ -1553,6 +1574,24 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/linkify-it@*":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@types/linkify-it/-/linkify-it-2.1.0.tgz#ea3dd64c4805597311790b61e872cbd1ed2cd806"
integrity sha512-Q7DYAOi9O/+cLLhdaSvKdaumWyHbm7HAk/bFwwyTuU0arR5yyCeW5GOoqt4tJTpDRxhpx9Q8kQL6vMpuw9hDSw==
"@types/markdown-it@^10.0.1":
version "10.0.1"
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-10.0.1.tgz#94e252ab689c8e9ceb9aff2946e0a458390105eb"
integrity sha512-L1ibTdA5IUe/cRBlf3N3syAOBQSN1WCMGtAWir6mKxibiRl4LmpZM4jLz+7zAqiMnhQuAP1sqZOF9wXgn2kpEg==
dependencies:
"@types/linkify-it" "*"
"@types/mdurl" "*"
"@types/mdurl@*":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@types/mdurl/-/mdurl-1.0.2.tgz#e2ce9d83a613bacf284c7be7d491945e39e1f8e9"
integrity sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@ -1609,6 +1648,14 @@
dependencies:
"@types/react" "*"
"@types/react-html-parser@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@types/react-html-parser/-/react-html-parser-2.0.1.tgz#2d9002ac5bf1adf9aff8eae77ace5488bd78c98d"
integrity sha512-Lyw0AtG3gahw78CX2pzmzhKaoZCfJNzzuhhPsFVhzFrylMv8NaCmzYaPKglMv3RRHpwBbHuMOkVx0HiwGZKgSA==
dependencies:
"@types/htmlparser2" "*"
"@types/react" "*"
"@types/react-redux@7.1.9":
version "7.1.9"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.1.9.tgz#280c13565c9f13ceb727ec21e767abe0e9b4aec3"
@ -4177,6 +4224,11 @@ entities@^2.0.0:
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4"
integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw==
entities@~2.0.0:
version "2.0.3"
resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f"
integrity sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==
errno@^0.1.3, errno@~0.1.7:
version "0.1.7"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
@ -5157,6 +5209,11 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
github-markdown-css@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/github-markdown-css/-/github-markdown-css-4.0.0.tgz#be9f4caf7a389228d4c368336260ffc909061f35"
integrity sha512-mH0bcIKv4XAN0mQVokfTdKo2OD5K8WJE9+lbMdM32/q0Ie5tXgVN/2o+zvToRMxSTUuiTRcLg5hzkFfOyBYreg==
glob-parent@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
@ -5495,7 +5552,7 @@ html-webpack-plugin@4.0.0-beta.11:
tapable "^1.1.3"
util.promisify "1.0.0"
htmlparser2@^3.3.0:
htmlparser2@^3.3.0, htmlparser2@^3.9.0:
version "3.10.1"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
@ -6915,6 +6972,13 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=
linkify-it@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.2.tgz#f55eeb8bc1d3ae754049e124ab3bb56d97797fb8"
integrity sha512-gDBO4aHNZS6coiZCKVhSNh43F9ioIL4JwRjLZPkoLIY4yZFwg264Y5lu2x6rb1Js42Gh6Yqm2f6L2AJcnkzinQ==
dependencies:
uc.micro "^1.0.1"
load-json-file@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0"
@ -7143,6 +7207,32 @@ map-visit@^1.0.0:
dependencies:
object-visit "^1.0.0"
markdown-it-emoji@^1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz#9bee0e9a990a963ba96df6980c4fddb05dfb4dcc"
integrity sha1-m+4OmpkKljupbfaYDE/dsF37Tcw=
markdown-it-regex@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/markdown-it-regex/-/markdown-it-regex-0.2.0.tgz#e09ad2d75209720d591d3949e1142c75c0fbecf6"
integrity sha512-111UnMGJSt37gy+DlgcpQNwEfS2jvscOFSztzGhuXUHk7K1J5eAEj6C3jifmKb0cWtTuxdpHgIt4PyGQ+DtDjw==
markdown-it-task-lists@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz#f68f4d2ac2bad5a2c373ba93081a1a6848417088"
integrity sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==
markdown-it@^11.0.0:
version "11.0.0"
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-11.0.0.tgz#dbfc30363e43d756ebc52c38586b91b90046b876"
integrity sha512-+CvOnmbSubmQFSA9dKz1BRiaSMV7rhexl3sngKqFyXSagoA3fBdJQ8oZWtRy2knXdpDXaBw44euz37DeJQ9asg==
dependencies:
argparse "^1.0.7"
entities "~2.0.0"
linkify-it "^3.0.1"
mdurl "^1.0.1"
uc.micro "^1.0.5"
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@ -7162,6 +7252,11 @@ mdn-data@2.0.6:
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.6.tgz#852dc60fcaa5daa2e8cf6c9189c440ed3e042978"
integrity sha512-rQvjv71olwNHgiTbfPZFkJtjNMciWgswYeciZhtvWLO8bmX3TnhyA62I6sTWOyZssWHJJjY6/KiWwqQsWWsqOA==
mdurl@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=
media-typer@0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
@ -9335,6 +9430,13 @@ react-error-overlay@^6.0.7:
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108"
integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==
react-html-parser@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/react-html-parser/-/react-html-parser-2.0.2.tgz#6dbe1ddd2cebc1b34ca15215158021db5fc5685e"
integrity sha512-XeerLwCVjTs3njZcgCOeDUqLgNIt/t+6Jgi5/qPsO/krUWl76kWKXMeVs2LhY2gwM6X378DkhLjur0zUQdpz0g==
dependencies:
htmlparser2 "^3.9.0"
react-i18next@11.5.1:
version "11.5.1"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.5.1.tgz#084e98555d8800d5508eccc8da1ac652df358d3e"
@ -11187,6 +11289,11 @@ typescript@3.9.5:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.5.tgz#586f0dba300cde8be52dd1ac4f7e1009c1b13f36"
integrity sha512-hSAifV3k+i6lEoCJ2k6R2Z/rp/H3+8sdmcn5NrS3/3kE7+RyZXm9aqvxWqjEXHAd8b0pShatpcdMTvEdvAJltQ==
uc.micro@^1.0.1, uc.micro@^1.0.5:
version "1.0.6"
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
uncontrollable@^7.0.0:
version "7.1.1"
resolved "https://registry.yarnpkg.com/uncontrollable/-/uncontrollable-7.1.1.tgz#f67fed3ef93637126571809746323a9db815d556"