Current section
Files
Jump to
Current section
Files
lib/bluesky/facet.ex
defmodule Bluesky.Facet do
@moduledoc """
Helper module for interfacing with the Bluesky Facet API: https://docs.bsky.app/docs/advanced-guides/post-richtext
"""
@doc """
Extracts both link and hashtag facets from the given text.
Scans the input text for URLs and hashtags, returning their positions along
with associated features.
## Parameters
- `text`: A string containing the text to scan for links and hashtags.
## Examples
iex> Bluesky.Facet.facets("Visit us at https://example.com, and use #elixir")
[
%{
"index" => %{"byteStart" => 12, "byteEnd" => 31},
"features" => [%{"$type" => "app.bsky.richtext.facet#link", "uri" => "https://example.com"}]
},
%{
"index" => %{"byteStart" => 41, "byteEnd" => 48},
"features" => [%{"$type" => "app.bsky.richtext.facet#tag", "tag" => "elixir"}]
}
]
"""
def facets(text) do
link_facets = links(text)
hashtag_facets = hashtags(text)
link_facets ++ hashtag_facets
end
@doc """
Extracts link facets from the given text.
Scans the input text for URLs and returns their positions along with
associated features.
## Parameters
- `text`: A string containing the text to scan for links.
## Examples
iex> Bluesky.Facet.links("Visit us at https://example.com.")
[
%{
"index" => %{"byteStart" => 12, "byteEnd" => 31},
"features" => [%{"$type" => "app.bsky.richtext.facet#link", "uri" => "https://example.com"}]
}
]
iex> Bluesky.Facet.links("Check out https://sub.example.com/path, it's cool!")
[
%{
"index" => %{"byteStart" => 10, "byteEnd" => 38},
"features" => [%{"$type" => "app.bsky.richtext.facet#link", "uri" => "https://sub.example.com/path"}]
}
]
"""
def links(text) do
# First pass: match URLs with the protocol
url_regex = ~r/https?:\/\/[a-zA-Z0-9][-a-zA-Z0-9.]*\.[a-zA-Z0-9][-a-zA-Z0-9%_.~\/#?&=:]*/u
# Find all matches
matches = Regex.scan(url_regex, text, return: :index)
Enum.map(matches, fn [{start_index, length}] ->
# Extract the URL string
url = binary_part(text, start_index, length)
# Process the URL to remove trailing punctuation
clean_url =
case Regex.run(~r/(.*?)([.,!?;:]*)$/, url) do
[_, url_part, punctuation] when punctuation != "" ->
url_part
_ ->
url
end
# Calculate the adjusted length
adjusted_length = String.length(clean_url)
%{
"index" => %{
"byteStart" => start_index,
"byteEnd" => start_index + adjusted_length
},
"features" => [
%{
"$type" => "app.bsky.richtext.facet#link",
"uri" => clean_url
}
]
}
end)
end
@doc """
Extracts hashtag facets from the given text.
Scans the input text for hashtags and returns their positions along with
associated features. Hashtags are captured from the '#' until the next
whitespace character and are only included if they consist entirely of
alphanumeric characters and underscores.
## Parameters
- `text`: A string containing the text to scan for hashtags.
## Examples
iex> Bluesky.Facet.hashtags("Check out the #elixir programming language!")
[
%{
"index" => %{"byteStart" => 14, "byteEnd" => 21},
"features" => [%{"$type" => "app.bsky.richtext.facet#tag", "tag" => "elixir"}]
}
]
iex> Bluesky.Facet.hashtags("Invalid hashtag: #$invalid")
[]
"""
def hashtags(text) do
# Capture hashtags: the # symbol followed by alphanumeric characters and underscores
# This will stop capturing at the first non-alphanumeric character
hashtag_regex = ~r/#([a-zA-Z0-9_]+)/u
hashtags = Regex.scan(hashtag_regex, text)
Enum.map(hashtags, fn [full_tag, tag_text] ->
{start_index, length} = :binary.match(text, full_tag)
%{
"index" => %{
"byteStart" => start_index,
"byteEnd" => start_index + length
},
"features" => [
%{
"$type" => "app.bsky.richtext.facet#tag",
"tag" => tag_text
}
]
}
end)
end
end