defmodule Oxide do @moduledoc """ Elixir bindings for Tailwind CSS Oxide — the Rust-powered content scanner. Oxide scans source files in parallel to extract Tailwind CSS candidate class names. It powers the fast incremental rebuilds in Tailwind CSS v4. ## Usage Create a scanner, scan for candidates, then feed them to the Tailwind compiler to generate CSS. # Create a scanner for your project scanner = Oxide.new(sources: [%{base: "lib/", pattern: "**/*.heex"}]) # Full scan — returns all candidates found across all files candidates = Oxide.scan(scanner) # ["flex", "sm:bg-red-500", "hover:text-white", ...] # Incremental scan — only returns NEW candidates from changed files new = Oxide.scan_files(scanner, [%{file: "lib/app_web/live/page.ex", extension: "ex"}]) For one-off extraction without a scanner: candidates = Oxide.extract(~s(class="flex bg-red-500"), "html") # [%{value: "class", position: 0}, %{value: "flex", position: 7}, ...] """ defmodule Source do @moduledoc "Source entry for scanner configuration." @type t :: %__MODULE__{base: String.t(), pattern: String.t(), negated: boolean()} defstruct base: "", pattern: "", negated: false end defmodule Glob do @moduledoc "Glob entry returned by the scanner." @type t :: %__MODULE__{base: String.t(), pattern: String.t()} defstruct base: "", pattern: "" end defmodule Changed do @moduledoc "Changed content for incremental scanning." @type t :: %__MODULE__{ file: String.t() | nil, content: String.t() | nil, extension: String.t() } defstruct file: nil, content: nil, extension: "" end defmodule Candidate do @moduledoc "Extracted candidate with byte position." @type t :: %__MODULE__{value: String.t(), position: non_neg_integer()} defstruct value: "", position: 0 end @doc """ Create a new scanner. ## Options * `:sources` — list of source entries, each a map or `%Oxide.Source{}` with `:base`, `:pattern`, and optional `:negated` (default `false`) ## Examples scanner = Oxide.new(sources: [ %{base: "lib/", pattern: "**/*.{ex,heex}"}, %{base: "assets/", pattern: "**/*.vue"} ]) """ @spec new(keyword()) :: reference() def new(opts \\ []) do sources = opts |> Keyword.get(:sources, []) |> Enum.map(fn %Source{} = s -> s map -> struct!(Source, Map.put_new(map, :negated, false)) end) Oxide.Native.new_scanner(sources) end @doc """ Full scan — walks filesystem, extracts all candidates. Returns a sorted, deduplicated list of candidate strings. Subsequent calls only return candidates from files changed since the last scan. """ @spec scan(reference()) :: [String.t()] def scan(scanner) do Oxide.Native.scan(scanner) end @doc """ Incremental scan — extract candidates from specific changed files. Returns only NEW candidates not seen in previous scans. This is what makes HMR fast. ## Examples new_candidates = Oxide.scan_files(scanner, [ %{file: "lib/app_web/live/page.ex", extension: "ex"} ]) # Or with inline content (no filesystem read): new_candidates = Oxide.scan_files(scanner, [ %{content: ~s(class="flex mt-4"), extension: "html"} ]) """ @spec scan_files(reference(), [map()]) :: [String.t()] def scan_files(scanner, changed) do changed = Enum.map(changed, fn %Changed{} = c -> c map -> struct!(Changed, map) end) Oxide.Native.scan_files(scanner, changed) end @doc """ Extract candidates with byte positions from a string. Stateless — does not require a scanner. Useful for one-off extraction. ## Examples candidates = Oxide.extract(~s(class="flex bg-red-500"), "html") [%Oxide.Candidate{value: "class", position: 0}, ...] """ @spec extract(String.t(), String.t()) :: [Candidate.t()] def extract(content, extension) do Oxide.Native.get_candidates(content, extension) end @doc """ Get all files discovered by the scanner. """ @spec files(reference()) :: [String.t()] def files(scanner) do Oxide.Native.get_files(scanner) end @doc """ Get generated glob patterns for setting up file watchers. """ @spec globs(reference()) :: [Glob.t()] def globs(scanner) do Oxide.Native.get_globs(scanner) end end