/// <summary> /// Wraps the symbolic part of the operand with HTML link notation. If the operand /// doesn't have a linkable symbol, this return null. /// </summary> /// <remarks> /// We're playing games with string substitution that feel a little flimsy, but this /// is much simpler than reformatting the operand from scratch. /// </remarks> /// <param name="index">Display line index.</param> 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 = "<a href=\"#" + LABEL_LINK_PREFIX + sym.Label + "\">" + sym.Label + "</a>"; return(TextUtil.EscapeHTML(operand).Replace(sym.Label, linkified)); }
/// <summary> /// 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. /// </summary> /// <remarks> /// 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. /// </remarks> /// <param name="index">Index of line to output.</param> /// <param name="tw">Text output destination.</param> /// <param name="sb">String builder to append text to. Must be cleared before /// calling here. (This is a minor optimization.)</param> 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 { anchorLabel = "<a name=\"" + LABEL_LINK_PREFIX + parts.Label + "\">" + parts.Label + "</a>"; } } // 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.OrgDirective: 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 = parts.Addr + ":"; colPos = AddSpacedString(sb, colPos, mColStart[(int)Col.Address], str, str.Length); } } 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("<span style=\"background-color: #{0:x6}\">{1}</span>", 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); }