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:
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,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

Width:  |  Height:  |  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}/>
}
}
}