1
0
mirror of https://codeberg.org/setop/elm-scripting synced 2025-11-08 21:49:57 +00:00

feat: can pass content as flags

This commit is contained in:
2025-10-15 12:19:11 +02:00
parent 0758e1e9b4
commit 5916566385
6 changed files with 182 additions and 62 deletions

View File

@@ -1,21 +1,32 @@
A tool to generate HTML code from Elm source in the terminal using [QuickJS](https://bellard.org/quickjs/). 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 # 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. 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. 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. Next, we concatenate this with the Elm JavaScript output and an app launcher snippet, then ask Qjs to interpret all of it.
## Limitations ## Limitations
* No event loop * No event loop
* Hence, no [TEA](https://guide.elm-lang.org/architecture/); the `main` function must return a static view * Hence, no [TEA](https://guide.elm-lang.org/architecture/); no `update` can be triggered
* ~~The Elm app module must be called "Main"~~ * Hence, no Time, no Random, no Http
* Nodes can only have one parent (this should always be the case) * 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 # Usage
@@ -26,19 +37,80 @@ Next, we concatenate this with the Elm JavaScript output and an app launcher sni
```elm ```elm
module Hello exposing(main) 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:
<p>
```text
Hello World! Hello World!
</p>
``` ```
## 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 # Prior Work
There are more complete tools for generating static sites with Elm: There are more complete tools for generating static sites with Elm:

28
TODO.md Normal file
View File

@@ -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

View File

@@ -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}

55
dom.js
View File

@@ -2,30 +2,39 @@ function Node(parent, tag) {
this.parentNode = parent; // Node this.parentNode = parent; // Node
this.pos = 0; // position in siblings list this.pos = 0; // position in siblings list
this.tagName = tag; this.tagName = tag;
this.text = null;
// attributes are stored directly on the node object // attributes are stored directly on the node object
// see https://github.com/elm/virtual-dom/blob/master/src/Elm/Kernel/VirtualDom.js#L511 // 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.children = []; // List of Node
this.replaceChild = (newNode, domNode) => { this.replaceChild = (newNode, domNode) => {
this.children[domNode.pos-1] = newNode; this.children[domNode.pos-1] = newNode;
newNode.pos = domNode.pos; newNode.pos = domNode.pos;
}; };
if (parent != null) { if (parent != null) {
this.pos = parent.children.push(this); this.pos = parent.children.push(this);
} }
this.appendChild = (node) => { this.appendChild = (node) => {
node.pos = this.children.push(node); node.pos = this.children.push(node);
} }
this.dump = (d=0) => { this.replaceData = (offset, count, data) => {
if (this.text) { this.text = data; // elm runtime will always replace the whole text
print(this.text); }
return this.dump = (d=0) => {
} if (this.text != null) {
if (this.innerHTML) { print(this.text);
print(this.innerHTML); return
return }
} if (this.innerHTML) {
std.printf("<%s", this.tagName) print(this.innerHTML);
// Set.difference(other) is not avalable in qjs return
}
std.printf("<%s", this.tagName)
// Set.difference(other) is not avalable in qjs
for (a of Object.keys(this)) { for (a of Object.keys(this)) {
if (!NodeKeys.has(a)) { if (!NodeKeys.has(a)) {
std.printf(' %s="%s"', a, this[a]) std.printf(' %s="%s"', a, this[a])
@@ -34,18 +43,18 @@ function Node(parent, tag) {
if (this.children.length==0) { if (this.children.length==0) {
print("/>") print("/>")
} else { } else {
print(">") print(">")
for (c of this.children) { for (c of this.children) {
c.dump(d+1) c.dump(d+1)
} }
print("</"+this.tagName+">"); print("</"+this.tagName+">");
} }
} }
return this; return this;
} }
var document = new Node(null, "document"); const document = new Node(null, "document");
var target = new Node(document, "target"); const target = new Node(document, "target");
const NodeKeys = new Set(Object.keys(target)); const NodeKeys = new Set(Object.keys(target));
// getElementById is only used once, to get node Elm must hook into. // 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.createElement = (tag) => new Node(null, tag);
document.createTextNode = (text) => { t = new Node(null, "#text" ); t.text = text; return t } 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

41
elmscript.sh Executable file
View File

@@ -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}

View File

@@ -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;
}