Compare commits

..

No commits in common. "main" and "0.5.3" have entirely different histories.
main ... 0.5.3

21 changed files with 104 additions and 2953 deletions

3
.gitignore vendored
View file

@ -23,5 +23,4 @@ data.json
.DS_Store
package-lock.json
otherplugins
update.sh
otherplugins

View file

@ -1,6 +1,4 @@
{
"strict_mode": false,
"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 公众号行内代码样式"
"vaultDir": "D:\\iLanix\\Obsidian"
}

16
main.ts
View file

@ -1,7 +1,6 @@
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'
@ -9,17 +8,14 @@ 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;
easyapi: EasyAPI;
wxmp: Wxmp;
dialog_suggest: Function
dialog_prompt: Function
async onload() {
@ -35,12 +31,9 @@ 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);
@ -66,7 +59,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.settings.strict_mode);
this.fsEditor.sync_tfolder(file,dst,'mtime',true,false);
}
});
});
@ -131,11 +124,10 @@ 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 `![](./${assets}/${filename.replace(/ /g,'%20')})`;
return `![](./${assets}/${filename})`;
})
}
await this.fsEditor.fs.writeFile(

View file

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

View file

@ -20,9 +20,5 @@
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
},
"dependencies": {
"marked": "^9.1.6",
"highlight.js": "^11.9.0"
}
}

View file

@ -1,31 +1,28 @@
import {
Notice, TFile,
TFolder
import {
Notice, TFile
} 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';
@ -36,176 +33,174 @@ 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) {
console.log(`Copy ${item} to ${target}`, 5000)
let flag = plugin.fsEditor.copy_file(src,dst,'overwrite');
if(flag){
new Notice(`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)
}
}
@ -215,111 +210,23 @@ 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_export_wxmp,
cmd_export_as_single_note
cmd_download_git_repo
];
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

@ -1,40 +0,0 @@
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);
}
}
}
}
}

View file

@ -1,106 +0,0 @@
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;
}
}

View file

@ -1,473 +0,0 @@
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;
// 找下一个同级或更高的 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) {
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;
}
}

View file

@ -1,188 +0,0 @@
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 []
}
}
}

View file

@ -1,170 +0,0 @@
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
}
}

View file

@ -1,139 +0,0 @@
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
}
}

View file

@ -1,143 +0,0 @@
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 0N
* @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 - 0N-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
}
}

View file

@ -1,155 +0,0 @@
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_filetarget>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 []
}
}
}

View file

@ -1,353 +0,0 @@
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-601使
* @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;
}
}

View file

@ -1,38 +0,0 @@
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;
}
}

View file

@ -1,199 +0,0 @@
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

@ -94,11 +94,8 @@ export class FsEditor{
}
abspath(tfile:TFile|TFolder|string){
if(typeof tfile == 'string'){
return (this.root+'/'+tfile).replace(/\\/g,'/');
}
else if(tfile){
abspath(tfile:TFile|TFolder){
if(tfile){
return (this.root+'/'+tfile.path).replace(/\\/g,'/');
}else{
return null;
@ -182,20 +179,17 @@ 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;
@ -249,28 +243,15 @@ export class FsEditor{
}
}
async delete_file_or_dir(path:string){
delete_file_or_dir(path:string){
if(this.isfile(path)){
if (await this.plugin.dialog_suggest(['❌','✔️'],[false,true],path)){
console.log(`Delete file: ${path}`)
this.fs.unlinkSync(path)
}
this.fs.unlinkSync(path)
}else if(this.isdir(path)){
let items = this.list_dir(path,true)
if (await this.plugin.dialog_suggest(['❌','✔️'],[false,true],path)){
console.log(`Delete folder: ${path}`)
this.fs.rmdirSync(path)
}else{
return;
}
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)
}
this.delete_file_or_dir(item)
}
this.fs.rmdirSync(path)
}
}
@ -283,7 +264,7 @@ export class FsEditor{
let asrc = src+'/'+item
if(this.isfile(adst)){
if(!this.isfile(asrc)){
this.delete_file_or_dir(adst);
this.fs.unlinkSync(adst)
}
}else if(this.isdir(adst)){
if(!this.isdir(asrc)){
@ -330,25 +311,4 @@ 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;
}
}
}

View file

@ -8,20 +8,12 @@ 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',
wxmp_config: `
h1: ob h1
h2: ob h2
h3: ob hx
p code: ob
li code: ob
`.trim()
git_repo: 'https://github.com/zigholding/ObsidianZ/tree/master\nhttps://gitee.com/zigholding/ObsidianZ/tree/master'
}
export class NoteSyncSettingTab extends PluginSettingTab {
@ -79,14 +71,5 @@ 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();
}));
}
}

View file

@ -42,22 +42,6 @@ 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 '输入文件夹路径'
@ -113,15 +97,6 @@ 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';

View file

@ -1,655 +0,0 @@
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}&amp;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}&amp;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}&amp;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, &quot;Courier New&quot;, 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, &quot;Courier New&quot;, 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, &quot;Courier New&quot;, 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, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, 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, &quot;Helvetica Neue&quot;, &quot;PingFang SC&quot;, &quot;Hiragino Sans GB&quot;, &quot;Microsoft YaHei UI&quot;, &quot;Microsoft YaHei&quot;, 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;
}
}
}