mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-29 22:35:50 -04:00
Upgrade to CodeMirror 6 (#1787)
Upgrade to CodeMirror 6 Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
1a09bfa5f1
commit
6a6f6105b9
103 changed files with 1906 additions and 2615 deletions
|
@ -10,6 +10,7 @@ import { initialState } from './initial-state'
|
|||
import type { PresentFrontmatterExtractionResult } from './frontmatter-extractor/types'
|
||||
import { createNoteFrontmatterFromYaml } from './raw-note-frontmatter-parser/parser'
|
||||
import { generateNoteTitle } from './generate-note-title'
|
||||
import { calculateLineStartIndexes } from './calculate-line-start-indexes'
|
||||
|
||||
/**
|
||||
* Copies a {@link NoteDetails} but with another markdown content.
|
||||
|
@ -40,20 +41,27 @@ const buildStateFromMarkdownContentAndLines = (
|
|||
markdownContentLines: string[]
|
||||
): NoteDetails => {
|
||||
const frontmatterExtraction = extractFrontmatter(markdownContentLines)
|
||||
const lineStartIndexes = calculateLineStartIndexes(markdownContentLines)
|
||||
if (frontmatterExtraction.isPresent) {
|
||||
return buildStateFromFrontmatterUpdate(
|
||||
{
|
||||
...state,
|
||||
markdownContent: markdownContent,
|
||||
markdownContentLines: markdownContentLines
|
||||
markdownContent: {
|
||||
plain: markdownContent,
|
||||
lines: markdownContentLines,
|
||||
lineStartIndexes
|
||||
}
|
||||
},
|
||||
frontmatterExtraction
|
||||
)
|
||||
} else {
|
||||
return {
|
||||
...state,
|
||||
markdownContent: markdownContent,
|
||||
markdownContentLines: markdownContentLines,
|
||||
markdownContent: {
|
||||
plain: markdownContent,
|
||||
lines: markdownContentLines,
|
||||
lineStartIndexes
|
||||
},
|
||||
rawFrontmatter: '',
|
||||
noteTitle: generateNoteTitle(initialState.frontmatter, state.firstHeading),
|
||||
frontmatter: initialState.frontmatter,
|
||||
|
|
21
src/redux/note-details/calculate-line-start-indexes.test.ts
Normal file
21
src/redux/note-details/calculate-line-start-indexes.test.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { calculateLineStartIndexes } from './calculate-line-start-indexes'
|
||||
|
||||
describe('calculateLineStartIndexes', () => {
|
||||
it('works with an empty list', () => {
|
||||
expect(calculateLineStartIndexes([])).toEqual([])
|
||||
})
|
||||
it('works with an non empty list', () => {
|
||||
expect(calculateLineStartIndexes(['a', 'bc', 'def', 'ghij', 'klmno', 'pqrstu', 'vwxyz'])).toEqual([
|
||||
0, 2, 5, 9, 14, 20, 27
|
||||
])
|
||||
})
|
||||
it('works with an non empty list with empty lines', () => {
|
||||
expect(calculateLineStartIndexes(['', '', ''])).toEqual([0, 1, 2])
|
||||
})
|
||||
})
|
18
src/redux/note-details/calculate-line-start-indexes.ts
Normal file
18
src/redux/note-details/calculate-line-start-indexes.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculates the absolute start position of every line.
|
||||
*
|
||||
* @param markdownContentLines The lines of the document
|
||||
* @returns the calculated line starts
|
||||
*/
|
||||
export const calculateLineStartIndexes = (markdownContentLines: string[]): number[] => {
|
||||
return markdownContentLines.reduce((state, line, lineIndex, lines) => {
|
||||
const lastIndex = lineIndex === 0 ? 0 : state[lineIndex - 1] + lines[lineIndex - 1].length + 1
|
||||
return [...state, lastIndex]
|
||||
}, [] as number[])
|
||||
}
|
|
@ -7,21 +7,21 @@
|
|||
import { Mock } from 'ts-mockery'
|
||||
import * as wrapSelectionModule from './formatters/wrap-selection'
|
||||
import { applyFormatTypeToMarkdownLines } from './apply-format-type-to-markdown-lines'
|
||||
import type { CursorPosition, CursorSelection } from '../../editor/types'
|
||||
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 replaceLinesOfSelectionModule from './formatters/replace-lines-of-selection'
|
||||
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 markdownContentLinesMock = ['input']
|
||||
const markdownContentMock = 'input'
|
||||
const cursorSelectionMock = Mock.of<CursorSelection>()
|
||||
|
||||
const wrapSelectionMock = jest.spyOn(wrapSelectionModule, 'wrapSelection')
|
||||
const wrapSelectionMockResponse = Mock.of<string[]>()
|
||||
const wrapSelectionMockResponse = Mock.of<[string, CursorSelection]>()
|
||||
|
||||
const changeCursorsToWholeLineIfNoToCursorMock = jest.spyOn(
|
||||
changeCursorsToWholeLineIfNoToCursorModule,
|
||||
|
@ -29,24 +29,24 @@ describe('apply format type to markdown lines', () => {
|
|||
)
|
||||
const changeCursorsToWholeLineIfNoToCursorMockResponse = Mock.of<CursorSelection>()
|
||||
|
||||
const replaceLinesOfSelectionMock = jest.spyOn(replaceLinesOfSelectionModule, 'replaceLinesOfSelection')
|
||||
const prependLinesOfSelectionMock = jest.spyOn(prependLinesOfSelectionModule, 'prependLinesOfSelection')
|
||||
|
||||
const replaceSelectionMock = jest.spyOn(replaceSelectionModule, 'replaceSelection')
|
||||
const replaceSelectionMockResponse = Mock.of<string[]>()
|
||||
const replaceSelectionMockResponse = Mock.of<[string, CursorSelection]>()
|
||||
|
||||
const addLinkMock = jest.spyOn(addLinkModule, 'addLink')
|
||||
const addLinkMockResponse = Mock.of<string[]>()
|
||||
const addLinkMockResponse = Mock.of<[string, CursorSelection]>()
|
||||
|
||||
beforeAll(() => {
|
||||
wrapSelectionMock.mockReturnValue(wrapSelectionMockResponse)
|
||||
changeCursorsToWholeLineIfNoToCursorMock.mockReturnValue(changeCursorsToWholeLineIfNoToCursorMockResponse)
|
||||
replaceLinesOfSelectionMock.mockImplementation(
|
||||
prependLinesOfSelectionMock.mockImplementation(
|
||||
(
|
||||
lines: string[],
|
||||
markdownContent: string,
|
||||
selection: CursorSelection,
|
||||
replacer: (line: string, lineIndex: number) => string
|
||||
): string[] => {
|
||||
return lines.map(replacer)
|
||||
generatePrefix: (line: string, lineIndexInBlock: number) => string
|
||||
): [string, CursorSelection] => {
|
||||
return [generatePrefix(markdownContent, 0) + markdownContent, selection]
|
||||
}
|
||||
)
|
||||
replaceSelectionMock.mockReturnValue(replaceSelectionMockResponse)
|
||||
|
@ -58,57 +58,53 @@ describe('apply format type to markdown lines', () => {
|
|||
})
|
||||
|
||||
it('can process the format type bold', () => {
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentLinesMock, cursorSelectionMock, FormatType.BOLD)
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.BOLD)
|
||||
expect(result).toBe(wrapSelectionMockResponse)
|
||||
expect(wrapSelectionMock).toBeCalledWith(markdownContentLinesMock, cursorSelectionMock, '**', '**')
|
||||
expect(wrapSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '**', '**')
|
||||
})
|
||||
|
||||
it('can process the format type italic', () => {
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentLinesMock, cursorSelectionMock, FormatType.ITALIC)
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.ITALIC)
|
||||
expect(result).toBe(wrapSelectionMockResponse)
|
||||
expect(wrapSelectionMock).toBeCalledWith(markdownContentLinesMock, cursorSelectionMock, '*', '*')
|
||||
expect(wrapSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '*', '*')
|
||||
})
|
||||
|
||||
it('can process the format type strikethrough', () => {
|
||||
const result = applyFormatTypeToMarkdownLines(
|
||||
markdownContentLinesMock,
|
||||
cursorSelectionMock,
|
||||
FormatType.STRIKETHROUGH
|
||||
)
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.STRIKETHROUGH)
|
||||
expect(result).toBe(wrapSelectionMockResponse)
|
||||
expect(wrapSelectionMock).toBeCalledWith(markdownContentLinesMock, cursorSelectionMock, '~~', '~~')
|
||||
expect(wrapSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '~~', '~~')
|
||||
})
|
||||
|
||||
it('can process the format type underline', () => {
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentLinesMock, cursorSelectionMock, FormatType.UNDERLINE)
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.UNDERLINE)
|
||||
expect(result).toBe(wrapSelectionMockResponse)
|
||||
expect(wrapSelectionMock).toBeCalledWith(markdownContentLinesMock, cursorSelectionMock, '++', '++')
|
||||
expect(wrapSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '++', '++')
|
||||
})
|
||||
|
||||
it('can process the format type subscript', () => {
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentLinesMock, cursorSelectionMock, FormatType.SUBSCRIPT)
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.SUBSCRIPT)
|
||||
expect(result).toBe(wrapSelectionMockResponse)
|
||||
expect(wrapSelectionMock).toBeCalledWith(markdownContentLinesMock, cursorSelectionMock, '~', '~')
|
||||
expect(wrapSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '~', '~')
|
||||
})
|
||||
|
||||
it('can process the format type superscript', () => {
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentLinesMock, cursorSelectionMock, FormatType.SUPERSCRIPT)
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.SUPERSCRIPT)
|
||||
expect(result).toBe(wrapSelectionMockResponse)
|
||||
expect(wrapSelectionMock).toBeCalledWith(markdownContentLinesMock, cursorSelectionMock, '^', '^')
|
||||
expect(wrapSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '^', '^')
|
||||
})
|
||||
|
||||
it('can process the format type highlight', () => {
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentLinesMock, cursorSelectionMock, FormatType.HIGHLIGHT)
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.HIGHLIGHT)
|
||||
expect(result).toBe(wrapSelectionMockResponse)
|
||||
expect(wrapSelectionMock).toBeCalledWith(markdownContentLinesMock, cursorSelectionMock, '==', '==')
|
||||
expect(wrapSelectionMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '==', '==')
|
||||
})
|
||||
|
||||
it('can process the format type code fence', () => {
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentLinesMock, cursorSelectionMock, FormatType.CODE_FENCE)
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.CODE_FENCE)
|
||||
expect(result).toBe(wrapSelectionMockResponse)
|
||||
expect(changeCursorsToWholeLineIfNoToCursorMock).toBeCalledWith(markdownContentLinesMock, cursorSelectionMock)
|
||||
expect(changeCursorsToWholeLineIfNoToCursorMock).toBeCalledWith(markdownContentMock, cursorSelectionMock)
|
||||
expect(wrapSelectionMock).toBeCalledWith(
|
||||
markdownContentLinesMock,
|
||||
markdownContentMock,
|
||||
changeCursorsToWholeLineIfNoToCursorMockResponse,
|
||||
'```\n',
|
||||
'\n```'
|
||||
|
@ -116,91 +112,83 @@ describe('apply format type to markdown lines', () => {
|
|||
})
|
||||
|
||||
it('can process the format type unordered list', () => {
|
||||
const result = applyFormatTypeToMarkdownLines(
|
||||
markdownContentLinesMock,
|
||||
cursorSelectionMock,
|
||||
FormatType.UNORDERED_LIST
|
||||
)
|
||||
expect(result).toEqual(['- input'])
|
||||
expect(replaceLinesOfSelectionMock).toBeCalledWith(markdownContentLinesMock, cursorSelectionMock, expect.anything())
|
||||
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 unordered list', () => {
|
||||
const result = applyFormatTypeToMarkdownLines(
|
||||
markdownContentLinesMock,
|
||||
cursorSelectionMock,
|
||||
FormatType.ORDERED_LIST
|
||||
)
|
||||
expect(result).toEqual(['1. input'])
|
||||
expect(replaceLinesOfSelectionMock).toBeCalledWith(markdownContentLinesMock, cursorSelectionMock, expect.anything())
|
||||
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(markdownContentLinesMock, cursorSelectionMock, FormatType.CHECK_LIST)
|
||||
expect(result).toEqual(['- [ ] input'])
|
||||
expect(replaceLinesOfSelectionMock).toBeCalledWith(markdownContentLinesMock, cursorSelectionMock, expect.anything())
|
||||
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(markdownContentLinesMock, cursorSelectionMock, FormatType.QUOTES)
|
||||
expect(result).toEqual(['> input'])
|
||||
expect(replaceLinesOfSelectionMock).toBeCalledWith(markdownContentLinesMock, cursorSelectionMock, expect.anything())
|
||||
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 fromCursor = Mock.of<CursorPosition>()
|
||||
const randomCursorPosition = 138743857
|
||||
const result = applyFormatTypeToMarkdownLines(
|
||||
markdownContentLinesMock,
|
||||
{ from: fromCursor },
|
||||
markdownContentMock,
|
||||
{ from: randomCursorPosition },
|
||||
FormatType.HORIZONTAL_LINE
|
||||
)
|
||||
expect(result).toEqual(replaceSelectionMockResponse)
|
||||
expect(replaceSelectionMock).toBeCalledWith(markdownContentLinesMock, { from: fromCursor }, `\n----`)
|
||||
expect(replaceSelectionMock).toBeCalledWith(markdownContentMock, { from: randomCursorPosition }, `\n----`)
|
||||
})
|
||||
|
||||
it('can process the format type horizontal line with from and to cursor', () => {
|
||||
const fromCursor = Mock.of<CursorPosition>()
|
||||
const toCursor = Mock.of<CursorPosition>()
|
||||
const fromCursor = Math.random()
|
||||
const toCursor = Math.random()
|
||||
|
||||
const result = applyFormatTypeToMarkdownLines(
|
||||
markdownContentLinesMock,
|
||||
markdownContentMock,
|
||||
{ from: fromCursor, to: toCursor },
|
||||
FormatType.HORIZONTAL_LINE
|
||||
)
|
||||
expect(result).toEqual(replaceSelectionMockResponse)
|
||||
expect(replaceSelectionMock).toBeCalledWith(markdownContentLinesMock, { from: toCursor }, `\n----`)
|
||||
expect(replaceSelectionMock).toBeCalledWith(markdownContentMock, { from: toCursor }, `\n----`)
|
||||
})
|
||||
|
||||
it('can process the format type comment with only from cursor', () => {
|
||||
const fromCursor = Mock.of<CursorPosition>()
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentLinesMock, { from: fromCursor }, FormatType.COMMENT)
|
||||
const fromCursor = Math.random()
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentMock, { from: fromCursor }, FormatType.COMMENT)
|
||||
expect(result).toEqual(replaceSelectionMockResponse)
|
||||
expect(replaceSelectionMock).toBeCalledWith(markdownContentLinesMock, { from: fromCursor }, `\n> []`)
|
||||
expect(replaceSelectionMock).toBeCalledWith(markdownContentMock, { from: fromCursor }, `\n> []`)
|
||||
})
|
||||
|
||||
it('can process the format type comment with from and to cursor', () => {
|
||||
const fromCursor = Mock.of<CursorPosition>()
|
||||
const toCursor = Mock.of<CursorPosition>()
|
||||
const fromCursor = 0
|
||||
const toCursor = 1
|
||||
|
||||
const result = applyFormatTypeToMarkdownLines(
|
||||
markdownContentLinesMock,
|
||||
markdownContentMock,
|
||||
{ from: fromCursor, to: toCursor },
|
||||
FormatType.COMMENT
|
||||
)
|
||||
expect(result).toEqual(replaceSelectionMockResponse)
|
||||
expect(replaceSelectionMock).toBeCalledWith(markdownContentLinesMock, { from: toCursor }, `\n> []`)
|
||||
expect(replaceSelectionMock).toBeCalledWith(markdownContentMock, { from: toCursor }, `\n> []`)
|
||||
})
|
||||
|
||||
it('can process the format type collapsible block', () => {
|
||||
const result = applyFormatTypeToMarkdownLines(
|
||||
markdownContentLinesMock,
|
||||
markdownContentMock,
|
||||
cursorSelectionMock,
|
||||
FormatType.COLLAPSIBLE_BLOCK
|
||||
)
|
||||
expect(result).toBe(wrapSelectionMockResponse)
|
||||
expect(changeCursorsToWholeLineIfNoToCursorMock).toBeCalledWith(markdownContentLinesMock, cursorSelectionMock)
|
||||
expect(changeCursorsToWholeLineIfNoToCursorMock).toBeCalledWith(markdownContentMock, cursorSelectionMock)
|
||||
expect(wrapSelectionMock).toBeCalledWith(
|
||||
markdownContentLinesMock,
|
||||
markdownContentMock,
|
||||
changeCursorsToWholeLineIfNoToCursorMockResponse,
|
||||
':::spoiler Toggle label\n',
|
||||
'\n:::'
|
||||
|
@ -208,30 +196,26 @@ describe('apply format type to markdown lines', () => {
|
|||
})
|
||||
|
||||
it('can process the format type header level with existing level', () => {
|
||||
const inputLines = ['# text']
|
||||
const inputLines = '# text'
|
||||
const result = applyFormatTypeToMarkdownLines(inputLines, cursorSelectionMock, FormatType.HEADER_LEVEL)
|
||||
expect(result).toEqual(['## text'])
|
||||
expect(replaceLinesOfSelectionMock).toBeCalledWith(inputLines, cursorSelectionMock, expect.anything())
|
||||
expect(result).toEqual(['## text', cursorSelectionMock])
|
||||
expect(prependLinesOfSelectionMock).toBeCalledWith(inputLines, cursorSelectionMock, expect.anything())
|
||||
})
|
||||
|
||||
it('can process the format type link', () => {
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentLinesMock, cursorSelectionMock, FormatType.LINK)
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.LINK)
|
||||
expect(result).toEqual(addLinkMockResponse)
|
||||
expect(addLinkMock).toBeCalledWith(markdownContentLinesMock, cursorSelectionMock)
|
||||
expect(addLinkMock).toBeCalledWith(markdownContentMock, cursorSelectionMock)
|
||||
})
|
||||
|
||||
it('can process the format type image link', () => {
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentLinesMock, cursorSelectionMock, FormatType.IMAGE_LINK)
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, FormatType.IMAGE_LINK)
|
||||
expect(result).toEqual(addLinkMockResponse)
|
||||
expect(addLinkMock).toBeCalledWith(markdownContentLinesMock, cursorSelectionMock, '!')
|
||||
expect(addLinkMock).toBeCalledWith(markdownContentMock, cursorSelectionMock, '!')
|
||||
})
|
||||
|
||||
it('can process an unknown format type ', () => {
|
||||
const result = applyFormatTypeToMarkdownLines(
|
||||
markdownContentLinesMock,
|
||||
cursorSelectionMock,
|
||||
'UNKNOWN' as FormatType
|
||||
)
|
||||
expect(result).toEqual(markdownContentLinesMock)
|
||||
const result = applyFormatTypeToMarkdownLines(markdownContentMock, cursorSelectionMock, 'UNKNOWN' as FormatType)
|
||||
expect(result).toEqual([markdownContentMock, cursorSelectionMock])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -7,70 +7,68 @@
|
|||
import { FormatType } from '../types'
|
||||
import { wrapSelection } from './formatters/wrap-selection'
|
||||
import { addLink } from './formatters/add-link'
|
||||
import { replaceLinesOfSelection } from './formatters/replace-lines-of-selection'
|
||||
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 = (
|
||||
markdownContentLines: string[],
|
||||
markdownContent: string,
|
||||
selection: CursorSelection,
|
||||
type: FormatType
|
||||
): string[] => {
|
||||
): [string, CursorSelection] => {
|
||||
switch (type) {
|
||||
case FormatType.BOLD:
|
||||
return wrapSelection(markdownContentLines, selection, '**', '**')
|
||||
return wrapSelection(markdownContent, selection, '**', '**')
|
||||
case FormatType.ITALIC:
|
||||
return wrapSelection(markdownContentLines, selection, '*', '*')
|
||||
return wrapSelection(markdownContent, selection, '*', '*')
|
||||
case FormatType.STRIKETHROUGH:
|
||||
return wrapSelection(markdownContentLines, selection, '~~', '~~')
|
||||
return wrapSelection(markdownContent, selection, '~~', '~~')
|
||||
case FormatType.UNDERLINE:
|
||||
return wrapSelection(markdownContentLines, selection, '++', '++')
|
||||
return wrapSelection(markdownContent, selection, '++', '++')
|
||||
case FormatType.SUBSCRIPT:
|
||||
return wrapSelection(markdownContentLines, selection, '~', '~')
|
||||
return wrapSelection(markdownContent, selection, '~', '~')
|
||||
case FormatType.SUPERSCRIPT:
|
||||
return wrapSelection(markdownContentLines, selection, '^', '^')
|
||||
return wrapSelection(markdownContent, selection, '^', '^')
|
||||
case FormatType.HIGHLIGHT:
|
||||
return wrapSelection(markdownContentLines, selection, '==', '==')
|
||||
return wrapSelection(markdownContent, selection, '==', '==')
|
||||
case FormatType.CODE_FENCE:
|
||||
return wrapSelection(
|
||||
markdownContentLines,
|
||||
changeCursorsToWholeLineIfNoToCursor(markdownContentLines, selection),
|
||||
markdownContent,
|
||||
changeCursorsToWholeLineIfNoToCursor(markdownContent, selection),
|
||||
'```\n',
|
||||
'\n```'
|
||||
)
|
||||
case FormatType.UNORDERED_LIST:
|
||||
return replaceLinesOfSelection(markdownContentLines, selection, (line) => `- ${line}`)
|
||||
return prependLinesOfSelection(markdownContent, selection, () => `- `)
|
||||
case FormatType.ORDERED_LIST:
|
||||
return replaceLinesOfSelection(
|
||||
markdownContentLines,
|
||||
return prependLinesOfSelection(
|
||||
markdownContent,
|
||||
selection,
|
||||
(line, lineIndexInBlock) => `${lineIndexInBlock + 1}. ${line}`
|
||||
(line, lineIndexInBlock) => `${lineIndexInBlock + 1}. `
|
||||
)
|
||||
case FormatType.CHECK_LIST:
|
||||
return replaceLinesOfSelection(markdownContentLines, selection, (line) => `- [ ] ${line}`)
|
||||
return prependLinesOfSelection(markdownContent, selection, () => `- [ ] `)
|
||||
case FormatType.QUOTES:
|
||||
return replaceLinesOfSelection(markdownContentLines, selection, (line) => `> ${line}`)
|
||||
return prependLinesOfSelection(markdownContent, selection, () => `> `)
|
||||
case FormatType.HEADER_LEVEL:
|
||||
return replaceLinesOfSelection(markdownContentLines, selection, (line) =>
|
||||
line.startsWith('#') ? `#${line}` : `# ${line}`
|
||||
)
|
||||
return prependLinesOfSelection(markdownContent, selection, (line) => (line.startsWith('#') ? `#` : `# `))
|
||||
case FormatType.HORIZONTAL_LINE:
|
||||
return replaceSelection(markdownContentLines, { from: selection.to ?? selection.from }, '\n----')
|
||||
return replaceSelection(markdownContent, { from: selection.to ?? selection.from }, '\n----')
|
||||
case FormatType.COMMENT:
|
||||
return replaceSelection(markdownContentLines, { from: selection.to ?? selection.from }, '\n> []')
|
||||
return replaceSelection(markdownContent, { from: selection.to ?? selection.from }, '\n> []')
|
||||
case FormatType.COLLAPSIBLE_BLOCK:
|
||||
return wrapSelection(
|
||||
markdownContentLines,
|
||||
changeCursorsToWholeLineIfNoToCursor(markdownContentLines, selection),
|
||||
markdownContent,
|
||||
changeCursorsToWholeLineIfNoToCursor(markdownContent, selection),
|
||||
':::spoiler Toggle label\n',
|
||||
'\n:::'
|
||||
)
|
||||
case FormatType.LINK:
|
||||
return addLink(markdownContentLines, selection)
|
||||
return addLink(markdownContent, selection)
|
||||
case FormatType.IMAGE_LINK:
|
||||
return addLink(markdownContentLines, selection, '!')
|
||||
return addLink(markdownContent, selection, '!')
|
||||
default:
|
||||
return markdownContentLines
|
||||
return [markdownContent, selection]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,91 +9,78 @@ import { addLink } from './add-link'
|
|||
describe('add link', () => {
|
||||
describe('without to-cursor', () => {
|
||||
it('inserts a link', () => {
|
||||
const actual = addLink([''], { from: { line: 0, character: 0 } }, '')
|
||||
expect(actual).toEqual(['[](https://)'])
|
||||
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: { line: 0, character: 1 } }, '')
|
||||
expect(actual).toEqual(['a[](https://)a'])
|
||||
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: { line: 0, character: 0 } }, 'prefix')
|
||||
expect(actual).toEqual(['prefix[](https://)'])
|
||||
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'],
|
||||
'a',
|
||||
{
|
||||
from: { line: 0, character: 0 },
|
||||
to: {
|
||||
line: 0,
|
||||
character: 1
|
||||
}
|
||||
from: 0,
|
||||
to: 1
|
||||
},
|
||||
''
|
||||
)
|
||||
expect(actual).toEqual(['[a](https://)'])
|
||||
expect(actual).toEqual(['[a](https://)', { from: 0, to: 13 }])
|
||||
})
|
||||
|
||||
it('wraps the selection inside of a line', () => {
|
||||
const actual = addLink(['aba'], { from: { line: 0, character: 1 }, to: { line: 0, character: 2 } }, '')
|
||||
expect(actual).toEqual(['a[b](https://)a'])
|
||||
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: { line: 0, character: 0 }, to: { line: 0, character: 1 } }, 'prefix')
|
||||
expect(actual).toEqual(['prefix[a](https://)'])
|
||||
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', 'b', 'c'], { from: { line: 0, character: 0 }, to: { line: 2, character: 1 } }, '')
|
||||
expect(actual).toEqual(['[a', 'b', 'c](https://)'])
|
||||
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'],
|
||||
'https://google.com',
|
||||
{
|
||||
from: { line: 0, character: 0 },
|
||||
to: {
|
||||
line: 0,
|
||||
character: 18
|
||||
}
|
||||
from: 0,
|
||||
to: 18
|
||||
},
|
||||
''
|
||||
)
|
||||
expect(actual).toEqual(['[](https://google.com)'])
|
||||
expect(actual).toEqual(['[](https://google.com)', { from: 0, to: 22 }])
|
||||
})
|
||||
|
||||
it('wraps the selection with a prefix', () => {
|
||||
const actual = addLink(
|
||||
['https://google.com'],
|
||||
'https://google.com',
|
||||
{
|
||||
from: { line: 0, character: 0 },
|
||||
to: {
|
||||
line: 0,
|
||||
character: 18
|
||||
}
|
||||
from: 0,
|
||||
to: 18
|
||||
},
|
||||
'prefix'
|
||||
)
|
||||
expect(actual).toEqual(['prefix[](https://google.com)'])
|
||||
expect(actual).toEqual(['prefix[](https://google.com)', { from: 0, to: 28 }])
|
||||
})
|
||||
|
||||
it(`wraps a multi line selection not as link`, () => {
|
||||
const actual = addLink(
|
||||
['a', 'https://google.com', 'c'],
|
||||
{ from: { line: 0, character: 0 }, to: { line: 2, character: 1 } },
|
||||
''
|
||||
)
|
||||
expect(actual).toEqual(['[a', 'https://google.com', 'c](https://)'])
|
||||
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 }])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -15,29 +15,23 @@ const afterLink = ')'
|
|||
/**
|
||||
* Creates a copy of the given markdown content lines but inserts a new link tag.
|
||||
*
|
||||
* @param markdownContentLines The lines of the document to modify
|
||||
* @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 = (markdownContentLines: string[], selection: CursorSelection, prefix = ''): string[] => {
|
||||
export const addLink = (
|
||||
markdownContent: string,
|
||||
selection: CursorSelection,
|
||||
prefix = ''
|
||||
): [string, CursorSelection] => {
|
||||
const from = selection.from
|
||||
const to = selection.to ?? from
|
||||
|
||||
return markdownContentLines.map((currentLine, currentLineIndex) => {
|
||||
if (from.line === to.line && currentLineIndex === from.line) {
|
||||
const selectedText = markdownContentLines[from.line].slice(from.character, to.character)
|
||||
const link = buildLink(selectedText, prefix)
|
||||
return stringSplice(currentLine, from.character, link, selectedText.length)
|
||||
} else if (currentLineIndex === from.line) {
|
||||
return stringSplice(currentLine, from.character, beforeDescription)
|
||||
} else if (currentLineIndex === to.line) {
|
||||
return stringSplice(currentLine, to.character, afterDescriptionBeforeLink + defaultUrl + afterLink)
|
||||
} else {
|
||||
return currentLine
|
||||
}
|
||||
})
|
||||
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 => {
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 }])
|
||||
})
|
||||
})
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 }]
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { replaceLinesOfSelection } from './replace-lines-of-selection'
|
||||
|
||||
describe('replace lines of selection', () => {
|
||||
it('replaces only the from-cursor line if no to-cursor is present', () => {
|
||||
const actual = replaceLinesOfSelection(
|
||||
['a', 'b', 'c'],
|
||||
{
|
||||
from: {
|
||||
line: 1,
|
||||
character: 123
|
||||
}
|
||||
},
|
||||
(line, lineIndexInBlock) => `text_${line}_${lineIndexInBlock}`
|
||||
)
|
||||
expect(actual).toEqual(['a', 'text_b_0', 'c'])
|
||||
})
|
||||
|
||||
it('replaces only one line if from-cursor and to-cursor are in the same line', () => {
|
||||
const actual = replaceLinesOfSelection(
|
||||
['a', 'b', 'c'],
|
||||
{
|
||||
from: {
|
||||
line: 1,
|
||||
character: 12
|
||||
},
|
||||
to: {
|
||||
line: 1,
|
||||
character: 34
|
||||
}
|
||||
},
|
||||
(line, lineIndexInBlock) => `text_${line}_${lineIndexInBlock}`
|
||||
)
|
||||
expect(actual).toEqual(['a', 'text_b_0', 'c'])
|
||||
})
|
||||
|
||||
it('replaces multiple lines', () => {
|
||||
const actual = replaceLinesOfSelection(
|
||||
['a', 'b', 'c', 'd', 'e'],
|
||||
{
|
||||
from: {
|
||||
line: 1,
|
||||
character: 1
|
||||
},
|
||||
to: {
|
||||
line: 3,
|
||||
character: 1
|
||||
}
|
||||
},
|
||||
(line, lineIndexInBlock) => `text_${line}_${lineIndexInBlock}`
|
||||
)
|
||||
expect(actual).toEqual(['a', 'text_b_0', 'text_c_1', 'text_d_2', 'e'])
|
||||
})
|
||||
})
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { CursorSelection } from '../../../editor/types'
|
||||
|
||||
/**
|
||||
* 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 replaceLinesOfSelection = (
|
||||
markdownContentLines: string[],
|
||||
selection: CursorSelection,
|
||||
replacer: (line: string, lineIndexInBlock: number) => string
|
||||
): string[] => {
|
||||
const toLineIndex = selection.to?.line ?? selection.from.line
|
||||
return markdownContentLines.map((currentLine, currentLineIndex) => {
|
||||
if (currentLineIndex < selection.from.line || currentLineIndex > toLineIndex) {
|
||||
return currentLine
|
||||
} else {
|
||||
const lineIndexInBlock = currentLineIndex - selection.from.line
|
||||
return replacer(currentLine, lineIndexInBlock)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -9,69 +9,48 @@ 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'],
|
||||
'text1',
|
||||
{
|
||||
from: {
|
||||
line: 0,
|
||||
character: 2
|
||||
}
|
||||
from: 2
|
||||
},
|
||||
'text2'
|
||||
)
|
||||
expect(actual).toEqual(['tetext2xt1'])
|
||||
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'],
|
||||
'text1',
|
||||
{
|
||||
from: {
|
||||
line: 0,
|
||||
character: 2
|
||||
},
|
||||
to: {
|
||||
line: 0,
|
||||
character: 2
|
||||
}
|
||||
from: 2,
|
||||
to: 2
|
||||
},
|
||||
'text2'
|
||||
)
|
||||
expect(actual).toEqual(['tetext2xt1'])
|
||||
expect(actual).toEqual(['tetext2xt1', { from: 2, to: 7 }])
|
||||
})
|
||||
|
||||
it('replaces a single line text', () => {
|
||||
const actual = replaceSelection(
|
||||
['text1', 'text2', 'text3'],
|
||||
'text1\ntext2\ntext3',
|
||||
{
|
||||
from: {
|
||||
line: 1,
|
||||
character: 1
|
||||
},
|
||||
to: {
|
||||
line: 1,
|
||||
character: 2
|
||||
}
|
||||
from: 7,
|
||||
to: 8
|
||||
},
|
||||
'text4'
|
||||
)
|
||||
expect(actual).toEqual(['text1', 'ttext4xt2', 'text3'])
|
||||
expect(actual).toEqual(['text1\nttext4xt2\ntext3', { from: 7, to: 12 }])
|
||||
})
|
||||
|
||||
it('replaces a multi line text', () => {
|
||||
const actual = replaceSelection(
|
||||
['text1', 'text2', 'text3'],
|
||||
'text1\ntext2\ntext3',
|
||||
{
|
||||
from: {
|
||||
line: 0,
|
||||
character: 2
|
||||
},
|
||||
to: {
|
||||
line: 2,
|
||||
character: 3
|
||||
}
|
||||
from: 2,
|
||||
to: 15
|
||||
},
|
||||
'text4'
|
||||
)
|
||||
expect(actual).toEqual(['tetext4', 't3'])
|
||||
expect(actual).toEqual(['tetext4t3', { from: 2, to: 7 }])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -5,87 +5,25 @@
|
|||
*/
|
||||
|
||||
import { stringSplice } from './utils/string-splice'
|
||||
import type { CursorPosition, CursorSelection } from '../../../editor/types'
|
||||
import type { CursorSelection } from '../../../editor/types'
|
||||
|
||||
/**
|
||||
* Creates a new {@link NoteDetails note state} but replaces the selected text.
|
||||
*
|
||||
* @param markdownContentLines The lines of the document to modify
|
||||
* @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 = (
|
||||
markdownContentLines: string[],
|
||||
markdownContent: string,
|
||||
selection: CursorSelection,
|
||||
insertText: string
|
||||
): string[] => {
|
||||
): [string, CursorSelection] => {
|
||||
const fromCursor = selection.from
|
||||
const toCursor = selection.to ?? selection.from
|
||||
const processLine = fromCursor.line === toCursor.line ? processSingleLineSelection : processMultiLineSelection
|
||||
return markdownContentLines
|
||||
.map((currentLine, currentLineIndex) =>
|
||||
processLine(currentLine, currentLineIndex, insertText, fromCursor, toCursor)
|
||||
)
|
||||
.filter((currentLine, currentLineIndex) => filterLinesBetweenFromAndTo(currentLineIndex, fromCursor, toCursor))
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters out every line that is between the from and the to cursor.
|
||||
*
|
||||
* @param currentLineIndex The index of the current line
|
||||
* @param fromCursor The cursor position where the selection starts
|
||||
* @param toCursor The cursor position where the selection ends
|
||||
* @return {@code true} if the line should be present, {@code false} if it should be omitted.
|
||||
*/
|
||||
const filterLinesBetweenFromAndTo = (currentLineIndex: number, fromCursor: CursorPosition, toCursor: CursorPosition) =>
|
||||
currentLineIndex <= fromCursor.line || currentLineIndex >= toCursor.line
|
||||
|
||||
/**
|
||||
* Modifies a line if the selection is only in one line.
|
||||
*
|
||||
* @param line The current line content
|
||||
* @param lineIndex The index of the current line in the document
|
||||
* @param insertText The text to insert at the from cursor
|
||||
* @param fromCursor The cursor position where the selection starts
|
||||
* @param toCursor The cursor position where the selection ends
|
||||
* @return the modified line if the current line index matches the line index in the from cursor position, the unmodified line otherwise.
|
||||
*/
|
||||
const processSingleLineSelection = (
|
||||
line: string,
|
||||
lineIndex: number,
|
||||
insertText: string,
|
||||
fromCursor: CursorPosition,
|
||||
toCursor: CursorPosition
|
||||
) => {
|
||||
return lineIndex !== fromCursor.line
|
||||
? line
|
||||
: stringSplice(line, fromCursor.character, insertText, toCursor.character - fromCursor.character)
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the start and the end line of a multi line selection by cutting the tail and head of these lines.
|
||||
*
|
||||
* @param line The current line content
|
||||
* @param lineIndex The index of the current line in the document
|
||||
* @param insertText The text to insert at the from cursor
|
||||
* @param fromCursor The cursor position where the selection starts
|
||||
* @param toCursor The cursor position where the selection ends
|
||||
* @return The modified line if it's the line at the from/to cursor position. The lines between will be unmodified because a filter will take care of them.
|
||||
*/
|
||||
const processMultiLineSelection = (
|
||||
line: string,
|
||||
lineIndex: number,
|
||||
insertText: string,
|
||||
fromCursor: CursorPosition,
|
||||
toCursor: CursorPosition
|
||||
) => {
|
||||
if (lineIndex === fromCursor.line) {
|
||||
return line.slice(0, fromCursor.character) + insertText
|
||||
} else if (lineIndex === toCursor.line) {
|
||||
return line.slice(toCursor.character)
|
||||
} else {
|
||||
return line
|
||||
}
|
||||
const newContent = stringSplice(markdownContent, fromCursor, insertText, toCursor - fromCursor)
|
||||
return [newContent, { from: fromCursor, to: insertText.length + fromCursor }]
|
||||
}
|
||||
|
|
|
@ -4,57 +4,92 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { changeCursorsToWholeLineIfNoToCursor } from './change-cursors-to-whole-line-if-no-to-cursor'
|
||||
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: {
|
||||
line: 0,
|
||||
character: 0
|
||||
},
|
||||
to: {
|
||||
line: 0,
|
||||
character: 0
|
||||
}
|
||||
from: 0,
|
||||
to: 0
|
||||
}
|
||||
|
||||
expect(changeCursorsToWholeLineIfNoToCursor([], givenSelection)).toEqual(givenSelection)
|
||||
expect(changeCursorsToWholeLineIfNoToCursor('', givenSelection)).toEqual(givenSelection)
|
||||
})
|
||||
|
||||
it(`returns the corrected selection if to cursor isn't present and referred line does exist`, () => {
|
||||
it(`returns the corrected selection if cursor is in a line`, () => {
|
||||
const givenSelection = {
|
||||
from: {
|
||||
line: 0,
|
||||
character: 123
|
||||
}
|
||||
from: 9
|
||||
}
|
||||
|
||||
const expectedSelection: CursorSelection = {
|
||||
from: {
|
||||
line: 0,
|
||||
character: 0
|
||||
},
|
||||
to: {
|
||||
line: 0,
|
||||
character: 27
|
||||
}
|
||||
from: 6,
|
||||
to: 14
|
||||
}
|
||||
|
||||
expect(changeCursorsToWholeLineIfNoToCursor([`I'm a friendly test string!`], givenSelection)).toEqual(
|
||||
expect(changeCursorsToWholeLineIfNoToCursor(`I'm a\nfriendly\ntest string!`, givenSelection)).toEqual(
|
||||
expectedSelection
|
||||
)
|
||||
})
|
||||
|
||||
it(`fails if to cursor isn't present and referred line doesn't exist`, () => {
|
||||
it(`returns the corrected selection if cursor is out of bounds`, () => {
|
||||
const givenSelection = {
|
||||
from: {
|
||||
line: 1,
|
||||
character: 123
|
||||
}
|
||||
from: 123
|
||||
}
|
||||
|
||||
expect(() => changeCursorsToWholeLineIfNoToCursor([''], givenSelection)).toThrow()
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -5,31 +5,66 @@
|
|||
*/
|
||||
|
||||
import type { CursorSelection } from '../../../../editor/types'
|
||||
import Optional from 'optional-js'
|
||||
|
||||
/**
|
||||
* 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 markdownContentLines The markdown content lines that are used to calculate the line length for the to cursor
|
||||
* @param selection The selection to check
|
||||
* @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 = (
|
||||
markdownContentLines: string[],
|
||||
markdownContent: string,
|
||||
selection: CursorSelection
|
||||
): CursorSelection =>
|
||||
selection.to !== undefined
|
||||
? selection
|
||||
: Optional.ofNullable(markdownContentLines[selection.from.line])
|
||||
.map((line) => ({
|
||||
from: {
|
||||
line: selection.from.line,
|
||||
character: 0
|
||||
},
|
||||
to: {
|
||||
line: selection.from.line,
|
||||
character: line.length
|
||||
}
|
||||
}))
|
||||
.orElseThrow(() => new Error(`No line with index ${selection.from.line} found.`))
|
||||
): 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
|
||||
}
|
||||
|
|
|
@ -9,57 +9,42 @@ import { wrapSelection } from './wrap-selection'
|
|||
describe('wrap selection', () => {
|
||||
it(`doesn't modify any line if no to-cursor is present`, () => {
|
||||
const actual = wrapSelection(
|
||||
['a', 'b', 'c'],
|
||||
'a\nb\nc',
|
||||
{
|
||||
from: {
|
||||
line: 0,
|
||||
character: 0
|
||||
}
|
||||
from: 0
|
||||
},
|
||||
'before',
|
||||
'after'
|
||||
)
|
||||
|
||||
expect(actual).toEqual(['a', 'b', 'c'])
|
||||
expect(actual).toStrictEqual(['a\nb\nc', { from: 0 }])
|
||||
})
|
||||
|
||||
it(`wraps the selected text in the same line`, () => {
|
||||
const actual = wrapSelection(
|
||||
['a', 'b', 'c'],
|
||||
'a\nb\nc',
|
||||
{
|
||||
from: {
|
||||
line: 0,
|
||||
character: 0
|
||||
},
|
||||
to: {
|
||||
line: 0,
|
||||
character: 1
|
||||
}
|
||||
from: 0,
|
||||
to: 1
|
||||
},
|
||||
'before',
|
||||
'after'
|
||||
)
|
||||
|
||||
expect(actual).toEqual(['beforeaafter', 'b', 'c'])
|
||||
expect(actual).toStrictEqual(['beforeaafter\nb\nc', { from: 0, to: 12 }])
|
||||
})
|
||||
|
||||
it(`wraps the selected text in different lines`, () => {
|
||||
const actual = wrapSelection(
|
||||
['a', 'b', 'c'],
|
||||
'a\nb\nc',
|
||||
{
|
||||
from: {
|
||||
line: 0,
|
||||
character: 0
|
||||
},
|
||||
to: {
|
||||
line: 2,
|
||||
character: 1
|
||||
}
|
||||
from: 0,
|
||||
to: 5
|
||||
},
|
||||
'before',
|
||||
'after'
|
||||
)
|
||||
|
||||
expect(actual).toEqual(['beforea', 'b', 'cafter'])
|
||||
expect(actual).toStrictEqual(['beforea\nb\ncafter', { from: 0, to: 16 }])
|
||||
})
|
||||
})
|
||||
|
|
|
@ -10,7 +10,7 @@ import type { CursorSelection } from '../../../editor/types'
|
|||
/**
|
||||
* Creates a copy of the given markdown content lines but wraps the selection.
|
||||
*
|
||||
* @param markdownContentLines The lines of the document to modify
|
||||
* @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
|
||||
|
@ -18,30 +18,19 @@ import type { CursorSelection } from '../../../editor/types'
|
|||
* @return the modified copy of lines
|
||||
*/
|
||||
export const wrapSelection = (
|
||||
markdownContentLines: string[],
|
||||
markdownContent: string,
|
||||
selection: CursorSelection,
|
||||
symbolStart: string,
|
||||
symbolEnd: string
|
||||
): string[] => {
|
||||
): [string, CursorSelection] => {
|
||||
if (selection.to === undefined) {
|
||||
return markdownContentLines
|
||||
return [markdownContent, selection]
|
||||
}
|
||||
|
||||
const to = selection.to ?? selection.from
|
||||
const from = selection.from
|
||||
|
||||
return markdownContentLines.map((currentLine, currentLineIndex) => {
|
||||
if (currentLineIndex === to.line) {
|
||||
if (to.line === from.line) {
|
||||
const moddedLine = stringSplice(currentLine, to.character, symbolEnd)
|
||||
return stringSplice(moddedLine, from.character, symbolStart)
|
||||
} else {
|
||||
return stringSplice(currentLine, to.character, symbolEnd)
|
||||
}
|
||||
} else if (currentLineIndex === from.line) {
|
||||
return stringSplice(currentLine, from.character, symbolStart)
|
||||
} else {
|
||||
return currentLine
|
||||
}
|
||||
})
|
||||
const afterToModify = stringSplice(markdownContent, to, symbolEnd)
|
||||
const afterFromModify = stringSplice(afterToModify, from, symbolStart)
|
||||
return [afterFromModify, { from, to: to + symbolEnd.length + symbolStart.length }]
|
||||
}
|
||||
|
|
|
@ -18,9 +18,12 @@ export const initialSlideOptions: SlideOptions = {
|
|||
}
|
||||
|
||||
export const initialState: NoteDetails = {
|
||||
markdownContent: '',
|
||||
markdownContentLines: [],
|
||||
selection: { from: { line: 0, character: 0 } },
|
||||
markdownContent: {
|
||||
plain: '',
|
||||
lines: [],
|
||||
lineStartIndexes: []
|
||||
},
|
||||
selection: { from: 0 },
|
||||
rawFrontmatter: '',
|
||||
frontmatterRendererInfo: {
|
||||
frontmatterInvalid: false,
|
||||
|
|
|
@ -19,7 +19,7 @@ import type {
|
|||
UpdateTaskListCheckboxAction
|
||||
} from './types'
|
||||
import { NoteDetailsActionType } from './types'
|
||||
import type { CursorPosition, CursorSelection } from '../editor/types'
|
||||
import type { CursorSelection } from '../editor/types'
|
||||
|
||||
/**
|
||||
* Sets the content of the current note, extracts and parses the frontmatter and extracts the markdown content part.
|
||||
|
@ -83,35 +83,12 @@ export const replaceInMarkdownContent = (replaceable: string, replacement: strin
|
|||
}
|
||||
|
||||
export const updateCursorPositions = (selection: CursorSelection): void => {
|
||||
const correctedSelection: CursorSelection = isFromAfterTo(selection)
|
||||
? {
|
||||
to: selection.from,
|
||||
from: selection.to as CursorPosition
|
||||
}
|
||||
: selection
|
||||
|
||||
store.dispatch({
|
||||
type: NoteDetailsActionType.UPDATE_CURSOR_POSITION,
|
||||
selection: correctedSelection
|
||||
selection
|
||||
} as UpdateCursorPositionAction)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the from cursor position in the given selection is after the to cursor position.
|
||||
*
|
||||
* @param selection The cursor selection to check
|
||||
* @return {@code true} if the from cursor position is after the to position
|
||||
*/
|
||||
const isFromAfterTo = (selection: CursorSelection): boolean => {
|
||||
if (selection.to === undefined) {
|
||||
return false
|
||||
}
|
||||
if (selection.from.line < selection.to.line) {
|
||||
return false
|
||||
}
|
||||
return selection.from.line !== selection.to.line || selection.from.character > selection.to.character
|
||||
}
|
||||
|
||||
export const formatSelection = (formatType: FormatType): void => {
|
||||
store.dispatch({
|
||||
type: NoteDetailsActionType.FORMAT_SELECTION,
|
||||
|
|
|
@ -36,19 +36,15 @@ describe('build state from add table at cursor', () => {
|
|||
const actual = buildStateFromAddTableAtCursor(
|
||||
{
|
||||
...initialState,
|
||||
markdownContentLines: ['a', 'b', 'c'],
|
||||
markdownContent: 'a\nb\nc',
|
||||
markdownContent: { plain: 'a\nb\nc', lines: ['a', 'b', 'c'], lineStartIndexes: [0, 2, 4] },
|
||||
selection: {
|
||||
from: {
|
||||
line: 1,
|
||||
character: 0
|
||||
}
|
||||
from: 2
|
||||
}
|
||||
},
|
||||
3,
|
||||
3
|
||||
)
|
||||
expect(actual.markdownContent).toEqual(
|
||||
expect(actual.markdownContent.plain).toEqual(
|
||||
'a\n\n| # 1 | # 2 | # 3 |\n' +
|
||||
'| ---- | ---- | ---- |\n' +
|
||||
'| Text | Text | Text |\n' +
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import type { NoteDetails } from '../types/note-details'
|
||||
import { buildStateFromUpdatedMarkdownContentLines } from '../build-state-from-updated-markdown-content'
|
||||
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'
|
||||
|
||||
|
@ -19,10 +19,16 @@ import { createNumberRangeArray } from '../../../components/common/number-range/
|
|||
*/
|
||||
export const buildStateFromAddTableAtCursor = (state: NoteDetails, rows: number, columns: number): NoteDetails => {
|
||||
const table = createMarkdownTable(rows, columns)
|
||||
return buildStateFromUpdatedMarkdownContentLines(
|
||||
state,
|
||||
replaceSelection(state.markdownContentLines, { from: state.selection.to ?? state.selection.from }, table)
|
||||
const [newContent, newSelection] = replaceSelection(
|
||||
state.markdownContent.plain,
|
||||
{ from: state.selection.to ?? state.selection.from },
|
||||
table
|
||||
)
|
||||
const newState = buildStateFromUpdatedMarkdownContent(state, newContent)
|
||||
return {
|
||||
...newState,
|
||||
selection: newSelection
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -26,7 +26,10 @@ describe('build state from replace in markdown content', () => {
|
|||
})
|
||||
|
||||
it('updates the markdown content with the replacement', () => {
|
||||
const startState = { ...initialState, markdownContent: 'replaceable' }
|
||||
const startState: NoteDetails = {
|
||||
...initialState,
|
||||
markdownContent: { ...initialState.markdownContent, plain: 'replaceable' }
|
||||
}
|
||||
const result = buildStateFromReplaceInMarkdownContent(startState, 'replaceable', 'replacement')
|
||||
expect(result).toBe(mockedNoteDetails)
|
||||
expect(buildStateFromUpdatedMarkdownContentMock).toHaveBeenCalledWith(startState, 'replacement')
|
||||
|
|
|
@ -35,5 +35,5 @@ export const buildStateFromReplaceInMarkdownContent = (
|
|||
replaceable: string,
|
||||
replacement: string
|
||||
): NoteDetails => {
|
||||
return buildStateFromUpdatedMarkdownContent(state, replaceAll(state.markdownContent, replaceable, replacement))
|
||||
return buildStateFromUpdatedMarkdownContent(state, replaceAll(state.markdownContent.plain, replaceable, replacement))
|
||||
}
|
||||
|
|
|
@ -13,47 +13,55 @@ import { initialState } from '../initial-state'
|
|||
import type { CursorSelection } from '../../editor/types'
|
||||
|
||||
describe('build state from replace selection', () => {
|
||||
const buildStateFromUpdatedMarkdownContentLinesMock = jest.spyOn(
|
||||
const buildStateFromUpdatedMarkdownContentMock = jest.spyOn(
|
||||
buildStateFromUpdatedMarkdownContentLinesModule,
|
||||
'buildStateFromUpdatedMarkdownContentLines'
|
||||
'buildStateFromUpdatedMarkdownContent'
|
||||
)
|
||||
const replaceSelectionMock = jest.spyOn(replaceSelectionModule, 'replaceSelection')
|
||||
const mockedNoteDetails = Mock.of<NoteDetails>()
|
||||
const mockedReplacedLines = ['replaced']
|
||||
const mockedNoteDetails = { content: 'mocked' } as unknown as NoteDetails
|
||||
const mockedFormattedContent = 'formatted'
|
||||
const mockedCursor = Mock.of<CursorSelection>()
|
||||
|
||||
beforeAll(() => {
|
||||
buildStateFromUpdatedMarkdownContentLinesMock.mockImplementation(() => mockedNoteDetails)
|
||||
replaceSelectionMock.mockImplementation(() => mockedReplacedLines)
|
||||
buildStateFromUpdatedMarkdownContentMock.mockImplementation(() => mockedNoteDetails)
|
||||
replaceSelectionMock.mockImplementation(() => [mockedFormattedContent, mockedCursor])
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
buildStateFromUpdatedMarkdownContentLinesMock.mockReset()
|
||||
buildStateFromUpdatedMarkdownContentMock.mockReset()
|
||||
replaceSelectionMock.mockReset()
|
||||
})
|
||||
|
||||
it('builds a new state with the provided cursor', () => {
|
||||
const originalLines = ['original']
|
||||
const startState = { ...initialState, markdownContentLines: originalLines }
|
||||
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).toBe(mockedNoteDetails)
|
||||
expect(buildStateFromUpdatedMarkdownContentLinesMock).toHaveBeenCalledWith(startState, mockedReplacedLines)
|
||||
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 originalLines = 'original'
|
||||
const selection = Mock.of<CursorSelection>()
|
||||
const startState = { ...initialState, markdownContentLines: originalLines, selection }
|
||||
const startState: NoteDetails = {
|
||||
...initialState,
|
||||
markdownContent: { plain: originalLines, lines: [originalLines], lineStartIndexes: [0] },
|
||||
selection
|
||||
}
|
||||
const textReplacement = 'replacement'
|
||||
|
||||
const result = buildStateFromReplaceSelection(startState, 'replacement')
|
||||
|
||||
expect(result).toBe(mockedNoteDetails)
|
||||
expect(buildStateFromUpdatedMarkdownContentLinesMock).toHaveBeenCalledWith(startState, mockedReplacedLines)
|
||||
expect(result).toStrictEqual({ content: 'mocked', selection: mockedCursor })
|
||||
expect(buildStateFromUpdatedMarkdownContentMock).toHaveBeenCalledWith(startState, mockedFormattedContent)
|
||||
expect(replaceSelectionMock).toHaveBeenCalledWith(originalLines, selection, textReplacement)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -6,12 +6,18 @@
|
|||
|
||||
import type { NoteDetails } from '../types/note-details'
|
||||
import type { CursorSelection } from '../../editor/types'
|
||||
import { buildStateFromUpdatedMarkdownContentLines } from '../build-state-from-updated-markdown-content'
|
||||
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) => {
|
||||
return buildStateFromUpdatedMarkdownContentLines(
|
||||
state,
|
||||
replaceSelection(state.markdownContentLines, cursorSelection ? cursorSelection : state.selection, text)
|
||||
const [newContent, newSelection] = replaceSelection(
|
||||
state.markdownContent.plain,
|
||||
cursorSelection ? cursorSelection : state.selection,
|
||||
text
|
||||
)
|
||||
const newState = buildStateFromUpdatedMarkdownContent(state, newContent)
|
||||
return {
|
||||
...newState,
|
||||
selection: newSelection
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,34 +14,38 @@ import { FormatType } from '../types'
|
|||
import type { CursorSelection } from '../../editor/types'
|
||||
|
||||
describe('build state from selection format', () => {
|
||||
const buildStateFromUpdatedMarkdownContentLinesMock = jest.spyOn(
|
||||
const buildStateFromUpdatedMarkdownContentMock = jest.spyOn(
|
||||
buildStateFromUpdatedMarkdownContentLinesModule,
|
||||
'buildStateFromUpdatedMarkdownContentLines'
|
||||
'buildStateFromUpdatedMarkdownContent'
|
||||
)
|
||||
const mockedNoteDetails = Mock.of<NoteDetails>()
|
||||
const mockedNoteDetails = { content: 'mocked' } as unknown as NoteDetails
|
||||
const applyFormatTypeToMarkdownLinesMock = jest.spyOn(
|
||||
applyFormatTypeToMarkdownLinesModule,
|
||||
'applyFormatTypeToMarkdownLines'
|
||||
)
|
||||
const mockedFormattedLines = ['formatted']
|
||||
const mockedFormattedContent = 'formatted'
|
||||
const mockedCursor = Mock.of<CursorSelection>()
|
||||
|
||||
beforeAll(() => {
|
||||
buildStateFromUpdatedMarkdownContentLinesMock.mockImplementation(() => mockedNoteDetails)
|
||||
applyFormatTypeToMarkdownLinesMock.mockImplementation(() => mockedFormattedLines)
|
||||
buildStateFromUpdatedMarkdownContentMock.mockImplementation(() => mockedNoteDetails)
|
||||
applyFormatTypeToMarkdownLinesMock.mockImplementation(() => [mockedFormattedContent, mockedCursor])
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
buildStateFromUpdatedMarkdownContentLinesMock.mockReset()
|
||||
buildStateFromUpdatedMarkdownContentMock.mockReset()
|
||||
applyFormatTypeToMarkdownLinesMock.mockReset()
|
||||
})
|
||||
|
||||
it('builds a new state with the formatted code', () => {
|
||||
const originalLines = ['original']
|
||||
const customCursor = Mock.of<CursorSelection>()
|
||||
const startState = { ...initialState, markdownContentLines: originalLines, selection: customCursor }
|
||||
const originalContent = 'original'
|
||||
const startState: NoteDetails = {
|
||||
...initialState,
|
||||
markdownContent: { ...initialState.markdownContent, plain: originalContent },
|
||||
selection: mockedCursor
|
||||
}
|
||||
const result = buildStateFromSelectionFormat(startState, FormatType.BOLD)
|
||||
expect(result).toBe(mockedNoteDetails)
|
||||
expect(buildStateFromUpdatedMarkdownContentLinesMock).toHaveBeenCalledWith(startState, mockedFormattedLines)
|
||||
expect(applyFormatTypeToMarkdownLinesMock).toHaveBeenCalledWith(originalLines, customCursor, FormatType.BOLD)
|
||||
expect(result).toStrictEqual({ content: 'mocked', selection: mockedCursor })
|
||||
expect(buildStateFromUpdatedMarkdownContentMock).toHaveBeenCalledWith(startState, mockedFormattedContent)
|
||||
expect(applyFormatTypeToMarkdownLinesMock).toHaveBeenCalledWith(originalContent, mockedCursor, FormatType.BOLD)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -6,12 +6,14 @@
|
|||
|
||||
import type { NoteDetails } from '../types/note-details'
|
||||
import type { FormatType } from '../types'
|
||||
import { buildStateFromUpdatedMarkdownContentLines } from '../build-state-from-updated-markdown-content'
|
||||
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 => {
|
||||
return buildStateFromUpdatedMarkdownContentLines(
|
||||
state,
|
||||
applyFormatTypeToMarkdownLines(state.markdownContentLines, state.selection, type)
|
||||
)
|
||||
const [newContent, newSelection] = applyFormatTypeToMarkdownLines(state.markdownContent.plain, state.selection, type)
|
||||
const newState = buildStateFromUpdatedMarkdownContent(state, newContent)
|
||||
return {
|
||||
...newState,
|
||||
selection: newSelection
|
||||
}
|
||||
}
|
||||
|
|
|
@ -118,10 +118,12 @@ describe('build state from set note data from server', () => {
|
|||
slideOptions: initialSlideOptions
|
||||
},
|
||||
noteTitle: '',
|
||||
selection: { from: { line: 0, character: 0 } },
|
||||
|
||||
markdownContent: 'line1\nline2',
|
||||
markdownContentLines: ['line1', 'line2'],
|
||||
selection: { from: 0 },
|
||||
markdownContent: {
|
||||
plain: 'line1\nline2',
|
||||
lines: ['line1', 'line2'],
|
||||
lineStartIndexes: [0, 6]
|
||||
},
|
||||
firstHeading: '',
|
||||
rawFrontmatter: '',
|
||||
id: 'id',
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { NoteDetails } from '../types/note-details'
|
|||
import { buildStateFromUpdatedMarkdownContent } from '../build-state-from-updated-markdown-content'
|
||||
import { initialState } from '../initial-state'
|
||||
import { DateTime } from 'luxon'
|
||||
import { calculateLineStartIndexes } from '../calculate-line-start-indexes'
|
||||
|
||||
/**
|
||||
* Builds a {@link NoteDetails} redux state from a DTO received as an API response.
|
||||
|
@ -17,7 +18,7 @@ import { DateTime } from 'luxon'
|
|||
*/
|
||||
export const buildStateFromServerDto = (dto: NoteDto): NoteDetails => {
|
||||
const newState = convertNoteDtoToNoteDetails(dto)
|
||||
return buildStateFromUpdatedMarkdownContent(newState, newState.markdownContent)
|
||||
return buildStateFromUpdatedMarkdownContent(newState, newState.markdownContent.plain)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -27,10 +28,14 @@ export const buildStateFromServerDto = (dto: NoteDto): NoteDetails => {
|
|||
* @return The NoteDetails object corresponding to the DTO.
|
||||
*/
|
||||
const convertNoteDtoToNoteDetails = (note: NoteDto): NoteDetails => {
|
||||
const newLines = note.content.split('\n')
|
||||
return {
|
||||
...initialState,
|
||||
markdownContent: note.content,
|
||||
markdownContentLines: note.content.split('\n'),
|
||||
markdownContent: {
|
||||
plain: note.content,
|
||||
lines: newLines,
|
||||
lineStartIndexes: calculateLineStartIndexes(newLines)
|
||||
},
|
||||
rawFrontmatter: '',
|
||||
id: note.metadata.id,
|
||||
createTime: DateTime.fromISO(note.metadata.createTime),
|
||||
|
|
|
@ -28,14 +28,20 @@ describe('build state from task list update', () => {
|
|||
const markdownContentLines = ['no task', '- [ ] not checked', '- [x] checked']
|
||||
|
||||
it(`doesn't change the state if the line doesn't contain a task`, () => {
|
||||
const startState = { ...initialState, markdownContentLines: markdownContentLines }
|
||||
const startState: NoteDetails = {
|
||||
...initialState,
|
||||
markdownContent: { ...initialState.markdownContent, lines: markdownContentLines }
|
||||
}
|
||||
const result = buildStateFromTaskListUpdate(startState, 0, true)
|
||||
expect(result).toBe(startState)
|
||||
expect(buildStateFromUpdatedMarkdownContentLinesMock).toBeCalledTimes(0)
|
||||
})
|
||||
|
||||
it(`can change the state of a task to checked`, () => {
|
||||
const startState = { ...initialState, markdownContentLines: markdownContentLines }
|
||||
const startState: NoteDetails = {
|
||||
...initialState,
|
||||
markdownContent: { ...initialState.markdownContent, lines: markdownContentLines }
|
||||
}
|
||||
const result = buildStateFromTaskListUpdate(startState, 1, true)
|
||||
expect(result).toBe(mockedNoteDetails)
|
||||
expect(buildStateFromUpdatedMarkdownContentLinesMock).toBeCalledWith(startState, [
|
||||
|
@ -46,7 +52,10 @@ describe('build state from task list update', () => {
|
|||
})
|
||||
|
||||
it(`can change the state of a task to unchecked`, () => {
|
||||
const startState = { ...initialState, markdownContentLines: markdownContentLines }
|
||||
const startState: NoteDetails = {
|
||||
...initialState,
|
||||
markdownContent: { ...initialState.markdownContent, lines: markdownContentLines }
|
||||
}
|
||||
const result = buildStateFromTaskListUpdate(startState, 2, false)
|
||||
expect(result).toBe(mockedNoteDetails)
|
||||
expect(buildStateFromUpdatedMarkdownContentLinesMock).toBeCalledWith(startState, [
|
||||
|
|
|
@ -21,7 +21,7 @@ export const buildStateFromTaskListUpdate = (
|
|||
changedLineIndex: number,
|
||||
checkboxChecked: boolean
|
||||
): NoteDetails => {
|
||||
const lines = [...state.markdownContentLines]
|
||||
const lines = [...state.markdownContent.lines]
|
||||
return Optional.ofNullable(TASK_REGEX.exec(lines[changedLineIndex]))
|
||||
.map((results) => {
|
||||
const [, beforeCheckbox, afterCheckbox] = results
|
||||
|
|
|
@ -8,8 +8,28 @@ import type { NoteDetails } from '../types/note-details'
|
|||
import type { CursorSelection } from '../../editor/types'
|
||||
|
||||
export const buildStateFromUpdateCursorPosition = (state: NoteDetails, selection: CursorSelection): NoteDetails => {
|
||||
const correctedSelection = isFromAfterTo(selection)
|
||||
? {
|
||||
to: selection.from,
|
||||
from: selection.to as number
|
||||
}
|
||||
: selection
|
||||
|
||||
return {
|
||||
...state,
|
||||
selection
|
||||
selection: correctedSelection
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the from cursor position in the given selection is after the to cursor position.
|
||||
*
|
||||
* @param selection The cursor selection to check
|
||||
* @return {@code true} if the from cursor position is after the to position
|
||||
*/
|
||||
const isFromAfterTo = (selection: CursorSelection): boolean => {
|
||||
if (selection.to === undefined) {
|
||||
return false
|
||||
}
|
||||
return selection.from > selection.to
|
||||
}
|
||||
|
|
|
@ -13,8 +13,11 @@ import type { CursorSelection } from '../../editor/types'
|
|||
* Redux state containing the currently loaded note with its content and metadata.
|
||||
*/
|
||||
export interface NoteDetails {
|
||||
markdownContent: string
|
||||
markdownContentLines: string[]
|
||||
markdownContent: {
|
||||
plain: string
|
||||
lines: string[]
|
||||
lineStartIndexes: number[]
|
||||
}
|
||||
selection: CursorSelection
|
||||
rawFrontmatter: string
|
||||
frontmatter: NoteFrontmatter
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue