Get Adobe Flash player

This class is the bridge that connects to the classes within the AdobeSpellingFramework.swc.

package ca.xty.xSite.xUtils {
    //These classes are imported from the AdobeSpellingFramework.swc
    import com.adobe.linguistics.spelling.SpellChecker;
    import com.adobe.linguistics.spelling.HunspellDictionary;

    import flash.display.*;
    import flash.text.*;
    import flash.geom.Rectangle;
    import flash.ui.ContextMenu;
    import flash.ui.ContextMenuItem;
    import flash.ui.Keyboard;
    import flash.utils.Timer;

First note the package declaration. You need to change this if you want to move this class into your own library system. Next, we import two classes from the swc file - SpellChecker, and HunspellDictionary. After that we import the usual suspects that give us access to the flash display, events and text classes. To draw the squiggly red lines we need the Rectangle class from the geom package, 3 classes from the flash.ui package to run the Contextmenu and give us access to the KeyBoard events, and finally from the flash.utils class we grab the Timer class.

public class XtySpellChecker extends Sprite {
    // Declare our variables
    private const _wordPattern:RegExp = /\b\w+\b/;
    private var _txt:TextField;
    private var _aff:String;
    private var _url:String;
    private var _canvas:Shape;
    private var _checker:SpellChecker;
    private var _dict:HunspellDictionary;
    private var _context:ContextMenu;
    private var _replaceOffsetBegin:int;
    private var _replaceOffsetEnd:int;
    private var _textScrollTimer:Timer;
    private var _visibleLines:int;
    private var _lastTxtLength:int = 0;
    private var _txtLength:int = 0;
    private var doCheck:Boolean = false;

Now we set up our XtySpellChecker class and declare the variables we will be using.

The first variable is a Regular Expression, and while I am no expert on RegExes, I will try to explain a bit about this one. What that expression basically says is "find a word which starts with a space and ends with a space. In other words, an entire word. From the Adobe Flash Platform page: "There are two ways to create a regular expression instance. One way uses forward slash characters (/) to delineate the regular expression; the other uses the new constructor. For example, the following regular expressions are equivalent:"

var pattern1:RegExp = /bob/i;
var pattern2:RegExp = new RegExp("bob", "i");

Breaking down the regular expression we are using we have:
\b Matches the position between a word character and a nonword character
\w Matches a word character (AZ–, az–, 0-9, or _). Note that \w does not match non-English characters, such as é , ñ , or ç .
+ (plus) Matches the previous item repeated one or more times.

The reason for the backslashes - \b - is to escape the letter "b" and let the complier know that we are using "b" the special char and not "b" the regular letter.

The rest of our variables are pretty straightforward. We have a var to represent the incoming TextField, our rules var and dictionary var, canvas will be our place holder, _checker is an instance of the SpellChecker class, _dict is an instance of the HunspellDictionary class; _context is an instance of our ContextMenu, _replaceOffsetBegin and _replaceOffsetEnd will represent the beginning and ending of a word, _visibleLines hold the number of lines which are visible, _curTxtLength and _txtLength will hold the numbers representing the text we are checking, and doCheck is a Boolean flag I will explain in a few moments.

Now for our constructor function.

public function XtySpellChecker(txt:TextField, affUrl:String = "", dictUrl:String = "")	{
    // Place our incoming parameters into our local variables
    _txt = txt;
    _url = dictUrl;
    _aff = affUrl;

    // Calculate the line height using the TextLineMetrics class
    var metrics:TextLineMetrics = _txt.getLineMetrics(0);
    var lineHeight:Number = metrics.ascent + metrics.descent + metrics.leading;

    // Determine how many lines are shown
    _visibleLines = Math.floor(_txt.height / lineHeight);

    // Set up our context menu
    _context = new ContextMenu();
    _context.addEventListener(ContextMenuEvent.MENU_SELECT, onContextMenuSelect);

    // Assign it to our TextField representation
    _txt.contextMenu = _context;

    // Set up our dictionary
    _dict = new HunspellDictionary();
    _dict.addEventListener(Event.COMPLETE, onDictionaryLoaded);
    _dict.addEventListener(IOErrorEvent.IO_ERROR, onIOError)

    // Initiate the timer for scrolling text and checking afterwards
    _textScrollTimer = new Timer(40, 1);
    _textScrollTimer.addEventListener(TimerEvent.TIMER_COMPLETE, onScrollCheckTimerComplete);

    // If out TextField is not yet available in the swf add the ADDED_TO_STAGE listener
    // If it is available draw our canvas
    if(_txt.parent == null)	{
        _txt.addEventListener(Event.ADDED_TO_STAGE, onTxtAddedToStage);

    // Provided we have a file name for our dictionary load that dictionary
    if(dictUrl != ""){
        loadDictionary(affUrl, dictUrl);

First we take the parameters passed in the constructor and assign them to our local variables.

Next, we calculate the line height with the help of the TextLineMetrics class. To learn all the ins and outs of this class have a look here. They have an excellent diagram which explains all.

Now we use our lineHeight to determine the number of visible lines by dividing the height of our TextField by the lineHeight and rounding that result down using Math.floor.

After that we set up our ContextMenu. Telling it to hideBuiltInItems gets rid of as much as you are allowed to of the default ContextMenu settings. We then assign ContextMenuEvent.MENU_SELECT event and send you to the onContextMenuSelect function whenever someone clicks on an item in the ContextMenu.

Next we assign our custom ContextMenu to our TextField representation.

We set up a new HunspellDictionary() instance and assign two event listeners: the first listener tells us when the dictionary has successfully loaded, and the second tells us if it didn't load.

Now, set up the scroller timer and assign it an event listener that will respond to the TimerEvent.TIMER_COMPLETE and send you to the function onScrollCheckTimerComplete.

Then, we run a check to see if our textField has loaded in the parent swf. If it hasn't, and it is equal to null, then we add the event listener Event.ADDED_TO_STAGE, and send you to the function onTxtAddedToStage once the textField has finally loaded. If it is on stage and available, we instruct it to visit our _addCanvas function.

Lastly, we make sure that the dictUrl variable is not empty and, if it's not, then we load the dictionary files.

private function onTxtAddedToStage(event:Event):void{
    _txt.removeEventListener(Event.ADDED_TO_STAGE, onTxtAddedToStage);

The onTxtAddedToStage fires once the parent swf's TextField instance has loaded and is available for use. Once that has happened, we then go to the _addCanvas function.

private function _addCanvas():void	{
    _canvas = new Shape();
    _canvas.x = _txt.x;
    _canvas.y = _txt.y;

    _txt.parent.addChildAt(_canvas, _txt.parent.getChildIndex(_txt));

    // Add 3 event listener to our TextField
    _txt.addEventListener(Event.CHANGE, onTextChanged);
    _txt.addEventListener(Event.SCROLL, onTextScroll);
    _txt.addEventListener(KeyboardEvent.KEY_DOWN, keyDownHandler);

    // Provided our textField is successfully loaded, run the checkText function if the dictionary is also successfully loaded

This function draws a container which will sit on top of the TextField in the parent swf. We set the x and y coordinates to match the TextField and then use the addChildAt function to place it just above the TextField by setting to the same child index as the TextField, which has the effect of driving the TextField down one layer.

Next we add three event listeners. The first listener responds to the CHANGE event, the second one responds to the SCROLL event and the third one responds to the KEY_DOWN event. We will go over these in detail in a minute.

The last thing we do here is run the checkText function if the dictionary is loaded.

private function loadDictionary(affUrl:String, dictUrl:String):void	{
    if(dictUrl == ""){
    _dict.load(affUrl, dictUrl);

This function loads the dictionary files provided that the variable dictUrl is not equal to nothing.

private function onDictionaryLoaded(event:Event):void{
    _checker = new SpellChecker(_dict);

    if(_canvas != null)	{

Once the dictionary files are successfully loaded, we create a new instance of the SpellChecker class and pass it our _dict variable. Now, if our _canvas instance is loaded, and therefore not equal to null, we run the checkText function.

private function onIOError(event:IOErrorEvent):void{

This function is provided to give you a mechanism to handle a failed loading of the dictionary files. As it is here, it does nothing but trace the fact for you.

private function onScrollCheckTimerComplete(event:TimerEvent):void{

This function fires at the end of the scroller timer's execution, and, once again, runs the checkText function.

private function onTextScroll(event:Event):void{
        // clear old error highlights;


The function triggered when a user scrolls the TextField. First it makes sure that we have a dictionary loaded, and if we do, then it clears any squiggly lines from our _canvas and then resets and restarts our scroller timer.

private function onTextChanged(event:Event):void {
        var txtContents:String = _txt.text;
        _txtLength = txtContents.length;
        if(_txtLength - _lastTxtLength > 1 || doCheck){
        _lastTxtLength = _txtLength;

The spell checker is set up to check your spelling as you type. Originally this class accomplished that by checking the spelling every time a character was entered. This approach had the effect of telling you every word was misspelled until you finished typing the whole word. I found this annoying. I mean, at least give me a shot at getting the word spelled right! So I thought, what is the one thing that happens when you have finished typing a word? You hit the space bar before going on to the next word. Ah ha! Let's use the KEY_DOWN event to check for when the SPACE bar was pressed, and then check the spelling. This worked great and was much more satisfying. BUT... Suppose a user copies and pastes a block of text into the TextField? They shouldn't have to then hit the space bar to check the spelling of the newly pasted text. So, I put the CHANGE event back in action, but had it check to see if more than 1 character had been added by comparing the current text length to the last text length. This worked wonderful too! BUT... When there was a spelling error, and you went back to correct it, you once again had to hit the space bar to see if you got it right this time. Grrrr! So, I created a Boolean flag that is set to true when there is a spelling error, and set back to false before the next spell check. And that is why we have a couple of listeners to check the spelling.

The onTextChanged function is the first event to check the spelling. Once it checks the status of our dictionary, it assigns the text in the TextField to the var txtContents. Then it assigns the length of that text to the variable _txtLength. Now it compares the length of _txtLength to the _lastTxtLength variable, and if the difference is greater than 1 OR if the doCheck variable is true, then we fire the checkText function. Lastly we assign the current _txtLength to the _lastTxtlength.

private function keyDownHandler(e:KeyboardEvent):void{
    if(e.keyCode == Keyboard.SPACE){

This function fires when the space bar has been pressed. Since every key press creates an event, we first check to make sure that the event was fired by the pressing of the SPACE bar, and then, if the dictionary is loaded, we run the checkText function.

private function checkText():void {;
    doCheck = false;

    var inputValue:String = _txt.text;
    var offset:int, curPos:int;

    var startOffset:Number = _txt.getLineOffset(_txt.scrollV - 1);

    var maxLines:int = _txt.scrollV + _visibleLines;
    var endOffset:Number = 0;
    var delta:Number = 0;
    if(maxLines < _txt.numLines){
        endOffset = _txt.getLineOffset(maxLines - 1);
        delta = _txt.text.length - endOffset;
        endOffset = endOffset - startOffset;

        inputValue = inputValue.substr(startOffset, endOffset);
        inputValue = inputValue.substr(startOffset);

    var found:Boolean = true;
    var wordList:Array;

    while(found) {
        // lookup word by word....
        wordList = inputValue.match(_wordPattern);

        if(wordList == null)	{
            found = false;
            curPos = inputValue.indexOf(wordList[0]);
            if(!_checker.checkWord(wordList[0])) {
                offset = (_txt.text.length - delta) - inputValue.length;
                _drawErrorHighlight(offset + curPos, offset + curPos + (wordList[0].length - 1));
            inputValue = inputValue.substr(curPos + wordList[0].length);

Finally we are at the checkText function you have heard so much about.

First we clear the graphics - squiggly lines - from our _canvas instance and set doCheck to false.

Next we create a var to hold our TextField text, as well as 2 integer variables to hold the offset position and the position of the cursor. Then we assign a number to a var called startOffset. This var is calculated using the getLineOffset method of the TextField class which "Returns the character index of the first character in the line that the lineIndex parameter specifies." We pass this function of the vertical text scroll max minus one. The var maxLines is made up of _txt.scrollV - "The vertical position of text in a text field." - and our _visibleLines variable. Then we create the var endOffset and assign it an initial value of zero, as well as a var called delta.

Now, if the maxLines is less than the TextFields numLines, then the endOffset equals the result of using the getLineOffset method once again, this time passing in the parameter of maxLines minus one. The delta variable is then calculated using the overall length of the text in the TextField minus the endOffset.

Lastly, we assign a substring value to our inputValue var by instructing the substr function to start at the startOffset index and then grab everything up to the endOffset index.

However, if maxLines is not less than the TextField's numLines then the inputValue grabs everything by only passing the start index parameter, startOffset. And now we have the block of text we want to spell check sorted.

The var found is a Boolean which starts life as true. We create an Array called wordList and then use a while loop to cycle through the text and compile an array of individual words using the Regular Expression we created way back at the beginning of this class as the parameter for the TextField's method match().

If there are no words in our block of text for some reason, then we set the var found to false. However, if wordList does not equal null, we set our curPos variable to equal the index position of the first word in our wordList array.

Next we use the SpellChecker class instance _checker to run the checkWord method using the first word in our wordList array as the parameter to check. In others words we are looking to see if this word is in the dictionary. If it is not in the dictionary, then we set our offset variable to equal the (overall text length minus our var delta) minus inputValue var's length. Then we call our _drawErrorHighlight function and pass the parameters beginIndex and endIndex. The beginIndex value is calculated by adding the offset and the curPos. The endIndex is calculated by adding the offset and the curPos and the length of the first word in our wordList array minus one.

The last thing we do before continuing the while loop, is to change the inputValue var so that it moves ahead by one word, and thus eliminates us checking the same text over and over.

// If a misspelled word is found, draw the squiggly line to highlight it
private function _drawErrorHighlight(beginIndex:int,endIndex:int):void {
    if(endIndex < beginIndex){


    var rect1:Rectangle;
    while(rect1 == null && beginIndex + 1 < endIndex){
        rect1 = _txt.getCharBoundaries(++beginIndex);

    // cannot find boundaries => letter is not displayed
    if(rect1 == null){


    var rect2:Rectangle;
    while(rect2 == null && endIndex - 1 > beginIndex){
        rect2 = _txt.getCharBoundaries(--endIndex);

    // cannot find boundaries => letter is not displayed
    if(rect2 == null){

    // if line isn't rendered, forget it and leave
    var lineNum:int = _txt.getLineIndexOfChar(beginIndex);
    if(lineNum >= _txt.bottomScrollV){

    // reposition canvas
    var metrics:TextLineMetrics = _txt.getLineMetrics(lineNum);
    var lineHeight:Number = metrics.ascent + metrics.descent + metrics.leading;
    _canvas.y = _txt.y - (_txt.scrollV - 1) * lineHeight;

    // get vals for drawing highlight
    var x1:int = rect1.x;
    var x2:int = rect2.x + rect2.width;
    var y1:int = rect1.y + metrics.ascent + 2;
    var w:Number = x2 - x1;

    if(w > 0){, 0xff0000, 1);
        var xPos:int = x1;
        var yPos:int = y1;
        var len:int = Math.ceil(w/6);
        for(var i:int = 0; i < len; i++){
  , yPos);
   + 2, yPos + 2, xPos + 4, yPos);
   + 4, yPos - 2, xPos + 6, yPos);
            xPos += 6;
            yPos = y1;
    doCheck = true;

This function handles drawing and positioning the highlight (squiggly line) when we have encountered a misspelled word. The parameters beginIndex and endIndex are first compared to make sure that endIndex is not less than beginIndex. If it is, we return, otherwise, we subtract one from our beginIndex. This gets rid of the space and lines us up directly with the first character in the word.

Now we create a var rect1, which is going to tell us about the first letter in the misspelled word. If the rect1 equals null - does not exist yet - and if the beginIndex plus one is less than the endIndex, then rect1 uses the textField method getCharBoundaries to drawing a bounding box around the first letter. You pass the parameter beginIndex plus 1 to let it know where to start drawing.

If rect1 is still null, then return. If not, then proceed to rect2. First add one to the endIndex variable and then repeat the same procedure, except that here we want the last character in the word so we pass the endIndex minus 1 to the getCharBoundaries method and it draws a bounding box around the last character of the misspelled word.

If rect2 is still null, return. If not, then we next check to see if the line the spelling mistake is on has been rendered by assigning the result of the TextField's method getLineIndexOfChar after passing the parameter of beginIndex. Then, if the lineNum greater than or equal to the TextField's bottom vertical scroll position, meaning that it has not be rendered, we return. Otherwise we reposition the _canvas. We create a variable to represent the TextLineMetrics class called metrics and set that equal to the result of the TextField's getLineMetrics method using the lineNum as the parameter to be passed. Then, using the metrics instance we calculate the lineHeight taking into account several of the TextLineMetrics class properties. With this information in hand we set the y position of the _canvas to equal the TextField's y position minus (TextField's scrollV minus one) times the lineHeight. Now the _canvas's corrected position allows us to draw the squiggly line in the right place.

Next up we figure out the variables we will require. The x1 variable will equal the rect1 bounding box's x position. Remember, this is the bounding box around the first character of the misspelled word. Then the variable x2 equals the x position of rect2 - the bounding box around the last letter of the misspelled word - plus the width of that bounding box. For the y position we take rect1's y position and add to it the metrics variables ascent property plus 2. The final variable w is assigned the value of x2 minus x1. This will give us the overall length of the squiggly line we want to draw, and make sure that x2 is in fact larger than x1, which is what we do next.

If w is greater than zero, we prepare to draw our squiggly line. First we set the lineStyle for our squiggly line. Here is where you can alter the look of it. The settings as they are here provide you with a one pixel wide red line whose alpha is also 1, or fully opaque. Next we set a variable called xPos to equal x1, and a variable called yPos to equal y1. The squiggly line as it is set out takes a total of 6 pixels to draw it completely once. We need to figure how times we have to draw this sequence based on the length of word we need to highlight. So, we take our w variable and divide it by 6, and use Math.ceil to round up. I would rather have it a little longer than a little shorter. If you prefer it the other way, use Math.floor. Either way, you want to end up with an integer. After we determine the variable len with the above calculation, we start a for loop running. First we use the moveTo method to get our starting position using our xPos and yPos variables. Next we use the curveTo method to draw the squiggles. By first adding two to the xPos and two to the yPos we begin a curved line heading down and then curving back up to our original yPos, but an additional two pixels further along the x axis. In the next curveTo method we continue along the x axis in the same manner, but this time we subtract two from the yPos in order to curve the line upwards and give us that squiggle we are after. When we are done drawing, we add six to our xPos and make certain that our yPos is back to it's original position, and then repeat as many times as necessary.

Finally, now that we have a squiggly line on stage, we set our doCheck variable to true to enable us to check the spelling each time a single character is changed until the spelling mistake is eradicated.

The final functions in this class deal with our custom ContextMenu. The first of these functions deals with the user right clicking to produce the ContextMenu.

private function onContextMenuSelect(event:ContextMenuEvent):void{
    var index:int = _txt.getCharIndexAtPoint(_txt.mouseX, _txt.mouseY);
    _context.customItems = [];

    var inputValue:String = _txt.text;
    var offset:int, curPos:int;

    var found:Boolean = true;
    var words:Array;

    var begin:int = inputValue.lastIndexOf(" ", index);
    if(begin >= 0){
        inputValue = inputValue.substr(begin + 1);

        // lookup word by word....
        words = inputValue.match(_wordPattern);

        if(words == null){
            found = false;
            curPos = inputValue.indexOf(words[0]);
            offset = _txt.text.length - inputValue.length;
            inputValue = inputValue.substr(curPos + words[0].length);

            if(offset <= index && index <= (offset + words[0].length)){

                _replaceOffsetBegin = offset;
                _replaceOffsetEnd = offset + words[0].length;
                found = false;

The first thing we do is create a variable called index and set its value to the result from the TextField's getCharIndexAtPoint method by sending the parameters _txt.mouseX and _txt.mouseY. Then we initialize the ContextMenu's customItems array to an empty array.

Then we assign the variable inputValue to the text of the TextField, and create the variables offset and curPos. Next comes the variable found, which is set to true, and then we create an array called words. Another variable called begin is assigned the result of the last occurrence of - lastIndexOf - a blank space using our index variable to set the starting position to look at. If begin is greater than or equal to zero, we assign the inputValue to be a substring of itself using a starting index of begin plus one.

Next comes a familiar while loop which proceeds to look at all the words to make sure they match, provided our words array is not null. The variable curPos is set to the first occurrence - indexOf - our first word in the substring inputValue. Our offset variable is set to the overall text length minus the length of the substring inputValue. Then inputValue is recalculated to another substring which uses a starting position calculated from the addition of curPos plus the length of the first word in our words array.

Now, if our offset variable is less than or equal to our index variable and index is less than or equal to (offset plus the length of first word in the words array), then we call the function getSuggestions and pass it the parameter of our first word in the words array. Then we set our _replaceOffsetBegin variable to equal our offset variable, our _replaceOffsetEnd to equal the offset plus the length of the first word in our words array, and set found to equal false. This means that we have found the word for which we want a suggestion list, and so we break out of the while loop.

private function getSuggestions(word:String):void{
        var suggestions:Array = _checker.getSuggestions(word);
        var len:int = suggestions.length;
        var items:Array = [];

        for(var i:int = 0;i < len;i++){
            var suggestedWord:String = suggestions[i];
            if(suggestedWord != null && suggestedWord != ""){
                var menuItem:ContextMenuItem = new ContextMenuItem(suggestedWord);
                menuItem.addEventListener(ContextMenuEvent.MENU_ITEM_SELECT, onContextMenuItemSelect, false, 0, true);

        if(items.length == 0){
            items.push(new ContextMenuItem("No suggestions found", false, false));

        _context.customItems = items;

The first thing to do is to double check that the word we want suggestions for is not in fact in our dictionary, so we use the checkWord method of the SpellChecker class instance _checker. If the word does not exist, we create an array called suggestions and assign it the results of the _checker's getSuggestions method. Then we set a variable called len to equal the length of our suggestions array. And last, but not least, we create an array called items and initialize it as an empty array.

Next, we use a for loop to go through the suggested words one at a time. The variable suggestedWord is assigned the value of the i index of the suggestions array. Then we make sure that suggestedWord is not null or an empty string. If it passes this test, we create a variable called menuItem, which is an instance of the ContextMenuItem class, and thereby create a new menuItem by passing the suggestedWord as the constructor parameter. Then we add an event listener to our new menuItem which listens for the MENU_ITEM_SELECT event and fires the onContextMenuItemSelect function. Now we add the new menuItem to our items array.

After the for loop has finished running, we check to see whether there is anything in our items array, and if there is not, then we create a new instance of the ContextMenuItem class and set it's caption property to equal the "No suggestions found" phrase. And, finally we assign our items array to the array of our Contextmenu instance _context.

private function onContextMenuItemSelect(event:ContextMenuEvent):void{
    var item:ContextMenuItem = as ContextMenuItem;
    if(item != null){
        _txt.replaceText(_replaceOffsetBegin, _replaceOffsetEnd, item.caption);

        var caretPosition:Number = _replaceOffsetBegin + item.caption.length;
        _txt.setSelection(caretPosition, caretPosition);


This final function handles the clicking of a suggested item from the ContextMenu. First we create an instance of the ContextMenuItem class called item and assign it the value of the, casting it to be a ContextMenuItem. Then, if item does not equal null, we use the TextField's replaceText function to add the suggested word in place of the misspelled word. The parameters we send are the start index - _replaceOffsetBegin -, the end index - _replaceOffsetEnd - and the string itself in the form of the item's caption property. To get our cursor in the right place we create a variable called caretPosition and assign it's value to equal the beginning index - _replaceOffsetBegin - plus the item's caption property's length. Then we use the TextField's method setSelection to place the cursor. This method takes two parameters - start index and end index. Because we want the cursor in one spot we simply pass in the variable caretPosition twice.

And that's it!

Just One Thing


Digging a little deeper into the contractions problem I find that it is not all contractions that it regards as a spelling mistake. Things like can't, won't, let's all work fine. Hmmmm. Still digging.

Squiggly uses the Hunspell Dictionary. This dictionary apparently has no way to deal with contractions - didn't, aren't, wouldn't, etc - and will call every contraction a spelling mistake. In the bit of research I did, I found nothing but me scratching my head. OpenOffice also uses the Hunspell dictionary, but my version of it doesn't consider contractions to be spelled wrong. So, what does that mean? If anyone is aware of a solution to this problem please let me know!

As always, if you have any questions, don't be shy. Use the comment form below, or drop me a line./p>

Get Adobe Flash player


No Comments

Page 1


Warning: date() []: It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected 'America/New_York' for 'EST/-5.0/no DST' instead in /home/xtydigi/public_html/footer.php on line 2