This commit is contained in:
ZigHolding 2026-01-04 21:20:38 +08:00
parent 56f7156e2e
commit 58d58d13ce
8 changed files with 767 additions and 330 deletions

View file

@ -1,7 +1,7 @@
{
"id": "note-sync",
"name": "Note Sync",
"version": "0.6.2",
"version": "0.6.3",
"minAppVersion": "1.8.3",
"description": "Sync notes or plugins between vaults.",
"author": "ZigHolding",

View file

@ -1,30 +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',
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',
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';
@ -35,176 +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',
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){
console.log(`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',
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)
}
}
@ -214,42 +215,103 @@ const cmd_download_git_repo = (plugin:NoteSyncPlugin) => ({
}
});
const cmd_export_wxmp = (plugin:NoteSyncPlugin) => ({
const cmd_export_wxmp = (plugin: NoteSyncPlugin) => ({
id: 'cmd_export_wxmp',
name: plugin.strings.cmd_export_wxmp,
icon:'aperture',
icon: 'aperture',
hotkeys: [{ modifiers: ['Alt', 'Shift'], key: 'P' }],
callback: async () => {
if(!plugin.easyapi.cfile){return}
if (!plugin.easyapi.cfile) { return }
let ctx = plugin.easyapi.ceditor.getSelection();
if(!ctx){
if (!ctx) {
plugin.wxmp.tfile_to_wxmp(plugin.easyapi.cfile);
}else{
} else {
await plugin.wxmp.selection_to_wxmp();
}
new Notice(`${plugin.strings.cmd_export_wxmp}: OK`,5000)
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;
// 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}
}
let 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);
}
// 3. 打开「另存为」对话框
const result = await dialog.showSaveDialog({
title: plugin.strings.cmd_export_as_single_note,
defaultPath: (cfile.parent?.name || cfile.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> = [
const commandBuilders: Array<Function> = [
cmd_export_wxmp,
];
const commandBuildersDesktop:Array<Function> = [
const commandBuildersDesktop: Array<Function> = [
cmd_export_current_note,
cmd_set_vexporter,
cmd_export_plugin,
cmd_download_git_repo,
cmd_export_wxmp
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));
});

View file

@ -10,6 +10,7 @@ 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;
@ -21,6 +22,7 @@ export class EasyAPI {
waiter: Waiter
tpl: Templater
time: Time
web: Web
constructor(app: App) {
this.app = app;
@ -31,7 +33,8 @@ export class EasyAPI {
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.time = new Time(app,this);
this.web = new Web(app);
}
get_plugin(name:string){
@ -50,6 +53,10 @@ export class EasyAPI {
return this.get_plugin('note-sync');
}
get wv(){
return this.get_plugin('webview-llm');
}
get qa(){
return this.get_plugin('quickadd')?.api;
}

View file

@ -1,50 +1,50 @@
import { App, View, WorkspaceLeaf,TFile } from 'obsidian';
import { App, View, WorkspaceLeaf, TFile } from 'obsidian';
import {EasyAPI} from 'src/easyapi/easyapi'
import { EasyAPI } from 'src/easyapi/easyapi'
export class EasyEditor {
yamljs = require('js-yaml');
app: App;
ea: EasyAPI;
constructor(app: App, api:EasyAPI) {
constructor(app: App, api: EasyAPI) {
this.app = app;
this.ea = api;
}
cn2num(chinese:string) {
cn2num(chinese: string) {
let v = parseFloat(chinese);
if(!Number.isNaN(v)){return v}
if (!Number.isNaN(v)) { return v }
chinese = chinese.trim()
const cnNumbers:{[key:string]:number} = {
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;
@ -52,14 +52,14 @@ export class EasyEditor {
temp = 0;
continue;
}
if(!(c in cnNumbers)){
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; // 特殊处理"十"前无数字的情况
@ -79,33 +79,33 @@ export class EasyEditor {
}
}
}
// 处理最后的临时值
integer_total += temp;
return sign * (integer_total + decimal_total);
}
slice_by_position(ctx:string,pos:any){
if(pos.position){
slice_by_position(ctx: string, pos: any) {
if (pos.position) {
pos = pos.position
}
return ctx.slice(pos.start.offset,pos.end.offset);
return ctx.slice(pos.start.offset, pos.end.offset);
}
parse_list_regx(aline:string,regx:RegExp,field:{[key:string]:number}={}){
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){
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){
parse_list_dataview(aline: string, src = '_src_') {
let res: { [key: string]: string } = {};
if (src) {
res[src] = aline;
}
let regex = /[($$](.*?)::(.*?)[)$$]/g;
@ -117,203 +117,295 @@ export class EasyEditor {
}
return res;
}
keys_in(keys:Array<string>,obj:object){
for(let k of keys){
if(!(k in obj)){
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){
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 []}
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());
}
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;
}
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){
async get_selection(cancel_selection = false) {
let editor = (this.app.workspace as any).getActiveFileView()?.editor;
if(editor){
if (editor) {
let sel = editor.getSelection();
if(cancel_selection){
let cursor = editor.getCursor(); 
if (cancel_selection) {
let cursor = editor.getCursor();
await editor.setSelection(cursor, cursor);
}
return sel;
}else{
return '';
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){
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 section = 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);
})[idx]
if(section){
let c = ctx.slice(
section.position.start.offset,
section.position.end.offset
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(as_simple){
return c.slice(4+ctype.length,c.length-4)
}else{
let res = {
code:c,
section:section,
ctx:ctx
}
return res;
}
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
};
}
}
async get_heading_section(tfile:TFile,heading:string,idx=0, with_heading=true){
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;
// 找下一个同级或更高的 headinglevel <= 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){
if (!dvmeta?.headings) {
return '';
}
let section = dvmeta?.headings?.filter(x=>x.heading==heading)[idx]
if(section){
let idx = dvmeta.headings.indexOf(section)+1;
while(idx<dvmeta.headings.length){
let csec = dvmeta.headings[idx];
if(csec.level<=section.level){
break
}
idx = idx+1;
}
if(idx<dvmeta.headings.length){
let csec = dvmeta.headings[idx];
let c = ctx.slice(
with_heading?section.position.start.offset : section.position.end.offset,
csec.position.start.offset
);
return c;
}else{
let c = ctx.slice(
with_heading?section.position.start.offset : section.position.end.offset,
);
return c;
}
// 找到所有匹配开头的 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(){
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){return null}
if(cursor){
let section = cache?.sections?.filter(
x=>{return x.position.start.line<=cursor.line && x.position.end.line>=cursor.line}
)[0]
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);
if(!section){return ''}
return ctx.slice(
ctx = ctx.slice(
section.position.start.offset,
section.position.end.offset
)
}else{
return null;
}
}
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
}
}
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 {
get_obj_value(data: any, key: string): any {
try {
// key 直接在对象中
if(data[key]){
if (data[key]) {
return data[key]
}
@ -321,59 +413,61 @@ export class EasyEditor {
let left = keys[0];
let right = keys.slice(1).join('.');
if(left){
if (left) {
// key[3],key[-3]
let items = left.match(/^(.*?)(\[-?\d+\])?$/)
if(!items){return null}
if(items[1]){
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){
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))
} 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'){
} else if (typeof data == 'object') {
let keys = Object.keys(data).sort();
if(keys.length==0){
if (keys.length == 0) {
data = null;
}else{
let i = parseInt(items[2].slice(1,items[2].length-1))
} 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){
if (!right) {
return data;
}else{
return this.get_obj_value(data,right);
} else {
return this.get_obj_value(data, right);
}
} catch (error) {
return null;
}
}
} catch (error) {
return null;
}
}
// LINE 存在时在其之后插件,不存在在末尾
async insert_after_line(tfile:TFile,aline:string,LINE:string, tail=true,suffix='\n\n'){
if(!tfile){return false}
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){
if (idx == -1 && tail) {
ctx = `${ctx}${suffix}${aline}`
}else{
ctx = `${ctx.slice(0,idx+LINE.length)}\n${aline}${ctx.slice(idx+LINE.length)}`
} else {
ctx = `${ctx.slice(0, idx + LINE.length)}\n${aline}${ctx.slice(idx + LINE.length)}`
}
await this.ea.app.vault.modify(tfile,ctx)
await this.ea.app.vault.modify(tfile, ctx)
return true;
}
}

View file

@ -14,8 +14,11 @@ export class File {
this.api = api;
}
get_tfile(path:string|TFile,only_first=true){
get_tfile(path:string|TFile|null,only_first=true){
try{
if(!path){
return null;
}
if(path instanceof TFile){
return path;
}
@ -57,6 +60,11 @@ export class File {
}
}
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 = [];
@ -73,6 +81,30 @@ export class File {
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 空格
@ -102,5 +134,35 @@ export class File {
});
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
}
}

199
src/easyapi/web.ts Normal file
View 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 });
}
}

View file

@ -50,6 +50,14 @@ export class Strings{
}
}
get cmd_export_as_single_note(){
if(this.language=='zh'){
return '导出目录文件';
}else{
return 'Export folder';
}
}
get prompt_path_of_folder(){
if(this.language=='zh'){
return '输入文件夹路径'

View file

@ -56,8 +56,10 @@ export class Wxmp {
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;
}
@ -127,27 +129,27 @@ export class Wxmp {
console.warn(`图片 ${src} 转换失败:`, e);
}
}
return new XMLSerializer().serializeToString(doc.body);
}
async section_to_wxmp(section:any,sec:string){
async section_to_wxmp(section:any,sec:string,ctx:string){
if (section.type == 'yaml') {
return null;
}
let rhtml:any = null;
for(let k in this.ctx_map){
let ctx_map = this.ctx_map;
for(let k in ctx_map){
if(k.startsWith('section@')){
let tpl = this.ctx_map[k];
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},[0]
tpl, true,{section:section,sec:sec,ctx:ctx},[0]
);
if (rendered.length > 0 && rendered[0].trim() != '') {
rendered = rendered.filter(x=>x);
if (rendered && rendered.length > 0 && rendered[0].trim() != '') {
rhtml = rendered[0];
}
}
@ -191,7 +193,9 @@ export class Wxmp {
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')
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;
}
@ -201,7 +205,7 @@ export class Wxmp {
}
if(sec==''){continue}
let rhtml = await this.section_to_wxmp(section,sec);
let rhtml = await this.section_to_wxmp(section,sec,sec);
if(!rhtml){continue}
if(Array.isArray(rhtml)){
@ -222,7 +226,7 @@ export class Wxmp {
continue
}
let sec = this.plugin.easyapi.editor.slice_by_position(ctx, section.position);
let rhtml = await this.section_to_wxmp(section,sec);
let rhtml = await this.section_to_wxmp(section,sec,ctx);
if(!rhtml){continue}
if(Array.isArray(rhtml)){
@ -250,9 +254,10 @@ export class Wxmp {
rhtml = this.html_replace_code(rhtml)
// 替换标题
for (let k in this.ctx_map) {
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, this.ctx_map[k]);
rhtml = await this.set_tag_with_tpl(rhtml, k, ctx_map[k]);
}
// [[格式化图片链接]]