defmodule SEO.Utils do
@moduledoc false
# `SEO.JSONLD.Actions` is emitted by `:seo_jsonld` after `:elixir` runs
# in this library's own compile chain (and is always present in user
# apps, where compiler ordering is reversed). The directive suppresses
# the bootstrap-only warning; the module is guaranteed at runtime.
@compile {:no_warn_undefined, SEO.JSONLD.Actions}
use Phoenix.Component
alias SEO.JSONLD.Actions
attr :property, :string, required: true
attr :content, :any, required: true, doc: "Either a string representing a URI, or a URI"
def url(assigns) do
case assigns[:content] do
%URI{} ->
~H"""
"""
url when is_binary(url) ->
~H"""
"""
end
end
## TODO
# - Tokenizer that turns HTML into sentences. eg: https://github.com/wardbradt/HTMLST
def truncate(text, length \\ 200) do
if String.length(text) <= length do
text
else
String.slice(text, 0..(length - 1))
end
|> String.trim()
end
def squash_newlines(text), do: String.replace(text, "\n", " ")
def to_iso8601(%NaiveDateTime{} = ndt), do: NaiveDateTime.to_iso8601(ndt)
def to_iso8601(%DateTime{} = dt), do: DateTime.to_iso8601(dt)
def to_iso8601(%Date{} = d), do: Date.to_iso8601(d)
def to_iso8601(%Time{} = t), do: Time.to_iso8601(t)
def to_iso8601(%Duration{} = d), do: Duration.to_iso8601(d)
def to_iso8601(iso8601_string) when is_binary(iso8601_string), do: iso8601_string
def to_url_string(%URI{} = uri), do: URI.to_string(uri)
def to_url_string(url) when is_binary(url), do: url
# Build pipeline helpers, shared by every generated SEO.JSONLD.* module.
# Each step is a no-op when its metadata table is empty, so modules with
# no enum/date/uri fields pay zero runtime cost for that stage.
def build_convert_enums(map, enum_fields) when is_map(enum_fields) do
Enum.reduce(enum_fields, map, fn {field, values}, acc ->
case Map.fetch(acc, field) do
:error ->
acc
{:ok, atom} when is_atom(atom) ->
Map.put(acc, field, Map.fetch!(values, atom))
{:ok, other} ->
raise ArgumentError,
"expected an atom for #{inspect(field)}, got: #{inspect(other)}. " <>
"Valid values: #{inspect(Map.keys(values))}"
end
end)
end
# Walk the attrs map and coerce any Date/DateTime/Time/Duration/URI
# struct value to its canonical string form — at the top level and inside
# top-level lists. This sidesteps per-field allowlists: if the user passes
# a struct we can't serialize, convert it regardless of whether the
# schema.org range includes a URL/Date type. Nested maps are assumed to
# already be serializable (typically built by another `build/1` call).
def build_coerce_structs(map) do
Map.new(map, fn {k, v} -> {k, coerce_value(v)} end)
end
defp coerce_value(%Date{} = v), do: to_iso8601(v)
defp coerce_value(%DateTime{} = v), do: to_iso8601(v)
defp coerce_value(%NaiveDateTime{} = v), do: to_iso8601(v)
defp coerce_value(%Time{} = v), do: to_iso8601(v)
defp coerce_value(%Duration{} = v), do: to_iso8601(v)
defp coerce_value(%URI{} = v), do: to_url_string(v)
defp coerce_value(list) when is_list(list), do: Enum.map(list, &coerce_value/1)
defp coerce_value(v), do: v
def build_camelize_keys(map, key_map) do
Map.new(map, fn {k, v} ->
key =
case k do
atom when is_atom(atom) -> Map.get(key_map, atom) || Atom.to_string(atom)
other -> other
end
{key, v}
end)
end
# For Action descendants only (classes whose ancestry includes
# schema:Action). Pops the `:inputs` and `:outputs` pseudo-fields and
# expands them via `SEO.JSONLD.Actions` into the hyphenated
# `-input` / `-output` keys Schema.org's Action
# syntax requires (https://schema.org/docs/actions.html#part-4). Keys
# are already in their final `"-input"` string form, so the
# subsequent `build_camelize_keys` pass leaves them alone.
def build_expand_action_io(map) do
map
|> expand_action_io(:inputs, &Actions.inputs/1)
|> expand_action_io(:outputs, &Actions.outputs/1)
end
defp expand_action_io(map, key, expander) do
case Map.pop(map, key) do
{nil, _} -> map
{entries, rest} -> Map.merge(rest, expander.(entries))
end
end
def merge_defaults(_mod, nil, nil), do: nil
def merge_defaults(mod, attrs, nil), do: struct(mod, to_map(attrs))
def merge_defaults(mod, attrs, []), do: struct(mod, to_map(attrs))
def merge_defaults(mod, attrs, defaults) do
struct(mod, Map.merge(to_map(defaults), to_map(attrs)))
end
def to_map(nil), do: %{}
def to_map([]), do: %{}
def to_map(x) when is_struct(x), do: Map.from_struct(x) |> drop_nils()
def to_map(x) when is_list(x), do: Enum.into(x, %{}) |> drop_nils()
def to_map(x) when is_map(x), do: drop_nils(x)
defp drop_nils(map) when is_map(map) do
for {k, v} <- map, v != nil, into: %{}, do: {k, v}
end
end