References dialogs: Caption, Index, Table of Contents, Create Source (#402) (#410)

* References dialogs: Caption, Index, Table of Contents, Create Source (#402)

- Caption: labels (built-in plus New/Delete Label, kept with the interface
  settings), above/below the item, leave the label out, Numbering (format,
  chapter number from a heading level with a separator) -> SEQ \* \s and
  STYLEREF n \s fields; Update Field renumbers SEQ with \s restarts, \r, \c, \h.
- Index: indented/run-in, columns, right-aligned page numbers with tab leader,
  letter headings -> INDEX \h \c \e \r; Mark Index Entry (subentry,
  cross-reference, page-range bookmark, bold/italic page numbers, Mark All)
  -> XE \t \r \b \i. The index is rebuilt honouring them; columns become a
  continuous section of their own. Index commands move to cmd/index.rs.
- Custom Table of Contents: page numbers, right align, tab leader,
  hyperlinks, levels, Options (heading styles, other styles with levels,
  outline levels, TC fields), Modify for TOC 1-9 -> TOC \o \h \z \u \t \f
  \l \n \p; updating honours them (hyperlinks to _Toc bookmarks).
- Source Manager and Create/Edit Source: source types with their fields,
  show every bibliography field (new Source fields); Insert Citation menu
  lists the sources and Add New Source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt (#402)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Valentin Klein 2026-10-11 21:56:41 +02:00 committed by GitHub
parent 4ca58d67e1
commit 56f9456f03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 3332 additions and 129 deletions

View File

@ -131,6 +131,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
| Date | What landed |
|---|---|
| 2026-10-11 | References dialogs (#402): Caption (labels, chapter numbering), Index and Mark Entry, Custom Table of Contents, Source Manager and Create/Edit Source, with TOC/INDEX/XE/SEQ switches honoured on update |
| 2026-10-11 | Document grid (#391): Page Setup › Document Grid sets lines per page and characters per line (`w:docGrid` round trip); lines snap to the line pitch, East Asian characters to the character pitch; View › Gridlines shows it |
| 2026-10-11 | Move tracking (#400): `w:moveFrom`/`w:moveTo` and their named ranges kept in DOCX, a tracked cut and paste of a paragraph or sentence records a move, accept/reject at either end resolves both, green double strikethrough/underline and "Moved from/to" balloons |
| 2026-10-11 | Footnote and Endnote dialog from the References › Footnotes launcher: options per section or document (below text, end of section, number format, start at, restart each section or page), custom marks, convert footnotes ↔ endnotes; kept in DOCX (#385) |

View File

@ -331,7 +331,7 @@ impl Document {
pub struct Source {
/// Unique tag cited by CITATION fields.
pub tag: String,
/// book, article, website, report, film, other
/// book, bookSection, article (journal), periodical, conference, report, website, film, other
pub kind: String,
/// "Last, First; Last, First"
pub author: String,
@ -343,6 +343,15 @@ pub struct Source {
pub volume: String,
pub pages: String,
pub url: String,
/// More bibliography fields (Create Source › Show All Bibliography Fields).
pub editor: String,
pub edition: String,
pub issue: String,
pub month: String,
pub day: String,
/// Date accessed (websites).
pub accessed: String,
pub doi: String,
}
/// A custom document property (File › Info › Properties › Custom). Citation managers keep

View File

@ -319,28 +319,6 @@ pub fn specs() -> Vec<CommandSpec> {
update_generated(s)?;
sel_result(s)
}),
CommandSpec::new("references.markEntry", "Mark Entry", "References › Index", |s, v| {
let entry = match p::str(v, "entry") {
Some(e) => e.to_string(),
None => s.selected_text().trim().to_string(),
};
if entry.is_empty() {
return Err(CmdError::Params("select a word or give `entry`".into()));
}
let (_, b) = s.sel.ordered();
s.doc.insert_object(&b, InlineObject::Field { instr: format!("XE \"{}\"", entry.replace('"', "")), result: String::new(), locked: false, code: false }, &CharProps { hidden: Some(true), ..Default::default() })?;
Ok(json!({"entry": entry}))
})
.params(r#"{"entry"?: string}"#)
.key(super::non_mac("Alt+Shift+X")),
CommandSpec::new("references.index", "Insert Index", "References › Index", |s, _| {
generated_list(s, "INDEX", "Index")?;
sel_result(s)
}),
CommandSpec::new("references.updateIndex", "Update Index", "References › Index", |s, _| {
update_generated(s)?;
sel_result(s)
}),
CommandSpec::new("references.markCitation", "Mark Citation", "References › Table of Authorities", |s, v| {
let entry = p::str(v, "entry").map(str::to_string).unwrap_or_else(|| s.selected_text().trim().to_string());
if entry.is_empty() {
@ -547,8 +525,16 @@ fn sources(s: &mut Session, v: &Value) -> CmdResult {
.collect();
src.tag = format!("{base}{}", src.year);
}
s.doc.sources.retain(|x| x.tag != src.tag);
s.doc.sources.push(src);
// Edit Source keeps the source's place in the list and refreshes its citations.
match s.doc.sources.iter().position(|x| x.tag == src.tag) {
Some(i) => {
if let Some(x) = s.doc.sources.get_mut(i) {
*x = src;
}
update_citations(s)?;
}
None => s.doc.sources.push(src),
}
s.touch();
}
if let Some(t) = p::str(v, "remove") {
@ -685,7 +671,7 @@ fn refresh_citations(s: &mut Session) -> Result<(), CmdError> {
/// Generated lists (BIBLIOGRAPHY, INDEX, table of figures, TOA): a heading paragraph holding
/// the field, then entry paragraphs marked with the "GeneratedEntry" style family.
fn generated_list(s: &mut Session, instr: &str, title: &str) -> Result<(), CmdError> {
pub(crate) fn generated_list(s: &mut Session, instr: &str, title: &str) -> Result<(), CmdError> {
let at = delete_selection(s)?;
if at.story != StoryRef::Body || at.path.depth() > 0 {
return Err(CmdError::Failed("lists go in the main text".into()));
@ -734,6 +720,11 @@ pub fn update_generated(s: &mut Session) -> Result<(), CmdError> {
i += 1;
continue;
};
if kind.starts_with("INDEX") {
super::index::rebuild(s, i, None)?;
i += 1;
continue;
}
// Remove old entries.
while s.doc.body.get(i + 1).and_then(|b| b.as_para()).is_some_and(|p| p.props.style.as_deref() == Some(ENTRY_STYLE)) {
s.doc.remove_block(StoryRef::Body, &Path::top(i + 1))?;

View File

@ -0,0 +1,440 @@
//! References › Index: Mark Entry (XE fields), Insert Index and Update Index (the INDEX field).
//! Field switches follow ECMA-376 §17.16.5.32 (INDEX) and §17.16.5.79 (XE).
use std::collections::BTreeMap;
use serde_json::{Value, json};
use wordcraft_doc::para::InlineObject;
use wordcraft_doc::props::{CharProps, TabAlign, TabLeader, TabStop};
use wordcraft_doc::section::SectionStart;
use wordcraft_doc::{Block, Paragraph, Path, Pos, StoryRef};
use super::references::{FieldCode, leader};
use super::sel_result;
use crate::{CmdError, CmdResult, CommandSpec, Session, p};
/// The paragraph style generated lists use for their entries (see `citations.rs`).
const ENTRY_STYLE: &str = "ListEntry";
pub fn specs() -> Vec<CommandSpec> {
vec![
CommandSpec::new("references.markEntry", "Mark Entry", "References › Index", mark_entry).params(
r#"{"entry"?: string (the selection), "subentry"?: string, "crossReference"?: "See …", "bookmark"?: page-range bookmark, "bold"?: bool, "italic"?: bool, "all"?: bool (Mark All: every paragraph's first match of "text" or the selection)}"#,
)
.key(super::non_mac("Alt+Shift+X")),
CommandSpec::new("references.index", "Insert Index", "References › Index", insert_index).params(
r#"{"type"?: "indented|runIn", "columns"?: 1-4, "rightAlign"?: bool, "tabLeader"?: "dot|hyphen|underscore|none", "headings"?: bool (letter headings)} (replaces an existing index's settings)"#,
),
CommandSpec::new("references.updateIndex", "Update Index", "References › Index", |s, _| {
super::citations::update_generated(s)?;
sel_result(s)
}),
]
}
fn clean(t: &str, max: usize) -> String {
t.chars().filter(|c| *c != '"' && !c.is_control()).take(max).collect::<String>().trim().to_string()
}
/// The XE field for `references.markEntry`'s parameters: `XE "Main:Sub" \t "See X"` or
/// `\r bookmark`, then `\b` and `\i` for a bold or italic page number.
fn xe_instr(s: &Session, v: &Value, main: &str) -> Result<String, CmdError> {
let sub = p::str(v, "subentry").map(|t| clean(t, 200)).unwrap_or_default();
let mut instr = if sub.is_empty() { format!("XE \"{main}\"") } else { format!("XE \"{main}:{sub}\"") };
if let Some(see) = p::str(v, "crossReference").map(|t| clean(t, 200)).filter(|t| !t.is_empty()) {
instr.push_str(&format!(" \\t \"{see}\""));
} else if let Some(bm) = p::str(v, "bookmark").filter(|b| !b.trim().is_empty()) {
if !s.doc.bookmarks().iter().any(|(n, _)| n == bm) {
return Err(CmdError::Params(format!("no bookmark `{bm}`")));
}
instr.push_str(&format!(" \\r {}", bm.trim()));
}
if p::bool(v, "bold").unwrap_or(false) {
instr.push_str(" \\b");
}
if p::bool(v, "italic").unwrap_or(false) {
instr.push_str(" \\i");
}
Ok(instr)
}
fn mark_entry(s: &mut Session, v: &Value) -> CmdResult {
let main = clean(&p::str(v, "entry").map(str::to_string).unwrap_or_else(|| s.selected_text()), 200);
if main.is_empty() {
return Err(CmdError::Params("select a word or give `entry`".into()));
}
let instr = xe_instr(s, v, &main)?;
let hidden = CharProps { hidden: Some(true), ..Default::default() };
let field = || InlineObject::Field { instr: instr.clone(), result: String::new(), locked: false, code: false };
if !p::bool(v, "all").unwrap_or(false) {
let (_, b) = s.sel.ordered();
s.doc.insert_object(&b, field(), &hidden)?;
return Ok(json!({"entry": main, "marked": 1}));
}
// Mark All: the first whole-word, same-case match in every paragraph that hasn't this entry.
let text = p::str(v, "text").map(str::to_string).unwrap_or_else(|| {
let t = s.selected_text();
if t.trim().is_empty() { main.clone() } else { t.trim().to_string() }
});
if text.is_empty() {
return Err(CmdError::Params("nothing to look for".into()));
}
let mut marked = 0u32;
for path in s.doc.para_paths(StoryRef::Body) {
let Some(para) = s.doc.para(StoryRef::Body, &path) else { continue };
if para.props.style.as_deref().is_some_and(|st| st == ENTRY_STYLE || st.starts_with("TOC")) {
continue;
}
if para.objects.iter().any(|o| matches!(o, InlineObject::Field { instr: i, .. } if *i == instr)) {
continue;
}
let whole = |a: usize, b: usize| {
let before = para.text.get(..a).and_then(|t| t.chars().next_back()).is_none_or(|c| !c.is_alphanumeric());
let after = para.text.get(b..).and_then(|t| t.chars().next()).is_none_or(|c| !c.is_alphanumeric());
before && after
};
let Some(end) = para.text.match_indices(&text).map(|(a, m)| (a, a + m.len())).find(|(a, b)| whole(*a, *b)).map(|(_, b)| b) else {
continue;
};
s.doc.insert_object(&Pos { story: StoryRef::Body, path, off: end }, field(), &hidden)?;
marked += 1;
}
s.clamp_selection();
Ok(json!({"entry": main, "marked": marked}))
}
/// The INDEX field for `references.index`'s parameters: `\h "A"` letter headings, `\c "n"`
/// columns, `\e` with a tab for right-aligned page numbers, `\r` run-in subentries.
pub fn index_instr(v: &Value) -> String {
let mut s = String::from("INDEX");
if p::bool(v, "headings").unwrap_or(true) {
s.push_str(" \\h \"A\"");
}
let cols = p::u64(v, "columns").unwrap_or(1).clamp(1, 4);
if cols > 1 {
s.push_str(&format!(" \\c \"{cols}\""));
}
if p::bool(v, "rightAlign").unwrap_or(false) {
s.push_str(" \\e \"\t\"");
}
if p::str(v, "type") == Some("runIn") {
s.push_str(" \\r");
}
s
}
/// The body block holding the INDEX field, and its instruction.
fn index_field(s: &Session) -> Option<(usize, String)> {
s.doc.body.iter().enumerate().find_map(|(i, b)| {
b.as_para()?.objects.iter().find_map(|o| match o {
InlineObject::Field { instr, .. } if FieldCode::parse(instr).name == "INDEX" => Some((i, instr.clone())),
_ => None,
})
})
}
fn insert_index(s: &mut Session, v: &Value) -> CmdResult {
let instr = index_instr(v);
match index_field(s) {
Some((i, _)) => {
// Settings for the index that is there: a new field code, then rebuild it.
let para = s.doc.para_mut(StoryRef::Body, &Path::top(i))?;
for o in para.objects.iter_mut() {
if let InlineObject::Field { instr: old, .. } = o
&& FieldCode::parse(old).name == "INDEX"
{
*old = instr.clone();
}
}
para.touch();
}
None => super::citations::generated_list(s, &instr, "Index")?,
}
if let Some((i, _)) = index_field(s) {
rebuild(s, i, p::str(v, "tabLeader").map(leader))?;
}
s.clamp_selection();
sel_result(s)
}
/// One page reference of an entry.
#[derive(Clone, Debug, PartialEq)]
struct PageRef {
page: String,
bold: bool,
italic: bool,
}
#[derive(Default)]
struct Node {
/// The entry's levels as written (the key is their lower-case form).
levels: Vec<String>,
pages: Vec<PageRef>,
see: Vec<String>,
}
/// Every XE entry in the body with its page (or page range), grouped by entry.
fn collect(s: &mut Session) -> BTreeMap<Vec<String>, Node> {
let mut marks: Vec<(FieldCode, Pos)> = Vec::new();
for path in s.doc.para_paths(StoryRef::Body) {
let Some(p) = s.doc.para(StoryRef::Body, &path) else { continue };
for off in p.object_offsets() {
if let Some(InlineObject::Field { instr, .. }) = p.object_at(off) {
let f = FieldCode::parse(instr);
if f.name == "XE" {
marks.push((f, Pos { story: StoryRef::Body, path: path.clone(), off }));
}
}
}
}
let l = s.layout();
let page = |pos: &Pos| l.caret(pos).and_then(|c| l.pages.get(c.page)).map(|p| p.number).unwrap_or(1);
let bookmark_end = |name: &str| {
s.doc.para_paths(StoryRef::Body).into_iter().find_map(|path| {
let p = s.doc.para(StoryRef::Body, &path)?;
let off = p.object_offsets().into_iter().find(|o| matches!(p.object_at(*o), Some(InlineObject::BookmarkEnd { name: n }) if n == name))?;
Some(Pos { story: StoryRef::Body, path, off })
})
};
let starts = s.doc.bookmarks();
let mut tree: BTreeMap<Vec<String>, Node> = BTreeMap::new();
for (f, pos) in marks {
let text = f.args.first().cloned().unwrap_or_default();
let levels: Vec<String> = text.split(':').map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).take(9).collect();
if levels.is_empty() {
continue;
}
// Every level above gets a line of its own, even without pages.
for k in 1..levels.len() {
let head: Vec<String> = levels.iter().take(k).cloned().collect();
tree.entry(head.iter().map(|x| x.to_lowercase()).collect()).or_insert_with(|| Node { levels: head, ..Default::default() });
}
let node =
tree.entry(levels.iter().map(|x| x.to_lowercase()).collect()).or_insert_with(|| Node { levels: levels.clone(), ..Default::default() });
if let Some(see) = f.arg('t') {
if !node.see.iter().any(|x| x == see) {
node.see.push(see.to_string());
}
continue;
}
let range = f.arg('r').and_then(|bm| {
let a = starts.iter().find(|(n, _)| n == bm).map(|(_, p)| page(p))?;
let b = bookmark_end(bm).map(|p| page(&p)).unwrap_or(a);
Some(if b > a { format!("{a}\u{2013}{b}") } else { a.to_string() })
});
let r = PageRef { page: range.unwrap_or_else(|| page(&pos).to_string()), bold: f.has('b'), italic: f.has('i') };
match node.pages.iter_mut().find(|x| x.page == r.page) {
Some(x) => {
x.bold |= r.bold;
x.italic |= r.italic;
}
None => node.pages.push(r),
}
}
tree
}
/// Text runs of one index line.
type Line = Vec<(String, CharProps)>;
fn refs(line: &mut Line, node: &Node, sep: &str) {
for (k, r) in node.pages.iter().enumerate() {
line.push((if k == 0 { sep.to_string() } else { ", ".into() }, CharProps::default()));
line.push((r.page.clone(), CharProps { bold: r.bold.then_some(true), italic: r.italic.then_some(true), ..Default::default() }));
}
for (k, see) in node.see.iter().enumerate() {
let lead = if k > 0 || !node.pages.is_empty() { "; " } else { ". " };
line.push((lead.into(), CharProps::default()));
line.push((see.clone(), CharProps { italic: Some(true), ..Default::default() }));
}
}
/// Rebuild the index whose field is in body block `i`: entries sorted and grouped, indented or
/// run-in (`\r`), letter headings (`\h`), the page separator (`\e`, a tab right-aligns the page
/// numbers with `leader` or the leader the entries had), and `\c` columns as a continuous
/// section of their own.
pub fn rebuild(s: &mut Session, i: usize, leader: Option<TabLeader>) -> Result<(), CmdError> {
let Some(instr) = s.doc.body.get(i).and_then(|b| b.as_para()).and_then(|p| {
p.objects.iter().find_map(|o| match o {
InlineObject::Field { instr, .. } if FieldCode::parse(instr).name == "INDEX" => Some(instr.clone()),
_ => None,
})
}) else {
return Ok(());
};
let f = FieldCode::parse(&instr);
// Old entries go; keep their leader and the section their columns had.
let mut old_leader = None;
let mut old_sect = None;
while let Some(p) = s.doc.body.get(i + 1).and_then(|b| b.as_para()).filter(|p| p.props.style.as_deref() == Some(ENTRY_STYLE)) {
old_leader = old_leader.or_else(|| p.props.tabs.as_ref().and_then(|t| t.last()).map(|t| t.leader));
if p.section.is_some() {
old_sect = p.section.clone();
}
s.doc.remove_block(StoryRef::Body, &Path::top(i + 1))?;
}
let leader = leader.or(old_leader).unwrap_or(TabLeader::Dot);
let cols = f.arg('c').and_then(|c| c.trim().parse::<u32>().ok()).unwrap_or(1).clamp(1, 4);
let sep = f.arg('e').map(str::to_string).unwrap_or_else(|| ", ".into());
let run_in = f.has('r');
let headings = f.switches.iter().find(|(c, _)| *c == 'h').map(|(_, a)| a.clone().unwrap_or_default());
let tree = collect(s);
let mut lines: Vec<(usize, Line)> = Vec::new();
let mut letter: Option<String> = None;
let mut keys = tree.keys().peekable();
while let Some(key) = keys.next() {
let Some(node) = tree.get(key) else { continue };
let Some(main) = node.levels.first() else { continue };
if let Some(h) = &headings {
let first = main.chars().next().map(|c| c.to_uppercase().to_string());
if first != letter {
if letter.is_some() || !h.is_empty() {
lines.push((0, vec![(if h.is_empty() { String::new() } else { first.clone().unwrap_or_default() }, CharProps::default())]));
}
letter = first;
}
}
if run_in {
// Subentries follow their main entry on its line: `Main, 3: sub, 4; other, 5`.
if node.levels.len() > 1 {
continue;
}
let mut line: Line = vec![(main.clone(), CharProps::default())];
refs(&mut line, node, &sep);
let mut first = true;
while let Some(sub) = keys.peek().filter(|k| k.len() > 1 && k.first() == key.first()).and_then(|k| tree.get(*k)) {
keys.next();
line.push((if first { ": " } else { "; " }.into(), CharProps::default()));
first = false;
line.push((sub.levels.iter().skip(1).cloned().collect::<Vec<_>>().join(", "), CharProps::default()));
refs(&mut line, sub, ", ");
}
lines.push((0, line));
} else {
let mut line: Line = vec![(node.levels.last().cloned().unwrap_or_default(), CharProps::default())];
refs(&mut line, node, &sep);
lines.push((node.levels.len().saturating_sub(1), line));
}
}
// Entries are laid out in the index's columns.
let sects = s.doc.sections();
let sect = sects.get(s.doc.section_index_of(i + 1)).map(|(_, x)| (*x).clone()).unwrap_or_default();
let space = old_sect.as_ref().map_or(sect.columns.space, |x| x.columns.space);
let col_w = (sect.text_width() - space * (cols as f32 - 1.0)) / cols as f32;
let n = lines.len();
for (k, (level, runs)) in lines.into_iter().enumerate() {
let mut para = Paragraph::new().styled(ENTRY_STYLE);
for (t, props) in runs {
let at = para.len();
para.insert_text(at, &t, &props)?;
}
para.props.indent_left = Some(18.0 * (level as f32 + 1.0));
para.props.indent_first = Some(-18.0);
if para.text.contains('\t') {
para.props.tabs = Some(vec![TabStop { pos: (col_w - 0.5).max(18.0), align: TabAlign::Right, leader }]);
}
s.doc.insert_block(StoryRef::Body, &Path::top(i + 1 + k), Block::Para(para))?;
}
columns(s, i, n, cols, old_sect.map(|b| *b))?;
s.touch();
Ok(())
}
/// Put the `n` entries after the index field's paragraph `i` in a continuous section with `cols`
/// columns (the field's paragraph ends the section before), or merge that section back when the
/// index has one column again.
fn columns(s: &mut Session, i: usize, n: usize, cols: u32, old: Option<wordcraft_doc::section::SectionProps>) -> Result<(), CmdError> {
let last = i + n;
if cols > 1 && n > 0 {
// A paragraph after the index keeps the following section from being empty.
if last + 1 >= s.doc.body.len() {
s.doc.insert_block(StoryRef::Body, &Path::top(last + 1), Block::Para(Paragraph::new()))?;
}
let after = s.doc.sections().get(s.doc.section_index_of(last + 1)).map(|(_, x)| (*x).clone()).unwrap_or_default();
let head = s.doc.para_mut(StoryRef::Body, &Path::top(i))?;
let new = head.section.is_none();
if new {
head.section = Some(Box::new(after.clone()));
}
let mut sect = old.unwrap_or_else(|| after.clone());
sect.columns.count = cols;
sect.columns.widths.clear();
sect.start = SectionStart::Continuous;
sect.page_num_start = None;
let end = s.doc.para_mut(StoryRef::Body, &Path::top(last))?;
end.section = Some(Box::new(sect));
if new {
let rest = s.doc.section_mut(last + 1);
rest.start = SectionStart::Continuous;
rest.page_num_start = None;
}
} else if old.is_some() {
// Back to one column: the sections around the index join again, with the first one's start.
let head = s.doc.para_mut(StoryRef::Body, &Path::top(i))?;
if let Some(before) = head.section.take() {
head.touch();
let rest = s.doc.section_mut(i);
rest.start = before.start;
rest.page_num_start = before.page_num_start;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn index_lines(s: &Session) -> Vec<String> {
s.doc.body.iter().filter_map(|b| b.as_para()).filter(|p| p.props.style.as_deref() == Some(ENTRY_STYLE)).map(|p| p.plain_text()).collect()
}
/// #402: Mark Entry writes XE switches (subentry, bold page, cross-reference, Mark All) and
/// the INDEX field's switches change the index: run-in subentries, letter headings, a tab
/// before right-aligned page numbers, and two columns in a continuous section.
#[test]
fn index_switches_shape_the_index() {
let mut s = Session::new(wordcraft_doc::Document::from_text("Kilns are hot.\nGlaze the kilns.\nKilns again.\nPresses."));
s.run("select.text", &json!({"text": "Kilns"})).unwrap();
s.run("references.markEntry", &json!({"subentry": "firing", "bold": true})).unwrap();
s.run("select.text", &json!({"text": "Presses"})).unwrap();
s.run("references.markEntry", &json!({"crossReference": "See Kilns"})).unwrap();
let all = s.run("references.markEntry", &json!({"entry": "Kilns", "text": "Kilns", "all": true})).unwrap();
assert_eq!(all["marked"], 2, "the paragraphs with a whole-word, same-case match");
let instrs: Vec<String> = s
.doc
.body
.iter()
.filter_map(|b| b.as_para())
.flat_map(|p| p.objects.iter())
.filter_map(|o| match o {
InlineObject::Field { instr, .. } => Some(instr.clone()),
_ => None,
})
.collect();
assert!(
instrs.contains(&"XE \"Kilns:firing\" \\b".to_string()) && instrs.contains(&"XE \"Presses\" \\t \"See Kilns\"".to_string()),
"{instrs:?}"
);
s.run("caret.docEnd", &json!({})).unwrap();
s.run("text.newParagraph", &json!({})).unwrap();
s.run("references.index", &json!({"type": "runIn", "rightAlign": true, "columns": 2, "tabLeader": "hyphen"})).unwrap();
let (i, instr) = index_field(&s).unwrap();
assert_eq!(instr, "INDEX \\h \"A\" \\c \"2\" \\e \"\t\" \\r");
assert_eq!(index_lines(&s), vec!["K", "Kilns\t1: firing, 1", "P", "Presses. See Kilns"]);
let entry = s.doc.body[i + 2].as_para().unwrap();
assert_eq!(entry.props.tabs.as_ref().unwrap()[0].leader, TabLeader::Hyphen);
assert!(entry.runs.iter().any(|r| r.props.bold == Some(true)), "the \\b page number is bold");
// The entries sit in a two-column continuous section.
let last = s.doc.body[i + 4].as_para().unwrap();
let sect = last.section.as_ref().expect("index section");
assert_eq!((sect.columns.count, sect.start), (2, SectionStart::Continuous));
// Indented again: subentries get lines of their own; the leader stays on update.
s.run("references.index", &json!({"columns": 1, "rightAlign": true})).unwrap();
assert_eq!(index_lines(&s), vec!["K", "Kilns\t1", "firing\t1", "P", "Presses. See Kilns"]);
assert!(s.doc.body.iter().filter_map(|b| b.as_para()).all(|p| p.section.is_none()), "one section again");
s.run("references.updateIndex", &json!({})).unwrap();
let (i, _) = index_field(&s).unwrap();
assert_eq!(s.doc.body[i + 2].as_para().unwrap().props.tabs.as_ref().unwrap()[0].leader, TabLeader::Hyphen);
}
}

View File

@ -12,6 +12,7 @@ pub mod fields;
pub mod file;
pub mod fmt_revisions;
pub mod format;
pub mod index;
pub mod ink_shape;
pub mod insert;
pub mod inspector;
@ -66,6 +67,7 @@ pub fn registry() -> Registry {
v.extend(fields::specs());
v.extend(mailings::specs());
v.extend(citations::specs());
v.extend(index::specs());
v.extend(objects::specs());
v.extend(draw::specs());
v.extend(canvas::specs());

View File

@ -10,8 +10,9 @@ use crate::{CmdError, CmdResult, CommandSpec, Selection, Session, p};
pub fn specs() -> Vec<CommandSpec> {
vec![
CommandSpec::new("references.toc", "Table of Contents", "References › Table of Contents", insert_toc)
.params(r#"{"levels"?: 1-9, "title"?: string}"#),
CommandSpec::new("references.toc", "Table of Contents", "References › Table of Contents", insert_toc).params(
r#"{"levels"?: 1-9, "title"?: string, "pageNumbers"?: bool, "rightAlign"?: bool, "tabLeader"?: "dot|hyphen|underscore|none", "hyperlinks"?: bool, "headingStyles"?: bool, "outlineLevels"?: bool, "tcFields"?: bool, "styleLevels"?: {style name: 1-9}} (replaces an existing table; {} updates it)"#,
),
CommandSpec::new("references.updateToc", "Update Table", "References › Table of Contents", |s, _| {
update_toc(s)?;
sel_result(s)
@ -67,7 +68,7 @@ pub fn specs() -> Vec<CommandSpec> {
})
.pure(),
CommandSpec::new("references.caption", "Insert Caption", "References › Captions", caption)
.params(r#"{"label"?: "Figure|Table|Equation", "text"?: string}"#),
.params(r#"{"label"?: "Figure|Table|Equation|any label", "text"?: string, "position"?: "below|above", "excludeLabel"?: bool, "format"?: "ARABIC|alphabetic|ALPHABETIC|roman|ROMAN", "chapter"?: heading level 1-9, "separator"?: "-|.|:|—|–"}"#),
// F9: update fields; the selected ones that showed their code show their result again.
CommandSpec::new("references.updateFields", "Update Field", "References", |s, _| {
update_fields(s)?;
@ -78,8 +79,170 @@ pub fn specs() -> Vec<CommandSpec> {
]
}
/// Headings (outline level < levels) in the body: (path, level, text).
fn headings(s: &Session, levels: u8) -> Vec<(Path, u8, String)> {
/// A field instruction split into its name, positional arguments and switches (ECMA-376
/// §17.16.1): `TOC \o "1-3" \h` → name `TOC`, switches `o` = `1-3` and `h`. Quoted text stays
/// whole (without the quotes; `\"` and `\\` inside it are a quote and a backslash). A switch
/// takes the word after it as its argument unless that word is another switch.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct FieldCode {
pub name: String,
pub args: Vec<String>,
/// Switch letter (lower-cased; `*`, `@`, `#` for the general switches) and argument.
pub switches: Vec<(char, Option<String>)>,
}
impl FieldCode {
pub fn parse(instr: &str) -> FieldCode {
let mut words: Vec<(String, bool)> = Vec::new();
let mut chars = instr.chars().peekable();
while let Some(&c) = chars.peek() {
if c.is_whitespace() {
chars.next();
} else if c == '"' {
chars.next();
let mut w = String::new();
while let Some(c) = chars.next() {
match c {
'"' => break,
'\\' if matches!(chars.peek(), Some('"' | '\\')) => w.extend(chars.next()),
c => w.push(c),
}
}
words.push((w, true));
} else {
let mut w = String::new();
while let Some(&c) = chars.peek() {
if c.is_whitespace() || (c == '"' && !w.is_empty()) {
break;
}
w.push(c);
chars.next();
}
words.push((w, false));
}
}
let mut words = words.into_iter().peekable();
let mut f =
FieldCode { name: words.next().map(|(w, _)| w.trim_start_matches('=').to_ascii_uppercase()).unwrap_or_default(), ..Default::default() };
while let Some((w, quoted)) = words.next() {
let sw = if quoted { None } else { w.strip_prefix('\\').and_then(|r| r.chars().next()) };
match sw {
Some(c) => {
let arg = match words.peek() {
Some((n, q)) if *q || !n.starts_with('\\') => words.next().map(|(n, _)| n),
_ => None,
};
f.switches.push((c.to_ascii_lowercase(), arg));
}
None => f.args.push(w),
}
}
f
}
pub fn has(&self, c: char) -> bool {
self.switches.iter().any(|(k, _)| *k == c)
}
/// The argument of switch `c`, if the switch is there with one.
pub fn arg(&self, c: char) -> Option<&str> {
self.switches.iter().find(|(k, _)| *k == c).and_then(|(_, a)| a.as_deref())
}
}
/// A level range such as `1-3` (or one level, `2`), clamped to 1–9.
pub fn level_range(s: &str) -> Option<(u8, u8)> {
let mut it = s.split('-').map(|x| x.trim().parse::<u8>().ok());
let a = it.next().flatten()?.clamp(1, 9);
let b = match it.next() {
Some(b) => b?.clamp(1, 9),
None => a,
};
Some((a.min(b), a.max(b)))
}
/// The tab leader named in a command parameter.
pub fn leader(name: &str) -> TabLeader {
match name {
"none" => TabLeader::None,
"hyphen" => TabLeader::Hyphen,
"underscore" => TabLeader::Underscore,
"middleDot" => TabLeader::MiddleDot,
_ => TabLeader::Dot,
}
}
/// The TOC field instruction for `references.toc`'s parameters (ECMA-376 §17.16.5.68): `\o`
/// heading styles, `\h` hyperlinks, `\z` hide tab and page numbers in Web view, `\u` outline
/// levels, `\f`/`\l` TC fields, `\n` without page numbers, `\p` separator, `\t` styles with levels.
pub fn toc_instr(v: &Value) -> String {
let levels = p::u64(v, "levels").unwrap_or(3).clamp(1, 9);
let on = |k: &str, d: bool| p::bool(v, k).unwrap_or(d);
let mut s = String::from("TOC");
if on("headingStyles", true) {
s.push_str(&format!(" \\o \"1-{levels}\""));
}
if on("hyperlinks", true) {
s.push_str(" \\h");
}
s.push_str(" \\z");
if on("outlineLevels", true) {
s.push_str(" \\u");
}
if on("tcFields", false) {
s.push_str(&format!(" \\f \\l \"1-{levels}\""));
}
if !on("pageNumbers", true) {
s.push_str(&format!(" \\n \"1-{levels}\""));
} else if !on("rightAlign", true) {
s.push_str(" \\p \" \"");
}
let clean = |n: &str| n.chars().filter(|c| !matches!(c, '"' | ',' | '\\')).take(64).collect::<String>().trim().to_string();
let pairs: Vec<String> = match v.get("styleLevels") {
Some(Value::Object(m)) => m
.iter()
.take(64)
.filter_map(|(name, l)| Some((clean(name), l.as_u64()?.clamp(1, 9))))
.filter(|(n, _)| !n.is_empty())
.map(|(n, l)| format!("{n},{l}"))
.collect(),
_ => Vec::new(),
};
if !pairs.is_empty() {
s.push_str(&format!(" \\t \"{}\"", pairs.join(",")));
}
s
}
/// A TOC entry: the paragraph it points at, its level (0-based), its text, and the leading page
/// breaks of that paragraph (its page is where the text after them lands).
struct TocEntry {
path: Path,
level: u8,
text: String,
lead: usize,
}
/// The heading level (1–9) of a built-in heading style.
fn heading_style_level(p: &Paragraph) -> Option<u8> {
p.props.style.as_deref()?.strip_prefix("Heading")?.trim().parse::<u8>().ok().filter(|l| (1..=9).contains(l))
}
/// The entries a TOC field collects from the body, in document order: built-in heading styles
/// in the `\o` range, paragraphs with an outline level (`\u`), styles listed in `\t`, and TC
/// fields (`\f`, levels `\l`).
fn toc_entries(s: &Session, f: &FieldCode) -> Vec<TocEntry> {
let o = f.has('o').then(|| f.arg('o').and_then(level_range).unwrap_or((1, 9)));
let max = o.map(|r| r.1).unwrap_or(9);
let styles: Vec<(String, u8)> = f
.arg('t')
.map(|t| {
let parts: Vec<&str> = t.split([',', ';']).map(str::trim).collect();
parts.chunks(2).filter_map(|c| Some((c.first()?.to_string(), c.get(1)?.parse::<u8>().ok()?.clamp(1, 9)))).collect()
})
.unwrap_or_default();
let tc = f.has('f').then(|| f.arg('f').unwrap_or("").to_ascii_uppercase());
let tc_levels = f.arg('l').and_then(level_range).unwrap_or((1, 9));
let mut v = Vec::new();
for path in s.doc.para_paths(StoryRef::Body) {
if path.depth() > 0 {
@ -90,26 +253,51 @@ fn headings(s: &Session, levels: u8) -> Vec<(Path, u8, String)> {
if style.starts_with("TOC") {
continue;
}
let rp = s.doc.styles.resolve_para(&p.props);
if let Some(l) = rp.outline_level
&& l < levels
{
let lead = p.text.bytes().take_while(|b| *b == 0x0C).count();
let by_style =
styles.iter().find(|(n, _)| s.doc.styles.get(style).is_some_and(|st| st.name.eq_ignore_ascii_case(n)) || n == style).map(|(_, l)| *l);
// `Some(9)` direct outline level is body text: "Do Not Show in Table of Contents".
let by_outline = f.has('u').then_some(p.props.outline_level).flatten().filter(|l| *l < 9).map(|l| l + 1).filter(|l| *l <= max);
let hidden = f.has('u') && p.props.outline_level == Some(9);
let by_heading = o.and_then(|(a, b)| {
let l = heading_style_level(p).or_else(|| {
s.doc
.styles
.resolve_para(&wordcraft_doc::ParaProps { style: p.props.style.clone(), ..Default::default() })
.outline_level
.map(|l| l + 1)
})?;
(a..=b).contains(&l).then_some(l)
});
if let Some(l) = by_style.or(by_outline).or(if hidden { None } else { by_heading }) {
let t = p.plain_text().replace(['\u{000C}', '\n', '\t'], " ").trim().to_string();
if !t.is_empty() {
v.push((path, l, t));
v.push(TocEntry { path: path.clone(), level: l - 1, text: t, lead });
}
}
if let Some(id) = &tc {
for off in p.object_offsets() {
let Some(InlineObject::Field { instr, .. }) = p.object_at(off) else { continue };
let c = FieldCode::parse(instr);
if c.name != "TC" {
continue;
}
let kind = c.arg('f').unwrap_or("C").to_ascii_uppercase();
let l = c.arg('l').and_then(|l| l.trim().parse::<u8>().ok()).unwrap_or(1).clamp(1, 9);
let text = c.args.first().map(|t| t.trim().to_string()).unwrap_or_default();
if (id.is_empty() || *id == kind) && (tc_levels.0..=tc_levels.1).contains(&l) && !text.is_empty() {
v.push(TocEntry { path: path.clone(), level: l - 1, text, lead: off });
}
}
}
}
v
}
/// The TOC block range in the body: the paragraph holding the TOC field and the TOC entries after it.
/// The TOC block range in the body: the paragraph holding the TOC field and the TOC entries after
/// it. A TOC with `\c` is a table of figures, kept by the generated lists.
fn toc_range(s: &Session) -> Option<(usize, usize)> {
let start = s.doc.body.iter().position(|b| {
b.as_para().is_some_and(|p| {
p.objects.iter().any(|o| matches!(o, InlineObject::Field { instr, .. } if instr.trim_start().to_ascii_uppercase().starts_with("TOC")))
})
})?;
let start = s.doc.body.iter().position(|b| b.as_para().is_some_and(|p| toc_field(p).is_some()))?;
let mut end = start;
while let Some(Block::Para(p)) = s.doc.body.get(end + 1).map(|b| &**b) {
if p.props.style.as_deref().is_some_and(|st| st.starts_with("TOC") && st != "TOCHeading") {
@ -121,33 +309,39 @@ fn toc_range(s: &Session) -> Option<(usize, usize)> {
Some((start, end))
}
fn toc_levels(s: &Session, start: usize) -> u8 {
s.doc
.body
.get(start)
.and_then(|b| b.as_para())
.and_then(|p| {
p.objects.iter().find_map(|o| match o {
InlineObject::Field { instr, .. } => instr
.split("\\o")
.nth(1)
.and_then(|r| r.trim().trim_matches('"').split('-').nth(1))
.and_then(|n| n.trim_matches('"').parse::<u8>().ok()),
_ => None,
})
})
.unwrap_or(3)
.clamp(1, 9)
/// The TOC field instruction in `p` (not a table of figures).
fn toc_field(p: &Paragraph) -> Option<String> {
p.objects.iter().find_map(|o| match o {
InlineObject::Field { instr, .. } => {
let f = FieldCode::parse(instr);
(f.name == "TOC" && !f.has('c')).then(|| instr.clone())
}
_ => None,
})
}
fn insert_toc(s: &mut Session, v: &Value) -> CmdResult {
if toc_range(s).is_some() {
update_toc(s)?;
return sel_result(s);
}
let levels = p::u64(v, "levels").unwrap_or(3).clamp(1, 9) as u8;
let replace = v.as_object().is_some_and(|m| !m.is_empty());
// Replacing a table puts the new one where the old one was, in place of an empty paragraph.
let mut placeholder = false;
let at = match toc_range(s) {
// `{}` updates the table there; settings replace it where it is.
Some(_) if !replace => {
update_toc(s)?;
return sel_result(s);
}
Some((start, end)) => {
for i in (start..=end).rev() {
s.doc.remove_block(StoryRef::Body, &Path::top(i))?;
}
s.doc.insert_block(StoryRef::Body, &Path::top(start), Block::Para(Paragraph::new()))?;
placeholder = true;
s.sel = Selection::caret(Pos { story: StoryRef::Body, path: Path::top(start), off: 0 });
s.sel.focus.clone()
}
None => delete_selection(s)?,
};
let title = p::str(v, "title").unwrap_or("Contents").to_string();
let at = delete_selection(s)?;
if at.story != StoryRef::Body || at.path.depth() > 0 {
return Err(CmdError::Failed("a table of contents goes in the main text".into()));
}
@ -155,28 +349,95 @@ fn insert_toc(s: &mut Session, v: &Value) -> CmdResult {
let mut head = Paragraph::with_text(&title, CharProps::default()).styled("TOCHeading");
head.insert_object(
head.len(),
InlineObject::Field { instr: format!("TOC \\o \"1-{levels}\" \\h \\z \\u"), result: String::new(), locked: false, code: false },
InlineObject::Field { instr: toc_instr(v), result: String::new(), locked: false, code: false },
&CharProps::default(),
)?;
let i = if s.doc.para_at(&at).is_some_and(|p| p.is_empty()) { at.path.last() } else { s.doc.split_paragraph(&at)?.path.last() };
let i = if s.doc.para_at(&at).is_some_and(|p| p.is_empty()) {
if placeholder {
s.doc.remove_block(StoryRef::Body, &at.path)?;
}
at.path.last()
} else {
s.doc.split_paragraph(&at)?.path.last()
};
s.doc.insert_block(StoryRef::Body, &Path::top(i), Block::Para(head))?;
update_toc(s)?;
let leader = leader(p::str(v, "tabLeader").unwrap_or("dot"));
regenerate_toc(s, Some(leader))?;
s.clamp_selection();
sel_result(s)
}
/// A hidden `_Toc` bookmark around the heading at body block `i` (after its leading page breaks),
/// reusing one already there; its name.
fn toc_bookmark(s: &mut Session, i: usize, lead: usize) -> Result<String, CmdError> {
const MARK: usize = wordcraft_doc::para::OBJ.len_utf8();
let para = s.doc.para(StoryRef::Body, &Path::top(i)).ok_or_else(|| CmdError::Failed("heading is gone".into()))?;
if let Some(InlineObject::BookmarkStart { name }) = para.object_at(lead)
&& name.starts_with("_Toc")
{
return Ok(name.clone());
}
let next =
s.doc.bookmarks().iter().filter_map(|(n, _)| n.strip_prefix("_Toc").and_then(|d| d.parse::<u64>().ok())).max().unwrap_or(0).saturating_add(1);
let name = format!("_Toc{next:09}");
let end = para.len();
let props = CharProps::default();
let pos = |off| Pos { story: StoryRef::Body, path: Path::top(i), off };
s.doc.insert_object(&pos(end), InlineObject::BookmarkEnd { name: name.clone() }, &props)?;
s.doc.insert_object(&pos(lead), InlineObject::BookmarkStart { name: name.clone() }, &props)?;
// The caret in that paragraph stays on the same text.
let shift = |p: &mut Pos| {
if p.story == StoryRef::Body && p.path == Path::top(i) {
if p.off > end {
p.off = p.off.saturating_add(2 * MARK);
} else if p.off >= lead && p.off > 0 {
p.off = p.off.saturating_add(MARK);
}
}
};
shift(&mut s.sel.anchor);
shift(&mut s.sel.focus);
Ok(name)
}
/// Regenerate the TOC entries with page numbers (two layout passes: entries change pagination).
pub fn update_toc(s: &mut Session) -> Result<(), CmdError> {
regenerate_toc(s, None)
}
/// Rebuild the entries the TOC field's switches ask for. The tab leader is `leader`, or the one
/// the entries had (Word keeps it in the entries' tab stops, not in the field).
fn regenerate_toc(s: &mut Session, leader: Option<TabLeader>) -> Result<(), CmdError> {
let Some((start, end)) = toc_range(s) else { return Ok(()) };
let levels = toc_levels(s, start);
let Some(instr) = s.doc.body.get(start).and_then(|b| b.as_para()).and_then(toc_field) else { return Ok(()) };
let f = FieldCode::parse(&instr);
let leader = leader.unwrap_or_else(|| {
s.doc
.body
.get(start + 1)
.filter(|_| end > start)
.and_then(|b| b.as_para())
.and_then(|p| p.props.tabs.as_ref()?.last().map(|t| t.leader))
.unwrap_or(TabLeader::Dot)
});
for i in (start + 1..=end).rev() {
s.doc.remove_block(StoryRef::Body, &Path::top(i))?;
}
let width = super::page::sect(s).text_width();
let entries = headings(s, levels);
// A heading typed after a manual page break starts with that break; its page is where the
// text after the break lands, so look the page up past any leading breaks.
let leads: Vec<usize> =
entries.iter().map(|(path, ..)| s.doc.para(StoryRef::Body, path).map_or(0, |p| p.text.bytes().take_while(|b| *b == 0x0C).count())).collect();
let entries = toc_entries(s, &f);
let links: Vec<Option<String>> = if f.has('h') {
let mut v = Vec::new();
for e in &entries {
// TC fields link to their paragraph's start.
let lead = s.doc.para(StoryRef::Body, &e.path).map_or(0, |p| p.text.bytes().take_while(|b| *b == 0x0C).count());
v.push(Some(format!("#{}", toc_bookmark(s, e.path.last(), lead)?)));
}
v
} else {
vec![None; entries.len()]
};
let no_pages = f.has('n').then(|| f.arg('n').and_then(level_range).unwrap_or((1, 9)));
let sep = f.arg('p').map(str::to_string).unwrap_or_else(|| "\t".into());
for pass in 0..2 {
// Pass 1 lays out with the entries in place, so headings below the TOC sit `entries.len()`
// blocks further down and their pages account for the TOC's own length.
@ -189,12 +450,18 @@ pub fn update_toc(s: &mut Session) -> Result<(), CmdError> {
}
}
let shift = if pass == 0 { 0 } else { entries.len() };
for (k, (path, level, text)) in entries.iter().enumerate() {
let target = Path::top(path.last() + if path.last() > start { shift } else { 0 });
let off = leads.get(k).copied().unwrap_or(0);
let page = l.caret(&Pos { story: StoryRef::Body, path: target, off }).and_then(|c| l.pages.get(c.page)).map(|pg| pg.number).unwrap_or(1);
let mut para = Paragraph::with_text(&format!("{text}\t{page}"), CharProps::default()).styled(&format!("TOC{}", level + 1));
para.props.tabs = Some(vec![TabStop { pos: width - 0.5, align: TabAlign::Right, leader: TabLeader::Dot }]);
for (k, e) in entries.iter().enumerate() {
let target = Path::top(e.path.last() + if e.path.last() > start { shift } else { 0 });
let page =
l.caret(&Pos { story: StoryRef::Body, path: target, off: e.lead }).and_then(|c| l.pages.get(c.page)).map(|pg| pg.number).unwrap_or(1);
let show_page = no_pages.is_none_or(|(a, b)| !(a..=b).contains(&(e.level + 1)));
let text = if show_page { format!("{}{sep}{page}", e.text) } else { e.text.clone() };
let props = CharProps { link: links.get(k).cloned().flatten(), ..Default::default() };
let mut para = Paragraph::with_text(&text, props).styled(&format!("TOC{}", e.level + 1));
para.mark = CharProps::default();
if show_page && sep.contains('\t') {
para.props.tabs = Some(vec![TabStop { pos: width - 0.5, align: TabAlign::Right, leader }]);
}
s.doc.insert_block(StoryRef::Body, &Path::top(start + 1 + k), Block::Para(para))?;
}
}
@ -337,45 +604,152 @@ fn note_refs(s: &Session) -> Vec<Pos> {
v
}
fn caption(s: &mut Session, v: &Value) -> CmdResult {
let label = p::str(v, "label").unwrap_or("Figure").to_string();
let text = p::str(v, "text").unwrap_or("").to_string();
// Number = existing captions with this label before the caret + 1.
let caret = s.sel.focus.clone();
let n = s
.doc
.para_paths(StoryRef::Body)
.into_iter()
.filter(|q| *q < caret.path)
.filter(|q| s.doc.para(StoryRef::Body, q).is_some_and(|p| p.props.style.as_deref() == Some("Caption") && p.text.starts_with(&label)))
.count()
+ 1;
let f = s.sel.focus.clone();
let len = s.doc.para_at(&f).map(|p| p.len()).unwrap_or(0);
let at = Pos { off: len, ..f };
let new = s.doc.split_paragraph(&at)?;
let para = s.doc.para_mut(new.story, &new.path)?;
para.props = wordcraft_doc::ParaProps { style: Some("Caption".into()), ..Default::default() };
para.mark = CharProps::default();
let base = format!("{label} ");
para.insert_text(0, &base, &CharProps::default())?;
para.insert_object(
base.len(),
InlineObject::Field { instr: format!("SEQ {label} \\* ARABIC"), result: n.to_string(), locked: false, code: false },
&CharProps::default(),
)?;
let end = para.len();
if !text.is_empty() {
para.insert_text(end, &format!(": {text}"), &CharProps::default())?;
/// The `\*` format switch value for a caption's number format (OOXML names work too).
pub fn seq_format(name: &str) -> &'static str {
match name {
"alphabetic" | "lowerLetter" => "alphabetic",
"ALPHABETIC" | "upperLetter" => "ALPHABETIC",
"roman" | "lowerRoman" => "roman",
"ROMAN" | "upperRoman" => "ROMAN",
_ => "ARABIC",
}
let off = para.len();
s.sel = Selection::caret(Pos { off, ..new });
}
/// Separators Word offers between a chapter number and a caption number.
pub const CHAPTER_SEPARATORS: [&str; 5] = ["-", ".", ":", "\u{2014}", "\u{2013}"];
/// Insert a caption paragraph below (or above) the caret's paragraph, table or object: the label,
/// a chapter number (`STYLEREF n \s` and a separator) and a `SEQ label \* format \s n` number
/// (ECMA-376 §17.16.5.56, §17.16.5.59).
fn caption(s: &mut Session, v: &Value) -> CmdResult {
let label: String = p::str(v, "label").unwrap_or("Figure").chars().filter(|c| !matches!(c, '"' | '\\') && !c.is_control()).take(40).collect();
let label = if label.trim().is_empty() { "Figure".to_string() } else { label.trim().to_string() };
// SEQ identifiers are one word: Word joins a multi-word label with underscores.
let ident = label.split_whitespace().collect::<Vec<_>>().join("_");
let text = p::str(v, "text").unwrap_or("").to_string();
let above = p::str(v, "position") == Some("above");
let format = seq_format(p::str(v, "format").unwrap_or("ARABIC"));
let chapter = p::u64(v, "chapter").filter(|l| (1..=9).contains(l));
let sep = p::str(v, "separator").and_then(|x| CHAPTER_SEPARATORS.iter().find(|s| **s == x)).copied().unwrap_or("-");
let mut para = Paragraph::new().styled("Caption");
let field = |instr: String| InlineObject::Field { instr, result: String::new(), locked: false, code: false };
if !p::bool(v, "excludeLabel").unwrap_or(false) {
para.insert_text(0, &format!("{label} "), &CharProps::default())?;
}
let mut seq = format!("SEQ {ident} \\* {format}");
if let Some(l) = chapter {
para.insert_object(para.len(), field(format!("STYLEREF {l} \\s")), &CharProps::default())?;
para.insert_text(para.len(), sep, &CharProps::default())?;
seq.push_str(&format!(" \\s {l}"));
}
para.insert_object(para.len(), field(seq), &CharProps::default())?;
if !text.is_empty() {
para.insert_text(para.len(), &format!(": {text}"), &CharProps::default())?;
}
// Next to the caret's top-level block: a paragraph, or the whole table it is in.
let f = s.sel.focus.clone();
let top = f.path.0.first().copied().unwrap_or(0) as usize;
let i = if above { top } else { top.saturating_add(1) };
s.doc.insert_block(f.story, &Path::top(i), Block::Para(para))?;
update_seq(s)?;
let off = s.doc.para(f.story, &Path::top(i)).map_or(0, |p| p.len());
s.sel = Selection::caret(Pos { story: f.story, path: Path::top(i), off });
sel_result(s)
}
/// The number part of a heading's list label (`Chapter 2.` → `2`).
fn chapter_number(label: &str) -> String {
let word = label.split_whitespace().last().unwrap_or("");
word.trim_matches(|c: char| !c.is_alphanumeric()).to_string()
}
/// Renumber the body's SEQ fields in document order, honouring `\* format`, `\s level` (restart
/// after each heading of that level), `\r n` (reset to n), `\c` (repeat) and `\h` (hide), and
/// fill chapter-number `STYLEREF n \s` fields with the last level-n heading's number (its list
/// label, or its count when the heading isn't numbered).
pub fn update_seq(s: &mut Session) -> Result<(), CmdError> {
let mut counters = wordcraft_doc::numbering::Counters::default();
let mut headings = [0u32; 9];
let mut chapter: [String; 9] = Default::default();
let mut heading_text: [String; 9] = Default::default();
let mut seq: std::collections::HashMap<String, (u32, u32)> = Default::default();
for path in s.doc.para_paths(StoryRef::Body) {
let Some(p) = s.doc.para(StoryRef::Body, &path) else { continue };
let label = p
.props
.numbering
.or_else(|| s.doc.styles.resolve_para(&p.props).numbering)
.filter(|n| n.num != 0)
.and_then(|n| counters.next_label(&s.doc.numbering, n.num, n.level))
.map(|(l, _)| l);
if let Some(l) = heading_style_level(p)
&& let Some(k) = (l as usize).checked_sub(1)
&& let Some(count) = headings.get_mut(k)
{
*count = count.saturating_add(1);
let n = label.map(|l| chapter_number(&l)).filter(|n| !n.is_empty()).unwrap_or_else(|| count.to_string());
if let Some(c) = chapter.get_mut(k) {
*c = n;
}
if let Some(t) = heading_text.get_mut(k) {
*t = p.plain_text().trim().to_string();
}
}
let mut updates: Vec<(usize, String)> = Vec::new();
for (k, o) in p.objects.iter().enumerate() {
let InlineObject::Field { instr, locked: false, .. } = o else { continue };
let f = FieldCode::parse(instr);
match f.name.as_str() {
"SEQ" => {
let Some(id) = f.args.first() else { continue };
let restart =
f.arg('s').and_then(|l| l.parse::<usize>().ok()).and_then(|l| l.checked_sub(1)).and_then(|l| headings.get(l)).copied();
let n = seq.entry(id.clone()).or_insert((0, 0));
if let Some(epoch) = restart
&& n.1 != epoch
{
*n = (0, epoch);
}
if let Some(r) = f.arg('r').and_then(|r| r.parse::<u32>().ok()) {
n.0 = r;
} else if !f.has('c') {
n.0 = n.0.saturating_add(1);
}
let fmt = wordcraft_layout::fields::format_switch(instr).unwrap_or_default();
updates.push((k, if f.has('h') { String::new() } else { fmt.format(n.0) }));
}
"STYLEREF" => {
let level = f
.args
.first()
.and_then(|a| a.strip_prefix("Heading").unwrap_or(a).trim().parse::<usize>().ok())
.and_then(|l| l.checked_sub(1));
let Some(level) = level else { continue };
let r = if f.has('s') || f.has('n') { chapter.get(level) } else { heading_text.get(level) };
if let Some(r) = r {
updates.push((k, r.clone()));
}
}
_ => {}
}
}
if updates.is_empty() {
continue;
}
let para = s.doc.para_mut(StoryRef::Body, &path)?;
for (k, r) in updates {
if let Some(InlineObject::Field { result, .. }) = para.objects.get_mut(k) {
*result = r;
}
}
para.touch();
}
Ok(())
}
/// Update fields in the body: dates, SEQ numbering, cross-references, TOC.
pub fn update_fields(s: &mut Session) -> Result<(), CmdError> {
let mut seq: std::collections::HashMap<String, u32> = Default::default();
update_seq(s)?;
let mut pages: std::collections::HashMap<String, u32> = Default::default();
for (name, pos) in s.doc.bookmarks() {
let l = s.layout();
@ -396,12 +770,6 @@ pub fn update_fields(s: &mut Session) -> Result<(), CmdError> {
let pic = date_picture(instr).unwrap_or_else(|| if name == "TIME" { "h:mm am/pm".into() } else { "M/d/yyyy".into() });
updates.push((k, super::insert::format_date(&pic)));
}
"SEQ" => {
let id = instr.split_whitespace().nth(1).unwrap_or("").to_string();
let n = seq.entry(id).or_insert(0);
*n += 1;
updates.push((k, n.to_string()));
}
"REF" | "PAGEREF" => {
let target = instr.split_whitespace().nth(1).unwrap_or("");
let r = if name == "REF" { bookmark_text(&s.doc, target) } else { pages.get(target).map(u32::to_string) };
@ -452,3 +820,83 @@ pub fn bookmark_text(doc: &wordcraft_doc::Document, name: &str) -> Option<String
.unwrap_or(para.len());
Some(doc.copy_range(&start, &Pos { off: end, ..start.clone() }).plain_text().trim().to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn fields(s: &Session) -> Vec<(String, String)> {
let mut out = Vec::new();
for path in s.doc.para_paths(StoryRef::Body) {
for o in &s.doc.para(StoryRef::Body, &path).unwrap().objects {
if let InlineObject::Field { instr, result, .. } = o {
out.push((instr.clone(), result.clone()));
}
}
}
out
}
/// #402: Custom Table of Contents settings become TOC switches, and updating honours them:
/// levels (`\o`), a style with a level (`\t`), no hyperlinks, page numbers after a space
/// (`\p`), or none at all (`\n`).
#[test]
fn toc_switches_shape_the_entries() {
let mut s = Session::new(wordcraft_doc::Document::from_text("Intro\nPart\nDeep\nNote"));
for (text, style) in [("Intro", "Heading 1"), ("Part", "Heading 2"), ("Deep", "Heading 3"), ("Note", "Quote")] {
s.run("select.text", &json!({"text": text})).unwrap();
s.run("styles.apply", &json!({"style": style})).unwrap();
}
s.run("caret.docStart", &json!({})).unwrap();
s.run("references.toc", &json!({"levels": 2, "hyperlinks": false, "rightAlign": false, "styleLevels": {"Quote": 2}})).unwrap();
assert_eq!(fields(&s)[0].0, "TOC \\o \"1-2\" \\z \\u \\p \" \" \\t \"Quote,2\"");
let entries: Vec<(String, String)> = s
.doc
.body
.iter()
.filter_map(|b| b.as_para())
.filter(|p| p.props.style.as_deref().is_some_and(|st| st.starts_with("TOC") && st != "TOCHeading"))
.map(|p| (p.props.style.clone().unwrap(), p.plain_text()))
.collect();
assert_eq!(entries, [("TOC1", "Intro 1"), ("TOC2", "Part 1"), ("TOC2", "Note 1")].map(|(a, b)| (a.to_string(), b.to_string())));
assert!(s.doc.bookmarks().is_empty(), "no hyperlinks, no _Toc bookmarks");
// Settings again replace the table in place; hyperlinks bookmark the headings.
s.run("references.toc", &json!({"levels": 3, "pageNumbers": false})).unwrap();
assert_eq!(s.doc.plain_text(StoryRef::Body).matches("Contents").count(), 1);
let toc: Vec<String> =
s.doc.body.iter().filter_map(|b| b.as_para()).filter(|p| p.props.style.as_deref() == Some("TOC3")).map(|p| p.plain_text()).collect();
assert_eq!(toc, ["Deep"]);
let first = s.doc.body.iter().filter_map(|b| b.as_para()).find(|p| p.props.style.as_deref() == Some("TOC1")).unwrap();
let link = first.runs[0].props.link.clone().unwrap();
assert!(s.doc.bookmarks().iter().any(|(n, _)| format!("#{n}") == link), "{link}");
}
/// #402: Caption numbering — a custom label above the paragraph, letters, and chapter
/// numbers that restart after each Heading 1 (`STYLEREF 1 \s`, `SEQ … \s 1`).
#[test]
fn caption_numbering_with_chapters() {
let mut s = Session::new(wordcraft_doc::Document::from_text("One\nPic\nPic two\nTwo\nPic three"));
for t in ["One", "Two"] {
s.run("select.text", &json!({"text": t})).unwrap();
s.run("styles.apply", &json!({"style": "Heading 1"})).unwrap();
}
let opts = json!({"label": "Photo Plate", "format": "upperLetter", "chapter": 1, "separator": "."});
for t in ["Pic three", "Pic two", "Pic"] {
s.run("select.text", &json!({"text": t})).unwrap();
let mut v = opts.clone();
v["position"] = json!("above");
v["text"] = json!(t);
s.run("references.caption", &v).unwrap();
}
let caps: Vec<String> =
s.doc.body.iter().filter_map(|b| b.as_para()).filter(|p| p.props.style.as_deref() == Some("Caption")).map(|p| p.plain_text()).collect();
assert_eq!(caps, ["Photo Plate 1.A: Pic", "Photo Plate 1.B: Pic two", "Photo Plate 2.A: Pic three"]);
assert!(fields(&s).contains(&("SEQ Photo_Plate \\* ALPHABETIC \\s 1".to_string(), "A".to_string())), "{:?}", fields(&s));
// The caption sits above its paragraph; excluding the label leaves the number.
let i = s.doc.body.iter().position(|b| b.as_para().is_some_and(|p| p.plain_text() == "Pic")).unwrap();
assert_eq!(s.doc.body[i - 1].as_para().unwrap().plain_text(), "Photo Plate 1.A: Pic");
s.run("caret.docEnd", &json!({})).unwrap();
s.run("references.caption", &json!({"label": "Table", "excludeLabel": true, "format": "ROMAN"})).unwrap();
assert_eq!(s.doc.body.last().unwrap().as_para().unwrap().plain_text(), "I");
}
}

View File

@ -55,7 +55,7 @@ pub fn field_name(instr: &str) -> String {
}
/// `\* FORMAT` switch value (Arabic, roman, ROMAN, alphabetic, ALPHABETIC, CardText, Ordinal…).
fn format_switch(instr: &str) -> Option<NumFormat> {
pub fn format_switch(instr: &str) -> Option<NumFormat> {
let mut it = instr.split_whitespace();
while let Some(w) = it.next() {
if w == "\\*" {

View File

@ -2638,6 +2638,6 @@ mod tests;
#[cfg(test)]
mod tests_bidi;
#[cfg(test)]
mod tests_textbox;
#[cfg(test)]
mod tests_grid;
#[cfg(test)]
mod tests_textbox;

View File

@ -134,6 +134,9 @@ pub enum Dialog {
ModifyStyle {
#[serde(skip)]
back: bool,
/// The dialog that comes back when this closes (Table of Contents › Modify…).
#[serde(skip)]
resume: Option<Box<Dialog>>,
id: String,
name: String,
font: String,
@ -170,6 +173,9 @@ pub enum Dialog {
},
/// Columns, Symbol and Field ([`crate::dialogs_insert`], #321).
Insert(Box<crate::dialogs_insert::InsertDialog>),
/// Caption, Index, Mark Index Entry, Table of Contents, Source Manager and Create / Edit
/// Source ([`crate::dialogs_refs`], #402).
Refs(Box<crate::dialogs_refs::RefsDialog>),
/// Insert › Icons: the icon library, searchable and filtered by category (empty for all).
Icons {
query: String,
@ -705,6 +711,7 @@ impl Dialog {
Dialog::TableStyle { .. } => "modifyTableStyle",
Dialog::Commands { .. } => "commands",
Dialog::Insert(d) => d.name(),
Dialog::Refs(d) => d.name(),
Dialog::Icons { .. } => "icons",
Dialog::PasteSpecial { .. } => "pasteSpecial",
Dialog::About { .. } => "about",
@ -751,6 +758,9 @@ impl Dialog {
if let Some(d) = crate::dialogs_insert::open(name, app) {
return Some(Dialog::Insert(Box::new(d)));
}
if let Some(d) = crate::dialogs_refs::open(name, app) {
return Some(Dialog::Refs(Box::new(d)));
}
let st = app.session.run("format.state", &json!({})).unwrap_or_default();
let s = |k: &str| st.get(k).and_then(Value::as_str).unwrap_or("").to_string();
let b = |k: &str| st.get(k).and_then(Value::as_bool).unwrap_or(false);
@ -967,6 +977,7 @@ impl Dialog {
let rp = app.session.doc.styles.resolve_para(&wordcraft_doc::ParaProps { style: Some(id.into()), ..Default::default() });
Some(Dialog::ModifyStyle {
back: false,
resume: None,
id: id.into(),
name: st.name.clone(),
font: rc.font,
@ -1080,6 +1091,7 @@ pub fn show(app: &mut WordApp, ctx: &egui::Context) {
Dialog::TableStyle { .. } => "Modify Table Style",
Dialog::Commands { .. } => "Search Commands",
Dialog::Insert(d) => d.title(),
Dialog::Refs(d) => d.title(),
Dialog::Icons { .. } => "Icons",
Dialog::PasteSpecial { .. } => "Paste Special",
Dialog::About { .. } => "About WordCraft",
@ -1687,7 +1699,7 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
}
ok || cancel
}
Dialog::ModifyStyle { back, id, name, font, size, bold, italic, color, before, after } => {
Dialog::ModifyStyle { back, resume, id, name, font, size, bold, italic, color, before, after } => {
egui::Grid::new("ms").num_columns(2).show(ui, |ui| {
ui.label(tl!("Name:"));
ui.text_edit_singleline(name);
@ -1724,6 +1736,11 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
if (ok || cancel) && *back {
app.dialog = Some(Dialog::ManageStyles { alphabetical: false, selected: id.clone() });
}
if (ok || cancel)
&& let Some(r) = resume.take()
{
app.dialog = Some(*r);
}
ok || cancel
}
Dialog::ManageStyles { alphabetical, selected } => manage_styles(app, ui, alphabetical, selected),
@ -1819,6 +1836,7 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
ok || cancel
}
Dialog::Insert(f) => crate::dialogs_insert::body(app, ui, f),
Dialog::Refs(f) => crate::dialogs_refs::body(app, ui, f),
Dialog::Commands { query } => {
let r = ui.add(egui::TextEdit::singleline(query).hint_text(tl!("Type a command, e.g. \"insert table\"")).desired_width(380.0));
r.request_focus();
@ -2461,7 +2479,7 @@ fn manage_styles(app: &mut WordApp, ui: &mut Ui, alphabetical: &mut bool, select
if let Some(Dialog::ModifyStyle { id, name, font, size, bold, italic, color, before, after, .. }) =
Dialog::modify_style(app, selected)
{
app.dialog = Some(Dialog::ModifyStyle { back: true, id, name, font, size, bold, italic, color, before, after });
app.dialog = Some(Dialog::ModifyStyle { back: true, resume: None, id, name, font, size, bold, italic, color, before, after });
}
}
}
@ -2498,7 +2516,7 @@ fn manage_styles(app: &mut WordApp, ui: &mut Ui, alphabetical: &mut bool, select
if ui.add_enabled(ty != "table", egui::Button::new(tl!("Modify…"))).clicked()
&& let Some(Dialog::ModifyStyle { id, name, font, size, bold, italic, color, before, after, .. }) = Dialog::modify_style(app, selected)
{
app.dialog = Some(Dialog::ModifyStyle { back: true, id, name, font, size, bold, italic, color, before, after });
app.dialog = Some(Dialog::ModifyStyle { back: true, resume: None, id, name, font, size, bold, italic, color, before, after });
}
if ui.button(tl!("New Style…")).clicked() {
app.dialog = Some(Dialog::NewStyle { name: "Style1".into(), based_on: str_of(&cur, "name"), back: true });

File diff suppressed because it is too large Load Diff

View File

@ -142,6 +142,7 @@ impl Exempt {
|| plain.contains('┬') || plain.contains('\u{2061}') // equation input (UnicodeMath), drawn as math
|| matches!(plain, "WordCraft" | "WordCraft User" | "Discord" | "https://" | "auto" | "I. II. III." | "i. ii. iii.")
|| plain == "CONFIDENTIAL" // the watermark dialog's text: document content, kept as typed
|| plain == "Figure 1" // the caption dialog's sample: the label goes into the document as is
}
}
@ -195,6 +196,12 @@ fn untranslated_interface_text() {
"models",
"insertMergeField",
"findRecipient",
"caption",
"index",
"markEntry",
"toc",
"sourceManager",
"createSource",
];
for name in dialogs {
let _ = app.run("ui.dialog", json!({"name": name}));

View File

@ -1469,6 +1469,104 @@
Continuous numbering Numeración continua
Apply Aplicar
Convert Notes Convertir notas
A, B, C, … A, B, C, …
Above the item Encima del elemento
Accessed on Consultado el
Add New Source… Agregar nueva fuente…
Add the chapter number Agregar el número de capítulo
Atmosphere Atmósfera
Below the item Debajo del elemento
Book Libro
Book title Título del libro
Caption Leyenda
Caption Numbering Numeración de leyendas
Caption text Texto de la leyenda
Caption: Leyenda:
Chapter in a book Capítulo de libro
Chapters begin at: Los capítulos empiezan en:
Collect entries from: Recoger entradas de:
Columns: Columnas:
Conference Conferencia
Conference paper Ponencia de conferencia
Container Title Título del contenedor
Create Source Crear fuente
Cross-reference: Referencia cruzada:
Custom Table of Contents… Tabla de contenido personalizada…
DOI DOI
Day Día
Delete Label Eliminar rótulo
Edit Source Editar fuente
Edition Edición
Edit… Editar…
Entry marked. Entrada marcada.
Example: {sample} Ejemplo: {sample}
Figure Figura
Film Película
Heading styles (levels 1–{n}) Estilos de título (niveles 1–{n})
Heading {n} Título {n}
I, II, III, … I, II, III, …
Include page numbers Incluir números de página
Indented Con sangría
Institution Institución
Ionosphere Ionosfera
Issue Número
Journal Revista
Label: Rótulo:
Last, First; Last, First Apellido, Nombre; Apellido, Nombre
Leave the label out of the caption Omitir el rótulo en la leyenda
Letter headings Encabezados de letra
Levels: Niveles:
Link entries to their headings Vincular las entradas a sus títulos
Made from the author and year when empty Si está vacío, se forma con autor y año
Magazine or newspaper article Artículo de revista o periódico
Main entry: Entrada principal:
Mark Marcar
Mark All Marcar todas
Mark Entry… Marcar entrada…
Mark Index Entry Marcar entrada de índice
Mesosphere Mesosfera
Month Mes
New Label Nuevo rótulo
New Label… Nuevo rótulo…
New… Nueva…
No sources yet. Aún no hay fuentes.
Numbering… Numeración…
Options… Opciones…
Other Otro
Other styles and their TOC level: Otros estilos y su nivel en la tabla:
Outline levels Niveles de esquema
Page number style Estilo del número de página
Page numbers at the right margin Números de página en el margen derecho
Page title Título de la página
Pages of bookmark: Páginas del marcador:
Periodical Publicación periódica
Position: Posición:
Publisher Editorial
Run-in Continuo
See Véase
Separator: Separador:
Show every bibliography field Mostrar todos los campos bibliográficos
Site name Nombre del sitio
Source Manager Administrador de fuentes
Source type: Tipo de fuente:
Sources in this document: Fuentes de este documento:
Styles: Estilos:
Subentry: Subentrada:
TC entry fields Campos de entrada TC
Tab leader: Carácter de relleno:
Table of Contents Options Opciones de tabla de contenido
Table of Contents Styles Estilos de tabla de contenido
Tag Etiqueta
This page Esta página
Title Título
Type: Tipo:
URL URL
Volume Volumen
Website Sitio web
Year Año
a, b, c, … a, b, c, …
i, ii, iii, … i, ii, iii, …
{n} entries marked. Entradas marcadas: {n}.
Lasso Select Selección con lazo
Move Selected Ink Mover la tinta seleccionada
Delete Selected Ink Eliminar la tinta seleccionada

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -1463,6 +1463,104 @@
Continuous numbering Pidev nummerdus
Apply Rakenda
Convert Notes Teisenda märkused
A, B, C, … A, B, C, …
Above the item Objekti kohal
Accessed on Vaadatud
Add New Source… Lisa uus allikas…
Add the chapter number Lisa peatüki number
Atmosphere Atmosfäär
Below the item Objekti all
Book Raamat
Book title Raamatu pealkiri
Caption Pealdis
Caption Numbering Pealdiste nummerdamine
Caption text Pealdise tekst
Caption: Pealdis:
Chapter in a book Peatükk raamatus
Chapters begin at: Peatükid algavad:
Collect entries from: Kirjed võetakse:
Columns: Veerud:
Conference Konverents
Conference paper Konverentsiartikkel
Container Title Kogumiku pealkiri
Create Source Loo allikas
Cross-reference: Ristviide:
Custom Table of Contents… Kohandatud sisukord…
DOI DOI
Day Päev
Delete Label Kustuta silt
Edit Source Muuda allikat
Edition Väljaanne
Edit… Muuda…
Entry marked. Kirje märgitud.
Example: {sample} Näide: {sample}
Figure Joonis
Film Film
Heading styles (levels 1–{n}) Pealkirjalaadid (tasemed 1–{n})
Heading {n} Pealkiri {n}
I, II, III, … I, II, III, …
Include page numbers Lisa leheküljenumbrid
Indented Taandega
Institution Asutus
Ionosphere Ionosfäär
Issue Number
Journal Ajakiri
Label: Silt:
Last, First; Last, First Perenimi, Eesnimi; Perenimi, Eesnimi
Leave the label out of the caption Jäta silt pealdisest välja
Letter headings Tähepealkirjad
Levels: Tasemed:
Link entries to their headings Lingi kirjed pealkirjadega
Made from the author and year when empty Tühjana tehakse autorist ja aastast
Magazine or newspaper article Ajakirja- või ajaleheartikkel
Main entry: Põhikirje:
Mark Märgi
Mark All Märgi kõik
Mark Entry… Märgi kirje…
Mark Index Entry Märgi registrikirje
Mesosphere Mesosfäär
Month Kuu
New Label Uus silt
New Label… Uus silt…
New… Uus…
No sources yet. Allikaid veel pole.
Numbering… Nummerdamine…
Options… Suvandid…
Other Muu
Other styles and their TOC level: Muud laadid ja nende sisukorratase:
Outline levels Liigendustasemed
Page number style Leheküljenumbri laad
Page numbers at the right margin Leheküljenumbrid paremas servas
Page title Lehe pealkiri
Pages of bookmark: Järjehoidja leheküljed:
Periodical Perioodika
Position: Asukoht:
Publisher Kirjastus
Run-in Jooksev
See Vt
Separator: Eraldaja:
Show every bibliography field Näita kõiki bibliograafiavälju
Site name Saidi nimi
Source Manager Allikahaldur
Source type: Allika tüüp:
Sources in this document: Selle dokumendi allikad:
Styles: Laadid:
Subentry: Alamkirje:
TC entry fields TC-kirjeväljad
Tab leader: Tabelduse täide:
Table of Contents Options Sisukorra suvandid
Table of Contents Styles Sisukorra laadid
Tag Silt
This page See lehekülg
Title Pealkiri
Type: Tüüp:
URL URL
Volume Köide
Website Veebisait
Year Aasta
a, b, c, … a, b, c, …
i, ii, iii, … i, ii, iii, …
{n} entries marked. Märgitud kirjeid: {n}.
Lasso Select Lassoga valimine
Move Selected Ink Teisalda valitud tint
Delete Selected Ink Kustuta valitud tint

Can't render this file because it has a wrong number of fields in line 12.

View File

@ -1453,6 +1453,104 @@
Continuous numbering 通し番号
Apply 適用
Convert Notes 注の変換
A, B, C, … A, B, C, …
Above the item 項目の上
Accessed on アクセス日
Add New Source… 新しい資料文献の追加…
Add the chapter number 章番号を付ける
Atmosphere 大気
Below the item 項目の下
Book 書籍
Book title 書籍名
Caption 図表番号
Caption Numbering 図表番号の書式
Caption text 図表番号の文字列
Caption: 図表番号:
Chapter in a book 書籍の章
Chapters begin at: 章の開始:
Collect entries from: 項目の取得元:
Columns: 段数:
Conference 会議
Conference paper 会議論文
Container Title 掲載元の名前
Create Source 資料文献の作成
Cross-reference: 相互参照:
Custom Table of Contents… ユーザー設定の目次…
DOI DOI
Day 日
Delete Label ラベル削除
Edit Source 資料文献の編集
Edition 版
Edit… 編集…
Entry marked. 項目を登録しました。
Example: {sample} 例: {sample}
Figure 図
Film 映画
Heading styles (levels 1–{n}) 見出しスタイル (レベル 1–{n})
Heading {n} 見出し {n}
I, II, III, … I, II, III, …
Include page numbers ページ番号を含める
Indented インデント
Institution 機関
Ionosphere 電離圏
Issue 号
Journal 学術誌
Label: ラベル:
Last, First; Last, First 姓, 名; 姓, 名
Leave the label out of the caption ラベルを図表番号に含めない
Letter headings 文字見出し
Levels: レベル:
Link entries to their headings 項目を見出しにリンクする
Made from the author and year when empty 空欄なら著者と年から作成
Magazine or newspaper article 雑誌・新聞の記事
Main entry: 主項目:
Mark 登録
Mark All すべて登録
Mark Entry… 索引登録…
Mark Index Entry 索引登録
Mesosphere 中間圏
Month 月
New Label 新しいラベル
New Label… ラベル名…
New… 新規作成…
No sources yet. 資料文献はまだありません。
Numbering… 番号付け…
Options… オプション…
Other その他
Other styles and their TOC level: その他のスタイルと目次レベル:
Outline levels アウトライン レベル
Page number style ページ番号のスタイル
Page numbers at the right margin ページ番号を右端に配置
Page title ページ名
Pages of bookmark: ブックマークのページ:
Periodical 定期刊行物
Position: 位置:
Publisher 出版社
Run-in 追い込み
See 参照
Separator: 区切り文字:
Show every bibliography field すべての文献フィールドを表示
Site name サイト名
Source Manager 資料文献の管理
Source type: 資料文献の種類:
Sources in this document: この文書の資料文献:
Styles: スタイル:
Subentry: 副項目:
TC entry fields TC 項目フィールド
Tab leader: タブ リーダー:
Table of Contents Options 目次オプション
Table of Contents Styles 目次スタイル
Tag タグ
This page このページ
Title タイトル
Type: 種類:
URL URL
Volume 巻
Website Web サイト
Year 年
a, b, c, … a, b, c, …
i, ii, iii, … i, ii, iii, …
{n} entries marked. {n} 件の項目を登録しました。
Lasso Select なげなわ選択
Move Selected Ink 選択したインクを移動
Delete Selected Ink 選択したインクを削除

Can't render this file because it has a wrong number of fields in line 3.

View File

@ -1465,6 +1465,104 @@
Continuous numbering Numeração contínua
Apply Aplicar
Convert Notes Converter notas
A, B, C, … A, B, C, …
Above the item Acima do item
Accessed on Acessado em
Add New Source… Adicionar nova fonte…
Add the chapter number Adicionar o número do capítulo
Atmosphere Atmosfera
Below the item Abaixo do item
Book Livro
Book title Título do livro
Caption Legenda
Caption Numbering Numeração de legendas
Caption text Texto da legenda
Caption: Legenda:
Chapter in a book Capítulo de livro
Chapters begin at: Os capítulos começam em:
Collect entries from: Coletar entradas de:
Columns: Colunas:
Conference Conferência
Conference paper Artigo de conferência
Container Title Título do contêiner
Create Source Criar fonte
Cross-reference: Referência cruzada:
Custom Table of Contents… Sumário personalizado…
DOI DOI
Day Dia
Delete Label Excluir rótulo
Edit Source Editar fonte
Edition Edição
Edit… Editar…
Entry marked. Entrada marcada.
Example: {sample} Exemplo: {sample}
Figure Figura
Film Filme
Heading styles (levels 1–{n}) Estilos de título (níveis 1–{n})
Heading {n} Título {n}
I, II, III, … I, II, III, …
Include page numbers Incluir números de página
Indented Recuado
Institution Instituição
Ionosphere Ionosfera
Issue Número
Journal Periódico científico
Label: Rótulo:
Last, First; Last, First Sobrenome, Nome; Sobrenome, Nome
Leave the label out of the caption Omitir o rótulo na legenda
Letter headings Cabeçalhos de letra
Levels: Níveis:
Link entries to their headings Vincular as entradas aos títulos
Made from the author and year when empty Se vazio, é formado pelo autor e ano
Magazine or newspaper article Artigo de revista ou jornal
Main entry: Entrada principal:
Mark Marcar
Mark All Marcar todas
Mark Entry… Marcar entrada…
Mark Index Entry Marcar entrada de índice
Mesosphere Mesosfera
Month Mês
New Label Novo rótulo
New Label… Novo rótulo…
New… Nova…
No sources yet. Ainda não há fontes.
Numbering… Numeração…
Options… Opções…
Other Outro
Other styles and their TOC level: Outros estilos e seu nível no sumário:
Outline levels Níveis de estrutura de tópicos
Page number style Estilo do número de página
Page numbers at the right margin Números de página na margem direita
Page title Título da página
Pages of bookmark: Páginas do indicador:
Periodical Periódico
Position: Posição:
Publisher Editora
Run-in Contínuo
See Ver
Separator: Separador:
Show every bibliography field Mostrar todos os campos bibliográficos
Site name Nome do site
Source Manager Gerenciador de fontes
Source type: Tipo de fonte:
Sources in this document: Fontes deste documento:
Styles: Estilos:
Subentry: Subentrada:
TC entry fields Campos de entrada TC
Tab leader: Preenchimento de tabulação:
Table of Contents Options Opções do sumário
Table of Contents Styles Estilos do sumário
Tag Marca
This page Esta página
Title Título
Type: Tipo:
URL URL
Volume Volume
Website Site
Year Ano
a, b, c, … a, b, c, …
i, ii, iii, … i, ii, iii, …
{n} entries marked. Entradas marcadas: {n}.
Lasso Select Seleção com laço
Move Selected Ink Mover a tinta selecionada
Delete Selected Ink Excluir a tinta selecionada

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -1803,6 +1803,101 @@ preview Title Пример
Right-to-Left Section Раздел справа налево
Table direction: Направление таблицы:
Section direction: Направление раздела:
A, B, C, … A, B, C, …
Above the item Над объектом
Accessed on Дата обращения
Add New Source… Добавить новый источник…
Add the chapter number Добавить номер главы
Atmosphere Атмосфера
Below the item Под объектом
Book Книга
Book title Название книги
Caption Numbering Нумерация названий
Caption text Текст названия
Caption: Название:
Chapter in a book Глава книги
Chapters begin at: Главы начинаются с:
Collect entries from: Собирать записи из:
Columns: Столбцы:
Conference Конференция
Conference paper Доклад на конференции
Container Title Название контейнера
Create Source Создать источник
Cross-reference: Перекрёстная ссылка:
Custom Table of Contents… Настраиваемое оглавление…
DOI DOI
Day День
Delete Label Удалить подпись
Edit Source Изменить источник
Edition Издание
Edit… Изменить…
Entry marked. Запись помечена.
Example: {sample} Пример: {sample}
Figure Рисунок
Film Фильм
Heading styles (levels 1–{n}) Стили заголовков (уровни 1–{n})
I, II, III, … I, II, III, …
Include page numbers Включить номера страниц
Indented Со ступенчатым отступом
Institution Учреждение
Ionosphere Ионосфера
Issue Выпуск
Journal Журнал
Label: Подпись:
Last, First; Last, First Фамилия, Имя; Фамилия, Имя
Leave the label out of the caption Не включать подпись в название
Letter headings Заголовки с буквами
Levels: Уровни:
Link entries to their headings Связать записи с заголовками
Made from the author and year when empty Если пусто, составляется из автора и года
Magazine or newspaper article Статья в журнале или газете
Main entry: Основная запись:
Mark Пометить
Mark All Пометить все
Mark Entry… Пометить запись…
Mark Index Entry Пометка элемента указателя
Mesosphere Мезосфера
Month Месяц
New Label Новая подпись
New Label… Новая подпись…
New… Создать…
No sources yet. Источников пока нет.
Numbering… Нумерация…
Options… Параметры…
Other Другое
Other styles and their TOC level: Другие стили и их уровень в оглавлении:
Outline levels Уровни структуры
Page number style Стиль номеров страниц
Page numbers at the right margin Номера страниц по правому краю
Page title Название страницы
Pages of bookmark: Страницы закладки:
Periodical Периодическое издание
Position: Положение:
Publisher Издатель
Run-in В подбор
See См.
Separator: Разделитель:
Show every bibliography field Показать все поля библиографии
Site name Название сайта
Source Manager Диспетчер источников
Source type: Тип источника:
Sources in this document: Источники в этом документе:
Styles: Стили:
Subentry: Подзапись:
TC entry fields Поля записей TC
Tab leader: Заполнитель табуляции:
Table of Contents Options Параметры оглавления
Table of Contents Styles Стили оглавления
Tag Тег
This page Эта страница
Type: Тип:
URL URL
Volume Том
Website Веб-сайт
Year Год
a, b, c, … a, b, c, …
i, ii, iii, … i, ii, iii, …
{n} entries marked. Помечено записей: {n}.
Lasso Select Лассо
Move Selected Ink Переместить выделенные штрихи
Delete Selected Ink Удалить выделенные штрихи

Can't render this file because it contains an unexpected character in line 7 and column 25.

View File

@ -1463,6 +1463,104 @@
Continuous numbering Neprekidna numeracija
Apply Primeni
Convert Notes Pretvori napomene
A, B, C, … A, B, C, …
Above the item Iznad stavke
Accessed on Pristupljeno
Add New Source… Dodaj novi izvor…
Add the chapter number Dodaj broj poglavlja
Atmosphere Atmosfera
Below the item Ispod stavke
Book Knjiga
Book title Naslov knjige
Caption Natpis
Caption Numbering Numerisanje natpisa
Caption text Tekst natpisa
Caption: Natpis:
Chapter in a book Poglavlje u knjizi
Chapters begin at: Poglavlja počinju na:
Collect entries from: Stavke uzeti iz:
Columns: Kolone:
Conference Konferencija
Conference paper Rad sa konferencije
Container Title Naslov zbirke
Create Source Napravi izvor
Cross-reference: Unakrsna referenca:
Custom Table of Contents… Prilagođeni sadržaj…
DOI DOI
Day Dan
Delete Label Obriši oznaku
Edit Source Uredi izvor
Edition Izdanje
Edit… Uredi…
Entry marked. Stavka je označena.
Example: {sample} Primer: {sample}
Figure Slika
Film Film
Heading styles (levels 1–{n}) Stilovi naslova (nivoi 1–{n})
Heading {n} Naslov {n}
I, II, III, … I, II, III, …
Include page numbers Uključi brojeve stranica
Indented Uvučeno
Institution Institucija
Ionosphere Jonosfera
Issue Broj
Journal Časopis
Label: Oznaka:
Last, First; Last, First Prezime, Ime; Prezime, Ime
Leave the label out of the caption Izostavi oznaku iz natpisa
Letter headings Naslovi po slovima
Levels: Nivoi:
Link entries to their headings Poveži stavke sa naslovima
Made from the author and year when empty Ako je prazno, pravi se od autora i godine
Magazine or newspaper article Članak u magazinu ili novinama
Main entry: Glavna stavka:
Mark Označi
Mark All Označi sve
Mark Entry… Označi stavku…
Mark Index Entry Označi stavku indeksa
Mesosphere Mezosfera
Month Mesec
New Label Nova oznaka
New Label… Nova oznaka…
New… Novo…
No sources yet. Još nema izvora.
Numbering… Numerisanje…
Options… Opcije…
Other Drugo
Other styles and their TOC level: Ostali stilovi i njihov nivo u sadržaju:
Outline levels Nivoi strukture
Page number style Stil broja stranice
Page numbers at the right margin Brojevi stranica uz desnu marginu
Page title Naslov stranice
Pages of bookmark: Stranice obeleživača:
Periodical Periodika
Position: Položaj:
Publisher Izdavač
Run-in U nizu
See Vidi
Separator: Razdvajač:
Show every bibliography field Prikaži sva polja bibliografije
Site name Naziv sajta
Source Manager Upravljač izvorima
Source type: Vrsta izvora:
Sources in this document: Izvori u ovom dokumentu:
Styles: Stilovi:
Subentry: Podstavka:
TC entry fields Polja stavki TC
Tab leader: Popuna tabulatora:
Table of Contents Options Opcije sadržaja
Table of Contents Styles Stilovi sadržaja
Tag Oznaka
This page Ova stranica
Title Naslov
Type: Vrsta:
URL URL
Volume Tom
Website Veb-sajt
Year Godina
a, b, c, … a, b, c, …
i, ii, iii, … i, ii, iii, …
{n} entries marked. Označeno stavki: {n}.
Lasso Select Izbor lasom
Move Selected Ink Premesti izabrani rukopis
Delete Selected Ink Obriši izabrani rukopis

Can't render this file because it contains an unexpected character in line 6 and column 25.

View File

@ -1467,6 +1467,104 @@
Continuous numbering Непрекидна нумерација
Apply Примени
Convert Notes Претвори напомене
A, B, C, … A, B, C, …
Above the item Изнад ставке
Accessed on Приступљено
Add New Source… Додај нови извор…
Add the chapter number Додај број поглавља
Atmosphere Атмосфера
Below the item Испод ставке
Book Књига
Book title Наслов књиге
Caption Натпис
Caption Numbering Нумерисање натписа
Caption text Текст натписа
Caption: Натпис:
Chapter in a book Поглавље у књизи
Chapters begin at: Поглавља почињу на:
Collect entries from: Ставке узети из:
Columns: Колоне:
Conference Конференција
Conference paper Рад са конференције
Container Title Наслов збирке
Create Source Направи извор
Cross-reference: Унакрсна референца:
Custom Table of Contents… Прилагођени садржај…
DOI DOI
Day Дан
Delete Label Обриши ознаку
Edit Source Уреди извор
Edition Издање
Edit… Уреди…
Entry marked. Ставка је означена.
Example: {sample} Пример: {sample}
Figure Слика
Film Филм
Heading styles (levels 1–{n}) Стилови наслова (нивои 1–{n})
Heading {n} Наслов {n}
I, II, III, … I, II, III, …
Include page numbers Укључи бројеве страница
Indented Увучено
Institution Институција
Ionosphere Јоносфера
Issue Број
Journal Часопис
Label: Ознака:
Last, First; Last, First Презиме, Име; Презиме, Име
Leave the label out of the caption Изостави ознаку из натписа
Letter headings Наслови по словима
Levels: Нивои:
Link entries to their headings Повежи ставке са насловима
Made from the author and year when empty Ако је празно, прави се од аутора и године
Magazine or newspaper article Чланак у магазину или новинама
Main entry: Главна ставка:
Mark Означи
Mark All Означи све
Mark Entry… Означи ставку…
Mark Index Entry Означи ставку индекса
Mesosphere Мезосфера
Month Месец
New Label Нова ознака
New Label… Нова ознака…
New… Ново…
No sources yet. Још нема извора.
Numbering… Нумерисање…
Options… Опције…
Other Друго
Other styles and their TOC level: Остали стилови и њихов ниво у садржају:
Outline levels Нивои структуре
Page number style Стил броја странице
Page numbers at the right margin Бројеви страница уз десну маргину
Page title Наслов странице
Pages of bookmark: Странице обележивача:
Periodical Периодика
Position: Положај:
Publisher Издавач
Run-in У низу
See Види
Separator: Раздвајач:
Show every bibliography field Прикажи сва поља библиографије
Site name Назив сајта
Source Manager Управљач изворима
Source type: Врста извора:
Sources in this document: Извори у овом документу:
Styles: Стилови:
Subentry: Подставка:
TC entry fields Поља ставки TC
Tab leader: Попуна табулатора:
Table of Contents Options Опције садржаја
Table of Contents Styles Стилови садржаја
Tag Ознака
This page Ова страница
Title Наслов
Type: Врста:
URL URL
Volume Том
Website Веб-сајт
Year Година
a, b, c, … a, b, c, …
i, ii, iii, … i, ii, iii, …
{n} entries marked. Означено ставки: {n}.
Lasso Select Избор ласом
Move Selected Ink Премести изабрани рукопис
Delete Selected Ink Обриши изабрани рукопис

Can't render this file because it contains an unexpected character in line 11 and column 25.

View File

@ -1421,6 +1421,104 @@
Right-to-Left Section Sağdan Sola Bölüm
Table direction: Tablo yönü:
Section direction: Bölüm yönü:
A, B, C, … A, B, C, …
Above the item Öğenin üstünde
Accessed on Erişim tarihi
Add New Source… Yeni Kaynak Ekle…
Add the chapter number Bölüm numarasını ekle
Atmosphere Atmosfer
Below the item Öğenin altında
Book Kitap
Book title Kitap başlığı
Caption Başlık
Caption Numbering Başlık Numaralandırma
Caption text Başlık metni
Caption: Başlık:
Chapter in a book Kitapta bölüm
Chapters begin at: Bölümler şunla başlar:
Collect entries from: Girdileri şuradan topla:
Columns: Sütunlar:
Conference Konferans
Conference paper Konferans bildirisi
Container Title Kapsayıcı Başlığı
Create Source Kaynak Oluştur
Cross-reference: Çapraz başvuru:
Custom Table of Contents… Özel İçindekiler Tablosu…
DOI DOI
Day Gün
Delete Label Etiketi Sil
Edit Source Kaynağı Düzenle
Edition Baskı
Edit… Düzenle…
Entry marked. Girdi işaretlendi.
Example: {sample} Örnek: {sample}
Figure Şekil
Film Film
Heading styles (levels 1–{n}) Başlık stilleri (1–{n}. düzeyler)
Heading {n} Başlık {n}
I, II, III, … I, II, III, …
Include page numbers Sayfa numaralarını ekle
Indented Girintili
Institution Kurum
Ionosphere İyonosfer
Issue Sayı
Journal Dergi
Label: Etiket:
Last, First; Last, First Soyadı, Adı; Soyadı, Adı
Leave the label out of the caption Etiketi başlıktan çıkar
Letter headings Harf başlıkları
Levels: Düzeyler:
Link entries to their headings Girdileri başlıklarına bağla
Made from the author and year when empty Boşsa yazardan ve yıldan oluşturulur
Magazine or newspaper article Dergi veya gazete makalesi
Main entry: Ana girdi:
Mark İşaretle
Mark All Tümünü İşaretle
Mark Entry… Girdiyi İşaretle…
Mark Index Entry Dizin Girdisini İşaretle
Mesosphere Mezosfer
Month Ay
New Label Yeni Etiket
New Label… Yeni Etiket…
New… Yeni…
No sources yet. Henüz kaynak yok.
Numbering… Numaralandırma…
Options… Seçenekler…
Other Diğer
Other styles and their TOC level: Diğer stiller ve İçindekiler düzeyleri:
Outline levels Anahat düzeyleri
Page number style Sayfa numarası stili
Page numbers at the right margin Sayfa numaraları sağ kenar boşluğunda
Page title Sayfa başlığı
Pages of bookmark: Yer işaretinin sayfaları:
Periodical Süreli yayın
Position: Konum:
Publisher Yayıncı
Run-in Yan yana
See Bkz.
Separator: Ayırıcı:
Show every bibliography field Tüm kaynakça alanlarını göster
Site name Site adı
Source Manager Kaynak Yöneticisi
Source type: Kaynak türü:
Sources in this document: Bu belgedeki kaynaklar:
Styles: Stiller:
Subentry: Alt girdi:
TC entry fields TC girdi alanları
Tab leader: Sekme öncülü:
Table of Contents Options İçindekiler Tablosu Seçenekleri
Table of Contents Styles İçindekiler Tablosu Stilleri
Tag Etiket
This page Bu sayfa
Title Başlık
Type: Tür:
URL URL
Volume Cilt
Website Web sitesi
Year Yıl
a, b, c, … a, b, c, …
i, ii, iii, … i, ii, iii, …
{n} entries marked. {n} girdi işaretlendi.
Lasso Select Kement Seçimi
Move Selected Ink Seçili Mürekkebi Taşı
Delete Selected Ink Seçili Mürekkebi Sil

Can't render this file because it has a wrong number of fields in line 18.

View File

@ -1465,6 +1465,104 @@
Continuous numbering Наскрізна нумерація
Apply Застосувати
Convert Notes Перетворити примітки
A, B, C, … A, B, C, …
Above the item Над елементом
Accessed on Дата звернення
Add New Source… Додати нове джерело…
Add the chapter number Додати номер розділу
Atmosphere Атмосфера
Below the item Під елементом
Book Книга
Book title Назва книги
Caption Підпис
Caption Numbering Нумерація підписів
Caption text Текст підпису
Caption: Підпис:
Chapter in a book Розділ книги
Chapters begin at: Розділи починаються з:
Collect entries from: Брати елементи з:
Columns: Стовпці:
Conference Конференція
Conference paper Доповідь конференції
Container Title Назва видання
Create Source Створити джерело
Cross-reference: Перехресне посилання:
Custom Table of Contents… Настроюваний зміст…
DOI DOI
Day День
Delete Label Видалити мітку
Edit Source Редагувати джерело
Edition Видання
Edit… Редагувати…
Entry marked. Елемент позначено.
Example: {sample} Приклад: {sample}
Figure Рисунок
Film Фільм
Heading styles (levels 1–{n}) Стилі заголовків (рівні 1–{n})
Heading {n} Заголовок {n}
I, II, III, … I, II, III, …
Include page numbers Додавати номери сторінок
Indented З відступом
Institution Установа
Ionosphere Іоносфера
Issue Випуск
Journal Журнал
Label: Мітка:
Last, First; Last, First Прізвище, Ім’я; Прізвище, Ім’я
Leave the label out of the caption Не включати мітку в підпис
Letter headings Літерні заголовки
Levels: Рівні:
Link entries to their headings Пов’язати елементи із заголовками
Made from the author and year when empty Якщо порожньо, утворюється з автора й року
Magazine or newspaper article Стаття в журналі чи газеті
Main entry: Основний елемент:
Mark Позначити
Mark All Позначити всі
Mark Entry… Позначити елемент…
Mark Index Entry Позначити елемент покажчика
Mesosphere Мезосфера
Month Місяць
New Label Нова мітка
New Label… Нова мітка…
New… Створити…
No sources yet. Джерел ще немає.
Numbering… Нумерація…
Options… Параметри…
Other Інше
Other styles and their TOC level: Інші стилі та їхній рівень у змісті:
Outline levels Рівні структури
Page number style Стиль номера сторінки
Page numbers at the right margin Номери сторінок біля правого поля
Page title Назва сторінки
Pages of bookmark: Сторінки закладки:
Periodical Періодичне видання
Position: Розташування:
Publisher Видавець
Run-in Суцільний
See Див.
Separator: Роздільник:
Show every bibliography field Показати всі поля бібліографії
Site name Назва сайту
Source Manager Диспетчер джерел
Source type: Тип джерела:
Sources in this document: Джерела в цьому документі:
Styles: Стилі:
Subentry: Піделемент:
TC entry fields Поля елементів TC
Tab leader: Заповнювач табуляції:
Table of Contents Options Параметри змісту
Table of Contents Styles Стилі змісту
Tag Тег
This page Ця сторінка
Title Назва
Type: Тип:
URL URL
Volume Том
Website Вебсайт
Year Рік
a, b, c, … a, b, c, …
i, ii, iii, … i, ii, iii, …
{n} entries marked. Позначено елементів: {n}.
Lasso Select Виділення ласо
Move Selected Ink Перемістити виділений рукопис
Delete Selected Ink Видалити виділений рукопис

Can't render this file because it contains an unexpected character in line 6 and column 31.

View File

@ -1465,6 +1465,104 @@
Continuous numbering 连续编号
Apply 应用
Convert Notes 转换注释
A, B, C, … A, B, C, …
Above the item 项目上方
Accessed on 访问日期
Add New Source… 添加新源…
Add the chapter number 添加章节编号
Atmosphere 大气
Below the item 项目下方
Book 书籍
Book title 书名
Caption 题注
Caption Numbering 题注编号
Caption text 题注文字
Caption: 题注:
Chapter in a book 书籍章节
Chapters begin at: 章节起始于:
Collect entries from: 条目来源:
Columns: 栏数:
Conference 会议
Conference paper 会议论文
Container Title 载体标题
Create Source 创建源
Cross-reference: 交叉引用:
Custom Table of Contents… 自定义目录…
DOI DOI
Day 日
Delete Label 删除标签
Edit Source 编辑源
Edition 版次
Edit… 编辑…
Entry marked. 已标记条目。
Example: {sample} 示例:{sample}
Figure 图
Film 电影
Heading styles (levels 1–{n}) 标题样式(级别 1–{n})
Heading {n} 标题 {n}
I, II, III, … I, II, III, …
Include page numbers 包含页码
Indented 缩进式
Institution 机构
Ionosphere 电离层
Issue 期号
Journal 期刊
Label: 标签:
Last, First; Last, First 姓, 名; 姓, 名
Leave the label out of the caption 题注中不含标签
Letter headings 字母标题
Levels: 级别:
Link entries to their headings 将条目链接到标题
Made from the author and year when empty 留空时由作者和年份生成
Magazine or newspaper article 杂志或报纸文章
Main entry: 主索引项:
Mark 标记
Mark All 全部标记
Mark Entry… 标记条目…
Mark Index Entry 标记索引项
Mesosphere 中间层
Month 月
New Label 新建标签
New Label… 新建标签…
New… 新建…
No sources yet. 尚无源。
Numbering… 编号…
Options… 选项…
Other 其他
Other styles and their TOC level: 其他样式及其目录级别:
Outline levels 大纲级别
Page number style 页码样式
Page numbers at the right margin 页码靠右边距
Page title 页面标题
Pages of bookmark: 书签所在页:
Periodical 期刊名
Position: 位置:
Publisher 出版商
Run-in 接排式
See 参见
Separator: 分隔符:
Show every bibliography field 显示所有书目字段
Site name 网站名称
Source Manager 源管理器
Source type: 源类型:
Sources in this document: 本文档中的源:
Styles: 样式:
Subentry: 次索引项:
TC entry fields TC 条目域
Tab leader: 制表符前导符:
Table of Contents Options 目录选项
Table of Contents Styles 目录样式
Tag 标记名
This page 当前页
Title 标题
Type: 类型:
URL URL
Volume 卷
Website 网站
Year 年份
a, b, c, … a, b, c, …
i, ii, iii, … i, ii, iii, …
{n} entries marked. 已标记 {n} 个条目。
Lasso Select 套索选择
Move Selected Ink 移动所选墨迹
Delete Selected Ink 删除所选墨迹

Can't render this file because it contains an unexpected character in line 7 and column 25.

View File

@ -1454,6 +1454,104 @@
Continuous numbering 連續編號
Apply 套用
Convert Notes 轉換附註
A, B, C, … A, B, C, …
Above the item 項目上方
Accessed on 存取日期
Add New Source… 新增來源…
Add the chapter number 加入章節編號
Atmosphere 大氣
Below the item 項目下方
Book 書籍
Book title 書名
Caption 標號
Caption Numbering 標號編號
Caption text 標號文字
Caption: 標號:
Chapter in a book 書籍章節
Chapters begin at: 章節起始於:
Collect entries from: 項目來源:
Columns: 欄數:
Conference 會議
Conference paper 會議論文
Container Title 載體標題
Create Source 建立來源
Cross-reference: 交互參照:
Custom Table of Contents… 自訂目錄…
DOI DOI
Day 日
Delete Label 刪除標籤
Edit Source 編輯來源
Edition 版次
Edit… 編輯…
Entry marked. 已標記項目。
Example: {sample} 範例:{sample}
Figure 圖
Film 電影
Heading styles (levels 1–{n}) 標題樣式(階層 1–{n})
Heading {n} 標題 {n}
I, II, III, … I, II, III, …
Include page numbers 包含頁碼
Indented 縮排式
Institution 機構
Ionosphere 電離層
Issue 期號
Journal 期刊
Label: 標籤:
Last, First; Last, First 姓, 名; 姓, 名
Leave the label out of the caption 標號中不含標籤
Letter headings 字母標題
Levels: 階層:
Link entries to their headings 將項目連結到標題
Made from the author and year when empty 留空時由作者和年份產生
Magazine or newspaper article 雜誌或報紙文章
Main entry: 主索引項:
Mark 標記
Mark All 全部標記
Mark Entry… 標記項目…
Mark Index Entry 標記索引項目
Mesosphere 中氣層
Month 月
New Label 新增標籤
New Label… 新增標籤…
New… 新增…
No sources yet. 尚無來源。
Numbering… 編號…
Options… 選項…
Other 其他
Other styles and their TOC level: 其他樣式及其目錄階層:
Outline levels 大綱階層
Page number style 頁碼樣式
Page numbers at the right margin 頁碼靠右邊界
Page title 頁面標題
Pages of bookmark: 書籤所在頁:
Periodical 期刊名
Position: 位置:
Publisher 出版者
Run-in 接排式
See 參見
Separator: 分隔符號:
Show every bibliography field 顯示所有書目欄位
Site name 網站名稱
Source Manager 來源管理員
Source type: 來源類型:
Sources in this document: 此文件中的來源:
Styles: 樣式:
Subentry: 次索引項:
TC entry fields TC 項目功能變數
Tab leader: 定位點前置字元:
Table of Contents Options 目錄選項
Table of Contents Styles 目錄樣式
Tag 標記名
This page 目前頁面
Title 標題
Type: 類型:
URL URL
Volume 卷
Website 網站
Year 年份
a, b, c, … a, b, c, …
i, ii, iii, … i, ii, iii, …
{n} entries marked. 已標記 {n} 個項目。
Lasso Select 套索選取
Move Selected Ink 移動選取的筆跡
Delete Selected Ink 刪除選取的筆跡

Can't render this file because it has a wrong number of fields in line 4.

View File

@ -24,6 +24,7 @@ pub mod dialogs_keyboard;
pub mod dialogs_lists;
pub mod dialogs_notes;
pub mod dialogs_para;
pub mod dialogs_refs;
pub mod dialogs_ribbon;
pub mod equation_tab;
pub mod file_dialogs;
@ -135,6 +136,9 @@ pub struct UiState {
/// Custom ribbon tabs and groups, hidden tabs and the Quick Access Toolbar (Customize Ribbon,
/// #379); the session owns them while running.
pub ribbon: wordcraft_engine::RibbonLayout,
/// Caption labels the user added (References › Insert Caption › New Label), kept between
/// runs like Word keeps them in its Normal template (#402).
pub caption_labels: Vec<String>,
/// Draw › Add Pen: the pens added to the pen gallery; the session owns them while running.
/// Entries that aren't pens are dropped on reading.
#[serde(deserialize_with = "wordcraft_engine::cmd::draw::lenient_pens")]
@ -166,6 +170,7 @@ impl Default for UiState {
dark_page: false,
keyboard: wordcraft_engine::KeyMap::default(),
ribbon: wordcraft_engine::RibbonLayout::default(),
caption_labels: Vec::new(),
ink_pens: Vec::new(),
}
}
@ -1200,6 +1205,14 @@ fn input_dialog(id: &str, params: &Value) -> Option<&'static str> {
"format.enclose" if params.get("shape").is_none() => Some("enclose"),
// Footnote and Endnote without settings shows its dialog (#385).
"references.noteOptions" if params.as_object().is_none_or(|m| m.is_empty()) => Some("noteOptions"),
// References dialogs (#402): Insert Caption, Insert Index, Mark Entry, Manage Sources and
// Insert Citation without a source.
"references.caption" | "references.index" if params.as_object().is_none_or(|m| m.is_empty()) => {
Some(if id == "references.caption" { "caption" } else { "index" })
}
"references.markEntry" if !has("entry") => Some("markEntry"),
"references.sources" if params.as_object().is_none_or(|m| m.is_empty()) => Some("sourceManager"),
"references.citation" if !has("tag") && !has("source") => Some("createSource"),
_ => None,
}
}

View File

@ -1089,6 +1089,8 @@ fn references(app: &mut WordApp, ui: &mut Ui) {
menu_button(ui, app, "toc", Some("Table of\nContents"), "Table of Contents", true, |ui, app| {
mi(ui, app, "Automatic Table 1 (3 levels)", "references.toc", json!({"levels": 3}));
mi(ui, app, "Automatic Table 2 (2 levels)", "references.toc", json!({"levels": 2, "title": "Table of Contents"}));
ui.separator();
mi(ui, app, "Custom Table of Contents…", "ui.dialog", json!({"name": "toc"}));
mi(ui, app, "Remove Table of Contents", "references.removeToc", json!({}));
});
stack(ui, |ui| {
@ -1113,7 +1115,18 @@ fn references(app: &mut WordApp, ui: &mut Ui) {
big(ui, app, "researcher", "Researcher", "references.researcher", json!({}), false);
});
group(ui, "Citations & Bibliography", None, app, |ui, app| {
big(ui, app, "citation", "Insert\nCitation", "references.citation", json!({}), false);
// The document's sources to cite, then a new one (Create Source, #402).
menu_button(ui, app, "citation", Some("Insert\nCitation"), "Insert Citation", true, |ui, app| {
let sources: Vec<(String, String)> =
app.session.doc.sources.iter().map(|s| (s.tag.clone(), format!("{} — {}", s.author, s.title))).collect();
for (tag, label) in sources {
mi_text(ui, app, &label, "references.citation", json!({"tag": tag}));
}
if !app.session.doc.sources.is_empty() {
ui.separator();
}
mi(ui, app, "Add New Source…", "ui.dialog", json!({"name": "createSource"}));
});
stack(ui, |ui| {
small(ui, app, "sources", Some("Manage Sources"), "Manage Sources", "references.sources", json!({}), false);
let current = app.session.bib_style.clone();

View File

@ -17,7 +17,7 @@ first (`gh pr list`), and update this file and the parity doc when a gap closes.
| 4 | **Pagination differs from Word**: Aptos has no metric-matched substitute; legacy compatibility options ignored; no page-by-page comparison harness | B | [layout-parity.md](layout-parity.md); [typing-parity.md](typing-parity.md) Known gaps | Page counts and line breaks differ, which matters for forms, legal and academic work | 30–50 + owner (font) | [layout-parity.md](layout-parity.md) |
| 5 | **Content controls** are lost (`w:sdt` unwrapped). Move tracking landed (#400): `w:moveFrom`/`w:moveTo` with their named ranges round-trip, a tracked cut and paste of a paragraph or sentence records a move, either end accepts/rejects both, green double strike/underline markup and "Moved from/to" balloons; still missing: drag-and-drop moves (no text drag yet) and move options in Track Changes Options. Formatting revisions landed (#41): `w:rPrChange`/`w:pPrChange`/`w:tblPrChange`/`w:trPrChange`/`w:tcPrChange`/`w:sectPrChange`/`w:numberingChange` round-trip, are recorded while tracking, accepted/rejected and shown in balloons; still missing: tracked style-definition changes and table/section changes made in WordCraft | B | Issues #41, #400; source grep | Legal and editorial review workflows; templates with form controls | 5–8 | [file-format-parity.md](file-format-parity.md) |
| 6 | **No native printing** on desktop: File › Print opens the PDF in the system viewer, which prints (#286); no print dialog of our own (printer, copies, ranges); web prints through the browser (#209) | B | `ui.print` → `apps/wordcraft/src/print.rs`; issue #15 | Everyone who prints | 5–8 | [hardware-parity.md](hardware-parity.md) |
| 7 | **Dialog depth**: 35 modal dialogs vs Word's ~100 (Caption, Index, TOC options, Citation, Compare, Protect, Language, Hyphenation, Line Numbers, Envelopes, Labels, AutoCorrect, Options panes…) | B | `Dialog` enum in `crates/ui-egui/src/dialogs.rs` | Power users reach for dialogs the ribbon alone doesn't cover | 20–30 | [ui-parity.md](ui-parity.md) |
| 7 | **Dialog depth**: 41 modal dialogs vs Word's ~100 (Compare, Protect, Language, Hyphenation, Line Numbers, Envelopes, Labels, AutoCorrect, Options panes…) | B | `Dialog` enum in `crates/ui-egui/src/dialogs.rs` | Power users reach for dialogs the ribbon alone doesn't cover | 15–25 | [ui-parity.md](ui-parity.md) |
| 8 | **Proofing is English only**; no language per run, no dictionaries for other languages | | `crates/proof` has one dictionary; Word ships 120 proofing tools; issues #25, #40, #100 | Every non-English writer | 25–45 | [localization-parity.md](localization-parity.md) |
| 9 | **Password-protected files** can't be opened or saved | | #55; `docbin` detects and refuses RC4/XOR | Corporate and legal users | 6–10 | [file-format-parity.md](file-format-parity.md) |
| 10 | **Objects**: no group (#267 open), text in a freely rotated text box (free rotation and flips landed, #332; text direction, alignment and linked text boxes landed, #369; contour wrap and wrap side landed, #354), shape effects, WordArt, connectors, dragging members inside a Drawing Canvas (the canvas landed, #344) | | [parity-checklist.md](parity-checklist.md) (Shape Format 6/6, Layout › Group) | Newsletters, flyers, reports with diagrams | 20–30 | [target-app-parity.md](target-app-parity.md) |
@ -64,6 +64,7 @@ The same gaps grouped the way the parity documents are, for agents working in on
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | trivial | Caption, Index, Mark Index Entry, Custom Table of Contents, Source Manager and Create/Edit Source dialogs landed (#402); removed from the dialog-depth list |
| 2026-10-11 | trivial | Document grid landed (#391); removed from the East Asian gap |
| 2026-10-11 | trivial | Move tracking landed (#400); gap #5 narrowed to content controls, 5–8 h |
| 2026-10-11 | trivial | #4: note options per section (below text, end of section, restart each section/page) and the Footnote and Endnote dialog (#385) |

View File

@ -64,7 +64,7 @@ full parity for the area.
| Page layout and sections | 7 | 70% | 72% | 12–20 | Margins, size, columns, breaks, page borders, line numbers, vertical alignment, drop caps, hyphenation. Columns don't balance; no vertical text; RTL sections missing. Detail: [layout-parity.md](layout-parity.md). |
| Headers, footers, page numbers, fields | 5 | 70% | 70% | 8–12 | First/even/odd, link to previous, TOC as an updatable field (#52), cross-references to real bookmarks (#227). Field coverage is a subset of Word's ~90 field codes; Field dialog missing. |
| Footnotes and endnotes | 3 | 60% | 60% | 3–5 | Placed and editable, numbers from Word files (#103); long notes continue onto the next page under the continuation separator (#352). Footnote and Endnote dialog with options per section: below text, end of section, number format, start at, restart each section or page, custom mark, convert (#385). |
| References (TOC, citations, bibliography, captions, index, TOA) | 5 | 65% | 60% | 12–20 | Working first versions, Zotero integration (#189), Zotero/Mendeley `ADDIN` fields round-trip. All 12 of Word's bibliography styles (APA, MLA, Chicago, IEEE, Harvard, ISO 690 author-date and numerical, Turabian, GB/T 7714, GOST name and title sort, SIST02; #383); source manager depth; bibliography sources not yet in DOCX (#169 open). |
| References (TOC, citations, bibliography, captions, index, TOA) | 5 | 65% | 60% | 12–20 | Working first versions, Zotero integration (#189), Zotero/Mendeley `ADDIN` fields round-trip. Caption, Index, Mark Entry, Custom TOC and Create/Edit Source dialogs; updates honour the TOC (`\o \u \t \f \n \p \h`), INDEX (`\c \e \h \r`), XE (`\b \i \t \r`) and SEQ/STYLEREF chapter switches (#402). All 12 of Word's bibliography styles (APA, MLA, Chicago, IEEE, Harvard, ISO 690 author-date and numerical, Turabian, GB/T 7714, GOST name and title sort, SIST02; #383); bibliography sources not yet in DOCX (#169 open). |
| Review (proofing UI, comments, track changes, compare, protect) | 8 | 72% | 75% | 15–25 | Comment balloons, tracked insert/delete incl. paragraph marks (#244, #125), compare, restrict editing. Formatting revisions (#41): kept in DOCX, recorded while tracking, accepted/rejected, "Formatted: …" balloons and change bars. Moves (#400): kept in DOCX, recorded on a tracked cut and paste, accepted/rejected at either end, green markup and "Moved from/to" balloons. Missing: drag-and-drop moves, insert/delete balloons, Translate, Block Authors, modern comment threads/mentions. Lowered: these gaps are confirmed by source (no `rPrChange` anywhere). |
| Proofing (spelling, grammar, thesaurus, languages) | 5 | 35% | (in Review) | 25–45 | English only: one dictionary (Moby list plus our supplement of modern words such as "desktop", #520), a rule-based grammar checker. Word ships 120 proofing tools for ~50 languages plus Editor (style refinements, similarity). Issues #25, #40, #100. |
| Mailings | 2 | 70% | 70% | 6–10 | Mail merge from CSV, fields, rules (IF, SKIPIF, NEXT, NEXTIF, evaluated alike in preview and merge), fields in headers/footers (one section per letter), preview, envelopes, labels. Select Recipients takes a typed list, CSV (#247) or a sheet of an Excel (.xlsx/.xlsm) or OpenDocument (.ods) workbook, with Select Table to pick the sheet (#334); no Outlook/Contacts/Access data sources, no email merge. |
@ -249,6 +249,7 @@ can be used without committing Word output.
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | trivial | References: Caption, Index, Mark Entry, Custom Table of Contents and Create/Edit Source dialogs, with the field switches they write honoured on update (#402); no percentage change |
| 2026-10-11 | trivial | Move tracking (#400): Review row updated; no percentage change |
| 2026-10-11 | trivial | Footnotes and endnotes: Footnote and Endnote dialog, options per section (position, format, start, restart each section/page), convert notes (#385); no percentage change |
| 2026-10-11 | trivial | Pictures, shapes, text boxes: text direction, Align Text and linked text boxes (Create Link / Break Link), DOCX `bodyPr` `vert`/`anchor` and `wps:linkedTxbx` (#369); no percentage change |

View File

@ -16,7 +16,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
| Quick Access Toolbar | AutoSave, then the user's commands (Save, Undo, Redo by default); add, remove and reorder from its … menu (common commands, More Commands…), right-click on any ribbon button, or Customize Ribbon; saved per user (#379, #376). Not shown below the ribbon | customizable | 90% | 0.5 |
| Customize Ribbon / Customize Keyboard | Customize Keyboard (#368): categories, a command's keys, press a combination to see which command uses it, Assign / Remove / Reset All; custom keys override built-ins, removed built-ins do nothing, saved per user; `tools.customizeKeyboard` for agents. Customize Ribbon (#379): show, hide and reorder tabs, custom tabs and custom groups (on any tab) holding any commands, rename, remove, Reset; right-click on the ribbon; saved per user; `tools.customizeRibbon` for agents. Built-in tabs can't be renamed; no import/export file | ✅ | 85% | 1–2 |
| Keyboard shortcuts | **125** commands bound in `CommandSpec::key`, 144 key combinations (measured on Linux/Windows); styles, spacing, symbols, fields, views, mail merge (#330); field keys Alt+F9, Shift+F9, Ctrl+Shift+F9, Ctrl+F11, Ctrl+Shift+F11, F11, Shift+F11 (#339); Alt+Shift+arrows (move paragraphs, promote/demote), Shift+F5 Go Back and Alt+X (#343). Missing: en dash on the numeric keypad | ~250 default shortcuts | ~70% | 2–4 |
| Modal dialogs | **38** (`Dialog` enum: Font, Enclose Characters, Paragraph, Tabs, Borders and Shading, Find, Go To, Insert Table, Table Properties, Page Setup, Link, Bookmark, Word Count, Zoom, Watermark, New Style, Modify Style, Manage Styles, Table Style, Commands, Icons, Paste Special, About, Save Changes, Password, Encrypt with Password, Define New Multilevel List, Track Changes Options; Columns, Symbol with Special Characters, Field (#321); Field code (#339); mail merge Recipients, Insert Merge Field, Find Recipient, Rules, Match Fields, Check for Errors) plus panes and menus for the rest | ~100 (Caption, Index, TOC options, Citation, Compare, Protect, Language, Hyphenation, Line Numbers, Envelopes, Labels, AutoCorrect, Options panes…) | ~40% | 20–30 |
| Modal dialogs | **44** (`Dialog` enum: Font, Enclose Characters, Paragraph, Tabs, Borders and Shading, Find, Go To, Insert Table, Table Properties, Page Setup, Link, Bookmark, Word Count, Zoom, Watermark, New Style, Modify Style, Manage Styles, Table Style, Commands, Icons, Paste Special, About, Save Changes, Password, Encrypt with Password, Define New Multilevel List, Track Changes Options; Columns, Symbol with Special Characters, Field (#321); Field code (#339); mail merge Recipients, Insert Merge Field, Find Recipient, Rules, Match Fields, Check for Errors; Caption with New Label and Numbering, Index, Mark Index Entry, Custom Table of Contents with Options and TOC styles, Source Manager, Create/Edit Source (#402)) plus panes and menus for the rest | ~100 (Compare, Protect, Language, Hyphenation, Line Numbers, Envelopes, Labels, AutoCorrect, Options panes…) | ~45% | 15–25 |
| Task panes (Navigation, Styles, Comments, Reviewing, Format Picture/Shape, Thesaurus, Accessibility, Clipboard, Selection) | Navigation, Styles, Comments, Thesaurus, Accessibility, Read Aloud, Zotero; Style Inspector (#236); Clipboard pane (#264) open | all | 70% | 4–6 |
| Context menus (right-click) | text, spelling suggestions, fields (Update Field, Edit Field…, Toggle Field Codes, #339); objects and tables partial | rich per context | 55% | 3–5 |
| Backstage (Home, New, Open, Info, Save As, Print, Share, Export, Options) | Home and New separate (#254), Info properties kept (#268) | | 68% | 4–6 |
@ -37,6 +37,7 @@ own (`AGENTS.md`: no Microsoft iconography, ever).
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | trivial | References dialogs: Caption (New/Delete Label, Numbering with chapter numbers), Index and Mark Index Entry (Mark All), Custom Table of Contents (Options, TOC styles via Modify Style), Source Manager and Create/Edit Source; modal dialog count 34 (#402) |
| 2026-10-11 | trivial | Customize Ribbon (custom tabs and groups, hidden and reordered tabs) and a customizable Quick Access Toolbar with a working … menu (#379, #376) |
| 2026-10-11 | trivial | Customize Keyboard dialog (Tools › Customize Keyboard, File › Options › Keyboard shortcuts): custom keys override built-ins and persist (#368) |
| 2026-10-11 | minor | Keyboard shortcuts 101 → 125 commands (#330): Reset Paragraph/Character Formatting, symbols, DATE/TIME/PAGE fields, view switches, Word Count, mail merge, F4 Repeat now reachable |