Documentation
¶
Overview ¶
Package portabletext provides parsing, validation, and manipulation of Portable Text documents.
Portable Text is a JSON-based rich text specification from Sanity.io that represents structured content as an abstract syntax tree (AST). This package provides a complete Go implementation with strong typing, path-aware errors, and flexible validation.
Quick Start ¶
Parse Portable Text from JSON:
input := `[{"_type":"block","children":[{"_type":"span","text":"Hello"}]}]`
doc, err := portabletext.DecodeString(input)
if err != nil {
log.Fatal(err)
}
Access the parsed content:
for _, node := range doc {
if node.IsBlock() {
fmt.Println(node.GetText())
}
}
Build documents programmatically:
block := portabletext.NewBlock("normal").
AddSpan("Hello ", "strong").
AddSpan("world!")
doc := portabletext.Document{*block}
Core Types ¶
The main types are:
- Document: An ordered list of Portable Text nodes
- Node: A block or custom object with type, style, children, and marks
- Span: An inline text element within a block
- MarkDef: A mark definition (e.g., link with href)
All types preserve unknown/custom fields in a Raw map for full round-trip fidelity.
Decoding and Encoding ¶
Decode from io.Reader or string:
doc, err := portabletext.Decode(reader) doc, err := portabletext.DecodeString(jsonString)
Encode to io.Writer or string:
err := portabletext.Encode(writer, doc) jsonString, err := portabletext.EncodeString(doc)
Validation ¶
Basic validation checks for required fields and proper structure:
errs := portabletext.Validate(doc)
for _, err := range errs {
fmt.Println(err)
}
Advanced validation with custom options:
opts := portabletext.ValidationOptions{
RequireKeys: true, // Require _key on blocks
CheckMarkDefRefs: true, // Verify mark references
AllowEmptyText: false, // Disallow empty spans
}
errs := portabletext.ValidateWithOptions(doc, opts)
Traversal ¶
Walk all nodes:
err := portabletext.Walk(doc, func(node *portabletext.Node) error {
fmt.Println(node.Type)
return nil
})
Walk with context information:
portabletext.WalkWithContext(doc, func(n *portabletext.Node, ctx portabletext.WalkContext) error {
fmt.Printf("Node %d: %s\n", ctx.Index, n.Type)
return nil
})
Filtering and Transformation ¶
Filter nodes by predicate:
blocks := portabletext.Filter(doc, func(n *portabletext.Node) bool {
return n.IsBlock()
})
Transform nodes (returns new document):
transformed := portabletext.Transform(doc, func(n *portabletext.Node) *portabletext.Node {
if n.GetStyle() == "h1" {
h2 := "h2"
n.Style = &h2
}
return n
})
Working with Nodes ¶
Node provides convenience methods:
// Check type
if node.IsBlock() { }
// Get values with defaults
style := node.GetStyle() // "normal" if nil
level := node.GetListLevel() // 1 if nil
text := node.GetText() // concatenated span text
// Build fluently
node.AddSpan("text", "strong", "em")
node.AddMarkDef("link1", "link", map[string]any{"href": "https://..."})
// Clone (deep copy)
clone := node.Clone()
Working with Spans ¶
Check for marks:
if span.HasMark("strong") {
fmt.Println("Bold text:", *span.Text)
}
Access custom fields:
if href, ok := markDef.Raw["href"].(string); ok {
fmt.Println("Link:", href)
}
Error Handling ¶
Errors include path information for debugging:
doc, err := portabletext.Decode(reader)
if err != nil {
var pErr *portabletext.Error
if errors.As(err, &pErr) {
// pErr.Path shows where the error occurred
// e.g., "[2].children[1].marks"
fmt.Printf("Error at %s: %v\n", pErr.Path, pErr.Err)
}
}
Validation errors provide structured information:
for _, err := range errs {
if ve, ok := err.(*portabletext.ValidationError); ok {
fmt.Printf("%s: %s\n", ve.Path, ve.Message)
// ve.Node provides reference to the problematic node
}
}
Custom Fields ¶
Unknown fields are preserved in Raw maps:
node.Raw["customField"] = "value" span.Raw["customAttr"] = 123 markDef.Raw["target"] = "_blank"
These fields are included when encoding back to JSON.
Thread Safety ¶
Documents are safe for concurrent reads without synchronization. Concurrent writes require external synchronization. Filter() and Transform() create new documents and are safe to call concurrently.
Examples ¶
Extract all text:
func ExtractText(doc portabletext.Document) string {
var buf strings.Builder
for _, node := range doc {
if node.IsBlock() {
buf.WriteString(node.GetText())
buf.WriteString("\n")
}
}
return buf.String()
}
Find all links:
func FindLinks(doc portabletext.Document) []string {
var links []string
for _, node := range doc {
for _, md := range node.MarkDefs {
if md.Type == "link" {
if href, ok := md.Raw["href"].(string); ok {
links = append(links, href)
}
}
}
}
return links
}
Generate table of contents:
func GenerateTOC(doc portabletext.Document) []string {
var toc []string
portabletext.Walk(doc, func(n *portabletext.Node) error {
style := n.GetStyle()
if style == "h1" || style == "h2" {
toc = append(toc, n.GetText())
}
return nil
})
return toc
}
Specification ¶
This package implements the Portable Text specification: https://github.com/portabletext/portabletext
Key concepts:
- Block: Top-level content node (paragraph, heading, list item, etc.)
- Span: Inline text within a block, optionally with marks
- Mark: Text annotation (bold, italic, link, etc.)
- MarkDef: Mark definition with additional data (e.g., link href)
- Custom Objects: Extensible beyond standard types
Index ¶
- Variables
- func Encode(w io.Writer, doc Document) error
- func EncodeString(doc Document) (string, error)
- func Validate(doc Document) []error
- func ValidateWithOptions(doc Document, opts ValidationOptions) []error
- func Walk(doc Document, fn func(*Node) error) error
- func WalkWithContext(doc Document, fn func(*Node, WalkContext) error) error
- type Document
- type Error
- type MarkDef
- type Node
- func (n *Node) AddMarkDef(key, markType string, raw map[string]any) *Node
- func (n *Node) AddSpan(text string, marks ...string) *Node
- func (n *Node) Clone() *Node
- func (n *Node) GetListLevel() int
- func (n *Node) GetStyle() string
- func (n *Node) GetText() string
- func (n *Node) IsBlock() bool
- func (n Node) MarshalJSON() ([]byte, error)
- type Span
- type ValidationError
- type ValidationOptions
- type WalkContext
Constants ¶
This section is empty.
Variables ¶
var ( ErrMissingType = errors.New("missing _type") ErrInvalidType = errors.New("invalid _type") ErrExpectedObject = errors.New("expected JSON object") ErrExpectedArray = errors.New("expected JSON array") ErrInvalidMarks = errors.New("marks must be an array of strings") ErrInvalidNumber = errors.New("invalid number") ErrUnexpectedToken = errors.New("unexpected JSON token") )
Functions ¶
func Encode ¶
Encode serializes the AST back to JSON. - Re-emits all known and unknown fields - Does not mutate the input document
func EncodeString ¶ added in v0.1.1
EncodeString is a convenience wrapper for Encode.
func ValidateWithOptions ¶ added in v0.1.1
func ValidateWithOptions(doc Document, opts ValidationOptions) []error
ValidateWithOptions performs validation with custom options.
func WalkWithContext ¶ added in v0.1.1
func WalkWithContext(doc Document, fn func(*Node, WalkContext) error) error
WalkWithContext visits all top-level nodes with additional context.
Types ¶
type Document ¶
type Document []Node
Document is an ordered list of Portable Text nodes. Document is not safe for concurrent modification. For concurrent reads, no synchronization is needed. For concurrent writes, external synchronization is required.
func Decode ¶
Decode parses JSON Portable Text into a Document. - Requires _type on all nodes and child spans/markDefs where present - Captures unknown fields into Raw (including explicit nulls) - Does not normalize or semantically validate
func DecodeString ¶ added in v0.1.1
DecodeString is a convenience wrapper for Decode.
type Error ¶
type MarkDef ¶
type MarkDef struct {
Key string `json:"_key"`
Type string `json:"_type"`
Raw map[string]any `json:"-"`
}
MarkDef represents an annotation definition (e.g. link objects).
func (MarkDef) MarshalJSON ¶
type Node ¶
type Node struct {
// Required
Type string `json:"_type"`
Key string `json:"_key,omitempty"`
// Common block fields
Style *string `json:"style,omitempty"`
Children []Span `json:"children,omitempty"`
MarkDefs []MarkDef `json:"markDefs,omitempty"`
// List-related fields
ListItem *string `json:"listItem,omitempty"`
Level *int `json:"level,omitempty"`
// Raw holds unknown/custom fields and preserves explicit nulls.
Raw map[string]any `json:"-"`
}
Node represents a Portable Text node (block or custom object). Known fields are modeled; unknown/custom fields are preserved in Raw.
func (*Node) AddMarkDef ¶ added in v0.1.1
AddMarkDef adds a mark definition to a block node.
func (*Node) GetListLevel ¶ added in v0.1.1
GetListLevel returns the list level or 1 if not set.
func (Node) MarshalJSON ¶
type Span ¶
type Span struct {
Type string `json:"_type"`
Text *string `json:"text,omitempty"`
Marks []string `json:"marks,omitempty"`
Raw map[string]any `json:"-"`
}
Span represents an inline node in a block's children array. Usually _type == "span", but inline objects are allowed too. For inline objects, Text is typically nil and Raw holds object fields.
func (Span) MarshalJSON ¶
type ValidationError ¶ added in v0.1.1
type ValidationError struct {
Path string
Message string
Node *Node // Optional reference to problematic node
}
ValidationError represents a validation error with context.
func (*ValidationError) Error ¶ added in v0.1.1
func (e *ValidationError) Error() string
type ValidationOptions ¶ added in v0.1.1
type ValidationOptions struct {
RequireKeys bool // Require _key on all blocks
CheckMarkDefRefs bool // Verify mark references exist in markDefs
AllowEmptyText bool // Allow empty text in spans
}
ValidationOptions controls what Validate checks.
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
01-extract-text
command
|
|
|
02-find-links
command
|
|
|
03-build-document
command
|
|
|
04-transform-headings
command
|
|
|
05-table-of-contents
command
|