diff --git a/src/components/editor-page/editor-pane/editor-pane.tsx b/src/components/editor-page/editor-pane/editor-pane.tsx index 5a7734945..f5bb0a520 100644 --- a/src/components/editor-page/editor-pane/editor-pane.tsx +++ b/src/components/editor-page/editor-pane/editor-pane.tsx @@ -25,12 +25,13 @@ import { useCodeMirrorScrollWatchExtension } from './hooks/code-mirror-extension import { useCodeMirrorPasteExtension } from './hooks/code-mirror-extensions/use-code-mirror-paste-extension' import { useCodeMirrorFileDropExtension } from './hooks/code-mirror-extensions/use-code-mirror-file-drop-extension' import { markdown, markdownLanguage } from '@codemirror/lang-markdown' -import { languages } from '@codemirror/language-data' import { EditorView } from '@codemirror/view' import { autocompletion } from '@codemirror/autocomplete' import { useCodeMirrorFocusReference } from './hooks/use-code-mirror-focus-reference' import { useOffScreenScrollProtection } from './hooks/use-off-screen-scroll-protection' import { cypressId } from '../../../utils/cypress-attribute' +import { findLanguageByCodeBlockName } from '../../markdown-renderer/markdown-extension/code-block-markdown-extension/find-language-by-code-block-name' +import { languages } from '@codemirror/language-data' const logger = new Logger('EditorPane') @@ -62,7 +63,10 @@ export const EditorPane: React.FC = ({ scrollState, onScroll, onMak const extensions = useMemo( () => [ - markdown({ base: markdownLanguage, codeLanguages: languages }), + markdown({ + base: markdownLanguage, + codeLanguages: (input) => findLanguageByCodeBlockName(languages, input) + }), ...saveOffFocusScrollStateExtensions, focusExtension, EditorView.lineWrapping, diff --git a/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/code-block-markdown-plugin.ts b/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/code-block-markdown-plugin.ts index 927b6ebed..5502c5dbd 100644 --- a/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/code-block-markdown-plugin.ts +++ b/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/code-block-markdown-plugin.ts @@ -6,9 +6,10 @@ import type MarkdownIt from 'markdown-it' import type { RuleCore } from 'markdown-it/lib/parser_core' +import Optional from 'optional-js' +import { parseCodeBlockParameters } from './code-block-parameters' const ruleName = 'code-highlighter' -const codeFenceArguments = /^ *([\w-]*)(.*)$/ /** * Extracts the language name and additional flags from the code fence parameter and sets them as attributes in the token. @@ -19,16 +20,13 @@ const codeFenceArguments = /^ *([\w-]*)(.*)$/ const rule: RuleCore = (state) => { state.tokens.forEach((token) => { if (token.type === 'fence') { - const highlightInfos = codeFenceArguments.exec(token.info) - if (!highlightInfos) { - return - } - if (highlightInfos[1]) { - token.attrJoin('data-highlight-language', highlightInfos[1]) - } - if (highlightInfos[2]) { - token.attrJoin('data-extra', highlightInfos[2]) - } + const highlightInfos = parseCodeBlockParameters(token.info) + Optional.ofNullable(highlightInfos.language).ifPresent((language) => + token.attrJoin('data-highlight-language', language) + ) + Optional.ofNullable(highlightInfos.codeFenceParameters).ifPresent((language) => + token.attrJoin('data-extra', language) + ) } }) return true diff --git a/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/code-block-parameters.test.ts b/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/code-block-parameters.test.ts new file mode 100644 index 000000000..d0b39dc63 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/code-block-parameters.test.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { parseCodeBlockParameters } from './code-block-parameters' + +describe('Code block parameter parsing', () => { + it('should detect just the language', () => { + const result = parseCodeBlockParameters('esperanto') + expect(result.language).toBe('esperanto') + expect(result.codeFenceParameters).toBe('') + }) + it('should detect an empty string', () => { + const result = parseCodeBlockParameters('') + expect(result.language).toBe('') + expect(result.codeFenceParameters).toBe('') + }) + it('should detect additional information after the language', () => { + const result = parseCodeBlockParameters('esperanto!!!!!') + expect(result.language).toBe('esperanto') + expect(result.codeFenceParameters).toBe('!!!!!') + }) + it('should detect just the additional information if no language is given', () => { + const result = parseCodeBlockParameters('!!!!!esperanto') + expect(result.language).toBe('') + expect(result.codeFenceParameters).toBe('!!!!!esperanto') + }) + it('should detect additional information if separated from the language with a space', () => { + const result = parseCodeBlockParameters('esperanto sed multe') + expect(result.language).toBe('esperanto') + expect(result.codeFenceParameters).toBe('sed multe') + }) + it('should ignore spaces at the beginning and the end', () => { + const result = parseCodeBlockParameters(' esperanto sed multe ') + expect(result.language).toBe('esperanto') + expect(result.codeFenceParameters).toBe('sed multe') + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/code-block-parameters.ts b/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/code-block-parameters.ts new file mode 100644 index 000000000..0dc1d1220 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/code-block-parameters.ts @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const codeFenceArguments = /^ *([\w-]*)(.*)$/ + +interface CodeBlockParameters { + language: string + codeFenceParameters: string +} + +/** + * Parses the language name and additional parameters from a code block name input. + * + * @param text The text to parse + * @return The parsed parameters + */ +export const parseCodeBlockParameters = (text: string): CodeBlockParameters => { + const parsedText = codeFenceArguments.exec(text) + return { + language: parsedText?.[1].trim() ?? '', + codeFenceParameters: parsedText?.[2].trim() ?? '' + } +} diff --git a/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/find-language-by-code-block-name.test.ts b/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/find-language-by-code-block-name.test.ts new file mode 100644 index 000000000..7fc277380 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/find-language-by-code-block-name.test.ts @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { findLanguageByCodeBlockName } from './find-language-by-code-block-name' +import { Mock } from 'ts-mockery' +import type { LanguageDescription } from '@codemirror/language' + +describe('filter language name', () => { + const mockedLanguage1 = Mock.of({ name: 'Mocky', alias: ['mocky'] }) + const mockedLanguage2 = Mock.of({ name: 'Blocky', alias: ['blocky'] }) + const mockedLanguage3 = Mock.of({ name: 'Rocky', alias: ['rocky'] }) + const mockedLanguage4 = Mock.of({ name: 'Zocky', alias: ['zocky'] }) + const mockedLanguages = [mockedLanguage1, mockedLanguage2, mockedLanguage3, mockedLanguage4] + + it('should detect just the name of a language', () => { + expect(findLanguageByCodeBlockName(mockedLanguages, 'Mocky')).toBe(mockedLanguage1) + }) + + it('should detect the name of a language with parameters', () => { + expect(findLanguageByCodeBlockName(mockedLanguages, 'Blocky!!!')).toBe(mockedLanguage2) + }) + + it('should detect just the alias of a language', () => { + expect(findLanguageByCodeBlockName(mockedLanguages, 'rocky')).toBe(mockedLanguage3) + }) + + it('should detect the alias of a language with parameters', () => { + expect(findLanguageByCodeBlockName(mockedLanguages, 'zocky!!!')).toBe(mockedLanguage4) + }) + + it("shouldn't return a language if no match", () => { + expect(findLanguageByCodeBlockName(mockedLanguages, 'Docky')).toBe(null) + }) +}) diff --git a/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/find-language-by-code-block-name.ts b/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/find-language-by-code-block-name.ts new file mode 100644 index 000000000..101140ba4 --- /dev/null +++ b/src/components/markdown-renderer/markdown-extension/code-block-markdown-extension/find-language-by-code-block-name.ts @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import Optional from 'optional-js' +import type { LanguageDescription } from '@codemirror/language' +import { parseCodeBlockParameters } from './code-block-parameters' + +/** + * Finds the {@link LanguageDescription code mirror language descriptions} that matches the given language name or any alias. + * It ignores additional code block name parameters. + * + * @param languages The languages in which the description should be found + * @param inputLanguageName The input from the code block + * @return The found language description or null if no language could be found by name or alias + */ +export const findLanguageByCodeBlockName = ( + languages: LanguageDescription[], + inputLanguageName: string +): LanguageDescription | null => { + return Optional.ofNullable(parseCodeBlockParameters(inputLanguageName).language) + .map((filteredLanguage) => + languages.find((language) => language.name === filteredLanguage || language.alias.includes(filteredLanguage)) + ) + .orElse(null) +}