mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2026-06-23 04:10:17 +00:00
refactor(commons): frontmatter validator uses zod and allows custom options
The frontmatter validator was still the one left place that used Joi instead of the now widely used zod in HedgeDoc. Since zod can do validation, coercion and providing types based on the schema, the code could be drastically reduced compared to the old frontmatter validator. At the same time, the validator is now less strict. Custom fields are still allowed for people that want to add their own frontmatter tags which are unrelated to HedgeDoc. Furthermore, we now allow the complete set of RevealOptions for the slideOptions key instead of only a few handpicked ones. Fixes #5946 Signed-off-by: Erik Michelson <github@erik.michelson.eu> Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
committed by
Philip Molares
parent
016ec6fd90
commit
89e441597d
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
import { describe, it, expect, beforeAll, beforeEach, afterEach, jest } from '@jest/globals';
|
import { describe, it, expect, beforeAll, beforeEach, afterEach, jest } from '@jest/globals';
|
||||||
import { SortMode } from '@hedgedoc/commons';
|
import { SortMode } from '@hedgedoc/commons';
|
||||||
import { OptionalNoteType, OptionalSortMode } from '@hedgedoc/commons';
|
import type { OptionalSortMode } from '@hedgedoc/commons';
|
||||||
import {
|
import {
|
||||||
FieldNameGroup,
|
FieldNameGroup,
|
||||||
FieldNameNote,
|
FieldNameNote,
|
||||||
@@ -157,7 +157,7 @@ describe('ExploreService', () => {
|
|||||||
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid" from "note" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) as "latest_revisions_per_note" where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note"."owner_id" = \$3 and LOWER\("revision"."title"\) LIKE \$4 order by "revision"."created_at" desc limit \$5/,
|
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid" from "note" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) as "latest_revisions_per_note" where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note"."owner_id" = \$3 and LOWER\("revision"."title"\) LIKE \$4 order by "revision"."created_at" desc limit \$5/,
|
||||||
[1, true, mockUserId, '%test%', ENTRIES_PER_PAGE_LIMIT],
|
[1, true, mockUserId, '%test%', ENTRIES_PER_PAGE_LIMIT],
|
||||||
],
|
],
|
||||||
] as [string, OptionalNoteType, OptionalSortMode, string, RegExp, unknown[]][])(
|
] as [string, NoteType, OptionalSortMode, string, RegExp, unknown[]][])(
|
||||||
'correctly get all notes owned by user with',
|
'correctly get all notes owned by user with',
|
||||||
(name, noteType, sortBy, search, regex, bindings) => {
|
(name, noteType, sortBy, search, regex, bindings) => {
|
||||||
// oxlint-disable-next-line jest/valid-title
|
// oxlint-disable-next-line jest/valid-title
|
||||||
@@ -226,7 +226,7 @@ describe('ExploreService', () => {
|
|||||||
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid" from "note_user_permission" inner join "note" on "note_user_permission"."note_id" = "note"."id" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) as "latest_revisions_per_note" where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note_user_permission"."user_id" = \$3 and LOWER\("revision"."title"\) LIKE \$4 order by "revision"."created_at" desc limit \$5/,
|
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid" from "note_user_permission" inner join "note" on "note_user_permission"."note_id" = "note"."id" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) as "latest_revisions_per_note" where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note_user_permission"."user_id" = \$3 and LOWER\("revision"."title"\) LIKE \$4 order by "revision"."created_at" desc limit \$5/,
|
||||||
[1, true, mockUserId, '%test%', ENTRIES_PER_PAGE_LIMIT],
|
[1, true, mockUserId, '%test%', ENTRIES_PER_PAGE_LIMIT],
|
||||||
],
|
],
|
||||||
] as [string, OptionalNoteType, OptionalSortMode, string, RegExp, unknown[]][])(
|
] as [string, NoteType, OptionalSortMode, string, RegExp, unknown[]][])(
|
||||||
'correctly get all notes shared with the user with',
|
'correctly get all notes shared with the user with',
|
||||||
(name, noteType, sortBy, search, regex, bindings) => {
|
(name, noteType, sortBy, search, regex, bindings) => {
|
||||||
// oxlint-disable-next-line jest/valid-title
|
// oxlint-disable-next-line jest/valid-title
|
||||||
@@ -295,7 +295,7 @@ describe('ExploreService', () => {
|
|||||||
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid" from "note" inner join "note_group_permission" on "note"."id" = "note_group_permission"."note_id" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) as "latest_revisions_per_note" where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note_group_permission"."group_id" = \$3 and "note"."publicly_visible" = \$4 and LOWER\("revision"."title"\) LIKE \$5 order by "revision"."created_at" desc limit \$6/,
|
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid" from "note" inner join "note_group_permission" on "note"."id" = "note_group_permission"."note_id" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) as "latest_revisions_per_note" where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" where "alias"."is_primary" = \$2 and "note_group_permission"."group_id" = \$3 and "note"."publicly_visible" = \$4 and LOWER\("revision"."title"\) LIKE \$5 order by "revision"."created_at" desc limit \$6/,
|
||||||
[1, true, mockEveryoneGroupId, true, '%test%', ENTRIES_PER_PAGE_LIMIT],
|
[1, true, mockEveryoneGroupId, true, '%test%', ENTRIES_PER_PAGE_LIMIT],
|
||||||
],
|
],
|
||||||
] as [string, OptionalNoteType, OptionalSortMode, string, RegExp, unknown[]][])(
|
] as [string, NoteType, OptionalSortMode, string, RegExp, unknown[]][])(
|
||||||
'correctly get all public notes with',
|
'correctly get all public notes with',
|
||||||
(name, noteType, sortBy, search, regex, bindings) => {
|
(name, noteType, sortBy, search, regex, bindings) => {
|
||||||
// oxlint-disable-next-line jest/valid-title
|
// oxlint-disable-next-line jest/valid-title
|
||||||
@@ -403,7 +403,7 @@ describe('ExploreService', () => {
|
|||||||
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid", "visited_notes"."visited_at" as "lastVisitedAt", "note"."id" as "noteId" from "note" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) as "latest_revisions_per_note" where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" left join "visited_notes" on "visited_notes"."note_id" = "note"."id" where "alias"."is_primary" = \$2 and "visited_notes"."user_id" = \$3 and LOWER\("revision"."title"\) LIKE \$4 order by "revision"."created_at" desc limit \$5/,
|
/select "alias"."alias" as "primaryAlias", "revision"."title" as "title", "revision"."note_type" as "noteType", "user"."username" as "ownerUsername", "note"."created_at" as "createdAt", "revision"."created_at" as "lastChangedAt", "revision"."uuid" as "revisionUuid", "visited_notes"."visited_at" as "lastVisitedAt", "note"."id" as "noteId" from "note" inner join "alias" on "alias"."note_id" = "note"."id" inner join "user" on "user"."id" = "note"."owner_id" inner join \(select "uuid", "note_id" from \(select "uuid", "note_id", row_number\(\) over \(partition by "note_id" order by "created_at" desc\) as rn from "revision"\) as "latest_revisions_per_note" where "rn" = \$1\) as "latest_revision" on "latest_revision"."note_id" = "note"."id" inner join "revision" on "revision"."note_id" = "note"."id" and "revision"."uuid" = "latest_revision"."uuid" left join "visited_notes" on "visited_notes"."note_id" = "note"."id" where "alias"."is_primary" = \$2 and "visited_notes"."user_id" = \$3 and LOWER\("revision"."title"\) LIKE \$4 order by "revision"."created_at" desc limit \$5/,
|
||||||
[1, true, mockUserId, '%test%', ENTRIES_PER_PAGE_LIMIT],
|
[1, true, mockUserId, '%test%', ENTRIES_PER_PAGE_LIMIT],
|
||||||
],
|
],
|
||||||
] as [string, OptionalNoteType, OptionalSortMode, string, RegExp, unknown[]][])(
|
] as [string, NoteType, OptionalSortMode, string, RegExp, unknown[]][])(
|
||||||
'correctly get all notes visited by the user with',
|
'correctly get all notes visited by the user with',
|
||||||
(name, noteType, sortBy, search, regex, bindings) => {
|
(name, noteType, sortBy, search, regex, bindings) => {
|
||||||
// oxlint-disable-next-line jest/valid-title
|
// oxlint-disable-next-line jest/valid-title
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ describe('revision entity', () => {
|
|||||||
'---\ntitle: \n - 1\n - 2\n---\nThis is a note content',
|
'---\ntitle: \n - 1\n - 2\n---\nThis is a note content',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(title).toBe('');
|
expect(title).toBe('1,2');
|
||||||
expect(description).toBe('');
|
expect(description).toBe('');
|
||||||
expect(tags).toStrictEqual([]);
|
expect(tags).toStrictEqual([]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,14 +4,13 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
convertRawFrontmatterToNoteFrontmatter,
|
|
||||||
defaultNoteFrontmatter,
|
defaultNoteFrontmatter,
|
||||||
extractFirstHeading,
|
extractFirstHeading,
|
||||||
extractFrontmatter,
|
extractFrontmatter,
|
||||||
generateNoteTitle,
|
generateNoteTitle,
|
||||||
NoteFrontmatter,
|
type NoteFrontmatter,
|
||||||
NoteType,
|
NoteType,
|
||||||
parseRawFrontmatterFromYaml,
|
parseNoteFrontmatter,
|
||||||
} from '@hedgedoc/commons';
|
} from '@hedgedoc/commons';
|
||||||
import { parseDocument } from 'htmlparser2';
|
import { parseDocument } from 'htmlparser2';
|
||||||
import MarkdownIt from 'markdown-it';
|
import MarkdownIt from 'markdown-it';
|
||||||
@@ -81,11 +80,11 @@ function parseFrontmatter(content: string): FrontmatterParserResult | undefined
|
|||||||
}
|
}
|
||||||
|
|
||||||
const firstLineOfContentIndex = extractionResult.lineOffset + 1;
|
const firstLineOfContentIndex = extractionResult.lineOffset + 1;
|
||||||
const rawDataValidation = parseRawFrontmatterFromYaml(rawText);
|
const frontmatterParseResult = parseNoteFrontmatter(rawText);
|
||||||
const noteFrontmatter =
|
const noteFrontmatter =
|
||||||
rawDataValidation.error !== undefined
|
frontmatterParseResult.error !== undefined
|
||||||
? defaultNoteFrontmatter
|
? defaultNoteFrontmatter
|
||||||
: convertRawFrontmatterToNoteFrontmatter(rawDataValidation.value);
|
: frontmatterParseResult.value;
|
||||||
return {
|
return {
|
||||||
frontmatter: noteFrontmatter,
|
frontmatter: noteFrontmatter,
|
||||||
firstLineOfContentIndex: firstLineOfContentIndex,
|
firstLineOfContentIndex: firstLineOfContentIndex,
|
||||||
|
|||||||
@@ -46,7 +46,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"domhandler": "5.0.3",
|
"domhandler": "5.0.3",
|
||||||
"eventemitter2": "6.4.9",
|
"eventemitter2": "6.4.9",
|
||||||
"joi": "17.13.3",
|
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
"reveal.js": "5.2.1",
|
"reveal.js": "5.2.1",
|
||||||
"ws": "8.19.0",
|
"ws": "8.19.0",
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export * from './explore-page/index.js'
|
|||||||
export * from './frontmatter-extractor/index.js'
|
export * from './frontmatter-extractor/index.js'
|
||||||
export * from './message-transporters/index.js'
|
export * from './message-transporters/index.js'
|
||||||
export * from './note-frontmatter/index.js'
|
export * from './note-frontmatter/index.js'
|
||||||
export * from './note-frontmatter-parser/index.js'
|
|
||||||
export * from './parse-url/index.js'
|
export * from './parse-url/index.js'
|
||||||
export * from './permissions/index.js'
|
export * from './permissions/index.js'
|
||||||
export * from './title-extraction/index.js'
|
export * from './title-extraction/index.js'
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { RealtimeUser, RemoteCursor } from './realtime-user.js'
|
import type { RealtimeUser, RemoteCursor } from './realtime-user.js'
|
||||||
|
|
||||||
export enum MessageType {
|
export enum MessageType {
|
||||||
NOTE_CONTENT_STATE_REQUEST = 'NOTE_CONTENT_STATE_REQUEST',
|
NOTE_CONTENT_STATE_REQUEST = 'NOTE_CONTENT_STATE_REQUEST',
|
||||||
|
|||||||
-48
@@ -1,48 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2025 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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { NoteFrontmatter } from '../note-frontmatter/index.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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
export * from './parse-raw-frontmatter-from-yaml.js'
|
|
||||||
export * from './convert-raw-frontmatter-to-note-frontmatter.js'
|
|
||||||
export * from './parse-tags.js'
|
|
||||||
export * from './types.js'
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2025 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')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { defaultNoteFrontmatter } from '../note-frontmatter/index.js'
|
|
||||||
import { NoteTextDirection, NoteType, OpenGraph } from '../note-frontmatter/index.js'
|
|
||||||
import { SlideOptions } from '../note-frontmatter/index.js'
|
|
||||||
import { ISO6391 } from '../note-frontmatter/iso6391.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)
|
|
||||||
.lowercase()
|
|
||||||
.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)
|
|
||||||
|
|
||||||
type ParserResult =
|
|
||||||
| {
|
|
||||||
error: undefined
|
|
||||||
warning?: ValidationError
|
|
||||||
value: RawNoteFrontmatter
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
error: Error
|
|
||||||
warning?: ValidationError
|
|
||||||
value: undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseRawFrontmatterFromYaml = (rawYaml: string): ParserResult => {
|
|
||||||
try {
|
|
||||||
const rawNoteFrontmatter = load(rawYaml)
|
|
||||||
return schema.validate(rawNoteFrontmatter, { convert: true })
|
|
||||||
} catch {
|
|
||||||
return { error: new Error('Invalid YAML'), value: undefined }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2025 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',
|
|
||||||
])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
/*
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
Iso6391Language,
|
|
||||||
NoteTextDirection,
|
|
||||||
NoteType,
|
|
||||||
OpenGraph,
|
|
||||||
} from '../note-frontmatter/index.js'
|
|
||||||
import { SlideOptions } from '../note-frontmatter/index.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
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NoteType } from './note-type.js'
|
||||||
|
import { NoteTextDirection } from './note-text-direction.js'
|
||||||
|
|
||||||
|
export const defaultNoteFrontmatter = {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
tags: [],
|
||||||
|
type: NoteType.DOCUMENT as const,
|
||||||
|
breaks: true,
|
||||||
|
dir: NoteTextDirection.LTR,
|
||||||
|
robots: '',
|
||||||
|
lang: 'en' as const,
|
||||||
|
license: '',
|
||||||
|
opengraph: {},
|
||||||
|
slideOptions: {
|
||||||
|
transition: 'zoom',
|
||||||
|
autoSlide: 0,
|
||||||
|
autoSlideStoppable: true,
|
||||||
|
backgroundTransition: 'fade',
|
||||||
|
slideNumber: false,
|
||||||
|
} as const,
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { NoteFrontmatter, NoteTextDirection, NoteType } from './frontmatter.js'
|
|
||||||
import { SlideOptions } from './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,
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import { ISO6391 } from './iso6391.js'
|
|
||||||
import { SlideOptions } from './slide-show-options.js'
|
|
||||||
|
|
||||||
export type Iso6391Language = (typeof ISO6391)[number]
|
|
||||||
|
|
||||||
export type OpenGraph = Record<string, string>
|
|
||||||
|
|
||||||
export enum NoteTextDirection {
|
|
||||||
LTR = 'ltr',
|
|
||||||
RTL = 'rtl',
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum NoteType {
|
|
||||||
DOCUMENT = 'document',
|
|
||||||
SLIDE = 'slide',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type OptionalNoteType = NoteType | ''
|
|
||||||
|
|
||||||
export interface NoteFrontmatter {
|
|
||||||
title: string
|
|
||||||
description: string
|
|
||||||
tags: string[]
|
|
||||||
robots: string
|
|
||||||
lang: Iso6391Language
|
|
||||||
dir: NoteTextDirection
|
|
||||||
newlinesAreBreaks: boolean
|
|
||||||
license: string
|
|
||||||
type: NoteType
|
|
||||||
opengraph: OpenGraph
|
|
||||||
slideOptions: SlideOptions
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,10 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export * from './default-note-frontmatter.js'
|
||||||
export * from './iso6391.js'
|
export * from './iso6391.js'
|
||||||
export * from './frontmatter.js'
|
export * from './note-frontmatter.js'
|
||||||
export * from './slide-show-options.js'
|
export * from './note-text-direction.js'
|
||||||
export * from './default-values.js'
|
export * from './note-type.js'
|
||||||
|
export * from './parse-note-frontmatter.js'
|
||||||
|
export * from './parse-tags-field.js'
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import z from 'zod'
|
||||||
|
import type { RevealOptions } from 'reveal.js'
|
||||||
|
import { parseTagsField } from './parse-tags-field.js'
|
||||||
|
import { NoteType } from './note-type.js'
|
||||||
|
import { ISO6391 } from './iso6391.js'
|
||||||
|
import { defaultNoteFrontmatter } from './default-note-frontmatter.js'
|
||||||
|
import { NoteTextDirection } from './note-text-direction.js'
|
||||||
|
|
||||||
|
// Reveal.js provides types but no runtime validation of fields, so we validate them as unknown and accept everything
|
||||||
|
const slideOptionsSchema = z
|
||||||
|
.record(z.unknown())
|
||||||
|
.default(defaultNoteFrontmatter.slideOptions)
|
||||||
|
.transform((slideOptions) => slideOptions as Partial<RevealOptions>)
|
||||||
|
|
||||||
|
export const NoteFrontmatterSchema = z
|
||||||
|
.object({
|
||||||
|
title: z.coerce.string().default(defaultNoteFrontmatter.title).describe('Title of the note'),
|
||||||
|
description: z.coerce
|
||||||
|
.string()
|
||||||
|
.default(defaultNoteFrontmatter.description)
|
||||||
|
.describe('Description of the note'),
|
||||||
|
tags: z
|
||||||
|
.preprocess(parseTagsField, z.array(z.string()))
|
||||||
|
.describe('List of tags for filtering on the explore page')
|
||||||
|
.default([]),
|
||||||
|
type: z
|
||||||
|
.nativeEnum(NoteType)
|
||||||
|
.default(defaultNoteFrontmatter.type)
|
||||||
|
.describe('Type of the renderer to use'),
|
||||||
|
robots: z.string().default(defaultNoteFrontmatter.robots).describe('Robots meta tag'),
|
||||||
|
lang: z.enum(ISO6391).default(defaultNoteFrontmatter.lang).describe('Language of the note'),
|
||||||
|
dir: z
|
||||||
|
.nativeEnum(NoteTextDirection)
|
||||||
|
.default(defaultNoteFrontmatter.dir)
|
||||||
|
.describe('Text writing direction'),
|
||||||
|
breaks: z
|
||||||
|
.boolean()
|
||||||
|
.default(defaultNoteFrontmatter.breaks)
|
||||||
|
.describe('Treat newlines as line break'),
|
||||||
|
license: z
|
||||||
|
.string()
|
||||||
|
.default(defaultNoteFrontmatter.license)
|
||||||
|
.describe('License header field to add to the HTML'),
|
||||||
|
opengraph: z
|
||||||
|
.record(z.coerce.string())
|
||||||
|
.default(defaultNoteFrontmatter.opengraph)
|
||||||
|
.describe('OpenGraph meta tags'),
|
||||||
|
slideOptions: slideOptionsSchema.describe('Reveal.js options for slides'),
|
||||||
|
})
|
||||||
|
.describe('Frontmatter options parsed by HedgeDoc, others are ignored')
|
||||||
|
.passthrough()
|
||||||
|
|
||||||
|
export type NoteFrontmatter = z.infer<typeof NoteFrontmatterSchema>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
export enum NoteTextDirection {
|
||||||
|
LTR = 'ltr',
|
||||||
|
RTL = 'rtl',
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2025 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
export enum NoteType {
|
||||||
|
DOCUMENT = 'document',
|
||||||
|
SLIDE = 'slide',
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { describe, expect, it } from '@jest/globals'
|
||||||
|
import { parseNoteFrontmatter } from './parse-note-frontmatter.js'
|
||||||
|
|
||||||
|
describe('parseNoteFrontmatter', () => {
|
||||||
|
it('marks comma-separated tags as deprecated', () => {
|
||||||
|
const result = parseNoteFrontmatter('tags: foo, bar')
|
||||||
|
|
||||||
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(result.usesDeprecatedTagsFormat).toBe(true)
|
||||||
|
expect(result.value?.tags).toStrictEqual(['foo', 'bar'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not mark array tags as deprecated', () => {
|
||||||
|
const result = parseNoteFrontmatter('tags:\n- foo\n- bar')
|
||||||
|
|
||||||
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(result.usesDeprecatedTagsFormat).toBe(false)
|
||||||
|
expect(result.value?.tags).toStrictEqual(['foo', 'bar'])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { load } from 'js-yaml'
|
||||||
|
import { type NoteFrontmatter, NoteFrontmatterSchema } from './note-frontmatter.js'
|
||||||
|
|
||||||
|
export type NoteFrontmatterParserResult =
|
||||||
|
| {
|
||||||
|
error: undefined
|
||||||
|
value: NoteFrontmatter
|
||||||
|
usesDeprecatedTagsFormat: boolean
|
||||||
|
rawTagsField: unknown
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
error: Error
|
||||||
|
value: undefined
|
||||||
|
usesDeprecatedTagsFormat: boolean
|
||||||
|
rawTagsField: unknown
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the note frontmatter from the given YAML string and returns the parsed result with collected errors.
|
||||||
|
*
|
||||||
|
* @param rawYaml The raw YAML string to parse
|
||||||
|
* @returns A wrapper containing the parsed note frontmatter and an error in case parsing failed
|
||||||
|
*/
|
||||||
|
export const parseNoteFrontmatter = (rawYaml: string): NoteFrontmatterParserResult => {
|
||||||
|
try {
|
||||||
|
const rawNoteFrontmatter = load(rawYaml)
|
||||||
|
const rawNoteFrontmatterRecord =
|
||||||
|
typeof rawNoteFrontmatter === 'object' &&
|
||||||
|
rawNoteFrontmatter !== null &&
|
||||||
|
!Array.isArray(rawNoteFrontmatter)
|
||||||
|
? (rawNoteFrontmatter as Record<string, unknown>)
|
||||||
|
: undefined
|
||||||
|
const rawTagsField = rawNoteFrontmatterRecord?.tags
|
||||||
|
const usesDeprecatedTagsFormat =
|
||||||
|
rawNoteFrontmatterRecord !== undefined &&
|
||||||
|
Object.hasOwn(rawNoteFrontmatterRecord, 'tags') &&
|
||||||
|
!Array.isArray(rawNoteFrontmatterRecord.tags)
|
||||||
|
if (
|
||||||
|
typeof rawNoteFrontmatter !== 'object' ||
|
||||||
|
rawNoteFrontmatter === null ||
|
||||||
|
Array.isArray(rawNoteFrontmatter) ||
|
||||||
|
Object.keys(rawNoteFrontmatter).length === 0
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
error: new Error('Invalid YAML'),
|
||||||
|
value: undefined,
|
||||||
|
usesDeprecatedTagsFormat: false,
|
||||||
|
rawTagsField: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const parsedNoteFrontmatter = NoteFrontmatterSchema.safeParse(rawNoteFrontmatter)
|
||||||
|
if (!parsedNoteFrontmatter.success) {
|
||||||
|
return {
|
||||||
|
error: parsedNoteFrontmatter.error,
|
||||||
|
value: undefined,
|
||||||
|
usesDeprecatedTagsFormat,
|
||||||
|
rawTagsField: rawTagsField,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
error: undefined,
|
||||||
|
value: parsedNoteFrontmatter.data,
|
||||||
|
usesDeprecatedTagsFormat,
|
||||||
|
rawTagsField: rawTagsField,
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
return {
|
||||||
|
error: error as Error,
|
||||||
|
value: undefined,
|
||||||
|
usesDeprecatedTagsFormat: false,
|
||||||
|
rawTagsField: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
import { describe, expect, it } from '@jest/globals'
|
||||||
|
import { parseTagsField } from './parse-tags-field.js'
|
||||||
|
|
||||||
|
describe('parseTagsField', () => {
|
||||||
|
it('parses comma-separated strings', () => {
|
||||||
|
expect(parseTagsField('foo, bar,baz')).toStrictEqual(['foo', 'bar', 'baz'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes empty string entries from comma-separated strings', () => {
|
||||||
|
expect(parseTagsField('foo,, bar')).toStrictEqual(['foo', 'bar'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parses arrays by stringifying each item', () => {
|
||||||
|
expect(parseTagsField(['foo', 123, true])).toStrictEqual(['foo', '123', 'true'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('removes empty string entries from array entries', () => {
|
||||||
|
expect(parseTagsField(['foo', '', 'bar'])).toStrictEqual(['foo', 'bar'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns an empty array for invalid input', () => {
|
||||||
|
expect(parseTagsField(undefined)).toStrictEqual([])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 The HedgeDoc developers (see AUTHORS file)
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the tags field from the note frontmatter, which should be an array,
|
||||||
|
* but for backwards-compatibility we also accept comma-separated strings.
|
||||||
|
* Empty tags are dropped from the list and whitespace is trimmed.
|
||||||
|
*
|
||||||
|
* @param input The input to parse
|
||||||
|
* @returns The parsed list of tags or an empty array if the input is invalid
|
||||||
|
*/
|
||||||
|
export const parseTagsField = (input: unknown): string[] => {
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
return input
|
||||||
|
.split(',')
|
||||||
|
.map((tag) => tag.trim())
|
||||||
|
.filter((tag) => !!tag)
|
||||||
|
}
|
||||||
|
if (Array.isArray(input)) {
|
||||||
|
return input.map((tag) => tag.toString().trim()).filter((tag) => !!tag)
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
import type { RevealOptions } from 'reveal.js'
|
|
||||||
|
|
||||||
type WantedRevealOptions =
|
|
||||||
| 'autoSlide'
|
|
||||||
| 'autoSlideStoppable'
|
|
||||||
| 'transition'
|
|
||||||
| 'backgroundTransition'
|
|
||||||
| 'slideNumber'
|
|
||||||
|
|
||||||
export type SlideOptions = Pick<RevealOptions, WantedRevealOptions>
|
|
||||||
@@ -3,9 +3,11 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { NoteFrontmatter, NoteTextDirection, NoteType } from '../note-frontmatter/frontmatter.js'
|
|
||||||
import { generateNoteTitle } from './generate-note-title.js'
|
|
||||||
import { describe, expect, it } from '@jest/globals'
|
import { describe, expect, it } from '@jest/globals'
|
||||||
|
import type { NoteFrontmatter } from '../note-frontmatter/note-frontmatter.js'
|
||||||
|
import { NoteType } from '../note-frontmatter/note-type.js'
|
||||||
|
import { generateNoteTitle } from './generate-note-title.js'
|
||||||
|
import { NoteTextDirection } from '../note-frontmatter/note-text-direction.js'
|
||||||
|
|
||||||
const testFrontmatter: NoteFrontmatter = {
|
const testFrontmatter: NoteFrontmatter = {
|
||||||
title: '',
|
title: '',
|
||||||
@@ -14,7 +16,7 @@ const testFrontmatter: NoteFrontmatter = {
|
|||||||
robots: '',
|
robots: '',
|
||||||
lang: 'en',
|
lang: 'en',
|
||||||
dir: NoteTextDirection.LTR,
|
dir: NoteTextDirection.LTR,
|
||||||
newlinesAreBreaks: true,
|
breaks: true,
|
||||||
license: '',
|
license: '',
|
||||||
type: NoteType.DOCUMENT,
|
type: NoteType.DOCUMENT,
|
||||||
opengraph: {},
|
opengraph: {},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*
|
*
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import type { NoteFrontmatter } from '../note-frontmatter/frontmatter.js'
|
import type { NoteFrontmatter } from '../note-frontmatter/note-frontmatter.js'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates the note title from the given frontmatter or the first heading in the markdown content.
|
* Generates the note title from the given frontmatter or the first heading in the markdown content.
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ import { useMemo } from 'react'
|
|||||||
*/
|
*/
|
||||||
export const useSendAdditionalConfigurationToRenderer = (rendererReady: boolean): void => {
|
export const useSendAdditionalConfigurationToRenderer = (rendererReady: boolean): void => {
|
||||||
const darkModePreference = useApplicationState((state) => state.darkMode.darkModePreference)
|
const darkModePreference = useApplicationState((state) => state.darkMode.darkModePreference)
|
||||||
const newlinesAreBreaks = useApplicationState((state) => state.noteDetails?.frontmatter.newlinesAreBreaks)
|
const newlinesAreBreaks = useApplicationState((state) => state.noteDetails?.frontmatter.breaks)
|
||||||
|
|
||||||
useSendToRenderer(
|
useSendToRenderer(
|
||||||
useMemo(() => {
|
useMemo(() => {
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ describe('FrontmatterLinter', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
it('does not warn for the new array format', () => {
|
||||||
|
const frontmatterLinter = new FrontmatterLinter()
|
||||||
|
const editorView = mockEditorView('---\ntags:\n- a\n---')
|
||||||
|
|
||||||
|
expect(frontmatterLinter.lint(editorView)).toStrictEqual([])
|
||||||
|
})
|
||||||
it('with invalid yaml', () => {
|
it('with invalid yaml', () => {
|
||||||
testFrontmatterLinter('---\n1\n 2: 3\n---', {
|
testFrontmatterLinter('---\n1\n 2: 3\n---', {
|
||||||
from: 4,
|
from: 4,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import type { Linter } from './linter'
|
import type { Linter } from './linter'
|
||||||
import type { Diagnostic } from '@codemirror/lint'
|
import type { Diagnostic } from '@codemirror/lint'
|
||||||
import type { EditorView } from '@codemirror/view'
|
import type { EditorView } from '@codemirror/view'
|
||||||
import { extractFrontmatter, parseRawFrontmatterFromYaml, parseTags } from '@hedgedoc/commons'
|
import { extractFrontmatter, parseNoteFrontmatter, parseTagsField } from '@hedgedoc/commons'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,13 +24,15 @@ export class FrontmatterLinter implements Linter {
|
|||||||
const frontmatterLines = lines.slice(1, frontmatterExtraction.lineOffset - 1)
|
const frontmatterLines = lines.slice(1, frontmatterExtraction.lineOffset - 1)
|
||||||
const startOfYaml = lines[0].length + 1
|
const startOfYaml = lines[0].length + 1
|
||||||
const endOfYaml = startOfYaml + frontmatterLines.join('\n').length
|
const endOfYaml = startOfYaml + frontmatterLines.join('\n').length
|
||||||
const rawNoteFrontmatter = parseRawFrontmatterFromYaml(frontmatterExtraction.rawText)
|
const frontmatterParseResult = parseNoteFrontmatter(frontmatterExtraction.rawText)
|
||||||
if (rawNoteFrontmatter.error) {
|
if (frontmatterParseResult.error) {
|
||||||
return this.createErrorDiagnostics(startOfYaml, endOfYaml, rawNoteFrontmatter.error, 'error')
|
return this.createErrorDiagnostics(startOfYaml, endOfYaml, frontmatterParseResult.error, 'error')
|
||||||
} else if (rawNoteFrontmatter.warning) {
|
} else if (frontmatterParseResult.usesDeprecatedTagsFormat) {
|
||||||
return this.createErrorDiagnostics(startOfYaml, endOfYaml, rawNoteFrontmatter.warning, 'warning')
|
return this.createReplaceSingleStringTagsDiagnostic(
|
||||||
} else if (!Array.isArray(rawNoteFrontmatter.value.tags)) {
|
frontmatterParseResult.rawTagsField,
|
||||||
return this.createReplaceSingleStringTagsDiagnostic(rawNoteFrontmatter.value.tags, frontmatterLines, startOfYaml)
|
frontmatterLines,
|
||||||
|
startOfYaml
|
||||||
|
)
|
||||||
}
|
}
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -52,12 +54,12 @@ export class FrontmatterLinter implements Linter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private createReplaceSingleStringTagsDiagnostic(
|
private createReplaceSingleStringTagsDiagnostic(
|
||||||
rawTags: string,
|
rawTags: unknown,
|
||||||
frontmatterLines: string[],
|
frontmatterLines: string[],
|
||||||
startOfYaml: number
|
startOfYaml: number
|
||||||
): Diagnostic[] {
|
): Diagnostic[] {
|
||||||
const tags: string[] = parseTags(rawTags)
|
const tags = this.parseDeprecatedTags(rawTags)
|
||||||
const replacedText = 'tags:\n- ' + tags.join('\n- ')
|
const replacedText = `tags:\n- ${tags.join('\n- ')}`
|
||||||
const tagsLineIndex = frontmatterLines.findIndex((value) => value.startsWith('tags: '))
|
const tagsLineIndex = frontmatterLines.findIndex((value) => value.startsWith('tags: '))
|
||||||
const linesBeforeTagsLine = frontmatterLines.slice(0, tagsLineIndex)
|
const linesBeforeTagsLine = frontmatterLines.slice(0, tagsLineIndex)
|
||||||
const from = startOfYaml + linesBeforeTagsLine.join('\n').length + linesBeforeTagsLine.length
|
const from = startOfYaml + linesBeforeTagsLine.join('\n').length + linesBeforeTagsLine.length
|
||||||
@@ -81,4 +83,17 @@ export class FrontmatterLinter implements Linter {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseDeprecatedTags(rawTags: unknown): string[] {
|
||||||
|
if (rawTags === undefined || rawTags === null) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if (typeof rawTags === 'string' || Array.isArray(rawTags)) {
|
||||||
|
return parseTagsField(rawTags)
|
||||||
|
}
|
||||||
|
if (typeof rawTags === 'number' || typeof rawTags === 'boolean' || typeof rawTags === 'bigint') {
|
||||||
|
return parseTagsField(String(rawTags))
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import { Logger } from '../../../utils/logger'
|
import { Logger } from '../../../utils/logger'
|
||||||
import type { SlideOptions } from '@hedgedoc/commons'
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import type Reveal from 'reveal.js'
|
import type Reveal from 'reveal.js'
|
||||||
|
import type { RevealOptions } from 'reveal.js'
|
||||||
|
|
||||||
const log = new Logger('reveal.js')
|
const log = new Logger('reveal.js')
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ const initialSlideState: SlideState = {
|
|||||||
* @return The current state of reveal.js
|
* @return The current state of reveal.js
|
||||||
* @see https://revealjs.com/
|
* @see https://revealjs.com/
|
||||||
*/
|
*/
|
||||||
export const useReveal = (markdownContentLines: string[], slideOptions?: SlideOptions): REVEAL_STATUS => {
|
export const useReveal = (markdownContentLines: string[], slideOptions?: RevealOptions): REVEAL_STATUS => {
|
||||||
const [deck, setDeck] = useState<Reveal>()
|
const [deck, setDeck] = useState<Reveal>()
|
||||||
const [revealStatus, setRevealStatus] = useState<REVEAL_STATUS>(REVEAL_STATUS.NOT_INITIALISED)
|
const [revealStatus, setRevealStatus] = useState<REVEAL_STATUS>(REVEAL_STATUS.NOT_INITIALISED)
|
||||||
const currentSlideState = useRef<SlideState>(initialSlideState)
|
const currentSlideState = useRef<SlideState>(initialSlideState)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useRendererReceiveHandler } from './window-post-message-communicator/ho
|
|||||||
import type { BaseConfiguration } from './window-post-message-communicator/rendering-message'
|
import type { BaseConfiguration } from './window-post-message-communicator/rendering-message'
|
||||||
import { CommunicationMessageType, RendererType } from './window-post-message-communicator/rendering-message'
|
import { CommunicationMessageType, RendererType } from './window-post-message-communicator/rendering-message'
|
||||||
import { countWords } from './word-counter'
|
import { countWords } from './word-counter'
|
||||||
import type { SlideOptions } from '@hedgedoc/commons'
|
import type { RevealOptions } from 'reveal.js'
|
||||||
import { EventEmitter2 } from 'eventemitter2'
|
import { EventEmitter2 } from 'eventemitter2'
|
||||||
import React, { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { setPrintMode } from '../../redux/print-mode/methods'
|
import { setPrintMode } from '../../redux/print-mode/methods'
|
||||||
@@ -32,7 +32,7 @@ export const RenderPageContent: React.FC = () => {
|
|||||||
const communicator = useRendererToEditorCommunicator()
|
const communicator = useRendererToEditorCommunicator()
|
||||||
const sendScrolling = useRef<boolean>(false)
|
const sendScrolling = useRef<boolean>(false)
|
||||||
const [newLinesAreBreaks, setNewLinesAreBreaks] = useState<boolean>(true)
|
const [newLinesAreBreaks, setNewLinesAreBreaks] = useState<boolean>(true)
|
||||||
const [slideOptions, setSlideOptions] = useState<SlideOptions>()
|
const [slideOptions, setSlideOptions] = useState<RevealOptions>()
|
||||||
|
|
||||||
useRendererReceiveHandler(
|
useRendererReceiveHandler(
|
||||||
CommunicationMessageType.SET_SLIDE_OPTIONS,
|
CommunicationMessageType.SET_SLIDE_OPTIONS,
|
||||||
|
|||||||
+2
-2
@@ -11,11 +11,11 @@ import { RendererType } from '../../window-post-message-communicator/rendering-m
|
|||||||
import type { CommonMarkdownRendererProps } from '../common-markdown-renderer-props'
|
import type { CommonMarkdownRendererProps } from '../common-markdown-renderer-props'
|
||||||
import { LoadingSlide } from './loading-slide'
|
import { LoadingSlide } from './loading-slide'
|
||||||
import styles from './slideshow.module.scss'
|
import styles from './slideshow.module.scss'
|
||||||
import type { SlideOptions } from '@hedgedoc/commons'
|
import type { RevealOptions } from 'reveal.js'
|
||||||
import React, { useMemo, useRef } from 'react'
|
import React, { useMemo, useRef } from 'react'
|
||||||
|
|
||||||
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps {
|
export interface SlideshowMarkdownRendererProps extends CommonMarkdownRendererProps {
|
||||||
slideOptions?: SlideOptions
|
slideOptions?: RevealOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@
|
|||||||
*/
|
*/
|
||||||
import type { DarkModePreference } from '../../../redux/dark-mode/types'
|
import type { DarkModePreference } from '../../../redux/dark-mode/types'
|
||||||
import type { ScrollState } from '../../editor-page/synced-scroll/scroll-props'
|
import type { ScrollState } from '../../editor-page/synced-scroll/scroll-props'
|
||||||
import type { SlideOptions } from '@hedgedoc/commons'
|
import type { RevealOptions } from 'reveal.js'
|
||||||
|
|
||||||
export enum CommunicationMessageType {
|
export enum CommunicationMessageType {
|
||||||
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
|
SET_MARKDOWN_CONTENT = 'SET_MARKDOWN_CONTENT',
|
||||||
@@ -81,7 +81,7 @@ export interface SetScrollStateMessage {
|
|||||||
|
|
||||||
export interface SetSlideOptionsMessage {
|
export interface SetSlideOptionsMessage {
|
||||||
type: CommunicationMessageType.SET_SLIDE_OPTIONS
|
type: CommunicationMessageType.SET_SLIDE_OPTIONS
|
||||||
slideOptions: SlideOptions
|
slideOptions: RevealOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OnHeightChangeMessage {
|
export interface OnHeightChangeMessage {
|
||||||
|
|||||||
@@ -7,13 +7,7 @@ import { calculateLineStartIndexes } from './calculate-line-start-indexes'
|
|||||||
import { initialState } from './initial-state'
|
import { initialState } from './initial-state'
|
||||||
import type { NoteDetails } from './types'
|
import type { NoteDetails } from './types'
|
||||||
import type { FrontmatterExtractionResult, NoteFrontmatter } from '@hedgedoc/commons'
|
import type { FrontmatterExtractionResult, NoteFrontmatter } from '@hedgedoc/commons'
|
||||||
import {
|
import { extractFrontmatter, generateNoteTitle, parseNoteFrontmatter } from '@hedgedoc/commons'
|
||||||
convertRawFrontmatterToNoteFrontmatter,
|
|
||||||
extractFrontmatter,
|
|
||||||
generateNoteTitle,
|
|
||||||
parseRawFrontmatterFromYaml
|
|
||||||
} from '@hedgedoc/commons'
|
|
||||||
import { Optional } from '@mrdrogdrog/optional'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Copies a {@link NoteDetails} but with another markdown content.
|
* Copies a {@link NoteDetails} but with another markdown content.
|
||||||
@@ -93,12 +87,12 @@ const buildStateFromFrontmatterUpdate = (
|
|||||||
return buildStateFromFrontmatter(state, parseFrontmatter(frontmatterExtraction), frontmatterExtraction)
|
return buildStateFromFrontmatter(state, parseFrontmatter(frontmatterExtraction), frontmatterExtraction)
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseFrontmatter = (frontmatterExtraction: FrontmatterExtractionResult) => {
|
const parseFrontmatter = (frontmatterExtraction: FrontmatterExtractionResult): NoteFrontmatter => {
|
||||||
return Optional.of(parseRawFrontmatterFromYaml(frontmatterExtraction.rawText))
|
const parseResult = parseNoteFrontmatter(frontmatterExtraction.rawText)
|
||||||
.filter((frontmatter) => frontmatter.error === undefined)
|
if (parseResult.error !== undefined) {
|
||||||
.map((frontmatter) => frontmatter.value)
|
return initialState.frontmatter
|
||||||
.map((value) => convertRawFrontmatterToNoteFrontmatter(value))
|
}
|
||||||
.orElse(initialState.frontmatter)
|
return parseResult.value
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildStateFromFrontmatter = (
|
const buildStateFromFrontmatter = (
|
||||||
|
|||||||
+1
-1
@@ -78,7 +78,7 @@ describe('build state from set note data from server', () => {
|
|||||||
lang: 'en',
|
lang: 'en',
|
||||||
license: '',
|
license: '',
|
||||||
dir: NoteTextDirection.LTR,
|
dir: NoteTextDirection.LTR,
|
||||||
newlinesAreBreaks: true,
|
breaks: true,
|
||||||
type: NoteType.DOCUMENT,
|
type: NoteType.DOCUMENT,
|
||||||
opengraph: {},
|
opengraph: {},
|
||||||
slideOptions: {
|
slideOptions: {
|
||||||
|
|||||||
@@ -2832,22 +2832,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@hapi/hoek@npm:^9.0.0, @hapi/hoek@npm:^9.3.0":
|
|
||||||
version: 9.3.0
|
|
||||||
resolution: "@hapi/hoek@npm:9.3.0"
|
|
||||||
checksum: 10c0/a096063805051fb8bba4c947e293c664b05a32b47e13bc654c0dd43813a1cec993bdd8f29ceb838020299e1d0f89f68dc0d62a603c13c9cc8541963f0beca055
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@hapi/topo@npm:^5.1.0":
|
|
||||||
version: 5.1.0
|
|
||||||
resolution: "@hapi/topo@npm:5.1.0"
|
|
||||||
dependencies:
|
|
||||||
"@hapi/hoek": "npm:^9.0.0"
|
|
||||||
checksum: 10c0/b16b06d9357947149e032bdf10151eb71aea8057c79c4046bf32393cb89d0d0f7ca501c40c0f7534a5ceca078de0700d2257ac855c15e59fe4e00bba2f25c86f
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@hedgedoc/backend@workspace:backend":
|
"@hedgedoc/backend@workspace:backend":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@hedgedoc/backend@workspace:backend"
|
resolution: "@hedgedoc/backend@workspace:backend"
|
||||||
@@ -2945,7 +2929,6 @@ __metadata:
|
|||||||
domhandler: "npm:5.0.3"
|
domhandler: "npm:5.0.3"
|
||||||
eventemitter2: "npm:6.4.9"
|
eventemitter2: "npm:6.4.9"
|
||||||
jest: "npm:29.7.0"
|
jest: "npm:29.7.0"
|
||||||
joi: "npm:17.13.3"
|
|
||||||
js-yaml: "npm:4.1.1"
|
js-yaml: "npm:4.1.1"
|
||||||
reveal.js: "npm:5.2.1"
|
reveal.js: "npm:5.2.1"
|
||||||
ts-jest: "npm:29.4.6"
|
ts-jest: "npm:29.4.6"
|
||||||
@@ -5246,29 +5229,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@sideway/address@npm:^4.1.5":
|
|
||||||
version: 4.1.5
|
|
||||||
resolution: "@sideway/address@npm:4.1.5"
|
|
||||||
dependencies:
|
|
||||||
"@hapi/hoek": "npm:^9.0.0"
|
|
||||||
checksum: 10c0/638eb6f7e7dba209053dd6c8da74d7cc995e2b791b97644d0303a7dd3119263bcb7225a4f6804d4db2bc4f96e5a9d262975a014f58eae4d1753c27cbc96ef959
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@sideway/formula@npm:^3.0.1":
|
|
||||||
version: 3.0.1
|
|
||||||
resolution: "@sideway/formula@npm:3.0.1"
|
|
||||||
checksum: 10c0/3fe81fa9662efc076bf41612b060eb9b02e846ea4bea5bd114f1662b7f1541e9dedcf98aff0d24400bcb92f113964a50e0290b86e284edbdf6346fa9b7e2bf2c
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@sideway/pinpoint@npm:^2.0.0":
|
|
||||||
version: 2.0.0
|
|
||||||
resolution: "@sideway/pinpoint@npm:2.0.0"
|
|
||||||
checksum: 10c0/d2ca75dacaf69b8fc0bb8916a204e01def3105ee44d8be16c355e5f58189eb94039e15ce831f3d544f229889ccfa35562a0ce2516179f3a7ee1bbe0b71e55b36
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"@sinclair/typebox@npm:^0.27.8":
|
"@sinclair/typebox@npm:^0.27.8":
|
||||||
version: 0.27.8
|
version: 0.27.8
|
||||||
resolution: "@sinclair/typebox@npm:0.27.8"
|
resolution: "@sinclair/typebox@npm:0.27.8"
|
||||||
@@ -12377,19 +12337,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"joi@npm:17.13.3":
|
|
||||||
version: 17.13.3
|
|
||||||
resolution: "joi@npm:17.13.3"
|
|
||||||
dependencies:
|
|
||||||
"@hapi/hoek": "npm:^9.3.0"
|
|
||||||
"@hapi/topo": "npm:^5.1.0"
|
|
||||||
"@sideway/address": "npm:^4.1.5"
|
|
||||||
"@sideway/formula": "npm:^3.0.1"
|
|
||||||
"@sideway/pinpoint": "npm:^2.0.0"
|
|
||||||
checksum: 10c0/9262aef1da3f1bec5b03caf50c46368899fe03b8ff26cbe3d53af4584dd1049079fc97230bbf1500b6149db7cc765b9ee45f0deb24bb6fc3fa06229d7148c17f
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"jose@npm:5.10.0":
|
"jose@npm:5.10.0":
|
||||||
version: 5.10.0
|
version: 5.10.0
|
||||||
resolution: "jose@npm:5.10.0"
|
resolution: "jose@npm:5.10.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user