Convert chelekom .eex source into per-helper axis-value clauses
that the MishkaCMS installer uses to narrow installs (e.g. base-only)
without guessing positional-arg semantics.
Two conceptual stages, exposed as one combined surface plus a few primitives:
Index build (
build_index/1) — walk the raw.eex, identify everydefp NAME(args) dodeclaration, and record the enclosing<%= if … do %>chain. Innermost wrapper comes LAST in the list so the consumer ANDs them.Clause conversion (
from_conditions/1) — convert a list of condition ASTs into a flat[%{"axis" => …, "values" => [...]}]list the installer can apply directly.
Both stages use real parsers — EEx.tokenize/1 for the template,
Code.string_to_quoted/1 + Macro.prewalk/3 for defp discovery
and condition shapes — so the chain is predictable and resilient
to whitespace / comment / paren noise.
Why
Chelekom's .eex source uses gating like:
<%= if "base" in @variant and ("red" in @color or "black" in @color) do %>
defp color_variant("base", "red"), do: "..."
defp color_variant("base", "black"), do: "..."
<% end %>The exporter EEx-evals the template with all options enabled, so the
bundle's helpers[] array contains the wrapped defps without their
wrapping context. This module recovers the context by re-parsing
the raw .eex (without evaluating).
Recognised condition shapes
"X" in @axis → [{axis, [X]}]
@axis in [X, Y] → [{axis, [X, Y]}]
"X" in @axis or "Y" in @axis → [{axis, [X, Y]}]
is_nil(@axis) or "X" in @axis → [{axis, [X]}] (is_nil branch
is "no filter applied", so
when the user DOES filter,
only the value path matters)
A and B → discriminators(A) ++ discriminators(B)
not A → ignored
everything else → ignoredWhen the same axis appears in multiple ANDed wrappers (different
<%= if %> blocks), value sets are intersected — both
conditions must hold.
Summary
Functions
Parse eex_source and return the helper-condition index.
Convert a single condition AST into a flat list of axis clauses.
Convert a list of condition ASTs into a flat list of axis clauses. Same-axis clauses produced by different conditions are MERGED via intersection (AND semantics — both wrappers must pass).
Collapse whitespace in an argument-list string + trim. Both this
module and the exporter call this on their respective sources so
(name, args) signatures match exactly.
Types
Functions
Parse eex_source and return the helper-condition index.
%{{helper_name, normalized_arg_pattern} => [condition_ast, ...]}Returns %{} on tokenization or parse failure (caller should fall
back to un-discriminated emission).
@spec from_condition(condition_ast()) :: [axis_clause()]
Convert a single condition AST into a flat list of axis clauses.
@spec from_conditions([condition_ast()]) :: [axis_clause()]
Convert a list of condition ASTs into a flat list of axis clauses. Same-axis clauses produced by different conditions are MERGED via intersection (AND semantics — both wrappers must pass).
Collapse whitespace in an argument-list string + trim. Both this
module and the exporter call this on their respective sources so
(name, args) signatures match exactly.