mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-20 02:05:21 -04:00
Restructure repository (#426)
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:
parent
66258ca615
commit
0fadc09f2b
254 changed files with 384 additions and 403 deletions
|
@ -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)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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}/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
.gist-frame {
|
||||
.one-click-embedding-preview {
|
||||
filter: blur(3px);
|
||||
}
|
||||
}
|
|
@ -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 Width: | Height: | Size: 30 KiB |
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
}
|
||||
|
||||
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>)
|
||||
}
|
|
@ -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}/>
|
||||
}
|
||||
}
|
|
@ -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}/>
|
||||
)
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export interface ImageProxyResponse {
|
||||
src: string
|
||||
}
|
|
@ -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}/>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.pdf-frame {
|
||||
width: 100%;
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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}/>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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}/>
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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}/>
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue