From d7813d8e1ba26109c2239b3e523756adde985405 Mon Sep 17 00:00:00 2001 From: thzxx <1440196015@qq.com> Date: Fri, 24 Jul 2026 17:24:11 +0800 Subject: [PATCH] fix(v0.1.14): disk file system, syntax highlight transparency, zero-dep Mermaid fix(plugins): rewrite fileSystem to use File System Access API - openFile(): showOpenFilePicker for real .md disk files - saveFile(): showSaveFilePicker or write to existing handle - saveFileAs(): force save-as dialog - Graceful fallback when API unsupported fix(core): syntax highlight layer transparency - textarea gets me-textarea-highlight class when syntaxHighlight enabled - CSS: color transparent, caret-color visible, selection bg visible - Highlight layer positioned after gutter (left:36px) verify(parser): Mermaid self-implemented container - Creates
with
- Zero third-party dependencies required
- Users optionally load Mermaid.js to initialize rendering
---
 src/core.js    |   3 ++
 src/plugins.js | 142 +++++++++++++++++++++++--------------------------
 src/styles.js  |  18 +++++--
 3 files changed, 85 insertions(+), 78 deletions(-)

diff --git a/src/core.js b/src/core.js
index a7bb169..ee8f3c8 100644
--- a/src/core.js
+++ b/src/core.js
@@ -229,6 +229,9 @@ class MarkdownEditor {
     textarea.style.tabSize = String(this.config.tabSize || 2);
     textarea.placeholder = this.config.placeholder || i18nT('placeholder');
     textarea.setAttribute('aria-label', i18nT('edit'));
+    if (this.config.syntaxHighlight) {
+      textarea.classList.add('me-textarea-highlight');
+    }
     editorInner.appendChild(textarea);
 
     editorPane.appendChild(editorInner);
diff --git a/src/plugins.js b/src/plugins.js
index 7e6a1b5..fbb9700 100644
--- a/src/plugins.js
+++ b/src/plugins.js
@@ -600,102 +600,94 @@ const shortcutHelpPlugin = {
 };
 
 /**
- * 文件系统存储插件(v0.1.14)
- * 使用 IndexedDB 持久化编辑器内容
+ * 文件系统插件(v0.1.14)
+ * 使用 File System Access API 读写磁盘文件
  */
 const fileSystemPlugin = {
   name: 'fileSystem',
   version: '0.1.0',
-  description: 'IndexedDB 文件系统存储',
+  description: '磁盘文件读写(File System Access API)',
   priority: 90,
-  dbName: 'metona-editor-fs',
-  storeName: 'documents',
 
-  _db: null,
-
-  _openDB() {
-    if (this._db) return Promise.resolve(this._db);
-    if (typeof indexedDB === 'undefined') return Promise.reject(new Error('IndexedDB not supported'));
-    return new Promise((resolve, reject) => {
-      const req = indexedDB.open(this.dbName, 1);
-      req.onupgradeneeded = (e) => {
-        const db = e.target.result;
-        if (!db.objectStoreNames.contains(this.storeName)) {
-          db.createObjectStore(this.storeName, { keyPath: 'id' });
-        }
-      };
-      req.onsuccess = (e) => { this._db = e.target.result; resolve(this._db); };
-      req.onerror = () => reject(req.error);
-    });
-  },
+  _fileHandle: null,
 
   install(editor) {
-    if (!editor || typeof indexedDB === 'undefined') return;
-    if (typeof editor.getValue !== 'function') return;
+    if (!editor || typeof editor.getValue !== 'function') return;
 
-    editor.saveToFile = async (docId) => {
-      const id = docId || (editor.id || 'default');
-      const content = editor.getValue();
+    // 检查 API 支持
+    const hasAPI = typeof window !== 'undefined' && typeof window.showOpenFilePicker === 'function';
+
+    /** 打开磁盘文件 */
+    editor.openFile = async (opts = {}) => {
+      if (!hasAPI) {
+        editor.toast?.('File System Access API 不支持此浏览器', { type: 'warning' });
+        return null;
+      }
       try {
-        const db = await this._openDB();
-        return new Promise((resolve, reject) => {
-          const tx = db.transaction(this.storeName, 'readwrite');
-          tx.objectStore(this.storeName).put({ id, content, updatedAt: Date.now() });
-          tx.oncomplete = () => resolve(true);
-          tx.onerror = () => reject(tx.error);
+        const [handle] = await window.showOpenFilePicker({
+          types: [{ accept: { 'text/markdown': ['.md', '.txt', '.markdown'] } }],
+          ...opts,
         });
-      } catch (e) { console.error('FileSystem save error:', e); return false; }
+        this._fileHandle = handle;
+        const file = await handle.getFile();
+        const content = await file.text();
+        editor.setValue(content);
+        editor._emit?.('fileOpened', { name: file.name, handle });
+        return { name: file.name, content, handle };
+      } catch (e) {
+        if (e.name !== 'AbortError') console.error('Open file error:', e);
+        return null;
+      }
     };
 
-    editor.loadFromFile = async (docId) => {
-      const id = docId || (editor.id || 'default');
+    /** 保存到当前文件(如已打开)或弹出另存为对话框 */
+    editor.saveFile = async (opts = {}) => {
+      let handle = this._fileHandle;
+      if (!handle || opts.saveAs) {
+        if (!hasAPI) {
+          editor.toast?.('File System Access API 不支持此浏览器', { type: 'warning' });
+          return false;
+        }
+        try {
+          handle = await window.showSaveFilePicker({
+            types: [{ accept: { 'text/markdown': ['.md'] } }],
+            suggestedName: opts.name || 'document.md',
+          });
+          this._fileHandle = handle;
+        } catch (e) {
+          if (e.name !== 'AbortError') console.error('Save file error:', e);
+          return false;
+        }
+      }
       try {
-        const db = await this._openDB();
-        return new Promise((resolve, reject) => {
-          const tx = db.transaction(this.storeName, 'readonly');
-          const req = tx.objectStore(this.storeName).get(id);
-          req.onsuccess = () => {
-            if (req.result) { editor.setValue(req.result.content, { silent: true }); }
-            resolve(req.result ? req.result.content : null);
-          };
-          req.onerror = () => reject(req.error);
-        });
-      } catch (e) { console.error('FileSystem load error:', e); return null; }
+        const writable = await handle.createWritable();
+        await writable.write(editor.getValue());
+        await writable.close();
+        editor._emit?.('fileSaved', { handle });
+        return true;
+      } catch (e) {
+        // 权限过期,清除句柄重试
+        this._fileHandle = null;
+        if (!opts.saveAs) return editor.saveFile({ ...opts, saveAs: true });
+        console.error('Write file error:', e);
+        return false;
+      }
     };
 
-    editor.deleteFile = async (docId) => {
-      const id = docId || (editor.id || 'default');
-      try {
-        const db = await this._openDB();
-        return new Promise((resolve, reject) => {
-          const tx = db.transaction(this.storeName, 'readwrite');
-          tx.objectStore(this.storeName).delete(id);
-          tx.oncomplete = () => resolve(true);
-          tx.onerror = () => reject(tx.error);
-        });
-      } catch (e) { return false; }
-    };
+    /** 另存为新文件 */
+    editor.saveFileAs = (name) => editor.saveFile({ saveAs: true, name });
 
-    editor.listFiles = async () => {
-      try {
-        const db = await this._openDB();
-        return new Promise((resolve, reject) => {
-          const tx = db.transaction(this.storeName, 'readonly');
-          const req = tx.objectStore(this.storeName).getAll();
-          req.onsuccess = () => resolve(req.result || []);
-          req.onerror = () => reject(req.error);
-        });
-      } catch (e) { return []; }
-    };
+    /** 获取当前文件句柄 */
+    editor.getFileHandle = () => this._fileHandle;
   },
 
   destroy(editor) {
-    if (this._db) { this._db.close(); this._db = null; }
+    this._fileHandle = null;
     if (editor) {
-      delete editor.saveToFile;
-      delete editor.loadFromFile;
-      delete editor.deleteFile;
-      delete editor.listFiles;
+      delete editor.openFile;
+      delete editor.saveFile;
+      delete editor.saveFileAs;
+      delete editor.getFileHandle;
     }
   },
 };
diff --git a/src/styles.js b/src/styles.js
index 68ecdf0..c435fdc 100644
--- a/src/styles.js
+++ b/src/styles.js
@@ -134,11 +134,11 @@ const generateCSS = () => {
     .me-editor-inner { display: flex; height: 100%; overflow: hidden; position: relative; }
     /* 语法高亮叠加层(v0.1.14) */
     .me-highlight-layer {
-      position: absolute; top: 0; left: 0; right: 0; bottom: 0;
-      padding: 16px 18px; margin: 0; border: 0; outline: 0; overflow: auto;
+      position: absolute; top: 0; left: 36px; right: 0; bottom: 0;
+      padding: 16px 18px; margin: 0; border: 0; outline: 0; overflow: hidden;
       font-family: var(--md-mono); font-size: 13.5px; line-height: 1.7;
       white-space: pre-wrap; word-wrap: break-word;
-      color: transparent; pointer-events: none; background: transparent;
+      pointer-events: none; background: transparent;
     }
     .mh-heading { color: var(--md-accent); font-weight: 700; }
     .mh-bold { font-weight: 700; }
@@ -204,6 +204,18 @@ const generateCSS = () => {
       display: block;
       white-space: pre-wrap;
       word-wrap: break-word;
+      position: relative;
+      z-index: 1;
+    }
+    /* v0.1.14: 语法高亮模式 — textarea 透明,高亮层显示在下方 */
+    .me-textarea-highlight {
+      color: transparent !important;
+      caret-color: var(--md-text);
+      background: transparent !important;
+    }
+    .me-textarea-highlight::selection {
+      background: rgba(59, 130, 246, 0.25);
+      color: transparent;
     }
     .me-textarea::placeholder { color: var(--md-muted); opacity: 0.7; }
     .me-textarea:focus { outline: 0; }