diff --git a/README.md b/README.md index 6914a9a..0429641 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,32 @@ A tool to generate HTML code from Elm source in the terminal using [QuickJS](https://bellard.org/quickjs/). +# Requirements + +* depends only on elm compiler, quickjs cli and a posix shell +* do not alter elm compiler +* do not alter quickjs cli +* do not patch elm compiler output +* provide acceptable performances (500ms for a big script) + + # Design QuickJS (Qjs) is a [JavaScript runtime](https://en.wikipedia.org/wiki/List_of_JavaScript_engines), similar to V8 or SpiderMonkey, but lighter and faster. As any runtime, Qjs can interpret JavaScript code, but it is not a web browser. It has no concept of an HTML document. -To bridge this gap, we add a minimal [DOM](https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model) implementation. +To bridge this gap, we add a minimal [DOM](https://dom.spec.whatwg.org/) implementation. Next, we concatenate this with the Elm JavaScript output and an app launcher snippet, then ask Qjs to interpret all of it. ## Limitations * No event loop -* Hence, no [TEA](https://guide.elm-lang.org/architecture/); the `main` function must return a static view -* ~~The Elm app module must be called "Main"~~ +* Hence, no [TEA](https://guide.elm-lang.org/architecture/); no `update` can be triggered +* Hence, no Time, no Random, no Http * Nodes can only have one parent (this should always be the case) +* Does not scale well : creating thousands of Nodes consumes a [lot of RAM](#Performances) + # Usage @@ -26,19 +37,80 @@ Next, we concatenate this with the Elm JavaScript output and an app launcher sni ```elm module Hello exposing(main) -import Html exposing (p, text) +import Html exposing (text) -main = p [] [text "Hello World!"] +main = text "Hello World!" ``` -4. Run `./build.sh src/Hello.elm`; this generates the corresponding HTML code: +4. Run `./elmscript.sh src/Hello.elm` -```html -

+This produces the expected output: + +```text Hello World! -

``` +## passing input + +Input content can be passed to the script via the standard input. The content is then passed to the Elm script as "flags". The script must then use `Browser.element` and implement `init flags` function. + + +Create `src/Greet.elm` with the following content: + +```elm +module Greet exposing (main) + +import Browser +import Html exposing (text) + + +main = + Browser.element + { init = \f -> (f, Cmd.none) + , update = \m _ -> (m, Cmd.none) + , view = \m -> text ("hello " ++ m) + , subscriptions = always Sub.none + } +``` + +Run `./elmscript.sh src/Greet.elm <<< 'Elm'` + +This produces the expected output: `hello Elm`. + + +I's even more natural when passing file : + +* run `elm install elm-explorations/markdown` +* create `src/MdRender.elm` with the following content: + +```elm +module MdRender exposing (main) + +import Browser +import Html.Attributes exposing (class) +import Markdown + +main = + Browser.element + { init = \s -> (s, Cmd.none) + , update = \m c -> (m, Cmd.none) + , view = \m -> Markdown.toHtml [class "content"] m + , subscriptions = always Sub.none + } + +``` + +* run `./elmscript.sh src/MdRender.elm < README.md > README.html` + + + +## Performances + +Acceptable for small scripts : 250ms on a modest x86_64 CPU and 64MB RAM for a 500 records into a table ; but is does not scale well as everything is loaded before processing ; no streaming contrary to the usual Unix way. + +Generate 500k "li" loop took 17s and 900MB RAM. + + # Prior Work There are more complete tools for generating static sites with Elm: diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..5ed91ff --- /dev/null +++ b/TODO.md @@ -0,0 +1,28 @@ + +# Major + +- [ ] find a way to process external data + - [x] from stdout (use case : json), use flags + - [ ] from files (use case : SSG) + - [ ] from http (use case : spider) + +- implements some missing Web API: + - [Events](https://dom.spec.whatwg.org/#events) + - [Fetch](https://fetch.spec.whatwg.org/)(or [XMLHttpRequest](https://xhr.spec.whatwg.org/)) + - [Promise](https://webidl.spec.whatwg.org/#a-new-promise) (if needed by the above) + +- [ ] find a way to create a standalone executable (maybe with a combination of Google Closure Compiler and qjsc) + +- [ ] find a way to stream instead of having the whole document in memory (output as soon as a node is created ? a child is added ?) + + +# Minor + +- [ ] support for Elm debug (`-d`) mode (qjs does not implement `console.warn`, only `console.log`) + - [ ] in debug mode, keep the output +- [x] allow to specify or guess the main module name +- [x] if `innerHTML` attribute is set, output its value instead of the node tree +- [x] allow to create standalone (`-s`) HTML document instead of fragment => use template engine instead +- [x] silence elm compiler message when successful +- [ ] skip compilation steps if source has not been modified ; run directly ; this mean keeping the output +- [x] add proper copyright and license diff --git a/build.sh b/build.sh deleted file mode 100755 index 7c67b11..0000000 --- a/build.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh -eu - -w1="$(mktemp out_$$_1_XXXX.js)" - -elm make --optimize --output=${w1} $1 1>&2 - -w2="$(mktemp out_$$_XXXX.js)" - -cat dom.js ${w1} launch.js > ${w2} - -rm ${w1} - -qjs --std ${w2} - -# if qjs fails, output file will stay for debugging, else - -rm ${w2} diff --git a/dom.js b/dom.js index 29963c2..91536c8 100644 --- a/dom.js +++ b/dom.js @@ -2,30 +2,39 @@ function Node(parent, tag) { this.parentNode = parent; // Node this.pos = 0; // position in siblings list this.tagName = tag; + this.text = null; // attributes are stored directly on the node object // see https://github.com/elm/virtual-dom/blob/master/src/Elm/Kernel/VirtualDom.js#L511 + // but Html.Attributes.attribute/2 uses Element.setAttribute + this.setAttribute = (key, val) => { + // as of now, there is no check if key conflict with the ones defined by Dom implementation + this[key] = val; + } this.children = []; // List of Node this.replaceChild = (newNode, domNode) => { this.children[domNode.pos-1] = newNode; newNode.pos = domNode.pos; - }; + }; if (parent != null) { this.pos = parent.children.push(this); } this.appendChild = (node) => { node.pos = this.children.push(node); } - this.dump = (d=0) => { - if (this.text) { - print(this.text); - return - } - if (this.innerHTML) { - print(this.innerHTML); - return - } - std.printf("<%s", this.tagName) - // Set.difference(other) is not avalable in qjs + this.replaceData = (offset, count, data) => { + this.text = data; // elm runtime will always replace the whole text + } + this.dump = (d=0) => { + if (this.text != null) { + print(this.text); + return + } + if (this.innerHTML) { + print(this.innerHTML); + return + } + std.printf("<%s", this.tagName) + // Set.difference(other) is not avalable in qjs for (a of Object.keys(this)) { if (!NodeKeys.has(a)) { std.printf(' %s="%s"', a, this[a]) @@ -34,18 +43,18 @@ function Node(parent, tag) { if (this.children.length==0) { print("/>") } else { - print(">") - for (c of this.children) { - c.dump(d+1) - } - print(""); + print(">") + for (c of this.children) { + c.dump(d+1) + } + print(""); } - } + } return this; } -var document = new Node(null, "document"); -var target = new Node(document, "target"); +const document = new Node(null, "document"); +const target = new Node(document, "target"); const NodeKeys = new Set(Object.keys(target)); // getElementById is only used once, to get node Elm must hook into. @@ -56,8 +65,8 @@ document.getElementById = (_id) => { return target} document.createElement = (tag) => new Node(null, tag); document.createTextNode = (text) => { t = new Node(null, "#text" ); t.text = text; return t } -var global = {}; -try { +// workaround for elm-explorations/markdown Markdown.toHtml +const global = {}; + -// here will come the Elm app code diff --git a/elmscript.sh b/elmscript.sh new file mode 100755 index 0000000..c647cb6 --- /dev/null +++ b/elmscript.sh @@ -0,0 +1,41 @@ +#!/bin/sh -eu + +CMDD=$(dirname $(realpath 0)) + +w1="$(mktemp out_$$_1_XXXX.js)" + +elm make --optimize --output=${w1} $1 1>/dev/null + +w2="$(mktemp out_$$_XXXX.js)" + +cat ${CMDD}/dom.js >> ${w2} +cat << EEE >> ${w2} +try { +EEE +cat ${w1} >> ${w2} +cat << EEE >> ${w2} + Elm[Object.keys(Elm)[0]].init({ + node: document.getElementById("elm") +EEE +flags=$(grep -c 'main = $elm$browser$Browser$element' ${w1} || true) +if [ $flags -eq 1 ]; +then +cat << EEE >> ${w2}; + , flags: std.in.readAsString() +EEE +fi +cat << EEE >> ${w2} + }); + document.children[0].dump(); +} catch(e) { + throw e; +} +EEE + +rm ${w1} + +qjs --std ${w2} + +# if qjs fails, output file will stay for debugging, else + +rm ${w2} diff --git a/launch.js b/launch.js deleted file mode 100644 index 20c6fd8..0000000 --- a/launch.js +++ /dev/null @@ -1,13 +0,0 @@ - - - // above is the DOM implementation - // and the Elm app code - - // here we lanch the app - Elm[Object.keys(Elm)[0]].init({ node: document.getElementById("elm") }); - - document.children[0].dump(); - -} catch(e) { - throw e; -}