defmodule PostCSS.Root do @moduledoc """ Represents the root node of a PostCSS AST. The root contains all top-level rules, at-rules, and comments. """ defstruct [ :source, nodes: [], raws: %{}, type: :root ] @type t :: %__MODULE__{ nodes: [struct()], source: map() | nil, raws: map(), type: :root } defimpl String.Chars do def to_string(root) do # Follow PostCSS root() method: this.body(node) + node.raws.after body_content = body(root) after_content = Map.get(root.raws || %{}, :after, "") "#{body_content}#{after_content}" end # Implements PostCSS body() method logic 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 container_semicolon = Map.get(node.raws || %{}, :semicolon, false) node.nodes |> Enum.with_index() |> Enum.map(fn {child, index} -> # Get "before" whitespace using PostCSS raw() logic # Special case: first child of root gets empty "before" (PostCSS logic) before = if index == 0 do # First rule in root gets empty string case Map.get(child.raws || %{}, :before) do nil -> "" value -> value end else raw(child, :before) end # 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 case Map.get(node.raws || %{}, :before) do nil -> # Apply PostCSS logic: first rule in root gets "" # For now, we'll use a simple fallback since we don't have parent references case node do %PostCSS.Declaration{} -> Raws.get(node, :before, Map.get(Raws.defaults(), :before_decl)) %PostCSS.Comment{} -> Raws.get(node, :before, Map.get(Raws.defaults(), :before_comment)) _ -> Raws.get(node, :before, Map.get(Raws.defaults(), :before_rule)) end value -> value end end end end