/** A page of the menu, as the command palette lists it. */ export interface MenuPage { label: string; to: string; } /** The words of a label, for telling whether two labels name one thing. */ const wordsOf = (value: string): string => value .toLowerCase() .replace(/&/g, ' and ') .replace(/[^\p{L}\p{N}]+/gu, ' ') .trim(); /** A path without its query or hash, and without a trailing slash. */ const pathnameOf = (path: string): string => path.split(/[?#]/)[0].replace(/\/+$/, '') || '/'; /** * The page a link in a reply opens, when it is one: its path is a page of * the menu and its words name that page ("Titles & meta" → /seo/titles). * The model also writes made-up routes, and real routes under an action's * words ("Confirm generation" → /ai-visibility/contents); those return * null and go back to it as a follow-up. * * @param {string} href - Where the link points, a plugin route. * @param {string} label - The words it was written as. * @param {MenuPage[]} pages - The pages of the menu. * @returns {string | null} The route to open, query kept, or null. */ export function pageLinkTarget( href: string, label: string, pages: MenuPage[] ): string | null { if (!href.startsWith('/')) return null; const words: string = wordsOf(label); if (words === '') return null; const page: MenuPage | undefined = pages.find( candidate => pathnameOf(candidate.to) === pathnameOf(href) ); if (!page) return null; const named: string = wordsOf(page.label); return named !== '' && (words === named || words.includes(named)) ? href : null; }