mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-16 16:14:43 -04:00
fix(cheatsheet): refactor cheatsheet to use app extensions as source
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
9d49401b4d
commit
24b0070909
53 changed files with 1164 additions and 275 deletions
|
@ -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'>
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -7,3 +7,9 @@
|
|||
.table-cheatsheet > tr > td {
|
||||
vertical-align: middle !important;
|
||||
}
|
||||
|
||||
.sticky {
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
bottom: 1rem;
|
||||
}
|
|
@ -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>
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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])
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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)`,
|
||||
``,
|
||||
':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
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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' : ''}`}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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/') }]
|
||||
}
|
||||
}
|
|
@ -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/')
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -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'
|
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue