You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Don't splice a link into the middle of a URL - #117
Copy a link from a native app or a clipboard tool and paste it into a GitHub comment box, and the Markdown comes out with a [ planted in the middle of the URL:
It renders as two links, one of them junk. The shape that triggers it — text/plain is the URL, text/html is an anchor labelled with a shortened rendering of that URL — is how a lot of tooling writes a link to the pasteboard (macOS automation scripts, clipboard managers, "copy link" affordances that shorten the visible text).
Cause
convertToMarkdown splices [label](href) over the label at the offset where indexOf finds it in the plain text:
That's right for prose containing a link — example link plus more text — because there the plain text is a flattening of the HTML. When the plain text is the URL and the label is a piece of it, the label is found inside the URL (offset 25 in the example above) and the splice covers only the tail.
Fix
When the label's occurrence sits inside a longer whitespace-delimited token, decide by what that token is:
it is this link's own href — the label is a shortened rendering of the URL, so the link replaces the whole token. This produces the link the clipboard meant.
it is some other URL — leave the paste alone. Splicing inside a URL is never right, and there is no sensible Markdown to emit.
anything else — unchanged: the label's own occurrence is the span, so <a href="…">foo</a>bar pasted alongside foobar still yields [foo](…)bar.
areEqualLinks already handles the trailing-slash and casing differences between an anchor's normalized href and the pasted text, so the comparison reuses it.
Tests
Two added, both failing before this change and passing after (38 passed, 2 failed → 40 passed):
links the whole url when the label is a shortened rendering of it
doesn't splice a link inside a url that is not its own href
The reason will be displayed to describe this comment to others. Learn more.
Review details
Suppressed comments (2)
src/paste-markdown-html.ts:153
The URL check returns before the punctuation-trimmed candidate is examined whenever the raw token starts with a scheme. Consequently, a shortened link followed by normal sentence punctuation (for example, plaintext https://github.com/owner/repo. with an href lacking the period) is declined even though lines 124–125 say this case is recognized. Check every candidate for an href match first, and only then decide that the token is some other URL.
for (const span of [token, withoutWrappingPunctuation(markdown, token)]) {
const text = markdown.slice(span.index, span.index + span.length)
// The label describes this whole URL, so the link replaces the whole URL.
if (areEqualLinks(href, text)) return span
// Splicing inside a URL is never right, so a label inside one that is not this link's own
// href leaves the paste alone rather than corrupting it.
if (looksLikeURL(text)) return null
src/paste-markdown-html.ts:109
This recognizes only hierarchical schemes containing ://, despite the function's contract covering text that starts with a scheme. A label inside mailto:user@example.com, urn:..., or another valid non-hierarchical URL still falls through and is spliced into that URL when the anchor has a different href.
function looksLikeURL(text: string): boolean {
return /^[a-z][a-z\d+.-]*:\/\//i.test(text)
`convertToMarkdown` turns HTML into Markdown by splicing `[label](href)` over
the label at the offset where it finds that label in the plain-text flavor.
That assumes `text/plain` is a flattening of `text/html`, which holds for prose
containing a link but not for how many native apps and clipboard tools copy a
link: `text/plain` is the URL, and `text/html` is an anchor labelled with a
shortened rendering of it.
The label's only occurrence is then inside the URL, so the `[` lands there:
text/plain: https://github.com/owner/repo/blob/main/a.js#L7
text/html: <a href="…">repo/blob/main/a.js#L7</a>
pasted: https://github.com/owner/[repo/blob/main/a.js#L7](https://github.com/owner/repo/blob/main/a.js#L7)
Such a label describes the whole URL, so replace the whole whitespace-delimited
token when it is this link's own href — which yields the link the clipboard
meant, `[repo/blob/main/a.js#L7](https://…#L7)`.
When the token is a URL but *not* this href, leave the paste alone rather than
corrupt it; splicing inside a URL is never right. Every other case keeps the
label's own occurrence, so a label that merely abuts other text
(`<a href="…">foo</a>bar` pasted alongside `foobar`) is unaffected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The equal-length guard and the fallback both returned the label's own
occurrence, so two of four return paths produced the same value and the guard
read as a distinct case. Nesting the URL checks under the length comparison
leaves one such return, and computes the token's text only where it is used.
Also renames `isURL` to `looksLikeURL`. paste-markdown-link.ts already has an
`isURL` that requires the whole string to round-trip through `new URL()`; this
one only tests for a scheme, because the question here is whether splicing into
the text would corrupt a URL rather than whether it is a valid one. The shared
name invited a future consolidation that would change behaviour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`looksLikeURL` tests for a scheme at offset 0, so `(https://example.com/a)` did
not read as a URL: neither branch applied and the label was still spliced inside
it, leaving `(https://github.com/owner/[repo](…))`.
Try the whitespace-delimited token first and then the same token with wrapping
punctuation trimmed off both ends. Order matters: a URL can end in a bracket of
its own, as `…/wiki/Ruby_(programming_language)` does, and trimming first would
strip that bracket, fail the href comparison, and decline a paste that has a
perfectly good link in it. Both orders are covered by tests.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A url that ends in a bracket of its own, wrapped by prose that closes over it
again, matched neither the token as it stood nor the token with every closing
character trimmed: `(…/wiki/Ruby_(programming_language))` lost its own `)` to
the trim, and the paste was declined. The candidates are now each length from
the whole token down to the fully trimmed one, so the url's own bracket can stay.
Every candidate is compared against the href before any of them is taken as
some other url. Declining as soon as the untrimmed token looked like a url meant
a link followed by a full stop, `https://…/a.js#L7.`, was declined too.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@francinelucca done — fixed in e8f8960 and rebased onto main; details are in the thread. CI hasn't run on the new push yet; could you approve the workflows when you get a chance?
A label is matched to the whitespace-delimited token it starts in, but a label
with a space in it, `https://example.com/ docs`, runs on into the next token.
Treating it as a url inside that token replaced only the url and left ` docs`
in twice, or, with a different href, declined a perfectly good link. Such a
label isn't inside a url, so its own occurrence is the span, as before this PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A matching candidate can exclude punctuation belonging to the anchor label itself. For <a href="https://example.com/">(https://example.com/)</a>. with matching plaintext, this returns only the inner URL's span, but linkify emits the full parenthesized label. The result is ([(https://example.com/)](https://example.com/))., duplicating both parentheses. Use the URL span only if it contains the entire label occurrence; otherwise retain the original label span. Add a regression test with punctuation inside the label.
Preserve URL anchor labels surrounded by punctuation
src/paste-markdown-html.ts:162
This rejects valid links whose complete URL-shaped label is surrounded by prose punctuation. For <a href="https://example.com/docs">https://example.com/</a>. with matching plaintext, conversion leaves the text unchanged and loses the /docs destination; without the period, it converts correctly. Parentheses around the anchor cause the same regression. Preserve the label span when it exactly covers a punctuation-trimmed candidate, and add regression tests for trailing and surrounding punctuation.
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
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 #118.
Problem
Copy a link from a native app or a clipboard tool and paste it into a GitHub comment box, and the Markdown comes out with a
[planted in the middle of the URL:It renders as two links, one of them junk. The shape that triggers it —
text/plainis the URL,text/htmlis an anchor labelled with a shortened rendering of that URL — is how a lot of tooling writes a link to the pasteboard (macOS automation scripts, clipboard managers, "copy link" affordances that shorten the visible text).Cause
convertToMarkdownsplices[label](href)over the label at the offset whereindexOffinds it in the plain text:https://github.com/github/paste-markdown/blob/main/src/paste-markdown-html.ts#L83-L90
That's right for prose containing a link —
example link plus more text— because there the plain text is a flattening of the HTML. When the plain text is the URL and the label is a piece of it, the label is found inside the URL (offset 25 in the example above) and the splice covers only the tail.Fix
When the label's occurrence sits inside a longer whitespace-delimited token, decide by what that token is:
<a href="…">foo</a>barpasted alongsidefoobarstill yields[foo](…)bar.areEqualLinksalready handles the trailing-slash and casing differences between an anchor's normalizedhrefand the pasted text, so the comparison reuses it.Tests
Two added, both failing before this change and passing after (
38 passed, 2 failed→40 passed):links the whole url when the label is a shortened rendering of itdoesn't splice a link inside a url that is not its own hrefAll existing tests pass unchanged.