mirror of
https://github.com/zigholding/obsidian-notesync-plugin.git
synced 2026-07-22 12:20:23 +00:00
Compare commits
12 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1539fb2eb8 | ||
|
|
4ba68e4f24 | ||
|
|
58d58d13ce | ||
|
|
56f7156e2e | ||
|
|
66385c0201 | ||
|
|
6673fe5d50 | ||
|
|
a4a820ff60 | ||
|
|
09cb452eb7 | ||
|
|
af393a6ba4 | ||
|
|
36cc29a2dc | ||
|
|
d1de7bea54 | ||
|
|
d8fefd2af8 |
21 changed files with 2954 additions and 105 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -23,4 +23,5 @@ data.json
|
|||
.DS_Store
|
||||
|
||||
package-lock.json
|
||||
otherplugins
|
||||
otherplugins
|
||||
update.sh
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
{
|
||||
"strict_mode": false,
|
||||
"vaultDir": "D:\\iLanix\\Obsidian"
|
||||
"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 公众号行内代码样式"
|
||||
}
|
||||
16
main.ts
16
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -124,10 +131,11 @@ export default class NoteSyncPlugin extends Plugin {
|
|||
let assets = fm[this.yaml]?.Assets
|
||||
|
||||
if(fm[this.yaml]?.UseGitLink && assets){
|
||||
|
||||
ctx = ctx.replace(
|
||||
/\!\[\[(.*?)\]\]/g,
|
||||
(match, filename) => {
|
||||
return ``;
|
||||
return `})`;
|
||||
})
|
||||
}
|
||||
await this.fsEditor.fs.writeFile(
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "note-sync",
|
||||
"name": "Note Sync",
|
||||
"version": "0.5.3",
|
||||
"version": "0.6.3",
|
||||
"minAppVersion": "1.8.3",
|
||||
"description": "Sync notes or plugins between vaults.",
|
||||
"author": "ZigHolding",
|
||||
|
|
|
|||
|
|
@ -20,5 +20,9 @@
|
|||
"obsidian": "latest",
|
||||
"tslib": "2.4.0",
|
||||
"typescript": "4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"marked": "^9.1.6",
|
||||
"highlight.js": "^11.9.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
269
src/commands.ts
269
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,174 +36,176 @@ 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 cmd_download_git_repo = (plugin:NoteSyncPlugin) => ({
|
||||
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 repo = await plugin.dialog_suggest(repos, repos);
|
||||
if (!repo) { return }
|
||||
|
||||
let match = repo.match(/^https?:\/\/(.*)\.com\/([^/]*)\/([^/]*)\/tree\/([^/]*)\/?(.*)$/);
|
||||
if(!match){return}
|
||||
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') {
|
||||
|
||||
async function list_files_of_path(repoOwner: string, repoName: string, path: string, branch = 'master') {
|
||||
let url;
|
||||
if(SOURCE=='github'){
|
||||
if (SOURCE == 'github') {
|
||||
url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/${path}?ref=${branch}`;
|
||||
}else{
|
||||
} 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)){
|
||||
if (!Array.isArray(req)) {
|
||||
req = [req]
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
async function download_file(url:string,folder_path:string,file_name:string){
|
||||
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('.')){
|
||||
if (folder_path.startsWith('.')) {
|
||||
let flag = (plugin.app.vault as any).exists(folder_path);
|
||||
if(!flag){
|
||||
if (!flag) {
|
||||
await plugin.app.vault.createFolder(folder_path)
|
||||
}
|
||||
flag = (plugin.app.vault as any).exists(tfile_path);
|
||||
if(flag){
|
||||
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)
|
||||
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{
|
||||
} else {
|
||||
let folder = plugin.app.vault.getFolderByPath(folder_path)
|
||||
if(!folder){
|
||||
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)
|
||||
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){
|
||||
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
|
||||
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'){
|
||||
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)
|
||||
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'){
|
||||
} 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'){
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -210,23 +215,111 @@ const cmd_download_git_repo = (plugin:NoteSyncPlugin) => ({
|
|||
}
|
||||
});
|
||||
|
||||
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 commandBuilders:Array<Function> = [
|
||||
|
||||
];
|
||||
|
||||
const commandBuildersDesktop:Array<Function> = [
|
||||
const commandBuildersDesktop: Array<Function> = [
|
||||
cmd_export_current_note,
|
||||
cmd_set_vexporter,
|
||||
cmd_export_plugin,
|
||||
cmd_download_git_repo
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -94,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;
|
||||
|
|
@ -179,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;
|
||||
|
|
@ -243,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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -264,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)){
|
||||
|
|
@ -311,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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,20 @@ export interface MySettings {
|
|||
strict_mode: boolean;
|
||||
vaultDir:string;
|
||||
git_repo:string;
|
||||
wxmp_config:string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: MySettings = {
|
||||
strict_mode:false,
|
||||
vaultDir: '',
|
||||
git_repo: 'https://github.com/zigholding/ObsidianZ/tree/master\nhttps://gitee.com/zigholding/ObsidianZ/tree/master'
|
||||
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 {
|
||||
|
|
@ -71,5 +79,14 @@ export class NoteSyncSettingTab extends PluginSettingTab {
|
|||
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();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,22 @@ export class Strings{
|
|||
}
|
||||
}
|
||||
|
||||
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 '输入文件夹路径'
|
||||
|
|
@ -97,6 +113,15 @@ export class Strings{
|
|||
}
|
||||
}
|
||||
|
||||
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