mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-31 21:56:50 -06:00
Cross-references keep edited text and break at spaces
InDesign keeps text typed over a cross-reference's generated text (until the cross-reference is updated), with its page number still live. The IDML importer dropped that text and generated it again from the format, so a credits list whose format reads "Last, First. ... YYYY. Volume X, issue Y." showed those placeholders instead of the names and dates typed in. - CrossRef gains `text` (edited text, `<pageNum />` for the live page) and `page` (the page last shown). Import keeps saved text that no longer reads as its format produces it; export writes it back with InDesign's XRefPageNumber variable. Setting a format drops the edits, as InDesign does. - Edited text whose destination is gone shows its saved page, not "??". - Spaces in a cross-reference's text are word spaces the line may break at (it was set as one unbreakable piece, so long ones broke mid-word). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
This commit is contained in:
@@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
Newest first.
|
||||
|
||||
- **2026-10-17** · Cross-references keep text typed over their generated text (IDML import and export), with the page number still live, as InDesign shows them until updated; a cross-reference whose destination is gone shows its saved text and page instead of "??"; and a cross-reference's text breaks at its spaces instead of inside words. No readiness percentage change.
|
||||
- **2026-10-16** · Threading: the loaded text cursor threads into a text frame inside a group (it hit the group and showed it could not thread); a plain click still selects the group (#1075). Gap 27 closed. No readiness percentage change.
|
||||
- **2026-10-16** · Greek text hyphenates. The patterns were never compiled in, so Greek set unhyphenated however narrow the column; `"Greek"`, `"Greek: Greece"`, `el` and `el-GR` all resolve now and ανθρωπος breaks αν-θρω-πος. They are the monotonic patterns (hyph-el-monoton, Dimitrios Filippou, under MIT), which is what polytonic text hyphenates by too. Five of InDesign's ~40 hyphenation languages. No readiness percentage change.
|
||||
- **2026-10-16** · A swatch applied to a placed greyscale or bitmap image colours it, as in InDesign: IDML's `<Image FillColor>` is read and written, greyscale pixels become a tint of the swatch on opaque white and a bitmap's white lets the frame show through, on screen and in PDF (spot swatches print on their plate). Colour images keep their colours. Applying the swatch from the Swatches panel comes next. No readiness percentage change.
|
||||
|
||||
@@ -1175,7 +1175,12 @@ fn shape_segment(
|
||||
for (gi, sg) in shaped.iter().enumerate() {
|
||||
let up = upright.get(gi).copied().unwrap_or(false);
|
||||
let (byte, len, ch) = if replacement.is_some() {
|
||||
(range.start, if gi == 0 { range.len() } else { 0 }, text[range.start..].chars().next().unwrap_or(' '))
|
||||
let marker = text[range.start..].chars().next().unwrap_or(' ');
|
||||
// A cross-reference's text is real text in InDesign: its spaces are word spaces the
|
||||
// line may break at (text variables and page numbers stay whole). The first glyph
|
||||
// keeps the marker, as it owns the marker's bytes.
|
||||
let space = gi > 0 && marker == designcraft_doc::XREF_MARK && src.get(sg.cluster..).is_some_and(|t| t.starts_with(' '));
|
||||
(range.start, if gi == 0 { range.len() } else { 0 }, if space { ' ' } else { marker })
|
||||
} else {
|
||||
let cl = range.start + sg.cluster;
|
||||
let end = shaped[gi + 1..].iter().map(|r| range.start + r.cluster).find(|&c| c > cl).unwrap_or(range.end);
|
||||
|
||||
@@ -7242,3 +7242,34 @@ fn greek_text_gets_greek_hyphenation() {
|
||||
let el = hyphen::Lang::for_language("Greek").unwrap();
|
||||
assert_eq!(hyphen::hyphen_points_in("ανθρωπος", &hyphen::Limits::default(), el), vec![2, 5]);
|
||||
}
|
||||
|
||||
/// A cross-reference's text breaks at its spaces like ordinary text (it is real text in
|
||||
/// InDesign), not inside words; edited text whose destination is gone shows its saved page.
|
||||
#[test]
|
||||
fn cross_reference_text_breaks_at_its_spaces() {
|
||||
let (mut d, sid, _) = doc_with("See X.", Rect::new(36.0, 36.0, 110.0, 400.0), ParaAttrs::default());
|
||||
let at = d.story(sid).unwrap().text.find('X').unwrap();
|
||||
let st = d.story_mut(sid).unwrap();
|
||||
st.delete(at..at + 1);
|
||||
st.insert_xref(
|
||||
at,
|
||||
designcraft_doc::CrossRef {
|
||||
text: Some("Page <pageNum />: Bove, Vinny. \"Advent Horizon.\" 1996. Volume 6, issue 5.".into()),
|
||||
page: Some("7".into()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
let x = d.story(sid).unwrap().xrefs[0].clone();
|
||||
let shown = crate::xref::xref_text(&d, &x, None);
|
||||
assert_eq!(shown, "Page 7: Bove, Vinny. \"Advent Horizon.\" 1996. Volume 6, issue 5.");
|
||||
let cs = compose_story(&d, sid, &ComposeOptions::default());
|
||||
let lines: Vec<String> =
|
||||
cs.frames[0].lines.iter().map(|l| l.glyphs.iter().filter(|g| g.visible).map(|g| g.rendered_char).collect::<String>()).collect();
|
||||
assert!(lines.len() > 3, "{lines:?}");
|
||||
let words: Vec<&str> = format!("See {shown}.").leak().split(' ').collect();
|
||||
for l in &lines {
|
||||
for w in l.split(' ').filter(|w| !w.is_empty()) {
|
||||
assert!(words.contains(&w), "a line breaks inside a word: {w:?} in {lines:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,14 @@ impl XrefIndex {
|
||||
}
|
||||
|
||||
/// The text a cross-reference shows ("??" when its destination is gone, like an unresolved
|
||||
/// reference in typesetting).
|
||||
/// reference in typesetting, unless the text was edited).
|
||||
pub fn xref_text(doc: &Document, x: &xref::CrossRef, index: Option<&XrefIndex>) -> String {
|
||||
// Edited text stays as typed; its page numbers still follow the destination, or show the
|
||||
// page last shown when the destination is gone (as in InDesign).
|
||||
if let Some(t) = &x.text {
|
||||
let live = index.filter(|_| doc.xref_values(x.target).is_some()).and_then(|i| i.page(x.target));
|
||||
return xref::expand_edited(t, live.or(x.page.as_deref()).unwrap_or("?"));
|
||||
}
|
||||
let Some(mut v) = doc.xref_values(x.target) else { return "??".into() };
|
||||
v.page = index.and_then(|i| i.page(x.target)).unwrap_or("?").to_string();
|
||||
let def = doc.xref_format(&x.format).map_or("<fullPara />", |f| f.definition.as_str());
|
||||
|
||||
+61
-4
@@ -29,13 +29,58 @@ pub struct TextAnchor {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CrossRef {
|
||||
/// The destination anchor (0 = unresolved, e.g. pasted without its destination).
|
||||
pub target: u64,
|
||||
/// Name of the format in [`Document::xref_formats`].
|
||||
pub format: String,
|
||||
/// Text typed over the generated text (InDesign keeps a cross-reference's edited text until
|
||||
/// it is updated): shown as is, except that each `<pageNum />` shows the destination's page.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub text: Option<String>,
|
||||
/// The page last shown (for edited text whose destination is gone).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub page: Option<String>,
|
||||
}
|
||||
|
||||
/// The `<pageNum />` building block, as it appears in [`CrossRef::text`].
|
||||
pub const PAGE_NUM_BLOCK: &str = "<pageNum />";
|
||||
|
||||
/// Edited cross-reference text with its page numbers filled in (quotes kept as typed).
|
||||
pub fn expand_edited(text: &str, page: &str) -> String {
|
||||
text.replace(PAGE_NUM_BLOCK, page)
|
||||
}
|
||||
|
||||
/// The literal text between a format's building blocks, in order.
|
||||
pub fn literal_parts(definition: &str) -> Vec<&str> {
|
||||
let mut out = Vec::new();
|
||||
let mut rest = definition;
|
||||
while let Some(i) = rest.find('<') {
|
||||
let Some(j) = rest[i..].find('>') else { break };
|
||||
if i > 0 {
|
||||
out.push(&rest[..i]);
|
||||
}
|
||||
rest = &rest[i + j + 1..];
|
||||
}
|
||||
if !rest.is_empty() {
|
||||
out.push(rest);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Does `text` (a cross-reference's text as saved) still read as its format would generate it:
|
||||
/// every literal part of the definition, in order? Text that doesn't was edited by hand.
|
||||
pub fn matches_format(text: &str, definition: &str) -> bool {
|
||||
let mut rest = text;
|
||||
for part in literal_parts(definition) {
|
||||
match rest.find(part) {
|
||||
Some(i) => rest = &rest[i + part.len()..],
|
||||
None => return false,
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// A cross-reference format: building blocks between literal text.
|
||||
@@ -198,7 +243,7 @@ impl Story {
|
||||
if n > 0 {
|
||||
let k = self.marks_before(XREF_MARK, pos);
|
||||
for i in 0..n {
|
||||
self.xrefs.insert(k + i, Arc::new(CrossRef { target: 0, format: String::new() }));
|
||||
self.xrefs.insert(k + i, Arc::new(CrossRef::default()));
|
||||
}
|
||||
}
|
||||
let n = text.matches(crate::index::INDEX_MARK).count();
|
||||
@@ -268,7 +313,7 @@ impl Story {
|
||||
let n = self.text.matches(XREF_MARK).count();
|
||||
self.xrefs.truncate(n);
|
||||
while self.xrefs.len() < n {
|
||||
self.xrefs.push(Arc::new(CrossRef { target: 0, format: String::new() }));
|
||||
self.xrefs.push(Arc::new(CrossRef::default()));
|
||||
}
|
||||
let n = self.text.matches(crate::index::INDEX_MARK).count();
|
||||
self.index_refs.truncate(n);
|
||||
@@ -388,6 +433,18 @@ pub fn clean(s: &str) -> String {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Saved text that still reads as its format produced it is generated again; text typed
|
||||
/// over it is kept.
|
||||
#[test]
|
||||
fn edited_text_is_recognised() {
|
||||
let def = "Page <pageNum />: Last, First. \"<txtAnchrName />.\" YYYY. Volume X, issue Y.";
|
||||
assert_eq!(literal_parts(def), ["Page ", ": Last, First. \"", ".\" YYYY. Volume X, issue Y."]);
|
||||
assert!(matches_format("Page <pageNum />: Last, First. \"Advent Horizon.\" YYYY. Volume X, issue Y.", def));
|
||||
assert!(!matches_format("Page <pageNum />: Bove, Vinny. \"Advent Horizon.\" 1996. Volume 6, issue 5.", def));
|
||||
assert!(matches_format("anything", "<paraText />"));
|
||||
assert_eq!(expand_edited("p. <pageNum /> and <pageNum />", "4"), "p. 4 and 4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_expand() {
|
||||
let v =
|
||||
@@ -407,7 +464,7 @@ mod tests {
|
||||
let mut s = Story::with_text(StoryId(3), "Heading\nBody text.", Default::default());
|
||||
let a = s.insert_anchor(0, "Heading");
|
||||
assert_eq!(a >> 24, 3);
|
||||
s.insert_xref(13, CrossRef { target: a, format: "Page Number".into() });
|
||||
s.insert_xref(13, CrossRef { target: a, format: "Page Number".into(), ..Default::default() });
|
||||
s.check().unwrap();
|
||||
assert_eq!(s.anchor_pos(a), Some(0));
|
||||
s.insert(0, "New ");
|
||||
|
||||
@@ -94,7 +94,7 @@ fn insert(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let r = t2.range();
|
||||
let r = r.start.min(st.len())..r.end.min(st.len());
|
||||
st.delete(r.clone());
|
||||
st.insert_xref(r.start, CrossRef { target, format });
|
||||
st.insert_xref(r.start, CrossRef { target, format, ..Default::default() });
|
||||
let pos = r.start + designcraft_doc::XREF_MARK.len_utf8();
|
||||
sel.text = Some(TextSel { anchor: pos, focus: pos, ..t2 });
|
||||
Ok(json!({"target": target, "pos": r.start}))
|
||||
@@ -161,7 +161,10 @@ fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let st = d.story_mut(sid).ok_or(designcraft_doc::DocError::NoStory(sid))?;
|
||||
let x = st.xrefs.get_mut(k).ok_or_else(|| bad("xref.setFormat", "no such cross-reference"))?;
|
||||
std::sync::Arc::make_mut(x).format = format;
|
||||
let x = std::sync::Arc::make_mut(x);
|
||||
x.format = format;
|
||||
// A new format generates the text again, replacing any edits (InDesign does the same).
|
||||
x.text = None;
|
||||
st.rev = st.rev.wrapping_add(1);
|
||||
Ok(Value::Null)
|
||||
})
|
||||
|
||||
@@ -2478,9 +2478,34 @@ impl<'a> Ex<'a> {
|
||||
designcraft_doc::xref::expand(self.d.xref_format(&x.format).map_or("<fullPara />", |ff| ff.definition.as_str()), &v)
|
||||
});
|
||||
let mut c = self.csr_el(&f);
|
||||
let mut content = El::new("Content");
|
||||
content.children.push(Node::Text(text));
|
||||
c.push(content);
|
||||
match &x.text {
|
||||
// Edited text as typed, its page numbers as InDesign's live
|
||||
// cross-reference page number.
|
||||
Some(t) => {
|
||||
let page = x.page.clone().unwrap_or_else(|| "?".into());
|
||||
for (n, part) in t.split(designcraft_doc::xref::PAGE_NUM_BLOCK).enumerate() {
|
||||
if n > 0 {
|
||||
c.push(
|
||||
El::new("TextVariableInstance")
|
||||
.attr("Self", self.fresh())
|
||||
.attr("Name", "<?AID 001b?>TV XRefPageNumber")
|
||||
.attr("ResultText", &page)
|
||||
.attr("AssociatedTextVariable", "dTextVariablen<?AID 001b?>TV XRefPageNumber"),
|
||||
);
|
||||
}
|
||||
if !part.is_empty() {
|
||||
let mut content = El::new("Content");
|
||||
content.children.push(Node::Text(part.to_string()));
|
||||
c.push(content);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let mut content = El::new("Content");
|
||||
content.children.push(Node::Text(text));
|
||||
c.push(content);
|
||||
}
|
||||
}
|
||||
let mut src = El::new("CrossReferenceSource")
|
||||
.attr("Self", &me)
|
||||
.attr("AppliedFormat", fmt)
|
||||
|
||||
@@ -2079,10 +2079,19 @@ impl<'r> Importer<'r> {
|
||||
anchor_list.push(Arc::new(designcraft_doc::TextAnchor { id: aid, name }));
|
||||
}
|
||||
let mut xref_list = Vec::new();
|
||||
for (k, (me, fmt)) in xrefs.into_iter().enumerate() {
|
||||
self.xref_srcs.insert(me, (id, k));
|
||||
let format = self.xref_format_names.get(&fmt).cloned().unwrap_or_else(|| "Full Paragraph & Page Number".into());
|
||||
xref_list.push(Arc::new(designcraft_doc::CrossRef { target: 0, format }));
|
||||
for (k, x) in xrefs.into_iter().enumerate() {
|
||||
self.xref_srcs.insert(x.me, (id, k));
|
||||
let format = self.xref_format_names.get(&x.format).cloned().unwrap_or_else(|| "Full Paragraph & Page Number".into());
|
||||
// Text typed over the generated text is kept (InDesign shows it until the
|
||||
// cross-reference is updated); text the format still produces is generated again.
|
||||
let definition = self.xref_formats.iter().find(|f| f.name == format).map_or("<fullPara />", |f| f.definition.as_str());
|
||||
let edited = !x.text.is_empty() && !designcraft_doc::xref::matches_format(&x.text, definition);
|
||||
xref_list.push(Arc::new(designcraft_doc::CrossRef {
|
||||
target: 0,
|
||||
format,
|
||||
text: edited.then_some(x.text),
|
||||
page: x.page.filter(|_| edited),
|
||||
}));
|
||||
}
|
||||
let mut st = Story {
|
||||
id,
|
||||
@@ -2331,7 +2340,14 @@ impl<'r> Importer<'r> {
|
||||
}
|
||||
"CrossReferenceSource" => {
|
||||
// The generated text is regenerated from the format.
|
||||
b.xrefs.push((c.get("Self").unwrap_or("").to_string(), c.get("AppliedFormat").unwrap_or("").to_string()));
|
||||
let (mut text, mut page) = (String::new(), None);
|
||||
xref_saved_text(c, &mut text, &mut page, 0);
|
||||
b.xrefs.push(XrefSource {
|
||||
me: c.get("Self").unwrap_or("").to_string(),
|
||||
format: c.get("AppliedFormat").unwrap_or("").to_string(),
|
||||
text,
|
||||
page,
|
||||
});
|
||||
// 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, cf));
|
||||
@@ -3377,10 +3393,41 @@ struct StoryBuilder {
|
||||
/// Text destinations (Self, name) and cross-reference sources (Self, format Self) in order.
|
||||
anchors: Vec<(String, String)>,
|
||||
index_refs: Vec<designcraft_doc::IndexRef>,
|
||||
xrefs: Vec<(String, String)>,
|
||||
xrefs: Vec<XrefSource>,
|
||||
objects: Vec<designcraft_doc::AnchoredObject>,
|
||||
}
|
||||
|
||||
/// A cross-reference source as saved: its id, its format's id, and its text with each page number
|
||||
/// as `<pageNum />` (and the page it last showed).
|
||||
struct XrefSource {
|
||||
me: String,
|
||||
format: String,
|
||||
text: String,
|
||||
page: Option<String>,
|
||||
}
|
||||
|
||||
/// The text a `CrossReferenceSource` holds, with its live page numbers (InDesign's
|
||||
/// `XRefPageNumber` text variable) as `<pageNum />`. Returns the text and the page shown.
|
||||
fn xref_saved_text(e: &El, text: &mut String, page: &mut Option<String>, depth: usize) {
|
||||
if depth > 16 {
|
||||
return;
|
||||
}
|
||||
for c in e.elements() {
|
||||
match c.local() {
|
||||
"Content" => text.push_str(&c.text_content()),
|
||||
"TextVariableInstance" if c.get("Name").is_some_and(|n| n.contains("XRefPageNumber")) => {
|
||||
text.push_str(designcraft_doc::xref::PAGE_NUM_BLOCK);
|
||||
if page.is_none() {
|
||||
*page = c.get("ResultText").map(str::to_string);
|
||||
}
|
||||
}
|
||||
"TextVariableInstance" => text.push_str(c.get("ResultText").unwrap_or("")),
|
||||
"CharacterStyleRange" => xref_saved_text(c, text, page, depth + 1),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl StoryBuilder {
|
||||
/// A table gets an anchor paragraph of its own.
|
||||
fn push_table(&mut self, t: Table, pf: &ParaFormat, cf: &CharFormat) {
|
||||
|
||||
@@ -1154,7 +1154,7 @@ fn round_trips_cross_references() {
|
||||
d.add_text_frame(SpreadRef::Doc(0), Rect::new(40.0, 40.0, 400.0, 400.0), lid, "Results: found\nSee .", ParaFormat::default()).unwrap();
|
||||
let target = d.paragraph_anchor(sid, 0).unwrap();
|
||||
let at = d.story(sid).unwrap().text.find(" .").unwrap() + 1;
|
||||
d.story_mut(sid).unwrap().insert_xref(at, designcraft_doc::CrossRef { target, format: "Paragraph Text".into() });
|
||||
d.story_mut(sid).unwrap().insert_xref(at, designcraft_doc::CrossRef { target, format: "Paragraph Text".into(), ..Default::default() });
|
||||
d.xref_formats.push(designcraft_doc::XrefFormat {
|
||||
name: "Mine".into(),
|
||||
definition: "(<partialPara delim=\":\" includeDelim=\"true\" />, p. <pageNum />)".into(),
|
||||
@@ -4189,3 +4189,31 @@ mod liquid_page_rules {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Text typed over a cross-reference's generated text is kept, its page number live (InDesign's
|
||||
/// XRefPageNumber variable); text the format still produces is generated again. The edits
|
||||
/// survive export and import.
|
||||
#[test]
|
||||
fn keeps_edited_cross_reference_text() {
|
||||
let story = 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"><ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/$ID/NormalParagraphStyle">
|
||||
<CrossReferenceSource Self="x1" AppliedFormat="n"><CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]"><Content>Page </Content></CharacterStyleRange><CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]"><TextVariableInstance Self="v1" Name="<?AID 001b?>TV XRefPageNumber" ResultText="7" AssociatedTextVariable="dTextVariablen<?AID 001b?>TV XRefPageNumber"/></CharacterStyleRange><CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]"><Content>: Bove, Vinny. "Advent Horizon." 1996.</Content></CharacterStyleRange></CrossReferenceSource>
|
||||
<CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]"><Content> and </Content></CharacterStyleRange>
|
||||
<CrossReferenceSource Self="x2" AppliedFormat="n"><CharacterStyleRange AppliedCharacterStyle="CharacterStyle/$ID/[No character style]"><Content>"Intro" on page </Content><TextVariableInstance Self="v2" Name="<?AID 001b?>TV XRefPageNumber" ResultText="3" AssociatedTextVariable="dTextVariablen<?AID 001b?>TV XRefPageNumber"/></CharacterStyleRange></CrossReferenceSource>
|
||||
</ParagraphStyleRange></Story></idPkg:Story>"#;
|
||||
let d = import_idml(&fixture_with_story(story)).unwrap();
|
||||
d.check().unwrap();
|
||||
let s = d.stories.values().find(|s| !s.xrefs.is_empty()).expect("story with cross-references");
|
||||
assert_eq!(s.xrefs.len(), 2);
|
||||
assert_eq!(s.xrefs[0].text.as_deref(), Some("Page <pageNum />: Bove, Vinny. \"Advent Horizon.\" 1996."));
|
||||
assert_eq!(s.xrefs[0].page.as_deref(), Some("7"));
|
||||
assert_eq!(s.xrefs[1].text, None, "unedited text follows its format");
|
||||
// Its destination is gone: the page it last showed.
|
||||
assert_eq!(designcraft_compose::xref::xref_text(&d, &s.xrefs[0], None), "Page 7: Bove, Vinny. \"Advent Horizon.\" 1996.");
|
||||
let back = import_idml(&export_idml(&d)).unwrap();
|
||||
back.check().unwrap();
|
||||
let b = back.stories.values().find(|s| !s.xrefs.is_empty()).unwrap();
|
||||
assert_eq!(b.xrefs[0].text, s.xrefs[0].text);
|
||||
assert_eq!(b.xrefs[0].page, s.xrefs[0].page);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user