Web caching, Part 3: Database Integration
XSL: create optional class attributes in HTML output
<xsl:variable name="class">Please note the following:
<xsl:if test="position() = 1"> FirstInSeries</xsl:if>
<xsl:if test="position() = last()"> LastInSeries</xsl:if>
</xsl:variable>
<xsl:if test="$class != ''">
<xsl:attribute name="class">
<xsl:value-of select="substring-after($class,' ')" />
</xsl:attribute>
</xsl:if>
- Each value in the list has a leading whitespace. The list values are thus properly separated, but the result has an extra leading whitespace.
- The last xsl:if instruction tests if the class attribute is needed (detailed description of this trick).
- The substring-after function removes the leading whitespace, resulting in the desired value for the class attribute.
XSL: avoid empty attributes in output stream
To avoid this problem, capture the conditional results into a variable and create the attribute only if the variable is non-empty, for example:
<xsl:variable name="style">
<xsl:if test="w:rPr/w:i">font-style: italic;;</xsl:if>
<xsl:if test="w:rPr/w:b">font-weight: bold;</xsl:if>
</xsl:variable>
<xsl:if test="$style != ''">
<xsl:attribute name="style"><xsl:value-of select="$style" /></xsl:attribute>
</xsl:if>
XSL: transform an element only if it's a descendant of another element
<xsl:template match="w:p[ancestor::w:body]">There is (as always) a much more elegant solution:
<xsl:template match="w:body//w:p">
XSL: Detect first-of-type element in a list
<?xml version="1.0" encoding="UTF-8" ?>... you might want to process the first editor in a slightly different manner. There are two simple solutions:
<list>
<author>John Brown</author>
<author>Jim Small</author>
<editor>Jane Doe</editor>
<editor>Grace Kelly</editor>
</list>
(A) Write two transformation rules, one for the first element, one for the remaining ones:
<xsl:template match="editor[1]">(B) Use preceding-sibling axis to check whether an element is the first of its type:
<!-- transform the first editor in list -->
</xsl:template>
<xsl:template match="editor">
<!-- transform the remaining editor elements -->
</xsl:template>
<xsl:template match="editor">You should use the first method when the handling of the first element is radically different from the rest and the second one when you only need to set a few attributes or write a lead-in text.
<xsl:if test="not(preceding-sibling::editor)">Editors:</xsl:if>
<xsl:value-of select="text()" /> (<xsl:value-of select="position()" />)
</xsl:template>
IE7 breaks some AJAX libraries
if (IE) {... Internet Explorer 7 will constantly complain about the page trying to use ActiveX controls (at least IE6 was silent unless you've disabled ActiveX in which case AJAX broke anyway). The proper way to deal with this particular quirk of IE is to test for window.XMLHttpRequest first (instead of relying on browser type) and use ActiveX only if needed.
x = new ActiveXObject("Microsoft.XMLHTTP")
} else {
x = new XMLHttpRequest();
}
Note: If you use Sarissa (which broke my AJAX application), download the latest version, it contains all relevant IE7 fixes.
XPath position() function returns unexpected results
<?xml version="1.0" encoding="UTF-8" ?>... processed with a simple XML stylesheet ...
<list>
<author>John Brown</author>
<editor>Jane Doe</editor>
<author>Jim Small</author>
<editor>Grace Kelly</editor>
</list>
<?xml version="1.0" encoding="utf-8" ?>... returns surprising results:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="text" />
<xsl:template match="list">
Results: <xsl:apply-templates />
</xsl:template>
<xsl:template match="author|editor">
<xsl:value-of select="text()" /> (<xsl:value-of select="position()" />)
</xsl:template>
</xsl:stylesheet>
Results:The reason for this unexpected behavior are whitespace text nodes between the author and editor elements which are also counted by the position() function. To skip them, either create XML documents without extra whitespace or use more specific xsl:apply-template statements, for example:
John Brown (2)
Jane Doe (4)
Jim Small (6)
Grace Kelly (8)
<xsl:template match="list">The select="*" option in the last example selects only child nodes of the current XML node and thus skips over text fragments (including whitespace nodes).
Results: <xsl:apply-templates select="*"/>
</xsl:template>
Web Caching, Part 2: Reduce the Download Time
When properly implemented, this solution can drastically reduce the amount of information downloaded to visitor's browser, thus increasing the overall responsiveness of your web application.
Web Caching, Part 1: Explicit Content Expiration
Flash and Search Engines
Detect included libraries in ASP
To work around this problem, I wrote a small routine that checks for existence of prerequisite functions and throws an exception documenting what went wrong.
'
' CheckIncludedLibrary - checks if the incRoutine is available,
' otherwise throws an exception documenting that srcLibrary
' needs incLibrary
'
Sub CheckIncludedLibrary(incRoutine,srcLibrary,incLibrary)
Dim routineRef
On Error Resume Next
Set routineRef = GetRef(incRoutine)
If Err.Number <> 0 Then On Error Goto 0 : _
Err.Raise vbObjectError+1,srcLibrary, _
"You have to include " & incLibrary
End Sub
You would use this routine in a way similar to the example below:
'
' This library requires inclusion of /forms/xmlLibrary.asp,
' /forms/lib/editLibrary.asp and /asp/incPostingPreview.asp
'
CheckIncludedLibrary "NewXMLTextElement", _
"listRoutesLibrary","/forms/xmlLibrary"
CheckIncludedLibrary "EnrichElementText", _
"listRoutesLibrary","/forms/lib/editLibrary"
CheckIncludedLibrary "PostToPreview", _
"listRoutesLibrary","/asp/incPostingPreview"
“You've asked for it” series
So, to help my fellow programmers, I've started a series of “You've asked for it” posts answering the questions that brought many of you to my site in the first place (and, don't forget, you can always send me an interesting question with the Send a message link on my bio page.
Use XSLT to generate RSS item description
RSS specifications do not allow HTML markup in title or description elements. If you want to include HTML markup in RSS elements, it has to be quoted, for example
The following XSLT templates solve the problem: call the outputQuotedTree template in the context of input node containing HTML markup and it will generate quoted contents of its child nodes.
<xsl:template name="outputQuotedTree">
<xsl:for-each select="node() ¦ text()">
<xsl:call-template name="outputTextNode" />
</xsl:for-each>
</xsl:template>
<xsl:template name="outputTextNode">
<xsl:choose>
<xsl:when test="name() = ''">
<xsl:value-of select="." />
</xsl:when>
<xsl:otherwise>
<!-- Emit the opening tag -->
<xsl:text><</xsl:text><xsl:value-of select="name()" />
<!-- Emit the attributes of the opening tag -->
<xsl:for-each select="@*">
<xsl:text> </xsl:text>
<xsl:value-of select="name()"/><xsl:text>='</xsl:text>
<xsl:value-of select="."/>
<xsl:text>'</xsl:text>
</xsl:for-each>
<xsl:choose>
<xsl:when test="node() ¦ text()">
<!-- If there are children, close the start tag and
process the children -->
<xsl:text>></xsl:text>
<xsl:for-each select="node() ¦ text()">
<xsl:call-template name="outputTextNode" />
</xsl:for-each>
<!-- Emit the closing tag -->
<xsl:text></</xsl:text>
<xsl:value-of select="name()" />
<xsl:text>></xsl:text>
</xsl:when>
<xsl:otherwise>
<!-- No children, emit the self-closing tag -->
<xsl:text>/></xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Centering CSS-based layouts, Detecting text resize and more
- The article by Kris Hadlock has some great tips on creating CSS-based window-heigth or centered content.
- This article explains how to detect browser font size changes.
- CSS support in various browsers
- The anatomy of an RSS feed
MSXML firstChild property in VBScript
Windows that resize to fit their content
Another way of writing less-than
if (window.libraryVersion > minimumVersion) ...... I wrote ...
if (Math.max(window.libraryVersion,minimumVersion) == minimumVersion) ...
Embedding JavaScript in XSL documents
Apart from migrating all JavaScript code in a separate .js file (which is the best solution anyway) or using tricks to avoid < and & in the JavaScript code, the only way to make things work (at least in some modern XHTML-aware browsers) is to structure the web page to be XHTML-compliant with proper <!DOCTYPE> declaration and <html xmlns="http://www.w3.org/1999/xhtml"> root element. When the browsers recognize a page as XHTML-compliant page, they might be willing to transform < escape within the <script> tag into less-than sign (works for Opera, but not for Firefox).
Reading the XSLT-related standards, one would get an impression that using the <xsl:output method="html" /> command should force the transformation engine to write pure text instead of XML escapes within the <script> tag. However, it's almost impossible to force all the XSLT transformation engines being used today to do it properly (for example, it looks like MSXML engine on the IIS server simply ignores it).
Setting text property of MSXML Node object destroys its children
Manipulating innerHtml disables event handlers set with JavaScript
For example, if you set up onclick handler like this:
<div id="outerDiv">... the onclick handler will stop working if you change innerHtml of outerDiv, for example to add another paragraph:
<p id="para1">Click here</p>
</div>
<script>
var x = document.getElementById("para1");
x.onclick = function () { alert("Clicked"); }
</script>
<script>
var outerDiv = document.getElementById("outerDiv") ;
outerDiv.innerHtml += "<p id='para2'>Another paragraph</p>" ;
</script>
Browser-independent Object Fading
Detecting cached CSS style sheets
<script>if (xHasStyleSheets() && ! xHasStyleSelector('.myClass')) location.reload(true);</script>
Detecting out-of-date JavaScript libraries
You might solve this problem by renaming the JavaScript libraries (.js files) every time you make a significant change that could break the client-side application. While being bullet-proof, this approach requires republishing of all pages referencing the renamed library (which could be your entire web site).
It's way better to include the cache-checking code in your application:
- Within your library, set a global variable (or a property of window object) to library's version number. The version number could be an increasing number, the date the change was made or (ideally) a version number from your source code control system (SCCS for Unix fans or Visual SourceSafe for Windows)
- In every web page that relies on particular functionality of that library, check the version number and force a reload if there's a mismatch.
window.version_MyLibrary = 20060810; // library last changed on August 10th, 2006
<script>if (window.version_MyLibrary < 20060810) location.reload(true);</script>
Browser-independent style sheet Javascript code
xHasStyleSelector('.menuButton') will return true if any of the stylesheets attached to the document (including the inline stylesheets) contain definition of .menuButton.
// Copyright 2006 Ivan Pepelnjak (www.zaplana.net)
// Distributed under the terms of the GNU LGPL
// xGetStyleSheetFromLink - extracts style sheet object from the HTML LINK object (IE vs. DOM CSS level 2)
//
function xGetStyleSheetFromLink(cl) { return cl.styleSheet ? cl.styleSheet : cl.sheet; }
// xGetCSSRules - extracts CSS rules from the style sheet object (IE vs. DOM CSS level 2)
//
function xGetCSSRules(ss) { return ss.rules ? ss.rules : ss.cssRules; }
// xHasStyleSheets - checks browser support for stylesheet related objects (IE or DOM compliant)
//
function xHasStyleSheets() {
return document.styleSheets ? true : false ;
}
//
// xTraverseStyleSheet (stylesheet, callback)
// traverses all rules in the stylesheet, calling callback function on each rule.
// recursively handles stylesheets imported with @import CSS directive
// stops when the callback function returns true (it has found what it's been looking for)
//
// returns:
// undefined - problems with CSS-related objects
// true - callback function returned true at least once
// false - callback function always returned false
//
function xTraverseStyleSheet(ss,cb) {
if (!ss) return false;
var rls = xGetCSSRules(ss) ; if (!rls) return undefined ;
var result;
for (var j = 0; j < rls.length; j++) {
var cr = rls[j];
if (cr.selectorText) { result = cb(cr); if (result) return true; }
if (cr.type && cr.type == 3 && cr.styleSheet) xTraverseStyleSheet(cr.styleSheet,cb);
}
if (ss.imports) {
for (var j = 0 ; j < ss.imports.length; j++) {
if (xTraverseStyleSheet(ss.imports[j],cb)) return true;
}
}
return false;
}
//
// xTraverseDocumentStyleSheets(callback)
// traverses all stylesheets attached to a document (linked as well as internal)
//
function xTraverseDocumentStyleSheets(cb) {
var ssList = document.styleSheets; if (!ssList) return undefined;
for (i = 0; i < ssList.length; i++) {
var ss = ssList[i] ; if (! ss) continue;
if (xTraverseStyleSheet(ss,cb)) return true;
}
return false;
}
// xHasStyleSelector(styleSelectorString)
// checks whether any of the stylesheets attached to the document contain the definition of the specified
// style selector (simple string matching at the moment)
//
// returns:
// undefined - style sheet scripting not supported by the browser
// true/false - found/not found
//
function xHasStyleSelector(ss) {
if (! xHasStyleSheets()) return undefined ;
return cr.selectorText.indexOf(ss) >= 0;
}
return xTraverseDocumentStyleSheets(testSelector);
}
Improve your search engine rankings with AJAX
Computing server time zone difference in classic ASP ... take 2
Here is the minimum code to get the desired result:
<script runat="server" language="jscript">
var TZDiff = new Date().getTimezoneOffset();
</script>
<% Response.Write TZDiff %>
Of course, if you care about ASP performance, you might not want to invoke two script interpreter engines in a single page. In that case, a more optimized solution is:
GetTZ.asp:
<%@ LANGUAGE=JScript %>
<% Application("TZOffset") = new Date().getTimezoneOffset() %>
Any other ASP page:
<%
If Not IsNumeric(Application("TZOffset")) Then Server.Execute("GetTZ.asp")
' use Application("TZOffset") from here on
%>
Computing Timezone difference from GMT in ASP VBScript
- the browsers always get the most accurate data and
- the dial-up users don't wait for data they have already cached.
Before getting there, you'll hit a major obstacle - all HTTP timestamps are in GMT (and in an obscure format, on top of that) and the time you get with the now() function is local to your timezone. No big deal in most languages - you just use system calls or DLLs to figure out the difference between local time and GMT ... but you can't do that in ASP. Writing a custom COM module is always an option if you own the web server (in which case you can hardcode the timezone offset anyway), but for those of us who rely on hosting (and distributing code), things get almost impossibly complex.
But, as always, there is a trick - using the XMLHTTPRequest object, the ASP script can execute a request on its own server (http://127.0.0.1) and compare the GMT timestamp in the reply with the current time to get the local timezone difference.
Here is the code to do it:
'
' FromHTTPDate - helper function that converts HTTP date
' into a date value
'
Function FromHTTPDate (SDate)
Dim n,LC,S
n=InStr(1,SDate,";",vbTextCompare)
If (n>0) Then
S = Left(SDate,n-5)
Else
If SDate > "9" Then S = Left(SDate,Len(SDate) - 4)
End If
n=InStr(1,S," ",vbTextCompare)
If (n>0) Then S = Mid(S,n+1,Len(S))
LC = Response.LCID : Response.LCID = 1033
FromHTTPDate=CDate(S) : Response.LCID = LC
End Function
'
' GetTimezoneDifference - returns hourly difference between
' local time and GMT
'
Function GetTimezoneDifference
Dim TZD
TZD = Application ("TimezoneDifference") ' reuse the result
If TypeName(TZD) = "Integer" Then ' if available
GetTimezoneDifference = TZD : Exit Function
End If
Dim HttpReq,Port,URL,RDT
Set HttpReq = Server.CreateObject("MSXML2.ServerXMLHTTP")
Port = Request.ServerVariables("SERVER_PORT")
URL = "http://127.0.0.1" & ":" & Port & "/"
HttpReq.open "GET", URL , False
HttpReq.send
RDT = FromHTTPDate(HttpReq.getResponseHeader("Date"))
TZD = CInt((Now() - RDT) * 24 + 0.05)
Application ("TimezoneDifference") = TZD ' cache the result
GetTimezoneDifference = TZD
End Function
Handling Variant byte array in ASP
The trick is (as always) very simple: you treat variant byte array as string, using regular string functions (like Len, Mid or Asc). However, due to Unicode support in VBScript, you have to use their byte versions. So, to dump the variant byte array, use this code:
For I = 1 to LenB(varArray)
Response.Write AscB(MidB(varArray,I,1))
Next