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

Wednesday, July 15, 2015

Controlled Attribute Values for your DITA Project

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

Frequently when editing DITA content you will feel the need to enforce a controlled set of values when editing certain attributes. For example you may want to impose that the values for the @outputclass attribute on the element codeblock are either language-xml or language-css. This is useful in order to remind writers that any other value will not be interpreted by the build process in a significant manner.

Oxygen has a couple of easy ways in which controlled values can be imposed for certain attributes:
  1. You can edit the XML configuration file OXYGEN_INSTALL_DIR/frameworks/dita/resources/cc_value_config.xml and provide additional entries. In the case of our small example for providing controlled values for the @attribute the configuration file should contain an additional entry:
    <match elementName="codeblock" attributeName="outputclass">
     <items action="addIfEmpty">
      <item value="language-xml" annotation="XML Syntax Highlight"/>
      <item value="language-css" annotation="CSS Syntax Highlight"/>
     </items>     
    </match>
    Besides providing a hard-coded list of values the content completion configuration file is flexible enough to allow calling an XSLT stylesheet which could retrieve those values from other sources (for example via HTTP from an Exist database).
  2. Provide those controlled values via a Subject Scheme Map (my favorite). Coming back to our example, you can create a small Subject Scheme map with the file name controlledValues.ditamap and the content:
    <!DOCTYPE subjectScheme PUBLIC "-//OASIS//DTD DITA Subject Scheme Map//EN""map.dtd"> 
    <subjectScheme>
        <subjectHead>
            <subjectHeadMeta>
                <navtitle>Provide controlled attributes</navtitle>
            </subjectHeadMeta>
        </subjectHead>
        <hasInstance>
            <subjectdef keys="languageTypeKey">
                <subjectdef keys="language-xml">
                    <topicmeta>
                        <navtitle>XML Syntax Highlight</navtitle>
                    </topicmeta>
                </subjectdef>
                <subjectdef keys="language-css">
                    <topicmeta>
                        <navtitle>CSS Syntax Highlight</navtitle>
                    </topicmeta>
                </subjectdef>
            </subjectdef>
        </hasInstance>
        <enumerationdef>
            <elementdef name="codeblock"/>
            <attributedef name="outputclass"/>
            <subjectdef keyref="languageTypeKey"/>
        </enumerationdef>
    </subjectScheme>
    then you can refer to it from your main DITA Map like:
    <topicref href="controlledValues.ditamap" format="ditamap" type="subjectScheme"/>
  3. If the attributes on which you want to impose certain values are DITA profiling attributes, you can go to the Oxygen Preferences->Editor / Edit modes / Author / Profiling/Conditional Text page and define the set of allowed values for them.

The only problem with the first approach is the fact that validation will not impose those values and writers will not receive validation error messages if they set another value for the specific attribute. So you will probably need to add a Schematron check in order to signal errors when a certain attribute's value does not match the list of controlled attribute values. For both the second and third approaches, validation will warn the writers if certain attribute values do not match values in the controller values list.