Add codemirror keybindings and addons (#311)

* added codemirror addons
- fullScreen
- autorefresh
added a default:
- extraKeys

Co-authored-by: Tilman Vatteroth <tilman.vatteroth@tu-dortmund.de>
Co-authored-by: mrdrogdrog <mr.drogdrog@gmail.com>
Co-authored-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
Philip Molares 2020-08-06 13:43:48 +02:00 committed by GitHub
parent dbce0181a4
commit fc2e2bd592
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 1758 additions and 382 deletions

View file

@ -81,6 +81,7 @@
"react-router-dom": "5.2.0",
"react-scripts": "3.4.1",
"redux": "4.0.5",
"ts-mockery": "^1.2.0",
"typescript": "3.9.7",
"use-media": "1.4.0",
"use-resize-observer": "6.1.0"

View file

@ -204,7 +204,10 @@
"editorToolbar": {
"bold": "Bold",
"italic": "Italic",
"underline": "Underline",
"strikethrough": "Strikethrough",
"subscript": "Subscript",
"superscript": "Superscript",
"header": "Heading",
"code": "Code",
"blockquote": "Blockquote",

View file

@ -1,4 +1,5 @@
@import '../../../../node_modules/codemirror/lib/codemirror.css';
@import '../../../../node_modules/codemirror/addon/display/fullscreen.css';
@import './one-dark.css';
.CodeMirror {

View file

@ -1,4 +1,7 @@
import { Editor } from 'codemirror'
import 'codemirror/addon/comment/comment'
import 'codemirror/addon/display/autorefresh'
import 'codemirror/addon/display/fullscreen'
import 'codemirror/addon/display/placeholder'
import 'codemirror/addon/edit/closebrackets'
import 'codemirror/addon/edit/closetag'
@ -11,11 +14,11 @@ import 'codemirror/addon/search/match-highlighter'
import 'codemirror/addon/selection/active-line'
import 'codemirror/keymap/sublime.js'
import 'codemirror/mode/gfm/gfm.js'
import React, { useCallback, useState } from 'react'
import React, { useState } from 'react'
import { Controlled as ControlledCodeMirror } from 'react-codemirror2'
import { useTranslation } from 'react-i18next'
import './editor-window.scss'
import { Positions, SelectionData } from './interfaces'
import { defaultKeyMap } from './key-map'
import { ToolBar } from './tool-bar/tool-bar'
export interface EditorWindowProps {
@ -25,39 +28,12 @@ export interface EditorWindowProps {
export const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, content }) => {
const { t } = useTranslation()
const [positions, setPositions] = useState<Positions>({
startPosition: {
ch: 0,
line: 0
},
endPosition: {
ch: 0,
line: 0
}
})
const onSelection = useCallback((editor, data: SelectionData) => {
const { anchor, head } = data.ranges[0]
const headFirst = head.line < anchor.line || (head.line === anchor.line && head.ch < anchor.ch)
setPositions({
startPosition: {
line: headFirst ? head.line : anchor.line,
ch: headFirst ? head.ch : anchor.ch
},
endPosition: {
line: headFirst ? anchor.line : head.line,
ch: headFirst ? anchor.ch : head.ch
}
})
}, [])
const [editor, setEditor] = useState<Editor>()
return (
<div className={'d-flex flex-column h-100'}>
<ToolBar
content={content}
onContentChange={onContentChange}
positions={positions}
editor={editor}
/>
<ControlledCodeMirror
className="overflow-hidden w-100 flex-fill"
@ -73,7 +49,6 @@ export const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, con
showCursorWhenSelecting: true,
highlightSelectionMatches: true,
indentUnit: 4,
// continueComments: 'Enter',
inputStyle: 'textarea',
matchBrackets: true,
autoCloseBrackets: true,
@ -87,18 +62,17 @@ export const EditorWindow: React.FC<EditorWindowProps> = ({ onContentChange, con
'authorship-gutters',
'CodeMirror-foldgutter'
],
// extraKeys: this.defaultExtraKeys,
extraKeys: defaultKeyMap,
flattenSpans: true,
addModeClass: true,
// autoRefresh: true,
// otherCursors: true
autoRefresh: true,
// otherCursors: true,
placeholder: t('editor.placeholder')
}}
editorDidMount={mountedEditor => setEditor(mountedEditor)}
onBeforeChange={(editor, data, value) => {
onContentChange(value)
}}
onSelection={onSelection}
/>
</div>
/></div>
)
}

View file

@ -1,15 +0,0 @@
import CodeMirror from 'codemirror'
export interface SelectionData {
ranges: AnchorAndHead[]
}
interface AnchorAndHead {
anchor: CodeMirror.Position
head: CodeMirror.Position
}
export interface Positions {
startPosition: CodeMirror.Position
endPosition: CodeMirror.Position
}

View file

@ -0,0 +1,93 @@
import CodeMirror, { Editor, KeyMap, Pass } from 'codemirror'
import {
makeSelectionBold,
makeSelectionItalic,
markSelection,
strikeThroughSelection,
underlineSelection
} from './tool-bar/utils'
const isMac = navigator.platform.toLowerCase().includes('mac')
const isVim = (keyMapName?: string) => (keyMapName?.substr(0, 3) === 'vim')
const f10 = (editor: Editor): void | typeof Pass => editor.setOption('fullScreen', !editor.getOption('fullScreen'))
const esc = (editor: Editor): void | typeof Pass => {
if (editor.getOption('fullScreen') && !isVim(editor.getOption('keyMap'))) {
editor.setOption('fullScreen', false)
} else {
return CodeMirror.Pass
}
}
const suppressSave = (): undefined => undefined
const tab = (editor: Editor) => {
const tab = '\t'
// contruct x length spaces
const spaces = Array((editor.getOption('indentUnit') ?? 0) + 1).join(' ')
// auto indent whole line when in list or blockquote
const cursor = editor.getCursor()
const line = editor.getLine(cursor.line)
// this regex match the following patterns
// 1. blockquote starts with "> " or ">>"
// 2. unorder list starts with *+-parseInt
// 3. order list starts with "1." or "1)"
const regex = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))/
let match
const multiple = editor.getSelection().split('\n').length > 1 ||
editor.getSelections().length > 1
if (multiple) {
editor.execCommand('defaultTab')
} else if ((match = regex.exec(line)) !== null) {
const ch = match[1].length
const pos = {
line: cursor.line,
ch: ch
}
if (editor.getOption('indentWithTabs')) {
editor.replaceRange(tab, pos, pos, '+input')
} else {
editor.replaceRange(spaces, pos, pos, '+input')
}
} else {
if (editor.getOption('indentWithTabs')) {
editor.execCommand('defaultTab')
} else {
editor.replaceSelection(spaces)
}
}
}
export const defaultKeyMap: KeyMap = !isMac ? {
F10: f10,
Esc: esc,
'Ctrl-S': suppressSave,
Enter: 'newlineAndIndentContinueMarkdownList',
Tab: tab,
Home: 'goLineLeftSmart',
End: 'goLineRight',
'Ctrl-I': makeSelectionItalic,
'Ctrl-B': makeSelectionBold,
'Ctrl-U': underlineSelection,
'Ctrl-D': strikeThroughSelection,
'Ctrl-M': markSelection
} : {
F10: f10,
Esc: esc,
'Cmd-S': suppressSave,
Enter: 'newlineAndIndentContinueMarkdownList',
Tab: tab,
'Cmd-Left': 'goLineLeftSmart',
'Cmd-Right': 'goLineRight',
Home: 'goLineLeftSmart',
End: 'goLineRight',
'Cmd-I': makeSelectionItalic,
'Cmd-B': makeSelectionBold,
'Cmd-U': underlineSelection,
'Cmd-D': strikeThroughSelection,
'Cmd-M': markSelection
}

View file

@ -1,80 +1,97 @@
import { Editor } from 'codemirror'
import React from 'react'
import { Button, ButtonToolbar } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
import { ForkAwesomeIcon } from '../../../common/fork-awesome/fork-awesome-icon'
import { Positions } from '../interfaces'
import './tool-bar.scss'
import { addCodeFences, addHeaderLevel, addLink, addMarkup, addQuotes, createList, replaceSelection } from './utils'
import {
addCodeFences,
addComment,
addHeaderLevel,
addImage,
addLine,
addLink,
addList,
addOrderedList,
addQuotes,
addTable,
addTaskList,
makeSelectionBold,
makeSelectionItalic,
strikeThroughSelection,
subscriptSelection,
superscriptSelection,
underlineSelection
} from './utils'
export interface ToolBarProps {
content: string
onContentChange: (content: string) => void
positions: Positions
editor: Editor | undefined
}
export const ToolBar: React.FC<ToolBarProps> = ({ content, positions, onContentChange }) => {
export const ToolBar: React.FC<ToolBarProps> = ({ editor }) => {
const { t } = useTranslation()
const notImplemented = () => {
alert('This feature is not yet implemented')
}
const makeSelectionBold = () => addMarkup(content, positions.startPosition, positions.endPosition, onContentChange, '**')
const makeSelectionItalic = () => addMarkup(content, positions.startPosition, positions.endPosition, onContentChange, '*')
const strikeThroughSelection = () => addMarkup(content, positions.startPosition, positions.endPosition, onContentChange, '~~')
const addList = () => createList(content, positions.startPosition, positions.endPosition, onContentChange, () => '-')
const addOrderedList = () => createList(content, positions.startPosition, positions.endPosition, onContentChange, j => `${j}.`)
const addTaskList = () => createList(content, positions.startPosition, positions.endPosition, onContentChange, () => '- [ ]')
const addLine = () => replaceSelection(content, positions.startPosition, positions.endPosition, onContentChange, '----')
const addComment = () => replaceSelection(content, positions.startPosition, positions.endPosition, onContentChange, '> []')
const addTable = () => replaceSelection(content, positions.startPosition, positions.endPosition, onContentChange, '| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |')
if (!editor) {
return null
}
return (
<ButtonToolbar className='flex-nowrap bg-light'>
<Button variant='light' onClick={makeSelectionBold} title={t('editor.editorToolbar.bold')}>
<Button variant='light' onClick={() => makeSelectionBold(editor)} title={t('editor.editorToolbar.bold')}>
<ForkAwesomeIcon icon="bold"/>
</Button>
<Button variant='light' onClick={makeSelectionItalic} title={t('editor.editorToolbar.italic')}>
<Button variant='light' onClick={() => makeSelectionItalic(editor)} title={t('editor.editorToolbar.italic')}>
<ForkAwesomeIcon icon="italic"/>
</Button>
<Button variant='light' onClick={strikeThroughSelection} title={t('editor.editorToolbar.strikethrough')}>
<Button variant='light' onClick={() => underlineSelection(editor)} title={t('editor.editorToolbar.underline')}>
<ForkAwesomeIcon icon="underline"/>
</Button>
<Button variant='light' onClick={() => strikeThroughSelection(editor)} title={t('editor.editorToolbar.strikethrough')}>
<ForkAwesomeIcon icon="strikethrough"/>
</Button>
<Button variant='light' onClick={() => addHeaderLevel(content, positions.startPosition, onContentChange)} title={t('editor.editorToolbar.header')}>
<Button variant='light' onClick={() => subscriptSelection(editor)} title={t('editor.editorToolbar.subscript')}>
<ForkAwesomeIcon icon="subscript"/>
</Button>
<Button variant='light' onClick={() => superscriptSelection(editor)} title={t('editor.editorToolbar.superscript')}>
<ForkAwesomeIcon icon="superscript"/>
</Button>
<Button variant='light' onClick={() => addHeaderLevel(editor)} title={t('editor.editorToolbar.header')}>
<ForkAwesomeIcon icon="header"/>
</Button>
<Button variant='light' onClick={() => addCodeFences(content, positions.startPosition, positions.endPosition, onContentChange)} title={t('editor.editorToolbar.code')}>
<Button variant='light' onClick={() => addCodeFences(editor)} title={t('editor.editorToolbar.code')}>
<ForkAwesomeIcon icon="code"/>
</Button>
<Button variant='light' onClick={() => addQuotes(content, positions.startPosition, positions.endPosition, onContentChange)} title={t('editor.editorToolbar.blockquote')}>
<Button variant='light' onClick={() => addQuotes(editor)} title={t('editor.editorToolbar.blockquote')}>
<ForkAwesomeIcon icon="quote-right"/>
</Button>
<Button variant='light' onClick={addList} title={t('editor.editorToolbar.unorderedList')}>
<Button variant='light' onClick={() => addList(editor)} title={t('editor.editorToolbar.unorderedList')}>
<ForkAwesomeIcon icon="list"/>
</Button>
<Button variant='light' onClick={addOrderedList} title={t('editor.editorToolbar.orderedList')}>
<Button variant='light' onClick={() => addOrderedList(editor)} title={t('editor.editorToolbar.orderedList')}>
<ForkAwesomeIcon icon="list-ol"/>
</Button>
<Button variant='light' onClick={addTaskList} title={t('editor.editorToolbar.checkList')}>
<Button variant='light' onClick={() => addTaskList(editor)} title={t('editor.editorToolbar.checkList')}>
<ForkAwesomeIcon icon="check-square"/>
</Button>
<Button variant='light' onClick={() => addLink(content, positions.startPosition, positions.endPosition, onContentChange)} title={t('editor.editorToolbar.link')}>
<Button variant='light' onClick={() => addLink(editor)} title={t('editor.editorToolbar.link')}>
<ForkAwesomeIcon icon="link"/>
</Button>
<Button variant='light' onClick={() => addLink(content, positions.startPosition, positions.endPosition, onContentChange, '!')} title={t('editor.editorToolbar.image')}>
<Button variant='light' onClick={() => addImage(editor)} title={t('editor.editorToolbar.image')}>
<ForkAwesomeIcon icon="picture-o"/>
</Button>
<Button variant='light' onClick={notImplemented} title={t('editor.editorToolbar.uploadImage')}>
<ForkAwesomeIcon icon="upload"/>
</Button>
<Button variant='light' onClick={addTable} title={t('editor.editorToolbar.table')}>
<Button variant='light' onClick={() => addTable(editor)} title={t('editor.editorToolbar.table')}>
<ForkAwesomeIcon icon="table"/>
</Button>
<Button variant='light' onClick={addLine} title={t('editor.editorToolbar.line')}>
<Button variant='light' onClick={() => addLine(editor)} title={t('editor.editorToolbar.line')}>
<ForkAwesomeIcon icon="minus"/>
</Button>
<Button variant='light' onClick={addComment} title={t('editor.editorToolbar.comment')}>
<Button variant='light' onClick={() => addComment(editor)} title={t('editor.editorToolbar.comment')}>
<ForkAwesomeIcon icon="comment"/>
</Button>
</ButtonToolbar>

File diff suppressed because it is too large Load diff

View file

@ -1,104 +1,100 @@
import CodeMirror from 'codemirror'
import { Editor } from 'codemirror'
export const replaceSelection = (content: string, startPosition: CodeMirror.Position, endPosition: CodeMirror.Position, onContentChange: (content: string) => void, replaceText: string): void => {
const contentLines = content.split('\n')
const replaceTextLines = replaceText.split('\n')
const numberOfExtraLines = replaceTextLines.length - 1 - (endPosition.line - startPosition.line)
const replaceTextIncludeNewline = replaceText.includes('\n')
if (!replaceTextIncludeNewline) {
contentLines[startPosition.line] = contentLines[startPosition.line].slice(0, startPosition.ch) + replaceText + contentLines[startPosition.line].slice(endPosition.ch)
} else {
const lastPart = contentLines[endPosition.line].slice(endPosition.ch)
contentLines.push(...contentLines.slice(endPosition.line + 1))
contentLines[startPosition.line] = contentLines[startPosition.line].slice(0, startPosition.ch) + replaceTextLines[0]
contentLines.splice(startPosition.line + 1, replaceTextLines.length - 1, ...replaceTextLines.slice(1))
contentLines[numberOfExtraLines + endPosition.line] += lastPart
}
onContentChange(contentLines.join('\n'))
}
export const makeSelectionBold = (editor: Editor): void => wrapTextWith(editor, '**')
export const makeSelectionItalic = (editor: Editor): void => wrapTextWith(editor, '*')
export const strikeThroughSelection = (editor: Editor): void => wrapTextWith(editor, '~~')
export const underlineSelection = (editor: Editor): void => wrapTextWith(editor, '++')
export const subscriptSelection = (editor: Editor): void => wrapTextWith(editor, '~')
export const superscriptSelection = (editor: Editor): void => wrapTextWith(editor, '^')
export const markSelection = (editor: Editor): void => wrapTextWith(editor, '==')
export const extractSelection = (content: string, startPosition: CodeMirror.Position, endPosition: CodeMirror.Position): string => {
if (startPosition.line === endPosition.line && startPosition.ch === endPosition.ch) {
return ''
}
export const addHeaderLevel = (editor: Editor): void => changeLines(editor, line => line.startsWith('#') ? `#${line}` : `# ${line}`)
export const addCodeFences = (editor: Editor): void => wrapTextWith(editor, '```\n', '\n```')
export const addQuotes = (editor: Editor): void => insertOnStartOfLines(editor, '> ')
const lines = content.split('\n')
export const addList = (editor: Editor): void => createList(editor, () => '- ')
export const addOrderedList = (editor: Editor): void => createList(editor, j => `${j}. `)
export const addTaskList = (editor: Editor): void => createList(editor, () => '- [ ] ')
if (startPosition.line === endPosition.line) {
return removeLastNewLine(lines[startPosition.line].slice(startPosition.ch, endPosition.ch))
}
export const addImage = (editor: Editor): void => addLink(editor, '!')
let multiLineSelection = lines[startPosition.line].slice(startPosition.ch) + '\n'
for (let i = startPosition.line + 1; i <= endPosition.line; i++) {
if (i === endPosition.line) {
multiLineSelection += lines[i].slice(0, endPosition.ch)
} else {
multiLineSelection += lines[i] + '\n'
}
}
return multiLineSelection
}
export const addLine = (editor: Editor): void => changeLines(editor, line => `${line}\n----`)
export const addComment = (editor: Editor): void => changeLines(editor, line => `${line}\n> []`)
export const addTable = (editor: Editor): void => changeLines(editor, line => `${line}\n| Column 1 | Column 2 | Column 3 |\n| -------- | -------- | -------- |\n| Text | Text | Text |`)
export const removeLastNewLine = (selection: string): string => {
if (selection.endsWith('\n')) {
selection = selection.slice(0, -1)
}
return selection
}
export const addMarkup = (content: string, startPosition: CodeMirror.Position, endPosition: CodeMirror.Position, onContentChange: (content: string) => void, markUp: string): void => {
const selection = extractSelection(content, startPosition, endPosition)
if (selection === '') {
export const wrapTextWith = (editor: Editor, symbol: string, endSymbol?: string): void => {
if (!editor.getSelection()) {
return
}
replaceSelection(content, startPosition, endPosition, onContentChange, `${markUp}${selection}${markUp}`)
}
export const createList = (content: string, startPosition: CodeMirror.Position, endPosition: CodeMirror.Position, onContentChange: (content: string) => void, listMark: (j: number) => string): void => {
const lines = content.split('\n')
let j = 1
for (let i = startPosition.line; i <= endPosition.line; i++) {
lines[i] = `${listMark(j)} ${lines[i]}`
j++
}
onContentChange(lines.join('\n'))
}
export const addHeaderLevel = (content: string, startPosition: CodeMirror.Position, onContentChange: (content: string) => void): void => {
const lines = content.split('\n')
const startLine = lines[startPosition.line]
const isHeadingAlready = startLine.startsWith('#')
lines[startPosition.line] = `#${!isHeadingAlready ? ' ' : ''}${startLine}`
onContentChange(lines.join('\n'))
}
export const addLink = (content: string, startPosition: CodeMirror.Position, endPosition: CodeMirror.Position, onContentChange: (content: string) => void, prefix?: string): void => {
const selection = extractSelection(content, startPosition, endPosition)
const linkRegex = /^(?:https?|ftp|mailto):/
if (linkRegex.exec(selection)) {
replaceSelection(content, startPosition, endPosition, onContentChange, `${prefix || ''}[](${selection})`)
} else {
replaceSelection(content, startPosition, endPosition, onContentChange, `${prefix || ''}[${selection}](https://)`)
}
}
export const addQuotes = (content: string, startPosition: CodeMirror.Position, endPosition: CodeMirror.Position, onContentChange: (content: string) => void): void => {
const selection = extractSelection(content, startPosition, endPosition)
if (selection === '') {
replaceSelection(content, startPosition, endPosition, onContentChange, '> ')
} else if (!selection.includes('\n')) {
const lines = content.split('\n')
replaceSelection(content, startPosition, endPosition, onContentChange, '> ' + lines[startPosition.line])
} else {
const lines = content.split('\n')
for (let i = startPosition.line; i <= endPosition.line; i++) {
lines[i] = `> ${lines[i]}`
const ranges = editor.listSelections()
for (const range of ranges) {
if (range.empty()) {
continue
}
onContentChange(lines.join('\n'))
const from = range.from()
const to = range.to()
const selection = editor.getRange(from, to)
editor.replaceRange(symbol + selection + (endSymbol || symbol), from, to, '+input')
range.head.ch += symbol.length
range.anchor.ch += endSymbol ? endSymbol.length : symbol.length
}
editor.setSelections(ranges)
}
export const addCodeFences = (content: string, startPosition: CodeMirror.Position, endPosition: CodeMirror.Position, onContentChange: (content: string) => void): void => {
const selection = extractSelection(content, startPosition, endPosition)
replaceSelection(content, startPosition, endPosition, onContentChange, `\`\`\`\n${selection}\n\`\`\``)
export const insertOnStartOfLines = (editor: Editor, symbol: string): void => {
const cursor = editor.getCursor()
const ranges = editor.listSelections()
for (const range of ranges) {
const from = range.empty() ? { line: cursor.line, ch: 0 } : range.from()
const to = range.empty() ? { line: cursor.line, ch: editor.getLine(cursor.line).length } : range.to()
const selection = editor.getRange(from, to)
const lines = selection.split('\n')
editor.replaceRange(lines.map(line => `${symbol}${line}`).join('\n'), from, to, '+input')
}
editor.setSelections(ranges)
}
export const changeLines = (editor: Editor, replaceFunction: (line: string) => string): void => {
const cursor = editor.getCursor()
const ranges = editor.listSelections()
for (const range of ranges) {
const lineNumber = range.empty() ? cursor.line : range.from().line
const line = editor.getLine(lineNumber)
editor.replaceRange(replaceFunction(line), { line: lineNumber, ch: 0 }, {
line: lineNumber,
ch: line.length
}, '+input')
}
editor.setSelections(ranges)
}
export const createList = (editor: Editor, listMark: (i: number) => string): void => {
const cursor = editor.getCursor()
const ranges = editor.listSelections()
for (const range of ranges) {
const from = range.empty() ? { line: cursor.line, ch: 0 } : range.from()
const to = range.empty() ? { line: cursor.line, ch: editor.getLine(cursor.line).length } : range.to()
const selection = editor.getRange(from, to)
const lines = selection.split('\n')
editor.replaceRange(lines.map((line, i) => `${listMark(i + 1)}${line}`).join('\n'), from, to, '+input')
}
editor.setSelections(ranges)
}
export const addLink = (editor: Editor, prefix?: string): void => {
const cursor = editor.getCursor()
const ranges = editor.listSelections()
for (const range of ranges) {
const from = range.empty() ? { line: cursor.line, ch: cursor.ch } : range.from()
const to = range.empty() ? { line: cursor.line, ch: cursor.ch } : range.to()
const selection = editor.getRange(from, to)
const linkRegex = /^(?:https?|ftp|mailto):/
if (linkRegex.exec(selection)) {
editor.replaceRange(`${prefix || ''}[](${selection})`, from, to, '+input')
} else {
editor.replaceRange(`${prefix || ''}[${selection}](https://)`, from, to, '+input')
}
}
}

View file

@ -9,6 +9,10 @@ opengraph:
# Embedding demo
[TOC]
## some plain text
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
## MathJax
You can render *LaTeX* mathematical expressions using **MathJax**, as on [math.stackexchange.com](https://math.stackexchange.com/):

View file

@ -0,0 +1,8 @@
import 'codemirror'
declare module 'codemirror' {
// noinspection JSUnusedGlobalSymbols
interface EditorConfiguration {
fullScreen?: boolean;
}
}

View file

@ -12254,6 +12254,11 @@ ts-loader@8.0.2:
micromatch "^4.0.0"
semver "^6.0.0"
ts-mockery@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/ts-mockery/-/ts-mockery-1.2.0.tgz#aa76521653d729e99b3808836817f64e61a213dd"
integrity sha512-ArGPMUzO4H25KBYVTWmmE36y5bCOFAwC7XdW4CLTqYg+gQcvxJzKoj5URSc+luzwI8QdtwAkHtazBmrKepX81g==
ts-pnp@1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.1.6.tgz#389a24396d425a0d3162e96d2b4638900fdc289a"