ColdFusion hCard Microformats Custom Tag

I a huge fan of of microformats (or µF), if you're not familiar with them take a look at www.microformats.org and the entry at Wikipedia.

Basically it's a way of marking up HTML to give it more more meaning, think of it as the semantic web (with a small s) that is available now.

hCard is a HTML representation of vCard and while no browers currently support it natively, the next versions of Firefox and IE will support it. There are also plugins currently available for Firefox (Operator and Tails ) and Safari (various bookmarklets) and IE (various favlets).

So what does a hCard look like?

<div class="vcard">
   <div class="fn">Justin Mclean</div>
   <div class="adr"><span class="street-address">170 Campbell St</span>, <span class="locality">Sydney</span> <span class="region">NSW</span> <span class="postal-code">2010</span></div>
   <div class="tel"><span class="type"><abbr title="work">Phone:</abbr></span> <span class="value">(02) 9368 1014</span></div>
   <div class="tel"><span class="type"><abbr title="cell">Mobile:</abbr></span> <span class="value">0416119342</span></div>
   <div>Email: <a class="email" href="mailto:justin@classsoftware.com">justin@classsoftware.com</a></div>   
</div>

As you can see you can easily mark up an address as a microformat and then style it with CSS.

If you have several 100 addresses that you needed to convert it would get a bit tedious doing it all by hand. That's why I've created a ColdFusion custom tag to do the work for you.

<!---
   Name : cfhcard
   Author : Justin Mclean
   Copyright : Class Software 2007 (http://www.classsoftware.com)
   License : Licensed under Creative Commons attribution license
--->

<cfif thisTag.executionMode is "end">
   <cfscript>
   // function to take an address string and break it up into it's elements
   function parseAddress(addressstring)
   {
      var   nolines = 0;
      var   lastline = '';
      var   line = '';
      var address = structnew();
      
      // split address on commas and new lines
      newline = chr(13) & chr(10);
      newlinecomma = '#newline#,';
      
      if (find(',', addressstring) or find(newline,addressstring)) {
         nolines = listlen(addressstring, newlinecomma);
         
         address.address = listfirst(addressstring, newlinecomma);
         lastaddress = '';
         
         // try and find state postcode line
         for (i = 2; i lte nolines; i = i + 1) {
            lastline = listgetat(addressstring, i-1, newlinecomma);
            line = trim(listgetat(addressstring, i, newlinecomma));
            
            // look for line matching city state and postcode         
            if (refind('[A-Z|a-z]+ [A-Z|a-z]+ [0-9]+$',line)) {
               address.city = trim(listfirst(line, ' '));
               address.state = trim(listgetat(line, 2, ' '));
               address.postcode = trim(listlast(line, ' '));
               break;
            }
            // look for line matching state and postcode   
            else if (refind('[A-Z|a-z]+ [0-9]+$',line)) {
               address.address = lastaddress;
               address.city = trim(lastline);
               address.state = trim(listfirst(line, ' '));
               address.postcode = trim(listlast(line, ' '));
               break;
            }
            else {
               lastaddress = address.address;
               address.address = '#address.address##line##newline#';
            }
         }
         
         // check for country if we still have lines to go
         if (i lt nolines) {
            line = trim(listgetat(addressstring, i+1, newlinecomma));
            if (refind('[A-Z|a-z]+ [0-9]+$',line)) {
               address.country = line;
            }         
         }
      }
         
      return address;
   }
   </cfscript>
   
   <cfset content = thisTag.GeneratedContent>

   <cfscript>
      // find address lines assume they comes first
      newline = chr(13) & chr(10);
      content = replace(content, ',', newline, 'ALL');
      endaddress = 0;
      address = structnew();
      addressstr = '';

      nolines = listlen(content, newline);
      for (i=2; i lte nolines; i = i + 1) {
         line = trim(listgetat(content, i, newline));
         addressstr = '#addressstr##line##newline#';
            
         // look for (city) state postcode line
         if (refind('[A-Z|a-z]+ [0-9]+$', line) or refind('[A-Z|a-z]+ [A-Z|a-z]+ [0-9]+$', line)) {
            endaddress = i;
            break;
         }
      }
   
      // check if next line could be country
      if (endaddress gt 0) {
         line = listgetat(content, endaddress + 1, newline);
         if (refind('[A-Z][a-z]+$', line)) {
            addressstr = '#addressstr##line##newline#';
            endaddress = endaddress + 1;
         }
      }
      // parse address
      address = parseAddress(addressstr);
      
      // set other address fields
      address.name = listfirst(content, newline);
      
      // check other lines for other known values
      for (i = endaddress + 1; i lte nolines; i = i + 1) {
         line = trim(listgetat(content, i, newline));

         if (refind('[A-Z|a-z]+[\:|\-| ]+[0-9|\-|\+|\(|\)| ]+$',line)) {
            label = trim(listfirst(line, ':-'));
            if (lcase(label) is 'mobile' or lcase(label) is 'm') {
               address.mobile = trim(listlast(line, ':-'));
            }
            if (lcase(label) is 'phone' or lcase(label) is 'telephone' or lcase(label) is 'work' or lcase(label) is 'p' or lcase(label) is 't' or lcase(label) is 'w') {
               address.phone = trim(listlast(line, ':-'));
            }            
         }
         else if (refind('[0-9|\-|\+\(|\)| ]+$',line)) {
            address.phone = line;
         }
         else if (refind('[A-Z|a-z]+[\:|\-| ]+[A-Z|a-z|0-9]+\@[A-Z|a-z|0-9|\.]+$', line)) {
            label = listfirst(line, ':-');
            if (lcase(label) is 'email') {
               address.email = trim(listlast(line, ':-'));
            }
         }
         else if (refind('[A-Z|a-z|0-9]+\@[A-Z|a-z|0-9|\.]+$', line)) {
            address.email = line;
         }         
      }
   </cfscript>
   
   <cfsavecontent variable="hcard">
   <cfoutput>
<div class="vcard">
   <div class="fn">#address.name#</div>
   <div class="adr"><span class="street-address">#address.address#</span>, <span class="locality">#address.city#</span> <span class="region">#address.state#</span> <span class="postal-code">#address.postcode#</span><cfif isdefined("address.country")> <span class="country-name">#address.country#</span></cfif></div>
   <cfif isdefined("address.phone")><div class="tel"><span class="type"><abbr title="work">Phone:</abbr></span> <span class="value">#address.phone#</span></div>
   </cfif>
   <cfif isdefined("address.mobile")><div class="tel"><span class="type"><abbr title="cell">Mobile:</abbr></span> <span class="value">#address.mobile#</span></div>
   </cfif>   
   <cfif isdefined("address.email")><div>Email: <a class="email" href="mailto:#address.email#">#address.email#</a></div>
   </cfif>
</div>
   </cfoutput>
   </cfsavecontent>
      
   <cfset thisTag.GeneratedContent = hcard>
</cfif>

The above custom tag will take any plain text address and convert it to a hCard microformat, and while not perfect it understand a wide range of address formats and telephone and email addresses.

Currently the tag assumes Australian style addresses but it wouldn't be too hard to extend to other address formats/styles.

Here is a example of it's use:

<cf_hcard>
Justin Mclean
170 Campbell St, Sydney, NSW 2010, Australia
email: justin@classsoftware.com
mobile: 0416119342
phone: (02) 9368 1014
</cf_hcard>

<cf_hcard>
Justin Mclean
170 Campbell St
Sydney
NSW 2010
Email: justin@classsoftware.com
Mobile: 0416119342
</cf_hcard>

<cf_hcard>
Justin Mclean
170 Campbell St
Sydney NSW 2010
M: 0416119342
W: +61 2 9368 1014
</cf_hcard>

<cf_hcard>
Justin Mclean
170 Campbell St
Sydney
NSW 2010
justin@classsoftware.com
041 6119 342
</cf_hcard>

Update - This project can now be found at:
http://cfhcard.riaforge.org/

Comments (Comment Moderation is enabled. Your comment will not appear until approved.)
Kevin Parker's Gravatar Thanks for this post. I've been using Microformats and wanting to do more, but had not taken the time to automate the markup process. This gives me some great ideas.
# Posted By Kevin Parker | 10/15/09 2:05 PM