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
+16
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 + 3 19 ## 1.1.0 4 20 5 21 ### Minor Changes
+14
README.md
··· 197 197 198 198 `reverse`: Whether or not to return the leaflet documents in reverse order. By default this is false. 199 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 200 214 201 215 ## License 202 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 + }
+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 });
+68 -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, ··· 161 163 "*": ["class", "style"], 162 164 img: ["src", "height", "width", "alt"], 163 165 a: ["href", "target", "rel"], 166 + iframe: ["height", "allow", "loading", "src"], 164 167 }, 165 168 allowedTags: [ 166 169 "img", ··· 182 185 "hr", 183 186 "div", 184 187 "span", 188 + "blockquote", 189 + "iframe", 185 190 ], 186 191 selfClosing: ["img"], 187 192 }); ··· 244 249 } 245 250 } 246 251 247 - 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({ 248 304 block, 249 305 did, 250 306 }: { ··· 254 310 let html = ""; 255 311 256 312 if (is(PubLeafletBlocksText.mainSchema, block.block)) { 257 - const rt = new RichText({ 258 - text: block.block.plaintext, 259 - facets: block.block.facets || [], 260 - }); 261 - const children = []; 262 - for (const segment of rt.segments()) { 263 - const link = segment.facet?.find( 264 - (segment) => segment.$type === "pub.leaflet.richtext.facet#link", 265 - ); 266 - const isBold = segment.facet?.find( 267 - (segment) => segment.$type === "pub.leaflet.richtext.facet#bold", 268 - ); 269 - const isCode = segment.facet?.find( 270 - (segment) => segment.$type === "pub.leaflet.richtext.facet#code", 271 - ); 272 - const isStrikethrough = segment.facet?.find( 273 - (segment) => 274 - segment.$type === "pub.leaflet.richtext.facet#strikethrough", 275 - ); 276 - const isUnderline = segment.facet?.find( 277 - (segment) => segment.$type === "pub.leaflet.richtext.facet#underline", 278 - ); 279 - const isItalic = segment.facet?.find( 280 - (segment) => segment.$type === "pub.leaflet.richtext.facet#italic", 281 - ); 282 - if (isCode) { 283 - children.push(`<pre><code>${segment.text}</code></pre>`); 284 - } else if (link) { 285 - children.push( 286 - `<a href="${link.uri}" target="_blank" rel="noopener noreferrer">${segment.text}</a>`, 287 - ); 288 - } else if (isBold) { 289 - children.push(`<b>${segment.text}</b>`); 290 - } else if (isStrikethrough) { 291 - children.push(`<s>${segment.text}</s>`); 292 - } else if (isUnderline) { 293 - children.push( 294 - `<span style="text-decoration:underline;">${segment.text}</span>`, 295 - ); 296 - } else if (isItalic) { 297 - children.push(`<i>${segment.text}</i>`); 298 - } else { 299 - children.push(`${segment.text}`); 300 - } 301 - } 302 - html += `<p>${children.join("")}</p>`; 313 + html += parseTextBlock(block.block); 303 314 } 304 315 305 316 if (is(PubLeafletBlocksHeader.mainSchema, block.block)) { ··· 343 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>`; 344 355 } 345 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 + 346 366 return html.trim(); 347 367 } 348 368 349 - function renderListItem({ 369 + export function renderListItem({ 350 370 item, 351 371 did, 352 372 }: {
+1 -1
package.json
··· 1 1 { 2 2 "name": "@nulfrost/leaflet-loader-astro", 3 - "version": "1.1.0", 3 + "version": "1.3.0", 4 4 "description": "A leaflet.pub astro collection loader", 5 5 "keywords": [ 6 6 "astro",
+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 + });