Skip to content

Commit

Permalink
исправления
Browse files Browse the repository at this point in the history
  • Loading branch information
rvrhiv committed Dec 1, 2023
1 parent 5bd9031 commit 42c3ea7
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 143 deletions.
22 changes: 2 additions & 20 deletions src/editor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import type { BaseEditorOptions, TextRange, Model } from './types';
import History, { HistoryEntry } from './history';
import { getTextRange, rangeToLocation, setDOMRange, setRange } from './range';
import {
applyAnimojiParams,
cutText,
getText,
insertText,
Expand All @@ -26,7 +25,6 @@ import parseHTML from '../parser/html2';
import toHTML from '../render/html';
import { last } from '../parser/utils';
import { objectMerge } from '../utils/objectMerge';
import type { EmojiParams } from '../parser/types';

const enum DiffActionType {
Insert = 'insert',
Expand Down Expand Up @@ -259,7 +257,6 @@ export default class Editor {
});
}
}

this.paste(fragment, range[0], range[1]);
this.setSelection(range[0] + len);

Expand Down Expand Up @@ -418,28 +415,13 @@ export default class Editor {
}

/**
* Заменяет текст в указанном диапазоне `from:to` на новый
* Заменяет текст в указанном диапазоне `from:to` на новый текст или токены
*/
replaceText(from: number, to: number, text: string): Model {
replaceText(from: number, to: number, text: string | Model): Model {
const result = this.paste(text, from, to);
return result;
}

/**
* Заменяет текст в указанном диапазоне `from:to` на один эмоджи, и записывает ему указанные параметры
* Если передать более одного эмоджи, указанные параметры будут записаны всем переданным эмоджи
*/
replaceOnEmoji(from: number, to: number, emoji: string, params?: EmojiParams): Model {
emoji = this.sanitizeText(emoji);
let nextModel = replaceText(this.model, emoji, from, to, this.options);

// устанавливаем параметры анимоджи в диапазоне куда мы добавили эмоджи
nextModel = applyAnimojiParams(nextModel, from, from + emoji.length, params);

const pos = from + emoji.length;
return this.updateModel(nextModel, DiffActionType.Insert, [pos, pos]);
}

/**
* Вырезает фрагмент по указанному диапазону из модели и возвращает его
* @returns Вырезанный фрагмент модели
Expand Down
84 changes: 4 additions & 80 deletions src/editor/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import parse, { TokenFormat } from '../parser';
import {
insertText as plainInsertText, removeText as plainRemoveText, replaceText as plainReplaceText,
mdInsertText, mdRemoveText, mdReplaceText, mdCutText, mdToText, textToMd,
cutText as plainCutText, setFormat as plainSetFormat, setLink, slice,
cutText as plainCutText, setFormat as plainSetFormat, setLink, slice, updateEmojiData,
} from '../formatted-string';
import type { TokenFormatUpdate, CutText } from '../formatted-string';
import { isCustomLink, tokenForPos, tokenRange } from '../formatted-string/utils';
import { createEmojiUpdatePayload, isCustomLink, tokenForPos } from '../formatted-string/utils';
import type { BaseEditorOptions, TextRange, Model } from './types';
import { getInputText, isCollapsed, startsWith } from './utils';
import type { EmojiParams } from '../parser/types';

const skipInputTypes = new Set<string>([
'insertOrderedList',
Expand Down Expand Up @@ -122,7 +121,8 @@ export function applyFormatFromFragment(model: Model, fragment: Model, offset =
}

if (token.emoji?.length) {
model = setEmojiParamsFromFragment(model, token, offset, len);
const payload = createEmojiUpdatePayload(token.emoji, offset, token.value);
model = updateEmojiData(model, payload);
}

offset += len;
Expand Down Expand Up @@ -282,79 +282,3 @@ export function getInputEventText(evt: InputEvent): string {

return '';
}

/**
* Устанавливает параметры эмоджи из фрагмента
*/
export function setEmojiParamsFromFragment(tokens: Token[], fragment: Token, pos: number, posLen = 0): Token[] {

const [start, end] = tokenRange(tokens, pos, pos + posLen);

if (start.index === -1 || end.index === -1 || end.index < start.index) {
return tokens;
}

const startTokens = tokens.slice(0, start.index);
let midTokens = tokens.slice(start.index, end.index + 1);
const endTokens = tokens.slice(end.index + 1);

const offset = pos - getText(startTokens).length;

let len = 0;
midTokens = midTokens.map((token) => {
token = {
...token,
emoji: token.emoji?.map((emoji) => {
const fragmentEmoji = fragment.emoji?.find((item) => item.from + offset === len + emoji.from);

if (fragmentEmoji) {
return {
...emoji,
params: fragmentEmoji.params,
}
}
return emoji;
}),
};

len += token.value.length;
return token;
});

return [...startTokens, ...midTokens, ...endTokens];
}

/**
* Применяет параметры анимоджи ко всем эмоджи в указаном диапазоне
*/
export function applyAnimojiParams(model: Model, from: number, to: number, params: EmojiParams): Model {
const [start, end] = tokenRange(model, from, to);

if (start.index !== -1 && end.index !== -1) {
for (let tokenIndex = start.index; tokenIndex <= end.index; tokenIndex++) {
// если это токен между start-токеном и end-токеном, то проходимся по всему диапазону
let offsetFrom = 0;
if (tokenIndex === start.index && start.offset !== -1) {
offsetFrom = start.offset;
}

let offsetTo = model[tokenIndex].value.length;
if (tokenIndex === end.index && end.offset !== -1) {
offsetTo = end.offset;
}

model[tokenIndex].emoji = model[tokenIndex].emoji?.map((item) => {
if (item.from >= offsetFrom && item.to <= offsetTo) {
return {
...item,
params,
}
}

return item;
});
}
}

return model;
}
131 changes: 99 additions & 32 deletions src/formatted-string/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import parse, { getText, getLength, normalize, TokenType } from '../parser';
import type { ParserOptions, Token, TokenFormat, TokenLink, TokenText } from '../parser';
import { mdToText, textToMd } from './markdown';
import type { TokenFormatUpdate, TextRange, CutText } from './types';
import type { TokenFormatUpdate, TextRange, CutText, EmojiUpdatePayload } from './types';
import {
tokenForPos, isSolidToken, isCustomLink, isAutoLink, splitToken,
sliceToken, toLink, toText, tokenRange, createToken
sliceToken, toLink, toText, tokenRange, createToken, createEmojiUpdatePayload
} from './utils';
import { objectMerge } from '../utils/objectMerge';
import type { Emoji } from '../parser/types';

export { mdToText, textToMd, tokenForPos }
export type { CutText, TokenFormatUpdate, TextRange }
export { mdToText, textToMd, tokenForPos, createEmojiUpdatePayload }
export type { CutText, TokenFormatUpdate, TextRange, EmojiUpdatePayload }


/**
Expand Down Expand Up @@ -257,6 +258,96 @@ export function mdSetFormat(tokens: Token[], format: TokenFormatUpdate | TokenFo
return parse(textToMd(updated, range), options);
}

/**
* Обновляет данные эмоджи в нужном токене.
* @param tokens массив токенов
* @param payload массив данных эмоджи, которые нужно обновить.
* NB позиция эмоджи указывается относительно всего текста
*/
export function updateEmojiData(tokens: Token[], payload: EmojiUpdatePayload[]): Token[] {
const updatedTokens = tokens.slice();

// Убедимся, что payloads отсортирован
payload = payload.sort((a, b) => a.pos - b.pos);

// Для каждого элемента из payload находим токен и эмоджи в нём
payload.forEach((emojiPayload) => {
const start = tokenForPos(updatedTokens, emojiPayload.pos, 'start');

// убедимся, что нашли нужный токен и позицию в нем
if (start.index === -1 || start.offset === -1) {
return;
}

const token = updatedTokens[start.index];
const emoji = token.emoji?.find((item) => item.from === start.offset);

if (emoji) {
// Если есть hint, убедимся, что эмоджи совпадают
if (emojiPayload.hint) {
const emojiRaw = token.value.slice(emoji.from, emoji.to);

// Если не совпадают, то не меняем данные эмоджи
if (emojiPayload.hint !== emojiRaw) {
return;
}
}

// нашли эмоджи, проверили hint, следовательно меняем/удаляем данные
if (emojiPayload.data) {
emoji.emojiData = emojiPayload.data;
} else if (emoji.emojiData) {
delete emoji.emojiData;
}
}
});

return updatedTokens;
}

/**
* Переносит данные эмоджи из `startToken` (до start.offset) и `endToken` (после end.offset) в `tokens`
* @param tokens токены в которые нужно перенести данные
* @param startToken токен в котором было начало вставки
* @param startOffset в какую позицию была вставка относительно текста `startToken`
* @param endToken токен в котором был конец вставки (может быть равен `startToken`)
* @param endOffset в какую позицию была вставка относительно текста `endToken`
* @param textBound длина нового текста без учета `endToken.value` (после `endOffset`)
*/
function saveEmojiDataForUpdate(tokens: Token[], startToken: Token, startOffset: number, endToken: Token, endOffset: number, textBound: number) {
// собираем эмоджи из startToken до startOffset
const startTokenEmojis: Emoji[] = [];
for (let index = 0; index < startToken.emoji?.length || 0; index++) {
const emoji = startToken.emoji[index];
if (emoji.from < startOffset) {
startTokenEmojis.push(emoji);
} else {
break;
}
}
const startTokenEmojiPayload = createEmojiUpdatePayload(startTokenEmojis, 0, startToken.value);

// собираем эмоджи из endToken после endOffset
const endTokenEmojis: Emoji[] = [];
for (let index = (endToken.emoji?.length || 0) - 1; index >= 0; index--) {
const emoji = endToken.emoji[index];
if (emoji.from >= endOffset) {
endTokenEmojis.push(emoji);
} else {
break;
}
}
// так как updateEmojiData принимает позиции эмоджи относительно всего текста
// необходимо учесть длину текста до endToken
const tokenEndPos = tokenForPos(tokens, textBound, 'start');
const textLengthBeforeEndToken = getText(tokens.slice(0, tokenEndPos.index)).length;
const offset = textLengthBeforeEndToken + tokenEndPos.offset - endOffset;

const endTokenEmojiPayload = createEmojiUpdatePayload(endTokenEmojis, offset, endToken.value);

return updateEmojiData(tokens, [ ...startTokenEmojiPayload, ...endTokenEmojiPayload ]);
}

/**
* Универсальный метод для обновления списка токенов: добавление, удаление и замена
* текста в списке указанных токенов
Expand Down Expand Up @@ -299,35 +390,11 @@ function updateTokens(tokens: Token[], value: string, from: number, to: number,

if (nextTokens.length) {
// Вставляем/заменяем фрагмент
let len = 0;
nextTokens.forEach((token) => {
token.format = startToken.format;

// переносим параметры эмоджи из startToken (до start.offset) и endToken (после end.offset),
// так как после parse() эти параметры теряются
token.emoji = token.emoji?.map((emoji) => {
if (!len && emoji.from < start.offset) {
const startTokenEmoji = startToken.emoji?.find((item) => item.from === emoji.from);

if (startTokenEmoji) {
return {
...emoji,
params: startTokenEmoji?.params,
}
}
}
nextTokens.forEach((t) => t.format = startToken.format);

if (len + emoji.from >= textBound) {
const endTokenEmoji = endToken.emoji?.find((item) => textBound + item.from - end.offset === len + emoji.from);
return {
...emoji,
params: endTokenEmoji?.params,
}
}
return emoji;
});
len += token.value.length;
});
// переносим параметры эмоджи из startToken (до start.offset) и endToken (после end.offset),
// так как после parse() эти параметры теряются
nextTokens = saveEmojiDataForUpdate(nextTokens, startToken, start.offset, endToken, end.offset, textBound);

// Применяем форматирование из концевых токенов, но только если можем
// сделать это безопасно: применяем только для текста
Expand Down
13 changes: 13 additions & 0 deletions src/formatted-string/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Token, TokenFormat } from '../parser';
import type { EmojiData } from '../parser/types';

/**
* Объект для обновления формата формата
Expand All @@ -19,3 +20,15 @@ export interface CutText {
/** Модифицированная строка без вырезанного текста */
tokens: Token[];
}

export interface EmojiUpdatePayload {
/** Позиция эмоджи относительно всей строки */
pos: number;
/** Данные, которые нужно добавить. Если `null` — удалить данные */
data: EmojiData | null;
/**
* Эмоджи-подсказка. Если указано, то сначала проверим, что эмоджи в модели
* совпадает с переданным. Если не совпадает — ничего не меняем
*/
hint?: string;
}
17 changes: 17 additions & 0 deletions src/formatted-string/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TokenType, TokenFormat } from '../parser';
import type { Token, Emoji, TokenLink, TokenText } from '../parser';
import { objectMerge } from '../utils/objectMerge';
import type { EmojiUpdatePayload } from './types';

export interface TokenForPos {
/** Индекс найденного токена (будет -1, если такой токен не найден) */
Expand Down Expand Up @@ -246,3 +247,19 @@ export function createToken(text: string, format: TokenFormat = 0, sticky = fals
export function isSticky(token: Token): boolean {
return 'sticky' in token && token.sticky;
}

/**
* Создает EmojiUpdatePayload из массива emoji.
* @param text текст токена в котором находятся эмоджи
*/
export function createEmojiUpdatePayload(emojis: Emoji[], offset = 0, text?: string): EmojiUpdatePayload[] {
const sortedEmojis = emojis.slice().sort((a, b) => a.from - b.from);

return sortedEmojis.map((emoji) => {
const pos = offset + emoji.from;
const data = emoji.emojiData || null;
const hint = text && text.slice(emoji.from, emoji.to);

return { pos, data, hint };
});
}
6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ export type { EditorEvent } from './editor';
export { rangeToLocation, locationToRange } from './editor/range';
export { default as render } from './render';
export type { RenderOptions } from './render';
export { setFormat, setLink, textToMd, mdToText, slice, tokenForPos } from './formatted-string';
export type { TextRange } from './formatted-string';
export { setFormat, setLink, textToMd, mdToText, slice, tokenForPos, updateEmojiData, createEmojiUpdatePayload } from './formatted-string';
export type { TextRange, EmojiUpdatePayload } from './formatted-string';
export { default as split } from './formatted-string/split';
export { createTree, type Tree } from './parser/tree';

export { TokenType, TokenFormat } from './parser/types';
export type { Token, TokenCommand, TokenLink, TokenHashTag, TokenMarkdown, TokenMention, TokenText, TokenUserSticker } from './parser/types';
export type { Token, TokenCommand, TokenLink, TokenHashTag, TokenMarkdown, TokenMention, TokenText, TokenUserSticker, Emoji } from './parser/types';
Loading

0 comments on commit 42c3ea7

Please sign in to comment.