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.

at trunk 3.9 kB View raw
1import type { BlockNode } from '../blocks/render'; 2 3/** Block names whose `url` attribute may hold a held (data:) image. Mirrors blob.ts. */ 4const IMAGE_BLOCKS = new Set( [ 'core/image' ] ); 5 6/** True when `value` is a `data:` URL string. */ 7export function isDataUrl( value: unknown ): value is string { 8 return typeof value === 'string' && value.startsWith( 'data:' ); 9} 10 11/** Rebuild a `Blob` (bytes + mime type) from a base64 `data:` URL. */ 12export function dataUrlToBlob( dataUrl: string ): Blob { 13 const comma = dataUrl.indexOf( ',' ); 14 const header = dataUrl.slice( 5, comma ); // after "data:" 15 const mimeType = header.split( ';' )[ 0 ] || 'application/octet-stream'; 16 const base64 = dataUrl.slice( comma + 1 ); 17 const binary = atob( base64 ); 18 const bytes = new Uint8Array( binary.length ); 19 for ( let i = 0; i < binary.length; i++ ) { 20 bytes[ i ] = binary.charCodeAt( i ); 21 } 22 return new Blob( [ bytes ], { type: mimeType } ); 23} 24 25export interface AssetSkeleton { 26 blocks: BlockNode[]; 27 /** `'cover'` when a held cover exists, else `null`. */ 28 cover: string | null; 29} 30 31/** 32 * Replace every held (`data:`) image URL in the tree — and the cover — with a short token, 33 * returning the lightweight skeleton plus a `token → dataUrl` map. Body tokens are `a0`, 34 * `a1`, … allocated depth-first; the cover token is the literal `'cover'`. External image 35 * URLs and non-image blocks are left untouched. Pure; returns new objects. 36 */ 37export function splitAssets( 38 blocks: BlockNode[], 39 coverDataUrl: string | null 40): { skeleton: AssetSkeleton; assets: Record< string, string > } { 41 const assets: Record< string, string > = {}; 42 let n = 0; 43 44 const walk = ( nodes: BlockNode[] ): BlockNode[] => 45 nodes.map( ( node ) => { 46 const url = node.attributes?.url; 47 const held = IMAGE_BLOCKS.has( node.name ) && isDataUrl( url ); 48 let attributes = node.attributes ? { ...node.attributes } : {}; 49 if ( held ) { 50 const token = `a${ n++ }`; 51 assets[ token ] = url as string; 52 attributes = { ...attributes, url: token }; 53 } 54 return { 55 name: node.name, 56 attributes, 57 innerBlocks: walk( node.innerBlocks ?? [] ), 58 }; 59 } ); 60 61 const skeletonBlocks = walk( blocks ); 62 let cover: string | null = null; 63 if ( isDataUrl( coverDataUrl ) ) { 64 assets.cover = coverDataUrl; 65 cover = 'cover'; 66 } 67 return { skeleton: { blocks: skeletonBlocks, cover }, assets }; 68} 69 70/** A body asset token allocated by `splitAssets` (`a0`, `a1`, …). */ 71function isAssetToken( value: string ): boolean { 72 return /^a\d+$/.test( value ); 73} 74 75/** Inverse of `splitAssets`: swap each token back to its `data:` URL. Pure. */ 76export function mergeAssets( 77 skeleton: AssetSkeleton, 78 assets: Record< string, string > 79): { blocks: BlockNode[]; coverDataUrl: string | null } { 80 const walk = ( nodes: BlockNode[] ): BlockNode[] => 81 nodes.map( ( node ) => { 82 const url = node.attributes?.url; 83 const isImage = IMAGE_BLOCKS.has( node.name ) && typeof url === 'string'; 84 let attributes: Record< string, unknown >; 85 if ( isImage && ( url as string ) in assets ) { 86 attributes = { ...node.attributes, url: assets[ url as string ] }; 87 } else if ( isImage && isAssetToken( url as string ) ) { 88 // Held bytes missing (e.g. IndexedDB evicted while the localStorage skeleton 89 // survived): drop the dangling token rather than emit a broken `a0` src that 90 // would also be published verbatim. External image URLs are not tokens, so 91 // they fall through and are preserved. 92 const { url: _dropped, ...rest } = node.attributes ?? {}; 93 attributes = { ...rest }; 94 } else { 95 attributes = { ...( node.attributes ?? {} ) }; 96 } 97 return { 98 name: node.name, 99 attributes, 100 innerBlocks: walk( node.innerBlocks ?? [] ), 101 }; 102 } ); 103 104 return { 105 blocks: walk( skeleton.blocks ), 106 coverDataUrl: skeleton.cover ? assets[ skeleton.cover ] ?? null : null, 107 }; 108}