fix: better handling of internal vs external links

This commit is contained in:
Simon 2026-07-01 15:17:22 +02:00
parent 0da1c014ef
commit c1da4fc874
4 changed files with 2268 additions and 20 deletions

View file

@ -1,7 +1,7 @@
{
"id": "rich-text-editor",
"name": "Rich Text Editor",
"version": "1.1.7",
"version": "1.1.8",
"minAppVersion": "1.5.7",
"description": "A rich text markdown editor based on MDXEditor.",
"author": "Simon Tysland",

View file

@ -147,6 +147,8 @@ interface Props {
onImageUpload: (image: File) => Promise<string>;
onResolveImage: (src: string) => string;
onNavigate: (path: string) => void;
// Returns true if a note/file with this linkpath exists in the vault.
onResolveLink: (linkpath: string) => boolean;
onPickInternalLink: () => Promise<string | null>;
onRenderEmbed?: EmbedRenderer;
}
@ -1122,30 +1124,35 @@ export const RichTextEditor = forwardRef<RichTextEditorRef, Props>(
// Lexical's link plugin auto-prefixes scheme-less hrefs with
// "https://", so a wikilink to "My Note" ends up as
// "https://My%20Note". Two cases to detect:
// 1. Non-aliased: stripped href matches the rendered text.
// 2. Aliased ([Alias](My Note)): text is the alias, so it
// won't match — instead, check that the "hostname" portion
// contains no dot. Real domains always have a TLD; vault
// basenames almost never do.
// "https://My%20Note". A genuine scheme-less URL the user typed
// (e.g. "www.google.com") gets the same treatment, so we can't
// tell them apart by "stripped href matches the rendered text".
// Look at the "hostname" portion:
// - No dot => a vault basename (real domains always have a TLD).
// Treat as internal even if the note doesn't exist yet, so
// clicking a brand-new [[Note]] still creates it.
// - Has a dot => ambiguous between a real domain and a note whose
// name contains a dot (e.g. "readme.md"). Resolve against the
// vault: if such a note exists, navigate internally; otherwise
// fall through to the browser.
const wasAutoPrefixed = /^https?:\/\//i.test(href);
const strippedHref = (() => {
try {
return decodeURI(href.replace(/^https?:\/\//i, ""));
} catch {
return href.replace(/^https?:\/\//i, "");
}
})();
if (text.length > 0 && strippedHref === text) {
props.onNavigate(text);
return;
}
if (wasAutoPrefixed) {
const strippedHref = (() => {
try {
return decodeURI(href.replace(/^https?:\/\//i, ""));
} catch {
return href.replace(/^https?:\/\//i, "");
}
})();
const hostPart = strippedHref
.split("/")[0]
.split("#")[0]
.split("?")[0];
if (hostPart.length > 0 && !hostPart.includes(".")) {
if (
hostPart.length > 0 &&
(!hostPart.includes(".") ||
props.onResolveLink(strippedHref))
) {
props.onNavigate(strippedHref);
return;
}

View file

@ -762,6 +762,12 @@ export class RichTextOverlay {
false,
);
}}
onResolveLink={(linkpath) =>
this.view.app.metadataCache.getFirstLinkpathDest(
linkpath,
this.view.file?.path || "",
) !== null
}
/>
</StrictMode>,
);

2237
styles.css

File diff suppressed because one or more lines are too long