From d3ff1f6effb3daa8fec96b3dc1e77a94730b4948 Mon Sep 17 00:00:00 2001 From: Andy McFadden Date: Fri, 13 Sep 2019 17:09:06 -0700 Subject: [PATCH] Implement HTML export Pretty straightforward formatted-text dump, with links for internal labels, and a table of exported symbols at the end. --- SourceGen/DisasmProject.cs | 9 +- SourceGen/Exporter.cs | 298 +++++++++++++++++++++- SourceGen/MainController.cs | 5 + SourceGen/Res/Strings.xaml | 3 + SourceGen/Res/Strings.xaml.cs | 6 + SourceGen/RuntimeData/ExportTemplate.html | 29 +++ SourceGen/RuntimeData/SGStyle.css | 13 + SourceGen/WpfGui/Export.xaml | 6 +- 8 files changed, 359 insertions(+), 10 deletions(-) create mode 100644 SourceGen/RuntimeData/ExportTemplate.html create mode 100644 SourceGen/RuntimeData/SGStyle.css diff --git a/SourceGen/DisasmProject.cs b/SourceGen/DisasmProject.cs index e418f40..f265fc5 100644 --- a/SourceGen/DisasmProject.cs +++ b/SourceGen/DisasmProject.cs @@ -135,9 +135,8 @@ namespace SourceGen { /// public string ProjectPathName { get; set; } - // Name of data file. This is used for debugging and when naming DLLs for - // project-local extension scripts. Just the filename, not the path. - private string mDataFileName; + // Filename only of data file. This is used for debugging and text export. + public string DataFileName { get; private set; } // This holds working state for the code and data analyzers. Some of the state // is presented directly to the user, e.g. status flags. All of the data here @@ -258,7 +257,7 @@ namespace SourceGen { Debug.Assert(fileData.Length == FileDataLength); mFileData = fileData; - mDataFileName = dataFileName; + DataFileName = dataFileName; FileDataCrc32 = CommonUtil.CRC32.OnWholeBuffer(0, mFileData); #if DATA_PRESCAN ScanFileData(); @@ -343,7 +342,7 @@ namespace SourceGen { Debug.Assert(fileData.Length == FileDataLength); Debug.Assert(CRC32.OnWholeBuffer(0, fileData) == FileDataCrc32); mFileData = fileData; - mDataFileName = dataFileName; + DataFileName = dataFileName; FixAndValidate(ref report); diff --git a/SourceGen/Exporter.cs b/SourceGen/Exporter.cs index 06a7b93..a85019a 100644 --- a/SourceGen/Exporter.cs +++ b/SourceGen/Exporter.cs @@ -17,6 +17,8 @@ using System; using System.Diagnostics; using System.IO; using System.Text; +using System.Windows; +using System.Windows.Media; using Asm65; using CommonUtil; @@ -350,8 +352,302 @@ namespace SourceGen { } } + #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"; + public void OutputToHtml(string pathName, bool overwriteCss) { - Debug.WriteLine("HTML"); // TODO + 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; + } + + // Perform some quick substitutions. + tmplStr = tmplStr.Replace("$ProjectName$", mProject.DataFileName); + tmplStr = tmplStr.Replace("$AppVersion$", App.ProgramVersion.ToString()); + + // 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; + } + string template1 = tmplStr.Substring(0, splitPoint); + string template2 = tmplStr.Substring(splitPoint + CodeLinesStr.Length); + + + // Generate UTF-8 text, without a byte-order mark. + using (StreamWriter sw = new StreamWriter(pathName, false, new UTF8Encoding(false))) { + sw.Write(template1); + + // With the style "code { white-space: pre; }", leading spaces and EOL markers + // are preserved. + sw.Write(""); + StringBuilder sb = new StringBuilder(128); + for (int lineIndex = 0; lineIndex < mCodeLineList.Count; lineIndex++) { + if (Selection != null && !Selection[lineIndex]) { + continue; + } + + if (GenerateHtmlLine(lineIndex, sb)) { + sw.WriteLine(sb.ToString()); + //sw.WriteLine("
"); + } + sb.Clear(); + } + sw.WriteLine("
\r\n"); + + sw.Write(template2); + } + + 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; + } + } } + /// + /// Generates a line of HTML output. The line will not have EOL markers 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. + /// String builder to append text to. Must be cleared before + /// calling here. (This is a minor optimization.) + private bool GenerateHtmlLine(int index, StringBuilder sb) { + Debug.Assert(sb.Length == 0); + + // Width of "bytes" field, without '+' or trailing space. + int bytesWidth = mColEnd[(int)Col.Bytes] - mColEnd[(int)Col.Address] - 2; + + LineListGen.Line line = mCodeLineList[index]; + DisplayList.FormattedParts parts = mCodeLineList.GetFormattedParts(index); + + // TODO: linkify label and operand fields + + 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.OrgDirective: + case LineListGen.Line.Type.LocalVariableTable: + if (parts.IsLongComment) { + // This happens for long comments embedded in LV tables. + if (mColEnd[(int)Col.Attr] != 0) { + TextUtil.AppendPaddedString(sb, string.Empty, mColEnd[(int)Col.Attr]); + } + sb.Append(parts.Comment); + break; + } + + if ((mLeftFlags & ActiveColumnFlags.Offset) != 0) { + TextUtil.AppendPaddedString(sb, parts.Offset, + mColEnd[(int)Col.Offset]); + } + if ((mLeftFlags & ActiveColumnFlags.Address) != 0) { + string str; + if (!string.IsNullOrEmpty(parts.Addr)) { + str = parts.Addr + ":"; + } else { + str = string.Empty; + } + TextUtil.AppendPaddedString(sb, str, mColEnd[(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, mColEnd[(int)Col.Bytes]); + } + if ((mLeftFlags & ActiveColumnFlags.Flags) != 0) { + TextUtil.AppendPaddedString(sb, parts.Flags, mColEnd[(int)Col.Flags]); + } + if ((mLeftFlags & ActiveColumnFlags.Attr) != 0) { + TextUtil.AppendPaddedString(sb, parts.Attr, mColEnd[(int)Col.Attr]); + } + int labelOffset = sb.Length; + TextUtil.AppendPaddedString(sb, parts.Label, mColEnd[(int)Col.Label]); + TextUtil.AppendPaddedString(sb, parts.Opcode, mColEnd[(int)Col.Opcode]); + int operandOffset = sb.Length; + TextUtil.AppendPaddedString(sb, parts.Operand, mColEnd[(int)Col.Operand]); + if (string.IsNullOrEmpty(parts.Comment)) { + // Trim trailing spaces off opcode or operand. Would be more efficient + // to just not generate the spaces, but this is simpler and we're not + // in a hurry. + TextUtil.TrimEnd(sb); + } else { + sb.Append(parts.Comment); + } + + // Replace label with anchor label. We do it this late because we need the + // spacing to be properly set, and I don't feel like changing how all the + // AppendPaddedString code works. + if ((line.LineType == LineListGen.Line.Type.Code || + line.LineType == LineListGen.Line.Type.Data || + line.LineType == LineListGen.Line.Type.EquDirective) && + !string.IsNullOrEmpty(parts.Label)) { + string linkLabel = "" + parts.Label + ""; + sb.Remove(labelOffset, parts.Label.Length); + sb.Insert(labelOffset, linkLabel); + + // Adjust operand position. + operandOffset += linkLabel.Length - parts.Label.Length; + } + + if ((line.LineType == LineListGen.Line.Type.Code || + line.LineType == LineListGen.Line.Type.Data) && + parts.Operand.Length > 0) { + string linkOperand = GetLinkOperand(index, parts.Operand); + if (!string.IsNullOrEmpty(linkOperand)) { + sb.Remove(operandOffset, parts.Operand.Length); + sb.Insert(operandOffset, linkOperand); + } + } + break; + case LineListGen.Line.Type.LongComment: + case LineListGen.Line.Type.Note: + if (line.LineType == LineListGen.Line.Type.Note && !IncludeNotes) { + return false; + } + if (mColEnd[(int)Col.Attr] != 0) { + // Long comments aren't the left-most field, so pad it out. + TextUtil.AppendPaddedString(sb, string.Empty, mColEnd[(int)Col.Attr]); + } + + // 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); + } + } + if (rgb != 0) { + sb.AppendFormat("", rgb); + sb.Append(parts.Comment); + sb.Append(""); + } else { + sb.Append(parts.Comment); + } + break; + case LineListGen.Line.Type.Blank: + break; + default: + Debug.Assert(false); + break; + } + return true; + } + + /// + /// 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 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("\r\n"); + count++; + } + + if (count == 0) { + sb.AppendFormat("

{0}

\r\n", Res.Strings.NO_EXPORTED_SYMBOLS_FOUND); + } else { + sb.Append("
" + + sym.Label + "" + mFormatter.FormatHexValue(sym.Value, 2) + "
\r\n"); + } + + return sb.ToString(); + } + + #endregion HTML } } diff --git a/SourceGen/MainController.cs b/SourceGen/MainController.cs index 74dee5b..dc80163 100644 --- a/SourceGen/MainController.cs +++ b/SourceGen/MainController.cs @@ -1314,6 +1314,9 @@ namespace SourceGen { Exporter eport = new Exporter(mProject, CodeLineList, mOutputFormatter, colFlags, rightWidths); eport.Selection = selection; + + // Might want to set Mouse.OverrideCursor if the selection exceeds a few + // hundred thousand lines. eport.SelectionToString(true, out string fullText, out string csvText); DataObject dataObject = new DataObject(); @@ -2005,6 +2008,8 @@ namespace SourceGen { eport.Selection = selection; } + // This is generally fast enough that I don't feel the need to create a + // progress window. try { Mouse.OverrideCursor = Cursors.Wait; diff --git a/SourceGen/Res/Strings.xaml b/SourceGen/Res/Strings.xaml index ebd26e7..5e6198c 100644 --- a/SourceGen/Res/Strings.xaml +++ b/SourceGen/Res/Strings.xaml @@ -49,9 +49,11 @@ limitations under the License. Bad symbol reference part Type hint not recognized Removing duplicate label '{0}' (offset +{1:x6}) + Failed copying {0} to {1}: {2}. The file {0} exists, but is not a directory. File Error File not found: {0} + Failed reading {0}: {1}. Cannot write to read-only file {0}. Could not convert value to integer Key value is out of range @@ -91,6 +93,7 @@ limitations under the License. • Clear variables • Empty variable table no files available + No exported symbols found. The file doesn't exist. File is empty Unable to load data file diff --git a/SourceGen/Res/Strings.xaml.cs b/SourceGen/Res/Strings.xaml.cs index 36903ff..8213616 100644 --- a/SourceGen/Res/Strings.xaml.cs +++ b/SourceGen/Res/Strings.xaml.cs @@ -79,12 +79,16 @@ namespace SourceGen.Res { (string)Application.Current.FindResource("str_ErrBadTypeHint"); public static string ERR_DUPLICATE_LABEL_FMT = (string)Application.Current.FindResource("str_ErrDuplicateLabelFmt"); + public static string ERR_FILE_COPY_FAILED_FMT = + (string)Application.Current.FindResource("str_ErrFileCopyFailedFmt"); public static string ERR_FILE_EXISTS_NOT_DIR_FMT = (string)Application.Current.FindResource("str_ErrFileExistsNotDirFmt"); public static string ERR_FILE_GENERIC_CAPTION = (string)Application.Current.FindResource("str_ErrFileGenericCaption"); public static string ERR_FILE_NOT_FOUND_FMT = (string)Application.Current.FindResource("str_ErrFileNotFoundFmt"); + public static string ERR_FILE_READ_FAILED_FMT = + (string)Application.Current.FindResource("str_ErrFileReadFailedFmt"); public static string ERR_FILE_READ_ONLY_FMT = (string)Application.Current.FindResource("str_ErrFileReadOnlyFmt"); public static string ERR_INVALID_INT_VALUE = @@ -163,6 +167,8 @@ namespace SourceGen.Res { (string)Application.Current.FindResource("str_LocalVariableTableEmpty"); public static string NO_FILES_AVAILABLE = (string)Application.Current.FindResource("str_NoFilesAvailable"); + public static string NO_EXPORTED_SYMBOLS_FOUND = + (string)Application.Current.FindResource("str_NoExportedSymbolsFound"); public static string OPEN_DATA_DOESNT_EXIST = (string)Application.Current.FindResource("str_OpenDataDoesntExist"); public static string OPEN_DATA_EMPTY = diff --git a/SourceGen/RuntimeData/ExportTemplate.html b/SourceGen/RuntimeData/ExportTemplate.html new file mode 100644 index 0000000..7cf1edd --- /dev/null +++ b/SourceGen/RuntimeData/ExportTemplate.html @@ -0,0 +1,29 @@ + + + + + $ProjectName$ Disassembly + + + + + + + + + + +

$ProjectName$ Disassembly

+ +
+$CodeLines$ +
+ +

Symbol Table

+
+$SymbolTable$ +
+ + + + diff --git a/SourceGen/RuntimeData/SGStyle.css b/SourceGen/RuntimeData/SGStyle.css new file mode 100644 index 0000000..489ff5c --- /dev/null +++ b/SourceGen/RuntimeData/SGStyle.css @@ -0,0 +1,13 @@ +/* + * 6502bench SourceGen disassembly output style. + */ +body { + font-family: Arial, Helvetica, sans-serif; + font-size: 14px; /* 16 recommended for mobile */ + padding: 0px; + margin: 20px 10px 10px 10px; /* TRBL order */ +} +table, th, td { + border: 1px solid black; + border-collapse: collapse; +} diff --git a/SourceGen/WpfGui/Export.xaml b/SourceGen/WpfGui/Export.xaml index 6bc33ae..106f1e8 100644 --- a/SourceGen/WpfGui/Export.xaml +++ b/SourceGen/WpfGui/Export.xaml @@ -36,7 +36,8 @@ limitations under the License. - + + @@ -86,9 +87,6 @@ limitations under the License. - - -