From 58d58d13ce85d560168131b203c2fc447d4a6cd0 Mon Sep 17 00:00:00 2001 From: ZigHolding Date: Sun, 4 Jan 2026 21:20:38 +0800 Subject: [PATCH] v0.6.3 --- manifest.json | 2 +- src/commands.ts | 258 ++++++++++++-------- src/easyapi/easyapi.ts | 9 +- src/easyapi/editor.ts | 524 ++++++++++++++++++++++++----------------- src/easyapi/file.ts | 64 ++++- src/easyapi/web.ts | 199 ++++++++++++++++ src/strings.ts | 8 + src/wxmp.ts | 33 +-- 8 files changed, 767 insertions(+), 330 deletions(-) create mode 100644 src/easyapi/web.ts diff --git a/manifest.json b/manifest.json index a2918a1..3a3457d 100644 --- a/manifest.json +++ b/manifest.json @@ -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", diff --git a/src/commands.ts b/src/commands.ts index 618f869..cd8505b 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -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 = [ + +const commandBuilders: Array = [ cmd_export_wxmp, - + ]; -const commandBuildersDesktop:Array = [ +const commandBuildersDesktop: Array = [ 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)); }); diff --git a/src/easyapi/easyapi.ts b/src/easyapi/easyapi.ts index 7bcfb32..8faeb18 100644 --- a/src/easyapi/easyapi.ts +++ b/src/easyapi/easyapi.ts @@ -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; } diff --git a/src/easyapi/editor.ts b/src/easyapi/editor.ts index c7d7b58..6979eb3 100644 --- a/src/easyapi/editor.ts +++ b/src/easyapi/editor.ts @@ -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,obj:object){ - for(let k of keys){ - if(!(k in obj)){ + + keys_in(keys: Array, 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; + + // 找下一个同级或更高的 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){ + + 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 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,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 = [] + 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 + } } diff --git a/src/easyapi/web.ts b/src/easyapi/web.ts new file mode 100644 index 0000000..98b0418 --- /dev/null +++ b/src/easyapi/web.ts @@ -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 + */ + async request(urlStr: string, options: WebRequestOptions = {}): Promise { + // 解析 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 + */ + async get(urlStr: string, options: Omit = {}): Promise { + return this.request(urlStr, { ...options, method: 'GET' }); + } + + /** + * POST 请求 + * @param urlStr 请求 URL + * @param body 请求体(可选) + * @param options 请求选项(可选) + * @returns Promise + */ + async post(urlStr: string, body?: any, options: Omit = {}): Promise { + return this.request(urlStr, { ...options, method: 'POST', body: body }); + } + + /** + * PUT 请求 + * @param urlStr 请求 URL + * @param body 请求体(可选) + * @param options 请求选项(可选) + * @returns Promise + */ + async put(urlStr: string, body?: any, options: Omit = {}): Promise { + return this.request(urlStr, { ...options, method: 'PUT', body: body }); + } + + /** + * DELETE 请求 + * @param urlStr 请求 URL + * @param options 请求选项(可选) + * @returns Promise + */ + async delete(urlStr: string, options: Omit = {}): Promise { + return this.request(urlStr, { ...options, method: 'DELETE' }); + } + + /** + * PATCH 请求 + * @param urlStr 请求 URL + * @param body 请求体(可选) + * @param options 请求选项(可选) + * @returns Promise + */ + async patch(urlStr: string, body?: any, options: Omit = {}): Promise { + return this.request(urlStr, { ...options, method: 'PATCH', body: body }); + } +} + diff --git a/src/strings.ts b/src/strings.ts index ce3aeb6..0dae822 100644 --- a/src/strings.ts +++ b/src/strings.ts @@ -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 '输入文件夹路径' diff --git a/src/wxmp.ts b/src/wxmp.ts index 3016ba4..124c62b 100644 --- a/src/wxmp.ts +++ b/src/wxmp.ts @@ -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]); } // [[格式化图片链接]]