From 2656c76fe8bf2967c2f996727b98adfb17ae9149 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Fri, 13 Nov 2026 06:16:43 -0500 Subject: [PATCH] Preserve structural story processing instructions --- crates/idml/src/model/story.rs | 36 ++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/crates/idml/src/model/story.rs b/crates/idml/src/model/story.rs index 7ea28b51e..19574a382 100644 --- a/crates/idml/src/model/story.rs +++ b/crates/idml/src/model/story.rs @@ -44,6 +44,12 @@ pub enum TextNode { Content(Vec), /// `Br`: the end of a paragraph. Break, + /// A processing instruction at a structural story position. The parent serializer preserves + /// it in place; `to_el` represents it as a one-instruction `Content` fragment. + ProcessingInstruction { + target: String, + data: String, + }, /// Text inside another element ([`CONTAINERS`]): a hyperlink's source, a tracked change, an /// XML element, a cross-reference's source, a footnote, a note. Container(Container), @@ -354,11 +360,18 @@ pub struct CellOptions { /// The children of `el` other than ``, as text nodes. fn nodes(el: &El) -> Vec { - el.elements().filter(|e| !e.is(Local("Properties"))).map(TextNode::from_el).collect() + el.children + .iter() + .filter_map(|node| match node { + Node::El(e) if !e.is(Local("Properties")) => Some(TextNode::from_el(e)), + Node::Pi { target, data } => Some(TextNode::ProcessingInstruction { target: target.clone(), data: data.clone() }), + _ => None, + }) + .collect() } fn with_children(mut el: El, children: &[TextNode]) -> El { - el.children.extend(children.iter().map(|c| Node::El(c.to_el()))); + el.children.extend(children.iter().map(TextNode::to_node)); el } @@ -397,6 +410,11 @@ impl TextNode { c } TextNode::Break => El::new("Br"), + TextNode::ProcessingInstruction { target, data } => { + let mut content = El::new("Content"); + content.children.push(Node::Pi { target: target.clone(), data: data.clone() }); + content + } TextNode::Container(c) => with_children(c.values.element(&c.name), &c.children), TextNode::Table(t) => t.to_el(), TextNode::Anchor(a) => a.values.element(&a.name), @@ -407,6 +425,15 @@ impl TextNode { } } + /// Serializes this node in its parent position, preserving processing instructions as XML + /// instructions rather than wrapping them in an element. + pub fn to_node(&self) -> Node { + match self { + TextNode::ProcessingInstruction { target, data } => Node::Pi { target: target.clone(), data: data.clone() }, + _ => Node::El(self.to_el()), + } + } + /// The nodes this one holds; none for content, breaks, page items and others. pub fn children(&self) -> &[TextNode] { match self { @@ -525,6 +552,11 @@ fn text(nodes: &[TextNode], ace: &dyn Fn(Ace) -> Option) -> String { } } TextNode::Break => out.push('\n'), + TextNode::ProcessingInstruction { target, data } if target == "ACE" => { + if let Ok(code) = data.trim().parse::() { + out.extend(ace(Ace::from_code(code))); + } + } TextNode::Paragraph(r) | TextNode::Character(r) => stack.extend(r.children.iter().rev()), TextNode::Container(c) if inline_container(&c.name) => stack.extend(c.children.iter().rev()), _ => {}