Chuyển đến nội dung chính

dsh-composer-markdown

Đã xác minh

dsh-composer-markdown · v0.1.4 · MIT · Giao diện web

DSH web 插件:对话输入框 Markdown 编辑增强 —— 无序/有序列表自动续行、列表层级缩进阶梯(Tab 降级 / Shift+Tab 或前缀后 Backspace 升级,整棵子树联动,降级限父级下一级,顶层回归普通文本)、行内代码样式(保留字面反引号)、代码块围栏自动闭合与围栏内 Enter 保护。仅增强编辑态视觉与按键行为,发送文本保持字面 Markdown 逐字节保真。/ DSH web plugin: markdown editing aids for the composer — l

Cài đặt

dsh plugin add dsh-composer-markdown

Xác nhận layer đã áp bằng dsh --profile default --dump-config — xem hướng dẫn cài plugin.

Mã nguồn

Thẻ

Readme

dsh-composer-markdown

English · npm · GitHub

npm license node CI stars

一句话简介:DSH(DeepSeek Harness)Web 纯客户端插件,为对话输入框(composer)提供 Markdown 编辑增强 —— 列表自动续行与序号规整、行内代码样式(反引号渲染后隐藏)、代码块围栏自动闭合与原子化交互。所有编辑手势做在 Shift+Enter 上;Enter 保持 DSH 原生「直接发送」语义,从不拦截。除「有序列表序号规整」外一切只作用于编辑态视觉与按键,发送文本始终保持字面 Markdown、逐字节保真。

  • 平台:web(DSH Web GUI,dsh plugin --profile web)
  • 形态:纯客户端插件 —— host 半为空操作载体,零运行时依赖,可在「设置 → 插件」中随时开关
  • 测试:475 个断言全部通过(零依赖测试器:产物新鲜度 + 纯逻辑单测 + bundle 形状断言 + 可选宿主契约冒烟)
  • 许可:MIT

目录

用途与适用场景

DSH 的 composer 是一个 Lexical 纯文本编辑器,原生只提供「Enter 发送 / Shift+Enter 软换行」——写 Markdown 全靠手敲字面字符。本插件把输入框的 Markdown 编辑体验补齐到成熟聊天产品的水平:

  • 在输入框里写结构化内容:列表、步骤、代码片段、代码块,不用离开键盘去点工具栏;
  • 让字面 Markdown 变得可读:• 圆点标记、等宽序号、行内代码底色、成对的 ``` 渲染成一个真正的代码块 —— 所见接近发送后消息的渲染效果;
  • 保住「纯文本」的契约:一切增强都是编辑态视觉层,发出的就是你敲的字面 Markdown,逐字节一致 —— 模型侧、消息渲染侧、剪贴板投影完全不受影响;
  • 不改变任何原生习惯:Enter 照常发送,/、@ 菜单、@xxx 引用 chip、图像粘贴、IME 中文输入全部照旧。

不适用:需要富文本(非 Markdown)编辑、或希望在发送阶段改写/美化 Markdown 语法的场景 —— 本插件刻意不做任何发送态变换。

功能一览

  你输入                     编辑态呈现                        发送文本
─────────────────────────────────────────────────────────────────────
  - 苹果                   • 苹果   (圆点标记)                - 苹果
      ⇧↵ (Shift+Enter)    - ␣      (下一行自动带前缀)          …
  1. 第一步                1. 第一步 (等宽序号)                1. 第一步
      ⇧↵                   2. ␣     (自动递增,组内恒连续)       …
  `code`                   code     (等宽+底色,反引号隐藏)      `code`
  ```ts ⇧↵                ┌─────────────────┐                 ```
                          │ ts 徽标          │                 (空行)
                          │ ␣ (光标在此)     │                 ```
                          └─────────────────┘

按功能域:

域 能力
列表 无序/有序续行(任意视觉行与任意嵌套深度,含软换行与多行粘贴行)、光标处截断续行、空项退出、组内序号恒连续(删行接上/隔断重起/合并累加/嵌套感知)、中间插入整体顺延、层级缩进阶梯(Tab 降级 / Shift+Tab 或前缀后 Backspace 升级,整棵子树递归联动,顶层取消列表回归普通文本;降级上限 = 父级下一级,不可无限降级)、前缀原子删除(缩进并入原子)、方向键原子跨越(含缩进)、原子内部不可停留、标记差异化渲染(• 圆点 / 等宽序号)
行内代码 成对反引号内部文本呈行内代码样式、反引号闭合瞬间隐藏(零步进隐形,保留字体度量)、选区覆盖时显形(所选即所见)
代码块 ``` / ```ts + Shift+Enter 自动闭合骨架、行首敲 ``` 的保留内容流、成对提交制渲染(整块代码观感 + 语言徽标浮层 + 标记行零高度)、块边界一次解包(去围栏保正文)、↑/↓ 跳过标记行、围栏内 Shift+Enter 插入新段、任意行行首开块(软行提升——引用 chip 内嵌换行只阻断其自身边界,其余边界照常提升,v0.1.3)、孤儿闭合标记自动清理

安装

前提

  • DSH 0.1.7-rc.x(已对 0.1.7-rc.2 实测;composer 为 Lexical 纯文本编辑器 + @lexical/plain-text,lexical 0.49);
  • Node.js ^22.19.0 || >=24.0.0(与 DSH 本体的支持窗口一致,包的 engines 字段与其对齐);
  • 现代桌面浏览器(Chrome / Edge / Firefox / Safari)。

安装插件

包内声明了 dsh.bundle.patch(cordis.patch.yml:一条 insert 行,挂载 host 半空操作 entry,使插件出现在「设置 → 插件」中)。

本地检出(开发常用):

dsh plugin --profile web add link:/absolute/path/to/dsh-composer-markdown

从 npm registry:

dsh plugin --profile web add dsh-composer-markdown

从 GitHub(构建产物 client.js 随仓库提交,即使 pnpm 跳过 prepare 构建脚本插件也能正常加载;建议用 #<sha> 固定 commit,避免后续 push 改变实际运行的代码):

dsh plugin --profile web add github:chendefine/dsh-composer-markdown

pnpm ≥ 10 会在用户显式放行前阻塞 git 依赖的 prepare 脚本。若希望安装时从 src/client/ 重新构建 client.js,请把 pnpm 打印的键(如 dsh-composer-markdown: true)加进 profile 的 pnpm-workspace.yaml 的 allowBuilds 下再重新 add —— 这一步等于授权该包在你机器上于安装期执行代码,请仅放行信任的源。

或经 DSH 插件市场(设置 → DSH 插件市场)—— 给仓库打上 dsh-plugin topic 即被自动收录。

安装后重启 dsh web 服务并刷新浏览器页面生效。本地 link 方式安装后,修改 src/client/ 源码并执行 node scripts/build.mjs 重新生成 client.js,开发态下 HMR 会自动热载重新生成的产物(无需刷新页面;若 HMR 未运行则刷新一次)。

开关与卸载

  • 开关:设置 → 插件 → composer-markdown,禁用即恢复全部原生行为(刷新后生效);
  • 卸载:dsh plugin --profile web remove dsh-composer-markdown。

禁用/卸载时自动清理:document 监听器、注入的样式标签,并清除编辑器内所有代码样式标志位(草稿文本内容不变)。

使用

所有编辑手势在 Shift+Enter 上;Enter = 原生发送,从不拦截;未命中任何场景的 Shift+Enter = 原生软换行。Tab / Shift+Tab 仅当光标行是列表项时被认领(层级阶梯),其余场景保持原生(焦点移动等)。普通 Backspace / Delete / 方向键仅在恰好命中下述边界时被认领,其余保持原生逐字符/逐字符移动。

列表

操作 触发 行为
无序/有序续行 行首 - / * / N. (任意缩进深度)后输入内容,按 Shift+Enter 下方新行自动带 {indent}- / {n+1}. 前缀(嵌套层级原样保留),光标停在前缀后;段落首行、软换行行、多行粘贴行任意视觉行均生效
行中间截断 光标在列表项内容中间按 Shift+Enter 从光标处截断:光标后的内容移到新 item、接在新前缀之后(字节保真);光标在行首/前缀内部/行尾时维持「下方新增整行」
中间插入顺延 在有序组中间 Shift+Enter 插入新项 新行拿到 n+1,其下同组所有成员依次 +1(1. 2. 3. 在 1 与 2 之间插入 → 1. 2. 3. 4.),整组保持连续且不重复;插入与顺延同属一个撤销步
序号规整(不变式) 任意改动之后(删除/插入行、空项退出、粘贴、撤销/重做) 每个有序组(连续有序行、同缩进、围栏外)恒为 1. … n. 从首成员连续编号:删行接上;组被普通行/空行/无序行隔断 → 后一组从 1. 重起;隔断行被删、两组相连 → 合并累加;更深缩进只挂起外层组(嵌套子列表不打断外层,外层缩进回归时续接原计数),嵌套组自身独立规整。有意取舍:组不能保留非 1 起始号或手动跳号——手改序号立即回弹(连续性即契约)
空项退出 只有前缀的空列表项上按 Shift+Enter 移除前缀,保留空段(软行上保留空行)—— 连按两次即可干净退出列表;光标留在清空后的本行(v0.1.2)——空项在尾部、段中或块首行均如此,既不回上一项行尾、也不跳下一行行首
层级缩进(降级) 光标在列表项那一行的任意一处(含内容中间)按 Tab 该项连同下方全部更深层级(嵌套子列表、更深缩进的续行文本)每行缩进 +2 空格;同层兄弟项与其后内容不动;光标在行内相对位置随行平移;整棵子树一次按键、一个撤销步;嵌套有序组随后按不变式独立规整(如 2. 挂到上层后重排为嵌套组的 1.)。降级有上限(v2.9):任何项最多只能降到其父级(上方最近的列表项,普通行跳过、空行/围栏隔断)的下一级——已处于父级下一级的项、以及列表首项(上方无可嵌套的父级)按 Tab 为认领但不动的空操作(焦点不会因此跳出输入框),不会无限降级
层级缩进(升级/取消) 同上位置按 Shift+Tab,或折叠光标恰在前缀原子之后(内容首)按 Backspace 嵌套项(缩进 ≥1 空格)→ 该项及全部子层级各 −2 空格(奇数缩进按 min(2, 缩进) 取整到 0);顶层项 → 取消列表:删除整个前缀原子,只留正文成普通文本行(光标留在该行,v0.1.2),子层级仍各升一级。取消列表留下普通行会隔断有序组,其后同缩进成员按不变式从 1. 重起。长按 Tab 降一级后停在层级上限(认领但不动);长按 Shift+Tab 每次重复升一级——两键焦点都不会从列表行跳走
前缀原子删除 折叠光标恰在列表项行首(整个原子之前)按 Delete 缩进 + 前缀整体一次删净(如 ␣␣1. 六字符一并消失),该行回归顶层普通文本,独立撤销步;缩进无法被逐字符选中删除
前缀方向键原子跨越 折叠光标贴在任意列表原子边界按 ← / → → 在行首一次跨过整个 ␣␣1. / ␣␣- (缩进含在内)落到内容首字符左侧;← 在前缀后一次跨回行首;光标已落入原子内部(含缩进空格内,点击/纵向移动)时按方向键直接跳出 —— 键盘永不逐字符穿过缩进或前缀内部。Shift+方向键保持原生(选区仍可精确覆盖序号字符手动改号)
标记差异化渲染 草稿中任意视觉行行首的 - / * / N. (围栏外,含空前缀) 有序序号换用等宽字体;无序横线/星号渲染为 • (圆点 + 空格为一个整体标记单元);标记字节原样保留在草稿/复制/发送文本中;选区真正覆盖标记字符时显形原始字符
原子内部不可停留 ↑/↓、鼠标点击等任意途径把折叠光标落入原子内部(含缩进空格之间) 光标被移到最近的原子边界(平局取行首边界);边界本身(行首/内容首)是合法停留点;选区不受影响

行内代码

  • 段内出现成对反引号 `非空内容`,且内容不跨行、首尾紧邻定界的字符非空白(含全角空格/NBSP)时:内部文本呈行内代码样式(等宽字体、浅底、圆角,复用 DSH 主题 token);
  • 配对反引号闭合瞬间即隐藏(不可见、不占宽,仍逐字节保留在草稿与发送文本中);光标移入配对内部或紧邻两侧时保持隐藏;
  • 只有选区真正覆盖到某个反引号字符时(Shift+方向键、全选等)该配对才临时显形(所选即所见),选区收起后重新隐藏;跨段落选区(Ctrl+A)按各段分别判定,段边界不漏显;
  • 不满足条件的配对完全惰性(反引号保持可见,不影响后续配对); `` 等多重反引号定界不识别(朴素两两配对)。

代码块

操作 触发 行为
骨架闭合 光标所在行恰为 ```(可带语言标识 ```ts)且光标位于标记串之内/之后,按 Shift+Enter 插入三行骨架 ``` / 空行 / ```,光标落在空行行首;**任意一行**的行首输入都生效(软换行/多行粘贴产生的行同样识别);光标在标记串**之前**(裸 ``` 行 offset 0)为普通文本,保持原生软换行
保留内容流 在已有内容的行行首输入 ``` 后(光标仍紧随其后)按 Shift+Enter 从 ``` 之后截断:标记行保留 ```,插入空行 + 闭合骨架,**原行 ``` 之后的全部内容移到闭合标记下方**(块外、逐字节保留);语言标识流不受影响(```ts 行尾仍得到带徽标的骨架)
成对渲染 草稿中出现成对 ```…``` 包裹区域 整块呈一个代码块:内部代码底色 + 等宽字体(与消息渲染侧 CodeBlock 同一套主题 token 与几何);首行标记零高度,语言标识以浮层徽标显示在块内右上角;``` 标记完全不可感知(不可见、光标不停留);**未闭合的 ``` 一律普通文本** —— 行中键入 ``` 不会把下方内容吞进代码块
一次解包 光标在块边界按 Backspace / Delete(内容首行行首/块后一段行首 ← 退格;块前一段行尾/内容末行行尾 → 删除键) 一次按键去掉一对 ``` 标记行:代码块解除渲染,块内正文逐字节保留为普通段落,光标原地不动,独立撤销步
退出块 内容末行行尾按 ↓(块为草稿末尾时自动生长一个空段) 光标落到块下方新段,继续输入普通文本;↑/↓ 在块边界垂直移动时直接跨过标记行;标记行驱逐光标时落点为文本锚点(v0.1.3),被送回正文边缘后 ↑/↓ 依然可用——块顶/块底绝不成为键盘死角
围栏内换段 光标在围栏内(含闭合行)按 Shift+Enter 插入新段(而非软换行),从光标处截断;普通 Enter 在围栏内也照常发送

与原生行为的共存

  • Enter:保持 DSH 原生「直接发送」,本插件从不拦截(列表行上、围栏内也一样);
  • Ctrl/Cmd+Enter:保持原生「加速提交」,不拦截;
  • Shift+Enter(未命中场景):保持原生软换行(<br>);
  • Tab / Shift+Tab(非列表行、围栏内、选区状态、带修饰键):完全保持原生(浏览器焦点移动等),从不拦截;触发菜单打开时同样让位;列表行上的长按重复与单击同样重新规划(绝不落到原生焦点移动);
  • /、@ 触发菜单打开时:对 Enter 系按键完全让位;
  • 中文输入法(IME):组合中的按键不触发任何本插件行为(三信号防护:isComposing / keyCode 229 / compositionend 后 10ms 窗口,与 DSH 输入机自身防护一致);
  • 斜杠命令认领、@ 引用 chip、图像拖放/粘贴、busy/locked 状态、问答卡片与审批卡片均不受影响;
  • 与其他 composer 插件共存:捕获阶段先到先得 —— 已被 preventDefault 的按键本插件让位;本插件消费的按键 stopImmediatePropagation。

技术架构

双端结构

DSH 插件分 host(node)半与 browser 半,本插件是极端的纯客户端形态:

┌─ host 半 (node) ──────────────────────────────────────────────┐
│ index.js          空操作载体:给 bundle 一个可导入的 entry 行,  │
│                   使插件出现在「设置 → 插件」中可开关            │
└───────────────────────────────────────────────────────────────┘
┌─ browser 半 (web) ─────────────────────────────────────────────┐
│ client.js         单文件装载产物(DSH 装载器只认单文件 bundle),│
│                   由 src/client/ 15 个模块经 scripts/build.mjs  │
│                   拼装生成并入库;经                             │
│                   window.__ModuleLoader__.load({id, factory})   │
│                   注册,内部自带 CommonJS 风格模块注册表          │
└───────────────────────────────────────────────────────────────┘

源码分层(src/client/,自上而下单向依赖)

constants.js     常量 + 行文法正则 + CSS 类名(样式表选择器由它拼装,杜绝漂移)
editor.js        宿主契约缝:唯一触碰宿主下划线内部
                 (__lexicalEditor/_nodes/_nodeMap/_selection/_compositionKey)的模块
grammar.js       行文法基底:视觉行、围栏区间 + 提交制覆盖、共享光标行解析、
                 选区覆盖显形规则 —— 每次读取只推导一次
doc.js           文档读取:块/叶子扁平几何 + 光标/选区映射
                 (committed/live 两读法)+ 空壳块查询
fence-plan.js    纯围栏投影与决策(行/块角色、软行提升、围栏对、孤儿清理、
                 原子键、光标归位)
code-plan.js     纯行内代码核 + 每块 span/定界投影
enter-plan.js    纯 Shift+Enter 仲裁(先围栏后列表的一棵决策树)
list-plan.js     纯列表规划(状态驱动重编号、标记字形、原子删/跨越/归位、
                 合并/脱离、顺延、重锚)
analysis.js      一次 analyzeDraft(texts):模型 + 全部域投影
edits.js         编辑代数:全部活节点变更的唯一所在(平坦区间切除/软行提升/
                 段落切分/数字改写/格式形状)
style-sheet.js   CSS 文本(由类名常量拼装)+ 样式标签生命周期
present.js       呈现层:块标记(围栏类/徽标)+ 唯一的字形标记引擎
                 ({at, class, reveal} 表)+ stripDom
gestures.js      键盘面:守卫 + 手势策略表(prepare/apply/history 三步)
                 + 唯一 claim 路径
restyle.js       收敛引擎:有序 STAGES 表(规范化 → DOM 着色 → 修复 →
                 光标归位 → 代码/标记形状),每轮一次分析、至多一次结构提交
                 + 跨扫描记忆 + 环路守卫,实例状态非模块全局
index.js         组合根:生命周期 + __internals 组装

五条贯穿性设计

所有功能不是一条条 feature 各自铺管线,而是同一组正交抽象上的行:

  1. 一条行文法:视觉行切分(段落按 \n 切行,软换行 \n 与粘贴 \n 等价)、围栏配对与提交制覆盖、光标行解析只在 grammar.js 推导一次;列表/行内代码/围栏/Enter 仲裁全部消费同一模型,构造上不可能互相漂移。
  2. 一次草稿分析:analyzeDraft(texts) 把围栏角色、软行提升、行内代码 span/定界、列表标记字形、重编号不变式组装为单一对象 —— 各投影看见同一些行、同一份围栏覆盖。
  3. 一套编辑代数:plan(纯数据,单测覆盖)与 apply(活节点操作,edits.js 独占)彻底分离;手势策略与收敛引擎都不直接碰节点。
  4. 一个呈现引擎:编辑态视觉只有两种形状 —— 块标记(围栏段落类/徽标)与字形标记(单字符叶 + 类 + 显形规则);隐藏反引号与列表标记样式共用同一引擎与同一显形谓词,新增一种字形是加一行表项,不是加一个 pass。
  5. 一张表驱动的双驱动:键盘面是策略表(每个键族一条 prepare/apply/history),收敛面是阶段表(每阶段一个纯 plan 函数);新增行为是向表里加行,而不是再铺一条管线。

关键技术细节

键盘路径(为什么是 document 捕获阶段):DSH 的 Enter 命令处理器以 CRITICAL 优先级吞掉一切非 Shift Enter(提交),命令层无插入口;而 Lexical 的 Enter 派发源自根元素 keydown 监听。因此插件在 document 捕获阶段监听 keydown —— 必然先于 Lexical —— 仅在命中编辑场景时 preventDefault() + stopImmediatePropagation() 阻断原生软换行,改用 editor.update() 完成插入。普通 Enter 从不进入该路径。

隐藏字形技巧(零步进隐形):Lexical 每个文本节点渲染为独立元素。样式写入先把反引号/标记字符两侧切开(splitText)并把该单字符叶子标记为 unmergeable(Lexical 归一化会把同格式相邻叶子合并回去,只有这个 detail 位能保住隔离),随后的 DOM 扫描用 editor.getElementByKey() 给这些叶子挂类:等宽字体 + letter-spacing: -1ch 恰好抵消字符步进、color: transparent 隐去墨迹 —— 保留真实字体度量。不能用 display: none(无盒子,浏览器无法在其旁锚定光标),也不能用 font-size: 0(光标高度取自其锚定文本节点的字体,会得到零高度不可见光标)。无序圆点由 ::after 伪元素渲染(排在零步进横线之后,行首光标因此渲染在整个 • 单元之前)。

围栏成对提交制:只有闭合的围栏才渲染;未闭合区间在覆盖判定中只占标记行自身 —— 行中键入 ``` 既不把下方内容渲染进代码块,也不关掉下方行的列表续行/行内代码配对。

软行提升(任意行开块):围栏文法以段落为单位,而软换行是段内 <br>、多行粘贴把字面 \n 拼进文本节点。restyle 第 1 阶段把与围栏相邻的软行边界提升为真正的段落边界(文本投影恒等,草稿/发送文本逐字节不变),使 ``` 在任意一行行首输入都能提交成块。

收敛引擎:restyle 由编辑器 update 监听器驱动,每轮读一次文档、跑一遍阶段表、至多提交一次结构更新;提交再次触发监听器,下一轮发现无差异即停(写只在有差异时发生,幂等收敛)。自触发写入限速(滚动窗口 16 次/秒)兜底环路;IME 组合中直接让路。格式/样式写入带 history-merge 标签,不污染撤销栈;结构性手势(插入+顺延、合并/脱离+改号)是离散撤销步,一次 Ctrl+Z 整体还原。

宿主契约与安全降级:lexical 并非模块表的共享模块,插件也不能打包第二份 lexical(双副本会分裂模块态与节点类)。所有节点操作都通过宿主编辑器实例完成:editor._nodes 取真实节点类、editorState._nodeMap/_selection 读取与定位、全部使用宿主节点自身的方法(insertAfter/append/select/splitText/spliceText/setFormat)。这些内部访问点与 __lexicalEditor 同属事实契约,集中在 editor.js;升级破坏契约时插件安全降级为无操作(console.warn 一次)。

发送保真:除「有序列表序号规整」(它本身就把序号改写为连续后的字面值,所见即所发)外,其余各条线都不触碰文本内容 —— 发送文本 = composer 的 clipboard 投影 = 逐文本节点 getTextContent(),样式标志位与 DOM 类不影响序列化结果。

兼容性

  • DSH 0.1.7-rc.x(已对 0.1.7-rc.2 实测;composer 为 Lexical 纯文本编辑器 + @lexical/plain-text,lexical 0.49);
  • 现代桌面浏览器(Chrome / Edge / Firefox / Safari);
  • 与其他 composer 插件共存:捕获阶段先到先得(见「与原生行为的共存」)。

已知限制

  • 列表标记样式与反引号隐藏均为编辑态视觉层:字符本身仍在草稿/复制/发送文本中;选区覆盖时恢复原始字样与字宽。无序圆点与原横线的字宽不完全一致(圆点用等宽字体 ::after),极端窄列下可能比原 - 略宽,属可接受的排版差异。
  • 纵向归位有短暂瞬态:↑/↓ 落入标记内部后,驱逐发生在随后的 restyle 扫描(微任务级),极端情况下可能见到一帧内部位置;←/→ 按键期仲裁无此瞬态。
  • 代码块语言标识不可直接编辑(标记行不可达):修改语言 = 一次 Backspace 解包(保留正文)后重敲围栏。
  • 未闭合的 ``` 一律不渲染(含粘贴/草稿恢复出的),在标记行上按 Shift+Enter(或补上闭合 ```)即成块。
  • /```` x`` ```` 等多重反引号定界不识别(朴素两两配对)。
  • 有序列表规整按现状重写一切非连续序号:手动跳号/非 1 起始会回弹;更深缩进的行只挂起外层组(空行、围栏、缩进不深于本组的普通行/无序行仍断组);同一组超过 9 位序号(≥10 亿行)不适用。
  • 层级阶梯的子树边界按「更深缩进」判定:空行、围栏区域、缩进不深于该项的行都终止子树(与规整分组的边界语义一致);空行后的深层缩进行不算前项的子层级,不会被联动平移。
  • 降级上限(v2.9):父级 = 上方最近的列表项行(普通文本行跳过不算,空行/围栏区域隔断查找);项最多处于父级下一级(父级缩进 +2)。已达上限的项与列表首项按 Tab 是「认领但不动」的空操作——不会无限降级,也不会因此让焦点跳出输入框;要再深一级,须先让其父级(或上方兄弟)先降级。升级方向无上限(0 级为自然下限)。
  • 取消列表(顶层升级)会隔断有序组:留下的普通行使下方同缩进成员按不变式从 1. 重起——这是既定编号契约的直接后果,非 bug。v2.8 起该手势取代了旧的「有序项退格合并进上一行」行为。
  • 缩进/前缀原子的「不可选中、不可删除」为稳态键盘体验:折叠光标无法进入原子内部(方向键跨越、越界驱逐、行首 Delete 整体删除);拖拽/Ctrl+A 等范围选区仍可覆盖原子字符(与序号手动改号的「所选即所见」规则一致),此时原生删除可用。
  • 行内代码内容若恰为 lexicon 中的 /name、@name 文本引用 token,样式可能被 chip 着色覆盖(文本内容不受影响)。
  • 引用 chip(@xxx)内部含换行时无法在其上提升软行(chip 为原子节点);这类行的围栏判定退化为整段判定。
  • 完全无显形的行内代码方案需 chip 节点(技术文档路线 B,留作后续演进)。

开发须知与规范

构建与测试

node scripts/build.mjs                       # 由 src/client/ 重新生成 client.js
node scripts/build.mjs --check               # 仅校验 client.js 是否与源码同步
node tests/run-tests.mjs                     # 产物新鲜度 + 纯逻辑单测 + bundle 形状断言
DSH_CHECKOUT=/path/to/dsh node tests/run-tests.mjs   # 追加宿主契约冒烟(grep 断言)

npm run build / npm test 为等价快捷方式。手工验收清单见 tests/e2e-recipe.md(可用 playwright-cli 驱动,键盘事件需是真实 keydown)。设计与证据文档:仓库根 dsh-composer-markdown-view-tech.md(需求、证据索引、已否决方案)。

源码规范

  • 不要直接改 client.js:它是 scripts/build.mjs 生成的产物且入库,测试会校验新鲜度;改 src/client/ 后重新构建并提交再生的产物。DSH 装载器把每个插件的 ./client 导出作为单个 <script src> 加载(无多文件插件支持),这是存在构建步骤的唯一原因。
  • 模块约定:每个 src/client/*.js 文件是一个 CommonJS 风格的模块体(接收 module, exports, require,require('./xxx') 解析到 bundle 内部注册表);构建时按 MODULES 表顺序原样拼接,不做转译或重排版。零运行时依赖 —— 不得引入任何外部包。
  • 绝不 import/bundle lexical:lexical 不是模块表的共享模块,打包第二份会分裂模块态与节点类。一切节点操作经宿主编辑器实例(editor.js 是唯一契约缝);新增宿主内部访问点时加进 editor.js 并保持「破坏契约 → warn 一次 → 无操作」的降级。
  • 扩展方式是向表里加行:新按键手势 → 在 gestures.js 的策略表加一条 {keys, guard, prepare, apply, history};新收敛阶段 → 在 restyle.js 的 STAGES 加一个纯 plan 函数;新字形样式 → 在 present.js 的字形标记表加一行 {at, class, reveal}。不铺新管线。
  • plan/apply 分离:规划器必须是当前草稿的纯函数(无跨扫描记忆,围栏孤儿清理的配对记忆除外),经 __internals 暴露给零浏览器测试器单测;活节点变更只写在 edits.js。
  • 收敛纪律:写只在有差异时发生;每轮至多一次结构提交;样式/光标写入带 history-merge,结构性手势用离散撤销步;尊重 MAX_WRITES_PER_SECOND 环路守卫与 IME 让路。
  • 样式复用 DSH 主题 token(--ds-font-family-code、--dsw-alias-markdown-inline-code、--dsw-alias-markdown-code-block*),选择器由 constants.js 的类名常量拼装,禁止硬编码颜色或让 CSS 与类名漂移。

许可

MIT(见 LICENSE)。