diff --git a/src/components/common/simple-alert/simple-alert-props.ts b/src/components/common/simple-alert/simple-alert-props.ts new file mode 100644 index 000000000..10ae63ade --- /dev/null +++ b/src/components/common/simple-alert/simple-alert-props.ts @@ -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 +} diff --git a/src/components/common/wait-spinner/wait-spinner.tsx b/src/components/common/wait-spinner/wait-spinner.tsx new file mode 100644 index 000000000..e1f0f4ced --- /dev/null +++ b/src/components/common/wait-spinner/wait-spinner.tsx @@ -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 ( +
+ +
+ ) +} diff --git a/src/components/document-read-only-page/ErrorWhileLoadingNoteAlert.tsx b/src/components/document-read-only-page/ErrorWhileLoadingNoteAlert.tsx index c24d5c777..8e6f59cbc 100644 --- a/src/components/document-read-only-page/ErrorWhileLoadingNoteAlert.tsx +++ b/src/components/document-read-only-page/ErrorWhileLoadingNoteAlert.tsx @@ -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 = ({ show }) => { +export const ErrorWhileLoadingNoteAlert: React.FC = ({ show }) => { useTranslation() return ( diff --git a/src/components/document-read-only-page/LoadingNoteAlert.tsx b/src/components/document-read-only-page/LoadingNoteAlert.tsx index 2886094af..a11c476fd 100644 --- a/src/components/document-read-only-page/LoadingNoteAlert.tsx +++ b/src/components/document-read-only-page/LoadingNoteAlert.tsx @@ -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 = ({ show }) => { +export const LoadingNoteAlert: React.FC = ({ show }) => { return ( diff --git a/src/components/document-read-only-page/document-read-only-page.tsx b/src/components/document-read-only-page/document-read-only-page.tsx index 64942eeeb..59df0961d 100644 --- a/src/components/document-read-only-page/document-read-only-page.tsx +++ b/src/components/document-read-only-page/document-read-only-page.tsx @@ -65,3 +65,5 @@ export const DocumentReadOnlyPage: React.FC = () => { ) } + +export default DocumentReadOnlyPage diff --git a/src/components/editor-page/app-bar/help-button/cheatsheet-line.tsx b/src/components/editor-page/app-bar/help-button/cheatsheet-line.tsx new file mode 100644 index 000000000..92465fefb --- /dev/null +++ b/src/components/editor-page/app-bar/help-button/cheatsheet-line.tsx @@ -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 = ({ code, onTaskCheckedChange }) => { + const checkboxClick = useCallback((lineInMarkdown: number, newValue: boolean) => { + onTaskCheckedChange(newValue) + }, [onTaskCheckedChange]) + + return ( + + + }> + + + + + + + + + + ) +} diff --git a/src/components/editor-page/app-bar/help-button/cheatsheet.tsx b/src/components/editor-page/app-bar/help-button/cheatsheet.tsx index 850dce7b7..9edfe9ebe 100644 --- a/src/components/editor-page/app-bar/help-button/cheatsheet.tsx +++ b/src/components/editor-page/app-bar/help-button/cheatsheet.tsx @@ -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(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 ( @@ -49,21 +43,13 @@ export const Cheatsheet: React.FC = () => { - { codes.map((code, key) => { - return ( - - - - - ) - }) } + { + codes.map((code) => + ) + }
- - - -
) } + +export default Cheatsheet diff --git a/src/components/editor-page/app-bar/help-button/help-button.tsx b/src/components/editor-page/app-bar/help-button/help-button.tsx index 3084c306d..13400e46f 100644 --- a/src/components/editor-page/app-bar/help-button/help-button.tsx +++ b/src/components/editor-page/app-bar/help-button/help-button.tsx @@ -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.Cheatsheet) - - const tabContent = (): React.ReactElement => { - switch (tab) { - case HelpTabStatus.Cheatsheet: - return () - case HelpTabStatus.Shortcuts: - return () - case HelpTabStatus.Links: - return () - } - } + const onHide = useCallback(() => setShow(false), []) return ( @@ -40,37 +21,7 @@ export const HelpButton: React.FC = () => { onClick={ () => setShow(true) }> - setShow(false) } animation={ true } className='text-dark' size='lg'> - - - - - - - - { tabContent() } - - + ) } diff --git a/src/components/editor-page/app-bar/help-button/help-modal.tsx b/src/components/editor-page/app-bar/help-button/help-modal.tsx new file mode 100644 index 000000000..482808f7b --- /dev/null +++ b/src/components/editor-page/app-bar/help-button/help-modal.tsx @@ -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 = ({ show, onHide }) => { + const [tab, setTab] = useState(HelpTabStatus.Cheatsheet) + const { t } = useTranslation() + + const tabContent = useMemo(() => { + switch (tab) { + case HelpTabStatus.Cheatsheet: + return () + case HelpTabStatus.Shortcuts: + return () + case HelpTabStatus.Links: + return () + } + }, [tab]) + + const tabTitle = useMemo(() => t('editor.documentBar.help') + ' - ' + t(`editor.help.${ tab }`), [t, tab]) + + return ( + + + + { tabContent } + + ) +} diff --git a/src/components/editor-page/document-bar/share/share-modal.tsx b/src/components/editor-page/document-bar/share/share-modal.tsx index 043376d65..7bc4c254d 100644 --- a/src/components/editor-page/document-bar/share/share-modal.tsx +++ b/src/components/editor-page/document-bar/share/share-modal.tsx @@ -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 = ({ show, onHide }) => { - + diff --git a/src/components/editor-page/note-frontmatter/note-frontmatter.test.ts b/src/components/editor-page/note-frontmatter/note-frontmatter.test.ts index 3a22fb237..46b68c194 100644 --- a/src/components/editor-page/note-frontmatter/note-frontmatter.test.ts +++ b/src/components/editor-page/note-frontmatter/note-frontmatter.test.ts @@ -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, expectedFinished: Partial) => { 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() - }) + `) + + expect(noteFrontmatter.opengraph) + .toEqual(new Map()) }) - it('opengraph title only', () => { - testFrontmatter(`--- + it('should parse an opengraph title', () => { + const noteFrontmatter = testFrontmatter(`--- opengraph: title: Testtitle ___ - `, - { - opengraph: { - title: 'Testtitle' - } - }, - { - opengraph: new Map(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(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') }) }) diff --git a/src/components/editor-page/note-frontmatter/note-frontmatter.ts b/src/components/editor-page/note-frontmatter/note-frontmatter.ts index e6d791461..9c1bc80c1 100644 --- a/src/components/editor-page/note-frontmatter/note-frontmatter.ts +++ b/src/components/editor-page/note-frontmatter/note-frontmatter.ts @@ -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 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', diff --git a/src/components/editor-page/table-of-contents/build-react-dom-from-toc-ast.tsx b/src/components/editor-page/table-of-contents/build-react-dom-from-toc-ast.tsx new file mode 100644 index 000000000..617d76901 --- /dev/null +++ b/src/components/editor-page/table-of-contents/build-react-dom-from-toc-ast.tsx @@ -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, + 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 = ( + + 0 }> + { rawName } + + 0 }> +
    + { + toc.c.map(child => + (buildReactDomFromTocAst(child, levelsToShowUnderThis - 1, headerCounts, true, baseUrl))) + } +
+
+
+ ) + + if (wrapInListItem) { + return ( +
  • + { content } +
  • + ) + } else { + return content + } +} diff --git a/src/components/editor-page/table-of-contents/table-of-contents.tsx b/src/components/editor-page/table-of-contents/table-of-contents.tsx index 5956205f8..f00b9870b 100644 --- a/src/components/editor-page/table-of-contents/table-of-contents.tsx +++ b/src/components/editor-page/table-of-contents/table-of-contents.tsx @@ -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, - 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 = ( - - 0 }> - { rawName } - - 0 }> -
      - { - toc.c.map(child => - (convertLevel(child, levelsToShowUnderThis - 1, headerCounts, true, baseUrl))) - } -
    -
    -
    - ) - - if (wrapInListItem) { - return ( -
  • - { content } -
  • - ) - } else { - return content - } -} - export const TableOfContents: React.FC = ({ ast, maxDepth = 3, @@ -72,7 +25,8 @@ export const TableOfContents: React.FC = ({ baseUrl }) => { useTranslation() - const tocTree = useMemo(() => convertLevel(ast, maxDepth, new Map(), false, baseUrl), [ast, maxDepth, + const tocTree = useMemo(() => buildReactDomFromTocAst(ast, maxDepth, new Map(), false, baseUrl), [ast, + maxDepth, baseUrl]) return ( diff --git a/src/components/editor-page/table-of-contents/toc-slugify.ts b/src/components/editor-page/table-of-contents/toc-slugify.ts new file mode 100644 index 000000000..1f514786b --- /dev/null +++ b/src/components/editor-page/table-of-contents/toc-slugify.ts @@ -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, '-')) +} diff --git a/src/components/intro-page/intro-page.tsx b/src/components/intro-page/intro-page.tsx index a16066800..961e120a5 100644 --- a/src/components/intro-page/intro-page.tsx +++ b/src/components/intro-page/intro-page.tsx @@ -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 = () => { - + ComponentReplacer[], - markdownIt: MarkdownIt, - documentReference?: RefObject + 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 + useAlternativeBreaks?: boolean } export const BasicMarkdownRenderer: React.FC = ( { 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() + const markdownBodyRef = useRef(null) + const currentLineMarkers = useRef() + const hasNewYamlError = useRef(false) + const tocAst = useRef() + 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 ( -
    - -
    +
    + + +
    { markdownReactDom }
    ) } + +export default BasicMarkdownRenderer diff --git a/src/components/markdown-renderer/document-length-limit-reached-alert.tsx b/src/components/markdown-renderer/document-length-limit-reached-alert.tsx index e0b910f2b..c5d031124 100644 --- a/src/components/markdown-renderer/document-length-limit-reached-alert.tsx +++ b/src/components/markdown-renderer/document-length-limit-reached-alert.tsx @@ -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 = ({ contentLength }) => { +export const DocumentLengthLimitReachedAlert: React.FC = ({ show }) => { useTranslation() + const maxLength = useSelector((state: ApplicationState) => state.config.maxDocumentLength) return ( - maxLength }> + diff --git a/src/components/markdown-renderer/full-markdown-renderer.tsx b/src/components/markdown-renderer/full-markdown-renderer.tsx deleted file mode 100644 index 173ad5736..000000000 --- a/src/components/markdown-renderer/full-markdown-renderer.tsx +++ /dev/null @@ -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 - baseUrl?: string - onImageClick?: ImageClickHandler -} - -export const FullMarkdownRenderer: React.FC = ( - { - 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() - const firstHeadingRef = useRef() - const documentElement = useRef(null) - const currentLineMarkers = useRef() - usePostFrontmatterOnChange(rawMetaRef.current, firstHeadingRef.current, onFrontmatterChange, onFirstHeadingChange) - useCalculateLineMarkerPosition(documentElement, currentLineMarkers.current, onLineMarkerPositionChanged, documentElement.current?.offsetTop ?? 0) - useExtractFirstHeadline(documentElement, content, onFirstHeadingChange) - - const tocAst = useRef() - 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 ( -
    - - -
    - ) -} diff --git a/src/components/markdown-renderer/hooks/use-replacer-instance-list-creator.ts b/src/components/markdown-renderer/hooks/use-component-replacers.ts similarity index 87% rename from src/components/markdown-renderer/hooks/use-replacer-instance-list-creator.ts rename to src/components/markdown-renderer/hooks/use-component-replacers.ts index 87ac97716..80bbef93a 100644 --- a/src/components/markdown-renderer/hooks/use-replacer-instance-list-creator.ts +++ b/src/components/markdown-renderer/hooks/use-component-replacers.ts @@ -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(), diff --git a/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts b/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts index 33eb31ed2..16bb38350 100644 --- a/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts +++ b/src/components/markdown-renderer/hooks/use-convert-markdown-to-react-dom.ts @@ -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() @@ -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]) } diff --git a/src/components/markdown-renderer/hooks/use-extract-first-headline.ts b/src/components/markdown-renderer/hooks/use-extract-first-headline.ts index 7b14319e6..7af4bcb58 100644 --- a/src/components/markdown-renderer/hooks/use-extract-first-headline.ts +++ b/src/components/markdown-renderer/hooks/use-extract-first-headline.ts @@ -12,12 +12,12 @@ export const useExtractFirstHeadline = (documentElement: React.RefObject 0) { node.childNodes.forEach((child) => { innerText += extractInnerText(child) @@ -37,11 +37,11 @@ export const useExtractFirstHeadline = (documentElement: React.RefObject(reference: MutableRefObject, onChange?: (newValue?: T) => void): void => { + const lastValue = useRef() + useEffect(() => { + if (onChange && !equal(reference, lastValue.current)) { + lastValue.current = reference.current + onChange(reference.current) + } + }) +} diff --git a/src/components/markdown-renderer/hooks/use-post-frontmatter-on-change.ts b/src/components/markdown-renderer/hooks/use-post-frontmatter-on-change.ts deleted file mode 100644 index 5875f105b..000000000 --- a/src/components/markdown-renderer/hooks/use-post-frontmatter-on-change.ts +++ /dev/null @@ -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() - const oldFirstHeadingRef = useRef() - - 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 - } - }) -} diff --git a/src/components/markdown-renderer/hooks/use-post-toc-ast-on-change.ts b/src/components/markdown-renderer/hooks/use-post-toc-ast-on-change.ts deleted file mode 100644 index 6db684875..000000000 --- a/src/components/markdown-renderer/hooks/use-post-toc-ast-on-change.ts +++ /dev/null @@ -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, onTocChange?: (ast: TocAst) => void): void => { - const lastTocAst = useRef() - useEffect(() => { - if (onTocChange && tocAst.current && !equal(tocAst, lastTocAst.current)) { - lastTocAst.current = tocAst.current - onTocChange(tocAst.current) - } - }) -} diff --git a/src/components/markdown-renderer/hooks/use-trimmed-content.ts b/src/components/markdown-renderer/hooks/use-trimmed-content.ts new file mode 100644 index 000000000..075c77f7f --- /dev/null +++ b/src/components/markdown-renderer/hooks/use-trimmed-content.ts @@ -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] +} diff --git a/src/components/markdown-renderer/invalid-yaml-alert.tsx b/src/components/markdown-renderer/invalid-yaml-alert.tsx index 3b7015a42..a0842eff4 100644 --- a/src/components/markdown-renderer/invalid-yaml-alert.tsx +++ b/src/components/markdown-renderer/invalid-yaml-alert.tsx @@ -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 = ({ showYamlError }) => { +export const InvalidYamlAlert: React.FC = ({ show }) => { useTranslation() return ( - + diff --git a/src/components/markdown-renderer/markdown-it-configurator/BasicMarkdownItConfigurator.tsx b/src/components/markdown-renderer/markdown-it-configurator/BasicMarkdownItConfigurator.tsx index 9d450ccd2..9633581e5 100644 --- a/src/components/markdown-renderer/markdown-it-configurator/BasicMarkdownItConfigurator.tsx +++ b/src/components/markdown-renderer/markdown-it-configurator/BasicMarkdownItConfigurator.tsx @@ -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 { + 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 diff --git a/src/components/markdown-renderer/markdown-it-configurator/FullMarkdownItConfigurator.tsx b/src/components/markdown-renderer/markdown-it-configurator/FullMarkdownItConfigurator.tsx deleted file mode 100644 index 52aa7ecd9..000000000 --- a/src/components/markdown-renderer/markdown-it-configurator/FullMarkdownItConfigurator.tsx +++ /dev/null @@ -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)) - ) - } - } -} diff --git a/src/components/markdown-renderer/markdown-it-configurator/MarkdownItConfigurator.tsx b/src/components/markdown-renderer/markdown-it-configurator/MarkdownItConfigurator.tsx deleted file mode 100644 index 51e5e3a1f..000000000 --- a/src/components/markdown-renderer/markdown-it-configurator/MarkdownItConfigurator.tsx +++ /dev/null @@ -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; -} diff --git a/src/components/markdown-renderer/markdown-it-plugins/document-table-of-contents.ts b/src/components/markdown-renderer/markdown-it-plugins/document-table-of-contents.ts new file mode 100644 index 000000000..b7803a030 --- /dev/null +++ b/src/components/markdown-renderer/markdown-it-plugins/document-table-of-contents.ts @@ -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) +} diff --git a/src/components/markdown-renderer/markdown-it-plugins/document-toc.ts b/src/components/markdown-renderer/markdown-it-plugins/document-toc.ts index 197c9ddd2..b09c6acd1 100644 --- a/src/components/markdown-renderer/markdown-it-plugins/document-toc.ts +++ b/src/components/markdown-renderer/markdown-it-plugins/document-toc.ts @@ -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 callback: (code: string, ast: TocAst): void => { onToc(ast) }, - slugify: slugify + slugify: tocSlugify }) } diff --git a/src/components/markdown-renderer/markdown-it-plugins/frontmatter.ts b/src/components/markdown-renderer/markdown-it-plugins/frontmatter.ts index 621387e5f..509881dde 100644 --- a/src/components/markdown-renderer/markdown-it-plugins/frontmatter.ts +++ b/src/components/markdown-renderer/markdown-it-plugins/frontmatter.ts @@ -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 = (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) - } - }) -} diff --git a/src/components/markdown-renderer/markdown-it-plugins/quote-extra.ts b/src/components/markdown-renderer/markdown-it-plugins/quote-extra.ts index 685e182c7..af63eb683 100644 --- a/src/components/markdown-renderer/markdown-it-plugins/quote-extra.ts +++ b/src/components/markdown-renderer/markdown-it-plugins/quote-extra.ts @@ -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 diff --git a/src/components/markdown-renderer/replace-components/flow/flowchart/flowchart.tsx b/src/components/markdown-renderer/replace-components/flow/flowchart/flowchart.tsx index d85688012..d24b68f6e 100644 --- a/src/components/markdown-renderer/replace-components/flow/flowchart/flowchart.tsx +++ b/src/components/markdown-renderer/replace-components/flow/flowchart/flowchart.tsx @@ -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 = ({ 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) diff --git a/src/components/markdown-renderer/replace-components/graphviz/graphviz-frame.tsx b/src/components/markdown-renderer/replace-components/graphviz/graphviz-frame.tsx index 5ee7e2203..b3024d2a0 100644 --- a/src/components/markdown-renderer/replace-components/graphviz/graphviz-frame.tsx +++ b/src/components/markdown-renderer/replace-components/graphviz/graphviz-frame.tsx @@ -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 = ({ code }) => { } const actualContainer = container.current - import('@hpcc-js/wasm') + import(/* webpackChunkName: "d3-graphviz" */'@hpcc-js/wasm') .then((wasmPlugin) => { wasmPlugin.wasmFolder('/static/js') }) diff --git a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.scss b/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.scss index 0f53c417d..cba9bf038 100644 --- a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.scss +++ b/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.scss @@ -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'; diff --git a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx b/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx index a331d1362..a1c389939 100644 --- a/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx +++ b/src/components/markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code.tsx @@ -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, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') + .replaceAll(/&/g, '&') + .replaceAll(//g, '>') + .replaceAll(/"/g, '"') + .replaceAll(/'/g, ''') } const replaceCode = (code: string): ReactElement[][] => { @@ -69,3 +72,5 @@ export const HighlightedCode: React.FC = ({ code, language
    ) } + +export default HighlightedCode diff --git a/src/components/markdown-renderer/replace-components/linemarker/line-number-marker.ts b/src/components/markdown-renderer/replace-components/linemarker/line-number-marker.ts index 99415e2f7..440ea6692 100644 --- a/src/components/markdown-renderer/replace-components/linemarker/line-number-marker.ts +++ b/src/components/markdown-renderer/replace-components/linemarker/line-number-marker.ts @@ -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 = (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[] = [] diff --git a/src/components/markdown-renderer/replace-components/markmap/markmap-frame.tsx b/src/components/markdown-renderer/replace-components/markmap/markmap-frame.tsx index d22c18738..dfba34126 100644 --- a/src/components/markdown-renderer/replace-components/markmap/markmap-frame.tsx +++ b/src/components/markdown-renderer/replace-components/markmap/markmap-frame.tsx @@ -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 = ({ 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 ( diff --git a/src/components/markdown-renderer/replace-components/mermaid/mermaid-chart.tsx b/src/components/markdown-renderer/replace-components/mermaid/mermaid-chart.tsx index 4c53cf7ce..5c31ae6f7 100644 --- a/src/components/markdown-renderer/replace-components/mermaid/mermaid-chart.tsx +++ b/src/components/markdown-renderer/replace-components/mermaid/mermaid-chart.tsx @@ -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 = ({ 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 = ({ 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 diff --git a/src/components/markdown-renderer/replace-components/task-list/task-list-replacer.tsx b/src/components/markdown-renderer/replace-components/task-list/task-list-replacer.tsx index 5ca470d57..c0206a60d 100644 --- a/src/components/markdown-renderer/replace-components/task-list/task-list-replacer.tsx +++ b/src/components/markdown-renderer/replace-components/task-list/task-list-replacer.tsx @@ -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 } diff --git a/src/components/markdown-renderer/replace-components/vega-lite/vega-chart.tsx b/src/components/markdown-renderer/replace-components/vega-lite/vega-chart.tsx index 1192513e5..420432a16 100644 --- a/src/components/markdown-renderer/replace-components/vega-lite/vega-chart.tsx +++ b/src/components/markdown-renderer/replace-components/vega-lite/vega-chart.tsx @@ -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 = ({ 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 diff --git a/src/components/render-page/markdown-document.tsx b/src/components/render-page/markdown-document.tsx index 149e43ea7..7b8cdd5c0 100644 --- a/src/components/render-page/markdown-document.tsx +++ b/src/components/render-page/markdown-document.tsx @@ -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 = ( disableToc }) => { const rendererRef = useRef(null) - const internalDocumentRenderPaneRef = useRef(null) - const [tocAst, setTocAst] = useState() - - const internalDocumentRenderPaneSize = useResizeObserver({ ref: internalDocumentRenderPaneRef.current }) const rendererSize = useResizeObserver({ ref: rendererRef.current }) + const internalDocumentRenderPaneRef = useRef(null) + const internalDocumentRenderPaneSize = useResizeObserver({ ref: internalDocumentRenderPaneRef.current }) const containerWidth = internalDocumentRenderPaneSize.width ?? 0 + const [tocAst, setTocAst] = useState() + + const useAlternativeBreaks = useSelector((state: ApplicationState) => state.noteDetails.frontmatter.breaks) + useEffect(() => { if (!onHeightChange) { return @@ -77,9 +81,9 @@ export const MarkdownDocument: React.FC = (
    - = ( onTaskCheckedChange={ onTaskCheckedChange } onTocChange={ setTocAst } baseUrl={ baseUrl } - onImageClick={ onImageClick }/> + onImageClick={ onImageClick } + useAlternativeBreaks={ useAlternativeBreaks }/>
    diff --git a/src/components/render-page/render-page.tsx b/src/components/render-page/render-page.tsx index d0e6d670a..9424311b6 100644 --- a/src/components/render-page/render-page.tsx +++ b/src/components/render-page/render-page.tsx @@ -78,7 +78,6 @@ export const RenderPage: React.FC = () => { return ( 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( diff --git a/src/redux/note-details/methods.ts b/src/redux/note-details/methods.ts index 294768086..ec42f8b61 100644 --- a/src/redux/note-details/methods.ts +++ b/src/redux/note-details/methods.ts @@ -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) } diff --git a/src/redux/note-details/reducers.ts b/src/redux/note-details/reducers.ts index a2a6a0aab..6b75fa21d 100644 --- a/src/redux/note-details/reducers.ts +++ b/src/redux/note-details/reducers.ts @@ -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() } } diff --git a/src/redux/note-details/types.ts b/src/redux/note-details/types.ts index 14eb57e61..313c50202 100644 --- a/src/redux/note-details/types.ts +++ b/src/redux/note-details/types.ts @@ -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 { diff --git a/src/style/index.scss b/src/style/index.scss index 516642efd..1220c63d8 100644 --- a/src/style/index.scss +++ b/src/style/index.scss @@ -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%; }