mirror of
https://github.com/Ionaru/easy-markdown-editor
synced 2025-06-28 13:41:01 -06:00
Some fixes
This commit is contained in:
parent
3aa82e009e
commit
779331aaa0
724
package-lock.json
generated
724
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -48,6 +48,7 @@
|
|||||||
"@types/node": "^18.16.2",
|
"@types/node": "^18.16.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||||
"@vitest/coverage-c8": "^0.30.1",
|
"@vitest/coverage-c8": "^0.30.1",
|
||||||
|
"@vitest/coverage-v8": "^0.34.6",
|
||||||
"@vitest/ui": "^0.30.1",
|
"@vitest/ui": "^0.30.1",
|
||||||
"cypress": "^12.11.0",
|
"cypress": "^12.11.0",
|
||||||
"eslint": "^8.39.0",
|
"eslint": "^8.39.0",
|
||||||
@ -65,6 +66,6 @@
|
|||||||
"sass": "^1.62.1",
|
"sass": "^1.62.1",
|
||||||
"tslib": "^2.5.0",
|
"tslib": "^2.5.0",
|
||||||
"typescript": "^5.0.4",
|
"typescript": "^5.0.4",
|
||||||
"vitest": "^0.30.1"
|
"vitest": "^0.34.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 { markdown, markdownLanguage } from '@codemirror/lang-markdown';
|
||||||
import {
|
import {
|
||||||
HighlightStyle,
|
HighlightStyle,
|
||||||
@ -10,8 +10,8 @@ import { drawSelection, EditorView } from '@codemirror/view';
|
|||||||
import { tags } from '@lezer/highlight';
|
import { tags } from '@lezer/highlight';
|
||||||
import { marked } from 'marked';
|
import { marked } from 'marked';
|
||||||
|
|
||||||
import { AlreadyConstructedError } from "./errors/already-constructed-error";
|
import { AlreadyConstructedError } from './errors/already-constructed-error';
|
||||||
import { NotConstructedError } from "./errors/not-constructed-error";
|
import { NotConstructedError } from './errors/not-constructed-error';
|
||||||
import { importDefaultToolbar, importToolbar } from './imports';
|
import { importDefaultToolbar, importToolbar } from './imports';
|
||||||
import { InputOptions, Options } from './options';
|
import { InputOptions, Options } from './options';
|
||||||
|
|
||||||
@ -62,10 +62,6 @@ export class EasyMDE {
|
|||||||
private static verifyAndReturnElement(
|
private static verifyAndReturnElement(
|
||||||
element?: HTMLElement,
|
element?: HTMLElement,
|
||||||
): HTMLTextAreaElement {
|
): HTMLTextAreaElement {
|
||||||
if (!element) {
|
|
||||||
throw new Error('EasyMDE: Parameter "element" is null.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(element instanceof HTMLTextAreaElement)) {
|
if (!(element instanceof HTMLTextAreaElement)) {
|
||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
'EasyMDE: Parameter "element" must be a TextArea.',
|
'EasyMDE: Parameter "element" must be a TextArea.',
|
||||||
|
@ -4,7 +4,7 @@ import { EditorView } from '@codemirror/view';
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { checkBlock } from './toggle-block';
|
import { checkBlock, toggleBlock } from './toggle-block';
|
||||||
|
|
||||||
const getEditor = (
|
const getEditor = (
|
||||||
document: string,
|
document: string,
|
||||||
@ -28,6 +28,7 @@ describe.each(['*', '**', '`', '_', '__', '~~'])(
|
|||||||
const wordSimple = `${character}foo${character}`;
|
const wordSimple = `${character}foo${character}`;
|
||||||
const wordSimpleWithSpace = `${character} foo ${character}`;
|
const wordSimpleWithSpace = `${character} foo ${character}`;
|
||||||
const wordSquished = `bla${character}foo${character}bla`;
|
const wordSquished = `bla${character}foo${character}bla`;
|
||||||
|
const wordMultiWords = `${character}foo boo${character}`;
|
||||||
const wordMultiWord = `${character}foo${character} ${character}boo${character}`;
|
const wordMultiWord = `${character}foo${character} ${character}boo${character}`;
|
||||||
const wordMultiLine = `${character}foo${character}\n${character}boo${character}`;
|
const wordMultiLine = `${character}foo${character}\n${character}boo${character}`;
|
||||||
const wordMultiTab = `${character}foo${character}\t${character}boo${character}`;
|
const wordMultiTab = `${character}foo${character}\t${character}boo${character}`;
|
||||||
@ -234,6 +235,33 @@ describe.each(['*', '**', '`', '_', '__', '~~'])(
|
|||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
expect(result[1]).toBe('boo');
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -2,14 +2,18 @@ import { EditorSelection, EditorState } from '@codemirror/state';
|
|||||||
import { EditorView } from '@codemirror/view';
|
import { EditorView } from '@codemirror/view';
|
||||||
import escapeStringRegexp from 'escape-string-regexp';
|
import escapeStringRegexp from 'escape-string-regexp';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether the selection matches a formatted block of text.
|
||||||
|
*/
|
||||||
export const checkBlock = (
|
export const checkBlock = (
|
||||||
editor: EditorView,
|
editor: EditorView,
|
||||||
characters: string,
|
characters: string,
|
||||||
|
minimal = false,
|
||||||
): RegExpExecArray | null => {
|
): RegExpExecArray | null => {
|
||||||
// Checks whether the selection matches a block of formatted text.
|
// Checks whether the selection matches a block of formatted text.
|
||||||
|
|
||||||
const { state } = editor;
|
const { state } = editor;
|
||||||
const { from, to } = getExpandedSelection(state, characters);
|
const { from, to } = getExpandedSelection(state, characters, minimal);
|
||||||
const text = state.sliceDoc(from, to);
|
const text = state.sliceDoc(from, to);
|
||||||
const escapedCharacters = escapeStringRegexp(characters);
|
const escapedCharacters = escapeStringRegexp(characters);
|
||||||
const regularExpression = new RegExp(
|
const regularExpression = new RegExp(
|
||||||
@ -22,8 +26,8 @@ export const checkBlock = (
|
|||||||
let doubleCharactersCheckResult = null;
|
let doubleCharactersCheckResult = null;
|
||||||
let tripleCharactersCheckResult = null;
|
let tripleCharactersCheckResult = null;
|
||||||
if (characters.length === 1) {
|
if (characters.length === 1) {
|
||||||
doubleCharactersCheckResult = checkBlock(editor, characters.repeat(2));
|
doubleCharactersCheckResult = checkBlock(editor, characters.repeat(2), minimal);
|
||||||
tripleCharactersCheckResult = checkBlock(editor, characters.repeat(3));
|
tripleCharactersCheckResult = checkBlock(editor, characters.repeat(3), minimal);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -43,11 +47,15 @@ export const checkBlock = (
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles a block of text to be formatted.
|
||||||
|
*/
|
||||||
export const toggleBlock = (editor: EditorView, characters: string) => {
|
export const toggleBlock = (editor: EditorView, characters: string) => {
|
||||||
const { state } = editor;
|
const { state } = editor;
|
||||||
const { from, to } = getExpandedSelection(state, characters);
|
const { from, to } = getExpandedSelection(state, characters);
|
||||||
const text = state.sliceDoc(from, to);
|
const text = state.sliceDoc(from, to);
|
||||||
const textMatch = checkBlock(editor, characters);
|
const textMatch = checkBlock(editor, characters);
|
||||||
|
console.log(from, to, text, textMatch);
|
||||||
|
|
||||||
editor.dispatch(
|
editor.dispatch(
|
||||||
state.changeByRange(() =>
|
state.changeByRange(() =>
|
||||||
@ -78,29 +86,36 @@ export const toggleBlock = (editor: EditorView, characters: string) => {
|
|||||||
editor.focus();
|
editor.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to expand the cursor selection to the nearest logical block of text needs to be formatted.
|
||||||
|
*/
|
||||||
export const getExpandedSelection = (
|
export const getExpandedSelection = (
|
||||||
state: EditorState,
|
state: EditorState,
|
||||||
characters: string,
|
characters: string,
|
||||||
|
minimal = false,
|
||||||
): { from: number; to: number } => {
|
): { from: number; to: number } => {
|
||||||
let { from, to } = state.selection.main;
|
let { from, to } = state.selection.main;
|
||||||
|
|
||||||
let fromPosition = from;
|
let fromPosition = from;
|
||||||
while (fromPosition > 0) {
|
while (fromPosition >= 0) {
|
||||||
const newText = state.sliceDoc(fromPosition, to);
|
const newText = state.sliceDoc(fromPosition, to);
|
||||||
if (
|
|
||||||
newText.startsWith('\n') ||
|
if (newText.startsWith('\n') || newText.startsWith('\t')) {
|
||||||
newText.startsWith('\t') ||
|
|
||||||
newText.startsWith(' ')
|
|
||||||
) {
|
|
||||||
fromPosition++;
|
fromPosition++;
|
||||||
break;
|
break;
|
||||||
}
|
} else if (minimal && newText.startsWith(' ')) {
|
||||||
if (
|
fromPosition++;
|
||||||
|
break;
|
||||||
|
} else if (newText.startsWith(characters + ' ')) {
|
||||||
|
fromPosition += characters.length + 1;
|
||||||
|
break;
|
||||||
|
} else if (
|
||||||
newText.length > characters.length &&
|
newText.length > characters.length &&
|
||||||
newText.startsWith(characters)
|
newText.startsWith(characters)
|
||||||
) {
|
) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
fromPosition--;
|
fromPosition--;
|
||||||
}
|
}
|
||||||
from = fromPosition;
|
from = fromPosition;
|
||||||
@ -108,15 +123,13 @@ export const getExpandedSelection = (
|
|||||||
let toPosition = to;
|
let toPosition = to;
|
||||||
while (toPosition < state.doc.length) {
|
while (toPosition < state.doc.length) {
|
||||||
const newText = state.sliceDoc(from, toPosition);
|
const newText = state.sliceDoc(from, toPosition);
|
||||||
if (
|
if (newText.endsWith('\n') || newText.endsWith('\t')) {
|
||||||
newText.endsWith('\n') ||
|
|
||||||
newText.endsWith('\t') ||
|
|
||||||
newText.endsWith(' ')
|
|
||||||
) {
|
|
||||||
toPosition--;
|
toPosition--;
|
||||||
break;
|
break;
|
||||||
}
|
} else if (minimal && newText.endsWith(' ')) {
|
||||||
if (
|
toPosition--;
|
||||||
|
break;
|
||||||
|
} else if (
|
||||||
newText.length > characters.length &&
|
newText.length > characters.length &&
|
||||||
newText.endsWith(characters)
|
newText.endsWith(characters)
|
||||||
) {
|
) {
|
||||||
@ -126,5 +139,23 @@ export const getExpandedSelection = (
|
|||||||
}
|
}
|
||||||
to = toPosition;
|
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 };
|
return { from, to };
|
||||||
};
|
};
|
||||||
|
@ -47,6 +47,8 @@ last
|
|||||||
```js
|
```js
|
||||||
const x = "moooo";
|
const x = "moooo";
|
||||||
```
|
```
|
||||||
|
|
||||||
|
This is a longer sentence!
|
||||||
</textarea
|
</textarea
|
||||||
>
|
>
|
||||||
<script type="module">
|
<script type="module">
|
||||||
|
@ -7,7 +7,7 @@ export default defineConfig({
|
|||||||
clean: true,
|
clean: true,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
include: ['src/**/*.ts'],
|
include: ['src/**/*.ts'],
|
||||||
provider: 'c8',
|
provider: 'v8',
|
||||||
},
|
},
|
||||||
dir: 'src',
|
dir: 'src',
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user