feat: add linter and linterGutter (#2237)

Signed-off-by: Philip Molares <philip.molares@udo.edu>
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

Co-authored-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Philip Molares 2022-07-31 18:25:03 +02:00 committed by GitHub
parent 57cc08739d
commit 1bd18cc0ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 471 additions and 182 deletions

View file

@ -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')
})
})

View file

@ -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></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": {

View file

@ -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",

View file

@ -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<ScrollProps> = ({ 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<ScrollProps> = ({ scrollState, onScroll, onMak
firstEditorUpdateExtension
],
[
linter,
editorScrollExtension,
tablePasteExtensions,
fileInsertExtension,

View file

@ -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
}
}

View file

@ -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<Diagnostic>,
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'
})
})
})

View file

@ -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
}
}
}

View file

@ -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])
}

View file

@ -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<Text>()
docMock.toString = () => content
return Mock.of<EditorView>({
state: Mock.of<EditorState>({
doc: docMock
}),
dispatch: jest.fn()
})
}
const testSingleLineRegexLinter = (
regex: RegExp,
replace: (match: string) => string,
content: string,
expectedDiagnostics: Partial<Diagnostic>[]
): 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', [])
})
})

View file

@ -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'
}
}
}

View file

@ -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<Partial<CommonModalProps>> = ({ show }) => {
useTranslation()
return (
<ShowIf condition={!!show}>
<Alert {...cypressId('yamlArrayDeprecationAlert')} className={'text-wrap'} variant='warning' dir='auto'>
<span className={'text-wrap'}>
<span className={'text-wrap'}>
<Trans i18nKey='editor.deprecatedTags' />
</span>
</span>
<br />
<TranslatedExternalLink i18nKey={'common.readForMoreInfo'} href={links.faq} className={'text-primary'} />
</Alert>
</ShowIf>
)
}

View file

@ -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<SimpleAlertProps> = ({ show }) => {
useTranslation()
return (
<ShowIf condition={show}>
<Alert variant='warning' dir='auto'>
<Trans i18nKey='editor.invalidYaml'>
<InternalLink text='yaml-metadata' href='/n/yaml-metadata' className='text-primary' />
</Trans>
</Alert>
</ShowIf>
)
}

View file

@ -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
)
]
}
}

View file

@ -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) => `<a href="${match}">${match}</a>`
} as RegexOptions)
}

View file

@ -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) => `<a href='https://www.slideshare.net/${match}'>https://www.slideshare.net/${match}</a>`
} as RegexOptions)
}

View file

@ -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) => `<a href="https://speakerdeck.com/${match}">https://speakerdeck.com/${match}</a>`
} as RegexOptions)
}

View file

@ -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 []
}
}

View file

@ -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 (
<Alert {...cypressId('yaml')} className={'mt-2'} variant={'warning'}>
<span className={'text-wrap'}>
<Trans i18nKey={'renderer.sequence.deprecationWarning'} />
</span>
<br />
<TranslatedExternalLink i18nKey={'common.readForMoreInfo'} className={'text-primary'} href={links.faq} />
</Alert>
)
}

View file

@ -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')]
}
}

View file

@ -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<CodeProps> = ({ code }) => {
return (
<Fragment>
<DeprecationWarning />
<MermaidChart code={'sequenceDiagram\n' + code} />
</Fragment>
)
return <MermaidChart code={'sequenceDiagram\n' + code} />
}

View file

@ -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

View file

@ -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}`
)
]
}
}

View file

@ -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

View file

@ -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}`
)
]
}
}

View file

@ -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<MarkdownDocumentProps> = ({
onTouchStart={onMakeScrollSource}>
<div className={styles['markdown-document-side']} />
<div className={styles['markdown-document-content']}>
<InvalidYamlAlert show={!!frontmatterInfo?.frontmatterInvalid} />
<YamlArrayDeprecationAlert show={!!frontmatterInfo?.deprecatedSyntax} />
<DocumentMarkdownRenderer
outerContainerRef={rendererRef}
className={`mb-3 ${additionalRendererClasses ?? ''}`}

View file

@ -92,7 +92,6 @@ const buildStateFromFrontmatterUpdate = (
title: generateNoteTitle(frontmatter, state.firstHeading),
frontmatterRendererInfo: {
lineOffset: frontmatterExtraction.lineOffset,
deprecatedSyntax: frontmatter.deprecatedTagsSyntax,
frontmatterInvalid: false,
slideOptions: frontmatter.slideOptions
}
@ -105,7 +104,6 @@ const buildStateFromFrontmatterUpdate = (
frontmatter: initialState.frontmatter,
frontmatterRendererInfo: {
lineOffset: frontmatterExtraction.lineOffset,
deprecatedSyntax: false,
frontmatterInvalid: true,
slideOptions: initialState.frontmatterRendererInfo.slideOptions
}

View file

@ -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
*/
@ -28,7 +28,6 @@ export const initialState: NoteDetails = {
rawFrontmatter: '',
frontmatterRendererInfo: {
frontmatterInvalid: false,
deprecatedSyntax: false,
lineOffset: 0,
slideOptions: initialSlideOptions
},
@ -50,7 +49,6 @@ export const initialState: NoteDetails = {
title: '',
description: '',
tags: [],
deprecatedTagsSyntax: false,
robots: '',
lang: 'en',
dir: NoteTextDirection.LTR,

View file

@ -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
*/
@ -20,7 +20,6 @@ describe('yaml frontmatter', () => {
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"', () => {

View file

@ -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
}
}

View file

@ -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

View file

@ -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
},

View file

@ -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
}

View file

@ -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