Notebook
Notebook was inspired by Mathematica's and Jupyter's notebook interfaces. It is a light-weight way to add executable JavaScript cells to a document written in Markdown or HTML.
In the default styling Notebook supports black File
and blue Run
cells
and a green Result
cell.
File
cells
Black File
cells represent editable,
constant, uniquely named strings containing lines of text.
<textarea class='nb-file' id='a'> string content </textarea>
File
cells are implemented as uniquely named,
editable (and selectable) text areas with black borders
and a light gray label:
Leading and trailing white space is removed.
The content is accessible using the load()
function in a Run
cell.
class='nb-file'
and a unique id
attribute are required; other attributes such as rows
or readonly
can be added.
Run
cells
Blue Run
cells represent editable, executable code snippets.
<input type='text' class='nb-run' value=' JavaScript code '></input>
<textarea class='nb-run'> JavaScript code </textarea>
Run
cells are implemented as editable (and selectable) input areas
with blue borders and a light blue run label:
Leading and trailing white space is removed.
class='nb-run'
is required; other attributes such as rows
can be added.
Once the run label is clicked the cell content is executed by a JavaScript interpreter.
Output lines produced by calling the print()
function
or a line with a computed value if any
are appended to a Result
cell.
input
elements are intended for one line of input.
In this case pressing the return key is equivalent to clicking run.
The attribute data-result-rows
can be specified for a Run
cell
to define the number of rows for the Result
cell.
The attribute data-print-limit
can be specified for a Run
cell
to limit the number of printed lines;
the default is configured as printLimit
.
Result
cell
The green Result
cell represents the JavaScript interpreter.
It contains output from calls to the print()
function or computed values.
The Result
cell appears when a run label is clicked
and — unless the option key is pressed during the click —
the previous content of the cell is first erased.
The cell is always located immediately following the Run
cell where the label was clicked.
The Result
cell is implemented as read-only text area
with green borders and a light green label.
The borders turn red if the interpreter terminates.
While the JavaScript interpreter is running, the label shows a stop symbol ⬛
which can be clicked to terminate the interpreter.
If the interpreter is stopped or terminated, the label shows a close symbol ╳
which can be clicked to dispose both,
the interpreter and the Result
cell.
A run-away loop can be terminated by clicking the stop symbol ⬛
during execution;
however, a run-away loop can still crash the web page if it consumes too many resources, e.g., by printing.
load()
JavaScript code in a Run
cell can use the load()
function
to include JavaScript code from File
cells.
The function takes one or more arguments;
each must be a string which is defined as the id
attribute of a File
cell.
Note that var
and function
definitions are hoisted out of File
cells,
but class
, const
, and let
definitions are not:
print()
JavaScript code in a Run
cell can use the print()
function.
This function concatenates its arguments, if any, separated by single blanks,
and appends one line to the Result
cell.
The number of printed lines produced by a Run
cell can be limited
by specifying the maximum with the attribute data-print-limit
.
JavaScript evaluation
JavaScript evaluation is implemented as a
Worker
which is created or contacted by the Run
cell
and uses eval()
in the onmessage()
method.
If the label of a Run
cell is clicked
it sends a message to the Worker
.
The message contains the code string of the cell and a collection
with the ids of all File
cells as keys
and the current code string in each cell as the corresponding values.
The Worker
cannot read from the HTML document;
therefore,
the load()
function relies on this collection to evaluate code in File
cells.
The Worker
uses eval()
to evaluate the Run
cell's code string
and replies to the cell with exactly one of the following three messages:
['exit']
['exit', 'result value']
['error', 'error mesaage']
The Worker
cannot insert into the HTML document;
therefore,
each call to the print()
function sends one message
['print', string values if any ]
which contains strings with the values to be printed, if any.
Using a Worker
for JavaScript interpretation has two advantages:
-
interpretation cannot block the user interface in the browser, i.e., the stop symbol
⬛
in aResult
cell can be clicked and event handling will applyterminate()
to theWorker
to abort interpretation. -
the global scope for
eval()
is theWorker
, not the browser, i.e., code in theFile
andRun
cells cannot affect the document.
Still,
this code does have the privileges of the Worker
,
i.e., it could, for example, mischievously employ
XMLHttpRequest
or replace the load()
and print()
functions.
In an attempt to protect the implementation of the onmessage()
method,
each time a Worker
is created
NB
is initialized with a shallow copy of all the global methods used.
Examples
Euclid's algorithm
Compute the greatest common divisor of two positive integers x
and y
.
Ackerman's function
How many recursive calls before the interpreter fails?
The catch
block prevents the interpreter from terminating, i.e.,
the Result
cell does not turn red.
Too many lines
The following Run
cell is specified with data-print-limit="10"
to
terminate the infinite printing loop.
Here the interpreter terminates,
i.e., the Result
cell will turn red.
Run this and the previous example a few times in a row (using option-click)
to see the difference in the Result
cell.
Fun with conversions
There is a difference between a number
and a Number
object:
Zero usually is false:
Empty strings usually are false:
Boolean
forces a logical value:
Infinite sequences
An infinite sequence can be represented as a function which returns an ordered pair,
i.e., an object where
car
is a value of the sequence and cdr
is the rest of the sequence:
ints(1)
is the sequence of natural numbers:
The following function take()
collects the first n
elements of a sequence s
into an array:
Note that the result of take()
needs to be reversed:
The function zipWith()
combines two sequences with a function:
E.g., the integers beginning with 1
can be pairwise concatenated with the integers beginning with 5
:
Arrow syntax
Modern JavaScript implementations support a more concise syntax for functions. The previous examples can be expressed as follows:
The following Run
cells demonstrate that the old and new functions
produce the same results:
Classes
Modern JavaScript has a syntax for classes. Here is an example of objects containing a counter belonging to a class with methods to access and modify the current value.
Classes — like functions — can have names;
however, in a File
cell var
has to be used
to hoist a class definition.
Thanks to the rest parameter ...values
,
the method add()
will add any combination of arrays and numbers to the counter:
The example takes advantage of a (questionable) aspect of arrow functions:
they don't hide this
.
The method would require a closure if it is implemented using a function
:
Getting started
Insert cells (input type='text'
and textarea
elements as described above) into a
MarkDown
document.
Add the following code at the very end of the document:
<script src='https://code.jquery.com/jquery-3.3.1.min.js'></script>
<link rel='stylesheet' type='text/css' href='notebook.css'>
<script type='module'>
import { init } from 'notebook.js';
// wrap cells below the selected element
// and use indicated worker code
init($('body'), 'worker.js');
</script>
Alternatively, equivalent code can be added to the head
of a HTML document.
Note that the paths above are relative to the document.
notebook.js implements the Notebook
module,
i.e., models, views, and controllers for the cells described above,
worker.js implements the JavaScript interpreter,
i.e., the sandboxed runtime support for the Run
cells,
and
notebook.css
is the required stylesheet — which could be modified.