diff --git a/src/telegram/format.ts b/src/telegram/format.ts
index 7feab62aeadf..c970e6f8ce8f 100644
--- a/src/telegram/format.ts
+++ b/src/telegram/format.ts
@@ -41,7 +41,6 @@ const FILE_EXTENSIONS_WITH_TLD = new Set([
"at", // Assembly (Austria)
"be", // Backend files (Belgium)
"cc", // C++ source (Cocos Islands)
- "co", // Configuration (Colombia)
]);
/** Detects when markdown-it linkify auto-generated a link from a bare filename (e.g. README.md → http://README.md) */
@@ -150,7 +149,7 @@ export function wrapFileReferencesInHtml(html: string): string {
return `${escapeHtml(label)}`;
});
const filePattern = new RegExp(
- `(^|>|[\\s])([a-zA-Z0-9_.\\-./]+\\.(?:${extensionsPattern}))(?=$|[\\s<])`,
+ `(^|[^a-zA-Z0-9_\\-/])([a-zA-Z0-9_.\\-./]+\\.(?:${extensionsPattern}))(?=$|[^a-zA-Z0-9_\\-/])`,
"gi",
);
@@ -173,11 +172,18 @@ export function wrapFileReferencesInHtml(html: string): string {
// Process text before this tag
const textBefore = html.slice(lastIndex, tagStart);
- result += textBefore.replace(filePattern, (m, prefix, filename) => {
+ result += textBefore.replace(filePattern, (m, prefix, filename, offset, source) => {
// Skip if inside protected tags or if it's a URL
if (codeDepth > 0 || preDepth > 0 || anchorDepth > 0) {
return m;
}
+ // Skip if we're inside any HTML tag (e.g., attributes on tags other than code/pre/a)
+ const filenameOffset = Number(offset) + String(prefix).length;
+ const lastOpen = String(source).lastIndexOf("<", filenameOffset);
+ const lastClose = String(source).lastIndexOf(">", filenameOffset);
+ if (lastOpen > lastClose) {
+ return m;
+ }
if (filename.startsWith("//")) {
return m;
}
@@ -203,10 +209,16 @@ export function wrapFileReferencesInHtml(html: string): string {
// Process remaining text
const remainingText = html.slice(lastIndex);
- result += remainingText.replace(filePattern, (m, prefix, filename) => {
+ result += remainingText.replace(filePattern, (m, prefix, filename, offset, source) => {
if (codeDepth > 0 || preDepth > 0 || anchorDepth > 0) {
return m;
}
+ const filenameOffset = Number(offset) + String(prefix).length;
+ const lastOpen = String(source).lastIndexOf("<", filenameOffset);
+ const lastClose = String(source).lastIndexOf(">", filenameOffset);
+ if (lastOpen > lastClose) {
+ return m;
+ }
if (filename.startsWith("//")) {
return m;
}
diff --git a/src/telegram/format.wrap-md.test.ts b/src/telegram/format.wrap-md.test.ts
index e882ef837b70..9fc3e386f323 100644
--- a/src/telegram/format.wrap-md.test.ts
+++ b/src/telegram/format.wrap-md.test.ts
@@ -73,6 +73,13 @@ describe("wrapFileReferencesInHtml", () => {
expect(wrapFileReferencesInHtml("Ends with file.md")).toContain("file.md");
});
+ it("wraps file refs with punctuation boundaries", () => {
+ expect(wrapFileReferencesInHtml("See README.md.")).toContain("README.md.");
+ expect(wrapFileReferencesInHtml("See README.md,")).toContain("README.md,");
+ expect(wrapFileReferencesInHtml("(README.md)")).toContain("(README.md)");
+ expect(wrapFileReferencesInHtml("README.md:")).toContain("README.md:");
+ });
+
it("de-linkifies auto-linkified file ref anchors", () => {
const input = 'README.md';
expect(wrapFileReferencesInHtml(input)).toBe("README.md");
@@ -193,15 +200,12 @@ describe("edge cases", () => {
expect(result).toBe("README.md");
});
- it("wraps supported TLD extensions (.am, .at, .be, .cc, .co)", () => {
- const result = markdownToTelegramHtml(
- "Makefile.am and code.at and app.be and main.cc and config.co",
- );
+ it("wraps supported TLD extensions (.am, .at, .be, .cc)", () => {
+ const result = markdownToTelegramHtml("Makefile.am and code.at and app.be and main.cc");
expect(result).toContain("Makefile.am");
expect(result).toContain("code.at");
expect(result).toContain("app.be");
expect(result).toContain("main.cc");
- expect(result).toContain("config.co");
});
it("does not wrap popular domain TLDs (.ai, .io, .tv, .fm)", () => {
@@ -214,6 +218,14 @@ describe("edge cases", () => {
expect(result).toContain('');
});
+ it("keeps .co domains as links", () => {
+ const result = markdownToTelegramHtml("Visit t.co and openclaw.co");
+ expect(result).toContain('');
+ expect(result).toContain('');
+ expect(result).not.toContain("t.co");
+ expect(result).not.toContain("openclaw.co");
+ });
+
it("does not wrap non-TLD extensions", () => {
const result = markdownToTelegramHtml("image.png and style.css and script.js");
expect(result).not.toContain("image.png");