Compare changes

Choose any two refs to compare.

+5
.changeset/sweet-rockets-fetch.md
··· 1 + --- 2 + "@nulfrost/leaflet-loader-astro": patch 3 + --- 4 + 5 + Add JSDoc comments for available loader options for leafletStaticLoader and leafletLiveLoader
-5
.changeset/tasty-humans-swim.md
··· 1 - --- 2 - "leaflet-loader-astro": major 3 - --- 4 - 5 - This is the initial release for `leaflet-loader-astro`
+5
.changeset/tired-suits-double.md
··· 1 + --- 2 + "@nulfrost/leaflet-loader-astro": minor 3 + --- 4 + 5 + Add support for blockquotes
+1 -1
.github/workflows/ci.yml
··· 1 - name: Linting / Testing / Formatting 1 + name: CI 2 2 3 3 on: 4 4 push:
+34
CHANGELOG.md
··· 1 + # leaflet-loader-astro 2 + 3 + ## 1.1.0 4 + 5 + ### Minor Changes 6 + 7 + - 6d70cc6: Added support for these leaflet blocks: 8 + 9 + - ul/li 10 + - math 11 + - code 12 + - img 13 + - hr 14 + 15 + 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 16 + 17 + - 5524ce5: Added the ability to use a handle or did when specifying a repo for leafletStaticLoader and leafletLiveLoader 18 + 19 + ```ts 20 + import { defineLiveCollection } from "astro:content"; 21 + import { leafletLiveLoader } from "leaflet-loader-astro"; 22 + 23 + const documents = defineLiveCollection({ 24 + loader: leafletLiveLoader({ repo: "dane.computer" }), // or repo: did:plc:qttsv4e7pu2jl3ilanfgc3zn, both work! 25 + }); 26 + 27 + export const collections = { documents }; 28 + ``` 29 + 30 + ## 1.0.0 31 + 32 + ### Major Changes 33 + 34 + - b4309c0: This is the initial release for `leaflet-loader-astro`
+46 -7
README.md
··· 10 10 ## Installation 11 11 12 12 ```bash 13 - npm install leaflet-loader-astro 13 + npm install @nulfrost/leaflet-loader-astro 14 14 ``` 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 22 23 import { defineCollection, z } from "astro:content"; 23 - import { leafletStaticLoader } from "leaflet-loader-astro"; 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 ··· 101 104 ```ts 102 105 // src/live.config.ts 103 106 import { defineLiveCollection, z } from "astro:content"; 104 - import { leafletLiveLoader } from "leaflet-loader-astro"; 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 + 161 200 162 201 ## License 163 202
+1 -1
lex.config.js
··· 2 2 3 3 export default defineLexiconConfig({ 4 4 files: ["lexicons/**/*.json"], 5 - outdir: "src/lexicons/", 5 + outdir: "lib/lexicons/", 6 6 });
+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 + }
+11
lexicons/pub/leaflet/blocks/horizontalRule.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "pub.leaflet.blocks.horizontalRule", 4 + "defs": { 5 + "main": { 6 + "type": "object", 7 + "required": [], 8 + "properties": {} 9 + } 10 + } 11 + }
+24 -19
lib/leaflet-live-loader.ts
··· 1 - import { Agent } from "@atproto/api"; 2 - import { isDid } from "@atproto/did"; 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 2 + import { isHandle } from "@atcute/lexicons/syntax"; 3 3 import type { LiveLoader } from "astro/loaders"; 4 4 import type { 5 5 CollectionFilter, ··· 11 11 import { 12 12 getLeafletDocuments, 13 13 getSingleLeafletDocument, 14 + isPlcDid, 14 15 leafletBlocksToHTML, 15 16 leafletDocumentRecordToView, 16 17 LiveLoaderError, ··· 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 { 44 51 name: "leaflet-loader-astro", 45 52 loadCollection: async ({ filter }) => { 46 53 try { 47 - const pds_url = await resolveMiniDoc(repo); 48 - const agent = new Agent({ service: pds_url }); 54 + const { pds, did } = await resolveMiniDoc(repo); 55 + const handler = simpleFetchHandler({ service: pds }); 56 + const rpc = new Client({ handler }); 49 57 50 58 const { documents } = await getLeafletDocuments({ 51 - agent, 59 + rpc, 52 60 repo, 53 61 reverse: filter?.reverse, 54 62 cursor: filter?.cursor, ··· 67 75 }), 68 76 rendered: { 69 77 html: leafletBlocksToHTML({ 70 - id, 71 - uri: document.uri, 72 - cid: document.cid, 73 - value: document.value as unknown as LeafletDocumentRecord, 78 + record: document.value as unknown as LeafletDocumentRecord, 79 + did, 74 80 }), 75 81 }, 76 82 }; ··· 95 101 }; 96 102 } 97 103 try { 98 - const pds_url = await resolveMiniDoc(repo); 99 - const agent = new Agent({ service: pds_url }); 104 + const { pds, did } = await resolveMiniDoc(repo); 105 + const handler = simpleFetchHandler({ service: pds }); 106 + const rpc = new Client({ handler }); 100 107 const document = await getSingleLeafletDocument({ 101 - agent, 108 + rpc, 102 109 id: filter.id, 103 110 repo, 104 111 }); ··· 114 121 }), 115 122 rendered: { 116 123 html: leafletBlocksToHTML({ 117 - id: filter.id, 118 - uri: document.uri, 119 - cid, 120 - value: document.value as unknown as LeafletDocumentRecord, 124 + record: document.value as unknown as LeafletDocumentRecord, 125 + did, 121 126 }), 122 127 }, 123 128 };
+24 -17
lib/leaftlet-static-loader.ts
··· 1 - import { Agent } from "@atproto/api"; 2 - import { isDid } from "@atproto/did"; 1 + import { Client, simpleFetchHandler } from "@atcute/client"; 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 { ··· 7 7 StaticLeafletLoaderOptions, 8 8 } from "types.js"; 9 9 import { 10 + getLeafletDocuments, 11 + isPlcDid, 12 + leafletBlocksToHTML, 13 + leafletDocumentRecordToView, 10 14 LiveLoaderError, 11 15 resolveMiniDoc, 12 - getLeafletDocuments, 13 16 uriToRkey, 14 - leafletDocumentRecordToView, 15 - leafletBlocksToHTML, 16 17 } from "utils.js"; 17 18 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 { ··· 43 50 }: LoaderContext) => { 44 51 try { 45 52 logger.info("fetching latest leaflet documents"); 46 - const pds_url = await resolveMiniDoc(repo); 47 - const agent = new Agent({ service: pds_url }); 53 + const { pds, did } = await resolveMiniDoc(repo); 54 + const handler = simpleFetchHandler({ service: pds }); 55 + const rpc = new Client({ handler }); 48 56 49 57 let cursor: string | undefined; 50 58 let count = 0; ··· 52 60 fetching: do { 53 61 const { documents, cursor: documentsCursor } = 54 62 await getLeafletDocuments({ 55 - agent, 63 + rpc, 56 64 repo, 57 65 cursor, 58 - limit: 100, 66 + reverse, 67 + limit: 50, 59 68 }); 60 69 for (const document of documents) { 61 70 if (limit && count >= limit) { ··· 83 92 digest, 84 93 rendered: { 85 94 html: leafletBlocksToHTML({ 86 - id, 87 - uri: document.uri, 88 - cid: document.cid, 89 - value: document.value as unknown as LeafletDocumentRecord, 95 + record: document.value as unknown as LeafletDocumentRecord, 96 + did, 90 97 }), 91 98 }, 92 99 });
+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"; 5 + export * as PubLeafletBlocksHorizontalRule from "./types/pub/leaflet/blocks/horizontalRule.js"; 4 6 export * as PubLeafletBlocksImage from "./types/pub/leaflet/blocks/image.js"; 5 7 export * as PubLeafletBlocksMath from "./types/pub/leaflet/blocks/math.js"; 6 8 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> {}
+16
lib/lexicons/types/pub/leaflet/blocks/horizontalRule.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.horizontalRule"), 7 + ), 8 + }); 9 + 10 + type main$schematype = typeof _mainSchema; 11 + 12 + export interface mainSchema extends main$schematype {} 13 + 14 + export const mainSchema = _mainSchema as mainSchema; 15 + 16 + export interface Main extends v.InferInput<typeof mainSchema> {}
+21 -8
lib/types.ts
··· 1 - import type { Agent } from "@atproto/api"; 1 + import type { Client } from "@atcute/client"; 2 + import type { ActorIdentifier } from "@atcute/lexicons"; 3 + import type { XRPCProcedures, XRPCQueries } from "@atcute/lexicons/ambient"; 2 4 import type { PubLeafletRichtextFacet } from "./lexicons/index.js"; 3 5 4 6 export interface LiveLeafletLoaderOptions { 5 7 /** 6 - * @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 7 9 */ 8 10 repo: string; 9 11 } 10 12 11 13 export interface StaticLeafletLoaderOptions { 12 14 /** 13 - * @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 14 16 */ 15 17 repo: string; 16 - filter?: string; 17 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. 18 20 * @default 50 19 21 */ 20 22 limit?: number; 23 + /** 24 + * @description Whether or not the records should be returned in reverse order. 25 + * @default undefined 26 + */ 27 + reverse?: boolean; 21 28 } 22 29 23 30 export interface LeafletDocumentRecord { ··· 58 65 } 59 66 60 67 export interface GetLeafletDocumentsParams { 61 - repo: string; 62 - agent: Agent; 68 + repo: ActorIdentifier; 69 + rpc: Client<XRPCQueries, XRPCProcedures>; 63 70 cursor?: string; 64 71 limit?: number; 65 72 reverse?: boolean; 66 73 } 67 74 68 75 export interface GetSingleLeafletDocumentParams { 69 - repo: string; 70 - agent: Agent; 76 + repo: ActorIdentifier; 77 + rpc: Client<XRPCQueries, XRPCProcedures>; 71 78 id: string; 72 79 } 73 80 ··· 76 83 text: string; 77 84 facet?: Exclude<Facet["features"], { $type: string }>; 78 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}`;
+207 -107
lib/utils.ts
··· 1 + import type {} from "@atcute/atproto"; 1 2 import { is } from "@atcute/lexicons"; 2 3 import { AtUri, UnicodeString } from "@atproto/api"; 4 + import katex from "katex"; 3 5 import sanitizeHTML from "sanitize-html"; 4 6 import { 7 + PubLeafletBlocksBlockquote, 8 + PubLeafletBlocksCode, 5 9 PubLeafletBlocksHeader, 10 + PubLeafletBlocksHorizontalRule, 11 + PubLeafletBlocksImage, 12 + PubLeafletBlocksMath, 6 13 PubLeafletBlocksText, 14 + PubLeafletBlocksUnorderedList, 7 15 PubLeafletPagesLinearDocument, 8 16 } from "./lexicons/index.js"; 9 17 import type { 18 + Did, 10 19 Facet, 11 20 GetLeafletDocumentsParams, 12 21 GetSingleLeafletDocumentParams, ··· 29 38 export function uriToRkey(uri: string): string { 30 39 const u = AtUri.make(uri); 31 40 if (!u.rkey) { 32 - throw new Error("Failed to get rkey from uri."); 41 + throw new Error("failed to get rkey"); 33 42 } 34 43 return u.rkey; 35 44 } ··· 47 56 } 48 57 const data = (await response.json()) as MiniDoc; 49 58 50 - return data.pds; 59 + return { 60 + pds: data.pds, 61 + did: data.did, 62 + }; 51 63 } catch { 52 64 throw new Error(`failed to resolve handle: ${handleOrDid}`); 53 65 } ··· 57 69 repo, 58 70 reverse, 59 71 cursor, 60 - agent, 72 + rpc, 61 73 limit, 62 74 }: GetLeafletDocumentsParams) { 63 - const response = await agent.com.atproto.repo.listRecords({ 64 - repo, 65 - collection: "pub.leaflet.document", 66 - cursor, 67 - reverse, 68 - limit, 75 + const { ok, data } = await rpc.get("com.atproto.repo.listRecords", { 76 + params: { 77 + collection: "pub.leaflet.document", 78 + cursor, 79 + reverse, 80 + limit, 81 + repo, 82 + }, 69 83 }); 70 84 71 - if (response.success === false) { 85 + if (!ok) { 72 86 throw new LiveLoaderError( 73 87 "error fetching leaflet documents", 74 88 "DOCUMENT_FETCH_ERROR", ··· 76 90 } 77 91 78 92 return { 79 - documents: response?.data?.records, 80 - cursor: response?.data?.cursor, 93 + documents: data?.records, 94 + cursor: data?.cursor, 81 95 }; 82 96 } 83 97 84 98 export async function getSingleLeafletDocument({ 85 - agent, 99 + rpc, 86 100 repo, 87 101 id, 88 102 }: GetSingleLeafletDocumentParams) { 89 - const response = await agent.com.atproto.repo.getRecord({ 90 - repo, 91 - collection: "pub.leaflet.document", 92 - rkey: id, 103 + const { ok, data } = await rpc.get("com.atproto.repo.getRecord", { 104 + params: { 105 + collection: "pub.leaflet.document", 106 + repo, 107 + rkey: id, 108 + }, 93 109 }); 94 110 95 - if (response.success === false) { 111 + if (!ok) { 96 112 throw new LiveLoaderError( 97 113 "error fetching single document", 98 114 "DOCUMENT_FETCH_ERROR", 99 115 ); 100 116 } 101 117 102 - return response?.data; 118 + return data; 103 119 } 104 120 105 121 export function leafletDocumentRecordToView({ ··· 122 138 }; 123 139 } 124 140 125 - export function leafletBlocksToHTML(record: { 126 - id: string; 127 - uri: string; 128 - cid: string; 129 - value: LeafletDocumentRecord; 141 + export function leafletBlocksToHTML({ 142 + record, 143 + did, 144 + }: { 145 + record: LeafletDocumentRecord; 146 + did: string; 130 147 }) { 131 148 let html = ""; 132 - const firstPage = record.value.pages[0]; 149 + const firstPage = record.pages[0]; 133 150 let blocks: PubLeafletPagesLinearDocument.Block[] = []; 151 + 134 152 if (is(PubLeafletPagesLinearDocument.mainSchema, firstPage)) { 135 153 blocks = firstPage.blocks || []; 136 154 } 137 155 138 156 for (const block of blocks) { 139 - if (is(PubLeafletBlocksText.mainSchema, block.block)) { 140 - const rt = new RichText({ 141 - text: block.block.plaintext, 142 - facets: block.block.facets || [], 143 - }); 144 - const children = []; 145 - for (const segment of rt.segments()) { 146 - const link = segment.facet?.find( 147 - (segment) => segment.$type === "pub.leaflet.richtext.facet#link", 148 - ); 149 - const isBold = segment.facet?.find( 150 - (segment) => segment.$type === "pub.leaflet.richtext.facet#bold", 151 - ); 152 - const isCode = segment.facet?.find( 153 - (segment) => segment.$type === "pub.leaflet.richtext.facet#code", 154 - ); 155 - const isStrikethrough = segment.facet?.find( 156 - (segment) => 157 - segment.$type === "pub.leaflet.richtext.facet#strikethrough", 158 - ); 159 - const isUnderline = segment.facet?.find( 160 - (segment) => segment.$type === "pub.leaflet.richtext.facet#underline", 161 - ); 162 - const isItalic = segment.facet?.find( 163 - (segment) => segment.$type === "pub.leaflet.richtext.facet#italic", 164 - ); 165 - if (isCode) { 166 - children.push(` <code> 167 - ${segment.text} 168 - </code>`); 169 - } else if (link) { 170 - children.push( 171 - ` <a 172 - href="${link.uri}" 173 - target="_blank" 174 - > 175 - ${segment.text} 176 - </a>`, 177 - ); 178 - } else if (isBold) { 179 - children.push(`<b>${segment.text}</b>`); 180 - } else if (isStrikethrough) { 181 - children.push(`<s>${segment.text}</s>`); 182 - } else if (isUnderline) { 183 - children.push( 184 - `<span style="text-decoration:underline;">${segment.text}</span>`, 185 - ); 186 - } else if (isItalic) { 187 - children.push(`<i>${segment.text}</i>`); 188 - } else { 189 - children.push( 190 - ` 191 - ${segment.text} 192 - `, 193 - ); 194 - } 195 - } 196 - html += `<p>${children.join("\n")}</p>`; 197 - } 198 - 199 - if (is(PubLeafletBlocksHeader.mainSchema, block.block)) { 200 - if (block.block.level === 1) { 201 - html += `<h2>${block.block.plaintext}</h2>`; 202 - } 203 - } 204 - if (is(PubLeafletBlocksHeader.mainSchema, block.block)) { 205 - if (block.block.level === 2) { 206 - html += `<h3>${block.block.plaintext}</h3>`; 207 - } 208 - } 209 - if (is(PubLeafletBlocksHeader.mainSchema, block.block)) { 210 - if (block.block.level === 3) { 211 - html += `<h4>${block.block.plaintext}</h4>`; 212 - } 213 - } 214 - if (is(PubLeafletBlocksHeader.mainSchema, block.block)) { 215 - if (!block.block.level) { 216 - html += `<h6>${block.block.plaintext}</h6>`; 217 - } 218 - } 157 + html += parseBlocks({ block, did }); 219 158 } 220 159 221 - return sanitizeHTML(html); 160 + return sanitizeHTML(html, { 161 + allowedAttributes: { 162 + "*": ["class", "style"], 163 + img: ["src", "height", "width", "alt"], 164 + a: ["href", "target", "rel"], 165 + }, 166 + allowedTags: [ 167 + "img", 168 + "pre", 169 + "code", 170 + "p", 171 + "a", 172 + "b", 173 + "s", 174 + "ul", 175 + "li", 176 + "i", 177 + "h1", 178 + "h2", 179 + "h3", 180 + "h4", 181 + "h5", 182 + "h6", 183 + "hr", 184 + "div", 185 + "span", 186 + "blockquote", 187 + ], 188 + selfClosing: ["img"], 189 + }); 222 190 } 223 191 224 192 export class RichText { 225 193 unicodeText: UnicodeString; 226 194 facets?: Facet[]; 227 - 228 195 constructor(props: { text: string; facets: Facet[] }) { 229 196 this.unicodeText = new UnicodeString(props.text); 230 197 this.facets = props.facets; ··· 278 245 } 279 246 } 280 247 } 248 + 249 + export function parseTextBlock(block: PubLeafletBlocksText.Main) { 250 + let html = ""; 251 + const rt = new RichText({ 252 + text: block.plaintext, 253 + facets: block.facets || [], 254 + }); 255 + const children = []; 256 + for (const segment of rt.segments()) { 257 + const link = segment.facet?.find( 258 + (segment) => segment.$type === "pub.leaflet.richtext.facet#link", 259 + ); 260 + const isBold = segment.facet?.find( 261 + (segment) => segment.$type === "pub.leaflet.richtext.facet#bold", 262 + ); 263 + const isCode = segment.facet?.find( 264 + (segment) => segment.$type === "pub.leaflet.richtext.facet#code", 265 + ); 266 + const isStrikethrough = segment.facet?.find( 267 + (segment) => segment.$type === "pub.leaflet.richtext.facet#strikethrough", 268 + ); 269 + const isUnderline = segment.facet?.find( 270 + (segment) => segment.$type === "pub.leaflet.richtext.facet#underline", 271 + ); 272 + const isItalic = segment.facet?.find( 273 + (segment) => segment.$type === "pub.leaflet.richtext.facet#italic", 274 + ); 275 + if (isCode) { 276 + children.push(`<pre><code>${segment.text}</code></pre>`); 277 + } else if (link) { 278 + children.push( 279 + `<a href="${link.uri}" target="_blank" rel="noopener noreferrer">${segment.text}</a>`, 280 + ); 281 + } else if (isBold) { 282 + children.push(`<b>${segment.text}</b>`); 283 + } else if (isStrikethrough) { 284 + children.push(`<s>${segment.text}</s>`); 285 + } else if (isUnderline) { 286 + children.push( 287 + `<span style="text-decoration:underline;">${segment.text}</span>`, 288 + ); 289 + } else if (isItalic) { 290 + children.push(`<i>${segment.text}</i>`); 291 + } else { 292 + children.push(`${segment.text}`); 293 + } 294 + } 295 + html += `<p>${children.join("")}</p>`; 296 + 297 + return html.trim(); 298 + } 299 + 300 + export function parseBlocks({ 301 + block, 302 + did, 303 + }: { 304 + block: PubLeafletPagesLinearDocument.Block; 305 + did: string; 306 + }): string { 307 + let html = ""; 308 + 309 + if (is(PubLeafletBlocksText.mainSchema, block.block)) { 310 + html += parseTextBlock(block.block); 311 + } 312 + 313 + if (is(PubLeafletBlocksHeader.mainSchema, block.block)) { 314 + if (block.block.level === 1) { 315 + html += `<h2>${block.block.plaintext}</h2>`; 316 + } 317 + } 318 + if (is(PubLeafletBlocksHeader.mainSchema, block.block)) { 319 + if (block.block.level === 2) { 320 + html += `<h3>${block.block.plaintext}</h3>`; 321 + } 322 + } 323 + if (is(PubLeafletBlocksHeader.mainSchema, block.block)) { 324 + if (block.block.level === 3) { 325 + html += `<h4>${block.block.plaintext}</h4>`; 326 + } 327 + } 328 + if (is(PubLeafletBlocksHeader.mainSchema, block.block)) { 329 + if (!block.block.level) { 330 + html += `<h6>${block.block.plaintext}</h6>`; 331 + } 332 + } 333 + 334 + if (is(PubLeafletBlocksHorizontalRule.mainSchema, block.block)) { 335 + html += `<hr />`; 336 + } 337 + if (is(PubLeafletBlocksUnorderedList.mainSchema, block.block)) { 338 + html += `<ul>${block.block.children.map((child) => renderListItem({ item: child, did })).join("")}</ul>`; 339 + } 340 + 341 + if (is(PubLeafletBlocksMath.mainSchema, block.block)) { 342 + html += `<div>${katex.renderToString(block.block.tex, { displayMode: true, output: "html", throwOnError: false })}</div>`; 343 + } 344 + 345 + if (is(PubLeafletBlocksCode.mainSchema, block.block)) { 346 + html += `<pre><code data-language=${block.block.language}>${block.block.plaintext}</code></pre>`; 347 + } 348 + 349 + if (is(PubLeafletBlocksImage.mainSchema, block.block)) { 350 + // @ts-ignore 351 + 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>`; 352 + } 353 + 354 + if (is(PubLeafletBlocksBlockquote.mainSchema, block.block)) { 355 + html += `<blockquote>${parseTextBlock(block.block)}</blockquote>`; 356 + } 357 + 358 + return html.trim(); 359 + } 360 + 361 + export function renderListItem({ 362 + item, 363 + did, 364 + }: { 365 + item: PubLeafletBlocksUnorderedList.ListItem; 366 + did: string; 367 + }): string { 368 + const children: string | null = item.children?.length 369 + ? `<ul>${item.children.map((child) => renderListItem({ item: child, did }))}</ul>` 370 + : ""; 371 + 372 + return `<li>${parseBlocks({ block: { block: item.content }, did })}${children}</li>`; 373 + } 374 + 375 + // yoinked from: https://github.com/mary-ext/atcute/blob/trunk/packages/lexicons/lexicons/lib/syntax/handle.ts 376 + const PLC_DID_RE = /^did:plc:([a-z2-7]{24})$/; 377 + 378 + export const isPlcDid = (input: string): input is Did<"plc"> => { 379 + return input.length === 32 && PLC_DID_RE.test(input); 380 + };
+13 -6
package.json
··· 1 1 { 2 - "name": "leaflet-loader-astro", 3 - "version": "0.0.1", 2 + "name": "@nulfrost/leaflet-loader-astro", 3 + "version": "1.1.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" ··· 23 23 "lex": "lex-cli generate -c ./lex.config.js", 24 24 "test": "vitest --run", 25 25 "typecheck": "tsc", 26 + "release": "pnpm run build && changeset publish", 26 27 "build": "rm -rf dist && tsup --format esm --dts", 27 - "pack": "pnpm build && pnpm pack" 28 + "pack": "rm -rf *.tgz && pnpm build && pnpm pack" 28 29 }, 29 30 "license": "MIT", 30 31 "files": [ 31 32 "dist" 32 33 ], 34 + "publishConfig": { 35 + "access": "public" 36 + }, 33 37 "type": "module", 34 38 "main": "dist/index.js", 35 39 "module": "dist/index.js", ··· 41 45 } 42 46 }, 43 47 "devDependencies": { 48 + "@atcute/atproto": "^3.1.1", 44 49 "@atcute/lex-cli": "^2.1.1", 45 50 "@biomejs/biome": "2.1.3", 46 51 "@changesets/cli": "^2.29.5", 52 + "@types/bun": "^1.2.19", 47 53 "@types/sanitize-html": "^2.16.0", 48 54 "astro": "^5.12.8", 49 55 "tsup": "^8.5.0", ··· 51 57 "vitest": "^3.2.4" 52 58 }, 53 59 "dependencies": { 60 + "@atcute/client": "^4.0.3", 54 61 "@atcute/lexicons": "^1.1.0", 55 - "@atproto/api": "^0.16.1", 56 - "@atproto/did": "^0.1.5", 62 + "@atproto/api": "^0.16.2", 63 + "katex": "^0.16.22", 57 64 "sanitize-html": "^2.17.0" 58 65 } 59 66 }
+118 -48
pnpm-lock.yaml
··· 8 8 9 9 .: 10 10 dependencies: 11 + '@atcute/client': 12 + specifier: ^4.0.3 13 + version: 4.0.3 11 14 '@atcute/lexicons': 12 15 specifier: ^1.1.0 13 16 version: 1.1.0 14 17 '@atproto/api': 15 - specifier: ^0.16.1 16 - version: 0.16.1 17 - '@atproto/did': 18 - specifier: ^0.1.5 19 - version: 0.1.5 18 + specifier: ^0.16.2 19 + version: 0.16.2 20 + katex: 21 + specifier: ^0.16.22 22 + version: 0.16.22 20 23 sanitize-html: 21 24 specifier: ^2.17.0 22 25 version: 2.17.0 23 26 devDependencies: 27 + '@atcute/atproto': 28 + specifier: ^3.1.1 29 + version: 3.1.1 24 30 '@atcute/lex-cli': 25 31 specifier: ^2.1.1 26 32 version: 2.1.1 ··· 30 36 '@changesets/cli': 31 37 specifier: ^2.29.5 32 38 version: 2.29.5 39 + '@types/bun': 40 + specifier: ^1.2.19 41 + version: 1.2.19(@types/react@19.1.9) 33 42 '@types/sanitize-html': 34 43 specifier: ^2.16.0 35 44 version: 2.16.0 36 45 astro: 37 46 specifier: ^5.12.8 38 - version: 5.12.8(@types/node@24.2.0)(rollup@4.46.2)(typescript@5.9.2) 47 + version: 5.12.9(@types/node@24.2.1)(rollup@4.46.2)(typescript@5.9.2) 39 48 tsup: 40 49 specifier: ^8.5.0 41 50 version: 8.5.0(postcss@8.5.6)(typescript@5.9.2) ··· 44 53 version: 5.9.2 45 54 vitest: 46 55 specifier: ^3.2.4 47 - version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.0) 56 + version: 3.2.4(@types/debug@4.1.12)(@types/node@24.2.1) 48 57 49 58 packages: 50 59 ··· 65 74 resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==} 66 75 engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} 67 76 77 + '@atcute/atproto@3.1.1': 78 + resolution: {integrity: sha512-D+RLTIPF0xLu7BPZY8KSewAPemJFh+3n3zeQ3ROsLxbTtCHbrTDMAmAFexaVRAPGcPYrwXaBUlv7yZjScJolMg==} 79 + 80 + '@atcute/client@4.0.3': 81 + resolution: {integrity: sha512-RIOZWFVLca/HiPAAUDqQPOdOreCxTbL5cb+WUf5yqQOKIu5yEAP3eksinmlLmgIrlr5qVOE7brazUUzaskFCfw==} 82 + 83 + '@atcute/identity@1.0.3': 84 + resolution: {integrity: sha512-mNMxbKHFGys03A8JXKk0KfMBzdd0vrYMzZZWjpw1nYTs0+ea6bo5S1hwqVUZxHdo1gFHSe/t63jxQIF4yL9aKw==} 85 + 68 86 '@atcute/lex-cli@2.1.1': 69 87 resolution: {integrity: sha512-QaR0sOP8Z24opGHKsSfleDbP/ahUb6HECkVaOqSwG7ORZzbLK1w0265o1BRjCVr2dT6FxlsMUa2Ge85JMA9bxg==} 70 88 hasBin: true ··· 75 93 '@atcute/lexicons@1.1.0': 76 94 resolution: {integrity: sha512-LFqwnria78xLYb62Ri/+WwQpUTgZp2DuyolNGIIOV1dpiKhFFFh//nscHMA6IExFLQRqWDs3tTjy7zv0h3sf1Q==} 77 95 78 - '@atproto/api@0.16.1': 79 - resolution: {integrity: sha512-w48BlTmzKym7nZETWxgiuUX/wwRXU3xsLLKORWo/xtGnwlvpchUFnHKI3k4ttYJ2/JQE59+/4C16BaLzDyiU2w==} 96 + '@atproto/api@0.16.2': 97 + resolution: {integrity: sha512-sSTg31J8ws8DNaoiizp+/uJideRxRaJsq+Nyl8rnSxGw0w3oCvoeRU19iRWh2t0jZEmiRJAGkveGu23NKmPYEQ==} 80 98 81 99 '@atproto/common-web@0.4.2': 82 100 resolution: {integrity: sha512-vrXwGNoFGogodjQvJDxAeP3QbGtawgZute2ed1XdRO0wMixLk3qewtikZm06H259QDJVu6voKC5mubml+WgQUw==} 83 - 84 - '@atproto/did@0.1.5': 85 - resolution: {integrity: sha512-8+1D08QdGE5TF0bB0vV8HLVrVZJeLNITpRTUVEoABNMRaUS7CoYSVb0+JNQDeJIVmqMjOL8dOjvCUDkp3gEaGQ==} 86 101 87 102 '@atproto/lexicon@0.4.12': 88 103 resolution: {integrity: sha512-fcEvEQ1GpQYF5igZ4IZjPWEoWVpsEF22L9RexxLS3ptfySXLflEyH384e7HITzO/73McDeaJx3lqHIuqn9ulnw==} ··· 672 687 673 688 '@swc/helpers@0.5.17': 674 689 resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} 690 + 691 + '@types/bun@1.2.19': 692 + resolution: {integrity: sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg==} 675 693 676 694 '@types/chai@5.2.2': 677 695 resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} ··· 703 721 '@types/node@12.20.55': 704 722 resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} 705 723 706 - '@types/node@24.2.0': 707 - resolution: {integrity: sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw==} 724 + '@types/node@24.2.1': 725 + resolution: {integrity: sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==} 726 + 727 + '@types/react@19.1.9': 728 + resolution: {integrity: sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==} 708 729 709 730 '@types/sanitize-html@2.16.0': 710 731 resolution: {integrity: sha512-l6rX1MUXje5ztPT0cAFtUayXF06DqPhRyfVXareEN5gGCFaP/iwsxIyKODr9XDhfxPpN6vXUFNfo5kZMXCxBtw==} ··· 800 821 resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} 801 822 engines: {node: '>=12'} 802 823 803 - astro@5.12.8: 804 - resolution: {integrity: sha512-KkJ7FR+c2SyZYlpakm48XBiuQcRsrVtdjG5LN5an0givI/tLik+ePJ4/g3qrAVhYMjJOxBA2YgFQxANPiWB+Mw==} 824 + astro@5.12.9: 825 + resolution: {integrity: sha512-cZ7kZ61jyE5nwSrFKSRyf5Gds+uJELqQxJFqMkcgiWQvhWZJUSShn8Uz3yc9WLyLw5Kim5P5un9SkJSGogfEZQ==} 805 826 engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} 806 827 hasBin: true 807 828 ··· 845 866 brotli@1.3.3: 846 867 resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} 847 868 869 + bun-types@1.2.19: 870 + resolution: {integrity: sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ==} 871 + peerDependencies: 872 + '@types/react': ^19 873 + 848 874 bundle-require@5.1.0: 849 875 resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} 850 876 engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} ··· 931 957 resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 932 958 engines: {node: '>= 6'} 933 959 960 + commander@8.3.0: 961 + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} 962 + engines: {node: '>= 12'} 963 + 934 964 common-ancestor-path@1.0.1: 935 965 resolution: {integrity: sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==} 936 966 ··· 967 997 engines: {node: '>=4'} 968 998 hasBin: true 969 999 1000 + csstype@3.1.3: 1001 + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 1002 + 970 1003 debug@4.4.1: 971 1004 resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} 972 1005 engines: {node: '>=6.0'} ··· 1336 1369 1337 1370 jsonfile@4.0.0: 1338 1371 resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} 1372 + 1373 + katex@0.16.22: 1374 + resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} 1375 + hasBin: true 1339 1376 1340 1377 kleur@3.0.3: 1341 1378 resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} ··· 1876 1913 resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} 1877 1914 engines: {node: '>=8'} 1878 1915 1879 - smol-toml@1.4.1: 1880 - resolution: {integrity: sha512-CxdwHXyYTONGHThDbq5XdwbFsuY4wlClRGejfE2NtwUtiHYsP1QtNsHb/hnj31jKYSchztJsaA8pSQoVzkfCFg==} 1916 + smol-toml@1.4.2: 1917 + resolution: {integrity: sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==} 1881 1918 engines: {node: '>= 18'} 1882 1919 1883 1920 source-map-js@1.2.1: ··· 2218 2255 yaml: 2219 2256 optional: true 2220 2257 2221 - vite@7.0.6: 2222 - resolution: {integrity: sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg==} 2258 + vite@7.1.1: 2259 + resolution: {integrity: sha512-yJ+Mp7OyV+4S+afWo+QyoL9jFWD11QFH0i5i7JypnfTcA1rmgxCbiA8WwAICDEtZ1Z1hzrVhN8R8rGTqkTY8ZQ==} 2223 2260 engines: {node: ^20.19.0 || >=22.12.0} 2224 2261 hasBin: true 2225 2262 peerDependencies: ··· 2398 2435 remark-rehype: 11.1.2 2399 2436 remark-smartypants: 3.0.2 2400 2437 shiki: 3.9.2 2401 - smol-toml: 1.4.1 2438 + smol-toml: 1.4.2 2402 2439 unified: 11.0.5 2403 2440 unist-util-remove-position: 5.0.0 2404 2441 unist-util-visit: 5.0.0 ··· 2423 2460 transitivePeerDependencies: 2424 2461 - supports-color 2425 2462 2463 + '@atcute/atproto@3.1.1': 2464 + dependencies: 2465 + '@atcute/lexicons': 1.1.0 2466 + 2467 + '@atcute/client@4.0.3': 2468 + dependencies: 2469 + '@atcute/identity': 1.0.3 2470 + '@atcute/lexicons': 1.1.0 2471 + 2472 + '@atcute/identity@1.0.3': 2473 + dependencies: 2474 + '@atcute/lexicons': 1.1.0 2475 + '@badrap/valita': 0.4.6 2476 + 2426 2477 '@atcute/lex-cli@2.1.1': 2427 2478 dependencies: 2428 2479 '@atcute/lexicon-doc': 1.0.3 ··· 2439 2490 dependencies: 2440 2491 esm-env: 1.2.2 2441 2492 2442 - '@atproto/api@0.16.1': 2493 + '@atproto/api@0.16.2': 2443 2494 dependencies: 2444 2495 '@atproto/common-web': 0.4.2 2445 2496 '@atproto/lexicon': 0.4.12 ··· 2455 2506 graphemer: 1.4.0 2456 2507 multiformats: 9.9.0 2457 2508 uint8arrays: 3.0.0 2458 - zod: 3.25.76 2459 - 2460 - '@atproto/did@0.1.5': 2461 - dependencies: 2462 2509 zod: 3.25.76 2463 2510 2464 2511 '@atproto/lexicon@0.4.12': ··· 3001 3048 dependencies: 3002 3049 tslib: 2.8.1 3003 3050 3051 + '@types/bun@1.2.19(@types/react@19.1.9)': 3052 + dependencies: 3053 + bun-types: 1.2.19(@types/react@19.1.9) 3054 + transitivePeerDependencies: 3055 + - '@types/react' 3056 + 3004 3057 '@types/chai@5.2.2': 3005 3058 dependencies: 3006 3059 '@types/deep-eql': 4.0.2 ··· 3015 3068 3016 3069 '@types/fontkit@2.0.8': 3017 3070 dependencies: 3018 - '@types/node': 24.2.0 3071 + '@types/node': 24.2.1 3019 3072 3020 3073 '@types/hast@3.0.4': 3021 3074 dependencies: ··· 3033 3086 3034 3087 '@types/node@12.20.55': {} 3035 3088 3036 - '@types/node@24.2.0': 3089 + '@types/node@24.2.1': 3037 3090 dependencies: 3038 3091 undici-types: 7.10.0 3039 3092 3093 + '@types/react@19.1.9': 3094 + dependencies: 3095 + csstype: 3.1.3 3096 + 3040 3097 '@types/sanitize-html@2.16.0': 3041 3098 dependencies: 3042 3099 htmlparser2: 8.0.2 ··· 3053 3110 chai: 5.2.1 3054 3111 tinyrainbow: 2.0.0 3055 3112 3056 - '@vitest/mocker@3.2.4(vite@7.0.6(@types/node@24.2.0))': 3113 + '@vitest/mocker@3.2.4(vite@7.1.1(@types/node@24.2.1))': 3057 3114 dependencies: 3058 3115 '@vitest/spy': 3.2.4 3059 3116 estree-walker: 3.0.3 3060 3117 magic-string: 0.30.17 3061 3118 optionalDependencies: 3062 - vite: 7.0.6(@types/node@24.2.0) 3119 + vite: 7.1.1(@types/node@24.2.1) 3063 3120 3064 3121 '@vitest/pretty-format@3.2.4': 3065 3122 dependencies: ··· 3126 3183 3127 3184 assertion-error@2.0.1: {} 3128 3185 3129 - astro@5.12.8(@types/node@24.2.0)(rollup@4.46.2)(typescript@5.9.2): 3186 + astro@5.12.9(@types/node@24.2.1)(rollup@4.46.2)(typescript@5.9.2): 3130 3187 dependencies: 3131 3188 '@astrojs/compiler': 2.12.2 3132 3189 '@astrojs/internal-helpers': 0.7.1 ··· 3173 3230 rehype: 13.0.2 3174 3231 semver: 7.7.2 3175 3232 shiki: 3.9.2 3176 - smol-toml: 1.4.1 3233 + smol-toml: 1.4.2 3177 3234 tinyexec: 0.3.2 3178 3235 tinyglobby: 0.2.14 3179 3236 tsconfck: 3.1.6(typescript@5.9.2) ··· 3182 3239 unist-util-visit: 5.0.0 3183 3240 unstorage: 1.16.1 3184 3241 vfile: 6.0.3 3185 - vite: 6.3.5(@types/node@24.2.0) 3186 - vitefu: 1.1.1(vite@6.3.5(@types/node@24.2.0)) 3242 + vite: 6.3.5(@types/node@24.2.1) 3243 + vitefu: 1.1.1(vite@6.3.5(@types/node@24.2.1)) 3187 3244 xxhash-wasm: 1.1.0 3188 3245 yargs-parser: 21.1.1 3189 3246 yocto-spinner: 0.2.3 ··· 3268 3325 dependencies: 3269 3326 base64-js: 1.5.1 3270 3327 3328 + bun-types@1.2.19(@types/react@19.1.9): 3329 + dependencies: 3330 + '@types/node': 24.2.1 3331 + '@types/react': 19.1.9 3332 + 3271 3333 bundle-require@5.1.0(esbuild@0.25.8): 3272 3334 dependencies: 3273 3335 esbuild: 0.25.8 ··· 3335 3397 3336 3398 commander@4.1.1: {} 3337 3399 3400 + commander@8.3.0: {} 3401 + 3338 3402 common-ancestor-path@1.0.1: {} 3339 3403 3340 3404 confbox@0.1.8: {} ··· 3367 3431 source-map-js: 1.2.1 3368 3432 3369 3433 cssesc@3.0.0: {} 3434 + 3435 + csstype@3.1.3: {} 3370 3436 3371 3437 debug@4.4.1: 3372 3438 dependencies: ··· 3793 3859 optionalDependencies: 3794 3860 graceful-fs: 4.2.11 3795 3861 3862 + katex@0.16.22: 3863 + dependencies: 3864 + commander: 8.3.0 3865 + 3796 3866 kleur@3.0.3: {} 3797 3867 3798 3868 kleur@4.1.5: {} ··· 4550 4620 4551 4621 slash@3.0.0: {} 4552 4622 4553 - smol-toml@1.4.1: {} 4623 + smol-toml@1.4.2: {} 4554 4624 4555 4625 source-map-js@1.2.1: {} 4556 4626 ··· 4815 4885 '@types/unist': 3.0.3 4816 4886 vfile-message: 4.0.3 4817 4887 4818 - vite-node@3.2.4(@types/node@24.2.0): 4888 + vite-node@3.2.4(@types/node@24.2.1): 4819 4889 dependencies: 4820 4890 cac: 6.7.14 4821 4891 debug: 4.4.1 4822 4892 es-module-lexer: 1.7.0 4823 4893 pathe: 2.0.3 4824 - vite: 7.0.6(@types/node@24.2.0) 4894 + vite: 7.1.1(@types/node@24.2.1) 4825 4895 transitivePeerDependencies: 4826 4896 - '@types/node' 4827 4897 - jiti ··· 4836 4906 - tsx 4837 4907 - yaml 4838 4908 4839 - vite@6.3.5(@types/node@24.2.0): 4909 + vite@6.3.5(@types/node@24.2.1): 4840 4910 dependencies: 4841 4911 esbuild: 0.25.8 4842 4912 fdir: 6.4.6(picomatch@4.0.3) ··· 4845 4915 rollup: 4.46.2 4846 4916 tinyglobby: 0.2.14 4847 4917 optionalDependencies: 4848 - '@types/node': 24.2.0 4918 + '@types/node': 24.2.1 4849 4919 fsevents: 2.3.3 4850 4920 4851 - vite@7.0.6(@types/node@24.2.0): 4921 + vite@7.1.1(@types/node@24.2.1): 4852 4922 dependencies: 4853 4923 esbuild: 0.25.8 4854 4924 fdir: 6.4.6(picomatch@4.0.3) ··· 4857 4927 rollup: 4.46.2 4858 4928 tinyglobby: 0.2.14 4859 4929 optionalDependencies: 4860 - '@types/node': 24.2.0 4930 + '@types/node': 24.2.1 4861 4931 fsevents: 2.3.3 4862 4932 4863 - vitefu@1.1.1(vite@6.3.5(@types/node@24.2.0)): 4933 + vitefu@1.1.1(vite@6.3.5(@types/node@24.2.1)): 4864 4934 optionalDependencies: 4865 - vite: 6.3.5(@types/node@24.2.0) 4935 + vite: 6.3.5(@types/node@24.2.1) 4866 4936 4867 - vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.2.0): 4937 + vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.2.1): 4868 4938 dependencies: 4869 4939 '@types/chai': 5.2.2 4870 4940 '@vitest/expect': 3.2.4 4871 - '@vitest/mocker': 3.2.4(vite@7.0.6(@types/node@24.2.0)) 4941 + '@vitest/mocker': 3.2.4(vite@7.1.1(@types/node@24.2.1)) 4872 4942 '@vitest/pretty-format': 3.2.4 4873 4943 '@vitest/runner': 3.2.4 4874 4944 '@vitest/snapshot': 3.2.4 ··· 4886 4956 tinyglobby: 0.2.14 4887 4957 tinypool: 1.1.1 4888 4958 tinyrainbow: 2.0.0 4889 - vite: 7.0.6(@types/node@24.2.0) 4890 - vite-node: 3.2.4(@types/node@24.2.0) 4959 + vite: 7.1.1(@types/node@24.2.1) 4960 + vite-node: 3.2.4(@types/node@24.2.1) 4891 4961 why-is-node-running: 2.3.0 4892 4962 optionalDependencies: 4893 4963 '@types/debug': 4.1.12 4894 - '@types/node': 24.2.0 4964 + '@types/node': 24.2.1 4895 4965 transitivePeerDependencies: 4896 4966 - jiti 4897 4967 - less
-5
pnpm-workspace.yaml
··· 1 - ignoredBuiltDependencies: 2 - - sharp 3 - 4 - onlyBuiltDependencies: 5 - - esbuild
+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 + });