Fossil

Check-in [b48212f6ea]
Login

Check-in [b48212f6ea]

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

Overview
Comment:Reworked how /fileedit loads its JS - it now fetches them rather than embedding them inline. Moved fossil.fetch() docs from style.c into the JS file.
Downloads: Tarball | ZIP archive
Timelines: family | ancestors | descendants | both | fileedit-ajaxify
Files: files | file ages | folders
SHA3-256: b48212f6ea83876b7d9721081dfdbbc8e6f4743fdad9c3f16551555b78edf155
User & Date: stephan 2020-05-06 01:51:37.519
Context
2020-05-06
01:58
Removed the FORM element - it was superfluous and particularly stubborn in how it responded to every button. ... (check-in: 7635d9345d user: stephan tags: fileedit-ajaxify)
01:51
Reworked how /fileedit loads its JS - it now fetches them rather than embedding them inline. Moved fossil.fetch() docs from style.c into the JS file. ... (check-in: b48212f6ea user: stephan tags: fileedit-ajaxify)
2020-05-05
23:54
Re-added the editor font size selector. Added a button to discard/reload edits, but it's too easy to activate by accident, so it's disabled until we have a common confirmation mechanism in place. Added timestamp to fossil.message() and fossil.error() output. ... (check-in: 25dfd243a1 user: stephan tags: fileedit-ajaxify)
Changes
Unified Diff Ignore Whitespace Patch
Changes to src/default_css.txt.
859
860
861
862
863
864
865





































































866
867
868
869
870
871
872
//   vertical-align: top;
// }
// #setup_skinedit_css_defaults > tbody > tr > td:nth-of-type(2) > div {
//   max-width: 30em;
//   overflow: auto;
// }
// .fileedit-XXX => /fileedit page





































































form.fileedit textarea {
  font-family: monospace;
  width: 100%;
}
form.fileedit fieldset {
  margin: 0.5em 0 0.5em 0;
  border-radius: 0.5em;







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







859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
//   vertical-align: top;
// }
// #setup_skinedit_css_defaults > tbody > tr > td:nth-of-type(2) > div {
//   max-width: 30em;
//   overflow: auto;
// }
// .fileedit-XXX => /fileedit page
.hidden {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
//.hidden {
//  display: none;
//}
#fossil-status-bar {
  display: block;
  font-family: monospace;
  border-width: 1px;
  border-style: inset;
  border-color: inherit;
  min-height: 1.5em;
  font-size: 1.2em;
  padding: 0.2em;
  margin: 0.25em 0;
  flex: 0 0 auto;
}
#fossil-status-bar.error {
  color: darkred;
  background: yellow;
}
//////////////////////////////////
// Styles for fossil.tabs.js:
.tab-container {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: stretch;
}
.tab-container > #fossil-status-bar {
  margin-top: 0;
}
.tab-container > .tabs {
  padding: 0.25em;
  margin: 0;
  display: flex;
  flex-direction: column;
  border-width: 1px;
  border-style: outset;
  border-color: inherit;
}
.tab-container > .tabs > .tab-panel {
  align-self: stretch;
  flex: 10 1 auto;
  display: block;
}
.tab-container > .tab-bar {
  display: flex;
  flex-direction: row;
  flex: 1 10 auto;
  align-self: stretch;
  flex-wrap: wrap;
}
.tab-container > .tab-bar > button {
  border-radius: 0.5em 0.5em 0 0;
  margin: 0.5em 0.5em 0 0.5em;
  align-self: baseline;
}
.tab-container > .tab-bar > button.selected {
  font-style: italic;
  font-weight: bold;
  margin: 0 0.5em;
  text-decoration: underline;
}
//////////////////////////////////
// Styles for /fileedit:
form.fileedit textarea {
  font-family: monospace;
  width: 100%;
}
form.fileedit fieldset {
  margin: 0.5em 0 0.5em 0;
  border-radius: 0.5em;
915
916
917
918
919
920
921
922
923
924
925
926
927



928




929
930











931



932


933


934


935


936
937












938
939
940
941
942
943
944
  margin: 0;
  padding: 0;
}
#fileedit-comment {
  width: 100%;
  font-family: monospace;
}
#fossil-status-bar {
  display: block;
  font-family: monospace;
  border-width: 1px;
  border-style: inset;
  border-color: inherit;



  min-height: 1.5em;




  font-size: 1.2em;
  padding: 0.2em;











  margin: 0.25em 0;



  flex: 0 0 auto;


}


#fossil-status-bar.error {


  color: darkred;


  background: yellow;
}












.input-with-label {
  border: 1px inset #808080;
  border-radius: 0.5em;
  padding: 0.25em 0.4em;
  margin: 0 0.5em;
  display: inline-block;
  cursor: pointer;







|
|
|
|
|
|
>
>
>
|
>
>
>
>
|
|
>
>
>
>
>
>
>
>
>
>
>
|
>
>
>
|
>
>

>
>
|
>
>
|
>
>
|

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







984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
  margin: 0;
  padding: 0;
}
#fileedit-comment {
  width: 100%;
  font-family: monospace;
}
.tab-container > .tabs > .tab-panel > .fileedit-options {
  margin-top: 0;
  border: none;
  border-radius: 0;
  border-bottom-width: 1px;
  border-bottom-style: dotted;
}
.tab-container > .tabs > .tab-panel > .fileedit-options > button {
  vertical-align: middle;
  margin: 0.5em;
}
////////////////////////////////////////////////////////////////////
// Styles developed for /fileedit but which have wider
// applicability:
.flex-container {
    display: flex;
}
.flex-container.row {
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
}
.flex-container.row.stretch {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: stretch;
    margin: 0;
}
.flex-container.column {
    flex-direction: column;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
}
.flex-container.column.stretch {
    align-items: stretch;
    margin: 0;
}
.font-size-100 {
  font-size: 100%;
}
.font-size-125 {
  font-size: 125%;
}
.font-size-150 {
  font-size: 150%;
}
.font-size-175 {
  font-size: 175%;
}
.font-size-200 {
  font-size: 200%;
}
//////////////////////////////////////////////////////////////////
// .input-with-label is intended to be a wrapper element which
// contains a SPAN label and an INPUT control.
.input-with-label {
  border: 1px inset #808080;
  border-radius: 0.5em;
  padding: 0.25em 0.4em;
  margin: 0 0.5em;
  display: inline-block;
  cursor: pointer;
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
.input-with-label > input[type=radio] {
    vertical-align: sub;
}
.input-with-label > span {
    margin: 0 0.25em 0 0.25em;
    vertical-align: middle;
}
.hidden {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}
//.hidden {
//  display: none;
//}
.font-size-100 {
  font-size: 100%;
}
.font-size-125 {
  font-size: 125%;
}
.font-size-150 {
  font-size: 150%;
}
.font-size-175 {
  font-size: 175%;
}
.font-size-200 {
  font-size: 200%;
}

.tab-container {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: stretch;
}
.tab-container > #fossil-status-bar {
  margin-top: 0;
}
.tab-container > .tabs {
  padding: 0.25em;
  margin: 0;
  display: flex;
  flex-direction: column;
  border-width: 1px;
  border-style: outset;
  border-color: inherit;
}
.tab-container > .tabs > .tab-panel {
  align-self: stretch;
  flex: 10 1 auto;
  display: block;
}
.tab-container > .tab-bar {
  display: flex;
  flex-direction: row;
  flex: 1 10 auto;
  align-self: stretch;
  flex-wrap: wrap;
}
.tab-container > .tab-bar > button {
  border-radius: 0.5em 0.5em 0 0;
  margin: 0.5em 0.5em 0 0.5em;
  align-self: baseline;
}
.tab-container > .tab-bar > button.selected {
  font-style: italic;
  font-weight: bold;
  margin: 0 0.5em;
  text-decoration: underline;
}
.tab-container > .tabs > .tab-panel > .fileedit-options {
  margin-top: 0;
  border: none;
  border-radius: 0;
  border-bottom-width: 1px;
  border-bottom-style: dotted;
}
.tab-container > .tabs > .tab-panel > .fileedit-options > button {
  vertical-align: middle;
  margin: 0.5em;
}
.flex-container {
    display: flex;
}
.flex-container.row {
    flex-direction: row;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
}
.flex-container.row.stretch {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: stretch;
    margin: 0;
}
.flex-container.column {
    flex-direction: column;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
}
.flex-container.column.stretch {
    align-items: stretch;
    margin: 0;
}







<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
<
1074
1075
1076
1077
1078
1079
1080





































































































.input-with-label > input[type=radio] {
    vertical-align: sub;
}
.input-with-label > span {
    margin: 0 0.25em 0 0.25em;
    vertical-align: middle;
}





































































































Changes to src/fileedit.c.
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
end_cleanup:
  fossil_free(zNewUuid);
  blob_reset(&err);
  blob_reset(&manifest);
  CheckinMiniInfo_cleanup(&cimi);
}


/*
** Emits utility script code specific to the /fileedit page.
*/
static void fileedit_emit_page_script(){
  style_emit_script_fetch();
  style_emit_script_tabs();
  style_emit_script_builtin("fossil.page.fileedit.js");
}

/*
** WEBPAGE: fileedit
**
** EXPERIMENTAL and subject to change and removal at any time. The goal
** is to allow online edits of files.
**
** Query parameters:







<
<
<
<
<
<
<
<
<
<







1450
1451
1452
1453
1454
1455
1456










1457
1458
1459
1460
1461
1462
1463
end_cleanup:
  fossil_free(zNewUuid);
  blob_reset(&err);
  blob_reset(&manifest);
  CheckinMiniInfo_cleanup(&cimi);
}











/*
** WEBPAGE: fileedit
**
** EXPERIMENTAL and subject to change and removal at any time. The goal
** is to allow online edits of files.
**
** Query parameters:
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
                                           because that param is handled
                                           specially by the core. */
  const char * zRev;                    /* checkin version */
  const char * zFileMime = 0;           /* File mime type guess */
  CheckinMiniInfo cimi;                 /* Checkin state */
  int previewHtmlHeight = 0;            /* iframe height (EMs) */
  int previewRenderMode = FE_RENDER_GUESS; /* preview mode */
  char * zFileUuid = 0;                 /* File content UUID */
  Blob err = empty_blob;                /* Error report */
  Blob endScript = empty_blob;          /* Script code to run at the
                                           end. This content will be
                                           combined into a single JS
                                           function call, thus each
                                           entry must end with a
                                           semicolon. */







<







1475
1476
1477
1478
1479
1480
1481

1482
1483
1484
1485
1486
1487
1488
                                           because that param is handled
                                           specially by the core. */
  const char * zRev;                    /* checkin version */
  const char * zFileMime = 0;           /* File mime type guess */
  CheckinMiniInfo cimi;                 /* Checkin state */
  int previewHtmlHeight = 0;            /* iframe height (EMs) */
  int previewRenderMode = FE_RENDER_GUESS; /* preview mode */

  Blob err = empty_blob;                /* Error report */
  Blob endScript = empty_blob;          /* Script code to run at the
                                           end. This content will be
                                           combined into a single JS
                                           function call, thus each
                                           entry must end with a
                                           semicolon. */
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802

1803


1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
  
  /* Dynamically populate the editor... */
  blob_appendf(&endScript,
               "fossil.page.loadFile('%j','%j');",
               zFilename, cimi.zParentUuid);

end_footer:
  fossil_free(zFileUuid);
  if(stmt.pStmt){
    db_finalize(&stmt);
  }
  if(blob_size(&err)){
    CX("<div class='fileedit-error-report'>%s</div>",
       blob_str(&err));
  }
  blob_reset(&err);
  CheckinMiniInfo_cleanup(&cimi);

  fileedit_emit_page_script();


  if(blob_size(&endScript)>0){
    style_emit_script_tag(0);
    CX("(function(){\n");
    CX("try{\n%b\n}catch(e){console.error('Exception:',e)}\n",
       &endScript);
    CX("})();");
    style_emit_script_tag(1);
  }
  db_end_transaction(0);
  style_footer();
}







<









>
|
>
>

|




|




1775
1776
1777
1778
1779
1780
1781

1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
  
  /* Dynamically populate the editor... */
  blob_appendf(&endScript,
               "fossil.page.loadFile('%j','%j');",
               zFilename, cimi.zParentUuid);

end_footer:

  if(stmt.pStmt){
    db_finalize(&stmt);
  }
  if(blob_size(&err)){
    CX("<div class='fileedit-error-report'>%s</div>",
       blob_str(&err));
  }
  blob_reset(&err);
  CheckinMiniInfo_cleanup(&cimi);
  style_emit_script_fossil_bootstrap(0);
  style_emit_script_fetch(0);
  style_emit_script_tabs(0);
  style_emit_script_builtin("fossil.page.fileedit.js",0);
  if(blob_size(&endScript)>0){
    style_emit_script_tag(0,0);
    CX("(function(){\n");
    CX("try{\n%b\n}catch(e){console.error('Exception:',e)}\n",
       &endScript);
    CX("})();");
    style_emit_script_tag(1,0);
  }
  db_end_transaction(0);
  style_footer();
}
Changes to src/fossil.bootstrap.js.
1
2
3
4
5
6
7




8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
"use strict";
(function(global){
  /* Bootstrapping bits for the global.fossil object. Must be
     loaded after style.c:style_emit_script_tag() has initialized
     that object.
  */





  const timestring = function f(){
    if(!f.rx1){
      f.rx1 = /\.\d+Z$/;
    }
    const d = new Date();
    return d.toISOString().replace(f.rx1,'').split('T').join(' ');
  };


  /*
  ** By default fossil.message() sends its arguments console.debug(). If
  ** fossil.message.targetElement is set, it is assumed to be a DOM
  ** element, its innerText gets assigned to the concatenation of all
  ** arguments (with a space between each), and the CSS 'error' class is
  ** removed from the object. Pass it a falsy value to clear the target
  ** element.
  **
  ** Returns this object.
  */
  global.fossil.message = function f(msg){
    const args = Array.prototype.slice.call(arguments,0);
    const tgt = f.targetElement;
    args.unshift(timestring()+' UTC:');
    if(tgt){
      tgt.classList.remove('error');
      tgt.innerText = args.join(' ');
    }
    else{
      args.unshift('Fossil status:');
      console.debug.apply(console,args);







>
>
>
>







<














|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
"use strict";
(function(global){
  /* Bootstrapping bits for the global.fossil object. Must be
     loaded after style.c:style_emit_script_tag() has initialized
     that object.
  */

  /**
     Returns the current time in something approximating
     ISO-8601 format.
  */
  const timestring = function f(){
    if(!f.rx1){
      f.rx1 = /\.\d+Z$/;
    }
    const d = new Date();
    return d.toISOString().replace(f.rx1,'').split('T').join(' ');
  };


  /*
  ** By default fossil.message() sends its arguments console.debug(). If
  ** fossil.message.targetElement is set, it is assumed to be a DOM
  ** element, its innerText gets assigned to the concatenation of all
  ** arguments (with a space between each), and the CSS 'error' class is
  ** removed from the object. Pass it a falsy value to clear the target
  ** element.
  **
  ** Returns this object.
  */
  global.fossil.message = function f(msg){
    const args = Array.prototype.slice.call(arguments,0);
    const tgt = f.targetElement;
    args.unshift(timestring(),'UTC:');
    if(tgt){
      tgt.classList.remove('error');
      tgt.innerText = args.join(' ');
    }
    else{
      args.unshift('Fossil status:');
      console.debug.apply(console,args);
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
  ** that element and sets its content as defined for message().
  **
  ** Returns this object.
  */
  global.fossil.error = function f(msg){
    const args = Array.prototype.slice.call(arguments,0);
    const tgt = global.fossil.message.targetElement;
    args.unshift(timestring()+' UTC:');
    if(tgt){
      tgt.classList.add('error');
      tgt.innerText = args.join(' ');
    }
    else{
      args.unshift('Fossil error:');
      console.error.apply(console,args);







|







53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
  ** that element and sets its content as defined for message().
  **
  ** Returns this object.
  */
  global.fossil.error = function f(msg){
    const args = Array.prototype.slice.call(arguments,0);
    const tgt = global.fossil.message.targetElement;
    args.unshift(timestring(),'UTC:');
    if(tgt){
      tgt.classList.add('error');
      tgt.innerText = args.join(' ');
    }
    else{
      args.unshift('Fossil error:');
      console.error.apply(console,args);
Changes to src/fossil.fetch.js.
1
2

3



4

5




















































6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
"use strict";
/**

  Documented in style.c:style_emit_script_fetch(). Requires that



  window.fossil has already been set up (which happens via the routine

  which emits this code C-side).




















































*/
window.fossil.fetch = function f(uri,opt){
  if(!f.onerror){
    f.onerror = function(e/*event or exception*/){
      console.error("Ajax error:",e);
      if(e instanceof Error){
        fossil.error('Exception:',e);
      }
      else if(e.originalTarget && e.originalTarget.responseType==='text'){
        const txt = e.originalTarget.responseText;
        try{
          /* The convention from the /filepage_xyz routes is to
          ** return error responses in JSON form if possible:
          ** {error: "..."}
          */
          const j = JSON.parse(txt);
          console.error("Error JSON:",j);
          if(j.error){ fossil.error(j.error) };
        }catch(e){/* Try harder */
          fossil.error(txt)
        }


>
|
>
>
>
|
>
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>












|
|







1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
"use strict";
/**
   Requires that window.fossil has already been set up.

   window.fossil.fetch() is an HTTP request/response mini-framework
   similar (but not identical) to the not-quite-ubiquitous
   window.fetch().

   JS usages:

   fossil.fetch( URI [, onLoadCallback] );

   fossil.fetch( URI [, optionsObject = {}] );

   Noting that URI must be relative to the top of the repository and
   should not start with a slash (if it does, it is stripped). It gets
   the equivalent of "%R/" prepended to it.

   The optionsObject may be an onload callback or an object with any
   of these properties:

   - onload: callback(responseData) (default = output response to
   the console).

   - onerror: callback(XHR onload event | exception)
   (default = event or exception to the console).

   - method: 'POST' | 'GET' (default = 'GET')

   - payload: anything acceptable by XHR2.send(ARG) (DOMString,
   Document, FormData, Blob, File, ArrayBuffer), or a plain object or
   array, either of which gets JSON.stringify()'d. If payload is set
   then the method is automatically set to 'POST'. If an object/array
   is converted to JSON, the contentType option is automatically set
   to 'application/json'. By default XHR2 will set the content type
   based on the payload type.

   - contentType: Optional request content type when POSTing. Ignored
   if the method is not 'POST'.

   - responseType: optional string. One of ("text", "arraybuffer",
   "blob", or "document") (as specified by XHR2). Default = "text".
   As an extension, it supports "json", which tells it that the
   response is expected to be text and that it should be JSON.parse()d
   before passing it on to the onload() callback.

   - urlParams: string|object. If a string, it is assumed to be a
   URI-encoded list of params in the form "key1=val1&key2=val2...",
   with NO leading '?'.  If it is an object, all of its properties get
   converted to that form. Either way, the parameters get appended to
   the URL before submitting the request.

   When an options object does not provide onload() or onerror()
   handlers of its own, this function falls back to
   fossil.fetch.onload() and fossil.fetch.onerror() as defaults. The
   default implementations route the data through the dev console and
   (for onerror()) through fossil.error(). Individual pages may
   overwrite those members to provide default implementations suitable
   for the page's use.

   Returns this object, noting that the XHR request is asynchronous,
   and still in transit (or has yet to be sent) when that happens.
*/
window.fossil.fetch = function f(uri,opt){
  if(!f.onerror){
    f.onerror = function(e/*event or exception*/){
      console.error("Ajax error:",e);
      if(e instanceof Error){
        fossil.error('Exception:',e);
      }
      else if(e.originalTarget && e.originalTarget.responseType==='text'){
        const txt = e.originalTarget.responseText;
        try{
          /* The convention from the /filepage_xyz routes is to
             return error responses in JSON form if possible:
             {error: "..."}
          */
          const j = JSON.parse(txt);
          console.error("Error JSON:",j);
          if(j.error){ fossil.error(j.error) };
        }catch(e){/* Try harder */
          fossil.error(txt)
        }
Changes to src/style.c.
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434








1435
1436
1437
1438
1439


1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454

1455

1456
1457





1458
1459
1460
1461
1462
1463


1464
1465



1466


1467



1468

1469
1470
1471
1472



1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484


1485
1486
1487

1488
1489



1490


1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566








1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578




1579
1580
1581
1582
1583
1584
1585
1586
  }
  va_end(vargs);
}

/*
** The first time this is called, it emits code to install and
** bootstrap the window.fossil object, using the built-in file
** fossil.bootstrap.js (not to be confused with bootstrap.js). It does
** NOT wrap that in a script tag because it's called from
** style_emit_script_tag().
**
** Subsequent calls are no-ops.








*/
static void style_emit_script_fossil_bootstrap(){
  static int once = 0;
  if(0==once++){
    /* Set up the generic/app-agnostic parts of window.fossil */


    CX("(function(){\n"
       "if(!window.fossil) window.fossil={};\n"
       "window.fossil.version = \"%j\";\n"
    /* fossil.rootPath is the top-most CGI/server path,
       including a trailing slash. */
       "window.fossil.rootPath = \"%j\"+'/';\n",
       get_version(), g.zTop);
    /*
    ** fossil.page holds info about the current page. This is
    ** also where the current page "should" store any of its
    ** own page-specific state.
    */
    CX("window.fossil.page = {"
       "page:\"%T\""
       "};\n", g.zPath);

    /* The remaining code is not dependent on C-runtime state... */

    CX("%s\n", builtin_text("fossil.bootstrap.js"));
    CX("})();\n");





  }
}

/*
** If passed 0, it emits a script opener tag with this request's
** nonce. If passed non-0 it emits a script closing tag.


**
** The very first time it is called, it emits some bootstrapping JS



** code immediately after the script opener. Specifically, it defines


** window.fossil if it's not already defined, and sets up its most



** basic functionality.

*/
void style_emit_script_tag(int phase){
  static int once = 0;
  if(0==phase){



    CX("<script nonce='%s'>", style_nonce());
    if(0==once++){
      style_emit_script_fossil_bootstrap();
    }
  }else{
    CX("</script>\n");
  }
}

/*
** Emits the text of builtin_text(zName), which is assumed to be
** JavaScript code, and wrapps that in a pair of calls to


** style_emit_script_tag().
*/
void style_emit_script_builtin(char const * zName){

    style_emit_script_tag(0);
    CX("%s", builtin_text(zName));



    style_emit_script_tag(1);


}

/*
** The first time this is called, it emits a JS script block,
** including tags, using the contents of the built-in file
** fossil.fetch.js, which defines window.fossil.fetch(), an HTTP
** request/response mini-framework similar (but not identical) to the
** not-quite-ubiquitous window.fetch(). It calls
** style_emit_script_tag(), which may inject other JS bootstrapping
** bits. Subsequent calls are no-ops.
**
** JS usages:
**
** fossil.fetch( URI [, onLoadCallback] );
**
** fossil.fetch( URI [, optionsObject = {}] );
**
** Noting that URI must be relative to the top of the repository and
** should not start with a slash (if it does, it is stripped). It gets
** the equivalent of "%R/" prepended to it.
**
** The optionsObject may be an onload callback or an object with any
** of these properties:
**
** - onload: callback(responseData) (default = output response to
**   the console).
**
** - onerror: callback(XHR onload event | exception)
**   (default = event or exception to the console).
**
** - method: 'POST' | 'GET' (default = 'GET')
**
** - payload: anything acceptable by XHR2.send(ARG) (DOMString,
**   Document, FormData, Blob, File, ArrayBuffer), or a plain object
**   or array, either of which gets JSON.stringify()'d. If payload is
**   set then the method is automatically set to 'POST'. If an
**   object/array is converted to JSON, the contentType option is
**   automatically set to 'application/json'. By default XHR2 will set
**   the content type based on the payload type.
**
** - contentType: Optional request content type when POSTing. Ignored
**   if the method is not 'POST'.
**
** - responseType: optional string. One of ("text", "arraybuffer",
**   "blob", or "document") (as specified by XHR2). Default = "text".
**   As an extension, it supports "json", which tells it that the
**   response is expected to be text and that it should be
**   JSON.parse()d before passing it on to the onload() callback.
**
** - urlParams: string|object. If a string, it is assumed to be a
**   URI-encoded list of params in the form "key1=val1&key2=val2...",
**   with NO leading '?'.  If it is an object, all of its properties
**   get converted to that form. Either way, the parameters get
**   appended to the URL before submitting the request.
**
** When an options object does not provide onload() or onerror()
** handlers of its own, this function falls back to
** fossil.fetch.onload() and fossil.fetch.onerror() as defaults. The
** default implementations route the data through the dev console and
** (for onerror()) through fossil.error(). Individual pages may
** overwrite those members to provide default implementations suitable
** for the page's use.
**
** Returns this object, noting that the XHR request is asynchronous,
** and still in transit (or has yet to be sent) when that happens.
*/
void style_emit_script_fetch(){
  static int once = 0;
  if(0==once++){
    style_emit_script_builtin("fossil.fetch.js");
  }
}

/*
** The first time this is called, it emits the JS code from the
** built-in file fossil.dom.js. Subsequent calls are no-ops.








*/
void style_emit_script_dom(){
  static int once = 0;
  if(0==once++){
    style_emit_script_builtin("fossil.dom.js");
  }
}

/*
** The first time this is called, it calls style_emit_script_dom() and
** emits the JS code from the built-in file fossil.tabs.js.
** Subsequent calls are no-ops.




*/
void style_emit_script_tabs(){
  static int once = 0;
  if(0==once++){
    style_emit_script_dom();
    style_emit_script_builtin("fossil.tabs.js");
  }
}







|
<
<


>
>
>
>
>
>
>
>

|


|
>
>















>

>
|
<
>
>
>
>
>




|
|
>
>

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

|
<
|
>
>
>
|
<
<







|
|
>
>
|

|
>
|

>
>
>
|
>
>



|
<
<
<
<
<
|

<
<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
|
|
<

<
<
<
<
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
<
<
<
<
|
<
<
<
<
<
<
|
<
<

|


|






>
>
>
>
>
>
>
>

|


|




|
|
|
>
>
>
>

|


|
|


1423
1424
1425
1426
1427
1428
1429
1430


1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466

1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480

1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494

1495
1496
1497
1498
1499


1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526





1527
1528














1529


1530
1531

1532













1533









1534






1535


1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
  }
  va_end(vargs);
}

/*
** The first time this is called, it emits code to install and
** bootstrap the window.fossil object, using the built-in file
** fossil.bootstrap.js (not to be confused with bootstrap.js).


**
** Subsequent calls are no-ops.
**
** If passed a true value, it emits the contents directly to the page
** output, else it emits a script tag with a src=builtin/... to load
** the script. It always outputs a small pre-bootstrap element in its
** own script tag to initialize parts which need C-runtime-level
** information, because loading the main fossil.bootstrap.js either
** inline or via a <script src=...>, as specified by the first
** argument.
*/
void style_emit_script_fossil_bootstrap(int asInline){
  static int once = 0;
  if(0==once++){
    /* Set up the generic/app-agnostic parts of window.fossil
    ** which require C-level state... */
    style_emit_script_tag(0,0);
    CX("(function(){\n"
       "if(!window.fossil) window.fossil={};\n"
       "window.fossil.version = \"%j\";\n"
    /* fossil.rootPath is the top-most CGI/server path,
       including a trailing slash. */
       "window.fossil.rootPath = \"%j\"+'/';\n",
       get_version(), g.zTop);
    /*
    ** fossil.page holds info about the current page. This is
    ** also where the current page "should" store any of its
    ** own page-specific state.
    */
    CX("window.fossil.page = {"
       "page:\"%T\""
       "};\n", g.zPath);
    CX("})();\n");
    /* The remaining code is not dependent on C-runtime state... */
    if(asInline){
      CX("%s\n", builtin_text("fossil.bootstrap.js"));

    }
    style_emit_script_tag(1,0);
    if(asInline==0){
      style_emit_script_tag(0,"builtin/fossil.bootstrap.js");
    }
  }
}

/*
** If passed 0 as its first argument, it emits a script opener tag
** with this request's nonce. If passed non-0 it emits a script
** closing tag. Mnemonic for remembering the order in which to pass 0
** or 1 as the first argument to this function: 0 comes before 1.
**

** If passed 0 as its first argument and a non-NULL/non-empty zSrc,
** then it instead emits:
**
** <script src='%R/{{zSrc}}'></script>
**
** zSrc is always assumed to be a repository-relative path without
** a leading slash, and has %R/ prepended to it.
**
** Meaning that no follow-up call to pass a non-0 first argument
** to close the tag. zSrc is ignored if the first argument is not
** 0.
**
*/
void style_emit_script_tag(int isCloser, const char * zSrc){

  if(0==isCloser){
    if(zSrc!=0 && zSrc[0]!=0){
      CX("<script src='%R/%T'></script>\n", zSrc);
    }else{
      CX("<script nonce='%s'>", style_nonce());


    }
  }else{
    CX("</script>\n");
  }
}

/*
** Emits a script tag which uses content from a builtin script file.
**
** If asInline is true, it is emitted directly as an opening tag, the
** content of the zName builtin file, and a closing tag. If it is false,
** a script tag loading it via src=builtin/{{zName}} is emitted.
*/
void style_emit_script_builtin(char const * zName, int asInline){
  if(asInline){
    style_emit_script_tag(0,0);
    CX("%s", builtin_text(zName));
    style_emit_script_tag(1,0);
  }else{
    char * zFull = mprintf("builtin/%s",zName);
    style_emit_script_tag(0,zFull);
    fossil_free(zFull);
  }
}

/*
** The first time this is called, it emits the JS code from the





** built-in file fossil.fossil.js. Subsequent calls are no-ops.
**














** If passed a true value, it emits the contents directly


** to the page output, else it emits a script tag with a
** src=builtin/... to load the script.

**













** Note that this code relies on that loaded via









** style_emit_script_fossil_bootstrap() but it does not call that






** routine.


*/
void style_emit_script_fetch(int asInline){
  static int once = 0;
  if(0==once++){
    style_emit_script_builtin("fossil.fetch.js", asInline);
  }
}

/*
** The first time this is called, it emits the JS code from the
** built-in file fossil.dom.js. Subsequent calls are no-ops.
**
** If passed a true value, it emits the contents directly
** to the page output, else it emits a script tag with a
** src=builtin/... to load the script.
**
** Note that this code relies on that loaded via
** style_emit_script_fossil_bootstrap(), but it does not call that
** routine.
*/
void style_emit_script_dom(int asInline){
  static int once = 0;
  if(0==once++){
    style_emit_script_builtin("fossil.dom.js", asInline);
  }
}

/*
** The first time this is called, it calls style_emit_script_dom(),
** passing it the given asInline value, and emits the JS code from the
** built-in file fossil.tabs.js. Subsequent calls are no-ops.
**
** If passed a true value, it emits the contents directly
** to the page output, else it emits a script tag with a
** src=builtin/... to load the script.
*/
void style_emit_script_tabs(int asInline){
  static int once = 0;
  if(0==once++){
    style_emit_script_dom(asInline);
    style_emit_script_builtin("fossil.tabs.js",asInline);
  }
}