2024-08-07 02:11:38 +08:00
import {
editor ,
events ,
space ,
system ,
} from "@silverbulletmd/silverbullet/syscalls" ;
import { readYamlPage } from "@silverbulletmd/silverbullet/lib/yaml_page" ;
2023-05-24 02:53:53 +08:00
import { builtinPlugNames } from "../builtin_plugs.ts" ;
2024-12-14 16:39:37 +08:00
import { findNodeMatching } from "@silverbulletmd/silverbullet/lib/tree" ;
import { parseMarkdown } from "$common/markdown_parser/parser.ts" ;
import { addParentPointers } from "@silverbulletmd/silverbullet/lib/tree" ;
import { findNodeOfType } from "@silverbulletmd/silverbullet/lib/tree" ;
import { assert } from "@std/assert/assert" ;
import { builtinLanguages } from "$common/languages.ts" ;
2022-11-20 17:56:52 +08:00
2024-12-14 16:39:37 +08:00
const plugsPage = "PLUGS" ;
2022-11-20 17:56:52 +08:00
const plugsPrelude =
2024-12-14 16:39:37 +08:00
"#meta\n\nThis file lists all plugs added with the {[Plugs: Add]} command. Run the {[Plugs: Update]} command to update all Plugs defined anywhere using Space Config.\n\n" ;
2022-06-28 20:14:15 +08:00
export async function updatePlugsCommand() {
2022-10-14 21:11:33 +08:00
await editor . save ( ) ;
await editor . flashNotification ( "Updating plugs..." ) ;
2024-12-14 16:42:46 +08:00
await system . reloadConfig ( ) ;
2022-07-15 17:17:02 +08:00
try {
2024-12-14 16:39:37 +08:00
const plugList : string [ ] = [ ] ;
const configPlugs : any [ ] = await system . getSpaceConfig ( "plugs" , [ ] ) ;
if ( ! Array . isArray ( configPlugs ) ) {
throw new Error ( "Expected 'plugs' in Space Config to be an array" ) ;
}
const stringPlugs = configPlugs . filter ( ( plug ) = > typeof plug === "string" ) ;
if ( stringPlugs . length !== configPlugs . length ) {
throw new Error (
` ${
configPlugs . length - stringPlugs . length
} plugs in Space Config aren ' t set as strings ` ,
) ;
}
plugList . push ( . . . stringPlugs ) ;
if ( await space . fileExists ( "PLUGS.md" ) ) {
// This is not primary mode of managing plugs anymore, only here for backwards compatibility.
try {
const pagePlugs : any [ ] = await readYamlPage ( "PLUGS" ) ;
if ( Array . isArray ( pagePlugs ) ) {
// It's possible that the user is using it for something else, but if it has yaml with an array, assume it's plugs
const pageStringPlugs = pagePlugs . filter ( ( plug ) = >
typeof plug === "string"
) ;
if ( pageStringPlugs . length !== pagePlugs . length ) {
throw new Error (
` ${
pagePlugs . length - pageStringPlugs . length
} plugs from PLUG page were not in a yaml list format ` ,
) ;
}
plugList . push ( . . . pageStringPlugs ) ;
if ( pageStringPlugs . length > 0 ) {
editor . flashNotification (
` ${ pageStringPlugs . length } plugs in PLUGS page can be moved to Space Config for better editor support ` ,
) ;
}
}
} catch ( e : any ) {
editor . flashNotification (
` Error processing PLUGS page: ${ e . message } ` ,
2023-07-04 22:52:43 +08:00
"error" ,
) ;
return ;
}
2023-05-24 02:53:53 +08:00
}
2024-12-14 16:39:37 +08:00
// De-duplicate URIs, this is safe because by definition they point to the same plug
plugList . forEach ( ( uri , index ) = > {
if ( plugList . indexOf ( uri ) !== index ) {
plugList . splice ( index , 1 ) ;
}
} ) ;
console . log ( "Found Plug URIs:" , plugList ) ;
2023-05-24 02:53:53 +08:00
const allCustomPlugNames : string [ ] = [ ] ;
for ( const plugUri of plugList ) {
const [ protocol , . . . rest ] = plugUri . split ( ":" ) ;
2024-07-30 03:14:16 +08:00
let plugName : string ;
if ( protocol == "ghr" ) {
// For GitHub Release, the plug is expected to be named same as repository
plugName = rest [ 0 ] . split ( "/" ) [ 1 ] ; // skip repo owner
// Strip "silverbullet-foo" into "foo" (multiple plugs follow this convention)
if ( plugName . startsWith ( "silverbullet-" ) ) {
plugName = plugName . slice ( "silverbullet-" . length ) ;
}
} else {
// Other URIs are expected to contain the file .plug.js at the end
const plugNameMatch = /\/([^\/]+)\.plug\.js$/ . exec ( plugUri ) ;
if ( ! plugNameMatch ) {
console . error (
"Could not extract plug name from " ,
plugUri ,
"ignoring..." ,
) ;
continue ;
}
2023-05-24 02:53:53 +08:00
2024-07-30 03:14:16 +08:00
plugName = plugNameMatch [ 1 ] ;
}
2023-05-24 02:53:53 +08:00
2024-12-14 16:39:37 +08:00
// Validate the extracted name
if ( builtinPlugNames . includes ( plugName ) ) {
throw new Error (
` Plug name ' ${ plugName } ' is conflicting with a built-in plug ` ,
) ;
}
if ( allCustomPlugNames . includes ( plugName ) ) {
throw new Error (
` Plug name ' ${ plugName } ' defined by more than one URI ` ,
) ;
}
2023-05-24 02:53:53 +08:00
const manifests = await events . dispatchEvent (
` get-plug: ${ protocol } ` ,
rest . join ( ":" ) ,
) ;
if ( manifests . length === 0 ) {
console . error ( "Could not resolve plug" , plugUri ) ;
}
// console.log("Got manifests", plugUri, protocol, manifests);
const workerCode = manifests [ 0 ] as string ;
allCustomPlugNames . push ( plugName ) ;
// console.log("Writing", `_plug/${plugName}.plug.js`, workerCode);
await space . writeAttachment (
2024-08-07 02:11:38 +08:00
` _plug/ ${ plugName } .plug.js ` ,
2023-05-26 20:04:32 +08:00
new TextEncoder ( ) . encode ( workerCode ) ,
2023-05-24 02:53:53 +08:00
) ;
}
const allPlugNames = [ . . . builtinPlugNames , . . . allCustomPlugNames ] ;
// And delete extra ones
2023-12-07 01:44:48 +08:00
for ( const { name : existingPlug } of await space . listPlugs ( ) ) {
2023-05-24 02:53:53 +08:00
const plugName = existingPlug . substring (
2024-08-07 02:11:38 +08:00
"_plug/" . length ,
2023-05-24 02:53:53 +08:00
existingPlug . length - ".plug.js" . length ,
) ;
if ( ! allPlugNames . includes ( plugName ) ) {
await space . deleteAttachment ( existingPlug ) ;
}
}
2022-10-14 21:11:33 +08:00
await editor . flashNotification ( "And... done!" ) ;
2022-07-15 17:17:02 +08:00
} catch ( e : any ) {
2022-10-14 21:11:33 +08:00
editor . flashNotification ( "Error updating plugs: " + e . message , "error" ) ;
2022-07-15 17:17:02 +08:00
}
2022-06-28 20:14:15 +08:00
}
2024-12-14 16:39:37 +08:00
export async function addPlugCommand ( _cmdDef : any , uriSuggestion : string = "" ) {
let uri = await editor . prompt ( "Plug URI:" , uriSuggestion ) ;
if ( ! uri ) {
2022-11-20 17:56:52 +08:00
return ;
}
// Support people copy & pasting the YAML version
2024-12-14 16:39:37 +08:00
if ( uri . startsWith ( "-" ) ) {
uri = uri . replace ( /^\-\s*/ , "" ) ;
2022-11-20 17:56:52 +08:00
}
2024-12-14 16:39:37 +08:00
let plugPageContent = plugsPrelude ;
if ( await space . fileExists ( plugsPage + ".md" ) ) {
plugPageContent = await space . readPage ( plugsPage ) ;
} else {
space . writePage ( plugsPage , plugPageContent ) ;
2022-11-20 17:56:52 +08:00
}
2024-12-14 16:39:37 +08:00
await editor . navigate ( { page : plugsPage } ) ;
// Here we are on the PLUGS page, if it didn't exist before it's filled with prelude
const changeList = insertIntoPlugPage ( uri , plugPageContent ) ;
for ( const { from , to , text } of changeList ) {
editor . replaceRange ( from , to , text ) ;
2022-11-20 17:56:52 +08:00
}
await editor . flashNotification ( "Plug added!" ) ;
2022-11-25 20:05:41 +08:00
system . reloadPlugs ( ) ;
2022-11-20 17:56:52 +08:00
}
2024-12-14 16:39:37 +08:00
/ * * A d d t h e p l u g t o t h e e n d o f t h e p l u g s l i s t i n S p a c e C o n f i g i n s i d e t h e P L U G S p a g e c o n t e n t
* Returns an array for ` editor.replaceRange ` syscalls .
*
* Rewrites the ` yaml ` block to ` space-config ` if present
* Appends the ` space-config ` block if needed .
* Appends the ` plugs ` key on root level if needed .
*
* It ' s exported only to allow testing .
* There are a bunch of asserts to please the type checker that will fail with a malformed page .
* /
export function insertIntoPlugPage (
uri : string ,
pageContent : string ,
) : Array < { from : number ; to : number ; text : string } > {
const edits : Array < { from : number ; to : number ; text : string } > = [ ] ;
const tree = parseMarkdown ( pageContent ) ;
addParentPointers ( tree ) ;
const yamlInfo = findNodeMatching ( tree , ( n ) = > {
return n . type === "CodeInfo" &&
n . children !== undefined &&
n . children . length === 1 &&
n . children [ 0 ] . text === "yaml" ;
} ) ;
const configInfo = findNodeMatching ( tree , ( n ) = > {
return n . type === "CodeInfo" &&
n . children !== undefined &&
n . children . length === 1 &&
n . children [ 0 ] . text === "space-config" ;
} ) ;
if ( yamlInfo ) {
// replace YAML with Space Config, add plugs: line at the start, and the new URI at the end
assert ( yamlInfo . from && yamlInfo . to ) ;
edits . push ( { from : yamlInfo . from , to : yamlInfo.to , text : "space-config" } ) ;
assert ( yamlInfo . parent ) ;
const yamlText = findNodeOfType ( yamlInfo . parent , "CodeText" ) ;
assert ( yamlText && yamlText . from && yamlText . to ) ;
edits . push ( { from : yamlText . from , to : yamlText.from , text : "plugs:\n" } ) ;
edits . push ( { from : yamlText . to , to : yamlText.to , text : ` \ n- ${ uri } ` } ) ;
} else if ( configInfo ) {
// Append the required parts into the Space Config block, using lezer's (upstream) parser
assert ( configInfo . parent ) ;
const configText = findNodeOfType ( configInfo . parent , "CodeText" ) ;
assert ( configText && configText . from && configText . to ) ;
assert ( configText . children ? . length === 1 && configText . children [ 0 ] . text ) ;
const config = configText . children [ 0 ] . text ;
const configTree = builtinLanguages [ "yaml" ] . parser . parse ( config ) ;
configTree . iterate ( {
enter : ( n ) = > {
if (
n . name === "Document" &&
config . substring ( n . from , n . to ) . startsWith ( "plugs:" )
) {
assert ( configText . from ) ;
if (
n . node . lastChild &&
config . substring ( n . node . lastChild . from , n . node . lastChild . to ) === "]"
) {
// This is a list with square brackets
edits . push ( {
from : configText . from + n . node . lastChild . from ,
to : configText.from + n . node . lastChild . from ,
text : ` , " ${ uri } " ` ,
} ) ;
} else {
edits . push ( {
from : configText . from + n . to ,
to : configText.from + n . to ,
text : ` \ n- ${ uri } ` ,
} ) ;
}
return false ; // Found the right node, no need to traverse any more
} else {
return true ;
}
} ,
} ) ;
if ( edits . length === 0 ) {
// No plugs in this block
edits . push ( {
from : configText . to ,
to : configText.to ,
text : ` \ nplugs: \ n- ${ uri } ` ,
} ) ;
}
} else {
// Just add the whole block if there's nothing
const configBlock = ` \` \` \` space-config
plugs :
- $ { uri }
\ ` \` \` ` ;
edits . push ( {
from : pageContent . length ,
to : pageContent.length ,
// Start on an empty line
text : ( pageContent . endsWith ( "\n" ) || pageContent === "" )
? configBlock
: ( "\n" + configBlock ) ,
} ) ;
}
// Sort edits from end to start, so they don't affect each other's positions
edits . sort ( ( a , b ) = > b . from - a . from ) ;
return edits ;
}
2023-05-24 02:53:53 +08:00
export async function getPlugHTTPS ( url : string ) : Promise < string > {
2022-10-14 21:11:33 +08:00
const fullUrl = ` https: ${ url } ` ;
2023-05-24 02:53:53 +08:00
console . log ( "Now fetching plug code from" , fullUrl ) ;
2022-10-14 21:11:33 +08:00
const req = await fetch ( fullUrl ) ;
2022-06-29 21:02:53 +08:00
if ( req . status !== 200 ) {
2023-05-24 02:53:53 +08:00
throw new Error ( ` Could not fetch plug code from ${ fullUrl } ` ) ;
2022-06-29 21:02:53 +08:00
}
2023-05-24 02:53:53 +08:00
return req . text ( ) ;
2022-06-29 21:02:53 +08:00
}
2023-05-24 02:53:53 +08:00
export function getPlugGithub ( identifier : string ) : Promise < string > {
2022-10-14 21:11:33 +08:00
const [ owner , repo , path ] = identifier . split ( "/" ) ;
2022-06-29 21:02:53 +08:00
let [ repoClean , branch ] = repo . split ( "@" ) ;
if ( ! branch ) {
branch = "main" ; // or "master"?
}
return getPlugHTTPS (
2022-10-12 17:47:13 +08:00
` //raw.githubusercontent.com/ ${ owner } / ${ repoClean } / ${ branch } / ${ path } ` ,
2022-06-29 21:02:53 +08:00
) ;
}
2022-07-24 02:57:59 +08:00
2022-09-12 20:50:37 +08:00
export async function getPlugGithubRelease (
2022-10-12 17:47:13 +08:00
identifier : string ,
2023-05-24 02:53:53 +08:00
) : Promise < string > {
2022-07-24 02:57:59 +08:00
let [ owner , repo , version ] = identifier . split ( "/" ) ;
2024-07-30 03:14:16 +08:00
let releaseInfo : any = { } ;
let req : Response ;
2022-07-24 02:57:59 +08:00
if ( ! version || version === "latest" ) {
2024-07-30 03:14:16 +08:00
console . log ( ` Fetching release manifest of latest version for ${ repo } ` ) ;
req = await fetch (
2022-10-12 17:47:13 +08:00
` https://api.github.com/repos/ ${ owner } / ${ repo } /releases/latest ` ,
2022-09-12 20:50:37 +08:00
) ;
2024-07-30 03:14:16 +08:00
} else {
console . log ( ` Fetching release manifest of version ${ version } for ${ repo } ` ) ;
req = await fetch (
` https://api.github.com/repos/ ${ owner } / ${ repo } /releases/tags/ ${ version } ` ,
) ;
}
if ( req . status !== 200 ) {
throw new Error (
` Could not fetch release manifest from ${ identifier } ` ,
) ;
}
releaseInfo = await req . json ( ) ;
version = releaseInfo . tag_name ;
let assetName : string | undefined ;
const shortName = repo . startsWith ( "silverbullet-" )
? repo . slice ( "silverbullet-" . length )
: undefined ;
for ( const asset of releaseInfo . assets ? ? [ ] ) {
if ( asset . name === ` ${ repo } .plug.js ` ) {
assetName = asset . name ;
break ;
}
// Support plug like foo.plug.js are in repo silverbullet-foo
if ( shortName && asset . name === ` ${ shortName } .plug.js ` ) {
assetName = asset . name ;
break ;
2022-07-24 02:57:59 +08:00
}
2022-09-12 20:50:37 +08:00
}
2024-07-30 03:14:16 +08:00
if ( ! assetName ) {
throw new Error (
` Could not find " ${ repo } .plug.js" ` +
( shortName ? ` or " ${ shortName } .plug.js" ` : "" ) +
` in release ${ version } ` ,
) ;
}
2022-10-12 17:47:13 +08:00
const finalUrl =
2024-07-30 03:14:16 +08:00
` //github.com/ ${ owner } / ${ repo } /releases/download/ ${ version } / ${ assetName } ` ;
2022-07-24 02:57:59 +08:00
return getPlugHTTPS ( finalUrl ) ;
2022-09-12 20:50:37 +08:00
}