diff --git a/cypress/e2e/yamlArrayDeprecationMessage.spec.ts b/cypress/e2e/yamlArrayDeprecationMessage.spec.ts deleted file mode 100644 index 3aa7f9294..000000000 --- a/cypress/e2e/yamlArrayDeprecationMessage.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -describe('YAML Array for deprecated syntax of document tags in frontmatter', () => { - beforeEach(() => { - cy.visitTestNote() - }) - - it('is shown when using old syntax', () => { - cy.setCodemirrorContent('---\ntags: a, b, c\n---') - cy.getIframeBody().findByCypressId('yamlArrayDeprecationAlert').should('be.visible') - }) - - it("isn't shown when using inline yaml-array", () => { - cy.setCodemirrorContent("---\ntags: ['a', 'b', 'c']\n---") - cy.getIframeBody().findByCypressId('yamlArrayDeprecationAlert').should('not.exist') - }) - - it("isn't shown when using multi line yaml-array", () => { - cy.setCodemirrorContent('---\ntags:\n - a\n - b\n - c\n---') - cy.getIframeBody().findByCypressId('yamlArrayDeprecationAlert').should('not.exist') - }) -}) diff --git a/locales/en.json b/locales/en.json index 04dc0abd2..741d34cdd 100644 --- a/locales/en.json +++ b/locales/en.json @@ -25,9 +25,6 @@ "mermaid": { "unknownError": "Unknown rendering error. Please check your browser console." }, - "sequence": { - "deprecationWarning": "The use of 'sequence' as code block language is deprecated." - }, "vega-lite": { "png": "Save as PNG", "svg": "Save as SVG", @@ -228,6 +225,13 @@ } }, "editor": { + "linter": { + "defaultAction": "Fix", + "sequence": "The use of 'sequence' as code block language is deprecated and will be removed in a future release.", + "shortcode": "The {{shortcode}} short-code is deprecated and will be removed in a future release. Use a single line URL instead.", + "frontmatter": "The yaml-metadata is invalid.", + "frontmatter-tags": "The comma-separated definition of tags in the yaml-metadata is deprecated. Use a yaml-array instead." + }, "upload": { "uploadFile": { "withoutDescription": "Uploading file {{fileName}}", @@ -238,8 +242,6 @@ }, "untitledNote": "Untitled", "placeholder": "← Start by entering a title here\n===\nVisit the features page if you don't know what to do.\nHappy hacking :)", - "invalidYaml": "The yaml-header is invalid. See <0> for more information.", - "deprecatedTags": "The comma-separated definition of tags in the yaml-metadata is deprecated. Use a yaml-array instead.", "infoToc": "Structure your note with headings to see a table-of-contents here.", "help": { "shortcuts": { diff --git a/package.json b/package.json index 931086227..aaeab2dce 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@codemirror/lang-markdown": "6.0.1", "@codemirror/language": "6.2.1", "@codemirror/language-data": "6.1.0", + "@codemirror/lint": "6.0.0", "@codemirror/state": "6.1.0", "@codemirror/theme-one-dark": "6.0.0", "@codemirror/view": "6.1.2", diff --git a/src/components/editor-page/editor-pane/editor-pane.tsx b/src/components/editor-page/editor-pane/editor-pane.tsx index 4dde3cb1e..b73da4f4f 100644 --- a/src/components/editor-page/editor-pane/editor-pane.tsx +++ b/src/components/editor-page/editor-pane/editor-pane.tsx @@ -36,6 +36,13 @@ import { useInsertNoteContentIntoYTextInMockModeEffect } from './hooks/yjs/use-i import { useOnFirstEditorUpdateExtension } from './hooks/yjs/use-on-first-editor-update-extension' import { useIsConnectionSynced } from './hooks/yjs/use-is-connection-synced' import { useMarkdownContentYText } from './hooks/yjs/use-markdown-content-y-text' +import { lintGutter } from '@codemirror/lint' +import { useLinter } from './linter/linter' +import { YoutubeMarkdownExtension } from '../../markdown-renderer/markdown-extension/youtube/youtube-markdown-extension' +import { VimeoMarkdownExtension } from '../../markdown-renderer/markdown-extension/vimeo/vimeo-markdown-extension' +import { SequenceDiagramMarkdownExtension } from '../../markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram-markdown-extension' +import { LegacyShortcodesMarkdownExtension } from '../../markdown-renderer/markdown-extension/legacy-short-codes/legacy-shortcodes-markdown-extension' +import { FrontmatterLinter } from './linter/frontmatter-linter' /** * Renders the text editor pane of the editor. @@ -78,8 +85,23 @@ export const EditorPane: React.FC = ({ scrollState, onScroll, onMak const [firstEditorUpdateExtension, firstUpdateHappened] = useOnFirstEditorUpdateExtension() useInsertNoteContentIntoYTextInMockModeEffect(firstUpdateHappened, websocketConnection) + // ToDo: Don't initialize new extension array here, instead refactor to global extension array + const markdownExtensionsLinters = useMemo(() => { + return [ + new YoutubeMarkdownExtension(), + new VimeoMarkdownExtension(), + new SequenceDiagramMarkdownExtension(), + new LegacyShortcodesMarkdownExtension() + ] + .flatMap((extension) => extension.buildLinter()) + .concat(new FrontmatterLinter()) + }, []) + const linter = useLinter(markdownExtensionsLinters) + const extensions = useMemo( () => [ + linter, + lintGutter(), markdown({ base: markdownLanguage, codeLanguages: (input) => findLanguageByCodeBlockName(languages, input) @@ -95,6 +117,7 @@ export const EditorPane: React.FC = ({ scrollState, onScroll, onMak firstEditorUpdateExtension ], [ + linter, editorScrollExtension, tablePasteExtensions, fileInsertExtension, diff --git a/src/components/editor-page/editor-pane/extended-codemirror/codemirror.module.scss b/src/components/editor-page/editor-pane/extended-codemirror/codemirror.module.scss index 22db56292..b865d3a09 100644 --- a/src/components/editor-page/editor-pane/extended-codemirror/codemirror.module.scss +++ b/src/components/editor-page/editor-pane/extended-codemirror/codemirror.module.scss @@ -21,6 +21,11 @@ display: none; } + .cm-diagnostic { + max-width: 400px; + padding: 10px; + } + //workarounds for line break problem.. see https://github.com/yjs/y-codemirror.next/pull/12 } } diff --git a/src/components/editor-page/editor-pane/linter/frontmatter-linter.spec.ts b/src/components/editor-page/editor-pane/linter/frontmatter-linter.spec.ts new file mode 100644 index 000000000..ecbce1d42 --- /dev/null +++ b/src/components/editor-page/editor-pane/linter/frontmatter-linter.spec.ts @@ -0,0 +1,85 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { mockI18n } from '../../../markdown-renderer/test-utils/mock-i18n' +import type { Diagnostic } from '@codemirror/lint' +import { mockEditorView } from './single-line-regex-linter.spec' +import { FrontmatterLinter } from './frontmatter-linter' +import { t } from 'i18next' + +const testFrontmatterLinter = ( + editorContent: string, + expectedDiagnostics: Partial, + expectedReplacement?: string +): void => { + const frontmatterLinter = new FrontmatterLinter() + const editorView = mockEditorView(editorContent) + const calculatedDiagnostics = frontmatterLinter.lint(editorView) + expect(calculatedDiagnostics).toHaveLength(1) + expect(calculatedDiagnostics[0].from).toEqual(expectedDiagnostics.from) + expect(calculatedDiagnostics[0].to).toEqual(expectedDiagnostics.to) + expect(calculatedDiagnostics[0].severity).toEqual(expectedDiagnostics.severity) + if (expectedReplacement !== undefined) { + const spy = jest.spyOn(editorView, 'dispatch') + expect(calculatedDiagnostics[0].actions).toHaveLength(1) + expect(calculatedDiagnostics[0].actions?.[0].name).toEqual(t('editor.linter.defaultAction')) + calculatedDiagnostics[0].actions?.[0].apply(editorView, calculatedDiagnostics[0].from, calculatedDiagnostics[0].to) + expect(spy).toHaveBeenCalledWith({ + changes: { + from: calculatedDiagnostics[0].from, + to: calculatedDiagnostics[0].to, + insert: expectedReplacement + } + }) + } +} + +describe('FrontmatterLinter', () => { + beforeAll(async () => { + await mockI18n() + }) + describe('with invalid tags', () => { + it('(one)', () => { + testFrontmatterLinter( + '---\ntags: a\n---', + { + from: 4, + to: 11, + severity: 'warning' + }, + 'tags:\n- a' + ) + }) + it('(one, but a number)', () => { + testFrontmatterLinter( + '---\ntags: 1\n---', + { + from: 4, + to: 11, + severity: 'warning' + }, + 'tags:\n- 1' + ) + }) + it('(multiple)', () => { + testFrontmatterLinter( + '---\ntags: 123, a\n---', + { + from: 4, + to: 16, + severity: 'warning' + }, + 'tags:\n- 123\n- a' + ) + }) + }) + it('with invalid yaml', () => { + testFrontmatterLinter('---\n1\n 2: 3\n---', { + from: 0, + to: 16, + severity: 'error' + }) + }) +}) diff --git a/src/components/editor-page/editor-pane/linter/frontmatter-linter.ts b/src/components/editor-page/editor-pane/linter/frontmatter-linter.ts new file mode 100644 index 000000000..ad24df013 --- /dev/null +++ b/src/components/editor-page/editor-pane/linter/frontmatter-linter.ts @@ -0,0 +1,78 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +import type { Linter } from './linter' +import type { EditorView } from '@codemirror/view' +import type { Diagnostic } from '@codemirror/lint' +import { extractFrontmatter } from '../../../../redux/note-details/frontmatter-extractor/extractor' +import { load } from 'js-yaml' +import type { RawNoteFrontmatter } from '../../../../redux/note-details/raw-note-frontmatter-parser/types' +import { t } from 'i18next' + +/** + * Creates a {@link Linter linter} for the yaml frontmatter. + * + * It checks that the yaml is valid and that the tags are not using the deprecated comma-seperated list syntax. + */ +export class FrontmatterLinter implements Linter { + lint(view: EditorView): Diagnostic[] { + const lines = view.state.doc.toString().split('\n') + const frontmatterExtraction = extractFrontmatter(lines) + if (!frontmatterExtraction.isPresent) { + return [] + } + const frontmatterLines = lines.slice(0, frontmatterExtraction.lineOffset + 1) + const rawNoteFrontmatter = FrontmatterLinter.loadYaml(frontmatterExtraction.rawText) + if (rawNoteFrontmatter === undefined) { + return [ + { + from: 0, + to: frontmatterLines.join('\n').length, + message: t('editor.linter.frontmatter'), + severity: 'error' + } + ] + } + if (typeof rawNoteFrontmatter.tags !== 'string' && typeof rawNoteFrontmatter.tags !== 'number') { + return [] + } + const tags: string[] = + rawNoteFrontmatter?.tags + .toString() + .split(',') + .map((entry) => entry.trim()) ?? [] + const replacedText = 'tags:\n- ' + tags.join('\n- ') + const tagsLineIndex = frontmatterLines.findIndex((value) => value.startsWith('tags: ')) + const linesBeforeTagsLine = frontmatterLines.slice(0, tagsLineIndex) + const from = linesBeforeTagsLine.join('\n').length + 1 + const to = from + frontmatterLines[tagsLineIndex].length + return [ + { + from: from, + to: to, + actions: [ + { + name: t('editor.linter.defaultAction'), + apply: (view: EditorView, from: number, to: number) => { + view.dispatch({ + changes: { from, to, insert: replacedText } + }) + } + } + ], + message: t('editor.linter.frontmatter-tags'), + severity: 'warning' + } + ] + } + + private static loadYaml(raw: string): RawNoteFrontmatter | undefined { + try { + return load(raw) as RawNoteFrontmatter + } catch { + return undefined + } + } +} diff --git a/src/components/editor-page/editor-pane/linter/linter.ts b/src/components/editor-page/editor-pane/linter/linter.ts new file mode 100644 index 000000000..a87fdf187 --- /dev/null +++ b/src/components/editor-page/editor-pane/linter/linter.ts @@ -0,0 +1,30 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { Diagnostic } from '@codemirror/lint' +import { linter } from '@codemirror/lint' +import { useMemo } from 'react' +import type { Extension } from '@codemirror/state' +import type { EditorView } from '@codemirror/view' + +/** + * The Linter interface. + * + * This should be implemented by each linter we want to use. + */ +export interface Linter { + lint(view: EditorView): Diagnostic[] +} + +/** + * The hook to create a single codemirror linter from all our linters. + * + * @param linters The {@link Linter linters} to use for the codemirror linter. + * @return The build codemirror linter + */ +export const useLinter = (linters: Linter[]): Extension => { + return useMemo(() => linter((view) => linters.flatMap((aLinter) => aLinter.lint(view))), [linters]) +} diff --git a/src/components/editor-page/editor-pane/linter/single-line-regex-linter.spec.ts b/src/components/editor-page/editor-pane/linter/single-line-regex-linter.spec.ts new file mode 100644 index 000000000..9eacdde3a --- /dev/null +++ b/src/components/editor-page/editor-pane/linter/single-line-regex-linter.spec.ts @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { SingleLineRegexLinter } from './single-line-regex-linter' +import type { Diagnostic } from '@codemirror/lint' +import { mockI18n } from '../../../markdown-renderer/test-utils/mock-i18n' +import type { EditorView } from '@codemirror/view' +import { Mock } from 'ts-mockery' +import type { EditorState, Text } from '@codemirror/state' + +export const mockEditorView = (content: string): EditorView => { + const docMock = Mock.of() + docMock.toString = () => content + return Mock.of({ + state: Mock.of({ + doc: docMock + }), + dispatch: jest.fn() + }) +} + +const testSingleLineRegexLinter = ( + regex: RegExp, + replace: (match: string) => string, + content: string, + expectedDiagnostics: Partial[] +): void => { + const testMessage = 'message' + const testActionLabel = 'actionLabel' + const singleLineRegexLinter = new SingleLineRegexLinter(regex, testMessage, replace, testActionLabel) + const editorView = mockEditorView(content) + const calculatedDiagnostics = singleLineRegexLinter.lint(editorView) + expect(calculatedDiagnostics).toHaveLength(expectedDiagnostics.length) + calculatedDiagnostics.forEach((calculatedDiagnostic, index) => { + expect(calculatedDiagnostic.from).toEqual(expectedDiagnostics[index].from) + expect(calculatedDiagnostic.to).toEqual(expectedDiagnostics[index].to) + expect(calculatedDiagnostic.message).toEqual(testMessage) + expect(calculatedDiagnostic.severity).toEqual('warning') + expect(calculatedDiagnostic.actions).toHaveLength(1) + expect(calculatedDiagnostic.actions?.[0].name).toEqual(testActionLabel) + }) +} + +describe('SingleLineRegexLinter', () => { + beforeAll(async () => { + await mockI18n() + }) + it('works for a simple regex', () => { + testSingleLineRegexLinter(/^foo$/, () => 'bar', 'This\nis\na\ntest\nfoo\nbar\n123', [ + { + from: 15, + to: 18 + } + ]) + }) + it('works for a multiple hits', () => { + testSingleLineRegexLinter(/^foo$/, () => 'bar', 'This\nfoo\na\ntest\nfoo\nbar\n123', [ + { + from: 5, + to: 8 + }, + { + from: 16, + to: 19 + } + ]) + }) + it('work if there are no hits', () => { + testSingleLineRegexLinter(/^nothing$/, () => 'bar', 'This\nfoo\na\ntest\nfoo\nbar\n123', []) + }) +}) diff --git a/src/components/editor-page/editor-pane/linter/single-line-regex-linter.ts b/src/components/editor-page/editor-pane/linter/single-line-regex-linter.ts new file mode 100644 index 000000000..3613c76fe --- /dev/null +++ b/src/components/editor-page/editor-pane/linter/single-line-regex-linter.ts @@ -0,0 +1,74 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { Linter } from './linter' +import type { EditorView } from '@codemirror/view' +import type { Diagnostic } from '@codemirror/lint' +import { t } from 'i18next' + +interface LineWithStartIndex { + line: string + startIndex: number +} + +/** + * Creates a {@link Linter linter} from {@link RegExp regexp} for single lines. + * + * @param regex The {@link RegExp regexp} to execute on each line + * @param message The message to display if the {@link RegExp regexp} hits + * @param replace The function to replace what was found by {@link RegExp regexp} + * @param actionLabel The optional label to translate and use as the fix button for the linter. + */ +export class SingleLineRegexLinter implements Linter { + constructor( + private regex: RegExp, + private message: string, + private replace: (match: string) => string, + private actionLabel?: string + ) {} + + lint(view: EditorView): Diagnostic[] { + return view.state.doc + .toString() + .split('\n') + .reduce( + (state, line, lineIndex, lines) => [ + ...state, + { + line, + startIndex: lineIndex === 0 ? 0 : state[lineIndex - 1].startIndex + lines[lineIndex - 1].length + 1 + } as LineWithStartIndex + ], + [] as LineWithStartIndex[] + ) + .map(({ line, startIndex }) => ({ + lineStartIndex: startIndex, + regexResult: this.regex.exec(line) + })) + .filter(({ regexResult }) => regexResult !== null && regexResult.length !== 0) + .map(({ lineStartIndex, regexResult }) => this.createDiagnostic(lineStartIndex, regexResult as RegExpExecArray)) + } + + private createDiagnostic(from: number, found: RegExpExecArray): Diagnostic { + const replacedText = this.replace(found[1]) + return { + from: from, + to: from + found[0].length, + actions: [ + { + name: t(this.actionLabel ?? 'editor.linter.defaultAction'), + apply: (view: EditorView, from: number, to: number) => { + view.dispatch({ + changes: { from, to, insert: replacedText } + }) + } + } + ], + message: this.message, + severity: 'warning' + } + } +} diff --git a/src/components/editor-page/renderer-pane/yaml-array-deprecation-alert.tsx b/src/components/editor-page/renderer-pane/yaml-array-deprecation-alert.tsx deleted file mode 100644 index 6efbcfc08..000000000 --- a/src/components/editor-page/renderer-pane/yaml-array-deprecation-alert.tsx +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import React from 'react' -import { Alert } from 'react-bootstrap' -import { Trans, useTranslation } from 'react-i18next' -import links from '../../../links.json' -import { TranslatedExternalLink } from '../../common/links/translated-external-link' -import { ShowIf } from '../../common/show-if/show-if' -import type { CommonModalProps } from '../../common/modals/common-modal' -import { cypressId } from '../../../utils/cypress-attribute' - -/** - * Renders an alert that indicated that the front matter tags property is using a deprecated format. - * - * @param show If the alert should be shown. - */ -export const YamlArrayDeprecationAlert: React.FC> = ({ show }) => { - useTranslation() - - return ( - - - - - - - -
- -
-
- ) -} diff --git a/src/components/markdown-renderer/invalid-yaml-alert.tsx b/src/components/markdown-renderer/invalid-yaml-alert.tsx deleted file mode 100644 index 2014e1d4f..000000000 --- a/src/components/markdown-renderer/invalid-yaml-alert.tsx +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import React from 'react' -import { Alert } from 'react-bootstrap' -import { Trans, useTranslation } from 'react-i18next' -import { InternalLink } from '../common/links/internal-link' -import { ShowIf } from '../common/show-if/show-if' -import type { SimpleAlertProps } from '../common/simple-alert/simple-alert-props' - -/** - * Renders an alert if the frontmatter yaml is invalid. - * - * @param show If this alert should be shown - */ -export const InvalidYamlAlert: React.FC = ({ show }) => { - useTranslation() - - return ( - - - - - - - - ) -} diff --git a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/legacy-shortcodes-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/legacy-shortcodes-markdown-extension.ts index 7a5377c6f..0de7707d7 100644 --- a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/legacy-shortcodes-markdown-extension.ts +++ b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/legacy-shortcodes-markdown-extension.ts @@ -1,14 +1,17 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ import { MarkdownExtension } from '../markdown-extension' import type MarkdownIt from 'markdown-it' -import { legacyPdfShortCode } from './replace-legacy-pdf-short-code' -import { legacySlideshareShortCode } from './replace-legacy-slideshare-short-code' -import { legacySpeakerdeckShortCode } from './replace-legacy-speakerdeck-short-code' +import { legacyPdfRegex, legacyPdfShortCode } from './replace-legacy-pdf-short-code' +import { legacySlideshareRegex, legacySlideshareShortCode } from './replace-legacy-slideshare-short-code' +import { legacySpeakerdeckRegex, legacySpeakerdeckShortCode } from './replace-legacy-speakerdeck-short-code' +import { SingleLineRegexLinter } from '../../../editor-page/editor-pane/linter/single-line-regex-linter' +import type { Linter } from '../../../editor-page/editor-pane/linter/linter' +import { t } from 'i18next' /** * Adds support for legacy shortcodes (pdf, slideshare and speakerdeck) by replacing them with anchor elements. @@ -19,4 +22,24 @@ export class LegacyShortcodesMarkdownExtension extends MarkdownExtension { legacySlideshareShortCode(markdownIt) legacySpeakerdeckShortCode(markdownIt) } + + public buildLinter(): Linter[] { + return [ + new SingleLineRegexLinter( + legacySpeakerdeckRegex, + t('editor.linter.shortcode', { shortcode: 'SpeakerDeck' }), + (match: string) => `https://speakerdeck.com/${match}` + ), + new SingleLineRegexLinter( + legacySlideshareRegex, + t('editor.linter.shortcode', { shortcode: 'SlideShare' }), + (match: string) => `https://www.slideshare.net/${match}` + ), + new SingleLineRegexLinter( + legacyPdfRegex, + t('editor.linter.shortcode', { shortcode: 'PDF' }), + (match: string) => match + ) + ] + } } diff --git a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-pdf-short-code.ts b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-pdf-short-code.ts index 39c7f99fc..acbc37d3a 100644 --- a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-pdf-short-code.ts +++ b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-pdf-short-code.ts @@ -8,7 +8,7 @@ import markdownItRegex from 'markdown-it-regex' import type MarkdownIt from 'markdown-it/lib' import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' -const finalRegex = /^{%pdf (\S*) *%}$/ +export const legacyPdfRegex = /^{%pdf (\S*) *%}$/ /** * Configure the given {@link MarkdownIt} to render legacy hedgedoc 1 pdf shortcodes as html links. @@ -18,7 +18,7 @@ const finalRegex = /^{%pdf (\S*) *%}$/ export const legacyPdfShortCode: MarkdownIt.PluginSimple = (markdownIt) => { markdownItRegex(markdownIt, { name: 'legacy-pdf-short-code', - regex: finalRegex, + regex: legacyPdfRegex, replace: (match) => `${match}` } as RegexOptions) } diff --git a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-slideshare-short-code.ts b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-slideshare-short-code.ts index 7751364b9..15289923f 100644 --- a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-slideshare-short-code.ts +++ b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-slideshare-short-code.ts @@ -8,7 +8,7 @@ import markdownItRegex from 'markdown-it-regex' import type MarkdownIt from 'markdown-it/lib' import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' -const finalRegex = /^{%slideshare (\w+\/[\w-]+) ?%}$/ +export const legacySlideshareRegex = /^{%slideshare (\w+\/[\w-]+) ?%}$/ /** * Configure the given {@link MarkdownIt} to render legacy hedgedoc 1 slideshare shortcodes as HTML links. @@ -18,7 +18,7 @@ const finalRegex = /^{%slideshare (\w+\/[\w-]+) ?%}$/ export const legacySlideshareShortCode: MarkdownIt.PluginSimple = (markdownIt) => { markdownItRegex(markdownIt, { name: 'legacy-slideshare-short-code', - regex: finalRegex, + regex: legacySlideshareRegex, replace: (match) => `https://www.slideshare.net/${match}` } as RegexOptions) } diff --git a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-speakerdeck-short-code.ts b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-speakerdeck-short-code.ts index 5d6c5d897..9e8a2da17 100644 --- a/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-speakerdeck-short-code.ts +++ b/src/components/markdown-renderer/markdown-extension/legacy-short-codes/replace-legacy-speakerdeck-short-code.ts @@ -8,7 +8,7 @@ import markdownItRegex from 'markdown-it-regex' import type MarkdownIt from 'markdown-it/lib' import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface' -const finalRegex = /^{%speakerdeck (\w+\/[\w-]+) ?%}$/ +export const legacySpeakerdeckRegex = /^{%speakerdeck (\w+\/[\w-]+) ?%}$/ /** * Configure the given {@link MarkdownIt} to render legacy hedgedoc 1 speakerdeck shortcodes as HTML links. @@ -18,7 +18,7 @@ const finalRegex = /^{%speakerdeck (\w+\/[\w-]+) ?%}$/ export const legacySpeakerdeckShortCode: MarkdownIt.PluginSimple = (markdownIt) => { markdownItRegex(markdownIt, { name: 'legacy-speakerdeck-short-code', - regex: finalRegex, + regex: legacySpeakerdeckRegex, replace: (match) => `https://speakerdeck.com/${match}` } as RegexOptions) } diff --git a/src/components/markdown-renderer/markdown-extension/markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/markdown-extension.ts index 0f3cd53e3..85149bd6d 100644 --- a/src/components/markdown-renderer/markdown-extension/markdown-extension.ts +++ b/src/components/markdown-renderer/markdown-extension/markdown-extension.ts @@ -7,6 +7,7 @@ import type MarkdownIt from 'markdown-it' import type { NodeProcessor } from '../node-preprocessors/node-processor' import type { ComponentReplacer } from '../replace-components/component-replacer' +import type { Linter } from '../../editor-page/editor-pane/linter/linter' /** * Base class for Markdown extensions. @@ -33,4 +34,8 @@ export abstract class MarkdownExtension { public buildTagNameWhitelist(): string[] { return [] } + + public buildLinter(): Linter[] { + return [] + } } diff --git a/src/components/markdown-renderer/markdown-extension/sequence-diagram/deprecation-warning.tsx b/src/components/markdown-renderer/markdown-extension/sequence-diagram/deprecation-warning.tsx deleted file mode 100644 index 5540ab529..000000000 --- a/src/components/markdown-renderer/markdown-extension/sequence-diagram/deprecation-warning.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) - * - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import React from 'react' -import { Alert } from 'react-bootstrap' -import { Trans, useTranslation } from 'react-i18next' -import links from '../../../../links.json' -import { cypressId } from '../../../../utils/cypress-attribute' -import { TranslatedExternalLink } from '../../../common/links/translated-external-link' - -/** - * Renders a deprecation warning. - */ -export const DeprecationWarning: React.FC = () => { - useTranslation() - - return ( - - - - -
- -
- ) -} diff --git a/src/components/markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram-markdown-extension.ts index 9ee184cbc..a4846774c 100644 --- a/src/components/markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram-markdown-extension.ts +++ b/src/components/markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram-markdown-extension.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,6 +8,9 @@ import { CodeBlockComponentReplacer } from '../../replace-components/code-block- import type { ComponentReplacer } from '../../replace-components/component-replacer' import { SequenceDiagram } from './sequence-diagram' import { CodeBlockMarkdownExtension } from '../code-block-markdown-extension/code-block-markdown-extension' +import type { Linter } from '../../../editor-page/editor-pane/linter/linter' +import { SingleLineRegexLinter } from '../../../editor-page/editor-pane/linter/single-line-regex-linter' +import { t } from 'i18next' /** * Adds legacy support for sequence diagram to the markdown rendering using code fences with "sequence" as language. @@ -16,4 +19,8 @@ export class SequenceDiagramMarkdownExtension extends CodeBlockMarkdownExtension public buildReplacers(): ComponentReplacer[] { return [new CodeBlockComponentReplacer(SequenceDiagram, 'sequence')] } + + public buildLinter(): Linter[] { + return [new SingleLineRegexLinter(/```sequence/, t('editor.linter.sequence'), () => '```mermaid\nsequenceDiagram')] + } } diff --git a/src/components/markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram.tsx b/src/components/markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram.tsx index 08dffd1f0..08f1a9d64 100644 --- a/src/components/markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram.tsx +++ b/src/components/markdown-renderer/markdown-extension/sequence-diagram/sequence-diagram.tsx @@ -1,13 +1,12 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ -import React, { Fragment } from 'react' +import React from 'react' import type { CodeProps } from '../../replace-components/code-block-component-replacer' import { MermaidChart } from '../mermaid/mermaid-chart' -import { DeprecationWarning } from './deprecation-warning' /** * Renders a sequence diagram with a deprecation notice. @@ -15,10 +14,5 @@ import { DeprecationWarning } from './deprecation-warning' * @param code the sequence diagram code */ export const SequenceDiagram: React.FC = ({ code }) => { - return ( - - - - - ) + return } diff --git a/src/components/markdown-renderer/markdown-extension/vimeo/replace-legacy-vimeo-short-code.ts b/src/components/markdown-renderer/markdown-extension/vimeo/replace-legacy-vimeo-short-code.ts index c2b744fce..854161941 100644 --- a/src/components/markdown-renderer/markdown-extension/vimeo/replace-legacy-vimeo-short-code.ts +++ b/src/components/markdown-renderer/markdown-extension/vimeo/replace-legacy-vimeo-short-code.ts @@ -9,6 +9,8 @@ import { VimeoMarkdownExtension } from './vimeo-markdown-extension' import type MarkdownIt from 'markdown-it' import markdownItRegex from 'markdown-it-regex' +export const legacyVimeoRegex = /^{%vimeo ([\d]{6,11}) ?%}$/ + /** * Configure the given {@link MarkdownIt} to render legacy hedgedoc 1 vimeo short codes as embeddings. * @@ -16,7 +18,7 @@ import markdownItRegex from 'markdown-it-regex' */ const replaceLegacyVimeoShortCode: RegexOptions = { name: 'legacy-vimeo-short-code', - regex: /^{%vimeo ([\d]{6,11}) ?%}$/, + regex: legacyVimeoRegex, replace: (match) => { // ESLint wants to collapse this tag, but then the tag won't be valid html anymore. // noinspection CheckTagEmptyBody diff --git a/src/components/markdown-renderer/markdown-extension/vimeo/vimeo-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/vimeo/vimeo-markdown-extension.ts index 90244bae8..6b6efd7ee 100644 --- a/src/components/markdown-renderer/markdown-extension/vimeo/vimeo-markdown-extension.ts +++ b/src/components/markdown-renderer/markdown-extension/vimeo/vimeo-markdown-extension.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,7 +10,10 @@ import type { ComponentReplacer } from '../../replace-components/component-repla import { CustomTagWithIdComponentReplacer } from '../../replace-components/custom-tag-with-id-component-replacer' import { replaceVimeoLinkMarkdownItPlugin } from './replace-vimeo-link' import { VimeoFrame } from './vimeo-frame' -import { replaceLegacyVimeoShortCodeMarkdownItPlugin } from './replace-legacy-vimeo-short-code' +import { legacyVimeoRegex, replaceLegacyVimeoShortCodeMarkdownItPlugin } from './replace-legacy-vimeo-short-code' +import type { Linter } from '../../../editor-page/editor-pane/linter/linter' +import { SingleLineRegexLinter } from '../../../editor-page/editor-pane/linter/single-line-regex-linter' +import { t } from 'i18next' /** * Adds vimeo video embeddings using link detection and the legacy vimeo short code syntax. @@ -30,4 +33,14 @@ export class VimeoMarkdownExtension extends MarkdownExtension { public buildTagNameWhitelist(): string[] { return [VimeoMarkdownExtension.tagName] } + + public buildLinter(): Linter[] { + return [ + new SingleLineRegexLinter( + legacyVimeoRegex, + t('editor.linter.shortcode', { shortcode: 'Vimeo' }), + (match: string) => `https://player.vimeo.com/video/${match}` + ) + ] + } } diff --git a/src/components/markdown-renderer/markdown-extension/youtube/replace-legacy-youtube-short-code.ts b/src/components/markdown-renderer/markdown-extension/youtube/replace-legacy-youtube-short-code.ts index bdc98656f..c2ad55bf3 100644 --- a/src/components/markdown-renderer/markdown-extension/youtube/replace-legacy-youtube-short-code.ts +++ b/src/components/markdown-renderer/markdown-extension/youtube/replace-legacy-youtube-short-code.ts @@ -9,6 +9,8 @@ import { YoutubeMarkdownExtension } from './youtube-markdown-extension' import markdownItRegex from 'markdown-it-regex' import type MarkdownIt from 'markdown-it' +export const legacyYouTubeRegex = /^{%youtube ([^"&?\\/\s]{11}) ?%}$/ + /** * Configure the given {@link MarkdownIt} to render legacy hedgedoc 1 youtube short codes as embeddings. * @@ -17,7 +19,7 @@ import type MarkdownIt from 'markdown-it' export const replaceLegacyYoutubeShortCodeMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt): void => markdownItRegex(markdownIt, { name: 'legacy-youtube-short-code', - regex: /^{%youtube ([^"&?\\/\s]{11}) ?%}$/, + regex: legacyYouTubeRegex, replace: (match) => { // ESLint wants to collapse this tag, but then the tag won't be valid html anymore. // noinspection CheckTagEmptyBody diff --git a/src/components/markdown-renderer/markdown-extension/youtube/youtube-markdown-extension.ts b/src/components/markdown-renderer/markdown-extension/youtube/youtube-markdown-extension.ts index 09b77db47..f619c7c7f 100644 --- a/src/components/markdown-renderer/markdown-extension/youtube/youtube-markdown-extension.ts +++ b/src/components/markdown-renderer/markdown-extension/youtube/youtube-markdown-extension.ts @@ -1,16 +1,19 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ import { MarkdownExtension } from '../markdown-extension' import { replaceYouTubeLinkMarkdownItPlugin } from './replace-youtube-link' -import { replaceLegacyYoutubeShortCodeMarkdownItPlugin } from './replace-legacy-youtube-short-code' +import { legacyYouTubeRegex, replaceLegacyYoutubeShortCodeMarkdownItPlugin } from './replace-legacy-youtube-short-code' import type MarkdownIt from 'markdown-it' import type { ComponentReplacer } from '../../replace-components/component-replacer' import { CustomTagWithIdComponentReplacer } from '../../replace-components/custom-tag-with-id-component-replacer' import { YouTubeFrame } from './youtube-frame' +import type { Linter } from '../../../editor-page/editor-pane/linter/linter' +import { SingleLineRegexLinter } from '../../../editor-page/editor-pane/linter/single-line-regex-linter' +import { t } from 'i18next' /** * Adds YouTube video embeddings using link detection and the legacy YouTube short code syntax. @@ -30,4 +33,14 @@ export class YoutubeMarkdownExtension extends MarkdownExtension { public buildTagNameWhitelist(): string[] { return [YoutubeMarkdownExtension.tagName] } + + public buildLinter(): Linter[] { + return [ + new SingleLineRegexLinter( + legacyYouTubeRegex, + t('editor.linter.shortcode', { shortcode: 'YouTube' }), + (match: string) => `https://www.youtube.com/watch?v=${match}` + ) + ] + } } diff --git a/src/components/render-page/markdown-document.tsx b/src/components/render-page/markdown-document.tsx index 615f8633d..f28771f49 100644 --- a/src/components/render-page/markdown-document.tsx +++ b/src/components/render-page/markdown-document.tsx @@ -8,7 +8,6 @@ import type { TocAst } from 'markdown-it-toc-done-right' import type { MutableRefObject } from 'react' import React, { useEffect, useMemo, useRef, useState } from 'react' import useResizeObserver from '@react-hook/resize-observer' -import { YamlArrayDeprecationAlert } from '../editor-page/renderer-pane/yaml-array-deprecation-alert' import { useDocumentSyncScrolling } from './hooks/sync-scroll/use-document-sync-scrolling' import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props' import { DocumentMarkdownRenderer } from '../markdown-renderer/document-markdown-renderer' @@ -17,7 +16,6 @@ import styles from './markdown-document.module.scss' import { WidthBasedTableOfContents } from './width-based-table-of-contents' import { ShowIf } from '../common/show-if/show-if' import { useApplicationState } from '../../hooks/common/use-application-state' -import { InvalidYamlAlert } from '../markdown-renderer/invalid-yaml-alert' import type { RendererFrontmatterInfo } from '../../redux/note-details/types/note-details' export interface RendererProps extends ScrollProps { @@ -106,8 +104,6 @@ export const MarkdownDocument: React.FC = ({ onTouchStart={onMakeScrollSource}>
- - { it('should parse the deprecated tags syntax', () => { const noteFrontmatter = createNoteFrontmatterFromYaml('tags: test123, abc') expect(noteFrontmatter.tags).toEqual(['test123', 'abc']) - expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(true) }) it('should parse the tags list syntax', () => { @@ -29,13 +28,11 @@ describe('yaml frontmatter', () => { - abc `) expect(noteFrontmatter.tags).toEqual(['test123', 'abc']) - expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(false) }) it('should parse the tag inline-list syntax', () => { const noteFrontmatter = createNoteFrontmatterFromYaml("tags: ['test123', 'abc']") expect(noteFrontmatter.tags).toEqual(['test123', 'abc']) - expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(false) }) it('should parse "breaks"', () => { diff --git a/src/redux/note-details/raw-note-frontmatter-parser/parser.ts b/src/redux/note-details/raw-note-frontmatter-parser/parser.ts index 41305e51e..8ba43f80c 100644 --- a/src/redux/note-details/raw-note-frontmatter-parser/parser.ts +++ b/src/redux/note-details/raw-note-frontmatter-parser/parser.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -29,16 +29,12 @@ export const createNoteFrontmatterFromYaml = (rawYaml: string): NoteFrontmatter */ const parseRawNoteFrontmatter = (rawData: RawNoteFrontmatter): NoteFrontmatter => { let tags: string[] - let deprecatedTagsSyntax: boolean if (typeof rawData?.tags === 'string') { tags = rawData?.tags?.split(',').map((entry) => entry.trim()) ?? [] - deprecatedTagsSyntax = true } else if (typeof rawData?.tags === 'object') { tags = rawData?.tags?.filter((tag) => tag !== null) ?? [] - deprecatedTagsSyntax = false } else { tags = [...initialState.frontmatter.tags] - deprecatedTagsSyntax = false } return { @@ -53,8 +49,7 @@ const parseRawNoteFrontmatter = (rawData: RawNoteFrontmatter): NoteFrontmatter = dir: parseTextDirection(rawData), opengraph: parseOpenGraph(rawData), slideOptions: parseSlideOptions(rawData), - tags, - deprecatedTagsSyntax + tags } } diff --git a/src/redux/note-details/raw-note-frontmatter-parser/types.d.ts b/src/redux/note-details/raw-note-frontmatter-parser/types.d.ts index 731199d92..d0c485de7 100644 --- a/src/redux/note-details/raw-note-frontmatter-parser/types.d.ts +++ b/src/redux/note-details/raw-note-frontmatter-parser/types.d.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -7,7 +7,7 @@ export interface RawNoteFrontmatter { title: string | undefined description: string | undefined - tags: string | string[] | undefined + tags: string | number | string[] | undefined robots: string | undefined lang: string | undefined dir: string | undefined diff --git a/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.test.ts b/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.test.ts index 148b18be1..7b4c5a647 100644 --- a/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.test.ts +++ b/src/redux/note-details/reducers/build-state-from-set-note-data-from-server.test.ts @@ -81,7 +81,6 @@ describe('build state from set note data from server', () => { title: '', description: '', tags: [], - deprecatedTagsSyntax: false, robots: '', lang: 'en', dir: NoteTextDirection.LTR, @@ -100,7 +99,6 @@ describe('build state from set note data from server', () => { }, frontmatterRendererInfo: { frontmatterInvalid: false, - deprecatedSyntax: false, lineOffset: 0, slideOptions: initialSlideOptions }, diff --git a/src/redux/note-details/types/note-details.ts b/src/redux/note-details/types/note-details.ts index 8ceedf2c0..f6a5157d8 100644 --- a/src/redux/note-details/types/note-details.ts +++ b/src/redux/note-details/types/note-details.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) * * SPDX-License-Identifier: AGPL-3.0-only */ @@ -37,7 +37,6 @@ export interface NoteFrontmatter { title: string description: string tags: string[] - deprecatedTagsSyntax: boolean robots: string lang: Iso6391Language dir: NoteTextDirection @@ -62,6 +61,5 @@ export enum NoteType { export interface RendererFrontmatterInfo { lineOffset: number frontmatterInvalid: boolean - deprecatedSyntax: boolean slideOptions: SlideOptions } diff --git a/yarn.lock b/yarn.lock index 21e3a21fd..b021c2c01 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1655,7 +1655,7 @@ __metadata: languageName: node linkType: hard -"@codemirror/lint@npm:^6.0.0": +"@codemirror/lint@npm:6.0.0, @codemirror/lint@npm:^6.0.0": version: 6.0.0 resolution: "@codemirror/lint@npm:6.0.0" dependencies: @@ -1902,6 +1902,7 @@ __metadata: "@codemirror/lang-markdown": 6.0.1 "@codemirror/language": 6.2.1 "@codemirror/language-data": 6.1.0 + "@codemirror/lint": 6.0.0 "@codemirror/state": 6.1.0 "@codemirror/theme-one-dark": 6.0.0 "@codemirror/view": 6.1.2