import { html } from "/js/lib/lit-html.js"; import { sanitizeUri } from "/js/utils.js"; import { tokenizeRichText, isEmojiOnlyTokens } from "/js/richTextHelpers.js"; import { linkToHashtag, linkToProfileByDid } from "/js/navigation.js"; const KNOWN_UNSUPPORTED_FACET_TYPES = [ "blue.poll.post.facet#option", "blue.poll.post.facet#question", "blue.moji.richtext.facet", ]; // Matches social-app behavior export function truncateUrl(url) { try { const urlp = new URL(url); if (urlp.protocol !== "http:" && urlp.protocol !== "https:") { return url; } const path = (urlp.pathname === "/" ? "" : urlp.pathname) + urlp.search + urlp.hash; if (path.length > 15) { return urlp.host + path.slice(0, 13) + "..."; } return urlp.host + path; } catch { return url; } } function facetTemplate({ facet, wrappedText, truncateUrls }) { // only support 1 feature for now const feature = facet.features[0]; if (!feature) { console.warn("no feature found for facet", facet); return wrappedText; } switch (feature.$type) { case "app.bsky.richtext.facet#link": const uri = feature.uri; return html`${truncateUrls ? truncateUrl(wrappedText) : wrappedText}`; case "app.bsky.richtext.facet#tag": const tag = feature.tag; return html`${wrappedText}`; case "app.bsky.richtext.facet#mention": const did = feature.did; // Handle unresolved mentions return html`${wrappedText}`; default: if (!KNOWN_UNSUPPORTED_FACET_TYPES.includes(feature.$type)) { console.warn("unknown facet type " + feature.$type, feature); } return wrappedText; } } // tokens: ({ type: "text" } / { type: "facet" } / { type: "inline" } / { type: "block" }) // placeholderFacetTypes: Set of facet feature $types to render as // invisible-but-space-preserving spans (used while a rich-text transform // that claims the type is still pending, to avoid flashing plaintext). export function richTextTokensTemplate({ tokens, truncateUrls = false, renderNodeToken = () => null, placeholderFacetTypes = null, }) { const isEmojiOnly = isEmojiOnlyTokens(tokens); const parts = []; tokens.forEach((token, index) => { switch (token.type) { case "text": { let value = token.value; // Trim the newlines adjoining a block token so blocks don't render // with double gaps (the text is displayed white-space: pre-wrap). if (tokens[index - 1]?.type === "block" && value.startsWith("\n")) { value = value.slice(1); } if (tokens[index + 1]?.type === "block" && value.endsWith("\n")) { value = value.slice(0, -1); } parts.push(value); break; } case "facet": { if ( placeholderFacetTypes && token.facet.features?.some((feature) => placeholderFacetTypes.has(feature.$type), ) ) { parts.push( html``, ); break; } parts.push( facetTemplate({ facet: token.facet, wrappedText: token.text, truncateUrls, }), ); break; } case "inline": case "block": { const element = renderNodeToken(token) ?? null; if (!element) break; parts.push( token.type === "block" ? html`