XQuery and HTML5

XQuery generated HTML5XQuery is amazing at generating server-side dynamic XHTML.  PHP, Java, .Net and the like are good too but don’t have the advantage of a seamless connection to a storage model.  However, they do hav a big advantage over XQuery when it comes to HTML5 because they can serialize non-XML compliant HTML text.

With XQuery and XHTML the following code works just fine:

xquery version "1.0";
xdmp:set-response-content-type("text/html; charset=UTF-8"),
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta name="description" content="Awesome" />
        <title>Title</title>
        <script type="text/javascript" src="js/app.js" />
...

The advantage of building your XHTML in XQuery is now we can stick this code in a “View” as part of a Model-View-Controller software pattern.  I can write the view once and reuse it as a code component throughout the project, or across multiple projects.  However HTML5, the latest and greatest web technology with tons of momentum in the mobile web arena, prefers serialization that looks something more like the following:

<!doctype html>
<html>
    <head>
        <meta content="description" content="Awesome">
        <title>Title</title>
        <script src="js/app.js">
...

This is a problem for XQuery.  The un-closed tags are invalid XML, so XQuery causes an evaluation error that looks something like:

XDMP-UNEXPECTED: (err:XPST0003) Unexpected token syntax error, unexpected $end, expecting EndTagOpen_

The problem is that HTML not only allows for un-closed tags like <script> and <meta>, but actually almost requires them.  I believe this traces back to the SGML origins of HTML.  The following HTML creation attempts in XQuery are not parsed correctly by browsers:

<script src="js/app.js"/>   or   <script src="js/app.js"></script>

Because the XQuery running in MarkLogic is running in the “default” XML serialization mode, both of the script tags above are collapsed to the self closed tag

<script src="js/app.js"/>

This is a problem for many browsers which will tend to include the rest of the HTML body as a child of the script tag, either causing the read of the javascript in the header no to be parsed or actually eating the rest of the page and never rendering the body DOM.  I have found two solutions: Either write your XQuery a non-empty script tags so that XQuery won’t collapse them:

<script src="js/app.js">&nbsp;</script>

or change the output serialization to HTML mode in the declare section at the top of your script:

declare option xdmp:output "method=html";

You’ll have to remember to do this for every module that generates HTML because other tags, like <textarea>, have the same pitfall.  You could also do this for a MarkLogic app server across the board by changing it’s settings in the admin console.  I prefer the declare and see it as just another reason to have a separate “View” section of your MVC app.

The next hurdle to tackle is getting inline javascript and JQuery plugins and templating libraries which like putting curly braces { } in HTML attribute values to work well.  I’ll leave that for another day.  Here is my current take at an XQuery HTML5 Boilerplate view library:

xquery version "1.0-ml";

module namespace v-h5bp = "http://framework/view/v-h5bp";

(:  Stick this at the top of any module that generates HTML so that
    empty tags don't get truncated to non-empty tags :)
declare option xdmp:output "method=html";

(:
    HTML5 Template
    @author Dave http://www.front2backdev.com
    XQuery adaptation Public Domain.
        html5 boilerplate: Public Domain
        jQuery: MIT/GPL license
        Modernizr: MIT/BSD license
        Respond.js: MIT/GPL license
        Normalize.css: Public Domain

    Basis of this code is the HTML5 BOILERPLATE project
    Checkout http://html5boilerplate.com/mobile
:)

(:
    HTML5 Mobile Boilerplate layout

    $title -- The html head title of the page
    $script -- extra HTML5 nodes for the html head
        carful with your self-closed tags like <script/>, these may not parse correctly
        in browsers.  Instead make sure XML serialization creates something like the following:
            <script src="url to source"></script>
        If you don't put something inside the tag, MarkLogic will serialize this to
            <script src="url to source"/>
        which is supported by xhtml but not html5 parsers
    $html  -- HTML5 nodes to put in the body
    $meta-description -- meta description text
    $meta-content -- meta conent text
    $goog-analytics-id -- your UA-* Id for google analytics
    $cache-version -- iterate this variable to ignore cached CSS and JS
:)
declare function v-h5bp:mbp-page-layout(
    $title as xs:string,
    $script as node()*,
    $html as node()*,
    $meta-description as xs:string?,
    $meta-content as xs:string?,
    $google-analytics-id as xs:string?,
    $cache-version as xs:double
) {

xdmp:set-response-content-type("text/html"),
'<!doctype html>',
<!-- Conditional comment for mobile ie7 http://blogs.msdn.com/b/iemobile/ -->,
<!--[if IEMobile 7 ]>    <html class="no-js iem7"> <![endif]-->,
<!--[if (gt IEMobile 7)|!(IEMobile)]><!-->, <html class="no-js"> <!--<![endif]-->
<head>
    <meta charset="utf-8"/>

    <title>{$title}</title>
    <meta name="description" content="{$meta-description}"/>
    <meta name="author" content="{$meta-content}"/>

    <meta name="HandheldFriendly" content="True"/>
    <meta name="MobileOptimized" content="320"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>

    <link rel="apple-touch-icon-precomposed" sizes="114x114" href="img/h/apple-touch-icon.png"/>
    <link rel="apple-touch-icon-precomposed" sizes="72x72" href="img/m/apple-touch-icon.png"/>
    <link rel="apple-touch-icon-precomposed" href="img/l/apple-touch-icon-precomposed.png"/>
    <link rel="shortcut icon" href="img/l/apple-touch-icon.png"/>

    <meta name="apple-mobile-web-app-capable" content="yes"/>
    <meta name="apple-mobile-web-app-status-bar-style" content="black"/>
    <script>/* <![CDATA[ */(function(a,b,c){if(c in b&&b[c]){var d,e=a.location,f=/^(a|html)$/i;a.addEventListener("click",function(a){d=a.target;while(!f.test(d.nodeName))d=d.parentNode;"href"in d&&(d.href.indexOf("http")||~d.href.indexOf(e.host))&&(a.preventDefault(),e.href=d.href)},!1)}})(document,window.navigator,"standalone")/* ]]> */</script>
    <link rel="apple-touch-startup-image" href="img/l/splash.png"/>

    <meta http-equiv="cleartype" content="on"/>
    <link rel="stylesheet" href="/css/style.css?v={$cache-version}"/>

    <script src="js/libs/modernizr-custom.js">&nbsp;</script>
    <script>/* <![CDATA[ */Modernizr.mq('(min-width:0)') || document.write('<script src="js/libs/respond.min.js">\x3C/script>')/* ]]> */</script>

    {$script}  

</head>

<body>

    {$html}

    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js">&nbsp;</script>
    <script>/* <![CDATA[ */window.jQuery || document.write('<script src="js/libs/jquery-1.6.2.min.js"><\/script>')/* ]]> */</script>

    <script src="js/script.js?v={$cache-version}"></script>
    <script src="js/mylibs/helper.js"></script>
    <script>/* <![CDATA[ */MBP.scaleFix();/* ]]> */</script>

    <script>
      var _gaq=[["_setAccount","{$google-analytics-id}"],["_trackPageview"]];
      (function(d,t){{var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.async=1;
        g.src=("https:"==location.protocol?"//ssl":"//www")+".google-analytics.com/ga.js";
        s.parentNode.insertBefore(g,s)}}(document,"script"));
    </script>

</body>
</html>

};

 


Updates:

In the effort of leaving around the best code samples possible I will likely be updating my posts as I learn better / cleaner ways of working with XQuery.  Here’s to progressive enhancement!

12/3/2011 : Using XQuery HTML output serialization to avoid empty tag collapsing.

12/4/2011 : Forgot helper script, better inlining of google analytics code.  Note double {{ and }} which XQuery serializes to a single { and } .