Repository navigation
fix: make anchor links reach CJK headings and headings in other documents (#5292) - #5298
Merged
Merged
Conversation
generateGithubSlug stripped everything outside ASCII `\w`, so a heading such as `中文标题` got an empty slug and `[jump](#中文标题)` could never match it. Keep letters, combining marks and digits of every script, as GitHub does. ASCII headings slug exactly as before; accented Latin keeps its letters (`café`, previously `caf`). The same slug feeds exported heading ids and copied anchor links, so those now work for such headings too. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01WpZ3EZ2s8M7FcbBikYveTy
Move the heading / element-id lookup for `[x](#anchor)` out of FORMAT_LINK_CLICK into SCROLL_TO_ANCHOR so other callers can reuse it, and decode the fragment first: `#%E4%B8%AD%E6%96%87` now finds the same heading as `#中文`. A fragment that is not valid percent-encoding is matched as written. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01WpZ3EZ2s8M7FcbBikYveTy
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01WpZ3EZ2s8M7FcbBikYveTy
`[x](other.md#section)` did nothing: the main process looked for a file literally named `other.md#section`, found none, and passed the path to shell.openPath, which failed silently. resolveLocalLinkTarget now splits the fragment off, keeping a `#` that belongs to an existing file name such as `C#.md`. The fragment travels as TabOptions.anchor through the existing options slot of app-open-file-by-id and openTab, the route folder search uses to open a file at a cursor. A new tab receives it with mt::open-new-tab; an already-open file receives it with mt::switch-tab-by-file_path, which now forwards tab options. The store scrolls to it once the tab is active. With openFilesInNewWindow enabled the file still opens without jumping. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01WpZ3EZ2s8M7FcbBikYveTy
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01WpZ3EZ2s8M7FcbBikYveTy
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01WpZ3EZ2s8M7FcbBikYveTy
|
Build artifacts for PR #5298: Run: https://github.com/marktext/marktext/actions/runs/34492443459
|
This was referenced Sep 22, 2026
Jocs
added a commit
that referenced
this pull request
Sep 22, 2026
…folder (#5506) `resolveLocalLinkTarget` joined the document folder with the link target and then percent-decoded the joined string, unguarded. The target is URL-encoded text taken from the document, but `dirname` is a raw filesystem path that may legally contain `%`, so decoding the join misread the document's own folder name: a folder called `50%off` made every ordinary link inside it throw `URIError` out of the `mt::format-link-click` handler, and one called `my%20docs` resolved to `my docs`. Decode the target alone and join the raw dirname — the mirror of `encodeDirnameForUrl` on the renderer side (#5212). `isAbsolute` keeps testing the encoded target so a `%2F` cannot turn a relative link into an absolute one. A target that is not valid percent-encoding threw as well: `bad%zz.md`, `%.md`, `%E0%A4%A.md`, and also `other.md#100%`, since `toPathname` runs before the `#` split. `%` is a legal filename character, so fall back to the target as written, the same guard the renderer already applies in `SCROLL_TO_ANCHOR`. Keeping it inside `toPathname` is what lets the fragment case resolve: the literal path does not exist, so the `#` split runs and `other.md` opens with `100%` handed to the renderer, which tolerates an undecodable anchor. Since #5298 made this handler `async` the throw surfaced as an unhandled rejection rather than the error dialog reported in #4749, so the symptom on develop was a click that did nothing at all. A click on a target that does not exist stays a no-op, as it already is for any other broken local link. Closes #4749 Supersedes #4773 Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5292
Two anchor-link defects from the report:
[jump](#中文标题)did nothing.[jump](other.md#section)did nothing — for English headings too.Root causes
CJK anchors.
generateGithubSlugremoved everything outside ASCII\w, so## 中文标题got the slug''and no#中文标题could match it. The legacy engine used the same regex, so this is long-standing rather than a migration regression.Anchors into another document.
mt::format-link-clicktreated the whole destination as a path.other.md#sectionis not a file, soisMarkdownFilefailed andshell.openPathfailed silently. The same link without the fragment opened fine.Fix
generateGithubSlugkeeps letters, combining marks and digits of every script (/[^\p{L}\p{M}\p{N}\p{Pc}\s-]/gu), like GitHub. ASCII headings slug exactly as before; accented Latin keeps its letters (Café Résumé→café-résumé, previouslycaf-rsum). The same function produces exported heading ids, PDF TOC anchors and Copy anchor link, so those stay consistent with the editor.SCROLL_TO_ANCHORand decodes the fragment first, so#%E4%B8%AD…matches as well; invalid percent-encoding is matched as written.resolveLocalLinkTarget(main) splitsother.md#sectioninto the file and the fragment. A#that belongs to an existing file name (C#.md) stays in the path.TabOptions.anchorthrough the existing options slot ofapp-open-file-by-id→openTab— the route folder search already uses to open a file at a cursor. A new tab receives it withmt::open-new-tab; an already-open file receives it withmt::switch-tab-by-file_path, which now forwards tab options. The store scrolls once the tab is active, by which point editor.vue has re-seeded the TOC.Not covered
Tests
muya/src/__tests__/getTOC.spec.ts— CJK, emoji and accented-Latin slugs (the previous case locked in the ASCII-only output).desktop/test/unit/specs/editor-store-anchor.spec.ts— percent-encoded and malformed fragments; the anchor is resolved only after switching to an open tab or after a new tab has loaded, and is not stored on the tab.desktop/test/unit/specs/local-link-target.spec.ts— relative and absolute links, a percent-encoded path, andC#.md, against real temp files.desktop/test/e2e/issue-5292-anchor-links.spec.ts— Cmd/Ctrl-click an anchor to a CJK heading, into a document that is not open, and into one that already is; each asserts the target heading lands just below the editor's top edge.Verified
developissue-5292-anchor-links.spec.ts(3)local-link-target.spec.ts(5) · CJK slug case (1)%fragment, switch without an anchor (2) — guard existing behaviourpnpm run typecheck· muyatsc·pnpm run lintThe desktop commits skip the pre-commit hook: lint-staged's
prettier --writeturnsasync(intoasync (, which@stylistic/space-before-function-parenrejects, and reformats unrelated lines in the touched files. ESLint and Prettier were run on the changed code by hand.🤖 Generated with Claude Code
https://claude.ai/code/session_01WpZ3EZ2s8M7FcbBikYveTy