c0e8fa6df3a0bd406c4188d49ee00f20aef203e5
max
  Mon Sep 7 12:07:18 2026 -0700
Substitute trackDb variables in hub track description pages

A hub's description page comes straight off the hub's web server and has
never been through variable substitution, so a $db or $parentTrack in it
reached the reader as literal text. Native trackDb pages are fine, since
hgTrackDb substitutes them when it loads trackDb, but there was no
equivalent step for a hub.

hgc's getTrackHtml and hgTrackUi's trackUi both call hVarSubstTrackDbHtml
on a hub track's html.  Only a short list of variables is recognized there and nothing is an
error, because a hub page written before this existed can easily contain
a dollar sign inside a shell example, and silently rewriting that would
be worse than not substituting at all.

Adds $parentTrack, the name of the container a track sits in, which is
what a subtrack description page needs to link back to its superTrack or
composite. Views are skipped, since a view has no page of its own, and
the hub_<id>_ prefix is kept so the name works as hgTrackUi's g=
parameter. Documents $track, $parentTrack and $hgsid in trackDb/README.

refs #37599

diff --git src/hg/lib/hVarSubst.c src/hg/lib/hVarSubst.c
index 07a96decb4c..d471025f201 100644
--- src/hg/lib/hVarSubst.c
+++ src/hg/lib/hVarSubst.c
@@ -1,343 +1,442 @@
 /* Handle variable substitutions in strings from trackDb and other
  * labels. See trackDb/README for descriptions of values that can be
  * substitute.  This code needs to do a special handle to remain compatibility
  * with behavior of the old substitution mechanism. */
 
 /* Copyright (C) 2014 The Regents of the University of California 
  * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */
 #include "common.h"
 #include "trackDb.h"
 #include "hdb.h"
 #include "hui.h"
 #include "sqlNum.h"
+#include "hubConnect.h"
 #include "hVarSubst.h"
 
 static boolean isVarEnd(boolean inBraces, char c)
 /** does the character end a variable reference. */
 {
 if (inBraces)
     return (c == '}');
 else
     return !((c == '_') || isalnum(c));
 }
 
 static char *parseVarName(char *desc, char *varStart, char *varName, int varNameSize)
 /* parse substitution variable name out of a string, returning next position
  * after name. */
 {
 char *p = varStart+1;
 if (*p == '\0')
     errAbort("trailing `$' while doing variable substitution in %s", desc);
 boolean inBraces = (*p == '{');
 if (inBraces)
     p++;
 int nameIdx = 0;
 while ((nameIdx < varNameSize-1) && !isVarEnd(inBraces, *p))
     varName[nameIdx++] = *p++;
 if (nameIdx == 0)
     errAbort("empty variable name in %s varStart='%s' varName='%s'", desc, varStart, varName);
 if (nameIdx == varNameSize)
     errAbort("variable name in desc %s exceeds maximum length of %d, starting with: \"%.*s\"",
              desc, varNameSize-1, varNameSize-1, varName);
 varName[nameIdx] = '\0';
 if (inBraces)
     p++;
 return p;
 }
 
+static char *parseVarNameMaybe(char *varStart, char *varName, int varNameSize)
+/* Like parseVarName, but return NULL instead of aborting when what follows the `$' is not
+ * a well formed variable reference.  Used in hubHtml mode, where a stray dollar sign in a
+ * description page has to survive untouched. */
+{
+char *p = varStart+1;
+boolean inBraces = (*p == '{');
+if (inBraces)
+    p++;
+int nameIdx = 0;
+while ((nameIdx < varNameSize-1) && (*p != '\0') && !isVarEnd(inBraces, *p))
+    varName[nameIdx++] = *p++;
+if ((nameIdx == 0) || (nameIdx == varNameSize-1))
+    return NULL;
+varName[nameIdx] = '\0';
+if (inBraces)
+    {
+    if (*p != '}')
+        return NULL;
+    p++;
+    }
+return p;
+}
+
 static char *lookupTrackDbSubVar(char *desc, struct trackDb *tdb, char *settingName, char *varName)
 /* get the specified track setting to substitute or die trying; more useful
  * message than trackDbRequiredSetting when doing substitution */
 {
 char *val = trackDbSettingClosestToHome(tdb, settingName);
 if (val == NULL)
    errAbort("trackDb (%s) setting \"%s\" not found for variable substitution of \"$%s\" in %s",
             tdb->track, settingName, varName, desc);
 return val;
 }
 
 static char *lookupOtherDb(char *desc, struct trackDb *tdb, char *varName)
 /* look up the otherDb variable, which is needed for substituting varName */
 {
 return lookupTrackDbSubVar(desc, tdb, "otherDb", varName);
 }
 
 static void insertLinearGapHtml(struct trackDb *tdb, char *linearGap,
                              struct dyString *dest)
 /* Generate HTML table from chain linearGap variable */
 {
 if (sameWord("medium",linearGap))
     {
 dyStringPrintf(dest, "<PRE>-linearGap=%s\n\n\
 tableSize    11\n\
 smallSize   111\n\
 position  1   2   3   11  111  2111  12111  32111   72111  152111  252111\n\
 qGap    350 425 450  600  900  2900  22900  57900  117900  217900  317900\n\
 tGap    350 425 450  600  900  2900  22900  57900  117900  217900  317900\n\
 bothGap 750 825 850 1000 1300  3300  23300  58300  118300  218300  318300\n\
 </PRE>", linearGap);
     }
 else if (sameWord("loose", linearGap))
     {
 dyStringPrintf(dest, "<PRE>-linearGap=%s\n\n\
 tablesize    11\n\
 smallSize   111\n\
 position  1   2   3   11  111  2111  12111  32111  72111  152111  252111\n\
 qGap    325 360 400  450  600  1100   3600   7600  15600   31600   56600\n\
 tGap    325 360 400  450  600  1100   3600   7600  15600   31600   56600\n\
 bothGap 625 660 700  750  900  1400   4000   8000  16000   32000   57000\n\
 </PRE>", linearGap);
     }
 else
     errAbort("Invalid chainLinearGap specified '%s', can only be 'medium' or 'loose'", linearGap);
 }
 
 static void insertMatrixHtml(struct trackDb *tdb, char *matrix,
                              char *matrixHeader, struct dyString *dest)
 /* Generate HTML table from matrix setting in trackDb. matrixHeader is
  * optional. */
 {
 char *words[100];
 char *headerWords[10];
 int size;
 int i, j, k;
 int wordCount = 0, headerCount = 0;
 
 wordCount = chopString(cloneString(matrix), ", \t", words, ArraySize(words));
 if (matrixHeader != NULL)
     headerCount = chopString(cloneString(matrixHeader),
                     ", \t", headerWords, ArraySize(headerWords));
 errno = 0;
 size = sqrt(sqlDouble(words[0]));
 if (errno)
     errAbort("Invalid matrix size in for track %s: %s\n", tdb->track,
              words[0]);
 dyStringAppend(dest, "The following matrix was used:<P>\n");
 k = 1;
 dyStringAppend(dest, "<BLOCKQUOTE><TABLE class='chainTbl'>\n");
 if (matrixHeader)
     {
     dyStringAppend(dest, "<TR ALIGN=right><TD>&nbsp;</TD>");
     for (i = 0; i < size && i < headerCount; i++)
         dyStringPrintf(dest, "<TD><B>%s</B></TD>", headerWords[i]);
     dyStringAppend(dest, "</TR>\n");
     }
 for (i = 0; i < size; i++)
     {
     dyStringAppend(dest, "<TR ALIGN=right>");
     if (matrixHeader)
         dyStringPrintf(dest, "<TD><B>%s<B></TD>", headerWords[i]);
     for (j = 0; j < size && k < wordCount ; j++)
         dyStringPrintf(dest, "<TD>%s</TD>", words[k++]);
     dyStringAppend(dest, "</TR>\n");
     }
 dyStringAppend(dest, "</TABLE></BLOCKQUOTE></P>\n");
 }
 
 static void substLinearGap(struct trackDb *tdb, struct dyString *dest)
 /* Generate HTML table from matrix setting in trackDb.  Note: for
  * compatibility, substitutes and empty string if matrix setting not found in
  * trackDb. */
 {
 char *linearGap = trackDbSettingClosestToHome(tdb, "chainLinearGap");
 if (linearGap != NULL)
     insertLinearGapHtml(tdb, linearGap, dest);
 }
 
 static void substMatrixHtml(struct trackDb *tdb, struct dyString *dest)
 /* Generate HTML table from matrix setting in trackDb.  Note: for
  * compatibility, substitutes and empty string if matrix setting not found in
  * trackDb. */
 {
 char *matrix = trackDbSettingClosestToHome(tdb, "matrix");
 if (matrix != NULL)
     insertMatrixHtml(tdb, matrix, trackDbSettingClosestToHome(tdb, "matrixHeader"), dest);
 }
 
 static boolean isAbbrevScientificName(char *name)
 /* Return true if name looks like an abbreviated scientific name
 * (e.g. D. yakuba). */
 {
 return (name != NULL && strlen(name) > 4 &&
 	isalpha(name[0]) &&
 	name[1] == '.' && name[2] == ' ' &&
 	isalpha(name[3]));
 }
 
 static boolean isDatabaseVar(char *varBase)
 /* Is this a variable that can be resolved only from the database name?
  * Specify the base name, excluding the o_ prefix. */
 {
 return (strcasecmp(varBase, "organism") == 0)
     || (strcasecmp(varBase, "date") == 0)
     || (strcasecmp(varBase, "linkToGatewayPage") == 0)
     || (strcasecmp(varBase, "db") == 0)
     || (strcasecmp(varBase, "hgsid") == 0);
 }
 
 static char *valOrDb(char *val, char *database)
 /* return val if not-null, or a clone of database if it is null */
 {
 if (val == NULL)
     val = cloneString(database);
 return val;
 }
 
 static void substDatabaseVar(char *database, struct cart *cart, char *varBase,
                              struct dyString *dest)
 /* substitute a variable resolved from the database name.
  * Specify the base name, excluding the o_ prefix. If database
  * can be looked up, just substitute the database name. */
 {
 if (sameString(varBase, "Organism"))
     {
     char *org = valOrDb(hOrganism(database), database);
     dyStringAppend(dest, org);
     freeMem(org);
     }
 else if (sameString(varBase, "ORGANISM"))
     {
     char *org = hOrganism(database);
     if (org != NULL)
         touppers(org);
     else
         org = valOrDb(org, database);
     dyStringAppend(dest, org);
     freeMem(org);
     }
 else if (sameString(varBase, "organism"))
     {
     char *org = hOrganism(database);
     if ((org != NULL) && !isAbbrevScientificName(org))
             tolowers(org);
     else
             org = valOrDb(org, database);
     dyStringAppend(dest, org);
     freeMem(org);
     }
 else if (sameString(varBase, "date"))
     {
     char *date = valOrDb(hFreezeDateOpt(database), database);
     dyStringAppend(dest, date);
     freeMem(date);
     }
 else if (sameString(varBase, "db"))
     dyStringAppend(dest, database);
 else if (sameString(varBase, "hgsid") && cart != NULL)
     dyStringAppend(dest, cartSessionId(cart));
 }
 
+static char *parentTrackName(struct trackDb *tdb)
+/* Name of the container holding tdb, in the form hgTrackUi's g= parameter needs: with the
+ * hub_<id>_ prefix when this is a hub track, since that is what trackHubAddNamePrefix put
+ * into tdb->track.  Views are skipped, a view has no description page of its own.  Returns
+ * the track's own name when it is not in a container. */
+{
+struct trackDb *parent = tdb->parent;
+char *viewName = NULL;
+while ((parent != NULL) && tdbIsView(parent, &viewName))
+    parent = parent->parent;
+return (parent != NULL) ? parent->track : tdb->track;
+}
+
+/* The variables a hub's description page may use.  Deliberately a short explicit list and
+ * not every trackDb setting the way native trackDb allows: a hub page written before this
+ * substitution existed can easily contain something like "$track" inside a shell example,
+ * and silently rewriting that would be worse than not substituting at all. */
+static char *hubHtmlVars[] = {"db", "hgsid", "track", "parentTrack",
+                              "organism", "Organism", "ORGANISM", "date", "downloadsServer"};
+
+static boolean isHubHtmlVar(struct cart *cart, struct trackDb *tdb, char *varName)
+/* Is varName one of the variables a hub description page may use, and can this call
+ * resolve it?  Asked only in hubHtml mode, to tell a variable reference from a dollar sign
+ * that happens to be followed by a word. */
+{
+if (tdb == NULL)
+    return FALSE;
+if (sameString(varName, "hgsid") && (cart == NULL))
+    return FALSE;
+int i;
+for (i = 0;  i < ArraySize(hubHtmlVars);  i++)
+    if (sameString(varName, hubHtmlVars[i]))
+        return TRUE;
+return FALSE;
+}
+
 static void substTrackDbVar(char *desc, struct trackDb *tdb, char *database,
                             char *varName, struct dyString *dest)
 /* substitute a variable value obtained from trackDb */
 {
 if (sameString(varName, "matrix"))
     substMatrixHtml(tdb, dest);
 else if (sameString(varName, "chainLinearGap"))
     substLinearGap(tdb, dest);
 else if (sameString(varName, "downloadsServer"))
     dyStringAppend(dest, hDownloadsServer());
 else if (sameString(varName, "track"))
     dyStringAppend(dest, tdb->track);
+else if (sameString(varName, "parentTrack"))
+    dyStringAppend(dest, parentTrackName(tdb));
 else
     dyStringAppend(dest, lookupTrackDbSubVar(desc, tdb, varName, varName));
 }
 
 static void substVar(char *desc, struct cart *cart, struct trackDb *tdb, char *database,
                      char *varName, struct dyString *dest)
 /* look up varName and insert value in output string.  Error if variable
  * can't be found */
 {
 if (isDatabaseVar(varName))
     substDatabaseVar(database, cart, varName, dest);
 else if (tdb == NULL)
     errAbort("invalid variable \"%s\" to substitute in %s",
              varName, desc);
 else if (startsWith("o_", varName) && isDatabaseVar(varName+2))
     substDatabaseVar(lookupOtherDb(desc, tdb, varName), cart, varName+2, dest);
 else
     substTrackDbVar(desc, tdb, database, varName, dest);
 }
 
 static char *hVarSubstExt(char *desc, struct cart *cart, struct trackDb *tdb, char *database,
-                          char *src)
+                          char *src, boolean hubHtml)
 /* Parse a string and substitute variable references.  Return NULL if
  * no variable references were found.  Error on missing variables (except
  * $matrix).  desc is a brief description to print on an error to help with
  * debugging. tdb maybe NULL to only do substitutions based on database
- * and organism.  cart may be NULL. See trackDb/README for more information.*/
+ * and organism.  cart may be NULL. See trackDb/README for more information.
+ * In hubHtml mode nothing is an error and only the variables in hubHtmlVars are
+ * recognized: every other `$' is copied through unchanged. */
 {
 struct dyString *dest = NULL;
 char *start = src;  // start of current static string in src
 char *next = src;   // cursor
 char varName[65];
 
 while ((next = strchr(next, '$')) != NULL)
     {
     if (dest == NULL)
         dest = dyStringNew(strlen(src));
     dyStringAppendN(dest, start, next-start);
     if (*(next+1) == '$')
         {
         // $$ is a literal $
         dyStringAppendC(dest, '$');
         start = next = next + 2;
         }
+    else if (hubHtml)
+        {
+        // variable reference, or just a dollar sign in the text
+        char *after = parseVarNameMaybe(next, varName, sizeof(varName));
+        if ((after != NULL) && isHubHtmlVar(cart, tdb, varName))
+            {
+            substVar(desc, cart, tdb, database, varName, dest);
+            start = next = after;
+            }
+        else
+            {
+            dyStringAppendC(dest, '$');
+            start = next = next + 1;
+            }
+        }
     else
         {
         // variable reference
         start = next = parseVarName(desc, next, varName, sizeof(varName));
         substVar(desc, cart, tdb, database, varName, dest);
         }
     }
 if (dest != NULL)
     {
     dyStringAppend(dest, start);
     return dyStringCannibalize(&dest);
     }
 else
     return NULL; // no substitutions
 }
 
 char *hVarSubst(char *desc, struct trackDb *tdb, char *database, char *src)
 /* Parse a string and substitute variable references.  Return NULL if
  * no variable references were found.  Error on missing variables (except
  * $matrix).  desc is a brief description to print on error to help with
  * debugging. tdb maybe NULL to only do substitutions based on database
  * and organism. See trackDb/README for more information.*/
 {
-return hVarSubstExt(desc, NULL, tdb, database, src);
+return hVarSubstExt(desc, NULL, tdb, database, src, FALSE);
 }
 
 void hVarSubstInVar(char *desc, struct trackDb *tdb, char *database, char **varPtr)
 /* hVarSubst on a dynamically allocated string, replacing string in substitutions
  * occur, freeing the old memory if necessary.  See hVarSubst for details.
  */
 {
-char *dest = hVarSubstExt(desc, NULL, tdb, database, *varPtr);
+char *dest = hVarSubstExt(desc, NULL, tdb, database, *varPtr, FALSE);
 if (dest != NULL)
     {
     freez(varPtr);
     *varPtr = dest;
     }
 }
 
 void hVarSubstTrackDb(struct trackDb *tdb, char *database)
 /* Substitute variables in trackDb shortLabel, longLabel, and html fields. */
 {
 hVarSubstInVar(tdb->track, tdb, database, &tdb->shortLabel);
 hVarSubstInVar(tdb->track, tdb, database, &tdb->longLabel);
 hVarSubstInVar(tdb->track, tdb, database, &tdb->html);
 }
 
 void hVarSubstWithCart(char *desc, struct cart *cart, struct trackDb *tdb, char *database,
                        char **varPtr)
 /* Like hVarSubstInVar, but if cart is non-NULL, $hgsid will be substituted. */
 {
-char *dest = hVarSubstExt(desc, cart, tdb, database, *varPtr);
+char *dest = hVarSubstExt(desc, cart, tdb, database, *varPtr, FALSE);
 if (dest != NULL)
     {
     freez(varPtr);
     *varPtr = dest;
     }
 }
+
+void hVarSubstTrackDbHtml(struct cart *cart, struct trackDb *tdb, char *database)
+/* Substitute variables in the description page of a hub track.  Native trackDb needs no
+ * such call: hgTrackDb already substituted the html when it loaded trackDb.  A hub's html
+ * comes straight off the hub's web server and has never been through substitution, so it
+ * is done here, at render time, where $db, $hgsid and $parentTrack resolve to the hub_<id>_
+ * names the CGIs actually use.  Only the variables in hubHtmlVars are recognized and
+ * nothing is an error, so a dollar sign in a description page that was not written with
+ * this in mind stays a dollar sign. */
+{
+if ((tdb == NULL) || isEmpty(tdb->html) || !isHubTrack(tdb->track))
+    return;
+char *dest = hVarSubstExt(tdb->track, cart, tdb, database, tdb->html, TRUE);
+if (dest != NULL)
+    {
+    freez(&tdb->html);
+    tdb->html = dest;
+    }
+}