mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-15 07:34:42 -04:00
Merge basic and full markdown renderer (#1040)
The original idea of the basic-markdown-renderer and the full-markdown-renderer was to reduce the complexity. The basic markdown renderer should just render markdown code and the full markdown renderer should implement all the special hedgedoc stuff like the embeddings. While developing other aspects of the software I noticed, that it makes more sense to split the markdown-renderer by the view and not by the features. E.g.: The slide markdown renderer must translate <hr> into <sections> for the slides and the document markdown renderer must provide precise scroll positions. But both need e.g. the ability to show a youtube video. Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
parent
364aec1318
commit
d9292e4db0
51 changed files with 777 additions and 979 deletions
9
src/components/common/simple-alert/simple-alert-props.ts
Normal file
9
src/components/common/simple-alert/simple-alert-props.ts
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface SimpleAlertProps {
|
||||||
|
show: boolean
|
||||||
|
}
|
16
src/components/common/wait-spinner/wait-spinner.tsx
Normal file
16
src/components/common/wait-spinner/wait-spinner.tsx
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
import { ForkAwesomeIcon } from '../fork-awesome/fork-awesome-icon'
|
||||||
|
|
||||||
|
export const WaitSpinner: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<div className={ 'm-3 d-flex align-items-center justify-content-center' }>
|
||||||
|
<ForkAwesomeIcon icon={ 'spinner' } className={ 'fa-spin' }/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -8,12 +8,9 @@ import React from 'react'
|
||||||
import { Alert } from 'react-bootstrap'
|
import { Alert } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
|
import { SimpleAlertProps } from '../common/simple-alert/simple-alert-props'
|
||||||
|
|
||||||
export interface ErrorWhileLoadingNoteAlertProps {
|
export const ErrorWhileLoadingNoteAlert: React.FC<SimpleAlertProps> = ({ show }) => {
|
||||||
show: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ErrorWhileLoadingNoteAlert: React.FC<ErrorWhileLoadingNoteAlertProps> = ({ show }) => {
|
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -8,12 +8,9 @@ import React from 'react'
|
||||||
import { Alert } from 'react-bootstrap'
|
import { Alert } from 'react-bootstrap'
|
||||||
import { Trans } from 'react-i18next'
|
import { Trans } from 'react-i18next'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
|
import { SimpleAlertProps } from '../common/simple-alert/simple-alert-props'
|
||||||
|
|
||||||
export interface LoadingNoteAlertProps {
|
export const LoadingNoteAlert: React.FC<SimpleAlertProps> = ({ show }) => {
|
||||||
show: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LoadingNoteAlert: React.FC<LoadingNoteAlertProps> = ({ show }) => {
|
|
||||||
return (
|
return (
|
||||||
<ShowIf condition={ show }>
|
<ShowIf condition={ show }>
|
||||||
<Alert variant={ 'info' } className={ 'my-2' }>
|
<Alert variant={ 'info' } className={ 'my-2' }>
|
||||||
|
|
|
@ -65,3 +65,5 @@ export const DocumentReadOnlyPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default DocumentReadOnlyPage
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { Suspense, useCallback } from 'react'
|
||||||
|
import { WaitSpinner } from '../../../common/wait-spinner/wait-spinner'
|
||||||
|
|
||||||
|
export interface CheatsheetLineProps {
|
||||||
|
code: string,
|
||||||
|
onTaskCheckedChange: (newValue: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const HighlightedCode = React.lazy(() => import('../../../markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code'))
|
||||||
|
const BasicMarkdownRenderer = React.lazy(() => import('../../../markdown-renderer/basic-markdown-renderer'))
|
||||||
|
|
||||||
|
export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ code, onTaskCheckedChange }) => {
|
||||||
|
const checkboxClick = useCallback((lineInMarkdown: number, newValue: boolean) => {
|
||||||
|
onTaskCheckedChange(newValue)
|
||||||
|
}, [onTaskCheckedChange])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={ <tr>
|
||||||
|
<td colSpan={ 2 }><WaitSpinner/></td>
|
||||||
|
</tr> }>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<BasicMarkdownRenderer
|
||||||
|
content={ code }
|
||||||
|
baseUrl={ 'https://example.org' }
|
||||||
|
onTaskCheckedChange={ checkboxClick }/>
|
||||||
|
</td>
|
||||||
|
<td className={ 'markdown-body' }>
|
||||||
|
<HighlightedCode code={ code } wrapLines={ true } startLineNumber={ 1 } language={ 'markdown' }/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
|
@ -4,17 +4,16 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import { Table } from 'react-bootstrap'
|
import { Table } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { BasicMarkdownRenderer } from '../../../markdown-renderer/basic-markdown-renderer'
|
|
||||||
import { BasicMarkdownItConfigurator } from '../../../markdown-renderer/markdown-it-configurator/BasicMarkdownItConfigurator'
|
|
||||||
import { HighlightedCode } from '../../../markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code'
|
|
||||||
import './cheatsheet.scss'
|
import './cheatsheet.scss'
|
||||||
|
import { CheatsheetLine } from './cheatsheet-line'
|
||||||
|
|
||||||
export const Cheatsheet: React.FC = () => {
|
export const Cheatsheet: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const codes = [
|
const [checked, setChecked] = useState<boolean>(false)
|
||||||
|
const codes = useMemo(() => [
|
||||||
`**${ t('editor.editorToolbar.bold') }**`,
|
`**${ t('editor.editorToolbar.bold') }**`,
|
||||||
`*${ t('editor.editorToolbar.italic') }*`,
|
`*${ t('editor.editorToolbar.italic') }*`,
|
||||||
`++${ t('editor.editorToolbar.underline') }++`,
|
`++${ t('editor.editorToolbar.underline') }++`,
|
||||||
|
@ -28,17 +27,12 @@ export const Cheatsheet: React.FC = () => {
|
||||||
`> ${ t('editor.editorToolbar.blockquote') }`,
|
`> ${ t('editor.editorToolbar.blockquote') }`,
|
||||||
`- ${ t('editor.editorToolbar.unorderedList') }`,
|
`- ${ t('editor.editorToolbar.unorderedList') }`,
|
||||||
`1. ${ t('editor.editorToolbar.orderedList') }`,
|
`1. ${ t('editor.editorToolbar.orderedList') }`,
|
||||||
`- [ ] ${ t('editor.editorToolbar.checkList') }`,
|
`- [${ checked ? 'x' : ' ' }] ${ t('editor.editorToolbar.checkList') }`,
|
||||||
`[${ t('editor.editorToolbar.link') }](https://example.com)`,
|
`[${ t('editor.editorToolbar.link') }](https://example.com)`,
|
||||||
``,
|
``,
|
||||||
':smile:',
|
':smile:',
|
||||||
`:::info\n${ t('editor.help.cheatsheet.exampleAlert') }\n:::`
|
`:::info\n${ t('editor.help.cheatsheet.exampleAlert') }\n:::`
|
||||||
]
|
], [checked, t])
|
||||||
|
|
||||||
const markdownIt = useMemo(() => {
|
|
||||||
return new BasicMarkdownItConfigurator()
|
|
||||||
.buildConfiguredMarkdownIt()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className="table-condensed table-cheatsheet">
|
<Table className="table-condensed table-cheatsheet">
|
||||||
|
@ -49,21 +43,13 @@ export const Cheatsheet: React.FC = () => {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{ codes.map((code, key) => {
|
{
|
||||||
return (
|
codes.map((code) =>
|
||||||
<tr key={ key }>
|
<CheatsheetLine code={ code } key={ code } onTaskCheckedChange={ setChecked }/>)
|
||||||
<td>
|
}
|
||||||
<BasicMarkdownRenderer
|
|
||||||
content={ code }
|
|
||||||
markdownIt={ markdownIt }/>
|
|
||||||
</td>
|
|
||||||
<td className={ 'markdown-body' }>
|
|
||||||
<HighlightedCode code={ code } wrapLines={ true } startLineNumber={ 1 } language={ 'markdown' }/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)
|
|
||||||
}) }
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</Table>
|
</Table>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default Cheatsheet
|
||||||
|
|
|
@ -4,35 +4,16 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Fragment, useState } from 'react'
|
import React, { Fragment, useCallback, useState } from 'react'
|
||||||
import { Button, Modal } from 'react-bootstrap'
|
import { Button } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||||
import { Cheatsheet } from './cheatsheet'
|
import { HelpModal } from './help-modal'
|
||||||
import { Links } from './links'
|
|
||||||
import { Shortcut } from './shortcuts'
|
|
||||||
|
|
||||||
export enum HelpTabStatus {
|
|
||||||
Cheatsheet = 'cheatsheet.title',
|
|
||||||
Shortcuts = 'shortcuts.title',
|
|
||||||
Links = 'links.title'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const HelpButton: React.FC = () => {
|
export const HelpButton: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [show, setShow] = useState(false)
|
const [show, setShow] = useState(false)
|
||||||
const [tab, setTab] = useState<HelpTabStatus>(HelpTabStatus.Cheatsheet)
|
const onHide = useCallback(() => setShow(false), [])
|
||||||
|
|
||||||
const tabContent = (): React.ReactElement => {
|
|
||||||
switch (tab) {
|
|
||||||
case HelpTabStatus.Cheatsheet:
|
|
||||||
return (<Cheatsheet/>)
|
|
||||||
case HelpTabStatus.Shortcuts:
|
|
||||||
return (<Shortcut/>)
|
|
||||||
case HelpTabStatus.Links:
|
|
||||||
return (<Links/>)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
|
@ -40,37 +21,7 @@ export const HelpButton: React.FC = () => {
|
||||||
onClick={ () => setShow(true) }>
|
onClick={ () => setShow(true) }>
|
||||||
<ForkAwesomeIcon icon="question-circle"/>
|
<ForkAwesomeIcon icon="question-circle"/>
|
||||||
</Button>
|
</Button>
|
||||||
<Modal show={ show } onHide={ () => setShow(false) } animation={ true } className='text-dark' size='lg'>
|
<HelpModal show={ show } onHide={ onHide }/>
|
||||||
<Modal.Header closeButton>
|
|
||||||
<Modal.Title>
|
|
||||||
<ForkAwesomeIcon icon='question-circle'/> <Trans i18nKey={ 'editor.documentBar.help' }/> – <Trans
|
|
||||||
i18nKey={ `editor.help.${ tab }` }/>
|
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
|
||||||
<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' : '' }` }
|
|
||||||
onClick={ () => setTab(HelpTabStatus.Shortcuts) }
|
|
||||||
>
|
|
||||||
<Trans i18nKey={ 'editor.help.shortcuts.title' }/>
|
|
||||||
</Button>
|
|
||||||
<Button variant={ 'light' }
|
|
||||||
className={ `nav-link nav-item ${ tab === HelpTabStatus.Links ? 'active' : '' }` }
|
|
||||||
onClick={ () => setTab(HelpTabStatus.Links) }
|
|
||||||
>
|
|
||||||
<Trans i18nKey={ 'editor.help.links.title' }/>
|
|
||||||
</Button>
|
|
||||||
</nav>
|
|
||||||
{ tabContent() }
|
|
||||||
</Modal.Body>
|
|
||||||
</Modal>
|
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Button, Modal } from 'react-bootstrap'
|
||||||
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
|
import React, { useMemo, useState } from 'react'
|
||||||
|
import { CommonModal } from '../../../common/modals/common-modal'
|
||||||
|
import { Shortcut } from './shortcuts'
|
||||||
|
import { Links } from './links'
|
||||||
|
import { Cheatsheet } from './cheatsheet'
|
||||||
|
|
||||||
|
export enum HelpTabStatus {
|
||||||
|
Cheatsheet = 'cheatsheet.title',
|
||||||
|
Shortcuts = 'shortcuts.title',
|
||||||
|
Links = 'links.title'
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HelpModalProps {
|
||||||
|
show: boolean,
|
||||||
|
onHide: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HelpModal: React.FC<HelpModalProps> = ({ show, onHide }) => {
|
||||||
|
const [tab, setTab] = useState<HelpTabStatus>(HelpTabStatus.Cheatsheet)
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
const tabContent = useMemo(() => {
|
||||||
|
switch (tab) {
|
||||||
|
case HelpTabStatus.Cheatsheet:
|
||||||
|
return (<Cheatsheet/>)
|
||||||
|
case HelpTabStatus.Shortcuts:
|
||||||
|
return (<Shortcut/>)
|
||||||
|
case HelpTabStatus.Links:
|
||||||
|
return (<Links/>)
|
||||||
|
}
|
||||||
|
}, [tab])
|
||||||
|
|
||||||
|
const tabTitle = useMemo(() => t('editor.documentBar.help') + ' - ' + t(`editor.help.${ tab }`), [t, tab])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CommonModal icon={ 'question-circle' } show={ show } onHide={ onHide } title={ tabTitle }>
|
||||||
|
<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' : '' }` }
|
||||||
|
onClick={ () => setTab(HelpTabStatus.Shortcuts) }>
|
||||||
|
<Trans i18nKey={ 'editor.help.shortcuts.title' }/>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={ 'light' }
|
||||||
|
className={ `nav-link nav-item ${ tab === HelpTabStatus.Links ? 'active' : '' }` }
|
||||||
|
onClick={ () => setTab(HelpTabStatus.Links) }>
|
||||||
|
<Trans i18nKey={ 'editor.help.links.title' }/>
|
||||||
|
</Button>
|
||||||
|
</nav>
|
||||||
|
{ tabContent }
|
||||||
|
</Modal.Body>
|
||||||
|
</CommonModal>)
|
||||||
|
}
|
|
@ -16,6 +16,7 @@ import { CopyableField } from '../../../common/copyable/copyable-field/copyable-
|
||||||
import { CommonModal } from '../../../common/modals/common-modal'
|
import { CommonModal } from '../../../common/modals/common-modal'
|
||||||
import { ShowIf } from '../../../common/show-if/show-if'
|
import { ShowIf } from '../../../common/show-if/show-if'
|
||||||
import { EditorPagePathParams } from '../../editor-page'
|
import { EditorPagePathParams } from '../../editor-page'
|
||||||
|
import { NoteType } from '../../note-frontmatter/note-frontmatter'
|
||||||
|
|
||||||
export interface ShareModalProps {
|
export interface ShareModalProps {
|
||||||
show: boolean,
|
show: boolean,
|
||||||
|
@ -39,7 +40,7 @@ export const ShareModal: React.FC<ShareModalProps> = ({ show, onHide }) => {
|
||||||
<Trans i18nKey={ 'editor.modal.shareLink.editorDescription' }/>
|
<Trans i18nKey={ 'editor.modal.shareLink.editorDescription' }/>
|
||||||
<CopyableField content={ `${ baseUrl }/n/${ id }?${ editorMode }` } nativeShareButton={ true }
|
<CopyableField content={ `${ baseUrl }/n/${ id }?${ editorMode }` } nativeShareButton={ true }
|
||||||
url={ `${ baseUrl }/n/${ id }?${ editorMode }` }/>
|
url={ `${ baseUrl }/n/${ id }?${ editorMode }` }/>
|
||||||
<ShowIf condition={ noteFrontmatter.type === 'slide' }>
|
<ShowIf condition={ noteFrontmatter.type === NoteType.SLIDE }>
|
||||||
<Trans i18nKey={ 'editor.modal.shareLink.slidesDescription' }/>
|
<Trans i18nKey={ 'editor.modal.shareLink.slidesDescription' }/>
|
||||||
<CopyableField content={ `${ baseUrl }/p/${ id }` } nativeShareButton={ true }
|
<CopyableField content={ `${ baseUrl }/p/${ id }` } nativeShareButton={ true }
|
||||||
url={ `${ baseUrl }/p/${ id }` }/>
|
url={ `${ baseUrl }/p/${ id }` }/>
|
||||||
|
|
|
@ -9,9 +9,9 @@ import MarkdownIt from 'markdown-it'
|
||||||
import frontmatter from 'markdown-it-front-matter'
|
import frontmatter from 'markdown-it-front-matter'
|
||||||
import { NoteFrontmatter, RawNoteFrontmatter } from './note-frontmatter'
|
import { NoteFrontmatter, RawNoteFrontmatter } from './note-frontmatter'
|
||||||
|
|
||||||
describe('yaml frontmatter tests', () => {
|
describe('yaml frontmatter', () => {
|
||||||
let raw: RawNoteFrontmatter | undefined
|
const testFrontmatter = (input: string): NoteFrontmatter => {
|
||||||
let finished: NoteFrontmatter | undefined
|
let processedFrontmatter: NoteFrontmatter | undefined = undefined
|
||||||
const md = new MarkdownIt('default', {
|
const md = new MarkdownIt('default', {
|
||||||
html: true,
|
html: true,
|
||||||
breaks: true,
|
breaks: true,
|
||||||
|
@ -19,117 +19,91 @@ describe('yaml frontmatter tests', () => {
|
||||||
typographer: true
|
typographer: true
|
||||||
})
|
})
|
||||||
md.use(frontmatter, (rawMeta: string) => {
|
md.use(frontmatter, (rawMeta: string) => {
|
||||||
raw = yaml.load(rawMeta) as RawNoteFrontmatter
|
const parsedFrontmatter = yaml.load(rawMeta) as RawNoteFrontmatter | undefined
|
||||||
finished = new NoteFrontmatter(raw)
|
expect(parsedFrontmatter)
|
||||||
|
.not
|
||||||
|
.toBe(undefined)
|
||||||
|
if (parsedFrontmatter === undefined) {
|
||||||
|
fail('Parsed frontmatter is undefined')
|
||||||
|
}
|
||||||
|
processedFrontmatter = new NoteFrontmatter(parsedFrontmatter)
|
||||||
})
|
})
|
||||||
|
|
||||||
// generate default YAMLMetadata
|
|
||||||
md.render('---\n---')
|
|
||||||
const defaultYAML = finished
|
|
||||||
|
|
||||||
const testFrontmatter = (input: string, expectedRaw: Partial<RawNoteFrontmatter>, expectedFinished: Partial<NoteFrontmatter>) => {
|
|
||||||
md.render(input)
|
md.render(input)
|
||||||
expect(raw)
|
|
||||||
.not
|
if (processedFrontmatter === undefined) {
|
||||||
.toBe(undefined)
|
fail('NoteFrontmatter is undefined')
|
||||||
expect(raw)
|
|
||||||
.toEqual(expectedRaw)
|
|
||||||
expect(finished)
|
|
||||||
.not
|
|
||||||
.toBe(undefined)
|
|
||||||
expect(finished)
|
|
||||||
.toEqual({
|
|
||||||
...defaultYAML,
|
|
||||||
...expectedFinished
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
return processedFrontmatter
|
||||||
raw = undefined
|
}
|
||||||
finished = undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
it('title only', () => {
|
it('should parse "title"', () => {
|
||||||
testFrontmatter(`---
|
const noteFrontmatter = testFrontmatter(`---
|
||||||
title: test
|
title: test
|
||||||
___
|
___
|
||||||
`,
|
`)
|
||||||
{
|
|
||||||
title: 'test'
|
expect(noteFrontmatter.title)
|
||||||
},
|
.toEqual('test')
|
||||||
{
|
|
||||||
title: 'test'
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('robots only', () => {
|
it('should parse "robots"', () => {
|
||||||
testFrontmatter(`---
|
const noteFrontmatter = testFrontmatter(`---
|
||||||
robots: index, follow
|
robots: index, follow
|
||||||
___
|
___
|
||||||
`,
|
`)
|
||||||
{
|
|
||||||
robots: 'index, follow'
|
expect(noteFrontmatter.robots)
|
||||||
},
|
.toEqual('index, follow')
|
||||||
{
|
|
||||||
robots: 'index, follow'
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tags only (old syntax)', () => {
|
it('should parse the deprecated tags syntax', () => {
|
||||||
testFrontmatter(`---
|
const noteFrontmatter = testFrontmatter(`---
|
||||||
tags: test123, abc
|
tags: test123, abc
|
||||||
___
|
___
|
||||||
`,
|
`)
|
||||||
{
|
|
||||||
tags: 'test123, abc'
|
expect(noteFrontmatter.tags)
|
||||||
},
|
.toEqual(['test123', 'abc'])
|
||||||
{
|
expect(noteFrontmatter.deprecatedTagsSyntax)
|
||||||
tags: ['test123', 'abc'],
|
.toEqual(true)
|
||||||
deprecatedTagsSyntax: true
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tags only', () => {
|
it('should parse the tags list syntax', () => {
|
||||||
testFrontmatter(`---
|
const noteFrontmatter = testFrontmatter(`---
|
||||||
tags:
|
tags:
|
||||||
- test123
|
- test123
|
||||||
- abc
|
- abc
|
||||||
___
|
___
|
||||||
`,
|
`)
|
||||||
{
|
|
||||||
tags: ['test123', 'abc']
|
expect(noteFrontmatter.tags)
|
||||||
},
|
.toEqual(['test123', 'abc'])
|
||||||
{
|
expect(noteFrontmatter.deprecatedTagsSyntax)
|
||||||
tags: ['test123', 'abc'],
|
.toEqual(false)
|
||||||
deprecatedTagsSyntax: false
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('tags only (alternative syntax)', () => {
|
it('should parse the tag inline-list syntax', () => {
|
||||||
testFrontmatter(`---
|
const noteFrontmatter = testFrontmatter(`---
|
||||||
tags: ['test123', 'abc']
|
tags: ['test123', 'abc']
|
||||||
___
|
___
|
||||||
`,
|
`)
|
||||||
{
|
|
||||||
tags: ['test123', 'abc']
|
expect(noteFrontmatter.tags)
|
||||||
},
|
.toEqual(['test123', 'abc'])
|
||||||
{
|
expect(noteFrontmatter.deprecatedTagsSyntax)
|
||||||
tags: ['test123', 'abc'],
|
.toEqual(false)
|
||||||
deprecatedTagsSyntax: false
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('breaks only', () => {
|
it('should parse "breaks"', () => {
|
||||||
testFrontmatter(`---
|
const noteFrontmatter = testFrontmatter(`---
|
||||||
breaks: false
|
breaks: false
|
||||||
___
|
___
|
||||||
`,
|
`)
|
||||||
{
|
|
||||||
breaks: false
|
expect(noteFrontmatter.breaks)
|
||||||
},
|
.toEqual(false)
|
||||||
{
|
|
||||||
breaks: false
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -191,56 +165,41 @@ describe('yaml frontmatter tests', () => {
|
||||||
})
|
})
|
||||||
*/
|
*/
|
||||||
|
|
||||||
it('opengraph nothing', () => {
|
it('should parse an empty opengraph object', () => {
|
||||||
testFrontmatter(`---
|
const noteFrontmatter = testFrontmatter(`---
|
||||||
opengraph:
|
opengraph:
|
||||||
___
|
___
|
||||||
`,
|
`)
|
||||||
{
|
|
||||||
opengraph: null
|
expect(noteFrontmatter.opengraph)
|
||||||
},
|
.toEqual(new Map<string, string>())
|
||||||
{
|
|
||||||
opengraph: new Map<string, string>()
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opengraph title only', () => {
|
it('should parse an opengraph title', () => {
|
||||||
testFrontmatter(`---
|
const noteFrontmatter = testFrontmatter(`---
|
||||||
opengraph:
|
opengraph:
|
||||||
title: Testtitle
|
title: Testtitle
|
||||||
___
|
___
|
||||||
`,
|
`)
|
||||||
{
|
|
||||||
opengraph: {
|
expect(noteFrontmatter.opengraph.get('title'))
|
||||||
title: 'Testtitle'
|
.toEqual('Testtitle')
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
opengraph: new Map<string, string>(Object.entries({ title: 'Testtitle' }))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opengraph more attributes', () => {
|
it('should opengraph values', () => {
|
||||||
testFrontmatter(`---
|
const noteFrontmatter = testFrontmatter(`---
|
||||||
opengraph:
|
opengraph:
|
||||||
title: Testtitle
|
title: Testtitle
|
||||||
image: https://dummyimage.com/48.png
|
image: https://dummyimage.com/48.png
|
||||||
image:type: image/png
|
image:type: image/png
|
||||||
___
|
___
|
||||||
`,
|
`)
|
||||||
{
|
|
||||||
opengraph: {
|
expect(noteFrontmatter.opengraph.get('title'))
|
||||||
title: 'Testtitle',
|
.toEqual('Testtitle')
|
||||||
image: 'https://dummyimage.com/48.png',
|
expect(noteFrontmatter.opengraph.get('image'))
|
||||||
'image:type': 'image/png'
|
.toEqual('https://dummyimage.com/48.png')
|
||||||
}
|
expect(noteFrontmatter.opengraph.get('image:type'))
|
||||||
},
|
.toEqual('image/png')
|
||||||
{
|
|
||||||
opengraph: new Map<string, string>(Object.entries({
|
|
||||||
title: 'Testtitle',
|
|
||||||
image: 'https://dummyimage.com/48.png',
|
|
||||||
'image:type': 'image/png'
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -6,210 +6,6 @@
|
||||||
|
|
||||||
// import { RevealOptions } from 'reveal.js'
|
// import { RevealOptions } from 'reveal.js'
|
||||||
|
|
||||||
type iso6391 =
|
|
||||||
'aa'
|
|
||||||
| 'ab'
|
|
||||||
| 'af'
|
|
||||||
| 'am'
|
|
||||||
| 'ar'
|
|
||||||
| 'ar-ae'
|
|
||||||
| 'ar-bh'
|
|
||||||
| 'ar-dz'
|
|
||||||
| 'ar-eg'
|
|
||||||
| 'ar-iq'
|
|
||||||
| 'ar-jo'
|
|
||||||
| 'ar-kw'
|
|
||||||
| 'ar-lb'
|
|
||||||
| 'ar-ly'
|
|
||||||
| 'ar-ma'
|
|
||||||
| 'ar-om'
|
|
||||||
| 'ar-qa'
|
|
||||||
| 'ar-sa'
|
|
||||||
| 'ar-sy'
|
|
||||||
| 'ar-tn'
|
|
||||||
| 'ar-ye'
|
|
||||||
| 'as'
|
|
||||||
| 'ay'
|
|
||||||
| 'de-at'
|
|
||||||
| 'de-ch'
|
|
||||||
| 'de-li'
|
|
||||||
| 'de-lu'
|
|
||||||
| 'div'
|
|
||||||
| 'dz'
|
|
||||||
| 'el'
|
|
||||||
| 'en'
|
|
||||||
| 'en-au'
|
|
||||||
| 'en-bz'
|
|
||||||
| 'en-ca'
|
|
||||||
| 'en-gb'
|
|
||||||
| 'en-ie'
|
|
||||||
| 'en-jm'
|
|
||||||
| 'en-nz'
|
|
||||||
| 'en-ph'
|
|
||||||
| 'en-tt'
|
|
||||||
| 'en-us'
|
|
||||||
| 'en-za'
|
|
||||||
| 'en-zw'
|
|
||||||
| 'eo'
|
|
||||||
| 'es'
|
|
||||||
| 'es-ar'
|
|
||||||
| 'es-bo'
|
|
||||||
| 'es-cl'
|
|
||||||
| 'es-co'
|
|
||||||
| 'es-cr'
|
|
||||||
| 'es-do'
|
|
||||||
| 'es-ec'
|
|
||||||
| 'es-es'
|
|
||||||
| 'es-gt'
|
|
||||||
| 'es-hn'
|
|
||||||
| 'es-mx'
|
|
||||||
| 'es-ni'
|
|
||||||
| 'es-pa'
|
|
||||||
| 'es-pe'
|
|
||||||
| 'es-pr'
|
|
||||||
| 'es-py'
|
|
||||||
| 'es-sv'
|
|
||||||
| 'es-us'
|
|
||||||
| 'es-uy'
|
|
||||||
| 'es-ve'
|
|
||||||
| 'et'
|
|
||||||
| 'eu'
|
|
||||||
| 'fa'
|
|
||||||
| 'fi'
|
|
||||||
| 'fj'
|
|
||||||
| 'fo'
|
|
||||||
| 'fr'
|
|
||||||
| 'fr-be'
|
|
||||||
| 'fr-ca'
|
|
||||||
| 'fr-ch'
|
|
||||||
| 'fr-lu'
|
|
||||||
| 'fr-mc'
|
|
||||||
| 'fy'
|
|
||||||
| 'ga'
|
|
||||||
| 'gd'
|
|
||||||
| 'gl'
|
|
||||||
| 'gn'
|
|
||||||
| 'gu'
|
|
||||||
| 'ha'
|
|
||||||
| 'he'
|
|
||||||
| 'hi'
|
|
||||||
| 'hr'
|
|
||||||
| 'hu'
|
|
||||||
| 'hy'
|
|
||||||
| 'ia'
|
|
||||||
| 'id'
|
|
||||||
| 'ie'
|
|
||||||
| 'ik'
|
|
||||||
| 'in'
|
|
||||||
| 'is'
|
|
||||||
| 'it'
|
|
||||||
| 'it-ch'
|
|
||||||
| 'iw'
|
|
||||||
| 'ja'
|
|
||||||
| 'ji'
|
|
||||||
| 'jw'
|
|
||||||
| 'ka'
|
|
||||||
| 'kk'
|
|
||||||
| 'kl'
|
|
||||||
| 'km'
|
|
||||||
| 'kn'
|
|
||||||
| 'ko'
|
|
||||||
| 'kok'
|
|
||||||
| 'ks'
|
|
||||||
| 'ku'
|
|
||||||
| 'ky'
|
|
||||||
| 'kz'
|
|
||||||
| 'la'
|
|
||||||
| 'ln'
|
|
||||||
| 'lo'
|
|
||||||
| 'ls'
|
|
||||||
| 'lt'
|
|
||||||
| 'lv'
|
|
||||||
| 'mg'
|
|
||||||
| 'mi'
|
|
||||||
| 'mk'
|
|
||||||
| 'ml'
|
|
||||||
| 'mn'
|
|
||||||
| 'mo'
|
|
||||||
| 'mr'
|
|
||||||
| 'ms'
|
|
||||||
| 'mt'
|
|
||||||
| 'my'
|
|
||||||
| 'na'
|
|
||||||
| 'nb-no'
|
|
||||||
| 'ne'
|
|
||||||
| 'nl'
|
|
||||||
| 'nl-be'
|
|
||||||
| 'nn-no'
|
|
||||||
| 'no'
|
|
||||||
| 'oc'
|
|
||||||
| 'om'
|
|
||||||
| 'or'
|
|
||||||
| 'pa'
|
|
||||||
| 'pl'
|
|
||||||
| 'ps'
|
|
||||||
| 'pt'
|
|
||||||
| 'pt-br'
|
|
||||||
| 'qu'
|
|
||||||
| 'rm'
|
|
||||||
| 'rn'
|
|
||||||
| 'ro'
|
|
||||||
| 'ro-md'
|
|
||||||
| 'ru'
|
|
||||||
| 'ru-md'
|
|
||||||
| 'rw'
|
|
||||||
| 'sa'
|
|
||||||
| 'sb'
|
|
||||||
| 'sd'
|
|
||||||
| 'sg'
|
|
||||||
| 'sh'
|
|
||||||
| 'si'
|
|
||||||
| 'sk'
|
|
||||||
| 'sl'
|
|
||||||
| 'sm'
|
|
||||||
| 'sn'
|
|
||||||
| 'so'
|
|
||||||
| 'sq'
|
|
||||||
| 'sr'
|
|
||||||
| 'ss'
|
|
||||||
| 'st'
|
|
||||||
| 'su'
|
|
||||||
| 'sv'
|
|
||||||
| 'sv-fi'
|
|
||||||
| 'sw'
|
|
||||||
| 'sx'
|
|
||||||
| 'syr'
|
|
||||||
| 'ta'
|
|
||||||
| 'te'
|
|
||||||
| 'tg'
|
|
||||||
| 'th'
|
|
||||||
| 'ti'
|
|
||||||
| 'tk'
|
|
||||||
| 'tl'
|
|
||||||
| 'tn'
|
|
||||||
| 'to'
|
|
||||||
| 'tr'
|
|
||||||
| 'ts'
|
|
||||||
| 'tt'
|
|
||||||
| 'tw'
|
|
||||||
| 'uk'
|
|
||||||
| 'ur'
|
|
||||||
| 'us'
|
|
||||||
| 'uz'
|
|
||||||
| 'vi'
|
|
||||||
| 'vo'
|
|
||||||
| 'wo'
|
|
||||||
| 'xh'
|
|
||||||
| 'yi'
|
|
||||||
| 'yo'
|
|
||||||
| 'zh'
|
|
||||||
| 'zh-cn'
|
|
||||||
| 'zh-hk'
|
|
||||||
| 'zh-mo'
|
|
||||||
| 'zh-sg'
|
|
||||||
| 'zh-tw'
|
|
||||||
| 'zu'
|
|
||||||
|
|
||||||
export interface RawNoteFrontmatter {
|
export interface RawNoteFrontmatter {
|
||||||
title: string | undefined
|
title: string | undefined
|
||||||
description: string | undefined
|
description: string | undefined
|
||||||
|
@ -225,32 +21,57 @@ export interface RawNoteFrontmatter {
|
||||||
opengraph: { [key: string]: string } | null
|
opengraph: { [key: string]: string } | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ISO6391 = ['aa', 'ab', 'af', 'am', 'ar', 'ar-ae', 'ar-bh', 'ar-dz', 'ar-eg', 'ar-iq', 'ar-jo', 'ar-kw',
|
||||||
|
'ar-lb', 'ar-ly', 'ar-ma', 'ar-om', 'ar-qa', 'ar-sa', 'ar-sy', 'ar-tn', 'ar-ye', 'as', 'ay', 'de-at', 'de-ch',
|
||||||
|
'de-li', 'de-lu', 'div', 'dz', 'el', 'en', 'en-au', 'en-bz', 'en-ca', 'en-gb', 'en-ie', 'en-jm', 'en-nz', 'en-ph',
|
||||||
|
'en-tt', 'en-us', 'en-za', 'en-zw', 'eo', 'es', 'es-ar', 'es-bo', 'es-cl', 'es-co', 'es-cr', 'es-do', 'es-ec',
|
||||||
|
'es-es', 'es-gt', 'es-hn', 'es-mx', 'es-ni', 'es-pa', 'es-pe', 'es-pr', 'es-py', 'es-sv', 'es-us', 'es-uy', 'es-ve',
|
||||||
|
'et', 'eu', 'fa', 'fi', 'fj', 'fo', 'fr', 'fr-be', 'fr-ca', 'fr-ch', 'fr-lu', 'fr-mc', 'fy', 'ga', 'gd', 'gl', 'gn',
|
||||||
|
'gu', 'ha', 'he', 'hi', 'hr', 'hu', 'hy', 'ia', 'id', 'ie', 'ik', 'in', 'is', 'it', 'it-ch', 'iw', 'ja', 'ji', 'jw',
|
||||||
|
'ka', 'kk', 'kl', 'km', 'kn', 'ko', 'kok', 'ks', 'ku', 'ky', 'kz', 'la', 'ln', 'lo', 'ls', 'lt', 'lv', 'mg', 'mi',
|
||||||
|
'mk', 'ml', 'mn', 'mo', 'mr', 'ms', 'mt', 'my', 'na', 'nb-no', 'ne', 'nl', 'nl-be', 'nn-no', 'no', 'oc', 'om', 'or',
|
||||||
|
'pa', 'pl', 'ps', 'pt', 'pt-br', 'qu', 'rm', 'rn', 'ro', 'ro-md', 'ru', 'ru-md', 'rw', 'sa', 'sb', 'sd', 'sg', 'sh',
|
||||||
|
'si', 'sk', 'sl', 'sm', 'sn', 'so', 'sq', 'sr', 'ss', 'st', 'su', 'sv', 'sv-fi', 'sw', 'sx', 'syr', 'ta', 'te', 'tg',
|
||||||
|
'th', 'ti', 'tk', 'tl', 'tn', 'to', 'tr', 'ts', 'tt', 'tw', 'uk', 'ur', 'us', 'uz', 'vi', 'vo', 'wo', 'xh', 'yi',
|
||||||
|
'yo', 'zh', 'zh-cn', 'zh-hk', 'zh-mo', 'zh-sg', 'zh-tw', 'zu'] as const
|
||||||
|
|
||||||
|
export enum NoteType {
|
||||||
|
DOCUMENT = '',
|
||||||
|
SLIDE = 'slide'
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum NoteTextDirection {
|
||||||
|
LTR = 'ltr',
|
||||||
|
RTL = 'rtl'
|
||||||
|
}
|
||||||
|
|
||||||
export class NoteFrontmatter {
|
export class NoteFrontmatter {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
tags: string[]
|
tags: string[]
|
||||||
deprecatedTagsSyntax: boolean
|
deprecatedTagsSyntax: boolean
|
||||||
robots: string
|
robots: string
|
||||||
lang: iso6391
|
lang: typeof ISO6391[number]
|
||||||
dir: 'ltr' | 'rtl'
|
dir: NoteTextDirection
|
||||||
breaks: boolean
|
breaks: boolean
|
||||||
GA: string
|
GA: string
|
||||||
disqus: string
|
disqus: string
|
||||||
type: 'slide' | ''
|
type: NoteType
|
||||||
// slideOptions: RevealOptions
|
// slideOptions: RevealOptions
|
||||||
opengraph: Map<string, string>
|
opengraph: Map<string, string>
|
||||||
|
|
||||||
constructor(rawData: RawNoteFrontmatter) {
|
constructor(rawData: RawNoteFrontmatter) {
|
||||||
this.title = rawData?.title ?? ''
|
this.title = rawData.title ?? ''
|
||||||
this.description = rawData?.description ?? ''
|
this.description = rawData.description ?? ''
|
||||||
this.robots = rawData?.robots ?? ''
|
this.robots = rawData.robots ?? ''
|
||||||
this.breaks = rawData?.breaks ?? true
|
this.breaks = rawData.breaks ?? true
|
||||||
this.GA = rawData?.GA ?? ''
|
this.GA = rawData.GA ?? ''
|
||||||
this.disqus = rawData?.disqus ?? ''
|
this.disqus = rawData.disqus ?? ''
|
||||||
|
this.lang = (rawData.lang ? ISO6391.find(lang => lang === rawData.lang) : undefined) ?? 'en'
|
||||||
this.type = (rawData?.type as NoteFrontmatter['type']) ?? ''
|
this.type = (rawData.type ? Object.values(NoteType)
|
||||||
this.lang = (rawData?.lang as iso6391) ?? 'en'
|
.find(type => type === rawData.type) : undefined) ?? NoteType.DOCUMENT
|
||||||
this.dir = (rawData?.dir as NoteFrontmatter['dir']) ?? 'ltr'
|
this.dir = (rawData.dir ? Object.values(NoteTextDirection)
|
||||||
|
.find(dir => dir === rawData.dir) : undefined) ?? NoteTextDirection.LTR
|
||||||
|
|
||||||
/* this.slideOptions = (rawData?.slideOptions as RevealOptions) ?? {
|
/* this.slideOptions = (rawData?.slideOptions as RevealOptions) ?? {
|
||||||
transition: 'none',
|
transition: 'none',
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TocAst } from 'markdown-it-toc-done-right'
|
||||||
|
import React, { Fragment, ReactElement } from 'react'
|
||||||
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
|
import { createJumpToMarkClickEventHandler } from '../../markdown-renderer/replace-components/link-replacer/link-replacer'
|
||||||
|
import { tocSlugify } from './toc-slugify'
|
||||||
|
|
||||||
|
export const buildReactDomFromTocAst = (toc: TocAst, levelsToShowUnderThis: number, headerCounts: Map<string, number>,
|
||||||
|
wrapInListItem: boolean, baseUrl?: string): ReactElement | null => {
|
||||||
|
if (levelsToShowUnderThis < 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawName = toc.n.trim()
|
||||||
|
const nameCount = (headerCounts.get(rawName) ?? -1) + 1
|
||||||
|
const slug = `#${ tocSlugify(rawName) }${ nameCount > 0 ? `-${ nameCount }` : '' }`
|
||||||
|
const headlineUrl = new URL(slug, baseUrl).toString()
|
||||||
|
|
||||||
|
headerCounts.set(rawName, nameCount)
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<Fragment>
|
||||||
|
<ShowIf condition={ toc.l > 0 }>
|
||||||
|
<a href={ headlineUrl } title={ rawName }
|
||||||
|
onClick={ createJumpToMarkClickEventHandler(slug.substr(1)) }>{ rawName }</a>
|
||||||
|
</ShowIf>
|
||||||
|
<ShowIf condition={ toc.c.length > 0 }>
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
toc.c.map(child =>
|
||||||
|
(buildReactDomFromTocAst(child, levelsToShowUnderThis - 1, headerCounts, true, baseUrl)))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</ShowIf>
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (wrapInListItem) {
|
||||||
|
return (
|
||||||
|
<li key={ headlineUrl }>
|
||||||
|
{ content }
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,10 +5,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { TocAst } from 'markdown-it-toc-done-right'
|
import { TocAst } from 'markdown-it-toc-done-right'
|
||||||
import React, { Fragment, ReactElement, useMemo } from 'react'
|
import React, { useMemo } from 'react'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { ShowIf } from '../../common/show-if/show-if'
|
import { ShowIf } from '../../common/show-if/show-if'
|
||||||
import { createJumpToMarkClickEventHandler } from '../../markdown-renderer/replace-components/link-replacer/link-replacer'
|
import { buildReactDomFromTocAst } from './build-react-dom-from-toc-ast'
|
||||||
import './table-of-contents.scss'
|
import './table-of-contents.scss'
|
||||||
|
|
||||||
export interface TableOfContentsProps {
|
export interface TableOfContentsProps {
|
||||||
|
@ -18,53 +18,6 @@ export interface TableOfContentsProps {
|
||||||
baseUrl?: string
|
baseUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const slugify = (content: string): string => {
|
|
||||||
return encodeURIComponent(content.trim()
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/\s+/g, '-'))
|
|
||||||
}
|
|
||||||
|
|
||||||
const convertLevel = (toc: TocAst, levelsToShowUnderThis: number, headerCounts: Map<string, number>,
|
|
||||||
wrapInListItem: boolean, baseUrl?: string): ReactElement | null => {
|
|
||||||
if (levelsToShowUnderThis < 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const rawName = toc.n.trim()
|
|
||||||
const nameCount = (headerCounts.get(rawName) ?? -1) + 1
|
|
||||||
const slug = `#${ slugify(rawName) }${ nameCount > 0 ? `-${ nameCount }` : '' }`
|
|
||||||
const headlineUrl = new URL(slug, baseUrl).toString()
|
|
||||||
|
|
||||||
headerCounts.set(rawName, nameCount)
|
|
||||||
|
|
||||||
const content = (
|
|
||||||
<Fragment>
|
|
||||||
<ShowIf condition={ toc.l > 0 }>
|
|
||||||
<a href={ headlineUrl } title={ rawName }
|
|
||||||
onClick={ createJumpToMarkClickEventHandler(slug.substr(1)) }>{ rawName }</a>
|
|
||||||
</ShowIf>
|
|
||||||
<ShowIf condition={ toc.c.length > 0 }>
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
toc.c.map(child =>
|
|
||||||
(convertLevel(child, levelsToShowUnderThis - 1, headerCounts, true, baseUrl)))
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
</ShowIf>
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (wrapInListItem) {
|
|
||||||
return (
|
|
||||||
<li key={ headlineUrl }>
|
|
||||||
{ content }
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TableOfContents: React.FC<TableOfContentsProps> = ({
|
export const TableOfContents: React.FC<TableOfContentsProps> = ({
|
||||||
ast,
|
ast,
|
||||||
maxDepth = 3,
|
maxDepth = 3,
|
||||||
|
@ -72,7 +25,8 @@ export const TableOfContents: React.FC<TableOfContentsProps> = ({
|
||||||
baseUrl
|
baseUrl
|
||||||
}) => {
|
}) => {
|
||||||
useTranslation()
|
useTranslation()
|
||||||
const tocTree = useMemo(() => convertLevel(ast, maxDepth, new Map<string, number>(), false, baseUrl), [ast, maxDepth,
|
const tocTree = useMemo(() => buildReactDomFromTocAst(ast, maxDepth, new Map<string, number>(), false, baseUrl), [ast,
|
||||||
|
maxDepth,
|
||||||
baseUrl])
|
baseUrl])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
11
src/components/editor-page/table-of-contents/toc-slugify.ts
Normal file
11
src/components/editor-page/table-of-contents/toc-slugify.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const tocSlugify = (content: string): string => {
|
||||||
|
return encodeURIComponent(content.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, '-'))
|
||||||
|
}
|
|
@ -17,8 +17,8 @@ import { CoverButtons } from './cover-buttons/cover-buttons'
|
||||||
import { FeatureLinks } from './feature-links'
|
import { FeatureLinks } from './feature-links'
|
||||||
import { useIntroPageContent } from './hooks/use-intro-page-content'
|
import { useIntroPageContent } from './hooks/use-intro-page-content'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
import { ForkAwesomeIcon } from '../common/fork-awesome/fork-awesome-icon'
|
|
||||||
import { RendererType } from '../render-page/rendering-message'
|
import { RendererType } from '../render-page/rendering-message'
|
||||||
|
import { WaitSpinner } from '../common/wait-spinner/wait-spinner'
|
||||||
|
|
||||||
export const IntroPage: React.FC = () => {
|
export const IntroPage: React.FC = () => {
|
||||||
const introPageContent = useIntroPageContent()
|
const introPageContent = useIntroPageContent()
|
||||||
|
@ -38,7 +38,7 @@ export const IntroPage: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
<CoverButtons/>
|
<CoverButtons/>
|
||||||
<ShowIf condition={ showSpinner }>
|
<ShowIf condition={ showSpinner }>
|
||||||
<ForkAwesomeIcon icon={ 'spinner' } className={ 'fa-spin' }/>
|
<WaitSpinner/>
|
||||||
</ShowIf>
|
</ShowIf>
|
||||||
<RenderIframe
|
<RenderIframe
|
||||||
frameClasses={ 'w-100 overflow-y-hidden' }
|
frameClasses={ 'w-100 overflow-y-hidden' }
|
||||||
|
|
|
@ -4,45 +4,112 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import MarkdownIt from 'markdown-it'
|
import React, { Ref, useCallback, useMemo, useRef, useState } from 'react'
|
||||||
import React, { RefObject, useMemo } from 'react'
|
|
||||||
import { useSelector } from 'react-redux'
|
|
||||||
import { ApplicationState } from '../../redux'
|
|
||||||
import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert'
|
import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert'
|
||||||
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
|
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
|
||||||
import './markdown-renderer.scss'
|
import './markdown-renderer.scss'
|
||||||
import { ComponentReplacer } from './replace-components/ComponentReplacer'
|
import { ComponentReplacer } from './replace-components/ComponentReplacer'
|
||||||
import { AdditionalMarkdownRendererProps } from './types'
|
import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
|
||||||
|
import { useComponentReplacers } from './hooks/use-component-replacers'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { NoteFrontmatter, RawNoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
|
||||||
|
import { LineMarkers } from './replace-components/linemarker/line-number-marker'
|
||||||
|
import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
|
||||||
|
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
||||||
|
import { TocAst } from 'markdown-it-toc-done-right'
|
||||||
|
import { useOnRefChange } from './hooks/use-on-ref-change'
|
||||||
|
import { BasicMarkdownItConfigurator } from './markdown-it-configurator/BasicMarkdownItConfigurator'
|
||||||
|
import { ImageClickHandler } from './replace-components/image/image-replacer'
|
||||||
|
import { InvalidYamlAlert } from './invalid-yaml-alert'
|
||||||
|
import { useTrimmedContent } from './hooks/use-trimmed-content'
|
||||||
|
|
||||||
export interface BasicMarkdownRendererProps {
|
export interface BasicMarkdownRendererProps {
|
||||||
componentReplacers?: () => ComponentReplacer[],
|
additionalReplacers?: () => ComponentReplacer[],
|
||||||
markdownIt: MarkdownIt,
|
|
||||||
documentReference?: RefObject<HTMLDivElement>
|
|
||||||
onBeforeRendering?: () => void
|
onBeforeRendering?: () => void
|
||||||
onAfterRendering?: () => void
|
onAfterRendering?: () => void
|
||||||
|
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||||
|
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
||||||
|
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
|
||||||
|
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||||
|
onTocChange?: (ast?: TocAst) => void
|
||||||
|
baseUrl?: string
|
||||||
|
onImageClick?: ImageClickHandler
|
||||||
|
outerContainerRef?: Ref<HTMLDivElement>
|
||||||
|
useAlternativeBreaks?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & AdditionalMarkdownRendererProps> = (
|
export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & AdditionalMarkdownRendererProps> = (
|
||||||
{
|
{
|
||||||
className,
|
className,
|
||||||
content,
|
content,
|
||||||
componentReplacers,
|
additionalReplacers,
|
||||||
markdownIt,
|
|
||||||
documentReference,
|
|
||||||
onBeforeRendering,
|
onBeforeRendering,
|
||||||
onAfterRendering
|
onAfterRendering,
|
||||||
|
onFirstHeadingChange,
|
||||||
|
onLineMarkerPositionChanged,
|
||||||
|
onFrontmatterChange,
|
||||||
|
onTaskCheckedChange,
|
||||||
|
onTocChange,
|
||||||
|
baseUrl,
|
||||||
|
onImageClick,
|
||||||
|
outerContainerRef,
|
||||||
|
useAlternativeBreaks
|
||||||
}) => {
|
}) => {
|
||||||
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
const rawMetaRef = useRef<RawNoteFrontmatter>()
|
||||||
const trimmedContent = useMemo(() => content.length > maxLength ? content.substr(0, maxLength) : content, [content,
|
const markdownBodyRef = useRef<HTMLDivElement>(null)
|
||||||
maxLength])
|
const currentLineMarkers = useRef<LineMarkers[]>()
|
||||||
const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, markdownIt, componentReplacers, onBeforeRendering, onAfterRendering)
|
const hasNewYamlError = useRef(false)
|
||||||
|
const tocAst = useRef<TocAst>()
|
||||||
|
const [showYamlError, setShowYamlError] = useState(false)
|
||||||
|
const [trimmedContent, contentExceedsLimit] = useTrimmedContent(content)
|
||||||
|
|
||||||
|
const markdownIt = useMemo(() =>
|
||||||
|
new BasicMarkdownItConfigurator({
|
||||||
|
useFrontmatter: !!onFrontmatterChange,
|
||||||
|
onParseError: errorState => hasNewYamlError.current = errorState,
|
||||||
|
onRawMetaChange: rawMeta => rawMetaRef.current = rawMeta,
|
||||||
|
onToc: toc => tocAst.current = toc,
|
||||||
|
onLineMarkers: onLineMarkerPositionChanged === undefined ? undefined
|
||||||
|
: lineMarkers => currentLineMarkers.current = lineMarkers,
|
||||||
|
useAlternativeBreaks
|
||||||
|
}).buildConfiguredMarkdownIt(), [onFrontmatterChange, onLineMarkerPositionChanged, useAlternativeBreaks])
|
||||||
|
|
||||||
|
const clearFrontmatter = useCallback(() => {
|
||||||
|
hasNewYamlError.current = false
|
||||||
|
rawMetaRef.current = undefined
|
||||||
|
onBeforeRendering?.()
|
||||||
|
}, [onBeforeRendering])
|
||||||
|
|
||||||
|
const checkYamlErrorState = useCallback(() => {
|
||||||
|
setShowYamlError(hasNewYamlError.current)
|
||||||
|
onAfterRendering?.()
|
||||||
|
}, [onAfterRendering])
|
||||||
|
|
||||||
|
const baseReplacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl)
|
||||||
|
const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, markdownIt, baseReplacers, additionalReplacers, clearFrontmatter, checkYamlErrorState)
|
||||||
|
|
||||||
|
useTranslation()
|
||||||
|
useCalculateLineMarkerPosition(markdownBodyRef, currentLineMarkers.current, onLineMarkerPositionChanged, markdownBodyRef.current?.offsetTop ?? 0)
|
||||||
|
useExtractFirstHeadline(markdownBodyRef, content, onFirstHeadingChange)
|
||||||
|
useOnRefChange(tocAst, onTocChange)
|
||||||
|
useOnRefChange(rawMetaRef, (newValue) => {
|
||||||
|
if (!newValue) {
|
||||||
|
onFrontmatterChange?.(undefined)
|
||||||
|
} else {
|
||||||
|
onFrontmatterChange?.(new NoteFrontmatter(newValue))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ `${ className ?? '' } d-flex flex-column align-items-center` }>
|
<div ref={ outerContainerRef } className={ 'position-relative' }>
|
||||||
<DocumentLengthLimitReachedAlert contentLength={ content.length }/>
|
<InvalidYamlAlert show={ showYamlError }/>
|
||||||
<div ref={ documentReference } className={ 'markdown-body w-100 d-flex flex-column align-items-center' }>
|
<DocumentLengthLimitReachedAlert show={ contentExceedsLimit }/>
|
||||||
|
<div ref={ markdownBodyRef }
|
||||||
|
className={ `${ className ?? '' } markdown-body w-100 d-flex flex-column align-items-center` }>
|
||||||
{ markdownReactDom }
|
{ markdownReactDom }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default BasicMarkdownRenderer
|
||||||
|
|
|
@ -10,17 +10,15 @@ import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import { ApplicationState } from '../../redux'
|
import { ApplicationState } from '../../redux'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
|
import { SimpleAlertProps } from '../common/simple-alert/simple-alert-props'
|
||||||
|
|
||||||
export interface DocumentLengthLimitReachedAlertProps {
|
export const DocumentLengthLimitReachedAlert: React.FC<SimpleAlertProps> = ({ show }) => {
|
||||||
contentLength: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DocumentLengthLimitReachedAlert: React.FC<DocumentLengthLimitReachedAlertProps> = ({ contentLength }) => {
|
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowIf condition={ contentLength > maxLength }>
|
<ShowIf condition={ show }>
|
||||||
<Alert variant='danger' dir={ 'auto' } data-cy={ 'limitReachedMessage' }>
|
<Alert variant='danger' dir={ 'auto' } data-cy={ 'limitReachedMessage' }>
|
||||||
<Trans i18nKey={ 'editor.error.limitReached.description' } values={ { maxLength } }/>
|
<Trans i18nKey={ 'editor.error.limitReached.description' } values={ { maxLength } }/>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
|
|
@ -1,106 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { TocAst } from 'markdown-it-toc-done-right'
|
|
||||||
import React, { Ref, useCallback, useMemo, useRef, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { NoteFrontmatter, RawNoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
|
|
||||||
import { BasicMarkdownRenderer } from './basic-markdown-renderer'
|
|
||||||
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
|
|
||||||
import { usePostFrontmatterOnChange } from './hooks/use-post-frontmatter-on-change'
|
|
||||||
import { usePostTocAstOnChange } from './hooks/use-post-toc-ast-on-change'
|
|
||||||
import { useReplacerInstanceListCreator } from './hooks/use-replacer-instance-list-creator'
|
|
||||||
import { InvalidYamlAlert } from './invalid-yaml-alert'
|
|
||||||
import { FullMarkdownItConfigurator } from './markdown-it-configurator/FullMarkdownItConfigurator'
|
|
||||||
import { ImageClickHandler } from './replace-components/image/image-replacer'
|
|
||||||
import { LineMarkers } from './replace-components/linemarker/line-number-marker'
|
|
||||||
import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
|
|
||||||
import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
|
|
||||||
|
|
||||||
export interface FullMarkdownRendererProps {
|
|
||||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
|
||||||
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
|
|
||||||
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
|
|
||||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
|
||||||
onTocChange?: (ast: TocAst) => void
|
|
||||||
rendererRef?: Ref<HTMLDivElement>
|
|
||||||
baseUrl?: string
|
|
||||||
onImageClick?: ImageClickHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & AdditionalMarkdownRendererProps> = (
|
|
||||||
{
|
|
||||||
onFirstHeadingChange,
|
|
||||||
onLineMarkerPositionChanged,
|
|
||||||
onFrontmatterChange,
|
|
||||||
onTaskCheckedChange,
|
|
||||||
onTocChange,
|
|
||||||
content,
|
|
||||||
className,
|
|
||||||
rendererRef,
|
|
||||||
baseUrl,
|
|
||||||
onImageClick
|
|
||||||
}) => {
|
|
||||||
const allReplacers = useReplacerInstanceListCreator(onTaskCheckedChange, onImageClick, baseUrl)
|
|
||||||
useTranslation()
|
|
||||||
|
|
||||||
const [showYamlError, setShowYamlError] = useState(false)
|
|
||||||
const hasNewYamlError = useRef(false)
|
|
||||||
|
|
||||||
const rawMetaRef = useRef<RawNoteFrontmatter>()
|
|
||||||
const firstHeadingRef = useRef<string>()
|
|
||||||
const documentElement = useRef<HTMLDivElement>(null)
|
|
||||||
const currentLineMarkers = useRef<LineMarkers[]>()
|
|
||||||
usePostFrontmatterOnChange(rawMetaRef.current, firstHeadingRef.current, onFrontmatterChange, onFirstHeadingChange)
|
|
||||||
useCalculateLineMarkerPosition(documentElement, currentLineMarkers.current, onLineMarkerPositionChanged, documentElement.current?.offsetTop ?? 0)
|
|
||||||
useExtractFirstHeadline(documentElement, content, onFirstHeadingChange)
|
|
||||||
|
|
||||||
const tocAst = useRef<TocAst>()
|
|
||||||
usePostTocAstOnChange(tocAst, onTocChange)
|
|
||||||
|
|
||||||
const markdownIt = useMemo(() => {
|
|
||||||
return (new FullMarkdownItConfigurator(
|
|
||||||
!!onFrontmatterChange,
|
|
||||||
errorState => hasNewYamlError.current = errorState,
|
|
||||||
rawMeta => {
|
|
||||||
rawMetaRef.current = rawMeta
|
|
||||||
},
|
|
||||||
toc => {
|
|
||||||
tocAst.current = toc
|
|
||||||
},
|
|
||||||
onLineMarkerPositionChanged === undefined
|
|
||||||
? undefined
|
|
||||||
: lineMarkers => {
|
|
||||||
currentLineMarkers.current = lineMarkers
|
|
||||||
}
|
|
||||||
)).buildConfiguredMarkdownIt()
|
|
||||||
}, [onLineMarkerPositionChanged, onFrontmatterChange])
|
|
||||||
|
|
||||||
const clearFrontmatter = useCallback(() => {
|
|
||||||
hasNewYamlError.current = false
|
|
||||||
rawMetaRef.current = undefined
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const checkYamlErrorState = useCallback(() => {
|
|
||||||
if (hasNewYamlError.current !== showYamlError) {
|
|
||||||
setShowYamlError(hasNewYamlError.current)
|
|
||||||
}
|
|
||||||
}, [setShowYamlError, showYamlError])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ rendererRef } className={ 'position-relative' }>
|
|
||||||
<InvalidYamlAlert showYamlError={ showYamlError }/>
|
|
||||||
<BasicMarkdownRenderer
|
|
||||||
className={ className }
|
|
||||||
content={ content }
|
|
||||||
componentReplacers={ allReplacers }
|
|
||||||
markdownIt={ markdownIt }
|
|
||||||
documentReference={ documentElement }
|
|
||||||
onBeforeRendering={ clearFrontmatter }
|
|
||||||
onAfterRendering={ checkYamlErrorState }/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useMemo } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { AbcReplacer } from '../replace-components/abc/abc-replacer'
|
import { AbcReplacer } from '../replace-components/abc/abc-replacer'
|
||||||
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
|
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
|
||||||
import { ComponentReplacer } from '../replace-components/ComponentReplacer'
|
import { ComponentReplacer } from '../replace-components/ComponentReplacer'
|
||||||
|
@ -21,14 +21,13 @@ import { MarkmapReplacer } from '../replace-components/markmap/markmap-replacer'
|
||||||
import { MermaidReplacer } from '../replace-components/mermaid/mermaid-replacer'
|
import { MermaidReplacer } from '../replace-components/mermaid/mermaid-replacer'
|
||||||
import { ColoredBlockquoteReplacer } from '../replace-components/colored-blockquote/colored-blockquote-replacer'
|
import { ColoredBlockquoteReplacer } from '../replace-components/colored-blockquote/colored-blockquote-replacer'
|
||||||
import { SequenceDiagramReplacer } from '../replace-components/sequence-diagram/sequence-diagram-replacer'
|
import { SequenceDiagramReplacer } from '../replace-components/sequence-diagram/sequence-diagram-replacer'
|
||||||
import { TaskListReplacer } from '../replace-components/task-list/task-list-replacer'
|
import { TaskCheckedChangeHandler, TaskListReplacer } from '../replace-components/task-list/task-list-replacer'
|
||||||
import { VegaReplacer } from '../replace-components/vega-lite/vega-replacer'
|
import { VegaReplacer } from '../replace-components/vega-lite/vega-replacer'
|
||||||
import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
|
import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
|
||||||
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
|
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
|
||||||
|
|
||||||
export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void,
|
export const useComponentReplacers = (onTaskCheckedChange?: TaskCheckedChangeHandler, onImageClick?: ImageClickHandler, baseUrl?: string): () => ComponentReplacer[] =>
|
||||||
onImageClick?: ImageClickHandler, baseUrl?: string): () => ComponentReplacer[] => useMemo(() =>
|
useCallback(() => [
|
||||||
() => [
|
|
||||||
new LinemarkerReplacer(),
|
new LinemarkerReplacer(),
|
||||||
new GistReplacer(),
|
new GistReplacer(),
|
||||||
new YoutubeReplacer(),
|
new YoutubeReplacer(),
|
|
@ -15,7 +15,8 @@ import { calculateNewLineNumberMapping } from '../utils/line-number-mapping'
|
||||||
export const useConvertMarkdownToReactDom = (
|
export const useConvertMarkdownToReactDom = (
|
||||||
markdownCode: string,
|
markdownCode: string,
|
||||||
markdownIt: MarkdownIt,
|
markdownIt: MarkdownIt,
|
||||||
componentReplacers?: () => ComponentReplacer[],
|
baseReplacers: () => ComponentReplacer[],
|
||||||
|
additionalReplacers?: () => ComponentReplacer[],
|
||||||
onBeforeRendering?: () => void,
|
onBeforeRendering?: () => void,
|
||||||
onAfterRendering?: () => void): ReactElement[] => {
|
onAfterRendering?: () => void): ReactElement[] => {
|
||||||
const oldMarkdownLineKeys = useRef<LineKeys[]>()
|
const oldMarkdownLineKeys = useRef<LineKeys[]>()
|
||||||
|
@ -33,11 +34,14 @@ export const useConvertMarkdownToReactDom = (
|
||||||
} = calculateNewLineNumberMapping(contentLines, oldMarkdownLineKeys.current ?? [], lastUsedLineId.current)
|
} = calculateNewLineNumberMapping(contentLines, oldMarkdownLineKeys.current ?? [], lastUsedLineId.current)
|
||||||
oldMarkdownLineKeys.current = newLines
|
oldMarkdownLineKeys.current = newLines
|
||||||
lastUsedLineId.current = newLastUsedLineId
|
lastUsedLineId.current = newLastUsedLineId
|
||||||
const transformer = componentReplacers ? buildTransformer(newLines, componentReplacers()) : undefined
|
|
||||||
|
const replacers = baseReplacers()
|
||||||
|
.concat(additionalReplacers ? additionalReplacers() : [])
|
||||||
|
const transformer = replacers.length > 0 ? buildTransformer(newLines, replacers) : undefined
|
||||||
const rendering = ReactHtmlParser(html, { transform: transformer })
|
const rendering = ReactHtmlParser(html, { transform: transformer })
|
||||||
if (onAfterRendering) {
|
if (onAfterRendering) {
|
||||||
onAfterRendering()
|
onAfterRendering()
|
||||||
}
|
}
|
||||||
return rendering
|
return rendering
|
||||||
}, [onBeforeRendering, onAfterRendering, markdownCode, markdownIt, componentReplacers])
|
}, [onBeforeRendering, markdownIt, markdownCode, baseReplacers, additionalReplacers, onAfterRendering])
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,12 +12,12 @@ export const useExtractFirstHeadline = (documentElement: React.RefObject<HTMLDiv
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
let innerText = ''
|
|
||||||
|
|
||||||
if ((node as HTMLElement).classList?.contains('katex-mathml')) {
|
if ((node as HTMLElement).classList?.contains('katex-mathml')) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let innerText = ''
|
||||||
|
|
||||||
if (node.childNodes && node.childNodes.length > 0) {
|
if (node.childNodes && node.childNodes.length > 0) {
|
||||||
node.childNodes.forEach((child) => {
|
node.childNodes.forEach((child) => {
|
||||||
innerText += extractInnerText(child)
|
innerText += extractInnerText(child)
|
||||||
|
@ -37,11 +37,11 @@ export const useExtractFirstHeadline = (documentElement: React.RefObject<HTMLDiv
|
||||||
const firstHeading = documentElement.current.getElementsByTagName('h1')
|
const firstHeading = documentElement.current.getElementsByTagName('h1')
|
||||||
.item(0)
|
.item(0)
|
||||||
const headingText = extractInnerText(firstHeading)
|
const headingText = extractInnerText(firstHeading)
|
||||||
if (headingText === lastFirstHeading.current) {
|
.trim()
|
||||||
return
|
if (headingText !== lastFirstHeading.current) {
|
||||||
}
|
|
||||||
lastFirstHeading.current = headingText
|
lastFirstHeading.current = headingText
|
||||||
onFirstHeadingChange(headingText)
|
onFirstHeadingChange(headingText)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, [documentElement, extractInnerText, onFirstHeadingChange, content])
|
}, [documentElement, extractInnerText, onFirstHeadingChange, content])
|
||||||
}
|
}
|
||||||
|
|
18
src/components/markdown-renderer/hooks/use-on-ref-change.ts
Normal file
18
src/components/markdown-renderer/hooks/use-on-ref-change.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import equal from 'fast-deep-equal'
|
||||||
|
import { MutableRefObject, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
export const useOnRefChange = <T>(reference: MutableRefObject<T | undefined>, onChange?: (newValue?: T) => void): void => {
|
||||||
|
const lastValue = useRef<T | undefined>()
|
||||||
|
useEffect(() => {
|
||||||
|
if (onChange && !equal(reference, lastValue.current)) {
|
||||||
|
lastValue.current = reference.current
|
||||||
|
onChange(reference.current)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,35 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import equal from 'fast-deep-equal'
|
|
||||||
import { useEffect, useRef } from 'react'
|
|
||||||
import { NoteFrontmatter, RawNoteFrontmatter } from '../../editor-page/note-frontmatter/note-frontmatter'
|
|
||||||
|
|
||||||
export const usePostFrontmatterOnChange = (
|
|
||||||
rawFrontmatter: RawNoteFrontmatter | undefined,
|
|
||||||
firstHeadingRef: string | undefined,
|
|
||||||
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void,
|
|
||||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
|
||||||
): void => {
|
|
||||||
const oldMetaRef = useRef<RawNoteFrontmatter>()
|
|
||||||
const oldFirstHeadingRef = useRef<string>()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (onFrontmatterChange && !equal(oldMetaRef.current, rawFrontmatter)) {
|
|
||||||
if (rawFrontmatter) {
|
|
||||||
const newFrontmatter = new NoteFrontmatter(rawFrontmatter)
|
|
||||||
onFrontmatterChange(newFrontmatter)
|
|
||||||
} else {
|
|
||||||
onFrontmatterChange(undefined)
|
|
||||||
}
|
|
||||||
oldMetaRef.current = rawFrontmatter
|
|
||||||
}
|
|
||||||
if (onFirstHeadingChange && !equal(firstHeadingRef, oldFirstHeadingRef.current)) {
|
|
||||||
onFirstHeadingChange(firstHeadingRef || undefined)
|
|
||||||
oldFirstHeadingRef.current = firstHeadingRef
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import equal from 'fast-deep-equal'
|
|
||||||
import { TocAst } from 'markdown-it-toc-done-right'
|
|
||||||
import { RefObject, useEffect, useRef } from 'react'
|
|
||||||
|
|
||||||
export const usePostTocAstOnChange = (tocAst: RefObject<TocAst | undefined>, onTocChange?: (ast: TocAst) => void): void => {
|
|
||||||
const lastTocAst = useRef<TocAst>()
|
|
||||||
useEffect(() => {
|
|
||||||
if (onTocChange && tocAst.current && !equal(tocAst, lastTocAst.current)) {
|
|
||||||
lastTocAst.current = tocAst.current
|
|
||||||
onTocChange(tocAst.current)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { ApplicationState } from '../../../redux'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
|
||||||
|
export const useTrimmedContent = (content: string): [trimmedContent: string, contentExceedsLimit: boolean] => {
|
||||||
|
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
|
||||||
|
const contentExceedsLimit = content.length > maxLength
|
||||||
|
|
||||||
|
const trimmedContent = useMemo(() => contentExceedsLimit ? content.substr(0, maxLength) : content, [content,
|
||||||
|
contentExceedsLimit,
|
||||||
|
maxLength])
|
||||||
|
return [trimmedContent, contentExceedsLimit]
|
||||||
|
}
|
|
@ -9,16 +9,13 @@ import { Alert } from 'react-bootstrap'
|
||||||
import { Trans, useTranslation } from 'react-i18next'
|
import { Trans, useTranslation } from 'react-i18next'
|
||||||
import { InternalLink } from '../common/links/internal-link'
|
import { InternalLink } from '../common/links/internal-link'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
|
import { SimpleAlertProps } from '../common/simple-alert/simple-alert-props'
|
||||||
|
|
||||||
export interface InvalidYamlAlertProps {
|
export const InvalidYamlAlert: React.FC<SimpleAlertProps> = ({ show }) => {
|
||||||
showYamlError: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const InvalidYamlAlert: React.FC<InvalidYamlAlertProps> = ({ showYamlError }) => {
|
|
||||||
useTranslation()
|
useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShowIf condition={ showYamlError }>
|
<ShowIf condition={ show }>
|
||||||
<Alert variant='warning' dir='auto'>
|
<Alert variant='warning' dir='auto'>
|
||||||
<Trans i18nKey='editor.invalidYaml'>
|
<Trans i18nKey='editor.invalidYaml'>
|
||||||
<InternalLink text='yaml-metadata' href='/n/yaml-metadata' className='text-primary'/>
|
<InternalLink text='yaml-metadata' href='/n/yaml-metadata' className='text-primary'/>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
|
@ -19,11 +19,78 @@ import { MarkdownItParserDebugger } from '../markdown-it-plugins/parser-debugger
|
||||||
import { spoilerContainer } from '../markdown-it-plugins/spoiler-container'
|
import { spoilerContainer } from '../markdown-it-plugins/spoiler-container'
|
||||||
import { tasksLists } from '../markdown-it-plugins/tasks-lists'
|
import { tasksLists } from '../markdown-it-plugins/tasks-lists'
|
||||||
import { twitterEmojis } from '../markdown-it-plugins/twitter-emojis'
|
import { twitterEmojis } from '../markdown-it-plugins/twitter-emojis'
|
||||||
import { MarkdownItConfigurator } from './MarkdownItConfigurator'
|
import { RawNoteFrontmatter } from '../../editor-page/note-frontmatter/note-frontmatter'
|
||||||
|
import { TocAst } from 'markdown-it-toc-done-right'
|
||||||
|
import { LineMarkers, lineNumberMarker } from '../replace-components/linemarker/line-number-marker'
|
||||||
|
import { plantumlWithError } from '../markdown-it-plugins/plantuml'
|
||||||
|
import { headlineAnchors } from '../markdown-it-plugins/headline-anchors'
|
||||||
|
import { KatexReplacer } from '../replace-components/katex/katex-replacer'
|
||||||
|
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
|
||||||
|
import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
|
||||||
|
import { GistReplacer } from '../replace-components/gist/gist-replacer'
|
||||||
|
import { legacyPdfShortCode } from '../regex-plugins/replace-legacy-pdf-short-code'
|
||||||
|
import { legacySlideshareShortCode } from '../regex-plugins/replace-legacy-slideshare-short-code'
|
||||||
|
import { legacySpeakerdeckShortCode } from '../regex-plugins/replace-legacy-speakerdeck-short-code'
|
||||||
|
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
|
||||||
|
import { highlightedCode } from '../markdown-it-plugins/highlighted-code'
|
||||||
|
import { quoteExtraColor } from '../markdown-it-plugins/quote-extra-color'
|
||||||
|
import { quoteExtra } from '../markdown-it-plugins/quote-extra'
|
||||||
|
import { documentTableOfContents } from '../markdown-it-plugins/document-table-of-contents'
|
||||||
|
import { frontmatterExtract } from '../markdown-it-plugins/frontmatter'
|
||||||
|
|
||||||
|
export interface ConfiguratorDetails {
|
||||||
|
useFrontmatter: boolean,
|
||||||
|
onParseError: (error: boolean) => void,
|
||||||
|
onRawMetaChange: (rawMeta: RawNoteFrontmatter) => void,
|
||||||
|
onToc: (toc: TocAst) => void,
|
||||||
|
onLineMarkers?: (lineMarkers: LineMarkers[]) => void
|
||||||
|
useAlternativeBreaks?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
|
||||||
|
protected readonly options: T
|
||||||
|
protected configurations: MarkdownIt.PluginSimple[] = []
|
||||||
|
protected postConfigurations: MarkdownIt.PluginSimple[] = []
|
||||||
|
|
||||||
|
constructor(options: T) {
|
||||||
|
this.options = options
|
||||||
|
}
|
||||||
|
|
||||||
|
public pushConfig(plugin: MarkdownIt.PluginSimple): this {
|
||||||
|
this.configurations.push(plugin)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
public buildConfiguredMarkdownIt(): MarkdownIt {
|
||||||
|
const markdownIt = new MarkdownIt('default', {
|
||||||
|
html: true,
|
||||||
|
breaks: this.options.useAlternativeBreaks ?? true,
|
||||||
|
langPrefix: '',
|
||||||
|
typographer: true
|
||||||
|
})
|
||||||
|
this.configure(markdownIt)
|
||||||
|
this.configurations.forEach((configuration) => markdownIt.use(configuration))
|
||||||
|
this.postConfigurations.forEach((postConfiguration) => markdownIt.use(postConfiguration))
|
||||||
|
return markdownIt
|
||||||
|
}
|
||||||
|
|
||||||
export class BasicMarkdownItConfigurator extends MarkdownItConfigurator {
|
|
||||||
protected configure(markdownIt: MarkdownIt): void {
|
protected configure(markdownIt: MarkdownIt): void {
|
||||||
this.configurations.push(
|
this.configurations.push(
|
||||||
|
plantumlWithError,
|
||||||
|
headlineAnchors,
|
||||||
|
KatexReplacer.markdownItPlugin,
|
||||||
|
YoutubeReplacer.markdownItPlugin,
|
||||||
|
VimeoReplacer.markdownItPlugin,
|
||||||
|
GistReplacer.markdownItPlugin,
|
||||||
|
legacyPdfShortCode,
|
||||||
|
legacySlideshareShortCode,
|
||||||
|
legacySpeakerdeckShortCode,
|
||||||
|
AsciinemaReplacer.markdownItPlugin,
|
||||||
|
highlightedCode,
|
||||||
|
quoteExtraColor,
|
||||||
|
quoteExtra('name', 'user'),
|
||||||
|
quoteExtra('time', 'clock-o'),
|
||||||
|
documentTableOfContents(this.options.onToc),
|
||||||
twitterEmojis,
|
twitterEmojis,
|
||||||
abbreviation,
|
abbreviation,
|
||||||
definitionList,
|
definitionList,
|
||||||
|
@ -35,8 +102,19 @@ export class BasicMarkdownItConfigurator extends MarkdownItConfigurator {
|
||||||
imsize,
|
imsize,
|
||||||
tasksLists,
|
tasksLists,
|
||||||
alertContainer,
|
alertContainer,
|
||||||
spoilerContainer
|
spoilerContainer)
|
||||||
)
|
|
||||||
|
if (this.options.useFrontmatter) {
|
||||||
|
this.configurations.push(frontmatterExtract({
|
||||||
|
onParseError: this.options.onParseError,
|
||||||
|
onRawMetaChange: this.options.onRawMetaChange
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.options.onLineMarkers) {
|
||||||
|
this.configurations.push(lineNumberMarker(this.options.onLineMarkers))
|
||||||
|
}
|
||||||
|
|
||||||
this.postConfigurations.push(
|
this.postConfigurations.push(
|
||||||
linkifyExtra,
|
linkifyExtra,
|
||||||
MarkdownItParserDebugger
|
MarkdownItParserDebugger
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import MarkdownIt from 'markdown-it'
|
|
||||||
import { TocAst } from 'markdown-it-toc-done-right'
|
|
||||||
import { RawNoteFrontmatter } from '../../editor-page/note-frontmatter/note-frontmatter'
|
|
||||||
import { documentToc } from '../markdown-it-plugins/document-toc'
|
|
||||||
import { frontmatterExtract } from '../markdown-it-plugins/frontmatter'
|
|
||||||
import { headlineAnchors } from '../markdown-it-plugins/headline-anchors'
|
|
||||||
import { highlightedCode } from '../markdown-it-plugins/highlighted-code'
|
|
||||||
import { plantumlWithError } from '../markdown-it-plugins/plantuml'
|
|
||||||
import { quoteExtra } from '../markdown-it-plugins/quote-extra'
|
|
||||||
import { legacySlideshareShortCode } from '../regex-plugins/replace-legacy-slideshare-short-code'
|
|
||||||
import { legacySpeakerdeckShortCode } from '../regex-plugins/replace-legacy-speakerdeck-short-code'
|
|
||||||
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
|
|
||||||
import { GistReplacer } from '../replace-components/gist/gist-replacer'
|
|
||||||
import { KatexReplacer } from '../replace-components/katex/katex-replacer'
|
|
||||||
import { LineMarkers, lineNumberMarker } from '../replace-components/linemarker/line-number-marker'
|
|
||||||
import { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
|
|
||||||
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
|
|
||||||
import { BasicMarkdownItConfigurator } from './BasicMarkdownItConfigurator'
|
|
||||||
import { quoteExtraColor } from '../markdown-it-plugins/quote-extra-color'
|
|
||||||
import { legacyPdfShortCode } from '../regex-plugins/replace-legacy-pdf-short-code'
|
|
||||||
|
|
||||||
export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
|
|
||||||
constructor(
|
|
||||||
private useFrontmatter: boolean,
|
|
||||||
private passYamlErrorState: (error: boolean) => void,
|
|
||||||
private onRawMeta: (rawMeta: RawNoteFrontmatter) => void,
|
|
||||||
private onToc: (toc: TocAst) => void,
|
|
||||||
private onLineMarkers?: (lineMarkers: LineMarkers[]) => void
|
|
||||||
) {
|
|
||||||
super()
|
|
||||||
}
|
|
||||||
|
|
||||||
protected configure(markdownIt: MarkdownIt): void {
|
|
||||||
super.configure(markdownIt)
|
|
||||||
|
|
||||||
this.configurations.push(
|
|
||||||
plantumlWithError,
|
|
||||||
(markdownIt) => {
|
|
||||||
frontmatterExtract(markdownIt,
|
|
||||||
!this.useFrontmatter
|
|
||||||
? undefined
|
|
||||||
: {
|
|
||||||
onParseError: (hasError: boolean) => this.passYamlErrorState(hasError),
|
|
||||||
onRawMeta: (rawMeta: RawNoteFrontmatter) => this.onRawMeta(rawMeta)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
headlineAnchors,
|
|
||||||
KatexReplacer.markdownItPlugin,
|
|
||||||
YoutubeReplacer.markdownItPlugin,
|
|
||||||
VimeoReplacer.markdownItPlugin,
|
|
||||||
GistReplacer.markdownItPlugin,
|
|
||||||
legacyPdfShortCode,
|
|
||||||
legacySlideshareShortCode,
|
|
||||||
legacySpeakerdeckShortCode,
|
|
||||||
AsciinemaReplacer.markdownItPlugin,
|
|
||||||
highlightedCode,
|
|
||||||
quoteExtraColor,
|
|
||||||
quoteExtra({
|
|
||||||
quoteLabel: 'name',
|
|
||||||
icon: 'user'
|
|
||||||
}),
|
|
||||||
quoteExtra({
|
|
||||||
quoteLabel: 'time',
|
|
||||||
icon: 'clock-o'
|
|
||||||
}),
|
|
||||||
(markdownIt) => documentToc(markdownIt, this.onToc))
|
|
||||||
if (this.onLineMarkers) {
|
|
||||||
const callback = this.onLineMarkers
|
|
||||||
this.configurations.push(
|
|
||||||
(markdownIt) => lineNumberMarker(markdownIt, (lineMarkers) => callback(lineMarkers))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
/*
|
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
import MarkdownIt from 'markdown-it'
|
|
||||||
|
|
||||||
export abstract class MarkdownItConfigurator {
|
|
||||||
protected configurations: MarkdownIt.PluginSimple[] = []
|
|
||||||
protected postConfigurations: MarkdownIt.PluginSimple[] = []
|
|
||||||
|
|
||||||
public pushConfig(plugin: MarkdownIt.PluginSimple): this {
|
|
||||||
this.configurations.push(plugin)
|
|
||||||
return this
|
|
||||||
}
|
|
||||||
|
|
||||||
public buildConfiguredMarkdownIt(): MarkdownIt {
|
|
||||||
const markdownIt = new MarkdownIt('default', {
|
|
||||||
html: true,
|
|
||||||
breaks: true,
|
|
||||||
langPrefix: '',
|
|
||||||
typographer: true
|
|
||||||
})
|
|
||||||
this.configure(markdownIt)
|
|
||||||
this.configurations.forEach((configuration) => markdownIt.use(configuration))
|
|
||||||
this.postConfigurations.forEach((postConfiguration) => markdownIt.use(postConfiguration))
|
|
||||||
return markdownIt
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract configure(markdownIt: MarkdownIt): void;
|
|
||||||
}
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import MarkdownIt from 'markdown-it/lib'
|
||||||
|
import { TocAst } from 'markdown-it-toc-done-right'
|
||||||
|
import { documentToc } from './document-toc'
|
||||||
|
|
||||||
|
export const documentTableOfContents = (onTocChange: ((toc: TocAst) => void)): MarkdownIt.PluginSimple => {
|
||||||
|
return (markdownIt) => documentToc(markdownIt, onTocChange)
|
||||||
|
}
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
import MarkdownIt from 'markdown-it'
|
import MarkdownIt from 'markdown-it'
|
||||||
import toc, { TocAst } from 'markdown-it-toc-done-right'
|
import toc, { TocAst } from 'markdown-it-toc-done-right'
|
||||||
import { slugify } from '../../editor-page/table-of-contents/table-of-contents'
|
import { tocSlugify } from '../../editor-page/table-of-contents/toc-slugify'
|
||||||
|
|
||||||
export type DocumentTocPluginOptions = (ast: TocAst) => void
|
export type DocumentTocPluginOptions = (ast: TocAst) => void
|
||||||
|
|
||||||
|
@ -21,6 +21,6 @@ export const documentToc: MarkdownIt.PluginWithOptions<DocumentTocPluginOptions>
|
||||||
callback: (code: string, ast: TocAst): void => {
|
callback: (code: string, ast: TocAst): void => {
|
||||||
onToc(ast)
|
onToc(ast)
|
||||||
},
|
},
|
||||||
slugify: slugify
|
slugify: tocSlugify
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,22 +11,20 @@ import { RawNoteFrontmatter } from '../../editor-page/note-frontmatter/note-fron
|
||||||
|
|
||||||
interface FrontmatterPluginOptions {
|
interface FrontmatterPluginOptions {
|
||||||
onParseError: (error: boolean) => void,
|
onParseError: (error: boolean) => void,
|
||||||
onRawMeta: (rawMeta: RawNoteFrontmatter) => void,
|
onRawMetaChange: (rawMeta: RawNoteFrontmatter) => void,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const frontmatterExtract: MarkdownIt.PluginWithOptions<FrontmatterPluginOptions> = (markdownIt: MarkdownIt, options) => {
|
export const frontmatterExtract: (options: FrontmatterPluginOptions) => MarkdownIt.PluginSimple = (options) =>
|
||||||
if (!options) {
|
(markdownIt) => {
|
||||||
return
|
|
||||||
}
|
|
||||||
frontmatter(markdownIt, (rawMeta: string) => {
|
frontmatter(markdownIt, (rawMeta: string) => {
|
||||||
try {
|
try {
|
||||||
const meta: RawNoteFrontmatter = yaml.load(rawMeta) as RawNoteFrontmatter
|
const meta: RawNoteFrontmatter = yaml.load(rawMeta) as RawNoteFrontmatter
|
||||||
options.onParseError(false)
|
options.onParseError(false)
|
||||||
options.onRawMeta(meta)
|
options.onRawMetaChange(meta)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
options.onParseError(true)
|
options.onParseError(true)
|
||||||
options.onRawMeta({} as RawNoteFrontmatter)
|
options.onRawMetaChange({} as RawNoteFrontmatter)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,17 +8,12 @@ import MarkdownIt from 'markdown-it/lib'
|
||||||
import Token from 'markdown-it/lib/token'
|
import Token from 'markdown-it/lib/token'
|
||||||
import { IconName } from '../../common/fork-awesome/types'
|
import { IconName } from '../../common/fork-awesome/types'
|
||||||
|
|
||||||
export interface QuoteExtraOptions {
|
export const quoteExtra: (quoteLabel: string, icon: IconName) => MarkdownIt.PluginSimple =
|
||||||
quoteLabel: string
|
(quoteLabel: string, icon: IconName) => (md) => {
|
||||||
icon: IconName
|
md.inline.ruler.push(`extraQuote_${ quoteLabel }`, (state) => {
|
||||||
}
|
|
||||||
|
|
||||||
export const quoteExtra: (pluginOptions: QuoteExtraOptions) => MarkdownIt.PluginSimple =
|
|
||||||
(pluginOptions) => (md) => {
|
|
||||||
md.inline.ruler.push(`extraQuote_${ pluginOptions.quoteLabel }`, (state) => {
|
|
||||||
const quoteExtraTagValues = parseQuoteExtraTag(state.src, state.pos, state.posMax)
|
const quoteExtraTagValues = parseQuoteExtraTag(state.src, state.pos, state.posMax)
|
||||||
|
|
||||||
if (!quoteExtraTagValues || quoteExtraTagValues.label !== pluginOptions.quoteLabel) {
|
if (!quoteExtraTagValues || quoteExtraTagValues.label !== quoteLabel) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
state.pos = quoteExtraTagValues.valueEndIndex + 1
|
state.pos = quoteExtraTagValues.valueEndIndex + 1
|
||||||
|
@ -32,7 +27,7 @@ export const quoteExtra: (pluginOptions: QuoteExtraOptions) => MarkdownIt.Plugin
|
||||||
)
|
)
|
||||||
|
|
||||||
const token = state.push('quote-extra', '', 0)
|
const token = state.push('quote-extra', '', 0)
|
||||||
token.attrSet('icon', pluginOptions.icon)
|
token.attrSet('icon', icon)
|
||||||
token.children = tokens
|
token.children = tokens
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState } from 'react'
|
import React, { useEffect, useRef, useState } from 'react'
|
||||||
|
@ -25,7 +25,8 @@ export const FlowChart: React.FC<FlowChartProps> = ({ code }) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const currentDiagramRef = diagramRef.current
|
const currentDiagramRef = diagramRef.current
|
||||||
import(/* webpackChunkName: "flowchart.js" */ 'flowchart.js').then((imp) => {
|
import(/* webpackChunkName: "flowchart.js" */ 'flowchart.js')
|
||||||
|
.then((imp) => {
|
||||||
const parserOutput = imp.parse(code)
|
const parserOutput = imp.parse(code)
|
||||||
try {
|
try {
|
||||||
parserOutput.drawSVG(currentDiagramRef, {
|
parserOutput.drawSVG(currentDiagramRef, {
|
||||||
|
@ -42,9 +43,7 @@ export const FlowChart: React.FC<FlowChartProps> = ({ code }) => {
|
||||||
setError(true)
|
setError(true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => console.error('error while loading flowchart.js'))
|
||||||
console.error('error while loading flowchart.js')
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Array.from(currentDiagramRef.children)
|
Array.from(currentDiagramRef.children)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
@ -32,7 +32,7 @@ export const GraphvizFrame: React.FC<GraphvizFrameProps> = ({ code }) => {
|
||||||
}
|
}
|
||||||
const actualContainer = container.current
|
const actualContainer = container.current
|
||||||
|
|
||||||
import('@hpcc-js/wasm')
|
import(/* webpackChunkName: "d3-graphviz" */'@hpcc-js/wasm')
|
||||||
.then((wasmPlugin) => {
|
.then((wasmPlugin) => {
|
||||||
wasmPlugin.wasmFolder('/static/js')
|
wasmPlugin.wasmFolder('/static/js')
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
/*
|
/*!
|
||||||
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.markdown-body {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body {
|
.markdown-body {
|
||||||
@import '../../../../../../node_modules/highlight.js/styles/github';
|
@import '../../../../../../node_modules/highlight.js/styles/github';
|
||||||
|
|
||||||
|
|
|
@ -17,13 +17,16 @@ export interface HighlightedCodeProps {
|
||||||
wrapLines: boolean
|
wrapLines: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const escapeHtml = (unsafe: string): string => {
|
/*
|
||||||
|
TODO: Test method or rewrite code so this is not necessary anymore
|
||||||
|
*/
|
||||||
|
const escapeHtml = (unsafe: string): string => {
|
||||||
return unsafe
|
return unsafe
|
||||||
.replace(/&/g, '&')
|
.replaceAll(/&/g, '&')
|
||||||
.replace(/</g, '<')
|
.replaceAll(/</g, '<')
|
||||||
.replace(/>/g, '>')
|
.replaceAll(/>/g, '>')
|
||||||
.replace(/"/g, '"')
|
.replaceAll(/"/g, '"')
|
||||||
.replace(/'/g, ''')
|
.replaceAll(/'/g, ''')
|
||||||
}
|
}
|
||||||
|
|
||||||
const replaceCode = (code: string): ReactElement[][] => {
|
const replaceCode = (code: string): ReactElement[][] => {
|
||||||
|
@ -69,3 +72,5 @@ export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language
|
||||||
</div>
|
</div>
|
||||||
</Fragment>)
|
</Fragment>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default HighlightedCode
|
||||||
|
|
|
@ -18,7 +18,7 @@ export type LineNumberMarkerOptions = (lineMarkers: LineMarkers[]) => void;
|
||||||
* This plugin adds markers to the dom, that are used to map line numbers to dom elements.
|
* This plugin adds markers to the dom, that are used to map line numbers to dom elements.
|
||||||
* It also provides a list of line numbers for the top level dom elements.
|
* It also provides a list of line numbers for the top level dom elements.
|
||||||
*/
|
*/
|
||||||
export const lineNumberMarker: MarkdownIt.PluginWithOptions<LineNumberMarkerOptions> = (md: MarkdownIt, options) => {
|
export const lineNumberMarker: (options: LineNumberMarkerOptions) => MarkdownIt.PluginSimple = (options) => (md: MarkdownIt) => {
|
||||||
// add app_linemarker token before each opening or self-closing level-0 tag
|
// add app_linemarker token before each opening or self-closing level-0 tag
|
||||||
md.core.ruler.push('line_number_marker', (state) => {
|
md.core.ruler.push('line_number_marker', (state) => {
|
||||||
const lineMarkers: LineMarkers[] = []
|
const lineMarkers: LineMarkers[] = []
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Fragment, useEffect, useRef, useState } from 'react'
|
import React, { Fragment, useEffect, useRef, useState } from 'react'
|
||||||
|
@ -45,7 +45,8 @@ export const MarkmapFrame: React.FC<MarkmapFrameProps> = ({ code }) => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const actualContainer = diagramContainer.current
|
const actualContainer = diagramContainer.current
|
||||||
import('./markmap-loader').then(({ markmapLoader }) => {
|
import(/* webpackChunkName: "markmap" */'./markmap-loader')
|
||||||
|
.then(({ markmapLoader }) => {
|
||||||
try {
|
try {
|
||||||
const svg: SVGSVGElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
const svg: SVGSVGElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||||
svg.setAttribute('width', '100%')
|
svg.setAttribute('width', '100%')
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
@ -27,7 +27,8 @@ export const MermaidChart: React.FC<MermaidChartProps> = ({ code }) => {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mermaidInitialized) {
|
if (!mermaidInitialized) {
|
||||||
import('mermaid').then((mermaid) => {
|
import(/* webpackChunkName: "mermaid" */'mermaid')
|
||||||
|
.then((mermaid) => {
|
||||||
mermaid.default.initialize({ startOnLoad: false })
|
mermaid.default.initialize({ startOnLoad: false })
|
||||||
mermaidInitialized = true
|
mermaidInitialized = true
|
||||||
})
|
})
|
||||||
|
@ -51,7 +52,8 @@ export const MermaidChart: React.FC<MermaidChartProps> = ({ code }) => {
|
||||||
if (!diagramContainer.current) {
|
if (!diagramContainer.current) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
import('mermaid').then((mermaid) => {
|
import(/* webpackChunkName: "mermaid" */'mermaid')
|
||||||
|
.then((mermaid) => {
|
||||||
try {
|
try {
|
||||||
if (!diagramContainer.current) {
|
if (!diagramContainer.current) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -8,10 +8,12 @@ import { DomElement } from 'domhandler'
|
||||||
import React, { ReactElement } from 'react'
|
import React, { ReactElement } from 'react'
|
||||||
import { ComponentReplacer } from '../ComponentReplacer'
|
import { ComponentReplacer } from '../ComponentReplacer'
|
||||||
|
|
||||||
|
export type TaskCheckedChangeHandler = (lineInMarkdown: number, checked: boolean) => void
|
||||||
|
|
||||||
export class TaskListReplacer extends ComponentReplacer {
|
export class TaskListReplacer extends ComponentReplacer {
|
||||||
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
|
||||||
|
|
||||||
constructor(onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void) {
|
constructor(onTaskCheckedChange?: TaskCheckedChangeHandler) {
|
||||||
super()
|
super()
|
||||||
this.onTaskCheckedChange = onTaskCheckedChange
|
this.onTaskCheckedChange = onTaskCheckedChange
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
@ -31,7 +31,8 @@ export const VegaChart: React.FC<VegaChartProps> = ({ code }) => {
|
||||||
if (!diagramContainer.current) {
|
if (!diagramContainer.current) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
import(/* webpackChunkName: "vega" */ 'vega-embed').then((embed) => {
|
import(/* webpackChunkName: "vega" */ 'vega-embed')
|
||||||
|
.then((embed) => {
|
||||||
try {
|
try {
|
||||||
if (!diagramContainer.current) {
|
if (!diagramContainer.current) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -15,9 +15,11 @@ import { YamlArrayDeprecationAlert } from '../editor-page/renderer-pane/yaml-arr
|
||||||
import { useSyncedScrolling } from '../editor-page/synced-scroll/hooks/use-synced-scrolling'
|
import { useSyncedScrolling } from '../editor-page/synced-scroll/hooks/use-synced-scrolling'
|
||||||
import { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
import { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
||||||
import { TableOfContents } from '../editor-page/table-of-contents/table-of-contents'
|
import { TableOfContents } from '../editor-page/table-of-contents/table-of-contents'
|
||||||
import { FullMarkdownRenderer } from '../markdown-renderer/full-markdown-renderer'
|
import { BasicMarkdownRenderer } from '../markdown-renderer/basic-markdown-renderer'
|
||||||
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
|
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
|
||||||
import './markdown-document.scss'
|
import './markdown-document.scss'
|
||||||
|
import { useSelector } from 'react-redux'
|
||||||
|
import { ApplicationState } from '../../redux'
|
||||||
|
|
||||||
export interface RendererProps extends ScrollProps {
|
export interface RendererProps extends ScrollProps {
|
||||||
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
onFirstHeadingChange?: (firstHeading: string | undefined) => void
|
||||||
|
@ -53,14 +55,16 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = (
|
||||||
disableToc
|
disableToc
|
||||||
}) => {
|
}) => {
|
||||||
const rendererRef = useRef<HTMLDivElement | null>(null)
|
const rendererRef = useRef<HTMLDivElement | null>(null)
|
||||||
const internalDocumentRenderPaneRef = useRef<HTMLDivElement>(null)
|
|
||||||
const [tocAst, setTocAst] = useState<TocAst>()
|
|
||||||
|
|
||||||
const internalDocumentRenderPaneSize = useResizeObserver({ ref: internalDocumentRenderPaneRef.current })
|
|
||||||
const rendererSize = useResizeObserver({ ref: rendererRef.current })
|
const rendererSize = useResizeObserver({ ref: rendererRef.current })
|
||||||
|
|
||||||
|
const internalDocumentRenderPaneRef = useRef<HTMLDivElement>(null)
|
||||||
|
const internalDocumentRenderPaneSize = useResizeObserver({ ref: internalDocumentRenderPaneRef.current })
|
||||||
const containerWidth = internalDocumentRenderPaneSize.width ?? 0
|
const containerWidth = internalDocumentRenderPaneSize.width ?? 0
|
||||||
|
|
||||||
|
const [tocAst, setTocAst] = useState<TocAst>()
|
||||||
|
|
||||||
|
const useAlternativeBreaks = useSelector((state: ApplicationState) => state.noteDetails.frontmatter.breaks)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!onHeightChange) {
|
if (!onHeightChange) {
|
||||||
return
|
return
|
||||||
|
@ -77,9 +81,9 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = (
|
||||||
<div className={ 'markdown-document-side' }/>
|
<div className={ 'markdown-document-side' }/>
|
||||||
<div className={ 'markdown-document-content' }>
|
<div className={ 'markdown-document-content' }>
|
||||||
<YamlArrayDeprecationAlert/>
|
<YamlArrayDeprecationAlert/>
|
||||||
<FullMarkdownRenderer
|
<BasicMarkdownRenderer
|
||||||
rendererRef={ rendererRef }
|
outerContainerRef={ rendererRef }
|
||||||
className={ `flex-fill mb-3 ${ additionalRendererClasses ?? '' }` }
|
className={ `mb-3 ${ additionalRendererClasses ?? '' }` }
|
||||||
content={ markdownContent }
|
content={ markdownContent }
|
||||||
onFirstHeadingChange={ onFirstHeadingChange }
|
onFirstHeadingChange={ onFirstHeadingChange }
|
||||||
onLineMarkerPositionChanged={ onLineMarkerPositionChanged }
|
onLineMarkerPositionChanged={ onLineMarkerPositionChanged }
|
||||||
|
@ -87,7 +91,8 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = (
|
||||||
onTaskCheckedChange={ onTaskCheckedChange }
|
onTaskCheckedChange={ onTaskCheckedChange }
|
||||||
onTocChange={ setTocAst }
|
onTocChange={ setTocAst }
|
||||||
baseUrl={ baseUrl }
|
baseUrl={ baseUrl }
|
||||||
onImageClick={ onImageClick }/>
|
onImageClick={ onImageClick }
|
||||||
|
useAlternativeBreaks={ useAlternativeBreaks }/>
|
||||||
</div>
|
</div>
|
||||||
<div className={ 'markdown-document-side pt-4' }>
|
<div className={ 'markdown-document-side pt-4' }>
|
||||||
<ShowIf condition={ !!tocAst && !disableToc }>
|
<ShowIf condition={ !!tocAst && !disableToc }>
|
||||||
|
|
|
@ -78,7 +78,6 @@ export const RenderPage: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<MarkdownDocument
|
<MarkdownDocument
|
||||||
additionalOuterContainerClasses={ 'vh-100 bg-light' }
|
additionalOuterContainerClasses={ 'vh-100 bg-light' }
|
||||||
additionalRendererClasses={ 'mb-3' }
|
|
||||||
markdownContent={ markdownContent }
|
markdownContent={ markdownContent }
|
||||||
onTaskCheckedChange={ onTaskCheckedChange }
|
onTaskCheckedChange={ onTaskCheckedChange }
|
||||||
onFirstHeadingChange={ onFirstHeadingChange }
|
onFirstHeadingChange={ onFirstHeadingChange }
|
||||||
|
|
|
@ -93,7 +93,8 @@ export type RendererToEditorIframeMessage =
|
||||||
|
|
||||||
export enum RendererType {
|
export enum RendererType {
|
||||||
DOCUMENT,
|
DOCUMENT,
|
||||||
INTRO
|
INTRO,
|
||||||
|
SLIDESHOW
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseConfiguration {
|
export interface BaseConfiguration {
|
||||||
|
|
|
@ -11,7 +11,6 @@ import { BrowserRouter as Router, Redirect, Route, Switch } from 'react-router-d
|
||||||
import { ApplicationLoader } from './components/application-loader/application-loader'
|
import { ApplicationLoader } from './components/application-loader/application-loader'
|
||||||
import { NotFoundErrorScreen } from './components/common/routing/not-found-error-screen'
|
import { NotFoundErrorScreen } from './components/common/routing/not-found-error-screen'
|
||||||
import { Redirector } from './components/common/routing/redirector'
|
import { Redirector } from './components/common/routing/redirector'
|
||||||
import { DocumentReadOnlyPage } from './components/document-read-only-page/document-read-only-page'
|
|
||||||
import { ErrorBoundary } from './components/error-boundary/error-boundary'
|
import { ErrorBoundary } from './components/error-boundary/error-boundary'
|
||||||
import { HistoryPage } from './components/history-page/history-page'
|
import { HistoryPage } from './components/history-page/history-page'
|
||||||
import { IntroPage } from './components/intro-page/intro-page'
|
import { IntroPage } from './components/intro-page/intro-page'
|
||||||
|
@ -25,8 +24,9 @@ import './style/dark.scss'
|
||||||
import './style/index.scss'
|
import './style/index.scss'
|
||||||
import { isTestMode } from './utils/is-test-mode'
|
import { isTestMode } from './utils/is-test-mode'
|
||||||
|
|
||||||
const EditorPage = React.lazy(() => import(/* webpackPrefetch: true */ './components/editor-page/editor-page'))
|
const EditorPage = React.lazy(() => import(/* webpackPrefetch: true *//* webpackChunkName: "editor" */ './components/editor-page/editor-page'))
|
||||||
const RenderPage = React.lazy(() => import (/* webpackPrefetch: true */ './components/render-page/render-page'))
|
const RenderPage = React.lazy(() => import (/* webpackPrefetch: true *//* webpackChunkName: "renderPage" */ './components/render-page/render-page'))
|
||||||
|
const DocumentReadOnlyPage = React.lazy(() => import (/* webpackPrefetch: true *//* webpackChunkName: "documentReadOnly" */ './components/document-read-only-page/document-read-only-page'))
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Provider store={ store }>
|
<Provider store={ store }>
|
||||||
|
|
|
@ -34,7 +34,7 @@ export const setNoteDataFromServer = (apiResponse: Note): void => {
|
||||||
export const updateNoteTitleByFirstHeading = (firstHeading?: string): void => {
|
export const updateNoteTitleByFirstHeading = (firstHeading?: string): void => {
|
||||||
store.dispatch({
|
store.dispatch({
|
||||||
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING,
|
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING,
|
||||||
firstHeading: firstHeading ?? ''
|
firstHeading: firstHeading
|
||||||
} as UpdateNoteTitleByFirstHeadingAction)
|
} as UpdateNoteTitleByFirstHeadingAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,11 @@
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import { Reducer } from 'redux'
|
import { Reducer } from 'redux'
|
||||||
import { Note } from '../../api/notes'
|
import { Note } from '../../api/notes'
|
||||||
import { NoteFrontmatter } from '../../components/editor-page/note-frontmatter/note-frontmatter'
|
import {
|
||||||
|
NoteFrontmatter,
|
||||||
|
NoteTextDirection,
|
||||||
|
NoteType
|
||||||
|
} from '../../components/editor-page/note-frontmatter/note-frontmatter'
|
||||||
import {
|
import {
|
||||||
NoteDetails,
|
NoteDetails,
|
||||||
NoteDetailsAction,
|
NoteDetailsAction,
|
||||||
|
@ -40,11 +44,11 @@ export const initialState: NoteDetails = {
|
||||||
deprecatedTagsSyntax: false,
|
deprecatedTagsSyntax: false,
|
||||||
robots: '',
|
robots: '',
|
||||||
lang: 'en',
|
lang: 'en',
|
||||||
dir: 'ltr',
|
dir: NoteTextDirection.LTR,
|
||||||
breaks: true,
|
breaks: true,
|
||||||
GA: '',
|
GA: '',
|
||||||
disqus: '',
|
disqus: '',
|
||||||
type: '',
|
type: NoteType.DOCUMENT,
|
||||||
opengraph: new Map<string, string>()
|
opengraph: new Map<string, string>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,7 +32,7 @@ export interface NoteDetails {
|
||||||
alias: string
|
alias: string
|
||||||
authorship: number[]
|
authorship: number[]
|
||||||
noteTitle: string
|
noteTitle: string
|
||||||
firstHeading: string
|
firstHeading?: string
|
||||||
frontmatter: NoteFrontmatter
|
frontmatter: NoteFrontmatter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ export interface SetNoteDetailsFromServerAction extends NoteDetailsAction {
|
||||||
|
|
||||||
export interface UpdateNoteTitleByFirstHeadingAction extends NoteDetailsAction {
|
export interface UpdateNoteTitleByFirstHeadingAction extends NoteDetailsAction {
|
||||||
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING
|
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING
|
||||||
firstHeading: string
|
firstHeading?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetNoteFrontmatterFromRenderingAction extends NoteDetailsAction {
|
export interface SetNoteFrontmatterFromRenderingAction extends NoteDetailsAction {
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
@import "variables.light";
|
@import "variables.light";
|
||||||
@import "../../node_modules/bootstrap/scss/bootstrap";
|
@import "../../node_modules/bootstrap/scss/bootstrap";
|
||||||
@import '../../node_modules/react-bootstrap-typeahead/css/Typeahead';
|
@import '../../node_modules/react-bootstrap-typeahead/css/Typeahead';
|
||||||
@import "~@fontsource/source-sans-pro/index";
|
@import "../../node_modules/@fontsource/source-sans-pro/index";
|
||||||
@import "fonts/twemoji/twemoji";
|
@import "fonts/twemoji/twemoji";
|
||||||
@import '../../node_modules/fork-awesome/css/fork-awesome.min';
|
@import '../../node_modules/fork-awesome/css/fork-awesome.min';
|
||||||
|
|
||||||
|
@ -24,6 +24,10 @@ body {
|
||||||
background-color: $dark;
|
background-color: $dark;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue