From d1871b7b110e2253412ac2c0e231fe71c1d00110 Mon Sep 17 00:00:00 2001 From: Guillaume Hivert Date: Sun, 4 Aug 2024 18:22:26 +0200 Subject: [PATCH] wip: generate CSS from sketch styles --- sketch_css/.gitignore | 4 + sketch_css/README.md | 24 +++ sketch_css/gleam.toml | 23 +++ sketch_css/manifest.toml | 22 +++ sketch_css/src/sketch/css.gleam | 202 +++++++++++++++++++++++ sketch_css/src/sketch/css/error.gleam | 22 +++ sketch_css/styles/main_css.css | 24 +++ sketch_css/test/main_css.gleam | 43 +++++ sketch_css/test/sketch_css_test.gleam | 19 +++ sketch_lustre/.github/workflows/test.yml | 23 --- 10 files changed, 383 insertions(+), 23 deletions(-) create mode 100644 sketch_css/.gitignore create mode 100644 sketch_css/README.md create mode 100644 sketch_css/gleam.toml create mode 100644 sketch_css/manifest.toml create mode 100644 sketch_css/src/sketch/css.gleam create mode 100644 sketch_css/src/sketch/css/error.gleam create mode 100644 sketch_css/styles/main_css.css create mode 100644 sketch_css/test/main_css.gleam create mode 100644 sketch_css/test/sketch_css_test.gleam delete mode 100644 sketch_lustre/.github/workflows/test.yml diff --git a/sketch_css/.gitignore b/sketch_css/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/sketch_css/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/sketch_css/README.md b/sketch_css/README.md new file mode 100644 index 0000000..d061d93 --- /dev/null +++ b/sketch_css/README.md @@ -0,0 +1,24 @@ +# sketch_css + +[![Package Version](https://img.shields.io/hexpm/v/sketch_css)](https://hex.pm/packages/sketch_css) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/sketch_css/) + +```sh +gleam add sketch_css@1 +``` +```gleam +import sketch_css + +pub fn main() { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/sketch_css/gleam.toml b/sketch_css/gleam.toml new file mode 100644 index 0000000..2036824 --- /dev/null +++ b/sketch_css/gleam.toml @@ -0,0 +1,23 @@ +name = "sketch_css" +version = "1.0.0" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +gleam_stdlib = ">= 0.34.0 and < 2.0.0" +glance = ">= 0.11.0 and < 1.0.0" +sketch = { path = "../sketch" } +simplifile = ">= 2.0.1 and < 3.0.0" +gleam_erlang = ">= 0.25.0 and < 1.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/sketch_css/manifest.toml b/sketch_css/manifest.toml new file mode 100644 index 0000000..8f0c3c6 --- /dev/null +++ b/sketch_css/manifest.toml @@ -0,0 +1,22 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "filepath", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "EFB6FF65C98B2A16378ABC3EE2B14124168C0CE5201553DE652E2644DCFDB594" }, + { name = "glance", version = "0.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "glexer"], otp_app = "glance", source = "hex", outer_checksum = "8F3314D27773B7C3B9FB58D8C02C634290422CE531988C0394FA0DF8676B964D" }, + { name = "gleam_erlang", version = "0.25.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "054D571A7092D2A9727B3E5D183B7507DAB0DA41556EC9133606F09C15497373" }, + { name = "gleam_otp", version = "0.10.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "0B04FE915ACECE539B317F9652CAADBBC0F000184D586AAAF2D94C100945D72B" }, + { name = "gleam_stdlib", version = "0.39.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "2D7DE885A6EA7F1D5015D1698920C9BAF7241102836CE0C3837A4F160128A9C4" }, + { name = "gleeunit", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "F7A7228925D3EE7D0813C922E062BFD6D7E9310F0BEE585D3A42F3307E3CFD13" }, + { name = "glexer", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "glexer", source = "hex", outer_checksum = "BD477AD657C2B637FEF75F2405FAEFFA533F277A74EF1A5E17B55B1178C228FB" }, + { name = "simplifile", version = "2.0.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "5FFEBD0CAB39BDD343C3E1CCA6438B2848847DC170BA2386DF9D7064F34DF000" }, + { name = "sketch", version = "2.3.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib"], source = "local", path = "../sketch" }, +] + +[requirements] +glance = { version = ">= 0.11.0 and < 1.0.0" } +gleam_erlang = { version = ">= 0.25.0 and < 1.0.0"} +gleam_stdlib = { version = ">= 0.34.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } +simplifile = { version = ">= 2.0.1 and < 3.0.0" } +sketch = { path = "../sketch" } diff --git a/sketch_css/src/sketch/css.gleam b/sketch_css/src/sketch/css.gleam new file mode 100644 index 0000000..1724d92 --- /dev/null +++ b/sketch_css/src/sketch/css.gleam @@ -0,0 +1,202 @@ +import glance.{ + type Expression, Call, Discarded, Expression, Field, FieldAccess, List, Module, + Named, Variable, +} +import gleam/bool +import gleam/function +import gleam/io +import gleam/list +import gleam/option +import gleam/pair +import gleam/result +import gleam/string +import simplifile +import sketch/css/error + +pub type Module { + Module(path: String, content: String, ast: option.Option(glance.Module)) +} + +pub type Css { + Css(classes: List(#(String, String)), content: List(String)) +} + +/// Assumes dir is a directory. Should be checked before calling the function. +fn recursive_modules_read(dir: String) { + use dir_content <- result.map(simplifile.read_directory(dir)) + list.flatten({ + use path <- list.filter_map(dir_content) + let path = string.join([dir, path], "/") + use is_dir <- result.try(simplifile.is_directory(path)) + use <- bool.guard(when: is_dir, return: recursive_modules_read(path)) + use content <- result.map(simplifile.read(path)) + [Module(path: path, content: content, ast: option.None)] + }) +} + +fn parse_modules(modules: List(Module)) { + use module <- list.filter_map(modules) + use ast <- result.map(glance.module(module.content) |> error.glance) + Module(..module, ast: option.Some(ast)) +} + +fn select_css_files(modules: List(Module)) { + use module <- list.filter(modules) + string.ends_with(module.path, "_styles.gleam") + || string.ends_with(module.path, "_css.gleam") + || string.ends_with(module.path, "_sketch.gleam") +} + +fn find_sketch_imports(imports: List(glance.Definition(glance.Import))) { + let imports = list.filter(imports, fn(i) { i.definition.module == "sketch" }) + let aliases = + list.map(imports, fn(i) { + case i.definition.alias { + option.None -> i.definition.module + option.Some(Discarded(_)) -> i.definition.module + option.Some(Named(s)) -> s + } + }) + |> list.unique + let exposed = { + use i <- list.flat_map(imports) + use value <- list.filter_map(i.definition.unqualified_values) + use <- bool.guard(when: value.name != "class", return: Error(Nil)) + option.map(value.alias, Ok) |> option.unwrap(Ok(value.name)) + } + let properties = { + use i <- list.flat_map(imports) + use value <- list.filter_map(i.definition.unqualified_values) + use <- bool.guard(when: value.name == "class", return: Error(Nil)) + Ok(#(value.alias |> option.unwrap(value.name), value.name)) + } + #(aliases, exposed, properties) +} + +fn parse_css_modules( + styles_modules: List(Module), + _modules: List(Module), +) -> List(#(Module, Css)) { + use styles_module <- list.filter_map(styles_modules) + case styles_module.ast { + option.None -> Error(Nil) + option.Some(ast) -> + Ok({ + #(styles_module, { + let #(imports, exposed, properties) = find_sketch_imports(ast.imports) + use css, function <- list.fold(ast.functions, Css([], [])) + function_definition(imports, exposed, properties, function.definition) + |> result.map(fn(content) { + let classes = list.prepend(css.classes, #(content.0, content.1)) + let content = list.prepend(css.content, content.2) + Css(classes:, content:) + }) + |> result.unwrap(css) + }) + }) + } +} + +fn keep_fn_call(function: glance.Function) { + case function.body { + [Expression(Call(call, [Field(_, List(body, _))]))] -> Ok(#(call, body)) + _ -> Error(Nil) + } +} + +fn keep_valid_class( + imports: List(String), + exposed: List(String), + call: glance.Expression, +) { + case call { + Variable(fcall) -> { + let is_class = !list.contains(exposed, fcall) + use <- bool.guard(when: is_class, return: Error(Nil)) + Ok(Nil) + } + FieldAccess(Variable(module), fcall) -> { + let not_class = fcall != "class" + let not_sketch_class = !list.contains(imports, module) || not_class + use <- bool.guard(when: not_sketch_class, return: Error(Nil)) + Ok(Nil) + } + _ -> Error(Nil) + } +} + +fn function_definition( + imports: List(String), + exposed: List(String), + properties: List(#(String, String)), + function: glance.Function, +) -> Result(#(String, String, String), Nil) { + use #(call, body) <- result.try(keep_fn_call(function)) + use _ <- result.try(keep_valid_class(imports, exposed, call)) + let name = string.replace(function.name, each: "_", with: "-") + let body = class_body(imports, properties, body) |> string.join("\n") + let head = "." <> name <> " {\n" + let body = head <> body <> "\n}" + Ok(#(function.name, name, body)) +} + +fn skip(name) { + use <- bool.guard(when: name == "compose", return: Error(Nil)) + Ok(name) +} + +fn class_body( + imports: List(String), + properties: List(#(String, String)), + body: List(Expression), +) -> List(String) { + use property <- list.filter_map(body) + case property { + Call(Variable(name), param) -> + list.key_find(properties, name) + |> result.map(property_name) + |> result.try(skip) + |> result.map(fn(v) { " " <> v <> ": " <> property_body(param) <> ";" }) + Call(FieldAccess(Variable(module_name), name), param) -> { + let is_sketch = list.contains(imports, module_name) + use <- bool.guard(when: !is_sketch, return: Error(Nil)) + use <- bool.guard(when: result.is_error(skip(name)), return: Error(Nil)) + let prop = property_name(name) + Ok(" " <> prop <> ": " <> property_body(param) <> ";") + } + _ -> Error(Nil) + } +} + +fn property_name(name) { + name + |> string.split("-") + |> list.filter(fn(a) { a != "" }) + |> string.join("-") +} + +fn property_body(param) { + string.inspect(param) +} + +pub fn generate_stylesheets(src: String, dst: String) { + use is_dir <- result.try(simplifile.is_directory(src) |> error.simplifile) + use <- bool.guard(when: !is_dir, return: error.not_a_directory(src)) + use modules <- result.map(recursive_modules_read(src) |> error.simplifile) + let modules = parse_modules(modules) + let styles_modules = select_css_files(modules) + let css_modules = parse_css_modules(styles_modules, modules) + let _ = simplifile.create_directory_all(dst) + use #(module, css_module) <- list.each(css_modules) + let dst_path = string.replace(module.path, each: src, with: dst) + let dst_path = string.replace(dst_path, each: ".gleam", with: ".css") + let parent_dst_path = + dst_path + |> string.split("/") + |> list.reverse + |> list.drop(1) + |> list.reverse() + |> string.join("/") + let _ = simplifile.create_directory_all(parent_dst_path) + let _ = simplifile.write(dst_path, string.join(css_module.content, "\n\n")) +} diff --git a/sketch_css/src/sketch/css/error.gleam b/sketch_css/src/sketch/css/error.gleam new file mode 100644 index 0000000..b112ad0 --- /dev/null +++ b/sketch_css/src/sketch/css/error.gleam @@ -0,0 +1,22 @@ +import glance +import gleam/result +import simplifile + +pub type Error { + ApplicationError(message: String) + SimplifileError(error: simplifile.FileError) + GlanceError(error: glance.Error) +} + +pub fn simplifile(err) { + result.map_error(err, SimplifileError) +} + +pub fn glance(err) { + result.map_error(err, GlanceError) +} + +pub fn not_a_directory(dir: String) { + let error = ApplicationError(dir <> " is not a directory") + Error(error) +} diff --git a/sketch_css/styles/main_css.css b/sketch_css/styles/main_css.css new file mode 100644 index 0000000..e7f0109 --- /dev/null +++ b/sketch_css/styles/main_css.css @@ -0,0 +1,24 @@ +.card { + background: [Field(None, String("#ddd"))]; + background: [Field(None, String("red"))]; + display: [Field(None, String("block"))]; +} + +.card-body { + background: [Field(None, String("#ddd"))]; + background: [Field(None, String("red"))]; + display: [Field(None, String("block"))]; +} + +.block { + background: [Field(None, String("#ccc"))]; + display: [Field(None, String("flex"))]; +} + +.body { + +} + +.main { + background: [Field(None, String("#ddd"))]; +} \ No newline at end of file diff --git a/sketch_css/test/main_css.gleam b/sketch_css/test/main_css.gleam new file mode 100644 index 0000000..836d9b3 --- /dev/null +++ b/sketch_css/test/main_css.gleam @@ -0,0 +1,43 @@ +import sketch +import sketch.{background, class as t} as s + +fn custom_color(custom) { + sketch.color(custom) +} + +pub fn card(custom) { + sketch.class([ + sketch.background("#ddd"), + background("red"), + s.display("block"), + custom_color(custom), + ]) +} + +pub fn card_body(custom) { + sketch.class([ + sketch.background("#ddd"), + background("red"), + s.display("block"), + custom_color(custom), + ]) +} + +pub fn block(custom) { + sketch.class([ + sketch.background("#ccc"), + sketch.display("flex"), + sketch.compose(card(custom)), + ]) +} + +pub fn body(custom) { + sketch.class([ + sketch.compose(card(custom)), + sketch.background("#ccc") |> sketch.important, + ]) +} + +pub fn main() { + t([sketch.background("#ddd")]) +} diff --git a/sketch_css/test/sketch_css_test.gleam b/sketch_css/test/sketch_css_test.gleam new file mode 100644 index 0000000..2f9abe8 --- /dev/null +++ b/sketch_css/test/sketch_css_test.gleam @@ -0,0 +1,19 @@ +import gleam/io +import gleam/string +import gleeunit +import gleeunit/should +import simplifile +import sketch +import sketch/css + +pub fn main() { + gleeunit.main() +} + +// gleeunit test functions end in `_test` +pub fn read_test() { + let assert Ok(cwd) = simplifile.current_directory() + let src_folder = string.join([cwd, "test"], "/") + let dst_folder = string.join([cwd, "styles"], "/") + css.generate_stylesheets(src_folder, dst_folder) +} diff --git a/sketch_lustre/.github/workflows/test.yml b/sketch_lustre/.github/workflows/test.yml deleted file mode 100644 index da7701a..0000000 --- a/sketch_lustre/.github/workflows/test.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: test - -on: - push: - branches: - - master - - main - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: erlef/setup-beam@v1 - with: - otp-version: "26.0.2" - gleam-version: "1.3.2" - rebar3-version: "3" - # elixir-version: "1.15.4" - - run: gleam deps download - - run: gleam test - - run: gleam format --check src test