Fossil SCM

Initial work on ajaxifying /fileedit. Fetching content, preview, and diffs are ajax'd, but save is not yet.

stephan 2020-05-05 04:06 checkin-without-checkout
Commit 8edf9dbfc2858f106d5b08d0fb51c38d5bb6915fe1ebb68571d396063c4a80fb
--- src/default_css.txt
+++ src/default_css.txt
@@ -918,11 +918,24 @@
918918
padding: 0;
919919
}
920920
.fileedit-diff > div:first-child {
921921
border-bottom: 1px dashed;
922922
}
923
-
923
+#fossil-status-bar {
924
+ display: block;
925
+ font-family: monospace;
926
+ border-width: 1px;
927
+ border-style: inset;
928
+ border-color: inherit;
929
+ min-height: 1.5em;
930
+ font-size: 1.2em;
931
+ padding: 0.2em;
932
+}
933
+#fossil-status-bar.error {
934
+ color: darkred;
935
+ background: yellow;
936
+}
924937
.input-with-label {
925938
border: 1px inset #808080;
926939
border-radius: 0.5em;
927940
padding: 0.25em 0.4em;
928941
margin: 0 0.5em;
929942
--- src/default_css.txt
+++ src/default_css.txt
@@ -918,11 +918,24 @@
918 padding: 0;
919 }
920 .fileedit-diff > div:first-child {
921 border-bottom: 1px dashed;
922 }
923
 
 
 
 
 
 
 
 
 
 
 
 
 
924 .input-with-label {
925 border: 1px inset #808080;
926 border-radius: 0.5em;
927 padding: 0.25em 0.4em;
928 margin: 0 0.5em;
929
--- src/default_css.txt
+++ src/default_css.txt
@@ -918,11 +918,24 @@
918 padding: 0;
919 }
920 .fileedit-diff > div:first-child {
921 border-bottom: 1px dashed;
922 }
923 #fossil-status-bar {
924 display: block;
925 font-family: monospace;
926 border-width: 1px;
927 border-style: inset;
928 border-color: inherit;
929 min-height: 1.5em;
930 font-size: 1.2em;
931 padding: 0.2em;
932 }
933 #fossil-status-bar.error {
934 color: darkred;
935 background: yellow;
936 }
937 .input-with-label {
938 border: 1px inset #808080;
939 border-radius: 0.5em;
940 padding: 0.25em 0.4em;
941 margin: 0 0.5em;
942
+267 -227
--- src/fileedit.c
+++ src/fileedit.c
@@ -898,118 +898,16 @@
898898
fossil_free(zGlobs);
899899
}
900900
return glob_match(pGlobs, zFilename);
901901
}
902902
903
-/*
904
-** Emits a script tag which defines window.fossil.fetch(), which works
905
-** similarly (not identically) to the not-quite-ubiquitous global
906
-** fetch().
907
-**
908
-** JS usages:
909
-**
910
-** fossilFetch( URI, onLoadCallback );
911
-**
912
-** fossilFetch( URI, optionsObject );
913
-**
914
-** Noting that URI must be relative to the top of the repository and
915
-** must not start with a slash (if it does, it is stripped). It gets
916
-** %R/ prepended to it.
917
-**
918
-** The optionsObject may be an onload callback or an object with any
919
-** of these properties:
920
-**
921
-** - onload: callback(responseData) (default = output response to
922
-** console).
923
-**
924
-** - onerror: callback(XHR onload event) (default = console output)
925
-**
926
-** - method: 'POST' | 'GET' (default = 'GET')
927
-**
928
-** - payload: anything acceptable by XHR2.send(ARG) (DOMString,
929
-** Document, FormData, Blob, File, ArrayBuffer), or a plain object
930
-** or array, either of which gets JSON.stringify()'d. If set then
931
-** the method is automatically set to 'POST'. If an object/array is
932
-** converted to JSON, the content-type is set to 'application/json'.
933
-** By default XHR2 will set the content type based on the payload
934
-** type.
935
-**
936
-** - contentType: Optional request content type when POSTing. Ignored
937
-** if the method is not 'POST'.
938
-**
939
-** - responseType: optional string. One of ("text", "arraybuffer",
940
-** "blob", or "document") (as specified by XHR2). Default = "text".
941
-**
942
-** - urlParams: string|object. If a string, it is assumed to be a
943
-** URI-encoded list of params in the form "key1=val1&key2=val2...",
944
-** with NO leading '?'. If it is an object, all of its properties
945
-** get converted to that form. Either way, the parameters get
946
-** appended to the URL.
947
-**
948
-*/
949
-void fileedit_emit_script_fetch(){
950
- style_emit_script_tag(0);
951
- CX("fossil.fetch = function(path,opt){\n");
952
- CX(" if('/'===path[0]) path = path.substr(1);\n");
953
- CX(" if(!opt){\n");
954
- CX(" opt = {onload:(r)=>console.debug('response:',r)};\n");
955
- CX(" }else if('function'===typeof opt){\n");
956
- CX(" opt={onload:opt,\n");
957
- CX(" onerror:(e)=>console.error('ajax error:',e)};\n");
958
- CX(" }\n");
959
- CX(" let payload = opt.payload;\n");
960
- CX(" if(payload){\n");
961
- CX(" opt.method = 'POST';\n");
962
- CX(" if(!(payload instanceof FormData)\n");
963
- CX(" && !(payload instanceof Document)\n");
964
- CX(" && !(payload instanceof Blob)\n");
965
- CX(" && !(payload instanceof File)\n");
966
- CX(" && !(payload instanceof ArrayBuffer)){\n");
967
- CX(" if('object'===typeof payload || payload instanceof Array){\n");
968
- CX(" payload = JSON.stringify(payload);\n");
969
- CX(" opt.contentType = 'application/json';\n");
970
- CX(" }\n");
971
- CX(" }\n");
972
- CX(" }\n");
973
- CX(" const url=['%R/'+path], x=new XMLHttpRequest();\n");
974
- CX(" if(opt.urlParams){\n");
975
- CX(" url.push('?');\n");
976
- CX(" if('string'===typeof opt.urlParams){\n");
977
- CX(" url.push(opt.urlParams);\n");
978
- CX(" }else{/*assume object*/\n");
979
- CX(" let k, i = 0;\n");
980
- CX(" for( k in opt.urlParams ){\n");
981
- CX(" if(i++) url.push('&');\n");
982
- CX(" url.push(k,'=',encodeURIComponent(opt.urlParams[k]));\n");
983
- CX(" }\n");
984
- CX(" }\n");
985
- CX(" }\n");
986
- CX(" if('POST'===opt.method && 'string'===typeof opt.contentType){\n");
987
- CX(" x.setRequestHeader('Content-Type',opt.contentType);\n");
988
- CX(" }\n");
989
- CX(" x.open(opt.method||'GET', url.join(''), true);\n");
990
- CX(" x.responseType=opt.responseType||'text';\n");
991
- CX(" if(opt.onload){\n");
992
- CX(" x.onload = function(e){\n");
993
- CX(" if(200!==this.status){\n");
994
- CX(" if(opt.onerror) opt.onerror(e);\n");
995
- CX(" return;\n");
996
- CX(" }\n");
997
- CX(" opt.onload(this.response);\n");
998
- CX(" }\n");
999
- CX(" }\n");
1000
- CX(" if(payload) x.send(payload);\n");
1001
- CX(" else x.send();\n");
1002
- CX("};\n");
1003
- style_emit_script_tag(1);
1004
-};
1005
-
1006903
1007904
enum fileedit_render_preview_flags {
1008905
FE_PREVIEW_LINE_NUMBERS = 1
1009906
};
1010907
enum fileedit_render_modes {
908
+/* GUESS must be 0. All others have specified values. */
1011909
FE_RENDER_GUESS = 0,
1012910
FE_RENDER_PLAIN_TEXT,
1013911
FE_RENDER_HTML,
1014912
FE_RENDER_WIKI
1015913
};
@@ -1037,12 +935,10 @@
1037935
const char * zMime;
1038936
zMime = mimetype_from_name(zFilename);
1039937
if(FE_RENDER_GUESS==renderMode){
1040938
renderMode = fileedit_render_mode_for_mimetype(zMime);
1041939
}
1042
- CX("<div class='fileedit-preview'>");
1043
- CX("<div>Preview</div>");
1044940
switch(renderMode){
1045941
case FE_RENDER_HTML:{
1046942
char * z64 = encode64(blob_str(pContent), blob_size(pContent));
1047943
CX("<iframe width='100%%' frameborder='0' "
1048944
"marginwidth='0' style='height:%dem' "
@@ -1068,11 +964,10 @@
1068964
CX("<pre>%h</pre>", zExt+1, zContent);
1069965
}
1070966
break;
1071967
}
1072968
}
1073
- CX("</div><!--.fileedit-preview-->\n");
1074969
}
1075970
1076971
/*
1077972
** Renders diffs for the /fileedit page. pContent is the
1078973
** locally-edited content. frid is the RID of the file's blob entry
@@ -1087,19 +982,15 @@
1087982
Blob out = empty_blob;
1088983
u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG | DIFF_STRIP_EOLCR
1089984
| (isSbs ? DIFF_SIDEBYSIDE : DIFF_LINENO);
1090985
content_get(frid, &orig);
1091986
text_diff(&orig, pContent, &out, 0, diffFlags);
1092
- CX("<div class='fileedit-diff'>");
1093
- CX("<div>Diff <code>[%S]</code> &rarr; Local Edits</div>",
1094
- zManifestUuid);
1095987
if(isSbs){
1096988
CX("%b",&out);
1097989
}else{
1098990
CX("<pre class='udiff'>%b</pre>",&out);
1099991
}
1100
- CX("</div><!--.fileedit-diff-->\n");
1101992
blob_reset(&orig);
1102993
blob_reset(&out);
1103994
}
1104995
1105996
/*
@@ -1122,10 +1013,226 @@
11221013
}
11231014
}
11241015
db_finalize(&stmt);
11251016
return zFileUuid;
11261017
}
1018
+
1019
+/*
1020
+** Helper for /filepage_xyz routes. Clears the CGI content buffer,
1021
+** sets an error status code, and queues up a JSON response in the
1022
+** form of an object:
1023
+**
1024
+** {error: formatted message}
1025
+**
1026
+** After calling this, the caller should immediately return.
1027
+ */
1028
+static void fileedit_ajax_error(int httpCode, const char * zFmt, ...){
1029
+ Blob msg = empty_blob;
1030
+ Blob content = empty_blob;
1031
+ va_list vargs;
1032
+ va_start(vargs,zFmt);
1033
+ blob_vappendf(&msg, zFmt, vargs);
1034
+ va_end(vargs);
1035
+ blob_appendf(&content,"{\"error\":\"%j\"}", blob_str(&msg));
1036
+ blob_reset(&msg);
1037
+ cgi_set_content(&content);
1038
+ cgi_set_status(httpCode, "Error");
1039
+ cgi_set_content_type("application/json");
1040
+}
1041
+
1042
+/*
1043
+** Performs bootstrapping common to the /fileedit_xyz AJAX routes.
1044
+** Returns 0 if bootstrapping fails (wrong permissions), in which
1045
+** case it has reported the error and the route should immediately
1046
+** return. Returns true on success.
1047
+*/
1048
+static int fileedit_ajax_boostrap(){
1049
+ login_check_credentials();
1050
+ if( !g.perm.Write ){
1051
+ fileedit_ajax_error(403,"Write permissions required.");
1052
+ return 0;
1053
+ }
1054
+ return 1;
1055
+}
1056
+/*
1057
+** Returns true if the current user is allowed to edit the given
1058
+** filename, as determined by fileedit_is_editable(), else false,
1059
+** in which case it queues up an error response and the caller
1060
+** must return immediately.
1061
+*/
1062
+static int fileedit_ajax_check_filename(const char * zFilename){
1063
+ if(0==fileedit_is_editable(zFilename)){
1064
+ fileedit_ajax_error(403, "File is disallowed by the "
1065
+ "fileedit-glob setting.");
1066
+ return 0;
1067
+ }
1068
+ return 1;
1069
+}
1070
+
1071
+/*
1072
+** Passed the values of the "r" and "file" request properties,
1073
+** this function verifies that they are valid and populates:
1074
+**
1075
+** - *zRevUuid = the fully-expanded value of zRev (owned by the
1076
+** caller). zRevUuid may be NULL.
1077
+**
1078
+** - *vid = the RID of zRevUuid. May not be NULL.
1079
+**
1080
+** - *frid = the RID of zFilename's blob content. May not be NULL.
1081
+**
1082
+** Returns 0 if the given file is not in the given checkin or if
1083
+** fileedit_ajax_check_filename() fails, else returns true. If it
1084
+** returns false, it queues up an error response and the caller must
1085
+** return immediately.
1086
+*/
1087
+static int fileedit_ajax_setup_filerev(const char * zRev,
1088
+ char ** zRevUuid,
1089
+ int * vid,
1090
+ const char * zFilename,
1091
+ int * frid){
1092
+ char * zCi = 0; /* fully-resolved checkin UUID */
1093
+ char * zFileUuid; /* file UUID */
1094
+
1095
+ if(!fileedit_ajax_check_filename(zFilename)){
1096
+ return 0;
1097
+ }
1098
+ *vid = symbolic_name_to_rid(zRev, "ci");
1099
+ if(0==*vid){
1100
+ fileedit_ajax_error(404,"Cannot resolve name as a checkin: %s",
1101
+ zRev);
1102
+ return 0;
1103
+ }
1104
+ zFileUuid = fileedit_file_uuid(zFilename, *vid, 0);
1105
+ if(zFileUuid==0){
1106
+ fileedit_ajax_error(404,"Checkin does not contain file.");
1107
+ return 0;
1108
+ }
1109
+ zCi = rid_to_uuid(*vid);
1110
+ *frid = fast_uuid_to_rid(zFileUuid);
1111
+ fossil_free(zFileUuid);
1112
+ if(zRevUuid!=0){
1113
+ *zRevUuid = zCi;
1114
+ }else{
1115
+ fossil_free(zCi);
1116
+ }
1117
+ return 1;
1118
+}
1119
+
1120
+
1121
+/*
1122
+** WEBPAGE: fileedit_content
1123
+**
1124
+** Query parameters:
1125
+**
1126
+** file=FILENAME
1127
+** r=CHECKIN_NAME
1128
+**
1129
+** User must have Write access to use this page.
1130
+**
1131
+** Responds with the raw content of the given page. On error it
1132
+** produces a JSON response as documented for fileedit_ajax_error().
1133
+*/
1134
+void fileedit_ajax_content(){
1135
+ const char * zFilename = PD("file",P("name"));
1136
+ const char * zRev = P("r");
1137
+ int vid, frid;
1138
+ Blob content = empty_blob;
1139
+ const char * zMime;
1140
+ if(!fileedit_ajax_boostrap()
1141
+ || !fileedit_ajax_setup_filerev(zRev, 0, &vid,
1142
+ zFilename, &frid)){
1143
+ return;
1144
+ }
1145
+ zMime = mimetype_from_name(zFilename);
1146
+ content_get(frid, &content);
1147
+ cgi_set_content_type(zMime ? zMime : "application/octet-stream");
1148
+ cgi_set_content(&content);
1149
+}
1150
+
1151
+/*
1152
+** WEBPAGE: fileedit_preview
1153
+**
1154
+** Query parameters:
1155
+**
1156
+** file=FILENAME
1157
+** render_mode=integer (FE_RENDER_xxx) (default=FE_RENDER_GUESS)
1158
+** content=text
1159
+**
1160
+** User must have Write access to use this page.
1161
+**
1162
+** Responds with the HTML content of the preview. On error it produces
1163
+** a JSON response as documented for fileedit_ajax_error().
1164
+*/
1165
+void fileedit_ajax_preview(){
1166
+ const char * zFilename = PD("file",P("name"));
1167
+ const char * zContent = P("content");
1168
+ int renderMode = atoi(PD("render_mode","0"));
1169
+ int ln = atoi(PD("ln","0"));
1170
+ int iframeHeight = atoi(PD("iframe_height","40"));
1171
+ Blob content = empty_blob;
1172
+ if(!fileedit_ajax_boostrap()
1173
+ || !fileedit_ajax_check_filename(zFilename)){
1174
+ return;
1175
+ }
1176
+ cgi_set_content_type("text/html");
1177
+ blob_init(&content, zContent, -1);
1178
+ fileedit_render_preview(&content, zFilename,
1179
+ ln ? FE_PREVIEW_LINE_NUMBERS : 0,
1180
+ renderMode, iframeHeight);
1181
+}
1182
+
1183
+/*
1184
+** WEBPAGE: fileedit_diff
1185
+**
1186
+** file=FILENAME
1187
+** sbs=integer (1=side-by-side or 0=unified)
1188
+** content=text
1189
+** r=checkin version
1190
+**
1191
+** User must have Write access to use this page.
1192
+**
1193
+** Responds with the HTML content of the diff. On error it produces a
1194
+** JSON response as documented for fileedit_ajax_error().
1195
+*/
1196
+void fileedit_ajax_diff(){
1197
+ /*
1198
+ ** Reminder: we only need the filename to perform valdiation
1199
+ ** against fileedit_is_editable(), else this route could be
1200
+ ** abused to get diffs against content disallowed by the
1201
+ ** whitelist.
1202
+ */
1203
+ const char * zFilename = PD("file",P("name"));
1204
+ const char * zRev = P("r");
1205
+ const char * zContent = P("content");
1206
+ char * zRevUuid = 0;
1207
+ int isSbs = atoi(PD("sbs","0"));
1208
+ int vid, frid;
1209
+ Blob content = empty_blob;
1210
+ if(!fileedit_ajax_boostrap()
1211
+ || !fileedit_ajax_setup_filerev(zRev, &zRevUuid, &vid,
1212
+ zFilename, &frid)){
1213
+ return;
1214
+ }
1215
+ if(!zContent){
1216
+ zContent = "";
1217
+ }
1218
+ cgi_set_content_type("text/html");
1219
+ blob_init(&content, zContent, -1);
1220
+ fileedit_render_diff(&content, frid, zRevUuid, isSbs);
1221
+ fossil_free(zRevUuid);
1222
+ blob_reset(&content);
1223
+}
1224
+
1225
+
1226
+/*
1227
+** Emits utility script code specific to the /fileedit page.
1228
+*/
1229
+static void fileedit_emit_page_script(){
1230
+ style_emit_script_tag(0);
1231
+ CX("%s\n", builtin_text("fossil.page.fileedit.js"));
1232
+ style_emit_script_tag(1);
1233
+}
11271234
11281235
/*
11291236
** WEBPAGE: fileedit
11301237
**
11311238
** EXPERIMENTAL and subject to change and removal at any time. The goal
@@ -1171,33 +1278,12 @@
11711278
combined into a single JS
11721279
function call, thus each
11731280
entry must end with a
11741281
semicolon. */
11751282
Stmt stmt = empty_Stmt;
1176
- const int loadMode = 0; /* See next comment block */
1177
- /* loadMode: How to populate the TEXTAREA:
1178
- **
1179
- ** 0: HTML encode: despite my personal reservations regarding HTML
1180
- ** escaping, this seems to be the only reliable approach
1181
- ** until/unless we completely AJAXify this page.
1182
- **
1183
- ** 1: JSON mode: JSON-izes the file content and injects it, via JS,
1184
- ** into the editor TEXTAREA. This works wonderfully until the input
1185
- ** file contains an raw <SCRIPT> tag, at which points the HTML
1186
- ** parser chokes on it.
1187
- **
1188
- ** 2: AJAX mode: can only load content from the db, not preview/dry-run
1189
- ** content. Unless this page is refactored to work solely over AJAX
1190
- ** (which is a potential TODO), this method is only useful on the
1191
- ** initial hit to this page, where the file is loaded.
1192
- **
1193
- ** loadMode is not generally configurable: change it only for
1194
- ** testing/development purposes.
1195
- */
11961283
#define fail(EXPR) blob_appendf EXPR; goto end_footer
11971284
1198
- assert(loadMode==0 || loadMode==1 || loadMode==2);
11991285
login_check_credentials();
12001286
if( !g.perm.Write ){
12011287
login_needed(g.anon.Write);
12021288
return;
12031289
}
@@ -1352,34 +1438,39 @@
13521438
cimi.pMfOut = 0;
13531439
blob_reset(&manifest);
13541440
break;
13551441
}
13561442
1443
+ CX("<div id='fossil-status-bar'>Async. status messages will go "
1444
+ "here.</div>\n");
13571445
CX("<h1>Editing:</h1>");
13581446
CX("<p class='fileedit-hint'>");
13591447
CX("File: "
1360
- "[<a id='finfo-link' href='%R/finfo?name=%T&m=%!S'>info</a>] "
1361
- "<code>%h</code><br>",
1362
- zFilename, zFileUuid, zFilename);
1448
+ "[<a id='finfo-link' href='#'>info</a>] "
1449
+ /* %R/finfo?name=%T&m=%!S */
1450
+ "<code id='finfo-file-name'>(loading)</code><br>");
13631451
CX("Checkin Version: "
1364
- "[<a id='r-link' href='%R/info/%!S'>info</a>] "
1365
- "<code id='r-label'>%s</code><br>",
1366
- cimi.zParentUuid, cimi.zParentUuid);
1452
+ "[<a id='r-link' href='#'>info</a>] "
1453
+ /* %R/info/%!S */
1454
+ "<code id='r-label'>(loading...)</code><br>"
1455
+ );
13671456
CX("Permalink: <code>"
1368
- "<a id='permalink' href='%R/fileedit?file=%T&r=%!S'>"
1369
- "/fileedit?file=%T&r=%!S</a></code><br>"
1457
+ "<a id='permalink' href='#'>(loading...)</a></code><br>"
13701458
"(Clicking the permalink will reload the page and discard "
13711459
"all edits!)",
13721460
zFilename, cimi.zParentUuid,
13731461
zFilename, cimi.zParentUuid);
13741462
CX("</p>");
13751463
CX("<p>This page is <em>NEW AND EXPERIMENTAL</em>. "
13761464
"USE AT YOUR OWN RISK, preferably on a test "
13771465
"repo.</p>\n");
13781466
1379
- CX("<form action='%R/fileedit#options' method='POST' "
1380
- "class='fileedit'>\n");
1467
+ CX("<form action='#' method='POST' "
1468
+ "class='fileedit' id='fileedit-form' "
1469
+ "onsubmit='function(e){"
1470
+ "e.preventDefault(); e.stopPropagation(); return false;"
1471
+ "}'>\n");
13811472
13821473
/******* Hidden fields *******/
13831474
CX("<input type='hidden' name='r' value='%s'>",
13841475
cimi.zParentUuid);
13851476
CX("<input type='hidden' name='file' value='%T'>",
@@ -1387,16 +1478,11 @@
13871478
13881479
/******* Content *******/
13891480
CX("<h3>File Content</h3>\n");
13901481
CX("<textarea name='content' id='fileedit-content' "
13911482
"rows='20' cols='80'>");
1392
- if(0==loadMode){
1393
- CX("%h",blob_str(&cimi.fileContent));
1394
- }else{
1395
- CX("Loading...");
1396
- /* Performed via JS later on */
1397
- }
1483
+ CX("Loading...");
13981484
CX("</textarea>\n");
13991485
/******* Flags/options *******/
14001486
CX("<fieldset class='fileedit-options' id='options'>"
14011487
"<legend>Options</legend><div>"
14021488
/* Chrome does not sanely lay out multiple
@@ -1459,108 +1545,60 @@
14591545
CX("</div></fieldset>\n");
14601546
14611547
/******* Buttons *******/
14621548
CX("<a id='buttons'></a>");
14631549
CX("<fieldset class='fileedit-options'>"
1464
- "<legend>Tell the server to...</legend><div>");
1465
- CX("<button type='submit' name='submit' value='%d'>"
1466
- "Commit</button>", SUBMIT_SAVE);
1467
- CX("<button type='submit' name='submit' value='%d'>"
1468
- "Preview</button>", SUBMIT_PREVIEW);
1550
+ "<legend>Ask the server to...</legend><div>");
1551
+ CX("<button id='fileedit-btn-commit'>Commit</button>");
1552
+ CX("<button id='fileedit-btn-preview'>Preview</button>");
14691553
{
14701554
/* Preview rendering mode selection... */
14711555
previewRenderMode = atoi(PD("preview_render_mode","0"));
1472
- if(0==previewRenderMode){
1473
- previewRenderMode = fileedit_render_mode_for_mimetype(zFileMime);
1474
- }
1556
+ previewRenderMode = fileedit_render_mode_for_mimetype(zFileMime);
14751557
style_select_list_int("preview_render_mode",
14761558
"Preview Mode",
14771559
"Preview mode format.",
14781560
previewRenderMode,
14791561
"Guess", FE_RENDER_GUESS,
14801562
"Wiki/Markdown", FE_RENDER_WIKI,
14811563
"HTML (iframe)", FE_RENDER_HTML,
14821564
"Plain Text", FE_RENDER_PLAIN_TEXT,
14831565
NULL);
1484
- if(FE_RENDER_HTML==previewRenderMode){
1485
- /* HTML preview mode iframe height... */
1486
- if(submitMode==SUBMIT_PREVIEW){
1487
- previewHtmlHeight = atoi(PD("preview_html_ems","0"));
1488
- }else{
1489
- previewHtmlHeight = 40;
1490
- }
1491
- /* Allow selection of HTML preview iframe height */
1492
- style_select_list_int("preview_html_ems",
1493
- "Preview IFrame Height (EMs)",
1494
- "Height (in EMs) of the iframe used for "
1495
- "HTML preview",
1496
- previewHtmlHeight,
1497
- "", 20, "", 40,
1498
- "", 60, "", 80,
1499
- "", 100, NULL);
1500
- }
1501
- else if(FE_RENDER_PLAIN_TEXT==previewRenderMode){
1502
- style_labeled_checkbox("preview_ln",
1503
- "Add line numbers to plain-text previews?",
1504
- "1",
1505
- "If on, plain-text files (only) will get "
1506
- "line numbers added to the preview.",
1507
- previewLn);
1508
- }
1509
- }
1510
- CX("<button type='submit' name='submit' value='%d'>"
1511
- "Diff (SBS)</button>", SUBMIT_DIFF_SBS);
1512
- CX("<button type='submit' name='submit' value='%d'>"
1513
- "Diff (Unified)</button>", SUBMIT_DIFF_UNIFIED);
1566
+ previewHtmlHeight = atoi(PD("preview_html_ems","0"));
1567
+ if(!previewHtmlHeight){
1568
+ previewHtmlHeight = 40;
1569
+ }
1570
+ /* Allow selection of HTML preview iframe height */
1571
+ style_select_list_int("preview_html_ems",
1572
+ "HTML Preview IFrame Height (EMs)",
1573
+ "Height (in EMs) of the iframe used for "
1574
+ "HTML preview",
1575
+ previewHtmlHeight,
1576
+ "", 20, "", 40,
1577
+ "", 60, "", 80,
1578
+ "", 100, NULL);
1579
+ style_labeled_checkbox("preview_ln",
1580
+ "Add line numbers to plain-text previews?",
1581
+ "1",
1582
+ "If on, plain-text files (only) will get "
1583
+ "line numbers added to the preview.",
1584
+ previewLn);
1585
+ }
1586
+ CX("<button id='fileedit-btn-diffsbs'>Diff (SBS)</button>");
1587
+ CX("<button id='fileedit-btn-diffu'>Diff (Unified)</button>");
15141588
CX("</div></fieldset>");
15151589
15161590
/******* End of form *******/
15171591
CX("</form>\n");
15181592
1519
- /*
1520
- ** We cannot intercept the output for PREVIEW
1521
- ** and DIFF modes, and therefore have to render those
1522
- ** last.
1523
- */
1524
- if(SUBMIT_PREVIEW==submitMode){
1525
- int pflags = 0;
1526
- if(previewLn) pflags |= FE_PREVIEW_LINE_NUMBERS;
1527
- fileedit_render_preview(&cimi.fileContent, cimi.zFilename, pflags,
1528
- previewRenderMode, previewHtmlHeight);
1529
- }else if(SUBMIT_DIFF_SBS==submitMode
1530
- || SUBMIT_DIFF_UNIFIED==submitMode){
1531
- fileedit_render_diff(&cimi.fileContent, frid, cimi.zParentUuid,
1532
- SUBMIT_DIFF_SBS==submitMode);
1533
- }
1534
-
1593
+ CX("<div id='ajax-target'></div>"
1594
+ /* this is where preview/diff go */);
1595
+
15351596
/* Dynamically populate the editor... */
1536
- fileedit_emit_script_fetch();
1537
- if(1==loadMode || (2==loadMode && submitMode>SUBMIT_NONE)){
1538
- char const * zQuoted = 0;
1539
- if(blob_size(&cimi.fileContent)>0){
1540
- db_prepare(&stmt, "SELECT json_quote(%B)", &cimi.fileContent);
1541
- db_step(&stmt);
1542
- zQuoted = db_column_text(&stmt,0);
1543
- }
1544
- blob_appendf(&endScript,
1545
- "/* populate editor form */\n"
1546
- "document.getElementById('fileedit-content')"
1547
- ".value=%s;", zQuoted ? zQuoted : "'';\n");
1548
- if(stmt.pStmt){
1549
- db_finalize(&stmt);
1550
- }
1551
- }else if(2==loadMode){
1552
- assert(submitMode==SUBMIT_NONE);
1553
- blob_appendf(&endScript,
1554
- "window.fossil.fetch('raw/%s',{"
1555
- "onload: (r)=>document.getElementById('fileedit-content')"
1556
- ".value=r,"
1557
- "onerror:()=>document.getElementById('fileedit-content')"
1558
- ".value="
1559
- "'Error loading content'"
1560
- "});\n", zFileUuid);
1561
- }
1597
+ blob_appendf(&endScript,
1598
+ "fossil.page.loadFile('%j','%j');",
1599
+ zFilename, cimi.zParentUuid);
15621600
15631601
end_footer:
15641602
zContent = 0;
15651603
fossil_free(zFileUuid);
15661604
if(stmt.pStmt){
@@ -1573,10 +1611,12 @@
15731611
CX("%b",&submitResult);
15741612
}
15751613
blob_reset(&submitResult);
15761614
blob_reset(&err);
15771615
CheckinMiniInfo_cleanup(&cimi);
1616
+ style_emit_script_fetch();
1617
+ fileedit_emit_page_script();
15781618
if(blob_size(&endScript)>0){
15791619
style_emit_script_tag(0);
15801620
CX("(function(){\n");
15811621
CX("try{\n%b\n}catch(e){console.error('Exception:',e)}\n",
15821622
&endScript);
15831623
15841624
ADDED src/fossil.bootstrap.js
15851625
ADDED src/fossil.fetch.js
15861626
ADDED src/fossil.page.fileedit.js
--- src/fileedit.c
+++ src/fileedit.c
@@ -898,118 +898,16 @@
898 fossil_free(zGlobs);
899 }
900 return glob_match(pGlobs, zFilename);
901 }
902
903 /*
904 ** Emits a script tag which defines window.fossil.fetch(), which works
905 ** similarly (not identically) to the not-quite-ubiquitous global
906 ** fetch().
907 **
908 ** JS usages:
909 **
910 ** fossilFetch( URI, onLoadCallback );
911 **
912 ** fossilFetch( URI, optionsObject );
913 **
914 ** Noting that URI must be relative to the top of the repository and
915 ** must not start with a slash (if it does, it is stripped). It gets
916 ** %R/ prepended to it.
917 **
918 ** The optionsObject may be an onload callback or an object with any
919 ** of these properties:
920 **
921 ** - onload: callback(responseData) (default = output response to
922 ** console).
923 **
924 ** - onerror: callback(XHR onload event) (default = console output)
925 **
926 ** - method: 'POST' | 'GET' (default = 'GET')
927 **
928 ** - payload: anything acceptable by XHR2.send(ARG) (DOMString,
929 ** Document, FormData, Blob, File, ArrayBuffer), or a plain object
930 ** or array, either of which gets JSON.stringify()'d. If set then
931 ** the method is automatically set to 'POST'. If an object/array is
932 ** converted to JSON, the content-type is set to 'application/json'.
933 ** By default XHR2 will set the content type based on the payload
934 ** type.
935 **
936 ** - contentType: Optional request content type when POSTing. Ignored
937 ** if the method is not 'POST'.
938 **
939 ** - responseType: optional string. One of ("text", "arraybuffer",
940 ** "blob", or "document") (as specified by XHR2). Default = "text".
941 **
942 ** - urlParams: string|object. If a string, it is assumed to be a
943 ** URI-encoded list of params in the form "key1=val1&key2=val2...",
944 ** with NO leading '?'. If it is an object, all of its properties
945 ** get converted to that form. Either way, the parameters get
946 ** appended to the URL.
947 **
948 */
949 void fileedit_emit_script_fetch(){
950 style_emit_script_tag(0);
951 CX("fossil.fetch = function(path,opt){\n");
952 CX(" if('/'===path[0]) path = path.substr(1);\n");
953 CX(" if(!opt){\n");
954 CX(" opt = {onload:(r)=>console.debug('response:',r)};\n");
955 CX(" }else if('function'===typeof opt){\n");
956 CX(" opt={onload:opt,\n");
957 CX(" onerror:(e)=>console.error('ajax error:',e)};\n");
958 CX(" }\n");
959 CX(" let payload = opt.payload;\n");
960 CX(" if(payload){\n");
961 CX(" opt.method = 'POST';\n");
962 CX(" if(!(payload instanceof FormData)\n");
963 CX(" && !(payload instanceof Document)\n");
964 CX(" && !(payload instanceof Blob)\n");
965 CX(" && !(payload instanceof File)\n");
966 CX(" && !(payload instanceof ArrayBuffer)){\n");
967 CX(" if('object'===typeof payload || payload instanceof Array){\n");
968 CX(" payload = JSON.stringify(payload);\n");
969 CX(" opt.contentType = 'application/json';\n");
970 CX(" }\n");
971 CX(" }\n");
972 CX(" }\n");
973 CX(" const url=['%R/'+path], x=new XMLHttpRequest();\n");
974 CX(" if(opt.urlParams){\n");
975 CX(" url.push('?');\n");
976 CX(" if('string'===typeof opt.urlParams){\n");
977 CX(" url.push(opt.urlParams);\n");
978 CX(" }else{/*assume object*/\n");
979 CX(" let k, i = 0;\n");
980 CX(" for( k in opt.urlParams ){\n");
981 CX(" if(i++) url.push('&');\n");
982 CX(" url.push(k,'=',encodeURIComponent(opt.urlParams[k]));\n");
983 CX(" }\n");
984 CX(" }\n");
985 CX(" }\n");
986 CX(" if('POST'===opt.method && 'string'===typeof opt.contentType){\n");
987 CX(" x.setRequestHeader('Content-Type',opt.contentType);\n");
988 CX(" }\n");
989 CX(" x.open(opt.method||'GET', url.join(''), true);\n");
990 CX(" x.responseType=opt.responseType||'text';\n");
991 CX(" if(opt.onload){\n");
992 CX(" x.onload = function(e){\n");
993 CX(" if(200!==this.status){\n");
994 CX(" if(opt.onerror) opt.onerror(e);\n");
995 CX(" return;\n");
996 CX(" }\n");
997 CX(" opt.onload(this.response);\n");
998 CX(" }\n");
999 CX(" }\n");
1000 CX(" if(payload) x.send(payload);\n");
1001 CX(" else x.send();\n");
1002 CX("};\n");
1003 style_emit_script_tag(1);
1004 };
1005
1006
1007 enum fileedit_render_preview_flags {
1008 FE_PREVIEW_LINE_NUMBERS = 1
1009 };
1010 enum fileedit_render_modes {
 
1011 FE_RENDER_GUESS = 0,
1012 FE_RENDER_PLAIN_TEXT,
1013 FE_RENDER_HTML,
1014 FE_RENDER_WIKI
1015 };
@@ -1037,12 +935,10 @@
1037 const char * zMime;
1038 zMime = mimetype_from_name(zFilename);
1039 if(FE_RENDER_GUESS==renderMode){
1040 renderMode = fileedit_render_mode_for_mimetype(zMime);
1041 }
1042 CX("<div class='fileedit-preview'>");
1043 CX("<div>Preview</div>");
1044 switch(renderMode){
1045 case FE_RENDER_HTML:{
1046 char * z64 = encode64(blob_str(pContent), blob_size(pContent));
1047 CX("<iframe width='100%%' frameborder='0' "
1048 "marginwidth='0' style='height:%dem' "
@@ -1068,11 +964,10 @@
1068 CX("<pre>%h</pre>", zExt+1, zContent);
1069 }
1070 break;
1071 }
1072 }
1073 CX("</div><!--.fileedit-preview-->\n");
1074 }
1075
1076 /*
1077 ** Renders diffs for the /fileedit page. pContent is the
1078 ** locally-edited content. frid is the RID of the file's blob entry
@@ -1087,19 +982,15 @@
1087 Blob out = empty_blob;
1088 u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG | DIFF_STRIP_EOLCR
1089 | (isSbs ? DIFF_SIDEBYSIDE : DIFF_LINENO);
1090 content_get(frid, &orig);
1091 text_diff(&orig, pContent, &out, 0, diffFlags);
1092 CX("<div class='fileedit-diff'>");
1093 CX("<div>Diff <code>[%S]</code> &rarr; Local Edits</div>",
1094 zManifestUuid);
1095 if(isSbs){
1096 CX("%b",&out);
1097 }else{
1098 CX("<pre class='udiff'>%b</pre>",&out);
1099 }
1100 CX("</div><!--.fileedit-diff-->\n");
1101 blob_reset(&orig);
1102 blob_reset(&out);
1103 }
1104
1105 /*
@@ -1122,10 +1013,226 @@
1122 }
1123 }
1124 db_finalize(&stmt);
1125 return zFileUuid;
1126 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1127
1128 /*
1129 ** WEBPAGE: fileedit
1130 **
1131 ** EXPERIMENTAL and subject to change and removal at any time. The goal
@@ -1171,33 +1278,12 @@
1171 combined into a single JS
1172 function call, thus each
1173 entry must end with a
1174 semicolon. */
1175 Stmt stmt = empty_Stmt;
1176 const int loadMode = 0; /* See next comment block */
1177 /* loadMode: How to populate the TEXTAREA:
1178 **
1179 ** 0: HTML encode: despite my personal reservations regarding HTML
1180 ** escaping, this seems to be the only reliable approach
1181 ** until/unless we completely AJAXify this page.
1182 **
1183 ** 1: JSON mode: JSON-izes the file content and injects it, via JS,
1184 ** into the editor TEXTAREA. This works wonderfully until the input
1185 ** file contains an raw <SCRIPT> tag, at which points the HTML
1186 ** parser chokes on it.
1187 **
1188 ** 2: AJAX mode: can only load content from the db, not preview/dry-run
1189 ** content. Unless this page is refactored to work solely over AJAX
1190 ** (which is a potential TODO), this method is only useful on the
1191 ** initial hit to this page, where the file is loaded.
1192 **
1193 ** loadMode is not generally configurable: change it only for
1194 ** testing/development purposes.
1195 */
1196 #define fail(EXPR) blob_appendf EXPR; goto end_footer
1197
1198 assert(loadMode==0 || loadMode==1 || loadMode==2);
1199 login_check_credentials();
1200 if( !g.perm.Write ){
1201 login_needed(g.anon.Write);
1202 return;
1203 }
@@ -1352,34 +1438,39 @@
1352 cimi.pMfOut = 0;
1353 blob_reset(&manifest);
1354 break;
1355 }
1356
 
 
1357 CX("<h1>Editing:</h1>");
1358 CX("<p class='fileedit-hint'>");
1359 CX("File: "
1360 "[<a id='finfo-link' href='%R/finfo?name=%T&m=%!S'>info</a>] "
1361 "<code>%h</code><br>",
1362 zFilename, zFileUuid, zFilename);
1363 CX("Checkin Version: "
1364 "[<a id='r-link' href='%R/info/%!S'>info</a>] "
1365 "<code id='r-label'>%s</code><br>",
1366 cimi.zParentUuid, cimi.zParentUuid);
 
1367 CX("Permalink: <code>"
1368 "<a id='permalink' href='%R/fileedit?file=%T&r=%!S'>"
1369 "/fileedit?file=%T&r=%!S</a></code><br>"
1370 "(Clicking the permalink will reload the page and discard "
1371 "all edits!)",
1372 zFilename, cimi.zParentUuid,
1373 zFilename, cimi.zParentUuid);
1374 CX("</p>");
1375 CX("<p>This page is <em>NEW AND EXPERIMENTAL</em>. "
1376 "USE AT YOUR OWN RISK, preferably on a test "
1377 "repo.</p>\n");
1378
1379 CX("<form action='%R/fileedit#options' method='POST' "
1380 "class='fileedit'>\n");
 
 
 
1381
1382 /******* Hidden fields *******/
1383 CX("<input type='hidden' name='r' value='%s'>",
1384 cimi.zParentUuid);
1385 CX("<input type='hidden' name='file' value='%T'>",
@@ -1387,16 +1478,11 @@
1387
1388 /******* Content *******/
1389 CX("<h3>File Content</h3>\n");
1390 CX("<textarea name='content' id='fileedit-content' "
1391 "rows='20' cols='80'>");
1392 if(0==loadMode){
1393 CX("%h",blob_str(&cimi.fileContent));
1394 }else{
1395 CX("Loading...");
1396 /* Performed via JS later on */
1397 }
1398 CX("</textarea>\n");
1399 /******* Flags/options *******/
1400 CX("<fieldset class='fileedit-options' id='options'>"
1401 "<legend>Options</legend><div>"
1402 /* Chrome does not sanely lay out multiple
@@ -1459,108 +1545,60 @@
1459 CX("</div></fieldset>\n");
1460
1461 /******* Buttons *******/
1462 CX("<a id='buttons'></a>");
1463 CX("<fieldset class='fileedit-options'>"
1464 "<legend>Tell the server to...</legend><div>");
1465 CX("<button type='submit' name='submit' value='%d'>"
1466 "Commit</button>", SUBMIT_SAVE);
1467 CX("<button type='submit' name='submit' value='%d'>"
1468 "Preview</button>", SUBMIT_PREVIEW);
1469 {
1470 /* Preview rendering mode selection... */
1471 previewRenderMode = atoi(PD("preview_render_mode","0"));
1472 if(0==previewRenderMode){
1473 previewRenderMode = fileedit_render_mode_for_mimetype(zFileMime);
1474 }
1475 style_select_list_int("preview_render_mode",
1476 "Preview Mode",
1477 "Preview mode format.",
1478 previewRenderMode,
1479 "Guess", FE_RENDER_GUESS,
1480 "Wiki/Markdown", FE_RENDER_WIKI,
1481 "HTML (iframe)", FE_RENDER_HTML,
1482 "Plain Text", FE_RENDER_PLAIN_TEXT,
1483 NULL);
1484 if(FE_RENDER_HTML==previewRenderMode){
1485 /* HTML preview mode iframe height... */
1486 if(submitMode==SUBMIT_PREVIEW){
1487 previewHtmlHeight = atoi(PD("preview_html_ems","0"));
1488 }else{
1489 previewHtmlHeight = 40;
1490 }
1491 /* Allow selection of HTML preview iframe height */
1492 style_select_list_int("preview_html_ems",
1493 "Preview IFrame Height (EMs)",
1494 "Height (in EMs) of the iframe used for "
1495 "HTML preview",
1496 previewHtmlHeight,
1497 "", 20, "", 40,
1498 "", 60, "", 80,
1499 "", 100, NULL);
1500 }
1501 else if(FE_RENDER_PLAIN_TEXT==previewRenderMode){
1502 style_labeled_checkbox("preview_ln",
1503 "Add line numbers to plain-text previews?",
1504 "1",
1505 "If on, plain-text files (only) will get "
1506 "line numbers added to the preview.",
1507 previewLn);
1508 }
1509 }
1510 CX("<button type='submit' name='submit' value='%d'>"
1511 "Diff (SBS)</button>", SUBMIT_DIFF_SBS);
1512 CX("<button type='submit' name='submit' value='%d'>"
1513 "Diff (Unified)</button>", SUBMIT_DIFF_UNIFIED);
1514 CX("</div></fieldset>");
1515
1516 /******* End of form *******/
1517 CX("</form>\n");
1518
1519 /*
1520 ** We cannot intercept the output for PREVIEW
1521 ** and DIFF modes, and therefore have to render those
1522 ** last.
1523 */
1524 if(SUBMIT_PREVIEW==submitMode){
1525 int pflags = 0;
1526 if(previewLn) pflags |= FE_PREVIEW_LINE_NUMBERS;
1527 fileedit_render_preview(&cimi.fileContent, cimi.zFilename, pflags,
1528 previewRenderMode, previewHtmlHeight);
1529 }else if(SUBMIT_DIFF_SBS==submitMode
1530 || SUBMIT_DIFF_UNIFIED==submitMode){
1531 fileedit_render_diff(&cimi.fileContent, frid, cimi.zParentUuid,
1532 SUBMIT_DIFF_SBS==submitMode);
1533 }
1534
1535 /* Dynamically populate the editor... */
1536 fileedit_emit_script_fetch();
1537 if(1==loadMode || (2==loadMode && submitMode>SUBMIT_NONE)){
1538 char const * zQuoted = 0;
1539 if(blob_size(&cimi.fileContent)>0){
1540 db_prepare(&stmt, "SELECT json_quote(%B)", &cimi.fileContent);
1541 db_step(&stmt);
1542 zQuoted = db_column_text(&stmt,0);
1543 }
1544 blob_appendf(&endScript,
1545 "/* populate editor form */\n"
1546 "document.getElementById('fileedit-content')"
1547 ".value=%s;", zQuoted ? zQuoted : "'';\n");
1548 if(stmt.pStmt){
1549 db_finalize(&stmt);
1550 }
1551 }else if(2==loadMode){
1552 assert(submitMode==SUBMIT_NONE);
1553 blob_appendf(&endScript,
1554 "window.fossil.fetch('raw/%s',{"
1555 "onload: (r)=>document.getElementById('fileedit-content')"
1556 ".value=r,"
1557 "onerror:()=>document.getElementById('fileedit-content')"
1558 ".value="
1559 "'Error loading content'"
1560 "});\n", zFileUuid);
1561 }
1562
1563 end_footer:
1564 zContent = 0;
1565 fossil_free(zFileUuid);
1566 if(stmt.pStmt){
@@ -1573,10 +1611,12 @@
1573 CX("%b",&submitResult);
1574 }
1575 blob_reset(&submitResult);
1576 blob_reset(&err);
1577 CheckinMiniInfo_cleanup(&cimi);
 
 
1578 if(blob_size(&endScript)>0){
1579 style_emit_script_tag(0);
1580 CX("(function(){\n");
1581 CX("try{\n%b\n}catch(e){console.error('Exception:',e)}\n",
1582 &endScript);
1583
1584 DDED src/fossil.bootstrap.js
1585 DDED src/fossil.fetch.js
1586 DDED src/fossil.page.fileedit.js
--- src/fileedit.c
+++ src/fileedit.c
@@ -898,118 +898,16 @@
898 fossil_free(zGlobs);
899 }
900 return glob_match(pGlobs, zFilename);
901 }
902
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
903
904 enum fileedit_render_preview_flags {
905 FE_PREVIEW_LINE_NUMBERS = 1
906 };
907 enum fileedit_render_modes {
908 /* GUESS must be 0. All others have specified values. */
909 FE_RENDER_GUESS = 0,
910 FE_RENDER_PLAIN_TEXT,
911 FE_RENDER_HTML,
912 FE_RENDER_WIKI
913 };
@@ -1037,12 +935,10 @@
935 const char * zMime;
936 zMime = mimetype_from_name(zFilename);
937 if(FE_RENDER_GUESS==renderMode){
938 renderMode = fileedit_render_mode_for_mimetype(zMime);
939 }
 
 
940 switch(renderMode){
941 case FE_RENDER_HTML:{
942 char * z64 = encode64(blob_str(pContent), blob_size(pContent));
943 CX("<iframe width='100%%' frameborder='0' "
944 "marginwidth='0' style='height:%dem' "
@@ -1068,11 +964,10 @@
964 CX("<pre>%h</pre>", zExt+1, zContent);
965 }
966 break;
967 }
968 }
 
969 }
970
971 /*
972 ** Renders diffs for the /fileedit page. pContent is the
973 ** locally-edited content. frid is the RID of the file's blob entry
@@ -1087,19 +982,15 @@
982 Blob out = empty_blob;
983 u64 diffFlags = DIFF_HTML | DIFF_NOTTOOBIG | DIFF_STRIP_EOLCR
984 | (isSbs ? DIFF_SIDEBYSIDE : DIFF_LINENO);
985 content_get(frid, &orig);
986 text_diff(&orig, pContent, &out, 0, diffFlags);
 
 
 
987 if(isSbs){
988 CX("%b",&out);
989 }else{
990 CX("<pre class='udiff'>%b</pre>",&out);
991 }
 
992 blob_reset(&orig);
993 blob_reset(&out);
994 }
995
996 /*
@@ -1122,10 +1013,226 @@
1013 }
1014 }
1015 db_finalize(&stmt);
1016 return zFileUuid;
1017 }
1018
1019 /*
1020 ** Helper for /filepage_xyz routes. Clears the CGI content buffer,
1021 ** sets an error status code, and queues up a JSON response in the
1022 ** form of an object:
1023 **
1024 ** {error: formatted message}
1025 **
1026 ** After calling this, the caller should immediately return.
1027 */
1028 static void fileedit_ajax_error(int httpCode, const char * zFmt, ...){
1029 Blob msg = empty_blob;
1030 Blob content = empty_blob;
1031 va_list vargs;
1032 va_start(vargs,zFmt);
1033 blob_vappendf(&msg, zFmt, vargs);
1034 va_end(vargs);
1035 blob_appendf(&content,"{\"error\":\"%j\"}", blob_str(&msg));
1036 blob_reset(&msg);
1037 cgi_set_content(&content);
1038 cgi_set_status(httpCode, "Error");
1039 cgi_set_content_type("application/json");
1040 }
1041
1042 /*
1043 ** Performs bootstrapping common to the /fileedit_xyz AJAX routes.
1044 ** Returns 0 if bootstrapping fails (wrong permissions), in which
1045 ** case it has reported the error and the route should immediately
1046 ** return. Returns true on success.
1047 */
1048 static int fileedit_ajax_boostrap(){
1049 login_check_credentials();
1050 if( !g.perm.Write ){
1051 fileedit_ajax_error(403,"Write permissions required.");
1052 return 0;
1053 }
1054 return 1;
1055 }
1056 /*
1057 ** Returns true if the current user is allowed to edit the given
1058 ** filename, as determined by fileedit_is_editable(), else false,
1059 ** in which case it queues up an error response and the caller
1060 ** must return immediately.
1061 */
1062 static int fileedit_ajax_check_filename(const char * zFilename){
1063 if(0==fileedit_is_editable(zFilename)){
1064 fileedit_ajax_error(403, "File is disallowed by the "
1065 "fileedit-glob setting.");
1066 return 0;
1067 }
1068 return 1;
1069 }
1070
1071 /*
1072 ** Passed the values of the "r" and "file" request properties,
1073 ** this function verifies that they are valid and populates:
1074 **
1075 ** - *zRevUuid = the fully-expanded value of zRev (owned by the
1076 ** caller). zRevUuid may be NULL.
1077 **
1078 ** - *vid = the RID of zRevUuid. May not be NULL.
1079 **
1080 ** - *frid = the RID of zFilename's blob content. May not be NULL.
1081 **
1082 ** Returns 0 if the given file is not in the given checkin or if
1083 ** fileedit_ajax_check_filename() fails, else returns true. If it
1084 ** returns false, it queues up an error response and the caller must
1085 ** return immediately.
1086 */
1087 static int fileedit_ajax_setup_filerev(const char * zRev,
1088 char ** zRevUuid,
1089 int * vid,
1090 const char * zFilename,
1091 int * frid){
1092 char * zCi = 0; /* fully-resolved checkin UUID */
1093 char * zFileUuid; /* file UUID */
1094
1095 if(!fileedit_ajax_check_filename(zFilename)){
1096 return 0;
1097 }
1098 *vid = symbolic_name_to_rid(zRev, "ci");
1099 if(0==*vid){
1100 fileedit_ajax_error(404,"Cannot resolve name as a checkin: %s",
1101 zRev);
1102 return 0;
1103 }
1104 zFileUuid = fileedit_file_uuid(zFilename, *vid, 0);
1105 if(zFileUuid==0){
1106 fileedit_ajax_error(404,"Checkin does not contain file.");
1107 return 0;
1108 }
1109 zCi = rid_to_uuid(*vid);
1110 *frid = fast_uuid_to_rid(zFileUuid);
1111 fossil_free(zFileUuid);
1112 if(zRevUuid!=0){
1113 *zRevUuid = zCi;
1114 }else{
1115 fossil_free(zCi);
1116 }
1117 return 1;
1118 }
1119
1120
1121 /*
1122 ** WEBPAGE: fileedit_content
1123 **
1124 ** Query parameters:
1125 **
1126 ** file=FILENAME
1127 ** r=CHECKIN_NAME
1128 **
1129 ** User must have Write access to use this page.
1130 **
1131 ** Responds with the raw content of the given page. On error it
1132 ** produces a JSON response as documented for fileedit_ajax_error().
1133 */
1134 void fileedit_ajax_content(){
1135 const char * zFilename = PD("file",P("name"));
1136 const char * zRev = P("r");
1137 int vid, frid;
1138 Blob content = empty_blob;
1139 const char * zMime;
1140 if(!fileedit_ajax_boostrap()
1141 || !fileedit_ajax_setup_filerev(zRev, 0, &vid,
1142 zFilename, &frid)){
1143 return;
1144 }
1145 zMime = mimetype_from_name(zFilename);
1146 content_get(frid, &content);
1147 cgi_set_content_type(zMime ? zMime : "application/octet-stream");
1148 cgi_set_content(&content);
1149 }
1150
1151 /*
1152 ** WEBPAGE: fileedit_preview
1153 **
1154 ** Query parameters:
1155 **
1156 ** file=FILENAME
1157 ** render_mode=integer (FE_RENDER_xxx) (default=FE_RENDER_GUESS)
1158 ** content=text
1159 **
1160 ** User must have Write access to use this page.
1161 **
1162 ** Responds with the HTML content of the preview. On error it produces
1163 ** a JSON response as documented for fileedit_ajax_error().
1164 */
1165 void fileedit_ajax_preview(){
1166 const char * zFilename = PD("file",P("name"));
1167 const char * zContent = P("content");
1168 int renderMode = atoi(PD("render_mode","0"));
1169 int ln = atoi(PD("ln","0"));
1170 int iframeHeight = atoi(PD("iframe_height","40"));
1171 Blob content = empty_blob;
1172 if(!fileedit_ajax_boostrap()
1173 || !fileedit_ajax_check_filename(zFilename)){
1174 return;
1175 }
1176 cgi_set_content_type("text/html");
1177 blob_init(&content, zContent, -1);
1178 fileedit_render_preview(&content, zFilename,
1179 ln ? FE_PREVIEW_LINE_NUMBERS : 0,
1180 renderMode, iframeHeight);
1181 }
1182
1183 /*
1184 ** WEBPAGE: fileedit_diff
1185 **
1186 ** file=FILENAME
1187 ** sbs=integer (1=side-by-side or 0=unified)
1188 ** content=text
1189 ** r=checkin version
1190 **
1191 ** User must have Write access to use this page.
1192 **
1193 ** Responds with the HTML content of the diff. On error it produces a
1194 ** JSON response as documented for fileedit_ajax_error().
1195 */
1196 void fileedit_ajax_diff(){
1197 /*
1198 ** Reminder: we only need the filename to perform valdiation
1199 ** against fileedit_is_editable(), else this route could be
1200 ** abused to get diffs against content disallowed by the
1201 ** whitelist.
1202 */
1203 const char * zFilename = PD("file",P("name"));
1204 const char * zRev = P("r");
1205 const char * zContent = P("content");
1206 char * zRevUuid = 0;
1207 int isSbs = atoi(PD("sbs","0"));
1208 int vid, frid;
1209 Blob content = empty_blob;
1210 if(!fileedit_ajax_boostrap()
1211 || !fileedit_ajax_setup_filerev(zRev, &zRevUuid, &vid,
1212 zFilename, &frid)){
1213 return;
1214 }
1215 if(!zContent){
1216 zContent = "";
1217 }
1218 cgi_set_content_type("text/html");
1219 blob_init(&content, zContent, -1);
1220 fileedit_render_diff(&content, frid, zRevUuid, isSbs);
1221 fossil_free(zRevUuid);
1222 blob_reset(&content);
1223 }
1224
1225
1226 /*
1227 ** Emits utility script code specific to the /fileedit page.
1228 */
1229 static void fileedit_emit_page_script(){
1230 style_emit_script_tag(0);
1231 CX("%s\n", builtin_text("fossil.page.fileedit.js"));
1232 style_emit_script_tag(1);
1233 }
1234
1235 /*
1236 ** WEBPAGE: fileedit
1237 **
1238 ** EXPERIMENTAL and subject to change and removal at any time. The goal
@@ -1171,33 +1278,12 @@
1278 combined into a single JS
1279 function call, thus each
1280 entry must end with a
1281 semicolon. */
1282 Stmt stmt = empty_Stmt;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1283 #define fail(EXPR) blob_appendf EXPR; goto end_footer
1284
 
1285 login_check_credentials();
1286 if( !g.perm.Write ){
1287 login_needed(g.anon.Write);
1288 return;
1289 }
@@ -1352,34 +1438,39 @@
1438 cimi.pMfOut = 0;
1439 blob_reset(&manifest);
1440 break;
1441 }
1442
1443 CX("<div id='fossil-status-bar'>Async. status messages will go "
1444 "here.</div>\n");
1445 CX("<h1>Editing:</h1>");
1446 CX("<p class='fileedit-hint'>");
1447 CX("File: "
1448 "[<a id='finfo-link' href='#'>info</a>] "
1449 /* %R/finfo?name=%T&m=%!S */
1450 "<code id='finfo-file-name'>(loading)</code><br>");
1451 CX("Checkin Version: "
1452 "[<a id='r-link' href='#'>info</a>] "
1453 /* %R/info/%!S */
1454 "<code id='r-label'>(loading...)</code><br>"
1455 );
1456 CX("Permalink: <code>"
1457 "<a id='permalink' href='#'>(loading...)</a></code><br>"
 
1458 "(Clicking the permalink will reload the page and discard "
1459 "all edits!)",
1460 zFilename, cimi.zParentUuid,
1461 zFilename, cimi.zParentUuid);
1462 CX("</p>");
1463 CX("<p>This page is <em>NEW AND EXPERIMENTAL</em>. "
1464 "USE AT YOUR OWN RISK, preferably on a test "
1465 "repo.</p>\n");
1466
1467 CX("<form action='#' method='POST' "
1468 "class='fileedit' id='fileedit-form' "
1469 "onsubmit='function(e){"
1470 "e.preventDefault(); e.stopPropagation(); return false;"
1471 "}'>\n");
1472
1473 /******* Hidden fields *******/
1474 CX("<input type='hidden' name='r' value='%s'>",
1475 cimi.zParentUuid);
1476 CX("<input type='hidden' name='file' value='%T'>",
@@ -1387,16 +1478,11 @@
1478
1479 /******* Content *******/
1480 CX("<h3>File Content</h3>\n");
1481 CX("<textarea name='content' id='fileedit-content' "
1482 "rows='20' cols='80'>");
1483 CX("Loading...");
 
 
 
 
 
1484 CX("</textarea>\n");
1485 /******* Flags/options *******/
1486 CX("<fieldset class='fileedit-options' id='options'>"
1487 "<legend>Options</legend><div>"
1488 /* Chrome does not sanely lay out multiple
@@ -1459,108 +1545,60 @@
1545 CX("</div></fieldset>\n");
1546
1547 /******* Buttons *******/
1548 CX("<a id='buttons'></a>");
1549 CX("<fieldset class='fileedit-options'>"
1550 "<legend>Ask the server to...</legend><div>");
1551 CX("<button id='fileedit-btn-commit'>Commit</button>");
1552 CX("<button id='fileedit-btn-preview'>Preview</button>");
 
 
1553 {
1554 /* Preview rendering mode selection... */
1555 previewRenderMode = atoi(PD("preview_render_mode","0"));
1556 previewRenderMode = fileedit_render_mode_for_mimetype(zFileMime);
 
 
1557 style_select_list_int("preview_render_mode",
1558 "Preview Mode",
1559 "Preview mode format.",
1560 previewRenderMode,
1561 "Guess", FE_RENDER_GUESS,
1562 "Wiki/Markdown", FE_RENDER_WIKI,
1563 "HTML (iframe)", FE_RENDER_HTML,
1564 "Plain Text", FE_RENDER_PLAIN_TEXT,
1565 NULL);
1566 previewHtmlHeight = atoi(PD("preview_html_ems","0"));
1567 if(!previewHtmlHeight){
1568 previewHtmlHeight = 40;
1569 }
1570 /* Allow selection of HTML preview iframe height */
1571 style_select_list_int("preview_html_ems",
1572 "HTML Preview IFrame Height (EMs)",
1573 "Height (in EMs) of the iframe used for "
1574 "HTML preview",
1575 previewHtmlHeight,
1576 "", 20, "", 40,
1577 "", 60, "", 80,
1578 "", 100, NULL);
1579 style_labeled_checkbox("preview_ln",
1580 "Add line numbers to plain-text previews?",
1581 "1",
1582 "If on, plain-text files (only) will get "
1583 "line numbers added to the preview.",
1584 previewLn);
1585 }
1586 CX("<button id='fileedit-btn-diffsbs'>Diff (SBS)</button>");
1587 CX("<button id='fileedit-btn-diffu'>Diff (Unified)</button>");
 
 
 
 
 
 
 
 
1588 CX("</div></fieldset>");
1589
1590 /******* End of form *******/
1591 CX("</form>\n");
1592
1593 CX("<div id='ajax-target'></div>"
1594 /* this is where preview/diff go */);
1595
 
 
 
 
 
 
 
 
 
 
 
 
 
1596 /* Dynamically populate the editor... */
1597 blob_appendf(&endScript,
1598 "fossil.page.loadFile('%j','%j');",
1599 zFilename, cimi.zParentUuid);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1600
1601 end_footer:
1602 zContent = 0;
1603 fossil_free(zFileUuid);
1604 if(stmt.pStmt){
@@ -1573,10 +1611,12 @@
1611 CX("%b",&submitResult);
1612 }
1613 blob_reset(&submitResult);
1614 blob_reset(&err);
1615 CheckinMiniInfo_cleanup(&cimi);
1616 style_emit_script_fetch();
1617 fileedit_emit_page_script();
1618 if(blob_size(&endScript)>0){
1619 style_emit_script_tag(0);
1620 CX("(function(){\n");
1621 CX("try{\n%b\n}catch(e){console.error('Exception:',e)}\n",
1622 &endScript);
1623
1624 DDED src/fossil.bootstrap.js
1625 DDED src/fossil.fetch.js
1626 DDED src/fossil.page.fileedit.js
--- a/src/fossil.bootstrap.js
+++ b/src/fossil.bootstrap.js
@@ -0,0 +1,30 @@
1
+"use strict";
2
+
3
+ the DOM element must supwindowvalu
4
+ loaded after stylu})(wi,"u})(window);
5
+
6
+ r}) ID,
7
+ the DOM element must sup"u})(wi,"u})(window) element, WIcont support .valu
8
+ loai,"u})(window);
9
+
10
+ r}) ID,
11
+ the DOM element must support .v"u})(wi,"u})(window);
12
+
13
+ r}) ID,
14
+ the DOM element must supporc value. Explained below.
15
+
16
+ ndow);
17
+
18
+ r}) ID,
19
+ the DOM elementust support .valu
20
+ loaded after style.c: target "previewer"
21
+ ** element.
22
+**
23
+ element, WIconten*/
24
+windowethodNamesce[f-data
25
+
26
+ <button id='t data-f-preview-from='#fileeditif(tgt){
27
+loaded aftertyle.c: target "previewer"
28
+ }
29
+ else{ r}) ID,
30
+ th'Fossil status:'})(windiew-via: to ,msg ||msg ||
--- a/src/fossil.bootstrap.js
+++ b/src/fossil.bootstrap.js
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/src/fossil.bootstrap.js
+++ b/src/fossil.bootstrap.js
@@ -0,0 +1,30 @@
1 "use strict";
2
3 the DOM element must supwindowvalu
4 loaded after stylu})(wi,"u})(window);
5
6 r}) ID,
7 the DOM element must sup"u})(wi,"u})(window) element, WIcont support .valu
8 loai,"u})(window);
9
10 r}) ID,
11 the DOM element must support .v"u})(wi,"u})(window);
12
13 r}) ID,
14 the DOM element must supporc value. Explained below.
15
16 ndow);
17
18 r}) ID,
19 the DOM elementust support .valu
20 loaded after style.c: target "previewer"
21 ** element.
22 **
23 element, WIconten*/
24 windowethodNamesce[f-data
25
26 <button id='t data-f-preview-from='#fileeditif(tgt){
27 loaded aftertyle.c: target "previewer"
28 }
29 else{ r}) ID,
30 th'Fossil status:'})(windiew-via: to ,msg ||msg ||
--- a/src/fossil.fetch.js
+++ b/src/fossil.fetch.js
@@ -0,0 +1,18 @@
1
+r onload() handler tDocumented in style.c:style_emit_script_fetch(). Requive already been set up named "error" is considered to be a server-generated
2
+ error.
3
+
4
+ suffers a connectt while awaiting a rfossil.error("Ajax error!");payloado t. By default XHR2name='timeout',
5
+ "this".
6
+ based on the payload type.oner
7
+ caught and silently ignoreds**()
8
+ will get _two_ callback cal**sponse
9
+ followed immediately by an ontimeout() response. Error objects
10
+ .'. If it is an object, all of its properties get
11
+ converted to that form. Either way, the parameters get appended to
12
+ the URL before submitting the requignore: not JSON*/}
13
+ }
14
+ }{
15
+ opt = {};
16
+ }{
17
+ opt={onload:opt};
18
+ }if(!form. Either way, the paootPath+uri],opt.urlParams){
--- a/src/fossil.fetch.js
+++ b/src/fossil.fetch.js
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/src/fossil.fetch.js
+++ b/src/fossil.fetch.js
@@ -0,0 +1,18 @@
1 r onload() handler tDocumented in style.c:style_emit_script_fetch(). Requive already been set up named "error" is considered to be a server-generated
2 error.
3
4 suffers a connectt while awaiting a rfossil.error("Ajax error!");payloado t. By default XHR2name='timeout',
5 "this".
6 based on the payload type.oner
7 caught and silently ignoreds**()
8 will get _two_ callback cal**sponse
9 followed immediately by an ontimeout() response. Error objects
10 .'. If it is an object, all of its properties get
11 converted to that form. Either way, the parameters get appended to
12 the URL before submitting the requignore: not JSON*/}
13 }
14 }{
15 opt = {};
16 }{
17 opt={onload:opt};
18 }if(!form. Either way, the paootPath+uri],opt.urlParams){
--- a/src/fossil.page.fileedit.js
+++ b/src/fossil.page.fileedit.js
@@ -0,0 +1,68 @@
1
+(function(label,
2
+ ticks: 3odP.e.taEditor.className = e.targetP.e.taEditor,rfile, r:rShortr{f bootstrappin
3
+ Shortr{file:file,rpreview.');'R;
4
+ confirmText:
5
+ossil.page', this.finfo.rodif(P.eText: label,
6
+ ticks: 3odP.e.taetor.classN("#filbtnR;
7
+ confirmTeodif(P.e: E("#fileedit-btn-diffsbs"),
8
+ //btnDiffU: E("#fileedit-btn-diffu"}, form: E('#fileedit-formfalse }, false)F.pageF.pageF.pageprevieweview.');',th
9
+ confirmText:odif(P.e.btnR;
10
+ confirmode'),
11
+ mitting every
12
+ label,
13
+ ticks:P.e.btnR;
14
+ confirmText: label,
15
+ ticks: 3odP.e.taEditor.className = e.tText: label,
16
+ ticks: 3odP.);
17
+ if('html'!==name) label,
18
+ "submit else unlabel,
19
+ "submit", function(e) {
20
+ e.target.checkValidity();
21
+ /* All of this is needed to keep the form from submitting every
22
+ }, false);ame = e.targetP.e.taEditorr',r', this.finfo.rodif(P.e.btnR;
23
+ odif(P.e.btnR;
24
+ confirmText: label,
25
+ ticks: 3odP.e.taEditor.className = e.targeform')firmText: label,
26
+ ticks: 3odP.stopEvent(e);
27
+ stopEvent(e)file, r;
28
+ confirmText: label,
29
+ ticks: 3odP.e.taEditor.className = e.tText: label,
30
+ ticks: 3odP.);
31
+ if('html'!==name) label,
32
+ "submit else unlabel,
33
+ "submit", function(e) {
34
+ e.target.checkValidity();
35
+ /* All of this is needed to keep the form from submitting every
36
+ }, false);ame = e.targetP.e.taEditorr',r', this.finfo.rodif(P.e.btnR;
37
+ odif(P.e.btnR;
38
+ confirmText: label,
39
+ ticks: 3odP.e.taEditor.className = e.targeform')firmText: label,
40
+ ticks: 3odP.stopEvent(e);
41
+ stopEvent(e)file, r:rShortr{file:file,rpreview.');',this.finfo.fildif(P.e.btnR;
42
+ confirmText: label,
43
+ ticks: 3odP.e.taEditor.className = e.targetP.e.taEditor confi',this.finfo.);
44
+ target.innerTpreviewe = e.targetP.e.todif(P.e.btnR;
45
+ F.pageF.pageF.page})(window.fossil);
46
+if(c)else.taEditor,rfconsole.debug('abel,
47
+ ticks: (fun(function(label,
48
+(function(label,
49
+ tick', this.finfo.rodfileodif(PP.e.taEditor.className = e.targetP.e.taEditor,rfile, r:rShortr{file:file,rpreview',this.finfo.fildif(P.e.relevantticks: 3odP.e.taEditor.classNam;
50
+ windowr', this.finfo.rodif(P.e.const P = F.page; label,
51
+ ticks: 3odP.e.taEditor.className =P.e.formfirmText: label,
52
+ "submit", function(e) {
53
+ e.target.checkValidity();
54
+ /* All of this is needed to keep the form from submitting every
55
+ time any button in the form is clicked: */
56
+ e.preventDefault();
57
+ is.finfo.rodif(P.e.const P = .stopEvent(e:rShortr{file:file,rpreview.')rtr{file:file,rpreview.');',this.finfo.fildif(P.e.btnR;
58
+ confiText: label,
59
+ ticks: 3odP.e.taEP.e.taEditor time antaEditor confi',this.finfo.);
60
+ ntDefault();
61
+ is.finfFo(P.e.btnR;
62
+ odif(P.e.btnR;
63
+ confirmText: label,
64
+ ticks: 3odP.e.taEditor.className = e.targeform')firmText: label,
65
+ ticks: 3odP.stopEvent(e);
66
+ stopEvent(e)file, r:rShortr{file:file,rpreview.');',this.finfo.fildif(P.e.btnR;
67
+ confirmText: label,
68
+ ticks: 3odP.e.taEditor.classNa})();
--- a/src/fossil.page.fileedit.js
+++ b/src/fossil.page.fileedit.js
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
--- a/src/fossil.page.fileedit.js
+++ b/src/fossil.page.fileedit.js
@@ -0,0 +1,68 @@
1 (function(label,
2 ticks: 3odP.e.taEditor.className = e.targetP.e.taEditor,rfile, r:rShortr{f bootstrappin
3 Shortr{file:file,rpreview.');'R;
4 confirmText:
5 ossil.page', this.finfo.rodif(P.eText: label,
6 ticks: 3odP.e.taetor.classN("#filbtnR;
7 confirmTeodif(P.e: E("#fileedit-btn-diffsbs"),
8 //btnDiffU: E("#fileedit-btn-diffu"}, form: E('#fileedit-formfalse }, false)F.pageF.pageF.pageprevieweview.');',th
9 confirmText:odif(P.e.btnR;
10 confirmode'),
11 mitting every
12 label,
13 ticks:P.e.btnR;
14 confirmText: label,
15 ticks: 3odP.e.taEditor.className = e.tText: label,
16 ticks: 3odP.);
17 if('html'!==name) label,
18 "submit else unlabel,
19 "submit", function(e) {
20 e.target.checkValidity();
21 /* All of this is needed to keep the form from submitting every
22 }, false);ame = e.targetP.e.taEditorr',r', this.finfo.rodif(P.e.btnR;
23 odif(P.e.btnR;
24 confirmText: label,
25 ticks: 3odP.e.taEditor.className = e.targeform')firmText: label,
26 ticks: 3odP.stopEvent(e);
27 stopEvent(e)file, r;
28 confirmText: label,
29 ticks: 3odP.e.taEditor.className = e.tText: label,
30 ticks: 3odP.);
31 if('html'!==name) label,
32 "submit else unlabel,
33 "submit", function(e) {
34 e.target.checkValidity();
35 /* All of this is needed to keep the form from submitting every
36 }, false);ame = e.targetP.e.taEditorr',r', this.finfo.rodif(P.e.btnR;
37 odif(P.e.btnR;
38 confirmText: label,
39 ticks: 3odP.e.taEditor.className = e.targeform')firmText: label,
40 ticks: 3odP.stopEvent(e);
41 stopEvent(e)file, r:rShortr{file:file,rpreview.');',this.finfo.fildif(P.e.btnR;
42 confirmText: label,
43 ticks: 3odP.e.taEditor.className = e.targetP.e.taEditor confi',this.finfo.);
44 target.innerTpreviewe = e.targetP.e.todif(P.e.btnR;
45 F.pageF.pageF.page})(window.fossil);
46 if(c)else.taEditor,rfconsole.debug('abel,
47 ticks: (fun(function(label,
48 (function(label,
49 tick', this.finfo.rodfileodif(PP.e.taEditor.className = e.targetP.e.taEditor,rfile, r:rShortr{file:file,rpreview',this.finfo.fildif(P.e.relevantticks: 3odP.e.taEditor.classNam;
50 windowr', this.finfo.rodif(P.e.const P = F.page; label,
51 ticks: 3odP.e.taEditor.className =P.e.formfirmText: label,
52 "submit", function(e) {
53 e.target.checkValidity();
54 /* All of this is needed to keep the form from submitting every
55 time any button in the form is clicked: */
56 e.preventDefault();
57 is.finfo.rodif(P.e.const P = .stopEvent(e:rShortr{file:file,rpreview.')rtr{file:file,rpreview.');',this.finfo.fildif(P.e.btnR;
58 confiText: label,
59 ticks: 3odP.e.taEP.e.taEditor time antaEditor confi',this.finfo.);
60 ntDefault();
61 is.finfFo(P.e.btnR;
62 odif(P.e.btnR;
63 confirmText: label,
64 ticks: 3odP.e.taEditor.className = e.targeform')firmText: label,
65 ticks: 3odP.stopEvent(e);
66 stopEvent(e)file, r:rShortr{file:file,rpreview.');',this.finfo.fildif(P.e.btnR;
67 confirmText: label,
68 ticks: 3odP.e.taEditor.classNa})();
--- src/main.mk
+++ src/main.mk
@@ -218,10 +218,13 @@
218218
$(SRCDIR)/accordion.js \
219219
$(SRCDIR)/ci_edit.js \
220220
$(SRCDIR)/copybtn.js \
221221
$(SRCDIR)/diff.tcl \
222222
$(SRCDIR)/forum.js \
223
+ $(SRCDIR)/fossil.bootstrap.js \
224
+ $(SRCDIR)/fossil.fetch.js \
225
+ $(SRCDIR)/fossil.page.fileedit.js \
223226
$(SRCDIR)/graph.js \
224227
$(SRCDIR)/href.js \
225228
$(SRCDIR)/login.js \
226229
$(SRCDIR)/markdown.md \
227230
$(SRCDIR)/menu.js \
228231
--- src/main.mk
+++ src/main.mk
@@ -218,10 +218,13 @@
218 $(SRCDIR)/accordion.js \
219 $(SRCDIR)/ci_edit.js \
220 $(SRCDIR)/copybtn.js \
221 $(SRCDIR)/diff.tcl \
222 $(SRCDIR)/forum.js \
 
 
 
223 $(SRCDIR)/graph.js \
224 $(SRCDIR)/href.js \
225 $(SRCDIR)/login.js \
226 $(SRCDIR)/markdown.md \
227 $(SRCDIR)/menu.js \
228
--- src/main.mk
+++ src/main.mk
@@ -218,10 +218,13 @@
218 $(SRCDIR)/accordion.js \
219 $(SRCDIR)/ci_edit.js \
220 $(SRCDIR)/copybtn.js \
221 $(SRCDIR)/diff.tcl \
222 $(SRCDIR)/forum.js \
223 $(SRCDIR)/fossil.bootstrap.js \
224 $(SRCDIR)/fossil.fetch.js \
225 $(SRCDIR)/fossil.page.fileedit.js \
226 $(SRCDIR)/graph.js \
227 $(SRCDIR)/href.js \
228 $(SRCDIR)/login.js \
229 $(SRCDIR)/markdown.md \
230 $(SRCDIR)/menu.js \
231
+86 -5
--- src/style.c
+++ src/style.c
@@ -1420,14 +1420,95 @@
14201420
*/
14211421
void style_emit_script_tag(int phase){
14221422
static int once = 0;
14231423
if(0==phase){
14241424
CX("<script nonce='%s'>", style_nonce());
1425
- if(0==once){
1426
- once = 1;
1427
- CX("\nif(!window.fossil) window.fossil={};\n");
1428
- CX("window.fossil.version = %Q;\n", get_version());
1429
- }
1425
+ if(0==once){
1426
+ once = 1;
1427
+ /* Set up the generic/app-agnostic parts of window.fossil */
1428
+ CX("(function(){\n");
1429
+ CX("\nif(!window.fossil) window.fossil={};\n");
1430
+ CX("window.fossil.version = '%j';\n", get_version());
1431
+ /* fossil.rootPath is the top-most CGI/server path,
1432
+ including a trailing slash. */
1433
+ CX("window.fossil.rootPath = '%j'+'/';\n", g.zTop);
1434
+ /*
1435
+ ** fossil.page holds info about the current page. This is
1436
+ ** also where the current page "should" store any of its
1437
+ ** own page-specific state.
1438
+ */
1439
+ CX("window.fossil.page = {"
1440
+ "page:'%T'"
1441
+ "};\n", g.zPath);
1442
+ CX("%s\n", builtin_text("fossil.bootstrap.js"));
1443
+ CX("})();\n");
1444
+ }
14301445
}else{
14311446
CX("</script>\n");
14321447
}
14331448
}
1449
+
1450
+
1451
+/*
1452
+** The *FIRST* time this is called, it emits a JS script block,
1453
+** including tags, which defines window.fossil.fetch(), which works
1454
+** similarly (not identically) to the not-quite-ubiquitous global
1455
+** fetch(). It calls style_emit_script_tag(), which may inject
1456
+** other JS bootstrapping bits.
1457
+**
1458
+** JS usages:
1459
+**
1460
+** fossilFetch( URI, onLoadCallback );
1461
+**
1462
+** fossilFetch( URI, optionsObject );
1463
+**
1464
+** Noting that URI must be relative to the top of the repository and
1465
+** should not start with a slash (if it does, it is stripped). It gets
1466
+** the equivalent of "%R/" prepended to it.
1467
+**
1468
+** The optionsObject may be an onload callback or an object with any
1469
+** of these properties:
1470
+**
1471
+** - onload: callback(responseData) (default = output response to
1472
+** the console).
1473
+**
1474
+** - onerror: callback(XHR onload event | exception)
1475
+** (default = output event or exception to the console).
1476
+**
1477
+** - method: 'POST' | 'GET' (default = 'GET')
1478
+**
1479
+** - payload: anything acceptable by XHR2.send(ARG) (DOMString,
1480
+** Document, FormData, Blob, File, ArrayBuffer), or a plain object
1481
+** or array, either of which gets JSON.stringify()'d. If set then
1482
+** the method is automatically set to 'POST'. If an object/array is
1483
+** converted to JSON, the content-type is set to 'application/json'.
1484
+** By default XHR2 will set the content type based on the payload
1485
+** type.
1486
+**
1487
+** - contentType: Optional request content type when POSTing. Ignored
1488
+** if the method is not 'POST'.
1489
+**
1490
+** - responseType: optional string. One of ("text", "arraybuffer",
1491
+** "blob", or "document") (as specified by XHR2). Default = "text".
1492
+** As an extension, it supports "json", which tells it that the
1493
+** response is expected to be text and that it should be
1494
+** JSON.parse()d before passing it on to the onload() callback. In
1495
+** this case, if the payload property is an object/array.
1496
+**
1497
+** - urlParams: string|object. If a string, it is assumed to be a
1498
+** URI-encoded list of params in the form "key1=val1&key2=val2...",
1499
+** with NO leading '?'. If it is an object, all of its properties
1500
+** get converted to that form. Either way, the parameters get
1501
+** appended to the URL.
1502
+**
1503
+** Returns this object, noting that the XHR request is still in
1504
+** transit (or has yet to be sent) when that happens.
1505
+*/
1506
+void style_emit_script_fetch(){
1507
+ static int once = 0;
1508
+ if(0==once){
1509
+ once = 1;
1510
+ style_emit_script_tag(0);
1511
+ CX("%s", builtin_text("fossil.fetch.js"));
1512
+ style_emit_script_tag(1);
1513
+ }
1514
+}
14341515
--- src/style.c
+++ src/style.c
@@ -1420,14 +1420,95 @@
1420 */
1421 void style_emit_script_tag(int phase){
1422 static int once = 0;
1423 if(0==phase){
1424 CX("<script nonce='%s'>", style_nonce());
1425 if(0==once){
1426 once = 1;
1427 CX("\nif(!window.fossil) window.fossil={};\n");
1428 CX("window.fossil.version = %Q;\n", get_version());
1429 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1430 }else{
1431 CX("</script>\n");
1432 }
1433 }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1434
--- src/style.c
+++ src/style.c
@@ -1420,14 +1420,95 @@
1420 */
1421 void style_emit_script_tag(int phase){
1422 static int once = 0;
1423 if(0==phase){
1424 CX("<script nonce='%s'>", style_nonce());
1425 if(0==once){
1426 once = 1;
1427 /* Set up the generic/app-agnostic parts of window.fossil */
1428 CX("(function(){\n");
1429 CX("\nif(!window.fossil) window.fossil={};\n");
1430 CX("window.fossil.version = '%j';\n", get_version());
1431 /* fossil.rootPath is the top-most CGI/server path,
1432 including a trailing slash. */
1433 CX("window.fossil.rootPath = '%j'+'/';\n", g.zTop);
1434 /*
1435 ** fossil.page holds info about the current page. This is
1436 ** also where the current page "should" store any of its
1437 ** own page-specific state.
1438 */
1439 CX("window.fossil.page = {"
1440 "page:'%T'"
1441 "};\n", g.zPath);
1442 CX("%s\n", builtin_text("fossil.bootstrap.js"));
1443 CX("})();\n");
1444 }
1445 }else{
1446 CX("</script>\n");
1447 }
1448 }
1449
1450
1451 /*
1452 ** The *FIRST* time this is called, it emits a JS script block,
1453 ** including tags, which defines window.fossil.fetch(), which works
1454 ** similarly (not identically) to the not-quite-ubiquitous global
1455 ** fetch(). It calls style_emit_script_tag(), which may inject
1456 ** other JS bootstrapping bits.
1457 **
1458 ** JS usages:
1459 **
1460 ** fossilFetch( URI, onLoadCallback );
1461 **
1462 ** fossilFetch( URI, optionsObject );
1463 **
1464 ** Noting that URI must be relative to the top of the repository and
1465 ** should not start with a slash (if it does, it is stripped). It gets
1466 ** the equivalent of "%R/" prepended to it.
1467 **
1468 ** The optionsObject may be an onload callback or an object with any
1469 ** of these properties:
1470 **
1471 ** - onload: callback(responseData) (default = output response to
1472 ** the console).
1473 **
1474 ** - onerror: callback(XHR onload event | exception)
1475 ** (default = output event or exception to the console).
1476 **
1477 ** - method: 'POST' | 'GET' (default = 'GET')
1478 **
1479 ** - payload: anything acceptable by XHR2.send(ARG) (DOMString,
1480 ** Document, FormData, Blob, File, ArrayBuffer), or a plain object
1481 ** or array, either of which gets JSON.stringify()'d. If set then
1482 ** the method is automatically set to 'POST'. If an object/array is
1483 ** converted to JSON, the content-type is set to 'application/json'.
1484 ** By default XHR2 will set the content type based on the payload
1485 ** type.
1486 **
1487 ** - contentType: Optional request content type when POSTing. Ignored
1488 ** if the method is not 'POST'.
1489 **
1490 ** - responseType: optional string. One of ("text", "arraybuffer",
1491 ** "blob", or "document") (as specified by XHR2). Default = "text".
1492 ** As an extension, it supports "json", which tells it that the
1493 ** response is expected to be text and that it should be
1494 ** JSON.parse()d before passing it on to the onload() callback. In
1495 ** this case, if the payload property is an object/array.
1496 **
1497 ** - urlParams: string|object. If a string, it is assumed to be a
1498 ** URI-encoded list of params in the form "key1=val1&key2=val2...",
1499 ** with NO leading '?'. If it is an object, all of its properties
1500 ** get converted to that form. Either way, the parameters get
1501 ** appended to the URL.
1502 **
1503 ** Returns this object, noting that the XHR request is still in
1504 ** transit (or has yet to be sent) when that happens.
1505 */
1506 void style_emit_script_fetch(){
1507 static int once = 0;
1508 if(0==once){
1509 once = 1;
1510 style_emit_script_tag(0);
1511 CX("%s", builtin_text("fossil.fetch.js"));
1512 style_emit_script_tag(1);
1513 }
1514 }
1515
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -640,10 +640,13 @@
640640
$(SRCDIR)/accordion.js \
641641
$(SRCDIR)/ci_edit.js \
642642
$(SRCDIR)/copybtn.js \
643643
$(SRCDIR)/diff.tcl \
644644
$(SRCDIR)/forum.js \
645
+ $(SRCDIR)/fossil.bootstrap.js \
646
+ $(SRCDIR)/fossil.fetch.js \
647
+ $(SRCDIR)/fossil.page.fileedit.js \
645648
$(SRCDIR)/graph.js \
646649
$(SRCDIR)/href.js \
647650
$(SRCDIR)/login.js \
648651
$(SRCDIR)/markdown.md \
649652
$(SRCDIR)/menu.js \
650653
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -640,10 +640,13 @@
640 $(SRCDIR)/accordion.js \
641 $(SRCDIR)/ci_edit.js \
642 $(SRCDIR)/copybtn.js \
643 $(SRCDIR)/diff.tcl \
644 $(SRCDIR)/forum.js \
 
 
 
645 $(SRCDIR)/graph.js \
646 $(SRCDIR)/href.js \
647 $(SRCDIR)/login.js \
648 $(SRCDIR)/markdown.md \
649 $(SRCDIR)/menu.js \
650
--- win/Makefile.mingw
+++ win/Makefile.mingw
@@ -640,10 +640,13 @@
640 $(SRCDIR)/accordion.js \
641 $(SRCDIR)/ci_edit.js \
642 $(SRCDIR)/copybtn.js \
643 $(SRCDIR)/diff.tcl \
644 $(SRCDIR)/forum.js \
645 $(SRCDIR)/fossil.bootstrap.js \
646 $(SRCDIR)/fossil.fetch.js \
647 $(SRCDIR)/fossil.page.fileedit.js \
648 $(SRCDIR)/graph.js \
649 $(SRCDIR)/href.js \
650 $(SRCDIR)/login.js \
651 $(SRCDIR)/markdown.md \
652 $(SRCDIR)/menu.js \
653
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -547,10 +547,13 @@
547547
$(SRCDIR)\accordion.js \
548548
$(SRCDIR)\ci_edit.js \
549549
$(SRCDIR)\copybtn.js \
550550
$(SRCDIR)\diff.tcl \
551551
$(SRCDIR)\forum.js \
552
+ $(SRCDIR)\fossil.bootstrap.js \
553
+ $(SRCDIR)\fossil.fetch.js \
554
+ $(SRCDIR)\fossil.page.fileedit.js \
552555
$(SRCDIR)\graph.js \
553556
$(SRCDIR)\href.js \
554557
$(SRCDIR)\login.js \
555558
$(SRCDIR)\markdown.md \
556559
$(SRCDIR)\menu.js \
557560
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -547,10 +547,13 @@
547 $(SRCDIR)\accordion.js \
548 $(SRCDIR)\ci_edit.js \
549 $(SRCDIR)\copybtn.js \
550 $(SRCDIR)\diff.tcl \
551 $(SRCDIR)\forum.js \
 
 
 
552 $(SRCDIR)\graph.js \
553 $(SRCDIR)\href.js \
554 $(SRCDIR)\login.js \
555 $(SRCDIR)\markdown.md \
556 $(SRCDIR)\menu.js \
557
--- win/Makefile.msc
+++ win/Makefile.msc
@@ -547,10 +547,13 @@
547 $(SRCDIR)\accordion.js \
548 $(SRCDIR)\ci_edit.js \
549 $(SRCDIR)\copybtn.js \
550 $(SRCDIR)\diff.tcl \
551 $(SRCDIR)\forum.js \
552 $(SRCDIR)\fossil.bootstrap.js \
553 $(SRCDIR)\fossil.fetch.js \
554 $(SRCDIR)\fossil.page.fileedit.js \
555 $(SRCDIR)\graph.js \
556 $(SRCDIR)\href.js \
557 $(SRCDIR)\login.js \
558 $(SRCDIR)\markdown.md \
559 $(SRCDIR)\menu.js \
560

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button