mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-20 02:05:21 -04:00
Add prettier for codestyle and re-format everything (#1294)
This commit is contained in:
parent
8b78154075
commit
0aae1f70d2
319 changed files with 4809 additions and 3936 deletions
|
@ -15,5 +15,9 @@ export type NativeRenderer = () => ReactElement
|
|||
export type MarkdownItPlugin = MarkdownIt.PluginSimple | MarkdownIt.PluginWithOptions | MarkdownIt.PluginWithParams
|
||||
|
||||
export abstract class ComponentReplacer {
|
||||
public abstract getReplacement(node: DomElement, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): (ReactElement | null | undefined);
|
||||
public abstract getReplacement(
|
||||
node: DomElement,
|
||||
subNodeTransform: SubNodeTransform,
|
||||
nativeRenderer: NativeRenderer
|
||||
): ReactElement | null | undefined
|
||||
}
|
||||
|
|
|
@ -28,5 +28,5 @@ export const AbcFrame: React.FC<AbcFrameProps> = ({ code }) => {
|
|||
})
|
||||
}, [code])
|
||||
|
||||
return <div ref={ container } className={ 'abcjs-score bg-white text-black text-center overflow-x-auto' }/>
|
||||
return <div ref={container} className={'abcjs-score bg-white text-black text-center overflow-x-auto'} />
|
||||
}
|
||||
|
|
|
@ -11,12 +11,19 @@ import { AbcFrame } from './abc-frame'
|
|||
|
||||
export class AbcReplacer implements ComponentReplacer {
|
||||
getReplacement(codeNode: DomElement): React.ReactElement | undefined {
|
||||
if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || codeNode.attribs['data-highlight-language'] !== 'abc' || !codeNode.children || !codeNode.children[0]) {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'abc' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const code = codeNode.children[0].data as string
|
||||
|
||||
return <AbcFrame code={ code }/>
|
||||
return <AbcFrame code={code} />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,12 +14,15 @@ export interface AsciinemaFrameProps {
|
|||
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` }/>
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -22,9 +22,7 @@ export class AsciinemaReplacer extends ComponentReplacer {
|
|||
const attributes = getAttributesFromHedgeDocTag(node, 'asciinema')
|
||||
if (attributes && attributes.id) {
|
||||
const asciinemaId = attributes.id
|
||||
return (
|
||||
<AsciinemaFrame id={ asciinemaId }/>
|
||||
)
|
||||
return <AsciinemaFrame id={asciinemaId} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,6 @@ export const replaceAsciinemaLink: RegexOptions = {
|
|||
replace: (match) => {
|
||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-asciinema id="${ match }"></app-asciinema>`
|
||||
return `<app-asciinema id="${match}"></app-asciinema>`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ 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')
|
||||
return node.name === 'span' && node.attribs.class === 'quote-extra'
|
||||
}
|
||||
|
||||
const findQuoteOptionsParent = (nodes: DomElement[]): DomElement | undefined => {
|
||||
|
@ -25,7 +25,11 @@ const findQuoteOptionsParent = (nodes: DomElement[]): DomElement | undefined =>
|
|||
}
|
||||
|
||||
export class ColoredBlockquoteReplacer extends ComponentReplacer {
|
||||
public getReplacement(node: DomElement, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): ReactElement | undefined {
|
||||
public getReplacement(
|
||||
node: DomElement,
|
||||
subNodeTransform: SubNodeTransform,
|
||||
nativeRenderer: NativeRenderer
|
||||
): ReactElement | undefined {
|
||||
if (node.name !== 'blockquote' || !node.children || node.children.length < 1) {
|
||||
return
|
||||
}
|
||||
|
@ -38,12 +42,12 @@ export class ColoredBlockquoteReplacer extends ComponentReplacer {
|
|||
if (!optionsTag) {
|
||||
return
|
||||
}
|
||||
paragraph.children = childElements.filter(elem => !isColorExtraElement(elem))
|
||||
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'] };` })
|
||||
node.attribs = Object.assign(node.attribs || {}, { style: `border-left-color: ${attributes['data-color']};` })
|
||||
return nativeRenderer()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,22 +9,26 @@ import { parseCsv } from './csv-parser'
|
|||
describe('test CSV parser', () => {
|
||||
it('normal table', () => {
|
||||
const input = 'A;B;C\nD;E;F\nG;H;I'
|
||||
const expected = [['A', 'B', 'C'], ['D', 'E', 'F'], ['G', 'H', 'I']]
|
||||
expect(parseCsv(input, ';'))
|
||||
.toEqual(expected)
|
||||
const expected = [
|
||||
['A', 'B', 'C'],
|
||||
['D', 'E', 'F'],
|
||||
['G', 'H', 'I']
|
||||
]
|
||||
expect(parseCsv(input, ';')).toEqual(expected)
|
||||
})
|
||||
|
||||
it('blank lines', () => {
|
||||
const input = 'A;B;C\n\nG;H;I'
|
||||
const expected = [['A', 'B', 'C'], ['G', 'H', 'I']]
|
||||
expect(parseCsv(input, ';'))
|
||||
.toEqual(expected)
|
||||
const expected = [
|
||||
['A', 'B', 'C'],
|
||||
['G', 'H', 'I']
|
||||
]
|
||||
expect(parseCsv(input, ';')).toEqual(expected)
|
||||
})
|
||||
|
||||
it('items with delimiter', () => {
|
||||
const input = 'A;B;C\n"D;E;F"\nG;H;I'
|
||||
const expected = [['A', 'B', 'C'], ['"D;E;F"'], ['G', 'H', 'I']]
|
||||
expect(parseCsv(input, ';'))
|
||||
.toEqual(expected)
|
||||
expect(parseCsv(input, ';')).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -10,6 +10,5 @@ export const parseCsv = (csvText: string, csvColumnDelimiter: string): string[][
|
|||
return []
|
||||
}
|
||||
const splitRegex = new RegExp(`${csvColumnDelimiter}(?=(?:[^"]*"[^"]*")*[^"]*$)`)
|
||||
return rows.filter(row => row !== '')
|
||||
.map(row => row.split(splitRegex))
|
||||
return rows.filter((row) => row !== '').map((row) => row.split(splitRegex))
|
||||
}
|
||||
|
|
|
@ -11,7 +11,14 @@ import { CsvTable } from './csv-table'
|
|||
|
||||
export class CsvReplacer extends ComponentReplacer {
|
||||
public getReplacement(codeNode: DomElement): React.ReactElement | undefined {
|
||||
if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || codeNode.attribs['data-highlight-language'] !== 'csv' || !codeNode.children || !codeNode.children[0]) {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'csv' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -29,6 +36,6 @@ export class CsvReplacer extends ComponentReplacer {
|
|||
showHeader = extraInfos[3] !== undefined
|
||||
}
|
||||
|
||||
return <CsvTable code={ code } delimiter={ delimiter } showHeader={ showHeader }/>
|
||||
return <CsvTable code={code} delimiter={delimiter} showHeader={showHeader} />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,13 @@ export interface CsvTableProps {
|
|||
tableColumnClassName?: string
|
||||
}
|
||||
|
||||
export const CsvTable: React.FC<CsvTableProps> = ({ code, delimiter, showHeader, tableRowClassName, tableColumnClassName }) => {
|
||||
export const CsvTable: React.FC<CsvTableProps> = ({
|
||||
code,
|
||||
delimiter,
|
||||
showHeader,
|
||||
tableRowClassName,
|
||||
tableColumnClassName
|
||||
}) => {
|
||||
const { rowsWithColumns, headerRow } = useMemo(() => {
|
||||
const rowsWithColumns = parseCsv(code.trim(), delimiter)
|
||||
let headerRow: string[] = []
|
||||
|
@ -29,17 +35,11 @@ export const CsvTable: React.FC<CsvTableProps> = ({ code, delimiter, showHeader,
|
|||
if (row !== []) {
|
||||
return (
|
||||
<thead>
|
||||
<tr>
|
||||
{
|
||||
row.map((column, columnNumber) => (
|
||||
<th
|
||||
key={ `header-${ columnNumber }` }
|
||||
>
|
||||
{ column }
|
||||
</th>
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
<tr>
|
||||
{row.map((column, columnNumber) => (
|
||||
<th key={`header-${columnNumber}`}>{column}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
)
|
||||
}
|
||||
|
@ -48,30 +48,23 @@ export const CsvTable: React.FC<CsvTableProps> = ({ code, delimiter, showHeader,
|
|||
const renderTableBody = (rows: string[][]) => {
|
||||
return (
|
||||
<tbody>
|
||||
{
|
||||
rows.map((row, rowNumber) => (
|
||||
<tr className={ tableRowClassName } key={ `row-${ rowNumber }` }>
|
||||
{
|
||||
row.map((column, columnIndex) => (
|
||||
<td
|
||||
className={ tableColumnClassName }
|
||||
key={ `cell-${ rowNumber }-${ columnIndex }` }
|
||||
>
|
||||
{ column.replace(/^"|"$/g, '') }
|
||||
</td>
|
||||
))
|
||||
}
|
||||
{rows.map((row, rowNumber) => (
|
||||
<tr className={tableRowClassName} key={`row-${rowNumber}`}>
|
||||
{row.map((column, columnIndex) => (
|
||||
<td className={tableColumnClassName} key={`cell-${rowNumber}-${columnIndex}`}>
|
||||
{column.replace(/^"|"$/g, '')}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
))}
|
||||
</tbody>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<table className={ 'csv-html-table table-striped' }>
|
||||
{ renderTableHeader(headerRow) }
|
||||
{ renderTableBody(rowsWithColumns) }
|
||||
<table className={'csv-html-table table-striped'}>
|
||||
{renderTableHeader(headerRow)}
|
||||
{renderTableBody(rowsWithColumns)}
|
||||
</table>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,12 +11,19 @@ import { FlowChart } from './flowchart/flowchart'
|
|||
|
||||
export class FlowchartReplacer extends ComponentReplacer {
|
||||
public getReplacement(codeNode: DomElement): React.ReactElement | undefined {
|
||||
if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || codeNode.attribs['data-highlight-language'] !== 'flow' || !codeNode.children || !codeNode.children[0]) {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'flow' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const code = codeNode.children[0].data as string
|
||||
|
||||
return <FlowChart code={ code }/>
|
||||
return <FlowChart code={code} />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,17 +46,17 @@ export const FlowChart: React.FC<FlowChartProps> = ({ code }) => {
|
|||
.catch(() => console.error('error while loading flowchart.js'))
|
||||
|
||||
return () => {
|
||||
Array.from(currentDiagramRef.children)
|
||||
.forEach(value => value.remove())
|
||||
Array.from(currentDiagramRef.children).forEach((value) => value.remove())
|
||||
}
|
||||
}, [code, darkModeActivated])
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert variant={ 'danger' }>
|
||||
<Trans i18nKey={ 'renderer.flowchart.invalidSyntax' }/>
|
||||
</Alert>)
|
||||
<Alert variant={'danger'}>
|
||||
<Trans i18nKey={'renderer.flowchart.invalidSyntax'} />
|
||||
</Alert>
|
||||
)
|
||||
} else {
|
||||
return <div ref={ diagramRef } data-cy={ 'flowchart' } className={ 'text-center' }/>
|
||||
return <div ref={diagramRef} data-cy={'flowchart'} className={'text-center'} />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ interface resizeEvent {
|
|||
|
||||
export const GistFrame: React.FC<GistFrameProps> = ({ id }) => {
|
||||
const iframeHtml = useMemo(() => {
|
||||
return (`
|
||||
return `
|
||||
<html lang="en">
|
||||
<head>
|
||||
<base target="_parent">
|
||||
|
@ -29,7 +29,7 @@ export const GistFrame: React.FC<GistFrameProps> = ({ id }) => {
|
|||
</style>
|
||||
<script type="text/javascript">
|
||||
function doLoad() {
|
||||
window.parent.postMessage({eventType: 'gistResize', size: document.body.scrollHeight, id: '${ id }'}, '*')
|
||||
window.parent.postMessage({eventType: 'gistResize', size: document.body.scrollHeight, id: '${id}'}, '*')
|
||||
tweakLinks();
|
||||
}
|
||||
function tweakLinks() {
|
||||
|
@ -41,20 +41,23 @@ export const GistFrame: React.FC<GistFrameProps> = ({ id }) => {
|
|||
</script>
|
||||
</head>
|
||||
<body onload="doLoad()">
|
||||
<script type="text/javascript" src="https://gist.github.com/${ id }.js"></script>
|
||||
<script type="text/javascript" src="https://gist.github.com/${id}.js"></script>
|
||||
</body>
|
||||
</html>`)
|
||||
</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])
|
||||
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)
|
||||
|
@ -65,12 +68,13 @@ export const GistFrame: React.FC<GistFrameProps> = ({ id }) => {
|
|||
|
||||
return (
|
||||
<iframe
|
||||
sandbox="allow-scripts allow-top-navigation-by-user-activation allow-popups"
|
||||
data-cy={ 'gh-gist' }
|
||||
sandbox='allow-scripts allow-top-navigation-by-user-activation allow-popups'
|
||||
data-cy={'gh-gist'}
|
||||
width='100%'
|
||||
height={ `${ frameHeight }px` }
|
||||
height={`${frameHeight}px`}
|
||||
frameBorder='0'
|
||||
title={ `gist ${ id }` }
|
||||
src={ `data:text/html;base64,${ btoa(iframeHtml) }` }/>
|
||||
title={`gist ${id}`}
|
||||
src={`data:text/html;base64,${btoa(iframeHtml)}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -28,11 +28,11 @@ export class GistReplacer extends ComponentReplacer {
|
|||
const gistId = attributes.id
|
||||
return (
|
||||
<OneClickEmbedding
|
||||
previewContainerClassName={ 'gist-frame' }
|
||||
loadingImageUrl={ preview }
|
||||
hoverIcon={ 'github' }
|
||||
tooltip={ 'click to load gist' }>
|
||||
<GistFrame id={ gistId }/>
|
||||
previewContainerClassName={'gist-frame'}
|
||||
loadingImageUrl={preview}
|
||||
hoverIcon={'github'}
|
||||
tooltip={'click to load gist'}>
|
||||
<GistFrame id={gistId} />
|
||||
</OneClickEmbedding>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -19,6 +19,6 @@ export const replaceGistLink: RegexOptions = {
|
|||
replace: (match) => {
|
||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-gist id="${ match }"></app-gist>`
|
||||
return `<app-gist id="${match}"></app-gist>`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,6 @@ export const replaceLegacyGistShortCode: RegexOptions = {
|
|||
replace: (match) => {
|
||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-gist id="${ match }"></app-gist>`
|
||||
return `<app-gist id="${match}"></app-gist>`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,8 +23,7 @@ export const GraphvizFrame: React.FC<GraphvizFrameProps> = ({ code }) => {
|
|||
}
|
||||
setError(error)
|
||||
console.error(error)
|
||||
container.current.querySelectorAll('svg')
|
||||
.forEach(child => child.remove())
|
||||
container.current.querySelectorAll('svg').forEach((child) => child.remove())
|
||||
}, [])
|
||||
|
||||
const frontendBaseUrl = useFrontendBaseUrl()
|
||||
|
@ -35,20 +34,21 @@ export const GraphvizFrame: React.FC<GraphvizFrameProps> = ({ code }) => {
|
|||
}
|
||||
const actualContainer = container.current
|
||||
|
||||
import(/* webpackChunkName: "d3-graphviz" */'@hpcc-js/wasm')
|
||||
import(/* webpackChunkName: "d3-graphviz" */ '@hpcc-js/wasm')
|
||||
.then((wasmPlugin) => {
|
||||
wasmPlugin.wasmFolder(`${ frontendBaseUrl }/static/js`)
|
||||
wasmPlugin.wasmFolder(`${frontendBaseUrl}/static/js`)
|
||||
})
|
||||
.then(() => import(/* webpackChunkName: "d3-graphviz" */ 'd3-graphviz'))
|
||||
.then((graphvizImport) => {
|
||||
try {
|
||||
setError(undefined)
|
||||
graphvizImport.graphviz(actualContainer, {
|
||||
useWorker: false,
|
||||
zoom: false
|
||||
})
|
||||
.onerror(showError)
|
||||
.renderDot(code)
|
||||
graphvizImport
|
||||
.graphviz(actualContainer, {
|
||||
useWorker: false,
|
||||
zoom: false
|
||||
})
|
||||
.onerror(showError)
|
||||
.renderDot(code)
|
||||
} catch (error) {
|
||||
showError(error)
|
||||
}
|
||||
|
@ -60,10 +60,10 @@ export const GraphvizFrame: React.FC<GraphvizFrameProps> = ({ code }) => {
|
|||
|
||||
return (
|
||||
<Fragment>
|
||||
<ShowIf condition={ !!error }>
|
||||
<Alert variant={ 'warning' }>{ error }</Alert>
|
||||
<ShowIf condition={!!error}>
|
||||
<Alert variant={'warning'}>{error}</Alert>
|
||||
</ShowIf>
|
||||
<div className={ 'text-center overflow-x-auto' } data-cy={ 'graphviz' } ref={ container }/>
|
||||
<div className={'text-center overflow-x-auto'} data-cy={'graphviz'} ref={container} />
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,12 +11,19 @@ import { GraphvizFrame } from './graphviz-frame'
|
|||
|
||||
export class GraphvizReplacer implements ComponentReplacer {
|
||||
getReplacement(codeNode: DomElement): React.ReactElement | undefined {
|
||||
if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || codeNode.attribs['data-highlight-language'] !== 'graphviz' || !codeNode.children || !codeNode.children[0]) {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'graphviz' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const code = codeNode.children[0].data as string
|
||||
|
||||
return <GraphvizFrame code={ code }/>
|
||||
return <GraphvizFrame code={code} />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,8 +11,8 @@ import '../../../utils/button-inside.scss'
|
|||
import './highlighted-code.scss'
|
||||
|
||||
export interface HighlightedCodeProps {
|
||||
code: string,
|
||||
language?: string,
|
||||
code: string
|
||||
language?: string
|
||||
startLineNumber?: number
|
||||
wrapLines: boolean
|
||||
}
|
||||
|
@ -30,47 +30,46 @@ const escapeHtml = (unsafe: string): string => {
|
|||
}
|
||||
|
||||
const replaceCode = (code: string): ReactElement[][] => {
|
||||
return code.split('\n')
|
||||
.filter(line => !!line)
|
||||
.map(line => ReactHtmlParser(line))
|
||||
return code
|
||||
.split('\n')
|
||||
.filter((line) => !!line)
|
||||
.map((line) => ReactHtmlParser(line))
|
||||
}
|
||||
|
||||
export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language, startLineNumber, wrapLines }) => {
|
||||
const [dom, setDom] = useState<ReactElement[]>()
|
||||
|
||||
useEffect(() => {
|
||||
import(/* webpackChunkName: "highlight.js" */ '../../../../common/hljs/hljs').then((hljs) => {
|
||||
const languageSupported = (lang: string) => hljs.default.listLanguages()
|
||||
.includes(lang)
|
||||
const unreplacedCode = !!language && languageSupported(language) ? hljs.default.highlight(code, { language }).value : escapeHtml(code)
|
||||
const replacedDom = replaceCode(unreplacedCode)
|
||||
.map((line, index) => (
|
||||
<Fragment key={ index }>
|
||||
<span className={ 'linenumber' }>
|
||||
{ (startLineNumber || 1) + index }
|
||||
</span>
|
||||
<div className={ 'codeline' }>
|
||||
{ line }
|
||||
</div>
|
||||
import(/* webpackChunkName: "highlight.js" */ '../../../../common/hljs/hljs')
|
||||
.then((hljs) => {
|
||||
const languageSupported = (lang: string) => hljs.default.listLanguages().includes(lang)
|
||||
const unreplacedCode =
|
||||
!!language && languageSupported(language)
|
||||
? hljs.default.highlight(code, { language }).value
|
||||
: escapeHtml(code)
|
||||
const replacedDom = replaceCode(unreplacedCode).map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
<span className={'linenumber'}>{(startLineNumber || 1) + index}</span>
|
||||
<div className={'codeline'}>{line}</div>
|
||||
</Fragment>
|
||||
))
|
||||
setDom(replacedDom)
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('error while loading highlight.js')
|
||||
})
|
||||
setDom(replacedDom)
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('error while loading highlight.js')
|
||||
})
|
||||
}, [code, language, startLineNumber])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<code
|
||||
className={ `hljs ${ startLineNumber !== undefined ? 'showGutter' : '' } ${ wrapLines ? 'wrapLines' : '' }` }>
|
||||
{ dom }
|
||||
<code className={`hljs ${startLineNumber !== undefined ? 'showGutter' : ''} ${wrapLines ? 'wrapLines' : ''}`}>
|
||||
{dom}
|
||||
</code>
|
||||
<div className={ 'text-right button-inside' }>
|
||||
<CopyToClipboardButton content={ code } data-cy="copy-code-button"/>
|
||||
<div className={'text-right button-inside'}>
|
||||
<CopyToClipboardButton content={code} data-cy='copy-code-button' />
|
||||
</div>
|
||||
</Fragment>)
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default HighlightedCode
|
||||
|
|
|
@ -13,7 +13,13 @@ export class HighlightedCodeReplacer extends ComponentReplacer {
|
|||
private lastLineNumber = 0
|
||||
|
||||
public getReplacement(codeNode: DomElement): React.ReactElement | undefined {
|
||||
if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || !codeNode.children || !codeNode.children[0]) {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -31,18 +37,21 @@ export class HighlightedCodeReplacer extends ComponentReplacer {
|
|||
wrapLines = extraInfos[3] === '!'
|
||||
}
|
||||
|
||||
const startLineNumber = startLineNumberAttribute === '+' ? this.lastLineNumber : (parseInt(startLineNumberAttribute) || 1)
|
||||
const startLineNumber =
|
||||
startLineNumberAttribute === '+' ? this.lastLineNumber : parseInt(startLineNumberAttribute) || 1
|
||||
const code = codeNode.children[0].data as string
|
||||
|
||||
if (showLineNumbers) {
|
||||
this.lastLineNumber = startLineNumber + code.split('\n')
|
||||
.filter(line => !!line).length
|
||||
this.lastLineNumber = startLineNumber + code.split('\n').filter((line) => !!line).length
|
||||
}
|
||||
|
||||
return <HighlightedCode
|
||||
language={ language }
|
||||
startLineNumber={ showLineNumbers ? startLineNumber : undefined }
|
||||
wrapLines={ wrapLines }
|
||||
code={ code }/>
|
||||
return (
|
||||
<HighlightedCode
|
||||
language={language}
|
||||
startLineNumber={showLineNumbers ? startLineNumber : undefined}
|
||||
wrapLines={wrapLines}
|
||||
code={code}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,21 +21,20 @@ export interface ImageLightboxModalProps {
|
|||
export const ImageLightboxModal: React.FC<ImageLightboxModalProps> = ({ show, onHide, src, alt, title }) => {
|
||||
return (
|
||||
<Modal
|
||||
animation={ true }
|
||||
centered={ true }
|
||||
dialogClassName={ 'text-dark lightbox' }
|
||||
show={ show && !!src }
|
||||
onHide={ onHide }
|
||||
size={ 'xl' }>
|
||||
<Modal.Header closeButton={ true }>
|
||||
<Modal.Title className={ 'h6' }>
|
||||
<ForkAwesomeIcon icon={ 'picture-o' }/>
|
||||
animation={true}
|
||||
centered={true}
|
||||
dialogClassName={'text-dark lightbox'}
|
||||
show={show && !!src}
|
||||
onHide={onHide}
|
||||
size={'xl'}>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title className={'h6'}>
|
||||
<ForkAwesomeIcon icon={'picture-o'} />
|
||||
|
||||
<span>{ alt ?? title ?? '' }</span>
|
||||
<span>{alt ?? title ?? ''}</span>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<ProxyImageFrame alt={ alt } src={ src } title={ title } className={ 'w-100 cursor-zoom-out' }
|
||||
onClick={ onHide }/>
|
||||
<ProxyImageFrame alt={alt} src={src} title={title} className={'w-100 cursor-zoom-out'} onClick={onHide} />
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import React from 'react'
|
|||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import { ProxyImageFrame } from './proxy-image-frame'
|
||||
|
||||
export type ImageClickHandler = (event: React.MouseEvent<HTMLImageElement, MouseEvent>) => void;
|
||||
export type ImageClickHandler = (event: React.MouseEvent<HTMLImageElement, MouseEvent>) => void
|
||||
|
||||
export class ImageReplacer extends ComponentReplacer {
|
||||
private readonly clickHandler?: ImageClickHandler
|
||||
|
@ -21,16 +21,18 @@ export class ImageReplacer extends ComponentReplacer {
|
|||
|
||||
public getReplacement(node: DomElement): React.ReactElement | undefined {
|
||||
if (node.name === 'img' && node.attribs) {
|
||||
return <ProxyImageFrame
|
||||
id={ node.attribs.id }
|
||||
className={ `${ node.attribs.class } cursor-zoom-in` }
|
||||
src={ node.attribs.src }
|
||||
alt={ node.attribs.alt }
|
||||
title={ node.attribs.title }
|
||||
width={ node.attribs.width }
|
||||
height={ node.attribs.height }
|
||||
onClick={ this.clickHandler }
|
||||
/>
|
||||
return (
|
||||
<ProxyImageFrame
|
||||
id={node.attribs.id}
|
||||
className={`${node.attribs.class} cursor-zoom-in`}
|
||||
src={node.attribs.src}
|
||||
alt={node.attribs.alt}
|
||||
title={node.attribs.title}
|
||||
width={node.attribs.width}
|
||||
height={node.attribs.height}
|
||||
onClick={this.clickHandler}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,13 +9,7 @@ import { useSelector } from 'react-redux'
|
|||
import { getProxiedUrl } from '../../../../api/media'
|
||||
import { ApplicationState } from '../../../../redux'
|
||||
|
||||
export const ProxyImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = (
|
||||
{
|
||||
src,
|
||||
title,
|
||||
alt,
|
||||
...props
|
||||
}) => {
|
||||
export const ProxyImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>> = ({ src, title, alt, ...props }) => {
|
||||
const [imageUrl, setImageUrl] = useState('')
|
||||
const imageProxyEnabled = useSelector((state: ApplicationState) => state.config.useImageProxy)
|
||||
|
||||
|
@ -24,10 +18,9 @@ export const ProxyImageFrame: React.FC<React.ImgHTMLAttributes<HTMLImageElement>
|
|||
return
|
||||
}
|
||||
getProxiedUrl(src)
|
||||
.then(proxyResponse => setImageUrl(proxyResponse.src))
|
||||
.catch(err => console.error(err))
|
||||
.then((proxyResponse) => setImageUrl(proxyResponse.src))
|
||||
.catch((err) => console.error(err))
|
||||
}, [imageProxyEnabled, src])
|
||||
|
||||
return <img src={ imageProxyEnabled ? imageUrl : (src ?? '') } title={ title ?? alt ?? '' } alt={ alt } { ...props }/>
|
||||
return <img src={imageProxyEnabled ? imageUrl : src ?? ''} title={title ?? alt ?? ''} alt={alt} {...props} />
|
||||
}
|
||||
|
||||
|
|
|
@ -11,17 +11,17 @@ import React from 'react'
|
|||
import { ComponentReplacer } from '../ComponentReplacer'
|
||||
import './katex.scss'
|
||||
|
||||
const getNodeIfKatexBlock = (node: DomElement): (DomElement | undefined) => {
|
||||
const getNodeIfKatexBlock = (node: DomElement): DomElement | undefined => {
|
||||
if (node.name !== 'p' || !node.children || node.children.length === 0) {
|
||||
return
|
||||
}
|
||||
return node.children.find((subnode) => {
|
||||
return (subnode.name === 'app-katex' && subnode.attribs?.inline === undefined)
|
||||
return subnode.name === 'app-katex' && subnode.attribs?.inline === undefined
|
||||
})
|
||||
}
|
||||
|
||||
const getNodeIfInlineKatex = (node: DomElement): (DomElement | undefined) => {
|
||||
return (node.name === 'app-katex' && node.attribs?.inline !== undefined) ? node : undefined
|
||||
const getNodeIfInlineKatex = (node: DomElement): DomElement | undefined => {
|
||||
return node.name === 'app-katex' && node.attribs?.inline !== undefined ? node : undefined
|
||||
}
|
||||
|
||||
const KaTeX = React.lazy(() => import(/* webpackChunkName: "katex" */ '@matejmazur/react-katex'))
|
||||
|
@ -40,8 +40,8 @@ export class KatexReplacer extends ComponentReplacer {
|
|||
const katex = getNodeIfKatexBlock(node) || getNodeIfInlineKatex(node)
|
||||
if (katex?.children && katex.children[0]) {
|
||||
const mathJaxContent = katex.children[0]?.data as string
|
||||
const isInline = (katex.attribs?.inline) !== undefined
|
||||
return <KaTeX block={ !isInline } math={ mathJaxContent } errorColor={ '#cc0000' }/>
|
||||
const isInline = katex.attribs?.inline !== undefined
|
||||
return <KaTeX block={!isInline} math={mathJaxContent} errorColor={'#cc0000'} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,67 +12,74 @@ export interface LineMarkers {
|
|||
endLine: number
|
||||
}
|
||||
|
||||
export type LineNumberMarkerOptions = (lineMarkers: LineMarkers[]) => void;
|
||||
export type LineNumberMarkerOptions = (lineMarkers: LineMarkers[]) => void
|
||||
|
||||
/**
|
||||
* This plugin adds markers to the dom, that are used to map line numbers to dom elements.
|
||||
* It also provides a list of line numbers for the top level dom elements.
|
||||
*/
|
||||
export const lineNumberMarker: (options: LineNumberMarkerOptions) => MarkdownIt.PluginSimple = (options) => (md: MarkdownIt) => {
|
||||
// add app_linemarker token before each opening or self-closing level-0 tag
|
||||
md.core.ruler.push('line_number_marker', (state) => {
|
||||
const lineMarkers: LineMarkers[] = []
|
||||
tagTokens(state.tokens, lineMarkers)
|
||||
if (options) {
|
||||
options(lineMarkers)
|
||||
export const lineNumberMarker: (options: LineNumberMarkerOptions) => MarkdownIt.PluginSimple =
|
||||
(options) => (md: MarkdownIt) => {
|
||||
// add app_linemarker token before each opening or self-closing level-0 tag
|
||||
md.core.ruler.push('line_number_marker', (state) => {
|
||||
const lineMarkers: LineMarkers[] = []
|
||||
tagTokens(state.tokens, lineMarkers)
|
||||
if (options) {
|
||||
options(lineMarkers)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
md.renderer.rules.app_linemarker = (tokens: Token[], index: number): string => {
|
||||
const startLineNumber = tokens[index].attrGet('data-start-line')
|
||||
const endLineNumber = tokens[index].attrGet('data-end-line')
|
||||
|
||||
if (!startLineNumber || !endLineNumber) {
|
||||
// don't render broken linemarkers without a linenumber
|
||||
return ''
|
||||
}
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-linemarker data-start-line='${startLineNumber}' data-end-line='${endLineNumber}'></app-linemarker>`
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
md.renderer.rules.app_linemarker = (tokens: Token[], index: number): string => {
|
||||
const startLineNumber = tokens[index].attrGet('data-start-line')
|
||||
const endLineNumber = tokens[index].attrGet('data-end-line')
|
||||
|
||||
if (!startLineNumber || !endLineNumber) {
|
||||
// don't render broken linemarkers without a linenumber
|
||||
return ''
|
||||
const insertNewLineMarker = (
|
||||
startLineNumber: number,
|
||||
endLineNumber: number,
|
||||
tokenPosition: number,
|
||||
level: number,
|
||||
tokens: Token[]
|
||||
) => {
|
||||
const startToken = new Token('app_linemarker', 'app-linemarker', 0)
|
||||
startToken.level = level
|
||||
startToken.attrPush(['data-start-line', `${startLineNumber}`])
|
||||
startToken.attrPush(['data-end-line', `${endLineNumber}`])
|
||||
tokens.splice(tokenPosition, 0, startToken)
|
||||
}
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-linemarker data-start-line='${ startLineNumber }' data-end-line='${ endLineNumber }'></app-linemarker>`
|
||||
}
|
||||
|
||||
const insertNewLineMarker = (startLineNumber: number, endLineNumber: number, tokenPosition: number, level: number, tokens: Token[]) => {
|
||||
const startToken = new Token('app_linemarker', 'app-linemarker', 0)
|
||||
startToken.level = level
|
||||
startToken.attrPush(['data-start-line', `${ startLineNumber }`])
|
||||
startToken.attrPush(['data-end-line', `${ endLineNumber }`])
|
||||
tokens.splice(tokenPosition, 0, startToken)
|
||||
}
|
||||
const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[]) => {
|
||||
for (let tokenPosition = 0; tokenPosition < tokens.length; tokenPosition++) {
|
||||
const token = tokens[tokenPosition]
|
||||
if (token.hidden) {
|
||||
continue
|
||||
}
|
||||
|
||||
const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[]) => {
|
||||
for (let tokenPosition = 0; tokenPosition < tokens.length; tokenPosition++) {
|
||||
const token = tokens[tokenPosition]
|
||||
if (token.hidden) {
|
||||
continue
|
||||
}
|
||||
if (!token.map) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (!token.map) {
|
||||
continue
|
||||
}
|
||||
const startLineNumber = token.map[0] + 1
|
||||
const endLineNumber = token.map[1] + 1
|
||||
|
||||
const startLineNumber = token.map[0] + 1
|
||||
const endLineNumber = token.map[1] + 1
|
||||
if (token.level === 0) {
|
||||
lineMarkers.push({ startLine: startLineNumber, endLine: endLineNumber })
|
||||
}
|
||||
|
||||
if (token.level === 0) {
|
||||
lineMarkers.push({ startLine: startLineNumber, endLine: endLineNumber })
|
||||
}
|
||||
insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens)
|
||||
tokenPosition += 1
|
||||
|
||||
insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens)
|
||||
tokenPosition += 1
|
||||
|
||||
if (token.children) {
|
||||
tagTokens(token.children, lineMarkers)
|
||||
if (token.children) {
|
||||
tagTokens(token.children, lineMarkers)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,8 +9,7 @@ import { ComponentReplacer, NativeRenderer, SubNodeTransform } from '../Componen
|
|||
|
||||
export const createJumpToMarkClickEventHandler = (id: string) => {
|
||||
return (event: React.MouseEvent<HTMLElement, MouseEvent>): void => {
|
||||
document.getElementById(id)
|
||||
?.scrollIntoView()
|
||||
document.getElementById(id)?.scrollIntoView()
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +19,11 @@ export class LinkReplacer extends ComponentReplacer {
|
|||
super()
|
||||
}
|
||||
|
||||
public getReplacement(node: DomElement, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): (ReactElement | null | undefined) {
|
||||
public getReplacement(
|
||||
node: DomElement,
|
||||
subNodeTransform: SubNodeTransform,
|
||||
nativeRenderer: NativeRenderer
|
||||
): ReactElement | null | undefined {
|
||||
if (node.name !== 'a' || !node.attribs || !node.attribs.href) {
|
||||
return undefined
|
||||
}
|
||||
|
@ -29,7 +32,7 @@ export class LinkReplacer extends ComponentReplacer {
|
|||
|
||||
// eslint-disable-next-line no-script-url
|
||||
if (url.startsWith('data:') || url.startsWith('javascript:')) {
|
||||
return <span>{ node.attribs.href }</span>
|
||||
return <span>{node.attribs.href}</span>
|
||||
}
|
||||
|
||||
const isJumpMark = url.substr(0, 1) === '#'
|
||||
|
@ -43,9 +46,7 @@ export class LinkReplacer extends ComponentReplacer {
|
|||
}
|
||||
|
||||
if (isJumpMark) {
|
||||
return <span onClick={ createJumpToMarkClickEventHandler(id) }>
|
||||
{ nativeRenderer() }
|
||||
</span>
|
||||
return <span onClick={createJumpToMarkClickEventHandler(id)}>{nativeRenderer()}</span>
|
||||
} else {
|
||||
node.attribs.rel = 'noreferer noopener'
|
||||
node.attribs.target = '_blank'
|
||||
|
|
|
@ -45,13 +45,12 @@ export const MarkmapFrame: React.FC<MarkmapFrameProps> = ({ code }) => {
|
|||
return
|
||||
}
|
||||
const actualContainer = diagramContainer.current
|
||||
import(/* webpackChunkName: "markmap" */'./markmap-loader')
|
||||
import(/* webpackChunkName: "markmap" */ './markmap-loader')
|
||||
.then(({ markmapLoader }) => {
|
||||
try {
|
||||
const svg: SVGSVGElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
svg.setAttribute('width', '100%')
|
||||
actualContainer.querySelectorAll('svg')
|
||||
.forEach(child => child.remove())
|
||||
actualContainer.querySelectorAll('svg').forEach((child) => child.remove())
|
||||
actualContainer.appendChild(svg)
|
||||
markmapLoader(svg, code)
|
||||
} catch (error) {
|
||||
|
@ -64,11 +63,14 @@ export const MarkmapFrame: React.FC<MarkmapFrameProps> = ({ code }) => {
|
|||
}, [code])
|
||||
|
||||
return (
|
||||
<div data-cy={ 'markmap' }>
|
||||
<div className={ 'text-center' } ref={ diagramContainer }/>
|
||||
<div className={ 'text-right button-inside' }>
|
||||
<LockButton locked={ disablePanAndZoom } onLockedChanged={ (newState => setDisablePanAndZoom(newState)) }
|
||||
title={ disablePanAndZoom ? t('renderer.markmap.locked') : t('renderer.markmap.unlocked') }/>
|
||||
<div data-cy={'markmap'}>
|
||||
<div className={'text-center'} ref={diagramContainer} />
|
||||
<div className={'text-right button-inside'}>
|
||||
<LockButton
|
||||
locked={disablePanAndZoom}
|
||||
onLockedChanged={(newState) => setDisablePanAndZoom(newState)}
|
||||
title={disablePanAndZoom ? t('renderer.markmap.locked') : t('renderer.markmap.unlocked')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -11,12 +11,19 @@ import { MarkmapFrame } from './markmap-frame'
|
|||
|
||||
export class MarkmapReplacer implements ComponentReplacer {
|
||||
getReplacement(codeNode: DomElement): React.ReactElement | undefined {
|
||||
if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || codeNode.attribs['data-highlight-language'] !== 'markmap' || !codeNode.children || !codeNode.children[0]) {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'markmap' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const code = codeNode.children[0].data as string
|
||||
|
||||
return <MarkmapFrame code={ code }/>
|
||||
return <MarkmapFrame code={code} />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ export const MermaidChart: React.FC<MermaidChartProps> = ({ code }) => {
|
|||
|
||||
useEffect(() => {
|
||||
if (!mermaidInitialized) {
|
||||
import(/* webpackChunkName: "mermaid" */'mermaid')
|
||||
import(/* webpackChunkName: "mermaid" */ 'mermaid')
|
||||
.then((mermaid) => {
|
||||
mermaid.default.initialize({ startOnLoad: false })
|
||||
mermaidInitialized = true
|
||||
|
@ -38,21 +38,23 @@ export const MermaidChart: React.FC<MermaidChartProps> = ({ code }) => {
|
|||
}
|
||||
}, [])
|
||||
|
||||
const showError = useCallback((error: string) => {
|
||||
setError(error)
|
||||
console.error(error)
|
||||
if (!diagramContainer.current) {
|
||||
return
|
||||
}
|
||||
diagramContainer.current.querySelectorAll('svg')
|
||||
.forEach(child => child.remove())
|
||||
}, [setError])
|
||||
const showError = useCallback(
|
||||
(error: string) => {
|
||||
setError(error)
|
||||
console.error(error)
|
||||
if (!diagramContainer.current) {
|
||||
return
|
||||
}
|
||||
diagramContainer.current.querySelectorAll('svg').forEach((child) => child.remove())
|
||||
},
|
||||
[setError]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!diagramContainer.current) {
|
||||
return
|
||||
}
|
||||
import(/* webpackChunkName: "mermaid" */'mermaid')
|
||||
import(/* webpackChunkName: "mermaid" */ 'mermaid')
|
||||
.then((mermaid) => {
|
||||
try {
|
||||
if (!diagramContainer.current) {
|
||||
|
@ -71,10 +73,12 @@ export const MermaidChart: React.FC<MermaidChartProps> = ({ code }) => {
|
|||
.catch(() => showError('Error while loading mermaid'))
|
||||
}, [code, showError, t])
|
||||
|
||||
return <Fragment>
|
||||
<ShowIf condition={ !!error }>
|
||||
<Alert variant={ 'warning' }>{ error }</Alert>
|
||||
</ShowIf>
|
||||
<div className={ 'text-center mermaid text-black' } ref={ diagramContainer }/>
|
||||
</Fragment>
|
||||
return (
|
||||
<Fragment>
|
||||
<ShowIf condition={!!error}>
|
||||
<Alert variant={'warning'}>{error}</Alert>
|
||||
</ShowIf>
|
||||
<div className={'text-center mermaid text-black'} ref={diagramContainer} />
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,12 +11,19 @@ import { MermaidChart } from './mermaid-chart'
|
|||
|
||||
export class MermaidReplacer implements ComponentReplacer {
|
||||
getReplacement(codeNode: DomElement): React.ReactElement | undefined {
|
||||
if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || codeNode.attribs['data-highlight-language'] !== 'mermaid' || !codeNode.children || !codeNode.children[0]) {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'mermaid' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const code = codeNode.children[0].data as string
|
||||
|
||||
return <MermaidChart code={ code }/>
|
||||
return <MermaidChart code={code} />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,17 @@ interface OneClickFrameProps {
|
|||
onActivate?: () => void
|
||||
}
|
||||
|
||||
export const OneClickEmbedding: React.FC<OneClickFrameProps> = ({ previewContainerClassName, containerClassName, onImageFetch, loadingImageUrl, children, tooltip, hoverIcon, hoverTextI18nKey, onActivate }) => {
|
||||
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)
|
||||
|
||||
|
@ -47,22 +57,24 @@ export const OneClickEmbedding: React.FC<OneClickFrameProps> = ({ previewContain
|
|||
}, [onImageFetch])
|
||||
|
||||
return (
|
||||
<span className={ containerClassName }>
|
||||
<ShowIf condition={ showFrame }>
|
||||
{ children }
|
||||
</ShowIf>
|
||||
<ShowIf condition={ !showFrame }>
|
||||
<span className={ `one-click-embedding ${ previewContainerClassName || '' }` } onClick={ showChildren }>
|
||||
<ShowIf condition={ !!previewImageUrl }>
|
||||
<ProxyImageFrame className={ 'one-click-embedding-preview' } src={ previewImageUrl } alt={ tooltip || '' }
|
||||
title={ tooltip || '' }/>
|
||||
<span className={containerClassName}>
|
||||
<ShowIf condition={showFrame}>{children}</ShowIf>
|
||||
<ShowIf condition={!showFrame}>
|
||||
<span className={`one-click-embedding ${previewContainerClassName || ''}`} onClick={showChildren}>
|
||||
<ShowIf condition={!!previewImageUrl}>
|
||||
<ProxyImageFrame
|
||||
className={'one-click-embedding-preview'}
|
||||
src={previewImageUrl}
|
||||
alt={tooltip || ''}
|
||||
title={tooltip || ''}
|
||||
/>
|
||||
</ShowIf>
|
||||
<ShowIf condition={ !!hoverIcon }>
|
||||
<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 }/>
|
||||
<i className={`fa fa-${hoverIcon as string} fa-5x mb-2`} />
|
||||
<ShowIf condition={!!hoverTextI18nKey}>
|
||||
<br />
|
||||
<Trans i18nKey={hoverTextI18nKey} />
|
||||
</ShowIf>
|
||||
</span>
|
||||
</ShowIf>
|
||||
|
|
|
@ -14,12 +14,12 @@ export const DeprecationWarning: React.FC = () => {
|
|||
useTranslation()
|
||||
|
||||
return (
|
||||
<Alert data-cy={ 'yaml' } className={ 'mt-2' } variant={ 'warning' }>
|
||||
<span className={ 'text-wrap' }>
|
||||
<Trans i18nKey={ 'renderer.sequence.deprecationWarning' }/>
|
||||
<Alert data-cy={'yaml'} className={'mt-2'} variant={'warning'}>
|
||||
<span className={'text-wrap'}>
|
||||
<Trans i18nKey={'renderer.sequence.deprecationWarning'} />
|
||||
</span>
|
||||
<br/>
|
||||
<TranslatedExternalLink i18nKey={ 'common.readForMoreInfo' } className={ 'text-primary' } href={ links.faq }/>
|
||||
<br />
|
||||
<TranslatedExternalLink i18nKey={'common.readForMoreInfo'} className={'text-primary'} href={links.faq} />
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -12,15 +12,24 @@ import { DeprecationWarning } from './deprecation-warning'
|
|||
|
||||
export class SequenceDiagramReplacer implements ComponentReplacer {
|
||||
getReplacement(codeNode: DomElement): React.ReactElement | undefined {
|
||||
if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || codeNode.attribs['data-highlight-language'] !== 'sequence' || !codeNode.children || !codeNode.children[0]) {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'sequence' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const code = codeNode.children[0].data as string
|
||||
|
||||
return <Fragment>
|
||||
<DeprecationWarning/>
|
||||
<MermaidChart code={ 'sequenceDiagram\n' + code }/>
|
||||
</Fragment>
|
||||
return (
|
||||
<Fragment>
|
||||
<DeprecationWarning />
|
||||
<MermaidChart code={'sequenceDiagram\n' + code} />
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,19 +25,19 @@ export class TaskListReplacer extends ComponentReplacer {
|
|||
}
|
||||
}
|
||||
|
||||
public getReplacement(node: DomElement): (ReactElement | undefined) {
|
||||
public getReplacement(node: DomElement): ReactElement | undefined {
|
||||
if (node.attribs?.class !== 'task-list-item-checkbox') {
|
||||
return
|
||||
}
|
||||
return (
|
||||
<input
|
||||
disabled={ this.onTaskCheckedChange === undefined }
|
||||
className="task-list-item-checkbox"
|
||||
type="checkbox"
|
||||
checked={ node.attribs.checked !== undefined }
|
||||
onChange={ this.handleCheckboxChange }
|
||||
id={ node.attribs.id }
|
||||
data-line={ node.attribs['data-line'] }
|
||||
disabled={this.onTaskCheckedChange === undefined}
|
||||
className='task-list-item-checkbox'
|
||||
type='checkbox'
|
||||
checked={node.attribs.checked !== undefined}
|
||||
onChange={this.handleCheckboxChange}
|
||||
id={node.attribs.id}
|
||||
data-line={node.attribs['data-line']}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,8 +6,11 @@
|
|||
|
||||
import { DomElement } from 'domhandler'
|
||||
|
||||
export const getAttributesFromHedgeDocTag = (node: DomElement, tagName: string): ({ [s: string]: string; } | undefined) => {
|
||||
if (node.name !== `app-${ tagName }` || !node.attribs) {
|
||||
export const getAttributesFromHedgeDocTag = (
|
||||
node: DomElement,
|
||||
tagName: string
|
||||
): { [s: string]: string } | undefined => {
|
||||
if (node.name !== `app-${tagName}` || !node.attribs) {
|
||||
return
|
||||
}
|
||||
return node.attribs
|
||||
|
|
|
@ -39,20 +39,21 @@ export const VegaChart: React.FC<VegaChartProps> = ({ code }) => {
|
|||
}
|
||||
|
||||
const spec = JSON.parse(code) as VisualizationSpec
|
||||
embed.default(diagramContainer.current, spec, {
|
||||
actions: {
|
||||
export: true,
|
||||
source: false,
|
||||
compiled: false,
|
||||
editor: false
|
||||
},
|
||||
i18n: {
|
||||
PNG_ACTION: t('renderer.vega-lite.png'),
|
||||
SVG_ACTION: t('renderer.vega-lite.svg')
|
||||
}
|
||||
})
|
||||
.then(() => setError(undefined))
|
||||
.catch((error: Error) => showError(error.message))
|
||||
embed
|
||||
.default(diagramContainer.current, spec, {
|
||||
actions: {
|
||||
export: true,
|
||||
source: false,
|
||||
compiled: false,
|
||||
editor: false
|
||||
},
|
||||
i18n: {
|
||||
PNG_ACTION: t('renderer.vega-lite.png'),
|
||||
SVG_ACTION: t('renderer.vega-lite.svg')
|
||||
}
|
||||
})
|
||||
.then(() => setError(undefined))
|
||||
.catch((error: Error) => showError(error.message))
|
||||
} catch (error) {
|
||||
showError(t('renderer.vega-lite.errorJson'))
|
||||
}
|
||||
|
@ -62,12 +63,14 @@ export const VegaChart: React.FC<VegaChartProps> = ({ code }) => {
|
|||
})
|
||||
}, [code, showError, t])
|
||||
|
||||
return <Fragment>
|
||||
<ShowIf condition={ !!error }>
|
||||
<Alert variant={ 'danger' }>{ error }</Alert>
|
||||
</ShowIf>
|
||||
<div className={ 'text-center' }>
|
||||
<div ref={ diagramContainer }/>
|
||||
</div>
|
||||
</Fragment>
|
||||
return (
|
||||
<Fragment>
|
||||
<ShowIf condition={!!error}>
|
||||
<Alert variant={'danger'}>{error}</Alert>
|
||||
</ShowIf>
|
||||
<div className={'text-center'}>
|
||||
<div ref={diagramContainer} />
|
||||
</div>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,12 +11,19 @@ import { VegaChart } from './vega-chart'
|
|||
|
||||
export class VegaReplacer implements ComponentReplacer {
|
||||
getReplacement(codeNode: DomElement): React.ReactElement | undefined {
|
||||
if (codeNode.name !== 'code' || !codeNode.attribs || !codeNode.attribs['data-highlight-language'] || codeNode.attribs['data-highlight-language'] !== 'vega-lite' || !codeNode.children || !codeNode.children[0]) {
|
||||
if (
|
||||
codeNode.name !== 'code' ||
|
||||
!codeNode.attribs ||
|
||||
!codeNode.attribs['data-highlight-language'] ||
|
||||
codeNode.attribs['data-highlight-language'] !== 'vega-lite' ||
|
||||
!codeNode.children ||
|
||||
!codeNode.children[0]
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const code = codeNode.children[0].data as string
|
||||
|
||||
return <VegaChart code={ code }/>
|
||||
return <VegaChart code={code} />
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,6 @@ export const replaceLegacyVimeoShortCode: RegexOptions = {
|
|||
replace: (match) => {
|
||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-vimeo id="${ match }"></app-vimeo>`
|
||||
return `<app-vimeo id="${match}"></app-vimeo>`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,9 @@ 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 vimeoVideoUrlRegex = new RegExp(
|
||||
`(?:${protocolRegex.source}${domainRegex.source}${idRegex.source}${tailRegex.source})`
|
||||
)
|
||||
const linkRegex = new RegExp(`^${vimeoVideoUrlRegex.source}$`, 'i')
|
||||
|
||||
export const replaceVimeoLink: RegexOptions = {
|
||||
|
@ -19,6 +21,6 @@ export const replaceVimeoLink: RegexOptions = {
|
|||
replace: (match) => {
|
||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-vimeo id="${ match }"></app-vimeo>`
|
||||
return `<app-vimeo id="${match}"></app-vimeo>`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,14 +19,14 @@ export interface VimeoFrameProps {
|
|||
|
||||
export const VimeoFrame: React.FC<VimeoFrameProps> = ({ id }) => {
|
||||
const getPreviewImageLink = useCallback(async () => {
|
||||
const response = await fetch(`https://vimeo.com/api/v2/video/${ id }.json`, {
|
||||
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[]
|
||||
const vimeoResponse: VimeoApiResponse[] = (await response.json()) as VimeoApiResponse[]
|
||||
|
||||
if (vimeoResponse[0] && vimeoResponse[0].thumbnail_large) {
|
||||
return vimeoResponse[0].thumbnail_large
|
||||
|
@ -36,13 +36,18 @@ export const VimeoFrame: React.FC<VimeoFrameProps> = ({ id }) => {
|
|||
}, [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
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ export class VimeoReplacer extends ComponentReplacer {
|
|||
const attributes = getAttributesFromHedgeDocTag(node, 'vimeo')
|
||||
if (attributes && attributes.id) {
|
||||
const videoId = attributes.id
|
||||
return <VimeoFrame id={ videoId }/>
|
||||
return <VimeoFrame id={videoId} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,6 @@ export const replaceLegacyYoutubeShortCode: RegexOptions = {
|
|||
replace: (match) => {
|
||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-youtube id="${ match }"></app-youtube>`
|
||||
return `<app-youtube id="${match}"></app-youtube>`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,9 @@ 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 youtubeVideoUrlRegex = new RegExp(
|
||||
`(?:${protocolRegex.source}${subdomainRegex.source}${pathRegex.source}${idRegex.source}${tailRegex.source})`
|
||||
)
|
||||
const linkRegex = new RegExp(`^${youtubeVideoUrlRegex.source}$`, 'i')
|
||||
|
||||
export const replaceYouTubeLink: RegexOptions = {
|
||||
|
@ -20,6 +22,6 @@ export const replaceYouTubeLink: RegexOptions = {
|
|||
replace: (match) => {
|
||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||
// noinspection CheckTagEmptyBody
|
||||
return `<app-youtube id="${ match }"></app-youtube>`
|
||||
return `<app-youtube id="${match}"></app-youtube>`
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,12 +13,17 @@ export interface YouTubeFrameProps {
|
|||
|
||||
export const YouTubeFrame: React.FC<YouTubeFrameProps> = ({ id }) => {
|
||||
return (
|
||||
<OneClickEmbedding containerClassName={ 'embed-responsive embed-responsive-16by9' }
|
||||
previewContainerClassName={ 'embed-responsive-item' } hoverIcon={ 'youtube-play' }
|
||||
loadingImageUrl={ `https://i.ytimg.com/vi/${ id }/maxresdefault.jpg` }>
|
||||
<iframe className='embed-responsive-item' title={ `youtube video of ${ id }` }
|
||||
src={ `https://www.youtube-nocookie.com/embed/${ id }?autoplay=1` }
|
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"/>
|
||||
<OneClickEmbedding
|
||||
containerClassName={'embed-responsive embed-responsive-16by9'}
|
||||
previewContainerClassName={'embed-responsive-item'}
|
||||
hoverIcon={'youtube-play'}
|
||||
loadingImageUrl={`https://i.ytimg.com/vi/${id}/maxresdefault.jpg`}>
|
||||
<iframe
|
||||
className='embed-responsive-item'
|
||||
title={`youtube video of ${id}`}
|
||||
src={`https://www.youtube-nocookie.com/embed/${id}?autoplay=1`}
|
||||
allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture'
|
||||
/>
|
||||
</OneClickEmbedding>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ export class YoutubeReplacer extends ComponentReplacer {
|
|||
const attributes = getAttributesFromHedgeDocTag(node, 'youtube')
|
||||
if (attributes && attributes.id) {
|
||||
const videoId = attributes.id
|
||||
return <YouTubeFrame id={ videoId }/>
|
||||
return <YouTubeFrame id={videoId} />
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue