HTMLLogRenderer         LogListener derived class which shadows a given Log's output with an html version with additional features

 

This was posted at the Ogre3D Forum by Johnpus - topic

Introduction

I've put together a class that derives from LogListener and shadows a given Log's output with an html version that adds a couple of features.

You can specify an image to appear at the top of the html file and specify it's dimensions.

You can give HTMLLogRenderer a list of strings to be displayed with newlines between them as a header to the file.

You can specify special formatting strings to be applied to Log entries depeding on the first word in the entry. For instance if you've defined a style whose "flag" is "Error:" with text color white and highlight color red, that formatting will be applied to each Log message that starts with "Error:". A style can have user prefix and postfix strings which can be used for things like making the message a url or adding an annoying <blink>.

You can log "Additional Information" by logging a message in this format: +Section_Name Entry_String. The information is displayed at the bottom of the file with each Entry_String appearing numbered below each Section_Name.

Here's what it looks like in practice. This is a contrived example because Ogre's various log entries obviously don't take this naming scheme into account.

Configuration

Here's the config file that defines the styles used in that screenshot. You'll need to either copy the ogrelogo-small.jpg image to the program's running folder or change the path to the image:

# comment out these top two lines if you don't want an image at the top
    headerImage=ogrelogo-small.jpg
    headerImageDimensions=269 158
    # style=FLAG TEXT_COLOR HIGHLIGHT_COLOR
    style=App: blue white
    style=Error: red white
    style=Fatal_Error: white red
    style=Resources: green white
    style=Info: orange white
    style=Blinky: black white <blink> </blink>

Code

#ifndef __HTMLLogRenderer_h__
    #define __HTMLLogRenderer_h__

    #include "Ogre.h"
    using namespace Ogre;

    #include <list>
    #include <map>
    using namespace std;

    typedef list<String> StringList;
    typedef map<String,StringList*> StringMap;

    class StringUtilEx : public StringUtil
    {
    public:
       /* 
       found this find and replace code on the internet at
       http://www.pscode.com/vb/scripts/ShowCode.asp?txtCodeId=7447&lngWId=3
       I don't know if it's ok to just take it or not 
       */
       static string findAndReplace( String str, String find, String replaceWith )
       {
          unsigned long iIndex1 = str.find(find, 0);
          unsigned long iIndex2 = 0;
          unsigned long iLengthOld = find.length();
          unsigned long iLengthNew = replaceWith.length();
          while (iIndex1 != string::npos)
          {
             iIndex2 = iIndex1 + iLengthNew + 1;
             str = str.erase(iIndex1, iLengthOld);
             str = str.insert(iIndex1, replaceWith);
             iIndex1 = str.find(find, iIndex2);
          }

          return str;
       }

       static bool areStringsEqual( String str1, String str2 )
       {
          int length = ( (str1.length() > str2.length()) ? str1.length() : str2.length() );
          if( !strncmp( str1.c_str(), str2.c_str(), length ) )
             return true;

          return false;
       }
    };

    class HTMLLogRenderer : public LogListener
    {
    protected:
       // stores the formatting strings that are put before and after a message if
       // it's first word matches the style's flag string
       class Style
       {
       public:
          Style( String flag, String prefix, String postfix )
          {
             mFlag = flag;
             mPrefix = prefix;
             mPostfix = postfix;
          }
          String mFlag;
          String mPrefix;
          String mPostfix;
       };
       typedef list<Style*> StyleList;

    public:
       HTMLLogRenderer( const String &originalLogFilename, const String &rendererConfigFilename,
          StringList &headerInfo ) : LogListener()
       {
          mLoggingLevel = LL_NORMAL;
          mOriginalLogFilename = originalLogFilename;
          mNumEntries = 0;

          // open renderer config file
          ConfigFile cf;
          cf.loadDirect( rendererConfigFilename, "\t=" );
          
          String headerImageFilename;
          int headerImageWidth = 0;
          int headerImageHeight = 0;

          // parse the config file
          ConfigFile::SettingsIterator config_iter = cf.getSettingsIterator();
          while( config_iter.hasMoreElements() )
          {
             String key = config_iter.peekNextKey();
             String value = config_iter.peekNextValue();
                
             // if this element is a style, add it
             if( StringUtilEx::areStringsEqual( key, "style" ) )
             {
                stringstream ss( value );
                String flag, textColour, highlightColour, prefix, postfix;

                    ss >> flag >> textColour >> highlightColour >> prefix >> postfix;
                addStyle( flag, textColour, highlightColour, prefix, postfix );
             }
             // set the header image filename
             else if( StringUtilEx::areStringsEqual( key, "headerImage" ) )
             {
                stringstream ss( value );
                ss >> headerImageFilename;
             }
             // set the header dimensions
             else if( StringUtilEx::areStringsEqual( key, "headerImageDimensions" ) )
             {
                stringstream ss( value );
                ss >> headerImageWidth >> headerImageHeight;
             }

             config_iter.getNext();
          }

          // compile renderer output filename
          String newFilename = originalLogFilename;
          newFilename.append( ".html" );
          mFile.open( newFilename.c_str() );

            // write HTML header
          mFile << "<header></header><body>";
          if( !StringUtilEx::areStringsEqual( "", headerImageFilename ) )
          {
             mFile << "<img src=\"" << headerImageFilename << "\" ";
             //if header image dimensions have been specified ( and the value isn't 0 ) write dimensions      
             if( headerImageHeight > 0 && headerImageWidth > 0 )
                mFile << " width=" << headerImageWidth << " height=" << headerImageHeight;
             // close img tag 
             mFile << "><br>";
          }

          // write font tag
          mFile << "<font style=\"FONT-FAMILY: \'Courier New\'\" size=2>";

          // write any supplied header strings
          for( StringList::iterator iter = headerInfo.begin(); iter != headerInfo.end(); ++iter )
          {
             String headerLine = static_cast<String>(*iter);
             mFile << headerLine << "<br>";
          }

          // write the stuff that preceeds the log entries
          mFile << "<br>Log<br>";
          mFile << "--------------------------------------------------" << "<br>";
          mFile << "</b>";

          mFile.flush();
       }

       virtual ~HTMLLogRenderer()
       {
          // this is the line that appears after the last message in the log
          mFile << "--------------------------------------------------" << "<br>";
          mFile << "</b>";
          mFile << "<br>";
          renderAdditionalInfo();
          mFile << "</body>";

          // not sure if it's necessary to call this before close()
          mFile.flush();
          mFile.close();

          // delete the styles
          for( StyleList::iterator iter = mStyleList.begin(); iter != mStyleList.end(); iter )
          {
             Style *style = *iter;
             delete style;
             iter = mStyleList.erase( iter );
          }
       }

       virtual void write( const String& name, const String& message, 
          LogMessageLevel lml = LML_NORMAL, bool maskDebug = false )
       {
          // check if the message is meant for the log this is shadowing
          if( StringUtilEx::areStringsEqual( name, mOriginalLogFilename ) )
             // check for the '+' sign that indicates an additional info entry
             if( StringUtil::startsWith( message, "+" ) )
                addAdditionalInfo( message );
             // check if the logging level of the message meets the threshold, copied from the Log class
             else if( ( mLoggingLevel + lml ) >= OGRE_LOG_THRESHOLD )
             {
                // write time ( slightly changed code from Ogre's log class )
                struct tm *pTime;
                time_t ctTime; time(&ctTime);
                pTime = localtime( &ctTime );
                mFile << "#" << std::setw(4) << std::setfill('0') << mNumEntries++;
                mFile
                   << " " << std::setw(2) << std::setfill('0') << pTime->tm_hour
                   << ":" << std::setw(2) << std::setfill('0') << pTime->tm_min
                   << ":" << std::setw(2) << std::setfill('0') << pTime->tm_sec;
                mFile << "&nbsp;";

                // get the first word in the message, which will be the prefix if one is present
                stringstream ss( message );
                String firstWord;
                ss >> firstWord;

                // if firstWord is equal to a style's flag string, get a pointer to that style
                Style *style = 0;
                for( StyleList::iterator iter = mStyleList.begin(); iter != mStyleList.end(); ++iter )
                {            
                   if( StringUtilEx::areStringsEqual( firstWord, static_cast<Style*>(*iter)->mFlag ) )
                      style = *iter;
                }

                // this string is the string to be output to mFile
                String outMessage;
                // if a style was found
                if( style )
                {
                   // add the prefix and postfix around the original message
                   outMessage.append( style->mPrefix );
                   outMessage.append( message );
                   outMessage.append( style->mPostfix );
                }
                // otherwise just copy the original message
                else
                {
                   outMessage = message;
                }
             
                // change any \n newlines into HTML's <br>, 
                // and add enough spaces so that the new line is indented past the entry number and time
                outMessage = StringUtilEx::findAndReplace( outMessage, "\n", "<br>\t\t\t\t&nbsp;&nbsp;&nbsp;" );
                // change any \t tabs into five HTML &nbsp
                outMessage = StringUtilEx::findAndReplace( outMessage, "\t", "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" );
                
                outMessage.append( "<br>" );

                mFile << outMessage;
                mFile.flush();
             }
       }

       void setLoggingLevel( LoggingLevel lml )
       {
          mLoggingLevel = lml;
       }

    protected:
       void addStyle( String flag, 
          String textColour, String highlightColour, 
          String customPrefix, String customPostfix )
       {
          stringstream prefix;
          prefix << "<font  color=\""   << textColour << 
             "\" style=\"FONT-FAMILY: 'Courier New';BACKGROUND-COLOR:" << highlightColour << "\" size=2>" << customPrefix;
          stringstream postfix;
          postfix << customPostfix << "</font>";

          Style *style = new Style( flag, prefix.str(), postfix.str() );
          mStyleList.push_back( style );
       }

       void addAdditionalInfo( String sInfo )
       {
          stringstream ss( sInfo );
          String section;
          String message;

          ss >> section;
          message = sInfo.substr( section.length(), sInfo.length() );
          
          // remove '+' character
          section = section.substr( 1, section.length() - 1 );
          
          StringList *itemList = 0;
          for( StringMap::iterator iter = mAdditionalInfo.begin(); iter != mAdditionalInfo.end(); ++iter )
          {
             if( !strncmp( static_cast<String>(iter->first).c_str(), section.c_str(), section.length() ) )
                itemList = iter->second;
          }

          if( !itemList )
          {
             StringList *itemList = new StringList;
             mAdditionalInfo[section] = itemList;
          }

          itemList = mAdditionalInfo[section];
          itemList->push_back( message );
       }

       void renderAdditionalInfo()
       {
          mFile << "Additional Info: " << "<br>";
          mFile << "--------------------------------------------------" << "<br><br>";
          // for each additional info section
          for( StringMap::iterator k_iter = mAdditionalInfo.begin(); k_iter != mAdditionalInfo.end(); ++k_iter )
          {
             string headerName = k_iter->first;
             // underscores can be substituted for space in a section name, so fix that up
             headerName = StringUtilEx::findAndReplace( headerName, "_", "&nbsp;" );
             mFile << headerName << ": " << "<br>";

             int i = 1;
             StringList *lines = k_iter->second;
             // for each entry in the section
             for( StringList::iterator v_iter = lines->begin(); v_iter != lines->end(); ++v_iter )
             {
                String line = *v_iter;
                mFile << "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;" << i << ")" << "&nbsp;" << line << "<br>";
                ++i;
             }

             mFile << "<br>";
          }      
          mFile << "--------------------------------------------------" << "<br>";
       }

    protected:
       ofstream mFile;
       LoggingLevel mLoggingLevel;
       String mOriginalLogFilename;
       StyleList mStyleList;
       StringMap mAdditionalInfo;
       int mNumEntries;
    };

    #endif

Fun Fact

Fun Fact: I wrote a version of this that output to RTF format before I realized "oh yeah, html".

Notes

You may notice a shotgun scattering of "const" throughout the file. That's because I've never really bothered to "const" things before and I'm still a little shakey on where and when and why to do that. I meant to check that all over before I posted this, but forgot so here you go.