Packages
phoenix_kit
1.7.76
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/modules/publishing/metadata.ex
defmodule PhoenixKit.Modules.Publishing.Metadata do
@moduledoc """
Content metadata helpers for the Publishing module.
Provides title extraction from markdown/component content.
"""
@default_title PhoenixKit.Modules.Publishing.Constants.default_title()
@doc """
Extracts title from markdown content.
Looks for the first H1 heading (# Title) within the first few lines.
Falls back to the first line if no H1 found.
"""
@spec extract_title_from_content(String.t()) :: String.t()
def extract_title_from_content(content) when is_binary(content) do
content
|> String.trim()
|> do_extract_title()
end
def extract_title_from_content(_), do: @default_title
defp do_extract_title(""), do: @default_title
defp do_extract_title(content) do
content
|> extract_title_from_lines()
|> case do
@default_title ->
extract_title_from_components(content) || @default_title
title ->
title
end
end
defp extract_title_from_lines(""), do: @default_title
defp extract_title_from_lines(content) do
lines =
content
|> extract_candidate_lines()
|> Enum.take(15)
# Look for first H1 heading (# Title)
h1_line =
Enum.find(lines, fn line ->
String.starts_with?(line, "# ") and String.length(line) > 2
end)
cond do
h1_line != nil ->
h1_line
|> String.trim_leading("# ")
|> String.trim()
not Enum.empty?(lines) ->
List.first(lines)
|> String.slice(0, 100)
true ->
@default_title
end
end
defp extract_candidate_lines(content) do
{lines, _depth} =
content
|> String.split("\n")
|> Enum.reduce({[], 0}, fn raw_line, {acc, depth} ->
line = String.trim(raw_line)
cond do
line == "" and depth == 0 ->
{acc, depth}
component_self_closing?(line) ->
{acc, depth}
component_open?(line) ->
{acc, depth + 1}
depth > 0 and multiline_self_close?(raw_line) ->
{acc, max(depth - 1, 0)}
component_close?(line) and depth > 0 ->
{acc, max(depth - 1, 0)}
depth > 0 ->
{acc, depth}
true ->
{[line | acc], depth}
end
end)
lines
|> Enum.reverse()
|> Enum.reject(&(&1 == ""))
end
defp component_open?(line) do
String.starts_with?(line, "<") and
not String.starts_with?(line, "</") and
Regex.match?(~r/^<[A-Z][\w-]*/, line)
end
defp component_close?(line) do
Regex.match?(~r{^</[A-Z][\w-]*>}, line)
end
defp component_self_closing?(line) do
component_open?(line) and String.ends_with?(line, "/>")
end
defp multiline_self_close?(line) do
line
|> String.trim()
|> case do
"/>" -> true
">" -> false
other -> String.ends_with?(other, "/>")
end
end
defp extract_title_from_components(content) do
component_title(content, "Headline") ||
component_attribute(content, "Hero", "title") ||
component_title(content, "Title")
end
defp component_title(content, tag) do
regex = ~r/<#{tag}\b[^>]*>(.*?)<\/#{tag}>/is
case Regex.run(regex, content, capture: :all_but_first) do
[inner | _] -> sanitize_component_text(inner)
_ -> nil
end
end
defp component_attribute(content, tag, attr) do
regex = ~r/<#{tag}\b[^>]*#{attr}="([^"]+)"[^>]*>/i
case Regex.run(regex, content, capture: :all_but_first) do
[value | _] -> sanitize_component_text(value)
_ -> nil
end
end
defp sanitize_component_text(text) do
text
|> String.trim()
|> String.replace(~r/<[^>]+>/, "")
|> String.replace(~r/\s+/, " ")
|> String.trim()
|> case do
"" -> nil
cleaned -> String.slice(cleaned, 0, 100)
end
end
end