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

@ -14,7 +14,7 @@ import { EditorViewMode } from './editor-view-mode'
import { HelpButton } from './help-button/help-button'
import { NavbarBranding } from './navbar-branding'
import { SyncScrollButtons } from './sync-scroll-buttons/sync-scroll-buttons'
import { NoteType } from '../note-frontmatter/note-frontmatter'
import { NoteType } from '../../common/note-frontmatter/types'
import { SlideModeButton } from './slide-mode-button'
import { ReadOnlyModeButton } from './read-only-mode-button'
import { NewNoteButton } from './new-note-button'

View file

@ -13,7 +13,7 @@ import { CopyableField } from '../../../common/copyable/copyable-field/copyable-
import { CommonModal } from '../../../common/modals/common-modal'
import { ShowIf } from '../../../common/show-if/show-if'
import { EditorPagePathParams } from '../../editor-page'
import { NoteType } from '../../note-frontmatter/note-frontmatter'
import { NoteType } from '../../../common/note-frontmatter/types'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
export interface ShareModalProps {

View file

@ -11,8 +11,7 @@ import { useDocumentTitleWithNoteTitle } from '../../hooks/common/use-document-t
import { useNoteMarkdownContent } from '../../hooks/common/use-note-markdown-content'
import {
setCheckboxInMarkdownContent,
setNoteFrontmatter,
setNoteMarkdownContent,
setNoteContent,
updateNoteTitleByFirstHeading
} from '../../redux/note-details/methods'
import { MotdBanner } from '../common/motd-banner/motd-banner'
@ -50,6 +49,7 @@ export const EditorPage: React.FC = () => {
const markdownContent = useNoteMarkdownContent()
const scrollSource = useRef<ScrollSource>(ScrollSource.EDITOR)
const documentContent = useApplicationState((state) => state.noteDetails.documentContent)
const editorMode: EditorMode = useApplicationState((state) => state.editorConfig.editorMode)
const editorSyncScroll: boolean = useApplicationState((state) => state.editorConfig.syncScroll)
@ -98,14 +98,14 @@ export const EditorPage: React.FC = () => {
const leftPane = useMemo(
() => (
<EditorPane
onContentChange={setNoteMarkdownContent}
content={markdownContent}
onContentChange={setNoteContent}
content={documentContent}
scrollState={scrollState.editorScrollState}
onScroll={onEditorScroll}
onMakeScrollSource={setEditorToScrollSource}
/>
),
[markdownContent, onEditorScroll, scrollState.editorScrollState, setEditorToScrollSource]
[documentContent, onEditorScroll, scrollState.editorScrollState, setEditorToScrollSource]
)
const rightPane = useMemo(
@ -116,7 +116,6 @@ export const EditorPage: React.FC = () => {
onMakeScrollSource={setRendererToScrollSource}
onFirstHeadingChange={updateNoteTitleByFirstHeading}
onTaskCheckedChange={setCheckboxInMarkdownContent}
onFrontmatterChange={setNoteFrontmatter}
onScroll={onMarkdownRendererScroll}
scrollState={scrollState.rendererScrollState}
rendererType={RendererType.DOCUMENT}

View file

@ -1,189 +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 { NoteFrontmatter, RawNoteFrontmatter } from './note-frontmatter'
describe('yaml frontmatter', () => {
const testFrontmatter = (input: string): NoteFrontmatter => {
let processedFrontmatter: NoteFrontmatter | undefined = undefined
const md = new MarkdownIt('default', {
html: true,
breaks: true,
langPrefix: '',
typographer: true
})
md.use(frontmatter, (rawMeta: string) => {
const parsedFrontmatter = yaml.load(rawMeta) as RawNoteFrontmatter | undefined
expect(parsedFrontmatter).not.toBe(undefined)
if (parsedFrontmatter === undefined) {
fail('Parsed frontmatter is undefined')
}
processedFrontmatter = new NoteFrontmatter(parsedFrontmatter)
})
md.render(input)
if (processedFrontmatter === undefined) {
fail('NoteFrontmatter is undefined')
}
return processedFrontmatter
}
it('should parse "title"', () => {
const noteFrontmatter = testFrontmatter(`---
title: test
___
`)
expect(noteFrontmatter.title).toEqual('test')
})
it('should parse "robots"', () => {
const noteFrontmatter = testFrontmatter(`---
robots: index, follow
___
`)
expect(noteFrontmatter.robots).toEqual('index, follow')
})
it('should parse the deprecated tags syntax', () => {
const noteFrontmatter = testFrontmatter(`---
tags: test123, abc
___
`)
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(true)
})
it('should parse the tags list syntax', () => {
const noteFrontmatter = testFrontmatter(`---
tags:
- test123
- abc
___
`)
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(false)
})
it('should parse the tag inline-list syntax', () => {
const noteFrontmatter = testFrontmatter(`---
tags: ['test123', 'abc']
___
`)
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(false)
})
it('should parse "breaks"', () => {
const noteFrontmatter = testFrontmatter(`---
breaks: false
___
`)
expect(noteFrontmatter.breaks).toEqual(false)
})
/*
it('slideOptions nothing', () => {
testFrontmatter(`---
slideOptions:
___
`,
{
slideOptions: null
},
{
slideOptions: {
theme: 'white',
transition: 'none'
}
})
})
it('slideOptions.theme only', () => {
testFrontmatter(`---
slideOptions:
theme: sky
___
`,
{
slideOptions: {
theme: 'sky',
transition: undefined
}
},
{
slideOptions: {
theme: 'sky',
transition: 'none'
}
})
})
it('slideOptions full', () => {
testFrontmatter(`---
slideOptions:
transition: zoom
theme: sky
___
`,
{
slideOptions: {
theme: 'sky',
transition: 'zoom'
}
},
{
slideOptions: {
theme: 'sky',
transition: 'zoom'
}
})
})
*/
it('should parse an empty opengraph object', () => {
const noteFrontmatter = testFrontmatter(`---
opengraph:
___
`)
expect(noteFrontmatter.opengraph).toEqual(new Map<string, string>())
})
it('should parse an opengraph title', () => {
const noteFrontmatter = testFrontmatter(`---
opengraph:
title: Testtitle
___
`)
expect(noteFrontmatter.opengraph.get('title')).toEqual('Testtitle')
})
it('should opengraph values', () => {
const noteFrontmatter = testFrontmatter(`---
opengraph:
title: Testtitle
image: https://dummyimage.com/48.png
image:type: image/png
___
`)
expect(noteFrontmatter.opengraph.get('title')).toEqual('Testtitle')
expect(noteFrontmatter.opengraph.get('image')).toEqual('https://dummyimage.com/48.png')
expect(noteFrontmatter.opengraph.get('image:type')).toEqual('image/png')
})
})

View file

@ -1,286 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
// import { RevealOptions } from 'reveal.js'
export interface RawNoteFrontmatter {
title: string | undefined
description: string | undefined
tags: string | string[] | undefined
robots: string | undefined
lang: string | undefined
dir: string | undefined
breaks: boolean | undefined
GA: string | undefined
disqus: string | undefined
type: string | undefined
slideOptions: unknown
opengraph: { [key: string]: string } | null
}
export const ISO6391 = [
'aa',
'ab',
'af',
'am',
'ar',
'ar-ae',
'ar-bh',
'ar-dz',
'ar-eg',
'ar-iq',
'ar-jo',
'ar-kw',
'ar-lb',
'ar-ly',
'ar-ma',
'ar-om',
'ar-qa',
'ar-sa',
'ar-sy',
'ar-tn',
'ar-ye',
'as',
'ay',
'de-at',
'de-ch',
'de-li',
'de-lu',
'div',
'dz',
'el',
'en',
'en-au',
'en-bz',
'en-ca',
'en-gb',
'en-ie',
'en-jm',
'en-nz',
'en-ph',
'en-tt',
'en-us',
'en-za',
'en-zw',
'eo',
'es',
'es-ar',
'es-bo',
'es-cl',
'es-co',
'es-cr',
'es-do',
'es-ec',
'es-es',
'es-gt',
'es-hn',
'es-mx',
'es-ni',
'es-pa',
'es-pe',
'es-pr',
'es-py',
'es-sv',
'es-us',
'es-uy',
'es-ve',
'et',
'eu',
'fa',
'fi',
'fj',
'fo',
'fr',
'fr-be',
'fr-ca',
'fr-ch',
'fr-lu',
'fr-mc',
'fy',
'ga',
'gd',
'gl',
'gn',
'gu',
'ha',
'he',
'hi',
'hr',
'hu',
'hy',
'ia',
'id',
'ie',
'ik',
'in',
'is',
'it',
'it-ch',
'iw',
'ja',
'ji',
'jw',
'ka',
'kk',
'kl',
'km',
'kn',
'ko',
'kok',
'ks',
'ku',
'ky',
'kz',
'la',
'ln',
'lo',
'ls',
'lt',
'lv',
'mg',
'mi',
'mk',
'ml',
'mn',
'mo',
'mr',
'ms',
'mt',
'my',
'na',
'nb-no',
'ne',
'nl',
'nl-be',
'nn-no',
'no',
'oc',
'om',
'or',
'pa',
'pl',
'ps',
'pt',
'pt-br',
'qu',
'rm',
'rn',
'ro',
'ro-md',
'ru',
'ru-md',
'rw',
'sa',
'sb',
'sd',
'sg',
'sh',
'si',
'sk',
'sl',
'sm',
'sn',
'so',
'sq',
'sr',
'ss',
'st',
'su',
'sv',
'sv-fi',
'sw',
'sx',
'syr',
'ta',
'te',
'tg',
'th',
'ti',
'tk',
'tl',
'tn',
'to',
'tr',
'ts',
'tt',
'tw',
'uk',
'ur',
'us',
'uz',
'vi',
'vo',
'wo',
'xh',
'yi',
'yo',
'zh',
'zh-cn',
'zh-hk',
'zh-mo',
'zh-sg',
'zh-tw',
'zu'
] as const
export enum NoteType {
DOCUMENT = '',
SLIDE = 'slide'
}
export enum NoteTextDirection {
LTR = 'ltr',
RTL = 'rtl'
}
export class NoteFrontmatter {
title: string
description: string
tags: string[]
deprecatedTagsSyntax: boolean
robots: string
lang: typeof ISO6391[number]
dir: NoteTextDirection
breaks: boolean
GA: string
disqus: string
type: NoteType
// slideOptions: RevealOptions
opengraph: Map<string, string>
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
/* this.slideOptions = (rawData?.slideOptions as RevealOptions) ?? {
transition: 'none',
theme: 'white'
} */
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>()
}
}

View file

@ -25,7 +25,6 @@ export interface RenderIframeProps extends RendererProps {
export const RenderIframe: React.FC<RenderIframeProps> = ({
markdownContent,
onTaskCheckedChange,
onFrontmatterChange,
scrollState,
onFirstHeadingChange,
onScroll,
@ -39,6 +38,7 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
const [lightboxDetails, setLightboxDetails] = useState<ImageDetails | undefined>(undefined)
const frameReference = useRef<HTMLIFrameElement>(null)
const frontmatterInfo = useApplicationState((state) => state.noteDetails.frontmatterRendererInfo)
const rendererOrigin = useApplicationState((state) => state.config.iframeCommunication.rendererOrigin)
const renderPageUrl = `${rendererOrigin}render`
const resetRendererReady = useCallback(() => setRendererStatus(false), [])
@ -67,11 +67,6 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
return () => iframeCommunicator.onFirstHeadingChange(undefined)
}, [iframeCommunicator, onFirstHeadingChange])
useEffect(() => {
iframeCommunicator.onFrontmatterChange(onFrontmatterChange)
return () => iframeCommunicator.onFrontmatterChange(undefined)
}, [iframeCommunicator, onFrontmatterChange])
useEffect(() => {
iframeCommunicator.onSetScrollState(onScroll)
return () => iframeCommunicator.onSetScrollState(undefined)
@ -128,6 +123,12 @@ export const RenderIframe: React.FC<RenderIframeProps> = ({
}
}, [iframeCommunicator, markdownContent, rendererReady])
useEffect(() => {
if (rendererReady && frontmatterInfo !== undefined) {
iframeCommunicator.sendSetFrontmatterInfo(frontmatterInfo)
}
}, [iframeCommunicator, rendererReady, frontmatterInfo])
return (
<Fragment>
<ShowOnPropChangeImageLightbox details={lightboxDetails} />

View file

@ -10,14 +10,13 @@ import { Trans, useTranslation } from 'react-i18next'
import links from '../../../links.json'
import { TranslatedExternalLink } from '../../common/links/translated-external-link'
import { ShowIf } from '../../common/show-if/show-if'
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { CommonModalProps } from '../../common/modals/common-modal'
export const YamlArrayDeprecationAlert: React.FC = () => {
export const YamlArrayDeprecationAlert: React.FC<Partial<CommonModalProps>> = ({ show }) => {
useTranslation()
const yamlDeprecatedTags = useApplicationState((state) => state.noteDetails.frontmatter.deprecatedTagsSyntax)
return (
<ShowIf condition={yamlDeprecatedTags}>
<ShowIf condition={!!show}>
<Alert data-cy={'yamlArrayDeprecationAlert'} className={'text-wrap'} variant='warning' dir='auto'>
<span className={'text-wrap'}>
<span className={'text-wrap'}>

View file

@ -8,17 +8,17 @@ import React, { useCallback } from 'react'
import sanitize from 'sanitize-filename'
import { store } from '../../../redux'
import { Trans, useTranslation } from 'react-i18next'
import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content'
import { download } from '../../common/download/download'
import { SidebarButton } from './sidebar-button'
import { useApplicationState } from '../../../hooks/common/use-application-state'
export const ExportMarkdownSidebarEntry: React.FC = () => {
const { t } = useTranslation()
const markdownContent = useNoteMarkdownContent()
const documentContent = useApplicationState((state) => state.noteDetails.documentContent)
const onClick = useCallback(() => {
const sanitized = sanitize(store.getState().noteDetails.noteTitle)
download(markdownContent, `${sanitized !== '' ? sanitized : t('editor.untitledNote')}.md`, 'text/markdown')
}, [markdownContent, t])
download(documentContent, `${sanitized !== '' ? sanitized : t('editor.untitledNote')}.md`, 'text/markdown')
}, [documentContent, t])
return (
<SidebarButton data-cy={'menu-export-markdown'} onClick={onClick} icon={'file-text'}>

View file

@ -7,7 +7,7 @@
import React, { Fragment, useCallback, useRef } from 'react'
import { Trans, useTranslation } from 'react-i18next'
import { useNoteMarkdownContent } from '../../../hooks/common/use-note-markdown-content'
import { setNoteMarkdownContent } from '../../../redux/note-details/methods'
import { setNoteContent } from '../../../redux/note-details/methods'
import { SidebarButton } from './sidebar-button'
import { UploadInput } from './upload-input'
@ -21,7 +21,7 @@ export const ImportMarkdownSidebarEntry: React.FC = () => {
const fileReader = new FileReader()
fileReader.addEventListener('load', () => {
const newContent = fileReader.result as string
setNoteMarkdownContent(markdownContent.length === 0 ? newContent : `${markdownContent}\n${newContent}`)
setNoteContent(markdownContent.length === 0 ? newContent : `${markdownContent}\n${newContent}`)
})
fileReader.addEventListener('loadend', () => {
resolve()