Skip to content

Commit

Permalink
wip: generate CSS from sketch styles
Browse files Browse the repository at this point in the history
  • Loading branch information
ghivert committed Aug 4, 2024
1 parent 25deb04 commit d1871b7
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 23 deletions.
4 changes: 4 additions & 0 deletions sketch_css/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*.beam
*.ez
/build
erl_crash.dump
24 changes: 24 additions & 0 deletions sketch_css/README.md
Original file line number Diff line number Diff line change
@@ -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 <https://hexdocs.pm/sketch_css>.

## Development

```sh
gleam run # Run the project
gleam test # Run the tests
```
23 changes: 23 additions & 0 deletions sketch_css/gleam.toml
Original file line number Diff line number Diff line change
@@ -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"
22 changes: 22 additions & 0 deletions sketch_css/manifest.toml
Original file line number Diff line number Diff line change
@@ -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" }
202 changes: 202 additions & 0 deletions sketch_css/src/sketch/css.gleam
Original file line number Diff line number Diff line change
@@ -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"))
}
22 changes: 22 additions & 0 deletions sketch_css/src/sketch/css/error.gleam
Original file line number Diff line number Diff line change
@@ -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)
}
24 changes: 24 additions & 0 deletions sketch_css/styles/main_css.css
Original file line number Diff line number Diff line change
@@ -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"))];
}
43 changes: 43 additions & 0 deletions sketch_css/test/main_css.gleam
Original file line number Diff line number Diff line change
@@ -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")])
}
19 changes: 19 additions & 0 deletions sketch_css/test/sketch_css_test.gleam
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit d1871b7

Please sign in to comment.