bluesky_text
bluesky_text is a package that elegantly resolves RichText
called Facet
in Bluesky API.
This package automatically extracts basic entities from text, such as handles (@)
and links (http|https)
, and generates facets that conform to the Bluesky API specification.
In bluesky_text, text elements are counted as Unicode Grapheme Clusters. In other words, multibyte characters includes emojis are also counted as a single character.
If you use this package with Bluesky API, see bluesky.
Features ⭐
- ✅ Zero Dependency
- ✅ Automatic Detection of
Handle
,Link
,Tag
in text - ✅ Supports Automatic Conversion to Facet
- ✅ 100% Compatible with bluesky
- ✅ Allows Extraction of Custom Entities
- ✅ Supports Unicode Grapheme Clusters
- ✅ Supports Safe Text Splitting
- ✅ Works in All Languages
- ✅ Supports Markdown Style Links
- ✅ Well Documented and Well Tested
- ✅ 100% Null Safety
Getting Started 💪
Install
See the Install Package section for more details on how to install a package in your Dart and Flutter app.
With Dart:
dart pub add bluesky_text
dart pub get
With Flutter:
flutter pub add bluesky_text
flutter pub get
Import
Just by writing following one-line import, you can use all the features provided by bluesky_text.
import 'package:bluesky_text/bluesky_text.dart';
Instantiate BlueskyText
All you have to do with the bluesky_text is pass any text to the BlueskyText object.
import 'package:bluesky_text/bluesky_text.dart';
void main() {
// You just need to pass text to parse.
final text = BlueskyText(
'I speak 日本語 and English 🚀 @shinyakato.dev and @shinyakato.bsky.social. '
'Visit 🚀 https://shinyakato.dev.',
);
print(text.value); // String equivalent to the string passed to BlueskyText.
}
Extract Entities
The entities in the text passed to BlueskyText
in the above example are as follows.
- Handles
@shinyakato.dev
@shinyakato.bsky.social
- Links
https://shinyakato.dev
To extract all of these entities, call the entities
property as follows.
import 'dart:convert';
import 'package:bluesky_text/bluesky_text.dart';
void main() {
// You just need to pass text to parse.
final text = BlueskyText(
'I speak 日本語 and English 🚀 @shinyakato.dev and @shinyakato.bsky.social. '
'Visit 🚀 https://shinyakato.dev.',
);
// Extract all entities.
final entities = text.entities;
// Convert entities to JSON representation.
print(entities.map((e) => jsonEncode(e.toJson())).toList());
}
Then you will then get the following result, which includes all the entities just listed. The response is formatted as JSON to make the structure of the response easier to understand.
[
{
"type": "handle",
"value": "@shinyakato.dev",
"indices": { "start": 35, "end": 50 }
},
{
"type": "handle",
"value": "@shinyakato.bsky.social",
"indices": { "start": 55, "end": 78 }
},
{
"type": "link",
"value": "https://shinyakato.dev",
"indices": { "start": 91, "end": 113 }
}
]
As shown above, for each type of entity, you can correctly obtain the entity and the Indices of the location where the entity appears. This Indices is the position of occurrence expressed in bytes.
If you want to extract only Handles, you can use the handles
property. And if you want to extract only Links, you can use the links
property.
import 'package:bluesky_text/bluesky_text.dart';
void main() {
// You just need to pass text to parse.
final text = BlueskyText(
'I speak 日本語 and English 🚀 @shinyakato.dev and @shinyakato.bsky.social. '
'Visit 🚀 https://shinyakato.dev.',
);
print(text.handles); // Only handles.
print(text.links); // Only links.
}
And check following table.
Method | Description |
---|---|
handles | Extracts all handles and byte string unit Indices in the text. |
links | Extracts all links and byte string unit Indices in the text. |
entities | Extracts all handles and links and byte string unit Indices in the text. |
The current specification defines 300 characters as the maximum number of characters for bluesky_text, which is the text limit defined in the Lexicon of app.bsky.feed.post.
This means that you must check to see if the text you pass to the BlueskyText
object exceeds the maximum number of characters and Split Text if necessary.
You can check if the string passed to the BlueskyText
object exceeds the maximum number of characters with isLengthLimitExceeded
or isNotLengthLimitExceeded
as follows.
import 'package:bluesky_text/bluesky_text.dart';
Future<void> main() async {
final text = BlueskyText(
'I speak 日本語 and English 🚀 @shinyakato.dev and @shinyakato.bsky.social. '
'Visit 🚀 https://shinyakato.dev.',
);
print(text.isLengthLimitExceeded);
print(text.isNotLengthLimitExceeded);
}
Generate Facets
To generate Facets that can be used with the Bluesky API using the extracted collection of facets, use toFacets
method as follows.
import 'dart:convert';
import 'package:bluesky_text/bluesky_text.dart';
Future<void> main() async {
// You just need to pass text to parse.
final text = BlueskyText(
'I speak 日本語 and English 🚀 @shinyakato.dev and @shinyakato.bsky.social. '
'Visit 🚀 https://shinyakato.dev.',
);
final entities = text.entities;
// Convert entities to collection of facets.
final facets = await entities.toFacets();
// Convert to JSON representation.
print(jsonEncode(facets));
}
The toFacets
method involves asynchronous processing to resolve user handles as DIDs, so add the Future
and async
modifiers to the calling method.
Then specify await
when calling the toFacets
method. If the Entity's collection contains only links, it will also be processed asynchronously.
Executing the above example, you will get a JSON object converted to a Bluesky API compliant facets structure, as shown below. In the following example, the response is converted to JSON to make the structure of the response easier to understand.
[
{
"index": { "byteStart": 35, "byteEnd": 50 },
"features": [
{
"$type": "app.bsky.richtext.facet#mention",
"did": "did:plc:iijrtk7ocored6zuziwmqq3c"
}
]
},
{
"index": { "byteStart": 55, "byteEnd": 78 },
"features": [
{
"$type": "app.bsky.richtext.facet#mention",
"did": "did:plc:wpyvghtrmnflwxmknbz67vct"
}
]
},
{
"index": { "byteStart": 91, "byteEnd": 113 },
"features": [
{
"$type": "app.bsky.richtext.facet#link",
"uri": "https://shinyakato.dev"
}
]
}
]
More Tips 🏄
Unicode Grapheme Clusters
Text in Bluesky is counted as Unicode Grapheme Clusters. So, even multibyte characters such as Japanese and emoji are counted as one character.
bluesky_text also counts characters in Grapheme Cluster units.
The count of characters related to the text interpreted by bluesky_text can be obtained with the .length
property as follows.
import 'package:bluesky_text/bluesky_text.dart';
Future<void> main() async {
final text = BlueskyText(
'I speak 日本語 and English 🚀 @shinyakato.dev and @shinyakato.bsky.social. '
'Visit 🚀 https://shinyakato.dev.',
);
// Japanese and emojis are counted as one character.
print(text.length);
}
If you want to know about Grapheme Clusters, check this page.
Use with bluesky
Facet generated by bluesky_text can be easily integrated into the bluesky package. There are several endpoints that use Facet in the Bluesky API, but we will use the endpoint that creates a post as an example.
You can integrate bluesky_text and bluesky as follows.
import 'package:bluesky/bluesky.dart' as bsky;
import 'package:bluesky_text/bluesky_text.dart';
Future<void> main() async {
final text = BlueskyText(
'I speak 日本語 and English 🚀 @shinyakato.dev and @shinyakato.bsky.social. '
'Visit 🚀 https://shinyakato.dev.',
);
// Bluesky object.
final bluesky = bsky.Bluesky.fromSession(await _session);
// Convert entities to facets representation.
final facets = await text.entities.toFacets();
await bluesky.feed.createPost(
text: text.value,
// Convert JSON to Facet object like this.
facets: facets.map(bsky.Facet.fromJson).toList(),
);
}
You see that there is nothing difficult to implement here. Extracting and faceting entities using bluesky_text is implemented as described in Getting Started.
And the bluesky.feed.createPost
used as an example is a fairly basic method for creating a post using the bluesky package.
But, this is where bluesky_text is most powerful, as it can be converted into a Facet
object in the bluesky package without any difficult processing.
With bluesky_text, you can create Facets that are difficult to structure when creating a post using the Bluesky API without having to think about it.
Split Text
As mentioned in the section describing entity extraction, the BlueskyText
object has a maximum number of characters for text.
This is because the maximum number of characters allowed for each text in the Bluesky API is clearly defined, and any operation on a text that exceeds the maximum number of characters is meaningless.
So do you always need to be aware of the number of characters of text you pass to the BlueskyText
object in units of Grapheme Cluster? No, you don't need it. Instead, just call .split()
as follows.
You can also use isLengthLimitExceeded
or isNotLengthLimitExceeded
to determine if the text exceeds the maximum number of characters.
import 'package:bluesky_text/bluesky_text.dart';
Future<void> main() async {
final text = BlueskyText('a' * 301);
// Whether the maximum number of characters is exceeded.
if (text.isLengthLimitExceeded) {
// Split based on the maximum number of characters.
// Returns new collection of BlueskyText.
final texts = text.split();
for (final _text in texts) {
print(_text.entities);
}
}
}