Friday, July 17, 2015

Controlled Attribute Values (Part 2 - Advanced)

Share to Facebook Share to Twitter Email This Share on Google Plus Share on Tumblr

As already presented in Controlled Attribute Values for your DITA Project, Oxygen allows you to add or replace possible values for attributes or elements based on a simple configuration file. A more complex scenario is one in which in order to decide which values to provide, you need more context information. Let's take this DITA fragment:

<metadata>
    <othermeta name="name" content="value"/>
</metadata>

What we want is to offer proposals for @content but the possible values for @content depend on the value of @name. We will see how we can solve this dependency.

The configuration file

The configuration file (cc_value_config.xml) allows calling an XSLT stylesheet and that's just what we will do:
<match elementName="othermeta" attributeName="content">
    <xslt href="meta.xsl" useCache="false"/>
</match>

As you can see, we can't express the dependency between @content and @name inside the configuration file . I also want to mention that because the values for @content are dynamic, we want the XSLT script to execute every time the values are requested (we shouldn't cache the results). We enforce this by setting @useCache to false.

The XSLT script

The XSLT script has access to the XML document (through the documentSystemID parameter) but it lacks any context information, we can't really tell for which othermeta element was the script invoked. To counter this limitation, we will use Java extension functions and we will call Oxygen's Java-based API from the XSLT. Here how it looks:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:xd="http://www.oxygenxml.com/ns/doc/xsl"
    xmlns:tei="http://www.oxygenxml.com/ns/doc/xsl"
    xmlns:prov="java:ro.sync.exml.workspace.api.PluginWorkspaceProvider"
    xmlns:work="java:ro.sync.exml.workspace.api.PluginWorkspace"
    xmlns:editorAccess="java:ro.sync.exml.workspace.api.editor.WSEditor"
    xmlns:saxon="http://saxon.sf.net/"
    xmlns:textpage="java:ro.sync.exml.workspace.api.editor.page.text.xml.WSXMLTextEditorPage"
    xmlns:authorPage="java:ro.sync.exml.workspace.api.editor.page.author.WSAuthorEditorPage"
    xmlns:ctrl="java:ro.sync.ecss.extensions.api.AuthorDocumentController"
    exclude-result-prefixes="xs xd"
    version="2.0">
    <xsl:param name="documentSystemID" as="xs:string"></xsl:param>
    
    <xsl:template name="start">
        <xsl:variable name="workspace" select="prov:getPluginWorkspace()"/>
        <xsl:variable name="editorAccess" select="work:getEditorAccess($workspace, xs:anyURI($documentSystemID), 0)"/>
        <xsl:variable name="pageID" as="xs:string" select="editorAccess:getCurrentPageID($editorAccess)"/>
        
        <xsl:variable name="name" as="xs:string">
            <xsl:choose>
                <xsl:when test="$pageID='Text'">
                    <xsl:variable name="textpage" select="editorAccess:getCurrentPage($editorAccess)"/>
                    <!-- In the text page, the context is the @content attribute -->
                    <xsl:value-of select="textpage:evaluateXPath($textpage, 'xs:string(./parent::node()/@name)')"/>
                </xsl:when>
                <xsl:when test="$pageID='Author'">
                    <xsl:variable name="authorPage" select="editorAccess:getCurrentPage($editorAccess)"/>
                    <xsl:variable name="caretOffset" select="authorPage:getCaretOffset($authorPage)"/>
                    <xsl:variable name="ctrl" select="authorPage:getDocumentController($authorPage)"/>
                    <xsl:variable name="contextNode" select="ctrl:getNodeAtOffset($ctrl, $caretOffset)"/>
                    <!-- In the author page, the context is the "othermeta" element -->
                    <xsl:value-of select="ctrl:evaluateXPath($ctrl, 'xs:string(@name)', $contextNode, false(), false(), false(), false())[1]"/>
                </xsl:when>
            </xsl:choose>
        </xsl:variable>
        
        <items>        
            <xsl:choose>
                <xsl:when test="$name = 'temperatureScale'">
                    <item value="Celsius" annotation="(symbol C)"/>
                    <item value="Fahrenheit" annotation="(symbol F)"/>
                </xsl:when>
                <xsl:when test="$name = 'measurement'">
                    <item value="Metric" annotation="Metric system"/>
                    <item value="Imperial" annotation="Also known as British Imperial"/>
                </xsl:when>
            </xsl:choose>
        </items>
    </xsl:template>    
</xsl:stylesheet>

It is also worth mentioning that in the next Oxygen version (17.1) we will provide a more elegant solution to this situation. The XSLT script will receive a new parameter, an XPath expression that will identify the element for which content completion was invoked. But maybe we will talk about that in a future post...

2 comments:

  1. I have an element (Image) which has two attributes, @id and @label. The list of Images is kept in a database. I'd like to do content completion on the @label, via XSLT query, but insert the corresponding @id as well. Can you recommend a method for that?

    ReplyDelete
  2. Hi,

    Do you want to do this in the Author Page? If the answer is yes(I hope it is) then I suggest using author actions [1]. The user will click a button (you can put it directly in the document as a button form control[2]) and a dialog will be shown presenting the labels. When the user makes a choice both the @label and the @id are updated. Such an action can be implemented either as a JSOperation:

    function insert() {

    //The current node is either entirely selected...
    var currentNode = authorAccess.getEditorAccess().getFullySelectedNode();
    if (currentNode == null) {
    //or the cursor is placed in it
    caretOffset = authorAccess.getEditorAccess().getCaretOffset();
    currentNode = authorAccess.getDocumentController().getNodeAtOffset(caretOffset);
    }
    //Get current value of the @label attribute
    var currentLabelValue = "";
    currentLabelValueAttr = currentNode.getAttribute("label");
    if (currentLabelValueAttr != null) {
    currentLabelValue = currentLabelValueAttr.getValue();
    }

    var newLabelValue = javax.swing.JOptionPane.showInputDialog(
    Packages.ro.sync.exml.workspace.api.PluginWorkspaceProvider.getPluginWorkspace().getParentFrame(),
    "Select the value",
    "The title",
    3,
    null,
    [ "Label1", "Label2", "Label3"],
    currentLabelValue);

    if (newLabelValue != null) {
    //Create and set the new attribute value for the @type attribute.
    var attrValue = new Packages.ro.sync.ecss.extensions.api.node.AttrValue(newLabelValue);
    authorAccess.getDocumentController().setAttribute("label", attrValue, currentNode);

    // Set the @id as well.
    var idValue = new Packages.ro.sync.ecss.extensions.api.node.AttrValue(newLabelValue.substring(newLabelValue.length() - 1));
    authorAccess.getDocumentController().setAttribute("id", idValue, currentNode);
    }
    }

    or as a XQueryUpdateOperation:

    declare namespace jop = "javax.swing.JOptionPane";
    declare namespace prov = "ro.sync.exml.workspace.api.PluginWorkspaceProvider";
    declare namespace work="ro.sync.exml.workspace.api.PluginWorkspace";

    let $inst := prov:getPluginWorkspace()

    let $newLabel := jop:showInputDialog(
    work:getParentFrame($inst),
    "Select the value",
    "The title",
    3,
    null,
    ("Label1", "Label2", "Label3"),
    string(@label))


    return

    (replace value of node @label with $newLabel,
    replace value of node @id with substring($newLabel, string-length($newLabel)))

    If you write me an email on support@oxygenxml.com I will send you a sample framework with these actions.


    [1] https://oxygenxml.com/doc/versions/18.1/ug-editor/topics/the-action-dialog.html
    [2] https://oxygenxml.com/doc/versions/18.0/ug-editor/topics/button-editor.html

    ReplyDelete