cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm leaflet readability golang
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}