Latest Posts

Archives [+]

Categories [+]

Authors [+]

Entries filed under 'javascript'

    Posted by Kas Thomas AUG 25, 2010

    Posted in ajax, crx, crx gems, development, javascript, rest and sling Comments 7

    In previous posts, I've shown how to load movie data into CRX and how to render data for individual movies via HTML, SVG, and PDF. What I'd like to do now is show how easy it is to build interactivity into an app using a bit of AJAX combined with Sling's support for RESTful XPath-based search.

    It turns out that all we have to do to query the repository for, say, all nodes that have a value of "Hitchcock" under the property named "Director" is put together an XPath expression like

    //*[jcr:contains(@Director,'hitchcock')]

    and pass it to Sling in a URL that looks like:

    http://localhost:7402/content.query.json?queryType=xpath&statement=//*[jcr:contains(@Director,'hitchcock')]

    (assuming the repository is on port 7402 of localhost). This request will invoke a Lucene search of all nodes stored under the /content subtree. The results will come back as a JSON-formatted array:

    [
        {
            "name": "notorious",
            "jcr:path": "/content/films/notorious",
            "jcr:score": 3331
        },
        {
            "name": "under_capricorn",
            "jcr:path": "/content/films/under_capricorn",
            "jcr:score": 3331
        },
                . . .
    ]


    This is perfect, because it means we can use the JSON data to populate a dropdown menu (a "select" control in an HTML form) showing the names of films; and we can arrange things so that when the user clicks a "Show Details" button, the form updates to show detail information (title, director, year, genre, actor, actress, etc.) for the film in question. To get the detail information, of course, we can perform a behind-the-scenes AJAX query to the server. I already showed, in a previous post, how to render detail information for a given movie in an HTML page. All we really need to do at this point is put that HTML page into its own iframe, and (right next to it) add search controls to the page.

    The following form shows one possible way of handling things.

    /content/ddc/blog/2010/08/crx_gems_interactiv0/jcr:content/par/image_0/file

     

    Basically, we have an HTML form in which there are two action buttons: One is a Search button ("Search Films by") that initiates an XPath-based search of the repository based on a user-chosen criterion of Title, Director, Year, Genre, Actor, or Actress. The other is a Show Details button, underneath a picklist of films. Clicking the Search button populates the picklist with hits. When the user chooses a hit from the list and clicks Show Details, the left side of the page updates with detail information.

    The form consists of 200 lines of JavaScript and markup, as follows:

    <html>
    <head>
    <script>

    var CRX_BASE_URL = "http://localhost:7402";

    function addEventListeners( ) {

            document.getElementById( "_Query_" ).addEventListener(
            "keypress", function( e ) {
                    if ( 13 == e.keyCode )
                    handleClick( null );
            },
            false );

            document.getElementById( "_QueryButton_" ).addEventListener(
            "click", handleClick, false );



            document.getElementById( "_Fetch_" ).addEventListener(
            "click", handleFetch, false );

    }

    function getSearchMode( ) {

            return document.getElementById("_Select_").value;
    }

    function handleFetch( e ) {

            var list = document.getElementById("_Hits_");

            if (list.value) {
                    var url =  CRX_BASE_URL + list.value + ".html";
                    var iframe = document.getElementsByTagName("iframe")[0];

                    // force a reload of the iframe:
                    iframe.src = url;
            }
    }

    // get user's input and call server
    function handleClick( e ) {

            var userData =
            document.getElementById( "_Query_" ).value;

            if ( !userData )
                return;   // nothing to do

            var CRX_QUERY_PATH = "/content.query.json?queryType=xpath&statement=";
            var GETheader = {
                    "Accept": "application/json",
            };

            var query = createXPathQuery( userData );
            var url = CRX_BASE_URL + CRX_QUERY_PATH + query;

            myHttpGet( url, GETheader, handleResponse ); // hit server
    }


    function myHttpGet( url, header, handler ) {

            try {
                    request = new XMLHttpRequest();
                    request.open("GET", url, true);
                    for (i in header)
                    request.setRequestHeader( i, header[i] );
                    request.onreadystatechange = handler;

                    request.send("");
            }
            catch(e ) {
                    alert("Problem sending request: " + e.toString());
            }
    }

    function handleResponse( ) {

            if (request.readyState == 4) {
                    showResults( request );
            }
    }

    function showResults( request )  {

            var json = request.responseText;

            var hits = eval ( json );

            if ( null == hits ) {
                    alert( "No hits were found." );
                    return;
            }

            display( hits );
    }


    function display( hits ) {

            var div = document.getElementById( "_Hits_" );

            if ( null == div )
                throw( "Problem getting div for hitlist." );

            showHitCount( hits.length );

            var markup = "";

            for (var i = 0; i < hits.length; i++) {
                    markup += "<option value=\"" + hits[i][ "jcr:path" ] + "\">";
                    markup += fixName( hits[i].name );
                    markup += "</option>";
            }
            div.innerHTML = markup;
    }

    function fixName( name ) {
            var tmp = name.split("_");
            for (var i = 0; i < tmp.length; i++)
                tmp[i] = capitalize( tmp[i] );
            return tmp.join(" ");
    }

    function capitalize(a) {

            return typeof a[0] == 'undefined'?
               "":a[0].toUpperCase() + a.substring(1);

    }

    function showHitCount( numberOfHits ) {
            var div = document.getElementById( "_hitcount_" );
            if ( null != div )
                div.innerHTML = ("Total hits: " + numberOfHits).italics();
    }

    // build xpath query url
    function createXPathQuery( userString ) {

            var xpathTerms = [];

            var querySemantics = " and ";

            // trim leading & trailing spaces off query
            var terms = userString.replace(/^\s+/,"").replace(/\s+$/,"");

            // split on whitespace
            terms = terms.split(/\s+/);

            var _queryBasis =  "//*[_#_]/@location";
            var mode = getSearchMode( );

            for ( var i = 0; i < terms.length; i++ )
                xpathTerms.push( "jcr:contains(@" + mode + ",'" + terms[i] + "')" );

            var query = _queryBasis.replace( '_#_', xpathTerms.join( querySemantics ) );

            return query;
    }

    </script>
    </head>


    <body onload="addEventListeners()">

    <iframe width="380" height="410" style="border:none" src="http://localhost:7402/content/films/wild_at_heart.html"></iframe>

    <span style="font-size:small;position:absolute;right:80px;top:7px;">
    <input type="text"     id="_Query_" size="25"/>
    <input type="button"   id="_QueryButton_" value="Search Films by:"/>

    <select id="_Select_">
    <option value="Title">Title</option>
    <option value="Director">Director</option>
    <option value="Actor">Actor</option>
    <option value="Actress">Actress</option>
    <option value="Year">Year</option>
    <option value="Subject">Genre</option>
    </select>
    <br/>

    <select id="_Hits_" size="10"></select><div id="_hitcount_"></div>
    <br/>
    <input type="button" id="_Fetch_" value="Show Details"/>


    </span>
    <div id="hitlist"></div>
    </body>
    </html>
     

    This form (movieForm.html), along with the data for 1700 films (and scripts and PDF files discussed in prior posts), is available in the zip file below, which can also be downloaded from Day Package Share. After installing the package, go to http://localhost:7402/apps/films/movieForm.html to see the form in action (assuming your CRX is on port 7402).

    * MovieApp-1.zip
    Sample code and data for MovieApp.

    Posted by Kas Thomas AUG 03, 2010

    Posted in crx, crx gems, dynamic languages and javascript Comments 4

    I recently showed a simple way to render CRX content as PDF. The trick is to use a PDF form as a container, into which form data is imported using XFDF (the latter being Adobe's XML Forms Data Format). Populating a PDF form this way is very easy. But the form we ended up with last time was rather static, with no user interactivity to speak of. It simply showed movie data in a predetermined format.

    What would be good is if the user could, say, choose a film title from a scrolling list, then see the form update (in real time) with data for the selected film title. This turns out to be fairly easy to do.

    In addition to text fields, buttons, checkboxes, and radio buttons, PDF forms support the notion of a list box. A list box can be initialized with

    list.setItems( [ [Item1, exportValue1], [Item2,

       exportValue2 ] ... ] );

    where Item1, Item2, etc. are the names of list items that will be seen by the user and exportValue1, exportValue2, etc. are the corresponding values that get sent to the server in a POST request. This is quite handy, because it means we can arrange things so that if the user selects "Terminator 2" from a list of films, the value that gets sent to the server is "terminator_2.xfdf." This is exactly what we want.

    Our new form, containing the list box, looks like this:

    /content/ddc/blog/2010/08/crx_gems_interactiv/jcr:content/par/image/file

     

    The list box (on the right) needs to be initialized with a list of film names (and corresponding export values) at form-load time. For that to happen, we need an onload script that does something like

    var list = this.getField( "list" );
    list.setItems( movies );


    where movies is an array of arrays (containing items and export values) per the previous code snippet (above). The question is how to get the data for movies into the form. Either this information has to be hardcoded into the form to begin with (which is certainly possible), or it has to come from the server. We've seen before that it is easy to pull data into a PDF form with XFDF. In this case, we have a script on the server that is dynamically generating our XFDF. We can set things up so that the script generates the array of movies (and export values) as part of the XFDF.

    Unfortunately, XFDF won't populate a list box directly. But what we can do is suck movie data into an invisible text field, then have our onload script slurp the data from the text field and programmatically populate the list box at onload time. The onload script looks like this:

    // if hiddenText data is in JSON format . . .
    movies = eval( this.getField( "hiddenText" ).value );

    // populate the list of movies
    var list = this.getField( "list" );
    list.setItems( movies.sort( ) );


    The Acrobat JavaScript API doesn't directly support a notion of onload events per se, but in practice all we have to do to get a script to execute at load time is attach it to the form as a document-level script. In Acrobat 9 Pro, you can create such a script using the Document JavaScripts command under Advanced > Document Processing.

    In addition to the onload script, our PDF form needs one additional script. Underneath the list box, we have a button that says "Click here to view details for the selected movie." When that button is clicked, we want a script to fire. The script will request the XFDF containing film data for the selected film (causing the form to repopulate). This script looks like:

    var f = this.getField("list");
    var a = f.currentValueIndices;
    selection= f.getItemAt(a, true);
    getURL( "/content/films/" + selection + ".xfdf" );


    The first line simply gets a reference to the list box. The second and third lines fetch the user's selection (whatever is highlighted in the list box). The fourth line sends a properly formed GET request to the server.

    Recall from last time that in our repository, we have content nodes under /content/films that look like:

    In my earlier post, I presented a script (xfdf.esp) that produces XFDF-formatted movie data. The script looked like this:

    <?xml version="1.0" encoding="UTF-8"?>
    <xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
    <f href="/apps/films/FilmSummary.pdf"/>
    <% response.setContentType("application/vnd.adobe.xfdf" ); %>
    <fields>
       
        <field name="Director">
            <value><%= currentNode.Director %></value>
        </field>
        <field name="Subject">
            <value><%= currentNode.Subject %></value>
        </field>
        <field name="Year">
            <value><%= currentNode.Year %></value>  
        </field>
        <field name="Title">
            <value><%= currentNode.Title %></value>
        </field>
        <field name="Length">
            <value><%= currentNode.Length %></value>
        </field>
        <field name="Actor">
            <value><%= currentNode.Actor %></value>
        </field>
        <field name="Actress">
            <value><%= currentNode.Actress %></value>
        </field>
        <field name="Popularity">
            <value><%= currentNode.Popularity %></value>
        </field>

    </fields>

    </xfdf>
     

    We can shorten this script quite a bit by creating the XML programmatically using E4X:

    fields = <fields/>;
    names = ["Title","Director","Subject",
        "Actor","Actress","Length",
        "Popularity","Year"];

    for (var i = 0; i < names.length;i++) {
       field = <field><value>{currentNode[names[i]]}</value></field>;
       field.@name = names[i];
       fields.* += field;
    }


    The second change to the script that we need to make is to add a section that generates the movie list that will go in the form's list box. The data needs to be an array of arrays, in JSON format, of the form [ [Title1, exportValue 1], [Title2, exportValue2] ... ]. Thus, we need to add the following block of code:

    parent = currentNode.getParent();
    allFilms = parent.getNodes();
    json = "[ ";
    for ( i in allFilms ) {
       nodeName = i;
       filmTitle = allFilms[i].Title;
       json += "[ \"" + filmTitle + "\",\"" +
        nodeName + "\"],"
    }
    json += "[] ]";
    field = <field><value>{ json }</value></field>;
    field.@name = "hiddenText";
    fields.* += field;



    The complete server-side script, xfdf.esp, now looks like this:

    <?xml version="1.0" encoding="UTF-8"?>
    <xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
    <f href="http://localhost:7402/apps/films/FilmSummary2.pdf"/>

    <%
    fields = <fields/>;
    names = ["Title","Director","Subject",
        "Actor","Actress","Length",
        "Popularity","Year"];

    for (var i = 0; i < names.length;i++) {
       field = <field><value>{currentNode[names[i]]}</value></field>;
       field.@name = names[i];
       fields.* += field;
    }

    parent = currentNode.getParent();
    allFilms = parent.getNodes();
    json = "[ ";
    for ( i in allFilms ) {
       nodeName = i;
       filmTitle = allFilms[i].Title;
       json += "[ \"" + filmTitle + "\",\"" +
       nodeName + "\"],";
    }
    json += "[] ]";
    field = <field><value>{ json }</value></field>;
    field.@name = "hiddenText";
    fields.* += field;

    %>

     <%=  fields.toXMLString() %>

    </xfdf>

     

    And that's all there is to it: two dozen lines of code on the server, and seven lines of code in the PDF form (a 3-line onload script plus a 4-line button click handler). That's all that's required to have an interactive PDF form-driven application on CRX.

    Of course, there's still room for improvement. The way things are now, this form reloads and repaints the listbox data as part of every roundtrip to the server. That's not necessarily the worst thing in the world to do (it causes about two seconds of extra latency), but it would be nice if we didn't have to reload all the movie titles with each request. It would also be nice if the user could look up movies based not just on the title, but on the director, year, starring actor, or other criteria. In other words, it would be nice to have a search-driven app. We'll tackle that in a future blog.

    Posted by Kas Thomas JUL 28, 2010

    Posted in crx, crx gems, development, dynamic languages, javascript and xml Comment 1

    I promised last time to show a simple way to render CRX content as PDF. The technique in question involves using a PDF form as the readymade container, into which form data is imported using XFDF. The latter is the XML version of Adobe's Forms Data Format, which in turn is a file format specifically designed to allow import and export of data to and from PDF forms.

    The way it works is simple: Suppose you have a PDF form that you want to populate with data. You merely need to create a small data file (in XFDF format) and put it on the server. When a user requests the data file (which has a mimetype of "application/vnd.adobe.xfdf"), Acrobat Reader (or the Reader browser plug-in) detects the fact that form data will need to be imported into a form. The XFDF file itself contains a pointer to the actual form to be used. Reader fetches the form, then imports the form data into it, and renders the result as a PDF file containing the data. It all happens transparently to the user, and the user need only have Acrobat Reader (not a full copy of Acrobat Professional).

    In the example I'm going to show below, we generate the XFDF file dynamically on the server, via a script called (what else?) xfdf.esp. We'll get to that in a minute.

    The example we're going to talk about assumes that there is content in CRX (under a path of /content/films) that looks something like this:

    This particular content node is named terminator_2. It lives under /content/films/ in my CRX repository.

    Notice, in the above list, that there is a property (at the bottom) called sling:resourceType, set to a value of "films." This tells CRX to look under /apps/films for any scripts that might be necessary to render the content.

    In previous blogs, I've shown how to write scripts that render this content as HTML, SVG, or CSV. Right now, what we need is an XFDF renderer. That turns out to be pretty easy to set up.

    First, we need to create a PDF form to hold our data. In the Acrobat Professional forms editor, such a form looks like this:

    FilmSummary.pdf

    Notice that there are text fields with names like Director, Subject, Year, Title, and so on. (These fields can be read-only, or editable; the example below will work fine either way.)

    I've given this form a name of FilmSummary.pdf and placed it in CRX under a path of /apps/films/.

    In order to populate the form, we need to be able to generate an XML file that conforms to Adobe's XFDF schema. The script that does this, xfdf.esp, is very straightforward:

    <?xml version="1.0" encoding="UTF-8"?>
    <xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
    <f href="/apps/films/FilmSummary.pdf"/>
    <% response.setContentType("application/vnd.adobe.xfdf" ); %>
    <fields>
       
        <field name="Director">
            <value><%= currentNode.Director %></value>
        </field>
        <field name="Subject">
            <value><%= currentNode.Subject %></value>
        </field>
        <field name="Year">
            <value><%= currentNode.Year %></value>  
        </field>
        <field name="Title">
            <value><%= currentNode.Title %></value>
        </field>
        <field name="Length">
            <value><%= currentNode.Length %></value>
        </field>
        <field name="Actor">
            <value><%= currentNode.Actor %></value>
        </field>
        <field name="Actress">
            <value><%= currentNode.Actress %></value>
        </field>
        <field name="Popularity">
            <value><%= currentNode.Popularity %></value>
        </field>

    </fields>

    </xfdf>
     
     

    Note that we use response.setContentType( ) to explicitly tell the browser that this is an XFDF file. Note also the <f> element near the top, which contains a pointer to our PDF form. The rest of the file is pretty much self-documenting.

    With xfdf.esp in the repository under /apps/films, we're now able to issue a browser request for http://localhost:7402/content/films/terminator_2.xfdf, and automatically the browser (with help from the Reader plug-in) will fetch the PDF form (FilmSummary.pdf) and merge our data into it, to produce a rendered view of:

    /content/ddc/blog/2010/07/crx_gems_rendering1/jcr:content/par/image_0/file

    About the only thing this PDF form lacks is interactivity. It would be nice to give the end-user a way to select a film from a list of titles, then have the form automatically populate with data for the requested film. And that's exactly what we'll tackle next time.

    Posted by Kas Thomas JUL 26, 2010

    Posted in crx, crx gems, development, dynamic languages, javascript and standards Comments 2

    I've shown how easy it is to push spreadsheet data into CRX (in such a way that there is one content node per row of data, where properties on that node correspond to column data). The reverse is also possible: It's easy to write a script that converts sibling nodes to row data formatted as CSV (comma-separated values per RFC 4180). Such a script, csv.esp, looks something like this:

    <%
    // Given a list of sibling nodes (presumably
    // similar in structure), and an array of
    // property names, convert each node
    // to one "row" of CSV data, where
    // columns correspond to properties.
    // We will encode all property data as
    // comma-separated values per RFC 4180.
    function nodesToCSV( nodes, propertyNames ) {

            var records = new Array( );

            for ( var i = 0; i < nodes.length; i++ ) {

                    var aRecord = new Array( );

                    // suck in the data for each property:
                    for ( var k = 0; k < propertyNames.length; k++ ) {
                            var data = nodes[ i ][ propertyNames[ k ] ];
                            var escaped = escapeData( data );
                            aRecord.push( escaped );
                    }
                    records.push( aRecord.join( "," ) );
            }

            var CRLF = String.fromCharCode(13) +
            String.fromCharCode(10);

            return records.join( CRLF );
    }

    // Return an array of property names for this node
    function getOrderedProperties( node ) {

            var array = new Array();
            for ( var i in node )
            array.push( i );

            return array;
    }

    // Escape field data per RFC 4180
    function escapeData( data ) {

            // replace " with ""
            data = String(data).replace( /"/g, "\"\"" );

            // if data contains comma, CRLF, or "
            // we need to wrap the entire thing in double quotes
            var escapables = /,|(\r\n)|"/;
            if ( data.match( escapables ) )
            return "\"" + data + "\"";

            return data;
    }
    %>
    <% nodes = currentNode.getNodes( );
    // get a list of property names
    propertyNames =
    getOrderedProperties( nodes[0] );%>
    <%= nodesToCSV( nodes, propertyNames ) %>
     

    The rules for escaping data for CSV are extremely simple. First, any data string that contains the double-quote (") character needs to have each such character converted to two double-quotes (""). Secondly, if the data contains a comma, the entire data string needs to be wrapped in quotation marks. The same is true for any data that contains double-quotes or line breaks (which RFC 4180 defines as CRLF -- carriage return followed by linefeed). The following very simple function enforces these escaping rules:

    // Escape field data per RFC 4180
    function escapeData( data ) {

       // replace " with ""
       data = String(data).replace( /"/g, "\"\"" );
     
       // if data contains comma, CRLF, or "
       // we need to wrap the entire thing in double quotes  
       var escapables = /,|(\r\n)|"/;
       if ( data.match( escapables ) )
          return "\"" + data + "\"";
          
       return data;
    }

    The function that actually converts nodes to records is very straightforward as well:

    function nodesToCSV( nodes, propertyNames ) {

       var records = new Array( );

       for ( var i = 0; i < nodes.length; i++ ) {

          var aRecord = new Array( );

          // suck in the data for each property:
          for ( var k = 0; k < propertyNames.length; k++ ) {
             var data = nodes[ i ][ propertyNames[ k ] ];
             var escaped = escapeData( data );
             aRecord.push( escaped );
         }
          records.push( aRecord.join( "," ) );
       }

       var CRLF = String.fromCharCode(13) +
                        String.fromCharCode(10);

       return records.join( CRLF );
    }

    Note that we need to explicitly provide the function a list of property names, rather than (say) let the function iterate through property names on an introspective basis. The reason for this is that if we simply try gathering property names with a for/in loop, we will get back property names in no particular order. And the order will, in fact, vary from content node to content node even if all of the content nodes have properties with exactly the same names. The unorderedness of the properties (as obtained through simple iteration) would scramble the column data in our CSV file. We don't want that. Hence, we pass in an array of property names, and march through the array in orderly fashion when pulling property data from each node.

    When I placed csv.esp in my repository under /apps/films and then navigated to http://localhost:7402/content/films.csv, CRX dutifully fired my script and produced a CSV file containing all of the data from my /films content nodes, causing my browser (in turn) to inform me that I was downloading a file of type "csv" (it then asked me what program I wanted to use to open the file; I specified scalc.exe, and OpenOffice dutifully loaded the file as a spreadsheet).

    So far, I've shown how to render /films data as HTML, SVG, and CSV. Next time, I want to show a simple trick for rendering the data as PDF. It's easier than you think!

    Posted by Kas Thomas JUL 22, 2010

    Posted in crx, crx gems, development, dynamic languages, javascript, rest and sling Add comment

    A few days ago, I talked about how to "shred and store" a spreadsheet -- i.e., how to push rows of a spreadsheet into individual nodes in CRX (one node per row, with column data stored as properties). I also gave JavaScript code for doing this in an OpenOffice macro. For testing purposes, I used the CSV file a1-film.csv, representing 1741 movies catalogued by Georgia Tech's College of Computing.

    After running my OpenOffice macro on the Georgia Tech CSV file, my CRX repository now contains movie data (Title, Director, Year, etc.) for 1741 films, each film with its own nt:unstructured node under the path /content/films/. In the CRX Content Explorer, a given node (in this case, the node at http://localhost:7402/content/films/terminator_2) looks something like this:

    /content/ddc/blog/2010/07/crx_gems_rendering/jcr:content/par/image_0/file

    Notice that the spreadsheet's column data now show up as properties (Actor, Actress, Director, etc.) with values like "Schwarzenegger, A.," "Hamilton, Linda," and so forth.

    Notice also that I've included a property of sling:resourceType, with a value of "films," for every movie node. This is important, because it tells Sling to look under /apps/films/ for any runtime scripts that may need to be applied in order to render a particular node (such as http://localhost:7402/content/films/terminator_2).

    Let's see how this works in practice. Suppose I want to render a movie node as HTML. I could put a file called html.esp under /apps/films/, containing the following markup:

    <html>
    <head>
    <link rel="stylesheet" type="text/css" href="/apps/films/films.css" />
    </head>
    <body>

    <img src="/apps/films/Film.png" width="95" height="92" />
    <br/>

    <span class="head"> <%= currentNode.Title %> </span><br/>

    <span class="normal">Director:&nbsp;&nbsp;&nbsp;</span>
    <span class="tdata"><%= currentNode.Director %></span><br/>

    <span class="normal">Year:&nbsp;&nbsp;&nbsp;</span>
    <span class="tdata"><%= currentNode.Year %></span><br/>

    <span class="normal">Genre:&nbsp;&nbsp;&nbsp;</span>
    <span class="tdata"><%= currentNode.Subject %></span><br/>

    <span class="normal">Actor:&nbsp;&nbsp;&nbsp;</span>
    <span class="tdata"><%= currentNode.Actor %></span><br/>

    <span class="normal">Actress:&nbsp;&nbsp;&nbsp;</span>
    <span class="tdata"><%= currentNode.Actress %></span><br/>

    <span class="normal">Runtime:&nbsp;&nbsp;&nbsp;</span>
    <span class="tdata"><%= currentNode.Length %> minutes</span><br/>

    <span class="normal">Popularity:&nbsp;&nbsp;&nbsp;</span>
    <span class="tdata"><%= currentNode.Popularity %></span><br/>

    </body>
    </html>

    As it turns out, I've also got a small PNG graphic, Film.png, located under /apps/films/, as well as a CSS file called (what else?) films.css, which looks like:

    .head {
    font-family:Tahoma;
    font-size:32pt;
    fill:#990000;
    color:#990000;
    }

    .normal {
    font-family:Tahoma;
    font-size:15pt;
    fill:#444444;
    }

    .tdata {
    font-family:Verdana;
    font-size:15pt;
    font-weight: bold;
    fill:#992222;
    color:#992222;
    }

    With these files in place, I can now direct my browser to go to http://localhost:7402/content/films/terminator_2.html, and Sling will automatically detect the need to use the html.esp script to render the node as HTML. The resulting rendition looks something like this:

    /content/ddc/blog/2010/07/crx_gems_rendering/jcr:content/par/image_1/file























    But suppose I want to be able to provide a Scalable Vector Graphics rendition, for browsers (like Firefox) that can render SVG. Not a problem: All I need to do is create a script called svg.esp and place it under /apps/films/. The svg.esp script might look something like this:

    <?xml version="1.0" standalone="no"?>
    <?xml-stylesheet type="text/css" href="/apps/films/films.css" ?>
    <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
    "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">

    <svg width="100%" height="100%" version="1.1"
    xmlns="http://www.w3.org/2000/svg"
    xmlns:xlink="http://www.w3.org/1999/xlink">

    <!--  Add a custom filter effect  -->
     <defs>
        <filter id="MyFilter" filterUnits="userSpaceOnUse" x="0" y="0" width="600" height="400">
          <feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur"/>
          <feOffset in="blur" dx="4" dy="4" result="offsetBlur"/>

          <feSpecularLighting in="blur" surfaceScale="5" specularConstant=".75"
       specularExponent="20" lighting-color="#992222"  
       result="specOut">
            <fePointLight x="-5000" y="-10000" z="20000"/>
          </feSpecularLighting>
          <feComposite in="specOut" in2="SourceAlpha" operator="in" result="specOut"/>
          <feComposite in="SourceGraphic" in2="specOut" operator="arithmetic"
       k1="0" k2="1" k3="1" k4="0" result="litPaint"/>
          <feMerge>
            <feMergeNode in="offsetBlur"/>
            <feMergeNode in="litPaint"/>
          </feMerge>

        </filter>
      </defs>

    <image x="20" y="20" width="95" height="92" xlink:href="/apps/films/Film.png"/>

    <!-- Apply the filter to Title -->
     <g filter="url(#MyFilter)" >
        <g  transform="matrix(1.15 0 0 1 0 0)"  >
          <text class="head" x="18" y="160" >
          <%= currentNode.Title %></text>      
        </g>
      </g>

    <text class="normal" x="20" y="200" >Director</text>
    <text class="tdata" x="200" y="200"><%= currentNode.Director %></text>

    <text class="normal" x="20" y="230" >Genre</text>
    <text x="200" y="230" class="tdata"><%= currentNode.Subject %></text>

    <text class="normal" x="20" y="260" >Year</text>
    <text x="200" y="260"  class="tdata"><%= currentNode.Year %></text>

    <text class="normal" x="20" y="290" >Actor</text>
    <text x="200" y="290" class="tdata"><%= currentNode.Actor %></text>

    <text class="normal" x="20" y="320" >Actress</text>
    <text x="200" y="320" class="tdata"><%= currentNode.Actress %></text>

    <text class="normal" x="20" y="350" >Runtime</text>
    <text x="200" y="350" class="tdata"><%= currentNode.Length %></text>

    <text class="normal" x="20" y="380" >Popularity</text>
    <text x="200" y="380" class="tdata"><%= currentNode.Popularity %></text>

    </svg>

    The big <defs> section near the beginning is an (optional) SVG filter effect, designed to provide a little extra visual appeal to the movie title by giving it a drop-shadow. The result (as rendered by Firefox) looks like this:

    /content/ddc/blog/2010/07/crx_gems_rendering/jcr:content/par/image_2/file

























    Of course, in a real-world component or application, you would have logic somewhere (whether on the server side or in client-side code) that detects the type of browser the user has and fetches the SVG rendition only if the user's browser is SVG-capable.