refactor: move frontmatter parser into commons package

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-05-19 11:49:29 +02:00
parent 4d0a2cb79e
commit db43e1db3f
26 changed files with 462 additions and 321 deletions

View file

@ -5,10 +5,15 @@
*/
import { calculateLineStartIndexes } from './calculate-line-start-indexes'
import { initialState } from './initial-state'
import { createNoteFrontmatterFromYaml } from './raw-note-frontmatter-parser/parser'
import type { NoteDetails } from './types/note-details'
import { extractFrontmatter, generateNoteTitle } from '@hedgedoc/commons'
import type { FrontmatterExtractionResult } from '@hedgedoc/commons'
import type { FrontmatterExtractionResult, NoteFrontmatter } from '@hedgedoc/commons'
import {
convertRawFrontmatterToNoteFrontmatter,
extractFrontmatter,
generateNoteTitle,
parseRawFrontmatterFromYaml
} from '@hedgedoc/commons'
import { Optional } from '@mrdrogdrog/optional'
/**
* Copies a {@link NoteDetails} but with another markdown content.
@ -62,7 +67,7 @@ const buildStateFromMarkdownContentAndLines = (
},
startOfContentLineOffset: 0,
rawFrontmatter: '',
title: generateNoteTitle(initialState.frontmatter, state.firstHeading),
title: generateNoteTitle(initialState.frontmatter, () => state.firstHeading),
frontmatter: initialState.frontmatter
}
}
@ -81,22 +86,27 @@ const buildStateFromFrontmatterUpdate = (
if (frontmatterExtraction.rawText === state.rawFrontmatter) {
return state
}
try {
const frontmatter = createNoteFrontmatterFromYaml(frontmatterExtraction.rawText)
return {
...state,
rawFrontmatter: frontmatterExtraction.rawText,
frontmatter: frontmatter,
title: generateNoteTitle(frontmatter, state.firstHeading),
startOfContentLineOffset: frontmatterExtraction.lineOffset
}
} catch (e) {
return {
...state,
title: generateNoteTitle(initialState.frontmatter, state.firstHeading),
rawFrontmatter: frontmatterExtraction.rawText,
frontmatter: initialState.frontmatter,
startOfContentLineOffset: frontmatterExtraction.lineOffset
}
return buildStateFromFrontmatter(state, parseFrontmatter(frontmatterExtraction), frontmatterExtraction)
}
const parseFrontmatter = (frontmatterExtraction: FrontmatterExtractionResult) => {
return Optional.of(parseRawFrontmatterFromYaml(frontmatterExtraction.rawText))
.filter((frontmatter) => frontmatter.error === undefined)
.map((frontmatter) => frontmatter.value)
.map((value) => convertRawFrontmatterToNoteFrontmatter(value))
.orElse(initialState.frontmatter)
}
const buildStateFromFrontmatter = (
state: NoteDetails,
noteFrontmatter: NoteFrontmatter,
frontmatterExtraction: FrontmatterExtractionResult
) => {
return {
...state,
title: generateNoteTitle(noteFrontmatter, () => state.firstHeading),
rawFrontmatter: frontmatterExtraction.rawText,
frontmatter: noteFrontmatter,
startOfContentLineOffset: frontmatterExtraction.lineOffset
}
}

View file

@ -4,16 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { NoteDetails } from './types/note-details'
import { NoteTextDirection, NoteType } from '@hedgedoc/commons'
import type { SlideOptions } from '@hedgedoc/commons'
export const initialSlideOptions: SlideOptions = {
transition: 'zoom',
autoSlide: 0,
autoSlideStoppable: true,
backgroundTransition: 'fade',
slideNumber: false
}
import { defaultNoteFrontmatter } from '@hedgedoc/commons'
export const initialState: NoteDetails = {
updateUsername: null,
@ -40,17 +31,5 @@ export const initialState: NoteDetails = {
editedBy: [],
title: '',
firstHeading: '',
frontmatter: {
title: '',
description: '',
tags: [],
robots: '',
lang: 'en',
dir: NoteTextDirection.LTR,
newlinesAreBreaks: true,
license: '',
type: NoteType.DOCUMENT,
opengraph: {},
slideOptions: initialSlideOptions
}
frontmatter: defaultNoteFrontmatter
}

View file

@ -1,64 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { createNoteFrontmatterFromYaml } from './parser'
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'])
})
it('should parse the tags list syntax', () => {
const noteFrontmatter = createNoteFrontmatterFromYaml(`tags:
- test123
- abc
`)
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
})
it('should parse the tag inline-list syntax', () => {
const noteFrontmatter = createNoteFrontmatterFromYaml("tags: ['test123', 'abc']")
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
})
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({})
})
it('should parse an opengraph title', () => {
const noteFrontmatter = createNoteFrontmatterFromYaml(`opengraph:
title: Testtitle
`)
expect(noteFrontmatter.opengraph.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.title).toEqual('Testtitle')
expect(noteFrontmatter.opengraph.image).toEqual('https://dummyimage.com/48.png')
expect(noteFrontmatter.opengraph['image:type']).toEqual('image/png')
})
})

View file

@ -1,139 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { initialSlideOptions, initialState } from '../initial-state'
import type { RawNoteFrontmatter } from './types'
import type { Iso6391Language, NoteFrontmatter, OpenGraph, SlideOptions } from '@hedgedoc/commons'
import { ISO6391, NoteTextDirection, NoteType } from '@hedgedoc/commons'
import { load } from 'js-yaml'
/**
* 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)
}
/**
* 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.
*/
const parseRawNoteFrontmatter = (rawData: RawNoteFrontmatter): NoteFrontmatter => {
let tags: string[]
if (typeof rawData?.tags === 'string') {
tags = rawData?.tags?.split(',').map((entry) => entry.trim()) ?? []
} else if (typeof rawData?.tags === 'object') {
tags = rawData?.tags?.filter((tag) => tag !== null) ?? []
} else {
tags = [...initialState.frontmatter.tags]
}
return {
title: rawData.title ?? initialState.frontmatter.title,
description: rawData.description ?? initialState.frontmatter.description,
robots: rawData.robots ?? initialState.frontmatter.robots,
newlinesAreBreaks: parseBoolean(rawData.breaks) ?? initialState.frontmatter.newlinesAreBreaks,
lang: parseLanguage(rawData),
type: parseNoteType(rawData),
dir: parseTextDirection(rawData),
opengraph: parseOpenGraph(rawData),
slideOptions: parseSlideOptions(rawData),
license: rawData.license ?? initialState.frontmatter.license,
tags
}
}
/**
* Parses the {@link OpenGraph open graph} from the {@link RawNoteFrontmatter}.
*
* @param rawData The raw note frontmatter data.
* @return the parsed {@link OpenGraph open graph}
*/
const parseOpenGraph = (rawData: RawNoteFrontmatter): OpenGraph => {
return { ...(rawData.opengraph ?? initialState.frontmatter.opengraph) }
}
/**
* Parses the {@link Iso6391Language iso 6391 language code} from the {@link RawNoteFrontmatter}.
*
* @param rawData The raw note frontmatter data.
* @return the parsed {@link Iso6391Language iso 6391 language code}
*/
const parseLanguage = (rawData: RawNoteFrontmatter): Iso6391Language => {
return (rawData.lang ? ISO6391.find((lang) => lang === rawData.lang) : undefined) ?? initialState.frontmatter.lang
}
/**
* Parses the {@link NoteType note type} from the {@link RawNoteFrontmatter}.
*
* @param rawData The raw note frontmatter data.
* @return the parsed {@link NoteType note type}
*/
const parseNoteType = (rawData: RawNoteFrontmatter): NoteType => {
return rawData.type !== undefined
? rawData.type === NoteType.SLIDE
? NoteType.SLIDE
: NoteType.DOCUMENT
: initialState.frontmatter.type
}
/**
* Parses the {@link NoteTextDirection note text direction} from the {@link RawNoteFrontmatter}.
*
* @param rawData The raw note frontmatter data.
* @return the parsed {@link NoteTextDirection note text direction}
*/
const parseTextDirection = (rawData: RawNoteFrontmatter): NoteTextDirection => {
return rawData.dir !== undefined
? rawData.dir === NoteTextDirection.LTR
? NoteTextDirection.LTR
: NoteTextDirection.RTL
: initialState.frontmatter.dir
}
/**
* 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
}

View file

@ -1,19 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export interface RawNoteFrontmatter {
title: string | undefined
description: string | undefined
tags: string | number | string[] | undefined
robots: string | undefined
lang: string | undefined
dir: string | undefined
breaks: boolean | undefined
license: string | undefined
type: string | undefined
slideOptions: { [key: string]: string } | null
opengraph: { [key: string]: string } | null
}

View file

@ -16,6 +16,6 @@ export const buildStateFromFirstHeadingUpdate = (state: NoteDetails, firstHeadin
return {
...state,
firstHeading: firstHeading,
title: generateNoteTitle(state.frontmatter, firstHeading)
title: generateNoteTitle(state.frontmatter, () => firstHeading)
}
}