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
- 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; }