Move toolbar functionality from redux to codemirror dispatch (#2083)

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-06-08 01:10:49 +02:00 committed by GitHub
parent a8bd22aef3
commit e93607c96e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
99 changed files with 1730 additions and 1721 deletions

View file

@ -14,11 +14,6 @@ export enum EditorConfigActionType {
SET_SMART_PASTE = 'editor/preferences/setSmartPaste'
}
export interface CursorSelection {
from: number
to?: number
}
export interface EditorConfig {
editorMode: EditorMode
syncScroll: boolean

View file

@ -1,221 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Mock } from 'ts-mockery'
import * as wrapSelectionModule from './formatters/wrap-selection'
import { applyFormatTypeToMarkdownLines } from './apply-format-type-to-markdown-lines'
import type { CursorSelection } from '../../editor/types'
import { FormatType } from '../types'
import * as changeCursorsToWholeLineIfNoToCursorModule from './formatters/utils/change-cursors-to-whole-line-if-no-to-cursor'
import * as prependLinesOfSelectionModule from './formatters/prepend-lines-of-selection'
import * as replaceSelectionModule from './formatters/replace-selection'
import * as addLinkModule from './formatters/add-link'
describe('apply format type to markdown lines', () => {
Mock.configure('jest')
const markdownContentMock = 'input'
const cursorSelectionMock = Mock.of<CursorSelection>()
const wrapSelectionMock = jest.spyOn(wrapSelectionModule, 'wrapSelection')
const wrapSelectionMockResponse = Mock.of<[string, CursorSelection]>()
const changeCursorsToWholeLineIfNoToCursorMock = jest.spyOn(
changeCursorsToWholeLineIfNoToCursorModule,
'changeCursorsToWholeLineIfNoToCursor'
)
const changeCursorsToWholeLineIfNoToCursorMockResponse = Mock.of<CursorSelection>()
const prependLinesOfSelectionMock = jest.spyOn(prependLinesOfSelectionModule, 'prependLinesOfSelection')
const replaceSelectionMock = jest.spyOn(replaceSelectionModule, 'replaceSelection')
const replaceSelectionMockResponse = Mock.of<[string, CursorSelection]>()
const addLinkMock = jest.spyOn(addLinkModule, 'addLink')
const addLinkMockResponse = Mock.of<[string, CursorSelection]>()
beforeAll(() => {
wrapSelectionMock.mockReturnValue(wrapSelectionMockResponse)
changeCursorsToWholeLineIfNoToCursorMock.mockReturnValue(changeCursorsToWholeLineIfNoToCursorMockResponse)
prependLinesOfSelectionMock.mockImplementation(
(
markdownContent: string,
selection: CursorSelection,
generatePrefix: (line: string, lineIndexInBlock: number) => string
): [string, CursorSelection] => {
return [generatePrefix(markdownContent, 0) + markdownContent, selection]
}
)
replaceSelectionMock.mockReturnValue(replaceSelectionMockResponse)
addLinkMock.mockReturnValue(addLinkMockResponse)
})
afterAll(() => {
jest.resetAllMocks()
})
it('can process the format type bold', () => {
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.BOLD)
expect(result).toBe(wrapSelectionMockResponse)
expect(wrapSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '**', '**')
})
it('can process the format type italic', () => {
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.ITALIC)
expect(result).toBe(wrapSelectionMockResponse)
expect(wrapSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '*', '*')
})
it('can process the format type strikethrough', () => {
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.STRIKETHROUGH)
expect(result).toBe(wrapSelectionMockResponse)
expect(wrapSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '~~', '~~')
})
it('can process the format type underline', () => {
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.UNDERLINE)
expect(result).toBe(wrapSelectionMockResponse)
expect(wrapSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '++', '++')
})
it('can process the format type subscript', () => {
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.SUBSCRIPT)
expect(result).toBe(wrapSelectionMockResponse)
expect(wrapSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '~', '~')
})
it('can process the format type superscript', () => {
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.SUPERSCRIPT)
expect(result).toBe(wrapSelectionMockResponse)
expect(wrapSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '^', '^')
})
it('can process the format type highlight', () => {
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.HIGHLIGHT)
expect(result).toBe(wrapSelectionMockResponse)
expect(wrapSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '==', '==')
})
it('can process the format type code fence', () => {
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.CODE_FENCE)
expect(result).toBe(wrapSelectionMockResponse)
expect(changeCursorsToWholeLineIfNoToCursorMock).toBeCalledWith(markdownContentMock, cursorSelectionMock)
expect(wrapSelectionMock).toBeCalledWith(
markdownContentMock,
changeCursorsToWholeLineIfNoToCursorMockResponse,
'```\n',
'\n```'
)
})
it('can process the format type unordered list', () => {
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.UNORDERED_LIST)
expect(result).toEqual(['- input', cursorSelectionMock])
expect(prependLinesOfSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, expect.anything())
})
it('can process the format type ordered list', () => {
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.ORDERED_LIST)
expect(result).toEqual(['1. input', cursorSelectionMock])
expect(prependLinesOfSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, expect.anything())
})
it('can process the format type check list', () => {
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.CHECK_LIST)
expect(result).toEqual(['- [ ] input', cursorSelectionMock])
expect(prependLinesOfSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, expect.anything())
})
it('can process the format type quotes', () => {
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.QUOTES)
expect(result).toEqual(['> input', cursorSelectionMock])
expect(prependLinesOfSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, expect.anything())
})
it('can process the format type horizontal line with only from cursor', () => {
const randomCursorPosition = 138743857
const result = applyFormatTypeToMarkdownLines(
markdownContentMock,
{ from: randomCursorPosition },
FormatType.HORIZONTAL_LINE
)
expect(result).toEqual(replaceSelectionMockResponse)
expect(replaceSelectionMock).toBeCalledWith(markdownContentMock, { from: randomCursorPosition }, `\n----`)
})
it('can process the format type horizontal line with from and to cursor', () => {
const fromCursor = Math.random()
const toCursor = Math.random()
const result = applyFormatTypeToMarkdownLines(
markdownContentMock,
{ from: fromCursor, to: toCursor },
FormatType.HORIZONTAL_LINE
)
expect(result).toEqual(replaceSelectionMockResponse)
expect(replaceSelectionMock).toBeCalledWith(markdownContentMock, { from: toCursor }, `\n----`)
})
it('can process the format type comment with only from cursor', () => {
const fromCursor = Math.random()
const result = applyFormatTypeToMarkdownLines(markdownContentMock, { from: fromCursor }, FormatType.COMMENT)
expect(result).toEqual(replaceSelectionMockResponse)
expect(replaceSelectionMock).toBeCalledWith(markdownContentMock, { from: fromCursor }, `\n> []`)
})
it('can process the format type comment with from and to cursor', () => {
const fromCursor = 0
const toCursor = 1
const result = applyFormatTypeToMarkdownLines(
markdownContentMock,
{ from: fromCursor, to: toCursor },
FormatType.COMMENT
)
expect(result).toEqual(replaceSelectionMockResponse)
expect(replaceSelectionMock).toBeCalledWith(markdownContentMock, { from: toCursor }, `\n> []`)
})
it('can process the format type collapsible block', () => {
const result = applyFormatTypeToMarkdownLines(
markdownContentMock,
cursorSelectionMock,
FormatType.COLLAPSIBLE_BLOCK
)
expect(result).toBe(wrapSelectionMockResponse)
expect(changeCursorsToWholeLineIfNoToCursorMock).toBeCalledWith(markdownContentMock, cursorSelectionMock)
expect(wrapSelectionMock).toBeCalledWith(
markdownContentMock,
changeCursorsToWholeLineIfNoToCursorMockResponse,
':::spoiler Toggle label\n',
'\n:::'
)
})
it('can process the format type header level with existing level', () => {
const inputLines = '# text'
const result = applyFormatTypeToMarkdownLines(inputLines, cursorSelectionMock, FormatType.HEADER_LEVEL)
expect(result).toEqual(['## text', cursorSelectionMock])
expect(prependLinesOfSelectionMock).toBeCalledWith(inputLines, cursorSelectionMock, expect.anything())
})
it('can process the format type link', () => {
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.LINK)
expect(result).toEqual(addLinkMockResponse)
expect(addLinkMock).toBeCalledWith(markdownContentMock, cursorSelectionMock)
})
it('can process the format type image link', () => {
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.IMAGE_LINK)
expect(result).toEqual(addLinkMockResponse)
expect(addLinkMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '!')
})
it('can process an unknown format type ', () => {
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, 'UNKNOWN' as FormatType)
expect(result).toEqual([markdownContentMock, cursorSelectionMock])
})
})

View file

@ -1,74 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { FormatType } from '../types'
import { wrapSelection } from './formatters/wrap-selection'
import { addLink } from './formatters/add-link'
import { prependLinesOfSelection } from './formatters/prepend-lines-of-selection'
import type { CursorSelection } from '../../editor/types'
import { changeCursorsToWholeLineIfNoToCursor } from './formatters/utils/change-cursors-to-whole-line-if-no-to-cursor'
import { replaceSelection } from './formatters/replace-selection'
export const applyFormatTypeToMarkdownLines = (
markdownContent: string,
selection: CursorSelection,
type: FormatType
): [string, CursorSelection] => {
switch (type) {
case FormatType.BOLD:
return wrapSelection(markdownContent, selection, '**', '**')
case FormatType.ITALIC:
return wrapSelection(markdownContent, selection, '*', '*')
case FormatType.STRIKETHROUGH:
return wrapSelection(markdownContent, selection, '~~', '~~')
case FormatType.UNDERLINE:
return wrapSelection(markdownContent, selection, '++', '++')
case FormatType.SUBSCRIPT:
return wrapSelection(markdownContent, selection, '~', '~')
case FormatType.SUPERSCRIPT:
return wrapSelection(markdownContent, selection, '^', '^')
case FormatType.HIGHLIGHT:
return wrapSelection(markdownContent, selection, '==', '==')
case FormatType.CODE_FENCE:
return wrapSelection(
markdownContent,
changeCursorsToWholeLineIfNoToCursor(markdownContent, selection),
'```\n',
'\n```'
)
case FormatType.UNORDERED_LIST:
return prependLinesOfSelection(markdownContent, selection, () => `- `)
case FormatType.ORDERED_LIST:
return prependLinesOfSelection(
markdownContent,
selection,
(line, lineIndexInBlock) => `${lineIndexInBlock + 1}. `
)
case FormatType.CHECK_LIST:
return prependLinesOfSelection(markdownContent, selection, () => `- [ ] `)
case FormatType.QUOTES:
return prependLinesOfSelection(markdownContent, selection, () => `> `)
case FormatType.HEADER_LEVEL:
return prependLinesOfSelection(markdownContent, selection, (line) => (line.startsWith('#') ? `#` : `# `))
case FormatType.HORIZONTAL_LINE:
return replaceSelection(markdownContent, { from: selection.to ?? selection.from }, '\n----')
case FormatType.COMMENT:
return replaceSelection(markdownContent, { from: selection.to ?? selection.from }, '\n> []')
case FormatType.COLLAPSIBLE_BLOCK:
return wrapSelection(
markdownContent,
changeCursorsToWholeLineIfNoToCursor(markdownContent, selection),
':::spoiler Toggle label\n',
'\n:::'
)
case FormatType.LINK:
return addLink(markdownContent, selection)
case FormatType.IMAGE_LINK:
return addLink(markdownContent, selection, '!')
default:
return [markdownContent, selection]
}
}

View file

@ -1,86 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { addLink } from './add-link'
describe('add link', () => {
describe('without to-cursor', () => {
it('inserts a link', () => {
const actual = addLink('', { from: 0 }, '')
expect(actual).toEqual(['[](https://)', { from: 0, to: 12 }])
})
it('inserts a link into a line', () => {
const actual = addLink('aa', { from: 1 }, '')
expect(actual).toEqual(['a[](https://)a', { from: 1, to: 13 }])
})
it('inserts a link with a prefix', () => {
const actual = addLink('', { from: 0 }, 'prefix')
expect(actual).toEqual(['prefix[](https://)', { from: 0, to: 18 }])
})
})
describe('with a normal text selected', () => {
it('wraps the selection', () => {
const actual = addLink(
'a',
{
from: 0,
to: 1
},
''
)
expect(actual).toEqual(['[a](https://)', { from: 0, to: 13 }])
})
it('wraps the selection inside of a line', () => {
const actual = addLink('aba', { from: 1, to: 2 }, '')
expect(actual).toEqual(['a[b](https://)a', { from: 1, to: 14 }])
})
it('wraps the selection with a prefix', () => {
const actual = addLink('a', { from: 0, to: 1 }, 'prefix')
expect(actual).toEqual(['prefix[a](https://)', { from: 0, to: 19 }])
})
it('wraps a multi line selection', () => {
const actual = addLink('a\nb\nc', { from: 0, to: 5 }, '')
expect(actual).toEqual(['[a\nb\nc](https://)', { from: 0, to: 17 }])
})
})
describe('with a url selected', () => {
it('wraps the selection', () => {
const actual = addLink(
'https://google.com',
{
from: 0,
to: 18
},
''
)
expect(actual).toEqual(['[](https://google.com)', { from: 0, to: 22 }])
})
it('wraps the selection with a prefix', () => {
const actual = addLink(
'https://google.com',
{
from: 0,
to: 18
},
'prefix'
)
expect(actual).toEqual(['prefix[](https://google.com)', { from: 0, to: 28 }])
})
it(`wraps a multi line selection not as link`, () => {
const actual = addLink('a\nhttps://google.com\nc', { from: 0, to: 22 }, '')
expect(actual).toEqual(['[a\nhttps://google.com\nc](https://)', { from: 0, to: 34 }])
})
})
})

View file

@ -1,44 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { stringSplice } from './utils/string-splice'
import type { CursorSelection } from '../../../editor/types'
const beforeDescription = '['
const afterDescriptionBeforeLink = ']('
const defaultUrl = 'https://'
const afterLink = ')'
/**
* Creates a copy of the given markdown content lines but inserts a new link tag.
*
* @param markdownContent The content of the document to modify
* @param selection If the selection has no to cursor then the tag will be inserted at this position.
* If the selection has a to cursor then the selected text will be inserted into the description or the URL part.
* @param prefix An optional prefix for the link
* @return the modified copy of lines
*/
export const addLink = (
markdownContent: string,
selection: CursorSelection,
prefix = ''
): [string, CursorSelection] => {
const from = selection.from
const to = selection.to ?? from
const selectedText = markdownContent.slice(from, to)
const link = buildLink(selectedText, prefix)
const newContent = stringSplice(markdownContent, selection.from, link, selectedText.length)
return [newContent, { from, to: from + link.length }]
}
const buildLink = (selectedText: string, prefix: string): string => {
const linkRegex = /^(?:https?|mailto):/
if (linkRegex.test(selectedText)) {
return prefix + beforeDescription + afterDescriptionBeforeLink + selectedText + afterLink
} else {
return prefix + beforeDescription + selectedText + afterDescriptionBeforeLink + defaultUrl + afterLink
}
}

View file

@ -1,44 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { prependLinesOfSelection } from './prepend-lines-of-selection'
describe('replace lines of selection', () => {
it('replaces only the from-cursor line if no to-cursor is present', () => {
const actual = prependLinesOfSelection(
'a\nb\nc',
{
from: 2
},
(line, lineIndexInBlock) => `text_${lineIndexInBlock}_`
)
expect(actual).toStrictEqual(['a\ntext_0_b\nc', { from: 2, to: 10 }])
})
it('replaces only one line if from-cursor and to-cursor are in the same line', () => {
const actual = prependLinesOfSelection(
'a\nb\nc',
{
from: 2,
to: 2
},
(line, lineIndexInBlock) => `text_${lineIndexInBlock}_`
)
expect(actual).toStrictEqual(['a\ntext_0_b\nc', { from: 2, to: 10 }])
})
it('replaces multiple lines', () => {
const actual = prependLinesOfSelection(
'a\nb\nc\nd\ne',
{
from: 2,
to: 6
},
(line, lineIndexInBlock) => `text_${lineIndexInBlock}_`
)
expect(actual).toEqual(['a\ntext_0_b\ntext_1_c\ntext_2_d\ne', { from: 2, to: 28 }])
})
})

View file

@ -1,49 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { CursorSelection } from '../../../editor/types'
import { searchForEndOfLine, searchForStartOfLine } from './utils/change-cursors-to-whole-line-if-no-to-cursor'
import { stringSplice } from './utils/string-splice'
/**
* Creates a copy of the given markdown content lines but modifies the whole selected lines.
*
* @param markdownContentLines The lines of the document to modify
* @param selection If the selection has no to cursor then only the from line will be modified.
* If the selection has a to cursor then all lines in the selection will be modified.
* @param replacer A function that modifies the selected lines
* @return the modified copy of lines
*/
export const prependLinesOfSelection = (
markdownContentLines: string,
selection: CursorSelection,
generatePrefix: (line: string, lineIndexInBlock: number) => string
): [string, CursorSelection] => {
let currentContent = markdownContentLines
let toIndex = selection.to ?? selection.from
let currentIndex = selection.from
let indexInBlock = 0
let newStartOfSelection = selection.from
let newEndOfSelection = toIndex
while (currentIndex <= toIndex && currentIndex < currentContent.length) {
const startOfLine = searchForStartOfLine(currentContent, currentIndex)
if (startOfLine < newStartOfSelection) {
newStartOfSelection = startOfLine
}
const endOfLine = searchForEndOfLine(currentContent, currentIndex)
const line = currentContent.slice(startOfLine, endOfLine)
const replacement = generatePrefix(line, indexInBlock)
indexInBlock += 1
currentContent = stringSplice(currentContent, startOfLine, replacement)
toIndex += replacement.length
const newEndOfLine = endOfLine + replacement.length
currentIndex = newEndOfLine + 1
if (newEndOfLine > newEndOfSelection) {
newEndOfSelection = newEndOfLine
}
}
return [currentContent, { from: newStartOfSelection, to: newEndOfSelection }]
}

View file

@ -1,56 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { replaceSelection } from './replace-selection'
describe('replace selection', () => {
it('inserts a text after the from-cursor if no to-cursor is present', () => {
const actual = replaceSelection(
'text1',
{
from: 2
},
'text2'
)
expect(actual).toEqual(['tetext2xt1', { from: 2, to: 7 }])
})
it('inserts a text if from-cursor and to-cursor are the same', () => {
const actual = replaceSelection(
'text1',
{
from: 2,
to: 2
},
'text2'
)
expect(actual).toEqual(['tetext2xt1', { from: 2, to: 7 }])
})
it('replaces a single line text', () => {
const actual = replaceSelection(
'text1\ntext2\ntext3',
{
from: 7,
to: 8
},
'text4'
)
expect(actual).toEqual(['text1\nttext4xt2\ntext3', { from: 7, to: 12 }])
})
it('replaces a multi line text', () => {
const actual = replaceSelection(
'text1\ntext2\ntext3',
{
from: 2,
to: 15
},
'text4'
)
expect(actual).toEqual(['tetext4t3', { from: 2, to: 7 }])
})
})

View file

@ -1,29 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { stringSplice } from './utils/string-splice'
import type { CursorSelection } from '../../../editor/types'
/**
* Creates a new {@link NoteDetails note state} but replaces the selected text.
*
* @param markdownContent The content of the document to modify
* @param selection If the selection has no to cursor then text will only be inserted.
* If the selection has a to cursor then the selection will be replaced.
* @param insertText The text that should be inserted
* @return The modified state
*/
export const replaceSelection = (
markdownContent: string,
selection: CursorSelection,
insertText: string
): [string, CursorSelection] => {
const fromCursor = selection.from
const toCursor = selection.to ?? selection.from
const newContent = stringSplice(markdownContent, fromCursor, insertText, toCursor - fromCursor)
return [newContent, { from: fromCursor, to: insertText.length + fromCursor }]
}

View file

@ -1,95 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import {
changeCursorsToWholeLineIfNoToCursor,
searchForEndOfLine,
searchForStartOfLine
} from './change-cursors-to-whole-line-if-no-to-cursor'
import type { CursorSelection } from '../../../../editor/types'
describe('changeCursorsToWholeLineIfNoToCursor', () => {
it(`returns the given selection if to cursor is present`, () => {
const givenSelection = {
from: 0,
to: 0
}
expect(changeCursorsToWholeLineIfNoToCursor('', givenSelection)).toEqual(givenSelection)
})
it(`returns the corrected selection if cursor is in a line`, () => {
const givenSelection = {
from: 9
}
const expectedSelection: CursorSelection = {
from: 6,
to: 14
}
expect(changeCursorsToWholeLineIfNoToCursor(`I'm a\nfriendly\ntest string!`, givenSelection)).toEqual(
expectedSelection
)
})
it(`returns the corrected selection if cursor is out of bounds`, () => {
const givenSelection = {
from: 123
}
const expectedSelection: CursorSelection = {
from: 0,
to: 27
}
expect(changeCursorsToWholeLineIfNoToCursor(`I'm a friendly test string!`, givenSelection)).toEqual(
expectedSelection
)
})
})
describe('searchForStartOfLine', () => {
it('finds the start of the string', () => {
expect(searchForStartOfLine('a', 1)).toBe(0)
})
it('finds the start of the string if the index is lower out of bounds', () => {
expect(searchForStartOfLine('a', -100)).toBe(0)
})
it('finds the start of the string if the index is upper out of bounds', () => {
expect(searchForStartOfLine('a', 100)).toBe(0)
})
it('finds the start of a line', () => {
expect(searchForStartOfLine('a\nb', 3)).toBe(2)
})
it('finds the start of a line if the index is lower out of bounds', () => {
expect(searchForStartOfLine('a\nb', -100)).toBe(0)
})
it('finds the start of a line if the index is upper out of bounds', () => {
expect(searchForStartOfLine('a\nb', 100)).toBe(2)
})
})
describe('searchForEndOfLine', () => {
it('finds the end of the string', () => {
expect(searchForEndOfLine('a', 1)).toBe(1)
})
it('finds the end of the string if the index is lower out of bounds', () => {
expect(searchForEndOfLine('a', -100)).toBe(1)
})
it('finds the end of the string if the index is upper out of bounds', () => {
expect(searchForEndOfLine('a', 100)).toBe(1)
})
it('finds the start of a line', () => {
expect(searchForEndOfLine('a\nb', 2)).toBe(3)
})
it('finds the start of a line if the index is lower out of bounds', () => {
expect(searchForEndOfLine('a\nb', -100)).toBe(1)
})
it('finds the start of a line if the index is upper out of bounds', () => {
expect(searchForEndOfLine('a\nb', 100)).toBe(3)
})
})

View file

@ -1,70 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { CursorSelection } from '../../../../editor/types'
/**
* If the given cursor selection has no to position then the selection will be changed to cover the whole line of the from cursor.
*
* @param markdownContent The markdown content that is used to calculate the start and end position of the line
* @param selection The selection that is in the line whose start and end index should be calculated
* @return The corrected selection if no to cursor is present or the unmodified selection otherwise
* @throws Error if the line, that the from cursor is referring to, doesn't exist.
*/
export const changeCursorsToWholeLineIfNoToCursor = (
markdownContent: string,
selection: CursorSelection
): CursorSelection => {
if (selection.to !== undefined) {
return selection
}
const newFrom = searchForStartOfLine(markdownContent, selection.from)
const newTo = searchForEndOfLine(markdownContent, selection.from)
return {
from: newFrom,
to: newTo
}
}
/**
* Finds the position of the first character after the nearest
* new line before the given start position.
*
* @param content The content that should be looked through
* @param startPosition The position from which the search should start
* @return The found new line character or the start of the content if no new line could be found
*/
export const searchForStartOfLine = (content: string, startPosition: number): number => {
const adjustedStartPosition = Math.min(Math.max(0, startPosition), content.length)
for (let characterIndex = adjustedStartPosition; characterIndex > 0; characterIndex -= 1) {
if (content.slice(characterIndex - 1, characterIndex) === '\n') {
return characterIndex
}
}
return 0
}
/**
* Finds the position of the last character before the nearest
* new line after the given start position.
*
* @param content The content that should be looked through
* @param startPosition The position from which the search should start
* @return The found new line character or the end of the content if no new line could be found
*/
export const searchForEndOfLine = (content: string, startPosition: number): number => {
const adjustedStartPosition = Math.min(Math.max(0, startPosition), content.length)
for (let characterIndex = adjustedStartPosition; characterIndex < content.length; characterIndex += 1) {
if (content.slice(characterIndex, characterIndex + 1) === '\n') {
return characterIndex
}
}
return content.length
}

View file

@ -1,41 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { stringSplice } from './string-splice'
describe('string splice', () => {
it(`won't modify a string without deletion or text to add`, () => {
expect(stringSplice('I am your friendly test string!', 0, '')).toEqual('I am your friendly test string!')
})
it('can insert a string in the string', () => {
expect(stringSplice('I am your friendly test string!', 10, 'very ')).toEqual('I am your very friendly test string!')
})
it('can append a string if the index is beyond the upper bounds', () => {
expect(stringSplice('I am your friendly test string!', 100, ' And will ever be!')).toEqual(
'I am your friendly test string! And will ever be!'
)
})
it('can prepend a string if the index is beyond the lower bounds', () => {
expect(stringSplice('I am your friendly test string!', -100, 'Here I come! ')).toEqual(
'Here I come! I am your friendly test string!'
)
})
it('can delete parts of a string', () => {
expect(stringSplice('I am your friendly test string!', 4, '', 5)).toEqual('I am friendly test string!')
})
it('can delete and insert parts of a string', () => {
expect(stringSplice('I am your friendly test string!', 10, 'great', 8)).toEqual('I am your great test string!')
})
it(`will ignore a negative delete length`, () => {
expect(stringSplice('I am your friendly test string!', 100, '', -100)).toEqual('I am your friendly test string!')
})
})

View file

@ -1,24 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* Modifies a string by inserting another string and/or deleting characters.
*
* @param text Text to modify
* @param changePosition The position where the other text should be inserted and characters should be deleted
* @param textToInsert The text to insert
* @param deleteLength The number of characters to delete
* @return The modified string
*/
export const stringSplice = (
text: string,
changePosition: number,
textToInsert: string,
deleteLength?: number
): string => {
const correctedDeleteLength = deleteLength === undefined || deleteLength < 0 ? 0 : deleteLength
return text.slice(0, changePosition) + textToInsert + text.slice(changePosition + correctedDeleteLength)
}

View file

@ -1,50 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { wrapSelection } from './wrap-selection'
describe('wrap selection', () => {
it(`doesn't modify any line if no to-cursor is present`, () => {
const actual = wrapSelection(
'a\nb\nc',
{
from: 0
},
'before',
'after'
)
expect(actual).toStrictEqual(['a\nb\nc', { from: 0 }])
})
it(`wraps the selected text in the same line`, () => {
const actual = wrapSelection(
'a\nb\nc',
{
from: 0,
to: 1
},
'before',
'after'
)
expect(actual).toStrictEqual(['beforeaafter\nb\nc', { from: 0, to: 12 }])
})
it(`wraps the selected text in different lines`, () => {
const actual = wrapSelection(
'a\nb\nc',
{
from: 0,
to: 5
},
'before',
'after'
)
expect(actual).toStrictEqual(['beforea\nb\ncafter', { from: 0, to: 16 }])
})
})

View file

@ -1,36 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { stringSplice } from './utils/string-splice'
import type { CursorSelection } from '../../../editor/types'
/**
* Creates a copy of the given markdown content lines but wraps the selection.
*
* @param markdownContent The lines of the document to modify
* @param selection If the selection has no to cursor then nothing will happen.
* If the selection has a to cursor then the selected text will be wrapped.
* @param symbolStart A text that will be inserted before the from cursor
* @param symbolEnd A text that will be inserted after the to cursor
* @return the modified copy of lines
*/
export const wrapSelection = (
markdownContent: string,
selection: CursorSelection,
symbolStart: string,
symbolEnd: string
): [string, CursorSelection] => {
if (selection.to === undefined) {
return [markdownContent, selection]
}
const to = selection.to ?? selection.from
const from = selection.from
const afterToModify = stringSplice(markdownContent, to, symbolEnd)
const afterFromModify = stringSplice(afterToModify, from, symbolStart)
return [afterFromModify, { from, to: to + symbolEnd.length + symbolStart.length }]
}

View file

@ -7,20 +7,14 @@
import { store } from '..'
import type { Note, NotePermissions } from '../../api/notes/types'
import type {
AddTableAtCursorAction,
FormatSelectionAction,
FormatType,
InsertTextAtCursorAction,
ReplaceInMarkdownContentAction,
SetNoteDetailsFromServerAction,
SetNoteDocumentContentAction,
SetNotePermissionsFromServerAction,
UpdateCursorPositionAction,
UpdateNoteTitleByFirstHeadingAction,
UpdateTaskListCheckboxAction
UpdateNoteTitleByFirstHeadingAction
} from './types'
import { NoteDetailsActionType } from './types'
import type { CursorSelection } from '../editor/types'
import type { CursorSelection } from '../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection'
/**
* Sets the content of the current note, extracts and parses the frontmatter and extracts the markdown content part.
@ -66,60 +60,9 @@ export const updateNoteTitleByFirstHeading = (firstHeading?: string): void => {
} as UpdateNoteTitleByFirstHeadingAction)
}
/**
* Changes a checkbox state in the note document content. Triggered when a checkbox in the rendering is clicked.
*
* @param lineInDocumentContent The line in the document content to change.
* @param checked true if the checkbox is checked, false otherwise.
*/
export const setCheckboxInMarkdownContent = (lineInDocumentContent: number, checked: boolean): void => {
store.dispatch({
type: NoteDetailsActionType.UPDATE_TASK_LIST_CHECKBOX,
checkboxChecked: checked,
changedLine: lineInDocumentContent
} as UpdateTaskListCheckboxAction)
}
/**
* Replaces a string in the markdown content in the global application state.
*
* @param replaceable The string that should be replaced
* @param replacement The replacement for the replaceable
*/
export const replaceInMarkdownContent = (replaceable: string, replacement: string): void => {
store.dispatch({
type: NoteDetailsActionType.REPLACE_IN_MARKDOWN_CONTENT,
placeholder: replaceable,
replacement
} as ReplaceInMarkdownContentAction)
}
export const updateCursorPositions = (selection: CursorSelection): void => {
store.dispatch({
type: NoteDetailsActionType.UPDATE_CURSOR_POSITION,
selection
} as UpdateCursorPositionAction)
}
export const formatSelection = (formatType: FormatType): void => {
store.dispatch({
type: NoteDetailsActionType.FORMAT_SELECTION,
formatType
} as FormatSelectionAction)
}
export const addTableAtCursor = (rows: number, columns: number): void => {
store.dispatch({
type: NoteDetailsActionType.ADD_TABLE_AT_CURSOR,
rows,
columns
} as AddTableAtCursorAction)
}
export const replaceSelection = (text: string, cursorSelection?: CursorSelection): void => {
store.dispatch({
type: NoteDetailsActionType.REPLACE_SELECTION,
text,
cursorSelection
} as InsertTextAtCursorAction)
}

View file

@ -13,11 +13,6 @@ import { buildStateFromUpdatedMarkdownContent } from './build-state-from-updated
import { buildStateFromUpdateCursorPosition } from './reducers/build-state-from-update-cursor-position'
import { buildStateFromFirstHeadingUpdate } from './reducers/build-state-from-first-heading-update'
import { buildStateFromServerDto } from './reducers/build-state-from-set-note-data-from-server'
import { buildStateFromAddTableAtCursor } from './reducers/build-state-from-add-table-at-cursor'
import { buildStateFromReplaceSelection } from './reducers/build-state-from-replace-selection'
import { buildStateFromTaskListUpdate } from './reducers/build-state-from-task-list-update'
import { buildStateFromSelectionFormat } from './reducers/build-state-from-selection-format'
import { buildStateFromReplaceInMarkdownContent } from './reducers/build-state-from-replace-in-markdown-content'
import { buildStateFromServerPermissions } from './reducers/build-state-from-server-permissions'
export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsActions> = (
@ -35,16 +30,6 @@ export const NoteDetailsReducer: Reducer<NoteDetails, NoteDetailsActions> = (
return buildStateFromFirstHeadingUpdate(state, action.firstHeading)
case NoteDetailsActionType.SET_NOTE_DATA_FROM_SERVER:
return buildStateFromServerDto(action.noteFromServer)
case NoteDetailsActionType.UPDATE_TASK_LIST_CHECKBOX:
return buildStateFromTaskListUpdate(state, action.changedLine, action.checkboxChecked)
case NoteDetailsActionType.REPLACE_IN_MARKDOWN_CONTENT:
return buildStateFromReplaceInMarkdownContent(state, action.placeholder, action.replacement)
case NoteDetailsActionType.FORMAT_SELECTION:
return buildStateFromSelectionFormat(state, action.formatType)
case NoteDetailsActionType.ADD_TABLE_AT_CURSOR:
return buildStateFromAddTableAtCursor(state, action.rows, action.columns)
case NoteDetailsActionType.REPLACE_SELECTION:
return buildStateFromReplaceSelection(state, action.text, action.cursorSelection)
default:
return state
}

View file

@ -1,56 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { buildStateFromAddTableAtCursor } from './build-state-from-add-table-at-cursor'
import { initialState } from '../initial-state'
describe('build state from add table at cursor', () => {
it('fails if number of rows is negative', () => {
expect(() =>
buildStateFromAddTableAtCursor(
{
...initialState
},
-1,
1
)
).toThrow()
})
it('fails if number of columns is negative', () => {
expect(() =>
buildStateFromAddTableAtCursor(
{
...initialState
},
1,
-1
)
).toThrow()
})
it('generates a table with the correct size', () => {
const actual = buildStateFromAddTableAtCursor(
{
...initialState,
markdownContent: { plain: 'a\nb\nc', lines: ['a', 'b', 'c'], lineStartIndexes: [0, 2, 4] },
selection: {
from: 2
}
},
3,
3
)
expect(actual.markdownContent.plain).toEqual(
'a\n\n| # 1 | # 2 | # 3 |\n' +
'| ---- | ---- | ---- |\n' +
'| Text | Text | Text |\n' +
'| Text | Text | Text |\n' +
'| Text | Text | Text |b\n' +
'c'
)
})
})

View file

@ -1,48 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { NoteDetails } from '../types/note-details'
import { buildStateFromUpdatedMarkdownContent } from '../build-state-from-updated-markdown-content'
import { replaceSelection } from '../format-selection/formatters/replace-selection'
import { createNumberRangeArray } from '../../../components/common/number-range/number-range'
/**
* Copies the given {@link NoteDetails note details state} but adds a markdown table with the given table at the end of the cursor selection.
*
* @param state The original {@link NoteDetails}
* @param rows The number of rows of the new table
* @param columns The number of columns of the new table
* @return the copied but modified {@link NoteDetails note details state}
*/
export const buildStateFromAddTableAtCursor = (state: NoteDetails, rows: number, columns: number): NoteDetails => {
const table = createMarkdownTable(rows, columns)
const [newContent, newSelection] = replaceSelection(
state.markdownContent.plain,
{ from: state.selection.to ?? state.selection.from },
table
)
const newState = buildStateFromUpdatedMarkdownContent(state, newContent)
return {
...newState,
selection: newSelection
}
}
/**
* Creates a markdown table with the given size.
*
* @param rows The number of table rows
* @param columns The number of table columns
* @return The created markdown table
*/
const createMarkdownTable = (rows: number, columns: number): string => {
const rowArray = createNumberRangeArray(rows)
const colArray = createNumberRangeArray(columns).map((col) => col + 1)
const head = '| # ' + colArray.join(' | # ') + ' |'
const divider = '| ' + colArray.map(() => '----').join(' | ') + ' |'
const body = rowArray.map(() => '| ' + colArray.map(() => 'Text').join(' | ') + ' |').join('\n')
return `\n${head}\n${divider}\n${body}`
}

View file

@ -1,37 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as buildStateFromUpdatedMarkdownContentModule from '../build-state-from-updated-markdown-content'
import { Mock } from 'ts-mockery'
import type { NoteDetails } from '../types/note-details'
import { buildStateFromReplaceInMarkdownContent } from './build-state-from-replace-in-markdown-content'
import { initialState } from '../initial-state'
describe('build state from replace in markdown content', () => {
const buildStateFromUpdatedMarkdownContentMock = jest.spyOn(
buildStateFromUpdatedMarkdownContentModule,
'buildStateFromUpdatedMarkdownContent'
)
const mockedNoteDetails = Mock.of<NoteDetails>()
beforeAll(() => {
buildStateFromUpdatedMarkdownContentMock.mockImplementation(() => mockedNoteDetails)
})
afterAll(() => {
buildStateFromUpdatedMarkdownContentMock.mockReset()
})
it('updates the markdown content with the replacement', () => {
const startState: NoteDetails = {
...initialState,
markdownContent: { ...initialState.markdownContent, plain: 'replaceable' }
}
const result = buildStateFromReplaceInMarkdownContent(startState, 'replaceable', 'replacement')
expect(result).toBe(mockedNoteDetails)
expect(buildStateFromUpdatedMarkdownContentMock).toHaveBeenCalledWith(startState, 'replacement')
})
})

View file

@ -1,39 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { NoteDetails } from '../types/note-details'
import { buildStateFromUpdatedMarkdownContent } from '../build-state-from-updated-markdown-content'
const replaceAllExists = String.prototype.replaceAll !== undefined
/**
* A replace-all string function that uses a polyfill if the environment doesn't
* support replace-all (like node 14 for unit tests).
* TODO: Remove polyfill when node 14 is removed
*
* @param haystack The string that should be modified
* @param needle The string that should get replaced
* @param replacement The string that should replace
* @return The modified string
*/
const replaceAll = (haystack: string, needle: string, replacement: string): string =>
replaceAllExists ? haystack.replaceAll(needle, replacement) : haystack.split(needle).join(replacement)
/**
* Builds a {@link NoteDetails} redux state with a modified markdown content.
*
* @param state The previous redux state
* @param replaceable The string that should be replaced in the old markdown content
* @param replacement The string that should replace the replaceable
* @return An updated {@link NoteDetails} redux state
*/
export const buildStateFromReplaceInMarkdownContent = (
state: NoteDetails,
replaceable: string,
replacement: string
): NoteDetails => {
return buildStateFromUpdatedMarkdownContent(state, replaceAll(state.markdownContent.plain, replaceable, replacement))
}

View file

@ -1,67 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as buildStateFromUpdatedMarkdownContentLinesModule from '../build-state-from-updated-markdown-content'
import * as replaceSelectionModule from '../format-selection/formatters/replace-selection'
import { Mock } from 'ts-mockery'
import type { NoteDetails } from '../types/note-details'
import { buildStateFromReplaceSelection } from './build-state-from-replace-selection'
import { initialState } from '../initial-state'
import type { CursorSelection } from '../../editor/types'
describe('build state from replace selection', () => {
const buildStateFromUpdatedMarkdownContentMock = jest.spyOn(
buildStateFromUpdatedMarkdownContentLinesModule,
'buildStateFromUpdatedMarkdownContent'
)
const replaceSelectionMock = jest.spyOn(replaceSelectionModule, 'replaceSelection')
const mockedNoteDetails = { content: 'mocked' } as unknown as NoteDetails
const mockedFormattedContent = 'formatted'
const mockedCursor = Mock.of<CursorSelection>()
beforeAll(() => {
buildStateFromUpdatedMarkdownContentMock.mockImplementation(() => mockedNoteDetails)
replaceSelectionMock.mockImplementation(() => [mockedFormattedContent, mockedCursor])
})
afterAll(() => {
buildStateFromUpdatedMarkdownContentMock.mockReset()
replaceSelectionMock.mockReset()
})
it('builds a new state with the provided cursor', () => {
const originalLines = 'original'
const startState = {
...initialState,
markdownContent: { plain: originalLines, lines: [originalLines], lineStartIndexes: [0] }
}
const customCursor = Mock.of<CursorSelection>()
const textReplacement = 'replacement'
const result = buildStateFromReplaceSelection(startState, 'replacement', customCursor)
expect(result).toStrictEqual({ content: 'mocked', selection: mockedCursor })
expect(buildStateFromUpdatedMarkdownContentMock).toHaveBeenCalledWith(startState, mockedFormattedContent)
expect(replaceSelectionMock).toHaveBeenCalledWith(originalLines, customCursor, textReplacement)
})
it('builds a new state with the state cursor', () => {
const originalLines = 'original'
const selection = Mock.of<CursorSelection>()
const startState: NoteDetails = {
...initialState,
markdownContent: { plain: originalLines, lines: [originalLines], lineStartIndexes: [0] },
selection
}
const textReplacement = 'replacement'
const result = buildStateFromReplaceSelection(startState, 'replacement')
expect(result).toStrictEqual({ content: 'mocked', selection: mockedCursor })
expect(buildStateFromUpdatedMarkdownContentMock).toHaveBeenCalledWith(startState, mockedFormattedContent)
expect(replaceSelectionMock).toHaveBeenCalledWith(originalLines, selection, textReplacement)
})
})

View file

@ -1,23 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { NoteDetails } from '../types/note-details'
import type { CursorSelection } from '../../editor/types'
import { buildStateFromUpdatedMarkdownContent } from '../build-state-from-updated-markdown-content'
import { replaceSelection } from '../format-selection/formatters/replace-selection'
export const buildStateFromReplaceSelection = (state: NoteDetails, text: string, cursorSelection?: CursorSelection) => {
const [newContent, newSelection] = replaceSelection(
state.markdownContent.plain,
cursorSelection ? cursorSelection : state.selection,
text
)
const newState = buildStateFromUpdatedMarkdownContent(state, newContent)
return {
...newState,
selection: newSelection
}
}

View file

@ -1,51 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as buildStateFromUpdatedMarkdownContentLinesModule from '../build-state-from-updated-markdown-content'
import { Mock } from 'ts-mockery'
import type { NoteDetails } from '../types/note-details'
import * as applyFormatTypeToMarkdownLinesModule from '../format-selection/apply-format-type-to-markdown-lines'
import { buildStateFromSelectionFormat } from './build-state-from-selection-format'
import { initialState } from '../initial-state'
import { FormatType } from '../types'
import type { CursorSelection } from '../../editor/types'
describe('build state from selection format', () => {
const buildStateFromUpdatedMarkdownContentMock = jest.spyOn(
buildStateFromUpdatedMarkdownContentLinesModule,
'buildStateFromUpdatedMarkdownContent'
)
const mockedNoteDetails = { content: 'mocked' } as unknown as NoteDetails
const applyFormatTypeToMarkdownLinesMock = jest.spyOn(
applyFormatTypeToMarkdownLinesModule,
'applyFormatTypeToMarkdownLines'
)
const mockedFormattedContent = 'formatted'
const mockedCursor = Mock.of<CursorSelection>()
beforeAll(() => {
buildStateFromUpdatedMarkdownContentMock.mockImplementation(() => mockedNoteDetails)
applyFormatTypeToMarkdownLinesMock.mockImplementation(() => [mockedFormattedContent, mockedCursor])
})
afterAll(() => {
buildStateFromUpdatedMarkdownContentMock.mockReset()
applyFormatTypeToMarkdownLinesMock.mockReset()
})
it('builds a new state with the formatted code', () => {
const originalContent = 'original'
const startState: NoteDetails = {
...initialState,
markdownContent: { ...initialState.markdownContent, plain: originalContent },
selection: mockedCursor
}
const result = buildStateFromSelectionFormat(startState, FormatType.BOLD)
expect(result).toStrictEqual({ content: 'mocked', selection: mockedCursor })
expect(buildStateFromUpdatedMarkdownContentMock).toHaveBeenCalledWith(startState, mockedFormattedContent)
expect(applyFormatTypeToMarkdownLinesMock).toHaveBeenCalledWith(originalContent, mockedCursor, FormatType.BOLD)
})
})

View file

@ -1,19 +0,0 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { NoteDetails } from '../types/note-details'
import type { FormatType } from '../types'
import { buildStateFromUpdatedMarkdownContent } from '../build-state-from-updated-markdown-content'
import { applyFormatTypeToMarkdownLines } from '../format-selection/apply-format-type-to-markdown-lines'
export const buildStateFromSelectionFormat = (state: NoteDetails, type: FormatType): NoteDetails => {
const [newContent, newSelection] = applyFormatTypeToMarkdownLines(state.markdownContent.plain, state.selection, type)
const newState = buildStateFromUpdatedMarkdownContent(state, newContent)
return {
...newState,
selection: newSelection
}
}

View file

@ -5,9 +5,9 @@
*/
import { initialState } from '../initial-state'
import type { CursorSelection } from '../../editor/types'
import { Mock } from 'ts-mockery'
import { buildStateFromUpdateCursorPosition } from './build-state-from-update-cursor-position'
import type { CursorSelection } from '../../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection'
describe('build state from update cursor position', () => {
it('creates a new state with the given cursor', () => {

View file

@ -5,7 +5,7 @@
*/
import type { NoteDetails } from '../types/note-details'
import type { CursorSelection } from '../../editor/types'
import type { CursorSelection } from '../../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection'
export const buildStateFromUpdateCursorPosition = (state: NoteDetails, selection: CursorSelection): NoteDetails => {
const correctedSelection = isFromAfterTo(selection)

View file

@ -6,40 +6,14 @@
import type { Action } from 'redux'
import type { Note, NotePermissions } from '../../api/notes/types'
import type { CursorSelection } from '../editor/types'
import type { CursorSelection } from '../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection'
export enum NoteDetailsActionType {
SET_DOCUMENT_CONTENT = 'note-details/content/set',
SET_NOTE_DATA_FROM_SERVER = 'note-details/data/server/set',
SET_NOTE_PERMISSIONS_FROM_SERVER = 'note-details/data/permissions/set',
UPDATE_NOTE_TITLE_BY_FIRST_HEADING = 'note-details/update-note-title-by-first-heading',
UPDATE_TASK_LIST_CHECKBOX = 'note-details/update-task-list-checkbox',
UPDATE_CURSOR_POSITION = 'note-details/updateCursorPosition',
REPLACE_IN_MARKDOWN_CONTENT = 'note-details/replace-in-markdown-content',
FORMAT_SELECTION = 'note-details/format-selection',
ADD_TABLE_AT_CURSOR = 'note-details/add-table-at-cursor',
REPLACE_SELECTION = 'note-details/replace-selection'
}
export enum FormatType {
BOLD = 'bold',
ITALIC = 'italic',
STRIKETHROUGH = 'strikethrough',
UNDERLINE = 'underline',
SUBSCRIPT = 'subscript',
SUPERSCRIPT = 'superscript',
HIGHLIGHT = 'highlight',
CODE_FENCE = 'code',
UNORDERED_LIST = 'unorderedList',
ORDERED_LIST = 'orderedList',
CHECK_LIST = 'checkList',
QUOTES = 'blockquote',
HORIZONTAL_LINE = 'horizontalLine',
COMMENT = 'comment',
COLLAPSIBLE_BLOCK = 'collapsibleBlock',
HEADER_LEVEL = 'header',
LINK = 'link',
IMAGE_LINK = 'imageLink'
UPDATE_CURSOR_POSITION = 'note-details/updateCursorPosition'
}
export type NoteDetailsActions =
@ -47,12 +21,7 @@ export type NoteDetailsActions =
| SetNoteDetailsFromServerAction
| SetNotePermissionsFromServerAction
| UpdateNoteTitleByFirstHeadingAction
| UpdateTaskListCheckboxAction
| UpdateCursorPositionAction
| ReplaceInMarkdownContentAction
| FormatSelectionAction
| AddTableAtCursorAction
| InsertTextAtCursorAction
/**
* Action for updating the document content of the currently loaded note.
@ -86,39 +55,7 @@ export interface UpdateNoteTitleByFirstHeadingAction extends Action<NoteDetailsA
firstHeading?: string
}
/**
* Action for manipulating the document content of the currently loaded note by changing the checked state of a task list checkbox.
*/
export interface UpdateTaskListCheckboxAction extends Action<NoteDetailsActionType> {
type: NoteDetailsActionType.UPDATE_TASK_LIST_CHECKBOX
changedLine: number
checkboxChecked: boolean
}
export interface ReplaceInMarkdownContentAction extends Action<NoteDetailsActionType> {
type: NoteDetailsActionType.REPLACE_IN_MARKDOWN_CONTENT
placeholder: string
replacement: string
}
export interface UpdateCursorPositionAction extends Action<NoteDetailsActionType> {
type: NoteDetailsActionType.UPDATE_CURSOR_POSITION
selection: CursorSelection
}
export interface FormatSelectionAction extends Action<NoteDetailsActionType> {
type: NoteDetailsActionType.FORMAT_SELECTION
formatType: FormatType
}
export interface AddTableAtCursorAction extends Action<NoteDetailsActionType> {
type: NoteDetailsActionType.ADD_TABLE_AT_CURSOR
rows: number
columns: number
}
export interface InsertTextAtCursorAction extends Action<NoteDetailsActionType> {
type: NoteDetailsActionType.REPLACE_SELECTION
text: string
cursorSelection?: CursorSelection
}

View file

@ -6,8 +6,8 @@
import type { SlideOptions } from './slide-show-options'
import type { ISO6391 } from './iso6391'
import type { CursorSelection } from '../../editor/types'
import type { NoteMetadata } from '../../../api/notes/types'
import type { CursorSelection } from '../../../components/editor-page/editor-pane/tool-bar/formatters/types/cursor-selection'
type UnnecessaryNoteAttributes = 'updatedAt' | 'createdAt' | 'tags' | 'description'