@portabletext/toolkit

Utility functions for converting Portable Text blocks into plain text, nested marks, and HTML-ready list trees.

Library
npm
v6.0.0
14stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
46/100Fair
Development Activity56
Maintenance40
Community20
Maturity56
Momentum12

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
77/100Good
Architecture78
Code Quality90
Innovation85
Learning Curve55

@portabletext/toolkit is a small, dependency-light set of utility functions for working with Portable Text, the rich-text JSON format used by Sanity and other structured-content tools. Rather than rendering anything itself, it does the unglamorous transformation work that rendering libraries need: extracting plain text from blocks, nesting overlapping marks (bold, links, custom annotations) into a proper tree instead of a flat span list, and turning Portable Text’s flat, level-based list items into nested list structures suitable for HTML or React output.

It’s built as the shared foundation underneath higher-level Portable Text renderers (such as react-portabletext and to-html implementations), so consumers rarely need to think about span ordering or list-level bookkeeping themselves — they call a handful of pure, well-typed functions and get back a tree that’s ready to walk and render.

What You Get

  • toPlainText() to strip a Portable Text block or array of blocks down to plain text, correctly handling whitespace around inline non-span nodes
  • buildMarksTree() to convert a block’s flat span list into a nested tree so overlapping marks (bold, links, custom annotations) render without duplicate wrapper elements
  • nestLists() to turn Portable Text’s flat, level-tagged list items into properly nested list trees, in either ‘html’ or ‘direct’ nesting mode
  • sortMarksByOccurences() to compute the optimal mark ordering that minimizes nested/repeated elements when rendering overlapping marks
  • A set of strict and loose type-guard functions (isPortableTextBlock, isPortableTextSpan, isPortableTextListItemBlock, etc.) for safely narrowing arbitrary typed objects
  • Fully typed toolkit-specific node types (ToolkitNestedPortableTextSpan, ToolkitPortableTextList, ToolkitTextNode) that model the intermediate trees these functions produce

Common Use Cases

  • Building a custom Portable Text-to-HTML or Portable Text-to-React renderer that needs correctly nested marks and lists
  • Extracting plain-text excerpts or search-index content from rich-text fields stored as Portable Text
  • Implementing a new output target (PDF, plain email, Slack blocks) for content authored in Portable Text
  • Writing custom serializers on top of react-portabletext or similar libraries that need lower-level access to the mark/list tree

Under The Hood

Architecture The package is a flat, single-purpose utility library — src/index.ts re-exports seven modules (asserters, buildMarksTree, nestLists, sortMarksByOccurences, spanToPlainText, toPlainText, types) with no internal layering. Each export is a pure function operating on plain JS objects passed in as arguments, with no shared mutable state, no classes, and no I/O. buildMarksTree.ts depends on asserters.ts and sortMarksByOccurences.ts to walk a block’s children and produce a nested tree of toolkit-specific span and text nodes; nestLists.ts is self-contained, building nested list trees through recursive helpers that clone rather than mutate the nodes they’re given. Because every function is a pure input-to-output transform, the abstraction is easy to reason about in isolation, and swapping the internal tree-building algorithm would only affect callers that inspect the toolkit-specific node shapes directly — chiefly the rendering libraries built on top of it.

Tech Stack Written in TypeScript, built with tsdown (a Rolldown-based bundler) into ESM-only output, with a single runtime dependency on @portabletext/types — the shared type definitions used across the Portable Text ecosystem. Testing runs on Vitest with v8 coverage, linting on oxlint in type-aware mode, and formatting on oxfmt (the Rust-based Oxc toolchain). Documentation is generated from TSDoc comments via TypeDoc and published to GitHub Pages on release, with releases themselves automated through Changesets. There’s no runtime framework, database, or server component — this is a pure library consumed by other npm packages.

Code Quality Each of the six exported modules has a dedicated test file mirroring it one-to-one, plus snapshot tests for the more structurally complex outputs (mark trees, nested lists), giving thorough coverage of edge cases like lists that change level or style mid-sequence and blocks containing non-span inline objects. All source is strictly typed, with generics used deliberately — buildMarksTree and nestLists both carry type parameters and, in nestLists’ case, function overloads that give callers a precisely-typed return value based on the nesting mode argument. Runtime type guards in asserters.ts avoid trusting arbitrary input blindly, and CI runs oxlint with type-checking and warnings-as-errors enabled. The single console.warn in nestLists.ts is a deliberate fallback for an unreachable state, not a silently swallowed error path.

API Design The public surface is a small set of named, single-purpose functions that need no setup, client instantiation, or configuration object — each is called directly with the Portable Text data. Every export carries detailed TSDoc comments explaining exact behavior and edge cases, which double as the source for the published documentation site. Naming is consistent and descriptive of the exact transform performed (toPlainText, buildMarksTree, nestLists), and nestLists’ overloaded signatures return correctly-typed results depending on the chosen nesting mode. Because the exports are pure functions with no side effects, integrating the toolkit into any rendering approach — React, string templating, or otherwise — is straightforward, and the library deliberately stays a low-level building block rather than trying to be a complete rendering solution itself.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search