Fix empty section body when content follows a property drawer
section_body_text() checked for a drawer START (:NAME:) before checking for a drawer END (:END:). But :END: itself matches the generic :NAME: pattern, so the end line re-opened a drawer instead of closing it, leaving in_drawer set and swallowing the body that followed. A section whose only content came after a property drawer therefore rendered an empty /body (and, by extension, empty fuzzy-resolve bodies and GUI sections). Reorder the two checks: when already inside a drawer, handle :END: first and consume the line, before the drawer-start pattern can match it. This also leaves planning-line and leading-blank handling intact. Add regression tests: body after a drawer directly under the headline, and body after a planning line plus drawer. Server tests: 63 passed.
This commit is contained in:
parent
2f040bf810
commit
de43a3721d
|
|
@ -1126,6 +1126,18 @@ fn section_body_text(content: &str, s: &Section) -> String {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle drawer end first: when inside a drawer, consume lines until
|
||||||
|
// :END:. This must precede the drawer-start check below, because
|
||||||
|
// :END: also matches the generic :NAME: drawer-start pattern and
|
||||||
|
// would otherwise be misread as opening a new drawer (swallowing the
|
||||||
|
// body that follows).
|
||||||
|
if in_drawer {
|
||||||
|
if trimmed == ":END:" {
|
||||||
|
in_drawer = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle drawer start
|
// Handle drawer start
|
||||||
if trimmed.starts_with(':') && trimmed.ends_with(':') && trimmed.len() > 2 {
|
if trimmed.starts_with(':') && trimmed.ends_with(':') && trimmed.len() > 2 {
|
||||||
let drawer_name = &trimmed[1..trimmed.len() - 1];
|
let drawer_name = &trimmed[1..trimmed.len() - 1];
|
||||||
|
|
@ -1135,14 +1147,6 @@ fn section_body_text(content: &str, s: &Section) -> String {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle drawer end
|
|
||||||
if in_drawer {
|
|
||||||
if trimmed == ":END:" {
|
|
||||||
in_drawer = false;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is actual content
|
// This is actual content
|
||||||
seen_content = true;
|
seen_content = true;
|
||||||
result_lines.push(line);
|
result_lines.push(line);
|
||||||
|
|
@ -2408,6 +2412,29 @@ mod tests {
|
||||||
(dir, state)
|
(dir, state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn body_after_drawer_is_not_swallowed() {
|
||||||
|
// Regression: :END: matched the generic :NAME: drawer-start pattern,
|
||||||
|
// re-opening a drawer and swallowing the body that followed. A section
|
||||||
|
// whose only content follows a property drawer must still expose it.
|
||||||
|
let (_d, state) = state_with(
|
||||||
|
"#+TITLE: W\n* Task A\n:PROPERTIES:\n:ID: a1\n:END:\nThis is the body of A.\n",
|
||||||
|
);
|
||||||
|
let body = state.section_body("w", "a1").unwrap();
|
||||||
|
assert_eq!(body, "This is the body of A.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn body_after_planning_and_drawer() {
|
||||||
|
// Planning line + property drawer, then body. All three preamble parts
|
||||||
|
// are stripped; only the body remains.
|
||||||
|
let (_d, state) = state_with(
|
||||||
|
"#+TITLE: W\n* TODO Blocked\nDEADLINE: <2025-01-01>\n:PROPERTIES:\n:BLOCKER: approval\n:END:\nWaiting on external input.\n",
|
||||||
|
);
|
||||||
|
let body = state.section_body("w", "blocked").unwrap();
|
||||||
|
assert_eq!(body, "Waiting on external input.");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_delete_section() {
|
fn test_delete_section() {
|
||||||
let (_d, state) = state_with("#+TITLE: W\n* One\n* Two\n* Three\n");
|
let (_d, state) = state_with("#+TITLE: W\n* One\n* Two\n* Three\n");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue