辛宝Otto

辛宝Otto 的玄酒清谈

北漂前端程序员儿 / 探索新事物 / Web Worker 主播之一/内向话痨
xiaoyuzhou
email

Quick Guide to Obsidian Docs - Focus on Plugin Development

image.png

Internal Links:

  • [[【Technical Perspective on xlog】Smoother User Experience 3 Implementing an Obsidian Plugin]]

Related:

There is a lot of official content:

  • Plugin development, the core of this session
  • Theme development. Ignored this time
  • Refer listing APIs
  • Others

You can scroll down to see quick code snippets

Plugin Development#

I want to explain the structure and basic usage by introducing a simple plugin written in pure JS. This plugin is very practical and can help us better develop and debug. For convenience, I recommend first installing a plugin called "hot-reload," which you can find at https://github.com/pjeby/hot-reload.

For users using Vue, I also recommend a template that combines Vue3, Vite4, and Obsidian plugins, making it easier for you to start development. You can find this template at https://github.com/Otto-J/Obsidian-Vue-Starter.

Before explaining the specific usage of the plugin, I want to introduce the plugin lifecycle. The plugin lifecycle includes two phases: onload and onunload.

Mobile plugins are ignored.

Pages developed with Vue are mounted onto ItemView. This means we can use various Vue features in ItemView to develop custom pages.

  • Start
    • Introduce the structure and basic usage through a simple pure JS plugin
    • Be sure to install https://github.com/pjeby/hot-reload early for development and debugging
    • Vue users can use this template, vue3+vite4+obsidian plugin
    • Lifecycle onload - onunload
    • Mobile plugins provide how to enable simulation, just to know
    • Pages developed with Vue are mounted onto ItemView
  • UI
    • Introduction to UI structure, aligning the names of different areas
      • Show an image that introduces different positions; sidebar is called ribbon actions, which is a bit strange
      • This is the image Pasted image 20230909121152
    • 【Command Palette】
      • Similar to vsCode, use cmd + p to bring up the command palette; plugin development registers using this.addCommand method in onload, which is not complicated, for example, handling the content of the currently active editor
      • Commands can have pre-checks using the checkCallback method to verify if they should run, adding them all saves trouble
      • You can access the editor with the editorCallback method, which has parameters for editor and view, and there is also an editorCheckCallback option
      • You can register hotkeys to add another trigger method. Details ignored
    • 【Right-click Menu】
      • Import obsidian.Menu, instantiate menu.addItem can be added, this is placed in the sidebar
      • It can also be placed in the file list file-menu and editor-menu at the top right of the editor Pasted image 20230909125832
    • 【HTML Container】 can place HTML containers in the plugin settings, in PluginSettingTab
      • You can use traditional JS createEl, similar to h rendering functions.
      • The styles.css in the plugin root directory, use Vue to simplify
    • 【Icons】 can be found here, https://lucide.dev/ supports Flutter quite well
      • Custom icons need to prepare the original SVG content, no need to worry about Vue icons
    • 【Modals】 pop-ups for user input
      • Import interface from Obsidian.Modal, need to implement onOpen and onClose methods
      • There are corresponding callbacks when clicking submit
      • There is also a SuggestModal for assisting input, and FuzzySuggestModal can be ignored for now
    • 【Sidebar】 the official term is ribbon actions
      • Usage is this.addRibbonIcon, which will definitely be mentioned in the demo
    • 【Settings】 this is the public settings, where the plugin fills in configurations
      • Plugin settings are persistent, so the process is to first load this.loadData to merge and get the configuration result
      • The official example code is the same as the documentation
    • 【Status Bar】 the status bar at the bottom right
      • this.addStatusBarItem to add
      • Multiple can be added
    • 【Views】 resource management, editor, preview are all views
      • Get view ID getViewType
    • 【Workspace】 not understood, tree structure
      • Can open multiple documents side by side, split way
      • Not understood, the documentation is poorly written
      • The keyword is Leaf something something
      • Just look at the quick operation code snippets below
  • Editor
    • 【Editor】 Class to get and edit the MD in editing mode
      • See the code below
    • 【Markdown Post-processing】 the MD in preview mode is HTML, can adjust HTML
      • Example 1, replace with emoji icons
      • Example 2, parse and add special syntax content, oh, it's data transformation, like converting CSV to table
    • 【Editor Extensions】 can be ignored for now, introducing CodeMirror6 content
      • The underlying dependency is CodeMirror6, which is used for writing extensions, not looking at this now
      • 【Decorators】 supplement styles for the above editor extension content, not related
        • Not looking at this now
      • 【State Fields】 editor extension, not understood
      • 【State Management】 state management for editor extensions
        • If you want to undo, you need to retain the state
      • 【View Plugins】 editor extension plugins, can access viewport, ignored
      • 【Viewport】 high-performance editors cannot do without virtual scrolling to avoid lag; the viewport will update and recalculate when scrolling the document
      • 【Editor Communication】 ignored
  • Releasing
    • CI operations - the documentation provides a Github action to assist in building plugins
    • Before submitting to the official market, provide Readme.md license, mainfest.json, the official example is available
    • Just follow the documentation, the official maintains a JSON, just submit a PR
    • Note that Node and Electron are only allowed for desktop use
    • If it's a beta version, consider the brat plugin
  • Event
    • We might want a timer, like setTimeInterval to try updating something; there is a dedicated this.registerInterval method this.registerInterval( window.setInterval(() => this.updateStatusBar(), 1000) );
    • Time formatting, built-in obsidian.moment
  • Vault
    • A collection of notes, encapsulating these FS operations
    • Document read-only without operations use cachedRead()
    • Document read and modify can use read()
    • Modify file using vault.modify(), this is an overwrite operation
    • vault.process callback address has data indicating current content, only supports synchronous operations
    • For asynchronous modifications consider vault.cachedRead() - asynchronous operation - vault.process() update
    • Delete file trash() is the recycle bin, permanently delete is delete()
    • app.vault.getAbstractFileByPath("folderOrFile") the result of the absolute path may be a file or folder, determined by instanceof TFile/ TFolder

Common Snippets#

There are two methods to read the content of a file: read() and cachedRead()

  • If you only want to display content to the user, use to avoid multiple reads from disk. cachedRead()
  • If you want to read content, modify it, and write it back to disk, then use to avoid possibly overwriting the file with an outdated copy. read()
// Get the content of the active text
app.workspace.getActiveFileView().data

// Also can
app.workspace.activeEditor

// User activated cursor position
app.workspace.getActiveFileView().editor.getCursor()
// {line:2,ch:2}

// Append text at a specified position, like the current date or template
app.workspace.getActiveFileView().editor.replaceRange('222',{line: 2, ch: 2})

// The currently active editor
const editor = app.workspace.getActiveFileView().editor

// User selected a portion of content
app.workspace.getActiveFileView().editor.getSelection()

// Replace content, for example, after upload becomes CDN address sha
editor.replaceSelection('xxx')

// Get all MD documents
app.vault.getMarkdownFiles()

// Get all files, then filter
app.vault.getFile()

// Get text content
var file = app.vault.getMarkdownFiles()[0]
const content = await app.vault.cachedRead(file)

// Modify content, this is a synchronous operation
app.vault.process(file, (data) => { return data.replace(":)", "🙂"); })

this.app.workspace.on('editor-paste', (evt, editor, view) => {
if (evt.clipboardData) {}
}))

// Can get the current file file
this.app.workspace.on("file-menu", (menu, file) => {
if (file instanceof TFile) {}
});

// Given a string path, check if the file exists, safer
const isExist = await this.app.vault.adapter.exists('xx.md')

// Given a string, find the best matching file
app.metadataCache.getFirstLinkpathDest('folder/download.png','')

// Get TFile to read content
app.metadataCache.getFileCache(file)
// where embeds indicate associated images, but do not include external link standard syntax, need to be used together
// frontmatter is among them
// frontmatterPosition is the start and end position of fm, can be deleted and replaced
// links indicate external links of the current article

// Read binary content of an image, later display and upload the blob
const conArrayBuffer = await plugin.app.vault.readBinary(TFile);
// Convert to binary, upload via post
const blob = new Blob([conArrayBuffer], {
    type: "image/" + obInnerFile.extension,
});

Handling front-matter is quite common and troublesome, providing a common solution

export const useObsidianFrontmatter = (file: TFile, app: App) => {
  // Use more semantic function names
  const doesFileExist = () => !!app.metadataCache.getFileCache(file);

  const currentFrontMatter = () =>
    app.metadataCache.getFileCache(file)?.frontmatter ?? {};
  const addOrUpdateFrontMatter = async (obj: Record<string, string>) => {
    const fileCache = app.metadataCache.getFileCache(file);
    // If the file does not exist, return directly
    if (!fileCache) {
      new Notice("File does not exist");
      return;
    }

    const currentFrontMatter = fileCache?.frontmatter ?? {};
    const newFrontMatter = `---\n${stringifyYaml({
      ...currentFrontMatter,
      ...obj,
    })}\n---\n`;

    // const { frontmatterPosition } = fileCache;
    // readcontent or con = vault.cachedRead(file)
    const fileContents = await app.vault.read(file);

    const frontmatterPosition = fileCache.frontmatterPosition ?? {
      start: {
        line: 0,
        col: 0,
        offset: 0,
      },
      end: {
        line: 0,
        col: 0,
        offset: 0,
      },
    };

    // The logic here is a bit convoluted, the goal is to rewrite the file content; if there is an API later, it might be solved with one line of code, similar to metadataCache.update
    const {
      start: { offset: deleteFrom },
      end: { offset: deleteTo },
    } = frontmatterPosition;

    const newFileContents =
      fileContents.slice(0, deleteFrom) +
      newFrontMatter +
      fileContents.slice(deleteTo);

    await app.vault.modify(file, newFileContents);
  };

  return {
    doesFileExist,
    addOrUpdateFrontMatter,
    currentFrontMatter,
  };
};

Quick Familiarization with TS API#

Official Documentation is particularly poorly written, and I haven't seen good introductions around.

Quickly list

  • abstract-input-suggest provides some hints in the input box, like historical tags
  • abstract-text-component seems to be related to text input box abstract classes
  • app application-level properties, can access files, vault, hotkeys, workspace, etc.
  • base-component can set disabled
  • class BlockCache extends CacheItem
  • block-sub-path-result
  • button-component button component, no need to look, unified packaging
  • cached-metadata--interface
  • cache-item--interface
  • closeable-component--class provides close methods, no need to look, all packaged
  • ColorComponent--class, color picker, can get color hex, can be packaged
  • Command--interface registers command-related methods
  • component--class core entry component
  • data-adapter--interface handles files and temperature, recommended Vault's API
  • data-write-options--interface
  • debouncer--interface
  • dropdown-component--class can be packaged
  • Editor--class smooths out differences in codemirror versions, should provide many auxiliary functions
  • editor-change--interface
  • editor-position--interface
  • editor-range--interface
  • editor-range-or-caret--interface
  • editor-scroll-info
  • editorxxx skipped
  • events--class related to events
  • extra-button-component--class packaged
  • file-manager--class manages file creation, deletion, renaming from a UI perspective
  • file-stats--interface
  • file-system-adapter implements DataAdapter--class
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.