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