XSLT 2.0 transformation with Swift or WKWebView

I am re-writing with Xcode and Swift two applications (DDB Access, SmartHanzi) initially written with Xamarin and C#. Both apps are with separate macOS and iOS versions (storyboard).

In the original version, XSLT 2.0 transformations were applied with C#. With Swift and WKWebView, after carefully reviewing the documentation, I just found:

  • XSLT 1.0 transformation for macOS (Swift).
  • Nothing at all for iOS.

Some years ago, there seemed to be a possibility with an external C library, but it was also mentioned that at a moment it was no more accepted by the App Store.

Did I miss something in the documentation and how can I apply these XSLT 2.0 transformations (preferrably from an XML string but temporary files would be acceptable)?

Answered by DTS Engineer in 796019022

WKWebView supports the following APIs on both macOS and iOS:

XSLTProcessor - https://developer.mozilla.org/en-US/docs/Web/API/XSLTProcessor

DOMParser - https://developer.mozilla.org/en-US/docs/Web/API/DOMParser

Here is an example HTML file that includes JavaScript code for performing XSLT transformations to XML data. (Tested in a WKWebView on an iPhone 15 running iOS 17.5.1 (21F90)):


<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
  
  <title>XSLT Transform Example for iOS</title>
 
 <!-- some style elements added by the XSLT transform -->
 <style>
     .badword {
         border:1px solid rgb(0%,0%,80%);
         color:rgb(0%,0%,80%);
         font-size:1.2em;
         font-weight:bold;
         padding-left:3px;
         padding-right:3px;
         margin-left:2px;
         margin-right:2px;
         -webkit-border-radius:3px;
     }
     .customword {
         border:1px solid rgb(89%,71%,23%);
         color:rgb(80%,36%,12%);
         font-size:1.2em;
         font-weight:bold;
         padding-left:3px;
         padding-right:3px;
         margin-left:2px;
         margin-right:2px;
         -webkit-border-radius:3px;
     }
    </style>
      
</head>

<body>
    
  <h1>XSLT Transform Example for iOS</h1>
  
  <p>Testing <span class="badword">xslt</span> transforms.</p>
  <div id="contents_div">
      <p>document startup</p>
  </div>
  
</body>

<script type="text/javascript" language="JavaScript">
    
        /* a simple xslt style sheet
         -- note, I have added backslashes at at the end of lines for legibility 
         as they are stored in this example as JavaScript strings.  It is not
         necessary to store your XML/XSLT data as strings: you may also
         retrieve your XML/XSLT data through some other method.  */
    var theXSLTText = '\
    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">\
        <xsl:template match="/">\
            <xsl:for-each select="/matches/*">\
                <xsl:choose>\
                    <xsl:when test="name()=\'no\'">\
                        <span kind="no">\
                            <xsl:value-of select="."/>\
                        </span>\
                    </xsl:when>\
                    <xsl:when test="name() = \'bad\'">\
                        <span kind="bad" class="badword">\
                            <xsl:value-of select="."/>\
                        </span>\
                    </xsl:when>\
                    <xsl:when test="name() = \'custom\'">\
                        <span kind="custom" class="customword">\
                            <xsl:value-of select="."/>\
                        </span>\
                    </xsl:when>\
                </xsl:choose>\
            </xsl:for-each>\
        </xsl:template>\
    </xsl:stylesheet>\
    '
    
        /* some xml text to transform */
    theXMLText = '\
    <matches>\
        <no>this</no>\
        <bad> is </bad>\
        <no>a</no>\
        <custom> test </custom>\
        <no>of</no>\
        <bad> xslt </bad>\
        <no>.</no>\
    </matches>\
    '
    
        /* helper function for adding paragraphs*/
    function addPText(containerNode, textToAdd) {
        var generatedP = document.createElement("p")
        generatedP.appendChild(document.createTextNode(textToAdd))
        containerNode.appendChild( generatedP )
    }
    
    
    try {
            /* get a reference to the div for output */
        var containerNode = document.getElementById('contents_div')
        addPText(containerNode, 'starting transform')

            /* use a DOMParser to parse the xslt and xml text into DOM trees */
        var xml_parser = new DOMParser()
        var xslStylesheet = xml_parser.parseFromString( theXSLTText, 'text/xml' )
        var xmlDOMTree = xml_parser.parseFromString( theXMLText, 'text/xml' )

            /* import the parsed XSLT style sheet into an xslt processor */
        xsltProcessor = new XSLTProcessor()
        xsltProcessor.importStylesheet( xslStylesheet )

            /* transform the xml into a document fragment */
        var newDocumentFragment = xsltProcessor.transformToFragment( xmlDOMTree, document )
        
            /* announce start of results */
        addPText(containerNode, '- start of transform results -')

            /* add the transformed nodes from the xslt result
             to the html.  Here I destructively remove values, but
             you could also perform a traversal or some other algorithm. */
        while ( newDocumentFragment.hasChildNodes() ) {
                /* pop the first node */
            var theNode = newDocumentFragment.firstChild
            newDocumentFragment.removeChild(newDocumentFragment.firstChild)
                /* add it to the document */
            containerNode.appendChild( theNode )
        }
        
            /* announce end of results */
        addPText(containerNode, '- end of transform results -')

    }
    catch (e) {
        console.log('error ~ ' + e)
        var containerNode = document.getElementById('contents_div')
        addPText(containerNode, 'error ~ ' + e)
    }

</script>

</html>

Some years ago, there seemed to be a possibility with an external C library, but it was also mentioned that at a moment it was no more accepted by the App Store.

Can you say more about that? The open-source libxslt is XSLT 1.0 only. I'm curious to know what XSLT 2 library you're referring to.

The current version (Xamarin) uses a C# XSLT transform. It works correctly and I had no reason to care about the XSLT version. I don't know if the underlying library is 1.0 or 2.0.

  • Mac:

As far as I understand, Swift/macOS has an XSLT transform limited to 1.0. For an XSL 2.0 stylesheet, I get the message:

xsl:version: only 1.1 version features are supported

(So it's 1.1, not 1.0)

I don't know if the stylesheet is marked 2.0 because it does need 2.0 or with no specific reason. My plan is to make a try with what is available and check if the result is at least acceptable.

  • iPhone, iPad:

It seems that the Swift/macOS 1.0 or 1.1 stylesheet is not available for iOS/iPadOS.

At the moment, I have no solution at all for iPhone/iPad.

Hi endecotp, I realize that my post yesterday did not correctly answer your question. The "external library" I mentioned in the initial post is libxslt but I had not checked the details.

The main point for me is to find a solution for both macOS and iOS. At the moment, it is not clear if 2.0 is strictly required or if 1.0/1.1 would be acceptable.

WKWebView supports the following APIs on both macOS and iOS:

XSLTProcessor - https://developer.mozilla.org/en-US/docs/Web/API/XSLTProcessor

DOMParser - https://developer.mozilla.org/en-US/docs/Web/API/DOMParser

Here is an example HTML file that includes JavaScript code for performing XSLT transformations to XML data. (Tested in a WKWebView on an iPhone 15 running iOS 17.5.1 (21F90)):


<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1"/>
  
  <title>XSLT Transform Example for iOS</title>
 
 <!-- some style elements added by the XSLT transform -->
 <style>
     .badword {
         border:1px solid rgb(0%,0%,80%);
         color:rgb(0%,0%,80%);
         font-size:1.2em;
         font-weight:bold;
         padding-left:3px;
         padding-right:3px;
         margin-left:2px;
         margin-right:2px;
         -webkit-border-radius:3px;
     }
     .customword {
         border:1px solid rgb(89%,71%,23%);
         color:rgb(80%,36%,12%);
         font-size:1.2em;
         font-weight:bold;
         padding-left:3px;
         padding-right:3px;
         margin-left:2px;
         margin-right:2px;
         -webkit-border-radius:3px;
     }
    </style>
      
</head>

<body>
    
  <h1>XSLT Transform Example for iOS</h1>
  
  <p>Testing <span class="badword">xslt</span> transforms.</p>
  <div id="contents_div">
      <p>document startup</p>
  </div>
  
</body>

<script type="text/javascript" language="JavaScript">
    
        /* a simple xslt style sheet
         -- note, I have added backslashes at at the end of lines for legibility 
         as they are stored in this example as JavaScript strings.  It is not
         necessary to store your XML/XSLT data as strings: you may also
         retrieve your XML/XSLT data through some other method.  */
    var theXSLTText = '\
    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">\
        <xsl:template match="/">\
            <xsl:for-each select="/matches/*">\
                <xsl:choose>\
                    <xsl:when test="name()=\'no\'">\
                        <span kind="no">\
                            <xsl:value-of select="."/>\
                        </span>\
                    </xsl:when>\
                    <xsl:when test="name() = \'bad\'">\
                        <span kind="bad" class="badword">\
                            <xsl:value-of select="."/>\
                        </span>\
                    </xsl:when>\
                    <xsl:when test="name() = \'custom\'">\
                        <span kind="custom" class="customword">\
                            <xsl:value-of select="."/>\
                        </span>\
                    </xsl:when>\
                </xsl:choose>\
            </xsl:for-each>\
        </xsl:template>\
    </xsl:stylesheet>\
    '
    
        /* some xml text to transform */
    theXMLText = '\
    <matches>\
        <no>this</no>\
        <bad> is </bad>\
        <no>a</no>\
        <custom> test </custom>\
        <no>of</no>\
        <bad> xslt </bad>\
        <no>.</no>\
    </matches>\
    '
    
        /* helper function for adding paragraphs*/
    function addPText(containerNode, textToAdd) {
        var generatedP = document.createElement("p")
        generatedP.appendChild(document.createTextNode(textToAdd))
        containerNode.appendChild( generatedP )
    }
    
    
    try {
            /* get a reference to the div for output */
        var containerNode = document.getElementById('contents_div')
        addPText(containerNode, 'starting transform')

            /* use a DOMParser to parse the xslt and xml text into DOM trees */
        var xml_parser = new DOMParser()
        var xslStylesheet = xml_parser.parseFromString( theXSLTText, 'text/xml' )
        var xmlDOMTree = xml_parser.parseFromString( theXMLText, 'text/xml' )

            /* import the parsed XSLT style sheet into an xslt processor */
        xsltProcessor = new XSLTProcessor()
        xsltProcessor.importStylesheet( xslStylesheet )

            /* transform the xml into a document fragment */
        var newDocumentFragment = xsltProcessor.transformToFragment( xmlDOMTree, document )
        
            /* announce start of results */
        addPText(containerNode, '- start of transform results -')

            /* add the transformed nodes from the xslt result
             to the html.  Here I destructively remove values, but
             you could also perform a traversal or some other algorithm. */
        while ( newDocumentFragment.hasChildNodes() ) {
                /* pop the first node */
            var theNode = newDocumentFragment.firstChild
            newDocumentFragment.removeChild(newDocumentFragment.firstChild)
                /* add it to the document */
            containerNode.appendChild( theNode )
        }
        
            /* announce end of results */
        addPText(containerNode, '- end of transform results -')

    }
    catch (e) {
        console.log('error ~ ' + e)
        var containerNode = document.getElementById('contents_div')
        addPText(containerNode, 'error ~ ' + e)
    }

</script>

</html>

WKWebView supports the following APIs on both macOS and iOS: XSLTProcessor - https://developer.mozilla.org/en-US/docs/Web/API/XSLTProcessor

That’s not XSLT 2, is it?

(Is that available in JavascriptCore?)

Accepted Answer

I don't see an obvious way to figure out the version number from that API. But, I tried the above with the following example (from section 3.7 on this page https://www.w3.org/TR/xslt20/) and it seems to work ok:

    var theXSLTText2 = '\
    <html xsl:version="2.0"\
          xmlns:xsl="http://www.w3.org/1999/XSL/Transform"\
          xmlns="http://www.w3.org/1999/xhtml">\
      <head>\
        <title>Expense Report Summary</title>\
      </head>\
      <body>\
        <p>Total Amount: <xsl:value-of select="expense-report/total"/></p>\
      </body>\
    </html>\
    '
    
    var theXMLText2 = '\
    <expense-report>\
        <total>20</total>\
    </expense-report>\
    '

I'll see if I can dig up more information about exactly what version of the XSLT processor is being used inside of the WKWebView.

No, the XML processor and the XSLT processor are not imported into JavaScriptCore contexts as they are for the javascript contexts set up for web views (both macOS and iOS).

I guess the 2.0 in the version part is ignored in the above test. Here are the details about the xslt version supported on our platforms:

Apple OSes ship libxslt v1.1.35 plus security fixes, which is what is available through WKWebView. Even if we had the latest libxslt release (v1.1.42), only the XSLT 1.0 standard is supported. XSLT 2.0 is not supported.

So, if you need XSLT 2.0 support you will need to add your own support for that.

If you want to call libxml/libxslt routines from your application code (instead of JavaScript code inside of a WKWebView), you can link against libxslt and libxml in iOS and macOS Xcode projects. Use the Link Binary With Libraries section under Build Phases in your Xcode project settings to add those libraries to your projects.

Documentation for the XML/XSLT libraries can be found here (and other places on the web):

for libxml - http://xmlsoft.org/html/

for libxslt - https://gitlab.gnome.org/GNOME/libxslt/-/wikis/home

If you're calling these libraries from Swift, you will need to take special care when calling as they are written in C language. To get you started, there's a bunch of articles on C and Objective-C interoperability with Swift that you can find here:

https://developer.apple.com/documentation/swift/imported-c-and-objective-c-apis

The key is setting up a bridging header as described in this article:

https://developer.apple.com/documentation/swift/importing-objective-c-into-swift/

After that, the rest is just a matter of getting the parameters set correctly. For a head start on that, here are some APIs that I have found particularly useful (some code pulled from a sqlite project I had handy):

Use your Swift strings as C Strings:

withCString(_:)

eg:

try queryText.withCString { query_ptr in /* UnsafePointer<sqliteStatement>? */
    var sqliteStatement: OpaquePointer? = nil
    if sqlite3_prepare_v2(self.database_ref, query_ptr, 
              Int32(strlen(query_ptr)), &sqliteStatement, nil) == SQLITE_OK {

Convert your cStrings back into Swift strings:

init(cString:)

eg:

var myString:String = String(cString: <pointer to a c string from a call to the C library>)

Get a path to a file you can provide to a C function:

withUnsafeFileSystemRepresentation(_:)

eg:

adjustedURL.withUnsafeFileSystemRepresentation({ filePath in
    if sqlite3_open(filePath, &self.database_ref) == SQLITE_OK {...

If I try to summarize my understanding:

  1. In my case, it is worth trying the first solution with Webkit, which seems reasonably simple and safe. At this point, it is not clear for me if it is v1 or v2.

  2. libslt (version 1 only) can be used with a significant system work. Looking at the apparent lack of clear statement in the reference documentation, I can't regard it as safe in the medium term.

  3. If none of the above provides the expected result, I have to do my own development.

I downloaded your existing apps and all of the XSLT files are 1.0. Can you confirm that you are re-implementing the XSLT operations as 2.0? And can you confirm that you absolutely, desperately need XSLT 2.0 support and no other solution will suffice? XSLT 2.0 is usually only used via Java, in a server context, and only in a handful of very specific, XML-friendly industry domains.

This is the best reference that I've seen for XSLT version support: (https://stackoverflow.com/tags/xslt/info/)

I don't know what you mean about that external C library that is no longer acceptable in the App Store. The only reason I could think of would be a GPL-licensed project. If that's the case, then the App Store isn't the problem, it's the license. The GPL license is designed to prevent you from using the code with Apple platforms. Releasing such code in the App Store would be a violation of the license. That makes the app illegal and Apple doesn't allow illegal apps in the App Store.

From my casual inspection of your app bundles, it seems that XSLT may only be a tiny part of your apps. It doesn't sound like trying to support XSTL 2.0 is worth the effort. If there was something specific you need that XSLT 1.0 doesn't provide, maybe you could implement your own EXSLT function.

Anything that Apple provides is going to be XSLT 1.0. There really isn't any WebKit interface per se. I think that just refers to the standard XSLT HTML stylesheets that I think all web browsers support. There's no point in trying to do XSL via Javascript in WebKit. That's just a Rube Goldberg architecture.

My software makes very heavy use of XSLT and all of it is XSLT 1.0. You can do pretty much anything in XSLT 1.0. Certain things may be easier to do in XSLT 2.0, but the effort required to support that far exceeds the gain in convenience. And if you are willing to write some EXSLT, then there's virtually nothing that you can't do in XSLT 1.0.

Apple has some native XML and XSLT APIs available: (https://developer.apple.com/documentation/foundation/archives_and_serialization/xml_processing_and_modeling/)

Personally, I don't use Apple's APIs. The DOM processing APIs, which include the XSLT logic, are macOS-only. It was quite easy to implement my own, more powerful, wrappers around the lower-level APIs provided by libxml2 and libxslt for parsing XML and transforming XSLT. I do all of the XML generation logic on my own. I've only written EXSLT functions in Perl, where they are very easy. They are probably much more difficult to implement in C.

I certainly wouldn't consider Apple's Objective-C API, or the lower-level C APIs, to be "unsafe". They are C-based, so you have to be careful. You may get some minor memory leakage in instruments, but not enough to worry about. The biggest problem is that they are Mac-only and essentially forgotten. Apple doesn't pay much attention to them and neither do most app developers. Using the lower-level C APIs provides some insulation against potential deprecation and removal of the Apple wrapper APIs.

I'm not sure what you mean about a "clear statement in the reference documentation". XML, and by extension XSL, are not "popular" technologies, in the social sense. While they are widely used in many industries, you won't find much about them on the internet except as fodder for mocking older developers. There is very little overlap between XML and the practice of most app developers for Apple platforms. It is safe to assume that everything is XSLT 1.0 and will always remain so.

I wasn't aware that libxslt (and also libexslt apparently) is public on iOS; that's useful to know. (I've used XSLT a fair bit over the years but always on the server side.)

You really need to determine whether or not you need XSLT 2 functionality. I suspect that you don't, because a bit of googling finds StackOverflow answers saying that .NET doesn't support XSLT 2. If you actually do... I don't really know what to suggest, except fixing the XSLT to not need the version 2 features.

Otherwise, everything boils down to libxslt in the end. So your choice is between

  1. Swift -> Javascript -> C -> libxslt -> C -> Javascript -> Swift
  2. Swift -> C -> libxslt -> C -> Swift

Some of those steps will involve serialising / deserialising XML text to/from XML object representations.

Personally, I'd choose the second option. Swift/C interop is pretty good. If you've not done it before - now's your chance to learn! Using a WKWebView is a horrible hack IMO. The usual reason for using WKWebView is that it has JIT Javascript, which JavascriptCore doesn't have, giving a worthwhile performance boost (maybe 10X). But that's not an issue here, as the JIT would just call the native C libxslt to do the work, and you can call that directly.

Thank you for these comments and for taking the time to look at my bundles.

Just one of the XSLT style sheets, ddb.xsl in DDB Access, requests version 2, but it is a critical one. As I mentioned above, I don't know if there is a specific reason. I just couldn't ignore it. Clearly, the main point is that I had no solution at all for iOS.

Using JavaScript was the first answer from Apple. I had not thought of this kind of solution. Later on, Apple had a second thought and suggested to use xslt rather than JavaScript.

My reference to an external C library some years ago comes from a very old comment in Stackoverflow #462440 (cf. discussion in the bottom). I am neither an XSLT expert nor a system expert, and it seems that finding the appropriate answer was not immediate. I regard libxslt as "unsafe in the medium term" not for security reasons, but because as you mention XSLT is not frequently used in this environment. From an Apple perspective, it can easily be removed if needed.

My lookup (detail) pages are based on WKWebView. I could actually get a first result with the most complex style sheet (ddb.xsl) in a few hours, with just a few lines of code. The most difficult point was to use correctly callAsyncJavaScript. It would be difficult to use evaluateJavaScript for transparency reasons, while callAsyncJavaScript takes care of the transparency.

I shall try to progress on this basis. If it works correctly, I shall save a lot of time.

Thanks again for your help.

Sorry. I only looked at the other app. I missed that file. However, I looked at the file and, other than the version number, it looks like XSLT 1.0. I changed the version from "2.0" to "1.0" and ran it against some sample data posted by acmuller and it seems to work fine. I admit that I'm not familiar with XSLT 2.0. There may be some differences in behaviour even if the syntax is identical, but it seems like you can just change the version number back to "1.0".

Unfortunately, after looking more closely at this project, it seems much more problematic than I originally assumed. I thought this would be a straightforward code-level question involving one of my favourite technologies. I was wrong.

So, I don't know what to tell you. Symbol conflicts between your code and any private frameworks are easy to fix. Apple only officially supports the public APIs as documented on the Apple developer site. Internally, Apple may implement those APIs with various other 3rd party frameworks, but you can't use those. It's always possible to have a symbol conflict between Apple's private frameworks and your own code, but you can just rename the symbols in your code.

Going beyond code-level issues, I see a couple of potential problems getting the app published in the App Store for iOS. But I'm not Apple, so all I can do is speculate and that gets messy in a hurry.

From my perspective, this thread has effectively solved the XSLT problem, or at least clarified the situation.

Any suggestions on other topics are welcome, please be kind to contact me by mail (cf. the contact address on the SmartHanzi website, it is not accepted on this forum)

Thanks again for your help!

XSLT 2.0 transformation with Swift or WKWebView
 
 
Q