Move frontmatter types (#1664)

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2021-12-02 23:41:07 +01:00 committed by GitHub
parent 8a23aa1401
commit b68a55aa94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 157 additions and 144 deletions

View file

@ -1,94 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { extractFrontmatter } from './extract-frontmatter'
import type { PresentFrontmatterExtractionResult } from './types'
describe('frontmatter extraction', () => {
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.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.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.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.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.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.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.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.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.isPresent).toBe(true)
})
})
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.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.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.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.lineOffset).toEqual(5)
})
})
describe('rawText property', () => {
it('contains single-line frontmatter text', () => {
const testNote = '---\nsingle-line\n...\ncontent'
const extraction = extractFrontmatter(testNote) as PresentFrontmatterExtractionResult
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.rawText).toEqual('multi\nline')
})
})
})

View file

@ -1,40 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { FrontmatterExtractionResult } from './types'
const FRONTMATTER_BEGIN_REGEX = /^-{3,}$/
const FRONTMATTER_END_REGEX = /^(?:-{3,}|\.{3,})$/
/**
* Extracts a frontmatter block from a given multiline string.
* 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 { 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.
*/
export const extractFrontmatter = (content: string): FrontmatterExtractionResult => {
const lines = content.split('\n')
if (lines.length < 2 || !FRONTMATTER_BEGIN_REGEX.test(lines[0])) {
return {
isPresent: false
}
}
for (let i = 1; i < lines.length; i++) {
if (lines[i].length === lines[0].length && FRONTMATTER_END_REGEX.test(lines[i])) {
return {
isPresent: true,
rawText: lines.slice(1, i).join('\n'),
lineOffset: i + 1
}
}
}
return {
isPresent: false
}
}

View file

@ -1,68 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { createNoteFrontmatterFromYaml } from './note-frontmatter'
describe('yaml frontmatter', () => {
it('should parse "title"', () => {
const noteFrontmatter = createNoteFrontmatterFromYaml('title: test')
expect(noteFrontmatter.title).toEqual('test')
})
it('should parse "robots"', () => {
const noteFrontmatter = createNoteFrontmatterFromYaml('robots: index, follow')
expect(noteFrontmatter.robots).toEqual('index, follow')
})
it('should parse the deprecated tags syntax', () => {
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 = createNoteFrontmatterFromYaml(`tags:
- test123
- abc
`)
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(false)
})
it('should parse the tag inline-list syntax', () => {
const noteFrontmatter = createNoteFrontmatterFromYaml("tags: ['test123', 'abc']")
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(false)
})
it('should parse "breaks"', () => {
const noteFrontmatter = createNoteFrontmatterFromYaml('breaks: false')
expect(noteFrontmatter.newlinesAreBreaks).toEqual(false)
})
it('should parse an empty opengraph object', () => {
const noteFrontmatter = createNoteFrontmatterFromYaml('opengraph:')
expect(noteFrontmatter.opengraph).toEqual(new Map<string, string>())
})
it('should parse an opengraph title', () => {
const noteFrontmatter = createNoteFrontmatterFromYaml(`opengraph:
title: Testtitle
`)
expect(noteFrontmatter.opengraph.get('title')).toEqual('Testtitle')
})
it('should parse multiple opengraph values', () => {
const noteFrontmatter = createNoteFrontmatterFromYaml(`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,123 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
// import { RevealOptions } from 'reveal.js'
import { load } from 'js-yaml'
import type { RawNoteFrontmatter, SlideOptions } from './types'
import { ISO6391, NoteTextDirection, NoteType } from './types'
import { initialSlideOptions } from '../../../redux/note-details/initial-state'
/**
* Class that represents the parsed frontmatter metadata of a note.
*/
export interface NoteFrontmatter {
title: string
description: string
tags: string[]
deprecatedTagsSyntax: boolean
robots: string
lang: typeof ISO6391[number]
dir: NoteTextDirection
newlinesAreBreaks: boolean
GA: string
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.
*/
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
}
return {
title: rawData.title ?? '',
description: rawData.description ?? '',
robots: rawData.robots ?? '',
newlinesAreBreaks: 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

@ -1,264 +0,0 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { 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 {
lineOffset: number
frontmatterInvalid: boolean
deprecatedSyntax: boolean
slideOptions: SlideOptions
}
export interface PresentFrontmatterExtractionResult {
isPresent: true
rawText: string
lineOffset: number
}
interface NonPresentFrontmatterExtractionResult {
isPresent: false
}
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: { [key: string]: string } | null
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'
}

View file

@ -14,11 +14,11 @@ 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 '../../common/note-frontmatter/types'
import { SlideModeButton } from './slide-mode-button'
import { ReadOnlyModeButton } from './read-only-mode-button'
import { NewNoteButton } from './new-note-button'
import { useApplicationState } from '../../../hooks/common/use-application-state'
import { NoteType } from '../../../redux/note-details/types/note-details'
export enum AppBarMode {
BASIC,

View file

@ -14,8 +14,8 @@ import type { ModalVisibilityProps } from '../../../common/modals/common-modal'
import { CommonModal } from '../../../common/modals/common-modal'
import { ShowIf } from '../../../common/show-if/show-if'
import type { EditorPagePathParams } from '../../editor-page'
import { NoteType } from '../../../common/note-frontmatter/types'
import { useApplicationState } from '../../../../hooks/common/use-application-state'
import { NoteType } from '../../../../redux/note-details/types/note-details'
export const ShareModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) => {
useTranslation()
@ -37,7 +37,7 @@ export const ShareModal: React.FC<ModalVisibilityProps> = ({ show, onHide }) =>
<Trans i18nKey={'editor.modal.shareLink.slidesDescription'} />
<CopyableField content={`${baseUrl}p/${id}`} nativeShareButton={true} url={`${baseUrl}p/${id}`} />
</ShowIf>
<ShowIf condition={noteFrontmatter.type === ''}>
<ShowIf condition={noteFrontmatter.type === NoteType.DOCUMENT}>
<Trans i18nKey={'editor.modal.shareLink.viewOnlyDescription'} />
<CopyableField content={`${baseUrl}s/${id}`} nativeShareButton={true} url={`${baseUrl}s/${id}`} />
</ShowIf>

View file

@ -29,7 +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'
import { NoteType } from '../../redux/note-details/types/note-details'
export interface EditorPagePathParams {
id: string

View file

@ -8,8 +8,8 @@ import { useSendToRenderer } from '../../../render-page/window-post-message-comm
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 type { RendererFrontmatterInfo } from '../../../common/note-frontmatter/types'
import equal from 'fast-deep-equal'
import type { RendererFrontmatterInfo } from '../../../../redux/note-details/types/note-details'
/**
* Extracts the {@link RendererFrontmatterInfo frontmatter data}

View file

@ -7,7 +7,7 @@
import { useEffect, useRef, useState } from 'react'
import Reveal from 'reveal.js'
import { Logger } from '../../../utils/logger'
import type { SlideOptions } from '../../common/note-frontmatter/types'
import type { SlideOptions } from '../../../redux/note-details/types/slide-show-options'
const log = new Logger('reveal.js')

View file

@ -15,11 +15,11 @@ import { REVEAL_STATUS, useReveal } from './hooks/use-reveal'
import './slideshow.scss'
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
import { DocumentLengthLimitReachedAlert } from './document-length-limit-reached-alert'
import type { SlideOptions } from '../common/note-frontmatter/types'
import type { CommonMarkdownRendererProps } from './common-markdown-renderer-props'
import { LoadingSlide } from './loading-slide'
import { RevealMarkdownExtension } from './markdown-extension/reveal/reveal-markdown-extension'
import { useMarkdownExtensions } from './hooks/use-markdown-extensions'
import type { SlideOptions } from '../../redux/note-details/types/slide-show-options'
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps {
slideOptions: SlideOptions

View file

@ -13,11 +13,11 @@ import type { ImageClickHandler } from '../markdown-renderer/markdown-extension/
import { useImageClickHandler } from './hooks/use-image-click-handler'
import { MarkdownDocument } from './markdown-document'
import { countWords } from './word-counter'
import type { 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'
import type { RendererFrontmatterInfo } from '../../redux/note-details/types/note-details'
export const IframeMarkdownRenderer: React.FC = () => {
const [markdownContent, setMarkdownContent] = useState('')

View file

@ -17,8 +17,8 @@ import './markdown-document.scss'
import { WidthBasedTableOfContents } from './width-based-table-of-contents'
import { ShowIf } from '../common/show-if/show-if'
import { useApplicationState } from '../../hooks/common/use-application-state'
import type { RendererFrontmatterInfo } from '../common/note-frontmatter/types'
import { InvalidYamlAlert } from '../markdown-renderer/invalid-yaml-alert'
import type { RendererFrontmatterInfo } from '../../redux/note-details/types/note-details'
export interface RendererProps extends ScrollProps {
onFirstHeadingChange?: (firstHeading: string | undefined) => void

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ScrollState } from '../../editor-page/synced-scroll/scroll-props'
import type { RendererFrontmatterInfo } from '../../common/note-frontmatter/types'
import type { RendererFrontmatterInfo } from '../../../redux/note-details/types/note-details'
export enum CommunicationMessageType {
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',