Implementing generated texts for cross-references

This guide will describe how to generate texts for cross-reference elementsIn 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 introductionfigure 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 and to where, without having to manually update those titles by hand.

This is does not affect your published content. That is a specific issue that needs to be addressed for each of your output platforms separately.

We are going to do this by converting the cross-reference element twice: once for 'normal' references and once for 'automatic' references. The example will assume that empty cross-reference elements should have a text generated while the ones that have a text should display that text. Empty cross-reference elements will use a double click operation to place the selection in an automatically generated text, allowing you to set another text by typing. 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 generate 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. In this guide we will switch between those cross-references depending on whether they have any element content, but you will be able to change this condition 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:

configureSxModule.js
// 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:

configureSxModule.js
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: ':contextual-convert-to-manual-xref',
		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 :contextual-convert-to-manual-xref 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 generate the text

This step is highly dependent on your schema and architecture, for example if you are using JIT, then the element you're referring to may not be loaded yet or if you are linking to another document you need to first navigate to that document to link it.

To start, create a new XQuery module file at the root of a package. It would be preferable to use the package that is configuring the cross-reference element. Declare a module in it (refer to the Configure 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 in the configureAsInlineObjectInFrame call of the previous step to this: 

configureSxModule.js
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-uri"; app:compute-title(.)', // This line
				sourceNode,
				readOnlyBlueprint
			)
		],
		backgroundColor: 'grey',
		doubleClickOperation: ':contextual-convert-to-manual-xref',
		inlineBefore: []
	}
);

Example 1: References to a node in a loaded document

This example assumes that there is an element that has an href 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 the configureSxModule of the package that the cross-reference implementation depends on. 

We suggest to make separate packages for adding an attribute index and an XQuery module. The package which has the XQuery module must depend on the package which adds the attribute index. This is because Fonto first registers XQuery modules before processing the configureSxModule.js files. Please refer to documentation on the application structure for more information on how to do this.


configureSxModule.js
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:

function.xqm
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 $href := $node/@href
	let $referredNode := app:id-index($href)
	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.

function.xqm
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 $href := $node/@href
	let $referredNode := fonto:document($href)/*
	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 fonto:document custom XPath function 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 the documents loaded at one time, which means the title will not always be available 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:

configureSxModule.js
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:

function.xqm
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 fonto:resolve-permanent-id custom XPath 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. The example will use XQuery combined with the Selection Processing Instructions to do this.

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

operations.json
":contextual-convert-to-manual-xref": {
	"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 that we have been working in so far:

function.xqm
declare %public %updating function app:convert-xref-to-manual ($node as element()) {
	insert nodes (
		<?fontoxml-selection start?>,
		app:compute-title($node),
		<?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 of the configureAsInlineLink call of step 1:

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

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

operations.json
":contextual-convert-to-automatic-xref": {
	"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:

function.xqm
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.

Was this page helpful?