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 8cddc96881
commit 1e4709c087
209 changed files with 286 additions and 243 deletions

View file

@ -0,0 +1,133 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Highlighted Code can hide the line numbers 1`] = `
<code
class="hljs wrapLines"
data-testid="code-highlighter"
>
<span
class="linenumber"
>
1
</span>
<div
class="codeline"
>
<span>
const a = 1
</span>
</div>
</code>
`;
exports[`Highlighted Code highlights code 1`] = `
<code
class="hljs showGutter "
data-testid="code-highlighter"
>
<span
class="linenumber"
>
1
</span>
<div
class="codeline"
>
<span
class="hljs-keyword"
>
const
</span>
a =
<span
class="hljs-number"
>
1
</span>
</div>
</code>
`;
exports[`Highlighted Code renders plain text 1`] = `
<code
class="hljs showGutter "
data-testid="code-highlighter"
>
<span
class="linenumber"
>
1
</span>
<div
class="codeline"
>
<span>
a
</span>
</div>
<span
class="linenumber"
>
2
</span>
<div
class="codeline"
>
<span>
b
</span>
</div>
<span
class="linenumber"
>
3
</span>
<div
class="codeline"
>
<span>
c
</span>
</div>
</code>
`;
exports[`Highlighted Code starts with a specific line 1`] = `
<code
class="hljs showGutter wrapLines"
data-testid="code-highlighter"
>
<span
class="linenumber"
>
100
</span>
<div
class="codeline"
>
<span>
const a = 1
</span>
</div>
</code>
`;
exports[`Highlighted Code wraps code 1`] = `
<code
class="hljs showGutter wrapLines"
data-testid="code-highlighter"
>
<span
class="linenumber"
>
1
</span>
<div
class="codeline"
>
<span>
const a = 1
</span>
</div>
</code>
`;

View file

@ -0,0 +1,57 @@
/*!
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.code-highlighter {
position: relative;
:global(code.hljs) {
overflow-x: auto;
background-color: rgba(27, 31, 35, .05);
padding: 16px;
display: grid !important;
grid-template-columns: auto minmax(0, 1fr);
:global(body.dark) & {
background-color: rgb(27, 31, 35);
}
.codeline {
grid-column: 2;
white-space: pre;
}
.linenumber {
grid-column: 1;
position: relative;
cursor: default;
z-index: 4;
padding: 0 8px 0 0;
min-width: 20px;
box-sizing: content-box;
color: #afafaf;
border-right: 3px solid #6ce26c;
flex-direction: column;
overflow: hidden;
user-select: none;
align-items: flex-end;
display: none;
}
&.showGutter {
.linenumber {
display: flex;
}
.codeline {
margin: 0 0 0 16px;
}
}
&.wrapLines .codeline {
white-space: pre-wrap;
}
}
}

View file

@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { mockI18n } from '../../../test-utils/mock-i18n'
import HighlightedCode from './highlighted-code'
import { render, screen } from '@testing-library/react'
describe('Highlighted Code', () => {
beforeAll(() => mockI18n())
it('renders plain text', async () => {
render(<HighlightedCode code={'a\nb\nc'} startLineNumber={1} language={''} wrapLines={false}></HighlightedCode>)
expect(await screen.findByTestId('code-highlighter')).toMatchSnapshot()
})
it('highlights code', async () => {
render(
<HighlightedCode
code={'const a = 1'}
language={'typescript'}
startLineNumber={1}
wrapLines={false}></HighlightedCode>
)
expect(await screen.findByTestId('code-highlighter')).toMatchSnapshot()
})
it('wraps code', async () => {
render(<HighlightedCode code={'const a = 1'} wrapLines={true} startLineNumber={1} language={''}></HighlightedCode>)
expect(await screen.findByTestId('code-highlighter')).toMatchSnapshot()
})
it('starts with a specific line', async () => {
render(
<HighlightedCode code={'const a = 1'} startLineNumber={100} language={''} wrapLines={true}></HighlightedCode>
)
expect(await screen.findByTestId('code-highlighter')).toMatchSnapshot()
})
it('can hide the line numbers', async () => {
render(
<HighlightedCode
code={'const a = 1'}
startLineNumber={undefined}
language={''}
wrapLines={true}></HighlightedCode>
)
expect(await screen.findByTestId('code-highlighter')).toMatchSnapshot()
})
})

View file

@ -0,0 +1,57 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AsyncLoadingBoundary } from '../../../components/common/async-loading-boundary/async-loading-boundary'
import { CopyToClipboardButton } from '../../../components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button'
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
import { testId } from '../../../utils/test-id'
import styles from './highlighted-code.module.scss'
import { useAsyncHighlightJsImport } from './hooks/use-async-highlight-js-import'
import { useAttachLineNumbers } from './hooks/use-attach-line-numbers'
import { useCodeDom } from './hooks/use-code-dom'
import React from 'react'
export interface HighlightedCodeProps {
code: string
language?: string
startLineNumber?: number
wrapLines: boolean
}
/**
* Shows the given code as highlighted code block.
*
* @param code The code to highlight
* @param language The language that should be used for highlighting
* @param startLineNumber The number of the first line in the block. Will be 1 if omitted.
* @param wrapLines Defines if lines should be wrapped or if the block should show a scroll bar.
* @see https://highlightjs.org/
*/
export const HighlightedCode: React.FC<HighlightedCodeProps> = ({ code, language, startLineNumber, wrapLines }) => {
const showGutter = startLineNumber !== undefined
const { value: hljsApi, loading, error } = useAsyncHighlightJsImport()
const codeDom = useCodeDom(code, hljsApi, language)
const wrappedDomLines = useAttachLineNumbers(codeDom, startLineNumber)
return (
<AsyncLoadingBoundary loading={loading || !hljsApi} error={!!error} componentName={'highlight.js'}>
<div className={styles['code-highlighter']} {...cypressId('highlighted-code-block')}>
<code
{...testId('code-highlighter')}
{...cypressId('code-highlighter')}
{...cypressAttribute('showgutter', showGutter ? 'true' : 'false')}
{...cypressAttribute('wraplines', wrapLines ? 'true' : 'false')}
className={`hljs ${showGutter ? styles['showGutter'] : ''} ${wrapLines ? styles['wrapLines'] : ''}`}>
{wrappedDomLines}
</code>
<div className={'text-right button-inside'}>
<CopyToClipboardButton content={code} {...cypressId('copy-code-button')} />
</div>
</div>
</AsyncLoadingBoundary>
)
}
export default HighlightedCode

View file

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Logger } from '../../../../utils/logger'
import type { HLJSApi } from 'highlight.js'
import { useAsync } from 'react-use'
import type { AsyncState } from 'react-use/lib/useAsyncFn'
const log = new Logger('HighlightedCode')
/**
* Lazy loads the highlight js library.
*
* @return the loaded js lib
*/
export const useAsyncHighlightJsImport = (): AsyncState<HLJSApi> => {
return useAsync(async () => {
try {
return (await import(/* webpackChunkName: "highlight.js" */ '../preconfigured-highlight-js')).default
} catch (error) {
log.error('Error while loading highlight.js', error)
throw error
}
}, [])
}

View file

@ -0,0 +1,34 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { cypressId } from '../../../../utils/cypress-attribute'
import styles from '../highlighted-code.module.scss'
import type { ReactElement } from 'react'
import { Fragment, useMemo } from 'react'
/**
* Wraps the given {@link ReactElement elements} to attach line numbers to them.
*
* @param lines The elements to wrap
* @param startLineNumber The line number to start with. Will default to 1 if omitted.
*/
export const useAttachLineNumbers = (
lines: undefined | ReactElement[],
startLineNumber = 1
): undefined | ReactElement[] =>
useMemo(
() =>
lines?.map((line, index) => (
<Fragment key={index}>
<span {...cypressId('linenumber')} className={styles['linenumber']}>
{startLineNumber + index}
</span>
<div {...cypressId('codeline')} className={styles['codeline']}>
{line}
</div>
</Fragment>
)),
[startLineNumber, lines]
)

View file

@ -0,0 +1,65 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { HtmlToReact } from '../../../../components/common/html-to-react/html-to-react'
import type { HLJSApi } from 'highlight.js'
import type { ReactElement } from 'react'
import React, { useMemo } from 'react'
/**
* Highlights the given code using highlight.js. If the language wasn't recognized then it won't be highlighted.
*
* @param code The code to highlight
* @param hljs The highlight.js API. Needs to be imported or lazy loaded.
* @param language The language of the code to use for highlighting
* @return The react elements that represent the highlighted code
*/
export const useCodeDom = (code: string, hljs: HLJSApi | undefined, language?: string): ReactElement[] | undefined => {
return useMemo(() => {
if (!hljs) {
return
}
if (!!language && hljs.listLanguages().includes(language)) {
const highlightedHtml = hljs.highlight(code, { language }).value
return createHtmlLinesToReactDOM(omitNewLineAtEnd(highlightedHtml).split('\n'))
} else {
return createPlaintextToReactDOM(code)
}
}, [code, hljs, language])
}
/**
* Converts the given html code to react elements without any custom transformation but with sanitizing.
*
* @param code The code to convert
* @return the code represented as react elements
*/
const createHtmlLinesToReactDOM = (code: string[]): ReactElement[] => {
return code.map((line, lineIndex) => <HtmlToReact htmlCode={line} key={lineIndex} />)
}
/**
* Converts the given line based text to plain text react elements but without interpreting them as html first.
*
* @param text The text to convert
* @return the text represented as react elements.
*/
const createPlaintextToReactDOM = (text: string): ReactElement[] => {
return text.split('\n').map((line, lineIndex) => React.createElement('span', { key: lineIndex }, line))
}
/**
* Returns the given code but without the last new line if the string ends with a new line.
*
* @param code The code to inspect
* @return the modified code
*/
const omitNewLineAtEnd = (code: string): string => {
if (code.endsWith('\n')) {
return code.slice(0, -1)
} else {
return code
}
}

View file

@ -0,0 +1,389 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import hljs from 'highlight.js/lib/core'
import abnf from 'highlight.js/lib/languages/abnf'
import accesslog from 'highlight.js/lib/languages/accesslog'
import actionscript from 'highlight.js/lib/languages/actionscript'
import ada from 'highlight.js/lib/languages/ada'
import angelscript from 'highlight.js/lib/languages/angelscript'
import apache from 'highlight.js/lib/languages/apache'
import applescript from 'highlight.js/lib/languages/applescript'
import arcade from 'highlight.js/lib/languages/arcade'
import arduino from 'highlight.js/lib/languages/arduino'
import armasm from 'highlight.js/lib/languages/armasm'
import asciidoc from 'highlight.js/lib/languages/asciidoc'
import aspectj from 'highlight.js/lib/languages/aspectj'
import autohotkey from 'highlight.js/lib/languages/autohotkey'
import autoit from 'highlight.js/lib/languages/autoit'
import avrasm from 'highlight.js/lib/languages/avrasm'
import awk from 'highlight.js/lib/languages/awk'
import axapta from 'highlight.js/lib/languages/axapta'
import bash from 'highlight.js/lib/languages/bash'
import basic from 'highlight.js/lib/languages/basic'
import bnf from 'highlight.js/lib/languages/bnf'
import brainfuck from 'highlight.js/lib/languages/brainfuck'
import c from 'highlight.js/lib/languages/c'
import cal from 'highlight.js/lib/languages/cal'
import capnproto from 'highlight.js/lib/languages/capnproto'
import ceylon from 'highlight.js/lib/languages/ceylon'
import clean from 'highlight.js/lib/languages/clean'
import clojure from 'highlight.js/lib/languages/clojure'
import clojure_repl from 'highlight.js/lib/languages/clojure-repl'
import cmake from 'highlight.js/lib/languages/cmake'
import coffeescript from 'highlight.js/lib/languages/coffeescript'
import coq from 'highlight.js/lib/languages/coq'
import cos from 'highlight.js/lib/languages/cos'
import cpp from 'highlight.js/lib/languages/cpp'
import crmsh from 'highlight.js/lib/languages/crmsh'
import crystal from 'highlight.js/lib/languages/crystal'
import csharp from 'highlight.js/lib/languages/csharp'
import csp from 'highlight.js/lib/languages/csp'
import css from 'highlight.js/lib/languages/css'
import d from 'highlight.js/lib/languages/d'
import dart from 'highlight.js/lib/languages/dart'
import delphi from 'highlight.js/lib/languages/delphi'
import diff from 'highlight.js/lib/languages/diff'
import django from 'highlight.js/lib/languages/django'
import dns from 'highlight.js/lib/languages/dns'
import dockerfile from 'highlight.js/lib/languages/dockerfile'
import dos from 'highlight.js/lib/languages/dos'
import dsconfig from 'highlight.js/lib/languages/dsconfig'
import dts from 'highlight.js/lib/languages/dts'
import dust from 'highlight.js/lib/languages/dust'
import ebnf from 'highlight.js/lib/languages/ebnf'
import elixir from 'highlight.js/lib/languages/elixir'
import elm from 'highlight.js/lib/languages/elm'
import erb from 'highlight.js/lib/languages/erb'
import erlang from 'highlight.js/lib/languages/erlang'
import erlang_repl from 'highlight.js/lib/languages/erlang-repl'
import excel from 'highlight.js/lib/languages/excel'
import fix from 'highlight.js/lib/languages/fix'
import flix from 'highlight.js/lib/languages/flix'
import fortran from 'highlight.js/lib/languages/fortran'
import fsharp from 'highlight.js/lib/languages/fsharp'
import gams from 'highlight.js/lib/languages/gams'
import gauss from 'highlight.js/lib/languages/gauss'
import gcode from 'highlight.js/lib/languages/gcode'
import gherkin from 'highlight.js/lib/languages/gherkin'
import glsl from 'highlight.js/lib/languages/glsl'
import gml from 'highlight.js/lib/languages/gml'
import go from 'highlight.js/lib/languages/go'
import golo from 'highlight.js/lib/languages/golo'
import gradle from 'highlight.js/lib/languages/gradle'
import groovy from 'highlight.js/lib/languages/groovy'
import haml from 'highlight.js/lib/languages/haml'
import handlebars from 'highlight.js/lib/languages/handlebars'
import haskell from 'highlight.js/lib/languages/haskell'
import haxe from 'highlight.js/lib/languages/haxe'
import hsp from 'highlight.js/lib/languages/hsp'
import http from 'highlight.js/lib/languages/http'
import hy from 'highlight.js/lib/languages/hy'
import inform7 from 'highlight.js/lib/languages/inform7'
import ini from 'highlight.js/lib/languages/ini'
import irpf90 from 'highlight.js/lib/languages/irpf90'
import isbl from 'highlight.js/lib/languages/isbl'
import java from 'highlight.js/lib/languages/java'
import javascript from 'highlight.js/lib/languages/javascript'
import jboss_cli from 'highlight.js/lib/languages/jboss-cli'
import json from 'highlight.js/lib/languages/json'
import julia from 'highlight.js/lib/languages/julia'
import julia_repl from 'highlight.js/lib/languages/julia-repl'
import kotlin from 'highlight.js/lib/languages/kotlin'
import lasso from 'highlight.js/lib/languages/lasso'
import latex from 'highlight.js/lib/languages/latex'
import ldif from 'highlight.js/lib/languages/ldif'
import leaf from 'highlight.js/lib/languages/leaf'
import less from 'highlight.js/lib/languages/less'
import lisp from 'highlight.js/lib/languages/lisp'
import livecodeserver from 'highlight.js/lib/languages/livecodeserver'
import livescript from 'highlight.js/lib/languages/livescript'
import llvm from 'highlight.js/lib/languages/llvm'
import lsl from 'highlight.js/lib/languages/lsl'
import lua from 'highlight.js/lib/languages/lua'
import makefile from 'highlight.js/lib/languages/makefile'
import markdown from 'highlight.js/lib/languages/markdown'
import mathematica from 'highlight.js/lib/languages/mathematica'
import matlab from 'highlight.js/lib/languages/matlab'
import maxima from 'highlight.js/lib/languages/maxima'
import mel from 'highlight.js/lib/languages/mel'
import mercury from 'highlight.js/lib/languages/mercury'
import mipsasm from 'highlight.js/lib/languages/mipsasm'
import mizar from 'highlight.js/lib/languages/mizar'
import mojolicious from 'highlight.js/lib/languages/mojolicious'
import monkey from 'highlight.js/lib/languages/monkey'
import moonscript from 'highlight.js/lib/languages/moonscript'
import n1ql from 'highlight.js/lib/languages/n1ql'
import nestedtext from 'highlight.js/lib/languages/nestedtext'
import nginx from 'highlight.js/lib/languages/nginx'
import nim from 'highlight.js/lib/languages/nim'
import nix from 'highlight.js/lib/languages/nix'
import node_repl from 'highlight.js/lib/languages/node-repl'
import nsis from 'highlight.js/lib/languages/nsis'
import objectivec from 'highlight.js/lib/languages/objectivec'
import ocaml from 'highlight.js/lib/languages/ocaml'
import openscad from 'highlight.js/lib/languages/openscad'
import oxygene from 'highlight.js/lib/languages/oxygene'
import parser3 from 'highlight.js/lib/languages/parser3'
import perl from 'highlight.js/lib/languages/perl'
import pf from 'highlight.js/lib/languages/pf'
import pgsql from 'highlight.js/lib/languages/pgsql'
import php from 'highlight.js/lib/languages/php'
import php_template from 'highlight.js/lib/languages/php-template'
import plaintext from 'highlight.js/lib/languages/plaintext'
import pony from 'highlight.js/lib/languages/pony'
import powershell from 'highlight.js/lib/languages/powershell'
import processing from 'highlight.js/lib/languages/processing'
import profile from 'highlight.js/lib/languages/profile'
import prolog from 'highlight.js/lib/languages/prolog'
import properties from 'highlight.js/lib/languages/properties'
import protobuf from 'highlight.js/lib/languages/protobuf'
import puppet from 'highlight.js/lib/languages/puppet'
import purebasic from 'highlight.js/lib/languages/purebasic'
import python from 'highlight.js/lib/languages/python'
import python_repl from 'highlight.js/lib/languages/python-repl'
import q from 'highlight.js/lib/languages/q'
import qml from 'highlight.js/lib/languages/qml'
import r from 'highlight.js/lib/languages/r'
import reasonml from 'highlight.js/lib/languages/reasonml'
import rib from 'highlight.js/lib/languages/rib'
import roboconf from 'highlight.js/lib/languages/roboconf'
import routeros from 'highlight.js/lib/languages/routeros'
import rsl from 'highlight.js/lib/languages/rsl'
import ruby from 'highlight.js/lib/languages/ruby'
import ruleslanguage from 'highlight.js/lib/languages/ruleslanguage'
import rust from 'highlight.js/lib/languages/rust'
import sas from 'highlight.js/lib/languages/sas'
import scala from 'highlight.js/lib/languages/scala'
import scheme from 'highlight.js/lib/languages/scheme'
import scilab from 'highlight.js/lib/languages/scilab'
import scss from 'highlight.js/lib/languages/scss'
import shell from 'highlight.js/lib/languages/shell'
import smali from 'highlight.js/lib/languages/smali'
import smalltalk from 'highlight.js/lib/languages/smalltalk'
import sml from 'highlight.js/lib/languages/sml'
import sqf from 'highlight.js/lib/languages/sqf'
import sql from 'highlight.js/lib/languages/sql'
import stan from 'highlight.js/lib/languages/stan'
import stata from 'highlight.js/lib/languages/stata'
import step21 from 'highlight.js/lib/languages/step21'
import stylus from 'highlight.js/lib/languages/stylus'
import subunit from 'highlight.js/lib/languages/subunit'
import swift from 'highlight.js/lib/languages/swift'
import taggerscript from 'highlight.js/lib/languages/taggerscript'
import tap from 'highlight.js/lib/languages/tap'
import tcl from 'highlight.js/lib/languages/tcl'
import thrift from 'highlight.js/lib/languages/thrift'
import tp from 'highlight.js/lib/languages/tp'
import twig from 'highlight.js/lib/languages/twig'
import typescript from 'highlight.js/lib/languages/typescript'
import vala from 'highlight.js/lib/languages/vala'
import vbnet from 'highlight.js/lib/languages/vbnet'
import vbscript from 'highlight.js/lib/languages/vbscript'
import vbscript_html from 'highlight.js/lib/languages/vbscript-html'
import verilog from 'highlight.js/lib/languages/verilog'
import vhdl from 'highlight.js/lib/languages/vhdl'
import vim from 'highlight.js/lib/languages/vim'
import wasm from 'highlight.js/lib/languages/wasm'
import x86asm from 'highlight.js/lib/languages/x86asm'
import xl from 'highlight.js/lib/languages/xl'
import xml from 'highlight.js/lib/languages/xml'
import xquery from 'highlight.js/lib/languages/xquery'
import yaml from 'highlight.js/lib/languages/yaml'
import zephir from 'highlight.js/lib/languages/zephir'
hljs.registerLanguage('abnf', abnf)
hljs.registerLanguage('accesslog', accesslog)
hljs.registerLanguage('actionscript', actionscript)
hljs.registerLanguage('ada', ada)
hljs.registerLanguage('angelscript', angelscript)
hljs.registerLanguage('apache', apache)
hljs.registerLanguage('applescript', applescript)
hljs.registerLanguage('arcade', arcade)
hljs.registerLanguage('arduino', arduino)
hljs.registerLanguage('armasm', armasm)
hljs.registerLanguage('xml', xml)
hljs.registerLanguage('asciidoc', asciidoc)
hljs.registerLanguage('aspectj', aspectj)
hljs.registerLanguage('autohotkey', autohotkey)
hljs.registerLanguage('autoit', autoit)
hljs.registerLanguage('avrasm', avrasm)
hljs.registerLanguage('awk', awk)
hljs.registerLanguage('axapta', axapta)
hljs.registerLanguage('bash', bash)
hljs.registerLanguage('basic', basic)
hljs.registerLanguage('bnf', bnf)
hljs.registerLanguage('brainfuck', brainfuck)
hljs.registerLanguage('c', c)
hljs.registerLanguage('cal', cal)
hljs.registerLanguage('capnproto', capnproto)
hljs.registerLanguage('ceylon', ceylon)
hljs.registerLanguage('clean', clean)
hljs.registerLanguage('clojure', clojure)
hljs.registerLanguage('clojure-repl', clojure_repl)
hljs.registerLanguage('cmake', cmake)
hljs.registerLanguage('coffeescript', coffeescript)
hljs.registerLanguage('coq', coq)
hljs.registerLanguage('cos', cos)
hljs.registerLanguage('cpp', cpp)
hljs.registerLanguage('crmsh', crmsh)
hljs.registerLanguage('crystal', crystal)
hljs.registerLanguage('csharp', csharp)
hljs.registerLanguage('csp', csp)
hljs.registerLanguage('css', css)
hljs.registerLanguage('d', d)
hljs.registerLanguage('markdown', markdown)
hljs.registerLanguage('dart', dart)
hljs.registerLanguage('delphi', delphi)
hljs.registerLanguage('diff', diff)
hljs.registerLanguage('django', django)
hljs.registerLanguage('dns', dns)
hljs.registerLanguage('dockerfile', dockerfile)
hljs.registerLanguage('dos', dos)
hljs.registerLanguage('dsconfig', dsconfig)
hljs.registerLanguage('dts', dts)
hljs.registerLanguage('dust', dust)
hljs.registerLanguage('ebnf', ebnf)
hljs.registerLanguage('elixir', elixir)
hljs.registerLanguage('elm', elm)
hljs.registerLanguage('ruby', ruby)
hljs.registerLanguage('erb', erb)
hljs.registerLanguage('erlang-repl', erlang_repl)
hljs.registerLanguage('erlang', erlang)
hljs.registerLanguage('excel', excel)
hljs.registerLanguage('fix', fix)
hljs.registerLanguage('flix', flix)
hljs.registerLanguage('fortran', fortran)
hljs.registerLanguage('fsharp', fsharp)
hljs.registerLanguage('gams', gams)
hljs.registerLanguage('gauss', gauss)
hljs.registerLanguage('gcode', gcode)
hljs.registerLanguage('gherkin', gherkin)
hljs.registerLanguage('glsl', glsl)
hljs.registerLanguage('gml', gml)
hljs.registerLanguage('go', go)
hljs.registerLanguage('golo', golo)
hljs.registerLanguage('gradle', gradle)
hljs.registerLanguage('groovy', groovy)
hljs.registerLanguage('haml', haml)
hljs.registerLanguage('handlebars', handlebars)
hljs.registerLanguage('haskell', haskell)
hljs.registerLanguage('haxe', haxe)
hljs.registerLanguage('hsp', hsp)
hljs.registerLanguage('html', xml)
hljs.registerLanguage('http', http)
hljs.registerLanguage('hy', hy)
hljs.registerLanguage('inform7', inform7)
hljs.registerLanguage('ini', ini)
hljs.registerLanguage('irpf90', irpf90)
hljs.registerLanguage('isbl', isbl)
hljs.registerLanguage('java', java)
hljs.registerLanguage('javascript', javascript)
hljs.registerLanguage('jboss-cli', jboss_cli)
hljs.registerLanguage('js', javascript)
hljs.registerLanguage('json', json)
hljs.registerLanguage('julia', julia)
hljs.registerLanguage('julia-repl', julia_repl)
hljs.registerLanguage('kotlin', kotlin)
hljs.registerLanguage('lasso', lasso)
hljs.registerLanguage('latex', latex)
hljs.registerLanguage('ldif', ldif)
hljs.registerLanguage('leaf', leaf)
hljs.registerLanguage('less', less)
hljs.registerLanguage('lisp', lisp)
hljs.registerLanguage('livecodeserver', livecodeserver)
hljs.registerLanguage('livescript', livescript)
hljs.registerLanguage('llvm', llvm)
hljs.registerLanguage('lsl', lsl)
hljs.registerLanguage('lua', lua)
hljs.registerLanguage('makefile', makefile)
hljs.registerLanguage('mathematica', mathematica)
hljs.registerLanguage('matlab', matlab)
hljs.registerLanguage('maxima', maxima)
hljs.registerLanguage('mel', mel)
hljs.registerLanguage('mercury', mercury)
hljs.registerLanguage('mipsasm', mipsasm)
hljs.registerLanguage('mizar', mizar)
hljs.registerLanguage('perl', perl)
hljs.registerLanguage('mojolicious', mojolicious)
hljs.registerLanguage('monkey', monkey)
hljs.registerLanguage('moonscript', moonscript)
hljs.registerLanguage('nestedtext', nestedtext)
hljs.registerLanguage('n1ql', n1ql)
hljs.registerLanguage('nginx', nginx)
hljs.registerLanguage('nim', nim)
hljs.registerLanguage('nix', nix)
hljs.registerLanguage('node-repl', node_repl)
hljs.registerLanguage('nsis', nsis)
hljs.registerLanguage('objectivec', objectivec)
hljs.registerLanguage('ocaml', ocaml)
hljs.registerLanguage('openscad', openscad)
hljs.registerLanguage('oxygene', oxygene)
hljs.registerLanguage('parser3', parser3)
hljs.registerLanguage('pf', pf)
hljs.registerLanguage('pgsql', pgsql)
hljs.registerLanguage('php', php)
hljs.registerLanguage('php-template', php_template)
hljs.registerLanguage('plaintext', plaintext)
hljs.registerLanguage('pony', pony)
hljs.registerLanguage('powershell', powershell)
hljs.registerLanguage('processing', processing)
hljs.registerLanguage('profile', profile)
hljs.registerLanguage('prolog', prolog)
hljs.registerLanguage('properties', properties)
hljs.registerLanguage('protobuf', protobuf)
hljs.registerLanguage('puppet', puppet)
hljs.registerLanguage('purebasic', purebasic)
hljs.registerLanguage('python', python)
hljs.registerLanguage('python-repl', python_repl)
hljs.registerLanguage('q', q)
hljs.registerLanguage('qml', qml)
hljs.registerLanguage('r', r)
hljs.registerLanguage('reasonml', reasonml)
hljs.registerLanguage('rib', rib)
hljs.registerLanguage('roboconf', roboconf)
hljs.registerLanguage('routeros', routeros)
hljs.registerLanguage('rsl', rsl)
hljs.registerLanguage('ruleslanguage', ruleslanguage)
hljs.registerLanguage('rust', rust)
hljs.registerLanguage('sas', sas)
hljs.registerLanguage('scala', scala)
hljs.registerLanguage('scheme', scheme)
hljs.registerLanguage('scilab', scilab)
hljs.registerLanguage('scss', scss)
hljs.registerLanguage('shell', shell)
hljs.registerLanguage('smali', smali)
hljs.registerLanguage('smalltalk', smalltalk)
hljs.registerLanguage('sml', sml)
hljs.registerLanguage('sqf', sqf)
hljs.registerLanguage('sql', sql)
hljs.registerLanguage('stan', stan)
hljs.registerLanguage('stata', stata)
hljs.registerLanguage('step21', step21)
hljs.registerLanguage('stylus', stylus)
hljs.registerLanguage('subunit', subunit)
hljs.registerLanguage('swift', swift)
hljs.registerLanguage('taggerscript', taggerscript)
hljs.registerLanguage('yaml', yaml)
hljs.registerLanguage('tap', tap)
hljs.registerLanguage('tcl', tcl)
hljs.registerLanguage('thrift', thrift)
hljs.registerLanguage('tp', tp)
hljs.registerLanguage('twig', twig)
hljs.registerLanguage('typescript', typescript)
hljs.registerLanguage('vala', vala)
hljs.registerLanguage('vbnet', vbnet)
hljs.registerLanguage('vbscript', vbscript)
hljs.registerLanguage('vbscript-html', vbscript_html)
hljs.registerLanguage('verilog', verilog)
hljs.registerLanguage('vhdl', vhdl)
hljs.registerLanguage('vim', vim)
hljs.registerLanguage('x86asm', x86asm)
hljs.registerLanguage('xl', xl)
hljs.registerLanguage('xquery', xquery)
hljs.registerLanguage('zephir', zephir)
hljs.registerLanguage('wasmn', wasm)
export default hljs

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { allAppExtensions } from '../../../../extensions/extra-integrations/all-app-extensions'
import { allAppExtensions } from '../../../../extensions/all-app-extensions'
import type { CheatsheetEntry, CheatsheetExtension } from '../../cheatsheet/cheatsheet-extension'
import { isCheatsheetGroup } from '../../cheatsheet/cheatsheet-extension'
import { CategoryAccordion } from './category-accordion'
@ -25,10 +25,7 @@ export const CheatsheetContent: React.FC = () => {
setSelectedEntry(isCheatsheetGroup(value) ? value.entries[0] : value)
}, [])
const extensions = useMemo(
() => allAppExtensions.flatMap((extension) => extension.buildCheatsheetExtensions()),
[]
)
const extensions = useMemo(() => allAppExtensions.flatMap((extension) => extension.buildCheatsheetExtensions()), [])
return (
<Row className={`mt-2`}>

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import HighlightedCode from '../../../../extensions/extra-integrations/highlighted-code-fence/highlighted-code'
import HighlightedCode from '../../../common/highlighted-code/highlighted-code'
import { HtmlToReact } from '../../../common/html-to-react/html-to-react'
import { ExtensionEventEmitterProvider } from '../../../markdown-renderer/hooks/use-extension-event-emitter'
import { RendererType } from '../../../render-page/window-post-message-communicator/rendering-message'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { allAppExtensions } from '../../../../extensions/extra-integrations/all-app-extensions'
import { allAppExtensions } from '../../../../extensions/all-app-extensions'
import type { CheatsheetExtensionComponentProps } from '../../cheatsheet/cheatsheet-extension'
import { isCheatsheetGroup } from '../../cheatsheet/cheatsheet-extension'
import type { ReactElement } from 'react'

View file

@ -8,7 +8,7 @@ import { ORIGIN, useBaseUrl } from '../../../hooks/common/use-base-url'
import { useDarkModeState } from '../../../hooks/common/use-dark-mode-state'
import { useMayEdit } from '../../../hooks/common/use-may-edit'
import { cypressAttribute, cypressId } from '../../../utils/cypress-attribute'
import { findLanguageByCodeBlockName } from '../../markdown-renderer/extensions/base/code-block-markdown-extension/find-language-by-code-block-name'
import { findLanguageByCodeBlockName } from '../../markdown-renderer/extensions/_base-classes/code-block-markdown-extension/find-language-by-code-block-name'
import type { ScrollProps } from '../synced-scroll/scroll-props'
import styles from './extended-codemirror/codemirror.module.scss'
import { useCodeMirrorAutocompletionsExtension } from './hooks/codemirror-extensions/use-code-mirror-autocompletions-extension'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { allAppExtensions } from '../../../../../extensions/extra-integrations/all-app-extensions'
import { allAppExtensions } from '../../../../../extensions/all-app-extensions'
import { autocompletion } from '@codemirror/autocomplete'
import type { Extension } from '@codemirror/state'
import { useMemo } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { allAppExtensions } from '../../../../extensions/extra-integrations/all-app-extensions'
import { allAppExtensions } from '../../../../extensions/all-app-extensions'
import type { ReactElement } from 'react'
import React, { Fragment, useMemo } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { allAppExtensions } from '../../../../extensions/extra-integrations/all-app-extensions'
import { allAppExtensions } from '../../../../extensions/all-app-extensions'
import { useDarkModeState } from '../../../../hooks/common/use-dark-mode-state'
import { FrontmatterLinter } from './frontmatter-linter'
import type { Diagnostic } from '@codemirror/lint'

View file

@ -1,70 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AppExtension } from '../../../../extensions/base/app-extension'
import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
import { basicCompletion } from '../../../editor-page/editor-pane/autocompletions/basic-completion'
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { BasicMarkdownSyntaxMarkdownExtension } from './basic-markdown-syntax-markdown-extension'
import type { CompletionSource } from '@codemirror/autocomplete'
import { t } from 'i18next'
export class BasicMarkdownSyntaxAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new BasicMarkdownSyntaxMarkdownExtension()]
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [
{
i18nKey: 'basics.basicFormatting',
categoryI18nKey: 'basic'
},
{
i18nKey: 'basics.abbreviation',
categoryI18nKey: 'basic'
},
{
i18nKey: 'basics.footnote',
categoryI18nKey: 'basic'
},
{
i18nKey: 'basics.headlines',
categoryI18nKey: 'basic',
entries: [
{
i18nKey: 'hashtag'
},
{
i18nKey: 'equal'
}
]
},
{
i18nKey: 'basics.code',
categoryI18nKey: 'basic',
entries: [{ i18nKey: 'inline' }, { i18nKey: 'block' }]
},
{
i18nKey: 'basics.lists',
categoryI18nKey: 'basic',
entries: [{ i18nKey: 'unordered' }, { i18nKey: 'ordered' }]
},
{
i18nKey: 'basics.images',
categoryI18nKey: 'basic',
entries: [{ i18nKey: 'basic' }, { i18nKey: 'size' }]
},
{
i18nKey: 'basics.links',
categoryI18nKey: 'basic'
}
]
}
buildAutocompletion(): CompletionSource[] {
return [basicCompletion(/(^|\s)\[/, '[](https://)', t('editor.autocompletions.link') ?? undefined)]
}
}

View file

@ -1,31 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { imageSize } from '@hedgedoc/markdown-it-plugins'
import type MarkdownIt from 'markdown-it'
import abbreviation from 'markdown-it-abbr'
import definitionList from 'markdown-it-deflist'
import footnote from 'markdown-it-footnote'
import inserted from 'markdown-it-ins'
import marked from 'markdown-it-mark'
import subscript from 'markdown-it-sub'
import superscript from 'markdown-it-sup'
/**
* Adds some common markdown syntaxes to the markdown rendering.
*/
export class BasicMarkdownSyntaxMarkdownExtension extends MarkdownRendererExtension {
public configureMarkdownIt(markdownIt: MarkdownIt): void {
abbreviation(markdownIt)
definitionList(markdownIt)
subscript(markdownIt)
superscript(markdownIt)
inserted(markdownIt)
marked(markdownIt)
footnote(markdownIt)
imageSize(markdownIt)
}
}

View file

@ -1,37 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`bootstrap icon markdown extension doesn't render invalid icon 1`] = `
<div>
<p>
:bi-INVALIDICONNAME:
</p>
</div>
`;
exports[`bootstrap icon markdown extension doesn't render missing icon 1`] = `
<div>
<p>
:bi-:
</p>
</div>
`;
exports[`bootstrap icon markdown extension renders correct icon 1`] = `
<div>
<p>
<span
data-svg-mock="true"
data-testid="lazy-bootstrap-icon-alarm"
fill="currentColor"
height="1em"
width="1em"
/>
</p>
</div>
`;

View file

@ -1,37 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AppExtension } from '../../../../extensions/base/app-extension'
import { BootstrapLazyIcons } from '../../../common/icons/bootstrap-icons'
import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
import { regexCompletion } from '../../../editor-page/editor-pane/autocompletions/regex-completion'
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { BootstrapIconMarkdownExtension } from './bootstrap-icon-markdown-extension'
import type { CompletionSource } from '@codemirror/autocomplete'
import { t } from 'i18next'
const bootstrapIconNames = Object.keys(BootstrapLazyIcons)
export class BootstrapIconAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new BootstrapIconMarkdownExtension()]
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [{ i18nKey: 'bootstrapIcon', readMoreUrl: new URL('https://icons.getbootstrap.com/') }]
}
buildAutocompletion(): CompletionSource[] {
return [
regexCompletion(
/:(?:[\w-]+:?)?/,
bootstrapIconNames.map((icon) => ({
detail: t('editor.autocompletions.icon') ?? undefined,
label: `:bi-${icon}:`
}))
)
]
}
}

View file

@ -1,37 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { isBootstrapIconName } from '../../../common/icons/bootstrap-icons'
import { LazyBootstrapIcon } from '../../../common/icons/lazy-bootstrap-icon'
import type { NodeReplacement } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import { BootstrapIconMarkdownExtension } from './bootstrap-icon-markdown-extension'
import type { Element } from 'domhandler'
import React from 'react'
/**
* Replaces a bootstrap icon tag with the bootstrap icon react component.
*
* @see BootstrapIcon
*/
export class BootstrapIconComponentReplacer extends ComponentReplacer {
constructor() {
super()
}
public replace(node: Element): NodeReplacement {
const iconName = this.extractIconName(node)
if (!iconName || !isBootstrapIconName(iconName)) {
return DO_NOT_REPLACE
}
return React.createElement(LazyBootstrapIcon, { icon: iconName })
}
private extractIconName(element: Element): string | undefined {
return element.name === BootstrapIconMarkdownExtension.tagName && element.attribs && element.attribs.id
? element.attribs.id
: undefined
}
}

View file

@ -1,31 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
import { BootstrapIconMarkdownExtension } from './bootstrap-icon-markdown-extension'
import { render, screen } from '@testing-library/react'
import React from 'react'
describe('bootstrap icon markdown extension', () => {
it('renders correct icon', async () => {
const view = render(
<TestMarkdownRenderer extensions={[new BootstrapIconMarkdownExtension()]} content={':bi-alarm:'} />
)
await screen.findByTestId('lazy-bootstrap-icon-alarm')
expect(view.container).toMatchSnapshot()
})
it("doesn't render missing icon", () => {
const view = render(<TestMarkdownRenderer extensions={[new BootstrapIconMarkdownExtension()]} content={':bi-:'} />)
expect(view.container).toMatchSnapshot()
})
it("doesn't render invalid icon", () => {
const view = render(
<TestMarkdownRenderer extensions={[new BootstrapIconMarkdownExtension()]} content={':bi-INVALIDICONNAME:'} />
)
expect(view.container).toMatchSnapshot()
})
})

View file

@ -1,29 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { BootstrapIconComponentReplacer } from './bootstrap-icon-component-replacer'
import { replaceBootstrapIconsMarkdownItPlugin } from './replace-bootstrap-icons'
import type MarkdownIt from 'markdown-it'
/**
* Adds Bootstrap icons via the :bi-$name: syntax.
*/
export class BootstrapIconMarkdownExtension extends MarkdownRendererExtension {
public static readonly tagName = 'app-bootstrap-icon'
public configureMarkdownIt(markdownIt: MarkdownIt): void {
replaceBootstrapIconsMarkdownItPlugin(markdownIt)
}
public buildReplacers(): ComponentReplacer[] {
return [new BootstrapIconComponentReplacer()]
}
public buildTagNameAllowList(): string[] {
return [BootstrapIconMarkdownExtension.tagName]
}
}

View file

@ -1,39 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { replaceBootstrapIconsMarkdownItPlugin } from './replace-bootstrap-icons'
import MarkdownIt from 'markdown-it'
describe('Replace bootstrap icons', () => {
let markdownIt: MarkdownIt
beforeEach(() => {
markdownIt = new MarkdownIt('default', {
html: false,
breaks: true,
langPrefix: '',
typographer: true
})
markdownIt.use(replaceBootstrapIconsMarkdownItPlugin)
})
it(`can detect a correct icon`, () => {
expect(markdownIt.renderInline(':bi-alarm:')).toBe('<app-bootstrap-icon id="alarm"></app-bootstrap-icon>')
})
it("won't detect an invalid id", () => {
const invalidIcon = ':bi-invalid:'
expect(markdownIt.renderInline(invalidIcon)).toBe(invalidIcon)
})
it("won't detect an empty id", () => {
const invalidIcon = ':bi-:'
expect(markdownIt.renderInline(invalidIcon)).toBe(invalidIcon)
})
it("won't detect a wrong id", () => {
const invalidIcon = ':bi-%?(:'
expect(markdownIt.renderInline(invalidIcon)).toBe(invalidIcon)
})
})

View file

@ -1,30 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { RegexOptions } from '../../../../external-types/markdown-it-regex/interface'
import { isBootstrapIconName } from '../../../common/icons/bootstrap-icons'
import { BootstrapIconMarkdownExtension } from './bootstrap-icon-markdown-extension'
import type MarkdownIt from 'markdown-it'
import markdownItRegex from 'markdown-it-regex'
const biRegex = /:bi-([\w-]+):/i
/**
* Replacer for bootstrap icon via the :bi-$name: syntax.
*/
export const replaceBootstrapIconsMarkdownItPlugin: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt) =>
markdownItRegex(markdownIt, {
name: 'bootstrap-icons',
regex: biRegex,
replace: (match) => {
if (isBootstrapIconName(match)) {
// ESLint wants to collapse this tag, but then the tag won't be valid html anymore.
// noinspection CheckTagEmptyBody
return `<${BootstrapIconMarkdownExtension.tagName} id="${match}"></${BootstrapIconMarkdownExtension.tagName}>`
} else {
return `:bi-${match}:`
}
}
} as RegexOptions)

View file

@ -5,7 +5,7 @@
*/
import { Logger } from '../../../utils/logger'
import { isDevMode } from '../../../utils/test-modes'
import { MarkdownRendererExtension } from './base/markdown-renderer-extension'
import { MarkdownRendererExtension } from './_base-classes/markdown-renderer-extension'
import type MarkdownIt from 'markdown-it'
const log = new Logger('DebuggerMarkdownExtension')

View file

@ -1,21 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Emoji Markdown Extension renders a skin tone code 1`] = `
<div>
<p>
🏽
</p>
</div>
`;
exports[`Emoji Markdown Extension renders an emoji code 1`] = `
<div>
<p>
😄
</p>
</div>
`;

View file

@ -1,40 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AppExtension } from '../../../../extensions/base/app-extension'
import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
import { regexCompletion } from '../../../editor-page/editor-pane/autocompletions/regex-completion'
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { EmojiMarkdownExtension } from './emoji-markdown-extension'
import { emojiShortcodes } from './mapping'
import type { CompletionSource } from '@codemirror/autocomplete'
import { t } from 'i18next'
export class EmojiAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new EmojiMarkdownExtension()]
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [
{
i18nKey: 'emoji',
readMoreUrl: new URL('https://twemoji.twitter.com/')
}
]
}
buildAutocompletion(): CompletionSource[] {
return [
regexCompletion(
/:(?:[\w-+]+:?)?/,
emojiShortcodes.map((shortcode) => ({
detail: t('editor.autocompletions.emoji') ?? undefined,
label: `:${shortcode}:`
}))
)
]
}
}

View file

@ -1,31 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { mockI18n } from '../../../../test-utils/mock-i18n'
import { TestMarkdownRenderer } from '../../test-utils/test-markdown-renderer'
import { EmojiMarkdownExtension } from './emoji-markdown-extension'
import { render } from '@testing-library/react'
import React from 'react'
describe('Emoji Markdown Extension', () => {
beforeAll(async () => {
await mockI18n()
})
afterAll(() => {
jest.resetModules()
jest.restoreAllMocks()
})
it('renders an emoji code', () => {
const view = render(<TestMarkdownRenderer extensions={[new EmojiMarkdownExtension()]} content={':smile:'} />)
expect(view.container).toMatchSnapshot()
})
it('renders a skin tone code', () => {
const view = render(<TestMarkdownRenderer extensions={[new EmojiMarkdownExtension()]} content={':skin-tone-3:'} />)
expect(view.container).toMatchSnapshot()
})
})

View file

@ -1,20 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { combinedEmojiData } from './mapping'
import type MarkdownIt from 'markdown-it'
import emoji from 'markdown-it-emoji/bare'
/**
* Adds support for utf-8 emojis.
*/
export class EmojiMarkdownExtension extends MarkdownRendererExtension {
public configureMarkdownIt(markdownIt: MarkdownIt): void {
markdownIt.use(emoji, {
defs: combinedEmojiData
})
}
}

View file

@ -1,35 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import emojiData from 'emoji-picker-element-data/en/emojibase/data.json'
interface EmojiEntry {
shortcodes: string[]
emoji: string
}
type ShortCodeMap = { [key: string]: string }
const shortCodeMap = (emojiData as unknown as EmojiEntry[]).reduce((reduceObject, emoji) => {
emoji.shortcodes.forEach((shortcode) => {
reduceObject[shortcode] = emoji.emoji
})
return reduceObject
}, {} as ShortCodeMap)
const emojiSkinToneModifierMap = [1, 2, 3, 4, 5].reduce((reduceObject, modifierValue) => {
const lightSkinCode = 127995
const codepoint = lightSkinCode + (modifierValue - 1)
const shortcode = `skin-tone-${modifierValue}`
reduceObject[shortcode] = `&#${codepoint};`
return reduceObject
}, {} as ShortCodeMap)
export const combinedEmojiData = {
...shortCodeMap,
...emojiSkinToneModifierMap
}
export const emojiShortcodes = Object.keys(combinedEmojiData)

View file

@ -1,27 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { MarkdownRendererExtensionOptions } from '../../../../extensions/base/app-extension'
import { AppExtension } from '../../../../extensions/base/app-extension'
import { RendererType } from '../../../render-page/window-post-message-communicator/rendering-message'
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { ExtractFirstHeadlineEditorExtension } from './extract-first-headline-editor-extension'
import { ExtractFirstHeadlineMarkdownExtension } from './extract-first-headline-markdown-extension'
/**
* Provides first headline extraction
*/
export class ExtractFirstHeadlineAppExtension extends AppExtension {
buildMarkdownRendererExtensions(options: MarkdownRendererExtensionOptions): MarkdownRendererExtension[] {
if (options.rendererType === RendererType.SIMPLE) {
return []
}
return [new ExtractFirstHeadlineMarkdownExtension(options.eventEmitter)]
}
buildEditorExtensionComponent(): React.FC {
return ExtractFirstHeadlineEditorExtension
}
}

View file

@ -1,18 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { updateNoteTitleByFirstHeading } from '../../../../redux/note-details/methods'
import { useExtensionEventEmitterHandler } from '../../hooks/use-extension-event-emitter'
import { ExtractFirstHeadlineNodeProcessor } from './extract-first-headline-node-processor'
import type React from 'react'
/**
* Receives the {@link ExtractFirstHeadlineNodeProcessor.EVENT_NAME first heading extraction event}
* and saves the title in the global application state.
*/
export const ExtractFirstHeadlineEditorExtension: React.FC = () => {
useExtensionEventEmitterHandler(ExtractFirstHeadlineNodeProcessor.EVENT_NAME, updateNoteTitleByFirstHeading)
return null
}

View file

@ -1,17 +0,0 @@
/*
* 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 { EventMarkdownRendererExtension } from '../base/event-markdown-renderer-extension'
import { ExtractFirstHeadlineNodeProcessor } from './extract-first-headline-node-processor'
/**
* Adds first heading extraction to the renderer
*/
export class ExtractFirstHeadlineMarkdownExtension extends EventMarkdownRendererExtension {
buildNodeProcessors(): NodeProcessor[] {
return [new ExtractFirstHeadlineNodeProcessor(this.eventEmitter)]
}
}

View file

@ -1,28 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { NodeProcessor } from '../../node-preprocessors/node-processor'
import { extractFirstHeading } from '@hedgedoc/commons'
import { Optional } from '@mrdrogdrog/optional'
import type { Document } from 'domhandler'
import type { EventEmitter2 } from 'eventemitter2'
/**
* Searches for the first headline tag and extracts its plain text content.
*/
export class ExtractFirstHeadlineNodeProcessor extends NodeProcessor {
public static readonly EVENT_NAME = 'HeadlineExtracted'
constructor(private eventEmitter: EventEmitter2) {
super()
}
process(nodes: Document): Document {
Optional.ofNullable(extractFirstHeading(nodes))
.filter((text) => text !== '')
.ifPresent((text) => this.eventEmitter.emit(ExtractFirstHeadlineNodeProcessor.EVENT_NAME, text))
return nodes
}
}

View file

@ -1,24 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownRendererExtension } from './base/markdown-renderer-extension'
import type MarkdownIt from 'markdown-it'
import anchor from 'markdown-it-anchor'
/**
* Adds headline anchors to the markdown rendering.
*/
export class HeadlineAnchorsMarkdownExtension extends MarkdownRendererExtension {
public configureMarkdownIt(markdownIt: MarkdownIt): void {
anchor(markdownIt, {
permalink: anchor.permalink.ariaHidden({
symbol: '🔗',
class: 'heading-anchor text-dark',
renderHref: (slug: string): string => `#${slug}`,
placement: 'before'
})
})
}
}

View file

@ -1,24 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AppExtension } from '../../../../extensions/base/app-extension'
import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { IframeCapsuleMarkdownExtension } from './iframe-capsule-markdown-extension'
export class IframeCapsuleAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new IframeCapsuleMarkdownExtension()]
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [
{
i18nKey: 'iframeCapsule',
categoryI18nKey: 'embedding'
}
]
}
}

View file

@ -1,21 +0,0 @@
/*
* 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 '../base/markdown-renderer-extension'
import { IframeCapsuleReplacer } from './iframe-capsule-replacer'
/**
* Adds a replacer that capsules iframes in a click shield.
*/
export class IframeCapsuleMarkdownExtension extends MarkdownRendererExtension {
public buildReplacers(): ComponentReplacer[] {
return [new IframeCapsuleReplacer()]
}
public buildTagNameAllowList(): string[] {
return ['iframe']
}
}

View file

@ -1,31 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ClickShield } from '../../replace-components/click-shield/click-shield'
import type { NativeRenderer, NodeReplacement, SubNodeTransform } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import type { Element } from 'domhandler'
import React from 'react'
import { Globe as IconGlobe } from 'react-bootstrap-icons'
/**
* Capsules <iframe> elements with a click shield.
*
* @see ClickShield
*/
export class IframeCapsuleReplacer extends ComponentReplacer {
replace(node: Element, subNodeTransform: SubNodeTransform, nativeRenderer: NativeRenderer): NodeReplacement {
return node.name !== 'iframe' ? (
DO_NOT_REPLACE
) : (
<ClickShield
hoverIcon={IconGlobe}
targetDescription={node.attribs.src}
data-cypress-id={'iframe-capsule-click-shield'}>
{nativeRenderer()}
</ClickShield>
)
}
}

View file

@ -1,34 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ImagePlaceholderMarkdownExtension } from './image-placeholder-markdown-extension'
import type MarkdownIt from 'markdown-it/lib'
/**
* A {@link MarkdownIt.PluginSimple markdown it plugin} that adds the line number of the markdown code to every placeholder image.
*
* @param markdownIt The markdown it instance to which the plugin should be added
*/
export const addLineToPlaceholderImageTags: MarkdownIt.PluginSimple = (markdownIt: MarkdownIt) => {
markdownIt.core.ruler.push('image-placeholder', (state) => {
state.tokens.forEach((token) => {
if (token.type !== 'inline') {
return
}
token.children?.forEach((childToken) => {
if (
childToken.type === 'image' &&
childToken.attrGet('src') === ImagePlaceholderMarkdownExtension.PLACEHOLDER_URL
) {
const line = token.map?.[0]
if (line !== undefined) {
childToken.attrSet('data-line', String(line))
}
}
})
})
return true
})
}

View file

@ -1,42 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Logger } from '../../../../../utils/logger'
import { FileContentFormat, readFile } from '../../../../../utils/read-file'
import { useRendererToEditorCommunicator } from '../../../../editor-page/render-context/renderer-to-editor-communicator-context-provider'
import { CommunicationMessageType } from '../../../../render-page/window-post-message-communicator/rendering-message'
import { useCallback } from 'react'
const log = new Logger('useOnImageUpload')
/**
* Provides a callback that sends a {@link File file} to the editor via iframe communication.
*
* @param lineIndex The index of the line in the markdown content where the placeholder is defined
* @param placeholderIndexInLine The index of the placeholder in the markdown content line
*/
export const useOnImageUpload = (
lineIndex: number | undefined,
placeholderIndexInLine: number | undefined
): ((file: File) => void) => {
const communicator = useRendererToEditorCommunicator()
return useCallback(
(file: File) => {
readFile(file, FileContentFormat.DATA_URL)
.then((dataUri) => {
communicator.sendMessageToOtherSide({
type: CommunicationMessageType.IMAGE_UPLOAD,
dataUri,
fileName: file.name,
lineIndex,
placeholderIndexInLine
})
})
.catch((error: ProgressEvent) => log.error('Error while uploading image', error))
},
[communicator, placeholderIndexInLine, lineIndex]
)
}

View file

@ -1,26 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { calculatePlaceholderContainerSize } from '../utils/build-placeholder-size-css'
import type { CSSProperties } from 'react'
import { useMemo } from 'react'
/**
* Creates the style attribute for a placeholder container with width and height.
*
* @param width The wanted width
* @param height The wanted height
* @return The created style attributes
*/
export const usePlaceholderSizeStyle = (width?: string | number, height?: string | number): CSSProperties => {
return useMemo(() => {
const [convertedWidth, convertedHeight] = calculatePlaceholderContainerSize(width, height)
return {
width: `${convertedWidth}px`,
height: `${convertedHeight}px`
}
}, [height, width])
}

View file

@ -1,37 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { AppExtension } from '../../../../extensions/base/app-extension'
import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
import { basicCompletion } from '../../../editor-page/editor-pane/autocompletions/basic-completion'
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { ImagePlaceholderMarkdownExtension } from './image-placeholder-markdown-extension'
import type { CompletionSource } from '@codemirror/autocomplete'
import { t } from 'i18next'
export class ImagePlaceholderAppExtension extends AppExtension {
buildMarkdownRendererExtensions(): MarkdownRendererExtension[] {
return [new ImagePlaceholderMarkdownExtension()]
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [
{
i18nKey: 'imagePlaceholder'
}
]
}
buildAutocompletion(): CompletionSource[] {
return [
basicCompletion(/(^|\s)!\[?/, '![alt text](https://)', t('editor.autocompletions.image') ?? undefined),
basicCompletion(
/(^|\s)!\[?/,
'![alt text](https:// =200x500)',
t('editor.autocompletions.imageWithDimensions') ?? undefined
)
]
}
}

View file

@ -1,25 +0,0 @@
/*
* 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 '../base/markdown-renderer-extension'
import { addLineToPlaceholderImageTags } from './add-line-to-placeholder-image-tags'
import { ImagePlaceholderReplacer } from './image-placeholder-replacer'
import type MarkdownIt from 'markdown-it/lib'
/**
* Adds support for {@link ImagePlaceholder}.
*/
export class ImagePlaceholderMarkdownExtension extends MarkdownRendererExtension {
public static readonly PLACEHOLDER_URL = 'https://'
configureMarkdownIt(markdownIt: MarkdownIt): void {
addLineToPlaceholderImageTags(markdownIt)
}
buildReplacers(): ComponentReplacer[] {
return [new ImagePlaceholderReplacer()]
}
}

View file

@ -1,44 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { NodeReplacement } from '../../replace-components/component-replacer'
import { ComponentReplacer, DO_NOT_REPLACE } from '../../replace-components/component-replacer'
import { ImagePlaceholder } from './image-placeholder'
import { ImagePlaceholderMarkdownExtension } from './image-placeholder-markdown-extension'
import type { Element } from 'domhandler'
/**
* Replaces every image tag that has the {@link ImagePlaceholderMarkdownExtension.PLACEHOLDER_URL placeholder url} with the {@link ImagePlaceholder image placeholder element}.
*/
export class ImagePlaceholderReplacer extends ComponentReplacer {
private countPerSourceLine = new Map<number, number>()
constructor() {
super()
}
reset(): void {
this.countPerSourceLine = new Map<number, number>()
}
replace(node: Element): NodeReplacement {
if (node.name !== 'img' || node.attribs?.src !== ImagePlaceholderMarkdownExtension.PLACEHOLDER_URL) {
return DO_NOT_REPLACE
}
const lineIndex = Number(node.attribs['data-line'])
const indexInLine = this.countPerSourceLine.get(lineIndex) ?? 0
this.countPerSourceLine.set(lineIndex, indexInLine + 1)
return (
<ImagePlaceholder
alt={node.attribs.alt}
title={node.attribs.title}
width={node.attribs.width}
height={node.attribs.height}
lineIndex={isNaN(lineIndex) ? undefined : lineIndex}
placeholderIndexInLine={indexInLine}
/>
)
}
}

View file

@ -1,21 +0,0 @@
/*!
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
.image-drop {
border: 3px dashed var(--bs-dark);
border-radius: 3px;
transition: background-color 50ms, color 50ms;
.altText {
text-overflow: ellipsis;
flex: 1 1;
overflow: hidden;
width: 100%;
white-space: nowrap;
text-align: center;
}
}

View file

@ -1,114 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { cypressId } from '../../../../utils/cypress-attribute'
import { UiIcon } from '../../../common/icons/ui-icon'
import { acceptedMimeTypes } from '../../../common/upload-image-mimetypes'
import { useOnImageUpload } from './hooks/use-on-image-upload'
import { usePlaceholderSizeStyle } from './hooks/use-placeholder-size-style'
import styles from './image-placeholder.module.scss'
import React, { useCallback, useMemo, useRef, useState } from 'react'
import { Button } from 'react-bootstrap'
import { Upload as IconUpload } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'
export interface PlaceholderImageFrameProps {
alt?: string
title?: string
width?: string | number
height?: string | number
lineIndex?: number
placeholderIndexInLine?: number
}
/**
* Shows a placeholder for an actual image with the possibility to upload images via button or drag'n'drop.
*
* @param alt The alt text of the image. Will be shown in the placeholder
* @param title The title text of the image. Will be shown in the placeholder
* @param width The width of the placeholder
* @param height The height of the placeholder
* @param lineIndex The index of the line in the markdown content where the placeholder is defined
* @param placeholderIndexInLine The index of the placeholder in the markdown line
*/
export const ImagePlaceholder: React.FC<PlaceholderImageFrameProps> = ({
alt,
title,
width,
height,
lineIndex,
placeholderIndexInLine
}) => {
useTranslation()
const fileInputReference = useRef<HTMLInputElement>(null)
const onImageUpload = useOnImageUpload(lineIndex, placeholderIndexInLine)
const [showDragStatus, setShowDragStatus] = useState(false)
const onDropHandler = useCallback(
(event: React.DragEvent<HTMLSpanElement>) => {
event.preventDefault()
if (event?.dataTransfer?.files?.length > 0) {
onImageUpload(event.dataTransfer.files[0])
}
},
[onImageUpload]
)
const onDragOverHandler = useCallback((event: React.DragEvent<HTMLSpanElement>) => {
event.preventDefault()
setShowDragStatus(true)
}, [])
const onDragLeave = useCallback(() => {
setShowDragStatus(false)
}, [])
const onChangeHandler = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const fileList = event.target.files
if (!fileList || fileList.length < 1) {
return
}
onImageUpload(fileList[0])
},
[onImageUpload]
)
const uploadButtonClicked = useCallback(() => fileInputReference.current?.click(), [])
const containerStyle = usePlaceholderSizeStyle(width, height)
const containerDragClasses = useMemo(() => (showDragStatus ? 'bg-primary text-white' : 'text-dark'), [showDragStatus])
return (
<span
{...cypressId('image-placeholder-image-drop')}
className={`${styles['image-drop']} d-inline-flex flex-column align-items-center ${containerDragClasses} p-1`}
style={containerStyle}
onDrop={onDropHandler}
onDragOver={onDragOverHandler}
onDragLeave={onDragLeave}>
<input
type='file'
className='d-none'
accept={acceptedMimeTypes}
onChange={onChangeHandler}
ref={fileInputReference}
/>
<div className={'align-items-center flex-column justify-content-center flex-fill d-flex'}>
<div className={'d-flex flex-column'}>
<span className='my-2'>
<Trans i18nKey={'editor.embeddings.placeholderImage.placeholderText'} />
</span>
<span className={styles['altText']}>{alt ?? title ?? ''}</span>
</div>
</div>
<Button size={'sm'} variant={'primary'} onClick={uploadButtonClicked}>
<UiIcon icon={IconUpload} className='my-2' />
<Trans i18nKey={'editor.embeddings.placeholderImage.upload'} className='my-2' />
</Button>
</span>
)
}

View file

@ -1,66 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { calculatePlaceholderContainerSize, parseSizeNumber } from './build-placeholder-size-css'
describe('parseSizeNumber', () => {
it('undefined', () => {
expect(parseSizeNumber(undefined)).toBe(undefined)
})
it('zero as number', () => {
expect(parseSizeNumber(0)).toBe(0)
})
it('positive number', () => {
expect(parseSizeNumber(234)).toBe(234)
})
it('negative number', () => {
expect(parseSizeNumber(-123)).toBe(-123)
})
it('zero as string', () => {
expect(parseSizeNumber('0')).toBe(0)
})
it('negative number as string', () => {
expect(parseSizeNumber('-123')).toBe(-123)
})
it('positive number as string', () => {
expect(parseSizeNumber('345')).toBe(345)
})
it('positive number with px as string', () => {
expect(parseSizeNumber('456px')).toBe(456)
})
it('negative number with px as string', () => {
expect(parseSizeNumber('-456px')).toBe(-456)
})
})
describe('calculatePlaceholderContainerSize', () => {
it('width undefined | height undefined', () => {
expect(calculatePlaceholderContainerSize(undefined, undefined)).toStrictEqual([500, 200])
})
it('width 200 | height undefined', () => {
expect(calculatePlaceholderContainerSize(200, undefined)).toStrictEqual([200, 80])
})
it('width undefined | height 100', () => {
expect(calculatePlaceholderContainerSize(undefined, 100)).toStrictEqual([250, 100])
})
it('width "0" | height 0', () => {
expect(calculatePlaceholderContainerSize('0', 0)).toStrictEqual([0, 0])
})
it('width 0 | height "0"', () => {
expect(calculatePlaceholderContainerSize(0, '0')).toStrictEqual([0, 0])
})
it('width -345 | height 234', () => {
expect(calculatePlaceholderContainerSize(-345, 234)).toStrictEqual([-345, 234])
})
it('width 345 | height -234', () => {
expect(calculatePlaceholderContainerSize(345, -234)).toStrictEqual([345, -234])
})
it('width "-345" | height -234', () => {
expect(calculatePlaceholderContainerSize('-345', -234)).toStrictEqual([-345, -234])
})
it('width -345 | height "-234"', () => {
expect(calculatePlaceholderContainerSize(-345, '-234')).toStrictEqual([-345, -234])
})
})

View file

@ -1,66 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
const regex = /^(-?[0-9]+)px$/
/**
* Inspects the given value and checks if it is a number or a pixel size string.
*
* @param value The value to check
* @return the number representation of the string or undefined if it couldn't be parsed
*/
export const parseSizeNumber = (value: string | number | undefined): number | undefined => {
if (value === undefined) {
return undefined
}
if (typeof value === 'number') {
return value
}
const regexMatches = regex.exec(value)
if (regexMatches !== null) {
if (regexMatches && regexMatches.length > 1) {
return parseInt(regexMatches[1])
} else {
return undefined
}
}
if (!Number.isNaN(value)) {
return parseInt(value)
}
}
/**
* Calculates the final width and height for a placeholder container.
* Every parameter that is empty will be defaulted using a 500:200 ratio.
*
* @param width The wanted width
* @param height The wanted height
* @return the calculated size
*/
export const calculatePlaceholderContainerSize = (
width: string | number | undefined,
height: string | number | undefined
): [width: number, height: number] => {
const defaultWidth = 500
const defaultHeight = 200
const ratio = defaultWidth / defaultHeight
const convertedWidth = parseSizeNumber(width)
const convertedHeight = parseSizeNumber(height)
if (convertedWidth === undefined && convertedHeight !== undefined) {
return [convertedHeight * ratio, convertedHeight]
} else if (convertedWidth !== undefined && convertedHeight === undefined) {
return [convertedWidth, convertedWidth * (1 / ratio)]
} else if (convertedWidth !== undefined && convertedHeight !== undefined) {
return [convertedWidth, convertedHeight]
} else {
return [defaultWidth, defaultHeight]
}
}

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { MarkdownRendererExtension } from '../_base-classes/markdown-renderer-extension'
import { ProxyImageReplacer } from './proxy-image-replacer'
/**

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { MarkdownRendererExtension } from '../_base-classes/markdown-renderer-extension'
import type { LineMarkers } from './add-line-marker-markdown-it-plugin'
import { addLineMarkerMarkdownItPlugin } from './add-line-marker-markdown-it-plugin'
import { LinemarkerReplacer } from './linemarker-replacer'

View file

@ -5,7 +5,7 @@
*/
import type { NodeProcessor } from '../../node-preprocessors/node-processor'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { MarkdownRendererExtension } from '../_base-classes/markdown-renderer-extension'
import { AnchorNodePreprocessor } from './anchor-node-preprocessor'
import { JumpAnchorReplacer } from './jump-anchor-replacer'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { MarkdownRendererExtension } from '../_base-classes/markdown-renderer-extension'
import type MarkdownIt from 'markdown-it'
import linkify from 'markdown-it/lib/rules_core/linkify'
import tlds from 'tlds'

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { NodeProcessor } from '../../node-preprocessors/node-processor'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { MarkdownRendererExtension } from '../_base-classes/markdown-renderer-extension'
import { RevealCommentCommandNodePreprocessor } from './process-reveal-comment-nodes'
import { addSlideSectionsMarkdownItPlugin } from './reveal-sections'
import type MarkdownIt from 'markdown-it'

View file

@ -1,39 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { MarkdownRendererExtensionOptions } from '../../../../extensions/base/app-extension'
import { AppExtension } from '../../../../extensions/base/app-extension'
import type { CheatsheetExtension } from '../../../editor-page/cheatsheet/cheatsheet-extension'
import { basicCompletion } from '../../../editor-page/editor-pane/autocompletions/basic-completion'
import type { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { TableOfContentsMarkdownExtension } from './table-of-contents-markdown-extension'
import type { CompletionSource } from '@codemirror/autocomplete'
import { t } from 'i18next'
export class TableOfContentsAppExtension extends AppExtension {
buildMarkdownRendererExtensions(options: MarkdownRendererExtensionOptions): MarkdownRendererExtension[] {
return [new TableOfContentsMarkdownExtension(options.eventEmitter)]
}
buildCheatsheetExtensions(): CheatsheetExtension[] {
return [
{
i18nKey: 'toc',
entries: [
{
i18nKey: 'basic'
},
{
i18nKey: 'levelLimit'
}
]
}
]
}
buildAutocompletion(): CompletionSource[] {
return [basicCompletion(/\[(?:t|to|toc)?/, '[toc]', t('editor.autocompletions.toc') ?? undefined)]
}
}

View file

@ -1,37 +0,0 @@
/*
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { tocSlugify } from '../../../editor-page/table-of-contents/toc-slugify'
import { EventMarkdownRendererExtension } from '../base/event-markdown-renderer-extension'
import type { TocAst } from '@hedgedoc/markdown-it-plugins'
import { toc } from '@hedgedoc/markdown-it-plugins'
import equal from 'fast-deep-equal'
import type MarkdownIt from 'markdown-it'
/**
* Adds table of content to the markdown rendering.
*/
export class TableOfContentsMarkdownExtension extends EventMarkdownRendererExtension {
public static readonly EVENT_NAME = 'TocChange'
private lastAst: TocAst | undefined = undefined
public configureMarkdownIt(markdownIt: MarkdownIt): void {
const eventEmitter = this.eventEmitter
if (eventEmitter !== undefined) {
toc(markdownIt, {
listType: 'ul',
level: [1, 2, 3],
callback: (ast: TocAst): void => {
if (equal(ast, this.lastAst)) {
return
}
this.lastAst = ast
eventEmitter.emit(TableOfContentsMarkdownExtension.EVENT_NAME, ast)
},
slugify: tocSlugify
})
}
}
}

View file

@ -3,8 +3,8 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { usePlaceholderSizeStyle } from '../../../../extensions/essential-app-extensions/image-placeholder/hooks/use-placeholder-size-style'
import { UiIcon } from '../../../common/icons/ui-icon'
import { usePlaceholderSizeStyle } from '../image-placeholder/hooks/use-placeholder-size-style'
import React from 'react'
import { GearFill as IconGearFill } from 'react-bootstrap-icons'
import { Trans, useTranslation } from 'react-i18next'

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { MarkdownRendererExtension } from '../base/markdown-renderer-extension'
import { MarkdownRendererExtension } from '../_base-classes/markdown-renderer-extension'
import { UploadIndicatingImageFrameReplacer } from './upload-indicating-image-frame-replacer'
/**

View file

@ -3,10 +3,10 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { allAppExtensions } from '../../../extensions/extra-integrations/all-app-extensions'
import { allAppExtensions } from '../../../extensions/all-app-extensions'
import { useFrontendConfig } from '../../common/frontend-config-context/use-frontend-config'
import type { RendererType } from '../../render-page/window-post-message-communicator/rendering-message'
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
import type { MarkdownRendererExtension } from '../extensions/_base-classes/markdown-renderer-extension'
import { DebuggerMarkdownExtension } from '../extensions/debugger-markdown-extension'
import { ProxyImageMarkdownExtension } from '../extensions/image/proxy-image-markdown-extension'
import { LinkAdjustmentMarkdownExtension } from '../extensions/link-replacer/link-adjustment-markdown-extension'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { MarkdownRendererExtension } from '../../extensions/base/markdown-renderer-extension'
import type { MarkdownRendererExtension } from '../../extensions/_base-classes/markdown-renderer-extension'
import type { Document } from 'domhandler'
import { useMemo } from 'react'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { MarkdownRendererExtension } from '../../extensions/base/markdown-renderer-extension'
import type { MarkdownRendererExtension } from '../../extensions/_base-classes/markdown-renderer-extension'
import MarkdownIt from 'markdown-it/lib'
import { useMemo } from 'react'

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { HtmlToReact } from '../../common/html-to-react/html-to-react'
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
import type { MarkdownRendererExtension } from '../extensions/_base-classes/markdown-renderer-extension'
import { useCombinedNodePreprocessor } from './hooks/use-combined-node-preprocessor'
import { useConfiguredMarkdownIt } from './hooks/use-configured-markdown-it'
import { LineContentToLineIdMapper } from './utils/line-content-to-line-id-mapper'

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { MarkdownRendererExtension } from '../../extensions/base/markdown-renderer-extension'
import { MarkdownRendererExtension } from '../../extensions/_base-classes/markdown-renderer-extension'
import type { NodeProcessor } from '../../node-preprocessors/node-processor'
import type { ComponentReplacer } from '../../replace-components/component-replacer'
import { TestNodeProcessor } from './test-node-processor'

View file

@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { StoreProvider } from '../../../redux/store-provider'
import type { MarkdownRendererExtension } from '../extensions/base/markdown-renderer-extension'
import type { MarkdownRendererExtension } from '../extensions/_base-classes/markdown-renderer-extension'
import { MarkdownToReact } from '../markdown-to-react/markdown-to-react'
import React, { useMemo } from 'react'

View file

@ -5,7 +5,6 @@
*/
import { cypressId } from '../../../../utils/cypress-attribute'
import type { ScrollProps } from '../../../editor-page/synced-scroll/scroll-props'
import { HeadlineAnchorsMarkdownExtension } from '../../../markdown-renderer/extensions/headline-anchors-markdown-extension'
import type { LineMarkers } from '../../../markdown-renderer/extensions/linemarker/add-line-marker-markdown-it-plugin'
import { LinemarkerMarkdownExtension } from '../../../markdown-renderer/extensions/linemarker/linemarker-markdown-extension'
import { useCalculateLineMarkerPosition } from '../../../markdown-renderer/hooks/use-calculate-line-marker-positions'
@ -69,13 +68,7 @@ export const DocumentMarkdownRenderer: React.FC<DocumentMarkdownRendererProps> =
const extensions = useMarkdownExtensions(
baseUrl,
RendererType.DOCUMENT,
useMemo(
() => [
new HeadlineAnchorsMarkdownExtension(),
new LinemarkerMarkdownExtension((values) => (currentLineMarkers.current = values))
],
[]
)
useMemo(() => [new LinemarkerMarkdownExtension((values) => (currentLineMarkers.current = values))], [])
)
useCalculateLineMarkerPosition(markdownBodyRef, currentLineMarkers.current, recalculateLineMarkers)

View file

@ -3,7 +3,7 @@
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { TableOfContentsMarkdownExtension } from '../../../markdown-renderer/extensions/table-of-contents/table-of-contents-markdown-extension'
import { TableOfContentsMarkdownExtension } from '../../../../extensions/essential-app-extensions/table-of-contents/table-of-contents-markdown-extension'
import { useExtensionEventEmitterHandler } from '../../../markdown-renderer/hooks/use-extension-event-emitter'
import styles from './markdown-document.module.scss'
import { WidthBasedTableOfContents } from './width-based-table-of-contents'