feat(web): 新增 Markdown 即时渲染装饰层(Typora 式)
- web/src/editor/:syntax(lezer 语法树 → 装饰描述,纯函数)、reveal(行粒度展开 + 合并区间二分)、widgets(圆点/引用竖线/水平线/图片/围栏语言标签/任务框,全部 createElement + textContent、零 innerHTML)、decorations(Spec → DecorationSet + 原子区间 + 图片 src 白名单 + 可见区间补解析)、livePreview(ViewPlugin + atomicRanges + Compartment 源码开关 + view.composing 期间冻结装饰) - Editor.tsx:挂载装饰层,新增可选 sourceMode prop - index.css:抽出 --md-* 排版契约变量(阅读态 .pn-note-body 与编辑态 .cm-md-* 共用, 杜绝两份样式漂移)、.cm-md-* 装饰样式(对齐阅读态字号/衬线标题/引用弱化色/代码底色) - 数据层零改动:onChange 拿到的仍是 Markdown 原文,公开页渲染管线与 sanitize 防线不动; 运行时零新增 npm 依赖 P3 加固(长文档惰性解析补解析、Cmd+A 全选退化、光标行在视口外也装饰)包含在 decorations.ts / livePreview.ts 内,回归用例见后续 test 提交,方案与实测见 docs/editor-live-rendering.md §12.5。
This commit is contained in:
+200
-12
@@ -99,6 +99,20 @@
|
||||
--font-serif-display: var(--font-serif);
|
||||
--tracking-tight: -0.012em;
|
||||
|
||||
/* Markdown 排版契约(方案 §4.6):阅读态 .pn-note-body 与编辑态 .cm-md-* 共用,
|
||||
杜绝两份样式漂移。改这里 = 同时改编辑态与公开页。 */
|
||||
--md-body-size: 15px;
|
||||
--md-body-lh: 1.8;
|
||||
--md-h1-size: 26px;
|
||||
--md-h2-size: 24px;
|
||||
--md-h3-size: 19px;
|
||||
--md-h4-size: 16px;
|
||||
--md-heading-lh: 1.35;
|
||||
--md-heading-font: var(--font-serif);
|
||||
--md-code-size: 13px;
|
||||
--md-table-size: 13.5px;
|
||||
--md-img-radius: var(--radius-md);
|
||||
|
||||
/* 圆角 / 阴影 / 间距基数 */
|
||||
--radius: 0.75rem;
|
||||
--radius-sm: 0.375rem;
|
||||
@@ -804,8 +818,8 @@ button.pn-note-back:hover {
|
||||
============================================================ */
|
||||
.pn-note-body {
|
||||
margin-top: 40px;
|
||||
font-size: 15px;
|
||||
line-height: 1.8;
|
||||
font-size: var(--md-body-size);
|
||||
line-height: var(--md-body-lh);
|
||||
color: var(--foreground);
|
||||
}
|
||||
.pn-note-body > :first-child {
|
||||
@@ -819,23 +833,23 @@ button.pn-note-back:hover {
|
||||
.pn-note-body h3,
|
||||
.pn-note-body h4 {
|
||||
margin: 40px 0 16px;
|
||||
font-family: var(--font-serif);
|
||||
font-family: var(--md-heading-font);
|
||||
font-weight: 400;
|
||||
line-height: 1.35;
|
||||
line-height: var(--md-heading-lh);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
color: var(--foreground);
|
||||
}
|
||||
.pn-note-body h1 {
|
||||
font-size: 26px;
|
||||
font-size: var(--md-h1-size);
|
||||
}
|
||||
.pn-note-body h2 {
|
||||
font-size: 24px;
|
||||
font-size: var(--md-h2-size);
|
||||
}
|
||||
.pn-note-body h3 {
|
||||
font-size: 19px;
|
||||
font-size: var(--md-h3-size);
|
||||
}
|
||||
.pn-note-body h4 {
|
||||
font-size: 16px;
|
||||
font-size: var(--md-h4-size);
|
||||
}
|
||||
.pn-note-body blockquote {
|
||||
margin: 32px 0;
|
||||
@@ -851,7 +865,7 @@ button.pn-note-back:hover {
|
||||
}
|
||||
.pn-note-body code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 13px;
|
||||
font-size: var(--md-code-size);
|
||||
background: var(--muted);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 2px 6px;
|
||||
@@ -870,7 +884,7 @@ button.pn-note-back:hover {
|
||||
display: block;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
font-size: 13px;
|
||||
font-size: var(--md-code-size);
|
||||
line-height: 1.7;
|
||||
color: var(--foreground);
|
||||
white-space: pre;
|
||||
@@ -892,7 +906,7 @@ button.pn-note-back:hover {
|
||||
}
|
||||
.pn-note-body img {
|
||||
max-width: 100%;
|
||||
border-radius: var(--radius-md);
|
||||
border-radius: var(--md-img-radius);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.pn-note-body hr {
|
||||
@@ -904,7 +918,7 @@ button.pn-note-back:hover {
|
||||
width: 100%;
|
||||
margin: 24px 0;
|
||||
border-collapse: collapse;
|
||||
font-size: 13.5px;
|
||||
font-size: var(--md-table-size);
|
||||
}
|
||||
.pn-note-body th,
|
||||
.pn-note-body td {
|
||||
@@ -1975,6 +1989,180 @@ button.pn-ae-tool svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
即时渲染(Typora 式)装饰层 —— 与 .pn-note-body 共用 --md-* 契约
|
||||
仅 .pn-cm-live 包裹时生效;源码模式下整块不生效(§4.7 逃生舱)
|
||||
============================================================ */
|
||||
.pn-cm-live .cm-scroller {
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
.pn-cm-live .cm-content {
|
||||
font-size: var(--md-body-size);
|
||||
line-height: var(--md-body-lh);
|
||||
padding: 8px 2px 40px;
|
||||
}
|
||||
.pn-cm-live .cm-editor {
|
||||
font-size: var(--md-body-size);
|
||||
}
|
||||
/* Typora 无行号、无活动行高亮(§4.6-4) */
|
||||
.pn-cm-live .cm-gutters {
|
||||
display: none;
|
||||
}
|
||||
.pn-cm-live .cm-activeLine {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 行内:对齐 .pn-note-body(§4.3) */
|
||||
.cm-md-strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
.cm-md-em {
|
||||
font-style: italic;
|
||||
}
|
||||
.cm-md-del {
|
||||
color: var(--muted-foreground);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.cm-md-code {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--md-code-size);
|
||||
background: var(--muted);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 2px 6px;
|
||||
}
|
||||
.cm-md-link {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
text-decoration-color: var(--border-strong);
|
||||
}
|
||||
|
||||
/* 块级:标题(§4.4) */
|
||||
.cm-md-h1,
|
||||
.cm-md-h2,
|
||||
.cm-md-h3,
|
||||
.cm-md-h4,
|
||||
.cm-md-h5,
|
||||
.cm-md-h6 {
|
||||
font-family: var(--md-heading-font);
|
||||
font-weight: 400;
|
||||
line-height: var(--md-heading-lh);
|
||||
letter-spacing: var(--tracking-tight);
|
||||
}
|
||||
.cm-md-h1 {
|
||||
font-size: var(--md-h1-size);
|
||||
}
|
||||
.cm-md-h2 {
|
||||
font-size: var(--md-h2-size);
|
||||
}
|
||||
.cm-md-h3 {
|
||||
font-size: var(--md-h3-size);
|
||||
}
|
||||
.cm-md-h4 {
|
||||
font-size: var(--md-h4-size);
|
||||
}
|
||||
.cm-md-h5 {
|
||||
font-size: var(--md-body-size);
|
||||
font-weight: 600;
|
||||
}
|
||||
.cm-md-h6 {
|
||||
font-size: var(--md-body-size);
|
||||
font-weight: 600;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* 块级:引用行(竖线由 widget 承接,§5.3) */
|
||||
.cm-md-quote {
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
.cm-md-quote-bar {
|
||||
display: inline-block;
|
||||
width: 0.6em;
|
||||
height: 1.05em;
|
||||
margin-right: 8px;
|
||||
border-left: 2px solid var(--border-strong);
|
||||
vertical-align: -0.12em;
|
||||
}
|
||||
|
||||
/* 块级:无序列表圆点(§5.1) */
|
||||
.cm-md-bullet {
|
||||
display: inline-block;
|
||||
width: 1.15em;
|
||||
text-align: center;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
.cm-md-ol-mark {
|
||||
color: var(--muted-foreground);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 块级:代码围栏(§4.4) */
|
||||
.cm-md-fenced {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--md-code-size);
|
||||
line-height: 1.7;
|
||||
background: var(--muted);
|
||||
padding-left: 18px;
|
||||
padding-right: 18px;
|
||||
}
|
||||
.cm-md-fenced-first {
|
||||
border-top-left-radius: var(--radius-md);
|
||||
border-top-right-radius: var(--radius-md);
|
||||
padding-top: 8px;
|
||||
}
|
||||
.cm-md-fenced-last {
|
||||
border-bottom-left-radius: var(--radius-md);
|
||||
border-bottom-right-radius: var(--radius-md);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.cm-md-fence-lang {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* 块级:表格(源码对齐,决策 E5) */
|
||||
.cm-md-table {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--md-table-size);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.cm-md-table-mark {
|
||||
color: var(--muted-foreground);
|
||||
opacity: 0.55;
|
||||
}
|
||||
.cm-md-table-delim {
|
||||
opacity: 0.38;
|
||||
}
|
||||
|
||||
/* 块级:图片 / 水平线 / 任务框(§5.4) */
|
||||
.cm-md-image {
|
||||
max-width: 100%;
|
||||
border-radius: var(--md-img-radius);
|
||||
border: 1px solid var(--border);
|
||||
vertical-align: bottom;
|
||||
}
|
||||
.cm-md-image-error {
|
||||
display: inline-block;
|
||||
min-width: 140px;
|
||||
min-height: 42px;
|
||||
background: var(--muted);
|
||||
}
|
||||
.cm-md-hr {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
border-top: 1px solid var(--border);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.cm-md-task {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 0 2px 0 0;
|
||||
vertical-align: -2px;
|
||||
accent-color: var(--primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ── 动效偏好 ─────────────────────────────────────────── */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
|
||||
Reference in New Issue
Block a user