Friday, July 27, 2007

Transforming Mac OS X Property List (plist) XML Files for Easier Handling


If you've every dealt with a Mac OS X property list XML file (.plist), such as that produced when exporting an iTunes playlist file, then you've maybe noticed the structure is a pain to work with, especially with respect to XSL transformations.




Consider an example:




<plist version="1.0">
<dict>
<key>Major Version</key><integer>1</integer>
<key>Minor Version</key><integer>1</integer>
<key>Application Version</key><string>7.3.1</string>
...



The xpath expression to select the Application Version looks something like:




/plist/dict/string[preceding-sibling::key[1] = 'Application Version']



All those selectors using "preceding-sibling" are a pain to write and less-than-clear. I think it's helpful to first transform the plist into a more useful form first. What if we could make the above plist snippet look like the following:




<?xml version="1.0"?>
<plist version="1.0">
<dict>
<entry key="Major Version">
<integer>1</integer>
</entry>
<entry key="Minor Version">
<integer>1</integer>
</entry>
<entry key="Application Version">
<string>7.3.1</string>
</entry>
...



With this new form, we can now rewrite our selector as:




/plist/dict/entry[@key = 'Application Version']/string



It's more concise, easier to write, and clear about what is actual going on in the code. I like it. We can use XSL to transform any plist XML file into this new form. The transform is as follows:




<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:template match="/">
<xsl:apply-templates mode="element" />
</xsl:template>

<xsl:template mode="element" match="dict">
<xsl:copy>
<xsl:for-each select="@*">
<xsl:attribute name="{name()}">
<xsl:value-of select="." />
</xsl:attribute>
</xsl:for-each>
<xsl:apply-templates mode="dict" select="key" />
</xsl:copy>
</xsl:template>

<xsl:template mode="dict" match="key">
<xsl:element name="entry">
<xsl:attribute name="key">
<xsl:value-of select="." />
</xsl:attribute>
<xsl:apply-templates mode="element" select="following-sibling::*[1]" />
</xsl:element>
</xsl:template>

<xsl:template mode="element" match="*" >
<xsl:copy>
<xsl:for-each select="@*">
<xsl:attribute name="{name()}">
<xsl:value-of select="." />
</xsl:attribute>
</xsl:for-each>
<xsl:apply-templates mode="element" />
</xsl:copy>
</xsl:template>

</xsl:stylesheet>



In Mac OS X, you can apply this transform right at the command line with the xsltproc command.