mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-25 18:56:50 -06:00
Merge idml-deleted-text: IDML: tracked deleted text takes no room in the layout (#581)
Conflict: crates/idml/src/import.rs: the branch's `over.change` line moved into next's `range_format()`, which takes the enclosing format for it (its CrossReferenceSource caller passes it too); next's `TextVariableInstance` arm kept beside the branch's `Change` arm, with next's ignore list Conflict: crates/idml/src/export.rs: kept next's metadata, text-variable and CJK settings helpers and the branch's `change_type` Conflict: crates/idml/src/tests.rs: kept next's `$ID/` font test and the branch's tracked-change test
This commit is contained in:
@@ -740,6 +740,7 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
|
||||
};
|
||||
let para_ranges = story.para_ranges();
|
||||
let np = para_ranges.len();
|
||||
let deleted = deleted_paras(story, ¶_ranges);
|
||||
let hyph_exceptions = doc.hyphenation_exception_map();
|
||||
let mut list_counter = ListCounter::default();
|
||||
// Named lists number independently of layout: one pass up front, for the labels.
|
||||
@@ -749,9 +750,10 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
|
||||
.paras
|
||||
.iter()
|
||||
.zip(¶_ranges)
|
||||
.map(|(p, r)| {
|
||||
.zip(&deleted)
|
||||
.map(|((p, r), del)| {
|
||||
let (pp, _) = doc.styles.resolve_para(p);
|
||||
if pp.list_type != designcraft_doc::ListType::Numbers || pp.list_name.is_empty() || r.is_empty() {
|
||||
if pp.list_type != designcraft_doc::ListType::Numbers || pp.list_name.is_empty() || r.is_empty() || *del {
|
||||
return None;
|
||||
}
|
||||
let c = counters.entry(pp.list_name.clone()).or_insert_with(|| doc.list_start(story.id, &pp.list_name));
|
||||
@@ -809,6 +811,27 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C
|
||||
pi = j;
|
||||
continue 'paras;
|
||||
}
|
||||
if deleted.get(pi).copied().unwrap_or(false) {
|
||||
// A deleted paragraph takes no room; for keeps it stands in for the one before it.
|
||||
let filler = info.last().map_or(
|
||||
ParaInfo {
|
||||
start: (cur.fi, cur.col),
|
||||
at_top: cur.last_baseline.is_none(),
|
||||
end: (cur.fi, cur.col),
|
||||
lines: 0,
|
||||
lines_in_end_col: 0,
|
||||
keep_with_next: 0,
|
||||
keep_together: false,
|
||||
keep_all: false,
|
||||
keep_first: 1,
|
||||
keep_last: 1,
|
||||
},
|
||||
|p| ParaInfo { lines_in_end_col: 0, ..p.clone() },
|
||||
);
|
||||
info.push(filler);
|
||||
pi += 1;
|
||||
continue;
|
||||
}
|
||||
let pf = &story.paras[pi];
|
||||
let (mut pp, base_chars) = doc.styles.resolve_para(pf);
|
||||
// A break character that ends the previous paragraph sends this one on, like a start option.
|
||||
@@ -2108,6 +2131,33 @@ fn rewind(
|
||||
}
|
||||
}
|
||||
|
||||
/// Paragraphs that tracked changes delete: all their text is deleted, and so is their return or
|
||||
/// the return before them (then they join a paragraph and add nothing to it).
|
||||
fn deleted_paras(story: &Story, ranges: &[Range<usize>]) -> Vec<bool> {
|
||||
let mut spans: Vec<Range<usize>> = Vec::new();
|
||||
for (r, f) in story.runs() {
|
||||
if f.over.change != Some(designcraft_doc::ChangeMark::Deleted) {
|
||||
continue;
|
||||
}
|
||||
match spans.last_mut() {
|
||||
Some(l) if l.end == r.start => l.end = r.end,
|
||||
_ => spans.push(r),
|
||||
}
|
||||
}
|
||||
if spans.is_empty() {
|
||||
return vec![false; ranges.len()];
|
||||
}
|
||||
let covered = |r: Range<usize>| {
|
||||
r.is_empty() || spans.get(spans.partition_point(|s| s.end <= r.start)).is_some_and(|s| s.start <= r.start && s.end >= r.end)
|
||||
};
|
||||
let return_deleted = |r: &Range<usize>| r.end < story.text.len() && covered(r.end..r.end + 1);
|
||||
ranges
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, r)| covered(r.clone()) && (return_deleted(r) || i.checked_sub(1).and_then(|p| ranges.get(p)).is_some_and(return_deleted)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Where a placed paragraph sits (for keep resolution).
|
||||
#[derive(Clone, Debug)]
|
||||
struct ParaInfo {
|
||||
|
||||
@@ -2378,14 +2378,34 @@ impl<'a> Ex<'a> {
|
||||
flush_content(&mut pending, &mut out);
|
||||
let is_last_seg = k + 1 == nseg;
|
||||
// A break character that ends the paragraph was written as its `Br`.
|
||||
if is_last_seg && !last && !ends_with_break {
|
||||
out.push(Node::El(El::new("Br")));
|
||||
}
|
||||
if !out.is_empty() || csrs.is_empty() {
|
||||
let para_br = is_last_seg && !last && !ends_with_break;
|
||||
if !out.is_empty() || csrs.is_empty() || para_br {
|
||||
let mut c = self.csr_el(&f);
|
||||
c.children.extend(out);
|
||||
csrs.push(c);
|
||||
}
|
||||
// Tracked changes wrap the text they mark.
|
||||
if let Some(kind) = change_type(f.over.change) {
|
||||
for c in csrs.iter_mut().filter(|c| c.name == "CharacterStyleRange" && !c.children.is_empty()) {
|
||||
let mut ch = El::new("Change").attr("Self", self.fresh()).attr("ChangeType", kind);
|
||||
ch.children = std::mem::take(&mut c.children);
|
||||
c.push(ch);
|
||||
}
|
||||
}
|
||||
if para_br && let Some(c) = csrs.last_mut() {
|
||||
let nl = s.runs().find(|(rr, _)| rr.contains(&r.end)).and_then(|(_, nf)| change_type(nf.over.change));
|
||||
match nl {
|
||||
None => c.push(El::new("Br")),
|
||||
Some(kind) => match c.children.last_mut() {
|
||||
Some(Node::El(ch)) if ch.name == "Change" && ch.get("ChangeType") == Some(kind) => ch.push(El::new("Br")),
|
||||
_ => {
|
||||
let mut ch = El::new("Change").attr("Self", self.fresh()).attr("ChangeType", kind);
|
||||
ch.push(El::new("Br"));
|
||||
c.push(ch);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
for c in csrs {
|
||||
psr.push(c);
|
||||
}
|
||||
@@ -3115,3 +3135,12 @@ impl Ex<'_> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The IDML `ChangeType` of a tracked change.
|
||||
fn change_type(mark: Option<designcraft_doc::ChangeMark>) -> Option<&'static str> {
|
||||
match mark {
|
||||
Some(designcraft_doc::ChangeMark::Deleted) => Some("DeletedText"),
|
||||
Some(designcraft_doc::ChangeMark::Inserted) => Some("InsertedText"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2022,11 +2022,12 @@ impl<'r> Importer<'r> {
|
||||
}
|
||||
|
||||
/// The format of the text in a `CharacterStyleRange`: its style, and its local attributes over
|
||||
/// the enclosing paragraph range's.
|
||||
fn range_format(&mut self, c: &El, pchars: &CharAttrs) -> CharFormat {
|
||||
/// the enclosing paragraph range's, inside a tracked change if `outer` (the enclosing format) is.
|
||||
fn range_format(&mut self, c: &El, pchars: &CharAttrs, outer: &CharFormat) -> CharFormat {
|
||||
let style = c.get("AppliedCharacterStyle").map(|r| self.char_style_ref(r)).unwrap_or_else(|| st::NO_CHAR_STYLE.into());
|
||||
let mut over = pchars.clone();
|
||||
over.merge(&self.char_attrs(c));
|
||||
over.change = over.change.or(outer.over.change);
|
||||
CharFormat { style, over }
|
||||
}
|
||||
|
||||
@@ -2050,7 +2051,7 @@ impl<'r> Importer<'r> {
|
||||
self.walk_story(c, b, &npf, &chars, &pcf, None);
|
||||
}
|
||||
"CharacterStyleRange" => {
|
||||
let ncf = self.range_format(c, pchars);
|
||||
let ncf = self.range_format(c, pchars, cf);
|
||||
let bt = c.get("ParagraphBreakType").filter(|t| *t != "Anywhere");
|
||||
b.last = ncf.clone();
|
||||
self.walk_story(c, b, pf, pchars, &ncf, bt);
|
||||
@@ -2116,7 +2117,7 @@ impl<'r> Importer<'r> {
|
||||
b.xrefs.push((c.get("Self").unwrap_or("").to_string(), c.get("AppliedFormat").unwrap_or("").to_string()));
|
||||
// A source at paragraph level wraps a range; inside a range it holds the
|
||||
// text directly and takes that range's format.
|
||||
let f = c.find("CharacterStyleRange").map_or_else(|| cf.clone(), |r| self.range_format(r, pchars));
|
||||
let f = c.find("CharacterStyleRange").map_or_else(|| cf.clone(), |r| self.range_format(r, pchars, cf));
|
||||
b.push(&designcraft_doc::XREF_MARK.to_string(), &f);
|
||||
}
|
||||
"Footnote" => {
|
||||
@@ -2195,6 +2196,17 @@ impl<'r> Importer<'r> {
|
||||
let t = self.text_variable_instance(c);
|
||||
b.push(&t, cf);
|
||||
}
|
||||
"Change" => {
|
||||
// Tracked changes: deleted text stays in the story, marked, and takes no
|
||||
// room in the layout.
|
||||
let mut ncf = cf.clone();
|
||||
match c.get("ChangeType") {
|
||||
Some("DeletedText") => ncf.over.change = Some(designcraft_doc::ChangeMark::Deleted),
|
||||
Some("InsertedText" | "MovedText") => ncf.over.change = Some(designcraft_doc::ChangeMark::Inserted),
|
||||
_ => {}
|
||||
}
|
||||
self.walk_story(c, b, pf, pchars, &ncf, brk);
|
||||
}
|
||||
"Properties" | "Note" | "StoryPreference" | "InCopyExportOption" => {}
|
||||
_ => self.walk_story(c, b, pf, pchars, cf, brk),
|
||||
},
|
||||
|
||||
@@ -3173,3 +3173,55 @@ fn applied_font_keeps_spaces_and_font_style_nothing_inherits() {
|
||||
check(&d);
|
||||
check(&import_idml(&export_idml(&d)).unwrap());
|
||||
}
|
||||
|
||||
fn tracked_story(body: &str) -> String {
|
||||
format!(
|
||||
r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<idPkg:Story xmlns:idPkg="http://ns.adobe.com/AdobeInDesign/idml/1.0/packaging" DOMVersion="16.0">
|
||||
<Story Self="s1">{body}</Story>
|
||||
</idPkg:Story>"#
|
||||
)
|
||||
}
|
||||
|
||||
const CSR: &str = r#"<CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]">"#;
|
||||
|
||||
#[test]
|
||||
fn tracked_deleted_text_takes_no_room_and_round_trips() {
|
||||
let psr = r#"<ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/$ID/NormalParagraphStyle">"#;
|
||||
let kwn = r#"<ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/$ID/NormalParagraphStyle" KeepWithNext="2">"#;
|
||||
let tracked = tracked_story(&format!(
|
||||
r#"{psr}{CSR}<Content>The first paragraph keeps a </Content><Change Self="c1" ChangeType="DeletedText"><Content>deleted </Content></Change><Content>word.</Content><Br/></CharacterStyleRange></ParagraphStyleRange>
|
||||
{kwn}{CSR}<Change Self="c2" ChangeType="DeletedText"><Br/></Change></CharacterStyleRange></ParagraphStyleRange>
|
||||
{psr}{CSR}<Change Self="c3" ChangeType="InsertedText"><Content>New </Content></Change><Content>second paragraph.</Content></CharacterStyleRange></ParagraphStyleRange>"#
|
||||
));
|
||||
let plain = tracked_story(&format!(
|
||||
r#"{psr}{CSR}<Content>The first paragraph keeps a word.</Content><Br/></CharacterStyleRange></ParagraphStyleRange>
|
||||
{psr}{CSR}<Content>New second paragraph.</Content></CharacterStyleRange></ParagraphStyleRange>"#
|
||||
));
|
||||
let lines = |d: &Document| {
|
||||
let sid = *d.stories.keys().next().unwrap();
|
||||
let cs = designcraft_compose::compose_story(d, sid, &Default::default());
|
||||
cs.frames.iter().flat_map(|f| f.lines.iter().map(|l| (l.baseline, l.x0, l.end_x))).collect::<Vec<_>>()
|
||||
};
|
||||
let d = import_idml_with(&fixture_with_story(&tracked), &|_| None).unwrap();
|
||||
let story = d.stories.values().next().unwrap();
|
||||
assert_eq!(story.text, "The first paragraph keeps a deleted word.\n\nNew second paragraph.");
|
||||
let marks = |s: &designcraft_doc::Story| s.runs().map(|(r, f)| (s.text[r].to_string(), f.over.change)).collect::<Vec<_>>();
|
||||
use designcraft_doc::ChangeMark::{Deleted, Inserted};
|
||||
let want = vec![
|
||||
("The first paragraph keeps a ".to_string(), None),
|
||||
("deleted ".to_string(), Some(Deleted)),
|
||||
("word.\n".to_string(), None),
|
||||
("\n".to_string(), Some(Deleted)),
|
||||
("New ".to_string(), Some(Inserted)),
|
||||
("second paragraph.".to_string(), None),
|
||||
];
|
||||
assert_eq!(marks(story), want);
|
||||
let reference = import_idml_with(&fixture_with_story(&plain), &|_| None).unwrap();
|
||||
let expected = lines(&reference);
|
||||
assert!(expected.len() >= 2);
|
||||
assert_eq!(lines(&d), expected);
|
||||
// Export writes the changes back as `Change` elements.
|
||||
let back = import_idml(&export_idml(&d)).unwrap();
|
||||
assert_eq!(marks(back.stories.values().next().unwrap()), want);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user