Skip to content

Instantly share code, notes, and snippets.

@kwboone
Created July 13, 2015 16:19
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save kwboone/95759c7d96f0028213d0 to your computer and use it in GitHub Desktop.
Save kwboone/95759c7d96f0028213d0 to your computer and use it in GitHub Desktop.
Compares HL7 CDA templates developed using Trifolia
<?xml version="1.0" encoding="UTF-8"?>
<!--
This stylesheet compares Implementation Guides in Trifolia format (see trifolia.lantanagroup.com)
It takes one parameter: newVersion, which identifies the filename of the new version of the guide to compare.
Its input is the older version of the guide.
Its output is HTML. It produces a section in the HTML for each template in the old guide that is present in some version in the new guide. The table heading
compares the template metadata and highlights changed rows in the first column. It then compares the Narrative text constraints for the two templates, using
heuristics to find the closest matching template constraints based on context and RIM class used for the template.
Differences are highlighted in a color based on the strength of the original conformance constraint:
RED = SHALL
YELLOW = SHOULD
GREEN = MAY
WHITE = No change
Text in the narrative constraint contained inside () is ignored during the comparison.
Copyright (c) 2015 by Keith W. Boone
-->
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:lg="http://www.lantanagroup.com" xmlns:set="http://exslt.org/sets"
xmlns:exslt="http://exslt.org/common" extension-element-prefixes="set exslt" version="1.0">
<xsl:param name="newVersion" select="'C-CDA R2.1.xml'"/>
<xsl:variable name="currentVersionDoc" select="document($newVersion)"/>
<xsl:variable name="currentVersion">
<xsl:apply-templates select="$currentVersionDoc" mode="copy"/>
</xsl:variable>
<xsl:variable name="current" select="exslt:node-set($currentVersion)"/>
<xsl:variable name="refactored">
<xsl:apply-templates select="/" mode="copy"/>
</xsl:variable>
<xsl:template match="/">
<xsl:variable name="processed">
<xsl:call-template name="process"/>
</xsl:variable>
<xsl:apply-templates select="exslt:node-set($processed)" mode="fixTable"/>
</xsl:template>
<xsl:template name="process">
<html>
<style>
.MAY {
background-color: C0FFC0;
}
.SHOULD {
background-color: FFFFC0;
}
.SHALL {
background-color: FFC0C0;
}
.different {
background-color: C0C0C0;
}</style>
<body>
<xsl:for-each select="exslt:node-set($refactored)//lg:Template">
<xsl:variable name="id"
select="substring-after(substring-after(@identifier,':'),':')"/>
<xsl:call-template name="compare">
<xsl:with-param name="old" select="."/>
<!--select="document(concat($original, '\', $id, '.xml'))//lg:Template"/>-->
<xsl:with-param name="new"
select="$current//lg:Template[contains(@identifier, concat(&quot;:&quot;,$id,&quot;:&quot;))]"/>
<!--select="document(concat($current, '\', $id, '.xml'))//lg:Template"/>-->
</xsl:call-template>
</xsl:for-each>
</body>
</html>
</xsl:template>
<xsl:template name="compare">
<xsl:param name="old"/>
<xsl:param name="new"/>
<xsl:if test="count($new//*) != 0">
<section>
<h2>
<xsl:value-of select="$old/@title"/>
</h2>
<table border="1" cellspacing="0">
<col width="40%"/>
<col width="40%"/>
<thead>
<tr>
<th width="20%">Feature</th>
<th width="40%">Original</th>
<th width="40%">New</th>
</tr>
</thead>
<thead>
<tr>
<th align="left" width="20%">
<xsl:if
test="$old/@owningImplementationGuideName != $new/@owningImplementationGuideName">
<xsl:attribute name="class">different</xsl:attribute>
</xsl:if>
<xsl:text>IG</xsl:text>
</th>
<td width="40%">
<xsl:value-of select="$old/@owningImplementationGuideName"/>
</td>
<td width="40%">
<xsl:value-of select="$new/@owningImplementationGuideName"/>
</td>
</tr>
<tr>
<th align="left">
<xsl:if test="$old/@title != $new/@title">
<xsl:attribute name="class">different</xsl:attribute>
</xsl:if>
<xsl:text>title</xsl:text>
</th>
<td>
<xsl:value-of select="$old/@title"/>
</td>
<td>
<xsl:value-of select="$new/@title"/>
</td>
</tr>
<tr>
<th align="left">
<xsl:if
test="substring-before(substring-after(substring-after(concat($old/@identifier,':'),':'),':'),':') !=
substring-before(substring-after(substring-after(concat($new/@identifier,':'),':'),':'),':')">
<xsl:attribute name="class">different</xsl:attribute>
</xsl:if>
<xsl:text>identifier</xsl:text>
</th>
<td>
<xsl:value-of
select="substring-before(substring-after(substring-after(concat($old/@identifier,':'),':'),':'),':')"
/>
</td>
<td>
<xsl:value-of
select="substring-before(substring-after(substring-after(concat($new/@identifier,':'),':'),':'),':')"
/>
</td>
</tr>
<tr>
<th align="left">
<xsl:if
test="substring-after(substring-after(substring-after(concat($old/@identifier,':'),':'),':'),':') !=
substring-after(substring-after(substring-after(concat($new/@identifier,':'),':'),':'),':')">
<xsl:attribute name="class">different</xsl:attribute>
</xsl:if>
<xsl:text>version</xsl:text>
</th>
<td>
<xsl:value-of
select="substring-after(substring-after(substring-after(concat($old/@identifier,':'),':'),':'),':')"
/>
</td>
<td>
<xsl:value-of
select="substring-after(substring-after(substring-after(concat($new/@identifier,':'),':'),':'),':')"
/>
</td>
</tr>
<tr>
<th align="left">
<xsl:if test="$old/@publishStatus != $new/@publishStatus">
<xsl:attribute name="class">different</xsl:attribute>
</xsl:if>
<xsl:text>publishStatus</xsl:text>
</th>
<td>
<xsl:value-of select="$old/@publishStatus"/>
</td>
<td>
<xsl:value-of select="$new/@publishStatus"/>
</td>
</tr>
<tr>
<th align="left">
<xsl:if test="$old/@templateType != $new/@templateType">
<xsl:attribute name="class">different</xsl:attribute>
</xsl:if>
<xsl:text>templateType</xsl:text>
</th>
<td>
<xsl:value-of select="$old/@templateType"/>
</td>
<td>
<xsl:value-of select="$new/@templateType"/>
</td>
</tr>
<tr>
<th align="left">
<xsl:if test="$old/@isOpen != $new/@isOpen">
<xsl:attribute name="class">different</xsl:attribute>
</xsl:if>
<xsl:text>isOpen</xsl:text>
</th>
<td>
<xsl:value-of select="$old/@isOpen"/>
</td>
<td>
<xsl:value-of select="$new/@isOpen"/>
</td>
</tr>
<tr>
<th align="left">
<xsl:if
test="string($old/lg:PreviousVersion) != string($new/lg:PreviousVersion)">
<xsl:attribute name="class">different</xsl:attribute>
</xsl:if>
<xsl:text>PreviousVersion</xsl:text>
</th>
<td>
<xsl:apply-templates select="$old/lg:PreviousVersion"/>
</td>
<td>
<xsl:apply-templates select="$new/lg:PreviousVersion"/>
</td>
</tr>
<tr>
<th align="left">
<xsl:if
test="string($old/lg:Description) != string($new/lg:Description)">
<xsl:attribute name="class">different</xsl:attribute>
</xsl:if>
<xsl:text>Description</xsl:text>
</th>
<td>
<xsl:apply-templates select="$old/lg:Description"/>
</td>
<td>
<xsl:apply-templates select="$new/lg:Description"/>
</td>
</tr>
<tr>
<th align="left">
<xsl:value-of select="$old/@contextType"/>
</th>
<td colspan='2'>&#xA0;</td>
</tr>
</thead>
<xsl:call-template name="contrastContexts">
<xsl:with-param name="contexts"
select="set:distinct($old/lg:Constraint/@context|$new/lg:Constraint/@context)"/>
<xsl:with-param name="oldP" select="$old"/>
<xsl:with-param name="newP" select="$new"/>
<xsl:with-param name="previous" select="concat($old/@context,'/')"/>
</xsl:call-template>
</table>
</section>
</xsl:if>
</xsl:template>
<xsl:template match="lg:PreviousVersion">
<p><xsl:value-of select="@name"/> (<xsl:value-of select="@identifier"/>)</p>
</xsl:template>
<xsl:template match="lg:Description|lg:NarrativeText">
<p>
<xsl:value-of select="text()"/>
</p>
</xsl:template>
<xsl:template name="contrastContexts">
<xsl:param name="contexts"/>
<xsl:param name="oldP"/>
<xsl:param name="newP"/>
<xsl:param name="previous"/>
<tbody>
<!-- Nexting tbody's this way isn't really legal, but I'm going to fix it in a post processing pass
Counting the max depth of tbody will tell me how many columns to add at the beginning of the table
and how to add rowspans to the first td of each tr.
-->
<xsl:for-each select="$contexts">
<xsl:sort select="." data-type="text"/>
<xsl:variable name="ctx" select="string(.)"/>
<xsl:variable name="oldCL" select="$oldP/lg:Constraint[@context = $ctx]"/>
<xsl:variable name="newCL" select="$newP/lg:Constraint[@context = $ctx]"/>
<xsl:variable name="numbers" select="set:distinct($oldCL/@number|$newCL/@number)"/>
<!-- Only split them by number if count($oldCL) != 1 or count ($newCL != 1) -->
<xsl:choose>
<xsl:when test="count($oldCL) &gt; 1 or count($newCL) &gt; 1">
<xsl:for-each select="$numbers">
<xsl:sort select="." data-type="number"/>
<xsl:variable name="number" select="string(.)"/>
<xsl:variable name="oldC"
select="$oldP/lg:Constraint[@context = $ctx and @number = $number]"/>
<xsl:variable name="newC"
select="$newP/lg:Constraint[@context = $ctx and @number = $number]"/>
<xsl:call-template name="compareConstraint">
<xsl:with-param name="ctx" select="$ctx"/>
<xsl:with-param name="oldC" select="$oldC"/>
<xsl:with-param name="newC" select="$newC"/>
<xsl:with-param name="previous" select="$previous"/>
<xsl:with-param name="numbers" select="true()"/>
</xsl:call-template>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="number" select="string(.)"/>
<xsl:variable name="oldC" select="$oldCL"/>
<xsl:variable name="newC" select="$newCL"/>
<xsl:call-template name="compareConstraint">
<xsl:with-param name="ctx" select="$ctx"/>
<xsl:with-param name="oldC" select="$oldCL"/>
<xsl:with-param name="newC" select="$newCL"/>
<xsl:with-param name="previous" select="$previous"/>
<xsl:with-param name="numbers" select="false()"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</tbody>
</xsl:template>
<xsl:template name="compareConstraint">
<xsl:param name="oldC"/>
<xsl:param name="newC"/>
<xsl:param name="ctx"/>
<xsl:param name="previous"/>
<xsl:param name="numbers"/>
<xsl:variable name="oldNarr">
<xsl:apply-templates select="$oldC/lg:NarrativeText/text()" mode="narrative"/>
</xsl:variable>
<xsl:variable name="newNarr">
<xsl:apply-templates select="$newC/lg:NarrativeText/text()" mode="narrative"/>
</xsl:variable>
<tr>
<th align="left">
<xsl:if
test=" translate(normalize-space(string($oldNarr)),'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz') !=
translate(normalize-space(string($newNarr)),'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz') ">
<xsl:attribute name="class">
<xsl:value-of select="$oldC/@conformance"/>
</xsl:attribute>
</xsl:if>
<p>
<!-- <xsl:value-of select="$previous"/> -->
<xsl:value-of select="$ctx"/>
<xsl:if test="$numbers">
<xsl:text> (</xsl:text>
<xsl:value-of select="$oldC/@number"/>
<xsl:text>/</xsl:text>
<xsl:value-of select="$newC/@number"/>
<xsl:text>)</xsl:text>
</xsl:if>
</p>
</th>
<td>
<p>
<xsl:value-of select="$oldC/lg:NarrativeText/text()"/>
</p>
</td>
<td>
<p>
<xsl:value-of select="$newC/lg:NarrativeText/text()"/>
</p>
</td>
</tr>
<xsl:if test="$oldC/lg:Constraint|$newC/lg:Constraint">
<xsl:call-template name="contrastContexts">
<xsl:with-param name="contexts"
select="set:distinct($oldC/lg:Constraint/@context|$newC/lg:Constraint/@context)"/>
<xsl:with-param name="oldP" select="$oldC"/>
<xsl:with-param name="newP" select="$newC"/>
<xsl:with-param name="previous"
select="concat($previous,substring-before(concat($ctx,'['),'['))"
/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<!-- Strip non-essential text from text constraints for comparison of differences:
The numbers in the CONF: preceding the -
The variance in versioned identifiers starting with urn:oid and urn:hl7ii
-->
<xsl:template name="stripParenthetical" match="text()" mode="narrative">
<xsl:param name="text" select="."/>
<xsl:variable name="left" select="normalize-space(substring-before($text,'('))"/>
<xsl:variable name="right" select="normalize-space(substring-after($text,')'))"/>
<xsl:value-of select="$left"/>
<xsl:if test="$right!=''">
<xsl:call-template name="stripParenthetical">
<xsl:with-param name="text" select="$right"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<!--
When you see this:
<Constraint isBranch="true" isStatic="true" cardinality="1..*" number="8939" context="entryRelationship" isVerbose="false" conformance="SHALL">
<NarrativeText>SHALL contain at least one [1..*] entryRelationship (CONF:8939) such that it</NarrativeText>
<Constraint isBranchIdentifier="true" isStatic="true" cardinality="1..1" number="8940" context="@typeCode" isVerbose="false" conformance="SHALL">
<SingleValueCode code="REFR" displayName="Refers to" />
<CodeSystem oid="urn:oid:2.16.840.1.113883.5.1002" />
<NarrativeText>SHALL contain exactly one [1..1] @typeCode="REFR" Refers to (CodeSystem: HL7ActRelationshipType urn:oid:2.16.840.1.113883.5.1002 STATIC) (CONF:8940).</NarrativeText>
</Constraint>
Change it to:
<Constraint isBranch="true" isStatic="true" cardinality="1..*" number="8939" context="entryRelationship[@typeCode='REFR']" isVerbose="false" conformance="SHALL">
<NarrativeText>SHALL contain at least one [1..*] entryRelationship (CONF:8939) such that it<br/>
SHALL contain exactly one [1..1] @typeCode="REFR" Refers to (CodeSystem: HL7ActRelationshipType urn:oid:2.16.840.1.113883.5.1002 STATIC) (CONF:8940).<br/>
AND
</NarrativeText>
-->
<xsl:template match="lg:Constraint[lg:Constraint/@context='@typeCode']" mode="copy">
<xsl:copy>
<xsl:copy-of select="@*[not(local-name()='context')]"/>
<xsl:attribute name="context">
<xsl:value-of select="@context"/>
<xsl:text>[@typeCode='</xsl:text>
<xsl:value-of select="lg:Constraint[@context='@typeCode']/lg:SingleValueCode/@code"/>
<xsl:text>' and </xsl:text>
<xsl:value-of select="lg:Constraint/@containedTemplateType"/>
<xsl:text>]</xsl:text>
</xsl:attribute>
<xsl:apply-templates select="*[not(@context='@typeCode')]" mode="copy"/>
</xsl:copy>
</xsl:template>
<xsl:template match="lg:NarrativeText[../lg:Constraint/@context='@typeCode']" mode="copy">
<xsl:copy>
<xsl:copy-of select="@*"/>
<xsl:value-of select="."/>
<xsl:text>&#xA;</xsl:text>
<xsl:value-of select="../lg:Constraint[@context='@typeCode']/lg:NarrativeText"/>
<xsl:text>&#xA; AND&#xA;</xsl:text>
</xsl:copy>
</xsl:template>
<xsl:template match="@*|node()" mode="copy">
<xsl:copy>
<xsl:apply-templates select="@*|node()" mode="copy"/>
</xsl:copy>
</xsl:template>
<xsl:template name="computeDepth">
<xsl:for-each select="//tbody">
<xsl:sort select="count(ancestor-or-self::tbody)" order="descending" data-type="number"/>
<xsl:if test="position() = 1">
<xsl:value-of select="count(ancestor-or-self::tbody)"/>
</xsl:if>
</xsl:for-each>
</xsl:template>
<xsl:template name="cols">
<xsl:param name='count'/>
<xsl:if test='$count != 0'>
<col width='1%'/>
<xsl:call-template name="cols">
<xsl:with-param name="count" select="$count - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<!-- compute the maximum depth in tbody sections -->
<xsl:template match="html" mode="fixTable">
<xsl:variable name="depth">
<xsl:call-template name="computeDepth"/>
</xsl:variable>
<xsl:copy>
<xsl:apply-templates select="@*|node()" mode="fixTable">
<xsl:with-param name="depth" select="$depth + 1"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="table" mode="fixTable">
<xsl:param name="depth"/>
<xsl:copy>
<xsl:attribute name="cols">
<xsl:value-of select="$depth + 2"/>
</xsl:attribute>
<xsl:apply-templates select="@*" mode="fixTable"/>
<xsl:call-template name="cols">
<xsl:with-param name="count" select="$depth - 1"/>
</xsl:call-template>
<col width="{normalize-space(20 - $depth)}%"/>
<xsl:apply-templates select="node()" mode="fixTable">
<xsl:with-param name="depth" select="$depth"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="thead/tr/th[1]" mode="fixTable">
<xsl:param name="depth"/>
<xsl:copy>
<xsl:attribute name="colspan">
<xsl:value-of select="$depth"/>
</xsl:attribute>
<xsl:apply-templates select="@*|node()" mode="fixTable">
<xsl:with-param name="depth" select="$depth"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="tbody[../table]" mode="fixTable">
<xsl:param name="depth"/>
<xsl:copy>
<xsl:apply-templates select="@*|node()" mode="fixTable">
<xsl:with-param name="depth" select="$depth"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="tbody[not(../table)]" mode="fixTable">
<xsl:param name="depth"/>
<xsl:apply-templates select="@*|node()" mode="fixTable">
<xsl:with-param name="depth" select="$depth - 1"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="tbody/tr/th" mode="fixTable">
<xsl:param name="depth"/>
<xsl:if test="../../tr[1] = ..">
<td rowspan='{count(../..//tr)}'>&#xA0;</td>
</xsl:if>
<xsl:copy>
<xsl:attribute name="colspan">
<xsl:value-of select="$depth"/>
</xsl:attribute>
<xsl:apply-templates select="@*|node()" mode="fixTable">
<xsl:with-param name="depth" select="$depth"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="@*|node()" mode="fixTable">
<xsl:param name="depth"/>
<xsl:copy>
<xsl:apply-templates select="@*|node()" mode="fixTable">
<xsl:with-param name="depth" select="$depth"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment