JS-P1


/**.:....1....:....2....:....3....:....4....:....5....:....6....:.
 *
 *  JS-P1: Input/Output
 *
 *  by John P. Spurgeon
 *  https://errless.blogspot.com/2018/06/js-p1.html
 *
 *  revised on 16 June 2018
 */

// to Don Knuth

/** 
 *  Table of Contents
 *
 *  Preface
 *  Introduction
 *
 *  Part 1. A Library
 *
 *    1.00. Lib
 *    1.01. Timer
 *    1.02. ArrayStringBuffer
 *    1.03. StringStringBuffer
 *    1.04. StringBuilder
 *    1.05. PopupWindow
 *    1.06. FunStats
 *    1.07. ReadonlyFunStats
 *    1.08. Reader
 *    1.09. Writer
 *    1.10. ReaderWriter
 *    1.11. PageElement
 *    1.12. Assemble the object.
 *
 *  Part 2. A Webpage
 *
 *    2.00. Page
 *    2.01. setInnerHTML
 *    2.02. setHtml
 *    2.03. setHead
 *    2.04. setStyle
 *    2.05. setBody
 *    2.06. addClickEventListeners
 *
 *  Part 3. Input/Output Objects
 *
 *    3.01. System.io
 *    3.02. System.out
 *
 *  PART 4. Page.io
 *
 */

/**
 *  Preface
 *
 */

/**
 *  Introduction
 *
 *  This is a unique variation of the traditional first program
 *  to write when learning a new programming language, namely,
 *
 *          Print the words
 *              hello, world
 *
 *  See:
 *
 *  1. Brian W. Kernighan and Dennis M. Ritchie,
 *     _The C Programming Language_, 2nd edition (1988, 1978);
 *     Chapter 1, "A Tutorial Introduction";
 *     Section 1.1, "Getting Started," page 5.
 *
 *  2. https://en.wikipedia.org/wiki/%22Hello,_World!%22_program
 */

/**
 *          Genuine examples of large software programs are rarely
 *       found in books; yet students of computer science will get
 *          a warped perspective of the subject if they experience
 *                    only "toy programs" of textbook proportions.
 *
 *                     -- DONALD E. KNUTH, TeX: The Program (1986)
 *
 *    We all begin as close readers. Even before we learn to read,
 *    the process of being read aloud to, and of listening, is one
 *    in which we are taking in one word after another, one phrase
 *    at a time, in which we are paying attention to whatever each
 *    word or phrase is transmitting. Word by word is how we learn
 *   to hear and then read, which seems fitting, because it is how
 *       the books we are reading were written in the first place.
 *
 *                 -- FRANCINE PROSE, Reading Like a Writer (2006)
 */

/**
 *  PART 1. A Library
 *
 *  1.00. Lib
 *  1.01. Timer
 *  1.02. ArrayStringBuffer
 *  1.03. StringStringBuffer
 *  1.04. StringBuilder
 *  1.05. PopupWindow
 *  1.06. FunStats
 *  1.07. ReadonlyFunStats
 *  1.08. Reader
 *  1.09. Writer
 *  1.10. ReaderWriter
 *  1.11. PageElement
 *  1.12. Assemble the object.
 */

/**
 *  1.00. Lib
 */
Lib = (function makeLib()
{
  /** 1.01. Timer.
   *
   */
  function Timer(name)
  {
    const that = this;
    let t1 = 0, t2 = 0;

    that.getElapsedTime = function() {
      return t2 - t1;
    };
    that.getUnits = function() {
      return "ms";
    }
    that.start = function() {
      t1 = getTimeInMs();
      return that;
    };
    that.stop = function() {
      t2 = getTimeInMs();
      return that;
    };
    that.toString = function() {
      let string = "snapshotOfThatTimer = {"
      string += "\n  name: \"" + name + '"';
      string += ",\n  elapsedTime: " + that.getElapsedTime();
      string += ",\n  units: \"" + that.getUnits() + '"';
      string += ",\n  t1: " + t1;
      string += ",\n  t2: " + t2;
      string += "\n};";
      return string;
    };

    function getTimeInMs() {
      return performance.now();
    }
  }

  /**
   *  1.02. StringStringBuffer
   *
   */
  function StringStringBuffer()
  {
    const that = this;
    let string = "";

    that.appendString = function(s) { string += s; }
    that.clearBuffer = function() { string = ""; }
    that.toString = function() { return string; }
  }

  /**
   *  1.03. ArrayStringBuffer
   *
   */
  function ArrayStringBuffer()
  {
    const that = this;
    let array = [];

    that.appendString = function(s) { array.push(s); };
    that.clearBuffer = function() { array = []; };
    that.toString = function() { return array.join(""); };
  }

  /**
   *  1.04. StringBuilder
   *
   */
  function StringBuilder(buffer)
  {
    const that = this;
    const sb = (buffer === undefined) ?
      new ArrayStringBuffer : buffer;

    that.append = function(object) {
      return appendString(object.toString());
    };
    that.appendln = function(object) {
      return appendString(object.toString() + "\n");
    };
    that.replace = function(object) {
      return that.erase().append(object);
    };
    that.replaceln = function(object) {
      return that.erase().appendln(object);
    };
    that.erase = function() {
      sb.clearBuffer();
      return that;
    };
    that.toString = function() {
      return sb.toString();
    };

    function appendString(s) {
      sb.appendString(s);
      return that;
    }
  }

  /**
   *  1.05. PopupWindow
   *
   */
  function PopupWindow(method)
  {
    const that = this;
    const contents = new StringBuilder();
    let footnote = "";
    let enabled = true;

    if (method === undefined || method === null) {
      method = confirm;
      footnote = "\n(Cancel disables this window.)"
    }

    that.appendText = function(moreText) {
      contents.append(moreText);
      return that;
    };
    that.replaceText = function(newText) {
      contents.replace(newText);
      return that;
    };
    that.getText = function() {
      return contents.toString();
    };
    that.showWindow = function() {
      if (enabled) {
        if (!method(contents.toString() + footnote)) {
          enabled = false;
        }
      }
      return that;
    };
    that.enable = function() {
      enabled = true;
    };
    that.disable = function() {
      enabled = false;
    };    
  }

  /**
   *  1.06. FunStats
   *
   */
  function FunStats(kind, units)
  {
    const that = this;
    const INDENT1 = "  ";
    const INDENT2 = INDENT1 + INDENT1;
    const UNDEFINED = "undefined";
    const values = [];
    let sum, min, max;

    that.add = function(value) {
      values.push(value);
      if (values.length === 1) {
        sum = min = max = value;
      }
      else {
        sum += value;
        if (value < min) {
          min = value;
        }
        else if (value > max) {
          max = value;
        }
      }
    };
    that.count = function() { return values.length; };
    that.sum = function() { return getValue(sum); };
    that.min = function() { return getValue(min); };
    that.max = function() { return getValue(max); };
    that.avg = function() { return computeValue(getAvg); };
    that.sd = function() { return computeValue(getSD); };
    that.toString = function() {
      const sb = new StringBuilder();
      sb.appendln(INDENT1 + kind + ": " + that.count());
      sb.appendln(getLabel("sum") + that.sum());
      sb.appendln(getLabel("min") + that.min());
      sb.appendln(getLabel("max") + that.max());
      sb.appendln(getLabel("avg") + that.avg());
      sb.appendln(getLabel("sd") + that.sd());
      return sb.toString();
    };

    function getLabel(statName) {
      return INDENT2 + statName + " (" + units + "): ";
    }
    function getValue(statVariable) {
      return that.count() == 0 ? UNDEFINED : statVariable;
    }
    function computeValue(statFunction) {
      return that.count() == 0 ? UNDEFINED : statFunction();
    }
    function getAvg() { return sum / values.length; }
    function getSD() { return Math.sqrt(getVariance()); }
    function getVariance() {
      const n = values.length;
      const avg = sum / n;
      let sumOfSquares = 0;
      for (let i = 0; i < n; i++) {
        const diff = values[i] - avg;
        sumOfSquares += diff * diff;
      }
      return sumOfSquares / n;
    }
  }

  /** 1.07. ReadonlyFunStats.
   *
   */
  function ReadonlyFunStats(stats)
  {
    const that = this;

    that.count = function() { return stats.count(); };
    that.sum = function() { return stats.sum(); };
    that.min = function() { return stats.min(); };
    that.max = function() { return stats.max(); };
    that.avg = function() { return stats.avg(); };
    that.sd = function() { return stats.sd(); };
    that.toString = function() { return stats.toString(); }
  }

  /** 1.08. Reader.
   *
   */
  function Reader(name, readMethod)
  {
    const that = this;
    const timer = new Timer();
    const stats = new FunStats("reads", timer.getUnits());
    const readonlyStats = new ReadonlyFunStats(stats);

    that.read = function(args) {
      timer.start();
      const contents = readMethod(args);
      stats.add(timer.stop().getElapsedTime());
      return contents;
    };
    that.getReaderFunStats = function() {
      return readonlyStats;
    };
    that.toString = function() {
      return name + "\n" + readonlyStats.toString();
    };
  }

  /** 1.09. Writer.
   *
   */
  function Writer(name, appendMethod, replaceMethod)
  {
    const that = this;
    const timer = new Timer();
    const stats = new FunStats("writes", timer.getUnits());
    const readonlyStats = new ReadonlyFunStats(stats);

    that.append = function(object) {
      return appendString(stringify(object));
    };
    that.appendln = function(object) {
      return appendString(stringify(object) + "\n");
    };
    that.replace = function(object) {
      return replaceString(stringify(object));
    };
    that.replaceln = function(object) {
      return replaceString(stringify(object) + "\n");
    };
    that.erase = function(object) {
      return eraseString();
    };
    that.getWriterFunStats = function() {
      return readonlyStats;
    };
    that.toString = function() {
      return name + "\n" + readonlyStats.toString();
    };

    function stringify(object) {
      return object !== undefined && object !== null ?
        object.toString() : "";
    }
    function appendString(notUnefinedNotNullString) {
      timer.start();
      appendMethod(notUnefinedNotNullString);
      stats.add(timer.stop().getElapsedTime());
      return that;
    }
    function replaceString(notUnefinedNotNullString) {
      timer.start();
      replaceMethod(notUnefinedNotNullString);
      stats.add(timer.stop().getElapsedTime());
      return that;
    }
    function eraseString() {
      timer.start();
      replaceMethod("");
      stats.add(timer.stop().getElapsedTime());
      return that;
    }
  }

  /** 1.10. ReaderWriter.
   *
   */
  function ReaderWriter(
    name,
    readMethod,
    appendMethod,
    replaceMethod)
  {
    const that = this;

    Reader.call(that, name, readMethod);
    Writer.call(that, name, appendMethod, replaceMethod);

    that.toString = function() {
      sb = new StringBuilder();
      sb.appendln(name);
      sb.append(that.getReaderFunStats());
      sb.append(that.getWriterFunStats());
      return sb.toString();
    };
  }

  /** 1.11. PageElement.
   *
   */
  function PageElement(element)
  {
    const that = this;

    that.bindByTagName = function(name, i) {
      if (i === undefined) i = 0;
      element = document.getElementsByTagName(name)[i];
      return this;
    }
    that.bindById = function(id) {
      element = document.getElementById(id);
      return this;
    }
    that.setText = function(contents) {
      return set("innerText", contents);
    };
    that.setHtml = function(contents) {
      return set("innerHTML", contents);
    };
    that.setValue = function(contents) {
      return set("value", contents);
    };
    that.getText = function() {
      return get("innerText");
    };
    that.getHtml = function() {
      return get("innerHTML");
    };
    that.getValue = function() {
    return get("value");
    };

    function set(propertyName, contents) {
      if (element === null || element === undefined) {
        return null;
      }
      element[propertyName] = contents.toString();
      return element;
    }
    function get(propertyName) {
      return (element != null && element != undefined) ?
        element[propertyName] : null;
    }
  };

  /** 1.12. Assemble the object.
   *
   *  Now that all the components of the library are defined, we
   *  assemble them together in an object, freeze the object, and
   *  return it.
   *
   *  See also: 1.00. Lib.
   */
  return Object.freeze({
    Timer: Timer,
    ArrayStringBuffer: ArrayStringBuffer,
    StringStringBuffer: StringStringBuffer,
    StringBuilder: StringBuilder,
    PopupWindow: PopupWindow,
    FunStats: FunStats,
    ReadonlyFunStats: ReadonlyFunStats,
    Reader: Reader,
    Writer: Writer,
    ReaderWriter: ReaderWriter,
    PageElement: PageElement,
  });

})();

/*..:....1....:....2....:....3....:....4....:....5....:....6....:.
 *
 *  PART 2. A Webpage
 *
 *  2.00. Page
 *  2.01. setInnerHTML
 *  2.02. setHtml
 *  2.03. setHead
 *  2.04. setStyle
 *  2.05. setBody
 *  2.06. addElementsToPage
 *  2.07. addClickEventListeners
 *  2.08. return page
 */

/**
 *  2.00. Page
 *
 */

Page = (function makePage() {

  const helloButtonId = "hello";
  const resetButtonId = "reset";
  const dcodeButtonId = "dcode";
  const statsButtonId = "stats";
  const a51TextAreaId = "a51ta";
  const bpxTextAreaId = "bpxta";
  const page = {};

  /** 2.01. setInnerHTML
   *
   */
  function setInnerHTML(tagName, html)
  {
    const e = (new Lib.PageElement()).bindByTagName(tagName);
    return e.setHtml(html);
  }

  /** 2.02. setHtml
   *
   */
  (function setHtml()
  {
    const html = new Lib.StringBuilder();

    html.appendln("<head>");
    html.appendln("</head>");
    html.appendln("<body>");
    html.appendln("</body>");

    return setInnerHTML("html", html);
  })();

  /** 2.03. setHead
   *
   */
  (function setHead()
  {
    const html = new Lib.StringBuilder();

    html.appendln("<title>ErrLess JS-P1</title>");
    html.appendln("<style>");
    html.appendln("</style>");

    return setInnerHTML("head", html);
  })();

  /** 2.04. setStyle
   *
   */
  (function setStyle()
  {
    const css = new Lib.StringBuilder();

    css.appendln("div {");
    css.appendln("  display: block;");
    css.appendln("}");

    css.appendln("h1 {");
    css.appendln("  color: black;");
    css.appendln("}");

    css.appendln("h2 {");
    css.appendln("  color: darkgray;");
    css.appendln("}");

    css.appendln("textarea {");
    css.appendln("  width: 32em;");
    //css.appendln("  height: 51em;");
    css.appendln("  height: 20em;");
    css.appendln("}");

    css.appendln("button {");
    css.appendln("  margin-top: 0.5em;");
    css.appendln("}");

    css.appendln("#" + resetButtonId + " {");
    css.appendln("  background-color: tomato;");
    css.appendln("  color: white;");
    css.appendln("}");

    css.appendln("#" + dcodeButtonId + " {");
    css.appendln("  background-color: black;");
    css.appendln("  color: white;");
    css.appendln("}");

    css.appendln("#" + helloButtonId + " {");
    css.appendln("  background-color: green;");
    css.appendln("  color: white;");
    css.appendln("}");

    css.appendln("#" + statsButtonId + " {");
    css.appendln("  background-color: gold;");
    css.appendln("  color: black;");
    css.appendln("}");

    return setInnerHTML("style", css);
  })();

  /** 2.05. setBody
   *
   */
  (function setBody()
  {
    const blog = "errless.blogspot.com";
    const post = "/2018/06/js1-io.html";
    const http = "https://" + blog + post;
    const link = '<a href="' + http + '">source code</a>';

    const html = new Lib.StringBuilder();

    html.appendln("<center>");

    html.appendln("<div>");
    html.appendln("<h1>Input/Output</h1>");
    html.append('<button id="' + helloButtonId + '">');
    html.append('Page.io.broadcast("hello, world");');
    html.appendln('</button>');
    html.append('<button id="' + resetButtonId + '">');
    html.append('Reset');
    html.appendln('</button>');
    html.appendln("</div>");

    html.appendln("<div>");
    html.appendln("<h2>Area 51</h2>");
    html.append("<textarea id=" + '"');
    html.append(a51TextAreaId + '"' + ">");
    html.appendln("</textarea>");
    html.appendln("</br>");
    html.append('<button id="' + statsButtonId + '">');
    html.append('Page.io.a51.replace(Page.io);');
    html.appendln('</button>');
    html.appendln("</div>");

    html.appendln("<div>");
    html.appendln("<h2>Bletchley Park</h2>");
    html.append("<textarea id=" + '"');
    html.append(bpxTextAreaId + '"' + ">");
    html.appendln("</textarea>");
    html.appendln("</br>");
    html.append('<button id="' + dcodeButtonId + '">');
    html.append("Page.io.popup.replace(Page.io.bpx.read());");
    html.appendln('</button>');
    html.appendln("</div>");

    html.appendln("<p>" + link);

    html.appendln("</center>");

    return setInnerHTML("body", html);
  })();

  /** 2.06. addElementsToPage
   *
   */
   (function addElementsToPage()
   {
     page.helloButton = document.getElementById(helloButtonId);
     page.statsButton = document.getElementById(statsButtonId);
     page.dcodeButton = document.getElementById(dcodeButtonId);
     page.resetButton = document.getElementById(resetButtonId);
     page.a51TextArea = document.getElementById(a51TextAreaId);
     page.bpxTextArea = document.getElementById(bpxTextAreaId);
   })();


  /** 2.07. addClickEventListeners
   *
   */
  (function addClickEventListeners()
  {
    page.helloButton.addEventListener("click",
      function() { Page.io.broadcast("hello, world"); }
    );
    page.statsButton.addEventListener("click",
      function() { Page.io.a51.replace(Page.io); }
    );
    page.dcodeButton.addEventListener("click",
      function() {
        Page.io.popup.replace(Page.io.bpx.read());
      }
    );
    page.resetButton.addEventListener("click",
      function() { Page.io.reset(); }
    );
  })();

  /** 2.08. return page
   *
   */
  return page;

})();

/**.:....1....:....2....:....3....:....4....:....5....:....6....:.
 *
 *  PART 3. System
 *
 */

System = {};

/** 3.01. System.io
 *
 */
System.io = (function makeSystemIO()
{
  const asb = new Lib.ArrayStringBuffer();
  const ssb = new Lib.StringStringBuffer()
  const logA = new Lib.StringBuilder(asb);
  const logS = new Lib.StringBuilder(ssb);
  const popupWindow = new Lib.PopupWindow();

  const that = Object.freeze(
  {
    console: new Lib.Writer("console",
      console.log,
      function() {
        alert("Operation not supported.");
      }
    ),
    logA: new Lib.ReaderWriter("logA",
      logA.toString,
      logA.append,
      logA.replace
    ),
    logS: new Lib.ReaderWriter("logS",
      logS.toString,
      logS.append,
      logS.replace
    ),
    popup: new Lib.ReaderWriter("popup",
      popupWindow.showWindow,
      function(object) {
        popupWindow.appendText(object).showWindow();
      },
      function(object) {
        popupWindow.replaceText(object).showWindow();
      }
    ),
    prompt: new Lib.Reader("prompt",
      prompt
    ),
    reset: function() {
      that.logA.replace("");
      that.logS.replace("");
      popupWindow.replaceText("").enable();
    },
    toString: function() {
      const sb = new Lib.StringBuilder();
      sb.appendln(that.popup);
      sb.appendln(that.logA);
      sb.appendln(that.logS);
      sb.appendln(that.console);
      sb.appendln(that.prompt);
      return sb.toString();
    },
  });

  return that;

})();

/** 3.02. System.out
 *
 */
System.out = {
  print: function(object) {
    System.io.console.append(object);
    return System.out;
  },
  println: function(object) {
    System.io.console.appendln(object);
    return System.out;
  }
};

Object.freeze(System);


/**..:....1....:....2....:....3....:....4....:....5....:....6....:.
 *
 *  PART 4. Page.io
 *
 */
Page.io = (function makePageIO()
{
  const encode = btoa;
  const decode = atob;
  const a51 = new Lib.PageElement(Page.a51TextArea);
  const bpx = new Lib.PageElement(Page.bpxTextArea);

  const that = Object.freeze(
  {
    popup: System.io.popup,
    logA: System.io.logA,
    logS: System.io.logS,
    console: System.io.console,
    prompt: System.io.prompt,

    a51: new Lib.ReaderWriter("a51",
      a51.getValue,
      function(val) {
        a51.setValue(a51.getValue() + val.toString());
      },
      a51.setValue
    ),
    bpx: new Lib.ReaderWriter("bpx",
      function() {
        return decode(bpx.getValue());
      },
      function(val) {
        const unencoded = decode(bpx.getValue()) + val.toString();
        bpx.setValue(encode(unencoded));
      },
      function(val) { bpx.setValue(encode(val.toString())); }
    ),
    broadcast: function(message) {
      that.popup.appendln(message);
      that.bpx.appendln(message);
      that.a51.appendln(message);
      that.logA.appendln(message);
      that.logS.appendln(message);
      that.console.append(message);
      return that;
    },
    toString: function() {
      const sb = new Lib.StringBuilder();
      sb.appendln(that.a51);
      sb.appendln(that.bpx);
      sb.appendln(that.popup);
      sb.appendln(that.logA);
      sb.appendln(that.logS);
      sb.appendln(that.console);
      sb.appendln(that.prompt);
      return sb.toString();
    },
    reset: function() {
      that.a51.replaceln("TESTBEDS\n");
      that.a51.appendln("Google Chrome: 67.0.3396.87 (64-bit)");
      that.a51.appendln("OS: Windows");
      that.a51.appendln("JavaScript: V8 6.7.288.46");
      that.a51.appendln();
      that.a51.appendln("Internet Explorer: 11.0.9600.19035CO");
      that.a51.appendln();
      that.bpx.replaceln("Station X");
      that.bpx.appendln(decode("QWxhbiBUdXJpbmcgd2FzIGhlcmUu"));
      System.io.reset();
      return that;
    }
  });

  return that;

})();


Object.freeze(Page).io.reset();