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

@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
NoteFrontmatter,
NoteTextDirection,
NoteType,
OpenGraph
} from '../note-frontmatter/frontmatter.js'
import { SlideOptions } from '../note-frontmatter/slide-show-options.js'
import { convertRawFrontmatterToNoteFrontmatter } from './convert-raw-frontmatter-to-note-frontmatter.js'
import { describe, expect, it } from '@jest/globals'
describe('convertRawFrontmatterToNoteFrontmatter', () => {
it.each([false, true])(
'returns the correct note frontmatter with `breaks: %s`',
(breaks) => {
const slideOptions: SlideOptions = {}
const opengraph: OpenGraph = {}
expect(
convertRawFrontmatterToNoteFrontmatter({
title: 'title',
description: 'description',
robots: 'robots',
lang: 'de',
type: NoteType.DOCUMENT,
dir: NoteTextDirection.LTR,
license: 'license',
breaks: breaks,
opengraph: opengraph,
slideOptions: slideOptions,
tags: 'tags'
})
).toStrictEqual({
title: 'title',
description: 'description',
robots: 'robots',
newlinesAreBreaks: breaks,
lang: 'de',
type: NoteType.DOCUMENT,
dir: NoteTextDirection.LTR,
opengraph: opengraph,
slideOptions: slideOptions,
license: 'license',
tags: ['tags']
} as NoteFrontmatter)
}
)
})

View file

@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NoteFrontmatter } from '../note-frontmatter/frontmatter.js'
import { parseTags } from './parse-tags.js'
import { RawNoteFrontmatter } from './types.js'
/**
* 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 convertRawFrontmatterToNoteFrontmatter = (
rawData: RawNoteFrontmatter
): NoteFrontmatter => {
return {
title: rawData.title,
description: rawData.description,
robots: rawData.robots,
newlinesAreBreaks: rawData.breaks,
lang: rawData.lang,
type: rawData.type,
dir: rawData.dir,
opengraph: rawData.opengraph,
slideOptions: rawData.slideOptions,
license: rawData.license,
tags: parseTags(rawData.tags)
}
}

View file

@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
NoteFrontmatter,
NoteTextDirection,
NoteType
} from '../note-frontmatter/frontmatter.js'
import { SlideOptions } from '../note-frontmatter/slide-show-options.js'
export const defaultSlideOptions: SlideOptions = {
transition: 'zoom',
autoSlide: 0,
autoSlideStoppable: true,
backgroundTransition: 'fade',
slideNumber: false
}
export const defaultNoteFrontmatter: NoteFrontmatter = {
title: '',
description: '',
tags: [],
robots: '',
lang: 'en',
dir: NoteTextDirection.LTR,
newlinesAreBreaks: true,
license: '',
type: NoteType.DOCUMENT,
opengraph: {},
slideOptions: defaultSlideOptions
}

View file

@ -0,0 +1,76 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { parseRawFrontmatterFromYaml } from './parse-raw-frontmatter-from-yaml.js'
import { describe, expect, it } from '@jest/globals'
describe('yaml frontmatter', () => {
it('should parse "title"', () => {
const noteFrontmatter = parseRawFrontmatterFromYaml('title: test')
expect(noteFrontmatter.value?.title).toEqual('test')
})
it('should parse "robots"', () => {
const noteFrontmatter = parseRawFrontmatterFromYaml('robots: index, follow')
expect(noteFrontmatter.value?.robots).toEqual('index, follow')
})
it('should parse the deprecated tags syntax', () => {
const noteFrontmatter = parseRawFrontmatterFromYaml('tags: test123, abc')
expect(noteFrontmatter.value?.tags).toEqual('test123, abc')
})
it('should parse the tags list syntax', () => {
const noteFrontmatter = parseRawFrontmatterFromYaml(`tags:
- test123
- abc
`)
expect(noteFrontmatter.value?.tags).toEqual(['test123', 'abc'])
})
it('should parse the tag inline-list syntax', () => {
const noteFrontmatter = parseRawFrontmatterFromYaml(
"tags: ['test123', 'abc']"
)
expect(noteFrontmatter.value?.tags).toEqual(['test123', 'abc'])
})
it('should parse "breaks"', () => {
const noteFrontmatter = parseRawFrontmatterFromYaml('breaks: false')
expect(noteFrontmatter.value?.breaks).toEqual(false)
})
it('should parse an opengraph title', () => {
const noteFrontmatter = parseRawFrontmatterFromYaml(`opengraph:
title: Testtitle
`)
expect(noteFrontmatter.value?.opengraph.title).toEqual('Testtitle')
})
it('should parse multiple opengraph values', () => {
const noteFrontmatter = parseRawFrontmatterFromYaml(`opengraph:
title: Testtitle
image: https://dummyimage.com/48.png
image:type: image/png
`)
expect(noteFrontmatter.value?.opengraph.title).toEqual('Testtitle')
expect(noteFrontmatter.value?.opengraph.image).toEqual(
'https://dummyimage.com/48.png'
)
expect(noteFrontmatter.value?.opengraph['image:type']).toEqual('image/png')
})
it('allows unknown additional options', () => {
const noteFrontmatter = parseRawFrontmatterFromYaml(`title: title
additonal: "additonal"`)
expect(noteFrontmatter.value?.title).toBe('title')
})
it('throws an error if the yaml is invalid', () => {
const a = parseRawFrontmatterFromYaml('A: asd\n B: asd')
expect(a.error?.message).toStrictEqual('Invalid YAML')
})
})

View file

@ -0,0 +1,94 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
NoteTextDirection,
NoteType,
OpenGraph
} from '../note-frontmatter/frontmatter.js'
import { ISO6391 } from '../note-frontmatter/iso6391.js'
import { SlideOptions } from '../note-frontmatter/slide-show-options.js'
import { defaultNoteFrontmatter } from './default-values.js'
import type { RawNoteFrontmatter } from './types.js'
import type { ValidationError } from 'joi'
import Joi from 'joi'
import { load } from 'js-yaml'
const schema = Joi.object<RawNoteFrontmatter>({
title: Joi.string().optional().default(defaultNoteFrontmatter.title),
description: Joi.string()
.optional()
.default(defaultNoteFrontmatter.description),
tags: Joi.alternatives(
Joi.array().items(Joi.string()),
Joi.string(),
Joi.number().cast('string')
)
.optional()
.default(defaultNoteFrontmatter.tags),
robots: Joi.string().optional().default(defaultNoteFrontmatter.robots),
lang: Joi.string()
.valid(...ISO6391)
.optional()
.default(defaultNoteFrontmatter.lang),
dir: Joi.string()
.valid(...Object.values(NoteTextDirection))
.optional()
.default(defaultNoteFrontmatter.dir),
breaks: Joi.boolean()
.optional()
.default(defaultNoteFrontmatter.newlinesAreBreaks),
license: Joi.string().optional().default(defaultNoteFrontmatter.license),
type: Joi.string()
.valid(...Object.values(NoteType))
.optional()
.default(defaultNoteFrontmatter.type),
slideOptions: Joi.object<SlideOptions>({
autoSlide: Joi.number().optional(),
transition: Joi.string().optional(),
backgroundTransition: Joi.string().optional(),
autoSlideStoppable: Joi.boolean().optional(),
slideNumber: Joi.boolean().optional()
})
.optional()
.default(defaultNoteFrontmatter.slideOptions),
opengraph: Joi.object<OpenGraph>({
title: Joi.string().optional(),
image: Joi.string().uri().optional()
})
.unknown(true)
.optional()
.default(defaultNoteFrontmatter.opengraph)
})
.default(defaultNoteFrontmatter)
.unknown(true)
const loadYaml = (rawYaml: string): unknown => {
try {
return load(rawYaml)
} catch {
return undefined
}
}
type ParserResult =
| {
error: undefined
warning?: ValidationError
value: RawNoteFrontmatter
}
| {
error: Error
warning?: ValidationError
value: undefined
}
export const parseRawFrontmatterFromYaml = (rawYaml: string): ParserResult => {
const rawNoteFrontmatter = loadYaml(rawYaml)
if (rawNoteFrontmatter === undefined) {
return { error: new Error('Invalid YAML'), value: undefined }
}
return schema.validate(rawNoteFrontmatter, { convert: true })
}

View file

@ -0,0 +1,31 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { parseTags } from './parse-tags.js'
import { expect, it, describe } from '@jest/globals'
describe('parse tags', () => {
it('converts comma separated string tags into string list', () => {
expect(parseTags('a,b,c,d,e,f')).toStrictEqual([
'a',
'b',
'c',
'd',
'e',
'f'
])
})
it('accepts a string list as tags', () => {
expect(parseTags(['a', 'b', ' c', 'd ', 'e', 'f'])).toStrictEqual([
'a',
'b',
'c',
'd',
'e',
'f'
])
})
})

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* Parses the given value as tags array.
*
* @param rawTags The raw value to parse
* @return the parsed tags
*/
export const parseTags = (rawTags: string | string[]): string[] => {
return (Array.isArray(rawTags) ? rawTags : rawTags.split(','))
.map((entry) => entry.trim())
.filter((tag) => !!tag)
}

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
Iso6391Language,
NoteTextDirection,
NoteType,
OpenGraph
} from '../note-frontmatter/frontmatter.js'
import { SlideOptions } from '../note-frontmatter/slide-show-options.js'
export interface RawNoteFrontmatter {
title: string
description: string
tags: string | string[]
robots: string
lang: Iso6391Language
dir: NoteTextDirection
breaks: boolean
license: string
type: NoteType
slideOptions: SlideOptions
opengraph: OpenGraph
}