feat(frontend): replace forkawesome with bootstrap icons

These icon replace fork awesome. A linter informs the user about the deprecation.

See https://github.com/hedgedoc/hedgedoc/issues/2929

Co-authored-by: Philip Molares <philip.molares@udo.edu>
Co-authored-by: Tilman Vatteroth <git@tilmanvatteroth.de>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Philip Molares 2023-02-05 22:05:02 +01:00 committed by Tilman Vatteroth
parent e7246f1484
commit 1c16e25e14
179 changed files with 4974 additions and 1943 deletions

View file

@ -0,0 +1,35 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`bootstrap icon markdown extension doesn't render invalid icon 1`] = `
<div>
<p />
</div>
`;
exports[`bootstrap icon markdown extension doesn't render missing icon 1`] = `
<div>
<p>
:bi-:
</p>
</div>
`;
exports[`bootstrap icon markdown extension renders correct icon 1`] = `
<div>
<p>
<span
data-svg-mock="true"
data-testid="lazy-bootstrap-icon-alarm"
fill="currentColor"
height="1em"
width="1em"
/>
</p>
</div>
`;

View file

@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { isBootstrapIconName } from '../../../common/icons/bootstrap-icons'
import { LazyBootstrapIcon } from '../../../common/icons/lazy-bootstrap-icon'
import type { NodeReplacement } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import { BootstrapIconMarkdownExtension } from './bootstrap-icon-markdown-extension'
import type { Element } from 'domhandler'
import React from 'react'
/**
* Replaces a bootstrap icon tag with the bootstrap icon react component.
*
* @see BootstrapIcon
*/
export class BootstrapIconComponentReplacer extends ComponentReplacer {
constructor() {
super()
}
public replace(node: Element): NodeReplacement {
const iconName = this.extractIconName(node)
if (!iconName || !isBootstrapIconName(iconName)) {
return DO_NOT_REPLACE
}
return React.createElement(LazyBootstrapIcon, { icon: iconName })
}
private extractIconName(element: Element): string | undefined {
return element.name === BootstrapIconMarkdownExtension.tagName && element.attribs && element.attribs.id
? element.attribs.id
: undefined
}
}

View file

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
import { BootstrapIconMarkdownExtension } from './bootstrap-icon-markdown-extension'
import { render, screen } from '@testing-library/react'
import React from 'react'
describe('bootstrap icon markdown extension', () => {
it('renders correct icon', async () => {
const view = render(
<TestMarkdownRenderer extensions={[new BootstrapIconMarkdownExtension()]} content={':bi-alarm:'} />
)
await screen.findByTestId('lazy-bootstrap-icon-alarm')
expect(view.container).toMatchSnapshot()
})
it("doesn't render missing icon", () => {
const view = render(<TestMarkdownRenderer extensions={[new BootstrapIconMarkdownExtension()]} content={':bi-:'} />)
expect(view.container).toMatchSnapshot()
})
it("doesn't render invalid icon", () => {
const view = render(
<TestMarkdownRenderer extensions={[new BootstrapIconMarkdownExtension()]} content={':bi-123:'} />
)
expect(view.container).toMatchSnapshot()
})
})

View file

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { BootstrapIconComponentReplacer } from './bootstrap-icon-component-replacer'
import { replaceBootstrapIconsMarkdownItPlugin } from './replace-bootstrap-icons'
import type MarkdownIt from 'markdown-it'
/**
* Adds Bootstrap icons via the :bi-$name: syntax.
*/
export class BootstrapIconMarkdownExtension extends MarkdownRendererExtension {
public static readonly tagName = 'app-bootstrap-icon'
public configureMarkdownIt(markdownIt: MarkdownIt): void {
replaceBootstrapIconsMarkdownItPlugin(markdownIt)
}
public buildReplacers(): ComponentReplacer[] {
return [new BootstrapIconComponentReplacer()]
}
public buildTagNameAllowList(): string[] {
return [BootstrapIconMarkdownExtension.tagName]
}
}

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { replaceBootstrapIconsMarkdownItPlugin } from './replace-bootstrap-icons'
import MarkdownIt from 'markdown-it'
describe('Replace bootstrap icons', () => {
let markdownIt: MarkdownIt
beforeEach(() => {
markdownIt = new MarkdownIt('default', {
html: false,
breaks: true,
langPrefix: '',
typographer: true
})
markdownIt.use(replaceBootstrapIconsMarkdownItPlugin)
})
it(`can detect a correct icon`, () => {
expect(markdownIt.renderInline(':bi-alarm:')).toBe('<app-bootstrap-icon id="alarm"></app-bootstrap-icon>')
})
it("won't detect an invalid id", () => {
const invalidIcon = ':bi-invalid:'
expect(markdownIt.renderInline(invalidIcon)).toBe(invalidIcon)
})
it("won't detect an empty id", () => {
const invalidIcon = ':bi-:'
expect(markdownIt.renderInline(invalidIcon)).toBe(invalidIcon)
})
it("won't detect a wrong id", () => {
const invalidIcon = ':bi-%?(:'
expect(markdownIt.renderInline(invalidIcon)).toBe(invalidIcon)
})
})

View file

@ -0,0 +1,30 @@
/*
* 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 { isBootstrapIconName } from '../../../common/icons/bootstrap-icons'
import { BootstrapIconMarkdownExtension } from './bootstrap-icon-markdown-extension'
import type MarkdownIt from 'markdown-it'
import markdownItRegex from 'markdown-it-regex'
const biRegex = /:bi-([\w-]+):/i
/**
* Replacer for bootstrap icon via the :bi-$name: syntax.
*/
export const replaceBootstrapIconsMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt) =>
markdownItRegex(markdownIt, {
name: 'bootstrap-icons',
regex: biRegex,
replace: (match) => {
if (isBootstrapIconName(match)) {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<${BootstrapIconMarkdownExtension.tagName} id="${match}"></${BootstrapIconMarkdownExtension.tagName}>`
} else {
return `:bi-${match}:`
}
}
} as RegexOptions)

View file

@ -1,17 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Emoji Markdown Extension renders a fork awesome code 1`] = `
<div>
<p>
<i
class="fa fa-circle-thin"
/>
</p>
</div>
`;
exports[`Emoji Markdown Extension renders a skin tone code 1`] = `
<div>
<p>

View file

@ -24,13 +24,6 @@ describe('Emoji Markdown Extension', () => {
expect(view.container).toMatchSnapshot()
})
it('renders a fork awesome code', () => {
const view = render(
<TestMarkdownRenderer extensions={[new EmojiMarkdownExtension()]} content={':fa-circle-thin:'} />
)
expect(view.container).toMatchSnapshot()
})
it('renders a skin tone code', () => {
const view = render(<TestMarkdownRenderer extensions={[new EmojiMarkdownExtension()]} content={':skin-tone-3:'} />)
expect(view.container).toMatchSnapshot()

View file

@ -3,7 +3,6 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ForkAwesomeIcons } from '../../../common/fork-awesome/fork-awesome-icons'
import emojiData from 'emoji-picker-element-data/en/emojibase/data.json'
interface EmojiEntry {
@ -28,15 +27,7 @@ const emojiSkinToneModifierMap = [1, 2, 3, 4, 5].reduce((reduceObject, modifierV
return reduceObject
}, {} as ShortCodeMap)
const forkAwesomeIconMap = ForkAwesomeIcons.reduce((reduceObject, icon) => {
const shortcode = `fa-${icon}`
// noinspection CheckTagEmptyBody
reduceObject[shortcode] = `<i class='fa fa-${icon}'></i>`
return reduceObject
}, {} as ShortCodeMap)
export const combinedEmojiData = {
...shortCodeMap,
...emojiSkinToneModifierMap,
...forkAwesomeIconMap
...emojiSkinToneModifierMap
}

View file

@ -7,6 +7,8 @@ import { ClickShield } from '../../replace-components/click-shield/click-shield'
import type { NativeRenderer, NodeReplacement, SubNodeTransform } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import type { Element } from 'domhandler'
import React from 'react'
import { Globe as IconGlobe } from 'react-bootstrap-icons'
/**
* Capsules <iframe> elements with a click shield.
@ -19,7 +21,7 @@ export class IframeCapsuleReplacer extends ComponentReplacer {
DO_NOT_REPLACE
) : (
<ClickShield
hoverIcon={'globe'}
hoverIcon={IconGlobe}
targetDescription={node.attribs.src}
data-cypress-id={'iframe-capsule-click-shield'}>
{nativeRenderer()}

View file

@ -4,13 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { cypressId } from '../../../../utils/cypress-attribute'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import { UiIcon } from '../../../common/icons/ui-icon'
import { acceptedMimeTypes } from '../../../common/upload-image-mimetypes'
import { useOnImageUpload } from './hooks/use-on-image-upload'
import { usePlaceholderSizeStyle } from './hooks/use-placeholder-size-style'
import styles from './image-placeholder.module.scss'
import React, { useCallback, useMemo, useRef, useState } from 'react'
import { Button } from 'react-bootstrap'
import { Upload as IconUpload } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
export interface PlaceholderImageFrameProps {
@ -105,7 +106,7 @@ export const ImagePlaceholder: React.FC<PlaceholderImageFrameProps> = ({
</div>
</div>
<Button size={'sm'} variant={'primary'} onClick={uploadButtonClicked}>
<ForkAwesomeIcon icon={'upload'} fixedWidth={true} className='my-2' />
<UiIcon icon={IconUpload} className='my-2' />
<Trans i18nKey={'editor.embeddings.placeholderImage.upload'} className='my-2' />
</Button>
</span>

View file

@ -3,9 +3,10 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import { UiIcon } from '../../../common/icons/ui-icon'
import { usePlaceholderSizeStyle } from '../image-placeholder/hooks/use-placeholder-size-style'
import React from 'react'
import { GearFill as IconGearFill } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
export interface UploadIndicatingFrameProps {
@ -30,7 +31,7 @@ export const UploadIndicatingFrame: React.FC<UploadIndicatingFrameProps> = ({ wi
<span className={'h1 border-bottom-0 my-2'}>
<Trans i18nKey={'renderer.uploadIndicator.uploadMessage'} />
</span>
<ForkAwesomeIcon icon={'cog'} size={'5x'} fixedWidth={true} className='my-2 fa-spin' />
<UiIcon icon={IconGearFill} size={5} className='my-2' spin={true} />
</span>
)
}

View file

@ -5,6 +5,7 @@
*/
import { optionalAppExtensions } from '../../../extensions/extra-integrations/optional-app-extensions'
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
import { BootstrapIconMarkdownExtension } from '../extensions/bootstrap-icons/bootstrap-icon-markdown-extension'
import { DebuggerMarkdownExtension } from '../extensions/debugger-markdown-extension'
import { EmojiMarkdownExtension } from '../extensions/emoji/emoji-markdown-extension'
import { GenericSyntaxMarkdownExtension } from '../extensions/generic-syntax-markdown-extension'
@ -53,6 +54,7 @@ export const useMarkdownExtensions = (
new UploadIndicatingImageFrameMarkdownExtension(),
new LinkAdjustmentMarkdownExtension(baseUrl),
new EmojiMarkdownExtension(),
new BootstrapIconMarkdownExtension(),
new GenericSyntaxMarkdownExtension(),
new LinkifyFixMarkdownExtension(),
new DebuggerMarkdownExtension(),

View file

@ -6,14 +6,13 @@
import type { PropsWithDataCypressId } from '../../../../utils/cypress-attribute'
import { cypressId } from '../../../../utils/cypress-attribute'
import { Logger } from '../../../../utils/logger'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import type { IconName } from '../../../common/fork-awesome/types'
import { ShowIf } from '../../../common/show-if/show-if'
import { ProxyImageFrame } from '../../extensions/image/proxy-image-frame'
import styles from './click-shield.module.scss'
import type { Property } from 'csstype'
import type { PropsWithChildren } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import type { Icon } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
const log = new Logger('OneClickEmbedding')
@ -21,7 +20,7 @@ const log = new Logger('OneClickEmbedding')
export interface ClickShieldProps extends PropsWithChildren<PropsWithDataCypressId> {
onImageFetch?: () => Promise<string>
fallbackPreviewImageUrl?: string
hoverIcon: IconName
hoverIcon: Icon
targetDescription: string
containerClassName?: string
fallbackBackgroundColor?: Property.BackgroundColor
@ -104,6 +103,16 @@ export const ClickShield: React.FC<ClickShieldProps> = ({
const hoverTextTranslationValues = useMemo(() => ({ target: targetDescription }), [targetDescription])
const icon = useMemo(
() =>
React.createElement(hoverIcon, {
width: '5em',
height: '5em',
className: 'mb-2'
}),
[hoverIcon]
)
return (
<span className={containerClassName} {...cypressId(props['data-cypress-id'])}>
<ShowIf condition={showChildren}>{children}</ShowIf>
@ -114,7 +123,7 @@ export const ClickShield: React.FC<ClickShieldProps> = ({
<span className={`${styles['preview-hover-text']}`}>
<Trans i18nKey={'renderer.clickShield.previewHoverText'} values={hoverTextTranslationValues} />
</span>
<ForkAwesomeIcon icon={hoverIcon} size={'5x'} className={'mb-2'} />
{icon}
</span>
</span>
</ShowIf>