A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

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

1/** 2 * The light reader renderer (Decision 0003) — block tree → HTML and → plain text. 3 * 4 * DEPENDENCY-FREE BY DESIGN: this module imports nothing from `@wordpress/*`, so 5 * it runs anywhere (pure Node build, Cloudflare/Vercel edge, the browser) and 6 * keeps reading pages free of the editor's weight (brief §6). Its output fidelity 7 * is locked to `@wordpress/blocks.serialize()` by `render.test.ts`, which uses the 8 * real packages as the oracle. 9 * 10 * Covers the curated allowlist (Decision 0002). Image/gallery/pullquote/embed 11 * rendering is stubbed pending SP3 (blobs) / SP4 (oEmbed + sanitisation). 12 */ 13 14export interface BlockNode { 15 name: string; 16 attributes?: Record< string, unknown >; 17 innerBlocks?: BlockNode[]; 18} 19 20function attr( node: BlockNode, key: string ): string { 21 const value = node.attributes?.[ key ]; 22 return value === undefined || value === null ? '' : String( value ); 23} 24 25function renderBlock( node: BlockNode ): string { 26 const inner = () => renderBlocks( node.innerBlocks ?? [] ); 27 28 switch ( node.name ) { 29 case 'core/paragraph': 30 return `<p>${ attr( node, 'content' ) }</p>`; 31 32 case 'core/heading': { 33 const level = Number( node.attributes?.level ?? 2 ); 34 return `<h${ level } class="wp-block-heading">${ attr( node, 'content' ) }</h${ level }>`; 35 } 36 37 case 'core/list': { 38 const tag = node.attributes?.ordered ? 'ol' : 'ul'; 39 return `<${ tag } class="wp-block-list">${ inner() }</${ tag }>`; 40 } 41 42 case 'core/list-item': 43 return `<li>${ attr( node, 'content' ) }</li>`; 44 45 case 'core/quote': { 46 const citation = attr( node, 'citation' ); 47 const cite = citation ? `<cite>${ citation }</cite>` : ''; 48 return `<blockquote class="wp-block-quote">${ inner() }${ cite }</blockquote>`; 49 } 50 51 case 'core/pullquote': { 52 const citation = attr( node, 'citation' ); 53 const cite = citation ? `<cite>${ citation }</cite>` : ''; 54 return `<figure class="wp-block-pullquote"><blockquote><p>${ attr( node, 'value' ) }</p>${ cite }</blockquote></figure>`; 55 } 56 57 case 'core/code': 58 return `<pre class="wp-block-code"><code>${ attr( node, 'content' ) }</code></pre>`; 59 60 case 'core/separator': 61 return '<hr class="wp-block-separator has-alpha-channel-opacity"/>'; 62 63 case 'core/image': { 64 const url = attr( node, 'url' ); 65 const alt = attr( node, 'alt' ); 66 if ( ! url ) { 67 return ''; 68 } 69 const caption = attr( node, 'caption' ); 70 const fig = caption ? `<figcaption>${ caption }</figcaption>` : ''; 71 return `<figure class="wp-block-image"><img src="${ url }" alt="${ alt }"/>${ fig }</figure>`; 72 } 73 74 default: 75 // Unknown / not-yet-supported block: degrade gracefully to its text. 76 return attr( node, 'content' ) ? `<p>${ attr( node, 'content' ) }</p>` : ''; 77 } 78} 79 80/** Render a block tree to frontend HTML (no block-delimiter comments). */ 81export function renderBlocks( blocks: BlockNode[] ): string { 82 return blocks.map( renderBlock ).join( '\n' ); 83} 84 85const TEXT_ATTRIBUTES = [ 'content', 'value', 'citation', 'caption' ]; 86 87export function decodeEntities( input: string ): string { 88 return input 89 .replace( /&lt;/g, '<' ) 90 .replace( /&gt;/g, '>' ) 91 .replace( /&quot;/g, '"' ) 92 .replace( /&#0?39;/g, "'" ) 93 .replace( /&apos;/g, "'" ) 94 .replace( /&nbsp;/g, ' ' ) 95 .replace( /&amp;/g, '&' ); 96} 97 98function stripMarkup( value: string ): string { 99 return decodeEntities( 100 value.replace( /<!--[\s\S]*?-->/g, '' ).replace( /<[^>]*>/g, '' ) 101 ) 102 .replace( /\s+/g, ' ' ) 103 .trim(); 104} 105 106function collectText( blocks: BlockNode[], out: string[] ): void { 107 for ( const block of blocks ) { 108 for ( const key of TEXT_ATTRIBUTES ) { 109 const raw = block.attributes?.[ key ]; 110 if ( raw === undefined || raw === null || raw === '' ) { 111 continue; 112 } 113 const text = stripMarkup( typeof raw === 'string' ? raw : String( raw ) ); 114 if ( text ) { 115 out.push( text ); 116 } 117 } 118 if ( block.innerBlocks?.length ) { 119 collectText( block.innerBlocks, out ); 120 } 121 } 122} 123 124/** 125 * Render a block tree to clean plain text in document order — the source for the 126 * lexicon's `textContent` (brief §3: Bluesky computes reading-time + search from 127 * it, ignoring the structured `content`). 128 */ 129export function blocksToText( blocks: BlockNode[] ): string { 130 const out: string[] = []; 131 collectText( blocks, out ); 132 return out.join( '\n\n' ); 133}