a tool for shared writing and social publishing
0
fork

Configure Feed

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

add rich text to published docs!

+728 -135
+62 -58
actions/publishToPublication.ts
··· 10 10 PubLeafletBlocksText, 11 11 PubLeafletDocument, 12 12 PubLeafletPagesLinearDocument, 13 + PubLeafletRichtextFacet, 13 14 } from "lexicons/api"; 14 15 import { Block } from "components/Blocks/Block"; 15 16 import { TID } from "@atproto/common"; ··· 17 18 import { scanIndex, scanIndexLocal } from "src/replicache/utils"; 18 19 import type { Fact } from "src/replicache"; 19 20 import type { Attribute } from "src/replicache/attributes"; 20 - import { YJSFragmentToString } from "components/Blocks/TextBlock/RenderYJSFragment"; 21 + import { 22 + Delta, 23 + YJSFragmentToString, 24 + } from "components/Blocks/TextBlock/RenderYJSFragment"; 21 25 import { ids } from "lexicons/api/lexicons"; 22 26 import { OmitKey } from "lexicons/api/util"; 23 27 import { BlobRef } from "@atproto/lexicon"; 24 28 import { IdResolver } from "@atproto/identity"; 25 29 import { AtUri } from "@atproto/syntax"; 26 30 import { Json } from "supabase/database.types"; 31 + import { UnicodeString } from "@atproto/api"; 27 32 28 33 const idResolver = new IdResolver(); 29 34 export async function publishToPublication({ ··· 104 109 validate: false, //TODO publish the lexicon so we can validate! 105 110 }); 106 111 107 - console.log(result); 108 112 console.log( 109 113 await supabaseServerClient.from("documents").upsert({ 110 114 uri: result.uri, ··· 124 128 }) 125 129 .eq("leaflet", leaflet_id) 126 130 .eq("publication", publication_uri), 127 - sendPostToEmailSubscribers(publication_uri, { 128 - title: title || "Untitiled", 129 - content: blocksToHtml(blocks, imageMap, scan, publication_uri), 130 - }), 131 131 ]); 132 132 133 133 let handle = await idResolver.did.resolve(credentialSession.did!); ··· 147 147 Y.applyUpdate(doc, update); 148 148 let nodes = doc.getXmlElement("prosemirror").toArray(); 149 149 let stringValue = YJSFragmentToString(nodes[0]); 150 - return stringValue; 150 + let facets = YJSFragmentToFacets(nodes[0]); 151 + return [stringValue, facets]; 151 152 }; 152 153 return blocks.flatMap((b) => { 153 154 if (b.type !== "text" && b.type !== "heading" && b.type !== "image") ··· 155 156 if (b.type === "heading") { 156 157 let [headingLevel] = scan.eav(b.value, "block/heading-level"); 157 158 158 - let stringValue = getBlockContent(b.value); 159 + let [stringValue, facets] = getBlockContent(b.value); 159 160 return [ 160 161 { 161 162 $type: "pub.leaflet.pages.linearDocument#block", ··· 163 164 $type: "pub.leaflet.blocks.header", 164 165 level: headingLevel?.data.value || 1, 165 166 plaintext: stringValue, 167 + facets, 166 168 }, 167 169 } as PubLeafletPagesLinearDocument.Block, 168 170 ]; 169 171 } 170 172 171 173 if (b.type == "text") { 172 - let stringValue = getBlockContent(b.value); 174 + let [stringValue, facets] = getBlockContent(b.value); 173 175 return [ 174 176 { 175 177 $type: "pub.leaflet.pages.linearDocument#block", 176 178 block: { 177 179 $type: ids.PubLeafletBlocksText, 178 180 plaintext: stringValue, 181 + facets, 179 182 }, 180 183 } as PubLeafletPagesLinearDocument.Block, 181 184 ]; ··· 203 206 }); 204 207 } 205 208 206 - function blocksToHtml( 207 - blocks: Block[], 208 - imageMap: Map<string, BlobRef>, 209 - scan: ReturnType<typeof scanIndexLocal>, 210 - publication_uri: string, 211 - ) { 212 - const getBlockContent = (b: string) => { 213 - let [content] = scan.eav(b, "block/text"); 214 - if (!content) return ""; 215 - let doc = new Y.Doc(); 216 - const update = base64.toByteArray(content.data.value); 217 - Y.applyUpdate(doc, update); 218 - let nodes = doc.getXmlElement("prosemirror").toArray(); 219 - let stringValue = YJSFragmentToString(nodes[0]); 220 - return stringValue; 221 - }; 222 - return blocks 223 - .flatMap((b) => { 224 - if (b.type !== "text" && b.type !== "heading" && b.type !== "image") 225 - return []; 226 - if (b.type === "heading") { 227 - let [headingLevel] = scan.eav(b.value, "block/heading-level"); 228 - 229 - let stringValue = getBlockContent(b.value); 230 - let l = headingLevel?.data.value || 1; 231 - return [`<h${l}>${stringValue}</h${l}>`]; 232 - } 233 - 234 - if (b.type == "text") { 235 - let stringValue = getBlockContent(b.value); 236 - return `<p>${stringValue}</p>`; 237 - } 238 - if (b.type == "image") { 239 - let [image] = scan.eav(b.value, "block/image"); 240 - if (!image) return []; 241 - let blobref = imageMap.get(image.data.src); 242 - if (!blobref) return []; 243 - let uri = new AtUri(publication_uri); 244 - return `<img 245 - height=${image.data.height} 246 - width=${image.data.width}> 247 - src="https://bsky.social/xrpc/com.atproto.sync.getBlob?did=${uri.hostname}&cid=${(blobref as unknown as { $link: string })["$link"]}" 248 - </img>`; 249 - } 250 - return [""]; 251 - }) 252 - .join("\n"); 253 - } 254 - 255 209 async function sendPostToEmailSubscribers( 256 210 publication_uri: string, 257 211 post: { content: string; title: string }, ··· 296 250 ), 297 251 }); 298 252 } 253 + 254 + function YJSFragmentToFacets( 255 + node: Y.XmlElement | Y.XmlText | Y.XmlHook, 256 + ): PubLeafletRichtextFacet.Main[] { 257 + if (node.constructor === Y.XmlElement) { 258 + return node 259 + .toArray() 260 + .map((f) => YJSFragmentToFacets(f)) 261 + .flat(); 262 + } 263 + if (node.constructor === Y.XmlText) { 264 + let facets: PubLeafletRichtextFacet.Main[] = []; 265 + let delta = node.toDelta() as Delta[]; 266 + let byteStart = 0; 267 + console.log(delta); 268 + for (let d of delta) { 269 + let unicodestring = new UnicodeString(d.insert); 270 + let facet: PubLeafletRichtextFacet.Main = { 271 + index: { 272 + byteStart, 273 + byteEnd: byteStart + unicodestring.length, 274 + }, 275 + features: [], 276 + }; 277 + 278 + if (d.attributes?.strikethrough) 279 + facet.features.push({ 280 + $type: "pub.leaflet.richtext.facet#strikethrough", 281 + }); 282 + 283 + if (d.attributes?.highlight) 284 + facet.features.push({ $type: "pub.leaflet.richtext.facet#highlight" }); 285 + if (d.attributes?.underline) 286 + facet.features.push({ $type: "pub.leaflet.richtext.facet#underline" }); 287 + if (d.attributes?.strong) 288 + facet.features.push({ $type: "pub.leaflet.richtext.facet#bold" }); 289 + if (d.attributes?.em) 290 + facet.features.push({ $type: "pub.leaflet.richtext.facet#italic" }); 291 + if (d.attributes?.link) 292 + facet.features.push({ 293 + $type: "pub.leaflet.richtext.facet#link", 294 + uri: d.attributes.link.href, 295 + }); 296 + facets.push(facet); 297 + byteStart += unicodestring.length; 298 + } 299 + return facets; 300 + } 301 + return []; 302 + }
+115
app/lish/[did]/[publication]/[rkey]/TextBlock.tsx
··· 1 + "use client"; 2 + import { UnicodeString } from "@atproto/api"; 3 + import { PubLeafletRichtextFacet } from "lexicons/api"; 4 + import { useMemo } from "react"; 5 + 6 + type Facet = PubLeafletRichtextFacet.Main; 7 + export function TextBlock(props: { plaintext: string; facets?: Facet[] }) { 8 + const children = []; 9 + let richText = useMemo( 10 + () => new RichText({ text: props.plaintext, facets: props.facets || [] }), 11 + [props.plaintext, props.facets], 12 + ); 13 + let counter = 0; 14 + for (const segment of richText.segments()) { 15 + let link = segment.facet?.find(PubLeafletRichtextFacet.isLink); 16 + let isBold = segment.facet?.find(PubLeafletRichtextFacet.isBold); 17 + let isStrikethrough = segment.facet?.find( 18 + PubLeafletRichtextFacet.isStrikethrough, 19 + ); 20 + let isUnderline = segment.facet?.find(PubLeafletRichtextFacet.isUnderline); 21 + let isItalic = segment.facet?.find(PubLeafletRichtextFacet.isItalic); 22 + let isHighlighted = segment.facet?.find( 23 + PubLeafletRichtextFacet.isHighlight, 24 + ); 25 + let className = ` 26 + ${isBold ? "font-bold" : ""} 27 + ${isItalic ? "italic" : ""} 28 + ${isUnderline ? "underline" : ""} 29 + ${isStrikethrough ? "line-through decoration-tertiary" : ""} 30 + ${isHighlighted ? "highlight bg-highlight-1" : ""}`; 31 + 32 + if (link) { 33 + children.push( 34 + <a 35 + key={counter} 36 + href={link.uri} 37 + className={`text-accent-contrast hover:underline ${className}`} 38 + target="_blank" 39 + > 40 + {segment.text} 41 + </a>, 42 + ); 43 + } else { 44 + children.push( 45 + <span key={counter} className={className}> 46 + {segment.text} 47 + </span>, 48 + ); 49 + } 50 + 51 + counter++; 52 + } 53 + return <>{children}</>; 54 + } 55 + 56 + type RichTextSegment = { 57 + text: string; 58 + facet?: Exclude<Facet["features"], { $type: string }>; 59 + }; 60 + 61 + export class RichText { 62 + unicodeText: UnicodeString; 63 + facets?: Facet[]; 64 + 65 + constructor(props: { text: string; facets: Facet[] }) { 66 + this.unicodeText = new UnicodeString(props.text); 67 + this.facets = props.facets; 68 + if (this.facets) { 69 + this.facets = this.facets 70 + .filter((facet) => facet.index.byteStart <= facet.index.byteEnd) 71 + .sort((a, b) => a.index.byteStart - b.index.byteStart); 72 + } 73 + } 74 + 75 + *segments(): Generator<RichTextSegment, void, void> { 76 + const facets = this.facets || []; 77 + if (!facets.length) { 78 + yield { text: this.unicodeText.utf16 }; 79 + return; 80 + } 81 + 82 + let textCursor = 0; 83 + let facetCursor = 0; 84 + do { 85 + const currFacet = facets[facetCursor]; 86 + if (textCursor < currFacet.index.byteStart) { 87 + yield { 88 + text: this.unicodeText.slice(textCursor, currFacet.index.byteStart), 89 + }; 90 + } else if (textCursor > currFacet.index.byteStart) { 91 + facetCursor++; 92 + continue; 93 + } 94 + if (currFacet.index.byteStart < currFacet.index.byteEnd) { 95 + const subtext = this.unicodeText.slice( 96 + currFacet.index.byteStart, 97 + currFacet.index.byteEnd, 98 + ); 99 + if (!subtext.trim()) { 100 + // dont empty string entities 101 + yield { text: subtext }; 102 + } else { 103 + yield { text: subtext, facet: currFacet.features }; 104 + } 105 + } 106 + textCursor = currFacet.index.byteEnd; 107 + facetCursor++; 108 + } while (facetCursor < facets.length); 109 + if (textCursor < this.unicodeText.length) { 110 + yield { 111 + text: this.unicodeText.slice(textCursor, this.unicodeText.length), 112 + }; 113 + } 114 + } 115 + }
+78 -67
app/lish/[did]/[publication]/[rkey]/page.tsx
··· 11 11 } from "lexicons/api"; 12 12 import { Metadata } from "next"; 13 13 import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 14 + import { TextBlock } from "./TextBlock"; 15 + import { ThemeProvider } from "components/ThemeManager/ThemeProvider"; 14 16 15 17 export async function generateMetadata(props: { 16 18 params: Promise<{ publication: string; did: string; rkey: string }>; ··· 58 60 blocks = firstPage.blocks || []; 59 61 } 60 62 return ( 61 - <div className="postPage w-full h-screen bg-[#FDFCFA] flex items-stretch"> 62 - <div className="pubWrapper flex flex-col w-full "> 63 - <div className="pubContent flex flex-col px-3 sm:px-4 py-3 sm:py-9 mx-auto max-w-prose h-full w-full overflow-auto"> 64 - <div className="flex flex-col pb-8"> 65 - <Link 66 - className="font-bold hover:no-underline text-accent-contrast" 67 - href={getPublicationURL( 68 - document.documents_in_publications[0].publications, 69 - )} 70 - > 71 - {decodeURIComponent((await props.params).publication)} 72 - </Link> 73 - <h2 className="">{record.title}</h2> 74 - {record.description ? ( 75 - <p className="italic text-secondary">{record.description}</p> 76 - ) : null} 77 - {record.publishedAt ? ( 78 - <p className="text-sm text-tertiary pt-3"> 79 - Published{" "} 80 - {new Date(record.publishedAt).toLocaleDateString(undefined, { 81 - year: "numeric", 82 - month: "long", 83 - day: "2-digit", 84 - })} 85 - </p> 86 - ) : null} 87 - </div> 88 - {blocks.map((b, index) => { 89 - switch (true) { 90 - case PubLeafletBlocksImage.isMain(b.block): { 91 - return ( 92 - <img 93 - key={index} 94 - height={b.block.aspectRatio?.height} 95 - width={b.block.aspectRatio?.width} 96 - className="pb-2 sm:pb-3" 97 - src={`https://bsky.social/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${(b.block.image.ref as unknown as { $link: string })["$link"]}`} 98 - /> 99 - ); 100 - } 101 - case PubLeafletBlocksText.isMain(b.block): 102 - return ( 103 - <p key={index} className="pt-0 pb-2 sm:pb-3"> 104 - {b.block.plaintext} 105 - </p> 106 - ); 107 - case PubLeafletBlocksHeader.isMain(b.block): { 108 - if (b.block.level === 1) 63 + <ThemeProvider entityID={null}> 64 + <div className="postPage w-full h-screen bg-[#FDFCFA] flex items-stretch"> 65 + <div className="pubWrapper flex flex-col w-full "> 66 + <div className="pubContent flex flex-col px-3 sm:px-4 py-3 sm:py-9 mx-auto max-w-prose h-full w-full overflow-auto"> 67 + <div className="flex flex-col pb-8"> 68 + <Link 69 + className="font-bold hover:no-underline text-accent-contrast" 70 + href={getPublicationURL( 71 + document.documents_in_publications[0].publications, 72 + )} 73 + > 74 + {decodeURIComponent((await props.params).publication)} 75 + </Link> 76 + <h2 className="">{record.title}</h2> 77 + {record.description ? ( 78 + <p className="italic text-secondary">{record.description}</p> 79 + ) : null} 80 + {record.publishedAt ? ( 81 + <p className="text-sm text-tertiary pt-3"> 82 + Published{" "} 83 + {new Date(record.publishedAt).toLocaleDateString(undefined, { 84 + year: "numeric", 85 + month: "long", 86 + day: "2-digit", 87 + })} 88 + </p> 89 + ) : null} 90 + </div> 91 + {blocks.map((b, index) => { 92 + switch (true) { 93 + case PubLeafletBlocksImage.isMain(b.block): { 109 94 return ( 110 - <h1 key={index} className="pb-0 pt-2 sm:pt-3"> 111 - {b.block.plaintext} 112 - </h1> 95 + <img 96 + key={index} 97 + height={b.block.aspectRatio?.height} 98 + width={b.block.aspectRatio?.width} 99 + className="pb-2 sm:pb-3" 100 + src={`https://bsky.social/xrpc/com.atproto.sync.getBlob?did=${did}&cid=${(b.block.image.ref as unknown as { $link: string })["$link"]}`} 101 + /> 113 102 ); 114 - if (b.block.level === 2) 103 + } 104 + case PubLeafletBlocksText.isMain(b.block): 115 105 return ( 116 - <h3 key={index} className="pb-0 pt-2 sm:pt-3"> 117 - {b.block.plaintext} 118 - </h3> 106 + <div key={index} className="pt-0 pb-2 sm:pb-3"> 107 + <TextBlock 108 + facets={b.block.facets} 109 + plaintext={b.block.plaintext} 110 + /> 111 + </div> 119 112 ); 120 - if (b.block.level === 3) 113 + case PubLeafletBlocksHeader.isMain(b.block): { 114 + if (b.block.level === 1) 115 + return ( 116 + <h1 key={index} className="pb-0 pt-2 sm:pt-3"> 117 + <TextBlock {...b.block} /> 118 + </h1> 119 + ); 120 + if (b.block.level === 2) 121 + return ( 122 + <h3 key={index} className="pb-0 pt-2 sm:pt-3"> 123 + <TextBlock {...b.block} /> 124 + </h3> 125 + ); 126 + if (b.block.level === 3) 127 + return ( 128 + <h4 key={index} className="pb-0 pt-2 sm:pt-3"> 129 + <TextBlock {...b.block} /> 130 + </h4> 131 + ); 132 + // if (b.block.level === 4) return <h4>{b.block.plaintext}</h4>; 133 + // if (b.block.level === 5) return <h5>{b.block.plaintext}</h5>; 121 134 return ( 122 - <h4 key={index} className="pb-0 pt-2 sm:pt-3"> 123 - {b.block.plaintext} 124 - </h4> 135 + <h6 key={index}> 136 + <TextBlock {...b.block} /> 137 + </h6> 125 138 ); 126 - // if (b.block.level === 4) return <h4>{b.block.plaintext}</h4>; 127 - // if (b.block.level === 5) return <h5>{b.block.plaintext}</h5>; 128 - return <h6 key={index}>{b.block.plaintext}</h6>; 139 + } 140 + default: 141 + return null; 129 142 } 130 - default: 131 - return null; 132 - } 133 - })} 143 + })} 144 + </div> 134 145 </div> 135 146 </div> 136 - </div> 147 + </ThemeProvider> 137 148 ); 138 149 }
+2 -2
components/Blocks/TextBlock/RenderYJSFragment.tsx
··· 79 79 } 80 80 }; 81 81 82 - type Delta = { 82 + export type Delta = { 83 83 insert: string; 84 84 attributes?: { 85 85 strong?: {}; ··· 133 133 .map((d) => { 134 134 return d.insert; 135 135 }) 136 - .join(" "); 136 + .join(""); 137 137 } 138 138 return ""; 139 139 }
+12
lexicons/api/index.ts
··· 11 11 import * as PubLeafletBlocksImage from './types/pub/leaflet/blocks/image' 12 12 import * as PubLeafletBlocksText from './types/pub/leaflet/blocks/text' 13 13 import * as PubLeafletPagesLinearDocument from './types/pub/leaflet/pages/linearDocument' 14 + import * as PubLeafletRichtextFacet from './types/pub/leaflet/richtext/facet' 14 15 import * as ComAtprotoLabelDefs from './types/com/atproto/label/defs' 15 16 import * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites' 16 17 import * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createRecord' ··· 31 32 export * as PubLeafletBlocksImage from './types/pub/leaflet/blocks/image' 32 33 export * as PubLeafletBlocksText from './types/pub/leaflet/blocks/text' 33 34 export * as PubLeafletPagesLinearDocument from './types/pub/leaflet/pages/linearDocument' 35 + export * as PubLeafletRichtextFacet from './types/pub/leaflet/richtext/facet' 34 36 export * as ComAtprotoLabelDefs from './types/com/atproto/label/defs' 35 37 export * as ComAtprotoRepoApplyWrites from './types/com/atproto/repo/applyWrites' 36 38 export * as ComAtprotoRepoCreateRecord from './types/com/atproto/repo/createRecord' ··· 85 87 publication: PublicationRecord 86 88 blocks: PubLeafletBlocksNS 87 89 pages: PubLeafletPagesNS 90 + richtext: PubLeafletRichtextNS 88 91 89 92 constructor(client: XrpcClient) { 90 93 this._client = client 91 94 this.blocks = new PubLeafletBlocksNS(client) 92 95 this.pages = new PubLeafletPagesNS(client) 96 + this.richtext = new PubLeafletRichtextNS(client) 93 97 this.document = new DocumentRecord(client) 94 98 this.publication = new PublicationRecord(client) 95 99 } ··· 104 108 } 105 109 106 110 export class PubLeafletPagesNS { 111 + _client: XrpcClient 112 + 113 + constructor(client: XrpcClient) { 114 + this._client = client 115 + } 116 + } 117 + 118 + export class PubLeafletRichtextNS { 107 119 _client: XrpcClient 108 120 109 121 constructor(client: XrpcClient) {
+106 -2
lexicons/api/lexicons.ts
··· 98 98 defs: { 99 99 main: { 100 100 type: 'object', 101 - required: [], 101 + required: ['plaintext'], 102 102 properties: { 103 103 level: { 104 104 type: 'integer', ··· 107 107 }, 108 108 plaintext: { 109 109 type: 'string', 110 + }, 111 + facets: { 112 + type: 'array', 113 + items: { 114 + type: 'ref', 115 + ref: 'lex:pub.leaflet.richtext.facet', 116 + }, 110 117 }, 111 118 }, 112 119 }, ··· 156 163 defs: { 157 164 main: { 158 165 type: 'object', 159 - required: [], 166 + required: ['plaintext'], 160 167 properties: { 161 168 plaintext: { 162 169 type: 'string', 163 170 }, 171 + facets: { 172 + type: 'array', 173 + items: { 174 + type: 'ref', 175 + ref: 'lex:pub.leaflet.richtext.facet', 176 + }, 177 + }, 164 178 }, 165 179 }, 166 180 }, ··· 211 225 }, 212 226 textAlignRight: { 213 227 type: 'token', 228 + }, 229 + }, 230 + }, 231 + PubLeafletRichtextFacet: { 232 + lexicon: 1, 233 + id: 'pub.leaflet.richtext.facet', 234 + defs: { 235 + main: { 236 + type: 'object', 237 + description: 'Annotation of a sub-string within rich text.', 238 + required: ['index', 'features'], 239 + properties: { 240 + index: { 241 + type: 'ref', 242 + ref: 'lex:pub.leaflet.richtext.facet#byteSlice', 243 + }, 244 + features: { 245 + type: 'array', 246 + items: { 247 + type: 'union', 248 + refs: [ 249 + 'lex:pub.leaflet.richtext.facet#link', 250 + 'lex:pub.leaflet.richtext.facet#highlight', 251 + 'lex:pub.leaflet.richtext.facet#underline', 252 + 'lex:pub.leaflet.richtext.facet#strikethrough', 253 + 'lex:pub.leaflet.richtext.facet#bold', 254 + 'lex:pub.leaflet.richtext.facet#italic', 255 + ], 256 + }, 257 + }, 258 + }, 259 + }, 260 + byteSlice: { 261 + type: 'object', 262 + description: 263 + 'Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.', 264 + required: ['byteStart', 'byteEnd'], 265 + properties: { 266 + byteStart: { 267 + type: 'integer', 268 + minimum: 0, 269 + }, 270 + byteEnd: { 271 + type: 'integer', 272 + minimum: 0, 273 + }, 274 + }, 275 + }, 276 + link: { 277 + type: 'object', 278 + description: 279 + 'Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.', 280 + required: ['uri'], 281 + properties: { 282 + uri: { 283 + type: 'string', 284 + format: 'uri', 285 + }, 286 + }, 287 + }, 288 + highlight: { 289 + type: 'object', 290 + description: 'Facet feature for highlighted text.', 291 + required: [], 292 + properties: {}, 293 + }, 294 + underline: { 295 + type: 'object', 296 + description: 'Facet feature for underline markup', 297 + required: [], 298 + properties: {}, 299 + }, 300 + strikethrough: { 301 + type: 'object', 302 + description: 'Facet feature for strikethrough markup', 303 + required: [], 304 + properties: {}, 305 + }, 306 + bold: { 307 + type: 'object', 308 + description: 'Facet feature for bold text', 309 + required: [], 310 + properties: {}, 311 + }, 312 + italic: { 313 + type: 'object', 314 + description: 'Facet feature for italic text', 315 + required: [], 316 + properties: {}, 214 317 }, 215 318 }, 216 319 }, ··· 1199 1302 PubLeafletBlocksImage: 'pub.leaflet.blocks.image', 1200 1303 PubLeafletBlocksText: 'pub.leaflet.blocks.text', 1201 1304 PubLeafletPagesLinearDocument: 'pub.leaflet.pages.linearDocument', 1305 + PubLeafletRichtextFacet: 'pub.leaflet.richtext.facet', 1202 1306 ComAtprotoLabelDefs: 'com.atproto.label.defs', 1203 1307 ComAtprotoRepoApplyWrites: 'com.atproto.repo.applyWrites', 1204 1308 ComAtprotoRepoCreateRecord: 'com.atproto.repo.createRecord',
+3 -1
lexicons/api/types/pub/leaflet/blocks/header.ts
··· 5 5 import { CID } from 'multiformats/cid' 6 6 import { validate as _validate } from '../../../../lexicons' 7 7 import { $Typed, is$typed as _is$typed, OmitKey } from '../../../../util' 8 + import type * as PubLeafletRichtextFacet from '../richtext/facet' 8 9 9 10 const is$typed = _is$typed, 10 11 validate = _validate ··· 13 14 export interface Main { 14 15 $type?: 'pub.leaflet.blocks.header' 15 16 level?: number 16 - plaintext?: string 17 + plaintext: string 18 + facets?: PubLeafletRichtextFacet.Main[] 17 19 } 18 20 19 21 const hashMain = 'main'
+3 -1
lexicons/api/types/pub/leaflet/blocks/text.ts
··· 5 5 import { CID } from 'multiformats/cid' 6 6 import { validate as _validate } from '../../../../lexicons' 7 7 import { $Typed, is$typed as _is$typed, OmitKey } from '../../../../util' 8 + import type * as PubLeafletRichtextFacet from '../richtext/facet' 8 9 9 10 const is$typed = _is$typed, 10 11 validate = _validate ··· 12 13 13 14 export interface Main { 14 15 $type?: 'pub.leaflet.blocks.text' 15 - plaintext?: string 16 + plaintext: string 17 + facets?: PubLeafletRichtextFacet.Main[] 16 18 } 17 19 18 20 const hashMain = 'main'
+144
lexicons/api/types/pub/leaflet/richtext/facet.ts
··· 1 + /** 2 + * GENERATED CODE - DO NOT MODIFY 3 + */ 4 + import { ValidationResult, BlobRef } from '@atproto/lexicon' 5 + import { CID } from 'multiformats/cid' 6 + import { validate as _validate } from '../../../../lexicons' 7 + import { $Typed, is$typed as _is$typed, OmitKey } from '../../../../util' 8 + 9 + const is$typed = _is$typed, 10 + validate = _validate 11 + const id = 'pub.leaflet.richtext.facet' 12 + 13 + /** Annotation of a sub-string within rich text. */ 14 + export interface Main { 15 + $type?: 'pub.leaflet.richtext.facet' 16 + index: ByteSlice 17 + features: ( 18 + | $Typed<Link> 19 + | $Typed<Highlight> 20 + | $Typed<Underline> 21 + | $Typed<Strikethrough> 22 + | $Typed<Bold> 23 + | $Typed<Italic> 24 + | { $type: string } 25 + )[] 26 + } 27 + 28 + const hashMain = 'main' 29 + 30 + export function isMain<V>(v: V) { 31 + return is$typed(v, id, hashMain) 32 + } 33 + 34 + export function validateMain<V>(v: V) { 35 + return validate<Main & V>(v, id, hashMain) 36 + } 37 + 38 + /** Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets. */ 39 + export interface ByteSlice { 40 + $type?: 'pub.leaflet.richtext.facet#byteSlice' 41 + byteStart: number 42 + byteEnd: number 43 + } 44 + 45 + const hashByteSlice = 'byteSlice' 46 + 47 + export function isByteSlice<V>(v: V) { 48 + return is$typed(v, id, hashByteSlice) 49 + } 50 + 51 + export function validateByteSlice<V>(v: V) { 52 + return validate<ByteSlice & V>(v, id, hashByteSlice) 53 + } 54 + 55 + /** Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL. */ 56 + export interface Link { 57 + $type?: 'pub.leaflet.richtext.facet#link' 58 + uri: string 59 + } 60 + 61 + const hashLink = 'link' 62 + 63 + export function isLink<V>(v: V) { 64 + return is$typed(v, id, hashLink) 65 + } 66 + 67 + export function validateLink<V>(v: V) { 68 + return validate<Link & V>(v, id, hashLink) 69 + } 70 + 71 + /** Facet feature for highlighted text. */ 72 + export interface Highlight { 73 + $type?: 'pub.leaflet.richtext.facet#highlight' 74 + } 75 + 76 + const hashHighlight = 'highlight' 77 + 78 + export function isHighlight<V>(v: V) { 79 + return is$typed(v, id, hashHighlight) 80 + } 81 + 82 + export function validateHighlight<V>(v: V) { 83 + return validate<Highlight & V>(v, id, hashHighlight) 84 + } 85 + 86 + /** Facet feature for underline markup */ 87 + export interface Underline { 88 + $type?: 'pub.leaflet.richtext.facet#underline' 89 + } 90 + 91 + const hashUnderline = 'underline' 92 + 93 + export function isUnderline<V>(v: V) { 94 + return is$typed(v, id, hashUnderline) 95 + } 96 + 97 + export function validateUnderline<V>(v: V) { 98 + return validate<Underline & V>(v, id, hashUnderline) 99 + } 100 + 101 + /** Facet feature for strikethrough markup */ 102 + export interface Strikethrough { 103 + $type?: 'pub.leaflet.richtext.facet#strikethrough' 104 + } 105 + 106 + const hashStrikethrough = 'strikethrough' 107 + 108 + export function isStrikethrough<V>(v: V) { 109 + return is$typed(v, id, hashStrikethrough) 110 + } 111 + 112 + export function validateStrikethrough<V>(v: V) { 113 + return validate<Strikethrough & V>(v, id, hashStrikethrough) 114 + } 115 + 116 + /** Facet feature for bold text */ 117 + export interface Bold { 118 + $type?: 'pub.leaflet.richtext.facet#bold' 119 + } 120 + 121 + const hashBold = 'bold' 122 + 123 + export function isBold<V>(v: V) { 124 + return is$typed(v, id, hashBold) 125 + } 126 + 127 + export function validateBold<V>(v: V) { 128 + return validate<Bold & V>(v, id, hashBold) 129 + } 130 + 131 + /** Facet feature for italic text */ 132 + export interface Italic { 133 + $type?: 'pub.leaflet.richtext.facet#italic' 134 + } 135 + 136 + const hashItalic = 'italic' 137 + 138 + export function isItalic<V>(v: V) { 139 + return is$typed(v, id, hashItalic) 140 + } 141 + 142 + export function validateItalic<V>(v: V) { 143 + return validate<Italic & V>(v, id, hashItalic) 144 + }
+2
lexicons/build.ts
··· 5 5 6 6 import * as fs from "fs"; 7 7 import * as path from "path"; 8 + import { PubLeafletRichTextFacet } from "./src/facet"; 8 9 9 10 const outdir = path.join("lexicons", "pub", "leaflet"); 10 11 ··· 15 16 16 17 const lexicons = [ 17 18 PubLeafletDocument, 19 + PubLeafletRichTextFacet, 18 20 PageLexicons.PubLeafletPagesLinearDocument, 19 21 ...BlockLexicons, 20 22 ...Object.values(PublicationLexicons),
+10 -1
lexicons/pub/leaflet/blocks/header.json
··· 4 4 "defs": { 5 5 "main": { 6 6 "type": "object", 7 - "required": [], 7 + "required": [ 8 + "plaintext" 9 + ], 8 10 "properties": { 9 11 "level": { 10 12 "type": "integer", ··· 13 15 }, 14 16 "plaintext": { 15 17 "type": "string" 18 + }, 19 + "facets": { 20 + "type": "array", 21 + "items": { 22 + "type": "ref", 23 + "ref": "pub.leaflet.richtext.facet" 24 + } 16 25 } 17 26 } 18 27 }
+10 -1
lexicons/pub/leaflet/blocks/text.json
··· 4 4 "defs": { 5 5 "main": { 6 6 "type": "object", 7 - "required": [], 7 + "required": [ 8 + "plaintext" 9 + ], 8 10 "properties": { 9 11 "plaintext": { 10 12 "type": "string" 13 + }, 14 + "facets": { 15 + "type": "array", 16 + "items": { 17 + "type": "ref", 18 + "ref": "pub.leaflet.richtext.facet" 19 + } 11 20 } 12 21 } 13 22 }
+95
lexicons/pub/leaflet/richtext/facet.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "pub.leaflet.richtext.facet", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "description": "Annotation of a sub-string within rich text.", 8 + "required": [ 9 + "index", 10 + "features" 11 + ], 12 + "properties": { 13 + "index": { 14 + "type": "ref", 15 + "ref": "#byteSlice" 16 + }, 17 + "features": { 18 + "type": "array", 19 + "items": { 20 + "type": "union", 21 + "refs": [ 22 + "#link", 23 + "#highlight", 24 + "#underline", 25 + "#strikethrough", 26 + "#bold", 27 + "#italic" 28 + ] 29 + } 30 + } 31 + } 32 + }, 33 + "byteSlice": { 34 + "type": "object", 35 + "description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.", 36 + "required": [ 37 + "byteStart", 38 + "byteEnd" 39 + ], 40 + "properties": { 41 + "byteStart": { 42 + "type": "integer", 43 + "minimum": 0 44 + }, 45 + "byteEnd": { 46 + "type": "integer", 47 + "minimum": 0 48 + } 49 + } 50 + }, 51 + "link": { 52 + "type": "object", 53 + "description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.", 54 + "required": [ 55 + "uri" 56 + ], 57 + "properties": { 58 + "uri": { 59 + "type": "string", 60 + "format": "uri" 61 + } 62 + } 63 + }, 64 + "highlight": { 65 + "type": "object", 66 + "description": "Facet feature for highlighted text.", 67 + "required": [], 68 + "properties": {} 69 + }, 70 + "underline": { 71 + "type": "object", 72 + "description": "Facet feature for underline markup", 73 + "required": [], 74 + "properties": {} 75 + }, 76 + "strikethrough": { 77 + "type": "object", 78 + "description": "Facet feature for strikethrough markup", 79 + "required": [], 80 + "properties": {} 81 + }, 82 + "bold": { 83 + "type": "object", 84 + "description": "Facet feature for bold text", 85 + "required": [], 86 + "properties": {} 87 + }, 88 + "italic": { 89 + "type": "object", 90 + "description": "Facet feature for italic text", 91 + "required": [], 92 + "properties": {} 93 + } 94 + } 95 + }
+11 -2
lexicons/src/blocks.ts
··· 1 1 import { LexiconDoc, LexRefUnion } from "@atproto/lexicon"; 2 + import { PubLeafletRichTextFacet } from "./facet"; 2 3 3 4 export const PubLeafletBlocksText: LexiconDoc = { 4 5 lexicon: 1, ··· 6 7 defs: { 7 8 main: { 8 9 type: "object", 9 - required: [], 10 + required: ["plaintext"], 10 11 properties: { 11 12 plaintext: { type: "string" }, 13 + facets: { 14 + type: "array", 15 + items: { type: "ref", ref: PubLeafletRichTextFacet.id }, 16 + }, 12 17 }, 13 18 }, 14 19 }, ··· 20 25 defs: { 21 26 main: { 22 27 type: "object", 23 - required: [], 28 + required: ["plaintext"], 24 29 properties: { 25 30 level: { type: "integer", minimum: 1, maximum: 6 }, 26 31 plaintext: { type: "string" }, 32 + facets: { 33 + type: "array", 34 + items: { type: "ref", ref: PubLeafletRichTextFacet.id }, 35 + }, 27 36 }, 28 37 }, 29 38 },
+75
lexicons/src/facet.ts
··· 1 + import { LexiconDoc } from "@atproto/lexicon"; 2 + const FacetItems: LexiconDoc["defs"] = { 3 + link: { 4 + type: "object", 5 + description: 6 + "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.", 7 + required: ["uri"], 8 + properties: { 9 + uri: { type: "string", format: "uri" }, 10 + }, 11 + }, 12 + highlight: { 13 + type: "object", 14 + description: "Facet feature for highlighted text.", 15 + required: [], 16 + properties: {}, 17 + }, 18 + underline: { 19 + type: "object", 20 + description: "Facet feature for underline markup", 21 + required: [], 22 + properties: {}, 23 + }, 24 + strikethrough: { 25 + type: "object", 26 + description: "Facet feature for strikethrough markup", 27 + required: [], 28 + properties: {}, 29 + }, 30 + bold: { 31 + type: "object", 32 + description: "Facet feature for bold text", 33 + required: [], 34 + properties: {}, 35 + }, 36 + italic: { 37 + type: "object", 38 + description: "Facet feature for italic text", 39 + required: [], 40 + properties: {}, 41 + }, 42 + }; 43 + 44 + export const PubLeafletRichTextFacet = { 45 + lexicon: 1, 46 + id: "pub.leaflet.richtext.facet", 47 + defs: { 48 + main: { 49 + type: "object", 50 + description: "Annotation of a sub-string within rich text.", 51 + required: ["index", "features"], 52 + properties: { 53 + index: { type: "ref", ref: "#byteSlice" }, 54 + features: { 55 + type: "array", 56 + items: { 57 + type: "union", 58 + refs: Object.keys(FacetItems).map((k) => `#${k}`), 59 + }, 60 + }, 61 + }, 62 + }, 63 + byteSlice: { 64 + type: "object", 65 + description: 66 + "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.", 67 + required: ["byteStart", "byteEnd"], 68 + properties: { 69 + byteStart: { type: "integer", minimum: 0 }, 70 + byteEnd: { type: "integer", minimum: 0 }, 71 + }, 72 + }, 73 + ...FacetItems, 74 + }, 75 + };