mirror of
https://github.com/zigholding/obsidian-notesync-plugin.git
synced 2026-07-22 05:43:30 +00:00
Compare commits
17 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1539fb2eb8 | ||
|
|
4ba68e4f24 | ||
|
|
58d58d13ce | ||
|
|
56f7156e2e | ||
|
|
66385c0201 | ||
|
|
6673fe5d50 | ||
|
|
a4a820ff60 | ||
|
|
09cb452eb7 | ||
|
|
af393a6ba4 | ||
|
|
36cc29a2dc | ||
|
|
d1de7bea54 | ||
|
|
d8fefd2af8 | ||
|
|
fbcbad87ad | ||
|
|
5d0fb9cb07 | ||
|
|
439da040d8 | ||
|
|
0dba51432f | ||
|
|
4910bccf99 |
24 changed files with 3152 additions and 201 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -23,4 +23,5 @@ data.json
|
|||
.DS_Store
|
||||
|
||||
package-lock.json
|
||||
otherplugins
|
||||
otherplugins
|
||||
update.sh
|
||||
137
README.md
137
README.md
|
|
@ -1,68 +1,125 @@
|
|||
|
||||
[Eng](./README.md) | [中文版](./readMe_中文.md)
|
||||
|
||||
This plugin depends on: [Templater](https://github.com/SilentVoid13/Templater) and [NoteChain](https://github.com/zigholding/obsidian-notechain-plugin)
|
||||
|
||||
### Exporting Notes
|
||||
### 导出笔记/Export Notes
|
||||
|
||||
Sync files or folders:
|
||||
1. Right-click on the file or folder in the file list;
|
||||
2. Click `Sync to other vault`;
|
||||
3. Enter the target vault root directory;
|
||||
4. Notes, folders, and attachments embedded in notes will be copied to the target vault following the same file structure. If a file with the same name exists in the target vault, it will be determined whether to overwrite based on the last modified time;
|
||||
同步文件或文件夹:
|
||||
To sync a file or folder:
|
||||
|
||||
Exporting plugins:
|
||||
1. `Note Sync: export plugin`
|
||||
2. Select the plugin to export;
|
||||
3. Choose whether to export `data.json`;
|
||||
4. Enter the plugin save directory;
|
||||
5. Press Enter to confirm.
|
||||
1. 在文件列表中,右键点击文件或文件夹;
|
||||
Right-click the file or folder in the file list;
|
||||
|
||||
2. 点击 `mirror to other vault`;
|
||||
Click `mirror to other vault`;
|
||||
|
||||
3. 输入目标库根目录;
|
||||
Enter the root directory of the target vault;
|
||||
|
||||
4. 笔记、文件夹以及笔记嵌入的附件,会按相同的文件结构复制到目标库。如果目录库中存在同名文件,则根据更新时间判定是否覆盖;
|
||||
Notes, folders, and embedded attachments will be copied to the target vault with the same structure. If a file with the same name exists, it will be overwritten only if the source is newer.
|
||||
|
||||
### Exporting Notes as readMe
|
||||
### 导出插件/Export Plugins
|
||||
|
||||
Execute `Note Sync:Set config to export note` to set export information:
|
||||
- `Dir`: Export path
|
||||
- `Name`: File name, default is readMe
|
||||
- `Assets`: Attachment storage path
|
||||
- `RemoveMeta`: Whether to remove metadata, default is true
|
||||
- `UseGitLink`: Use Git format for attachment links, default is true
|
||||
1. 执行命令 `Note Sync: export plugin`;
|
||||
Run the command `Note Sync: export plugin`;
|
||||
|
||||
2. 选择要导出的插件;
|
||||
Select the plugin you want to export;
|
||||
|
||||
3. 选择是否导出 `data.json`;
|
||||
Choose whether to export `data.json`;
|
||||
|
||||
4. 输入插件保存目录;
|
||||
Enter the directory where the plugin will be saved;
|
||||
|
||||
5. 输入回车键确认;
|
||||
Press Enter to confirm.
|
||||
|
||||
> [!NOTE]+ Example of file export configuration
|
||||
|
||||
### 将笔记导出为 readMe / Export a Note as `readMe`
|
||||
|
||||
执行 `Note Sync:Set config to export note`,设置导出信息:
|
||||
Run `Note Sync:Set config to export note` to configure the export settings:
|
||||
|
||||
- `Dir`:导出路径
|
||||
`Dir`: Export directory
|
||||
- `Name`:文件名称,默认为 readMe
|
||||
`Name`: File name (default: readMe)
|
||||
- `Assets`:附件存放路径
|
||||
`Assets`: Path to save attachments
|
||||
- `RemoveMeta`:是否移除元数据,默认为 true
|
||||
`RemoveMeta`: Whether to remove metadata (default: true)
|
||||
- `UseGitLink`:附件链接使用 Git 格式,默认 true
|
||||
`UseGitLink`: Use Git-style links for attachments (default: true)
|
||||
|
||||
> [!NOTE]+ 文件导出配置示例
|
||||
> [!NOTE]+ Example Export Configuration
|
||||
>
|
||||
> ```yaml
|
||||
> note-sync:
|
||||
> Dir: D:\github\ObsidianZ-dev\.obsidian\plugins\note-sync
|
||||
> Name: readMe_en
|
||||
> Name: readMe_中文
|
||||
> Assets: ./assets
|
||||
> RemoveMeta: true
|
||||
> UseGitLink: true
|
||||
> ```
|
||||
|
||||
Then execute `Note Sync:Export current note` to set the export note.
|
||||
然后执行 `Note Sync:Export current note` 导出当前笔记。
|
||||
Then run `Note Sync:Export current note` to export the current note.
|
||||
|
||||
### Settings Page
|
||||
---
|
||||
|
||||

|
||||
### 下载示例库文件/Download Example Vault Files
|
||||
|
||||
> [!NOTE]+ Root dir of vault
|
||||
> When exporting plugins, right-click to sync files and select the preset vault. Use line breaks to separate multiple vaults.
|
||||
在 `设置` 页面配置 `Git 仓库`,需要包含分支名称:
|
||||
Configure the `Git repository` in the **Settings** page. The URL must include the branch name:
|
||||
|
||||
> [!Danger]+ Strict mode
|
||||
> When right-clicking to sync a folder, delete notes or attachments that are in the target folder but not in the source folder. This ensures that the synced folder and the current folder are the same. This setting will delete files, please operate with caution.
|
||||
> [https://github.com/zigholding/ObsidianZ/tree/master](https://github.com/zigholding/ObsidianZ/tree/master)
|
||||
> [https://gitee.com/zigholding/ObsidianZ/tree/master](https://gitee.com/zigholding/ObsidianZ/tree/master)
|
||||
|
||||
### Functions
|
||||
执行 `下载 Git 仓库文件` 命令,依次选择仓库、文件夹和文件进行下载。输入 `all` 可下载所有文件(不包括子文件夹)。
|
||||
Run `Download git repo`, select the repository, then choose folders and files to download. Enter `all` to download all top-level files (excluding subfolders).
|
||||
|
||||
`NoteSync` provides three functions for synchronizing files and folders, which can be used in your own script notes.
|
||||

|
||||
|
||||
`mode` specifies the handling method when a file already exists in the target vault:
|
||||
- `pass`: Skip;
|
||||
- `overwrite`: Overwrite;
|
||||
- `mtime`: Take the latest modified time;
|
||||
下载文件时,也可以预设路径,快速下载:
|
||||
You can also preset the path for quick download when fetching files:
|
||||
|
||||
> [https://github.com/zigholding/ObsidianZ/tree/master/.obsidian/plugins](https://github.com/zigholding/ObsidianZ/tree/master/.obsidian/plugins)
|
||||
|
||||
|
||||
### 设置页/Settings Page
|
||||
|
||||

|
||||
|
||||
> [!NOTE]+ Root dir of vault
|
||||
> 导出插件或同步文件时,选择预设的目标库。多个库请用换行符分隔。
|
||||
> Set predefined vaults for export/sync. Use newlines to separate multiple vaults.
|
||||
|
||||
> [!Danger]+ Strict mode
|
||||
> 启用严格模式,在目标文件夹中删除源文件夹中不存在的笔记或附件。请谨慎操作,此设置会**删除文件**。
|
||||
> Enabling strict mode will delete notes or attachments in the target folder that are not present in the source. **Use with caution**—this option will delete files.
|
||||
|
||||
|
||||
### 函数/Functions
|
||||
|
||||
`NoteSync` 提供了两个个函数用于同步文件和文件夹,可用于脚本笔记中。
|
||||
`NoteSync` provides two functions to sync files and folders, which can be used in your script notes.
|
||||
|
||||
参数 `mode` 用于指定当目标库中已有文件时的处理方式:
|
||||
The `mode` parameter determines how to handle existing files in the target vault:
|
||||
- `pass`:跳过
|
||||
`pass`: Skip
|
||||
- `overwrite`:覆盖
|
||||
`overwrite`: Overwrite
|
||||
- `mtime`:根据更新时间覆盖
|
||||
`mtime`: Overwrite if the source is newer
|
||||
|
||||
```js
|
||||
let ns = app.plugins.plugins['note-sync']
|
||||
|
||||
// 同步系统文件夹
|
||||
// 同步系统文件夹
|
||||
// Sync system folder
|
||||
ns.fsEditor.sync_folder(
|
||||
src:string,
|
||||
dst:string,
|
||||
|
|
@ -70,7 +127,8 @@ ns.fsEditor.sync_folder(
|
|||
strict=false
|
||||
)
|
||||
|
||||
// 同步笔记文件夹
|
||||
// 同步 Obsidian 笔记文件夹
|
||||
// Sync Obsidian TFolder
|
||||
ns.fsEditor.sync_tfolder(
|
||||
tfolder:TFolder,
|
||||
vault_root:string,
|
||||
|
|
@ -79,4 +137,5 @@ ns.fsEditor.sync_tfolder(
|
|||
outlink=false,
|
||||
strict=false
|
||||
)
|
||||
```
|
||||
```
|
||||
|
||||
|
|
|
|||
BIN
assets/下载笔记.gif
Normal file
BIN
assets/下载笔记.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 346 KiB |
|
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"strict_mode": false,
|
||||
"vaultDir": ""
|
||||
"vaultDir": "D:\\iLanix\\Obsidian\nD:\\github\\ObsidianZY",
|
||||
"git_repo": "https://github.com/zigholding/ObsidianZ/tree/master\nhttps://gitee.com/zigholding/ObsidianZ/tree/master",
|
||||
"wxmp_config": "h1: ob 公众号标题 h1 样式\nh2: ob 公众号标题 h2 样式\nh3: ob 公众号标题 hx 样式\np code: ob 公众号行内代码样式\nli code: ob 公众号行内代码样式"
|
||||
}
|
||||
34
main.ts
34
main.ts
|
|
@ -1,6 +1,7 @@
|
|||
import {Notice, Plugin, TFile, TFolder } from 'obsidian';
|
||||
|
||||
import { FsEditor } from 'src/fseditor';
|
||||
import { Wxmp } from 'src/wxmp';
|
||||
import { Strings } from 'src/strings';
|
||||
import {MySettings,NoteSyncSettingTab,DEFAULT_SETTINGS} from 'src/setting'
|
||||
|
||||
|
|
@ -8,14 +9,17 @@ import { addCommands } from 'src/commands';
|
|||
|
||||
import {dialog_suggest} from 'src/gui/inputSuggester'
|
||||
import {dialog_prompt} from 'src/gui/inputPrompt'
|
||||
import {EasyAPI} from 'src/easyapi/easyapi'
|
||||
|
||||
export default class NoteSyncPlugin extends Plugin {
|
||||
strings : Strings;
|
||||
settings: MySettings;
|
||||
fsEditor : FsEditor;
|
||||
yaml: string;
|
||||
dialog_suggest: Function
|
||||
dialog_prompt: Function
|
||||
dialog_suggest: Function;
|
||||
dialog_prompt: Function;
|
||||
easyapi: EasyAPI;
|
||||
wxmp: Wxmp;
|
||||
|
||||
|
||||
async onload() {
|
||||
|
|
@ -31,9 +35,12 @@ export default class NoteSyncPlugin extends Plugin {
|
|||
async _onload_() {
|
||||
this.yaml = 'note-sync'
|
||||
this.strings = new Strings();
|
||||
this.easyapi = new EasyAPI(this.app);
|
||||
|
||||
|
||||
await this.loadSettings();
|
||||
this.fsEditor = new FsEditor(this);
|
||||
this.wxmp = new Wxmp(this);
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new NoteSyncSettingTab(this.app, this));
|
||||
addCommands(this);
|
||||
|
|
@ -59,7 +66,7 @@ export default class NoteSyncPlugin extends Plugin {
|
|||
this.fsEditor.sync_tfile(file,dst,'mtime',true,false);
|
||||
|
||||
}else if(file instanceof TFolder){
|
||||
this.fsEditor.sync_tfolder(file,dst,'mtime',true,false);
|
||||
this.fsEditor.sync_tfolder(file,dst,'mtime',true,false,this.settings.strict_mode);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -84,7 +91,7 @@ export default class NoteSyncPlugin extends Plugin {
|
|||
if(!tfile){return}
|
||||
|
||||
let mcache = this.app.metadataCache.getFileCache(tfile);
|
||||
let ctx = await this.app.vault.cachedRead(tfile)
|
||||
let ctx = await this.app.vault.read(tfile);
|
||||
|
||||
let fm: { [key: string]: any } = {};
|
||||
if(mcache && mcache['frontmatter']){
|
||||
|
|
@ -116,18 +123,21 @@ export default class NoteSyncPlugin extends Plugin {
|
|||
}
|
||||
|
||||
if(fm[this.yaml]?.RemoveMeta){
|
||||
ctx = ctx.replace(
|
||||
/---[\n(\r\n)][\s\S]*?---[\n(\r\n)]/,
|
||||
''
|
||||
)
|
||||
}
|
||||
let assets = fm[this.yaml]?.Assets
|
||||
|
||||
if(fm[this.yaml]?.UseGitLink && assets){
|
||||
if(mcache?.frontmatterPosition?.end?.offset){
|
||||
ctx = ctx.slice(mcache.frontmatterPosition.end.offset);
|
||||
}
|
||||
}
|
||||
|
||||
let assets = fm[this.yaml]?.Assets
|
||||
|
||||
if(fm[this.yaml]?.UseGitLink && assets){
|
||||
|
||||
ctx = ctx.replace(
|
||||
/\!\[\[(.*?)\]\]/g,
|
||||
(match, filename) => {
|
||||
return `})`;
|
||||
})
|
||||
}
|
||||
await this.fsEditor.fs.writeFile(
|
||||
target, ctx, 'utf-8',
|
||||
(err:Error) => {return;}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"id": "note-sync",
|
||||
"name": "Note Sync",
|
||||
"version": "0.5.0",
|
||||
"minAppVersion": "1.7.7",
|
||||
"version": "0.6.3",
|
||||
"minAppVersion": "1.8.3",
|
||||
"description": "Sync notes or plugins between vaults.",
|
||||
"author": "ZigHolding",
|
||||
"isDesktopOnly": true
|
||||
|
|
|
|||
|
|
@ -20,5 +20,9 @@
|
|||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"marked": "^9.1.6",
|
||||
"highlight.js": "^11.9.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
88
readMe_中文.md
88
readMe_中文.md
|
|
@ -1,88 +0,0 @@
|
|||
v
|
||||
[English](./README.md) | [中文版](./readMe_中文.md)
|
||||
|
||||
|
||||
本插件依赖:[Templater](https://github.com/SilentVoid13/Templater) 和 [NoteChain](https://github.com/zigholding/obsidian-notechain-plugin)
|
||||
|
||||
### 导出笔记
|
||||
|
||||
同步文件或文件夹:
|
||||
1. 在文件列表中,右键点击文件或文件夹;
|
||||
2. 点击 `mirror to other vault`;
|
||||
3. 输入目标库根目录;
|
||||
4. 笔记、文件夹以及笔记嵌入的附件,会按相同的文件结构复制到目标库。如果目录库中存在同名文件,则根据更新时间判定是否覆盖;
|
||||
|
||||
导出插件
|
||||
1. `Note Sync: export plugin`
|
||||
2. 选择要导出的插件;
|
||||
4. 选择是否导出 `data.json`;
|
||||
3. 输入插件保存目录;
|
||||
5. 输入回车键确认
|
||||
|
||||
### 将笔记导出为 readMe
|
||||
|
||||
执行 `Note Sync:Set config to export note`,设置导出信息:
|
||||
- `Dir`:导出路径
|
||||
- `Name`:文件名称,默认为 readMe
|
||||
- `Assets`:附件存放路径
|
||||
- `RemoveMeta`:是否移除元数据,默认为 true
|
||||
- `UseGitLink`:附件链接使用 Git 格式,默认 true
|
||||
|
||||
> [!NOTE]+ 文件导出配置示例
|
||||
> ```yaml
|
||||
> note-sync:
|
||||
> Dir: D:\github\ObsidianZ-dev\.obsidian\plugins\note-sync
|
||||
> Name: readMe_中文
|
||||
> Assets: ./assets
|
||||
> RemoveMeta: true
|
||||
> UseGitLink: true
|
||||
> ```
|
||||
|
||||
再执行 `Note Sync:Export current note`,设置导出笔记。
|
||||
|
||||
### 设置页
|
||||
|
||||

|
||||
|
||||
> [!NOTE]+ Root dir of vault
|
||||
> 在导出插件,右键同步文件时,选择预设的库。多个库使用换行分割。
|
||||
|
||||
> [!Danger]+ Strict mode
|
||||
> 右键同步文件夹时,删除在目标文件夹中,但不在源文件夹中的笔记或附件。保证同步的文件夹和当前文件夹是相同的。此设置会删除文件,请谨慎操作。
|
||||
|
||||
### 函数
|
||||
|
||||
`NoteSync` 提供了三个函数用于同步文件和文件夹,可以在自己的脚本笔记中使用。
|
||||
|
||||
`mode` 指当目标库中存在文件时的处理方法:
|
||||
- `pass`:跳过;
|
||||
- `overwrite`:覆盖;
|
||||
- `mtime`:取最新更新时间;
|
||||
|
||||
```js
|
||||
let ns = app.plugins.plugins['note-sync']
|
||||
|
||||
// 同步系统文件夹
|
||||
ns.fsEditor.sync_folder(
|
||||
src:string,
|
||||
dst:string,
|
||||
mode='mtime',
|
||||
strict=false
|
||||
)
|
||||
|
||||
// 同步笔记文件夹
|
||||
ns.fsEditor.sync_tfolder(
|
||||
tfolder:TFolder,
|
||||
vault_root:string,
|
||||
mode='mtime',
|
||||
attachment=true,
|
||||
outlink=false,
|
||||
strict=false
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
290
src/commands.ts
290
src/commands.ts
|
|
@ -1,28 +1,31 @@
|
|||
import {
|
||||
Notice, TFile
|
||||
import {
|
||||
Notice, TFile,
|
||||
TFolder
|
||||
} from 'obsidian';
|
||||
|
||||
import NoteSyncPlugin from '../main';
|
||||
import { it } from 'node:test';
|
||||
|
||||
const cmd_export_current_note = (plugin:NoteSyncPlugin) => ({
|
||||
const cmd_export_current_note = (plugin: NoteSyncPlugin) => ({
|
||||
id: 'export_current_note',
|
||||
name: plugin.strings.cmd_export_current_note,
|
||||
icon: 'file-export',
|
||||
callback: async () => {
|
||||
let tfile = plugin.app.workspace.getActiveFile();
|
||||
await plugin.export_readme(tfile,null);
|
||||
await plugin.export_readme(tfile, null);
|
||||
}
|
||||
});
|
||||
|
||||
const cmd_set_vexporter = (plugin:NoteSyncPlugin) => ({
|
||||
const cmd_set_vexporter = (plugin: NoteSyncPlugin) => ({
|
||||
id: 'set_vexporter',
|
||||
name: plugin.strings.cmd_set_vexporter,
|
||||
icon: 'settings',
|
||||
callback: async () => {
|
||||
let tfile = plugin.app.workspace.getActiveFile();
|
||||
if(!tfile){return}
|
||||
if (!tfile) { return }
|
||||
let dir = await plugin.dialog_prompt(plugin.strings.prompt_path_of_folder);
|
||||
let item: { [key: string]: any } = {};
|
||||
if(plugin.fsEditor.fs.existsSync(dir)){
|
||||
if (plugin.fsEditor.fs.existsSync(dir)) {
|
||||
item['Dir'] = dir;
|
||||
}
|
||||
item['Name'] = 'readMe';
|
||||
|
|
@ -33,97 +36,290 @@ const cmd_set_vexporter = (plugin:NoteSyncPlugin) => ({
|
|||
|
||||
await plugin.app.fileManager.processFrontMatter(
|
||||
tfile,
|
||||
async(fm) =>{
|
||||
async (fm) => {
|
||||
fm[plugin.yaml] = item
|
||||
}
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
const cmd_export_plugin = (plugin:NoteSyncPlugin) => ({
|
||||
const cmd_export_plugin = (plugin: NoteSyncPlugin) => ({
|
||||
id: 'export_plugin',
|
||||
name: plugin.strings.cmd_export_plugin,
|
||||
icon: 'arrow-right-from-line',
|
||||
callback: async () => {
|
||||
|
||||
|
||||
let plugins = Object.keys((plugin.app as any).plugins.plugins);
|
||||
let p = await plugin.dialog_suggest(plugins,plugins);
|
||||
let p = await plugin.dialog_suggest(plugins, plugins);
|
||||
let eplugin = (plugin.app as any).plugins.getPlugin(p);
|
||||
if(eplugin){
|
||||
if (eplugin) {
|
||||
let paths = plugin.settings.vaultDir.split("\n")
|
||||
let target = await plugin.fsEditor.select_valid_dir(
|
||||
paths
|
||||
)
|
||||
if(target){
|
||||
let items = plugin.fsEditor.list_dir(target,false)
|
||||
items = items.filter((x:string)=>x.startsWith('.') && x!='.git').filter(
|
||||
(x:string)=>{
|
||||
let path = plugin.fsEditor.path.join(target,x)
|
||||
if(!plugin.fsEditor.isdir(path)){
|
||||
if (target) {
|
||||
let items = plugin.fsEditor.list_dir(target, false)
|
||||
items = items.filter((x: string) => x.startsWith('.') && x != '.git').filter(
|
||||
(x: string) => {
|
||||
let path = plugin.fsEditor.path.join(target, x)
|
||||
if (!plugin.fsEditor.isdir(path)) {
|
||||
return false
|
||||
}
|
||||
let items = plugin.fsEditor.list_dir(path,false)
|
||||
let items = plugin.fsEditor.list_dir(path, false)
|
||||
return items.contains('plugins')
|
||||
}
|
||||
)
|
||||
if(items.length==1){
|
||||
target = plugin.fsEditor.path.join(target,items[0],'plugins')
|
||||
}else if(items.length>1){
|
||||
if (items.length == 1) {
|
||||
target = plugin.fsEditor.path.join(target, items[0], 'plugins')
|
||||
} else if (items.length > 1) {
|
||||
let item = await plugin.dialog_suggest(
|
||||
items,items,'config'
|
||||
items, items, 'config'
|
||||
)
|
||||
if(item){
|
||||
target = plugin.fsEditor.path.join(target,item,'plugins')
|
||||
if (item) {
|
||||
target = plugin.fsEditor.path.join(target, item, 'plugins')
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!plugin.fsEditor.fs.existsSync(target) ||
|
||||
plugin.fsEditor.path.basename(target)!='plugins'){
|
||||
if (!plugin.fsEditor.fs.existsSync(target) ||
|
||||
plugin.fsEditor.path.basename(target) != 'plugins') {
|
||||
target = await plugin.dialog_prompt(plugin.strings.prompt_path_of_folder);
|
||||
}
|
||||
|
||||
target = target.replace(/\\/g,'/');
|
||||
if(!target.endsWith('/' + p)){
|
||||
target = target.replace(/\\/g, '/');
|
||||
if (!target.endsWith('/' + p)) {
|
||||
target = target + '/' + p;
|
||||
}
|
||||
if(!plugin.fsEditor.fs.existsSync(target)){
|
||||
if (!plugin.fsEditor.fs.existsSync(target)) {
|
||||
plugin.fsEditor.fs.mkdirSync(target);
|
||||
}
|
||||
let items = ['main.js','manifest.json','styles.css'];
|
||||
let items = ['main.js', 'manifest.json', 'styles.css'];
|
||||
let dj = await plugin.dialog_suggest(
|
||||
[plugin.strings.item_skip_data_json,plugin.strings.item_copy_data_json],
|
||||
[false,true],
|
||||
[plugin.strings.item_skip_data_json, plugin.strings.item_copy_data_json],
|
||||
[false, true],
|
||||
''
|
||||
)
|
||||
if(dj){
|
||||
if (dj) {
|
||||
items.push('data.json')
|
||||
}
|
||||
for(let item of items){
|
||||
for (let item of items) {
|
||||
let src = `${plugin.fsEditor.root}/${eplugin.manifest.dir}/${item}`;
|
||||
let dst = `${target}/${item}`;
|
||||
let flag = plugin.fsEditor.copy_file(src,dst,'overwrite');
|
||||
if(flag){
|
||||
new Notice(`Copy ${item} to ${target}`,5000)
|
||||
let flag = plugin.fsEditor.copy_file(src, dst, 'overwrite');
|
||||
if (flag) {
|
||||
console.log(`Copy ${item} to ${target}`, 5000)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const commandBuilders:Array<Function> = [
|
||||
|
||||
const cmd_download_git_repo = (plugin: NoteSyncPlugin) => ({
|
||||
id: 'cmd_download_git_repo',
|
||||
name: plugin.strings.cmd_download_git_repo,
|
||||
icon: 'cloud-download',
|
||||
callback: async () => {
|
||||
let repos = plugin.settings.git_repo.split('\n')
|
||||
let repo = await plugin.dialog_suggest(repos, repos);
|
||||
if (!repo) { return }
|
||||
|
||||
let match = repo.match(/^https?:\/\/(.*)\.com\/([^/]*)\/([^/]*)\/tree\/([^/]*)\/?(.*)$/);
|
||||
if (!match) { return }
|
||||
|
||||
let SOURCE = match[1]; // gitee
|
||||
let repoOwner = match[2]; // 开发者
|
||||
let repoName = match[3]; // 项目名称
|
||||
let branch = match[4]; // 分支名称
|
||||
let path = match[5]; // 初始路径
|
||||
|
||||
async function list_files_of_path(repoOwner: string, repoName: string, path: string, branch = 'master') {
|
||||
let url;
|
||||
if (SOURCE == 'github') {
|
||||
url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${path}?ref=${branch}`;
|
||||
} else {
|
||||
url = `https://gitee.com/api/v5/repos/${repoOwner}/${repoName}/contents/${path}?ref=${branch}`
|
||||
}
|
||||
let req = await (window as any).requestUrl(url)
|
||||
req = JSON.parse(req.text)
|
||||
if (!Array.isArray(req)) {
|
||||
req = [req]
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
async function download_file(url: string, folder_path: string, file_name: string) {
|
||||
let req = await (window as any).requestUrl(url)
|
||||
// req = JSON.parse(req.text)
|
||||
// let ctx = atob(req.content)
|
||||
let ctx = req.text
|
||||
let tfile_path = `${folder_path}/${file_name}`
|
||||
// console.log(ctx)
|
||||
if (folder_path.startsWith('.')) {
|
||||
let flag = (plugin.app.vault as any).exists(folder_path);
|
||||
if (!flag) {
|
||||
await plugin.app.vault.createFolder(folder_path)
|
||||
}
|
||||
flag = (plugin.app.vault as any).exists(tfile_path);
|
||||
if (flag) {
|
||||
await (plugin.app.vault as any).adapter.remove(tfile_path)
|
||||
await plugin.app.vault.create(tfile_path, ctx)
|
||||
new Notice(`更新:${tfile_path}`, 5000)
|
||||
} else {
|
||||
await plugin.app.vault.create(tfile_path, ctx)
|
||||
new Notice(`下载:${tfile_path}`, 5000)
|
||||
}
|
||||
} else {
|
||||
let folder = plugin.app.vault.getFolderByPath(folder_path)
|
||||
if (!folder) {
|
||||
folder = await plugin.app.vault.createFolder(folder_path)
|
||||
}
|
||||
|
||||
let tfile = plugin.app.vault.getFileByPath(tfile_path)
|
||||
if (!tfile) {
|
||||
await plugin.app.vault.create(tfile_path, ctx);
|
||||
new Notice(`下载:${tfile_path}`, 5000)
|
||||
} else {
|
||||
await plugin.app.vault.modify(tfile, ctx)
|
||||
new Notice(`更新:${tfile.path}`, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
async function download_file_of_dir(repoOwner: string, repoName: string, path: string, branch: string) {
|
||||
let items = await list_files_of_path(repoOwner, repoName, path, branch)
|
||||
let nc = this.app.plugins.getPlugin('note-chain');
|
||||
let item = await nc.dialog_suggest(
|
||||
items.map((x: any) => (x.type == 'file' ? '📃' : '📁') + x.path),
|
||||
items, '', true
|
||||
);
|
||||
if (!item) { return }
|
||||
if (typeof (item) == 'string' && item == 'all') {
|
||||
for (let item of items) {
|
||||
if (item.type == 'file') {
|
||||
let file_name = item.path.split('/').last();
|
||||
let folder_path = item.path.slice(0, item.path.length - file_name.length - 1);
|
||||
await download_file(item.download_url, folder_path, file_name)
|
||||
}
|
||||
}
|
||||
} else if (item.type == 'file') {
|
||||
let file_name = item.path.split('/').last();
|
||||
let folder_path = item.path.slice(0, item.path.length - file_name.length - 1);
|
||||
await download_file(item.download_url, folder_path, file_name)
|
||||
} else if (item.type == 'dir') {
|
||||
await download_file_of_dir(repoOwner, repoName, item.path, branch)
|
||||
}
|
||||
}
|
||||
|
||||
download_file_of_dir(repoOwner, repoName, path, branch)
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
const cmd_export_wxmp = (plugin: NoteSyncPlugin) => ({
|
||||
id: 'cmd_export_wxmp',
|
||||
name: plugin.strings.cmd_export_wxmp,
|
||||
icon: 'aperture',
|
||||
hotkeys: [{ modifiers: ['Alt', 'Shift'], key: 'P' }],
|
||||
callback: async () => {
|
||||
if (!plugin.easyapi.cfile) { return }
|
||||
let ctx = plugin.easyapi.ceditor.getSelection();
|
||||
if (!ctx) {
|
||||
plugin.wxmp.tfile_to_wxmp(plugin.easyapi.cfile);
|
||||
} else {
|
||||
await plugin.wxmp.selection_to_wxmp();
|
||||
}
|
||||
new Notice(`${plugin.strings.cmd_export_wxmp}: OK`, 5000)
|
||||
}
|
||||
});
|
||||
|
||||
const cmd_export_as_single_note = (plugin: NoteSyncPlugin) => ({
|
||||
id: 'cmd_export_as_single_note',
|
||||
name: plugin.strings.cmd_export_as_single_note,
|
||||
icon: 'aperture',
|
||||
// hotkeys: [{ modifiers: ['Alt', 'Shift'], key: 'P' }],
|
||||
callback: async () => {
|
||||
if (!plugin.easyapi.cfile) { return }
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { dialog } = require("electron").remote;
|
||||
|
||||
let cfiles = plugin.easyapi.file.get_selected_files();
|
||||
if(cfiles.length<=2){
|
||||
// 1. 当前文件
|
||||
let cfile = plugin.easyapi.cfile;
|
||||
if (!cfile) {
|
||||
new Notice("未找到当前文件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 读取内容
|
||||
let n = 0;
|
||||
if(cfile.parent && cfile.parent.children.filter(x=> x instanceof TFolder).length>0){
|
||||
n = await plugin.easyapi.dialog_suggest([`-1 - All`,`0 - Brother`,`1 - Subfolder`],[-1,0,1]);
|
||||
if(n == null){n=0}
|
||||
}
|
||||
|
||||
cfiles = plugin.easyapi.file.get_tfiles_of_folder(cfile.parent,n)
|
||||
}
|
||||
|
||||
if(plugin.easyapi.nc){
|
||||
cfiles = plugin.easyapi.nc.chain.sort_tfiles_by_chain(cfiles);
|
||||
}
|
||||
|
||||
if(cfiles.length==0){
|
||||
new Notice("没有文件");
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 打开「另存为」对话框
|
||||
const result = await dialog.showSaveDialog({
|
||||
title: plugin.strings.cmd_export_as_single_note,
|
||||
defaultPath: (cfiles[0].parent?.name || cfiles[0].basename+'_parent') + ".md",
|
||||
filters: [
|
||||
{ name: "Markdown", extensions: ["md"] },
|
||||
{ name: "All Files", extensions: ["*"] }
|
||||
]
|
||||
});
|
||||
|
||||
if (result.canceled || !result.filePath) {
|
||||
new Notice("已取消另存为");
|
||||
return;
|
||||
}
|
||||
|
||||
let content = '';
|
||||
for(let cfile of cfiles){
|
||||
let tmp = await plugin.app.vault.read(cfile);
|
||||
content = content + `=====\n${cfile.name}\n=====\n\n${tmp}\n\n`
|
||||
}
|
||||
// 4. 写入外部文件
|
||||
fs.writeFileSync(result.filePath, content, "utf-8");
|
||||
|
||||
new Notice(`已保存到:${result.filePath}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
const commandBuilders: Array<Function> = [
|
||||
cmd_export_wxmp,
|
||||
|
||||
];
|
||||
|
||||
const commandBuildersDesktop:Array<Function> = [
|
||||
const commandBuildersDesktop: Array<Function> = [
|
||||
cmd_export_current_note,
|
||||
cmd_set_vexporter,
|
||||
cmd_export_plugin
|
||||
cmd_export_plugin,
|
||||
cmd_download_git_repo,
|
||||
cmd_export_wxmp,
|
||||
cmd_export_as_single_note
|
||||
];
|
||||
|
||||
export function addCommands(plugin:NoteSyncPlugin) {
|
||||
commandBuilders.forEach((c) => {
|
||||
plugin.addCommand(c(plugin));
|
||||
});
|
||||
if((plugin.app as any).isMobile==false){
|
||||
export function addCommands(plugin: NoteSyncPlugin) {
|
||||
commandBuilders.forEach((c) => {
|
||||
plugin.addCommand(c(plugin));
|
||||
});
|
||||
if ((plugin.app as any).isMobile == false) {
|
||||
commandBuildersDesktop.forEach((c) => {
|
||||
plugin.addCommand(c(plugin));
|
||||
});
|
||||
|
|
|
|||
40
src/easyapi/css.ts
Normal file
40
src/easyapi/css.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
|
||||
|
||||
|
||||
import { App, View, WorkspaceLeaf,TFile,TFolder } from 'obsidian';
|
||||
|
||||
import {EasyAPI} from 'src/easyapi/easyapi'
|
||||
|
||||
export class CSS {
|
||||
app: App;
|
||||
ea: EasyAPI;
|
||||
|
||||
constructor(app: App, api:EasyAPI) {
|
||||
this.app = app;
|
||||
this.ea = api;
|
||||
}
|
||||
|
||||
async toogle_note_css(document:any,name:string,refresh=false) {
|
||||
let tfile = this.ea.file.get_tfile(name);
|
||||
if(!tfile){return}
|
||||
|
||||
let link = document.getElementById(tfile.basename);
|
||||
if(link && refresh){
|
||||
link.remove()
|
||||
}else{
|
||||
let css = await this.ea.editor.extract_code_block(tfile,'css')
|
||||
let inner = css.join('\n')
|
||||
if(link){
|
||||
link.innerHTML = inner
|
||||
}else{
|
||||
if(inner!=''){
|
||||
let styleElement = document.createElement('style')
|
||||
styleElement.innerHTML=inner;
|
||||
styleElement.id = tfile.basename;
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
106
src/easyapi/easyapi.ts
Normal file
106
src/easyapi/easyapi.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
|
||||
|
||||
import { App, View, WorkspaceLeaf } from 'obsidian';
|
||||
|
||||
import {dialog_suggest} from './gui/inputSuggester'
|
||||
import {dialog_prompt} from './gui/inputPrompt'
|
||||
import {EasyEditor } from './editor';
|
||||
import {File } from './file';
|
||||
import {Random } from './random';
|
||||
import { Waiter } from './waiter';
|
||||
import { Templater } from './templater';
|
||||
import {Time} from './time'
|
||||
import { Web } from './web';
|
||||
|
||||
export class EasyAPI {
|
||||
app: App;
|
||||
dialog_suggest: Function
|
||||
dialog_prompt: Function
|
||||
editor: EasyEditor
|
||||
file: File
|
||||
random: Random
|
||||
waiter: Waiter
|
||||
tpl: Templater
|
||||
time: Time
|
||||
web: Web
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
this.dialog_suggest = dialog_suggest;
|
||||
this.dialog_prompt = dialog_prompt;
|
||||
this.editor = new EasyEditor(app,this);
|
||||
this.file = new File(app,this);
|
||||
this.waiter = new Waiter(app,this);
|
||||
this.random = new Random(app,this);
|
||||
this.tpl = new Templater(app,this);
|
||||
this.time = new Time(app,this);
|
||||
this.web = new Web(app);
|
||||
}
|
||||
|
||||
get_plugin(name:string){
|
||||
return (this.app as any).plugins?.plugins[name]
|
||||
}
|
||||
|
||||
get ea(){
|
||||
return this.get_plugin('easyapi');
|
||||
}
|
||||
|
||||
get nc(){
|
||||
return this.get_plugin('note-chain');
|
||||
}
|
||||
|
||||
get ns(){
|
||||
return this.get_plugin('note-sync');
|
||||
}
|
||||
|
||||
get wv(){
|
||||
return this.get_plugin('webview-llm');
|
||||
}
|
||||
|
||||
get qa(){
|
||||
return this.get_plugin('quickadd')?.api;
|
||||
}
|
||||
|
||||
get dv(){
|
||||
return this.get_plugin('dataview')?.api;
|
||||
}
|
||||
|
||||
get cfile(){
|
||||
return this.app.workspace.getActiveFile();
|
||||
}
|
||||
|
||||
get cmeta(){
|
||||
let cfile = this.cfile;
|
||||
if(cfile){
|
||||
return this.app.metadataCache.getFileCache(cfile)
|
||||
}
|
||||
}
|
||||
|
||||
get cfm(){
|
||||
let cmeta = this.cmeta;
|
||||
if(cmeta){
|
||||
return cmeta.frontmatter;
|
||||
}
|
||||
}
|
||||
|
||||
get ccontent(){
|
||||
let cfile = this.cfile;
|
||||
if(cfile){
|
||||
return this.app.vault.read(cfile);
|
||||
}
|
||||
}
|
||||
|
||||
get cfolder(){
|
||||
return this.cfile?.parent;
|
||||
}
|
||||
|
||||
get cview(){
|
||||
let view = (this.app.workspace as any).getActiveFileView()
|
||||
return view;
|
||||
}
|
||||
|
||||
get ceditor(){
|
||||
let editor = this.cview?.editor;
|
||||
return editor;
|
||||
}
|
||||
}
|
||||
473
src/easyapi/editor.ts
Normal file
473
src/easyapi/editor.ts
Normal file
|
|
@ -0,0 +1,473 @@
|
|||
|
||||
|
||||
|
||||
import { App, View, WorkspaceLeaf, TFile } from 'obsidian';
|
||||
|
||||
import { EasyAPI } from 'src/easyapi/easyapi'
|
||||
|
||||
export class EasyEditor {
|
||||
yamljs = require('js-yaml');
|
||||
app: App;
|
||||
ea: EasyAPI;
|
||||
|
||||
constructor(app: App, api: EasyAPI) {
|
||||
this.app = app;
|
||||
this.ea = api;
|
||||
}
|
||||
|
||||
cn2num(chinese: string) {
|
||||
let v = parseFloat(chinese);
|
||||
if (!Number.isNaN(v)) { return v }
|
||||
|
||||
chinese = chinese.trim()
|
||||
const cnNumbers: { [key: string]: number } = {
|
||||
"零": 0, "一": 1, "二": 2, "三": 3, "四": 4,
|
||||
"五": 5, "六": 6, "七": 7, "八": 8, "九": 9,
|
||||
"十": 10, "百": 100, "千": 1000, "万": 10000
|
||||
};
|
||||
|
||||
let sign = 1.0;
|
||||
let i = 0;
|
||||
|
||||
// 处理负号(JavaScript中汉字是双字节字符)
|
||||
if (i + 1 <= chinese.length && chinese[i] === "负") {
|
||||
sign = -1.0;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let integer_total = 0;
|
||||
let decimal_total = 0.0;
|
||||
let temp = 0;
|
||||
let processing_decimal = false;
|
||||
let decimal_factor = 0.1;
|
||||
|
||||
while (i < chinese.length) {
|
||||
const c = chinese[i];
|
||||
i += 1;
|
||||
|
||||
// 处理小数点
|
||||
if (c === "点") {
|
||||
processing_decimal = true;
|
||||
integer_total += temp;
|
||||
temp = 0;
|
||||
continue;
|
||||
}
|
||||
if (!(c in cnNumbers)) {
|
||||
return parseFloat('-')
|
||||
}
|
||||
if (!processing_decimal) {
|
||||
// 整数部分处理
|
||||
if (cnNumbers.hasOwnProperty(c)) {
|
||||
const num = cnNumbers[c];
|
||||
|
||||
if (num >= 10) { // 处理单位
|
||||
if (temp === 0 && num === 10) {
|
||||
integer_total += 1 * num; // 特殊处理"十"前无数字的情况
|
||||
} else {
|
||||
integer_total += temp * num;
|
||||
}
|
||||
temp = 0; // 重置temp
|
||||
} else { // 处理数字
|
||||
temp = temp * 10 + num;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 小数部分处理
|
||||
if (cnNumbers.hasOwnProperty(c) && cnNumbers[c] < 10) {
|
||||
decimal_total += cnNumbers[c] * decimal_factor;
|
||||
decimal_factor *= 0.1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理最后的临时值
|
||||
integer_total += temp;
|
||||
|
||||
return sign * (integer_total + decimal_total);
|
||||
}
|
||||
|
||||
slice_by_position(ctx: string, pos: any) {
|
||||
if (pos.position) {
|
||||
pos = pos.position
|
||||
}
|
||||
return ctx.slice(pos.start.offset, pos.end.offset);
|
||||
}
|
||||
|
||||
parse_list_regx(aline: string, regx: RegExp, field: { [key: string]: number } = {}) {
|
||||
let match = aline.match(regx);
|
||||
if (!match) { return null }
|
||||
let res: { [key: string]: string } = { src: aline }
|
||||
for (let k in field) {
|
||||
res[k] = match[field[k]]
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
parse_list_dataview(aline: string, src = '_src_') {
|
||||
let res: { [key: string]: string } = {};
|
||||
if (src) {
|
||||
res[src] = aline;
|
||||
}
|
||||
let regex = /[($$](.*?)::(.*?)[)$$]/g;
|
||||
let match;
|
||||
while ((match = regex.exec(aline)) !== null) {
|
||||
let key = match[1].trim(); // 提取 key 并去除两端空格
|
||||
let value = match[2].trim(); // 提取 value 并去除两端空格
|
||||
res[key] = value;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
keys_in(keys: Array<string>, obj: object) {
|
||||
for (let k of keys) {
|
||||
if (!(k in obj)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async extract_code_block(tfile: TFile | string, btype: string) {
|
||||
let xfile = this.ea.file.get_tfile(tfile);
|
||||
if (xfile) {
|
||||
tfile = await this.app.vault.cachedRead(xfile);
|
||||
}
|
||||
if (typeof (tfile) != 'string') { return [] }
|
||||
|
||||
let blocks = [];
|
||||
let reg = new RegExp(`\`\`\`${btype}\\n([\\s\\S]*?)\n\`\`\``, 'g');;
|
||||
let matches;
|
||||
while ((matches = reg.exec(tfile)) !== null) {
|
||||
blocks.push(matches[1].trim());
|
||||
}
|
||||
|
||||
reg = new RegExp(`~~~${btype}\\n([\\s\\S]*?)\n~~~`, 'g');;
|
||||
while ((matches = reg.exec(tfile)) !== null) {
|
||||
blocks.push(matches[1].trim());
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
async get_selection(cancel_selection = false) {
|
||||
let editor = (this.app.workspace as any).getActiveFileView()?.editor;
|
||||
if (editor) {
|
||||
let sel = editor.getSelection();
|
||||
if (cancel_selection) {
|
||||
let cursor = editor.getCursor();
|
||||
await editor.setSelection(cursor, cursor);
|
||||
}
|
||||
if (sel) {
|
||||
return sel;
|
||||
}
|
||||
}
|
||||
|
||||
let selection = window.getSelection();
|
||||
if (selection && !selection.isCollapsed) {
|
||||
const selectedText = selection.toString();
|
||||
if (selectedText) {
|
||||
return selectedText;
|
||||
}
|
||||
}
|
||||
let areas = document.querySelectorAll('textarea');
|
||||
for (let area of Array.from(areas)) {
|
||||
let sel = area.value.slice(area.selectionStart, area.selectionEnd);
|
||||
|
||||
if (sel) {
|
||||
area.selectionStart = area.selectionEnd;
|
||||
area.blur();
|
||||
return sel
|
||||
}
|
||||
}
|
||||
return ''
|
||||
|
||||
}
|
||||
|
||||
async get_code_section(tfile: TFile, ctype = '', idx = 0, as_simple = true) {
|
||||
let dvmeta = this.app.metadataCache.getFileCache(tfile);
|
||||
let ctx = await this.app.vault.cachedRead(tfile);
|
||||
|
||||
let sections = dvmeta?.sections
|
||||
?.filter(x => x.type == 'code')
|
||||
.filter(x => {
|
||||
let c = ctx.slice(x.position.start.offset, x.position.end.offset).trim();
|
||||
return c.startsWith('```' + ctype) || c.startsWith('~~~' + ctype);
|
||||
});
|
||||
|
||||
if (!sections || sections.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let selected: any;
|
||||
|
||||
if(sections.length==1){
|
||||
selected = sections[0];
|
||||
}else if (idx >= 2 && sections[idx]) {
|
||||
selected = sections[idx];
|
||||
} else {
|
||||
let sel = await this.ea.dialog_suggest(
|
||||
sections.map(x => ctx.slice(x.position.start.offset, x.position.end.offset)),
|
||||
[...Array(sections.length).keys()]
|
||||
);
|
||||
if (sel == null) return;
|
||||
selected = sections[sel];
|
||||
}
|
||||
|
||||
// Now safely use selected
|
||||
let c = ctx.slice(
|
||||
selected.position.start.offset,
|
||||
selected.position.end.offset
|
||||
);
|
||||
|
||||
if (as_simple) {
|
||||
return c.slice(4 + ctype.length, c.length - 4);
|
||||
} else {
|
||||
return {
|
||||
code: c,
|
||||
section: selected,
|
||||
ctx: ctx
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get_heading_ctx(ctx: string, headings: any[], heading: any, with_heading = true) {
|
||||
// 找到 heading 在 headings 中的位置
|
||||
if(!heading){return ''}
|
||||
let idx = headings.indexOf(heading);
|
||||
let nextIdx = idx + 1;
|
||||
|
||||
// 找下一个同级或更高的 heading(level <= target.level)
|
||||
while (nextIdx < headings.length) {
|
||||
if (headings[nextIdx].level <= heading.level) {
|
||||
break;
|
||||
}
|
||||
nextIdx++;
|
||||
}
|
||||
|
||||
// 起点
|
||||
let start = with_heading
|
||||
? heading.position.start.offset
|
||||
: heading.position.end.offset;
|
||||
|
||||
// 终点:如果找到下一个 heading
|
||||
if (nextIdx < headings.length) {
|
||||
let nextSec = headings[nextIdx];
|
||||
return ctx.slice(start, nextSec.position.start.offset);
|
||||
}
|
||||
|
||||
// 没有更下一级 heading,截到文末
|
||||
return ctx.slice(start);
|
||||
}
|
||||
|
||||
|
||||
async get_heading_section(tfile: TFile, heading: string, idx = 0, with_heading = true) {
|
||||
let dvmeta = this.app.metadataCache.getFileCache(tfile);
|
||||
let ctx = await this.app.vault.cachedRead(tfile);
|
||||
|
||||
if (!dvmeta?.headings) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 找到所有匹配开头的 headings
|
||||
let sections = dvmeta.headings.filter(x => x.heading==heading);
|
||||
|
||||
if (sections.length === 0) return '';
|
||||
|
||||
let selected: any;
|
||||
|
||||
// idx >= 0 时直接取
|
||||
if(sections.length==1){
|
||||
selected = sections[0];
|
||||
}else if (idx >= 2 && sections[idx]) {
|
||||
selected = sections[idx];
|
||||
} else {
|
||||
// 弹窗选择
|
||||
const choices = sections.map(x =>
|
||||
this.get_heading_ctx(ctx,dvmeta?.headings??[],x,with_heading)
|
||||
);
|
||||
|
||||
const nums = [...Array(sections.length).keys()];
|
||||
let sel = await this.ea.dialog_suggest(choices, nums);
|
||||
|
||||
if (sel == null) return;
|
||||
selected = sections[sel];
|
||||
}
|
||||
return this.get_heading_ctx(ctx,dvmeta.headings??[],selected,with_heading).trim();
|
||||
}
|
||||
|
||||
|
||||
async get_current_section(with_section = false) {
|
||||
let editor = this.ea.ceditor;
|
||||
let tfile = this.ea.cfile;
|
||||
if (!editor || !tfile) { return null }
|
||||
let cursor = editor.getCursor();
|
||||
let cache = this.app.metadataCache.getFileCache(tfile)
|
||||
if (!cache || !cache?.sections) { return null }
|
||||
if (cursor) {
|
||||
let section = cache?.sections?.filter(
|
||||
x => { return x.position.start.line <= cursor.line && x.position.end.line >= cursor.line }
|
||||
)[0]
|
||||
if (!section && cursor.line > cache.sections[cache.sections.length - 1].position.end.line) {
|
||||
section = cache.sections[cache.sections.length - 1]
|
||||
}
|
||||
if (!section && cursor.line < cache.sections[0].position.start.line) {
|
||||
section = cache.sections[0]
|
||||
}
|
||||
if (!section) {
|
||||
return null;
|
||||
}
|
||||
let ctx = await this.app.vault.cachedRead(tfile);
|
||||
ctx = ctx.slice(
|
||||
section.position.start.offset,
|
||||
section.position.end.offset
|
||||
)
|
||||
if (with_section) {
|
||||
return {
|
||||
'section': section,
|
||||
'sec': ctx
|
||||
}
|
||||
} else {
|
||||
return ctx;
|
||||
}
|
||||
return
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
set_obj_value(data: any, key: string, value: any) {
|
||||
let items = key.trim().split('.')
|
||||
if (!items) { return }
|
||||
let curr = data
|
||||
for (let item of items.slice(0, items.length - 1)) {
|
||||
let kv = item.match(/^(.*?)(\[-?\d+\])?$/) // 匹配数组索引, 如 key[0] 或 key
|
||||
if (!kv) { return }
|
||||
let k = kv[1] // 键名
|
||||
if (kv[2]) { // 有索引
|
||||
let i = parseInt(kv[2].slice(1, kv[2].length - 1)) // 索引
|
||||
if (!(k in curr)) { // 键不存在
|
||||
curr[k] = [{}] // 创建空数组
|
||||
curr = curr[k][0]
|
||||
} else {
|
||||
if (Array.isArray(curr[k])) {
|
||||
let tmp = {}
|
||||
if (i < 0) {
|
||||
curr[k].splice(-i - 1, 0, tmp)
|
||||
} else if (i < curr[k].length) {
|
||||
curr[k][i] = tmp
|
||||
} else {
|
||||
curr[k].push(tmp)
|
||||
}
|
||||
curr = tmp
|
||||
} else {
|
||||
curr[k] = [{}]
|
||||
curr = curr[k][0]
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!(k in curr)) {
|
||||
curr[k] = {}
|
||||
curr = curr[k]
|
||||
} else {
|
||||
if (typeof (curr[k]) != 'object') {
|
||||
curr[k] = {}
|
||||
curr = curr[k]
|
||||
} else {
|
||||
curr = curr[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let kv = items[items.length - 1].match(/^(.*?)(\[-?\d+\])?$/)
|
||||
if (!kv) { return }
|
||||
let k = kv[1]
|
||||
if (kv[2]) {
|
||||
let i = parseInt(kv[2].slice(1, kv[2].length - 1))
|
||||
if (k in curr) {
|
||||
if (Array.isArray(curr[k])) {
|
||||
if (i < 0) {
|
||||
curr[k].splice(-i - 1, 0, value)
|
||||
} else if (i < curr[k].length) {
|
||||
curr[k][i] = value
|
||||
} else {
|
||||
curr[k].push(value)
|
||||
}
|
||||
} else {
|
||||
curr[k] = value
|
||||
}
|
||||
} else {
|
||||
curr[k] = [value]
|
||||
}
|
||||
} else {
|
||||
curr[k] = value
|
||||
}
|
||||
}
|
||||
|
||||
get_obj_value(data: any, key: string): any {
|
||||
try {
|
||||
// key 直接在对象中
|
||||
if (data[key]) {
|
||||
return data[key]
|
||||
}
|
||||
|
||||
let keys = key.split('.')
|
||||
let left = keys[0];
|
||||
let right = keys.slice(1).join('.');
|
||||
|
||||
if (left) {
|
||||
// key[3],key[-3]
|
||||
let items = left.match(/^(.*?)(\[-?\d+\])?$/)
|
||||
if (!items) { return null }
|
||||
if (items[1]) {
|
||||
data = data[items[1]]
|
||||
}
|
||||
if (!data) { return null }
|
||||
if (items[2]) {
|
||||
if (Array.isArray(data)) {
|
||||
if (data.length == 0) {
|
||||
data = null;
|
||||
} else {
|
||||
let i = parseInt(items[2].slice(1, items[2].length - 1))
|
||||
i = ((i % data.length) + data.length) % data.length;
|
||||
data = data[i]
|
||||
}
|
||||
} else if (typeof data == 'object') {
|
||||
let keys = Object.keys(data).sort();
|
||||
if (keys.length == 0) {
|
||||
data = null;
|
||||
} else {
|
||||
let i = parseInt(items[2].slice(1, items[2].length - 1))
|
||||
i = ((i % keys.length) + keys.length) % keys.length;
|
||||
data = data[keys[i]]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!right) {
|
||||
return data;
|
||||
} else {
|
||||
return this.get_obj_value(data, right);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// LINE 存在时在其之后插件,不存在在末尾
|
||||
async insert_after_line(tfile: TFile, aline: string, LINE: string, tail = true, suffix = '\n\n') {
|
||||
if (!tfile) { return false }
|
||||
let ctx = await this.ea.app.vault.cachedRead(tfile)
|
||||
|
||||
let idx = ctx.indexOf(LINE)
|
||||
|
||||
if (idx == -1 && tail) {
|
||||
ctx = `${ctx}${suffix}${aline}`
|
||||
} else {
|
||||
ctx = `${ctx.slice(0, idx + LINE.length)}\n${aline}${ctx.slice(idx + LINE.length)}`
|
||||
}
|
||||
await this.ea.app.vault.modify(tfile, ctx)
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
188
src/easyapi/file.ts
Normal file
188
src/easyapi/file.ts
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
|
||||
|
||||
|
||||
import { App, View, WorkspaceLeaf,TFile,TFolder } from 'obsidian';
|
||||
|
||||
import {EasyAPI} from 'src/easyapi/easyapi'
|
||||
|
||||
export class File {
|
||||
app: App;
|
||||
api: EasyAPI;
|
||||
|
||||
constructor(app: App, api:EasyAPI) {
|
||||
this.app = app;
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
get_tfile(path:string|TFile|null,only_first=true){
|
||||
try{
|
||||
if(!path){
|
||||
return null;
|
||||
}
|
||||
if(path instanceof TFile){
|
||||
return path;
|
||||
}
|
||||
path = path.split('|')[0].replace('![[','').replace('[[','').replace(']]','');
|
||||
let tfile = this.app.vault.getFileByPath(path)
|
||||
if(tfile){
|
||||
return tfile;
|
||||
}
|
||||
|
||||
let tfiles = (this.app.metadataCache as any).uniqueFileLookup.get(path.toLowerCase());
|
||||
if(!tfiles){
|
||||
tfiles = (this.app.metadataCache as any).uniqueFileLookup.get(path.toLowerCase()+'.md');
|
||||
if(!tfiles){
|
||||
return null;
|
||||
}else{
|
||||
path = path+'.md'
|
||||
}
|
||||
}
|
||||
|
||||
let ctfiles = tfiles.filter((x:TFile)=>x.name==path)
|
||||
if(ctfiles.length>0){
|
||||
if(only_first){
|
||||
return ctfiles[0]
|
||||
}else{
|
||||
return ctfiles
|
||||
}
|
||||
}
|
||||
|
||||
if(tfiles.length>0){
|
||||
if(only_first){
|
||||
return tfiles[0]
|
||||
}else{
|
||||
return tfiles
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}catch{
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
get_all_tfiles(){
|
||||
let files = this.app.vault.getMarkdownFiles();
|
||||
return files;
|
||||
}
|
||||
|
||||
get_tfiles_of_folder(tfolder:TFolder|null,n=0):any{
|
||||
if(!tfolder){return [];}
|
||||
let notes = [];
|
||||
for(let c of tfolder.children){
|
||||
if(c instanceof TFile && c.extension==='md'){
|
||||
notes.push(c);
|
||||
}else if(c instanceof TFolder && n!=0){
|
||||
let tmp = this.get_tfiles_of_folder(c,n-1);
|
||||
for(let x of tmp){
|
||||
notes.push(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
return notes;
|
||||
}
|
||||
|
||||
get_all_tfiles_of_tags(tags:string|Array<string>,sort_mode=''){
|
||||
if(!Array.isArray(tags)){
|
||||
tags = [tags]
|
||||
}
|
||||
|
||||
tags = tags.map(x=>{
|
||||
if(x.startsWith('#')){
|
||||
return x;
|
||||
}else{
|
||||
return '#'+x;
|
||||
}
|
||||
})
|
||||
|
||||
let tfiles = this.get_all_tfiles().filter(x=>{
|
||||
let ttags = this.get_tags(x);
|
||||
for(let tag of tags){
|
||||
if(ttags.contains(tag)){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
})
|
||||
return tfiles;
|
||||
}
|
||||
|
||||
generate_structure(tfolder:TFolder, depth = 0, isRoot = true,only_folder=false,only_md=true) {
|
||||
let structure = '';
|
||||
const indentUnit = ' '; // 关键修改点:每层缩进 4 空格
|
||||
const verticalLine = '│ '; // 垂直连接线密度增强
|
||||
const indent = verticalLine.repeat(Math.max(depth - 1, 0)) + indentUnit.repeat(depth > 0 ? 1 : 0);
|
||||
const children = tfolder.children || [];
|
||||
|
||||
// 显示根目录名称
|
||||
if (isRoot) {
|
||||
structure += `${tfolder.name}/\n`;
|
||||
isRoot = false;
|
||||
}
|
||||
|
||||
children.forEach((child, index) => {
|
||||
const isLast = index === children.length - 1;
|
||||
const prefix = isLast ? '└── ' : '├── '; // 统一符号风格
|
||||
|
||||
if (child instanceof TFolder) {
|
||||
// 目录节点:增加垂直连接线密度
|
||||
structure += `${indent}${prefix}${child.name}/\n`;
|
||||
structure += this.generate_structure(child, depth + 1, isRoot,only_folder,only_md);
|
||||
} else if(!only_folder) {
|
||||
// 文件节点:对齐符号与目录
|
||||
if(only_md && (child as TFile).extension!='md'){return}
|
||||
structure += `${indent}${prefix}${child.name}\n`;
|
||||
}
|
||||
});
|
||||
return structure;
|
||||
}
|
||||
|
||||
get_tags(tfile:TFile){
|
||||
if(!tfile){return []}
|
||||
let mcache= this.app.metadataCache.getFileCache(tfile);
|
||||
let tags:Array<string> = []
|
||||
if(mcache?.tags){
|
||||
for(let curr of mcache.tags){
|
||||
if(!tags.contains(curr.tag)){
|
||||
tags.push(curr.tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
if(mcache?.frontmatter?.tags){
|
||||
if(Array.isArray(mcache.frontmatter.tags)){
|
||||
for(let curr of mcache.frontmatter.tags){
|
||||
let tag = '#'+curr;
|
||||
if(!tags.contains(tag)){
|
||||
tags.push(tag)
|
||||
}
|
||||
}
|
||||
}else if(typeof mcache.frontmatter.tags === 'string'){
|
||||
let tag = `#`+mcache.frontmatter.tags
|
||||
if(!tags.contains(tag)){
|
||||
tags.push(tag)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return tags
|
||||
}
|
||||
|
||||
get_selected_files(current_if_no_selected = true) {
|
||||
let selector = document.querySelectorAll(
|
||||
".tree-item-self.is-selected"
|
||||
);
|
||||
let items = Object.values(selector).map((x: any) => {
|
||||
var _a;
|
||||
return (_a = x.dataset) == null ? void 0 : _a.path;
|
||||
});
|
||||
let tfiles = items.map(
|
||||
(x) => this.get_tfile(x)).filter((x) => x.extension == "md"
|
||||
)
|
||||
if (tfiles.length > 0) {
|
||||
return tfiles
|
||||
} else if (current_if_no_selected && this.app.workspace.getActiveFile()) {
|
||||
return [this.app.workspace.getActiveFile()]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
170
src/easyapi/gui/inputPrompt.ts
Normal file
170
src/easyapi/gui/inputPrompt.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import type { App} from "obsidian";
|
||||
import { ButtonComponent, Modal, TextComponent } from "obsidian";
|
||||
|
||||
/**
|
||||
* Copy from QuickAdd
|
||||
*/
|
||||
|
||||
export default class InputPrompt extends Modal {
|
||||
public waitForClose: Promise<string>;
|
||||
|
||||
private resolvePromise: (input: string) => void;
|
||||
private rejectPromise: (reason?: unknown) => void;
|
||||
private didSubmit = false;
|
||||
private inputComponent: TextComponent;
|
||||
private input: string;
|
||||
private readonly placeholder: string;
|
||||
|
||||
public static Prompt(
|
||||
app: App,
|
||||
header: string,
|
||||
placeholder?: string,
|
||||
value?: string
|
||||
): Promise<string> {
|
||||
const newPromptModal = new InputPrompt(
|
||||
app,
|
||||
header,
|
||||
placeholder,
|
||||
value
|
||||
);
|
||||
return newPromptModal.waitForClose;
|
||||
}
|
||||
|
||||
protected constructor(
|
||||
app: App,
|
||||
private header: string,
|
||||
placeholder?: string,
|
||||
value?: string
|
||||
) {
|
||||
super(app);
|
||||
this.placeholder = placeholder ?? "";
|
||||
this.input = value ?? "";
|
||||
|
||||
this.waitForClose = new Promise<string>((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.rejectPromise = reject;
|
||||
});
|
||||
|
||||
this.display();
|
||||
this.open();
|
||||
|
||||
}
|
||||
|
||||
private display() {
|
||||
this.containerEl.addClass("quickAddModal", "qaInputPrompt");
|
||||
this.contentEl.empty();
|
||||
this.titleEl.textContent = this.header;
|
||||
|
||||
const mainContentContainer: HTMLDivElement = this.contentEl.createDiv();
|
||||
this.inputComponent = this.createInputField(
|
||||
mainContentContainer,
|
||||
this.placeholder,
|
||||
this.input
|
||||
);
|
||||
this.createButtonBar(mainContentContainer);
|
||||
}
|
||||
|
||||
protected createInputField(
|
||||
container: HTMLElement,
|
||||
placeholder?: string,
|
||||
value?: string
|
||||
) {
|
||||
const textComponent = new TextComponent(container);
|
||||
(textComponent as any).inputEl.classList.add("input-field");
|
||||
(textComponent as any)
|
||||
.setPlaceholder(placeholder ?? "")
|
||||
.setValue(value ?? "")
|
||||
.onChange((value:string) => (this.input = value))
|
||||
.inputEl.addEventListener("keydown", this.submitEnterCallback);
|
||||
|
||||
return textComponent;
|
||||
}
|
||||
|
||||
private createButton(
|
||||
container: HTMLElement,
|
||||
text: string,
|
||||
callback: (evt: MouseEvent) => unknown
|
||||
) {
|
||||
const btn = new ButtonComponent(container);
|
||||
btn.setButtonText(text).onClick(callback);
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
private createButtonBar(mainContentContainer: HTMLDivElement) {
|
||||
const buttonBarContainer: HTMLDivElement =
|
||||
mainContentContainer.createDiv();
|
||||
this.createButton(
|
||||
buttonBarContainer,
|
||||
"Ok",
|
||||
this.submitClickCallback
|
||||
).setCta();
|
||||
this.createButton(
|
||||
buttonBarContainer,
|
||||
"Cancel",
|
||||
this.cancelClickCallback
|
||||
);
|
||||
|
||||
buttonBarContainer.classList.add("button-bar");
|
||||
|
||||
}
|
||||
|
||||
private submitClickCallback = (evt: MouseEvent) => this.submit();
|
||||
private cancelClickCallback = (evt: MouseEvent) => this.cancel();
|
||||
|
||||
private submitEnterCallback = (evt: KeyboardEvent) => {
|
||||
if (!evt.isComposing && evt.key === "Enter") {
|
||||
evt.preventDefault();
|
||||
this.submit();
|
||||
}
|
||||
};
|
||||
|
||||
private submit() {
|
||||
this.didSubmit = true;
|
||||
|
||||
this.close();
|
||||
}
|
||||
|
||||
private cancel() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
private resolveInput() {
|
||||
if (!this.didSubmit) this.rejectPromise("No input given.");
|
||||
else this.resolvePromise(this.input);
|
||||
}
|
||||
|
||||
private removeInputListener() {
|
||||
this.inputComponent.inputEl.removeEventListener(
|
||||
"keydown",
|
||||
this.submitEnterCallback
|
||||
);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
super.onOpen();
|
||||
|
||||
this.inputComponent.inputEl.focus();
|
||||
this.inputComponent.inputEl.select();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
super.onClose();
|
||||
this.resolveInput();
|
||||
this.removeInputListener();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function dialog_prompt(header: string='Input', placeholder: string='',value:string='') {
|
||||
try{
|
||||
return await InputPrompt.Prompt(
|
||||
this.app,
|
||||
header,
|
||||
placeholder,
|
||||
value
|
||||
)
|
||||
}catch{
|
||||
return null
|
||||
}
|
||||
}
|
||||
139
src/easyapi/gui/inputSuggester.ts
Normal file
139
src/easyapi/gui/inputSuggester.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { FuzzySuggestModal } from "obsidian";
|
||||
import type { FuzzyMatch , App} from "obsidian";
|
||||
|
||||
// 添加类型声明
|
||||
interface SuggesterChooser {
|
||||
values: {
|
||||
item: string;
|
||||
match: { score: number; matches: unknown[] };
|
||||
}[];
|
||||
selectedItem: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 扩展FuzzySuggestModal的类型
|
||||
interface ExtendedFuzzySuggestModal extends FuzzySuggestModal<string> {
|
||||
chooser: SuggesterChooser;
|
||||
}
|
||||
|
||||
type Options = {
|
||||
limit: FuzzySuggestModal<string>["limit"];
|
||||
emptyStateText: FuzzySuggestModal<string>["emptyStateText"];
|
||||
placeholder: Parameters<
|
||||
FuzzySuggestModal<string>["setPlaceholder"]
|
||||
>[0] extends string
|
||||
? string
|
||||
: never;
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy from QuickAdd
|
||||
*/
|
||||
export default class InputSuggester extends FuzzySuggestModal<string> {
|
||||
private resolvePromise: (value: string) => void;
|
||||
private rejectPromise: (reason?: unknown) => void;
|
||||
public promise: Promise<string>;
|
||||
private resolved: boolean;
|
||||
public new_value: boolean;
|
||||
inputEl: any;
|
||||
|
||||
public static Suggest(
|
||||
app: App,
|
||||
displayItems: string[],
|
||||
items: string[],
|
||||
options: Partial<Options> = {},
|
||||
new_value:boolean=false
|
||||
) {
|
||||
const newSuggester = new InputSuggester(
|
||||
app,
|
||||
displayItems,
|
||||
items,
|
||||
options,
|
||||
new_value
|
||||
);
|
||||
return newSuggester.promise;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
app: App,
|
||||
private displayItems: string[],
|
||||
private items: string[],
|
||||
options: Partial<Options> = {},
|
||||
new_value: boolean = false
|
||||
) {
|
||||
super(app);
|
||||
this.new_value = new_value
|
||||
|
||||
this.promise = new Promise<string>((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.rejectPromise = reject;
|
||||
});
|
||||
|
||||
this.inputEl.addEventListener("keydown", (event: KeyboardEvent) => {
|
||||
if (event.code !== "Tab") {
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用类型断言来访问chooser
|
||||
const self = this as unknown as ExtendedFuzzySuggestModal;
|
||||
const { values, selectedItem } = self.chooser;
|
||||
|
||||
const { value } = this.inputEl;
|
||||
this.inputEl.value = values[selectedItem].item ?? value;
|
||||
});
|
||||
|
||||
if (options.placeholder) this.setPlaceholder(options.placeholder);
|
||||
if (options.limit) this.limit = options.limit;
|
||||
if (options.emptyStateText)
|
||||
this.emptyStateText = options.emptyStateText;
|
||||
|
||||
this.open();
|
||||
}
|
||||
|
||||
getItemText(item: string): string {
|
||||
if (item === this.inputEl.value) return item;
|
||||
|
||||
return this.displayItems[this.items.indexOf(item)];
|
||||
}
|
||||
|
||||
getItems(): string[] {
|
||||
if (this.inputEl.value === ""||!this.new_value) return this.items;
|
||||
return [...this.items,this.inputEl.value];
|
||||
}
|
||||
|
||||
selectSuggestion(
|
||||
value: FuzzyMatch<string>,
|
||||
evt: MouseEvent | KeyboardEvent
|
||||
) {
|
||||
this.resolved = true;
|
||||
super.selectSuggestion(value, evt);
|
||||
}
|
||||
|
||||
onChooseItem(item: string, evt: MouseEvent | KeyboardEvent): void {
|
||||
this.resolved = true;
|
||||
this.resolvePromise(item);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
super.onClose();
|
||||
if (!this.resolved) this.rejectPromise("no input given.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function dialog_suggest(displayItems:Array<string>,items:Array<any>,placeholder='',new_value=false) {
|
||||
try{
|
||||
return await InputSuggester.Suggest(
|
||||
this.app,
|
||||
displayItems,
|
||||
items,
|
||||
{
|
||||
placeholder: placeholder,
|
||||
},
|
||||
new_value
|
||||
)
|
||||
}catch(error){
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
143
src/easyapi/random.ts
Normal file
143
src/easyapi/random.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { App, TFile,moment } from "obsidian";
|
||||
import { EasyAPI } from "./easyapi";
|
||||
|
||||
|
||||
export class Random {
|
||||
app:App;
|
||||
ea:EasyAPI;
|
||||
constructor(app:App,ea:EasyAPI){
|
||||
this.app = app;
|
||||
this.ea = ea;
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机获取 M 个值,位于 0~N 之间
|
||||
* @param {number} N - 最大值(不包含)
|
||||
* @param {number} M - 需要获取的随机数数量
|
||||
* @param {boolean} repeat - 是否允许重复值
|
||||
* @returns {number[]} - 包含 M 个随机数的数组
|
||||
*/
|
||||
random_number(N:number, M:number, repeat = false) {
|
||||
if (M <= 0) return [];
|
||||
if (!repeat && M > N) {
|
||||
throw new Error("当不允许重复时,M 不能大于 N");
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
if (repeat) {
|
||||
// 允许重复值的情况
|
||||
for (let i = 0; i < M; i++) {
|
||||
result.push(Math.floor(Math.random() * N));
|
||||
}
|
||||
} else {
|
||||
// 不允许重复值的情况
|
||||
const numbers = Array.from({ length: N }, (_, i) => i);
|
||||
|
||||
// 使用 Fisher-Yates 洗牌算法
|
||||
for (let i = numbers.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[numbers[i], numbers[j]] = [numbers[j], numbers[i]];
|
||||
}
|
||||
|
||||
// 取前 M 个
|
||||
result.push(...numbers.slice(0, M));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 线性同余生成器 (LCG)
|
||||
lcg(seed:number) {
|
||||
const a = 1664525;
|
||||
const c = 1013904223;
|
||||
const m = Math.pow(2, 32);
|
||||
return (a * seed + c) % m;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于日期生成固定随机数序列
|
||||
* @param {moment} t - 时间对象(使用moment.js)
|
||||
* @param {number} N - 随机数范围上限(0到N-1)
|
||||
* @param {number} M - 需要的随机数数量
|
||||
* @returns {number[]} - 排序后的随机数数组
|
||||
*/
|
||||
random_number_for_date(t:moment.Moment, N:number, M:number) {
|
||||
if (M <= 0) return [];
|
||||
if (M >= N) return Array.from({length: N}, (_, i) => i);
|
||||
|
||||
// 使用年月日作为种子,确保同一天生成相同的序列
|
||||
const dateStr = t.format('YYYY-MM-DD');
|
||||
let seed = 0;
|
||||
for (let i = 0; i < dateStr.length; i++) {
|
||||
seed = (seed << 5) - seed + dateStr.charCodeAt(i);
|
||||
seed |= 0; // 转换为32位整数
|
||||
}
|
||||
|
||||
// 使用Fisher-Yates算法生成随机排列
|
||||
const numbers = Array.from({length: N}, (_, i) => i);
|
||||
let currentSeed = seed;
|
||||
|
||||
for (let i = N - 1; i > 0; i--) {
|
||||
currentSeed = this.lcg(currentSeed);
|
||||
const j = Math.abs(currentSeed) % (i + 1);
|
||||
[numbers[i], numbers[j]] = [numbers[j], numbers[i]];
|
||||
}
|
||||
|
||||
// 取前M个并排序
|
||||
return numbers.slice(0, M).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
// 根据字符串返回 0~N 之间的整数
|
||||
string_to_random_number(str:string, N:number) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = (hash << 5) - hash + str.charCodeAt(i);
|
||||
hash |= 0; // 转换为32位整数
|
||||
}
|
||||
return Math.abs(hash) % N;
|
||||
}
|
||||
|
||||
// 从数组中随机获取 N 个元素
|
||||
random_elements(arr:any[], n:number ) {
|
||||
// 复制数组避免修改原数组
|
||||
const shuffled = [...arr];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; // 交换
|
||||
}
|
||||
return shuffled.slice(0, n); // 取前N个
|
||||
}
|
||||
|
||||
_get_tfiles_(filter:null|Function){
|
||||
let tfiles = this.ea.nc.chain.get_all_tfiles();
|
||||
if(filter){
|
||||
tfiles = tfiles.filter((x:TFile)=>filter(x))
|
||||
}
|
||||
return tfiles;
|
||||
}
|
||||
random_notes(n=3,filter=null){
|
||||
let tfiles = this._get_tfiles_(filter);
|
||||
let idx = this.random_number(tfiles.length,n)
|
||||
tfiles = idx.map(i=>tfiles[i])
|
||||
return tfiles
|
||||
}
|
||||
|
||||
random_daily_notes(n=3,before_today=true,filter=null){
|
||||
let t = moment(moment().format('YYYY-MM-DD') )
|
||||
let dnote = this.ea.nc.chain.get_last_daily_note()
|
||||
if(dnote){
|
||||
t = moment(dnote.basename)
|
||||
}
|
||||
let tfiles = this._get_tfiles_(filter);
|
||||
|
||||
if(before_today){
|
||||
tfiles = tfiles.filter(
|
||||
(f:TFile)=>f.stat.ctime<t.unix()*1000
|
||||
)
|
||||
}
|
||||
let idx = this.random_number_for_date(t,tfiles.length,n)
|
||||
tfiles = idx.map(i=>tfiles[i])
|
||||
return tfiles
|
||||
}
|
||||
}
|
||||
155
src/easyapi/templater.ts
Normal file
155
src/easyapi/templater.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { App, TFile,moment } from "obsidian";
|
||||
import { EasyAPI } from "./easyapi";
|
||||
|
||||
|
||||
export class Templater {
|
||||
app:App;
|
||||
ea:EasyAPI;
|
||||
constructor(app:App,ea:EasyAPI){
|
||||
this.app = app;
|
||||
this.ea = ea;
|
||||
}
|
||||
|
||||
get tpl(){
|
||||
return this.ea.get_plugin('templater-obsidian');
|
||||
}
|
||||
|
||||
get_tp_func(target:string) {
|
||||
|
||||
let items = target.split(".");
|
||||
if(items[0].localeCompare("tp")!=0 || items.length!=3){return undefined;}
|
||||
|
||||
let modules = this.tpl.templater.functions_generator.
|
||||
internal_functions.modules_array.filter(
|
||||
(item:any)=>(item.name.localeCompare(items[1])==0)
|
||||
);
|
||||
if(modules.length==0){return undefined}
|
||||
return modules[0].static_functions.get(items[2]);
|
||||
}
|
||||
|
||||
async get_tp_user_func(target:string) {
|
||||
if(!target.match(/^tp\.user\.\w+$/)){
|
||||
return null
|
||||
}
|
||||
|
||||
let items = target.split(".");
|
||||
if(items[0].localeCompare("tp")!=0 || items[1].localeCompare("user")!=0 || items.length!=3){return undefined;}
|
||||
|
||||
let funcs = await this.tpl.templater.
|
||||
functions_generator.
|
||||
user_functions.
|
||||
user_script_functions.
|
||||
generate_user_script_functions();
|
||||
return funcs.get(items[2])
|
||||
}
|
||||
|
||||
async templater$1(template:string|TFile|null, active_file:TFile|null, target_file:any,extra=null) {
|
||||
let config = {
|
||||
template_file: template,
|
||||
active_file: active_file,
|
||||
target_file: target_file,
|
||||
extra: extra,
|
||||
run_mode: "DynamicProcessor",
|
||||
};
|
||||
|
||||
let {templater} = this.tpl;
|
||||
let functions = await templater.functions_generator.internal_functions.generate_object(config);
|
||||
functions.user = {};
|
||||
let userScriptFunctions = await templater.functions_generator.user_functions.user_script_functions.generate_user_script_functions(config);
|
||||
userScriptFunctions.forEach((value:any,key:any)=>{
|
||||
functions.user[key] = value;
|
||||
}
|
||||
);
|
||||
if (template) {
|
||||
let userSystemFunctions = await templater.functions_generator.user_functions.user_system_functions.generate_system_functions(config);
|
||||
userSystemFunctions.forEach((value:any,key:any)=>{
|
||||
functions.user[key] = value;
|
||||
}
|
||||
);
|
||||
}
|
||||
return async(command:any)=>{
|
||||
return await templater.parser.parse_commands(command, functions);
|
||||
};
|
||||
}
|
||||
|
||||
async extract_templater_block(tfile:TFile|string,reg=/<%\*\s*([\s\S]*?)\s*-?%>/g){
|
||||
let xfile = this.ea.file.get_tfile(tfile);
|
||||
if(xfile){
|
||||
tfile = await this.app.vault.cachedRead(xfile);
|
||||
}
|
||||
if(typeof(tfile)!='string'){return []}
|
||||
|
||||
let blocks = [];
|
||||
let matches;
|
||||
while ((matches = reg.exec(tfile)) !== null) {
|
||||
blocks.push(matches[0].trim());
|
||||
}
|
||||
|
||||
let tpls = await this.ea.editor.extract_code_block(tfile,'js //templater');
|
||||
for(let tpl of tpls){
|
||||
blocks.push(`<%*\n${tpl}\n-%>`)
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// target_file:target>activate>template
|
||||
async parse_templater(template:string|TFile,extract=true,extra:any=null,idx:number[]|null=null,target='') {
|
||||
let file = this.ea.file.get_tfile(template)
|
||||
if(file){
|
||||
template = file
|
||||
}
|
||||
let blocks:Array<string>;
|
||||
let template_file = null;
|
||||
if(template instanceof TFile){
|
||||
template_file = template
|
||||
if(extract){
|
||||
blocks = await this.extract_templater_block(template);
|
||||
}else{
|
||||
let item = await this.app.vault.cachedRead(template)
|
||||
blocks = [item]
|
||||
}
|
||||
}else{
|
||||
if(extract){
|
||||
blocks = await this.extract_templater_block(template);
|
||||
}else{
|
||||
blocks = [template]
|
||||
}
|
||||
}
|
||||
|
||||
let active_file = this.ea.cfile;
|
||||
let target_file:any = this.ea.file.get_tfile(target);
|
||||
if(!target){
|
||||
if(active_file){
|
||||
target_file = active_file;
|
||||
}else if (file){
|
||||
target_file = file;
|
||||
}else{
|
||||
throw new Error("Target File must be TFile");
|
||||
}
|
||||
}
|
||||
|
||||
let templateFunc = await this.templater$1(template_file,active_file,target_file,extra=extra);
|
||||
if(templateFunc){
|
||||
let res = []
|
||||
if(idx){
|
||||
for(let i of idx){
|
||||
let block = blocks[i];
|
||||
if(block){
|
||||
let item = await templateFunc(block);
|
||||
res.push(item);
|
||||
}else{
|
||||
res.push('');
|
||||
}
|
||||
}
|
||||
}else{
|
||||
for(let block of blocks){
|
||||
let item = await templateFunc(block);
|
||||
res.push(item)
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}else{
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
353
src/easyapi/time.ts
Normal file
353
src/easyapi/time.ts
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
import { moment,App } from 'obsidian';
|
||||
import { Moment } from 'moment';
|
||||
import { EasyAPI } from "./easyapi";
|
||||
|
||||
export class Time{
|
||||
app:App;
|
||||
ea:EasyAPI;
|
||||
constructor(app:App,ea:EasyAPI){
|
||||
this.app = app;
|
||||
this.ea = ea;
|
||||
}
|
||||
|
||||
get today(){
|
||||
let t = moment().format('YYYY-MM-DD');
|
||||
return moment(t)
|
||||
}
|
||||
|
||||
as_date(t:Moment){
|
||||
let xt = t.format('YYYY-MM-DD');
|
||||
return moment(xt)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相对于基准日期的偏移月份的指定日期
|
||||
* @param {number} dayIndex - 日期索引(正数表示第几天,负数表示倒数第几天)
|
||||
* @param {number} monthOffset - 月份偏移量(正数为未来月份,负数为过去月份)
|
||||
* @param {Date|string|moment.Moment} baseDate - 基准日期,默认为当日
|
||||
* @returns {moment.Moment} 计算后的目标日期
|
||||
*/
|
||||
relative_month_day(dayIndex:number, monthOffset:number=0, baseDate=this.today) {
|
||||
// 创建基准日期的moment对象
|
||||
let baseMoment = moment(baseDate).clone();
|
||||
|
||||
// 计算目标月份
|
||||
let targetMoment = baseMoment.clone().add(monthOffset, 'months');
|
||||
|
||||
// 处理日期索引
|
||||
if (dayIndex > 0) {
|
||||
// 正数索引:设置为目标月份的第N天
|
||||
targetMoment.startOf('month').add(dayIndex - 1, 'days');
|
||||
} else {
|
||||
// 负数索引:设置为目标月份的倒数第N天
|
||||
targetMoment.endOf('month').add(dayIndex + 1, 'days');
|
||||
}
|
||||
return this.as_date(targetMoment);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相对于基准日期的偏移周数的指定星期几
|
||||
* @param {number} dayIndex - 星期索引(0-6,0为周日,1为周一,依此类推;或使用负数表示倒数)
|
||||
* @param {number} weekOffset - 周数偏移量(正数为未来周数,负数为过去周数)
|
||||
* @param {Date|string|moment.Moment} baseDate - 基准日期,默认为当日
|
||||
* @returns {moment.Moment} 计算后的目标日期
|
||||
*
|
||||
* @example
|
||||
* relative_week_day(1, 0) // 本周一
|
||||
* relative_week_day(0, -1) // 上周日
|
||||
* relative_week_day(6, 2) // 两周后的周六
|
||||
* relative_week_day(-1, 1) // 下周的倒数第1天(周六)
|
||||
*/
|
||||
relative_week_day(dayIndex:number, weekOffset:number = 0, baseDate = this.today) {
|
||||
// 创建基准日期的moment对象并克隆(避免污染原对象)
|
||||
let baseMoment = moment(baseDate).clone();
|
||||
|
||||
// 处理周偏移:先移动到目标周的开始(周一)
|
||||
let targetMoment = baseMoment.add(weekOffset, 'weeks');
|
||||
|
||||
// 处理星期索引
|
||||
if (dayIndex >= 0) {
|
||||
// 正数索引:直接设置为目标星期(0=周日到6=周六)
|
||||
targetMoment.day(dayIndex);
|
||||
} else {
|
||||
// 负数索引:计算目标周的倒数第N天
|
||||
// 1. 先移动到目标周的周末(周日)
|
||||
// 2. 再向前移动 |dayIndex| - 1 天(例如dayIndex=-1为周六)
|
||||
targetMoment.endOf('week').add(dayIndex + 1, 'days');
|
||||
}
|
||||
|
||||
// 返回标准化后的日期(去除时间部分)
|
||||
return this.as_date(targetMoment);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析中文自然语言日期(新增支持"下个月5号"/"上个月15号"等格式)
|
||||
* @param {string} msg - 包含日期的文本(如"下个月5号开会")
|
||||
* @param {moment.Moment} base - 基准日期,默认为当天
|
||||
* @returns {{date: string, text: string}} 处理后的日期和文本
|
||||
*
|
||||
* @example
|
||||
* parse_date("下个月5号评审") // {date: "2025-07-05", text: "评审"}
|
||||
* parse_date("上个月15号账单") // {date: "2025-05-15", text: "账单"}
|
||||
*/
|
||||
extract_chinese_date(msg:string, base = this.today) {
|
||||
let result:{[key:string]:any} = { date: base.format('YYYY-MM-DD'), text: msg };
|
||||
|
||||
// 1. 处理相对天数(今天/昨天/明天等)
|
||||
let dayKeywords = [
|
||||
{ pattern: /^大前天/, days: -3 },
|
||||
{ pattern: /^前天/, days: -2 },
|
||||
{ pattern: /^昨天/, days: -1 },
|
||||
{ pattern: /^今天/, days: 0 },
|
||||
{ pattern: /^明天/, days: 1 },
|
||||
{ pattern: /^后天/, days: 2 },
|
||||
{ pattern: /^大后天/, days: 3 }
|
||||
];
|
||||
for (let { pattern, days } of dayKeywords) {
|
||||
if (pattern.test(msg)) {
|
||||
result.date = base.clone().add(days, 'days').format('YYYY-MM-DD');
|
||||
result.text = msg.replace(pattern, '').trim();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 处理「下个月X号」或「上个月X号」格式
|
||||
let monthDayMatch = msg.match(/^(上个月|下个月)(\d{1,2})号?/);
|
||||
if (monthDayMatch) {
|
||||
let [fullMatch, direction, day] = monthDayMatch;
|
||||
let monthOffset = direction === '上个月' ? -1 : 1;
|
||||
let targetDate = base.clone().add(monthOffset, 'months').date(parseInt(day));
|
||||
|
||||
if (targetDate.date() !== parseInt(day)) {
|
||||
targetDate.endOf('month');
|
||||
}
|
||||
|
||||
result.date = targetDate.format('YYYY-MM-DD');
|
||||
result.text = msg.slice(fullMatch.length).trim();
|
||||
return result;
|
||||
}
|
||||
|
||||
// 4. 处理周几和下周几
|
||||
let weekMatch = msg.match(/^([上下]([一二三四五六七八九十两]|\d+)周周|上上周|上上星期|上周|上星期|周|星期|下周|下星期|下下周|下下星期)([一二三四五六七日]|[1-7])/);
|
||||
if (weekMatch) {
|
||||
let [fullMatch,weekStr, weekCount,dayChar] = weekMatch;
|
||||
let dayMap:{[key:string]:any} = { '一':1, '二':2, '三':3, '四':4, '五':5, '六':6, '日':7, '七':7};
|
||||
if(weekCount){
|
||||
let nmap:{[key:string]:any} = { '一':1, '二':2, '三':3, '四':4, '五':5, '六':6, '日':7, '七':7,'八':8,'九':9,'两':2};
|
||||
weekCount = nmap[weekCount] || parseInt(weekCount);
|
||||
}
|
||||
let targetDay = dayMap[dayChar] || parseInt(dayChar);
|
||||
let weekOffset = ['周','星期'].contains(weekStr)? 0 :
|
||||
['下周','下星期'].contains(weekStr)? 1 :
|
||||
['下下周','下下星期'].contains(weekStr)? 2 :
|
||||
['上周','上星期'].contains(weekStr)? -1 :
|
||||
['上上周','上上星期'].contains(weekStr)? -2 :
|
||||
(msg.slice(0,1)=='上' ? -weekCount : weekCount);
|
||||
let date = this.relative_week_day(targetDay,weekOffset as number,base);
|
||||
|
||||
if (date.isBefore(base, 'day')) {
|
||||
date.add(1, 'week');
|
||||
}
|
||||
|
||||
result.date = date.format('YYYY-MM-DD');
|
||||
result.text = msg.slice(fullMatch.length).trim();
|
||||
return result;
|
||||
}
|
||||
|
||||
// 5. 处理x月y号格式
|
||||
let absoluteMonthMatch = msg.match(/^(\d{1,2})月(\d{1,2})(?:号|日)?/);
|
||||
if (absoluteMonthMatch) {
|
||||
const [fullMatch, month, day] = absoluteMonthMatch;
|
||||
let date = base.clone().month(parseInt(month) - 1).date(parseInt(day));
|
||||
|
||||
if (date.isBefore(base, 'day')) {
|
||||
date.add(1, 'year');
|
||||
}
|
||||
|
||||
result.date = date.format('YYYY-MM-DD');
|
||||
result.text = msg.slice(fullMatch.length).trim();
|
||||
return result;
|
||||
}
|
||||
if(result.text==msg){
|
||||
result.date = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
parse_minutes(xt:string) {
|
||||
if(typeof xt == 'number'){return xt;}
|
||||
if(xt.match(/^\d*$/) && parseInt(xt)){return parseInt(xt);}
|
||||
|
||||
let items = xt.match(/^(.{1,2})个半小时$/);
|
||||
if(items){return this.ea.editor.cn2num(items[1])*60+30}
|
||||
|
||||
let compoundMatch = xt.match(/^(.*?)(h|hour|hours|时|小时|个小时)(.*?)(m|min|minute|minutes|分|分钟)?$/i);
|
||||
if (compoundMatch) {
|
||||
let hours = this.ea.editor.cn2num(compoundMatch[1]) || 0;
|
||||
let minutes = this.ea.editor.cn2num(compoundMatch[3]) || 0;
|
||||
return Math.round(hours * 60 + minutes);
|
||||
}
|
||||
// 处理简单格式,仅分钟
|
||||
let simpleMatch = xt.match(/^(.*?)(m|min|minute|minutes|分|分钟)$/i);
|
||||
if (simpleMatch) {
|
||||
let value = this.ea.editor.cn2num(simpleMatch[1]);
|
||||
return Math.round(value);
|
||||
}
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
|
||||
parse_time(st:string|Moment, date:Moment|string = this.today,nearest=true) {
|
||||
if(!st){return null}
|
||||
if(moment.isMoment(st)){return st}
|
||||
|
||||
if(moment.isMoment(date)){
|
||||
date = date.format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
let items = st.match(/^(\d{2}):?(\d{2})$/);
|
||||
|
||||
if(items){
|
||||
let t = moment(`${date} ${items[1]}:${items[2]}:00`, "YYYY-MM-DD HH:mm:ss");
|
||||
if(t.isValid()){return t}
|
||||
}
|
||||
|
||||
let cnTimeRegex = /^(早上|上午|凌晨|下午|晚上)?([零一二三四五六七八九十百]+|[\d]+)点(半|([零一二三四五六七八九十]+)分?|([\d]+)分?)?$/;
|
||||
let match = st.match(cnTimeRegex);
|
||||
|
||||
if (match) {
|
||||
let [_, period, hourStr, minuteCnStr] = match;
|
||||
let hour = this.ea.editor.cn2num(hourStr);
|
||||
|
||||
let minute = 0;
|
||||
if (minuteCnStr === '半') {
|
||||
minute = 30;
|
||||
} else if (minuteCnStr) {
|
||||
minute = this.ea.editor.cn2num(minuteCnStr);
|
||||
}
|
||||
|
||||
if (['下午'].includes(period)) {
|
||||
hour = hour >= 12 ? hour : hour + 12;
|
||||
} else if (['晚上'].includes(period)){
|
||||
hour = hour >=5 && hour<12? hour+12:hour;
|
||||
}else if (!period && nearest && hour<=12) {
|
||||
let t = moment();
|
||||
let a = t.hour()*60+t.minutes();
|
||||
let b = hour*60+minute;
|
||||
if(a>b && (a-b)>(b-a+12*60)){
|
||||
hour=hour+12;
|
||||
}
|
||||
}
|
||||
|
||||
hour %= 24;
|
||||
return moment(`${date} ${hour}:${minute}`, "YYYY-MM-DD HH:mm");
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
time_plus_minutes(st:string,xt:string){
|
||||
let t = this.parse_time(st);
|
||||
let n = this.parse_minutes(xt);
|
||||
if(!t || typeof t == 'string' || Number.isNaN(n)){return null}
|
||||
return t.clone().add(xt, 'minutes');
|
||||
}
|
||||
|
||||
generate_start_times(jobs:Array<any>,delta=10, is_today = true,st:string|Moment='06:45',compress=true) {
|
||||
// 从 st 到当前时间
|
||||
let _st = this.parse_time(st)
|
||||
if(!_st){return []}
|
||||
st = _st;
|
||||
let timeList = [];
|
||||
let t = this.parse_time(moment().format('HH:mm'));
|
||||
if (!is_today || true) {
|
||||
t = this.parse_time(moment().format('23:59'));
|
||||
}
|
||||
if(!t){return []}
|
||||
for (let hour = st.hour(); hour <= t.hour(); hour++) {
|
||||
let startMinute = (hour === st.hour()) ? st.minute() : 0;
|
||||
let endMinute = (hour === t.hour()) ? t.minute()+1 : 60;
|
||||
for (let minute = startMinute; minute < endMinute; minute += delta) {
|
||||
let time = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
||||
let ct = this.parse_time(time);
|
||||
if(!ct){continue}
|
||||
let flag = true;
|
||||
for (let item of jobs) {
|
||||
if (item.st <= ct && item.et > ct) {
|
||||
flag = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (flag) {
|
||||
timeList.push(time);
|
||||
}
|
||||
}
|
||||
}
|
||||
let et = this.get_max_endt(jobs)?.format('HH:mm');
|
||||
if (et && !timeList.contains(et)) {
|
||||
timeList.push(et)
|
||||
}
|
||||
timeList = timeList.sort((a, b) => -a.localeCompare(b));
|
||||
if(compress){
|
||||
return this.compress_timelist(timeList,delta)
|
||||
}else{
|
||||
return timeList
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
get_max_endt(jobs:Array<any>,st='06:45') {
|
||||
if (jobs.length == 0) {
|
||||
return this.parse_time(st)
|
||||
} else {
|
||||
return moment.unix(
|
||||
Math.max(...jobs.map(x => x.et)) / 1000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compress_timelist(timeList:Array<string>,delta=5){
|
||||
let compressedList = [];
|
||||
let startRange = null;
|
||||
let prevTime = null;
|
||||
|
||||
for (let i = 0; i < timeList.length; i++) {
|
||||
let currentTime = timeList[i];
|
||||
let currentParsed = this.parse_time(currentTime);
|
||||
if(!currentParsed){continue}
|
||||
|
||||
if (prevTime === null) {
|
||||
startRange = currentTime;
|
||||
} else {
|
||||
let prevParsed = this.parse_time(prevTime);
|
||||
if(!prevParsed){continue}
|
||||
// 检查是否连续(相差5分钟)
|
||||
let diffMinutes = (prevParsed.hour() * 60 + prevParsed.minute()) -
|
||||
(currentParsed.hour() * 60 + currentParsed.minute());
|
||||
|
||||
if (diffMinutes !== delta) {
|
||||
if (startRange !== prevTime) {
|
||||
compressedList.push(startRange)
|
||||
compressedList.push(prevTime);
|
||||
} else {
|
||||
compressedList.push(startRange);
|
||||
}
|
||||
startRange = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
prevTime = currentTime;
|
||||
}
|
||||
|
||||
// 处理最后一个范围
|
||||
if (startRange !== prevTime) {
|
||||
compressedList.push(startRange)
|
||||
compressedList.push(prevTime);
|
||||
} else if (prevTime !== null) {
|
||||
compressedList.push(prevTime);
|
||||
}
|
||||
|
||||
return compressedList;
|
||||
}
|
||||
}
|
||||
38
src/easyapi/waiter.ts
Normal file
38
src/easyapi/waiter.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { App, TFile,moment } from "obsidian";
|
||||
import { EasyAPI } from "./easyapi";
|
||||
|
||||
|
||||
export class Waiter {
|
||||
app:App;
|
||||
ea:EasyAPI;
|
||||
constructor(app:App,ea:EasyAPI){
|
||||
this.app = app;
|
||||
this.ea = ea;
|
||||
}
|
||||
|
||||
async wait(condition:Function,timeout:number=0){
|
||||
let start = moment();
|
||||
while (!condition()) {
|
||||
let end = moment();
|
||||
if ((start.valueOf()-end.valueOf())/1000 > timeout) {
|
||||
return false;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async wait_for(vfunc:Function,timeout:number=30){
|
||||
let start = moment();
|
||||
let res = await vfunc();
|
||||
while (!res) {
|
||||
let end = moment();
|
||||
if ((start.valueOf()-end.valueOf())/1000 > timeout) {
|
||||
return null;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
res = await vfunc();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
199
src/easyapi/web.ts
Normal file
199
src/easyapi/web.ts
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import { App } from 'obsidian';
|
||||
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const url = require('url');
|
||||
|
||||
export interface WebRequestOptions {
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
|
||||
headers?: { [key: string]: string };
|
||||
body?: any;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export interface WebResponse {
|
||||
statusCode: number;
|
||||
headers: { [key: string]: string };
|
||||
body: any;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export class Web {
|
||||
app: App;
|
||||
|
||||
constructor(app: App) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 HTTP 请求
|
||||
* @param urlStr 请求 URL
|
||||
* @param options 请求选项
|
||||
* @returns Promise<WebResponse>
|
||||
*/
|
||||
async request(urlStr: string, options: WebRequestOptions = {}): Promise<WebResponse> {
|
||||
// 解析 URL 并正确处理编码
|
||||
const parsed = url.parse(urlStr);
|
||||
const isHttps = parsed.protocol === 'https:';
|
||||
const client = isHttps ? https : http;
|
||||
|
||||
// 处理路径和查询字符串的编码
|
||||
let requestPath = parsed.pathname || '/';
|
||||
|
||||
// 处理查询字符串,确保中文字符被正确编码
|
||||
if (parsed.query || parsed.search) {
|
||||
let queryString = '';
|
||||
if (parsed.search) {
|
||||
// 解析查询字符串并重新编码
|
||||
const queryParams = new URLSearchParams(parsed.search.substring(1));
|
||||
queryString = '?' + queryParams.toString();
|
||||
} else if (parsed.query) {
|
||||
// 手动处理查询字符串
|
||||
const parts: string[] = [];
|
||||
const queryPairs = parsed.query.split('&');
|
||||
for (const pair of queryPairs) {
|
||||
const [key, ...valueParts] = pair.split('=');
|
||||
const value = valueParts.join('=');
|
||||
// 对键和值进行编码(如果还没有编码)
|
||||
const encodedKey = encodeURIComponent(decodeURIComponent(key || ''));
|
||||
const encodedValue = encodeURIComponent(decodeURIComponent(value || ''));
|
||||
parts.push(`${encodedKey}=${encodedValue}`);
|
||||
}
|
||||
queryString = '?' + parts.join('&');
|
||||
}
|
||||
requestPath = requestPath + queryString;
|
||||
}
|
||||
|
||||
const method = options.method || 'GET';
|
||||
const headers = options.headers || {};
|
||||
const timeout = options.timeout || 30000; // 默认 30 秒
|
||||
|
||||
// 如果没有设置 Content-Type,根据 body 类型自动设置
|
||||
if (method !== 'GET' && options.body && !headers['Content-Type']) {
|
||||
if (typeof options.body === 'string') {
|
||||
headers['Content-Type'] = 'text/plain; charset=utf-8';
|
||||
} else if (options.body instanceof Buffer) {
|
||||
headers['Content-Type'] = 'application/octet-stream';
|
||||
} else {
|
||||
headers['Content-Type'] = 'application/json; charset=utf-8';
|
||||
options.body = JSON.stringify(options.body);
|
||||
}
|
||||
}
|
||||
|
||||
const requestOptions: any = {
|
||||
hostname: parsed.hostname,
|
||||
port: parsed.port || (isHttps ? 443 : 80),
|
||||
path: requestPath,
|
||||
method: method,
|
||||
headers: headers,
|
||||
timeout: timeout
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = client.request(requestOptions, (res: any) => {
|
||||
let data = '';
|
||||
|
||||
res.on('data', (chunk: Buffer) => {
|
||||
data += chunk.toString();
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
let body: any = data;
|
||||
|
||||
// 尝试解析 JSON
|
||||
const contentType = res.headers['content-type'] || '';
|
||||
if (contentType.includes('application/json')) {
|
||||
try {
|
||||
body = JSON.parse(data);
|
||||
} catch {
|
||||
// 解析失败,保持原样
|
||||
}
|
||||
}
|
||||
|
||||
const response: WebResponse = {
|
||||
statusCode: res.statusCode || 200,
|
||||
headers: res.headers,
|
||||
body: body,
|
||||
text: data
|
||||
};
|
||||
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (error: Error) => {
|
||||
reject(error);
|
||||
});
|
||||
|
||||
req.on('timeout', () => {
|
||||
req.destroy();
|
||||
reject(new Error('Request timeout'));
|
||||
});
|
||||
|
||||
// 发送请求体
|
||||
if (method !== 'GET' && options.body) {
|
||||
if (typeof options.body === 'string' || options.body instanceof Buffer) {
|
||||
req.write(options.body);
|
||||
} else {
|
||||
req.write(JSON.stringify(options.body));
|
||||
}
|
||||
}
|
||||
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求
|
||||
* @param urlStr 请求 URL
|
||||
* @param options 请求选项(可选)
|
||||
* @returns Promise<WebResponse>
|
||||
*/
|
||||
async get(urlStr: string, options: Omit<WebRequestOptions, 'method'> = {}): Promise<WebResponse> {
|
||||
return this.request(urlStr, { ...options, method: 'GET' });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求
|
||||
* @param urlStr 请求 URL
|
||||
* @param body 请求体(可选)
|
||||
* @param options 请求选项(可选)
|
||||
* @returns Promise<WebResponse>
|
||||
*/
|
||||
async post(urlStr: string, body?: any, options: Omit<WebRequestOptions, 'method' | 'body'> = {}): Promise<WebResponse> {
|
||||
return this.request(urlStr, { ...options, method: 'POST', body: body });
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT 请求
|
||||
* @param urlStr 请求 URL
|
||||
* @param body 请求体(可选)
|
||||
* @param options 请求选项(可选)
|
||||
* @returns Promise<WebResponse>
|
||||
*/
|
||||
async put(urlStr: string, body?: any, options: Omit<WebRequestOptions, 'method' | 'body'> = {}): Promise<WebResponse> {
|
||||
return this.request(urlStr, { ...options, method: 'PUT', body: body });
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 请求
|
||||
* @param urlStr 请求 URL
|
||||
* @param options 请求选项(可选)
|
||||
* @returns Promise<WebResponse>
|
||||
*/
|
||||
async delete(urlStr: string, options: Omit<WebRequestOptions, 'method'> = {}): Promise<WebResponse> {
|
||||
return this.request(urlStr, { ...options, method: 'DELETE' });
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH 请求
|
||||
* @param urlStr 请求 URL
|
||||
* @param body 请求体(可选)
|
||||
* @param options 请求选项(可选)
|
||||
* @returns Promise<WebResponse>
|
||||
*/
|
||||
async patch(urlStr: string, body?: any, options: Omit<WebRequestOptions, 'method' | 'body'> = {}): Promise<WebResponse> {
|
||||
return this.request(urlStr, { ...options, method: 'PATCH', body: body });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,7 +41,6 @@ export class FsEditor{
|
|||
}
|
||||
return null;
|
||||
}catch{
|
||||
// console.log(path)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -95,8 +94,11 @@ export class FsEditor{
|
|||
}
|
||||
|
||||
|
||||
abspath(tfile:TFile|TFolder){
|
||||
if(tfile){
|
||||
abspath(tfile:TFile|TFolder|string){
|
||||
if(typeof tfile == 'string'){
|
||||
return (this.root+'/'+tfile).replace(/\\/g,'/');
|
||||
}
|
||||
else if(tfile){
|
||||
return (this.root+'/'+tfile.path).replace(/\\/g,'/');
|
||||
}else{
|
||||
return null;
|
||||
|
|
@ -180,17 +182,20 @@ export class FsEditor{
|
|||
if(mode==='overwrite'){
|
||||
fs.unlinkSync(dst);
|
||||
fs.copyFileSync(src,dst);
|
||||
console.log('Overwrite: '+src+' to '+dst);
|
||||
return true;
|
||||
}else if(mode==='mtime'){
|
||||
// dst 更新时间小于 src
|
||||
if(fs.statSync(dst).mtimeMs<fs.statSync(src).mtimeMs){
|
||||
fs.unlinkSync(dst);
|
||||
fs.copyFileSync(src,dst);
|
||||
console.log('Update: '+src+' to '+dst);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}else{
|
||||
fs.copyFileSync(src,dst);
|
||||
console.log('Copy: '+src+' to '+dst);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -244,15 +249,28 @@ export class FsEditor{
|
|||
}
|
||||
}
|
||||
|
||||
delete_file_or_dir(path:string){
|
||||
async delete_file_or_dir(path:string){
|
||||
if(this.isfile(path)){
|
||||
this.fs.unlinkSync(path)
|
||||
if (await this.plugin.dialog_suggest(['❌','✔️'],[false,true],path)){
|
||||
console.log(`Delete file: ${path}`)
|
||||
this.fs.unlinkSync(path)
|
||||
}
|
||||
|
||||
}else if(this.isdir(path)){
|
||||
let items = this.list_dir(path,true)
|
||||
for(let item of items){
|
||||
this.delete_file_or_dir(item)
|
||||
if (await this.plugin.dialog_suggest(['❌','✔️'],[false,true],path)){
|
||||
console.log(`Delete folder: ${path}`)
|
||||
this.fs.rmdirSync(path)
|
||||
}else{
|
||||
return;
|
||||
}
|
||||
this.fs.rmdirSync(path)
|
||||
for(let item of items){
|
||||
if (await this.plugin.dialog_suggest(['❌','✔️'],[false,true],item)){
|
||||
console.log(`Delete file: ${path}`)
|
||||
this.delete_file_or_dir(item)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -265,7 +283,7 @@ export class FsEditor{
|
|||
let asrc = src+'/'+item
|
||||
if(this.isfile(adst)){
|
||||
if(!this.isfile(asrc)){
|
||||
this.fs.unlinkSync(adst)
|
||||
this.delete_file_or_dir(adst);
|
||||
}
|
||||
}else if(this.isdir(adst)){
|
||||
if(!this.isdir(asrc)){
|
||||
|
|
@ -312,4 +330,25 @@ export class FsEditor{
|
|||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
list_dir_recursive(path:string,with_folder=false):Array<string>{
|
||||
if(!this.isdir(path)){return []}
|
||||
let res = []
|
||||
let items = this.plugin.fsEditor.list_dir(path,true)
|
||||
for(let item of items){
|
||||
if(this.isfile(item)){
|
||||
res.push(item)
|
||||
}else if(this.isdir(item)){
|
||||
if(with_folder){
|
||||
res.push()
|
||||
}
|
||||
let sitems = this.list_dir_recursive(item,with_folder);
|
||||
for(let i of sitems){
|
||||
res.push(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,11 +7,21 @@ import NoteSyncPlugin from '../main';
|
|||
export interface MySettings {
|
||||
strict_mode: boolean;
|
||||
vaultDir:string;
|
||||
git_repo:string;
|
||||
wxmp_config:string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: MySettings = {
|
||||
strict_mode:false,
|
||||
vaultDir:''
|
||||
vaultDir: '',
|
||||
git_repo: 'https://github.com/zigholding/ObsidianZ/tree/master\nhttps://gitee.com/zigholding/ObsidianZ/tree/master',
|
||||
wxmp_config: `
|
||||
h1: ob 公众号标题 h1 样式
|
||||
h2: ob 公众号标题 h2 样式
|
||||
h3: ob 公众号标题 hx 样式
|
||||
p code: ob 公众号行内代码样式
|
||||
li code: ob 公众号行内代码样式
|
||||
`.trim()
|
||||
}
|
||||
|
||||
export class NoteSyncSettingTab extends PluginSettingTab {
|
||||
|
|
@ -60,5 +70,23 @@ export class NoteSyncSettingTab extends PluginSettingTab {
|
|||
this.plugin.strings.setting_strict_mode_desc,
|
||||
'strict_mode'
|
||||
);
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(this.plugin.strings.setting_git_repo)
|
||||
.addTextArea(text => text
|
||||
.setValue(this.plugin.settings.git_repo)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.git_repo = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(this.plugin.strings.setting_wxmp_config)
|
||||
.addTextArea(text => text
|
||||
.setValue(this.plugin.settings.wxmp_config)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.wxmp_config = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,30 @@ export class Strings{
|
|||
}
|
||||
}
|
||||
|
||||
get cmd_download_git_repo(){
|
||||
if(this.language=='zh'){
|
||||
return '下载 Git 仓库文件'
|
||||
}else{
|
||||
return 'Download git repo';
|
||||
}
|
||||
}
|
||||
|
||||
get cmd_export_wxmp(){
|
||||
if(this.language=='zh'){
|
||||
return '导出微信公众号';
|
||||
}else{
|
||||
return 'Export wxmp';
|
||||
}
|
||||
}
|
||||
|
||||
get cmd_export_as_single_note(){
|
||||
if(this.language=='zh'){
|
||||
return '导出多条笔记';
|
||||
}else{
|
||||
return 'Export notes';
|
||||
}
|
||||
}
|
||||
|
||||
get prompt_path_of_folder(){
|
||||
if(this.language=='zh'){
|
||||
return '输入文件夹路径'
|
||||
|
|
@ -81,6 +105,23 @@ export class Strings{
|
|||
}
|
||||
}
|
||||
|
||||
get setting_git_repo(){
|
||||
if(this.language=='zh'){
|
||||
return 'Git 仓库';
|
||||
}else{
|
||||
return 'Git repository';
|
||||
}
|
||||
}
|
||||
|
||||
get setting_wxmp_config(){
|
||||
if(this.language=='zh'){
|
||||
return '微信公众号样式配置';
|
||||
}else{
|
||||
return 'Style config for wxmp';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get item_copy_data_json(){
|
||||
if(this.language=='zh'){
|
||||
return '复制 data.json';
|
||||
|
|
|
|||
655
src/wxmp.ts
Normal file
655
src/wxmp.ts
Normal file
|
|
@ -0,0 +1,655 @@
|
|||
|
||||
import { App, Editor, MarkdownView, Modal, Notice, Plugin, PluginSettingTab, Setting, TFile, TFolder } from 'obsidian';
|
||||
import NoteSyncPlugin from "../main";
|
||||
import { on } from 'node:events';
|
||||
|
||||
export class Wxmp {
|
||||
marked: any;
|
||||
hljs: any;
|
||||
app: App;
|
||||
plugin: NoteSyncPlugin;
|
||||
|
||||
constructor(plugin: NoteSyncPlugin) {
|
||||
this.plugin = plugin;
|
||||
this.app = plugin.app;
|
||||
this.marked = require('marked');
|
||||
this.hljs = require('highlight.js');
|
||||
}
|
||||
|
||||
get blank_line(){
|
||||
return '<section><br></section>';
|
||||
}
|
||||
|
||||
get ctx_map() {
|
||||
let msg = this.plugin.settings.wxmp_config.trim();
|
||||
if(msg.trim()==''){
|
||||
return {};
|
||||
}
|
||||
let config = this.plugin.easyapi.editor.yamljs.load(msg);
|
||||
if(!config){
|
||||
return {};
|
||||
}
|
||||
if(!config['h1']){
|
||||
config['h1'] = this.format_wxmp_h1;
|
||||
}
|
||||
|
||||
if (!config['h1']) {
|
||||
config['h1'] = this.format_wxmp_h1.bind(this);
|
||||
}
|
||||
if (!config['h2']) {
|
||||
config['h2'] = this.format_wxmp_h2.bind(this);
|
||||
}
|
||||
if (!config['h3']) {
|
||||
config['h3'] = this.format_wxmp_h3.bind(this);
|
||||
}
|
||||
if (!config['p code']) {
|
||||
config['p code'] = this.format_wxmp_p_code.bind(this);
|
||||
}
|
||||
if (!config['li code']) {
|
||||
config['li code'] = this.format_wxmp_li_code.bind(this);
|
||||
}
|
||||
|
||||
if (!config['section@html']) {
|
||||
config['section@html'] = this.format_section_html.bind(this);
|
||||
}
|
||||
|
||||
if (!config['section@cards-album']) {
|
||||
config['section@cards-album'] = this.format_code_block_cards_album.bind(this);
|
||||
}
|
||||
let tfiles = this.plugin.easyapi.file.get_all_tfiles_of_tags('NoteSyncWxmp');
|
||||
for(let i in tfiles){
|
||||
config[`section@${i}`] = tfiles[i].basename;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
arrayBufferToBase64(buffer: ArrayBuffer) {
|
||||
let binary = '';
|
||||
let bytes = new Uint8Array(buffer);
|
||||
let len = bytes.byteLength;
|
||||
for (let i = 0; i < len; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return window.btoa(binary);
|
||||
}
|
||||
|
||||
async read_html_from_clipboard(mime = 'text/html') {
|
||||
let ctxs = await navigator.clipboard.read()
|
||||
|
||||
for (let ctx of ctxs) {
|
||||
let blob = await ctx.getType(mime)
|
||||
let html = await blob.text()
|
||||
return html
|
||||
}
|
||||
}
|
||||
|
||||
async replace_regx_with_tpl(rhtml: string, regx: RegExp, tpl: string) {
|
||||
let matches = [...rhtml.matchAll(regx)];
|
||||
|
||||
let replacements = await Promise.all(
|
||||
matches.map(async ([match, title]) => {
|
||||
let msg = await this.plugin.easyapi.tpl.parse_templater(tpl, true, title);
|
||||
return { match, replacement: msg[0] };
|
||||
})
|
||||
);
|
||||
|
||||
// 逐个替换
|
||||
for (let { match, replacement } of replacements) {
|
||||
rhtml = rhtml.replace(match, replacement);
|
||||
}
|
||||
|
||||
return rhtml;
|
||||
}
|
||||
|
||||
convertVaultImageLinksToImgTag(htmlString: string) {
|
||||
return htmlString.replace(/!\[\[([^\]]+?)\]\]/g, (match, filename) => {
|
||||
return `<img src="${filename.trim()}">`;
|
||||
});
|
||||
}
|
||||
|
||||
async convertImageTagsToBase64(htmlString: string) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||
|
||||
const imgElements = doc.querySelectorAll('img');
|
||||
|
||||
for (let img of Array.from(imgElements)) {
|
||||
let src = img.getAttribute('src');
|
||||
if (!src) continue;
|
||||
|
||||
// 只处理 vault 中的本地图片
|
||||
let fname = decodeURIComponent(src.replace(/^.*[\\\/]/, ''));
|
||||
|
||||
try {
|
||||
let base64 = await this.image_to_img(fname, true); // 返回 base64
|
||||
if (base64) {
|
||||
img.setAttribute('src', base64);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`图片 ${src} 转换失败:`, e);
|
||||
}
|
||||
}
|
||||
return new XMLSerializer().serializeToString(doc.body);
|
||||
}
|
||||
|
||||
async section_to_wxmp(section:any,sec:string,ctx:string){
|
||||
if (section.type == 'yaml') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let rhtml:any = null;
|
||||
let ctx_map = this.ctx_map;
|
||||
for(let k in ctx_map){
|
||||
if(k.startsWith('section@')){
|
||||
let tpl = ctx_map[k];
|
||||
if (typeof tpl == 'function') {
|
||||
rhtml = await tpl(section,sec);
|
||||
} else {
|
||||
let rendered = await this.plugin.easyapi.tpl.parse_templater(
|
||||
tpl, true,{section:section,sec:sec,ctx:ctx},[0]
|
||||
);
|
||||
rendered = rendered.filter(x=>x);
|
||||
if (rendered && rendered.length > 0 && rendered[0].trim() != '') {
|
||||
rhtml = rendered[0];
|
||||
}
|
||||
}
|
||||
if(rhtml){
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!rhtml){
|
||||
if(section.type == 'code'){
|
||||
let items = sec.split('\n');
|
||||
items[0] = items[0].slice(0,3)+'js'+'\n//'+items[0].slice(3);
|
||||
sec = items.join('\n');
|
||||
}
|
||||
let html = this.marked.marked(sec);
|
||||
rhtml = await this.html_to_wxmp(html);
|
||||
}
|
||||
return rhtml;
|
||||
}
|
||||
|
||||
async selection_to_wxmp(){
|
||||
let htmls = [this.blank_line];
|
||||
let sel = this.plugin.easyapi.ceditor.cm.state.selection.main;
|
||||
if(sel.from==sel.to){
|
||||
return null
|
||||
}
|
||||
let ctx = await this.plugin.easyapi.ccontent;
|
||||
if(!ctx){
|
||||
return null
|
||||
}
|
||||
for(let section of this.plugin.easyapi.cmeta?.sections || []){
|
||||
if(sel.to<=section.position.start.offset){
|
||||
continue
|
||||
}
|
||||
if(sel.from>=section.position.end.offset){
|
||||
continue
|
||||
}
|
||||
let from = Math.max(sel.from,section.position.start.offset);
|
||||
let to = Math.min(sel.to,section.position.end.offset);
|
||||
let sec = ctx.slice(from,to).trim();
|
||||
|
||||
if(section.type=='code'){
|
||||
let items = this.plugin.easyapi.editor.slice_by_position(
|
||||
ctx, section.position
|
||||
).trim().split('\n')
|
||||
if(!sec.startsWith(items[0])){
|
||||
sec = items[0]+'\n'+sec;
|
||||
}
|
||||
if(!sec.endsWith(items.last() || '')){
|
||||
sec = sec + '\n' + items.last();
|
||||
}
|
||||
}
|
||||
|
||||
if(sec==''){continue}
|
||||
let rhtml = await this.section_to_wxmp(section,sec,sec);
|
||||
if(!rhtml){continue}
|
||||
|
||||
if(Array.isArray(rhtml)){
|
||||
htmls.push(...rhtml);
|
||||
}else{
|
||||
htmls.push(rhtml);
|
||||
}
|
||||
htmls.push(this.blank_line)
|
||||
}
|
||||
this.copy_as_html(htmls);
|
||||
}
|
||||
|
||||
async tfile_to_wxmp(tfile: TFile) {
|
||||
let htmls = [this.blank_line];
|
||||
let ctx = await this.app.vault.read(tfile);
|
||||
for (let section of this.app.metadataCache.getFileCache(tfile)?.sections || []) {
|
||||
if (section.type == 'yaml') {
|
||||
continue
|
||||
}
|
||||
let sec = this.plugin.easyapi.editor.slice_by_position(ctx, section.position);
|
||||
let rhtml = await this.section_to_wxmp(section,sec,ctx);
|
||||
if(!rhtml){continue}
|
||||
|
||||
if(Array.isArray(rhtml)){
|
||||
htmls.push(...rhtml);
|
||||
}else{
|
||||
htmls.push(rhtml);
|
||||
}
|
||||
htmls.push(this.blank_line);
|
||||
}
|
||||
this.copy_as_html(htmls);
|
||||
}
|
||||
|
||||
|
||||
|
||||
async html_to_wxmp(html: string) {
|
||||
let rhtml;
|
||||
|
||||
// 替换图片
|
||||
rhtml = this.convertVaultImageLinksToImgTag(html);
|
||||
rhtml = await this.convertImageTagsToBase64(rhtml);
|
||||
// 替换链接
|
||||
rhtml = this.html_replace_url(rhtml);
|
||||
|
||||
// 替换代码
|
||||
rhtml = this.html_replace_code(rhtml)
|
||||
|
||||
// 替换标题
|
||||
let ctx_map = this.ctx_map;
|
||||
for (let k in ctx_map) {
|
||||
if(k.contains('@')){continue;}
|
||||
rhtml = await this.set_tag_with_tpl(rhtml, k, ctx_map[k]);
|
||||
}
|
||||
|
||||
// [[格式化图片链接]]
|
||||
rhtml = this.formatWeChatImageLink(rhtml)
|
||||
|
||||
// 列表最后一个元素段后距设置为 24px
|
||||
rhtml = this.setLastLiMargin(rhtml)
|
||||
// 列表之前一个元素段后距设置为 8px
|
||||
rhtml = this.setParagraphSpacingBeforeList(rhtml)
|
||||
return rhtml
|
||||
}
|
||||
|
||||
async set_tag_with_tpl(htmlString: string, selector: string, tpl: string | Function) {
|
||||
let parser = new DOMParser();
|
||||
let doc = parser.parseFromString(htmlString, 'text/html');
|
||||
let items = doc.querySelectorAll(selector);
|
||||
|
||||
await Promise.all(Array.from(items).map(async (item) => {
|
||||
let content = item.textContent;
|
||||
|
||||
if (typeof tpl == 'function') {
|
||||
let rendered = tpl(content);
|
||||
if (rendered) {
|
||||
item.innerHTML = rendered;
|
||||
}
|
||||
} else {
|
||||
// 模板渲染,传入content
|
||||
let rendered = await this.plugin.easyapi.tpl.parse_templater(tpl, true, content);
|
||||
if (rendered.length > 0) {
|
||||
item.innerHTML = rendered[0];
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// 序列化回字符串
|
||||
let serializer = new XMLSerializer();
|
||||
let modifiedHtmlString = serializer.serializeToString(doc.body);
|
||||
return modifiedHtmlString;
|
||||
}
|
||||
|
||||
|
||||
setLastLiMargin(htmlString: string) {
|
||||
let parser = new DOMParser();
|
||||
let doc = parser.parseFromString(htmlString, 'text/html');
|
||||
|
||||
let lists = doc.querySelectorAll('ol, ul');
|
||||
|
||||
lists.forEach(list => {
|
||||
let lis = list.querySelectorAll('li');
|
||||
if (lis.length > 0) {
|
||||
let lastLi = lis[lis.length - 1];
|
||||
|
||||
// 保留原始内容,包裹一个section加margin
|
||||
let originalHTML = lastLi.innerHTML;
|
||||
lastLi.innerHTML = `
|
||||
<section style="margin-bottom: 24px;">
|
||||
${originalHTML}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
});
|
||||
|
||||
let serializer = new XMLSerializer();
|
||||
return serializer.serializeToString(doc.body);
|
||||
}
|
||||
|
||||
|
||||
setParagraphSpacingBeforeList(htmlString: string) {
|
||||
// 创建一个新的DOM解析器
|
||||
let parser = new DOMParser();
|
||||
// 将HTML字符串解析为文档对象
|
||||
let doc = parser.parseFromString(htmlString, 'text/html');
|
||||
|
||||
// 获取文档中的所有有序列表和无序列表
|
||||
let lists = doc.querySelectorAll('ol, ul');
|
||||
|
||||
lists.forEach(list => {
|
||||
// 找到列表前面的第一个段落
|
||||
let precedingParagraph = list.previousElementSibling;
|
||||
if (precedingParagraph && precedingParagraph.tagName.toLowerCase() === 'p') {
|
||||
// 设置段后距为8px
|
||||
(precedingParagraph as any).style.marginBottom = '8px';
|
||||
}
|
||||
});
|
||||
|
||||
// 将修改后的文档对象转换回HTML字符串
|
||||
let serializer = new XMLSerializer();
|
||||
let modifiedHtmlString = serializer.serializeToString(doc.body);
|
||||
|
||||
return modifiedHtmlString;
|
||||
}
|
||||
|
||||
html_replace_url(html: string) {
|
||||
let regx = /<a[^>]*class="external-link"[^>]*href="(.*?)"[^>]*?>([\s\S]*?)<\/a>/g
|
||||
let rhtml = html.replace(regx, (m, href, text) => {
|
||||
|
||||
let flag = false
|
||||
for (let url of [
|
||||
'https://mmbiz.qpic.cn',
|
||||
'https://mp.weixin.qq.com'
|
||||
]) {
|
||||
if (href.trim().startsWith(url)) {
|
||||
flag = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!flag) {
|
||||
return `<a>${text}</a>`
|
||||
}
|
||||
return `<a href="${href}" textvalue="${text}" data-itemshowtype="0" target="_blank" linktype="text" data-linktype="2">${text}</a>`
|
||||
})
|
||||
return rhtml
|
||||
}
|
||||
|
||||
|
||||
html_replace_code(html: string) {
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const codeBlocks = Array.from(doc.querySelectorAll('pre > code[class^="language-"]'));
|
||||
|
||||
codeBlocks.forEach(preCode => {
|
||||
const langMatch = preCode.className.match(/language-(\w+)/);
|
||||
const lang = langMatch ? langMatch[1] : 'plaintext';
|
||||
const rawCode = preCode.textContent;
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = this.hljs.highlight(rawCode, { language: lang });
|
||||
} catch (e) {
|
||||
console.warn(`高亮失败: ${lang}`, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// 替换 class 为公众号 class
|
||||
const classPairs = [
|
||||
['hljs-keyword', 'code-snippet__keyword'],
|
||||
['hljs-attr', 'code-snippet__attr'],
|
||||
['hljs-string', 'code-snippet__string'],
|
||||
['hljs-number', 'code-snippet__number'],
|
||||
['hljs-comment', 'code-snippet__comment'],
|
||||
['hljs-title', 'code-snippet__title'],
|
||||
['hljs-variable', 'code-snippet__variable'],
|
||||
['hljs-operator', 'code-snippet__operator'],
|
||||
['hljs-punctuation', 'code-snippet__punctuation'],
|
||||
];
|
||||
let highlightedHtml = result.value;
|
||||
for (let [from, to] of classPairs) {
|
||||
highlightedHtml = highlightedHtml.replaceAll(from, to);
|
||||
}
|
||||
|
||||
const lines = highlightedHtml.split('\n');
|
||||
|
||||
// 构造 section 容器
|
||||
const section = doc.createElement('section');
|
||||
section.className = `code-snippet__fix code-snippet__${lang}`;
|
||||
|
||||
// 构造行号
|
||||
const ul = doc.createElement('ul');
|
||||
ul.className = `code-snippet__line-index code-snippet__${lang}`;
|
||||
lines.forEach(() => ul.appendChild(doc.createElement('li')));
|
||||
section.appendChild(ul);
|
||||
|
||||
// 构造代码主体
|
||||
const pre = doc.createElement('pre');
|
||||
pre.className = `code-snippet__${lang}`;
|
||||
pre.setAttribute('data-lang', lang);
|
||||
|
||||
lines.forEach((line: string) => {
|
||||
const codeLine = doc.createElement('code');
|
||||
const span = doc.createElement('span');
|
||||
span.setAttribute('leaf', '');
|
||||
span.innerHTML = line.trim() === '' ? '<br>' : line;
|
||||
codeLine.appendChild(span);
|
||||
pre.appendChild(codeLine);
|
||||
});
|
||||
|
||||
section.appendChild(pre);
|
||||
|
||||
// 隐藏的 mp-style-type
|
||||
const mpStyleP = doc.createElement('p');
|
||||
mpStyleP.setAttribute('style', 'display: none;');
|
||||
mpStyleP.innerHTML = `<mp-style-type data-value="3"></mp-style-type>`;
|
||||
|
||||
// 替换原来的 <pre>
|
||||
const preElement = preCode.parentElement;
|
||||
if (preElement) {
|
||||
preElement.replaceWith(section, mpStyleP);
|
||||
}
|
||||
});
|
||||
|
||||
return new XMLSerializer().serializeToString(doc.body);
|
||||
}
|
||||
|
||||
|
||||
async image_to_img(fname: string, as_base64 = false) {
|
||||
if (fname.startsWith('!')) {
|
||||
fname = fname.slice(1)
|
||||
}
|
||||
let img_ext = ['png', 'jpg', 'jpeg']
|
||||
let tfile = this.plugin.easyapi.file.get_tfile(fname)
|
||||
if (!tfile) { return }
|
||||
let ext = tfile.extension.toLowerCase()
|
||||
if (!img_ext.contains(ext)) { return }
|
||||
let data = await this.app.vault.readBinary(tfile)
|
||||
let text = this.arrayBufferToBase64(data);
|
||||
|
||||
let bs64 = `data:image/png;base64,${text}`
|
||||
if (as_base64) {
|
||||
return bs64;
|
||||
}
|
||||
let html = `<img src="${bs64}">`
|
||||
return html
|
||||
}
|
||||
|
||||
async copy_as_html(ctx: string | string[],pre_blank=true,next_blank=true) {
|
||||
if (typeof (ctx) == 'string') {
|
||||
ctx = [ctx]
|
||||
}
|
||||
if(pre_blank && ctx[0]!=this.blank_line){
|
||||
ctx.unshift(this.blank_line)
|
||||
}
|
||||
if(next_blank && ctx[ctx.length-1]!=this.blank_line){
|
||||
ctx.push(this.blank_line)
|
||||
}
|
||||
let data = new ClipboardItem({
|
||||
"text/html": new Blob(ctx, {
|
||||
type: "text/html"
|
||||
}),
|
||||
"text/plain": new Blob(ctx, {
|
||||
type: "text/plain"
|
||||
}),
|
||||
});
|
||||
await navigator.clipboard.write([data]);
|
||||
}
|
||||
|
||||
formatWeChatImageLink(inputHtml: string) {
|
||||
// 创建一个临时的 div 元素来解析 HTML
|
||||
let tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = inputHtml;
|
||||
|
||||
// 获取所有的 <a> 标签
|
||||
let aTags = tempDiv.querySelectorAll('a');
|
||||
|
||||
// 遍历所有的 <a> 标签
|
||||
aTags.forEach(aTag => {
|
||||
let imgTag = aTag.querySelector('img');
|
||||
|
||||
if (imgTag) {
|
||||
// 提取 href 和 src
|
||||
let href = aTag.getAttribute('href');
|
||||
let src = imgTag.getAttribute('src');
|
||||
|
||||
// 获取图片格式(如 png, jpg, gif 等)
|
||||
let imgFormat = src?.split('.').pop();
|
||||
|
||||
// 构建新的 HTML
|
||||
let newHtml = `
|
||||
<a href="${href}" imgurl="${src}?wx_fmt=${imgFormat}&from=appmsg" linktype="image" tab="innerlink" data-itemshowtype="" target="_blank" data-linktype="1">
|
||||
<span class="js_jump_icon h5_image_link">
|
||||
<img data-src="${src}?wx_fmt=${imgFormat}&from=appmsg" class="rich_pages wxw-img" data-ratio="0.18611111111111112" data-s="300,640" data-type="${imgFormat}" data-w="1080" type="block" data-imgfileid="100005308" src="${src}?wx_fmt=${imgFormat}&from=appmsg">
|
||||
</span>
|
||||
</a>
|
||||
`;
|
||||
|
||||
// 替换原始的 <a> 标签
|
||||
aTag.outerHTML = newHtml;
|
||||
}
|
||||
});
|
||||
|
||||
// 返回格式化后的 HTML
|
||||
return tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
format_wxmp_h1(title: string) {
|
||||
let css = `
|
||||
<h1 style="box-sizing: border-box; border-width: 0px 0px 2px; border-style: solid; border-bottom-color: rgb(0, 152, 116); font-size: 19.6px; font-weight: bold; margin: 2em auto 1em; text-align: center; line-height: 1.75; font-family: Menlo, Monaco, "Courier New", monospace; display: table; padding: 0.5em 1em; color: rgb(63, 63, 63); text-shadow: rgba(0, 0, 0, 0.1) 2px 2px 4px; visibility: visible;"><span leaf="" style="visibility: visible;">${title}</span></h1>
|
||||
`.trim()
|
||||
return css;
|
||||
}
|
||||
|
||||
format_wxmp_h2(title: string) {
|
||||
let css = `
|
||||
<h2 style="box-sizing: border-box;border-width: 0px;border-style: solid;border-color: hsl(var(--border));font-size: 18.2px;font-weight: bold;margin: 4em auto 2em;text-align: center;line-height: 1.75;font-family: Menlo, Monaco, "Courier New", monospace;display: table;padding: 0.3em 1em;color: rgb(255, 255, 255);background: rgb(0, 152, 116);border-radius: 8px;box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px;"><span leaf="">${title}</span></h2>
|
||||
`.trim()
|
||||
return css;
|
||||
}
|
||||
|
||||
format_wxmp_h3(title: string) {
|
||||
let css = `
|
||||
<h3 style="box-sizing: border-box;border-width: 0px 0px 1px 4px;border-style: solid solid dashed;border-bottom-color: rgb(0, 152, 116);border-left-color: rgb(0, 152, 116);font-size: 16.8px;font-weight: bold;margin: 2em 8px 0.75em 0px;text-align: left;line-height: 1.2;font-family: Menlo, Monaco, "Courier New", monospace;padding-left: 12px;color: rgb(63, 63, 63);"><span leaf="">${title}</span></h3>
|
||||
`.trim()
|
||||
return css;
|
||||
}
|
||||
|
||||
format_wxmp_p_code(code: string) {
|
||||
let css = `
|
||||
<code style="box-sizing: border-box; border-width: 0px; border-style: solid; border-color: hsl(var(--border)); font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-feature-settings: normal; font-variation-settings: normal; font-size: 12.6px; text-align: left; line-height: 1.75; color: rgb(221, 17, 68); background: rgba(27, 31, 35, 0.05); padding: 3px 5px; border-radius: 4px; visibility: visible;"><span leaf="" style="visibility: visible;">${code}</span></code>`.trim()
|
||||
return css;
|
||||
}
|
||||
|
||||
format_wxmp_li_code(code: string) {
|
||||
let css = `
|
||||
<code style="box-sizing: border-box; border-width: 0px; border-style: solid; border-color: hsl(var(--border)); font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-feature-settings: normal; font-variation-settings: normal; font-size: 12.6px; text-align: left; line-height: 1.75; color: rgb(221, 17, 68); background: rgba(27, 31, 35, 0.05); padding: 3px 5px; border-radius: 4px; visibility: visible;"><span leaf="" style="visibility: visible;">${code}</span></code>`.trim()
|
||||
return css;
|
||||
}
|
||||
|
||||
is_code_balck(section:any,sec:string,lang:string){
|
||||
return section.type == 'code' && sec.trim().slice(3).startsWith(lang)
|
||||
}
|
||||
|
||||
async format_code_block_cards_album(section: any, sec:string) {
|
||||
if(section.type != 'code' || !sec.trim().slice(3).startsWith('cards-album')){
|
||||
return null;
|
||||
}
|
||||
let items = [];
|
||||
for(let ctx of sec.split('images:')[1].split(/\n\s+/)){
|
||||
if(ctx.trim() == ''){
|
||||
continue;
|
||||
}
|
||||
let img = await this.images2html(ctx);
|
||||
if(img){
|
||||
items.push(img);;
|
||||
}
|
||||
}
|
||||
if(items.length == 0){
|
||||
return null;
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
async format_section_html(section: any, sec:string) {
|
||||
if(section.type=='html'){
|
||||
return sec;
|
||||
}else{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
generateSideBySideImages(base64Images: string[]) {
|
||||
const sectionStart = `<section style="color: rgb(0, 0, 0);font-family: 'Microsoft YaHei';font-size: medium;font-style: normal;font-variant-ligatures: normal;font-variant-caps: normal;font-weight: 400;letter-spacing: normal;orphans: 2;text-align: start;text-indent: 0px;text-transform: none;widows: 2;word-spacing: 0px;-webkit-text-stroke-width: 0px;white-space: normal;text-decoration-thickness: initial;text-decoration-style: initial;text-decoration-color: initial;margin-bottom: 18px;display: flex;justify-content: space-between;flex-shrink: 0;">`;
|
||||
|
||||
const sectionEnd = `</section>`;
|
||||
|
||||
const imageSections = base64Images.map((base64, index) => {
|
||||
return `
|
||||
<section key="${index}" style="font-family: 'PingFang SC', system-ui, -apple-system, BlinkMacSystemFont, 'Helvetica Neue', 'Hiragino Sans GB', 'Microsoft YaHei UI', 'Microsoft YaHei', Arial, sans-serif;display: inline-block;flex: 1 1 0%;padding: 0px 4px;">
|
||||
<section>
|
||||
<section style="text-align: center;margin-bottom: 12px;" nodeleaf="">
|
||||
<img alt="Image" class="rich_pages wxw-img"
|
||||
style="display: inline-block; max-width: 100%; height: auto !important; border-radius: 12px; visibility: visible !important; width: 330.5px !important;"
|
||||
src="${base64}" crossorigin="anonymous">
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
return sectionStart + imageSections + sectionEnd;
|
||||
}
|
||||
|
||||
async images2html(imgs: string | string[]): Promise<string | null> {
|
||||
if (Array.isArray(imgs)) {
|
||||
let ximgs = [];
|
||||
for (let x of imgs) {
|
||||
if (x.startsWith('http')) {
|
||||
ximgs.push(x)
|
||||
} else {
|
||||
let c = await this.image_to_img(x, true);
|
||||
ximgs.push(c)
|
||||
}
|
||||
}
|
||||
ximgs = ximgs.filter((x): x is string => x !== undefined && x !== null);
|
||||
if (ximgs.length > 0) {
|
||||
let html = this.generateSideBySideImages(ximgs);
|
||||
return html
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} else if (typeof imgs == 'string') {
|
||||
let obsidianImgs = imgs.match(/!?\[\[([^|\]]+\.(?:png|jpg|jpeg|gif|webp|bmp|svg))(?:\|.*?)?]]/g) || [];
|
||||
|
||||
let markdownImgs = Array.from(
|
||||
imgs.matchAll(/!\[\]\((https?:\/\/[^\s)]+)\)/g),
|
||||
m => m[1]
|
||||
);
|
||||
|
||||
// 合并两种图片链接
|
||||
let ximgs = [...obsidianImgs, ...markdownImgs];
|
||||
return this.images2html(ximgs);
|
||||
}else{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue