MishkaChelekom.CmsBundle.Discriminators (Mishka Chelekom v0.0.9-alpha.20)

Copy Markdown View Source

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:

  1. Index build (build_index/1) — walk the raw .eex, identify every defp NAME(args) do declaration, and record the enclosing <%= if … do %> chain. Innermost wrapper comes LAST in the list so the consumer ANDs them.

  2. 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                  ignored

When 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

axis_clause()

@type axis_clause() :: %{required(String.t()) => any()}

condition_ast()

@type condition_ast() :: Macro.t()

index()

@type index() :: %{required(signature()) => [condition_ast()]}

signature()

@type signature() :: {String.t(), String.t()}

Functions

build_index(eex_source)

@spec build_index(String.t()) :: index()

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).

from_condition(ast)

@spec from_condition(condition_ast()) :: [axis_clause()]

Convert a single condition AST into a flat list of axis clauses.

from_conditions(conditions)

@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).

normalize_args(args)

@spec normalize_args(String.t()) :: String.t()

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.