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.
Table of contents
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 << " "; // 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   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
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.