2023-04-28 02:51:04 +02:00
|
|
|
import { EditorSelection, EditorState } from '@codemirror/state';
|
|
|
|
|
import { EditorView } from '@codemirror/view';
|
|
|
|
|
import escapeStringRegexp from 'escape-string-regexp';
|
|
|
|
|
|
2024-06-07 12:37:45 +02:00
|
|
|
/**
|
|
|
|
|
* Checks whether the selection matches a formatted block of text.
|
|
|
|
|
*/
|
2023-04-28 02:51:04 +02:00
|
|
|
export const checkBlock = (
|
|
|
|
|
editor: EditorView,
|
|
|
|
|
characters: string,
|
2024-06-07 12:37:45 +02:00
|
|
|
minimal = false,
|
2023-04-28 02:51:04 +02:00
|
|
|
): RegExpExecArray | null => {
|
|
|
|
|
// Checks whether the selection matches a block of formatted text.
|
|
|
|
|
|
|
|
|
|
const { state } = editor;
|
2024-06-07 12:37:45 +02:00
|
|
|
const { from, to } = getExpandedSelection(state, characters, minimal);
|
2023-04-28 02:51:04 +02:00
|
|
|
const text = state.sliceDoc(from, to);
|
|
|
|
|
const escapedCharacters = escapeStringRegexp(characters);
|
|
|
|
|
const regularExpression = new RegExp(
|
|
|
|
|
`^${escapedCharacters}(.*)${escapedCharacters}$`,
|
|
|
|
|
'gs',
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const checkResult = regularExpression.exec(text);
|
|
|
|
|
|
|
|
|
|
let doubleCharactersCheckResult = null;
|
|
|
|
|
let tripleCharactersCheckResult = null;
|
|
|
|
|
if (characters.length === 1) {
|
2024-06-07 12:37:45 +02:00
|
|
|
doubleCharactersCheckResult = checkBlock(editor, characters.repeat(2), minimal);
|
|
|
|
|
tripleCharactersCheckResult = checkBlock(editor, characters.repeat(3), minimal);
|
2023-04-28 02:51:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
(checkResult &&
|
|
|
|
|
doubleCharactersCheckResult &&
|
|
|
|
|
tripleCharactersCheckResult) ||
|
|
|
|
|
(checkResult &&
|
|
|
|
|
!doubleCharactersCheckResult &&
|
|
|
|
|
tripleCharactersCheckResult) ||
|
|
|
|
|
(checkResult &&
|
|
|
|
|
!doubleCharactersCheckResult &&
|
|
|
|
|
!tripleCharactersCheckResult)
|
|
|
|
|
) {
|
|
|
|
|
return checkResult;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
};
|
|
|
|
|
|
2024-06-07 12:37:45 +02:00
|
|
|
/**
|
|
|
|
|
* Toggles a block of text to be formatted.
|
|
|
|
|
*/
|
2023-04-28 02:51:04 +02:00
|
|
|
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);
|
2024-06-07 12:37:45 +02:00
|
|
|
console.log(from, to, text, textMatch);
|
2023-04-28 02:51:04 +02:00
|
|
|
|
|
|
|
|
editor.dispatch(
|
|
|
|
|
state.changeByRange(() =>
|
|
|
|
|
textMatch
|
|
|
|
|
? {
|
|
|
|
|
changes: [{ from, insert: textMatch[1], to }],
|
|
|
|
|
range: EditorSelection.range(
|
|
|
|
|
from,
|
|
|
|
|
to - (characters.length + characters.length),
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
: {
|
|
|
|
|
changes: [
|
|
|
|
|
{
|
|
|
|
|
from,
|
|
|
|
|
insert: `${characters}${text}${characters}`,
|
|
|
|
|
to,
|
|
|
|
|
},
|
|
|
|
|
],
|
|
|
|
|
range: EditorSelection.range(
|
|
|
|
|
from,
|
|
|
|
|
to + (characters.length + characters.length),
|
|
|
|
|
),
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
editor.focus();
|
|
|
|
|
};
|
|
|
|
|
|
2024-06-07 12:37:45 +02:00
|
|
|
/**
|
|
|
|
|
* Attempts to expand the cursor selection to the nearest logical block of text needs to be formatted.
|
|
|
|
|
*/
|
2023-04-28 02:51:04 +02:00
|
|
|
export const getExpandedSelection = (
|
|
|
|
|
state: EditorState,
|
|
|
|
|
characters: string,
|
2024-06-07 12:37:45 +02:00
|
|
|
minimal = false,
|
2023-04-28 02:51:04 +02:00
|
|
|
): { from: number; to: number } => {
|
|
|
|
|
let { from, to } = state.selection.main;
|
|
|
|
|
|
|
|
|
|
let fromPosition = from;
|
2024-06-07 12:37:45 +02:00
|
|
|
while (fromPosition >= 0) {
|
2023-04-28 02:51:04 +02:00
|
|
|
const newText = state.sliceDoc(fromPosition, to);
|
2024-06-07 12:37:45 +02:00
|
|
|
|
|
|
|
|
if (newText.startsWith('\n') || newText.startsWith('\t')) {
|
2023-04-28 02:51:04 +02:00
|
|
|
fromPosition++;
|
|
|
|
|
break;
|
2024-06-07 12:37:45 +02:00
|
|
|
} else if (minimal && newText.startsWith(' ')) {
|
|
|
|
|
fromPosition++;
|
|
|
|
|
break;
|
|
|
|
|
} else if (newText.startsWith(characters + ' ')) {
|
|
|
|
|
fromPosition += characters.length + 1;
|
|
|
|
|
break;
|
|
|
|
|
} else if (
|
2023-04-28 02:51:04 +02:00
|
|
|
newText.length > characters.length &&
|
|
|
|
|
newText.startsWith(characters)
|
|
|
|
|
) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
2024-06-07 12:37:45 +02:00
|
|
|
|
2023-04-28 02:51:04 +02:00
|
|
|
fromPosition--;
|
|
|
|
|
}
|
|
|
|
|
from = fromPosition;
|
|
|
|
|
|
|
|
|
|
let toPosition = to;
|
|
|
|
|
while (toPosition < state.doc.length) {
|
|
|
|
|
const newText = state.sliceDoc(from, toPosition);
|
2024-06-07 12:37:45 +02:00
|
|
|
if (newText.endsWith('\n') || newText.endsWith('\t')) {
|
2023-04-28 02:51:04 +02:00
|
|
|
toPosition--;
|
|
|
|
|
break;
|
2024-06-07 12:37:45 +02:00
|
|
|
} else if (minimal && newText.endsWith(' ')) {
|
|
|
|
|
toPosition--;
|
|
|
|
|
break;
|
|
|
|
|
} else if (
|
2023-04-28 02:51:04 +02:00
|
|
|
newText.length > characters.length &&
|
|
|
|
|
newText.endsWith(characters)
|
|
|
|
|
) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
toPosition++;
|
|
|
|
|
}
|
|
|
|
|
to = toPosition;
|
|
|
|
|
|
2024-06-07 12:37:45 +02:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-28 02:51:04 +02:00
|
|
|
return { from, to };
|
|
|
|
|
};
|