mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 11:16:31 -05:00
feat(frontend): Add Asciinema replacer
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
9ccfaf3d0e
commit
5a2a3a4964
11 changed files with 253 additions and 0 deletions
|
@ -53,6 +53,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
||||||
- Notes may now be deleted directly from the history page
|
- Notes may now be deleted directly from the history page
|
||||||
- HedgeDoc instances can be branded either with a '@ \<custom string\>' or '@ \<custom logo\>' after the HedgeDoc logo and text
|
- HedgeDoc instances can be branded either with a '@ \<custom string\>' or '@ \<custom logo\>' after the HedgeDoc logo and text
|
||||||
- Images will be loaded via proxy if an image proxy is configured in the backend
|
- Images will be loaded via proxy if an image proxy is configured in the backend
|
||||||
|
- Asciinema videos may be embedded by pasting the URL of one video into a single line
|
||||||
- The toolbar includes an emoji and fork-awesome icon picker.
|
- The toolbar includes an emoji and fork-awesome icon picker.
|
||||||
- Collapsible blocks can be added via a toolbar button or via autocompletion of "<details"
|
- Collapsible blocks can be added via a toolbar button or via autocompletion of "<details"
|
||||||
- Added shortcodes for [fork-awesome icons](https://forkaweso.me/Fork-Awesome/icons/) (e.g. `:fa-picture-o:`)
|
- Added shortcodes for [fork-awesome icons](https://forkaweso.me/Fork-Awesome/icons/) (e.g. `:fa-picture-o:`)
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Asciinema renders a click shield 1`] = `
|
||||||
|
<div>
|
||||||
|
<span>
|
||||||
|
This is a click shield for
|
||||||
|
<span
|
||||||
|
class="ratio ratio-16x9"
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
allowfullscreen=""
|
||||||
|
class=""
|
||||||
|
src="https://asciinema.org/a/validAsciinemaId/iframe?autoplay=1"
|
||||||
|
title="asciinema cast validAsciinemaId"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`asciinema markdown extension renders plain asciinema URLs 1`] = `
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<span>
|
||||||
|
this is a mock for the asciinema frame with id
|
||||||
|
190123709
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
`;
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import type { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
|
||||||
|
import { AppExtension } from '../../base/app-extension'
|
||||||
|
import { AsciinemaMarkdownExtension } from './asciinema-markdown-extension'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds support for Asciinema embeddings to the markdown rendering.
|
||||||
|
*
|
||||||
|
* @see https://asciinema.org
|
||||||
|
*/
|
||||||
|
export class AsciinemaAppExtension extends AppExtension {
|
||||||
|
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
|
||||||
|
return [new AsciinemaMarkdownExtension()]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import * as ClickShieldModule from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
|
||||||
|
import type { ClickShieldProps } from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
|
||||||
|
import { AsciinemaFrame } from './asciinema-frame'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
jest.mock('../../../components/markdown-renderer/replace-components/click-shield/click-shield')
|
||||||
|
|
||||||
|
describe('Asciinema', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(ClickShieldModule, 'ClickShield').mockImplementation((({ children }) => {
|
||||||
|
return <span>This is a click shield for {children}</span>
|
||||||
|
}) as React.FC<ClickShieldProps>)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders a click shield', () => {
|
||||||
|
const view = render(<AsciinemaFrame id={'validAsciinemaId'} />)
|
||||||
|
expect(view.container).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { ClickShield } from '../../../components/markdown-renderer/replace-components/click-shield/click-shield'
|
||||||
|
import type { IdProps } from '../../../components/markdown-renderer/replace-components/custom-tag-with-id-component-replacer'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders an embedding for https://asciinema.org
|
||||||
|
*
|
||||||
|
* @param id The id from the asciinema url
|
||||||
|
*/
|
||||||
|
export const AsciinemaFrame: React.FC<IdProps> = ({ id }) => {
|
||||||
|
return (
|
||||||
|
<ClickShield
|
||||||
|
hoverIcon={'play'}
|
||||||
|
targetDescription={'asciinema'}
|
||||||
|
fallbackPreviewImageUrl={`https://asciinema.org/a/${id}.png`}
|
||||||
|
fallbackBackgroundColor={'#d40000'}
|
||||||
|
containerClassName={''}
|
||||||
|
data-cypress-id={'click-shield-asciinema'}>
|
||||||
|
<span className={'ratio ratio-16x9'}>
|
||||||
|
<iframe
|
||||||
|
allowFullScreen={true}
|
||||||
|
className=''
|
||||||
|
title={`asciinema cast ${id}`}
|
||||||
|
src={`https://asciinema.org/a/${id}/iframe?autoplay=1`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</ClickShield>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import type { IdProps } from '../../../components/markdown-renderer/replace-components/custom-tag-with-id-component-replacer'
|
||||||
|
import { mockI18n } from '../../../components/markdown-renderer/test-utils/mock-i18n'
|
||||||
|
import { TestMarkdownRenderer } from '../../../components/markdown-renderer/test-utils/test-markdown-renderer'
|
||||||
|
import * as AsciinemaFrameModule from './asciinema-frame'
|
||||||
|
import { AsciinemaMarkdownExtension } from './asciinema-markdown-extension'
|
||||||
|
import { render } from '@testing-library/react'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
jest.mock('./asciinema-frame')
|
||||||
|
|
||||||
|
describe('asciinema markdown extension', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest
|
||||||
|
.spyOn(AsciinemaFrameModule, 'AsciinemaFrame')
|
||||||
|
.mockImplementation((({ id }) => (
|
||||||
|
<span>this is a mock for the asciinema frame with id {id}</span>
|
||||||
|
)) as React.FC<IdProps>)
|
||||||
|
await mockI18n()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.resetAllMocks()
|
||||||
|
jest.resetModules()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders plain asciinema URLs', () => {
|
||||||
|
const view = render(
|
||||||
|
<TestMarkdownRenderer
|
||||||
|
extensions={[new AsciinemaMarkdownExtension()]}
|
||||||
|
content={'https://asciinema.org/a/190123709'}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
expect(view.container).toMatchSnapshot()
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { MarkdownRendererExtension } from '../../../components/markdown-renderer/extensions/base/markdown-renderer-extension'
|
||||||
|
import type { ComponentReplacer } from '../../../components/markdown-renderer/replace-components/component-replacer'
|
||||||
|
import { CustomTagWithIdComponentReplacer } from '../../../components/markdown-renderer/replace-components/custom-tag-with-id-component-replacer'
|
||||||
|
import { AsciinemaFrame } from './asciinema-frame'
|
||||||
|
import { replaceAsciinemaLinkMarkdownItPlugin } from './replace-asciinema-link'
|
||||||
|
import type MarkdownIt from 'markdown-it/lib'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds asciinema embeddings to the markdown rendering by detecting asciinema.org links.
|
||||||
|
*/
|
||||||
|
export class AsciinemaMarkdownExtension extends MarkdownRendererExtension {
|
||||||
|
public static readonly tagName = 'app-asciinema'
|
||||||
|
|
||||||
|
public configureMarkdownIt(markdownIt: MarkdownIt): void {
|
||||||
|
replaceAsciinemaLinkMarkdownItPlugin(markdownIt)
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildReplacers(): ComponentReplacer[] {
|
||||||
|
return [new CustomTagWithIdComponentReplacer(AsciinemaFrame, AsciinemaMarkdownExtension.tagName)]
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildTagNameAllowList(): string[] {
|
||||||
|
return [AsciinemaMarkdownExtension.tagName]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { replaceAsciinemaLinkMarkdownItPlugin } from './replace-asciinema-link'
|
||||||
|
import MarkdownIt from 'markdown-it/lib'
|
||||||
|
|
||||||
|
describe('Replace asciinema link', () => {
|
||||||
|
let markdownIt: MarkdownIt
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
markdownIt = new MarkdownIt('default', {
|
||||||
|
html: false,
|
||||||
|
breaks: true,
|
||||||
|
langPrefix: '',
|
||||||
|
typographer: true
|
||||||
|
})
|
||||||
|
markdownIt.use(replaceAsciinemaLinkMarkdownItPlugin)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('will replace a valid URL', () => {
|
||||||
|
expect(markdownIt.renderInline('https://asciinema.org/a/123981234')).toBe(
|
||||||
|
`<app-asciinema id='123981234'></app-asciinema>`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("won't replace an URL without path", () => {
|
||||||
|
expect(markdownIt.renderInline('https://asciinema.org/123981234')).toBe(`https://asciinema.org/123981234`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("won't replace an URL with non-numeric id", () => {
|
||||||
|
expect(markdownIt.renderInline('https://asciinema.org/a/12f3981234')).toBe(`https://asciinema.org/a/12f3981234`)
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import type { RegexOptions } from '../../../external-types/markdown-it-regex/interface'
|
||||||
|
import { AsciinemaMarkdownExtension } from './asciinema-markdown-extension'
|
||||||
|
import type MarkdownIt from 'markdown-it'
|
||||||
|
import markdownItRegex from 'markdown-it-regex'
|
||||||
|
|
||||||
|
const protocolRegex = /(?:http(?:s)?:\/\/)?/
|
||||||
|
const domainRegex = /(?:asciinema\.org\/a\/)/
|
||||||
|
const idRegex = /(\d+)/
|
||||||
|
const tailRegex = /(?:[./?#].*)?/
|
||||||
|
const asciinemaUrlRegex = new RegExp(
|
||||||
|
`^(?:${protocolRegex.source}${domainRegex.source}${idRegex.source}${tailRegex.source})$`,
|
||||||
|
'i'
|
||||||
|
)
|
||||||
|
|
||||||
|
const replaceAsciinemaLink: RegexOptions = {
|
||||||
|
name: 'asciinema-link',
|
||||||
|
regex: asciinemaUrlRegex,
|
||||||
|
replace: (match) => {
|
||||||
|
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||||
|
// noinspection CheckTagEmptyBody
|
||||||
|
return `<${AsciinemaMarkdownExtension.tagName} id='${match}'></${AsciinemaMarkdownExtension.tagName}>`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replacer for asciinema links.
|
||||||
|
*/
|
||||||
|
export const replaceAsciinemaLinkMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt) =>
|
||||||
|
markdownItRegex(markdownIt, replaceAsciinemaLink)
|
|
@ -6,6 +6,7 @@
|
||||||
import type { AppExtension } from '../base/app-extension'
|
import type { AppExtension } from '../base/app-extension'
|
||||||
import { AbcjsAppExtension } from './abcjs/abcjs-app-extension'
|
import { AbcjsAppExtension } from './abcjs/abcjs-app-extension'
|
||||||
import { AlertAppExtension } from './alert/alert-app-extension'
|
import { AlertAppExtension } from './alert/alert-app-extension'
|
||||||
|
import { AsciinemaAppExtension } from './asciinema/asciinema-app-extension'
|
||||||
import { BlockquoteAppExtension } from './blockquote/blockquote-app-extension'
|
import { BlockquoteAppExtension } from './blockquote/blockquote-app-extension'
|
||||||
import { CsvTableAppExtension } from './csv/csv-table-app-extension'
|
import { CsvTableAppExtension } from './csv/csv-table-app-extension'
|
||||||
import { FlowchartAppExtension } from './flowchart/flowchart-app-extension'
|
import { FlowchartAppExtension } from './flowchart/flowchart-app-extension'
|
||||||
|
@ -36,6 +37,7 @@ export const optionalAppExtensions: AppExtension[] = [
|
||||||
new GistAppExtension(),
|
new GistAppExtension(),
|
||||||
new GraphvizAppExtension(),
|
new GraphvizAppExtension(),
|
||||||
new KatexAppExtension(),
|
new KatexAppExtension(),
|
||||||
|
new AsciinemaAppExtension(),
|
||||||
new LegacyShortcodesAppExtension(),
|
new LegacyShortcodesAppExtension(),
|
||||||
new MermaidAppExtension(),
|
new MermaidAppExtension(),
|
||||||
new PlantumlAppExtension(),
|
new PlantumlAppExtension(),
|
||||||
|
|
Loading…
Reference in a new issue