v61:笔记补齐成熟软件的块(Markdown 引擎加表格与任务清单,命令表扩全)
用户反馈:「笔记应该增加更多的块:段落、清单、图片、代码……去查成熟笔记
软件都分哪些块,我们都应该支持。」
对照 Notion / flomo / 思源 / Logseq 的块清单逐项核对(块类型仍只有
段落/图片两种——v29 用户拍板"清单并入 Markdown"),补齐的是
**Markdown 引擎与编辑器到 GFM 关键子集**:
app/Markdown.php
- GFM 表格:表头+分隔行两行联合识别(正文里有竖线不会误判成表格);
`:---`/`---:`/`:-:` 左右中对齐;列数向表头看齐(多丢少补,格子不错位);
单元格走 inline()(先 htmlspecialchars,`<script>` 只会被画成字面文本);
段落收集遇到"紧跟正文的表格"会断开(GFM 不强制空行)
- GFM 任务清单:`- [ ]`/`- [x]` 渲染成**只读**勾选框(笔记不是任务系统,
能点却不保存是假交互;要勾就改源码里的 x)
- 摘要清洗:表格分隔行/竖线/任务方括号不进列表卡片
assets/js/note_editor.js
- 命令表补齐:H1/H2/H3、有序列表、待办清单、删除线、行内代码、表格模板、
分割线(工具条与斜线菜单仍是同一份表)
- 🔴 修真 no-op:v29 的"逐行套"写着逐行实际原样返回,选中三行点列表只给
第一行挂前缀;现在行首记号型命令逐行套(空行跳过)
- 表格模板光标落在第一行数据的第一格,插完可直接打字
templates/notes/form.php:工具条 8→14 键(高频);全部命令斜线 `/` 可调
assets/css/app.css:.md-table / .md-task 样式(跟主题变量,不写死色)
刻意不做:代码语法高亮(引库=几 MB,用户当年已拍板轻量)、折叠块
(与"逐段编辑"打架,flomo 也没有)、页内子页/数据库(家书没有那个形态)。
验证:_fb_v61_ui.py 真机 30/0(含 XSS 夹具、紧跟正文断段、实时预览同源、
逐段编辑接口、v28 老语法零回退、历史 list 块不丢);
_b61_falsify.py 8 处注入 8/8 全红在预期断言;回归 b29 38/0、v55 30/0、v60 28/0。
This commit is contained in:
+43
-11
@@ -41,6 +41,7 @@
|
||||
|| document.body.getAttribute('data-media-base')
|
||||
|| 'media.php?k=');
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// ---------------------------------------------------------------------
|
||||
// 🔴 v29 第 7 条:Markdown 命令表(**工具条与斜线菜单共用这一份**)
|
||||
// ---------------------------------------------------------------------
|
||||
@@ -48,13 +49,29 @@
|
||||
// ⚠️ 为什么只有一份:如果工具条和斜线各写一套,迟早出现"按钮能插、
|
||||
// 斜线插不了"或者"两处语法不一样"(这正是用户抱怨的"要记住所有命令")。
|
||||
// 每个命令:wrap = 用来包住选中文本(含 %s),或 insert = 无选中时插的模板。
|
||||
// 🔴 v61(用户 2026-10-18):按成熟笔记软件的块清单补齐 ——
|
||||
// 标题一/二/三、有序列表、任务清单、表格、分割线。
|
||||
// ⚠️ 这仍然是**往段落块里插 Markdown 记号**(不是新增块类型):
|
||||
// v29 用户拍板"清单并入 markdown,块只留 段落/图片",服务端白名单不变。
|
||||
// line:true = "行首记号"型命令 —— 选中多行时**逐行**套(选中三行点「列表」
|
||||
// 应得到三个列表项;只套第一行是老实现的缺陷,这次一并修)。
|
||||
var CMDS = {
|
||||
b: { word: '粗体', keys: ['bold', 'b'], wrap: '**%s**', tip: '加粗' },
|
||||
i: { word: '斜体', keys: ['italic', 'i'], wrap: '*%s*', tip: '斜体' },
|
||||
h2: { word: '标题', keys: ['h2', 'head'], wrap: '## %s', tip: '小标题' },
|
||||
ul: { word: '列表', keys: ['ul', 'list'], wrap: '- %s', tip: '列表项' },
|
||||
quote: { word: '引用', keys: ['quote', 'q'], wrap: '> %s', tip: '引用' },
|
||||
code: { word: '代码', keys: ['code'], wrap: '```\n%s\n```', tip: '代码块' },
|
||||
s: { word: '删除线', keys: ['strike', 's'], wrap: '~~%s~~', tip: '删除线' },
|
||||
h1: { word: '标题一', keys: ['h1', 'title'], wrap: '# %s', line: true, tip: '一级标题' },
|
||||
h2: { word: '标题二', keys: ['h2', 'head'], wrap: '## %s', line: true, tip: '小标题' },
|
||||
h3: { word: '标题三', keys: ['h3'], wrap: '### %s', line: true, tip: '三级标题' },
|
||||
ul: { word: '无序列表', keys: ['ul', 'list', 'bullet'], wrap: '- %s', line: true, tip: '项目符号列表' },
|
||||
ol: { word: '有序列表', keys: ['ol', 'number', 'num'], wrap: '1. %s', line: true, tip: '编号列表' },
|
||||
todo: { word: '待办清单', keys: ['todo', 'task', 'check'], wrap: '- [ ] %s', line: true, tip: '任务清单([x] 已完成)' },
|
||||
quote: { word: '引用', keys: ['quote', 'q'], wrap: '> %s', line: true, tip: '引用' },
|
||||
code: { word: '代码块', keys: ['code', 'codeblock'], wrap: '```\n%s\n```', tip: '围栏代码块' },
|
||||
icode: { word: '行内代码', keys: ['inline', 'icode'], wrap: '`%s`', tip: '行内代码' },
|
||||
table: { word: '表格', keys: ['table', 'tf'],
|
||||
// %s = 光标落点(第一行数据的第一格),插完可以直接打字
|
||||
insert: '| 列一 | 列二 | 列三 |\n| --- | --- | --- |\n| %s | | |\n', tip: '插入表格' },
|
||||
hr: { word: '分割线', keys: ['hr', 'divider', 'split'], insert: '\n---\n', tip: '横向分割线' },
|
||||
link: { word: '链接', keys: ['link', 'url'], wrap: '[%s](https://)', tip: '链接' },
|
||||
img: { word: '图片', keys: ['img'], insert: '+ 图片(点左侧「+ 图片」按钮选文件)' }
|
||||
};
|
||||
@@ -72,14 +89,29 @@
|
||||
if (c.insert) {
|
||||
out = c.insert;
|
||||
ta.value = v.slice(0, s) + out + v.slice(e);
|
||||
ta.selectionStart = ta.selectionEnd = s + out.length;
|
||||
// 🔴 v61:insert 里有 `%s` 的(表格),光标落在**第一个空格子**而不是整段末尾
|
||||
var mark = out.indexOf('%s');
|
||||
if (mark >= 0) {
|
||||
ta.value = v.slice(0, s) + out.replace('%s', '') + v.slice(e);
|
||||
ta.selectionStart = ta.selectionEnd = s + mark;
|
||||
} else {
|
||||
ta.selectionStart = ta.selectionEnd = s + out.length;
|
||||
}
|
||||
} else if (sel !== '' && c.line) {
|
||||
// 🔴 v61:**行首记号型**命令 + 有选中 → **逐行**套(选中三行点「列表」
|
||||
// 得到三个列表项)。v29 的老实现这里写着"逐行"、实际是个原样返回的
|
||||
// no-op(`map(function (l) { return l; })`),所以选中三行只会给
|
||||
// 第一行挂上前缀 —— 这次把它做成真的。
|
||||
// ⚠️ 只跳过空行(空行挂 `- ` 会渲染出一个空列表项)。
|
||||
out = sel.split('\n').map(function (l) {
|
||||
return l.trim() === '' ? l : c.wrap.replace('%s', l);
|
||||
}).join('\n');
|
||||
ta.value = v.slice(0, s) + out + v.slice(e);
|
||||
ta.selectionStart = s;
|
||||
ta.selectionEnd = s + out.length;
|
||||
} else if (sel !== '') {
|
||||
// 有选中 → 包住它(多行时**逐行**套,这样"选中三行 → 列表"会得到三个列表项)
|
||||
out = c.wrap.indexOf('%s') >= 0
|
||||
? c.wrap.replace('%s', sel.indexOf('\n') >= 0
|
||||
? sel.split('\n').map(function (l) { return l; }).join('\n')
|
||||
: sel)
|
||||
: sel + c.wrap;
|
||||
// 行内型命令(粗斜体/行内码/链接):整段包住;跨行也包(渲染层支持)
|
||||
out = c.wrap.indexOf('%s') >= 0 ? c.wrap.replace('%s', sel) : sel + c.wrap;
|
||||
ta.value = v.slice(0, s) + out + v.slice(e);
|
||||
ta.selectionStart = s;
|
||||
ta.selectionEnd = s + out.length;
|
||||
|
||||
Reference in New Issue
Block a user