@imessaging/core 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imessaging/core",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Core messaging transport contracts and Telegram peer storage",
5
5
  "keywords": [
6
6
  "messaging",
package/src/index.ts CHANGED
@@ -18,6 +18,7 @@ export {
18
18
  canMarkRead,
19
19
  canReact,
20
20
  canReceive,
21
+ canReceiveEdits,
21
22
  canShowTyping,
22
23
  hasCommandMenu,
23
24
  isTelegramRecipient,
@@ -36,6 +37,7 @@ export type {
36
37
  export type {
37
38
  EditableMessage,
38
39
  EditableMessageTransport,
40
+ EditedMessageTransport,
39
41
  EditResult,
40
42
  EmailRecipient,
41
43
  HeadhunterRecipient,
package/src/markup.ts CHANGED
@@ -13,7 +13,20 @@
13
13
  /** Как написан исходный текст сообщения. */
14
14
  export type TextFormat = "plain" | "markdown" | "html";
15
15
 
16
- export type TextEntityType = "bold" | "italic" | "underline" | "strike" | "code" | "pre" | "link";
16
+ /**
17
+ * Общая модель для всех каналов: Telegram и MAX отдают выделения со смещениями, WhatsApp — звёздочки в тексте,
18
+ * почта — HTML; транспорт переводит своё в эти типы, а для чего пары нет (упоминания, хэштеги) — отбрасывает.
19
+ */
20
+ export type TextEntityType =
21
+ | "bold"
22
+ | "italic"
23
+ | "underline"
24
+ | "strike"
25
+ | "spoiler"
26
+ | "code"
27
+ | "pre"
28
+ | "link"
29
+ | "quote";
17
30
 
18
31
  export type TextEntity = {
19
32
  type: TextEntityType;
@@ -96,6 +109,14 @@ function parseInline(src: string, out: Builder): void {
96
109
  }
97
110
 
98
111
  const pair = src.slice(i, i + 2);
112
+ if (pair === "||" && !isSpace(src[i + 2])) {
113
+ const end = findCloser(src, i + 2, "||", (at) => at > i + 2 && !isSpace(src[at - 1]));
114
+ if (end !== -1) {
115
+ wrap(out, "spoiler", () => parseInline(src.slice(i + 2, end), out));
116
+ i = end + 2;
117
+ continue;
118
+ }
119
+ }
99
120
  if (pair === "**" || pair === "__" || pair === "~~") {
100
121
  // `__` — жирный только на границе слова: `snake__case` остаётся как есть.
101
122
  const bounded = pair === "__";
@@ -166,8 +187,27 @@ export function parseMarkdown(source: string): FormattedText {
166
187
 
167
188
  function parseLines(block: string, out: Builder): void {
168
189
  const lines = block.split("\n");
190
+ // Подряд идущие строки `> …` — одна цитата, как её показывает любой мессенджер.
191
+ let quote: { offset: number; at: number } | undefined;
192
+ const closeQuote = (end: number) => {
193
+ if (quote && end > quote.offset) {
194
+ out.entities.splice(quote.at, 0, {
195
+ type: "quote",
196
+ offset: quote.offset,
197
+ length: end - quote.offset,
198
+ });
199
+ }
200
+ quote = undefined;
201
+ };
169
202
  lines.forEach((line, n) => {
203
+ const quoted = /^>\s?(.*)$/.exec(line);
204
+ if (!quoted) closeQuote(out.text.length);
170
205
  if (n > 0) out.text += "\n";
206
+ if (quoted) {
207
+ quote ??= { offset: out.text.length, at: out.entities.length };
208
+ parseInline(quoted[1]!, out);
209
+ return;
210
+ }
171
211
  const heading = /^#{1,6}\s+(.*?)\s*#*$/.exec(line);
172
212
  if (heading) {
173
213
  wrap(out, "bold", () => parseInline(heading[1]!, out));
@@ -175,6 +215,7 @@ function parseLines(block: string, out: Builder): void {
175
215
  }
176
216
  parseInline(line.replace(/^(\s*)[*•●·]\s+/, "$1- "), out);
177
217
  });
218
+ closeQuote(out.text.length);
178
219
  }
179
220
 
180
221
  const HTML_TAGS: Record<string, TextEntityType> = {
@@ -190,6 +231,8 @@ const HTML_TAGS: Record<string, TextEntityType> = {
190
231
  code: "code",
191
232
  pre: "pre",
192
233
  a: "link",
234
+ "tg-spoiler": "spoiler",
235
+ blockquote: "quote",
193
236
  };
194
237
 
195
238
  function decodeHtml(s: string): string {
@@ -310,6 +353,10 @@ function openTag(e: TextEntity): string {
310
353
  return e.language ? `<pre><code class="language-${escapeAttribute(e.language)}">` : "<pre>";
311
354
  case "link":
312
355
  return `<a href="${escapeAttribute(e.url ?? "")}">`;
356
+ case "spoiler":
357
+ return "<tg-spoiler>";
358
+ case "quote":
359
+ return "<blockquote>";
313
360
  }
314
361
  }
315
362
 
@@ -319,9 +366,11 @@ function closeTag(e: TextEntity): string {
319
366
  italic: "i",
320
367
  underline: "u",
321
368
  strike: "s",
369
+ spoiler: "tg-spoiler",
322
370
  code: "code",
323
371
  pre: "pre",
324
372
  link: "a",
373
+ quote: "blockquote",
325
374
  }[e.type];
326
375
  return e.type === "pre" && e.language ? "</code></pre>" : `</${name}>`;
327
376
  }
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { TextFormat } from "./markup";
1
+ import type { TextEntity, TextFormat } from "./markup";
2
2
 
3
3
  export type UsernameRecipient = {
4
4
  type: "username";
@@ -300,6 +300,11 @@ export type IncomingMessage = {
300
300
  editedAt?: Date;
301
301
  /** В сообщении упомянут аккаунт или это ответ на его сообщение (так Telegram определяет упоминание); нет — канал не сообщает. */
302
302
  mentionsMe?: boolean;
303
+ /**
304
+ * Форматирование текста в той же модели, что исходящая разметка, смещения в UTF-16;
305
+ * undefined = канал не сообщает форматирование, [] = сообщил, форматирования нет.
306
+ */
307
+ entities?: TextEntity[];
303
308
  attachments: IncomingAttachment[];
304
309
  /**
305
310
  * Адреса из сообщения, в порядке появления.
@@ -348,6 +353,20 @@ export function canReceive(transport: MessageTransport): transport is IncomingMe
348
353
  return typeof (transport as IncomingMessageTransport).onMessage === "function";
349
354
  }
350
355
 
356
+ /**
357
+ * Канал, умеющий сообщать о правках сообщений.
358
+ *
359
+ * Отдельная подписка, потому что подписчики onMessage не ждут правок и сочтут повторный
360
+ * messageId за новое сообщение.
361
+ */
362
+ export interface EditedMessageTransport extends MessageTransport {
363
+ onEdit(handler: (message: IncomingMessage) => void | Promise<void>): () => void;
364
+ }
365
+
366
+ export function canReceiveEdits(transport: MessageTransport): transport is EditedMessageTransport {
367
+ return typeof (transport as EditedMessageTransport).onEdit === "function";
368
+ }
369
+
351
370
  /** Пункт меню команд канала: `/menu` и подпись рядом с ним. */
352
371
  export type ChannelCommand = {
353
372
  /** Без ведущей косой черты. */