Reorganize redux and hooks (1/4) (#985)

Signed-off-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
This commit is contained in:
Tilman Vatteroth 2021-02-01 22:55:49 +01:00 committed by GitHub
parent bdf8110676
commit 1b7abf9f27
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
61 changed files with 898 additions and 986 deletions

View file

@ -18,7 +18,7 @@ SPDX-License-Identifier: CC-BY-SA-4.0
- `{%speakerdeck foobar %}` -> Embedding removed
- `{%pdf https://example.org/example-pdf.pdf %}` -> Embedding removed
- The use of `sequence` as code block language ([Why?](https://hedgedoc.org/faq/))
- Comma-separated definition of tags in the yaml-metadata
- Comma-separated definition of tags in the yaml-frontmatter
### Removed

View file

@ -6,7 +6,7 @@
describe('Autocompletion', () => {
beforeEach(() => {
cy.visit('/n/test')
cy.visitTestEditor()
cy.get('.CodeMirror')
.click()
.get('textarea')

View file

@ -9,7 +9,7 @@ import { branding } from '../support/config'
const title = 'This is a test title'
describe('Document Title', () => {
beforeEach(() => {
cy.visit('/n/test')
cy.visitTestEditor()
cy.get('.btn.active.btn-outline-secondary > i.fa-columns')
.should('exist')
})

View file

@ -6,14 +6,14 @@
describe('Editor mode from URL parameter is used', () => {
it('mode view', () => {
cy.visit('/n/features?view')
cy.visitTestEditor('view')
cy.get('.splitter.left')
.should('have.class', 'd-none')
cy.get('.splitter.right')
.should('not.have.class', 'd-none')
})
it('mode both', () => {
cy.visit('/n/features?both')
cy.visitTestEditor('both')
cy.get('.splitter.left')
.should('not.have.class', 'd-none')
cy.get('.splitter.separator')
@ -22,7 +22,7 @@ describe('Editor mode from URL parameter is used', () => {
.should('not.have.class', 'd-none')
})
it('mode edit', () => {
cy.visit('/n/features?edit')
cy.visitTestEditor('edit')
cy.get('.splitter.left')
.should('not.have.class', 'd-none')
cy.get('.splitter.right')

View file

@ -9,7 +9,7 @@ describe('Export', () => {
const testContent = `---\ntitle: ${testTitle}\n---\nThis is some test content`
beforeEach(() => {
cy.visit('/n/test')
cy.visitTestEditor()
cy.codemirrorFill(testContent)
})

View file

@ -8,7 +8,7 @@ const imageUrl = 'http://example.com/non-existing.png'
describe('File upload', () => {
beforeEach(() => {
cy.visit('/n/test')
cy.visitTestEditor()
})
it('doesn\'t prevent drag\'n\'drop of plain text', () => {

View file

@ -6,7 +6,7 @@
describe('Help Dialog', () => {
beforeEach(() => {
cy.visit('/n/test')
cy.visitTestEditor()
})
it('ToDo-List', () => {

View file

@ -12,7 +12,7 @@ const findHljsCodeBlock = () => {
describe('Code', () => {
beforeEach(() => {
cy.visit('/n/test')
cy.visitTestEditor()
})
describe('with just the language', () => {

View file

@ -6,7 +6,7 @@
describe('Import markdown file', () => {
beforeEach(() => {
cy.visit('/n/test')
cy.visitTestEditor()
})
it('import on blank note', () => {

View file

@ -10,7 +10,7 @@ describe('The status bar text length info', () => {
const tooMuchTestContent = `${dangerTestContent}a`
beforeEach(() => {
cy.visit('/n/test')
cy.visitTestEditor()
})
it('shows the maximal length of the document as number of available characters in the tooltip', () => {

View file

@ -9,7 +9,7 @@ describe('Toolbar Buttons', () => {
const testLink = 'http://hedgedoc.org'
beforeEach(() => {
cy.visit('/n/test')
cy.visitTestEditor()
cy.get('.CodeMirror')
.click()

View file

@ -6,7 +6,7 @@
describe('YAML Array for deprecated syntax of document tags in frontmatter', () => {
beforeEach(() => {
cy.visit('/n/features')
cy.visitTestEditor()
})
it('is shown when using old syntax', () => {

View file

@ -26,3 +26,4 @@ import './config'
import './fill'
import './getMarkdownRenderer'
import './login'
import './visit-test-editor'

View file

@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
declare namespace Cypress {
interface Chainable {
visitTestEditor (query?: string): Chainable<Cypress.AUTWindow>
}
}
export const testNoteId = 'test'
Cypress.Commands.add('visitTestEditor', (query?: string) => {
return cy.visit(`/n/${testNoteId}${query ? `?${query}` : ''}`)
})
beforeEach(() => {
cy.intercept(`/api/v2/notes/${testNoteId}-get`, {
"id": "ABC123",
"alias": "banner",
"lastChange": {
"userId": "test",
"timestamp": 1600033920
},
"viewCount": 0,
"createTime": 1600033920,
"content": "",
"authorship": [],
"preVersionTwoNote": true
})
})

File diff suppressed because one or more lines are too long

View file

@ -15,15 +15,17 @@ export interface Note {
id: string
alias: string
lastChange: LastChange
viewcount: number
createtime: number
viewCount: number
createTime: number
content: string
authorship: number[]
preVersionTwoNote: boolean
}
export const getNote = async (noteId: string): Promise<Note> => {
const response = await fetch(getApiUrl() + `/notes/${noteId}`, {
// The "-get" suffix is necessary, because in our mock api (filesystem) the note id might already be a folder.
// TODO: [mrdrogdrog] replace -get with actual api route as soon as api backend is ready.
const response = await fetch(getApiUrl() + `/notes/${noteId}-get`, {
...defaultFetchConfig
})
expectResponseCode(response)

View file

@ -1,17 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { YAMLMetaData } from '../../editor/yaml-metadata/yaml-metadata'
export const extractNoteTitle = (defaultTitle: string, noteMetadata?: YAMLMetaData, firstHeading?: string): string => {
if (noteMetadata?.title && noteMetadata?.title !== '') {
return noteMetadata.title
} else if (noteMetadata?.opengraph && noteMetadata?.opengraph.get('title') && noteMetadata?.opengraph.get('title') !== '') {
return (noteMetadata?.opengraph.get('title') ?? defaultTitle)
} else {
return (firstHeading ?? defaultTitle).trim()
}
}

View file

@ -1,20 +1,19 @@
/*
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'
import { Alert, Button, Col, ListGroup, Modal, Row } from 'react-bootstrap'
import ReactDiffViewer, { DiffMethod } from 'react-diff-viewer'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useParams } from 'react-router'
import { getAllRevisions, getRevision } from '../../../../api/revisions'
import { Revision, RevisionListEntry } from '../../../../api/revisions/types'
import { UserResponse } from '../../../../api/users/types'
import { useIsDarkModeActivated } from '../../../../hooks/common/use-is-dark-mode-activated'
import { ApplicationState } from '../../../../redux'
import { useNoteMarkdownContent } from '../../../../hooks/common/use-note-markdown-content'
import { CommonModal } from '../../../common/modals/common-modal'
import { ShowIf } from '../../../common/show-if/show-if'
import { RevisionModalListEntry } from './revision-modal-list-entry'
@ -58,7 +57,7 @@ export const RevisionModal: React.FC<PermissionsModalProps> = ({ show, onHide })
}).catch(() => setError(true))
}, [selectedRevisionTimestamp, id])
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
const markdownContent = useNoteMarkdownContent()
return (
<CommonModal show={show} onHide={onHide} titleI18nKey={'editor.modal.revision.title'} icon={'history'} closeButton={true} size={'xl'} additionalClasses='revision-modal'>

View file

@ -7,7 +7,7 @@
import equal from 'fast-deep-equal'
import React from 'react'
import { Modal } from 'react-bootstrap'
import { useTranslation , Trans } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useParams } from 'react-router-dom'
import { useFrontendBaseUrl } from '../../../../hooks/common/use-frontend-base-url'
@ -24,7 +24,7 @@ export interface ShareModalProps {
export const ShareModal: React.FC<ShareModalProps> = ({ show, onHide }) => {
useTranslation()
const noteMetadata = useSelector((state: ApplicationState) => state.documentContent.metadata, equal)
const noteFrontmatter = useSelector((state: ApplicationState) => state.noteDetails.frontmatter, equal)
const editorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
const baseUrl = useFrontendBaseUrl()
const { id } = useParams<EditorPathParams>()
@ -39,11 +39,11 @@ export const ShareModal: React.FC<ShareModalProps> = ({ show, onHide }) => {
<Trans i18nKey={'editor.modal.shareLink.editorDescription'}/>
<CopyableField content={`${baseUrl}/n/${id}?${editorMode}`} nativeShareButton={true}
url={`${baseUrl}/n/${id}?${editorMode}`}/>
<ShowIf condition={noteMetadata.type === 'slide'}>
<ShowIf condition={noteFrontmatter.type === 'slide'}>
<Trans i18nKey={'editor.modal.shareLink.slidesDescription'}/>
<CopyableField content={`${baseUrl}/p/${id}`} nativeShareButton={true} url={`${baseUrl}/p/${id}`}/>
</ShowIf>
<ShowIf condition={noteMetadata.type === ''}>
<ShowIf condition={noteFrontmatter.type === ''}>
<Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'}/>
<CopyableField content={`${baseUrl}/s/${id}`} nativeShareButton={true} url={`${baseUrl}/s/${id}`}/>
</ShowIf>

View file

@ -3,21 +3,24 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import equal from 'fast-deep-equal'
import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useSelector } from 'react-redux'
import { useIsDarkModeActivated } from '../../../hooks/common/use-is-dark-mode-activated'
import { ApplicationState } from '../../../redux'
import { isTestMode } from '../../../utils/is-test-mode'
import { ImageLightboxModal } from '../../markdown-renderer/replace-components/image/image-lightbox-modal'
import { IframeEditorToRendererCommunicator } from '../../render-page/iframe-editor-to-renderer-communicator'
import { ImageDetails } from '../../render-page/rendering-message'
import { ScrollingDocumentRenderPaneProps } from './scrolling-document-render-pane'
import { ScrollState } from '../scroll/scroll-props'
import { DocumentRenderPaneProps } from './document-render-pane'
import { useOnIframeLoad } from './hooks/use-on-iframe-load'
import { ShowOnPropChangeImageLightbox } from './show-on-prop-change-image-lightbox'
export const DocumentIframe: React.FC<ScrollingDocumentRenderPaneProps> = (
export const DocumentIframe: React.FC<DocumentRenderPaneProps> = (
{
markdownContent,
onTaskCheckedChange,
onMetadataChange,
onFrontmatterChange,
scrollState,
onFirstHeadingChange,
wide,
@ -25,40 +28,40 @@ export const DocumentIframe: React.FC<ScrollingDocumentRenderPaneProps> = (
onMakeScrollSource,
extraClasses
}) => {
const frameReference = useRef<HTMLIFrameElement>(null)
const darkMode = useIsDarkModeActivated()
const [rendererReady, setRendererReady] = useState<boolean>(false)
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
const frameReference = useRef<HTMLIFrameElement>(null)
const rendererOrigin = useSelector((state: ApplicationState) => state.config.iframeCommunication.rendererOrigin)
const renderPageUrl = `${rendererOrigin}/render`
const resetRendererReady = useCallback(() => setRendererReady(false), [])
const iframeCommunicator = useMemo(() => new IframeEditorToRendererCommunicator(), [])
const onIframeLoad = useOnIframeLoad(frameReference, iframeCommunicator, rendererOrigin, renderPageUrl, resetRendererReady)
useEffect(() => () => iframeCommunicator.unregisterEventListener(), [iframeCommunicator])
const [rendererReady, setRendererReady] = useState<boolean>(false)
useEffect(() => iframeCommunicator.onFirstHeadingChange(onFirstHeadingChange), [iframeCommunicator, onFirstHeadingChange])
useEffect(() => iframeCommunicator.onMetaDataChange(onMetadataChange), [iframeCommunicator, onMetadataChange])
useEffect(() => iframeCommunicator.onFrontmatterChange(onFrontmatterChange), [iframeCommunicator, onFrontmatterChange])
useEffect(() => iframeCommunicator.onSetScrollState(onScroll), [iframeCommunicator, onScroll])
useEffect(() => iframeCommunicator.onSetScrollSourceToRenderer(onMakeScrollSource), [iframeCommunicator, onMakeScrollSource])
useEffect(() => iframeCommunicator.onTaskCheckboxChange(onTaskCheckedChange), [iframeCommunicator, onTaskCheckedChange])
useEffect(() => iframeCommunicator.onImageClicked(setLightboxDetails), [iframeCommunicator])
useEffect(() => iframeCommunicator.onRendererReady(() => setRendererReady(true)), [darkMode, iframeCommunicator, scrollState, wide])
useEffect(() => {
if (rendererReady) {
iframeCommunicator.sendSetMarkdownContent(markdownContent)
}
}, [iframeCommunicator, markdownContent, rendererReady])
useEffect(() => {
if (rendererReady) {
iframeCommunicator.sendSetDarkmode(darkMode)
}
}, [darkMode, iframeCommunicator, rendererReady])
const oldScrollState = useRef<ScrollState | undefined>(undefined)
useEffect(() => {
if (rendererReady) {
if (rendererReady && !equal(scrollState, oldScrollState.current)) {
oldScrollState.current = scrollState
iframeCommunicator.sendScrollState(scrollState)
}
}, [iframeCommunicator, rendererReady, scrollState])
useEffect(() => {
if (rendererReady) {
iframeCommunicator.sendSetWide(wide ?? false)
@ -69,37 +72,17 @@ export const DocumentIframe: React.FC<ScrollingDocumentRenderPaneProps> = (
if (rendererReady) {
iframeCommunicator.sendSetBaseUrl(window.location.toString())
}
}, [iframeCommunicator, rendererReady,])
}, [iframeCommunicator, rendererReady])
const sendToRenderPage = useRef<boolean>(true)
const onLoad = useCallback(() => {
const frame = frameReference.current
if (!frame || !frame.contentWindow) {
iframeCommunicator.unsetOtherSide()
return
useEffect(() => {
if (rendererReady) {
iframeCommunicator.sendSetMarkdownContent(markdownContent)
}
if (sendToRenderPage.current) {
iframeCommunicator.setOtherSide(frame.contentWindow, rendererOrigin)
sendToRenderPage.current = false
return
} else {
setRendererReady(false)
console.error("Navigated away from unknown URL")
frame.src = renderPageUrl
sendToRenderPage.current = true
}
}, [iframeCommunicator, renderPageUrl, rendererOrigin])
const hideLightbox = useCallback(() => {
setLightboxDetails(undefined)
}, [])
}, [iframeCommunicator, markdownContent, rendererReady])
return <Fragment>
<ImageLightboxModal show={!!lightboxDetails} onHide={hideLightbox} src={lightboxDetails?.src}
alt={lightboxDetails?.alt} title={lightboxDetails?.title}/>
<iframe data-cy={'documentIframe'} onLoad={onLoad} title="render" src={renderPageUrl}
<ShowOnPropChangeImageLightbox details={lightboxDetails}/>
<iframe data-cy={'documentIframe'} onLoad={onIframeLoad} title="render" src={renderPageUrl}
{...isTestMode() ? {} : { sandbox: 'allow-downloads allow-same-origin allow-scripts allow-popups' }}
ref={frameReference} className={`h-100 w-100 border-0 ${extraClasses ?? ''}`}/>
</Fragment>

View file

@ -5,26 +5,23 @@
*/
import { TocAst } from 'markdown-it-toc-done-right'
import React, { MutableRefObject, useCallback, useRef, useState } from 'react'
import React, { MutableRefObject, useMemo, useRef, useState } from 'react'
import { Dropdown } from 'react-bootstrap'
import useResizeObserver from 'use-resize-observer'
import { ForkAwesomeIcon } from '../../common/fork-awesome/fork-awesome-icon'
import { ShowIf } from '../../common/show-if/show-if'
import { FullMarkdownRenderer } from '../../markdown-renderer/full-markdown-renderer'
import { ImageClickHandler } from '../../markdown-renderer/replace-components/image/image-replacer'
import { LineMarkerPosition } from '../../markdown-renderer/types'
import { NoteFrontmatter } from '../note-frontmatter/note-frontmatter'
import { ScrollProps } from '../scroll/scroll-props'
import { TableOfContents } from '../table-of-contents/table-of-contents'
import { YAMLMetaData } from '../yaml-metadata/yaml-metadata'
import { useAdaptedLineMarkerCallback } from './use-adapted-line-markers-callback'
import { useSyncedScrolling } from './hooks/use-synced-scrolling'
import { YamlArrayDeprecationAlert } from './yaml-array-deprecation-alert'
export interface DocumentRenderPaneProps {
export interface DocumentRenderPaneProps extends ScrollProps {
extraClasses?: string
onFirstHeadingChange?: (firstHeading: string | undefined) => void
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
onMetadataChange?: (metaData: YAMLMetaData | undefined) => void
onMouseEnterRenderer?: () => void
onScrollRenderer?: () => void
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
documentRenderPaneRef?: MutableRefObject<HTMLDivElement | null>
wide?: boolean,
@ -37,33 +34,27 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
{
extraClasses,
onFirstHeadingChange,
onLineMarkerPositionChanged,
onMetadataChange,
onMouseEnterRenderer,
onScrollRenderer,
onFrontmatterChange,
onMakeScrollSource,
onTaskCheckedChange,
documentRenderPaneRef,
wide,
baseUrl,
markdownContent,
onImageClick
onImageClick,
onScroll,
scrollState
}) => {
const [tocAst, setTocAst] = useState<TocAst>()
const internalDocumentRenderPaneRef = useRef<HTMLDivElement>()
const { width } = useResizeObserver({ ref: internalDocumentRenderPaneRef.current })
const realWidth = width ?? 0
const rendererRef = useRef<HTMLDivElement | null>(null)
const changeLineMarker = useAdaptedLineMarkerCallback(documentRenderPaneRef, rendererRef, onLineMarkerPositionChanged)
const setContainerReference = useCallback((instance: HTMLDivElement | null) => {
if (documentRenderPaneRef) {
documentRenderPaneRef.current = instance || null
}
internalDocumentRenderPaneRef.current = instance || undefined
}, [documentRenderPaneRef])
const internalDocumentRenderPaneRef = useRef<HTMLDivElement>(null)
const [tocAst, setTocAst] = useState<TocAst>()
const width = useResizeObserver({ ref: internalDocumentRenderPaneRef.current }).width ?? 0
const contentLineCount = useMemo(() => markdownContent.split('\n').length, [markdownContent])
const [onLineMarkerPositionChanged, onUserScroll] = useSyncedScrolling(internalDocumentRenderPaneRef, rendererRef, contentLineCount, scrollState, onScroll)
return (
<div className={`bg-light m-0 pb-5 row ${extraClasses ?? ''}`}
ref={setContainerReference} onScroll={onScrollRenderer} onMouseEnter={onMouseEnterRenderer}>
<div className={`overflow-y-scroll h-100 bg-light m-0 pb-5 row ${extraClasses ?? ''}`}
ref={internalDocumentRenderPaneRef} onScroll={onUserScroll} onMouseEnter={onMakeScrollSource}>
<div className={'col-md d-none d-md-block'}/>
<div className={'bg-light col'}>
<YamlArrayDeprecationAlert/>
@ -73,8 +64,8 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
className={'flex-fill pt-4 mb-3'}
content={markdownContent}
onFirstHeadingChange={onFirstHeadingChange}
onLineMarkerPositionChanged={changeLineMarker}
onMetaDataChange={onMetadataChange}
onLineMarkerPositionChanged={onLineMarkerPositionChanged}
onFrontmatterChange={onFrontmatterChange}
onTaskCheckedChange={onTaskCheckedChange}
onTocChange={(tocAst) => setTocAst(tocAst)}
wide={wide}
@ -85,10 +76,10 @@ export const DocumentRenderPane: React.FC<DocumentRenderPaneProps> = (
<div className={'col-md pt-4'}>
<ShowIf condition={!!tocAst}>
<ShowIf condition={realWidth >= 1280}>
<ShowIf condition={width >= 1280}>
<TableOfContents ast={tocAst as TocAst} className={'sticky'} baseUrl={baseUrl}/>
</ShowIf>
<ShowIf condition={realWidth < 1280}>
<ShowIf condition={width < 1280}>
<div className={'markdown-toc-sidebar-button'}>
<Dropdown drop={'up'}>
<Dropdown.Toggle id="toc-overlay-button" variant={'secondary'} className={'no-arrow'}>

View file

@ -1,11 +1,11 @@
/*
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 { RefObject, useCallback } from 'react'
import { LineMarkerPosition } from '../../markdown-renderer/types'
import { LineMarkerPosition } from '../../../markdown-renderer/types'
export const useAdaptedLineMarkerCallback = (documentRenderPaneRef: RefObject<HTMLDivElement> | undefined,
rendererRef: RefObject<HTMLDivElement>,

View file

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { RefObject, useCallback, useRef } from 'react'
import { IframeEditorToRendererCommunicator } from '../../../render-page/iframe-editor-to-renderer-communicator'
export const useOnIframeLoad = (frameReference: RefObject<HTMLIFrameElement>, iframeCommunicator: IframeEditorToRendererCommunicator,
rendererOrigin: string, renderPageUrl: string, onNavigateAway: () => void): () => void => {
const sendToRenderPage = useRef<boolean>(true)
return useCallback(() => {
const frame = frameReference.current
if (!frame || !frame.contentWindow) {
iframeCommunicator.unsetOtherSide()
return
}
if (sendToRenderPage.current) {
iframeCommunicator.setOtherSide(frame.contentWindow, rendererOrigin)
sendToRenderPage.current = false
return
} else {
onNavigateAway()
console.error("Navigated away from unknown URL")
frame.src = renderPageUrl
sendToRenderPage.current = true
}
}, [frameReference, iframeCommunicator, onNavigateAway, renderPageUrl, rendererOrigin])
}

View file

@ -0,0 +1,38 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useState } from 'react'
import { LineMarkerPosition } from '../../../markdown-renderer/types'
import { useOnUserScroll } from '../../scroll/hooks/use-on-user-scroll'
import { useScrollToLineMark } from '../../scroll/hooks/use-scroll-to-line-mark'
import { ScrollState } from '../../scroll/scroll-props'
export const useSyncedScrolling = (outerContainerRef: React.RefObject<HTMLElement>,
rendererRef: React.RefObject<HTMLElement>,
numberOfLines: number,
scrollState?: ScrollState,
onScroll?: (scrollState: ScrollState) => void): [(lineMarkers: LineMarkerPosition[]) => void, () => void] => {
const [lineMarks, setLineMarks] = useState<LineMarkerPosition[]>()
const onLineMarkerPositionChanged = useCallback((linkMarkerPositions: LineMarkerPosition[]) => {
if (!outerContainerRef.current || !rendererRef.current) {
return
}
const documentRenderPaneTop = (outerContainerRef.current.offsetTop ?? 0)
const rendererTop = (rendererRef.current.offsetTop ?? 0)
const offset = rendererTop - documentRenderPaneTop
const adjustedLineMakerPositions = linkMarkerPositions.map(oldMarker => ({
line: oldMarker.line,
position: oldMarker.position + offset
}))
setLineMarks(adjustedLineMakerPositions)
}, [outerContainerRef, rendererRef])
const onUserScroll = useOnUserScroll(lineMarks, outerContainerRef, onScroll)
useScrollToLineMark(scrollState, lineMarks, numberOfLines, outerContainerRef)
return [onLineMarkerPositionChanged, onUserScroll]
}

View file

@ -1,59 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useMemo, useRef, useState } from 'react'
import { LineMarkerPosition } from '../../markdown-renderer/types'
import { useOnUserScroll } from '../scroll/hooks/use-on-user-scroll'
import { useScrollToLineMark } from '../scroll/hooks/use-scroll-to-line-mark'
import { ScrollProps } from '../scroll/scroll-props'
import { DocumentRenderPane, DocumentRenderPaneProps } from './document-render-pane'
type ImplementedProps =
'onLineMarkerPositionChanged'
| 'onScrollRenderer'
| 'rendererReference'
| 'onMouseEnterRenderer'
export type ScrollingDocumentRenderPaneProps = Omit<(DocumentRenderPaneProps & ScrollProps), ImplementedProps>
export const ScrollingDocumentRenderPane: React.FC<ScrollingDocumentRenderPaneProps> = (
{
scrollState,
wide,
onFirstHeadingChange,
onMakeScrollSource,
onMetadataChange,
onScroll,
onTaskCheckedChange,
markdownContent,
extraClasses,
baseUrl,
onImageClick
}) => {
const renderer = useRef<HTMLDivElement>(null)
const [lineMarks, setLineMarks] = useState<LineMarkerPosition[]>()
const contentLineCount = useMemo(() => markdownContent.split('\n').length, [markdownContent])
useScrollToLineMark(scrollState, lineMarks, contentLineCount, renderer)
const userScroll = useOnUserScroll(lineMarks, renderer, onScroll)
return (
<DocumentRenderPane
extraClasses={`overflow-y-scroll h-100 ${extraClasses || ''}`}
documentRenderPaneRef={renderer}
wide={wide}
onFirstHeadingChange={onFirstHeadingChange}
onLineMarkerPositionChanged={setLineMarks}
onMetadataChange={onMetadataChange}
onMouseEnterRenderer={onMakeScrollSource}
onScrollRenderer={userScroll}
onTaskCheckedChange={onTaskCheckedChange}
markdownContent={markdownContent}
baseUrl={baseUrl}
onImageClick={onImageClick}
/>
)
}

View file

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useEffect, useState } from 'react'
import { ImageLightboxModal } from '../../markdown-renderer/replace-components/image/image-lightbox-modal'
import { ImageDetails } from '../../render-page/rendering-message'
export interface ShowOnPropChangeImageLightboxProps {
details?: ImageDetails
}
export const ShowOnPropChangeImageLightbox: React.FC<ShowOnPropChangeImageLightboxProps> = ({ details }) => {
const [show, setShow] = useState<boolean>(false)
const hideLightbox = useCallback(() => {
setShow(false)
}, [])
useEffect(() => {
if (details) {
setShow(true)
}
}, [details])
return (
<ImageLightboxModal show={show} onHide={hideLightbox} src={details?.src}
alt={details?.alt} title={details?.title}/>
)
}

View file

@ -15,7 +15,7 @@ import { ShowIf } from '../../common/show-if/show-if'
export const YamlArrayDeprecationAlert: React.FC = () => {
useTranslation()
const yamlDeprecatedTags = useSelector((state: ApplicationState) => state.documentContent.metadata.deprecatedTagsSyntax)
const yamlDeprecatedTags = useSelector((state: ApplicationState) => state.noteDetails.frontmatter.deprecatedTagsSyntax)
return <ShowIf condition={yamlDeprecatedTags}>
<Alert data-cy={'yamlArrayDeprecationAlert'} className={'text-wrap'} variant='warning' dir='auto'>

View file

@ -21,7 +21,7 @@ export const handleUpload = (file: File, editor: Editor): void => {
}
const cursor = editor.getCursor()
const uploadPlaceholder = `![${i18n.t('editor.upload.uploadFile', { fileName: file.name })}]()`
const noteId = store.getState().documentContent.noteId
const noteId = store.getState().noteDetails.id
editor.replaceRange(uploadPlaceholder, cursor, cursor, '+input')
uploadFile(noteId, mimeType, file)
.then(({ link }) => {

View file

@ -7,25 +7,32 @@
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useLocation, useParams } from 'react-router'
import { useLocation } from 'react-router'
import useMedia from 'use-media'
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
import { useDocumentTitle } from '../../hooks/common/use-document-title'
import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-title-with-note-title'
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
import { ApplicationState } from '../../redux'
import { setDocumentContent, setDocumentMetadata, setNoteId } from '../../redux/document-content/methods'
import { setEditorMode } from '../../redux/editor/methods'
import { extractNoteTitle } from '../common/document-title/note-title-extractor'
import {
SetCheckboxInMarkdownContent,
setNoteFrontmatter,
setNoteMarkdownContent,
updateNoteTitleByFirstHeading
} from '../../redux/note-details/methods'
import { MotdBanner } from '../common/motd-banner/motd-banner'
import { ShowIf } from '../common/show-if/show-if'
import { ErrorWhileLoadingNoteAlert } from '../pad-view-only/ErrorWhileLoadingNoteAlert'
import { LoadingNoteAlert } from '../pad-view-only/LoadingNoteAlert'
import { AppBar, AppBarMode } from './app-bar/app-bar'
import { EditorMode } from './app-bar/editor-view-mode'
import { DocumentIframe } from './document-renderer-pane/document-iframe'
import { EditorPane } from './editor-pane/editor-pane'
import { editorTestContent } from './editorTestContent'
import { useViewModeShortcuts } from './hooks/useViewModeShortcuts'
import { DualScrollState, ScrollState } from './scroll/scroll-props'
import { Sidebar } from './sidebar/sidebar'
import { Splitter } from './splitter/splitter'
import { YAMLMetaData } from './yaml-metadata/yaml-metadata'
import { useLoadNoteFromServer } from './useLoadNoteFromServer'
export interface EditorPathParams {
id: string
@ -36,18 +43,11 @@ export enum ScrollSource {
RENDERER
}
const TASK_REGEX = /(\s*[-*] )(\[[ xX]])( .*)/
export const Editor: React.FC = () => {
const { t } = useTranslation()
const { id } = useParams<EditorPathParams>()
useTranslation()
const { search } = useLocation()
const untitledNote = t('editor.untitledNote')
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
const markdownContent = useNoteMarkdownContent()
const isWide = useMedia({ minWidth: 576 }, true)
const [documentTitle, setDocumentTitle] = useState(untitledNote)
const noteMetadata = useRef<YAMLMetaData>()
const firstHeading = useRef<string>()
const scrollSource = useRef<ScrollSource>(ScrollSource.EDITOR)
const editorMode: EditorMode = useSelector((state: ApplicationState) => state.editorConfig.editorMode)
@ -59,7 +59,6 @@ export const Editor: React.FC = () => {
}))
useEffect(() => {
setDocumentContent(editorTestContent)
const requestedMode = search.substr(1)
const mode = Object.values(EditorMode).find(mode => mode === requestedMode)
if (mode) {
@ -67,39 +66,6 @@ export const Editor: React.FC = () => {
}
}, [search])
const updateDocumentTitle = useCallback(() => {
const noteTitle = extractNoteTitle(untitledNote, noteMetadata.current, firstHeading.current)
setDocumentTitle(noteTitle)
}, [noteMetadata, firstHeading, untitledNote])
const onFirstHeadingChange = useCallback((newFirstHeading: string | undefined) => {
firstHeading.current = newFirstHeading
updateDocumentTitle()
}, [updateDocumentTitle])
const onMetadataChange = useCallback((metaData: YAMLMetaData | undefined) => {
noteMetadata.current = metaData
setDocumentMetadata(metaData)
updateDocumentTitle()
}, [updateDocumentTitle])
const onTaskCheckedChange = useCallback((lineInMarkdown: number, checked: boolean) => {
const lines = markdownContent.split('\n')
const results = TASK_REGEX.exec(lines[lineInMarkdown])
if (results) {
const before = results[1]
const after = results[3]
lines[lineInMarkdown] = `${before}[${checked ? 'x' : ' '}]${after}`
setDocumentContent(lines.join('\n'))
}
}, [markdownContent])
useViewModeShortcuts()
useEffect(() => {
setNoteId(id)
}, [id])
useEffect(() => {
if (!isWide && editorMode === EditorMode.BOTH) {
setEditorMode(EditorMode.PREVIEW)
@ -118,8 +84,10 @@ export const Editor: React.FC = () => {
}
}, [editorSyncScroll])
useViewModeShortcuts()
useApplyDarkMode()
useDocumentTitle(documentTitle)
useDocumentTitleWithNoteTitle()
const [error, loading] = useLoadNoteFromServer()
const setRendererToScrollSource = useCallback(() => {
scrollSource.current = ScrollSource.RENDERER
@ -134,32 +102,39 @@ export const Editor: React.FC = () => {
<MotdBanner/>
<div className={'d-flex flex-column vh-100'}>
<AppBar mode={AppBarMode.EDITOR}/>
<div className={"flex-fill d-flex h-100 w-100 overflow-hidden flex-row"}>
<Splitter
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
left={
<EditorPane
onContentChange={setDocumentContent}
content={markdownContent}
scrollState={scrollState.editorScrollState}
onScroll={onEditorScroll}
onMakeScrollSource={setEditorToScrollSource}/>
}
showRight={editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH}
right={
<DocumentIframe
markdownContent={markdownContent}
onMakeScrollSource={setRendererToScrollSource}
onFirstHeadingChange={onFirstHeadingChange}
onTaskCheckedChange={onTaskCheckedChange}
onMetadataChange={onMetadataChange}
onScroll={onMarkdownRendererScroll}
wide={editorMode === EditorMode.PREVIEW}
scrollState={scrollState.rendererScrollState}/>
}
containerClassName={'overflow-hidden'}/>
<Sidebar/>
<div className={'container'}>
<ErrorWhileLoadingNoteAlert show={error}/>
<LoadingNoteAlert show={loading}/>
</div>
<ShowIf condition={!error && !loading}>
<div className={"flex-fill d-flex h-100 w-100 overflow-hidden flex-row"}>
<Splitter
showLeft={editorMode === EditorMode.EDITOR || editorMode === EditorMode.BOTH}
left={
<EditorPane
onContentChange={setNoteMarkdownContent}
content={markdownContent}
scrollState={scrollState.editorScrollState}
onScroll={onEditorScroll}
onMakeScrollSource={setEditorToScrollSource}/>
}
showRight={editorMode === EditorMode.PREVIEW || editorMode === EditorMode.BOTH}
right={
<DocumentIframe
markdownContent={markdownContent}
onMakeScrollSource={setRendererToScrollSource}
onFirstHeadingChange={updateNoteTitleByFirstHeading}
onTaskCheckedChange={SetCheckboxInMarkdownContent}
onFrontmatterChange={setNoteFrontmatter}
onScroll={onMarkdownRendererScroll}
wide={editorMode === EditorMode.PREVIEW}
scrollState={scrollState.rendererScrollState}/>
}
containerClassName={'overflow-hidden'}/>
<Sidebar/>
</div>
</ShowIf>
</div>
</Fragment>
)

View file

@ -1,303 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { isTestMode } from '../../utils/is-test-mode'
export const editorTestContent = isTestMode() ? '' : `---
title: Features
description: Many features, such wow!
robots: noindex
tags:
- hedgedoc
- demo
- react
opengraph:
title: Features
---
# Embedding demo
[TOC]
## markmap
\`\`\`markmap
# MarkMap
## Pro
### written in typescript
## Cons
### must redeclare types
\`\`\`
## Vega-Lite
\`\`\`vega-lite
{
"$schema": "https://vega.github.io/schema/vega-lite/v4.json",
"description": "Reproducing http://robslink.com/SAS/democd91/pyramid_pie.htm",
"data": {
"values": [
{"category": "Sky", "value": 75, "order": 3},
{"category": "Shady side of a pyramid", "value": 10, "order": 1},
{"category": "Sunny side of a pyramid", "value": 15, "order": 2}
]
},
"mark": {"type": "arc", "outerRadius": 80},
"encoding": {
"theta": {
"field": "value", "type": "quantitative",
"scale": {"range": [2.35619449, 8.639379797]},
"stack": true
},
"color": {
"field": "category", "type": "nominal",
"scale": {
"domain": ["Sky", "Shady side of a pyramid", "Sunny side of a pyramid"],
"range": ["#416D9D", "#674028", "#DEAC58"]
},
"legend": {
"orient": "none",
"title": null,
"columns": 1,
"legendX": 200,
"legendY": 80
}
},
"order": {
"field": "order"
}
},
"view": {"stroke": null}
}
\`\`\`
## GraphViz
\`\`\`graphviz
graph {
a -- b
a -- b
b -- a [color=blue]
}
\`\`\`
\`\`\`graphviz
digraph structs {
node [shape=record];
struct1 [label="<f0> left|<f1> mid&#92; dle|<f2> right"];
struct2 [label="<f0> one|<f1> two"];
struct3 [label="hello&#92;nworld |{ b |{c|<here> d|e}| f}| g | h"];
struct1:f1 -> struct2:f0;
struct1:f2 -> struct3:here;
}
\`\`\`
\`\`\`graphviz
digraph G {
main -> parse -> execute;
main -> init;
main -> cleanup;
execute -> make_string;
execute -> printf
init -> make_string;
main -> printf;
execute -> compare;
}
\`\`\`
\`\`\`graphviz
digraph D {
node [fontname="Arial"];
node_A [shape=record label="shape=record|{above|middle|below}|right"];
node_B [shape=plaintext label="shape=plaintext|{curly|braces and|bars without}|effect"];
}
\`\`\`
\`\`\`graphviz
digraph D {
A -> {B, C, D} -> {F}
}
\`\`\`
## High Res Image
![Wheat Field with Cypresses](/img/highres.jpg)
## Sequence Diagram (deprecated)
\`\`\`sequence
Title: Here is a title
note over A: asdd
A->B: Normal line
B-->C: Dashed line
C->>D: Open arrow
D-->>A: Dashed open arrow
participant IOOO
\`\`\`
## Mermaid
\`\`\`mermaid
gantt
title A Gantt Diagram
section Section
A task: a1, 2014-01-01, 30d
Another task: after a1, 20d
section Another
Task in sec: 2014-01-12, 12d
Another task: 24d
\`\`\`
## Flowchart
\`\`\`flow
st=>start: Start
e=>end: End
op=>operation: My Operation
op2=>operation: lalala
cond=>condition: Yes or No?
st->op->op2->cond
cond(yes)->e
cond(no)->op2
\`\`\`
## ABC
\`\`\`abc
X:1
T:Speed the Plough
M:4/4
C:Trad.
K:G
|:GABc dedB|dedB dedB|c2ec B2dB|c2A2 A2BA|
GABc dedB|dedB dedB|c2ec B2dB|A2F2 G4:|
|:g2gf gdBd|g2f2 e2d2|c2ec B2dB|c2A2 A2df|
g2gf g2Bd|g2f2 e2d2|c2ec B2dB|A2F2 G4:|
\`\`\`
## CSV
\`\`\`csv delimiter=; header
Username; Identifier;First name;Last name
"booker12; rbooker";9012;Rachel;Booker
grey07;2070;Laura;Grey
johnson81;4081;Craig;Johnson
jenkins46;9346;Mary;Jenkins
smith79;5079;Jamie;Smith
\`\`\`
## some plain text
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
## KaTeX
You can render *LaTeX* mathematical expressions using **KaTeX**, as on [math.stackexchange.com](https://math.stackexchange.com/):
The *Gamma function* satisfying $\\Gamma(n) = (n-1)!\\quad\\forall n\\in\\mathbb N$ is via the Euler integral
$$
x = {-b \\pm \\sqrt{b^2-4ac} \\over 2a}.
$$
$$
\\Gamma(z) = \\int_0^\\infty t^{z-1}e^{-t}dt\\,.
$$
> More information about **LaTeX** mathematical expressions [here](https://meta.math.stackexchange.com/questions/5020/mathjax-basic-tutorial-and-quick-reference).
## Blockquote
> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
> Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
> [color=red] [name=John Doe] [time=2020-06-21 22:50]
## Slideshare
{%slideshare mazlan1/internet-of-things-the-tip-of-an-iceberg %}
## Gist
https://gist.github.com/schacon/1
## YouTube
https://www.youtube.com/watch?v=YE7VzlLtp-4
## Vimeo
https://vimeo.com/23237102
## Asciinema
https://asciinema.org/a/117928
## PDF
{%pdf https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf %}
## Code highlighting
\`\`\`js=
var s = "JavaScript syntax highlighting";
alert(s);
function $initHighlight(block, cls) {
try {
if (cls.search(/\\bno\\-highlight\\b/) != -1)
return process(block, true, 0x0F) +
' class=""';
} catch (e) {
/* handle exception */
}
for (var i = 0 / 2; i < classes.length; i++) {
if (checkCondition(classes[i]) === undefined)
return /\\d+[\\s/]/g;
}
}
\`\`\`
## PlantUML
\`\`\`plantuml
@startuml
participant Alice
participant "The **Famous** Bob" as Bob
Alice -> Bob : hello --there--
... Some ~~long delay~~ ...
Bob -> Alice : ok
note left
This is **bold**
This is //italics//
This is ""monospaced""
This is --stroked--
This is __underlined__
This is ~~waved~~
end note
Alice -> Bob : A //well formatted// message
note right of Alice
This is <back:cadetblue><size:18>displayed</size></back>
__left of__ Alice.
end note
note left of Bob
<u:red>This</u> is <color #118888>displayed</color>
**<color purple>left of</color> <s:red>Alice</strike> Bob**.
end note
note over Alice, Bob
<w:#FF33FF>This is hosted</w> by <img sourceforge.jpg>
end note
@enduml
\`\`\`
## ToDo List
- [ ] ToDos
- [X] Buy some salad
- [ ] Brush teeth
- [x] Drink some water
- [ ] **Click my box** and see the source code, if you're allowed to edit!
`

View file

@ -7,11 +7,11 @@
import yaml from 'js-yaml'
import MarkdownIt from 'markdown-it'
import frontmatter from 'markdown-it-front-matter'
import { RawYAMLMetadata, YAMLMetaData } from './yaml-metadata'
import { NoteFrontmatter, RawNoteFrontmatter } from './note-frontmatter'
describe('yaml tests', () => {
let raw: RawYAMLMetadata | undefined
let finished: YAMLMetaData | undefined
describe('yaml frontmatter tests', () => {
let raw: RawNoteFrontmatter | undefined
let finished: NoteFrontmatter | undefined
const md = new MarkdownIt('default', {
html: true,
breaks: true,
@ -19,15 +19,15 @@ describe('yaml tests', () => {
typographer: true
})
md.use(frontmatter, (rawMeta: string) => {
raw = yaml.load(rawMeta) as RawYAMLMetadata
finished = new YAMLMetaData(raw)
raw = yaml.load(rawMeta) as RawNoteFrontmatter
finished = new NoteFrontmatter(raw)
})
// generate default YAMLMetadata
md.render('---\n---')
const defaultYAML = finished
const testMetadata = (input: string, expectedRaw: Partial<RawYAMLMetadata>, expectedFinished: Partial<YAMLMetaData>) => {
const testFrontmatter = (input: string, expectedRaw: Partial<RawNoteFrontmatter>, expectedFinished: Partial<NoteFrontmatter>) => {
md.render(input)
expect(raw).not.toBe(undefined)
expect(raw).toEqual(expectedRaw)
@ -44,47 +44,47 @@ describe('yaml tests', () => {
})
it('title only', () => {
testMetadata(`---
testFrontmatter(`---
title: test
___
`,
{
title: 'test'
},
{
title: 'test'
})
{
title: 'test'
},
{
title: 'test'
})
})
it('robots only', () => {
testMetadata(`---
testFrontmatter(`---
robots: index, follow
___
`,
{
robots: 'index, follow'
},
{
robots: 'index, follow'
})
{
robots: 'index, follow'
},
{
robots: 'index, follow'
})
})
it('tags only (old syntax)', () => {
testMetadata(`---
testFrontmatter(`---
tags: test123, abc
___
`,
{
tags: 'test123, abc'
},
{
tags: ['test123', 'abc'],
deprecatedTagsSyntax: true
})
{
tags: 'test123, abc'
},
{
tags: ['test123', 'abc'],
deprecatedTagsSyntax: true
})
})
it('tags only', () => {
testMetadata(`---
testFrontmatter(`---
tags:
- test123
- abc
@ -100,7 +100,7 @@ describe('yaml tests', () => {
})
it('tags only (alternative syntax)', () => {
testMetadata(`---
testFrontmatter(`---
tags: ['test123', 'abc']
___
`,
@ -114,21 +114,21 @@ describe('yaml tests', () => {
})
it('breaks only', () => {
testMetadata(`---
testFrontmatter(`---
breaks: false
___
`,
{
breaks: false
},
{
breaks: false
})
{
breaks: false
},
{
breaks: false
})
})
/*
it('slideOptions nothing', () => {
testMetadata(`---
testFrontmatter(`---
slideOptions:
___
`,
@ -144,7 +144,7 @@ describe('yaml tests', () => {
})
it('slideOptions.theme only', () => {
testMetadata(`---
testFrontmatter(`---
slideOptions:
theme: sky
___
@ -164,7 +164,7 @@ describe('yaml tests', () => {
})
it('slideOptions full', () => {
testMetadata(`---
testFrontmatter(`---
slideOptions:
transition: zoom
theme: sky
@ -186,46 +186,46 @@ describe('yaml tests', () => {
*/
it('opengraph nothing', () => {
testMetadata(`---
testFrontmatter(`---
opengraph:
___
`,
{
opengraph: null
},
{
opengraph: new Map<string, string>()
})
{
opengraph: null
},
{
opengraph: new Map<string, string>()
})
})
it('opengraph title only', () => {
testMetadata(`---
testFrontmatter(`---
opengraph:
title: Testtitle
___
`,
{
opengraph: {
title: 'Testtitle'
}
},
{
{
opengraph: {
title: 'Testtitle'
}
},
{
opengraph: new Map<string, string>(Object.entries({ title: 'Testtitle' }))
})
})
it('opengraph more attributes', () => {
testMetadata(`---
testFrontmatter(`---
opengraph:
title: Testtitle
image: https://dummyimage.com/48.png
image:type: image/png
___
`,
{
opengraph: {
title: 'Testtitle',
image: 'https://dummyimage.com/48.png',
{
opengraph: {
title: 'Testtitle',
image: 'https://dummyimage.com/48.png',
'image:type': 'image/png'
}
},

View file

@ -8,7 +8,7 @@
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 RawYAMLMetadata {
export interface RawNoteFrontmatter {
title: string | undefined
description: string | undefined
tags: string | string[] | undefined
@ -23,7 +23,7 @@ export interface RawYAMLMetadata {
opengraph: { [key: string]:string } | null
}
export class YAMLMetaData {
export class NoteFrontmatter {
title: string
description: string
tags: string[]
@ -38,7 +38,7 @@ export class YAMLMetaData {
// slideOptions: RevealOptions
opengraph: Map<string, string>
constructor (rawData: RawYAMLMetadata) {
constructor (rawData: RawNoteFrontmatter) {
this.title = rawData?.title ?? ''
this.description = rawData?.description ?? ''
this.robots = rawData?.robots ?? ''
@ -46,9 +46,9 @@ export class YAMLMetaData {
this.GA = rawData?.GA ?? ''
this.disqus = rawData?.disqus ?? ''
this.type = (rawData?.type as YAMLMetaData['type']) ?? ''
this.type = (rawData?.type as NoteFrontmatter['type']) ?? ''
this.lang = (rawData?.lang as iso6391) ?? 'en'
this.dir = (rawData?.dir as YAMLMetaData['dir']) ?? 'ltr'
this.dir = (rawData?.dir as NoteFrontmatter['dir']) ?? 'ltr'
/* this.slideOptions = (rawData?.slideOptions as RevealOptions) ?? {
transition: 'none',

View file

@ -8,13 +8,13 @@ import { RefObject, useCallback } from 'react'
import { LineMarkerPosition } from '../../../markdown-renderer/types'
import { ScrollState } from '../scroll-props'
export const useOnUserScroll = (lineMarks: LineMarkerPosition[] | undefined, renderer: RefObject<HTMLElement>, onScroll: ((newScrollState: ScrollState) => void) | undefined): () => void =>
useCallback(() => {
if (!renderer.current || !lineMarks || lineMarks.length === 0 || !onScroll) {
export const useOnUserScroll = (lineMarks: LineMarkerPosition[] | undefined, scrollContainer: RefObject<HTMLElement>, onScroll: ((newScrollState: ScrollState) => void) | undefined): () => void => {
return useCallback(() => {
if (!scrollContainer.current || !lineMarks || lineMarks.length === 0 || !onScroll) {
return
}
const scrollTop = renderer.current.scrollTop
const scrollTop = scrollContainer.current.scrollTop
const lineMarksBeforeScrollTop = lineMarks.filter(lineMark => lineMark.position <= scrollTop)
if (lineMarksBeforeScrollTop.length === 0) {
@ -44,4 +44,5 @@ export const useOnUserScroll = (lineMarks: LineMarkerPosition[] | undefined, ren
const newScrollState: ScrollState = { firstLineInView: line, scrolledPercentage: innerScrolling }
onScroll(newScrollState)
}, [lineMarks, onScroll, renderer])
}, [lineMarks, onScroll, scrollContainer])
}

View file

@ -9,21 +9,21 @@ import { LineMarkerPosition } from '../../../markdown-renderer/types'
import { ScrollState } from '../scroll-props'
import { findLineMarks } from '../utils'
export const useScrollToLineMark = (scrollState: ScrollState | undefined, lineMarks: LineMarkerPosition[] | undefined, contentLineCount: number, renderer: RefObject<HTMLElement>): void => {
export const useScrollToLineMark = (scrollState: ScrollState | undefined, lineMarks: LineMarkerPosition[] | undefined, contentLineCount: number, scrollContainer: RefObject<HTMLElement>): void => {
const lastScrollPosition = useRef<number>()
const scrollTo = useCallback((targetPosition: number): void => {
if (!renderer.current || targetPosition === lastScrollPosition.current) {
if (!scrollContainer.current || targetPosition === lastScrollPosition.current) {
return
}
lastScrollPosition.current = targetPosition
renderer.current.scrollTo({
scrollContainer.current.scrollTo({
top: targetPosition
})
}, [renderer])
}, [scrollContainer])
useEffect(() => {
if (!renderer.current || !lineMarks || lineMarks.length === 0 || !scrollState) {
if (!scrollContainer.current || !lineMarks || lineMarks.length === 0 || !scrollState) {
return
}
if (scrollState.firstLineInView < lineMarks[0].line) {
@ -31,12 +31,12 @@ export const useScrollToLineMark = (scrollState: ScrollState | undefined, lineMa
return
}
if (scrollState.firstLineInView > lineMarks[lineMarks.length - 1].line) {
scrollTo(renderer.current.offsetHeight)
scrollTo(scrollContainer.current.offsetHeight)
return
}
const { lastMarkBefore, firstMarkAfter } = findLineMarks(lineMarks, scrollState.firstLineInView)
const positionBefore = lastMarkBefore ? lastMarkBefore.position : lineMarks[0].position
const positionAfter = firstMarkAfter ? firstMarkAfter.position : renderer.current.offsetHeight
const positionAfter = firstMarkAfter ? firstMarkAfter.position : scrollContainer.current.offsetHeight
const lastMarkBeforeLine = lastMarkBefore ? lastMarkBefore.line : 1
const firstMarkAfterLine = firstMarkAfter ? firstMarkAfter.line : contentLineCount
const linesBetweenMarkers = firstMarkAfterLine - lastMarkBeforeLine
@ -45,5 +45,5 @@ export const useScrollToLineMark = (scrollState: ScrollState | undefined, lineMa
const position = positionBefore + (scrollState.firstLineInView - lastMarkBeforeLine) * lineHeight + scrollState.scrolledPercentage / 100 * lineHeight
const correctedPosition = Math.floor(position)
scrollTo(correctedPosition)
}, [contentLineCount, lineMarks, renderer, scrollState, scrollTo])
}, [contentLineCount, lineMarks, scrollContainer, scrollState, scrollTo])
}

View file

@ -6,16 +6,14 @@
import React, { useCallback } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content'
import { download } from '../../common/download/download'
import { SidebarButton } from './sidebar-button'
export const ExportMarkdownSidebarEntry: React.FC = () => {
useTranslation()
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
const markdownContent = useNoteMarkdownContent()
const onClick = useCallback(() => {
download(markdownContent, `title.md`, 'text/markdown') //todo: replace hard coded title with redux
}, [markdownContent])

View file

@ -6,14 +6,13 @@
import React, { Fragment, useCallback, useRef } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../../redux'
import { setDocumentContent } from '../../../redux/document-content/methods'
import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content'
import { setNoteMarkdownContent } from '../../../redux/note-details/methods'
import { SidebarButton } from './sidebar-button'
import { UploadInput } from './upload-input'
export const ImportMarkdownSidebarEntry: React.FC = () => {
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
const markdownContent = useNoteMarkdownContent()
useTranslation()
const onImportMarkdown = useCallback((file: File) => {
@ -21,11 +20,7 @@ export const ImportMarkdownSidebarEntry: React.FC = () => {
const fileReader = new FileReader()
fileReader.addEventListener('load', () => {
const newContent = fileReader.result as string
if (markdownContent.length === 0) {
setDocumentContent(newContent)
} else {
setDocumentContent(markdownContent + '\n' + newContent)
}
setNoteMarkdownContent(markdownContent.length === 0 ? newContent : `${markdownContent}\n${newContent}`)
})
fileReader.addEventListener('loadend', () => {
resolve()
@ -39,15 +34,16 @@ export const ImportMarkdownSidebarEntry: React.FC = () => {
const clickRef = useRef<(() => void)>()
const buttonClick = useCallback(() => {
clickRef.current?.();
},[]);
clickRef.current?.()
}, [])
return (
<Fragment>
<SidebarButton data-cy={"menu-import-markdown"} icon={"file-text-o"} onClick={buttonClick}>
<Trans i18nKey={'editor.import.file'}/>
</SidebarButton>
<UploadInput onLoad={onImportMarkdown} data-cy={"menu-import-markdown-input"} acceptedFiles={'.md, text/markdown, text/plain'} onClickRef={clickRef}/>
<UploadInput onLoad={onImportMarkdown} data-cy={"menu-import-markdown-input"}
acceptedFiles={'.md, text/markdown, text/plain'} onClickRef={clickRef}/>
</Fragment>
)
}

View file

@ -0,0 +1,32 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useEffect, useState } from 'react'
import { useParams } from 'react-router'
import { getNote } from '../../api/notes'
import { setNoteDataFromServer } from '../../redux/note-details/methods'
import { EditorPathParams } from './editor'
export const useLoadNoteFromServer = (): [boolean, boolean] => {
const { id } = useParams<EditorPathParams>()
const [error, setError] = useState(false)
const [loading, setLoading] = useState(true)
useEffect(() => {
getNote(id)
.then(note => {
setNoteDataFromServer(note)
})
.catch((e) => {
setError(true)
console.error(e)
})
.finally(() => setLoading(false))
}, [id])
return [error, loading]
}

View file

@ -10,10 +10,10 @@ 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 { RawYAMLMetadata, YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
import { NoteFrontmatter, RawNoteFrontmatter } from '../editor/note-frontmatter/note-frontmatter'
import { BasicMarkdownRenderer } from './basic-markdown-renderer'
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
import { usePostMetaDataOnChange } from './hooks/use-post-meta-data-on-change'
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 { FullMarkdownItConfigurator } from './markdown-it-configurator/FullMarkdownItConfigurator'
@ -25,7 +25,7 @@ import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-po
export interface FullMarkdownRendererProps {
onFirstHeadingChange?: (firstHeading: string | undefined) => void
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
onTocChange?: (ast: TocAst) => void
rendererRef?: Ref<HTMLDivElement>
@ -37,7 +37,7 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
{
onFirstHeadingChange,
onLineMarkerPositionChanged,
onMetaDataChange,
onFrontmatterChange,
onTaskCheckedChange,
onTocChange,
content,
@ -53,11 +53,11 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
const [showYamlError, setShowYamlError] = useState(false)
const hasNewYamlError = useRef(false)
const rawMetaRef = useRef<RawYAMLMetadata>()
const rawMetaRef = useRef<RawNoteFrontmatter>()
const firstHeadingRef = useRef<string>()
const documentElement = useRef<HTMLDivElement>(null)
const currentLineMarkers = useRef<LineMarkers[]>()
usePostMetaDataOnChange(rawMetaRef.current, firstHeadingRef.current, onMetaDataChange, onFirstHeadingChange)
usePostFrontmatterOnChange(rawMetaRef.current, firstHeadingRef.current, onFrontmatterChange, onFirstHeadingChange)
useCalculateLineMarkerPosition(documentElement, currentLineMarkers.current, onLineMarkerPositionChanged, documentElement.current?.offsetTop ?? 0)
useExtractFirstHeadline(documentElement, content, onFirstHeadingChange)
@ -66,7 +66,7 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
const markdownIt = useMemo(() => {
return (new FullMarkdownItConfigurator(
!!onMetaDataChange,
!!onFrontmatterChange,
errorState => hasNewYamlError.current = errorState,
rawMeta => {
rawMetaRef.current = rawMeta
@ -78,9 +78,9 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
currentLineMarkers.current = lineMarkers
}
)).buildConfiguredMarkdownIt()
}, [onMetaDataChange])
}, [onFrontmatterChange])
const clearMetadata = useCallback(() => {
const clearFrontmatter = useCallback(() => {
hasNewYamlError.current = false
rawMetaRef.current = undefined
}, [])
@ -107,7 +107,7 @@ export const FullMarkdownRenderer: React.FC<FullMarkdownRendererProps & Addition
componentReplacers={allReplacers}
markdownIt={markdownIt}
documentReference={documentElement}
onBeforeRendering={clearMetadata}
onBeforeRendering={clearFrontmatter}
onAfterRendering={checkYamlErrorState}
/>
</div>

View file

@ -4,10 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useEffect } from 'react'
import React, { useCallback, useEffect, useRef } from 'react'
export const useExtractFirstHeadline = (documentElement: React.RefObject<HTMLDivElement>, content: string, onFirstHeadingChange?: (firstHeading: string | undefined) => void): void => {
const extractInnerText = useCallback((node: ChildNode): string => {
const extractInnerText = useCallback((node: ChildNode | null): string => {
if (!node) {
return ''
}
let innerText = ''
if ((node as HTMLElement).classList?.contains("katex-mathml")) {
@ -15,7 +19,9 @@ export const useExtractFirstHeadline = (documentElement: React.RefObject<HTMLDiv
}
if (node.childNodes && node.childNodes.length > 0) {
node.childNodes.forEach((child) => { innerText += extractInnerText(child) })
node.childNodes.forEach((child) => {
innerText += extractInnerText(child)
})
} else if (node.nodeName === 'IMG') {
innerText += (node as HTMLImageElement).getAttribute('alt')
} else {
@ -24,14 +30,17 @@ export const useExtractFirstHeadline = (documentElement: React.RefObject<HTMLDiv
return innerText
}, [])
const lastFirstHeading = useRef<string | undefined>()
useEffect(() => {
if (onFirstHeadingChange && documentElement.current) {
const firstHeading = documentElement.current.getElementsByTagName('h1').item(0)
if (firstHeading) {
onFirstHeadingChange(extractInnerText(firstHeading))
} else {
onFirstHeadingChange(undefined)
const headingText = extractInnerText(firstHeading)
if (headingText === lastFirstHeading.current) {
return
}
lastFirstHeading.current = headingText
onFirstHeadingChange(headingText)
}
}, [documentElement, extractInnerText, onFirstHeadingChange, content])
}

View file

@ -0,0 +1,35 @@
/*
* 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/note-frontmatter/note-frontmatter'
export const usePostFrontmatterOnChange = (
rawFrontmatter: RawNoteFrontmatter | undefined,
firstHeadingRef: string | undefined,
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void,
onFirstHeadingChange?: (firstHeading: string | undefined) => void
): void => {
const oldMetaRef = useRef<RawNoteFrontmatter>()
const oldFirstHeadingRef = useRef<string>()
useEffect(() => {
if (onFrontmatterChange && !equal(oldMetaRef.current, rawFrontmatter)) {
if (rawFrontmatter) {
const newFrontmatter = new NoteFrontmatter(rawFrontmatter)
onFrontmatterChange(newFrontmatter)
} else {
onFrontmatterChange(undefined)
}
oldMetaRef.current = rawFrontmatter
}
if (onFirstHeadingChange && !equal(firstHeadingRef, oldFirstHeadingRef.current)) {
onFirstHeadingChange(firstHeadingRef || undefined)
oldFirstHeadingRef.current = firstHeadingRef
}
})
}

View file

@ -1,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 { RawYAMLMetadata, YAMLMetaData } from '../../editor/yaml-metadata/yaml-metadata'
export const usePostMetaDataOnChange = (
rawMetaRef: RawYAMLMetadata|undefined,
firstHeadingRef: string|undefined,
onMetaDataChange?: (yamlMetaData: YAMLMetaData | undefined) => void,
onFirstHeadingChange?: (firstHeading: string | undefined) => void
): void => {
const oldMetaRef = useRef<RawYAMLMetadata>()
const oldFirstHeadingRef = useRef<string>()
useEffect(() => {
if (onMetaDataChange && !equal(oldMetaRef.current, rawMetaRef)) {
if (rawMetaRef) {
const newMetaData = new YAMLMetaData(rawMetaRef)
onMetaDataChange(newMetaData)
} else {
onMetaDataChange(undefined)
}
oldMetaRef.current = rawMetaRef
}
if (onFirstHeadingChange && !equal(firstHeadingRef, oldFirstHeadingRef.current)) {
onFirstHeadingChange(firstHeadingRef || undefined)
oldFirstHeadingRef.current = firstHeadingRef
}
})
}

View file

@ -6,7 +6,7 @@
import MarkdownIt from 'markdown-it'
import { TocAst } from 'markdown-it-toc-done-right'
import { RawYAMLMetadata } from '../../editor/yaml-metadata/yaml-metadata'
import { RawNoteFrontmatter } from '../../editor/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'
@ -28,7 +28,7 @@ export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
constructor (
private useFrontmatter: boolean,
private passYamlErrorState: (error: boolean) => void,
private onRawMeta: (rawMeta: RawYAMLMetadata) => void,
private onRawMeta: (rawMeta: RawNoteFrontmatter) => void,
private onToc: (toc: TocAst) => void,
private onLineMarkers: (lineMarkers: LineMarkers[]) => void
) {
@ -45,8 +45,8 @@ export class FullMarkdownItConfigurator extends BasicMarkdownItConfigurator {
!this.useFrontmatter
? undefined
: {
onYamlError: (hasError: boolean) => this.passYamlErrorState(hasError),
onRawMeta: (rawMeta: RawYAMLMetadata) => this.onRawMeta(rawMeta)
onParseError: (hasError: boolean) => this.passYamlErrorState(hasError),
onRawMeta: (rawMeta: RawNoteFrontmatter) => this.onRawMeta(rawMeta)
})
},
headlineAnchors,

View file

@ -7,11 +7,11 @@
import yaml from 'js-yaml'
import MarkdownIt from 'markdown-it'
import frontmatter from 'markdown-it-front-matter'
import { RawYAMLMetadata } from '../../editor/yaml-metadata/yaml-metadata'
import { RawNoteFrontmatter } from '../../editor/note-frontmatter/note-frontmatter'
interface FrontmatterPluginOptions {
onYamlError: (error: boolean) => void,
onRawMeta: (rawMeta: RawYAMLMetadata) => void,
onParseError: (error: boolean) => void,
onRawMeta: (rawMeta: RawNoteFrontmatter) => void,
}
export const frontmatterExtract: MarkdownIt.PluginWithOptions<FrontmatterPluginOptions> = (markdownIt: MarkdownIt, options) => {
@ -20,13 +20,13 @@ export const frontmatterExtract: MarkdownIt.PluginWithOptions<FrontmatterPluginO
}
frontmatter(markdownIt, (rawMeta: string) => {
try {
const meta: RawYAMLMetadata = yaml.load(rawMeta) as RawYAMLMetadata
options.onYamlError(false)
const meta: RawNoteFrontmatter = yaml.load(rawMeta) as RawNoteFrontmatter
options.onParseError(false)
options.onRawMeta(meta)
} catch (e) {
console.error(e)
options.onYamlError(true)
options.onRawMeta({} as RawYAMLMetadata)
options.onParseError(true)
options.onRawMeta({} as RawNoteFrontmatter)
}
})
}

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import { ShowIf } from '../common/show-if/show-if'
export interface ErrorWhileLoadingNoteAlertProps {
show: boolean
}
export const ErrorWhileLoadingNoteAlert: React.FC<ErrorWhileLoadingNoteAlertProps> = ({ show }) => {
useTranslation()
return (
<ShowIf condition={show}>
<Alert variant={'danger'} className={'my-2'}>
<b><Trans i18nKey={'views.readOnly.error.title'}/></b>
<br/>
<Trans i18nKey={'views.readOnly.error.description'}/>
</Alert>
</ShowIf>
)
}

View file

@ -0,0 +1,24 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
import { Alert } from 'react-bootstrap'
import { Trans } from 'react-i18next'
import { ShowIf } from '../common/show-if/show-if'
export interface LoadingNoteAlertProps {
show: boolean
}
export const LoadingNoteAlert: React.FC<LoadingNoteAlertProps> = ({ show }) => {
return (
<ShowIf condition={show}>
<Alert variant={'info'} className={'my-2'}>
<Trans i18nKey={'views.readOnly.loading'}/>
</Alert>
</ShowIf>
)
}

View file

@ -1,8 +1,8 @@
/*
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 { DateTime } from 'luxon'
import React from 'react'
@ -17,9 +17,9 @@ import './document-infobar.scss'
export interface DocumentInfobarProps {
changedAuthor: string
changedTime: number
changedTime: DateTime
createdAuthor: string
createdTime: number
createdTime: DateTime
editable: boolean
noteId: string
viewCount: number
@ -43,12 +43,12 @@ export const DocumentInfobar: React.FC<DocumentInfobarProps> = ({
<div className={'d-flex flex-column'}>
<DocumentInfoTimeLine
mode={DocumentInfoLineWithTimeMode.CREATED}
time={ DateTime.fromSeconds(createdTime) }
time={createdTime}
userName={createdAuthor}
profileImageSrc={'/img/avatar.png'}/>
<DocumentInfoTimeLine
mode={DocumentInfoLineWithTimeMode.EDITED}
time={ DateTime.fromSeconds(changedTime) }
time={changedTime}
userName={changedAuthor}
profileImageSrc={'/img/avatar.png'}/>
<hr/>

View file

@ -4,99 +4,61 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { Alert } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next'
import React, { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { useParams } from 'react-router'
import { getNote, Note } from '../../api/notes'
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
import { useDocumentTitle } from '../../hooks/common/use-document-title'
import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-title-with-note-title'
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
import { ApplicationState } from '../../redux'
import { setDocumentContent, setDocumentMetadata } from '../../redux/document-content/methods'
import { extractNoteTitle } from '../common/document-title/note-title-extractor'
import { setNoteFrontmatter, updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
import { MotdBanner } from '../common/motd-banner/motd-banner'
import { ShowIf } from '../common/show-if/show-if'
import { AppBar, AppBarMode } from '../editor/app-bar/app-bar'
import { DocumentIframe } from '../editor/document-renderer-pane/document-iframe'
import { EditorPathParams } from '../editor/editor'
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
import { useLoadNoteFromServer } from '../editor/useLoadNoteFromServer'
import { DocumentInfobar } from './document-infobar'
import { ErrorWhileLoadingNoteAlert } from './ErrorWhileLoadingNoteAlert'
import { LoadingNoteAlert } from './LoadingNoteAlert'
export const PadViewOnly: React.FC = () => {
const { t } = useTranslation()
useTranslation()
const { id } = useParams<EditorPathParams>()
const untitledNote = t('editor.untitledNote')
const [documentTitle, setDocumentTitle] = useState(untitledNote)
const [noteData, setNoteData] = useState<Note>()
const [error, setError] = useState(false)
const [loading, setLoading] = useState(true)
const noteMetadata = useRef<YAMLMetaData>()
const firstHeading = useRef<string>()
const updateDocumentTitle = useCallback(() => {
const noteTitle = extractNoteTitle(untitledNote, noteMetadata.current, firstHeading.current)
setDocumentTitle(noteTitle)
}, [untitledNote])
const onFirstHeadingChange = useCallback((newFirstHeading: string | undefined) => {
firstHeading.current = newFirstHeading
updateDocumentTitle()
}, [updateDocumentTitle])
const onMetadataChange = useCallback((metaData: YAMLMetaData | undefined) => {
noteMetadata.current = metaData
setDocumentMetadata(metaData)
updateDocumentTitle()
}, [updateDocumentTitle])
useEffect(() => {
getNote(id)
.then(note => {
setNoteData(note)
setDocumentContent(note.content)
})
.catch(() => setError(true))
.finally(() => setLoading(false))
}, [id])
useApplyDarkMode()
useDocumentTitle(documentTitle)
const markdownContent = useSelector((state: ApplicationState) => state.documentContent.content)
useDocumentTitleWithNoteTitle()
const onFirstHeadingChange = useCallback(updateNoteTitleByFirstHeading, [])
const onFrontmatterChange = useCallback(setNoteFrontmatter, [])
const [error, loading] = useLoadNoteFromServer()
const markdownContent = useNoteMarkdownContent()
const noteDetails = useSelector((state: ApplicationState) => state.noteDetails)
return (
<div className={'d-flex flex-column mvh-100 bg-light'}>
<MotdBanner/>
<AppBar mode={AppBarMode.BASIC}/>
<div className={'container'}>
<ShowIf condition={error}>
<Alert variant={'danger'} className={'my-2'}>
<b><Trans i18nKey={'views.readOnly.error.title'}/></b>
<br/>
<Trans i18nKey={'views.readOnly.error.description'}/>
</Alert>
</ShowIf>
<ShowIf condition={loading}>
<Alert variant={'info'} className={'my-2'}>
<Trans i18nKey={'views.readOnly.loading'}/>
</Alert>
</ShowIf>
<ErrorWhileLoadingNoteAlert show={error}/>
<LoadingNoteAlert show={loading}/>
</div>
<ShowIf condition={!error && !loading}>
{ /* TODO set editable and created author properly */}
<DocumentInfobar
changedAuthor={noteData?.lastChange.userId ?? ''}
changedTime={noteData?.lastChange.timestamp ?? 0}
changedAuthor={noteDetails.lastChange.userId ?? ''}
changedTime={noteDetails.lastChange.timestamp}
createdAuthor={'Test'}
createdTime={noteData?.createtime ?? 0}
createdTime={noteDetails.createTime}
editable={true}
noteId={id}
viewCount={noteData?.viewcount ?? 0}
viewCount={noteDetails.viewCount}
/>
<DocumentIframe extraClasses={"flex-fill"}
markdownContent={markdownContent}
onFirstHeadingChange={onFirstHeadingChange}
onMetadataChange={onMetadataChange}/>
onFrontmatterChange={onFrontmatterChange}/>
</ShowIf>
</div>
)

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NoteFrontmatter } from "../editor/note-frontmatter/note-frontmatter"
import { ScrollState } from "../editor/scroll/scroll-props"
import { YAMLMetaData } from "../editor/yaml-metadata/yaml-metadata"
import { IframeCommunicator } from "./iframe-communicator"
import {
EditorToRendererIframeMessage,
@ -18,36 +18,13 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<Edito
private onSetScrollSourceToRendererHandler?: () => void
private onTaskCheckboxChangeHandler?: (lineInMarkdown: number, checked: boolean) => void
private onFirstHeadingChangeHandler?: (heading?: string) => void
private onMetaDataChangeHandler?: (metaData?: YAMLMetaData) => void
private onFrontmatterChangeHandler?: (metaData?: NoteFrontmatter) => void
private onSetScrollStateHandler?: (scrollState: ScrollState) => void
private onRendererReadyHandler?: () => void
private onImageClickedHandler?: (details: ImageDetails) => void
protected handleEvent (event: MessageEvent<RendererToEditorIframeMessage>): boolean | undefined {
const renderMessage = event.data
switch (renderMessage.type) {
case RenderIframeMessageType.RENDERER_READY:
this.onRendererReadyHandler?.()
return false
case RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER:
this.onSetScrollSourceToRendererHandler?.()
return false
case RenderIframeMessageType.SET_SCROLL_STATE:
this.onSetScrollStateHandler?.(renderMessage.scrollState)
return false
case RenderIframeMessageType.ON_FIRST_HEADING_CHANGE:
this.onFirstHeadingChangeHandler?.(renderMessage.firstHeading)
return false
case RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE:
this.onTaskCheckboxChangeHandler?.(renderMessage.lineInMarkdown, renderMessage.checked)
return false
case RenderIframeMessageType.ON_SET_META_DATA:
this.onMetaDataChangeHandler?.(renderMessage.metaData)
return false
case RenderIframeMessageType.IMAGE_CLICKED:
this.onImageClickedHandler?.(renderMessage.details)
return false
}
public onFrontmatterChange (handler?: (frontmatter?: NoteFrontmatter) => void): void {
this.onFrontmatterChangeHandler = handler
}
public onImageClicked (handler?: (details: ImageDetails) => void): void {
@ -70,8 +47,31 @@ export class IframeEditorToRendererCommunicator extends IframeCommunicator<Edito
this.onFirstHeadingChangeHandler = handler
}
public onMetaDataChange (handler?: (metaData?: YAMLMetaData) => void): void {
this.onMetaDataChangeHandler = handler
protected handleEvent (event: MessageEvent<RendererToEditorIframeMessage>): boolean | undefined {
const renderMessage = event.data
switch (renderMessage.type) {
case RenderIframeMessageType.RENDERER_READY:
this.onRendererReadyHandler?.()
return false
case RenderIframeMessageType.SET_SCROLL_SOURCE_TO_RENDERER:
this.onSetScrollSourceToRendererHandler?.()
return false
case RenderIframeMessageType.SET_SCROLL_STATE:
this.onSetScrollStateHandler?.(renderMessage.scrollState)
return false
case RenderIframeMessageType.ON_FIRST_HEADING_CHANGE:
this.onFirstHeadingChangeHandler?.(renderMessage.firstHeading)
return false
case RenderIframeMessageType.ON_TASK_CHECKBOX_CHANGE:
this.onTaskCheckboxChangeHandler?.(renderMessage.lineInMarkdown, renderMessage.checked)
return false
case RenderIframeMessageType.ON_SET_FRONTMATTER:
this.onFrontmatterChangeHandler?.(renderMessage.frontmatter)
return false
case RenderIframeMessageType.IMAGE_CLICKED:
this.onImageClickedHandler?.(renderMessage.details)
return false
}
}
public onSetScrollState (handler?: (scrollState: ScrollState) => void): void {

View file

@ -4,8 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NoteFrontmatter } from "../editor/note-frontmatter/note-frontmatter"
import { ScrollState } from "../editor/scroll/scroll-props"
import { YAMLMetaData } from "../editor/yaml-metadata/yaml-metadata"
import { IframeCommunicator } from "./iframe-communicator"
import {
EditorToRendererIframeMessage,
@ -68,10 +68,10 @@ export class IframeRendererToEditorCommunicator extends IframeCommunicator<Rende
})
}
public sendSetMetaData (metaData: YAMLMetaData | undefined): void {
public sendSetFrontmatter (frontmatter: NoteFrontmatter | undefined): void {
this.sendMessageToOtherSide({
type: RenderIframeMessageType.ON_SET_META_DATA,
metaData
type: RenderIframeMessageType.ON_SET_FRONTMATTER,
frontmatter: frontmatter
})
}

View file

@ -3,16 +3,15 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import equal from "fast-deep-equal"
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useSelector } from 'react-redux'
import { useApplyDarkMode } from '../../hooks/common/use-apply-dark-mode'
import { ApplicationState } from '../../redux'
import { setDarkMode } from '../../redux/dark-mode/methods'
import { setDocumentMetadata } from '../../redux/document-content/methods'
import { ScrollingDocumentRenderPane } from '../editor/document-renderer-pane/scrolling-document-render-pane'
import { setNoteFrontmatter } from '../../redux/note-details/methods'
import { DocumentRenderPane } from '../editor/document-renderer-pane/document-render-pane'
import { NoteFrontmatter } from '../editor/note-frontmatter/note-frontmatter'
import { ScrollState } from '../editor/scroll/scroll-props'
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
import { IframeRendererToEditorCommunicator } from './iframe-renderer-to-editor-communicator'
@ -41,11 +40,7 @@ export const RenderPage: React.FC = () => {
useEffect(() => iframeCommunicator.onSetMarkdownContent(setMarkdownContent), [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetDarkMode(setDarkMode), [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetWide(setWide), [iframeCommunicator])
useEffect(() => iframeCommunicator.onSetScrollState((newScrollState) => {
if (!equal(scrollState, newScrollState)) {
setScrollState(newScrollState)
}
}), [iframeCommunicator, scrollState])
useEffect(() => iframeCommunicator.onSetScrollState(setScrollState), [iframeCommunicator, scrollState])
const onTaskCheckedChange = useCallback((lineInMarkdown: number, checked: boolean) => {
iframeCommunicator.sendTaskCheckBoxChange(lineInMarkdown, checked)
@ -59,9 +54,9 @@ export const RenderPage: React.FC = () => {
iframeCommunicator.sendSetScrollSourceToRenderer()
}, [iframeCommunicator])
const onMetaDataChange = useCallback((metaData?: YAMLMetaData) => {
setDocumentMetadata(metaData)
iframeCommunicator.sendSetMetaData(metaData)
const onFrontmatterChange = useCallback((frontmatter?: NoteFrontmatter) => {
setNoteFrontmatter(frontmatter)
iframeCommunicator.sendSetFrontmatter(frontmatter)
}, [iframeCommunicator])
const onScroll = useCallback((scrollState: ScrollState) => {
@ -86,14 +81,14 @@ export const RenderPage: React.FC = () => {
return (
<div className={"vh-100 w-100"}>
<ScrollingDocumentRenderPane
<DocumentRenderPane
extraClasses={'w-100'}
markdownContent={markdownContent}
wide={isWide}
onTaskCheckedChange={onTaskCheckedChange}
onFirstHeadingChange={onFirstHeadingChange}
onMakeScrollSource={onMakeScrollSource}
onMetadataChange={onMetaDataChange}
onFrontmatterChange={onFrontmatterChange}
scrollState={scrollState}
onScroll={onScroll}
baseUrl={baseUrl}

View file

@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NoteFrontmatter } from '../editor/note-frontmatter/note-frontmatter'
import { ScrollState } from '../editor/scroll/scroll-props'
import { YAMLMetaData } from '../editor/yaml-metadata/yaml-metadata'
export enum RenderIframeMessageType {
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
@ -15,7 +15,7 @@ export enum RenderIframeMessageType {
ON_FIRST_HEADING_CHANGE = 'ON_FIRST_HEADING_CHANGE',
SET_SCROLL_SOURCE_TO_RENDERER = 'SET_SCROLL_SOURCE_TO_RENDERER',
SET_SCROLL_STATE = 'SET_SCROLL_STATE',
ON_SET_META_DATA = 'ON_SET_META_DATA',
ON_SET_FRONTMATTER = 'ON_SET_FRONTMATTER',
IMAGE_CLICKED = 'IMAGE_CLICKED',
SET_BASE_URL = 'SET_BASE_URL'
}
@ -71,9 +71,9 @@ export interface OnFirstHeadingChangeMessage {
firstHeading: string | undefined
}
export interface OnMetadataChangeMessage {
type: RenderIframeMessageType.ON_SET_META_DATA,
metaData: YAMLMetaData | undefined
export interface OnFrontmatterChangeMessage {
type: RenderIframeMessageType.ON_SET_FRONTMATTER,
frontmatter: NoteFrontmatter | undefined
}
export type EditorToRendererIframeMessage =
@ -87,6 +87,6 @@ export type RendererToEditorIframeMessage =
RendererToEditorSimpleMessage |
OnFirstHeadingChangeMessage |
OnTaskCheckboxChangeMessage |
OnMetadataChangeMessage |
OnFrontmatterChangeMessage |
SetScrollStateMessage |
ImageClickedMessage

View file

@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { ApplicationState } from '../../redux'
import { useDocumentTitle } from './use-document-title'
export const useDocumentTitleWithNoteTitle = (): void => {
const { t } = useTranslation()
const untitledNote = t('editor.untitledNote')
const noteTitle = useSelector((state: ApplicationState) => state.noteDetails.noteTitle)
useDocumentTitle(noteTitle === '' ? untitledNote : noteTitle)
}

View file

@ -0,0 +1,12 @@
/*
* 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'
export const useNoteMarkdownContent = (): string => {
return useSelector((state: ApplicationState) => state.noteDetails.markdownContent)
}

View file

@ -1,8 +1,8 @@
/*
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 from 'react'
import ReactDOM from 'react-dom'
@ -86,7 +86,7 @@ ReactDOM.render(
)
if (isTestMode()) {
console.log("This build runs in test mode. This means:\n - No default content in the editor\n - no sandboxed iframe")
console.log("This build runs in test mode. This means:\n - no sandboxed iframe")
}
// If you want your app to work offline and load faster, you can change

View file

@ -1,42 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { store } from '..'
import { YAMLMetaData } from '../../components/editor/yaml-metadata/yaml-metadata'
import { initialState } from './reducers'
import {
DocumentContentActionType,
SetDocumentContentAction,
SetDocumentMetadataAction,
SetNoteIdAction
} from './types'
export const setDocumentContent = (content: string): void => {
const action: SetDocumentContentAction = {
type: DocumentContentActionType.SET_DOCUMENT_CONTENT,
content
}
store.dispatch(action)
}
export const setNoteId = (noteId: string): void => {
const action: SetNoteIdAction = {
type: DocumentContentActionType.SET_NOTE_ID,
noteId
}
store.dispatch(action)
}
export const setDocumentMetadata = (metadata: YAMLMetaData | undefined): void => {
if (!metadata) {
metadata = initialState.metadata
}
const action: SetDocumentMetadataAction = {
type: DocumentContentActionType.SET_DOCUMENT_METADATA,
metadata
}
store.dispatch(action)
}

View file

@ -1,56 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Reducer } from 'redux'
import {
DocumentContent,
DocumentContentAction,
DocumentContentActionType,
SetDocumentContentAction,
SetDocumentMetadataAction,
SetNoteIdAction
} from './types'
export const initialState: DocumentContent = {
content: '',
noteId: '',
metadata: {
title: '',
description: '',
tags: [],
deprecatedTagsSyntax: false,
robots: '',
lang: 'en',
dir: 'ltr',
breaks: true,
GA: '',
disqus: '',
type: '',
opengraph: new Map<string, string>()
}
}
export const DocumentContentReducer: Reducer<DocumentContent, DocumentContentAction> = (state: DocumentContent = initialState, action: DocumentContentAction) => {
switch (action.type) {
case DocumentContentActionType.SET_DOCUMENT_CONTENT:
return {
...state,
content: (action as SetDocumentContentAction).content
}
case DocumentContentActionType.SET_NOTE_ID:
return {
...state,
noteId: (action as SetNoteIdAction).noteId
}
case DocumentContentActionType.SET_DOCUMENT_METADATA:
return {
...state,
metadata: (action as SetDocumentMetadataAction).metadata
}
default:
return state
}
}

View file

@ -1,36 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Action } from 'redux'
import { YAMLMetaData } from '../../components/editor/yaml-metadata/yaml-metadata'
export enum DocumentContentActionType {
SET_DOCUMENT_CONTENT = 'document-content/set',
SET_NOTE_ID = 'document-content/noteid/set',
SET_DOCUMENT_METADATA = 'document-content/metadata/set'
}
export interface DocumentContent {
content: string
noteId: string,
metadata: YAMLMetaData
}
export interface DocumentContentAction extends Action<DocumentContentActionType> {
type: DocumentContentActionType
}
export interface SetDocumentContentAction extends DocumentContentAction {
content: string
}
export interface SetNoteIdAction extends DocumentContentAction {
noteId: string
}
export interface SetDocumentMetadataAction extends DocumentContentAction {
metadata: YAMLMetaData
}

View file

@ -13,10 +13,10 @@ import { BannerState } from './banner/types'
import { ConfigReducer } from './config/reducers'
import { DarkModeConfigReducer } from './dark-mode/reducers'
import { DarkModeConfig } from './dark-mode/types'
import { DocumentContentReducer } from './document-content/reducers'
import { DocumentContent } from './document-content/types'
import { EditorConfigReducer } from './editor/reducers'
import { EditorConfig } from './editor/types'
import { NoteDetailsReducer } from './note-details/reducers'
import { NoteDetails } from './note-details/types'
import { UserReducer } from './user/reducers'
import { MaybeUserState } from './user/types'
@ -27,7 +27,7 @@ export interface ApplicationState {
apiUrl: ApiUrlObject;
editorConfig: EditorConfig;
darkMode: DarkModeConfig;
documentContent: DocumentContent;
noteDetails: NoteDetails;
}
export const allReducers: Reducer<ApplicationState> = combineReducers<ApplicationState>({
@ -37,7 +37,7 @@ export const allReducers: Reducer<ApplicationState> = combineReducers<Applicatio
apiUrl: ApiUrlReducer,
editorConfig: EditorConfigReducer,
darkMode: DarkModeConfigReducer,
documentContent: DocumentContentReducer
noteDetails: NoteDetailsReducer
})
export const store = createStore(allReducers)

View file

@ -0,0 +1,57 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { store } from '..'
import { Note } from '../../api/notes'
import { NoteFrontmatter } from '../../components/editor/note-frontmatter/note-frontmatter'
import { initialState } from './reducers'
import {
NoteDetailsActionType,
SetCheckboxInMarkdownContentAction,
SetNoteDetailsAction,
SetNoteDetailsFromServerAction,
SetNoteFrontmatterFromRenderingAction,
UpdateNoteTitleByFirstHeadingAction
} from './types'
export const setNoteMarkdownContent = (content: string): void => {
store.dispatch({
type: NoteDetailsActionType.SET_DOCUMENT_CONTENT,
content
} as SetNoteDetailsAction)
}
export const setNoteDataFromServer = (apiResponse: Note): void => {
store.dispatch({
type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER,
note: apiResponse
} as SetNoteDetailsFromServerAction)
}
export const updateNoteTitleByFirstHeading = (firstHeading?: string): void => {
store.dispatch({
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING,
firstHeading: firstHeading ?? ''
} as UpdateNoteTitleByFirstHeadingAction)
}
export const setNoteFrontmatter = (frontmatter: NoteFrontmatter | undefined): void => {
if (!frontmatter) {
frontmatter = initialState.frontmatter
}
store.dispatch({
type: NoteDetailsActionType.SET_NOTE_FRONTMATTER,
frontmatter: frontmatter
} as SetNoteFrontmatterFromRenderingAction)
}
export const SetCheckboxInMarkdownContent = (lineInMarkdown: number, checked: boolean): void => {
store.dispatch({
type: NoteDetailsActionType.SET_CHECKBOX_IN_MARKDOWN_CONTENT,
checked: checked,
lineInMarkdown: lineInMarkdown
} as SetCheckboxInMarkdownContentAction)
}

View file

@ -0,0 +1,127 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { DateTime } from 'luxon'
import { Reducer } from 'redux'
import { Note } from '../../api/notes'
import { NoteFrontmatter } from '../../components/editor/note-frontmatter/note-frontmatter'
import {
NoteDetails,
NoteDetailsAction,
NoteDetailsActionType,
SetCheckboxInMarkdownContentAction,
SetNoteDetailsAction,
SetNoteDetailsFromServerAction,
SetNoteFrontmatterFromRenderingAction,
UpdateNoteTitleByFirstHeadingAction
} from './types'
export const initialState: NoteDetails = {
markdownContent: '',
id: '',
createTime: DateTime.fromSeconds(0),
lastChange: {
timestamp: DateTime.fromSeconds(0),
userId: ''
},
alias: '',
preVersionTwoNote: false,
viewCount: 0,
authorship: [],
noteTitle: '',
firstHeading: '',
frontmatter: {
title: '',
description: '',
tags: [],
deprecatedTagsSyntax: false,
robots: '',
lang: 'en',
dir: 'ltr',
breaks: true,
GA: '',
disqus: '',
type: '',
opengraph: new Map<string, string>()
}
}
export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsAction> = (state: NoteDetails = initialState, action: NoteDetailsAction) => {
switch (action.type) {
case NoteDetailsActionType.SET_DOCUMENT_CONTENT:
return {
...state,
markdownContent: (action as SetNoteDetailsAction).content
}
case NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING:
return {
...state,
firstHeading: (action as UpdateNoteTitleByFirstHeadingAction).firstHeading,
noteTitle: generateNoteTitle(state.frontmatter, (action as UpdateNoteTitleByFirstHeadingAction).firstHeading)
}
case NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER:
return convertNoteToNoteDetails((action as SetNoteDetailsFromServerAction).note)
case NoteDetailsActionType.SET_NOTE_FRONTMATTER:
return {
...state,
frontmatter: (action as SetNoteFrontmatterFromRenderingAction).frontmatter,
noteTitle: generateNoteTitle((action as SetNoteFrontmatterFromRenderingAction).frontmatter, state.firstHeading)
}
case NoteDetailsActionType.SET_CHECKBOX_IN_MARKDOWN_CONTENT:
return {
...state,
markdownContent: setCheckboxInMarkdownContent(
state.markdownContent,
(action as SetCheckboxInMarkdownContentAction).lineInMarkdown,
(action as SetCheckboxInMarkdownContentAction).checked
)
}
default:
return state
}
}
const TASK_REGEX = /(\s*[-*] )(\[[ xX]])( .*)/
const setCheckboxInMarkdownContent = (markdownContent: string, lineInMarkdown: number, checked: boolean): string => {
const lines = markdownContent.split('\n')
const results = TASK_REGEX.exec(lines[lineInMarkdown])
if (results) {
const before = results[1]
const after = results[3]
lines[lineInMarkdown] = `${before}[${checked ? 'x' : ' '}]${after}`
return lines.join('\n')
}
return markdownContent
}
const generateNoteTitle = (frontmatter: NoteFrontmatter, firstHeading?: string) => {
if (frontmatter?.title && frontmatter?.title !== '') {
return frontmatter.title.trim()
} else if (frontmatter?.opengraph && frontmatter?.opengraph.get('title') && frontmatter?.opengraph.get('title') !== '') {
return (frontmatter?.opengraph.get('title') ?? firstHeading ?? '').trim()
} else {
return (firstHeading ?? firstHeading ?? '').trim()
}
}
const convertNoteToNoteDetails = (note: Note): NoteDetails => {
return {
markdownContent: note.content,
frontmatter: initialState.frontmatter,
id: note.id,
noteTitle: initialState.noteTitle,
createTime: DateTime.fromSeconds(note.createTime),
lastChange: {
userId: note.lastChange.userId,
timestamp: DateTime.fromSeconds(note.lastChange.timestamp)
},
firstHeading: initialState.firstHeading,
preVersionTwoNote: note.preVersionTwoNote,
viewCount: note.viewCount,
alias: note.alias,
authorship: note.authorship
}
}

View file

@ -0,0 +1,67 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { DateTime } from 'luxon'
import { Action } from 'redux'
import { Note } from '../../api/notes'
import { NoteFrontmatter } from '../../components/editor/note-frontmatter/note-frontmatter'
export enum NoteDetailsActionType {
SET_DOCUMENT_CONTENT = 'note-details/set',
SET_NOTE_DATA_FROM_SERVER = 'note-details/data/server/set',
SET_NOTE_FRONTMATTER = 'note-details/frontmatter/set',
UPDATE_NOTE_TITLE_BY_FIRST_HEADING = 'note-details/update-note-title-by-first-heading',
SET_CHECKBOX_IN_MARKDOWN_CONTENT = 'note-details/toggle-checkbox-in-markdown-content'
}
interface LastChange {
userId: string
timestamp: DateTime
}
export interface NoteDetails {
markdownContent: string
id: string
createTime: DateTime
lastChange: LastChange
preVersionTwoNote: boolean
viewCount: number
alias: string
authorship: number[]
noteTitle: string
firstHeading: string
frontmatter: NoteFrontmatter
}
export interface NoteDetailsAction extends Action<NoteDetailsActionType> {
type: NoteDetailsActionType
}
export interface SetNoteDetailsAction extends NoteDetailsAction {
type: NoteDetailsActionType.SET_DOCUMENT_CONTENT
content: string
}
export interface SetNoteDetailsFromServerAction extends NoteDetailsAction {
type: NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER
note: Note
}
export interface UpdateNoteTitleByFirstHeadingAction extends NoteDetailsAction {
type: NoteDetailsActionType.UPDATE_NOTE_TITLE_BY_FIRST_HEADING
firstHeading: string
}
export interface SetNoteFrontmatterFromRenderingAction extends NoteDetailsAction {
type: NoteDetailsActionType.SET_NOTE_FRONTMATTER
frontmatter: NoteFrontmatter
}
export interface SetCheckboxInMarkdownContentAction extends NoteDetailsAction {
type: NoteDetailsActionType.SET_CHECKBOX_IN_MARKDOWN_CONTENT,
lineInMarkdown: number,
checked: boolean
}