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("