diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..0bf33a6
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for UnitJS
+
+## 0.1.0 -- 2020-05-17
+
+* Release UnitJS as a [SJW](https://git.marvid.fr/Tissevert/SJW) package still useable as a standalone script.
diff --git a/Makefile b/Makefile
index 9d6aeec..ad88687 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
BUILD_DIR=dist
TARGET=$(BUILD_DIR)/unit.js
-SRC=$(wildcard *.js)
+SRC=$(wildcard src/UnitJS/*.js)
TEMPLATE=unit.js.tpl
all: $(TARGET)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..7226b3e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,43 @@
+# UnitJS
+
+UnitJS is a tiny Javascript framework to write web applications. It provides four essential modules to deal with asynchronous functions (technically, it's a monad obtained by currifying the CPS functions of Javascript like `setTimeout`), value caching, DOM manipulation and basic functional needs like composition or object projections.
+
+## Usage
+
+### With [SJW](https://git.marvid.fr/Tissevert/SJW)
+
+UnitJS comes as a SJW package. You can install it by copying the `src/` directory of this repository to your package database by issuing the following command from the current directory :
+
+```
+$ cp -r src/ ~/.sjw/unitJS
+```
+
+Then, using unitJS in your projects is as simple as writing :
+
+```
+import * as Async from UnitJS.Async;
+```
+
+in any module if you want to use the `Async` module for instance and compile your code using the `--include` option of `sjw` with package `unitJS` :
+
+```
+$ sjw -I unitJS your/code/src -o your/code/main.js
+```
+
+Note that all modules have their path prefixed by a common `UnitJS` component and that SJW will only include the modules actually used in your code, not all of UnitJS.
+
+### As a standalone JS script
+
+If you don't want to or can't use SJW, it's still possible to use UnitJS by generating a single script that can then be loaded from your web page like this :
+
+```
+
+```
+
+The file `unit.js` can be easily generated with `make`, which actually just calls the `unit.js.tpl` script. Note that you can still generate custom «partial» versions of `unit.js` if you don't use all of it by overriding the `SRC` variable of the `Makefile` like so :
+
+```
+make SRC="src/UnitJS/Dom.js src/UnitJS/Cache.js"
+```
+
+or by manually calling `./unit.js.tpl` with the files you want, like the `Makefile` does.
diff --git a/async.js b/async.js
deleted file mode 100644
index 747a9be..0000000
--- a/async.js
+++ /dev/null
@@ -1,131 +0,0 @@
-function Async() {
- return {
- apply: apply,
- bind: bind,
- fail: fail,
- http: http,
- map: map,
- parallel: parallel,
- run: run,
- sequence: sequence,
- wait: wait,
- wrap: wrap
- };
-
- function apply(f, x) {
- return function(g) {
- g(f(x));
- };
- }
-
- function bind() {
- var m, steps, i;
- if(arguments.length < 1) {
- return wrap();
- }
- m = arguments[0];
- steps = arguments;
- i = 1;
- return function(f) {
- var step = function(x) {
- if(i < steps.length) {
- steps[i++](x)(step);
- } else {
- return f(x);
- }
- }
- m(step);
- };
- }
-
- function fail(message) {
- return function(f) {
- console.log(message);
- }
- }
-
- function map(mapper) {
- return function(x) {
- return function(f) {
- f(mapper(x));
- };
- };
- }
-
- function parallel() {
- var threads = arguments;
- var pending = threads.length;
- var results = [];
- var returned = [];
- return function(f) {
- var useResult = function(i) {
- return function(x) {
- if(!returned[i]) {
- results[i] = x;
- returned[i] = true;
- pending--;
- if(pending < 1) {
- f(results);
- }
- }
- };
- };
- for(var i = 0; i < threads.length; i++) {
- threads[i](useResult(i));
- }
- };
- }
-
- function run() {
- var m;
- if(arguments.length == 1) {
- m = arguments[0];
- } else {
- m = sequence.apply(null, arguments);
- }
- m(function() {});
- }
-
- function sequence() {
- var steps = arguments;
- var i = 0;
- return function(f) {
- var step = function(x) {
- if(i < steps.length) {
- steps[i++](step);
- } else {
- f(x);
- }
- }
- step();
- };
- }
-
- function wait(delay) {
- return function(f) {
- setTimeout(f, delay);
- };
- }
-
- function wrap(x) {
- return function(f) {
- f(x);
- };
- }
-
- function http(query) {
- return function(f) {
- var xhr = new XMLHttpRequest();
- xhr.addEventListener('load', function() {
- f(this);
- });
- xhr.open(query.method, query.url);
- if(query.method == 'POST') {
- xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
- }
- xhr.send(query.body);
-
- };
- }
-
-}
diff --git a/cache.js b/cache.js
deleted file mode 100644
index ec53101..0000000
--- a/cache.js
+++ /dev/null
@@ -1,54 +0,0 @@
-function Cache() {
- function Cache(loader) {
- this.loader = loader;
- this.loaded = {};
- this.loading = {};
- }
-
- Cache.prototype.get = function(key) {
- return function(f) {
- if(this.loaded[key] != undefined) {
- f(this.loaded[key]);
- } else {
- this.startLoading(key);
- this.loading[key].push(f);
- this.loader(key)(this.store(key));
- }
- }.bind(this);
- };
-
- Cache.prototype.warmUp = function(key) {
- if(this.loaded[key] == undefined) {
- this.startLoading(key);
- this.loader(key)(this.store(key));
- }
- };
-
- Cache.prototype.startLoading = function(key) {
- if(this.loading[key] == undefined) {
- this.loading[key] = [];
- }
- };
-
- Cache.prototype.store = function(key) {
- return function(value) {
- this.loaded[key] = value;
- for(var i = 0; i < this.loading[key].length; i++) {
- this.loading[key][i](value);
- }
- this.loading[key] = null;
- }.bind(this);
- };
-
- return {
- make: make
- };
-
- function make(loader) {
- var cache = new Cache(loader);
- return {
- get: cache.get.bind(cache),
- warmUp: cache.warmUp.bind(cache)
- };
- }
-}
diff --git a/dom.js b/dom.js
deleted file mode 100644
index 649da2d..0000000
--- a/dom.js
+++ /dev/null
@@ -1,39 +0,0 @@
-function Dom() {
- return {
- clear: clear,
- make: make
- }
-
- function clear(elem) {
- while(elem.firstChild) {
- elem.removeChild(elem.firstChild);
- }
- }
-
- function make(tag, properties, children) {
- var e = document.createElement(tag);
- properties = properties || {};
- children = children || [];
- for(key in properties) {
- var value = properties[key];
- switch(key) {
- case "class":
- e.className = Array.isArray(value) ? value.join(' ') : value;
- break;;
- case "maxlength":
- e.setAttribute("maxlength", value);
- break;
- case "onClick":
- e.addEventListener("click", value);
- break;;
- default:
- e[key] = value;
- }
- }
- for(var i = 0; i < children.length; i++) {
- e.appendChild(children[i]);
- }
- return e;
- }
-
-}
diff --git a/fun.js b/fun.js
deleted file mode 100644
index b18d6ef..0000000
--- a/fun.js
+++ /dev/null
@@ -1,74 +0,0 @@
-function Fun() {
- return {
- compare: compare,
- compose: compose,
- defined: defined,
- id: id,
- insert: insert,
- map: map,
- mapFilter: mapFilter,
- of: of,
- proj: proj
- };
-
- function compare(on) {
- on = on || id;
- return function(a, b) {
- var va = on(a), vb = on(b);
- return va < vb ? -1 : (va > vb ? 1 : 0)
- };
- }
-
- function compose(f, g) {
- return function() {
- return f(g.apply(null, arguments));
- };
- }
-
- function defined(x) {
- return x != undefined;
- }
-
- function id(x) {
- return x;
- }
-
- function insert(obj, t, comparer, min, max) {
- min = defined(min) ? min : 0;
- max = defined(max) ? max : t.length;
- comparer = comparer || compare();
- if(max - min < 1) {
- return min;
- }
- var avg = Math.floor((max + min) / 2);
- if (compare(obj, t[avg]) < 0) {
- return insert(obj, t, compare, min, avg);
- } else {
- return insert(obj, t, compare, avg+1, max);
- }
- }
-
- function map(mapper, f) {
- return function() {
- var args = Array.prototype.map.call(arguments, mapper);
- return f.apply(null, args);
- }
- }
-
- function mapFilter(mapper, predicate) {
- return function(array) {
- return array.reduce(function(accumulator, elem) {
- var v = mapper(elem);
- return predicate(v) ? accumulator.concat(v) : accumulator;
- }, []);
- };
- }
-
- function of(o) {
- return function(key) {return o[key];};
- }
-
- function proj(key) {
- return function(o) {return o[key];};
- }
-}
diff --git a/src/UnitJS/Async.js b/src/UnitJS/Async.js
new file mode 100644
index 0000000..1df682d
--- /dev/null
+++ b/src/UnitJS/Async.js
@@ -0,0 +1,128 @@
+return {
+ apply: apply,
+ bind: bind,
+ fail: fail,
+ http: http,
+ map: map,
+ parallel: parallel,
+ run: run,
+ sequence: sequence,
+ wait: wait,
+ wrap: wrap
+};
+
+function apply(f, x) {
+ return function(g) {
+ g(f(x));
+ };
+}
+
+function bind() {
+ var m, steps, i;
+ if(arguments.length < 1) {
+ return wrap();
+ }
+ m = arguments[0];
+ steps = arguments;
+ i = 1;
+ return function(f) {
+ var step = function(x) {
+ if(i < steps.length) {
+ steps[i++](x)(step);
+ } else {
+ return f(x);
+ }
+ }
+ m(step);
+ };
+}
+
+function fail(message) {
+ return function(f) {
+ console.log(message);
+ }
+}
+
+function map(mapper) {
+ return function(x) {
+ return function(f) {
+ f(mapper(x));
+ };
+ };
+}
+
+function parallel() {
+ var threads = arguments;
+ var pending = threads.length;
+ var results = [];
+ var returned = [];
+ return function(f) {
+ var useResult = function(i) {
+ return function(x) {
+ if(!returned[i]) {
+ results[i] = x;
+ returned[i] = true;
+ pending--;
+ if(pending < 1) {
+ f(results);
+ }
+ }
+ };
+ };
+ for(var i = 0; i < threads.length; i++) {
+ threads[i](useResult(i));
+ }
+ };
+}
+
+function run() {
+ var m;
+ if(arguments.length == 1) {
+ m = arguments[0];
+ } else {
+ m = sequence.apply(null, arguments);
+ }
+ m(function() {});
+}
+
+function sequence() {
+ var steps = arguments;
+ var i = 0;
+ return function(f) {
+ var step = function(x) {
+ if(i < steps.length) {
+ steps[i++](step);
+ } else {
+ f(x);
+ }
+ }
+ step();
+ };
+}
+
+function wait(delay) {
+ return function(f) {
+ setTimeout(f, delay);
+ };
+}
+
+function wrap(x) {
+ return function(f) {
+ f(x);
+ };
+}
+
+function http(query) {
+ return function(f) {
+ var xhr = new XMLHttpRequest();
+ xhr.addEventListener('load', function() {
+ f(this);
+ });
+ xhr.open(query.method, query.url);
+ if(query.method == 'POST') {
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+ }
+ xhr.send(query.body);
+
+ };
+}
diff --git a/src/UnitJS/Cache.js b/src/UnitJS/Cache.js
new file mode 100644
index 0000000..da31b4e
--- /dev/null
+++ b/src/UnitJS/Cache.js
@@ -0,0 +1,52 @@
+function Cache(loader) {
+ this.loader = loader;
+ this.loaded = {};
+ this.loading = {};
+}
+
+Cache.prototype.get = function(key) {
+ return function(f) {
+ if(this.loaded[key] != undefined) {
+ f(this.loaded[key]);
+ } else {
+ this.startLoading(key);
+ this.loading[key].push(f);
+ this.loader(key)(this.store(key));
+ }
+ }.bind(this);
+};
+
+Cache.prototype.warmUp = function(key) {
+ if(this.loaded[key] == undefined) {
+ this.startLoading(key);
+ this.loader(key)(this.store(key));
+ }
+};
+
+Cache.prototype.startLoading = function(key) {
+ if(this.loading[key] == undefined) {
+ this.loading[key] = [];
+ }
+};
+
+Cache.prototype.store = function(key) {
+ return function(value) {
+ this.loaded[key] = value;
+ for(var i = 0; i < this.loading[key].length; i++) {
+ this.loading[key][i](value);
+ }
+ this.loading[key] = null;
+ }.bind(this);
+};
+
+return {
+ make: make
+};
+
+function make(loader) {
+ var cache = new Cache(loader);
+ return {
+ get: cache.get.bind(cache),
+ warmUp: cache.warmUp.bind(cache)
+ };
+}
diff --git a/src/UnitJS/Dom.js b/src/UnitJS/Dom.js
new file mode 100644
index 0000000..9c8bee2
--- /dev/null
+++ b/src/UnitJS/Dom.js
@@ -0,0 +1,36 @@
+return {
+ clear: clear,
+ make: make
+}
+
+function clear(elem) {
+ while(elem.firstChild) {
+ elem.removeChild(elem.firstChild);
+ }
+}
+
+function make(tag, properties, children) {
+ var e = document.createElement(tag);
+ properties = properties || {};
+ children = children || [];
+ for(key in properties) {
+ var value = properties[key];
+ switch(key) {
+ case "class":
+ e.className = Array.isArray(value) ? value.join(' ') : value;
+ break;;
+ case "maxlength":
+ e.setAttribute("maxlength", value);
+ break;
+ case "onClick":
+ e.addEventListener("click", value);
+ break;;
+ default:
+ e[key] = value;
+ }
+ }
+ for(var i = 0; i < children.length; i++) {
+ e.appendChild(children[i]);
+ }
+ return e;
+}
diff --git a/src/UnitJS/Fun.js b/src/UnitJS/Fun.js
new file mode 100644
index 0000000..a0868ae
--- /dev/null
+++ b/src/UnitJS/Fun.js
@@ -0,0 +1,72 @@
+return {
+ compare: compare,
+ compose: compose,
+ defined: defined,
+ id: id,
+ insert: insert,
+ map: map,
+ mapFilter: mapFilter,
+ of: of,
+ proj: proj
+};
+
+function compare(on) {
+ on = on || id;
+ return function(a, b) {
+ var va = on(a), vb = on(b);
+ return va < vb ? -1 : (va > vb ? 1 : 0)
+ };
+}
+
+function compose(f, g) {
+ return function() {
+ return f(g.apply(null, arguments));
+ };
+}
+
+function defined(x) {
+ return x != undefined;
+}
+
+function id(x) {
+ return x;
+}
+
+function insert(obj, t, comparer, min, max) {
+ min = defined(min) ? min : 0;
+ max = defined(max) ? max : t.length;
+ comparer = comparer || compare();
+ if(max - min < 1) {
+ return min;
+ }
+ var avg = Math.floor((max + min) / 2);
+ if (compare(obj, t[avg]) < 0) {
+ return insert(obj, t, compare, min, avg);
+ } else {
+ return insert(obj, t, compare, avg+1, max);
+ }
+}
+
+function map(mapper, f) {
+ return function() {
+ var args = Array.prototype.map.call(arguments, mapper);
+ return f.apply(null, args);
+ }
+}
+
+function mapFilter(mapper, predicate) {
+ return function(array) {
+ return array.reduce(function(accumulator, elem) {
+ var v = mapper(elem);
+ return predicate(v) ? accumulator.concat(v) : accumulator;
+ }, []);
+ };
+}
+
+function of(o) {
+ return function(key) {return o[key];};
+}
+
+function proj(key) {
+ return function(o) {return o[key];};
+}
diff --git a/unit.js.tpl b/unit.js.tpl
index ebbc7c8..418033d 100755
--- a/unit.js.tpl
+++ b/unit.js.tpl
@@ -2,15 +2,36 @@
indent()
{
- local tabs="$(printf '\t%.0s' `seq 1 $1`)"
- sed "s|^|${tabs}|"
+ sed "s|^\(.\)|\t\1|"
+}
+
+moduleName()
+{
+ local fileName="${1##*/}"
+ printf "${fileName%.*}"
+}
+
+MODULES=""
+for file in "${@}"
+do
+ MODULES="${MODULES}:$(moduleName "${file}")"
+done
+MODULES="${MODULES#:}"
+
+includeModule()
+{
+cat <