Add zero-pad option for numbered chapter titles
A "Zero-pad #" checkbox pads chapter titles that default to their position number with leading zeros to a uniform width (the digit count of the largest chapter number, e.g. 03. of 12). Padding never applies to "# Title:" headers or manual overrides. The setting persists in config. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -69,7 +69,9 @@ Terminal=false
|
||||
auto-derived one (the field shows the automatic title as a hint). Leaving it
|
||||
blank falls back to a `# Title:` line in the header, and if there is none, to
|
||||
the chapter's **position in the left pane** followed by a period (`1.`, `2.`,
|
||||
…). Titles are saved to `titles.json` and sync with git.
|
||||
…). Tick **Zero-pad #** in the top bar to pad those numbered titles with
|
||||
leading zeros to a uniform width (e.g. `03.` when there are 12 chapters).
|
||||
Titles are saved to `titles.json` and sync with git.
|
||||
5. Set the **Export** path (type it, or click **📂** to choose the `.odt` file in a
|
||||
native save dialog) and press **Export ODT** to produce the concatenated
|
||||
document. Each file becomes a chapter headed by its title, optionally followed
|
||||
|
||||
+49
-10
@@ -294,17 +294,28 @@ impl App {
|
||||
};
|
||||
}
|
||||
|
||||
/// Digits to zero-pad a defaulted chapter number to: the width of the largest
|
||||
/// chapter number when padding is enabled, otherwise 1 (no padding).
|
||||
fn index_pad_width(&self) -> usize {
|
||||
if self.config.zero_pad_index {
|
||||
self.files.len().to_string().len().max(1)
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
/// The title a chapter at `idx` would get without a manual override: its
|
||||
/// `# Title:` header, or its 1-based position (e.g. "3."). Shown as the hint
|
||||
/// in the chapter-title field.
|
||||
fn auto_title(&self, idx: usize, markdown: &str) -> String {
|
||||
let header = crate::preprocess::parse(markdown, &self.config.draft_marker);
|
||||
resolve_chapter_title(None, header.title.as_deref(), idx)
|
||||
resolve_chapter_title(None, header.title.as_deref(), idx, self.index_pad_width())
|
||||
}
|
||||
|
||||
fn export_odt(&mut self) {
|
||||
self.save_current();
|
||||
let marker = self.config.draft_marker.clone();
|
||||
let pad_width = self.index_pad_width();
|
||||
let mut chapters = Vec::new();
|
||||
for (i, name) in self.files.iter().enumerate() {
|
||||
let raw = std::fs::read_to_string(self.path_for(name)).unwrap_or_default();
|
||||
@@ -318,6 +329,7 @@ impl App {
|
||||
self.titles.get(name).map(String::as_str),
|
||||
header.title.as_deref(),
|
||||
i,
|
||||
pad_width,
|
||||
);
|
||||
chapters.push(Chapter {
|
||||
title,
|
||||
@@ -471,6 +483,17 @@ impl App {
|
||||
if marker_resp.lost_focus() {
|
||||
self.config.save();
|
||||
}
|
||||
ui.separator();
|
||||
if ui
|
||||
.checkbox(&mut self.config.zero_pad_index, "Zero-pad #")
|
||||
.on_hover_text(
|
||||
"When a chapter title defaults to its number, pad it with \
|
||||
leading zeros (e.g. 03. of 12).",
|
||||
)
|
||||
.changed()
|
||||
{
|
||||
self.config.save();
|
||||
}
|
||||
});
|
||||
ui.add_space(4.0);
|
||||
});
|
||||
@@ -815,18 +838,20 @@ fn render_preview(ui: &mut egui::Ui, markdown: &str) {
|
||||
}
|
||||
|
||||
/// Resolve a chapter's title: a non-empty manual `override_title` wins, then the
|
||||
/// `# Title:` header value, otherwise the chapter's 1-based position (e.g. "3.").
|
||||
/// `# Title:` header value, otherwise the chapter's 1-based position followed by
|
||||
/// a period (e.g. "3."), zero-padded to `pad_width` digits (`1` = no padding).
|
||||
fn resolve_chapter_title(
|
||||
override_title: Option<&str>,
|
||||
header_title: Option<&str>,
|
||||
index: usize,
|
||||
pad_width: usize,
|
||||
) -> String {
|
||||
override_title
|
||||
.map(str::trim)
|
||||
.filter(|t| !t.is_empty())
|
||||
.or_else(|| header_title.map(str::trim).filter(|t| !t.is_empty()))
|
||||
.map(|t| t.to_string())
|
||||
.unwrap_or_else(|| format!("{}.", index + 1))
|
||||
.unwrap_or_else(|| format!("{:0width$}.", index + 1, width = pad_width))
|
||||
}
|
||||
|
||||
/// Count words in a string: runs of non-whitespace separated by whitespace.
|
||||
@@ -873,19 +898,33 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn chapter_title_falls_back_to_position() {
|
||||
// No override, no header title -> 1-based index with a period.
|
||||
assert_eq!(resolve_chapter_title(None, None, 0), "1.");
|
||||
assert_eq!(resolve_chapter_title(None, None, 2), "3.");
|
||||
// No override, no header title -> 1-based index with a period (width 1).
|
||||
assert_eq!(resolve_chapter_title(None, None, 0, 1), "1.");
|
||||
assert_eq!(resolve_chapter_title(None, None, 2, 1), "3.");
|
||||
// Header title is used when present.
|
||||
assert_eq!(resolve_chapter_title(None, Some("The Gate"), 4), "The Gate");
|
||||
assert_eq!(resolve_chapter_title(None, Some("The Gate"), 4, 1), "The Gate");
|
||||
// Override wins over everything, even a header title.
|
||||
assert_eq!(
|
||||
resolve_chapter_title(Some("My Override"), Some("The Gate"), 4),
|
||||
resolve_chapter_title(Some("My Override"), Some("The Gate"), 4, 1),
|
||||
"My Override"
|
||||
);
|
||||
// Blank/whitespace override or header title are ignored.
|
||||
assert_eq!(resolve_chapter_title(Some(" "), None, 1), "2.");
|
||||
assert_eq!(resolve_chapter_title(Some(""), Some(" "), 6), "7.");
|
||||
assert_eq!(resolve_chapter_title(Some(" "), None, 1, 1), "2.");
|
||||
assert_eq!(resolve_chapter_title(Some(""), Some(" "), 6, 1), "7.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chapter_number_zero_pads_to_width() {
|
||||
// Width 2 pads single digits; wider numbers are unaffected.
|
||||
assert_eq!(resolve_chapter_title(None, None, 0, 2), "01.");
|
||||
assert_eq!(resolve_chapter_title(None, None, 8, 2), "09.");
|
||||
assert_eq!(resolve_chapter_title(None, None, 11, 2), "12.");
|
||||
assert_eq!(resolve_chapter_title(None, None, 4, 3), "005.");
|
||||
// Padding never applies to a real title.
|
||||
assert_eq!(
|
||||
resolve_chapter_title(None, Some("The Gate"), 0, 3),
|
||||
"The Gate"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -16,6 +16,10 @@ pub struct Config {
|
||||
/// An empty value disables header splitting.
|
||||
#[serde(default = "default_marker")]
|
||||
pub draft_marker: String,
|
||||
/// When a chapter title defaults to its position number, pad it with leading
|
||||
/// zeros to the width of the largest chapter number (e.g. "03." of 12).
|
||||
#[serde(default)]
|
||||
pub zero_pad_index: bool,
|
||||
}
|
||||
|
||||
/// Default header/body separator, matching the manuscript drafting convention.
|
||||
@@ -32,6 +36,7 @@ impl Default for Config {
|
||||
export_path,
|
||||
show_preview: false,
|
||||
draft_marker: default_marker(),
|
||||
zero_pad_index: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user