");
sw.Write("");
StringBuilder sb = new StringBuilder(128);
for (int lineIndex = 0; lineIndex < mCodeLineList.Count; lineIndex++) {
if (Selection != null && !Selection[lineIndex]) {
continue;
}
GenerateHtmlLine(lineIndex, sw, sb);
if (worker.CancellationPending) {
break;
}
int perc = (lineIndex * 100) / mCodeLineList.Count;
if (perc != lastProgressPerc) {
lastProgressPerc = perc;
worker.ReportProgress(perc);
}
}
sw.WriteLine("
\r\n");
sw.Write(template2);
}
if (worker.CancellationPending) {
Debug.WriteLine("Cancel requested, deleting " + pathName);
File.Delete(pathName);
return false;
}
string cssFile = RuntimeDataAccess.GetPathName(HTML_EXPORT_CSS_FILE);
string outputDir = Path.GetDirectoryName(pathName);
string outputPath = Path.Combine(outputDir, HTML_EXPORT_CSS_FILE);
if (File.Exists(cssFile) && (overwriteCss || !File.Exists(outputPath))) {
Debug.WriteLine("Copying '" + cssFile + "' -> '" + outputPath + "'");
try {
File.Copy(cssFile, outputPath, true);
} catch (Exception ex) {
string msg = string.Format(Res.Strings.ERR_FILE_COPY_FAILED_FMT,
cssFile, outputPath, ex.Message);
MessageBox.Show(msg, Res.Strings.ERR_FILE_GENERIC_CAPTION,
MessageBoxButton.OK, MessageBoxImage.Error);
return false;
}
}
return true;
}
///
/// Generates HTML output for one display line. This may result in more than one line
/// of HTML output, e.g. if the label is longer than the field. EOL markers will
/// be added.
///
///
/// Currently just generating a line of pre-formatted text. We could also output
/// every line as a table row, with HTML column definitions for each of our columns.
///
/// Index of line to output.
/// Text output destination.
/// String builder to append text to. Must be cleared before
/// calling here. (This is a minor optimization.)
private void GenerateHtmlLine(int index, TextWriter tw, StringBuilder sb) {
LineListGen.Line line = mCodeLineList[index];
if (line.LineType == LineListGen.Line.Type.Note && !IncludeNotes) {
return;
}
sb.Clear();
// Width of "bytes" field, without '+' or trailing space.
int bytesWidth = mColStart[(int)Col.Bytes + 1] - mColStart[(int)Col.Bytes] - 2;
// Width of "label" field, without trailing space.
int maxLabelLen = mColStart[(int)Col.Label + 1] - mColStart[(int)Col.Label] - 1;
DisplayList.FormattedParts parts = mCodeLineList.GetFormattedParts(index);
// If needed, create an HTML anchor for the label field.
string anchorLabel = null;
if ((line.LineType == LineListGen.Line.Type.Code ||
line.LineType == LineListGen.Line.Type.Data ||
line.LineType == LineListGen.Line.Type.EquDirective) &&
!string.IsNullOrEmpty(parts.Label)) {
if (parts.Label.StartsWith(mFormatter.NonUniqueLabelPrefix)) {
// TODO(someday): handle non-unique labels. ':' is valid in HTML anchors,
// so we can use that to distinguish them from other labels, but we still
// need to ensure that the label is unique and all references point to the
// correct instance. We can't get that from the Parts list.
} else {
string trimLabel = Symbol.TrimAndValidateLabel(parts.Label,
mFormatter.NonUniqueLabelPrefix, out bool isValid, out bool unused1,
out bool unused2, out bool unused3, out Symbol.LabelAnnotation unusedAnno);
anchorLabel = "" + parts.Label + "";
}
}
// If needed, create an HTML link for the operand field.
string linkOperand = null;
if ((line.LineType == LineListGen.Line.Type.Code ||
line.LineType == LineListGen.Line.Type.Data) &&
parts.Operand.Length > 0) {
linkOperand = GetLinkOperand(index, parts.Operand);
}
// Put long labels on their own line if desired.
bool suppressLabel = false;
if (LongLabelNewLine && (line.LineType == LineListGen.Line.Type.Code ||
line.LineType == LineListGen.Line.Type.Data)) {
int labelLen = parts.Label.Length;
if (labelLen > maxLabelLen) {
// put on its own line
string lstr;
if (anchorLabel != null) {
lstr = anchorLabel;
} else {
lstr = parts.Label;
}
AddSpacedString(sb, 0, mColStart[(int)Col.Label], lstr, parts.Label.Length);
tw.WriteLine(sb);
sb.Clear();
suppressLabel = true;
}
}
int colPos = 0;
switch (line.LineType) {
case LineListGen.Line.Type.Code:
case LineListGen.Line.Type.Data:
case LineListGen.Line.Type.EquDirective:
case LineListGen.Line.Type.RegWidthDirective:
case LineListGen.Line.Type.DataBankDirective:
case LineListGen.Line.Type.ArStartDirective:
case LineListGen.Line.Type.ArEndDirective:
case LineListGen.Line.Type.LocalVariableTable:
if (parts.IsLongComment) {
// This happens for long comments embedded in LV tables, e.g.
// "clear table".
AddSpacedString(sb, 0, mColStart[(int)Col.Label],
TextUtil.EscapeHTML(parts.Comment), parts.Comment.Length);
break;
}
// these columns are optional
if ((mLeftFlags & ActiveColumnFlags.Offset) != 0) {
colPos = AddSpacedString(sb, colPos, mColStart[(int)Col.Offset],
parts.Offset, parts.Offset.Length);
}
if ((mLeftFlags & ActiveColumnFlags.Address) != 0) {
if (!string.IsNullOrEmpty(parts.Addr)) {
string str;
if (parts.IsNonAddressable) {
str = "" + parts.Addr + "";
} else {
str = parts.Addr;
}
str += ":";
colPos = AddSpacedString(sb, colPos, mColStart[(int)Col.Address],
str, parts.Addr.Length + 1);
}
}
if ((mLeftFlags & ActiveColumnFlags.Bytes) != 0) {
// Shorten the "...".
string bytesStr = parts.Bytes;
if (bytesStr != null) {
if (bytesStr.Length > bytesWidth) {
bytesStr = bytesStr.Substring(0, bytesWidth) + "+";
}
colPos = AddSpacedString(sb, colPos, mColStart[(int)Col.Bytes],
bytesStr, bytesStr.Length);
}
}
if ((mLeftFlags & ActiveColumnFlags.Flags) != 0) {
colPos = AddSpacedString(sb, colPos, mColStart[(int)Col.Flags],
parts.Flags, parts.Flags.Length);
}
if ((mLeftFlags & ActiveColumnFlags.Attr) != 0) {
colPos = AddSpacedString(sb, colPos, mColStart[(int)Col.Attr],
TextUtil.EscapeHTML(parts.Attr), parts.Attr.Length);
}
// remaining columns are mandatory, but may be empty
if (suppressLabel) {
// label on previous line
} else if (anchorLabel != null) {
colPos = AddSpacedString(sb, colPos, mColStart[(int)Col.Label],
anchorLabel, parts.Label.Length);
} else if (parts.Label != null) {
colPos = AddSpacedString(sb, colPos, mColStart[(int)Col.Label],
parts.Label, parts.Label.Length);
}
colPos = AddSpacedString(sb, colPos, mColStart[(int)Col.Opcode],
parts.Opcode, parts.Opcode.Length);
if (linkOperand != null) {
colPos = AddSpacedString(sb, colPos, mColStart[(int)Col.Operand],
linkOperand, parts.Operand.Length);
} else {
colPos = AddSpacedString(sb, colPos, mColStart[(int)Col.Operand],
TextUtil.EscapeHTML(parts.Operand), parts.Operand.Length);
}
if (parts.Comment != null) {
colPos = AddSpacedString(sb, colPos, mColStart[(int)Col.Comment],
TextUtil.EscapeHTML(parts.Comment), parts.Comment.Length);
}
break;
case LineListGen.Line.Type.LongComment:
case LineListGen.Line.Type.Note:
// Notes have a background color. Use this to highlight the text. We
// don't apply it to the padding on the left columns.
int rgb = 0;
if (parts.HasBackgroundColor) {
SolidColorBrush b = parts.BackgroundBrush as SolidColorBrush;
if (b != null) {
rgb = (b.Color.R << 16) | (b.Color.G << 8) | (b.Color.B);
}
}
string cstr;
if (rgb != 0) {
cstr = string.Format("{1}",
rgb, TextUtil.EscapeHTML(parts.Comment));
} else {
cstr = TextUtil.EscapeHTML(parts.Comment);
}
colPos = AddSpacedString(sb, colPos, mColStart[(int)Col.Label], cstr,
parts.Comment.Length);
break;
case LineListGen.Line.Type.VisualizationSet:
if (!GenerateImageFiles) {
// generate nothing at all
return;
}
while (colPos < mColStart[(int)Col.Label]) {
sb.Append(' ');
colPos++;
}
OutputVisualizationSet(line.FileOffset, sb);
break;
case LineListGen.Line.Type.Blank:
break;
default:
Debug.Assert(false);
break;
}
tw.WriteLine(sb);
}
///
/// Generate one or more GIF image files, and output references to them.
///
/// Visualization set file offset.
/// String builder for the HTML output.
private void OutputVisualizationSet(int offset, StringBuilder sb) {
const int IMAGE_SIZE = 64;
const int MAX_WIDTH_PER_LINE = 768;
if (!mProject.VisualizationSets.TryGetValue(offset,
out VisualizationSet visSet)) {
sb.Append("Internal error - visualization set missing");
Debug.Assert(false);
return;
}
if (visSet.Count == 0) {
sb.Append("Internal error - empty visualization set");
Debug.Assert(false);
return;
}
string imageDirFileName = Path.GetFileName(mImageDirPath);
int outputWidth = 0;
for (int index = 0; index < visSet.Count; index++) {
string fileName = "vis" + offset.ToString("x6") + "_" + index.ToString("d2");
int dispWidth, dispHeight;
Visualization vis = visSet[index];
if (vis is VisBitmapAnimation) {
// Animated visualization.
VisBitmapAnimation visAnim = (VisBitmapAnimation)vis;
int frameDelay = PluginCommon.Util.GetFromObjDict(visAnim.VisGenParams,
VisBitmapAnimation.P_FRAME_DELAY_MSEC_PARAM, 330);
AnimatedGifEncoder encoder = new AnimatedGifEncoder();
// Gather list of frames.
for (int i = 0; i < visAnim.Count; i++) {
Visualization avis = VisualizationSet.FindVisualizationBySerial(
mProject.VisualizationSets, visAnim[i]);
if (avis != null) {
encoder.AddFrame(BitmapFrame.Create(avis.CachedImage), frameDelay);
} else {
Debug.Assert(false); // not expected
}
}
#if false
// try feeding the animated GIF into our GIF unpacker
using (MemoryStream ms = new MemoryStream()) {
encoder.Save(ms, out dispWidth, out dispHeight);
Debug.WriteLine("TESTING");
UnpackedGif anim = UnpackedGif.Create(ms.GetBuffer());
anim.DebugDump();
}
#endif
// Create new or replace existing image file.
fileName += "_ani.gif";
string pathName = Path.Combine(mImageDirPath, fileName);
try {
using (FileStream stream = new FileStream(pathName, FileMode.Create)) {
encoder.Save(stream, out dispWidth, out dispHeight);
}
} catch (Exception ex) {
// TODO: add an error report
Debug.WriteLine("Error creating animated GIF file '" + pathName +
"': " + ex.Message);
dispWidth = dispHeight = 1;
}
} else if (vis is VisWireframeAnimation) {
AnimatedGifEncoder encoder = new AnimatedGifEncoder();
((VisWireframeAnimation)vis).EncodeGif(encoder, IMAGE_SIZE);
// Create new or replace existing image file.
fileName += "_ani.gif";
string pathName = Path.Combine(mImageDirPath, fileName);
try {
using (FileStream stream = new FileStream(pathName, FileMode.Create)) {
encoder.Save(stream, out dispWidth, out dispHeight);
}
} catch (Exception ex) {
// TODO: add an error report
Debug.WriteLine("Error creating animated WF GIF file '" + pathName +
"': " + ex.Message);
dispWidth = dispHeight = 1;
}
} else {
// Bitmap visualization -or- non-animated wireframe visualization.
//
// Encode a GIF the same size as the original bitmap. For a wireframe
// visualization this means the bitmap will be the same size as the
// generated thumbnail.
GifBitmapEncoder encoder = new GifBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(vis.CachedImage));
// Create new or replace existing image file.
fileName += ".gif";
string pathName = Path.Combine(mImageDirPath, fileName);
try {
using (FileStream stream = new FileStream(pathName, FileMode.Create)) {
encoder.Save(stream);
}
} catch (Exception ex) {
// Something went wrong with file creation. We don't have an error
// reporting mechanism, so this will just appear as a broken or stale
// image reference.
// TODO: add an error report
Debug.WriteLine("Error creating GIF file '" + pathName + "': " +
ex.Message);
}
dispWidth = (int)vis.CachedImage.Width;
dispHeight = (int)vis.CachedImage.Height;
}
// Output thumbnail-size IMG element, preserving proportions. I'm assuming
// images will be small enough that generating a separate thumbnail would be
// counter-productive. This seems to look best if the height is consistent
// across all visualization lines, but that can create some monsters (e.g.
// a bitmap that's 1 pixel high and 40 wide), so we cap the width.
int dimMult = IMAGE_SIZE;
double maxDim = dispHeight;
if (dispWidth > dispHeight * 2) {
// Too proportionally wide, so use the width as the limit. Allow it to
// up to 2x the max width (which can't cause the thumb height to exceed
// the height limit).
maxDim = dispWidth;
dimMult *= 2;
}
int thumbWidth = (int)Math.Round(dimMult * (dispWidth / maxDim));
int thumbHeight = (int)Math.Round(dimMult * (dispHeight / maxDim));
//Debug.WriteLine(dispWidth + "x" + dispHeight + " --> " +
// thumbWidth + "x" + thumbHeight + " (" + maxDim + ")");
if (outputWidth > MAX_WIDTH_PER_LINE) {
// Add a line break. In "pre" mode the bitmaps just run off the right
// edge of the screen. The way we're doing it is imprecise and doesn't
// flow with changes to the browser width, but it'll do for now.
sb.AppendLine("
");
for (int i = 0; i < mColStart[(int)Col.Label]; i++) {
sb.Append(' ');
}
outputWidth = 0;
} else if (index != 0) {
sb.Append(" ");
}
outputWidth += thumbWidth;
sb.Append("");
}
}
///
/// Appends a string to the string buffer. If the number of characters in the buffer
/// is less than the desired start position, spaces will be added. At least one space
/// will always be added if the start position is greater than zero and the string
/// is non-empty.
///
///
/// This is useful for things like linkified HTML, where we want to pad out the
/// string with spaces based on the text that will be presented to the user, rather
/// than the text that has HTML markup and other goodies.
///
/// Line being constructed.
/// Line position on entry.
/// Desired starting position.
/// String to append.
/// Length of string we're pretending to add.
/// Updated line position.
private int AddSpacedString(StringBuilder sb, int initialPosn, int colStart, string str,
int virtualLength) {
if (string.IsNullOrEmpty(str)) {
return initialPosn;
}
int toAdd = colStart - initialPosn;
if (toAdd < 1 && colStart > 0) {
// Already some text present, and we're adding more text, but we're past the
// column start. Add a space so the columns don't run into each other.
toAdd = 1;
}
int newPosn = initialPosn;
while (toAdd-- > 0) {
sb.Append(' ');
newPosn++;
}
sb.Append(str);
return newPosn + virtualLength;
}
///
/// Wraps the symbolic part of the operand with HTML link notation. If the operand
/// doesn't have a linkable symbol, this return null.
///
///
/// We're playing games with string substitution that feel a little flimsy, but this
/// is much simpler than reformatting the operand from scratch.
///
/// Display line index.
private string GetLinkOperand(int index, string operand) {
LineListGen.Line line = mCodeLineList[index];
if (line.FileOffset < 0) {
// EQU directive - shouldn't be here
Debug.Assert(false);
return null;
}
// Check for a format descriptor with a symbol.
Debug.Assert(line.LineType == LineListGen.Line.Type.Code ||
line.LineType == LineListGen.Line.Type.Data);
Anattrib attr = mProject.GetAnattrib(line.FileOffset);
if (attr.DataDescriptor == null || !attr.DataDescriptor.HasSymbol) {
return null;
}
// Symbol refs are weak. If the symbol doesn't exist, the value will be
// formatted in hex. We can't simply check to see if the formatted operand
// contains the symbol, because we could false-positive on the use of symbols
// whose label is a valid hex value, e.g. "ABCD = $ABCD".
//
// We also want to exclude references to local variables, since those aren't
// unique. To handle local refs we could just create anchors by line number or
// some other means of unique identification.
if (!mProject.SymbolTable.TryGetNonVariableValue(attr.DataDescriptor.SymbolRef.Label,
out Symbol sym)) {
return null;
}
string linkified = "" +
sym.Label + "";
return TextUtil.EscapeHTML(operand).Replace(sym.Label, linkified);
}
///
/// Generates a table of global/exported symbols. If none exist, a "no symbols found"
/// message is generated instead.
///
private string GenerateHtmlSymbolTable() {
StringBuilder sb = new StringBuilder();
int count = 0;
foreach (Symbol sym in mProject.SymbolTable) {
if (sym.SymbolType != Symbol.Type.GlobalAddrExport) {
continue;
}
if (count == 0) {
sb.Append("\r\n");
sb.Append(" Label Value ");
}
sb.Append(" ");
sb.Append("" +
sym.Label + " ");
sb.Append("");
if (sym.Value != Address.NON_ADDR) {
sb.Append(mFormatter.FormatHexValue(sym.Value, 2));
} else {
sb.Append(Address.NON_ADDR_STR);
}
sb.Append("
");
sb.Append(" \r\n");
count++;
}
if (count == 0) {
sb.AppendFormat("{0}
\r\n", Res.Strings.NO_EXPORTED_SYMBOLS_FOUND);
} else {
sb.Append("
\r\n");
}
return sb.ToString();
}
#endregion HTML
}
}