//Yeah this is a super trash code import { Plugin, Notice, Command } from "obsidian"; import { TFile } from "obsidian"; import MarkdownIt from "markdown-it"; import * as request from "request"; import * as fs from "fs"; import * as path from "path"; const token = "HIDDEN_TOKEN" const apilink = "YOUR_API_LINK"; let autoUpdate = true; let autoSync = false; let split_filename = ""; export default class MyPlugin extends Plugin { private markdownIt: MarkdownIt; async onload() { this.markdownIt = new MarkdownIt({ html: true, breaks: true }); this.addCommand({ id: "meta-create", name: "Create Meta Data", callback: async () => { const openMdFile = this.app.workspace.getActiveFile(); if (!openMdFile) { return new Notice("No active file open"); } // format 2024-09-26 19:55:11 const nowTime = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, ''); const metaLines = [ '---', `title: ${openMdFile.basename.replace(".md", "")}`, 'tags: REPLACE_ME', 'excerpt: REPLACE_ME', "cover: IMAGE_LINK", `date: ${nowTime}`, '---' ]; const metaContent = metaLines.join("\n"); const content = await this.app.vault.read(openMdFile); await this.app.vault.modify(openMdFile, metaContent.trim() + "\n" + content); new Notice("Meta Data created successfully"); } }) this.addCommand({ id: "sync-blog", name: "Sync Blog", callback: async () => { new Notice("Syncing, Please wait..."); const option = { url: `${apilink}/sync`, headers: { "Authorization": `Bearer ${token}` } } request.post(option, (err, res, body) => { if (err) { new Notice("Sync failed, Please try again later"); return console.log(err); }; if (typeof body == "string") body = JSON.parse(body); if (body.status == "success") { const noticeMsg = `Sync success, All files updated`; new Notice(noticeMsg); } }); } }) this.addCommand({ id: "blog-server-start", name: "Start Blog Server", callback: async () => { new Notice("Starting, Please wait..."); const option = { url: `${apilink}/server/start`, headers: { "Authorization": `Bearer ${token}` } } autoSync = true; const option_ = { url: `${apilink}/sync`, headers: { "Authorization": `Bearer ${token}` } } request.post(option_, (err, res, body) => { if (err) { new Notice("Sync failed, Please try again later"); return console.log(err); }; if (typeof body == "string") body = JSON.parse(body); if (body.status == "success") { const noticeMsg = `Sync success, All files updated`; new Notice(noticeMsg); } }); request.post(option, async (err, res, body) => { if (err) { new Notice("Server start failed, Please try again later"); return console.log(err); }; if (typeof body == "string") body = JSON.parse(body); if (body.status == "success") { const noticeMsg = `Server started, View here: ${body.link}\nLink copied to clipboard`; await navigator.clipboard.writeText(body.link); new Notice(noticeMsg, 3000); } }); } }) this.addCommand({ id: "blog-server-close", name: "Stop Blog Server", callback: async () => { //sync first const option = { url: `${apilink}/server/close`, headers: { "Authorization": `Bearer ${token}` } } autoSync = false; request.post(option, (err, res, body) => { if (err) { new Notice("Server close failed, Please try again later"); return console.log(err); }; if (typeof body == "string") body = JSON.parse(body); if (body.status == "success") { const noticeMsg = `Server closed successfully`; new Notice(noticeMsg); } }); } }) this.addCommand({ id: "deploy-blog", name: "Deploy Blog", callback: async () => { new Notice("Syncing before deploy, Please wait..."); const syncOption = { url: `${apilink}/sync`, headers: { "Authorization": `Bearer ${token}` } } try { await new Promise((resolve, reject) => { request.post(syncOption, (err, res, body) => { if (err) { new Notice("Sync failed, Please try again later"); console.log(err); return reject(err); } if (typeof body == "string") body = JSON.parse(body); if (body.status === "success") { new Notice("Sync success, All files updated"); resolve(); } else { return reject(new Error("Sync unsuccessful")); } }); }); } catch (error) { // 同步失敗則中止 deploy 流程 return; } // 等待 1.5 秒 await new Promise(res => setTimeout(res, 1500)); new Notice("Deploying, Please wait..."); const option = { url: `${apilink}/deploy`, headers: { "Authorization": `Bearer ${token}` }, json: { keepServer: false } } request.post(option, (err, res, body) => { if (err) { new Notice("Deploy failed, Please try again later"); return console.log(err); }; if (typeof body == "string") body = JSON.parse(body); if (body.status == "success") { const noticeMsg = `Deploy completed`; new Notice(noticeMsg); } }); } }) this.addCommand({ id: "deploy-blog-keepserver", name: "Deploy Blog and Keep Server", callback: async () => { autoSync = true; new Notice("Syncing before deploy, Please wait..."); const syncOption = { url: `${apilink}/sync`, headers: { "Authorization": `Bearer ${token}` } } try { await new Promise((resolve, reject) => { request.post(syncOption, (err, res, body) => { if (err) { new Notice("Sync failed, Please try again later"); console.log(err); return reject(err); } if (typeof body == "string") body = JSON.parse(body); if (body.status === "success") { new Notice("Sync success, All files updated"); resolve(); } else { return reject(new Error("Sync unsuccessful")); } }); }); } catch (error) { // 同步失敗則中止 deploy 流程 return; } // 等待 1.5 秒 await new Promise(res => setTimeout(res, 1500)); new Notice("Deploying, Please wait..."); const option = { url: `${apilink}/deploy`, headers: { "Authorization": `Bearer ${token}` }, json: { keepServer: true } } request.post(option, async (err, res, body) => { if (err) { new Notice("Deploy failed, Please try again later"); return console.log(err); }; if (typeof body == "string") body = JSON.parse(body); if (body.status == "success") { const noticeMsg = `Deploy completed, View here: ${body.serverLink}\nLink copied to clipboard`; await navigator.clipboard.writeText(body.serverLink); new Notice(noticeMsg, 3000); } }); } }) this.addCommand({ id: "view-blog-obsidian", name: "View Blog in here", callback: async () => { console.log("enter"); const openMdFile = this.app.workspace.getActiveFile(); if (!openMdFile) { return new Notice("No active file open"); } if (!openMdFile.path.includes("Write Area")) { return new Notice("This command only works in Write Area"); } await this.RenderMarkdown(openMdFile.path); setTabs(); setHidden(); const previewFilePath = path.join(openMdFile.path.replace(/Write Area/gi, "Preview Area")).replace(/\\/g, '/'); const file = this.app.vault.getAbstractFileByPath(previewFilePath) as TFile; if (file.basename === split_filename) return; if (!file) return new Notice("File not found"); split_filename = file.basename; // 在右側開啟當前檔案的 preview 模式 const newLeaf = this.app.workspace.getLeaf('split', 'vertical'); await newLeaf.openFile(file, { state: { mode: 'preview', active: false, focus: false } }); }, }); this.addCommand({ id: "render-all-blog", name: "Render All Blog Markdown", callback: async () => { const files = this.app.vault.getMarkdownFiles(); for (const file of files) { await this.RenderMarkdown(file.path); } new Notice("All Markdown files have been rendered"); } }) this.addCommand({ id: "render-auto-update", name: "Auto Render Markdown", callback: () => { autoUpdate = !autoUpdate; new Notice(`Auto Update is ${autoUpdate ? "enabled" : "disabled"}`); }, }) this.addCommand({ id: "sync-auto-update", name: "Auto Sync Markdown to Server", callback: () => { autoSync = !autoSync; new Notice(`Auto Sync is ${autoSync ? "enabled" : "disabled"}`); }, }) function debounce void>(func: T, delay: number): T { let timer: NodeJS.Timeout | null = null; return function (this: any, ...args: Parameters) { if (timer) clearTimeout(timer); timer = setTimeout(() => { func.apply(this, args); timer = null; }, delay); } as T; } this.registerEvent( this.app.workspace.on( 'editor-change', debounce(async (elm: any) => { const openMdFile = this.app.workspace.getActiveFile(); if (!openMdFile || !openMdFile.path.includes("Write Area")) return; if (autoUpdate) { console.log("Rendering..."); if (!openMdFile) { new Notice("No active file open"); return; } await this.RenderMarkdown(openMdFile.path); setTabs(); setHidden(); console.log("Render completed"); } if (autoSync) { console.log("Syncing..."); const option = { url: `${apilink}/sync`, headers: { "Authorization": `Bearer ${token}` } } request.post(option, (err, res, body) => { if (err) { new Notice("Sync failed, Please try again later"); return console.log(err); } }); } }, 3000) ) ); } async RenderMarkdown(filePath: string) { try { let fileContent = await this.app.vault.adapter.read(filePath); fileContent = this.renderCustom(fileContent); fileContent = this.markdownIt.render(fileContent); fileContent = this.codeblockFix(fileContent); const basePath = (this.app.vault.adapter as any).getBasePath(); // 將檔案位於 Write Area 的路徑,替換成 Preview Area,保留相對資料夾結構 const previewFilePath = path.join( basePath, filePath.replace(/Write Area/gi, "Preview Area") ); const parentDir = path.dirname(previewFilePath); if (!fs.existsSync(parentDir)) { fs.mkdirSync(parentDir, { recursive: true }); } fs.writeFileSync(previewFilePath, fileContent, "utf-8"); } catch (error) { console.error("Error processing Markdown file:", error); } } onunload() { } private codeblockFix(content: string): string { const regex = /
([\w\W]+?)<\/code><\/pre>/g;
        return content.replace(regex, (_m, lang, inner) => {
            const replaced = lang
                ? inner.replace(/\n/gm, "\n
") : inner.replace(/\n/gm, "
"); return `
${replaced}
`; }); } private renderers(content: string): string { const renderers = [ this.applyButton, this.applyCheckbox, this.applyHidden, this.applyItalic, this.applyNewLine, this.applySubtext, this.applyUnderline, ]; renderers.forEach((fn) => { content = fn.call(this, content); }); content = this.second_layer_render(content); return content; } private second_layer_render(content: string): string { const admontionRegex = /:::\((info|note|danger|success|warn|warning)\)\[(.*?)\]\n([\s\S]*?)\n\s*:::/gm; const foldingRegex = /^>\[(info|note|danger|success|warn|df)\]\s(.+)\n([\s\S]*?)\n^<<<$/gm; const tabRegex = /\[\[(.*?)\]\]([\s\S]*?)\[\[end\]\]/gm; const recursionAdmontion = content.match(admontionRegex) ? true : false; const recursionFolding = content.match(foldingRegex) ? true : false; const recursionTab = content.match(tabRegex) ? true : false; content = this.applyAdmonition(content, recursionAdmontion); content = this.applyFolding(content, recursionFolding); content = this.applyNavTab(content, recursionTab); const allRecursion = recursionAdmontion || recursionFolding || recursionTab; if (allRecursion) { return this.second_layer_render(content); } return content; } private renderCustom(content: string): string { content = this.applyMetadata(content); const codeBlocks: string[] = []; content = content.replace(/```([\s\S]*?)```/g, (match) => { codeBlocks.push(match); return "\uE000CODEBLOCK_PLACEHOLDER\uE000"; }); const insideContentRenderers = [ this.applyNavTab, this.applyFolding, this.applyAdmonition, ]; insideContentRenderers.forEach((fn) => { content = fn.call(this, content); }); content = this.renderers(content); content = content.replace( /\uE000CODEBLOCK_PLACEHOLDER\uE000/g, () => this.markdownIt.render(codeBlocks.shift() || "") ); return content } private applyMetadata(content: string): string { const metaRegex = /^---\n([\s\S]+?)\n---\n/; const match = content.match(metaRegex); if (match) { const metaContent = match[1]; const lines = metaContent.split('\n').filter(line => line.trim().length > 0); let htmlContent = `\n`; content = content.replace(metaRegex, ''); content = htmlContent + content; } return content; } private applyAdmonition(content: string, recursion: boolean): string { if (!recursion) return content; const regex = /:::\((info|note|danger|success|warn|warning)\)\[(.*?)\]\n([\s\S]*?)\n\s*:::/gm; const cfg: Record = { info: "其他資訊", note: "小提示", danger: "注意警告", success: "相關資訊", warn: "特別注意", warning: "注意事項", }; return content.replace(regex, (_m, type, title, body) => { title = title.trim() || cfg[type]; return `

${title}

${this.markdownIt.render( this.renderers(body) )}
`; }); } private applyButton(content: string): string { return content.replace(/\[{(.+)}\]/gm, (_m, p1) => { const [text, url] = p1.split(",").map((e: string) => e.trim()); return `${text}`; }); } private applyCheckbox(content: string): string { const regex = /-\s\[(\s|x|-|_)\]\s(.+?)(\||$)(.+)?/gm; const icons: Record = { "x": `❌ `, " ": `✅ `, "-": `🟨 `, "_": `🟦 `, }; return content.replace(regex, (_m, check, text, _d, slogan) => { return `${icons[check] || icons[1]} ${text}${slogan ? ` ${slogan}` : ""}`; }); } private applyFolding(content: string, recursion: boolean): string { if (!recursion) return content; const regex = /^>\[(info|note|danger|success|warn|df)\]\s(.+)\n([\s\S]*?)\n^<<<$/gm; const colors: Record = { info: "blue", note: "gray", danger: "red", success: "green", warn: "orange", df: "white", }; content = content.replace(regex, (_m, type, title, body) => { return `
${title}
${this.markdownIt.render(this.renderers(body))}
`; }); return content } private applyHidden(content: string): string { return content.replace(/\|\|(.+?)\|\|/gm, (_m, text) => { return `${text}`; }); } private applyItalic(content: string): string { return content.replace(/\\(.+?)\\/g, (_m, text) => `${text}`); } private applyNavTab(content: string, recursion: boolean): string { if (!recursion) return content; const TABS_BLOCK_REGEX: RegExp = /\[\[(.*?)\]\]([\s\S]*?)\[\[end\]\]/gm; const TAB_BLOCK_REGEX: RegExp = /^\[(.*?)\]\n([\s\S]*?)(?=^\[(?:.+)\]$|\u200B$)/gm; const parseArgs = (args: string[]): string[] => { return args.join(" ").includes("::") ? args.join(" ").split("::") : args.join(" ").split(","); }; const extractMatches = (content: string): string[] => { const matches: string[] = []; let match: RegExpExecArray | null; content += "\u200B"; while ((match = TAB_BLOCK_REGEX.exec(content)) !== null) { matches.push(match[1]); matches.push(match[2]); } return matches; }; const buildTabNavAndContent = ( matches: string[], tabName: string, tabActive: number ): { tabNav: string; tabContent: string } => { let tabNav = ""; let tabContent = ""; for (let i = 0; i < matches.length; i += 2) { const tabParameters: string[] = matches[i].split("@"); const tabCaption: string = tabParameters[0] || ""; const tabIcon: string = tabParameters[1] || ""; const tabHref: string = (tabName + " " + (i / 2 + 1)) .toLowerCase() .split(" ") .join("-"); const renderedContent: string = this.markdownIt .render(matches[i + 1]) .trim(); const isActive: string = (tabActive > 0 && tabActive === i / 2 + 1) || (tabActive === 0 && i === 0) ? " active" : ""; tabNav += `
  • ${tabIcon + tabCaption.trim()}
  • `; tabContent += `
    ${renderedContent}
    `; } return { tabNav, tabContent }; }; return content.replace( TABS_BLOCK_REGEX, (_m: string, tabsId: string, tabsContent: string): string => { const [tabName, tabActiveStr] = parseArgs(tabsId.split(" ")); const activeTabIndex: number = Number(tabActiveStr) || 0; if (!tabName) console.warn("Tabs block must have unique name!"); const matches: string[] = extractMatches(tabsContent); const { tabNav, tabContent } = buildTabNavAndContent( matches, tabName, activeTabIndex ); const finalTabNav: string = ``; let finalcontent = this.markdownIt.render(tabContent).replace(/
    /g, '\n'); finalcontent = finalcontent.replace('\uE000CODEBLOCK_PLACEHOLDER\uE000', '\n\uE000CODEBLOCK_PLACEHOLDER\uE000\n') const finalTabContent: string = `
    ${finalcontent}
    `; return `
    ${finalTabNav}${finalTabContent}
    `; } ); } private applyNewLine(content: string): string { return content.replace(/^\\n(.*)/gm, (_m, text) => `
    ${text}`); } private applySubtext(content: string): string { return content.replace(/-#\s*(.+)$/gm, (_m, text) => `${text}`); } private applyUnderline(content: string): string { return content.replace(/__(.+?)__/g, (_m, text) => `${text}`); } } function setHidden(): void { document.querySelectorAll('.hidden-box').forEach(function (element) { element.addEventListener('click', function () { element.classList.toggle('hiddenblock'); }); }); } function setTabs(): void { const tabs = document.querySelectorAll(".tabs .nav-tabs"); if (!tabs) return; tabs.forEach((tab) => { const links = tab.querySelectorAll("a"); links.forEach((link) => { link.addEventListener("click", (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); const target = e.target as HTMLElement; const parentTab = target.parentElement?.parentElement?.parentElement; if (!parentTab) return; const activeNav = parentTab.querySelector(".nav-tabs .active"); activeNav?.classList.remove("active"); target.parentElement?.classList.add("active"); const activeContent = parentTab.querySelector(".tab-content .active"); activeContent?.classList.remove("active"); const classList = Array.from(target.classList).filter( (cls) => cls.trim().length > 0 ); if (classList.length === 0) return; const selector = classList.join("."); const newActiveContent = parentTab.querySelector(selector); newActiveContent?.classList.add("active"); return false; }); }); }); }