Packages
surface
0.9.4
0.12.3
0.12.2
0.12.1
0.12.0
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
0.1.0-alpha.2
0.1.0-alpha.1
0.1.0-alpha.0
A component based library for Phoenix LiveView
Current section
Files
Jump to
Current section
Files
lib/surface/compiler/css_translator.ex
defmodule Surface.Compiler.CSSTranslator do
@moduledoc false
alias Surface.Compiler.CSSParser
@closing_symbol %{
"{" => "}",
"(" => ")",
"[" => "]"
}
@scope_attr_prefix "s-"
@self_attr "s-self"
@scope_id_length 5
@var_name_length 5
def scope_attr(component, func) do
@scope_attr_prefix <> scope_id(component, func)
end
def scope_id(component, func) do
hash("#{inspect(component)}.#{func}", @scope_id_length)
end
def var_name(scope, expr) do
"--#{hash(scope <> expr, @var_name_length)}"
end
def scope_attr_prefix do
@scope_attr_prefix
end
def self_attr do
@self_attr
end
defmacro scope_id do
mod = __CALLER__.module
{func, _} = __CALLER__.function
Surface.Compiler.CSSTranslator.scope_id(mod, func)
end
defmacro scope_attr do
mod = __CALLER__.module
{func, _} = __CALLER__.function
Surface.Compiler.CSSTranslator.scope_attr(mod, func)
end
def translate!(css, opts \\ []) do
module = Keyword.get(opts, :module)
func = Keyword.get(opts, :func)
scope_id = Keyword.get(opts, :scope_id) || scope_id(module, func)
scope_attr_prefix = Keyword.get(opts, :scope_attr_prefix) || @scope_attr_prefix
file = Keyword.get(opts, :file)
line = Keyword.get(opts, :line) || 1
env = Keyword.get(opts, :env) || :dev
state = %{
scope_id: scope_id,
scope_attr_prefix: scope_attr_prefix,
file: file,
line: line,
env: env,
vars: %{},
selectors_buffer: [],
requires_data_attrs_on_root: false,
selectors: %{
elements: MapSet.new(),
classes: MapSet.new(),
ids: MapSet.new(),
combined: MapSet.new(),
other: MapSet.new()
}
}
{updated_tokens, state} =
css
|> CSSParser.parse!(opts)
|> translate([], state)
%{
scope_id: scope_id,
file: file,
line: line,
css: to_string(updated_tokens),
selectors: state.selectors,
vars: state.vars,
requires_data_attrs_on_root: state.requires_data_attrs_on_root
}
end
defp translate([{:text, "s-bind"}, {:block, "(", arg, meta} | rest], acc, state) do
expr = s_bind_arg_to_string(arg)
name = var_name(state.scope_id, expr)
vars = Map.put(state.vars, name, {expr, meta})
translate(rest, ["var(#{name})" | acc], %{state | vars: vars})
end
defp translate([:semicolon | rest], acc, state) do
translate(rest, [";" | acc], state)
end
defp translate([{:comma, _} | rest], acc, state) do
translate(rest, ["," | acc], state)
end
defp translate([{:ws, ws} | rest], acc, state) do
translate(rest, [ws | acc], state)
end
defp translate([{:text, text} | rest], acc, state) do
translate(rest, [text | acc], state)
end
defp translate([{:string, delimiter, text} | rest], acc, state) do
translate(rest, [delimiter, text, delimiter | acc], state)
end
defp translate([{:comment, text} | rest], acc, state) do
translate(rest, ["*/", text, "/*" | acc], state)
end
defp translate([{:at_rule, tokens} | rest], acc, state) do
{updated_tokens, state} = translate(tokens, [], state)
acc = [updated_tokens | acc]
translate(rest, acc, state)
end
defp translate([{:selector_list, []} | rest], acc, state) do
translate(rest, acc, state)
end
defp translate([{:selector_list, [tokens | list]} | rest], acc, state) do
{updated_tokens, state} = translate_selector_list(tokens, [], state)
acc = [updated_tokens | acc]
translate([{:selector_list, list} | rest], acc, state)
end
defp translate([{:declaration, tokens} | rest], acc, state) do
{updated_tokens, state} = translate(tokens, [], state)
acc = [updated_tokens | acc]
translate(rest, acc, state)
end
defp translate([{:block, symbol, tokens, _meta} | rest], acc, state) do
{updated_tokens, state} = translate(tokens, [], state)
acc = [@closing_symbol[symbol], updated_tokens, symbol | acc]
translate(rest, acc, state)
end
defp translate([], acc, state) do
{Enum.reverse(acc), state}
end
# :deep as the first selector
defp translate_selector_list([{:text, ":deep"}, {:block, "(", arg, _meta} | rest], acc, state) do
{updated_tokens, state} = translate(arg, [], state)
state = %{state | requires_data_attrs_on_root: true}
acc = [updated_tokens, "[#{@self_attr}][#{state.scope_attr_prefix}#{state.scope_id}] " | acc]
translate_selector(rest, acc, state)
end
defp translate_selector_list(tokens, acc, state) do
translate_selector(tokens, acc, state)
end
# TODO: check if it's maybe better to just always add [the-prefix-self][the-prefix-xxxxxx]
defp translate_selector([{:text, ":deep"}, {:block, "(", arg, _meta} | rest], acc, state) do
{updated_tokens, state} = translate(arg, [], state)
acc = [updated_tokens | acc]
translate_selector(rest, acc, state)
end
defp translate_selector([{:text, ":global"}, {:block, "(", arg, _meta} | rest], acc, state) do
{updated_tokens, state} = translate(arg, [], state)
acc = [updated_tokens | acc]
translate_selector(rest, acc, state)
end
defp translate_selector([{:ws, ws} | rest], acc, state) do
state = end_selector(state)
translate_selector(rest, [ws | acc], state)
end
defp translate_selector([{:text, combinator} | rest], acc, state)
when combinator in [">", "~", "+"] do
state = end_selector(state)
translate_selector(rest, [combinator | acc], state)
end
defp translate_selector([{:text, text} | rest], acc, state) do
{scoped?, state} =
case text do
"." <> class ->
state = %{state | selectors_buffer: [{:classes, "." <> class, class} | state.selectors_buffer]}
{true, state}
"#" <> id ->
state = %{state | selectors_buffer: [{:ids, "#" <> id, id} | state.selectors_buffer]}
{true, state}
<<first, _::binary>> when first in ?a..?z ->
state = %{state | selectors_buffer: [{:elements, text, text} | state.selectors_buffer]}
{true, state}
c when c in ["*", "&"] ->
state = %{state | selectors_buffer: [{:other, c, c} | state.selectors_buffer]}
{true, state}
_ ->
{false, state}
end
acc =
if scoped? do
["#{text}[#{state.scope_attr_prefix}#{state.scope_id}]" | acc]
else
[text | acc]
end
translate_selector(rest, acc, state)
end
defp translate_selector([token | rest], acc, state) do
{updated_tokens, state} = translate([token], [], state)
translate_selector(rest, [updated_tokens | acc], state)
end
defp translate_selector([], acc, state) do
state = end_selector(state)
{Enum.reverse(acc), state}
end
defp s_bind_arg_to_string(tokens) do
# TODO: validate invalid value
[expr] =
Enum.reduce(tokens, [], fn
{:string, _, text}, acc -> [text | acc]
{:ws, _}, acc -> acc
end)
expr
end
defp hash(text, length) do
:crypto.hash(:md5, text)
|> Base.encode32(case: :lower, padding: false)
|> String.slice(0, length)
end
defp put_selector(state, group, selector) do
update_in(state, [:selectors, group], fn sels ->
MapSet.put(sels, selector)
end)
end
defp end_selector(%{selectors_buffer: selectors_buffer} = state) do
state =
case selectors_buffer do
[] ->
state
[{group, _text, sel}] ->
put_selector(state, group, sel)
list ->
combined = MapSet.new(list, fn {_group, text, _sel} -> text end)
put_selector(state, :combined, combined)
end
%{state | selectors_buffer: []}
end
end