Restructure repository ()

organized repository 

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
Co-authored-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
Co-authored-by: Philip Molares <git@molar.es>
This commit is contained in:
mrdrogdrog 2020-08-16 16:02:26 +02:00 committed by GitHub
parent 66258ca615
commit 0fadc09f2b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
254 changed files with 384 additions and 403 deletions

View file

@ -0,0 +1,15 @@
import Renderer from 'markdown-it/lib/renderer'
import Token from 'markdown-it/lib/token'
type RenderContainerReturn = (tokens: Token[], index: number, options: any, env: any, self: Renderer) => void;
type ValidAlertLevels = ('warning' | 'danger' | 'success' | 'info')
export const validAlertLevels: ValidAlertLevels[] = ['success', 'danger', 'info', 'warning']
export const createRenderContainer = (level: ValidAlertLevels): RenderContainerReturn => {
return (tokens: Token[], index: number, options: any, env: any, self: Renderer) => {
tokens[index].attrJoin('role', 'alert')
tokens[index].attrJoin('class', 'alert')
tokens[index].attrJoin('class', `alert-${level}`)
return self.renderToken(tokens, index, options)
}
}

View file

@ -0,0 +1,29 @@
import MarkdownIt from 'markdown-it/lib'
const highlightRegex = /^ *(\w*)(=(\d*|\+))?(!?)$/
export const highlightedCode: MarkdownIt.PluginSimple = (md: MarkdownIt) => {
md.core.ruler.push('highlighted-code', (state) => {
state.tokens.forEach(token => {
if (token.type === 'fence') {
const highlightInfos = highlightRegex.exec(token.info)
if (!highlightInfos) {
return
}
if (highlightInfos[1]) {
token.attrJoin('data-highlight-language', highlightInfos[1])
}
if (highlightInfos[2]) {
token.attrJoin('data-show-line-numbers', '')
}
if (highlightInfos[3]) {
token.attrJoin('data-start-line-number', highlightInfos[3])
}
if (highlightInfos[4]) {
token.attrJoin('data-wrap-lines', '')
}
}
})
return true
})
}

View file

@ -0,0 +1,13 @@
import MarkdownIt from 'markdown-it/lib'
import linkify from 'markdown-it/lib/rules_core/linkify'
export const linkifyExtra: MarkdownIt.PluginSimple = (md) => {
md.core.ruler.push('linkify', state => {
try {
state.md.options.linkify = true
return linkify(state)
} finally {
state.md.options.linkify = false
}
})
}

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,18 @@
import MarkdownIt, { Options } from 'markdown-it/lib'
import Renderer, { RenderRule } from 'markdown-it/lib/renderer'
import Token from 'markdown-it/lib/token'
export const plantumlError: MarkdownIt.PluginSimple = (md) => {
const defaultRenderer: RenderRule = md.renderer.rules.fence || (() => '')
md.renderer.rules.fence = (tokens: Token[], idx: number, options: Options, env, slf: Renderer) => {
const token = tokens[idx]
if (token.info === 'plantuml') {
return `
<p class="alert alert-danger">
PlantUML plugin is enabled but not properly configured.
</p>
`
}
return defaultRenderer(tokens, idx, options, env, slf)
}
}

View file

@ -0,0 +1,62 @@
@import '../../../node_modules/github-markdown-css/github-markdown.css';
.markdown-body {
position: relative;
font-family: 'Source Sans Pro', "twemoji", sans-serif;
word-break: break-word;
.alert > p, .alert > ul {
margin-bottom: 0;
}
// This is necessary since we need to set this for all DOM Element that could be children of .markdown-body and since we support all of HTML that would literally be everything
& > * {
width: 100%;
max-width: 900px;
}
& > img {
width: unset;
background-color: unset;
}
&.wider {
max-width: 1500px;
& > .wider-possible {
max-width: 1500px;
}
}
a.heading-anchor {
margin-left: -1.25em;
font-size: 0.75em;
margin-top: 0.25em;
opacity: 0;
&:hover {
opacity: 1;
}
}
blockquote .quote-extra {
font-size: 0.85em;
margin-inline-start: 0.5em;
&:first-of-type {
&::before {
content: '\2014 \00A0'
}
}
}
pre {
overflow: visible;
code {
white-space: pre-wrap;
word-break: break-word;
}
}
}

View file

@ -0,0 +1,307 @@
import equal from 'deep-equal'
import { DomElement } from 'domhandler'
import emojiData from 'emoji-mart/data/twitter.json'
import { Data } from 'emoji-mart/dist-es/utils/data'
import yaml from 'js-yaml'
import MarkdownIt from 'markdown-it'
import abbreviation from 'markdown-it-abbr'
import anchor from 'markdown-it-anchor'
import markdownItContainer from 'markdown-it-container'
import definitionList from 'markdown-it-deflist'
import emoji from 'markdown-it-emoji'
import footnote from 'markdown-it-footnote'
import frontmatter from 'markdown-it-front-matter'
import imsize from 'markdown-it-imsize'
import inserted from 'markdown-it-ins'
import marked from 'markdown-it-mark'
import mathJax from 'markdown-it-mathjax'
import plantuml from 'markdown-it-plantuml'
import markdownItRegex from 'markdown-it-regex'
import subscript from 'markdown-it-sub'
import superscript from 'markdown-it-sup'
import taskList from 'markdown-it-task-lists'
import toc from 'markdown-it-toc-done-right'
import React, { ReactElement, useEffect, useMemo, useRef, useState } from 'react'
import { Alert } from 'react-bootstrap'
import ReactHtmlParser, { convertNodeToElement, Transform } from 'react-html-parser'
import { Trans } from 'react-i18next'
import MathJaxReact from 'react-mathjax'
import { useSelector } from 'react-redux'
import { TocAst } from '../../external-types/markdown-it-toc-done-right/interface'
import { ApplicationState } from '../../redux'
import { InternalLink } from '../common/links/internal-link'
import { ShowIf } from '../common/show-if/show-if'
import { ForkAwesomeIcons } from '../editor/editor-pane/tool-bar/emoji-picker/icon-names'
import { slugify } from '../editor/table-of-contents/table-of-contents'
import { RawYAMLMetadata, YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
import { createRenderContainer, validAlertLevels } from './container-plugins/alert'
import { highlightedCode } from './markdown-it-plugins/highlighted-code'
import { linkifyExtra } from './markdown-it-plugins/linkify-extra'
import { MarkdownItParserDebugger } from './markdown-it-plugins/parser-debugger'
import { plantumlError } from './markdown-it-plugins/plantuml-error'
import './markdown-renderer.scss'
import { replaceAsciinemaLink } from './regex-plugins/replace-asciinema-link'
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 { replacePdfShortCode } from './regex-plugins/replace-pdf-short-code'
import { replaceQuoteExtraAuthor } from './regex-plugins/replace-quote-extra-author'
import { replaceQuoteExtraColor } from './regex-plugins/replace-quote-extra-color'
import { replaceQuoteExtraTime } from './regex-plugins/replace-quote-extra-time'
import { replaceVimeoLink } from './regex-plugins/replace-vimeo-link'
import { replaceYouTubeLink } from './regex-plugins/replace-youtube-link'
import { AsciinemaReplacer } from './replace-components/asciinema/asciinema-replacer'
import { ComponentReplacer, SubNodeConverter } from './replace-components/ComponentReplacer'
import { GistReplacer } from './replace-components/gist/gist-replacer'
import { HighlightedCodeReplacer } from './replace-components/highlighted-fence/highlighted-fence-replacer'
import { ImageReplacer } from './replace-components/image/image-replacer'
import { MathjaxReplacer } from './replace-components/mathjax/mathjax-replacer'
import { PdfReplacer } from './replace-components/pdf/pdf-replacer'
import { PossibleWiderReplacer } from './replace-components/possible-wider/possible-wider-replacer'
import { QuoteOptionsReplacer } from './replace-components/quote-options/quote-options-replacer'
import { TocReplacer } from './replace-components/toc/toc-replacer'
import { VimeoReplacer } from './replace-components/vimeo/vimeo-replacer'
import { YoutubeReplacer } from './replace-components/youtube/youtube-replacer'
export interface MarkdownRendererProps {
content: string
wide?: boolean
className?: string
onTocChange?: (ast: TocAst) => void
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void
onFirstHeadingChange?: (firstHeading: string | undefined) => void
}
const markdownItTwitterEmojis = Object.keys((emojiData as unknown as Data).emojis)
.reduce((reduceObject, emojiIdentifier) => {
const emoji = (emojiData as unknown as Data).emojis[emojiIdentifier]
const emojiCodes = emoji.unified ?? emoji.b
if (emojiCodes) {
reduceObject[emojiIdentifier] = emojiCodes.split('-').map(char => `&#x${char};`).join('')
}
return reduceObject
}, {} as { [key: string]: string })
const emojiSkinToneModifierMap = [2, 3, 4, 5, 6]
.reduce((reduceObject, modifierValue) => {
const lightSkinCode = 127995
const codepoint = lightSkinCode + (modifierValue - 2)
const shortcode = `skin-tone-${modifierValue}`
reduceObject[shortcode] = `&#${codepoint};`
return reduceObject
}, {} as { [key: string]: string })
const forkAwesomeIconMap = Object.keys(ForkAwesomeIcons)
.reduce((reduceObject, icon) => {
const shortcode = `fa-${icon}`
// noinspection CheckTagEmptyBody
reduceObject[shortcode] = `<i class="fa fa-${icon}"></i>`
return reduceObject
}, {} as { [key: string]: string })
export const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({ content, onMetaDataChange, onFirstHeadingChange, onTocChange, className, wide }) => {
const [tocAst, setTocAst] = useState<TocAst>()
const [lastTocAst, setLastTocAst] = useState<TocAst>()
const [yamlError, setYamlError] = useState(false)
const rawMetaRef = useRef<RawYAMLMetadata>()
const oldMetaRef = useRef<RawYAMLMetadata>()
const firstHeadingRef = useRef<string>()
const oldFirstHeadingRef = useRef<string>()
useEffect(() => {
if (onMetaDataChange && !equal(oldMetaRef.current, rawMetaRef.current)) {
if (rawMetaRef.current) {
const newMetaData = new YAMLMetaData(rawMetaRef.current)
onMetaDataChange(newMetaData)
} else {
onMetaDataChange(undefined)
}
oldMetaRef.current = rawMetaRef.current
}
if (onFirstHeadingChange && !equal(firstHeadingRef.current, oldFirstHeadingRef.current)) {
onFirstHeadingChange(firstHeadingRef.current || undefined)
oldFirstHeadingRef.current = firstHeadingRef.current
}
})
const plantumlServer = useSelector((state: ApplicationState) => state.config.plantumlServer)
const markdownIt = useMemo(() => {
const md = new MarkdownIt('default', {
html: true,
breaks: true,
langPrefix: '',
typographer: true
})
if (onFirstHeadingChange) {
md.core.ruler.after('normalize', 'extract first L1 heading', (state) => {
const lines = state.src.split('\n')
const linkAltTextRegex = /!?\[([^\]]*)]\([^)]*\)/
for (const line of lines) {
if (line.startsWith('# ')) {
firstHeadingRef.current = line.replace('# ', '').replace(linkAltTextRegex, '$1')
return true
}
}
firstHeadingRef.current = undefined
return true
})
}
if (onMetaDataChange) {
md.use(frontmatter, (rawMeta: string) => {
try {
const meta: RawYAMLMetadata = yaml.safeLoad(rawMeta) as RawYAMLMetadata
setYamlError(false)
rawMetaRef.current = meta
} catch (e) {
console.error(e)
setYamlError(true)
}
})
}
md.use(taskList)
if (plantumlServer) {
md.use(plantuml, {
openMarker: '```plantuml',
closeMarker: '```',
server: plantumlServer
})
} else {
md.use(plantumlError)
}
md.use(emoji, {
defs: {
...markdownItTwitterEmojis,
...emojiSkinToneModifierMap,
...forkAwesomeIconMap
}
})
md.use(abbreviation)
md.use(definitionList)
md.use(subscript)
md.use(superscript)
md.use(inserted)
md.use(marked)
md.use(footnote)
if (onMetaDataChange) {
md.use(frontmatter, (rawMeta: string) => {
try {
const meta: RawYAMLMetadata = yaml.safeLoad(rawMeta) as RawYAMLMetadata
setYamlError(false)
rawMetaRef.current = meta
} catch (e) {
console.error(e)
setYamlError(true)
rawMetaRef.current = ({} as RawYAMLMetadata)
}
})
}
md.use(imsize)
// noinspection CheckTagEmptyBody
md.use(anchor, {
permalink: true,
permalinkBefore: true,
permalinkClass: 'heading-anchor text-dark',
permalinkSymbol: '<i class="fa fa-link"></i>'
})
md.use(mathJax({
beforeMath: '<codimd-mathjax>',
afterMath: '</codimd-mathjax>',
beforeInlineMath: '<codimd-mathjax inline>',
afterInlineMath: '</codimd-mathjax>',
beforeDisplayMath: '<codimd-mathjax>',
afterDisplayMath: '</codimd-mathjax>'
}))
md.use(markdownItRegex, replaceLegacyYoutubeShortCode)
md.use(markdownItRegex, replaceLegacyVimeoShortCode)
md.use(markdownItRegex, replaceLegacyGistShortCode)
md.use(markdownItRegex, replaceLegacySlideshareShortCode)
md.use(markdownItRegex, replaceLegacySpeakerdeckShortCode)
md.use(markdownItRegex, replacePdfShortCode)
md.use(markdownItRegex, replaceAsciinemaLink)
md.use(markdownItRegex, replaceYouTubeLink)
md.use(markdownItRegex, replaceVimeoLink)
md.use(markdownItRegex, replaceGistLink)
md.use(highlightedCode)
md.use(markdownItRegex, replaceQuoteExtraAuthor)
md.use(markdownItRegex, replaceQuoteExtraColor)
md.use(markdownItRegex, replaceQuoteExtraTime)
md.use(toc, {
placeholder: '(\\[TOC\\]|\\[toc\\])',
listType: 'ul',
level: [1, 2, 3],
callback: (code: string, ast: TocAst): void => {
setTocAst(ast)
},
slugify: slugify
})
md.use(linkifyExtra)
if (process.env.NODE_ENV !== 'production') {
md.use(MarkdownItParserDebugger)
}
validAlertLevels.forEach(level => {
md.use(markdownItContainer, level, { render: createRenderContainer(level) })
})
return md
}, [onMetaDataChange, onFirstHeadingChange, plantumlServer])
useEffect(() => {
if (onTocChange && tocAst && !equal(tocAst, lastTocAst)) {
onTocChange(tocAst)
setLastTocAst(tocAst)
}
}, [tocAst, onTocChange, lastTocAst])
const tryToReplaceNode = (node: DomElement, index: number, allReplacers: ComponentReplacer[], nodeConverter: SubNodeConverter) => {
return allReplacers
.map((componentReplacer) => componentReplacer.getReplacement(node, index, nodeConverter))
.find((replacement) => !!replacement)
}
const result: ReactElement[] = useMemo(() => {
const allReplacers: ComponentReplacer[] = [
new PossibleWiderReplacer(),
new GistReplacer(),
new YoutubeReplacer(),
new VimeoReplacer(),
new AsciinemaReplacer(),
new PdfReplacer(),
new ImageReplacer(),
new TocReplacer(),
new HighlightedCodeReplacer(),
new QuoteOptionsReplacer(),
new MathjaxReplacer()
]
if (onMetaDataChange) {
// This is used if the front-matter callback is never called, because the user deleted everything regarding metadata from the document
rawMetaRef.current = undefined
}
const html: string = markdownIt.render(content)
const transform: Transform = (node, index) => {
const subNodeConverter = (subNode: DomElement, subIndex: number) => convertNodeToElement(subNode, subIndex, transform)
return tryToReplaceNode(node, index, allReplacers, subNodeConverter) || convertNodeToElement(node, index, transform)
}
return ReactHtmlParser(html, { transform: transform })
}, [content, markdownIt, onMetaDataChange])
return (
<div className={`markdown-body ${className || ''} d-flex flex-column align-items-center ${wide ? 'wider' : ''}`}>
<ShowIf condition={yamlError}>
<Alert variant='warning' dir='auto'>
<Trans i18nKey='editor.invalidYaml'>
<InternalLink text='yaml-metadata' href='/n/yaml-metadata' className='text-dark'/>
</Trans>
</Alert>
</ShowIf>
<MathJaxReact.Provider>
{result}
</MathJaxReact.Provider>
</div>
)
}

View file

@ -0,0 +1,18 @@
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface'
const protocolRegex = /(?:http(?:s)?:\/\/)?/
const domainRegex = /(?:asciinema\.org\/a\/)/
const idRegex = /(\d+)/
const tailRegex = /(?:[./?#].*)?/
const gistUrlRegex = new RegExp(`(?:${protocolRegex.source}${domainRegex.source}${idRegex.source}${tailRegex.source})`)
const linkRegex = new RegExp(`^${gistUrlRegex.source}$`, 'i')
export const replaceAsciinemaLink: RegexOptions = {
name: 'asciinema-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-asciinema id="${match}"></codimd-asciinema>`
}
}

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,11 @@
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface'
export const replacePdfShortCode: RegexOptions = {
name: 'pdf-short-code',
regex: /^{%pdf (.*) ?%}$/,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<codimd-pdf url="${match}"></codimd-pdf>`
}
}

View file

@ -0,0 +1,11 @@
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface'
export const replaceQuoteExtraAuthor: RegexOptions = {
name: 'quote-extra-name',
regex: /\[name=([^\]]+)]/,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<span class="quote-extra"><i class="fa fa-user mx-1"></i> ${match}</span>`
}
}

View file

@ -0,0 +1,13 @@
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface'
const cssColorRegex = /\[color=(#(?:[0-9a-f]{2}){2,4}|(?:#[0-9a-f]{3})|black|silver|gray|whitesmoke|maroon|red|purple|fuchsia|green|lime|olivedrab|yellow|navy|blue|teal|aquamarine|orange|aliceblue|antiquewhite|aqua|azure|beige|bisque|blanchedalmond|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|currentcolor|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|gainsboro|ghostwhite|goldenrod|gold|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavenderblush|lavender|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|limegreen|linen|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|oldlace|olive|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|rebeccapurple|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|thistle|tomato|transparent|turquoise|violet|wheat|white|yellowgreen)]/i
export const replaceQuoteExtraColor: RegexOptions = {
name: 'quote-extra-color',
regex: cssColorRegex,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<span class="quote-extra" data-color='${match}' style='color: ${match}'><i class="fa fa-tag"></i></span>`
}
}

View file

@ -0,0 +1,11 @@
import { RegexOptions } from '../../../external-types/markdown-it-regex/interface'
export const replaceQuoteExtraTime: RegexOptions = {
name: 'quote-extra-time',
regex: /\[time=([^\]]+)]/,
replace: (match) => {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<span class="quote-extra"><i class="fa fa-clock-o mx-1"></i> ${match}</span>`
}
}

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,8 @@
import { DomElement } from 'domhandler'
import { ReactElement } from 'react'
export type SubNodeConverter = (node: DomElement, index: number) => ReactElement
export interface ComponentReplacer {
getReplacement: (node: DomElement, index:number, subNodeConverter: SubNodeConverter) => (ReactElement|undefined)
}

View file

@ -0,0 +1,19 @@
import React from 'react'
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
export interface AsciinemaFrameProps {
id: string
}
export const AsciinemaFrame: React.FC<AsciinemaFrameProps> = ({ id }) => {
return (
<OneClickEmbedding
containerClassName={'embed-responsive embed-responsive-16by9'}
previewContainerClassName={'embed-responsive-item'}
hoverIcon={'play'}
loadingImageUrl={`https://asciinema.org/a/${id}.png`}>
<iframe className='embed-responsive-item' title={`asciinema cast ${id}`}
src={`https://asciinema.org/a/${id}/embed?autoplay=1`}/>
</OneClickEmbedding>
)
}

View file

@ -0,0 +1,21 @@
import { DomElement } from 'domhandler'
import React from 'react'
import { getAttributesFromCodiMdTag } from '../codi-md-tag-utils'
import { ComponentReplacer } from '../ComponentReplacer'
import { AsciinemaFrame } from './asciinema-frame'
export class AsciinemaReplacer implements ComponentReplacer {
private counterMap: Map<string, number> = new Map<string, number>()
getReplacement (node: DomElement): React.ReactElement | undefined {
const attributes = getAttributesFromCodiMdTag(node, 'asciinema')
if (attributes && attributes.id) {
const asciinemaId = attributes.id
const count = (this.counterMap.get(asciinemaId) || 0) + 1
this.counterMap.set(asciinemaId, count)
return (
<AsciinemaFrame key={`asciinema_${asciinemaId}_${count}`} id={asciinemaId}/>
)
}
}
}

View file

@ -0,0 +1,8 @@
import { DomElement } from 'domhandler'
export const getAttributesFromCodiMdTag = (node: DomElement, tagName: string): ({ [s: string]: string; }|undefined) => {
if (node.name !== `codimd-${tagName}` || !node.attribs) {
return
}
return node.attribs
}

View file

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

View file

@ -0,0 +1,69 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import './gist-frame.scss'
export interface GistFrameProps {
id: string
}
interface resizeEvent {
size: number
id: string
}
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)}`}/>
)
}

Binary file not shown.

After

(image error) Size: 30 KiB

View file

@ -0,0 +1,25 @@
import { DomElement } from 'domhandler'
import React from 'react'
import { getAttributesFromCodiMdTag } from '../codi-md-tag-utils'
import { ComponentReplacer } from '../ComponentReplacer'
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
import { GistFrame } from './gist-frame'
import preview from './gist-preview.png'
export class GistReplacer implements ComponentReplacer {
private counterMap: Map<string, number> = new Map<string, number>()
getReplacement (node: DomElement): React.ReactElement | undefined {
const attributes = getAttributesFromCodiMdTag(node, 'gist')
if (attributes && attributes.id) {
const gistId = attributes.id
const count = (this.counterMap.get(gistId) || 0) + 1
this.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>
)
}
}
}

View file

@ -0,0 +1,42 @@
.markdown-body {
@import '../../../../../../node_modules/highlight.js/styles/github-gist.css';
}
.markdown-body pre code.hljs {
padding: 16px;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
&.showGutter {
.linenumber {
position: relative;
cursor: default;
z-index: 4;
padding: 0 8px 0 0;
min-width: 20px;
box-sizing: content-box;
color: #afafaf;
border-right: 3px solid #6ce26c;
flex-direction: column;
overflow: hidden;
user-select: none;
display: flex;
align-items: flex-end;
&:before {
content: attr(data-line-number);
}
}
}
&.showGutter .codeline {
margin: 0 0 0 16px;
}
&.wrapLines .codeline {
white-space: pre-wrap;
}
}

View file

@ -0,0 +1,59 @@
import hljs from 'highlight.js'
import React, { Fragment, useMemo } from 'react'
import ReactHtmlParser from 'react-html-parser'
import './highlighted-code.scss'
export interface HighlightedCodeProps {
code: string,
language?: string,
startLineNumber?: number
wrapLines: boolean
}
export const escapeHtml = (unsafe: string): string => {
return unsafe
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}
const checkIfLanguageIsSupported = (language: string): boolean => {
return hljs.listLanguages().indexOf(language) > -1
}
const correctLanguage = (language: string | undefined): string | undefined => {
switch (language) {
case 'html':
return 'xml'
default:
return language
}
}
export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language, startLineNumber, wrapLines }) => {
const highlightedCode = useMemo(() => {
const replacedLanguage = correctLanguage(language)
return ((!!replacedLanguage && checkIfLanguageIsSupported(replacedLanguage)) ? hljs.highlight(replacedLanguage, code).value : escapeHtml(code))
.split('\n')
.filter(line => !!line)
.map(line => ReactHtmlParser(line))
}, [code, language])
return (
<code className={`hljs ${startLineNumber !== undefined ? 'showGutter' : ''} ${wrapLines ? 'wrapLines' : ''}`}>
{
highlightedCode
.map((line, index) => {
return <Fragment key={index}>
<span className={'linenumber'} data-line-number={(startLineNumber || 1) + index}/>
<div className={'codeline'}>
{line}
</div>
</Fragment>
})
}
</code>)
}

View file

@ -0,0 +1,29 @@
import { DomElement } from 'domhandler'
import React from 'react'
import { HighlightedCode } from './highlighted-code/highlighted-code'
import { ComponentReplacer } from '../ComponentReplacer'
export class HighlightedCodeReplacer implements ComponentReplacer {
private lastLineNumber = 0;
getReplacement (codeNode: DomElement, index: number): React.ReactElement | undefined {
if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || !codeNode.children || !codeNode.children[0]) {
return
}
const language = codeNode.attribs['data-highlight-language']
const showLineNumbers = codeNode.attribs['data-show-line-numbers'] !== undefined
const startLineNumberAttribute = codeNode.attribs['data-start-line-number']
const startLineNumber = startLineNumberAttribute === '+' ? this.lastLineNumber : (parseInt(startLineNumberAttribute) || 1)
const wrapLines = codeNode.attribs['data-wrap-lines'] !== undefined
const code = codeNode.children[0].data as string
if (showLineNumbers) {
this.lastLineNumber = startLineNumber + code.split('\n')
.filter(line => !!line).length
}
return <HighlightedCode key={index} language={language} startLineNumber={showLineNumbers ? startLineNumber : undefined} wrapLines={wrapLines} code={code}/>
}
}

View file

@ -0,0 +1,28 @@
import React, { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { getProxiedUrl } from '../../../../api/media'
import { ApplicationState } from '../../../../redux'
export const ImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = ({ alt, src, ...props }) => {
const [imageUrl, setImageUrl] = useState('')
const imageProxyEnabled = useSelector((state: ApplicationState) => state.config.useImageProxy)
useEffect(() => {
if (!imageProxyEnabled || !src) {
return
}
getProxiedUrl(src)
.then(proxyResponse => setImageUrl(proxyResponse.src))
.catch(err => console.error(err))
}, [imageProxyEnabled, src])
if (imageProxyEnabled) {
return (
<img alt={alt} src={imageUrl} {...props}/>
)
}
return (
<img alt={alt} src={src ?? ''} {...props}/>
)
}

View file

@ -0,0 +1,20 @@
import { DomElement } from 'domhandler'
import React from 'react'
import { ComponentReplacer } from '../ComponentReplacer'
import { ImageFrame } from './image-frame'
export class ImageReplacer implements ComponentReplacer {
getReplacement (node: DomElement, index: number): React.ReactElement | undefined {
if (node.name === 'img' && node.attribs) {
return <ImageFrame
key={index}
id={node.attribs.id}
className={node.attribs.class}
src={node.attribs.src}
alt={node.attribs.alt}
width={node.attribs.width}
height={node.attribs.height}
/>
}
}
}

View file

@ -0,0 +1,3 @@
export interface ImageProxyResponse {
src: string
}

View file

@ -0,0 +1,27 @@
import { DomElement } from 'domhandler'
import React from 'react'
import MathJax from 'react-mathjax'
import { ComponentReplacer, SubNodeConverter } from '../ComponentReplacer'
const getNodeIfMathJaxBlock = (node: DomElement): (DomElement|undefined) => {
if (node.name !== 'p' || !node.children || node.children.length !== 1) {
return
}
const mathJaxNode = node.children[0]
return (mathJaxNode.name === 'codimd-mathjax' && mathJaxNode.attribs?.inline === undefined) ? mathJaxNode : undefined
}
const getNodeIfInlineMathJax = (node: DomElement): (DomElement|undefined) => {
return (node.name === 'codimd-mathjax' && node.attribs?.inline !== undefined) ? node : undefined
}
export class MathjaxReplacer implements ComponentReplacer {
getReplacement (node: DomElement, index: number, subNodeConverter: SubNodeConverter): React.ReactElement | undefined {
const mathJax = getNodeIfMathJaxBlock(node) || getNodeIfInlineMathJax(node)
if (mathJax?.children && mathJax.children[0]) {
const mathJaxContent = mathJax.children[0]?.data as string
const isInline = (mathJax.attribs?.inline) !== undefined
return <MathJax.Node key={index} inline={isInline} formula={mathJaxContent}/>
}
}
}

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 > .one-click-embedding-icon {
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,63 @@
import React, { useEffect, useState } from 'react'
import { Trans } from 'react-i18next'
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
hoverTextI18nKey?: string
tooltip?: string
containerClassName?: string
previewContainerClassName?: string
onActivate?: () => void
}
export const OneClickEmbedding: React.FC<OneClickFrameProps> = ({ previewContainerClassName, containerClassName, onImageFetch, loadingImageUrl, children, tooltip, hoverIcon, hoverTextI18nKey, onActivate }) => {
const [showFrame, setShowFrame] = useState(false)
const [previewImageUrl, setPreviewImageUrl] = useState(loadingImageUrl)
const showChildren = () => {
setShowFrame(true)
if (onActivate) {
onActivate()
}
}
useEffect(() => {
if (!onImageFetch) {
return
}
onImageFetch().then((imageLink) => {
setPreviewImageUrl(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}>
<ShowIf condition={!!previewImageUrl}>
<img className={'one-click-embedding-preview'} src={previewImageUrl} alt={tooltip || ''} title={tooltip || ''}/>
</ShowIf>
<ShowIf condition={!!hoverIcon}>
<span className='one-click-embedding-icon text-center'>
<i className={`fa fa-${hoverIcon as string} fa-5x mb-2`} />
<ShowIf condition={!!hoverTextI18nKey}>
<br />
<Trans i18nKey={hoverTextI18nKey} />
</ShowIf>
</span>
</ShowIf>
</span>
</ShowIf>
</span>
)
}

View file

@ -0,0 +1,3 @@
.pdf-frame {
width: 100%;
}

View file

@ -0,0 +1,24 @@
import React, { useState } from 'react'
import { ExternalLink } from '../../../common/links/external-link'
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
import './pdf-frame.scss'
export interface PdfFrameProps {
url: string
}
export const PdfFrame: React.FC<PdfFrameProps> = ({ url }) => {
const [activated, setActivated] = useState(false)
return (
<OneClickEmbedding containerClassName={`embed-responsive embed-responsive-${activated ? '4by3' : '16by9'}`}
previewContainerClassName={'embed-responsive-item bg-danger'}
hoverIcon={'file-pdf-o'}
hoverTextI18nKey={'editor.embeddings.clickToLoad'}
onActivate={() => setActivated(true)}>
<object type={'application/pdf'} data={url} className={'pdf-frame'}>
<ExternalLink text={url} href={url}/>
</object>
</OneClickEmbedding>
)
}

View file

@ -0,0 +1,19 @@
import { DomElement } from 'domhandler'
import React from 'react'
import { getAttributesFromCodiMdTag } from '../codi-md-tag-utils'
import { ComponentReplacer, SubNodeConverter } from '../ComponentReplacer'
import { PdfFrame } from './pdf-frame'
export class PdfReplacer implements ComponentReplacer {
private counterMap: Map<string, number> = new Map<string, number>()
getReplacement (node: DomElement, index: number, subNodeConverter: SubNodeConverter): React.ReactElement | undefined {
const attributes = getAttributesFromCodiMdTag(node, 'pdf')
if (attributes && attributes.url) {
const pdfUrl = attributes.url
const count = (this.counterMap.get(pdfUrl) || 0) + 1
this.counterMap.set(pdfUrl, count)
return <PdfFrame key={`pdf_${pdfUrl}_${count}`} url={pdfUrl}/>
}
}
}

View file

@ -0,0 +1,26 @@
import { DomElement } from 'domhandler'
import React from 'react'
import { ComponentReplacer, SubNodeConverter } from '../ComponentReplacer'
export class PossibleWiderReplacer implements ComponentReplacer {
getReplacement (node: DomElement, index: number, subNodeConverter: SubNodeConverter): React.ReactElement | undefined {
if (node.name !== 'p') {
return
}
if (!node.children || node.children.length !== 1) {
return
}
const childIsImage = node.children[0].name === 'img'
const childIsYoutube = node.children[0].name === 'codimd-youtube'
const childIsVimeo = node.children[0].name === 'codimd-vimeo'
const childIsAsciinema = node.children[0].name === 'codimd-asciinema'
const childIsPDF = node.children[0].name === 'codimd-pdf'
if (!(childIsImage || childIsYoutube || childIsVimeo || childIsAsciinema || childIsPDF)) {
return
}
// This appends the 'wider-possible' class to the node for a wider view in view-mode
node.attribs = Object.assign(node.attribs || {}, { class: `wider-possible ${node.attribs?.class || ''}` })
return subNodeConverter(node, index)
}
}

View file

@ -0,0 +1,42 @@
import { DomElement } from 'domhandler'
import { ComponentReplacer, SubNodeConverter } from '../ComponentReplacer'
const isColorExtraElement = (node: DomElement | undefined): boolean => {
if (!node || !node.attribs || !node.attribs.class || !node.attribs['data-color']) {
return false
}
return (node.name === 'span' && node.attribs.class === 'quote-extra')
}
const findQuoteOptionsParent = (nodes: DomElement[]): DomElement | undefined => {
return nodes.find((child) => {
if (child.name !== 'p' || !child.children || child.children.length < 1) {
return false
}
return child.children.find(isColorExtraElement) !== undefined
})
}
export class QuoteOptionsReplacer implements ComponentReplacer {
getReplacement (node: DomElement, index: number, subNodeConverter: SubNodeConverter): React.ReactElement | undefined {
if (node.name !== 'blockquote' || !node.children || node.children.length < 1) {
return
}
const paragraph = findQuoteOptionsParent(node.children)
if (!paragraph) {
return
}
const childElements = paragraph.children || []
const optionsTag = childElements.find(isColorExtraElement)
if (!optionsTag) {
return
}
paragraph.children = childElements.filter(elem => !isColorExtraElement(elem))
const attributes = optionsTag.attribs
if (!attributes || !attributes['data-color']) {
return
}
node.attribs = Object.assign(node.attribs || {}, { style: `border-left-color: ${attributes['data-color']};` })
return subNodeConverter(node, index)
}
}

View file

@ -0,0 +1,17 @@
import { DomElement } from 'domhandler'
import { ComponentReplacer, SubNodeConverter } from '../ComponentReplacer'
export class TocReplacer implements ComponentReplacer {
getReplacement (node: DomElement, index: number, subNodeConverter: SubNodeConverter): React.ReactElement | undefined {
if (node.name !== 'p' || node.children?.length !== 1) {
return
}
const possibleTocDiv = node.children[0]
if (possibleTocDiv.name === 'div' && possibleTocDiv.attribs && possibleTocDiv.attribs.class &&
possibleTocDiv.attribs.class === 'table-of-contents' && possibleTocDiv.children && possibleTocDiv.children.length === 1) {
const listElement = possibleTocDiv.children[0]
listElement.attribs = Object.assign(listElement.attribs || {}, { class: 'table-of-contents' })
return subNodeConverter(listElement, index)
}
}
}

View file

@ -0,0 +1,40 @@
import React, { useCallback } from 'react'
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
interface VimeoApiResponse {
// Vimeo uses strange names for their fields. ESLint doesn't like that.
// eslint-disable-next-line camelcase
thumbnail_large?: string
}
export interface VimeoFrameProps {
id: string
}
export const VimeoFrame: React.FC<VimeoFrameProps> = ({ 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>
)
}

View file

@ -0,0 +1,19 @@
import { DomElement } from 'domhandler'
import React from 'react'
import { getAttributesFromCodiMdTag } from '../codi-md-tag-utils'
import { ComponentReplacer } from '../ComponentReplacer'
import { VimeoFrame } from './vimeo-frame'
export class VimeoReplacer implements ComponentReplacer {
private counterMap: Map<string, number> = new Map<string, number>()
getReplacement (node: DomElement): React.ReactElement | undefined {
const attributes = getAttributesFromCodiMdTag(node, 'vimeo')
if (attributes && attributes.id) {
const videoId = attributes.id
const count = (this.counterMap.get(videoId) || 0) + 1
this.counterMap.set(videoId, count)
return <VimeoFrame key={`vimeo_${videoId}_${count}`} id={videoId}/>
}
}
}

View file

@ -0,0 +1,18 @@
import React from 'react'
import { OneClickEmbedding } from '../one-click-frame/one-click-embedding'
export interface YouTubeFrameProps {
id: string
}
export const YouTubeFrame: React.FC<YouTubeFrameProps> = ({ 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>
)
}

View file

@ -0,0 +1,19 @@
import { DomElement } from 'domhandler'
import React from 'react'
import { getAttributesFromCodiMdTag } from '../codi-md-tag-utils'
import { ComponentReplacer } from '../ComponentReplacer'
import { YouTubeFrame } from './youtube-frame'
export class YoutubeReplacer implements ComponentReplacer {
private counterMap: Map<string, number> = new Map<string, number>()
getReplacement (node: DomElement): React.ReactElement | undefined {
const attributes = getAttributesFromCodiMdTag(node, 'youtube')
if (attributes && attributes.id) {
const videoId = attributes.id
const count = (this.counterMap.get(videoId) || 0) + 1
this.counterMap.set(videoId, count)
return <YouTubeFrame key={`youtube_${videoId}_${count}`} id={videoId}/>
}
}
}