Comments/bodies: autolink #N to issues and PRN to pull requests (boundary-delimited, narrow) #340
Labels
No labels
actions
bug
cli
duplicate
enhancement
fork
forum
git storage
help wanted
insights
invalid
issues
moderation
oidc
ownership transfer
packages
pr/merge protection rules
projects
pull requests
question
releases
sponsorships
tags
webhooks
wiki
wontfix
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
crueber/walhub#340
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What's requested
In issue comments (and issue/PR bodies), two narrowly-scoped autolinks:
#<integer>→ links to that issue in the same repo (e.g.#3→/{owner}/{repo}/issues/3).PR<integer>/ any capitalization of "PR" (pr3,Pr3,PR3) → links to that pull request (e.g./{owner}/{repo}/pull/3).Both match only when bounded by whitespace or punctuation (start/end of text, space, punctuation) — never inside a word, a URL, or an existing markdown link.
Scope rules (deliberately tight, per the request)
#must be followed by digits only — one or more.#3,#123link;#3.2,#x3,#-3,#3abcdo not (the#3abccase links#3? No — the integer run must terminate at the punctuation/whitespace boundary;#3abcis#followed by3abc, which is not#+ integer, so no link).PRprefix: exactly the two lettersPRin any case (pr,Pr,pR,PR) + digits, bounded as above.PR3links;xPR3,PR3xdo not. NotePR3abcdoes not link (the digits must end at the boundary).#003,pr007) — link as written, resolved numerically; decide and document (simplest: link them, the href is the numeric value).[see #3](x)and`#3`must pass through untouched. Inside fenced code blocks: untouched.https://x/#3must not double-process (the URL autolinker and this rule must not fight — order matters, see below).#3in a markdown heading (### 3) — heading markers are#s adjacent to a space, so###never matches#+digit at a boundary; but a heading like# 3 daysHAS#+ space + 3, which does NOT match (integer must directly follow#). Document the boundary rule in tests.Where it lives (code evidence)
marked(GFM) → DOMPurify → HTML (web/src/lib/render-md.js:1-45,renderBody/renderMarkdownHtml). The autolink belongs as a marked extension or a post-marked/DOMPurify-safe text-node pass insiderenderMarkdownHtml— the headless entry point — so every consumer (comments, issue body, PR body) gets it for free.#-reference pass must ignore content inside<a href>(marked may already have autolinked URLs) and inside<code>/<pre>. Cleanest: run the reference linkifier as a marked extension (tokenizer-level, so code spans/fences are already exempt), or as a DOM text-node walk between marked and DOMPurify that skipsa/code/preancestors. Both are DOMPurify-safe; the extension approach keeps it headless-testable without a DOM — preferred given the module's headless contract.linkifyBody,web/src/lib/diff.js:204) is a separate surface — whether commit bodies get#3autolinks is out of scope for this ticket (flag it as a follow-up decision; the underlying helper should be written so it can be reused there).Link target + rendering
#3→<a href="/{owner}/{repo}/issues/3">#3</a>;PR3→<a href="/{owner}/{repo}/pull/3">PR3</a>— preserve the author's original text exactly (case included:pr3links as "pr3").renderBody(src, {owner, repo, ref, dir})—render-md.js), so building the href needs nothing new.#999when issue 999 doesn't exist — still render the link (GitHub behavior; the 404 is honest and the author intent is clear). No existence check in the renderer (would cost a fetch per reference and the pipeline is synchronous/headless). Note this in the PR.href— inside the DOMPurify allowlist already (http/https/mailto/relative kept, render-md.js:44). No allowlist change.Acceptance criteria
#3(and multi-digit) in a comment renders as a link to/issues/3;pr3/PR3/Pr3render as links to/pull/3— original text and case preserved.x#3,#3x,#3.2,xPR3,PR3xdo NOT link;(#3),#3,,#3., end-of-text, and after whitespace DO.#3/PR3pass through unlinked.#fragments are not double-processed.renderBodyconsumers) — one implementation, not per-page hacks.Fixed by PR #354 (#354): #N/PRN autolinks in renderMarkdownHtml, thread pages pass {owner, repo} mdCtx. Notes: dead refs still link (honest 404, no per-ref fetch); leading zeros link as written with numeric hrefs (#003 → /issues/3); commit bodies (diff.js linkifyBody) flagged as follow-up — linkifyRefText is exported reusable for it.
Review: PR #354 (fix/issue-340) — autolink #N/PRN
Reviewed in scratch worktree at
origin/fix/issue-340(main worktree untouched, still clean). No browser used — node tests + reasoning only, per instructions. No docker/system changes.Match table — all verified adversarially (not just the test file)
Core:
web/src/lib/render-md.js:206(REF_RE = /(?<![A-Za-z0-9_&])(#\d+|[Pp][Rr]\d+)(?![A-Za-z0-9_]|\.[0-9])/g), corelinkifyRefText(:213-221), walkerlinkifyIssueRefs(:228-274), pipeline wiring (:277-279, linkify runs AFTERresolveMarkdownUrls).#3,#123,PR3/pr3/Pr3/pR3,(#3),#3,,#3., EOL, after whitespace/newline — all link with exact hrefs (/o/r/issues/3,/o/r/pull/3), original text/case preserved.x#3,#3x,#3abc,#x3,#-3,# 3,xPR3,PR3x,PR3abc,aPR3b,#3_,#3.2(lookahead\.[0-9]blocks the version run; greedy\d+can't backtrack past it).#003→href/issues/3as written; 30-digit runs stay literal (regex strip, noNumber()→ no1e+30).`#3`/ fences (skipped via<code>/<pre>depth counter),[see #3](x)/[PR3](x)(skipped via<a>depth),https://x.test/a#3(single anchor, fragment intact — marked-autolinked URL sits inside<a>).# 3 daysand### 3never match (no#survives into the HTML — verified<h1>3 days</h1>/<h3>3</h3>);##3links the inner#3(pinned in test, correct — second#is a punctuation boundary).title="a>b"doesn't break parsing); attribute values containing#N(title="issue #3") pass through untouched;<!-- #3 -->untouched (whole comment consumed as one tag); uppercase<A HREF>skipped (case-insensitiveSKIP_RE); nested<pre><code>skipped; unclosed non-skip tags still link (correct);&excluded from leading boundary so'/3entities never corrupt (verifiedit's #3→ entity intact + ref links)./{o}/{r}/issues|pull/Nhrefs are minted after the resolver runs, and the pass only ADDS anchors — verifiedfilectx test asserts no/blob/main/o/r/issuesmangling. Resolver matrix untouched.[#PRpr0-9];escAttr(:208-210) escapes&<>"in owner/repo-derived hrefs (verified with quote-in-owner probe). Sanitizer config (PURIFY_CONFIG:33-44) byte-identical — zero changes, pinned by test.Consumers — one finding, fixed
Audited all 7
renderBodycall sites: ThreadTimeline (:83 viaprops.mdCtx— Issue.jsx:423 + Pull.jsx:655 pass{owner, repo}✓), Blob/Tree/Release (full file ctx — refs link in prose, documented intent ✓), Repos bios ×2 (no ctx → stay plain; out of scope, safe degradation, OK).renderBody(getBody())without ctx → preview stayed plain until posted. Fix pushed toorigin/fix/issue-340(commit7530f4f): preview passes{ owner: ctx.owner, repo: ctx.name }(same shape as Issue.jsx; no ref/dir so relative URLs stay verbatim), wiring pin added to the test, doc clause updated per law 12.Laws / docs / hygiene
package.json/lock changes — no new deps. Law 7: N/A (synchronous pure function, no goroutines/channels). Law 8: no seam changes, no upward imports. Law 12:docs/go/12_web_ui.mddecision accurate (verified each claim against code); #328 entry intact at line 674 (diff is append-only there).Test results
node --test web/test/unit/refs-autolink.test.js: 21/21 pass (incl. new IssueNew pin).node --test web/test/unit/*.test.js: 685/688 — the 3 failures are allsmoke.test.jslive-server probes (WALHUB_TEST_WEB_BASE_URL→ 127.0.0.1:8080, 503 with no server running); environmental, unrelated to this diff, same as PR description claims.vite build+esbuildSDK bundle: green (chunk-size warning is advisory). Note: build deletes trackedweb/dist/.keep— restored before commit, not part of the push.MERGE RECOMMENDATION: ready to merge (after CI passes)
Fixed by PR #354 (review clean + one IssueNew preview-ctx fix by reviewer; adversarial match-table fully verified; 685/688 with only env smoke failures), merged. Closing.