mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-30 05:17:25 -06:00
- Model: NoteProps (pos, numFmt, numStart, numRestart) in the settings and each section; Document::note_options resolves section > document > defaults. - DOCX: w:footnotePr / w:endnotePr read and written in settings and every w:sectPr in schema order; hostile numStart clamped (ECMA-376 §17.11). - Layout: footnotes below the text, endnotes at the end of each section, numbering restarted per section or per page (from where notes land, at most three passes), per-section number formats. - Commands: references.noteOptions takes kind/pos/numFmt/numStart/ numRestart/scope; references.convertNotes (footnote/endnote/swap, one undo step); references.footnote/endnote take a custom mark. - UI: Footnote and Endnote dialog from the Footnotes group launcher; strings in every catalog. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f8f1d80c38
commit
4e710a920b
@@ -358,16 +358,9 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
}),
|
||||
CommandSpec::new("insert.crossReference", "Cross-reference", "Insert › Links", cross_ref)
|
||||
.params(r#"{"to": "heading|bookmark|figure|table", "target": string (text / name / number), "show"?: "text|page|number|aboveBelow"} or {} to list targets"#),
|
||||
CommandSpec::new("references.noteOptions", "Footnote and Endnote", "References › Footnotes", |s, v| {
|
||||
if let Some(f) = p::str(v, "footnoteFormat") {
|
||||
s.doc.settings.footnote_format = wordcraft_doc::section::NumFormat::from_ooxml(f);
|
||||
}
|
||||
if let Some(f) = p::str(v, "endnoteFormat") {
|
||||
s.doc.settings.endnote_format = wordcraft_doc::section::NumFormat::from_ooxml(f);
|
||||
}
|
||||
Ok(json!({"footnoteFormat": s.doc.settings.footnote_format, "endnoteFormat": s.doc.settings.endnote_format}))
|
||||
})
|
||||
.params(r#"{"footnoteFormat"?: "decimal|lowerRoman|upperRoman|lowerLetter|upperLetter", "endnoteFormat"?: …}"#),
|
||||
CommandSpec::new("references.noteOptions", "Footnote and Endnote", "References › Footnotes", super::references::note_options).params(
|
||||
r#"{"kind"?: "footnote|endnote", "pos"?: "pageBottom|beneathText" (footnotes) | "sectEnd|docEnd" (endnotes), "numFmt"?: "decimal|lowerRoman|upperRoman|lowerLetter|upperLetter|…", "numStart"?: 1-32767, "numRestart"?: "continuous|eachSect|eachPage" (each page: footnotes only), "scope"?: "document|section", "footnoteFormat"?: …, "endnoteFormat"?: …} — no settings: the options in effect at the caret"#,
|
||||
),
|
||||
CommandSpec::new("references.researcher", "Researcher", "References › Research", |s, _| {
|
||||
s.status = "Researcher needs an online service; WordCraft keeps your documents offline.".into();
|
||||
Ok(json!({"available": false}))
|
||||
|
||||
@@ -103,7 +103,7 @@ pub fn sect(s: &Session) -> SectionProps {
|
||||
}
|
||||
|
||||
/// Apply to the section(s) of the selection.
|
||||
fn with_sect(s: &mut Session, f: impl Fn(&mut SectionProps)) -> CmdResult {
|
||||
pub(crate) fn with_sect(s: &mut Session, f: impl Fn(&mut SectionProps)) -> CmdResult {
|
||||
let (a, b) = s.sel.ordered();
|
||||
let (i0, i1) = (a.path.0.first().copied().unwrap_or(0) as usize, b.path.0.first().copied().unwrap_or(0) as usize);
|
||||
let ends: Vec<usize> = s.doc.sections().iter().map(|(e, _)| *e).collect();
|
||||
|
||||
@@ -32,10 +32,21 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
.params(r#"{"level": 0 (do not show) | 1-9}"#),
|
||||
CommandSpec::new("references.footnote", "Insert Footnote", "References › Footnotes", |s, v| note(s, v, NoteKind::Footnote))
|
||||
.key("Mod+Alt+F")
|
||||
.params(r#"{"text"?: string}"#),
|
||||
.params(r#"{"text"?: string, "mark"?: string (a custom mark instead of the number)}"#),
|
||||
CommandSpec::new("references.endnote", "Insert Endnote", "References › Footnotes", |s, v| note(s, v, NoteKind::Endnote))
|
||||
.key("Mod+Alt+D")
|
||||
.params(r#"{"text"?: string}"#),
|
||||
.params(r#"{"text"?: string, "mark"?: string (a custom mark instead of the number)}"#),
|
||||
CommandSpec::new("references.convertNotes", "Convert Notes", "References › Footnotes", |s, v| {
|
||||
let (foot, end) = match p::req_str(v, "to")? {
|
||||
"endnote" | "endnotes" => (true, false),
|
||||
"footnote" | "footnotes" => (false, true),
|
||||
"swap" => (true, true),
|
||||
t => return Err(CmdError::Params(format!("unknown `to` {t:?}; use endnote, footnote or swap"))),
|
||||
};
|
||||
let n = s.doc.convert_notes(foot, end);
|
||||
Ok(json!({"converted": n}))
|
||||
})
|
||||
.params(r#"{"to": "endnote" (footnotes become endnotes) | "footnote" (endnotes become footnotes) | "swap"}"#),
|
||||
CommandSpec::new("references.nextFootnote", "Next Footnote", "References › Footnotes", |s, _| {
|
||||
let caret = s.sel.focus.clone();
|
||||
let refs = note_refs(s);
|
||||
@@ -193,6 +204,8 @@ pub fn update_toc(s: &mut Session) -> Result<(), CmdError> {
|
||||
|
||||
fn note(s: &mut Session, v: &Value, kind: NoteKind) -> CmdResult {
|
||||
let text = p::str(v, "text").unwrap_or("").to_string();
|
||||
// A custom mark: a few characters, as Word's dialog takes.
|
||||
let custom: String = p::str(v, "mark").unwrap_or("").trim().chars().filter(|c| !c.is_control()).take(10).collect();
|
||||
let (pk, style, refstyle) = match kind {
|
||||
NoteKind::Footnote => (PartKind::Footnote, "FootnoteText", "FootnoteReference"),
|
||||
NoteKind::Endnote => (PartKind::Endnote, "EndnoteText", "EndnoteReference"),
|
||||
@@ -201,23 +214,116 @@ fn note(s: &mut Session, v: &Value, kind: NoteKind) -> CmdResult {
|
||||
let id = s.doc.add_part(pk, Vec::new());
|
||||
para.insert_object(
|
||||
0,
|
||||
InlineObject::NoteRef { kind, id, custom: String::new() },
|
||||
InlineObject::NoteRef { kind, id, custom: custom.clone() },
|
||||
&CharProps { style: Some(refstyle.into()), ..Default::default() },
|
||||
)?;
|
||||
let len = para.len();
|
||||
para.insert_text(len, &format!(" {text}"), &CharProps::default())?;
|
||||
s.doc.set_story(StoryRef::Part(id), vec![para_block(para)])?;
|
||||
let at = delete_selection(s)?;
|
||||
s.doc.insert_object(
|
||||
&at,
|
||||
InlineObject::NoteRef { kind, id, custom: String::new() },
|
||||
&CharProps { style: Some(refstyle.into()), ..Default::default() },
|
||||
)?;
|
||||
s.doc.insert_object(&at, InlineObject::NoteRef { kind, id, custom }, &CharProps { style: Some(refstyle.into()), ..Default::default() })?;
|
||||
// Like Word, the caret moves into the new note.
|
||||
s.sel = Selection::caret(s.doc.end_of(StoryRef::Part(id)));
|
||||
Ok(json!({"id": id}))
|
||||
}
|
||||
|
||||
/// `references.noteOptions`: footnote or endnote placement, number format, start and restart,
|
||||
/// for the whole document (each section then follows it) or the selection's sections.
|
||||
pub(super) fn note_options(s: &mut Session, v: &Value) -> CmdResult {
|
||||
use wordcraft_doc::section::{MAX_NOTE_START, NotePos, NoteProps, NoteRestart, NumFormat};
|
||||
let fmt = |k: &str| -> Result<Option<NumFormat>, CmdError> {
|
||||
match p::str(v, k) {
|
||||
None => Ok(None),
|
||||
Some(f) if NumFormat::from_ooxml(f).ooxml() == f => Ok(Some(NumFormat::from_ooxml(f))),
|
||||
Some(f) => Err(CmdError::Params(format!("unknown number format {f:?}"))),
|
||||
}
|
||||
};
|
||||
// The earlier form: the document's number formats.
|
||||
let (old_foot, old_end) = (fmt("footnoteFormat")?, fmt("endnoteFormat")?);
|
||||
let endnote = match p::str(v, "kind") {
|
||||
None | Some("footnote") => false,
|
||||
Some("endnote") => true,
|
||||
Some(k) => return Err(CmdError::Params(format!("unknown `kind` {k:?}; use footnote or endnote"))),
|
||||
};
|
||||
let pos = match p::str(v, "pos") {
|
||||
None => None,
|
||||
Some(x) => match NotePos::from_ooxml(x) {
|
||||
Some(p @ (NotePos::PageBottom | NotePos::BeneathText)) if !endnote => Some(p),
|
||||
Some(p @ (NotePos::SectEnd | NotePos::DocEnd)) if endnote => Some(p),
|
||||
_ => {
|
||||
let ok = if endnote { "sectEnd or docEnd" } else { "pageBottom or beneathText" };
|
||||
return Err(CmdError::Params(format!("`pos` {x:?} doesn't apply here; use {ok}")));
|
||||
}
|
||||
},
|
||||
};
|
||||
let num_restart = match p::str(v, "numRestart") {
|
||||
None => None,
|
||||
Some(r) => match NoteRestart::from_ooxml(r) {
|
||||
Some(NoteRestart::EachPage) if endnote => return Err(CmdError::Params("endnotes can't start again on each page".into())),
|
||||
Some(r) => Some(r),
|
||||
None => return Err(CmdError::Params(format!("unknown `numRestart` {r:?}; use continuous, eachSect or eachPage"))),
|
||||
},
|
||||
};
|
||||
let change = NoteProps {
|
||||
pos,
|
||||
num_fmt: fmt("numFmt")?,
|
||||
num_start: p::f32(v, "numStart").map(|n| n.round().clamp(1.0, MAX_NOTE_START as f32) as u32),
|
||||
num_restart,
|
||||
};
|
||||
let whole = match p::str(v, "scope") {
|
||||
None | Some("document") => true,
|
||||
Some("section") => false,
|
||||
Some(x) => return Err(CmdError::Params(format!("unknown `scope` {x:?}; use document or section"))),
|
||||
};
|
||||
// Set `from`'s fields on `to`; `clear` instead unsets the fields `from` sets.
|
||||
let merge = |to: &mut NoteProps, from: &NoteProps, clear: bool| {
|
||||
if from.pos.is_some() {
|
||||
to.pos = if clear { None } else { from.pos };
|
||||
}
|
||||
if from.num_fmt.is_some() {
|
||||
to.num_fmt = if clear { None } else { from.num_fmt };
|
||||
}
|
||||
if from.num_start.is_some() {
|
||||
to.num_start = if clear { None } else { from.num_start };
|
||||
}
|
||||
if from.num_restart.is_some() {
|
||||
to.num_restart = if clear { None } else { from.num_restart };
|
||||
}
|
||||
};
|
||||
fn pick(sect: &mut wordcraft_doc::SectionProps, endnote: bool) -> &mut NoteProps {
|
||||
if endnote { &mut sect.endnote_pr } else { &mut sect.footnote_pr }
|
||||
}
|
||||
if change.is_empty() {
|
||||
// Nothing to change: the options in effect.
|
||||
} else if whole {
|
||||
let st = &mut s.doc.settings;
|
||||
if let Some(f) = change.num_fmt {
|
||||
*(if endnote { &mut st.endnote_format } else { &mut st.footnote_format }) = f;
|
||||
}
|
||||
merge(if endnote { &mut st.endnote_pr } else { &mut st.footnote_pr }, &NoteProps { num_fmt: None, ..change }, false);
|
||||
// Every section follows the document now.
|
||||
let ends: Vec<usize> = s.doc.sections().iter().map(|(e, _)| *e).collect();
|
||||
for e in ends {
|
||||
merge(pick(s.doc.section_mut(e), endnote), &change, true);
|
||||
}
|
||||
} else {
|
||||
super::page::with_sect(s, |sect| merge(pick(sect, endnote), &change, false))?;
|
||||
}
|
||||
if let Some(f) = old_foot {
|
||||
s.doc.settings.footnote_format = f;
|
||||
}
|
||||
if let Some(f) = old_end {
|
||||
s.doc.settings.endnote_format = f;
|
||||
}
|
||||
let sect = super::page::sect(s);
|
||||
Ok(json!({
|
||||
"footnote": s.doc.note_options(§, false),
|
||||
"endnote": s.doc.note_options(§, true),
|
||||
"footnoteFormat": s.doc.settings.footnote_format,
|
||||
"endnoteFormat": s.doc.settings.endnote_format,
|
||||
}))
|
||||
}
|
||||
|
||||
fn note_refs(s: &Session) -> Vec<Pos> {
|
||||
let mut v = Vec::new();
|
||||
for path in s.doc.para_paths(StoryRef::Body) {
|
||||
|
||||
Reference in New Issue
Block a user