Skip to content

fix: make anchor links reach CJK headings and headings in other documents (#5292) - #5298

Merged
Jocs merged 6 commits into
developfrom
fix/anchor-links-5292
Sep 10, 2026
Merged

Jocs merged 6 commits into
developfrom
fix/anchor-links-5292

Conversation

@Jocs

@Jocs Jocs commented Sep 10, 2026

Copy link
Copy Markdown
Member

Fixes #5292

Two anchor-link defects from the report:

  1. [jump](#中文标题) did nothing.
  2. [jump](other.md#section) did nothing — for English headings too.

Root causes

CJK anchors. generateGithubSlug removed 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-click treated the whole destination as a path. other.md#section is not a file, so isMarkdownFile failed and shell.openPath failed silently. The same link without the fragment opened fine.

Fix

  • generateGithubSlug keeps 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é, previously caf-rsum). The same function produces exported heading ids, PDF TOC anchors and Copy anchor link, so those stay consistent with the editor.
  • The store's anchor lookup moves into SCROLL_TO_ANCHOR and decodes the fragment first, so #%E4%B8%AD… matches as well; invalid percent-encoding is matched as written.
  • resolveLocalLinkTarget (main) splits other.md#section into the file and the fragment. A # that belongs to an existing file name (C#.md) stays in the path.
  • The fragment rides as TabOptions.anchor through the existing options slot of app-open-file-by-id → openTab — the route folder search already 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 once the tab is active, by which point editor.vue has re-seeded the TOC.

Not covered

  • With Open files in new window enabled, the linked file opens but does not jump: that path builds the window from a plain file list with no tab options.
  • Same-document anchors still resolve only top-level headings, as before.

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, and C#.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

develop this branch
e2e issue-5292-anchor-links.spec.ts (3) 3 fail 3 pass
related e2e — anchor scroll, copy anchor link, TOC scroll, tabs, tab-switch cursor, #4356, folder search (25) — 25 pass
store unit: percent-encoded fragment, open-tab switch, new-tab load (3) · local-link-target.spec.ts (5) · CJK slug case (1) all fail all pass
store unit: malformed % fragment, switch without an anchor (2) — guard existing behaviour pass pass
desktop unit / muya unit — 801 / 1483 pass
pnpm run typecheck · muya tsc · pnpm run lint — clean (lint: 0 errors, warnings unchanged)

The desktop commits skip the pre-commit hook: lint-staged's prettier --write turns async( into async (, which @stylistic/space-before-function-paren rejects, 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

Jocs and others added 6 commits September 10, 2026 22:48
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
`[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
@github-actions

Copy link
Copy Markdown

Build artifacts for PR #5298:

Run: https://github.com/marktext/marktext/actions/runs/34492443459

Artifact Size Link
marktext-windows-arm64 279.4 MB Download
marktext-macos-x64 297.6 MB Download
marktext-windows-x64 287.6 MB Download
marktext-linux 637.8 MB Download
marktext-macos-arm64 287.3 MB Download

@Jocs
Jocs merged commit 7ab05ff into develop Sep 10, 2026
16 checks passed
@Jocs
Jocs deleted the fix/anchor-links-5292 branch September 10, 2026 16:02
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] anchor point navigation issue

1 participant