Skip to main content

History: HTMLLogRenderer

Source of version: 7 (current)

Copy to clipboard
            {REMARKSBOX(type="comment",title="",close="n")}This was posted at the Ogre3D Forum by Johnpus - [http://www.ogre3d.org/forums/viewtopic.php?f=5&t=13089|topic]{REMARKSBOX}
{img align=right fileId=2042 thumb=y width=250 rel=box[g]}
!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.

{maketoc}

!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:
{CODE(wrap="1", colors="ini")}
    # 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}

!Code
{CODE(wrap="1", colors="c++")}
    #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 << " ";

                // 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   " );
                // change any \t tabs into five HTML &nbsp
                outMessage = StringUtilEx::findAndReplace( outMessage, "\t", "     " );
                
                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, "_", " " );
             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 << "     " << i << ")" << " " << line << "<br>";
                ++i;
             }

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

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

    #endif
{CODE}

!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.