feat(web): виды сообщений, автокомплиты композера и ссылки на комнаты
- лента оформляет виды сообщений от сервера: action (/me) — имя акцентом и курсив, whisper — приглушённый блок с бейджем, scream — крупнее и жирнее, private (/ls) — рамка, замок и бейдж «лично», system — центрированная строка без аватара и панели действий; реакции, ответы, правка и удаление работают как раньше, группировка не сливает нестандартные виды с обычными; - автокомплиты в поле ввода с навигацией ↑/↓, Enter/Tab, Escape и мышью: @ — участники сервера (в DM собеседник), # — видимые текстовые комнаты, : — словарь на 113 эмодзи, / — команды с описанием; подсказки не появляются внутри слова и в блоках кода, вставка восстанавливает каретку и не расходится со слоем подсветки; - `<#id>` рендерится как #имя комнаты из снапшота (иначе #id); - 287 тестов Vitest (было 262), check/lint/test/build зелёные.
This commit is contained in:
+19
-11
@@ -16,7 +16,8 @@
|
||||
* поэтому HTML из сообщения остаётся текстом. Поддерживается: `**жирный**`,
|
||||
* `__жирный__`, `*курсив*`, `_курсив_`, `***жирный курсив***`, `` `код` ``,
|
||||
* ```блок кода```, `~~зачёркнутый~~`, `==выделение==`, `||спойлер||`,
|
||||
* `> цитата`, ссылки `http(s)://…`, упоминания `<@id>` и `#канал`.
|
||||
* `> цитата`, ссылки `http(s)://…`, упоминания `<@id>`, ссылки на комнаты
|
||||
* `<#id>` и `#канал`.
|
||||
*
|
||||
* Незакрытые маркеры остаются обычным текстом: шаблон требует закрывающую
|
||||
* пару, поэтому `==фыв` и `**фыв` рендерятся как есть.
|
||||
@@ -32,6 +33,7 @@ export type InlineKind =
|
||||
| 'spoiler'
|
||||
| 'mark'
|
||||
| 'mention'
|
||||
| 'channel-link'
|
||||
| 'link'
|
||||
| 'channel';
|
||||
|
||||
@@ -53,12 +55,15 @@ export interface InlineToken {
|
||||
/**
|
||||
* Порядок альтернатив важен: сначала более длинные маркеры (`***` раньше `**`
|
||||
* и `*`), ссылки — раньше `#канала`, чтобы `https://x/#якорь` целиком остался
|
||||
* ссылкой. Одиночная `*` не считается маркером внутри ряда звёздочек
|
||||
* (`**фыв` не превращается в курсив со «звёздочкой» в тексте), а `_` — внутри
|
||||
* слова (`foo_bar_baz`).
|
||||
* ссылкой, а `<#id>` — раньше `#канала`, иначе угловые скобки остались бы
|
||||
* текстом. Идентификатор в `<@id>`/`<#id>` — снежинка; допускаем и другую
|
||||
* запись без пробелов и угловых скобок, чтобы ссылка не разваливалась на
|
||||
* «текст со скобками». Одиночная `*` не считается маркером внутри ряда
|
||||
* звёздочек (`**фыв` не превращается в курсив со «звёздочкой» в тексте), а
|
||||
* `_` — внутри слова (`foo_bar_baz`).
|
||||
*/
|
||||
const INLINE_PATTERN =
|
||||
/(`[^`\n]+`)|(\*\*\*[^*\n]+\*\*\*)|(\*\*[^*\n]+\*\*)|(__[^_\n]+__)|(~~[^~\n]+~~)|(\|\|[^|\n]+\|\|)|((?<!\*)\*(?!\*)[^*\n]+(?<!\*)\*(?!\*))|((?<![\p{L}\p{N}_])_(?!_)[^_\n]+(?<!_)_(?![\p{L}\p{N}_]))|(==[^=\n]+==)|(<@\d+>)|(https?:\/\/[^\s<>()]+)|(#[\p{L}\p{N}_-]+)/gu;
|
||||
/(`[^`\n]+`)|(\*\*\*[^*\n]+\*\*\*)|(\*\*[^*\n]+\*\*)|(__[^_\n]+__)|(~~[^~\n]+~~)|(\|\|[^|\n]+\|\|)|((?<!\*)\*(?!\*)[^*\n]+(?<!\*)\*(?!\*))|((?<![\p{L}\p{N}_])_(?!_)[^_\n]+(?<!_)_(?![\p{L}\p{N}_]))|(==[^=\n]+==)|(<@[^\s<>]+>)|(<#[^\s<>]+>)|(https?:\/\/[^\s<>()]+)|(#[\p{L}\p{N}_-]+)/gu;
|
||||
|
||||
/** Вид токена по номеру группы шаблона (порядок альтернатив выше). */
|
||||
const GROUP_KINDS: readonly InlineKind[] = [
|
||||
@@ -72,6 +77,7 @@ const GROUP_KINDS: readonly InlineKind[] = [
|
||||
'italic',
|
||||
'mark',
|
||||
'mention',
|
||||
'channel-link',
|
||||
'link',
|
||||
'channel',
|
||||
];
|
||||
@@ -85,8 +91,10 @@ const MARKER_LENGTH: Readonly<Record<InlineKind, number>> = {
|
||||
strike: 2,
|
||||
spoiler: 2,
|
||||
mark: 2,
|
||||
// У упоминаний, ссылок и каналов маркеров нет: содержимое — весь токен.
|
||||
// У упоминаний, ссылок на комнаты, ссылок и каналов маркеров нет:
|
||||
// содержимое — весь токен.
|
||||
mention: 0,
|
||||
'channel-link': 0,
|
||||
link: 0,
|
||||
channel: 0,
|
||||
};
|
||||
@@ -107,8 +115,8 @@ function contentBounds(
|
||||
raw: string,
|
||||
start: number,
|
||||
): { contentStart: number; contentEnd: number } {
|
||||
if (kind === 'mention') {
|
||||
// `<@123>`: содержимое — идентификатор без `<@` и `>`.
|
||||
if (kind === 'mention' || kind === 'channel-link') {
|
||||
// `<@123>` и `<#123>`: содержимое — идентификатор без `<@`/`<#` и `>`.
|
||||
return { contentStart: start + 2, contentEnd: start + raw.length - 1 };
|
||||
}
|
||||
const marker = MARKER_LENGTH[kind];
|
||||
@@ -141,11 +149,11 @@ export function parseInline(text: string, offset = 0): InlineToken[] {
|
||||
}
|
||||
|
||||
/**
|
||||
* Есть ли у токена парные маркеры. У упоминаний, ссылок и каналов их нет:
|
||||
* содержимое — весь токен, прятать нечего.
|
||||
* Есть ли у токена парные маркеры. У упоминаний, ссылок на комнаты, ссылок и
|
||||
* каналов их нет: содержимое — весь токен, прятать нечего.
|
||||
*/
|
||||
export function hasMarkers(kind: InlineKind): boolean {
|
||||
return kind !== 'mention' && kind !== 'link' && kind !== 'channel';
|
||||
return kind !== 'mention' && kind !== 'channel-link' && kind !== 'link' && kind !== 'channel';
|
||||
}
|
||||
|
||||
/** Ссылка: обрезаем хвостовую пунктуацию, которая не часть адреса. */
|
||||
|
||||
Reference in New Issue
Block a user