initial commit

This commit is contained in:
Aaron Manning 2023-07-09 07:47:52 +10:00
commit fe8a832d74
9 changed files with 2033 additions and 0 deletions

12
.config/dotnet-tools.json Normal file
View File

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"fable": {
"version": "4.1.4",
"commands": [
"fable"
]
}
}
}

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
bin/
obj/
out/
fable_modules/
*.fs.js

1700
Obsidian.fs Normal file

File diff suppressed because it is too large Load Diff

266
Program.fs Normal file
View File

@ -0,0 +1,266 @@
module Program
open Obsidian
open Fable.Core
open Fable.Core.JsInterop
[<Fable.Core.ImportAll(from = "obsidian")>]
let obsidian : Obsidian.IExports = jsNative
type SamplePluginSettings = {
mutable mySetting : string
}
let defaultSettings : SamplePluginSettings = {
mySetting = "default"
}
[<Fable.Core.Import("Command", from = "obsidian")>]
type DefaultCommand() =
do ()
let defaultCommand () =
let mutable mid = ""
let mutable mname = ""
let mutable _cb = None
let mutable _ccb = None
let mutable _hotkeys = None
let mutable _editorCallback = None
let mutable _editorCheckCallback = None
{ new Command with
member this.callback
with get () = _cb
and set v = _cb <- v
member this.checkCallback
with get () = _ccb
and set v = _ccb <- v
member this.editorCallback
with get () = _editorCallback
and set v = _editorCallback <- v
member this.editorCheckCallback
with get () = _editorCheckCallback
and set v = _editorCheckCallback <- v
member this.hotkeys
with get () = _hotkeys
and set v = _hotkeys <- v
member this.icon: string option = None
member this.icon
with set (v: string option): unit = ()
member this.id: string = mid
member this.id
with set (v: string): unit = mid <- v
member this.mobileOnly: bool option = None
member this.mobileOnly
with set (v: bool option): unit = ()
member this.name: string = mname
member this.name
with set (v: string): unit = mname <- v
}
[<Fable.Core.Import("PluginSettingTab", from = "obsidian")>]
type DefaultPluginSettingTab(app, plugin) =
do ()
type SamplePluginSettingTab(app, plugin) as instance =
inherit DefaultPluginSettingTab(app, plugin)
let settings =
instance :> obj :?> PluginSettingTab
let init () =
settings?plugin <- plugin
instance?display <- fun () ->
let containerEl : HTMLElement = settings?containerEl
containerEl.empty ()
let mutable setting : Setting = obsidian.Setting.Create(containerEl)
setting <- setting
.setName(U2.Case1 "Setting #1")
.setDesc(U2.Case1 "It's a secret")
.addText(fun text ->
text
.setPlaceholder("Enter your secret")
.setValue(plugin?settings.mySetting)
.onChange(fun value ->
settings?plugin?settings.mySetting <- value
settings?plugin?saveSettings ()
Some ()
) |> ignore
Some ()
)
do init ()
[<Fable.Core.Import("Modal", from = "obsidian")>]
type DefaultModal(app) =
do ()
type SampleModal(app) as instance =
inherit DefaultModal(app)
let modal =
instance :> obj :?> Modal
let init () =
modal?onOpen <- fun () ->
let contentEl : HTMLElement = modal?contentEl
U2.Case1 "Woah!" |> contentEl.setText
modal?onClose <- fun () ->
let contentEl : HTMLElement = modal?contentEl
contentEl.empty ()
do init ()
// This workaround allows the members to be defined dynamically, without implementing
// the whole interface.
[<Fable.Core.Import("Plugin", from = "obsidian")>]
type DefaultPlugin(app, manifest) =
do ()
// Note: This cannot be named "Plugin", or the import from the Obsidian API will be renamed
type SamplePlugin(app, manifest) as instance =
inherit DefaultPlugin(app, manifest)
// This is what is actually holding the plugin data
// It would be better to have the main plugin implement this interface, but I can't quite
// get it to work right without an absurd amount of boilerplate
let plugin =
instance :> obj :?> Plugin
let init () : unit =
plugin?loadSettings <- (fun _ ->
promise {
let! data = plugin.loadData ()
match data with
| None -> plugin?settings <- defaultSettings
| Some v ->
try
plugin?settings <- v :?> SamplePluginSettings
with
| _ ->
plugin?settings <- defaultSettings
}
)
plugin?saveSettings <- (fun () ->
promise {
do! plugin.saveData (Some(!!plugin?settings))
}
)
// This creates an icon in the left ribbon.
plugin?ribbonIconEl <-
// Called when the user clicks the icon.
let callback (evt : Browser.Types.MouseEvent) : Obsidian.Notice =
obsidian.Notice.Create(U2.Case1 "This is a notice!")
in
plugin?addRibbonIcon("dice", "Sample Plugin", callback)
// Perform additional things with the ribbon
(plugin?ribbonIconEl : Element).addClass (
let array = new ResizeArray<string>(1)
array.Add("my-plugin-ribbon-class")
array
)
// This adds a status bar item to the bottom of the app. Does not work on mobile apps.
let statusBarItemEl : Element = plugin?addStatusBarItem ()
"Status Bar Text" |> U2.Case1 |> statusBarItemEl.setText
// This adds a simple command that can be triggered anywhere
plugin?addCommand (
let mutable cmd = defaultCommand ()
cmd.id <- "open-sample-modal-simple"
cmd.name <- "Open sample modal (simple)"
cmd.callback <- Some (fun _ ->
Some((new SampleModal(plugin?app) :> obj :?> Modal).``open`` ())
)
cmd
)
// This adds an editor command that can perform some operation on the current editor instance
plugin?addCommand (
let mutable cmd = defaultCommand ()
cmd.id <- "sample-editor-command"
cmd.name <- "Sample editor command"
cmd.editorCallback <- Some (fun (editor : Editor) (view : MarkdownView) ->
printf "%A" (editor.getSelection ())
let replacement = editor.replaceSelection ("Sample Editor Command")
Some replacement
)
cmd
)
// This adds a complex command that can check whether the current state of the app allows execution of the command
plugin?addCommand (
let mutable cmd = defaultCommand ()
cmd.id <- "open-sample-modal-complex"
cmd.name <- "Open sample modal (complex)"
cmd.checkCallback <- Some (fun checking ->
// Conditions to check
let app : App = plugin?app
let markdownView =
obsidian.MarkdownView :?> Constructor<MarkdownView>
|> app.workspace.getActiveViewOfType
match markdownView with
| Some markdownView ->
// If checking is true, we're simply "checking" if the command can be run.
// If checking is false, then we want to actually perform the operation.
if checking |> not then
Some((new SampleModal(plugin?app) :> obj :?> Modal).``open`` ())
|> ignore
// This command will only show up in Command Palette when the check function returns true
true |> U2.Case1
| None ->
false |> U2.Case1
)
cmd
)
// This adds a settings tab so the user can configure various aspects of the plugin
plugin?addSettingTab (new SamplePluginSettingTab (plugin?app, plugin))
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this plugin)
// Using this function will automatically remove the event listener when this plugin is disabled.
plugin?registerDomEvent (Browser.Dom.document, "click", fun (evt : Browser.Types.MouseEvent) -> (
printfn "click: %A" evt
))
// When registering intervals, this function will automatically clear the interval when the plugin is disabled.
plugin?registerInterval Browser.Dom.window.setInterval (fun () -> printfn "setInterval") (5 * 60 * 1000)
()
let onload: unit -> unit = fun _ -> plugin?settings <- plugin?loadSettings ()
do init ()
do plugin?onload <- onload
// DO NOT REMOVE
// Change the name here to match the type name used above
emitJsStatement "" "module.exports = SamplePlugin";

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# Obsidian Sample Plugin F\#
This is a recreation of the [official Obsidian sample plugin](https://github.com/obsidianmd/obsidian-sample-plugin) in F\# using Fable as a proof of concept.

11
justfile Executable file
View File

@ -0,0 +1,11 @@
build:
dotnet fable --lang javascript --noCache
esbuild ./Program.fs.js --bundle --external:obsidian --outfile=./out/main.js --format=cjs
cp -a ./meta/. ./out/
clean:
rm -rf ./fable_modules/ ./bin/ ./obj/ ./out/ *.fs.js

10
meta/manifest.json Normal file
View File

@ -0,0 +1,10 @@
{
"id": "obsidian-sample-plugin-fs",
"name": "FSharp Sample Plugin",
"version": "1.0.0",
"minAppVersion": "0.15.0",
"description": "Sample project to demonstrate how to use F# and Fable to create an Obsidian plugin.",
"author": "Aaron Manning",
"authorUrl": "https://aaronmanning.net",
"isDesktopOnly": false
}

3
meta/versions.json Normal file
View File

@ -0,0 +1,3 @@
{
"1.0.0": "0.15.0"
}

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<RootNamespace>obsidian_sample_plugin_fs</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Compile Include="Obsidian.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fable.Browser.Css" Version="2.3.0" />
<PackageReference Include="Fable.Browser.Dom" Version="2.14.0" />
<PackageReference Include="Fable.Browser.XMLHttpRequest" Version="1.3.0" />
<PackageReference Include="Fable.Core" Version="4.0.0" />
<PackageReference Include="Fable.Promise" Version="3.2.0" />
</ItemGroup>
</Project>