Documentation
¶
Overview ¶
Package textanchor creates and resolves durable references to spans of text in Markdown documents.
An anchor describes a span by its content and surroundings rather than by a byte offset, so it can be relocated after the document has been edited. Each anchor stores the quoted text, ~50 characters of prefix and suffix context, the containing sentence (for short quotes), and structural context (the nearest preceding heading and the paragraph index within that section).
Resolution finds candidate matches, scores each by context similarity, and returns the best one along with a confidence score. When no candidate scores above the threshold the anchor is reported as orphaned rather than guessed at — callers are expected to surface orphans to the user, not to discard them.
Typical use:
a, err := textanchor.New(document, start, end, nil)
// ... persist a ...
res := textanchor.Resolve(editedDocument, a, nil)
if res.Orphaned {
// show the anchor's Quote as context; do not silently drop it
}
Use ResolveAll to resolve many anchors against one document; it hoists the per-document structural analysis out of the per-anchor loop.
Anchors index the document by BYTE offset, matching Go's string indexing.
The subpackage quotefind maps a selection made over RENDERED markdown back to offsets in the markdown source, which is what a browser text selection needs before an anchor can be created from it.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( ErrInvalidRange = errors.New("invalid range: start must be less than end") ErrOutOfBounds = errors.New("range out of bounds") ErrEmptyQuote = errors.New("quote cannot be empty") )
Sentinel errors for the anchor package.
Functions ¶
This section is empty.
Types ¶
type Anchor ¶
type Anchor struct {
// Quote is the exact text that was selected/annotated.
Quote string `toml:"quote" json:"quote"`
// Prefix is approximately 50 characters of text immediately before
// the quote. Used for disambiguation when the quote appears multiple
// times in the document.
Prefix string `toml:"prefix" json:"prefix"`
// Suffix is approximately 50 characters of text immediately after
// the quote. Used for disambiguation.
Suffix string `toml:"suffix" json:"suffix"`
// ContainingSentence holds the full sentence containing the quote,
// useful for word/phrase-level annotations where the quote itself
// is very short.
ContainingSentence string `toml:"containing_sentence,omitempty" json:"containing_sentence,omitempty"`
// HeadingContext is the text of the nearest preceding heading,
// providing structural context for resolution.
HeadingContext string `toml:"heading_context,omitempty" json:"heading_context,omitempty"`
// ParagraphIndex is the 0-based index of the paragraph within the
// section defined by HeadingContext. -1 if not applicable.
ParagraphIndex int `toml:"paragraph_index,omitempty" json:"paragraph_index,omitempty"`
}
Anchor represents a persistent reference to a span of text in a document. It stores enough context to relocate the text even after the document has been edited.
func New ¶
func New(document string, start, end int, opts *CreateOptions) (Anchor, error)
New creates an anchor for the given text range in a document.
Parameters:
- document: The full text of the Markdown document
- start: Start byte offset of the selection (inclusive)
- end: End byte offset of the selection (exclusive)
- opts: Optional configuration (nil for defaults)
Returns an Anchor that can be serialized and later resolved.
type CreateOptions ¶
type CreateOptions struct {
// PrefixLen is the number of characters to capture before the quote.
// Default: 50
PrefixLen int
// SuffixLen is the number of characters to capture after the quote.
// Default: 50
SuffixLen int
// IncludeContainingSentence controls whether to extract the full
// sentence for short quotes. Default: true for quotes < 50 chars.
IncludeContainingSentence bool
// IncludeStructuralContext controls whether to extract heading
// and paragraph position. Default: true
IncludeStructuralContext bool
}
CreateOptions configures anchor creation.
func DefaultCreateOptions ¶
func DefaultCreateOptions() *CreateOptions
DefaultCreateOptions returns the default options for anchor creation.
type Document ¶ added in v0.3.0
type Document struct {
// contains filtered or unexported fields
}
Document is a document prepared for resolving anchors against it.
Resolving needs several per-document structures: the whitespace-collapsed form, the paragraph table and the heading positions. Building them is linear in the document and independent of the anchor, so a caller resolving many anchors against one document builds a Document once and calls Document.Resolve for each. Resolve and ResolveAll are wrappers that do exactly that.
A Document is immutable after construction and safe for concurrent use.
func NewDocument ¶ added in v0.3.0
NewDocument prepares text for resolving anchors against it.
func (*Document) Resolve ¶ added in v0.3.0
func (d *Document) Resolve(anchor Anchor, opts *ResolveOptions) ResolveResult
Resolve locates anchor in the document. See the package-level Resolve for the algorithm and the meaning of the result.
type Range ¶
type Range struct {
Start int // Inclusive, 0-based byte offset
End int // Exclusive, 0-based byte offset
}
Range represents a character range in a document.
type ResolveOptions ¶
type ResolveOptions struct {
// MinConfidence is the minimum confidence score required to consider
// an anchor resolved. Below this threshold, the anchor is orphaned.
// Default: 0.5
MinConfidence float64
// PreferStructuralMatch gives weight to matches that are in the
// same structural position (same heading, similar paragraph index).
// Default: true
PreferStructuralMatch bool
}
ResolveOptions configures anchor resolution.
func DefaultResolveOptions ¶
func DefaultResolveOptions() *ResolveOptions
DefaultResolveOptions returns the default options for anchor resolution.
type ResolveResult ¶
type ResolveResult struct {
// Range is the located character range. Nil if orphaned.
//
// Matching ignores whitespace differences, so this span may be a different
// length from the anchor's Quote — a quote the document has since rewrapped
// resolves to a span containing the newline. Slice the document with
// Range.Start and Range.End; do not compute the end as Start + len(Quote).
Range *Range
// Confidence is a score from 0.0 to 1.0 indicating how confident
// we are in this match. 1.0 = exact match with perfect context.
Confidence float64
// Orphaned is true if the anchor could not be resolved.
Orphaned bool
// OrphanReason explains why resolution failed, if applicable.
OrphanReason string
}
ResolveResult contains the result of attempting to resolve an anchor.
func Resolve ¶
func Resolve(document string, anchor Anchor, opts *ResolveOptions) ResolveResult
Resolve attempts to locate an anchor in a document.
The resolution algorithm:
- Find all exact matches for anchor.Quote
- If there are none, find fuzzy matches: within one paragraph, and for a quote spanning several paragraphs, across them (see findCrossBlock)
- Score each candidate by prefix/suffix similarity, structural context and uniqueness
- Return the best match above the confidence threshold
If no match meets the threshold, returns Orphaned: true.
To resolve several anchors against one document, use NewDocument once and Document.Resolve per anchor.
func ResolveAll ¶
func ResolveAll(document string, anchors []Anchor, opts *ResolveOptions) []ResolveResult
ResolveAll resolves multiple anchors against one document, preparing the document once.