mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-22 03:05:19 -04:00
refactor: move frontmatter parser into commons package
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
4d0a2cb79e
commit
db43e1db3f
26 changed files with 462 additions and 321 deletions
|
@ -3,13 +3,11 @@
|
|||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import type { RawNoteFrontmatter } from '../../../../redux/note-details/raw-note-frontmatter-parser/types'
|
||||
import type { Linter } from './linter'
|
||||
import type { Diagnostic } from '@codemirror/lint'
|
||||
import type { EditorView } from '@codemirror/view'
|
||||
import { extractFrontmatter } from '@hedgedoc/commons'
|
||||
import { extractFrontmatter, parseRawFrontmatterFromYaml, parseTags } from '@hedgedoc/commons'
|
||||
import { t } from 'i18next'
|
||||
import { load } from 'js-yaml'
|
||||
|
||||
/**
|
||||
* Creates a {@link Linter linter} for the yaml frontmatter.
|
||||
|
@ -23,27 +21,42 @@ export class FrontmatterLinter implements Linter {
|
|||
if (frontmatterExtraction === undefined) {
|
||||
return []
|
||||
}
|
||||
const startOfYaml = lines[0].length + 1
|
||||
const frontmatterLines = lines.slice(1, frontmatterExtraction.lineOffset - 1)
|
||||
const rawNoteFrontmatter = FrontmatterLinter.loadYaml(frontmatterExtraction.rawText)
|
||||
if (rawNoteFrontmatter === undefined) {
|
||||
return [
|
||||
{
|
||||
from: startOfYaml,
|
||||
to: startOfYaml + frontmatterLines.join('\n').length,
|
||||
message: t('editor.linter.frontmatter'),
|
||||
severity: 'error'
|
||||
}
|
||||
]
|
||||
const startOfYaml = lines[0].length + 1
|
||||
const endOfYaml = startOfYaml + frontmatterLines.join('\n').length
|
||||
const rawNoteFrontmatter = parseRawFrontmatterFromYaml(frontmatterExtraction.rawText)
|
||||
if (rawNoteFrontmatter.error) {
|
||||
return this.createErrorDiagnostics(startOfYaml, endOfYaml, rawNoteFrontmatter.error, 'error')
|
||||
} else if (rawNoteFrontmatter.warning) {
|
||||
return this.createErrorDiagnostics(startOfYaml, endOfYaml, rawNoteFrontmatter.warning, 'warning')
|
||||
} else if (!Array.isArray(rawNoteFrontmatter.value.tags)) {
|
||||
return this.createReplaceSingleStringTagsDiagnostic(rawNoteFrontmatter.value.tags, frontmatterLines, startOfYaml)
|
||||
}
|
||||
if (typeof rawNoteFrontmatter.tags !== 'string' && typeof rawNoteFrontmatter.tags !== 'number') {
|
||||
return []
|
||||
}
|
||||
const tags: string[] =
|
||||
rawNoteFrontmatter?.tags
|
||||
.toString()
|
||||
.split(',')
|
||||
.map((entry) => entry.trim()) ?? []
|
||||
return []
|
||||
}
|
||||
|
||||
private createErrorDiagnostics(
|
||||
startOfYaml: number,
|
||||
endOfYaml: number,
|
||||
error: Error,
|
||||
severity: 'error' | 'warning'
|
||||
): Diagnostic[] {
|
||||
return [
|
||||
{
|
||||
from: startOfYaml,
|
||||
to: endOfYaml,
|
||||
message: error.message,
|
||||
severity: severity
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
private createReplaceSingleStringTagsDiagnostic(
|
||||
rawTags: string,
|
||||
frontmatterLines: string[],
|
||||
startOfYaml: number
|
||||
): Diagnostic[] {
|
||||
const tags: string[] = parseTags(rawTags)
|
||||
const replacedText = 'tags:\n- ' + tags.join('\n- ')
|
||||
const tagsLineIndex = frontmatterLines.findIndex((value) => value.startsWith('tags: '))
|
||||
const linesBeforeTagsLine = frontmatterLines.slice(0, tagsLineIndex)
|
||||
|
@ -68,12 +81,4 @@ export class FrontmatterLinter implements Linter {
|
|||
}
|
||||
]
|
||||
}
|
||||
|
||||
private static loadYaml(raw: string): RawNoteFrontmatter | undefined {
|
||||
try {
|
||||
return load(raw) as RawNoteFrontmatter
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -16,6 +16,6 @@ export const buildStateFromFirstHeadingUpdate = (state: NoteDetails, firstHeadin
|
|||
return {
|
||||
...state,
|
||||
firstHeading: firstHeading,
|
||||
title: generateNoteTitle(state.frontmatter, firstHeading)
|
||||
title: generateNoteTitle(state.frontmatter, () => firstHeading)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue