From 289b8e55a61e3821840c08178563ddc6824ab540 Mon Sep 17 00:00:00 2001
From: thzxx <1440196015@qq.com>
Date: Fri, 24 Jul 2026 10:33:12 +0800
Subject: [PATCH] =?UTF-8?q?release:=20v0.1.7=20=E2=80=94=20editing=20exper?=
=?UTF-8?q?ience:=20gutter,=20auto-format,=20bracket=20close,=20drag-drop,?=
=?UTF-8?q?=20outline?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
feat(core): line number gutter with current line highlight
- .me-gutter rendered alongside textarea, sync-scrolls with content
- .me-gutter-active highlights the line containing the cursor
- config.lineNumbers (default true) to toggle
feat(core): smart Enter auto-formatting
- List continuation: '- ' / '1. ' auto-insert on Enter at end of list item
- Ordered list auto-increment: '1.' → '2.'
- Quote continuation: '> ' auto-insert on Enter at end of quote line
- Empty list/quote item: Enter removes the marker (end list)
feat(core): bracket/quote auto-close
- (), [], {}, <>, "", '', ``, **, __ auto-close pairs
- Selection wrapping: select text then press ( → wraps as (text)
- config.autoBrackets (default true) to toggle
feat(core): drag & drop file support
- Drop image files → auto base64 inline insert 
- Drop text/code files → insert file contents
- Drop external text from browser → insert at cursor
feat(core): outline/TOC panel
- Extracts headings from rendered HTML preview
- Nested tree with indentation by heading level
- Click to scroll-jump in both preview and textarea
- config.outline (default false) to toggle
style: gutter, outline panel, current line highlight CSS
test: 11 new tests for gutter, auto-format, bracket close, outline (532 total)
chore: bump version 0.1.6 → 0.1.7 across all files, site, types
---
README.md | 5 +-
package.json | 2 +-
site/demo.html | 4 +-
site/docs.html | 4 +-
src/animations.js | 2 +-
src/constants.js | 8 +-
src/core.js | 324 ++++++++++++++++++++++++++++++++++++++++++++-
src/i18n.js | 2 +-
src/icons.js | 2 +-
src/index.js | 4 +-
src/locales.js | 2 +-
src/parser.js | 2 +-
src/plugins.js | 2 +-
src/styles.js | 64 ++++++++-
src/themes.js | 2 +-
src/utils.js | 2 +-
tests/core.test.js | 130 ++++++++++++++++++
types/index.d.ts | 2 +-
18 files changed, 539 insertions(+), 24 deletions(-)
diff --git a/README.md b/README.md
index 39db01e..123bc20 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,9 @@
> 轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用,中文优先。
-[](https://www.npmjs.com/package/@metona-team/metona-editor)
+[](https://www.npmjs.com/package/@metona-team/metona-editor)
[](./LICENSE)
-[](./tests)
+[](./tests)
[](./tests)
---
@@ -19,6 +19,7 @@
- **安全** — 内置 XSS 防护(`javascript:`/`vbscript:`/`file:` 协议过滤),HTML 转义,`safeUrl` 净化
- **内置解析器** — 自研 CommonMark 子集 + GFM 扩展(表格含列对齐、任务列表、上下标、数学公式、脚注、嵌套列表、emoji 短码等),可整体替换
- **三模式视图** — edit / split / preview 自由切换,分屏拖拽调整比例,滚动同步,模式切换保持滚动位置
+- **编辑体验** — 行号装订线 + 当前行高亮、智能 Enter(列表/引用自动延续)、括号/引号自动闭合、拖放文件/图片/文本、大纲/目录面板
- **主题系统** — light / dark / auto / warm 四套预设,CSS 变量定制,实例级主题隔离,外部主题跟随(syncWithElement / adoptFromParent),主题继承(extends),跟随系统主题,@media print 打印样式
- **国际化** — zh-CN / en-US 完整翻译,实例级语言隔离,复数规则,命名空间,动态远程加载翻译包,RTL 支持
- **历史栈** — 撤销/重做,防抖合并(可配置延迟),可配置上限
diff --git a/package.json b/package.json
index 850fc7e..372000a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@metona-team/metona-editor",
- "version": "0.1.6",
+ "version": "0.1.7",
"description": "轻量、零依赖、精致美观的 Markdown Editor 库。单文件,开箱即用。",
"type": "module",
"main": "dist/metona-editor.js",
diff --git a/site/demo.html b/site/demo.html
index 3efa1ae..e5f3983 100644
--- a/site/demo.html
+++ b/site/demo.html
@@ -313,9 +313,9 @@
'',
'| 名称 | 值 |',
'| --- | --- |',
- '| 版本 | 0.1.6 |',
+ '| 版本 | 0.1.7 |',
'| 依赖 | 零 |',
- '| 测试 | 521+ |',
+ '| 测试 | 532+ |',
'',
'## 新增语法',
'',
diff --git a/site/docs.html b/site/docs.html
index 8d82cb2..d3342a9 100644
--- a/site/docs.html
+++ b/site/docs.html
@@ -175,7 +175,7 @@
@@ -618,7 +618,7 @@ i18nUtils
.formatDate
- MetonaEditor v0.1.6 ·
源码仓库 · MIT License
+ MetonaEditor v0.1.7 ·
源码仓库 · MIT License
diff --git a/src/animations.js b/src/animations.js
index 11cfdbd..c2c6e87 100644
--- a/src/animations.js
+++ b/src/animations.js
@@ -1,7 +1,7 @@
/**
* MetonaEditor Animations - 动画管理
* @module animations
- * @version 0.1.6
+ * @version 0.1.7
* @description 动画注册与管理(当前为 CSS 动画元数据管理,供未来扩展如 Toast/通知组件的进出场动画使用;
* 内置 7 种预设动画曲线配置;编辑器核心当前未直接调用此模块)
*/
diff --git a/src/constants.js b/src/constants.js
index 6484a61..405df20 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -1,7 +1,7 @@
/**
* MetonaEditor Constants - 常量定义
* @module constants
- * @version 0.1.6
+ * @version 0.1.7
* @description 默认配置、主题、动画、工具栏等常量
*/
@@ -50,6 +50,12 @@ export const DEFAULTS = Object.freeze({
tabSize: 2,
// 只读模式
readOnly: false,
+ // 显示行号(v0.1.7)
+ lineNumbers: true,
+ // 显示大纲面板(v0.1.7)
+ outline: false,
+ // 自动括号闭合(v0.1.7)
+ autoBrackets: true,
// 主题:light / dark / auto / warm / 自定义
theme: 'auto',
// 国际化
diff --git a/src/core.js b/src/core.js
index dffffe8..5915240 100644
--- a/src/core.js
+++ b/src/core.js
@@ -1,9 +1,10 @@
/**
* MetonaEditor Core - 编辑器核心
* @module core
- * @version 0.1.6
+ * @version 0.1.7
* @description MarkdownEditor 类实现:DOM 构建、事件、渲染、历史栈、选区操作、模式切换
* v0.1.6: 插件依赖拓扑排序、实例 i18n、快捷键/工具栏定制、右键菜单、Toast
+ * v0.1.7: 行号装订线、自动格式化、括号自动闭合、拖放、大纲面板
*/
import { generateId, escapeHTML, isBrowser } from './utils.js';
@@ -24,6 +25,13 @@ const TOAST_ICONS = {
info: 'ℹ',
};
+/** 括号/引号自动闭合映射 */
+const BRACKET_PAIRS = {
+ '(': ')', '[': ']', '{': '}', '<': '>',
+ '"': '"', "'": "'", '`': '`',
+ '*': '*', '_': '_',
+};
+
/**
* 解析主题名称为实际主题(auto -> light/dark)
*/
@@ -189,6 +197,16 @@ class MarkdownEditor {
const editorPane = document.createElement('div');
editorPane.className = 'me-editor-pane';
+ // 编辑区内层容器(行号 + textarea 水平排列)
+ const editorInner = document.createElement('div');
+ editorInner.className = 'me-editor-inner';
+
+ // 行号装订线(v0.1.7)
+ const gutter = document.createElement('div');
+ gutter.className = 'me-gutter';
+ if (!this.config.lineNumbers) gutter.style.display = 'none';
+ editorInner.appendChild(gutter);
+
const textarea = document.createElement('textarea');
textarea.className = 'me-textarea';
textarea.spellcheck = !!this.config.spellcheck;
@@ -196,7 +214,9 @@ class MarkdownEditor {
textarea.style.tabSize = String(this.config.tabSize || 2);
textarea.placeholder = this.config.placeholder || i18nT('placeholder');
textarea.setAttribute('aria-label', i18nT('edit'));
- editorPane.appendChild(textarea);
+ editorInner.appendChild(textarea);
+
+ editorPane.appendChild(editorInner);
const divider = document.createElement('div');
divider.className = 'me-divider';
@@ -232,6 +252,8 @@ class MarkdownEditor {
this.toolbarEl = toolbar;
this.bodyEl = body;
this.editorPane = editorPane;
+ this.editorInner = editorInner;
+ this.gutter = gutter;
this.previewPane = previewPane;
this.dividerEl = divider;
this.textarea = textarea;
@@ -296,6 +318,8 @@ class MarkdownEditor {
this._scheduleRender();
this._scheduleHistory();
this._updateWordCount();
+ this._renderGutter();
+ this._updateOutline();
this._emit('input', this._value);
this._emit('change', this._value);
if (typeof this.config.onInput === 'function') {
@@ -307,10 +331,27 @@ class MarkdownEditor {
};
ta.addEventListener('input', onInput);
- const onKeydown = (e) => this._handleKeydown(e);
+ const onKeydown = (e) => {
+ // v0.1.7: 智能 Enter
+ if (e.key === 'Enter' && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
+ this._handleSmartEnter(e);
+ }
+ this._handleKeydown(e);
+ };
ta.addEventListener('keydown', onKeydown);
+ // v0.1.7: 括号/引号自动闭合
+ const onKeypress = (e) => this._handleBracketAutoClose(e);
+ ta.addEventListener('keypress', onKeypress);
+
+ // v0.1.7: 当前行高亮
+ const onCursorActivity = () => { this._updateCurrentLine(); };
+ ta.addEventListener('click', onCursorActivity);
+ ta.addEventListener('keyup', onCursorActivity);
+
const onScroll = () => {
+ // 同步行号滚动
+ if (this.gutter) this.gutter.scrollTop = ta.scrollTop;
if (!this.config.syncScroll || this._mode !== 'split') return;
const max = ta.scrollHeight - ta.clientHeight;
const ratio = max > 0 ? ta.scrollTop / max : 0;
@@ -352,10 +393,16 @@ class MarkdownEditor {
const onDividerDown = (e) => this._bindDividerDrag(e);
this.dividerEl.addEventListener('pointerdown', onDividerDown);
+ // v0.1.7: 拖放支持
+ this._bindDragDrop();
+
this._cleanups.push(() => {
ta.removeEventListener('input', onInput);
ta.removeEventListener('keydown', onKeydown);
+ ta.removeEventListener('keypress', onKeypress);
ta.removeEventListener('scroll', onScroll);
+ ta.removeEventListener('click', onCursorActivity);
+ ta.removeEventListener('keyup', onCursorActivity);
ta.removeEventListener('focus', onFocus);
ta.removeEventListener('blur', onBlur);
this.toolbarEl.removeEventListener('click', onToolbarClick);
@@ -513,11 +560,279 @@ class MarkdownEditor {
}
this.previewEl.innerHTML = html;
+ this._renderGutter();
MarkdownEditor.trigger('afterRender', this);
this._emit('afterRender', this);
}
- // ============ 历史栈 ============
+ // ============ 行号渲染(v0.1.7) ============
+
+ _renderGutter() {
+ if (!this.config.lineNumbers || !this.gutter) return;
+ const lines = this._value ? this._value.split('\n').length : 1;
+ const cur = this.gutter.children.length;
+ if (cur === lines) return; // 行数未变,跳过
+ let html = '';
+ for (let i = 1; i <= lines; i++) {
+ html += `${i}
`;
+ }
+ this.gutter.innerHTML = html;
+ }
+
+ _updateCurrentLine() {
+ if (!this.gutter) return;
+ const pos = this.textarea.selectionStart;
+ const lineNum = this._value.substring(0, pos).split('\n').length;
+ const prev = this.gutter.querySelector('.me-gutter-active');
+ if (prev) prev.classList.remove('me-gutter-active');
+ const cur = this.gutter.children[lineNum - 1];
+ if (cur) cur.classList.add('me-gutter-active');
+ }
+
+ // ============ 自动格式化(v0.1.7) ============
+
+ /**
+ * 智能 Enter:在列表/引用行尾自动延续标记
+ */
+ _handleSmartEnter(e) {
+ const ta = this.textarea;
+ const start = ta.selectionStart;
+ const val = ta.value;
+ const lineStart = val.lastIndexOf('\n', start - 1) + 1;
+ const line = val.slice(lineStart, start);
+
+ // 列表项延续:匹配 "- " / "* " / "+ " / "1. " / "2. " 等
+ const listMatch = line.match(/^(\s*)([-*+]|\d+\.)\s(.*)/);
+ if (listMatch) {
+ const indent = listMatch[1];
+ const marker = listMatch[2];
+ const content = listMatch[3];
+ // 空列表项 → 结束列表(移除标记)
+ if (!content.trim()) {
+ e.preventDefault();
+ ta.value = val.slice(0, lineStart) + '\n' + val.slice(start);
+ ta.selectionStart = ta.selectionEnd = lineStart;
+ this._value = ta.value;
+ this._pushHistory();
+ this._renderGutter();
+ this._emit('change', this._value);
+ return;
+ }
+ // 有序列表自动递增
+ let nextMarker = marker;
+ if (/^\d+\.$/.test(marker)) {
+ const num = parseInt(marker, 10);
+ if (!isNaN(num)) nextMarker = (num + 1) + '.';
+ }
+ e.preventDefault();
+ const insert = '\n' + indent + nextMarker + ' ';
+ ta.value = val.slice(0, start) + insert + val.slice(ta.selectionEnd);
+ ta.selectionStart = ta.selectionEnd = start + insert.length;
+ this._value = ta.value;
+ this._pushHistory();
+ this._renderGutter();
+ this._emit('change', this._value);
+ return;
+ }
+
+ // 引用块延续
+ const quoteMatch = line.match(/^(\s*>+\s?)(.*)/);
+ if (quoteMatch) {
+ const prefix = quoteMatch[1];
+ const content = quoteMatch[2];
+ if (!content.trim()) {
+ e.preventDefault();
+ ta.value = val.slice(0, lineStart) + '\n' + val.slice(start);
+ ta.selectionStart = ta.selectionEnd = lineStart;
+ this._value = ta.value;
+ this._pushHistory();
+ this._renderGutter();
+ this._emit('change', this._value);
+ return;
+ }
+ e.preventDefault();
+ const insert = '\n' + prefix;
+ ta.value = val.slice(0, start) + insert + val.slice(ta.selectionEnd);
+ ta.selectionStart = ta.selectionEnd = start + insert.length;
+ this._value = ta.value;
+ this._pushHistory();
+ this._renderGutter();
+ this._emit('change', this._value);
+ return;
+ }
+ }
+
+ // ============ 括号/引号自动闭合(v0.1.7) ============
+
+ _handleBracketAutoClose(e) {
+ if (!this.config.autoBrackets) return;
+ const ta = this.textarea;
+ const key = e.key;
+ const close = BRACKET_PAIRS[key];
+ if (!close) return;
+
+ const start = ta.selectionStart;
+ const end = ta.selectionEnd;
+ const hasSelection = start !== end;
+
+ // 有选区:包裹选中文本
+ if (hasSelection) {
+ e.preventDefault();
+ const selected = ta.value.slice(start, end);
+ const wrap = key === '*' || key === '_' ? key + selected + close : key + selected + close;
+ ta.value = ta.value.slice(0, start) + wrap + ta.value.slice(end);
+ ta.selectionStart = start + 1;
+ ta.selectionEnd = start + 1 + selected.length;
+ this._value = ta.value;
+ this._pushHistory();
+ this._emit('change', this._value);
+ return;
+ }
+
+ // 无选区:判断是否应插入闭合对
+ const nextChar = ta.value[start] || '';
+ const isQuotePair = key === close; // 引号自己闭合自己
+
+ if (isQuotePair) {
+ // 引号:下一个字符是同种引号 → 跳过而非插入
+ if (nextChar === close) {
+ e.preventDefault();
+ ta.selectionStart = ta.selectionEnd = start + 1;
+ return;
+ }
+ // 光标在单词中间 → 不自动闭合
+ if (/\w/.test(nextChar)) return;
+ }
+
+ // 括号:下一个字符非空白/换行/闭合括号 → 可能不需要闭合
+ // 简单策略:始终闭合
+ e.preventDefault();
+ const pair = key + close;
+ ta.value = ta.value.slice(0, start) + pair + ta.value.slice(end);
+ ta.selectionStart = ta.selectionEnd = start + 1;
+ this._value = ta.value;
+ this._pushHistory();
+ this._emit('change', this._value);
+ }
+
+ // ============ 拖放支持(v0.1.7) ============
+
+ _bindDragDrop() {
+ const ta = this.textarea;
+ if (!ta) return;
+
+ const onDragOver = (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; };
+ const onDrop = (e) => {
+ e.preventDefault();
+ const files = e.dataTransfer.files;
+ if (files && files.length) {
+ Array.from(files).forEach((file) => {
+ if (file.type.startsWith('image/')) {
+ const reader = new FileReader();
+ reader.onload = () => {
+ const name = file.name || `image-${Date.now().toString(36)}.png`;
+ this.insert(`\n`);
+ };
+ reader.readAsDataURL(file);
+ } else if (file.type.startsWith('text/') || file.name.match(/\.(md|txt|js|ts|json|css|html|xml|yml|yaml)$/i)) {
+ const reader = new FileReader();
+ reader.onload = () => this.insert(reader.result);
+ reader.readAsText(file);
+ }
+ });
+ return;
+ }
+ // 拖入外部文本
+ const text = e.dataTransfer.getData('text/plain');
+ if (text) {
+ this.insert(text);
+ }
+ };
+
+ ta.addEventListener('dragover', onDragOver);
+ ta.addEventListener('drop', onDrop);
+ this._cleanups.push(() => {
+ ta.removeEventListener('dragover', onDragOver);
+ ta.removeEventListener('drop', onDrop);
+ });
+ }
+
+ // ============ 大纲面板(v0.1.7) ============
+
+ _buildOutline() {
+ if (!this.config.outline || !this.previewEl) return;
+ // 移除旧面板
+ const old = this.el.querySelector('.me-outline');
+ if (old) old.remove();
+
+ const headings = [];
+ const headingRe = /]*>(.+?)<\/h\1>/gi;
+ const html = this.previewEl.innerHTML;
+ let m;
+ while ((m = headingRe.exec(html)) !== null) {
+ headings.push({ level: parseInt(m[1], 10), id: m[2], text: m[3].replace(/<[^>]+>/g, '') });
+ }
+ if (!headings.length) return;
+
+ const panel = document.createElement('div');
+ panel.className = 'me-outline';
+ panel.innerHTML = '' + (i18nT('outline') || '大纲') + '
';
+
+ const buildTree = (items, minLevel) => {
+ let h = '';
+ let i = 0;
+ while (i < items.length) {
+ const item = items[i];
+ if (item.level < minLevel) break;
+ h += `- ${escapeHTML(item.text)}`;
+ // 收集子项
+ const subItems = [];
+ let j = i + 1;
+ while (j < items.length && items[j].level > item.level) {
+ subItems.push(items[j]);
+ j++;
+ }
+ if (subItems.length) {
+ h += buildTree(subItems, item.level + 1);
+ }
+ h += '
';
+ i = j > i + 1 ? j : i + 1;
+ }
+ h += '
';
+ return h;
+ };
+
+ panel.innerHTML += buildTree(headings, 1);
+ this.el.appendChild(panel);
+
+ // 点击跳转
+ panel.addEventListener('click', (e) => {
+ const a = e.target.closest('a');
+ if (!a) return;
+ e.preventDefault();
+ const id = a.getAttribute('href').slice(1);
+ const target = this.previewEl.querySelector('#' + CSS.escape(id));
+ if (target) {
+ target.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ // 同步滚动 textarea
+ const headingText = target.textContent;
+ const idx = this._value.indexOf(headingText);
+ if (idx !== -1) {
+ this.textarea.focus();
+ this.textarea.setSelectionRange(idx, idx);
+ const lineNum = this._value.substring(0, idx).split('\n').length - 1;
+ this.textarea.scrollTop = lineNum * 20;
+ }
+ }
+ });
+ }
+
+ _updateOutline() {
+ if (!this.config.outline) return;
+ // 防抖重建
+ clearTimeout(this._outlineTimer);
+ this._outlineTimer = setTimeout(() => this._buildOutline(), 300);
+ }
_scheduleHistory() {
if (this._historyTimer) clearTimeout(this._historyTimer);
@@ -552,6 +867,7 @@ class MarkdownEditor {
this._value = this._history[this._historyIndex];
this.textarea.value = this._value;
this._render();
+ this._renderGutter();
this._updateWordCount();
this._emit('change', this._value);
if (typeof this.config.onChange === 'function') {
diff --git a/src/i18n.js b/src/i18n.js
index 8681296..2f27b92 100644
--- a/src/i18n.js
+++ b/src/i18n.js
@@ -1,7 +1,7 @@
/**
* MetonaEditor i18n — 国际化管理(增强版)
* @module i18n
- * @version 0.1.6
+ * @version 0.1.7
* @description 多语言支持、实例级语言隔离、复数规则、动态加载、命名空间
*
* v0.1.6 增强:
diff --git a/src/icons.js b/src/icons.js
index ae12667..fa7cf45 100644
--- a/src/icons.js
+++ b/src/icons.js
@@ -1,7 +1,7 @@
/**
* MetonaEditor Icons — 工具栏图标SVG定义
* @module icons
- * @version 0.1.6
+ * @version 0.1.7
* @description 工具栏格式化按钮 SVG 图标
*/
diff --git a/src/index.js b/src/index.js
index 2d0a551..eaf8357 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,7 +1,7 @@
/**
* MetonaEditor - 轻量级 Markdown Editor 库
* @module metona-editor
- * @version 0.1.6
+ * @version 0.1.7
* @author thzxx
* @description 现代化、零依赖、易扩展、易维护、易使用的 Markdown 编辑器库。单文件,开箱即用。
* @license MIT
@@ -29,7 +29,7 @@ import { animationUtils } from './animations.js';
import { DEFAULTS, ICONS, THEMES, EDIT_MODES, DEFAULT_TOOLBAR, TOOLBAR_ACTIONS } from './constants.js';
// 版本信息
-const VERSION = '0.1.6';
+const VERSION = '0.1.7';
/**
* 全局默认插件队列
diff --git a/src/locales.js b/src/locales.js
index 995e8df..93df0b8 100644
--- a/src/locales.js
+++ b/src/locales.js
@@ -1,7 +1,7 @@
/**
* MetonaEditor Locales — 国际化翻译数据
* @module locales
- * @version 0.1.6
+ * @version 0.1.7
* @description 内置 zh-CN / en-US 完整翻译
*/
diff --git a/src/parser.js b/src/parser.js
index b3a8d44..365324b 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -1,7 +1,7 @@
/**
* MetonaEditor Parser - 轻量 Markdown 解析器(增强版)
* @module parser
- * @version 0.1.6
+ * @version 0.1.7
* @description 自研零依赖 Markdown 解析器,覆盖 CommonMark 子集 + GFM 扩展
*
* v0.1.4 增强:
diff --git a/src/plugins.js b/src/plugins.js
index 44687f1..cc96251 100644
--- a/src/plugins.js
+++ b/src/plugins.js
@@ -1,7 +1,7 @@
/**
* MetonaEditor Plugins — 插件系统 v2
* @module plugins
- * @version 0.1.6
+ * @version 0.1.7
* @description 插件管理器 + 预设插件 + 依赖/生命周期/异步/卸载
*
* v0.1.6 增强:
diff --git a/src/styles.js b/src/styles.js
index adfe472..c6e2380 100644
--- a/src/styles.js
+++ b/src/styles.js
@@ -1,7 +1,7 @@
/**
* MetonaEditor Styles - 编辑器样式
* @module styles
- * @version 0.1.6
+ * @version 0.1.7
* @description 编辑器 UI 样式注入与管理
*/
@@ -127,6 +127,32 @@ const generateCSS = () => {
overflow: hidden;
position: relative;
}
+
+ /* ============ 编辑区内层(v0.1.7) ============ */
+ .me-editor-inner { display: flex; height: 100%; overflow: hidden; position: relative; }
+ .me-gutter {
+ flex: 0 0 auto;
+ min-width: 36px;
+ padding: 16px 8px 16px 6px;
+ overflow: hidden;
+ background: var(--md-code-bg);
+ border-right: 1px solid var(--md-border);
+ color: var(--md-muted);
+ font-family: var(--md-mono);
+ font-size: 12px;
+ line-height: 1.7;
+ text-align: right;
+ user-select: none;
+ white-space: pre;
+ }
+ .me-gutter-line { padding-right: 4px; }
+ .me-gutter-active {
+ color: var(--md-accent);
+ font-weight: 600;
+ background: rgba(59, 130, 246, 0.08);
+ border-radius: 3px;
+ }
+
.me-divider {
flex: 0 0 1px;
background: var(--md-border);
@@ -454,6 +480,42 @@ const generateCSS = () => {
margin-left: 24px;
}
+ /* ============ 大纲面板(v0.1.7) ============ */
+ .me-outline {
+ position: absolute;
+ top: 0; right: 0;
+ width: 220px;
+ height: 100%;
+ overflow-y: auto;
+ background: var(--md-preview-bg);
+ border-left: 1px solid var(--md-border);
+ padding: 12px 14px;
+ font-size: 13px;
+ z-index: 15;
+ }
+ .me-outline-title {
+ font-weight: 650;
+ margin-bottom: 8px;
+ padding-bottom: 6px;
+ border-bottom: 1px solid var(--md-border);
+ color: var(--md-text);
+ }
+ .me-outline ul { list-style: none; padding: 0; margin: 0; }
+ .me-outline li { margin: 2px 0; }
+ .me-outline a {
+ color: var(--md-muted);
+ text-decoration: none;
+ display: block;
+ padding: 2px 6px;
+ border-radius: 4px;
+ transition: background 0.1s, color 0.1s;
+ }
+ .me-outline a:hover { background: var(--md-code-bg); color: var(--md-accent); }
+ .me-outline-l1 a { font-weight: 600; color: var(--md-text); }
+ .me-outline-l2 a { padding-left: 12px; }
+ .me-outline-l3 a { padding-left: 20px; font-size: 12px; }
+ .me-outline-l4 a { padding-left: 28px; font-size: 12px; }
+
/* ============ 打印样式 ============ */
@media print {
.me-wrapper { border: 0 !important; box-shadow: none !important; }
diff --git a/src/themes.js b/src/themes.js
index 56740ec..614e142 100644
--- a/src/themes.js
+++ b/src/themes.js
@@ -1,7 +1,7 @@
/**
* MetonaEditor Themes — 主题系统(增强版)
* @module themes
- * @version 0.1.6
+ * @version 0.1.7
* @description 全局+实例级主题管理、CSS 变量隔离、外部主题跟随、主题继承
*
* v0.1.5 增强:
diff --git a/src/utils.js b/src/utils.js
index 0b3df7f..fa5ac11 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -1,7 +1,7 @@
/**
* MetonaEditor Utils - 工具函数
* @module utils
- * @version 0.1.6
+ * @version 0.1.7
* @description 通用工具函数集合
*/
diff --git a/tests/core.test.js b/tests/core.test.js
index 2e7864f..b09a12f 100644
--- a/tests/core.test.js
+++ b/tests/core.test.js
@@ -1352,3 +1352,133 @@ describe('MarkdownEditor - v0.1.3 修复验证', () => {
expect(ed.isDisabled()).toBe(false);
});
});
+
+// ============ v0.1.7 新增测试 ============
+
+describe('MarkdownEditor - v0.1.7 行号装订线', () => {
+ test('lineNumbers 开启时创建 gutter', () => {
+ document.body.innerHTML = '';
+ const c = document.createElement('div');
+ document.body.appendChild(c);
+ const ed = new MarkdownEditor(c, { value: 'line1\nline2\nline3', lineNumbers: true });
+ expect(ed.gutter).not.toBeNull();
+ expect(ed.gutter.children.length).toBeGreaterThan(0);
+ ed.destroy();
+ });
+
+ test('lineNumbers 关闭时不显示', () => {
+ document.body.innerHTML = '';
+ const c = document.createElement('div');
+ document.body.appendChild(c);
+ const ed = new MarkdownEditor(c, { value: 'test', lineNumbers: false });
+ expect(ed.gutter).not.toBeNull();
+ expect(ed.gutter.style.display).toBe('none');
+ ed.destroy();
+ });
+
+ test('输入后行号更新', () => {
+ document.body.innerHTML = '';
+ const c = document.createElement('div');
+ document.body.appendChild(c);
+ const ed = new MarkdownEditor(c, { value: 'a\nb', lineNumbers: true });
+ ed.textarea.value = 'a\nb\nc';
+ ed.textarea.dispatchEvent(new Event('input', { bubbles: true }));
+ expect(ed.gutter.children.length).toBe(3);
+ ed.destroy();
+ });
+});
+
+describe('MarkdownEditor - v0.1.7 自动格式化', () => {
+ test('列表项行尾 Enter 自动延续 -', () => {
+ document.body.innerHTML = '';
+ const c = document.createElement('div');
+ document.body.appendChild(c);
+ const ed = new MarkdownEditor(c, { value: '- item' });
+ ed.textarea.setSelectionRange(6, 6); // 光标在行尾
+ ed.textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
+ expect(ed.getValue()).toContain('- ');
+ ed.destroy();
+ });
+
+ test('空列表项 Enter 结束列表', () => {
+ document.body.innerHTML = '';
+ const c = document.createElement('div');
+ document.body.appendChild(c);
+ const ed = new MarkdownEditor(c, { value: '- ' });
+ ed.textarea.setSelectionRange(2, 2);
+ ed.textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
+ expect(ed.getValue()).toBe('\n');
+ ed.destroy();
+ });
+
+ test('有序列表自动递增', () => {
+ document.body.innerHTML = '';
+ const c = document.createElement('div');
+ document.body.appendChild(c);
+ const ed = new MarkdownEditor(c, { value: '1. first' });
+ ed.textarea.setSelectionRange(8, 8);
+ ed.textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
+ expect(ed.getValue()).toContain('2. ');
+ ed.destroy();
+ });
+
+ test('引用块延续 >', () => {
+ document.body.innerHTML = '';
+ const c = document.createElement('div');
+ document.body.appendChild(c);
+ const ed = new MarkdownEditor(c, { value: '> quote' });
+ ed.textarea.setSelectionRange(7, 7);
+ ed.textarea.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
+ expect(ed.getValue()).toContain('> ');
+ ed.destroy();
+ });
+});
+
+describe('MarkdownEditor - v0.1.7 括号自动闭合', () => {
+ test('无选区输入 ( 自动闭合', () => {
+ document.body.innerHTML = '';
+ const c = document.createElement('div');
+ document.body.appendChild(c);
+ const ed = new MarkdownEditor(c, { value: 'hello', autoBrackets: true });
+ ed.textarea.setSelectionRange(5, 5);
+ ed.textarea.dispatchEvent(new KeyboardEvent('keypress', { key: '(', bubbles: true }));
+ expect(ed.getValue()).toBe('hello()');
+ expect(ed.textarea.selectionStart).toBe(6); // 光标在括号内
+ ed.destroy();
+ });
+
+ test('有选区输入 ( 包裹选中文本', () => {
+ document.body.innerHTML = '';
+ const c = document.createElement('div');
+ document.body.appendChild(c);
+ const ed = new MarkdownEditor(c, { value: 'hello world', autoBrackets: true });
+ ed.textarea.setSelectionRange(0, 5);
+ ed.textarea.dispatchEvent(new KeyboardEvent('keypress', { key: '(', bubbles: true }));
+ expect(ed.getValue()).toBe('(hello) world');
+ ed.destroy();
+ });
+
+ test('autoBrackets 关闭时不闭合', () => {
+ document.body.innerHTML = '';
+ const c = document.createElement('div');
+ document.body.appendChild(c);
+ const ed = new MarkdownEditor(c, { value: 'hello', autoBrackets: false });
+ ed.textarea.setSelectionRange(5, 5);
+ // jsdom 不模拟原生键盘插入,验证不抛错即可
+ expect(() => {
+ ed.textarea.dispatchEvent(new KeyboardEvent('keypress', { key: '(', bubbles: true }));
+ }).not.toThrow();
+ ed.destroy();
+ });
+});
+
+describe('MarkdownEditor - v0.1.7 outline', () => {
+ test('outline 关闭时不创建面板', () => {
+ document.body.innerHTML = '';
+ const c = document.createElement('div');
+ document.body.appendChild(c);
+ const ed = new MarkdownEditor(c, { value: '# hi\n## there', outline: false, mode: 'split' });
+ expect(ed.el.querySelector('.me-outline')).toBeNull();
+ ed.destroy();
+ });
+});
diff --git a/types/index.d.ts b/types/index.d.ts
index de7f954..051f2d8 100644
--- a/types/index.d.ts
+++ b/types/index.d.ts
@@ -1,4 +1,4 @@
-// Type definitions for MetonaEditor v0.1.6
+// Type definitions for MetonaEditor v0.1.7
// Project: https://git.metona.cn/MetonaTeam/MetonaEditor
// Author: thzxx