From d9292e4db060aaf50459fb0518e9510d21b99255 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Wed, 17 Feb 2021 22:58:21 +0100 Subject: [PATCH] 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
into 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 --- .../common/simple-alert/simple-alert-props.ts | 9 + .../common/wait-spinner/wait-spinner.tsx | 16 ++ .../ErrorWhileLoadingNoteAlert.tsx | 7 +- .../LoadingNoteAlert.tsx | 7 +- .../document-read-only-page.tsx | 2 + .../app-bar/help-button/cheatsheet-line.tsx | 40 +++ .../app-bar/help-button/cheatsheet.tsx | 40 +-- .../app-bar/help-button/help-button.tsx | 61 +---- .../app-bar/help-button/help-modal.tsx | 69 +++++ .../document-bar/share/share-modal.tsx | 3 +- .../note-frontmatter/note-frontmatter.test.ts | 219 ++++++--------- .../note-frontmatter/note-frontmatter.ts | 255 +++--------------- .../build-react-dom-from-toc-ast.tsx | 52 ++++ .../table-of-contents/table-of-contents.tsx | 54 +--- .../table-of-contents/toc-slugify.ts | 11 + src/components/intro-page/intro-page.tsx | 6 +- .../basic-markdown-renderer.tsx | 105 ++++++-- .../document-length-limit-reached-alert.tsx | 10 +- .../full-markdown-renderer.tsx | 106 -------- ...-creator.ts => use-component-replacers.ts} | 9 +- .../use-convert-markdown-to-react-dom.ts | 10 +- .../hooks/use-extract-first-headline.ts | 12 +- .../hooks/use-on-ref-change.ts | 18 ++ .../hooks/use-post-frontmatter-on-change.ts | 35 --- .../hooks/use-post-toc-ast-on-change.ts | 19 -- .../hooks/use-trimmed-content.ts | 19 ++ .../markdown-renderer/invalid-yaml-alert.tsx | 9 +- .../BasicMarkdownItConfigurator.tsx | 92 ++++++- .../FullMarkdownItConfigurator.tsx | 80 ------ .../MarkdownItConfigurator.tsx | 32 --- .../document-table-of-contents.ts | 13 + .../markdown-it-plugins/document-toc.ts | 4 +- .../markdown-it-plugins/frontmatter.ts | 30 +-- .../markdown-it-plugins/quote-extra.ts | 15 +- .../flow/flowchart/flowchart.tsx | 45 ++-- .../graphviz/graphviz-frame.tsx | 8 +- .../highlighted-code/highlighted-code.scss | 6 +- .../highlighted-code/highlighted-code.tsx | 17 +- .../linemarker/line-number-marker.ts | 2 +- .../markmap/markmap-frame.tsx | 37 +-- .../mermaid/mermaid-chart.tsx | 52 ++-- .../task-list/task-list-replacer.tsx | 4 +- .../vega-lite/vega-chart.tsx | 61 ++--- .../render-page/markdown-document.tsx | 23 +- src/components/render-page/render-page.tsx | 1 - .../render-page/rendering-message.ts | 3 +- src/index.tsx | 6 +- src/redux/note-details/methods.ts | 2 +- src/redux/note-details/reducers.ts | 10 +- src/redux/note-details/types.ts | 4 +- src/style/index.scss | 6 +- 51 files changed, 777 insertions(+), 979 deletions(-) create mode 100644 src/components/common/simple-alert/simple-alert-props.ts create mode 100644 src/components/common/wait-spinner/wait-spinner.tsx create mode 100644 src/components/editor-page/app-bar/help-button/cheatsheet-line.tsx create mode 100644 src/components/editor-page/app-bar/help-button/help-modal.tsx create mode 100644 src/components/editor-page/table-of-contents/build-react-dom-from-toc-ast.tsx create mode 100644 src/components/editor-page/table-of-contents/toc-slugify.ts delete mode 100644 src/components/markdown-renderer/full-markdown-renderer.tsx rename src/components/markdown-renderer/hooks/{use-replacer-instance-list-creator.ts => use-component-replacers.ts} (87%) create mode 100644 src/components/markdown-renderer/hooks/use-on-ref-change.ts delete mode 100644 src/components/markdown-renderer/hooks/use-post-frontmatter-on-change.ts delete mode 100644 src/components/markdown-renderer/hooks/use-post-toc-ast-on-change.ts create mode 100644 src/components/markdown-renderer/hooks/use-trimmed-content.ts delete mode 100644 src/components/markdown-renderer/markdown-it-configurator/FullMarkdownItConfigurator.tsx delete mode 100644 src/components/markdown-renderer/markdown-it-configurator/MarkdownItConfigurator.tsx create mode 100644 src/components/markdown-renderer/markdown-it-plugins/document-table-of-contents.ts 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%; }