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 <div class="me-mermaid"> with <pre class="mermaid">
- Zero third-party dependencies required
- Users optionally load Mermaid.js to initialize rendering
This commit is contained in:
2026-07-24 17:24:11 +08:00
parent b9e98a4ff4
commit d7813d8e1b
3 changed files with 85 additions and 78 deletions
+3
View File
@@ -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);
+66 -74
View File
@@ -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 {
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);
handle = await window.showSaveFilePicker({
types: [{ accept: { 'text/markdown': ['.md'] } }],
suggestedName: opts.name || 'document.md',
});
} catch (e) { console.error('FileSystem load error:', e); return null; }
this._fileHandle = handle;
} catch (e) {
if (e.name !== 'AbortError') console.error('Save file error:', e);
return false;
}
}
try {
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;
}
},
};
+15 -3
View File
@@ -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; }