Current section
Files
Jump to
Current section
Files
lib/excavator.ex
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 `<script>` tags.
## Example
iex> html = "<html><script>var apiKey = \\"secret\\";</script></html>"
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 = "<html><script>window.__STATE__ = {\\"ok\\": true};</script></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