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:
Tilman Vatteroth 2021-02-17 22:58:21 +01:00 committed by GitHub
parent 364aec1318
commit d9292e4db0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 777 additions and 979 deletions

View file

@ -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>
)
}

View file

@ -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)`,
`![${ t('editor.editorToolbar.image') }](/icons/mstile-70x70.png)`,
`![${ t('editor.editorToolbar.image') }](/icons/apple-touch-icon.png)`,
':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

View file

@ -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>
)
}

View file

@ -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>)
}

View file

@ -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 }` }/>

View file

@ -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')
})
})

View file

@ -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',

View file

@ -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
}
}

View file

@ -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 (

View 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, '-'))
}