👁️
5
fork

Configure Feed

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

show richtext in opengraph

+213 -2
+168
src/lib/__tests__/richtext-convert.test.ts
··· 8 8 ParagraphBlock, 9 9 } from "@/lib/lexicons/types/com/deckbelcher/richtext"; 10 10 import { 11 + documentToPlainText, 11 12 type LexiconDocument, 12 13 lexiconToTree, 13 14 treeToLexicon, ··· 1992 1993 ); 1993 1994 }); 1994 1995 }); 1996 + 1997 + describe("documentToPlainText", () => { 1998 + it("returns undefined for empty content", () => { 1999 + const doc: LexiconDocument = { content: [] }; 2000 + expect(documentToPlainText(doc)).toBeUndefined(); 2001 + }); 2002 + 2003 + it("returns undefined for missing content", () => { 2004 + const doc: LexiconDocument = {} as LexiconDocument; 2005 + expect(documentToPlainText(doc)).toBeUndefined(); 2006 + }); 2007 + 2008 + it("extracts text from paragraph", () => { 2009 + const doc: LexiconDocument = { 2010 + content: [ 2011 + { 2012 + $type: "com.deckbelcher.richtext#paragraphBlock", 2013 + text: "Hello world", 2014 + }, 2015 + ], 2016 + }; 2017 + expect(documentToPlainText(doc)).toMatchInlineSnapshot(`"Hello world"`); 2018 + }); 2019 + 2020 + it("extracts text from heading", () => { 2021 + const doc: LexiconDocument = { 2022 + content: [ 2023 + { 2024 + $type: "com.deckbelcher.richtext#headingBlock", 2025 + level: 1, 2026 + text: "My Heading", 2027 + }, 2028 + ], 2029 + }; 2030 + expect(documentToPlainText(doc)).toMatchInlineSnapshot(`"My Heading"`); 2031 + }); 2032 + 2033 + it("extracts text from code block", () => { 2034 + const doc: LexiconDocument = { 2035 + content: [ 2036 + { 2037 + $type: "com.deckbelcher.richtext#codeBlock", 2038 + text: "const x = 1;", 2039 + }, 2040 + ], 2041 + }; 2042 + expect(documentToPlainText(doc)).toMatchInlineSnapshot(`"const x = 1;"`); 2043 + }); 2044 + 2045 + it("extracts text from bullet list", () => { 2046 + const doc: LexiconDocument = { 2047 + content: [ 2048 + { 2049 + $type: "com.deckbelcher.richtext#bulletListBlock", 2050 + items: [ 2051 + { $type: "com.deckbelcher.richtext#listItem", text: "Item one" }, 2052 + { $type: "com.deckbelcher.richtext#listItem", text: "Item two" }, 2053 + ], 2054 + }, 2055 + ], 2056 + }; 2057 + expect(documentToPlainText(doc)).toMatchInlineSnapshot(` 2058 + "Item one 2059 + Item two" 2060 + `); 2061 + }); 2062 + 2063 + it("extracts text from ordered list", () => { 2064 + const doc: LexiconDocument = { 2065 + content: [ 2066 + { 2067 + $type: "com.deckbelcher.richtext#orderedListBlock", 2068 + items: [ 2069 + { $type: "com.deckbelcher.richtext#listItem", text: "First" }, 2070 + { $type: "com.deckbelcher.richtext#listItem", text: "Second" }, 2071 + ], 2072 + }, 2073 + ], 2074 + }; 2075 + expect(documentToPlainText(doc)).toMatchInlineSnapshot(` 2076 + "First 2077 + Second" 2078 + `); 2079 + }); 2080 + 2081 + it("ignores horizontal rules", () => { 2082 + const doc: LexiconDocument = { 2083 + content: [ 2084 + { 2085 + $type: "com.deckbelcher.richtext#paragraphBlock", 2086 + text: "Before", 2087 + }, 2088 + { $type: "com.deckbelcher.richtext#horizontalRuleBlock" }, 2089 + { 2090 + $type: "com.deckbelcher.richtext#paragraphBlock", 2091 + text: "After", 2092 + }, 2093 + ], 2094 + }; 2095 + expect(documentToPlainText(doc)).toMatchInlineSnapshot(` 2096 + "Before 2097 + 2098 + After" 2099 + `); 2100 + }); 2101 + 2102 + it("joins multiple blocks with newlines", () => { 2103 + const doc: LexiconDocument = { 2104 + content: [ 2105 + { 2106 + $type: "com.deckbelcher.richtext#headingBlock", 2107 + level: 1, 2108 + text: "Title", 2109 + }, 2110 + { 2111 + $type: "com.deckbelcher.richtext#paragraphBlock", 2112 + text: "First paragraph.", 2113 + }, 2114 + { 2115 + $type: "com.deckbelcher.richtext#paragraphBlock", 2116 + text: "Second paragraph.", 2117 + }, 2118 + ], 2119 + }; 2120 + expect(documentToPlainText(doc)).toMatchInlineSnapshot(` 2121 + "Title 2122 + First paragraph. 2123 + Second paragraph." 2124 + `); 2125 + }); 2126 + 2127 + it("strips formatting facets", () => { 2128 + const doc: LexiconDocument = { 2129 + content: [ 2130 + { 2131 + $type: "com.deckbelcher.richtext#paragraphBlock", 2132 + text: "Bold and italic text", 2133 + facets: [ 2134 + { 2135 + index: { byteStart: 0, byteEnd: 4 }, 2136 + features: [{ $type: "com.deckbelcher.richtext.facet#bold" }], 2137 + }, 2138 + { 2139 + index: { byteStart: 9, byteEnd: 15 }, 2140 + features: [{ $type: "com.deckbelcher.richtext.facet#italic" }], 2141 + }, 2142 + ], 2143 + }, 2144 + ], 2145 + }; 2146 + expect(documentToPlainText(doc)).toMatchInlineSnapshot( 2147 + `"Bold and italic text"`, 2148 + ); 2149 + }); 2150 + 2151 + it("returns undefined for whitespace-only content", () => { 2152 + const doc: LexiconDocument = { 2153 + content: [ 2154 + { 2155 + $type: "com.deckbelcher.richtext#paragraphBlock", 2156 + text: " ", 2157 + }, 2158 + ], 2159 + }; 2160 + expect(documentToPlainText(doc)).toBeUndefined(); 2161 + }); 2162 + });
+30
src/lib/richtext-convert.ts
··· 550 550 return null; 551 551 } 552 552 } 553 + 554 + function blockToPlainText( 555 + block: NonNullable<LexiconDocument["content"]>[number], 556 + ): string { 557 + switch (block.$type) { 558 + case "com.deckbelcher.richtext#headingBlock": 559 + case "com.deckbelcher.richtext#paragraphBlock": 560 + return block.text ?? ""; 561 + case "com.deckbelcher.richtext#codeBlock": 562 + return block.text; 563 + case "com.deckbelcher.richtext#bulletListBlock": 564 + case "com.deckbelcher.richtext#orderedListBlock": 565 + return block.items.map((item) => item.text ?? "").join("\n"); 566 + case "com.deckbelcher.richtext#horizontalRuleBlock": 567 + return ""; 568 + default: 569 + return ""; 570 + } 571 + } 572 + 573 + /** 574 + * Convert a lexicon Document to plain text, stripping all formatting. 575 + * Useful for OpenGraph descriptions and other plain text contexts. 576 + * Returns undefined if there's no content. 577 + */ 578 + export function documentToPlainText(doc: LexiconDocument): string | undefined { 579 + if (!doc.content) return undefined; 580 + const text = doc.content.map(blockToPlainText).join("\n").trim(); 581 + return text || undefined; 582 + }
+6 -1
src/routes/profile/$did/deck/$rkey/index.tsx
··· 72 72 const ogTitle = format ? `${deck.name} (${format})` : deck.name; 73 73 74 74 const cardCount = deck.cards.reduce((sum, c) => sum + c.quantity, 0); 75 - const description = `${cardCount} card${cardCount === 1 ? "" : "s"}`; 75 + const primerText = deck.primer 76 + ? documentToPlainText(deck.primer) 77 + : undefined; 78 + const description = primerText 79 + ? `${primerText.slice(0, 150)}${primerText.length > 150 ? "..." : ""}` 80 + : `${cardCount} card${cardCount === 1 ? "" : "s"}`; 76 81 77 82 // Use first commander's image, or first card if no commanders 78 83 const commanders = deck.cards.filter((c) => c.section === "commander");
+9 -1
src/routes/profile/$did/list/$rkey/index.tsx
··· 26 26 import { didDocumentQueryOptions, extractHandle } from "@/lib/did-to-handle"; 27 27 import type { Document } from "@/lib/lexicons/types/com/deckbelcher/richtext"; 28 28 import { getCardByIdQueryOptions } from "@/lib/queries"; 29 + import { documentToPlainText } from "@/lib/richtext-convert"; 29 30 import { getImageUri } from "@/lib/scryfall-utils"; 30 31 import { useAuth } from "@/lib/useAuth"; 31 32 ··· 51 52 parts.push(`${cardCount} card${cardCount === 1 ? "" : "s"}`); 52 53 if (deckCount > 0) 53 54 parts.push(`${deckCount} deck${deckCount === 1 ? "" : "s"}`); 54 - const description = parts.length > 0 ? parts.join(", ") : "Empty list"; 55 + const itemSummary = parts.length > 0 ? parts.join(", ") : "Empty list"; 56 + 57 + const descriptionText = list.description 58 + ? documentToPlainText(list.description) 59 + : undefined; 60 + const description = descriptionText 61 + ? `${descriptionText.slice(0, 150)}${descriptionText.length > 150 ? "..." : ""}` 62 + : itemSummary; 55 63 56 64 const firstCard = list.items.find(isCardItem); 57 65 const cardImageUrl = firstCard