mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-10 18:40:27 -06:00
TOC-1: preserve right-indent separators and expose their picker
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user