mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-10 21:06:08 +00:00
The markdown link regex character class [^)\s>]+ stopped at the first closing paren, truncating URLs like wikipedia.org/wiki/URL_(disambiguation) at the opening (. Replace with a balanced-parens pattern that supports one level of parenthetical groups inside the URL path. Add trimUnbalancedTrailingParens for bare URLs in prose where a trailing ) belongs to surrounding punctuation rather than the URL itself.
This commit is contained in:
@@ -47,6 +47,21 @@ describe("extractUrls", () => {
|
||||
const urls = extractUrls("https://example.com/path?q=1&r=2#section");
|
||||
expect(urls).toEqual(["https://example.com/path?q=1&r=2#section"]);
|
||||
});
|
||||
|
||||
it("extracts markdown link hrefs with parentheses in the URL", () => {
|
||||
const url = "https://en.wikipedia.org/wiki/URL_(disambiguation)";
|
||||
expect(extractUrls(`[Wikipedia](${url})`)).toEqual([url]);
|
||||
});
|
||||
|
||||
it("handles bare URLs with trailing closing paren as punctuation", () => {
|
||||
const urls = extractUrls("(see https://example.com/path)");
|
||||
expect(urls).toEqual(["https://example.com/path"]);
|
||||
});
|
||||
|
||||
it("handles markdown link with angle brackets and parenthetical URL", () => {
|
||||
const url = "https://en.wikipedia.org/wiki/Special_(film)";
|
||||
expect(extractUrls(`[link](<${url}>)`)).toEqual([url]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addOsc8Hyperlinks", () => {
|
||||
@@ -126,6 +141,13 @@ describe("addOsc8Hyperlinks", () => {
|
||||
expect(result[0]).toContain(`\x1b]8;;${long}\x07${fragment}\x1b]8;;\x07`);
|
||||
});
|
||||
|
||||
it("wraps URLs with parentheses in markdown rendered text", () => {
|
||||
const url = "https://en.wikipedia.org/wiki/URL_(disambiguation)";
|
||||
const line = `Wikipedia (${url})`;
|
||||
const result = addOsc8Hyperlinks([line], [url]);
|
||||
expect(result[0]).toBe(`Wikipedia (\x1b]8;;${url}\x07${url}\x1b]8;;\x07)`);
|
||||
});
|
||||
|
||||
it("handles URL split across three lines", () => {
|
||||
const fullUrl = "https://example.com/a/very/long/path/that/keeps/going/and/going";
|
||||
const lines = ["https://example.com/a/very/lon", "g/path/that/keeps/going/and/g", "oing"];
|
||||
|
||||
@@ -6,6 +6,37 @@ const ANSI_RE = new RegExp(`${SGR_PATTERN}|${OSC8_PATTERN}`, "g");
|
||||
const SGR_START_RE = new RegExp(`^${SGR_PATTERN}`);
|
||||
const OSC8_START_RE = new RegExp(`^${OSC8_PATTERN}`);
|
||||
|
||||
/** Allow one level of balanced parentheses inside a URL so markdown link
|
||||
* targets like `https://en.wikipedia.org/wiki/URL_(disambiguation)` are
|
||||
* fully captured instead of truncated at the first `)`. */
|
||||
const URL_PATH_WITH_PARENS = /https?:\/\/[^()\s<>]*(?:\([^()\s<>]*\)[^()\s<>]*)*/g;
|
||||
|
||||
/** Strip trailing `)` characters that don't have a matching `(` in the URL.
|
||||
* Bare URLs in prose can pick up a trailing `)` that belongs to surrounding
|
||||
* punctuation, e.g. `(see https://example.com/path)` — the `)` after `path`
|
||||
* is sentence punctuation, not part of the URL. */
|
||||
function trimUnbalancedTrailingParens(url: string): string {
|
||||
let open = 0;
|
||||
for (const ch of url) {
|
||||
if (ch === "(") {
|
||||
open++;
|
||||
} else if (ch === ")") {
|
||||
open--;
|
||||
}
|
||||
}
|
||||
// If close parens outnumber open parens, the trailing ones are not
|
||||
// part of the URL — strip them.
|
||||
if (open >= 0) {
|
||||
return url;
|
||||
}
|
||||
let trimmed = url;
|
||||
while (trimmed.endsWith(")") && open < 0) {
|
||||
trimmed = trimmed.slice(0, -1);
|
||||
open++;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all unique URLs from raw markdown text.
|
||||
* Finds both bare URLs and markdown link hrefs [text](url).
|
||||
@@ -14,20 +45,20 @@ export function extractUrls(markdown: string): string[] {
|
||||
const urls = new Set<string>();
|
||||
|
||||
// Markdown link hrefs: [text](url), with optional <...> and optional title.
|
||||
const mdLinkRe = /\[(?:[^\]]*)\]\(\s*<?(https?:\/\/[^)\s>]+)>?(?:\s+["'][^"']*["'])?\s*\)/g;
|
||||
const mdLinkRe = new RegExp(
|
||||
`\\[(?:[^\\]]*)\\]\\(\\s*<?(${URL_PATH_WITH_PARENS.source})>?(?:\\s+["'][^"']*["'])?\\s*\\)`,
|
||||
"g",
|
||||
);
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = mdLinkRe.exec(markdown)) !== null) {
|
||||
urls.add(m[1]);
|
||||
}
|
||||
|
||||
// Bare URLs (remove markdown links first to avoid double-matching)
|
||||
const stripped = markdown.replace(
|
||||
/\[(?:[^\]]*)\]\(\s*<?https?:\/\/[^)\s>]+>?(?:\s+["'][^"']*["'])?\s*\)/g,
|
||||
"",
|
||||
);
|
||||
const bareRe = /https?:\/\/[^\s)\]>]+/g;
|
||||
const stripped = markdown.replace(mdLinkRe, "");
|
||||
const bareRe = /https?:\/\/[^\s\]>]+/g;
|
||||
while ((m = bareRe.exec(stripped)) !== null) {
|
||||
urls.add(m[0]);
|
||||
urls.add(trimUnbalancedTrailingParens(m[0]));
|
||||
}
|
||||
|
||||
return [...urls];
|
||||
@@ -86,12 +117,12 @@ function findUrlRanges(
|
||||
}
|
||||
|
||||
// Find new URL starts in visible text
|
||||
const urlRe = /https?:\/\/[^\s)\]>]+/g;
|
||||
const urlRe = /https?:\/\/[^\s\]>]+/g;
|
||||
urlRe.lastIndex = searchFrom;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = urlRe.exec(visibleText)) !== null) {
|
||||
const fragment = match[0];
|
||||
const fragment = trimUnbalancedTrailingParens(match[0]);
|
||||
const start = match.index;
|
||||
|
||||
// Resolve fragment to a known URL (exact > prefix > superstring)
|
||||
|
||||
Reference in New Issue
Block a user