fix(cheatsheet): refactor cheatsheet to use app extensions as source

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-02-21 19:46:56 +01:00
parent 9d49401b4d
commit 24b0070909
53 changed files with 1164 additions and 275 deletions

View file

@ -10,6 +10,7 @@ import { ShowIf } from '../../common/show-if/show-if'
import { SignInButton } from '../../landing-layout/navigation/sign-in-button'
import { UserDropdown } from '../../landing-layout/navigation/user-dropdown'
import { SettingsButton } from '../../layout/settings-dialog/settings-button'
import { CheatsheetButton } from './cheatsheet/cheatsheet-button'
import { HelpButton } from './help-button/help-button'
import { NavbarBranding } from './navbar-branding'
import { ReadOnlyModeButton } from './read-only-mode-button'
@ -47,6 +48,7 @@ export const AppBar: React.FC<AppBarProps> = ({ mode }) => {
<ReadOnlyModeButton />
</ShowIf>
<HelpButton />
<CheatsheetButton />
</ShowIf>
</Nav>
<Nav className='d-flex gap-2 align-items-center text-secondary justify-content-end'>

View file

@ -0,0 +1,73 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension'
import { EntryList } from './entry-list'
import React, { useMemo } from 'react'
import { Accordion } from 'react-bootstrap'
import { Trans } from 'react-i18next'
export interface GroupAccordionProps {
extensions: CheatsheetExtension[]
selectedEntry: CheatsheetExtension | undefined
onStateChange: (value: CheatsheetExtension) => void
}
const sortCategories = (
[keyA]: [string, CheatsheetExtension[]],
[keyB]: [string, CheatsheetExtension[]]
): -1 | 0 | 1 => {
if (keyA === keyB) {
return 0
} else if (keyA > keyB || keyA === 'other') {
return 1
} else {
return -1
}
}
type CheatsheetGroupMap = Map<string, CheatsheetExtension[]>
const reduceCheatsheetExtensionByCategory = (
state: CheatsheetGroupMap,
extension: CheatsheetExtension
): CheatsheetGroupMap => {
const groupKey = extension.categoryI18nKey ?? 'other'
const list = state.get(groupKey) ?? []
list.push(extension)
if (!state.has(groupKey)) {
state.set(groupKey, list)
}
return state
}
/**
* Renders {@link EntryList entry lists} grouped by category.
*
* @param extensions The extensions which should be listed
* @param selectedEntry The entry that should be displayed as selected
* @param onStateChange A callback that should be executed if a new entry was selected
*/
export const CategoryAccordion: React.FC<GroupAccordionProps> = ({ extensions, selectedEntry, onStateChange }) => {
const groupEntries = useMemo(() => {
const groupings = extensions.reduce(reduceCheatsheetExtensionByCategory, new Map<string, CheatsheetExtension[]>())
return Array.from(groupings.entries()).sort(sortCategories)
}, [extensions])
const elements = useMemo(() => {
return groupEntries.map(([groupKey, groupExtensions]) => (
<Accordion.Item eventKey={groupKey} key={groupKey}>
<Accordion.Header>
<Trans i18nKey={`cheatsheet.categories.${groupKey}`}></Trans>
</Accordion.Header>
<Accordion.Body className={'p-0'}>
<EntryList selectedEntry={selectedEntry} extensions={groupExtensions} onStateChange={onStateChange} />
</Accordion.Body>
</Accordion.Item>
))
}, [groupEntries, onStateChange, selectedEntry])
return <Accordion defaultActiveKey={groupEntries[0][0]}>{elements}</Accordion>
}

View file

@ -0,0 +1,44 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
import { cypressId } from '../../../../utils/cypress-attribute'
import { CommonModal } from '../../../common/modals/common-modal'
import { CheatsheetModalBody } from './cheatsheet-modal-body'
import React, { Fragment } from 'react'
import { Button } from 'react-bootstrap'
import { QuestionCircle as IconQuestionCircle } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
/**
* Shows a button that opens the cheatsheet dialog.
*/
export const CheatsheetButton: React.FC = () => {
const { t } = useTranslation()
const [modalVisibility, showModal, closeModal] = useBooleanState()
return (
<Fragment>
<Button
{...cypressId('open.cheatsheet-button')}
title={t('cheatsheet.button') ?? undefined}
className={'mx-2'}
variant='outline-dark'
size={'sm'}
onClick={showModal}>
<Trans i18nKey={'cheatsheet.button'}></Trans>
</Button>
<CommonModal
modalSize={'xl'}
titleIcon={IconQuestionCircle}
show={modalVisibility}
onHide={closeModal}
showCloseButton={true}
titleI18nKey={'cheatsheet.modal.title'}>
<CheatsheetModalBody />
</CommonModal>
</Fragment>
)
}

View file

@ -0,0 +1,85 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import HighlightedCode from '../../../../extensions/extra-integrations/highlighted-code-fence/highlighted-code'
import { HtmlToReact } from '../../../common/html-to-react/html-to-react'
import { ExtensionEventEmitterProvider } from '../../../markdown-renderer/hooks/use-extension-event-emitter'
import { RendererType } from '../../../render-page/window-post-message-communicator/rendering-message'
import type { CheatsheetEntry } from '../../cheatsheet/cheatsheet-extension'
import { EditorToRendererCommunicatorContextProvider } from '../../render-context/editor-to-renderer-communicator-context-provider'
import { RenderIframe } from '../../renderer-pane/render-iframe'
import { ReadMoreLinkItem } from './read-more-link-item'
import { useComponentsFromAppExtensions } from './use-components-from-app-extensions'
import MarkdownIt from 'markdown-it'
import React, { useEffect, useMemo, useState } from 'react'
import { ListGroupItem } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
interface CheatsheetRendererProps {
rootI18nKey?: string
extension: CheatsheetEntry
}
/**
* Renders the cheatsheet entry with description, example and rendered example.
*
* @param extension The extension to render
* @param rootI18nKey An additional i18n namespace
*/
export const CheatsheetEntryPane: React.FC<CheatsheetRendererProps> = ({ extension, rootI18nKey }) => {
const { t } = useTranslation()
const [content, setContent] = useState('')
const lines = useMemo(() => content.split('\n'), [content])
const i18nPrefix = useMemo(
() => `cheatsheet.${rootI18nKey ? `${rootI18nKey}.` : ''}${extension.i18nKey}.`,
[extension.i18nKey, rootI18nKey]
)
useEffect(() => {
setContent(t(`${i18nPrefix}example`) ?? '')
}, [extension, i18nPrefix, t])
const cheatsheetExtensionComponents = useComponentsFromAppExtensions(setContent)
const descriptionElements = useMemo(() => {
const content = t(`${i18nPrefix}description`)
const markdownIt = new MarkdownIt('default')
return <HtmlToReact htmlCode={markdownIt.render(content)}></HtmlToReact>
}, [i18nPrefix, t])
return (
<EditorToRendererCommunicatorContextProvider>
<ExtensionEventEmitterProvider>
{cheatsheetExtensionComponents}
<ListGroupItem>
<h4>
<Trans i18nKey={'cheatsheet.modal.headlines.description'} />
</h4>
{descriptionElements}
</ListGroupItem>
<ReadMoreLinkItem url={extension.readMoreUrl}></ReadMoreLinkItem>
<ListGroupItem>
<h4>
<Trans i18nKey={'cheatsheet.modal.headlines.exampleInput'} />
</h4>
<HighlightedCode code={content} wrapLines={true} language={'markdown'} startLineNumber={1} />
</ListGroupItem>
<ListGroupItem>
<h4>
<Trans i18nKey={'cheatsheet.modal.headlines.exampleOutput'} />
</h4>
<RenderIframe
frameClasses={'w-100'}
adaptFrameHeightToContent={true}
rendererType={RendererType.SIMPLE}
markdownContentLines={lines}></RenderIframe>
</ListGroupItem>
</ExtensionEventEmitterProvider>
</EditorToRendererCommunicatorContextProvider>
)
}

View file

@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { optionalAppExtensions } from '../../../../extensions/extra-integrations/optional-app-extensions'
import type { CheatsheetEntry, CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension'
import { isCheatsheetGroup } from '../../cheatsheet/cheatsheet-extension'
import { CategoryAccordion } from './category-accordion'
import { CheatsheetEntryPane } from './cheatsheet-entry-pane'
import { TopicSelection } from './topic-selection'
import React, { useCallback, useMemo, useState } from 'react'
import { Col, ListGroup, Modal, Row } from 'react-bootstrap'
import { Trans } from 'react-i18next'
/**
* Renders the tab content for the cheatsheet.
*/
export const CheatsheetModalBody: React.FC = () => {
const [selectedExtension, setSelectedExtension] = useState<CheatsheetExtension>()
const [selectedEntry, setSelectedEntry] = useState<CheatsheetEntry>()
const changeExtension = useCallback((value: CheatsheetExtension) => {
setSelectedExtension(value)
setSelectedEntry(isCheatsheetGroup(value) ? value.entries[0] : value)
}, [])
const extensions = useMemo(
() => optionalAppExtensions.flatMap((extension) => extension.buildCheatsheetExtensions()),
[]
)
return (
<Modal.Body>
<Row className={`mt-2`}>
<Col xs={3}>
<CategoryAccordion
extensions={extensions}
selectedEntry={selectedExtension}
onStateChange={changeExtension}
/>
</Col>
<Col xs={9}>
<ListGroup>
<TopicSelection
extension={selectedExtension}
selectedEntry={selectedEntry}
setSelectedEntry={setSelectedEntry}
/>
{selectedEntry !== undefined ? (
<CheatsheetEntryPane
rootI18nKey={isCheatsheetGroup(selectedExtension) ? selectedExtension.i18nKey : undefined}
extension={selectedEntry}
/>
) : (
<span>
<Trans i18nKey={'cheatsheet.modal.noSelection'}></Trans>
</span>
)}
</ListGroup>
</Col>
</Row>
</Modal.Body>
)
}

View file

@ -7,3 +7,9 @@
.table-cheatsheet > tr > td {
vertical-align: middle !important;
}
.sticky {
position: sticky;
top: 1rem;
bottom: 1rem;
}

View file

@ -0,0 +1,50 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension'
import styles from './cheatsheet.module.scss'
import React, { useMemo } from 'react'
import { ListGroup, ListGroupItem } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
interface CheatsheetListProps {
selectedEntry: CheatsheetExtension | undefined
extensions: CheatsheetExtension[]
onStateChange: (value: CheatsheetExtension) => void
}
const compareString = (value1: string, value2: string): -1 | 0 | 1 => {
return value1 === value2 ? 0 : value1 < value2 ? -1 : 1
}
/**
* Renders a list of cheatsheet entries.
*
* @param extensions The extensions whose cheatsheet entries should be listed
* @param selectedEntry The cheatsheet entry that should be rendered as selected.
* @param onStateChange A callback that is executed when a new entry has been selected
*/
export const EntryList: React.FC<CheatsheetListProps> = ({ extensions, selectedEntry, onStateChange }) => {
const { t } = useTranslation()
const listItems = useMemo(
() =>
extensions
.map((extension) => [extension, t(`cheatsheet.${extension.i18nKey}.title`)] as [CheatsheetExtension, string])
.sort(([, title1], [, title2]) => compareString(title1.toLowerCase(), title2.toLowerCase()))
.map(([cheatsheetExtension, title]) => (
<ListGroupItem
key={cheatsheetExtension.i18nKey}
action
active={cheatsheetExtension.i18nKey == selectedEntry?.i18nKey}
onClick={() => onStateChange(cheatsheetExtension)}>
{title}
</ListGroupItem>
)),
[extensions, onStateChange, selectedEntry, t]
)
return <ListGroup className={styles.sticky}>{listItems}</ListGroup>
}

View file

@ -0,0 +1,29 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ExternalLink } from '../../../common/links/external-link'
import React from 'react'
import { ListGroupItem } from 'react-bootstrap'
import { Trans } from 'react-i18next'
export interface ReadMoreLinkGroupProps {
url: URL | undefined
}
/**
* Renders the read more URL as external link.
*
* @param url The URL to display. If the URL is undefined then nothing will be rendered.
*/
export const ReadMoreLinkItem: React.FC<ReadMoreLinkGroupProps> = ({ url }) => {
return !url ? null : (
<ListGroupItem>
<h4>
<Trans i18nKey={'cheatsheet.modal.headlines.readMoreLink'} />
</h4>
<ExternalLink className={'text-dark'} text={url.toString()} href={url.toString()}></ExternalLink>
</ListGroupItem>
)
}

View file

@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { CheatsheetEntry, CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension'
import { isCheatsheetGroup } from '../../cheatsheet/cheatsheet-extension'
import React, { useMemo } from 'react'
import { Button, ButtonGroup, ListGroupItem } from 'react-bootstrap'
import { Trans } from 'react-i18next'
interface EntrySelectionProps {
extension: CheatsheetExtension | undefined
selectedEntry: CheatsheetEntry | undefined
setSelectedEntry: (value: CheatsheetEntry) => void
}
/**
* Renders a button group that contains the topics of the given extension.
* If the extension has no topics then the selection won't be displayed.
*
* @param extension The extension whose topics should be displayed
* @param selectedEntry The currently selected cheatsheet entry that should be displayed as active
* @param setSelectedEntry A callback that should be executed if a new topic has been selected
*/
export const TopicSelection: React.FC<EntrySelectionProps> = ({ extension, selectedEntry, setSelectedEntry }) => {
const listItems = useMemo(() => {
if (!isCheatsheetGroup(extension)) {
return null
}
return extension.entries.map((entry) => (
<Button
key={entry.i18nKey}
variant={selectedEntry?.i18nKey === entry.i18nKey ? 'primary' : 'outline-primary'}
onClick={() => setSelectedEntry(entry)}>
<Trans i18nKey={`cheatsheet.${extension.i18nKey}.${entry.i18nKey}.title`}></Trans>
</Button>
))
}, [extension, selectedEntry?.i18nKey, setSelectedEntry])
return !listItems ? null : (
<ListGroupItem>
<h4>
<Trans i18nKey={'cheatsheet.modal.headlines.selectTopic'} />
</h4>
<ButtonGroup className={'mb-2'}>{listItems}</ButtonGroup>
</ListGroupItem>
)
}

View file

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { optionalAppExtensions } from '../../../../extensions/extra-integrations/optional-app-extensions'
import type { CheatsheetExtensionComponentProps } from '../../cheatsheet/cheatsheet-extension'
import { isCheatsheetGroup } from '../../cheatsheet/cheatsheet-extension'
import type { ReactElement } from 'react'
import React, { Fragment, useMemo } from 'react'
/**
* Generates react elements from components which are provided by cheatsheet extensions.
*/
export const useComponentsFromAppExtensions = (
setContent: CheatsheetExtensionComponentProps['setContent']
): ReactElement => {
return useMemo(() => {
return (
<Fragment key={'app-extensions'}>
{optionalAppExtensions
.flatMap((extension) => extension.buildCheatsheetExtensions())
.flatMap((extension) => (isCheatsheetGroup(extension) ? extension.entries : extension))
.map((extension) => {
if (extension.cheatsheetExtensionComponent) {
return React.createElement(extension.cheatsheetExtensionComponent, { key: extension.i18nKey, setContent })
}
})}
</Fragment>
)
}, [setContent])
}

View file

@ -1,67 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { TaskCheckedEventPayload } from '../../../../extensions/extra-integrations/task-list/event-emitting-task-list-checkbox'
import { TaskListCheckboxAppExtension } from '../../../../extensions/extra-integrations/task-list/task-list-checkbox-app-extension'
import { WaitSpinner } from '../../../common/wait-spinner/wait-spinner'
import { eventEmitterContext } from '../../../markdown-renderer/hooks/use-extension-event-emitter'
import type { Listener } from 'eventemitter2'
import { EventEmitter2 } from 'eventemitter2'
import React, { Suspense, useEffect, useMemo } from 'react'
export interface CheatsheetLineProps {
markdown: string
onTaskCheckedChange: (newValue: boolean) => void
}
const HighlightedCode = React.lazy(
() => import('../../../../extensions/extra-integrations/highlighted-code-fence/highlighted-code')
)
const DocumentMarkdownRenderer = React.lazy(() => import('../../../markdown-renderer/document-markdown-renderer'))
/**
* Renders one line in the {@link CheatsheetTabContent cheat sheet}.
* This line shows an minimal markdown example and how it would be rendered.
*
* @param markdown The markdown to be shown and rendered
* @param onTaskCheckedChange A callback to call if a task would be clicked
*/
export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ markdown, onTaskCheckedChange }) => {
const lines = useMemo(() => markdown.split('\n'), [markdown])
const eventEmitter = useMemo(() => new EventEmitter2(), [])
useEffect(() => {
const handler = eventEmitter.on(
TaskListCheckboxAppExtension.EVENT_NAME,
({ checked }: TaskCheckedEventPayload) => onTaskCheckedChange(checked),
{ objectify: true }
) as Listener
return () => {
handler.off()
}
})
return (
<Suspense
fallback={
<tr>
<td colSpan={2}>
<WaitSpinner />
</td>
</tr>
}>
<tr>
<td>
<eventEmitterContext.Provider value={eventEmitter}>
<DocumentMarkdownRenderer markdownContentLines={lines} baseUrl={'https://example.org'} />
</eventEmitterContext.Provider>
</td>
<td className={'markdown-body'}>
<HighlightedCode code={markdown} wrapLines={true} startLineNumber={1} language={'markdown'} />
</td>
</tr>
</Suspense>
)
}

View file

@ -1,64 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { CheatsheetLine } from './cheatsheet-line'
import styles from './cheatsheet.module.scss'
import React, { useMemo, useState } from 'react'
import { Table } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
/**
* Renders the content of the cheat sheet for the {@link HelpModal}.
*/
export const CheatsheetTabContent: React.FC = () => {
const { t } = useTranslation()
const [checked, setChecked] = useState<boolean>(false)
const codes = useMemo(
() => [
`**${t('editor.editorToolbar.bold')}**`,
`*${t('editor.editorToolbar.italic')}*`,
`++${t('editor.editorToolbar.underline')}++`,
`~~${t('editor.editorToolbar.strikethrough')}~~`,
'H~2~O',
'19^th^',
`==${t('editor.help.cheatsheet.highlightedText')}==`,
`# ${t('editor.editorToolbar.header')}`,
`\`${t('editor.editorToolbar.code')}\``,
'```javascript=\nvar x = 5;\n```',
`> ${t('editor.editorToolbar.blockquote')}`,
`- ${t('editor.editorToolbar.unorderedList')}`,
`1. ${t('editor.editorToolbar.orderedList')}`,
`- [${checked ? 'x' : ' '}] ${t('editor.editorToolbar.checkList')}`,
`[${t('editor.editorToolbar.link')}](https://example.com)`,
`![${t('editor.editorToolbar.image')}](/icons/apple-touch-icon.png)`,
':smile:',
':bi-bootstrap:',
`:::info\n${t('editor.help.cheatsheet.exampleAlert')}\n:::`
],
[checked, t]
)
return (
<Table className={`table-condensed ${styles['table-cheatsheet']}`}>
<thead>
<tr>
<th>
<Trans i18nKey='editor.help.cheatsheet.example' />
</th>
<th>
<Trans i18nKey='editor.help.cheatsheet.syntax' />
</th>
</tr>
</thead>
<tbody>
{codes.map((code) => (
<CheatsheetLine markdown={code} key={code} onTaskCheckedChange={setChecked} />
))}
</tbody>
</Table>
)
}
export default CheatsheetTabContent

View file

@ -5,12 +5,11 @@
*/
import { useBooleanState } from '../../../../hooks/common/use-boolean-state'
import { cypressId } from '../../../../utils/cypress-attribute'
import { UiIcon } from '../../../common/icons/ui-icon'
import { IconButton } from '../../../common/icon-button/icon-button'
import { HelpModal } from './help-modal'
import React, { Fragment } from 'react'
import { Button } from 'react-bootstrap'
import { QuestionCircle as IconQuestionCircle } from 'react-bootstrap-icons'
import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
/**
* Renders the button to open the {@link HelpModal}.
@ -21,15 +20,16 @@ export const HelpButton: React.FC = () => {
return (
<Fragment>
<Button
<IconButton
icon={IconQuestionCircle}
{...cypressId('editor-help-button')}
title={t('editor.documentBar.help') ?? undefined}
className='ms-2 text-secondary'
className='ms-2'
size='sm'
variant='outline-light'
variant='outline-dark'
onClick={showModal}>
<UiIcon icon={IconQuestionCircle} />
</Button>
<Trans i18nKey={'editor.documentBar.help'} />
</IconButton>
<HelpModal show={modalVisibility} onHide={closeModal} />
</Fragment>
)

View file

@ -5,7 +5,6 @@
*/
import type { ModalVisibilityProps } from '../../../common/modals/common-modal'
import { CommonModal } from '../../../common/modals/common-modal'
import { CheatsheetTabContent } from './cheatsheet-tab-content'
import { LinksTabContent } from './links-tab-content'
import { ShortcutTabContent } from './shortcuts-tab-content'
import React, { useMemo, useState } from 'react'
@ -14,7 +13,6 @@ import { QuestionCircle as IconQuestionCircle } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
export enum HelpTabStatus {
Cheatsheet = 'cheatsheet.title',
Shortcuts = 'shortcuts.title',
Links = 'links.title'
}
@ -31,13 +29,11 @@ export enum HelpTabStatus {
* @param onHide A callback when the modal should be closed again
*/
export const HelpModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
const [tab, setTab] = useState<HelpTabStatus>(HelpTabStatus.Cheatsheet)
const [tab, setTab] = useState<HelpTabStatus>(HelpTabStatus.Shortcuts)
const { t } = useTranslation()
const tabContent = useMemo(() => {
switch (tab) {
case HelpTabStatus.Cheatsheet:
return <CheatsheetTabContent />
case HelpTabStatus.Shortcuts:
return <ShortcutTabContent />
case HelpTabStatus.Links:
@ -48,15 +44,9 @@ export const HelpModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
const modalTitle = useMemo(() => t('editor.documentBar.help') + ' - ' + t(`editor.help.${tab}`), [t, tab])
return (
<CommonModal modalSize={'lg'} titleIcon={IconQuestionCircle} show={show} onHide={onHide} title={modalTitle}>
<CommonModal modalSize={'xl'} titleIcon={IconQuestionCircle} show={show} onHide={onHide} title={modalTitle}>
<Modal.Body>
<nav className='nav nav-tabs'>
<Button
variant={'light'}
className={`nav-link nav-item ${tab === HelpTabStatus.Cheatsheet ? 'active' : ''}`}
onClick={() => setTab(HelpTabStatus.Cheatsheet)}>
<Trans i18nKey={'editor.help.cheatsheet.title'} />
</Button>
<Button
variant={'light'}
className={`nav-link nav-item ${tab === HelpTabStatus.Shortcuts ? 'active' : ''}`}

View file

@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type React from 'react'
export interface CheatsheetExtensionComponentProps {
setContent: (dispatcher: string | ((prevState: string) => string)) => void
}
export type CheatsheetExtension = CheatsheetEntry | CheatsheetGroup
export const isCheatsheetGroup = (extension: CheatsheetExtension | undefined): extension is CheatsheetGroup => {
return (extension as CheatsheetGroup)?.entries !== undefined
}
export interface CheatsheetGroup {
i18nKey: string
categoryI18nKey?: string
entries: CheatsheetEntry[]
}
export interface CheatsheetEntry {
i18nKey: string
categoryI18nKey?: string
cheatsheetExtensionComponent?: React.FC<CheatsheetExtensionComponentProps>
readMoreUrl?: URL
}

View file

@ -0,0 +1,63 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AppExtension } from '../../../../extensions/base/app-extension'
import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { BasicMarkdownSyntaxMarkdownExtension } from './basic-markdown-syntax-markdown-extension'
export class BasicMarkdownSyntaxAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new BasicMarkdownSyntaxMarkdownExtension()]
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [
{
i18nKey: 'basics.basicFormatting',
categoryI18nKey: 'basic'
},
{
i18nKey: 'basics.abbreviation',
categoryI18nKey: 'basic'
},
{
i18nKey: 'basics.footnote',
categoryI18nKey: 'basic'
},
{
i18nKey: 'basics.headlines',
categoryI18nKey: 'basic',
entries: [
{
i18nKey: 'hashtag'
},
{
i18nKey: 'equal'
}
]
},
{
i18nKey: 'basics.code',
categoryI18nKey: 'basic',
entries: [{ i18nKey: 'inline' }, { i18nKey: 'block' }]
},
{
i18nKey: 'basics.lists',
categoryI18nKey: 'basic',
entries: [{ i18nKey: 'unordered' }, { i18nKey: 'ordered' }]
},
{
i18nKey: 'basics.images',
categoryI18nKey: 'basic',
entries: [{ i18nKey: 'basic' }, { i18nKey: 'size' }]
},
{
i18nKey: 'basics.links',
categoryI18nKey: 'basic'
}
]
}
}

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownRendererExtension } from './base/markdown-renderer-extension'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { imageSize } from '@hedgedoc/markdown-it-plugins'
import type MarkdownIt from 'markdown-it'
import abbreviation from 'markdown-it-abbr'
@ -17,7 +17,7 @@ import superscript from 'markdown-it-sup'
/**
* Adds some common markdown syntaxes to the markdown rendering.
*/
export class GenericSyntaxMarkdownExtension extends MarkdownRendererExtension {
export class BasicMarkdownSyntaxMarkdownExtension extends MarkdownRendererExtension {
public configureMarkdownIt(markdownIt: MarkdownIt): void {
abbreviation(markdownIt)
definitionList(markdownIt)

View file

@ -0,0 +1,19 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AppExtension } from '../../../../extensions/base/app-extension'
import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { BootstrapIconMarkdownExtension } from './bootstrap-icon-markdown-extension'
export class BootstrapIconAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new BootstrapIconMarkdownExtension()]
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'bootstrapIcon', readMoreUrl: new URL('https://icons.getbootstrap.com/') }]
}
}

View file

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AppExtension } from '../../../../extensions/base/app-extension'
import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { EmojiMarkdownExtension } from './emoji-markdown-extension'
export class EmojiAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new EmojiMarkdownExtension()]
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [
{
i18nKey: 'emoji',
readMoreUrl: new URL('https://twemoji.twitter.com/')
}
]
}
}

View file

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AppExtension } from '../../../../extensions/base/app-extension'
import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { IframeCapsuleMarkdownExtension } from './iframe-capsule-markdown-extension'
export class IframeCapsuleAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new IframeCapsuleMarkdownExtension()]
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [
{
i18nKey: 'iframeCapsule',
categoryI18nKey: 'embedding'
}
]
}
}

View file

@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AppExtension } from '../../../../extensions/base/app-extension'
import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { ImagePlaceholderMarkdownExtension } from './image-placeholder-markdown-extension'
export class ImagePlaceholderAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new ImagePlaceholderMarkdownExtension()]
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [
{
i18nKey: 'imagePlaceholder'
}
]
}
}

View file

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AppExtension } from '../../../../extensions/base/app-extension'
import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { TableOfContentsMarkdownExtension } from './table-of-contents-markdown-extension'
import type EventEmitter2 from 'eventemitter2'
export class TableOfContentsAppExtension extends AppExtension {
buildMarkdownRendererExtensions(eventEmitter?: EventEmitter2): MarkdownRendererExtension[] {
return [new TableOfContentsMarkdownExtension(eventEmitter)]
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [
{
i18nKey: 'toc',
entries: [
{
i18nKey: 'basic'
},
{
i18nKey: 'levelLimit'
}
]
}
]
}
}

View file

@ -1,10 +1,10 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { tocSlugify } from '../../editor-page/table-of-contents/toc-slugify'
import { MarkdownRendererExtension } from './base/markdown-renderer-extension'
import { tocSlugify } from '../../../editor-page/table-of-contents/toc-slugify'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import type { TocAst } from '@hedgedoc/markdown-it-plugins'
import { toc } from '@hedgedoc/markdown-it-plugins'
import equal from 'fast-deep-equal'

View file

@ -1,20 +1,14 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
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'
import { IframeCapsuleMarkdownExtension } from '../extensions/iframe-capsule/iframe-capsule-markdown-extension'
import { ImagePlaceholderMarkdownExtension } from '../extensions/image-placeholder/image-placeholder-markdown-extension'
import { ProxyImageMarkdownExtension } from '../extensions/image/proxy-image-markdown-extension'
import { LinkAdjustmentMarkdownExtension } from '../extensions/link-replacer/link-adjustment-markdown-extension'
import { LinkifyFixMarkdownExtension } from '../extensions/linkify-fix/linkify-fix-markdown-extension'
import { TableOfContentsMarkdownExtension } from '../extensions/table-of-contents-markdown-extension'
import { UploadIndicatingImageFrameMarkdownExtension } from '../extensions/upload-indicating-image-frame/upload-indicating-image-frame-markdown-extension'
import { useExtensionEventEmitter } from './use-extension-event-emitter'
import { useMemo } from 'react'
@ -35,14 +29,8 @@ export const useMarkdownExtensions = (
return [
...optionalAppExtensions.flatMap((extension) => extension.buildMarkdownRendererExtensions(extensionEventEmitter)),
...additionalExtensions,
new TableOfContentsMarkdownExtension(),
new IframeCapsuleMarkdownExtension(),
new ImagePlaceholderMarkdownExtension(),
new UploadIndicatingImageFrameMarkdownExtension(),
new LinkAdjustmentMarkdownExtension(baseUrl),
new EmojiMarkdownExtension(),
new BootstrapIconMarkdownExtension(),
new GenericSyntaxMarkdownExtension(),
new LinkifyFixMarkdownExtension(),
new DebuggerMarkdownExtension(),
new ProxyImageMarkdownExtension()

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ShowIf } from '../common/show-if/show-if'
import { TableOfContentsMarkdownExtension } from '../markdown-renderer/extensions/table-of-contents-markdown-extension'
import { TableOfContentsMarkdownExtension } from '../markdown-renderer/extensions/table-of-contents/table-of-contents-markdown-extension'
import { useExtensionEventEmitterHandler } from '../markdown-renderer/hooks/use-extension-event-emitter'
import styles from './markdown-document.module.scss'
import { WidthBasedTableOfContents } from './width-based-table-of-contents'