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