XSLT Transformation of ICS to XML

An XSLT Template to Convert iCalendar Files to xCalendar Files

I wanted to find an XSLT template that would be able to parse an iCalendar ICS file into an XML format that would work well with the calendar application I had built with XSLT and Symphony. What I found was an XSLT template that converted XML to ICS. I found nothing that was able to reverse the process. So I created an XSLT template that can convert ICS to the xCalendar format.

There is one minor problem that I couldn’t get past, which was to create XML elements for the values of those properties that have multiple values, such as the rrule or category elements. I developed the following template with the help of Marc Liyanage’s TextXSLT application.

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

<xsl:output
    method="xml" 
    doctype-public="-//W3C//DTD XHTML 1.0 Strict//EN" 
    doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"
    omit-xml-declaration="yes"
    encoding="UTF-8" 
    indent="yes" />

<xsl:param name="ical-data-raw" select="/data/vcalendar"/>
<xsl:param name="ical-data" select="normalize-space($ical-data-raw)"/>
<xsl:param name="ical-properties" select="substring-before($ical-data,' BEGIN:VEVENT')"/>
<xsl:param name="ical-events">
<xsl:value-of select="concat('BEGIN:VEVENT ',substring-before(substring-after($ical-data,'BEGIN:VEVENT '),' END:VCALENDAR'))"/>
</xsl:param>

<xsl:template match="/">

    <h1>XSL Transformation of iCalendar ICS format to XHTML</h1>

    <h2>Individual iCalendar Events</h2>

    <h3>iCalendar to xCalendar XSL Transformation</h3>
    <xsl:call-template name="xcal-calendar">
        <xsl:with-param name="input" select="$ical-properties"/>
        <xsl:with-param name="type" select="'calendar'"/>
        <xsl:with-param name="output-format" select="'xcalendar'"/>
    </xsl:call-template>

</xsl:template>

<xsl:template name="xcal-calendar">
    <xsl:param name="input"/>
    <xsl:param name="type" select="'event'"/>
    <xsl:param name="output-format" select="'xcalendar'"/>
    <xsl:param name="ical-properties-data" select="$input"/>
    <xsl:param name="remaining-event-data" select="substring-after($input, $ical-properties-data)"/>
    <xsl:choose>
        <xsl:when test="$output-format = 'xcalendar'">
            <iCalendar>
                <vcalendar>
                    <xsl:call-template name="properties">
                        <xsl:with-param name="input" select="$ical-properties-data"/>
                        <xsl:with-param name="type" select="$type"/>
                        <xsl:with-param name="output-format" select="$output-format"/>
                    </xsl:call-template>
                    <xsl:call-template name="xcal-events">
                        <xsl:with-param name="input" select="$ical-events"/>
                        <xsl:with-param name="type" select="'event'"/>
                        <xsl:with-param name="output-format" select="$output-format"/>
                    </xsl:call-template>
                </vcalendar>
            </iCalendar>
        </xsl:when>
        <xsl:when test="$output-format = 'xcalendar-encoded'">
            <code><pre>
            <xsl:text>&amp;lt;iCalendar&amp;gt;&amp;#xd;</xsl:text>
            <xsl:text>&amp;lt;vcalendar&amp;#xd;</xsl:text>
            <xsl:call-template name="properties">
                <xsl:with-param name="input" select="$ical-properties-data"/>
                <xsl:with-param name="type" select="$type"/>
                <xsl:with-param name="output-format" select="$output-format"/>
            </xsl:call-template>
            <xsl:text>&amp;gt;&amp;#xd;</xsl:text>
            <xsl:call-template name="xcal-events">
                <xsl:with-param name="input" select="$ical-events"/>
                <xsl:with-param name="type" select="'event'"/>
                <xsl:with-param name="output-format" select="$output-format"/>
            </xsl:call-template>
            <xsl:text>&amp;lt;/vcalendar&amp;gt;&amp;#xd;</xsl:text>
            <xsl:text>&amp;lt;/iCalendar&amp;gt;&amp;#xd;</xsl:text>
            </pre></code>
        </xsl:when>
    </xsl:choose>
    <xsl:if test="$remaining-event-data != ''">
        <xsl:call-template name="xcal-calendar">
            <xsl:with-param name="input" select="$remaining-event-data"/>
            <xsl:with-param name="type" select="$type"/>
            <xsl:with-param name="output-format" select="$output-format"/>
        </xsl:call-template>
    </xsl:if>
</xsl:template>

<xsl:template name="xcal-events">
    <xsl:param name="input"/>
    <xsl:param name="type" select="'event'"/>
    <xsl:param name="output-format" select="'xcalendar'"/>
    <xsl:param name="ical-event-data" select="concat('BEGIN:VEVENT ',substring-before(substring-after($input,'BEGIN:VEVENT '),' END:VEVENT'),' END:VEVENT')"/>
    <xsl:param name="remaining-event-data" select="substring-after($input, $ical-event-data)"/>
    <xsl:choose>
        <xsl:when test="$output-format = 'xcalendar'">
            <vevent>
                <xsl:call-template name="properties">
                    <xsl:with-param name="input" select="$ical-event-data"/>
                    <xsl:with-param name="type" select="$type"/>
                    <xsl:with-param name="output-format" select="$output-format"/>
                </xsl:call-template>
            </vevent>
        </xsl:when>
        <xsl:when test="$output-format = 'xcalendar-encoded'">
            <xsl:text>&amp;#x9;&amp;lt;vevent&amp;gt;&amp;#xd;</xsl:text>
                <xsl:call-template name="properties">
                    <xsl:with-param name="input" select="$ical-event-data"/>
                    <xsl:with-param name="type" select="$type"/>
                    <xsl:with-param name="output-format" select="$output-format"/>
                </xsl:call-template>
            <xsl:text>&amp;#x9;&amp;lt;/vevent&amp;gt;&amp;#xd;</xsl:text>
        </xsl:when>
        <xsl:when test="$output-format = 'hcalendar'">
        </xsl:when>
    </xsl:choose>
    <xsl:if test="$remaining-event-data != ''">
        <xsl:call-template name="xcal-events">
            <xsl:with-param name="input" select="$remaining-event-data"/>
            <xsl:with-param name="type" select="$type"/>
            <xsl:with-param name="output-format" select="$output-format"/>
        </xsl:call-template>
    </xsl:if>
</xsl:template>

<xsl:template name="properties">
    <xsl:param name="input" select="$ical-data"/>
    <xsl:param name="type" select="'event'"/>
    <xsl:param name="output-format" select="'xcalendar'"/>
    <xsl:param name="property-and-attribute-caps" select="substring-before($input,':')"/>
    <xsl:param name="property-and-attribute" select="translate($property-and-attribute-caps,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')"/>
    <xsl:param name="property">
        <xsl:choose>
            <xsl:when test="contains($property-and-attribute,';')">
                <xsl:value-of select="substring-before($property-and-attribute,';')"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$property-and-attribute"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:param>
    <xsl:param name="property-attribute" select="substring-after($property-and-attribute,';')"/>
    <xsl:param name="property-attribute-name" select="substring-before($property-attribute,'=')"/>
    <xsl:param name="property-attribute-value" select="substring-after($property-attribute,'=')"/>
    <xsl:param name="string-after-property" select="substring-after($input,':')"/>
    <xsl:param name="next-string" select="substring-before($string-after-property,':')"/>
    <xsl:param name="reverse-next-string">
        <xsl:call-template name="reverse">
            <xsl:with-param name="input" select="$next-string"/>
        </xsl:call-template>
    </xsl:param>
    <xsl:param name="reverse-next-property" select="substring-before($reverse-next-string,' ')"/>
    <xsl:param name="next-property">
        <xsl:call-template name="reverse">
            <xsl:with-param name="input" select="substring-before($reverse-next-string,' ')"/>
        </xsl:call-template>
    </xsl:param>
    <xsl:param name="string-before-next-property" select="substring-before($next-string, concat(' ',$next-property))"/>
    <xsl:param name="plural-values" select="contains($string-before-next-property,';')"/>
    <xsl:param name="multiple-items">
        <xsl:if test="$plural-values">
            <xsl:value-of select="translate($string-before-next-property,'ABCDEFGHIJKLMNOPQRSTUVWXYZ','abcdefghijklmnopqrstuvwxyz')"/>
        </xsl:if>
    </xsl:param>
    <xsl:param name="items">
        <xsl:call-template name="items">
            <xsl:with-param name="input" select="$multiple-items"/>
            <xsl:with-param name="output-format" select="$output-format"/>
        </xsl:call-template>
    </xsl:param>
    <xsl:param name="value">
        <xsl:choose>
            <xsl:when test="$next-property != '' and contains($string-before-next-property,';') and $output-format = 'xcalendar-encoded'">
                <xsl:value-of select="$items"/>
            </xsl:when>
            <xsl:when test="$next-property != ''">
                <xsl:value-of select="$string-before-next-property"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$string-after-property"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:param>
    <xsl:param name="remaining-string" select="substring-after($string-after-property,concat($value,' '))"/>
    <xsl:if test="$property != 'begin' and $property != 'end'">
        <xsl:choose>
            <xsl:when test="$type = 'event'">
                <xsl:choose>
                    <xsl:when test="$output-format = 'xcalendar'">
                        <xsl:choose>
                            <xsl:when test="$property-attribute-name = 'value'">
                                <xsl:element name="{$property}">
                                    <xsl:attribute name="{$property-attribute-name}"><xsl:value-of select="$property-attribute-value"/></xsl:attribute>
                                    <xsl:value-of select="$value"/>
                                </xsl:element>
                            </xsl:when>
                            <xsl:when test="$plural-values and $output-format = 'xcalendar'">
                                <xsl:element name="{$property}">
                                    <xsl:copy-of select="$items"/>
                                </xsl:element>
                            </xsl:when>
                            <xsl:otherwise>
                                <xsl:element name="{$property}">
                                    <xsl:value-of select="$value"/>
                                </xsl:element>
                            </xsl:otherwise>
                        </xsl:choose>
                    </xsl:when>
                    <xsl:when test="$output-format = 'xcalendar-encoded'">
                        <xsl:choose>
                            <xsl:when test="$property-attribute-name = 'value'">
                                <xsl:text>&amp;#x9;&amp;#x9;&amp;lt;</xsl:text>
                                <xsl:value-of select="$property"/>
                                <xsl:text> </xsl:text>
                                <xsl:value-of select="$property-attribute-name"/>
                                <xsl:text>="</xsl:text>
                                <xsl:value-of select="$property-attribute-value"/>
                                <xsl:text>"&amp;gt;</xsl:text>
                                <xsl:value-of select="$value"/>
                                <xsl:text>&amp;lt;/</xsl:text>
                                <xsl:value-of select="$property"/>
                                <xsl:text>&amp;gt;&amp;#xd;</xsl:text>
                            </xsl:when>
                            <xsl:otherwise>
                                <xsl:text>&amp;#x9;&amp;#x9;&amp;lt;</xsl:text>
                                <xsl:value-of select="$property"/>
                                <xsl:text>&amp;gt;</xsl:text>
                                <xsl:value-of select="$value"/>
                                <xsl:text>&amp;lt;/</xsl:text>
                                <xsl:value-of select="$property"/>
                                <xsl:text>&amp;gt;&amp;#xd;</xsl:text>
                            </xsl:otherwise>
                        </xsl:choose>
                    </xsl:when>
                </xsl:choose>
            </xsl:when>
            <xsl:when test="$type = 'calendar'">
                <xsl:choose>
                    <xsl:when test="$output-format = 'xcalendar'">
                        <xsl:attribute name="{$property}"><xsl:value-of select="$value"/></xsl:attribute>
                    </xsl:when>
                    <xsl:when test="$output-format = 'xcalendar-encoded'">
                        <xsl:text>&amp;#x9;</xsl:text>
                        <xsl:value-of select="$property"/>
                        <xsl:text>="</xsl:text>
                        <xsl:value-of select="$value"/>
                        <xsl:text>"</xsl:text>
                    </xsl:when>
                </xsl:choose>
            </xsl:when>
        </xsl:choose>
    </xsl:if>
    <xsl:if test="$remaining-string != ''">
        <xsl:if test="$property != 'begin' and $property != 'end' and $output-format = 'xcalendar-encoded' and $type = 'calendar'">
            <xsl:text>&amp;#xd;</xsl:text>
        </xsl:if>
        <xsl:call-template name="properties">
            <xsl:with-param name="input" select="$remaining-string"/>
            <xsl:with-param name="type" select="$type"/>
            <xsl:with-param name="output-format" select="$output-format"/>
        </xsl:call-template>
    </xsl:if>
</xsl:template>


<xsl:template name="items">
    <xsl:param name="input"/>
    <xsl:param name="items" select="$input"/>
    <xsl:param name="output-format" select="'xcalendar'"/>
    <xsl:param name="delimiter" select="';'"/>
    <xsl:param name="item-and-value">
        <xsl:choose>
            <xsl:when test="contains($items,$delimiter)">
                <xsl:value-of select="substring-before($items,$delimiter)"/>
            </xsl:when>
            <xsl:otherwise>
                <xsl:value-of select="$items"/>
            </xsl:otherwise>
        </xsl:choose>
    </xsl:param>
    <xsl:param name="remaining-items" select="substring-after($items,$delimiter)"/>
    <xsl:param name="item" select="substring-before($item-and-value,'=')"/>
    <xsl:param name="value" select="substring-after($item-and-value,'=')"/>
    <xsl:choose>
        <xsl:when test="$output-format = 'xcalendar'">
            <item class="{$item}">
                <xsl:value-of select="$value"/>
            </item>
        </xsl:when>
        <xsl:when test="$output-format = 'xcalendar-encoded'">
            <xsl:text>&amp;#xd;&amp;#x9;&amp;#x9;&amp;#x9;&amp;lt;</xsl:text>
            <xsl:value-of select="$item"/>
            <xsl:text>&amp;gt;</xsl:text>
            <xsl:value-of select="$value"/>
            <xsl:text>&amp;lt;/</xsl:text>
            <xsl:value-of select="$item"/>
            <xsl:text>&amp;gt;</xsl:text>
        </xsl:when>
    </xsl:choose>
    <xsl:if test="$remaining-items != ''">
        <xsl:call-template name="items">
            <xsl:with-param name="input" select="$remaining-items"/>
            <xsl:with-param name="output-format" select="$output-format"/>
        </xsl:call-template>
    </xsl:if>
    <xsl:if test="$remaining-items = ''">
        <xsl:text>&amp;#xd;&amp;#x9;&amp;#x9;</xsl:text>
    </xsl:if>
</xsl:template>


<xsl:template name="reverse">
    <xsl:param name="input"/>
    <xsl:variable name="length" select="string-length($input)"/>
    <xsl:choose>
        <xsl:when test="$length &amp;lt; 2">
            <xsl:value-of select="$input"/>
        </xsl:when>     
        <xsl:when test="$length = 2">
            <xsl:value-of select="substring($input,2,1)"/>
            <xsl:value-of select="substring($input,1,1)"/>
        </xsl:when>     
        <xsl:otherwise>
            <xsl:variable name="middle" select="floor($length div 2)"/>
            <xsl:call-template name="reverse">
                <xsl:with-param name="input" select="substring($input,$middle + 1,$middle + 1)"/>
            </xsl:call-template>
            <xsl:call-template name="reverse">
                <xsl:with-param name="input" select="substring($input,1,$middle)"/>
            </xsl:call-template>
        </xsl:otherwise>        
    </xsl:choose>
</xsl:template>

</xsl:stylesheet>

29 July 2007 by Stephen Bau