nodeEditor: concordScript

/scripting.com/code/concord/concordscript/
  worknotes.md
    #### 12/4/25; 9:50:00 AM by DW
      Started managing this as another of my GitHub projects, thus a worknotes.md file.
      There was a comment here before that says, "this is where you go to configure Drummer in GitHub"
        * https://github.com/settings/developers
        * I figured out what it means -- there are GitHub verbs here, and some require authorization, and this is where you go to set up Drummer along with your other apps, if you want to use it.
  scripting.js
    function runScriptText (scriptText, callback) {
      if (callback == null) {
        callback = function () {};
        }
      function visitCodeTree (theTree, visit) {
        var stack = new Array ();
        function doVisit (node) { //depth-first traversal
          for (var x in node) {
            if (typeof node [x] == "object") {
              stack.push (node);
              doVisit (node [x], visit);
              stack.pop ();
              }
            }
          if (node != null) {
            visit (node, stack);
            }
          }
        doVisit (theTree);
        }
      function fixSpecialFunctionCalls (theTree) {
        visitCodeTree (theTree, function (node, stack) {
          if (node.type == "FunctionDeclaration" || node.type == 'FunctionExpression') {
            node.async = true;
            }
          if (node.type == "CallExpression" && node.callee !== undefined) {
            var nodecopy = Object.assign(new Object (), node);
            for (var x in node) {
              delete node [x];
              }
            node.type = "AwaitExpression";
            node.argument = nodecopy;
            }
          return (undefined); // don't replace
          });
        }
      function preprocessScript(scriptText) {
        var scriptBody = '', tokens, i, info;
        var code = acorn.parse (scriptText, {ecmaVersion: 2020});
        fixSpecialFunctionCalls (code);
        scriptBody = escodegen.generate (code);
        return "(async function () {" + scriptBody + "})()";
        }
      (async function () {
        var processedScriptText = preprocessScript (scriptText);
        console.log ("runScriptText: processedScriptText == " + processedScriptText);
        var scriptVal = eval (processedScriptText);
        console.log ("runScriptText: scriptVal == " + scriptVal);
        return (scriptVal);
        })()
        .then(function (response) {
          callback(null, response);
          })
        .catch(function (error) {
          callback(error);
          });
      }
  verbs.js
    var number = {
      random: function (lower, upper) {
        return (random (lower, upper));
        }
      }
    var op = {
      attributes: {
        getAll: function () {
          return ($(opGetActiveOutliner ()).concord ().op.attributes.getAll ());
          },
        getOne: function (name) {
          return $(opGetActiveOutliner ()).concord ().op.attributes.getOne (name);
          },
        setOne: function (name, value) {
          return $(opGetActiveOutliner ()).concord ().op.attributes.setOne (name, value);
          },
        addGroup: function (atts) {
          return $(opGetActiveOutliner ()).concord ().op.attributes.setGroup (atts);
          },
        deleteOne: function (name) {
          var atts = op.attributes.getAll (), flDeletedOne = false;
          if (atts [name] !== undefined) {
            delete atts [name];
            flDeletedOne = true;
            }
          op.attributes.addGroup (atts);
          return (flDeletedOne);
          },
        makeEmpty: function () {
          return (opAttsMakeEmpty ());
          },
        exists: function (attname) {
          return $(opGetActiveOutliner ()).concord ().op.attributes.exists (attname);
          }
        },
      bold: function () {
        return ($(opGetActiveOutliner ()).concord ().op.bold ());
        },
      collapse: function () {
        return ($(opGetActiveOutliner ()).concord ().op.collapse ());
        },
      collapseEverything: function () {
        return ($(opGetActiveOutliner ()).concord ().op.fullCollapse ());
        },
      countSubs: function () {
        return ($(opGetActiveOutliner ()).concord().op.countSubs ());
        },
      deleteSubs: function () {
        return ($(opGetActiveOutliner ()).concord ().op.deleteSubs ());
        },
      demote: function () {
        return ($(opGetActiveOutliner ()).concord ().op.demote ());
        },
      expand: function () {
        return ($(opGetActiveOutliner ()).concord ().op.expand ());
        },
      expandAllLevels: function () {
        return ($(opGetActiveOutliner ()).concord ().op.expandAllLevels ());
        },
      expandEverything: function () {
        return ($(opGetActiveOutliner ()).concord ().op.fullExpand ());
        },
      expandTo: function (headline) {
        expandToCursor (headline);
        return (setCursorActive (headline.getCursor ()));
        },
      firstSummit: function () {
        opFirstSummit ();
        return (true);
        },
      getCursor: function () {
        return (opGetCursor ());
        return ($(opGetActiveOutliner ()).concord ().op.getCursorRef ());
        },
      getCursorJstruct: function () { //11/13/21 by DW
        console.log ("getCursorJstruct");
        return (opSubOutlineJstruct (opGetBarCursor ()));
        },
      getCursorOpml: function (flSubsOnly=false) {
        Changes
          11/28/21; 9:35:00 AM by DW
            New optional param, flSubsOnly.
        if (flSubsOnly) {
          return (opGetCursorOpmlSubsOnly ());
          }
        else {
          return (opCursorToXml ());
          return ($(opGetActiveOutliner ()).concord ().op.cursorToXml ());
          }
        },
      getCursorUrl: function () {
        var parent = undefined;
        op.visitToSummit (function (headline) {
          var type = headline.attributes.getOne ("type");
          if (type != undefined) {
            parent = headline;
            return (false);
            }
          return (true);
          });
        return (getTrexUrl (opGetActiveOutliner (), parent, true));
        },
      getOutlineJstruct: function () { //11/16/21 by DW
        return (opWholeOutlineJstruct ());
        },
      getLineText: function () {
        return ($(opGetActiveOutliner ()).concord ().op.getLineText ());
        },
      getModified: function () {
        return ($(opGetActiveOutliner ()).concord ().op.changed ());
        },
      getRenderMode: function () { //7/28/13 by DW
        return ($(opGetActiveOutliner ()).concord ().op.getRenderMode ());
        },
      getSelectedText: function () {
        return (opGetSelectedText ());
        },
      go: function (dir, ct) {
        if (dir == right) {
          op.expand ();
          }
        return ($(opGetActiveOutliner ()).concord ().op.go (dir, ct));
        },
      hasSubs: function () {
        return (op.countSubs () > 0);
        },
      insert: function (s, direction) {
        Changes
          7/7/21; 12:20:37 PM by DW
            Instead of returning what Concord returns, we return true.
          4/9/21; 10:09:52 AM by DW
            Turn the value undefined into a string. There is no .toString function for undefined.
        if (s === undefined) { //4/9/21 by DW
          s = "undefined"
          }
        $(opGetActiveOutliner ()).concord ().op.insert (s, direction);
        return (true);
        },
      insertArray: function (theArray, title) { //4/9/21 by DW
        var dir = right;
        op.insert (title.toString (), down);
        theArray.forEach (function (item) {
          op.insert (item, dir);
          dir = down;
          });
        if (dir == down) {
          op.go (left, 1);
          }
        },
      insertInCalendar: function (s, atts) { //9/20/21 by DW
        return (opInsertInCalendar (s, atts));
        },
      insertOpml: function (opmltext, dir) {
        if (dir == undefined) {
          dir = down;
          }
        return ($(opGetActiveOutliner ()).concord ().op.insertXml (opmltext, dir));
        },
      isComment: function () {
        var isComment = op.attributes.getOne ("isComment")
        if ((isComment == undefined) || (isComment == "false")) {
          return (false);
          }
        else {
          return (true);
          }
        },
      italic: function () {
        return ($(opGetActiveOutliner ()).concord ().op.italic ());
        },
      link: function (url) {
        return ($(opGetActiveOutliner ()).concord ().op.link (url));
        },
      makeComment: function () {
        op.attributes.setOne ("isComment", "true");
        return ($(opGetActiveOutliner ()).concord ().script.makeComment ());
        },
      promote: function () {
        return ($(opGetActiveOutliner ()).concord ().op.promote ());
        },
      reorg: function (dir, ct) {
        if (ct == undefined) {
          ct = 1;
          }
        return ($(opGetActiveOutliner ()).concord ().op.reorg (dir, ct));
        },
      replaceSelectedText: function (replaceWith) {
        return (opReplaceSelectedText (replaceWith));
        },
      runSelection: function () {
        var value = eval (op.getLineText ());
        op.deleteSubs ();
        op.insert (value, right);
        op.go (left, 1);
        },
      setCursor: function (theCursor) {
        return (opSetCursor (theCursor));
        },
      setLineText: function (s) { //8/7/13 by DW
        return ($(opGetActiveOutliner ()).concord ().op.setLineText (s));
        },
      setModified: function () {
        return ($(opGetActiveOutliner ()).concord ().op.markChanged ());
        },
      setRenderMode: function (flrendermode) { //7/28/13 by DW
        $(opGetActiveOutliner ()).concord ().op.setRenderMode (flrendermode);
        },
      setTextMode: function (flTextMode) {
        return (opSetTextMode (flTextMode));
        },
      setTextMode: function (fltextmode) {
        $(opGetActiveOutliner ()).concord ().op.setTextMode (fltextmode);
        },
      strikethrough: function () {
        return ($(opGetActiveOutliner ()).concord ().op.strikethrough ());
        },
      toggleComment: function () {
        if (op.isComment ()) {
          op.unComment ();
          }
        else {
          op.makeComment ();
          }
        },
      toggleRenderMode: function () { //7/28/13 by DW
        op.setRenderMode (!op.getRenderMode ());
        },
      unComment: function () {
        op.attributes.deleteOne ("isComment");
        return ($(opGetActiveOutliner ()).concord ().script.unComment ());
        },
      visitAll: function (callback) {
        return (opVisitAll (callback));
        },
      visitSubs: function (lineCallback, indentCallback, outdentCallback) {
        var barcursor = opGetBarCursor ();
        opVisitSubs (barcursor, lineCallback);
        },
      visitToSummit: function (callback) {
        return ($(opGetActiveOutliner ()).concord ().op.visitToSummit (callback));
        },
      }
    var mastodon = { //11/15/22 by DW
      },
    var drummer = {
      runScript: function (scripttext) {
        return new Promise (function (resolve, reject) {
          runScriptText (scripttext, function (err, data) {
            if (err) {
              reject (err);
              }
            else {
              resolve (data);
              }
            });
          });
        },
      subscribeToOutline: function (url) {
        return new Promise (function (resolve, reject) {
          subscribeToInstantOutline (url, function (err, data) {
            if (err) {
              reject (err);
              }
            else {
              resolve (data);
              }
            });
          });
        },
      version: function () {
        return (myVersion);
        },
      productname: function () {
        return (appConsts.productname);
        },
      productnameForDisplay: function () {
        return (appConsts.productnameForDisplay);
        },
      useStylesheet: function (url) { //8/8/21 by DW
        useStylesheet (url);
        return (true);
        }
      }
    var tab = {
      openFile: function (fname, title) {
        return new Promise (function (resolve, reject) {
          openOutlineInTab (fname, title, undefined, function (err) {
            if (err) {
              reject (err);
              }
            else {
              resolve (true);
              }
            });
          });
        },
      openInstantOutline: function (url, title) {
        return new Promise (function (resolve, reject) {
          subscribeToInstantOutline (url, function (err) {
            if (err) {
              reject (err);
              }
            else {
              resolve (true);
              }
            });
          });
        },
      getPublicUrl: function () {
        var theTab = appPrefs.myTabs [appPrefs.ixCurrentTab];
        return (theTab.urlPublic);
        },
      getActiveTabStatus: function () {
        return (getActiveTabStatus ());
        }
      };
    var twitter = { //3/20/21 by DW
      getUserInfo: function (screenname) {
        return new Promise (function (resolve, reject) {
          servercall ("getuserinfoforverb", {screenname}, true, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("data == " + jsonStringify (data));
              resolve (data);
              }
            });
          });
        },
      getRawUserInfo: function (screenname) { //9/9/21 by DW
        return new Promise (function (resolve, reject) {
          servercall ("getuserinfo", {screenname}, true, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("data == " + jsonStringify (data));
              resolve (data);
              }
            });
          });
        },
      getScreenname: function (id) {
        Changes
          4/9/21; 9:39:52 AM by DW
            If the user couldn't be found, return undefined, don't throw an error.
        return new Promise (function (resolve, reject) {
          servercall ("getuserinfofromuserid", {id}, true, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              resolve (undefined);
              }
            else {
              console.log ("data == " + jsonStringify (data));
              resolve (data.screen_name);
              }
            });
          });
        },
      getMyScreenname: function () { //8/11/21 by DW
        return (twGetScreenName ());
        },
      getTweet: function (id) {
        return new Promise (function (resolve, reject) {
          servercall ("gettweetforverb", {id}, true, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("data == " + jsonStringify (data));
              resolve (data);
              }
            });
          });
        },
      getRawTweet: function (id) { //8/25/22 by DW
        return new Promise (function (resolve, reject) {
          servercall ("gettweet", {id}, true, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("data == " + jsonStringify (data));
              resolve (data);
              }
            });
          });
        },
      getThread: function (id) {
        return new Promise (function (resolve, reject) {
          var params = {
            id,
            reload: false
            };
          servercall ("getthread", params, true, function (err, theThread) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("data == " + jsonStringify (theThread));
              resolve (theThread);
              }
            });
          });
        },
      newPost: function (theText, idInReplyTo) {
        return new Promise (function (resolve, reject) {
          if (idInReplyTo === undefined) {
            idInReplyTo = 0;
            }
          var params = {
            status: theText,
            in_reply_to_status_id: idInReplyTo
            };
          servercall ("sendtweetforverb", params, true, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("data == " + jsonStringify (data));
              resolve (data);
              }
            });
          });
        },
      getHomeTimeline: function () {
        return new Promise (function (resolve, reject) {
          var params = {
            whichtimeline: "home",
            count: 200,
            trim_user: true,
            exclude_replies: false,
            include_entities: false,
            user_id: undefined
            since_id: undefined
            };
          servercall ("gettimeline", params, true, function (err, theTimeline) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("theTimeline == " + jsonStringify (theTimeline));
              resolve (theTimeline);
              }
            });
          });
        },
      getMentionsTimeline: function () {
        return new Promise (function (resolve, reject) {
          var params = {
            whichtimeline: "mentions",
            count: 200,
            trim_user: false,
            exclude_replies: false,
            include_entities: false,
            user_id: undefined
            since_id: undefined
            };
          servercall ("gettimeline", params, true, function (err, theTimeline) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("theTimeline == " + jsonStringify (theTimeline));
              resolve (theTimeline);
              }
            });
          });
        },
      getUserTimeline: function (screenname, includeReplies, idSince) {
        Changes
          6/4/21; 11:29:23 AM by DW
            New optional param, idSince.
        return new Promise (function (resolve, reject) {
          if (includeReplies === undefined) {
            includeReplies = false;
            }
          var params = {
            whichtimeline: "user",
            screen_name: screenname,
            count: 200,
            trim_user: true,
            exclude_replies: !includeReplies,
            include_entities: false,
            user_id: undefined,
            since_id: idSince
            };
          servercall ("gettimeline", params, true, function (err, theTimeline) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("theTimeline == " + jsonStringify (theTimeline));
              resolve (theTimeline);
              }
            });
          });
        },
      getFollowers: function (screenname) {
        return new Promise (function (resolve, reject) {
          var params = {
            screen_name: screenname
            };
          servercall ("getfollowers", params, true, function (err, theFollowers) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("theFollowers == " + jsonStringify (theFollowers));
              resolve (theFollowers);
              }
            });
          });
        },
      getFollowed: function (screenname) {
        return new Promise (function (resolve, reject) {
          var params = {
            screen_name: screenname
            };
          servercall ("getfollowed", params, true, function (err, theFollowed) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("theFollowed == " + jsonStringify (theFollowed));
              resolve (theFollowed);
              }
            });
          });
        },
      getFollowed: function (screenname) {
        return new Promise (function (resolve, reject) {
          var params = {
            screen_name: screenname
            };
          servercall ("getfollowed", params, true, function (err, theFollowed) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("theFollowed == " + jsonStringify (theFollowed));
              resolve (theFollowed);
              }
            });
          });
        },
      getUserLists: function (screenname) { //5/7/21 by DW
        return new Promise (function (resolve, reject) {
          servercall ("getuserlists", {screenname}, true, function (err, theLists) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("theLists == " + jsonStringify (theLists));
              resolve (theLists);
              }
            });
          });
        },
      getListMembers: function (idList) { //5/7/21 by DW
        return new Promise (function (resolve, reject) {
          servercall ("getlistmembers", {id: idList}, true, function (err, theMembers) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("theMembers == " + jsonStringify (theMembers));
              resolve (theMembers);
              }
            });
          });
        },
      getListInfo: function (idList) { //5/7/21 by DW
        return new Promise (function (resolve, reject) {
          servercall ("getlistinfo", {id: idList}, true, function (err, theInfo) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("theInfo == " + jsonStringify (theInfo));
              resolve (theInfo);
              }
            });
          });
        },
      updateListInfo: function (idList, theInfo) { //5/7/21 by DW
        return new Promise (function (resolve, reject) {
          var params = {
            id: idList,
            info: jsonStringify (theInfo)
            };
          servercall ("updatelistinfo", params, true, function (err, theInfo) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("theInfo == " + jsonStringify (theInfo));
              resolve (theInfo);
              }
            });
          });
        },
      addMemberToList: function (idList, screenname) { //5/7/21 by DW
        return new Promise (function (resolve, reject) {
          servercall ("addmembertolist", {id: idList, screenname}, true, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              resolve (data);
              }
            });
          });
        },
      addGroupToList: function (idList, listOfScreennames) { //5/7/21 by DW
        return new Promise (function (resolve, reject) {
          var params = {
            id: idList,
            listofscreennames: listOfScreennames //a comma-separated list of names
            }
          servercall ("addgrouptolist", params, true, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              resolve (data);
              }
            });
          });
        },
      removeUserFromList: function (idList, screenname) { //5/7/21 by DW
        return new Promise (function (resolve, reject) {
          var params = {
            id: idList,
            screenname
            }
          servercall ("removeuserfromlist", params, true, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              resolve (data);
              }
            });
          });
        },
      removeGroupFromList: function (idList, listOfScreennames) { //5/7/21 by DW
        return new Promise (function (resolve, reject) {
          var params = {
            id: idList,
            listofscreennames: listOfScreennames //a comma-separated list of names
            }
          servercall ("removegroupfromlist", params, true, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              resolve (data);
              }
            });
          });
        },
      proxy: async function (request) {
        var url = new URL(request.url);
        if (request.query != null) {
          for (var key of Object.keys(request.query)) {
            url.searchParams.set(key, request.query[key]);
            }
          }
        const data = {
          method: request.method || 'GET',
          url: url.toString(),
          accessToken: localStorage.twOauthToken,
          accessTokenSecret: localStorage.twOauthTokenSecret,
          params: request.params || {}
          };
        const response = await fetch(
          twGetDefaultServer () + 'proxytwitter',
          {
            method: 'POST',
            body: JSON.stringify(data)
            }
          );
        return response.json();
        },
      getUserProfile: async function (query) {
        if (typeof query === 'string') {
          query = { screen_name: query };
        }
        return twitter.proxy({
          method: 'GET',
          url: 'https://api.twitter.com/1.1/users/show.json',
          query: query
        });
        },
      getUserTimeLine: function (query) {
        if (typeof query === 'string') {
          query = { screen_name: query };
        }
        return twitter.proxy({
          method: 'GET',
          url: 'https://api.twitter.com/1.1/statuses/user_timeline.json',
          query: query
        });
        },
      getStatus: function (query) {
        if (typeof query === 'string' || typeof query === 'number') {
          query = { id: query };
        }
        return twitter.proxy({
          method: 'GET',
          url: 'https://api.twitter.com/1.1/statuses/show.json',
          query: query
        });
        }
      }
    var file = {
      exists: function (path) {
        return new Promise (function (resolve, reject) {
          servercall ("fileexists", {relpath: path}, true, function (err, data) {
            if (err) {
              reject (err);
              }
            else {
              console.log ("file.exists: data == " + jsonStringify (data));
              resolve (getBoolean (data.flExists));
              }
            });
          });
        },
      readWholeFile: function (path) {
        Changes
          2/24/21; 3:44:07 PM by DW
            Rewrote to use new readwholefile server call.
        return new Promise (function (resolve, reject) {
          servercall ("readwholefile", {relpath: path}, true, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              resolve (data.filetext);
              }
            });
          });
        },
      readOptionalFile: function (path) {
        Changes
          5/29/21; 12:03:09 PM by DW
            This was written before readWholeFile was rewritten to not care whether a file is public or private. Not needed now.
        return new Promise (function (resolve, reject) {
          getOptionalFilePublicOrPrivate (path, function (err, jstruct, flPrivate) {
            if (err) {
              resolve (undefined);
              }
            else {
              console.log ("readOptionalFile: jstruct == " + jsonStringify (jstruct));
              resolve ({
                filedata: jstruct.data.filedata,
                private: flPrivate
                });
              }
            });
          });
        },
      writeWholeFile: function (path, filetext) {
        Changes
          2/24/21; 3:44:07 PM by DW
            Rewrote to use new writewholefile server call.
        return new Promise (function (resolve, reject) {
          console.log ("file.writeWholeFile: filetext == " + filetext);
          serverpost ("writewholefile", {relpath: path}, true, filetext, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log ("writeWholeFile: data == " + jsonStringify (data));
              resolve (filetext);
              }
            });
          });
        },
      getFileInfo: function (relpath) {
        return new Promise (function (resolve, reject) {
          servercall ("getfileinfo", {relpath}, true, function (err, data) {
            if (err) {
              reject (err);
              }
            else {
              var fileInfo = {
                size: data.size,
                whenAccessed: new Date (data.whenAccessed),
                whenCreated: new Date (data.whenCreated),
                whenModified: new Date (data.whenModified),
                flPrivate: data.flPrivate,
                urlPublic: data.urlPublic //8/24/21 by DW
                };
              resolve (fileInfo);
              }
            });
          });
        },
      readWholeFile: function (path) {
        return new Promise (function (resolve, reject) {
          function getFile (flPrivate, getFileCallback) {
            twGetFile (path, true, flPrivate, function (err, data) {
              if (err) {
                return reject(err);
                }
              getFileCallback (data);
              });
            }
          getFile (false, function (data) {
            if (data === undefined) {
              getFile (true, function (data) {
                if (data === undefined) {
                resolve (undefined);
                }
                else {
                resolve (data.filedata.toString ());
                }
                });
              }
            else {
              resolve (data.filedata.toString ());
              }
            })
          });
        },
      writeWholeFile: function (path, data) {
        Changes
          2/20/21; 10:52:26 AM by DW
            Previously it created a public file. I switched to private. The rationale: since when you use the outliner interactively, when you create a file it is private and you must make it public, I want the scripting interface to work the same way.
            Also, twUpload file requires the file type as a parameter, but the server doesn't do anyting with it.
        return new Promise (function (resolve, reject) {
          var flPrivate = true;
          twUploadFile (path, data.toString (), "text/plain", flPrivate, function (data) {
            console.log ("writeWholeFile: data == " + jsonStringify (data));
            resolve (data.url);
            });
          });
        },
      makeFilePublic: function (path) {
        return new Promise (function (resolve, reject) {
          servercall ("makefilepublic", {relpath: path}, true, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              resolve (data.url);
              }
            });
          });
        },
      delete: function (path) {
        return new Promise (function (resolve, reject) {
          servercall ("deletefile", {relpath: path}, true, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              resolve (undefined);
              }
            });
          });
        },
      getFileHierarchy: function () {
        return new Promise (function (resolve, reject) {
          servercall ("getfilehierarchy", undefined, true, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log (jsonStringify (data));
              resolve (data);
              }
            });
          });
        },
      getMyFiles: function () { //3/30/21 by DW -- it doesn't return a jstruct, so can't use servercall
        return new Promise (function (resolve, reject) {
          servercall ("myfiles", undefined, true, function (err, data) {
            if (err) {
              console.log ("err.message == " + err.message);
              reject (err);
              }
            else {
              console.log (jsonStringify (data));
              resolve (data);
              }
            });
          });
        },
      getCurrentFilePath: function () {
        return (getActiveTabStatus ().fname);
        },
      getCurrentFileTitle: function () {
        return (getActiveTabStatus ().name);
        },
      getCurrentFileInfo: function () {
        return (getActiveTabStatus ());
        },
      viewCurrentFileInReader: async function () { //7/28/13 by DW
        var theUrl = await file.makeFileViewable ();
        if (theUrl === undefined) {
          dialog.alert ("Can't open the file in the reader, possibly because it's not public.");
          }
        else {
          webBrowser.openUrl (theUrl);
          }
        },
      makeFileViewable: function () { //1/15/17 by DW
        return new Promise(function (resolve) {
          makeFileViewable (resolve);
          });
        },
      getDatePath: function (theDate, flLastSeparator) {
        return (getDatePath (theDate, flLastSeparator));
        },
      getFileList: function () { //1/16/17 by DW
        return new Promise(function (resolve) {
          twGetUserFiles (true, function (privateFiles) {
            twGetUserFiles (false, function (publicFiles) {
              function addFiles (whichFiles) {
                for (var i = 0; i < whichFiles.length; i++) {
                var theFile = whichFiles [i];
                theFiles [theFiles.length] = theFile.path;
                }
                }
              var theFiles = new Array ();
              addFiles (publicFiles);
              addFiles (privateFiles);
              resolve (theFiles);
              });
            });
          });
        },
      getDatePath: getDatePath
      }
    var dns = {
      getDomainName: function (dottedid) {
        return new Promise (function (resolve, reject) {
          servercall ("getdomainname", {dottedid}, true, function (err, data) {
            if (err) {
              reject (err);
              }
            else {
              resolve (data.name);
              }
            });
          });
        },
      getDottedId: function (name) {
        return new Promise (function (resolve, reject) {
          servercall ("getdottedid", {name}, true, function (err, data) {
            if (err) {
              console.log (err.message);
              reject (err);
              }
            else {
              resolve (data.dottedid);
              }
            });
          });
        }
      }
    var string = {
      addPeriodAtEnd: addPeriodAtEnd,
      maxStringLength: maxStringLength,
      bumpUrlString: bumpUrlString,
      addCommas: stringAddCommas,
      decodeXml: decodeXml,
      randomSnarkySlogan: getRandomSnarkySlogan,
      formatDate: formatDate,
      beginsWith: beginsWith,
      contains: stringContains,
      countFields: stringCountFields,
      dayOfWeekToString: dayOfWeekToString,
      delete: stringDelete,
      encodeHtml: function (s) {
        return s.replace(/[\u00A0-\u9999<>\&]/g, function(i) {
          return '&#'+i.charCodeAt(0)+';';
          });
        },
      endsWith: endsWith,
      filledString: filledString,
      getRandomPassword: getRandomPassword,
      hashMD5: function (s) {
        return (SparkMD5.hash (s));
        },
      innerCaseName: innerCaseName,
      insert: stringInsert,
      isAlpha: isAlpha,
      isNumeric: isNumeric,
      isWhitespace: isWhitespace,
      isPunctuation: isPunctuation,
      lastField: stringLastField,
      lower: stringLower,
      upper: stringUpper,
      mid: stringMid,
      monthToString: monthToString, //January, February etc.
      nthField: stringNthField,
      padWithZeros: padWithZeros,
      popLastField: stringPopLastField,
      popTrailing: function (s, ch) { //11/25/13 by DW
        while (s.length > 0) {
          if (s [s.length - 1] != ch) {
            break;
            }
          s = string.delete (s, s.length, 1);
          }
        return (s);
        },
      popExtension: stringPopExtension, //1/18/17 by DW
      replaceAll: replaceAll,
      multipleReplaceAll: multipleReplaceAll, //1/18/17 by DW
      stripMarkup: stripMarkup,
      trimLeading: function (s, ch) {
        if (ch == undefined) {
          ch = " ";
          }
        return (trimLeading (s, ch));
        },
      trimTrailing: function (s, ch) {
        if (ch == undefined) {
          ch = " ";
          }
        return (trimTrailing (s, ch));
        },
      trimWhitespace: function (s, ch) { //8/24/13 by DWextensionToMimeType
        if (ch == undefined) {
          ch = " ";
          }
        return (trimLeading (trimTrailing (s, ch), ch))
        },
      extensionToMimeType: function (filepath) { //8/4/21 by DW
        const ext = stringLastField (filepath, ".");
        if (ext == filepath) { //no extension
          return (undefined);
          }
        else {
          return (httpExt2MIME (ext));
          }
        },
      markdownProcess: function (s) { //11/2/21 by DW
        var md = new Markdown.Converter ();
        return (md.makeHtml (s));
        }
      }
    var dialog = {
      alert: function (s) { //7/29/21 by DW
        return new Promise (function (resolve, reject) {
          alertDialog (s, function () {
            resolve (true);
            })
          });
        },
      ask: function (prompt, defaultValue, placeholder) {
        return new Promise(function (resolve) {
          return (askDialog (prompt, defaultValue, placeholder, function (value, flCancel) {
            if (flCancel) {
              resolve();
                }
              else {
                resolve(value);
                }
            }));
          });
        },
      viewText: function (prompt, s) {
        showViewTextDialog (prompt, s);
        },
      confirm: function (prompt) {
        return new Promise (function (resolve) {
          confirmDialogForScripting (prompt, resolve);
          });
        },
      about: function (opmltext, title) {
        console.log (urlOpml);
        if (title === undefined) {
          title = "About Dialog";
          }
        outlineDialog (title, opmltext, true, undefined, undefined);
        }
      }
    var date = {
      sameDay: sameDay,
      sameMonth: sameMonth,
      dayGreaterThanOrEqual: dayGreaterThanOrEqual,
      secondsSince: secondsSince,
      yesterday: dateYesterday,
      tomorrow: dateTomorrow,
      netStandardString: function (theDate) { //12/17/13 by DW
        return (new Date (theDate).toUTCString ());
        },
      convertToTimeZone: function (when, timeZoneString="0") { //11/15/21 by DW
        Changes
          11/15/21; 9:56:42 AM by DW
            Give me a date in your local timezone, we return a date in the time zone you specify.
            If x is a date returned by this routine, you can use the JavaScript routines to get components of a time, getHours, getMinutes, etc and they will return the correct values for the indicated time zone.
              https://www.w3schools.com/js/js_date_methods.asp
        function processTimeZoneString (s) { //convert someting like 5:30 to 5.5
          var splits = s.split (":");
          if (splits.length == 2) {
            var ctsecs = Number (splits [1]);
            var hourFraction = ctsecs / 60;
            if (s [0] == "-") {
              hourFraction = -hourFraction;
              }
            s = Number (splits [0]) + hourFraction;
            }
          return (s);
          }
        var offset = Number (processTimeZoneString (timeZoneString));
        var d = new Date (when);
        var localTime = d.getTime ();
        var localOffset = d.getTimezoneOffset () * 60000;
        var utc = localTime + localOffset;
        var blogTime = utc + (3600000 * offset);
        return (new Date (blogTime));
        }
      }
    var clock = {
      now: function () {
        return (new Date ());
        },
      waitSeconds: function (ctsecs) {
        return new Promise (function (resolve, reject) {
          var whenstart = new Date ();
          function doneWaiting () {
            resolve (date.secondsSince (whenstart));
            }
          setTimeout (doneWaiting, ctsecs * 1000)
          });
        }
      }
    var speaker = {
      beep: function () {
        speakerBeep ();
        }
      }
    var webBrowser = {
      openUrl: function (url) {
        Changes
          10/6/21; 12:40:04 PM by DW
            Call drummerOpenUrl works in both web Drummer and Electric Drummer, where you want to open the URL in an external browser window, not one inside the Electron environment.
        drummerOpenUrl (url); //10/6/21 by DW
        window.open (url);
        return (true);
        }
      }
    var feedLand = { //9/12/22 by DW
      config: {
        urlServer: "http://api.feedland.org/"
        },
      getUserPrefs: function (screenname, urlServer=feedLand.config.urlServer) {
        return new Promise (function (resolve, reject) {
          servercall ("getuserprefs", {screenname}, true, function (err, data) {
            if (err) {
              reject (err);
              }
            else {
              resolve (data);
              }
            }, urlServer);
          });
        },
      setUserPrefs: function (thePrefs, urlServer=feedLand.config.urlServer) {
        return new Promise (function (resolve, reject) {
          var jsontext = jsonStringify (thePrefs);
          servercall ("sendprefs", {prefs: jsontext}, true, function (err, data) {
            if (err) {
              reject (err);
              }
            else {
              resolve (data);
              }
            }, urlServer);
          });
        },
      checkFeedNow: function (feedUrl, urlServer=feedLand.config.urlServer) {
        return new Promise (function (resolve, reject) {
          servercall ("checkfeednow", {url: feedUrl}, true, function (err, data) {
            if (err) {
              reject (err);
              }
            else {
              resolve (data);
              }
            }, urlServer);
          });
        },
      getUserCategories: function (screenname, urlServer=feedLand.config.urlServer) {
        Changes
          9/19/22; 2:27:09 PM by DW
            Updated, the user prefs are now stored in the database.
          9/13/22; 3:07:34 PM by DW
            This verb calls the main FeedLand server because that's where the user's prefs are stored.
        return new Promise (function (resolve, reject) {
          servercall ("getuserprefs", {screenname}, true, function (err, data) {
            if (err) {
              reject (err);
              }
            else {
              var theCategories = {
                screenname,
                categories: data.categories,
                homePageCategories: data.homePageCategories,
                newsproductCategories: data.newsproductCategoryList
                };
              resolve (theCategories);
              }
            }, urlServer);
          });
        return new Promise (function (resolve, reject) {
          servercall ("getusercategories", {screenname}, true, function (err, data) {
            if (err) {
              reject (err);
              }
            else {
              resolve (data);
              }
            }, urlServer);
          });
        },
      getOneUserCategory: function (screenname, catname, urlServer=feedLand.config.urlServer) {
        return new Promise (function (resolve, reject) {
          servercall ("getfeedsincategory", {screenname, catname}, true, function (err, data) {
            if (err) {
              console.log (err.message);
              reject (err);
              }
            else {
              resolve (data);
              }
            }, urlServer);
          });
        },
      getFollowers: function (feedUrl, urlServer=feedLand.config.urlServer) {
        return new Promise (function (resolve, reject) {
          servercall ("getfollowers", {url: feedUrl}, true, function (err, data) {
            if (err) {
              reject (err);
              }
            else {
              console.log ("data == " + jsonStringify (data));
              resolve (data);
              }
            }, urlServer);
          });
        },
      getSubscription: function (feedUrl, urlServer=feedLand.config.urlServer) {
        return new Promise (function (resolve, reject) {
          servercall ("isusersubscribed", {url: feedUrl}, true, function (err, data) {
            if (err) {
              reject (err);
              }
            else {
              if (data.flSubscribed) {
                var sub = data.theSubscription;
                if (sub.categories !== undefined) { //9/6/22 by DW
                sub.categories = JSON.parse (sub.categories);
                }
                sub.screenname = sub.listName;
                delete sub.listName;
                resolve (sub);
                }
              else {
                resolve (undefined);
                }
              }
            }, urlServer);
          });
        },
      subscribe: function (feedUrl, urlServer=feedLand.config.urlServer) {
        return new Promise (function (resolve, reject) {
          servercall ("subscribe", {url: feedUrl}, true, function (err, data) {
            if (err) {
              reject (err);
              }
            else {
              resolve (data);
              }
            }, urlServer);
          });
        },
      unsubscribe: function (feedUrl, urlServer=feedLand.config.urlServer) {
        return new Promise (function (resolve, reject) {
          servercall ("unsubscribe", {url: feedUrl}, true, function (err, data) {
            if (err) {
              reject (err);
              }
            else {
              resolve (data);
              }
            }, urlServer);
          });
        }
      }
    var fargo = { //11/29/13 by DW
      version: function () {
        return (appConsts.version);
        },
      productname: function () {
        return (appConsts.productname);
        },
      productnameForDisplay: function () {
        return (appConsts.productnameForDisplay);
        }
      }
    var oldSchool = {
      buildBlog: function (flFullData=false) {
        Changes
          11/2/21; 12:42:45 PM by DW
            New optional param, flFullData. If true we return all the data Old School returns to us.
        return new Promise (function (resolve, reject) {
          var url = "http://drummercms.scripting.com/build?blog=" + twGetScreenName ();
          httpRequest (url, undefined, undefined, function (err, jsontext) {
            if (err) {
              console.log ("There was an error building the blog: " + err.message);
              reject (err);
              }
            else {
              try {
                var jstruct = JSON.parse (jsontext);
                opSetOneHoistedHeader ("urlPublic", jstruct.baseUrl)
                if (flFullData) {
                resolve (jstruct);
                }
                else {
                resolve (jstruct.baseUrl);
                }
                }
              catch (err) {
                console.log ("There was an error building the blog: " + err.message);
                reject (err);
                }
              }
            });
          });
        },
      getCursorLink: function () {
        Changes
          11/15/21; 10:29:14 AM by DW
            The first part of the link has to be in the blogger's timezone.
          11/14/21; 6:11:53 PM by DW
            Return the URL of the text that the bar cursor points to in the blog.
        var headers = opGetHeaders (), urlWebsite;
        if (headers.urlBlogWebsite !== undefined) {
          urlWebsite = headers.urlBlogWebsite;
          }
        else {
          urlWebsite = "http://oldschool.scripting.com/" + twitter.getMyScreenname () + "/";
          return (undefined);
          }
        function pad (n) {
          return (padWithZeros (n, 2));
          }
        function getPostNode () {
          var barcursor = opGetBarCursor (), postNode = barcursor;
          barcursor.visitToSummit (function (theNode) {
            if (theNode.attributes.getOne ("type") == "calendarDay") {
              return (false);
              }
            postNode = theNode;
            return (true); //keep looking
            });
          return (postNode);
          }
        var whenCursor = new Date (opGetOneAtt ("created"));
        var postNode = getPostNode ();
        var whenPost = new Date (postNode.attributes.getOne ("created"));
        var timeZoneOffset = (headers.timeZoneOffset === undefined) ? "-5" : headers.timeZoneOffset;
        console.log ("convertToTimeZone: timeZoneOffset == " + timeZoneOffset);
        whenPost = date.convertToTimeZone (whenPost, timeZoneOffset);
        var url = urlWebsite + whenPost.getFullYear () + "/" + pad (whenPost.getMonth () + 1) + "/" + pad (whenPost.getDate ()) + ".html";
        url += "#a" + pad (whenCursor.getUTCHours ()) + pad (whenCursor.getUTCMinutes ()) + pad (whenCursor.getUTCSeconds ());
        return (url)
        }
      }
    //opml verbs -- 9/5/21 by DW
      function readWholeOpmlFile (f, callback) {
        servercall ("readwholefile", {relpath: f}, true, function (err, data) {
          if (err) {
            callback (err);
            }
          else {
            try {
              var theOutline = opmlPackage ().parse (data.filetext);
              callback (undefined, theOutline);
              }
            catch (err) {
              callback (err);
              }
            }
          });
        }
      function writeWholeOpmlFile (f, theOutline, callback) {
        var opmltext = opmlPackage ().stringify (theOutline);
        serverpost ("writewholefile", {relpath: f}, true, opmltext, function (err) {
          if (err) {
            callback (err);
            }
          else {
            callback (undefined, true);
            }
          });
        }
      function opmlGetAttributes (f, callback) {
        var ixtab = findTabByFname (f);
        if (ixtab === undefined) { //the file is not open
          readWholeOpmlFile (f, function (err, theOutline) {
            if (err) {
              callback (err);
              }
            else {
              callback (err, theOutline.opml.head);
              }
            });
          }
        else {
          var id = "#outliner" + appPrefs.tabs [ixtab].serialnum
          callback (undefined, $(id).concord ().op.getHeaders ())
          }
        }
      function opmlSetAttributes (f, atts, callback) {
        var ixtab = findTabByFname (f);
        if (ixtab === undefined) { //the file is not open
          readWholeOpmlFile (f, function (err, theOutline) {
            if (err) {
              callback (err);
              }
            else {
              theOutline.opml.head = atts;
              writeWholeOpmlFile (f, theOutline, callback)
              }
            });
          }
        else {
          var id = "#outliner" + appPrefs.tabs [ixtab].serialnum
          $(id).concord ().op.setHeaders (atts);
          callback (undefined, true);
          }
        }
      var opml = {
        attributes: {
          getAll: function (f) {
            return new Promise (function (resolve, reject) {
              opmlGetAttributes (f, function (err, atts) {
                if (err) {
                reject (err);
                }
                else {
                resolve (atts);
                }
                });
              });
            },
          setAll: function (f, atts) {
            return new Promise (function (resolve, reject) {
              opmlSetAttributes (f, atts, function (err, data) {
                if (err) {
                reject (err);
                }
                else {
                resolve (atts);
                }
                });
              });
            },
          getOne: function (f, name) {
            return new Promise (function (resolve, reject) {
              opmlGetAttributes (f, function (err, atts) {
                if (err) {
                reject (err);
                }
                else {
                resolve (atts [name]);
                }
                });
              });
            },
          setOne: function (f, name, value) {
            return new Promise (function (resolve, reject) {
              opmlGetAttributes (f, function (err, atts) {
                if (err) {
                reject (err);
                }
                else {
                atts [name] = value;
                opmlSetAttributes (f, atts, function (err, data) {
                if (err) {
                reject (err);
                }
                else {
                resolve (atts);
                }
                });
                }
                });
              });
            },
          deleteOne: function (f, name) {
            return new Promise (function (resolve, reject) {
              opmlGetAttributes (f, function (err, atts) {
                if (err) {
                reject (err);
                }
                else {
                if (atts [name] !== undefined) {
                delete atts [name];
                opmlSetAttributes (f, atts, function (err, data) {
                if (err) {
                reject (err);
                }
                else {
                resolve (atts);
                }
                });
                }
                else {
                resolve (atts);
                }
                }
                });
              });
            },
          makeEmpty: function (f) {
            return new Promise (function (resolve, reject) {
              var atts = new Object ();
              opmlSetAttributes (f, atts, function (err, data) {
                if (err) {
                reject (err);
                }
                else {
                resolve (atts);
                }
                });
              });
            },
          addGroup: function (f, groupAtts) {
            return new Promise (function (resolve, reject) {
              opmlGetAttributes (f, function (err, atts) {
                if (err) {
                reject (err);
                }
                else {
                for (var x in groupAtts) {
                atts [x] = groupAtts [x];
                }
                opmlSetAttributes (f, atts, function (err, data) {
                if (err) {
                reject (err);
                }
                else {
                resolve (atts);
                }
                });
                }
                });
              });
            },
          exists: function (f, name) {
            return new Promise (function (resolve, reject) {
              opmlGetAttributes (f, function (err, atts) {
                if (err) {
                reject (err);
                }
                else {
                resolve (atts [name] !== undefined);
                }
                });
              });
            }
          },
        parse: function (opmltext) { //9/13/21 by DW
          var theOutline = opmlPackage ().parse (opmltext);
          return (theOutline);
          },
        stringify: function (theOutline) { //9/13/21 by DW
          var opmltext = opmlPackage ().stringify (theOutline);
          return (opmltext);
          },
        getHeaders: function () {
          return (opGetHeaders ());
          },
        setHeaders: function (headers) {
          return (opSetHeaders (headers));
          },
        getCurrentOpml: function () {
          return (getCurrentOpml ());
          },
        getCurrentObject: function () {
          var head = opml.getHeaders ();
          var opmltext = getCurrentOpml ();
          var xstruct = $($.parseXML (opmltext));
          var adrbody = getXstuctBody (xstruct);
          var body = outlineToJson (adrbody, "outline");
          return ({head, body});
          },
        getMarkdown: function (opmltext) { //11/28/21 by DW
          Changes
            11/28/21; 10:57:12 AM by DW
              Get the markdown text from the OPML text.
          var theOutline = opmlPackage ().parse (opmltext);
          return (subsToMarkdown (theOutline.opml.body.subs [0]));
          }
        }
    //base64 verbs -- 9/8/21 by DW
      const base64 = {
        encode: function (theString) {
          return (btoa (theString));
          },
        decode: function (theEncodedString) {
          return (atob (theEncodedString));
          }
        };
    //oldschool verbs -- 11/2/21 by DW
      const oldschool = {
        buildBlog: function (blogname, server="http://drummercms.scripting.com/") {
          var url = server + "?blog=" + blogname;
          return (JSON.parse (http.readUrl (url, false)));
          }
        }
    //http verbs -- 11/5/21 by DW
      var http = {
        readUrl: function (url, flUseProxyServer) {
          Changes
            8/9/21; 4:27:37 PM by DW
              New optional param, flUseProxyServer, default true.
              If false, we make a direct call from Drummer, w/o using the proxy server.
              This is necessary if you're calling a server running on the local machine.
          if (flUseProxyServer === undefined) {
            flUseProxyServer = true;
            }
          if (flUseProxyServer) {
            return new Promise (function (resolve, reject) {
              console.log ("Using proxy server.");
              servercall ("httpreadurl", {url}, true, function (err, data) {
                if (err) {
                reject (err);
                }
                else {
                resolve (data);
                }
                });
              });
            }
          else {
            return new Promise (function (resolve, reject) {
              console.log ("Not using proxy server.");
              httpRequest (url, undefined, undefined, function (err, data) {
                if (err) {
                reject (err);
                }
                else {
                resolve (data);
                }
                });
              });
            }
          },
        derefUrl: function (url) { //9/17/21 by DW
          return new Promise (function (resolve, reject) {
            servercall ("derefurl", {url}, true, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data.longurl);
                }
              });
            });
          },
        client: function (options, flUseProxyServer=true) { //11/5/21 by DW
          Changes
            11/8/21; 9:30:48 AM by DW
              We provide a user-agent header, which can be overridden.
            11/7/21; 10:04:26 AM by DW
              Implement headers. If the caller provides headers, we add them to the headers in the request, we don't overwrite the whole collection of headers. That way if later we want to initialize the headers with something that identifies the request as coming from Drummer, we can just add it to the default request struct.
            11/5/21; 11:43:37 AM by DW
              A work in progress. The goal is to eventually have all the features of HTTP available to scripts running in Drummer.
              The names of values in the options struct come from the defiinitions for the jQuery call.
                https://www.w3schools.com/jquery/ajax_ajax.asp
            8/2/21; 2:19:56 PM by DW
              If filedata is not a PlainObject or a string, convert it to a string.
          var request = { //defaults
            type: "GET",
            url: undefined, //defaults to the current page
            data: undefined,
            params: undefined,
            headers: {
              "User-Agent": drummer.productname () + " v" + drummer.version ()
              }
            }
          if (options.headers !== undefined) { //11/7/21 by DW
            for (var x in options.headers) {
              request.headers [x] = options.headers [x];
              }
            }
          for (var x in options) {
            if (x != "headers") {
              request [x] = options [x];
              }
            }
          if (request.data !== undefined) {
            if (!$.isPlainObject (request.data) && (typeof (request.data) != "string")) { //8/2/21 by DW
              request.data = request.data.toString ();
              }
            }
          if (request.params !== undefined) {
            request.url += "?" + drummerBuildParamList (request.params);
            }
          if (flUseProxyServer) {
            return new Promise (function (resolve, reject) {
              var proxyRequest = {
                method: request.type,
                url: request.url,
                body: request.data,
                headers: request.headers
                };
              var jsontext = jsonStringify (proxyRequest);
              servercall ("httprequest", {request: jsontext}, true, function (err, data) {
                if (err) {
                reject (err);
                }
                else {
                resolve (data);
                }
                });
              });
            }
          else {
            return new Promise (function (resolve, reject) {
              $.ajax (request)
                .success (function (data, status) {
                resolve (data);
                })
                .error (function (status) {
                var err = JSON.parse (status.responseText);
                reject (err);
                });
              });
            }
          }
        }
    //github verbs -- 11/8/21 by DW
      function gitHubUploader (commitMessage=".") { //11/26/21 by DW
        let whenstart = new Date ();
        let theOutline = opWholeOutlineJstruct ();
        let commitMessage = getRandomSnarkySlogan (); //fix this -- 11/16/21 by DW
        console.log ("gitHubUploader");
        function notComment (node) {
          return (!getBoolean (node.isComment));
          }
        function notBlank (node) {
          return (trimWhitespace (node.text) != "");
          }
        function subsToMarkdown (theFileNode) {
          Changes
            11/26/21; 9:48:58 AM by DW
              Cribbed from Old School.
            11/23/21; 9:45:04 AM by DW
              Implement flSinglespaceMarkdown attribute, otherwise we double-space.
                http://scripting.com/drummer/blog/2021/11/23/192342.html?title=nextIterationOnMarkdowninanoutline
            11/21/21; 9:35:21 AM by DW
              Enclose in divMarkdownText, because it's a div not a span.
            11/19/21; 2:07:52 PM by DW
              No more indentation, and only output one newline per headline.
            10/30/21; 1:04:46 PM by DW
              Use spaces to indent in markdown, not tabs.
          let markdowntext = "", indentlevel = 0;
          function addlevel (theNode, flSinglespace=false) {
            var flAddExtraReturn = false;
            function add (s) {
              markdowntext += s + ((flSinglespace) ? "\n" : "\n\n");
              markdowntext += utils.filledString (" ", indentlevel) + s + "\n\n";
              markdowntext += s + "\n\n";
              }
            if (theNode.flSinglespaceMarkdown !== undefined) {
              var newSingleVal = getBoolean (theNode.flSinglespaceMarkdown);
              if (newSingleVal && (!flSinglespace)) {
                flAddExtraReturn = true;
                }
              flSinglespace = newSingleVal;
              }
            if (theNode.subs !== undefined) {
              theNode.subs.forEach (function (sub) {
                add (sub.text);
                indentlevel++;
                addlevel (sub, flSinglespace);
                indentlevel--;
                });
              if (flAddExtraReturn) {
                markdowntext += "\n";
                }
              }
            }
          addlevel (theFileNode);
          return (markdowntext);
          }
        theOutline.opml.body.subs.forEach (function (theLocation) { //each top level head is a location
          if (notComment (theLocation) && notBlank (theLocation)) {
            let locationpath = theLocation.text;
            if (theLocation.subs !== undefined) {
              theLocation.subs.forEach (function (theFile) {
                if (notComment (theLocation) && notBlank (theLocation)) {
                let filepath = theFile.text;
                let filecontent = subsToMarkdown (theFile);
                let locationparts = locationpath.split (":");
                let service = locationparts [0]; //for now just github
                switch (service) {
                case "github":
                let username = locationparts [1];
                let repository = locationparts [2];
                let basepath = locationparts [3];
                let path = basepath + filepath;
                console.log ("github: path = " + path + ", repository == " + repository + ", filecontent.length == " + filecontent.length);
                var returnedobj = github.upload (username, repository, path, filecontent, commitMessage);
                console.log ("github: returnedobj = " + jsonStringify (returnedobj));
                break;
                }
                }
                });
              }
            }
          });
        console.log ("gitHubUploader: " + secondsSince (whenstart) + " secs.");
        }
      function gitHubUploader (commitMessage=".") { //11/16/21 by DW
        let whenstart = new Date ();
        let theOutline = opWholeOutlineJstruct ();
        let commitMessage = getRandomSnarkySlogan (); //fix this -- 11/16/21 by DW
        console.log ("gitHubUploader");
        function notComment (node) {
          return (!getBoolean (node.isComment));
          }
        function notBlank (node) {
          return (trimWhitespace (node.text) != "");
          }
        function getFileContent (theFileNode) {
          var markdowntext = "", indentlevel = 0;
          function add (s) {
            markdowntext += s + "\n";
            markdowntext += filledString ("\t", indentlevel) + s + "\n";
            }
          function addsubs (node) {
            if (node.subs !== undefined) {
              node.subs.forEach (function (sub) {
                add (sub.text);
                indentlevel++;
                addsubs (sub);
                indentlevel--;
                });
              }
            }
          addsubs (theFileNode);
          return (markdowntext);
          }
        theOutline.opml.body.subs.forEach (function (theLocation) { //each top level head is a location
          if (notComment (theLocation) && notBlank (theLocation)) {
            let locationpath = theLocation.text;
            if (theLocation.subs !== undefined) {
              theLocation.subs.forEach (function (theFile) {
                if (notComment (theLocation) && notBlank (theLocation)) {
                let filepath = theFile.text;
                let filecontent = getFileContent (theFile);
                let locationparts = locationpath.split (":");
                let service = locationparts [0]; //for now just github
                switch (service) {
                case "github":
                let username = locationparts [1];
                let repository = locationparts [2];
                let basepath = locationparts [3];
                let path = basepath + filepath;
                console.log ("github: path = " + path + ", repository == " + repository + ", filecontent.length == " + filecontent.length);
                var returnedobj = github.upload (username, repository, path, filecontent, commitMessage);
                console.log ("github: returnedobj = " + jsonStringify (returnedobj));
                break;
                }
                }
                });
              }
            }
          });
        console.log ("gitHubUploader: " + secondsSince (whenstart) + " secs.");
        }
      var github = {
        connectViaOauth: function () {
          drummerOpenUrl ("https://github.com/login/oauth/authorize?client_id=" + drummerGetIdGithubClient () + "&scope=repo&redirect_url=" + encodeURIComponent (window.location.href));
          return (true);
          },
        getAccessToken: function () {
          return (localStorage.githubAccessToken);
          },
        disconnect: function () {
          delete localStorage.githubAccessToken;
          return (true);
          },
        download: function (username, repository, path) {
          return new Promise (function (resolve, reject) {
            var params = {
              username, repository, path, accessToken: github.getAccessToken ()
              };
            servercall ("downloadfromgithub", params, true, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              });
            });
          },
        upload: function (username, repository, path, data, message) {
          var options = {
            username, repository, path, message, accessToken: github.getAccessToken ()
            };
          return new Promise (function (resolve, reject) {
            var params = {
              options: jsonStringify (options)
              };
            serverpost ("uploadtogithub", params, true, data, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              });
            });
          },
        getUserInfo: function (username) {
          return new Promise (function (resolve, reject) {
            var params = {
              username, accessToken: github.getAccessToken ()
              };
            servercall ("githubgetuserinfo", params, true, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              });
            });
          },
        getDirectory: function (username, repository, path) {
          return new Promise (function (resolve, reject) {
            if (path === undefined) { //you can leave out the path param
              path = "";
              }
            var params = {
              username, repository, path, accessToken: github.getAccessToken ()
              };
            servercall ("githubgetdirectory", params, true, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              });
            });
          },
        glorp: function (commitMessage=".") {
          gitHubUploader (commitMessage);
          }
        }
    //rss verbs -- //11/19/21 by DW
      var rss = {
        readFeed: function (feedUrl) {
          return new Promise (function (resolve, reject) {
            var url = "http://feeder.scripting.com/returnjson?url=" + feedUrl;
            httpRequest (url, undefined, undefined, function (err, jsontext) {
              if (err) {
                reject (err);
                }
              else {
                try {
                var jstruct = JSON.parse (jsontext);
                resolve (jstruct);
                }
                catch (err) {
                reject (err);
                }
                }
              });
            });
          }
        }
    //daytona verbs -- 12/11/21 by DW
      var urlDaytonaServer = "http://daytona.scripting.com/";
      var daytona = {
        ping: function (urlOutline, collection) {
          return new Promise (function (resolve, reject) {
            servercall ("ping", {url: urlOutline, collection}, true, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              }, urlDaytonaServer);
            });
          },
        query: function (query, collection) {
          return new Promise (function (resolve, reject) {
            var endpoint;
            switch (collection) {
              case "drummerdocs": case "scriptingnews":
                endpoint = "getindexforcollection";
                break;
              case "drummeruser":
                endpoint = "getindexforuser";
                break;
              }
            servercall (endpoint, {query, collection}, true, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                debugger;
                data.forEach (function (item) {
                console.log (item.jsontext.length);
                item.jstruct = JSON.parse (item.jsontext);
                delete item.jsontext;
                });
                resolve (data);
                }
              }, urlDaytonaServer);
            });
          },
        removeOutlineRefs: function (urlOutline, collection) {
          return new Promise (function (resolve, reject) {
            servercall ("removeoutlinerefs", {url: urlOutline, collection}, true, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (true);
                }
              }, urlDaytonaServer);
            });
          },
        resetMyIndex: function (collection) {
          return new Promise (function (resolve, reject) {
            servercall ("resetmyindex", {collection}, true, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (true);
                }
              }, urlDaytonaServer);
            });
          }
        };
    //opml package -- 7/26/21 by DW
      function opmlPackage () {
        Changes
          7/26/21; 11:11:12 AM by DW
            By including the source of the OPML package here, and returning opml, we hide the name opml, which is used in JSS for a set of verbs related to OPML.
            So if you want to call opml.stringify you would instead call opmlPackage ().stringify. It's maintainable but you have to rebuild Drummer to get the changes.
        [[http://scripting.com/code/opmlpackage/client/opml.js]]
        return (opml);
        }
    //wordpress verbs -- 9/11/23 by DW
      const wordpress = {
        getServerAddress: function () {
          return ("https://drummer.land/");
          return ("http://localhost:1229/");
          },
        connectViaOauth: function () {
          const apiUrl = wordpress.getServerAddress () + "connect?urlapphomepage=" + encodeURIComponent (location.href);
          drummerOpenUrl (apiUrl);
          return (true);
          },
        getUserInfo: function () {
          console.log ("wordpress.getUserInfo");
          return new Promise (function (resolve, reject) {
            var params = {
              token: localStorage.wordpressAccessToken
              };
            servercall ("wordpressgetuserinfo", params, false, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              });
            });
          },
        getUserSites: function () {
          console.log ("wordpress.getUserSites");
          return new Promise (function (resolve, reject) {
            var params = {
              token: localStorage.wordpressAccessToken
              };
            servercall ("wordpressgetusersites", params, false, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              });
            });
          },
        getUserSubscriptions: function () {
          console.log ("wordpress.getUserSubscriptions");
          return new Promise (function (resolve, reject) {
            var params = {
              token: localStorage.wordpressAccessToken
              };
            servercall ("wordpressgetsubscriptions", params, false, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              });
            });
          },
        getSitePosts: function (idsite) {
          console.log ("wordpress.getSitePosts");
          return new Promise (function (resolve, reject) {
            var params = {
              token: localStorage.wordpressAccessToken,
              idsite
              };
            servercall ("wordpressgetsiteposts", params, false, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              });
            });
          },
        getSiteUsers: function (idsite) {
          console.log ("wordpress.getSiteUsers");
          return new Promise (function (resolve, reject) {
            var params = {
              token: localStorage.wordpressAccessToken,
              idsite
              };
            servercall ("wordpressgetsiteusers", params, false, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              });
            });
          },
        getSiteInfo: function (idsite) {
          console.log ("wordpress.getSiteInfo");
          return new Promise (function (resolve, reject) {
            var params = {
              token: localStorage.wordpressAccessToken,
              idsite
              };
            servercall ("wordpressgetsiteinfo", params, false, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              });
            });
          },
        getSiteMedialist: function (idsite) {
          console.log ("wordpress.getSiteMedialist");
          return new Promise (function (resolve, reject) {
            var params = {
              token: localStorage.wordpressAccessToken,
              idsite
              };
            servercall ("wordpressgetsitemedialist", params, false, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              });
            });
          },
        getPost: function (idsite, idpost) {
          console.log ("wordpress.getPost");
          return new Promise (function (resolve, reject) {
            var params = {
              token: localStorage.wordpressAccessToken,
              idsite,
              idpost
              };
            servercall ("wordpressgetpost", params, false, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              });
            });
          },
        addPost: function (idsite, thePost) {
          console.log ("wordpress.newPost");
          return new Promise (function (resolve, reject) {
            var params = {
              token: localStorage.wordpressAccessToken,
              idsite,
              jsontext: jsonStringify (thePost)
              };
            servercall ("wordpressaddpost", params, false, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              });
            });
          },
        updatePost: function (idsite, idpost, thePost) {
          console.log ("wordpress.updatePost");
          return new Promise (function (resolve, reject) {
            var params = {
              token: localStorage.wordpressAccessToken,
              idsite,
              idpost,
              jsontext: jsonStringify (thePost)
              };
            servercall ("wordpressupdatepost", params, false, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              });
            });
          },
        deletePost: function (idsite, idpost, thePost) {
          console.log ("wordpress.deletePost");
          return new Promise (function (resolve, reject) {
            var params = {
              token: localStorage.wordpressAccessToken,
              idsite,
              idpost
              };
            servercall ("wordpressdeletepost", params, false, function (err, data) {
              if (err) {
                reject (err);
                }
              else {
                resolve (data);
                }
              });
            });
          }
        }
  archive/readme.md
    ## Integraging with Little Outliner
    Include script tags in this order before you include `code.js`.
    ```
    ```
    This copy of `verbs.js` replaces the version that is included in LO2.
    The function `runScriptText` replaces `eval`. For it to work with the menubar you'll look in `code.js` for the function `startScriptsMenu` and the callback passed into `xmlBuildMenusFromOpmltext`.
  jstokens.js
    // Copyright 2014, 2015, 2016, 2017, 2018, 2019, 2020 Simon Lydell
    // License: MIT.
    // From: https://github.com/lydell/js-tokens
    var IdentifierName, JSXIdentifier, JSXPunctuator, JSXString, JSXText, KeywordsWithExpressionAfter, KeywordsWithNoLineTerminatorAfter, LineTerminatorSequence, MultiLineComment, Newline, NumericLiteral, Punctuator, RegularExpressionLiteral, SingleLineComment, StringLiteral, Template, TokensNotPrecedingObjectLiteral, TokensPrecedingExpression, WhiteSpace, jsTokens;
    RegularExpressionLiteral = /\/(?![*\/])(?:\[(?:(?![\]\\]).|\\.)*\]|(?![\/\]\\]).|\\.)*(\/[$_\u200C\u200D\p{ID_Continue}]*|\\)?/yu;
    Punctuator = /--|\+\+|&&|\|\||=>|\.{3}|\??\.(?!\d)|\?{2}|(?:[+\-%&|^]|\*{1,2}|<{1,2}|>{1,3}|!=?|={1,2}|\/(?![\/*]))=?|[?~,:;[\](){}]/y;
    IdentifierName = /(?=[$_\p{ID_Start}\\])(?:[$_\u200C\u200D\p{ID_Continue}]|\\u[\da-fA-F]{4}|\\u\{[\da-fA-F]+\})+/yu;
    StringLiteral = /(['"])(?:(?!\1)[^\\\n\r]|\\(?:\r\n|[^]))*(\1)?/y;
    NumericLiteral = /(?:0[xX][\da-fA-F]+|0[oO][0-7]+|0[bB][01]+)n?|0n|[1-9]\d*n|(?:(?:0(?!\d)|0\d*[89]\d*|[1-9]\d*)(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?|0[0-7]+/y;
    Template = /[`}](?:[^`\\$]|\\[^]|\$(?!\{))*(`|\$\{)?/y;
    WhiteSpace = /[\t\v\f\ufeff\p{Zs}]+/yu;
    LineTerminatorSequence = /\r?\n|[\r\u2028\u2029]/y;
    MultiLineComment = /\/\*(?:[^*]|\*(?!\/))*(\*\/)?/y;
    SingleLineComment = /\/\/.*/y;
    JSXPunctuator = /[<>.:={}]|\/(?![\/*])/y;
    JSXIdentifier = /[$_\p{ID_Start}][$_\u200C\u200D\p{ID_Continue}-]*/yu;
    JSXString = /(['"])(?:(?!\1)[^])*(\1)?/y;
    JSXText = /[^<>{}]+/y;
    TokensPrecedingExpression = /^(?:[\/+-]|\.{3}|\?(?:InterpolationIn(?:JSX|Template)|NoLineTerminatorHere|NonExpressionParenEnd|UnaryIncDec))?$|[{}([,;<>=*%&|^!~?:]$/;
    TokensNotPrecedingObjectLiteral = /^(?:=>|[;\]){}]|else|\?(?:NoLineTerminatorHere|NonExpressionParenEnd))?$/;
    KeywordsWithExpressionAfter = /^(?:await|case|default|delete|do|else|instanceof|new|return|throw|typeof|void|yield)$/;
    KeywordsWithNoLineTerminatorAfter = /^(?:return|throw|yield)$/;
    Newline = RegExp(LineTerminatorSequence.source);
    jsTokens = function*(input, {jsx = false} = {}) {
      var braces, firstCodePoint, isExpression, lastIndex, lastSignificantToken, length, match, mode, nextLastIndex, nextLastSignificantToken, parenNesting, postfixIncDec, punctuator, stack;
      ({length} = input);
      lastIndex = 0;
      lastSignificantToken = "";
      stack = [
        {tag: "JS"}
        ];
      braces = [];
      parenNesting = 0;
      postfixIncDec = false;
      while (lastIndex < length) {
        mode = stack[stack.length - 1];
        switch (mode.tag) {
          case "JS":
          case "JSNonExpressionParen":
          case "InterpolationInTemplate":
          case "InterpolationInJSX":
            if (input[lastIndex] === "/" && (TokensPrecedingExpression.test(lastSignificantToken) || KeywordsWithExpressionAfter.test(lastSignificantToken))) {
              RegularExpressionLiteral.lastIndex = lastIndex;
              if (match = RegularExpressionLiteral.exec(input)) {
                lastIndex = RegularExpressionLiteral.lastIndex;
                lastSignificantToken = match[0];
                postfixIncDec = true;
                yield ({
                type: "RegularExpressionLiteral",
                value: match[0],
                closed: match[1] !== void 0 && match[1] !== "\\"
                });
                continue;
              }
            }
            Punctuator.lastIndex = lastIndex;
            if (match = Punctuator.exec(input)) {
              punctuator = match[0];
              nextLastIndex = Punctuator.lastIndex;
              nextLastSignificantToken = punctuator;
              switch (punctuator) {
                case "(":
                if (lastSignificantToken === "?NonExpressionParenKeyword") {
                stack.push({
                tag: "JSNonExpressionParen",
                nesting: parenNesting
                });
                }
                parenNesting++;
                postfixIncDec = false;
                break;
                case ")":
                parenNesting--;
                postfixIncDec = true;
                if (mode.tag === "JSNonExpressionParen" && parenNesting === mode.nesting) {
                stack.pop();
                nextLastSignificantToken = "?NonExpressionParenEnd";
                postfixIncDec = false;
                }
                break;
                case "{":
                Punctuator.lastIndex = 0;
                isExpression = !TokensNotPrecedingObjectLiteral.test(lastSignificantToken) && (TokensPrecedingExpression.test(lastSignificantToken) || KeywordsWithExpressionAfter.test(lastSignificantToken));
                braces.push(isExpression);
                postfixIncDec = false;
                break;
                case "}":
                switch (mode.tag) {
                case "InterpolationInTemplate":
                if (braces.length === mode.nesting) {
                Template.lastIndex = lastIndex;
                match = Template.exec(input);
                lastIndex = Template.lastIndex;
                lastSignificantToken = match[0];
                if (match[1] === "${") {
                lastSignificantToken = "?InterpolationInTemplate";
                postfixIncDec = false;
                yield ({
                type: "TemplateMiddle",
                value: match[0]
                });
                } else {
                stack.pop();
                postfixIncDec = true;
                yield ({
                type: "TemplateTail",
                value: match[0],
                closed: match[1] === "`"
                });
                }
                continue;
                }
                break;
                case "InterpolationInJSX":
                if (braces.length === mode.nesting) {
                stack.pop();
                lastIndex += 1;
                lastSignificantToken = "}";
                yield ({
                type: "JSXPunctuator",
                value: "}"
                });
                continue;
                }
                }
                postfixIncDec = braces.pop();
                nextLastSignificantToken = postfixIncDec ? "?ExpressionBraceEnd" : "}";
                break;
                case "]":
                postfixIncDec = true;
                break;
                case "++":
                case "--":
                nextLastSignificantToken = postfixIncDec ? "?PostfixIncDec" : "?UnaryIncDec";
                break;
                case "<":
                if (jsx && (TokensPrecedingExpression.test(lastSignificantToken) || KeywordsWithExpressionAfter.test(lastSignificantToken))) {
                stack.push({tag: "JSXTag"});
                lastIndex += 1;
                lastSignificantToken = "<";
                yield ({
                type: "JSXPunctuator",
                value: punctuator
                });
                continue;
                }
                postfixIncDec = false;
                break;
                default:
                postfixIncDec = false;
              }
              lastIndex = nextLastIndex;
              lastSignificantToken = nextLastSignificantToken;
              yield ({
                type: "Punctuator",
                value: punctuator
              });
              continue;
            }
            IdentifierName.lastIndex = lastIndex;
            if (match = IdentifierName.exec(input)) {
              lastIndex = IdentifierName.lastIndex;
              nextLastSignificantToken = match[0];
              switch (match[0]) {
                case "for":
                case "if":
                case "while":
                case "with":
                if (lastSignificantToken !== "." && lastSignificantToken !== "?.") {
                nextLastSignificantToken = "?NonExpressionParenKeyword";
                }
              }
              lastSignificantToken = nextLastSignificantToken;
              postfixIncDec = !KeywordsWithExpressionAfter.test(match[0]);
              yield ({
                type: "IdentifierName",
                value: match[0]
              });
              continue;
            }
            StringLiteral.lastIndex = lastIndex;
            if (match = StringLiteral.exec(input)) {
              lastIndex = StringLiteral.lastIndex;
              lastSignificantToken = match[0];
              postfixIncDec = true;
              yield ({
                type: "StringLiteral",
                value: match[0],
                closed: match[2] !== void 0
              });
              continue;
            }
            NumericLiteral.lastIndex = lastIndex;
            if (match = NumericLiteral.exec(input)) {
              lastIndex = NumericLiteral.lastIndex;
              lastSignificantToken = match[0];
              postfixIncDec = true;
              yield ({
                type: "NumericLiteral",
                value: match[0]
              });
              continue;
            }
            Template.lastIndex = lastIndex;
            if (match = Template.exec(input)) {
              lastIndex = Template.lastIndex;
              lastSignificantToken = match[0];
              if (match[1] === "${") {
                lastSignificantToken = "?InterpolationInTemplate";
                stack.push({
                tag: "InterpolationInTemplate",
                nesting: braces.length
                });
                postfixIncDec = false;
                yield ({
                type: "TemplateHead",
                value: match[0]
                });
              } else {
                postfixIncDec = true;
                yield ({
                type: "NoSubstitutionTemplate",
                value: match[0],
                closed: match[1] === "`"
                });
              }
              continue;
            }
            break;
          case "JSXTag":
          case "JSXTagEnd":
            JSXPunctuator.lastIndex = lastIndex;
            if (match = JSXPunctuator.exec(input)) {
              lastIndex = JSXPunctuator.lastIndex;
              nextLastSignificantToken = match[0];
              switch (match[0]) {
                case "<":
                stack.push({tag: "JSXTag"});
                break;
                case ">":
                stack.pop();
                if (lastSignificantToken === "/" || mode.tag === "JSXTagEnd") {
                nextLastSignificantToken = "?JSX";
                postfixIncDec = true;
                } else {
                stack.push({tag: "JSXChildren"});
                }
                break;
                case "{":
                stack.push({
                tag: "InterpolationInJSX",
                nesting: braces.length
                });
                nextLastSignificantToken = "?InterpolationInJSX";
                postfixIncDec = false;
                break;
                case "/":
                if (lastSignificantToken === "<") {
                stack.pop();
                if (stack[stack.length - 1].tag === "JSXChildren") {
                stack.pop();
                }
                stack.push({tag: "JSXTagEnd"});
                }
              }
              lastSignificantToken = nextLastSignificantToken;
              yield ({
                type: "JSXPunctuator",
                value: match[0]
              });
              continue;
            }
            JSXIdentifier.lastIndex = lastIndex;
            if (match = JSXIdentifier.exec(input)) {
              lastIndex = JSXIdentifier.lastIndex;
              lastSignificantToken = match[0];
              yield ({
                type: "JSXIdentifier",
                value: match[0]
              });
              continue;
            }
            JSXString.lastIndex = lastIndex;
            if (match = JSXString.exec(input)) {
              lastIndex = JSXString.lastIndex;
              lastSignificantToken = match[0];
              yield ({
                type: "JSXString",
                value: match[0],
                closed: match[2] !== void 0
              });
              continue;
            }
            break;
          case "JSXChildren":
            JSXText.lastIndex = lastIndex;
            if (match = JSXText.exec(input)) {
              lastIndex = JSXText.lastIndex;
              lastSignificantToken = match[0];
              yield ({
                type: "JSXText",
                value: match[0]
              });
              continue;
            }
            switch (input[lastIndex]) {
              case "<":
                stack.push({tag: "JSXTag"});
                lastIndex++;
                lastSignificantToken = "<";
                yield ({
                type: "JSXPunctuator",
                value: "<"
                });
                continue;
              case "{":
                stack.push({
                tag: "InterpolationInJSX",
                nesting: braces.length
                });
                lastIndex++;
                lastSignificantToken = "?InterpolationInJSX";
                postfixIncDec = false;
                yield ({
                type: "JSXPunctuator",
                value: "{"
                });
                continue;
            }
          }
        WhiteSpace.lastIndex = lastIndex;
        if (match = WhiteSpace.exec(input)) {
          lastIndex = WhiteSpace.lastIndex;
          yield ({
            type: "WhiteSpace",
            value: match[0]
            });
          continue;
          }
        LineTerminatorSequence.lastIndex = lastIndex;
        if (match = LineTerminatorSequence.exec(input)) {
          lastIndex = LineTerminatorSequence.lastIndex;
          postfixIncDec = false;
          if (KeywordsWithNoLineTerminatorAfter.test(lastSignificantToken)) {
            lastSignificantToken = "?NoLineTerminatorHere";
            }
          yield ({
            type: "LineTerminatorSequence",
            value: match[0]
            });
          continue;
          }
        MultiLineComment.lastIndex = lastIndex;
        if (match = MultiLineComment.exec(input)) {
          lastIndex = MultiLineComment.lastIndex;
          if (Newline.test(match[0])) {
            postfixIncDec = false;
            if (KeywordsWithNoLineTerminatorAfter.test(lastSignificantToken)) {
              lastSignificantToken = "?NoLineTerminatorHere";
              }
            }
          yield ({
            type: "MultiLineComment",
            value: match[0],
            closed: match[1] !== void 0
            });
          continue;
          }
        SingleLineComment.lastIndex = lastIndex;
        if (match = SingleLineComment.exec(input)) {
          lastIndex = SingleLineComment.lastIndex;
          postfixIncDec = false;
          yield ({
            type: "SingleLineComment",
            value: match[0]
            });
          continue;
          }
        firstCodePoint = String.fromCodePoint(input.codePointAt(lastIndex));
        lastIndex += firstCodePoint.length;
        lastSignificantToken = firstCodePoint;
        postfixIncDec = false;
        yield ({
          type: mode.tag.startsWith("JSX") ? "JSXInvalid" : "Invalid",
          value: firstCodePoint
          });
        }
      return void 0;
      };
  readme.md
    # concordScript
      The source code for the scripting functions in the Drummer outliner.
    ### What's here
      scripting.js contains one function, runScriptText, which is the equivalent of eval in JavaScript.
      verbs.js defines all the verbs in the language, this is where most of the work went, and should continue.
      jstokens.js -- part of the script compiler.
    ### Included files
      These files must be included for the code to run.
        * http://scripting.com/publicfolder/code/jsParserForLO2/acorn.js
        * http://scripting.com/publicfolder/code/jsParserForLO2/escodegen.browser.js
        * http://scripting.com/code/wpidentity/client/api.js
    ### How to read in Drummer
      All this code is managed in Drummer of course.
      To read all the code, which is in source.opml:
        1. Copy this URL on the clipoard.
        2. Open Drummer.
        3. Choose Open URL from the File menu.
        4. Paste the URL, click OK.
build script
  nodeEditorSuite.utilities.buildConcordScript ()