Fossil

Check-in [774fb7712f]
Login

Check-in [774fb7712f]

Many hyperlinks are disabled.
Use anonymous login to enable hyperlinks.

Overview
Comment:Enhance markdown to use the same hyperlink target resolving logic as Fossil wiki. That means that wiki page names and check-in and ticket hashes can now be used as markdown hyperlink URLs. Also enhance markdown hyperlinks so that if the display text is an empty string, the URL is used as the display text.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | trunk
Files: files | file ages | folders
SHA3-256: 774fb7712fb43fe3a59b168454f1816208dc1e0e40cbc2affcfdf9e26634bc71
User & Date: drh 2019-08-09 18:14:35.615
Context
2019-08-09
20:56
Fixes to the new markdown hyperlink logic to handle the case there the target URL is not defined. ... (check-in: 44545eef88 user: drh tags: trunk)
20:15
Contributing PowerShell script Set-ChildProject.ps1. This script wraps the code for converting a project into a child project into an easy to use PowerShell script. The script should be cross platform, but was only tested on Windows 10. ... (Closed-Leaf check-in: 0b70cf6d57 user: ckennedy tags: windows-tools)
18:14
Enhance markdown to use the same hyperlink target resolving logic as Fossil wiki. That means that wiki page names and check-in and ticket hashes can now be used as markdown hyperlink URLs. Also enhance markdown hyperlinks so that if the display text is an empty string, the URL is used as the display text. ... (check-in: 774fb7712f user: drh tags: trunk)
16:40
Two additional places where links to /markup_help are useful. ... (check-in: 5f328d9f90 user: drh tags: trunk)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/markdown.md.
24
25
26
27
28
29
30

31
32
33
34
35
36
37
38
39
40
41
42













43
44
45
46
47
48
49
> 1.  **\[display text\]\(URL\)**
> 2.  **\[display text\]\(URL "Title"\)**
> 3.  **\[display text\]\(URL 'Title'\)**
> 4.  **\<URL\>**
> 5.  **\[display text\]\[label\]**
> 6.  **\[display text\]\[\]**
> 7.  **\[display text\]**


> **URL** may optionally be written **\<URL\>**.  With link formats 5, 6, and 7
> ("reference links"), the URL is supplied elsewhere in the document, as shown
> below.  Link formats 6 and 7 reuse the display text as the label.  Labels are
> case-insensitive.  The title may be split onto the next line with optional
> indenting.

> * **\[label\]:&nbsp;URL**
> * **\[label\]:&nbsp;URL&nbsp;"Title"**
> * **\[label\]:&nbsp;URL&nbsp;'Title'**
> * **\[label\]:&nbsp;URL&nbsp;(Title)**














## Fonts ##

> *   _\*italic\*_
> *   *\_italic\_*
> *   __\*\*bold\*\*__
> *   **\_\_bold\_\_**
> *   ___\*\*\*italic+bold\*\*\*___







>

|
|
|
|
<






>
>
>
>
>
>
>
>
>
>
>
>
>







24
25
26
27
28
29
30
31
32
33
34
35
36

37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
> 1.  **\[display text\]\(URL\)**
> 2.  **\[display text\]\(URL "Title"\)**
> 3.  **\[display text\]\(URL 'Title'\)**
> 4.  **\<URL\>**
> 5.  **\[display text\]\[label\]**
> 6.  **\[display text\]\[\]**
> 7.  **\[display text\]**
> 8.  **\[\]\(URL\)**

> With link formats 5, 6, and 7 ("reference links"), the URL is supplied
> elsewhere in the document, as shown below.  Link formats 6 and 7 reuse
> the display text as the label.  Labels are case-insensitive.  The title
> may be split onto the next line with optional indenting.


> * **\[label\]:&nbsp;URL**
> * **\[label\]:&nbsp;URL&nbsp;"Title"**
> * **\[label\]:&nbsp;URL&nbsp;'Title'**
> * **\[label\]:&nbsp;URL&nbsp;(Title)**

> **URL** may optionally be written **\<URL\>**.
> In addition to ordinary URLs, the **URL** may be:
> <ul>
> <li>  A pathname starting with "/" in which case the Fossil server
>      URL prefix is prepended
> <li>  A wiki page name, or a wiki page name preceded by "wiki:"
> <li> An artifact or ticket hash or hash prefix
> <li>  A date and time stamp: "YYYY-MM-DD HH:MM:SS" or a subset that
>      includes at least the day of the month.</ul>

> In form 8, then the URL becomes the display text.  This is useful for
> hyperlinks that refer to wiki pages and check-in and ticket hashes.

## Fonts ##

> *   _\*italic\*_
> *   *\_italic\_*
> *   __\*\*bold\*\*__
> *   **\_\_bold\_\_**
> *   ___\*\*\*italic+bold\*\*\*___
Changes to src/markdown_html.c.
425
426
427
428
429
430
431
432
433
434
435
436
437
438

439
440
441
442
443
444

445
446
447
448
449
450
451
452
  struct Blob *ob,
  struct Blob *link,
  struct Blob *title,
  struct Blob *content,
  void *opaque
){
  char *zLink = blob_buffer(link);
  BLOB_APPEND_LITERAL(ob, "<a href=\"");
  if( zLink && zLink[0]=='/' && g.zTop ){
    /* For any hyperlink that begins with "/", make it refer to the root
    ** of the Fossil repository */
    blob_append(ob, g.zTop, -1);
  }
  html_quote(ob, blob_buffer(link), blob_size(link));

  if( title && blob_size(title)>0 ){
    BLOB_APPEND_LITERAL(ob, "\" title=\"");
    html_quote(ob, blob_buffer(title), blob_size(title));
  }
  BLOB_APPEND_LITERAL(ob, "\">");
  BLOB_APPEND_BLOB(ob, content);

  BLOB_APPEND_LITERAL(ob, "</a>");
  return 1;
}

static int html_triple_emphasis(
  struct Blob *ob,
  struct Blob *text,
  char c,







|
|
<
<
<
|
<
>
|
|
<
|
<
|
>
|







425
426
427
428
429
430
431
432
433



434

435
436
437

438

439
440
441
442
443
444
445
446
447
448
  struct Blob *ob,
  struct Blob *link,
  struct Blob *title,
  struct Blob *content,
  void *opaque
){
  char *zLink = blob_buffer(link);
  char *zTitle = title!=0 && blob_size(title)>0 ? blob_str(title) : 0;
  char zClose[20];



  

  wiki_resolve_hyperlink(ob, 0, zLink, zClose, sizeof(zClose), 0, zTitle);
  if( blob_size(content)==0 ){
    BLOB_APPEND_BLOB(ob, link);

  }else{

    BLOB_APPEND_BLOB(ob, content);
  }
  blob_append(ob, zClose, -1);
  return 1;
}

static int html_triple_emphasis(
  struct Blob *ob,
  struct Blob *text,
  char c,
Changes to src/style.c.
140
141
142
143
144
145
146


147



148
149
150
151



152
153

154
155
156
157
158
159
160
char *xhref(const char *zExtra, const char *zFormat, ...){
  char *zUrl;
  va_list ap;
  va_start(ap, zFormat);
  zUrl = vmprintf(zFormat, ap);
  va_end(ap);
  if( g.perm.Hyperlink && !g.javascriptHyperlink ){


    char *zHUrl = mprintf("<a %s href=\"%h\">", zExtra, zUrl);



    fossil_free(zUrl);
    return zHUrl;
  }
  needHrefJs = 1;



  return mprintf("<a %s data-href='%z' href='%R/honeypot'>",
                  zExtra, zUrl);

}
char *chref(const char *zExtra, const char *zFormat, ...){
  char *zUrl;
  va_list ap;
  va_start(ap, zFormat);
  zUrl = vmprintf(zFormat, ap);
  va_end(ap);







>
>
|
>
>
>




>
>
>
|
|
>







140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
char *xhref(const char *zExtra, const char *zFormat, ...){
  char *zUrl;
  va_list ap;
  va_start(ap, zFormat);
  zUrl = vmprintf(zFormat, ap);
  va_end(ap);
  if( g.perm.Hyperlink && !g.javascriptHyperlink ){
    char *zHUrl;
    if( zExtra ){
      zHUrl = mprintf("<a %s href=\"%h\">", zExtra, zUrl);
    }else{
      zHUrl = mprintf("<a href=\"%h\">", zUrl);
    }
    fossil_free(zUrl);
    return zHUrl;
  }
  needHrefJs = 1;
  if( zExtra==0 ){
    return mprintf("<a data-href='%z' href='%R/honeypot'>", zUrl);
  }else{
    return mprintf("<a %s data-href='%z' href='%R/honeypot'>",
                   zExtra, zUrl);
  }
}
char *chref(const char *zExtra, const char *zFormat, ...){
  char *zUrl;
  va_list ap;
  va_start(ap, zFormat);
  zUrl = vmprintf(zFormat, ap);
  va_end(ap);
Changes to src/wikiformat.c.
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
}

/*
** Return a pointer to the name part of zTarget (skipping the "wiki:" prefix
** if there is one) if zTarget is a valid wiki page name.  Return NULL if
** zTarget names a page that does not exist.
*/
static const char *validWikiPageName(Renderer *p, const char *zTarget){
  if( strncmp(zTarget, "wiki:", 5)==0
      && wiki_name_is_wellformed((const unsigned char*)zTarget) ){
    return zTarget+5;
  }
  if( strcmp(zTarget, "Sandbox")==0 ) return zTarget;
  if( wiki_name_is_wellformed((const unsigned char *)zTarget)
   && ((p->state & WIKI_NOBADLINKS)==0 ||
        db_exists("SELECT 1 FROM tag WHERE tagname GLOB 'wiki-%q'"
                  " AND (SELECT value FROM tagxref WHERE tagid=tag.tagid"
                  " ORDER BY mtime DESC LIMIT 1) > 0", zTarget))
  ){
    return zTarget;
  }
  return 0;







|






|







1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
}

/*
** Return a pointer to the name part of zTarget (skipping the "wiki:" prefix
** if there is one) if zTarget is a valid wiki page name.  Return NULL if
** zTarget names a page that does not exist.
*/
static const char *validWikiPageName(int mFlags, const char *zTarget){
  if( strncmp(zTarget, "wiki:", 5)==0
      && wiki_name_is_wellformed((const unsigned char*)zTarget) ){
    return zTarget+5;
  }
  if( strcmp(zTarget, "Sandbox")==0 ) return zTarget;
  if( wiki_name_is_wellformed((const unsigned char *)zTarget)
   && ((mFlags & WIKI_NOBADLINKS)==0 ||
        db_exists("SELECT 1 FROM tag WHERE tagname GLOB 'wiki-%q'"
                  " AND (SELECT value FROM tagxref WHERE tagid=tag.tagid"
                  " ORDER BY mtime DESC LIMIT 1) > 0", zTarget))
  ){
    return zTarget;
  }
  return 0;
1222
1223
1224
1225
1226
1227
1228
1229

1230
1231
1232
1233
1234

1235
1236
1237


1238




1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311

1312
1313
1314
1315
1316
1317
1318
**
**    [0123456789abcdef]
**
**    [#fragment]
**
**    [2010-02-27 07:13]
*/
static void openHyperlink(

  Renderer *p,            /* Rendering context */
  const char *zTarget,    /* Hyperlink target; text within [...] */
  char *zClose,           /* Write hyperlink closing text here */
  int nClose,             /* Bytes available in zClose[] */
  const char *zOrig       /* Complete document text */

){
  const char *zTerm = "</a>";
  const char *z;







  assert( nClose>=20 );
  if( strncmp(zTarget, "http:", 5)==0
   || strncmp(zTarget, "https:", 6)==0
   || strncmp(zTarget, "ftp:", 4)==0
   || strncmp(zTarget, "mailto:", 7)==0
  ){
    blob_appendf(p->pOut, "<a href=\"%s\">", zTarget);
  }else if( zTarget[0]=='/' ){
    blob_appendf(p->pOut, "<a href=\"%R%h\">", zTarget);
  }else if( zTarget[0]=='.'
         && (zTarget[1]=='/' || (zTarget[1]=='.' && zTarget[2]=='/'))
         && (p->state & WIKI_LINKSONLY)==0 ){
    blob_appendf(p->pOut, "<a href=\"%h\">", zTarget);
  }else if( zTarget[0]=='#' ){
    blob_appendf(p->pOut, "<a href=\"%h\">", zTarget);
  }else if( is_valid_hname(zTarget) ){
    int isClosed = 0;
    if( strlen(zTarget)<=HNAME_MAX && is_ticket(zTarget, &isClosed) ){
      /* Special display processing for tickets.  Display the hyperlink
      ** as crossed out if the ticket is closed.
      */
      if( isClosed ){
        if( g.perm.Hyperlink ){
          blob_appendf(p->pOut,
             "%z<span class=\"wikiTagCancelled\">[",
             href("%R/info/%s",zTarget)
          );
          zTerm = "]</span></a>";
        }else{
          blob_appendf(p->pOut,"<span class=\"wikiTagCancelled\">[");
          zTerm = "]</span>";
        }
      }else{
        if( g.perm.Hyperlink ){
          blob_appendf(p->pOut,"%z[", href("%R/info/%s", zTarget));
          zTerm = "]</a>";
        }else{
          blob_appendf(p->pOut, "[");
          zTerm = "]";
        }
      }
    }else if( !in_this_repo(zTarget) ){
      if( (p->state & (WIKI_LINKSONLY|WIKI_NOBADLINKS))!=0 ){
        zTerm = "";
      }else{
        blob_appendf(p->pOut, "<span class=\"brokenlink\">[");
        zTerm = "]</span>";
      }
    }else if( g.perm.Hyperlink ){
      blob_appendf(p->pOut, "%z[",href("%R/info/%s", zTarget));
      zTerm = "]</a>";
    }else{
      zTerm = "";
    }
  }else if( strlen(zTarget)>=10 && fossil_isdigit(zTarget[0]) && zTarget[4]=='-'
            && db_int(0, "SELECT datetime(%Q) NOT NULL", zTarget) ){
    blob_appendf(p->pOut, "<a href=\"%R/timeline?c=%T\">", zTarget);
  }else if( (z = validWikiPageName(p, zTarget))!=0 ){
    const char *zOverride = wiki_is_overridden(zTarget);
    if( zOverride ){
      blob_appendf(p->pOut, "<a href=\"%R/info/%S\">", zOverride);
    }else{
      blob_appendf(p->pOut, "<a href=\"%R/wiki?name=%T\">", z);
    }
  }else if( zTarget>=&zOrig[2] && !fossil_isspace(zTarget[-2]) ){
    /* Probably an array subscript in code */
    zTerm = "";
  }else if( (p->state & (WIKI_NOBADLINKS|WIKI_LINKSONLY))!=0 ){
    zTerm = "";
  }else{
    blob_appendf(p->pOut, "<span class=\"brokenlink\">[%h]", zTarget);
    zTerm = "</span>";
  }

  assert( strlen(zTerm)<nClose );
  sqlite3_snprintf(nClose, zClose, "%s", zTerm);
}

/*
** Check to see if the given parsed markup is the correct
** </verbatim> tag.







|
>
|



|
>



>
>

>
>
>
>






|

|


|
|

|








|

|



|




|


|




|


|



|






|
|


|

|

|


|


|


>







1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
**
**    [0123456789abcdef]
**
**    [#fragment]
**
**    [2010-02-27 07:13]
*/
void wiki_resolve_hyperlink(
  Blob *pOut,             /* Write the HTML output here */
  int mFlags,             /* Rendering option flags */
  const char *zTarget,    /* Hyperlink target; text within [...] */
  char *zClose,           /* Write hyperlink closing text here */
  int nClose,             /* Bytes available in zClose[] */
  const char *zOrig,      /* Complete document text */
  const char *zTitle      /* Title of the link */
){
  const char *zTerm = "</a>";
  const char *z;
  char *zExtra = 0;
  const char *zExtraNS = 0;

  if( zTitle ){
    zExtra = mprintf(" title='%h'", zTitle);
    zExtraNS = zExtra+1;
  }
  assert( nClose>=20 );
  if( strncmp(zTarget, "http:", 5)==0
   || strncmp(zTarget, "https:", 6)==0
   || strncmp(zTarget, "ftp:", 4)==0
   || strncmp(zTarget, "mailto:", 7)==0
  ){
    blob_appendf(pOut, "<a href=\"%s\"%s>", zTarget, zExtra);
  }else if( zTarget[0]=='/' ){
    blob_appendf(pOut, "<a href=\"%R%h\"%s>", zTarget, zExtra);
  }else if( zTarget[0]=='.'
         && (zTarget[1]=='/' || (zTarget[1]=='.' && zTarget[2]=='/'))
         && (mFlags & WIKI_LINKSONLY)==0 ){
    blob_appendf(pOut, "<a href=\"%h\"%s>", zTarget, zExtra);
  }else if( zTarget[0]=='#' ){
    blob_appendf(pOut, "<a href=\"%h\"%s>", zTarget, zExtra);
  }else if( is_valid_hname(zTarget) ){
    int isClosed = 0;
    if( strlen(zTarget)<=HNAME_MAX && is_ticket(zTarget, &isClosed) ){
      /* Special display processing for tickets.  Display the hyperlink
      ** as crossed out if the ticket is closed.
      */
      if( isClosed ){
        if( g.perm.Hyperlink ){
          blob_appendf(pOut,
             "%z<span class=\"wikiTagCancelled\">[",
             xhref(zExtraNS,"%R/info/%s",zTarget)
          );
          zTerm = "]</span></a>";
        }else{
          blob_appendf(pOut,"<span class=\"wikiTagCancelled\">[");
          zTerm = "]</span>";
        }
      }else{
        if( g.perm.Hyperlink ){
          blob_appendf(pOut,"%z[", xhref(zExtraNS,"%R/info/%s", zTarget));
          zTerm = "]</a>";
        }else{
          blob_appendf(pOut, "[");
          zTerm = "]";
        }
      }
    }else if( !in_this_repo(zTarget) ){
      if( (mFlags & (WIKI_LINKSONLY|WIKI_NOBADLINKS))!=0 ){
        zTerm = "";
      }else{
        blob_appendf(pOut, "<span class=\"brokenlink\">[");
        zTerm = "]</span>";
      }
    }else if( g.perm.Hyperlink ){
      blob_appendf(pOut, "%z[",xhref(zExtraNS, "%R/info/%s", zTarget));
      zTerm = "]</a>";
    }else{
      zTerm = "";
    }
  }else if( strlen(zTarget)>=10 && fossil_isdigit(zTarget[0]) && zTarget[4]=='-'
            && db_int(0, "SELECT datetime(%Q) NOT NULL", zTarget) ){
    blob_appendf(pOut, "<a href=\"%R/timeline?c=%T\"%s>", zTarget, zExtra);
  }else if( (z = validWikiPageName(mFlags, zTarget))!=0 ){
    const char *zOverride = wiki_is_overridden(zTarget);
    if( zOverride ){
      blob_appendf(pOut, "<a href=\"%R/info/%S\"%s>", zOverride, zExtra);
    }else{
      blob_appendf(pOut, "<a href=\"%R/wiki?name=%T\"%s>", z, zExtra);
    }
  }else if( zOrig && zTarget>=&zOrig[2] && !fossil_isspace(zTarget[-2]) ){
    /* Probably an array subscript in code */
    zTerm = "";
  }else if( (mFlags & (WIKI_NOBADLINKS|WIKI_LINKSONLY))!=0 ){
    zTerm = "";
  }else{
    blob_appendf(pOut, "<span class=\"brokenlink\">[%h]", zTarget);
    zTerm = "</span>";
  }
  if( zExtra ) fossil_free(zExtra);
  assert( strlen(zTerm)<nClose );
  sqlite3_snprintf(nClose, zClose, "%s", zTerm);
}

/*
** Check to see if the given parsed markup is the correct
** </verbatim> tag.
1489
1490
1491
1492
1493
1494
1495

1496
1497
1498
1499
1500
1501
1502
1503
        }
        z[i] = 0;
        if( zDisplay==0 ){
          zDisplay = zTarget;
        }else{
          while( fossil_isspace(*zDisplay) ) zDisplay++;
        }

        openHyperlink(p, zTarget, zClose, sizeof(zClose), zOrig);
        if( linksOnly || zClose[0]==0 || p->inVerbatim ){
          if( cS1 ) z[iS1] = cS1;
          if( zClose[0]!=']' ){
            blob_appendf(p->pOut, "[%h]%s", zTarget, zClose);
          }else{
            blob_appendf(p->pOut, "%h%s", zTarget, zClose);
          }







>
|







1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
        }
        z[i] = 0;
        if( zDisplay==0 ){
          zDisplay = zTarget;
        }else{
          while( fossil_isspace(*zDisplay) ) zDisplay++;
        }
        wiki_resolve_hyperlink(p->pOut, p->state,
                               zTarget, zClose, sizeof(zClose), zOrig, 0);
        if( linksOnly || zClose[0]==0 || p->inVerbatim ){
          if( cS1 ) z[iS1] = cS1;
          if( zClose[0]!=']' ){
            blob_appendf(p->pOut, "[%h]%s", zTarget, zClose);
          }else{
            blob_appendf(p->pOut, "%h%s", zTarget, zClose);
          }