bluesky_text
Provides the easiest and most powerful way to analyze the text for Bluesky Social.
1.8.0
3.7K downloads/mo
#4278 most downloaded on pub.dev
myConsciousness/atproto.dart
What this package is like to depend on
Last release 15 days ago
08 Aug 2026
Ships unpredictably
gaps range from 8 days to 8 months
Nearly every release is documented
notes for 74 of 75 stable releases
1 version withdrawn
withdrawn after publishing
3 years old
78 releases · first in 2023
15 releases in the last 12 months
see the full history below
Release timeline
78 releases · May 2023 to Aug 2026Releases
latest 60 of 78-
1.8.008 Aug 2026Release notes
Open source →-
feat:
Entity.toFacet,Entities.toFacetsResultandEntities.toFacetsaccept an optionalhttp.Client? client, forwarded to the built-in handle-resolution call. It is used only when noresolveris 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.socialand asserted the returned DID, so they broke on any outage, rate limit, or handle change and could not run offline. They now inject aMockClient(via the newclienthook) that answersresolveHandlelocally, covering the success path, the 4xx "unresolvable handle → no facet" swallow, the 5xx "surface the error" rethrow, and thatservicereaches the request. One meaningless "does not throw" assertion was replaced with a real output check. -
security:
isLinkFacadeno longer passes a display text it cannot parse. It used to returnfalse— "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.- Display text is now folded the way IDNA (UTS-46) folds it before comparison: the three full-stop variants (
。U+3002,.U+FF0E,。U+FF61) become., and the fullwidth ASCII block (U+FF01–U+FF5E) becomes plain ASCII.bsky。appandbsky.appare hosts a browser really resolves tobsky.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. - The authority now ends at a backslash as well as at
/,?and#. The WHATWG URL Standard makes\a synonym for/in a special scheme, so a browser reads the host ofhttps://bsky.app\@evil.example.comasbsky.app; that went unflagged while itshttps://bsky.app\evil.example.comsibling was flagged. - Wrappers and sentence punctuation are stripped from both ends, and a leading
//is read as a protocol-relative reference."bsky.app",<bsky.app>,(bsky.app),//bsky.appandbsky.app,were all unflagged whilebsky.app/and[bsky.app]were flagged.
- Display text is now folded the way IDNA (UTS-46) folds it before comparison: the three full-stop variants (
-
feat: added
checkLinkFacadeandLinkFacadeVerdict, which reportfacade,honest,notAUrlorundeterminedinstead of the single boolean.undeterminedis 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.isLinkFacadeis unchanged and is nowcheckLinkFacade(...) == LinkFacadeVerdict.facade. -
fix:
PostFacet.fromJsonrefuses malformed JSON instead of leaking a_TypeErrorout of the render path. A facet with noindex, anindexthat is not an object, abyteStart/byteEndthat is not a whole number, or afeaturesthat is not a list now throws aFormatExceptionnaming the field; a feature entry that is not an object is skipped, exactly as an unknown$typealready was, and a whole-numbereddoubleoffset such as2.0is accepted. AddedPostFacet.tryFromJson, which returnsnullwherefromJsonthrows, 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.
-
-
1.7.026 Jul 2026Nothing published for this version
-
1.6.026 Jul 2026Release notes
Open source →- 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-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, ignoringwww., trailing root dots, case, ports, paths and punycode encoding. It does not detect homographs, redirects or deceptive non-URL text, and says so. - feat: added
toDisplayHost, which decodes a punycode (xn--) host into the Unicode it stands for, so a link warning can show what the host actually says. - fix: the facet maps returned by
Entity.toFacet,Entities.toFacets,Entities.toFacetsResultandBlueskyText.toPostDataare now wire-complete. The facet carries"$type": "app.bsky.richtext.facet"and itsindexcarries"$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 passRichtextFacet.validate. Callers going throughfeed.post.createnever noticed, since the generated converter fills them in; callers assembling a record map themselves, forcom.atproto.repo.applyWritesor 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.
- feat: added
-
1.5.422 Jul 2026 -
1.5.318 Jul 2026 -
1.5.216 Jul 2026Release notes
Open source →- docs: added a runnable inline usage snippet to the README — instantiating
BlueskyText, extracting entities (.entities/.handles/.links), and converting withtoPostData({service, resolver})totext/facets/unresolvedHandles. - chore: bump
xrpcto^1.1.2.
- docs: added a runnable inline usage snippet to the README — instantiating
-
1.5.116 Jul 2026Release notes
Open source →- fix: email addresses are no longer partially linkified (the domain of
[email protected]is no longer turned into a link). - fix: URL paths containing non-ASCII characters are no longer truncated, so links such as
https://ja.wikipedia.org/wiki/日本語resolve to the full URL.
- fix: email addresses are no longer partially linkified (the domain of
-
1.5.013 Jul 2026Release notes
Open source →- FEAT: Added
BlueskyText.overflow, which returns aTextLengthOverflowdescribing the range of the text that exceeds the post-length limit (more than 300 graphemes or 3000 UTF-8 bytes), ornullwhen 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 withTextLengthOverflow.utf16Startand render the overflowing tail in red via a FlutterTextSpan.- The boundary always lands on a grapheme cluster boundary, so emoji and other multi-code-unit characters are never split.
- When the boundary would fall inside an entity (handle, link, tag, cashtag or markdown link) it is snapped back to that entity's start, so the entity is treated atomically — wholly within the limit or wholly in the overflow.
- Because the range is derived from the value, calling
.format().overflowreports the overflow of the formatted text (markdown expanded, links shortened), which is what is displayed and posted.
- FEAT: Added
BlueskyText.segments, which partitions the value into non-overlapping, gap-freeTextSegments 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 FlutterTextEditingControllercan 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 everyTextSegment.textreproduces the value, and no segment is ever split across an entity boundary. - FEAT: Added
renderFacets(text, facets)andPostFacetfor displaying a fetched post: it partitions the text intoTextSegments using the server-provided facets (authoritative mentions/links/tags, mentions already carrying their DID) instead of re-detecting entities. Each segment exposes aFacetFeaturewith the resolved DID / URI / tag, so a Flutter client can style received posts with oneTextSpanbuilder shared with the compose path.PostFacet.fromJsonparses theapp.bsky.richtext.facetAPI shape, and the byte→UTF-16Utf16IndexConverteris now public. - FEAT:
Entities.toFacets/ the newEntities.toFacetsResultaccept aHandleResolver, so mention DID resolution can be served from a cache or batched instead of the built-in per-handle network call.toFacetsResultadditionally returns the handles that failed to resolve, so a client can warn the user rather than silently posting a mention-less message. - FEAT: Added
BlueskyText.formatted(the memoized, posting-ready form) andBlueskyText.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). - FIX:
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. - FIX:
split()on aformat()ted instance now splits the original text instead of the lossy formatted value, soformat().split()behaves exactly likesplit()on the original. Splitting formatted text and re-extracting previously corrupted facets — a shortened link'suribecame 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. viachunk.toPostData()). - FIX:
split()now breaks on any Unicode whitespace — newlines, tabs and the ideographic (full-width) spaceU+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.word44becamewo|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). - PERF:
BlueskyTextnow lazily memoizes every derived value (length,handles,entities,overflow,segments,format()…), so touching several properties of one instance in a Flutterbuildcosts one analysis instead of one per property (~1.6x faster when touching seven). Note: as a resultBlueskyTextis no longerconst—const BlueskyText(...)must becomeBlueskyText(...). - PERF: The length-limit hot paths (polled on every keystroke in a Flutter
editor) avoid the regex-based entity extraction entirely unless it is needed.
isLengthLimitExceededandoverflowfall back to a cheap grapheme scan when within the limit,segmentsresolves the entities only once (instead of extracting them again viaoverflow), and the grapheme scan counts UTF-8 bytes without allocating an intermediate byte list. For over-limit text this cutsisLengthLimitExceeded~18x andsegments~2x; a 300-grapheme post segments in well under 0.1 ms.
- FEAT: Added
-
1.4.013 Jul 2026Release notes
Open source →- FIX: Fixed crashes on IDN (internationalized domain) URLs. Text containing
URLs such as
https://日本語.jporhttps://日本.example.comno longer throws from.links/.entities/.format()or markdown-link extraction. - BREAKING: Aligned several detectors with Bluesky's official implementation:
- The mention regex is now case-insensitive, so
@Alice.Bsky.Socialand@SHINYAKATO.DEVare detected. - The hashtag "emoji" character class was rewritten to drop whitespace,
U+3000, line separators, CJK punctuation, and lone surrogate ranges, so#タグ こんにちはis one tag and#tag3 #tag4are both preserved. - Tag facet values strip a single leading
#, and the tag length limit is now 64 graphemes (excluding#), matching the spec. - Mention preceding-character rules follow the official
(^|\s|\()boundary (the leftover twitter-textRT:alternative is removed). - Full-width
#is now recognized as a hashtag sign (partial;#tag1#tag2splitting is still deferred).
- The mention regex is now case-insensitive, so
- FIX: The chunk splitter now budgets by grapheme count instead of UTF-16
length, so emoji-heavy text is packed correctly.
split()also propagates the activeformat()replacements / link config to each chunk, so shortened display strings are no longer re-extracted into truncated facet URLs. - FIX:
http(s)scheme detection is case-insensitive and no longer double-prefixes (HTTPS://EXAMPLE.COMis handled;httpstatus.iois not a scheme). - FIX: Overlapping facets are resolved by priority (link > mention > tag >
cashtag), so an
@handleor#fragmentinside a URL no longer produces a duplicate facet. - FIX: Enforce the lexicon's 3000 UTF-8 byte limit alongside the 300
grapheme limit; misc fixes to
isEmojiOnly, the shorten threshold, andtoFaceterror propagation. - PERF:
toUtf8Indexis now incremental (no per-call full re-encode). - TEST: Added a WS-6 regression suite (IDN input, upper-case TLD/scheme,
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.
- FIX: Fixed crashes on IDN (internationalized domain) URLs. Text containing
URLs such as
-
1.3.009 Jul 2026Release notes
Open source →- BREAKING: Aligned cashtag detection with Bluesky's official
CASHTAG_REGEXin@atproto/api. Detection is now stricter and consistent with the reference implementation:- The ticker symbol is limited to 1–5 ASCII characters
(
[A-Za-z][A-Za-z0-9]{0,4}); longer candidates like$GOOGLEare rejected. - A cashtag must be preceded by a leading boundary — the start of the string,
a whitespace character (including
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日本株$AAPLor$AAPLですare intentionally not detected, matching the official Bluesky behavior. Full-width delimiters like($AAPL)and$AAPL。are likewise not treated as boundaries. - The ticker is normalized to upper case and the emitted
tagfacet keeps the leading$(e.g.$aapl→$AAPL), mirroring the official cashtag facet.
- The ticker symbol is limited to 1–5 ASCII characters
(
- REGEX: Removed the
cashtagBoundaryandendCashtagpatterns; thevalidCashtagpattern now embeds the official leading/trailing boundaries directly.cashSignsandvalidCashtagremain exported frompackage:bluesky_text/regex.dart. - TEST: Updated and expanded the cashtag test suite to pin the official-compliant boundaries, ticker length limit, upper-case normalization, and Japanese-adjacency behavior.
- BREAKING: Aligned cashtag detection with Bluesky's official
-
1.2.121 May 2026 -
1.2.021 May 2026Release notes
Open source →- FEATURE: Added support for cashtag detection (e.g.
$AAPL,$tsla).- New
BlueskyText.cashtagsgetter returns all cashtag entities along with their byte indices. - New
EntityType.cashtagandEntity.isCashtagfor type-safe handling. - Cashtags are also surfaced from
BlueskyText.entitiesalongside handles, links, and hashtags. - Cashtags are converted to
app.bsky.richtext.facet#tagfeatures when callingtoFacets(), mirroring how Bluesky represents tag-like facets. - Symbols must start with an ASCII letter and may contain ASCII letters or
digits afterwards. Bare dollar amounts like
$1000are not detected as cashtags.
- New
- REGEX: Added
cashSigns,cashtagBoundary,endCashtag, andvalidCashtagpatterns underpackage:bluesky_text/regex.dart. - TEST: Added 30+ test cases covering cashtag extraction, boundary detection, length limits, byte index accuracy, and interoperability with hashtags, handles, and links.
- FEATURE: Added support for cashtag detection (e.g.
-
1.1.130 Sep 2025Release notes
Open source →- FIX: Downgraded characters dependency from ^1.4.1 to ^1.4.0 for compatibility
-
1.1.027 Sep 2025Release notes
Open source →- FEATURE: Added support for Unicode space characters as hashtag delimiters. (#1933)
- Full-width space (U+3000) and other Unicode space characters are now recognized as valid hashtag boundaries
- Improved compatibility with Bluesky's official client behavior
- Enhanced hashtag recognition for international users, especially Japanese users
- ENHANCEMENT: Comprehensive test coverage expansion
- Added extensive tests for hashtags, handles, and links with real-world scenarios
- Added security tests to prevent Unicode normalization attacks and ReDoS vulnerabilities
- Added performance tests with large numbers of entities
- Added multilingual support tests for various languages
- Added boundary detection tests for edge cases
- IMPROVEMENT: Enhanced hashtag boundary detection with support for:
- Ideographic space (U+3000) - commonly used in Japanese text
- No-break space (U+00A0) - commonly used in HTML
- Regular space (U+0020) - standard ASCII space
- TEST: Added 22+ new comprehensive test cases covering edge cases and real-world usage patterns
- FEATURE: Added support for Unicode space characters as hashtag delimiters. (#1933)
-
1.0.425 Sep 2025Release notes
Open source →- DEPENDENCY: Updated
xrpcdependency to^1.0.3for compatibility withat_primitivesconsolidation.
- DEPENDENCY: Updated
-
1.0.306 Aug 2025 -
1.0.230 Jul 2025 -
1.0.128 Jul 2025 -
1.0.1-legacy28 Jul 2025 pre-releaseNothing published for this version
-
1.0.018 Jul 2025 -
1.0.0-legacy24 Jul 2025 pre-releaseNothing published for this version
-
0.7.207 Dec 2024 -
0.7.120 Nov 2024 -
0.7.016 Nov 2024 -
0.6.1004 Nov 2024 -
0.6.922 Feb 2024 -
0.6.818 Feb 2024 -
0.6.727 Jan 2024 -
0.6.630 Dec 2023 -
0.6.529 Dec 2023 -
0.6.421 Dec 2023 -
0.6.319 Dec 2023 -
0.6.215 Nov 2023 -
0.6.113 Nov 2023 -
0.6.009 Nov 2023 -
0.5.2101 Nov 2023 -
0.5.2031 Oct 2023 -
0.5.1928 Oct 2023 -
0.5.1828 Oct 2023Release notes
Open source →- Added
.isEmojiOnlyproperty. It can determine if the text contains only emojis.
- Added
-
0.5.1726 Oct 2023 -
0.5.1625 Oct 2023 -
0.5.1521 Oct 2023Release notes
Open source →- 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_Version_(Edited)).
- Improved markdown extraction algo. You can use as a link if the URL contains markdown symbols, such as
-
0.5.15-preview.017 Oct 2023 pre-release withdrawnNothing published for this version
-
0.5.1412 Oct 2023Release notes
Open source →- Hashtag formatted text is not allowed as Markdown.
- Improved the extraction algo for markdown. (#901)
-
0.5.1311 Oct 2023 -
0.5.1208 Oct 2023Release notes
Open source → -
0.5.1107 Oct 2023Release notes
Open source →- Improved handle extraction algorithm. From with this version, the use of spaces as well as URLs is no longer required. (#892)
-
0.5.1007 Oct 2023 -
0.5.907 Oct 2023 -
0.5.807 Oct 2023 -
0.5.707 Oct 2023 -
0.5.607 Oct 2023Release notes
Open source →- Improved entity extraction for unformatted markdown. For example,
[test](https://example.com)extracts entities so thattestcan be highlighted. Facets of this entity cannot be generated with.toFacetsuntil.formatis executed.- Also added
EntityType.markdownLink. If you want to exclude entities in the markdown without being.format, you can filter by this fixed value.
- Also added
- Improved entity extraction for unformatted markdown. For example,
-
0.5.504 Oct 2023 -
0.5.404 Oct 2023 -
0.5.302 Oct 2023Release notes
Open source →- 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 markdown format recognized as a facet. (#629)
- Supported markdown style links. You can set any links to any text such as
-
0.5.201 Oct 2023 -
0.5.130 Sep 2023 -
0.5.028 Sep 2023Release notes
Open source →- Improved algorithm for detecting links.
- Removed deprecated properties.
.hasHandle.hasNotHandle.hasLink.hasNotLink.hasEntity.hasNotEntity
- Changed property from
int maxGraphemeLengthtobool enableShorteningonLinkConfig.
-
0.4.526 Sep 2023 -
0.4.425 Sep 2023Release notes
Open source →- Improved processing when
.format()is executed. Correct if the original text link does not contain thehttpprotocol.
- Improved processing when