81d7cff6cb6665feb772b1339b6298273a3c49be
max
  Thu Aug 6 08:24:38 2026 -0700
hgBlat new results page: table redesign, rename modal, share link, and an XSS fix

Iterates on the new hgBlat "table" results page (rendered by hg/js/hgBlat.js from
JSON emitted by hgBlat.c) per feedback on #37893:

- Columns: "Open in Genome Browser" (position link + new-tab icon), "Show"
(base alignment), Query coverage moved before Locus, score-scaled bar in the
Score column, Identity as plain %, comma-formatted Span. Locus is plain text,
clipped with a CSS ellipsis (full value in title).
- Tooltips on every action link and every column header (via the existing
convertTitleTagsToMouseovers mechanism).
- "Rename BLAT Track": a real modal dialog (replaces the old inline toggle form),
reusing the existing hgc buildBigPsl call via a new window.blatRenameCt() helper;
it no longer depends on a generic page-global. New cfg fields canRename /
trackName / trackDescription drive it.
- "Share a link": trash-backed stable URL toggle, with a share-nodes icon.
- Security: cgiEncode the query name in the htcUserAli detailsUrl (its sibling
already did), and htmlEncode every URL before it goes into an href in hgBlat.js,
so a crafted query/sequence name can't break out of the attribute (XSS).
- Shared htmlEncode() moved into hg/js/utils.js for reuse instead of a per-file
escaper.
- hg.conf: blatNewPageBanner (invite banner, default off), blatOldTracks
(keep/hide/delete previous BLAT tracks at creation) documented in ex.hg.conf;
hgc.c tags BLAT tracks with blatResult=on and clears prior ones per blatOldTracks.

refs #37893

diff --git src/hg/hgc/hgc.c src/hg/hgc/hgc.c
index e8ae42ba93d..1f822180985 100644
--- src/hg/hgc/hgc.c
+++ src/hg/hgc/hgc.c
@@ -9073,94 +9073,110 @@
                 (ident >= 90) ? "#b45309" : "#b1301f";
 
 /* Offer "Share a link" only when a durable bigPsl custom track backs these results; without it there
  * is nothing for a shared session to rebuild the alignment from. */
 char *shareBb = blatFindPinnedBigPsl(cart);
 boolean canShare = (shareBb != NULL);
 freeMem(shareBb);
 
 /* Colors imported from the BLAT Redesign (slide 3): grey page, steel-blue section-header bars, navy
  * links with maroon hover, slate text.  The <h2>/<hr> the shared alignment code emits are hidden; its
  * <h4> section headings become the steel-blue bars.  The page chrome is webStartGbNoBanner (a menubar
  * and <main>, no legacy section tables), so we draw our own gold title bar in plain HTML. */
 printf("<style>"
        "#main-menu-whole{margin-bottom:0}"   /* no gap between the menubar and the title bar */
        "#mainContent{background:#eef1f4}"     /* grey page behind the white alignment panel */
-       ".blatTitleBar{background:#e9cf9a; color:#0a2b6b; box-sizing:border-box; display:flex;"
+       ".blatTitleBar{background:#eaca92; color:#000; box-sizing:border-box; display:flex;"
        " align-items:center; justify-content:space-between; padding:8px 16px}"  /* gold title band */
        ".blatTitleBar .blatTtl{font-size:18px; font-weight:700}"
        ".blatTitleBar .blatBtns{display:flex; gap:8px; align-items:center}"
        ".blatBtn{padding:4px 12px; font-size:13px; border:1px solid #999; border-radius:3px;"
        " background:#e6e6e6; text-decoration:none; white-space:nowrap; cursor:pointer}"
        /* nice_menu.css sets a:link blue (specificity 0,1,1); a.blatBtn:link (0,2,1) beats it */
        "a.blatBtn:link, a.blatBtn:visited, a.blatBtn:hover{color:#000; text-decoration:none}"
        ".blatBtn:hover{background:#d8d8d8}"
        "#blatAlnBody{font-family:'Helvetica Neue',Helvetica,Arial,sans-serif; color:#374a5e;"
        " display:grid; grid-template-columns:220px 1fr;"      /* full-height sidebar + content column */
        " background:#fff}"                                    /* edge to edge: no margin, no border */
        "#blatAlnBody a{color:#0a3a7a}"
        "#blatAlnBody a:hover{color:#8b1a1a}"
        "#blatAlnNav{grid-column:1; grid-row:1; background:#f4f7fb; border-right:1px solid #dde3ea}"
        /* keep the grey column full height, but pin the links so they stay visible while scrolling */
        "#blatAlnNavInner{position:sticky; top:0; padding:18px 20px; display:flex; flex-direction:column;"
        " gap:16px}"
        "#blatAlnNav a{font-weight:700; text-decoration:none}"   /* already obviously links; no underline */
        "#blatAlnBlocks{display:flex; flex-direction:column; gap:8px; margin:2px 0 0 14px}"  /* block links, indented under side-by-side */
        "#blatAlnBlocks a{font-weight:400; font-size:13px}"
        "#blatAlnContent{grid-column:2; grid-row:1; min-width:0; padding:0 20px 14px}"
        "#blatAlnContent h2{display:none}"
        "#blatAlnContent hr{display:none}"
-       "#blatAlnContent h4{margin:16px -20px 0; padding:8px 20px; background:#4c7093; color:#fff;"
+       "#blatAlnContent h4{margin:16px -20px 0; padding:8px 20px; background:#4c759c; color:#fff;"
        " font-size:15px; font-weight:700}"                     /* -20px: bar spans full content width */
        "#blatAlnContent h4:first-child{margin-top:0}"          /* Alignment Summary flush at top */
        "#blatAlnContent h4 a{color:#fff}"
        /* undo bootstrap.css (pulled in by webStartGbNoBanner's gbHeader) on the sequence blocks:
         * it would give <pre> a grey box, a border and word-break that mangles the alignment */
        "#blatAlnContent pre{margin:0; padding:2px 0 12px; line-height:1.4; background:none; border:0;"
        " border-radius:0; color:#374a5e; white-space:pre; word-break:normal; word-wrap:normal}"
+       /* key-value summary strip, mirroring hgBlat's .blatStrip (label over value, thin dividers) */
+       ".blatAlnStrip{display:flex; align-items:center; gap:24px; flex-wrap:wrap; margin:2px 0 14px}"
+       ".blatAlnStat{display:flex; flex-direction:column; gap:1px}"
+       ".blatAlnStat .k{font-size:12px; color:#5b6572; font-weight:700}"
+       ".blatAlnStat .v{font-size:14px; color:#1e2833; font-weight:700}"
+       ".blatAlnStrip .d{width:1px; height:28px; background:#d0d0d0}"
        "</style>\n");
 
 /* gold title bar, drawn directly (no framework subheadingBar, no JS): title on the left, then a
  * "Share a link" button (when a durable track backs the results) and a "Back to results" button. */
 printf("<div class='blatTitleBar'>");
 printf("<span class='blatTtl'>BLAT Base Alignment: %s</span>", blatAsmLabel(database));
 printf("<span class='blatBtns'>");
 printf("<a href='hgBlat?blatReopen=1&hgsid=%s' class='blatBtn'>"
        "\xe2\x80\xb9 Back to results</a>", cartSessionId(cart));
 if (canShare)
     printf("<a href='#' id='blatShareBtn' class='blatBtn'>Share a link</a>");
 printf("</span></div>\n");
 
 /* one white panel laid out as two grid columns: a full-height "jump to" sidebar on the left, and on
  * the right an "Alignment Summary" header, the summary line, and the base-by-base alignment inlined
  * so the whole page scrolls */
 printf("<div id='blatAlnBody'>\n");
 
 printf("<div id='blatAlnContent'>\n");
 printf("<h4>Alignment Summary</h4>\n");
 /* comma-format the coordinates and base counts, matching the new Table view (readable at the
  * hundreds-of-millions scale of genomic coordinates, and the convention elsewhere in the browser) */
 char tStartC[32], tEndC[32], matchC[32], qSizeC[32];
 sprintLongWithCommas(tStartC, psl->tStart + 1);
 sprintLongWithCommas(tEndC, psl->tEnd);
 sprintLongWithCommas(matchC, psl->match + psl->repMatch);
 sprintLongWithCommas(qSizeC, psl->qSize);
-printf("<p><b>%s</b> aligned to <b>%s:%s-%s</b>, "
-       "<b style='color:%s'>%.1f%% identity</b>, "
-       "%s of %s bases matched, strand <b>%s</b>.</p>\n",
-       qName, chrom, tStartC, tEndC, idColor, ident,
-       matchC, qSizeC, psl->strand);
+/* key-value strip (Query / Position / Identity / Matches / Strand), styled like hgBlat's summary
+ * strip so the two pages read as one design. */
+printf("<div class='blatAlnStrip'>"
+       "<div class='blatAlnStat'><span class='k'>Query</span><span class='v'>%s</span></div>"
+       "<div class='d'></div>"
+       "<div class='blatAlnStat'><span class='k'>Position</span><span class='v'>%s:%s-%s</span></div>"
+       "<div class='d'></div>"
+       "<div class='blatAlnStat'><span class='k'>Identity</span>"
+       "<span class='v' style='color:%s'>%.1f%%</span></div>"
+       "<div class='d'></div>"
+       "<div class='blatAlnStat'><span class='k'>Matches</span><span class='v'>%s of %s</span></div>"
+       "<div class='d'></div>"
+       "<div class='blatAlnStat'><span class='k'>Strand</span><span class='v'>%s</span></div>"
+       "</div>\n",
+       qName, chrom, tStartC, tEndC, idColor, ident, matchC, qSizeC, psl->strand);
 if (isNotEmpty(aliasStr))
     printf("<p>Genome sequence %s is also known as: %s.</p>\n", chrom, aliasStr);
 /* The shared library returns the number of alignment blocks it actually shows.  The DNA path merges
  * blocks separated by gaps <= 8 bases, so this can be fewer than psl->blockCount; use it (not
  * psl->blockCount) so the sidebar's "Block N" links match the #1..#N anchors that were emitted. */
 int blockCount;
 if (qType == gftRna || qType == gftDna)
     blockCount = showPartialDnaAlignment(psl, oSeq, stdout, cdsS, cdsE, FALSE);
 else
     blockCount = showGfAlignment(psl, oSeq, stdout, qType, qStart, qEnd, qName);
 printf("</div>\n");   /* #blatAlnContent */
 
 /* Sidebar, emitted after the alignment so blockCount is known; CSS grid puts it back in column 1.
  * The inner div is position:sticky so the links stay in view as the long alignment scrolls. */
 printf("<div id='blatAlnNav'><div id='blatAlnNavInner'>\n");
@@ -27384,40 +27400,71 @@
  * Bring up the bigPsl detail page with all the alignments. */
 {
 char *trackName = cartString(cart, "trackName");
 char *trackDescription = cartString(cart, "trackDescription");
 char *pslName, *faName, *qName;
 parseSs(fileNames, &pslName, &faName, &qName);
 
 struct tempName bigBedTn;
 trashDirDateFile(&bigBedTn, "hgBlat", "bp", ".bb");
 char *bigBedFile = bigBedTn.forCgi;
 makeBigPsl(pslName, faName, database, bigBedFile);
 
 char* host = getenv("HTTP_HOST");
 
 boolean isProt = cgiOptionalString("isProt") != NULL;
-char *customTextTemplate = "track type=bigPsl indelDoubleInsert=on indelQueryInsert=on pslFile=%s visibility=pack showAll=on htmlUrl=http://%s/goldenPath/help/hgUserPsl.html %s bigDataUrl=%s name=\"%s\" description=\"%s\" colorByStrand=\"0,0,0 0,0,150\" mouseOver=\"${oChromStart}-${oChromEnd} of ${oChromSize} bp, strand ${oStrand}\"\n";  
+// blatResult=on tags this as a BLAT results track so previous ones can be found (see blatOldTracks).
+char *customTextTemplate = "track type=bigPsl blatResult=on indelDoubleInsert=on indelQueryInsert=on pslFile=%s visibility=pack showAll=on htmlUrl=http://%s/goldenPath/help/hgUserPsl.html %s bigDataUrl=%s name=\"%s\" description=\"%s\" colorByStrand=\"0,0,0 0,0,150\" mouseOver=\"${oChromStart}-${oChromEnd} of ${oChromSize} bp, strand ${oStrand}\"\n";
 char *extraForMismatch = "indelPolyA=on showDiffBasesAllScales=. baseColorUseSequence=lfExtra baseColorDefault=diffBases";
   
 if (isProt)
     extraForMismatch = "";
 char buffer[4096];
 safef(buffer, sizeof buffer, customTextTemplate, bigBedTn.forCgi, host, extraForMismatch, bigBedTn.forCgi, trackName, trackDescription);
 
 struct customTrack *ctList = getCtList();
 struct customTrack *newCts = customFactoryParse(database, buffer, FALSE, NULL, NULL);
+
+/* Optionally clear PREVIOUS BLAT result tracks (those tagged blatResult=on) so the user is not
+ * confused about which results are current.  hg.conf "blatOldTracks":
+ *   keep   (default) - do nothing, every search's track stays as-is
+ *   hide             - leave earlier BLAT tracks in the session but set them to hide
+ *   delete           - remove earlier BLAT tracks from the session (their trash files age out)
+ * Only BLAT-tagged tracks are touched; the track just made is left alone. */
+char *oldTracks = cfgOptionDefault("blatOldTracks", "keep");
+if (differentString(oldTracks, "keep"))
+    {
+    struct customTrack *ct, *next, *keptList = NULL;
+    for (ct = ctList; ct != NULL; ct = next)
+        {
+        next = ct->next;
+        if (ct->tdb != NULL && sameOk(trackDbSetting(ct->tdb, "blatResult"), "on"))
+            {
+            if (sameString(oldTracks, "hide"))
+                {
+                cartSetString(cart, ct->tdb->track, "hide");
+                slAddHead(&keptList, ct);   /* keep it in the session, just hidden */
+                }
+            /* "delete": drop it from the list so customTracksSaveCart writes it out */
+            }
+        else
+            slAddHead(&keptList, ct);
+        }
+    slReverse(&keptList);
+    ctList = keptList;
+    }
+
 theCtList = customTrackAddToList(ctList, newCts, NULL, FALSE);
 
 customTracksSaveCart(database, cart, theCtList);
 
 /* Pin this bigPsl file in the cart so hgBlat's Table view can reopen exactly these results from a
  * shared session (see doShareReopen in hgBlat.c) - unambiguously, even when the cart holds several
  * BLAT custom tracks from earlier searches. */
 cartSetString(cart, "blatLastBigBed", bigBedFile);
 
 cartSetString(cart, "i", "PrintAllSequences");
 hgCustom(newCts->tdb->track, NULL);
 
 if (sameOk(cartOptionalString(cart, "autoRearr"), "1"))  
     {
     char snakeVar[256];