NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev · #4226 most downloaded on pub.dev
Provides the easiest and most powerful way to analyze the text for Bluesky Social.
Last release 2 months ago
08 Aug 2026
Ships unpredictably
gaps range from 8 days to 8 months
Nearly every release is documented
notes for 59 of the last 60 stable releases
1 version withdrawn
withdrawn after publishing
3 years old
78 releases · first in 2023
One column per quarter.
feat: Entity.toFacet, Entities.toFacetsResult and Entities.toFacets accept an optional http.Client? client, forwarded to the built-in handle-resolutio
feat: Entity.toFacet, Entities.toFacetsResult and Entities.toFacets accept an optional http.Client? client, forwarded to the built-in handle-resolution call. It is used only when no resolver is supplied and exists so the default (network) mention path can be driven against a mock transport. Existing callers are unaffected — the parameter is optional and additive.
test: the mention-facet tests no longer reach the live network. Seven tests resolved real handles against bsky.social and asserted the returned DID, so they broke on any outage, rate limit, or handle change and could not run offline. They now inject a MockClient (via the new client hook) that answers resolveHandle locally, covering the success path, the 4xx "unresolvable handle → no facet" swallow, the 5xx "surface the error" rethrow, and that service reaches the request. One meaningless "does not throw" assertion was replaced with a real output check.
security: isLinkFacade no longer passes a display text it cannot parse. It used to return false — "not a facade" — both when the text named no host and when it named one that could not be read, so every shape the parser did not recognize went through unflagged.
。 U+3002, . U+FF0E, 。 U+FF61) become ., and the fullwidth ASCII block (U+FF01–U+FF5E) becomes plain ASCII. bsky。app and bsky.app are hosts a browser really resolves to bsky.app — the reader sees a domain they trust and the link works — and they were not flagged when pointed at another host. The host of the target is folded the same way, so a link written with a fullwidth host still matches an ASCII display text. This is not homograph detection, which stays out of scope: only the characters IDNA maps onto ASCII are folded, and a Cyrillic look-alike is still a genuinely different host./, ? and #. The WHATWG URL Standard makes \ a synonym for / in a special scheme, so a browser reads the host of https://bsky.app\@evil.example.com as bsky.app; that went unflagged while its https://bsky.app\evil.example.com sibling was flagged.// is read as a protocol-relative reference. "bsky.app", <bsky.app>, (bsky.app), //bsky.app and bsky.app, were all unflagged while bsky.app/ and [bsky.app] were flagged.feat: added checkLinkFacade and LinkFacadeVerdict, which report facade, honest, notAUrl or undetermined instead of the single boolean. undetermined is the case a boolean cannot express — the display text reads as a URL but yields no host, it is host-shaped under an internationalized TLD this package carries no data for, or the link itself has no host to compare against. isLinkFacade is unchanged and is now checkLinkFacade(...) == LinkFacadeVerdict.facade.
fix: PostFacet.fromJson refuses malformed JSON instead of leaking a _TypeError out of the render path. A facet with no index, an index that is not an object, a byteStart/byteEnd that is not a whole number, or a features that is not a list now throws a FormatException naming the field; a feature entry that is not an object is skipped, exactly as an unknown $type already was, and a whole-numbered double offset such as 2.0 is accepted. Added PostFacet.tryFromJson, which returns null where fromJson throws, so one bad facet in a fetched post drops out instead of taking the whole render down.
perf: extracting entities is no longer quadratic in the length of a run of dotted labels or of a path of balanced parens. A run with no usable TLD was rescanned once per label, with the TLD alternation — thousands of literals wide — as the constant, and a path was matched against a star over an alternation that retried every prefix. 'a.' * 150, a legal 300-character post, took 43 ms and now takes 0.13 ms; 3000 characters took 11 s and now take 1.3 ms; https://a.com/ followed by 3000 (a) groups took 4.9 s and now takes 0.5 ms. Ordinary text is unaffected in either direction. Extraction is unchanged to the entity: the same facets come out of the existing suite and of a 10,344-entry corpus of real and generated posts, and the skip stands down entirely on any text where it could have changed an answer.
Nothing published for this version
feat: added isLinkFacade, which reports when a facet's display text reads as a URL or host that does not match the host its link points at — the link-
isLinkFacade, which reports when a facet's display text reads as a URL or host that does not match the host its link points at — the link-facade phishing shape. Display text that is not URL-looking is never flagged: a bare host is flagged only when this package would itself have linkified that same text. Hosts match on equality or a subdomain relation, ignoring www., trailing root dots, case, ports, paths and punycode encoding. It does not detect homographs, redirects or deceptive non-URL text, and says so.toDisplayHost, which decodes a punycode (xn--) host into the Unicode it stands for, so a link warning can show what the host actually says.Entity.toFacet, Entities.toFacets, Entities.toFacetsResult and BlueskyText.toPostData are now wire-complete. The facet carries "$type": "app.bsky.richtext.facet" and its index carries "$type": "app.bsky.richtext.facet#byteSlice", matching what the lexicon models serialize to — previously only the individual features were typed, and the returned map did not even pass RichtextFacet.validate. Callers going through feed.post.create never noticed, since the generated converter fills them in; callers assembling a record map themselves, for com.atproto.repo.applyWrites or to compute a record CID locally, silently produced a record that differed from the converter's output. The "no facet" results (an unresolvable handle, a raw markdown link) are still an empty map.- chore: bump xrpc to ^1.1.3.
xrpc to ^1.1.3.chore: bump dev dependency bluesky to ^2.1.0.
bluesky to ^2.1.0.docs: added a runnable inline usage snippet to the README — instantiating BlueskyText, extracting entities (.entities/.handles/.links), and converting
BlueskyText, extracting entities (.entities/.handles/.links), and converting with toPostData({service, resolver}) to text/facets/unresolvedHandles.xrpc to ^1.1.2.fix: email addresses are no longer partially linkified (the domain of mail@alice.bsky.social is no longer turned into a link).
mail@alice.bsky.social is no longer turned into a link).https://ja.wikipedia.org/wiki/日本語 resolve to the full URL.FEAT: Added BlueskyText.overflow, which returns a TextLengthOverflow describing the range of the text that exceeds the post-length limit (more than 30
BlueskyText.overflow, which returns a TextLengthOverflow
describing the range of the text that exceeds the post-length limit (more than
300 graphemes or 3000 UTF-8 bytes), or null when it is within both. The
boundary is reported in UTF-16, UTF-8 byte and grapheme coordinates so a UI
can, for example, split the value with TextLengthOverflow.utf16Start and
render the overflowing tail in red via a Flutter TextSpan.
.format().overflow
reports the overflow of the formatted text (markdown expanded, links
shortened), which is what is displayed and posted.BlueskyText.segments, which partitions the value into
non-overlapping, gap-free TextSegments in document order. Each segment
carries UTF-16 offsets, the entity it belongs to (if any) and whether it lies
in the overflow region, so a Flutter TextEditingController can color links,
handles and tags together with the over-limit tail (for example in red) in a
single pass — without merging the byte-based entity indices and the overflow
range by hand. Concatenating every TextSegment.text reproduces the value,
and no segment is ever split across an entity boundary.renderFacets(text, facets) and PostFacet for displaying a
fetched post: it partitions the text into TextSegments using the
server-provided facets (authoritative mentions/links/tags, mentions already
carrying their DID) instead of re-detecting entities. Each segment exposes a
FacetFeature with the resolved DID / URI / tag, so a Flutter client can
style received posts with one TextSpan builder shared with the compose path.
PostFacet.fromJson parses the app.bsky.richtext.facet API shape, and the
byte→UTF-16 Utf16IndexConverter is now public.Entities.toFacets / the new Entities.toFacetsResult accept a
HandleResolver, so mention DID resolution can be served from a cache or
batched instead of the built-in per-handle network call. toFacetsResult
additionally returns the handles that failed to resolve, so a client can warn
the user rather than silently posting a mention-less message.BlueskyText.formatted (the memoized, posting-ready form) and
BlueskyText.toPostData(...), which formats and resolves facets in one call —
the only correct order, since markdown links become link facets only after
formatting — returning (text, facets, unresolvedHandles).split() now budgets each chunk against both post limits (300
graphemes and 3000 UTF-8 bytes). Previously it budgeted graphemes only, so a
byte-heavy chunk — e.g. many multi-byte ZWJ emoji — could stay under 300
graphemes yet exceed 3000 bytes and still be rejected by the server.split() on a format()ted instance now splits the original text
instead of the lossy formatted value, so format().split() behaves exactly
like split() on the original. Splitting formatted text and re-extracting
previously corrupted facets — a shortened link's uri became its truncated
display text and a markdown link's facet vanished — because the chunks dropped
the position-bound replacements. Each chunk is a raw, independently-formattable
piece; format each one after splitting (e.g. via chunk.toPostData()).split() now breaks on any Unicode whitespace — newlines, tabs
and the ideographic (full-width) space U+3000 — not just the ASCII space.
Previously a multi-line or CJK post with no ASCII spaces was treated as one
giant word and hard-split mid-word (e.g. word44 became wo | rd44). The
author's newlines and spacing are now preserved within each chunk, and no
chunk starts or ends with whitespace. A markdown link is also kept atomic, so
one straddling a chunk boundary is no longer torn open (which would drop its
facet).BlueskyText now lazily memoizes every derived value (length,
handles, entities, overflow, segments, format()…), so touching
several properties of one instance in a Flutter build costs one analysis
instead of one per property (~1.6x faster when touching seven). Note: as a
result BlueskyText is no longer const — const BlueskyText(...) must
become BlueskyText(...).isLengthLimitExceeded and overflow fall back to a cheap grapheme scan when
within the limit, segments resolves the entities only once (instead of
extracting them again via overflow), and the grapheme scan counts UTF-8
bytes without allocating an intermediate byte list. For over-limit text this
cuts isLengthLimitExceeded ~18x and segments ~2x; a 300-grapheme post
segments in well under 0.1 ms.FIX: Fixed crashes on IDN (internationalized domain) URLs. Text containing URLs such as https://日本語.jp or https://日本.example.com no longer throws from
https://日本語.jp or https://日本.example.com no longer throws
from .links / .entities / .format() or markdown-link extraction.@Alice.Bsky.Social and
@SHINYAKATO.DEV are detected.U+3000, line separators, CJK punctuation, and lone surrogate ranges, so
#タグ こんにちは is one tag and #tag3 #tag4 are both preserved.#, and the tag length limit is now
64 graphemes (excluding #), matching the spec.(^|\s|\() boundary
(the leftover twitter-text RT: alternative is removed).# is now recognized as a hashtag sign (partial; #tag1#tag2
splitting is still deferred).split() also propagates the
active format() replacements / link config to each chunk, so shortened
display strings are no longer re-extracted into truncated facet URLs.http(s) scheme detection is case-insensitive and no longer
double-prefixes (HTTPS://EXAMPLE.COM is handled; httpstatus.io is not a
scheme).@handle or #fragment inside a URL no longer produces a
duplicate facet.isEmojiOnly, the shorten threshold, and
toFacet error propagation.toUtf8Index is now incremental (no per-call full re-encode).format() → split(), non-BMP splitting, facet overlap) and de-duplicated
test names. Where existing tests pinned non-official behavior, they were
updated to match the reference implementation.BREAKING: Aligned cashtag detection with Bluesky's official CASHTAG_REGEX in @atproto/api. Detection is now stricter and consistent with the reference
CASHTAG_REGEX
in @atproto/api. Detection is now stricter and consistent with the reference
implementation:
[A-Za-z][A-Za-z0-9]{0,4}); longer candidates like $GOOGLE are rejected.U+3000 / U+00A0), or an ASCII ( — and
followed by a trailing boundary — whitespace, the end of the string, or one
of the ASCII punctuation characters . , ; : ! ? ) " ' or ’ (U+2019).
As a result, cashtags glued to Japanese (or other non-delimiting) text such
as 日本株$AAPL or $AAPLです are intentionally not detected, matching the
official Bluesky behavior. Full-width delimiters like ($AAPL) and $AAPL。
are likewise not treated as boundaries.tag facet keeps the
leading $ (e.g. $aapl → $AAPL), mirroring the official cashtag facet.cashtagBoundary and endCashtag patterns; the
validCashtag pattern now embeds the official leading/trailing boundaries
directly. cashSigns and validCashtag remain exported from
package:bluesky_text/regex.dart.fix: do not use .substring when creating the cashtag entities.
.substring when creating the cashtag entities.FEATURE: Added support for cashtag detection (e.g. $AAPL, $tsla).
$AAPL, $tsla).
BlueskyText.cashtags getter returns all cashtag entities along with
their byte indices.EntityType.cashtag and Entity.isCashtag for type-safe handling.BlueskyText.entities alongside handles,
links, and hashtags.app.bsky.richtext.facet#tag features when
calling toFacets(), mirroring how Bluesky represents tag-like facets.$1000 are not detected as
cashtags.cashSigns, cashtagBoundary, endCashtag, and
validCashtag patterns under package:bluesky_text/regex.dart.FIX: Downgraded characters dependency from ^1.4.1 to ^1.4.0 for compatibility
Added security tests to prevent Unicode normalization attacks and ReDoS vulnerabilities
DEPENDENCY: Updated xrpc dependency to ^1.0.3 for compatibility with at_primitives consolidation.
xrpc dependency to ^1.0.3 for compatibility with at_primitives consolidation.- chore: update example.
Fix SDK constraint to '">=3.8.0 <4.0.0"'.
- chore: optimized docs.
Nothing published for this version
Bump SDK constraint to '^3.8.0'.
Nothing published for this version
- Bump xrpc.
xrpc.Exposed bluesky_text/regex.dart.
bluesky_text/regex.dart.- Bump xrpc.
xrpc.- Bump xrpc.
xrpc.Bump SDK constraint to '^3.3.0'.
- Upgraded xrpc.
xrpc.- Upgraded xrpc.
xrpc.- Upgraded xrpc.
xrpc.- Upgraded xrpc.
xrpc.- Upgraded xrpc.
xrpc.- Upgraded xrpc.
xrpc. (#1112)- Upgraded xrpc.
xrpc. (#999)Improved extraction algo for markdown links.
- Upgraded xrpc.
xrpc. (#989)- Exposed .getGraphemeLength.
.getGraphemeLength.- Upgraded xrpc package.
xrpc package.Exposed .isEmojiOnly as a function.
.isEmojiOnly as a function.Added .isEmojiOnly property. It can determine if the text contains only emojis.
.isEmojiOnly property. It can determine if the text contains only emojis.Supported hashtag with emoji strings.
Supported hashtag with - separated strings.
- separated strings. (#908)Improved markdown extraction algo. You can use as a link if the URL contains markdown symbols, such as https://wikipedia.com//track/We_Up_(Album_Versi
https://wikipedia.com//track/We_Up_(Album_Version_(Edited)).Nothing published for this version
Hashtag formatted text is not allowed as Markdown.
Improved the extraction algo for hashtags.
Mentions cannot be set for markdown text.
Improved handle extraction algorithm. From with this version, the use of spaces as well as URLs is no longer required.
Fixed to add https:// to markdown URLs when it is not given.
https:// to markdown URLs when it is not given.The markdown URL must always contain . symbol.
. symbol.Added enableMarkdown param on BlueskyText. Defaults to true.
enableMarkdown param on BlueskyText. Defaults to true.Fixed a bug regarding byte calculation when detecting markdowns.
Improved entity extraction for unformatted markdown. For example, [test](https://example.com) extracts entities so that test can be highlighted. Facet
[test](https://example.com) extracts entities so that test can be highlighted. Facets of this entity cannot be generated with .toFacets until .format is executed.
EntityType.markdownLink. If you want to exclude entities in the markdown without being .format, you can filter by this fixed value.Added service parameter on .toFacets method.
service parameter on .toFacets method. (#882)Fixed that .format doesn't merge if the URL Path of the detected link is only / when .format is executed.
Supported markdown style links. You can set any links to any text such as [test](https://foo.com). Be sure to execute .format() to make the link in ma
[test](https://foo.com). Be sure to execute .format() to make the link in markdown format recognized as a facet. (#629)Fixed safer processing when shortening links.
Improved link detection algorithm.
Improved algorithm for detecting links.
.hasHandle.hasNotHandle.hasLink.hasNotLink.hasEntity.hasNotEntityint maxGraphemeLength to bool enableShortening on LinkConfig.Supported hashtag detection on .entities and .hashtags.
.entities and .hashtags. (#839)Improved processing when .format() is executed. Correct if the original text link does not contain the http protocol.
.format() is executed. Correct if the original text link does not contain the http protocol.Your coding agent can read these notes before it upgrades. Set up the MCP server →