* 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:
parent
4ca58d67e1
commit
56f9456f03
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))?;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 == "\\*" {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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}));
|
||||
|
|
|
|||
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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.
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) |
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
Loading…
Reference in New Issue