@ihazz/bitrix24 0.2.0 → 0.2.2
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 +1 -1
- package/src/channel.ts +86 -13
- package/src/message-utils.ts +165 -35
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -38,6 +38,28 @@ interface GatewayState {
|
|
|
38
38
|
|
|
39
39
|
let gatewayState: GatewayState | null = null;
|
|
40
40
|
|
|
41
|
+
// ─── i18n helpers ────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
const I18N_MEDIA_DOWNLOAD_FAILED: Record<string, (files: string) => string> = {
|
|
44
|
+
en: (f) => `⚠️ Could not download file(s): ${f}.\n\nFile processing is currently only available for the primary user (webhook owner). This limitation will be removed in a future release.`,
|
|
45
|
+
ru: (f) => `⚠️ Не удалось загрузить файл(ы): ${f}.\n\nОбработка файлов пока доступна только для основного пользователя (автора вебхука). В будущих версиях это ограничение будет снято.`,
|
|
46
|
+
de: (f) => `⚠️ Datei(en) konnten nicht heruntergeladen werden: ${f}.\n\nDateiverarbeitung ist derzeit nur für den Hauptbenutzer (Webhook-Besitzer) verfügbar. Diese Einschränkung wird in einer zukünftigen Version behoben.`,
|
|
47
|
+
es: (f) => `⚠️ No se pudo descargar el/los archivo(s): ${f}.\n\nEl procesamiento de archivos actualmente solo está disponible para el usuario principal (propietario del webhook). Esta limitación se eliminará en una versión futura.`,
|
|
48
|
+
fr: (f) => `⚠️ Impossible de télécharger le(s) fichier(s) : ${f}.\n\nLe traitement des fichiers est actuellement réservé à l'utilisateur principal (propriétaire du webhook). Cette limitation sera levée dans une prochaine version.`,
|
|
49
|
+
pt: (f) => `⚠️ Não foi possível baixar o(s) arquivo(s): ${f}.\n\nO processamento de arquivos está disponível apenas para o usuário principal (dono do webhook). Essa limitação será removida em uma versão futura.`,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function mediaDownloadFailedMsg(lang: string | undefined, fileNames: string): string {
|
|
53
|
+
const code = (lang ?? 'en').toLowerCase().slice(0, 2);
|
|
54
|
+
const fn = I18N_MEDIA_DOWNLOAD_FAILED[code] ?? I18N_MEDIA_DOWNLOAD_FAILED.en;
|
|
55
|
+
return fn(fileNames);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ─── Default bot avatar (64×64 red circle with white "C") ───────────────────
|
|
59
|
+
|
|
60
|
+
const DEFAULT_AVATAR_BASE64 =
|
|
61
|
+
'iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAABp0lEQVR42u1bsa3DIBBNmVlcZgbKNF7BK6TIHG4yi2fwAjS/8AIs4IZAdC5RAtxh+LyTniJFlq33gLsD7i4XGOxU+7vduiA5OEwOs8PioB2Mw+5g6dfQ/ws9558fWiatHF4OG5FMxUbvUS2Qvjo8aCStADS9/1oj+SdNY1sA/jvPWoiPgiP+y4wYT/PetDZtBXgVjSbk1ddKyB9Yi0QN8u5sa90bs29QkuTvOURjLUOIu9TIi5NmFENxr3lzBvEMIQyLTyBvv9ZAPkGENTs6xIS6khYTInOTnKqIJwoxpgqgayYfIYJOze3tPxHARu0daFdnWiAfIYL5eRdJW85myEeI8GBZ+9LTVfD9miXjK5XWCn1HZcX9wjm9yNb5mwAbpwCVkf+cMX7L+dmmZYXkDwwhAaYOyHtMIQHmTgSYQwIsHAJUTt5jEY3/DQigQwKYTgQwIQH2TgTYQyc/PTjAA5gB8AGIAsgDkAliL4DdIM4DcCKEM0GcCuNeADdDuBvE7TDqA1AhghohVImhThCVoqgVRrU4+gXQMYKeIXSNoW8QnaPoHe7F3lyXWKOZrIuJAAAAAElFTkSuQmCC';
|
|
62
|
+
|
|
41
63
|
// ─── Default command keyboard ────────────────────────────────────────────────
|
|
42
64
|
|
|
43
65
|
/** Default keyboard shown with command responses and welcome messages. */
|
|
@@ -155,9 +177,16 @@ async function ensureBotRegistered(
|
|
|
155
177
|
|
|
156
178
|
if (existing) {
|
|
157
179
|
logger.info(`Bot "${code}" already registered (ID=${existing.ID}), updating EVENT_HANDLER`);
|
|
180
|
+
const updateProps: Record<string, unknown> = {
|
|
181
|
+
NAME: name,
|
|
182
|
+
WORK_POSITION: 'AI Assistant',
|
|
183
|
+
COLOR: 'RED',
|
|
184
|
+
};
|
|
185
|
+
updateProps.PERSONAL_PHOTO = config.botAvatar || DEFAULT_AVATAR_BASE64;
|
|
186
|
+
|
|
158
187
|
await api.updateBot(webhookUrl, existing.ID, {
|
|
159
188
|
EVENT_HANDLER: callbackUrl,
|
|
160
|
-
PROPERTIES:
|
|
189
|
+
PROPERTIES: updateProps,
|
|
161
190
|
});
|
|
162
191
|
return existing.ID;
|
|
163
192
|
}
|
|
@@ -174,7 +203,8 @@ async function ensureBotRegistered(
|
|
|
174
203
|
PROPERTIES: {
|
|
175
204
|
NAME: name,
|
|
176
205
|
WORK_POSITION: 'AI Assistant',
|
|
177
|
-
COLOR: '
|
|
206
|
+
COLOR: 'RED',
|
|
207
|
+
PERSONAL_PHOTO: config.botAvatar || DEFAULT_AVATAR_BASE64,
|
|
178
208
|
},
|
|
179
209
|
});
|
|
180
210
|
logger.info(`Bot "${code}" registered (ID=${botId})`);
|
|
@@ -292,6 +322,7 @@ export const bitrix24Plugin = {
|
|
|
292
322
|
reactions: false,
|
|
293
323
|
threads: false,
|
|
294
324
|
nativeCommands: true,
|
|
325
|
+
inlineButtons: 'all',
|
|
295
326
|
},
|
|
296
327
|
|
|
297
328
|
config: {
|
|
@@ -524,6 +555,21 @@ export const bitrix24Plugin = {
|
|
|
524
555
|
MediaUrls: downloaded.map((m) => m.path),
|
|
525
556
|
MediaTypes: downloaded.map((m) => m.contentType),
|
|
526
557
|
};
|
|
558
|
+
} else {
|
|
559
|
+
// All file downloads failed — notify the user
|
|
560
|
+
const fileNames = msgCtx.media.map((m) => m.name).join(', ');
|
|
561
|
+
logger.warn('All media downloads failed, notifying user', { fileNames });
|
|
562
|
+
const errSendCtx = {
|
|
563
|
+
webhookUrl: config.webhookUrl,
|
|
564
|
+
clientEndpoint: msgCtx.clientEndpoint,
|
|
565
|
+
botToken: msgCtx.botToken,
|
|
566
|
+
dialogId: msgCtx.chatId,
|
|
567
|
+
};
|
|
568
|
+
await sendService.sendText(
|
|
569
|
+
errSendCtx,
|
|
570
|
+
mediaDownloadFailedMsg(msgCtx.language, fileNames),
|
|
571
|
+
);
|
|
572
|
+
return;
|
|
527
573
|
}
|
|
528
574
|
}
|
|
529
575
|
|
|
@@ -558,7 +604,7 @@ export const bitrix24Plugin = {
|
|
|
558
604
|
RawBody: body,
|
|
559
605
|
From: `bitrix24:${msgCtx.chatId}`,
|
|
560
606
|
To: `bitrix24:${msgCtx.chatId}`,
|
|
561
|
-
SessionKey: route.sessionKey
|
|
607
|
+
SessionKey: `${route.sessionKey}:bitrix24:${msgCtx.chatId}`,
|
|
562
608
|
AccountId: route.accountId,
|
|
563
609
|
ChatType: msgCtx.isDm ? 'direct' : 'group',
|
|
564
610
|
ConversationLabel: msgCtx.senderName,
|
|
@@ -695,7 +741,7 @@ export const bitrix24Plugin = {
|
|
|
695
741
|
CommandBody: commandText,
|
|
696
742
|
CommandAuthorized: true,
|
|
697
743
|
CommandSource: 'native',
|
|
698
|
-
CommandTargetSessionKey: route.sessionKey
|
|
744
|
+
CommandTargetSessionKey: `${route.sessionKey}:bitrix24:${dialogId}`,
|
|
699
745
|
From: `bitrix24:${dialogId}`,
|
|
700
746
|
To: `slash:${senderId}`,
|
|
701
747
|
SessionKey: slashSessionKey,
|
|
@@ -756,23 +802,50 @@ export const bitrix24Plugin = {
|
|
|
756
802
|
},
|
|
757
803
|
|
|
758
804
|
onJoinChat: async (event: B24JoinChatEvent) => {
|
|
805
|
+
const dialogId = event.data.PARAMS.DIALOG_ID;
|
|
806
|
+
const botEntry = Object.values(event.data.BOT)[0];
|
|
759
807
|
logger.info('Bot joined chat', {
|
|
760
|
-
dialogId
|
|
808
|
+
dialogId,
|
|
761
809
|
userId: event.data.PARAMS.USER_ID,
|
|
810
|
+
hasBotEntry: !!botEntry,
|
|
811
|
+
botId: botEntry?.BOT_ID,
|
|
812
|
+
hasEndpoint: !!botEntry?.client_endpoint,
|
|
813
|
+
hasToken: !!botEntry?.access_token,
|
|
762
814
|
});
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
815
|
+
if (!dialogId) return;
|
|
816
|
+
|
|
817
|
+
const welcomeText = `${config.botName ?? 'OpenClaw'} ready. Send me a message or pick a command below.`;
|
|
818
|
+
const welcomeOpts = { KEYBOARD: DEFAULT_COMMAND_KEYBOARD };
|
|
819
|
+
|
|
820
|
+
try {
|
|
821
|
+
// Prefer token-based call; fall back to webhook URL
|
|
822
|
+
if (botEntry?.client_endpoint && botEntry?.access_token) {
|
|
767
823
|
await api.sendMessageWithToken(
|
|
768
824
|
botEntry.client_endpoint,
|
|
769
825
|
botEntry.access_token,
|
|
770
826
|
dialogId,
|
|
771
|
-
|
|
772
|
-
|
|
827
|
+
welcomeText,
|
|
828
|
+
welcomeOpts,
|
|
773
829
|
);
|
|
774
|
-
}
|
|
775
|
-
|
|
830
|
+
} else if (config.webhookUrl) {
|
|
831
|
+
await api.sendMessage(config.webhookUrl, dialogId, welcomeText, welcomeOpts);
|
|
832
|
+
} else {
|
|
833
|
+
logger.warn('No way to send welcome message — no token and no webhookUrl');
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
logger.info('Welcome message sent', { dialogId });
|
|
837
|
+
} catch (err: unknown) {
|
|
838
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
839
|
+
logger.error('Failed to send welcome message', { error: errMsg, dialogId });
|
|
840
|
+
// Retry via webhook if token-based call failed
|
|
841
|
+
if (botEntry?.client_endpoint && config.webhookUrl) {
|
|
842
|
+
try {
|
|
843
|
+
await api.sendMessage(config.webhookUrl, dialogId, welcomeText, welcomeOpts);
|
|
844
|
+
logger.info('Welcome message sent via webhook fallback', { dialogId });
|
|
845
|
+
} catch (err2: unknown) {
|
|
846
|
+
const errMsg2 = err2 instanceof Error ? err2.message : String(err2);
|
|
847
|
+
logger.error('Welcome message webhook fallback also failed', { error: errMsg2, dialogId });
|
|
848
|
+
}
|
|
776
849
|
}
|
|
777
850
|
}
|
|
778
851
|
},
|
package/src/message-utils.ts
CHANGED
|
@@ -1,44 +1,174 @@
|
|
|
1
1
|
import type { KeyboardButton, B24Keyboard } from './types.js';
|
|
2
2
|
|
|
3
|
+
// ─── Placeholder helpers ──────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
const PH_ESC = '\x00ESC'; // escape sequences
|
|
6
|
+
const PH_FCODE = '\x00FC'; // fenced code blocks
|
|
7
|
+
const PH_ICODE = '\x00IC'; // inline code
|
|
8
|
+
const PH_HR = '\x00HR'; // horizontal rules
|
|
9
|
+
|
|
3
10
|
/**
|
|
4
|
-
* Convert Markdown
|
|
11
|
+
* Convert Markdown (CommonMark + GFM subset) to Bitrix24 BB-code chat format.
|
|
5
12
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* - > blockquote → >>blockquote
|
|
13
|
+
* 4-phase pipeline:
|
|
14
|
+
* 1. Protect literals — escape sequences, fenced code, inline code → placeholders
|
|
15
|
+
* 2. Block rules — indented code, setext headings, horizontal rules, ATX headings,
|
|
16
|
+
* blockquotes, unordered lists
|
|
17
|
+
* 3. Inline rules — images, bold+italic, bold, italic, strikethrough, HTML inline
|
|
18
|
+
* formatting, links, autolinks
|
|
19
|
+
* 4. Restore — placeholders → BB-code equivalents
|
|
14
20
|
*/
|
|
15
21
|
export function markdownToBbCode(md: string): string {
|
|
16
|
-
let
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
22
|
+
let text = md;
|
|
23
|
+
|
|
24
|
+
// ── Phase 1: Protect literals ─────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
// 1a. Escape sequences: \* \# \_ etc. → placeholders
|
|
27
|
+
const escapes: string[] = [];
|
|
28
|
+
text = text.replace(/\\([\\`*_{}[\]()#+\-.!~|>])/g, (_match, ch: string) => {
|
|
29
|
+
escapes.push(ch);
|
|
30
|
+
return `${PH_ESC}${escapes.length - 1}\x00`;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// 1b. Fenced code blocks (backticks and tildes)
|
|
34
|
+
const fencedBlocks: string[] = [];
|
|
35
|
+
text = text.replace(/^(`{3,})[^\n`]*\n([\s\S]*?)^\1[ \t]*$/gm, (_match, _fence, code: string) => {
|
|
36
|
+
fencedBlocks.push(code.replace(/\n$/, ''));
|
|
37
|
+
return `${PH_FCODE}${fencedBlocks.length - 1}\x00`;
|
|
38
|
+
});
|
|
39
|
+
text = text.replace(/^(~{3,})[^\n~]*\n([\s\S]*?)^\1[ \t]*$/gm, (_match, _fence, code: string) => {
|
|
40
|
+
fencedBlocks.push(code.replace(/\n$/, ''));
|
|
41
|
+
return `${PH_FCODE}${fencedBlocks.length - 1}\x00`;
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// 1c. Inline code (backticks) — single and double backtick
|
|
45
|
+
const inlineCodes: string[] = [];
|
|
46
|
+
text = text.replace(/``([^`]+)``/g, (_match, code: string) => {
|
|
47
|
+
inlineCodes.push(code);
|
|
48
|
+
return `${PH_ICODE}${inlineCodes.length - 1}\x00`;
|
|
49
|
+
});
|
|
50
|
+
text = text.replace(/`([^`\n]+)`/g, (_match, code: string) => {
|
|
51
|
+
inlineCodes.push(code);
|
|
52
|
+
return `${PH_ICODE}${inlineCodes.length - 1}\x00`;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ── Phase 2: Block rules (line-level, order matters) ──────────────────────
|
|
56
|
+
|
|
57
|
+
// 2a. Indented code blocks (4 spaces or 1 tab after a blank line)
|
|
58
|
+
// Collect consecutive indented lines preceded by a blank line
|
|
59
|
+
text = text.replace(/(?:^|\n)\n((?:(?: |\t).+\n?)+)/g, (_match, block: string) => {
|
|
60
|
+
const code = block.replace(/^(?: |\t)/gm, '').replace(/\n$/, '');
|
|
61
|
+
fencedBlocks.push(code);
|
|
62
|
+
return `\n${PH_FCODE}${fencedBlocks.length - 1}\x00`;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 2b. Setext headings (BEFORE horizontal rules — `---` under text is H2, not HR)
|
|
66
|
+
text = text.replace(/^([^\n]+)\n={2,}[ \t]*$/gm, '[SIZE=24][B]$1[/B][/SIZE]');
|
|
67
|
+
text = text.replace(/^([^\n]+)\n-{2,}[ \t]*$/gm, '[SIZE=20][B]$1[/B][/SIZE]');
|
|
68
|
+
|
|
69
|
+
// 2c. Horizontal rules: ---, ***, ___, - - -, * * * (on their own line)
|
|
70
|
+
// Use placeholder to prevent underscores from being caught by bold/italic rules
|
|
71
|
+
text = text.replace(/^[ \t]*([-*_])[ \t]*\1[ \t]*\1(?:[ \t]*\1)*[ \t]*$/gm, `${PH_HR}\x00`);
|
|
72
|
+
text = text.replace(/^[ \t]*[-*_](?:[ \t]+[-*_]){2,}[ \t]*$/gm, `${PH_HR}\x00`);
|
|
73
|
+
|
|
74
|
+
// 2d. ATX headings: # through ######
|
|
75
|
+
text = text.replace(/^######\s+(.+?)(?:\s+#+)?$/gm, '[SIZE=16][B]$1[/B][/SIZE]');
|
|
76
|
+
text = text.replace(/^#####\s+(.+?)(?:\s+#+)?$/gm, '[SIZE=16][B]$1[/B][/SIZE]');
|
|
77
|
+
text = text.replace(/^####\s+(.+?)(?:\s+#+)?$/gm, '[SIZE=16][B]$1[/B][/SIZE]');
|
|
78
|
+
text = text.replace(/^###\s+(.+?)(?:\s+#+)?$/gm, '[SIZE=18][B]$1[/B][/SIZE]');
|
|
79
|
+
text = text.replace(/^##\s+(.+?)(?:\s+#+)?$/gm, '[SIZE=20][B]$1[/B][/SIZE]');
|
|
80
|
+
text = text.replace(/^#\s+(.+?)(?:\s+#+)?$/gm, '[SIZE=24][B]$1[/B][/SIZE]');
|
|
81
|
+
|
|
82
|
+
// 2e. Blockquotes: > text → >>text (multi-level: >> text → >>>>text)
|
|
83
|
+
text = text.replace(/^(>{1,})\s?(.*)$/gm, (_m, arrows: string, content: string) => {
|
|
84
|
+
return '>'.repeat(arrows.length * 2) + content;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// 2f. Task lists (GFM): - [x] done / - [ ] todo → emoji checkboxes
|
|
88
|
+
// Must run BEFORE generic list rules to avoid double-processing
|
|
89
|
+
text = text.replace(/^([ \t]*)[-*+]\s+\[x\]\s+(.*)$/gmi, (_m, indent: string, content: string) => {
|
|
90
|
+
const depth = Math.floor(indent.replace(/\t/g, ' ').length / 2);
|
|
91
|
+
return '\t'.repeat(depth) + '✅ ' + content;
|
|
92
|
+
});
|
|
93
|
+
text = text.replace(/^([ \t]*)[-*+]\s+\[ \]\s+(.*)$/gm, (_m, indent: string, content: string) => {
|
|
94
|
+
const depth = Math.floor(indent.replace(/\t/g, ' ').length / 2);
|
|
95
|
+
return '\t'.repeat(depth) + '☑️ ' + content;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// 2g. Ordered lists: 1. item / 2. item → number with dot
|
|
99
|
+
// Handle nested lists with tab indentation
|
|
100
|
+
text = text.replace(/^([ \t]*)(\d+)\.\s+(.*)$/gm, (_m, indent: string, num: string, content: string) => {
|
|
101
|
+
const depth = Math.floor(indent.replace(/\t/g, ' ').length / 2);
|
|
102
|
+
return '\t'.repeat(depth) + num + '. ' + content;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// 2h. Unordered lists: - item / * item / + item → • item
|
|
106
|
+
// Handle nested lists with tab indentation
|
|
107
|
+
text = text.replace(/^([ \t]*)[-*+]\s+(.*)$/gm, (_m, indent: string, content: string) => {
|
|
108
|
+
const depth = Math.floor(indent.replace(/\t/g, ' ').length / 2);
|
|
109
|
+
return '\t'.repeat(depth) + '• ' + content;
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ── Phase 3: Inline rules (order matters) ─────────────────────────────────
|
|
113
|
+
|
|
114
|
+
// 3a. Images:  → [IMG size=medium]url [/IMG] (note: space before closing tag is required by B24)
|
|
115
|
+
text = text.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '[IMG size=medium]$2 [/IMG]');
|
|
116
|
+
|
|
117
|
+
// 3b. Bold+Italic combined: ***text*** or ___text___ → [B][I]text[/I][/B]
|
|
118
|
+
text = text.replace(/\*\*\*(.+?)\*\*\*/g, '[B][I]$1[/I][/B]');
|
|
119
|
+
text = text.replace(/___(.+?)___/g, '[B][I]$1[/I][/B]');
|
|
120
|
+
|
|
121
|
+
// 3c. Bold: **text** or __text__
|
|
122
|
+
text = text.replace(/\*\*(.+?)\*\*/g, '[B]$1[/B]');
|
|
123
|
+
text = text.replace(/__(.+?)__/g, '[B]$1[/B]');
|
|
124
|
+
|
|
125
|
+
// 3d. Italic: *text* or _text_ (word boundaries to avoid false positives)
|
|
126
|
+
text = text.replace(/(?<!\w)\*([^*]+?)\*(?!\w)/g, '[I]$1[/I]');
|
|
127
|
+
text = text.replace(/(?<!\w)_([^_]+?)_(?!\w)/g, '[I]$1[/I]');
|
|
128
|
+
|
|
129
|
+
// 3e. Strikethrough: ~~text~~
|
|
130
|
+
text = text.replace(/~~(.+?)~~/g, '[S]$1[/S]');
|
|
131
|
+
|
|
132
|
+
// 3f. HTML inline formatting tags
|
|
133
|
+
text = text.replace(/<u>([\s\S]*?)<\/u>/gi, '[U]$1[/U]');
|
|
134
|
+
text = text.replace(/<b>([\s\S]*?)<\/b>/gi, '[B]$1[/B]');
|
|
135
|
+
text = text.replace(/<strong>([\s\S]*?)<\/strong>/gi, '[B]$1[/B]');
|
|
136
|
+
text = text.replace(/<i>([\s\S]*?)<\/i>/gi, '[I]$1[/I]');
|
|
137
|
+
text = text.replace(/<em>([\s\S]*?)<\/em>/gi, '[I]$1[/I]');
|
|
138
|
+
text = text.replace(/<s>([\s\S]*?)<\/s>/gi, '[S]$1[/S]');
|
|
139
|
+
text = text.replace(/<del>([\s\S]*?)<\/del>/gi, '[S]$1[/S]');
|
|
140
|
+
text = text.replace(/<strike>([\s\S]*?)<\/strike>/gi, '[S]$1[/S]');
|
|
141
|
+
|
|
142
|
+
// 3g. Links: [text](url) → [URL=url]text[/URL]
|
|
143
|
+
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '[URL=$2]$1[/URL]');
|
|
144
|
+
|
|
145
|
+
// 3h. Autolink URL: <https://...> → [URL]https://...[/URL]
|
|
146
|
+
text = text.replace(/<(https?:\/\/[^>]+)>/g, '[URL]$1[/URL]');
|
|
147
|
+
|
|
148
|
+
// 3i. Autolink email: <user@example.com> → [URL]mailto:user@example.com[/URL]
|
|
149
|
+
text = text.replace(/<([a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,})>/g, '[URL]mailto:$1[/URL]');
|
|
150
|
+
|
|
151
|
+
// ── Phase 4: Restore placeholders ─────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
// 4a. Horizontal rules → visual separator
|
|
154
|
+
text = text.replace(new RegExp(`${PH_HR.replace(/\x00/g, '\\x00')}\\x00`, 'g'), '____________');
|
|
155
|
+
|
|
156
|
+
// 4b. Inline code → [CODE]...[/CODE]
|
|
157
|
+
text = text.replace(new RegExp(`${PH_ICODE.replace(/\x00/g, '\\x00')}(\\d+)\\x00`, 'g'), (_m, idx: string) => {
|
|
158
|
+
return `[CODE]${inlineCodes[Number(idx)]}[/CODE]`;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// 4b. Fenced/indented code blocks → [CODE]...[/CODE]
|
|
162
|
+
text = text.replace(new RegExp(`${PH_FCODE.replace(/\x00/g, '\\x00')}(\\d+)\\x00`, 'g'), (_m, idx: string) => {
|
|
163
|
+
return `[CODE]${fencedBlocks[Number(idx)]}[/CODE]`;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// 4c. Escape sequences → literal characters
|
|
167
|
+
text = text.replace(new RegExp(`${PH_ESC.replace(/\x00/g, '\\x00')}(\\d+)\\x00`, 'g'), (_m, idx: string) => {
|
|
168
|
+
return escapes[Number(idx)];
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return text;
|
|
42
172
|
}
|
|
43
173
|
|
|
44
174
|
/**
|