/* ** Copyright (c) 2014 D. Richard Hipp ** ** This program is free software; you can redistribute it and/or ** modify it under the terms of the Simplified BSD License (also ** known as the "2-Clause License" or "FreeBSD License".) ** This program is distributed in the hope that it will be useful, ** but without any warranty; without even the implied warranty of ** merchantability or fitness for a particular purpose. ** ** Author contact information: ** drh@hwaci.com ** http://www.hwaci.com/drh/ ** ******************************************************************************* ** ** This file contains built-in string and BLOB resources packaged as ** byte arrays. */ #include "config.h" #include "builtin.h" #include /* ** The resources provided by this file are packaged by the "mkbuiltin.c" ** utility program during the built process and stored in the ** builtin_data.h file. Include that information here: */ #include "builtin_data.h" /* ** Return the index in the aBuiltinFiles[] array for the file ** whose name is zFilename. Or return -1 if the file is not ** found. */ static int builtin_file_index(const char *zFilename){ int lwr, upr, i, c; lwr = 0; upr = count(aBuiltinFiles) - 1; while( upr>=lwr ){ i = (upr+lwr)/2; c = strcmp(aBuiltinFiles[i].zName,zFilename); if( c<0 ){ lwr = i+1; }else if( c>0 ){ upr = i-1; }else{ return i; } } return -1; } /* ** Return a pointer to built-in content */ const unsigned char *builtin_file(const char *zFilename, int *piSize){ int i = builtin_file_index(zFilename); if( i>=0 ){ if( piSize ) *piSize = aBuiltinFiles[i].nByte; return aBuiltinFiles[i].pData; }else{ if( piSize ) *piSize = 0; return 0; } } const char *builtin_text(const char *zFilename){ return (char*)builtin_file(zFilename, 0); } /* ** COMMAND: test-builtin-list ** ** If -verbose is used, it outputs a line at the end ** with the total item count and size. ** ** List the names and sizes of all built-in resources. */ void test_builtin_list(void){ int i, size = 0;; for(i=0; i for(i=0; i%z(zUrl)%h(z) } @ style_finish_page(); } /* ** COMMAND: test-builtin-get ** ** Usage: %fossil test-builtin-get NAME ?OUTPUT-FILE? */ void test_builtin_get(void){ const unsigned char *pData; int nByte; Blob x; if( g.argc!=3 && g.argc!=4 ){ usage("NAME ?OUTPUT-FILE?"); } pData = builtin_file(g.argv[2], &nByte); if( pData==0 ){ fossil_fatal("no such built-in file: [%s]", g.argv[2]); } blob_init(&x, (const char*)pData, nByte); blob_write_to_file(&x, g.argc==4 ? g.argv[3] : "-"); blob_reset(&x); } /* ** Input zList is a list of numeric identifiers for files in ** aBuiltinFiles[]. Return the concatenation of all of those files ** using mimetype zType, or as text/javascript if zType is 0. */ static void builtin_deliver_multiple_js_files( const char *zList, /* List of numeric identifiers */ const char *zType /* Override mimetype */ ){ Blob *pOut; if( zType==0 ) zType = "text/javascript"; cgi_set_content_type(zType); pOut = cgi_output_blob(); while( zList[0] ){ int i = atoi(zList); if( i>0 && i<=count(aBuiltinFiles) ){ blob_appendf(pOut, "/* %s */\n", aBuiltinFiles[i-1].zName); blob_append(pOut, (const char*)aBuiltinFiles[i-1].pData, aBuiltinFiles[i-1].nByte); } while( zList[0] && fossil_isdigit(zList[0]) ) zList++; while( zList[0] && !fossil_isdigit(zList[0]) ) zList++; } return; } /* ** WEBPAGE: builtin loadavg-exempt ** ** Return one of many built-in content files. Query parameters: ** ** name=FILENAME Return the single file whose name is FILENAME. ** mimetype=TYPE Override the mimetype in the returned file to ** be TYPE. If this query parameter is omitted ** (the usual case) then the mimetype is inferred ** from the suffix on FILENAME ** m=IDLIST IDLIST is a comma-separated list of integers ** that specify multiple javascript files to be ** concatenated and returned all at once. ** id=UNIQUEID Version number of the "builtin" files. Used ** for cache control only. ** ** At least one of the name= or m= query parameters must be present. ** ** If the id= query parameter is present, then Fossil assumes that the ** result is immutable and sets a very large cache retention time (1 year). */ void builtin_webpage(void){ Blob out; const char *zName = P("name"); const char *zContent = 0; int nContent = 0; const char *zId = P("id"); const char *zType = P("mimetype"); int nId; if( zName ) zContent = (const char *)builtin_file(zName, &nContent); if( zContent==0 ){ const char *zM = P("m"); if( zM ){ if( zId && (nId = (int)strlen(zId))>=8 && strncmp(zId,fossil_exe_id(),nId)==0 ){ g.isConst = 1; } etag_check(0,0); builtin_deliver_multiple_js_files(zM, zType); return; } cgi_set_status(404, "Not Found"); @ File "%h(zName)" not found return; } if( zType==0 ){ if( sqlite3_strglob("*.js", zName)==0 ){ zType = "text/javascript"; }else{ zType = mimetype_from_name(zName); } } cgi_set_content_type(zType); if( zId && (nId = (int)strlen(zId))>=8 && strncmp(zId,fossil_exe_id(),nId)==0 ){ g.isConst = 1; } etag_check(0,0); blob_init(&out, zContent, nContent); cgi_set_content(&out); } /* Variables controlling the JS cache. */ static struct { int aReq[30]; /* Indexes of all requested built-in JS files */ int nReq; /* Number of slots in aReq[] currently used */ int nSent; /* Number of slots in aReq[] fulfilled */ int eDelivery; /* Delivery mechanism */ } builtin; #if INTERFACE /* Various delivery mechanisms. The 0 option is the default. ** MAINTENANCE NOTE: Review/update the builtin_set_js_delivery_mode() and ** builtin_get_js_delivery_mode_name() functions if values are changed/added. */ #define JS_INLINE 0 /* inline, batched together at end of file */ #define JS_SEPARATE 1 /* Separate HTTP request for each JS file */ #define JS_BUNDLED 2 /* One HTTP request to load all JS files */ /* concatenated together into a bundle */ #endif /* INTERFACE */ /* ** The argument is a request to change the javascript delivery mode. ** The argument is a string which is a command-line option or CGI ** parameter. Try to match it against one of the delivery options ** and set things up accordingly. Throw an error if no match unless ** bSilent is true. */ void builtin_set_js_delivery_mode(const char *zMode, int bSilent){ if( zMode==0 ) return; if( strcmp(zMode, "inline")==0 ){ builtin.eDelivery = JS_INLINE; }else if( strcmp(zMode, "separate")==0 ){ builtin.eDelivery = JS_SEPARATE; }else if( strcmp(zMode, "bundled")==0 ){ builtin.eDelivery = JS_BUNDLED; }else if( !bSilent ){ fossil_fatal("unknown javascript delivery mode \"%s\" - should be" " one of: inline separate bundled", zMode); } } /* ** Returns the current JS delivery mode: one of JS_INLINE, ** JS_SEPARATE, JS_BUNDLED. */ int builtin_get_js_delivery_mode(void){ return builtin.eDelivery; } /* ** Returns the name of the current JS delivery mode for reuse with the --jsmode ** option, i.e. the other way around than builtin_set_js_delivery_mode(). */ const char *builtin_get_js_delivery_mode_name(void){ switch( builtin.eDelivery ){ case JS_SEPARATE: { return "separate"; } case JS_BUNDLED: { return "bundled"; } case JS_INLINE: /*FALLTHROUGH*/ default: { return "inline"; } } } /* ** The caller wants the Javascript file named by zFilename to be ** included in the generated page. Add the file to the queue of ** requested javascript resources, if it is not there already. ** ** The current implementation queues the file to be included in the ** output later. However, the caller should not depend on that ** behavior. In the future, this routine might decide to insert ** the requested javascript inline, immedaitely, or to insert ** a \n"); break; } case JS_BUNDLED: { if( builtin.nSent+1\n", blob_str(&aList)+1, fossil_exe_id()); blob_reset(&aList); break; } /* If there is only one JS file, fall through into the ** JS_SEPARATE case below. */ /*FALLTHROUGH*/ } case JS_SEPARATE: { /* Each JS file as a separate resource */ while( builtin.nSent\n", aBuiltinFiles[i].zName, fossil_exe_id()); } break; } } } /***************************************************************************** ** A virtual table for accessing the information in aBuiltinFiles[]. */ /* builtinVtab_vtab is a subclass of sqlite3_vtab which is ** underlying representation of the virtual table */ typedef struct builtinVtab_vtab builtinVtab_vtab; struct builtinVtab_vtab { sqlite3_vtab base; /* Base class - must be first */ /* Add new fields here, as necessary */ }; /* builtinVtab_cursor is a subclass of sqlite3_vtab_cursor which will ** serve as the underlying representation of a cursor that scans ** over rows of the result */ typedef struct builtinVtab_cursor builtinVtab_cursor; struct builtinVtab_cursor { sqlite3_vtab_cursor base; /* Base class - must be first */ /* Insert new fields here. For this builtinVtab we only keep track ** of the rowid */ sqlite3_int64 iRowid; /* The rowid */ }; /* ** The builtinVtabConnect() method is invoked to create a new ** builtin virtual table. ** ** Think of this routine as the constructor for builtinVtab_vtab objects. ** ** All this routine needs to do is: ** ** (1) Allocate the builtinVtab_vtab object and initialize all fields. ** ** (2) Tell SQLite (via the sqlite3_declare_vtab() interface) what the ** result set of queries against the virtual table will look like. */ static int builtinVtabConnect( sqlite3 *db, void *pAux, int argc, const char *const*argv, sqlite3_vtab **ppVtab, char **pzErr ){ builtinVtab_vtab *pNew; int rc; rc = sqlite3_declare_vtab(db, "CREATE TABLE x(name,size,data)" ); if( rc==SQLITE_OK ){ pNew = sqlite3_malloc( sizeof(*pNew) ); *ppVtab = (sqlite3_vtab*)pNew; if( pNew==0 ) return SQLITE_NOMEM; memset(pNew, 0, sizeof(*pNew)); } return rc; } /* ** This method is the destructor for builtinVtab_vtab objects. */ static int builtinVtabDisconnect(sqlite3_vtab *pVtab){ builtinVtab_vtab *p = (builtinVtab_vtab*)pVtab; sqlite3_free(p); return SQLITE_OK; } /* ** Constructor for a new builtinVtab_cursor object. */ static int builtinVtabOpen(sqlite3_vtab *p, sqlite3_vtab_cursor **ppCursor){ builtinVtab_cursor *pCur; pCur = sqlite3_malloc( sizeof(*pCur) ); if( pCur==0 ) return SQLITE_NOMEM; memset(pCur, 0, sizeof(*pCur)); *ppCursor = &pCur->base; return SQLITE_OK; } /* ** Destructor for a builtinVtab_cursor. */ static int builtinVtabClose(sqlite3_vtab_cursor *cur){ builtinVtab_cursor *pCur = (builtinVtab_cursor*)cur; sqlite3_free(pCur); return SQLITE_OK; } /* ** Advance a builtinVtab_cursor to its next row of output. */ static int builtinVtabNext(sqlite3_vtab_cursor *cur){ builtinVtab_cursor *pCur = (builtinVtab_cursor*)cur; pCur->iRowid++; return SQLITE_OK; } /* ** Return values of columns for the row at which the builtinVtab_cursor ** is currently pointing. */ static int builtinVtabColumn( sqlite3_vtab_cursor *cur, /* The cursor */ sqlite3_context *ctx, /* First argument to sqlite3_result_...() */ int i /* Which column to return */ ){ builtinVtab_cursor *pCur = (builtinVtab_cursor*)cur; const struct BuiltinFileTable *pFile = aBuiltinFiles + pCur->iRowid - 1; switch( i ){ case 0: /* name */ sqlite3_result_text(ctx, pFile->zName, -1, SQLITE_STATIC); break; case 1: /* size */ sqlite3_result_int(ctx, pFile->nByte); break; case 2: /* data */ sqlite3_result_blob(ctx, pFile->pData, pFile->nByte, SQLITE_STATIC); break; } return SQLITE_OK; } /* ** Return the rowid for the current row. In this implementation, the ** rowid is the same as the output value. */ static int builtinVtabRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){ builtinVtab_cursor *pCur = (builtinVtab_cursor*)cur; *pRowid = pCur->iRowid; return SQLITE_OK; } /* ** Return TRUE if the cursor has been moved off of the last ** row of output. */ static int builtinVtabEof(sqlite3_vtab_cursor *cur){ builtinVtab_cursor *pCur = (builtinVtab_cursor*)cur; return pCur->iRowid>count(aBuiltinFiles); } /* ** This method is called to "rewind" the builtinVtab_cursor object back ** to the first row of output. This method is always called at least ** once prior to any call to builtinVtabColumn() or builtinVtabRowid() or ** builtinVtabEof(). */ static int builtinVtabFilter( sqlite3_vtab_cursor *pVtabCursor, int idxNum, const char *idxStr, int argc, sqlite3_value **argv ){ builtinVtab_cursor *pCur = (builtinVtab_cursor *)pVtabCursor; pCur->iRowid = 1; return SQLITE_OK; } /* ** SQLite will invoke this method one or more times while planning a query ** that uses the virtual table. This routine needs to create ** a query plan for each invocation and compute an estimated cost for that ** plan. */ static int builtinVtabBestIndex( sqlite3_vtab *tab, sqlite3_index_info *pIdxInfo ){ pIdxInfo->estimatedCost = (double)count(aBuiltinFiles); pIdxInfo->estimatedRows = count(aBuiltinFiles); return SQLITE_OK; } /* ** This following structure defines all the methods for the ** virtual table. */ static sqlite3_module builtinVtabModule = { /* iVersion */ 0, /* xCreate */ 0, /* The builtin vtab is eponymous and read-only */ /* xConnect */ builtinVtabConnect, /* xBestIndex */ builtinVtabBestIndex, /* xDisconnect */ builtinVtabDisconnect, /* xDestroy */ 0, /* xOpen */ builtinVtabOpen, /* xClose */ builtinVtabClose, /* xFilter */ builtinVtabFilter, /* xNext */ builtinVtabNext, /* xEof */ builtinVtabEof, /* xColumn */ builtinVtabColumn, /* xRowid */ builtinVtabRowid, /* xUpdate */ 0, /* xBegin */ 0, /* xSync */ 0, /* xCommit */ 0, /* xRollback */ 0, /* xFindMethod */ 0, /* xRename */ 0, /* xSavepoint */ 0, /* xRelease */ 0, /* xRollbackTo */ 0, /* xShadowName */ 0, /* xIntegrity */ 0 }; /* ** Register the builtin virtual table */ int builtin_vtab_register(sqlite3 *db){ int rc = sqlite3_create_module(db, "builtin", &builtinVtabModule, 0); return rc; } /* End of the builtin virtual table ******************************************************************************/ /* ** 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. ** ** It emits 2 parts: ** ** 1) window.fossil core object, some of which depends on C-level ** runtime data. That part of the script is always emitted inline. If ** addScriptTag is true then it is wrapped in its own SCRIPT tag, else ** it is assumed that the caller already opened a tag. ** ** 2) Emits the static fossil.bootstrap.js using builtin_request_js(). */ void builtin_emit_script_fossil_bootstrap(int addScriptTag){ static int once = 0; if(0==once++){ char * zName; /* Set up the generic/app-agnostic parts of window.fossil ** which require C-level state... */ if(addScriptTag!=0){ style_script_begin(__FILE__,__LINE__); } CX("(function(){\n"); CX(/*MSIE NodeList.forEach polyfill, courtesy of Mozilla: https://developer.mozilla.org/en-US/docs/Web/API/NodeList/forEach#Polyfill */ "if(window.NodeList && !NodeList.prototype.forEach){" "NodeList.prototype.forEach = Array.prototype.forEach;" "}\n"); CX("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.config = {...various config-level options...} */ CX("window.fossil.config = {"); zName = db_get("project-name", ""); CX("projectName: %!j,\n", zName); fossil_free(zName); zName = db_get("short-project-name", ""); CX("shortProjectName: %!j,\n", zName); fossil_free(zName); zName = db_get("project-code", ""); CX("projectCode: %!j,\n", zName); fossil_free(zName); CX("/* Length of UUID hashes for display purposes. */"); CX("hashDigits: %d, hashDigitsUrl: %d,\n", hash_digits(0), hash_digits(1)); CX("diffContextLines: %d,\n", diff_context_lines(0)); CX("editStateMarkers: {" "/*Symbolic markers to denote certain edit states.*/" "isNew:'[+]', isModified:'[*]', isDeleted:'[-]'},\n"); CX("confirmerButtonTicks: 3 " "/*default fossil.confirmer tick count.*/,\n"); /* Inject certain info about the current skin... */ CX("skin:{"); /* can leak a local filesystem path: CX("name: %!j,", skin_in_use());*/ CX("isDark: %s" "/*true if the current skin has the 'white-foreground' detail*/", skin_detail_boolean("white-foreground") ? "true" : "false"); CX("}\n"/*fossil.config.skin*/); CX("};\n"/* fossil.config */); CX("window.fossil.user = {"); CX("name: %!j,", (g.zLogin&&*g.zLogin) ? g.zLogin : "guest"); CX("isAdmin: %s", (g.perm.Admin || g.perm.Setup) ? "true" : "false"); CX("};\n"/*fossil.user*/); CX("if(fossil.config.skin.isDark) " "document.body.classList.add('fossil-dark-style');\n"); /* ** fossil.page holds info about the current page. This is also ** where the current page "should" store any of its own ** page-specific state, and it is reserved for that purpose. */ CX("window.fossil.page = {" "name:\"%T\"" "};\n", g.zPath); CX("})();\n"); if(addScriptTag!=0){ style_script_end(); } /* The remaining window.fossil bootstrap code is not dependent on ** C-runtime state... */ builtin_request_js("fossil.bootstrap.js"); } } /* ** Given the NAME part of fossil.NAME.js, this function checks whether ** that module has been emitted by this function before. If it has, ** it returns -1 with no side effects. If it has not, it queues up ** (via builtin_request_js()) an emit of the module via and all of its ** known (by this function) fossil.XYZ.js dependencies (in their ** dependency order) and returns 1. If it does not find the given ** module name it returns 0. ** ** As a special case, if passed 0 then it queues up all known modules ** and returns -1. ** ** The very first time this is called, it unconditionally calls ** builtin_emit_script_fossil_bootstrap(). ** ** Any given module is only queued once, whether it is explicitly ** passed to the function or resolved as a dependency. Any attempts to ** re-queue them later are harmless no-ops. */ static int builtin_emit_fossil_js_once(const char * zName){ static int once = 0; int i; static struct FossilJs { const char * zName; /* NAME part of fossil.NAME.js */ int emitted; /* True if already emitted. */ const char * zDeps; /* \0-delimited list of other FossilJs ** entries: all known deps of this one. Each ** REQUIRES an EXPLICIT trailing \0, including ** the final one! */ } fjs[] = { /* This list ordering isn't strictly important. */ {"confirmer", 0, 0}, {"copybutton", 0, "dom\0"}, {"diff", 0, "dom\0fetch\0storage\0" /* maintenance note: "diff" needs "storage" for storing the the ** sbs-sync-scroll toggle. */}, {"dom", 0, 0}, {"fetch", 0, 0}, {"numbered-lines", 0, "popupwidget\0copybutton\0"}, {"pikchr", 0, "dom\0"}, {"popupwidget", 0, "dom\0"}, {"storage", 0, 0}, {"tabs", 0, "dom\0"} }; const int nFjs = sizeof(fjs) / sizeof(fjs[0]); if(0==once){ ++once; builtin_emit_script_fossil_bootstrap(1); } if(0==zName){ for( i = 0; i < nFjs; ++i ){ builtin_emit_fossil_js_once(fjs[i].zName); } return -1; } for( i = 0; i < nFjs; ++i ){ if(0==strcmp(zName, fjs[i].zName)){ if(fjs[i].emitted){ return -1; }else{ char nameBuffer[50]; if(fjs[i].zDeps){ const char * zDep = fjs[i].zDeps; while(*zDep!=0){ builtin_emit_fossil_js_once(zDep); zDep += strlen(zDep)+1/*NUL delimiter*/; } } sqlite3_snprintf(sizeof(nameBuffer)-1, nameBuffer, "fossil.%s.js", fjs[i].zName); builtin_request_js(nameBuffer); fjs[i].emitted = 1; return 1; } } } return 0; } /* ** COMMAND: test-js-once ** ** Tester for builtin_emit_fossil_js_once(). ** ** Usage: %fossil test-js-once filename */ void test_js_once(void){ int i; if(g.argc<2){ usage("?FILENAME...?"); } if(2==g.argc){ builtin_emit_fossil_js_once(0); assert(builtin.nReq>8); }else{ for(i = 2; i < g.argc; ++i){ builtin_emit_fossil_js_once(g.argv[i]); } assert(builtin.nReq>1 && "don't forget implicit fossil.bootstrap.js"); } for(i = 0; i < builtin.nReq; ++i){ fossil_print("ndx#%d = %d = %s\n", i, builtin.aReq[i], aBuiltinFiles[builtin.aReq[i]].zName); } } /* ** Convenience wrapper which calls builtin_request_js() for a series ** of builtin scripts named fossil.NAME.js. The first time it is ** called, it also calls builtin_emit_script_fossil_bootstrap() to ** initialize the window.fossil JS API. The first argument is the NAME ** part of the first API to emit. All subsequent arguments must be ** strings of the NAME part of additional fossil.NAME.js files, ** followed by a NULL argument to terminate the list. ** ** e.g. pass it ("fetch", "dom", "tabs", NULL) to load those 3 APIs (or ** pass it ("fetch","tabs",NULL), as "dom" is a dependency of "tabs", so ** it will be automatically loaded). Do not forget the trailing NULL, ** and do not pass 0 instead, since that isn't always equivalent to NULL ** in this context. ** ** If it is JS_BUNDLED then this routine queues up an emit of ALL of ** the JS fossil.XYZ.js APIs which are not strictly specific to a ** single page, and then calls builtin_fulfill_js_requests(). The idea ** is that we can get better bundle caching and reduced HTTP requests ** by including all JS, rather than creating separate bundles on a ** per-page basis. In this case, all arguments are ignored! ** ** This function has an internal mapping of the dependencies for each ** of the known fossil.XYZ.js modules and ensures that the ** dependencies also get queued (recursively) and that each module is ** queued only once. ** ** If passed a name which is not a base fossil module name then it ** will fail fatally! ** ** DO NOT use this for loading fossil.page.*.js: use ** builtin_request_js() for those. ** ** If the current JS delivery mode is *not* JS_BUNDLED then this ** function queues up a request for each given module and its known ** dependencies, but does not immediately fulfill the request, thus it ** can be called multiple times. ** ** If a given module is ever passed to this more than once, either in ** a single invocation or multiples, it is only queued for emit a ** single time (i.e. the 2nd and subsequent ones become ** no-ops). Likewise, if a module is requested but was already ** automatically queued to fulfill a dependency, the explicit request ** becomes a no-op. ** ** Bundled mode is the only mode in which this API greatly improves ** aggregate over-the-wire and HTTP request costs. For other modes, ** reducing the inclusion of fossil.XYZ APIs to their bare minimum ** provides the lowest aggregate costs. For debate and details, see ** the discussion at: ** ** https://fossil-scm.org/forum/forumpost/3fa2633f3e ** ** In practice it is normally necessary (or preferred) to call ** builtin_fulfill_js_requests() after calling this, before proceeding ** to call builtin_request_js() for page-specific JS, in order to ** improve cachability. ** ** Minor caveat: the purpose of emitting all of the fossil.XYZ JS APIs ** at once is to reduce over-the-wire transfers by enabling cross-page ** caching, but if there are other JS scripts pending via ** builtin_request_js() when this is called then they will be included ** in the JS request emitted by this routine, resulting in a different ** script URL than if they were not included. Thus, if a given page ** has its own scripts to install via builtin_request_js(), they ** should, if possible, be delayed until after this is called OR the ** page should call builtin_fulfill_js_requests() to flush the request ** queue before calling this routine. ** ** Achtung: the fossil.page.XYZ.js files are page-specific, containing ** the app-level logic for that specific page, and loading more than ** one of them in a single page will break that page. Each of those ** expects to "own" the page it is loaded in, and it should be loaded ** as late in the JS-loading process as feasible, ideally bundled (via ** builtin_request_js()) with any other app-/page-specific JS it may ** need. ** ** Example usage: ** ** builtin_fossil_js_bundle_or("dom", "fetch", NULL); ** ** In bundled mode, that will (the first time it is called) emit all ** builtin fossil JS APIs and "fulfill" the queue immediately. In ** non-bundled mode it will queue up the "dom" and "fetch" APIs to be ** emitted the next time builtin_fulfill_js_requests() is called. */ NULL_SENTINEL void builtin_fossil_js_bundle_or( const char * zApi, ... ) { static int bundled = 0; const char *zArg; va_list vargs; if(JS_BUNDLED == builtin_get_js_delivery_mode()){ if(!bundled){ bundled = 1; builtin_emit_fossil_js_once(0); builtin_fulfill_js_requests(); } return; } va_start(vargs,zApi); for( zArg = zApi; zArg!=NULL; (zArg = va_arg (vargs, const char *))){ if(0==builtin_emit_fossil_js_once(zArg)){ fossil_fatal("Unknown fossil JS module: %s\n", zArg); } } va_end(vargs); }