chore: 收口 Batch C 证据与设计口径

This commit is contained in:
lix-2026
2026-05-21 13:28:23 +08:00
parent a0823a7e53
commit b0fa1e53d8
15 changed files with 807 additions and 20 deletions
@@ -6218,6 +6218,9 @@ fn next_available_directory_path(directory: &Path, stem: &str) -> PathBuf {
directory.join(format!("{stem}-{}", std::process::id()))
}
// 过渡实现:写侧手写 Markdown 回写入口函数。当前通过 editor_blocks_to_markdown_with_rewrite
// 将 editor block document 转为 Markdown 文本,AST 迁移 complete 后应替换为
// AST→Markdown 反向映射(见 design/03-rust-web/process/3-13 方案 Step 3)。
fn editor_blocks_to_markdown_for_file(
content: &Value,
root: &Path,
@@ -6387,6 +6390,9 @@ fn stable_hash_hex(value: &str) -> String {
format!("{:016x}", hasher.finish())
}
// 过渡实现:写侧手写 Markdown 回写核心函数。逐 block 遍历,通过
// inline_nodes_to_markdown 将行内节点转为 Markdown 文本。
// AST 迁移 complete 后应替换为 block→Markdown AST→文本 的两次映射。
fn editor_blocks_to_markdown_with_rewrite(
content: &Value,
local_file_context: Option<(&Path, &Path)>,
@@ -6995,6 +7001,9 @@ fn block_content_value(block: &Value) -> Option<&Value> {
block.get("content").or_else(|| block.get("contentNodes"))
}
// 过渡实现:手写行内节点→Markdown 回写函数。从 editor block 的 content 数组中
// 逐个节点提取 text + styles,按 legacy 样式格式输出为内联 Markdown。
// AST + 中间 IR 迁移 complete 后应统一走 MarkdownInline→Markdown 反向映射。
fn inline_nodes_to_markdown(value: &Value) -> String {
if let Some(text) = value.as_str() {
return escape_markdown_inline_text(text);
@@ -744,4 +744,72 @@ mod tests {
assert_eq!(first["props"]["src"].as_str(), Some("assets/photo.jpg"));
assert_eq!(first["props"]["alt"].as_str(), Some("示例图片"));
}
/// 固定空引用块行为:`>` 在 GFM AST 中产生 BlockQuote 节点,
/// collect_inline_children 返回空 vec → 输出 type=quote content=[]。
#[test]
fn markdown_empty_blockquote_parse() {
let blocks = markdown_to_blocks("> \n\n>");
let array = blocks.as_array().expect("blocks");
assert!(!array.is_empty(), "应至少产生一个引用块");
for block in array.iter() {
if block["type"].as_str() == Some("quote") {
let content = block["content"].as_array().expect("quote content");
assert!(
content.is_empty(),
"空引用块 content 应为空,实际 {:?}",
content
);
}
}
}
/// 固定空表格单元格行为:comrak 解析空单元格为空的 tiptap paragraph
/// content 字段为 []。
#[test]
fn markdown_table_empty_cells_parse() {
let blocks = markdown_to_blocks("| 左 | |\n| --- | --- |\n| | 右 |\n");
let array = blocks.as_array().expect("blocks");
let table = array
.iter()
.find(|b| b["type"].as_str() == Some("table"))
.expect("should have table block");
let tiptap_table = table["props"]["tiptapTable"]
.as_object()
.expect("tiptapTable object");
let content_rows = tiptap_table["content"]
.as_array()
.expect("table content rows");
// 表头行:两个单元格
let header_row = &content_rows[0]["content"].as_array().expect("header row cells");
assert_eq!(header_row.len(), 2);
// 表头第一个单元格(非空)
let h1_cell = &header_row[0]["content"].as_array().expect("header cell paragraphs");
assert!(!h1_cell.is_empty(), "表头第一个单元格不应为空");
// 表头第二个单元格(空):单元格的 content 为 [{type:"paragraph", content:[]}]
// 段落层的 content 应为空数组
let h2_par_content = &header_row[1]["content"][0]["content"];
let h2_inline = h2_par_content.as_array().expect("header cell para content");
assert!(
h2_inline.is_empty(),
"空表头单元格的 paragraph content 应为空,实际值: {}",
serde_json::to_string_pretty(h2_par_content).unwrap()
);
// 数据行
let data_row = &content_rows[1]["content"].as_array().expect("data row cells");
assert_eq!(data_row.len(), 2);
// 数据行第一个单元格(空):段落层的 content 应为空数组
let d1_par_content = &data_row[0]["content"][0]["content"];
let d1_inline = d1_par_content.as_array().expect("data cell para content");
assert!(
d1_inline.is_empty(),
"空数据单元格的 paragraph content 应为空,实际值: {}",
serde_json::to_string_pretty(d1_par_content).unwrap()
);
// 数据行第二个单元格(非空):段落层的 content 不应为空
let d2_par_content = &data_row[1]["content"][0]["content"];
let d2_inline = d2_par_content.as_array().expect("data cell para content");
assert!(!d2_inline.is_empty(), "非空数据单元格不应为空");
}
}
@@ -828,6 +828,10 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
return '';
};
// 过渡适配(TODO step-4):legacy→Tiptap inline marks 转换函数组。
// AST/block 迁移 complete 后,前端应直接消费 block document 中的
// tiptap 格式 marks(已由 Rust 侧 local_markdown_parser 输出),
// 不再需要此 JS 侧样式→marks 适配层。
const legacyStylesToTiptapMarks = (styles) => {
if (!styles || typeof styles !== 'object') return [];
const marks = [];
@@ -5153,6 +5157,10 @@ mod tests {
assert!(html.contains("legacyStylesToTiptapMarks"));
assert!(html.contains("legacyMarkArrayToTiptapMarks"));
assert!(html.contains("firstNonEmptyText(block?.props?.sourcePath"));
assert!(html.contains("marks.push({ type: 'bold' })"));
assert!(html.contains("marks.push({ type: 'italic' })"));
assert!(html.contains("marks.push({ type: 'underline' })"));
assert!(html.contains("marks.push({ type: 'strike' })"));
assert!(html.contains("marks.push({ type: 'code' })"));
assert!(html.contains("marks.push({ type: 'link', attrs: { href } })"));
assert!(html.contains("styles.link = href"));