export interface Heading { text: string; level: number; } export interface EditorBlock { name: string; attributes: Record; innerBlocks: EditorBlock[]; } const textOf = (html: string): string => html .replace(/<[^>]+>/g, '') .replace(/ /g, ' ') .replace(/\s+/g, ' ') .trim(); /** Every heading of the post as it stands in the editor, in order. */ const headingsOf = (blocks: EditorBlock[]): Heading[] => blocks.flatMap(block => { const own: Heading[] = block.name === 'core/heading' && textOf(String(block.attributes.content ?? '')) !== '' ? [ { text: textOf(String(block.attributes.content ?? '')), level: Number(block.attributes.level ?? 2), }, ] : []; return [...own, ...headingsOf(block.innerBlocks ?? [])]; }); /** * The headings as one string, what the block's store selector returns: the * same string for the same headings, so typing in a paragraph does not * render the list again (a new array on every call did, with a warning). * * @param blocks - The editor's blocks. * @return The headings, serialized. */ export const headingsKey = (blocks: EditorBlock[]): string => JSON.stringify(headingsOf(blocks)); /** * The headings a key holds, down to a level. * * @param key - From headingsKey(). * @param maxLevel - The deepest level listed. * @return The headings. */ export const readHeadings = (key: string, maxLevel: number): Heading[] => (JSON.parse(key) as Heading[]).filter(heading => heading.level <= maxLevel);