Strip comments and header metadata on ODT export

Each file is now cleaned before conversion: HTML comments are removed
and everything above the configurable Draft marker (default
"### Rough Draft:") is dropped. A "# Title:" line becomes the chapter
heading and a "# Slug:" line becomes an italic caption beneath it.

- new preprocess module (strip_comments + header parse) with tests
- Chapter gains a slug rendered via a Chapter_20_Caption style, italic
  at both paragraph-style and character-run level
- draft_marker config setting + top-bar field
- export title priority: override > # Title: > first # heading > filename

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
landon
2026-07-25 19:01:45 -05:00
parent f473b63ee3
commit 507e0ab724
6 changed files with 279 additions and 9 deletions
+31 -5
View File
@@ -276,21 +276,33 @@ impl App {
fn export_odt(&mut self) {
self.save_current();
let marker = self.config.draft_marker.clone();
let mut chapters = Vec::new();
for name in &self.files {
let markdown = std::fs::read_to_string(self.path_for(name)).unwrap_or_default();
// The leading `# heading` is always consumed as the chapter-title
// slot; a manual override, if set, replaces the derived title.
let (auto_title, body) = split_title(&markdown, name);
let raw = std::fs::read_to_string(self.path_for(name)).unwrap_or_default();
// Strip comments + the editorial header, and lift out any
// `# Title:` / `# Slug:` metadata.
let header = crate::preprocess::parse(&raw, &marker);
// Title priority: manual override > `# Title:` metadata > the body's
// leading `# heading` (consumed) > file name. When a title comes from
// metadata/override, the body is left intact; otherwise the leading
// `# heading` is consumed into the title slot to avoid duplication.
let (title, body) = if let Some(t) = &header.title {
(t.clone(), header.body.clone())
} else {
split_title(&header.body, name)
};
let title = self
.titles
.get(name)
.map(|t| t.trim())
.filter(|t| !t.is_empty())
.map(|t| t.to_string())
.unwrap_or(auto_title);
.unwrap_or(title);
chapters.push(Chapter {
title,
slug: header.slug.clone(),
markdown: body,
});
}
@@ -376,6 +388,20 @@ impl App {
{
self.config.save();
}
ui.separator();
ui.label("Draft marker:")
.on_hover_text(
"Text above this line in each file (its header) is dropped on export; \
# Title: / # Slug: lines become the chapter heading and caption. \
Leave blank to export files whole.",
);
let marker_resp = ui.add(
egui::TextEdit::singleline(&mut self.config.draft_marker)
.desired_width(140.0),
);
if marker_resp.lost_focus() {
self.config.save();
}
});
ui.add_space(4.0);
});