March 28, 2024   

Code Tutorials

Making a List of Anchor Links from Heading Tags

« back to tutorials page

Download source files 125KB (v1.00)

On a recent site I worked on (www.middlesexhospital.org) one of the requirements was to take the body from dynamic content pages and make an unordered list of anchored links pointing to the <h*> heading tags on the page.

Example body textMost copywriters today write correctly using headings (and nested headings).
(Click on the example image to the left).
<h1 id="Heading-1a">Heading 1a</h1>
<p>Some paragraph text.</p>
<h2 id="Heading-2a">Heading 2a</h2>
<p>Some paragraph text.</p>
<h2 id="Heading-2b">Heading 2b</h2>
<p>Some paragraph text.</p>
<h3 id="Heading-3a">Heading 3a</h3>
<p>Some paragraph text.</p>
Example unordered listReferencing the id attributes for the <h*> heading tags you can now provide anchored bookmark links pointing to them in an unordered list (apply styles as needed).
(Click on the example image to the right)
<ul title="Page navigation tree">
  <li><a href="#Heading-1a">Heading 1a
    <ul>
      <li><a href="#Heading-2a">Heading 2a</a></li>
      <li><a href="#Heading-2b">Heading 2b</a>
        <ul>
          <li><a href="#Heading-3a">Heading 3a</a></li>
        </ul>
      </li>
    </ul>
  </li>
</ul>
So given the body, how do we create the unordered list dynamically? Also, I don't want to make my users have to know how to set id attributes in <h*> heading tags (especially if they are using a WYSIWYG editor such as FCKEditor or htmlArea to edit their content)?

Reuirements:

  1. A copy of jTidy (Yes it's release date is 2001, but that was the last stable release. I tried using newer bightly builds, but they are riddled with bugs).
  2. Get a copy of Greg's makexHtmlValid() function and add it to a file called jTidy.cfc (note: His file is set to run on BlueDragon. To change his cfc to use CFMX, remark out the first set for "jTidy = loader..." and unremark the one three lines down). Please be aware that I had to remove the lines of code (near the end) that set the variables "startPos", "endPos", and "returnPart". If you download the zip below I have the modifications in it.
  3. In the same folder as the jTidy.cfc create this a file called headingInfo.cfc:
    (Note: I had to force line-breaks to allow this code to fit in 800x600 resolution.
    Please download these files so they are cleaner to read)
    <cfcomponent displayname="headingInfo" hint="returns &gt;h*&lt; elements
      and a menu list with ids in links">
      <cffunction name="getHeadingInfo" returntype="struct" hint="returns
        &gt;h*&lt; elements and a menu list with ids in links">
        <cfargument name="body" type="string" required="yes" default="" />
        <cfargument name="bEnableHeadings" type="boolean" required="yes"
                    default="1" />
        <cfscript>
          // Declare local variables
    
          var content = '';
          var myxmlcontent = '';
          var myheadings = '';
          var tmpVar = '';
          var i = '';
          var k = '';
          var outstr = '';
          var startPos = '';
          var endPos = '';
          var bodyContent = '';
          var numberOld = '';
          var navMenu = '';
          var numberCurrent = '';
          var stHeadingInfo = StructNew();
        </cfscript>
    
        <!--- Start: Code to get heading elements (<h*> elements)
              from body --->
        <cfif trim(arguments.body) neq '' and arguments.bEnableHeadings>
          <!--- Note: Uses jTidy (must be in the java class path
                on the CFMX server) --->
          <cfset oJtidy = createObject("component", "jTidy") />
          <cfset content = oJtidy.makexHtmlValid(strtoparse="
                 #arguments.body#") />
          <cfset content = mid(content,find(">",content,1)+1,len(content)) />
    
          <cfset myxmlcontent = XmlParse(trim(content)) />
          <cfset myheadings = XmlSearch(myxmlcontent,"//*[starts-with(name(),
    
                 'h') and string-length(name()) = 2]") />
    
          <!--- Loop through headings to create anchors --->
          <cfloop index="i" from="1" to="#ArrayLen(myheadings)#">
            <cfset tmpVar = ToString(myheadings[i]) />
            <cfset tmpVar = REReplaceNoCase(tmpVar,"
                   <#myheadings[i].xmlname#[^>]*>","","ONE") />
            <cfset tmpVar = ReplaceNoCase(tmpVar,"
                   </#myheadings[i].xmlname#>", "", "ONE") />
            <!--- Remove xml tag info --->
            <cfset tmpVar = Trim(REReplace(tmpVar, "<[^>]*>", "", "All")) />
            <!--- &amp; is not valid xhtml in ids --->
            <cfset tmpVar = Replace(tmpVar, "&amp;", "and", "ALL") />
            <!--- spaces are not valid xhtml in ids, however lets change them
                  to dashes for readability --->
            <cfset tmpVar = Replace(tmpVar, " ", "-", "ALL") />
            <!--- Remove all non-alphanumeric characters except "-" --->
            <cfset tmpVar = REReplace(tmpVar, "[^a-zA-Z0-9_-]", "", "ALL") />
            <cfset myheadings[i].xmlattributes["id"] = tmpVar />
          </cfloop>
        		
          <!--- Remove <?xml> and <body> elements --->
          <cfset outstr = ToString(myxmlcontent.html.body) />
          <cfset startPos = Find(">", outstr, Find("<body", outstr))+1 />
          <cfset endPos = Find("</body>", outstr) />
          <cfset bodyContent = Mid(outstr, startPos, endPos-startPos) />
        
          <!--- Create Nav Menu --->
          <cfsavecontent variable="NavMenu">
            <cfloop from="1" to="#arrayLen(myheadings)#" index="i">
              <!--- get the numbder after the 'h'. example <h3> = 3 --->
              <cfset numberCurrent = REReplaceNoCase(myheadings[i].XmlName,
                     "[[:alpha:]]","","ALL") />
              <cfif i eq 1>
                <cfset firstNumberFound = numberCurrent />
                <cfset numberOld = firstNumberFound />
              </cfif>
              <cfif numberCurrent gt numberOld>
                <cfif i neq 1>
                  <!--- If this is the first time we've run the loop ---> 
                  <cfoutput><ul><li></cfoutput>
                <cfelse>
                  <!--- I don't remember why I put this here :) --->
                  <cfoutput><ul><li></cfoutput>
                </cfif>
              <!--- If we just ended a nested </ul> --->
              <cfelseif numberCurrent lt numberOld>
                <!--- add as many ending list elements that are needed --->
                <cfloop index="j" from="1" to="#numberOld-numberCurrent#">
                <cfoutput></li></ul></cfoutput></cfloop>
                <cfoutput></li><li></cfoutput>
              <!--- ELSE the numbers are the same --->
              <cfelse>
                <!--- If ending one list element and creating a sibling list
                      element --->
                <cfif i neq 1>
                  <cfoutput></li><li></cfoutput>
                <!--- Otherwise we are creating a nested unordered list --->
                <cfelse>
                  <cfoutput><ul><li></cfoutput>
                </cfif>
              </cfif>
              <!--- Using the loop info above, output this row's info --->
              <cfoutput><a href="###Replace(myheadings[i].xmlattributes["id"],
                           "&", "&amp;", "ALL")#">
                           #Replace(myheadings[i].XmlText,
                           "&", "&amp;", "ALL")#</a></cfoutput>
              <!--- If we are all done running the loop, make sure we end the
                    list (and any nested lists if needed) --->
              <cfif i eq arrayLen(myheadings)>
                <cfloop index="k" from="1" to="
                        #numberCurrent-firstNumberFound+1#">
                <cfoutput></li></ul></cfoutput></cfloop>
              </cfif>
              <cfset numberOld = numberCurrent />
            </cfloop>
          </cfsavecontent>
          <!--- Now save our list of IDs to a variable within the
                structure --->
          <cfscript>
            StructInsert(stHeadingInfo, "bodyContent", bodyContent);
            StructInsert(stHeadingInfo, "navMenu", navMenu);
          </cfscript>
        <cfelse>
          <cfscript>
            StructInsert(stHeadingInfo, "bodyContent", arguments.body);
            StructInsert(stHeadingInfo, "navMenu", ');
          </cfscript>
        </cfif>
    
        <cfreturn stHeadingInfo />
    <!--- End: Code to get headers (<h*> elements) from body --->
      </cffunction>
    </cfcomponent>
    
  4. In the same folder as the jTidy.cfc create index.cfm:
    <cfsavecontent variable="myBodyContent">
      <h1 id="Heading-1a">Heading 1a</h1>
      <p>Some paragraph text.</p>
      <h2 id="Heading-2a">Heading 2a</h2>
      <p>Some paragraph text.</p>
      <h2 id="Heading-2b">Heading 2b</h2>
      <p>Some paragraph text.</p>
      <h3 id="Heading-3a">Heading 3a</h3>
      <p>Some paragraph text.</p>
    </cfsavecontent>
    
    <cfset oHeadingInfo = createObject("component", "headingInfo") />
    <cfset stHeadingInfo =
           oHeadingInfo.getHeadingInfo(body="#variables.myContent#") />
    
    <cfdump var="#stHeadingInfo#" />
    
    <cfsetting enablecfoutputonly="no" />
    

Acknowledgments:

Comments?

Download 125KB (v1.00)

Downloads so far: 7407