TOC-1: preserve right-indent separators and expose their picker

This commit is contained in:
moppeeer
2026-10-18 15:47:55 +02:00
parent d98b2cce01
commit e8ec23a88d
7 changed files with 97 additions and 28 deletions
+2
View File
@@ -346,6 +346,8 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
## Progress log
- **2026-10-18** · TOC separators preserve right-indent tabs through the dialog and IDML, including localized `$ID/^y` values. A separator picker exposes Tab Character, Right Indent Tab, Em Space and En Space. Existing paragraph tab leaders now work with the tutorial's TOC workflow. No readiness percentage change.
Newest first.
- **2026-10-16** · Threading: the loaded text cursor threads into a text frame inside a group (it hit the group and showed it could not thread); a plain click still selects the group (#1075). Gap 27 closed. No readiness percentage change.
+29
View File
@@ -31,6 +31,35 @@ pub const FRAME_BREAK: char = '\u{E003}';
pub const PAGE_BREAK: char = '\u{E004}';
pub const INDENT_HERE: char = '\u{E005}';
pub const RIGHT_INDENT_TAB: char = '\u{E006}';
/// Encode a table-of-contents separator using InDesign's special-character notation.
pub fn toc_separator_notation(text: &str) -> String {
text.replace('^', "^^").replace('\t', "^t").replace(RIGHT_INDENT_TAB, "^y").replace('\u{2003}', "^m").replace('\u{2002}', "^>")
}
/// Decode TOC notation, preserving unknown codes and an escaped literal caret.
pub fn toc_separator_text(notation: &str) -> String {
let mut text = String::new();
let mut chars = notation.chars();
while let Some(ch) = chars.next() {
if ch != '^' {
text.push(ch);
continue;
}
match chars.next() {
Some('t') => text.push('\t'),
Some('y') => text.push(RIGHT_INDENT_TAB),
Some('m') => text.push('\u{2003}'),
Some('>') => text.push('\u{2002}'),
Some('^') | None => text.push('^'),
Some(other) => {
text.push('^');
text.push(other);
}
}
}
text
}
pub const NEXT_PAGE_NUMBER: char = '\u{E007}';
pub const PREV_PAGE_NUMBER: char = '\u{E008}';
/// Anchor of a table: alone in its paragraph, whose [`ParaFormat::table`] names the table.
+1 -1
View File
@@ -510,7 +510,7 @@ fn table_of_contents_definition_round_trips_through_idml_and_updates() {
s.execute(
"toc.generate",
&json!({"page": page, "title": "Inside", "titleStyle": basic, "entries": [
{"style": "Cover Title", "level": 1, "pageNumber": "before", "separator": " ^ "},
{"style": "Cover Title", "level": 1, "pageNumber": "before", "separator": " ^ \u{E006} "},
{"style": "Headline", "level": 2, "entryStyle": basic, "pageNumber": "none"},
]}),
)
+17 -22
View File
@@ -837,30 +837,25 @@ pub fn digits_in(s: &str) -> Option<designcraft_doc::Digits> {
[Digits::Default, Digits::Arabic, Digits::Hindi, Digits::Farsi, Digits::Native].into_iter().find(|d| digits_out(*d) == s)
}
/// A TOC separator in IDML's special-character notation: `^t` is a tab, `^^` a caret.
/// A TOC separator in IDML's special-character notation.
pub fn toc_separator_out(s: &str) -> String {
s.replace('^', "^^").replace('\t', "^t")
designcraft_doc::story::toc_separator_notation(s)
}
/// IDML TOC separator → text (`^t` and `^y`, the right indent tab, are tabs; other codes stay).
/// IDML may prefix a separator with its localized-string marker.
pub fn toc_separator_in(s: &str) -> String {
// InDesign writes its built-in values with the `$ID/` prefix (`$ID/^y`, the right indent tab).
let s = s.strip_prefix("$ID/").unwrap_or(s);
let mut out = String::new();
let mut it = s.chars();
while let Some(c) = it.next() {
if c != '^' {
out.push(c);
continue;
}
match it.next() {
Some('t' | 'y') => out.push('\t'),
Some('^') => out.push('^'),
Some(o) => {
out.push('^');
out.push(o);
}
None => out.push('^'),
}
designcraft_doc::story::toc_separator_text(s.strip_prefix("$ID/").unwrap_or(s))
}
#[cfg(test)]
mod toc_separator_tests {
use super::*;
#[test]
fn localized_right_indent_tab_is_not_an_ordinary_tab() {
assert_eq!(toc_separator_in("$ID/^y"), "\u{E006}");
assert_eq!(toc_separator_in("$ID/ ^y "), " \u{E006} ");
assert_eq!(toc_separator_in("^t"), "\t");
assert_eq!(toc_separator_out("\u{E006}"), "^y");
assert_eq!(toc_separator_in("^^y ^q ^"), "^y ^q ^");
}
out
}
+1 -1
View File
@@ -4216,7 +4216,7 @@ fn toc_style_entries_read_id_prefixed_separators_and_styles() {
let d = import_idml(&fixture_with_table_styles(TOC_STYLES, TOC_STORY)).unwrap();
let toc = d.toc.as_ref().expect("the story's TOC style is imported");
let entry = toc.entries.iter().find(|e| e.style == "Heading 1").unwrap();
assert_eq!(entry.separator, None, "`$ID/^y` is the right indent tab, the default separator, not text: {:?}", entry.separator);
assert_eq!(entry.separator.as_deref(), Some("\u{E006}"), "right-indent tabs must not become ordinary tab stops");
assert_eq!(entry.entry_style.as_deref(), Some("TOC/TOC body text"));
assert_eq!(entry.page_number_style.as_deref(), Some("TOC Page Number"));
assert_eq!(toc.title_style.as_deref(), Some("TOC/TOC Header"));
+40 -3
View File
@@ -6212,11 +6212,11 @@ struct TocRow {
}
fn toc_separator_shown(s: &str) -> String {
s.replace('\t', "^t").replace('\u{2003}', "^m").replace('\u{2002}', "^>")
designcraft_doc::story::toc_separator_notation(s)
}
fn toc_separator_typed(s: &str) -> String {
s.replace("^t", "\t").replace("^m", "\u{2003}").replace("^>", "\u{2002}")
designcraft_doc::story::toc_separator_text(s)
}
fn toc_rows(d: &Dialog) -> Vec<TocRow> {
@@ -6407,7 +6407,19 @@ fn toc_dialog(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
toc_style_combo(ui, "page_number", &char_styles, none, &mut r.page_number_style);
ui.end_row();
crate::rtl::label(ui, tr("Between Entry and Number:"));
ui.add(egui::TextEdit::singleline(&mut r.separator).desired_width(80.0).horizontal_align(egui::Align::Min));
ui.horizontal(|ui| {
ui.add(egui::TextEdit::singleline(&mut r.separator).desired_width(80.0).horizontal_align(egui::Align::Min));
ui.menu_button("▸", |ui| {
for (label, code) in [("Tab Character", "^t"), ("Right Indent Tab", "^y"), ("Em Space", "^m"), ("En Space", "^>")] {
if ui.button(tr(label)).clicked() {
r.separator.push_str(code);
ui.close();
}
}
})
.response
.on_hover_text(tr("Insert Special Character"));
});
crate::rtl::label(ui, tr("Style:"));
toc_style_combo(ui, "separator", &char_styles, none, &mut r.separator_style);
ui.end_row();
@@ -7957,6 +7969,31 @@ mod tests {
assert_eq!(opts(&app), before);
}
#[test]
fn toc_right_indent_separator_survives_the_dialog_and_update() {
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
app.run("file.newSample", json!({})).unwrap();
open_toc(&mut app);
app.ui.dialog.as_mut().unwrap().fields.insert(
"included".into(),
json!([
{"style": "Headline", "level": 1, "separator": " ^y "}
]),
);
confirm(&mut app).unwrap();
let separator = format!(" {} ", designcraft_doc::story::RIGHT_INDENT_TAB);
let d = &app.session.doc().unwrap().doc;
let toc = d.toc.as_ref().unwrap();
assert_eq!(toc.entries[0].separator.as_deref(), Some(separator.as_str()));
assert!(d.story(toc.story).unwrap().text.contains(&separator));
app.run("toc.update", json!({})).unwrap();
open_toc(&mut app);
assert_eq!(toc_rows(app.ui.dialog.as_ref().unwrap())[0].separator, " ^y ");
for text in ["^y", "^t^y", "^m^>", "unknown ^q and trailing ^", "a\t\u{E006}\u{2003}\u{2002}z"] {
assert_eq!(toc_separator_typed(&toc_separator_shown(text)), text);
}
}
#[test]
fn toc_dialog_adds_a_style_and_ok_makes_the_table_of_contents() {
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
+7 -1
View File
@@ -1,6 +1,6 @@
# UI and interaction parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-15 · **Change:** minor (live corners work; document views row: scrollbars, Alt pans while editing text; Tabs panel listed as present (#343); spring-loaded Zoom navigation; paragraph style override feedback and clearing) · **Target:** Adobe InDesign 2026 (21.6, Medium Dark, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (live corners work; document views row: scrollbars, Alt pans while editing text; Tabs panel listed as present (#343); spring-loaded Zoom navigation; paragraph style override feedback and clearing) · **Target:** Adobe InDesign 2026 (21.6, Medium Dark, macOS)
Tools, handles, snapping, nudging, modifiers, shortcuts, panels, menus, context menus and feel,
against InDesign 2026. The look was measured from the running app
@@ -70,6 +70,12 @@ history (DC-1).
## Remaining effort
The Table of Contents separator field accepts `^y` (Right Indent Tab), with a special-character
picker alongside it. Literal carets use `^^`; ordinary tabs, em/en spaces and surrounding spaces
survive reopening. IDML preserves right-indent separators, including `$ID/^y`, so updating an
imported TOC keeps page numbers at the right edge and inherits paragraph tab leaders. This covers
10:54–16:08 of [Michael Bullo's TOC tutorial](https://www.youtube.com/watch?v=m4y2tm6LH_c).
Applying a paragraph or character style now removes local values equal to the applied style,
so text used to capture a style follows later edits to that style. Different local formatting
and named character styles stay intact. Paragraph-style application treats selection endpoints