@ihazz/bitrix24 1.1.4 → 1.1.6

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.
@@ -3,6 +3,7 @@ import type { KeyboardButton, B24Keyboard } from './types.js';
3
3
  // ─── Placeholder helpers ──────────────────────────────────────────────────────
4
4
 
5
5
  const PH_ESC = '\x00ESC'; // escape sequences
6
+ const PH_BBCODE = '\x00BB'; // existing BBCode blocks
6
7
  const PH_FCODE = '\x00FC'; // fenced code blocks
7
8
  const PH_ICODE = '\x00IC'; // inline code
8
9
  const PH_HR = '\x00HR'; // horizontal rules
@@ -25,14 +26,21 @@ export function markdownToBbCode(md: string): string {
25
26
 
26
27
  // ── Phase 1: Protect literals ─────────────────────────────────────────────
27
28
 
28
- // 1a. Escape sequences: \* \# \_ etc. → placeholders
29
+ // 1a. Existing [CODE]...[/CODE] blocks should pass through untouched.
30
+ const bbCodeCodeBlocks: string[] = [];
31
+ text = text.replace(/\[code(?:=[^\]]+)?\][\s\S]*?\[\/code\]/gi, (block: string) => {
32
+ bbCodeCodeBlocks.push(block);
33
+ return `${PH_BBCODE}${bbCodeCodeBlocks.length - 1}\x00`;
34
+ });
35
+
36
+ // 1b. Escape sequences: \* \# \_ etc. → placeholders
29
37
  const escapes: string[] = [];
30
38
  text = text.replace(/\\([\\`*_{}[\]()#+\-.!~|>])/g, (_match, ch: string) => {
31
39
  escapes.push(ch);
32
40
  return `${PH_ESC}${escapes.length - 1}\x00`;
33
41
  });
34
42
 
35
- // 1b. Fenced code blocks (backticks and tildes)
43
+ // 1c. Fenced code blocks (backticks and tildes)
36
44
  const fencedBlocks: string[] = [];
37
45
  text = text.replace(/^(`{3,})[^\n`]*\n([\s\S]*?)^\1[ \t]*$/gm, (_match, _fence, code: string) => {
38
46
  fencedBlocks.push(code.replace(/\n$/, ''));
@@ -43,7 +51,7 @@ export function markdownToBbCode(md: string): string {
43
51
  return `${PH_FCODE}${fencedBlocks.length - 1}\x00`;
44
52
  });
45
53
 
46
- // 1c. Inline code (backticks) — single and double backtick
54
+ // 1d. Inline code (backticks) — single and double backtick
47
55
  const inlineCodes: string[] = [];
48
56
  text = text.replace(/``([^`]+)``/g, (_match, code: string) => {
49
57
  inlineCodes.push(code);
@@ -182,10 +190,16 @@ export function markdownToBbCode(md: string): string {
182
190
  // 4b. Fenced/indented code blocks → [CODE]...[/CODE]
183
191
  text = text.replace(new RegExp(`${PH_FCODE.replace(/\x00/g, '\\x00')}(\\d+)\\x00`, 'g'), (_m, idx: string) => {
184
192
  const value = fencedBlocks[Number(idx)];
185
- return value != null ? `[CODE]${value}[/CODE]` : _m;
193
+ return value != null ? `[CODE]${preserveCodeBlockIndentation(value)}[/CODE]` : _m;
186
194
  });
187
195
 
188
- // 4c. Escape sequencesliteral characters
196
+ // 4c. Existing [CODE]...[/CODE] blocks original source, untouched
197
+ text = text.replace(new RegExp(`${PH_BBCODE.replace(/\x00/g, '\\x00')}(\\d+)\\x00`, 'g'), (_m, idx: string) => {
198
+ const value = bbCodeCodeBlocks[Number(idx)];
199
+ return value != null ? restoreBbCodeCodeBlock(value) : _m;
200
+ });
201
+
202
+ // 4d. Escape sequences → literal characters
189
203
  text = text.replace(new RegExp(`${PH_ESC.replace(/\x00/g, '\\x00')}(\\d+)\\x00`, 'g'), (_m, idx: string) => {
190
204
  const value = escapes[Number(idx)];
191
205
  return value != null ? value : _m;
@@ -196,13 +210,137 @@ export function markdownToBbCode(md: string): string {
196
210
 
197
211
  function isInlineAccentToken(value: string): boolean {
198
212
  const trimmed = value.trim();
199
- if (!trimmed || /\s/.test(trimmed)) return false;
213
+ if (!trimmed || /[\r\n\t]/.test(trimmed) || trimmed.length > 64) {
214
+ return false;
215
+ }
200
216
 
201
- if (/^\/[a-z0-9][a-z0-9._:-]*$/i.test(trimmed)) {
217
+ if (isWrappedInlineAccentToken(trimmed)) {
202
218
  return true;
203
219
  }
204
220
 
205
- return /^[A-Z][A-Z0-9+._/-]{1,15}$/.test(trimmed);
221
+ if (looksLikeInlineCodeSnippet(trimmed)) {
222
+ return false;
223
+ }
224
+
225
+ return isInlineAccentCandidate(trimmed);
226
+ }
227
+
228
+ function looksLikeInlineCodeSnippet(value: string): boolean {
229
+ if (/[`"'{};]/.test(value)) {
230
+ return true;
231
+ }
232
+
233
+ if (/(===|!==|==|!=|<=|>=|=>|\+\+|--|\|\||&&|\?\?)/.test(value)) {
234
+ return true;
235
+ }
236
+
237
+ return /\b(?:const|let|var|function|class|return|await|async|import|export|from|yield|switch|case)\b/.test(value);
238
+ }
239
+
240
+ function isWrappedInlineAccentToken(value: string): boolean {
241
+ const match = value.match(/^([("'`])(.+)([)"'`])$/);
242
+ if (!match) {
243
+ return false;
244
+ }
245
+
246
+ const open = match[1];
247
+ const inner = match[2].trim();
248
+ const close = match[3];
249
+ if (!inner || inner.length > 48) {
250
+ return false;
251
+ }
252
+
253
+ if ((open === '(' && close !== ')')
254
+ || (open === '[' && close !== ']')
255
+ || (open === '{' && close !== '}')
256
+ || ((open === '"' || open === "'") && close !== open)
257
+ || (open === '`' && close !== '`')) {
258
+ return false;
259
+ }
260
+
261
+ if (looksLikeInlineCodeSnippet(inner)) {
262
+ return false;
263
+ }
264
+
265
+ return isInlineAccentCandidate(inner);
266
+ }
267
+
268
+ function isLowercaseNaturalLanguageWord(value: string): boolean {
269
+ if (!value) {
270
+ return false;
271
+ }
272
+
273
+ return /^[\p{Ll}\p{N}-]+$/u.test(value);
274
+ }
275
+
276
+ function isInlineAccentWord(value: string): boolean {
277
+ if (!value) {
278
+ return false;
279
+ }
280
+
281
+ if (/^\/[a-z0-9][a-z0-9._:-]*$/i.test(value)) {
282
+ return true;
283
+ }
284
+
285
+ if (/^[vV]?\d+(?:\.\d+)*$/.test(value)) {
286
+ return true;
287
+ }
288
+
289
+ if (/^(?:[\p{L}\p{N}_-]+)(?:\.[\p{L}\p{N}_*:-]+)+(?:\([^"'`;{}]*\))?$/u.test(value)) {
290
+ return true;
291
+ }
292
+
293
+ if (/^[\p{L}\p{N}_][\p{L}\p{N}._/:=+*()[\],-]{0,47}$/u.test(value)) {
294
+ return true;
295
+ }
296
+
297
+ return /^(?=.*\p{Lu}.*\p{Lu})(?=.*\p{Ll})[\p{L}\p{N}+._/-]{2,24}$/u.test(value);
298
+ }
299
+
300
+ function isInlineAccentCandidate(value: string): boolean {
301
+ if (/^\/[a-z0-9][a-z0-9._:-]*$/i.test(value)) {
302
+ return true;
303
+ }
304
+
305
+ const words = value.split(/\s+/).filter(Boolean);
306
+ if (words.length === 0 || words.length > 6) {
307
+ return false;
308
+ }
309
+
310
+ if (words.length > 1 && words.every((word) => isLowercaseNaturalLanguageWord(stripInlineAccentPunctuation(word)))) {
311
+ return false;
312
+ }
313
+
314
+ return words.every((word) => isInlineAccentWord(stripInlineAccentPunctuation(word)));
315
+ }
316
+
317
+ function stripInlineAccentPunctuation(value: string): string {
318
+ return value.replace(/^[()[\]{}.,:;!?-]+|[()[\]{}.,:;!?-]+$/g, '');
319
+ }
320
+
321
+ function preserveCodeBlockIndentation(value: string): string {
322
+ if (!value.includes('\n')) {
323
+ return value;
324
+ }
325
+
326
+ return value
327
+ .split('\n')
328
+ .map((line) => {
329
+ const indentMatch = line.match(/^[ \t]+/);
330
+ if (!indentMatch) {
331
+ return line;
332
+ }
333
+
334
+ const visibleIndent = indentMatch[0].replace(/\t/g, ' ');
335
+ return `${'\u00A0'.repeat(visibleIndent.length)}${line.slice(indentMatch[0].length)}`;
336
+ })
337
+ .join('\n');
338
+ }
339
+
340
+ function restoreBbCodeCodeBlock(block: string): string {
341
+ return block.replace(/^(\[code(?:=[^\]]+)?\])([\s\S]*?)(\[\/code\])$/i, (_match, openTag: string, body: string, closeTag: string) => {
342
+ return `${openTag}${preserveCodeBlockIndentation(body)}${closeTag}`;
343
+ });
206
344
  }
207
345
 
208
346
  /**
@@ -109,7 +109,7 @@ export class SendService {
109
109
  ctx.messageId,
110
110
  ctx.commandDialogId ?? ctx.dialogId,
111
111
  chunks[0],
112
- chunks.length === 1 && options?.keyboard
112
+ options?.keyboard
113
113
  ? { keyboard: options.keyboard }
114
114
  : undefined,
115
115
  );
@@ -119,18 +119,13 @@ export class SendService {
119
119
  }
120
120
 
121
121
  for (let i = 1; i < chunks.length; i++) {
122
- const isLast = i === chunks.length - 1;
123
- const msgOptions = isLast && options?.keyboard
124
- ? { keyboard: options.keyboard }
125
- : undefined;
126
-
127
122
  try {
128
123
  lastMessageId = await this.api.sendMessage(
129
124
  ctx.webhookUrl,
130
125
  ctx.bot,
131
126
  ctx.dialogId,
132
127
  chunks[i],
133
- msgOptions,
128
+ undefined,
134
129
  );
135
130
  } catch (error) {
136
131
  this.logger.error('Failed to send command follow-up message', { error: serializeError(error) });