
|
 |
 |
 December 01, 2023
|

Code Tutorials
Making a List of Anchor Links from Heading Tags
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.
 Most 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>
 Referencing 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:
- 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).
- 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.
- 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 >h*< elements
and a menu list with ids in links">
<cffunction name="getHeadingInfo" returntype="struct" hint="returns
>h*< 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")) />
<!--- & is not valid xhtml in ids --->
<cfset tmpVar = Replace(tmpVar, "&", "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"],
"&", "&", "ALL")#">
#Replace(myheadings[i].XmlText,
"&", "&", "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>
- 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: 7230
|
 |