Fonto Why and How: How can I prevent nodes from being ‘cut’?!

Fonto Why and How: How can I prevent nodes from being ‘cut’?!

In this weekly series, Martin describes a question that was raised by a Fonto developer, how it was resolved and why Fonto behaved like that in the first place. This week, a partner reached out. Their authors are ‘cutting’ referenced nodes and pasting them in different documents! How can Fonto prevent these documents from breaking?!

How can I prevent nodes from being ‘cut’?! A partner reached out. Their authors are ‘cutting’ referenced nodes and pasting them in different documents! How can Fonto prevent these documents from breaking?! A support issue came in:

Hi, can we control what nodes may be “cut” or “copied” from a document in a “cut/copy/paste” scenario? We are trying to prevent users from breaking the structure of their manuals by accidentally cutting section references out of the parent document when using “ctrl+a” or other means to select the entire document.

On the other side, can we prevent those references from being pasted (in the scenario where the user has copied from an external source)?

Kind regards,

A Fonto partner

Manipulating cut, copy, and paste can usually be done with one API. For this we have a guide (How to configure the copy and paste behavior). It contains a short example on how to use the registerPastedNodesFilter API. Let’s make an example for this use-case. The partner did not mention how their reference nodes are called, so let’s imagine a referencing node looks like <ref id-ref="my-id"/>. The schema allows any element (with an ID) to be referenced, so a node like <some-element id="my-id" /> can be a referenced node. A reference can only be in the same document, so if we copy and paste a reference to another document, it will not resolve anymore.

If we then want to prevent these nodes to be pasted, we can just do something like this:

// This filter removes any descendant of pasted nodes that is called `ref`
registerPastedNodesFilter((nodes, blueprint) => {
    nodes.forEach(node =>
        evaluateXPathToNodes('descendant::ref', blueprint).forEach(unwantedNode =>
            blueprint.removeChild(blueprint.getParentNode(unwantedNode), unwantedNode)
        )
    );
 return nodes.filter(node => node.nodeName !== 'ref');
});

But this only solves half of the problem! We should also prevent any nodes that are being referenced from disappearing when we cut them. Even though the partner did not point it out explicitly, we should also deal with deleting those references using back-space or delete. There are two ways to do that: the first one uses the addMutationHook API to register a mutation hook that prevents these elements from being removed if they are referred to, the second one uses a central API to block the removal of these elements, unless explicitly removed.

The mutation hook set up would look something like this. This is an example from the Mutation hooks guide, adapted to this situation:

addAttributeIndex(
	null,
	'id-ref',
	'http://example.com/functions',
	'id-ref'
);

addMutationHook({
	selector: xq`self::*`,
	valueQuery: xq`
		import module namespace app="http://example.com/functions";
		if (id)
			then exists(app:id-ref(@id))
			else false()
	`,
	expectedResultType: evaluateXPath.BOOLEAN_TYPE,
	onEvent: {
		functionLocalName: 'on-element-removed',
		namespaceURI: 'http://example.com/functions'
	}
});
xquery version "3.0";

module namespace hooks = "http://example.com/functions";
import module namespace fonto-hooks = "http://www.fontoxml.com/functions/hooks";

declare %public function hooks:on-element-removed (
	$event-type as xs:string,
	$node as node(),
	$had-refs as xs:boolean?,
	$has-refs as xs:boolean?
) as map(*) {
	if ($event-type = "remove" and $had-refs) then
		fonto-hooks:not-allowed()
	else
		fonto-hooks:ok()
};

string($node)

This example works for cross-references, where a reference links to elements in the same document. It works less optimal for references that point to a child document, how DITA does topicrefs: <ref ref="documents/document.xml" />. The support ticket did not clarify which situation applies here, so let’s describe how to do the topicref approach. In some schemata, these references may be selected along when pressing selecting the whole document. You would not want to delete or copy these references: pasting them somewhere else may duplicate the links, pasting content that you did not intend to put on the clipboard.

Fonto’s delete and cut operations only delete visible content. If you configure the element as configureAsRemoved, it will not be removed. This is also how we configure topic references in our Dita example configuration. In general if you configure elements that do not match with the Elements configuration flowchart, finding a similar element in the Dita example configuration may help you!

I hope this explained how Fonto works and why it works like that. During the years we built quite the product, and we are aware some parts work in unexpected ways for those who have not been with it from the start. If you have any points of Fonto you would like some focus on, we are always ready and willing to share! Reach out on Twitter to Martin Middel or file a support issue!

Stay up-to-dateFonto Why & How posts direct in your inbox

Receive updates on new Fonto Why & How blog posts by email

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top