2
0
mirror of https://github.com/Ionaru/easy-markdown-editor synced 2025-06-27 13:11:01 -06:00

Some fixes

This commit is contained in:
Jeroen akkerman 2024-06-07 12:37:45 +02:00
parent 3aa82e009e
commit 779331aaa0
7 changed files with 685 additions and 260 deletions

724
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -48,6 +48,7 @@
"@types/node": "^18.16.2",
"@typescript-eslint/eslint-plugin": "^5.59.1",
"@vitest/coverage-c8": "^0.30.1",
"@vitest/coverage-v8": "^0.34.6",
"@vitest/ui": "^0.30.1",
"cypress": "^12.11.0",
"eslint": "^8.39.0",
@ -65,6 +66,6 @@
"sass": "^1.62.1",
"tslib": "^2.5.0",
"typescript": "^5.0.4",
"vitest": "^0.30.1"
"vitest": "^0.34.6"
}
}

View File

@ -1,4 +1,4 @@
/* eslint-disable sort-keys,@typescript-eslint/member-ordering,max-classes-per-file */
/* eslint-disable sort-keys,@typescript-eslint/member-ordering */
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import {
HighlightStyle,
@ -10,8 +10,8 @@ import { drawSelection, EditorView } from '@codemirror/view';
import { tags } from '@lezer/highlight';
import { marked } from 'marked';
import { AlreadyConstructedError } from "./errors/already-constructed-error";
import { NotConstructedError } from "./errors/not-constructed-error";
import { AlreadyConstructedError } from './errors/already-constructed-error';
import { NotConstructedError } from './errors/not-constructed-error';
import { importDefaultToolbar, importToolbar } from './imports';
import { InputOptions, Options } from './options';
@ -62,10 +62,6 @@ export class EasyMDE {
private static verifyAndReturnElement(
element?: HTMLElement,
): HTMLTextAreaElement {
if (!element) {
throw new Error('EasyMDE: Parameter "element" is null.');
}
if (!(element instanceof HTMLTextAreaElement)) {
throw new TypeError(
'EasyMDE: Parameter "element" must be a TextArea.',

View File

@ -4,7 +4,7 @@ import { EditorView } from '@codemirror/view';
// eslint-disable-next-line @typescript-eslint/no-shadow
import { describe, expect, it } from 'vitest';
import { checkBlock } from './toggle-block';
import { checkBlock, toggleBlock } from './toggle-block';
const getEditor = (
document: string,
@ -28,6 +28,7 @@ describe.each(['*', '**', '`', '_', '__', '~~'])(
const wordSimple = `${character}foo${character}`;
const wordSimpleWithSpace = `${character} foo ${character}`;
const wordSquished = `bla${character}foo${character}bla`;
const wordMultiWords = `${character}foo boo${character}`;
const wordMultiWord = `${character}foo${character} ${character}boo${character}`;
const wordMultiLine = `${character}foo${character}\n${character}boo${character}`;
const wordMultiTab = `${character}foo${character}\t${character}boo${character}`;
@ -234,6 +235,33 @@ describe.each(['*', '**', '`', '_', '__', '~~'])(
expect(result).toBeTruthy();
expect(result[1]).toBe('boo');
});
it('must detect an active block with selection on the first of a multi-word text', () => {
expect.assertions(2);
const anchor = Math.floor(wordMultiWords.split(' ')[0].length / 2);
const result = checkBlock(
getEditor(wordMultiWords, { anchor }),
character,
);
expect(result).toBeTruthy();
expect(result[1]).toBe('foo boo');
});
it('must detect an active block with selection on the second of a multi-word text', () => {
expect.assertions(2);
const multiWordSplit = wordMultiWords.split(' ');
const anchor =
Math.floor(multiWordSplit[1].length / 2) +
multiWordSplit[0].length;
const result = checkBlock(
getEditor(wordMultiWords, { anchor }),
character,
);
expect(result).toBeTruthy();
expect(result[1]).toBe('foo boo');
});
},
);
@ -290,3 +318,110 @@ describe('checkBlock special cases', () => {
},
);
});
describe.skip('toggleBlock', () => {
it('must toggle a single word on with the selection at the start', () => {
expect.assertions(1);
const editor = getEditor('Word', { anchor: 0 });
toggleBlock(editor, '*');
expect(editor.state.doc.toString()).toBe('*Word*');
});
it('must toggle a single word on with the selection in the middle', () => {
expect.assertions(1);
const editor = getEditor('Word', { anchor: 2 });
toggleBlock(editor, '*');
expect(editor.state.doc.toString()).toBe('*Word*');
});
it('must toggle a single word on with the selection at the end', () => {
expect.assertions(1);
const editor = getEditor('Word', { anchor: 4 });
toggleBlock(editor, '*');
expect(editor.state.doc.toString()).toBe('*Word*');
});
it('must toggle a single word on in between other words', () => {
expect.assertions(1);
const editor = getEditor('Many words are typed here', { anchor: 7 });
toggleBlock(editor, '*');
expect(editor.state.doc.toString()).toBe('Many *words* are typed here');
});
it('must toggle a single word on in between other words big selection', () => {
expect.assertions(1);
const editor = getEditor('Many words are typed here', {
anchor: 5,
head: 10,
});
toggleBlock(editor, '*');
expect(editor.state.doc.toString()).toBe('Many *words* are typed here');
});
it('must toggle a single word off with the selection at the start', () => {
expect.assertions(1);
const editor = getEditor('*Word*', { anchor: 0 });
toggleBlock(editor, '*');
expect(editor.state.doc.toString()).toBe('Word');
});
it('must toggle a single word off with the selection in the middle', () => {
expect.assertions(1);
const editor = getEditor('*Word*', { anchor: 3 });
toggleBlock(editor, '*');
expect(editor.state.doc.toString()).toBe('Word');
});
it('must toggle a single word off with the selection at the end', () => {
expect.assertions(1);
const editor = getEditor('*Word*', { anchor: 6 });
toggleBlock(editor, '*');
expect(editor.state.doc.toString()).toBe('Word');
});
it('must toggle a single word off in between other words', () => {
expect.assertions(1);
const editor = getEditor('Many *words* are typed here', { anchor: 8 });
toggleBlock(editor, '*');
expect(editor.state.doc.toString()).toBe('Many words are typed here');
});
it('must toggle a single word off in between other words big selection', () => {
expect.assertions(1);
const editor = getEditor('Many *words* are typed here', {
anchor: 6,
head: 11,
});
toggleBlock(editor, '*');
expect(editor.state.doc.toString()).toBe('Many words are typed here');
});
it('must toggle a formatted sentence off', () => {
expect.assertions(1);
const editor = getEditor('*Many words are typed here*', { anchor: 8 });
toggleBlock(editor, '*');
expect(editor.state.doc.toString()).toBe('Many words are typed here');
});
it('must toggle a formatted sentence off in a big selection', () => {
expect.assertions(1);
const editor = getEditor('*Many words are typed here*', {
anchor: 6,
head: 11,
});
toggleBlock(editor, '*');
expect(editor.state.doc.toString()).toBe('Many words are typed here');
});
});

View File

@ -2,14 +2,18 @@ import { EditorSelection, EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import escapeStringRegexp from 'escape-string-regexp';
/**
* Checks whether the selection matches a formatted block of text.
*/
export const checkBlock = (
editor: EditorView,
characters: string,
minimal = false,
): RegExpExecArray | null => {
// Checks whether the selection matches a block of formatted text.
const { state } = editor;
const { from, to } = getExpandedSelection(state, characters);
const { from, to } = getExpandedSelection(state, characters, minimal);
const text = state.sliceDoc(from, to);
const escapedCharacters = escapeStringRegexp(characters);
const regularExpression = new RegExp(
@ -22,8 +26,8 @@ export const checkBlock = (
let doubleCharactersCheckResult = null;
let tripleCharactersCheckResult = null;
if (characters.length === 1) {
doubleCharactersCheckResult = checkBlock(editor, characters.repeat(2));
tripleCharactersCheckResult = checkBlock(editor, characters.repeat(3));
doubleCharactersCheckResult = checkBlock(editor, characters.repeat(2), minimal);
tripleCharactersCheckResult = checkBlock(editor, characters.repeat(3), minimal);
}
if (
@ -43,11 +47,15 @@ export const checkBlock = (
return null;
};
/**
* Toggles a block of text to be formatted.
*/
export const toggleBlock = (editor: EditorView, characters: string) => {
const { state } = editor;
const { from, to } = getExpandedSelection(state, characters);
const text = state.sliceDoc(from, to);
const textMatch = checkBlock(editor, characters);
console.log(from, to, text, textMatch);
editor.dispatch(
state.changeByRange(() =>
@ -78,29 +86,36 @@ export const toggleBlock = (editor: EditorView, characters: string) => {
editor.focus();
};
/**
* Attempts to expand the cursor selection to the nearest logical block of text needs to be formatted.
*/
export const getExpandedSelection = (
state: EditorState,
characters: string,
minimal = false,
): { from: number; to: number } => {
let { from, to } = state.selection.main;
let fromPosition = from;
while (fromPosition > 0) {
while (fromPosition >= 0) {
const newText = state.sliceDoc(fromPosition, to);
if (
newText.startsWith('\n') ||
newText.startsWith('\t') ||
newText.startsWith(' ')
) {
if (newText.startsWith('\n') || newText.startsWith('\t')) {
fromPosition++;
break;
}
if (
} else if (minimal && newText.startsWith(' ')) {
fromPosition++;
break;
} else if (newText.startsWith(characters + ' ')) {
fromPosition += characters.length + 1;
break;
} else if (
newText.length > characters.length &&
newText.startsWith(characters)
) {
break;
}
fromPosition--;
}
from = fromPosition;
@ -108,15 +123,13 @@ export const getExpandedSelection = (
let toPosition = to;
while (toPosition < state.doc.length) {
const newText = state.sliceDoc(from, toPosition);
if (
newText.endsWith('\n') ||
newText.endsWith('\t') ||
newText.endsWith(' ')
) {
if (newText.endsWith('\n') || newText.endsWith('\t')) {
toPosition--;
break;
}
if (
} else if (minimal && newText.endsWith(' ')) {
toPosition--;
break;
} else if (
newText.length > characters.length &&
newText.endsWith(characters)
) {
@ -126,5 +139,23 @@ export const getExpandedSelection = (
}
to = toPosition;
return correctInvalidSelection({ from, to });
};
/**
* Sometimes the selection expands beyond the start of the document, which causes an error.
* This function corrects the selection if it is invalid.
*/
const correctInvalidSelection = ({
from,
to,
}: {
from: number;
to: number;
}): { from: number; to: number } => {
if (from < 0) {
from = 0;
}
return { from, to };
};

View File

@ -47,6 +47,8 @@ last
```js
const x = "moooo";
```
This is a longer sentence!
</textarea
>
<script type="module">

View File

@ -7,7 +7,7 @@ export default defineConfig({
clean: true,
enabled: true,
include: ['src/**/*.ts'],
provider: 'c8',
provider: 'v8',
},
dir: 'src',
environment: 'jsdom',