TipTap 排版问题调研:Pretext 与测量层
返回 🧭 富文本排版与布局 · 🗂 富文本编辑专题
阅读位置:这是一份跨 CSS、编辑器模型、虚拟化和测量架构的调研,不是第一次配置 TipTap 的起点。
- CJK 换行、长词、代码块:先完成一个最小 TipTap 编辑器,再直接读第 5、6 节。
- mention、chip、inline atom:先理解 Schema 与 NodeView,再读第 7 节。
- 分页、虚拟化、测量或 Pretext:先读 性能与可访问性 的性能部分,再从第 8 节继续。
任何编辑态方案都必须保留浏览器对 selection、caret 和 IME composition 的主导权;Pretext 是预测与辅助层,不是可编辑 DOM 的替代品。
0. 一句话结论
使用 TipTap 时的排版问题,不能只靠 TipTap 解决,也不应让 @chenglou/pretext 替代 TipTap / ProseMirror 的编辑视图。更稳的架构是:
TipTap / ProseMirror 管编辑模型、事务、选区、IME 与 DOM 编辑;浏览器 CSS 管编辑态真实排版;Pretext / Canvas 测量层只做预测、虚拟化、分页草算和只读渲染辅助;最终边界用真实 DOM reconciliation 修正。
如果把 Pretext 当成「编辑器布局引擎」,会撞上 selection、caret hit testing、IME composition、copy / paste、screen reader 与浏览器真实 shaping 的复杂边界。如果把它当成「测量与预排版服务」,它正好能补 TipTap 在大文档、高度预测、只读卡片、分页预估上的短板。
1. 问题定义:「排版问题」其实分成四类
1.1 编辑态的文本排版
典型问题:
- 中文、日文、韩文换行不自然。
- 英文长词、URL、token 撑破容器。
- 段落内
inline code、link、mention、chip 影响行高或换行。 - 代码块到底横向滚动还是自动换行。
- 输入法合成期间光标跳动、选区错位。
这类问题的约束是:用户正在 contentEditable 里编辑。任何排版策略都必须尊重浏览器编辑模型与 ProseMirror 的 DOM 同步机制。这里优先级最高的是正确性,不是视觉上最漂亮。
1.2 展示态 / 预览态排版
典型问题:
- 富文本卡片需要先知道高度,避免 masonry / feed 闪动。
- 聊天气泡需要算自然换行后的尺寸。
- 只读详情页希望做更精细的排版,比如避让图片、做多列、做摘要省略。
这类问题不需要处理光标、输入法和实时 selection,因此可以使用更主动的测量和自绘策略。Pretext 在这里更有空间。
1.3 长文档性能与虚拟化
典型问题:
- 文档很长时,全部挂载 DOM 导致输入延迟。
- 每次输入都读
getBoundingClientRect()/offsetHeight,触发 forced synchronous layout。 NodeView太多,React / Vue 同步 mount 和 update 成本高。- 需要虚拟滚动,但滚动条高度又依赖未知内容高度。
这类问题的核心不是「文字好不好看」,而是能否在不读大量 DOM 的情况下得到足够准确的高度估计。
1.4 分页 / 打印 / 类文档布局
典型问题:
- 要在编辑器里显示 A4 / Letter 页面。
- 标题、段落、图片、表格不能随便跨页。
- 编辑过程中页边界要跟着变化。
- 导出 PDF 时需要稳定分页。
这类问题最容易被误解。浏览器有 print / paged media 相关能力,但交互式编辑态不能简单依赖打印布局。实时精确分页会把文本测量、块级测量、图片加载、表格高度、脚注、浮动内容、字体加载都拖进按键关键路径。
2. 职责边界:TipTap、ProseMirror、CSS、Pretext 分别管什么
2.1 ProseMirror / TipTap 的职责
ProseMirror 的核心是:
- 不可变文档树。
- Schema 约束。
- Transaction / Step / Mapping。
- Selection。
- EditorView。
- Plugin / Decoration。
- NodeView。
TipTap 是 ProseMirror 之上的声明式封装:用 Extension / Node / Mark 把 schema、commands、shortcuts、input rules、paste rules、NodeView 聚合到一个扩展单元里。
因此 TipTap 适合管:
- 文档结构。
- 编辑行为。
- 命令与快捷键。
- 粘贴 / 序列化。
- 自定义节点和标记。
- 协同、评论、装饰、菜单等编辑器特性。
TipTap 不适合管:
- 每一行最终在哪里换行。
- 浏览器字体 shaping 的细节。
- 实时精确分页。
- 大量 DOM 几何读取后的布局调度。
这些不是 TipTap 抽象层的强项。
2.2 CSS 的职责
编辑态里,浏览器 CSS Text 是最可信的最终排版来源。尤其是:
- CJK 换行。
- Unicode line breaking。
- OpenType shaping。
- hyphenation。
- bidi。
- 字体 fallback。
- IME 合成文本的临时渲染。
实际工程里,排版第一步不应该是引入测量库,而是把 CSS 策略收敛清楚:
.ProseMirror {
font: 16px/1.6 system-ui, sans-serif;
white-space: pre-wrap;
overflow-wrap: break-word;
word-break: normal;
line-break: auto;
hyphens: auto;
tab-size: 2;
}
.ProseMirror pre,
.ProseMirror code {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.ProseMirror pre {
white-space: pre;
overflow-x: auto;
}
.ProseMirror pre[data-soft-wrap="true"] {
white-space: pre-wrap;
overflow-wrap: anywhere;
}这里的原则是:编辑态尽量让浏览器做真实排版,测量层只用于辅助决策。
2.3 Pretext 的职责
@chenglou/pretext 是纯 JS / TS 的多行文本测量与布局库。它的核心价值是:不通过 DOM 几何读取来预估文本换行、行数和高度。它通过预处理文本、分段、测量 segment 宽度,让后续在不同宽度下的 layout 变成相对便宜的计算。
Pretext 适合:
- paragraph / heading / list item 的高度预测。
- 聊天气泡、卡片、masonry 的只读布局。
- 长文档虚拟化的估算高度。
- 分页草算。
- canvas / SVG / WebGL / 服务端近似排版。
@chenglou/pretext/rich-inline覆盖简单 inline rich flow:code span、link、mention、chip。
Pretext 不适合:
- 接管
contentEditable内部编辑态换行。 - 替代 ProseMirror 的
coordsAtPos/posAtCoords。 - 决定真实 selection、caret 和 IME 合成文本位置。
- 作为导出 PDF 的唯一分页依据。
一句话:Pretext 是测量与预测层,不是 TipTap 的 View 层替代品。
3. 推荐总体架构
3.1 四层模型
┌──────────────────────────────────────────────┐
│ 应用层:页面、工具栏、业务状态、保存、协同 │
├──────────────────────────────────────────────┤
│ 编辑层:TipTap / ProseMirror │
│ Schema / Transaction / Selection / NodeView │
├──────────────────────────────────────────────┤
│ 排版策略层:CSS + NodeView discipline │
│ line-break / hyphens / code wrap / decorations│
├──────────────────────────────────────────────┤
│ 测量层:Pretext + ResizeObserver + DOM fallback│
│ height cache / pagination estimate / reconcile│
└──────────────────────────────────────────────┘关键点:
- 编辑层永远以 ProseMirror state 为真相。
- 编辑态最终视觉布局以浏览器 DOM / CSS 为真相。
- 测量层可以预测,但必须允许被 DOM 测量修正。
- 分页和虚拟化用 decoration / overlay 表达,不要频繁改正文档结构。
3.2 数据流
TipTap JSON / PM doc
↓ extract block metrics input
block text + marks + attrs + font config + container width
↓
Pretext estimate
↓
height cache
↓
virtualizer / pagination planner
↓
render visible DOM
↓
ResizeObserver / selective DOM measurement
↓
cache reconciliation这条链路的核心是「预测先行,真实 DOM 回填」。不要指望预测层永远准确,也不要把真实 DOM 测量放到每次按键的同步关键路径。
4. TipTap 内部哪些机制会影响排版
4.1 NodeView 是编辑体验扩展点,不是通用排版容器
NodeView 适合:
- 嵌入卡片。
- 图片 / 视频 / 文件。
- 表格或复杂 block。
- 交互式 widget。
- 不适合用普通 DOM 表达的块。
NodeView 不适合:
- 大量普通段落。
- 每个 mention 都挂 React 组件。
- 每个行内 code span 都挂复杂组件。
- 用 NodeView 模拟每一行。
原因很直接:每个 NodeView 都是 ProseMirror View 层要维护的一个边界。React / Vue NodeView 还会把框架渲染同步塞进 ProseMirror 更新流程。文档一大,它们会变成输入延迟来源。
建议:
- 普通 paragraph / heading / list 走
renderHTML+ CSS。 - 简单 inline 样式用 Mark。
- mention / chip 如果可接受「整体删除、整体选择」,用 inline atom node,但 DOM 保持轻。
- 只有复杂交互才上 NodeView。
- React NodeView 必须
memo、少传 props、传getPos而不是固定pos。
4.2 Decoration 适合表达「排版辅助 UI」
页边界、当前行高亮、搜索命中、评论锚点、协作者光标、临时测量标记,都不应该污染文档模型。优先使用 Decoration:
Decoration.inline:纯样式区间,最轻。Decoration.node:给节点加 class / attrs。Decoration.widget:插入不属于文档的 DOM,最贵,但适合评论锚点、页边界 marker。
分页场景尤其要避免把 page break 频繁写进真实文档。编辑时每次输入都插入/删除 page break node,会让 Step、Mapping、collab、undo、selection 都变复杂。更稳的做法是:
- 编辑态页边界 = Decoration / overlay。
- 导出态页边界 = 离线 pass 计算后生成。
4.3 Selection / IME 是不能牺牲的底线
任何排版方案只要影响以下能力,都要降级或放弃:
- 中文输入法 composition。
- Android 输入法。
- 鼠标拖选。
- Shift + Arrow 选择。
- copy / paste。
- screen reader 浏览。
posAtCoords/coordsAtPos。
这也是为什么不建议在编辑态用 Pretext 自绘行布局。你不仅要画出文本,还要重新实现浏览器和 ProseMirror 已经处理的大量编辑边界。
5. CSS 排版策略:先把浏览器能力用对
5.1 white-space
常见选择:
normal:普通段落,折叠空白。pre-wrap:保留换行和连续空格,同时允许自动换行。pre:代码块默认选择,保留空白且不自动换行。
TipTap 编辑器正文常见是 white-space: pre-wrap,因为用户输入的换行需要被保留。代码块默认 pre 更符合开发者预期。
5.2 overflow-wrap 与 word-break
建议默认:
.ProseMirror {
overflow-wrap: break-word;
word-break: normal;
}需要强行处理 URL / token 的容器,可以局部用:
.ProseMirror .break-anywhere {
overflow-wrap: anywhere;
}谨慎使用:
word-break: break-all;它会让英文单词随意断开,中文场景看似解决撑破,但会损害英文可读性。通常只适合特别窄的 badge / table cell / token 容器。
5.3 CJK: line-break
对中文、日文、韩文,需要关注:
line-break: auto;更细的策略可以按产品风格试:
loose:更宽松,适合窄屏或移动端。normal:普通。strict:更严格,可能更符合排版规范,但更容易产生空隙。
这类策略不应只在英文样本文档上验证。必须准备中文、日文、韩文、混排数字、全角标点、英文长词、emoji 的 corpus。
5.4 hyphens
英文或欧洲语言可使用:
.ProseMirror {
hyphens: auto;
}但它依赖 lang:
<p lang="en">...</p>
<p lang="de">...</p>没有正确 lang,浏览器很可能无法启用对应断词字典。不同浏览器的 hyphenation 字典和效果也会不同,所以它更适合改善视觉,不适合作为跨浏览器完全一致的布局依据。
5.5 text-wrap: pretty / balance
Chrome 等浏览器提供 text-wrap: pretty / balance 等更高级的换行优化。它们可能让标题或短段落更美观,但不能默认用于超长可编辑正文:
- 支持度不一。
- 可能有额外计算成本。
- 结果可能影响高度预测。
建议仅用于只读标题、卡片摘要,并用 CSS.supports 做 feature detection。
6. 代码块排版
代码块要和普通段落分开处理,因为它有三组冲突目标:
- 保留缩进与空白。
- 可复制。
- 不撑破布局。
- 不让 DOM 过重。
推荐默认:
.ProseMirror pre {
white-space: pre;
overflow-x: auto;
}提供用户可选 soft wrap:
.ProseMirror pre[data-wrap="true"] {
white-space: pre-wrap;
overflow-wrap: anywhere;
}对于高亮:
- 小代码块可以用 Shiki / highlight.js 生成 span。
- 大代码块避免每字符 span。
- 语言包和 theme 懒加载。
- 如果要虚拟化代码行,优先在只读 code viewer 做,不要把编辑态变成复杂行级 NodeView。
TipTap 的 CodeBlock 扩展本身解决的是 schema / command / input rule 层的问题,不是性能高亮或超大代码块渲染问题。
7. inline atom、mention、chip 与 rich inline
7.1 三种建模方案
| 方案 | 适合 | 风险 |
|---|---|---|
| Mark | link、bold、inline code、highlight | 无法表达整体不可编辑 chip |
| Inline node | mention、emoji、variable、token | selection / copy / 删除边界要测试 |
| Inline NodeView | 复杂交互 chip、popover trigger | DOM 与 selection 边界更脆,性能更重 |
建议默认顺序:
- 能用 Mark 就用 Mark。
- 需要原子整体行为时用 inline atom node。
- 只有需要复杂交互时才使用 inline NodeView。
7.2 Pretext rich-inline 的位置
@chenglou/pretext/rich-inline 可以作为「只读 inline flow helper」:让 code span、link、mention、chip 参与手动布局,并保持 chip 不被拆开。
适合:
- 消息气泡预览。
- 富文本卡片摘要。
- 只读评论渲染。
- 高度预测时把 mention/chip 当作额外宽度的不可断 segment。
不适合:
- 编辑态真实 selection。
- 可编辑 NodeView 内部行布局。
- 直接替代 ProseMirror 的 inline DOM。
7.3 测试清单
inline atom 必测:
- 光标从左右方向进入。
- Backspace / Delete 删除。
- Shift + Arrow 选择。
- 鼠标拖选跨 atom。
- 双击词选择。
- 复制粘贴 round-trip。
- 中文输入法在 atom 前后输入。
- Android / iOS 输入。
这类测试比视觉截图更重要,因为视觉正确不代表编辑语义正确。
8. 分页:最容易过度承诺的领域
8.1 为什么实时精确分页难
分页需要知道每个 block 的高度,但 block 高度可能取决于:
- 容器宽度。
- 字体加载状态。
- CJK / hyphenation。
- 图片加载结果。
- 表格列宽。
- code block wrap。
- NodeView 内部异步内容。
- decorations 是否影响布局。
- 浏览器 zoom。
如果每次按键都同步测量所有后续 block,会触发大量 layout read。长文档中,这会让输入延迟失控。
8.2 推荐分页分层
编辑态:
- 使用 page overlay / decoration 显示页边界。
- 根据缓存高度增量更新受影响页之后的少量范围。
- 暂时允许页边界轻微漂移。
- 用户停顿后做更准确 reconciliation。
导出态:
- 进入导出流程时锁定内容。
- 等字体、图片、异步 NodeView 内容稳定。
- 离线测量真实 DOM。
- 生成最终 page breaks / PDF。
8.3 不建议的做法
- 每次输入后从头扫描整篇文档。
- 把 page break node 写入正文并频繁移动。
- 用 Pretext 的预测结果直接作为最终分页。
- 为每一页创建一个独立 TipTap 实例,除非产品能接受跨页 selection / undo / collab 的复杂代价。
9. 虚拟化与高度预测
9.1 基本策略
长文档虚拟化的关键问题是:不可见内容没有 DOM,但滚动容器需要知道总高度。
可行策略:
- 为每个 block 建立估算高度。
- 可见 block 挂载真实 DOM。
- 用 ResizeObserver 回填真实高度。
- 更新 cache 和 offset map。
- 滚动时只挂载 viewport 附近内容。
Pretext 可用于第 1 步:对纯文本块快速估算行数和高度。
9.2 cache key
高度缓存不能只按文本内容做 key,至少要包含:
- node type:paragraph / heading / list item / blockquote。
- text hash。
- marks summary:link / code / bold 是否影响字体或 spacing。
- attrs:heading level、indent、alignment。
- container width。
- font family / font size / font weight / line-height。
- letter-spacing。
- white-space。
- locale / lang。
- code block wrap mode。
- inline atom metrics。
如果漏掉 font 或 width,缓存会在响应式布局、字号切换、主题切换时失效。
9.3 Pretext 估算与 DOM 修正
推荐策略:
estimateHeight(block):
if block is simple text:
return pretextLayout(block.text, width, fontConfig).height
if block is rich inline:
return richInlineEstimate(block.inlineRuns, width, fontConfig).height
if block is image/table/custom NodeView:
return previousMeasuredHeight ?? typeDefaultHeight然后:
onVisibleBlockResize(blockId, measuredHeight):
cache[blockId] = measuredHeight
offsetMap.updateFrom(blockId)
virtualizer.reconcileScrollPosition()这里的核心是接受「先估计,再修正」。不要追求首次估计 100% 准确。
10. DOM measurement 与 reflow discipline
10.1 避免 forced synchronous layout
典型反模式:
el.style.width = nextWidth + "px";
const height = el.offsetHeight;
el.style.transform = `translateY(${height}px)`;这会在写 DOM 后立刻读 layout,浏览器被迫同步 flush。
更稳的纪律:
- 同一帧先批量读,再批量写。
- 使用
requestAnimationFrame分离读写。 - 用 ResizeObserver 观察尺寸变化。
- 对不影响编辑的重算做 debounce。
- 不在 ProseMirror plugin
apply里读 DOM。
10.2 读写分离
let pending = false;
function scheduleMeasure() {
if (pending) return;
pending = true;
requestAnimationFrame(() => {
const measurements = visibleBlocks.map((block) => ({
id: block.id,
height: block.dom.getBoundingClientRect().height,
}));
requestAnimationFrame(() => {
pending = false;
applyMeasurements(measurements);
});
});
}这个模式不是银弹,但它表达了基本纪律:不要在同一个同步调用栈里交错 DOM write 和 layout read。
11. Pretext 集成草案
11.1 作为服务而不是组件
把 Pretext 包成一个 measurement service:
type TextMeasureInput = {
id: string;
text: string;
width: number;
font: string;
lineHeight: number;
whiteSpace: "normal" | "pre-wrap";
wordBreak?: "normal" | "keep-all";
letterSpacing?: number;
};
type TextMeasureResult = {
lineCount: number;
height: number;
naturalWidth?: number;
};TipTap 插件或外部 virtualizer 不直接依赖 Pretext 细节,只依赖这个接口。这样未来可以替换成 DOM measurement、Canvas fallback、worker 方案或服务端预估。
11.2 从 TipTap doc 提取测量输入
基本方法:
- 遍历 top-level block。
- 对 paragraph / heading / list item 提取
textBetween。 - 对 inline atom 提供 leaf text 或宽度占位。
- 对 code block 保留换行和空格,走
pre-wrap或pre策略。 - 对 table / image / custom NodeView 标记为
requiresDomMeasurement。
注意:如果 marks 改变字体宽度,不能简单丢弃 marks。例如 inline code 通常是 monospace 且有 padding / background;link 可能不改变宽度;bold 可能改变字重和实际宽度。
11.3 rich inline 的 run 模型
可以把 inline 内容转成 runs:
type InlineRun =
| { type: "text"; text: string; marks: string[] }
| { type: "code"; text: string }
| { type: "mention"; label: string; width: number; break: "never" }
| { type: "chip"; label: string; width: number; break: "never" };Pretext rich-inline 可以在只读或预测层处理这些 run,但 TipTap 编辑态仍保留真实 DOM。
11.4 校准策略
引入 Pretext 后必须做校准:
- 同一批样本文档,记录 Pretext 估算高度与真实 DOM 高度。
- 按语言拆分:中文、英文、日文、韩文、emoji、RTL、混排。
- 按节点类型拆分:paragraph、heading、list、blockquote、code。
- 按字体拆分:system-ui、serif、monospace、业务字体。
- 统计误差:平均误差、P95、最大误差。
如果某类内容误差过大,不要硬调全局参数;应把该类型标记为 DOM-only measurement 或增加类型专用估算。
12. 推荐落地路线
阶段 1:先修 CSS 与 schema
目标:解决 60% 的排版问题,不引入测量复杂度。
- 明确编辑器全局
font/line-height。 - 设置正文
white-space。 - 设置
overflow-wrap/word-break/line-break。 - 为多语言内容补
lang。 - 代码块区分 horizontal scroll 与 soft wrap。
- 检查 inline atom 的 schema 和 DOM 是否过重。
验收:
- 中文、英文长词、URL、emoji、代码块样本视觉正常。
- IME 输入不跳光标。
- copy / paste 正常。
阶段 2:建立测量缓存
目标:为虚拟化、卡片预览、分页预估准备基础设施。
- 设计 block id。
- 建立 block height cache。
- 简单文本块接入 Pretext。
- 复杂块用 ResizeObserver 回填。
- cache key 包含 width / font / line-height。
验收:
- 首屏无需全量 DOM 测量即可获得总高度估计。
- 可见块真实测量后滚动位置稳定修正。
- typing 时没有明显 forced layout。
阶段 3:虚拟化或分页草算
目标:解决长文档和类页面体验。
- 用 offset map 驱动虚拟滚动。
- 页边界用 overlay / Decoration。
- 用户输入时只重算受影响区域。
- idle 时做更大范围 reconciliation。
验收:
- 长文档输入延迟可量化下降。
- 页边界可以延迟收敛,但不阻塞输入。
- undo / redo / collab 不受 page overlay 污染。
阶段 4:导出与高保真分页
目标:把编辑态的近似体验和导出态的确定性分开。
- 导出前等待 fonts / images / async node 稳定。
- 使用真实 DOM 或专门排版环境测量。
- 生成最终 PDF / HTML page breaks。
- 记录导出与编辑预览的差异。
验收:
- 导出结果稳定。
- 编辑态不为导出精度牺牲输入性能。
13. 监控与 benchmark
需要记录的指标:
- keydown 到下一帧 paint 的延迟。
- ProseMirror transaction 时间。
- NodeView mount / update 次数。
- DecorationSet 重建次数。
- forced synchronous layout 次数。
- ResizeObserver 回调数量。
- 可见 DOM 节点数。
- 长任务数量和时长。
- Pretext 估算误差。
Chrome DevTools Performance 里重点看:
- Scripting。
- Rendering。
- Layout。
- Recalculate Style。
- Long Task。
- forced reflow 相关调用栈。
工程上可打点:
performance.mark("editor-input-start");
// dispatch transaction
performance.mark("editor-dispatch-end");
requestAnimationFrame(() => {
performance.mark("editor-next-paint");
performance.measure(
"input-to-next-paint",
"editor-input-start",
"editor-next-paint"
);
});14. 决策矩阵
| 需求 | 优先方案 | 不建议 |
|---|---|---|
| 普通编辑换行 | CSS Text + TipTap DOM | Pretext 接管编辑行布局 |
| CJK 换行 | line-break + 样本测试 | word-break: break-all 全局兜底 |
| 长 URL 撑破 | overflow-wrap 局部策略 | 全局破坏单词可读性 |
| mention / chip | inline node / 轻 DOM | 大量 React inline NodeView |
| 代码块 | 默认横向滚动 + 可选 soft wrap | 每字符复杂 DOM |
| 卡片高度 | Pretext 估算 + DOM 回填 | 首屏全量 DOM 测量 |
| 长文档 | block height cache + virtualizer | 全文挂载 + 每次读高度 |
| 编辑态分页 | overlay / decoration + 增量测量 | 每次输入改正文档 page break |
| 导出分页 | 离线真实测量 | 用编辑态预测结果直接导出 |
15. 对后续调研的启发
这套调研后续可以拆成几条独立路线:
- 排版标准路线:CSS Text、Unicode UAX #14、OpenType shaping、hyphenation、CJK line breaking。
- 编辑器内核路线:ProseMirror selection、coords mapping、NodeView、Decoration 与 IME。
- 性能工程路线:forced layout、ResizeObserver、virtualizer、height cache、React NodeView 成本。
- Pretext 路线:Canvas measurement、Intl.Segmenter、rich-inline、accuracy corpus。
- 分页路线:CSS Paged Media、浏览器打印、编辑态页面模拟、导出 pipeline。
更重要的是,这几条路线不能混成一个问题。每次遇到「TipTap 排版不对」,先判断它属于:
- CSS 策略问题。
- Schema / NodeView 建模问题。
- 浏览器编辑态问题。
- 性能与测量问题。
- 只读渲染问题。
- 导出分页问题。
分类正确之后,方案才会自然收敛。
参考
- ProseMirror Reference Manual: https://prosemirror.net/docs/ref
- ProseMirror Guide: https://prosemirror.net/docs/guide
- TipTap Node Views: https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views
- TipTap React Node Views: https://tiptap.dev/docs/editor/extensions/custom-extensions/node-views/react
- TipTap CodeBlock: https://tiptap.dev/docs/editor/extensions/nodes/code-block
chenglou/pretext: https://github.com/chenglou/pretext- Pretext demos: https://chenglou.me/pretext
- CSS Text Module Level 4: https://drafts.csswg.org/css-text-4
- Unicode Line Breaking Algorithm UAX #14: https://unicode.org/reports/tr14
- MDN
line-break: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/line-break - MDN Resize Observer API: https://developer.mozilla.org/en-US/docs/Web/API/Resize_Observer_API
- Chrome DevTools Performance reference: https://developer.chrome.com/docs/devtools/performance/reference
- Chrome
text-wrap: pretty: https://developer.chrome.com/blog/css-text-wrap-pretty - ProseMirror pagination discussion: https://discuss.prosemirror.net/t/implementing-pagination-with-prosemirror/6336
- ProseMirror virtual scroll discussion: https://discuss.prosemirror.net/t/virtual-scroll-for-prosemirror/8882
- ProseMirror inline atom issue: https://github.com/ProseMirror/prosemirror/issues/1199