Compare changes

Choose any two refs to compare.

-13
.changeset/plain-beans-taste.md
··· 1 - --- 2 - "@nulfrost/leaflet-loader-astro": minor 3 - --- 4 - 5 - Added support for these leaflet blocks: 6 - 7 - - ul/li 8 - - math 9 - - code 10 - - img 11 - - hr 12 - 13 - the only remaining block to implement is "website", though I haven't thought of a good way to output that yet. stay tuned for a further release
+43
CHANGELOG.md
··· 1 1 # leaflet-loader-astro 2 2 3 + ## 1.3.0 4 + 5 + ### Minor Changes 6 + 7 + - eb3bc4b: Add iframe block 8 + 9 + ## 1.2.0 10 + 11 + ### Minor Changes 12 + 13 + - f920153: Add support for blockquotes 14 + 15 + ### Patch Changes 16 + 17 + - 8922bb1: Add JSDoc comments for available loader options for leafletStaticLoader and leafletLiveLoader 18 + 19 + ## 1.1.0 20 + 21 + ### Minor Changes 22 + 23 + - 6d70cc6: Added support for these leaflet blocks: 24 + 25 + - ul/li 26 + - math 27 + - code 28 + - img 29 + - hr 30 + 31 + the only remaining block to implement is "website", though I haven't thought of a good way to output that yet. stay tuned for a further release 32 + 33 + - 5524ce5: Added the ability to use a handle or did when specifying a repo for leafletStaticLoader and leafletLiveLoader 34 + 35 + ```ts 36 + import { defineLiveCollection } from "astro:content"; 37 + import { leafletLiveLoader } from "leaflet-loader-astro"; 38 + 39 + const documents = defineLiveCollection({ 40 + loader: leafletLiveLoader({ repo: "dane.computer" }), // or repo: did:plc:qttsv4e7pu2jl3ilanfgc3zn, both work! 41 + }); 42 + 43 + export const collections = { documents }; 44 + ``` 45 + 3 46 ## 1.0.0 4 47 5 48 ### Major Changes
+57 -4
README.md
··· 15 15 16 16 ## Usage 17 17 18 - ### Build-time loader: leafletStaticLoader (recommended) 18 + <details> 19 + <summary>Build-time loader: leafletStaticLoader **(recommended)**</summary> 19 20 20 21 ```ts 21 22 // src/content.config.ts ··· 23 24 import { leafletStaticLoader } from "@nulfrost/leaflet-loader-astro"; 24 25 25 26 const documents = defineCollection({ 26 - loader: leafletStaticLoader({ repo: "did:plc:qttsv4e7pu2jl3ilanfgc3zn" }), 27 + loader: leafletStaticLoader({ repo: "did:plc:qttsv4e7pu2jl3ilanfgc3zn" }), // or repo: dane.is.extraordinarily.cool 27 28 }); 28 29 29 30 export const collections = { documents }; ··· 81 82 82 83 <Content /> 83 84 ``` 85 + </details> 84 86 85 - ### Live loader: leafletLiveLoader 87 + <details> 88 + <summary>Live loader: leafletLiveLoader</summary> 86 89 87 90 ```ts 88 91 // astro.config.mjs ··· 104 107 import { leafletLiveLoader } from "@nulfrost/leaflet-loader-astro"; 105 108 106 109 const documents = defineLiveCollection({ 107 - loader: leafletLiveLoader({ repo: "did:plc:qttsv4e7pu2jl3ilanfgc3zn" }), 110 + loader: leafletLiveLoader({ repo: "did:plc:qttsv4e7pu2jl3ilanfgc3zn" }), // or repo: dane.is.extraordinarily.cool 108 111 }); 109 112 110 113 export const collections = { documents }; ··· 158 161 159 162 <Content /> 160 163 ``` 164 + 165 + </details> 166 + 167 + ## Loader Options 168 + 169 + ### Static Loader 170 + 171 + ```ts 172 + leafletStaticLoader() 173 + ``` 174 + 175 + `repo`: This can be either your DID (did:plc:qttsv4e7pu2jl3ilanfgc3zn) or your handle (dane.is.extraordinarily.cool) 176 + 177 + `limit`: How many leaflet documents to return when calling `getCollection`. The default is 50 and the range is from 1 to 100. 178 + 179 + `reverse`: Whether or not to return the leaflet documents in reverse order. By default this is false. 180 + 181 + ### Live Loader 182 + 183 + ```ts 184 + leafletLiveLoader() 185 + ``` 186 + 187 + `repo`: This can be either your DID (did:plc:qttsv4e7pu2jl3ilanfgc3zn) or your handle (dane.is.extraordinarily.cool) 188 + 189 + > [!NOTE] 190 + > `getLiveCollection` supports a second argument where you can add additional filters, similar to the options you have access to for `leafletStaticLoader` 191 + 192 + ```ts 193 + getLiveCollection() 194 + ``` 195 + 196 + `limit`: How many leaflet documents to return when calling `getCollection`. The default is 50 and the range is from 1 to 100. 197 + 198 + `reverse`: Whether or not to return the leaflet documents in reverse order. By default this is false. 199 + 200 + ## Supported Leaflet Blocks 201 + 202 + - [ ] Bluesky post 203 + - [x] Iframe 204 + - [x] Horizontal Rule 205 + - [x] Unordered List 206 + - [x] Math 207 + - [x] Code 208 + - [ ] Website 209 + - [x] Image 210 + - [x] Blockquote 211 + - [x] Text 212 + - [x] Header 213 + - [x] List Item 161 214 162 215 ## License 163 216
+22
lexicons/pub/leaflet/blocks/blockquote.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "pub.leaflet.blocks.blockquote", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "required": ["plaintext"], 8 + "properties": { 9 + "plaintext": { 10 + "type": "string" 11 + }, 12 + "facets": { 13 + "type": "array", 14 + "items": { 15 + "type": "ref", 16 + "ref": "pub.leaflet.richtext.facet" 17 + } 18 + } 19 + } 20 + } 21 + } 22 + }
+21
lexicons/pub/leaflet/blocks/iframe.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "pub.leaflet.blocks.iframe", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "required": ["url"], 8 + "properties": { 9 + "url": { 10 + "type": "string", 11 + "format": "uri" 12 + }, 13 + "height": { 14 + "type": "integer", 15 + "minimum": 16, 16 + "maximum": 1600 17 + } 18 + } 19 + } 20 + } 21 + }
+12 -5
lib/leaflet-live-loader.ts
··· 1 - import { isDid } from "@atproto/did"; 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 2 + import { isHandle } from "@atcute/lexicons/syntax"; 2 3 import type { LiveLoader } from "astro/loaders"; 3 4 import type { 4 5 CollectionFilter, ··· 10 11 import { 11 12 getLeafletDocuments, 12 13 getSingleLeafletDocument, 14 + isPlcDid, 13 15 leafletBlocksToHTML, 14 16 leafletDocumentRecordToView, 15 17 LiveLoaderError, 16 18 resolveMiniDoc, 17 19 uriToRkey, 18 20 } from "./utils.js"; 19 - import { Client, simpleFetchHandler } from "@atcute/client"; 20 21 21 22 export function leafletLiveLoader( 22 23 options: LiveLeafletLoaderOptions, ··· 35 36 ); 36 37 } 37 38 38 - // not a valid did 39 - if (!isDid(repo)) { 40 - throw new LiveLoaderError("invalid did", "INVALID_DID"); 39 + // not a valid handle, check if valid did 40 + if (!isHandle(repo)) { 41 + // not a valid handle or did, throw 42 + if (!isPlcDid(repo)) { 43 + throw new LiveLoaderError( 44 + "invalid handle or did", 45 + "INVALID_HANDLE_OR_DID", 46 + ); 47 + } 41 48 } 42 49 43 50 return {
+14 -6
lib/leaftlet-static-loader.ts
··· 1 1 import { Client, simpleFetchHandler } from "@atcute/client"; 2 - import { isDid } from "@atproto/did"; 2 + import { isHandle } from "@atcute/lexicons/syntax"; 3 3 import type { Loader, LoaderContext } from "astro/loaders"; 4 4 import { LeafletDocumentSchema } from "schema.js"; 5 5 import type { ··· 8 8 } from "types.js"; 9 9 import { 10 10 getLeafletDocuments, 11 + isPlcDid, 11 12 leafletBlocksToHTML, 12 13 leafletDocumentRecordToView, 13 14 LiveLoaderError, ··· 18 19 export function leafletStaticLoader( 19 20 options: StaticLeafletLoaderOptions, 20 21 ): Loader { 21 - const { repo, limit } = options; 22 + const { repo, limit, reverse } = options; 22 23 23 24 if (!repo || typeof repo !== "string") { 24 25 throw new LiveLoaderError( ··· 27 28 ); 28 29 } 29 30 30 - // not a valid did 31 - if (!isDid(repo)) { 32 - throw new LiveLoaderError("invalid did", "INVALID_DID"); 31 + // not a valid handle, check if valid did 32 + if (!isHandle(repo)) { 33 + // not a valid handle or did, throw 34 + if (!isPlcDid(repo)) { 35 + throw new LiveLoaderError( 36 + "invalid handle or did", 37 + "INVALID_HANDLE_OR_DID", 38 + ); 39 + } 33 40 } 34 41 35 42 return { ··· 56 63 rpc, 57 64 repo, 58 65 cursor, 59 - limit: 100, 66 + reverse, 67 + limit: 50, 60 68 }); 61 69 for (const document of documents) { 62 70 if (limit && count >= limit) {
+2
lib/lexicons/index.ts
··· 1 1 export * as ComAtprotoRepoStrongRef from "./types/com/atproto/repo/strongRef.js"; 2 + export * as PubLeafletBlocksBlockquote from "./types/pub/leaflet/blocks/blockquote.js"; 2 3 export * as PubLeafletBlocksCode from "./types/pub/leaflet/blocks/code.js"; 3 4 export * as PubLeafletBlocksHeader from "./types/pub/leaflet/blocks/header.js"; 4 5 export * as PubLeafletBlocksHorizontalRule from "./types/pub/leaflet/blocks/horizontalRule.js"; 6 + export * as PubLeafletBlocksIframe from "./types/pub/leaflet/blocks/iframe.js"; 5 7 export * as PubLeafletBlocksImage from "./types/pub/leaflet/blocks/image.js"; 6 8 export * as PubLeafletBlocksMath from "./types/pub/leaflet/blocks/math.js"; 7 9 export * as PubLeafletBlocksText from "./types/pub/leaflet/blocks/text.js";
+23
lib/lexicons/types/pub/leaflet/blocks/blockquote.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + import * as PubLeafletRichtextFacet from "../richtext/facet.js"; 4 + 5 + const _mainSchema = /*#__PURE__*/ v.object({ 6 + $type: /*#__PURE__*/ v.optional( 7 + /*#__PURE__*/ v.literal("pub.leaflet.blocks.blockquote"), 8 + ), 9 + get facets() { 10 + return /*#__PURE__*/ v.optional( 11 + /*#__PURE__*/ v.array(PubLeafletRichtextFacet.mainSchema), 12 + ); 13 + }, 14 + plaintext: /*#__PURE__*/ v.string(), 15 + }); 16 + 17 + type main$schematype = typeof _mainSchema; 18 + 19 + export interface mainSchema extends main$schematype {} 20 + 21 + export const mainSchema = _mainSchema as mainSchema; 22 + 23 + export interface Main extends v.InferInput<typeof mainSchema> {}
+22
lib/lexicons/types/pub/leaflet/blocks/iframe.ts
··· 1 + import type {} from "@atcute/lexicons"; 2 + import * as v from "@atcute/lexicons/validations"; 3 + 4 + const _mainSchema = /*#__PURE__*/ v.object({ 5 + $type: /*#__PURE__*/ v.optional( 6 + /*#__PURE__*/ v.literal("pub.leaflet.blocks.iframe"), 7 + ), 8 + height: /*#__PURE__*/ v.optional( 9 + /*#__PURE__*/ v.constrain(/*#__PURE__*/ v.integer(), [ 10 + /*#__PURE__*/ v.integerRange(16, 1600), 11 + ]), 12 + ), 13 + url: /*#__PURE__*/ v.genericUriString(), 14 + }); 15 + 16 + type main$schematype = typeof _mainSchema; 17 + 18 + export interface mainSchema extends main$schematype {} 19 + 20 + export const mainSchema = _mainSchema as mainSchema; 21 + 22 + export interface Main extends v.InferInput<typeof mainSchema> {}
+1 -1
lib/lexicons/types/pub/leaflet/blocks/image.ts
··· 14 14 ), 15 15 alt: /*#__PURE__*/ v.optional(/*#__PURE__*/ v.string()), 16 16 get aspectRatio() { 17 - return _aspectRatioSchema; 17 + return aspectRatioSchema; 18 18 }, 19 19 image: /*#__PURE__*/ v.blob(), 20 20 });
+14 -3
lib/types.ts
··· 5 5 6 6 export interface LiveLeafletLoaderOptions { 7 7 /** 8 - * @description Your repo is your DID (did:plc... or did:web...). You can find this information using: https://pdsls.dev 8 + * @description Your repo is your DID (did:plc... or did:web...) or handle (username.bsky.social). You can find this information using: https://pdsls.dev 9 9 */ 10 10 repo: string; 11 11 } 12 12 13 13 export interface StaticLeafletLoaderOptions { 14 14 /** 15 - * @description Your repo is your DID (did:plc... or did:web...). You can find this information using: https://pdsls.dev 15 + * @description Your repo is your DID (did:plc... or did:web...) or handle (username.bsky.social). You can find this information using: https://pdsls.dev 16 16 */ 17 17 repo: string; 18 - filter?: string; 19 18 /** 19 + * @description The number of records leaflet records to return for getCollection, the default being 50. The range can be from 1 to 100. 20 20 * @default 50 21 21 */ 22 22 limit?: number; 23 + /** 24 + * @description Whether or not the records should be returned in reverse order. 25 + * @default undefined 26 + */ 27 + reverse?: boolean; 23 28 } 24 29 25 30 export interface LeafletDocumentRecord { ··· 78 83 text: string; 79 84 facet?: Exclude<Facet["features"], { $type: string }>; 80 85 } 86 + 87 + // yoinked from: https://github.com/mary-ext/atcute/blob/trunk/packages/lexicons/lexicons/lib/syntax/handle.ts 88 + /** 89 + * represents a decentralized identifier (DID). 90 + */ 91 + export type Did<Method extends string = string> = `did:${Method}:${string}`;
+76 -48
lib/utils.ts
··· 4 4 import katex from "katex"; 5 5 import sanitizeHTML from "sanitize-html"; 6 6 import { 7 + PubLeafletBlocksBlockquote, 7 8 PubLeafletBlocksCode, 8 9 PubLeafletBlocksHeader, 9 10 PubLeafletBlocksHorizontalRule, 11 + PubLeafletBlocksIframe, 10 12 PubLeafletBlocksImage, 11 13 PubLeafletBlocksMath, 12 14 PubLeafletBlocksText, ··· 14 16 PubLeafletPagesLinearDocument, 15 17 } from "./lexicons/index.js"; 16 18 import type { 19 + Did, 17 20 Facet, 18 21 GetLeafletDocumentsParams, 19 22 GetSingleLeafletDocumentParams, ··· 160 163 "*": ["class", "style"], 161 164 img: ["src", "height", "width", "alt"], 162 165 a: ["href", "target", "rel"], 166 + iframe: ["height", "allow", "loading", "src"], 163 167 }, 164 168 allowedTags: [ 165 169 "img", ··· 181 185 "hr", 182 186 "div", 183 187 "span", 188 + "blockquote", 189 + "iframe", 184 190 ], 185 191 selfClosing: ["img"], 186 192 }); ··· 243 249 } 244 250 } 245 251 246 - function parseBlocks({ 252 + export function parseTextBlock(block: PubLeafletBlocksText.Main) { 253 + let html = ""; 254 + const rt = new RichText({ 255 + text: block.plaintext, 256 + facets: block.facets || [], 257 + }); 258 + const children = []; 259 + for (const segment of rt.segments()) { 260 + const link = segment.facet?.find( 261 + (segment) => segment.$type === "pub.leaflet.richtext.facet#link", 262 + ); 263 + const isBold = segment.facet?.find( 264 + (segment) => segment.$type === "pub.leaflet.richtext.facet#bold", 265 + ); 266 + const isCode = segment.facet?.find( 267 + (segment) => segment.$type === "pub.leaflet.richtext.facet#code", 268 + ); 269 + const isStrikethrough = segment.facet?.find( 270 + (segment) => segment.$type === "pub.leaflet.richtext.facet#strikethrough", 271 + ); 272 + const isUnderline = segment.facet?.find( 273 + (segment) => segment.$type === "pub.leaflet.richtext.facet#underline", 274 + ); 275 + const isItalic = segment.facet?.find( 276 + (segment) => segment.$type === "pub.leaflet.richtext.facet#italic", 277 + ); 278 + if (isCode) { 279 + children.push(`<pre><code>${segment.text}</code></pre>`); 280 + } else if (link) { 281 + children.push( 282 + `<a href="${link.uri}" target="_blank" rel="noopener noreferrer">${segment.text}</a>`, 283 + ); 284 + } else if (isBold) { 285 + children.push(`<b>${segment.text}</b>`); 286 + } else if (isStrikethrough) { 287 + children.push(`<s>${segment.text}</s>`); 288 + } else if (isUnderline) { 289 + children.push( 290 + `<span style="text-decoration:underline;">${segment.text}</span>`, 291 + ); 292 + } else if (isItalic) { 293 + children.push(`<i>${segment.text}</i>`); 294 + } else { 295 + children.push(`${segment.text}`); 296 + } 297 + } 298 + html += `<p>${children.join("")}</p>`; 299 + 300 + return html.trim(); 301 + } 302 + 303 + export function parseBlocks({ 247 304 block, 248 305 did, 249 306 }: { ··· 253 310 let html = ""; 254 311 255 312 if (is(PubLeafletBlocksText.mainSchema, block.block)) { 256 - const rt = new RichText({ 257 - text: block.block.plaintext, 258 - facets: block.block.facets || [], 259 - }); 260 - const children = []; 261 - for (const segment of rt.segments()) { 262 - const link = segment.facet?.find( 263 - (segment) => segment.$type === "pub.leaflet.richtext.facet#link", 264 - ); 265 - const isBold = segment.facet?.find( 266 - (segment) => segment.$type === "pub.leaflet.richtext.facet#bold", 267 - ); 268 - const isCode = segment.facet?.find( 269 - (segment) => segment.$type === "pub.leaflet.richtext.facet#code", 270 - ); 271 - const isStrikethrough = segment.facet?.find( 272 - (segment) => 273 - segment.$type === "pub.leaflet.richtext.facet#strikethrough", 274 - ); 275 - const isUnderline = segment.facet?.find( 276 - (segment) => segment.$type === "pub.leaflet.richtext.facet#underline", 277 - ); 278 - const isItalic = segment.facet?.find( 279 - (segment) => segment.$type === "pub.leaflet.richtext.facet#italic", 280 - ); 281 - if (isCode) { 282 - children.push(`<pre><code>${segment.text}</code></pre>`); 283 - } else if (link) { 284 - children.push( 285 - `<a href="${link.uri}" target="_blank" rel="noopener noreferrer">${segment.text}</a>`, 286 - ); 287 - } else if (isBold) { 288 - children.push(`<b>${segment.text}</b>`); 289 - } else if (isStrikethrough) { 290 - children.push(`<s>${segment.text}</s>`); 291 - } else if (isUnderline) { 292 - children.push( 293 - `<span style="text-decoration:underline;">${segment.text}</span>`, 294 - ); 295 - } else if (isItalic) { 296 - children.push(`<i>${segment.text}</i>`); 297 - } else { 298 - children.push(`${segment.text}`); 299 - } 300 - } 301 - html += `<p>${children.join("")}</p>`; 313 + html += parseTextBlock(block.block); 302 314 } 303 315 304 316 if (is(PubLeafletBlocksHeader.mainSchema, block.block)) { ··· 342 354 html += `<div><img src="https://cdn.bsky.app/img/feed_fullsize/plain/${did}/${block.block.image.ref.$link}@jpeg" height="${block.block.aspectRatio.height}" width="${block.block.aspectRatio.width}" alt="${block.block.alt}" /></div>`; 343 355 } 344 356 357 + if (is(PubLeafletBlocksBlockquote.mainSchema, block.block)) { 358 + html += `<blockquote>${parseTextBlock(block.block)}</blockquote>`; 359 + } 360 + 361 + if (is(PubLeafletBlocksIframe.mainSchema, block.block)) { 362 + // @ts-ignore 363 + html += `<iframe height="${block.block.height}" allow="fullscreen" loading="lazy" src="${block.block.url}"></iframe>`; 364 + } 365 + 345 366 return html.trim(); 346 367 } 347 368 348 - function renderListItem({ 369 + export function renderListItem({ 349 370 item, 350 371 did, 351 372 }: { ··· 358 379 359 380 return `<li>${parseBlocks({ block: { block: item.content }, did })}${children}</li>`; 360 381 } 382 + 383 + // yoinked from: https://github.com/mary-ext/atcute/blob/trunk/packages/lexicons/lexicons/lib/syntax/handle.ts 384 + const PLC_DID_RE = /^did:plc:([a-z2-7]{24})$/; 385 + 386 + export const isPlcDid = (input: string): input is Did<"plc"> => { 387 + return input.length === 32 && PLC_DID_RE.test(input); 388 + };
+2 -3
package.json
··· 1 1 { 2 2 "name": "@nulfrost/leaflet-loader-astro", 3 - "version": "1.0.0", 3 + "version": "1.3.0", 4 4 "description": "A leaflet.pub astro collection loader", 5 5 "keywords": [ 6 6 "astro", ··· 12 12 "bugs": { 13 13 "url": "https://github.com/nulfrost/leaflet-loader-astro/issues" 14 14 }, 15 - "author": "Dane Miller", 15 + "author": "Dane Miller <me@dane.computer>", 16 16 "repository": { 17 17 "type": "git", 18 18 "url": "git+https://github.com/nulfrost/leaflet-loader-astro.git" ··· 60 60 "@atcute/client": "^4.0.3", 61 61 "@atcute/lexicons": "^1.1.0", 62 62 "@atproto/api": "^0.16.2", 63 - "@atproto/did": "^0.1.5", 64 63 "katex": "^0.16.22", 65 64 "sanitize-html": "^2.17.0" 66 65 }
-10
pnpm-lock.yaml
··· 17 17 '@atproto/api': 18 18 specifier: ^0.16.2 19 19 version: 0.16.2 20 - '@atproto/did': 21 - specifier: ^0.1.5 22 - version: 0.1.5 23 20 katex: 24 21 specifier: ^0.16.22 25 22 version: 0.16.22 ··· 101 98 102 99 '@atproto/common-web@0.4.2': 103 100 resolution: {integrity: sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw==} 104 - 105 - '@atproto/did@0.1.5': 106 - resolution: {integrity: sha512-8+1D08QdGE5TF0bB0vV8HLVrVZJeLNITpRTUVEoABNMRaUS7CoYSVb0+JNQDeJIVmqMjOL8dOjvCUDkp3gEaGQ==} 107 101 108 102 '@atproto/lexicon@0.4.12': 109 103 resolution: {integrity: sha512-fcEvEQ1GpQYF5igZ4IZjPWEoWVpsEF22L9RexxLS3ptfySXLflEyH384e7HITzO/73McDeaJx3lqHIuqn9ulnw==} ··· 2512 2506 graphemer: 1.4.0 2513 2507 multiformats: 9.9.0 2514 2508 uint8arrays: 3.0.0 2515 - zod: 3.25.76 2516 - 2517 - '@atproto/did@0.1.5': 2518 - dependencies: 2519 2509 zod: 3.25.76 2520 2510 2521 2511 '@atproto/lexicon@0.4.12':
+189
tests/parse-blocks.test.ts
··· 1 + import { expect, test } from "vitest"; 2 + import { parseBlocks } from "../lib/utils"; 3 + 4 + test("should correctly parse an h1 block to an h2 tag", () => { 5 + const html = parseBlocks({ 6 + block: { 7 + $type: "pub.leaflet.pages.linearDocument#block", 8 + block: { 9 + $type: "pub.leaflet.blocks.header", 10 + level: 1, 11 + facets: [], 12 + plaintext: "heading 1", 13 + }, 14 + }, 15 + did: "did:plc:qttsv4e7pu2jl3ilanfgc3zn", 16 + }); 17 + 18 + expect(html).toMatchInlineSnapshot(`"<h2>heading 1</h2>"`); 19 + }); 20 + 21 + test("should correctly parse an h2 block to an h3 tag", () => { 22 + const html = parseBlocks({ 23 + block: { 24 + $type: "pub.leaflet.pages.linearDocument#block", 25 + block: { 26 + $type: "pub.leaflet.blocks.header", 27 + level: 2, 28 + facets: [], 29 + plaintext: "heading 2", 30 + }, 31 + }, 32 + did: "did:plc:qttsv4e7pu2jl3ilanfgc3zn", 33 + }); 34 + 35 + expect(html).toMatchInlineSnapshot(`"<h3>heading 2</h3>"`); 36 + }); 37 + 38 + test("should correctly parse an h3 block to an h4 tag", () => { 39 + const html = parseBlocks({ 40 + block: { 41 + $type: "pub.leaflet.pages.linearDocument#block", 42 + block: { 43 + $type: "pub.leaflet.blocks.header", 44 + level: 3, 45 + facets: [], 46 + plaintext: "heading 3", 47 + }, 48 + }, 49 + did: "did:plc:qttsv4e7pu2jl3ilanfgc3zn", 50 + }); 51 + 52 + expect(html).toMatchInlineSnapshot(`"<h4>heading 3</h4>"`); 53 + }); 54 + 55 + test("should correctly parse a block with no level to an h6 tag", () => { 56 + const html = parseBlocks({ 57 + block: { 58 + $type: "pub.leaflet.pages.linearDocument#block", 59 + block: { 60 + $type: "pub.leaflet.blocks.header", 61 + facets: [], 62 + plaintext: "heading 6", 63 + }, 64 + }, 65 + did: "did:plc:qttsv4e7pu2jl3ilanfgc3zn", 66 + }); 67 + 68 + expect(html).toMatchInlineSnapshot(`"<h6>heading 6</h6>"`); 69 + }); 70 + 71 + test("should correctly parse an unordered list block", () => { 72 + const html = parseBlocks({ 73 + block: { 74 + $type: "pub.leaflet.pages.linearDocument#block", 75 + block: { 76 + $type: "pub.leaflet.blocks.unorderedList", 77 + children: [ 78 + { 79 + $type: "pub.leaflet.blocks.unorderedList#listItem", 80 + content: { 81 + $type: "pub.leaflet.blocks.text", 82 + facets: [ 83 + { 84 + index: { 85 + byteEnd: 18, 86 + byteStart: 0, 87 + }, 88 + features: [ 89 + { 90 + uri: "https://pdsls.dev/", 91 + $type: "pub.leaflet.richtext.facet#link", 92 + }, 93 + ], 94 + }, 95 + { 96 + index: { 97 + byteEnd: 28, 98 + byteStart: 22, 99 + }, 100 + features: [ 101 + { 102 + uri: "https://bsky.app/profile/juli.ee", 103 + $type: "pub.leaflet.richtext.facet#link", 104 + }, 105 + ], 106 + }, 107 + ], 108 + plaintext: "https://pdsls.dev/ by Juliet", 109 + }, 110 + children: [], 111 + }, 112 + { 113 + $type: "pub.leaflet.blocks.unorderedList#listItem", 114 + content: { 115 + $type: "pub.leaflet.blocks.text", 116 + facets: [ 117 + { 118 + index: { 119 + byteEnd: 34, 120 + byteStart: 0, 121 + }, 122 + features: [ 123 + { 124 + uri: "https://github.com/mary-ext/atcute", 125 + $type: "pub.leaflet.richtext.facet#link", 126 + }, 127 + ], 128 + }, 129 + { 130 + index: { 131 + byteEnd: 42, 132 + byteStart: 38, 133 + }, 134 + features: [ 135 + { 136 + uri: "https://bsky.app/profile/mary.my.id", 137 + $type: "pub.leaflet.richtext.facet#link", 138 + }, 139 + ], 140 + }, 141 + ], 142 + plaintext: "https://github.com/mary-ext/atcute by mary", 143 + }, 144 + children: [], 145 + }, 146 + { 147 + $type: "pub.leaflet.blocks.unorderedList#listItem", 148 + content: { 149 + $type: "pub.leaflet.blocks.text", 150 + facets: [ 151 + { 152 + index: { 153 + byteEnd: 27, 154 + byteStart: 0, 155 + }, 156 + features: [ 157 + { 158 + uri: "https://www.microcosm.blue/", 159 + $type: "pub.leaflet.richtext.facet#link", 160 + }, 161 + ], 162 + }, 163 + { 164 + index: { 165 + byteEnd: 35, 166 + byteStart: 31, 167 + }, 168 + features: [ 169 + { 170 + uri: "https://bsky.app/profile/bad-example.com", 171 + $type: "pub.leaflet.richtext.facet#link", 172 + }, 173 + ], 174 + }, 175 + ], 176 + plaintext: "https://www.microcosm.blue/ by phil", 177 + }, 178 + children: [], 179 + }, 180 + ], 181 + }, 182 + }, 183 + did: "did:plc:qttsv4e7pu2jl3ilanfgc3zn", 184 + }); 185 + 186 + expect(html).toMatchInlineSnapshot( 187 + `"<ul><li><p><a href="https://pdsls.dev/" target="_blank" rel="noopener noreferrer">https://pdsls.dev/</a> by <a href="https://bsky.app/profile/juli.ee" target="_blank" rel="noopener noreferrer">Juliet</a></p></li><li><p><a href="https://github.com/mary-ext/atcute" target="_blank" rel="noopener noreferrer">https://github.com/mary-ext/atcute</a> by <a href="https://bsky.app/profile/mary.my.id" target="_blank" rel="noopener noreferrer">mary</a></p></li><li><p><a href="https://www.microcosm.blue/" target="_blank" rel="noopener noreferrer">https://www.microcosm.blue/</a> by <a href="https://bsky.app/profile/bad-example.com" target="_blank" rel="noopener noreferrer">phil</a></p></li></ul>"`, 188 + ); 189 + });
+133
tests/parse-text-blocks.test.ts
··· 1 + import { expect, test } from "vitest"; 2 + import { parseTextBlock } from "../lib/utils"; 3 + 4 + test("should correctly parse a text block without facets", () => { 5 + const html = parseTextBlock({ 6 + $type: "pub.leaflet.blocks.text", 7 + facets: [], 8 + plaintext: "just plaintext no facets", 9 + }); 10 + 11 + expect(html).toMatchInlineSnapshot(`"<p>just plaintext no facets</p>"`); 12 + }); 13 + 14 + test("should correctly parse a text block with bolded text", () => { 15 + const html = parseTextBlock({ 16 + $type: "pub.leaflet.blocks.text", 17 + facets: [ 18 + { 19 + index: { 20 + byteEnd: 11, 21 + byteStart: 0, 22 + }, 23 + features: [ 24 + { 25 + $type: "pub.leaflet.richtext.facet#bold", 26 + }, 27 + ], 28 + }, 29 + ], 30 + plaintext: "bolded text with some plaintext", 31 + }); 32 + 33 + expect(html).toMatchInlineSnapshot( 34 + `"<p><b>bolded text</b> with some plaintext</p>"`, 35 + ); 36 + }); 37 + 38 + test("should correctly parse a text block with an inline link", () => { 39 + const html = parseTextBlock({ 40 + $type: "pub.leaflet.blocks.text", 41 + facets: [ 42 + { 43 + index: { 44 + byteEnd: 27, 45 + byteStart: 0, 46 + }, 47 + features: [ 48 + { 49 + uri: "https://blacksky.community/", 50 + $type: "pub.leaflet.richtext.facet#link", 51 + }, 52 + ], 53 + }, 54 + ], 55 + plaintext: "https://blacksky.community/", 56 + }); 57 + 58 + expect(html).toMatchInlineSnapshot( 59 + `"<p><a href="https://blacksky.community/" target="_blank" rel="noopener noreferrer">https://blacksky.community/</a></p>"`, 60 + ); 61 + }); 62 + 63 + test("should correctly parse a text block with strikethrough text", () => { 64 + const html = parseTextBlock({ 65 + $type: "pub.leaflet.blocks.text", 66 + facets: [ 67 + { 68 + index: { 69 + byteEnd: 13, 70 + byteStart: 0, 71 + }, 72 + features: [ 73 + { 74 + $type: "pub.leaflet.richtext.facet#strikethrough", 75 + }, 76 + ], 77 + }, 78 + ], 79 + plaintext: "strikethrough text with some plaintext", 80 + }); 81 + 82 + expect(html).toMatchInlineSnapshot( 83 + `"<p><s>strikethrough</s> text with some plaintext</p>"`, 84 + ); 85 + }); 86 + 87 + test("should correctly parse a text block with underlined text", () => { 88 + const html = parseTextBlock({ 89 + $type: "pub.leaflet.blocks.text", 90 + facets: [ 91 + { 92 + index: { 93 + byteEnd: 10, 94 + byteStart: 0, 95 + }, 96 + features: [ 97 + { 98 + $type: "pub.leaflet.richtext.facet#underline", 99 + }, 100 + ], 101 + }, 102 + ], 103 + plaintext: "underlined text with some plaintext", 104 + }); 105 + 106 + expect(html).toMatchInlineSnapshot( 107 + `"<p><span style="text-decoration:underline;">underlined</span> text with some plaintext</p>"`, 108 + ); 109 + }); 110 + 111 + test("should correctly parse a text block with italicized text", () => { 112 + const html = parseTextBlock({ 113 + $type: "pub.leaflet.blocks.text", 114 + facets: [ 115 + { 116 + index: { 117 + byteEnd: 10, 118 + byteStart: 0, 119 + }, 120 + features: [ 121 + { 122 + $type: "pub.leaflet.richtext.facet#italic", 123 + }, 124 + ], 125 + }, 126 + ], 127 + plaintext: "italicized text with some plaintext", 128 + }); 129 + 130 + expect(html).toMatchInlineSnapshot( 131 + `"<p><i>italicized</i> text with some plaintext</p>"`, 132 + ); 133 + });
+4
tests/uri-to-rkey.test.ts
··· 12 12 ), 13 13 ).toBe("3lvl7m6jd4s2e"); 14 14 }); 15 + 16 + test("should not pass if invalid uri is passed in", () => { 17 + expect(() => uriToRkey("invalid")).toThrowError(/failed to get rkey/i); 18 + });