Repository navigation
fix(desktop): resolve local link paths without decoding the document folder (#4749) - #5506
Merged
Merged
Conversation
…nt folder `resolveLocalLinkTarget` joined `dirname` and the link target and then ran `decodeURIComponent` over the result. The target is URL-encoded text taken from the document, but `dirname` is a raw filesystem path, so decoding the joined string misread the document's own folder name: - a folder containing a bare `%` (`50%off/`) made every ordinary link in the document throw `URIError` from the `mt::format-link-click` handler; - a folder that merely looks percent-encoded (`my%20docs/`) resolved to the wrong directory (`my docs/`). Decode the target alone and join the raw `dirname`, which mirrors `encodeDirnameForUrl` on the renderer side (#5212). `isAbsolute` keeps testing the encoded target so a `%2F` cannot turn a relative link into an absolute one. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
…ral path text (#4749) Ctrl/Cmd-clicking a local link whose target is not valid percent-encoding threw `URIError: URI malformed` out of the `mt::format-link-click` handler: `[broken](bad%zz.md)`, `[bare](%.md)`, `[partial](%E0%A4%A.md)`, and also `[frag](other.md#100%)` — `toPathname` runs before the `#` split, so an invalid escape anywhere in the destination was enough. `%` is a legal filename character, so fall back to the target as written rather than rejecting the click. This matches the renderer, which already guards the same call in `SCROLL_TO_ANCHOR`. Because the guard sits inside `toPathname`, the fragment case now resolves the way it should: 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. The click on a target that does not exist stays a no-op, as it already is for any other broken local link. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2 of 6 tasks
|
Build artifacts for PR #5506: Run: https://github.com/marktext/marktext/actions/runs/35680614125
|
wangty163
pushed a commit
to wangty163/marktext
that referenced
this pull request
Sep 22, 2026
Brings in 127 upstream commits (2026-09-01..2026-09-22): Pandoc export, source-mode line numbers, selection word count, the pandoc TeX-math preferences, soft-newline-as-space, Dutch/Russian locales, and the long tail of cut/backspace/table/footnote/search fixes. 32 files conflicted. How the overlapping work was resolved: - Code-block trailing newline (marktext#5114): took upstream's `mu-trailing-break` <br> in CodeBlockContent and dropped our zero-width caret anchor from `getHighlightHtml` (upstream removed its `handleLineEnding` parameter). Same coverage — math/HTML previews render through CodeBlockContent — and `<br>` contributes no text, so it is offset-neutral against our `getTextContent`/OFFSET_BLACKLIST mapping. Deleted our now-obsolete `highlightHTMLCaretAnchor.spec.ts` and `trailingNewlineCaret.spec.ts`; upstream's `trailingNewline.spec.ts` already asserts the trailing break adds no text, and its Chromium e2e covers caret painting and typing. - Soft line breaks: kept OUR rendering (no block-level `mu-line-end`, a `mu-caret-anchor` instead) because literal paragraph line breaks make the trailing `\n` lay out its own line already (marktext#5282), and added upstream's `softNewlineAsSpace` branch on top. Repointed upstream's `softLineBreak.spec.ts` case that asserted `MU_LINE_END` at the anchor. - Table column sizing (marktext#4894): both sides made the same `min-width` change. Took upstream's blanket `z-index: 2` on `.mu-table-cell-content`, which is a superset of our selected-cell-only rule and still satisfies our text-above-overlay stacking assertion. Kept our `.mu-table` overflow-x. - Word counter: kept our localized badge label (mapped `all` to upstream's `menu.counter.allCharacters`, dropping our duplicate `charactersWithSpaces` key so every locale stays identical to upstream's) and took upstream's document/selection tooltip. Locales resolve to upstream's wholesale; `.min.json` are gitignored build output. - Forward-delete merge (marktext#1845): took upstream's `getAnchor()` form, which carries our list-item-only narrowing plus the marktext#5423/marktext#5386 cases. - `format.ts` markerLen: upstream's `token.marker.length` subsumes our `mark` special case (`==` is two wide) and covers the new math markers. - Options rename: `math` -> `texMathDollars`, `isGitlabCompatibilityEnabled` -> `texMathGfm`; updated our blank-line specs to the new option set. - `resolveLocalLinkTarget` (marktext#5506) now decodes link targets, so our manual `decodeURIComponent` went away; kept `isEditableFile` so links to plain text documents still open. - Source mode: one `sourceMode()` helper feeds all three `setOption('mode')` call sites, so a plain-text document keeps `text/plain` even when a TeX math preference changes, while markdown documents get upstream's preference-driven `markdown-math` mode. - E2E: kept our unobtrusive launcher (`visibleWindow`, MARKTEXT_E2E_UNOBTRUSIVE) alongside upstream's new `env` option; TOC spec takes upstream's viewport alignment assertions plus our `showToc` helper and highlight test. - `test:e2e` points at the package root again: upstream moved `playwright.config.ts` out of `test/e2e/` (d3f6959). Backup of the pre-merge state: branch `backup/pre-upstream-merge-95144c5c`.
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.
Closes #4749. Supersedes #4773 (closed — it patched the pre-#5298 shape of this code and covered only one of the three triggers).
Summary
resolveLocalLinkTarget()joined the document folder with the link target and then randecodeURIComponent()over the joined string, unguarded. That is wrong in two independent ways, and together they made Ctrl/Cmd-clicking a perfectly ordinary local link throwURIError: URI malformedout of themt::format-link-clickhandler.1. The document folder was being decoded. The link target is URL-encoded text taken from the document, but
dirname(window.DIRNAME) is a raw filesystem path that may legally contain%. Decoding the joined string therefore misreads the document's own folder name:50%off/[a](plain.md)URIError50%off/plain.md50%off/[a](bad%20name.md)URIError50%off/bad name.mdmy%20docs/[a](plain.md)my docs/plain.md(wrong folder)my%20docs/plain.mdThis is the mirror image of the renderer-side asymmetry that
encodeDirnameForUrl()already documents (#5212 / #5302). The first commit decodes the target alone and joins the rawdirname.isAbsolutekeeps testing the encoded target, so a%2Fcannot turn a relative link into an absolute one.2. A target that is not valid percent-encoding threw.
%is a legal filename character, so the second commit falls back to the target as written instead of rejecting the click — the same fallback the renderer already applies to anchors inSCROLL_TO_ANCHOR:[broken](bad%zz.md)[bare](%.md)[partial](%E0%A4%A.md)[frag](other.md#100%)—toPathname()runs before the#split, so an invalid escape anywhere in the destination was enough to throw. fix: #4749 handle malformed percent-encoded local links #4773 wrapped the whole link instead, which baked#100%into the path so the file never opened. With the guard insidetoPathname()the case resolves correctly: the literal path does not exist, the#split runs, andother.mdopens with100%handed to the renderer, which tolerates an undecodable anchor.Note that since #5298 made this handler
async, the throw surfaced as anunhandledRejection(logged only inmain/index.ts) rather than theuncaughtExceptionerror dialog described in #4749 — so ondevelopthe symptom is currently a click that does nothing at all.Type of change
Test plan
Seven regression cases in
packages/desktop/test/unit/specs/local-link-target.spec.ts, covering all four destinations listed in #4749 plus the three folder-name cases from the table above. Each was watched failing againstdevelopfirst: the folder cases fail withURIErroratfilesystem/index.ts:41(andmy%20docs/fails on the wrong resolved path), the malformed-target cases fail withURIError.Notes for reviewers
Deliberately out of scope, both pre-existing and worth separate issues if wanted:
isMarkdownFile()requires the file to exist, so it falls through toshell.openPath()whose rejected promise is discarded). [Bug] Malformed percent-encoded local link throws an unhandled URIError in the main process #4749 asks for a "cannot open location" prompt; that would change behaviour for every broken local link, not just malformed escapes, so it is not bundled here.FormatLinkPayload.dirnameis optional inmain/menu/actions/file.tswhileipc.tsdeclares it required.Credit to @jianongHe, whose report in #4749 and analysis in #4773 established the right fallback semantics; the reproduction cases listed there are in the spec.
🤖 Generated with Claude Code