mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-24 18:56:32 -05:00
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:
parent
8ba2be7c70
commit
7189a63618
31 changed files with 637 additions and 16 deletions
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>`
|
||||
}
|
||||
}
|
|
@ -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>`
|
||||
}
|
||||
}
|
|
@ -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>`
|
||||
}
|
||||
}
|
|
@ -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>`
|
||||
}
|
||||
}
|
|
@ -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>`
|
||||
}
|
||||
}
|
|
@ -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>`
|
||||
}
|
||||
}
|
|
@ -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>`
|
||||
}
|
||||
}
|
|
@ -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>`
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.gist-frame {
|
||||
.one-click-embedding-preview {
|
||||
filter: blur(3px);
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 }
|
|
@ -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 }
|
6
src/external-types/markdown-it-emoji/index.d.ts
vendored
Normal file
6
src/external-types/markdown-it-emoji/index.d.ts
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
declare module 'markdown-it-emoji' {
|
||||
import MarkdownIt from 'markdown-it/lib'
|
||||
const markdownItEmoji: MarkdownIt.PluginSimple
|
||||
export = markdownItEmoji
|
||||
}
|
6
src/external-types/markdown-it-regex/index.d.ts
vendored
Normal file
6
src/external-types/markdown-it-regex/index.d.ts
vendored
Normal 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
|
||||
}
|
5
src/external-types/markdown-it-regex/interface.ts
Normal file
5
src/external-types/markdown-it-regex/interface.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export interface RegexOptions {
|
||||
name: string,
|
||||
regex: RegExp,
|
||||
replace: (match: string) => string
|
||||
}
|
6
src/external-types/markdown-it-task-lists/index.d.ts
vendored
Normal file
6
src/external-types/markdown-it-task-lists/index.d.ts
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
declare module 'markdown-it-task-lists' {
|
||||
import MarkdownIt from 'markdown-it/lib'
|
||||
const markdownItTaskLists: MarkdownIt.PluginSimple
|
||||
export = markdownItTaskLists
|
||||
}
|
BIN
src/global-style/TwemojiMozilla.ttf
Normal file
BIN
src/global-style/TwemojiMozilla.ttf
Normal file
Binary file not shown.
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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
109
yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue