/* * Copyright 2019 faddenSoft * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Text; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; using Asm65; using CommonUtil; using CommonWPF; namespace SourceGen { /// /// Source code export functions. /// /// /// The five columns on the left (offset, address, bytes, flags, attributes) are optional, /// and have a fixed width. The four columns on the right (label, opcode, operand, comment) /// are mandatory, and have configurable widths. /// public class Exporter { /// /// Optional selection specifier. If null, the entire file is included. /// public DisplayListSelection Selection { get; set; } /// /// Should notes be included in the output? /// public bool IncludeNotes { get; set; } /// /// Should image files be generated? /// public bool GenerateImageFiles { get; set; } /// /// If set, labels that are wider than the label column should go on their own line. /// public bool LongLabelNewLine { get; set; } /// /// Bit flags, used to indicate which of the optional columns are active. /// [FlagsAttribute] public enum ActiveColumnFlags { None = 0, Offset = 1, Address = 1 << 1, Bytes = 1 << 2, Flags = 1 << 3, Attr = 1 << 4, ALL = 0x7f } /// /// Flags indicating active optional columns. /// private ActiveColumnFlags mLeftFlags; /// /// Project reference. /// private DisasmProject mProject; /// /// List of formatted parts. /// private LineListGen mCodeLineList; /// /// Text formatter. /// private Formatter mFormatter; /// /// Directory path for image files. /// private string mImageDirPath; /// /// The cumulative width of the columns determines the start point. /// private int[] mColStart; private enum Col { Offset = 0, Address = 1, Bytes = 2, Flags = 3, Attr = 4, Label = 5, Opcode = 6, Operand = 7, Comment = 8, COUNT // number of elements, must be last } private string mParameterStringBase; /// /// Constructor. /// public Exporter(DisasmProject project, LineListGen codeLineList, Formatter formatter, ActiveColumnFlags leftFlags, int[] rightWidths) { mProject = project; mCodeLineList = codeLineList; mFormatter = formatter; mLeftFlags = leftFlags; ConfigureColumns(leftFlags, rightWidths); mParameterStringBase = GenerateParameterStringBase(leftFlags, rightWidths); } private void ConfigureColumns(ActiveColumnFlags leftFlags, int[] rightWidths) { mColStart = new int[(int)Col.COUNT]; int total = 0; int width; // mColStart[(int)Col.Offset] = 0 // offset "+123456" if ((leftFlags & ActiveColumnFlags.Offset) != 0) { total = mColStart[(int)Col.Offset + 1] = total + 7 + 1; } else { mColStart[(int)Col.Offset + 1] = total; } // address "1234:" or "12/4567:" if ((leftFlags & ActiveColumnFlags.Address) != 0) { width = mProject.CpuDef.HasAddr16 ? 5 : 8; total = mColStart[(int)Col.Address + 1] = total + width + 1; } else { mColStart[(int)Col.Address + 1] = total; } // bytes "12345678+" or "12 45 78 01+" if ((leftFlags & ActiveColumnFlags.Bytes) != 0) { // A limit of 8 gets us 4 bytes from dense display ("20edfd60") and 3 if spaces // are included ("20 ed fd") with no excess. We want to increase it to 11 so // we can always show 4 bytes. Add one for a trailing "+". width = mFormatter.Config.mSpacesBetweenBytes ? 12 : 9; total = mColStart[(int)Col.Bytes + 1] = total + width + 1; } else { mColStart[(int)Col.Bytes + 1] = total; } // flags "NVMXDIZC" or "NVMXDIZC E" if ((leftFlags & ActiveColumnFlags.Flags) != 0) { width = mProject.CpuDef.HasEmuFlag ? 10 : 8; total = mColStart[(int)Col.Flags + 1] = total + width + 1; } else { mColStart[(int)Col.Flags + 1] = total; } // attributes "@H!#>" if ((leftFlags & ActiveColumnFlags.Attr) != 0) { total = mColStart[(int)Col.Attr + 1] = total + 5 + 1; } else { mColStart[(int)Col.Attr + 1] = total; } total = mColStart[(int)Col.Label + 1] = total + rightWidths[0]; total = mColStart[(int)Col.Opcode + 1] = total + rightWidths[1]; total = mColStart[(int)Col.Operand + 1] = total + rightWidths[2]; //total = mColStart[(int)Col.Comment + 1] = total + rightWidths[3]; //Debug.WriteLine("Export col starts:"); //for (int i = 0; i < (int)Col.COUNT; i++) { // Debug.WriteLine(" " + i + "(" + ((Col)i) + ") " + mColStart[i]); //} } /// /// Generates description of some parameters that we only have during construction. /// private static string GenerateParameterStringBase(ActiveColumnFlags leftFlags, int[] rightWidths) { StringBuilder sb = new StringBuilder(); sb.Append("cols="); for (int i = 0; i < rightWidths.Length; i++) { if (i != 0) { sb.Append(','); } sb.Append(rightWidths[i]); } sb.Append(";extraCols="); bool first = true; foreach (ActiveColumnFlags flag in Enum.GetValues(typeof(ActiveColumnFlags))) { if (flag == ActiveColumnFlags.ALL) { continue; } if ((leftFlags & flag) != 0) { if (!first) { sb.Append(','); } sb.Append(flag); first = false; } } return sb.ToString(); } /// /// Generates a description of configured parameters. Intended to be human-readable, /// but possibly machine-readable as well. /// private string GenerateParameterString() { StringBuilder sb = new StringBuilder(mParameterStringBase); sb.Append(";byteSpc="); sb.Append(mFormatter.Config.mSpacesBetweenBytes.ToString()); sb.Append(";commaBulk="); sb.Append(mFormatter.Config.mCommaSeparatedDense.ToString()); sb.Append(";nonuPfx='"); sb.Append(mFormatter.Config.mNonUniqueLabelPrefix); sb.Append('\''); sb.Append(";varPfx='"); sb.Append(mFormatter.Config.mLocalVariableLabelPrefix); sb.Append('\''); sb.Append(";labelBrk="); sb.Append(LongLabelNewLine.ToString()); sb.Append(";notes="); sb.Append(IncludeNotes.ToString()); sb.Append(";gfx="); sb.Append(GenerateImageFiles.ToString()); sb.Append(";opWrap="); sb.Append(mFormatter.Config.mOperandWrapLen); // Not included: pseudo-op definitions; delimiter definitions return sb.ToString(); } /// /// Converts the selected lines to formatted text. /// /// Result; holds text of all selected lines. /// Result; holds text of all selected lines, in CSV format. public void SelectionToString(bool addCsv, out string fullText, out string csvText) { StringBuilder sb = new StringBuilder(128); StringWriter plainText = new StringWriter(); StringWriter csv = null; if (addCsv) { csv = new StringWriter(); } for (int lineIndex = 0; lineIndex < mCodeLineList.Count; lineIndex++) { if (!Selection[lineIndex]) { continue; } GenerateTextLine(lineIndex, plainText, sb); if (addCsv) { GenerateCsvLine(lineIndex, csv, sb); } } plainText.Close(); fullText = plainText.ToString(); if (addCsv) { csv.Close(); csvText = csv.ToString(); } else { csvText = null; } } /// /// Generates a full listing and writes it to the specified file. /// /// Full path to output file. /// Output as Comma Separated Values rather than plain text. public void OutputToText(string pathName, bool asCsv) { // Generate UTF-8 text. For plain text we omit the byte-order mark, for CSV // it appears to be meaningful (tested w/ very old version of Excel). using (StreamWriter sw = new StreamWriter(pathName, false, new UTF8Encoding(asCsv))) { StringBuilder sb = new StringBuilder(128); for (int lineIndex = 0; lineIndex < mCodeLineList.Count; lineIndex++) { if (Selection != null && !Selection[lineIndex]) { continue; } if (asCsv) { GenerateCsvLine(lineIndex, sw, sb); } else { GenerateTextLine(lineIndex, sw, sb); } } } } /// /// Generates text output for one display line. This may result in more than one line /// of output, e.g. if the label is longer than the field. EOL markers will be added. /// /// Index of line to output. /// Text output destination. /// Pre-allocated string builder (this is a minor optimization). private void GenerateTextLine(int index, TextWriter tw, StringBuilder sb) { LineListGen.Line line = mCodeLineList[index]; if (line.LineType == LineListGen.Line.Type.Note && !IncludeNotes) { return; } // 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); sb.Clear(); // 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 = string.IsNullOrEmpty(parts.Label) ? 0 : parts.Label.Length; if (labelLen > maxLabelLen) { // put on its own line TextUtil.AppendPaddedString(sb, parts.Label, mColStart[(int)Col.Label]); tw.WriteLine(sb); sb.Clear(); suppressLabel = true; } } 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 generated for LV tables (e.g. "empty // variable table"). TextUtil.AppendPaddedString(sb, parts.Comment, mColStart[(int)Col.Label]); break; } if ((mLeftFlags & ActiveColumnFlags.Offset) != 0) { TextUtil.AppendPaddedString(sb, parts.Offset, mColStart[(int)Col.Offset]); } if ((mLeftFlags & ActiveColumnFlags.Address) != 0) { if (!string.IsNullOrEmpty(parts.Addr)) { TextUtil.AppendPaddedString(sb, parts.Addr + ":", mColStart[(int)Col.Address]); } } if ((mLeftFlags & ActiveColumnFlags.Bytes) != 0) { // Shorten the "...". string bytesStr = parts.Bytes; if (bytesStr != null && bytesStr.Length > bytesWidth) { bytesStr = bytesStr.Substring(0, bytesWidth) + "+"; } TextUtil.AppendPaddedString(sb, bytesStr, mColStart[(int)Col.Bytes]); } if ((mLeftFlags & ActiveColumnFlags.Flags) != 0) { TextUtil.AppendPaddedString(sb, parts.Flags, mColStart[(int)Col.Flags]); } if ((mLeftFlags & ActiveColumnFlags.Attr) != 0) { TextUtil.AppendPaddedString(sb, parts.Attr, mColStart[(int)Col.Attr]); } if (!suppressLabel) { TextUtil.AppendPaddedString(sb, parts.Label, mColStart[(int)Col.Label]); } TextUtil.AppendPaddedString(sb, parts.Opcode, mColStart[(int)Col.Opcode]); TextUtil.AppendPaddedString(sb, parts.Operand, mColStart[(int)Col.Operand]); TextUtil.AppendPaddedString(sb, parts.Comment, mColStart[(int)Col.Comment]); break; case LineListGen.Line.Type.LongComment: case LineListGen.Line.Type.Note: TextUtil.AppendPaddedString(sb, parts.Comment, mColStart[(int)Col.Label]); break; case LineListGen.Line.Type.VisualizationSet: return; // show nothing case LineListGen.Line.Type.Blank: break; default: Debug.Assert(false); break; } tw.WriteLine(sb); } private void GenerateCsvLine(int index, TextWriter tw, StringBuilder sb) { LineListGen.Line line = mCodeLineList[index]; if (line.LineType == LineListGen.Line.Type.Note && !IncludeNotes) { return; } DisplayList.FormattedParts parts = mCodeLineList.GetFormattedParts(index); sb.Clear(); if ((mLeftFlags & ActiveColumnFlags.Offset) != 0) { sb.Append(TextUtil.EscapeCSV(parts.Offset)); sb.Append(','); } if ((mLeftFlags & ActiveColumnFlags.Address) != 0) { sb.Append(TextUtil.EscapeCSV(parts.Addr)); sb.Append(','); } if ((mLeftFlags & ActiveColumnFlags.Bytes) != 0) { sb.Append(TextUtil.EscapeCSV(parts.Bytes)); sb.Append(','); } if ((mLeftFlags & ActiveColumnFlags.Flags) != 0) { sb.Append(TextUtil.EscapeCSV(parts.Flags)); sb.Append(','); } if ((mLeftFlags & ActiveColumnFlags.Attr) != 0) { sb.Append(TextUtil.EscapeCSV(parts.Attr)); sb.Append(','); } if (parts.IsLongComment) { // put the comment in the Label column sb.Append(TextUtil.EscapeCSV(parts.Comment)); sb.Append(",,,"); } else { sb.Append(TextUtil.EscapeCSV(parts.Label)); sb.Append(','); sb.Append(TextUtil.EscapeCSV(parts.Opcode)); sb.Append(','); sb.Append(TextUtil.EscapeCSV(parts.Operand)); sb.Append(','); sb.Append(TextUtil.EscapeCSV(parts.Comment)); } tw.WriteLine(sb); } #region HTML private const string HTML_EXPORT_TEMPLATE = "ExportTemplate.html"; private const string HTML_EXPORT_CSS_FILE = "SGStyle.css"; private const string LABEL_LINK_PREFIX = "Sym"; private class ExportWorker : WorkProgress.IWorker { private Exporter mExporter; private string mPathName; private bool mOverwriteCss; public bool Success { get; private set; } public ExportWorker(Exporter exp, string pathName, bool overwriteCss) { mExporter = exp; mPathName = pathName; mOverwriteCss = overwriteCss; } public object DoWork(BackgroundWorker worker) { return mExporter.OutputToHtml(worker, mPathName, mOverwriteCss); } public void RunWorkerCompleted(object results) { if (results != null) { Success = (bool)results; } } } /// /// Generates HTML output to the specified path. /// /// Full pathname of output file (including ".html"). This /// defines the root directory if there are additional files. /// If set, existing CSS file will be replaced. public void OutputToHtml(Window parent, string pathName, bool overwriteCss) { ExportWorker ew = new ExportWorker(this, pathName, overwriteCss); WorkProgress dlg = new WorkProgress(parent, ew, false); if (dlg.ShowDialog() != true) { Debug.WriteLine("Export unsuccessful"); } else { Debug.WriteLine("Export complete"); } } private bool OutputToHtml(BackgroundWorker worker, string pathName, bool overwriteCss) { string exportTemplate = RuntimeDataAccess.GetPathName(HTML_EXPORT_TEMPLATE); string tmplStr; try { // exportTemplate will be null if Runtime access failed tmplStr = File.ReadAllText(exportTemplate); } catch (Exception ex) { string msg = string.Format(Res.Strings.ERR_FILE_READ_FAILED_FMT, pathName, ex.Message); MessageBox.Show(msg, Res.Strings.ERR_FILE_GENERIC_CAPTION, MessageBoxButton.OK, MessageBoxImage.Error); return false; } // We should only need the _IMG directory if there are visualizations. if (GenerateImageFiles && mProject.VisualizationSets.Count != 0) { string imageDirName = Path.GetFileNameWithoutExtension(pathName) + "_IMG"; string imageDirPath = Path.Combine(Path.GetDirectoryName(pathName), imageDirName); bool exists = false; try { FileAttributes attr = File.GetAttributes(imageDirPath); if ((attr & FileAttributes.Directory) != FileAttributes.Directory) { string msg = string.Format(Res.Strings.ERR_FILE_EXISTS_NOT_DIR_FMT, imageDirPath); MessageBox.Show(msg, Res.Strings.ERR_FILE_GENERIC_CAPTION, MessageBoxButton.OK, MessageBoxImage.Error); return false; } exists = true; } catch (FileNotFoundException) { } catch (DirectoryNotFoundException) { } if (!exists) { try { Directory.CreateDirectory(imageDirPath); } catch (Exception ex) { string msg = string.Format(Res.Strings.ERR_DIR_CREATE_FAILED_FMT, imageDirPath, ex.Message); MessageBox.Show(msg, Res.Strings.ERR_FILE_GENERIC_CAPTION, MessageBoxButton.OK, MessageBoxImage.Error); return false; } } // All good. mImageDirPath = imageDirPath; } if (mImageDirPath == null) { worker.ReportProgress(0, Res.Strings.EXPORTING_HTML); } else { worker.ReportProgress(0, Res.Strings.EXPORTING_HTML_AND_IMAGES); } // Perform some quick substitutions. This could be done more efficiently, // but we're only doing this on the template file, which should be small. tmplStr = tmplStr.Replace("$ProjectName$", mProject.DataFileName); tmplStr = tmplStr.Replace("$AppVersion$", App.ProgramVersion.ToString()); string expModeStr = AppSettings.Global.GetEnum(AppSettings.FMT_EXPRESSION_MODE, Formatter.FormatConfig.ExpressionMode.Unknown).ToString(); tmplStr = tmplStr.Replace("$ExpressionStyle$", expModeStr); string dateStr = DateTime.Now.ToString("yyyy/MM/dd"); string timeStr = DateTime.Now.ToString("HH:mm:ss zzz"); tmplStr = tmplStr.Replace("$CurrentDate$", dateStr); tmplStr = tmplStr.Replace("$CurrentTime$", timeStr); tmplStr = tmplStr.Replace("$GenParameters$", GenerateParameterString()); // Generate and substitute the symbol table. This should be small enough that // we won't break the world by doing it with string.Replace(). string symTabStr = GenerateHtmlSymbolTable(); tmplStr = tmplStr.Replace("$SymbolTable$", symTabStr); // For the main event we split the template in half, and generate the code lines // directly into the stream writer. const string CodeLinesStr = "$CodeLines$"; int splitPoint = tmplStr.IndexOf(CodeLinesStr); if (splitPoint < 0) { Debug.WriteLine("No place to put code"); return false; } string template1 = tmplStr.Substring(0, splitPoint); string template2 = tmplStr.Substring(splitPoint + CodeLinesStr.Length); int lastProgressPerc = 0; // Generate UTF-8 text, without a byte-order mark. using (StreamWriter sw = new StreamWriter(pathName, false, new UTF8Encoding(false))) { sw.Write(template1); //sw.Write(""); 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("\"vis\""); } } /// /// 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(" "); } sb.Append(" "); sb.Append(""); sb.Append(""); sb.Append("\r\n"); count++; } if (count == 0) { sb.AppendFormat("

{0}

\r\n", Res.Strings.NO_EXPORTED_SYMBOLS_FOUND); } else { sb.Append("
LabelValue
" + sym.Label + ""); if (sym.Value != Address.NON_ADDR) { sb.Append(mFormatter.FormatHexValue(sym.Value, 2)); } else { sb.Append(Address.NON_ADDR_STR); } sb.Append("
\r\n"); } return sb.ToString(); } #endregion HTML } }