Add slide mode with reveal.js

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2021-10-04 12:50:39 +02:00
parent 29565f8f89
commit 36e445e631
70 changed files with 1225 additions and 323 deletions

View file

@ -8,87 +8,87 @@ import { extractFrontmatter } from './extract-frontmatter'
import { PresentFrontmatterExtractionResult } from './types'
describe('frontmatter extraction', () => {
describe('frontmatterPresent property', () => {
describe('isPresent property', () => {
it('is false when note does not contain three dashes at all', () => {
const testNote = 'abcdef\nmore text'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(false)
expect(extraction.isPresent).toBe(false)
})
it('is false when note does not start with three dashes', () => {
const testNote = '\n---\nthis is not frontmatter'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(false)
expect(extraction.isPresent).toBe(false)
})
it('is false when note start with less than three dashes', () => {
const testNote = '--\nthis is not frontmatter'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(false)
expect(extraction.isPresent).toBe(false)
})
it('is false when note starts with three dashes but contains other characters in the same line', () => {
const testNote = '--- a\nthis is not frontmatter'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(false)
expect(extraction.isPresent).toBe(false)
})
it('is false when note has no ending marker for frontmatter', () => {
const testNote = '---\nthis is not frontmatter\nbecause\nthere is no\nend marker'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(false)
expect(extraction.isPresent).toBe(false)
})
it('is false when note end marker is present but with not the same amount of dashes as start marker', () => {
const testNote = '---\nthis is not frontmatter\n----\ncontent'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(false)
expect(extraction.isPresent).toBe(false)
})
it('is true when note end marker is present with the same amount of dashes as start marker', () => {
const testNote = '---\nthis is frontmatter\n---\ncontent'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(true)
expect(extraction.isPresent).toBe(true)
})
it('is true when note end marker is present with the same amount of dashes as start marker but without content', () => {
const testNote = '---\nthis is frontmatter\n---'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(true)
expect(extraction.isPresent).toBe(true)
})
it('is true when note end marker is present with the same amount of dots as start marker', () => {
const testNote = '---\nthis is frontmatter\n...\ncontent'
const extraction = extractFrontmatter(testNote)
expect(extraction.frontmatterPresent).toBe(true)
expect(extraction.isPresent).toBe(true)
})
})
describe('frontmatterLines property', () => {
describe('lineOffset property', () => {
it('is correct for single line frontmatter without content', () => {
const testNote = '---\nsingle line frontmatter\n...'
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
expect(extraction.frontmatterLines).toEqual(3)
expect(extraction.lineOffset).toEqual(3)
})
it('is correct for single line frontmatter with content', () => {
const testNote = '---\nsingle line frontmatter\n...\ncontent'
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
expect(extraction.frontmatterLines).toEqual(3)
expect(extraction.lineOffset).toEqual(3)
})
it('is correct for multi-line frontmatter without content', () => {
const testNote = '---\nabc\n123\ndef\n...'
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
expect(extraction.frontmatterLines).toEqual(5)
expect(extraction.lineOffset).toEqual(5)
})
it('is correct for multi-line frontmatter with content', () => {
const testNote = '---\nabc\n123\ndef\n...\ncontent'
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
expect(extraction.frontmatterLines).toEqual(5)
expect(extraction.lineOffset).toEqual(5)
})
})
describe('rawFrontmatterText property', () => {
describe('rawText property', () => {
it('contains single-line frontmatter text', () => {
const testNote = '---\nsingle-line\n...\ncontent'
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
expect(extraction.rawFrontmatterText).toEqual('single-line')
expect(extraction.rawText).toEqual('single-line')
})
it('contains multi-line frontmatter text', () => {
const testNote = '---\nmulti\nline\n...\ncontent'
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
expect(extraction.rawFrontmatterText).toEqual('multi\nline')
expect(extraction.rawText).toEqual('multi\nline')
})
})
})

View file

@ -13,7 +13,7 @@ const FRONTMATTER_END_REGEX = /^(?:-{3,}|\.{3,})$/
* A valid frontmatter block requires the content to start with a line containing at least three dashes.
* The block is terminated by a line containing the same amount of dashes or dots as the first line.
* @param content The multiline string from which the frontmatter should be extracted.
* @return { frontmatterPresent } false if no frontmatter block could be found, true if a block was found.
* @return { isPresent } false if no frontmatter block could be found, true if a block was found.
* { rawFrontmatterText } if a block was found, this property contains the extracted text without the fencing.
* { frontmatterLines } if a block was found, this property contains the number of lines to skip from the
* given multiline string for retrieving the non-frontmatter content.
@ -22,19 +22,19 @@ export const extractFrontmatter = (content: string): FrontmatterExtractionResult
const lines = content.split('\n')
if (lines.length < 2 || !FRONTMATTER_BEGIN_REGEX.test(lines[0])) {
return {
frontmatterPresent: false
isPresent: false
}
}
for (let i = 1; i < lines.length; i++) {
if (lines[i].length === lines[0].length && FRONTMATTER_END_REGEX.test(lines[i])) {
return {
frontmatterPresent: true,
rawFrontmatterText: lines.slice(1, i).join('\n'),
frontmatterLines: i + 1
isPresent: true,
rawText: lines.slice(1, i).join('\n'),
lineOffset: i + 1
}
}
}
return {
frontmatterPresent: false
isPresent: false
}
}

View file

@ -4,27 +4,27 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NoteFrontmatter } from './note-frontmatter'
import { createNoteFrontmatterFromYaml } from './note-frontmatter'
describe('yaml frontmatter', () => {
it('should parse "title"', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml('title: test')
const noteFrontmatter = createNoteFrontmatterFromYaml('title: test')
expect(noteFrontmatter.title).toEqual('test')
})
it('should parse "robots"', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml('robots: index, follow')
const noteFrontmatter = createNoteFrontmatterFromYaml('robots: index, follow')
expect(noteFrontmatter.robots).toEqual('index, follow')
})
it('should parse the deprecated tags syntax', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml('tags: test123, abc')
const noteFrontmatter = createNoteFrontmatterFromYaml('tags: test123, abc')
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(true)
})
it('should parse the tags list syntax', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml(`tags:
const noteFrontmatter = createNoteFrontmatterFromYaml(`tags:
- test123
- abc
`)
@ -33,30 +33,30 @@ describe('yaml frontmatter', () => {
})
it('should parse the tag inline-list syntax', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml("tags: ['test123', 'abc']")
const noteFrontmatter = createNoteFrontmatterFromYaml("tags: ['test123', 'abc']")
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(false)
})
it('should parse "breaks"', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml('breaks: false')
const noteFrontmatter = createNoteFrontmatterFromYaml('breaks: false')
expect(noteFrontmatter.breaks).toEqual(false)
})
it('should parse an empty opengraph object', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml('opengraph:')
const noteFrontmatter = createNoteFrontmatterFromYaml('opengraph:')
expect(noteFrontmatter.opengraph).toEqual(new Map<string, string>())
})
it('should parse an opengraph title', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml(`opengraph:
const noteFrontmatter = createNoteFrontmatterFromYaml(`opengraph:
title: Testtitle
`)
expect(noteFrontmatter.opengraph.get('title')).toEqual('Testtitle')
})
it('should parse multiple opengraph values', () => {
const noteFrontmatter = NoteFrontmatter.createFromYaml(`opengraph:
const noteFrontmatter = createNoteFrontmatterFromYaml(`opengraph:
title: Testtitle
image: https://dummyimage.com/48.png
image:type: image/png

View file

@ -6,12 +6,13 @@
// import { RevealOptions } from 'reveal.js'
import { load } from 'js-yaml'
import { ISO6391, NoteTextDirection, NoteType, RawNoteFrontmatter } from './types'
import { ISO6391, NoteTextDirection, NoteType, RawNoteFrontmatter, SlideOptions } from './types'
import { initialSlideOptions } from '../../../redux/note-details/initial-state'
/**
* Class that represents the parsed frontmatter metadata of a note.
*/
export class NoteFrontmatter {
export interface NoteFrontmatter {
title: string
description: string
tags: string[]
@ -24,47 +25,98 @@ export class NoteFrontmatter {
disqus: string
type: NoteType
opengraph: Map<string, string>
slideOptions: SlideOptions
}
/**
* Creates a new frontmatter metadata instance based on the given raw metadata properties.
* @param rawData A {@link RawNoteFrontmatter} object containing the properties of the parsed yaml frontmatter.
*/
constructor(rawData: RawNoteFrontmatter) {
this.title = rawData.title ?? ''
this.description = rawData.description ?? ''
this.robots = rawData.robots ?? ''
this.breaks = rawData.breaks ?? true
this.GA = rawData.GA ?? ''
this.disqus = rawData.disqus ?? ''
this.lang = (rawData.lang ? ISO6391.find((lang) => lang === rawData.lang) : undefined) ?? 'en'
this.type =
(rawData.type ? Object.values(NoteType).find((type) => type === rawData.type) : undefined) ?? NoteType.DOCUMENT
this.dir =
(rawData.dir ? Object.values(NoteTextDirection).find((dir) => dir === rawData.dir) : undefined) ??
NoteTextDirection.LTR
if (typeof rawData?.tags === 'string') {
this.tags = rawData?.tags?.split(',').map((entry) => entry.trim()) ?? []
this.deprecatedTagsSyntax = true
} else if (typeof rawData?.tags === 'object') {
this.tags = rawData?.tags?.filter((tag) => tag !== null) ?? []
this.deprecatedTagsSyntax = false
} else {
this.tags = []
this.deprecatedTagsSyntax = false
}
this.opengraph = rawData?.opengraph
? new Map<string, string>(Object.entries(rawData.opengraph))
: new Map<string, string>()
/**
* Creates a new frontmatter metadata instance based on the given raw metadata properties.
* @param rawData A {@link RawNoteFrontmatter} object containing the properties of the parsed yaml frontmatter.
*/
export const parseRawNoteFrontmatter = (rawData: RawNoteFrontmatter): NoteFrontmatter => {
let tags: string[]
let deprecatedTagsSyntax: boolean
if (typeof rawData?.tags === 'string') {
tags = rawData?.tags?.split(',').map((entry) => entry.trim()) ?? []
deprecatedTagsSyntax = true
} else if (typeof rawData?.tags === 'object') {
tags = rawData?.tags?.filter((tag) => tag !== null) ?? []
deprecatedTagsSyntax = false
} else {
tags = []
deprecatedTagsSyntax = false
}
/**
* Creates a new frontmatter metadata instance based on a raw yaml string.
* @param rawYaml The frontmatter content in yaml format.
* @throws Error when the content string is invalid yaml.
* @return Frontmatter metadata instance containing the parsed properties from the yaml content.
*/
static createFromYaml(rawYaml: string): NoteFrontmatter {
const rawNoteFrontmatter = load(rawYaml) as RawNoteFrontmatter
return new NoteFrontmatter(rawNoteFrontmatter)
return {
title: rawData.title ?? '',
description: rawData.description ?? '',
robots: rawData.robots ?? '',
breaks: rawData.breaks ?? true,
GA: rawData.GA ?? '',
disqus: rawData.disqus ?? '',
lang: (rawData.lang ? ISO6391.find((lang) => lang === rawData.lang) : undefined) ?? 'en',
type:
(rawData.type ? Object.values(NoteType).find((type) => type === rawData.type) : undefined) ?? NoteType.DOCUMENT,
dir:
(rawData.dir ? Object.values(NoteTextDirection).find((dir) => dir === rawData.dir) : undefined) ??
NoteTextDirection.LTR,
opengraph: rawData?.opengraph
? new Map<string, string>(Object.entries(rawData.opengraph))
: new Map<string, string>(),
slideOptions: parseSlideOptions(rawData),
tags,
deprecatedTagsSyntax
}
}
/**
* Parses the {@link SlideOptions} from the {@link RawNoteFrontmatter}.
*
* @param rawData The raw note frontmatter data.
* @return the parsed slide options
*/
const parseSlideOptions = (rawData: RawNoteFrontmatter): SlideOptions => {
const rawSlideOptions = rawData?.slideOptions
return {
autoSlide: parseNumber(rawSlideOptions?.autoSlide) ?? initialSlideOptions.autoSlide,
transition: rawSlideOptions?.transition ?? initialSlideOptions.transition,
backgroundTransition: rawSlideOptions?.backgroundTransition ?? initialSlideOptions.backgroundTransition,
autoSlideStoppable: parseBoolean(rawSlideOptions?.autoSlideStoppable) ?? initialSlideOptions.autoSlideStoppable,
slideNumber: parseBoolean(rawSlideOptions?.slideNumber) ?? initialSlideOptions.slideNumber
}
}
/**
* Parses an unknown variable into a boolean.
*
* @param rawData The raw data
* @return The parsed boolean or undefined if it's not possible to parse the data.
*/
const parseBoolean = (rawData: unknown | undefined): boolean | undefined => {
return rawData === undefined ? undefined : rawData === true
}
/**
* Parses an unknown variable into a number.
*
* @param rawData The raw data
* @return The parsed number or undefined if it's not possible to parse the data.
*/
const parseNumber = (rawData: unknown | undefined): number | undefined => {
if (rawData === undefined) {
return undefined
}
const numValue = Number(rawData)
return isNaN(numValue) ? undefined : numValue
}
/**
* Creates a new frontmatter metadata instance based on a raw yaml string.
* @param rawYaml The frontmatter content in yaml format.
* @throws Error when the content string is invalid yaml.
* @return Frontmatter metadata instance containing the parsed properties from the yaml content.
*/
export const createNoteFrontmatterFromYaml = (rawYaml: string): NoteFrontmatter => {
const rawNoteFrontmatter = load(rawYaml) as RawNoteFrontmatter
return parseRawNoteFrontmatter(rawNoteFrontmatter)
}

View file

@ -4,22 +4,33 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { RevealOptions } from 'reveal.js'
export type FrontmatterExtractionResult = PresentFrontmatterExtractionResult | NonPresentFrontmatterExtractionResult
export type WantedRevealOptions =
| 'autoSlide'
| 'autoSlideStoppable'
| 'transition'
| 'backgroundTransition'
| 'slideNumber'
export type SlideOptions = Required<Pick<RevealOptions, WantedRevealOptions>>
export interface RendererFrontmatterInfo {
offsetLines: number
lineOffset: number
frontmatterInvalid: boolean
deprecatedSyntax: boolean
slideOptions: SlideOptions
}
export interface PresentFrontmatterExtractionResult {
frontmatterPresent: true
rawFrontmatterText: string
frontmatterLines: number
isPresent: true
rawText: string
lineOffset: number
}
interface NonPresentFrontmatterExtractionResult {
frontmatterPresent: false
isPresent: false
}
export interface RawNoteFrontmatter {
@ -33,7 +44,7 @@ export interface RawNoteFrontmatter {
GA: string | undefined
disqus: string | undefined
type: string | undefined
slideOptions: unknown
slideOptions: { [key: string]: string } | null
opengraph: { [key: string]: string } | null
}

View file

@ -50,21 +50,7 @@ export const DocumentReadOnlyPage: React.FC = () => {
<LoadingNoteAlert show={loading} />
</div>
<ShowIf condition={!error && !loading}>
<DocumentInfobar
changedAuthor={noteDetails.lastChange.userName ?? ''}
changedTime={noteDetails.lastChange.timestamp}
createdAuthor={'Test'}
createdTime={noteDetails.createTime}
editable={true}
noteId={id}
viewCount={noteDetails.viewCount}
/>
<RenderIframe
frameClasses={'flex-fill h-100 w-100'}
markdownContent={markdownContent}
onFirstHeadingChange={onFirstHeadingChange}
rendererType={RendererType.DOCUMENT}
/>
<DocumentReadOnlyPageContent />
</ShowIf>
</div>
</EditorToRendererCommunicatorContextProvider>

View file

@ -15,7 +15,7 @@ export interface CheatsheetLineProps {
const HighlightedCode = React.lazy(
() => import('../../../markdown-renderer/replace-components/highlighted-fence/highlighted-code/highlighted-code')
)
const BasicMarkdownRenderer = React.lazy(() => import('../../../markdown-renderer/basic-markdown-renderer'))
const DocumentMarkdownRenderer = React.lazy(() => import('../../../markdown-renderer/document-markdown-renderer'))
export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ code, onTaskCheckedChange }) => {
const checkboxClick = useCallback(
@ -36,7 +36,11 @@ export const CheatsheetLine: React.FC<CheatsheetLineProps> = ({ code, onTaskChec
}>
<tr>
<td>
<BasicMarkdownRenderer content={code} baseUrl={'https://example.org'} onTaskCheckedChange={checkboxClick} />
<DocumentMarkdownRenderer
content={code}
baseUrl={'https://example.org'}
onTaskCheckedChange={checkboxClick}
/>
</td>
<td className={'markdown-body'}>
<HighlightedCode code={code} wrapLines={true} startLineNumber={1} language={'markdown'} />

View file

@ -21,5 +21,5 @@ export const EditorDocumentRenderer: React.FC<EditorDocumentRendererProps> = (pr
useSendFrontmatterInfoFromReduxToRenderer()
return <RenderIframe frameClasses={'h-100 w-100'} markdownContent={markdownContent} {...props} />
return <RenderIframe markdownContent={markdownContent} {...props} />
}

View file

@ -29,6 +29,7 @@ import { useApplicationState } from '../../hooks/common/use-application-state'
import { EditorDocumentRenderer } from './editor-document-renderer/editor-document-renderer'
import { EditorToRendererCommunicatorContextProvider } from './render-context/editor-to-renderer-communicator-context-provider'
import { Logger } from '../../utils/logger'
import { NoteType } from '../common/note-frontmatter/types'
export interface EditorPagePathParams {
id: string
@ -107,6 +108,7 @@ export const EditorPage: React.FC = () => {
),
[onEditorScroll, scrollState.editorScrollState, setEditorToScrollSource]
)
const noteType: NoteType = useApplicationState((state) => state.noteDetails.frontmatter.type)
const rightPane = useMemo(
() => (
@ -117,10 +119,10 @@ export const EditorPage: React.FC = () => {
onTaskCheckedChange={setCheckboxInMarkdownContent}
onScroll={onMarkdownRendererScroll}
scrollState={scrollState.rendererScrollState}
rendererType={RendererType.DOCUMENT}
rendererType={noteType === NoteType.SLIDE ? RendererType.SLIDESHOW : RendererType.DOCUMENT}
/>
),
[onMarkdownRendererScroll, scrollState.rendererScrollState, setRendererToScrollSource]
[noteType, onMarkdownRendererScroll, scrollState.rendererScrollState, setRendererToScrollSource]
)
return (

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useEffect, useRef } from 'react'
import { RendererType } from '../../../render-page/window-post-message-communicator/rendering-message'
/**
* Execute the given reload callback if the given render type changes.
*
* @param rendererType The render type to watch
* @param effectCallback The callback that should be executed if the render type changes.
*/
export const useEffectOnRenderTypeChange = (rendererType: RendererType, effectCallback: () => void): void => {
const lastRendererType = useRef<RendererType>(rendererType)
useEffect(() => {
if (lastRendererType.current === rendererType) {
return
}
effectCallback()
lastRendererType.current = rendererType
}, [effectCallback, rendererType])
}

View file

@ -5,9 +5,11 @@
*/
import { useSendToRenderer } from '../../../render-page/window-post-message-communicator/hooks/use-send-to-renderer'
import { useMemo } from 'react'
import { useMemo, useRef } from 'react'
import { CommunicationMessageType } from '../../../render-page/window-post-message-communicator/rendering-message'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
import { RendererFrontmatterInfo } from '../../../common/note-frontmatter/types'
import equal from 'fast-deep-equal'
/**
* Extracts the {@link RendererFrontmatterInfo frontmatter data}
@ -15,14 +17,24 @@ import { useApplicationState } from '../../../../hooks/common/use-application-st
*/
export const useSendFrontmatterInfoFromReduxToRenderer = (): void => {
const frontmatterInfo = useApplicationState((state) => state.noteDetails.frontmatterRendererInfo)
const lastFrontmatter = useRef<RendererFrontmatterInfo | undefined>(undefined)
const cachedFrontmatterInfo = useMemo(() => {
if (lastFrontmatter.current !== undefined && equal(lastFrontmatter.current, frontmatterInfo)) {
return lastFrontmatter.current
} else {
lastFrontmatter.current = frontmatterInfo
return frontmatterInfo
}
}, [frontmatterInfo])
return useSendToRenderer(
useMemo(
() => ({
type: CommunicationMessageType.SET_FRONTMATTER_INFO,
frontmatterInfo
frontmatterInfo: cachedFrontmatterInfo
}),
[frontmatterInfo]
[cachedFrontmatterInfo]
)
)
}

View file

@ -25,6 +25,7 @@ import { useSendMarkdownToRenderer } from './hooks/use-send-markdown-to-renderer
import { useSendScrollState } from './hooks/use-send-scroll-state'
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { Logger } from '../../../utils/logger'
import { useEffectOnRenderTypeChange } from './hooks/use-effect-on-render-type-change'
export interface RenderIframeProps extends RendererProps {
rendererType: RendererType
@ -50,9 +51,8 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
const iframeCommunicator = useEditorToRendererCommunicator()
const resetRendererReady = useCallback(() => {
log.debug('Reset render status')
iframeCommunicator.unsetMessageTarget()
setRendererStatus(false)
}, [iframeCommunicator])
}, [])
const rendererReady = useIsRendererReady()
const onIframeLoad = useForceRenderPageUrlOnIframeLoadCallback(frameReference, rendererOrigin, resetRendererReady)
const [frameHeight, setFrameHeight] = useState<number>(0)
@ -65,6 +65,12 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
[iframeCommunicator]
)
useEffect(() => {
if (!rendererReady) {
iframeCommunicator.unsetMessageTarget()
}
}, [iframeCommunicator, rendererReady])
useEditorReceiveHandler(
CommunicationMessageType.ON_FIRST_HEADING_CHANGE,
useCallback(
@ -123,6 +129,7 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
}, [iframeCommunicator, rendererOrigin, rendererType])
)
useEffectOnRenderTypeChange(rendererType, onIframeLoad)
useSendScrollState(scrollState)
useSendDarkModeStatusToRenderer(forcedDarkMode)
useSendMarkdownToRenderer(markdownContent)
@ -136,7 +143,9 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
onLoad={onIframeLoad}
title='render'
{...(isTestMode() ? {} : { sandbox: 'allow-downloads allow-same-origin allow-scripts allow-popups' })}
allowFullScreen={true}
ref={frameReference}
referrerPolicy={'no-referrer'}
className={`border-0 ${frameClasses ?? ''}`}
data-content-ready={rendererReady}
/>

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { TocAst } from 'markdown-it-toc-done-right'
import { ImageClickHandler } from './replace-components/image/image-replacer'
import { Ref } from 'react'
export interface CommonMarkdownRendererProps {
onFirstHeadingChange?: (firstHeading: string | undefined) => void
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
onTocChange?: (ast?: TocAst) => void
baseUrl?: string
onImageClick?: ImageClickHandler
outerContainerRef?: Ref<HTMLDivElement>
useAlternativeBreaks?: boolean
lineOffset?: number
className?: string
content: string
}

View file

@ -4,12 +4,11 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Ref, useCallback, useMemo, useRef } from 'react'
import React, { useMemo, useRef } from 'react'
import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert'
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
import './markdown-renderer.scss'
import { ComponentReplacer } from './replace-components/ComponentReplacer'
import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
import { LineMarkerPosition } from './types'
import { useComponentReplacers } from './hooks/use-component-replacers'
import { useTranslation } from 'react-i18next'
import { LineMarkers } from './replace-components/linemarker/line-number-marker'
@ -18,28 +17,16 @@ import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
import { TocAst } from 'markdown-it-toc-done-right'
import { useOnRefChange } from './hooks/use-on-ref-change'
import { BasicMarkdownItConfigurator } from './markdown-it-configurator/basic-markdown-it-configurator'
import { ImageClickHandler } from './replace-components/image/image-replacer'
import { useTrimmedContent } from './hooks/use-trimmed-content'
import { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
export interface BasicMarkdownRendererProps {
additionalReplacers?: () => ComponentReplacer[]
onBeforeRendering?: () => void
onAfterRendering?: () => void
onFirstHeadingChange?: (firstHeading: string | undefined) => void
export interface DocumentMarkdownRendererProps extends CommonMarkdownRendererProps {
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
onTocChange?: (ast?: TocAst) => void
baseUrl?: string
onImageClick?: ImageClickHandler
outerContainerRef?: Ref<HTMLDivElement>
useAlternativeBreaks?: boolean
frontmatterLineOffset?: number
}
export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & AdditionalMarkdownRendererProps> = ({
export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> = ({
className,
content,
additionalReplacers,
onFirstHeadingChange,
onLineMarkerPositionChanged,
onTaskCheckedChange,
@ -48,7 +35,7 @@ export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & Additi
onImageClick,
outerContainerRef,
useAlternativeBreaks,
frontmatterLineOffset
lineOffset
}) => {
const markdownBodyRef = useRef<HTMLDivElement>(null)
const currentLineMarkers = useRef<LineMarkers[]>()
@ -64,17 +51,12 @@ export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & Additi
? undefined
: (lineMarkers) => (currentLineMarkers.current = lineMarkers),
useAlternativeBreaks,
offsetLines: frontmatterLineOffset
lineOffset,
headlineAnchors: true
}).buildConfiguredMarkdownIt(),
[onLineMarkerPositionChanged, useAlternativeBreaks, frontmatterLineOffset]
[onLineMarkerPositionChanged, useAlternativeBreaks, lineOffset]
)
const baseReplacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl, frontmatterLineOffset)
const replacers = useCallback(
() => baseReplacers().concat(additionalReplacers ? additionalReplacers() : []),
[additionalReplacers, baseReplacers]
)
const replacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl, lineOffset)
const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, markdownIt, replacers)
useTranslation()
@ -99,4 +81,4 @@ export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & Additi
)
}
export default BasicMarkdownRenderer
export default DocumentMarkdownRenderer

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useCallback } from 'react'
import { useMemo } from 'react'
import { AbcReplacer } from '../replace-components/abc/abc-replacer'
import { AsciinemaReplacer } from '../replace-components/asciinema/asciinema-replacer'
import { ComponentReplacer } from '../replace-components/ComponentReplacer'
@ -41,8 +41,8 @@ export const useComponentReplacers = (
onImageClick?: ImageClickHandler,
baseUrl?: string,
frontmatterLinesToSkip?: number
): (() => ComponentReplacer[]) =>
useCallback(
): ComponentReplacer[] =>
useMemo(
() => [
new LinemarkerReplacer(),
new GistReplacer(),

View file

@ -11,6 +11,7 @@ import { LineKeys } from '../types'
import { buildTransformer } from '../utils/html-react-transformer'
import { calculateNewLineNumberMapping } from '../utils/line-number-mapping'
import convertHtmlToReact from '@hedgedoc/html-to-react'
import { Document } from 'domhandler'
/**
* Renders markdown code into react elements
@ -18,24 +19,19 @@ import convertHtmlToReact from '@hedgedoc/html-to-react'
* @param markdownCode The markdown code that should be rendered
* @param markdownIt The configured {@link MarkdownIt markdown it} instance that should render the code
* @param replacers A function that provides a list of {@link ComponentReplacer component replacers}
* @param onBeforeRendering A callback that gets executed before the rendering
* @param onAfterRendering A callback that gets executed after the rendering
* @param preprocessNodes A function that processes nodes after parsing the html code that is generated by markdown it.
* @return The React DOM that represents the rendered markdown code
*/
export const useConvertMarkdownToReactDom = (
markdownCode: string,
markdownIt: MarkdownIt,
replacers: () => ComponentReplacer[],
onBeforeRendering?: () => void,
onAfterRendering?: () => void
replacers: ComponentReplacer[],
preprocessNodes?: (nodes: Document) => Document
): ValidReactDomElement[] => {
const oldMarkdownLineKeys = useRef<LineKeys[]>()
const lastUsedLineId = useRef<number>(0)
return useMemo(() => {
if (onBeforeRendering) {
onBeforeRendering()
}
const html = markdownIt.render(markdownCode)
const contentLines = markdownCode.split('\n')
const { lines: newLines, lastUsedLineId: newLastUsedLineId } = calculateNewLineNumberMapping(
@ -46,12 +42,7 @@ export const useConvertMarkdownToReactDom = (
oldMarkdownLineKeys.current = newLines
lastUsedLineId.current = newLastUsedLineId
const currentReplacers = replacers()
const transformer = currentReplacers.length > 0 ? buildTransformer(newLines, currentReplacers) : undefined
const rendering = convertHtmlToReact(html, { transform: transformer })
if (onAfterRendering) {
onAfterRendering()
}
return rendering
}, [onBeforeRendering, markdownIt, markdownCode, replacers, onAfterRendering])
const transformer = replacers.length > 0 ? buildTransformer(newLines, replacers) : undefined
return convertHtmlToReact(html, { transform: transformer, preprocessNodes: preprocessNodes })
}, [markdownIt, markdownCode, replacers, preprocessNodes])
}

View file

@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useEffect, useState } from 'react'
import Reveal from 'reveal.js'
import { Logger } from '../../../utils/logger'
import { SlideOptions } from '../../common/note-frontmatter/types'
const log = new Logger('reveal.js')
export const useReveal = (content: string, slideOptions?: SlideOptions): void => {
const [deck, setDeck] = useState<Reveal>()
const [isInitialized, setIsInitialized] = useState<boolean>(false)
useEffect(() => {
if (isInitialized) {
return
}
setIsInitialized(true)
log.debug('Initialize with slide options', slideOptions)
const reveal = new Reveal({})
reveal
.initialize()
.then(() => {
reveal.layout()
reveal.slide(0, 0, 0)
setDeck(reveal)
log.debug('Initialisation finished')
})
.catch((error) => {
log.error('Error while initializing reveal.js', error)
})
}, [isInitialized, slideOptions])
useEffect(() => {
if (!deck) {
return
}
log.debug('Sync deck')
deck.layout()
}, [content, deck])
useEffect(() => {
if (!deck || slideOptions === undefined || Object.keys(slideOptions).length === 0) {
return
}
log.debug('Apply config', slideOptions)
deck.configure(slideOptions)
}, [deck, slideOptions])
}

View file

@ -35,12 +35,15 @@ import { highlightedCode } from '../markdown-it-plugins/highlighted-code'
import { quoteExtraColor } from '../markdown-it-plugins/quote-extra-color'
import { quoteExtra } from '../markdown-it-plugins/quote-extra'
import { documentTableOfContents } from '../markdown-it-plugins/document-table-of-contents'
import { addSlideSectionsMarkdownItPlugin } from '../markdown-it-plugins/reveal-sections'
export interface ConfiguratorDetails {
onToc: (toc: TocAst) => void
onLineMarkers?: (lineMarkers: LineMarkers[]) => void
useAlternativeBreaks?: boolean
offsetLines?: number
lineOffset?: number
headlineAnchors?: boolean
slideSections?: boolean
}
export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
@ -73,7 +76,6 @@ export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
protected configure(markdownIt: MarkdownIt): void {
this.configurations.push(
plantumlWithError,
headlineAnchors,
KatexReplacer.markdownItPlugin,
YoutubeReplacer.markdownItPlugin,
VimeoReplacer.markdownItPlugin,
@ -101,8 +103,16 @@ export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
spoilerContainer
)
if (this.options.headlineAnchors) {
this.configurations.push(headlineAnchors)
}
if (this.options.slideSections) {
this.configurations.push(addSlideSectionsMarkdownItPlugin)
}
if (this.options.onLineMarkers) {
this.configurations.push(lineNumberMarker(this.options.onLineMarkers, this.options.offsetLines ?? 0))
this.configurations.push(lineNumberMarker(this.options.onLineMarkers, this.options.lineOffset ?? 0))
}
this.postConfigurations.push(linkifyExtra, MarkdownItParserDebugger)

View file

@ -0,0 +1,79 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import MarkdownIt from 'markdown-it/lib'
import Token from 'markdown-it/lib/token'
import StateCore from 'markdown-it/lib/rules_core/state_core'
/**
* This functions adds a 'section close' token at currentTokenIndex in the state's token array,
* replacing the current token, if replaceCurrentToken is true.
* It also returns the currentTokenIndex, that will be increased only if the previous token was not replaced.
*
* @param {number} currentTokenIndex - the current position in the tokens array
* @param {StateCore} state - the state core
* @param {boolean} replaceCurrentToken - if the currentToken should be replaced
*/
const addSectionClose = (currentTokenIndex: number, state: StateCore, replaceCurrentToken: boolean): void => {
const sectionCloseToken = new Token('section', 'section', -1)
state.tokens.splice(currentTokenIndex, replaceCurrentToken ? 1 : 0, sectionCloseToken)
}
/**
* This functions adds a 'section open' token at insertIndex in the state's token array.
*
* @param {number} insertIndex - the index at which the token should be added
* @param {StateCore} state - the state core
*/
const addSectionOpen = (insertIndex: number, state: StateCore): void => {
const sectionOpenToken = new Token('section', 'section', 1)
state.tokens.splice(insertIndex, 0, sectionOpenToken)
}
/**
* Adds a plugin to the given {@link MarkdownIt markdown it instance} that
* replaces splits the content by horizontal lines and groups these blocks into
* html section tags.
*
* @param markdownIt The {@link MarkdownIt markdown it instance} to which the plugin should be added
*/
export const addSlideSectionsMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt): void => {
markdownIt.core.ruler.push('reveal.sections', (state) => {
let sectionBeginIndex = 0
let lastSectionWasBranch = false
for (let currentTokenIndex = 0; currentTokenIndex < state.tokens.length; currentTokenIndex++) {
const currentToken = state.tokens[currentTokenIndex]
if (currentToken.type !== 'hr') {
continue
}
addSectionOpen(sectionBeginIndex, state)
currentTokenIndex += 1
if (currentToken.markup === '---' && lastSectionWasBranch) {
lastSectionWasBranch = false
addSectionClose(currentTokenIndex, state, false)
currentTokenIndex += 1
} else if (currentToken.markup === '----' && !lastSectionWasBranch) {
lastSectionWasBranch = true
addSectionOpen(sectionBeginIndex, state)
currentTokenIndex += 1
}
addSectionClose(currentTokenIndex, state, true)
sectionBeginIndex = currentTokenIndex + 1
}
addSectionOpen(sectionBeginIndex, state)
addSectionClose(state.tokens.length, state, false)
if (lastSectionWasBranch) {
addSectionClose(state.tokens.length, state, false)
}
return true
})
}

View file

@ -0,0 +1,107 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { DataNode, Document, Element, hasChildren, isComment, isTag, Node } from 'domhandler'
import { Logger } from '../../utils/logger'
const log = new Logger('reveal.js > Comment Node Preprocessor')
const revealCommandSyntax = /^\s*\.(\w*):(.*)$/g
const dataAttributesSyntax = /\s*([\w-]*)=(?:"((?:[^"\\]|\\"|\\)*)"|'([^']*)')/g
/**
* Travels through the given {@link Document}, searches for reveal command comments and applies them.
*
* @param doc The document that should be changed
* @return The edited document
*/
export const processRevealCommentNodes = (doc: Document): Document => {
visitNode(doc)
return doc
}
/**
* Processes the given {@link Node} if it is a comment node. If the node has children then all child nodes will be processed.
* @param node The node to process.
*/
const visitNode = (node: Node): void => {
if (isComment(node)) {
processCommentNode(node)
} else if (hasChildren(node)) {
node.childNodes.forEach((childNode) => visitNode(childNode))
}
}
/**
* Processes the given {@link DataNode html comment} by parsing it, finding the element that should be changed and applies the contained changes.
*
* @param node The node that contains the reveal command.
*/
const processCommentNode = (node: DataNode): void => {
const regexResult = node.data.split(revealCommandSyntax)
if (regexResult.length === 1) {
return
}
const parentNode: Element | null = findTargetElement(node, regexResult[1])
if (!parentNode) {
return
}
for (const dataAttribute of regexResult[2].matchAll(dataAttributesSyntax)) {
const attributeName = dataAttribute[1]
const attributeValue = dataAttribute[2] ?? dataAttribute[3]
if (attributeValue) {
log.debug(
`Add attribute "${attributeName}"=>"${attributeValue}" to node`,
parentNode,
'because of',
regexResult[1],
'selector'
)
parentNode.attribs[attributeName] = attributeValue
}
}
}
/**
* Finds the ancestor element that should be changed based on the given selector.
*
* @param node The node whose ancestor should be found.
* @param selector The found ancestor node or null if no node could be found.
*/
const findTargetElement = (node: Node, selector: string): Element | null => {
if (selector === 'slide') {
return findNearestAncestorSection(node)
} else if (selector === 'element') {
return findParentElement(node)
} else {
return null
}
}
/**
* Returns the parent node if it is an {@link Element}.
*
* @param node the found node or null if no parent node exists or if the parent node isn't an {@link Element}.
*/
const findParentElement = (node: Node): Element | null => {
return node.parentNode !== null && isTag(node.parentNode) ? node.parentNode : null
}
/**
* Looks for the nearest ancestor of the node that is a section element.
*
* @param node the found section node or null if no section ancestor could be found.
*/
const findNearestAncestorSection = (node: Node): Element | null => {
let currentNode = node.parentNode
while (currentNode != null) {
if (isTag(currentNode) && currentNode.tagName === 'section') {
break
}
currentNode = node.parentNode
}
return currentNode
}

View file

@ -11,6 +11,8 @@
@import '../../../../../../node_modules/highlight.js/styles/github-dark';
}
position: relative;
code.hljs {
overflow-x: auto;
background-color: rgba(27, 31, 35, .05);
@ -50,10 +52,10 @@
.linenumber {
display: flex;
}
}
&.showGutter .codeline {
margin: 0 0 0 16px;
.codeline {
margin: 0 0 0 16px;
}
}
&.wrapLines .codeline {

View file

@ -18,13 +18,13 @@ export type LineNumberMarkerOptions = (lineMarkers: LineMarkers[]) => void
* This plugin adds markers to the dom, that are used to map line numbers to dom elements.
* It also provides a list of line numbers for the top level dom elements.
*/
export const lineNumberMarker: (options: LineNumberMarkerOptions, offsetLines: number) => MarkdownIt.PluginSimple =
(options, offsetLines = 0) =>
export const lineNumberMarker: (options: LineNumberMarkerOptions, lineOffset: number) => MarkdownIt.PluginSimple =
(options, lineOffset = 0) =>
(md: MarkdownIt) => {
// add app_linemarker token before each opening or self-closing level-0 tag
md.core.ruler.push('line_number_marker', (state) => {
const lineMarkers: LineMarkers[] = []
tagTokens(state.tokens, lineMarkers, offsetLines)
tagTokens(state.tokens, lineMarkers, lineOffset)
if (options) {
options(lineMarkers)
}
@ -57,7 +57,7 @@ export const lineNumberMarker: (options: LineNumberMarkerOptions, offsetLines: n
tokens.splice(tokenPosition, 0, startToken)
}
const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[], offsetLines: number) => {
const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[], lineOffset: number) => {
for (let tokenPosition = 0; tokenPosition < tokens.length; tokenPosition++) {
const token = tokens[tokenPosition]
if (token.hidden) {
@ -72,14 +72,14 @@ export const lineNumberMarker: (options: LineNumberMarkerOptions, offsetLines: n
const endLineNumber = token.map[1] + 1
if (token.level === 0) {
lineMarkers.push({ startLine: startLineNumber + offsetLines, endLine: endLineNumber + offsetLines })
lineMarkers.push({ startLine: startLineNumber + lineOffset, endLine: endLineNumber + lineOffset })
}
insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens)
tokenPosition += 1
if (token.children) {
tagTokens(token.children, lineMarkers, offsetLines)
tagTokens(token.children, lineMarkers, lineOffset)
}
}
}

View file

@ -66,7 +66,7 @@ export const MarkmapFrame: React.FC<MarkmapFrameProps> = ({ code }) => {
}, [code])
return (
<div data-cy={'markmap'}>
<div data-cy={'markmap'} className={'position-relative'}>
<div className={'svg-container'} ref={diagramContainer} />
<div className={'text-right button-inside'}>
<LockButton

View file

@ -0,0 +1,41 @@
/*!
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
// Default mixins and settings -----------------
@import "../../../node_modules/reveal.js/css/theme/template/mixins";
@import "../../../node_modules/reveal.js/css/theme/template/settings";
// ---------------------------------------------
// Override theme settings (see ../template/settings.scss)
$backgroundColor: #191919;
$mainColor: #fff;
$headingColor: #fff;
$mainFontSize: 42px;
$mainFont: 'Source Sans Pro', Helvetica, sans-serif;
$headingFont: 'Source Sans Pro', Helvetica, sans-serif;
$headingTextShadow: none;
$headingLetterSpacing: normal;
$headingTextTransform: uppercase;
$headingFontWeight: 600;
$linkColor: #42affa;
$linkColorHover: lighten($linkColor, 15%);
$selectionBackgroundColor: lighten($linkColor, 25%);
$heading1Size: 2.5em;
$heading2Size: 1.6em;
$heading3Size: 1.3em;
$heading4Size: 1.0em;
// Change text colors against light slide backgrounds
@include light-bg-text-color(#222);
// Theme template ------------------------------
@import "../../../node_modules/reveal.js/css/theme/template/theme";
// ---------------------------------------------

View file

@ -0,0 +1,79 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Fragment, useMemo, useRef } from 'react'
import { useConvertMarkdownToReactDom } from './hooks/use-convert-markdown-to-react-dom'
import './markdown-renderer.scss'
import { useComponentReplacers } from './hooks/use-component-replacers'
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
import { TocAst } from 'markdown-it-toc-done-right'
import { useOnRefChange } from './hooks/use-on-ref-change'
import { useTrimmedContent } from './hooks/use-trimmed-content'
import { useReveal } from './hooks/use-reveal'
import './slideshow.scss'
import { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert'
import { BasicMarkdownItConfigurator } from './markdown-it-configurator/basic-markdown-it-configurator'
import { SlideOptions } from '../common/note-frontmatter/types'
import { processRevealCommentNodes } from './process-reveal-comment-nodes'
import { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps {
slideOptions: SlideOptions
}
export const SlideshowMarkdownRenderer: React.FC<SlideshowMarkdownRendererProps & ScrollProps> = ({
className,
content,
onFirstHeadingChange,
onTaskCheckedChange,
onTocChange,
baseUrl,
onImageClick,
useAlternativeBreaks,
lineOffset,
slideOptions
}) => {
const markdownBodyRef = useRef<HTMLDivElement>(null)
const tocAst = useRef<TocAst>()
const [trimmedContent, contentExceedsLimit] = useTrimmedContent(content)
const markdownIt = useMemo(
() =>
new BasicMarkdownItConfigurator({
onToc: (toc) => (tocAst.current = toc),
useAlternativeBreaks,
lineOffset,
headlineAnchors: false,
slideSections: true
}).buildConfiguredMarkdownIt(),
[lineOffset, useAlternativeBreaks]
)
const replacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl, lineOffset)
const markdownReactDom = useConvertMarkdownToReactDom(
trimmedContent,
markdownIt,
replacers,
processRevealCommentNodes
)
useExtractFirstHeadline(markdownBodyRef, content, onFirstHeadingChange)
useOnRefChange(tocAst, onTocChange)
useReveal(content, slideOptions)
return (
<Fragment>
<DocumentLengthLimitReachedAlert show={contentExceedsLimit} />
<div className={'reveal'}>
<div ref={markdownBodyRef} className={`${className ?? ''} slides`}>
{markdownReactDom}
</div>
</div>
</Fragment>
)
}
export default SlideshowMarkdownRenderer

View file

@ -0,0 +1,14 @@
/*!
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
@import "../../../node_modules/reveal.js/css/reveal";
@import "../../../node_modules/reveal.js/dist/theme/fonts/league-gothic/league-gothic.css";
@import "slide-theme";
//Fix to make transitions work with bootstrap
.reveal [hidden] {
display: block !important;
}

View file

@ -13,8 +13,3 @@ export interface LineMarkerPosition {
line: number
position: number
}
export interface AdditionalMarkdownRendererProps {
className?: string
content: string
}

View file

@ -5,5 +5,7 @@
*/
.button-inside {
margin-top: -31px;
position: absolute;
bottom: 10px;
right: 10px;
}

View file

@ -6,11 +6,11 @@
import React, { useCallback, useState } from 'react'
import { LineMarkerPosition } from '../../../markdown-renderer/types'
import { ScrollState } from '../scroll-props'
import { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
import { useOnUserScroll } from './use-on-user-scroll'
import { useScrollToLineMark } from './use-scroll-to-line-mark'
export const useSyncedScrolling = (
export const useDocumentSyncScrolling = (
outerContainerRef: React.RefObject<HTMLElement>,
rendererRef: React.RefObject<HTMLElement>,
numberOfLines: number,

View file

@ -6,7 +6,7 @@
import { RefObject, useCallback } from 'react'
import { LineMarkerPosition } from '../../../markdown-renderer/types'
import { ScrollState } from '../scroll-props'
import { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
export const useOnUserScroll = (
lineMarks: LineMarkerPosition[] | undefined,

View file

@ -6,8 +6,8 @@
import { RefObject, useCallback, useEffect, useRef } from 'react'
import { LineMarkerPosition } from '../../../markdown-renderer/types'
import { ScrollState } from '../scroll-props'
import { findLineMarks } from '../utils'
import { ScrollState } from '../../../editor-page/synced-scroll/scroll-props'
import { findLineMarks } from '../../../editor-page/synced-scroll/utils'
export const useScrollToLineMark = (
scrollState: ScrollState | undefined,

View file

@ -19,16 +19,14 @@ import { countWords } from './word-counter'
import { RendererFrontmatterInfo } from '../common/note-frontmatter/types'
import { useRendererToEditorCommunicator } from '../editor-page/render-context/renderer-to-editor-communicator-context-provider'
import { useRendererReceiveHandler } from './window-post-message-communicator/hooks/use-renderer-receive-handler'
import { SlideshowMarkdownRenderer } from '../markdown-renderer/slideshow-markdown-renderer'
import { initialState } from '../../redux/note-details/initial-state'
export const IframeMarkdownRenderer: React.FC = () => {
const [markdownContent, setMarkdownContent] = useState('')
const [scrollState, setScrollState] = useState<ScrollState>({ firstLineInView: 1, scrolledPercentage: 0 })
const [baseConfiguration, setBaseConfiguration] = useState<BaseConfiguration | undefined>(undefined)
const [frontmatterInfo, setFrontmatterInfo] = useState<RendererFrontmatterInfo>({
offsetLines: 0,
frontmatterInvalid: false,
deprecatedSyntax: false
})
const [frontmatterInfo, setFrontmatterInfo] = useState<RendererFrontmatterInfo>(initialState.frontmatterRendererInfo)
const communicator = useRendererToEditorCommunicator()
@ -122,6 +120,18 @@ export const IframeMarkdownRenderer: React.FC = () => {
frontmatterInfo={frontmatterInfo}
/>
)
case RendererType.SLIDESHOW:
return (
<SlideshowMarkdownRenderer
content={markdownContent}
baseUrl={baseConfiguration.baseUrl}
onFirstHeadingChange={onFirstHeadingChange}
onImageClick={onImageClick}
scrollState={scrollState}
lineOffset={frontmatterInfo.lineOffset}
slideOptions={frontmatterInfo.slideOptions}
/>
)
case RendererType.INTRO:
return (
<MarkdownDocument

View file

@ -8,9 +8,9 @@ import { TocAst } from 'markdown-it-toc-done-right'
import React, { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
import useResizeObserver from 'use-resize-observer'
import { YamlArrayDeprecationAlert } from '../editor-page/renderer-pane/yaml-array-deprecation-alert'
import { useSyncedScrolling } from '../editor-page/synced-scroll/hooks/use-synced-scrolling'
import { useDocumentSyncScrolling } from './hooks/sync-scroll/use-document-sync-scrolling'
import { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
import { BasicMarkdownRenderer } from '../markdown-renderer/basic-markdown-renderer'
import { DocumentMarkdownRenderer } from '../markdown-renderer/document-markdown-renderer'
import { ImageClickHandler } from '../markdown-renderer/replace-components/image/image-replacer'
import './markdown-document.scss'
import { WidthBasedTableOfContents } from './width-based-table-of-contents'
@ -70,7 +70,7 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
}, [rendererSize.height, onHeightChange])
const contentLineCount = useMemo(() => markdownContent.split('\n').length, [markdownContent])
const [onLineMarkerPositionChanged, onUserScroll] = useSyncedScrolling(
const [onLineMarkerPositionChanged, onUserScroll] = useDocumentSyncScrolling(
internalDocumentRenderPaneRef,
rendererRef,
contentLineCount,
@ -88,7 +88,7 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
<div className={'markdown-document-content'}>
<InvalidYamlAlert show={!!frontmatterInfo?.frontmatterInvalid} />
<YamlArrayDeprecationAlert show={!!frontmatterInfo?.deprecatedSyntax} />
<BasicMarkdownRenderer
<DocumentMarkdownRenderer
outerContainerRef={rendererRef}
className={`mb-3 ${additionalRendererClasses ?? ''}`}
content={markdownContent}
@ -99,7 +99,7 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
baseUrl={baseUrl}
onImageClick={onImageClick}
useAlternativeBreaks={useAlternativeBreaks}
frontmatterLineOffset={frontmatterInfo?.offsetLines}
lineOffset={frontmatterInfo?.lineOffset}
/>
</div>
<div className={'markdown-document-side pt-4'}>

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React from 'react'
import { RenderIframe } from '../editor-page/renderer-pane/render-iframe'
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
import { useTranslation } from 'react-i18next'
import { useLoadNoteFromServer } from '../editor-page/hooks/useLoadNoteFromServer'
import { ShowIf } from '../common/show-if/show-if'
import { updateNoteTitleByFirstHeading } from '../../redux/note-details/methods'
import { RendererType } from '../render-page/window-post-message-communicator/rendering-message'
import { useSendFrontmatterInfoFromReduxToRenderer } from '../editor-page/renderer-pane/hooks/use-send-frontmatter-info-from-redux-to-renderer'
export const SlideShowPage: React.FC = () => {
const markdownContent = useNoteMarkdownContent()
useTranslation()
useSendFrontmatterInfoFromReduxToRenderer()
const [error, loading] = useLoadNoteFromServer()
return (
<ShowIf condition={!error && !loading}>
<div className={'vh-100 vw-100'}>
<RenderIframe
frameClasses={'h-100 w-100'}
markdownContent={markdownContent}
rendererType={RendererType.SLIDESHOW}
onFirstHeadingChange={updateNoteTitleByFirstHeading}
/>
</div>
</ShowIf>
)
}
export default SlideShowPage