2024-08-07 02:11:38 +08:00
import {
codeWidget ,
editor ,
markdown ,
system ,
} from "@silverbulletmd/silverbullet/syscalls" ;
2024-02-24 22:51:55 +08:00
import {
addParentPointers ,
collectNodesOfType ,
findNodeOfType ,
findParentMatching ,
2024-07-30 23:33:33 +08:00
type ParseTree ,
2024-02-24 22:51:55 +08:00
removeParentPointers ,
renderToText ,
2024-08-07 02:11:38 +08:00
} from "@silverbulletmd/silverbullet/lib/tree" ;
import { parseQuery } from "../../plug-api/lib/parse_query.ts" ;
2024-01-21 02:16:07 +08:00
import { loadPageObject , replaceTemplateVars } from "../template/page.ts" ;
2024-07-30 23:33:33 +08:00
import type { CodeWidgetContent } from "../../plug-api/types.ts" ;
2024-02-24 22:51:55 +08:00
import { jsonToMDTable } from "../template/util.ts" ;
2024-03-16 22:29:24 +08:00
import type { ChangeSpec } from "@codemirror/state" ;
2024-08-09 02:19:41 +08:00
import {
findNodeMatching ,
nodeAtPos ,
} from "@silverbulletmd/silverbullet/lib/tree" ;
2024-01-21 02:16:07 +08:00
export async function widget (
bodyText : string ,
pageName : string ,
) : Promise < CodeWidgetContent > {
2024-08-02 22:47:36 +08:00
const config = await system . getSpaceConfig ( ) ;
2024-01-21 02:16:07 +08:00
const pageObject = await loadPageObject ( pageName ) ;
try {
let resultMarkdown = "" ;
const parsedQuery = await parseQuery (
2024-08-02 22:47:36 +08:00
await replaceTemplateVars ( bodyText , pageObject , config ) ,
2024-01-21 02:16:07 +08:00
) ;
2024-08-15 17:26:59 +08:00
const results = await system . invokeFunction (
"query.renderQuery" ,
parsedQuery ,
{
page : pageObject ,
config ,
} ,
) ;
2024-02-03 02:19:07 +08:00
if ( Array . isArray ( results ) ) {
2024-08-24 18:35:09 +08:00
if ( results . length === 0 ) {
resultMarkdown = "No results" ;
} else {
resultMarkdown = jsonToMDTable ( results ) ;
}
2024-01-21 02:16:07 +08:00
} else {
2024-02-03 02:19:07 +08:00
resultMarkdown = results ;
2024-01-21 02:16:07 +08:00
}
return {
markdown : resultMarkdown ,
buttons : [
2024-02-24 22:51:55 +08:00
{
description : "Bake result" ,
svg :
` <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-left"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg> ` ,
2024-08-09 02:19:41 +08:00
invokeFunction : [ "query.bakeButton" , bodyText ] ,
2024-02-24 22:51:55 +08:00
} ,
2024-01-21 02:16:07 +08:00
{
description : "Edit" ,
svg :
` <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-edit"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg> ` ,
2024-08-09 02:19:41 +08:00
invokeFunction : [ "query.editButton" , bodyText ] ,
2024-01-21 02:16:07 +08:00
} ,
{
description : "Reload" ,
svg :
` <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-refresh-cw"><polyline points="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path></svg> ` ,
2024-08-09 02:19:41 +08:00
invokeFunction : [ "query.refreshAllWidgets" ] ,
2024-01-21 02:16:07 +08:00
} ,
] ,
} ;
} catch ( e : any ) {
return { markdown : ` **Error:** ${ e . message } ` } ;
}
}
2023-12-28 23:14:30 +08:00
export function refreshAllWidgets() {
2023-11-15 17:08:21 +08:00
codeWidget . refreshAll ( ) ;
}
2023-12-28 23:14:30 +08:00
2024-02-23 20:26:10 +08:00
export async function editButton ( bodyText : string ) {
const text = await editor . getText ( ) ;
2024-02-24 22:51:55 +08:00
// This is a bit of a heuristic and will point to the wrong place if the same body text appears in multiple places, which is easy to replicate but unlikely to happen in the real world
2024-02-23 20:26:10 +08:00
// A more accurate fix would be to update the widget (and therefore the index of where this widget appears) on every change, but this would be rather expensive. I think this is good enough.
const bodyPos = text . indexOf ( "\n" + bodyText + "\n" ) ;
if ( bodyPos === - 1 ) {
await editor . flashNotification ( "Could not find widget to edit" , "error" ) ;
return ;
}
await editor . moveCursor ( bodyPos + 1 ) ;
2023-12-28 23:14:30 +08:00
}
2024-02-24 22:51:55 +08:00
export async function bakeButton ( bodyText : string ) {
try {
const text = await editor . getText ( ) ;
const tree = await markdown . parseMarkdown ( text ) ;
addParentPointers ( tree ) ;
// Need to find it in page to make the replacement, see editButton for comment about finding by content
2024-08-09 02:19:41 +08:00
const textNode = findNodeMatching ( tree , ( n ) = > n . text === bodyText ) ||
nodeAtPos ( tree , text . indexOf ( bodyText ) ) ;
2024-02-24 22:51:55 +08:00
if ( ! textNode ) {
throw new Error ( ` Could not find node to bake ` ) ;
}
const blockNode = findParentMatching (
textNode ,
2024-08-09 02:19:41 +08:00
( n ) = > n . type === "FencedCode" || n . type === "Image" ,
2024-02-24 22:51:55 +08:00
) ;
if ( ! blockNode ) {
removeParentPointers ( textNode ) ;
console . error ( "baked node" , textNode ) ;
throw new Error ( "Could not find FencedCode above baked node" ) ;
}
const changes = await changeForBake ( blockNode ) ;
if ( changes ) {
await editor . dispatch ( { changes } ) ;
} else {
// Either something failed, or this widget does not meet requirements for baking and shouldn't show the button at all
throw new Error ( "Baking with button didn't produce any changes" ) ;
}
} catch ( error ) {
console . error ( error ) ;
await editor . flashNotification ( "Could not bake widget" , "error" ) ;
}
}
export async function bakeAllWidgets() {
const text = await editor . getText ( ) ;
const tree = await markdown . parseMarkdown ( text ) ;
const changes = ( await Promise . all (
collectNodesOfType ( tree , "FencedCode" ) . map ( changeForBake ) ,
) ) . filter ( ( c ) : c is ChangeSpec = > c !== null ) ;
await editor . dispatch ( { changes } ) ;
await editor . flashNotification ( ` Baked ${ changes . length } live blocks ` ) ;
}
/ * *
* Create change description to replace a widget source with its markdown output
2024-08-09 02:19:41 +08:00
* @param nodeToReplace node of type FencedCode or Image for a markdown widget ( eg . query , template , toc )
2024-02-24 22:51:55 +08:00
* @returns single replacement for the editor , or null if the widget didn ' t render to markdown
* /
async function changeForBake (
2024-08-09 02:19:41 +08:00
nodeToReplace : ParseTree ,
2024-02-24 22:51:55 +08:00
) : Promise < ChangeSpec | null > {
2024-08-09 02:19:41 +08:00
const lang = nodeToReplace . type === "FencedCode"
? renderToText ( findNodeOfType ( nodeToReplace , "CodeInfo" ) ? ? undefined )
: nodeToReplace . type === "Image"
? "transclusion"
: undefined ;
let body : string | undefined = undefined ;
if ( nodeToReplace . type === "FencedCode" ) {
body = renderToText ( findNodeOfType ( nodeToReplace , "CodeText" ) ? ? undefined ) ;
} else if ( nodeToReplace . type === "Image" ) {
body = renderToText ( nodeToReplace ) ;
}
2024-02-24 22:51:55 +08:00
if ( ! lang || body === undefined ) {
return null ;
}
const content = await codeWidget . render (
lang ,
body ,
await editor . getCurrentPage ( ) ,
) ;
if (
! content || ! content . markdown === undefined ||
2024-08-09 02:19:41 +08:00
nodeToReplace . from === undefined ||
nodeToReplace . to === undefined
2024-02-24 22:51:55 +08:00
) { // Check attributes for undefined because 0 or empty string could be valid
return null ;
}
return {
2024-08-09 02:19:41 +08:00
from : nodeToReplace . from ,
to : nodeToReplace.to ,
2024-02-24 22:51:55 +08:00
insert : content.markdown ,
} ;
}