fix: 收口批次D并修复本地垃圾箱清空
This commit is contained in:
@@ -78,18 +78,76 @@ struct MarkdownInlineStyles {
|
||||
}
|
||||
|
||||
pub fn parse_markdown_page(markdown: &str, file_name: &str) -> ParsedLocalMarkdownPage {
|
||||
let (_, body) = split_frontmatter(markdown);
|
||||
let title = file_stem_title(file_name);
|
||||
let (frontmatter, body) = split_frontmatter(markdown);
|
||||
let body_owned = body.to_string();
|
||||
// 标题优先级:frontmatter title > 正文第一条 H1 > 文件名。
|
||||
let title = parse_frontmatter_title(frontmatter.as_deref())
|
||||
.or_else(|| find_first_h1(&body_owned))
|
||||
.unwrap_or_else(|| file_stem_title(file_name));
|
||||
ParsedLocalMarkdownPage {
|
||||
title,
|
||||
body: body.to_string(),
|
||||
body: body_owned,
|
||||
}
|
||||
}
|
||||
|
||||
/// 从 frontmatter 中提取简单 `title:` 字段。
|
||||
fn parse_frontmatter_title(frontmatter: Option<&str>) -> Option<String> {
|
||||
let text = frontmatter?;
|
||||
for line in text.lines() {
|
||||
let trimmed = line.trim();
|
||||
if let Some(value) = trimmed.strip_prefix("title:") {
|
||||
let raw = value.trim();
|
||||
if raw.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let unquoted = if (raw.starts_with('"') && raw.ends_with('"'))
|
||||
|| (raw.starts_with('\'') && raw.ends_with('\''))
|
||||
{
|
||||
&raw[1..raw.len() - 1]
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
let title = unquoted.trim().to_string();
|
||||
if !title.is_empty() {
|
||||
return Some(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// 通过 AST 找正文第一条 H1,避免把代码块里的 `# ...` 误判为标题。
|
||||
fn find_first_h1(body: &str) -> Option<String> {
|
||||
let arena = Arena::new();
|
||||
let root = parse_document(&arena, body, &markdown_options());
|
||||
for node in root.descendants() {
|
||||
let value = node.data.borrow();
|
||||
if let NodeValue::Heading(heading) = &value.value {
|
||||
if heading.level != 1 {
|
||||
continue;
|
||||
}
|
||||
let title = collect_plain_text(node).trim().to_string();
|
||||
return (!title.is_empty()).then_some(title);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn markdown_to_blocks(markdown: &str) -> Value {
|
||||
markdown_ast_document_to_blocks(&parse_markdown_ast_document(markdown))
|
||||
}
|
||||
|
||||
fn markdown_options() -> Options<'static> {
|
||||
let mut options = Options::default();
|
||||
options.extension.table = true;
|
||||
options.extension.tasklist = true;
|
||||
options.extension.strikethrough = true;
|
||||
options.extension.autolink = true;
|
||||
options.extension.front_matter_delimiter = Some("---".to_string());
|
||||
options.parse.tasklist_in_table = true;
|
||||
options
|
||||
}
|
||||
|
||||
pub fn parse_markdown_attachment_link(trimmed: &str) -> Option<(String, String)> {
|
||||
let value = trimmed
|
||||
.strip_prefix('!')
|
||||
@@ -130,14 +188,7 @@ pub fn parse_markdown_attachment_link(trimmed: &str) -> Option<(String, String)>
|
||||
|
||||
fn parse_markdown_ast_document(markdown: &str) -> MarkdownAstDocument {
|
||||
let arena = Arena::new();
|
||||
let mut options = Options::default();
|
||||
options.extension.table = true;
|
||||
options.extension.tasklist = true;
|
||||
options.extension.strikethrough = true;
|
||||
options.extension.autolink = true;
|
||||
options.extension.front_matter_delimiter = Some("---".to_string());
|
||||
options.parse.tasklist_in_table = true;
|
||||
|
||||
let options = markdown_options();
|
||||
let root = parse_document(&arena, markdown, &options);
|
||||
let mut blocks = Vec::new();
|
||||
for node in root.children() {
|
||||
@@ -730,7 +781,7 @@ pub(crate) fn file_stem_title(file_name: &str) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::markdown_to_blocks;
|
||||
use super::{markdown_to_blocks, parse_markdown_page};
|
||||
|
||||
#[test]
|
||||
fn markdown_image_parses_as_image_block() {
|
||||
@@ -781,10 +832,14 @@ mod tests {
|
||||
.as_array()
|
||||
.expect("table content rows");
|
||||
// 表头行:两个单元格
|
||||
let header_row = &content_rows[0]["content"].as_array().expect("header row cells");
|
||||
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");
|
||||
let h1_cell = &header_row[0]["content"]
|
||||
.as_array()
|
||||
.expect("header cell paragraphs");
|
||||
assert!(!h1_cell.is_empty(), "表头第一个单元格不应为空");
|
||||
// 表头第二个单元格(空):单元格的 content 为 [{type:"paragraph", content:[]}]
|
||||
// 段落层的 content 应为空数组
|
||||
@@ -797,7 +852,9 @@ mod tests {
|
||||
);
|
||||
|
||||
// 数据行
|
||||
let data_row = &content_rows[1]["content"].as_array().expect("data row cells");
|
||||
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"];
|
||||
@@ -812,4 +869,69 @@ mod tests {
|
||||
let d2_inline = d2_par_content.as_array().expect("data cell para content");
|
||||
assert!(!d2_inline.is_empty(), "非空数据单元格不应为空");
|
||||
}
|
||||
|
||||
// 标题优先级:frontmatter title > H1 > 文件名。
|
||||
|
||||
#[test]
|
||||
fn parse_markdown_title_uses_frontmatter_title() {
|
||||
let parsed = parse_markdown_page(
|
||||
"---\ntitle: Frontmatter Title\n---\n# H1 Heading\nbody\n",
|
||||
"file.md",
|
||||
);
|
||||
assert_eq!(parsed.title, "Frontmatter Title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_markdown_title_uses_first_h1_when_no_frontmatter_title() {
|
||||
let parsed = parse_markdown_page("# H1 Title\nbody\n## Not H1\n", "file.md");
|
||||
assert_eq!(parsed.title, "H1 Title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_markdown_title_falls_back_to_filename() {
|
||||
let parsed = parse_markdown_page("plain text\nmore text\n", "My File.md");
|
||||
assert_eq!(parsed.title, "My File");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_markdown_title_prefers_frontmatter_over_h1() {
|
||||
let parsed = parse_markdown_page("---\ntitle: FM Title\n---\n# H1 here\n", "file.md");
|
||||
assert_eq!(parsed.title, "FM Title");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_markdown_title_handles_quoted_double() {
|
||||
let parsed = parse_markdown_page("---\ntitle: \"Quoted Double\"\n---\nbody\n", "file.md");
|
||||
assert_eq!(parsed.title, "Quoted Double");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_markdown_title_handles_quoted_single() {
|
||||
let parsed = parse_markdown_page("---\ntitle: 'Single Quoted'\n---\nbody\n", "file.md");
|
||||
assert_eq!(parsed.title, "Single Quoted");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_markdown_title_uses_h1_when_frontmatter_has_no_title() {
|
||||
let parsed = parse_markdown_page("---\nother: value\n---\n# From H1\n", "file.md");
|
||||
assert_eq!(parsed.title, "From H1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_markdown_title_uses_filename_when_body_has_no_h1() {
|
||||
let parsed = parse_markdown_page("---\ntags: foo\n---\nplain text\n", "NoH1.md");
|
||||
assert_eq!(parsed.title, "NoH1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_markdown_title_handles_atx_closing_markers() {
|
||||
let parsed = parse_markdown_page("# Heading With Closing #\n", "file.md");
|
||||
assert_eq!(parsed.title, "Heading With Closing");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_markdown_title_ignores_hash_inside_code_block() {
|
||||
let parsed = parse_markdown_page("```md\n# Not A Title\n```\nplain text\n", "Fallback.md");
|
||||
assert_eq!(parsed.title, "Fallback");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user