chore: 基于 obsidianmd/obsidian-sample-plugin 初始化项目

This commit is contained in:
Allen7D 2024-04-26 10:47:51 +08:00
parent e60294b950
commit adc0cacb18
17 changed files with 4523 additions and 230 deletions

1
.gitignore vendored
View File

@ -11,6 +11,7 @@ node_modules
# Don't include the compiled main.js file in the repo. # Don't include the compiled main.js file in the repo.
# They should be uploaded to GitHub releases instead. # They should be uploaded to GitHub releases instead.
main.js main.js
dist
# Exclude sourcemaps # Exclude sourcemaps
*.map *.map

11
.prettierignore Normal file
View File

@ -0,0 +1,11 @@
.DS_Store
package.json
/dist
.eslintignore
.editorconfig
.gitignore
.prettierignore
LICENSE
.eslintcache
*.lock
yarn-error.log

7
.prettierrc Normal file
View File

@ -0,0 +1,7 @@
{
"semi": true,
"printWidth": 120,
"tabWidth": 2,
"singleQuote": true,
"trailingComma": "all"
}

View File

@ -1,96 +1 @@
# Obsidian Sample Plugin # Obsidian Code Editor
This is a sample plugin for Obsidian (https://obsidian.md).
This project uses Typescript to provide type checking and documentation.
The repo depends on the latest plugin API (obsidian.d.ts) in Typescript Definition format, which contains TSDoc comments describing what it does.
**Note:** The Obsidian API is still in early alpha and is subject to change at any time!
This sample plugin demonstrates some of the basic functionality the plugin API can do.
- Adds a ribbon icon, which shows a Notice when clicked.
- Adds a command "Open Sample Modal" which opens a Modal.
- Adds a plugin setting tab to the settings page.
- Registers a global click event and output 'click' to the console.
- Registers a global interval which logs 'setInterval' to the console.
## First time developing plugins?
Quick starting guide for new plugin devs:
- Check if [someone already developed a plugin for what you want](https://obsidian.md/plugins)! There might be an existing plugin similar enough that you can partner up with.
- Make a copy of this repo as a template with the "Use this template" button (login to GitHub if you don't see it).
- Clone your repo to a local development folder. For convenience, you can place this folder in your `.obsidian/plugins/your-plugin-name` folder.
- Install NodeJS, then run `npm i` in the command line under your repo folder.
- Run `npm run dev` to compile your plugin from `main.ts` to `main.js`.
- Make changes to `main.ts` (or create new `.ts` files). Those changes should be automatically compiled into `main.js`.
- Reload Obsidian to load the new version of your plugin.
- Enable plugin in settings window.
- For updates to the Obsidian API run `npm update` in the command line under your repo folder.
## Releasing new releases
- Update your `manifest.json` with your new version number, such as `1.0.1`, and the minimum Obsidian version required for your latest release.
- Update your `versions.json` file with `"new-plugin-version": "minimum-obsidian-version"` so older versions of Obsidian can download an older version of your plugin that's compatible.
- Create new GitHub release using your new version number as the "Tag version". Use the exact version number, don't include a prefix `v`. See here for an example: https://github.com/obsidianmd/obsidian-sample-plugin/releases
- Upload the files `manifest.json`, `main.js`, `styles.css` as binary attachments. Note: The manifest.json file must be in two places, first the root path of your repository and also in the release.
- Publish the release.
> You can simplify the version bump process by running `npm version patch`, `npm version minor` or `npm version major` after updating `minAppVersion` manually in `manifest.json`.
> The command will bump version in `manifest.json` and `package.json`, and add the entry for the new version to `versions.json`
## Adding your plugin to the community plugin list
- Check https://github.com/obsidianmd/obsidian-releases/blob/master/plugin-review.md
- Publish an initial version.
- Make sure you have a `README.md` file in the root of your repo.
- Make a pull request at https://github.com/obsidianmd/obsidian-releases to add your plugin.
## How to use
- Clone this repo.
- Make sure your NodeJS is at least v16 (`node --version`).
- `npm i` or `yarn` to install dependencies.
- `npm run dev` to start compilation in watch mode.
## Manually installing the plugin
- Copy over `main.js`, `styles.css`, `manifest.json` to your vault `VaultFolder/.obsidian/plugins/your-plugin-id/`.
## Improve code quality with eslint (optional)
- [ESLint](https://eslint.org/) is a tool that analyzes your code to quickly find problems. You can run ESLint against your plugin to find common bugs and ways to improve your code.
- To use eslint with this project, make sure to install eslint from terminal:
- `npm install -g eslint`
- To use eslint to analyze this project use this command:
- `eslint main.ts`
- eslint will then create a report with suggestions for code improvement by file and line number.
- If your source code is in a folder, such as `src`, you can use eslint with this command to analyze all files in that folder:
- `eslint .\src\`
## Funding URL
You can include funding URLs where people who use your plugin can financially support it.
The simple way is to set the `fundingUrl` field to your link in your `manifest.json` file:
```json
{
"fundingUrl": "https://buymeacoffee.com"
}
```
If you have multiple URLs, you can also do:
```json
{
"fundingUrl": {
"Buy Me a Coffee": "https://buymeacoffee.com",
"GitHub Sponsor": "https://github.com/sponsors",
"Patreon": "https://www.patreon.com/"
}
}
```
## API Documentation
See https://github.com/obsidianmd/obsidian-api

View File

@ -1,43 +1,45 @@
import esbuild from "esbuild"; import esbuild from 'esbuild';
import process from "process"; import process from 'process';
import builtins from "builtin-modules"; import builtins from 'builtin-modules';
import { lessLoader } from 'esbuild-plugin-less';
const banner = const banner = `/*
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin if you want to view the source, please visit the github repository of this plugin
*/ */
`; `;
const prod = (process.argv[2] === "production"); const prod = process.argv[2] === 'production';
const context = await esbuild.context({ const context = await esbuild.context({
banner: { banner: {
js: banner, js: banner,
}, },
entryPoints: ["main.ts"], entryPoints: ['src/main.ts'],
bundle: true, bundle: true,
external: [ external: [
"obsidian", 'obsidian',
"electron", 'electron',
"@codemirror/autocomplete", '@codemirror/autocomplete',
"@codemirror/collab", '@codemirror/collab',
"@codemirror/commands", '@codemirror/commands',
"@codemirror/language", '@codemirror/language',
"@codemirror/lint", '@codemirror/lint',
"@codemirror/search", '@codemirror/search',
"@codemirror/state", '@codemirror/state',
"@codemirror/view", '@codemirror/view',
"@lezer/common", '@lezer/common',
"@lezer/highlight", '@lezer/highlight',
"@lezer/lr", '@lezer/lr',
...builtins], ...builtins,
format: "cjs", ],
target: "es2018", format: 'cjs',
logLevel: "info", target: 'es2018',
sourcemap: prod ? false : "inline", logLevel: 'info',
sourcemap: prod ? false : 'inline',
treeShaking: true, treeShaking: true,
outfile: "main.js", outfile: 'main.js',
plugins: [lessLoader()],
}); });
if (prod) { if (prod) {

2395
main.css Normal file

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,11 @@
{ {
"id": "sample-plugin", "id": "code-editor",
"name": "Sample Plugin", "name": "Code Editor",
"version": "1.0.0", "version": "1.0.0",
"minAppVersion": "0.15.0", "minAppVersion": "0.15.0",
"description": "Demonstrates some of the capabilities of the Obsidian API.", "description": "Demonstrates some of the capabilities of the Obsidian API.",
"author": "Obsidian", "author": "Allen7D",
"authorUrl": "https://obsidian.md", "authorUrl": "https://github.com/Allen7D",
"fundingUrl": "https://obsidian.md/pricing", "fundingUrl": "https://github.com/Allen7D/obsidian-code-editor",
"isDesktopOnly": false "isDesktopOnly": false
} }

View File

@ -1,5 +1,5 @@
{ {
"name": "obsidian-sample-plugin", "name": "obsidian-code-editor",
"version": "1.0.0", "version": "1.0.0",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)", "description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"main": "main.js", "main": "main.js",
@ -13,12 +13,22 @@
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@types/node": "^16.11.6", "@types/node": "^16.11.6",
"@types/react": "^18.2.69",
"@types/react-dom": "^18.2.22",
"@typescript-eslint/eslint-plugin": "5.29.0", "@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0", "@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0", "builtin-modules": "3.3.0",
"esbuild": "0.17.3", "esbuild": "0.17.3",
"esbuild-plugin-less": "^1.3.3",
"less": "^4.2.0",
"obsidian": "latest", "obsidian": "latest",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tslib": "2.4.0", "tslib": "2.4.0",
"typescript": "4.7.4" "typescript": "4.7.4"
},
"dependencies": {
"monaco-editor": "^0.44.0",
"react-monaco-editor": "^0.55.0"
} }
} }

31
src/SettingTab.ts Normal file
View File

@ -0,0 +1,31 @@
import { App, PluginSettingTab, Setting } from 'obsidian';
import ExamplePlugin from './main';
// 属性设置页
export class SettingTab extends PluginSettingTab {
plugin: ExamplePlugin;
constructor(app: App, plugin: ExamplePlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName('Setting #1')
.setDesc("It's a secret")
.addText((text) =>
text
.setPlaceholder('Enter your secret')
.setValue(this.plugin.settings.mySetting)
.onChange(async (value) => {
this.plugin.settings.mySetting = value;
await this.plugin.saveSettings();
}),
);
}
}

View File

@ -0,0 +1,3 @@
.app-main {
color: red;
}

View File

@ -0,0 +1,13 @@
import React from 'react';
import './index.less';
const App: React.FC = () => {
return (
<div className="app-main">
<h1>App Home</h1>
</div>
);
};
export default App;

View File

@ -0,0 +1,133 @@
import React from 'react';
import { Editor, Plugin } from 'obsidian';
import MonacoEditor from 'react-monaco-editor';
import 'monaco-editor/esm/vs/basic-languages/typescript/typescript.contribution';
import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution';
import 'monaco-editor/esm/vs/basic-languages/html/html.contribution';
import 'monaco-editor/esm/vs/basic-languages/css/css.contribution';
interface CodeEditorProps {
plugin: Plugin;
}
const options = {
selectOnLineNumbers: true, // 显示行号默认true
roundedSelection: false, //
readOnly: false, // 是否切换只读默认false
// cursorStyle: 'line', // 光标样式
automaticLayout: true, // 自适应布局默认为true
fontSize: 14, // 字体大小
tabSize: 2, // tab 缩进长度(包括回车换行后的自动缩进)
scrollBeyondLastLine: false, // 取消代码后面一大段空白为true时editor的高度会大于父容器
contextmenu: true, // 编辑器原生的右键菜单
};
const CodeEditor = (props: CodeEditorProps) => {
const { plugin } = props;
const [value, setValue] = React.useState<string>('');
const [language, setLanguage] = React.useState<string>('');
return (
<MonacoEditor
options={options}
width="100%"
height="600"
language={language}
theme="vs-dark"
value={value}
editorDidMount={() => {
const obsidianEditor = plugin.app.workspace.activeEditor?.editor!;
const [startLine, endLine, language] = getBoundaryLines(obsidianEditor, '```');
const textContent = getEditorContent(obsidianEditor, startLine + 1, endLine - 1);
setLanguage(language);
setValue(textContent);
}}
editorWillUnmount={(monacoEditor) => {
const obsidianEditor = plugin.app.workspace.activeEditor?.editor!;
const [startLine, endLine, language] = getBoundaryLines(obsidianEditor, '```');
const editorValue = monacoEditor.getValue();
obsidianEditor?.replaceRange(
`${editorValue}\n`,
{ line: startLine + 1, ch: 0 }, // 替换代码块内容的起始位置
{ line: endLine, ch: 0 }, // 替换代码块内容的结束位置
);
monacoEditor.dispose();
}}
/>
);
};
export default CodeEditor;
function getBoundaryLines(editor: Editor, target: string): [number, number, string] {
const cursor = editor.getCursor();
let startLine = cursor.line;
let endLine = cursor.line;
// Find the upper boundary line
for (let i = startLine; i >= 0; i--) {
if (editor.getLine(i).includes(target)) {
startLine = i;
break;
}
}
// Find the lower boundary line
const lineCount = editor.lineCount();
for (let i = endLine; i < lineCount; i++) {
if (editor.getLine(i).includes(target)) {
endLine = i;
break;
}
}
const languageKey = editor.getLine(startLine).split('```')[1].trim();
const language = matchLanguage(languageKey)!;
return [startLine, endLine, language];
}
function matchLanguage(languageKey: string) {
switch (languageKey) {
case 'js':
case 'es6':
case 'jsx':
case 'cjs':
case 'mjs':
return 'javascript';
case 'ts':
case 'tsx':
case 'cts':
case 'mts':
return 'typescript';
case 'css':
return 'css';
case 'html':
case 'htm':
case 'shtml':
case 'xhtml':
case 'mdoc':
case 'jsp':
case 'asp':
case 'aspx':
case 'jshtm':
return 'html';
case 'json':
return 'json';
}
}
function getEditorContent(editor: Editor, startLine: number, endLine: number): string {
const editorContent = editor.getRange({ line: startLine, ch: 0 }, { line: endLine + 1, ch: 0 });
return editorContent.trimEnd();
// 优化以下的代码
// const lines = [];
// for (let i = startLine; i <= endLine; i++) {
// lines.push(editor.getLine(i));
// }
// return lines.join('\n');
}

View File

@ -0,0 +1,31 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Plugin, Modal } from 'obsidian';
import CodeEditor from './CodeEditor';
class CodeEditorModal extends Modal {
plugin: Plugin;
constructor(plugin: Plugin) {
super(plugin.app);
this.plugin = plugin;
}
// 调用 Modal 的 open 方法是触发的回调函数
onOpen() {
// 设置 Modal 的宽高
this.modalEl.setCssProps({
'--dialog-width': '80vw',
'--dialog-height': '80vh',
});
// 将 CodeEditor 组件挂载到 Modal 的 contentEl
ReactDOM.render(<CodeEditor plugin={this.plugin} />, this.contentEl);
}
// 调用 Modal 的 close 方法是触发的回调函数
onClose() {
ReactDOM.unmountComponentAtNode(this.contentEl);
this.contentEl.empty();
}
}
export default CodeEditorModal;

View File

@ -1,23 +1,43 @@
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; import { App, Editor, MarkdownView, Menu, MenuItem, Modal, Notice, Plugin } from 'obsidian';
import { SettingTab } from './SettingTab';
import { View } from './view';
import CodeEditorModal from './components/CodeEditorModal';
// Remember to rename these classes and interfaces! // Remember to rename these classes and interfaces!
interface MyPluginSettings { interface ExamplePluginSettings {
mySetting: string; mySetting: string;
} }
const DEFAULT_SETTINGS: MyPluginSettings = { const DEFAULT_SETTINGS: ExamplePluginSettings = {
mySetting: 'default' mySetting: 'default',
} };
export default class MyPlugin extends Plugin { export default class ExamplePlugin extends Plugin {
settings: MyPluginSettings; settings: ExamplePluginSettings;
async onload() { async onload() {
this.registerView('exapmle', (leaf) => new View(leaf));
await this.loadSettings(); await this.loadSettings();
this.registerCommands();
this.registerEvent(
this.app.workspace.on('editor-menu', (menu: Menu) => {
menu.addItem((item: MenuItem) => {
item
.setTitle('在编辑器中编辑代码')
.setIcon('code')
.onClick(() => {
new CodeEditorModal(this).open();
});
});
}),
);
// This creates an icon in the left ribbon. // This creates an icon in the left ribbon.
const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin', (evt: MouseEvent) => { const ribbonIconEl = this.addRibbonIcon('dice', 'Sample Plugin1', (evt: MouseEvent) => {
// Called when the user clicks the icon. // Called when the user clicks the icon.
new Notice('This is a notice!'); new Notice('This is a notice!');
}); });
@ -28,45 +48,8 @@ export default class MyPlugin extends Plugin {
const statusBarItemEl = this.addStatusBarItem(); const statusBarItemEl = this.addStatusBarItem();
statusBarItemEl.setText('Status Bar Text'); statusBarItemEl.setText('Status Bar Text');
// This adds a simple command that can be triggered anywhere
this.addCommand({
id: 'open-sample-modal-simple',
name: 'Open sample modal (simple)',
callback: () => {
new SampleModal(this.app).open();
}
});
// This adds an editor command that can perform some operation on the current editor instance
this.addCommand({
id: 'sample-editor-command',
name: 'Sample editor command',
editorCallback: (editor: Editor, view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('Sample Editor Command');
}
});
// This adds a complex command that can check whether the current state of the app allows execution of the command
this.addCommand({
id: 'open-sample-modal-complex',
name: 'Open sample modal (complex)',
checkCallback: (checking: boolean) => {
// Conditions to check
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (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) {
new SampleModal(this.app).open();
}
// This command will only show up in Command Palette when the check function returns true
return true;
}
}
});
// This adds a settings tab so the user can configure various aspects of the plugin // This adds a settings tab so the user can configure various aspects of the plugin
this.addSettingTab(new SampleSettingTab(this.app, this)); this.addSettingTab(new SettingTab(this.app, this));
// If the plugin hooks up any global DOM events (on parts of the app that doesn't belong to this 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. // Using this function will automatically remove the event listener when this plugin is disabled.
@ -78,12 +61,54 @@ export default class MyPlugin extends Plugin {
this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000)); this.registerInterval(window.setInterval(() => console.log('setInterval'), 5 * 60 * 1000));
} }
onunload() { onunload() {}
// 注册快捷命令
registerCommands() {
// This adds a simple command that can be triggered anywhere
this.addCommand({
id: 'open-sample-modal-simple',
name: 'Open sample modal (simple)',
callback: () => {
new CommandModal(this.app).open();
},
});
// This adds an editor command that can perform some operation on the current editor instance
this.addCommand({
id: 'sample-editor-command',
name: 'Sample editor command',
// 对当前的编辑器内容进行处理
editorCallback: (editor: Editor, view: MarkdownView) => {
console.log(editor.getSelection());
editor.replaceSelection('Sample Editor Command');
},
});
// This adds a complex command that can check whether the current state of the app allows execution of the command
this.addCommand({
id: 'open-sample-modal-complex',
name: 'Open sample modal (complex)',
// checkCallback 允许命令行执行前进行校验
checkCallback: (checking: boolean) => {
// Conditions to check
// markdownView 为当前的 markdown 文件,如果当前没有 markdown 文件(即激活的 Tab 为空),则返回 null
const markdownView = this.app.workspace.getActiveViewOfType(MarkdownView);
if (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) {
new CommandModal(this.app).open();
}
// This command will only show up in Command Palette when the check function returns true
return true;
}
},
});
} }
async loadSettings() { async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); const localSettings = await this.loadData(); // 加载 data.json 配置
this.settings = Object.assign({}, DEFAULT_SETTINGS, localSettings);
} }
async saveSettings() { async saveSettings() {
@ -91,44 +116,19 @@ export default class MyPlugin extends Plugin {
} }
} }
class SampleModal extends Modal { // 弹窗
class CommandModal extends Modal {
constructor(app: App) { constructor(app: App) {
super(app); super(app);
} }
onOpen() { onOpen() {
const {contentEl} = this; const { contentEl } = this;
contentEl.setText('Woah!'); contentEl.setText('Woah!');
} }
onClose() { onClose() {
const {contentEl} = this; const { contentEl } = this;
contentEl.empty(); contentEl.empty();
} }
} }
class SampleSettingTab extends PluginSettingTab {
plugin: MyPlugin;
constructor(app: App, plugin: MyPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const {containerEl} = this;
containerEl.empty();
new Setting(containerEl)
.setName('Setting #1')
.setDesc('It\'s a secret')
.addText(text => text
.setPlaceholder('Enter your secret')
.setValue(this.plugin.settings.mySetting)
.onChange(async (value) => {
this.plugin.settings.mySetting = value;
await this.plugin.saveSettings();
}));
}
}

39
src/view.tsx Normal file
View File

@ -0,0 +1,39 @@
import React from 'react';
import { createRoot, Root } from 'react-dom/client';
import { TextFileView, TFile, WorkspaceLeaf } from 'obsidian';
import App from './components/App';
export class View extends TextFileView {
root: Root;
constructor(leaf: WorkspaceLeaf) {
super(leaf);
}
getViewData(): string {
throw new Error('Method not implemented.');
}
setViewData(data: string, clear: boolean): void {
throw new Error('Method not implemented.');
}
clear(): void {
throw new Error('Method not implemented.');
}
getViewType(): string {
throw new Error('Method not implemented.');
}
async render(file: TFile) {
this.root = this.root || createRoot(this.containerEl.children[1]);
let fileData = await this.app.vault.read(file);
console.log('fileData', fileData);
this.root?.render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
}
}

View File

@ -1,24 +1,19 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",
"inlineSourceMap": true, "inlineSourceMap": true,
"inlineSources": true, "inlineSources": true,
"module": "ESNext", "module": "ESNext",
"target": "ES6", "target": "ES6",
"allowJs": true, "allowJs": true,
"noImplicitAny": true, "noImplicitAny": true,
"moduleResolution": "node", "moduleResolution": "node",
"importHelpers": true, "importHelpers": true,
"isolatedModules": true, "isolatedModules": true,
"strictNullChecks": true, "strictNullChecks": true,
"lib": [ "lib": ["DOM", "ES5", "ES6", "ES7"],
"DOM", "jsx": "react",
"ES5", "allowSyntheticDefaultImports": true
"ES6", },
"ES7" "include": ["**/*.ts", "**/*.tsx"]
]
},
"include": [
"**/*.ts"
]
} }

1717
yarn.lock Normal file

File diff suppressed because it is too large Load Diff