Current section
13 Versions
Jump to
Current section
13 Versions
Compare versions
9
files changed
+199
additions
-26
deletions
| @@ -5,13 +5,23 @@ All notable changes to this project will be documented in this file. | |
| 5 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), |
| 6 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). |
| 7 7 | |
| 8 | - ## [0.3.2] |
| 8 | + ## [0.4.0] |
| 9 9 | |
| 10 10 | ### Added |
| 11 11 | |
| 12 | + - Allow stylsheets to define "special" key/value that can be compiled into the output asset |
| 13 | + - Added @safe_list for always-compile class names |
| 14 | + |
| 12 15 | ### Changed |
| 13 16 | |
| 14 | - - Relaxed `live_view_native` constraint |
| 17 | + - Emit stylesheets as json instead of Elixir maps |
| 18 | + - pretty printing is not availabile for the time being |
| 19 | + - Elixir 1.17 with OTP 27 is the minimum requirement |
| 20 | + - Ensure stylesheet recompiles if LiveViewNative, LiveViewNativeStylesheet or the format's client version changes |
| 21 | + |
| 22 | + ### Fixed |
| 23 | + |
| 24 | + ## [0.3.1] 2024-10-02 |
| 15 25 | |
| 16 26 | ### Fixed |
| @@ -12,7 +12,7 @@ by adding `live_view_native_stylesheet` to your list of dependencies in `mix.exs | |
| 12 12 | ```elixir |
| 13 13 | def deps do |
| 14 14 | [ |
| 15 | - {:live_view_native_stylesheet, "~> 0.3.2"} |
| 15 | + {:live_view_native_stylesheet, "~> 0.3.0"} |
| 16 16 | ] |
| 17 17 | end |
| 18 18 | ``` |
| @@ -4,16 +4,16 @@ | |
| 4 4 | {<<"GitHub">>, |
| 5 5 | <<"https://github.com/liveview-native/live_view_native_stylesheet">>}]}. |
| 6 6 | {<<"name">>,<<"live_view_native_stylesheet">>}. |
| 7 | - {<<"version">>,<<"0.3.2">>}. |
| 7 | + {<<"version">>,<<"0.4.0-rc.1">>}. |
| 8 8 | {<<"description">>,<<"Stylesheet primitives for LiveView Native clients">>}. |
| 9 | - {<<"elixir">>,<<"~> 1.15">>}. |
| 9 | + {<<"elixir">>,<<"~> 1.17">>}. |
| 10 10 | {<<"app">>,<<"live_view_native_stylesheet">>}. |
| 11 11 | {<<"licenses">>,[<<"MIT">>]}. |
| 12 12 | {<<"requirements">>, |
| 13 13 | [[{<<"name">>,<<"live_view_native">>}, |
| 14 14 | {<<"app">>,<<"live_view_native">>}, |
| 15 15 | {<<"optional">>,false}, |
| 16 | - {<<"requirement">>,<<"~> 0.3">>}, |
| 16 | + {<<"requirement">>,<<"~> 0.4.0-rc.1">>}, |
| 17 17 | {<<"repository">>,<<"hexpm">>}], |
| 18 18 | [{<<"name">>,<<"nimble_parsec">>}, |
| 19 19 | {<<"app">>,<<"nimble_parsec">>}, |
| @@ -33,8 +33,10 @@ | |
| 33 33 | <<"lib/live_view_native/stylesheet/sheet_parser/post_processors.ex">>, |
| 34 34 | <<"lib/live_view_native/stylesheet/sheet_parser/parser.ex">>, |
| 35 35 | <<"lib/live_view_native/stylesheet/extractor.ex">>, |
| 36 | + <<"lib/live_view_native/stylesheet/encoder.ex">>, |
| 36 37 | <<"lib/live_view_native/stylesheet/rules_parser.ex">>, |
| 37 38 | <<"lib/live_view_native/stylesheet/utils.ex">>, |
| 39 | + <<"lib/live_view_native/stylesheet/otp_version_check.ex">>, |
| 38 40 | <<"lib/live_view_native/stylesheet/sheet_parser.ex">>, |
| 39 41 | <<"lib/live_view_native/stylesheet.ex">>,<<"lib/mix">>,<<"lib/mix/tasks">>, |
| 40 42 | <<"lib/mix/tasks/lvn.stylesheet.gen.ex">>, |
| @@ -80,6 +80,19 @@ defmodule LiveViewNative.Stylesheet do | |
| 80 80 | ... |
| 81 81 | """ |
| 82 82 | end |
| 83 | + |
| 84 | + ## Safe lists |
| 85 | + |
| 86 | + Safe lists are similar to the Tailwind `safelist` configuration option. You can define any |
| 87 | + number of class names that will always compile into the stylesheet. Unlike Tailwind you define |
| 88 | + the safe list inside each stylsheet: |
| 89 | + |
| 90 | + defmodule MyWebApp.Style.SwiftUI do |
| 91 | + use LiveViewNative.Stylesheet, :swiftui |
| 92 | + |
| 93 | + @safe_list ~w{red-block dark-mode-searchable} |
| 94 | + |
| 95 | + If an import has a safe list it will be imported into the importing sheet. |
| 83 96 | ''' |
| 84 97 | |
| 85 98 | @doc ~S''' |
| @@ -98,7 +111,7 @@ defmodule LiveViewNative.Stylesheet do | |
| 98 111 | The following functions are injected into the module: |
| 99 112 | |
| 100 113 | * `compile_ast/1` - takes a list of class names, produces AST as an Elixir map |
| 101 | - * `compile_string/1` - takes a list of class names, produces a string of the AST. |
| 114 | + * `compile_string/1` - takes a list of class names, produces a string of the AST in JSON. |
| 102 115 | Formatting rules are applied from the `live_view_native_stylesheet` application config. |
| 103 116 | ''' |
| 104 117 | defmacro __using__(format) do |
| @@ -108,8 +121,12 @@ defmodule LiveViewNative.Stylesheet do | |
| 108 121 | format: format, |
| 109 122 | }) |
| 110 123 | |
| 124 | + plugin = LiveViewNative.fetch_plugin!(format) |
| 125 | + plugin_stylesheet = plugin.stylesheet |
| 126 | + |
| 111 127 | quote do |
| 112 128 | Module.register_attribute(__MODULE__, :import, accumulate: true) |
| 129 | + Module.register_attribute(__MODULE__, :safe_list, accumulate: true) |
| 113 130 | |
| 114 131 | import LiveViewNative.Stylesheet.SheetParser, only: [sigil_SHEET: 2] |
| 115 132 | import LiveViewNative.Stylesheet.RulesParser, only: [sigil_RULES: 2] |
| @@ -118,6 +135,13 @@ defmodule LiveViewNative.Stylesheet do | |
| 118 135 | @before_compile LiveViewNative.Stylesheet |
| 119 136 | @after_verify LiveViewNative.Stylesheet |
| 120 137 | |
| 138 | + unquote(if plugin_stylesheet do |
| 139 | + quote do |
| 140 | + use unquote(plugin_stylesheet) |
| 141 | + @before_compile unquote(plugin_stylesheet) |
| 142 | + end |
| 143 | + end) |
| 144 | + |
| 121 145 | def compile_ast({class_or_list, style_list}) do |
| 122 146 | class_map = |
| 123 147 | class_or_list |
| @@ -143,12 +167,19 @@ defmodule LiveViewNative.Stylesheet do | |
| 143 167 | style_list |
| 144 168 | |> List.flatten() |
| 145 169 | |> Enum.into(%{}, fn({style, path}) -> |
| 146 | - # {:safe, encoded_style} = Phoenix.HTML.html_escape(style) |
| 147 170 | style_ast = LiveViewNative.Stylesheet.RulesParser.parse(style, @format, file: path) |
| 148 171 | {style, List.wrap(style_ast)} |
| 149 172 | end) |
| 150 173 | |
| 151 | - Map.merge(class_map, style_map) |
| 174 | + unquote(if plugin_stylesheet do |
| 175 | + quote do |
| 176 | + class_map |
| 177 | + |> Map.merge(style_map) |
| 178 | + |> Map.merge(special()) |
| 179 | + end |
| 180 | + else |
| 181 | + quote do: Map.merge(class_map, style_map) |
| 182 | + end) |
| 152 183 | end |
| 153 184 | |
| 154 185 | def compile_ast(class_or_list) do |
| @@ -158,9 +189,14 @@ defmodule LiveViewNative.Stylesheet do | |
| 158 189 | def compile_string({class_or_list, style_list}) do |
| 159 190 | pretty = Application.get_env(:live_view_native_stylesheet, :pretty, false) |
| 160 191 | |
| 161 | - {class_or_list, style_list} |
| 162 | - |> compile_ast() |
| 163 | - |> inspect(limit: :infinity, charlists: :as_list, printable_limit: :infinity, pretty: pretty) |
| 192 | + ast = compile_ast({class_or_list, style_list}) |
| 193 | + |
| 194 | + if Code.ensure_loaded?(:json) && pretty && Kernel.function_exported?(:json, :format, 2) do |
| 195 | + :json.format(ast, &LiveViewNative.Stylesheet.Encoder.format/3) |
| 196 | + else |
| 197 | + :json.encode(ast, &LiveViewNative.Stylesheet.Encoder.encode/2) |
| 198 | + end |
| 199 | + |> IO.iodata_to_binary() |
| 164 200 | end |
| 165 201 | |
| 166 202 | def compile_string(class_or_list) do |
| @@ -187,11 +223,13 @@ defmodule LiveViewNative.Stylesheet do | |
| 187 223 | format = Module.get_attribute(env.module, :format) |
| 188 224 | export? = Module.get_attribute(env.module, :export, false) |
| 189 225 | imports = Module.get_attribute(env.module, :import, []) |
| 226 | + safe_list = Module.get_attribute(env.module, :safe_list, []) |
| 190 227 | |
| 191 228 | if export? do |
| 192 229 | native_opts = %{ |
| 193 230 | imports: imports, |
| 194 | - export?: export? |
| 231 | + export?: export?, |
| 232 | + safe_list: safe_list |
| 195 233 | } |
| 196 234 | |
| 197 235 | quote do |
| @@ -211,13 +249,23 @@ defmodule LiveViewNative.Stylesheet do | |
| 211 249 | |> Path.relative_to_cwd() |
| 212 250 | |> LiveViewNative.Stylesheet.Extractor.paths(format) |
| 213 251 | |
| 252 | + format_app = app_for_format(format) |
| 253 | + |
| 254 | + versions = Enum.map([ |
| 255 | + :live_view_native, |
| 256 | + :live_view_native_stylesheet, |
| 257 | + format_app |
| 258 | + ], &LiveViewNative.Stylesheet.app_version(&1)) |
| 259 | + |
| 260 | + data = [paths, versions] |
| 261 | + |
| 214 262 | filename = |
| 215 263 | Path.basename(env.file) |
| 216 264 | |> String.split(".ex") |
| 217 265 | |> Enum.at(0) |
| 218 266 | |> Kernel.<>(".styles") |
| 219 267 | |
| 220 | - file_hash = :erlang.md5(paths) |
| 268 | + file_hash = :erlang.md5(data) |
| 221 269 | |
| 222 270 | content = |
| 223 271 | Application.get_env(:live_view_native_stylesheet, :content, []) |
| @@ -229,6 +277,7 @@ defmodule LiveViewNative.Stylesheet do | |
| 229 277 | paths: paths, |
| 230 278 | filename: filename, |
| 231 279 | format: format, |
| 280 | + safe_list: safe_list, |
| 232 281 | config: %{ |
| 233 282 | content: content, |
| 234 283 | output: output |
| @@ -243,6 +292,10 @@ defmodule LiveViewNative.Stylesheet do | |
| 243 292 | @external_resource path |
| 244 293 | end |
| 245 294 | |
| 295 | + for import <- unquote(imports) do |
| 296 | + @safe_list Map.get(import.__native_opts__(), :safe_list, []) |
| 297 | + end |
| 298 | + |
| 246 299 | def __native_opts__ do |
| 247 300 | unquote(Macro.escape(native_opts)) |
| 248 301 | end |
| @@ -262,13 +315,22 @@ defmodule LiveViewNative.Stylesheet do | |
| 262 315 | |> Path.join(native_opts[:filename]) |
| 263 316 | |> File.exists?() |
| 264 317 | |
| 265 | - file_hash = |
| 318 | + paths = |
| 266 319 | unquote(env.file) |
| 267 320 | |> Path.relative_to_cwd() |
| 268 321 | |> LiveViewNative.Stylesheet.Extractor.paths(@format) |
| 269 | - |> :erlang.md5() |
| 270 322 | |
| 271 | - !(output_file_exists? && @stylesheet_paths_hash == file_hash) |
| 323 | + format_app = LiveViewNative.Stylesheet.app_for_format(@format) |
| 324 | + |
| 325 | + versions = Enum.map([ |
| 326 | + :live_view_native, |
| 327 | + :live_view_native_stylesheet, |
| 328 | + format_app |
| 329 | + ], &LiveViewNative.Stylesheet.app_version(&1)) |
| 330 | + |
| 331 | + hash = :erlang.md5([paths, versions]) |
| 332 | + |
| 333 | + !(output_file_exists? && @stylesheet_paths_hash == hash) |
| 272 334 | else |
| 273 335 | false |
| 274 336 | end |
| @@ -277,6 +339,33 @@ defmodule LiveViewNative.Stylesheet do | |
| 277 339 | end |
| 278 340 | end |
| 279 341 | |
| 342 | + @doc false |
| 343 | + def app_for_format(format) do |
| 344 | + case LiveViewNative.fetch_plugin(format) do |
| 345 | + {:ok, %{__struct__: module}} -> |
| 346 | + Application.started_applications() |
| 347 | + |> Enum.map(&elem(&1, 0)) |
| 348 | + |> Enum.find(&(Enum.member?(Application.spec(&1, :modules), module))) |
| 349 | + _error -> :error |
| 350 | + end |
| 351 | + end |
| 352 | + |
| 353 | + @doc false |
| 354 | + def app_version(app) do |
| 355 | + Application.spec(app, :vsn) || "unknown" |
| 356 | + end |
| 357 | + |
| 358 | + @doc false |
| 359 | + def inject_safe_list({class_or_list, style_list}, safe_list) do |
| 360 | + class_or_list = |
| 361 | + class_or_list |
| 362 | + |> List.wrap() |
| 363 | + |> List.insert_at(-1, safe_list) |
| 364 | + |> List.flatten() |
| 365 | + |
| 366 | + {class_or_list, style_list} |
| 367 | + end |
| 368 | + |
| 280 369 | @doc false |
| 281 370 | def __after_verify__(module) do |
| 282 371 | native_opts = module.__native_opts__() |
| @@ -284,11 +373,14 @@ defmodule LiveViewNative.Stylesheet do | |
| 284 373 | output = get_in(native_opts, [:config, :output]) |
| 285 374 | export? = Map.get(native_opts, :export?, false) |
| 286 375 | |
| 376 | + safe_list = Map.get(native_opts, :safe_list, []) |
| 377 | + |
| 287 378 | cond do |
| 288 379 | output && !export? -> |
| 289 380 | compiled_sheet = |
| 290 381 | native_opts |
| 291 382 | |> LiveViewNative.Stylesheet.Extractor.run() |
| 383 | + |> LiveViewNative.Stylesheet.inject_safe_list(safe_list) |
| 292 384 | |> module.compile_string() |
| 293 385 | |
| 294 386 | file_path = Path.join(output, native_opts[:filename]) |
| @@ -0,0 +1,48 @@ | |
| 1 | + defmodule LiveViewNative.Stylesheet.Encoder do |
| 2 | + def format({term, annotations, arguments}, encoder, state) do |
| 3 | + annotations = Enum.into(annotations, %{}, &(&1)) |
| 4 | + format([term, annotations, arguments], encoder, state) |
| 5 | + end |
| 6 | + |
| 7 | + def format({name, value}, encoder, state) when is_atom(name), |
| 8 | + do: :json.format_value(%{{name, value}}, encoder, state) |
| 9 | + |
| 10 | + def format(nil, _encoder, _state), |
| 11 | + do: ~c(null) |
| 12 | + |
| 13 | + def format(atom, encoder, state) when is_atom(atom) do |
| 14 | + cond do |
| 15 | + Code.ensure_loaded?(atom) -> |
| 16 | + "Elixir." <> module = Atom.to_string(atom) |
| 17 | + :json.format_value(module, encoder, state) |
| 18 | + true -> :json.format_value(atom, encoder, state) |
| 19 | + end |
| 20 | + end |
| 21 | + |
| 22 | + def format(dynamic, encoder, state), |
| 23 | + do: :json.format_value(dynamic, encoder, state) |
| 24 | + |
| 25 | + def encode({term, annotations, arguments}, encoder) do |
| 26 | + annotations = Enum.into(annotations, %{}, &(&1)) |
| 27 | + :json.encode_list([term, annotations, arguments], encoder) |
| 28 | + end |
| 29 | + |
| 30 | + def encode({name, value}, encoder) when is_atom(name), |
| 31 | + do: :json.encode_map(%{{name, value}}, encoder) |
| 32 | + |
| 33 | + |
| 34 | + def encode(nil, _encoder), |
| 35 | + do: ~c(null) |
| 36 | + |
| 37 | + def encode(atom, encoder) when is_atom(atom) do |
| 38 | + cond do |
| 39 | + Code.ensure_loaded?(atom) -> |
| 40 | + "Elixir." <> module = Atom.to_string(atom) |
| 41 | + :json.encode_binary(module) |
| 42 | + true -> :json.encode_atom(atom, encoder) |
| 43 | + end |
| 44 | + end |
| 45 | + |
| 46 | + def encode(dynamic, encoder), |
| 47 | + do: :json.encode_value(dynamic, encoder) |
| 48 | + end |
Loading more files…