mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-22 01:36:29 -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
|
||||
- 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
|
||||
- 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.
|
||||
- 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:`)
|
||||
|
|
|
@ -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 { AbcjsAppExtension } from './abcjs/abcjs-app-extension'
|
||||
import { AlertAppExtension } from './alert/alert-app-extension'
|
||||
import { AsciinemaAppExtension } from './asciinema/asciinema-app-extension'
|
||||
import { BlockquoteAppExtension } from './blockquote/blockquote-app-extension'
|
||||
import { CsvTableAppExtension } from './csv/csv-table-app-extension'
|
||||
import { FlowchartAppExtension } from './flowchart/flowchart-app-extension'
|
||||
|
@ -36,6 +37,7 @@ export const optionalAppExtensions: AppExtension[] = [
|
|||
new GistAppExtension(),
|
||||
new GraphvizAppExtension(),
|
||||
new KatexAppExtension(),
|
||||
new AsciinemaAppExtension(),
|
||||
new LegacyShortcodesAppExtension(),
|
||||
new MermaidAppExtension(),
|
||||
new PlantumlAppExtension(),
|
||||
|
|
Loading…
Reference in a new issue