Files
craft-libs/crates/idml/tests/icml.rs
Dylan M. Taylorandljkx c6ded901fb craft-idml: story model and ICML (InCopy Markup) (#1008)
* craft-idml: a typed model of the layout (spreads, pages, guides, page items, graphics, geometry)

The first step of the typed IDML model. Spreads and parent spreads, pages, guides, page items
(nested), placed graphics and their links are typed; each keeps its attributes and Properties
as written and reads them through accessors, path geometry among them. Elements the model
does not type are kept where they stood, so a node read and written again reads the same.

* craft-idml: the story model (ranges, content, tables, notes, anchored items)

Stories as IDML writes them: paragraph and character ranges, Content with the special
characters InDesign writes as <?ACE n?>, breaks, the containers text sits in (hyperlink
sources, tracked changes, XML elements, cross-reference sources, footnotes, notes), tables with
their cells, and page items anchored in the text. Each node keeps its values as written; text
is gathered without recursion.

* craft-idml: read and write ICML (InCopy Markup)

The format layer of storytold/designcraft#2248: an ICML file told by its InCopyInterchange
instruction, read within the size of one package part, its references to package parts left
out with a warning, a file without a story refused, and written with the two instructions that
mark it. Choosing the story to place and mapping it stay with the application.

---------

Co-authored-by: ljkx <177671075+ljkx@users.noreply.github.com>
2026-11-11 20:29:48 -05:00

53 lines
3.0 KiB
Rust

//! ICML: one story and what it uses, as one XML document.
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use craft_idml::icml::{MAX_ICML_BYTES, icml_xml, is_icml, read_icml};
use craft_idml::{El, Error};
const ICML: &str = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<?aid style="50" type="snippet" readerVersion="6.0" featureSet="513" product="16.0(1)" ?>
<?aid SnippetType="InCopyInterchange"?>
<Document DOMVersion="16.0" Self="d">
<Color Self="Color/Black" Model="Process" Space="CMYK" ColorValue="0 0 0 100" />
<RootParagraphStyleGroup Self="u1"><ParagraphStyle Self="ParagraphStyle/Body" Name="Body" /></RootParagraphStyleGroup>
<idPkg:Graphic src="Resources/Graphic.xml" />
<Story Self="us1"><ParagraphStyleRange AppliedParagraphStyle="ParagraphStyle/Body"><CharacterStyleRange><Content>Hello</Content></CharacterStyleRange></ParagraphStyleRange></Story>
<Story Self="us2"><ParagraphStyleRange><CharacterStyleRange><Content>Anchored</Content></CharacterStyleRange></ParagraphStyleRange></Story>
<Hyperlink Self="uh1" Source="uh9" />
</Document>
"#;
#[test]
fn an_icml_file_reads_as_its_resources_and_stories() {
assert!(is_icml(ICML.as_bytes()));
assert!(is_icml(format!("\u{FEFF} {ICML}").as_bytes()));
assert!(!is_icml(b"<Document/>"));
assert!(!is_icml(b"SnippetType=\"InCopyInterchange\""));
let icml = read_icml(ICML.as_bytes()).unwrap();
assert_eq!(icml.top.iter().map(|e| e.name.as_str()).collect::<Vec<_>>(), ["Color", "RootParagraphStyleGroup", "Story", "Story", "Hyperlink"]);
assert_eq!(icml.warnings, ["Resources/Graphic.xml: an ICML file has no package parts; it was not read"]);
let stories = icml.stories();
assert_eq!(stories.iter().map(|s| s.text(|_| None)).collect::<Vec<_>>(), ["Hello", "Anchored"]);
}
#[test]
fn an_icml_file_written_reads_back() {
let icml = read_icml(ICML.as_bytes()).unwrap();
let mut root = El::new("Document").with_attr("DOMVersion", craft_idml::DOM_VERSION).with_attr("Self", "d");
root.children.extend(icml.top.iter().cloned().map(craft_idml::Node::El));
let bytes = icml_xml(&root);
let text = String::from_utf8(bytes.clone()).unwrap();
assert!(text.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n<?aid style=\"50\" type=\"snippet\" readerVersion=\"6.0\" featureSet=\"513\" product=\"16.0(1)\" ?>\n<?aid SnippetType=\"InCopyInterchange\"?>\n<Document "), "{text}");
assert!(is_icml(&bytes));
assert_eq!(read_icml(&bytes).unwrap().stories(), icml.stories());
}
#[test]
fn what_is_no_icml_story_is_refused() {
assert!(matches!(read_icml(b"<Workbook/>"), Err(Error::NotIdml(m)) if m.contains("<Workbook>")));
assert!(matches!(read_icml(b"<Document><Color/></Document>"), Err(Error::NotIdml(m)) if m.contains("no story")));
assert!(matches!(read_icml(b"<Document><Story>"), Err(Error::Part { .. })), "cut off");
let huge = vec![b' '; MAX_ICML_BYTES + 1];
assert_eq!(read_icml(&huge), Err(Error::TooLarge { part: "ICML".into(), limit: MAX_ICML_BYTES as u64 }));
}