mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-22 01:36:29 -05:00
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:
parent
57cc08739d
commit
1bd18cc0ee
33 changed files with 471 additions and 182 deletions
|
@ -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')
|
|
||||||
})
|
|
||||||
})
|
|
|
@ -25,9 +25,6 @@
|
||||||
"mermaid": {
|
"mermaid": {
|
||||||
"unknownError": "Unknown rendering error. Please check your browser console."
|
"unknownError": "Unknown rendering error. Please check your browser console."
|
||||||
},
|
},
|
||||||
"sequence": {
|
|
||||||
"deprecationWarning": "The use of 'sequence' as code block language is deprecated."
|
|
||||||
},
|
|
||||||
"vega-lite": {
|
"vega-lite": {
|
||||||
"png": "Save as PNG",
|
"png": "Save as PNG",
|
||||||
"svg": "Save as SVG",
|
"svg": "Save as SVG",
|
||||||
|
@ -228,6 +225,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"editor": {
|
"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": {
|
"upload": {
|
||||||
"uploadFile": {
|
"uploadFile": {
|
||||||
"withoutDescription": "Uploading file {{fileName}}",
|
"withoutDescription": "Uploading file {{fileName}}",
|
||||||
|
@ -238,8 +242,6 @@
|
||||||
},
|
},
|
||||||
"untitledNote": "Untitled",
|
"untitledNote": "Untitled",
|
||||||
"placeholder": "← Start by entering a title here\n===\nVisit the features page if you don't know what to do.\nHappy hacking :)",
|
"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.",
|
"infoToc": "Structure your note with headings to see a table-of-contents here.",
|
||||||
"help": {
|
"help": {
|
||||||
"shortcuts": {
|
"shortcuts": {
|
||||||
|
|
|
@ -44,6 +44,7 @@
|
||||||
"@codemirror/lang-markdown": "6.0.1",
|
"@codemirror/lang-markdown": "6.0.1",
|
||||||
"@codemirror/language": "6.2.1",
|
"@codemirror/language": "6.2.1",
|
||||||
"@codemirror/language-data": "6.1.0",
|
"@codemirror/language-data": "6.1.0",
|
||||||
|
"@codemirror/lint": "6.0.0",
|
||||||
"@codemirror/state": "6.1.0",
|
"@codemirror/state": "6.1.0",
|
||||||
"@codemirror/theme-one-dark": "6.0.0",
|
"@codemirror/theme-one-dark": "6.0.0",
|
||||||
"@codemirror/view": "6.1.2",
|
"@codemirror/view": "6.1.2",
|
||||||
|
|
|
@ -36,6 +36,13 @@ import { useInsertNoteContentIntoYTextInMockModeEffect } from './hooks/yjs/use-i
|
||||||
import { useOnFirstEditorUpdateExtension } from './hooks/yjs/use-on-first-editor-update-extension'
|
import { useOnFirstEditorUpdateExtension } from './hooks/yjs/use-on-first-editor-update-extension'
|
||||||
import { useIsConnectionSynced } from './hooks/yjs/use-is-connection-synced'
|
import { useIsConnectionSynced } from './hooks/yjs/use-is-connection-synced'
|
||||||
import { useMarkdownContentYText } from './hooks/yjs/use-markdown-content-y-text'
|
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.
|
* 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()
|
const [firstEditorUpdateExtension, firstUpdateHappened] = useOnFirstEditorUpdateExtension()
|
||||||
useInsertNoteContentIntoYTextInMockModeEffect(firstUpdateHappened, websocketConnection)
|
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(
|
const extensions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
linter,
|
||||||
|
lintGutter(),
|
||||||
markdown({
|
markdown({
|
||||||
base: markdownLanguage,
|
base: markdownLanguage,
|
||||||
codeLanguages: (input) => findLanguageByCodeBlockName(languages, input)
|
codeLanguages: (input) => findLanguageByCodeBlockName(languages, input)
|
||||||
|
@ -95,6 +117,7 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
|
||||||
firstEditorUpdateExtension
|
firstEditorUpdateExtension
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
linter,
|
||||||
editorScrollExtension,
|
editorScrollExtension,
|
||||||
tablePasteExtensions,
|
tablePasteExtensions,
|
||||||
fileInsertExtension,
|
fileInsertExtension,
|
||||||
|
|
|
@ -21,6 +21,11 @@
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cm-diagnostic {
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
//workarounds for line break problem.. see https://github.com/yjs/y-codemirror.next/pull/12
|
//workarounds for line break problem.. see https://github.com/yjs/y-codemirror.next/pull/12
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
src/components/editor-page/editor-pane/linter/linter.ts
Normal file
30
src/components/editor-page/editor-pane/linter/linter.ts
Normal 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])
|
||||||
|
}
|
|
@ -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', [])
|
||||||
|
})
|
||||||
|
})
|
|
@ -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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MarkdownExtension } from '../markdown-extension'
|
import { MarkdownExtension } from '../markdown-extension'
|
||||||
import type MarkdownIt from 'markdown-it'
|
import type MarkdownIt from 'markdown-it'
|
||||||
import { legacyPdfShortCode } from './replace-legacy-pdf-short-code'
|
import { legacyPdfRegex, legacyPdfShortCode } from './replace-legacy-pdf-short-code'
|
||||||
import { legacySlideshareShortCode } from './replace-legacy-slideshare-short-code'
|
import { legacySlideshareRegex, legacySlideshareShortCode } from './replace-legacy-slideshare-short-code'
|
||||||
import { legacySpeakerdeckShortCode } from './replace-legacy-speakerdeck-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.
|
* 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)
|
legacySlideshareShortCode(markdownIt)
|
||||||
legacySpeakerdeckShortCode(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
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import markdownItRegex from 'markdown-it-regex'
|
||||||
import type MarkdownIt from 'markdown-it/lib'
|
import type MarkdownIt from 'markdown-it/lib'
|
||||||
import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
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.
|
* 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) => {
|
export const legacyPdfShortCode: MarkdownIt.PluginSimple = (markdownIt) => {
|
||||||
markdownItRegex(markdownIt, {
|
markdownItRegex(markdownIt, {
|
||||||
name: 'legacy-pdf-short-code',
|
name: 'legacy-pdf-short-code',
|
||||||
regex: finalRegex,
|
regex: legacyPdfRegex,
|
||||||
replace: (match) => `<a href="${match}">${match}</a>`
|
replace: (match) => `<a href="${match}">${match}</a>`
|
||||||
} as RegexOptions)
|
} as RegexOptions)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import markdownItRegex from 'markdown-it-regex'
|
||||||
import type MarkdownIt from 'markdown-it/lib'
|
import type MarkdownIt from 'markdown-it/lib'
|
||||||
import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
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.
|
* 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) => {
|
export const legacySlideshareShortCode: MarkdownIt.PluginSimple = (markdownIt) => {
|
||||||
markdownItRegex(markdownIt, {
|
markdownItRegex(markdownIt, {
|
||||||
name: 'legacy-slideshare-short-code',
|
name: 'legacy-slideshare-short-code',
|
||||||
regex: finalRegex,
|
regex: legacySlideshareRegex,
|
||||||
replace: (match) => `<a href='https://www.slideshare.net/${match}'>https://www.slideshare.net/${match}</a>`
|
replace: (match) => `<a href='https://www.slideshare.net/${match}'>https://www.slideshare.net/${match}</a>`
|
||||||
} as RegexOptions)
|
} as RegexOptions)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import markdownItRegex from 'markdown-it-regex'
|
||||||
import type MarkdownIt from 'markdown-it/lib'
|
import type MarkdownIt from 'markdown-it/lib'
|
||||||
import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
|
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.
|
* 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) => {
|
export const legacySpeakerdeckShortCode: MarkdownIt.PluginSimple = (markdownIt) => {
|
||||||
markdownItRegex(markdownIt, {
|
markdownItRegex(markdownIt, {
|
||||||
name: 'legacy-speakerdeck-short-code',
|
name: 'legacy-speakerdeck-short-code',
|
||||||
regex: finalRegex,
|
regex: legacySpeakerdeckRegex,
|
||||||
replace: (match) => `<a href="https://speakerdeck.com/${match}">https://speakerdeck.com/${match}</a>`
|
replace: (match) => `<a href="https://speakerdeck.com/${match}">https://speakerdeck.com/${match}</a>`
|
||||||
} as RegexOptions)
|
} as RegexOptions)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
import type MarkdownIt from 'markdown-it'
|
import type MarkdownIt from 'markdown-it'
|
||||||
import type { NodeProcessor } from '../node-preprocessors/node-processor'
|
import type { NodeProcessor } from '../node-preprocessors/node-processor'
|
||||||
import type { ComponentReplacer } from '../replace-components/component-replacer'
|
import type { ComponentReplacer } from '../replace-components/component-replacer'
|
||||||
|
import type { Linter } from '../../editor-page/editor-pane/linter/linter'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for Markdown extensions.
|
* Base class for Markdown extensions.
|
||||||
|
@ -33,4 +34,8 @@ export abstract class MarkdownExtension {
|
||||||
public buildTagNameWhitelist(): string[] {
|
public buildTagNameWhitelist(): string[] {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public buildLinter(): Linter[] {
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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
|
* 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 type { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||||
import { SequenceDiagram } from './sequence-diagram'
|
import { SequenceDiagram } from './sequence-diagram'
|
||||||
import { CodeBlockMarkdownExtension } from '../code-block-markdown-extension/code-block-markdown-extension'
|
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.
|
* 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[] {
|
public buildReplacers(): ComponentReplacer[] {
|
||||||
return [new CodeBlockComponentReplacer(SequenceDiagram, 'sequence')]
|
return [new CodeBlockComponentReplacer(SequenceDiagram, 'sequence')]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public buildLinter(): Linter[] {
|
||||||
|
return [new SingleLineRegexLinter(/```sequence/, t('editor.linter.sequence'), () => '```mermaid\nsequenceDiagram')]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* 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 type { CodeProps } from '../../replace-components/code-block-component-replacer'
|
||||||
import { MermaidChart } from '../mermaid/mermaid-chart'
|
import { MermaidChart } from '../mermaid/mermaid-chart'
|
||||||
import { DeprecationWarning } from './deprecation-warning'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders a sequence diagram with a deprecation notice.
|
* Renders a sequence diagram with a deprecation notice.
|
||||||
|
@ -15,10 +14,5 @@ import { DeprecationWarning } from './deprecation-warning'
|
||||||
* @param code the sequence diagram code
|
* @param code the sequence diagram code
|
||||||
*/
|
*/
|
||||||
export const SequenceDiagram: React.FC<CodeProps> = ({ code }) => {
|
export const SequenceDiagram: React.FC<CodeProps> = ({ code }) => {
|
||||||
return (
|
return <MermaidChart code={'sequenceDiagram\n' + code} />
|
||||||
<Fragment>
|
|
||||||
<DeprecationWarning />
|
|
||||||
<MermaidChart code={'sequenceDiagram\n' + code} />
|
|
||||||
</Fragment>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { VimeoMarkdownExtension } from './vimeo-markdown-extension'
|
||||||
import type MarkdownIt from 'markdown-it'
|
import type MarkdownIt from 'markdown-it'
|
||||||
import markdownItRegex from 'markdown-it-regex'
|
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.
|
* 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 = {
|
const replaceLegacyVimeoShortCode: RegexOptions = {
|
||||||
name: 'legacy-vimeo-short-code',
|
name: 'legacy-vimeo-short-code',
|
||||||
regex: /^{%vimeo ([\d]{6,11}) ?%}$/,
|
regex: legacyVimeoRegex,
|
||||||
replace: (match) => {
|
replace: (match) => {
|
||||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||||
// noinspection CheckTagEmptyBody
|
// noinspection CheckTagEmptyBody
|
||||||
|
|
|
@ -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
|
* 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 { CustomTagWithIdComponentReplacer } from '../../replace-components/custom-tag-with-id-component-replacer'
|
||||||
import { replaceVimeoLinkMarkdownItPlugin } from './replace-vimeo-link'
|
import { replaceVimeoLinkMarkdownItPlugin } from './replace-vimeo-link'
|
||||||
import { VimeoFrame } from './vimeo-frame'
|
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.
|
* 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[] {
|
public buildTagNameWhitelist(): string[] {
|
||||||
return [VimeoMarkdownExtension.tagName]
|
return [VimeoMarkdownExtension.tagName]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public buildLinter(): Linter[] {
|
||||||
|
return [
|
||||||
|
new SingleLineRegexLinter(
|
||||||
|
legacyVimeoRegex,
|
||||||
|
t('editor.linter.shortcode', { shortcode: 'Vimeo' }),
|
||||||
|
(match: string) => `https://player.vimeo.com/video/${match}`
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,8 @@ import { YoutubeMarkdownExtension } from './youtube-markdown-extension'
|
||||||
import markdownItRegex from 'markdown-it-regex'
|
import markdownItRegex from 'markdown-it-regex'
|
||||||
import type MarkdownIt from 'markdown-it'
|
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.
|
* 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 =>
|
export const replaceLegacyYoutubeShortCodeMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt): void =>
|
||||||
markdownItRegex(markdownIt, {
|
markdownItRegex(markdownIt, {
|
||||||
name: 'legacy-youtube-short-code',
|
name: 'legacy-youtube-short-code',
|
||||||
regex: /^{%youtube ([^"&?\\/\s]{11}) ?%}$/,
|
regex: legacyYouTubeRegex,
|
||||||
replace: (match) => {
|
replace: (match) => {
|
||||||
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
|
||||||
// noinspection CheckTagEmptyBody
|
// noinspection CheckTagEmptyBody
|
||||||
|
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { MarkdownExtension } from '../markdown-extension'
|
import { MarkdownExtension } from '../markdown-extension'
|
||||||
import { replaceYouTubeLinkMarkdownItPlugin } from './replace-youtube-link'
|
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 MarkdownIt from 'markdown-it'
|
||||||
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
import type { ComponentReplacer } from '../../replace-components/component-replacer'
|
||||||
import { CustomTagWithIdComponentReplacer } from '../../replace-components/custom-tag-with-id-component-replacer'
|
import { CustomTagWithIdComponentReplacer } from '../../replace-components/custom-tag-with-id-component-replacer'
|
||||||
import { YouTubeFrame } from './youtube-frame'
|
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.
|
* 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[] {
|
public buildTagNameWhitelist(): string[] {
|
||||||
return [YoutubeMarkdownExtension.tagName]
|
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}`
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import type { TocAst } from 'markdown-it-toc-done-right'
|
||||||
import type { MutableRefObject } from 'react'
|
import type { MutableRefObject } from 'react'
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import useResizeObserver from '@react-hook/resize-observer'
|
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 { useDocumentSyncScrolling } from './hooks/sync-scroll/use-document-sync-scrolling'
|
||||||
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
import type { ScrollProps } from '../editor-page/synced-scroll/scroll-props'
|
||||||
import { DocumentMarkdownRenderer } from '../markdown-renderer/document-markdown-renderer'
|
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 { WidthBasedTableOfContents } from './width-based-table-of-contents'
|
||||||
import { ShowIf } from '../common/show-if/show-if'
|
import { ShowIf } from '../common/show-if/show-if'
|
||||||
import { useApplicationState } from '../../hooks/common/use-application-state'
|
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'
|
import type { RendererFrontmatterInfo } from '../../redux/note-details/types/note-details'
|
||||||
|
|
||||||
export interface RendererProps extends ScrollProps {
|
export interface RendererProps extends ScrollProps {
|
||||||
|
@ -106,8 +104,6 @@ export const MarkdownDocument: React.FC<MarkdownDocumentProps> = ({
|
||||||
onTouchStart={onMakeScrollSource}>
|
onTouchStart={onMakeScrollSource}>
|
||||||
<div className={styles['markdown-document-side']} />
|
<div className={styles['markdown-document-side']} />
|
||||||
<div className={styles['markdown-document-content']}>
|
<div className={styles['markdown-document-content']}>
|
||||||
<InvalidYamlAlert show={!!frontmatterInfo?.frontmatterInvalid} />
|
|
||||||
<YamlArrayDeprecationAlert show={!!frontmatterInfo?.deprecatedSyntax} />
|
|
||||||
<DocumentMarkdownRenderer
|
<DocumentMarkdownRenderer
|
||||||
outerContainerRef={rendererRef}
|
outerContainerRef={rendererRef}
|
||||||
className={`mb-3 ${additionalRendererClasses ?? ''}`}
|
className={`mb-3 ${additionalRendererClasses ?? ''}`}
|
||||||
|
|
|
@ -92,7 +92,6 @@ const buildStateFromFrontmatterUpdate = (
|
||||||
title: generateNoteTitle(frontmatter, state.firstHeading),
|
title: generateNoteTitle(frontmatter, state.firstHeading),
|
||||||
frontmatterRendererInfo: {
|
frontmatterRendererInfo: {
|
||||||
lineOffset: frontmatterExtraction.lineOffset,
|
lineOffset: frontmatterExtraction.lineOffset,
|
||||||
deprecatedSyntax: frontmatter.deprecatedTagsSyntax,
|
|
||||||
frontmatterInvalid: false,
|
frontmatterInvalid: false,
|
||||||
slideOptions: frontmatter.slideOptions
|
slideOptions: frontmatter.slideOptions
|
||||||
}
|
}
|
||||||
|
@ -105,7 +104,6 @@ const buildStateFromFrontmatterUpdate = (
|
||||||
frontmatter: initialState.frontmatter,
|
frontmatter: initialState.frontmatter,
|
||||||
frontmatterRendererInfo: {
|
frontmatterRendererInfo: {
|
||||||
lineOffset: frontmatterExtraction.lineOffset,
|
lineOffset: frontmatterExtraction.lineOffset,
|
||||||
deprecatedSyntax: false,
|
|
||||||
frontmatterInvalid: true,
|
frontmatterInvalid: true,
|
||||||
slideOptions: initialState.frontmatterRendererInfo.slideOptions
|
slideOptions: initialState.frontmatterRendererInfo.slideOptions
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -28,7 +28,6 @@ export const initialState: NoteDetails = {
|
||||||
rawFrontmatter: '',
|
rawFrontmatter: '',
|
||||||
frontmatterRendererInfo: {
|
frontmatterRendererInfo: {
|
||||||
frontmatterInvalid: false,
|
frontmatterInvalid: false,
|
||||||
deprecatedSyntax: false,
|
|
||||||
lineOffset: 0,
|
lineOffset: 0,
|
||||||
slideOptions: initialSlideOptions
|
slideOptions: initialSlideOptions
|
||||||
},
|
},
|
||||||
|
@ -50,7 +49,6 @@ export const initialState: NoteDetails = {
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
deprecatedTagsSyntax: false,
|
|
||||||
robots: '',
|
robots: '',
|
||||||
lang: 'en',
|
lang: 'en',
|
||||||
dir: NoteTextDirection.LTR,
|
dir: NoteTextDirection.LTR,
|
||||||
|
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -20,7 +20,6 @@ describe('yaml frontmatter', () => {
|
||||||
it('should parse the deprecated tags syntax', () => {
|
it('should parse the deprecated tags syntax', () => {
|
||||||
const noteFrontmatter = createNoteFrontmatterFromYaml('tags: test123, abc')
|
const noteFrontmatter = createNoteFrontmatterFromYaml('tags: test123, abc')
|
||||||
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
|
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
|
||||||
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should parse the tags list syntax', () => {
|
it('should parse the tags list syntax', () => {
|
||||||
|
@ -29,13 +28,11 @@ describe('yaml frontmatter', () => {
|
||||||
- abc
|
- abc
|
||||||
`)
|
`)
|
||||||
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
|
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
|
||||||
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should parse the tag inline-list syntax', () => {
|
it('should parse the tag inline-list syntax', () => {
|
||||||
const noteFrontmatter = createNoteFrontmatterFromYaml("tags: ['test123', 'abc']")
|
const noteFrontmatter = createNoteFrontmatterFromYaml("tags: ['test123', 'abc']")
|
||||||
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
|
expect(noteFrontmatter.tags).toEqual(['test123', 'abc'])
|
||||||
expect(noteFrontmatter.deprecatedTagsSyntax).toEqual(false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should parse "breaks"', () => {
|
it('should parse "breaks"', () => {
|
||||||
|
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -29,16 +29,12 @@ export const createNoteFrontmatterFromYaml = (rawYaml: string): NoteFrontmatter
|
||||||
*/
|
*/
|
||||||
const parseRawNoteFrontmatter = (rawData: RawNoteFrontmatter): NoteFrontmatter => {
|
const parseRawNoteFrontmatter = (rawData: RawNoteFrontmatter): NoteFrontmatter => {
|
||||||
let tags: string[]
|
let tags: string[]
|
||||||
let deprecatedTagsSyntax: boolean
|
|
||||||
if (typeof rawData?.tags === 'string') {
|
if (typeof rawData?.tags === 'string') {
|
||||||
tags = rawData?.tags?.split(',').map((entry) => entry.trim()) ?? []
|
tags = rawData?.tags?.split(',').map((entry) => entry.trim()) ?? []
|
||||||
deprecatedTagsSyntax = true
|
|
||||||
} else if (typeof rawData?.tags === 'object') {
|
} else if (typeof rawData?.tags === 'object') {
|
||||||
tags = rawData?.tags?.filter((tag) => tag !== null) ?? []
|
tags = rawData?.tags?.filter((tag) => tag !== null) ?? []
|
||||||
deprecatedTagsSyntax = false
|
|
||||||
} else {
|
} else {
|
||||||
tags = [...initialState.frontmatter.tags]
|
tags = [...initialState.frontmatter.tags]
|
||||||
deprecatedTagsSyntax = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -53,8 +49,7 @@ const parseRawNoteFrontmatter = (rawData: RawNoteFrontmatter): NoteFrontmatter =
|
||||||
dir: parseTextDirection(rawData),
|
dir: parseTextDirection(rawData),
|
||||||
opengraph: parseOpenGraph(rawData),
|
opengraph: parseOpenGraph(rawData),
|
||||||
slideOptions: parseSlideOptions(rawData),
|
slideOptions: parseSlideOptions(rawData),
|
||||||
tags,
|
tags
|
||||||
deprecatedTagsSyntax
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -7,7 +7,7 @@
|
||||||
export interface RawNoteFrontmatter {
|
export interface RawNoteFrontmatter {
|
||||||
title: string | undefined
|
title: string | undefined
|
||||||
description: string | undefined
|
description: string | undefined
|
||||||
tags: string | string[] | undefined
|
tags: string | number | string[] | undefined
|
||||||
robots: string | undefined
|
robots: string | undefined
|
||||||
lang: string | undefined
|
lang: string | undefined
|
||||||
dir: string | undefined
|
dir: string | undefined
|
||||||
|
|
|
@ -81,7 +81,6 @@ describe('build state from set note data from server', () => {
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
deprecatedTagsSyntax: false,
|
|
||||||
robots: '',
|
robots: '',
|
||||||
lang: 'en',
|
lang: 'en',
|
||||||
dir: NoteTextDirection.LTR,
|
dir: NoteTextDirection.LTR,
|
||||||
|
@ -100,7 +99,6 @@ describe('build state from set note data from server', () => {
|
||||||
},
|
},
|
||||||
frontmatterRendererInfo: {
|
frontmatterRendererInfo: {
|
||||||
frontmatterInvalid: false,
|
frontmatterInvalid: false,
|
||||||
deprecatedSyntax: false,
|
|
||||||
lineOffset: 0,
|
lineOffset: 0,
|
||||||
slideOptions: initialSlideOptions
|
slideOptions: initialSlideOptions
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
@ -37,7 +37,6 @@ export interface NoteFrontmatter {
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
tags: string[]
|
tags: string[]
|
||||||
deprecatedTagsSyntax: boolean
|
|
||||||
robots: string
|
robots: string
|
||||||
lang: Iso6391Language
|
lang: Iso6391Language
|
||||||
dir: NoteTextDirection
|
dir: NoteTextDirection
|
||||||
|
@ -62,6 +61,5 @@ export enum NoteType {
|
||||||
export interface RendererFrontmatterInfo {
|
export interface RendererFrontmatterInfo {
|
||||||
lineOffset: number
|
lineOffset: number
|
||||||
frontmatterInvalid: boolean
|
frontmatterInvalid: boolean
|
||||||
deprecatedSyntax: boolean
|
|
||||||
slideOptions: SlideOptions
|
slideOptions: SlideOptions
|
||||||
}
|
}
|
||||||
|
|
|
@ -1655,7 +1655,7 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@codemirror/lint@npm:^6.0.0":
|
"@codemirror/lint@npm:6.0.0, @codemirror/lint@npm:^6.0.0":
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
resolution: "@codemirror/lint@npm:6.0.0"
|
resolution: "@codemirror/lint@npm:6.0.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -1902,6 +1902,7 @@ __metadata:
|
||||||
"@codemirror/lang-markdown": 6.0.1
|
"@codemirror/lang-markdown": 6.0.1
|
||||||
"@codemirror/language": 6.2.1
|
"@codemirror/language": 6.2.1
|
||||||
"@codemirror/language-data": 6.1.0
|
"@codemirror/language-data": 6.1.0
|
||||||
|
"@codemirror/lint": 6.0.0
|
||||||
"@codemirror/state": 6.1.0
|
"@codemirror/state": 6.1.0
|
||||||
"@codemirror/theme-one-dark": 6.0.0
|
"@codemirror/theme-one-dark": 6.0.0
|
||||||
"@codemirror/view": 6.1.2
|
"@codemirror/view": 6.1.2
|
||||||
|
|
Loading…
Reference in a new issue