refactor: organize app extensions

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2023-04-14 09:13:37 +02:00
parent f62c9489ce
commit 4fbe813af0
No known key found for this signature in database
GPG key ID: 42498463316F048B
209 changed files with 276 additions and 239 deletions

View file

@ -0,0 +1,42 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { parseCodeBlockParameters } from './code-block-parameters'
import { Optional } from '@mrdrogdrog/optional'
import type MarkdownIt from 'markdown-it'
import type { RuleCore } from 'markdown-it/lib/parser_core'
const ruleName = 'code-highlighter'
/**
* Extracts the language name and additional flags from the code fence parameter and sets them as attributes in the token.
*
* @param state The current state of the processing {@link MarkdownIt} instance.
* @see MarkdownIt.RuleCore
*/
const rule: RuleCore = (state): void => {
state.tokens.forEach((token) => {
if (token.type === 'fence') {
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)
)
}
})
}
/**
* Adds the rule to the given {@link MarkdownIt markdown-it instance} if it hasn't been added yet.
*
* @param markdownIt The {@link MarkdownIt markdown-it instance} to which the rule should be added
*/
export const codeBlockMarkdownPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt) => {
if (markdownIt.core.ruler.getRules(ruleName).length === 0) {
markdownIt.core.ruler.push(ruleName, rule, { alt: [ruleName] })
}
}

View file

@ -0,0 +1,22 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ComponentReplacer } from '../../../replace-components/component-replacer'
import { MarkdownRendererExtension } from '../markdown-renderer-extension'
import { codeBlockMarkdownPlugin } from './code-block-markdown-plugin'
import type MarkdownIt from 'markdown-it'
/**
* A {@link MarkdownRendererExtension markdown extension} that is used for code fence replacements.
*/
export abstract class CodeBlockMarkdownRendererExtension extends MarkdownRendererExtension {
public configureMarkdownIt(markdownIt: MarkdownIt): void {
codeBlockMarkdownPlugin(markdownIt)
}
public buildReplacers(): ComponentReplacer[] {
return []
}
}

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2023 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')
})
})

View file

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

View file

@ -0,0 +1,36 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { findLanguageByCodeBlockName } from './find-language-by-code-block-name'
import type { LanguageDescription } from '@codemirror/language'
import { Mock } from 'ts-mockery'
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)
})
})

View file

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { parseCodeBlockParameters } from './code-block-parameters'
import type { LanguageDescription } from '@codemirror/language'
import { Optional } from '@mrdrogdrog/optional'
/**
* 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)
}

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownRendererExtension } from './markdown-renderer-extension'
import type { EventEmitter2 } from 'eventemitter2'
/**
* Base class for Markdown renderer extensions that need an event emitter.
*/
export abstract class EventMarkdownRendererExtension extends MarkdownRendererExtension {
constructor(protected readonly eventEmitter: EventEmitter2) {
super()
}
}

View file

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { NodeProcessor } from '../../node-preprocessors/node-processor'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import type MarkdownIt from 'markdown-it'
/**
* Base class for Markdown extensions.
*/
export abstract class MarkdownRendererExtension {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public configureMarkdownIt(markdownIt: MarkdownIt): void {
return
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public configureMarkdownItPost(markdownIt: MarkdownIt): void {
return
}
public buildNodeProcessors(): NodeProcessor[] {
return []
}
public buildReplacers(): ComponentReplacer[] {
return []
}
public buildTagNameAllowList(): string[] {
return []
}
}