cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
29
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 439 lines 12 kB view raw
1// Package public defines leaflet publication schema types 2// 3// These types correspond to the pub.leaflet.* lexicons used by leaflet.pub 4// 5// The types here match the lexicon definitions from: 6// 7// https://github.com/hyperlink-academy/leaflet/tree/main/lexicons/pub/leaflet/ 8package public 9 10import ( 11 "encoding/json" 12 "time" 13) 14 15const ( 16 TypeDocument = "pub.leaflet.document" 17 TypeDocumentDraft = "pub.leaflet.document.draft" 18 TypePublication = "pub.leaflet.publication" 19 TypeLinearDocument = "pub.leaflet.pages.linearDocument" 20 TypeBlock = "pub.leaflet.pages.linearDocument#block" 21 22 TypeTextBlock = "pub.leaflet.blocks.text" 23 TypeHeaderBlock = "pub.leaflet.blocks.header" 24 TypeCodeBlock = "pub.leaflet.blocks.code" 25 TypeImageBlock = "pub.leaflet.blocks.image" 26 TypeBlockquoteBlock = "pub.leaflet.blocks.blockquote" 27 TypeUnorderedListBlock = "pub.leaflet.blocks.unorderedList" 28 TypeHorizontalRuleBlock = "pub.leaflet.blocks.horizontalRule" 29 30 TypeFacet = "pub.leaflet.richtext.facet" 31 TypeByteSlice = "pub.leaflet.richtext.facet#byteSlice" 32 TypeFacetBold = "pub.leaflet.richtext.facet#bold" 33 TypeFacetItalic = "pub.leaflet.richtext.facet#italic" 34 TypeFacetCode = "pub.leaflet.richtext.facet#code" 35 TypeFacetLink = "pub.leaflet.richtext.facet#link" 36 TypeFacetStrike = "pub.leaflet.richtext.facet#strikethrough" 37 TypeFacetUnderline = "pub.leaflet.richtext.facet#underline" 38 TypeFacetHighlight = "pub.leaflet.richtext.facet#highlight" 39 40 TypeListItem = "pub.leaflet.blocks.unorderedList#listItem" 41 TypeAspectRatio = "pub.leaflet.blocks.image#aspectRatio" 42 TypeBlob = "blob" 43) 44 45// Document represents a leaflet document (pub.leaflet.document) 46type Document struct { 47 Type string `json:"$type"` 48 Author string `json:"author"` // DID (Decentralized Identifier) 49 Title string `json:"title"` // Max 128 graphemes 50 Description string `json:"description"` // Max 300 graphemes 51 PublishedAt string `json:"publishedAt"` // ISO8601 datetime 52 Publication string `json:"publication"` // URI: at://did/pub.leaflet.publication/rkey 53 Pages []LinearDocument `json:"pages"` 54} 55 56// LinearDocument represents a page in a leaflet document (pub.leaflet.pages.linearDocument) 57type LinearDocument struct { 58 Type string `json:"$type"` 59 ID string `json:"id,omitempty"` 60 Blocks []BlockWrap `json:"blocks"` 61} 62 63// BlockWrap wraps a block with optional metadata (alignment, etc.) 64type BlockWrap struct { 65 Type string `json:"$type"` 66 Block any `json:"block"` // One of: TextBlock, HeaderBlock, etc. 67 Alignment string `json:"alignment,omitempty"` // #textAlignLeft, etc. 68} 69 70type TypeCheck struct { 71 Type string `json:"$type"` 72} 73 74// UnmarshalJSON custom unmarshaler for BlockWrap to properly type the Block field 75// 76// Matches against field $type to deserialize data 77func (bw *BlockWrap) UnmarshalJSON(data []byte) error { 78 type Alias BlockWrap 79 temp := &struct { 80 Block json.RawMessage `json:"block"` 81 *Alias 82 }{ 83 Alias: (*Alias)(bw), 84 } 85 86 if err := json.Unmarshal(data, temp); err != nil { 87 return err 88 } 89 90 var typeCheck struct { 91 Type string `json:"$type"` 92 } 93 if err := json.Unmarshal(temp.Block, &typeCheck); err != nil { 94 return err 95 } 96 97 switch typeCheck.Type { 98 case TypeTextBlock: 99 var block TextBlock 100 if err := json.Unmarshal(temp.Block, &block); err != nil { 101 return err 102 } 103 bw.Block = block 104 case TypeHeaderBlock: 105 var block HeaderBlock 106 if err := json.Unmarshal(temp.Block, &block); err != nil { 107 return err 108 } 109 bw.Block = block 110 case TypeCodeBlock: 111 var block CodeBlock 112 if err := json.Unmarshal(temp.Block, &block); err != nil { 113 return err 114 } 115 bw.Block = block 116 case TypeImageBlock: 117 var block ImageBlock 118 if err := json.Unmarshal(temp.Block, &block); err != nil { 119 return err 120 } 121 bw.Block = block 122 case TypeBlockquoteBlock: 123 var block BlockquoteBlock 124 if err := json.Unmarshal(temp.Block, &block); err != nil { 125 return err 126 } 127 bw.Block = block 128 case TypeUnorderedListBlock: 129 var block UnorderedListBlock 130 if err := json.Unmarshal(temp.Block, &block); err != nil { 131 return err 132 } 133 bw.Block = block 134 case TypeHorizontalRuleBlock: 135 var block HorizontalRuleBlock 136 if err := json.Unmarshal(temp.Block, &block); err != nil { 137 return err 138 } 139 bw.Block = block 140 default: 141 var block map[string]any 142 if err := json.Unmarshal(temp.Block, &block); err != nil { 143 return err 144 } 145 bw.Block = block 146 } 147 148 return nil 149} 150 151// TextBlock represents a text content block (pub.leaflet.blocks.text) 152type TextBlock struct { 153 Type string `json:"$type"` 154 Plaintext string `json:"plaintext"` 155 Facets []Facet `json:"facets,omitempty"` 156} 157 158// HeaderBlock represents a heading content block (pub.leaflet.blocks.header) 159type HeaderBlock struct { 160 Type string `json:"$type"` 161 Level int `json:"level,omitempty"` // h1 - h6 162 Plaintext string `json:"plaintext"` 163 Facets []Facet `json:"facets,omitempty"` 164} 165 166// CodeBlock represents a code content block (pub.leaflet.blocks.code) 167type CodeBlock struct { 168 Type string `json:"$type"` 169 Plaintext string `json:"plaintext"` 170 Language string `json:"language,omitempty"` 171 SyntaxHighlightingTheme string `json:"syntaxHighlightingTheme,omitempty"` 172} 173 174// ImageBlock represents an image content block (pub.leaflet.blocks.image) 175type ImageBlock struct { 176 Type string `json:"$type"` 177 Image Blob `json:"image"` 178 Alt string `json:"alt,omitempty"` 179 AspectRatio AspectRatio `json:"aspectRatio"` 180} 181 182// AspectRatio represents image dimensions (pub.leaflet.blocks.image#aspectRatio) 183type AspectRatio struct { 184 Type string `json:"$type"` 185 Width int `json:"width"` 186 Height int `json:"height"` 187} 188 189// BlockquoteBlock represents a blockquote content block (pub.leaflet.blocks.blockquote) 190type BlockquoteBlock struct { 191 Type string `json:"$type"` 192 Plaintext string `json:"plaintext"` 193 Facets []Facet `json:"facets,omitempty"` 194} 195 196// UnorderedListBlock represents an unordered list (pub.leaflet.blocks.unorderedList) 197type UnorderedListBlock struct { 198 Type string `json:"$type"` 199 Children []ListItem `json:"children"` 200} 201 202// ListItem represents a single list item (pub.leaflet.blocks.unorderedList#listItem) 203type ListItem struct { 204 Type string `json:"$type"` 205 Content any `json:"content"` // [TextBlock], [HeaderBlock], [ImageBlock] 206 Children []ListItem `json:"children,omitempty"` // Nested list items 207} 208 209// UnmarshalJSON custom unmarshaler for ListItem to properly type the Content field 210func (li *ListItem) UnmarshalJSON(data []byte) error { 211 type Alias ListItem 212 temp := &struct { 213 Content json.RawMessage `json:"content"` 214 *Alias 215 }{ 216 Alias: (*Alias)(li), 217 } 218 219 if err := json.Unmarshal(data, temp); err != nil { 220 return err 221 } 222 223 var typeCheck struct { 224 Type string `json:"$type"` 225 } 226 if err := json.Unmarshal(temp.Content, &typeCheck); err != nil { 227 return err 228 } 229 230 switch typeCheck.Type { 231 case TypeTextBlock: 232 var block TextBlock 233 if err := json.Unmarshal(temp.Content, &block); err != nil { 234 return err 235 } 236 li.Content = block 237 case TypeHeaderBlock: 238 var block HeaderBlock 239 if err := json.Unmarshal(temp.Content, &block); err != nil { 240 return err 241 } 242 li.Content = block 243 case TypeImageBlock: 244 var block ImageBlock 245 if err := json.Unmarshal(temp.Content, &block); err != nil { 246 return err 247 } 248 li.Content = block 249 default: 250 // For unknown types, leave as map 251 var block map[string]any 252 if err := json.Unmarshal(temp.Content, &block); err != nil { 253 return err 254 } 255 li.Content = block 256 } 257 258 return nil 259} 260 261// HorizontalRuleBlock represents a horizontal rule/thematic break (pub.leaflet.blocks.horizontalRule) 262type HorizontalRuleBlock struct { 263 Type string `json:"$type"` 264} 265 266// Facet represents text annotation (pub.leaflet.richtext.facet) 267type Facet struct { 268 Type string `json:"$type"` 269 Index ByteSlice `json:"index"` 270 Features []FacetFeature `json:"features"` 271} 272 273// UnmarshalJSON custom unmarshaler for Facet to properly type the Features field 274func (f *Facet) UnmarshalJSON(data []byte) error { 275 type Alias Facet 276 temp := &struct { 277 Features []json.RawMessage `json:"features"` 278 *Alias 279 }{ 280 Alias: (*Alias)(f), 281 } 282 283 if err := json.Unmarshal(data, temp); err != nil { 284 return err 285 } 286 287 f.Features = make([]FacetFeature, 0, len(temp.Features)) 288 for _, featureData := range temp.Features { 289 var typeCheck TypeCheck 290 if err := json.Unmarshal(featureData, &typeCheck); err != nil { 291 return err 292 } 293 294 var feature FacetFeature 295 switch typeCheck.Type { 296 case TypeFacetBold: 297 var fb FacetBold 298 if err := json.Unmarshal(featureData, &fb); err != nil { 299 return err 300 } 301 feature = fb 302 case TypeFacetItalic: 303 var fi FacetItalic 304 if err := json.Unmarshal(featureData, &fi); err != nil { 305 return err 306 } 307 feature = fi 308 case TypeFacetCode: 309 var fc FacetCode 310 if err := json.Unmarshal(featureData, &fc); err != nil { 311 return err 312 } 313 feature = fc 314 case TypeFacetLink: 315 var fl FacetLink 316 if err := json.Unmarshal(featureData, &fl); err != nil { 317 return err 318 } 319 feature = fl 320 case TypeFacetStrike: 321 var fs FacetStrikethrough 322 if err := json.Unmarshal(featureData, &fs); err != nil { 323 return err 324 } 325 feature = fs 326 case TypeFacetUnderline: 327 var fu FacetUnderline 328 if err := json.Unmarshal(featureData, &fu); err != nil { 329 return err 330 } 331 feature = fu 332 case TypeFacetHighlight: 333 var fh FacetHighlight 334 if err := json.Unmarshal(featureData, &fh); err != nil { 335 return err 336 } 337 feature = fh 338 default: 339 // Skip unknown feature types 340 continue 341 } 342 343 f.Features = append(f.Features, feature) 344 } 345 346 return nil 347} 348 349// ByteSlice specifies a substring range using UTF-8 byte offsets (pub.leaflet.richtext.facet#byteSlice) 350type ByteSlice struct { 351 Type string `json:"$type"` 352 ByteStart int `json:"byteStart"` 353 ByteEnd int `json:"byteEnd"` 354} 355 356// FacetFeature is a marker interface for facet features 357type FacetFeature interface { 358 GetFacetType() string 359} 360 361// FacetBold represents bold text styling 362type FacetBold struct { 363 Type string `json:"$type"` 364} 365 366func (f FacetBold) GetFacetType() string { return TypeFacetBold } 367 368// FacetItalic represents italic text styling 369type FacetItalic struct { 370 Type string `json:"$type"` 371} 372 373func (f FacetItalic) GetFacetType() string { return TypeFacetItalic } 374 375// FacetCode represents inline code styling 376type FacetCode struct { 377 Type string `json:"$type"` 378} 379 380func (f FacetCode) GetFacetType() string { return TypeFacetCode } 381 382// FacetLink represents a hyperlink 383type FacetLink struct { 384 Type string `json:"$type"` 385 URI string `json:"uri"` 386} 387 388func (f FacetLink) GetFacetType() string { return TypeFacetLink } 389 390// FacetStrikethrough represents strikethrough text styling 391type FacetStrikethrough struct { 392 Type string `json:"$type"` 393} 394 395func (f FacetStrikethrough) GetFacetType() string { return TypeFacetStrike } 396 397// FacetUnderline represents underline text styling 398type FacetUnderline struct { 399 Type string `json:"$type"` 400} 401 402func (f FacetUnderline) GetFacetType() string { return TypeFacetUnderline } 403 404// FacetHighlight represents highlighted text 405type FacetHighlight struct { 406 Type string `json:"$type"` 407} 408 409func (f FacetHighlight) GetFacetType() string { return TypeFacetHighlight } 410 411// Blob represents binary content (images, files) 412type Blob struct { 413 Type string `json:"$type"` 414 Ref CID `json:"ref"` 415 MimeType string `json:"mimeType"` 416 Size int `json:"size"` 417} 418 419// CID represents a Content Identifier (IPFS CID) 420type CID struct { 421 Link string `json:"$link"` 422} 423 424// Publication represents a leaflet publication (pub.leaflet.publication) 425type Publication struct { 426 Type string `json:"$type"` 427 Name string `json:"name"` 428 Description string `json:"description,omitempty"` 429 CreatedAt time.Time `json:"createdAt"` 430} 431 432// DocumentMeta holds metadata about a fetched document 433type DocumentMeta struct { 434 RKey string // Record key (TID) 435 CID string // Content identifier 436 URI string // Full AT URI 437 IsDraft bool // Draft vs published 438 FetchedAt time.Time // When we fetched it 439}