mirror of
https://github.com/zigholding/obsidian-notesync-plugin.git
synced 2026-07-22 05:43:30 +00:00
wxmp
This commit is contained in:
parent
d8fefd2af8
commit
d1de7bea54
19 changed files with 2145 additions and 8 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -23,4 +23,5 @@ data.json
|
|||
.DS_Store
|
||||
|
||||
package-lock.json
|
||||
otherplugins
|
||||
otherplugins
|
||||
update.sh
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"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"
|
||||
"git_repo": "https://github.com/zigholding/ObsidianZ/tree/master\nhttps://gitee.com/zigholding/ObsidianZ/tree/master",
|
||||
"wxmp_config": ""
|
||||
}
|
||||
11
main.ts
11
main.ts
|
|
@ -1,6 +1,7 @@
|
|||
import {Notice, Plugin, TFile, TFolder } from 'obsidian';
|
||||
|
||||
import { FsEditor } from 'src/fseditor';
|
||||
import { Wxmp } from 'src/wxmp';
|
||||
import { Strings } from 'src/strings';
|
||||
import {MySettings,NoteSyncSettingTab,DEFAULT_SETTINGS} from 'src/setting'
|
||||
|
||||
|
|
@ -8,14 +9,17 @@ import { addCommands } from 'src/commands';
|
|||
|
||||
import {dialog_suggest} from 'src/gui/inputSuggester'
|
||||
import {dialog_prompt} from 'src/gui/inputPrompt'
|
||||
import {EasyAPI} from 'src/easyapi/easyapi'
|
||||
|
||||
export default class NoteSyncPlugin extends Plugin {
|
||||
strings : Strings;
|
||||
settings: MySettings;
|
||||
fsEditor : FsEditor;
|
||||
yaml: string;
|
||||
dialog_suggest: Function
|
||||
dialog_prompt: Function
|
||||
dialog_suggest: Function;
|
||||
dialog_prompt: Function;
|
||||
easyapi: EasyAPI;
|
||||
wxmp: Wxmp;
|
||||
|
||||
|
||||
async onload() {
|
||||
|
|
@ -31,9 +35,12 @@ export default class NoteSyncPlugin extends Plugin {
|
|||
async _onload_() {
|
||||
this.yaml = 'note-sync'
|
||||
this.strings = new Strings();
|
||||
this.easyapi = new EasyAPI(this.app);
|
||||
|
||||
|
||||
await this.loadSettings();
|
||||
this.fsEditor = new FsEditor(this);
|
||||
this.wxmp = new Wxmp(this);
|
||||
// This adds a settings tab so the user can configure various aspects of the plugin
|
||||
this.addSettingTab(new NoteSyncSettingTab(this.app, this));
|
||||
addCommands(this);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "note-sync",
|
||||
"name": "Note Sync",
|
||||
"version": "0.5.3",
|
||||
"version": "0.6.0",
|
||||
"minAppVersion": "1.8.3",
|
||||
"description": "Sync notes or plugins between vaults.",
|
||||
"author": "ZigHolding",
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { it } from 'node:test';
|
|||
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);
|
||||
|
|
@ -17,6 +18,7 @@ const cmd_export_current_note = (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}
|
||||
|
|
@ -43,6 +45,7 @@ const cmd_set_vexporter = (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);
|
||||
|
|
@ -112,6 +115,7 @@ const cmd_export_plugin = (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);
|
||||
|
|
@ -210,8 +214,24 @@ 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 = await plugin.app.vault.read(plugin.easyapi.cfile);
|
||||
let html = plugin.wxmp.marked.marked(ctx);
|
||||
let rhtml = await plugin.wxmp.html_to_wxmp(html);
|
||||
plugin.wxmp.copy_as_html(rhtml);
|
||||
new Notice(`${plugin.strings.cmd_export_wxmp}: OK`,5000)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const commandBuilders:Array<Function> = [
|
||||
cmd_export_wxmp,
|
||||
|
||||
];
|
||||
|
||||
|
|
@ -219,7 +239,8 @@ const commandBuildersDesktop:Array<Function> = [
|
|||
cmd_export_current_note,
|
||||
cmd_set_vexporter,
|
||||
cmd_export_plugin,
|
||||
cmd_download_git_repo
|
||||
cmd_download_git_repo,
|
||||
cmd_export_wxmp
|
||||
];
|
||||
|
||||
export function addCommands(plugin:NoteSyncPlugin) {
|
||||
|
|
|
|||
40
src/easyapi/css.ts
Normal file
40
src/easyapi/css.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
|
||||
|
||||
|
||||
import { App, View, WorkspaceLeaf,TFile,TFolder } from 'obsidian';
|
||||
|
||||
import {EasyAPI} from 'src/easyapi/easyapi'
|
||||
|
||||
export class CSS {
|
||||
app: App;
|
||||
ea: EasyAPI;
|
||||
|
||||
constructor(app: App, api:EasyAPI) {
|
||||
this.app = app;
|
||||
this.ea = api;
|
||||
}
|
||||
|
||||
async toogle_note_css(document:any,name:string,refresh=false) {
|
||||
let tfile = this.ea.file.get_tfile(name);
|
||||
if(!tfile){return}
|
||||
|
||||
let link = document.getElementById(tfile.basename);
|
||||
if(link && refresh){
|
||||
link.remove()
|
||||
}else{
|
||||
let css = await this.ea.editor.extract_code_block(tfile,'css')
|
||||
let inner = css.join('\n')
|
||||
if(link){
|
||||
link.innerHTML = inner
|
||||
}else{
|
||||
if(inner!=''){
|
||||
let styleElement = document.createElement('style')
|
||||
styleElement.innerHTML=inner;
|
||||
styleElement.id = tfile.basename;
|
||||
document.head.appendChild(styleElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
99
src/easyapi/easyapi.ts
Normal file
99
src/easyapi/easyapi.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
|
||||
|
||||
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'
|
||||
|
||||
export class EasyAPI {
|
||||
app: App;
|
||||
dialog_suggest: Function
|
||||
dialog_prompt: Function
|
||||
editor: EasyEditor
|
||||
file: File
|
||||
random: Random
|
||||
waiter: Waiter
|
||||
tpl: Templater
|
||||
time: Time
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
379
src/easyapi/editor.ts
Normal file
379
src/easyapi/editor.ts
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
return sel;
|
||||
}else{
|
||||
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 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
|
||||
);
|
||||
|
||||
if(as_simple){
|
||||
return c.slice(4+ctype.length,c.length-4)
|
||||
}else{
|
||||
let res = {
|
||||
code:c,
|
||||
section:section,
|
||||
ctx:ctx
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 '';
|
||||
}
|
||||
let section = dvmeta?.headings?.filter(x=>x.heading==heading)[idx]
|
||||
if(section){
|
||||
let idx = dvmeta.headings.indexOf(section)+1;
|
||||
while(idx<dvmeta.headings.length){
|
||||
let csec = dvmeta.headings[idx];
|
||||
if(csec.level<=section.level){
|
||||
break
|
||||
}
|
||||
idx = idx+1;
|
||||
}
|
||||
if(idx<dvmeta.headings.length){
|
||||
let csec = dvmeta.headings[idx];
|
||||
let c = ctx.slice(
|
||||
with_heading?section.position.start.offset : section.position.end.offset,
|
||||
csec.position.start.offset
|
||||
);
|
||||
return c;
|
||||
}else{
|
||||
let c = ctx.slice(
|
||||
with_heading?section.position.start.offset : section.position.end.offset,
|
||||
);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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]
|
||||
let ctx = await this.app.vault.cachedRead(tfile);
|
||||
if(!section){return ''}
|
||||
return ctx.slice(
|
||||
section.position.start.offset,
|
||||
section.position.end.offset
|
||||
)
|
||||
}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;
|
||||
}
|
||||
}
|
||||
|
||||
106
src/easyapi/file.ts
Normal file
106
src/easyapi/file.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
|
||||
|
||||
|
||||
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,only_first=true){
|
||||
try{
|
||||
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_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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
170
src/easyapi/gui/inputPrompt.ts
Normal file
170
src/easyapi/gui/inputPrompt.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
import type { App} from "obsidian";
|
||||
import { ButtonComponent, Modal, TextComponent } from "obsidian";
|
||||
|
||||
/**
|
||||
* Copy from QuickAdd
|
||||
*/
|
||||
|
||||
export default class InputPrompt extends Modal {
|
||||
public waitForClose: Promise<string>;
|
||||
|
||||
private resolvePromise: (input: string) => void;
|
||||
private rejectPromise: (reason?: unknown) => void;
|
||||
private didSubmit = false;
|
||||
private inputComponent: TextComponent;
|
||||
private input: string;
|
||||
private readonly placeholder: string;
|
||||
|
||||
public static Prompt(
|
||||
app: App,
|
||||
header: string,
|
||||
placeholder?: string,
|
||||
value?: string
|
||||
): Promise<string> {
|
||||
const newPromptModal = new InputPrompt(
|
||||
app,
|
||||
header,
|
||||
placeholder,
|
||||
value
|
||||
);
|
||||
return newPromptModal.waitForClose;
|
||||
}
|
||||
|
||||
protected constructor(
|
||||
app: App,
|
||||
private header: string,
|
||||
placeholder?: string,
|
||||
value?: string
|
||||
) {
|
||||
super(app);
|
||||
this.placeholder = placeholder ?? "";
|
||||
this.input = value ?? "";
|
||||
|
||||
this.waitForClose = new Promise<string>((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.rejectPromise = reject;
|
||||
});
|
||||
|
||||
this.display();
|
||||
this.open();
|
||||
|
||||
}
|
||||
|
||||
private display() {
|
||||
this.containerEl.addClass("quickAddModal", "qaInputPrompt");
|
||||
this.contentEl.empty();
|
||||
this.titleEl.textContent = this.header;
|
||||
|
||||
const mainContentContainer: HTMLDivElement = this.contentEl.createDiv();
|
||||
this.inputComponent = this.createInputField(
|
||||
mainContentContainer,
|
||||
this.placeholder,
|
||||
this.input
|
||||
);
|
||||
this.createButtonBar(mainContentContainer);
|
||||
}
|
||||
|
||||
protected createInputField(
|
||||
container: HTMLElement,
|
||||
placeholder?: string,
|
||||
value?: string
|
||||
) {
|
||||
const textComponent = new TextComponent(container);
|
||||
(textComponent as any).inputEl.classList.add("input-field");
|
||||
(textComponent as any)
|
||||
.setPlaceholder(placeholder ?? "")
|
||||
.setValue(value ?? "")
|
||||
.onChange((value:string) => (this.input = value))
|
||||
.inputEl.addEventListener("keydown", this.submitEnterCallback);
|
||||
|
||||
return textComponent;
|
||||
}
|
||||
|
||||
private createButton(
|
||||
container: HTMLElement,
|
||||
text: string,
|
||||
callback: (evt: MouseEvent) => unknown
|
||||
) {
|
||||
const btn = new ButtonComponent(container);
|
||||
btn.setButtonText(text).onClick(callback);
|
||||
|
||||
return btn;
|
||||
}
|
||||
|
||||
private createButtonBar(mainContentContainer: HTMLDivElement) {
|
||||
const buttonBarContainer: HTMLDivElement =
|
||||
mainContentContainer.createDiv();
|
||||
this.createButton(
|
||||
buttonBarContainer,
|
||||
"Ok",
|
||||
this.submitClickCallback
|
||||
).setCta();
|
||||
this.createButton(
|
||||
buttonBarContainer,
|
||||
"Cancel",
|
||||
this.cancelClickCallback
|
||||
);
|
||||
|
||||
buttonBarContainer.classList.add("button-bar");
|
||||
|
||||
}
|
||||
|
||||
private submitClickCallback = (evt: MouseEvent) => this.submit();
|
||||
private cancelClickCallback = (evt: MouseEvent) => this.cancel();
|
||||
|
||||
private submitEnterCallback = (evt: KeyboardEvent) => {
|
||||
if (!evt.isComposing && evt.key === "Enter") {
|
||||
evt.preventDefault();
|
||||
this.submit();
|
||||
}
|
||||
};
|
||||
|
||||
private submit() {
|
||||
this.didSubmit = true;
|
||||
|
||||
this.close();
|
||||
}
|
||||
|
||||
private cancel() {
|
||||
this.close();
|
||||
}
|
||||
|
||||
private resolveInput() {
|
||||
if (!this.didSubmit) this.rejectPromise("No input given.");
|
||||
else this.resolvePromise(this.input);
|
||||
}
|
||||
|
||||
private removeInputListener() {
|
||||
this.inputComponent.inputEl.removeEventListener(
|
||||
"keydown",
|
||||
this.submitEnterCallback
|
||||
);
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
super.onOpen();
|
||||
|
||||
this.inputComponent.inputEl.focus();
|
||||
this.inputComponent.inputEl.select();
|
||||
}
|
||||
|
||||
onClose() {
|
||||
super.onClose();
|
||||
this.resolveInput();
|
||||
this.removeInputListener();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function dialog_prompt(header: string='Input', placeholder: string='',value:string='') {
|
||||
try{
|
||||
return await InputPrompt.Prompt(
|
||||
this.app,
|
||||
header,
|
||||
placeholder,
|
||||
value
|
||||
)
|
||||
}catch{
|
||||
return null
|
||||
}
|
||||
}
|
||||
139
src/easyapi/gui/inputSuggester.ts
Normal file
139
src/easyapi/gui/inputSuggester.ts
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
import { FuzzySuggestModal } from "obsidian";
|
||||
import type { FuzzyMatch , App} from "obsidian";
|
||||
|
||||
// 添加类型声明
|
||||
interface SuggesterChooser {
|
||||
values: {
|
||||
item: string;
|
||||
match: { score: number; matches: unknown[] };
|
||||
}[];
|
||||
selectedItem: number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 扩展FuzzySuggestModal的类型
|
||||
interface ExtendedFuzzySuggestModal extends FuzzySuggestModal<string> {
|
||||
chooser: SuggesterChooser;
|
||||
}
|
||||
|
||||
type Options = {
|
||||
limit: FuzzySuggestModal<string>["limit"];
|
||||
emptyStateText: FuzzySuggestModal<string>["emptyStateText"];
|
||||
placeholder: Parameters<
|
||||
FuzzySuggestModal<string>["setPlaceholder"]
|
||||
>[0] extends string
|
||||
? string
|
||||
: never;
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy from QuickAdd
|
||||
*/
|
||||
export default class InputSuggester extends FuzzySuggestModal<string> {
|
||||
private resolvePromise: (value: string) => void;
|
||||
private rejectPromise: (reason?: unknown) => void;
|
||||
public promise: Promise<string>;
|
||||
private resolved: boolean;
|
||||
public new_value: boolean;
|
||||
inputEl: any;
|
||||
|
||||
public static Suggest(
|
||||
app: App,
|
||||
displayItems: string[],
|
||||
items: string[],
|
||||
options: Partial<Options> = {},
|
||||
new_value:boolean=false
|
||||
) {
|
||||
const newSuggester = new InputSuggester(
|
||||
app,
|
||||
displayItems,
|
||||
items,
|
||||
options,
|
||||
new_value
|
||||
);
|
||||
return newSuggester.promise;
|
||||
}
|
||||
|
||||
public constructor(
|
||||
app: App,
|
||||
private displayItems: string[],
|
||||
private items: string[],
|
||||
options: Partial<Options> = {},
|
||||
new_value: boolean = false
|
||||
) {
|
||||
super(app);
|
||||
this.new_value = new_value
|
||||
|
||||
this.promise = new Promise<string>((resolve, reject) => {
|
||||
this.resolvePromise = resolve;
|
||||
this.rejectPromise = reject;
|
||||
});
|
||||
|
||||
this.inputEl.addEventListener("keydown", (event: KeyboardEvent) => {
|
||||
if (event.code !== "Tab") {
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用类型断言来访问chooser
|
||||
const self = this as unknown as ExtendedFuzzySuggestModal;
|
||||
const { values, selectedItem } = self.chooser;
|
||||
|
||||
const { value } = this.inputEl;
|
||||
this.inputEl.value = values[selectedItem].item ?? value;
|
||||
});
|
||||
|
||||
if (options.placeholder) this.setPlaceholder(options.placeholder);
|
||||
if (options.limit) this.limit = options.limit;
|
||||
if (options.emptyStateText)
|
||||
this.emptyStateText = options.emptyStateText;
|
||||
|
||||
this.open();
|
||||
}
|
||||
|
||||
getItemText(item: string): string {
|
||||
if (item === this.inputEl.value) return item;
|
||||
|
||||
return this.displayItems[this.items.indexOf(item)];
|
||||
}
|
||||
|
||||
getItems(): string[] {
|
||||
if (this.inputEl.value === ""||!this.new_value) return this.items;
|
||||
return [...this.items,this.inputEl.value];
|
||||
}
|
||||
|
||||
selectSuggestion(
|
||||
value: FuzzyMatch<string>,
|
||||
evt: MouseEvent | KeyboardEvent
|
||||
) {
|
||||
this.resolved = true;
|
||||
super.selectSuggestion(value, evt);
|
||||
}
|
||||
|
||||
onChooseItem(item: string, evt: MouseEvent | KeyboardEvent): void {
|
||||
this.resolved = true;
|
||||
this.resolvePromise(item);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
super.onClose();
|
||||
if (!this.resolved) this.rejectPromise("no input given.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function dialog_suggest(displayItems:Array<string>,items:Array<any>,placeholder='',new_value=false) {
|
||||
try{
|
||||
return await InputSuggester.Suggest(
|
||||
this.app,
|
||||
displayItems,
|
||||
items,
|
||||
{
|
||||
placeholder: placeholder,
|
||||
},
|
||||
new_value
|
||||
)
|
||||
}catch(error){
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
143
src/easyapi/random.ts
Normal file
143
src/easyapi/random.ts
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
import { App, TFile,moment } from "obsidian";
|
||||
import { EasyAPI } from "./easyapi";
|
||||
|
||||
|
||||
export class Random {
|
||||
app:App;
|
||||
ea:EasyAPI;
|
||||
constructor(app:App,ea:EasyAPI){
|
||||
this.app = app;
|
||||
this.ea = ea;
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机获取 M 个值,位于 0~N 之间
|
||||
* @param {number} N - 最大值(不包含)
|
||||
* @param {number} M - 需要获取的随机数数量
|
||||
* @param {boolean} repeat - 是否允许重复值
|
||||
* @returns {number[]} - 包含 M 个随机数的数组
|
||||
*/
|
||||
random_number(N:number, M:number, repeat = false) {
|
||||
if (M <= 0) return [];
|
||||
if (!repeat && M > N) {
|
||||
throw new Error("当不允许重复时,M 不能大于 N");
|
||||
}
|
||||
|
||||
const result = [];
|
||||
|
||||
if (repeat) {
|
||||
// 允许重复值的情况
|
||||
for (let i = 0; i < M; i++) {
|
||||
result.push(Math.floor(Math.random() * N));
|
||||
}
|
||||
} else {
|
||||
// 不允许重复值的情况
|
||||
const numbers = Array.from({ length: N }, (_, i) => i);
|
||||
|
||||
// 使用 Fisher-Yates 洗牌算法
|
||||
for (let i = numbers.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[numbers[i], numbers[j]] = [numbers[j], numbers[i]];
|
||||
}
|
||||
|
||||
// 取前 M 个
|
||||
result.push(...numbers.slice(0, M));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 线性同余生成器 (LCG)
|
||||
lcg(seed:number) {
|
||||
const a = 1664525;
|
||||
const c = 1013904223;
|
||||
const m = Math.pow(2, 32);
|
||||
return (a * seed + c) % m;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于日期生成固定随机数序列
|
||||
* @param {moment} t - 时间对象(使用moment.js)
|
||||
* @param {number} N - 随机数范围上限(0到N-1)
|
||||
* @param {number} M - 需要的随机数数量
|
||||
* @returns {number[]} - 排序后的随机数数组
|
||||
*/
|
||||
random_number_for_date(t:moment.Moment, N:number, M:number) {
|
||||
if (M <= 0) return [];
|
||||
if (M >= N) return Array.from({length: N}, (_, i) => i);
|
||||
|
||||
// 使用年月日作为种子,确保同一天生成相同的序列
|
||||
const dateStr = t.format('YYYY-MM-DD');
|
||||
let seed = 0;
|
||||
for (let i = 0; i < dateStr.length; i++) {
|
||||
seed = (seed << 5) - seed + dateStr.charCodeAt(i);
|
||||
seed |= 0; // 转换为32位整数
|
||||
}
|
||||
|
||||
// 使用Fisher-Yates算法生成随机排列
|
||||
const numbers = Array.from({length: N}, (_, i) => i);
|
||||
let currentSeed = seed;
|
||||
|
||||
for (let i = N - 1; i > 0; i--) {
|
||||
currentSeed = this.lcg(currentSeed);
|
||||
const j = Math.abs(currentSeed) % (i + 1);
|
||||
[numbers[i], numbers[j]] = [numbers[j], numbers[i]];
|
||||
}
|
||||
|
||||
// 取前M个并排序
|
||||
return numbers.slice(0, M).sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
// 根据字符串返回 0~N 之间的整数
|
||||
string_to_random_number(str:string, N:number) {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = (hash << 5) - hash + str.charCodeAt(i);
|
||||
hash |= 0; // 转换为32位整数
|
||||
}
|
||||
return Math.abs(hash) % N;
|
||||
}
|
||||
|
||||
// 从数组中随机获取 N 个元素
|
||||
random_elements(arr:any[], n:number ) {
|
||||
// 复制数组避免修改原数组
|
||||
const shuffled = [...arr];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; // 交换
|
||||
}
|
||||
return shuffled.slice(0, n); // 取前N个
|
||||
}
|
||||
|
||||
_get_tfiles_(filter:null|Function){
|
||||
let tfiles = this.ea.nc.chain.get_all_tfiles();
|
||||
if(filter){
|
||||
tfiles = tfiles.filter((x:TFile)=>filter(x))
|
||||
}
|
||||
return tfiles;
|
||||
}
|
||||
random_notes(n=3,filter=null){
|
||||
let tfiles = this._get_tfiles_(filter);
|
||||
let idx = this.random_number(tfiles.length,n)
|
||||
tfiles = idx.map(i=>tfiles[i])
|
||||
return tfiles
|
||||
}
|
||||
|
||||
random_daily_notes(n=3,before_today=true,filter=null){
|
||||
let t = moment(moment().format('YYYY-MM-DD') )
|
||||
let dnote = this.ea.nc.chain.get_last_daily_note()
|
||||
if(dnote){
|
||||
t = moment(dnote.basename)
|
||||
}
|
||||
let tfiles = this._get_tfiles_(filter);
|
||||
|
||||
if(before_today){
|
||||
tfiles = tfiles.filter(
|
||||
(f:TFile)=>f.stat.ctime<t.unix()*1000
|
||||
)
|
||||
}
|
||||
let idx = this.random_number_for_date(t,tfiles.length,n)
|
||||
tfiles = idx.map(i=>tfiles[i])
|
||||
return tfiles
|
||||
}
|
||||
}
|
||||
155
src/easyapi/templater.ts
Normal file
155
src/easyapi/templater.ts
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import { App, TFile,moment } from "obsidian";
|
||||
import { EasyAPI } from "./easyapi";
|
||||
|
||||
|
||||
export class Templater {
|
||||
app:App;
|
||||
ea:EasyAPI;
|
||||
constructor(app:App,ea:EasyAPI){
|
||||
this.app = app;
|
||||
this.ea = ea;
|
||||
}
|
||||
|
||||
get tpl(){
|
||||
return this.ea.get_plugin('templater-obsidian');
|
||||
}
|
||||
|
||||
get_tp_func(target:string) {
|
||||
|
||||
let items = target.split(".");
|
||||
if(items[0].localeCompare("tp")!=0 || items.length!=3){return undefined;}
|
||||
|
||||
let modules = this.tpl.templater.functions_generator.
|
||||
internal_functions.modules_array.filter(
|
||||
(item:any)=>(item.name.localeCompare(items[1])==0)
|
||||
);
|
||||
if(modules.length==0){return undefined}
|
||||
return modules[0].static_functions.get(items[2]);
|
||||
}
|
||||
|
||||
async get_tp_user_func(target:string) {
|
||||
if(!target.match(/^tp\.user\.\w+$/)){
|
||||
return null
|
||||
}
|
||||
|
||||
let items = target.split(".");
|
||||
if(items[0].localeCompare("tp")!=0 || items[1].localeCompare("user")!=0 || items.length!=3){return undefined;}
|
||||
|
||||
let funcs = await this.tpl.templater.
|
||||
functions_generator.
|
||||
user_functions.
|
||||
user_script_functions.
|
||||
generate_user_script_functions();
|
||||
return funcs.get(items[2])
|
||||
}
|
||||
|
||||
async templater$1(template:string|TFile|null, active_file:TFile|null, target_file:any,extra=null) {
|
||||
let config = {
|
||||
template_file: template,
|
||||
active_file: active_file,
|
||||
target_file: target_file,
|
||||
extra: extra,
|
||||
run_mode: "DynamicProcessor",
|
||||
};
|
||||
|
||||
let {templater} = this.tpl;
|
||||
let functions = await templater.functions_generator.internal_functions.generate_object(config);
|
||||
functions.user = {};
|
||||
let userScriptFunctions = await templater.functions_generator.user_functions.user_script_functions.generate_user_script_functions(config);
|
||||
userScriptFunctions.forEach((value:any,key:any)=>{
|
||||
functions.user[key] = value;
|
||||
}
|
||||
);
|
||||
if (template) {
|
||||
let userSystemFunctions = await templater.functions_generator.user_functions.user_system_functions.generate_system_functions(config);
|
||||
userSystemFunctions.forEach((value:any,key:any)=>{
|
||||
functions.user[key] = value;
|
||||
}
|
||||
);
|
||||
}
|
||||
return async(command:any)=>{
|
||||
return await templater.parser.parse_commands(command, functions);
|
||||
};
|
||||
}
|
||||
|
||||
async extract_templater_block(tfile:TFile|string,reg=/<%\*\s*([\s\S]*?)\s*-?%>/g){
|
||||
let xfile = this.ea.file.get_tfile(tfile);
|
||||
if(xfile){
|
||||
tfile = await this.app.vault.cachedRead(xfile);
|
||||
}
|
||||
if(typeof(tfile)!='string'){return []}
|
||||
|
||||
let blocks = [];
|
||||
let matches;
|
||||
while ((matches = reg.exec(tfile)) !== null) {
|
||||
blocks.push(matches[0].trim());
|
||||
}
|
||||
|
||||
let tpls = await this.ea.editor.extract_code_block(tfile,'js //templater');
|
||||
for(let tpl of tpls){
|
||||
blocks.push(`<%*\n${tpl}\n-%>`)
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
// target_file:target>activate>template
|
||||
async parse_templater(template:string|TFile,extract=true,extra:any=null,idx:number[]|null=null,target='') {
|
||||
let file = this.ea.file.get_tfile(template)
|
||||
if(file){
|
||||
template = file
|
||||
}
|
||||
let blocks:Array<string>;
|
||||
let template_file = null;
|
||||
if(template instanceof TFile){
|
||||
template_file = template
|
||||
if(extract){
|
||||
blocks = await this.extract_templater_block(template);
|
||||
}else{
|
||||
let item = await this.app.vault.cachedRead(template)
|
||||
blocks = [item]
|
||||
}
|
||||
}else{
|
||||
if(extract){
|
||||
blocks = await this.extract_templater_block(template);
|
||||
}else{
|
||||
blocks = [template]
|
||||
}
|
||||
}
|
||||
|
||||
let active_file = this.ea.cfile;
|
||||
let target_file:any = this.ea.file.get_tfile(target);
|
||||
if(!target){
|
||||
if(active_file){
|
||||
target_file = active_file;
|
||||
}else if (file){
|
||||
target_file = file;
|
||||
}else{
|
||||
throw new Error("Target File must be TFile");
|
||||
}
|
||||
}
|
||||
|
||||
let templateFunc = await this.templater$1(template_file,active_file,target_file,extra=extra);
|
||||
if(templateFunc){
|
||||
let res = []
|
||||
if(idx){
|
||||
for(let i of idx){
|
||||
let block = blocks[i];
|
||||
if(block){
|
||||
let item = await templateFunc(block);
|
||||
res.push(item);
|
||||
}else{
|
||||
res.push('');
|
||||
}
|
||||
}
|
||||
}else{
|
||||
for(let block of blocks){
|
||||
let item = await templateFunc(block);
|
||||
res.push(item)
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}else{
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
353
src/easyapi/time.ts
Normal file
353
src/easyapi/time.ts
Normal file
|
|
@ -0,0 +1,353 @@
|
|||
import { moment,App } from 'obsidian';
|
||||
import { Moment } from 'moment';
|
||||
import { EasyAPI } from "./easyapi";
|
||||
|
||||
export class Time{
|
||||
app:App;
|
||||
ea:EasyAPI;
|
||||
constructor(app:App,ea:EasyAPI){
|
||||
this.app = app;
|
||||
this.ea = ea;
|
||||
}
|
||||
|
||||
get today(){
|
||||
let t = moment().format('YYYY-MM-DD');
|
||||
return moment(t)
|
||||
}
|
||||
|
||||
as_date(t:Moment){
|
||||
let xt = t.format('YYYY-MM-DD');
|
||||
return moment(xt)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相对于基准日期的偏移月份的指定日期
|
||||
* @param {number} dayIndex - 日期索引(正数表示第几天,负数表示倒数第几天)
|
||||
* @param {number} monthOffset - 月份偏移量(正数为未来月份,负数为过去月份)
|
||||
* @param {Date|string|moment.Moment} baseDate - 基准日期,默认为当日
|
||||
* @returns {moment.Moment} 计算后的目标日期
|
||||
*/
|
||||
relative_month_day(dayIndex:number, monthOffset:number=0, baseDate=this.today) {
|
||||
// 创建基准日期的moment对象
|
||||
let baseMoment = moment(baseDate).clone();
|
||||
|
||||
// 计算目标月份
|
||||
let targetMoment = baseMoment.clone().add(monthOffset, 'months');
|
||||
|
||||
// 处理日期索引
|
||||
if (dayIndex > 0) {
|
||||
// 正数索引:设置为目标月份的第N天
|
||||
targetMoment.startOf('month').add(dayIndex - 1, 'days');
|
||||
} else {
|
||||
// 负数索引:设置为目标月份的倒数第N天
|
||||
targetMoment.endOf('month').add(dayIndex + 1, 'days');
|
||||
}
|
||||
return this.as_date(targetMoment);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相对于基准日期的偏移周数的指定星期几
|
||||
* @param {number} dayIndex - 星期索引(0-6,0为周日,1为周一,依此类推;或使用负数表示倒数)
|
||||
* @param {number} weekOffset - 周数偏移量(正数为未来周数,负数为过去周数)
|
||||
* @param {Date|string|moment.Moment} baseDate - 基准日期,默认为当日
|
||||
* @returns {moment.Moment} 计算后的目标日期
|
||||
*
|
||||
* @example
|
||||
* relative_week_day(1, 0) // 本周一
|
||||
* relative_week_day(0, -1) // 上周日
|
||||
* relative_week_day(6, 2) // 两周后的周六
|
||||
* relative_week_day(-1, 1) // 下周的倒数第1天(周六)
|
||||
*/
|
||||
relative_week_day(dayIndex:number, weekOffset:number = 0, baseDate = this.today) {
|
||||
// 创建基准日期的moment对象并克隆(避免污染原对象)
|
||||
let baseMoment = moment(baseDate).clone();
|
||||
|
||||
// 处理周偏移:先移动到目标周的开始(周一)
|
||||
let targetMoment = baseMoment.add(weekOffset, 'weeks');
|
||||
|
||||
// 处理星期索引
|
||||
if (dayIndex >= 0) {
|
||||
// 正数索引:直接设置为目标星期(0=周日到6=周六)
|
||||
targetMoment.day(dayIndex);
|
||||
} else {
|
||||
// 负数索引:计算目标周的倒数第N天
|
||||
// 1. 先移动到目标周的周末(周日)
|
||||
// 2. 再向前移动 |dayIndex| - 1 天(例如dayIndex=-1为周六)
|
||||
targetMoment.endOf('week').add(dayIndex + 1, 'days');
|
||||
}
|
||||
|
||||
// 返回标准化后的日期(去除时间部分)
|
||||
return this.as_date(targetMoment);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析中文自然语言日期(新增支持"下个月5号"/"上个月15号"等格式)
|
||||
* @param {string} msg - 包含日期的文本(如"下个月5号开会")
|
||||
* @param {moment.Moment} base - 基准日期,默认为当天
|
||||
* @returns {{date: string, text: string}} 处理后的日期和文本
|
||||
*
|
||||
* @example
|
||||
* parse_date("下个月5号评审") // {date: "2025-07-05", text: "评审"}
|
||||
* parse_date("上个月15号账单") // {date: "2025-05-15", text: "账单"}
|
||||
*/
|
||||
extract_chinese_date(msg:string, base = this.today) {
|
||||
let result:{[key:string]:any} = { date: base.format('YYYY-MM-DD'), text: msg };
|
||||
|
||||
// 1. 处理相对天数(今天/昨天/明天等)
|
||||
let dayKeywords = [
|
||||
{ pattern: /^大前天/, days: -3 },
|
||||
{ pattern: /^前天/, days: -2 },
|
||||
{ pattern: /^昨天/, days: -1 },
|
||||
{ pattern: /^今天/, days: 0 },
|
||||
{ pattern: /^明天/, days: 1 },
|
||||
{ pattern: /^后天/, days: 2 },
|
||||
{ pattern: /^大后天/, days: 3 }
|
||||
];
|
||||
for (let { pattern, days } of dayKeywords) {
|
||||
if (pattern.test(msg)) {
|
||||
result.date = base.clone().add(days, 'days').format('YYYY-MM-DD');
|
||||
result.text = msg.replace(pattern, '').trim();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 处理「下个月X号」或「上个月X号」格式
|
||||
let monthDayMatch = msg.match(/^(上个月|下个月)(\d{1,2})号?/);
|
||||
if (monthDayMatch) {
|
||||
let [fullMatch, direction, day] = monthDayMatch;
|
||||
let monthOffset = direction === '上个月' ? -1 : 1;
|
||||
let targetDate = base.clone().add(monthOffset, 'months').date(parseInt(day));
|
||||
|
||||
if (targetDate.date() !== parseInt(day)) {
|
||||
targetDate.endOf('month');
|
||||
}
|
||||
|
||||
result.date = targetDate.format('YYYY-MM-DD');
|
||||
result.text = msg.slice(fullMatch.length).trim();
|
||||
return result;
|
||||
}
|
||||
|
||||
// 4. 处理周几和下周几
|
||||
let weekMatch = msg.match(/^([上下]([一二三四五六七八九十两]|\d+)周周|上上周|上上星期|上周|上星期|周|星期|下周|下星期|下下周|下下星期)([一二三四五六七日]|[1-7])/);
|
||||
if (weekMatch) {
|
||||
let [fullMatch,weekStr, weekCount,dayChar] = weekMatch;
|
||||
let dayMap:{[key:string]:any} = { '一':1, '二':2, '三':3, '四':4, '五':5, '六':6, '日':7, '七':7};
|
||||
if(weekCount){
|
||||
let nmap:{[key:string]:any} = { '一':1, '二':2, '三':3, '四':4, '五':5, '六':6, '日':7, '七':7,'八':8,'九':9,'两':2};
|
||||
weekCount = nmap[weekCount] || parseInt(weekCount);
|
||||
}
|
||||
let targetDay = dayMap[dayChar] || parseInt(dayChar);
|
||||
let weekOffset = ['周','星期'].contains(weekStr)? 0 :
|
||||
['下周','下星期'].contains(weekStr)? 1 :
|
||||
['下下周','下下星期'].contains(weekStr)? 2 :
|
||||
['上周','上星期'].contains(weekStr)? -1 :
|
||||
['上上周','上上星期'].contains(weekStr)? -2 :
|
||||
(msg.slice(0,1)=='上' ? -weekCount : weekCount);
|
||||
let date = this.relative_week_day(targetDay,weekOffset as number,base);
|
||||
|
||||
if (date.isBefore(base, 'day')) {
|
||||
date.add(1, 'week');
|
||||
}
|
||||
|
||||
result.date = date.format('YYYY-MM-DD');
|
||||
result.text = msg.slice(fullMatch.length).trim();
|
||||
return result;
|
||||
}
|
||||
|
||||
// 5. 处理x月y号格式
|
||||
let absoluteMonthMatch = msg.match(/^(\d{1,2})月(\d{1,2})(?:号|日)?/);
|
||||
if (absoluteMonthMatch) {
|
||||
const [fullMatch, month, day] = absoluteMonthMatch;
|
||||
let date = base.clone().month(parseInt(month) - 1).date(parseInt(day));
|
||||
|
||||
if (date.isBefore(base, 'day')) {
|
||||
date.add(1, 'year');
|
||||
}
|
||||
|
||||
result.date = date.format('YYYY-MM-DD');
|
||||
result.text = msg.slice(fullMatch.length).trim();
|
||||
return result;
|
||||
}
|
||||
if(result.text==msg){
|
||||
result.date = null;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
|
||||
parse_minutes(xt:string) {
|
||||
if(typeof xt == 'number'){return xt;}
|
||||
if(xt.match(/^\d*$/) && parseInt(xt)){return parseInt(xt);}
|
||||
|
||||
let items = xt.match(/^(.{1,2})个半小时$/);
|
||||
if(items){return this.ea.editor.cn2num(items[1])*60+30}
|
||||
|
||||
let compoundMatch = xt.match(/^(.*?)(h|hour|hours|时|小时|个小时)(.*?)(m|min|minute|minutes|分|分钟)?$/i);
|
||||
if (compoundMatch) {
|
||||
let hours = this.ea.editor.cn2num(compoundMatch[1]) || 0;
|
||||
let minutes = this.ea.editor.cn2num(compoundMatch[3]) || 0;
|
||||
return Math.round(hours * 60 + minutes);
|
||||
}
|
||||
// 处理简单格式,仅分钟
|
||||
let simpleMatch = xt.match(/^(.*?)(m|min|minute|minutes|分|分钟)$/i);
|
||||
if (simpleMatch) {
|
||||
let value = this.ea.editor.cn2num(simpleMatch[1]);
|
||||
return Math.round(value);
|
||||
}
|
||||
return Number.NaN;
|
||||
}
|
||||
|
||||
|
||||
parse_time(st:string|Moment, date:Moment|string = this.today,nearest=true) {
|
||||
if(!st){return null}
|
||||
if(moment.isMoment(st)){return st}
|
||||
|
||||
if(moment.isMoment(date)){
|
||||
date = date.format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
let items = st.match(/^(\d{2}):?(\d{2})$/);
|
||||
|
||||
if(items){
|
||||
let t = moment(`${date} ${items[1]}:${items[2]}:00`, "YYYY-MM-DD HH:mm:ss");
|
||||
if(t.isValid()){return t}
|
||||
}
|
||||
|
||||
let cnTimeRegex = /^(早上|上午|凌晨|下午|晚上)?([零一二三四五六七八九十百]+|[\d]+)点(半|([零一二三四五六七八九十]+)分?|([\d]+)分?)?$/;
|
||||
let match = st.match(cnTimeRegex);
|
||||
|
||||
if (match) {
|
||||
let [_, period, hourStr, minuteCnStr] = match;
|
||||
let hour = this.ea.editor.cn2num(hourStr);
|
||||
|
||||
let minute = 0;
|
||||
if (minuteCnStr === '半') {
|
||||
minute = 30;
|
||||
} else if (minuteCnStr) {
|
||||
minute = this.ea.editor.cn2num(minuteCnStr);
|
||||
}
|
||||
|
||||
if (['下午'].includes(period)) {
|
||||
hour = hour >= 12 ? hour : hour + 12;
|
||||
} else if (['晚上'].includes(period)){
|
||||
hour = hour >=5 && hour<12? hour+12:hour;
|
||||
}else if (!period && nearest && hour<=12) {
|
||||
let t = moment();
|
||||
let a = t.hour()*60+t.minutes();
|
||||
let b = hour*60+minute;
|
||||
if(a>b && (a-b)>(b-a+12*60)){
|
||||
hour=hour+12;
|
||||
}
|
||||
}
|
||||
|
||||
hour %= 24;
|
||||
return moment(`${date} ${hour}:${minute}`, "YYYY-MM-DD HH:mm");
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
time_plus_minutes(st:string,xt:string){
|
||||
let t = this.parse_time(st);
|
||||
let n = this.parse_minutes(xt);
|
||||
if(!t || typeof t == 'string' || Number.isNaN(n)){return null}
|
||||
return t.clone().add(xt, 'minutes');
|
||||
}
|
||||
|
||||
generate_start_times(jobs:Array<any>,delta=10, is_today = true,st:string|Moment='06:45',compress=true) {
|
||||
// 从 st 到当前时间
|
||||
let _st = this.parse_time(st)
|
||||
if(!_st){return []}
|
||||
st = _st;
|
||||
let timeList = [];
|
||||
let t = this.parse_time(moment().format('HH:mm'));
|
||||
if (!is_today || true) {
|
||||
t = this.parse_time(moment().format('23:59'));
|
||||
}
|
||||
if(!t){return []}
|
||||
for (let hour = st.hour(); hour <= t.hour(); hour++) {
|
||||
let startMinute = (hour === st.hour()) ? st.minute() : 0;
|
||||
let endMinute = (hour === t.hour()) ? t.minute()+1 : 60;
|
||||
for (let minute = startMinute; minute < endMinute; minute += delta) {
|
||||
let time = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
|
||||
let ct = this.parse_time(time);
|
||||
if(!ct){continue}
|
||||
let flag = true;
|
||||
for (let item of jobs) {
|
||||
if (item.st <= ct && item.et > ct) {
|
||||
flag = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (flag) {
|
||||
timeList.push(time);
|
||||
}
|
||||
}
|
||||
}
|
||||
let et = this.get_max_endt(jobs)?.format('HH:mm');
|
||||
if (et && !timeList.contains(et)) {
|
||||
timeList.push(et)
|
||||
}
|
||||
timeList = timeList.sort((a, b) => -a.localeCompare(b));
|
||||
if(compress){
|
||||
return this.compress_timelist(timeList,delta)
|
||||
}else{
|
||||
return timeList
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
get_max_endt(jobs:Array<any>,st='06:45') {
|
||||
if (jobs.length == 0) {
|
||||
return this.parse_time(st)
|
||||
} else {
|
||||
return moment.unix(
|
||||
Math.max(...jobs.map(x => x.et)) / 1000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
compress_timelist(timeList:Array<string>,delta=5){
|
||||
let compressedList = [];
|
||||
let startRange = null;
|
||||
let prevTime = null;
|
||||
|
||||
for (let i = 0; i < timeList.length; i++) {
|
||||
let currentTime = timeList[i];
|
||||
let currentParsed = this.parse_time(currentTime);
|
||||
if(!currentParsed){continue}
|
||||
|
||||
if (prevTime === null) {
|
||||
startRange = currentTime;
|
||||
} else {
|
||||
let prevParsed = this.parse_time(prevTime);
|
||||
if(!prevParsed){continue}
|
||||
// 检查是否连续(相差5分钟)
|
||||
let diffMinutes = (prevParsed.hour() * 60 + prevParsed.minute()) -
|
||||
(currentParsed.hour() * 60 + currentParsed.minute());
|
||||
|
||||
if (diffMinutes !== delta) {
|
||||
if (startRange !== prevTime) {
|
||||
compressedList.push(startRange)
|
||||
compressedList.push(prevTime);
|
||||
} else {
|
||||
compressedList.push(startRange);
|
||||
}
|
||||
startRange = currentTime;
|
||||
}
|
||||
}
|
||||
|
||||
prevTime = currentTime;
|
||||
}
|
||||
|
||||
// 处理最后一个范围
|
||||
if (startRange !== prevTime) {
|
||||
compressedList.push(startRange)
|
||||
compressedList.push(prevTime);
|
||||
} else if (prevTime !== null) {
|
||||
compressedList.push(prevTime);
|
||||
}
|
||||
|
||||
return compressedList;
|
||||
}
|
||||
}
|
||||
38
src/easyapi/waiter.ts
Normal file
38
src/easyapi/waiter.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { App, TFile,moment } from "obsidian";
|
||||
import { EasyAPI } from "./easyapi";
|
||||
|
||||
|
||||
export class Waiter {
|
||||
app:App;
|
||||
ea:EasyAPI;
|
||||
constructor(app:App,ea:EasyAPI){
|
||||
this.app = app;
|
||||
this.ea = ea;
|
||||
}
|
||||
|
||||
async wait(condition:Function,timeout:number=0){
|
||||
let start = moment();
|
||||
while (!condition()) {
|
||||
let end = moment();
|
||||
if ((start.valueOf()-end.valueOf())/1000 > timeout) {
|
||||
return false;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async wait_for(vfunc:Function,timeout:number=30){
|
||||
let start = moment();
|
||||
let res = await vfunc();
|
||||
while (!res) {
|
||||
let end = moment();
|
||||
if ((start.valueOf()-end.valueOf())/1000 > timeout) {
|
||||
return null;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
res = await vfunc();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
|
@ -330,4 +330,25 @@ export class FsEditor{
|
|||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
list_dir_recursive(path:string,with_folder=false):Array<string>{
|
||||
if(!this.isdir(path)){return []}
|
||||
let res = []
|
||||
let items = this.plugin.fsEditor.list_dir(path,true)
|
||||
for(let item of items){
|
||||
if(this.isfile(item)){
|
||||
res.push(item)
|
||||
}else if(this.isdir(item)){
|
||||
if(with_folder){
|
||||
res.push()
|
||||
}
|
||||
let sitems = this.list_dir_recursive(item,with_folder);
|
||||
for(let i of sitems){
|
||||
res.push(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,20 @@ export interface MySettings {
|
|||
strict_mode: boolean;
|
||||
vaultDir:string;
|
||||
git_repo:string;
|
||||
wxmp_config:string;
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: MySettings = {
|
||||
strict_mode:false,
|
||||
vaultDir: '',
|
||||
git_repo: 'https://github.com/zigholding/ObsidianZ/tree/master\nhttps://gitee.com/zigholding/ObsidianZ/tree/master'
|
||||
git_repo: 'https://github.com/zigholding/ObsidianZ/tree/master\nhttps://gitee.com/zigholding/ObsidianZ/tree/master',
|
||||
wxmp_config: `
|
||||
h1: ob 公众号标题 h1 样式
|
||||
h2: ob 公众号标题 h2 样式
|
||||
h3: ob 公众号标题 hx 样式
|
||||
p code: ob 公众号行内代码样式
|
||||
li code: ob 公众号行内代码样式
|
||||
`.trim()
|
||||
}
|
||||
|
||||
export class NoteSyncSettingTab extends PluginSettingTab {
|
||||
|
|
@ -71,5 +79,14 @@ export class NoteSyncSettingTab extends PluginSettingTab {
|
|||
this.plugin.settings.git_repo = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(this.plugin.strings.setting_wxmp_config)
|
||||
.addTextArea(text => text
|
||||
.setValue(this.plugin.settings.wxmp_config)
|
||||
.onChange(async (value) => {
|
||||
this.plugin.settings.wxmp_config = value;
|
||||
await this.plugin.saveSettings();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,14 @@ export class Strings{
|
|||
}
|
||||
}
|
||||
|
||||
get cmd_export_wxmp(){
|
||||
if(this.language=='zh'){
|
||||
return '导出微信公众号';
|
||||
}else{
|
||||
return 'Export wxmp';
|
||||
}
|
||||
}
|
||||
|
||||
get prompt_path_of_folder(){
|
||||
if(this.language=='zh'){
|
||||
return '输入文件夹路径'
|
||||
|
|
@ -97,6 +105,15 @@ export class Strings{
|
|||
}
|
||||
}
|
||||
|
||||
get setting_wxmp_config(){
|
||||
if(this.language=='zh'){
|
||||
return '微信公众号样式配置';
|
||||
}else{
|
||||
return 'Style config for wxmp';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get item_copy_data_json(){
|
||||
if(this.language=='zh'){
|
||||
return '复制 data.json';
|
||||
|
|
|
|||
430
src/wxmp.ts
Normal file
430
src/wxmp.ts
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
|
||||
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 ctx_map(){
|
||||
let config:{[key:string]:any} = {};
|
||||
for(let line of this.plugin.settings.wxmp_config.split('\n')){
|
||||
let [key, value] = line.split(':');
|
||||
if(!key || !value){continue}
|
||||
config[key] = value;
|
||||
}
|
||||
if(!config['h1']){
|
||||
config['h1'] = this.format_wxmp_h1;
|
||||
}
|
||||
if(!config['h2']){
|
||||
config['h2'] = this.format_wxmp_h2;
|
||||
}
|
||||
if(!config['h3']){
|
||||
config['h3'] = this.format_wxmp_h3;
|
||||
}
|
||||
if(!config['p code']){
|
||||
config['p code'] = this.format_wxmp_p_code;
|
||||
}
|
||||
if(!config['li code']){
|
||||
config['li code'] = this.format_wxmp_li_code;
|
||||
}
|
||||
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 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)
|
||||
|
||||
// 替换标题
|
||||
for (let k in this.ctx_map) {
|
||||
rhtml = await this.set_tag_with_tpl(rhtml, k, this.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 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[]) {
|
||||
if (typeof (ctx) == 'string') {
|
||||
ctx = [ctx]
|
||||
}
|
||||
let data = new ClipboardItem({
|
||||
"text/html": new Blob(ctx, {
|
||||
type: "text/html"
|
||||
}),
|
||||
"text/plain": new Blob(ctx, {
|
||||
type: "text/plain"
|
||||
}),
|
||||
});
|
||||
await navigator.clipboard.write([data]);
|
||||
}
|
||||
|
||||
formatWeChatImageLink(inputHtml:string) {
|
||||
// 创建一个临时的 div 元素来解析 HTML
|
||||
let tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = inputHtml;
|
||||
|
||||
// 获取所有的 <a> 标签
|
||||
let aTags = tempDiv.querySelectorAll('a');
|
||||
|
||||
// 遍历所有的 <a> 标签
|
||||
aTags.forEach(aTag => {
|
||||
let imgTag = aTag.querySelector('img');
|
||||
|
||||
if (imgTag) {
|
||||
// 提取 href 和 src
|
||||
let href = aTag.getAttribute('href');
|
||||
let src = imgTag.getAttribute('src');
|
||||
|
||||
// 获取图片格式(如 png, jpg, gif 等)
|
||||
let imgFormat = src?.split('.').pop();
|
||||
|
||||
// 构建新的 HTML
|
||||
let newHtml = `
|
||||
<a href="${href}" imgurl="${src}?wx_fmt=${imgFormat}&from=appmsg" linktype="image" tab="innerlink" data-itemshowtype="" target="_blank" data-linktype="1">
|
||||
<span class="js_jump_icon h5_image_link">
|
||||
<img data-src="${src}?wx_fmt=${imgFormat}&from=appmsg" class="rich_pages wxw-img" data-ratio="0.18611111111111112" data-s="300,640" data-type="${imgFormat}" data-w="1080" type="block" data-imgfileid="100005308" src="${src}?wx_fmt=${imgFormat}&from=appmsg">
|
||||
</span>
|
||||
</a>
|
||||
`;
|
||||
|
||||
// 替换原始的 <a> 标签
|
||||
aTag.outerHTML = newHtml;
|
||||
}
|
||||
});
|
||||
|
||||
// 返回格式化后的 HTML
|
||||
return tempDiv.innerHTML;
|
||||
}
|
||||
|
||||
format_wxmp_h1(title:string){
|
||||
let css = `
|
||||
<h1 style="box-sizing: border-box; border-width: 0px 0px 2px; border-style: solid; border-bottom-color: rgb(0, 152, 116); font-size: 19.6px; font-weight: bold; margin: 2em auto 1em; text-align: center; line-height: 1.75; font-family: Menlo, Monaco, "Courier New", monospace; display: table; padding: 0.5em 1em; color: rgb(63, 63, 63); text-shadow: rgba(0, 0, 0, 0.1) 2px 2px 4px; visibility: visible;"><span leaf="" style="visibility: visible;">${title}</span></h1>
|
||||
`.trim()
|
||||
return css;
|
||||
}
|
||||
|
||||
format_wxmp_h2(title:string){
|
||||
let css = `
|
||||
<h2 style="box-sizing: border-box;border-width: 0px;border-style: solid;border-color: hsl(var(--border));font-size: 18.2px;font-weight: bold;margin: 4em auto 2em;text-align: center;line-height: 1.75;font-family: Menlo, Monaco, "Courier New", monospace;display: table;padding: 0.3em 1em;color: rgb(255, 255, 255);background: rgb(0, 152, 116);border-radius: 8px;box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px;"><span leaf="">${title}</span></h2>
|
||||
`.trim()
|
||||
return css;
|
||||
}
|
||||
|
||||
format_wxmp_h3(title:string){
|
||||
let css = `
|
||||
<h3 style="box-sizing: border-box;border-width: 0px 0px 1px 4px;border-style: solid solid dashed;border-bottom-color: rgb(0, 152, 116);border-left-color: rgb(0, 152, 116);font-size: 16.8px;font-weight: bold;margin: 2em 8px 0.75em 0px;text-align: left;line-height: 1.2;font-family: Menlo, Monaco, "Courier New", monospace;padding-left: 12px;color: rgb(63, 63, 63);"><span leaf="">${title}</span></h3>
|
||||
`.trim()
|
||||
return css;
|
||||
}
|
||||
|
||||
format_wxmp_p_code(code:string){
|
||||
let css = `
|
||||
<code style="box-sizing: border-box; border-width: 0px; border-style: solid; border-color: hsl(var(--border)); font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-feature-settings: normal; font-variation-settings: normal; font-size: 12.6px; text-align: left; line-height: 1.75; color: rgb(221, 17, 68); background: rgba(27, 31, 35, 0.05); padding: 3px 5px; border-radius: 4px; visibility: visible;"><span leaf="" style="visibility: visible;">${code}</span></code>`.trim()
|
||||
return css;
|
||||
}
|
||||
|
||||
format_wxmp_li_code(code:string){
|
||||
let css = `
|
||||
<code style="box-sizing: border-box; border-width: 0px; border-style: solid; border-color: hsl(var(--border)); font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-feature-settings: normal; font-variation-settings: normal; font-size: 12.6px; text-align: left; line-height: 1.75; color: rgb(221, 17, 68); background: rgba(27, 31, 35, 0.05); padding: 3px 5px; border-radius: 4px; visibility: visible;"><span leaf="" style="visibility: visible;">${code}</span></code>`.trim()
|
||||
return css;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue