Skip to main content

bluesky_text pub package Dart SDK Version

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.

info

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

tip

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.

tip

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.

MethodDescription
handlesExtracts all handles and byte string unit Indices in the text.
linksExtracts all links and byte string unit Indices in the text.
entitiesExtracts all handles and links and byte string unit Indices in the text.
caution

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);
}
tip

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.

info

The bluesky package is a powerful wrapper library that supports the AT Protocol and Bluesky APIs. You can see more details here.

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);
}
}
}