fix: parse crlf multibyte headings safely

This commit is contained in:
lucaronin
2026-04-30 20:18:31 +02:00
parent 50c304fce5
commit 4a03f1675e
2 changed files with 52 additions and 20 deletions

View File

@@ -25,20 +25,21 @@ pub(super) fn slug_to_title(stem: &str) -> String {
/// Returns `None` if no H1 is found on the first non-empty line.
pub(super) fn extract_h1_title(content: &str) -> Option<String> {
let body = strip_frontmatter(content);
for line in body.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if let Some(title) = trimmed.strip_prefix("# ") {
let title = strip_markdown_chars(title).trim().to_string();
if !title.is_empty() {
return Some(title);
}
}
break; // first non-empty line is not H1
}
None
let title = first_non_empty_line(body).and_then(markdown_h1_text)?;
non_empty_trimmed(&strip_markdown_chars(title)).map(str::to_string)
}
fn non_empty_trimmed(value: &str) -> Option<&str> {
let trimmed = value.trim();
(!trimmed.is_empty()).then_some(trimmed)
}
fn first_non_empty_line(value: &str) -> Option<&str> {
value.lines().map(str::trim).find(|line| !line.is_empty())
}
fn markdown_h1_text(line: &str) -> Option<&str> {
line.strip_prefix("# ").and_then(non_empty_trimmed)
}
/// Extract the display title for a note.
@@ -174,16 +175,17 @@ pub(super) fn extract_snippet(content: &str) -> String {
}
fn without_h1_line(s: &str) -> Option<&str> {
for (i, line) in s.lines().enumerate() {
if line.trim().starts_with("# ") {
// Return everything after this line
let offset: usize = s.lines().take(i + 1).map(|l| l.len() + 1).sum();
return Some(&s[offset.min(s.len())..]);
let mut offset = 0;
for line in s.split_inclusive('\n') {
let trimmed = line.trim_end_matches(['\r', '\n']).trim();
if trimmed.starts_with("# ") {
return Some(&s[offset + line.len()..]);
}
// If we hit non-empty non-heading content first, there's no H1 to skip
if !line.trim().is_empty() {
if !trimmed.is_empty() {
return None;
}
offset += line.len();
}
None
}
@@ -609,6 +611,16 @@ mod tests {
);
}
#[test]
fn test_extract_snippet_crlf_chinese_h1_table_content() {
let content =
"\r\n\r\n# 上海复盘\r\n\r\n| 指标 | 值 |\r\n| --- | --- |\r\n| 收入 | 增长 |\r\n\r\n正文包含中文字符。";
let snippet = extract_snippet(content);
assert!(snippet.contains("指标"));
assert!(snippet.contains("正文包含中文字符"));
}
// --- count_body_words tests ---
#[test]

View File

@@ -27,6 +27,11 @@ test.describe('Multibyte note-list search', () => {
].join('\n'),
'utf-8',
)
fs.writeFileSync(
path.join(noteDir, 'chinese-crlf-table.md'),
'\r\n\r\n# 上海复盘\r\n\r\n| 指标 | 值 |\r\n| --- | --- |\r\n| 收入 | 增长 |\r\n\r\n正文包含中文字符。',
'utf-8',
)
await openFixtureVaultDesktopHarness(page, tempVaultDir)
})
@@ -49,4 +54,19 @@ test.describe('Multibyte note-list search', () => {
'multibyte-search-boundary',
)
})
test('opening CRLF Chinese table content keeps the note searchable @smoke', async ({ page }) => {
await showNoteListSearch(page)
const noteList = page.getByTestId('note-list-container')
const searchInput = page.getByPlaceholder('Search notes...')
await searchInput.fill('收入')
await expect(page.getByTestId('note-list-search-loading')).toHaveCount(0)
await expect(noteList.getByText('上海复盘', { exact: true })).toBeVisible()
await noteList.getByText('上海复盘', { exact: true }).click()
await expect(page.getByTestId('breadcrumb-filename-trigger')).toContainText('chinese-crlf-table')
await expect(page.locator('.editor-content-wrapper').getByText('指标')).toBeVisible()
})
})