Move frontmatter extraction from renderer to redux (#1413)

This commit is contained in:
Erik Michelson 2021-09-02 11:15:31 +02:00 committed by GitHub
parent 7fb7c55877
commit 04e16d8880
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 680 additions and 589 deletions

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import React, { Ref, useCallback, useMemo, useRef, useState } from 'react'
import React, { Ref, useCallback, 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'
@ -12,7 +12,6 @@ import { ComponentReplacer } from './replace-components/ComponentReplacer'
import { AdditionalMarkdownRendererProps, LineMarkerPosition } from './types'
import { useComponentReplacers } from './hooks/use-component-replacers'
import { useTranslation } from 'react-i18next'
import { NoteFrontmatter, RawNoteFrontmatter } from '../editor-page/note-frontmatter/note-frontmatter'
import { LineMarkers } from './replace-components/linemarker/line-number-marker'
import { useCalculateLineMarkerPosition } from './utils/calculate-line-marker-positions'
import { useExtractFirstHeadline } from './hooks/use-extract-first-headline'
@ -20,7 +19,6 @@ import { TocAst } from 'markdown-it-toc-done-right'
import { useOnRefChange } from './hooks/use-on-ref-change'
import { BasicMarkdownItConfigurator } from './markdown-it-configurator/BasicMarkdownItConfigurator'
import { ImageClickHandler } from './replace-components/image/image-replacer'
import { InvalidYamlAlert } from './invalid-yaml-alert'
import { useTrimmedContent } from './hooks/use-trimmed-content'
export interface BasicMarkdownRendererProps {
@ -29,79 +27,57 @@ export interface BasicMarkdownRendererProps {
onAfterRendering?: () => void
onFirstHeadingChange?: (firstHeading: string | undefined) => void
onLineMarkerPositionChanged?: (lineMarkerPosition: LineMarkerPosition[]) => void
onFrontmatterChange?: (frontmatter: NoteFrontmatter | undefined) => void
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
onTocChange?: (ast?: TocAst) => void
baseUrl?: string
onImageClick?: ImageClickHandler
outerContainerRef?: Ref<HTMLDivElement>
useAlternativeBreaks?: boolean
frontmatterLineOffset?: number
}
export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & AdditionalMarkdownRendererProps> = ({
className,
content,
additionalReplacers,
onBeforeRendering,
onAfterRendering,
onFirstHeadingChange,
onLineMarkerPositionChanged,
onFrontmatterChange,
onTaskCheckedChange,
onTocChange,
baseUrl,
onImageClick,
outerContainerRef,
useAlternativeBreaks
useAlternativeBreaks,
frontmatterLineOffset
}) => {
const rawMetaRef = useRef<RawNoteFrontmatter>()
const markdownBodyRef = useRef<HTMLDivElement>(null)
const currentLineMarkers = useRef<LineMarkers[]>()
const hasNewYamlError = useRef(false)
const tocAst = useRef<TocAst>()
const [showYamlError, setShowYamlError] = useState(false)
const [trimmedContent, contentExceedsLimit] = useTrimmedContent(content)
const markdownIt = useMemo(
() =>
new BasicMarkdownItConfigurator({
useFrontmatter: !!onFrontmatterChange,
onParseError: (errorState) => (hasNewYamlError.current = errorState),
onRawMetaChange: (rawMeta) => (rawMetaRef.current = rawMeta),
onToc: (toc) => (tocAst.current = toc),
onLineMarkers:
onLineMarkerPositionChanged === undefined
? undefined
: (lineMarkers) => (currentLineMarkers.current = lineMarkers),
useAlternativeBreaks
useAlternativeBreaks,
offsetLines: frontmatterLineOffset
}).buildConfiguredMarkdownIt(),
[onFrontmatterChange, onLineMarkerPositionChanged, useAlternativeBreaks]
[onLineMarkerPositionChanged, useAlternativeBreaks, frontmatterLineOffset]
)
const clearFrontmatter = useCallback(() => {
hasNewYamlError.current = false
rawMetaRef.current = undefined
onBeforeRendering?.()
}, [onBeforeRendering])
const checkYamlErrorState = useCallback(() => {
setShowYamlError(hasNewYamlError.current)
onAfterRendering?.()
}, [onAfterRendering])
const baseReplacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl)
const baseReplacers = useComponentReplacers(onTaskCheckedChange, onImageClick, baseUrl, frontmatterLineOffset)
const replacers = useCallback(
() => baseReplacers().concat(additionalReplacers ? additionalReplacers() : []),
[additionalReplacers, baseReplacers]
)
const markdownReactDom = useConvertMarkdownToReactDom(
trimmedContent,
markdownIt,
replacers,
clearFrontmatter,
checkYamlErrorState
)
const markdownReactDom = useConvertMarkdownToReactDom(trimmedContent, markdownIt, replacers)
useTranslation()
useCalculateLineMarkerPosition(
@ -112,17 +88,9 @@ export const BasicMarkdownRenderer: React.FC<BasicMarkdownRendererProps & Additi
)
useExtractFirstHeadline(markdownBodyRef, content, onFirstHeadingChange)
useOnRefChange(tocAst, onTocChange)
useOnRefChange(rawMetaRef, (newValue) => {
if (!newValue) {
onFrontmatterChange?.(undefined)
} else {
onFrontmatterChange?.(new NoteFrontmatter(newValue))
}
})
return (
<div ref={outerContainerRef} className={'position-relative'}>
<InvalidYamlAlert show={showYamlError} />
<DocumentLengthLimitReachedAlert show={contentExceedsLimit} />
<div
ref={markdownBodyRef}

View file

@ -32,13 +32,15 @@ import { YoutubeReplacer } from '../replace-components/youtube/youtube-replacer'
* @param onTaskCheckedChange A callback that gets executed if a task checkbox gets clicked
* @param onImageClick A callback that should be executed if an image gets clicked
* @param baseUrl The base url for relative links
* @param frontmatterLinesToSkip The number of lines of the frontmatter part to add this as offset to line-numbers.
*
* @return the created list
*/
export const useComponentReplacers = (
onTaskCheckedChange?: TaskCheckedChangeHandler,
onImageClick?: ImageClickHandler,
baseUrl?: string
baseUrl?: string,
frontmatterLinesToSkip?: number
): (() => ComponentReplacer[]) =>
useCallback(
() => [
@ -59,8 +61,8 @@ export const useComponentReplacers = (
new HighlightedCodeReplacer(),
new ColoredBlockquoteReplacer(),
new KatexReplacer(),
new TaskListReplacer(onTaskCheckedChange),
new TaskListReplacer(onTaskCheckedChange, frontmatterLinesToSkip),
new LinkReplacer(baseUrl)
],
[onImageClick, onTaskCheckedChange, baseUrl]
[onImageClick, onTaskCheckedChange, baseUrl, frontmatterLinesToSkip]
)

View file

@ -19,7 +19,6 @@ import { MarkdownItParserDebugger } from '../markdown-it-plugins/parser-debugger
import { spoilerContainer } from '../markdown-it-plugins/spoiler-container'
import { tasksLists } from '../markdown-it-plugins/tasks-lists'
import { twitterEmojis } from '../markdown-it-plugins/twitter-emojis'
import { RawNoteFrontmatter } from '../../editor-page/note-frontmatter/note-frontmatter'
import { TocAst } from 'markdown-it-toc-done-right'
import { LineMarkers, lineNumberMarker } from '../replace-components/linemarker/line-number-marker'
import { plantumlWithError } from '../markdown-it-plugins/plantuml'
@ -36,15 +35,13 @@ import { highlightedCode } from '../markdown-it-plugins/highlighted-code'
import { quoteExtraColor } from '../markdown-it-plugins/quote-extra-color'
import { quoteExtra } from '../markdown-it-plugins/quote-extra'
import { documentTableOfContents } from '../markdown-it-plugins/document-table-of-contents'
import { frontmatterExtract } from '../markdown-it-plugins/frontmatter'
export interface ConfiguratorDetails {
useFrontmatter: boolean
onParseError: (error: boolean) => void
onRawMetaChange: (rawMeta: RawNoteFrontmatter) => void
onToc: (toc: TocAst) => void
onLineMarkers?: (lineMarkers: LineMarkers[]) => void
useAlternativeBreaks?: boolean
offsetLines?: number
}
export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
@ -105,17 +102,8 @@ export class BasicMarkdownItConfigurator<T extends ConfiguratorDetails> {
spoilerContainer
)
if (this.options.useFrontmatter) {
this.configurations.push(
frontmatterExtract({
onParseError: this.options.onParseError,
onRawMetaChange: this.options.onRawMetaChange
})
)
}
if (this.options.onLineMarkers) {
this.configurations.push(lineNumberMarker(this.options.onLineMarkers))
this.configurations.push(lineNumberMarker(this.options.onLineMarkers, this.options.offsetLines ?? 0))
}
this.postConfigurations.push(linkifyExtra, MarkdownItParserDebugger)

View file

@ -1,30 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import yaml from 'js-yaml'
import MarkdownIt from 'markdown-it'
import frontmatter from 'markdown-it-front-matter'
import { RawNoteFrontmatter } from '../../editor-page/note-frontmatter/note-frontmatter'
interface FrontmatterPluginOptions {
onParseError: (error: boolean) => void
onRawMetaChange: (rawMeta: RawNoteFrontmatter) => void
}
export const frontmatterExtract: (options: FrontmatterPluginOptions) => MarkdownIt.PluginSimple =
(options) => (markdownIt) => {
frontmatter(markdownIt, (rawMeta: string) => {
try {
const meta: RawNoteFrontmatter = yaml.load(rawMeta) as RawNoteFrontmatter
options.onParseError(false)
options.onRawMetaChange(meta)
} catch (e) {
console.error(e)
options.onParseError(true)
options.onRawMetaChange({} as RawNoteFrontmatter)
}
})
}

View file

@ -18,12 +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) => MarkdownIt.PluginSimple =
(options) => (md: MarkdownIt) => {
export const lineNumberMarker: (options: LineNumberMarkerOptions, offsetLines: number) => MarkdownIt.PluginSimple =
(options, offsetLines = 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)
tagTokens(state.tokens, lineMarkers, offsetLines)
if (options) {
options(lineMarkers)
}
@ -56,7 +57,7 @@ export const lineNumberMarker: (options: LineNumberMarkerOptions) => MarkdownIt.
tokens.splice(tokenPosition, 0, startToken)
}
const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[]) => {
const tagTokens = (tokens: Token[], lineMarkers: LineMarkers[], offsetLines: number) => {
for (let tokenPosition = 0; tokenPosition < tokens.length; tokenPosition++) {
const token = tokens[tokenPosition]
if (token.hidden) {
@ -71,14 +72,14 @@ export const lineNumberMarker: (options: LineNumberMarkerOptions) => MarkdownIt.
const endLineNumber = token.map[1] + 1
if (token.level === 0) {
lineMarkers.push({ startLine: startLineNumber, endLine: endLineNumber })
lineMarkers.push({ startLine: startLineNumber + offsetLines, endLine: endLineNumber + offsetLines })
}
insertNewLineMarker(startLineNumber, endLineNumber, tokenPosition, token.level, tokens)
tokenPosition += 1
if (token.children) {
tagTokens(token.children, lineMarkers)
tagTokens(token.children, lineMarkers, offsetLines)
}
}
}

View file

@ -15,16 +15,18 @@ export type TaskCheckedChangeHandler = (lineInMarkdown: number, checked: boolean
*/
export class TaskListReplacer extends ComponentReplacer {
onTaskCheckedChange?: (lineInMarkdown: number, checked: boolean) => void
private readonly frontmatterLinesOffset
constructor(onTaskCheckedChange?: TaskCheckedChangeHandler) {
constructor(onTaskCheckedChange?: TaskCheckedChangeHandler, frontmatterLinesOffset?: number) {
super()
this.onTaskCheckedChange = onTaskCheckedChange
this.frontmatterLinesOffset = frontmatterLinesOffset ?? 0
}
handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
const lineNum = Number(event.currentTarget.dataset.line)
if (this.onTaskCheckedChange) {
this.onTaskCheckedChange(lineNum, event.currentTarget.checked)
this.onTaskCheckedChange(lineNum + this.frontmatterLinesOffset, event.currentTarget.checked)
}
}