openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
520 lines (519 loc) • 19.4 kB
JavaScript
import { a as normalizeLowercaseStringOrEmpty } from "./string-coerce-mnp54Vah.js";
import "./string-coerce-runtime-CEGJWkQ_.js";
import { i as markdownToIR, n as renderMarkdownWithMarkers } from "./tables-BgtXxld3.js";
import { r as renderMarkdownIRChunksWithinLimit } from "./chunk-items-HPRB2OIa.js";
import { n as isAutoLinkedFileRef, t as FILE_REF_EXTENSIONS_WITH_TLD } from "./auto-linked-file-ref-t_9l4Xzl.js";
import "./text-chunking-D45TeeEs.js";
//#region extensions/telegram/src/format.ts
function escapeTelegramHtml(text) {
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function escapeHtml(text) {
return escapeTelegramHtml(text);
}
function escapeHtmlAttr(text) {
return escapeHtml(text).replace(/"/g, """);
}
/**
* File extensions that share TLDs and commonly appear in code/documentation.
* These are wrapped in <code> tags to prevent Telegram from generating
* spurious domain registrar previews.
*
* Only includes extensions that are:
* 1. Commonly used as file extensions in code/docs
* 2. Rarely used as intentional domain references
*
* Excluded: .ai, .io, .tv, .fm (popular domain TLDs like x.ai, vercel.io, github.io)
*/
function buildTelegramLink(link, text) {
const href = link.href.trim();
if (!href) return null;
if (link.start === link.end) return null;
if (isAutoLinkedFileRef(href, text.slice(link.start, link.end))) return null;
const safeHref = escapeHtmlAttr(href);
return {
start: link.start,
end: link.end,
open: `<a href="${safeHref}">`,
close: "</a>"
};
}
function buildTelegramCodeBlockOpen(span) {
if (!span.language) return "<pre><code>";
return `<pre><code class="language-${escapeHtmlAttr(span.language)}">`;
}
function renderTelegramHtml(ir) {
return renderMarkdownWithMarkers(ir, {
styleMarkers: {
bold: {
open: "<b>",
close: "</b>"
},
italic: {
open: "<i>",
close: "</i>"
},
strikethrough: {
open: "<s>",
close: "</s>"
},
code: {
open: "<code>",
close: "</code>"
},
code_block: {
open: buildTelegramCodeBlockOpen,
close: "</code></pre>"
},
spoiler: {
open: "<tg-spoiler>",
close: "</tg-spoiler>"
},
blockquote: {
open: "<blockquote>",
close: "</blockquote>"
}
},
escapeText: escapeHtml,
buildLink: buildTelegramLink
});
}
function leadingWhitespaceLength(line) {
let length = 0;
while (line[length] === " " || line[length] === " ") length++;
return length;
}
function isTelegramBulletLine(line) {
return /^[ \t]*(?:[•*+-])[ \t]+\S/.test(line);
}
function isTelegramListBoundaryLine(line) {
return /^[ \t]*(?:\d+\.|#{1,6})[ \t]+\S/.test(line);
}
function isMarkdownIndentedCodeLine(line) {
return /^(?: {4}|\t)/.test(line);
}
function shouldPreserveTelegramListBoundarySpacing(previous, next) {
return !isMarkdownIndentedCodeLine(previous) && !isMarkdownIndentedCodeLine(next) && isTelegramBulletLine(previous) && isTelegramListBoundaryLine(next) && leadingWhitespaceLength(next) <= leadingWhitespaceLength(previous);
}
function preserveTelegramListBoundarySpacing(markdown) {
const lines = markdown.split("\n");
const out = [];
let inFence = false;
let previousLine = "";
for (const line of lines) {
const normalizedLine = line.replace(/\r$/, "");
const isFenceLine = /^[ \t]*(?:```|~~~)/.test(normalizedLine);
if (!inFence && shouldPreserveTelegramListBoundarySpacing(previousLine, normalizedLine)) out.push("");
out.push(line);
if (isFenceLine) inFence = !inFence;
previousLine = normalizedLine;
}
return out.join("\n");
}
function markdownToTelegramHtml(markdown, options = {}) {
const telegramHtml = preserveSupportedTelegramHtmlTags(renderTelegramHtml(markdownToIR(preserveTelegramListBoundarySpacing(markdown ?? ""), {
linkify: true,
enableSpoilers: true,
headingStyle: "none",
blockquotePrefix: "",
tableMode: options.tableMode
})));
if (options.wrapFileRefs !== false) return wrapFileReferencesInHtml(telegramHtml);
return telegramHtml;
}
/**
* Wraps standalone file references (with TLD extensions) in <code> tags.
* This prevents Telegram from treating them as URLs and generating
* irrelevant domain registrar previews.
*
* Runs AFTER markdown→HTML conversion to avoid modifying HTML attributes.
* Skips content inside <code>, <pre>, and <a> tags to avoid nesting issues.
*/
/** Escape regex metacharacters in a string */
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
const AUTO_LINKED_ANCHOR_PATTERN = /<a\s+href="https?:\/\/([^"]+)"[^>]*>\1<\/a>/gi;
const HTML_TAG_PATTERN = /(<\/?)([a-zA-Z][a-zA-Z0-9-]*)\b[^>]*?>/gi;
const HTML_MODE_TAG_PATTERN = /^<(\/?)([a-zA-Z][a-zA-Z0-9-]*)([^<>]*)>$/;
const ESCAPED_HTML_TAG_PATTERN = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)(.*?)>/g;
const TELEGRAM_HTML_ANCHOR_PATTERN = /<a\b[^>]*\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))[^>]*>([\s\S]*?)<\/a\s*>/gi;
const TELEGRAM_HTML_BREAK_PATTERN = /<br\s*\/?>/gi;
const TELEGRAM_HTML_ENTITY_PATTERN = /&(#x[0-9A-Fa-f]+|#\d+|amp|lt|gt|quot|apos);/g;
const TELEGRAM_HTML_TAG_PATTERN = /<[^>]*>/g;
const TELEGRAM_SIMPLE_HTML_TAGS = new Set([
"b",
"strong",
"i",
"em",
"u",
"ins",
"s",
"strike",
"del",
"code",
"pre",
"tg-spoiler",
"blockquote"
]);
const TELEGRAM_ATTR_HTML_TAG_PATTERNS = new Map([
["a", /^\s+href="[^"]+"\s*$/],
["span", /^\s+class="tg-spoiler"\s*$/],
["tg-emoji", /^\s+emoji-id="[^"]+"\s*$/],
["tg-time", /^\s+datetime="[^"]+"\s*$/]
]);
const TELEGRAM_CODE_LANGUAGE_ATTR_PATTERN = /^\s+class="language-[^"]+"\s*$/;
let fileReferencePattern;
let orphanedTldPattern;
function popLastTagName(tags, name) {
for (let index = tags.length - 1; index >= 0; index -= 1) if (tags[index] === name) {
tags.splice(index, 1);
return true;
}
return false;
}
function isSupportedTelegramHtmlTag(rawTag) {
const match = HTML_MODE_TAG_PATTERN.exec(rawTag);
if (!match) return false;
const closing = match[1] === "/";
const name = normalizeLowercaseStringOrEmpty(match[2]);
const attrs = match[3] ?? "";
if (TELEGRAM_SIMPLE_HTML_TAGS.has(name)) return attrs.trim() === "";
if (closing) return attrs.trim() === "";
return TELEGRAM_ATTR_HTML_TAG_PATTERNS.get(name)?.test(attrs) ?? false;
}
function hasOpenTelegramHtmlTag(tags, name) {
return tags.includes(name);
}
function preserveTelegramHtmlTag(rawTag, openTags, escapeTag) {
const match = HTML_MODE_TAG_PATTERN.exec(rawTag);
if (!match) return escapeTag(rawTag);
const closing = match[1] === "/";
const tagName = normalizeLowercaseStringOrEmpty(match[2]);
const attrs = match[3] ?? "";
if (!closing && tagName === "code" && TELEGRAM_CODE_LANGUAGE_ATTR_PATTERN.test(attrs)) {
openTags.push(tagName);
if (hasOpenTelegramHtmlTag(openTags, "pre")) return rawTag;
return "<code>";
}
if (!isSupportedTelegramHtmlTag(rawTag)) return escapeTag(rawTag);
if (closing) return popLastTagName(openTags, tagName) ? rawTag : escapeTag(rawTag);
openTags.push(tagName);
return rawTag;
}
function escapeUnsupportedTelegramHtml(text) {
let result = "";
let index = 0;
const openTags = [];
while (index < text.length) {
const char = text[index];
if (char === "&") {
const entityEnd = findTelegramHtmlEntityEnd(text, index);
if (entityEnd !== -1) {
result += text.slice(index, entityEnd + 1);
index = entityEnd + 1;
} else {
result += "&";
index += 1;
}
continue;
}
if (char === "<") {
const end = text.indexOf(">", index + 1);
if (end !== -1) {
const rawTag = text.slice(index, end + 1);
result += preserveTelegramHtmlTag(rawTag, openTags, escapeHtml);
index = end + 1;
} else {
result += "<";
index += 1;
}
continue;
}
if (char === ">") {
result += ">";
index += 1;
continue;
}
result += char;
index += 1;
}
return result;
}
function decodeTelegramHtmlEntity(entity, fallback) {
if (entity.startsWith("#x") || entity.startsWith("#X")) {
const codePoint = Number.parseInt(entity.slice(2), 16);
return Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 1114111 ? String.fromCodePoint(codePoint) : fallback;
}
if (entity.startsWith("#")) {
const codePoint = Number.parseInt(entity.slice(1), 10);
return Number.isInteger(codePoint) && codePoint >= 0 && codePoint <= 1114111 ? String.fromCodePoint(codePoint) : fallback;
}
switch (entity) {
case "amp": return "&";
case "lt": return "<";
case "gt": return ">";
case "quot": return "\"";
case "apos": return "'";
default: return fallback;
}
}
function decodeTelegramHtmlEntities(text) {
return text.replace(TELEGRAM_HTML_ENTITY_PATTERN, (match, entity) => decodeTelegramHtmlEntity(entity, match));
}
function stripTelegramHtmlForPlainText(html) {
return decodeTelegramHtmlEntities(html.replace(TELEGRAM_HTML_BREAK_PATTERN, "\n").replace(TELEGRAM_HTML_TAG_PATTERN, ""));
}
function encodePlainTextForTelegramHtmlStrip(text) {
return text.replace(/[&<>]/g, (char) => {
switch (char) {
case "&": return "&";
case "<": return "<";
case ">": return ">";
default: return char;
}
});
}
function telegramHtmlToPlainTextFallback(html) {
TELEGRAM_HTML_ANCHOR_PATTERN.lastIndex = 0;
return stripTelegramHtmlForPlainText(html.replace(TELEGRAM_HTML_ANCHOR_PATTERN, (_match, doubleQuotedHref, singleQuotedHref, unquotedHref, labelHtml) => {
const href = decodeTelegramHtmlEntities(doubleQuotedHref ?? singleQuotedHref ?? unquotedHref ?? "").trim();
const label = stripTelegramHtmlForPlainText(labelHtml).trim();
if (!href) return encodePlainTextForTelegramHtmlStrip(label);
return encodePlainTextForTelegramHtmlStrip(!label || label === href ? href : `${label} (${href})`);
}));
}
function promoteEscapedSupportedTelegramTags(text, openTags) {
ESCAPED_HTML_TAG_PATTERN.lastIndex = 0;
return text.replace(ESCAPED_HTML_TAG_PATTERN, (match, closing, name, attrs) => preserveTelegramHtmlTag(`<${closing}${name}${attrs}>`, openTags, () => match));
}
function preserveSupportedTelegramHtmlTags(html) {
let codeDepth = 0;
let preDepth = 0;
let result = "";
let lastIndex = 0;
const openEscapedTags = [];
HTML_TAG_PATTERN.lastIndex = 0;
let match;
while ((match = HTML_TAG_PATTERN.exec(html)) !== null) {
const tagStart = match.index;
const tagEnd = HTML_TAG_PATTERN.lastIndex;
const tagName = normalizeLowercaseStringOrEmpty(match[2]);
const isClosing = match[1] === "</";
const textBefore = html.slice(lastIndex, tagStart);
result += codeDepth > 0 || preDepth > 0 ? textBefore : promoteEscapedSupportedTelegramTags(textBefore, openEscapedTags);
if (tagName === "code") codeDepth = isClosing ? Math.max(0, codeDepth - 1) : codeDepth + 1;
else if (tagName === "pre") preDepth = isClosing ? Math.max(0, preDepth - 1) : preDepth + 1;
result += html.slice(tagStart, tagEnd);
lastIndex = tagEnd;
}
const remainingText = html.slice(lastIndex);
result += codeDepth > 0 || preDepth > 0 ? remainingText : promoteEscapedSupportedTelegramTags(remainingText, openEscapedTags);
return result;
}
function getFileReferencePattern() {
if (fileReferencePattern) return fileReferencePattern;
const fileExtensionsPattern = Array.from(FILE_REF_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
fileReferencePattern = new RegExp(`(^|[^a-zA-Z0-9_\\-/])([a-zA-Z0-9_.\\-./]+\\.(?:${fileExtensionsPattern}))(?=$|[^a-zA-Z0-9_\\-/])`, "gi");
return fileReferencePattern;
}
function getOrphanedTldPattern() {
if (orphanedTldPattern) return orphanedTldPattern;
const fileExtensionsPattern = Array.from(FILE_REF_EXTENSIONS_WITH_TLD).map(escapeRegex).join("|");
orphanedTldPattern = new RegExp(`([^a-zA-Z0-9]|^)([A-Za-z]\\.(?:${fileExtensionsPattern}))(?=[^a-zA-Z0-9/]|$)`, "g");
return orphanedTldPattern;
}
function wrapStandaloneFileRef(match, prefix, filename) {
if (filename.startsWith("//")) return match;
if (/https?:\/\/$/i.test(prefix)) return match;
return `${prefix}<code>${escapeHtml(filename)}</code>`;
}
function wrapSegmentFileRefs(text, codeDepth, preDepth, anchorDepth) {
if (!text || codeDepth > 0 || preDepth > 0 || anchorDepth > 0) return text;
return text.replace(getFileReferencePattern(), wrapStandaloneFileRef).replace(getOrphanedTldPattern(), (match, prefix, tld) => prefix === ">" ? match : `${prefix}<code>${escapeHtml(tld)}</code>`);
}
function wrapFileReferencesInHtml(html) {
AUTO_LINKED_ANCHOR_PATTERN.lastIndex = 0;
const deLinkified = html.replace(AUTO_LINKED_ANCHOR_PATTERN, (_match, label) => {
if (!isAutoLinkedFileRef(`http://${label}`, label)) return _match;
return `<code>${escapeHtml(label)}</code>`;
});
let codeDepth = 0;
let preDepth = 0;
let anchorDepth = 0;
let result = "";
let lastIndex = 0;
HTML_TAG_PATTERN.lastIndex = 0;
let match;
while ((match = HTML_TAG_PATTERN.exec(deLinkified)) !== null) {
const tagStart = match.index;
const tagEnd = HTML_TAG_PATTERN.lastIndex;
const isClosing = match[1] === "</";
const tagName = normalizeLowercaseStringOrEmpty(match[2]);
const textBefore = deLinkified.slice(lastIndex, tagStart);
result += wrapSegmentFileRefs(textBefore, codeDepth, preDepth, anchorDepth);
if (tagName === "code") codeDepth = isClosing ? Math.max(0, codeDepth - 1) : codeDepth + 1;
else if (tagName === "pre") preDepth = isClosing ? Math.max(0, preDepth - 1) : preDepth + 1;
else if (tagName === "a") anchorDepth = isClosing ? Math.max(0, anchorDepth - 1) : anchorDepth + 1;
result += deLinkified.slice(tagStart, tagEnd);
lastIndex = tagEnd;
}
const remainingText = deLinkified.slice(lastIndex);
result += wrapSegmentFileRefs(remainingText, codeDepth, preDepth, anchorDepth);
return result;
}
function renderTelegramHtmlText(text, options = {}) {
if ((options.textMode ?? "markdown") === "html") return escapeUnsupportedTelegramHtml(text);
return markdownToTelegramHtml(text, { tableMode: options.tableMode });
}
const TELEGRAM_SELF_CLOSING_HTML_TAGS = new Set(["br"]);
function buildTelegramHtmlOpenPrefix(tags) {
return tags.map((tag) => tag.openTag).join("");
}
function buildTelegramHtmlCloseSuffix(tags) {
return tags.slice().toReversed().map((tag) => tag.closeTag).join("");
}
function buildTelegramHtmlCloseSuffixLength(tags) {
return tags.reduce((total, tag) => total + tag.closeTag.length, 0);
}
function findTelegramHtmlEntityEnd(text, start) {
if (text[start] !== "&") return -1;
let index = start + 1;
if (index >= text.length) return -1;
if (text[index] === "#") {
index += 1;
if (index >= text.length) return -1;
if (text[index] === "x" || text[index] === "X") {
index += 1;
const hexStart = index;
while (/[0-9A-Fa-f]/.test(text[index] ?? "")) index += 1;
if (index === hexStart) return -1;
} else {
const digitStart = index;
while (/[0-9]/.test(text[index] ?? "")) index += 1;
if (index === digitStart) return -1;
}
} else {
const nameStart = index;
while (/[A-Za-z0-9]/.test(text[index] ?? "")) index += 1;
if (index === nameStart) return -1;
}
return text[index] === ";" ? index : -1;
}
function findTelegramHtmlSafeSplitIndex(text, maxLength) {
if (text.length <= maxLength) return text.length;
const normalizedMaxLength = Math.max(1, Math.floor(maxLength));
const lastAmpersand = text.lastIndexOf("&", normalizedMaxLength - 1);
if (lastAmpersand === -1) return normalizedMaxLength;
if (lastAmpersand < text.lastIndexOf(";", normalizedMaxLength - 1)) return normalizedMaxLength;
const entityEnd = findTelegramHtmlEntityEnd(text, lastAmpersand);
if (entityEnd === -1 || entityEnd < normalizedMaxLength) return normalizedMaxLength;
return lastAmpersand;
}
function popTelegramHtmlTag(tags, name) {
for (let index = tags.length - 1; index >= 0; index -= 1) if (tags[index]?.name === name) {
tags.splice(index, 1);
return;
}
}
function splitTelegramHtmlChunks(html, limit) {
if (!html) return [];
const normalizedLimit = Math.max(1, Math.floor(limit));
if (html.length <= normalizedLimit) return [html];
const chunks = [];
const openTags = [];
let current = "";
let chunkHasPayload = false;
const resetCurrent = () => {
current = buildTelegramHtmlOpenPrefix(openTags);
chunkHasPayload = false;
};
const flushCurrent = () => {
if (!chunkHasPayload) return;
chunks.push(`${current}${buildTelegramHtmlCloseSuffix(openTags)}`);
resetCurrent();
};
const appendText = (segment) => {
let remaining = segment;
while (remaining.length > 0) {
const available = normalizedLimit - current.length - buildTelegramHtmlCloseSuffixLength(openTags);
if (available <= 0) {
if (!chunkHasPayload) throw new Error(`Telegram HTML chunk limit exceeded by tag overhead (limit=${normalizedLimit})`);
flushCurrent();
continue;
}
if (remaining.length <= available) {
current += remaining;
chunkHasPayload = true;
break;
}
const splitAt = findTelegramHtmlSafeSplitIndex(remaining, available);
if (splitAt <= 0) {
if (!chunkHasPayload) throw new Error(`Telegram HTML chunk limit exceeded by leading entity (limit=${normalizedLimit})`);
flushCurrent();
continue;
}
current += remaining.slice(0, splitAt);
chunkHasPayload = true;
remaining = remaining.slice(splitAt);
flushCurrent();
}
};
resetCurrent();
HTML_TAG_PATTERN.lastIndex = 0;
let lastIndex = 0;
let match;
while ((match = HTML_TAG_PATTERN.exec(html)) !== null) {
const tagStart = match.index;
const tagEnd = HTML_TAG_PATTERN.lastIndex;
appendText(html.slice(lastIndex, tagStart));
const rawTag = match[0];
const isClosing = match[1] === "</";
const tagName = normalizeLowercaseStringOrEmpty(match[2]);
const isSelfClosing = !isClosing && (TELEGRAM_SELF_CLOSING_HTML_TAGS.has(tagName) || rawTag.trimEnd().endsWith("/>"));
if (!isClosing) {
const nextCloseLength = isSelfClosing ? 0 : `</${tagName}>`.length;
if (chunkHasPayload && current.length + rawTag.length + buildTelegramHtmlCloseSuffixLength(openTags) + nextCloseLength > normalizedLimit) flushCurrent();
}
current += rawTag;
if (isSelfClosing) chunkHasPayload = true;
if (isClosing) popTelegramHtmlTag(openTags, tagName);
else if (!isSelfClosing) openTags.push({
name: tagName,
openTag: rawTag,
closeTag: `</${tagName}>`
});
lastIndex = tagEnd;
}
appendText(html.slice(lastIndex));
flushCurrent();
return chunks.length > 0 ? chunks : [html];
}
function renderTelegramChunkHtml(ir) {
return wrapFileReferencesInHtml(preserveSupportedTelegramHtmlTags(renderTelegramHtml(ir)));
}
function renderTelegramChunksWithinHtmlLimit(ir, limit) {
return renderMarkdownIRChunksWithinLimit({
ir,
limit,
renderChunk: renderTelegramChunkHtml,
measureRendered: (html) => html.length
}).map(({ source, rendered }) => ({
html: rendered,
text: source.text
}));
}
function markdownToTelegramChunks(markdown, limit, options = {}) {
return renderTelegramChunksWithinHtmlLimit(markdownToIR(preserveTelegramListBoundarySpacing(markdown ?? ""), {
linkify: true,
enableSpoilers: true,
headingStyle: "none",
blockquotePrefix: "",
tableMode: options.tableMode
}), limit);
}
function markdownToTelegramHtmlChunks(markdown, limit, options = {}) {
return markdownToTelegramChunks(markdown, limit, options).map((chunk) => chunk.html);
}
//#endregion
export { renderTelegramHtmlText as a, wrapFileReferencesInHtml as c, markdownToTelegramHtmlChunks as i, markdownToTelegramChunks as n, splitTelegramHtmlChunks as o, markdownToTelegramHtml as r, telegramHtmlToPlainTextFallback as s, escapeTelegramHtml as t };