defmodule Text.Sentiment.Backends.Lexicon do @moduledoc """ Default sentiment backend — lexicon-based, multilingual via the bundled AFINN lexicons. This module implements the `Text.Sentiment.Backend` behaviour by wrapping `Text.Sentiment.Lexicon.score/3`, picking the bundled `Text.Sentiment.Lexicons.AFINN` lexicon that matches the supplied `:language` option (with fallback to `:en`). Most callers don't reference this module directly — they go through `Text.Sentiment.analyze/2` or `label/2`, which dispatch via `Text.Sentiment.Backend.resolve/1`. """ @behaviour Text.Sentiment.Backend alias Text.Language, as: LanguageTag alias Text.Sentiment.{Lexicon, Lexicons} @default_language :en @impl true def analyze(text, options \\ []) when is_binary(text) do {language, lexicon, scoring_opts} = resolve_lexicon(options) text |> Lexicon.score(lexicon, scoring_opts) |> Map.put(:language, language) |> Map.put(:backend, __MODULE__) end # ---- internal: option resolution --------------------------------------- @scoring_keys [ :tokenizer, :fold_case, :negators, :intensifiers, :diminishers, :negation_window, :negation_scalar, :intensifier_boost, :diminisher_factor, :positive_threshold, :negative_threshold ] defp resolve_lexicon(options) do {scoring_opts, control_opts} = Keyword.split(options, @scoring_keys) cond do lexicon = Keyword.get(control_opts, :lexicon) -> language = control_opts |> Keyword.get(:language, :custom) |> normalize_or_default(:custom) {language, lexicon, scoring_opts} true -> language = control_opts |> Keyword.get(:language, @default_language) |> normalize_or_default(@default_language) fallback = control_opts |> Keyword.get(:fallback_language, @default_language) |> normalize_or_default(@default_language) {used, lexicon} = bundled_or_fallback(language, fallback) {used, lexicon, scoring_opts} end end defp normalize_or_default(input, default) when is_nil(input), do: default defp normalize_or_default(input, _default) do LanguageTag.normalize(input) rescue FunctionClauseError -> input end defp bundled_or_fallback(language, fallback) do if language in Lexicons.AFINN.available() do {language, Lexicons.AFINN.lexicon(language)} else {fallback, Lexicons.AFINN.lexicon(fallback)} end end end