defmodule ExCavator do @moduledoc """ Excavate JavaScript variable values from HTML script tags using AST parsing. Supports: - `var/let/const x = VALUE` declarations - `window.x = VALUE` assignments (including nested: `window.a.b = VALUE`) - `JSON.parse('...')` calls with string literal arguments - Literal values: strings, numbers, booleans, null - Object and array expressions """ alias ExCavator.{AstWalker, Html, JsParser} @type extraction :: %{ name: String.t(), value: term(), source: :variable_declaration | :assignment } @doc """ Extract all recognized variable assignments from all `" iex> {:ok, [%{name: "apiKey", value: "secret"}]} = ExCavator.extract_all(html) """ @spec extract_all(String.t()) :: {:ok, [extraction()]} | {:error, term()} def extract_all(html) do with {:ok, scripts} <- Html.extract_script_contents(html) do results = scripts |> Enum.flat_map(fn js -> case JsParser.parse(js) do {:ok, estree} -> AstWalker.extract_assignments(estree) {:error, _} -> [] end end) {:ok, results} end end @doc """ Extract a specific variable by name. Supports dotted paths like `"window.__INITIAL_STATE__"`. Also matches without `"window."` prefix, so `extract(html, "__STATE__")` finds `window.__STATE__` assignments too. ## Example iex> html = "" iex> {:ok, %{"ok" => true}} = ExCavator.extract(html, "__STATE__") """ @spec extract(String.t(), String.t()) :: {:ok, term()} | {:error, :not_found | term()} def extract(html, variable_name) do with {:ok, results} <- extract_all(html) do results |> Enum.filter(&name_matches?(&1.name, variable_name)) |> List.last() |> case do nil -> {:error, :not_found} %{value: value} -> {:ok, value} end end end @doc """ Extract from a JS code string directly (skip HTML parsing). ## Example iex> {:ok, [%{name: "x", value: 42}]} = ExCavator.extract_from_js("const x = 42;") """ @spec extract_from_js(String.t()) :: {:ok, [extraction()]} | {:error, term()} def extract_from_js(js_code) do with {:ok, estree} <- JsParser.parse(js_code) do {:ok, AstWalker.extract_assignments(estree)} end end defp name_matches?(actual, query) do actual == query or actual == "window." <> query or String.ends_with?(actual, "." <> query) end end