Current section
Files
Jump to
Current section
Files
lib/post_css/at_rule.ex
defmodule PostCSS.AtRule do
@moduledoc """
Represents a CSS at-rule like @import, @media, @keyframes, etc.
Examples:
- `@import url("styles.css");`
- `@media screen { ... }`
- `@keyframes slide { ... }`
"""
defstruct [
:name,
:params,
:source,
nodes: [],
raws: %{}
]
@type t :: %__MODULE__{
name: String.t(),
params: String.t() | nil,
nodes: [struct()],
source: map() | nil,
raws: map()
}
defimpl String.Chars do
def to_string(at_rule) do
# Follow PostCSS atrule() method logic
name = "@#{at_rule.name}"
params = at_rule.params || ""
# Add space between name and params if params exist
name_with_params =
if params != "" do
"#{name} #{params}"
else
name
end
if Enum.empty?(at_rule.nodes) do
# Simple at-rule like @import url(...);
"#{name_with_params};"
else
# Block at-rule like @media (...) { ... } - use same block logic as Rule
block(at_rule, name_with_params)
end
end
# Implements PostCSS block() method logic (same as Rule)
defp block(node, start) do
alias PostCSS.Raws
# Get "between" (space before opening brace) - defaults to beforeOpen (" ")
between = Map.get(node.raws || %{}, :between, Map.get(Raws.defaults(), :before_open))
# Build the opening: "start + between + {"
opening = "#{start}#{between}{"
# Handle body and after content
{body_content, after_content} =
if Enum.empty?(node.nodes) do
# Empty rule: use emptyBody default
{"", Map.get(node.raws || %{}, :after, Map.get(Raws.defaults(), :empty_body))}
else
# Has children: call body() and get after
body = body(node)
# If no explicit after raw, infer closing brace indentation from at-rule's own before indentation
after_raw =
case Map.get(node.raws || %{}, :after) do
nil ->
# Infer indentation from the at-rule's own before value
rule_before = Map.get(node.raws || %{}, :before, "")
if String.contains?(rule_before, "\n") do
# Extract the indentation part (everything after the last newline)
lines = String.split(rule_before, "\n")
last_line = List.last(lines) || ""
# Use the same indentation for the closing brace
"\n#{last_line}"
else
"\n"
end
explicit_after ->
explicit_after
end
{body, after_raw}
end
# Build final result: opening + body + after + "}"
"#{opening}#{body_content}#{after_content}}"
end
# Implements PostCSS body() method logic (same as Rule)
defp body(node) do
alias PostCSS.Raws
if Enum.empty?(node.nodes) do
""
else
# Find last non-comment node for semicolon logic
last_non_comment_index = find_last_non_comment_index(node.nodes)
# Get semicolon setting for the container (default true for CSS formatting)
container_semicolon = Map.get(node.raws || %{}, :semicolon, true)
node.nodes
|> Enum.with_index()
|> Enum.map(fn {child, index} ->
# Get "before" whitespace using PostCSS raw() logic
before = raw(child, :before)
# Stringify the child
child_str = Kernel.to_string(child)
# Add semicolon following PostCSS logic: last !== i || semicolon
needs_semicolon = index != last_non_comment_index or container_semicolon
semicolon_str =
if needs_semicolon and match?(%PostCSS.Declaration{}, child), do: ";", else: ""
"#{before}#{child_str}#{semicolon_str}"
end)
|> Enum.join("")
end
end
# Find the index of the last non-comment node
defp find_last_non_comment_index(nodes) do
nodes
|> Enum.with_index()
|> Enum.reverse()
|> Enum.find(fn {node, _index} -> not match?(%PostCSS.Comment{}, node) end)
|> case do
{_node, index} -> index
nil -> -1
end
end
# Implements PostCSS raw() method logic for "before" values
defp raw(node, :before) do
alias PostCSS.Raws
# Check if node has its own "before" raw
raw_before =
case Map.get(node.raws || %{}, :before) do
nil ->
# Apply PostCSS logic for different node types
case node do
%PostCSS.Declaration{} -> Map.get(Raws.defaults(), :before_decl)
%PostCSS.Comment{} -> Map.get(Raws.defaults(), :before_comment)
_ -> Map.get(Raws.defaults(), :before_rule)
end
value ->
# Normalize inline formatting to block formatting for declarations
if match?(%PostCSS.Declaration{}, node) and is_binary(value) and
not String.contains?(value, "\n") do
# Convert inline spacing (like " ") to proper block formatting
Map.get(Raws.defaults(), :before_decl)
else
value
end
end
# Apply PostCSS beforeAfter logic: add indentation if value contains newline
# BUT only if it doesn't already have proper indentation
if String.contains?(raw_before, "\n") do
# Check if the value already has indentation after the newline
lines = String.split(raw_before, "\n")
last_line = List.last(lines) || ""
# If the last line is empty or only whitespace, and we're at the default level,
# add indentation. Otherwise, preserve the existing formatting.
if last_line == "" or (String.trim(last_line) == "" and String.length(last_line) < 2) do
indent = Map.get(Raws.defaults(), :indent)
# For nodes inside a rule block, add 1 level of indentation (depth = 1)
raw_before <> indent
else
# Already has indentation, preserve it
raw_before
end
else
raw_before
end
end
end
end