mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-25 19:26:31 -05:00
Fix language detection in code mirror code blocks
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
ddea0edbf8
commit
f328681c0f
6 changed files with 146 additions and 13 deletions
|
@ -25,12 +25,13 @@ import { useCodeMirrorScrollWatchExtension } from './hooks/code-mirror-extension
|
||||||
import { useCodeMirrorPasteExtension } from './hooks/code-mirror-extensions/use-code-mirror-paste-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 { useCodeMirrorFileDropExtension } from './hooks/code-mirror-extensions/use-code-mirror-file-drop-extension'
|
||||||
import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
|
import { markdown, markdownLanguage } from '@codemirror/lang-markdown'
|
||||||
import { languages } from '@codemirror/language-data'
|
|
||||||
import { EditorView } from '@codemirror/view'
|
import { EditorView } from '@codemirror/view'
|
||||||
import { autocompletion } from '@codemirror/autocomplete'
|
import { autocompletion } from '@codemirror/autocomplete'
|
||||||
import { useCodeMirrorFocusReference } from './hooks/use-code-mirror-focus-reference'
|
import { useCodeMirrorFocusReference } from './hooks/use-code-mirror-focus-reference'
|
||||||
import { useOffScreenScrollProtection } from './hooks/use-off-screen-scroll-protection'
|
import { useOffScreenScrollProtection } from './hooks/use-off-screen-scroll-protection'
|
||||||
import { cypressId } from '../../../utils/cypress-attribute'
|
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')
|
const logger = new Logger('EditorPane')
|
||||||
|
|
||||||
|
@ -62,7 +63,10 @@ export const EditorPane: React.FC<ScrollProps> = ({ scrollState, onScroll, onMak
|
||||||
|
|
||||||
const extensions = useMemo(
|
const extensions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
markdown({ base: markdownLanguage, codeLanguages: languages }),
|
markdown({
|
||||||
|
base: markdownLanguage,
|
||||||
|
codeLanguages: (input) => findLanguageByCodeBlockName(languages, input)
|
||||||
|
}),
|
||||||
...saveOffFocusScrollStateExtensions,
|
...saveOffFocusScrollStateExtensions,
|
||||||
focusExtension,
|
focusExtension,
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
|
|
|
@ -6,9 +6,10 @@
|
||||||
|
|
||||||
import type MarkdownIt from 'markdown-it'
|
import type MarkdownIt from 'markdown-it'
|
||||||
import type { RuleCore } from 'markdown-it/lib/parser_core'
|
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 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.
|
* 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) => {
|
const rule: RuleCore = (state) => {
|
||||||
state.tokens.forEach((token) => {
|
state.tokens.forEach((token) => {
|
||||||
if (token.type === 'fence') {
|
if (token.type === 'fence') {
|
||||||
const highlightInfos = codeFenceArguments.exec(token.info)
|
const highlightInfos = parseCodeBlockParameters(token.info)
|
||||||
if (!highlightInfos) {
|
Optional.ofNullable(highlightInfos.language).ifPresent((language) =>
|
||||||
return
|
token.attrJoin('data-highlight-language', language)
|
||||||
}
|
)
|
||||||
if (highlightInfos[1]) {
|
Optional.ofNullable(highlightInfos.codeFenceParameters).ifPresent((language) =>
|
||||||
token.attrJoin('data-highlight-language', highlightInfos[1])
|
token.attrJoin('data-extra', language)
|
||||||
}
|
)
|
||||||
if (highlightInfos[2]) {
|
|
||||||
token.attrJoin('data-extra', highlightInfos[2])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -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')
|
||||||
|
})
|
||||||
|
})
|
|
@ -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() ?? ''
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<LanguageDescription>({ name: 'Mocky', alias: ['mocky'] })
|
||||||
|
const mockedLanguage2 = Mock.of<LanguageDescription>({ name: 'Blocky', alias: ['blocky'] })
|
||||||
|
const mockedLanguage3 = Mock.of<LanguageDescription>({ name: 'Rocky', alias: ['rocky'] })
|
||||||
|
const mockedLanguage4 = Mock.of<LanguageDescription>({ 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)
|
||||||
|
})
|
||||||
|
})
|
|
@ -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<LanguageDescription | null>((filteredLanguage) =>
|
||||||
|
languages.find((language) => language.name === filteredLanguage || language.alias.includes(filteredLanguage))
|
||||||
|
)
|
||||||
|
.orElse(null)
|
||||||
|
}
|
Loading…
Reference in a new issue