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,9 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface SimpleAlertProps {
show: boolean
}

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

View file

@ -8,12 +8,9 @@ import React from 'react'
import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { ShowIf } from '../common/show-if/show-if'
import { SimpleAlertProps } from '../common/simple-alert/simple-alert-props'
export interface ErrorWhileLoadingNoteAlertProps {
show: boolean
}
export const ErrorWhileLoadingNoteAlert: React.FC<ErrorWhileLoadingNoteAlertProps> = ({ show }) => {
export const ErrorWhileLoadingNoteAlert: React.FC<SimpleAlertProps> = ({ show }) => {
useTranslation()
return (

View file

@ -8,12 +8,9 @@ import React from 'react'
import { Alert } from 'react-bootstrap'
import { Trans } from 'react-i18next'
import { ShowIf } from '../common/show-if/show-if'
import { SimpleAlertProps } from '../common/simple-alert/simple-alert-props'
export interface LoadingNoteAlertProps {
show: boolean
}
export const LoadingNoteAlert: React.FC<LoadingNoteAlertProps> = ({ show }) => {
export const LoadingNoteAlert: React.FC<SimpleAlertProps> = ({ show }) => {
return (
<ShowIf condition={ show }>
<Alert variant={ 'info' } className={ 'my-2' }>

View file

@ -65,3 +65,5 @@ export const DocumentReadOnlyPage: React.FC = () => {
</div>
)
}
export default DocumentReadOnlyPage

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

View file

@ -1,7 +1,7 @@
/*
* 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, useState } from 'react'
@ -17,8 +17,8 @@ import { CoverButtons } from './cover-buttons/cover-buttons'
import { FeatureLinks } from './feature-links'
import { useIntroPageContent } from './hooks/use-intro-page-content'
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 { WaitSpinner } from '../common/wait-spinner/wait-spinner'
export const IntroPage: React.FC = () => {
const introPageContent = useIntroPageContent()
@ -38,7 +38,7 @@ export const IntroPage: React.FC = () => {
</div>
<CoverButtons/>
<ShowIf condition={ showSpinner }>
<ForkAwesomeIcon icon={ 'spinner' } className={ 'fa-spin' }/>
<WaitSpinner/>
</ShowIf>
<RenderIframe
frameClasses={ 'w-100 overflow-y-hidden' }

View file

@ -4,45 +4,112 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import MarkdownIt from 'markdown-it'
import React, { RefObject, useMemo } from 'react'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import React, { Ref, useCallback, useMemo, useRef, useState } from 'react'
import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert'
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
import './markdown-renderer.scss'
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 {
componentReplacers?: () => ComponentReplacer[],
markdownIt: MarkdownIt,
documentReference?: RefObject<HTMLDivElement>
additionalReplacers?: () => ComponentReplacer[],
onBeforeRendering?: () => 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> = (
{
className,
content,
componentReplacers,
markdownIt,
documentReference,
additionalReplacers,
onBeforeRendering,
onAfterRendering
onAfterRendering,
onFirstHeadingChange,
onLineMarkerPositionChanged,
onFrontmatterChange,
onTaskCheckedChange,
onTocChange,
baseUrl,
onImageClick,
outerContainerRef,
useAlternativeBreaks
}) => {
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
const trimmedContent = useMemo(() => content.length > maxLength ? content.substr(0, maxLength) : content, [content,
maxLength])
const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, markdownIt, componentReplacers, onBeforeRendering, onAfterRendering)
const rawMetaRef = useRef<RawNoteFrontmatter>()
const markdownBodyRef = useRef<HTMLDivElement>(null)
const currentLineMarkers = useRef<LineMarkers[]>()
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 (
<div className={ `${ className ?? '' } d-flex flex-column align-items-center` }>
<DocumentLengthLimitReachedAlert contentLength={ content.length }/>
<div ref={ documentReference } className={ 'markdown-body w-100 d-flex flex-column align-items-center' }>
<div ref={ outerContainerRef } className={ 'position-relative' }>
<InvalidYamlAlert show={ showYamlError }/>
<DocumentLengthLimitReachedAlert show={ contentExceedsLimit }/>
<div ref={ markdownBodyRef }
className={ `${ className ?? '' } markdown-body w-100 d-flex flex-column align-items-center` }>
{ markdownReactDom }
</div>
</div>
)
}
export default BasicMarkdownRenderer

View file

@ -10,17 +10,15 @@ import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import { ShowIf } from '../common/show-if/show-if'
import { SimpleAlertProps } from '../common/simple-alert/simple-alert-props'
export interface DocumentLengthLimitReachedAlertProps {
contentLength: number
}
export const DocumentLengthLimitReachedAlert: React.FC<DocumentLengthLimitReachedAlertProps> = ({ contentLength }) => {
export const DocumentLengthLimitReachedAlert: React.FC<SimpleAlertProps> = ({ show }) => {
useTranslation()
const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength)
return (
<ShowIf condition={ contentLength > maxLength }>
<ShowIf condition={ show }>
<Alert variant='danger' dir={ 'auto' } data-cy={ 'limitReachedMessage' }>
<Trans i18nKey={ 'editor.error.limitReached.description' } values={ { maxLength } }/>
</Alert>

View file

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

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useMemo } from 'react'
import { useCallback } from 'react'
import { AbcReplacer } from '../replace-components/abc/abc-replacer'
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
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 { ColoredBlockquoteReplacer } from '../replace-components/colored-blockquote/colored-blockquote-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 { VimeoReplacer } from '../replace-components/vimeo/vimeo-replacer'
import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
export const useReplacerInstanceListCreator = (onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void,
onImageClick?: ImageClickHandler, baseUrl?: string): () => ComponentReplacer[] => useMemo(() =>
() => [
export const useComponentReplacers = (onTaskCheckedChange?: TaskCheckedChangeHandler, onImageClick?: ImageClickHandler, baseUrl?: string): () => ComponentReplacer[] =>
useCallback(() => [
new LinemarkerReplacer(),
new GistReplacer(),
new YoutubeReplacer(),

View file

@ -15,7 +15,8 @@ import { calculateNewLineNumberMapping } from '../utils/line-number-mapping'
export const useConvertMarkdownToReactDom = (
markdownCode: string,
markdownIt: MarkdownIt,
componentReplacers?: () => ComponentReplacer[],
baseReplacers: () => ComponentReplacer[],
additionalReplacers?: () => ComponentReplacer[],
onBeforeRendering?: () => void,
onAfterRendering?: () => void): ReactElement[] => {
const oldMarkdownLineKeys = useRef<LineKeys[]>()
@ -33,11 +34,14 @@ export const useConvertMarkdownToReactDom = (
} = calculateNewLineNumberMapping(contentLines, oldMarkdownLineKeys.current ?? [], lastUsedLineId.current)
oldMarkdownLineKeys.current = newLines
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 })
if (onAfterRendering) {
onAfterRendering()
}
return rendering
}, [onBeforeRendering, onAfterRendering, markdownCode, markdownIt, componentReplacers])
}, [onBeforeRendering, markdownIt, markdownCode, baseReplacers, additionalReplacers, onAfterRendering])
}

View file

@ -12,12 +12,12 @@ export const useExtractFirstHeadline = (documentElement: React.RefObject<HTMLDiv
return ''
}
let innerText = ''
if ((node as HTMLElement).classList?.contains('katex-mathml')) {
return ''
}
let innerText = ''
if (node.childNodes && node.childNodes.length > 0) {
node.childNodes.forEach((child) => {
innerText += extractInnerText(child)
@ -37,11 +37,11 @@ export const useExtractFirstHeadline = (documentElement: React.RefObject<HTMLDiv
const firstHeading = documentElement.current.getElementsByTagName('h1')
.item(0)
const headingText = extractInnerText(firstHeading)
if (headingText === lastFirstHeading.current) {
return
.trim()
if (headingText !== lastFirstHeading.current) {
lastFirstHeading.current = headingText
onFirstHeadingChange(headingText)
}
lastFirstHeading.current = headingText
onFirstHeadingChange(headingText)
}
}, [documentElement, extractInnerText, onFirstHeadingChange, content])
}

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

View file

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

View file

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

View file

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

View file

@ -9,16 +9,13 @@ import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { InternalLink } from '../common/links/internal-link'
import { ShowIf } from '../common/show-if/show-if'
import { SimpleAlertProps } from '../common/simple-alert/simple-alert-props'
export interface InvalidYamlAlertProps {
showYamlError: boolean
}
export const InvalidYamlAlert: React.FC<InvalidYamlAlertProps> = ({ showYamlError }) => {
export const InvalidYamlAlert: React.FC<SimpleAlertProps> = ({ show }) => {
useTranslation()
return (
<ShowIf condition={ showYamlError }>
<ShowIf condition={ show }>
<Alert variant='warning' dir='auto'>
<Trans i18nKey='editor.invalidYaml'>
<InternalLink text='yaml-metadata' href='/n/yaml-metadata' className='text-primary'/>

View file

@ -1,7 +1,7 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
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 { tasksLists } from '../markdown-it-plugins/tasks-lists'
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 {
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,
abbreviation,
definitionList,
@ -35,8 +102,19 @@ export class BasicMarkdownItConfigurator extends MarkdownItConfigurator {
imsize,
tasksLists,
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(
linkifyExtra,
MarkdownItParserDebugger

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@
import MarkdownIt from 'markdown-it'
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
@ -21,6 +21,6 @@ export const documentToc: MarkdownIt.PluginWithOptions<DocumentTocPluginOptions>
callback: (code: string, ast: TocAst): void => {
onToc(ast)
},
slugify: slugify
slugify: tocSlugify
})
}

View file

@ -11,22 +11,20 @@ import { RawNoteFrontmatter } from '../../editor-page/note-frontmatter/note-fron
interface FrontmatterPluginOptions {
onParseError: (error: boolean) => void,
onRawMeta: (rawMeta: RawNoteFrontmatter) => void,
onRawMetaChange: (rawMeta: RawNoteFrontmatter) => void,
}
export const frontmatterExtract: MarkdownIt.PluginWithOptions<FrontmatterPluginOptions> = (markdownIt: MarkdownIt, options) => {
if (!options) {
return
export const frontmatterExtract: (options: FrontmatterPluginOptions) => MarkdownIt.PluginSimple = (options) =>
(markdownIt) => {
frontmatter(markdownIt, (rawMeta: string) => {
try {
const meta: RawNoteFrontmatter = yaml.load(rawMeta) as RawNoteFrontmatter
options.onParseError(false)
options.onRawMetaChange(meta)
} catch (e) {
console.error(e)
options.onParseError(true)
options.onRawMetaChange({} as RawNoteFrontmatter)
}
})
}
frontmatter(markdownIt, (rawMeta: string) => {
try {
const meta: RawNoteFrontmatter = yaml.load(rawMeta) as RawNoteFrontmatter
options.onParseError(false)
options.onRawMeta(meta)
} catch (e) {
console.error(e)
options.onParseError(true)
options.onRawMeta({} as RawNoteFrontmatter)
}
})
}

View file

@ -8,17 +8,12 @@ import MarkdownIt from 'markdown-it/lib'
import Token from 'markdown-it/lib/token'
import { IconName } from '../../common/fork-awesome/types'
export interface QuoteExtraOptions {
quoteLabel: string
icon: IconName
}
export const quoteExtra: (pluginOptions: QuoteExtraOptions) => MarkdownIt.PluginSimple =
(pluginOptions) => (md) => {
md.inline.ruler.push(`extraQuote_${ pluginOptions.quoteLabel }`, (state) => {
export const quoteExtra: (quoteLabel: string, icon: IconName) => MarkdownIt.PluginSimple =
(quoteLabel: string, icon: IconName) => (md) => {
md.inline.ruler.push(`extraQuote_${ quoteLabel }`, (state) => {
const quoteExtraTagValues = parseQuoteExtraTag(state.src, state.pos, state.posMax)
if (!quoteExtraTagValues || quoteExtraTagValues.label !== pluginOptions.quoteLabel) {
if (!quoteExtraTagValues || quoteExtraTagValues.label !== quoteLabel) {
return false
}
state.pos = quoteExtraTagValues.valueEndIndex + 1
@ -32,7 +27,7 @@ export const quoteExtra: (pluginOptions: QuoteExtraOptions) => MarkdownIt.Plugin
)
const token = state.push('quote-extra', '', 0)
token.attrSet('icon', pluginOptions.icon)
token.attrSet('icon', icon)
token.children = tokens
return true

View file

@ -1,7 +1,7 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useEffect, useRef, useState } from 'react'
@ -25,26 +25,25 @@ export const FlowChart: React.FC<FlowChartProps> = ({ code }) => {
return
}
const currentDiagramRef = diagramRef.current
import(/* webpackChunkName: "flowchart.js" */ 'flowchart.js').then((imp) => {
const parserOutput = imp.parse(code)
try {
parserOutput.drawSVG(currentDiagramRef, {
'line-width': 2,
fill: 'none',
'font-size': 16,
'line-color': darkModeActivated ? '#ffffff' : '#000000',
'element-color': darkModeActivated ? '#ffffff' : '#000000',
'font-color': darkModeActivated ? '#ffffff' : '#000000',
'font-family': 'Source Sans Pro, "Twemoji Mozilla", monospace'
})
setError(false)
} catch (error) {
setError(true)
}
})
.catch(() => {
console.error('error while loading flowchart.js')
})
import(/* webpackChunkName: "flowchart.js" */ 'flowchart.js')
.then((imp) => {
const parserOutput = imp.parse(code)
try {
parserOutput.drawSVG(currentDiagramRef, {
'line-width': 2,
fill: 'none',
'font-size': 16,
'line-color': darkModeActivated ? '#ffffff' : '#000000',
'element-color': darkModeActivated ? '#ffffff' : '#000000',
'font-color': darkModeActivated ? '#ffffff' : '#000000',
'font-family': 'Source Sans Pro, "Twemoji Mozilla", monospace'
})
setError(false)
} catch (error) {
setError(true)
}
})
.catch(() => console.error('error while loading flowchart.js'))
return () => {
Array.from(currentDiagramRef.children)

View file

@ -1,7 +1,7 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
@ -32,7 +32,7 @@ export const GraphvizFrame: React.FC<GraphvizFrameProps> = ({ code }) => {
}
const actualContainer = container.current
import('@hpcc-js/wasm')
import(/* webpackChunkName: "d3-graphviz" */'@hpcc-js/wasm')
.then((wasmPlugin) => {
wasmPlugin.wasmFolder('/static/js')
})

View file

@ -1,13 +1,9 @@
/*
/*!
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.markdown-body {
}
.markdown-body {
@import '../../../../../../node_modules/highlight.js/styles/github';

View file

@ -17,13 +17,16 @@ export interface HighlightedCodeProps {
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
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
.replaceAll(/&/g, '&amp;')
.replaceAll(/</g, '&lt;')
.replaceAll(/>/g, '&gt;')
.replaceAll(/"/g, '&quot;')
.replaceAll(/'/g, '&#039;')
}
const replaceCode = (code: string): ReactElement[][] => {
@ -69,3 +72,5 @@ export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language
</div>
</Fragment>)
}
export default HighlightedCode

View file

@ -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.
* 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
md.core.ruler.push('line_number_marker', (state) => {
const lineMarkers: LineMarkers[] = []

View file

@ -1,7 +1,7 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment, useEffect, useRef, useState } from 'react'
@ -45,21 +45,22 @@ export const MarkmapFrame: React.FC<MarkmapFrameProps> = ({ code }) => {
return
}
const actualContainer = diagramContainer.current
import('./markmap-loader').then(({ markmapLoader }) => {
try {
const svg: SVGSVGElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svg.setAttribute('width', '100%')
actualContainer.querySelectorAll('svg')
.forEach(child => child.remove())
actualContainer.appendChild(svg)
markmapLoader(svg, code)
} catch (error) {
console.error(error)
}
})
.catch(() => {
console.error('error while loading markmap')
})
import(/* webpackChunkName: "markmap" */'./markmap-loader')
.then(({ markmapLoader }) => {
try {
const svg: SVGSVGElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
svg.setAttribute('width', '100%')
actualContainer.querySelectorAll('svg')
.forEach(child => child.remove())
actualContainer.appendChild(svg)
markmapLoader(svg, code)
} catch (error) {
console.error(error)
}
})
.catch(() => {
console.error('error while loading markmap')
})
}, [code])
return (

View file

@ -1,7 +1,7 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
@ -27,13 +27,14 @@ export const MermaidChart: React.FC<MermaidChartProps> = ({ code }) => {
useEffect(() => {
if (!mermaidInitialized) {
import('mermaid').then((mermaid) => {
mermaid.default.initialize({ startOnLoad: false })
mermaidInitialized = true
})
.catch(() => {
console.error('error while loading mermaid')
})
import(/* webpackChunkName: "mermaid" */'mermaid')
.then((mermaid) => {
mermaid.default.initialize({ startOnLoad: false })
mermaidInitialized = true
})
.catch(() => {
console.error('error while loading mermaid')
})
}
}, [])
@ -51,22 +52,23 @@ export const MermaidChart: React.FC<MermaidChartProps> = ({ code }) => {
if (!diagramContainer.current) {
return
}
import('mermaid').then((mermaid) => {
try {
if (!diagramContainer.current) {
return
import(/* webpackChunkName: "mermaid" */'mermaid')
.then((mermaid) => {
try {
if (!diagramContainer.current) {
return
}
mermaid.default.parse(code)
delete diagramContainer.current.dataset.processed
diagramContainer.current.textContent = code
mermaid.default.init(diagramContainer.current)
setError(undefined)
} catch (error) {
const message = (error as MermaidParseError).str
showError(message || t('renderer.mermaid.unknownError'))
}
mermaid.default.parse(code)
delete diagramContainer.current.dataset.processed
diagramContainer.current.textContent = code
mermaid.default.init(diagramContainer.current)
setError(undefined)
} catch (error) {
const message = (error as MermaidParseError).str
showError(message || t('renderer.mermaid.unknownError'))
}
})
.catch(() => showError('Error while loading mermaid'))
})
.catch(() => showError('Error while loading mermaid'))
}, [code, showError, t])
return <Fragment>

View file

@ -8,10 +8,12 @@ import { DomElement } from 'domhandler'
import React, { ReactElement } from 'react'
import { ComponentReplacer } from '../ComponentReplacer'
export type TaskCheckedChangeHandler = (lineInMarkdown: number, checked: boolean) => void
export class TaskListReplacer extends ComponentReplacer {
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
constructor(onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void) {
constructor(onTaskCheckedChange?: TaskCheckedChangeHandler) {
super()
this.onTaskCheckedChange = onTaskCheckedChange
}

View file

@ -1,7 +1,7 @@
/*
SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
@ -31,34 +31,35 @@ export const VegaChart: React.FC<VegaChartProps> = ({ code }) => {
if (!diagramContainer.current) {
return
}
import(/* webpackChunkName: "vega" */ 'vega-embed').then((embed) => {
try {
if (!diagramContainer.current) {
return
}
const spec = JSON.parse(code) as VisualizationSpec
embed.default(diagramContainer.current, spec, {
actions: {
export: true,
source: false,
compiled: false,
editor: false
},
i18n: {
PNG_ACTION: t('renderer.vega-lite.png'),
SVG_ACTION: t('renderer.vega-lite.svg')
import(/* webpackChunkName: "vega" */ 'vega-embed')
.then((embed) => {
try {
if (!diagramContainer.current) {
return
}
})
.then(() => setError(undefined))
.catch(err => showError(err))
} catch (err) {
showError(t('renderer.vega-lite.errorJson'))
}
})
.catch(() => {
console.error('error while loading vega-light')
})
const spec = JSON.parse(code) as VisualizationSpec
embed.default(diagramContainer.current, spec, {
actions: {
export: true,
source: false,
compiled: false,
editor: false
},
i18n: {
PNG_ACTION: t('renderer.vega-lite.png'),
SVG_ACTION: t('renderer.vega-lite.svg')
}
})
.then(() => setError(undefined))
.catch(err => showError(err))
} catch (err) {
showError(t('renderer.vega-lite.errorJson'))
}
})
.catch(() => {
console.error('error while loading vega-light')
})
}, [code, showError, t])
return <Fragment>

View file

@ -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 { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
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 './markdown-document.scss'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
export interface RendererProps extends ScrollProps {
onFirstHeadingChange?: (firstHeading: string | undefined) => void
@ -53,14 +55,16 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = (
disableToc
}) => {
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 internalDocumentRenderPaneRef = useRef<HTMLDivElement>(null)
const internalDocumentRenderPaneSize = useResizeObserver({ ref: internalDocumentRenderPaneRef.current })
const containerWidth = internalDocumentRenderPaneSize.width ?? 0
const [tocAst, setTocAst] = useState<TocAst>()
const useAlternativeBreaks = useSelector((state: ApplicationState) => state.noteDetails.frontmatter.breaks)
useEffect(() => {
if (!onHeightChange) {
return
@ -77,9 +81,9 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = (
<div className={ 'markdown-document-side' }/>
<div className={ 'markdown-document-content' }>
<YamlArrayDeprecationAlert/>
<FullMarkdownRenderer
rendererRef={ rendererRef }
className={ `flex-fill mb-3 ${ additionalRendererClasses ?? '' }` }
<BasicMarkdownRenderer
outerContainerRef={ rendererRef }
className={ `mb-3 ${ additionalRendererClasses ?? '' }` }
content={ markdownContent }
onFirstHeadingChange={ onFirstHeadingChange }
onLineMarkerPositionChanged={ onLineMarkerPositionChanged }
@ -87,7 +91,8 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = (
onTaskCheckedChange={ onTaskCheckedChange }
onTocChange={ setTocAst }
baseUrl={ baseUrl }
onImageClick={ onImageClick }/>
onImageClick={ onImageClick }
useAlternativeBreaks={ useAlternativeBreaks }/>
</div>
<div className={ 'markdown-document-side pt-4' }>
<ShowIf condition={ !!tocAst && !disableToc }>

View file

@ -78,7 +78,6 @@ export const RenderPage: React.FC = () => {
return (
<MarkdownDocument
additionalOuterContainerClasses={ 'vh-100 bg-light' }
additionalRendererClasses={ 'mb-3' }
markdownContent={ markdownContent }
onTaskCheckedChange={ onTaskCheckedChange }
onFirstHeadingChange={ onFirstHeadingChange }

View file

@ -93,7 +93,8 @@ export type RendererToEditorIframeMessage =
export enum RendererType {
DOCUMENT,
INTRO
INTRO,
SLIDESHOW
}
export interface BaseConfiguration {

View file

@ -11,7 +11,6 @@ import { BrowserRouter as Router, Redirect, Route, Switch } from 'react-router-d
import { ApplicationLoader } from './components/application-loader/application-loader'
import { NotFoundErrorScreen } from './components/common/routing/not-found-error-screen'
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 { HistoryPage } from './components/history-page/history-page'
import { IntroPage } from './components/intro-page/intro-page'
@ -25,8 +24,9 @@ import './style/dark.scss'
import './style/index.scss'
import { isTestMode } from './utils/is-test-mode'
const EditorPage = React.lazy(() => import(/* webpackPrefetch: true */ './components/editor-page/editor-page'))
const RenderPage = React.lazy(() => import (/* webpackPrefetch: true */ './components/render-page/render-page'))
const EditorPage = React.lazy(() => import(/* webpackPrefetch: true *//* webpackChunkName: "editor" */ './components/editor-page/editor-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(
<Provider store={ store }>

View file

@ -34,7 +34,7 @@ export const setNoteDataFromServer = (apiResponse: Note): void => {
export const updateNoteTitleByFirstHeading = (firstHeading?: string): void => {
store.dispatch({
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING,
firstHeading: firstHeading ?? ''
firstHeading: firstHeading
} as UpdateNoteTitleByFirstHeadingAction)
}

View file

@ -7,7 +7,11 @@
import { DateTime } from 'luxon'
import { Reducer } from 'redux'
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 {
NoteDetails,
NoteDetailsAction,
@ -40,11 +44,11 @@ export const initialState: NoteDetails = {
deprecatedTagsSyntax: false,
robots: '',
lang: 'en',
dir: 'ltr',
dir: NoteTextDirection.LTR,
breaks: true,
GA: '',
disqus: '',
type: '',
type: NoteType.DOCUMENT,
opengraph: new Map<string, string>()
}
}

View file

@ -32,7 +32,7 @@ export interface NoteDetails {
alias: string
authorship: number[]
noteTitle: string
firstHeading: string
firstHeading?: string
frontmatter: NoteFrontmatter
}
@ -52,7 +52,7 @@ export interface SetNoteDetailsFromServerAction extends NoteDetailsAction {
export interface UpdateNoteTitleByFirstHeadingAction extends NoteDetailsAction {
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING
firstHeading: string
firstHeading?: string
}
export interface SetNoteFrontmatterFromRenderingAction extends NoteDetailsAction {

View file

@ -8,7 +8,7 @@
@import "variables.light";
@import "../../node_modules/bootstrap/scss/bootstrap";
@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 '../../node_modules/fork-awesome/css/fork-awesome.min';
@ -24,6 +24,10 @@ body {
background-color: $dark;
}
#root {
height: 100vh;
}
html {
height: 100%;
}