mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-24 20:14:35 -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
|
@ -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
|
||||
*/
|
||||
|
||||
import React, { useMemo } from 'react'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { Table } from 'react-bootstrap'
|
||||
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 { CheatsheetLine } from './cheatsheet-line'
|
||||
|
||||
export const Cheatsheet: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const codes = [
|
||||
const [checked, setChecked] = useState<boolean>(false)
|
||||
const codes = useMemo(() => [
|
||||
`**${ t('editor.editorToolbar.bold') }**`,
|
||||
`*${ t('editor.editorToolbar.italic') }*`,
|
||||
`++${ t('editor.editorToolbar.underline') }++`,
|
||||
|
@ -28,17 +27,12 @@ export const Cheatsheet: React.FC = () => {
|
|||
`> ${ t('editor.editorToolbar.blockquote') }`,
|
||||
`- ${ t('editor.editorToolbar.unorderedList') }`,
|
||||
`1. ${ t('editor.editorToolbar.orderedList') }`,
|
||||
`- [ ] ${ t('editor.editorToolbar.checkList') }`,
|
||||
`- [${ checked ? 'x' : ' ' }] ${ t('editor.editorToolbar.checkList') }`,
|
||||
`[${ t('editor.editorToolbar.link') }](https://example.com)`,
|
||||
``,
|
||||
``,
|
||||
':smile:',
|
||||
`:::info\n${ t('editor.help.cheatsheet.exampleAlert') }\n:::`
|
||||
]
|
||||
|
||||
const markdownIt = useMemo(() => {
|
||||
return new BasicMarkdownItConfigurator()
|
||||
.buildConfiguredMarkdownIt()
|
||||
}, [])
|
||||
], [checked, t])
|
||||
|
||||
return (
|
||||
<Table className="table-condensed table-cheatsheet">
|
||||
|
@ -49,21 +43,13 @@ export const Cheatsheet: React.FC = () => {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ codes.map((code, key) => {
|
||||
return (
|
||||
<tr key={ key }>
|
||||
<td>
|
||||
<BasicMarkdownRenderer
|
||||
content={ code }
|
||||
markdownIt={ markdownIt }/>
|
||||
</td>
|
||||
<td className={ 'markdown-body' }>
|
||||
<HighlightedCode code={ code } wrapLines={ true } startLineNumber={ 1 } language={ 'markdown' }/>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
}) }
|
||||
{
|
||||
codes.map((code) =>
|
||||
<CheatsheetLine code={ code } key={ code } onTaskCheckedChange={ setChecked }/>)
|
||||
}
|
||||
</tbody>
|
||||
</Table>
|
||||
)
|
||||
}
|
||||
|
||||
export default Cheatsheet
|
||||
|
|
|
@ -4,35 +4,16 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import React, { Fragment, useState } from 'react'
|
||||
import { Button, Modal } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import React, { Fragment, useCallback, useState } from 'react'
|
||||
import { Button } from 'react-bootstrap'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
|
||||
import { Cheatsheet } from './cheatsheet'
|
||||
import { Links } from './links'
|
||||
import { Shortcut } from './shortcuts'
|
||||
|
||||
export enum HelpTabStatus {
|
||||
Cheatsheet = 'cheatsheet.title',
|
||||
Shortcuts = 'shortcuts.title',
|
||||
Links = 'links.title'
|
||||
}
|
||||
import { HelpModal } from './help-modal'
|
||||
|
||||
export const HelpButton: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [show, setShow] = useState(false)
|
||||
const [tab, setTab] = useState<HelpTabStatus>(HelpTabStatus.Cheatsheet)
|
||||
|
||||
const tabContent = (): React.ReactElement => {
|
||||
switch (tab) {
|
||||
case HelpTabStatus.Cheatsheet:
|
||||
return (<Cheatsheet/>)
|
||||
case HelpTabStatus.Shortcuts:
|
||||
return (<Shortcut/>)
|
||||
case HelpTabStatus.Links:
|
||||
return (<Links/>)
|
||||
}
|
||||
}
|
||||
const onHide = useCallback(() => setShow(false), [])
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
|
@ -40,37 +21,7 @@ export const HelpButton: React.FC = () => {
|
|||
onClick={ () => setShow(true) }>
|
||||
<ForkAwesomeIcon icon="question-circle"/>
|
||||
</Button>
|
||||
<Modal show={ show } onHide={ () => setShow(false) } animation={ true } className='text-dark' size='lg'>
|
||||
<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>
|
||||
<HelpModal show={ show } onHide={ onHide }/>
|
||||
</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 { ShowIf } from '../../../common/show-if/show-if'
|
||||
import { EditorPagePathParams } from '../../editor-page'
|
||||
import { NoteType } from '../../note-frontmatter/note-frontmatter'
|
||||
|
||||
export interface ShareModalProps {
|
||||
show: boolean,
|
||||
|
@ -39,7 +40,7 @@ export const ShareModal: React.FC<ShareModalProps> = ({ show, onHide }) => {
|
|||
<Trans i18nKey={ 'editor.modal.shareLink.editorDescription' }/>
|
||||
<CopyableField content={ `${ baseUrl }/n/${ id }?${ editorMode }` } nativeShareButton={ true }
|
||||
url={ `${ baseUrl }/n/${ id }?${ editorMode }` }/>
|
||||
<ShowIf condition={ noteFrontmatter.type === 'slide' }>
|
||||
<ShowIf condition={ noteFrontmatter.type === NoteType.SLIDE }>
|
||||
<Trans i18nKey={ 'editor.modal.shareLink.slidesDescription' }/>
|
||||
<CopyableField content={ `${ baseUrl }/p/${ id }` } nativeShareButton={ true }
|
||||
url={ `${ baseUrl }/p/${ id }` }/>
|
||||
|
|
|
@ -9,127 +9,101 @@ import MarkdownIt from 'markdown-it'
|
|||
import frontmatter from 'markdown-it-front-matter'
|
||||
import { NoteFrontmatter, RawNoteFrontmatter } from './note-frontmatter'
|
||||
|
||||
describe('yaml frontmatter tests', () => {
|
||||
let raw: RawNoteFrontmatter | undefined
|
||||
let finished: NoteFrontmatter | undefined
|
||||
const md = new MarkdownIt('default', {
|
||||
html: true,
|
||||
breaks: true,
|
||||
langPrefix: '',
|
||||
typographer: true
|
||||
})
|
||||
md.use(frontmatter, (rawMeta: string) => {
|
||||
raw = yaml.load(rawMeta) as RawNoteFrontmatter
|
||||
finished = new NoteFrontmatter(raw)
|
||||
})
|
||||
describe('yaml frontmatter', () => {
|
||||
const testFrontmatter = (input: string): NoteFrontmatter => {
|
||||
let processedFrontmatter: NoteFrontmatter | undefined = undefined
|
||||
const md = new MarkdownIt('default', {
|
||||
html: true,
|
||||
breaks: true,
|
||||
langPrefix: '',
|
||||
typographer: true
|
||||
})
|
||||
md.use(frontmatter, (rawMeta: string) => {
|
||||
const parsedFrontmatter = yaml.load(rawMeta) as RawNoteFrontmatter | undefined
|
||||
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)
|
||||
expect(raw)
|
||||
.not
|
||||
.toBe(undefined)
|
||||
expect(raw)
|
||||
.toEqual(expectedRaw)
|
||||
expect(finished)
|
||||
.not
|
||||
.toBe(undefined)
|
||||
expect(finished)
|
||||
.toEqual({
|
||||
...defaultYAML,
|
||||
...expectedFinished
|
||||
})
|
||||
|
||||
if (processedFrontmatter === undefined) {
|
||||
fail('NoteFrontmatter is undefined')
|
||||
}
|
||||
|
||||
return processedFrontmatter
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
raw = undefined
|
||||
finished = undefined
|
||||
})
|
||||
|
||||
it('title only', () => {
|
||||
testFrontmatter(`---
|
||||
it('should parse "title"', () => {
|
||||
const noteFrontmatter = testFrontmatter(`---
|
||||
title: test
|
||||
___
|
||||
`,
|
||||
{
|
||||
title: 'test'
|
||||
},
|
||||
{
|
||||
title: 'test'
|
||||
})
|
||||
`)
|
||||
|
||||
expect(noteFrontmatter.title)
|
||||
.toEqual('test')
|
||||
})
|
||||
|
||||
it('robots only', () => {
|
||||
testFrontmatter(`---
|
||||
it('should parse "robots"', () => {
|
||||
const noteFrontmatter = testFrontmatter(`---
|
||||
robots: index, follow
|
||||
___
|
||||
`,
|
||||
{
|
||||
robots: 'index, follow'
|
||||
},
|
||||
{
|
||||
robots: 'index, follow'
|
||||
})
|
||||
`)
|
||||
|
||||
expect(noteFrontmatter.robots)
|
||||
.toEqual('index, follow')
|
||||
})
|
||||
|
||||
it('tags only (old syntax)', () => {
|
||||
testFrontmatter(`---
|
||||
it('should parse the deprecated tags syntax', () => {
|
||||
const noteFrontmatter = testFrontmatter(`---
|
||||
tags: test123, abc
|
||||
___
|
||||
`,
|
||||
{
|
||||
tags: 'test123, abc'
|
||||
},
|
||||
{
|
||||
tags: ['test123', 'abc'],
|
||||
deprecatedTagsSyntax: true
|
||||
})
|
||||
`)
|
||||
|
||||
expect(noteFrontmatter.tags)
|
||||
.toEqual(['test123', 'abc'])
|
||||
expect(noteFrontmatter.deprecatedTagsSyntax)
|
||||
.toEqual(true)
|
||||
})
|
||||
|
||||
it('tags only', () => {
|
||||
testFrontmatter(`---
|
||||
it('should parse the tags list syntax', () => {
|
||||
const noteFrontmatter = testFrontmatter(`---
|
||||
tags:
|
||||
- test123
|
||||
- abc
|
||||
___
|
||||
`,
|
||||
{
|
||||
tags: ['test123', 'abc']
|
||||
},
|
||||
{
|
||||
tags: ['test123', 'abc'],
|
||||
deprecatedTagsSyntax: false
|
||||
})
|
||||
`)
|
||||
|
||||
expect(noteFrontmatter.tags)
|
||||
.toEqual(['test123', 'abc'])
|
||||
expect(noteFrontmatter.deprecatedTagsSyntax)
|
||||
.toEqual(false)
|
||||
})
|
||||
|
||||
it('tags only (alternative syntax)', () => {
|
||||
testFrontmatter(`---
|
||||
it('should parse the tag inline-list syntax', () => {
|
||||
const noteFrontmatter = testFrontmatter(`---
|
||||
tags: ['test123', 'abc']
|
||||
___
|
||||
`,
|
||||
{
|
||||
tags: ['test123', 'abc']
|
||||
},
|
||||
{
|
||||
tags: ['test123', 'abc'],
|
||||
deprecatedTagsSyntax: false
|
||||
})
|
||||
`)
|
||||
|
||||
expect(noteFrontmatter.tags)
|
||||
.toEqual(['test123', 'abc'])
|
||||
expect(noteFrontmatter.deprecatedTagsSyntax)
|
||||
.toEqual(false)
|
||||
})
|
||||
|
||||
it('breaks only', () => {
|
||||
testFrontmatter(`---
|
||||
it('should parse "breaks"', () => {
|
||||
const noteFrontmatter = testFrontmatter(`---
|
||||
breaks: false
|
||||
___
|
||||
`,
|
||||
{
|
||||
breaks: false
|
||||
},
|
||||
{
|
||||
breaks: false
|
||||
})
|
||||
`)
|
||||
|
||||
expect(noteFrontmatter.breaks)
|
||||
.toEqual(false)
|
||||
})
|
||||
|
||||
/*
|
||||
|
@ -191,56 +165,41 @@ describe('yaml frontmatter tests', () => {
|
|||
})
|
||||
*/
|
||||
|
||||
it('opengraph nothing', () => {
|
||||
testFrontmatter(`---
|
||||
it('should parse an empty opengraph object', () => {
|
||||
const noteFrontmatter = testFrontmatter(`---
|
||||
opengraph:
|
||||
___
|
||||
`,
|
||||
{
|
||||
opengraph: null
|
||||
},
|
||||
{
|
||||
opengraph: new Map<string, string>()
|
||||
})
|
||||
`)
|
||||
|
||||
expect(noteFrontmatter.opengraph)
|
||||
.toEqual(new Map<string, string>())
|
||||
})
|
||||
|
||||
it('opengraph title only', () => {
|
||||
testFrontmatter(`---
|
||||
it('should parse an opengraph title', () => {
|
||||
const noteFrontmatter = testFrontmatter(`---
|
||||
opengraph:
|
||||
title: Testtitle
|
||||
___
|
||||
`,
|
||||
{
|
||||
opengraph: {
|
||||
title: 'Testtitle'
|
||||
}
|
||||
},
|
||||
{
|
||||
opengraph: new Map<string, string>(Object.entries({ title: 'Testtitle' }))
|
||||
})
|
||||
`)
|
||||
|
||||
expect(noteFrontmatter.opengraph.get('title'))
|
||||
.toEqual('Testtitle')
|
||||
})
|
||||
|
||||
it('opengraph more attributes', () => {
|
||||
testFrontmatter(`---
|
||||
it('should opengraph values', () => {
|
||||
const noteFrontmatter = testFrontmatter(`---
|
||||
opengraph:
|
||||
title: Testtitle
|
||||
image: https://dummyimage.com/48.png
|
||||
image:type: image/png
|
||||
___
|
||||
`,
|
||||
{
|
||||
opengraph: {
|
||||
title: 'Testtitle',
|
||||
image: 'https://dummyimage.com/48.png',
|
||||
'image:type': 'image/png'
|
||||
}
|
||||
},
|
||||
{
|
||||
opengraph: new Map<string, string>(Object.entries({
|
||||
title: 'Testtitle',
|
||||
image: 'https://dummyimage.com/48.png',
|
||||
'image:type': 'image/png'
|
||||
}))
|
||||
})
|
||||
`)
|
||||
|
||||
expect(noteFrontmatter.opengraph.get('title'))
|
||||
.toEqual('Testtitle')
|
||||
expect(noteFrontmatter.opengraph.get('image'))
|
||||
.toEqual('https://dummyimage.com/48.png')
|
||||
expect(noteFrontmatter.opengraph.get('image:type'))
|
||||
.toEqual('image/png')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -6,210 +6,6 @@
|
|||
|
||||
// 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 {
|
||||
title: string | undefined
|
||||
description: string | undefined
|
||||
|
@ -225,32 +21,57 @@ export interface RawNoteFrontmatter {
|
|||
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 {
|
||||
title: string
|
||||
description: string
|
||||
tags: string[]
|
||||
deprecatedTagsSyntax: boolean
|
||||
robots: string
|
||||
lang: iso6391
|
||||
dir: 'ltr' | 'rtl'
|
||||
lang: typeof ISO6391[number]
|
||||
dir: NoteTextDirection
|
||||
breaks: boolean
|
||||
GA: string
|
||||
disqus: string
|
||||
type: 'slide' | ''
|
||||
type: NoteType
|
||||
// slideOptions: RevealOptions
|
||||
opengraph: Map<string, string>
|
||||
|
||||
constructor(rawData: RawNoteFrontmatter) {
|
||||
this.title = rawData?.title ?? ''
|
||||
this.description = rawData?.description ?? ''
|
||||
this.robots = rawData?.robots ?? ''
|
||||
this.breaks = rawData?.breaks ?? true
|
||||
this.GA = rawData?.GA ?? ''
|
||||
this.disqus = rawData?.disqus ?? ''
|
||||
|
||||
this.type = (rawData?.type as NoteFrontmatter['type']) ?? ''
|
||||
this.lang = (rawData?.lang as iso6391) ?? 'en'
|
||||
this.dir = (rawData?.dir as NoteFrontmatter['dir']) ?? 'ltr'
|
||||
this.title = rawData.title ?? ''
|
||||
this.description = rawData.description ?? ''
|
||||
this.robots = rawData.robots ?? ''
|
||||
this.breaks = rawData.breaks ?? true
|
||||
this.GA = rawData.GA ?? ''
|
||||
this.disqus = rawData.disqus ?? ''
|
||||
this.lang = (rawData.lang ? ISO6391.find(lang => lang === rawData.lang) : undefined) ?? 'en'
|
||||
this.type = (rawData.type ? Object.values(NoteType)
|
||||
.find(type => type === rawData.type) : undefined) ?? NoteType.DOCUMENT
|
||||
this.dir = (rawData.dir ? Object.values(NoteTextDirection)
|
||||
.find(dir => dir === rawData.dir) : undefined) ?? NoteTextDirection.LTR
|
||||
|
||||
/* this.slideOptions = (rawData?.slideOptions as RevealOptions) ?? {
|
||||
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 React, { Fragment, ReactElement, useMemo } from 'react'
|
||||
import React, { useMemo } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
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'
|
||||
|
||||
export interface TableOfContentsProps {
|
||||
|
@ -18,53 +18,6 @@ export interface TableOfContentsProps {
|
|||
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> = ({
|
||||
ast,
|
||||
maxDepth = 3,
|
||||
|
@ -72,7 +25,8 @@ export const TableOfContents: React.FC<TableOfContentsProps> = ({
|
|||
baseUrl
|
||||
}) => {
|
||||
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])
|
||||
|
||||
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, '-'))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue