Implementing generated texts for cross references

In most schemata, there is an element that is used to 'link' to another element. This can be, for example, the xlink spec, HTML <a/> elements, the dita <xref href="..." format="dita"/>, or something more specific to your schema. Usually, these elements can contain text, which will be displayed as the text for the cross reference, often, they are just the title of whatever is referred (such as introduction, figure 4 or table 8. These titles need to be updated by hand whenever the actual title or number of the target changes.

This guide will describe how to automatically generate those titles in the Fonto editor, so that authors can see what is linked where, without updating the titles by hand. It will not go into doing the same in the output processor, that is a specific issue that needs to be addressed for each kind of output platform separately.

We are going to do this by converting the cross reference element twice: one for 'normal' references and once for 'automatic' references. The example will assume that empty cross reference elements should have a text computed while ones that have a text should display that text. Empty ones will use a double click operation to place the selection in an automatically generated one, allowing you to type in it to set a text. Those with manually added text will use an icon widget to empty it.

Requirements

Before you start this guide, you should already have implemented your cross ref element as an inline link. You should be able to see them, insert them and edit them.

This guide will use the following technologies:

  • Selection-dependent SX configurations, to render an element differently when the selection is in it

  • Use XQuery to compute the text for an xref automatically

  • Operations to convert between the two kinds of cross references: those with automatically generated texts, and those that are manually generated

  • XQuery Update facility to implement those operations

  • (Optional): Attribute indices to quickly resolve references

  • (Optional): Using the reference pipeline to first resolve a permanent id

Steps

By following this guide, you will be able to have two forms of cross references: those with automatically generated texts, and those without. The guide will switch between those depending on whether they have any element content, but you will be able to change that to for example toggling an attribute instead. While this guide will provide dita xref elements as an example, you should be able to choose any other schema by referring to different node names.

Step 1: different SX configurations

To change the configuration for your cross reference element, first find it. There should be a configuration line akin to this:

Other

// xref	
configureAsInlineLink(
	sxModule,
	'self::xref',
	t('link'),
	undefined,
	{
		emptyElementPlaceholderText: t('type the link text'),
		popoverComponentName: 'DitaCrossReferencePopover',
		popoverData: {
			editOperationName: ':contextual-edit-xref[@format=dita]',
			targetIsPermanentId: false,
			targetQuery: '@href'
		}
);

We are going to add another variant of the same:

Other

const CROSSREF_STYLES = applyCss({
	color: '#2196f3',
	textDecoration: 'underline solid',
	cursor: 'pointer'
});
configureAsInlineObjectInFrame(
	sxModule,
	`self::xref and
(: Ignore any PIs + comments, which are likely placeholders :)
 empty((./text(), ./element())) and
(: When the selection is here, just render it as a link :)
 not(fonto:selection-in-node(.))`,
	'',
	{
		createInnerJsonMl: (sourceNode, _renderer) => [
			'cv-ref',
			{ ...CROSSREF_STYLES, contentEditable: 'false' },
			evaluateXPathToString(
				'"Automatically generated!"',
				sourceNode,
				readOnlyBlueprint
			)
		],
		backgroundColor: 'grey',
		doubleClickOperation: 'xref-convert-to-manual',
		inlineBefore: []
	}
);

This snippet does the following:

  1. It defines any xref that contains no text or elements and in which we do not have the selection as an inline object

  2. It uses the createinnerjsonml @ fontoxml-families/src/configureasobject.js CVK option to set an automatically generated text

  3. It refers to an operation xref-convert-to-manual operation that we will declare later

Now open the editor and insert an empty cross reference, you should see it in the content view.

Step 2: Automatically compute the text

This step is highly dependent on your schema and architecture. If you are linking to another document, you need to navigate to that document, if you use JIT, the element you're referring may not be loaded yet, etcetera.

To start, create a new XQuery module file at the root of a package. Preferable the package that is configuring the cross reference element. Declare a module in it (refer to the Handle namespaces guide to see which namespace to use). All of the examples will declare all functions in the "app" namespace. Feel free to choose your own.

To call one of the example functions, change the call to evaluatexpathtostring @ fontoxml-selectors/src/evaluatexpathtostring.js in the configureasinlineobjectinframe @ fontoxml-families/src/configureasinlineobjectinframe.js call of the previous step to this:

Other

configureAsInlineObjectInFrame(
	sxModule,
	`self::xref and
(: Ignore any PIs + comments, which are likely placeholders :)
 empty((./text(), ./element())) and
(: When the selection is here, just render it as a link :)
 not(fonto:selection-in-node(.))`,
	'',
	{
		createInnerJsonMl: (sourceNode, _renderer) => [
			'cv-ref',
			{ ...CROSSREF_STYLES, contentEditable: 'false' },
			evaluateXPathToString(
				'import module namespace app="app"; app:compute-title(.)', // This line
				sourceNode,
				readOnlyBlueprint
			)
		],
		backgroundColor: 'grey',
		doubleClickOperation: 'xref-convert-to-manual',
		inlineBefore: []
	}
);

Example 1: References to a node in a loaded document

This example assumes there is an element that has an ref attribute which refers to the ID of an element in a loaded document. To make these look-ups faster, we need to use an attribute index. This can be declared in an configureSxModule of a package on which the cross references implementation depends. We suggest to make a separate package for this. Refer to documentation on the application structure for more information on how to do this.

Other

import addAttributeIndex from 'fontoxml-indices/src/addAttributeIndex.js';

export default function configureSxModule(sxModule) {
	// Add an attribute index on id, so that we can quickly find out which element has the id
	// attribute of the ref we have
	addAttributeIndex('', 'id', 'app-uri', 'id-index');
}

The XQuery module:

Other

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


declare %public function app:compute-title ($node as element()) as xs:string {
	let $ref := $node/@ref
	let $referredNode := app:id-index($ref)
	return if ($referredNode) then fonto:title-content($referredNode) else "Unknown reference"
};

This module defines a single function that receives an element, reads the ref attribute, leverages the attribute index to quickly find the element and either displays a warning or displays the title content of the referred node.

Example 2: References to another document that is loaded

This example assumes a reference is made to a full document.

Other

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

declare %public function app:compute-title ($node as element()) as xs:string {
	let $ref := $node/@ref
	let $referredNode := fonto:document($url)/*
	return if ($referredNode) then fonto:title-content($referredNode) else "Unknown reference"
};

This module defines a single function that receives an element, reads the ref attribute, leverages the TODO LINK FONTO:DOCUMENT to quickly find the document and either displays a warning or displays the title content of the referred node.

Example 3: References to another document that might not be loaded

Not all editors have all documents loaded at any time. We will therefore not always have a title for those documents. We do not want to display a placeholder string for those references. A good fix for this issue is to let the CMS generate titles as well and to place them in the document from which all documents are referenced (dita maps for example.) This example will be specific to dita but a similar approach works for other schemata as well.

It will work by first seeing whether a document is loaded. If it is not, it should find any topicref to that same document that has a navtitle set. To quickly find these topicrefs, we need to declare an attribute index:

Other

import addAttributeIndex from 'fontoxml-indices/src/addAttributeIndex.js';

export default function configureSxModule(sxModule) {
	// Add an attribute index on href, so that we can quickly find out which element has the href
	// attribute of the ref we have
	addAttributeIndex('', 'href', 'app-uri', 'href-index');
}

The XQuery function can be this:

Other

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

declare %public function app:compute-title($node as element()) as xs:string {
	let $url := $node/@href
	let $is-loaded := fonto:is-document-loaded($url)
	return if ($is-loaded) then
		(: document is loaded, just return the title content :)
		fonto:document($url)/* => fonto:title-content()
	else
	 (: Document is not loaded, try to find a map referencing the document :)
	 let $topicref := app:href-index($url)
	 let $titleA := $topicref/self::topicref[parent::map or parent::topicref][1]/topicmeta/navtitle
	 return if (not($topicref)) then
	 	"Unknown topicref" else
		if (not($titleA)) then "No title" else string($titleA[1])
};

Example 4: Permanent ids

For permanent ids, use the TODO LINK RESOLVE PERMANENT ID FUNCTION to resolve it. Base the rest of the configuration on one of the previous examples.

Step 3: Operations

Since it is impossible to place a cursor in an inline object, we are going to have to write an operation (operation). The example will use XQuery combined with the Selection Processing Instructions to do this.

In step 1, we already referenced the xref-convert-back operation. We will now implement it in an operations.json file in the same package as the configureSxModule.js file that references it.

Other

"xref-convert-to-manual": {
	"steps": [
		{
			"type": "operation/execute-update-script",
			"data": {
				"expression":
				"import module namespace app='app-uri'; app:convert-xref-to-manual($data?contextNode)"
			}
		}
	]
}

This operation will execute an XQuery function. Let's declare it in the same module as we have been working so far:

Other

declare %public %updating function app:convert-xref-to-manual ($node as element()) {
	insert nodes (
		<?fontoxml-selection-start?>,
		app:compute-title($data?contextNode),
		<?fontoxml-selection-end?>
	) into $node
};

This function uses XQuery update facility to insert the 'title' of the reference into the xref and place the Selection PIs around it. If the reference needs additional structure (such as a topicmeta element or a navtitle element in dita), add them here.

To convert back to automatic, we will use an icon widget. Add the following option to the cvkoptions (family/cvk/cvkoptions) of the configureasinlinelink @ fontoxml-families/src/configureasinlinelink.js call of step 1:

Other

configureAsInlineLink(
	sxModule,
	'self::xref',
	t('link'),
	undefined,
	{
		popoverComponentName: 'DitaCrossReferencePopover',
		popoverData: {
			editOperationName: ':contextual-edit-xref[@format=dita]',
			targetIsPermanentId: false,
			targetQuery: '@href'
		},
		inlineBefore: [
			createIconWidget('link', {
				clickOperation: 'xref-convert-to-automatic',
				tooltipContent: t('Click to make the link text automatically generated')
			})
		]
	}
);

This will add an icon widget with a click operation xref-convert-to-automatic. Let's declare this next to the other operation:

Other

"xref-convert-to-automatic": {
	"steps": [
		{
			"type": "operation/execute-update-script",
			"data": {
				"expression":
				"import module namespace app='app-uri'; app:convert-xref-to-automatic($data?contextNode)"
			}
		}
	]
}

Again, this is XQuery, add the function to the module we've been using:

Other

declare %public %updating function app:convert-xref-to-automatic ($node as element()) {
	delete nodes $node/node(),
	insert node <?fontoxml-selection ?> after $node
};

This will do two things: empty out the passed node and place the selection behind it.

Testing it

Refreshing the editor should show a working implementation of automatically generated cross reference titles. You should be able to click on the icon to convert them from manual to automatic and you should be able to double click on them to convert them back to manually set texts.