Example triple script — an in-depth walkthrough
Here, we'll go through an example where we implement a program as a triple script, starting from a blank slate.
The finished result is a program of 222 lines in length usable by anyone anywhere, because the system requirements are trivial to satisfy—as a triple script, the program is capable of running in the browser even if the user has no special-purpose runtime installed. And as a triple script, our program offers a homologous UI, so those who do have some optional, CLI-based environment already installed and set up on their machines and who prefer not to leave the terminal will be able to launch our script from the shell as a command-line app.
In observance of the same principles, there is no "setting up a development environment" step associated with this walkthrough. The only tools necessary to follow along are:
- a text editor; and
- a web browser—which you're probably using now (if you're reading this on triplescripts.org; or at the very least you probably have one installed and within easy reach)
Having NodeJS installed to test the resulting program at the command-line is helpful, but not required.
In light of the above, the advantage of making our program a triple script should be obvious, but just to make it clear: even if the project were to grow in size and scope there will never be any "implicit step zero" that other potential contributors would need to satisfy before working with our program, because it targets the browser as a baseline runtime.
This walkthrough is split into seven parts. Only the first five parts are necessary for the implementation of the program. Parts 6 and 7 deal with things like simple formatting considerations, reorganizing our source tree, and adding a README with build instructions. No build step (or any prior experience with a particular build system) is needed during development.
You may follow along here by typing out (or copying and pasting) the snippets shown here, or you can download a copy of the archive containing all parts of this walkthrough as well as a variant of this guide itself.
Table of contents
- Triple script fundamentals and first code
- Main program logic using the system interface
- Adding an implementation of the system interface)
- Revisting the shunting block
- Completing the system interface implementation
- Switching to the triple script file format
- Rounding things out with trplkt and a README
Appendix: lines.app.htm
source code
Part 1: Fundamentals
We start out by defining a use case—some problem to be solved. For our demonstration, we are going to create a program that's concerned with newlines. Specifically, we are concerned with the types of newlines that appear in a file. The program developed here will ingest a file and then print statistics about the sorts of newlines it uses—whether the file uses Unix-style LF (linefeed: ASCII 0xA), or DOS-style CR+LF (carriage return followed by linefeed: ASCII 0xA 0xD), or something completely different.
Next, we discuss the implementation strategy. Here we cover some fundamental patterns helpful for creating triple scripts.
Before starting work on this program—or any triple script—we need to consider its needs, especially with respect to system-level operations and how (not) to work with the bindings exposed by the underlying platform.
For example, we'll need our program to be able to perform file IO (or really, in our case, just a file read), and some way to output the results of our analysis.
In a triple script, the recommended approach is to define separate platform support modules designed to abstract over the details of the underlying platform. Following this design, the platform-specific code should be confined to these modules, which implement a simple, consistent system-level interface. The rest of the code, including the main application logic of our program, should consist of portable code written in the triple script dialect that doesn't directly access any of the underlying platform's bindings—instead, it should target the system-level interface defined and implemented in the platform support modules.
For example, in a program whose main application logic needs only one
system-level operation "read", the browser support modules will implement a
method read
, and the support modules for other environments will implement a
method with the same interface. From the perspective of the main application
logic, all file access will go through this system.read
.
In the case of our program, we'll need such a read
call ourselves (to get
the text of the file), and for outputting feedback to the user, we'll define a
system-level print
.
We're almost at a good enough place that we can jump in and begin writing code soon. But first, understand that there are actually four different types of line endings we'll deal with:
- CRLF
- LF
- CR
- "none"
(If there's confusion about "none", consider a file containing a single line. The file may or may not end abruptly—with no line terminator. In fact, this is the default for DOS-style text-processing utilities. In contrast to Unix, which typically expects LF at the end of all lines, on DOS and Windows, the CRLF newline sequence is generally used as a line separator, rather than a line terminator. So for a file of n lines, there will be n-1 CRLF sequences in between them, and no such sequence at the end of the file—unless the user meant for the nth line to be a blank one.)
To start out, let's just go ahead and define the routines that comprise the
heart of our program. We'll implement this as LineChecker
, and it will have
a method called getStats
that returns the counts of the different types of
line endings.
Here's how we'll implement the getStats
method we mentioned before:
export
function LineChecker(text) {
this.text = text;
this.position = 0;
}
LineChecker.prototype.getStats = function() {
let stats = [];
stats[LineChecker.TYPE_NONE] = 0;
stats[LineChecker.TYPE_CR] = 0;
stats[LineChecker.TYPE_LF] = 0;
stats[LineChecker.TYPE_CRLF] = 0;
while (this.position < this.text.length) {
let end = LineChecker.findLineEnd(this.text, this.position);
let kind = LineChecker.getEOLType(this.text, end);
++stats[kind];
if (kind == LineChecker.TYPE_CR || kind == LineChecker.TYPE_LF) {
this.position = end + 1;
} else if (kind == LineChecker.TYPE_CRLF) {
this.position = end + 2;
} else {
this.position = end;
}
}
return stats;
}
// NB: not ASCII codes--just enum-likes doubling as indexes into stats.
LineChecker.TYPE_NONE = 0;
LineChecker.TYPE_CR = 1;
LineChecker.TYPE_LF = 2;
LineChecker.TYPE_CRLF = 3;
The getStats
method defined above will return an array of four elements
describing the counts of the various types of line endings. To figure out how
many lines end with CRLF, for example, we can check:
stats[LineChecker.TYPE_CRLF]
... where stats
is the value returned.
We could have taken a different approach, such as creating and returning an anonymous object with propertes named after the different types of line endings, and there are no strictures preventing that. The use of an array is just an implementation choice made here.
Given that the code above relies on the existence of two other functions
LineChecker.findLineEnd
and LineChecker.getEOLType
, we'll take care of
those, too:
LineChecker.findLineEnd = function(contents, position) {
const LF_CODE = 0x0A;
const CR_CODE = 0x0D;
while (position < contents.length) {
let unit = contents.codePointAt(position);
if (unit != CR_CODE && unit != LF_CODE) {
++position;
continue;
}
break;
}
return position;
}
LineChecker.getEOLType = function(contents, position) {
const LF_CODE = 0x0A;
const CR_CODE = 0x0D;
if (position < contents.length) {
let unit = contents.codePointAt(position);
if (unit == CR_CODE) {
if (LineChecker.getEOLType(contents, position + 1) ==
LineChecker.TYPE_LF) {
return LineChecker.TYPE_CRLF;
}
return LineChecker.TYPE_CR;
} else if (unit == LF_CODE) {
return LineChecker.TYPE_LF;
}
}
return LineChecker.TYPE_NONE;
}
At this point, near the end of part 1, we should be able to give almost any kind of text to our line checker and get stats about the line endings in use.
let checker = new LineChecker("foo\r\nbar");
checker.getStats(); // returns [ 0, 0, 0, 1 ]
We can write tests for this to make sure everything is working correctly. The project archive's subtree for Part 1 includes some tests like:
test(function single_line_no_terminator() {
let text = "This ends with no line terminator";
$verify(text, { none: 1 });
})
test(function multi_line_CRLF_no_terminator() {
let text =
"first line" + "\r\n" +
"second line";
$verify(text, { none: 1, crlf: 1 });
})
... and these tests can be run with Inaft. Inaft itself is a triple script,
and a copy is included at tests/harness.app.htm
. Running this test harness
and feeding it the project source code should show 13 tests (defined in
tests/stats/index.js
) similar to those above, all passing. However, further
covering either Inaft or these tests in-depth is outside the scope of this
document.
With getStats
having been written, LineChecker
now makes for a simple
software library, but it's not a full-fledged executable program capable of
taking arbitrary input from users and printing as its output the results of
our analysis. In the next two sections, we'll take steps to implement exactly
that.
Part 2: Program
What our program needs at this point are routines for taking a file as input
and then printing the formatted results of our getStats
implementation.
Let's extend our LineChecker
with a (static) method LineChecker.analyze
:
LineChecker.analyze = function(system, path) {
return system.read(path).then((contents) => {
let checker = new LineChecker(contents);
let stats = checker.getStats();
system.print(" CR: " + stats[LineChecker.TYPE_CR]);
system.print(" LF: " + stats[LineChecker.TYPE_LF]);
system.print("CRLF: " + stats[LineChecker.TYPE_CRLF]);
if (stats[LineChecker.TYPE_NONE]) {
system.print("\nThis file doesn't end with a line terminator.");
}
});
}
This is our first encounter with the abstract system interface that was previously only discussed in part 1. We'll cover it more in-depth in part 3. The lines above are the only additional code we'll be adding in this section.
Overall, LineChecker.analyze
is fairly simple.
LineChecker.analyze
makes use of ECMA-262 Promises for handling asynchronous
input. It expects the system read
call to return a promise that resolves to
the file's contents.
Output (in the form of system print
calls) is handled synchronously, because
the underlying platform bindings that we'll be using in later sections
have very simple sychronous interfaces themselves.
With the addition of the ~15 lines above, our LineChecker
is essentially
"finished". In it, we have the brains of our program. LineChecker.analyze
serves more or less as the main program logic, given that it covers all our
program concerns, including file input, analysis, and user feedback. We won't
need to make further substantial modifications to LineChecker
itself; what
follows in the next sections is (a) filling in the system-level read
and
print
routines in parts 3 and 5, and (b) doing a little massaging so things
are wired correctly for the triple script file format in parts 4 and 6.
Part 3: System
To begin with, we'll implement the browser-based system-level operations first, because everyone has access to a browser.
In general, this is a good approach for writing triple scripts. Suppose you start out with the intention of writing a triple script and begin work on the command-line version targeting, say, NodeJS instead of targeting the browser first. There are two potential pitfalls:
The first is that if your time, energy, or attention dries up and you stop work after getting your program to work on the command-line using NodeJS but before ever getting anything to work in the browser, then you limit the reach and usefulness of your program to only the parts of your audience who have NodeJS installed and are comfortable using it to run programs. On the other hand, if you start by targeting the browser and quit before filling in the parts that allow it to work from the command-line, then even in a half-finished state, you haven't limited your audience at all, because—once again—everyone can run the browser-based implementation, even those who would prefer not to leave the terminal.
The second pitfall is that if you begin working on the command-line version first, you may make what turn out to be the wrong design decisions early on and end up with an overreliance on the platform-specific behavior of the underlying environment, especially if you're targeting NodeJS for the command-line. The same risk exists if you start working on the browser-based implementation, but the practices and conventions promoted in the NodeJS community are more apt to lead you to making assumptions and doing things that will need to be reworked and turned inside out by the time you finish.
To get our program to work in the browser, we transform our existing
LineChecker
module by:
- surrounding it with double slash script delimiters "
// <script>
" and "// </script>
", respectively - replacing the
export
keyword with a comment "// export
" reminding us that this was the original export of the module before we had to mangle the source text to get it to run in the browser (although we could just remove it entirely, but we won't—more on this later) - renaming the file to
lines.app.htm
, which when used in conjunction with the delimiters from (1) gets the browser to actually run our code when we open up the file (since browsers don't execute.js
files directly; they operate on web pages that may optionally rely on scripts)
Let's now look at an implementation of print
and read
written for the
browser:
// export
class BrowserSystem {
constructor() {
if (typeof(window) == "undefined") {
throw new Error("Not a browser environment");
}
this._page = window.document.body;
this._file = null;
this._fileInput = null;
}
read(path) {
return new Promise((resolve, reject) => {
if (path == this._file.webkitRelativePath) {
if (typeof(FileReader) != "undefined") {
let reader = new FileReader();
reader.addEventListener("loadend", (event) => {
resolve(event.target.result);
});
return void(reader.readAsText(this._file));
}
return reject(Error("FileReader API not implemented"));
}
return reject(Error("Cannot read file with path: " + path));
});
}
print(message) {
this._page.textContent += "\n" + message;
}
}
There are two important things to point out here before moving further, which
are the two _file
and _fileInput
properties. The latter is neither
initialized (to any useful value besides null) nor used anywhere in the
excerpt above, and the former is used but never initialized. We'll discuss
this, but let's first point out the obvious benefit of the existence of the
BrowserSystem
class.
Recall that the first parameter of the LineChecker.analyze
method defined
in part 2 should be an object implementing the abstract system interface.
Given the above BrowserSystem
implementation, there's a straightforward way
to use it with our existing LineChecker
code:
let system = new BrowserSystem();
LineChecker.analyze(system, "path/to/file.txt");
In fact, something like this will form the basis of our first pass at our
shunting block. However, rather than hardcoding the path of the
file—which we don't and can't know at the time of development—we'll instead
introduce a system-level run
method that will be responsible for making sure
we get the user-specified file, and our shunting block will defer to it:
run() {
this._page.onload = () => {
this._page.innerHTML = "";
this._page.style.fontFamily = "monospace";
this._page.style.whiteSpace = "pre-wrap";
let doc = this._page.ownerDocument;
let $$ = doc.createElement.bind(doc);
let button = $$("input");
button.type = "button";
button.value = "Load\u2026";
button.style.float = "right";
button.onclick = () => void(this._fileInput.click());
this._page.appendChild(button);
let fileInput = $$("input");
fileInput.type = "file";
fileInput.style.display = "none";
fileInput.onchange = this._onFileSelected.bind(this);
this._fileInput = this._page.appendChild(fileInput);
}
}
In the run
method above, we see the initialization and use of the
_fileInput
property. The _file
property will be initialized in the
_onFileSelected
method:
_onFileSelected(event) {
this._file = event.target.files[0];
let path = this._file.webkitRelativePath;
LineChecker.analyze(this, path);
}
Briefly, if we create a BrowserSystem
instance and call its run
method, it
will add a button to the page allowing the user to select the desired file.
After it's selected, the browser system module transfers control to the main
application logic we defined in parts 1 and 2 in the LineChecker.analyze
method. The only thing missing at this point to make this actually usable in
the browser is code to instantiate BrowserSystem
and invoke its run
method. We'll do this in the shunting block, which we have already mentioned.
In the code archive the shunting block for part 3 appears in its
non-finalized form at the bottom of line.app.htm
as:
// <script>
// import "BrowserSystem.js"
void function main() {
let system = new BrowserSystem();
system.run();
} ();
// </script>
You'll find now—with the above changes made, including the file rename to use
the .app.htm
extension mentioned before—that you can open lines.app.htm
in
your browser (e.g. by double clicking it), and it performs as expected. For
any text file fed to our program, our analyzer will print the stats for the
various types of line endings used in that file.
In part 4, we'll do an in-depth discussion of the role of a triple script's
shunting block, and rework ours into a form fit for use for when
lines.app.htm
is transformed to work from the command-line, which will be
covered in part 5.
Part 4: Shunting
The shunting block mentioned in part 3 serves a crucial purpose in a triple
script: it's a platform-neutral way to initialize the system modules and
transfer control to the application when it first starts up. For this reason,
we sometimes describe the shunting block as the "entry point" to a triple
script—and this is the same reason that by convention the function in the
shunting block is called main
. However, it is by necessity—not just
convention—that the shunting block is the last t-block in the file.
(Because our program is not yet homologous—it only works in the browser—we
don't yet actually have any t-blocks in our file, because t-blocks use
triple slash script delimiters, whereas we are using "// <script>
" for now,
which we might say makes these "d-blocks" instead.)
Let's revist the shunting block we defined in part 3 and get it into the form
we want it to take in the "complete" version of lines.app.htm
. Remember
that the shunting block will need to be able to route execution whether our
program is running in the browser or running in the terminal.
Here's a revised shunting block that works for these purposes:
// <script>
// import "BrowserSystem.js"
// import "NodeJSSystem.js"
void function main() {
function init($kind) {
try {
return new $kind;
} catch (ex) {
return null;
}
}
if (!system) var system = init(BrowserSystem);
if (!system) var system = init(NodeJSSystem);
if (!system) {
// Might be running in an Inaft test context
if (typeof(module) != "undefined") {
module.exports.LineChecker = LineChecker;
} else {
throw new Error("Unknown environment");
}
}
system && system.run();
} ();
// </script>
Our main
here is an immediately invoked function expression, so it runs as
soon as it is encountered. An IIFE is used here since the triple script
dialect has certain prohibitions on the sort of top-level code that can
appear in a triple script's global scope, to avoid littering the namespace
with incidental values.
Internally, our main
has a macro-like init
helper function defined to make
it easier to try to initialize the system support modules in series. Failing
the attempt to initialize BrowserSystem
, the shunting block will try to
initialize NodeJSSystem
instead. Failing that, we make a last ditch effort
in the event that we might be running inside a test environment. Failing
that, the shunting block will throw an error. There are other runtimes
available that might be able to run our programs, but for the sake of brevity
in this document we are only concerned with supporting the browser and NodeJS.
The final line system && system.run()
will transfer control to the
platform-specific run
method. We have already seen the BrowserSystem
implementation in part 3.
Previously, we hadn't defined a NodeJSSystem
module. We can add a stubbed
out implementation now:
// <script>
class NodeJSSystem {
constructor() {
throw new Error; // TODO
}
}
// </script>
With these changes added to our lines.app.htm
, we can see that our program
will still run in the browser, as before. However, we are now in a place from
which we can comfortably move to the last part of this walkthrough that
involves writing any substantial code: where we fill out the NodeJSSystem
to
turn our program into homologous app capable of running in the terminal.
What follows in part 5 is slightly less involved than the work we did defining
the BrowserSystem
implementation in part 3, since there is no DOM-based,
graphical UI programming involved in the NodeJSSystem
implementation.
Part 5: Completing
Recall that our LineChecker
program requires the system layer to provide a
read
method and a print
method. Additionally, it needs to implement a
run
method to handle control transfer from the shunting block.
Here is our modest implementation of this interface for the NodeJS platform support module:
// <script>
// import "LineChecker.js"
// export
class NodeJSSystem {
constructor() {
if (typeof(process) == "undefined") {
throw new Error("Not a NodeJS environment");
}
this._nodeFS = process.mainModule.require("fs");
let [ engine, script, ...args ] = process.argv;
this.args = args;
}
run() {
if (this.args.length == 1) {
return void(LineChecker.analyze(this, this.args[0]));
}
throw new Error("Expected file path");
}
read(path) {
return new Promise((resolve, reject) => {
this._nodeFS.readFile(path, { encoding: "utf8" }, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
print(message) {
console.log(message);
}
}
// </script>
Once again, our read
implementation uses ECMA-262 Promises to handle the
asynchronous file access operation.
The NodeJS system layer's implementation of run
differs somewhat from the
browser form, because on the command-line we take a file path as an argument
passed directly from the OS shell—rather than adding a visual file picker
control, as in the case of the browser-based UI—but notice how our main
application code itself, i.e., the routine we originally wrote as
LineChecker.analyze
, is able to remain unchanged.
With these new additions to lines.app.htm
, we have a self-contained program
with a homologous UI: it runs on the most widespread platform in existence—the
browser—and the same program can be used in the terminal when executed as a
command-line app from the OS shell. If you have NodeJS installed, you can try
running it in the terminal now by passing the path of a text file as the
argument:
node lines.app.htm foo.txt
You can even try running the program on itself:
node lines.app.htm lines.app.htm
If you're a developer who spends a lot of time in the terminal, triple scripts are nice because it means being able to target a hugely popular platform—the browser—for maximum compatibility with your potential audience without needing to make any assumptions about what the audience's computing setup looks like, and at the same time you yourself don't have to sacrifice the convenience of leaving the terminal just to run the program.
Despite being a single-file, homologous app, our program is not yet a triple script because it doesn't adhere to the triple script file format.
In the next two sections, we will go over some refactoring and reorganization that doesn't substantially change the implementation or functionality of our program. However, we will end with the program in the concatenated t-block format that is standard for triple scripts, and a light introduction to trplkt, triplescript.org's reference compiler.
Part 6: Format
At this time, we have a program usable from within the browser or the terminal, and it's self-contained by virtue of the fact that all our modules are embedded within the same file and it doesn't dynamically load any external code modules.
Consider, though, what would happen if a program like ours were to grow. It wouldn't necessarily be convenient to continue development if the size of our project reached, say, tens of thousands of lines of code in a single file. Developers are used to being able to split things up into modules that are ideally manageable with file-level abstractions. The triple script format addresses this.
With triple scripts, it's possible to organize the project source tree by
splitting areas of code into conventional, file-based modules, and then given
such a collection of modules, we can perform a traditional build step to give
us a compiled version our program in a self-contained app file bearing some
similarity to the version of lines.app.htm
that we have now. Importantly,
the compilation process for triple scripts has been designed to be
non-destructive. The non-destructive compilation process allows anyone to
take any given triple script and reverse the process and decompile it back
into the original source modules. We call this automorphism, and it's
achievable with the use of triple slash delimited "t-blocks".
Recall that in part 3 when we began implementing the system layer for the
browser, we began by adding "// <script>
" and "// </script>
" line comments
to surround our modules.
Valid triple scripts, however, do not use delimiters of this form. Instead
they use triple slash delimiters ("/// <script>
" and "/// </script>
").
The mechanism by which these work is still the same—they are valid line
comments as far as any ECMAScript-compliant engine is concerned—but the use of
triple slashes also serves an important role. Triple slash delimiter
appearing at the beginning of a file is an assertion that it's valid a triple
script. That is, it's a machine-readable way to distinguish the file from
others on the users system and know that it's intended to satisfy the three
triple script invariants. The goal of triplescripts.org is to popularize
this way of authoring programs grow the ecosystem of triple script-based
tooling, in light of the unrivaled accessibility and convenience afforded to
users and potential contributors. The idea with the use of triple slash
script delimeters, then, is to be able to clearly and unambiguously represent
to downstream recipients of a triple script the authors' intent to adhere to
the principles of the triple script ecosystem.
In part 3 we also commented out the export
keyword as a result of the
mangling we had to do to get it to actually be conformant to the grammar
expected by the runtimes. (Since the .app.htm
is not associated with an ES6
module context, neither browsers nor NodeJS will accept the export
keyword
appearing in the program text.) Triple scripts, however, support triple slash
export pragmas in the compiled form. As far as any conformant ECMAScript
engine is concerned, these are also just ordinary line comments.
Additionally, up to this point, we've been using developer-oriented comments
explaining the module dependencies in lieu of any ES6-style import
statements for reasons similar to the prohibition on the export
keyword.
For example, in the NodeJS system layer, we've included a comment:
// import "LineChecker.js"
These, too, will need to be changed—this time to use a triple slash import
pragma. The format of an import pragma is the same as the result of the
constraints imposed by an ES6 import
statement. So rather than writing the
above, it will need to become:
/// import { LineChecker } from "LineChecker.js";
(However, the triple script dialect has much stricter rules about the form of the import pragma than the freeform nature of ES6; the from of the pragma above is the only form allowed in triple scripts.)
In summary, to transform our lines.app.htm
program into proper concatenated
t-block format, we make the following changes:
- use triple slash import and export pragmas
- turn all our double slash script delimiters into triple slash script delimiters
After making the changes above, we will now have a syntactically valid triple script.
Note that, despite needing to make these changes, it's always a good idea to start out not using triple slash script delimiters especially while you're still implementing its basic parts. Remember, the triple slash script delimiters are an assertion that your script is a valid triple script. But if your script's basic parts are still unimplemented, then this assertion is untrue. So always start out using double slash script delimiters, as we've done here, and then switch to triple slash script delimiters when your program actually satisfies the three triple script invariants—especially the homologous invariant.
Now that we have a valid triple script, finally, in part 7 we cover some of the benefits of following the constraints of the dialect outlined here—namely, the availability of tooling from the triple scripts ecosystem and that has been created for processing the concatenated t-block file format.
Part 7: Details
We mentioned before in part 6 that a valid triple script is [automorphic], and
that this makes it trivial to take a compiled triple script and decompile it
into its original source code. Let's do that now, since developers tend to
prefer working with source code modules instead of monolithic files. A copy
of trplkt is included in the code archive under the name Build.app.htm
,
and you can also obtain a copy from releases.triplescripts.org for your own
projects.
If you double click Build.app.htm, it should open in your browser. From there, you can load the directory where you extracted the archive for this example project, and then run the following command:
decompile lines.app.htm
(Alternatively, if you prefer to work from the terminal, you can use GraalJS
or NodeJS to run it using node Build.app.htm decomplie lines.app.htm
. This
is because trplkt itself is implemented as a triple script.)
Successful completion of this command should leave you with the source code
modules LineChecker
, BrowserSystem
, NodeJSSystem
, and main
, which make
up our program. Although our program is fairly small, it's conceivable that
for a larger program made of many more modules and/or containing an order of
magnitude more lines of code, it would be preferable to do regular development
within a deconstructed source tree. And this is probably the form that that
will be preferred by a project maintainer for keeping the project repository
in, for any project corresponding to a triple script. Nonetheless, the
text-based app file can be trivially opened and modified just as readily as
you can make changes to the source tree containing "raw" source files, and in
any case, the property of automorphism means that the two forms, compiled and
uncompiled, are equivalent, given that they can be seamlessly transformed from
one form into the other (and back), infinitely with perfect fidelity.
Although the lines.app.htm
is a general purpose utility, it should be
obvious at this point, having used both the copy of trplkt
that's embedded
in the source code archive as Build.app.htm
and the copy of Inaft under
the path tests/harness.app.htm
, that the original use case that
triplescripts.org has envisioned for the application of triple scripts is as
project metatooling meant to ease the software development process.
Because triple scripts are by design self-contained and automorphic, this encourages that you embed them directly into the project source tree by checking them into the repository.
And because this triple script tooling runs in the browser, you need not worry about anyone not having some prerequisite installed just to be able to run something. At triplescripts.org, we've arrived at this point as a response to the fact that the state of development in 2019 unfortunately tends to work like this:
- Choose a software stack/ecosystem that you'd like to work in
- Install the appropriate tooling—which might itself take a lot of work
- Hope that any potential contributor with an interest in the problem domain (i.e. the one that your project exists as a solution for) is themselves is either comfortable and familiar with the tooling and ecosystem from (1) and (2), or they're willing to put in a lot of upfront work to get to the point where they can begin meaningfully contributing
In fact, this problem is a main factor behind the existence imprimatur of triplescripts.org.
We call the problem described in the above steps "implicit step zero"—meaning it's all the stuff that constitutes the zeroeth step that any given project maintainer assumes of contributors. The implicit step zero encapsulates everything that a potential contributor will need to accomplish before getting into a position to carry out step 1 in the respective project's README instructions for doing a build, or running any tests, or performing with any other task that is typical for the software development process today.
Our goal is to attack the problem of implicit step zero by trying to eliminate it completely, through the use of triple script-based tooling, as we've demonstrated here. For more info about the work of this group, visit triplescripts.org.
The entire source code listing for our line checking app follows below. Note
that you can also download a copy of the source code archive containing
lines.app.htm
in its final form, along with all of these steps and a
variant of this guide.
Appendix: lines.app.htm
source code
Here is our program lines.app.htm
in its final, compiled form:
/// <script>
/// export
function LineChecker(text) {
this.text = text;
this.position = 0;
}
LineChecker.analyze = function(system, path) {
return system.read(path).then((contents) => {
let checker = new LineChecker(contents);
let stats = checker.getStats();
system.print(" CR: " + stats[LineChecker.TYPE_CR]);
system.print(" LF: " + stats[LineChecker.TYPE_LF]);
system.print("CRLF: " + stats[LineChecker.TYPE_CRLF]);
if (stats[LineChecker.TYPE_NONE]) {
system.print("\nThis file doesn't end with a line terminator.");
}
});
}
LineChecker.prototype.getStats = function() {
let stats = [];
stats[LineChecker.TYPE_NONE] = 0;
stats[LineChecker.TYPE_CR] = 0;
stats[LineChecker.TYPE_LF] = 0;
stats[LineChecker.TYPE_CRLF] = 0;
while (this.position < this.text.length) {
let end = LineChecker.findLineEnd(this.text, this.position);
let kind = LineChecker.getEOLType(this.text, end);
++stats[kind];
if (kind == LineChecker.TYPE_CR || kind == LineChecker.TYPE_LF) {
this.position = end + 1;
} else if (kind == LineChecker.TYPE_CRLF) {
this.position = end + 2;
} else {
this.position = end;
}
}
return stats;
}
LineChecker.findLineEnd = function(contents, position) {
const LF_CODE = 0x0A;
const CR_CODE = 0x0D;
while (position < contents.length) {
let unit = contents.codePointAt(position);
if (unit != CR_CODE && unit != LF_CODE) {
++position;
continue;
}
break;
}
return position;
}
LineChecker.getEOLType = function(contents, position) {
const LF_CODE = 0x0A;
const CR_CODE = 0x0D;
if (position < contents.length) {
let unit = contents.codePointAt(position);
if (unit == CR_CODE) {
if (LineChecker.getEOLType(contents, position + 1) ==
LineChecker.TYPE_LF) {
return LineChecker.TYPE_CRLF;
}
return LineChecker.TYPE_CR;
} else if (unit == LF_CODE) {
return LineChecker.TYPE_LF;
}
}
return LineChecker.TYPE_NONE;
}
// NB: not ASCII codes--just enum-likes doubling as indexes into stats.
LineChecker.TYPE_NONE = 0;
LineChecker.TYPE_CR = 1;
LineChecker.TYPE_LF = 2;
LineChecker.TYPE_CRLF = 3;
/// </script>
/// <script>
/// import { LineChecker } from "LineChecker.js";
/// export
class BrowserSystem {
constructor() {
if (typeof(window) == "undefined") {
throw new Error("Not a browser environment");
}
this._page = window.document.body;
this._file = null;
this._fileInput = null;
}
run() {
this._page.onload = () => {
this._page.innerHTML = "";
this._page.style.fontFamily = "monospace";
this._page.style.whiteSpace = "pre-wrap";
let doc = this._page.ownerDocument;
let $$ = doc.createElement.bind(doc);
let button = $$("input");
button.type = "button";
button.value = "Load\u2026";
button.style.float = "right";
button.onclick = () => void(this._fileInput.click());
this._page.appendChild(button);
let fileInput = $$("input");
fileInput.type = "file";
fileInput.style.display = "none";
fileInput.onchange = this._onFileSelected.bind(this);
this._fileInput = this._page.appendChild(fileInput);
}
}
_onFileSelected(event) {
this._file = event.target.files[0];
let path = this._file.webkitRelativePath;
LineChecker.analyze(this, path);
}
read(path) {
return new Promise((resolve, reject) => {
if (path == this._file.webkitRelativePath) {
if (typeof(FileReader) != "undefined") {
let reader = new FileReader();
reader.addEventListener("loadend", (event) => {
resolve(event.target.result);
});
return void(reader.readAsText(this._file));
}
return reject(Error("FileReader API not implemented"));
}
return reject(Error("Cannot read file with path: " + path));
});
}
print(message) {
this._page.textContent += "\n" + message;
}
}
/// </script>
/// <script>
/// import { LineChecker } from "LineChecker.js";
/// export
class NodeJSSystem {
constructor() {
if (typeof(process) == "undefined") {
throw new Error("Not a NodeJS environment");
}
this._nodeFS = process.mainModule.require("fs");
let [ engine, script, ...args ] = process.argv;
this.args = args;
}
run() {
if (this.args.length == 1) {
return void(LineChecker.analyze(this, this.args[0]));
}
throw new Error("Expected file path");
}
read(path) {
return new Promise((resolve, reject) => {
this._nodeFS.readFile(path, { encoding: "utf8" }, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
print(message) {
console.log(message);
}
}
/// </script>
/// <script>
/// import { BrowserSystem } from "BrowserSystem.js";
/// import { NodeJSSystem } from "NodeJSSystem.js";
void function main() {
function init($kind) {
try {
return new $kind;
} catch (ex) {
return null;
}
}
if (!system) var system = init(BrowserSystem);
if (!system) var system = init(NodeJSSystem);
if (!system) {
// Might be running in an Inaft test context
if (typeof(module) != "undefined") {
module.exports.LineChecker = LineChecker;
} else {
throw new Error("Unknown environment");
}
}
system && system.run();
} ();
/// </script>