Initial commit

This commit is contained in:
Paul-Henri Froidmont 2025-08-03 19:51:02 +02:00
commit ed71df15f3
Signed by: phfroidmont
GPG key ID: BE948AFD7E7873BE
7 changed files with 218 additions and 0 deletions

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.bsp
.direnv
.metals
.scala-build
secrets.env
out

2
.scalafmt.conf Normal file
View file

@ -0,0 +1,2 @@
version = "3.7.17"
runner.dialect = scala3

6
build.mill Normal file
View file

@ -0,0 +1,6 @@
package build
import mill.*, scalalib.*
object core extends ScalaModule:
def scalaVersion = "3.7.2"
def scalacOptions = Seq("-Wunused:all")

94
core/src/main.scala Normal file
View file

@ -0,0 +1,94 @@
import scala.collection.immutable.ArraySeq
@main
def main =
val temlate = Template(TestLiveView.render)
println(temlate.init(MyModel("Initial title")))
println(temlate.update(MyModel("Updated title")))
trait LiveView[Model]:
val model = Dyn[Model, Model](identity)
def render: HtmlTag[Model]
final case class MyModel(title: String)
object TestLiveView extends LiveView[MyModel]:
def render: HtmlTag[MyModel] =
div(
div("some text"),
model(_.title)
)
class Dyn[I, O](f: I => O):
private var last: Option[O] = None
def apply[O2](f2: O => O2): Dyn[I, O2] = Dyn(f.andThen(f2))
def forceUpate(v: I): O =
val newValue = f(v)
last = Some(newValue)
newValue
def update(v: I): Option[O] =
val newValue = f(v)
last match
case Some(lastValue) if lastValue == newValue => None
case _ =>
last = Some(newValue)
last
enum Mod[T]:
case Tag(v: HtmlTag[T])
case Text(v: String)
case DynText(v: Dyn[T, String])
given [T]: Conversion[HtmlTag[T], Mod[T]] = Mod.Tag(_)
given [T]: Conversion[String, Mod[T]] = Mod.Text(_)
given [T]: Conversion[Dyn[T, String], Mod[T]] = Mod.DynText(_)
class Template[Model](
private val static: ArraySeq[String],
private val dynamic: ArraySeq[Dyn[Model, String]]
):
def init(model: Model): Template.InitialState =
Template.InitialState(
static,
dynamic.map(_.forceUpate(model))
)
def update(model: Model): Template.Diff =
Template.Diff(
dynamic.zipWithIndex.flatMap((dyn, i) => dyn.update(model).map(i -> _))
)
object Template:
final case class InitialState(static: Seq[String], dynamic: Seq[String])
final case class Diff(dynamic: Seq[(Int, String)])
def apply[Model](tag: HtmlTag[Model]) =
val (static, dynamic) = buildTag(tag)
new Template(static.to(ArraySeq), dynamic.to(ArraySeq))
def buildMod[Model](mod: Mod[Model]): (List[String], List[Dyn[Model, String]]) =
mod match
case Mod.Tag(v) => buildTag(v)
case Mod.Text(v) => (List(v), List.empty)
case Mod.DynText[Model](v) => (List.empty, List(v))
def buildTag[Model](
tag: HtmlTag[Model]
): (List[String], List[Dyn[Model, String]]) =
val modsBuilt: List[(List[String], List[Dyn[Model, String]])] =
tag.mods.map(buildMod)
val static =
List(s"<${tag.name}>") ++
modsBuilt.flatMap(_._1) ++
List(s"</${tag.name}>")
val dynamic = modsBuilt.flatMap(_._2)
(static, dynamic)
trait HtmlTag[Model]:
def name: String
def mods: List[Mod[Model]]
class Div[Model](val mods: List[Mod[Model]]) extends HtmlTag[Model]:
val name = "div"
def div[Model](mods: Mod[Model]*): Div[Model] = Div(mods.toList)

61
flake.lock generated Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1753939845,
"narHash": "sha256-K2ViRJfdVGE8tpJejs8Qpvvejks1+A4GQej/lBk5y7I=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "94def634a20494ee057c76998843c015909d6311",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

48
flake.nix Normal file
View file

@ -0,0 +1,48 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{ nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
mill = pkgs.mill.overrideAttrs (old: rec {
version = "1.0.2";
src = pkgs.fetchurl {
url = "https://repo1.maven.org/maven2/com/lihaoyi/mill-dist-native-linux-amd64/${version}/mill-dist-native-linux-amd64-${version}.exe";
hash = "sha256-+jRVJDxpH9DONuar+1CqB0Yl6thAuTn7dJYqOEsebGU=";
};
buildInputs = [ pkgs.zlib ];
nativeBuildInputs = [
pkgs.makeWrapper
]
++ pkgs.lib.optional pkgs.stdenvNoCC.isLinux pkgs.autoPatchelfHook;
installPhase = ''
runHook preInstall
install -Dm 555 $src $out/bin/.mill-wrapped
# can't use wrapProgram because it sets --argv0
makeWrapper $out/bin/.mill-wrapped $out/bin/mill \
--prefix PATH : "${pkgs.jre}/bin" \
--set-default JAVA_HOME "${pkgs.jre}"
runHook postInstall
'';
doInstallCheck = false;
});
in
{
devShell = pkgs.mkShell {
buildInputs = [
mill
pkgs.scalafmt
];
};
}
);
}