Current section

13 Versions

Jump to

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…