defmodule SimdXml do @moduledoc """ SIMD-accelerated XML parsing with full XPath 1.0 support. SimdXml parses XML into a flat structural index (~16 bytes per tag) using SIMD instructions, then evaluates XPath expressions against it using array operations. There is no DOM tree, no atom creation, and no XXE vulnerabilities. ## Quick start doc = SimdXml.parse!("Elixir") SimdXml.xpath_text!(doc, "//title") #=> ["Elixir"] ## Compiled queries For repeated queries across many documents, compile the XPath once and reuse it. The compiled query is a NIF resource that can be shared across processes safely: query = SimdXml.compile!("//title") SimdXml.eval_text!(doc, query) See `SimdXml.XPath` for details on the compile-once-run-many pattern. ## Query combinators Build queries programmatically with `SimdXml.Query` instead of writing XPath strings by hand: import SimdXml.Query query = descendant("book") |> child("title") |> text() SimdXml.query!(doc, query) See `SimdXml.Query` for the full combinator API. ## Element navigation Navigate the document tree through immutable element references. Elements implement `Enumerable`, so standard `Enum` functions work on child elements: root = SimdXml.Document.root(doc) Enum.map(root, & &1.tag) # child element tags See `SimdXml.Element` for the navigation API. ## Batch processing Process thousands of documents with a single compiled query. Bloom filter prescanning skips documents that cannot match: query = SimdXml.compile!("//claim") SimdXml.Batch.eval_text_bloom(xml_binaries, query) See `SimdXml.Batch` for batch operations. ## Quick grep mode For simple `//tagname` extraction at near-memory-bandwidth speed, skip the structural index entirely: scanner = SimdXml.Quick.new("claim") SimdXml.Quick.extract_first(scanner, xml) See `SimdXml.Quick` for the grep-mode API. ## Related modules * `SimdXml.Document` - parsed document handle and XPath evaluation * `SimdXml.Element` - element navigation and attribute access * `SimdXml.XPath` - compiled XPath expressions * `SimdXml.Query` - composable query builders * `SimdXml.Result` - convenience accessors (`one/2`, `fetch/2`) * `SimdXml.Batch` - multi-document batch processing * `SimdXml.Quick` - grep-mode fast path * `SimdXml.Error` - exception type for bang functions """ alias SimdXml.{Document, Native, Query, XPath} # --------------------------------------------------------------------------- # Parsing # --------------------------------------------------------------------------- @doc """ Parses an XML binary into a document. Returns `{:ok, document}` on success, or `{:error, reason}` if the XML is malformed. The returned `SimdXml.Document` holds an immutable structural index on the Rust side. It is reference-counted and garbage-collected by the BEAM -- no manual cleanup is needed. ## Examples iex> {:ok, doc} = SimdXml.parse("") iex> SimdXml.Document.tag_count(doc) 1 iex> {:error, _reason} = SimdXml.parse("not xml <<<") """ @spec parse(binary()) :: {:ok, Document.t()} | {:error, String.t()} def parse(xml) when is_binary(xml) do case Native.parse(xml) do {:ok, ref} -> {:ok, %Document{ref: ref}} {:error, _} = err -> err end end @doc """ Parses an XML binary into a document, raising on error. Same as `parse/1` but returns the document directly or raises `SimdXml.Error` if parsing fails. ## Examples iex> doc = SimdXml.parse!("") iex> SimdXml.Document.tag_count(doc) 3 """ @spec parse!(binary()) :: Document.t() def parse!(xml) do case parse(xml) do {:ok, doc} -> doc {:error, reason} -> raise SimdXml.Error, reason end end @doc """ Parses with query-driven optimization. Only indexes tags relevant to the given XPath expression. This is faster than `parse/1` when you know the query upfront and the document is large, because irrelevant structural data is never materialized. ## Examples iex> {:ok, doc} = SimdXml.parse_for_xpath("12", "//a") iex> SimdXml.xpath_text!(doc, "//a") ["1"] """ @spec parse_for_xpath(binary(), String.t()) :: {:ok, Document.t()} | {:error, String.t()} def parse_for_xpath(xml, xpath) when is_binary(xml) and is_binary(xpath) do case Native.parse_for_xpath(xml, xpath) do {:ok, ref} -> {:ok, %Document{ref: ref}} {:error, _} = err -> err end end @doc """ Parses with query-driven optimization, raising on error. Same as `parse_for_xpath/2` but returns the document directly or raises `SimdXml.Error`. ## Examples iex> doc = SimdXml.parse_for_xpath!("Hi", "//title") iex> SimdXml.xpath_text!(doc, "//title") ["Hi"] """ @spec parse_for_xpath!(binary(), String.t()) :: Document.t() def parse_for_xpath!(xml, xpath) do case parse_for_xpath(xml, xpath) do {:ok, doc} -> doc {:error, reason} -> raise SimdXml.Error, reason end end # --------------------------------------------------------------------------- # XPath (string expressions) # --------------------------------------------------------------------------- @doc """ Evaluates an XPath expression, returning direct child text of each match. This returns only the immediate text content of matched elements. For `

Hello world

`, querying for `//p` returns `["Hello "]` because only the direct text child is included. Use `xpath_string/2` if you need all descendant text concatenated. ## Examples iex> doc = SimdXml.parse!("12") iex> SimdXml.xpath_text(doc, "//a") {:ok, ["1"]} iex> doc = SimdXml.parse!("12") iex> SimdXml.xpath_text(doc, "//a") {:ok, ["1", "2"]} """ @spec xpath_text(Document.t(), String.t()) :: {:ok, [String.t()]} | {:error, String.t()} def xpath_text(%Document{} = doc, expr), do: Document.xpath_text(doc, expr) @doc """ Evaluates an XPath expression for direct child text, raising on error. Same as `xpath_text/2` but returns the list directly or raises `SimdXml.Error`. ## Examples iex> doc = SimdXml.parse!("12") iex> SimdXml.xpath_text!(doc, "//b") ["2"] iex> doc = SimdXml.parse!("1") iex> SimdXml.xpath_text!(doc, "//missing") [] """ @spec xpath_text!(Document.t(), String.t()) :: [String.t()] def xpath_text!(%Document{} = doc, expr) do case xpath_text(doc, expr) do {:ok, results} -> results {:error, reason} -> raise SimdXml.Error, reason end end @doc """ Evaluates an XPath expression, returning the string-value of each match. The string-value is all descendant text concatenated, which matches XPath's `string()` semantics. For `

Hello world

`, querying for `//p` returns `["Hello world"]`. Use this instead of `xpath_text/2` when elements contain mixed content (text interspersed with child elements). ## Examples iex> doc = SimdXml.parse!("

Hello world

") iex> SimdXml.xpath_string(doc, "//p") {:ok, ["Hello world"]} """ @spec xpath_string(Document.t(), String.t()) :: {:ok, [String.t()]} | {:error, String.t()} def xpath_string(%Document{} = doc, expr), do: Document.xpath_string(doc, expr) @doc """ Evaluates an XPath expression for string-values, raising on error. Same as `xpath_string/2` but returns the list directly or raises `SimdXml.Error`. ## Examples iex> doc = SimdXml.parse!("

Hello world

") iex> SimdXml.xpath_string!(doc, "//p") ["Hello world"] """ @spec xpath_string!(Document.t(), String.t()) :: [String.t()] def xpath_string!(%Document{} = doc, expr) do case xpath_string(doc, expr) do {:ok, results} -> results {:error, reason} -> raise SimdXml.Error, reason end end # --------------------------------------------------------------------------- # Compiled queries # --------------------------------------------------------------------------- @doc """ Compiles an XPath expression for reuse across documents. Returns `{:ok, xpath}` on success, or `{:error, reason}` if the expression is invalid. Compiled queries avoid re-parsing the XPath string on every evaluation and can be shared safely across processes. See `SimdXml.XPath` for more on the compile-once-run-many pattern. ## Examples iex> {:ok, query} = SimdXml.compile("//title") iex> query.expr "//title" iex> {:error, _reason} = SimdXml.compile("///invalid[") """ @spec compile(String.t()) :: {:ok, XPath.t()} | {:error, String.t()} def compile(expr) when is_binary(expr) do case Native.compile_xpath(expr) do {:ok, ref} -> {:ok, %XPath{ref: ref, expr: expr}} {:error, _} = err -> err end end @doc """ Compiles an XPath expression, raising on error. Same as `compile/1` but returns the `SimdXml.XPath` directly or raises `SimdXml.Error`. ## Examples iex> query = SimdXml.compile!("//title") iex> query.expr "//title" """ @spec compile!(String.t()) :: XPath.t() def compile!(expr) do case compile(expr) do {:ok, xpath} -> xpath {:error, reason} -> raise SimdXml.Error, reason end end @doc """ Evaluates a compiled XPath, returning direct child text of each match. This is the compiled-query equivalent of `xpath_text/2`. Use this when running the same query against many documents for best performance. ## Examples iex> query = SimdXml.compile!("//title") iex> doc = SimdXml.parse!("Hello") iex> SimdXml.eval_text(doc, query) {:ok, ["Hello"]} """ @spec eval_text(Document.t(), XPath.t()) :: {:ok, [String.t()]} | {:error, String.t()} def eval_text(%Document{ref: doc_ref}, %XPath{ref: xpath_ref}) do Native.compiled_eval_text(doc_ref, xpath_ref) end @doc """ Evaluates a compiled XPath for text, raising on error. Same as `eval_text/2` but returns the list directly or raises `SimdXml.Error`. ## Examples iex> query = SimdXml.compile!("//title") iex> doc = SimdXml.parse!("Hello") iex> SimdXml.eval_text!(doc, query) ["Hello"] """ @spec eval_text!(Document.t(), XPath.t()) :: [String.t()] def eval_text!(doc, xpath) do case eval_text(doc, xpath) do {:ok, results} -> results {:error, reason} -> raise SimdXml.Error, reason end end @doc """ Counts the number of matches for a compiled XPath. More efficient than evaluating and counting results, because no text extraction occurs. ## Examples iex> query = SimdXml.compile!("//item") iex> doc = SimdXml.parse!("") iex> SimdXml.eval_count(doc, query) {:ok, 3} """ @spec eval_count(Document.t(), XPath.t()) :: {:ok, non_neg_integer()} | {:error, String.t()} def eval_count(%Document{ref: doc_ref}, %XPath{ref: xpath_ref}) do Native.compiled_eval_count(doc_ref, xpath_ref) end @doc """ Counts matches for a compiled XPath, raising on error. Same as `eval_count/2` but returns the count directly or raises `SimdXml.Error`. ## Examples iex> query = SimdXml.compile!("//item") iex> doc = SimdXml.parse!("") iex> SimdXml.eval_count!(doc, query) 2 """ @spec eval_count!(Document.t(), XPath.t()) :: non_neg_integer() def eval_count!(doc, xpath) do case eval_count(doc, xpath) do {:ok, count} -> count {:error, reason} -> raise SimdXml.Error, reason end end @doc """ Checks whether a compiled XPath has any matches in the document. Short-circuits after the first match, so this is faster than `eval_count/2` when you only need a boolean answer. ## Examples iex> query = SimdXml.compile!("//item") iex> doc = SimdXml.parse!("") iex> SimdXml.eval_exists?(doc, query) {:ok, true} iex> query = SimdXml.compile!("//missing") iex> doc = SimdXml.parse!("") iex> SimdXml.eval_exists?(doc, query) {:ok, false} """ @spec eval_exists?(Document.t(), XPath.t()) :: {:ok, boolean()} | {:error, String.t()} def eval_exists?(%Document{ref: doc_ref}, %XPath{ref: xpath_ref}) do Native.compiled_eval_exists(doc_ref, xpath_ref) end # --------------------------------------------------------------------------- # Query combinator execution # --------------------------------------------------------------------------- @doc """ Executes a query combinator against a document. The query is compiled to an XPath string via `SimdXml.Query.to_xpath/1` and evaluated. The return type depends on the query's `:return_type` setting: * `:text` (default) - direct child text via `xpath_text/2` * `:string` - string-value (all descendant text) via `xpath_string/2` * `:nodes` - element node references * `:count` - match count as a number * `:exists` - boolean existence check ## Examples iex> import SimdXml.Query iex> doc = SimdXml.parse!("12") iex> SimdXml.query(doc, descendant("a") |> text()) {:ok, ["1"]} iex> import SimdXml.Query iex> doc = SimdXml.parse!("12") iex> SimdXml.query!(doc, descendant("a") |> text()) ["1", "2"] """ @spec query(Document.t(), Query.t()) :: {:ok, term()} | {:error, String.t()} def query(%Document{} = doc, %Query{} = q) do xpath_str = Query.to_xpath(q) case q.return_type do :text -> xpath_text(doc, xpath_str) :string -> xpath_string(doc, xpath_str) :nodes -> Document.xpath_nodes(doc, xpath_str) :count -> Document.eval(doc, "count(#{xpath_str})") :exists -> Document.eval(doc, "boolean(#{xpath_str})") end end @doc """ Executes a query combinator, raising on error. Same as `query/2` but returns the result directly or raises `SimdXml.Error`. ## Examples iex> import SimdXml.Query iex> doc = SimdXml.parse!("12") iex> SimdXml.query!(doc, descendant("a") |> text()) ["1"] """ @spec query!(Document.t(), Query.t()) :: term() def query!(doc, q) do case query(doc, q) do {:ok, results} -> results {:error, reason} -> raise SimdXml.Error, reason end end end