Fossil

Check-in Differences
Login

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

Difference From release To trunk

2020-10-29
19:18
Fixed a use of "above" in a document to refer to text that was later moved to another document entirely. We now link to that doc instead. (Leaf check-in: 6a6fd6b9 user: wyoung tags: trunk)
19:07
Reworked the "Clones and Backups" section of www/caps/admin-v-setup.md now that we have www/backup.md. (check-in: dd65d143 user: wyoung tags: trunk)
2020-08-20
13:27
Version 2.12.1 (Leaf check-in: b98ce23d user: drh tags: release, branch-2.12, version-2.12.1)
13:01
2.12.1 release candidate with security fixes. (check-in: 40feec32 user: drh tags: branch-2.12)

Changes to VERSION.

     1         -2.12.1
            1  +2.13

Changes to auto.def.

   174    174          configlog "Compiled OK: [join $cmdline]"
   175    175          configlog "============"
   176    176       }
   177    177       if {!$ok} {
   178    178         user-error "unable to compile SQLite compatibility test program"
   179    179       }
   180    180       set err [catch {exec-with-stderr ./conftest__} result errinfo]
   181         -    if {$err} {
   182         -      user-error $result
          181  +    if {[get-define build] eq [get-define host]} {
          182  +      set err [catch {exec-with-stderr ./conftest__} result errinfo]
          183  +      if {$err} {
          184  +        user-error $result
          185  +      }
   183    186       }
   184    187       file delete ./conftest__
   185    188     }
   186    189     test_system_sqlite
   187    190   
   188    191   }
   189    192   

Changes to skins/ardoise/css.txt.

     7      7   img,
     8      8   legend,
     9      9   table.login_out,
    10     10   table.login_out td,
    11     11   tr.timelineCurrent,
    12     12   tr.timelineCurrent td.timelineTableCell,
    13     13   tr.timelineSelected {
           14  +  background-color: initial;
    14     15     border: 0
    15     16   }
    16     17   ol,
    17     18   p,
    18     19   ul {
    19     20     margin-top: 0
    20     21   }
................................................................................
   570    571     margin: 0 .2rem;
   571    572     font-size: 90%;
   572    573     white-space: nowrap;
   573    574     background: #000;
   574    575     border: 2px solid #bbb;
   575    576     border-radius: 5px
   576    577   }
          578  +table.numbered-lines td.file-content > pre {
          579  +  margin-top: -2px/*offset CODE tag border*/;
          580  +}
   577    581   pre > code {
   578    582     padding: 1rem 1.5rem;
   579    583     white-space: pre
   580    584   }
   581    585   td,
   582    586   th {
   583    587     padding: 1px 5px;
................................................................................
   947    951   span.timelineSelected {
   948    952     border-radius: 5px;
   949    953     border: solid #ff8000;
   950    954     vertical-align: top;
   951    955     text-align: left;
   952    956     background: #442800
   953    957   }
   954         -.timelineSelected {}
          958  +.timelineSelected {
          959  +  box-shadow: none;
          960  +}
   955    961   .timelineSecondary {}
   956    962   .timelineSecondary > .timelineColumnarCell,
   957    963   .timelineSecondary > .timelineCompactCell,
   958    964   .timelineSecondary > .timelineDetailCell,
   959    965   .timelineSecondary > .timelineModernCell,
   960    966   .timelineSecondary > .timelineVerboseCell {
   961    967     padding: .75em;

Changes to skins/default/css.txt.

   222    222   }
   223    223   span.submenuctrl {
   224    224     white-space: nowrap;
   225    225   }
   226    226   div.submenu label {
   227    227     white-space: nowrap;
   228    228   }
   229         -
          229  +.fossil-tooltip.help-buttonlet-content {
          230  +  background-color: lightyellow;
          231  +}
   230    232   @media screen and (max-width: 600px) {
   231    233     /* Spacing for mobile */
   232    234     body {
   233    235       padding-left: 4px;
   234    236       padding-right: 4px;
   235    237     }
   236    238     .title {

Changes to skins/eagle/css.txt.

   397    397     font-family: "courier new";
   398    398   }
   399    399   
   400    400   div.filetreeline:hover {
   401    401     background-color: #7EA2D9;
   402    402   }
   403    403   
   404         -div.selectedText {
          404  +table.numbered-lines td.line-numbers span.selected-line {
   405    405     background-color: #7EA2D9;
   406    406   }
   407    407   
   408    408   .statistics-report-graph-line {
   409    409     background-color: #7EA2D9;
   410    410   }
   411    411   

Changes to skins/xekri/css.txt.

   998    998   
   999    999   
  1000   1000   /**************************************
  1001   1001    * Did not encounter these
  1002   1002    */
  1003   1003   
  1004   1004   /* selected lines of text within a linenumbered artifact display */
  1005         -div.selectedText {
         1005  +table.numbered-lines td.line-numbers span.selected-line {
  1006   1006     font-weight: bold;
  1007   1007     color: #00f;
  1008   1008     background-color: #d5d5ff;
  1009         -  border: 1px #00f solid;
         1009  +  border-color: #00f;
  1010   1010   }
  1011   1011   
  1012   1012   /* format for missing privileges note on user setup page */
  1013   1013   p.missingPriv {
  1014   1014     color: #00f;
  1015   1015   }
  1016   1016   
................................................................................
  1116   1116   tr.row0 {
  1117   1117     /* use default */
  1118   1118   }
  1119   1119   /* odd table row color */
  1120   1120   tr.row1 {
  1121   1121     /* Use default */
  1122   1122   }
         1123  +
         1124  +.fossil-tooltip.help-buttonlet-content {
         1125  +  background-color: #111;
         1126  +  border: 1px solid rgba(255,255,255,0.5);
         1127  +}

Changes to src/add.c.

   167    167                   " WHERE pathname=%Q %s", zPath, filename_collation()) ){
   168    168       db_multi_exec("UPDATE vfile SET deleted=0"
   169    169                     " WHERE pathname=%Q %s AND deleted",
   170    170                     zPath, filename_collation());
   171    171     }else{
   172    172       char *zFullname = mprintf("%s%s", g.zLocalRoot, zPath);
   173    173       int isExe = file_isexe(zFullname, RepoFILE);
          174  +    int isLink = file_islink(0);
   174    175       if( file_nondir_objects_on_path(g.zLocalRoot, zFullname) ){
   175    176         /* Do not add unsafe files to the vfile */
   176    177         doSkip = 1;
   177    178       }else{
   178    179         db_multi_exec(
   179    180           "INSERT INTO vfile(vid,deleted,rid,mrid,pathname,isexe,islink,mhash)"
   180    181           "VALUES(%d,0,0,0,%Q,%d,%d,NULL)",
   181         -        vid, zPath, isExe, file_islink(0));
          182  +        vid, zPath, isExe, isLink);
   182    183       }
   183    184       fossil_free(zFullname);
   184    185     }
   185    186     if( db_changes() && !doSkip ){
   186    187       fossil_print("ADDED  %s\n", zPath);
   187    188       return 1;
   188    189     }else{
................................................................................
   191    192     }
   192    193   }
   193    194   
   194    195   /*
   195    196   ** Add all files in the sfile temp table.
   196    197   **
   197    198   ** Automatically exclude the repository file and any other files
   198         -** with reserved names. Also exclude files that are beneath an 
          199  +** with reserved names. Also exclude files that are beneath an
   199    200   ** existing symlink.
   200    201   */
   201    202   static int add_files_in_sfile(int vid){
   202    203     const char *zRepo;        /* Name of the repository database file */
   203    204     int nAdd = 0;             /* Number of files added */
   204    205     int i;                    /* Loop counter */
   205    206     const char *zReserved;    /* Name of a reserved file */
................................................................................
   214    215       zRepo = blob_str(&repoName);
   215    216     }
   216    217     if( filenames_are_case_sensitive() ){
   217    218       xCmp = fossil_strcmp;
   218    219     }else{
   219    220       xCmp = fossil_stricmp;
   220    221     }
   221         -  db_prepare(&loop, 
          222  +  db_prepare(&loop,
   222    223        "SELECT pathname FROM sfile"
   223    224        " WHERE pathname NOT IN ("
   224    225          "SELECT sfile.pathname FROM vfile, sfile"
   225    226          " WHERE vfile.islink"
   226    227          "   AND NOT vfile.deleted"
   227    228          "   AND sfile.pathname>(vfile.pathname||'/')"
   228    229          "   AND sfile.pathname<(vfile.pathname||'0'))"
................................................................................
   549    550   **          setting is non-zero, files WILL BE removed from disk as well.
   550    551   **          This does NOT apply to the 'forget' command.
   551    552   **
   552    553   ** Options:
   553    554   **   --soft                  Skip removing files from the checkout.
   554    555   **                           This supersedes the --hard option.
   555    556   **   --hard                  Remove files from the checkout.
   556         -**   --case-sensitive <BOOL> Override the case-sensitive setting.
          557  +**   --case-sensitive BOOL   Override the case-sensitive setting.
   557    558   **   -n|--dry-run            If given, display instead of run actions.
   558    559   **   --reset                 Reset the DELETED state of a checkout, such
   559    560   **                           that all newly-rm'd (but not yet committed)
   560    561   **                           files are no longer removed. No flags other
   561    562   **                           than --verbose or --dry-run may be used with
   562    563   **                           --reset.
   563    564   **   --verbose|-v            Outputs information about each --reset file.
................................................................................
   651    652   **
   652    653   ** The case-sensitive setting determines the default value.  If
   653    654   ** the case-sensitive setting is undefined, then case sensitivity
   654    655   ** defaults off for Cygwin, Mac and Windows and on for all other unix.
   655    656   ** If case-sensitivity is enabled in the windows kernel, the Cygwin port
   656    657   ** of fossil.exe can detect that, and modifies the default to 'on'.
   657    658   **
   658         -** The --case-sensitive <BOOL> command-line option overrides any
          659  +** The "--case-sensitive BOOL" command-line option overrides any
   659    660   ** setting.
   660    661   */
   661    662   int filenames_are_case_sensitive(void){
   662    663     static int caseSensitive;
   663    664     static int once = 1;
   664    665   
   665    666     if( once ){
................................................................................
   987    988   **          setting is non-zero, files WILL BE renamed or moved on disk
   988    989   **          as well.  This does NOT apply to the 'rename' command.
   989    990   **
   990    991   ** Options:
   991    992   **   --soft                    Skip moving files within the checkout.
   992    993   **                             This supersedes the --hard option.
   993    994   **   --hard                    Move files within the checkout.
   994         -**   --case-sensitive <BOOL>   Override the case-sensitive setting.
          995  +**   --case-sensitive BOOL     Override the case-sensitive setting.
   995    996   **   -n|--dry-run              If given, display instead of run actions.
   996    997   **
   997    998   ** See also: [[changes]], [[status]]
   998    999   */
   999   1000   void mv_cmd(void){
  1000   1001     int i;
  1001   1002     int vid;

Changes to src/ajax.c.

    39     39   ** Emits JS code which initializes the
    40     40   ** fossil.page.previewModes object to a map of AJAX_RENDER_xxx values
    41     41   ** and symbolic names for use by client-side scripts.
    42     42   **
    43     43   ** If addScriptTag is true then the output is wrapped in a SCRIPT tag
    44     44   ** with the current nonce, else no SCRIPT tag is emitted.
    45     45   **
    46         -** Requires that style_emit_script_fossil_bootstrap() has already been
           46  +** Requires that builtin_emit_script_fossil_bootstrap() has already been
    47     47   ** called in order to initialize the window.fossil.page object.
    48     48   */
    49     49   void ajax_emit_js_preview_modes(int addScriptTag){
    50     50     if(addScriptTag){
    51         -    style_emit_script_tag(0,0);
    52         -    CX("\n");
           51  +    style_script_begin(__FILE__,__LINE__);
    53     52     }
    54     53     CX("fossil.page.previewModes={"
    55     54        "guess: %d, %d: 'guess', wiki: %d, %d: 'wiki',"
    56     55        "htmlIframe: %d, %d: 'htmlIframe', "
    57     56        "htmlInline: %d, %d: 'htmlInline', "
    58     57        "text: %d, %d: 'text'"
    59     58        "};\n",
    60     59        AJAX_RENDER_GUESS, AJAX_RENDER_GUESS,
    61     60        AJAX_RENDER_WIKI, AJAX_RENDER_WIKI,
    62     61        AJAX_RENDER_HTML_IFRAME, AJAX_RENDER_HTML_IFRAME,
    63     62        AJAX_RENDER_HTML_INLINE, AJAX_RENDER_HTML_INLINE,
    64     63        AJAX_RENDER_PLAIN_TEXT, AJAX_RENDER_PLAIN_TEXT);
    65     64     if(addScriptTag){
    66         -    style_emit_script_tag(1,0);
           65  +    style_script_end();
    67     66     }
    68     67   }
    69     68   
    70     69   /*
    71     70   ** Returns a value from the ajax_render_modes enum, based on the
    72     71   ** given mime type string (which may be NULL), defaulting to
    73     72   ** AJAX_RENDER_PLAIN_TEXT.
................................................................................
   128    127       case AJAX_RENDER_WIKI:
   129    128         safe_html_context(DOCSRC_FILE);
   130    129         wiki_render_by_mimetype(pContent, zMime);
   131    130         break;
   132    131       default:{
   133    132         const char *zContent = blob_str(pContent);
   134    133         if(AJAX_PREVIEW_LINE_NUMBERS & flags){
   135         -        output_text_with_line_numbers(zContent, "on");
          134  +        output_text_with_line_numbers(zContent, blob_size(pContent),
          135  +                                      zName, "on", 0);
   136    136         }else{
   137    137           const char *zExt = strrchr(zName,'.');
   138    138           if(zExt && zExt[1]){
   139    139             CX("<pre><code class='language-%s'>%h</code></pre>",
   140    140                zExt+1, zContent);
   141    141           }else{
   142    142             CX("<pre>%h</pre>", zContent);

Changes to src/alerts.c.

   934    934   /*
   935    935   ** SETTING: email-subname             width=16
   936    936   ** This is a short name used to identifies the repository in the Subject:
   937    937   ** line of email alerts. Traditionally this name is included in square
   938    938   ** brackets. Examples: "[fossil-src]", "[sqlite-src]".
   939    939   */
   940    940   /*
   941         -** SETTING: email-send-method         width=5 default=off
          941  +** SETTING: email-send-method         width=5 default=off sensitive
   942    942   ** Determine the method used to send email.  Allowed values are
   943    943   ** "off", "relay", "pipe", "dir", "db", and "stdout".  The "off" value
   944    944   ** means no email is ever sent.  The "relay" value means emails are sent
   945    945   ** to an Mail Sending Agent using SMTP located at email-send-relayhost.
   946    946   ** The "pipe" value means email messages are piped into a command 
   947    947   ** determined by the email-send-command setting. The "dir" value means
   948    948   ** emails are written to individual files in a directory determined
   949    949   ** by the email-send-dir setting.  The "db" value means that emails
   950    950   ** are added to an SQLite database named by the* email-send-db setting.
   951    951   ** The "stdout" value writes email text to standard output, for debugging.
   952    952   */
   953    953   /*
   954         -** SETTING: email-send-command       width=40
          954  +** SETTING: email-send-command       width=40 sensitive
   955    955   ** This is a command to which outbound email content is piped when the
   956    956   ** email-send-method is set to "pipe".  The command must extract
   957    957   ** recipient, sender, subject, and all other relevant information
   958    958   ** from the email header.
   959    959   */
   960    960   /*
   961         -** SETTING: email-send-dir           width=40
          961  +** SETTING: email-send-dir           width=40 sensitive
   962    962   ** This is a directory into which outbound emails are written as individual
   963    963   ** files if the email-send-method is set to "dir".
   964    964   */
   965    965   /*
   966         -** SETTING: email-send-db            width=40
          966  +** SETTING: email-send-db            width=40 sensitive
   967    967   ** This is an SQLite database file into which outbound emails are written
   968    968   ** if the email-send-method is set to "db".
   969    969   */
   970    970   /*
   971    971   ** SETTING: email-self               width=40
   972    972   ** This is the email address for the repository.  Outbound emails add
   973    973   ** this email address as the "From:" field.
   974    974   */
   975    975   /*
   976         -** SETTING: email-send-relayhost      width=40
          976  +** SETTING: email-send-relayhost      width=40 sensitive
   977    977   ** This is the hostname and TCP port to which output email messages
   978    978   ** are sent when email-send-method is "relay".  There should be an
   979    979   ** SMTP server configured as a Mail Submission Agent listening on the
   980    980   ** designated host and port and all times.
   981    981   */
   982    982   
   983    983   
................................................................................
  1559   1559   
  1560   1560   /*
  1561   1561   ** Either shutdown or completely delete a subscription entry given
  1562   1562   ** by the hex value zName.  Then paint a webpage that explains that
  1563   1563   ** the entry has been removed.
  1564   1564   */
  1565   1565   static void alert_unsubscribe(int sid){
  1566         -  char *zEmail;
  1567         -  zEmail = db_text(0, "SELECT semail FROM subscriber"
  1568         -                      " WHERE subscriberId=%d", sid);
         1566  +  const char *zEmail = 0;
         1567  +  const char *zLogin = 0;
         1568  +  int uid = 0;
         1569  +  Stmt q;
         1570  +  db_prepare(&q, "SELECT semail, suname FROM subscriber"
         1571  +                 " WHERE subscriberId=%d", sid);
         1572  +  if( db_step(&q)==SQLITE_ROW ){
         1573  +    zEmail = db_column_text(&q, 0);
         1574  +    zLogin = db_column_text(&q, 1);
         1575  +    uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zLogin);
         1576  +  }
  1569   1577     if( zEmail==0 ){
  1570   1578       style_header("Unsubscribe Fail");
  1571   1579       @ <p>Unable to locate a subscriber with the requested key</p>
  1572   1580     }else{
         1581  +    
  1573   1582       db_multi_exec(
  1574   1583         "DELETE FROM subscriber WHERE subscriberId=%d", sid
  1575   1584       );
  1576   1585       style_header("Unsubscribed");
  1577         -    @ <p>The "%h(zEmail)" email address has been delisted.
  1578         -    @ All traces of that email address have been removed</p>
         1586  +    @ <p>The "%h(zEmail)" email address has been unsubscribed and the
         1587  +    @ corresponding row in the subscriber table has been deleted.<p>
         1588  +    if( uid && g.perm.Admin ){
         1589  +       @ <p>You may also want to
         1590  +       @ <a href="%R/setup_uedit?id=%d(uid)">edit or delete
         1591  +       @ the corresponding user "%h(zLogin)"</a></p>
         1592  +    }
  1579   1593     }
         1594  +  db_finalize(&q);
  1580   1595     style_footer();
  1581   1596     return;
  1582   1597   }
  1583   1598   
  1584   1599   /*
  1585   1600   ** WEBPAGE: alerts
  1586   1601   **
................................................................................
  1767   1782       if( nName==64 ){
  1768   1783         db_multi_exec(
  1769   1784           "UPDATE subscriber SET sverified=1"
  1770   1785           " WHERE subscriberCode=hextoblob(%Q)",
  1771   1786           zName);
  1772   1787         if( db_get_boolean("selfreg-verify",0) ){
  1773   1788           char *zNewCap = db_get("default-perms","u");
         1789  +        db_unprotect(PROTECT_USER);
  1774   1790           db_multi_exec(
  1775   1791              "UPDATE user"
  1776   1792              "   SET cap=%Q"
  1777   1793              " WHERE cap='7' AND login=("
  1778   1794              "   SELECT suname FROM subscriber"
  1779   1795              "    WHERE subscriberCode=hextoblob(%Q))",
  1780   1796              zNewCap, zName
  1781   1797           );
         1798  +        db_protect_pop();
  1782   1799           login_set_capabilities(zNewCap, 0);
  1783   1800         }
  1784   1801         @ <h1>Your email alert subscription has been verified!</h1>
  1785   1802         @ <p>Use the form below to update your subscription information.</p>
  1786   1803         @ <p>Hint:  Bookmark this page so that you can more easily update
  1787   1804         @ your subscription information in the future</p>
  1788   1805       }else{

Changes to src/allrepo.c.

   135    135   **
   136    136   ** Repositories are automatically added to the set of known repositories
   137    137   ** when one of the following commands are run against the repository:
   138    138   ** clone, info, pull, push, or sync.  Even previously ignored repositories
   139    139   ** are added back to the list of repositories by these commands.
   140    140   **
   141    141   ** Options:
   142         -**   --showfile     Show the repository or checkout being operated upon.
   143         -**   --dontstop     Continue with other repositories even after an error.
   144         -**   --dry-run      If given, display instead of run actions.
          142  +**   --dry-run         If given, display instead of run actions.
          143  +**   --showfile        Show the repository or checkout being operated upon.
          144  +**   --stop-on-error   Halt immediately if any subprocess fails.
   145    145   */
   146    146   void all_cmd(void){
   147    147     int n;
   148    148     Stmt q;
   149    149     const char *zCmd;
   150    150     char *zSyscmd;
   151    151     Blob extra;
   152    152     int useCheckouts = 0;
   153    153     int quiet = 0;
   154    154     int dryRunFlag = 0;
   155    155     int showFile = find_option("showfile",0,0)!=0;
   156         -  int stopOnError = find_option("dontstop",0,0)==0;
          156  +  int stopOnError;
   157    157     int nToDel = 0;
   158    158     int showLabel = 0;
   159    159   
          160  +  (void)find_option("dontstop",0,0);   /* Legacy.  Now the default */
          161  +  stopOnError = find_option("stop-on-error",0,0)!=0;
   160    162     dryRunFlag = find_option("dry-run","n",0)!=0;
   161    163     if( !dryRunFlag ){
   162    164       dryRunFlag = find_option("test",0,0)!=0; /* deprecated */
   163    165     }
   164    166   
   165    167     if( g.argc<3 ){
   166    168       usage("SUBCOMMAND ...");
................................................................................
   297    299         blob_append_sql(&sql,
   298    300            "DELETE FROM global_config WHERE name GLOB '%s:%q'",
   299    301            useCheckouts?"ckout":"repo", blob_str(&fn)
   300    302         );
   301    303         if( dryRunFlag ){
   302    304           fossil_print("%s\n", blob_sql_text(&sql));
   303    305         }else{
          306  +        db_unprotect(PROTECT_CONFIG);
   304    307           db_multi_exec("%s", blob_sql_text(&sql));
          308  +        db_protect_pop();
   305    309         }
   306    310       }
   307    311       db_end_transaction(0);
   308    312       blob_reset(&sql);
   309    313       blob_reset(&fn);
   310    314       blob_reset(&extra);
   311    315       return;
................................................................................
   332    336         blob_append_sql(&sql,
   333    337            "INSERT OR IGNORE INTO global_config(name,value)"
   334    338            "VALUES('repo:%q',1)", z
   335    339         );
   336    340         if( dryRunFlag ){
   337    341           fossil_print("%s\n", blob_sql_text(&sql));
   338    342         }else{
          343  +        db_unprotect(PROTECT_CONFIG);
   339    344           db_multi_exec("%s", blob_sql_text(&sql));
          345  +        db_protect_pop();
   340    346         }
   341    347       }
   342    348       db_end_transaction(0);
   343    349       blob_reset(&sql);
   344    350       blob_reset(&fn);
   345    351       blob_reset(&extra);
   346    352       return;
................................................................................
   410    416       }
   411    417       if( !quiet || dryRunFlag ){
   412    418         fossil_print("%s\n", zSyscmd);
   413    419         fflush(stdout);
   414    420       }
   415    421       rc = dryRunFlag ? 0 : fossil_system(zSyscmd);
   416    422       free(zSyscmd);
   417         -    if( stopOnError && rc ){
   418         -      break;
          423  +    if( rc ){
          424  +      if( stopOnError ) break;
          425  +      /* If there is an error, pause briefly, but do not stop.  The brief
          426  +      ** pause is so that if the prior command failed with Ctrl-C then there
          427  +      ** will be time to stop the whole thing with a second Ctrl-C. */
          428  +      sqlite3_sleep(330);
   419    429       }
   420    430     }
   421    431     db_finalize(&q);
   422    432   
   423    433     blob_reset(&extra);
   424    434   
   425    435     /* If any repositories whose names appear in the ~/.fossil file could not
................................................................................
   426    436     ** be found, remove those names from the ~/.fossil file.
   427    437     */
   428    438     if( nToDel>0 ){
   429    439       const char *zSql = "DELETE FROM global_config WHERE name IN toDel";
   430    440       if( dryRunFlag ){
   431    441         fossil_print("%s\n", zSql);
   432    442       }else{
          443  +      db_unprotect(PROTECT_CONFIG);
   433    444         db_multi_exec("%s", zSql /*safe-for-%s*/ );
          445  +      db_protect_pop();
   434    446       }
   435    447     }
   436    448   }

Changes to src/attach.c.

   373    373                           " WHERE tagname GLOB 'tkt-%q*'", zTkt);
   374    374         if( zTkt==0 ) fossil_redirect_home();
   375    375       }
   376    376       zTarget = zTkt;
   377    377       zTargetType = mprintf("Ticket <a href=\"%R/tktview/%s\">%S</a>",
   378    378                             zTkt, zTkt);
   379    379     }
   380         -  if( zFrom==0 ) zFrom = mprintf("%s/home", g.zTop);
          380  +  if( zFrom==0 ) zFrom = mprintf("%R/home");
   381    381     if( P("cancel") ){
   382    382       cgi_redirect(zFrom);
   383    383     }
   384    384     if( P("ok") && szContent>0 && (goodCaptcha = captcha_is_correct(0)) ){
   385    385       int needModerator = (zTkt!=0 && ticket_need_moderation(0)) ||
   386    386                           (zPage!=0 && wiki_need_moderation(0));
   387    387       const char *zComment = PD("comment", "");
................................................................................
   447    447     if( !g.perm.RdTkt && !g.perm.RdWiki ){
   448    448       login_needed(g.anon.RdTkt || g.anon.RdWiki);
   449    449       return;
   450    450     }
   451    451     rid = name_to_rid_www("name");
   452    452     if( rid==0 ){ fossil_redirect_home(); }
   453    453     zUuid = db_text("", "SELECT uuid FROM blob WHERE rid=%d", rid);
   454         -#if 0
   455         -  /* Shunning here needs to get both the attachment control artifact and
   456         -  ** the object that is attached. */
   457         -  if( g.perm.Admin ){
   458         -    if( db_exists("SELECT 1 FROM shun WHERE uuid='%q'", zUuid) ){
   459         -      style_submenu_element("Unshun", "%s/shun?uuid=%s&sub=1",
   460         -            g.zTop, zUuid);
   461         -    }else{
   462         -      style_submenu_element("Shun", "%s/shun?shun=%s#addshun",
   463         -            g.zTop, zUuid);
   464         -    }
   465         -  }
   466         -#endif
   467    454     pAttach = manifest_get(rid, CFTYPE_ATTACHMENT, 0);
   468    455     if( pAttach==0 ) fossil_redirect_home();
   469    456     zTarget = pAttach->zAttachTarget;
   470    457     zSrc = pAttach->zAttachSrc;
   471    458     ridSrc = db_int(0,"SELECT rid FROM blob WHERE uuid='%q'", zSrc);
   472    459     zName = pAttach->zAttachName;
   473    460     zDesc = pAttach->zComment;
................................................................................
   615    602     blob_zero(&attach);
   616    603     if( fShowContent ){
   617    604       const char *z;
   618    605       content_get(ridSrc, &attach);
   619    606       blob_to_utf8_no_bom(&attach, 0);
   620    607       z = blob_str(&attach);
   621    608       if( zLn ){
   622         -      output_text_with_line_numbers(z, zLn);
          609  +      output_text_with_line_numbers(z, blob_size(&attach), zName, zLn, 1);
   623    610       }else{
   624    611         @ <pre>
   625    612         @ %h(z)
   626    613         @ </pre>
   627    614       }
   628    615     }else if( strncmp(zMime, "image/", 6)==0 ){
   629    616       int sz = db_int(0, "SELECT size FROM blob WHERE rid=%d", ridSrc);

Changes to src/backlink.c.

   167    167     Manifest *pWiki;
   168    168     if( tagid==0 ) return;
   169    169     rid = db_int(0, "SELECT rid FROM tagxref WHERE tagid=%d"
   170    170                     " ORDER BY mtime DESC LIMIT 1", tagid);
   171    171     if( rid==0 ) return;
   172    172     pWiki = manifest_get(rid, CFTYPE_WIKI, 0);
   173    173     if( pWiki ){
   174         -    backlink_extract(pWiki->zWiki, pWiki->zMimetype, tagid, 2, pWiki->rDate,1);
          174  +    backlink_extract(pWiki->zWiki, pWiki->zMimetype, tagid, BKLNK_WIKI,
          175  +                     pWiki->rDate, 1);
   175    176       manifest_destroy(pWiki);
   176    177     }
   177    178   }
   178    179   
   179    180   /*
   180    181   ** Structure used to pass down state information through the
   181    182   ** markup formatters into the BACKLINK generator.

Changes to src/backoffice.c.

   239    239   **
   240    240   ** No other process should start active backoffice processing until
   241    241   ** process (1) no longer exists and the current time exceeds (2).
   242    242   */
   243    243   static void backofficeReadLease(Lease *pLease){
   244    244     Stmt q;
   245    245     memset(pLease, 0, sizeof(*pLease));
          246  +  db_unprotect(PROTECT_CONFIG);
   246    247     db_prepare(&q, "SELECT value FROM repository.config"
   247    248                    " WHERE name='backoffice'");
   248    249     if( db_step(&q)==SQLITE_ROW ){
   249    250       const char *z = db_column_text(&q,0);
   250    251       z = backofficeParseInt(z, &pLease->idCurrent);
   251    252       z = backofficeParseInt(z, &pLease->tmCurrent);
   252    253       z = backofficeParseInt(z, &pLease->idNext);
   253    254       backofficeParseInt(z, &pLease->tmNext);
   254    255     }
   255    256     db_finalize(&q);
          257  +  db_protect_pop();
   256    258   }
   257    259   
   258    260   /*
   259    261   ** Return a string that describes how long it has been since the
   260    262   ** last backoffice run.  The string is obtained from fossil_malloc().
   261    263   */
   262    264   char *backoffice_last_run(void){
................................................................................
   275    277     return mprintf("%z ago", human_readable_age(rAge));
   276    278   }
   277    279   
   278    280   /*
   279    281   ** Write a lease to the backoffice property
   280    282   */
   281    283   static void backofficeWriteLease(Lease *pLease){
          284  +  db_unprotect(PROTECT_CONFIG);
   282    285     db_multi_exec(
   283    286       "REPLACE INTO repository.config(name,value,mtime)"
   284    287       " VALUES('backoffice','%lld %lld %lld %lld',now())",
   285    288       pLease->idCurrent, pLease->tmCurrent,
   286    289       pLease->idNext, pLease->tmNext);
          290  +  db_protect_pop();
   287    291   }
   288    292   
   289    293   /*
   290    294   ** Check to see if the specified Win32 process is still alive.  It
   291    295   ** should be noted that even if this function returns non-zero, the
   292    296   ** process may die before another operation on it can be completed.
   293    297   */

Changes to src/bisect.c.

    73     73   */
    74     74   static const struct {
    75     75     const char *zName;
    76     76     const char *zDefault;
    77     77     const char *zDesc;
    78     78   } aBisectOption[] = {
    79     79     { "auto-next",    "on",    "Automatically run \"bisect next\" after each "
    80         -                             "\"bisect good\" or \"bisect bad\"" },
           80  +                             "\"bisect good\", \"bisect bad\", or \"bisect "
           81  +                             "skip\"" },
    81     82     { "direct-only",  "on",    "Follow only primary parent-child links, not "
    82     83                                "merges\n" },
    83     84     { "display",    "chart",   "Command to run after \"next\".  \"chart\", "
    84     85                                "\"log\", \"status\", or \"none\"" },
    85     86   };
    86     87   
    87     88   /*
................................................................................
   226    227         char c;
   227    228         int rid;
   228    229         if( blob_size(&log) ) blob_append(&log, " ", 1);
   229    230         if( zDesc[0]=='n' ) blob_append(&log, "-", 1);
   230    231         if( zDesc[0]=='s' ) blob_append(&log, "s", 1);
   231    232         for(i=1; ((c = zDesc[i])>='0' && c<='9') || (c>='a' && c<='f'); i++){}
   232    233         if( i==1 ) break;
   233         -      rid = db_int(0, 
          234  +      rid = db_int(0,
   234    235           "SELECT rid FROM blob"
   235    236           " WHERE uuid LIKE '%.*q%%'"
   236    237           "   AND EXISTS(SELECT 1 FROM plink WHERE cid=rid)",
   237    238           i-1, zDesc+1
   238    239         );
   239    240         if( rid==0 ) break;
   240    241         blob_appendf(&log, "%d", rid);
................................................................................
   440    441   */
   441    442   void bisect_cmd(void){
   442    443     int n;
   443    444     const char *zCmd;
   444    445     int foundCmd = 0;
   445    446     db_must_be_within_tree();
   446    447     if( g.argc<3 ){
   447         -    usage("bad|good|log|next|options|reset|skip|status|undo");
          448  +    goto usage;
   448    449     }
   449    450     zCmd = g.argv[2];
   450    451     n = strlen(zCmd);
   451    452     if( n==0 ) zCmd = "-";
   452    453     if( strncmp(zCmd, "bad", n)==0 ){
   453    454       int ridBad;
   454    455       foundCmd = 1;
................................................................................
   485    486       if( g.argc==3 ){
   486    487         ridSkip = db_lget_int("checkout",0);
   487    488       }else{
   488    489         ridSkip = name_to_typed_rid(g.argv[3], "ci");
   489    490       }
   490    491       if( ridSkip>0 ){
   491    492         bisect_append_skip(ridSkip);
   492         -      if( bisect_option("auto-next") 
          493  +      if( bisect_option("auto-next")
   493    494          && db_lget_int("bisect-bad",0)>0
   494    495          && db_lget_int("bisect-good",0)>0
   495    496         ){
   496    497           zCmd = "next";
   497    498           n = 4;
   498    499         }
   499    500       }
................................................................................
   608    609     }else if( strncmp(zCmd, "vlist", n)==0
   609    610            || strncmp(zCmd, "ls", n)==0
   610    611            || strncmp(zCmd, "status", n)==0
   611    612     ){
   612    613       int fAll = find_option("all", "a", 0)!=0;
   613    614       bisect_list(!fAll);
   614    615     }else if( !foundCmd ){
   615         -    usage("bad|good|log|next|options|reset|status|ui|undo");
          616  +usage:
          617  +    usage("bad|good|log|chart|next|options|reset|skip|status|ui|undo");
   616    618     }
   617    619   }

Changes to src/browse.c.

   390    390           @ </iframe>
   391    391         }else{
   392    392           Blob content;
   393    393           const char *zMime = mimetype_from_name(zName);
   394    394           content_get(rid, &content);
   395    395           safe_html_context(DOCSRC_FILE);
   396    396           wiki_render_by_mimetype(&content, zMime);
          397  +        document_emit_js();
   397    398         }
   398    399       }
   399    400     }
   400    401     db_finalize(&q);
   401    402     style_footer();
   402    403   }
   403    404   

Changes to src/builtin.c.

   256    256     if( strcmp(zMode, "bundled")==0 ){
   257    257       builtin.eDelivery = JS_BUNDLED;
   258    258     }else if( !bSilent ){
   259    259       fossil_fatal("unknown javascript delivery mode \"%s\" - should be"
   260    260                    " one of: inline separate bundled", zMode);
   261    261     }
   262    262   }
          263  +
          264  +/*
          265  +** Returns the current JS delivery mode: one of JS_INLINE,
          266  +** JS_SEPARATE, JS_BUNDLED.
          267  +*/
          268  +int builtin_get_js_delivery_mode(void){
          269  +  return builtin.eDelivery;
          270  +}
   263    271   
   264    272   /*
   265    273   ** The caller wants the Javascript file named by zFilename to be
   266    274   ** included in the generated page.  Add the file to the queue of
   267    275   ** requested javascript resources, if it is not there already.
   268    276   **
   269    277   ** The current implementation queues the file to be included in the
................................................................................
   306    314   void builtin_fulfill_js_requests(void){
   307    315     if( builtin.nSent>=builtin.nReq ) return;  /* nothing to do */
   308    316     switch( builtin.eDelivery ){
   309    317       case JS_INLINE: {
   310    318         CX("<script nonce='%h'>\n",style_nonce());
   311    319         do{
   312    320           int i = builtin.aReq[builtin.nSent++];
   313         -        CX("/* %s */\n", aBuiltinFiles[i].zName);
          321  +        CX("/* %s %.60c*/\n", aBuiltinFiles[i].zName, '*');
   314    322           cgi_append_content((const char*)aBuiltinFiles[i].pData,
   315    323                              aBuiltinFiles[i].nByte);
   316    324         }while( builtin.nSent<builtin.nReq );
   317    325         CX("</script>\n");
   318    326         break;
   319    327       }
   320    328       case JS_BUNDLED: {
................................................................................
   557    565   */
   558    566   int builtin_vtab_register(sqlite3 *db){
   559    567     int rc = sqlite3_create_module(db, "builtin", &builtinVtabModule, 0);
   560    568     return rc;
   561    569   }
   562    570   /* End of the builtin virtual table
   563    571   ******************************************************************************/
          572  +
          573  +
          574  +/*
          575  +** The first time this is called, it emits code to install and
          576  +** bootstrap the window.fossil object, using the built-in file
          577  +** fossil.bootstrap.js (not to be confused with bootstrap.js).
          578  +**
          579  +** Subsequent calls are no-ops.
          580  +**
          581  +** It emits 2 parts:
          582  +**
          583  +** 1) window.fossil core object, some of which depends on C-level
          584  +** runtime data. That part of the script is always emitted inline. If
          585  +** addScriptTag is true then it is wrapped in its own SCRIPT tag, else
          586  +** it is assumed that the caller already opened a tag.
          587  +**
          588  +** 2) Emits the static fossil.bootstrap.js using builtin_request_js().
          589  +*/
          590  +void builtin_emit_script_fossil_bootstrap(int addScriptTag){
          591  +  static int once = 0;
          592  +  if(0==once++){
          593  +    char * zName;
          594  +    /* Set up the generic/app-agnostic parts of window.fossil
          595  +    ** which require C-level state... */
          596  +    if(addScriptTag!=0){
          597  +      style_script_begin(__FILE__,__LINE__);
          598  +    }
          599  +    CX("(function(){\n");
          600  +    CX(/*MSIE NodeList.forEach polyfill, courtesy of Mozilla:
          601  +    https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach#Polyfill
          602  +       */
          603  +       "if(window.NodeList && !NodeList.prototype.forEach){"
          604  +       "NodeList.prototype.forEach = Array.prototype.forEach;"
          605  +       "}\n");
          606  +    CX("if(!window.fossil) window.fossil={};\n"
          607  +       "window.fossil.version = %!j;\n"
          608  +    /* fossil.rootPath is the top-most CGI/server path,
          609  +    ** including a trailing slash. */
          610  +       "window.fossil.rootPath = %!j+'/';\n",
          611  +       get_version(), g.zTop);
          612  +    /* fossil.config = {...various config-level options...} */
          613  +    CX("window.fossil.config = {");
          614  +    zName = db_get("project-name", "");
          615  +    CX("projectName: %!j,\n", zName);
          616  +    fossil_free(zName);
          617  +    zName = db_get("short-project-name", "");
          618  +    CX("shortProjectName: %!j,\n", zName);
          619  +    fossil_free(zName);
          620  +    zName = db_get("project-code", "");
          621  +    CX("projectCode: %!j,\n", zName);
          622  +    fossil_free(zName);
          623  +    CX("/* Length of UUID hashes for display purposes. */");
          624  +    CX("hashDigits: %d, hashDigitsUrl: %d,\n",
          625  +       hash_digits(0), hash_digits(1));
          626  +    CX("editStateMarkers: {"
          627  +       "/*Symbolic markers to denote certain edit states.*/"
          628  +       "isNew:'[+]', isModified:'[*]', isDeleted:'[-]'},\n");
          629  +    CX("confirmerButtonTicks: 3 "
          630  +       "/*default fossil.confirmer tick count.*/,\n");
          631  +    /* Inject certain info about the current skin... */
          632  +    CX("skin:{");
          633  +    /* can leak a local filesystem path:
          634  +       CX("name: %!j,", skin_in_use());*/
          635  +    CX("isDark: %s"
          636  +       "/*true if the current skin has the 'white-foreground' detail*/",
          637  +       skin_detail_boolean("white-foreground") ? "true" : "false");
          638  +    CX("}\n"/*fossil.config.skin*/);
          639  +    CX("};\n"/* fossil.config */);
          640  +    CX("if(fossil.config.skin.isDark) "
          641  +       "document.body.classList.add('fossil-dark-style');\n");
          642  +#if 0
          643  +    /* Is it safe to emit the CSRF token here? Some pages add it
          644  +    ** as a hidden form field. */
          645  +    if(g.zCsrfToken[0]!=0){
          646  +      CX("window.fossil.csrfToken = %!j;\n",
          647  +         g.zCsrfToken);
          648  +    }
          649  +#endif
          650  +    /*
          651  +    ** fossil.page holds info about the current page. This is also
          652  +    ** where the current page "should" store any of its own
          653  +    ** page-specific state, and it is reserved for that purpose.
          654  +    */
          655  +    CX("window.fossil.page = {"
          656  +       "name:\"%T\""
          657  +       "};\n", g.zPath);
          658  +    CX("})();\n");
          659  +    if(addScriptTag!=0){
          660  +      style_script_end();
          661  +    }
          662  +    /* The remaining window.fossil bootstrap code is not dependent on
          663  +    ** C-runtime state... */
          664  +    builtin_request_js("fossil.bootstrap.js");
          665  +  }
          666  +}
          667  +
          668  +/*
          669  +** Given the NAME part of fossil.NAME.js, this function checks whether
          670  +** that module has been emitted by this function before.  If it has,
          671  +** it returns -1 with no side effects. If it has not, it queues up
          672  +** (via builtin_request_js()) an emit of the module via and all of its
          673  +** known (by this function) fossil.XYZ.js dependencies (in their
          674  +** dependency order) and returns 1. If it does not find the given
          675  +** module name it returns 0.
          676  +**
          677  +** As a special case, if passed 0 then it queues up all known modules
          678  +** and returns -1.
          679  +**
          680  +** The very first time this is called, it unconditionally calls
          681  +** builtin_emit_script_fossil_bootstrap().
          682  +**
          683  +** Any given module is only queued once, whether it is explicitly
          684  +** passed to the function or resolved as a dependency. Any attempts to
          685  +** re-queue them later are harmless no-ops.
          686  +*/
          687  +static int builtin_emit_fossil_js_once(const char * zName){
          688  +  static int once = 0;
          689  +  int i;
          690  +  static struct FossilJs {
          691  +    const char * zName; /* NAME part of fossil.NAME.js */
          692  +    int emitted;        /* True if already emitted. */
          693  +    const char * zDeps; /* \0-delimited list of other FossilJs
          694  +                        ** entries: all known deps of this one. Each
          695  +                        ** REQUIRES an EXPLICIT trailing \0, including
          696  +                        ** the final one! */
          697  +  } fjs[] = {
          698  +  /* This list ordering isn't strictly important. */
          699  +  {"confirmer",      0, 0},
          700  +  {"copybutton",     0, "dom\0"},
          701  +  {"dom",            0, 0},
          702  +  {"fetch",          0, 0},
          703  +  {"numbered-lines", 0, "popupwidget\0copybutton\0"},
          704  +  {"pikchr",         0, "dom\0"},
          705  +  {"popupwidget",    0, "dom\0"},
          706  +  {"storage",        0, 0},
          707  +  {"tabs",           0, "dom\0"}
          708  +  };
          709  +  const int nFjs = sizeof(fjs) / sizeof(fjs[0]);
          710  +  if(0==once){
          711  +    ++once;
          712  +    builtin_emit_script_fossil_bootstrap(1);
          713  +  }
          714  +  if(0==zName){
          715  +    for( i = 0; i < nFjs; ++i ){
          716  +      builtin_emit_fossil_js_once(fjs[i].zName);
          717  +    }
          718  +    return -1;
          719  +  }
          720  +  for( i = 0; i < nFjs; ++i ){
          721  +    if(0==strcmp(zName, fjs[i].zName)){
          722  +      if(fjs[i].emitted){
          723  +        return -1;
          724  +      }else{
          725  +        char nameBuffer[50];
          726  +        if(fjs[i].zDeps){
          727  +          const char * zDep = fjs[i].zDeps;
          728  +          while(*zDep!=0){
          729  +            builtin_emit_fossil_js_once(zDep);
          730  +            zDep += strlen(zDep)+1/*NUL delimiter*/;
          731  +          }
          732  +        }
          733  +        sqlite3_snprintf(sizeof(nameBuffer)-1, nameBuffer,
          734  +                         "fossil.%s.js", fjs[i].zName);
          735  +        builtin_request_js(nameBuffer);
          736  +        fjs[i].emitted = 1;
          737  +        return 1;
          738  +      }
          739  +    }
          740  +  }
          741  +  return 0;
          742  +}
          743  +
          744  +/*
          745  +** COMMAND: test-js-once
          746  +**
          747  +** Tester for builtin_emit_fossil_js_once().
          748  +**
          749  +** Usage: %fossil test-js-once filename
          750  +*/
          751  +void test_js_once(void){
          752  +  int i;
          753  +  if(g.argc<2){
          754  +    usage("?FILENAME...?");
          755  +  }
          756  +  if(2==g.argc){
          757  +    builtin_emit_fossil_js_once(0);
          758  +    assert(builtin.nReq>8);
          759  +  }else{
          760  +    for(i = 2; i < g.argc; ++i){
          761  +      builtin_emit_fossil_js_once(g.argv[i]);
          762  +    }
          763  +    assert(builtin.nReq>1 && "don't forget implicit fossil.bootstrap.js");
          764  +  }
          765  +  for(i = 0; i < builtin.nReq; ++i){
          766  +    fossil_print("ndx#%d = %d = %s\n", i, builtin.aReq[i],
          767  +                 aBuiltinFiles[builtin.aReq[i]].zName);
          768  +  }
          769  +}
          770  +
          771  +/*
          772  +** Convenience wrapper which calls builtin_request_js() for a series
          773  +** of builtin scripts named fossil.NAME.js. The first time it is
          774  +** called, it also calls builtin_emit_script_fossil_bootstrap() to
          775  +** initialize the window.fossil JS API. The first argument is the NAME
          776  +** part of the first API to emit. All subsequent arguments must be
          777  +** strings of the NAME part of additional fossil.NAME.js files,
          778  +** followed by a NULL argument to terminate the list.
          779  +**
          780  +** e.g. pass it ("fetch", "dom", "tabs", 0) to load those 3 APIs (or
          781  +** pass it ("fetch","tabs",0), as "dom" is a dependency of "tabs", so
          782  +** it will be automatically loaded). Do not forget the trailing 0!
          783  +**
          784  +** If it is JS_BUNDLED then this routine queues up an emit of ALL of
          785  +** the JS fossil.XYZ.js APIs which are not strictly specific to a
          786  +** single page, and then calls builtin_fulfill_js_requests(). The idea
          787  +** is that we can get better bundle caching and reduced HTTP requests
          788  +** by including all JS, rather than creating separate bundles on a
          789  +** per-page basis. In this case, all arguments are ignored!
          790  +**
          791  +** This function has an internal mapping of the dependencies for each
          792  +** of the known fossil.XYZ.js modules and ensures that the
          793  +** dependencies also get queued (recursively) and that each module is
          794  +** queued only once.
          795  +**
          796  +** If passed a name which is not a base fossil module name then it 
          797  +** will fail fatally!
          798  +**
          799  +** DO NOT use this for loading fossil.page.*.js: use
          800  +** builtin_request_js() for those.
          801  +**
          802  +** If the current JS delivery mode is *not* JS_BUNDLED then this
          803  +** function queues up a request for each given module and its known
          804  +** dependencies, but does not immediately fulfill the request, thus it
          805  +** can be called multiple times.
          806  +**
          807  +** If a given module is ever passed to this more than once, either in
          808  +** a single invocation or multiples, it is only queued for emit a
          809  +** single time (i.e. the 2nd and subsequent ones become
          810  +** no-ops). Likewise, if a module is requested but was already
          811  +** automatically queued to fulfill a dependency, the explicit request
          812  +** becomes a no-op.
          813  +**
          814  +** Bundled mode is the only mode in which this API greatly improves
          815  +** aggregate over-the-wire and HTTP request costs. For other modes,
          816  +** reducing the inclusion of fossil.XYZ APIs to their bare minimum
          817  +** provides the lowest aggregate costs. For debate and details, see
          818  +** the discussion at:
          819  +**
          820  +** https://fossil-scm.org/forum/forumpost/3fa2633f3e
          821  +**
          822  +** In practice it is normally necessary (or preferred) to call
          823  +** builtin_fulfill_js_requests() after calling this, before proceeding
          824  +** to call builtin_request_js() for page-specific JS, in order to
          825  +** improve cachability.
          826  +**
          827  +** Minor caveat: the purpose of emitting all of the fossil.XYZ JS APIs
          828  +** at once is to reduce over-the-wire transfers by enabling cross-page
          829  +** caching, but if there are other JS scripts pending via
          830  +** builtin_request_js() when this is called then they will be included
          831  +** in the JS request emitted by this routine, resulting in a different
          832  +** script URL than if they were not included. Thus, if a given page
          833  +** has its own scripts to install via builtin_request_js(), they
          834  +** should, if possible, be delayed until after this is called OR the
          835  +** page should call builtin_fulfill_js_requests() to flush the request
          836  +** queue before calling this routine.
          837  +**
          838  +** Achtung: the fossil.page.XYZ.js files are page-specific, containing
          839  +** the app-level logic for that specific page, and loading more than
          840  +** one of them in a single page will break that page. Each of those
          841  +** expects to "own" the page it is loaded in, and it should be loaded
          842  +** as late in the JS-loading process as feasible, ideally bundled (via
          843  +** builtin_request_js()) with any other app-/page-specific JS it may
          844  +** need.
          845  +**
          846  +** Example usage:
          847  +**
          848  +** builtin_fossil_js_bundle_or("dom", "fetch", 0);
          849  +**
          850  +** In bundled mode, that will (the first time it is called) emit all
          851  +** builtin fossil JS APIs and "fulfill" the queue immediately. In
          852  +** non-bundled mode it will queue up the "dom" and "fetch" APIs to be
          853  +** emitted the next time builtin_fulfill_js_requests() is called.
          854  +*/
          855  +void builtin_fossil_js_bundle_or( const char * zApi, ... ) {
          856  +  static int bundled = 0;
          857  +  const char *zArg;
          858  +  va_list vargs;
          859  +
          860  +  if(JS_BUNDLED == builtin_get_js_delivery_mode()){
          861  +    if(!bundled){
          862  +      bundled = 1;
          863  +      builtin_emit_fossil_js_once(0);
          864  +      builtin_fulfill_js_requests();
          865  +    }
          866  +    return;
          867  +  }
          868  +  va_start(vargs,zApi);
          869  +  for( zArg = zApi; zArg!=0; (zArg = va_arg (vargs, const char *))){
          870  +    if(0==builtin_emit_fossil_js_once(zArg)){
          871  +      fossil_fatal("Unknown fossil JS module: %s\n", zArg);
          872  +    }
          873  +  }
          874  +  va_end(vargs);
          875  +}

Changes to src/captcha.c.

   456    456     const char *zSecret;
   457    457     const char *z;
   458    458     Blob b;
   459    459     static char zRes[20];
   460    460   
   461    461     zSecret = db_get("captcha-secret", 0);
   462    462     if( zSecret==0 ){
          463  +    db_unprotect(PROTECT_CONFIG);
   463    464       db_multi_exec(
   464    465         "REPLACE INTO config(name,value)"
   465    466         " VALUES('captcha-secret', lower(hex(randomblob(20))));"
   466    467       );
          468  +    db_protect_pop();
   467    469       zSecret = db_get("captcha-secret", 0);
   468    470       assert( zSecret!=0 );
   469    471     }
   470    472     blob_init(&b, 0, 0);
   471    473     blob_appendf(&b, "%s-%x", zSecret, seed);
   472    474     sha1sum_blob(&b, &b);
   473    475     z = blob_buffer(&b);
................................................................................
   558    560   /*
   559    561   ** Add a "Speak the captcha" button.
   560    562   */
   561    563   void captcha_speakit_button(unsigned int uSeed, const char *zMsg){
   562    564     if( zMsg==0 ) zMsg = "Speak the text";
   563    565     @ <input aria-label="%h(zMsg)" type="button" value="%h(zMsg)" \
   564    566     @ id="speakthetext">
   565         -  @ <script nonce="%h(style_nonce())">
          567  +  @ <script nonce="%h(style_nonce())">/* captcha_speakit_button() */
   566    568     @ document.getElementById("speakthetext").onclick = function(){
   567    569     @   var audio = window.fossilAudioCaptcha \
   568    570     @ || new Audio("%R/captcha-audio/%u(uSeed)");
   569    571     @   window.fossilAudioCaptcha = audio;
   570    572     @   audio.currentTime = 0;
   571    573     @   audio.play();
   572    574     @ }

Changes to src/cgi.c.

  1051   1051   ** but REQUEST_URI is not, then compute REQUEST_URI from PATH_INFO and
  1052   1052   ** SCRIPT_NAME.  If neither REQUEST_URI nor PATH_INFO are provided, then
  1053   1053   ** assume that PATH_INFO is an empty string and set REQUEST_URI equal
  1054   1054   ** to PATH_INFO.
  1055   1055   **
  1056   1056   ** SCGI typically omits PATH_INFO.  CGI sometimes omits REQUEST_URI and
  1057   1057   ** PATH_INFO when it is empty.
         1058  +**
         1059  +** CGI Parameter quick reference:
         1060  +**
         1061  +**                                      REQUEST_URI
         1062  +**                               _____________|____________
         1063  +**                              /                          \
         1064  +**    https://www.fossil-scm.org/forum/info/12736b30c072551a?t=c
         1065  +**            \________________/\____/\____________________/ \_/
         1066  +**                    |            |             |            |
         1067  +**               HTTP_HOST         |        PATH_INFO     QUERY_STRING
         1068  +**                            SCRIPT_NAME
  1058   1069   */
  1059   1070   void cgi_init(void){
  1060   1071     char *z;
  1061   1072     const char *zType;
  1062   1073     char *zSemi;
  1063   1074     int len;
  1064   1075     const char *zRequestUri = cgi_parameter("REQUEST_URI",0);
................................................................................
  1069   1080   #endif
  1070   1081   
  1071   1082   #ifdef FOSSIL_ENABLE_JSON
  1072   1083     const int noJson = P("no_json")!=0;
  1073   1084   #endif
  1074   1085     g.isHTTP = 1;
  1075   1086     cgi_destination(CGI_BODY);
  1076         -  if( zScriptName==0 ) malformed_request("missing SCRIPT_NAME");
         1087  +
         1088  +  /* We must have SCRIPT_NAME. If the web server did not supply it, try
         1089  +  ** to compute it from REQUEST_URI and PATH_INFO. */
         1090  +  if( zScriptName==0 ){
         1091  +    size_t nRU, nPI;
         1092  +    if( zRequestUri==0 || zPathInfo==0 ){
         1093  +      malformed_request("missing SCRIPT_NAME");  /* Does not return */
         1094  +    }
         1095  +    nRU = strlen(zRequestUri);
         1096  +    nPI = strlen(zPathInfo);
         1097  +    if( nRU<nPI ){
         1098  +      malformed_request("PATH_INFO is longer than REQUEST_URI");
         1099  +    }
         1100  +    zScriptName = mprintf("%.*s", (int)(nRU-nPI), zRequestUri);
         1101  +    cgi_set_parameter("SCRIPT_NAME", zScriptName);
         1102  +  }
         1103  +
  1077   1104   #ifdef _WIN32
  1078   1105     /* The Microsoft IIS web server does not define REQUEST_URI, instead it uses
  1079   1106     ** PATH_INFO for virtually the same purpose.  Define REQUEST_URI the same as
  1080   1107     ** PATH_INFO and redefine PATH_INFO with SCRIPT_NAME removed from the 
  1081   1108     ** beginning. */
  1082   1109     if( zServerSoftware && strstr(zServerSoftware, "Microsoft-IIS") ){
  1083   1110       int i, j;
................................................................................
  1255   1282       }else{
  1256   1283         lo = mid+1;
  1257   1284       }
  1258   1285     }
  1259   1286   
  1260   1287     /* If no match is found and the name begins with an upper-case
  1261   1288     ** letter, then check to see if there is an environment variable
  1262         -  ** with the given name. Handle environment variables with empty values
  1263         -  ** the same as non-existent environment variables.
         1289  +  ** with the given name.
  1264   1290     */
  1265   1291     if( fossil_isupper(zName[0]) ){
  1266   1292       const char *zValue = fossil_getenv(zName);
  1267         -    if( zValue && zValue[0] ){
         1293  +    if( zValue ){
  1268   1294         cgi_set_parameter_nocopy(zName, zValue, 0);
  1269   1295         CGIDEBUG(("env-match [%s] = [%s]\n", zName, zValue));
  1270   1296         return zValue;
  1271   1297       }
  1272   1298     }
  1273   1299     CGIDEBUG(("no-match [%s]\n", zName));
  1274   1300     return zDefault;

Changes to src/checkin.c.

    60     60   
    61     61   /*
    62     62   ** Create a TEMP table named SFILE and add all unmanaged files named on
    63     63   ** the command-line to that table.  If directories are named, then add
    64     64   ** all unmanaged files contained underneath those directories.  If there
    65     65   ** are no files or directories named on the command-line, then add all
    66     66   ** unmanaged files anywhere in the checkout.
           67  +**
           68  +** This routine never follows symlinks.  It always treats symlinks as
           69  +** object unto themselves.
    67     70   */
    68     71   static void locate_unmanaged_files(
    69     72     int argc,           /* Number of command-line arguments to examine */
    70     73     char **argv,        /* values of command-line arguments */
    71     74     unsigned scanFlags, /* Zero or more SCAN_xxx flags */
    72     75     Glob *pIgnore       /* Do not add files that match this GLOB */
    73     76   ){
................................................................................
    78     81     int nRoot;   /* length of g.zLocalRoot */
    79     82   
    80     83     db_multi_exec("CREATE TEMP TABLE sfile(pathname TEXT PRIMARY KEY %s,"
    81     84                   " mtime INTEGER, size INTEGER)", filename_collation());
    82     85     nRoot = (int)strlen(g.zLocalRoot);
    83     86     if( argc==0 ){
    84     87       blob_init(&name, g.zLocalRoot, nRoot - 1);
    85         -    vfile_scan(&name, blob_size(&name), scanFlags, pIgnore, 0, RepoFILE);
           88  +    vfile_scan(&name, blob_size(&name), scanFlags, pIgnore, 0, SymFILE);
    86     89       blob_reset(&name);
    87     90     }else{
    88     91       for(i=0; i<argc; i++){
    89     92         file_canonical_name(argv[i], &name, 0);
    90     93         zName = blob_str(&name);
    91         -      isDir = file_isdir(zName, RepoFILE);
           94  +      isDir = file_isdir(zName, SymFILE);
    92     95         if( isDir==1 ){
    93         -        vfile_scan(&name, nRoot-1, scanFlags, pIgnore, 0, RepoFILE);
           96  +        vfile_scan(&name, nRoot-1, scanFlags, pIgnore, 0, SymFILE);
    94     97         }else if( isDir==0 ){
    95     98           fossil_warning("not found: %s", &zName[nRoot]);
    96     99         }else if( file_access(zName, R_OK) ){
    97    100           fossil_fatal("cannot open %s", &zName[nRoot]);
    98    101         }else{
    99    102           db_multi_exec(
   100    103              "INSERT OR IGNORE INTO sfile(pathname) VALUES(%Q)",
................................................................................
   411    414   **
   412    415   ** General options:
   413    416   **    --abs-paths       Display absolute pathnames.
   414    417   **    --rel-paths       Display pathnames relative to the current working
   415    418   **                      directory.
   416    419   **    --hash            Verify file status using hashing rather than
   417    420   **                      relying on file mtimes.
   418         -**    --case-sensitive <BOOL>  Override case-sensitive setting.
          421  +**    --case-sensitive BOOL  Override case-sensitive setting.
   419    422   **    --dotfiles        Include unmanaged files beginning with a dot.
   420    423   **    --ignore <CSG>    Ignore unmanaged files matching CSG glob patterns.
   421    424   **
   422    425   ** Options specific to the changes command:
   423    426   **    --header          Identify the repository if report is non-empty.
   424    427   **    -v|--verbose      Say "(none)" if the change report is empty.
   425    428   **    --classify        Start each line with the file's change type.
................................................................................
   673    676   **
   674    677   ** Options:
   675    678   **   --age                 Show when each file was committed.
   676    679   **   -v|--verbose          Provide extra information about each file.
   677    680   **   -t                    Sort output in time order.
   678    681   **   -r VERSION            The specific check-in to list.
   679    682   **   -R|--repository FILE  Extract info from repository FILE.
          683  +**   --hash                With -v, verify file status using hashing
          684  +**                         rather than relying on file sizes and mtimes.
   680    685   **
   681    686   ** See also: [[changes]], [[extras]], [[status]]
   682    687   */
   683    688   void ls_cmd(void){
   684    689     int vid;
   685    690     Stmt q;
   686    691     int verboseFlag;
   687    692     int showAge;
   688    693     int timeOrder;
   689    694     char *zOrderBy = "pathname";
   690    695     Blob where;
   691    696     int i;
          697  +  int useHash = 0;
   692    698     const char *zName;
   693    699     const char *zRev;
   694    700   
   695    701     verboseFlag = find_option("verbose","v", 0)!=0;
   696    702     if( !verboseFlag ){
   697    703       verboseFlag = find_option("l","l", 0)!=0; /* deprecated */
   698    704     }
   699    705     showAge = find_option("age",0,0)!=0;
   700    706     zRev = find_option("r","r",1);
   701    707     timeOrder = find_option("t","t",0)!=0;
          708  +  if( verboseFlag ){
          709  +    useHash = find_option("hash",0,0)!=0;
          710  +  }
   702    711   
   703    712     if( zRev!=0 ){
   704    713       db_find_and_open_repository(0, 0);
   705    714       verify_all_options();
   706    715       ls_cmd_rev(zRev,verboseFlag,showAge,timeOrder);
   707    716       return;
   708    717     }else if( find_option("R",0,1)!=0 ){
................................................................................
   732    741          " %s (pathname=%Q %s) "
   733    742          "OR (pathname>'%q/' %s AND pathname<'%q0' %s)",
   734    743          (blob_size(&where)>0) ? "OR" : "WHERE", zName,
   735    744          filename_collation(), zName, filename_collation(),
   736    745          zName, filename_collation()
   737    746       );
   738    747     }
   739         -  vfile_check_signature(vid, 0);
          748  +  vfile_check_signature(vid, useHash ? CKSIG_HASH : 0);
   740    749     if( showAge ){
   741    750       db_prepare(&q,
   742    751          "SELECT pathname, deleted, rid, chnged, coalesce(origname!=pathname,0),"
   743    752          "       datetime(checkin_mtime(%d,rid),'unixepoch',toLocal())"
   744    753          "  FROM vfile %s"
   745    754          " ORDER BY %s",
   746    755          vid, blob_sql_text(&where), zOrderBy /*safe-for-%s*/
................................................................................
   854    863     /* We should be done with options.. */
   855    864     verify_all_options();
   856    865   
   857    866     if( zIgnoreFlag==0 ){
   858    867       zIgnoreFlag = db_get("ignore-glob", 0);
   859    868     }
   860    869     pIgnore = glob_create(zIgnoreFlag);
   861         -#ifdef FOSSIL_LEGACY_ALLOW_SYMLINKS
   862         -  /* Always consider symlinks. */
   863         -  g.allowSymlinks = db_allow_symlinks_by_default();
   864         -#endif
   865    870     locate_unmanaged_files(g.argc-2, g.argv+2, scanFlags, pIgnore);
   866    871     glob_free(pIgnore);
   867    872   
   868    873     blob_zero(&report);
   869    874     status_report(&report, flags);
   870    875     if( blob_size(&report) ){
   871    876       if( showHdr ){
................................................................................
  1015   1020     }
  1016   1021     if( db_get_boolean("dotfiles", 0) ) scanFlags |= SCAN_ALL;
  1017   1022     verify_all_options();
  1018   1023     pIgnore = glob_create(zIgnoreFlag);
  1019   1024     pKeep = glob_create(zKeepFlag);
  1020   1025     pClean = glob_create(zCleanFlag);
  1021   1026     nRoot = (int)strlen(g.zLocalRoot);
  1022         -#ifdef FOSSIL_LEGACY_ALLOW_SYMLINKS
  1023         -  /* Always consider symlinks. */
  1024         -  g.allowSymlinks = db_allow_symlinks_by_default();
  1025         -#endif
  1026   1027     if( !dirsOnlyFlag ){
  1027   1028       Stmt q;
  1028   1029       Blob repo;
  1029   1030       if( !dryRunFlag && !disableUndo ) undo_begin();
  1030   1031       locate_unmanaged_files(g.argc-2, g.argv+2, scanFlags, pIgnore);
  1031   1032       db_prepare(&q,
  1032   1033           "SELECT %Q || pathname FROM sfile"
................................................................................
  1336   1337   **
  1337   1338   ** Space to hold the returned filename is obtained from fossil_malloc()
  1338   1339   ** and should be freed by the caller.  The caller should also unlink
  1339   1340   ** the file when it is done.
  1340   1341   */
  1341   1342   static char *prepare_commit_description_file(
  1342   1343     CheckinInfo *p,     /* Information about this commit */
  1343         -  int parent_rid      /* parent check-in */
         1344  +  int parent_rid,     /* parent check-in */
         1345  +  Blob *pComment,     /* Check-in comment */
         1346  +  int dryRunFlag      /* True for a dry-run only */
  1344   1347   ){
  1345   1348     Blob *pDesc;
  1346   1349     char *zTags;
  1347   1350     char *zFilename;
  1348   1351     Blob desc;
  1349         -  unsigned int r[2];
  1350   1352     blob_init(&desc, 0, 0);
  1351   1353     pDesc = &desc;
  1352   1354     blob_appendf(pDesc, "checkout %s\n", g.zLocalRoot);
  1353   1355     blob_appendf(pDesc, "repository %s\n", g.zRepositoryName);
  1354   1356     blob_appendf(pDesc, "user %s\n",
  1355   1357                  p->zUserOvrd ? p->zUserOvrd : login_name());
  1356   1358     blob_appendf(pDesc, "branch %s\n",
................................................................................
  1373   1375     status_report(pDesc, C_DEFAULT | C_FATAL);
  1374   1376     if( g.markPrivate ){
  1375   1377       blob_append(pDesc, "private-branch\n", -1);
  1376   1378     }
  1377   1379     if( p->integrateFlag ){
  1378   1380       blob_append(pDesc, "integrate\n", -1);
  1379   1381     }
  1380         -  sqlite3_randomness(sizeof(r), r);
  1381         -  zFilename = mprintf("%scommit-description-%08x%08x.txt",
  1382         -                      g.zLocalRoot, r[0], r[1]);
  1383         -  blob_write_to_file(pDesc, zFilename);
         1382  +  if( pComment && blob_size(pComment)>0 ){
         1383  +    blob_appendf(pDesc, "checkin-comment\n%s\n", blob_str(pComment));
         1384  +  }
         1385  +  if( dryRunFlag ){
         1386  +    zFilename = 0;
         1387  +    fossil_print("******* Commit Description *******\n%s"
         1388  +                 "***** End Commit Description *****\n",
         1389  +                 blob_str(pDesc));
         1390  +  }else{
         1391  +    unsigned int r[2];
         1392  +    sqlite3_randomness(sizeof(r), r);
         1393  +    zFilename = mprintf("%scommit-description-%08x%08x.txt",
         1394  +                        g.zLocalRoot, r[0], r[1]);
         1395  +    blob_write_to_file(pDesc, zFilename);
         1396  +  }
  1384   1397     blob_reset(pDesc);
  1385   1398     return zFilename;
  1386   1399   }
  1387   1400   
  1388   1401   
  1389   1402   /*
  1390   1403   ** Populate the Global.aCommitFile[] based on the command line arguments
................................................................................
  2414   2427       ){
  2415   2428         fossil_fatal("cannot commit against a closed leaf");
  2416   2429       }
  2417   2430   
  2418   2431       /* Always exit the loop on the second pass */
  2419   2432       if( bRecheck ) break;
  2420   2433   
  2421         -    /* Run before-commit hooks */
  2422         -    if( !noVerify ){
  2423         -      char *zAuxFile = prepare_commit_description_file(&sCiInfo,vid);
  2424         -      int rc = hook_run("before-commit",zAuxFile,bTrace);
  2425         -      file_delete(zAuxFile);
  2426         -      fossil_free(zAuxFile);
  2427         -      if( rc ){
  2428         -        fossil_fatal("Before-commit hook failed\n");
  2429         -      }
  2430         -    }
  2431   2434     
  2432   2435       /* Get the check-in comment.  This might involve prompting the
  2433   2436       ** user for the check-in comment, in which case we should resync
  2434   2437       ** to renew the check-in lock and repeat the checks for conflicts.
  2435   2438       */
  2436   2439       if( zComment ){
  2437   2440         blob_zero(&comment);
................................................................................
  2481   2484           cReply = 'N';
  2482   2485         }
  2483   2486         if( cReply!='y' && cReply!='Y' ){
  2484   2487           fossil_exit(1);
  2485   2488         }
  2486   2489       }
  2487   2490     }
         2491  +
         2492  +  if( !noVerify && hook_exists("before-commit") ){
         2493  +    /* Run before-commit hooks */
         2494  +    char *zAuxFile;
         2495  +    zAuxFile = prepare_commit_description_file(
         2496  +                     &sCiInfo, vid, &comment, dryRunFlag);
         2497  +    if( zAuxFile ){
         2498  +      int rc = hook_run("before-commit",zAuxFile,bTrace);
         2499  +      file_delete(zAuxFile);
         2500  +      fossil_free(zAuxFile);
         2501  +      if( rc ){
         2502  +        fossil_fatal("Before-commit hook failed\n");
         2503  +      }
         2504  +    }
         2505  +  }
  2488   2506   
  2489   2507     /*
  2490   2508     ** Step 1: Compute an aggregate MD5 checksum over the disk image
  2491   2509     ** of every file in vid.  The file names are part of the checksum.
  2492   2510     ** The resulting checksum is the same as is expected on the R-card
  2493   2511     ** of a manifest.
  2494   2512     */
................................................................................
  2496   2514   
  2497   2515     /* Step 2: Insert records for all modified files into the blob
  2498   2516     ** table. If there were arguments passed to this command, only
  2499   2517     ** the identified files are inserted (if they have been modified).
  2500   2518     */
  2501   2519     db_prepare(&q,
  2502   2520       "SELECT id, %Q || pathname, mrid, %s, %s, %s FROM vfile "
  2503         -    "WHERE chnged==1 AND NOT deleted AND is_selected(id)",
         2521  +    "WHERE chnged IN (1, 7, 9) AND NOT deleted AND is_selected(id)",
  2504   2522       g.zLocalRoot,
  2505   2523       glob_expr("pathname", db_get("crlf-glob",db_get("crnl-glob",""))),
  2506   2524       glob_expr("pathname", db_get("binary-glob","")),
  2507   2525       glob_expr("pathname", db_get("encoding-glob",""))
  2508   2526     );
  2509   2527     while( db_step(&q)==SQLITE_ROW ){
  2510   2528       int id, rid;
................................................................................
  2736   2754     undo_reset();
  2737   2755   
  2738   2756     /* Commit */
  2739   2757     db_multi_exec("DELETE FROM vvar WHERE name='ci-comment'");
  2740   2758     db_multi_exec("PRAGMA repository.application_id=252006673;");
  2741   2759     db_multi_exec("PRAGMA localdb.application_id=252006674;");
  2742   2760     if( dryRunFlag ){
  2743         -    leaf_ambiguity_warning(nvid,nvid);
  2744   2761       db_end_transaction(1);
  2745   2762       exit(1);
  2746   2763     }
  2747   2764     db_end_transaction(0);
  2748   2765   
  2749   2766     if( outputManifest & MFESTFLG_TAGS ){
  2750   2767       Blob tagslist;

Changes to src/checkout.c.

   178    178     int flg;
   179    179   
   180    180     flg = db_get_manifest_setting();
   181    181   
   182    182     if( flg & MFESTFLG_RAW ){
   183    183       blob_zero(&manifest);
   184    184       content_get(vid, &manifest);
   185         -    sterilize_manifest(&manifest);
          185  +    sterilize_manifest(&manifest, CFTYPE_MANIFEST);
   186    186       zManFile = mprintf("%smanifest", g.zLocalRoot);
   187    187       blob_write_to_file(&manifest, zManFile);
   188    188       free(zManFile);
   189    189     }else{
   190    190       if( !db_exists("SELECT 1 FROM vfile WHERE pathname='manifest'") ){
   191    191         zManFile = mprintf("%smanifest", g.zLocalRoot);
   192    192         file_delete(zManFile);

Changes to src/clone.c.

   165    165   
   166    166     url_parse(g.argv[2], urlFlags);
   167    167     if( zDefaultUser==0 && g.url.user!=0 ) zDefaultUser = g.url.user;
   168    168     if( g.url.isFile ){
   169    169       file_copy(g.url.name, g.argv[3]);
   170    170       db_close(1);
   171    171       db_open_repository(g.argv[3]);
          172  +    db_open_config(1,0);
   172    173       db_record_repository_filename(g.argv[3]);
   173    174       url_remember();
   174    175       if( !(syncFlags & SYNC_PRIVATE) ) delete_private_content();
   175    176       shun_artifacts();
   176    177       db_create_default_users(1, zDefaultUser);
   177    178       if( zDefaultUser ){
   178    179         g.zLogin = zDefaultUser;
................................................................................
   196    197       remember_or_get_http_auth(zHttpAuth, urlFlags & URL_REMEMBER, g.argv[2]);
   197    198       url_remember();
   198    199       if( g.zSSLIdentity!=0 ){
   199    200         /* If the --ssl-identity option was specified, store it as a setting */
   200    201         Blob fn;
   201    202         blob_zero(&fn);
   202    203         file_canonical_name(g.zSSLIdentity, &fn, 0);
          204  +      db_unprotect(PROTECT_ALL);
   203    205         db_set("ssl-identity", blob_str(&fn), 0);
          206  +      db_protect_pop();
   204    207         blob_reset(&fn);
   205    208       }
          209  +    db_unprotect(PROTECT_CONFIG);
   206    210       db_multi_exec(
   207    211         "REPLACE INTO config(name,value,mtime)"
   208    212         " VALUES('server-code', lower(hex(randomblob(20))), now());"
   209    213         "DELETE FROM config WHERE name='project-code';"
   210    214       );
          215  +    db_protect_pop();
   211    216       url_enable_proxy(0);
   212    217       clone_ssh_db_set_options();
   213    218       url_get_password_if_needed();
   214    219       g.xlinkClusterOnly = 1;
   215    220       nErr = client_sync(syncFlags,CONFIGSET_ALL,0,0);
   216    221       g.xlinkClusterOnly = 0;
   217    222       verify_cancel();
................................................................................
   233    238     }
   234    239     db_end_transaction(0);
   235    240     fossil_print("Vacuuming the database... "); fflush(stdout);
   236    241     if( db_int(0, "PRAGMA page_count")>1000
   237    242      && db_int(0, "PRAGMA page_size")<8192 ){
   238    243        db_multi_exec("PRAGMA page_size=8192;");
   239    244     }
          245  +  db_unprotect(PROTECT_ALL);
   240    246     db_multi_exec("VACUUM");
          247  +  db_protect_pop();
   241    248     fossil_print("\nproject-id: %s\n", db_get("project-code", 0));
   242    249     fossil_print("server-id:  %s\n", db_get("server-code", 0));
   243    250     zPassword = db_text(0, "SELECT pw FROM user WHERE login=%Q", g.zLogin);
   244    251     fossil_print("admin-user: %s (password is \"%s\")\n", g.zLogin, zPassword);
   245    252   }
   246    253   
   247    254   /*

Changes to src/comformat.c.

   549    549   **   --trimcrlf       Enable trimming of leading/trailing CR/LF.
   550    550   **   --trimspace      Enable trimming of leading/trailing spaces.
   551    551   **   --wordbreak      Attempt to break lines on word boundaries.
   552    552   **   --origbreak      Attempt to break when the original comment text
   553    553   **                    is detected.
   554    554   **   --indent         Number of spaces to indent (default (-1) is to
   555    555   **                    auto-detect).  Zero means no indent.
   556         -**   -W|--width <num> Width of lines (default (-1) is to auto-detect).
          556  +**   -W|--width NUM   Width of lines (default (-1) is to auto-detect).
   557    557   **                    Zero means no limit.
   558    558   */
   559    559   void test_comment_format(void){
   560    560     const char *zWidth;
   561    561     const char *zIndent;
   562    562     const char *zPrefix;
   563    563     char *zText;

Changes to src/configure.c.

    35     35   #define CONFIGSET_PROJ      0x000008     /* Project name */
    36     36   #define CONFIGSET_SHUN      0x000010     /* Shun settings */
    37     37   #define CONFIGSET_USER      0x000020     /* The USER table */
    38     38   #define CONFIGSET_ADDR      0x000040     /* The CONCEALED table */
    39     39   #define CONFIGSET_XFER      0x000080     /* Transfer configuration */
    40     40   #define CONFIGSET_ALIAS     0x000100     /* URL Aliases */
    41     41   #define CONFIGSET_SCRIBER   0x000200     /* Email subscribers */
    42         -#define CONFIGSET_ALL       0x0003ff     /* Everything */
           42  +#define CONFIGSET_IWIKI     0x000400     /* Interwiki codes */
           43  +#define CONFIGSET_ALL       0x0007ff     /* Everything */
    43     44   
    44     45   #define CONFIGSET_OVERWRITE 0x100000     /* Causes overwrite instead of merge */
    45     46   
    46     47   /*
    47     48   ** This mask is used for the common TH1 configuration settings (i.e. those
    48     49   ** that are not specific to one particular subsystem, such as the transfer
    49     50   ** subsystem).
................................................................................
    56     57   ** Names of the configuration sets
    57     58   */
    58     59   static struct {
    59     60     const char *zName;   /* Name of the configuration set */
    60     61     int groupMask;       /* Mask for that configuration set */
    61     62     const char *zHelp;   /* What it does */
    62     63   } aGroupName[] = {
    63         -  { "/email",       CONFIGSET_ADDR,  "Concealed email addresses in tickets" },
    64         -  { "/project",     CONFIGSET_PROJ,  "Project name and description"         },
           64  +  { "/email",       CONFIGSET_ADDR,    "Concealed email addresses in tickets" },
           65  +  { "/project",     CONFIGSET_PROJ,    "Project name and description"         },
    65     66     { "/skin",        CONFIGSET_SKIN | CONFIGSET_CSS,
    66         -                                     "Web interface appearance settings"    },
    67         -  { "/css",         CONFIGSET_CSS,   "Style sheet"                          },
    68         -  { "/shun",        CONFIGSET_SHUN,  "List of shunned artifacts"            },
    69         -  { "/ticket",      CONFIGSET_TKT,   "Ticket setup",                        },
    70         -  { "/user",        CONFIGSET_USER,  "Users and privilege settings"         },
    71         -  { "/xfer",        CONFIGSET_XFER,  "Transfer setup",                      },
    72         -  { "/alias",       CONFIGSET_ALIAS, "URL Aliases",                         },
    73         -  { "/subscriber",  CONFIGSET_SCRIBER,"Email notification subscriber list"  },
    74         -  { "/all",         CONFIGSET_ALL,   "All of the above"                     },
           67  +                                       "Web interface appearance settings"    },
           68  +  { "/css",         CONFIGSET_CSS,     "Style sheet"                          },
           69  +  { "/shun",        CONFIGSET_SHUN,    "List of shunned artifacts"            },
           70  +  { "/ticket",      CONFIGSET_TKT,     "Ticket setup",                        },
           71  +  { "/user",        CONFIGSET_USER,    "Users and privilege settings"         },
           72  +  { "/xfer",        CONFIGSET_XFER,    "Transfer setup",                      },
           73  +  { "/alias",       CONFIGSET_ALIAS,   "URL Aliases",                         },
           74  +  { "/subscriber",  CONFIGSET_SCRIBER, "Email notification subscriber list"   },
           75  +  { "/interwiki",   CONFIGSET_IWIKI,   "Inter-wiki link prefixes"             },
           76  +  { "/all",         CONFIGSET_ALL,     "All of the above"                     },
    75     77   };
    76     78   
    77     79   
    78     80   /*
    79     81   ** The following is a list of settings that we are willing to
    80     82   ** transfer.
    81     83   **
................................................................................
   141    143     { "clean-glob",             CONFIGSET_PROJ },
   142    144     { "ignore-glob",            CONFIGSET_PROJ },
   143    145     { "keep-glob",              CONFIGSET_PROJ },
   144    146     { "crlf-glob",              CONFIGSET_PROJ },
   145    147     { "crnl-glob",              CONFIGSET_PROJ },
   146    148     { "encoding-glob",          CONFIGSET_PROJ },
   147    149     { "empty-dirs",             CONFIGSET_PROJ },
   148         -#ifdef FOSSIL_LEGACY_ALLOW_SYMLINKS
   149         -  { "allow-symlinks",         CONFIGSET_PROJ },
   150         -#endif
   151    150     { "dotfiles",               CONFIGSET_PROJ },
   152    151     { "parent-project-code",    CONFIGSET_PROJ },
   153    152     { "parent-project-name",    CONFIGSET_PROJ },
   154    153     { "hash-policy",            CONFIGSET_PROJ },
   155    154     { "comment-format",         CONFIGSET_PROJ },
   156    155     { "mimetypes",              CONFIGSET_PROJ },
   157    156     { "forbid-delta-manifests", CONFIGSET_PROJ },
................................................................................
   174    173     { "@concealed",             CONFIGSET_ADDR },
   175    174   
   176    175     { "@shun",                  CONFIGSET_SHUN },
   177    176   
   178    177     { "@alias",                 CONFIGSET_ALIAS },
   179    178   
   180    179     { "@subscriber",            CONFIGSET_SCRIBER },
          180  +
          181  +  { "@interwiki",             CONFIGSET_IWIKI },
   181    182   
   182    183     { "xfer-common-script",     CONFIGSET_XFER },
   183    184     { "xfer-push-script",       CONFIGSET_XFER },
   184    185     { "xfer-commit-script",     CONFIGSET_XFER },
   185    186     { "xfer-ticket-script",     CONFIGSET_XFER },
   186    187   
   187    188   };
................................................................................
   257    258           m &= ~CONFIGSET_ADDR;
   258    259         }
   259    260         return m;
   260    261       }
   261    262     }
   262    263     if( strncmp(zName, "walias:/", 8)==0 ){
   263    264       return CONFIGSET_ALIAS;
          265  +  }
          266  +  if( strncmp(zName, "interwiki:", 10)==0 ){
          267  +    return CONFIGSET_IWIKI;
   264    268     }
   265    269     return 0;
   266    270   }
   267    271   
   268    272   /*
   269    273   ** A mask of all configuration tables that have been reset already.
   270    274   */
................................................................................
   439    443          blob_append_sql(&sql, ",\"%w\"", azToken[jj]);
   440    444       }
   441    445       blob_append_sql(&sql,") VALUES(%s,%s",
   442    446          azToken[1] /*safe-for-%s*/, azToken[0]/*safe-for-%s*/);
   443    447       for(jj=2; jj<nToken; jj+=2){
   444    448          blob_append_sql(&sql, ",%s", azToken[jj+1] /*safe-for-%s*/);
   445    449       }
          450  +    db_protect_only(PROTECT_SENSITIVE);
   446    451       db_multi_exec("%s)", blob_sql_text(&sql));
   447    452       if( db_changes()==0 ){
   448    453         blob_reset(&sql);
   449    454         blob_append_sql(&sql, "UPDATE \"%w\" SET mtime=%s",
   450    455                         &zName[1], azToken[0]/*safe-for-%s*/);
   451    456         for(jj=2; jj<nToken; jj+=2){
   452    457           blob_append_sql(&sql, ", \"%w\"=%s",
................................................................................
   453    458                           azToken[jj], azToken[jj+1]/*safe-for-%s*/);
   454    459         }
   455    460         blob_append_sql(&sql, " WHERE \"%w\"=%s AND mtime<%s",
   456    461                      aType[ii].zPrimKey, azToken[1]/*safe-for-%s*/,
   457    462                      azToken[0]/*safe-for-%s*/);
   458    463         db_multi_exec("%s", blob_sql_text(&sql));
   459    464       }
          465  +    db_protect_pop();
   460    466       blob_reset(&sql);
   461    467       rebuildMask |= thisMask;
   462    468     }
   463    469   }
   464    470   
   465    471   /*
   466    472   ** Process a file full of "config" cards.
................................................................................
   594    600         );
   595    601         blob_appendf(pOut, "config /config %d\n%s\n",
   596    602                      blob_size(&rec), blob_str(&rec));
   597    603         nCard++;
   598    604         blob_reset(&rec);
   599    605       }
   600    606       db_finalize(&q);
          607  +  }
          608  +  if( groupMask & CONFIGSET_IWIKI ){
          609  +    db_prepare(&q, "SELECT mtime, quote(name), quote(value) FROM config"
          610  +                   " WHERE name GLOB 'interwiki:*' AND mtime>=%lld", iStart);
          611  +    while( db_step(&q)==SQLITE_ROW ){
          612  +      blob_appendf(&rec,"%s %s value %s",
          613  +        db_column_text(&q, 0),
          614  +        db_column_text(&q, 1),
          615  +        db_column_text(&q, 2)
          616  +      );
          617  +      blob_appendf(pOut, "config /config %d\n%s\n",
          618  +                   blob_size(&rec), blob_str(&rec));
          619  +      nCard++;
          620  +      blob_reset(&rec);
          621  +    }
          622  +    db_finalize(&q);
   601    623     }
   602    624     if( (groupMask & CONFIGSET_SCRIBER)!=0
   603    625      && db_table_exists("repository","subscriber")
   604    626     ){
   605    627       db_prepare(&q, "SELECT mtime, quote(semail),"
   606    628                      " quote(suname), quote(sdigest),"
   607    629                      " quote(sdonotcall), quote(ssub),"
................................................................................
   708    730   ** accept the -R or --repository option to specify a repository.
   709    731   **
   710    732   ** >  fossil configuration export AREA FILENAME
   711    733   **
   712    734   **         Write to FILENAME exported configuration information for AREA.
   713    735   **         AREA can be one of:
   714    736   **
   715         -**             all email project shun skin ticket user alias subscriber
          737  +**             all email interwiki project shun skin
          738  +**             ticket user alias subscriber
   716    739   **
   717    740   ** >  fossil configuration import FILENAME
   718    741   **
   719    742   **         Read a configuration from FILENAME, overwriting the current
   720    743   **         configuration.
   721    744   **
   722    745   ** >  fossil configuration merge FILENAME
................................................................................
   837    860          "SELECT strftime('config-backup-%%Y%%m%%d%%H%%M%%f','now')");
   838    861       db_begin_transaction();
   839    862       export_config(mask, g.argv[3], 0, zBackup);
   840    863       for(i=0; i<count(aConfig); i++){
   841    864         const char *zName = aConfig[i].zName;
   842    865         if( (aConfig[i].groupMask & mask)==0 ) continue;
   843    866         if( zName[0]!='@' ){
          867  +        db_unprotect(PROTECT_CONFIG);
   844    868           db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
          869  +        db_protect_pop();
   845    870         }else if( fossil_strcmp(zName,"@user")==0 ){
          871  +        db_unprotect(PROTECT_USER);
   846    872           db_multi_exec("DELETE FROM user");
          873  +        db_protect_pop();
   847    874           db_create_default_users(0, 0);
   848    875         }else if( fossil_strcmp(zName,"@concealed")==0 ){
   849    876           db_multi_exec("DELETE FROM concealed");
   850    877         }else if( fossil_strcmp(zName,"@shun")==0 ){
   851    878           db_multi_exec("DELETE FROM shun");
   852    879         }else if( fossil_strcmp(zName,"@subscriber")==0 ){
   853    880           if( db_table_exists("repository","subscriber") ){
................................................................................
  1051   1078       if( zBlob ) fossil_fatal("cannot do both --file or --blob");
  1052   1079       blob_read_from_file(&x, zFile, ExtFILE);
  1053   1080     }else if( zBlob ){
  1054   1081       blob_read_from_file(&x, zBlob, ExtFILE);
  1055   1082     }else{
  1056   1083       blob_init(&x,g.argv[3],-1);
  1057   1084     }
         1085  +  db_unprotect(PROTECT_CONFIG);
  1058   1086     db_prepare(&ins,
  1059   1087        "REPLACE INTO config(name,value,mtime)"
  1060   1088        "VALUES(%Q,:val,now())", zVar);
  1061   1089     if( zBlob ){
  1062   1090       db_bind_blob(&ins, ":val", &x);
  1063   1091     }else{
  1064   1092       db_bind_text(&ins, ":val", blob_str(&x));
  1065   1093     }
  1066   1094     db_step(&ins);
  1067   1095     db_finalize(&ins);
         1096  +  db_protect_pop();
  1068   1097     blob_reset(&x);
  1069   1098   }

Changes to src/copybtn.js.

    78     78       }
    79     79       lockCopyText = false;
    80     80     }.bind(null,this.id),400);
    81     81   }
    82     82   /* Create a temporary <textarea> element and copy the contents to clipboard. */
    83     83   function copyTextToClipboard(text){
    84     84     if( window.clipboardData && window.clipboardData.setData ){
    85         -    clipboardData.setData('Text',text);
           85  +    window.clipboardData.setData('Text',text);
    86     86     }else{
    87     87       var x = document.createElement("textarea");
    88     88       x.style.position = 'fixed';
    89     89       x.value = text;
    90     90       document.body.appendChild(x);
    91     91       x.select();
    92     92       try{

Changes to src/db.c.

    67     67   */
    68     68   #define empty_Stmt_m {BLOB_INITIALIZER,NULL, NULL, NULL, 0, 0}
    69     69   #endif /* INTERFACE */
    70     70   const struct Stmt empty_Stmt = empty_Stmt_m;
    71     71   
    72     72   /*
    73     73   ** Call this routine when a database error occurs.
           74  +** This routine throws a fatal error.  It does not return.
    74     75   */
    75     76   static void db_err(const char *zFormat, ...){
    76     77     va_list ap;
    77     78     char *z;
    78     79     va_start(ap, zFormat);
    79     80     z = vmprintf(zFormat, ap);
    80     81     va_end(ap);
................................................................................
   111    112   }
   112    113   
   113    114   /*
   114    115   ** All static variable that a used by only this file are gathered into
   115    116   ** the following structure.
   116    117   */
   117    118   static struct DbLocalData {
          119  +  unsigned protectMask;     /* Prevent changes to database */
   118    120     int nBegin;               /* Nesting depth of BEGIN */
   119    121     int doRollback;           /* True to force a rollback */
   120    122     int nCommitHook;          /* Number of commit hooks */
   121    123     int wrTxn;                /* Outer-most TNX is a write */
   122    124     Stmt *pAllStmt;           /* List of all unfinalized statements */
   123    125     int nPrepare;             /* Number of calls to sqlite3_prepare_v2() */
   124    126     int nDeleteOnFail;        /* Number of entries in azDeleteOnFail[] */
................................................................................
   131    133     int nBeforeCommit;        /* Number of entries in azBeforeCommit */
   132    134     int nPriorChanges;        /* sqlite3_total_changes() at transaction start */
   133    135     const char *zStartFile;   /* File in which transaction was started */
   134    136     int iStartLine;           /* Line of zStartFile where transaction started */
   135    137     int (*xAuth)(void*,int,const char*,const char*,const char*,const char*);
   136    138     void *pAuthArg;           /* Argument to the authorizer */
   137    139     const char *zAuthName;    /* Name of the authorizer */
   138         -} db = {0, 0, 0, 0, 0, 0, };
          140  +  int bProtectTriggers;     /* True if protection triggers already exist */
          141  +  int nProtect;             /* Slots of aProtect used */
          142  +  unsigned aProtect[10];    /* Saved values of protectMask */
          143  +} db = {
          144  +  PROTECT_USER|PROTECT_CONFIG|PROTECT_BASELINE,  /* protectMask */
          145  +  0, 0, 0, 0, 0, 0, };
   139    146   
   140    147   /*
   141    148   ** Arrange for the given file to be deleted on a failure.
   142    149   */
   143    150   void db_delete_on_failure(const char *zFilename){
   144    151     assert( db.nDeleteOnFail<count(db.azDeleteOnFail) );
   145    152     if( zFilename==0 ) return;
................................................................................
   206    213     db.nBegin++;
   207    214   }
   208    215   /*
   209    216   ** Begin a new transaction for writing.
   210    217   */
   211    218   void db_begin_write_real(const char *zStartFile, int iStartLine){
   212    219     if( db.nBegin==0 ){
   213         -    db_multi_exec("BEGIN IMMEDIATE");
   214         -    sqlite3_commit_hook(g.db, db_verify_at_commit, 0);
   215         -    db.nPriorChanges = sqlite3_total_changes(g.db);
   216         -    db.doRollback = 0;
   217         -    db.zStartFile = zStartFile;
   218         -    db.iStartLine = iStartLine;
   219         -    db.wrTxn = 1;
          220  +    if( !db_is_writeable("repository") ){
          221  +      db_multi_exec("BEGIN");
          222  +    }else{
          223  +      db_multi_exec("BEGIN IMMEDIATE");
          224  +      sqlite3_commit_hook(g.db, db_verify_at_commit, 0);
          225  +      db.nPriorChanges = sqlite3_total_changes(g.db);
          226  +      db.doRollback = 0;
          227  +      db.zStartFile = zStartFile;
          228  +      db.iStartLine = iStartLine;
          229  +      db.wrTxn = 1;
          230  +    }
   220    231     }else if( !db.wrTxn ){
   221    232       fossil_warning("read txn at %s:%d might cause SQLITE_BUSY "
   222    233          "for the write txn at %s:%d",
   223    234          db.zStartFile, db.iStartLine, zStartFile, iStartLine);
   224    235     }
   225    236     db.nBegin++;
   226    237   }
................................................................................
   239    250       if( g.fSqlTrace ) fossil_trace("-- ROLLBACK by request\n");
   240    251     }
   241    252     db.nBegin--;
   242    253     if( db.nBegin==0 ){
   243    254       int i;
   244    255       if( db.doRollback==0 && db.nPriorChanges<sqlite3_total_changes(g.db) ){
   245    256         i = 0;
          257  +      db_protect_only(PROTECT_SENSITIVE);
   246    258         while( db.nBeforeCommit ){
   247    259           db.nBeforeCommit--;
   248    260           sqlite3_exec(g.db, db.azBeforeCommit[i], 0, 0, 0);
   249    261           sqlite3_free(db.azBeforeCommit[i]);
   250    262           i++;
   251    263         }
   252    264         leaf_do_pending_checks();
          265  +      db_protect_pop();
   253    266       }
   254    267       for(i=0; db.doRollback==0 && i<db.nCommitHook; i++){
   255    268         int rc = db.aHook[i].xHook();
   256    269         if( rc ){
   257    270           db.doRollback = 1;
   258    271           if( g.fSqlTrace ) fossil_trace("-- ROLLBACK due to aHook[%d]\n", i);
   259    272         }
................................................................................
   317    330         db.aHook[i].xHook = xS;
   318    331       }
   319    332     }
   320    333     db.aHook[db.nCommitHook].sequence = sequence;
   321    334     db.aHook[db.nCommitHook].xHook = x;
   322    335     db.nCommitHook++;
   323    336   }
          337  +
          338  +#if INTERFACE
          339  +/*
          340  +** Flag bits for db_protect() and db_unprotect() indicating which parts
          341  +** of the databases should be write protected or write enabled, respectively.
          342  +*/
          343  +#define PROTECT_USER       0x01  /* USER table */
          344  +#define PROTECT_CONFIG     0x02  /* CONFIG and GLOBAL_CONFIG tables */
          345  +#define PROTECT_SENSITIVE  0x04  /* Sensitive and/or global settings */
          346  +#define PROTECT_READONLY   0x08  /* everything except TEMP tables */
          347  +#define PROTECT_BASELINE   0x10  /* protection system is working */
          348  +#define PROTECT_ALL        0x1f  /* All of the above */
          349  +#define PROTECT_NONE       0x00  /* Nothing.  Everything is open */
          350  +#endif /* INTERFACE */
          351  +
          352  +/*
          353  +** Enable or disable database write protections.
          354  +**
          355  +**    db_protext(X)         Add protects on X
          356  +**    db_unprotect(X)       Remove protections on X
          357  +**    db_protect_only(X)    Remove all prior protections then set
          358  +**                          protections to only X.
          359  +**
          360  +** Each of these routines pushes the previous protection mask onto
          361  +** a finite-size stack.  Each should be followed by a call to
          362  +** db_protect_pop() to pop the stack and restore the protections that
          363  +** existed prior to the call.  The protection mask stack has a limited
          364  +** depth, so take care not to nest calls too deeply.
          365  +**
          366  +** About Database Write Protection
          367  +** -------------------------------
          368  +**
          369  +** This is *not* a primary means of defending the application from
          370  +** attack.  Fossil should be secure even if this mechanism is disabled.
          371  +** The purpose of database write protection is to provide an additional
          372  +** layer of defense in case SQL injection bugs somehow slip into other
          373  +** parts of the system.  In other words, database write protection is
          374  +** not primary defense but rather defense in depth.
          375  +**
          376  +** This mechanism mostly focuses on the USER table, to prevent an
          377  +** attacker from giving themselves Admin privilegs, and on the
          378  +** CONFIG table and specially "sensitive" settings such as
          379  +** "diff-command" or "editor" that if compromised by an attacker
          380  +** could lead to an RCE.
          381  +**
          382  +** By default, the USER and CONFIG tables are read-only.  Various
          383  +** subsystems that legitimately need to change those tables can
          384  +** temporarily do so using:
          385  +**
          386  +**     db_unprotect(PROTECT_xxx);
          387  +**     // make the legitmate changes here
          388  +**     db_protect_pop();
          389  +**
          390  +** Code that runs inside of reduced protections should be carefully
          391  +** reviewed to ensure that it is harmless and not subject to SQL
          392  +** injection.
          393  +**
          394  +** Read-only operations (such as many web pages like /timeline)
          395  +** can invoke db_protect(PROTECT_ALL) to effectively make the database
          396  +** read-only.  TEMP tables (which are often used for these kinds of
          397  +** pages) are still writable, however.
          398  +**
          399  +** The PROTECT_SENSITIVE protection is a subset of PROTECT_CONFIG
          400  +** that blocks changes to all of the global_config table, but only
          401  +** "sensitive" settings in the config table.  PROTECT_SENSITIVE
          402  +** relies on triggers and the protected_setting() SQL function to
          403  +** prevent changes to sensitive settings.
          404  +**
          405  +** Additional Notes
          406  +** ----------------
          407  +**
          408  +** Calls to routines like db_set() and db_unset() temporarily disable
          409  +** the PROTECT_CONFIG protection.  The assumption is that these calls
          410  +** cannot be invoked by an SQL injection and are thus safe.  Make sure
          411  +** this is the case by always using a string literal as the name argument
          412  +** to db_set() and db_unset() and friend, not a variable that might
          413  +** be compromised by an attack.
          414  +*/
          415  +void db_protect_only(unsigned flags){
          416  +  if( db.nProtect>=count(db.aProtect)-2 ){
          417  +    fossil_panic("too many db_protect() calls");
          418  +  }
          419  +  db.aProtect[db.nProtect++] = db.protectMask;
          420  +  if( (flags & PROTECT_SENSITIVE)!=0 
          421  +   && db.bProtectTriggers==0
          422  +   && g.repositoryOpen
          423  +  ){
          424  +    /* Create the triggers needed to protect sensitive settings from
          425  +    ** being created or modified the first time that PROTECT_SENSITIVE
          426  +    ** is enabled.  Deleting a sensitive setting is harmless, so there
          427  +    ** is not trigger to block deletes.  After being created once, the
          428  +    ** triggers persist for the life of the database connection. */
          429  +    db_multi_exec(
          430  +      "CREATE TEMP TRIGGER protect_1 BEFORE INSERT ON config"
          431  +      " WHEN protected_setting(new.name) BEGIN"
          432  +      "  SELECT raise(abort,'not authorized');"
          433  +      "END;\n"
          434  +      "CREATE TEMP TRIGGER protect_2 BEFORE UPDATE ON config"
          435  +      " WHEN protected_setting(new.name) BEGIN"
          436  +      "  SELECT raise(abort,'not authorized');"
          437  +      "END;\n"
          438  +    );
          439  +    db.bProtectTriggers = 1;
          440  +  }
          441  +  db.protectMask = flags;
          442  +}
          443  +void db_protect(unsigned flags){
          444  +  db_protect_only(db.protectMask | flags);
          445  +}
          446  +void db_unprotect(unsigned flags){
          447  +  if( db.nProtect>=count(db.aProtect)-2 ){
          448  +    fossil_panic("too many db_unprotect() calls");
          449  +  }
          450  +  db.aProtect[db.nProtect++] = db.protectMask;
          451  +  db.protectMask &= ~flags;
          452  +}
          453  +void db_protect_pop(void){
          454  +  if( db.nProtect<1 ){
          455  +    fossil_panic("too many db_protect_pop() calls");
          456  +  }
          457  +  db.protectMask = db.aProtect[--db.nProtect];
          458  +}
          459  +
          460  +/*
          461  +** Verify that the desired database write pertections are in place.
          462  +** Throw a fatal error if not.
          463  +*/
          464  +void db_assert_protected(unsigned flags){
          465  +  if( (flags & db.protectMask)!=flags ){
          466  +    fossil_panic("missing database write protection bits: %02x",
          467  +                 flags & ~db.protectMask);
          468  +  }
          469  +}
          470  +
          471  +/*
          472  +** Assert that either all protections are off (including PROTECT_BASELINE
          473  +** which is usually always enabled), or the setting named in the argument
          474  +** is no a sensitive setting.
          475  +**
          476  +** This assert() is used to verify that the db_set() and db_set_int()
          477  +** interfaces do not modify a sensitive setting.
          478  +*/
          479  +void db_assert_protection_off_or_not_sensitive(const char *zName){
          480  +  if( db.protectMask!=0 && db_setting_is_protected(zName) ){
          481  +    fossil_panic("unauthorized change to protected setting \"%s\"", zName);
          482  +  }
          483  +}
          484  +
          485  +/*
          486  +** Every Fossil database connection automatically registers the following
          487  +** overarching authenticator callback, and leaves it registered for the
          488  +** duration of the connection.  This authenticator will call any
          489  +** sub-authenticators that are registered using db_set_authorizer().
          490  +*/
          491  +int db_top_authorizer(
          492  +  void *pNotUsed,
          493  +  int eCode,
          494  +  const char *z0,
          495  +  const char *z1,
          496  +  const char *z2,
          497  +  const char *z3
          498  +){
          499  +  int rc = SQLITE_OK;
          500  +  switch( eCode ){
          501  +    case SQLITE_INSERT:
          502  +    case SQLITE_UPDATE:
          503  +    case SQLITE_DELETE: {
          504  +      if( (db.protectMask & PROTECT_USER)!=0
          505  +          && sqlite3_stricmp(z0,"user")==0 ){
          506  +        rc = SQLITE_DENY;
          507  +      }else if( (db.protectMask & PROTECT_CONFIG)!=0 &&
          508  +               (sqlite3_stricmp(z0,"config")==0 ||
          509  +                sqlite3_stricmp(z0,"global_config")==0) ){
          510  +        rc = SQLITE_DENY;
          511  +      }else if( (db.protectMask & PROTECT_SENSITIVE)!=0 &&
          512  +                sqlite3_stricmp(z0,"global_config")==0 ){
          513  +        rc = SQLITE_DENY;
          514  +      }else if( (db.protectMask & PROTECT_READONLY)!=0
          515  +                && sqlite3_stricmp(z2,"temp")!=0 ){
          516  +        rc = SQLITE_DENY;
          517  +      }
          518  +      break;
          519  +    }
          520  +    case SQLITE_DROP_TEMP_TRIGGER: {
          521  +      /* Do not allow the triggers that enforce PROTECT_SENSITIVE
          522  +      ** to be dropped */
          523  +      rc = SQLITE_DENY;
          524  +      break;
          525  +    }
          526  +  }
          527  +  if( db.xAuth && rc==SQLITE_OK ){
          528  +    rc = db.xAuth(db.pAuthArg, eCode, z0, z1, z2, z3);
          529  +  }
          530  +  return rc;
          531  +}
   324    532   
   325    533   /*
   326    534   ** Set or unset the query authorizer callback function
   327    535   */
   328    536   void db_set_authorizer(
   329    537     int(*xAuth)(void*,int,const char*,const char*,const char*,const char*),
   330    538     void *pArg,
   331    539     const char *zName /* for tracing */
   332    540   ){
   333    541     if( db.xAuth ){
   334    542       fossil_panic("multiple active db_set_authorizer() calls");
   335    543     }
   336         -  if( g.db ) sqlite3_set_authorizer(g.db, xAuth, pArg);
   337    544     db.xAuth = xAuth;
   338    545     db.pAuthArg = pArg;
   339    546     db.zAuthName = zName;
   340    547     if( g.fSqlTrace ) fossil_trace("-- set authorizer %s\n", zName);
   341    548   }
   342    549   void db_clear_authorizer(void){
   343    550     if( db.zAuthName && g.fSqlTrace ){
   344    551       fossil_trace("-- discontinue authorizer %s\n", db.zAuthName);
   345    552     }
   346         -  if( g.db ) sqlite3_set_authorizer(g.db, 0, 0);
   347    553     db.xAuth = 0;
   348    554     db.pAuthArg = 0;
          555  +  db.zAuthName = 0;
   349    556   }
   350    557   
   351    558   #if INTERFACE
   352    559   /*
   353    560   ** Possible flags to db_vprepare
   354    561   */
   355    562   #define DB_PREPARE_IGNORE_ERROR  0x001  /* Suppress errors */
................................................................................
   361    568   ** If the input string contains multiple SQL statements, only the first
   362    569   ** one is processed.  All statements beyond the first are silently ignored.
   363    570   */
   364    571   int db_vprepare(Stmt *pStmt, int flags, const char *zFormat, va_list ap){
   365    572     int rc;
   366    573     int prepFlags = 0;
   367    574     char *zSql;
          575  +  const char *zExtra = 0;
   368    576     blob_zero(&pStmt->sql);
   369    577     blob_vappendf(&pStmt->sql, zFormat, ap);
   370    578     va_end(ap);
   371    579     zSql = blob_str(&pStmt->sql);
   372    580     db.nPrepare++;
   373    581     if( flags & DB_PREPARE_PERSISTENT ){
   374    582       prepFlags = SQLITE_PREPARE_PERSISTENT;
   375    583     }
   376         -  rc = sqlite3_prepare_v3(g.db, zSql, -1, prepFlags, &pStmt->pStmt, 0);
          584  +  rc = sqlite3_prepare_v3(g.db, zSql, -1, prepFlags, &pStmt->pStmt, &zExtra);
   377    585     if( rc!=0 && (flags & DB_PREPARE_IGNORE_ERROR)==0 ){
   378    586       db_err("%s\n%s", sqlite3_errmsg(g.db), zSql);
          587  +  }else if( zExtra && !fossil_all_whitespace(zExtra) ){
          588  +    db_err("surplus text follows SQL: \"%s\"", zExtra);
   379    589     }
   380    590     pStmt->pNext = db.pAllStmt;
   381    591     pStmt->pPrev = 0;
   382    592     if( db.pAllStmt ) db.pAllStmt->pPrev = pStmt;
   383    593     db.pAllStmt = pStmt;
   384    594     pStmt->nStep = 0;
   385    595     pStmt->rc = rc;
................................................................................
   638    848     rc = db_reset(pStmt);
   639    849     db_check_result(rc, pStmt);
   640    850     return rc;
   641    851   }
   642    852   
   643    853   /*
   644    854   ** COMMAND: test-db-exec-error
          855  +** Usage: %fossil test-db-exec-error
   645    856   **
   646    857   ** Invoke the db_exec() interface with an erroneous SQL statement
   647    858   ** in order to verify the error handling logic.
   648    859   */
   649    860   void db_test_db_exec_cmd(void){
   650    861     Stmt err;
   651    862     db_find_and_open_repository(0,0);
   652    863     db_prepare(&err, "INSERT INTO repository.config(name) VALUES(NULL);");
   653    864     db_exec(&err);
   654    865   }
          866  +
          867  +/*
          868  +** COMMAND: test-db-prepare
          869  +** Usage: %fossil test-db-prepare ?OPTIONS? SQL
          870  +**
          871  +** Invoke db_prepare() on the SQL input.  Report any errors encountered.
          872  +** This command is used to verify error detection logic in the db_prepare()
          873  +** utility routine.
          874  +*/
          875  +void db_test_db_prepare(void){
          876  +  Stmt err;
          877  +  db_find_and_open_repository(0,0);
          878  +  verify_all_options();
          879  +  if( g.argc!=3 ) usage("?OPTIONS? SQL");
          880  +  db_prepare(&err, "%s", g.argv[2]/*safe-for-%s*/);
          881  +  db_finalize(&err);
          882  +}
   655    883   
   656    884   /*
   657    885   ** Print the output of one or more SQL queries on standard output.
   658    886   ** This routine is used for debugging purposes only.
   659    887   */
   660    888   int db_debug(const char *zSql, ...){
   661    889     Blob sql;
................................................................................
   878   1106     sqlite3 *xdb;
   879   1107     int rc;
   880   1108     const char *zSql;
   881   1109     va_list ap;
   882   1110   
   883   1111     xdb = db_open(zFileName ? zFileName : ":memory:");
   884   1112     sqlite3_exec(xdb, "BEGIN EXCLUSIVE", 0, 0, 0);
   885         -  if( db.xAuth ){
   886         -    sqlite3_set_authorizer(xdb, db.xAuth, db.pAuthArg);
   887         -  }
   888   1113     rc = sqlite3_exec(xdb, zSchema, 0, 0, 0);
   889   1114     if( rc!=SQLITE_OK ){
   890   1115       db_err("%s", sqlite3_errmsg(xdb));
   891   1116     }
   892   1117     va_start(ap, zSchema);
   893   1118     while( (zSql = va_arg(ap, const char*))!=0 ){
   894   1119       rc = sqlite3_exec(xdb, zSql, 0, 0, 0);
................................................................................
  1090   1315       sqlite3_result_error_nomem(context);
  1091   1316       return;
  1092   1317     }
  1093   1318     strcpy(zOut, zTemp = obscure((char*)zIn));
  1094   1319     fossil_free(zTemp);
  1095   1320     sqlite3_result_text(context, zOut, strlen(zOut), sqlite3_free);
  1096   1321   }
         1322  +
         1323  +/*
         1324  +** Return True if zName is a protected (a.k.a. "sensitive") setting.
         1325  +*/
         1326  +int db_setting_is_protected(const char *zName){
         1327  +  const Setting *pSetting = zName ? db_find_setting(zName,0) : 0;
         1328  +  return pSetting!=0 && pSetting->sensitive!=0;
         1329  +}
         1330  +
         1331  +/*
         1332  +** Implement the protected_setting(X) SQL function.  This function returns
         1333  +** true if X is the name of a protected (security-sensitive) setting and
         1334  +** the db.protectSensitive flag is enabled.  It returns false otherwise.
         1335  +*/
         1336  +LOCAL void db_protected_setting_func(
         1337  +  sqlite3_context *context,
         1338  +  int argc,
         1339  +  sqlite3_value **argv
         1340  +){
         1341  +  const char *zSetting;
         1342  +  if( (db.protectMask & PROTECT_SENSITIVE)==0 ){
         1343  +    sqlite3_result_int(context, 0);
         1344  +    return;
         1345  +  }
         1346  +  zSetting = (const char*)sqlite3_value_text(argv[0]);
         1347  +  sqlite3_result_int(context, db_setting_is_protected(zSetting));
         1348  +}
  1097   1349   
  1098   1350   /*
  1099   1351   ** Register the SQL functions that are useful both to the internal
  1100   1352   ** representation and to the "fossil sql" command.
  1101   1353   */
  1102   1354   void db_add_aux_functions(sqlite3 *db){
  1103   1355     sqlite3_create_function(db, "checkin_mtime", 2, SQLITE_UTF8, 0,
................................................................................
  1120   1372                             capability_fullcap, 0, 0);
  1121   1373     sqlite3_create_function(db, "find_emailaddr", 1, SQLITE_UTF8, 0,
  1122   1374                             alert_find_emailaddr_func, 0, 0);
  1123   1375     sqlite3_create_function(db, "display_name", 1, SQLITE_UTF8, 0,
  1124   1376                             alert_display_name_func, 0, 0);
  1125   1377     sqlite3_create_function(db, "obscure", 1, SQLITE_UTF8, 0,
  1126   1378                             db_obscure, 0, 0);
         1379  +  sqlite3_create_function(db, "protected_setting", 1, SQLITE_UTF8, 0,
         1380  +                          db_protected_setting_func, 0, 0);
  1127   1381   }
  1128   1382   
  1129   1383   #if USE_SEE
  1130   1384   /*
  1131   1385   ** This is a pointer to the saved database encryption key string.
  1132   1386   */
  1133   1387   static char *zSavedKey = 0;
................................................................................
  1378   1632       db, "if_selected", 3, SQLITE_UTF8, 0, file_is_selected,0,0
  1379   1633     );
  1380   1634     if( g.fSqlTrace ) sqlite3_trace_v2(db, SQLITE_TRACE_PROFILE, db_sql_trace, 0);
  1381   1635     db_add_aux_functions(db);
  1382   1636     re_add_sql_func(db);  /* The REGEXP operator */
  1383   1637     foci_register(db);    /* The "files_of_checkin" virtual table */
  1384   1638     sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_FKEY, 0, &rc);
         1639  +  sqlite3_set_authorizer(db, db_top_authorizer, db);
  1385   1640     return db;
  1386   1641   }
  1387   1642   
  1388   1643   
  1389   1644   /*
  1390   1645   ** Detaches the zLabel database.
  1391   1646   */
................................................................................
  1813   2068     assert( g.zLocalRoot );
  1814   2069     if( zRepo==0 ){
  1815   2070       zRepo = db_lget("repository", 0);
  1816   2071       if( zRepo && !file_is_absolute_path(zRepo) ){
  1817   2072         char * zFree = zRepo;
  1818   2073         zRepo = mprintf("%s%s", g.zLocalRoot, zRepo);
  1819   2074         fossil_free(zFree);
         2075  +      zFree = zRepo;
         2076  +      zRepo = file_canonical_name_dup(zFree);
         2077  +      fossil_free(zFree);
  1820   2078       }
  1821   2079     }
  1822   2080     return zRepo;
  1823   2081   }
  1824   2082   
  1825         -/*
  1826         -** Returns non-zero if the default value for the "allow-symlinks" setting
  1827         -** is "on".  When on Windows, this always returns false.
  1828         -*/
  1829         -int db_allow_symlinks_by_default(void){
  1830         -#if defined(_WIN32) || !defined(FOSSIL_LEGACY_ALLOW_SYMLINKS)
  1831         -  return 0;
  1832         -#else
  1833         -  return 1;
  1834         -#endif
  1835         -}
  1836         -
  1837   2083   /*
  1838   2084   ** Returns non-zero if support for symlinks is currently enabled.
  1839   2085   */
  1840   2086   int db_allow_symlinks(void){
  1841   2087     return g.allowSymlinks;
  1842   2088   }
  1843   2089   
................................................................................
  1875   2121       }
  1876   2122     }
  1877   2123     g.zRepositoryName = mprintf("%s", zDbName);
  1878   2124     db_open_or_attach(g.zRepositoryName, "repository");
  1879   2125     g.repositoryOpen = 1;
  1880   2126     sqlite3_file_control(g.db, "repository", SQLITE_FCNTL_DATA_VERSION,
  1881   2127                          &g.iRepoDataVers);
         2128  +
  1882   2129     /* Cache "allow-symlinks" option, because we'll need it on every stat call */
  1883         -  g.allowSymlinks = db_get_boolean("allow-symlinks",
  1884         -                                   db_allow_symlinks_by_default());
         2130  +  g.allowSymlinks = db_get_boolean("allow-symlinks",0);
         2131  +
  1885   2132     g.zAuxSchema = db_get("aux-schema","");
  1886   2133     g.eHashPolicy = db_get_int("hash-policy",-1);
  1887   2134     if( g.eHashPolicy<0 ){
  1888   2135       g.eHashPolicy = hname_default_policy();
  1889   2136       db_set_int("hash-policy", g.eHashPolicy, 0);
  1890   2137     }
  1891   2138   
................................................................................
  2167   2414     /* If the localdb has a lot of unused free space,
  2168   2415     ** then VACUUM it as we shut down.
  2169   2416     */
  2170   2417     if( db_database_slot("localdb")>=0 ){
  2171   2418       int nFree = db_int(0, "PRAGMA localdb.freelist_count");
  2172   2419       int nTotal = db_int(0, "PRAGMA localdb.page_count");
  2173   2420       if( nFree>nTotal/4 ){
         2421  +      db_unprotect(PROTECT_ALL);
  2174   2422         db_multi_exec("VACUUM localdb;");
         2423  +      db_protect_pop();
  2175   2424       }
  2176   2425     }
  2177   2426   
  2178   2427     if( g.db ){
  2179   2428       int rc;
  2180   2429       sqlite3_wal_checkpoint(g.db, 0);
  2181   2430       rc = sqlite3_close(g.db);
................................................................................
  2185   2434           fossil_warning("unfinalized SQL statement: [%s]", sqlite3_sql(pStmt));
  2186   2435         }
  2187   2436       }
  2188   2437       g.db = 0;
  2189   2438     }
  2190   2439     g.repositoryOpen = 0;
  2191   2440     g.localOpen = 0;
         2441  +  db.bProtectTriggers = 0;
  2192   2442     assert( g.dbConfig==0 );
  2193   2443     assert( g.zConfigDbName==0 );
  2194   2444     backoffice_run_if_needed();
  2195   2445   }
  2196   2446   
  2197   2447   /*
  2198   2448   ** Close the database as quickly as possible without unnecessary processing.
................................................................................
  2247   2497     }
  2248   2498     if( zUser==0 ){
  2249   2499       zUser = fossil_getenv("USERNAME");
  2250   2500     }
  2251   2501     if( zUser==0 ){
  2252   2502       zUser = "root";
  2253   2503     }
         2504  +  db_unprotect(PROTECT_USER);
  2254   2505     db_multi_exec(
  2255   2506        "INSERT OR IGNORE INTO user(login, info) VALUES(%Q,'')", zUser
  2256   2507     );
  2257   2508     db_multi_exec(
  2258   2509        "UPDATE user SET cap='s', pw=%Q"
  2259   2510        " WHERE login=%Q", fossil_random_password(10), zUser
  2260   2511     );
................................................................................
  2266   2517          "   VALUES('nobody','','gjorz','Nobody');"
  2267   2518          "INSERT OR IGNORE INTO user(login,pw,cap,info)"
  2268   2519          "   VALUES('developer','','ei','Dev');"
  2269   2520          "INSERT OR IGNORE INTO user(login,pw,cap,info)"
  2270   2521          "   VALUES('reader','','kptw','Reader');"
  2271   2522       );
  2272   2523     }
         2524  +  db_protect_pop();
  2273   2525   }
  2274   2526   
  2275   2527   /*
  2276   2528   ** Return a pointer to a string that contains the RHS of an IN operator
  2277   2529   ** that will select CONFIG table names that are in the list of control
  2278   2530   ** settings.
  2279   2531   */
................................................................................
  2317   2569     const char *zInitialDate,    /* Initial date of repository. (ex: "now") */
  2318   2570     const char *zDefaultUser     /* Default user for the repository */
  2319   2571   ){
  2320   2572     char *zDate;
  2321   2573     Blob hash;
  2322   2574     Blob manifest;
  2323   2575   
         2576  +  db_unprotect(PROTECT_ALL);
  2324   2577     db_set("content-schema", CONTENT_SCHEMA, 0);
  2325   2578     db_set("aux-schema", AUX_SCHEMA_MAX, 0);
  2326   2579     db_set("rebuilt", get_version(), 0);
  2327   2580     db_set("admin-log", "1", 0);
  2328   2581     db_set("access-log", "1", 0);
  2329   2582     db_multi_exec(
  2330   2583         "INSERT INTO config(name,value,mtime)"
................................................................................
  2375   2628         "  mtime = (SELECT u2.mtime FROM settingSrc.user u2"
  2376   2629         "           WHERE u2.login = user.login),"
  2377   2630         "  photo = (SELECT u2.photo FROM settingSrc.user u2"
  2378   2631         "           WHERE u2.login = user.login)"
  2379   2632         " WHERE user.login IN ('anonymous','nobody','developer','reader');"
  2380   2633       );
  2381   2634     }
         2635  +  db_protect_pop();
  2382   2636   
  2383   2637     if( zInitialDate ){
  2384   2638       int rid;
  2385   2639       blob_zero(&manifest);
  2386   2640       blob_appendf(&manifest, "C initial\\sempty\\scheck-in\n");
  2387   2641       zDate = date_in_standard_format(zInitialDate);
  2388   2642       blob_appendf(&manifest, "D %s\n", zDate);
................................................................................
  2871   3125       z = fossil_strdup(zDefault);
  2872   3126     }else if( zFormat!=0 ){
  2873   3127       z = db_text(0, "SELECT strftime(%Q,%Q,'unixepoch');", zFormat, z);
  2874   3128     }
  2875   3129     return z;
  2876   3130   }
  2877   3131   void db_set(const char *zName, const char *zValue, int globalFlag){
         3132  +  db_assert_protection_off_or_not_sensitive(zName);
         3133  +  db_unprotect(PROTECT_CONFIG);
  2878   3134     db_begin_transaction();
  2879   3135     if( globalFlag ){
  2880   3136       db_swap_connections();
  2881   3137       db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%Q)",
  2882   3138                      zName, zValue);
  2883   3139       db_swap_connections();
  2884   3140     }else{
................................................................................
  2885   3141       db_multi_exec("REPLACE INTO config(name,value,mtime) VALUES(%Q,%Q,now())",
  2886   3142                      zName, zValue);
  2887   3143     }
  2888   3144     if( globalFlag && g.repositoryOpen ){
  2889   3145       db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
  2890   3146     }
  2891   3147     db_end_transaction(0);
         3148  +  db_protect_pop();
  2892   3149   }
  2893   3150   void db_unset(const char *zName, int globalFlag){
  2894   3151     db_begin_transaction();
         3152  +  db_unprotect(PROTECT_CONFIG);
  2895   3153     if( globalFlag ){
  2896   3154       db_swap_connections();
  2897   3155       db_multi_exec("DELETE FROM global_config WHERE name=%Q", zName);
  2898   3156       db_swap_connections();
  2899   3157     }else{
  2900   3158       db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
  2901   3159     }
  2902   3160     if( globalFlag && g.repositoryOpen ){
  2903   3161       db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
  2904   3162     }
         3163  +  db_protect_pop();
  2905   3164     db_end_transaction(0);
  2906   3165   }
  2907   3166   int db_is_global(const char *zName){
  2908   3167     int rc = 0;
  2909   3168     if( g.zConfigDbName ){
  2910   3169       db_swap_connections();
  2911   3170       rc = db_exists("SELECT 1 FROM global_config WHERE name=%Q", zName);
................................................................................
  2931   3190       db_swap_connections();
  2932   3191       v = db_int(dflt, "SELECT value FROM global_config WHERE name=%Q", zName);
  2933   3192       db_swap_connections();
  2934   3193     }
  2935   3194     return v;
  2936   3195   }
  2937   3196   void db_set_int(const char *zName, int value, int globalFlag){
         3197  +  db_assert_protection_off_or_not_sensitive(zName);
         3198  +  db_unprotect(PROTECT_CONFIG);
  2938   3199     if( globalFlag ){
  2939   3200       db_swap_connections();
  2940   3201       db_multi_exec("REPLACE INTO global_config(name,value) VALUES(%Q,%d)",
  2941   3202                     zName, value);
  2942   3203       db_swap_connections();
  2943   3204     }else{
  2944   3205       db_multi_exec("REPLACE INTO config(name,value,mtime) VALUES(%Q,%d,now())",
  2945   3206                     zName, value);
  2946   3207     }
  2947   3208     if( globalFlag && g.repositoryOpen ){
  2948   3209       db_multi_exec("DELETE FROM config WHERE name=%Q", zName);
  2949   3210     }
         3211  +  db_protect_pop();
  2950   3212   }
  2951   3213   int db_get_boolean(const char *zName, int dflt){
  2952   3214     char *zVal = db_get(zName, dflt ? "on" : "off");
  2953   3215     if( is_truth(zVal) ){
  2954   3216       dflt = 1;
  2955   3217     }else if( is_false(zVal) ){
  2956   3218       dflt = 0;
  2957   3219     }
  2958   3220     fossil_free(zVal);
  2959   3221     return dflt;
  2960   3222   }
  2961         -#ifdef FOSSIL_LEGACY_ALLOW_SYMLINKS
  2962   3223   int db_get_versioned_boolean(const char *zName, int dflt){
  2963   3224     char *zVal = db_get_versioned(zName, 0);
  2964   3225     if( zVal==0 ) return dflt;
  2965   3226     if( is_truth(zVal) ) return 1;
  2966   3227     if( is_false(zVal) ) return 0;
  2967   3228     return dflt;
  2968   3229   }
  2969         -#endif /* FOSSIL_LEGACY_ALLOW_SYMLINKS */
  2970   3230   char *db_lget(const char *zName, const char *zDefault){
  2971   3231     return db_text(zDefault,
  2972   3232                    "SELECT value FROM vvar WHERE name=%Q", zName);
  2973   3233   }
  2974   3234   void db_lset(const char *zName, const char *zValue){
  2975   3235     db_multi_exec("REPLACE INTO vvar(name,value) VALUES(%Q,%Q)", zName, zValue);
  2976   3236   }
................................................................................
  3074   3334       if( !g.localOpen ) return;
  3075   3335       zName = db_repository_filename();
  3076   3336     }
  3077   3337     file_canonical_name(zName, &full, 0);
  3078   3338     (void)filename_collation();  /* Initialize before connection swap */
  3079   3339     db_swap_connections();
  3080   3340     zRepoSetting = mprintf("repo:%q", blob_str(&full));
         3341  +  
         3342  +  db_unprotect(PROTECT_CONFIG);
  3081   3343     db_multi_exec(
  3082   3344        "DELETE FROM global_config WHERE name %s = %Q;",
  3083   3345        filename_collation(), zRepoSetting
  3084   3346     );
  3085   3347     db_multi_exec(
  3086   3348        "INSERT OR IGNORE INTO global_config(name,value)"
  3087   3349        "VALUES(%Q,1);",
  3088   3350        zRepoSetting
  3089   3351     );
         3352  +  db_protect_pop();
  3090   3353     fossil_free(zRepoSetting);
  3091   3354     if( g.localOpen && g.zLocalRoot && g.zLocalRoot[0] ){
  3092   3355       Blob localRoot;
  3093   3356       file_canonical_name(g.zLocalRoot, &localRoot, 1);
  3094   3357       zCkoutSetting = mprintf("ckout:%q", blob_str(&localRoot));
         3358  +    db_unprotect(PROTECT_CONFIG);
  3095   3359       db_multi_exec(
  3096   3360          "DELETE FROM global_config WHERE name %s = %Q;",
  3097   3361          filename_collation(), zCkoutSetting
  3098   3362       );
  3099   3363       db_multi_exec(
  3100   3364         "REPLACE INTO global_config(name, value)"
  3101   3365         "VALUES(%Q,%Q);",
................................................................................
  3107   3371           filename_collation(), zCkoutSetting
  3108   3372       );
  3109   3373       db_optional_sql("repository",
  3110   3374           "REPLACE INTO config(name,value,mtime)"
  3111   3375           "VALUES(%Q,1,now());",
  3112   3376           zCkoutSetting
  3113   3377       );
         3378  +    db_protect_pop();
  3114   3379       fossil_free(zCkoutSetting);
  3115   3380       blob_reset(&localRoot);
  3116   3381     }else{
  3117   3382       db_swap_connections();
  3118   3383     }
  3119   3384     blob_reset(&full);
  3120   3385   }
................................................................................
  3145   3410   ** "new-name" term means that the cloned repository will be called
  3146   3411   ** "new-name.fossil".
  3147   3412   **
  3148   3413   ** Options:
  3149   3414   **   --empty           Initialize checkout as being empty, but still connected
  3150   3415   **                     with the local repository. If you commit this checkout,
  3151   3416   **                     it will become a new "initial" commit in the repository.
  3152         -**   --force           Continue with the open even if the working directory is
         3417  +**   -f|--force        Continue with the open even if the working directory is
  3153   3418   **                     not empty.
  3154   3419   **   --force-missing   Force opening a repository with missing content
  3155   3420   **   --keep            Only modify the manifest and manifest.uuid files
  3156   3421   **   --nested          Allow opening a repository inside an opened checkout
  3157   3422   **   --repodir DIR     If REPOSITORY is a URI that will be cloned, store
  3158   3423   **                     the clone in DIR rather than in "."
  3159   3424   **   --setmtime        Set timestamps of all files to match their SCM-side
................................................................................
  3165   3430   ** See also: [[close]], [[clone]]
  3166   3431   */
  3167   3432   void cmd_open(void){
  3168   3433     int emptyFlag;
  3169   3434     int keepFlag;
  3170   3435     int forceMissingFlag;
  3171   3436     int allowNested;
  3172         -#ifdef FOSSIL_LEGACY_ALLOW_SYMLINKS
  3173         -  int allowSymlinks;
  3174         -#endif
  3175   3437     int setmtimeFlag;              /* --setmtime.  Set mtimes on files */
  3176   3438     int bForce = 0;                /* --force.  Open even if non-empty dir */
  3177   3439     static char *azNewArgv[] = { 0, "checkout", "--prompt", 0, 0, 0, 0 };
  3178   3440     const char *zWorkDir;          /* --workdir value */
  3179   3441     const char *zRepo = 0;         /* Name of the repository file */
  3180   3442     const char *zRepoDir = 0;      /* --repodir value */
  3181   3443     char *zPwd;                    /* Initial working directory */
................................................................................
  3185   3447     emptyFlag = find_option("empty",0,0)!=0;
  3186   3448     keepFlag = find_option("keep",0,0)!=0;
  3187   3449     forceMissingFlag = find_option("force-missing",0,0)!=0;
  3188   3450     allowNested = find_option("nested",0,0)!=0;
  3189   3451     setmtimeFlag = find_option("setmtime",0,0)!=0;
  3190   3452     zWorkDir = find_option("workdir",0,1);
  3191   3453     zRepoDir = find_option("repodir",0,1);
  3192         -  bForce = find_option("force",0,0)!=0;  
         3454  +  bForce = find_option("force","f",0)!=0;  
  3193   3455     zPwd = file_getcwd(0,0);
  3194   3456     
  3195   3457   
  3196   3458     /* We should be done with options.. */
  3197   3459     verify_all_options();
  3198   3460   
  3199   3461     if( g.argc!=3 && g.argc!=4 ){
................................................................................
  3224   3486       }
  3225   3487       if( file_chdir(zWorkDir, 0) ){
  3226   3488         fossil_fatal("unable to make %s the working directory", zWorkDir);
  3227   3489       }
  3228   3490     }
  3229   3491     if( keepFlag==0 && bForce==0 && file_directory_size(".", 0, 1)>0 ){
  3230   3492       fossil_fatal("directory %s is not empty\n"
  3231         -                 "use the --force option to override", file_getcwd(0,0));
         3493  +                 "use the -f or --force option to override", file_getcwd(0,0));
  3232   3494     }
  3233   3495   
  3234   3496     if( db_open_local_v2(0, allowNested) ){
  3235   3497       fossil_fatal("there is already an open tree at %s", g.zLocalRoot);
  3236   3498     }
  3237   3499   
  3238   3500     /* If REPOSITORY looks like a URI, then try to clone it first */
................................................................................
  3278   3540       if( g.argc==4 ){
  3279   3541         g.zOpenRevision = g.argv[3];
  3280   3542       }else if( db_exists("SELECT 1 FROM event WHERE type='ci'") ){
  3281   3543         g.zOpenRevision = db_get("main-branch", 0);
  3282   3544       }
  3283   3545     }
  3284   3546   
  3285         -#ifdef FOSSIL_LEGACY_ALLOW_SYMLINKS
  3286         -  if( g.zOpenRevision ){
  3287         -    /* Since the repository is open and we know the revision now,
  3288         -    ** refresh the allow-symlinks flag.  Since neither the local
  3289         -    ** checkout nor the configuration database are open at this
  3290         -    ** point, this should always return the versioned setting,
  3291         -    ** if any, or the default value, which is negative one.  The
  3292         -    ** value negative one, in this context, means that the code
  3293         -    ** below should fallback to using the setting value from the
  3294         -    ** repository or global configuration databases only. */
  3295         -    allowSymlinks = db_get_versioned_boolean("allow-symlinks", -1);
  3296         -  }else{
  3297         -    allowSymlinks = -1; /* Use non-versioned settings only. */
  3298         -  }
  3299         -#endif
  3300   3547   
  3301   3548   #if defined(_WIN32) || defined(__CYGWIN__)
  3302   3549   # define LOCALDB_NAME "./_FOSSIL_"
  3303   3550   #else
  3304   3551   # define LOCALDB_NAME "./.fslckout"
  3305   3552   #endif
  3306   3553     db_init_database(LOCALDB_NAME, zLocalSchema, zLocalSchemaVmerge,
  3307   3554   #ifdef FOSSIL_LOCAL_WAL
  3308   3555                      "COMMIT; PRAGMA journal_mode=WAL; BEGIN;",
  3309   3556   #endif
  3310   3557                      (char*)0);
  3311   3558     db_delete_on_failure(LOCALDB_NAME);
  3312   3559     db_open_local(0);
  3313         -#ifdef FOSSIL_LEGACY_ALLOW_SYMLINKS
  3314         -  if( allowSymlinks>=0 ){
  3315         -    /* Use the value from the versioned setting, which was read
  3316         -    ** prior to opening the local checkout (i.e. which is most
  3317         -    ** likely empty and does not actually contain any versioned
  3318         -    ** setting files yet).  Normally, this value would be given
  3319         -    ** first priority within db_get_boolean(); however, this is
  3320         -    ** a special case because we know the on-disk files may not
  3321         -    ** exist yet. */
  3322         -    g.allowSymlinks = allowSymlinks;
  3323         -  }else{
  3324         -    /* Since the local checkout may not have any files at this
  3325         -    ** point, this will probably be the setting value from the
  3326         -    ** repository or global configuration databases. */
  3327         -    g.allowSymlinks = db_get_boolean("allow-symlinks",
  3328         -                                     db_allow_symlinks_by_default());
  3329         -  }
  3330         -#endif /* FOSSIL_LEGACY_ALLOW_SYMLINKS */
  3331   3560     db_lset("repository", zRepo);
  3332   3561     db_record_repository_filename(zRepo);
  3333   3562     db_set_checkout(0);
  3334   3563     azNewArgv[0] = g.argv[0];
  3335   3564     g.argv = azNewArgv;
  3336   3565     if( !emptyFlag ){
  3337   3566       g.argc = 3;
................................................................................
  3416   3645   */
  3417   3646   struct Setting {
  3418   3647     const char *name;     /* Name of the setting */
  3419   3648     const char *var;      /* Internal variable name used by db_set() */
  3420   3649     int width;            /* Width of display.  0 for boolean values and
  3421   3650                           ** negative for values which should not appear
  3422   3651                           ** on the /setup_settings page. */
  3423         -  int versionable;      /* Is this setting versionable? */
  3424         -  int forceTextArea;    /* Force using a text area for display? */
         3652  +  char versionable;     /* Is this setting versionable? */
         3653  +  char forceTextArea;   /* Force using a text area for display? */
         3654  +  char sensitive;       /* True if this a security-sensitive setting */
  3425   3655     const char *def;      /* Default value */
  3426   3656   };
  3427   3657   #endif /* INTERFACE */
  3428   3658   
  3429   3659   /*
  3430   3660   ** SETTING: access-log      boolean default=off
  3431   3661   **
................................................................................
  3435   3665   */
  3436   3666   /*
  3437   3667   ** SETTING: admin-log       boolean default=off
  3438   3668   **
  3439   3669   ** When the admin-log setting is enabled, configuration changes are recorded
  3440   3670   ** in the "admin_log" table of the repository.
  3441   3671   */
  3442         -#if !defined(FOSSIL_LEGACY_ALLOW_SYMLINKS)
  3443         -/*
  3444         -** SETTING: allow-symlinks  boolean default=off
  3445         -**
  3446         -** When allow-symlinks is OFF (which is the default and recommended setting)
  3447         -** symbolic links are treated like text files that contain a single line of
  3448         -** content which is the name of their target.  If allow-symlinks is ON,
  3449         -** the symbolic links are actually followed.
  3450         -**
  3451         -** The use of symbolic links is dangerous.  If you checkout a maliciously
  3452         -** crafted checkin that contains symbolic links, it is possible that files
  3453         -** outside of the working directory might be overwritten.
  3454         -**
  3455         -** Keep this setting OFF unless you have a very good reason to turn it
  3456         -** on and you implicitly trust the integrity of the repositories you
  3457         -** open.
  3458         -*/
  3459         -#endif
  3460         -#if defined(_WIN32) && defined(FOSSIL_LEGACY_ALLOW_SYMLINKS)
  3461         -/*
  3462         -** SETTING: allow-symlinks  boolean default=off versionable
  3463         -**
  3464         -** When allow-symlinks is OFF, symbolic links in the repository are followed
  3465         -** and treated no differently from real files.  When allow-symlinks is ON,
  3466         -** the object to which the symbolic link points is ignored, and the content
  3467         -** of the symbolic link that is stored in the repository is the name of the
  3468         -** object to which the symbolic link points.
  3469         -*/
  3470         -#endif
  3471         -#if !defined(_WIN32) && defined(FOSSIL_LEGACY_ALLOW_SYMLINKS)
  3472         -/*
  3473         -** SETTING: allow-symlinks  boolean default=on versionable
  3474         -**
  3475         -** When allow-symlinks is OFF, symbolic links in the repository are followed
  3476         -** and treated no differently from real files.  When allow-symlinks is ON,
  3477         -** the object to which the symbolic link points is ignored, and the content
  3478         -** of the symbolic link that is stored in the repository is the name of the
  3479         -** object to which the symbolic link points.
  3480         -*/
  3481         -#endif
         3672  +/*
         3673  +** SETTING: allow-symlinks  boolean default=off sensitive
         3674  +**
         3675  +** When allow-symlinks is OFF, Fossil does not see symbolic links 
         3676  +** (a.k.a "symlinks") on disk as a separate class of object.  Instead Fossil
         3677  +** sees the object that the symlink points to.  Fossil will only manage files
         3678  +** and directories, not symlinks.  When a symlink is added to a repository,
         3679  +** the object that the symlink points to is added, not the symlink itself.
         3680  +**
         3681  +** When allow-symlinks is ON, Fossil sees symlinks on disk as a separate
         3682  +** object class that is distinct from files and directories.  When a symlink
         3683  +** is added to a repository, Fossil stores the target filename. In other
         3684  +** words, Fossil stores the symlink itself, not the object that the symlink
         3685  +** points to.
         3686  +**
         3687  +** Symlinks are not cross-platform. They are not available on all
         3688  +** operating systems and file systems. Hence the allow-symlinks setting is
         3689  +** OFF by default, for portability.
         3690  +*/
  3482   3691   /*
  3483   3692   ** SETTING: auto-captcha    boolean default=on variable=autocaptcha
  3484   3693   ** If enabled, the /login page provides a button that will automatically
  3485   3694   ** fill in the captcha password.  This makes things easier for human users,
  3486   3695   ** at the expense of also making logins easier for malicious robots.
  3487   3696   */
  3488   3697   /*
................................................................................
  3528   3737   ** Backoffice processing does things such as delivering
  3529   3738   ** email notifications.  So if this setting is true, and if
  3530   3739   ** there is no cron job periodically running "fossil backoffice",
  3531   3740   ** email notifications and other work normally done by the
  3532   3741   ** backoffice will not occur.
  3533   3742   */
  3534   3743   /*
  3535         -** SETTING: backoffice-logfile width=40
         3744  +** SETTING: backoffice-logfile width=40 sensitive
  3536   3745   ** If backoffice-logfile is not an empty string and is a valid
  3537   3746   ** filename, then a one-line message is appended to that file
  3538   3747   ** every time the backoffice runs.  This can be used for debugging,
  3539   3748   ** to ensure that backoffice is running appropriately.
  3540   3749   */
  3541   3750   /*
  3542   3751   ** SETTING: binary-glob     width=40 versionable block-text
................................................................................
  3605   3814   ** The crnl-glob setting is a compatibility alias.
  3606   3815   */
  3607   3816   /*
  3608   3817   ** SETTING: crnl-glob       width=40 versionable block-text
  3609   3818   ** This is an alias for the crlf-glob setting.
  3610   3819   */
  3611   3820   /*
  3612         -** SETTING: default-perms   width=16 default=u
         3821  +** SETTING: default-perms   width=16 default=u sensitive
  3613   3822   ** Permissions given automatically to new users.  For more
  3614   3823   ** information on permissions see the Users page in Server
  3615   3824   ** Administration of the HTTP UI.
  3616   3825   */
  3617   3826   /*
  3618   3827   ** SETTING: diff-binary     boolean default=on
  3619   3828   ** If enabled, permit files that may be binary
  3620   3829   ** or that match the "binary-glob" setting to be used with
  3621   3830   ** external diff programs.  If disabled, skip these files.
  3622   3831   */
  3623   3832   /*
  3624         -** SETTING: diff-command    width=40
         3833  +** SETTING: diff-command    width=40 sensitive
  3625   3834   ** The value is an external command to run when performing a diff.
  3626   3835   ** If undefined, the internal text diff will be used.
  3627   3836   */
  3628   3837   /*
  3629   3838   ** SETTING: dont-push       boolean default=off
  3630   3839   ** If enabled, prevent this repository from pushing from client to
  3631   3840   ** server.  This can be used as an extra precaution to prevent
................................................................................
  3632   3841   ** accidental pushes to a public server from a private clone.
  3633   3842   */
  3634   3843   /*
  3635   3844   ** SETTING: dotfiles        boolean versionable default=off
  3636   3845   ** If enabled, include --dotfiles option for all compatible commands.
  3637   3846   */
  3638   3847   /*
  3639         -** SETTING: editor          width=32
         3848  +** SETTING: editor          width=32 sensitive
  3640   3849   ** The value is an external command that will launch the
  3641   3850   ** text editor command used for check-in comments.
  3642   3851   */
  3643   3852   /*
  3644   3853   ** SETTING: empty-dirs      width=40 versionable block-text
  3645   3854   ** The value is a comma or newline-separated list of pathnames. On
  3646   3855   ** update and checkout commands, if no file or directory
................................................................................
  3675   3884   ** A comma- or newline-separated list of globs of filenames
  3676   3885   ** which are allowed to be edited using the /fileedit page.
  3677   3886   ** An empty list prohibits editing via that page. Note that
  3678   3887   ** it cannot edit binary files, so the list should not
  3679   3888   ** contain any globs for, e.g., images or PDFs.
  3680   3889   */
  3681   3890   /*
  3682         -** SETTING: gdiff-command    width=40 default=gdiff
         3891  +** SETTING: gdiff-command    width=40 default=gdiff sensitive
  3683   3892   ** The value is an external command to run when performing a graphical
  3684   3893   ** diff. If undefined, text diff will be used.
  3685   3894   */
  3686   3895   /*
  3687         -** SETTING: gmerge-command   width=40
         3896  +** SETTING: gmerge-command   width=40 sensitive
  3688   3897   ** The value is a graphical merge conflict resolver command operating
  3689   3898   ** on four files.  Examples:
  3690   3899   **
  3691   3900   **     kdiff3 "%baseline" "%original" "%merge" -o "%output"
  3692   3901   **     xxdiff "%original" "%baseline" "%merge" -M "%output"
  3693   3902   **     meld "%baseline" "%original" "%merge" "%output"
  3694   3903   */
................................................................................
  3815   4024   ** SETTING: mv-rm-files      boolean default=off
  3816   4025   ** If enabled, the "mv" and "rename" commands will also move
  3817   4026   ** the associated files within the checkout -AND- the "rm"
  3818   4027   ** and "delete" commands will also remove the associated
  3819   4028   ** files from within the checkout.
  3820   4029   */
  3821   4030   /*
  3822         -** SETTING: pgp-command      width=40
         4031  +** SETTING: pgp-command      width=40 sensitive
  3823   4032   ** Command used to clear-sign manifests at check-in.
  3824   4033   ** Default value is "gpg --clearsign -o"
  3825   4034   */
  3826   4035   /*
  3827   4036   ** SETTING: forbid-delta-manifests    boolean default=off
  3828   4037   ** If enabled on a client, new delta manifests are prohibited on
  3829   4038   ** commits.  If enabled on a server, whenever a client attempts
................................................................................
  3875   4084   ** have a non-zero "repolist-skin" setting then the repository list is
  3876   4085   ** displayed using unadorned HTML ("skinless").
  3877   4086   **
  3878   4087   ** If repolist-skin has a value of 2, then the repository is omitted from
  3879   4088   ** the list in use cases 1 through 4, but not for 5 and 6.
  3880   4089   */
  3881   4090   /*
  3882         -** SETTING: self-register    boolean default=off
         4091  +** SETTING: self-register    boolean default=off sensitive
  3883   4092   ** Allow users to register themselves through the HTTP UI.
  3884   4093   ** This is useful if you want to see other names than
  3885   4094   ** "Anonymous" in e.g. ticketing system. On the other hand
  3886   4095   ** users can not be deleted.
  3887   4096   */
  3888   4097   /*
  3889         -** SETTING: ssh-command      width=40
         4098  +** SETTING: ssh-command      width=40 sensitive
  3890   4099   ** The command used to talk to a remote machine with  the "ssh://" protocol.
  3891   4100   */
  3892   4101   /*
  3893         -** SETTING: ssl-ca-location  width=40
         4102  +** SETTING: ssl-ca-location  width=40 sensitive
  3894   4103   ** The full pathname to a file containing PEM encoded
  3895   4104   ** CA root certificates, or a directory of certificates
  3896   4105   ** with filenames formed from the certificate hashes as
  3897   4106   ** required by OpenSSL.
  3898   4107   **
  3899   4108   ** If set, this will override the OS default list of
  3900   4109   ** OpenSSL CAs. If unset, the default list will be used.
  3901   4110   ** Some platforms may add additional certificates.
  3902   4111   ** Checking your platform behaviour is required if the
  3903   4112   ** exact contents of the CA root is critical for your
  3904   4113   ** application.
  3905   4114   */
  3906   4115   /*
  3907         -** SETTING: ssl-identity     width=40
         4116  +** SETTING: ssl-identity     width=40 sensitive
  3908   4117   ** The full pathname to a file containing a certificate
  3909   4118   ** and private key in PEM format. Create by concatenating
  3910   4119   ** the certificate and private key files.
  3911   4120   **
  3912   4121   ** This identity will be presented to SSL servers to
  3913   4122   ** authenticate this client, in addition to the normal
  3914   4123   ** password authentication.
  3915   4124   */
  3916   4125   #ifdef FOSSIL_ENABLE_TCL
  3917   4126   /*
  3918         -** SETTING: tcl              boolean default=off
         4127  +** SETTING: tcl              boolean default=off sensitive
  3919   4128   ** If enabled Tcl integration commands will be added to the TH1
  3920   4129   ** interpreter, allowing arbitrary Tcl expressions and
  3921   4130   ** scripts to be evaluated from TH1.  Additionally, the Tcl
  3922   4131   ** interpreter will be able to evaluate arbitrary TH1
  3923   4132   ** expressions and scripts.
  3924   4133   */
  3925   4134   /*
  3926         -** SETTING: tcl-setup        width=40 block-text
         4135  +** SETTING: tcl-setup        width=40 block-text sensitive
  3927   4136   ** This is the setup script to be evaluated after creating
  3928   4137   ** and initializing the Tcl interpreter.  By default, this
  3929   4138   ** is empty and no extra setup is performed.
  3930   4139   */
  3931   4140   #endif /* FOSSIL_ENABLE_TCL */
  3932   4141   /*
  3933         -** SETTING: tclsh            width=80 default=tclsh
         4142  +** SETTING: tclsh            width=80 default=tclsh sensitive
  3934   4143   ** Name of the external TCL interpreter used for such things
  3935   4144   ** as running the GUI diff viewer launched by the --tk option
  3936   4145   ** of the various "diff" commands.
  3937   4146   */
  3938   4147   #ifdef FOSSIL_ENABLE_TH1_DOCS
  3939   4148   /*
  3940         -** SETTING: th1-docs         boolean default=off
         4149  +** SETTING: th1-docs         boolean default=off sensitive
  3941   4150   ** If enabled, this allows embedded documentation files to contain
  3942   4151   ** arbitrary TH1 scripts that are evaluated on the server.  If native
  3943   4152   ** Tcl integration is also enabled, this setting has the
  3944   4153   ** potential to allow anybody with check-in privileges to
  3945   4154   ** do almost anything that the associated operating system
  3946   4155   ** user account could do.  Extreme caution should be used
  3947   4156   ** when enabling this setting.
................................................................................
  3990   4199   ** SETTING: uv-sync          boolean default=off
  3991   4200   ** If true, automatically send unversioned files as part
  3992   4201   ** of a "fossil clone" or "fossil sync" command.  The
  3993   4202   ** default is false, in which case the -u option is
  3994   4203   ** needed to clone or sync unversioned files.
  3995   4204   */
  3996   4205   /*
  3997         -** SETTING: web-browser      width=30
         4206  +** SETTING: web-browser      width=30 sensitive
  3998   4207   ** A shell command used to launch your preferred
  3999   4208   ** web browser when given a URL as an argument.
  4000   4209   ** Defaults to "start" on windows, "open" on Mac,
  4001   4210   ** and "firefox" on Unix.
  4002   4211   */
  4003   4212   
  4004   4213   /*
................................................................................
  4116   4325         }
  4117   4326         if( globalFlag && isManifest ){
  4118   4327           fossil_fatal("cannot set 'manifest' globally");
  4119   4328         }
  4120   4329         if( unsetFlag ){
  4121   4330           db_unset(pSetting->name, globalFlag);
  4122   4331         }else{
         4332  +        db_protect_only(PROTECT_NONE);
  4123   4333           db_set(pSetting->name, g.argv[3], globalFlag);
         4334  +        db_protect_pop();
  4124   4335         }
  4125   4336         if( isManifest && g.localOpen ){
  4126   4337           manifest_to_disk(db_lget_int("checkout", 0));
  4127   4338         }
  4128   4339       }else{
  4129   4340         while( pSetting->name ){
  4130   4341           if( exactFlag ){

Changes to src/default.css.

   438    438   }
   439    439   span.usertype:before {
   440    440     content:"'";
   441    441   }
   442    442   span.usertype:after {
   443    443     content:"'";
   444    444   }
   445         -div.selectedText {
   446         -  font-weight: bold;
   447         -  color: blue;
   448         -  background-color: #d5d5ff;
   449         -  border: 1px blue solid;
   450         -}
   451    445   p.missingPriv {
   452    446    color: blue;
   453    447   }
   454    448   span.wikiruleHead {
   455    449     font-weight: bold;
   456    450   }
   457    451   td.tktDspLabel {
................................................................................
   907    901   d='M4,5h4v1h-4zm0,2h4v1h-4z'/%3E%3Cpath style='fill:rgb(64,64,64)' \
   908    902   d='M5,3h5l3,3v7h-8z'/%3E%3Cpath style='fill:rgb(248,248,248)' \
   909    903   d='M10,4.4v1.6h1.6zm-4,-0.6h3v3h-3zm0,3h6v5.4h-6z'/%3E%3Cpath style='fill:rgb(80,128,208)' \
   910    904   d='M7,8h4v1h-4zm0,2h4v1h-4z'/%3E%3C/svg%3E");
   911    905     background-repeat: no-repeat;
   912    906     background-position: center;
   913    907     cursor: pointer;
          908  +}
          909  +.copy-button.disabled {
          910  +  filter: grayscale(1);
          911  +  opacity: 0.4;
   914    912   }
   915    913   .copy-button-flipped {
   916    914   /*Note: .16em is suitable for element grouping.*/
   917    915     margin-left: .16em;
   918    916     margin-right: 0;
   919    917   }
   920    918   .nobr {
................................................................................
   946    944     transition: max-height 0.25s ease-out;
   947    945   }
   948    946   .error {
   949    947     color: darkred;
   950    948     background: yellow;
   951    949   }
   952    950   .warning {
   953         -  color: darkred;
          951  +  color: black;
   954    952     background: yellow;
   955         -  opacity: 0.7;
   956    953   }
   957    954   .hidden {
   958         -  position: absolute;
   959         -  opacity: 0;
   960         -  pointer-events: none;
   961         -  display: none;
          955  +  /* The framework-wide way of hiding elements is to assign them this
          956  +     CSS class. To make them visible again, remove it. The !important
          957  +     qualifiers are unfortunate but sometimes necessary when hidden
          958  +     element has other classes which specify visibility-related
          959  +     options. */
          960  +  position: absolute !important;
          961  +  opacity: 0 !important;
          962  +  pointer-events: none !important;
          963  +  display: none !important;
   962    964   }
   963    965   input {
   964    966     max-width: 95%;
   965    967   }
   966    968   textarea {
   967    969     max-width: 95%;
   968    970   }
   969    971   img {
   970    972     max-width: 100%;
   971         -  height: auto;
   972    973   }
   973    974   hr {
   974    975     /* Needed to keep /dir README.txt from floating right in some skins */
   975    976     clear: both;
   976    977   }
   977    978   
   978    979   /**
................................................................................
   992    993     margin: 0;
   993    994     display: flex;
   994    995     flex-direction: column;
   995    996     border-width: 1px;
   996    997     border-style: outset;
   997    998     border-color: inherit;
   998    999   }
   999         -.tab-container > .tabs > .tab-panel,
  1000         -.tab-container > .tabs > fieldset.tab-wrapper {
  1001         -  align-self: stretch;
  1002         -  flex: 10 1 auto;
  1003         -  display: flex;
  1004         -  flex-direction: row;
  1005         -  border: 0;
  1006         -  padding: 0;
  1007         -  margin: 0;
  1008         -}
  1009         -.tab-container > .tabs > fieldset.tab-wrapper > .tab-panel{
         1000  +.tab-container > .tabs > .tab-panel {
  1010   1001     align-self: stretch;
  1011   1002     flex: 10 1 auto;
  1012   1003     display: block;
  1013   1004     border: 0;
  1014   1005     padding: 0;
  1015   1006     margin: 0;
  1016   1007   }
................................................................................
  1110   1101     font-size: 175%;
  1111   1102   }
  1112   1103   .font-size-200 {
  1113   1104     font-size: 200%;
  1114   1105   }
  1115   1106   
  1116   1107   /**
  1117         -   .input-with-label is intended to be a wrapper element which
  1118         -   contain both a LABEL tag and an INPUT or SELECT control.
  1119         -   The wrapper is "necessary", as opposed to placing the INPUT
  1120         -   in the LABEL, so that we can include multiple INPUT
  1121         -   elements (e.g. a set of radio buttons).
         1108  +   .input-with-label is intended to be a wrapper element which contain
         1109  +   both a LABEL tag and an INPUT or SELECT control.  The wrapper is
         1110  +   "necessary", as opposed to placing the INPUT in the LABEL, so that
         1111  +   we can include multiple INPUT elements (e.g. a set of radio
         1112  +   buttons). Note that these elements must sometimes be BLOCK elements
         1113  +   (e.g. DIV) so that certain nesting constructs are legal.
  1122   1114   */
  1123   1115   .input-with-label {
  1124         -  border: 1px inset #808080;
         1116  +  border: 1px inset rgba(128, 128, 128, 0.5);
  1125   1117     border-radius: 0.25em;
  1126         -  padding: 0.25em 0.4em;
         1118  +  padding: 0.1em;
  1127   1119     margin: 0 0.5em;
  1128   1120     display: inline-block;
  1129   1121     cursor: default;
         1122  +  white-space: nowrap;
  1130   1123   }
  1131   1124   .input-with-label > * {
  1132   1125     vertical-align: middle;
  1133   1126   }
  1134   1127   .input-with-label > label {
  1135   1128     display: inline; /* some skins set label display to block! */
         1129  +  cursor: pointer;
  1136   1130   }
  1137   1131   .input-with-label > input {
  1138   1132     margin: 0;
  1139   1133   }
  1140   1134   .input-with-label > button {
  1141   1135     margin: 0;
  1142   1136   }
................................................................................
  1145   1139   }
  1146   1140   .input-with-label > input[type=text] {
  1147   1141     margin: 0;
  1148   1142   }
  1149   1143   .input-with-label > textarea {
  1150   1144     margin: 0;
  1151   1145   }
         1146  +/* Browsers are unfortunately inconsistent in how they
         1147  +   align checkboxes and radio buttons, even if they're
         1148  +   given the same vertical-align value. 'middle' seems to
         1149  +   be the least bad option, rather than the ideal. */
  1152   1150   .input-with-label > input[type=checkbox] {
  1153         -  vertical-align: sub;
         1151  +  vertical-align: middle;
  1154   1152   }
  1155   1153   .input-with-label > input[type=radio] {
  1156         -  vertical-align: sub;
         1154  +  vertical-align: middle;
  1157   1155   }
  1158   1156   .input-with-label > label {
  1159   1157     font-weight: initial;
  1160   1158     margin: 0 0.25em 0 0.25em;
  1161   1159     vertical-align: middle;
  1162   1160   }
         1161  +
         1162  +table.numbered-lines {
         1163  +  width: 100%;
         1164  +  table-layout: fixed /* required to keep ultra-wide code from exceeding
         1165  +                         window width, and instead force a scrollbar
         1166  +                         on them. */;
         1167  +}
         1168  +table.numbered-lines > tbody > tr {
         1169  +  font-family: monospace;
         1170  +  line-height: 1.35;
         1171  +  white-space: pre;
         1172  +}
         1173  +table.numbered-lines > tbody > tr > td {
         1174  +  font-family: inherit;
         1175  +  font-size: inherit;
         1176  +  line-height: inherit;
         1177  +  white-space: inherit;
         1178  +  margin: 0;
         1179  +  vertical-align: top;
         1180  +  padding: 0.25em 0 0 0 /*prevents slight overlap at top */;
         1181  +}
         1182  +table.numbered-lines td.line-numbers {
         1183  +  width: 4.5em;
         1184  +}
         1185  +table.numbered-lines td.line-numbers > span:first-of-type {
         1186  +  margin-top: 0.25em/*must match top PADDING of
         1187  +                      td.file-content > pre > code*/;
         1188  +}
         1189  +table.numbered-lines td.line-numbers > span {
         1190  +  display: block;
         1191  +  margin: 0;
         1192  +  padding: 0;
         1193  +  line-height: inherit;
         1194  +  font-size: inherit;
         1195  +  font-family: inherit;
         1196  +  cursor: pointer;
         1197  +  white-space: pre;
         1198  +  margin-right: 2px/*keep selection from nudging the right column */;
         1199  +  text-align: right;
         1200  +}
         1201  +table.numbered-lines td.line-numbers > span:hover {
         1202  +  background-color: rgba(112, 112, 112, 0.25);
         1203  +}
         1204  +table.numbered-lines td.file-content {
         1205  +  padding-left: 0.25em;
         1206  +}
         1207  +table.numbered-lines td.file-content > pre,
         1208  +table.numbered-lines td.file-content > pre > code {
         1209  +  margin: 0;
         1210  +  padding: 0;
         1211  +  line-height: inherit;
         1212  +  font-size: inherit;
         1213  +  font-family: inherit;
         1214  +  white-space: pre;
         1215  +  display: block/*necessary for certain skins!*/;
         1216  +}
         1217  +table.numbered-lines td.file-content > pre {
         1218  +}
         1219  +table.numbered-lines td.file-content > pre > code {
         1220  +  overflow: auto;
         1221  +  padding-left: 0.5em;
         1222  +  padding-right: 0.5em;
         1223  +  padding-top: 0.25em/*any top padding here must match the top MARGIN of
         1224  +                       td.line-numbers's first span child or the
         1225  +                       lines/code will get misaligned. */;
         1226  +  padding-bottom: 0.25em/*prevents a slight overlap at bottom from
         1227  +                          triggering a scroller*/;
         1228  +}
         1229  +table.numbered-lines td.file-content > pre > code > * {
         1230  +  /* Defense against syntax highlighters indirectly messing up these
         1231  +     properties... */
         1232  +  line-height: inherit;
         1233  +  font-size: inherit;
         1234  +  font-family: inherit;
         1235  +}
         1236  +table.numbered-lines td.line-numbers span.selected-line/*replacement*/ {
         1237  +  font-weight: bold;
         1238  +  color: blue;
         1239  +  background-color: #d5d5ff;
         1240  +  border: 1px blue solid;
         1241  +  border-top-width: 0;
         1242  +  border-bottom-width: 0;
         1243  +  padding: 0;
         1244  +  margin: 0;
         1245  +}
         1246  +table.numbered-lines td.line-numbers span.selected-line.start {
         1247  +  border-top-width: 1px;
         1248  +  margin-top: -1px/*restore alignment*/;
         1249  +}
         1250  +table.numbered-lines td.line-numbers span.selected-line.end {
         1251  +  border-bottom-width: 1px;
         1252  +  margin-top: -1px/*restore alignment*/;
         1253  +}
         1254  +table.numbered-lines td.line-numbers span.selected-line.start.end {
         1255  +  margin-top: -2px/*restore alignment*/;
         1256  +}
         1257  +
         1258  +.fossil-tooltip {
         1259  +  text-align: center;
         1260  +  padding: 0.2em 1em;
         1261  +  border: 1px solid black;
         1262  +  border-radius: 0.25em;
         1263  +  position: absolute;
         1264  +  display: inline-block;
         1265  +  z-index: 19/*below default skin's hamburger popup*/;
         1266  +  box-shadow: -0.15em 0.15em 0.2em rgba(0, 0, 0, 0.75);
         1267  +  background-color: inherit;
         1268  +  color: inherit;
         1269  +}
         1270  +
         1271  +.fossil-toast-message {
         1272  +  /* "toast"-style popup message.
         1273  +     See fossil.popupwidget:toast() */
         1274  +  position: absolute;
         1275  +  display: block;
         1276  +  z-index: 101;
         1277  +  text-align: left;
         1278  +  padding: 0.15em 0.5em;
         1279  +  margin: 0;
         1280  +  font-size: 1em;
         1281  +  border-width: 1px;
         1282  +  border-style: solid;
         1283  +  border-color: rgba( 127, 127, 127, 0.75 );
         1284  +  border-radius: 0.25em;
         1285  +  background-color: rgba(20, 20, 20, 1)
         1286  +  /* problem: if we inherit the color it may either be
         1287  +     transparent or inherit translucency via the
         1288  +     skin, leaving it unreadable. Since we set the bg
         1289  +     color we must also set the fg color. */;
         1290  +  color: rgba(235, 235, 235, 0.9);
         1291  +}
         1292  +.fossil-toast-message.error,
         1293  +.fossil-toast-message.warning {
         1294  +  background: yellow;
         1295  +}
         1296  +.fossil-toast-message.error {
         1297  +  font-weight: bold;
         1298  +  color: darkred;
         1299  +  border-color: darkred;
         1300  +}
         1301  +.fossil-toast-message.warning {
         1302  +  color: black;
         1303  +}
         1304  +
         1305  +blockquote.file-content {
         1306  +  /* file content block in the /file page */
         1307  +  margin: 0 1em;
         1308  +}
         1309  +
         1310  +
         1311  +/**
         1312  +   Circular "help" buttons intended to be placed to the right of
         1313  +   another element and hold text text for it. These typically get
         1314  +   initialized automatically at page startup via
         1315  +   fossil.popupwidget.js, and can be manually initialized/created
         1316  +   using window.fossil.helpButtonlets.setup/create(). All of their
         1317  +   child content (plain text and/or DOM elements) gets moved out of
         1318  +   the DOM and shown in a singleton popup when they are clicked. They
         1319  +   may be SPAN elements if their children are all inline elements,
         1320  +   otherwise they must be DIVs (block elements) so that nesting of
         1321  +   block-element content is legal.
         1322  +*/
         1323  +.help-buttonlet {
         1324  +  display: inline-block;
         1325  +  min-width: 1em;
         1326  +  max-width: 1em;
         1327  +  min-height: 1em;
         1328  +  max-height: 1em;
         1329  +  cursor: pointer;
         1330  +  margin: 0 0 0 0.35em;
         1331  +  background-image: /* white question mark on blue circular background */
         1332  +    url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' \
         1333  +viewBox='0 0 15.867574 15.867574'%3e%3ccircle cx='7.9337869' cy='7.9337869' r='7.9337869' \
         1334  +style='fill:%23f0f0f0;stroke-width:1' /%3e%3ccircle cx='7.9337869' cy='7.9337869' \
         1335  +r='6.9662519' style='fill:%23404040;stroke-width:1' /%3e%3ccircle cx='7.9337869' \
         1336  +cy='7.9337869' r='5.9987168' style='fill:%235080d0;stroke-width:1' /%3e%3cpath \
         1337  +d='M 9.2253789,9.8629486 H 6.5997716 v -0.356384 q 0,-0.5963983 0.2400139,-1.0546067 \
         1338  +0.240014,-0.4654816 1.0109681,-1.1782504 L 8.316235,6.8518647 Q 8.7308046,6.473661 \
         1339  +8.9199065,6.1390961 9.1162816,5.8045312 9.1162816,5.4699662 q 0,-0.5091205 -0.3491113,-0.7927734 \
         1340  +-0.3491111,-0.2909259 -0.9746021,-0.2909259 -0.5891252,0 -1.2728012,0.247287 \
         1341  +-0.6836761,0.240014 -1.4255375,0.720042 V 3.0698267 q 0.8800513,-0.3054724 1.6073661,-0.4509353 \
         1342  +0.7273151,-0.145463 1.403718,-0.145463 1.7746486,0 2.7056104,0.727315 0.930965,0.720042 \
         1343  +0.930965,2.1092135 0,0.7127686 -0.283654,1.2800746 -0.283652,0.5600324 -0.967329,1.2073428 \
         1344  +L 10.025425,8.2119439 Q 9.530851,8.6628792 9.3781148,8.9392588 9.2253789,9.2083654 \
         1345  +9.2253789,9.535657 Z M 6.5997716,10.939376 h 2.6256073 v 2.589241 H 6.5997716 Z' \
         1346  +style='fill:%23f8f8f8;stroke-width:1.35412836' /%3e%3c/svg%3e ");    
         1347  +  background-repeat: no-repeat;
         1348  +  background-position: center;
         1349  +  /* When not using a background image, this additional style works
         1350  +     reasonably well along with a ::before content of "?": */
         1351  +  /*border-width: 1px;
         1352  +  border-style: outset;
         1353  +  border-radius: 0.5em;
         1354  +  font-size: 100%;
         1355  +  font-family: monspace;
         1356  +  font-weight: 700;
         1357  +  overflow: hidden;
         1358  +  background-color: rgba(54, 54, 255,1);
         1359  +  color: rgb(255, 255, 255);
         1360  +  text-align: center;
         1361  +  line-height: 1; */
         1362  +}
         1363  +/*.help-buttonlet::before {
         1364  +  content: "?";
         1365  +}*/
         1366  +/**
         1367  +   We really want to hide all help text via CSS but CSS cannot select
         1368  +   TEXT nodes. Thus we move them out of the way programmatically
         1369  +   during initialization.
         1370  +*/
         1371  +.help-buttonlet > *{}
         1372  +
         1373  +/**
         1374  +   CSS class for PopupWidget which wraps .help-buttonlet content.
         1375  +   They also have class fossil-tooltip. We need an overly-exact
         1376  +   selector here to be certain that this class's style overrides
         1377  +   that of fossil-tooltip.
         1378  +*/
         1379  +.fossil-tooltip.help-buttonlet-content {
         1380  +  cursor: default;
         1381  +  text-align: left;
         1382  +  border-style: outset;
         1383  +}
         1384  +
         1385  +noscript > .error {
         1386  +  /* Part of the style_emit_noscript_for_js_page() interface. */
         1387  +  padding: 1em;
         1388  +  font-size: 150%;
         1389  +}
         1390  +
         1391  +/************************************************************
         1392  + pikchr...
         1393  + DOM structure:
         1394  +  <DIV.pikchr-wrapper>
         1395  +    <DIV.pikchr-svg>
         1396  +      <SVG.pikchr>...</SVG>
         1397  +    </DIV.pikchr-svg>
         1398  +    <PRE.pikchr-src>...</PRE>
         1399  +  </DIV.pikchr-wrapper>
         1400  +
         1401  +************************************************************/
         1402  +div.pikchr-wrapper {/*outer wrapper elem for a pikchr construct*/}
         1403  +div.pikchr-svg {/*wrapper for SVG.pikchr element*/}
         1404  +svg.pikchr {/*pikchr SVG*/
         1405  +  width: 100%/*necessary for SOME SVGs for Chrome!*/;
         1406  +}
         1407  +pre.pikchr-src {/*source code view for a pikchr (see fossil.pikchr.js)*/
         1408  +  box-sizing: border-box;
         1409  +  text-align: left;
         1410  +}
         1411  +/* The .source-inline class tells the .source class that the
         1412  +   source view, when enbaled, should be "inline" (same position
         1413  +   as the graphic), else the sources are shifted to the left as
         1414  +   if they were "plain text". */
         1415  +div.pikchr-wrapper.center:not(.source),
         1416  +div.pikchr-wrapper.center.source.source-inline{
         1417  +  text-align: center;
         1418  +  /* Reminder for The Future: this impl also works:
         1419  +
         1420  +      display: grid; place-items: center;
         1421  +
         1422  +     and does not require setting display:inline-block on the relevant
         1423  +     child items, but caniuse.com/css-grid suggests that some
         1424  +     still-seemingly-legitimate browsers don't support grid mode. */
         1425  +}
         1426  +div.pikchr-wrapper.center > div.pikchr-svg {
         1427  +}
         1428  +div.pikchr-wrapper.center:not(.source) > pre.pikchr-src,
         1429  +div.pikchr-wrapper.center:not(.source) > div.pikchr-svg,
         1430  +/* ^^^ Centered non-source-view elements */
         1431  +div.pikchr-wrapper.center.source.source-inline > pre.pikchr-src,
         1432  +div.pikchr-wrapper.center.source.source-inline > div.pikchr-svg
         1433  +/* ^^^ Centered inline-source-view elements */{
         1434  +  display:inline-block/*allows parent text-align to do the alignment*/;
         1435  +}
         1436  +div.pikchr-wrapper.indent:not(.source),
         1437  +div.pikchr-wrapper.indent.source.source-inline{
         1438  +  margin-left: 4em;
         1439  +}
         1440  +div.pikchr-wrapper.float-left:not(.source),
         1441  +div.pikchr-wrapper.float-left.source.source-inline {
         1442  +  float: left;
         1443  +  padding: 4em;
         1444  +}
         1445  +div.pikchr-wrapper.float-right:not(.source),
         1446  +div.pikchr-wrapper.float-right.source.source-inline{
         1447  +  float: right;
         1448  +  padding: 4em;
         1449  +}
         1450  +
         1451  +/* For pikchr-wrapper.source mode, toggle pre.pikchr-src and
         1452  +   svg.pikchr visibility... */
         1453  +div.pikchr-wrapper.source > pre.pikchr-src {
         1454  +  /* Source code  ^^^^^^^ is visible, else it is hidden */
         1455  +}
         1456  +div.pikchr-wrapper:not(.source) > pre.pikchr-src {
         1457  +  /* Hide sources when image is being shown. */
         1458  +  position: absolute !important;
         1459  +  opacity: 0 !important;
         1460  +  pointer-events: none !important;
         1461  +  display: none !important;
         1462  +}
         1463  +div.pikchr-wrapper.source > div.pikchr-svg {
         1464  +  /* Hide image when sources are being shown. */
         1465  +  position: absolute !important;
         1466  +  opacity: 0 !important;
         1467  +  pointer-events: none !important;
         1468  +  display: none !important;
         1469  +}

Changes to src/descendants.c.

   342    342   ** Usage: %fossil descendants ?CHECKIN? ?OPTIONS?
   343    343   **
   344    344   ** Find all leaf descendants of the check-in specified or if the argument
   345    345   ** is omitted, of the check-in currently checked out.
   346    346   **
   347    347   ** Options:
   348    348   **    -R|--repository FILE       Extract info from repository FILE
   349         -**    -W|--width <num>           Width of lines (default is to auto-detect).
   350         -**                               Must be >20 or 0 (= no limit, resulting in a
   351         -**                               single line per entry).
          349  +**    -W|--width N               Width of lines (default is to auto-detect).
          350  +**                               Must be greater than 20 or else 0 for no
          351  +**                               limit, resulting in a one line per entry.
   352    352   **
   353    353   ** See also: [[finfo]], [[info]], [[leaves]]
   354    354   */
   355    355   void descendants_cmd(void){
   356    356     Stmt q;
   357    357     int base, width;
   358    358     const char *zWidth;
................................................................................
   402    402   **
   403    403   ** Options:
   404    404   **   -a|--all         show ALL leaves
   405    405   **   --bybranch       order output by branch name
   406    406   **   -c|--closed      show only closed leaves
   407    407   **   -m|--multiple    show only cases with multiple leaves on a single branch
   408    408   **   --recompute      recompute the "leaf" table in the repository DB
   409         -**   -W|--width <num> Width of lines (default is to auto-detect). Must be
   410         -**                    >39 or 0 (= no limit, resulting in a single line per
   411         -**                    entry).
          409  +**   -W|--width N     Width of lines (default is to auto-detect). Must be
          410  +**                    more than 39 or else 0 no limit, resulting in a single
          411  +**                    line per entry.
   412    412   **
   413    413   ** See also: [[descendants]], [[finfo]], [[info]], [[branch]]
   414    414   */
   415    415   void leaves_cmd(void){
   416    416     Stmt q;
   417    417     Blob sql;
   418    418     int showAll = find_option("all", "a", 0)!=0;

Changes to src/diff.c.

   123    123   /*
   124    124   ** Count the number of lines in the input string.  Include the last line
   125    125   ** in the count even if it lacks the \n terminator.  If an empty string
   126    126   ** is specified, the number of lines is zero.  For the purposes of this
   127    127   ** function, a string is considered empty if it contains no characters
   128    128   ** -OR- it contains only NUL characters.
   129    129   */
   130         -static int count_lines(
          130  +int count_lines(
   131    131     const char *z,
   132    132     int n,
   133    133     int *pnLine
   134    134   ){
   135    135     int nLine;
   136    136     const char *zNL, *z2;
   137    137     for(nLine=0, z2=z; (zNL = strchr(z2,'\n'))!=0; z2=zNL+1, nLine++){}
................................................................................
  1564   1564       }else if( iEX>iEXp ){
  1565   1565         iSXp = iSX;
  1566   1566         iSYp = iSY;
  1567   1567         iEXp = iEX;
  1568   1568         iEYp = iEY;
  1569   1569       }
  1570   1570     }
  1571         -  if( iSXb==iEXb && (iE1-iS1)*(iE2-iS2)<400 ){
         1571  +  if( iSXb==iEXb && (sqlite3_int64)(iE1-iS1)*(iE2-iS2)<400 ){
  1572   1572       /* If no common sequence is found using the hashing heuristic and
  1573   1573       ** the input is not too big, use the expensive exact solution */
  1574   1574       optimalLCS(p, iS1, iE1, iS2, iE2, piSX, piEX, piSY, piEY);
  1575   1575     }else{
  1576   1576       *piSX = iSXb;
  1577   1577       *piSY = iSYb;
  1578   1578       *piEX = iEXb;
................................................................................
  2481   2481     for(p=ann.aVers, i=0; i<ann.nVers; i++, p++){
  2482   2482       clr = gradient_color(clr1, clr2, ann.nVers-1, i);
  2483   2483       ann.aVers[i].zBgColor = mprintf("#%06x", clr);
  2484   2484     }
  2485   2485   
  2486   2486     @ <div id="annotation_log" style='display:%s(showLog?"block":"none");'>
  2487   2487     if( zOrigin ){
  2488         -    zLink = href("%R/finfo?name=%t&ci=%!S&orig=%!S",zFilename,zCI,zOrigin);
         2488  +    zLink = href("%R/finfo?name=%t&from=%!S&to=%!S",zFilename,zCI,zOrigin);
  2489   2489     }else{
  2490         -    zLink = href("%R/finfo?name=%t&ci=%!S",zFilename,zCI);
         2490  +    zLink = href("%R/finfo?name=%t&from=%!S",zFilename,zCI);
  2491   2491     }
  2492   2492     @ <h2>Versions of %z(zLink)%h(zFilename)</a> analyzed:</h2>
  2493   2493     @ <ol>
  2494   2494     for(p=ann.aVers, i=0; i<ann.nVers; i++, p++){
  2495   2495       @ <li><span style='background-color:%s(p->zBgColor);'>%s(p->zDate)
  2496   2496       @ check-in %z(href("%R/info/%!S",p->zMUuid))%S(p->zMUuid)</a>
  2497   2497       @ artifact %z(href("%R/artifact/%!S",p->zFUuid))%S(p->zFUuid)</a>
................................................................................
  2500   2500     @ </ol>
  2501   2501     @ <hr />
  2502   2502     @ </div>
  2503   2503   
  2504   2504     if( !ann.bMoreToDo ){
  2505   2505       assert( ann.origId==0 );  /* bMoreToDo always set for a point-to-point */
  2506   2506       @ <h2>Origin for each line in
  2507         -    @ %z(href("%R/finfo?name=%h&ci=%!S", zFilename, zCI))%h(zFilename)</a>
         2507  +    @ %z(href("%R/finfo?name=%h&from=%!S", zFilename, zCI))%h(zFilename)</a>
  2508   2508       @ from check-in %z(href("%R/info/%!S",zCI))%S(zCI)</a>:</h2>
  2509   2509     }else if( ann.origId>0 ){
  2510   2510       @ <h2>Lines of
  2511         -    @ %z(href("%R/finfo?name=%h&ci=%!S", zFilename, zCI))%h(zFilename)</a>
         2511  +    @ %z(href("%R/finfo?name=%h&from=%!S", zFilename, zCI))%h(zFilename)</a>
  2512   2512       @ from check-in %z(href("%R/info/%!S",zCI))%S(zCI)</a>
  2513   2513       @ that are changed by the sequence of edits moving toward
  2514   2514       @ check-in %z(href("%R/info/%!S",zOrigin))%S(zOrigin)</a>:</h2>
  2515   2515     }else{
  2516   2516       @ <h2>Lines added by the %d(ann.nVers) most recent ancestors of
  2517         -    @ %z(href("%R/finfo?name=%h&ci=%!S", zFilename, zCI))%h(zFilename)</a>
         2517  +    @ %z(href("%R/finfo?name=%h&from=%!S", zFilename, zCI))%h(zFilename)</a>
  2518   2518       @ from check-in %z(href("%R/info/%!S",zCI))%S(zCI)</a>:</h2>
  2519   2519     }
  2520   2520     @ <pre>
  2521   2521     szHash = 10;
  2522   2522     for(i=0; i<ann.nOrig; i++){
  2523   2523       int iVers = ann.aOrig[i].iVers;
  2524   2524       char *z = (char*)ann.aOrig[i].z;

Changes to src/diffcmd.c.

   813    813   ** when using an external diff program.
   814    814   **
   815    815   ** The "--binary" option causes files matching the glob PATTERN to be treated
   816    816   ** as binary when considering if they should be used with external diff program.
   817    817   ** This option overrides the "binary-glob" setting.
   818    818   **
   819    819   ** Options:
   820         -**   --binary PATTERN           Treat files that match the glob PATTERN as binary
   821         -**   --branch BRANCH            Show diff of all changes on BRANCH
   822         -**   --brief                    Show filenames only
   823         -**   --checkin VERSION          Show diff of all changes in VERSION
   824         -**   --command PROG             External diff program - overrides "diff-command"
   825         -**   --context|-c N             Use N lines of context
   826         -**   --diff-binary BOOL         Include binary files when using external commands
   827         -**   --exec-abs-paths           Force absolute path names with external commands.
   828         -**   --exec-rel-paths           Force relative path names with external commands.
   829         -**   --from|-r VERSION          Select VERSION as source for the diff
   830         -**   --internal|-i              Use internal diff logic
   831         -**   --new-file|-N              Show complete text of added and deleted files
   832         -**   --numstat                  Show only the number of lines delete and added
   833         -**   --side-by-side|-y          Side-by-side diff
   834         -**   --strip-trailing-cr        Strip trailing CR
   835         -**   --tclsh PATH               Tcl/Tk used for --tk (default: "tclsh")
   836         -**   --tk                       Launch a Tcl/Tk GUI for display
   837         -**   --to VERSION               Select VERSION as target for the diff
   838         -**   --undo                     Diff against the "undo" buffer
   839         -**   --unified                  Unified diff
   840         -**   -v|--verbose               Output complete text of added or deleted files
   841         -**   -w|--ignore-all-space      Ignore white space when comparing lines
   842         -**   -W|--width <num>           Width of lines in side-by-side diff
   843         -**   -Z|--ignore-trailing-space Ignore changes to end-of-line whitespace
          820  +**   --binary PATTERN            Treat files that match the glob PATTERN
          821  +**                               as binary
          822  +**   --branch BRANCH             Show diff of all changes on BRANCH
          823  +**   --brief                     Show filenames only
          824  +**   --checkin VERSION           Show diff of all changes in VERSION
          825  +**   --command PROG              External diff program. Overrides "diff-command"
          826  +**   --context|-c N              Use N lines of context
          827  +**   --diff-binary BOOL          Include binary files with external commands
          828  +**   --exec-abs-paths            Force absolute path names on external commands
          829  +**   --exec-rel-paths            Force relative path names on external commands
          830  +**   --from|-r VERSION           Select VERSION as source for the diff
          831  +**   --internal|-i               Use internal diff logic
          832  +**   --new-file|-N               Show complete text of added and deleted files
          833  +**   --numstat                   Show only the number of lines delete and added
          834  +**   --side-by-side|-y           Side-by-side diff
          835  +**   --strip-trailing-cr         Strip trailing CR
          836  +**   --tclsh PATH                Tcl/Tk used for --tk (default: "tclsh")
          837  +**   --tk                        Launch a Tcl/Tk GUI for display
          838  +**   --to VERSION                Select VERSION as target for the diff
          839  +**   --undo                      Diff against the "undo" buffer
          840  +**   --unified                   Unified diff
          841  +**   -v|--verbose                Output complete text of added or deleted files
          842  +**   -w|--ignore-all-space       Ignore white space when comparing lines
          843  +**   -W|--width N                Width of lines in side-by-side diff
          844  +**   -Z|--ignore-trailing-space  Ignore changes to end-of-line whitespace
   844    845   */
   845    846   void diff_cmd(void){
   846    847     int isGDiff;               /* True for gdiff.  False for normal diff */
   847    848     int isInternDiff;          /* True for internal diff */
   848    849     int verboseFlag;           /* True if -v or --verbose flag is used */
   849    850     const char *zFrom;         /* Source version number */
   850    851     const char *zTo;           /* Target version number */

Changes to src/dispatch.c.

   227    227     int i;
   228    228     for(i=3; i<n-1; i++){
   229    229       if( z[i]==' ' && z[i+1]!=' ' && z[i-1]==' ' && z[i-2]!='.' ) return i+1;
   230    230     }
   231    231     return 0 ;
   232    232   }
   233    233   
   234         -/*
   235         -** Append text to pOut, adding formatting markup.  Terms that
   236         -** have all lower-case letters are within <tt>..</tt>.  Terms
   237         -** that have all upper-case letters are within <i>..</i>.
   238         -*/
   239         -static void appendMixedFont(Blob *pOut, const char *z, int n){
   240         -  const char *zEnd = "";
   241         -  int i = 0;
   242         -  int j;
   243         -  while( i<n ){
   244         -    if( z[i]==' ' || z[i]=='=' ){
   245         -      for(j=i+1; j<n && (z[j]==' ' || z[j]=='='); j++){}
   246         -      blob_append(pOut, z+i, j-i);
   247         -      i = j;
   248         -    }else{
   249         -      for(j=i; j<n && z[j]!=' ' && z[j]!='=' && !fossil_isalpha(z[j]); j++){}
   250         -      if( j>=n || z[j]==' ' || z[j]=='=' ){
   251         -        zEnd = "";
   252         -      }else{
   253         -        if( fossil_isupper(z[j]) && z[i]!='-' ){
   254         -          blob_append(pOut, "<i>",3);
   255         -          zEnd = "</i>";
   256         -        }else{
   257         -          blob_append(pOut, "<tt>", 4);
   258         -          zEnd = "</tt>";
   259         -        }
   260         -      }
   261         -      while( j<n && z[j]!=' ' && z[j]!='=' ){ j++; }
   262         -      blob_appendf(pOut, "%#h", j-i, z+i);
   263         -      if( zEnd[0] ) blob_append(pOut, zEnd, -1);
   264         -      i = j;
   265         -    }
   266         -  }
   267         -}
   268         -
   269    234   /*
   270    235   ** Input string zIn starts with '['.  If the content is a hyperlink of the
   271    236   ** form [[...]] then return the index of the closing ']'.  Otherwise return 0.
   272    237   */
   273    238   static int help_is_link(const char *z, int n){
   274    239     int i;
   275    240     char c;
................................................................................
   278    243     for(i=3; i<n && (c = z[i])!=0; i++){
   279    244       if( c==']' && z[i-1]==']' ) return i;
   280    245     }
   281    246     return 0;
   282    247   }
   283    248   
   284    249   /*
   285         -** Append text to pOut, adding hyperlink markup for [...].
          250  +** Append text to pOut with changes:
          251  +**
          252  +**    *   Add hyperlink markup for [[...]]
          253  +**    *   Escape HTML characters: < > & and "
          254  +**    *   Change "%fossil" to just "fossil"
   286    255   */
   287    256   static void appendLinked(Blob *pOut, const char *z, int n){
   288    257     int i = 0;
   289    258     int j;
   290    259     while( i<n ){
   291         -    if( z[i]=='[' && (j = help_is_link(z+i, n-i))>0 ){
          260  +    char c = z[i];
          261  +    if( c=='[' && (j = help_is_link(z+i, n-i))>0 ){
   292    262         if( i ) blob_append(pOut, z, i);
   293    263         z += i+2;
   294    264         n -= i+2;
   295    265         blob_appendf(pOut, "<a href='%R/help?cmd=%.*s'>%.*s</a>",
   296    266            j-3, z, j-3, z);
   297    267         z += j-1;
   298    268         n -= j-1;
   299    269         i = 0;
          270  +    }else if( c=='%' && n-i>=7 && strncmp(z+i,"%fossil",7)==0 ){
          271  +      if( i ) blob_append(pOut, z, i);
          272  +      z += i+7;
          273  +      n -= i+7;
          274  +      blob_append(pOut, "fossil", 6);
          275  +      i = 0;
          276  +    }else if( c=='<' ){
          277  +      if( i ) blob_append(pOut, z, i);
          278  +      blob_append(pOut, "&lt;", 4);
          279  +      z += i+1;
          280  +      n -= i+1;
          281  +      i = 0;
          282  +    }else if( c=='>' ){
          283  +      if( i ) blob_append(pOut, z, i);
          284  +      blob_append(pOut, "&gt;", 4);
          285  +      z += i+1;
          286  +      n -= i+1;
          287  +      i = 0;
          288  +    }else if( c=='&' ){
          289  +      if( i ) blob_append(pOut, z, i);
          290  +      blob_append(pOut, "&amp;", 5);
          291  +      z += i+1;
          292  +      n -= i+1;
          293  +      i = 0;
   300    294       }else{
   301    295         i++;
   302    296       }
   303    297     }
   304    298     blob_append(pOut, z, i);
   305    299   }
          300  +
          301  +/*
          302  +** Append text to pOut, adding formatting markup.  Terms that
          303  +** have all lower-case letters are within <tt>..</tt>.  Terms
          304  +** that have all upper-case letters are within <i>..</i>.
          305  +*/
          306  +static void appendMixedFont(Blob *pOut, const char *z, int n){
          307  +  const char *zEnd = "";
          308  +  int i = 0;
          309  +  int j;
          310  +  while( i<n ){
          311  +    if( z[i]==' ' || z[i]=='=' ){
          312  +      for(j=i+1; j<n && (z[j]==' ' || z[j]=='='); j++){}
          313  +      appendLinked(pOut, z+i, j-i);
          314  +      i = j;
          315  +    }else{
          316  +      for(j=i; j<n && z[j]!=' ' && z[j]!='=' && !fossil_isalpha(z[j]); j++){}
          317  +      if( j>=n || z[j]==' ' || z[j]=='=' ){
          318  +        zEnd = "";
          319  +      }else{
          320  +        if( fossil_isupper(z[j]) && z[i]!='-' ){
          321  +          blob_append(pOut, "<i>",3);
          322  +          zEnd = "</i>";
          323  +        }else{
          324  +          blob_append(pOut, "<tt>", 4);
          325  +          zEnd = "</tt>";
          326  +        }
          327  +      }
          328  +      while( j<n && z[j]!=' ' && z[j]!='=' ){ j++; }
          329  +      appendLinked(pOut, z+i, j-i);
          330  +      if( zEnd[0] ) blob_append(pOut, zEnd, -1);
          331  +      i = j;
          332  +    }
          333  +  }
          334  +}
   306    335   
   307    336   /*
   308    337   ** Attempt to reformat plain-text help into HTML for display on a webpage.
   309    338   **
   310    339   ** The HTML output is appended to Blob pHtml, which should already be
   311    340   ** initialized.
   312    341   **
................................................................................
   339    368     static const char *zEndUL = "</ul>";
   340    369     static const char *zEndDD = "</dd>";
   341    370   
   342    371     aIndent[0] = 0;
   343    372     azEnd[0] = "";
   344    373     while( zHelp[0] ){
   345    374       i = 0;
   346         -    while( (c = zHelp[i])!=0
   347         -        && c!='\n'
   348         -        && c!='<'
   349         -        && (c!='%' || strncmp(zHelp+i,"%fossil",7)!=0)
   350         -    ){ i++; }
   351         -    if( c=='%' ){
   352         -      if( i ) blob_appendf(pHtml, "%#h", i, zHelp);
   353         -      zHelp += i + 1;
   354         -      wantBR = 1;
   355         -      continue;
   356         -    }else if( c=='<' ){
   357         -      if( i ) blob_appendf(pHtml, "%#h", i, zHelp);
   358         -      blob_append(pHtml, "&amp;", 5);
   359         -      zHelp += i + 1;
   360         -      continue;
          375  +    while( (c = zHelp[i])!=0 && c!='\n' ){
          376  +      if( c=='%' && i>2 && zHelp[i-2]==':' && strncmp(zHelp+i,"%fossil",7)==0 ){
          377  +        appendLinked(pHtml, zHelp, i);
          378  +        zHelp += i+1;
          379  +        i = 0;
          380  +        wantBR = 1;
          381  +        continue;
          382  +      }
          383  +      i++;
   361    384       }
   362    385       if( i>2 && zHelp[0]=='>' && zHelp[1]==' ' ){
   363    386         isDT = 1;
   364    387         for(nIndent=1; nIndent<i && zHelp[nIndent]==' '; nIndent++){}
   365    388       }else{
   366    389         isDT = 0;
   367    390         for(nIndent=0; nIndent<i && zHelp[nIndent]==' '; nIndent++){}
   368    391       }
   369    392       if( nIndent==i ){
   370    393         if( c==0 ) break;
   371         -      blob_append(pHtml, "\n", 1);
          394  +      if( iLevel && azEnd[iLevel]==zEndPRE ){
          395  +        /* Skip the newline at the end of a <pre> */
          396  +      }else{
          397  +        blob_append_char(pHtml, '\n');
          398  +      }
   372    399         wantP = 1;
   373    400         wantBR = 0;
   374    401         zHelp += i+1;
   375    402         continue;
   376    403       }
   377    404       if( nIndent+2<i && zHelp[nIndent]=='*' && zHelp[nIndent+1]==' ' ){
   378    405         nIndent += 2;
................................................................................
   425    452         if( iDD ){
   426    453           int x;
   427    454           assert( iLevel<ArraySize(aIndent)-1 );
   428    455           iLevel++;
   429    456           aIndent[iLevel] = x = nIndent+iDD;
   430    457           azEnd[iLevel] = zEndDD;
   431    458           appendMixedFont(pHtml, zHelp+nIndent, iDD-2);
   432         -        blob_appendf(pHtml, "</dt><dd>%#h\n", i-x, zHelp+x);
          459  +        blob_append(pHtml, "</dt><dd>",9);
          460  +        appendLinked(pHtml, zHelp+x, i-x);
   433    461         }else{
   434    462           appendMixedFont(pHtml, zHelp+nIndent, i-nIndent);
   435         -        blob_append(pHtml, "</dt>\n", 6);
   436    463         }
          464  +      blob_append(pHtml, "</dt>\n", 6);
   437    465       }else if( wantBR ){
   438    466         appendMixedFont(pHtml, zHelp+nIndent, i-nIndent);
   439    467         blob_append(pHtml, "<br>\n", 5);
   440    468         wantBR = 0;
   441    469       }else{
   442    470         appendLinked(pHtml, zHelp+nIndent, i-nIndent);
   443    471         blob_append_char(pHtml, '\n');
................................................................................
   719    747     if( zCmd==0 ) zCmd = P("name");
   720    748     if( zCmd && *zCmd ){
   721    749       int rc;
   722    750       const CmdOrPage *pCmd = 0;
   723    751   
   724    752       style_header("Help: %s", zCmd);
   725    753   
   726         -    style_submenu_element("Command-List", "%s/help", g.zTop);
          754  +    style_submenu_element("Command-List", "%R/help");
   727    755       rc = dispatch_name_search(zCmd, CMDFLAG_ANY|CMDFLAG_PREFIX, &pCmd);
   728    756       if( *zCmd=='/' ){
   729    757         /* Some of the webpages require query parameters in order to work.
   730    758         ** @ <h1>The "<a href='%R%s(zCmd)'>%s(zCmd)</a>" page:</h1> */
   731    759         @ <h1>The "%h(zCmd)" page:</h1>
   732    760       }else if( rc==0 && (pCmd->eCmdFlags & CMDFLAG_SETTING)!=0 ){
   733    761         @ <h1>The "%h(pCmd->zName)" setting:</h1>

Changes to src/doc.c.

   188    188     { "ogm",        3, "application/ogg"                   },
   189    189     { "pbm",        3, "image/x-portable-bitmap"           },
   190    190     { "pdb",        3, "chemical/x-pdb"                    },
   191    191     { "pdf",        3, "application/pdf"                   },
   192    192     { "pgm",        3, "image/x-portable-graymap"          },
   193    193     { "pgn",        3, "application/x-chess-pgn"           },
   194    194     { "pgp",        3, "application/pgp"                   },
          195  +  { "pikchr",     6, "text/x-pikchr"                     },
   195    196     { "pl",         2, "application/x-perl"                },
   196    197     { "pm",         2, "application/x-perl"                },
   197    198     { "png",        3, "image/png"                         },
   198    199     { "pnm",        3, "image/x-portable-anymap"           },
   199    200     { "pot",        3, "application/mspowerpoint"          },
   200    201     { "potx",       4, "application/vnd.openxmlformats-"
   201    202                        "officedocument.presentationml.template"},
................................................................................
   395    396           return z;
   396    397         default:
   397    398           assert(!"cannot happen - invalid tokenizerState value.");
   398    399       }
   399    400     }
   400    401     return 0;
   401    402   }
          403  +
          404  +/*
          405  +** Emit Javascript which applies (or optionally can apply) to both the
          406  +** /doc and /wiki pages. None of this implements required
          407  +** functionality, just nice-to-haves. Any calls after the first are
          408  +** no-ops.
          409  +*/
          410  +void document_emit_js(void){
          411  +  static int once = 0;
          412  +  if(0==once++){
          413  +    builtin_fossil_js_bundle_or("pikchr", 0);
          414  +    style_script_begin(__FILE__,__LINE__);
          415  +    CX("window.addEventListener('load', "
          416  +       "()=>window.fossil.pikchr.addSrcView(), "
          417  +       "false);\n");
          418  +    style_script_end();
          419  +  }
          420  +}
   402    421   
   403    422   /*
   404    423   ** Guess the mime-type of a document based on its name.
   405    424   */
   406    425   const char *mimetype_from_name(const char *zName){
   407    426     const char *z;
   408    427     int i;
................................................................................
   748    767       if( wiki_find_title(pBody, &title, &tail) ){
   749    768         style_header("%s", blob_str(&title));
   750    769         wiki_convert(&tail, 0, WIKI_BUTTONS);
   751    770       }else{
   752    771         style_header("%s", zDefaultTitle);
   753    772         wiki_convert(pBody, 0, WIKI_BUTTONS);
   754    773       }
          774  +    document_emit_js();
   755    775       style_footer();
   756    776     }else if( fossil_strcmp(zMime, "text/x-markdown")==0 ){
   757    777       Blob tail = BLOB_INITIALIZER;
   758    778       markdown_to_html(pBody, &title, &tail);
   759    779       if( blob_size(&title)>0 ){
   760    780         style_header("%s", blob_str(&title));
   761    781       }else{
   762    782         style_header("%s", zDefaultTitle);
   763    783       }
   764    784       convert_href_and_output(&tail);
          785  +    document_emit_js();
   765    786       style_footer();
   766    787     }else if( fossil_strcmp(zMime, "text/plain")==0 ){
   767    788       style_header("%s", zDefaultTitle);
   768    789       @ <blockquote><pre>
   769    790       @ %h(blob_str(pBody))
   770    791       @ </pre></blockquote>
          792  +    document_emit_js();
   771    793       style_footer();
   772    794     }else if( fossil_strcmp(zMime, "text/html")==0
   773    795               && doc_is_embedded_html(pBody, &title) ){
   774    796       if( blob_size(&title)==0 ) blob_append(&title,zFilename,-1);
   775    797       style_header("%s", blob_str(&title));
   776    798       convert_href_and_output(pBody);
          799  +    document_emit_js();
          800  +    style_footer();
          801  +  }else if( fossil_strcmp(zMime, "text/x-pikchr")==0 ){
          802  +    style_adunit_config(ADUNIT_RIGHT_OK);
          803  +    style_header("%s", zDefaultTitle);
          804  +    wiki_render_by_mimetype(pBody, zMime);
   777    805       style_footer();
   778    806   #ifdef FOSSIL_ENABLE_TH1_DOCS
   779    807     }else if( Th_AreDocsEnabled() &&
   780    808               fossil_strcmp(zMime, "application/x-th1")==0 ){
   781    809       int raw = P("raw")!=0;
   782    810       if( !raw ){
   783    811         Blob tail;
................................................................................
   790    818           style_header("%h", zFilename);
   791    819           Th_Render(blob_str(pBody));
   792    820         }
   793    821       }else{
   794    822         Th_Render(blob_str(pBody));
   795    823       }
   796    824       if( !raw ){
          825  +      document_emit_js();
   797    826         style_footer();
   798    827       }
   799    828   #endif
   800    829     }else{
   801    830       fossil_free(style_csp(1));
   802    831       cgi_set_content_type(zMime);
   803    832       cgi_set_content(pBody);

Changes to src/event.c.

   226    226       @ </pre>
   227    227     }
   228    228     zFullId = db_text(0, "SELECT SUBSTR(tagname,7)"
   229    229                          "  FROM tag"
   230    230                          " WHERE tagname GLOB 'event-%q*'",
   231    231                       zId);
   232    232     attachment_list(zFullId, "<hr /><h2>Attachments:</h2><ul>");
          233  +  document_emit_js();
   233    234     style_footer();
   234    235     manifest_destroy(pTNote);
   235    236   }
   236    237   
   237    238   /*
   238    239   ** Add or update a new tech note to the repository.  rid is id of
   239    240   ** the prior version of this technote, if any.

Changes to src/file.c.

    45     45   **
    46     46   ** The difference is in the handling of symbolic links.  RepoFILE should be
    47     47   ** used for files that are under management by a Fossil repository.  ExtFILE
    48     48   ** should be used for files that are not under management.  SymFILE is for
    49     49   ** a few special cases such as the "fossil test-tarball" command when we never
    50     50   ** want to follow symlinks.
    51     51   **
    52         -** If RepoFILE is used and if the allow-symlinks setting is true and if
    53         -** the object is a symbolic link, then the object is treated like an ordinary
    54         -** file whose content is name of the object to which the symbolic link
    55         -** points.
           52  +**   ExtFILE      Symbolic links always refer to the object to which the
           53  +**                link points.  Symlinks are never recognized as symlinks but
           54  +**                instead always appear to the the target object.
    56     55   **
    57         -** If ExtFILE is used or allow-symlinks is false, then operations on a
    58         -** symbolic link are the same as operations on the object to which the
    59         -** symbolic link points.
           56  +**   SymFILE      Symbolic links always appear to be files whose name is
           57  +**                the target pathname of the symbolic link.
    60     58   **
    61         -** SymFILE is like RepoFILE except that it always uses the target filename of
    62         -** a symbolic link as the content, instead of the content of the object
    63         -** that the symlink points to.  SymFILE acts as if allow-symlinks is always ON.
           59  +**   RepoFILE     Like SymFILE if allow-symlinks is true, or like
           60  +**                ExtFILE if allow-symlinks is false.  In other words,
           61  +**                symbolic links are only recognized as something different
           62  +**                from files or directories if allow-symlinks is true.
    64     63   */
    65     64   #define ExtFILE    0  /* Always follow symlinks */
    66     65   #define RepoFILE   1  /* Follow symlinks if and only if allow-symlinks is OFF */
    67     66   #define SymFILE    2  /* Never follow symlinks */
    68     67   
    69     68   #include <dirent.h>
    70     69   #if defined(_WIN32)
................................................................................
   132    131     const char *zFilename,  /* name of file or directory to inspect. */
   133    132     struct fossilStat *buf, /* pointer to buffer where info should go. */
   134    133     int eFType              /* Look at symlink itself if RepoFILE and enabled. */
   135    134   ){
   136    135     int rc;
   137    136     void *zMbcs = fossil_utf8_to_path(zFilename, 0);
   138    137   #if !defined(_WIN32)
   139         -  if( eFType>=RepoFILE && (eFType==SymFILE || db_allow_symlinks()) ){
          138  +  if( (eFType==RepoFILE && db_allow_symlinks())
          139  +   || eFType==SymFILE ){
          140  +    /* Symlinks look like files whose content is the name of the target */
   140    141       rc = lstat(zMbcs, buf);
   141    142     }else{
          143  +    /* Symlinks look like the object to which they point */
   142    144       rc = stat(zMbcs, buf);
   143    145     }
   144    146   #else
   145    147     rc = win32_stat(zMbcs, buf, eFType);
   146    148   #endif
   147    149     fossil_path_free(zMbcs);
   148    150     return rc;
................................................................................
   314    316     return file_perm(zFilename, eFType)==PERM_EXE;
   315    317   }
   316    318   
   317    319   /*
   318    320   ** Return TRUE if the named file is a symlink and symlinks are allowed.
   319    321   ** Return false for all other cases.
   320    322   **
   321         -** This routines RepoFILE - that zFilename is always a file under management.
          323  +** This routines assumes RepoFILE - that zFilename is always a file
          324  +** under management.
   322    325   **
   323    326   ** On Windows, always return False.
   324    327   */
   325    328   int file_islink(const char *zFilename){
   326    329     return file_perm(zFilename, RepoFILE)==PERM_LNK;
   327    330   }
   328    331   
................................................................................
   434    437     static const char *azReqTab[] = {
   435    438        "blob", "delta", "rcvfrom", "user", "config"
   436    439     };
   437    440     if( !file_isfile(zFilename, ExtFILE) ) return 0;
   438    441     sz = file_size(zFilename, ExtFILE);
   439    442     if( sz<35328 ) return 0;
   440    443     if( sz%512!=0 ) return 0;
   441         -  rc = sqlite3_open_v2(zFilename, &db, 
          444  +  rc = sqlite3_open_v2(zFilename, &db,
   442    445             SQLITE_OPEN_READWRITE, 0);
   443    446     if( rc!=0 ) goto not_a_repo;
   444    447     for(i=0; i<count(azReqTab); i++){
   445    448       if( sqlite3_table_column_metadata(db, "main", azReqTab[i],0,0,0,0,0,0) ){
   446    449         goto not_a_repo;
   447    450       }
   448    451     }
................................................................................
   640    643   ** zFilename is a symbolic link, it is the object that zFilename points
   641    644   ** to that is modified.
   642    645   */
   643    646   int file_setexe(const char *zFilename, int onoff){
   644    647     int rc = 0;
   645    648   #if !defined(_WIN32)
   646    649     struct stat buf;
   647         -  if( fossil_stat(zFilename, &buf, RepoFILE)!=0 
          650  +  if( fossil_stat(zFilename, &buf, RepoFILE)!=0
   648    651      || S_ISLNK(buf.st_mode)
   649    652      || S_ISDIR(buf.st_mode)
   650    653     ){
   651    654       return 0;
   652    655     }
   653    656     if( onoff ){
   654    657       int targetMode = (buf.st_mode & 0444)>>2;
................................................................................
   767    770     }
   768    771     if( rc!=1 ){
   769    772   #if defined(_WIN32)
   770    773       wchar_t *zMbcs = fossil_utf8_to_path(zName, 1);
   771    774       rc = _wmkdir(zMbcs);
   772    775   #else
   773    776       char *zMbcs = fossil_utf8_to_path(zName, 1);
   774         -    rc = mkdir(zName, 0755);
          777  +    rc = mkdir(zMbcs, 0755);
   775    778   #endif
   776    779       fossil_path_free(zMbcs);
   777    780       return rc;
   778    781     }
   779    782     return 0;
   780    783   }
   781    784   
................................................................................
   795    798     int nName, rc = 0;
   796    799     char *zName;
   797    800   
   798    801     nName = strlen(zFilename);
   799    802     zName = mprintf("%s", zFilename);
   800    803     nName = file_simplify_name(zName, nName, 0);
   801    804     while( nName>0 && zName[nName-1]!='/' ){ nName--; }
   802         -  if( nName ){
          805  +  if( nName>1 ){
   803    806       zName[nName-1] = 0;
   804    807       if( file_isdir(zName, eFType)!=1 ){
   805    808         rc = file_mkfolder(zName, eFType, forceFlag, errorReturn);
   806    809         if( rc==0 ){
   807    810           if( file_mkdir(zName, eFType, forceFlag)
   808    811            && file_isdir(zName, eFType)!=1
   809    812           ){
................................................................................
  1303   1306     const char *zPath,
  1304   1307     int slash,
  1305   1308     int reset
  1306   1309   ){
  1307   1310     char zBuf[200];
  1308   1311     char *z;
  1309   1312     Blob x;
         1313  +  char *zFull;
  1310   1314     int rc;
  1311   1315     sqlite3_int64 iMtime;
  1312   1316     struct fossilStat testFileStat;
  1313   1317     memset(zBuf, 0, sizeof(zBuf));
  1314   1318     blob_zero(&x);
  1315   1319     file_canonical_name(zPath, &x, slash);
  1316         -  fossil_print("[%s] -> [%s]\n", zPath, blob_buffer(&x));
  1317         -  blob_reset(&x);
         1320  +  zFull = blob_str(&x);
         1321  +  fossil_print("[%s] -> [%s]\n", zPath, zFull);
  1318   1322     memset(&testFileStat, 0, sizeof(struct fossilStat));
  1319   1323     rc = fossil_stat(zPath, &testFileStat, 0);
  1320   1324     fossil_print("  stat_rc                = %d\n", rc);
  1321   1325     sqlite3_snprintf(sizeof(zBuf), zBuf, "%lld", testFileStat.st_size);
  1322   1326     fossil_print("  stat_size              = %s\n", zBuf);
  1323   1327     if( g.db==0 ) sqlite3_open(":memory:", &g.db);
  1324   1328     z = db_text(0, "SELECT datetime(%lld, 'unixepoch')", testFileStat.st_mtime);
................................................................................
  1358   1362     fossil_print("  file_mode(RepoFILE)    = 0%o\n", file_mode(zPath,RepoFILE));
  1359   1363     fossil_print("  file_isfile(RepoFILE)  = %d\n", file_isfile(zPath,RepoFILE));
  1360   1364     fossil_print("  file_isfile_or_link    = %d\n", file_isfile_or_link(zPath));
  1361   1365     fossil_print("  file_islink            = %d\n", file_islink(zPath));
  1362   1366     fossil_print("  file_isexe(RepoFILE)   = %d\n", file_isexe(zPath,RepoFILE));
  1363   1367     fossil_print("  file_isdir(RepoFILE)   = %d\n", file_isdir(zPath,RepoFILE));
  1364   1368     fossil_print("  file_is_repository     = %d\n", file_is_repository(zPath));
         1369  +  fossil_print("  file_is_reserved_name  = %d\n",
         1370  +                                             file_is_reserved_name(zFull,-1));
         1371  +  blob_reset(&x);
  1365   1372     if( reset ) resetStat();
  1366   1373   }
  1367   1374   
  1368   1375   /*
  1369   1376   ** COMMAND: test-file-environment
  1370   1377   **
  1371   1378   ** Usage: %fossil test-file-environment FILENAME...
................................................................................
  1373   1380   ** Display the effective file handling subsystem "settings" and then
  1374   1381   ** display file system information about the files specified, if any.
  1375   1382   **
  1376   1383   ** Options:
  1377   1384   **
  1378   1385   **     --allow-symlinks BOOLEAN     Temporarily turn allow-symlinks on/off
  1379   1386   **     --open-config                Open the configuration database first.
  1380         -**     --slash                      Trailing slashes, if any, are retained.
  1381   1387   **     --reset                      Reset cached stat() info for each file.
         1388  +**     --root ROOT                  Use ROOT as the root of the checkout
         1389  +**     --slash                      Trailing slashes, if any, are retained.
  1382   1390   */
  1383   1391   void cmd_test_file_environment(void){
  1384   1392     int i;
  1385   1393     int slashFlag = find_option("slash",0,0)!=0;
  1386   1394     int resetFlag = find_option("reset",0,0)!=0;
         1395  +  const char *zRoot = find_option("root",0,1);
  1387   1396     const char *zAllow = find_option("allow-symlinks",0,1);
  1388   1397     if( find_option("open-config", 0, 0)!=0 ){
  1389   1398       Th_OpenConfig(1);
  1390   1399     }
  1391   1400     db_find_and_open_repository(OPEN_ANY_SCHEMA|OPEN_OK_NOT_FOUND, 0);
  1392   1401     fossil_print("filenames_are_case_sensitive() = %d\n",
  1393   1402                  filenames_are_case_sensitive());
  1394         -  fossil_print("db_allow_symlinks_by_default() = %d\n",
  1395         -               db_allow_symlinks_by_default());
  1396   1403     if( zAllow ){
  1397   1404       g.allowSymlinks = !is_false(zAllow);
  1398   1405     }
         1406  +  if( zRoot==0 ) zRoot = g.zLocalRoot;
  1399   1407     fossil_print("db_allow_symlinks() = %d\n", db_allow_symlinks());
         1408  +  fossil_print("local-root = [%s]\n", zRoot);
  1400   1409     for(i=2; i<g.argc; i++){
         1410  +    char *z;
  1401   1411       emitFileStat(g.argv[i], slashFlag, resetFlag);
         1412  +    z = file_canonical_name_dup(g.argv[i]);
         1413  +    fossil_print("  file_canonical_name    = %s\n", z);
         1414  +    fossil_print("  file_nondir_path       = ");
         1415  +    if( fossil_strnicmp(zRoot,z,(int)strlen(zRoot))!=0 ){
         1416  +      fossil_print("(--root is not a prefix of this file)\n");
         1417  +    }else{
         1418  +      int n = file_nondir_objects_on_path(zRoot, z);
         1419  +      fossil_print("%.*s\n", n, z);
         1420  +    }
         1421  +    fossil_free(z);
  1402   1422     }
  1403   1423   }
  1404   1424   
  1405   1425   /*
  1406   1426   ** COMMAND: test-canonical-name
  1407   1427   **
  1408   1428   ** Usage: %fossil test-canonical-name FILENAME...
................................................................................
  2470   2490     if( dryRunFlag!=0 ){
  2471   2491       fossil_print("dry-run: would have touched %d file(s)\n",
  2472   2492                    changeCount);
  2473   2493     }else{
  2474   2494       fossil_print("Touched %d file(s)\n", changeCount);
  2475   2495     }
  2476   2496   }
         2497  +
         2498  +/*
         2499  +** If zFileName is not NULL and contains a '.', this returns a pointer
         2500  +** to the position after the final '.', else it returns NULL. As a
         2501  +** special case, if it ends with a period then a pointer to the
         2502  +** terminating NUL byte is returned.
         2503  +*/
         2504  +const char * file_extension(const char *zFileName){
         2505  +  const char * zExt = zFileName ? strrchr(zFileName, '.') : 0;
         2506  +  return zExt ? &zExt[1] : 0;
         2507  +}
  2477   2508   
  2478   2509   /*
  2479   2510   ** Returns non-zero if the specified file name ends with any reserved name,
  2480   2511   ** e.g.: _FOSSIL_ or .fslckout.  Specifically, it returns 1 for exact match
  2481   2512   ** or 2 for a tail match on a longer file name.
  2482   2513   **
  2483   2514   ** For the sake of efficiency, zFilename must be a canonical name, e.g. an
................................................................................
  2522   2553         if( 8==nFilename ) return 1;
  2523   2554         return zEnd[-9]=='/' ? 2 : gotSuffix;
  2524   2555       }
  2525   2556       case 'T':
  2526   2557       case 't':{
  2527   2558         if( nFilename<9 || zEnd[-9]!='.'
  2528   2559          || fossil_strnicmp(".fslckout", &zEnd[-9], 9) ){
  2529         -        return 0; 
         2560  +        return 0;
  2530   2561         }
  2531   2562         if( 9==nFilename ) return 1;
  2532   2563         return zEnd[-10]=='/' ? 2 : gotSuffix;
  2533   2564       }
  2534   2565       default:{
  2535   2566         return 0;
  2536   2567       }
  2537   2568     }
  2538   2569   }
  2539         -
  2540         -/*
  2541         -** COMMAND: test-is-reserved-name
  2542         -**
  2543         -** Usage: %fossil test-is-ckout-db FILENAMES...
  2544         -**
  2545         -** Passes each given name to file_is_reserved_name() and outputs one
  2546         -** line per file: the result value of that function followed by the
  2547         -** name.
  2548         -*/
  2549         -void test_is_reserved_name_cmd(void){
  2550         -  int i;
  2551         -
  2552         -  if(g.argc<3){
  2553         -    usage("FILENAME_1 [...FILENAME_N]");
  2554         -  }
  2555         -  for( i = 2; i < g.argc; ++i ){
  2556         -    const int check = file_is_reserved_name(g.argv[i], -1);
  2557         -    fossil_print("%d %s\n", check, g.argv[i]);
  2558         -  }
  2559         -}

Changes to src/fileedit.c.

  1631   1631       }
  1632   1632       return;
  1633   1633     }
  1634   1634   
  1635   1635     db_begin_transaction();
  1636   1636     CheckinMiniInfo_init(&cimi);
  1637   1637     style_header("File Editor");
         1638  +  style_emit_noscript_for_js_page();
  1638   1639     /* As of this point, don't use return or fossil_fatal(). Write any
  1639   1640     ** error in (&err) and goto end_footer instead so that we can be
  1640   1641     ** sure to emit the error message, do any cleanup, and end the
  1641   1642     ** transaction cleanly.
  1642   1643     */
  1643   1644     {
  1644   1645       int isMissingArg = 0;
................................................................................
  1663   1664   
  1664   1665     /* The CSS for this page lives in a common file but much of it we
  1665   1666     ** don't want inadvertently being used by other pages. We don't
  1666   1667     ** have a common, page-specific container we can filter our CSS
  1667   1668     ** selectors, but we do have the BODY, which we can decorate with
  1668   1669     ** whatever CSS we wish...
  1669   1670     */
  1670         -  style_emit_script_tag(0,0);
         1671  +  style_script_begin(__FILE__,__LINE__);
  1671   1672     CX("document.body.classList.add('fileedit');\n");
  1672         -  style_emit_script_tag(1,0);
         1673  +  style_script_end();
  1673   1674     
  1674   1675     /* Status bar */
  1675   1676     CX("<div id='fossil-status-bar' "
  1676   1677        "title='Status message area. Double-click to clear them.'>"
  1677   1678        "Status messages will go here.</div>\n"
  1678   1679        /* will be moved into the tab container via JS */);
  1679   1680   
................................................................................
  1703   1704     {
  1704   1705       CX("<div id='fileedit-tab-content' "
  1705   1706          "data-tab-parent='fileedit-tabs' "
  1706   1707          "data-tab-label='File Content' "
  1707   1708          "class='hidden'"
  1708   1709          ">");
  1709   1710       CX("<div class='flex-container flex-row child-gap-small'>");
  1710         -    CX("<button class='fileedit-content-reload confirmer' "
  1711         -       "title='Reload the file from the server, discarding "
         1711  +    CX("<div class='input-with-label'>"
         1712  +       "<button class='fileedit-content-reload confirmer' "
         1713  +       ">Discard &amp; Reload</button>"
         1714  +       "<div class='help-buttonlet'>"
         1715  +       "Reload the file from the server, discarding "
  1712   1716          "any local edits. To help avoid accidental loss of "
  1713   1717          "edits, it requires confirmation (a second click) within "
  1714         -       "a few seconds or it will not reload.'"
  1715         -       ">Discard &amp; Reload</button>");
         1718  +       "a few seconds or it will not reload."
         1719  +       "</div>"
         1720  +       "</div>");
  1716   1721       style_select_list_int("select-font-size",
  1717   1722                             "editor_font_size", "Editor font size",
  1718   1723                             NULL/*tooltip*/,
  1719   1724                             100,
  1720   1725                             "100%", 100, "125%", 125,
  1721   1726                             "150%", 150, "175%", 175,
  1722   1727                             "200%", 200, NULL);
................................................................................
  1740   1745       CX("<button id='btn-preview-refresh' "
  1741   1746          "data-f-preview-from='fileContent' "
  1742   1747          /* ^^^ fossil.page[methodName]() OR text source elem ID,
  1743   1748         ** but we need a method in order to support clients swapping out
  1744   1749         ** the text editor with their own. */
  1745   1750          "data-f-preview-via='_postPreview' "
  1746   1751          /* ^^^ fossil.page[methodName](content, callback) */
  1747         -       "data-f-preview-to='#fileedit-tab-preview-wrapper' "
         1752  +       "data-f-preview-to='_previewTo' "
  1748   1753          /* ^^^ dest elem ID */
  1749   1754          ">Refresh</button>");
  1750   1755       /* Toggle auto-update of preview when the Preview tab is selected. */
  1751         -    style_labeled_checkbox("cb-preview-autoupdate",
  1752         -                           NULL,
  1753         -                           "Auto-refresh?",
  1754         -                           "1", 1,
  1755         -                           "If on, the preview will automatically "
  1756         -                           "refresh when this tab is selected.");
         1756  +    CX("<div class='input-with-label'>"
         1757  +       "<input type='checkbox' value='1' "
         1758  +       "id='cb-preview-autorefresh' checked>"
         1759  +       "<label for='cb-preview-autorefresh'>Auto-refresh?</label>"
         1760  +       "<div class='help-buttonlet'>"
         1761  +       "If on, the preview will automatically "
         1762  +       "refresh (if needed) when this tab is selected."
         1763  +       "</div>"
         1764  +       "</div>");
  1757   1765   
  1758   1766       /* Default preview rendering mode selection... */
  1759   1767       previewRenderMode = zFileMime
  1760   1768         ? ajax_render_mode_for_mimetype(zFileMime)
  1761   1769         : AJAX_RENDER_GUESS;
  1762   1770       style_select_list_int("select-preview-mode",
  1763   1771                             "preview_render_mode",
................................................................................
  1977   1985          "is unspecified and may differ across environments. When "
  1978   1986          "committing or force-reloading a file, local edits to that "
  1979   1987          "file/check-in combination are discarded.</li>");
  1980   1988       CX("</ul>");
  1981   1989     }
  1982   1990     CX("</div>"/*#fileedit-tab-help*/);
  1983   1991   
  1984         -  builtin_request_js("sbsdiff.js");
  1985         -  style_emit_fossil_js_apis(0, "fetch", "dom", "tabs", "confirmer",
  1986         -                            "storage", 0);
  1987         -  builtin_fulfill_js_requests();
         1992  +  builtin_fossil_js_bundle_or("fetch", "dom", "tabs", "confirmer",
         1993  +                              "storage", "popupwidget", "copybutton",
         1994  +                              "pikchr", 0);
  1988   1995     /*
  1989   1996     ** Set up a JS-side mapping of the AJAX_RENDER_xyz values. This is
  1990   1997     ** used for dynamically toggling certain UI components on and off.
  1991   1998     ** Must come after window.fossil has been intialized and before
  1992   1999     ** fossil.page.fileedit.js. Potential TODO: move this into the
  1993   2000     ** window.fossil bootstrapping so that we don't have to "fulfill"
  1994   2001     ** the JS multiple times.
  1995   2002     */
  1996   2003     ajax_emit_js_preview_modes(1);
         2004  +  builtin_request_js("sbsdiff.js");
  1997   2005     builtin_request_js("fossil.page.fileedit.js");
  1998   2006     builtin_fulfill_js_requests();
  1999   2007     {
  2000   2008       /* Dynamically populate the editor, display any error in the err
  2001   2009       ** blob, and/or switch to tab #0, where the file selector
  2002   2010       ** lives. The extra C scopes here correspond to JS-level scopes,
  2003   2011       ** to improve grokability. */
  2004         -    style_emit_script_tag(0,0);
         2012  +    style_script_begin(__FILE__,__LINE__);
  2005   2013       CX("\n(function(){\n");
  2006   2014       CX("try{\n");
  2007   2015       {
  2008   2016         char * zFirstLeafUuid = 0;
  2009   2017         CX("fossil.config['fileedit-glob'] = ");
  2010   2018         glob_render_json_to_cgi(fileedit_glob());
  2011   2019         CX(";\n");
................................................................................
  2046   2054         }
  2047   2055         CX("});\n")/*fossil.onPageLoad()*/;
  2048   2056       }
  2049   2057       CX("}catch(e){"
  2050   2058          "fossil.error(e); console.error('Exception:',e);"
  2051   2059          "}\n");
  2052   2060       CX("})();")/*anonymous function*/;
  2053         -    style_emit_script_tag(1,0);
         2061  +    style_script_end();
  2054   2062     }
  2055   2063     blob_reset(&err);
  2056   2064     CheckinMiniInfo_cleanup(&cimi);
  2057   2065     db_end_transaction(0);
  2058   2066     style_footer();
  2059   2067   }

Changes to src/finfo.c.

    42     42   **
    43     43   ** Options:
    44     44   **   -b|--brief           display a brief (one line / revision) summary
    45     45   **   --case-sensitive B   Enable or disable case-sensitive filenames.  B is a
    46     46   **                        boolean: "yes", "no", "true", "false", etc.
    47     47   **   -l|--log             select log mode (the default)
    48     48   **   -n|--limit N         Display the first N changes (default unlimited).
    49         -**                        N<=0 means no limit.
           49  +**                        N less than 0 means no limit.
    50     50   **   --offset P           skip P changes
    51     51   **   -p|--print           select print mode
    52     52   **   -r|--revision R      print the given revision (or ckout, if none is given)
    53     53   **                        to stdout (only in print mode)
    54     54   **   -s|--status          select status mode (print a status indicator for FILE)
    55         -**   -W|--width <num>     Width of lines (default is to auto-detect). Must be
    56         -**                        >22 or 0 (= no limit, resulting in a single line per
    57         -**                        entry).
           55  +**   -W|--width N         Width of lines (default is to auto-detect). Must be
           56  +**                        more than 22 or else 0 to indicate no limit.
    58     57   **
    59     58   ** See also: [[artifact]], [[cat]], [[descendants]], [[info]], [[leaves]]
    60     59   */
    61     60   void finfo_cmd(void){
    62     61     db_must_be_within_tree();
    63     62     if( find_option("status","s",0) ){
    64     63       Stmt q;
................................................................................
   270    269   }
   271    270   
   272    271   /* Values for the debug= query parameter to finfo */
   273    272   #define FINFO_DEBUG_MLINK  0x01
   274    273   
   275    274   /*
   276    275   ** WEBPAGE: finfo
   277         -** URL: /finfo?name=FILENAME
          276  +** Usage:
          277  +**   *  /finfo?name=FILENAME
          278  +**   *  /finfo?name=FILENAME&ci=HASH
   278    279   **
   279         -** Show the change history for a single file.
          280  +** Show the change history for a single file.  The name=FILENAME query
          281  +** parameter gives the filename and is a required parameter.  If the
          282  +** ci=HASH parameter is also supplied, then the FILENAME,HASH combination
          283  +** identifies a particular version of a file, and in that case all changes
          284  +** to that one file version are tracked across both edits and renames.
          285  +** If only the name=FILENAME parameter is supplied (if ci=HASH is omitted)
          286  +** then the graph shows all changes to any file while it happened
          287  +** to be called FILENAME and changes are not tracked across renames.
   280    288   **
   281    289   ** Additional query parameters:
   282    290   **
   283         -**    a=DATETIME Only show changes after DATETIME
   284         -**    b=DATETIME Only show changes before DATETIME
   285         -**    m=HASH     Mark this particular file version
   286         -**    n=NUM      Show the first NUM changes only
   287         -**    brbg       Background color by branch name
   288         -**    ubg        Background color by user name
   289         -**    ci=HASH    Ancestors of a particular check-in
   290         -**    orig=HASH  If both ci and orig are supplied, only show those
   291         -**                 changes on a direct path from orig to ci.
   292         -**    showid     Show RID values for debugging
          291  +**    a=DATETIME      Only show changes after DATETIME
          292  +**    b=DATETIME      Only show changes before DATETIME
          293  +**    ci=HASH         identify a particular version of a file and then
          294  +**                    track changes to that file across renames
          295  +**    m=HASH          Mark this particular file version.
          296  +**    n=NUM           Show the first NUM changes only
          297  +**    name=FILENAME   (Required) name of file whose history to show
          298  +**    brbg            Background color by branch name
          299  +**    ubg             Background color by user name
          300  +**    from=HASH       Ancestors only (not descendents) of the version of
          301  +**                    the file in this particular check-in.
          302  +**    to=HASH         If both from= and to= are supplied, only show those
          303  +**                    changes on the direct path between the two given
          304  +**                    checkins.
          305  +**    showid          Show RID values for debugging
          306  +**    showsql         Show the SQL query used to gather the data for
          307  +**                    the graph
   293    308   **
   294         -** DATETIME may be "now" or "YYYY-MM-DDTHH:MM:SS.SSS". If in
   295         -** year-month-day form, it may be truncated, and it may also name a
   296         -** timezone offset from UTC as "-HH:MM" (westward) or "+HH:MM"
   297         -** (eastward). Either no timezone suffix or "Z" means UTC.
          309  +** DATETIME may be in any of usual formats, including "now",
          310  +** "YYYY-MM-DDTHH:MM:SS.SSS", "YYYYMMDDHHMM", and others.
   298    311   */
   299    312   void finfo_page(void){
   300    313     Stmt q;
   301    314     const char *zFilename = PD("name","");
   302    315     char zPrevDate[20];
   303    316     const char *zA;
   304    317     const char *zB;
   305    318     int n;
   306         -  int baseCheckin;
   307         -  int origCheckin = 0;
          319  +  int ridFrom;
          320  +  int ridTo = 0;
          321  +  int ridCi = 0;
          322  +  const char *zCI = P("ci");
   308    323     int fnid;
   309    324     Blob title;
   310    325     Blob sql;
   311    326     HQuery url;
   312    327     GraphContext *pGraph;
   313    328     int brBg = P("brbg")!=0;
   314    329     int uBg = P("ubg")!=0;
................................................................................
   316    331     int fShowId = P("showid")!=0;
   317    332     Stmt qparent;
   318    333     int iTableId = timeline_tableid();
   319    334     int tmFlags = 0;            /* Viewing mode */
   320    335     const char *zStyle;         /* Viewing mode name */
   321    336     const char *zMark;          /* Mark this version of the file */
   322    337     int selRid = 0;             /* RID of the marked file version */
          338  +  int mxfnid;                 /* Maximum filename.fnid value */
   323    339   
   324    340     login_check_credentials();
   325    341     if( !g.perm.Read ){ login_needed(g.anon.Read); return; }
   326    342     fnid = db_int(0, "SELECT fnid FROM filename WHERE name=%Q", zFilename);
          343  +  ridCi = zCI ? name_to_rid_www("ci") : 0;
   327    344     if( fnid==0 ){
   328    345       style_header("No such file");
          346  +  }else if( ridCi==0 ){
          347  +    style_header("All files named \"%s\"", zFilename);
   329    348     }else{
   330         -    style_header("History for %s", zFilename);
          349  +    style_header("History of %s of %s",zFilename, zCI);
   331    350     }
   332    351     login_anonymous_available();
   333    352     tmFlags = timeline_ss_submenu();
   334    353     if( tmFlags & TIMELINE_COLUMNAR ){
   335    354       zStyle = "Columnar";
   336    355     }else if( tmFlags & TIMELINE_COMPACT ){
   337    356       zStyle = "Compact";
................................................................................
   341    360       zStyle = "Classic";
   342    361     }else{
   343    362       zStyle = "Modern";
   344    363     }
   345    364     url_initialize(&url, "finfo");
   346    365     if( brBg ) url_add_parameter(&url, "brbg", 0);
   347    366     if( uBg ) url_add_parameter(&url, "ubg", 0);
   348         -  baseCheckin = name_to_rid_www("ci");
          367  +  ridFrom = name_to_rid_www("from");
   349    368     zPrevDate[0] = 0;
   350    369     cookie_render();
   351    370     if( fnid==0 ){
   352    371       @ No such file: %h(zFilename)
   353    372       style_footer();
   354    373       return;
   355    374     }
   356    375     if( g.perm.Admin ){
   357    376       style_submenu_element("MLink Table", "%R/mlink?name=%t", zFilename);
   358    377     }
   359         -  if( baseCheckin ){
   360         -    if( P("orig")!=0 ){
   361         -      origCheckin = name_to_typed_rid(P("orig"),"ci");
   362         -      path_shortest_stored_in_ancestor_table(origCheckin, baseCheckin);
          378  +  if( ridFrom ){
          379  +    if( P("to")!=0 ){
          380  +      ridTo = name_to_typed_rid(P("to"),"ci");
          381  +      path_shortest_stored_in_ancestor_table(ridFrom,ridTo);
   363    382       }else{
   364         -      compute_direct_ancestors(baseCheckin);
          383  +      compute_direct_ancestors(ridFrom);
   365    384       }
   366    385     }
   367    386     url_add_parameter(&url, "name", zFilename);
   368    387     blob_zero(&sql);
          388  +  if( ridCi ){
          389  +    /* If we will be tracking changes across renames, some extra temp
          390  +    ** tables (implemented as CTEs) are required */
          391  +    blob_append_sql(&sql,
          392  +      /* The clade(fid,fnid) table is the set of all (fid,fnid) pairs
          393  +      ** that should participate in the output.  Clade is computed by
          394  +      ** walking the graph of mlink edges.
          395  +      */
          396  +      "WITH RECURSIVE clade(fid,fnid) AS (\n"
          397  +      "  SELECT blob.rid, %d FROM blob\n"         /* %d is fnid */
          398  +      "   WHERE blob.uuid=(SELECT uuid FROM files_of_checkin(%Q)"
          399  +                         " WHERE filename=%Q)\n"  /* %Q is the filename */
          400  +      "   UNION\n"
          401  +      "  SELECT mlink.fid, mlink.fnid\n"
          402  +      "    FROM clade, mlink\n"
          403  +      "   WHERE clade.fid=mlink.pid\n"
          404  +      "     AND ((mlink.pfnid=0 AND mlink.fnid=clade.fnid)\n"
          405  +      "          OR mlink.pfnid=clade.fnid)\n"
          406  +      "     AND (mlink.fid>0 OR NOT EXISTS(SELECT 1 FROM mlink AS mx"
          407  +                 " WHERE mx.mid=mlink.mid AND mx.pid=mlink.pid"
          408  +                 "   AND mx.fid>0 AND mx.pfnid=mlink.fnid))\n"
          409  +      "   UNION\n"
          410  +      "  SELECT mlink.pid,"
          411  +              " CASE WHEN mlink.pfnid>0 THEN mlink.pfnid ELSE mlink.fnid END\n"
          412  +      "    FROM clade, mlink\n"
          413  +      "   WHERE mlink.pid>0\n"
          414  +      "     AND mlink.fid=clade.fid\n"
          415  +      "     AND mlink.fnid=clade.fnid\n"
          416  +      ")\n",
          417  +      fnid, zCI, zFilename
          418  +    );
          419  +  }else{
          420  +    /* This is the case for all files with a given name.  We will still
          421  +    ** create a "clade(fid,fnid)" table that identifies all participates
          422  +    ** in the output graph, so that subsequent queries can all be the same,
          423  +    ** but in the case the clade table is much simplier, being just a
          424  +    ** single direct query against the mlink table.
          425  +    */
          426  +    blob_append_sql(&sql,
          427  +      "WITH clade(fid,fnid) AS (\n"
          428  +      "  SELECT DISTINCT fid, %d\n"
          429  +      "    FROM mlink\n"
          430  +      "   WHERE fnid=%d)",
          431  +      fnid, fnid
          432  +    );
          433  +  }
   369    434     blob_append_sql(&sql,
   370         -    "SELECT"
   371         -    " datetime(min(event.mtime),toLocal()),"         /* Date of change */
   372         -    " coalesce(event.ecomment, event.comment),"      /* Check-in comment */
   373         -    " coalesce(event.euser, event.user),"            /* User who made chng */
   374         -    " mlink.pid,"                                    /* Parent file rid */
   375         -    " mlink.fid,"                                    /* File rid */
   376         -    " (SELECT uuid FROM blob WHERE rid=mlink.pid),"  /* Parent file hash */
   377         -    " blob.uuid,"                                    /* Current file hash */
   378         -    " (SELECT uuid FROM blob WHERE rid=mlink.mid),"  /* Check-in hash */
   379         -    " event.bgcolor,"                                /* Background color */
   380         -    " (SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0"
   381         -                                " AND tagxref.rid=mlink.mid)," /* Branchname */
   382         -    " mlink.mid,"                                    /* check-in ID */
   383         -    " mlink.pfnid,"                                  /* Previous filename */
   384         -    " blob.size"                                     /* File size */
   385         -    "  FROM mlink, event, blob"
   386         -    " WHERE mlink.fnid=%d"
   387         -    "   AND event.objid=mlink.mid"
   388         -    "   AND mlink.fid=blob.rid",
   389         -    TAG_BRANCH, fnid
          435  +    "SELECT\n"
          436  +    "  datetime(min(event.mtime),toLocal()),\n"         /* Date of change */
          437  +    "  coalesce(event.ecomment, event.comment),\n"      /* Check-in comment */
          438  +    "  coalesce(event.euser, event.user),\n"            /* User who made chng */
          439  +    "  mlink.pid,\n"                                    /* Parent file rid */
          440  +    "  mlink.fid,\n"                                    /* File rid */
          441  +    "  (SELECT uuid FROM blob WHERE rid=mlink.pid),\n"  /* Parent file hash */
          442  +    "  blob.uuid,\n"                                    /* Current file hash */
          443  +    "  (SELECT uuid FROM blob WHERE rid=mlink.mid),\n"  /* Check-in hash */
          444  +    "  event.bgcolor,\n"                                /* Background color */
          445  +    "  (SELECT value FROM tagxref WHERE tagid=%d AND tagtype>0"
          446  +                             " AND tagxref.rid=mlink.mid),\n" /* Branchname */
          447  +    "  mlink.mid,\n"                                    /* check-in ID */
          448  +    "  mlink.pfnid,\n"                                  /* Previous filename */
          449  +    "  blob.size,\n"                                    /* File size */
          450  +    "  mlink.fnid,\n"                                   /* Current filename */
          451  +    "  filename.name\n"                                 /* Current filename */
          452  +    "FROM clade CROSS JOIN mlink, event"
          453  +    " LEFT JOIN blob ON blob.rid=clade.fid"
          454  +    " LEFT JOIN filename ON filename.fnid=clade.fnid\n"
          455  +    "WHERE mlink.fnid=clade.fnid AND mlink.fid=clade.fid\n"
          456  +    "  AND event.objid=mlink.mid\n",
          457  +    TAG_BRANCH
   390    458     );
   391    459     if( (zA = P("a"))!=0 ){
   392         -    blob_append_sql(&sql, " AND event.mtime>=julianday('%q')", zA);
          460  +    blob_append_sql(&sql, "  AND event.mtime>=%.16g\n",
          461  +         symbolic_name_to_mtime(zA,0));
   393    462       url_add_parameter(&url, "a", zA);
   394    463     }
   395    464     if( (zB = P("b"))!=0 ){
   396         -    blob_append_sql(&sql, " AND event.mtime<=julianday('%q')", zB);
          465  +    blob_append_sql(&sql, "  AND event.mtime<=%.16g\n",
          466  +         symbolic_name_to_mtime(zB,0));
   397    467       url_add_parameter(&url, "b", zB);
   398    468     }
   399         -  if( baseCheckin ){
          469  +  if( ridFrom ){
   400    470       blob_append_sql(&sql,
   401         -      " AND mlink.mid IN (SELECT rid FROM ancestor)"
   402         -      " GROUP BY mlink.fid"
          471  +      "  AND mlink.mid IN (SELECT rid FROM ancestor)\n"
          472  +      "GROUP BY mlink.fid\n"
   403    473       );
   404    474     }else{
   405    475       /* We only want each version of a file to appear on the graph once,
   406    476       ** at its earliest appearance.  All the other times that it gets merged
   407    477       ** into this or that branch can be ignored.  An exception is for when
   408    478       ** files are deleted (when they have mlink.fid==0).  If the same file
   409    479       ** is deleted in multiple places, we want to show each deletion, so
   410    480       ** use a "fake fid" which is derived from the parent-fid for grouping.
   411    481       ** The same fake-fid must be used on the graph.
   412    482       */
   413    483       blob_append_sql(&sql,
   414         -      " GROUP BY"
   415         -      "   CASE WHEN mlink.fid>0 THEN mlink.fid ELSE mlink.pid+1000000000 END"
          484  +      "GROUP BY"
          485  +      " CASE WHEN mlink.fid>0 THEN mlink.fid ELSE mlink.pid+1000000000 END,"
          486  +      " mlink.fnid\n"
   416    487       );
   417    488     }
   418         -  blob_append_sql(&sql, " ORDER BY event.mtime DESC /*sort*/");
          489  +  blob_append_sql(&sql, "ORDER BY event.mtime DESC");
   419    490     if( (n = atoi(PD("n","0")))>0 ){
   420    491       blob_append_sql(&sql, " LIMIT %d", n);
   421    492       url_add_parameter(&url, "n", P("n"));
   422    493     }
          494  +  blob_append_sql(&sql, " /*sort*/\n");
   423    495     db_prepare(&q, "%s", blob_sql_text(&sql));
   424    496     if( P("showsql")!=0 ){
   425         -    @ <p>SQL: %h(blob_str(&sql))</p>
          497  +    @ <p>SQL: <blockquote><pre>%h(blob_str(&sql))</blockquote></pre>
   426    498     }
   427    499     zMark = P("m");
   428    500     if( zMark ){
   429    501       selRid = symbolic_name_to_rid(zMark, "*");
   430    502     }
   431    503     blob_reset(&sql);
   432    504     blob_zero(&title);
   433         -  if( baseCheckin ){
   434         -    char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", baseCheckin);
          505  +  if( ridFrom ){
          506  +    char *zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", ridFrom);
   435    507       char *zLink = href("%R/info/%!S", zUuid);
   436         -    if( origCheckin ){
          508  +    if( ridTo ){
   437    509         blob_appendf(&title, "Changes to file ");
   438    510       }else if( n>0 ){
   439    511         blob_appendf(&title, "First %d ancestors of file ", n);
   440    512       }else{
   441    513         blob_appendf(&title, "Ancestors of file ");
   442    514       }
   443    515       blob_appendf(&title,"%z%h</a>",
   444    516                    href("%R/file?name=%T&ci=%!S", zFilename, zUuid),
   445    517                    zFilename);
   446    518       if( fShowId ) blob_appendf(&title, " (%d)", fnid);
   447         -    blob_append(&title, origCheckin ? " between " : " from ", -1);
          519  +    blob_append(&title, ridTo ? " between " : " from ", -1);
   448    520       blob_appendf(&title, "check-in %z%S</a>", zLink, zUuid);
   449         -    if( fShowId ) blob_appendf(&title, " (%d)", baseCheckin);
          521  +    if( fShowId ) blob_appendf(&title, " (%d)", ridFrom);
   450    522       fossil_free(zUuid);
   451         -    if( origCheckin ){
   452         -      zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", origCheckin);
          523  +    if( ridTo ){
          524  +      zUuid = db_text(0, "SELECT uuid FROM blob WHERE rid=%d", ridTo);
   453    525         zLink = href("%R/info/%!S", zUuid);
   454    526         blob_appendf(&title, " and check-in %z%S</a>", zLink, zUuid);
   455    527         fossil_free(zUuid);
   456    528       }
          529  +  }else if( ridCi ){
          530  +    blob_appendf(&title, "History of the file that is called ");
          531  +    hyperlinked_path(zFilename, &title, 0, "tree", "", LINKPATH_FILE);
          532  +    if( fShowId ) blob_appendf(&title, " (%d)", fnid);
          533  +    blob_appendf(&title, " at checkin %z%h</a>",
          534  +        href("%R/info?name=%t",zCI), zCI);
   457    535     }else{
   458    536       blob_appendf(&title, "History for ");
   459    537       hyperlinked_path(zFilename, &title, 0, "tree", "", LINKPATH_FILE);
   460    538       if( fShowId ) blob_appendf(&title, " (%d)", fnid);
   461    539     }
   462    540     if( uBg ){
   463    541       blob_append(&title, " (color-coded by user)", -1);
   464    542     }
   465    543     @ <h2>%b(&title)</h2>
   466    544     blob_reset(&title);
   467    545     pGraph = graph_init();
   468    546     @ <table id="timelineTable%d(iTableId)" class="timelineTable">
   469         -  if( baseCheckin ){
          547  +  mxfnid = db_int(0, "SELECT max(fnid) FROM filename");
          548  +  if( ridFrom ){
   470    549       db_prepare(&qparent,
   471         -      "SELECT DISTINCT pid FROM mlink"
          550  +      "SELECT DISTINCT pid*%d+CASE WHEN pfnid>0 THEN pfnid ELSE fnid END"
          551  +      "  FROM mlink"
   472    552         " WHERE fid=:fid AND mid=:mid AND pid>0 AND fnid=:fnid"
   473    553         "   AND pmid IN (SELECT rid FROM ancestor)"
   474         -      " ORDER BY isaux /*sort*/"
          554  +      " ORDER BY isaux /*sort*/", mxfnid+1
   475    555       );
   476    556     }else{
   477    557       db_prepare(&qparent,
   478         -      "SELECT DISTINCT pid FROM mlink"
          558  +      "SELECT DISTINCT pid*%d+CASE WHEN pfnid>0 THEN pfnid ELSE fnid END"
          559  +      "  FROM mlink"
   479    560         " WHERE fid=:fid AND mid=:mid AND pid>0 AND fnid=:fnid"
   480         -      " ORDER BY isaux /*sort*/"
          561  +      " ORDER BY isaux /*sort*/", mxfnid+1
   481    562       );
   482    563     }
   483    564     while( db_step(&q)==SQLITE_ROW ){
   484    565       const char *zDate = db_column_text(&q, 0);
   485    566       const char *zCom = db_column_text(&q, 1);
   486    567       const char *zUser = db_column_text(&q, 2);
   487    568       int fpid = db_column_int(&q, 3);
................................................................................
   490    571       const char *zUuid = db_column_text(&q, 6);
   491    572       const char *zCkin = db_column_text(&q,7);
   492    573       const char *zBgClr = db_column_text(&q, 8);
   493    574       const char *zBr = db_column_text(&q, 9);
   494    575       int fmid = db_column_int(&q, 10);
   495    576       int pfnid = db_column_int(&q, 11);
   496    577       int szFile = db_column_int(&q, 12);
          578  +    int fnid = db_column_int(&q, 13);
          579  +    const char *zFName = db_column_text(&q,14);
   497    580       int gidx;
   498    581       char zTime[10];
   499    582       int nParent = 0;
   500         -    int aParent[GR_MAX_RAIL];
          583  +    GraphRowId aParent[GR_MAX_RAIL];
   501    584   
   502    585       db_bind_int(&qparent, ":fid", frid);
   503    586       db_bind_int(&qparent, ":mid", fmid);
   504    587       db_bind_int(&qparent, ":fnid", fnid);
   505    588       while( db_step(&qparent)==SQLITE_ROW && nParent<count(aParent) ){
   506         -      aParent[nParent] = db_column_int(&qparent, 0);
          589  +      aParent[nParent] = db_column_int64(&qparent, 0);
   507    590         nParent++;
   508    591       }
   509    592       db_reset(&qparent);
   510    593       if( zBr==0 ) zBr = "trunk";
   511    594       if( uBg ){
   512    595         zBgClr = hash_color(zUser);
   513    596       }else if( brBg || zBgClr==0 || zBgClr[0]==0 ){
   514    597         zBgClr = strcmp(zBr,"trunk")==0 ? "" : hash_color(zBr);
   515    598       }
   516         -    gidx = graph_add_row(pGraph, frid>0 ? frid : fpid+1000000000,
   517         -                         nParent, 0, aParent, zBr, zBgClr,
   518         -                         zUuid, 0);
          599  +    gidx = graph_add_row(pGraph,
          600  +                   frid>0 ? (GraphRowId)frid*(mxfnid+1)+fnid : fpid+1000000000,
          601  +                   nParent, 0, aParent, zBr, zBgClr,
          602  +                   zUuid, 0);
   519    603       if( strncmp(zDate, zPrevDate, 10) ){
   520    604         sqlite3_snprintf(sizeof(zPrevDate), zPrevDate, "%.10s", zDate);
   521    605         @ <tr><td>
   522    606         @   <div class="divider timelineDate">%s(zPrevDate)</div>
   523    607         @ </td><td></td><td></td></tr>
   524    608       }
   525    609       memcpy(zTime, &zDate[11], 5);
................................................................................
   526    610       zTime[5] = 0;
   527    611       if( frid==selRid ){
   528    612         @ <tr class='timelineSelected'>
   529    613       }else{
   530    614         @ <tr>
   531    615       }
   532    616       @ <td class="timelineTime">\
   533         -    @ %z(href("%R/file?name=%T&ci=%!S",zFilename,zCkin))%s(zTime)</a></td>
          617  +    @ %z(href("%R/file?name=%T&ci=%!S",zFName,zCkin))%s(zTime)</a></td>
   534    618       @ <td class="timelineGraph"><div id="m%d(gidx)" class="tl-nodemark"></div>
   535    619       @ </td>
   536    620       if( zBgClr && zBgClr[0] ){
   537    621         @ <td class="timeline%s(zStyle)Cell" id='mc%d(gidx)'>
   538    622       }else{
   539    623         @ <td class="timeline%s(zStyle)Cell">
   540    624       }
   541    625       if( tmFlags & TIMELINE_COMPACT ){
   542    626         @ <span class='timelineCompactComment' data-id='%d(frid)'>
   543    627       }else{
   544    628         @ <span class='timeline%s(zStyle)Comment'>
          629  +      if( pfnid ){
          630  +        char *zPrevName = db_text(0,"SELECT name FROM filename WHERE fnid=%d",
          631  +                                   pfnid);
          632  +        @ <b>Renamed</b> %h(zPrevName) &rarr; %h(zFName).
          633  +        fossil_free(zPrevName);
          634  +      }
          635  +      if( zUuid && ridTo==0 && nParent==0 ){
          636  +        @ <b>Added:</b>
          637  +      }
          638  +      if( zUuid==0 ){
          639  +        char *zNewName;
          640  +        zNewName = db_text(0,
          641  +          "SELECT name FROM filename WHERE fnid = "
          642  +          "   (SELECT fnid FROM mlink"
          643  +          "     WHERE mid=%d"
          644  +          "       AND pfnid IN (SELECT fnid FROM filename WHERE name=%Q))",
          645  +          fmid, zFName);
          646  +        if( zNewName ){
          647  +          @ <b>Renamed</b> to
          648  +          @ %z(href("%R/finfo?name=%t",zNewName))%h(zNewName)</a>.
          649  +          fossil_free(zNewName);
          650  +        }else{
          651  +          @ <b>Deleted:</b>
          652  +        }
          653  +      }
   545    654         if( (tmFlags & TIMELINE_VERBOSE)!=0 && zUuid ){
   546    655           hyperlink_to_version(zUuid);
   547    656           @ part of check-in \
   548    657           hyperlink_to_version(zCkin);
   549    658         }
   550    659       }
   551    660       @ %W(zCom)</span>
................................................................................
   563    672       }
   564    673       if( tmFlags & TIMELINE_COMPACT ){
   565    674         cgi_printf("<span class='clutter' id='detail-%d'>",frid);
   566    675       }
   567    676       cgi_printf("<span class='timeline%sDetail'>", zStyle);
   568    677       if( tmFlags & (TIMELINE_COMPACT|TIMELINE_VERBOSE) ) cgi_printf("(");
   569    678       if( zUuid && (tmFlags & TIMELINE_VERBOSE)==0 ){
   570         -      @ file:&nbsp;%z(href("%R/file?name=%T&ci=%!S",zFilename,zCkin))[%S(zUuid)]</a>
          679  +      @ file:&nbsp;%z(href("%R/file?name=%T&ci=%!S",zFName,zCkin))\
          680  +      @ [%S(zUuid)]</a>
   571    681         if( fShowId ){
   572    682           int srcId = delta_source_rid(frid);
   573    683           if( srcId>0 ){
   574    684             @ id:&nbsp;%d(frid)&larr;%d(srcId)
   575    685           }else{
   576    686             @ id:&nbsp;%d(frid)
   577    687           }
................................................................................
   586    696       hyperlink_to_user(zUser, zDate, ",");
   587    697       @ branch:&nbsp;%z(href("%R/timeline?t=%T",zBr))%h(zBr)</a>,
   588    698       if( tmFlags & (TIMELINE_COMPACT|TIMELINE_VERBOSE) ){
   589    699         @ size:&nbsp;%d(szFile))
   590    700       }else{
   591    701         @ size:&nbsp;%d(szFile)
   592    702       }
   593         -    if( zUuid && origCheckin==0 ){
   594         -      if( nParent==0 ){
   595         -        @ <b>Added</b>
   596         -      }else if( pfnid ){
   597         -        char *zPrevName = db_text(0,"SELECT name FROM filename WHERE fnid=%d",
   598         -                                  pfnid);
   599         -        @ <b>Renamed</b> from
   600         -        @ %z(href("%R/finfo?name=%t", zPrevName))%h(zPrevName)</a>
   601         -      }
   602         -    }
   603         -    if( zUuid==0 ){
   604         -      char *zNewName;
   605         -      zNewName = db_text(0,
   606         -        "SELECT name FROM filename WHERE fnid = "
   607         -        "   (SELECT fnid FROM mlink"
   608         -        "     WHERE mid=%d"
   609         -        "       AND pfnid IN (SELECT fnid FROM filename WHERE name=%Q))",
   610         -        fmid, zFilename);
   611         -      if( zNewName ){
   612         -        @ <b>Renamed</b> to
   613         -        @ %z(href("%R/finfo?name=%t",zNewName))%h(zNewName)</a>
   614         -        fossil_free(zNewName);
   615         -      }else{
   616         -        @ <b>Deleted</b>
   617         -      }
   618         -    }
   619    703       if( g.perm.Hyperlink && zUuid ){
   620         -      const char *z = zFilename;
          704  +      const char *z = zFName;
   621    705         @ <span id='links-%d(frid)'><span class='timelineExtraLinks'>
   622    706         @ %z(href("%R/annotate?filename=%h&checkin=%s",z,zCkin))
   623    707         @ [annotate]</a>
   624    708         @ %z(href("%R/blame?filename=%h&checkin=%s",z,zCkin))
   625    709         @ [blame]</a>
   626    710         @ %z(href("%R/timeline?n=all&uf=%!S",zUuid))[check-ins&nbsp;using]</a>
   627    711         if( fpid>0 ){
   628    712           @ %z(href("%R/fdiff?v1=%!S&v2=%!S",zPUuid,zUuid))[diff]</a>
   629    713         }
   630         -      if( fileedit_is_editable(zFilename) ){
   631         -        @ %z(href("%R/fileedit?filename=%T&checkin=%!S",zFilename,zCkin))[edit]</a>
          714  +      if( fileedit_is_editable(zFName) ){
          715  +        @ %z(href("%R/fileedit?filename=%T&checkin=%!S",zFName,zCkin))\
          716  +        @ [edit]</a>
   632    717         }
   633    718         @ </span></span>
   634    719       }
   635    720       if( fDebug & FINFO_DEBUG_MLINK ){
   636    721         int ii;
   637    722         char *zAncLink;
   638         -      @ <br />fid=%d(frid) pid=%d(fpid) mid=%d(fmid)
          723  +      @ <br />fid=%d(frid) \
          724  +      @ graph-id=%lld(frid>0?(GraphRowId)frid*(mxfnid+1)+fnid:fpid+1000000000) \
          725  +      @ pid=%d(fpid) mid=%d(fmid) fnid=%d(fnid) \
          726  +      @ pfnid=%d(pfnid) mxfnid=%d(mxfnid)
   639    727         if( nParent>0 ){
   640         -        @ parents=%d(aParent[0])
          728  +        @ parents=%lld(aParent[0])
   641    729           for(ii=1; ii<nParent; ii++){
   642         -          @ %d(aParent[ii])
          730  +          @ %lld(aParent[ii])
   643    731           }
   644    732         }
   645         -      zAncLink = href("%R/finfo?name=%T&ci=%!S&debug=1",zFilename,zCkin);
          733  +      zAncLink = href("%R/finfo?name=%T&from=%!S&debug=1",zFName,zCkin);
   646    734         @ %z(zAncLink)[ancestry]</a>
   647    735       }
   648    736       tag_private_status(frid);
   649    737       /* End timelineDetail */
   650    738       if( tmFlags & TIMELINE_COMPACT ){
   651    739         @ </span></span>
   652    740       }else{

Changes to src/forum.c.

    24     24   /*
    25     25   ** Default to using Markdown markup
    26     26   */
    27     27   #define DEFAULT_FORUM_MIMETYPE  "text/x-markdown"
    28     28   
    29     29   #if INTERFACE
    30     30   /*
    31         -** Each instance of the following object represents a single message - 
           31  +** Each instance of the following object represents a single message -
    32     32   ** either the initial post, an edit to a post, a reply, or an edit to
    33     33   ** a reply.
    34     34   */
    35         -struct ForumEntry {
    36         -  int fpid;              /* rid for this entry */
    37         -  int fprev;             /* zero if initial entry.  non-zero if an edit */
    38         -  int firt;              /* This entry replies to firt */
    39         -  int mfirt;             /* Root in-reply-to */
    40         -  int nReply;            /* Number of replies to this entry */
           35  +struct ForumPost {
           36  +  int fpid;              /* rid for this post */
    41     37     int sid;               /* Serial ID number */
           38  +  int rev;               /* Revision number */
    42     39     char *zUuid;           /* Artifact hash */
    43         -  ForumEntry *pLeaf;     /* Most recent edit for this entry */
    44         -  ForumEntry *pEdit;     /* This entry is an edit of pEdit */
    45         -  ForumEntry *pNext;     /* Next in chronological order */
    46         -  ForumEntry *pPrev;     /* Previous in chronological order */
    47         -  ForumEntry *pDisplay;  /* Next in display order */
    48         -  int nIndent;           /* Number of levels of indentation for this entry */
           40  +  char *zDisplayName;    /* Name of user who wrote this post */
           41  +  double rDate;          /* Date for this post */
           42  +  ForumPost *pIrt;       /* This post replies to pIrt */
           43  +  ForumPost *pEditHead;  /* Original, unedited post */
           44  +  ForumPost *pEditTail;  /* Most recent edit for this post */
           45  +  ForumPost *pEditNext;  /* This post is edited by pEditNext */
           46  +  ForumPost *pEditPrev;  /* This post is an edit of pEditPrev */
           47  +  ForumPost *pNext;      /* Next in chronological order */
           48  +  ForumPost *pPrev;      /* Previous in chronological order */
           49  +  ForumPost *pDisplay;   /* Next in display order */
           50  +  int nEdit;             /* Number of edits to this post */
           51  +  int nIndent;           /* Number of levels of indentation for this post */
    49     52   };
    50     53   
    51     54   /*
    52     55   ** A single instance of the following tracks all entries for a thread.
    53     56   */
    54     57   struct ForumThread {
    55         -  ForumEntry *pFirst;    /* First entry in chronological order */
    56         -  ForumEntry *pLast;     /* Last entry in chronological order */
    57         -  ForumEntry *pDisplay;  /* Entries in display order */
    58         -  ForumEntry *pTail;     /* Last on the display list */
           58  +  ForumPost *pFirst;     /* First post in chronological order */
           59  +  ForumPost *pLast;      /* Last post in chronological order */
           60  +  ForumPost *pDisplay;   /* Entries in display order */
           61  +  ForumPost *pTail;      /* Last on the display list */
    59     62     int mxIndent;          /* Maximum indentation level */
    60     63   };
    61     64   #endif /* INTERFACE */
    62     65   
    63     66   /*
    64         -** Return true if the forum entry with the given rid has been
           67  +** Return true if the forum post with the given rid has been
    65     68   ** subsequently edited.
    66     69   */
    67     70   int forum_rid_has_been_edited(int rid){
    68     71     static Stmt q;
    69     72     int res;
    70     73     db_static_prepare(&q,
    71     74        "SELECT 1 FROM forumpost A, forumpost B"
................................................................................
    77     80     return res;
    78     81   }
    79     82   
    80     83   /*
    81     84   ** Delete a complete ForumThread and all its entries.
    82     85   */
    83     86   static void forumthread_delete(ForumThread *pThread){
    84         -  ForumEntry *pEntry, *pNext;
    85         -  for(pEntry=pThread->pFirst; pEntry; pEntry = pNext){
    86         -    pNext = pEntry->pNext;
    87         -    fossil_free(pEntry->zUuid);
    88         -    fossil_free(pEntry);
           87  +  ForumPost *pPost, *pNext;
           88  +  for(pPost=pThread->pFirst; pPost; pPost = pNext){
           89  +    pNext = pPost->pNext;
           90  +    fossil_free(pPost->zUuid);
           91  +    fossil_free(pPost->zDisplayName);
           92  +    fossil_free(pPost);
    89     93     }
    90     94     fossil_free(pThread);
    91     95   }
    92     96   
    93         -#if 0 /* not used */
    94     97   /*
    95         -** Search a ForumEntry list forwards looking for the entry with fpid
           98  +** Search a ForumPost list forwards looking for the post with fpid
    96     99   */
    97         -static ForumEntry *forumentry_forward(ForumEntry *p, int fpid){
          100  +static ForumPost *forumpost_forward(ForumPost *p, int fpid){
    98    101     while( p && p->fpid!=fpid ) p = p->pNext;
    99    102     return p;
   100    103   }
   101         -#endif
   102    104   
   103    105   /*
   104         -** Search backwards for a ForumEntry
          106  +** Search backwards for a ForumPost
   105    107   */
   106         -static ForumEntry *forumentry_backward(ForumEntry *p, int fpid){
          108  +static ForumPost *forumpost_backward(ForumPost *p, int fpid){
   107    109     while( p && p->fpid!=fpid ) p = p->pPrev;
   108    110     return p;
   109    111   }
   110    112   
   111    113   /*
   112         -** Add an entry to the display list
          114  +** Add a post to the display list
   113    115   */
   114         -static void forumentry_add_to_display(ForumThread *pThread, ForumEntry *p){
          116  +static void forumpost_add_to_display(ForumThread *pThread, ForumPost *p){
   115    117     if( pThread->pDisplay==0 ){
   116    118       pThread->pDisplay = p;
   117    119     }else{
   118    120       pThread->pTail->pDisplay = p;
   119    121     }
   120    122     pThread->pTail = p;
   121    123   }
   122    124   
   123    125   /*
   124    126   ** Extend the display list for pThread by adding all entries that
   125         -** reference fpid.  The first such entry will be no earlier then
   126         -** entry "p".
          127  +** reference fpid.  The first such post will be no earlier then
          128  +** post "p".
   127    129   */
   128    130   static void forumthread_display_order(
   129    131     ForumThread *pThread,    /* The complete thread */
   130         -  ForumEntry *pBase        /* Add replies to this entry */
          132  +  ForumPost *pBase         /* Add replies to this post */
   131    133   ){
   132         -  ForumEntry *p;
   133         -  ForumEntry *pPrev = 0;
          134  +  ForumPost *p;
          135  +  ForumPost *pPrev = 0;
          136  +  ForumPost *pBaseIrt;
   134    137     for(p=pBase->pNext; p; p=p->pNext){
   135         -    if( p->fprev==0 && p->mfirt==pBase->fpid ){
   136         -      if( pPrev ){
   137         -        pPrev->nIndent = pBase->nIndent + 1;
   138         -        forumentry_add_to_display(pThread, pPrev);
   139         -        forumthread_display_order(pThread, pPrev);
          138  +    if( !p->pEditPrev && p->pIrt ){
          139  +      pBaseIrt = p->pIrt->pEditHead ? p->pIrt->pEditHead : p->pIrt;
          140  +      if( pBaseIrt==pBase ){
          141  +        if( pPrev ){
          142  +          pPrev->nIndent = pBase->nIndent + 1;
          143  +          forumpost_add_to_display(pThread, pPrev);
          144  +          forumthread_display_order(pThread, pPrev);
          145  +        }
          146  +        pPrev = p;
   140    147         }
   141         -      pBase->nReply++;
   142         -      pPrev = p;
   143    148       }
   144    149     }
   145    150     if( pPrev ){
   146    151       pPrev->nIndent = pBase->nIndent + 1;
   147    152       if( pPrev->nIndent>pThread->mxIndent ) pThread->mxIndent = pPrev->nIndent;
   148         -    forumentry_add_to_display(pThread, pPrev);
          153  +    forumpost_add_to_display(pThread, pPrev);
   149    154       forumthread_display_order(pThread, pPrev);
   150    155     }
   151    156   }
   152    157   
   153    158   /*
   154    159   ** Construct a ForumThread object given the root record id.
   155    160   */
   156    161   static ForumThread *forumthread_create(int froot, int computeHierarchy){
   157    162     ForumThread *pThread;
   158         -  ForumEntry *pEntry;
          163  +  ForumPost *pPost;
          164  +  ForumPost *p;
   159    165     Stmt q;
   160    166     int sid = 1;
   161         -  Bag seen = Bag_INIT;
          167  +  int firt, fprev;
   162    168     pThread = fossil_malloc( sizeof(*pThread) );
   163    169     memset(pThread, 0, sizeof(*pThread));
   164    170     db_prepare(&q,
   165         -     "SELECT fpid, firt, fprev, (SELECT uuid FROM blob WHERE rid=fpid)"
          171  +     "SELECT fpid, firt, fprev, (SELECT uuid FROM blob WHERE rid=fpid), fmtime"
   166    172        "  FROM forumpost"
   167    173        " WHERE froot=%d ORDER BY fmtime",
   168    174        froot
   169    175     );
   170    176     while( db_step(&q)==SQLITE_ROW ){
   171         -    pEntry = fossil_malloc( sizeof(*pEntry) );
   172         -    memset(pEntry, 0, sizeof(*pEntry));
   173         -    pEntry->fpid = db_column_int(&q, 0);
   174         -    pEntry->firt = db_column_int(&q, 1);
   175         -    pEntry->fprev = db_column_int(&q, 2);
   176         -    pEntry->zUuid = fossil_strdup(db_column_text(&q,3));
   177         -    pEntry->mfirt = pEntry->firt;
   178         -    pEntry->sid = sid++;
   179         -    pEntry->pPrev = pThread->pLast;
   180         -    pEntry->pNext = 0;
   181         -    bag_insert(&seen, pEntry->fpid);
          177  +    pPost = fossil_malloc( sizeof(*pPost) );
          178  +    memset(pPost, 0, sizeof(*pPost));
          179  +    pPost->fpid = db_column_int(&q, 0);
          180  +    firt = db_column_int(&q, 1);
          181  +    fprev = db_column_int(&q, 2);
          182  +    pPost->zUuid = fossil_strdup(db_column_text(&q,3));
          183  +    pPost->rDate = db_column_double(&q,4);
          184  +    if( !fprev ) pPost->sid = sid++;
          185  +    pPost->pPrev = pThread->pLast;
          186  +    pPost->pNext = 0;
   182    187       if( pThread->pLast==0 ){
   183         -      pThread->pFirst = pEntry;
   184         -    }else{
   185         -      pThread->pLast->pNext = pEntry;
   186         -    }
   187         -    if( pEntry->firt && !bag_find(&seen,pEntry->firt) ){
   188         -      pEntry->firt = froot;
   189         -      pEntry->mfirt = froot;
   190         -    }
   191         -    pThread->pLast = pEntry;
   192         -  }
   193         -  db_finalize(&q);
   194         -  bag_clear(&seen);
   195         -
   196         -  /* Establish which entries are the latest edit.  After this loop
   197         -  ** completes, entries that have non-NULL pLeaf should not be
   198         -  ** displayed.
   199         -  */
   200         -  for(pEntry=pThread->pFirst; pEntry; pEntry=pEntry->pNext){
   201         -    if( pEntry->fprev ){
   202         -      ForumEntry *pBase = 0, *p;
   203         -      p = forumentry_backward(pEntry->pPrev, pEntry->fprev);
   204         -      pEntry->pEdit = p;
   205         -      while( p ){
   206         -        pBase = p;
   207         -        p->pLeaf = pEntry;
   208         -        p = pBase->pEdit;
   209         -      }
   210         -      for(p=pEntry->pNext; p; p=p->pNext){
   211         -        if( p->mfirt==pEntry->fpid ) p->mfirt = pBase->fpid;
   212         -      }
   213         -    }
   214         -  }
          188  +      pThread->pFirst = pPost;
          189  +    }else{
          190  +      pThread->pLast->pNext = pPost;
          191  +    }
          192  +    pThread->pLast = pPost;
          193  +
          194  +    /* Find the in-reply-to post.  Default to the topic post if the replied-to
          195  +    ** post cannot be found. */
          196  +    if( firt ){
          197  +      pPost->pIrt = pThread->pFirst;
          198  +      for(p=pThread->pFirst; p; p=p->pNext){
          199  +        if( p->fpid==firt ){
          200  +          pPost->pIrt = p;
          201  +          break;
          202  +        }
          203  +      }
          204  +    }
          205  +
          206  +    /* Maintain the linked list of post edits. */
          207  +    if( fprev ){
          208  +      p = forumpost_backward(pPost->pPrev, fprev);
          209  +      p->pEditNext = pPost;
          210  +      pPost->sid = p->sid;
          211  +      pPost->rev = p->rev+1;
          212  +      pPost->nEdit = p->nEdit+1;
          213  +      pPost->pEditPrev = p;
          214  +      pPost->pEditHead = p->pEditHead ? p->pEditHead : p;
          215  +      for(; p; p=p->pEditPrev ){
          216  +        p->nEdit = pPost->nEdit;
          217  +        p->pEditTail = pPost;
          218  +      }
          219  +    }
          220  +  }
          221  +  db_finalize(&q);
   215    222   
   216    223     if( computeHierarchy ){
   217    224       /* Compute the hierarchical display order */
   218         -    pEntry = pThread->pFirst;
   219         -    pEntry->nIndent = 1;
          225  +    pPost = pThread->pFirst;
          226  +    pPost->nIndent = 1;
   220    227       pThread->mxIndent = 1;
   221         -    forumentry_add_to_display(pThread, pEntry);
   222         -    forumthread_display_order(pThread, pEntry);
          228  +    forumpost_add_to_display(pThread, pPost);
          229  +    forumthread_display_order(pThread, pPost);
   223    230     }
   224    231   
   225    232     /* Return the result */
   226    233     return pThread;
   227    234   }
   228    235   
   229    236   /*
................................................................................
   263    270   ** This command is intended for testing an analysis only.
   264    271   */
   265    272   void forumthread_cmd(void){
   266    273     int fpid;
   267    274     int froot;
   268    275     const char *zName;
   269    276     ForumThread *pThread;
   270         -  ForumEntry *p;
          277  +  ForumPost *p;
   271    278   
   272    279     db_find_and_open_repository(0,0);
   273    280     verify_all_options();
   274    281     if( g.argc==2 ){
   275    282       forum_thread_list();
   276    283       return;
   277    284     }
................................................................................
   291    298     fossil_print("fpid  = %d\n", fpid);
   292    299     fossil_print("froot = %d\n", froot);
   293    300     pThread = forumthread_create(froot, 1);
   294    301     fossil_print("Chronological:\n");
   295    302     fossil_print(
   296    303   /* 0         1         2         3         4         5         6         7    */
   297    304   /*  123456789 123456789 123456789 123456789 123456789 123456789 123456789 123 */
   298         -  " sid      fpid      firt     fprev     mfirt     pLeaf  nReply  hash\n");
          305  +  " sid  rev      fpid      pIrt pEditPrev pEditTail hash\n");
   299    306     for(p=pThread->pFirst; p; p=p->pNext){
   300         -    fossil_print("%4d %9d %9d %9d %9d %9d  %6d  %8.8s\n", p->sid,
   301         -       p->fpid, p->firt, p->fprev, p->mfirt, p->pLeaf ? p->pLeaf->fpid : 0,
   302         -       p->nReply, p->zUuid);
          307  +    fossil_print("%4d %4d %9d %9d %9d %9d %8.8s\n", p->sid, p->rev,
          308  +       p->fpid, p->pIrt ? p->pIrt->fpid : 0,
          309  +       p->pEditPrev ? p->pEditPrev->fpid : 0,
          310  +       p->pEditTail ? p->pEditTail->fpid : 0, p->zUuid);
   303    311     }
   304    312     fossil_print("\nDisplay\n");
   305    313     for(p=pThread->pDisplay; p; p=p->pDisplay){
   306    314       fossil_print("%*s", (p->nIndent-1)*3, "");
   307         -    if( p->pLeaf ){
   308         -      fossil_print("%d->%d\n", p->fpid, p->pLeaf->fpid);
          315  +    if( p->pEditTail ){
          316  +      fossil_print("%d->%d\n", p->fpid, p->pEditTail->fpid);
   309    317       }else{
   310    318         fossil_print("%d\n", p->fpid);
   311    319       }
   312    320     }
   313    321     forumthread_delete(pThread);
   314    322   }
   315    323   
................................................................................
   331    339         @ <h1>%h(zTitle)</h1>
   332    340       }else{
   333    341         @ <h1><i>Deleted</i></h1>
   334    342       }
   335    343     }
   336    344     if( zContent && zContent[0] ){
   337    345       Blob x;
          346  +    const int isFossilWiki = zMimetype==0
          347  +      || fossil_strcmp(zMimetype, "text/x-fossil-wiki")==0;
   338    348       if( bScroll ){
   339    349         @ <div class='forumPostBody'>
   340    350       }else{
   341    351         @ <div class='forumPostFullBody'>
   342    352       }
   343    353       blob_init(&x, 0, 0);
   344    354       blob_append(&x, zContent, -1);
   345    355       safe_html_context(DOCSRC_FORUM);
          356  +    if( isFossilWiki ){
          357  +      /* Markdown and plain-text rendering add a wrapper DIV resp. PRE
          358  +      ** element around the post, and some CSS relies on its existence
          359  +      ** in order to handle expansion/collapse of the post. Fossil
          360  +      ** Wiki rendering does not do so, so we must wrap those manually
          361  +      ** here. */
          362  +      @ <div class='fossilWiki'>
          363  +    }
   346    364       wiki_render_by_mimetype(&x, zMimetype);
          365  +    if( isFossilWiki ){
          366  +      @ </div>
          367  +    }
   347    368       blob_reset(&x);
   348    369       @ </div>
   349    370     }else{
   350    371       @ <i>Deleted</i>
   351    372     }
   352    373     if( zClass ){
   353    374       @ </div>
   354    375     }
   355    376   }
   356    377   
   357         -/*
   358         -** Generate the buttons in the display that allow a forum supervisor to
   359         -** mark a user as trusted.  Only do this if:
   360         -**
   361         -**   (1)  The poster is an individual, not a special user like "anonymous"
   362         -**   (2)  The current user has Forum Supervisor privilege
   363         -*/
   364         -static void generateTrustControls(Manifest *pPost){
   365         -  if( !g.perm.AdminForum ) return;
   366         -  if( login_is_special(pPost->zUser) ) return;
   367         -  @ <br>
   368         -  @ <label><input type="checkbox" name="trust">
   369         -  @ Trust user "%h(pPost->zUser)"
   370         -  @ so that future posts by "%h(pPost->zUser)" do not require moderation.
   371         -  @ </label>
   372         -  @ <input type="hidden" name="trustuser" value="%h(pPost->zUser)">
   373         -}
   374         -
   375    378   /*
   376    379   ** Compute a display name from a login name.
   377    380   **
   378    381   ** If the input login is found in the USER table, then check the USER.INFO
   379    382   ** field to see if it has display-name followed by an email address.
   380    383   ** If it does, that becomes the new display name.  If not, let the display
   381    384   ** name just be the login.
   382    385   **
   383    386   ** Space to hold the returned name is obtained from fossil_strdup() or
   384    387   ** mprintf() and should be freed by the caller.
   385    388   */
   386         -char *display_name_from_login(const char *zLogin){
          389  +static char *display_name_from_login(const char *zLogin){
   387    390     static Stmt q;
   388    391     char *zResult;
   389    392     db_static_prepare(&q,
   390    393        "SELECT display_name(info) FROM user WHERE login=$login"
   391    394     );
   392    395     db_bind_text(&q, "$login", zLogin);
   393    396     if( db_step(&q)==SQLITE_ROW && db_column_type(&q,0)==SQLITE_TEXT ){
................................................................................
   401    404       zResult = fossil_strdup(zLogin);
   402    405     }
   403    406     db_reset(&q);
   404    407     return zResult;
   405    408   }
   406    409   
   407    410   /*
   408         -** Display all posts in a forum thread in chronological order
   409         -*/
   410         -static void forum_display_chronological(int froot, int target, int bRawMode){
   411         -  ForumThread *pThread = forumthread_create(froot, 0);
   412         -  ForumEntry *p;
   413         -  int notAnon = login_is_individual();
   414         -  char cMode = bRawMode ? 'r' : 'c';
   415         -  for(p=pThread->pFirst; p; p=p->pNext){
   416         -    char *zDate;
   417         -    Manifest *pPost;
   418         -    int isPrivate;        /* True for posts awaiting moderation */
   419         -    int sameUser;         /* True if author is also the reader */
   420         -    const char *zUuid;
   421         -    char *zDisplayName;   /* The display name */
   422         -    int sid;
   423         -
   424         -    pPost = manifest_get(p->fpid, CFTYPE_FORUM, 0);
   425         -    if( pPost==0 ) continue;
   426         -    if( p->fpid==target ){
   427         -      @ <div id="forum%d(p->fpid)" class="forumTime forumSel">
   428         -    }else if( p->pLeaf!=0 ){
   429         -      @ <div id="forum%d(p->fpid)" class="forumTime forumObs">
          411  +** Compute and return the display name for a ForumPost.  If
          412  +** pManifest is not NULL, then it is a Manifest object for the post.
          413  +** if pManifest is NULL, this routine has to fetch and parse the
          414  +** Manifest object for itself.
          415  +**
          416  +** Memory to hold the display name is attached to p->zDisplayName
          417  +** and will be freed together with the ForumPost object p when it
          418  +** is freed.
          419  +*/
          420  +static char *forum_post_display_name(ForumPost *p, Manifest *pManifest){
          421  +  Manifest *pToFree = 0;
          422  +  if( p->zDisplayName ) return p->zDisplayName;
          423  +  if( pManifest==0 ){
          424  +    pManifest = pToFree = manifest_get(p->fpid, CFTYPE_FORUM, 0);
          425  +    if( pManifest==0 ) return "(unknown)";
          426  +  }
          427  +  p->zDisplayName = display_name_from_login(pManifest->zUser);
          428  +  if( pToFree ) manifest_destroy(pToFree);
          429  +  if( p->zDisplayName==0 ) return "(unknown)";
          430  +  return p->zDisplayName;
          431  +}
          432  +
          433  +
          434  +/*
          435  +** Display a single post in a forum thread.
          436  +*/
          437  +static void forum_display_post(
          438  +  ForumPost *p,         /* Forum post to display */
          439  +  int iIndentScale,     /* Indent scale factor */
          440  +  int bRaw,             /* True to omit the border */
          441  +  int bUnf,             /* True to leave the post unformatted */
          442  +  int bHist,            /* True if showing edit history */
          443  +  int bSelect,          /* True if this is the selected post */
          444  +  char *zQuery          /* Common query string */
          445  +){
          446  +  char *zPosterName;    /* Name of user who originally made this post */
          447  +  char *zEditorName;    /* Name of user who provided the current edit */
          448  +  char *zDate;          /* The time/date string */
          449  +  char *zHist;          /* History query string */
          450  +  Manifest *pManifest;  /* Manifest comprising the current post */
          451  +  int bPrivate;         /* True for posts awaiting moderation */
          452  +  int bSameUser;        /* True if author is also the reader */
          453  +  int iIndent;          /* Indent level */
          454  +  const char *zMimetype;/* Formatting MIME type */
          455  +
          456  +  /* Get the manifest for the post.  Abort if not found (e.g. shunned). */
          457  +  pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0);
          458  +  if( !pManifest ) return;
          459  +
          460  +  /* When not in raw mode, create the border around the post. */
          461  +  if( !bRaw ){
          462  +    /* Open the <div> enclosing the post.  Set the class string to mark the post
          463  +    ** as selected and/or obsolete. */
          464  +    iIndent = (p->pEditHead ? p->pEditHead->nIndent : p->nIndent)-1;
          465  +    @ <div id='forum%d(p->fpid)' class='forumTime\
          466  +    @ %s(bSelect ? " forumSel" : "")\
          467  +    @ %s(p->pEditTail ? " forumObs" : "")' \
          468  +    if( iIndent && iIndentScale ){
          469  +      @ style='margin-left:%d(iIndent*iIndentScale)ex;'>
          470  +    }else{
          471  +      @ >
          472  +    }
          473  +
          474  +    /* If this is the first post (or an edit thereof), emit the thread title. */
          475  +    if( pManifest->zThreadTitle ){
          476  +      @ <h1>%h(pManifest->zThreadTitle)</h1>
          477  +    }
          478  +
          479  +    /* Begin emitting the header line.  The forum of the title
          480  +    ** varies depending on whether:
          481  +    **    *  The post is unedited
          482  +    **    *  The post was last edited by the original author
          483  +    **    *  The post was last edited by a different person
          484  +    */
          485  +    if( p->pEditHead ){
          486  +      zDate = db_text(0, "SELECT datetime(%.17g)", p->pEditHead->rDate);
          487  +    }else{
          488  +      zPosterName = forum_post_display_name(p, pManifest);
          489  +      zEditorName = zPosterName;
          490  +    }
          491  +    zDate = db_text(0, "SELECT datetime(%.17g)", p->rDate);
          492  +    if( p->pEditPrev ){
          493  +      zPosterName = forum_post_display_name(p->pEditHead, 0);
          494  +      zEditorName = forum_post_display_name(p, pManifest);
          495  +      zHist = bHist ? "" : "&hist";
          496  +      @ <h3 class='forumPostHdr'>(%d(p->sid)\
          497  +      @ .%0*d(fossil_num_digits(p->nEdit))(p->rev)) \
          498  +      if( fossil_strcmp(zPosterName, zEditorName)==0 ){
          499  +        @ By %h(zPosterName) on %h(zDate) edited from \
          500  +        @ %z(href("%R/forumpost/%S?%s%s",p->pEditPrev->zUuid,zQuery,zHist))\
          501  +        @ %d(p->sid).%0*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev)</a>
          502  +      }else{
          503  +        @ Originally by %h(zPosterName) \
          504  +        @ with edits by %h(zEditorName) on %h(zDate) from \
          505  +        @ %z(href("%R/forumpost/%S?%s%s",p->pEditPrev->zUuid,zQuery,zHist))\
          506  +        @ %d(p->sid).%0*d(fossil_num_digits(p->nEdit))(p->pEditPrev->rev)</a>
          507  +      }
   430    508       }else{
   431         -      @ <div id="forum%d(p->fpid)" class="forumTime">
          509  +      zPosterName = forum_post_display_name(p, pManifest);
          510  +      @ <h3 class='forumPostHdr'>(%d(p->sid)) \
          511  +      @ By %h(zPosterName) on %h(zDate)
   432    512       }
   433         -    if( pPost->zThreadTitle ){
   434         -      @ <h1>%h(pPost->zThreadTitle)</h1>
   435         -    }
   436         -    zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate);
   437         -    zDisplayName = display_name_from_login(pPost->zUser);
   438         -    sid = p->pEdit ? p->pEdit->sid : p->sid;
   439         -    @ <h3 class='forumPostHdr'>(%d(sid)) By %h(zDisplayName) on %h(zDate)
   440         -    fossil_free(zDisplayName);
   441    513       fossil_free(zDate);
   442         -    if( p->pEdit ){
   443         -      @ edit of %z(href("%R/forumpost/%S?t=%c",p->pEdit->zUuid,cMode))\
   444         -      @ %d(p->pEdit->sid)</a>
   445         -    }
          514  +
          515  +
          516  +    /* If debugging is enabled, link to the artifact page. */
   446    517       if( g.perm.Debug ){
   447    518         @ <span class="debug">\
   448    519         @ <a href="%R/artifact/%h(p->zUuid)">(artifact-%d(p->fpid))</a></span>
   449    520       }
   450         -    if( p->firt ){
   451         -      ForumEntry *pIrt = p->pPrev;
   452         -      while( pIrt && pIrt->fpid!=p->firt ) pIrt = pIrt->pPrev;
   453         -      if( pIrt ){
   454         -        @ in reply to %z(href("%R/forumpost/%S?t=%c",pIrt->zUuid,cMode))\
   455         -        @ %d(pIrt->sid)</a>
          521  +
          522  +    /* If this is a reply, refer back to the parent post. */
          523  +    if( p->pIrt ){
          524  +      @ in reply to %z(href("%R/forumpost/%S?%s",p->pIrt->zUuid,zQuery))\
          525  +      @ %d(p->pIrt->sid)\
          526  +      if( p->pIrt->nEdit ){
          527  +        @ .%0*d(fossil_num_digits(p->pIrt->nEdit))(p->pIrt->rev)\
   456    528         }
          529  +      @ </a>
   457    530       }
   458         -    zUuid = p->zUuid;
   459         -    if( p->pLeaf ){
   460         -      @ updated by %z(href("%R/forumpost/%S?t=%c",p->pLeaf->zUuid,cMode))\
   461         -      @ %d(p->pLeaf->sid)</a>
   462         -      zUuid = p->pLeaf->zUuid;
          531  +
          532  +    /* If this post was later edited, refer forward to the next edit. */
          533  +    if( p->pEditNext ){
          534  +      @ updated by %z(href("%R/forumpost/%S?%s",p->pEditNext->zUuid,zQuery))\
          535  +      @ %d(p->pEditNext->sid)\
          536  +      @ .%0*d(fossil_num_digits(p->nEdit))(p->pEditNext->rev)</a>
   463    537       }
   464         -    if( p->fpid!=target ){
   465         -      @ %z(href("%R/forumpost/%S?t=%c",zUuid,cMode))[link]</a>
          538  +
          539  +    /* Provide a link to select the individual post. */
          540  +    if( !bSelect ){
          541  +      @ %z(href("%R/forumpost/%S?%s",p->zUuid,zQuery))[link]</a>
   466    542       }
   467         -    if( !bRawMode ){
   468         -      @ %z(href("%R/forumpost/%S?raw",zUuid))[source]</a>
          543  +
          544  +    /* Provide a link to the raw source code. */
          545  +    if( !bUnf ){
          546  +      @ %z(href("%R/forumpost/%S?raw",p->zUuid))[source]</a>
   469    547       }
   470         -    isPrivate = content_is_private(p->fpid);
   471         -    sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0;
   472    548       @ </h3>
   473         -    if( isPrivate && !g.perm.ModForum && !sameUser ){
   474         -      @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
          549  +  }
          550  +
          551  +  /* Check if this post is approved, also if it's by the current user. */
          552  +  bPrivate = content_is_private(p->fpid);
          553  +  bSameUser = login_is_individual()
          554  +           && fossil_strcmp(pManifest->zUser, g.zLogin)==0;
          555  +
          556  +  /* Render the post if the user is able to see it. */
          557  +  if( bPrivate && !g.perm.ModForum && !bSameUser ){
          558  +    @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
          559  +  }else{
          560  +    if( bRaw || bUnf || p->pEditTail ){
          561  +      zMimetype = "text/plain";
   475    562       }else{
   476         -      const char *zMimetype;
   477         -      if( bRawMode ){
   478         -        zMimetype = "text/plain";
   479         -      }else if( p->pLeaf!=0 ){
   480         -        zMimetype = "text/plain";
   481         -      }else{
   482         -        zMimetype = pPost->zMimetype;
   483         -      }
   484         -      forum_render(0, zMimetype, pPost->zWiki, 0, 1);
          563  +      zMimetype = pManifest->zMimetype;
   485    564       }
   486         -    if( g.perm.WrForum && p->pLeaf==0 ){
   487         -      int sameUser = login_is_individual()
   488         -                     && fossil_strcmp(pPost->zUser, g.zLogin)==0;
          565  +    forum_render(0, zMimetype, pManifest->zWiki, 0, !bRaw);
          566  +  }
          567  +
          568  +  /* When not in raw mode, finish creating the border around the post. */
          569  +  if( !bRaw ){
          570  +    /* If the user is able to write to the forum and if this post has not been
          571  +    ** edited, create a form with various interaction buttons. */
          572  +    if( g.perm.WrForum && !p->pEditTail ){
   489    573         @ <div><form action="%R/forumedit" method="POST">
   490    574         @ <input type="hidden" name="fpid" value="%s(p->zUuid)">
   491         -      if( !isPrivate ){
   492         -        /* Reply and Edit are only available if the post has already
   493         -        ** been approved */
          575  +      if( !bPrivate ){
          576  +        /* Reply and Edit are only available if the post has been approved. */
   494    577           @ <input type="submit" name="reply" value="Reply">
   495         -        if( g.perm.Admin || sameUser ){
          578  +        if( g.perm.Admin || bSameUser ){
   496    579             @ <input type="submit" name="edit" value="Edit">
   497    580             @ <input type="submit" name="nullout" value="Delete">
   498    581           }
   499    582         }else if( g.perm.ModForum ){
   500         -        /* Provide moderators with moderation buttons for posts that
   501         -        ** are pending moderation */
          583  +        /* Allow moderators to approve or reject pending posts.  Also allow
          584  +        ** forum supervisors to mark non-special users as trusted and therefore
          585  +        ** able to post unmoderated. */
   502    586           @ <input type="submit" name="approve" value="Approve">
   503    587           @ <input type="submit" name="reject" value="Reject">
   504         -        generateTrustControls(pPost);
   505         -      }else if( sameUser ){
   506         -        /* A post that is pending moderation can be deleted by the
   507         -        ** person who originally submitted the post */
          588  +        if( g.perm.AdminForum && !login_is_special(pManifest->zUser) ){
          589  +          @ <br><label><input type="checkbox" name="trust">
          590  +          @ Trust user "%h(pManifest->zUser)" so that future posts by \
          591  +          @ "%h(pManifest->zUser)" do not require moderation.
          592  +          @ </label>
          593  +          @ <input type="hidden" name="trustuser" value="%h(pManifest->zUser)">
          594  +        }
          595  +      }else if( bSameUser ){
          596  +        /* Allow users to delete (reject) their own pending posts. */
   508    597           @ <input type="submit" name="reject" value="Delete">
   509    598         }
   510    599         @ </form></div>
   511    600       }
   512         -    manifest_destroy(pPost);
   513    601       @ </div>
   514    602     }
   515    603   
   516         -  /* Undocumented "threadtable" query parameter causes thread table
   517         -  ** to be displayed for debugging purposes.
   518         -  */
          604  +  /* Clean up. */
          605  +  manifest_destroy(pManifest);
          606  +}
          607  +
          608  +/*
          609  +** Possible display modes for forum_display_thread().
          610  +*/
          611  +enum {
          612  +  FD_RAW,     /* Like FD_SINGLE, but additionally omit the border, force
          613  +              ** unformatted mode, and inhibit history mode */
          614  +  FD_SINGLE,  /* Render a single post and (optionally) its edit history */
          615  +  FD_CHRONO,  /* Render all posts in chronological order */
          616  +  FD_HIER,    /* Render all posts in an indented hierarchy */
          617  +};
          618  +
          619  +/*
          620  +** Display a forum thread.  If mode is FD_RAW or FD_SINGLE, display only a
          621  +** single post from the thread and (optionally) its edit history.
          622  +*/
          623  +static void forum_display_thread(
          624  +  int froot,            /* Forum thread root post ID */
          625  +  int fpid,             /* Selected forum post ID, or 0 if none selected */
          626  +  int mode,             /* Forum display mode, one of the FD_* enumerations */
          627  +  int bUnf,             /* True if rendering unformatted */
          628  +  int bHist             /* True if showing edit history, ignored for FD_RAW */
          629  +){
          630  +  ForumThread *pThread; /* Thread structure */
          631  +  ForumPost *pSelect;   /* Currently selected post, or NULL if none */
          632  +  ForumPost *p;         /* Post iterator pointer */
          633  +  char *zQuery;         /* Common query string */
          634  +  int iIndentScale = 4; /* Indent scale factor, measured in "ex" units */
          635  +  int sid;              /* Comparison serial ID */
          636  +
          637  +  /* In raw mode, force unformatted display and disable history. */
          638  +  if( mode == FD_RAW ){
          639  +    bUnf = 1;
          640  +    bHist = 0;
          641  +  }
          642  +
          643  +  /* Thread together the posts and (optionally) compute the hierarchy. */
          644  +  pThread = forumthread_create(froot, mode==FD_HIER);
          645  +
          646  +  /* Compute the appropriate indent scaling. */
          647  +  if( mode==FD_HIER ){
          648  +    iIndentScale = 4;
          649  +    while( iIndentScale>1 && iIndentScale*pThread->mxIndent>25 ){
          650  +      iIndentScale--;
          651  +    }
          652  +  }else{
          653  +    iIndentScale = 0;
          654  +  }
          655  +
          656  +  /* Find the selected post, or (depending on parameters) its latest edit. */
          657  +  pSelect = fpid ? forumpost_forward(pThread->pFirst, fpid) : 0;
          658  +  if( !bHist && mode!=FD_RAW && pSelect && pSelect->pEditTail ){
          659  +    pSelect = pSelect->pEditTail;
          660  +  }
          661  +
          662  +  /* When displaying only a single post, abort if no post was selected or the
          663  +  ** selected forum post does not exist in the thread.  Otherwise proceed to
          664  +  ** display the entire thread without marking any posts as selected. */
          665  +  if( !pSelect && (mode==FD_RAW || mode==FD_SINGLE) ){
          666  +    return;
          667  +  }
          668  +
          669  +  /* Create the common query string to append to nearly all post links. */
          670  +  zQuery = mode==FD_RAW ? 0 : mprintf("t=%c%s%s",
          671  +      mode==FD_SINGLE ? 's' : mode==FD_CHRONO ? 'c' : 'h',
          672  +      bUnf ? "&unf" : "", bHist ? "&hist" : "");
          673  +
          674  +  /* Identify which post to display first.  If history is shown, start with the
          675  +  ** original, unedited post.  Otherwise advance to the post's latest edit.  */
          676  +  if( mode==FD_RAW || mode==FD_SINGLE ){
          677  +    p = pSelect;
          678  +    if( bHist && p->pEditHead ) p = p->pEditHead;
          679  +  }else{
          680  +    p = mode==FD_CHRONO ? pThread->pFirst : pThread->pDisplay;
          681  +    if( !bHist && p->pEditTail ) p = p->pEditTail;
          682  +  }
          683  +
          684  +  /* Display the appropriate subset of posts in sequence. */
          685  +  while( p ){
          686  +    /* Display the post. */
          687  +    forum_display_post(p, iIndentScale, mode==FD_RAW,
          688  +        bUnf, bHist, p==pSelect, zQuery);
          689  +
          690  +    /* Advance to the next post in the thread. */
          691  +    if( mode==FD_CHRONO ){
          692  +      /* Chronological mode: display posts (optionally including edits) in their
          693  +      ** original commit order. */
          694  +      if( bHist ){
          695  +        p = p->pNext;
          696  +      }else{
          697  +        sid = p->sid;
          698  +        if( p->pEditHead ) p = p->pEditHead;
          699  +        do p = p->pNext; while( p && p->sid<=sid );
          700  +        if( p && p->pEditTail ) p = p->pEditTail;
          701  +      }
          702  +    }else if( bHist && p->pEditNext ){
          703  +      /* Hierarchical and single mode: display each post's edits in sequence. */
          704  +      p = p->pEditNext;
          705  +    }else if( mode==FD_HIER ){
          706  +      /* Hierarchical mode: after displaying with each post (optionally
          707  +      ** including edits), go to the next post in computed display order. */
          708  +      p = p->pEditHead ? p->pEditHead->pDisplay : p->pDisplay;
          709  +      if( !bHist && p && p->pEditTail ) p = p->pEditTail;
          710  +    }else{
          711  +      /* Single and raw mode: terminate after displaying the selected post and
          712  +      ** (optionally) its edits. */
          713  +      break;
          714  +    }
          715  +  }
          716  +
          717  +  /* Undocumented "threadtable" query parameter causes thread table to be
          718  +  ** displayed for debugging purposes. */
   519    719     if( PB("threadtable") ){
   520    720       @ <hr>
   521    721       @ <table border="1" cellpadding="3" cellspacing="0">
   522         -    @ <tr><th>sid<th>fpid<th>firt<th>fprev<th>mfirt<th>pLeaf<th>nReply<th>hash
          722  +    @ <tr><th>sid<th>rev<th>fpid<th>pIrt<th>pEditHead<th>pEditTail\
          723  +    @ <th>pEditNext<th>pEditPrev<th>pDisplay<th>hash
   523    724       for(p=pThread->pFirst; p; p=p->pNext){
   524         -      @ <tr><td>%d(p->sid)<td>%d(p->fpid)<td>%d(p->firt)\
   525         -      @ <td>%d(p->fprev)<td>%d(p->mfirt)\
   526         -      @ <td>%d(p->pLeaf?p->pLeaf->fpid:0)<td>%d(p->nReply)\
          725  +      @ <tr><td>%d(p->sid)<td>%d(p->rev)<td>%d(p->fpid)\
          726  +      @ <td>%d(p->pIrt ? p->pIrt->fpid : 0)\
          727  +      @ <td>%d(p->pEditHead ? p->pEditHead->fpid : 0)\
          728  +      @ <td>%d(p->pEditTail ? p->pEditTail->fpid : 0)\
          729  +      @ <td>%d(p->pEditNext ? p->pEditNext->fpid : 0)\
          730  +      @ <td>%d(p->pEditPrev ? p->pEditPrev->fpid : 0)\
          731  +      @ <td>%d(p->pDisplay ? p->pDisplay->fpid : 0)\
   527    732         @ <td>%S(p->zUuid)</tr>
   528    733       }
   529    734       @ </table>
   530    735     }
   531    736   
          737  +  /* Clean up. */
   532    738     forumthread_delete(pThread);
   533         -}
   534         -/*
   535         -** Display all the edit history of post "target".
   536         -*/
   537         -static void forum_display_history(int froot, int target, int bRawMode){
   538         -  ForumThread *pThread = forumthread_create(froot, 0);
   539         -  ForumEntry *p;
   540         -  int notAnon = login_is_individual();
   541         -  char cMode = bRawMode ? 'r' : 'c';
   542         -  ForumEntry *pLeaf = 0;
   543         -  int cnt = 0;
   544         -  for(p=pThread->pFirst; p; p=p->pNext){
   545         -    if( p->fpid==target ){
   546         -      pLeaf = p->pLeaf ? p->pLeaf : p;
   547         -      break;
   548         -    }
   549         -  }
   550         -  for(p=pThread->pFirst; p; p=p->pNext){
   551         -    char *zDate;
   552         -    Manifest *pPost;
   553         -    int isPrivate;        /* True for posts awaiting moderation */
   554         -    int sameUser;         /* True if author is also the reader */
   555         -    const char *zUuid;
   556         -    char *zDisplayName;   /* The display name */
   557         -
   558         -    if( p->fpid!=pLeaf->fpid && p->pLeaf!=pLeaf ) continue;
   559         -    cnt++;
   560         -    pPost = manifest_get(p->fpid, CFTYPE_FORUM, 0);
   561         -    if( pPost==0 ) continue;
   562         -    @ <div id="forum%d(p->fpid)" class="forumTime">
   563         -    zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate);
   564         -    zDisplayName = display_name_from_login(pPost->zUser);
   565         -    @ <h3 class='forumPostHdr'>(%d(p->sid)) By %h(zDisplayName) on %h(zDate)
   566         -    fossil_free(zDisplayName);
   567         -    fossil_free(zDate);
   568         -    if( g.perm.Debug ){
   569         -      @ <span class="debug">\
   570         -      @ <a href="%R/artifact/%h(p->zUuid)">(artifact-%d(p->fpid))</a></span>
   571         -    }
   572         -    if( p->firt && cnt==1 ){
   573         -      ForumEntry *pIrt = p->pPrev;
   574         -      while( pIrt && pIrt->fpid!=p->firt ) pIrt = pIrt->pPrev;
   575         -      if( pIrt ){
   576         -        @ in reply to %z(href("%R/forumpost/%S?t=%c",pIrt->zUuid,cMode))\
   577         -        @ %d(pIrt->sid)</a>
   578         -      }
   579         -    }
   580         -    zUuid = p->zUuid;
   581         -    @ %z(href("%R/forumpost/%S?t=c",zUuid))[link]</a>
   582         -    if( !bRawMode ){
   583         -      @ %z(href("%R/forumpost/%S?raw",zUuid))[source]</a>
   584         -    }
   585         -    isPrivate = content_is_private(p->fpid);
   586         -    sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0;
   587         -    @ </h3>
   588         -    if( isPrivate && !g.perm.ModForum && !sameUser ){
   589         -      @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
   590         -    }else{
   591         -      forum_render(0, bRawMode?"text/plain":pPost->zMimetype, pPost->zWiki,
   592         -                   0, 1);
   593         -    }
   594         -    if( g.perm.WrForum && p->pLeaf==0 ){
   595         -      int sameUser = login_is_individual()
   596         -                     && fossil_strcmp(pPost->zUser, g.zLogin)==0;
   597         -      @ <div><form action="%R/forumedit" method="POST">
   598         -      @ <input type="hidden" name="fpid" value="%s(p->zUuid)">
   599         -      if( !isPrivate ){
   600         -        /* Reply and Edit are only available if the post has already
   601         -        ** been approved */
   602         -        @ <input type="submit" name="reply" value="Reply">
   603         -        if( g.perm.Admin || sameUser ){
   604         -          @ <input type="submit" name="edit" value="Edit">
   605         -          @ <input type="submit" name="nullout" value="Delete">
   606         -        }
   607         -      }else if( g.perm.ModForum ){
   608         -        /* Provide moderators with moderation buttons for posts that
   609         -        ** are pending moderation */
   610         -        @ <input type="submit" name="approve" value="Approve">
   611         -        @ <input type="submit" name="reject" value="Reject">
   612         -        generateTrustControls(pPost);
   613         -      }else if( sameUser ){
   614         -        /* A post that is pending moderation can be deleted by the
   615         -        ** person who originally submitted the post */
   616         -        @ <input type="submit" name="reject" value="Delete">
   617         -      }
   618         -      @ </form></div>
   619         -    }
   620         -    manifest_destroy(pPost);
   621         -    @ </div>
   622         -  }
   623         -  forumthread_delete(pThread);
          739  +  fossil_free(zQuery);
   624    740   }
   625    741   
   626    742   /*
   627         -** Display all messages in a forumthread with indentation.
          743  +** Emit Forum Javascript which applies (or optionally can apply)
          744  +** to all forum-related pages. It does not include page-specific
          745  +** code (e.g. "forum.js").
   628    746   */
   629         -static int forum_display_hierarchical(int froot, int target){
   630         -  ForumThread *pThread;
   631         -  ForumEntry *p;
   632         -  Manifest *pPost, *pOPost;
   633         -  int fpid;
   634         -  const char *zUuid;
   635         -  char *zDate;
   636         -  const char *zSel;
   637         -  int notAnon = login_is_individual();
   638         -  int iIndentScale = 4;
   639         -
   640         -  pThread = forumthread_create(froot, 1);
   641         -  for(p=pThread->pFirst; p; p=p->pNext){
   642         -    if( p->fpid==target ){
   643         -      while( p->pEdit ) p = p->pEdit;
   644         -      target = p->fpid;
   645         -      break;
   646         -    }
   647         -  }
   648         -  while( iIndentScale>1 && iIndentScale*pThread->mxIndent>25 ){
   649         -    iIndentScale--;
   650         -  }
   651         -  for(p=pThread->pDisplay; p; p=p->pDisplay){
   652         -    int isPrivate;         /* True for posts awaiting moderation */
   653         -    int sameUser;          /* True if reader is also the poster */
   654         -    char *zDisplayName;    /* User name to be displayed */
   655         -    pOPost = manifest_get(p->fpid, CFTYPE_FORUM, 0);
   656         -    if( p->pLeaf ){
   657         -      fpid = p->pLeaf->fpid;
   658         -      zUuid = p->pLeaf->zUuid;
   659         -      pPost = manifest_get(fpid, CFTYPE_FORUM, 0);
   660         -    }else{
   661         -      fpid = p->fpid;
   662         -      zUuid = p->zUuid;
   663         -      pPost = pOPost;
   664         -    }
   665         -    zSel = p->fpid==target ? " forumSel" : "";
   666         -    if( p->nIndent==1 ){
   667         -      @ <div id='forum%d(fpid)' class='forumHierRoot%s(zSel)'>
   668         -    }else{
   669         -      @ <div id='forum%d(fpid)' class='forumHier%s(zSel)' \
   670         -      @ style='margin-left: %d((p->nIndent-1)*iIndentScale)ex;'>
   671         -    }
   672         -    if( pPost==0 ) continue;
   673         -    if( pPost->zThreadTitle ){
   674         -      @ <h1>%h(pPost->zThreadTitle)</h1>
   675         -    }
   676         -    zDate = db_text(0, "SELECT datetime(%.17g)", pOPost->rDate);
   677         -    zDisplayName = display_name_from_login(pOPost->zUser);
   678         -    @ <h3 class='forumPostHdr'>\
   679         -    @ (%d(p->sid)) By %h(zDisplayName) on %h(zDate)
   680         -    fossil_free(zDisplayName);
   681         -    fossil_free(zDate);
   682         -    if( g.perm.Debug ){
   683         -      @ <span class="debug">\
   684         -      @ <a href="%R/artifact/%h(p->zUuid)">(artifact-%d(p->fpid))</a></span>
   685         -    }
   686         -    if( p->pLeaf ){
   687         -      zDate = db_text(0, "SELECT datetime(%.17g)", pPost->rDate);
   688         -      if( fossil_strcmp(pOPost->zUser,pPost->zUser)==0 ){
   689         -        @ and edited on %h(zDate)
   690         -      }else{
   691         -        @ as edited by %h(pPost->zUser) on %h(zDate)
   692         -      }
   693         -      fossil_free(zDate);
   694         -      if( g.perm.Debug ){
   695         -        @ <span class="debug">\
   696         -        @ <a href="%R/artifact/%h(p->pLeaf->zUuid)">\
   697         -        @ (artifact-%d(p->pLeaf->fpid))</a></span>
   698         -      }
   699         -      @ %z(href("%R/forumpost/%S?t=y",p->zUuid))[history]</a>
   700         -      manifest_destroy(pOPost);
   701         -    }
   702         -    if( fpid!=target ){
   703         -      @ %z(href("%R/forumpost/%S",zUuid))[link]</a>
   704         -    }
   705         -    @ %z(href("%R/forumpost/%S?raw",zUuid))[source]</a>
   706         -    if( p->firt ){
   707         -      ForumEntry *pIrt = p->pPrev;
   708         -      while( pIrt && pIrt->fpid!=p->mfirt ) pIrt = pIrt->pPrev;
   709         -      if( pIrt ){
   710         -        @ in reply to %z(href("%R/forumpost/%S?t=h",pIrt->zUuid))\
   711         -        @ %d(pIrt->sid)</a>
   712         -      }
   713         -    }
   714         -    @ </h3>
   715         -    isPrivate = content_is_private(fpid);
   716         -    sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0;
   717         -    if( isPrivate && !g.perm.ModForum && !sameUser ){
   718         -      @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
   719         -    }else{
   720         -      forum_render(0, pPost->zMimetype, pPost->zWiki, 0, 1);
   721         -    }
   722         -    if( g.perm.WrForum ){
   723         -      @ <div><form action="%R/forumedit" method="POST">
   724         -      @ <input type="hidden" name="fpid" value="%s(zUuid)">
   725         -      if( !isPrivate ){
   726         -        /* Reply and Edit are only available if the post has already
   727         -        ** been approved */
   728         -        @ <input type="submit" name="reply" value="Reply">
   729         -        if( g.perm.Admin || sameUser ){
   730         -          @ <input type="submit" name="edit" value="Edit">
   731         -          @ <input type="submit" name="nullout" value="Delete">
   732         -        }
   733         -      }else if( g.perm.ModForum ){
   734         -        /* Provide moderators with moderation buttons for posts that
   735         -        ** are pending moderation */
   736         -        @ <input type="submit" name="approve" value="Approve">
   737         -        @ <input type="submit" name="reject" value="Reject">
   738         -        generateTrustControls(pPost);
   739         -      }else if( sameUser ){
   740         -        /* A post that is pending moderation can be deleted by the
   741         -        ** person who originally submitted the post */
   742         -        @ <input type="submit" name="reject" value="Delete">
   743         -      }
   744         -      @ </form></div>
   745         -    }
   746         -    manifest_destroy(pPost);
   747         -    @ </div>
   748         -  }
   749         -  forumthread_delete(pThread);
   750         -  return target;
   751         -}
   752         -
   753         -/*
   754         -** Emits all JS code required by /forumpost.
   755         -*/
   756         -static void forumpost_emit_page_js(){
   757         -  static int once = 0;
   758         -  if(0==once){
   759         -    once = 1;
   760         -    style_emit_script_fossil_bootstrap(1);
   761         -    builtin_request_js("forum.js");
   762         -    builtin_request_js("fossil.dom.js");
   763         -    builtin_request_js("fossil.page.forumpost.js");
   764         -  }
          747  +static void forum_emit_js(void){
          748  +  builtin_fossil_js_bundle_or("copybutton", "pikchr", 0);
          749  +  builtin_request_js("fossil.page.forumpost.js");
   765    750   }
   766    751   
   767    752   /*
   768    753   ** WEBPAGE: forumpost
   769    754   **
   770    755   ** Show a single forum posting. The posting is shown in context with
   771         -** it's entire thread.  The selected posting is enclosed within
          756  +** its entire thread.  The selected posting is enclosed within
   772    757   ** <div class='forumSel'>...</div>.  Javascript is used to move the
   773    758   ** selected posting into view after the page loads.
   774    759   **
   775    760   ** Query parameters:
   776    761   **
   777         -**   name=X        REQUIRED.  The hash of the post to display
   778         -**   t=MODE        Display mode.
   779         -**                   'c' for chronological
   780         -**                   'h' for hierarchical
   781         -**                   'a' for automatic
   782         -**                   'r' for raw
   783         -**                   'y' for history of post X only
   784         -**   raw           If present, show only the post specified and
   785         -**                 show its original unformatted source text.
          762  +**   name=X        REQUIRED.  The hash of the post to display.
          763  +**   t=a           Automatic display mode, i.e. hierarchical for
          764  +**                 desktop and chronological for mobile.  This is the
          765  +**                 default if the "t" query parameter is omitted.
          766  +**   t=c           Show posts in the order they were written.
          767  +**   t=h           Show posts usin hierarchical indenting.
          768  +**   t=s           Show only the post specified by "name=X".
          769  +**   t=r           Alias for "t=c&unf&hist".
          770  +**   t=y           Alias for "t=s&unf&hist".
          771  +**   raw           Alias for "t=s&unf".  Additionally, omit the border
          772  +**                 around the post, and ignore "t" and "hist".
          773  +**   unf           Show the original, unformatted source text.
          774  +**   hist          Show edit history in addition to current posts.
   786    775   */
   787    776   void forumpost_page(void){
   788    777     forumthread_page();
   789    778   }
   790    779   
   791         -/*
   792         -** Add an appropriate style_header() to include title of the
   793         -** given forum post.
   794         -*/
   795         -static int forumthread_page_header(int froot, int fpid){
   796         -  char *zThreadTitle = 0;
   797         -
   798         -  zThreadTitle = db_text("",
   799         -    "SELECT"
   800         -    " substr(event.comment,instr(event.comment,':')+2)"
   801         -    " FROM forumpost, event"
   802         -    " WHERE event.objid=forumpost.fpid"
   803         -    "   AND forumpost.fpid=%d;",
   804         -    fpid
   805         -  );
   806         -  style_header("%s%s", zThreadTitle, zThreadTitle[0] ? "" : "Forum");
   807         -  fossil_free(zThreadTitle);
   808         -  return 0;
   809         -}
   810         -
   811    780   /*
   812    781   ** WEBPAGE: forumthread
   813    782   **
   814    783   ** Show all forum messages associated with a particular message thread.
   815    784   ** The result is basically the same as /forumpost except that none of
   816    785   ** the postings in the thread are selected.
   817    786   **
   818    787   ** Query parameters:
   819    788   **
   820    789   **   name=X        REQUIRED.  The hash of any post of the thread.
   821         -**   t=MODE        Display mode. MODE is...
   822         -**                   'c' for chronological, or
   823         -**                   'h' for hierarchical, or
   824         -**                   'a' for automatic, or
   825         -**                   'r' for raw.
   826         -**   raw           Show only the post given by name= and show it unformatted
   827         -**   hist          Show only the edit history for the name= post
          790  +**   t=a           Automatic display mode, i.e. hierarchical for
          791  +**                 desktop and chronological for mobile.  This is the
          792  +**                 default if the "t" query parameter is omitted.
          793  +**   t=c           Show posts in the order they were written.
          794  +**   t=h           Show posts using hierarchical indenting.
          795  +**   unf           Show the original, unformatted source text.
          796  +**   hist          Show edit history in addition to current posts.
   828    797   */
   829    798   void forumthread_page(void){
   830    799     int fpid;
   831    800     int froot;
          801  +  char *zThreadTitle;
   832    802     const char *zName = P("name");
   833    803     const char *zMode = PD("t","a");
   834    804     int bRaw = PB("raw");
          805  +  int bUnf = PB("unf");
          806  +  int bHist = PB("hist");
          807  +  int mode = 0;
   835    808     login_check_credentials();
   836    809     if( !g.perm.RdForum ){
   837    810       login_needed(g.anon.RdForum);
   838    811       return;
   839    812     }
   840    813     if( zName==0 ){
   841    814       webpage_error("Missing \"name=\" query parameter");
................................................................................
   845    818       webpage_error("Unknown or ambiguous forum id: \"%s\"", zName);
   846    819     }
   847    820     froot = db_int(0, "SELECT froot FROM forumpost WHERE fpid=%d", fpid);
   848    821     if( froot==0 ){
   849    822       webpage_error("Not a forum post: \"%s\"", zName);
   850    823     }
   851    824     if( fossil_strcmp(g.zPath,"forumthread")==0 ) fpid = 0;
   852         -  if( zMode[0]=='a' ){
   853         -    if( cgi_from_mobile() ){
   854         -      zMode = "c";  /* Default to chronological on mobile */
   855         -    }else{
   856         -      zMode = "h";
   857         -    }
   858         -  }
   859         -  if( zMode[0]!='y' ){
   860         -    forumthread_page_header(froot, fpid);
   861         -  }
   862         -  if( bRaw && fpid ){
   863         -    Manifest *pPost;
   864         -    pPost = manifest_get(fpid, CFTYPE_FORUM, 0);
   865         -    if( pPost==0 ){
   866         -      @ <p>No such forum post: %h(zName)
   867         -    }else{
   868         -      int isPrivate = content_is_private(fpid);
   869         -      int notAnon = login_is_individual();
   870         -      int sameUser = notAnon && fossil_strcmp(pPost->zUser, g.zLogin)==0;
   871         -      if( isPrivate && !g.perm.ModForum && !sameUser ){
   872         -        @ <p><span class="modpending">Awaiting Moderator Approval</span></p>
   873         -      }else{
   874         -        forum_render(0, "text/plain", pPost->zWiki, 0, 0);
   875         -      }
   876         -      manifest_destroy(pPost);
   877         -    }
   878         -  }else if( zMode[0]=='c' ){
   879         -    style_submenu_element("Hierarchical", "%R/%s/%s?t=h", g.zPath, zName);
   880         -    style_submenu_element("Unformatted", "%R/%s/%s?t=r", g.zPath, zName);
   881         -    forum_display_chronological(froot, fpid, 0);
   882         -  }else if( zMode[0]=='r' ){
   883         -    style_submenu_element("Chronological", "%R/%s/%s?t=c", g.zPath, zName);
   884         -    style_submenu_element("Hierarchical", "%R/%s/%s?t=h", g.zPath, zName);
   885         -    forum_display_chronological(froot, fpid, 1);
   886         -  }else if( zMode[0]=='y' ){
   887         -    style_header("Edit History Of A Forum Post");
   888         -    style_submenu_element("Complete Thread", "%R/%s/%s?t=a", g.zPath, zName);
   889         -    forum_display_history(froot, fpid, 1);
          825  +
          826  +  /* Decode the mode parameters. */
          827  +  if( bRaw ){
          828  +    mode = FD_RAW;
          829  +    bUnf = 1;
          830  +    bHist = 0;
          831  +    cgi_replace_query_parameter("unf", "on");
          832  +    cgi_delete_query_parameter("hist");
          833  +    cgi_delete_query_parameter("raw");
   890    834     }else{
   891         -    style_submenu_element("Chronological", "%R/%s/%s?t=c", g.zPath, zName);
   892         -    style_submenu_element("Unformatted", "%R/%s/%s?t=r", g.zPath, zName);
   893         -    forum_display_hierarchical(froot, fpid);
          835  +    switch( *zMode ){
          836  +      case 'a': mode = cgi_from_mobile() ? FD_CHRONO : FD_HIER; break;
          837  +      case 'c': mode = FD_CHRONO; break;
          838  +      case 'h': mode = FD_HIER; break;
          839  +      case 's': mode = FD_SINGLE; break;
          840  +      case 'r': mode = FD_CHRONO; break;
          841  +      case 'y': mode = FD_SINGLE; break;
          842  +      default: webpage_error("Invalid thread mode: \"%s\"", zMode);
          843  +    }
          844  +    if( *zMode=='r' || *zMode=='y') {
          845  +      bUnf = 1;
          846  +      bHist = 1;
          847  +      cgi_replace_query_parameter("t", mode==FD_CHRONO ? "c" : "s");
          848  +      cgi_replace_query_parameter("unf", "on");
          849  +      cgi_replace_query_parameter("hist", "on");
          850  +    }
   894    851     }
   895         -  forumpost_emit_page_js();
          852  +
          853  +  /* Define the page header. */
          854  +  zThreadTitle = db_text("",
          855  +    "SELECT"
          856  +    " substr(event.comment,instr(event.comment,':')+2)"
          857  +    " FROM forumpost, event"
          858  +    " WHERE event.objid=forumpost.fpid"
          859  +    "   AND forumpost.fpid=%d;",
          860  +    fpid
          861  +  );
          862  +  style_header("%s%s", zThreadTitle, *zThreadTitle ? "" : "Forum");
          863  +  fossil_free(zThreadTitle);
          864  +  if( mode!=FD_CHRONO ){
          865  +    style_submenu_element("Chronological", "%R/%s/%s?t=c%s%s", g.zPath, zName,
          866  +        bUnf ? "&unf" : "", bHist ? "&hist" : "");
          867  +  }
          868  +  if( mode!=FD_HIER ){
          869  +    style_submenu_element("Hierarchical", "%R/%s/%s?t=h%s%s", g.zPath, zName,
          870  +        bUnf ? "&unf" : "", bHist ? "&hist" : "");
          871  +  }
          872  +  style_submenu_checkbox("unf", "Unformatted", 0, 0);
          873  +  style_submenu_checkbox("hist", "History", 0, 0);
          874  +
          875  +  /* Display the thread. */
          876  +  forum_display_thread(froot, fpid, mode, bUnf, bHist);
          877  +
          878  +  /* Emit Forum Javascript. */
          879  +  builtin_request_js("forum.js");
          880  +  forum_emit_js();
          881  +
          882  +  /* Emit the page style. */
   896    883     style_footer();
   897    884   }
   898    885   
   899    886   /*
   900    887   ** Return true if a forum post should be moderated.
   901    888   */
   902    889   static int forum_need_moderation(void){
................................................................................
   947    934       iBasis = iInReplyTo;
   948    935     }
   949    936     webpage_assert( (zTitle==0)+(iInReplyTo==0)==1 );
   950    937     blob_init(&x, 0, 0);
   951    938     zDate = date_in_standard_format("now");
   952    939     blob_appendf(&x, "D %s\n", zDate);
   953    940     fossil_free(zDate);
   954         -  zG = db_text(0, 
          941  +  zG = db_text(0,
   955    942        "SELECT uuid FROM blob, forumpost"
   956    943        " WHERE blob.rid==forumpost.froot"
   957    944        "   AND forumpost.fpid=%d", iBasis);
   958    945     if( zG ){
   959    946       blob_appendf(&x, "G %s\n", zG);
   960    947       fossil_free(zG);
   961    948     }
................................................................................
  1015   1002       return 1;
  1016   1003     }
  1017   1004   }
  1018   1005   
  1019   1006   /*
  1020   1007   ** Paint the form elements for entering a Forum post
  1021   1008   */
  1022         -static void forum_entry_widget(
         1009  +static void forum_post_widget(
  1023   1010     const char *zTitle,
  1024   1011     const char *zMimetype,
  1025   1012     const char *zContent
  1026   1013   ){
  1027   1014     if( zTitle ){
  1028   1015       @ Title: <input type="input" name="title" value="%h(zTitle)" size="50"
  1029   1016       @ maxlength="125"><br>
................................................................................
  1084   1071     @ <form action="%R/login" method="POST">
  1085   1072     @ <input type="hidden" name="g" value="%s(zGoto)">
  1086   1073     @ <input type="hidden" name="noanon" value="1">
  1087   1074     @ <input type="submit" value="Login">
  1088   1075     @ </form>
  1089   1076     @ <td>Log into an existing account
  1090   1077     @ </table>
         1078  +  forum_emit_js();
  1091   1079     style_footer();
  1092   1080     fossil_free(zGoto);
  1093   1081   }
  1094   1082   
  1095   1083   /*
  1096   1084   ** Write the "From: USER" line on the webpage.
  1097   1085   */
................................................................................
  1124   1112       @ <h1>Preview:</h1>
  1125   1113       forum_render(zTitle, zMimetype, zContent, "forumEdit", 1);
  1126   1114     }
  1127   1115     style_header("New Forum Thread");
  1128   1116     @ <form action="%R/forume1" method="POST">
  1129   1117     @ <h1>New Thread:</h1>
  1130   1118     forum_from_line();
  1131         -  forum_entry_widget(zTitle, zMimetype, zContent);
         1119  +  forum_post_widget(zTitle, zMimetype, zContent);
  1132   1120     @ <input type="submit" name="preview" value="Preview">
  1133   1121     if( P("preview") && !whitespace_only(zContent) ){
  1134   1122       @ <input type="submit" name="submit" value="Submit">
  1135   1123     }else{
  1136   1124       @ <input type="submit" name="submit" value="Submit" disabled>
  1137   1125     }
  1138   1126     if( g.perm.Debug ){
................................................................................
  1144   1132       @ <br><label><input type="checkbox" name="domod" %s(PCK("domod"))> \
  1145   1133       @ Require moderator approval</label>
  1146   1134       @ <br><label><input type="checkbox" name="showqp" %s(PCK("showqp"))> \
  1147   1135       @ Show query parameters</label>
  1148   1136       @ </div>
  1149   1137     }
  1150   1138     @ </form>
         1139  +  forum_emit_js();
  1151   1140     style_footer();
  1152   1141   }
  1153   1142   
  1154   1143   /*
  1155   1144   ** WEBPAGE: forume2
  1156   1145   **
  1157   1146   ** Edit an existing forum message.
  1158   1147   ** Query parameters:
  1159   1148   **
  1160         -**   fpid=X        Hash of the post to be editted.  REQUIRED
         1149  +**   fpid=X        Hash of the post to be edited.  REQUIRED
  1161   1150   */
  1162   1151   void forumedit_page(void){
  1163   1152     int fpid;
  1164   1153     int froot;
  1165   1154     Manifest *pPost = 0;
  1166   1155     Manifest *pRootPost = 0;
  1167   1156     const char *zMimetype = 0;
................................................................................
  1193   1182       if( P("approve") ){
  1194   1183         const char *zUserToTrust;
  1195   1184         moderation_approve('f', fpid);
  1196   1185         if( g.perm.AdminForum
  1197   1186          && PB("trust")
  1198   1187          && (zUserToTrust = P("trustuser"))!=0
  1199   1188         ){
         1189  +        db_unprotect(PROTECT_USER);
  1200   1190           db_multi_exec("UPDATE user SET cap=cap||'4' "
  1201   1191                         "WHERE login=%Q AND cap NOT GLOB '*4*'",
  1202   1192                         zUserToTrust);
         1193  +        db_protect_pop();
  1203   1194         }
  1204   1195         cgi_redirectf("%R/forumpost/%S",P("fpid"));
  1205   1196         return;
  1206   1197       }
  1207   1198       if( P("reject") ){
  1208         -      char *zParent = 
         1199  +      char *zParent =
  1209   1200           db_text(0,
  1210   1201             "SELECT uuid FROM forumpost, blob"
  1211   1202             " WHERE forumpost.fpid=%d AND blob.rid=forumpost.firt",
  1212   1203             fpid
  1213   1204           );
  1214   1205         moderation_disapprove(fpid);
  1215   1206         if( zParent ){
................................................................................
  1274   1265         forum_render(zTitle, zMimetype, zContent,"forumEdit", 1);
  1275   1266       }
  1276   1267       @ <h2>Revised Message:</h2>
  1277   1268       @ <form action="%R/forume2" method="POST">
  1278   1269       @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
  1279   1270       @ <input type="hidden" name="edit" value="1">
  1280   1271       forum_from_line();
  1281         -    forum_entry_widget(zTitle, zMimetype, zContent);
         1272  +    forum_post_widget(zTitle, zMimetype, zContent);
  1282   1273     }else{
  1283   1274       /* Reply */
  1284   1275       char *zDisplayName;
  1285   1276       zMimetype = PD("mimetype",DEFAULT_FORUM_MIMETYPE);
  1286   1277       zContent = PDT("content","");
  1287   1278       style_header("Reply");
  1288   1279       if( pRootPost->zThreadTitle ){
................................................................................
  1300   1291         forum_render(0, zMimetype,zContent, "forumEdit", 1);
  1301   1292       }
  1302   1293       @ <h2>Enter Reply:</h2>
  1303   1294       @ <form action="%R/forume2" method="POST">
  1304   1295       @ <input type="hidden" name="fpid" value="%h(P("fpid"))">
  1305   1296       @ <input type="hidden" name="reply" value="1">
  1306   1297       forum_from_line();
  1307         -    forum_entry_widget(0, zMimetype, zContent);
         1298  +    forum_post_widget(0, zMimetype, zContent);
  1308   1299     }
  1309   1300     if( !isDelete ){
  1310   1301       @ <input type="submit" name="preview" value="Preview">
  1311   1302     }
  1312   1303     @ <input type="submit" name="cancel" value="Cancel">
  1313   1304     if( (P("preview") && !whitespace_only(zContent)) || isDelete ){
  1314   1305       @ <input type="submit" name="submit" value="Submit">
................................................................................
  1321   1312       @ <br><label><input type="checkbox" name="domod" %s(PCK("domod"))> \
  1322   1313       @ Require moderator approval</label>
  1323   1314       @ <br><label><input type="checkbox" name="showqp" %s(PCK("showqp"))> \
  1324   1315       @ Show query parameters</label>
  1325   1316       @ </div>
  1326   1317     }
  1327   1318     @ </form>
         1319  +  forum_emit_js();
  1328   1320     style_footer();
  1329   1321   }
  1330   1322   
  1331   1323   /*
  1332   1324   ** WEBPAGE: forummain
  1333   1325   ** WEBPAGE: forum
  1334   1326   **

Changes to src/fossil.bootstrap.js.

     1      1   "use strict";
            2  +(function () {
            3  +  /* CustomEvent polyfill, courtesy of Mozilla:
            4  +     https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent
            5  +  */
            6  +  if(typeof window.CustomEvent === "function") return false;
            7  +  window.CustomEvent = function(event, params) {
            8  +    if(!params) params = {bubbles: false, cancelable: false, detail: null};
            9  +    const evt = document.createEvent('CustomEvent');
           10  +    evt.initCustomEvent( event, !!params.bubbles, !!params.cancelable, params.detail );
           11  +    return evt;
           12  +  };
           13  +})();
     2     14   (function(global){
     3         -  /* Bootstrapping bits for the global.fossil object. Must be
     4         -     loaded after style.c:style_emit_script_tag() has initialized
     5         -     that object.
           15  +  /* Bootstrapping bits for the global.fossil object. Must be loaded
           16  +     after style.c:builtin_emit_script_fossil_bootstrap() has
           17  +     initialized that object.
     6     18     */
     7     19   
     8     20     const F = global.fossil;
     9     21   
    10     22     /**
    11     23        Returns the current time in something approximating
    12     24        ISO-8601 format.
................................................................................
   185    197       const n = ('number'===typeof forUrl)
   186    198             ? forUrl : F.config[forUrl ? 'hashDigitsUrl' : 'hashDigits'];
   187    199       return ('string'==typeof hash ? hash.substr(
   188    200         0, n
   189    201       ) : hash);
   190    202     };
   191    203   
   192         -  /**
   193         -     Sets up pseudo-automatic content preview handling between a
   194         -     source element (typically a TEXTAREA) and a target rendering
   195         -     element (typically a DIV). The selector argument must be one of:
   196         -
   197         -     - A single DOM element
   198         -     - A collection of DOM elements with a forEach method.
   199         -     - A CSS selector
   200         -
   201         -     Each element in the collection must have the following data
   202         -     attributes:
   203         -
   204         -     - data-f-preview-from: is either a DOM element id, WITH a leading
   205         -     '#' prefix, or the name of a method (see below). If it's an ID,
   206         -     the DOM element must support .value to get the content.
   207         -
   208         -     - data-f-preview-to: the DOM element id of the target "previewer"
   209         -       element, WITH a leading '#', or the name of a method (see below).
   210         -
   211         -     - data-f-preview-via: the name of a method (see below).
   212         -
   213         -     - OPTIONAL data-f-preview-as-text: a numeric value. Explained below.
   214         -
   215         -     Each element gets a click handler added to it which does the
   216         -     following:
   217         -
   218         -     1) Reads the content from its data-f-preview-from element or, if
   219         -     that property refers to a method, calls the method without
   220         -     arguments and uses its result as the content.
   221         -
   222         -     2) Passes the content to
   223         -     methodNamespace[f-data-post-via](content,callback). f-data-post-via
   224         -     is responsible for submitting the preview HTTP request, including
   225         -     any parameters the request might require. When the response
   226         -     arrives, it must pass the content of the response to its 2nd
   227         -     argument, an auto-generated callback installed by this mechanism
   228         -     which...
   229         -
   230         -     3) Assigns the response text to the data-f-preview-to element or
   231         -     passes it to the function methodNamespace[f-data-preview-to](content), as
   232         -     appropriate. If data-f-preview-to is a DOM element and
   233         -     data-f-preview-as-text is '0' (the default) then the content is
   234         -     assigned to the target element's innerHTML property, else it is
   235         -     assigned to the element's textContent property.
   236         -
   237         -     The methodNamespace (2nd argument) defaults to fossil.page, and
   238         -     any method-name data properties, e.g. data-f-preview-via and
   239         -     potentially data-f-preview-from/to, must be a single method name,
   240         -     not a property-access-style string. e.g. "myPreview" is legal but
   241         -     "foo.myPreview" is not (unless, of course, the method is actually
   242         -     named "foo.myPreview" (which is legal but would be
   243         -     unconventional)).
   244         -
   245         -     An example...
   246         -
   247         -     First an input button:
   248         -
   249         -     <button id='test-preview-connector'
   250         -       data-f-preview-from='#fileedit-content-editor' // elem ID or method name
   251         -       data-f-preview-via='myPreview' // method name
   252         -       data-f-preview-to='#fileedit-tab-preview-wrapper' // elem ID or method name
   253         -     >Preview update</button>
   254         -
   255         -     And a sample data-f-preview-via method:
   256         -
   257         -     fossil.page.myPreview = function(content,callback){
   258         -       const fd = new FormData();
   259         -       fd.append('foo', ...);
   260         -       fossil.fetch('preview_forumpost',{
   261         -         payload: fd,
   262         -         onload: callback,
   263         -         onerror: (e)=>{ // only if app-specific handling is needed
   264         -           fossil.fetch.onerror(e); // default impl
   265         -           ... any app-specific error reporting ...
   266         -         }
   267         -       });
   268         -     };
   269         -
   270         -     Then connect the parts with:
   271         -
   272         -     fossil.connectPagePreviewers('#test-preview-connector');
   273         -
   274         -     Note that the data-f-preview-from, data-f-preview-via, and
   275         -     data-f-preview-to selector are not resolved until the button is
   276         -     actually clicked, so they need not exist in the DOM at the
   277         -     instant when the connection is set up, so long as they can be
   278         -     resolved when the preview-refreshing element is clicked.
   279         -  */
   280         -  F.connectPagePreviewers = function f(selector,methodNamespace){
   281         -    if('string'===typeof selector){
   282         -      selector = document.querySelectorAll(selector);
   283         -    }else if(!selector.forEach){
   284         -      selector = [selector];
   285         -    }
   286         -    if(!methodNamespace){
   287         -      methodNamespace = F.page;
   288         -    }
   289         -    selector.forEach(function(e){
   290         -      e.addEventListener(
   291         -        'click', function(r){
   292         -          const eTo = '#'===e.dataset.fPreviewTo[0]
   293         -                ? document.querySelector(e.dataset.fPreviewTo)
   294         -                : methodNamespace[e.dataset.fPreviewTo],
   295         -                eFrom = '#'===e.dataset.fPreviewFrom[0]
   296         -                ? document.querySelector(e.dataset.fPreviewFrom)
   297         -                : methodNamespace[e.dataset.fPreviewFrom],
   298         -                asText = +(e.dataset.fPreviewAsText || 0);
   299         -          eTo.textContent = "Fetching preview...";
   300         -          methodNamespace[e.dataset.fPreviewVia](
   301         -            (eFrom instanceof Function ? eFrom() : eFrom.value),
   302         -            (r)=>{
   303         -              if(eTo instanceof Function) eTo(r||'');
   304         -              else eTo[asText ? 'textContent' : 'innerHTML'] = r||'';
   305         -            }
   306         -          );
   307         -        }, false
   308         -      );
   309         -    });
   310         -    return this;
   311         -  };
   312         -
   313    204     /**
   314    205        Convenience wrapper which adds an onload event listener to the
   315    206        window object. Returns this.
   316    207     */
   317    208     F.onPageLoad = function(callback){
   318    209       window.addEventListener('load', callback, false);
   319    210       return this;
   320    211     };
          212  +
          213  +  /**
          214  +     Convenience wrapper which adds a DOMContentLoadedevent listener
          215  +     to the window object. Returns this.
          216  +  */
          217  +  F.onDOMContentLoaded = function(callback){
          218  +    window.addEventListener('DOMContentLoaded', callback, false);
          219  +    return this;
          220  +  };
   321    221   
   322    222     /**
   323    223        Assuming name is a repo-style filename, this function returns
   324    224        a shortened form of that name:
   325    225   
   326    226        .../LastDirectoryPart/FilenamePart
   327    227   

Changes to src/fossil.confirmer.js.

   116    116   
   117    117   - Add an invert option which activates if the timeout is reached and
   118    118   "times out" if the element is clicked again. e.g. a button which says
   119    119   "Saving..." and cancels the op if it's clicked again, else it saves
   120    120   after X time/ticks.
   121    121   
   122    122   - Internally we save/restore the initial text of non-INPUT elements
   123         -using innerHTML. We should instead move their child nodes aside (into
   124         -an internal out-of-DOM element) and restore them as needed.
          123  +using a relatively expensive bit of DOMParser hoop-jumping. We
          124  +"should" instead move their child nodes aside (into an internal
          125  +out-of-DOM element) and restore them as needed.
   125    126   
   126    127   Terse Change history:
   127    128   
   128    129   - 20200811
   129    130     - Added pinSize option.
   130    131   
   131    132   - 20200507:
................................................................................
   154    155           this.target = target;
   155    156           this.opt = opt;
   156    157           this.timerID = undefined;
   157    158           this.state = this.states.initial;
   158    159           const isInput = f.isInput(target);
   159    160           const updateText = function(msg){
   160    161             if(isInput) target.value = msg;
   161         -          else target.innerHTML = msg;
          162  +          else{
          163  +            /* Jump through some hoops to avoid assigning to innerHTML... */
          164  +            const newNode = new DOMParser().parseFromString(msg, 'text/html');
          165  +            let childs = newNode.documentElement.querySelector('body');
          166  +            childs = childs ? Array.prototype.slice.call(childs.childNodes, 0) : [];
          167  +            target.innerText = '';
          168  +            childs.forEach((e)=>target.appendChild(e));
          169  +          }
   162    170           }
   163    171           const formatCountdown = (txt, number) => txt + " ["+number+"]";
   164    172           if(opt.pinSize && opt.confirmText){
   165    173             /* Try to pin the element's width the the greater of its
   166    174                current width or its waiting-on-confirmation width
   167    175                to avoid layout reflow when it's activated. */
   168    176             const digits = (''+(opt.timeout/1000 || opt.ticks)).length;
   169         -          const lblLong = formatCountdown(opt.confirmText, "00000000".substr(0,digits));
   170         -          const w1 = parseFloat(window.getComputedStyle(target).width);
          177  +          const lblLong = formatCountdown(opt.confirmText, "00000000".substr(0,digits+1));
          178  +          const w1 = parseInt(target.getBoundingClientRect().width);
   171    179             updateText(lblLong);
   172         -          const w2 = parseFloat(window.getComputedStyle(target).width);
   173         -          target.style.minWidth = target.style.maxWidth = (w1>w2 ? w1 : w2)+"px";
          180  +          const w2 = parseInt(target.getBoundingClientRect().width);
          181  +          if(w1 || w2){
          182  +            /* If target is not in visible part of the DOM, those values may be 0. */
          183  +            target.style.minWidth = target.style.maxWidth = (w1>w2 ? w1 : w2)+"px";
          184  +          }
   174    185           }
   175    186           updateText(this.opt.initialText);
   176    187           if(this.opt.ticks && !this.opt.ontick){
   177    188             this.opt.ontick = function(tick){
   178    189               updateText(formatCountdown(self.opt.confirmText,tick));
   179    190             };
   180    191           }

Added src/fossil.copybutton.js.

            1  +(function(F/*fossil object*/){
            2  +  /**
            3  +     A basic API for creating and managing a copy-to-clipboard button.
            4  +
            5  +     Requires: fossil.bootstrap, fossil.dom
            6  +  */
            7  +  const D = F.dom;
            8  +
            9  +  /**
           10  +     Initializes element e as a copy button using the given options
           11  +     object.
           12  +
           13  +     The first argument may be a DOM element or a string (CSS selector
           14  +     suitable for use with document.querySelector()).
           15  +
           16  +     Options:
           17  +
           18  +     .copyFromElement: DOM element
           19  +
           20  +     .copyFromId: DOM element ID
           21  +
           22  +     One of copyFromElement or copyFromId must be provided, but copyFromId
           23  +     may optionally be provided via e.dataset.copyFromId.
           24  +
           25  +     .extractText: optional callback which is triggered when the copy
           26  +     button is clicked. I tmust return the text to copy to the
           27  +     clipboard. The default is to extract it from the copy-from
           28  +     element, using its [value] member, if it has one, else its
           29  +     [innerText]. A client-provided callback may use any data source
           30  +     it likes, so long as it's synchronous. If this function returns a
           31  +     falsy value then the clipboard is not modified. This function is
           32  +     called with the fully expanded/resolved options object as its
           33  +     "this" (that's a different instance than the one passed to this
           34  +     function!).
           35  +
           36  +     .cssClass: optional CSS class, or list of classes, to apply to e.
           37  +
           38  +     .style: optional object of properties to copy directly into
           39  +     e.style.
           40  +
           41  +     .oncopy: an optional callback function which is added as an event
           42  +     listener for the 'text-copied' event (see below). There is
           43  +     functionally no difference from setting this option or adding a
           44  +     'text-copied' event listener to the element, and this option is
           45  +     considered to be a convenience form of that. For the sake of
           46  +     framework-level consistency, the default value is a callback
           47  +     which passes the copy button to fossil.dom.flashOnce().
           48  +
           49  +     Note that this function's own defaultOptions object holds default
           50  +     values for some options. Any changes made to that object affect
           51  +     any future calls to this function.
           52  +
           53  +     Be aware that clipboard functionality might or might not be
           54  +     available in any given environment. If this button appears to
           55  +     have no effect, that may be because it is not enabled/available
           56  +     in the current platform.
           57  +
           58  +     The copy button emits custom event 'text-copied' after it has
           59  +     successfully copied text to the clipboard. The event's "detail"
           60  +     member is an object with a "text" property holding the copied
           61  +     text. Other properties may be added in the future. The event is
           62  +     not fired if copying to the clipboard fails (e.g. is not
           63  +     available in the current environment).
           64  +
           65  +     As a special case, the copy button's click handler is suppressed
           66  +     (becomes a no-op) for as long as the element has the CSS class
           67  +     "disabled". This allows elements which cannot be disabled via
           68  +     HTML attributes, e.g. a SPAN, to act as a copy button while still
           69  +     providing a way to disable them.
           70  +
           71  +     Returns the copy-initialized element.
           72  +
           73  +     Example:
           74  +
           75  +     const button = fossil.copyButton('#my-copy-button', {
           76  +       copyFromId: 'some-other-element-id'
           77  +     });
           78  +     button.addEventListener('text-copied',function(ev){
           79  +       fossil.dom.flashOnce(ev.target);
           80  +       console.debug("Copied text:",ev.detail.text);
           81  +     });
           82  +  */
           83  +  F.copyButton = function f(e, opt){
           84  +    if('string'===typeof e){
           85  +      e = document.querySelector(e);
           86  +    }    
           87  +    opt = F.mergeLastWins(f.defaultOptions, opt);
           88  +    if(opt.cssClass){
           89  +      D.addClass(e, opt.cssClass);
           90  +    }
           91  +    var srcId, srcElem;
           92  +    if(opt.copyFromElement){
           93  +      srcElem = opt.copyFromElement;
           94  +    }else if((srcId = opt.copyFromId || e.dataset.copyFromId)){
           95  +      srcElem = document.querySelector('#'+srcId);
           96  +    }
           97  +    const extract = opt.extractText || (
           98  +      undefined===srcElem.value ? ()=>srcElem.innerText : ()=>srcElem.value
           99  +    );
          100  +    D.copyStyle(e, opt.style);
          101  +    e.addEventListener(
          102  +      'click',
          103  +      function(ev){
          104  +        ev.preventDefault();
          105  +        ev.stopPropagation();
          106  +        if(e.classList.contains('disabled')) return;
          107  +        const txt = extract.call(opt);
          108  +        if(txt && D.copyTextToClipboard(txt)){
          109  +          e.dispatchEvent(new CustomEvent('text-copied',{
          110  +            detail: {text: txt}
          111  +          }));
          112  +        }
          113  +      },
          114  +      false
          115  +    );
          116  +    if('function' === typeof opt.oncopy){
          117  +      e.addEventListener('text-copied', opt.oncopy, false);
          118  +    }
          119  +    return e;
          120  +  };
          121  +
          122  +  F.copyButton.defaultOptions = {
          123  +    cssClass: 'copy-button',
          124  +    oncopy: D.flashOnce.eventHandler,
          125  +    style: {/*properties copied as-is into element.style*/}
          126  +  };
          127  +  
          128  +})(window.fossil);

Changes to src/fossil.dom.js.

    70     70     dom.pre = dom.createElemFactory('pre');
    71     71     dom.header = dom.createElemFactory('header');
    72     72     dom.footer = dom.createElemFactory('footer');
    73     73     dom.section = dom.createElemFactory('section');
    74     74     dom.span = dom.createElemFactory('span');
    75     75     dom.strong = dom.createElemFactory('strong');
    76     76     dom.em = dom.createElemFactory('em');
    77         -  dom.label = dom.createElemFactory('label');
           77  +  /**
           78  +     Returns a LABEL element. If passed an argument,
           79  +     it must be an id or an HTMLElement with an id,
           80  +     and that id is set as the 'for' attribute of the
           81  +     label. If passed 2 arguments, the 2nd is text or
           82  +     a DOM element to append to the label.
           83  +  */
           84  +  dom.label = function(forElem, text){
           85  +    const rc = document.createElement('label');
           86  +    if(forElem){
           87  +      if(forElem instanceof HTMLElement){
           88  +        forElem = this.attr(forElem, 'id');
           89  +      }
           90  +      dom.attr(rc, 'for', forElem);
           91  +    }
           92  +    if(text) this.append(rc, text);
           93  +    return rc;
           94  +  };
           95  +  /**
           96  +     Returns an IMG element with an optional src
           97  +     attribute value.
           98  +  */
    78     99     dom.img = function(src){
    79         -    const e = dom.create('img');
          100  +    const e = this.create('img');
    80    101       if(src) e.setAttribute('src',src);
    81    102       return e;
    82    103     };
    83    104     /**
    84    105        Creates and returns a new anchor element with the given
    85    106        optional href and label. If label===true then href is used
    86    107        as the label.
    87    108     */
    88    109     dom.a = function(href,label){
    89         -    const e = dom.create('a');
          110  +    const e = this.create('a');
    90    111       if(href) e.setAttribute('href',href);
    91    112       if(label) e.appendChild(dom.text(true===label ? href : label));
    92    113       return e;
    93    114     };
    94    115     dom.hr = dom.createElemFactory('hr');
    95    116     dom.br = dom.createElemFactory('br');
    96    117     dom.text = (t)=>document.createTextNode(t||'');
    97    118     dom.button = function(label){
    98    119       const b = this.create('button');
    99    120       if(label) b.appendChild(this.text(label));
   100    121       return b;
   101    122     };
          123  +  /**
          124  +     Returns a TEXTAREA element.
          125  +
          126  +     Usages:
          127  +
          128  +     ([boolean readonly = false])
          129  +     (non-boolean rows[,cols[,readonly=false]])
          130  +
          131  +     Each of the rows/cols/readonly attributes is only set if it is
          132  +     truthy.
          133  +  */
          134  +  dom.textarea = function(){
          135  +    const rc = this.create('textarea');
          136  +    let rows, cols, readonly;
          137  +    if(1===arguments.length){
          138  +      if('boolean'===typeof arguments[0]){
          139  +        readonly = !!arguments[0];
          140  +      }else{
          141  +        rows = arguments[0];
          142  +      }
          143  +    }else if(arguments.length){
          144  +      rows = arguments[0];
          145  +      cols = arguments[1];
          146  +      readonly = arguments[2];
          147  +    }
          148  +    if(rows) rc.setAttribute('rows',rows);
          149  +    if(cols) rc.setAttribute('cols', cols);
          150  +    if(readonly) rc.setAttribute('readonly', true);
          151  +    return rc;
          152  +  };
          153  +
          154  +  /**
          155  +     Returns a new SELECT element.
          156  +  */
   102    157     dom.select = dom.createElemFactory('select');
          158  +
   103    159     /**
   104         -     Returns an OPTION element with the given value and label
   105         -     text (which defaults to the value).
          160  +     Returns an OPTION element with the given value and label text
          161  +     (which defaults to the value).
   106    162   
   107         -     May be called as (value), (selectElement), (selectElement,
   108         -     value), (value, label) or (selectElement, value,
   109         -     label). The latter appends the new element to the given
   110         -     SELECT element.
          163  +     Usage:
   111    164   
   112         -     If the value has the undefined value then it is NOT
   113         -     assigned as the option element's value.
          165  +     (value[, label])
          166  +     (selectElement [,value [,label = value]])
          167  +
          168  +     Any forms taking a SELECT element append the new element to the
          169  +     given SELECT element.
          170  +
          171  +     If any label is falsy and the value is not then the value is used
          172  +     as the label. A non-falsy label value may have any type suitable
          173  +     for passing as the 2nd argument to dom.append().
          174  +
          175  +     If the value has the undefined value then it is NOT assigned as
          176  +     the option element's value and no label is set unless it has a
          177  +     non-undefined value.
   114    178     */
   115    179     dom.option = function(value,label){
   116    180       const a = arguments;
   117    181       var sel;
   118    182       if(1==a.length){
   119    183         if(a[0] instanceof HTMLElement){
   120    184           sel = a[0];
................................................................................
   135    199         value = a[1];
   136    200         label = a[2];
   137    201       }
   138    202       const o = this.create('option');
   139    203       if(undefined !== value){
   140    204         o.value = value;
   141    205         this.append(o, this.text(label || value));
          206  +    }else if(undefined !== label){
          207  +      this.append(o, label);
   142    208       }
   143    209       if(sel) this.append(sel, o);
   144    210       return o;
   145    211     };
   146    212     dom.h = function(level){
   147    213       return this.create('h'+level);
   148    214     };
................................................................................
   176    242     dom.thead = dom.createElemFactoryWithOptionalParent('thead');
   177    243     dom.tbody = dom.createElemFactoryWithOptionalParent('tbody');
   178    244     dom.tfoot = dom.createElemFactoryWithOptionalParent('tfoot');
   179    245     dom.tr = dom.createElemFactoryWithOptionalParent('tr');
   180    246     dom.td = dom.createElemFactoryWithOptionalParent('td');
   181    247     dom.th = dom.createElemFactoryWithOptionalParent('th');
   182    248   
   183         -  
   184    249     /**
   185         -     Creates and returns a FIELDSET element, optionaly with a
   186         -     LEGEND element added to it.
          250  +     Creates and returns a FIELDSET element, optionaly with a LEGEND
          251  +     element added to it. If legendText is an HTMLElement then it is
          252  +     appended as-is, else it is assume (if truthy) to be a value
          253  +     suitable for passing to dom.append(aLegendElement,...).
   187    254     */
   188    255     dom.fieldset = function(legendText){
   189    256       const fs = this.create('fieldset');
   190    257       if(legendText){
   191    258         this.append(
   192    259           fs,
   193         -        this.append(
   194         -          this.create('legend'),
   195         -          legendText
   196         -        )
          260  +        (legendText instanceof HTMLElement)
          261  +          ? legendText
          262  +          : this.append(this.create('legend'),legendText)
   197    263         );
   198    264       }
   199    265       return fs;
   200    266     };
   201    267   
   202    268     /**
   203    269        Appends each argument after the first to the first argument
................................................................................
   216    282     */
   217    283     dom.append = function f(parent/*,...*/){
   218    284       const a = argsToArray(arguments);
   219    285       a.shift();
   220    286       for(let i in a) {
   221    287         var e = a[i];
   222    288         if(isArray(e) || e.forEach){
   223         -        e.forEach((x)=>f.call(this, parent,e));
          289  +        e.forEach((x)=>f.call(this, parent,x));
   224    290           continue;
   225    291         }
   226         -      if('string'===typeof e || 'number'===typeof e) e = this.text(e);
          292  +      if('string'===typeof e
          293  +         || 'number'===typeof e
          294  +         || 'boolean'===typeof e) e = this.text(e);
   227    295         parent.appendChild(e);
   228    296       }
   229    297       return parent;
   230    298     };
   231    299   
   232    300     dom.input = function(type){
   233    301       return this.attr(this.create('input'), 'type', type);
   234    302     };
   235         -  
          303  +  /**
          304  +     Returns a new CHECKBOX input element.
          305  +
          306  +     Usages:
          307  +
          308  +     ([boolean checked = false])
          309  +     (non-boolean value [,boolean checked])
          310  +  */
          311  +  dom.checkbox = function(value, checked){
          312  +    const rc = this.input('checkbox');
          313  +    if(1===arguments.length && 'boolean'===typeof value){
          314  +      checked = !!value;
          315  +      value = undefined;
          316  +    }
          317  +    if(undefined !== value) rc.value = value;
          318  +    if(!!checked) rc.checked = true;
          319  +    return rc;
          320  +  };
          321  +  /**
          322  +     Returns a new RADIO input element.
          323  +
          324  +     ([boolean checked = false])
          325  +     (string name [,boolean checked])
          326  +     (string name, non-boolean value [,boolean checked])
          327  +  */
          328  +  dom.radio = function(){
          329  +    const rc = this.input('radio');
          330  +    let name, value, checked;
          331  +    if(1===arguments.length && 'boolean'===typeof name){
          332  +      checked = arguments[0];
          333  +      name = value = undefined;
          334  +    }else if(2===arguments.length){
          335  +      name = arguments[0];
          336  +      if('boolean'===typeof arguments[1]){
          337  +        checked = arguments[1];
          338  +      }else{
          339  +        value = arguments[1];
          340  +        checked = undefined;
          341  +      }
          342  +    }else if(arguments.length){
          343  +      name = arguments[0];
          344  +      value = arguments[1];
          345  +      checked = arguments[2];
          346  +    }
          347  +    if(name) this.attr(rc, 'name', name);
          348  +    if(undefined!==value) rc.value = value;
          349  +    if(!!checked) rc.checked = true;
          350  +    return rc;
          351  +  };
          352  +
   236    353     /**
   237    354        Internal impl for addClass(), removeClass().
   238    355     */
   239    356     const domAddRemoveClass = function f(action,e){
   240    357       if(!f.rxSPlus){
   241    358         f.rxSPlus = /\s+/;
   242    359         f.applyAction = function(e,a,v){
................................................................................
   292    409     */
   293    410     dom.removeClass = function(e,c){
   294    411       const a = argsToArray(arguments);
   295    412       a.unshift('remove');
   296    413       return domAddRemoveClass.apply(this, a);
   297    414     };
   298    415   
          416  +  /**
          417  +     Toggles CSS class c on e (a single element for forEach-capable
          418  +     collection of elements. Returns its first argument.
          419  +  */
          420  +  dom.toggleClass = function f(e,c){
          421  +    if(e.forEach){
          422  +      e.forEach((x)=>x.classList.toggle(c));
          423  +    }else{
          424  +      e.classList.toggle(c);
          425  +    }
          426  +    return e;
          427  +  };
          428  +
          429  +  /**
          430  +     Returns true if DOM element e contains CSS class c, else
          431  +     false.
          432  +  */
   299    433     dom.hasClass = function(e,c){
   300    434       return (e && e.classList) ? e.classList.contains(c) : false;
   301    435     };
   302    436   
   303    437     /**
   304         -     Each argument after the first may be a single DOM element
   305         -     or a container of them with a forEach() method. All such
   306         -     elements are appended, in the given order, to the dest
   307         -     element.
          438  +     Each argument after the first may be a single DOM element or a
          439  +     container of them with a forEach() method. All such elements are
          440  +     appended, in the given order, to the dest element using
          441  +     dom.append(dest,theElement). Thus the 2nd and susequent arguments
          442  +     may be any type supported as the 2nd argument to that function.
   308    443   
   309    444        Returns dest.
   310    445     */
   311    446     dom.moveTo = function(dest,e){
   312    447       const n = arguments.length;
   313    448       var i = 1;
          449  +    const self = this;
   314    450       for( ; i < n; ++i ){
   315    451         e = arguments[i];
   316         -      if(e.forEach){
   317         -        e.forEach((x)=>dest.appendChild(x));
   318         -      }else{
   319         -        dest.appendChild(e);
   320         -      }
          452  +      this.append(dest, e);
   321    453       }
   322    454       return dest;
   323    455     };
   324    456     /**
   325    457        Each argument after the first may be a single DOM element
   326    458        or a container of them with a forEach() method. For each
   327    459        DOM element argument, all children of that DOM element
................................................................................
   396    528         old.parentNode.removeChild(old);
   397    529       }
   398    530     };
   399    531     dom.replaceNode.counter = 0;        
   400    532     /**
   401    533        Two args == getter: (e,key), returns value
   402    534   
   403         -     Three == setter: (e,key,val), returns e. If val===null
   404         -     or val===undefined then the attribute is removed. If (e)
   405         -     has a forEach method then this routine is applied to each
   406         -     element of that collection via that method.           
          535  +     Three or more == setter: (e,key,val[...,keyN,valN]), returns
          536  +     e. If val===null or val===undefined then the attribute is
          537  +     removed. If (e) has a forEach method then this routine is applied
          538  +     to each element of that collection via that method. Each pair of
          539  +     keys/values is applied to all elements designated by the first
          540  +     argument.
   407    541     */
   408    542     dom.attr = function f(e){
   409    543       if(2===arguments.length) return e.getAttribute(arguments[1]);
   410         -    if(e.forEach){
   411         -      e.forEach((x)=>f(x,arguments[1],arguments[2]));
          544  +    const a = argsToArray(arguments);
          545  +    if(e.forEach){ /* Apply to all elements in the collection */
          546  +      e.forEach(function(x){
          547  +        a[0] = x;
          548  +        f.apply(f,a);
          549  +      });
   412    550         return e;
   413         -    }            
   414         -    const key = arguments[1], val = arguments[2];
   415         -    if(null===val || undefined===val){
   416         -      e.removeAttribute(key);
   417         -    }else{
   418         -      e.setAttribute(key,val);
          551  +    }
          552  +    a.shift(/*element(s)*/);
          553  +    while(a.length){
          554  +      const key = a.shift(), val = a.shift();
          555  +      if(null===val || undefined===val){
          556  +        e.removeAttribute(key);
          557  +      }else{
          558  +        e.setAttribute(key,val);
          559  +      }
   419    560       }
   420    561       return e;
   421    562     };
   422    563   
   423    564     const enableDisable = function f(enable){
   424    565       var i = 1, n = arguments.length;
   425    566       for( ; i < n; ++i ){
................................................................................
   467    608         e = new Error("Cannot find DOM element: "+x);
   468    609         console.error(e, src);
   469    610         throw e;
   470    611       }
   471    612       return e;
   472    613     };
   473    614   
          615  +  /**
          616  +     "Blinks" the given element a single time for the given number of
          617  +     milliseconds, defaulting (if the 2nd argument is falsy or not a
          618  +     number) to flashOnce.defaultTimeMs. If a 3rd argument is passed
          619  +     in, it must be a function, and it gets callback back at the end
          620  +     of the asynchronous flashing processes.
          621  +
          622  +     This will only activate once per element during that timeframe -
          623  +     further calls will become no-ops until the blink is
          624  +     completed. This routine adds a dataset member to the element for
          625  +     the duration of the blink, to allow it to block multiple blinks.
          626  +
          627  +     If passed 2 arguments and the 2nd is a function, it behaves as if
          628  +     it were called as (arg1, undefined, arg2).
          629  +
          630  +     Returns e, noting that the flash itself is asynchronous and may
          631  +     still be running, or not yet started, when this function returns.
          632  +  */
          633  +  dom.flashOnce = function f(e,howLongMs,afterFlashCallback){
          634  +    if(e.dataset.isBlinking){
          635  +      return;
          636  +    }
          637  +    if(2===arguments.length && 'function' ===typeof howLongMs){
          638  +      afterFlashCallback = howLongMs;
          639  +      howLongMs = f.defaultTimeMs;
          640  +    }
          641  +    if(!howLongMs || 'number'!==typeof howLongMs){
          642  +      howLongMs = f.defaultTimeMs;
          643  +    }
          644  +    e.dataset.isBlinking = true;
          645  +    const transition = e.style.transition;
          646  +    e.style.transition = "opacity "+howLongMs+"ms ease-in-out";
          647  +    const opacity = e.style.opacity;
          648  +    e.style.opacity = 0;
          649  +    setTimeout(function(){
          650  +      e.style.transition = transition;
          651  +      e.style.opacity = opacity;
          652  +      delete e.dataset.isBlinking;
          653  +      if(afterFlashCallback) afterFlashCallback();
          654  +    }, howLongMs);
          655  +    return e;
          656  +  };
          657  +  dom.flashOnce.defaultTimeMs = 400;
          658  +  /**
          659  +     A DOM event handler which simply passes event.target
          660  +     to dom.flashOnce().
          661  +  */
          662  +  dom.flashOnce.eventHandler = (event)=>dom.flashOnce(event.target)
          663  +
          664  +  /**
          665  +     Attempts to copy the given text to the system clipboard. Returns
          666  +     true if it succeeds, else false.
          667  +  */
          668  +  dom.copyTextToClipboard = function(text){
          669  +    if( window.clipboardData && window.clipboardData.setData ){
          670  +      window.clipboardData.setData('Text',text);
          671  +      return true;
          672  +    }else{
          673  +      const x = document.createElement("textarea");
          674  +      x.style.position = 'fixed';
          675  +      x.value = text;
          676  +      document.body.appendChild(x);
          677  +      x.select();
          678  +      var rc;
          679  +      try{
          680  +        document.execCommand('copy');
          681  +        rc = true;
          682  +      }catch(err){
          683  +        rc = false;
          684  +      }finally{
          685  +        document.body.removeChild(x);
          686  +      }
          687  +      return rc;
          688  +    }
          689  +  };
          690  +
          691  +  /**
          692  +     Copies all properties from the 2nd argument (a plain object) into
          693  +     the style member of the first argument (DOM element or a
          694  +     forEach-capable list of elements). If the 2nd argument is falsy
          695  +     or empty, this is a no-op.
          696  +
          697  +     Returns its first argument.
          698  +  */
          699  +  dom.copyStyle = function f(e, style){
          700  +    if(e.forEach){
          701  +      e.forEach((x)=>f(x, style));
          702  +      return e;
          703  +    }
          704  +    if(style){
          705  +      let k;
          706  +      for(k in style){
          707  +        if(style.hasOwnProperty(k)) e.style[k] = style[k];
          708  +      }
          709  +    }
          710  +    return e;
          711  +  };
          712  +
          713  +  /**
          714  +     Parses a string as HTML.
          715  +
          716  +     Usages:
          717  +
          718  +     Array (htmlString)
          719  +     DOMElement (DOMElement target, htmlString)
          720  +
          721  +     The first form parses the string as HTML and returns an Array of
          722  +     all elements parsed from it. If string is falsy then it returns
          723  +     an empty array.
          724  +
          725  +     The second form parses the HTML string and appends all elements
          726  +     to the given target element using dom.append(), then returns the
          727  +     first argument.
          728  +
          729  +     Caveats:
          730  +
          731  +     - It expects a partial HTML document as input, not a full HTML
          732  +     document with a HEAD and BODY tags. Because of how DOMParser
          733  +     works, only children of the parsed document's (virtual) body are
          734  +     acknowledged by this routine.
          735  +  */
          736  +  dom.parseHtml = function(){
          737  +    let childs, string, tgt;
          738  +    if(1===arguments.length){
          739  +      string = arguments[0];
          740  +    }else if(2==arguments.length){
          741  +      tgt = arguments[0];
          742  +      string  = arguments[1];
          743  +    }
          744  +    if(string){
          745  +      const newNode = new DOMParser().parseFromString(string, 'text/html');
          746  +      childs = newNode.documentElement.querySelector('body');
          747  +      childs = childs ? Array.prototype.slice.call(childs.childNodes, 0) : [];
          748  +      /* ^^^ we need a clone of the list because reparenting them
          749  +         modifies a NodeList they're in. */
          750  +    }else{
          751  +      childs = [];
          752  +    }
          753  +    return tgt ? this.moveTo(tgt, childs) : childs;
          754  +  };
          755  +
          756  +  /**
          757  +     Sets up pseudo-automatic content preview handling between a
          758  +     source element (typically a TEXTAREA) and a target rendering
          759  +     element (typically a DIV). The selector argument must be one of:
          760  +
          761  +     - A single DOM element
          762  +     - A collection of DOM elements with a forEach method.
          763  +     - A CSS selector
          764  +
          765  +     Each element in the collection must have the following data
          766  +     attributes:
          767  +
          768  +     - data-f-preview-from: is either a DOM element id, WITH a leading
          769  +     '#' prefix, or the name of a method (see below). If it's an ID,
          770  +     the DOM element must support .value to get the content.
          771  +
          772  +     - data-f-preview-to: the DOM element id of the target "previewer"
          773  +       element, WITH a leading '#', or the name of a method (see below).
          774  +
          775  +     - data-f-preview-via: the name of a method (see below).
          776  +
          777  +     - OPTIONAL data-f-preview-as-text: a numeric value. Explained below.
          778  +
          779  +     Each element gets a click handler added to it which does the
          780  +     following:
          781  +
          782  +     1) Reads the content from its data-f-preview-from element or, if
          783  +     that property refers to a method, calls the method without
          784  +     arguments and uses its result as the content.
          785  +
          786  +     2) Passes the content to
          787  +     methodNamespace[f-data-post-via](content,callback). f-data-post-via
          788  +     is responsible for submitting the preview HTTP request, including
          789  +     any parameters the request might require. When the response
          790  +     arrives, it must pass the content of the response to its 2nd
          791  +     argument, an auto-generated callback installed by this mechanism
          792  +     which...
          793  +
          794  +     3) Assigns the response text to the data-f-preview-to element or
          795  +     passes it to the function
          796  +     methodNamespace[f-data-preview-to](content), as appropriate. If
          797  +     data-f-preview-to is a DOM element and data-f-preview-as-text is
          798  +     '0' (the default) then the target elements contents are replaced
          799  +     with the given content as HTML, else the content is assigned to
          800  +     the target's textContent property. (Note that this routine uses
          801  +     DOMParser, rather than assignment to innerHTML, to apply
          802  +     HTML-format content.)
          803  +
          804  +     The methodNamespace (2nd argument) defaults to fossil.page, and
          805  +     any method-name data properties, e.g. data-f-preview-via and
          806  +     potentially data-f-preview-from/to, must be a single method name,
          807  +     not a property-access-style string. e.g. "myPreview" is legal but
          808  +     "foo.myPreview" is not (unless, of course, the method is actually
          809  +     named "foo.myPreview" (which is legal but would be
          810  +     unconventional)). All such methods are called with
          811  +     methodNamespace as their "this".
          812  +
          813  +     An example...
          814  +
          815  +     First an input button:
          816  +
          817  +     <button id='test-preview-connector'
          818  +       data-f-preview-from='#fileedit-content-editor' // elem ID or method name
          819  +       data-f-preview-via='myPreview' // method name
          820  +       data-f-preview-to='#fileedit-tab-preview-wrapper' // elem ID or method name
          821  +     >Preview update</button>
          822  +
          823  +     And a sample data-f-preview-via method:
          824  +
          825  +     fossil.page.myPreview = function(content,callback){
          826  +       const fd = new FormData();
          827  +       fd.append('foo', ...);
          828  +       fossil.fetch('preview_forumpost',{
          829  +         payload: fd,
          830  +         onload: callback,
          831  +         onerror: (e)=>{ // only if app-specific handling is needed
          832  +           fossil.fetch.onerror(e); // default impl
          833  +           ... any app-specific error reporting ...
          834  +         }
          835  +       });
          836  +     };
          837  +
          838  +     Then connect the parts with:
          839  +
          840  +     fossil.connectPagePreviewers('#test-preview-connector');
          841  +
          842  +     Note that the data-f-preview-from, data-f-preview-via, and
          843  +     data-f-preview-to selector are not resolved until the button is
          844  +     actually clicked, so they need not exist in the DOM at the
          845  +     instant when the connection is set up, so long as they can be
          846  +     resolved when the preview-refreshing element is clicked.
          847  +
          848  +     Maintenance reminder: this method is not strictly part of
          849  +     fossil.dom, but is in its file because it needs access to
          850  +     dom.parseHtml() to avoid an innerHTML assignment and all code
          851  +     which uses this routine also needs fossil.dom.
          852  +  */
          853  +  F.connectPagePreviewers = function f(selector,methodNamespace){
          854  +    if('string'===typeof selector){
          855  +      selector = document.querySelectorAll(selector);
          856  +    }else if(!selector.forEach){
          857  +      selector = [selector];
          858  +    }
          859  +    if(!methodNamespace){
          860  +      methodNamespace = F.page;
          861  +    }
          862  +    selector.forEach(function(e){
          863  +      e.addEventListener(
          864  +        'click', function(r){
          865  +          const eTo = '#'===e.dataset.fPreviewTo[0]
          866  +                ? document.querySelector(e.dataset.fPreviewTo)
          867  +                : methodNamespace[e.dataset.fPreviewTo],
          868  +                eFrom = '#'===e.dataset.fPreviewFrom[0]
          869  +                ? document.querySelector(e.dataset.fPreviewFrom)
          870  +                : methodNamespace[e.dataset.fPreviewFrom],
          871  +                asText = +(e.dataset.fPreviewAsText || 0);
          872  +          eTo.textContent = "Fetching preview...";
          873  +          methodNamespace[e.dataset.fPreviewVia](
          874  +            (eFrom instanceof Function ? eFrom.call(methodNamespace) : eFrom.value),
          875  +            function(r){
          876  +              if(eTo instanceof Function) eTo.call(methodNamespace, r||'');
          877  +              else if(!r){
          878  +                dom.clearElement(eTo);
          879  +              }else if(asText){
          880  +                eTo.textContent = r;
          881  +              }else{
          882  +                dom.parseHtml(dom.clearElement(eTo), r);
          883  +              }
          884  +            }
          885  +          );
          886  +        }, false
          887  +      );
          888  +    });
          889  +    return this;
          890  +  }/*F.connectPagePreviewers()*/;
          891  +
   474    892     return F.dom = dom;
   475    893   })(window.fossil);

Added src/fossil.numbered-lines.js.

            1  +(function callee(arg){
            2  +  /*
            3  +    JS counterpart of info.c:output_text_with_line_numbers()
            4  +    which ties an event handler to the line numbers to allow
            5  +    selection of individual lines or ranges.
            6  +
            7  +    Requires: fossil.bootstrap, fossil.dom, fossil.popupwidget,
            8  +    fossil.copybutton
            9  +  */
           10  +  var tbl = arg || document.querySelectorAll('table.numbered-lines');
           11  +  if(tbl && !arg){
           12  +    if(tbl.length>1){ /* multiple query results: recurse */
           13  +      tbl.forEach( (t)=>callee(t) );
           14  +      return;
           15  +    }else{/* single query result */
           16  +      tbl = tbl[0];
           17  +    }
           18  +  }
           19  +  if(!tbl) return /* no matching elements */;
           20  +  const F = window.fossil, D = F.dom;
           21  +  const tdLn = tbl.querySelector('td.line-numbers');
           22  +  const lineState = {
           23  +    urlArgs: (window.location.search||'?')
           24  +      .replace(/&?\budc=[^&]*/,'') /* "update display prefs cookie" */
           25  +      .replace(/&?\bln=[^&]*/,'') /* inbound line number/range */
           26  +      .replace('?&','?'),
           27  +    start: 0, end: 0
           28  +  };
           29  +
           30  +  const lineTip = new F.PopupWidget({
           31  +    style: {
           32  +      cursor: 'pointer'
           33  +    },
           34  +    refresh: function(){
           35  +      const link = this.state.link;
           36  +      D.clearElement(link);
           37  +      if(lineState.start){
           38  +        const ls = [lineState.start];
           39  +        if(lineState.end) ls.push(lineState.end);
           40  +        link.dataset.url = (
           41  +          window.location.toString().split('?')[0]
           42  +            + lineState.urlArgs + '&ln='+ls.join('-')
           43  +        );
           44  +        D.append(
           45  +          D.clearElement(link),
           46  +          ' Copy link to '+(
           47  +            ls.length===1 ? 'line ' : 'lines '
           48  +          )+ls.join('-')
           49  +        );
           50  +      }else{
           51  +        D.append(link, "No lines selected.");
           52  +      }
           53  +    },
           54  +    init: function(){
           55  +      const e = this.e;
           56  +      const btnCopy = D.span(),
           57  +            link = D.span();
           58  +      this.state = {link};
           59  +      F.copyButton(btnCopy,{
           60  +        copyFromElement: link,
           61  +        extractText: ()=>link.dataset.url,
           62  +        oncopy: (ev)=>{
           63  +          D.flashOnce(ev.target, undefined, ()=>lineTip.hide());
           64  +          // arguably too snazzy: F.toast.message("Copied link to clipboard.");
           65  +        }
           66  +      });
           67  +      this.e.addEventListener('click', ()=>btnCopy.click(), false);
           68  +      D.append(this.e, btnCopy, link)
           69  +    }
           70  +  });
           71  +
           72  +  tbl.addEventListener('click', ()=>lineTip.hide(), true);
           73  +  
           74  +  tdLn.addEventListener('click', function f(ev){
           75  +    if('SPAN'!==ev.target.tagName) return;
           76  +    else if('number' !== typeof f.mode){
           77  +      f.mode = 0 /*0=none selected, 1=1 selected, 2=2 selected*/;
           78  +      f.spans = tdLn.querySelectorAll('span');
           79  +      f.selected = tdLn.querySelectorAll('span.selected-line');
           80  +      f.unselect = (e)=>D.removeClass(e, 'selected-line','start','end');
           81  +    }
           82  +    ev.stopPropagation();
           83  +    const ln = +ev.target.innerText;
           84  +    if(2===f.mode){/*Reset selection*/
           85  +      f.mode = 0;
           86  +    }
           87  +    if(0===f.mode){/*Select single line*/
           88  +      lineState.end = 0;
           89  +      lineState.start = ln;
           90  +      f.mode = 1;
           91  +    }else if(1===f.mode){
           92  +      if(ln === lineState.start){/*Unselect line*/
           93  +        lineState.start = 0;
           94  +        f.mode = 0;
           95  +      }else{/*Select range*/
           96  +        if(ln<lineState.start){
           97  +          lineState.end = lineState.start;
           98  +          lineState.start = ln;
           99  +        }else{
          100  +          lineState.end = ln;
          101  +        }
          102  +        f.mode = 2;
          103  +      }
          104  +    }
          105  +    if(f.selected){/*Unmark previously-selected lines.*/
          106  +      f.selected.forEach(f.unselect);
          107  +      f.selected = undefined;
          108  +    }
          109  +    if(0===f.mode){
          110  +      lineTip.hide();
          111  +    }else{/*Mark selected lines*/
          112  +      const rect = ev.target.getBoundingClientRect();
          113  +      f.selected = [];
          114  +      if(f.spans.length>=lineState.start){
          115  +        let i = lineState.start, end = lineState.end || lineState.start, span = f.spans[i-1];
          116  +        for( ; i<=end && span; span = f.spans[i++] ){
          117  +          span.classList.add('selected-line');
          118  +          f.selected.push(span);
          119  +          if(i===lineState.start) span.classList.add('start');
          120  +          if(i===end) span.classList.add('end');
          121  +        }
          122  +      }
          123  +      lineTip.refresh().show(rect.right+3, rect.top-4);
          124  +    }
          125  +  }, false);
          126  +  
          127  +})();

Changes to src/fossil.page.fileedit.js.

   462    462       e:{/*DOM element(s)*/},
   463    463       init: function(domInsertPoint/*insert widget BEFORE this element*/){
   464    464         const wrapper = D.addClass(
   465    465           D.attr(D.div(),'id','fileedit-stash-selector'),
   466    466           'input-with-label'
   467    467         );
   468    468         const sel = this.e.select = D.select();
   469         -      const btnClear = this.e.btnClear
   470         -            = D.button("Discard Edits");
          469  +      const btnClear = this.e.btnClear = D.button("Discard Edits"),
          470  +            btnHelp = D.append(
          471  +              D.addClass(D.div(), "help-buttonlet"),
          472  +              'Locally-edited files. Timestamps are the last local edit time. ',
          473  +              'Only the ',P.config.defaultMaxStashSize,' most recent files ',
          474  +              'are retained. Saving or reloading a file removes it from this list. ',
          475  +              D.append(D.code(),F.storage.storageImplName()),
          476  +              ' = ',F.storage.storageHelpDescription()
          477  +            );
          478  +
   471    479         D.append(wrapper, "Local edits (",
   472    480                  D.append(D.code(),
   473    481                           F.storage.storageImplName()),
   474    482                  "):",
   475         -               sel, btnClear);
   476         -      D.attr(wrapper, "title", [
   477         -        'Locally-edited files. Timestamps are the last local edit time.',
   478         -        'Only the',P.config.defaultMaxStashSize,'most recent checkin/file',
   479         -        'combinations are retained.',
   480         -        'Committing or reloading a file removes it from this list.'
   481         -      ].join(' '));
   482         -      D.option(D.disable(sel), "(empty)");
          483  +               btnHelp, sel, btnClear);
          484  +      F.helpButtonlets.setup(btnHelp);
          485  +      D.option(D.disable(sel), undefined, "(empty)");
   483    486         F.page.addEventListener('fileedit-stash-updated',(e)=>this.updateList(e.detail));
   484    487         F.page.addEventListener('fileedit-file-loaded',(e)=>this.updateList($stash, e.detail));
   485    488         sel.addEventListener('change',function(e){
   486    489           const opt = this.selectedOptions[0];
   487    490           if(opt && opt._finfo) P.loadFile(opt._finfo);
   488    491         });
   489    492         if(F.storage.isTransient()){/*Warn if our storage is particularly transient...*/
................................................................................
   490    493           D.append(wrapper, D.append(
   491    494             D.addClass(D.span(),'warning'),
   492    495             "Warning: persistent storage is not available, "+
   493    496               "so uncomitted edits will not survive a page reload."
   494    497           ));
   495    498         }
   496    499         domInsertPoint.parentNode.insertBefore(wrapper, domInsertPoint);
          500  +      P.tabs.switchToTab(1/*DOM visibility workaround*/);
   497    501         F.confirmer(btnClear, {
   498    502           /* must come after insertion into the DOM for the pinSize option to work. */
   499    503           pinSize: true,
   500    504           confirmText: "DISCARD all local edits?",
   501    505           onconfirm: function(e){
   502    506             if(P.finfo){
   503    507               const stashed = P.getStashedFinfo(P.finfo);
................................................................................
   507    511               P.clearStash();
   508    512             }
   509    513           },
   510    514           ticks: F.config.confirmerButtonTicks
   511    515         });
   512    516         D.addClass(this.e.btnClear,'hidden' /* must not be set until after confirmer is set up!*/);
   513    517         $stash._fireStashEvent(/*read the page-load-time stash*/);
          518  +      P.tabs.switchToTab(0/*DOM visibility workaround*/);
   514    519         delete this.init;
   515    520       },
   516    521       /**
   517    522          Regenerates the edit selection list.
   518    523        */
   519    524       updateList: function f(stasher,theFinfo){
   520    525         if(!f.compare){
................................................................................
   536    541         Object.keys(index).forEach((finfo)=>{
   537    542           ilist.push(index[finfo]);
   538    543         });
   539    544         const self = this;
   540    545         D.clearElement(this.e.select);
   541    546         if(0===ilist.length){
   542    547           D.addClass(this.e.btnClear, 'hidden');
   543         -        D.option(D.disable(this.e.select),"No local edits");
          548  +        D.option(D.disable(this.e.select),undefined,"No local edits");
   544    549           return;
   545    550         }
   546    551         D.enable(this.e.select);
   547    552         D.removeClass(this.e.btnClear, 'hidden');
   548    553         D.disable(D.option(this.e.select,0,"Select a local edit..."));
   549    554         const currentFinfo = theFinfo || P.finfo || {filename:''};
   550    555         ilist.sort(f.compare).forEach(function(finfo,n){
................................................................................
   636    641         D.enable(ajaxState.toDisable);
   637    642       }
   638    643     };
   639    644   
   640    645     F.onPageLoad(function() {
   641    646       P.base = {tag: E('base')};
   642    647       P.base.originalHref = P.base.tag.href;
   643         -    P.tabs = new fossil.TabManager('#fileedit-tabs');
          648  +    P.tabs = new F.TabManager('#fileedit-tabs');
   644    649       P.e = { /* various DOM elements we work with... */
   645    650         taEditor: E('#fileedit-content-editor'),
   646    651         taCommentSmall: E('#fileedit-comment'),
   647    652         taCommentBig: E('#fileedit-comment-big'),
   648    653         taComment: undefined/*gets set to one of taComment{Big,Small}*/,
   649    654         ajaxContentTarget: E('#ajax-target'),
   650    655         btnCommit: E("#fileedit-btn-commit"),
................................................................................
   652    657         selectPreviewMode: E('#select-preview-mode select'),
   653    658         selectHtmlEmsWrap: E('#select-preview-html-ems'),
   654    659         selectEolWrap:  E('#select-eol-style'),
   655    660         selectEol:  E('#select-eol-style select[name=eol]'),
   656    661         selectFontSizeWrap: E('#select-font-size'),
   657    662         selectDiffWS:  E('select[name=diff_ws]'),
   658    663         cbLineNumbersWrap: E('#cb-line-numbers'),
   659         -      cbAutoPreview: E('#cb-preview-autoupdate > input[type=checkbox]'),
          664  +      cbAutoPreview: E('#cb-preview-autorefresh'),
   660    665         previewTarget: E('#fileedit-tab-preview-wrapper'),
   661    666         manifestTarget: E('#fileedit-manifest'),
   662    667         diffTarget: E('#fileedit-tab-diff-wrapper'),
   663    668         cbIsExe: E('input[type=checkbox][name=exec_bit]'),
   664    669         cbManifest: E('input[type=checkbox][name=include_manifest]'),
   665    670         editStatus: E('#fileedit-edit-status'),
   666    671         tabs:{
................................................................................
   680    685       }else if(D.hasClass(P.e.taCommentBig,'hidden')){
   681    686         P.e.taComment = P.e.taCommentSmall;
   682    687       }else{
   683    688         P.e.taComment = P.e.taCommentSmall;
   684    689         D.addClass(P.e.taCommentBig, 'hidden');
   685    690       }
   686    691       D.removeClass(P.e.taComment, 'hidden');
   687         -    P.tabs.e.container.insertBefore(
   688         -      /* Move the status bar between the tab buttons and
   689         -         tab panels. Seems to be the best fit in terms of
   690         -         functionality and visibility. */
   691         -      E('#fossil-status-bar'), P.tabs.e.tabs
   692         -    );
   693         -    P.tabs.e.container.insertBefore(P.e.editStatus, P.tabs.e.tabs);
   694         -
          692  +    P.tabs.addCustomWidget( E('#fossil-status-bar') ).addCustomWidget(P.e.editStatus);
          693  +    let currentTab/*used for ctrl-enter switch between editor and preview*/;
   695    694       P.tabs.addEventListener(
   696    695         /* Set up auto-refresh of the preview tab... */
   697    696         'before-switch-to', function(ev){
          697  +        currentTab = ev.detail;
   698    698           if(ev.detail===P.e.tabs.preview){
   699    699             P.baseHrefForFile();
   700    700             if(P.previewNeedsUpdate && P.e.cbAutoPreview.checked) P.preview();
   701    701           }else if(ev.detail===P.e.tabs.diff){
   702    702             /* Work around a weird bug where the page gets wider than
   703    703                the window when the diff tab is NOT in view and the
   704    704                current SBS diff widget is wider than the window. When
................................................................................
   718    718             P.baseHrefRestore();
   719    719           }else if(ev.detail===P.e.tabs.diff){
   720    720             /* See notes in the before-switch-to handler. */
   721    721             D.addClass(P.e.diffTarget, 'hidden');
   722    722           }
   723    723         }
   724    724       );
          725  +    ////////////////////////////////////////////////////////////
          726  +    // Trigger preview on Ctrl-Enter. This only works on the built-in
          727  +    // editor widget, not a client-provided one.
          728  +    P.e.taEditor.addEventListener('keydown',function(ev){
          729  +      if(ev.ctrlKey && 13 === ev.keyCode){
          730  +        ev.preventDefault();
          731  +        ev.stopPropagation();
          732  +        P.e.taEditor.blur(/*force change event, if needed*/);
          733  +        P.tabs.switchToTab(P.e.tabs.preview);
          734  +        if(!P.e.cbAutoPreview.checked){/* If NOT in auto-preview mode, trigger an update. */
          735  +          P.preview();
          736  +        }
          737  +      }
          738  +    }, false);
          739  +    // If we're in the preview tab, have ctrl-enter switch back to the editor.
          740  +    document.body.addEventListener('keydown',function(ev){
          741  +      if(ev.ctrlKey && 13 === ev.keyCode){
          742  +        if(currentTab === P.e.tabs.preview){
          743  +          //ev.preventDefault();
          744  +          //ev.stopPropagation();
          745  +          P.tabs.switchToTab(P.e.tabs.content);
          746  +          P.e.taEditor.focus(/*doesn't work for client-supplied editor widget!
          747  +                              And it's slow as molasses for long docs, as focus()
          748  +                              forces a document reflow.*/);
          749  +        }
          750  +      }
          751  +    }, true);
   725    752   
   726    753       F.connectPagePreviewers(
   727    754         P.e.tabs.preview.querySelector(
   728    755           '#btn-preview-refresh'
   729    756         )
   730    757       );
   731    758   
................................................................................
   735    762       );
   736    763       diffButtons.querySelector('button.unified').addEventListener(
   737    764         "click",(e)=>P.diff(false), false
   738    765       );
   739    766       P.e.btnCommit.addEventListener(
   740    767         "click",(e)=>P.commit(), false
   741    768       );
          769  +    P.tabs.switchToTab(1/*DOM visibility workaround*/);
   742    770       F.confirmer(P.e.btnReload, {
   743    771         pinSize: true,
   744    772         confirmText: "Really reload, losing edits?",
   745    773         onconfirm: (e)=>P.unstashContent().loadFile(),
   746    774         ticks: F.config.confirmerButtonTicks
   747    775       });
   748    776       E('#comment-toggle').addEventListener(
   749    777         "click",(e)=>P.toggleCommentMode(), false
   750    778       );
   751    779   
   752         -    P.e.taEditor.addEventListener(
   753         -      'change', ()=>P.stashContentChange(), false
   754         -    );
          780  +    P.e.taEditor.addEventListener('change', ()=>P.notifyOfChange(), false);
   755    781       P.e.cbIsExe.addEventListener(
   756    782         'change', ()=>P.stashContentChange(true), false
   757    783       );
   758    784       
   759    785       /**
   760    786          Cosmetic: jump through some hoops to enable/disable
   761    787          certain preview options depending on the current
................................................................................
   848    874        P.fileContent(). Returns this object.
   849    875     */
   850    876     P.setContentMethods = function(getter, setter){
   851    877       this.fileContent.get = getter;
   852    878       this.fileContent.set = setter;
   853    879       return this;
   854    880     };
          881  +
          882  +  /**
          883  +     Alerts the editor app that a "change" has happened in the editor.
          884  +     When connecting 3rd-party editor widgets to this app, it is (or
          885  +     may be) necessary to call this for any "change" events the widget
          886  +     emits.  Whether or not "change" means that there were "really"
          887  +     edits is irrelevant.
          888  +
          889  +     This function may perform an arbitrary amount of work, so it
          890  +     should not be called for every keypress within the editor
          891  +     widget. Calling it for "blur" events is generally sufficient, and
          892  +     calling it for each Enter keypress is generally reasonable but
          893  +     also computationally costly.
          894  +  */
          895  +  P.notifyOfChange = function(){
          896  +    P.stashContentChange();
          897  +  };
   855    898   
   856    899     /**
   857    900        Removes the default editor widget (and any dependent elements)
   858    901        from the DOM, adds the given element in its place, removes this
   859    902        method from this object, and returns this object.
   860    903     */
   861    904     P.replaceEditorElement = function(newEditor){
................................................................................
  1096   1139        this page's input fields, and updates the UI with with the
  1097   1140        preview.
  1098   1141   
  1099   1142        Returns this object, noting that the operation is async.
  1100   1143     */
  1101   1144     P.preview = function f(switchToTab){
  1102   1145       if(!affirmHasFile()) return this;
  1103         -    const target = this.e.previewTarget,
  1104         -          self = this;
  1105         -    const updateView = function(c){
  1106         -      D.clearElement(target);
  1107         -      if('string'===typeof c) target.innerHTML = c;
         1146  +    return this._postPreview(this.fileContent(), function(c){
         1147  +      P._previewTo(c);
  1108   1148         if(switchToTab) self.tabs.switchToTab(self.e.tabs.preview);
  1109         -    };
  1110         -    return this._postPreview(this.fileContent(), updateView);
         1149  +    });
         1150  +  };
         1151  +
         1152  +    /**
         1153  +     Callback for use with F.connectPagePreviewers(). Gets passed
         1154  +     the preview content.
         1155  +  */
         1156  +  P._previewTo = function(c){
         1157  +    const target = this.e.previewTarget;
         1158  +    D.clearElement(target);
         1159  +    if('string'===typeof c) D.parseHtml(target,c);
         1160  +    if(F.pikchr){
         1161  +      F.pikchr.addSrcView(target.querySelectorAll('svg.pikchr'));
         1162  +    }
  1111   1163     };
  1112         -
         1164  +  
  1113   1165     /**
  1114   1166        Callback for use with F.connectPagePreviewers()
  1115   1167     */
  1116   1168     P._postPreview = function(content,callback){
  1117   1169       if(!affirmHasFile()) return this;
  1118   1170       if(!content){
  1119   1171         callback(content);
................................................................................
  1140   1192           P.dispatchEvent('fileedit-preview-updated',{
  1141   1193             previewMode: P.previewModes.current,
  1142   1194             mimetype: P.finfo.mimetype,
  1143   1195             element: P.e.previewTarget
  1144   1196           });
  1145   1197         },
  1146   1198         onerror: (e)=>{
  1147         -        fossil.fetch.onerror(e);
         1199  +        F.fetch.onerror(e);
  1148   1200           callback("Error fetching preview: "+e);
  1149   1201         }
  1150   1202       });
  1151   1203       return this;
  1152   1204     };
  1153   1205   
  1154   1206     /**
................................................................................
  1188   1240       fd.append('content',content);
  1189   1241       if(this.e.selectDiffWS) fd.append('ws',this.e.selectDiffWS.value);
  1190   1242       F.message(
  1191   1243         "Fetching diff..."
  1192   1244       ).fetch('fileedit/diff',{
  1193   1245         payload: fd,
  1194   1246         onload: function(c){
  1195         -        target.innerHTML = [
         1247  +        D.parseHtml(D.clearElement(target),[
  1196   1248             "<div>Diff <code>[",
  1197   1249             self.finfo.checkin,
  1198   1250             "]</code> &rarr; Local Edits</div>",
  1199   1251             c||'No changes.'
  1200         -        ].join('');
         1252  +        ].join(''));
  1201   1253           if(sbs) P.tweakSbsDiffs2();
  1202   1254           F.message('Updated diff.');
  1203   1255           self.tabs.switchToTab(self.e.tabs.diff);
  1204   1256         }
  1205   1257       });
  1206   1258       return this;
  1207   1259     };
................................................................................
  1220   1272             cbDryRun = E('[name=dry_run]'),
  1221   1273             isDryRun = cbDryRun.checked,
  1222   1274             filename = this.finfo.filename;
  1223   1275       if(!f.onload){
  1224   1276         f.onload = function(c){
  1225   1277           const oldFinfo = JSON.parse(JSON.stringify(self.finfo))
  1226   1278           if(c.manifest){
  1227         -          target.innerHTML = [
         1279  +          D.parseHtml(D.clearElement(target), [
  1228   1280               "<h3>Manifest",
  1229   1281               (c.dryRun?" (dry run)":""),
  1230   1282               ": ", F.hashDigits(c.checkin),"</h3>",
  1231         -            "<code class='fileedit-manifest'>",
  1232         -            c.manifest,
         1283  +            "<pre><code class='fileedit-manifest'>",
         1284  +            c.manifest.replace(/</g,'&lt;'),
         1285  +            /* ^^^ replace() necessary or this breaks if the manifest
         1286  +               comment contains an unclosed HTML tags,
         1287  +               e.g. <script> */
  1233   1288               "</code></pre>"
  1234         -          ].join('');
         1289  +          ].join(''));
  1235   1290             delete c.manifest/*so we don't stash this with finfo*/;
  1236   1291           }
  1237   1292           const msg = [
  1238   1293             'Committed',
  1239   1294             c.dryRun ? '(dry run)' : '',
  1240   1295             '[', F.hashDigits(c.checkin) ,'].'
  1241   1296           ];

Changes to src/fossil.page.forumpost.js.

     1      1   (function(F/*the fossil object*/){
     2      2     "use strict";
     3         -  /* JS code for /forumpage and friends. Requires fossil.dom. */
     4         -  const P = fossil.page, D = fossil.dom;
            3  +  /* JS code for /forumpage and friends. Requires fossil.dom
            4  +     and can optionally use fossil.pikchr. */
            5  +  const P = F.page, D = F.dom;
            6  +
            7  +  /**
            8  +     When the page is loaded, this handler does the following:
            9  +
           10  +     - Installs expand/collapse UI elements on "long" posts and collapses
           11  +     them.
           12  +  
           13  +     - Any pikchr-generated SVGs get a source-toggle button added to them
           14  +     which activates when the mouse is over the image or it is tapped.
     5     15   
           16  +     This is a harmless no-op if the current page has neither forum
           17  +     post constructs for (1) nor any pikchr images for (2), nor will
           18  +     NOT running this code cause any breakage for clients with no JS
           19  +     support: this is all "nice-to-have", not required functionality.
           20  +  */
     6     21     F.onPageLoad(function(){
     7     22       const scrollbarIsVisible = (e)=>e.scrollHeight > e.clientHeight;
     8     23       /* Returns an event handler which implements the post expand/collapse toggle
     9     24          on contentElem when the given widget is activated. */
    10     25       const getWidgetHandler = function(widget, contentElem){
    11     26         return function(ev){
    12     27           if(ev) ev.preventDefault();
................................................................................
    28     43       };
    29     44   
    30     45       /* Adds an Expand/Collapse toggle to all div.forumPostBody
    31     46          elements which are deemed "too large" (those for which
    32     47          scrolling is currently activated because they are taller than
    33     48          their max-height). */
    34     49       document.querySelectorAll(
    35         -      'div.forumHier, div.forumTime, div.forumHierRoot'
           50  +      'div.forumHier, div.forumTime, div.forumHierRoot, div.forumEdit'
    36     51       ).forEach(function f(forumPostWrapper){
    37     52         const content = forumPostWrapper.querySelector('div.forumPostBody');
    38     53         if(!content || !scrollbarIsVisible(content)) return;
    39     54         const parent = content.parentElement,
    40     55               widget =  D.addClass(
    41     56                 D.div(),
    42     57                 'forum-post-collapser','bottom'
................................................................................
    77     92           forumPostWrapper.insertBefore(widget, content.nextSibling);
    78     93         }else{
    79     94           forumPostWrapper.appendChild(widget);
    80     95         }
    81     96         content.appendChild(rightTapZone);
    82     97         rightTapZone.addEventListener('click', widgetEventHandler, false);
    83     98         refillTapZone();
    84         -    });
           99  +    })/*F.onPageLoad()*/;
          100  +
          101  +    if(F.pikchr){
          102  +      F.pikchr.addSrcView();
          103  +    }
    85    104     })/*onload callback*/;
    86    105     
    87    106   })(window.fossil);

Added src/fossil.page.pikchrshow.js.

            1  +(function(F/*the fossil object*/){
            2  +  "use strict";
            3  +  /**
            4  +     Client-side implementation of the /pikchrshow app. Requires that
            5  +     the fossil JS bootstrapping is complete and that these fossil JS
            6  +     APIs have been installed: fossil.fetch, fossil.dom,
            7  +     fossil.copybutton, fossil.popupwidget, fossil.storage
            8  +  */
            9  +  const E = (s)=>document.querySelector(s),
           10  +        D = F.dom,
           11  +        P = F.page;
           12  +
           13  +  P.previewMode = 0 /*0==rendered SVG, 1==pikchr text markdown,
           14  +                      2==pikchr text fossil, 3==raw SVG. */
           15  +  P.response = {/*stashed state for the server's preview response*/
           16  +    isError: false,
           17  +    inputText: undefined /* value of the editor field at render-time */,
           18  +    raw: undefined /* raw response text/HTML from server */,
           19  +    rawSvg: undefined /* plain-text SVG part of responses. Required
           20  +                         because the browser will convert \u00a0 to
           21  +                         &nbsp; if we extract the SVG from the DOM,
           22  +                         resulting in illegal SVG. */
           23  +  };
           24  +
           25  +  /**
           26  +     If string r contains an SVG element, this returns that section
           27  +     of the string, else it returns falsy.
           28  +   */
           29  +  const getResponseSvg = function(r){
           30  +    const i0 = r.indexOf("<svg");
           31  +    if(i0>=0){
           32  +      const i1 = r.indexOf("</svg");
           33  +      return r.substring(i0,i1+6);
           34  +    }
           35  +    return '';
           36  +  };
           37  +
           38  +  F.onPageLoad(function() {
           39  +    document.body.classList.add('pikchrshow');
           40  +    P.e = { /* various DOM elements we work with... */
           41  +      previewTarget: E('#pikchrshow-output'),
           42  +      previewLegend: E('#pikchrshow-output-wrapper > legend'),
           43  +      previewCopyButton: D.attr(
           44  +        D.addClass(D.span(),'copy-button'),
           45  +        'id','preview-copy-button' 
           46  +      ),
           47  +      previewModeLabel: D.label('preview-copy-button'),
           48  +      btnSubmit: E('#pikchr-submit-preview'),
           49  +      btnStash: E('#pikchr-stash'),
           50  +      btnUnstash: E('#pikchr-unstash'),
           51  +      btnClearStash: E('#pikchr-clear-stash'),
           52  +      cbDarkMode: E('#flipcolors-wrapper > input[type=checkbox]'),
           53  +      taContent: E('#content'),
           54  +      taPreviewText: D.textarea(20,0,true),
           55  +      uiControls: E('#pikchrshow-controls'),
           56  +      previewModeToggle: D.button("Preview mode"),
           57  +      markupAlignDefault: D.attr(D.radio('markup-align','',true),
           58  +                                 'id','markup-align-default'),
           59  +      markupAlignCenter: D.attr(D.radio('markup-align','center'),
           60  +                                'id','markup-align-center'),
           61  +      markupAlignIndent: D.attr(D.radio('markup-align','indent'),
           62  +                                'id','markup-align-indent'),
           63  +      markupAlignWrapper: D.addClass(D.span(), 'input-with-label')
           64  +    };
           65  +
           66  +    ////////////////////////////////////////////////////////////
           67  +    // Setup markup alignment selection...
           68  +    const alignEvent = function(ev){
           69  +      /* Update markdown/fossil wiki preview if it's active */
           70  +      if(P.previewMode==1 || P.previewMode==2){
           71  +        P.renderPreview();
           72  +      }
           73  +    };
           74  +    P.e.markupAlignRadios = [
           75  +      P.e.markupAlignDefault,
           76  +      P.e.markupAlignCenter,
           77  +      P.e.markupAlignIndent
           78  +    ];
           79  +    D.append(P.e.markupAlignWrapper,
           80  +             D.addClass(D.append(D.span(),"align:"),
           81  +                        'v-align-middle'));
           82  +    P.e.markupAlignRadios.forEach(
           83  +      function(e){
           84  +        e.addEventListener('change', alignEvent, false);
           85  +        D.append(P.e.markupAlignWrapper,
           86  +                 D.addClass([
           87  +                   e,
           88  +                   D.label(e, e.value || "left")
           89  +                 ], 'v-align-middle'));
           90  +      }
           91  +    );
           92  +
           93  +    ////////////////////////////////////////////////////////////
           94  +    // Setup the preview fieldset's LEGEND element...
           95  +    D.append( P.e.previewLegend,
           96  +              P.e.previewModeToggle,
           97  +              '\u00a0',
           98  +              P.e.previewCopyButton,
           99  +              P.e.previewModeLabel,
          100  +              P.e.markupAlignWrapper );
          101  +
          102  +    ////////////////////////////////////////////////////////////
          103  +    // Trigger preview on Ctrl-Enter.
          104  +    P.e.taContent.addEventListener('keydown',function(ev){
          105  +      if(ev.ctrlKey && 13 === ev.keyCode) P.preview();
          106  +    }, false);
          107  +
          108  +    ////////////////////////////////////////////////////////////
          109  +    // Setup clipboard-copy of markup/SVG...
          110  +    F.copyButton(P.e.previewCopyButton, {copyFromElement: P.e.taPreviewText});
          111  +    P.e.previewModeLabel.addEventListener('click', ()=>P.e.previewCopyButton.click(), false);
          112  +
          113  +    ////////////////////////////////////////////////////////////
          114  +    // Set up dark mode simulator...
          115  +    P.e.cbDarkMode.addEventListener('change', function(ev){
          116  +      if(ev.target.checked) D.addClass(P.e.previewTarget, 'dark-mode');
          117  +      else D.removeClass(P.e.previewTarget, 'dark-mode');
          118  +    }, false);
          119  +    if(P.e.cbDarkMode.checked) D.addClass(P.e.previewTarget, 'dark-mode');
          120  +
          121  +    ////////////////////////////////////////////////////////////
          122  +    // Set up preview update and preview mode toggle...
          123  +    P.e.btnSubmit.addEventListener('click', ()=>P.preview(), false);
          124  +    P.e.previewModeToggle.addEventListener('click', function(){
          125  +      /* Rotate through the 4 available preview modes */
          126  +      P.previewMode = ++P.previewMode % 4;
          127  +      P.renderPreview();
          128  +    }, false);
          129  +
          130  +    ////////////////////////////////////////////////////////////
          131  +    // Set up selection list of predefined scripts...
          132  +    if(true){
          133  +      const selectScript = P.e.selectScript = D.select(),
          134  +            cbAutoPreview = P.e.cbAutoPreview =
          135  +            D.attr(D.checkbox(true),'id', 'cb-auto-preview'),
          136  +            cbWrap = D.addClass(D.div(),'input-with-label')
          137  +      ;
          138  +      D.append(
          139  +        cbWrap,
          140  +        selectScript,
          141  +        cbAutoPreview,
          142  +        D.label(cbAutoPreview,"Auto-preview?"),
          143  +        F.helpButtonlets.create(
          144  +          D.append(D.div(),
          145  +                   'Auto-preview automatically previews selected ',
          146  +                   'built-in pikchr scripts by sending them to ',
          147  +                   'the server for rendering. Not recommended on a ',
          148  +                   'slow connection/server.',
          149  +                   D.br(),D.br(),
          150  +                   'Pikchr scripts may also be dragged/dropped from ',
          151  +                   'the local filesystem into the text area, if the ',
          152  +                   'environment supports it, but the auto-preview ',
          153  +                   'option does not apply to them.'
          154  +                  )
          155  +        )
          156  +      )/*.childNodes.forEach(function(ch){
          157  +        ch.style.margin = "0 0.25em";
          158  +      })*/;
          159  +      D.append(P.e.uiControls, cbWrap);
          160  +      P.predefinedPiks.forEach(function(script,ndx){
          161  +        const opt = D.option(script.code ? script.code.trim() :'', script.name);
          162  +        D.append(selectScript, opt);
          163  +        opt.$_sampleScript = script /* for response caching purposes */;
          164  +        if(!ndx) selectScript.selectedIndex = 0 /*timing/ordering workaround*/;
          165  +        if(!script.code) D.disable(opt);
          166  +      });
          167  +      delete P.predefinedPiks;
          168  +      selectScript.addEventListener('change', function(ev){
          169  +        const val = ev.target.value;
          170  +        if(!val) return;
          171  +        const opt = ev.target.selectedOptions[0];
          172  +        P.e.taContent.value = val;
          173  +        if(cbAutoPreview.checked){
          174  +          P.preview.$_sampleScript = opt.$_sampleScript;
          175  +          P.preview();
          176  +        }
          177  +      }, false);
          178  +    }
          179  +    
          180  +    ////////////////////////////////////////////////////////////
          181  +    // Move dark mode checkbox to the end and add a help buttonlet
          182  +    D.append(
          183  +      P.e.uiControls,
          184  +      D.append(
          185  +        P.e.cbDarkMode.parentNode/*the .input-with-label element*/,
          186  +        F.helpButtonlets.create(
          187  +          D.div(),
          188  +          'Dark mode changes the colors of rendered SVG to ',
          189  +          'make them more visible in dark-themed skins. ',
          190  +          'This only changes (using CSS) how they are rendered, ',
          191  +          'not any actual colors written in the script.',
          192  +          D.br(), D.br(),
          193  +          'In some color combinations, certain browsers might ',
          194  +          'cause the SVG image to blur considerably with this ',
          195  +          'setting enabled!'
          196  +        )
          197  +      )
          198  +    );
          199  +
          200  +    ////////////////////////////////////////////////////////////
          201  +    // File drag/drop pikchr scripts into P.e.taContent.
          202  +    // Adapted from: https://stackoverflow.com/a/58677161
          203  +    const dropHighlight = P.e.taContent;
          204  +    const dropEvents = {
          205  +      drop: function(ev){
          206  +        //ev.stopPropagation();
          207  +        ev.preventDefault();
          208  +        D.removeClass(dropHighlight, 'dragover');
          209  +        const file = ev.dataTransfer.files[0];
          210  +        if(file) {
          211  +          const reader = new FileReader();
          212  +          reader.addEventListener(
          213  +            'load', function(e) {P.e.taContent.value = e.target.result}, false
          214  +          );
          215  +          reader.readAsText(file, "UTF-8");
          216  +        }
          217  +      },
          218  +      dragenter: function(ev){
          219  +        //ev.stopPropagation();
          220  +        ev.preventDefault();
          221  +        ev.dataTransfer.dropEffect = "copy";
          222  +        D.addClass(dropHighlight, 'dragover');
          223  +        //console.debug("dragenter");
          224  +      },
          225  +      dragover: function(ev){
          226  +        //ev.stopPropagation();
          227  +        ev.preventDefault();
          228  +        //console.debug("dragover");
          229  +      },
          230  +      dragend: function(ev){
          231  +        //ev.stopPropagation();
          232  +        ev.preventDefault();
          233  +        //console.debug("dragend");
          234  +      },
          235  +      dragleave: function(ev){
          236  +        //ev.stopPropagation();
          237  +        ev.preventDefault();
          238  +        D.removeClass(dropHighlight, 'dragover');
          239  +        //console.debug("dragleave");
          240  +      }
          241  +    };
          242  +    /*
          243  +      The idea here is to accept drops at multiple points or, ideally,
          244  +      document.body, and apply them to P.e.taContent, but the precise
          245  +      combination of event handling needed to pull this off is eluding
          246  +      me.
          247  +    */
          248  +    [P.e.taContent
          249  +     //P.e.previewTarget,// works only until we drag over the SVG element!
          250  +     //document.body
          251  +     /* ideally we'd link only to document.body, but the events seem to
          252  +        get out of whack, with dropleave being triggered
          253  +        at unexpected points. */
          254  +    ].forEach(function(e){
          255  +        Object.keys(dropEvents).forEach(
          256  +          (k)=>e.addEventListener(k, dropEvents[k], true)
          257  +        );
          258  +    });
          259  +
          260  +    ////////////////////////////////////////////////////////////
          261  +    // Setup stash/unstash
          262  +    const stashKey = 'pikchrshow-stash';
          263  +    P.e.btnStash.addEventListener('click', function(){
          264  +      const val = P.e.taContent.value;
          265  +      if(val){
          266  +        F.storage.set(stashKey, val);
          267  +        D.enable(P.e.btnUnstash);
          268  +        F.toast.message("Stashed pikchr.");
          269  +      }
          270  +    }, false);
          271  +    P.e.btnUnstash.addEventListener('click', function(){
          272  +      const val = F.storage.get(stashKey);
          273  +      P.e.taContent.value = val || '';
          274  +    }, false);
          275  +    P.e.btnClearStash.addEventListener('click', function(){
          276  +      F.storage.remove(stashKey);
          277  +      D.disable(P.e.btnUnstash);
          278  +      F.toast.message("Cleared pikchr stash.");
          279  +    }, false);
          280  +    F.helpButtonlets.create(P.e.btnClearStash.nextElementSibling);
          281  +    // If we have stashed contents, enable Unstash, else disable it:
          282  +    if(F.storage.contains(stashKey)) D.enable(P.e.btnUnstash);
          283  +    else D.disable(P.e.btnUnstash);
          284  +
          285  +    ////////////////////////////////////////////////////////////
          286  +    // If we start with content, get it in sync with the state
          287  +    // generated by P.preview(). Normally the server pre-populates it
          288  +    // with an example.
          289  +    let needsPreview;
          290  +    if(!P.e.taContent.value){
          291  +      P.e.taContent.value = F.storage.get(stashKey,'');
          292  +      needsPreview = true;
          293  +    }
          294  +    if(P.e.taContent.value){
          295  +      /* Fill our "response" state so that renderPreview() can work */
          296  +      P.response.inputText = P.e.taContent.value;
          297  +      P.response.raw = P.e.previewTarget.innerHTML;
          298  +      P.response.rawSvg = getResponseSvg(
          299  +        P.response.raw /*note that this is already in the DOM,
          300  +                         which means that the browser has already mangled
          301  +                         \u00a0 to &nbsp;, so...*/.split('&nbsp;').join('\u00a0'));
          302  +      if(needsPreview) P.preview();
          303  +      else{
          304  +        /*If it's from the server, it's already rendered, but this
          305  +          gets all labels/headers in sync.*/
          306  +        P.renderPreview();
          307  +      }
          308  +    }    
          309  +  }/*F.onPageLoad()*/);
          310  +
          311  +  /**
          312  +     Updates the preview view based on the current preview mode and
          313  +     error state.
          314  +  */
          315  +  P.renderPreview = function f(){
          316  +    if(!f.hasOwnProperty('rxNonce')){
          317  +      f.rxNonce = /<!--.+-->\r?\n?/g /*pikchr nonce comments*/;
          318  +      f.showMarkupAlignment = function(showIt){
          319  +        P.e.markupAlignWrapper.classList[showIt ? 'remove' : 'add']('hidden');
          320  +      };
          321  +      f.getMarkupAlignmentClass = function(){
          322  +        if(P.e.markupAlignCenter.checked) return ' center';
          323  +        else if(P.e.markupAlignIndent.checked) return ' indent';
          324  +        return '';
          325  +      };
          326  +      f.getSvgNode = function(txt){
          327  +        const childs = D.parseHtml(txt);
          328  +        const wrapper = childs.filter((e)=>'DIV'===e.tagName)[0];
          329  +        return wrapper ? wrapper.querySelector('svg.pikchr') : undefined;
          330  +      };
          331  +    }
          332  +    const preTgt = this.e.previewTarget;
          333  +    if(this.response.isError){
          334  +      D.append(D.clearElement(preTgt), D.parseHtml(P.response.raw));
          335  +      D.addClass(preTgt, 'error');
          336  +      this.e.previewModeLabel.innerText = "Error";
          337  +      return;
          338  +    }
          339  +    D.removeClass(preTgt, 'error');
          340  +    D.removeClass(this.e.previewCopyButton, 'disabled');
          341  +    D.removeClass(this.e.markupAlignWrapper, 'hidden');
          342  +    D.enable(this.e.previewModeToggle, this.e.markupAlignRadios);
          343  +    let label, svg;
          344  +    switch(this.previewMode){
          345  +    case 0:
          346  +      label = "SVG";
          347  +      f.showMarkupAlignment(false);
          348  +      D.parseHtml(D.clearElement(preTgt), P.response.raw);
          349  +      svg = preTgt.querySelector('svg.pikchr');
          350  +      if(svg && P.response.rawSvg){ /*for copy button*/
          351  +        this.e.taPreviewText.value = P.response.rawSvg;
          352  +        F.pikchr.addSrcView(svg);
          353  +      }
          354  +      break;
          355  +    case 1:
          356  +      label = "Markdown";
          357  +      f.showMarkupAlignment(true);
          358  +      this.e.taPreviewText.value = [
          359  +        '```pikchr'+f.getMarkupAlignmentClass(),
          360  +        this.response.inputText.trim(), '```'
          361  +      ].join('\n');
          362  +      D.append(D.clearElement(preTgt), this.e.taPreviewText);
          363  +      break;
          364  +    case 2:
          365  +      label = "Fossil wiki";
          366  +      f.showMarkupAlignment(true);
          367  +      this.e.taPreviewText.value = [
          368  +        '<verbatim type="pikchr',
          369  +        f.getMarkupAlignmentClass(),
          370  +        '">', this.response.inputText.trim(), '</verbatim>'
          371  +      ].join('');
          372  +      D.append(D.clearElement(preTgt), this.e.taPreviewText);
          373  +      break;
          374  +    case 3:
          375  +      label = "Raw SVG";
          376  +      f.showMarkupAlignment(false);
          377  +      svg = f.getSvgNode(this.response.raw);
          378  +      if(svg){
          379  +        this.e.taPreviewText.value =
          380  +          P.response.rawSvg || "Error extracting SVG element.";
          381  +      }else{
          382  +        this.e.taPreviewText.value = "ERROR parsing response HTML:\n"+
          383  +          this.response.raw;
          384  +        console.error("svg parsed HTML nodes:",childs);
          385  +      }
          386  +      D.append(D.clearElement(preTgt), this.e.taPreviewText);
          387  +      break;
          388  +    }
          389  +    this.e.previewModeLabel.innerText = label;
          390  +  };
          391  +
          392  +  /**
          393  +     Fetches the preview from the server and updates the preview to
          394  +     the rendered SVG content or error report.
          395  +  */
          396  +  P.preview = function fp(){
          397  +    if(!fp.hasOwnProperty('toDisable')){
          398  +      fp.toDisable = [
          399  +        /* input elements to disable during ajax operations */
          400  +        this.e.btnSubmit, this.e.taContent,
          401  +        this.e.cbAutoPreview, this.e.selectScript,
          402  +        this.e.btnStash, this.e.btnClearStash
          403  +        /* handled separately: previewModeToggle, previewCopyButton,
          404  +           markupAlignRadios */
          405  +      ];
          406  +      fp.target = this.e.previewTarget;
          407  +      fp.updateView = function(c,isError){
          408  +        P.previewMode = 0;
          409  +        P.response.raw = c;
          410  +        P.response.rawSvg = getResponseSvg(c);
          411  +        P.response.isError = isError;
          412  +        D.enable(fp.toDisable);
          413  +        P.renderPreview();
          414  +      };
          415  +    }
          416  +    D.disable(fp.toDisable, this.e.previewModeToggle, this.e.markupAlignRadios);
          417  +    D.addClass(this.e.markupAlignWrapper, 'hidden');
          418  +    D.addClass(this.e.previewCopyButton, 'disabled');
          419  +    const content = this.e.taContent.value.trim();
          420  +    this.response.raw = this.response.rawSvg = undefined;
          421  +    this.response.inputText = content;
          422  +    const sampleScript = fp.$_sampleScript;
          423  +    delete fp.$_sampleScript;
          424  +    if(sampleScript && sampleScript.cached){
          425  +      fp.updateView(sampleScript.cached, false);
          426  +      return this;
          427  +    }
          428  +    if(!content){
          429  +      fp.updateView("No pikchr content!",true);
          430  +      return this;
          431  +    }
          432  +    const self = this;
          433  +    const fd = new FormData();
          434  +    fd.append('ajax', true);
          435  +    fd.append('content',content);
          436  +    F.fetch('pikchrshow',{
          437  +      payload: fd,
          438  +      responseHeaders: 'x-pikchrshow-is-error',
          439  +      onload: (r,isErrHeader)=>{
          440  +        const isErr = +isErrHeader ? true : false;
          441  +        if(!isErr && sampleScript){
          442  +          sampleScript.cached = r;
          443  +        }
          444  +        fp.updateView(r,isErr);
          445  +      },
          446  +      onerror: (e)=>{
          447  +        F.fetch.onerror(e);
          448  +        fp.updateView("Error fetching preview: "+e, true);
          449  +      }
          450  +    });
          451  +    return this;
          452  +  }/*preview()*/;
          453  +
          454  +  /**
          455  +     Predefined scripts. Each entry is an object:
          456  +
          457  +     {
          458  +     name: required string,
          459  +
          460  +     code: optional code string. An entry with no code
          461  +           is treated like a separator in the resulting
          462  +           SELECT element (a disabled OPTION).
          463  +
          464  +     }
          465  +  */
          466  +  P.predefinedPiks = [
          467  +    {name: "-- Example Scripts --"},
          468  +/*
          469  +  The following were imported from the pikchr test scripts:
          470  +
          471  +  https://fossil-scm.org/pikchr/dir/examples
          472  +*/
          473  +{name:"Cardinal headings",code:`   linerad = 5px
          474  +C: circle "Center" rad 150%
          475  +   circle "N"  at 1.0 n  of C; arrow from C to last chop ->
          476  +   circle "NE" at 1.0 ne of C; arrow from C to last chop <-
          477  +   circle "E"  at 1.0 e  of C; arrow from C to last chop <->
          478  +   circle "SE" at 1.0 se of C; arrow from C to last chop ->
          479  +   circle "S"  at 1.0 s  of C; arrow from C to last chop <-
          480  +   circle "SW" at 1.0 sw of C; arrow from C to last chop <->
          481  +   circle "W"  at 1.0 w  of C; arrow from C to last chop ->
          482  +   circle "NW" at 1.0 nw of C; arrow from C to last chop <-
          483  +   arrow from 2nd circle to 3rd circle chop
          484  +   arrow from 4th circle to 3rd circle chop
          485  +   arrow from SW to S chop <->
          486  +   circle "ESE" at 2.0 heading 112.5 from Center \
          487  +      thickness 150% fill lightblue radius 75%
          488  +   arrow from Center to ESE thickness 150% <-> chop
          489  +   arrow from ESE up 1.35 then to NE chop
          490  +   line dashed <- from E.e to (ESE.x,E.y)
          491  +   line dotted <-> thickness 50% from N to NW chop
          492  +`},{name:"Core object types",code:`AllObjects: [
          493  +
          494  +# First row of objects
          495  +box "box"
          496  +box rad 10px "box (with" "rounded" "corners)" at 1in right of previous
          497  +circle "circle" at 1in right of previous
          498  +ellipse "ellipse" at 1in right of previous
          499  +
          500  +# second row of objects
          501  +OVAL1: oval "oval" at 1in below first box
          502  +oval "(tall &amp;" "thin)" "oval" width OVAL1.height height OVAL1.width \
          503  +    at 1in right of previous
          504  +cylinder "cylinder" at 1in right of previous
          505  +file "file" at 1in right of previous
          506  +
          507  +# third row shows line-type objects
          508  +dot "dot" above at 1in below first oval
          509  +line right from 1.8cm right of previous "lines" above
          510  +arrow right from 1.8cm right of previous "arrows" above
          511  +spline from 1.8cm right of previous \
          512  +   go right .15 then .3 heading 30 then .5 heading 160 then .4 heading 20 \
          513  +   then right .15
          514  +"splines" at 3rd vertex of previous
          515  +
          516  +# The third vertex of the spline is not actually on the drawn
          517  +# curve.  The third vertex is a control point.  To see its actual
          518  +# position, uncomment the following line:
          519  +#dot color red at 3rd vertex of previous spline
          520  +
          521  +# Draw various lines below the first line
          522  +line dashed right from 0.3cm below start of previous line
          523  +line dotted right from 0.3cm below start of previous
          524  +line thin   right from 0.3cm below start of previous
          525  +line thick  right from 0.3cm below start of previous
          526  +
          527  +
          528  +# Draw arrows with different arrowhead configurations below
          529  +# the first arrow
          530  +arrow <-  right from 0.4cm below start of previous arrow
          531  +arrow <-> right from 0.4cm below start of previous
          532  +
          533  +# Draw splines with different arrowhead configurations below
          534  +# the first spline
          535  +spline same from .4cm below start of first spline ->
          536  +spline same from .4cm below start of previous <-
          537  +spline same from .4cm below start of previous <->
          538  +
          539  +] # end of AllObjects
          540  +
          541  +# Label the whole diagram
          542  +text "Examples Of Pikchr Objects" big bold  at .8cm above north of AllObjects
          543  +`},{name:"Swimlanes",code:`    $laneh = 0.75
          544  +
          545  +    # Draw the lanes
          546  +    down
          547  +    box width 3.5in height $laneh fill 0xacc9e3
          548  +    box same fill 0xc5d8ef
          549  +    box same as first box
          550  +    box same as 2nd box
          551  +    line from 1st box.sw+(0.2,0) up until even with 1st box.n \
          552  +      "Alan" above aligned
          553  +    line from 2nd box.sw+(0.2,0) up until even with 2nd box.n \
          554  +      "Betty" above aligned
          555  +    line from 3rd box.sw+(0.2,0) up until even with 3rd box.n \
          556  +      "Charlie" above aligned
          557  +    line from 4th box.sw+(0.2,0) up until even with 4th box.n \
          558  +       "Darlene" above aligned
          559  +
          560  +    # fill in content for the Alice lane
          561  +    right
          562  +A1: circle rad 0.1in at end of first line + (0.2,-0.2) \
          563  +       fill white thickness 1.5px "1" 
          564  +    arrow right 50%
          565  +    circle same "2"
          566  +    arrow right until even with first box.e - (0.65,0.0)
          567  +    ellipse "future" fit fill white height 0.2 width 0.5 thickness 1.5px
          568  +A3: circle same at A1+(0.8,-0.3) "3" fill 0xc0c0c0
          569  +    arrow from A1 to last circle chop "fork!" below aligned
          570  +
          571  +    # content for the Betty lane
          572  +B1: circle same as A1 at A1-(0,$laneh) "1"
          573  +    arrow right 50%
          574  +    circle same "2"
          575  +    arrow right until even with first ellipse.w
          576  +    ellipse same "future"
          577  +B3: circle same at A3-(0,$laneh) "3"
          578  +    arrow right 50%
          579  +    circle same as A3 "4"
          580  +    arrow from B1 to 2nd last circle chop
          581  +
          582  +    # content for the Charlie lane
          583  +C1: circle same as A1 at B1-(0,$laneh) "1"
          584  +    arrow 50%
          585  +    circle same "2"
          586  +    arrow right 0.8in "goes" "offline"
          587  +C5: circle same as A3 "5"
          588  +    arrow right until even with first ellipse.w \
          589  +      "back online" above "pushes 5" below "pulls 3 &amp; 4" below
          590  +    ellipse same "future"
          591  +
          592  +    # content for the Darlene lane
          593  +D1: circle same as A1 at C1-(0,$laneh) "1"
          594  +    arrow 50%
          595  +    circle same "2"
          596  +    arrow right until even with C5.w
          597  +    circle same "5"
          598  +    arrow 50%
          599  +    circle same as A3 "6"
          600  +    arrow right until even with first ellipse.w
          601  +    ellipse same "future"
          602  +D3: circle same as B3 at B3-(0,2*$laneh) "3"
          603  +    arrow 50%
          604  +    circle same "4"
          605  +    arrow from D1 to D3 chop
          606  +`}
          607  +
          608  +  ];
          609  +  
          610  +})(window.fossil);

Changes to src/fossil.page.wikiedit.js.

     1      1   (function(F/*the fossil object*/){
     2      2     "use strict";
     3      3     /**
     4      4        Client-side implementation of the /wikiedit app. Requires that
     5      5        the fossil JS bootstrapping is complete and that several fossil
     6      6        JS APIs have been installed: fossil.fetch, fossil.dom,
     7         -     fossil.tabs, fossil.storage, fossil.confirmer.
            7  +     fossil.tabs, fossil.storage, fossil.confirmer, fossil.popupwidget.
     8      8   
     9      9        Custom events which can be listened for via
    10     10        fossil.page.addEventListener():
    11     11   
    12     12        - Event 'wiki-page-loaded': passes on information when it
    13     13        loads a wiki (whether from the network or its internal local-edit
    14     14        cache), in the form of an "winfo" object:
................................................................................
   551    551         D.clearElement(parentElem);
   552    552         D.append(
   553    553           parentElem,
   554    554           D.append(D.fieldset("Select a page to edit"),
   555    555                    sel)
   556    556         );
   557    557         D.attr(sel, 'size', 12);
   558         -      D.option(D.disable(D.clearElement(sel)), "Loading...");
          558  +      D.option(D.disable(D.clearElement(sel)), undefined, "Loading...");
   559    559   
   560    560         /** Set up filter checkboxes for the various types
   561    561             of wiki pages... */
   562         -      const fsFilter = D.fieldset("Page types"),
          562  +      const fsFilter = D.addClass(D.fieldset("Page types"),"page-types-list"),
   563    563               fsFilterBody = D.div(),
   564    564               filters = ['normal', 'branch/...', 'tag/...', 'checkin/...']
   565    565         ;
   566    566         D.append(fsFilter, fsFilterBody);
   567    567         D.addClass(fsFilterBody, 'flex-container', 'flex-column', 'stretch');
   568    568   
   569    569         // Add filters by page type...
................................................................................
   595    595                 lbl = D.attr(D.append(D.label(),
   596    596                                       getEditMarker(getEditMarker.DELETED,false),
   597    597                                       'deleted'),
   598    598                              'for', cbId),
   599    599                 cb = D.attr(D.input('checkbox'), 'id', cbId);
   600    600           cb.checked = false;
   601    601           D.addClass(parentElem,'hide-deleted');
   602         -        D.attr(lbl, 'title',
   603         -               'Fossil considers empty pages to be "deleted" in some contexts.');
   604         -        D.append(fsFilterBody, D.append(D.span(), cb, lbl));
          602  +        D.attr(lbl);
          603  +        const deletedTip = F.helpButtonlets.create(
          604  +          D.span(),
          605  +          'Fossil considers empty pages to be "deleted" in some contexts.'
          606  +        );
          607  +        D.append(fsFilterBody, D.append(
          608  +          D.span(), cb, lbl, deletedTip
          609  +        ));
   605    610           cb.addEventListener(
   606    611             'change',
   607    612             function(ev){
   608    613               if(ev.target.checked) D.removeClass(parentElem,'hide-deleted');
   609    614               else D.addClass(parentElem,'hide-deleted');
   610    615             },
   611    616             false);
................................................................................
   662    667           /* Needed to handle the saved-an-empty-page case. */
   663    668           const page = ev.detail,
   664    669                 opt = self.cache.optByName[page.name];
   665    670           if(opt){
   666    671             if(page.isEmpty) opt.dataset.isDeleted = true;
   667    672             else delete opt.dataset.isDeleted;
   668    673             self._refreshStashMarks(opt);
   669         -        }else{
          674  +        }else if('sandbox'!==page.type){
   670    675             F.error("BUG: internal mis-handling of page object: missing OPTION for page "+page.name);
   671    676           }
   672    677         });
   673    678         delete this.init;
   674    679       }
   675    680     };
   676    681   
................................................................................
   680    685     P.stashWidget = {
   681    686       e:{/*DOM element(s)*/},
   682    687       init: function(domInsertPoint/*insert widget BEFORE this element*/){
   683    688         const wrapper = D.addClass(
   684    689           D.attr(D.div(),'id','wikiedit-stash-selector'),
   685    690           'input-with-label'
   686    691         );
   687         -      const sel = this.e.select = D.select();
   688         -      const btnClear = this.e.btnClear = D.button("Discard Edits");
          692  +      const sel = this.e.select = D.select(),
          693  +            btnClear = this.e.btnClear = D.button("Discard Edits"),
          694  +            btnHelp = D.append(
          695  +              D.addClass(D.div(), "help-buttonlet"),
          696  +              'Locally-edited wiki pages. Timestamps are the last local edit time. ',
          697  +              'Only the ',P.config.defaultMaxStashSize,' most recent pages ',
          698  +              'are retained. Saving or reloading a file removes it from this list. ',
          699  +              D.append(D.code(),F.storage.storageImplName()),
          700  +              ' = ',F.storage.storageHelpDescription()
          701  +            );
   689    702         D.append(wrapper, "Local edits (",
   690    703                  D.append(D.code(),
   691    704                           F.storage.storageImplName()),
   692    705                  "):",
   693         -               sel, btnClear);
   694         -      D.attr(wrapper, "title", [
   695         -        'Locally-edited wiki pages. Timestamps are the last local edit time.',
   696         -        'Only the',P.config.defaultMaxStashSize,'most recent pages',
   697         -        'are retained. Saving or reloading a file removes it from this list.'
   698         -      ].join(' '));
   699         -      D.option(D.disable(sel), "(empty)");
          706  +               btnHelp, sel, btnClear);
          707  +      F.helpButtonlets.setup(btnHelp);
          708  +      D.option(D.disable(sel), undefined, "(empty)");
   700    709         P.addEventListener('wiki-stash-updated',(e)=>this.updateList(e.detail));
   701    710         P.addEventListener('wiki-page-loaded',(e)=>this.updateList($stash, e.detail));
   702    711         sel.addEventListener('change',function(e){
   703    712           const opt = this.selectedOptions[0];
   704    713           if(opt && opt._winfo) P.loadPage(opt._winfo);
   705    714         });
   706    715         if(F.storage.isTransient()){/*Warn if our storage is particularly transient...*/
................................................................................
   747    756         Object.keys(index).forEach((winfo)=>{
   748    757           ilist.push(index[winfo]);
   749    758         });
   750    759         const self = this;
   751    760         D.clearElement(this.e.select);
   752    761         if(0===ilist.length){
   753    762           D.addClass(this.e.btnClear, 'hidden');
   754         -        D.option(D.disable(this.e.select),"No local edits");
          763  +        D.option(D.disable(this.e.select),undefined,"No local edits");
   755    764           return;
   756    765         }
   757    766         D.enable(this.e.select);
   758    767         if(true){
   759    768           /* The problem with this Clear button is that it allows the
   760    769              user to nuke a non-empty newly-added page without the
   761    770              failsafe confirmation we have if they use
   762    771              P.e.btnReload. Not yet sure how best to resolve that. */
   763    772           D.removeClass(this.e.btnClear, 'hidden');
   764    773         }
   765         -      D.disable(D.option(this.e.select,0,"Select a local edit..."));
          774  +      D.disable(D.option(this.e.select,undefined,"Select a local edit..."));
   766    775         const currentWinfo = theWinfo || P.winfo || {name:''};
   767    776         ilist.sort(f.compare).forEach(function(winfo,n){
   768    777           const key = stasher.indexKey(winfo),
   769    778                 rev = winfo.version || '';
   770    779           const opt = D.option(
   771    780             self.e.select, n+1/*value is (almost) irrelevant*/,
   772    781             [winfo.name,
................................................................................
   833    842       document.body.classList.add('wikiedit');
   834    843       P.base = {tag: E('base'), wikiUrl: F.repoUrl('wiki')};
   835    844       P.base.originalHref = P.base.tag.href;
   836    845       P.e = { /* various DOM elements we work with... */
   837    846         taEditor: E('#wikiedit-content-editor'),
   838    847         btnReload: E("#wikiedit-tab-content button.wikiedit-content-reload"),
   839    848         btnSave: E("button.wikiedit-save"),
   840         -      btnSaveClose: D.attr(E("button.wikiedit-save-close"),
   841         -                           'title',
   842         -                           'Save changes and return to the wiki reader.'),
          849  +      btnSaveClose: E("button.wikiedit-save-close"),
   843    850         selectMimetype: E('select[name=mimetype]'),
   844    851         selectFontSizeWrap: E('#select-font-size'),
   845    852   //      selectDiffWS:  E('select[name=diff_ws]'),
   846         -      cbAutoPreview: E('#cb-preview-autoupdate > input[type=checkbox]'),
          853  +      cbAutoPreview: E('#cb-preview-autorefresh'),
   847    854         previewTarget: E('#wikiedit-tab-preview-wrapper'),
   848    855         diffTarget: E('#wikiedit-tab-diff-wrapper'),
   849    856         editStatus: E('#wikiedit-edit-status'),
   850    857         tabContainer: E('#wikiedit-tabs'),
   851    858         tabs:{
   852    859           pageList: E('#wikiedit-tab-pages'),
   853    860           content: E('#wikiedit-tab-content'),
   854    861           preview: E('#wikiedit-tab-preview'),
   855    862           diff: E('#wikiedit-tab-diff'),
   856    863           misc: E('#wikiedit-tab-misc')
   857    864           //commit: E('#wikiedit-tab-commit')
   858    865         }
   859    866       };
   860         -    P.tabs = new fossil.TabManager(D.clearElement(P.e.tabContainer));
   861         -    P.tabs.e.container.insertBefore(
   862         -      /* Move the status bar between the tab buttons and
   863         -         tab panels. Seems to be the best fit in terms of
   864         -         functionality and visibility. */
   865         -      E('#fossil-status-bar'), P.tabs.e.tabs
   866         -    );
   867         -    P.tabs.e.container.insertBefore(P.e.editStatus, P.tabs.e.tabs);
          867  +    P.tabs = new F.TabManager(D.clearElement(P.e.tabContainer));
          868  +    /* Move the status bar between the tab buttons and
          869  +       tab panels. Seems to be the best fit in terms of
          870  +       functionality and visibility. */
          871  +    P.tabs.addCustomWidget( E('#fossil-status-bar') ).addCustomWidget(P.e.editStatus);
          872  +    let currentTab/*used for ctrl-enter switch between editor and preview*/;
   868    873       P.tabs.addEventListener(
   869    874         /* Set up some before-switch-to tab event tasks... */
   870    875         'before-switch-to', function(ev){
   871         -        const theTab = ev.detail, btnSlot = theTab.querySelector('.save-button-slot');
          876  +        const theTab = currentTab = ev.detail, btnSlot = theTab.querySelector('.save-button-slot');
   872    877           if(btnSlot){
   873    878             /* Several places make sense for a save button, so we'll
   874    879                move that button around to those tabs where it makes sense. */
   875         -          btnSlot.parentNode.insertBefore( P.e.btnSave, btnSlot );
   876         -          btnSlot.parentNode.insertBefore( P.e.btnSaveClose, btnSlot );
          880  +          btnSlot.parentNode.insertBefore( P.e.btnSave.parentNode, btnSlot );
          881  +          btnSlot.parentNode.insertBefore( P.e.btnSaveClose.parentNode, btnSlot );
   877    882             P.updateSaveButton();
   878    883           }
   879    884           if(theTab===P.e.tabs.preview){
   880    885             P.baseHrefForWiki();
   881    886             if(P.previewNeedsUpdate && P.e.cbAutoPreview.checked) P.preview();
   882    887           }else if(theTab===P.e.tabs.diff){
   883    888             /* Work around a weird bug where the page gets wider than
................................................................................
   900    905             P.baseHrefRestore();
   901    906           }else if(theTab===P.e.tabs.diff){
   902    907             /* See notes in the before-switch-to handler. */
   903    908             D.addClass(P.e.diffTarget, 'hidden');
   904    909           }
   905    910         }
   906    911       );
          912  +    ////////////////////////////////////////////////////////////
          913  +    // Trigger preview on Ctrl-Enter. This only works on the built-in
          914  +    // editor widget, not a client-provided one.
          915  +    P.e.taEditor.addEventListener('keydown',function(ev){
          916  +      if(ev.ctrlKey && 13 === ev.keyCode){
          917  +        ev.preventDefault();
          918  +        ev.stopPropagation();
          919  +        P.e.taEditor.blur(/*force change event, if needed*/);
          920  +        P.tabs.switchToTab(P.e.tabs.preview);
          921  +        if(!P.e.cbAutoPreview.checked){/* If NOT in auto-preview mode, trigger an update. */
          922  +          P.preview();
          923  +        }
          924  +      }
          925  +    }, false);
          926  +    // If we're in the preview tab, have ctrl-enter switch back to the editor.
          927  +    document.body.addEventListener('keydown',function(ev){
          928  +      if(ev.ctrlKey && 13 === ev.keyCode){
          929  +        if(currentTab === P.e.tabs.preview){
          930  +          //ev.preventDefault();
          931  +          //ev.stopPropagation();
          932  +          P.tabs.switchToTab(P.e.tabs.content);
          933  +          P.e.taEditor.focus(/*doesn't work for client-supplied editor widget!
          934  +                              And it's slow as molasses for long docs, as focus()
          935  +                              forces a document reflow. */);
          936  +          //console.debug("BODY ctrl-enter");
          937  +        }
          938  +      }
          939  +    }, true);
   907    940   
   908    941       F.connectPagePreviewers(
   909    942         P.e.tabs.preview.querySelector(
   910    943           '#btn-preview-refresh'
   911    944         )
   912    945       );
   913    946   
................................................................................
   956    989           delete P.winfo;
   957    990           P.updatePageTitle();
   958    991           F.message("Discarded new page ["+w.name+"].");
   959    992         }
   960    993       };
   961    994   
   962    995       if(P.config.useConfirmerButtons.reload){
          996  +      P.tabs.switchToTab(1/*DOM visibility workaround*/);
   963    997         F.confirmer(P.e.btnReload, {
   964    998           pinSize: true,
   965    999           confirmText: "Really reload, losing edits?",
   966   1000           onconfirm: doReload,
   967   1001           ticks: F.config.confirmerButtonTicks
   968   1002         });
   969   1003       }else{
   970   1004         P.e.btnReload.addEventListener('click', doReload, false);
   971   1005       }
   972   1006       if(P.config.useConfirmerButtons.save){
         1007  +      P.tabs.switchToTab(1/*DOM visibility workaround*/);
   973   1008         F.confirmer(P.e.btnSave, {
   974   1009           pinSize: true,
   975   1010           confirmText: "Really save changes?",
   976   1011           onconfirm: ()=>doSave(),
   977   1012           ticks: F.config.confirmerButtonTicks
   978   1013         });
   979   1014         F.confirmer(P.e.btnSaveClose, {
................................................................................
   983   1018           ticks: F.config.confirmerButtonTicks
   984   1019         });
   985   1020       }else{
   986   1021         P.e.btnSave.addEventListener('click', ()=>doSave(), false);
   987   1022         P.e.btnSaveClose.addEventListener('click', ()=>doSave(true), false);
   988   1023       }
   989   1024   
   990         -    P.e.taEditor.addEventListener(
   991         -      'change', function(){
   992         -        P._isDirty = true;
   993         -        P.stashContentChange();
   994         -      }, false
   995         -    );
         1025  +    P.e.taEditor.addEventListener('change', ()=>P.notifyOfChange(), false);
   996   1026       
   997   1027       P.selectMimetype(false, true);
   998   1028       P.e.selectMimetype.addEventListener(
   999   1029         'change',
  1000   1030         function(e){
  1001   1031           if(P.winfo && P.winfo.mimetype !== e.target.value){
  1002   1032             P.winfo.mimetype = e.target.value;
................................................................................
  1067   1097           if(!winfo.version && winfo.type!=='sandbox'){
  1068   1098             F.message('You are editing a new, unsaved page:',winfo.name);
  1069   1099           }
  1070   1100           P.updatePageTitle().updateSaveButton(/* b/c save() routes through here */);
  1071   1101         },
  1072   1102         false
  1073   1103       );
  1074         -    /* These init()s need to come after P's event handlers are registered */
         1104  +    /* These init()s need to come after P's event handlers are registered.
         1105  +       The tab-switching is a workaround for the pinSize option of the confirmer widgets:
         1106  +       it does not work if the confirmer button being initialized is in a hidden
         1107  +       part of the DOM :/. */
         1108  +    P.tabs.switchToTab(0);
  1075   1109       WikiList.init( P.e.tabs.pageList.firstElementChild );
         1110  +    P.tabs.switchToTab(1);
  1076   1111       P.stashWidget.init(P.e.tabs.content.lastElementChild);
         1112  +    P.tabs.switchToTab(0);
  1077   1113       //P.$wikiList = WikiList/*only for testing/debugging*/;
  1078   1114     }/*F.onPageLoad()*/);
  1079   1115   
  1080   1116     /**
  1081   1117        Returns true if fossil.page.winfo is set, indicating that a page
  1082   1118        has been loaded, else it reports an error and returns false.
  1083   1119   
................................................................................
  1178   1214        P.wikiContent(). Returns this object.
  1179   1215     */
  1180   1216     P.setContentMethods = function(getter, setter){
  1181   1217       this.wikiContent.get = getter;
  1182   1218       this.wikiContent.set = setter;
  1183   1219       return this;
  1184   1220     };
         1221  +
         1222  +  /**
         1223  +     Alerts the editor app that a "change" has happened in the editor.
         1224  +     When connecting 3rd-party editor widgets to this app, it is
         1225  +     necessary to call this for any "change" events the widget emits.
         1226  +     Whether or not "change" means that there were "really" edits is
         1227  +     irrelevant, but this app will not allow saving unless it believes
         1228  +     at least one "change" has been made (by being signaled through
         1229  +     this method).
         1230  +
         1231  +     This function may perform an arbitrary amount of work, so it
         1232  +     should not be called for every keypress within the editor
         1233  +     widget. Calling it for "blur" events is generally sufficient, and
         1234  +     calling it for each Enter keypress is generally reasonable but
         1235  +     also computationally costly.
         1236  +  */
         1237  +  P.notifyOfChange = function(){
         1238  +    P._isDirty = true;
         1239  +    P.stashContentChange();
         1240  +  };
  1185   1241   
  1186   1242     /**
  1187   1243        Removes the default editor widget (and any dependent elements)
  1188   1244        from the DOM, adds the given element in its place, removes this
  1189         -     method from this object, and returns this object.
         1245  +     method from this object, and returns this object. This is not
         1246  +     needed if the 3rd-party widget replaces or hides this app's
         1247  +     editor widget (e.g. TinyMCE).
  1190   1248     */
  1191   1249     P.replaceEditorElement = function(newEditor){
  1192   1250       P.e.taEditor.parentNode.insertBefore(newEditor, P.e.taEditor);
  1193   1251       P.e.taEditor.remove();
  1194   1252       P.e.selectFontSizeWrap.remove();
  1195   1253       delete this.replaceEditorElement;
  1196   1254       return P;
................................................................................
  1282   1340        this page's input fields, and updates the UI with with the
  1283   1341        preview.
  1284   1342   
  1285   1343        Returns this object, noting that the operation is async.
  1286   1344     */
  1287   1345     P.preview = function f(switchToTab){
  1288   1346       if(!affirmPageLoaded()) return this;
  1289         -    const target = this.e.previewTarget,
  1290         -          self = this;
  1291         -    const updateView = function(c){
  1292         -      D.clearElement(target);
  1293         -      if('string'===typeof c) target.innerHTML = c;
         1347  +    return this._postPreview(this.wikiContent(), function(c){
         1348  +      P._previewTo(c);
  1294   1349         if(switchToTab) self.tabs.switchToTab(self.e.tabs.preview);
  1295         -    };
  1296         -    return this._postPreview(this.wikiContent(), updateView);
         1350  +    });
  1297   1351     };
  1298   1352   
         1353  +  /**
         1354  +     Callback for use with F.connectPagePreviewers(). Gets passed
         1355  +     the preview content.
         1356  +  */
         1357  +  P._previewTo = function(c){
         1358  +    const target = this.e.previewTarget;
         1359  +    D.clearElement(target);
         1360  +    if('string'===typeof c) D.parseHtml(target,c);
         1361  +    if(F.pikchr){
         1362  +      F.pikchr.addSrcView(target.querySelectorAll('svg.pikchr'));
         1363  +    }
         1364  +  };
         1365  +  
  1299   1366     /**
  1300   1367        Callback for use with F.connectPagePreviewers()
  1301   1368     */
  1302   1369     P._postPreview = function(content,callback){
  1303   1370       if(!affirmPageLoaded()) return this;
  1304   1371       if(!content){
  1305   1372         callback(content);
................................................................................
  1320   1387           P.previewNeedsUpdate = false;
  1321   1388           P.dispatchEvent('wiki-preview-updated',{
  1322   1389             mimetype: mimetype,
  1323   1390             element: P.e.previewTarget
  1324   1391           });
  1325   1392         },
  1326   1393         onerror: (e)=>{
  1327         -        fossil.fetch.onerror(e);
         1394  +        F.fetch.onerror(e);
  1328   1395           callback("Error fetching preview: "+e);
  1329   1396         }
  1330   1397       });
  1331   1398       return this;
  1332   1399     };
  1333   1400   
  1334   1401     /**
................................................................................
  1367   1434       fd.append('content',content);
  1368   1435       if(this.e.selectDiffWS) fd.append('ws',this.e.selectDiffWS.value);
  1369   1436       F.message(
  1370   1437         "Fetching diff..."
  1371   1438       ).fetch('wikiajax/diff',{
  1372   1439         payload: fd,
  1373   1440         onload: function(c){
  1374         -        target.innerHTML = [
         1441  +        D.parseHtml(D.clearElement(target), [
  1375   1442             "<div>Diff <code>[",
  1376   1443             self.winfo.name,
  1377   1444             "]</code> &rarr; Local Edits</div>",
  1378   1445             c||'No changes.'
  1379         -        ].join('');
         1446  +        ].join(''));
  1380   1447           if(sbs) P.tweakSbsDiffs2();
  1381   1448           F.message('Updated diff.');
  1382   1449           self.tabs.switchToTab(self.e.tabs.diff);
  1383   1450         }
  1384   1451       });
  1385   1452       return this;
  1386   1453     };
................................................................................
  1436   1503         const wi = this.winfo;
  1437   1504         wi.mimetype = P.e.selectMimetype.value;
  1438   1505         if(onlyWinfo && $stash.hasStashedContent(wi)){
  1439   1506           $stash.updateWinfo(wi);
  1440   1507         }else{
  1441   1508           $stash.updateWinfo(wi, P.wikiContent());
  1442   1509         }
  1443         -      F.message("Stashed change(s) to page ["+wi.name+"].");
         1510  +      F.message("Stashed changes to page ["+wi.name+"].");
  1444   1511         P.updatePageTitle();
  1445   1512         $stash.prune();
  1446   1513         this.previewNeedsUpdate = true;
  1447   1514       }
  1448   1515       return this;
  1449   1516     };
  1450   1517   

Added src/fossil.pikchr.js.

            1  +(function(F/*window.fossil object*/){
            2  +  "use strict";
            3  +  const D = F.dom, P = F.pikchr = {};
            4  +
            5  +  /**
            6  +     Initializes pikchr-rendered elements with the ability to
            7  +     toggle between their SVG and source code.
            8  +
            9  +     The first argument may be any of:
           10  +
           11  +     - A single SVG.pikchr element.
           12  +
           13  +     - A collection (with a forEach method) of such elements.
           14  +
           15  +     - A CSS selector string for one or more such elements.
           16  +
           17  +     - An array of such strings.
           18  +
           19  +     Passing no value is equivalent to passing 'svg.pikchr'.
           20  +
           21  +     For each SVG in the resulting set, this function sets up event
           22  +     handlers which allow the user to toggle the SVG between image and
           23  +     source code modes. The image will switch modes in response to
           24  +     cltr-click and, if its *parent* element has the "toggle" CSS
           25  +     class, it will also switch modes in response to single-click.
           26  +
           27  +     If the parent element has the "source" CSS class, the image
           28  +     starts off with its source code visible and the image hidden,
           29  +     instead of the default of the other way around.
           30  +
           31  +     Returns this object.
           32  +
           33  +     Each element will only be processed once by this routine, even if
           34  +     it is passed to this function multiple times. Each processed
           35  +     element gets a "data" attribute set to it to indicate that it was
           36  +     already dealt with.
           37  +
           38  +     This code expects the following structure around the SVGs, and
           39  +     will not process any which don't match this:
           40  +
           41  +     <DIV.pikchr-wrapper>
           42  +       <DIV.pikchr-svg><SVG.pikchr></SVG></DIV>
           43  +       <PRE.pikchr-src></PRE>
           44  +     </DIV>
           45  +  */
           46  +  P.addSrcView = function f(svg){
           47  +    if(!f.hasOwnProperty('parentClick')){
           48  +      f.parentClick = function(ev){
           49  +        if(ev.altKey || ev.metaKey || ev.ctrlKey
           50  +           /* Every combination of special key (alt, shift, ctrl,
           51  +              meta) is handled differently everywhere. Shift is used
           52  +              by the browser, Ctrl doesn't work on an iMac, and Alt is
           53  +              intercepted by most Linux window managers to control
           54  +              window movement! So...  we just listen for *any* of them
           55  +              (except Shift) and the user will need to find one which
           56  +              works on on their environment. */
           57  +           || this.classList.contains('toggle')){
           58  +          this.classList.toggle('source');
           59  +          ev.stopPropagation();
           60  +          ev.preventDefault();
           61  +        }
           62  +      };
           63  +    };
           64  +    if(!svg) svg = 'svg.pikchr';
           65  +    if('string' === typeof svg){
           66  +      document.querySelectorAll(svg).forEach((e)=>f.call(this, e));
           67  +      return this;
           68  +    }else if(svg.forEach){
           69  +      svg.forEach((e)=>f.call(this, e));
           70  +      return this;
           71  +    }
           72  +    if(svg.dataset.pikchrProcessed){
           73  +      return this;
           74  +    }
           75  +    svg.dataset.pikchrProcessed = 1;
           76  +    const parent = svg.parentNode.parentNode /* outermost div.pikchr-wrapper */;
           77  +    const srcView = parent ? svg.parentNode.nextElementSibling : undefined;
           78  +    if(!srcView || !srcView.classList.contains('pikchr-src')){
           79  +      /* Without this element, there's nothing for us to do here. */
           80  +      return this;
           81  +    }
           82  +    parent.addEventListener('click', f.parentClick, false);
           83  +  };
           84  +})(window.fossil);

Added src/fossil.popupwidget.js.

            1  +(function(F/*fossil object*/){
            2  +  /**
            3  +     A very basic tooltip-like widget. It's intended to be popped up
            4  +     to display basic information or basic user interaction
            5  +     components, e.g. a copy-to-clipboard button.
            6  +
            7  +     Requires: fossil.bootstrap, fossil.dom
            8  +  */
            9  +  const D = F.dom;
           10  +
           11  +  /**
           12  +     Creates a new tooltip-like widget using the given options object.
           13  +
           14  +     Options:
           15  +
           16  +     .refresh: callback which is called just before the tooltip is
           17  +     revealed or moved. It must refresh the contents of the tooltip,
           18  +     if needed, by applying the content to/within this.e, which is the
           19  +     base DOM element for the tooltip (and is a child of
           20  +     document.body). If the contents are static and set up via the
           21  +     .init option then this callback is not needed.
           22  +
           23  +     .adjustX: an optional callback which is called when the tooltip
           24  +     is to be displayed at a given position and passed the X
           25  +     viewport-relative coordinate. This routine must either return its
           26  +     argument as-is or return an adjusted value. The intent is to
           27  +     allow a given tooltip may be positioned more appropriately for a
           28  +     given context, if needed (noting that the desired position can,
           29  +     and probably should, be passed to the show() method
           30  +     instead). This class's API assumes that clients give it
           31  +     viewport-relative coordinates, and it will take care to translate
           32  +     those to page-relative, so this callback should not do so.
           33  +
           34  +     .adjustY: the Y counterpart of adjustX.
           35  +
           36  +     .init: optional callback called one time to initialize the state
           37  +     of the tooltip. This is called after the this.e has been created
           38  +     and added (initially hidden) to the DOM. If this is called, it is
           39  +     removed from the object immediately after it is called.
           40  +
           41  +     All callback options are called with the PopupWidget object as
           42  +     their "this".
           43  +
           44  +
           45  +     .cssClass: optional CSS class, or list of classes, to apply to
           46  +     the new element.
           47  +
           48  +     .style: optional object of properties to copy directly into
           49  +     the element's style object.     
           50  +
           51  +     The options passed to this constructor get normalized into a
           52  +     separate object which includes any default values for options not
           53  +     provided by the caller. That object is available this the
           54  +     resulting PopupWidget's options property. Default values for any
           55  +     options not provided by the caller are pulled from
           56  +     PopupWidget.defaultOptions, and modifying those affects all
           57  +     future calls to this method but has no effect on existing
           58  +     instances.
           59  +
           60  +
           61  +     Example:
           62  +
           63  +     const tip = new fossil.PopupWidget({
           64  +       init: function(){
           65  +         // optionally populate DOM element this.e with the widget's
           66  +         // content.
           67  +       },
           68  +       refresh: function(){
           69  +         // (re)populate/refresh the contents of the main
           70  +         // wrapper element, this.e.
           71  +       }
           72  +     });
           73  +
           74  +     tip.show(50, 100);
           75  +     // ^^^ viewport-relative coordinates. See show() for other options.
           76  +
           77  +  */
           78  +  F.PopupWidget = function f(opt){
           79  +    opt = F.mergeLastWins(f.defaultOptions,opt);
           80  +    this.options = opt;
           81  +    const e = this.e = D.addClass(D.div(), opt.cssClass);
           82  +    this.show(false);
           83  +    if(opt.style){
           84  +      let k;
           85  +      for(k in opt.style){
           86  +        if(opt.style.hasOwnProperty(k)) e.style[k] = opt.style[k];
           87  +      }
           88  +    }
           89  +    D.append(document.body, e/*must be in the DOM for size calc. to work*/);
           90  +    D.copyStyle(e, opt.style);
           91  +    if(opt.init){
           92  +      opt.init.call(this);
           93  +      delete opt.init;
           94  +    }
           95  +  };
           96  +
           97  +  /**
           98  +     Default options for the PopupWidget constructor. These values are
           99  +     used for any options not provided by the caller. Any changes made
          100  +     to this instace affect future calls to PopupWidget() but have no
          101  +     effect on existing instances.
          102  +  */
          103  +  F.PopupWidget.defaultOptions = {
          104  +    cssClass: 'fossil-tooltip',
          105  +    style: undefined /*{optional properties copied as-is into element.style}*/,
          106  +    adjustX: (x)=>x,
          107  +    adjustY: (y)=>y,
          108  +    refresh: function(){},
          109  +    init: undefined /* optional initialization function */
          110  +  };
          111  +
          112  +  F.PopupWidget.prototype = {
          113  +
          114  +    /** Returns true if the widget is currently being shown, else false. */
          115  +    isShown: function(){return !this.e.classList.contains('hidden')},
          116  +
          117  +    /** Calls the refresh() method of the options object and returns
          118  +        this object. */
          119  +    refresh: function(){
          120  +      if(this.options.refresh){
          121  +        this.options.refresh.call(this);
          122  +      }
          123  +      return this;
          124  +    },
          125  +
          126  +    /**
          127  +       Shows or hides the tooltip.
          128  +
          129  +       Usages:
          130  +
          131  +       (bool showIt) => hide it or reveal it at its last position.
          132  +
          133  +       (x, y) => reveal/move it at/to the given
          134  +       relative-to-the-viewport position, which will be adjusted to make
          135  +       it page-relative.
          136  +
          137  +       (DOM element) => reveal/move it at/to a position based on the
          138  +       the given element (adjusted slightly).
          139  +
          140  +       For the latter two, this.options.adjustX() and adjustY() will
          141  +       be called to adjust it further.
          142  +
          143  +       Returns this object.
          144  +
          145  +       Sidebar: showing/hiding the widget is, as is conventional for
          146  +       this framework, done by removing/adding the 'hidden' CSS class
          147  +       to it, so that class must be defined appropriately.
          148  +    */
          149  +    show: function(){
          150  +      var x = undefined, y = undefined, showIt;
          151  +      if(2===arguments.length){
          152  +        x = arguments[0];
          153  +        y = arguments[1];
          154  +        showIt = true;
          155  +      }else if(1===arguments.length){
          156  +        if(arguments[0] instanceof HTMLElement){
          157  +          const p = arguments[0];
          158  +          const r = p.getBoundingClientRect();
          159  +          x = r.x + r.x/5;
          160  +          y = r.y - r.height/2;
          161  +          showIt = true;
          162  +        }else{
          163  +          showIt = !!arguments[0];
          164  +        }
          165  +      }
          166  +      if(showIt){
          167  +        this.refresh();
          168  +        x = this.options.adjustX.call(this,x);
          169  +        y = this.options.adjustY.call(this,y);
          170  +        x += window.pageXOffset;
          171  +        y += window.pageYOffset;
          172  +      }
          173  +      if(showIt){
          174  +        if('number'===typeof x && 'number'===typeof y){
          175  +          this.e.style.left = x+"px";
          176  +          this.e.style.top = y+"px";
          177  +        }
          178  +        D.removeClass(this.e, 'hidden');
          179  +      }else{
          180  +        D.addClass(this.e, 'hidden');
          181  +        this.e.style.removeProperty('left');
          182  +        this.e.style.removeProperty('top');
          183  +      }
          184  +      return this;
          185  +    },
          186  +
          187  +    hide: function(){return this.show(false)}
          188  +  }/*F.PopupWidget.prototype*/;
          189  +
          190  +  /**
          191  +     Internal impl for F.toast() and friends.
          192  +
          193  +     args:
          194  +
          195  +     1) CSS class to assign to the outer element, along with
          196  +     fossil-toast-message. Must be falsy for the non-warning/non-error
          197  +     case.
          198  +
          199  +     2) Multiplier of F.toast.config.displayTimeMs. Should be
          200  +     1 for default case and progressively higher for warning/error
          201  +     cases.
          202  +
          203  +     3) The 'arguments' object from the function which is calling
          204  +     this.
          205  +
          206  +     Returns F.toast.
          207  +  */
          208  +  const toastImpl = function f(cssClass, durationMult, argsObject){
          209  +    if(!f.toaster){
          210  +      f.toaster = new F.PopupWidget({
          211  +        cssClass: 'fossil-toast-message'
          212  +      });
          213  +    }
          214  +    const T = f.toaster;
          215  +    if(f._timer) clearTimeout(f._timer);
          216  +    D.clearElement(T.e);
          217  +    if(f._prevCssClass) T.e.classList.remove(f._prevCssClass);
          218  +    if(cssClass) T.e.classList.add(cssClass);
          219  +    f._prevCssClass = cssClass;
          220  +    D.append(T.e, Array.prototype.slice.call(argsObject,0));
          221  +    T.show(F.toast.config.position.x, F.toast.config.position.y);
          222  +    f._timer = setTimeout(
          223  +      ()=>T.hide(),
          224  +      F.toast.config.displayTimeMs * durationMult
          225  +    );
          226  +    return F.toast;
          227  +  };
          228  +
          229  +  F.toast = {
          230  +    config: {
          231  +      position: { x: 5, y: 5 /*viewport-relative, pixels*/ },
          232  +      displayTimeMs: 3000
          233  +    },
          234  +    /**
          235  +       Convenience wrapper around a PopupWidget which pops up a shared
          236  +       PopupWidget instance to show toast-style messages (commonly seen
          237  +       on Android). Its arguments may be anything suitable for passing
          238  +       to fossil.dom.append(), and each argument is first append()ed to
          239  +       the toast widget, then the widget is shown for
          240  +       F.toast.config.displayTimeMs milliseconds. This is called while
          241  +       a toast is currently being displayed, the first will be overwritten
          242  +       and the time until the message is hidden will be reset.
          243  +
          244  +       The toast is always shown at the viewport-relative coordinates
          245  +       defined by the F.toast.config.position.
          246  +
          247  +       The toaster's DOM element has the CSS classes fossil-tooltip
          248  +       and fossil-toast, so can be style via those.
          249  +    */
          250  +    message: function(/*...*/){
          251  +      return toastImpl(false,1, arguments);
          252  +    },
          253  +    /**
          254  +       Displays a toast with the 'warning' CSS class assigned to it. It
          255  +       displays for 1.5 times as long as a normal toast.
          256  +    */
          257  +    warning: function(/*...*/){
          258  +      return toastImpl('warning',1.5,arguments);
          259  +    },
          260  +    /**
          261  +       Displays a toast with the 'warning' CSS class assigned to it. It
          262  +       displays for twice as long as a normal toast.
          263  +    */
          264  +    error: function(/*...*/){
          265  +      return toastImpl('error',2,arguments);
          266  +    }
          267  +  }/*F.toast*/;
          268  +
          269  +
          270  +  F.helpButtonlets = {
          271  +    /**
          272  +       Initializes one or more "help buttonlets". It may be passed any of:
          273  +
          274  +       - A string: CSS selector (multiple matches are legal)
          275  +
          276  +       - A single DOM element.
          277  +
          278  +       - A forEach-compatible container of DOM elements.
          279  +
          280  +       - No arguments, which is equivalent to passing the string
          281  +       ".help-buttonlet:not(.processed)".
          282  +
          283  +       Passing the same element(s) more than once is a no-op: during
          284  +       initialization, each elements get the class'processed' added to
          285  +       it, and any elements with that class are skipped.
          286  +
          287  +       All child nodes of a help buttonlet are removed from the button
          288  +       during initialization and stashed away for use in a PopupWidget
          289  +       when the botton is clicked.
          290  +
          291  +    */
          292  +    setup: function f(){
          293  +      if(!f.hasOwnProperty('clickHandler')){
          294  +        f.clickHandler = function fch(ev){
          295  +          if(!fch.popup){
          296  +            fch.popup = new F.PopupWidget({
          297  +              cssClass: ['fossil-tooltip', 'help-buttonlet-content'],
          298  +              refresh: function(){
          299  +              }
          300  +            });
          301  +            fch.popup.e.style.maxWidth = '80%'/*of body*/;
          302  +            const hide = ()=>fch.popup.hide();
          303  +            fch.popup.e.addEventListener('click', hide, false);
          304  +            document.body.addEventListener('click', hide, true);
          305  +            document.body.addEventListener('keydown', function(ev){
          306  +              if(fch.popup.isShown() && 27===ev.which){
          307  +                fch.popup.hide();
          308  +              }
          309  +            }, true);
          310  +          }
          311  +          D.append(D.clearElement(fch.popup.e), ev.target.$helpContent);
          312  +          var popupRect = ev.target.getClientRects()[0];
          313  +          var x = popupRect.left, y = popupRect.top;
          314  +          if(x<0) x = 0;
          315  +          if(y<0) y = 0;
          316  +          /* Shift the help around a bit to "better" fit the
          317  +             screen. However, fch.popup.e.getClientRects() is empty
          318  +             until the popup is shown, so we have to show it,
          319  +             calculate the resulting size, then move and/or resize it.
          320  +
          321  +             This algorithm/these heuristics can certainly be improved
          322  +             upon.
          323  +          */
          324  +          fch.popup.show(x, y);
          325  +          x = popupRect.left, y = popupRect.top;
          326  +          popupRect = fch.popup.e.getBoundingClientRect();
          327  +          const rectBody = document.body.getClientRects()[0];
          328  +          if(popupRect.right > rectBody.right){
          329  +            x -= (popupRect.right - rectBody.right);
          330  +          }
          331  +          if(x + popupRect.width > rectBody.right){
          332  +            x = rectBody.x + (rectBody.width*0.1);
          333  +            fch.popup.e.style.minWidth = '70%';
          334  +          }else{
          335  +            fch.popup.e.style.removeProperty('min-width');
          336  +            x -= popupRect.width/2;
          337  +          }
          338  +          if(x<0) x = 0;
          339  +          //console.debug("dimensions",x,y, popupRect, rectBody);
          340  +          fch.popup.show(x, y);
          341  +        };
          342  +        f.foreachElement = function(e){
          343  +          if(e.classList.contains('processed')) return;
          344  +          e.classList.add('processed');
          345  +          e.$helpContent = [];
          346  +          /* We have to move all child nodes out of the way because we
          347  +             cannot hide TEXT nodes via CSS (which cannot select TEXT
          348  +             nodes). We have to do it in two steps to avoid invaliding
          349  +             the list during traversal. */
          350  +          e.childNodes.forEach((ch)=>e.$helpContent.push(ch));
          351  +          e.$helpContent.forEach((ch)=>ch.remove());
          352  +          e.addEventListener('click', f.clickHandler, false);
          353  +        };
          354  +      }/*static init*/
          355  +      var elems;
          356  +      if(!arguments.length){
          357  +        arguments[0] = '.help-buttonlet:not(.processed)';
          358  +        arguments.length = 1;
          359  +      }
          360  +      if(arguments.length){
          361  +        if('string'===typeof arguments[0]){
          362  +          elems = document.querySelectorAll(arguments[0]);
          363  +        }else if(arguments[0] instanceof HTMLElement){
          364  +          elems = [arguments[0]];
          365  +        }else if(arguments[0].forEach){/* assume DOM element list or array */
          366  +          elems = arguments[0];
          367  +        }
          368  +      }
          369  +      if(elems) elems.forEach(f.foreachElement);
          370  +    },
          371  +    
          372  +    /**
          373  +       Sets up the given element as a "help buttonlet", adding the CSS
          374  +       class help-buttonlet to it. Any (optional) arguments after the
          375  +       first are appended to the element using fossil.dom.append(), so
          376  +       that they become the content for the buttonlet's popup help.
          377  +
          378  +       The element is then passed to this.setup() before it
          379  +       is returned from this function.
          380  +    */
          381  +    create: function(elem/*...body*/){
          382  +      D.addClass(elem, 'help-buttonlet');
          383  +      if(arguments.length>1){
          384  +        const args = Array.prototype.slice.call(arguments,1);
          385  +        D.append(elem, args);
          386  +      }
          387  +      this.setup(elem);
          388  +      return elem;
          389  +    }
          390  +  }/*helpButtonlets*/;
          391  +
          392  +  F.onDOMContentLoaded( ()=>F.helpButtonlets.setup() );
          393  +  
          394  +})(window.fossil);

Changes to src/fossil.storage.js.

    87     87     /**
    88     88        A proxy for localStorage or sessionStorage or a
    89     89        page-instance-local proxy, if neither one is availble.
    90     90   
    91     91        Which exact storage implementation is uses is unspecified, and
    92     92        apps must not rely on it.
    93     93     */
    94         -  fossil.storage = {
           94  +  F.storage = {
    95     95       storageKeyPrefix: storageKeyPrefix,
    96     96       /** Sets the storage key k to value v, implicitly converting
    97     97           it to a string. */
    98     98       set: (k,v)=>$storage.setItem(storageKeyPrefix+k,v),
    99     99       /** Sets storage key k to JSON.stringify(v). */
   100    100       setJSON: (k,v)=>$storage.setItem(storageKeyPrefix+k,JSON.stringify(v)),
   101    101       /** Returns the value for the given storage key, or
................................................................................
   133    133           and sessionStorage are unavailable. */
   134    134       isTransient: ()=>$storageHolder!==$storage,
   135    135       /** Returns a symbolic name for the current storage mechanism. */
   136    136       storageImplName: function(){
   137    137         if($storage===window.localStorage) return 'localStorage';
   138    138         else if($storage===window.sessionStorage) return 'sessionStorage';
   139    139         else return 'transient';
          140  +    },
          141  +
          142  +    /**
          143  +       Returns a brief help text string for the currently-selected
          144  +       storage type.
          145  +    */
          146  +    storageHelpDescription: function(){
          147  +      return {
          148  +        localStorage: "Browser-local persistent storage with an "+
          149  +          "unspecified long-term lifetime (survives closing the browser, "+
          150  +          "but maybe not a browser upgrade).",
          151  +        sessionStorage: "Storage local to this browser tab, "+
          152  +          "lost if this tab is closed.",
          153  +        "transient": "Transient storage local to this invocation of this page."
          154  +      }[this.storageImplName()];
   140    155       }
   141    156     };
   142    157   
   143    158   })(window.fossil);

Changes to src/fossil.tabs.js.

    28     28     */
    29     29     TabManager.defaultOptions = {
    30     30       tabAccessKeys: true
    31     31     };
    32     32   
    33     33     /**
    34     34        Internal helper to normalize a method argument to a tab
    35         -     element. arg may be a tab DOM element or an index into
    36         -     tabMgr.e.tabs.childNodes. Returns the corresponding tab element.
           35  +     element. arg may be a tab DOM element, a selector string, or an
           36  +     index into tabMgr.e.tabs.childNodes. Returns the corresponding
           37  +     tab element.
    37     38     */
    38     39     const tabArg = function(arg,tabMgr){
    39     40       if('string'===typeof arg) arg = E(arg);
    40     41       else if(tabMgr && 'number'===typeof arg && arg>=0){
    41     42         arg = tabMgr.e.tabs.childNodes[arg];
    42     43       }
    43         -    if(arg){
    44         -      if('FIELDSET'===arg.tagName && arg.classList.contains('tab-wrapper')){
    45         -        arg = arg.firstElementChild;
    46         -      }
    47         -    }
    48     44       return arg;
    49     45     };
    50     46   
    51     47     /**
    52     48       Sets sets the visibility of tab element e to on or off. e MUST be
    53         -    a TabManager tab element which has been wrapped in a
    54         -    FIELDSET.tab-wrapper parent element. We disable the hidden
    55         -    FIELDSET.tab-wrapper elements so that any access keys assigned
    56         -    to their children cannot be inadvertently triggered
           49  +    a TabManager tab element.
    57     50     */
    58     51     const setVisible = function(e,yes){
    59         -    const fsWrapper = e.parentElement/*FIELDSET wrapper*/;
    60         -    if(yes){
    61         -      D.removeClass(e, 'hidden');
    62         -      D.enable(fsWrapper);
    63         -    }else{
    64         -      D.addClass(e, 'hidden');
    65         -      D.disable(fsWrapper);
    66         -    }
           52  +    D[yes ? 'removeClass' : 'addClass'](e, 'hidden');
    67     53     };
    68     54   
    69     55     TabManager.prototype = {
    70     56       /**
    71     57          Initializes the tabs associated with the given tab container
    72     58          (DOM element or selector for a single element). This must be
    73     59          called once before using any other member functions of a given
................................................................................
   162    148         if(!f.click){
   163    149           f.click = function(e){
   164    150            e.target.$manager.switchToTab(e.target.$tab);
   165    151           };
   166    152         }
   167    153         tab = tabArg(tab);
   168    154         tab.remove();
   169         -      const eFs = D.addClass(D.fieldset(), 'tab-wrapper');
   170         -      D.append(eFs, D.addClass(tab,'tab-panel'));
   171         -      D.append(this.e.tabs, eFs);
          155  +      D.append(this.e.tabs, D.addClass(tab,'tab-panel'));
   172    156         const tabCount = this.e.tabBar.childNodes.length+1;
   173    157         const lbl = tab.dataset.tabLabel || 'Tab #'+tabCount;
   174    158         const btn = D.addClass(D.append(D.span(), lbl), 'tab-button');
   175    159         D.append(this.e.tabBar,btn);
   176    160         btn.$manager = this;
   177    161         btn.$tab = tab;
   178    162         if(this.options.tabAccessKeys){
................................................................................
   218    202   
   219    203          Returns this object.
   220    204       */
   221    205       addEventListener: function(eventName, callback){
   222    206         this.e.container.addEventListener(eventName, callback, false);
   223    207         return this;
   224    208       },
          209  +
          210  +    /**
          211  +       Inserts the given DOM element immediately after the tab bar.
          212  +       Intended for a status bar or similar always-visible component.
          213  +       Returns this object.
          214  +    */
          215  +    addCustomWidget: function(e){
          216  +      this.e.container.insertBefore(e, this.e.tabs);
          217  +      return this;
          218  +    },
   225    219   
   226    220       /**
   227    221          If the given DOM element, unique selector, or integer (0-based
   228    222          tab number) is one of this object's tabs, the UI makes that tab
   229    223          the currently-visible one, firing any relevant events. Returns
   230    224          this object. If the argument is the current tab, this is a
   231    225          no-op, and no events are fired.
................................................................................
   236    230         if(tab===this._currentTab) return this;
   237    231         else if(this._currentTab){
   238    232           this._dispatchEvent('before-switch-from', this._currentTab);
   239    233         }
   240    234         delete this._currentTab;
   241    235         this.e.tabs.childNodes.forEach((e,ndx)=>{
   242    236           const btn = this.e.tabBar.childNodes[ndx];
   243         -        e = e.firstElementChild /* b/c arguments[0] is a FIELDSET wrapper */;
   244    237           if(e===tab){
   245    238             if(D.hasClass(e,'selected')){
   246    239               return;
   247    240             }
   248    241             self._dispatchEvent('before-switch-to',tab);
   249    242             setVisible(e, true);
   250    243             this._currentTab = e;

Added src/fossil.wikiedit-wysiwyg.js.

            1  +/**
            2  +   A slight adaptation of fossil's legacy wysiwyg wiki editor which
            3  +   makes it usable with the newer editor's edit widget replacement
            4  +   API.
            5  +
            6  +   Requires: window.fossil, fossil.dom, and that the current page is
            7  +   /wikiedit. If called from another page it returns without effect.
            8  +
            9  +   Caveat: this is an all-or-nothing solution. That is, once plugged
           10  +   in to /wikiedit, it cannot be removed without reloading the page.
           11  +   That is a limitation of the current editor-widget-swapping API.
           12  +*/
           13  +(function(F/*fossil object*/){
           14  +  'use strict';
           15  +  if(!F || !F.page || F.page.name!=='wikiedit') return;
           16  +
           17  +  const D = F.dom;
           18  +
           19  +  ////////////////////////////////////////////////////////////////////////
           20  +  // Install an app-specific stylesheet...
           21  +  (function(){
           22  +    const head = document.head || document.querySelector('head'),
           23  +          styleTag = document.createElement('style'),
           24  +          styleCSS = `
           25  +.intLink { cursor: pointer; }
           26  +img.intLink { border: 0; }
           27  +#wysiwyg-container {
           28  +  display: flex;
           29  +  flex-direction: column;
           30  +  max-width: 100% /* w/o this, toolbars don't wrap properly! */
           31  +}
           32  +#wysiwygBox {
           33  +  border: 1px solid rgba(127,127,127,0.3);
           34  +  border-radius: 0.25em;
           35  +  padding: 0.25em 1em;
           36  +  margin: 0;
           37  +  overflow: auto;
           38  +  min-height: 20em;
           39  +  resize: vertical;
           40  +}
           41  +#wysiwygEditMode { /* wrapper for radio buttons */
           42  +  border: 1px solid rgba(127,127,127,0.3);
           43  +  border-radius: 0.25em;
           44  +  padding: 0 0.35em 0 0.35em
           45  +}
           46  +#wysiwygEditMode > * {
           47  +  vertical-align: text-top;
           48  +}
           49  +#wysiwygEditMode label { cursor: pointer; }
           50  +#wysiwyg-toolbars {
           51  +  margin: 0 0 0.25em 0;
           52  +  display: flex;
           53  +  flex-wrap: wrap;
           54  +  flex-direction: column;
           55  +  align-items: flex-start;
           56  +}
           57  +#wysiwyg-toolbars > * {
           58  +  margin: 0 0.5em 0.25em 0;
           59  +}
           60  +#wysiwyg-toolBar1, #wysiwyg-toolBar2 {
           61  +  margin: 0 0.2em 0.2em 0;
           62  +  display: flex;
           63  +  flex-flow: row wrap;
           64  +}
           65  +#wysiwyg-toolBar1 > * { /* formatting buttons */
           66  +  vertical-align: middle;
           67  +  margin: 0 0.25em 0.25em 0;
           68  +}
           69  +#wysiwyg-toolBar2 > * { /* icons */
           70  +  border: 1px solid rgba(127,127,127,0.3);
           71  +  vertical-align: baseline;
           72  +  margin: 0.1em;
           73  +}
           74  +`;
           75  +    head.appendChild(styleTag);
           76  +    styleTag.type = 'text/css';
           77  +    D.append(styleTag, styleCSS);
           78  +  })();
           79  +
           80  +  const outerContainer = D.attr(D.div(), 'id', 'wysiwyg-container'),
           81  +        toolbars = D.attr(D.div(), 'id', 'wysiwyg-toolbars'),
           82  +        toolbar1 = D.attr(D.div(), 'id', 'wysiwyg-toolBar1'),
           83  +        // ^^^ formatting options
           84  +        toolbar2 = D.attr(D.div(), 'id', 'wysiwyg-toolBar2')
           85  +        // ^^^^ action icon buttons
           86  +  ;
           87  +  D.append(outerContainer, D.append(toolbars, toolbar1, toolbar2));
           88  +
           89  +  /** Returns a function which simplifies adding a list of options
           90  +      to the given select element. See below for example usage. */
           91  +  const addOptions = function(select){
           92  +    return function ff(value, label){
           93  +      D.option(select, value, label || value);
           94  +      return ff;
           95  +    };
           96  +  };
           97  +
           98  +  ////////////////////////////////////////////////////////////////////////
           99  +  // Edit mode selection (radio buttons).
          100  +  const radio0 =
          101  +        D.attr(
          102  +          D.input('radio'),
          103  +          'name','wysiwyg-mode',
          104  +          'id', 'wysiwyg-mode-0',
          105  +          'value',0,
          106  +          'checked',true),
          107  +        radio1 = D.attr(
          108  +          D.input('radio'),
          109  +          'id','wysiwyg-mode-1',
          110  +          'name','wysiwyg-mode',
          111  +          'value',1),
          112  +        radios = D.append(
          113  +          D.attr(D.span(), 'id', 'wysiwygEditMode'),
          114  +          radio0, D.append(
          115  +            D.attr(D.label(), 'for', 'wysiwyg-mode-0'),
          116  +            "WYSIWYG"
          117  +          ),
          118  +          radio1, D.append(
          119  +            D.attr(D.label(), 'for', 'wysiwyg-mode-1'),
          120  +            "Raw HTML"
          121  +          )
          122  +        );
          123  +  D.append(toolbar1, radios);
          124  +  const radioHandler = function(){setDocMode(+this.value)};
          125  +  radio0.addEventListener('change',radioHandler, false);
          126  +  radio1.addEventListener('change',radioHandler, false);
          127  +
          128  +
          129  +  ////////////////////////////////////////////////////////////////////////
          130  +  // Text formatting options...
          131  +  var select;
          132  +  select = D.addClass(D.select(), 'format');
          133  +  select.dataset.format = "formatblock";
          134  +  D.append(toolbar1, select);
          135  +  addOptions(select)(
          136  +    '', '- formatting -')(
          137  +    "h1", "Title 1 <h1>")(
          138  +    "h2", "Title 2 <h2>")(
          139  +    "h3", "Title 3 <h3>")(
          140  +    "h4", "Title 4 <h4>")(
          141  +    "h5", "Title 5 <h5>")(
          142  +    "h6", "Subtitle <h6>")(
          143  +    "p", "Paragraph <p>")(
          144  +    "pre", "Preformatted <pre>");
          145  +
          146  +  select = D.addClass(D.select(), 'format');
          147  +  select.dataset.format = "fontname";
          148  +  D.append(toolbar1, select);
          149  +  D.addClass(
          150  +    D.option(select, '', '- font -'),
          151  +    "heading"
          152  +  );
          153  +  addOptions(select)(
          154  +    'Arial')(
          155  +    'Arial Black')(
          156  +    'Courier New')(
          157  +    'Times New Roman');
          158  +
          159  +  select = D.addClass(D.select(), 'format');
          160  +  D.append(toolbar1, select);
          161  +  select.dataset.format = "fontsize";
          162  +  D.addClass(
          163  +    D.option(select, '', '- size -'),
          164  +    "heading"
          165  +  );
          166  +  addOptions(select)(
          167  +    "1", "Very small")(
          168  +    "2", "A bit small")(
          169  +    "3", "Normal")(
          170  +    "4", "Medium-large")(
          171  +    "5", "Big")(
          172  +    "6", "Very big")(
          173  +    "7", "Maximum");
          174  +
          175  +  select = D.addClass(D.select(), 'format');
          176  +  D.append(toolbar1, select);
          177  +  select.dataset.format = 'forecolor';
          178  +  D.addClass(
          179  +    D.option(select, '', '- color -'),
          180  +    "heading"
          181  +  );
          182  +  addOptions(select)(
          183  +    "red", "Red")(
          184  +    "blue", "Blue")(
          185  +    "green", "Green")(
          186  +    "black", "Black")(
          187  +    "grey", "Grey")(
          188  +    "yellow", "Yellow")(
          189  +    "cyan", "Cyan")(
          190  +    "magenta", "Magenta");
          191  +
          192  +
          193  +  ////////////////////////////////////////////////////////////////////////
          194  +  // Icon-based toolbar...
          195  +  /**
          196  +     Inject the icons...
          197  +
          198  +     mkbuiltins strips anything which looks like a C++-style comment,
          199  +     even if it's in a string literal, and thus the runs of "/"
          200  +     characters in the DOM element data attributes have been mangled
          201  +     to work around that: we simply use \x2f for every 2nd slash.
          202  +  */
          203  +  (function f(title,format,src){
          204  +    const img = D.img();
          205  +    D.append(toolbar2, img);
          206  +    D.addClass(img, 'intLink');
          207  +    D.attr(img, 'title', title);
          208  +    img.dataset.format = format;
          209  +    D.attr(img, 'src', 'string'===typeof src ? src : src.join(''));
          210  +    return f;
          211  +  })(
          212  +    'Undo', 'undo',
          213  +    ["data:image/gif;base64,R0lGODlhFgAWAOMKADljwliE33mOrpGjuYKl8aezxqPD+7",
          214  +     "/I19DV3NHa7P/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f",
          215  +     "/\x2f/\x2f/\x2f/yH5BAEKAA8ALAAAAAAWABYAAARR8MlJq704680",
          216  +     "7TkaYeJJBnES4EeUJvIGapWYAC0CsocQ7SDlWJkAkCA6ToMYWIARGQF3mRQVIEjkkSVLIbSfE",
          217  +     "whdRIH4fh/DZMICe3/C4nBQBADs="]
          218  +  )(
          219  +    'Redo','redo',
          220  +    ["data:image/gif;base64,R0lGODlhFgAWAMIHAB1ChDljwl9vj1iE34Kl8aPD+7/I1/",
          221  +     "/\x2f/yH5BAEKAAcALAAAAAAWABYAAANKeLrc/jDKSesyphi7SiEgsVXZEATDICqBVJjpqWZt9Na",
          222  +     "EDNbQK1wCQsxlYnxMAImhyDoFAElJasRRvAZVRqqQXUy7Cgx4TC6bswkAOw=="]
          223  +  )(
          224  +    "Remove formatting",
          225  +    "removeFormat",
          226  +    ["data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AA",
          227  +     "AABGdBTUEAALGPC/xhBQAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAOxAAADsQBlSsOGwA",
          228  +     "AAAd0SU1FB9oECQMCKPI8CIIAAAAIdEVYdENvbW1lbnQA9syWvwAAAuhJREFUOMtjYBgFxAB5",
          229  +     "01ZWBvVaL2nHnlmk6mXCJbF69zU+Hz/9fB5O1lx+bg45qhl8/fYr5it3XrP/YWTUvvvk3VeqG",
          230  +     "Xz70TvbJy8+Wv39+2/Hz19/mGwjZzuTYjALuoBv9jImaXHeyD3H7kU8fPj2ICML8z92dlbtMz",
          231  +     "deiG3fco7J08foH1kurkm3E9iw54YvKwuTuom+LPt/BgbWf3/\x2fsf37/1/c02cCG1lB8f/\x2ff95",
          232  +     "DZx74MTMzshhoSm6szrQ/a6Ir/Z2RkfEjBxuLYFpDiDi6Af/\x2f/2ckaHBp7+7wmavP5n76+P2C",
          233  +     "lrLIYl8H9W36auJCbCxM4szMTJac7Kza/\x2f/\x2fR3H1w2cfWAgafPbqs5g7D95++/P1B4+ECK8tA",
          234  +     "wMDw/1H7159+/7r7ZcvPz4fOHbzEwMDwx8GBgaGnNatfHZx8zqrJ+4VJBh5CQEGOySEua/v3n",
          235  +     "7hXmqI8WUGBgYGL3vVG7fuPK3i5GD9/fja7ZsMDAzMG/Ze52mZeSj4yu1XEq/ff7W5dvfVAS1",
          236  +     "lsXc4Db7z8C3r8p7Qjf/\x2f/2dnZGxlqJuyr3rPqQd/Hhyu7oSpYWScylDQsd3kzvnH738wMDzj",
          237  +     "5GBN1VIWW4c3KDon7VOvm7S3paB9u5qsU5/x5KUnlY+eexQbkLNsErK61+++VnAJcfkyMTIwf",
          238  +     "fj0QwZbJDKjcETs1Y8evyd48toz8y/ffzv/\x2fvPP4veffxpX77z6l5JewHPu8MqTDAwMDLzyrj",
          239  +     "b/mZm0JcT5Lj+89+Ybm6zz95oMh7s4XbygN3Sluq4Mj5K8iKMgP4f0/\x2f/\x2ffv77/\x2f8nLy+7MCc",
          240  +     "XmyYDAwODS9jM9tcvPypd35pne3ljdjvj26+H2dhYpuENikgfvQeXNmSl3tqepxXsqhXPyc66",
          241  +     "6s+fv1fMdKR3TK72zpix8nTc7bdfhfkEeVbC9KhbK/9iYWHiErbu6MWbY/7/\x2f8/4/\x2f9/pgOnH",
          242  +     "6jGVazvFDRtq2VgiBIZrUTIBgCk+ivHvuEKwAAAAABJRU5ErkJggg=="]
          243  +  )(
          244  +    "Bold",
          245  +    "bold",
          246  +    ["data:image/gif;base64,R0lGODlhFgAWAID/AMDAwAAAACH5BAEAAAAALAAAAAAWAB",
          247  +     "YAQAInhI+pa+H9mJy0LhdgtrxzDG5WGFVk6aXqyk6Y9kXvKKNuLbb6zgMFADs="]
          248  +  )(
          249  +    "Italic",
          250  +    "italic",
          251  +    ["data:image/gif;base64,R0lGODlhFgAWAKEDAAAAAF9vj5WIbf/\x2f/yH5BAEAAAMALA",
          252  +     "AAAAAWABYAAAIjnI+py+0Po5x0gXvruEKHrF2BB1YiCWgbMFIYpsbyTNd2UwAAOw=="]
          253  +  )(
          254  +    "Underline",
          255  +    "underline",
          256  +    ["data:image/gif;base64,R0lGODlhFgAWAKECAAAAAF9vj/\x2f/\x2f/\x2f/\x2fyH5BAEAAAIALA",
          257  +     "AAAAAWABYAAAIrlI+py+0Po5zUgAsEzvEeL4Ea15EiJJ5PSqJmuwKBEKgxVuXWtun+DwxCCgA",
          258  +     "7"]
          259  +  )(
          260  +    "Left align",
          261  +    "justifyleft",
          262  +    ["data:image/gif;base64,R0lGODlhFgAWAID/AMDAwAAAACH5BAEAAAAALAAAAAAWAB",
          263  +     "YAQAIghI+py+0Po5y02ouz3jL4D4JMGELkGYxo+qzl4nKyXAAAOw=="]
          264  +  )(
          265  +    "Center align",
          266  +    "justifycenter",
          267  +    ["data:image/gif;base64,R0lGODlhFgAWAID/AMDAwAAAACH5BAEAAAAALAAAAAAWAB",
          268  +     "YAQAIfhI+py+0Po5y02ouz3jL4D4JOGI7kaZ5Bqn4sycVbAQA7"]
          269  +  )(
          270  +    "Right align",
          271  +    "justifyright",
          272  +    ["data:image/gif;base64,R0lGODlhFgAWAID/AMDAwAAAACH5BAEAAAAALAAAAAAWAB",
          273  +     "YAQAIghI+py+0Po5y02ouz3jL4D4JQGDLkGYxouqzl43JyVgAAOw=="]
          274  +  )(
          275  +    "Numbered list",
          276  +    "insertorderedlist",
          277  +    ["data:image/gif;base64,R0lGODlhFgAWAMIGAAAAADljwliE35GjuaezxtHa7P/\x2f/\x2f",
          278  +     "/\x2f/yH5BAEAAAcALAAAAAAWABYAAAM2eLrc/jDKSespwjoRFvggCBUBoTFBeq6QIAysQnRHaEO",
          279  +     "zyaZ07Lu9lUBnC0UGQU1K52s6n5oEADs="]
          280  +  )(
          281  +    "Dotted list",
          282  +    "insertunorderedlist",
          283  +    ["data:image/gif;base64,R0lGODlhFgAWAMIGAAAAAB1ChF9vj1iE33mOrqezxv/\x2f/\x2f",
          284  +     "/\x2f/yH5BAEAAAcALAAAAAAWABYAAAMyeLrc/jDKSesppNhGRlBAKIZRERBbqm6YtnbfMY7lud6",
          285  +     "4UwiuKnigGQliQuWOyKQykgAAOw=="]
          286  +  )(
          287  +    "Quote",
          288  +    "formatblock",
          289  +    ["data:image/gif;base64,R0lGODlhFgAWAIQXAC1NqjFRjkBgmT9nqUJnsk9xrFJ7u2",
          290  +     "R9qmKBt1iGzHmOrm6Sz4OXw3Odz4Cl2ZSnw6KxyqO306K63bG70bTB0rDI3bvI4P",
          291  +     "/\x2f/\x2f/\x2f/\x2f/",
          292  +     "/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f",
          293  +     "/\x2f/\x2f/\x2fyH5BAEKAB8ALAAAAAAWABYAAAVP4CeOZGmeaKqubEs2Cekk",
          294  +     "ErvEI1zZuOgYFlakECEZFi0GgTGKEBATFmJAVXweVOoKEQgABB9IQDCmrLpjETrQQlhHjINrT",
          295  +     "q/b7/i8fp8PAQA7"]
          296  +  )(
          297  +    "Delete indentation",
          298  +    "outdent",
          299  +    ["data:image/gif;base64,R0lGODlhFgAWAMIHAAAAADljwliE35GjuaezxtDV3NHa7P",
          300  +     "/\x2f/yH5BAEAAAcALAAAAAAWABYAAAM2eLrc/jDKCQG9F2i7u8agQgyK1z2EIBil+TWqEMxhMcz",
          301  +     "sYVJ3e4ahk+sFnAgtxSQDqWw6n5cEADs="]
          302  +  )(
          303  +    "Add indentation",
          304  +    "indent",
          305  +    ["data:image/gif;base64,R0lGODlhFgAWAOMIAAAAADljwl9vj1iE35GjuaezxtDV3N",
          306  +     "Ha7P/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f",
          307  +     "/\x2f/\x2f/yH5BAEAAAgALAAAAAAWABYAAAQ7EMlJq704650",
          308  +     "B/x8gemMpgugwHJNZXodKsO5oqUOgo5KhBwWESyMQsCRDHu9VOyk5TM9zSpFSr9gsJwIAOw=="
          309  +    ]
          310  +  )(
          311  +    "Hyperlink",
          312  +    "createlink",
          313  +    ["data:image/gif;base64,R0lGODlhFgAWAOMKAB1ChDRLY19vj3mOrpGjuaezxrCztb",
          314  +     "/I19Ha7Pv8/f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f",
          315  +     "/yH5BAEKAA8ALAAAAAAWABYAAARY8MlJq704682",
          316  +     "7/2BYIQVhHg9pEgVGIklyDEUBy/RlE4FQF4dCj2AQXAiJQDCWQCAEBwIioEMQBgSAFhDAGghG",
          317  +     "i9XgHAhMNoSZgJkJei33UESv2+/4vD4TAQA7"]
          318  +  )(
          319  +    "Cut",
          320  +    "cut",
          321  +    ["data:image/gif;base64,R0lGODlhFgAWAIQSAB1ChBFNsRJTySJYwjljwkxwl19vj1",
          322  +     "dusYODhl6MnHmOrpqbmpGjuaezxrCztcDCxL/I18rL1P/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f",
          323  +     "/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/",
          324  +     "/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f",
          325  +     "yH5BAEAAB8ALAAAAAAWABYAAAVu4CeOZGmeaKqubDs6TNnE",
          326  +     "bGNApNG0kbGMi5trwcA9GArXh+FAfBAw5UexUDAQESkRsfhJPwaH4YsEGAAJGisRGAQY7UCC9",
          327  +     "ZAXBB+74LGCRxIEHwAHdWooDgGJcwpxDisQBQRjIgkDCVlfmZqbmiEAOw=="]
          328  +  )(
          329  +    "Copy",
          330  +    "copy",
          331  +    ["data:image/gif;base64,R0lGODlhFgAWAIQcAB1ChBFNsTRLYyJYwjljwl9vj1iE31",
          332  +     "iGzF6MnHWX9HOdz5GjuYCl2YKl8ZOt4qezxqK63aK/9KPD+7DI3b/I17LM/MrL1MLY9NHa7OP",
          333  +     "s++bx/Pv8/f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f",
          334  +     "/yH5BAEAAB8ALAAAAAAWABYAAAWG4CeOZGmeaKqubOum1SQ/",
          335  +     "kPVOW749BeVSus2CgrCxHptLBbOQxCSNCCaF1GUqwQbBd0JGJAyGJJiobE+LnCaDcXAaEoxhQ",
          336  +     "ACgNw0FQx9kP+wmaRgYFBQNeAoGihCAJQsCkJAKOhgXEw8BLQYciooHf5o7EA+kC40qBKkAAA",
          337  +     "Grpy+wsbKzIiEAOw=="]
          338  +  )(
          339  +    /* Paste, when activated via JS, has no effect in some (maybe all)
          340  +       environments. Activated externally, e.g. keyboard, it works. */
          341  +    "Paste (does not work in all environments)",
          342  +    "paste",
          343  +    ["data:image/gif;base64,R0lGODlhFgAWAIQUAD04KTRLY2tXQF9vj414WZWIbXmOrp",
          344  +     "qbmpGjudClFaezxsa0cb/I1+3YitHa7PrkIPHvbuPs+/fvrvv8/f/\x2f/\x2f/\x2f",
          345  +     "/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/",
          346  +     "/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f/\x2f",
          347  +     "yH5BAEAAB8ALAAAAAAWABYAAAWN4CeOZGmeaKqubGsusPvB",
          348  +     "SyFJjVDs6nJLB0khR4AkBCmfsCGBQAoCwjF5gwquVykSFbwZE+AwIBV0GhFog2EwIDchjwRiQ",
          349  +     "o9E2Fx4XD5R+B0DDAEnBXBhBhN2DgwDAQFjJYVhCQYRfgoIDGiQJAWTCQMRiwwMfgicnVcAAA",
          350  +     "MOaK+bLAOrtLUyt7i5uiUhADs="]
          351  +  );
          352  +
          353  +  ////////////////////////////////////////////////////////////////////////
          354  +  // The main editor area...
          355  +  const oDoc = D.attr(D.div(), 'id', "wysiwygBox");
          356  +  D.attr(oDoc, 'contenteditable', 'true');
          357  +  D.append(outerContainer, oDoc);
          358  +  
          359  +  /* Initialize the document editor */
          360  +  function initDoc() {
          361  +    initEventHandlers();
          362  +    if (!isWysiwyg()) { setDocMode(true); }
          363  +  }
          364  +
          365  +  function initEventHandlers() {
          366  +