import { editor, markdown, mq, space, sync } from "$sb/syscalls.ts"; import { addParentPointers, collectNodesOfType, findParentMatching, nodeAtPos, ParseTree, removeParentPointers, renderToText, traverseTree, } from "$sb/lib/tree.ts"; import { renderDirectives } from "./directives.ts"; import { extractFrontmatter } from "$sb/lib/frontmatter.ts"; import { isFederationPath } from "$sb/lib/resolve.ts"; import { MQMessage, PageMeta } from "$sb/types.ts"; import { sleep } from "$sb/lib/async.ts"; const directiveUpdateQueueName = "directiveUpdateQueue"; export async function updateDirectivesOnPageCommand() { // If `arg` is a string, it's triggered automatically via an event, not explicitly via a command const currentPage = await editor.getCurrentPage(); let pageMeta: PageMeta | undefined; try { pageMeta = await space.getPageMeta(currentPage); } catch { console.info("Page not found, not updating directives"); return; } const text = await editor.getText(); const tree = await markdown.parseMarkdown(text); const metaData = await extractFrontmatter(tree, { removeKeys: ["$disableDirectives"], }); if (isFederationPath(currentPage)) { console.info("Current page is a federation page, not updating directives."); return; } if (metaData.$disableDirectives) { console.info("Directives disabled in page meta, not updating them."); return; } if (!(await sync.hasInitialSyncCompleted())) { console.info( "Initial sync hasn't completed yet, not updating directives.", ); return; } await editor.save(); const replacements = await findReplacements(tree, text, pageMeta); // Iterate again and replace the bodies. Iterating again (not using previous positions) // because text may have changed in the mean time (directive processing may take some time) // Hypothetically in the mean time directives in text may have been changed/swapped, in which // case this will break. This would be a rare edge case, however. for (const replacement of replacements) { // Fetch the text every time, because dispatch() will have been made changes const text = await editor.getText(); // Determine the current position const index = text.indexOf(replacement.fullMatch); // This may happen if the query itself, or the user is editing inside the directive block (WHY!?) if (index === -1) { console.warn( "Text I got", text, ); console.warn( "Could not find directive in text, skipping", replacement.fullMatch, ); continue; } const from = index, to = index + replacement.fullMatch.length; const newText = await replacement.textPromise; if (text.substring(from, to) === newText) { // No change, skip continue; } await editor.dispatch({ changes: { from, to, insert: newText, }, }); } } export async function updateDirectivesInSpaceCommand() { await editor.flashNotification( "Updating directives in entire space, this can take a while...", ); await updateDirectivesInSpace(); // And notify the user await editor.flashNotification("Updating of all directives completed!"); } export async function processUpdateQueue(messages: MQMessage[]) { for (const message of messages) { const pageName: string = message.body; console.log("Updating directives in page", pageName); await updateDirectivesForPage(pageName); await mq.ack(directiveUpdateQueueName, message.id); } } async function findReplacements( tree: ParseTree, text: string, pageMeta: PageMeta, ) { // Collect all directives and their body replacements const replacements: { fullMatch: string; textPromise: Promise }[] = []; // Convenience array to wait for all promises to resolve const allPromises: Promise[] = []; removeParentPointers(tree); traverseTree(tree, (tree) => { if (tree.type !== "Directive") { return false; } const fullMatch = text.substring(tree.from!, tree.to!); try { const promise = renderDirectives(pageMeta, tree); replacements.push({ textPromise: promise, fullMatch, }); allPromises.push(promise); } catch (e: any) { replacements.push({ fullMatch, textPromise: Promise.resolve( `${renderToText(tree.children![0])}\n**ERROR:** ${e.message}\n${ renderToText(tree.children![tree.children!.length - 1]) }`, ), }); } return true; }); // Wait for all to have processed await Promise.all(allPromises); return replacements; } export async function updateDirectivesInSpace() { const pages = await space.listPages(); await mq.batchSend(directiveUpdateQueueName, pages.map((page) => page.name)); // Now let's wait for the processing to finish let queueStats = await mq.getQueueStats(directiveUpdateQueueName); while (queueStats.queued > 0 || queueStats.processing > 0) { sleep(1000); queueStats = await mq.getQueueStats(directiveUpdateQueueName); } console.log("Done updating directives in space!"); } async function updateDirectivesForPage( pageName: string, ) { const pageMeta = await space.getPageMeta(pageName); const currentText = await space.readPage(pageName); const tree = await markdown.parseMarkdown(currentText); const metaData = await extractFrontmatter(tree, { removeKeys: ["$disableDirectives"], }); if (isFederationPath(pageName)) { console.info("Current page is a federation page, not updating directives."); return; } if (metaData.$disableDirectives) { console.info("Directives disabled in page meta, not updating them."); return; } const newText = await updateDirectives(pageMeta, tree, currentText); if (newText !== currentText) { console.info("Content of page changed, saving", pageName); await space.writePage(pageName, newText); } } export async function updateDirectives( pageMeta: PageMeta, tree: ParseTree, text: string, ) { const replacements = await findReplacements(tree, text, pageMeta); // Iterate again and replace the bodies. for (const replacement of replacements) { text = text.replace( replacement.fullMatch, await replacement.textPromise, ); } return text; } export async function convertToLive() { const text = await editor.getText(); const pos = await editor.getCursor(); const tree = await markdown.parseMarkdown(text); addParentPointers(tree); const currentNode = nodeAtPos(tree, pos); const directive = findParentMatching( currentNode!, (node) => node.type === "Directive", ); if (!directive) { await editor.flashNotification( "No directive found at cursor position", "error", ); return; } console.log("Got this directive", directive); const startNode = directive.children![0]; const startNodeText = renderToText(startNode); if (startNodeText.includes("#query")) { const queryText = renderToText(startNode.children![1]); await editor.dispatch({ changes: { from: directive.from, to: directive.to, insert: "```query\n" + queryText + "\n```", }, }); } else if ( startNodeText.includes("#use") || startNodeText.includes("#include") ) { const pageRefMatch = /\[\[([^\]]+)\]\]\s*([^\-]+)?/.exec(startNodeText); if (!pageRefMatch) { await editor.flashNotification( "No page reference found in directive", "error", ); return; } const val = pageRefMatch[2]; await editor.dispatch({ changes: { from: directive.from, to: directive.to, insert: '```template\npage: "[[' + pageRefMatch[1] + ']]"\n' + (val ? `val: ${val}\n` : "") + "```", }, }); } } export async function convertSpaceToLive() { if ( !await editor.confirm( "This will convert all directives in the space to live queries. Are you sure?", ) ) { return; } const pages = await space.listPages(); for (const page of pages) { console.log("Now converting", page); const text = await space.readPage(page.name); const newText = await convertDirectivesOnPage(text); if (text !== newText) { console.log("Changes were made, writing", page.name); await space.writePage(page.name, newText); } } await editor.flashNotification("All done!"); } export async function convertDirectivesOnPage(text: string) { const tree = await markdown.parseMarkdown(text); collectNodesOfType(tree, "Directive").forEach((directive) => { const directiveText = renderToText(directive); console.log("Got this directive", directiveText); const startNode = directive.children![0]; const startNodeText = renderToText(startNode); if (startNodeText.includes("#query")) { const queryText = renderToText(startNode.children![1]); text = text.replace(directiveText, "```query\n" + queryText + "\n```"); } else if ( startNodeText.includes("#use") || startNodeText.includes("#include") ) { const pageRefMatch = /\[\[([^\]]+)\]\]\s*([^\-]+)?/.exec(startNodeText); if (!pageRefMatch) { return; } const val = pageRefMatch[2]; text = text.replace( directiveText, '```template\npage: "[[' + pageRefMatch[1] + ']]"\n' + (val ? `val: ${val}\n` : "") + "```", ); } }); // console.log("Converted page", text); return text; }